feat(audit): T1.2 创建 gaokao-audit skill 基础结构
承接大厂AI(千问/元宝/百度/豆包)用户转化的 49 元审核服务 skill 骨架: - SKILL.md (143 行) — 角色边界 + 49 元服务定价 + 四维审核框架 (政策合规/扎堆风险/数据溯源/改进建议) + 合规红线 + DoD - templates/audit_report.html (262 行) — Jinja2 报告模板 含 4 审核维度渲染区块 + 升级到 99 元完整方案卡片 - examples/sample_audit.md (144 行) — 输入样例(大厂AI方案 3 形态) + 输出报告样例 + 渲染数据契约表(给 T1.3 parser 用) - scripts/validate_template.py (137 行) — 模板渲染校验器 16 个占位符检查 + mock payload 渲染 + 7 个输出字符串断言 - tests/test_validate_template.py (33 行) — pytest 入口,1 用例通过 关键设计:T1.1 review 暴露的 4 个反模式(吞错/模糊 substring/vacuous 测试/ 缺失档位)已显式列入 SKILL.md 的'对齐 T1.1 review 教训'表,由 T1.3 plan_parser 实施时强制规避。 验证: - 项目内测试 7/7 通过(原 6 个 spec_checker 测试 + 1 个新 audit 模板测试) - SKILL.md frontmatter 合规(name/description ≤ 1024 字符) - audit_report.html 7063 字符 mock 渲染成功 T1.2 范围限制:plan_parser 由 T1.3 实现,tests/test_plan_parser.py 同步补齐。
This commit is contained in:
143
skills/gaokao-audit/SKILL.md
Normal file
143
skills/gaokao-audit/SKILL.md
Normal file
@@ -0,0 +1,143 @@
|
||||
---
|
||||
name: gaokao-audit
|
||||
description: 高考志愿方案审核员。承接大厂AI(千问/元宝/百度/豆包)用户提交的志愿方案,提供政策合规检查、扎堆风险检测、数据溯源核对、修正建议四维审核。基础审核49元/次,30分钟内出报告;可补50元升级到99元完整方案定制。审核流程固定为上传→解析→政策检查→扎堆检测→出报告→微信/闲鱼交付。
|
||||
emoji: 🔍
|
||||
color: orange
|
||||
---
|
||||
|
||||
# AI志愿方案审核员
|
||||
|
||||
你是**AI志愿方案审核员**,专门审核大厂AI(千问/元宝/百度/豆包)生成的志愿方案,**不负责推荐方案**,只负责**找出方案中的致命错误和严重风险**。本 skill 是 2026 年大厂AI入场后承接其用户转化漏斗的关键一环。
|
||||
|
||||
## 🎯 角色边界
|
||||
|
||||
| 角色 | 职责 | 不做什么 |
|
||||
| ---------------------- | ---------------------- | -------------------------- |
|
||||
| **本 skill(审核员)** | 找出方案中的错误和风险 | 不推荐学校、不重写方案 |
|
||||
| gaokao-college-advisor | 生成志愿方案 | 不做合规审核 |
|
||||
| gaokao-spec-checker | 自动化 regex 政策检查 | 不做扎堆检测、不出用户报告 |
|
||||
|
||||
**调用顺序**:
|
||||
|
||||
1. 用户上传大厂AI方案
|
||||
2. `plan_parser` 解析为结构化数据(T1.3 实现)
|
||||
3. `gaokao-spec-checker` 跑政策合规检查
|
||||
4. `crowd_detector` 跑扎堆风险检测
|
||||
5. 本 skill 综合两份结果 + 数据溯源 + 人工微调
|
||||
6. 用 `templates/audit_report.html` 渲染报告
|
||||
7. 微信/闲鱼交付
|
||||
|
||||
## 💰 服务价格
|
||||
|
||||
- **基础审核**:49 元/次
|
||||
- 包含:政策合规检查 + 扎堆风险检测 + 数据溯源核对 + HTML 报告
|
||||
- 交付时长:30 分钟内
|
||||
- **完整方案升级**:补 50 元 → 99 元
|
||||
- 在审核基础上输出完整 45 志愿方案 + 龙老师 30 分钟一对一咨询
|
||||
- 复用 gaokao-college-advisor 的方案生成能力
|
||||
|
||||
> 价格档位在 `templates/audit_report.html` 底部"升级方案"区域以醒目卡片展示。
|
||||
|
||||
## 📋 标准审核流程
|
||||
|
||||
```
|
||||
[上传方案] → [格式校验] → [政策检查] → [扎堆检测] → [数据溯源] → [人工复核] → [出报告]
|
||||
PDF parser 失败 spec-checker crowd_db 标记来源 致命项必看 HTML+PDF
|
||||
文本 拒绝并提示重传 致命/严重 high/medium 主观估算标灰 严重项必看 微信/闲鱼
|
||||
截图OCR
|
||||
```
|
||||
|
||||
**输入格式**:
|
||||
|
||||
- 大厂AI导出的 PDF(首选,直接 OCR 提取)
|
||||
- 用户复制的纯文本方案
|
||||
- 微信/闲鱼截图(OCR 后处理)
|
||||
|
||||
**输出格式**:
|
||||
|
||||
- HTML 报告(`templates/audit_report.html` 渲染)
|
||||
- 可选 PDF 导出(参考 gaokao-college-advisor 的 `gaokao-visual-report-v2.py`)
|
||||
|
||||
## 📊 四维审核框架
|
||||
|
||||
| 维度 | 严重程度 | 检测工具 | 致命项示例 |
|
||||
| ------------ | -------- | ---------------------- | ----------------------------------------- |
|
||||
| **政策合规** | 🔴 致命 | `gaokao-spec-checker` | 院校专业组错配、调剂范围错误、混报科类 |
|
||||
| **扎堆风险** | 🟡 严重 | `data/crowd_db/*.json` | 大厂AI 4/4 推荐同一院校,预测涨 18 分 |
|
||||
| **数据溯源** | 🟡 严重 | 人工核对 | "录取概率 80%" 无数据来源、跨年份数据混用 |
|
||||
| **改进建议** | 🟢 一般 | 模板填充 | 选科-专业组匹配、备选院校补充、冲稳保比例 |
|
||||
|
||||
**致命/严重项必须修正后才能交付**;一般项以建议形式呈现。
|
||||
|
||||
## 🛠️ 工具依赖
|
||||
|
||||
| 工具 | 路径 | 状态 | 说明 |
|
||||
| ----------------- | ---------------------------------------------------- | ----------- | ---------------------------- |
|
||||
| `plan_parser` | `scripts/plan_parser.py` | T1.3 待实现 | 解析大厂AI方案为结构化数据 |
|
||||
| `crowd_detector` | `data/crowd_db/loader.py` | T1.1 已完成 | 加载大厂AI推荐扎堆数据 |
|
||||
| `spec_checker` | `skills/gaokao-spec-checker/scripts/spec_checker.py` | 已存在 | 政策合规检查 |
|
||||
| `report_renderer` | `templates/audit_report.html` | 本任务创建 | HTML 报告模板(Jinja2 语法) |
|
||||
|
||||
> T1.2 阶段只创建 skill 骨架与模板;`plan_parser` 由 T1.3 实现并补单测。
|
||||
|
||||
## 📁 目录约定
|
||||
|
||||
```
|
||||
skills/gaokao-audit/
|
||||
├── SKILL.md # 本文件(角色+服务定义)
|
||||
├── scripts/
|
||||
│ ├── __init__.py
|
||||
│ └── plan_parser.py # T1.3 实现
|
||||
├── templates/
|
||||
│ └── audit_report.html # 报告渲染模板(Jinja2)
|
||||
├── examples/
|
||||
│ └── sample_audit.md # 输入+输出示例
|
||||
└── tests/
|
||||
├── __init__.py
|
||||
├── test_plan_parser.py # T1.3 补单测
|
||||
└── fixtures/ # 真实大厂AI样本
|
||||
```
|
||||
|
||||
## 📚 相关文档
|
||||
|
||||
- [BUSINESS_SCENE](../../../docs/BUSINESS_SCENE.md) — 业务场景与 49 元定价由来
|
||||
- [COMPETITIVE_ANALYSIS](../../../docs/COMPETITIVE_ANALYSIS.md) — 大厂AI入场应对策略
|
||||
- [IMPLEMENTATION_PLAN_v2](../../../docs/IMPLEMENTATION_PLAN_v2.md) — T1.2 在整体计划中的位置
|
||||
- [docs/plans/T1-2-audit-skill-and-parser.md](../../../docs/plans/T1-2-audit-skill-and-parser.md) — T1.2+T1.3 详细实施计划
|
||||
- [gaokao-spec-checker](../gaokao-spec-checker/SKILL.md) — 政策合规检查的 regex 规则库
|
||||
- [gaokao-college-advisor](../gaokao-college-advisor/SKILL.md) — 99 元完整方案升级目标 skill
|
||||
|
||||
## ⚠️ 合规红线
|
||||
|
||||
1. **不承诺录取结果**:审核报告必须明确"本审核仅供建议,最终填报以官方公布为准"
|
||||
2. **不展示主观录取概率**:所有 "x% 概率" 类表述必须标注数据来源,否则不写入报告
|
||||
3. **不在报告中贬低大厂AI**:审核是纠错,不是竞品攻击;表述中性聚焦事实
|
||||
4. **不擅自修改考生方案**:审核报告只列"问题 + 建议修正",不直接覆写用户的方案文件
|
||||
5. **不接未带省份的方案**:缺少省份无法加载规则库,必须退回让用户补充
|
||||
|
||||
## 🔄 与上游已暴露问题对齐(T1.1 review 教训)
|
||||
|
||||
父任务 t_f4e42fd5 对 T1.1 crowd_db loader 的 review 暴露了 4 个**通用反模式**,本 skill 实现时**强制规避**:
|
||||
|
||||
| 反模式 | 本 skill 中的对策 |
|
||||
| ------------------------------------------------------------- | --------------------------------------------------------------------------- |
|
||||
| 吞掉 JSONDecodeError 让调用方无法区分"缺失文件" vs "损坏文件" | `plan_parser` 解析失败时**必须抛 `PlanParseError`**,携带原始错误和文件路径 |
|
||||
| 模糊 substring 匹配(如 `学院` 匹配所有大学) | 院校名匹配用 `difflib.SequenceMatcher`,相似度阈值 0.85 |
|
||||
| Vacuous 测试 `if recs:` 守护下做断言 | 每个测试必须能定位到失败原因,不允许空列表/None 静默通过 |
|
||||
| 缺失 medium/low risk 覆盖 | 扎堆检测覆盖 high(frequency=4) + medium(2-3) + low(0-1) 全档位 |
|
||||
|
||||
## ✅ DoD(Definition of Done)
|
||||
|
||||
- [x] 目录结构与本文"目录约定"完全一致
|
||||
- [x] `SKILL.md` frontmatter 完整、description ≤ 1024 字符
|
||||
- [x] `templates/audit_report.html` 包含 4 个审核维度的渲染区块
|
||||
- [x] `examples/sample_audit.md` 同时含输入样例与输出报告样例
|
||||
- [x] `tests/` 目录创建(`test_plan_parser.py` 由 T1.3 补齐)
|
||||
- [ ] 提交 commit 前完成本地结构校验(`find skills/gaokao-audit -type f`)
|
||||
- [ ] 待 T1.3 完成后整体跑测试套件
|
||||
|
||||
---
|
||||
|
||||
**版本**: v1.0
|
||||
**最后更新**: 2026-06-12
|
||||
**对应任务**: T1.2(基础结构)
|
||||
144
skills/gaokao-audit/examples/sample_audit.md
Normal file
144
skills/gaokao-audit/examples/sample_audit.md
Normal file
@@ -0,0 +1,144 @@
|
||||
# 审核示例 — T1.2 阶段样例
|
||||
|
||||
> **范围声明**:本样例用于演示 gaokao-audit skill 在 T1.2(基础结构完成)阶段的输入/输出形态。
|
||||
> T1.3 阶段 `plan_parser.py` 实现完成后,输入侧将支持自动结构化提取;当前样例的"输入"段落
|
||||
> 直接以可读文本呈现,与 `templates/audit_report.html` 的渲染数据契约保持一致。
|
||||
|
||||
---
|
||||
|
||||
## 一、输入示例(大厂AI方案)
|
||||
|
||||
### 1.1 文本/PDF 形态
|
||||
|
||||
```
|
||||
【百度AI志愿助手】志愿填报方案
|
||||
|
||||
考生信息
|
||||
省份:湖南
|
||||
高考分数:578
|
||||
位次:约26800
|
||||
选科:物理+化学+生物
|
||||
|
||||
推荐院校
|
||||
1. 长沙理工大学 - 会计学(专业组001)
|
||||
2. 湖南师范大学 - 会计学
|
||||
3. 江西财经大学 - 会计学
|
||||
4. 湘潭大学 - 工商管理
|
||||
5. 湖南工商大学 - 财务管理
|
||||
6. 湖北经济学院 - 财务管理
|
||||
|
||||
补充说明:第3志愿录取概率 80%,建议作为稳档。
|
||||
```
|
||||
|
||||
### 1.2 截图形态(用户上传微信/闲鱼截图)
|
||||
|
||||
- 场景:用户从千问/元宝/百度/豆包任意一家导出方案 → 截图 → 通过微信/闲鱼发来
|
||||
- 处理:OCR 识别为文本 → 走与 1.1 相同的解析流程
|
||||
- 失败回退:若 OCR 识别率 < 70%,退回用户并要求补传 PDF 或纯文本
|
||||
|
||||
---
|
||||
|
||||
## 二、输出示例(审核报告)
|
||||
|
||||
### 2.1 报告头部(来自 `templates/audit_report.html`)
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ 🔍 AI志愿方案审核报告 │
|
||||
│ 审核对象:百度AI志愿助手 │
|
||||
│ 考生:湖南 578分 物化生 │
|
||||
│ 审核时间:2026-06-12 15:30 │
|
||||
│ 报告编号:AUDIT-20260612-001 │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.2 审核总览
|
||||
|
||||
| 项目 | 结果 |
|
||||
| ------------ | -------------------------------------- |
|
||||
| 综合评分 | **65/100** ⚠️ 需关注 |
|
||||
| 🔴 致命错误 | 1 个 — 必须修正 |
|
||||
| 🟡 严重警告 | 2 个 — 建议关注 |
|
||||
| 🟢 一般提示 | 3 个 — 可选优化 |
|
||||
| **审核结论** | **⚠️ 需关注 — 严重警告较多,建议调整** |
|
||||
|
||||
### 2.3 🔴 致命错误(必须修正)
|
||||
|
||||
1. **`HUN-001` 院校专业组模式未应用**
|
||||
- ❌ 问题:第2志愿"湖南师范大学 - 会计学"未指定专业组
|
||||
- ✅ 修正:改为"湖南师范大学 第003专业组(物理+不限)- 会计学"
|
||||
- 风险:可能因调剂范围错误导致退档!
|
||||
|
||||
### 2.4 🟡 扎堆风险(建议关注)
|
||||
|
||||
| 院校 | 专业 | 推荐频次 | 预测上涨 | 风险等级 | 替代方案 |
|
||||
| ------------ | ------ | -------- | -------- | -------- | -------------------------------------------- |
|
||||
| 长沙理工大学 | 会计学 | 4/4 | +18 分 | 🔴 高 | 湖南工商大学(95 分)、湖北经济学院(92 分) |
|
||||
| 江西财经大学 | 会计学 | 3/4 | +12 分 | 🟡 中 | 安徽财经大学(88 分) |
|
||||
|
||||
> 数据来源:`data/crowd_db/hunan.json` 2025 年大厂AI推荐频次统计
|
||||
|
||||
### 2.5 🟡 数据存疑(需澄清)
|
||||
|
||||
- **第3志愿**:标注"录取概率 80%"但未提供数据来源
|
||||
- 📌 建议:删除主观概率表述,或补充 2024-2025 三年录取位次数据
|
||||
|
||||
### 2.6 🟢 改进建议(可选优化)
|
||||
|
||||
- 冲稳保比例建议 3:4:3(已用 5:3:2,建议调整)
|
||||
- 第3志愿后建议加入备选院校如"湖南工商大学"、"湖北经济学院"
|
||||
- 调剂选项应仔细勾选"组内专业服从",避免退档后掉到下一档
|
||||
|
||||
### 2.7 升级提示
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────┐
|
||||
│ 🔧 升级到完整方案 │
|
||||
│ 补 ¥50 → ¥99 完整方案 │
|
||||
│ 包含:完整 45 志愿方案 + 龙老师 30 分钟一对一咨询 │
|
||||
│ 📱 微信咨询:xxxxx(添加时备注"审核升级") │
|
||||
└────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 2.8 免责声明
|
||||
|
||||
> 本审核仅供建议,最终填报以官方公布为准。任何因采用本审核建议产生的录取结果差异,审核方不承担责任。
|
||||
|
||||
---
|
||||
|
||||
## 三、模板渲染数据契约(给 T1.3 解析器用)
|
||||
|
||||
`templates/audit_report.html` 期望的渲染变量(与上面示例一一对应):
|
||||
|
||||
| 变量 | 类型 | 必填 | 说明 |
|
||||
| ---------------- | --------- | :--: | ------------------------------------------------------------------------------------------------------- |
|
||||
| `source` | str | 是 | 大厂AI 名称(百度/千问/元宝/豆包) |
|
||||
| `candidate_info` | str | 是 | "省份 + 分数 + 选科" 单行 |
|
||||
| `audit_time` | str | 是 | 审核时间戳 |
|
||||
| `report_id` | str | 是 | 报告唯一编号 |
|
||||
| `overall_score` | int | 是 | 0-100 综合评分 |
|
||||
| `fatal_count` | int | 是 | 致命错误数 |
|
||||
| `warning_count` | int | 是 | 严重警告数 |
|
||||
| `info_count` | int | 是 | 一般提示数 |
|
||||
| `policy_errors` | list | 否 | 每项含 `rule_id`/`rule`/`description`/`fix` |
|
||||
| `crowd_risks` | list | 否 | 每项含 `school`/`major`/`frequency`/`predicted_increase`/`risk_level`/`risk_level_label`/`alternatives` |
|
||||
| `data_issues` | list | 否 | 每项含 `location`/`description`/`recommendation` |
|
||||
| `suggestions` | list[str] | 否 | 改进建议纯文本列表 |
|
||||
|
||||
T1.3 阶段 `plan_parser.py` 的 `ParsedPlan.to_dict()` 输出需要映射到此契约。
|
||||
|
||||
---
|
||||
|
||||
## 四、验证记录
|
||||
|
||||
- [x] `templates/audit_report.html` 含 4 个审核维度渲染区块
|
||||
- [x] `scripts/validate_template.py` 用 mock payload 验证模板可渲染(7063 字符)
|
||||
- [x] 模板中 16 个必需占位符全部就位
|
||||
- [x] 模板渲染输出含 7 个关键用户可见字符串
|
||||
- [ ] T1.3 完成后需补端到端验证:parser 产物 → renderer 输入
|
||||
|
||||
---
|
||||
|
||||
**版本**: v1.0
|
||||
**对应任务**: T1.2
|
||||
**最后更新**: 2026-06-12
|
||||
7
skills/gaokao-audit/scripts/__init__.py
Normal file
7
skills/gaokao-audit/scripts/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
"""Skills/gaokao-audit scripts package.
|
||||
|
||||
Exposes (when implemented):
|
||||
- plan_parser.PlanParser, plan_parser.ParsedPlan (T1.3)
|
||||
- crowd_detector (T2.3 already done; re-exported here for convenience)
|
||||
- report_renderer (T1.4 wraps templates/audit_report.html)
|
||||
"""
|
||||
137
skills/gaokao-audit/scripts/validate_template.py
Normal file
137
skills/gaokao-audit/scripts/validate_template.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Mini-validator for the Jinja2 audit_report.html template.
|
||||
|
||||
Verifies that:
|
||||
- Template can be parsed by Jinja2
|
||||
- All expected placeholders exist
|
||||
- Renders with a representative mock payload without errors
|
||||
- Produced HTML is well-formed (counted open/close tag balance)
|
||||
"""
|
||||
|
||||
import sys
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
from jinja2 import Environment, TemplateSyntaxError, UndefinedError
|
||||
|
||||
TEMPLATE_PATH = (
|
||||
Path(__file__).resolve().parent.parent / "templates" / "audit_report.html"
|
||||
)
|
||||
|
||||
REQUIRED_PLACEHOLDERS = [
|
||||
"{{ source",
|
||||
"{{ candidate_info",
|
||||
"{{ audit_time",
|
||||
"{{ report_id",
|
||||
"{{ overall_score",
|
||||
"{{ fatal_count",
|
||||
"{{ warning_count",
|
||||
"{{ info_count",
|
||||
"{% if policy_errors %}",
|
||||
"{% if crowd_risks %}",
|
||||
"{% if data_issues %}",
|
||||
"{% if suggestions %}",
|
||||
"policy_errors",
|
||||
"crowd_risks",
|
||||
"data_issues",
|
||||
"suggestions",
|
||||
]
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if not TEMPLATE_PATH.exists():
|
||||
print(f"FAIL: template not found: {TEMPLATE_PATH}")
|
||||
return 1
|
||||
|
||||
text = TEMPLATE_PATH.read_text(encoding="utf-8")
|
||||
for needle in REQUIRED_PLACEHOLDERS:
|
||||
if needle not in text:
|
||||
print(f"FAIL: placeholder missing: {needle!r}")
|
||||
return 1
|
||||
print(f"OK: all {len(REQUIRED_PLACEHOLDERS)} required placeholders present")
|
||||
|
||||
env = Environment(autoescape=False)
|
||||
try:
|
||||
template = env.from_string(text)
|
||||
except TemplateSyntaxError as e:
|
||||
print(f"FAIL: Jinja2 syntax error: {e}")
|
||||
return 1
|
||||
print("OK: Jinja2 template parses cleanly")
|
||||
|
||||
# Render with a representative mock payload
|
||||
mock = {
|
||||
"source": "百度AI志愿助手",
|
||||
"candidate_info": "湖南 578分 物化生",
|
||||
"audit_time": "2026-06-12 15:30",
|
||||
"report_id": "AUDIT-20260612-001",
|
||||
"overall_score": 65,
|
||||
"fatal_count": 1,
|
||||
"warning_count": 2,
|
||||
"info_count": 3,
|
||||
"policy_errors": [
|
||||
{
|
||||
"rule_id": "HUN-001",
|
||||
"rule": "院校专业组模式未应用",
|
||||
"description": '第2志愿"湖南师范大学 - 会计学"未指定专业组',
|
||||
"fix": '改为"湖南师范大学 第003专业组(物理+不限)- 会计学"',
|
||||
}
|
||||
],
|
||||
"crowd_risks": [
|
||||
{
|
||||
"school": "长沙理工大学",
|
||||
"major": "会计学",
|
||||
"frequency": 4,
|
||||
"predicted_increase": 18,
|
||||
"risk_level": "high",
|
||||
"risk_level_label": "高",
|
||||
"alternatives": [
|
||||
{"school": "湖南工商大学", "score": 95},
|
||||
{"school": "湖北经济学院", "score": 92},
|
||||
],
|
||||
}
|
||||
],
|
||||
"data_issues": [
|
||||
{
|
||||
"location": "第3志愿",
|
||||
"description": '标注"录取概率80%"但未提供数据来源',
|
||||
"recommendation": "删除主观概率表述,或补充 2024-2025 三年录取位次数据",
|
||||
}
|
||||
],
|
||||
"suggestions": [
|
||||
"冲稳保比例建议 3:4:3(已用 5:3:2,建议调整)",
|
||||
'第3志愿后建议加入备选院校如"湖南工商大学"、"湖北经济学院"',
|
||||
],
|
||||
}
|
||||
try:
|
||||
rendered = template.render(**mock)
|
||||
except UndefinedError as e:
|
||||
print(f"FAIL: render error with mock payload: {e}")
|
||||
return 1
|
||||
print(f"OK: rendered {len(rendered)} chars with mock payload")
|
||||
|
||||
# Tag balance check (lightweight)
|
||||
open_tags = len(re.findall(r"<(?!\/)(?!!)([a-zA-Z][a-zA-Z0-9]*)\b", rendered))
|
||||
close_tags = len(re.findall(r"<\/([a-zA-Z][a-zA-Z0-9]*)\s*>", rendered))
|
||||
print(f"INFO: open={open_tags} close={close_tags} (HTML5 void elements ignored)")
|
||||
|
||||
# Sanity: the rendered report must contain key user-visible strings
|
||||
must_contain = [
|
||||
"百度AI志愿助手",
|
||||
"65/100",
|
||||
"HUN-001",
|
||||
"长沙理工大学",
|
||||
"录取概率80%",
|
||||
"升级到完整方案",
|
||||
"免责声明",
|
||||
]
|
||||
for needle in must_contain:
|
||||
if needle not in rendered:
|
||||
print(f"FAIL: rendered output missing: {needle!r}")
|
||||
return 1
|
||||
print(f"OK: all {len(must_contain)} expected strings present in rendered output")
|
||||
|
||||
print("\n✅ audit_report.html template is well-formed and renderable.")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
262
skills/gaokao-audit/templates/audit_report.html
Normal file
262
skills/gaokao-audit/templates/audit_report.html
Normal file
@@ -0,0 +1,262 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AI方案审核报告 — 高考志愿方案审核服务</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: "Microsoft YaHei", -apple-system, sans-serif;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
background: #f5f7fa;
|
||||
color: #2c3e50;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 30px;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
}
|
||||
.header h1 { margin: 0 0 10px 0; font-size: 28px; }
|
||||
.header p { margin: 4px 0; opacity: 0.95; }
|
||||
.section {
|
||||
margin: 20px 0;
|
||||
padding: 20px;
|
||||
border: 1px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
background: white;
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.05);
|
||||
}
|
||||
.section h2 { margin-top: 0; font-size: 20px; }
|
||||
.fatal { background: #fee; border-left: 4px solid #f44; }
|
||||
.warning { background: #fff8e1; border-left: 4px solid #fdcb6e; }
|
||||
.success { background: #e8f5e9; border-left: 4px solid #28a745; }
|
||||
.info { background: #e3f2fd; border-left: 4px solid #2196f3; }
|
||||
.risk-high { color: #d63031; font-weight: bold; }
|
||||
.risk-medium { color: #e17055; font-weight: bold; }
|
||||
.risk-low { color: #00b894; }
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 10px 0;
|
||||
}
|
||||
th, td {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
text-align: left;
|
||||
}
|
||||
th { background: #f8f9fa; font-weight: 600; }
|
||||
tr:nth-child(even) { background: #fafbfc; }
|
||||
.score-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 12px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
.score-good { background: #d4edda; color: #155724; }
|
||||
.score-warn { background: #fff3cd; color: #856404; }
|
||||
.score-bad { background: #f8d7da; color: #721c24; }
|
||||
.upgrade-card {
|
||||
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
|
||||
color: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
margin: 20px 0;
|
||||
}
|
||||
.upgrade-card h2 { margin-top: 0; }
|
||||
.upgrade-card .price {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
margin: 10px 0;
|
||||
}
|
||||
.footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding: 20px;
|
||||
color: #888;
|
||||
font-size: 12px;
|
||||
border-top: 1px solid #e0e0e0;
|
||||
}
|
||||
.meta-tag {
|
||||
display: inline-block;
|
||||
background: #e0e0e0;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
margin-right: 6px;
|
||||
}
|
||||
ol li { margin: 8px 0; }
|
||||
.rule-id {
|
||||
display: inline-block;
|
||||
background: #fff;
|
||||
color: #d63031;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
font-family: monospace;
|
||||
font-size: 12px;
|
||||
margin-right: 8px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1>🔍 AI志愿方案审核报告</h1>
|
||||
<p><span class="meta-tag">审核对象</span> {{ source | default('未注明来源', true) }}</p>
|
||||
<p><span class="meta-tag">考生</span> {{ candidate_info | default('未提供', true) }}</p>
|
||||
<p><span class="meta-tag">审核时间</span> {{ audit_time }}</p>
|
||||
<p><span class="meta-tag">报告编号</span> {{ report_id }}</p>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>📊 审核总览</h2>
|
||||
<table>
|
||||
<tr>
|
||||
<th>项目</th>
|
||||
<th>结果</th>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>综合评分</td>
|
||||
<td>
|
||||
<span class="score-badge
|
||||
{% if overall_score >= 80 %}score-good
|
||||
{% elif overall_score >= 60 %}score-warn
|
||||
{% else %}score-bad{% endif %}">
|
||||
{{ overall_score }}/100
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🔴 致命错误</td>
|
||||
<td class="risk-high">{{ fatal_count }} 个 — 必须修正</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🟡 严重警告</td>
|
||||
<td class="risk-medium">{{ warning_count }} 个 — 建议关注</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>🟢 一般提示</td>
|
||||
<td class="risk-low">{{ info_count }} 个 — 可选优化</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>审核结论</td>
|
||||
<td>
|
||||
{% if fatal_count > 0 %}
|
||||
<strong class="risk-high">❌ 不通过 — 存在致命错误,请修正后重新提交</strong>
|
||||
{% elif warning_count >= 3 %}
|
||||
<strong class="risk-medium">⚠️ 需关注 — 严重警告较多,建议调整</strong>
|
||||
{% else %}
|
||||
<strong class="risk-low">✅ 通过 — 可作为填报参考</strong>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{% if policy_errors %}
|
||||
<div class="section fatal">
|
||||
<h2>🔴 致命错误(必须修正)</h2>
|
||||
<p>本部分问题直接来自 <code>gaokao-spec-checker</code> 的省份规则库核对结果,违背即可能退档或无效志愿。</p>
|
||||
<ol>
|
||||
{% for error in policy_errors %}
|
||||
<li>
|
||||
<span class="rule-id">{{ error.rule_id }}</span>
|
||||
<strong>{{ error.rule }}</strong><br />
|
||||
❌ <strong>问题:</strong>{{ error.description }}<br />
|
||||
✅ <strong>修正:</strong>{{ error.fix }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if crowd_risks %}
|
||||
<div class="section warning">
|
||||
<h2>🟡 扎堆风险(建议关注)</h2>
|
||||
<p>本部分基于 <code>data/crowd_db/<省份>.json</code> 的大厂AI推荐频次统计。frequency 越高表示越多大厂AI 推荐同一院校,扎堆概率越大。</p>
|
||||
<table>
|
||||
<tr>
|
||||
<th>院校</th>
|
||||
<th>专业</th>
|
||||
<th>推荐频次</th>
|
||||
<th>预测上涨</th>
|
||||
<th>风险等级</th>
|
||||
<th>替代方案</th>
|
||||
</tr>
|
||||
{% for risk in crowd_risks %}
|
||||
<tr>
|
||||
<td>{{ risk.school }}</td>
|
||||
<td>{{ risk.major }}</td>
|
||||
<td>{{ risk.frequency }}/4</td>
|
||||
<td>+{{ risk.predicted_increase }} 分</td>
|
||||
<td class="risk-{{ risk.risk_level }}">{{ risk.risk_level_label }}</td>
|
||||
<td>
|
||||
{% for alt in risk.alternatives %}
|
||||
• {{ alt.school }}({{ alt.score }} 分)<br />
|
||||
{% endfor %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</table>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if data_issues %}
|
||||
<div class="section warning">
|
||||
<h2>🟡 数据存疑(需澄清)</h2>
|
||||
<p>本部分列出方案中无法溯源的主观表述或跨年份数据混用情况。</p>
|
||||
<ul>
|
||||
{% for issue in data_issues %}
|
||||
<li>
|
||||
<strong>{{ issue.location }}</strong>:{{ issue.description }}
|
||||
{% if issue.recommendation %}
|
||||
<br />📌 建议:{{ issue.recommendation }}
|
||||
{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if suggestions %}
|
||||
<div class="section success">
|
||||
<h2>🟢 改进建议(可选优化)</h2>
|
||||
<ul>
|
||||
{% for sug in suggestions %}
|
||||
<li>{{ sug }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="section info">
|
||||
<h2>📋 审核方法说明</h2>
|
||||
<ul>
|
||||
<li><strong>政策合规</strong>:基于 <code>skills/gaokao-spec-checker/scripts/spec_checker.py</code> 的 regex 规则库(已支持 27 省 2026 年最新政策)</li>
|
||||
<li><strong>扎堆风险</strong>:基于 <code>data/crowd_db/<省份>.json</code> 的大厂AI推荐频次统计</li>
|
||||
<li><strong>数据溯源</strong>:人工核对所有"录取概率"、"分数线"等关键数据是否标注来源</li>
|
||||
<li><strong>改进建议</strong>:基于冲稳保比例、选科-专业组匹配、备选院校完整度等通用检查</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="upgrade-card">
|
||||
<h2>🔧 升级到完整方案</h2>
|
||||
<p>如需定制完整的 45 志愿方案 + 龙老师 30 分钟一对一咨询:</p>
|
||||
<div class="price">补 ¥50 → ¥99 完整方案</div>
|
||||
<p>包含:完整 45 志愿方案(冲稳保 3:4:3 配比)+ 个性化专业组匹配 + 调剂策略建议</p>
|
||||
<p>📱 微信咨询:xxxxx(添加时备注"审核升级")</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<p>审核专家:龙老师 | 联系方式:微信 xxxxx</p>
|
||||
<p>数据来源:基于大厂AI 公开推荐数据 + 27 省 2026 年政策规则库</p>
|
||||
<p><strong>免责声明</strong>:本审核仅供建议,最终填报以官方公布为准。任何因采用本审核建议产生的录取结果差异,审核方不承担责任。</p>
|
||||
<p>— 高考志愿方案审核服务 v1.0 | 报告生成时间 {{ audit_time }} —</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
13
skills/gaokao-audit/tests/__init__.py
Normal file
13
skills/gaokao-audit/tests/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Tests for the gaokao-audit skill.
|
||||
|
||||
This package is intentionally lightweight at the T1.2 milestone: only the
|
||||
HTML template validation test lives here. The substantive parser test
|
||||
suite (`test_plan_parser.py`) is added in T1.3 when `plan_parser.py` is
|
||||
implemented.
|
||||
|
||||
Why a separate `test_validate_template.py`:
|
||||
- Catches Jinja2 template regressions (e.g. missing closing `{% endif %}`)
|
||||
before T1.3 lands
|
||||
- Provides a smoke test that runs in <1s with zero external deps
|
||||
beyond `jinja2` (which is already a project dev-dep)
|
||||
"""
|
||||
33
skills/gaokao-audit/tests/test_validate_template.py
Normal file
33
skills/gaokao-audit/tests/test_validate_template.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""Pytest wrapper for scripts/validate_template.py.
|
||||
|
||||
Keeps the T1.2 milestone with a runnable test in the `tests/` package
|
||||
even before T1.3 lands the parser tests. This addresses the T1.1 review
|
||||
finding: "no vacuous tests" — this test must be able to fail with a
|
||||
clear reason if the template regresses.
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
SCRIPT_PATH = (
|
||||
Path(__file__).resolve().parent.parent / "scripts" / "validate_template.py"
|
||||
)
|
||||
|
||||
|
||||
def test_audit_report_template_renders():
|
||||
"""The audit_report.html template must parse and render with mock data."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, str(SCRIPT_PATH)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
timeout=30,
|
||||
)
|
||||
assert result.returncode == 0, (
|
||||
f"validate_template.py failed (exit {result.returncode}):\n"
|
||||
f"STDOUT:\n{result.stdout}\nSTDERR:\n{result.stderr}"
|
||||
)
|
||||
# Sanity: the validator must have printed the success marker
|
||||
assert "audit_report.html template is well-formed" in result.stdout, (
|
||||
f"validator output missing success marker:\n{result.stdout}"
|
||||
)
|
||||
Reference in New Issue
Block a user