diff --git a/.gitignore b/.gitignore index 59519db0..64c459a5 100644 --- a/.gitignore +++ b/.gitignore @@ -24,3 +24,5 @@ skills/5writing/templates/**/missfont.log # LaTeX 模板编译生成的 PDF(保留 huaweibei 的 logo.pdf 和 title.pdf 模板资源) skills/5writing/templates/**/main.pdf +__pycache__/ +*.py[cod] diff --git a/README.md b/README.md index 8a9eef88..c4adcc89 100644 --- a/README.md +++ b/README.md @@ -96,11 +96,14 @@ MathModelAgent SKILL —— 直接在 Harness 中驱动的数学建模自动化 完全开源免费,可接入任何模型。 **🧠 端到端自动化** -从问题分析、建模、编码、绘图到论文排版和验收,一条 `/1start-mathmodel` 命令全自动完成,中间阶段自动串联,无需人工干预。 +从问题分析、建模、编码、绘图到论文排版和验收,一条 `/1start-mathmodel` 命令自动串联;国赛任务还会根据全过程证据自动生成 AI 使用详情。 **📄 17 套 Typst 论文模板** 内置中英文主流赛事模板(国赛、华数杯、华为杯、MCM/ICM 等),自动匹配赛事类型,生成排版精良、可直接提交的 PDF 论文。 +**🧾 国赛 AI 使用详情** +国赛任务从开始阶段持续记录真实 AI 使用情况;完整论文验收后自动归纳采纳、迭代修正与实际核验,并隐藏核心建模细节,同时生成 `paper/main.pdf` 和名称准确的 `supporting_materials/AI 工具使用详情.pdf`,缺少任一文件都不会报告最终完成。 + **📐 内置建模知识库** 包含完整的建模规范、模型选择决策树(AHP、TOPSIS、ARIMA、GA 等)、常见易错模式和 MCM/ICM 评分标准,每个阶段自动参考,降低模型幻觉。 @@ -148,6 +151,7 @@ codex: $start-mathmodel 完成这个数学建模任务 ``` /doctor: 检查环境配置 /typst-author: typst 知识 +/7ai-disclosure: 持续记录 AI 使用并生成“AI 工具使用详情.pdf” ``` diff --git a/README_EN.md b/README_EN.md index c4afa62c..213d150f 100644 --- a/README_EN.md +++ b/README_EN.md @@ -26,6 +26,8 @@ The desktop app bundles Claude Code and the full set of MathModelAgent SKILLS. No Python / Node.js / Redis installation and no manual SKILL setup — install it, add one model API key, and start modeling. +For CUMCM tasks, the workflow records actual AI use throughout the run, automatically summarizes evidence-backed adoption, revisions, and checks without exposing core modeling details, and requires both `paper/main.pdf` and `supporting_materials/AI 工具使用详情.pdf` before reporting final completion. + | OS | File | |----|------| | macOS (Apple Silicon) | `mathmodel--arm64.dmg` | @@ -62,6 +64,7 @@ Automatically generate an award-level modeling paper - Local Interpreter: Based on Jupyter, code saved as notebook for easy editing - Cloud Code Interpreter: [E2B](https://e2b.dev/) and [daytona](https://app.daytona.io/) - 📝 Generate a well-formatted paper +- 🧾 Generate the required CUMCM AI-usage disclosure PDF from confirmed run records - 🤝 Multi-agents: modeling expert, coding expert, paper expert, etc. - 🔄 Multi-LLMs: Different models for each agent - 🤖 Support for all models: [litellm](https://docs.litellm.ai/docs/providers) diff --git a/docs/superpowers/specs/2026-08-24-ai-disclosure-table-polish-design.md b/docs/superpowers/specs/2026-08-24-ai-disclosure-table-polish-design.md new file mode 100644 index 00000000..d9b84ba5 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-ai-disclosure-table-polish-design.md @@ -0,0 +1,49 @@ +# AI 工具使用详情表格化与版式美化设计 + +日期:2026-08-24 + +范围:`skills/7ai-disclosure` 的 LaTeX/PDF 模板及必要的 Skill 输出说明 + +## 目标 + +在不改变四部分内容范围的前提下,将第二部分“具体使用目的和环节”改为表格,并采用克制的学术蓝灰风格提升信息层次和评审阅读效率。 + +## 第二部分表格 + +第二部分使用三列表格: + +| 工具编号 | 使用环节 | 具体使用目的 | +| --- | --- | --- | +| TOOL-01 | 示例环节 | 示例目的 | + +- 每行表示一条使用记录。 +- 工具编号必须引用第一部分已有的 `TOOL-xx`。 +- 表格允许添加多行,并支持记录较多时自然分页。 +- 不改变日志字段和校验器的数据要求。 + +## 学术蓝灰风格 + +- 主色采用深蓝灰,浅蓝灰用于章节标题背景和表头。 +- 四个一级部分使用统一的浅色标题条和左侧强调线。 +- 第一、第二部分表格采用一致的表头、字号、行距和横线。 +- 第三、第四部分保留原有正文结构,只统一记录标题、字体颜色、段落间距和留白。 +- 页眉、页码和文档标题继续保留,但颜色和细线与主色协调。 +- 避免大面积深色色块,保证黑白打印仍可辨识。 + +## 变更边界 + +- 不修改四个一级部分的标题和顺序。 +- 不修改第一部分三列工具表。 +- 不修改第三部分典型交互的非表格形式。 +- 不修改第四部分的采纳、人工修改和核验字段。 +- 不修改日志模板或校验器逻辑。 +- 仅在 `SKILL.md` 中补充第二部分表格约束。 + +## 验收 + +- LaTeX 编译无错误、无溢出警告。 +- PDF 保持单页 A4;如实际记录增加,允许自然分页。 +- 第二部分包含“工具编号、使用环节、具体使用目的”三列表头。 +- 四个一级部分、`TOOL-01` 和中文文本层均可提取。 +- 逐页视觉检查无裁切、重叠、表格越界、乱码或异常留白。 +- Skill 结构校验和现有日志校验器自测继续通过。 diff --git a/docs/superpowers/specs/2026-08-24-ai-usage-disclosure-design.md b/docs/superpowers/specs/2026-08-24-ai-usage-disclosure-design.md new file mode 100644 index 00000000..6578e996 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-ai-usage-disclosure-design.md @@ -0,0 +1,188 @@ +# AI 工具使用详情记录与生成 Skill 设计 + +## 1. 目标 + +为全国大学生数学建模竞赛工作流增加 `7ai-disclosure` Skill,在比赛全过程按阶段记录 AI 工具使用情况,并在论文验收后生成支撑材料: + +- `supporting_materials/AI 工具使用详情.tex` +- `supporting_materials/AI 工具使用详情.pdf` + +本设计依据用户提供的 `2026国赛AI工具使用规定.pdf`(SHA-256:`4CF6F30CDD37D6EF2CDB3439C5DBA4D9F207C12D6AAFE24419E81F3C69ACF59A`)。当前仅核对了文件内容,未另行核验其公开发布来源。 + +## 2. 合规边界 + +生成材料必须覆盖规定列出的四类信息: + +1. AI 工具名称、版本或型号; +2. 具体使用目的和环节; +3. 主要提示方式与使用过程,可附典型交互示例; +4. AI 输出的采纳、人工修改和核验情况,语言润色可按规定简化。 + +Skill 只负责记录、整理、校验和排版,不得: + +- 编造工具、模型版本、提示、人工修改或核验过程; +- 将“生成了 PDF”等同于“符合竞赛规定”; +- 代替参赛队确认核心建模与分析是否由队伍主导; +- 保存 API Key、访问令牌、完整思维链或无必要的个人敏感信息; +- 把未经队员确认的内容标记为已核验。 + +## 3. 方案选择 + +采用单一 Skill、双模式设计: + +- `record`:每个工作流阶段结束后,追加一条阶段级记录; +- `finalize`:检查记录完整性,生成 XeLaTeX 源文件和最终 PDF。 + +相较于拆成两个 Skill,该方案减少安装和调用成本;相较于赛后一次性补填,该方案能保留更可靠的过程证据。 + +## 4. 文件结构 + +```text +skills/7ai-disclosure/ +├── SKILL.md +├── templates/ +│ ├── AI_USAGE_LOG.md +│ └── AI工具使用详情模板.tex +└── scripts/ + └── validate_ai_usage_log.py +``` + +运行时项目产物: + +```text +reports/AI_USAGE_LOG.md +supporting_materials/AI 工具使用详情.tex +supporting_materials/AI 工具使用详情.pdf +``` + +## 5. 记录数据模型 + +### 5.1 基本信息 + +- 竞赛名称; +- 赛题编号或名称; +- 参赛队号; +- 记录起止时间; +- 最终材料生成时间。 + +### 5.2 工具目录 + +每个工具使用稳定编号,例如 `TOOL-01`,字段包括: + +- 工具名称; +- 提供方; +- 版本、型号或模型名称; +- 使用入口或方式,例如网页、桌面应用、API、IDE 插件; +- 信息来源及确认状态。 + +### 5.3 阶段记录 + +每条记录使用稳定编号,例如 `AI-001`,字段包括: + +- 使用阶段; +- 关联工具编号; +- 使用目的; +- 主要提示方式或代表性提示摘要; +- 使用过程摘要; +- AI 输出采纳情况; +- 人工修改内容; +- 人工核验方法与结果; +- 对应证据文件或产物; +- 负责确认的队员; +- 当前状态:`已确认` 或 `待队员确认`。 + +记录粒度按竞赛阶段汇总。只有在用途、工具、模型或核验方式发生实质变化时,才拆分为多条记录。 + +## 6. 工作流集成 + +### 6.1 初始化 + +`1start-mathmodel` 在生成 `plan.md` 和 `todo.md` 时,从模板创建 `reports/AI_USAGE_LOG.md`,并把 AI 使用记录列入工作流产物。 + +### 6.2 过程记录 + +总控工作流在以下阶段结束后调用 `7ai-disclosure record`: + +1. 赛题分析与建模设计; +2. 编程实现和图表生成; +3. 流程与架构图绘制; +4. 竞赛论文撰写; +5. 验证和验收。 + +记录模式先从本阶段报告和实际会话中整理草稿,再让队员确认无法从证据判断的字段。不得仅凭文件修改时间推断使用目的或人工核验情况。 + +### 6.3 最终生成 + +完成 `6verity` 后执行 `7ai-disclosure finalize`: + +1. 运行日志校验脚本; +2. 汇总重复工具和同类阶段记录; +3. 对未确认字段停止最终验收并请求队员补充; +4. 复制并填充 XeLaTeX 模板; +5. 使用 XeLaTeX 编译两遍; +6. 对 PDF 做页数、文本层、文件名和视觉检查; +7. 将结果写入 `reports/VERIFY_REPORT.md` 的支撑材料检查项。 + +## 7. PDF 模板结构 + +PDF 使用 A4 中文版式,正文以表格和连续说明结合: + +1. 标题与基本信息; +2. 填写说明与责任边界; +3. AI 工具清单; +4. 分阶段使用详情; +5. 典型交互示例; +6. 人工修改与核验汇总; +7. 未采纳的重要 AI 建议或结果; +8. 参赛队最终确认栏。 + +模板使用 `ctexart`、`geometry`、`longtable`、`booktabs`、`array`、`xcolor`、`fancyhdr` 和 `hyperref`。长记录必须允许跨页,表格不得依赖手工分页。 + +## 8. 缺失信息与异常处理 + +- 缺少必填字段:写为 `待队员确认`,校验失败,不生成可提交状态; +- 工具版本无法从证据确认:保留“无法确认”,不得猜测; +- 没有找到 AI 使用记录:提示队员确认是否确实未使用,不自动生成空白详情 PDF; +- XeLaTeX 不可用:保留 `.tex`,将 PDF 状态标记为未生成; +- 编译失败:保留日志并报告首个有效错误,不用删除内容规避错误; +- PDF 出现乱码、越界、重叠或异常分页:修复模板后重新编译和检查; +- 最终文件名不精确:重命名为 `AI 工具使用详情.pdf` 后重新验收。 + +## 9. 校验脚本 + +`validate_ai_usage_log.py` 使用 Python 标准库完成确定性检查: + +- 必需章节存在; +- 工具编号和记录编号唯一; +- 每条阶段记录引用的工具存在; +- 规定要求的字段均非空; +- 不存在未解决的 `待队员确认`; +- 不出现常见密钥格式; +- 至少存在一条人工修改或人工核验说明; +- 输出清晰的错误列表和非零退出码。 + +脚本只检查结构完整性,不判断叙述是否真实,也不替代队员确认。 + +## 10. 验证与验收 + +实施完成后至少执行: + +1. 合格日志样例通过校验; +2. 缺字段、未知工具编号、重复编号、待确认状态和疑似密钥样例均被拒绝; +3. XeLaTeX 连续编译两遍成功; +4. 生成 PDF 文件名完全匹配规定; +5. 使用 PDF 工具确认文件非空、页面为 A4、有中文文本层; +6. 将每页渲染为 PNG,检查中文字体、表格跨页、页码、边距、重叠和裁切; +7. 检查 Git 工作区仅包含预期文件; +8. 确认现有数学建模阶段顺序未被破坏。 + +## 11. PR 范围 + +首个 PR 只包含: + +- 新增 `7ai-disclosure` Skill、模板和校验脚本; +- 在 `1start-mathmodel` 中加入初始化、阶段记录和最终生成步骤; +- 在 `6verity` 中加入支撑材料验收项; +- 更新必要的 Skill 使用说明。 + +不在本 PR 中实现自动抓取完整聊天记录、云端上传、用户账号体系或比赛规则在线更新。 diff --git a/docs/superpowers/specs/2026-08-24-ai-usage-disclosure-simplification-design.md b/docs/superpowers/specs/2026-08-24-ai-usage-disclosure-simplification-design.md new file mode 100644 index 00000000..7b86bd10 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-ai-usage-disclosure-simplification-design.md @@ -0,0 +1,98 @@ +# AI 工具使用详情简化设计 + +日期:2026-08-24 + +范围:`skills/7ai-disclosure` 及其在主工作流中的输出约定 + +## 目标 + +将最终提交的 `AI 工具使用详情.pdf` 精简为与赛事规定直接对应的四个部分,使评审能够快速识别所用工具、使用环节、提示过程以及人工采纳和核验情况。赛中记录和提交前校验仍应阻止虚构、待确认内容和敏感信息进入最终材料。 + +## 最终 PDF 结构 + +PDF 只保留文档标题、页眉页码和以下四个一级部分,不再增加基本信息、责任说明、分阶段记录、未采纳建议、最终确认或提交前检查等章节。 + +### 一、所用 AI 工具名称、版本或型号 + +使用简洁三列表格: + +| 编号 | AI 工具名称 | 版本或型号 | +| --- | --- | --- | +| TOOL-01 | 示例工具 | 示例版本或型号 | + +编号按 `TOOL-01`、`TOOL-02` 依次排列,供后续三个部分引用。不显示提供方、使用入口或方式,也不要求参赛队在最终材料中填写这些字段。 + +### 二、具体使用目的和环节 + +按工具或关键任务使用短条目说明: + +- 使用环节; +- 具体目的; +- 对应工具编号。 + +多个相近的小任务可以合并,避免逐轮罗列;每个条目必须标明对应的 `TOOL-xx`。 + +### 三、主要提示方式与使用过程说明 + +采用短段落说明提示组织方式、输入材料、约束条件、主要迭代和队员参与过程。每个过程说明标注所用 `TOOL-xx`。典型交互为可选内容,使用非表格形式: + +- 代表性提示摘要; +- AI 输出摘要; +- 队员如何处理。 + +不得复制完整聊天记录或模型私有思维链。 + +### 四、对 AI 输出的采纳、人工修改和核验的主要情况 + +按关键任务概括并标注对应的 `TOOL-xx`: + +- 完全采纳、部分采纳或未采纳的范围; +- 队员所作实质修改; +- 复算、测试、交叉验证或文献核查等核验方法与结果。 + +语言润色可以说明用途,但不强制展开人工修改和核验细节。 + +## Skill 与日志调整 + +`7ai-disclosure` 仍保留 `record` 和 `finalize` 两种模式,但记录结构直接映射到最终四部分。 + +- 工具目录只要求工具编号、工具名称和版本或型号。 +- 使用记录保留环节、目的、提示方式、过程、采纳、人工修改、人工核验和证据文件。 +- 典型交互为可选的非表格信息。 +- 删除日志中的基本信息、提供方、使用入口、未采纳建议独立章节和全局最终确认章节。 +- 每条使用记录保留“当前状态”,以区分“待队员确认”和“已确认”。 + +内部日志可以保留证据文件和确认状态,但这些控制字段不进入最终 PDF。 + +## 校验规则 + +`validate_ai_usage_log.py` 应: + +- 校验四个核心信息类别对应字段齐全; +- 校验工具编号唯一,并确保第二至第四部分引用的 `TOOL-xx` 均存在; +- 在 `finalize` 模式拒绝“待队员确认”、空值和占位符; +- 要求每条记录状态为“已确认”; +- 检测 API 密钥、访问令牌等疑似敏感信息; +- 不再要求基本信息、提供方、使用入口或全局最终确认字段。 + +## 生成与版式 + +- 最终源文件仍为 `supporting_materials/AI 工具使用详情.tex`。 +- 最终文件名仍为 `supporting_materials/AI 工具使用详情.pdf`。 +- 使用 XeLaTeX 编译两遍。 +- 模板采用 A4 中文排版,优先控制在 1–2 页;实际记录较多时允许自然分页。 +- 工具目录可以使用表格,典型交互不得使用表格。 + +## 验收 + +- Skill 结构校验通过。 +- 日志校验器自测通过,简化后的空白日志在 `record` 模式通过结构检查,在 `finalize` 模式因待确认内容而失败。 +- LaTeX 编译无错误和版式溢出警告。 +- PDF 非空、页面为 A4、中文文本层可提取。 +- 逐页视觉检查无裁切、重叠、表格越界、乱码或异常分页。 + +## 不在本次范围内 + +- 不修改论文正文中的“AI 工具使用声明”格式。 +- 不自动认定参赛队满足全部赛事合规要求。 +- 不保存完整聊天记录或自动推断队员已经完成核验。 diff --git a/docs/superpowers/specs/2026-08-24-automatic-ai-disclosure-design.md b/docs/superpowers/specs/2026-08-24-automatic-ai-disclosure-design.md new file mode 100644 index 00000000..a828a8ba --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-automatic-ai-disclosure-design.md @@ -0,0 +1,98 @@ +# 国赛 AI 使用详情全自动生成设计 + +本设计取代 `2026-08-24-desktop-ai-disclosure-integration-design.md` 中有关提交前人工确认的要求;其他已确认的四部分结构、双 PDF 门禁和版式要求继续有效。 + +## 目标 + +在桌面 Agent 识别到全国大学生数学建模竞赛任务后,全程自动维护 AI 使用记录,并在论文完成时同时生成: + +- `paper/main.pdf` +- `supporting_materials/AI 工具使用详情.pdf` + +流程不再向队员集中询问工具版本、采纳、修改或核验情况。详情 PDF 保持现有四部分结构和简洁版式。 + +## 方案选择 + +本次考虑三种方案: + +1. 自动补写符合常见参赛情形的人工经历。该方案会产生无法核实的竞赛申报内容,不采用。 +2. 保留提交前人工确认。真实性最强,但不满足无人值守生成要求,不采用。 +3. 根据任务证据自动归纳,并对核心建模内容脱敏。该方案兼顾自动化与可核查性,采用。 + +## 真实性边界 + +自动填写只能使用本次任务中可观察的证据,包括会话、运行时模型元数据、文件产物、代码执行结果、测试记录和验收报告。 + +- 不编造队员亲自重写、复算、查阅文献或讨论等人工经历。 +- 不把 Agent 的自动修改、程序测试或审稿过程表述为人工行为。 +- 无法确认的模型版本写为“运行环境未提供具体版本”,不得猜测。 +- 没有可核实人工修改时,最终 PDF 直接省略人工修改句,不输出“未记录到额外人工修改”或同义提示。 +- 采纳情况根据内容是否进入最终产物以及后续是否被替换、删除或修正来归纳。 +- 核验情况仅写实际执行成功并留有结果的检查,不把计划中的检查写成已经完成。 + +## 核心建模信息保护 + +AI 使用详情只说明工具使用方式,不复述参赛作品的核心建模过程。自动归纳时禁止写入: + +- 赛题原始数据、附件内容和具体题目条件; +- 模型结构、公式、算法流程、关键假设和参数值; +- 具体计算结果、敏感性结论、优劣比较和最终答案; +- 可据此复原参赛方案的连续提示词或长交互记录。 + +允许使用阶段级概括,例如“辅助思路梳理”“辅助代码实现与调试”“辅助结果一致性检查”“辅助论文结构和表达优化”。典型交互示例只保留经过脱敏的提示摘要和处理结果。 + +## 数据流 + +1. `1start-mathmodel` 识别国赛任务后加载 `7ai-disclosure`,创建 `reports/AI_USAGE_LOG.md`。 +2. 每个阶段结束时,从本阶段实际输入、输出和执行记录追加或更新工具及使用记录。 +3. `6verity` 完成论文检查并得到 `PAPER_PASS`。 +4. `7ai-disclosure finalize` 自动清理和归纳日志,不触发 `AskUserQuestion`。 +5. 生成器将日志映射到四部分 LaTeX 模板,编译并检查 `AI 工具使用详情.pdf`。 +6. 仅当论文 PDF 与详情 PDF 均通过检查时,最终状态为 `PASS`。 + +## 第四部分生成规则 + +第四部分按非纯语言润色记录生成,最多保留与评审理解直接相关的简要说明: + +- “采纳情况”写选择性采用、经修正采用或未采用,依据最终文件和过程记录判断。 +- “修改情况”只写实际可见的迭代修正,并明确属于自动流程时使用“经运行检查后修正”等中性表达。 +- “核验情况”写实际完成的复现运行、边界测试、异常检查、正文与结果一致性检查等。 +- 没有可核实人工修改时省略对应句,不生成否定性占位说明。 + +示例措辞: + +> 相关输出经程序复现和一致性检查后选择性采用;发现的不一致内容已重新计算或删除,并再次通过结果文件与论文表述核对。 + +只有在日志确有对应执行证据时才能生成上述表述。 + +## 校验与失败处理 + +最终校验必须拒绝: + +- `待队员确认`、模板占位符或空的必填工具信息; +- 未在任务中实际执行却写成已完成的测试或核验; +- 将自动操作写成队员人工操作的措辞; +- 题目条件、公式、参数、结果等核心建模信息; +- 密钥、令牌、完整聊天记录或模型私有思维链; +- 文件名不准确、PDF 为空、非 A4、缺少文本层或缺少四个规定部分。 + +当版本元数据不可得时使用明确的不可得说明,并继续生成;当核心字段无法从证据归纳时采用更抽象但真实的阶段描述,不虚构细节。 + +## 文件修改范围 + +- `skills/1start-mathmodel/SKILL.md`:删除最终人工询问,改为自动归纳并触发生成。 +- `skills/7ai-disclosure/SKILL.md`:定义证据驱动、核心信息脱敏和无人值守终检。 +- `skills/7ai-disclosure/templates/AI_USAGE_LOG.md`:将待确认式模板改为可自动填充结构。 +- `skills/7ai-disclosure/scripts/validate_ai_usage_log.py`:支持自动归纳状态和省略无证据人工修改,同时增加禁写内容检查。 +- `skills/7ai-disclosure/scripts/generate_ai_usage_pdf.py`:省略无证据人工修改句,并保证只生成脱敏概要。 +- `skills/7ai-disclosure/scripts/test_ai_disclosure.py` 与测试夹具:覆盖无人询问、版本不可得、无人工记录、核心信息过滤和双 PDF 门禁。 +- `skills/6verity/SKILL.md`、`README.md`、`README_EN.md`:同步最终行为说明。 + +## 验收标准 + +1. 国赛任务工作流中不存在 AI 使用详情相关的 `AskUserQuestion` 门禁。 +2. 无需用户补充即可生成两个名称正确的 PDF。 +3. 详情 PDF 恰好保留规定的四部分,且第一部分含工具编号、第二部分为表格、第三部分非表格。 +4. 最终 PDF 不包含“未记录到额外人工修改”或同义提示。 +5. 最终 PDF 不声称未经证据支持的人工操作,不泄露核心建模过程。 +6. 实际生成样例通过日志校验、单元测试、PDF 文本检查和逐页视觉检查。 diff --git a/docs/superpowers/specs/2026-08-24-desktop-ai-disclosure-integration-design.md b/docs/superpowers/specs/2026-08-24-desktop-ai-disclosure-integration-design.md new file mode 100644 index 00000000..4277fef7 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-desktop-ai-disclosure-integration-design.md @@ -0,0 +1,156 @@ +# 桌面版国赛 AI 工具使用详情集成设计 + +日期:2026-08-24 +范围:桌面版内置的 MathModelAgent Skills 工作流 + +## 1. 背景与边界 + +仓库 README 说明桌面版内置 Claude Code 与全套 MathModelAgent Skills,但当前仓库不包含 Electron、Tauri 或其他桌面壳及其打包源码。因此,本次改动以桌面版实际使用的 Skills 工作流为集成边界,不修改仓库中的旧 Vue/FastAPI 工作流,也不声称能够直接更新已经发布或安装的桌面程序。 + +原作者合并本改动并重新打包桌面版后,新版桌面程序才能包含该功能。 + +## 2. 目标 + +当用户在桌面版中启动全国大学生数学建模竞赛(国赛,CUMCM)论文生成任务时: + +1. 主工作流自动启用 `7ai-disclosure`。 +2. 从任务开始持续记录实际使用的 AI 工具、模型、用途、提示过程、输出采纳与核验情况。 +3. 完整论文通过验收后,只向队员集中询问一次必须由人确认的信息。 +4. 确认完成后生成名称完全一致的 `AI 工具使用详情.pdf`。 +5. `paper/main.pdf` 与该支撑材料 PDF 均通过验收后,任务才能报告最终完成。 + +## 3. 非目标 + +- 不为美赛或其他赛事强制生成国赛支撑材料。 +- 不修改已经发布的桌面安装包。 +- 不在缺少真实证据时推测或编造人工修改、人工核验情况。 +- 不把 API Key、访问令牌、完整敏感提示或内部调试信息写入最终 PDF。 +- 不把空白模板作为成功产物。 + +## 4. 方案 + +采用 Skill 工作流内的确定性集成,不依赖模型自行记住赛后补写材料。 + +```text +国赛任务启动 + -> 1start-mathmodel 初始化 AI_USAGE_LOG.md + -> 各工作阶段结束后调用 7ai-disclosure record + -> 5writing 生成完整论文 + -> 6verity 验收论文和项目产物 + -> 7ai-disclosure 汇总一次性人工确认问题 + -> 队员回复一次 + -> 7ai-disclosure finalize 生成并检查 PDF + -> 论文 PDF + AI 工具使用详情.pdf 均通过后报告完成 +``` + +## 5. 启用条件 + +`1start-mathmodel` 根据任务明确选择的赛事模板或题目信息判断赛事: + +- `CHINA`、`国赛`、`CUMCM` 或“全国大学生数学建模竞赛”视为国赛任务,必须启用 `7ai-disclosure`。 +- 明确为其他赛事时不启用该国赛支撑材料流程。 +- 无法判断赛事时,在任务开始阶段询问赛事类型;不得等到论文完成后再判断。 + +如果队伍确认全程未使用任何 AI,不生成空白 PDF,并在验收报告中保存该确认。桌面 Agent 本身参与了论文生成时,不得使用“全程未使用 AI”分支。 + +## 6. 记录机制 + +### 6.1 初始化 + +国赛工作流开始时,从模板创建 `reports/AI_USAGE_LOG.md`。日志使用稳定工具编号 `TOOL-01`、`TOOL-02` 等,使后续用途、提示过程和采纳核验记录能够对应具体工具。 + +工具名称与模型优先读取任务的真实运行配置和模型响应元数据;无法获得的字段进入最终一次性确认清单,不能自行猜测版本。 + +### 6.2 阶段记录 + +以下阶段结束后立即执行 `7ai-disclosure record`: + +- 赛题分析与任务拆解; +- 建模与方法选择; +- 代码生成、调试和计算; +- 图表与流程图生成; +- 论文写作与排版; +- 论文和项目验收。 + +每条记录至少包含工具编号、使用环节、具体目的、提示方式或提示摘要、AI 输出去向、已完成的修改和核验证据。记录只能来自本次任务的真实输入、对话、代码、结果和文件,不允许赛后凭空补造。 + +### 6.3 一次性人工确认 + +完成论文验收后,`7ai-disclosure` 汇总所有仍需人确认的内容,以一条消息集中询问。确认内容主要包括: + +- 队员是否采纳相关 AI 输出; +- 做过哪些非语言润色类人工修改; +- 使用什么方式人工核验关键结果; +- 无法从运行元数据确定的工具版本或型号。 + +队员只需回复一次。最终生成器把回复写入日志并重新校验;回复仍有缺项时,明确指出缺失项并停止生成,不输出带占位符的提交版 PDF。 + +## 7. PDF 生成 + +`7ai-disclosure finalize` 调用仓库内的确定性生成脚本,而不是仅依靠模型自由编辑模板。生成器负责: + +1. 解析并校验 `reports/AI_USAGE_LOG.md`。 +2. 将日志映射到既定四部分模板。 +3. 对插入 LaTeX 的动态内容进行转义。 +4. 写出 `supporting_materials/AI 工具使用详情.tex`。 +5. 优先使用 XeLaTeX 编译两遍;XeLaTeX 不可用时回退到 Tectonic。两者均不可用或编译失败时停止生成。 +6. 生成 `supporting_materials/AI 工具使用详情.pdf`。 +7. 检查文件非空、文件名完全一致、页面为 A4、存在文本层、四个部分齐全,且不含占位符或疑似密钥。 + +生成后的 PDF 保持当前已经确认的简洁四部分结构与版式,不增加责任说明、基本信息、最终确认或其他已删除章节。 + +## 8. 工作流门禁与错误处理 + +以下任一情况发生时,国赛任务不得报告最终完成: + +- `7ai-disclosure` 未被安装或无法加载; +- AI 使用日志缺失、字段不完整或仍有待确认内容; +- 一次性人工确认尚未完成; +- PDF 编译器不可用或编译失败; +- 最终文件名不完全等于 `AI 工具使用详情.pdf`; +- PDF 为空、不是 A4、没有文本层、缺少规定部分; +- PDF 含占位符、密钥或未经确认的陈述; +- `paper/main.pdf` 或 AI 支撑材料 PDF 任一缺失。 + +错误消息应说明失败环节、缺失内容和恢复方式。用户修正后从失败环节继续,不重复已经完成的建模与论文生成。 + +## 9. 仓库改动点 + +- `skills/skills.sh.json`:把 `7ai-disclosure` 加入工作流 Skill 清单。 +- `skills/1start-mathmodel/SKILL.md`:明确国赛触发、阶段记录、一次确认、最终双文件门禁。 +- `skills/6verity/SKILL.md`:把 AI 支撑材料纳入国赛最终验收。 +- `skills/7ai-disclosure/SKILL.md`:定义自动记录、一次确认和确定性生成流程。 +- `skills/7ai-disclosure/scripts/`:增加确定性 PDF 生成及必要的校验逻辑。 +- `skills/doctor/SKILL.md`:检查 `7ai-disclosure`、Python,以及 XeLaTeX 或 Tectonic PDF 编译依赖。 +- `README.md`、`README_EN.md`:说明国赛任务会同时生成 AI 使用详情,以及人工确认门禁。 + +不修改 `backend/` 与 `frontend/`;它们不是 README 所述桌面版的打包源码。 + +## 10. 测试与验收 + +至少覆盖以下自动测试: + +1. 国赛任务会初始化日志并进入披露流程。 +2. 非国赛任务不会生成国赛支撑材料。 +3. 存在待确认内容时,生成器拒绝输出提交版 PDF。 +4. 一次确认完成后,日志通过最终校验。 +5. 多个 AI 工具的编号能在四部分之间正确对应。 +6. 特殊字符不会破坏 LaTeX 编译。 +7. 模拟编译失败时,最终门禁失败并给出恢复提示。 +8. 成功样例生成名称准确的 A4 PDF,具有文本层和四个规定部分。 +9. 输出不包含占位符、疑似密钥和模板提示。 +10. `paper/main.pdf` 存在但 AI 支撑材料缺失时,国赛任务不能报告最终完成。 + +最终还需用一个最小国赛夹具执行端到端测试,确认输出目录同时包含 `paper/main.pdf` 和 `supporting_materials/AI 工具使用详情.pdf`。 + +## 11. 完成标准 + +本功能完成需同时满足: + +- `7ai-disclosure` 可被桌面 Skill 安装和发现; +- 国赛主工作流自动调用该 Skill; +- 人工信息只集中确认一次; +- 最终 PDF 由真实日志确定性生成并通过检查; +- 缺少该 PDF 时国赛任务不能显示最终成功; +- 自动测试和最小端到端测试均通过; +- 工作区不存在未说明的生成物或敏感信息。 diff --git a/skills/1start-mathmodel/SKILL.md b/skills/1start-mathmodel/SKILL.md index ce215597..eb60f72c 100644 --- a/skills/1start-mathmodel/SKILL.md +++ b/skills/1start-mathmodel/SKILL.md @@ -1,7 +1,7 @@ --- name: 1start-mathmodel -description: "数学建模竞赛工作流入口。用于启动完整建模流程:询问用户偏好,生成 plan.md 和 todo.md,并按阶段调用赛题分析、建模、代码与图表、流程图、论文撰写、验证验收等 skills。" -allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, Agent, WebSearch, WebFetch +description: "数学建模竞赛工作流入口。用于启动完整建模流程:询问用户偏好,生成 plan.md 和 todo.md,并按阶段调用赛题分析、建模、代码与图表、流程图、论文撰写、验证验收及 AI 使用披露等 skills。" +allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, Agent, AskUserQuestion, WebSearch, WebFetch --- # 数学建模工作流 @@ -12,12 +12,20 @@ allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, Agent, WebSearch, WebFetc 如需领域判断,读取 `../_references/math_modeling_norms.md`。该文件只提供数学建模基本规范和防错知识,不改变本 skill 的阶段顺序和产出约定。 +## 国赛识别与强制披露 + +- 用户选择 `CHINA`、国赛、CUMCM 或全国大学生数学建模竞赛时,立即将任务标记为国赛,并强制加载 `7ai-disclosure`。 +- 明确为其他赛事时,不生成国赛专用的 `AI 工具使用详情.pdf`。 +- 无法判断赛事时,在开始阶段询问一次赛事类型,不得等到论文完成后再判断。 +- 桌面 Agent 正在参与论文生成时,视为实际使用了 AI,不得走“全程未使用 AI”分支。 + ## 必须产出 在当前工作目录中创建或更新以下文件: - `plan.md`:整体流程方案、建模方向、阶段顺序、预期产物和风险控制。 - `todo.md`:具体待办事项列表,记录每个阶段的任务和状态。 +- 国赛任务的 `reports/AI_USAGE_LOG.md`:赛中持续维护并在终检时自动归纳的 AI 工具使用过程记录。 ## 工作流 @@ -57,6 +65,7 @@ workflow: 3. 流程与架构图绘制 - `4drawio` 4. 竞赛论文撰写 - `5writing` 5. 验证和验收 - `6verity` +6. AI 使用详情终检与生成 - `7ai-disclosure`(仅国赛强制) ``` ## 项目目录结构 @@ -72,6 +81,7 @@ workflow: │ ├── RESULTS_REPORT.md # 2: 结果报告(3coding-visual) │ ├── DRAWIO_REPORT.md # 3: 非数据图说明(4drawio) │ ├── VERIFY_REPORT.md # 5: 验收报告(6verity) +│ └── AI_USAGE_LOG.md # 国赛全程: AI 使用记录(7ai-disclosure) ├── code/ # 2: 代码(3coding-visual) │ ├── problem1.py │ ├── problem2.py @@ -85,6 +95,9 @@ workflow: ├── paper/ # 4: 论文(5writing) │ ├── main.typ / main.tex # 论文主文件(按用户选择的引擎) │ └── sections/ # 各节文件(.typ 或 .tex) +└── supporting_materials/ # 6: AI 使用支撑材料(确有使用 AI 时) + ├── AI 工具使用详情.tex + └── AI 工具使用详情.pdf ``` 方案必须明确每个阶段由哪个下游 skill 负责,以及该阶段应产出什么文件。 @@ -101,6 +114,7 @@ workflow: - [ ] 3. 流程与架构图绘制 - `4drawio` - [ ] 4. 竞赛论文撰写 - `5writing` - [ ] 5. 验证和验收 - `6verity` +- [ ] 6. AI 使用详情终检与生成 - `7ai-disclosure`(仅国赛强制) ``` 每完成一个阶段,都要更新 `todo.md` 中对应任务的状态。 @@ -116,6 +130,20 @@ workflow: | 流程与架构图绘制 | `4drawio` | 在论文确实需要时,绘制方法流程图、架构图和非数据型概念图。 | `figures/*.drawio`, `figures/*.pdf`, `DRAWIO_REPORT.md` | | 竞赛论文撰写 | `5writing` | 基于分析、建模、代码结果和图表撰写最终竞赛论文,并按章节直接插入图表。 | `paper/` | | 验证和验收 | `6verity` | 检查可复现性、一致性、产物完整性、格式规范和提交就绪状态。 | `VERIFY_REPORT.md` | +| AI 使用详情终检与生成(仅国赛强制) | `7ai-disclosure` | 校验全过程记录,生成并验收赛事要求的 AI 使用支撑材料。 | `AI_USAGE_LOG.md`, `AI 工具使用详情.pdf` | + +## AI 使用记录衔接 + +以下规则只对国赛任务强制执行: + +1. 启动时加载 `7ai-disclosure`,从其模板初始化 `reports/AI_USAGE_LOG.md`。优先根据本次任务的真实模型配置创建工具条目;无法获得版本元数据时写“运行环境未提供具体版本”。 +2. `2analysis-modeling`、`3coding-visual`、`4drawio`、`5writing` 和 `6verity` 每个阶段结束后,立即调用 `7ai-disclosure record`,根据真实对话、模型配置和产物追加记录;不得等到赛后凭记忆补造。 +3. `6verity` 首先完成论文验收并得到 `PAPER_PASS`。此状态只表示论文通过,不能表示整个国赛提交包已经完成。 +4. 随后调用 `7ai-disclosure finalize`。它根据本次会话、文件、运行结果和验收记录自动归纳并脱敏,不为填写详情另行询问队员。 +5. 自动归纳只能陈述有证据的采纳、迭代修正和核验;没有人工修改证据时省略该句,不得补造人工经历。通过最终校验后运行确定性生成脚本。 +6. 只有 `paper/main.pdf` 与名称完全一致的 `supporting_materials/AI 工具使用详情.pdf` 都存在并通过检查,才把 `todo.md` 的最后一步标记完成,并在 `reports/VERIFY_REPORT.md` 写最终 `PASS`。 + +若不是桌面 Agent 参与生成、且队伍能够确认全程未使用任何 AI,才允许不生成空白详情 PDF,并在验收报告中写明确认依据。 ## 阶段边界 diff --git a/skills/6verity/SKILL.md b/skills/6verity/SKILL.md index 1d0f6bf8..69df89c6 100644 --- a/skills/6verity/SKILL.md +++ b/skills/6verity/SKILL.md @@ -1,12 +1,12 @@ --- name: 6verity -description: "数学建模竞赛最终验证和验收阶段,支持 Typst 和 LaTeX 双引擎。用于论文写完后检查章节数量、标题顺序、图表引用、数值一致性、占位符、内部文件泄露、参考文献、代码可复现性、编译和提交就绪状态。" +description: "数学建模竞赛论文验证和验收阶段,支持 Typst 和 LaTeX 双引擎。用于论文写完后检查章节数量、标题顺序、图表引用、数值一致性、占位符、内部文件泄露、参考文献、代码可复现性、编译、AI 支撑材料状态和提交就绪状态。" allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, Agent, WebSearch, WebFetch --- # 验证和验收(Typst / LaTeX) -本 skill 是完整工作流的最后一关。它不重新建模、不生成新结果、不代替写作阶段重写论文;它负责发现硬错误、修复可直接修复的问题,并输出 `reports/VERIFY_REPORT.md`。 +本 skill 是论文与项目产物的验收门禁。它不重新建模、不生成新结果、不代替写作阶段重写论文;它负责发现硬错误、修复可直接修复的问题,并输出 `reports/VERIFY_REPORT.md`。若项目使用过 AI,随后还必须由 `7ai-disclosure` 完成支撑材料终检与生成。 ## 数学建模规范参考 @@ -16,6 +16,7 @@ allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, Agent, WebSearch, WebFetc - 本阶段负责:结构验收、文本质量门禁、图表引用检查、结果一致性检查、Typst/LaTeX 编译检查、PDF 视觉检查、提交清单。 - 本阶段不负责:重新设计模型、重新跑大规模实验、重新组织整篇论文。 +- 本阶段只核对 AI 使用日志和支撑材料状态,不补写或猜测 AI 使用记录;自动归纳由 `7ai-disclosure` 根据实际产物和检查证据完成。 - 发现硬错误时,优先做小范围修复;如果需要回到前序阶段,写入 `reports/VERIFY_REPORT.md` 并标记为未通过。 ## 输入 @@ -29,6 +30,7 @@ allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob, Agent, WebSearch, WebFetc 5. 图表目录 6. 可复现代码目录。 7. 编译后的 PDF,或可由入口文件编译得到的输出 PDF。 +8. 国赛任务必须提供 `reports/AI_USAGE_LOG.md`;最终验收还必须提供 `supporting_materials/AI 工具使用详情.tex` 和 `supporting_materials/AI 工具使用详情.pdf`。其他赛事仅在实际存在时检查。 不要假设论文目录一定叫 `paper/`,也不要假设结果文件一定在项目根。若项目使用不同命名,按实际结构传参并在 `reports/VERIFY_REPORT.md` 中说明。 @@ -185,7 +187,17 @@ fi 如果模型没有视觉能力,必须在 `reports/VERIFY_REPORT.md` 中明确写出“未执行视觉检查”的原因,并至少完成 PDF 非空、页数、页面尺寸等可程序化检查。 -### Step 9: 写验收报告 +### Step 9: 检查 AI 使用支撑材料状态 + +- 先读取 `plan.md` 的竞赛类型。`CHINA`、国赛、CUMCM 或全国大学生数学建模竞赛均按国赛处理。 +- 国赛任务必须能够加载 `7ai-disclosure`,并存在 `reports/AI_USAGE_LOG.md`。桌面 Agent 参与论文生成时,不得写“全程未使用 AI”。 +- 首次验收论文时,用 `record` 模式检查日志。论文自身全部通过可以记录为 `PAPER_PASS`,但此状态不是最终提交 `PASS`。 +- `7ai-disclosure` 完成证据驱动的自动归纳、脱敏和 PDF 生成后,再用 `finalize` 模式检查日志,并检查 `supporting_materials/AI 工具使用详情.pdf` 文件名完全一致。 +- 检查该 PDF 非空、可提取文本、页面为 A4、包含规定四部分且无占位符或疑似密钥,并逐页查看是否越界、重叠、裁切或乱码。 +- 国赛任务只有 `paper/main.pdf` 与 `supporting_materials/AI 工具使用详情.pdf` 均通过检查,整体状态才能写 `PASS`。 +- 非国赛任务只有在用户明确要求时才检查该国赛支撑材料。 + +### Step 10: 写验收报告 创建 `reports/VERIFY_REPORT.md`: @@ -193,7 +205,7 @@ fi # 验证和验收报告 ## 结论 -PASS / FAIL +PAPER_PASS / PASS / FAIL ## 检查项 | 检查项 | 结果 | 说明 | @@ -211,10 +223,12 @@ PASS / FAIL ## PDF 视觉检查 +## AI 工具使用支撑材料 + ## 仍需处理的问题 ``` -只有当硬错误都修复、文本门禁通过、核心图表都引用、数值一致、编译通过或明确说明不可编译原因、视觉检查通过或明确说明无法执行原因时,才写 `PASS`。 +论文自身通过、但国赛 AI 支撑材料尚未完成自动归纳或尚未生成时,只能写 `PAPER_PASS`。只有全部硬错误都修复,且国赛要求的两个 PDF 均通过最终检查时,才写 `PASS`。 ## 硬错误标准 @@ -232,6 +246,8 @@ PASS / FAIL - 编译器可用但论文编译失败。 - 编译后的 PDF 为空、缺页、页数异常或页面尺寸异常且无法解释。 - 视觉检查发现正文、表格、图片、公式、页眉页脚、页码等关键元素重叠、裁切、越界或乱码。 +- 日志表明使用过 AI,但最终提交缺少名称完全一致的 `AI 工具使用详情.pdf`。 +- AI 使用日志仍有待确认或未归纳内容、未经证据支持的人工表述、核心建模细节或疑似密钥,却将整体提交状态标记为 `PASS`。 ## 警告标准 diff --git a/skills/7ai-disclosure/SKILL.md b/skills/7ai-disclosure/SKILL.md new file mode 100644 index 00000000..b7df31fa --- /dev/null +++ b/skills/7ai-disclosure/SKILL.md @@ -0,0 +1,72 @@ +--- +name: 7ai-disclosure +description: "全国大学生数学建模竞赛 AI 工具使用过程记录与支撑材料生成。国赛论文工作流启动时自动启用,根据任务证据持续记录并自动归纳工具、用途、提示过程、采纳、修正和核验,在隐藏核心建模细节后生成四部分简版“AI 工具使用详情.pdf”。" +allowed-tools: Bash(*), Read, Write, Edit, Grep, Glob +--- + +# AI 工具使用记录与详情生成 + +本 skill 有 `record` 和 `finalize` 两种模式。国赛任务由 `1start-mathmodel` 强制调用;其他赛事只在用户明确要求时使用。它只整理本次任务中可观察的 AI 使用,不补造记录,也不自动宣称合规。整个披露流程无人值守,不为填写详情另行询问用户。 + +桌面 Agent 正在参与国赛论文生成时,视为实际使用了 AI,不得生成“全程未使用 AI”的空记录。 + +## 输出约束 + +最终 PDF 只包含标题、页眉页码和以下四个一级部分: + +1. 所用 AI 工具名称、版本或型号; +2. 具体使用目的和环节; +3. 主要提示方式与使用过程说明,可附非表格的典型交互示例; +4. 对 AI 输出的采纳、人工修改和核验的主要情况,语言润色除外。 + +第一部分使用“编号、AI 工具名称、版本或型号”三列表格;第二部分使用“工具编号、使用环节、具体使用目的”三列表格。工具编号采用 `TOOL-01`、`TOOL-02` 等,第二至第四部分必须引用对应编号。禁止写入密钥、密码、完整聊天记录、模型私有思维链或无关个人信息。 + +详情 PDF 只写阶段级概括。即使日志或论文中存在,也不得写入赛题条件、原始数据、模型或算法名称、公式、关键假设、参数、数值结果、结论以及足以复原解题方案的连续提示。使用“辅助方案梳理”“辅助代码实现与调试”“辅助结果一致性检查”“辅助论文结构与表达优化”等概括表达。 + +## record:持续记录 + +1. 若 `reports/AI_USAGE_LOG.md` 不存在,从 `templates/AI_USAGE_LOG.md` 复制创建。 +2. 查看当前阶段的真实对话、运行配置、文件差异、运行结果和产物。工具名称与模型优先读取本次任务的真实配置或响应元数据;不可获得时写“运行环境未提供具体版本”,不得根据供应商或界面猜测版本。 +3. 复用已有 `TOOL-xx`;新工具使用下一个连续编号,编号必须从 `TOOL-01` 开始连续递增。 +4. 追加一个连续编号的 `AI-xxx` 记录,写明工具编号、使用环节、具体目的、提示方式、使用过程、采纳情况、迭代修正、核验方法、证据文件和记录状态。 +5. 采纳情况根据内容是否进入最终文件以及后续是否被替换、删除或修正归纳。迭代修正只写文件差异或重新运行能够支持的事实;核验只写实际执行成功并有结果记录的检查。 +6. 只有会话中存在队员明确修改指令时,才可增加“人工修改内容”和“人工修改依据”。没有此类证据时直接省略这两个字段,不写任何“未记录人工修改”的提示。 +7. 仅用于语言润色时,将“是否仅用于语言润色”写为“是”,迭代修正和核验字段可以省略。 +8. 典型交互为可选项;如记录,只保留脱敏的提示、输出和处理结果摘要,不复制全部对话。 +9. 阶段处理中使用“记录中”;阶段证据归纳完成后使用“已归纳”。不得推断队员完成了未留下证据的人工操作。 +10. 每个国赛阶段结束后立即运行记录校验: + +```bash +PYTHON_BIN="$(command -v python3 || command -v python)" +"$PYTHON_BIN" "<本 skill 目录>/scripts/validate_ai_usage_log.py" reports/AI_USAGE_LOG.md --mode record +``` + +## finalize:生成支撑材料 + +1. 自动读取运行时元数据、各阶段报告、最终文件、代码执行结果和 `reports/VERIFY_REPORT.md`,逐条归纳工具版本、采纳、迭代修正和核验情况,不向用户发起确认或补充提问。 +2. 将所有公开描述改写为阶段级概括,删除核心建模细节。没有版本元数据时使用“运行环境未提供具体版本”;没有人工修改证据时删除人工修改字段,不输出否定性说明。 +3. 若缺少核验证据,先执行安全、可复现的轻量检查并保存结果,再填写实际结果;不得把计划中的检查写成已经完成。完成后把记录状态更新为“已归纳”。 +4. 运行最终校验: + +```bash +PYTHON_BIN="$(command -v python3 || command -v python)" +"$PYTHON_BIN" "<本 skill 目录>/scripts/validate_ai_usage_log.py" reports/AI_USAGE_LOG.md --mode finalize +``` + +存在待确认、缺项、无效工具引用、未归纳记录、疑似密钥、未经证据支持的人工表述或核心建模细节时停止生成。 + +5. 最终校验通过后,必须调用确定性生成脚本,不再让模型自由复制或手填模板: + +```bash +"$PYTHON_BIN" "<本 skill 目录>/scripts/generate_ai_usage_pdf.py" \ + reports/AI_USAGE_LOG.md \ + --output-dir supporting_materials +``` + +生成脚本负责日志到四部分模板的映射、LaTeX 转义、XeLaTeX 两遍编译、Tectonic 回退,以及文件名、非空、A4、文本层、规定部分、占位符和疑似密钥检查。典型交互使用正文段落,不使用表格;证据文件和内部状态只用于校验,不写入最终 PDF。 + +6. 必须得到名称完全一致的 `supporting_materials/AI 工具使用详情.pdf`,再逐页检查是否存在越界、重叠、裁切或乱码。 +7. 在 `reports/VERIFY_REPORT.md` 记录自动归纳、日志校验、编译、程序化 PDF 检查和逐页视觉检查结果。 +8. 只有 `paper/main.pdf` 与 `supporting_materials/AI 工具使用详情.pdf` 均通过检查,国赛提交包才能标记最终 `PASS`。 + +仅当本任务并非由桌面 Agent 生成、且队伍确认全程未使用任何 AI 时,不生成空白详情 PDF,并在验收报告中记录确认依据。 diff --git a/skills/7ai-disclosure/scripts/generate_ai_usage_pdf.py b/skills/7ai-disclosure/scripts/generate_ai_usage_pdf.py new file mode 100644 index 00000000..df98a3e1 --- /dev/null +++ b/skills/7ai-disclosure/scripts/generate_ai_usage_pdf.py @@ -0,0 +1,515 @@ +#!/usr/bin/env python3 +"""Generate the CUMCM AI usage disclosure from a validated Markdown log.""" + +from __future__ import annotations + +import argparse +import re +import shutil +import subprocess +import sys +from dataclasses import dataclass +from pathlib import Path + +from validate_ai_usage_log import ( + NO_HUMAN_RECORD_PHRASES, + PENDING_MARKERS, + SECRET_PATTERNS, + id_sections, + parse_fields, + validate, +) + +TEMPLATE_MARKERS = ( + "%%__TOOLS_ROWS__%%", + "%%__PURPOSE_ROWS__%%", + "%%__PROCESS_BLOCKS__%%", + "%%__ADOPTION_BLOCKS__%%", +) +REQUIRED_PDF_HEADINGS = ( + "一、所用AI工具名称、版本或型号", + "二、具体使用目的和环节", + "三、主要提示方式与使用过程说明", + "四、对AI输出的采纳、人工修改和核验的主要情况", +) + + +class GenerationError(RuntimeError): + """Raised when a disclosure artifact cannot be generated safely.""" + + +@dataclass(frozen=True) +class ToolEntry: + """One AI tool declared in the usage log.""" + + tool_id: str + name: str + version: str + + +@dataclass(frozen=True) +class UsageRecord: + """One stage-level AI usage record.""" + + record_id: str + tool_ids: tuple[str, ...] + stage: str + purpose: str + language_only: bool + prompt_method: str + process: str + adoption: str + iteration: str + verification: str + human_edit: str + + +@dataclass(frozen=True) +class InteractionExample: + """One optional representative interaction example.""" + + example_id: str + record_ids: tuple[str, ...] + prompt_summary: str + output_summary: str + process_result: str + + +@dataclass(frozen=True) +class DisclosureLog: + """Structured data parsed from the Markdown log.""" + + tools: tuple[ToolEntry, ...] + records: tuple[UsageRecord, ...] + examples: tuple[InteractionExample, ...] + + +def _field(fields: dict[str, list[str]], name: str, default: str = "") -> str: + """Return a stable, compact value for a structured Markdown field.""" + values = [value.strip() for value in fields.get(name, []) if value.strip()] + return ";".join(values) if values else default + + +def parse_log(path: Path) -> DisclosureLog: + """Parse a validated AI usage log into structured records. + + Args: + path: Markdown log path. + + Returns: + Structured disclosure data. + """ + text = path.read_text(encoding="utf-8-sig") + tools: list[ToolEntry] = [] + records: list[UsageRecord] = [] + examples: list[InteractionExample] = [] + + for item_id, block in id_sections(text): + fields = parse_fields(block) + if item_id.startswith("TOOL-"): + tools.append( + ToolEntry( + tool_id=item_id, + name=_field(fields, "工具名称"), + version=_field(fields, "版本或型号"), + ) + ) + elif item_id.startswith("AI-"): + tool_ids = tuple( + dict.fromkeys(re.findall(r"TOOL-\d{2}", _field(fields, "工具编号"))) + ) + records.append( + UsageRecord( + record_id=item_id, + tool_ids=tool_ids, + stage=_field(fields, "使用环节"), + purpose=_field(fields, "具体使用目的"), + language_only=_field(fields, "是否仅用于语言润色") == "是", + prompt_method=_field(fields, "主要提示方式"), + process=_field(fields, "使用过程说明"), + adoption=_field(fields, "AI 输出采纳情况"), + iteration=_field(fields, "迭代修正情况"), + verification=_field(fields, "核验方法与结果"), + human_edit=_field(fields, "人工修改内容"), + ) + ) + elif item_id.startswith("EX-"): + record_ids = tuple( + dict.fromkeys(re.findall(r"AI-\d{3}", _field(fields, "对应记录"))) + ) + examples.append( + InteractionExample( + example_id=item_id, + record_ids=record_ids, + prompt_summary=_field(fields, "代表性提示摘要"), + output_summary=_field(fields, "AI 输出摘要"), + process_result=_field(fields, "处理结果"), + ) + ) + + return DisclosureLog(tuple(tools), tuple(records), tuple(examples)) + + +def latex_escape(value: str) -> str: + """Escape untrusted log text for LaTeX text mode. + + Args: + value: Plain text from the automatically summarized usage log. + + Returns: + A single-line LaTeX-safe string. + """ + compact = re.sub(r"\s+", " ", value).strip() + replacements = { + "\\": r"\textbackslash{}", + "{": r"\{", + "}": r"\}", + "$": r"\$", + "&": r"\&", + "#": r"\#", + "_": r"\_", + "%": r"\%", + "~": r"\textasciitilde{}", + "^": r"\textasciicircum{}", + } + return "".join(replacements.get(char, char) for char in compact) + + +def _tools_rows(data: DisclosureLog) -> str: + return "\n".join( + f"{tool.tool_id} & {latex_escape(tool.name)} & " + f"{latex_escape(tool.version)} \\\\" + for tool in data.tools + ) + + +def _purpose_rows(data: DisclosureLog) -> str: + return "\n".join( + f"{latex_escape('、'.join(record.tool_ids))} & " + f"{latex_escape(record.stage)} & {latex_escape(record.purpose)} \\\\" + for record in data.records + ) + + +def _process_blocks(data: DisclosureLog) -> str: + examples_by_record: dict[str, list[InteractionExample]] = {} + for example in data.examples: + for record_id in example.record_ids: + examples_by_record.setdefault(record_id, []).append(example) + + blocks: list[str] = [] + for record in data.records: + tool_ids = "、".join(record.tool_ids) + lines = [ + rf"\recordheading{{{record.record_id}}}{{{latex_escape(tool_ids)}}}", + rf"\textbf{{主要提示方式:}}{latex_escape(record.prompt_method)}。", + "", + rf"\textbf{{使用过程:}}{latex_escape(record.process)}。", + ] + for example in examples_by_record.get(record.record_id, []): + lines.extend( + [ + "", + rf"\noindent\textbf{{典型交互示例 {example.example_id}}}", + "", + rf"\textbf{{代表性提示摘要:}}{latex_escape(example.prompt_summary)}。", + "", + rf"\textbf{{AI 输出摘要:}}{latex_escape(example.output_summary)}。", + "", + rf"\textbf{{处理结果:}}{latex_escape(example.process_result)}。", + ] + ) + blocks.append("\n".join(lines)) + return "\n\n".join(blocks) + + +def _adoption_blocks(data: DisclosureLog) -> str: + blocks: list[str] = [] + for record in data.records: + if record.language_only: + continue + tool_ids = "、".join(record.tool_ids) + lines = [ + rf"\recordheading{{{record.record_id}}}{{{latex_escape(tool_ids)}}}", + rf"\textbf{{采纳情况:}}{latex_escape(record.adoption)}。", + ] + if record.human_edit: + lines.extend( + ["", rf"\textbf{{人工修改:}}{latex_escape(record.human_edit)}。"] + ) + if record.iteration: + lines.extend( + ["", rf"\textbf{{迭代修正:}}{latex_escape(record.iteration)}。"] + ) + if record.verification: + lines.extend( + ["", rf"\textbf{{核验情况:}}{latex_escape(record.verification)}。"] + ) + blocks.append("\n".join(lines)) + if not blocks: + return "本次记录仅涉及语言表达辅助,第四部分不作展开。" + return "\n\n".join(blocks) + + +def render_tex(log_path: Path, template_path: Path, tex_path: Path) -> Path: + """Render a final TeX source from a validated log. + + Args: + log_path: Finalized Markdown log. + template_path: LaTeX template containing deterministic markers. + tex_path: Destination TeX path. + + Returns: + The written TeX path. + + Raises: + GenerationError: If validation or template rendering fails. + """ + errors = validate(log_path, "finalize") + if errors: + raise GenerationError("日志未通过最终校验:\n- " + "\n- ".join(errors)) + if not template_path.is_file(): + raise GenerationError(f"LaTeX 模板不存在:{template_path}") + + template = template_path.read_text(encoding="utf-8-sig") + for marker in TEMPLATE_MARKERS: + if template.count(marker) != 1: + raise GenerationError(f"模板标记缺失或重复:{marker}") + + data = parse_log(log_path) + rendered = template + replacements = { + TEMPLATE_MARKERS[0]: _tools_rows(data), + TEMPLATE_MARKERS[1]: _purpose_rows(data), + TEMPLATE_MARKERS[2]: _process_blocks(data), + TEMPLATE_MARKERS[3]: _adoption_blocks(data), + } + for marker, content in replacements.items(): + rendered = rendered.replace(marker, content) + + if any(marker in rendered for marker in TEMPLATE_MARKERS): + raise GenerationError("生成后的 TeX 仍含模板标记") + for marker in PENDING_MARKERS: + if marker.casefold() in rendered.casefold(): + raise GenerationError(f"生成后的 TeX 仍含待确认或占位内容:{marker}") + for phrase in NO_HUMAN_RECORD_PHRASES: + if phrase in rendered: + raise GenerationError(f"生成后的 TeX 含禁止的无人工记录提示:{phrase}") + for label, pattern in SECRET_PATTERNS.items(): + if pattern.search(rendered): + raise GenerationError(f"生成后的 TeX 检测到疑似{label}") + + tex_path.parent.mkdir(parents=True, exist_ok=True) + with tex_path.open("w", encoding="utf-8", newline="\n") as output: + output.write(rendered) + return tex_path + + +def _run_compiler(command: list[str], cwd: Path) -> None: + """Run a PDF compiler and raise a compact actionable error on failure.""" + try: + result = subprocess.run( + command, + cwd=cwd, + capture_output=True, + text=True, + encoding="utf-8", + errors="replace", + timeout=180, + check=False, + ) + except subprocess.TimeoutExpired as exc: + raise GenerationError(f"PDF 编译超时:{' '.join(command)}") from exc + if result.returncode != 0: + output = (result.stdout + "\n" + result.stderr).strip() + raise GenerationError( + f"PDF 编译失败(退出码 {result.returncode}):\n{output[-4000:]}" + ) + + +def compile_pdf(tex_path: Path, compiler: str = "auto") -> Path: + """Compile the rendered TeX using XeLaTeX or Tectonic. + + Args: + tex_path: Rendered TeX source. + compiler: ``auto``, ``xelatex``, or ``tectonic``. + + Returns: + Compiled PDF path. + """ + candidates = ("xelatex", "tectonic") if compiler == "auto" else (compiler,) + selected = next( + ((name, shutil.which(name)) for name in candidates if shutil.which(name)), + None, + ) + if selected is None: + expected = " 或 ".join(candidates) + raise GenerationError(f"缺少 PDF 编译器:需要 {expected}") + + name, executable = selected + assert executable is not None + if name == "xelatex": + command = [ + executable, + "-interaction=nonstopmode", + "-halt-on-error", + tex_path.name, + ] + _run_compiler(command, tex_path.parent) + _run_compiler(command, tex_path.parent) + else: + _run_compiler([executable, "--keep-logs", tex_path.name], tex_path.parent) + + pdf_path = tex_path.with_suffix(".pdf") + if not pdf_path.is_file() or pdf_path.stat().st_size == 0: + raise GenerationError(f"编译器未生成非空 PDF:{pdf_path}") + return pdf_path + + +def cleanup_auxiliary_files(tex_path: Path) -> None: + """Remove compiler by-products after a fully verified build.""" + names = [ + tex_path.with_suffix(suffix) + for suffix in (".aux", ".log", ".out", ".toc", ".xdv") + ] + names.append(tex_path.with_name(f"{tex_path.stem}.synctex.gz")) + for path in names: + if path.is_file(): + path.unlink() + + +def _normalize_pdf_text(text: str) -> str: + return re.sub(r"\s+", "", text) + + +def inspect_pdf(pdf_path: Path, source_tex: Path | None = None) -> list[str]: + """Check filename, A4 pages, text layer, headings, and sensitive content. + + Some PDF parsers cannot decode CJK ToUnicode maps produced by every TeX + engine. In that case the compiled source verifies the required headings, + while the PDF parser still verifies that a non-empty text layer exists. + """ + errors: list[str] = [] + if pdf_path.name != "AI 工具使用详情.pdf": + errors.append("最终 PDF 文件名必须完全等于“AI 工具使用详情.pdf”") + if not pdf_path.is_file() or pdf_path.stat().st_size < 1024: + return errors + ["最终 PDF 不存在或文件过小"] + if pdf_path.read_bytes()[:5] != b"%PDF-": + errors.append("最终文件不是有效 PDF") + + pages: list[tuple[float, float, str]] = [] + try: + import fitz # type: ignore[import-unresolved] + + with fitz.open(pdf_path) as document: + pages = [ + (float(page.rect.width), float(page.rect.height), page.get_text("text")) + for page in document + ] + except ImportError: + try: + from pypdf import PdfReader # type: ignore[import-unresolved] + + reader = PdfReader(str(pdf_path)) + pages = [ + ( + float(page.mediabox.width), + float(page.mediabox.height), + page.extract_text() or "", + ) + for page in reader.pages + ] + except ImportError: + errors.append("缺少 PyMuPDF 或 pypdf,无法检查页面尺寸和文本层") + return errors + except Exception as exc: # noqa: BLE001 + errors.append(f"PDF 读取失败:{exc}") + return errors + except Exception as exc: # noqa: BLE001 + errors.append(f"PDF 读取失败:{exc}") + return errors + + if not pages: + return errors + ["最终 PDF 没有页面"] + for index, (width, height, _) in enumerate(pages, start=1): + if abs(width - 595.28) > 5 or abs(height - 841.89) > 5: + errors.append( + f"第 {index} 页不是 A4 纵向页面:{width:.1f} x {height:.1f} pt" + ) + + text = "\n".join(page_text for _, _, page_text in pages) + normalized_pdf = _normalize_pdf_text(text) + if not normalized_pdf: + errors.append("最终 PDF 没有可提取文本层") + heading_text = text + if source_tex is not None and source_tex.is_file(): + heading_text = source_tex.read_text(encoding="utf-8-sig") + normalized_headings = _normalize_pdf_text(heading_text) + for heading in REQUIRED_PDF_HEADINGS: + if heading not in normalized_headings: + errors.append(f"最终 PDF 缺少规定部分:{heading}") + for marker in PENDING_MARKERS: + if marker.casefold() in text.casefold(): + errors.append(f"最终 PDF 仍含待确认或占位内容:{marker}") + inspection_text = text + if source_tex is not None and source_tex.is_file(): + inspection_text += "\n" + source_tex.read_text(encoding="utf-8-sig") + for phrase in NO_HUMAN_RECORD_PHRASES: + if phrase in inspection_text: + errors.append(f"最终 PDF 含禁止的无人工记录提示:{phrase}") + for label, pattern in SECRET_PATTERNS.items(): + if pattern.search(text): + errors.append(f"最终 PDF 检测到疑似{label}") + return list(dict.fromkeys(errors)) + + +def main() -> int: + parser = argparse.ArgumentParser( + description="根据自动归纳的 AI 使用日志生成国赛 AI 工具使用详情 PDF" + ) + parser.add_argument("log_path", type=Path, help="reports/AI_USAGE_LOG.md 路径") + parser.add_argument( + "--output-dir", + type=Path, + default=Path("supporting_materials"), + help="输出目录,默认 supporting_materials", + ) + parser.add_argument( + "--template", + type=Path, + default=Path(__file__).resolve().parents[1] + / "templates" + / "AI工具使用详情模板.tex", + ) + parser.add_argument( + "--compiler", + choices=("auto", "xelatex", "tectonic"), + default="auto", + ) + parser.add_argument( + "--no-compile", + action="store_true", + help="只生成 TeX,不编译;仅用于自动测试", + ) + args = parser.parse_args() + + tex_path = args.output_dir / "AI 工具使用详情.tex" + try: + render_tex(args.log_path, args.template, tex_path) + print(f"TEX GENERATED: {tex_path}") + if args.no_compile: + return 0 + pdf_path = compile_pdf(tex_path, args.compiler) + errors = inspect_pdf(pdf_path, tex_path) + if errors: + raise GenerationError("PDF 验收失败:\n- " + "\n- ".join(errors)) + cleanup_auxiliary_files(tex_path) + print(f"PDF GENERATED AND VERIFIED: {pdf_path}") + return 0 + except GenerationError as exc: + print(f"ERROR: {exc}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/skills/7ai-disclosure/scripts/test_ai_disclosure.py b/skills/7ai-disclosure/scripts/test_ai_disclosure.py new file mode 100644 index 00000000..c37a535f --- /dev/null +++ b/skills/7ai-disclosure/scripts/test_ai_disclosure.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Regression tests for the 7ai-disclosure validator and renderer.""" + +from __future__ import annotations + +import tempfile +import unittest +from contextlib import ExitStack +from pathlib import Path +from unittest.mock import patch + +from generate_ai_usage_pdf import GenerationError, compile_pdf, parse_log, render_tex +from validate_ai_usage_log import validate + +FIXTURE_PATH = ( + Path(__file__).resolve().parents[1] / "tests" / "fixtures" / "valid_multi_tool.md" +) +TEMPLATE_LOG_PATH = ( + Path(__file__).resolve().parents[1] / "templates" / "AI_USAGE_LOG.md" +) +VALID_MULTI_TOOL_LOG = FIXTURE_PATH.read_text(encoding="utf-8") + + +class DisclosureGenerationTests(unittest.TestCase): + """Verify stable mapping, safety gates, and LaTeX rendering.""" + + def setUp(self) -> None: + self.temp_dir = tempfile.TemporaryDirectory() + self.addCleanup(self.temp_dir.cleanup) + self.root = Path(self.temp_dir.name) + self.log_path = self.root / "AI_USAGE_LOG.md" + self.log_path.write_text(VALID_MULTI_TOOL_LOG, encoding="utf-8") + self.template = ( + Path(__file__).resolve().parents[1] / "templates" / "AI工具使用详情模板.tex" + ) + + def test_final_log_is_valid_and_ids_map_to_records(self) -> None: + self.assertEqual(validate(self.log_path, "finalize"), []) + data = parse_log(self.log_path) + self.assertEqual([tool.tool_id for tool in data.tools], ["TOOL-01", "TOOL-02"]) + self.assertEqual(data.records[0].tool_ids, ("TOOL-01",)) + self.assertEqual(data.examples[0].record_ids, ("AI-001",)) + + def test_initial_template_passes_record_mode_only(self) -> None: + record_errors = validate(TEMPLATE_LOG_PATH, "record") + final_errors = validate(TEMPLATE_LOG_PATH, "finalize") + self.assertEqual(record_errors, []) + self.assertTrue(any("占位内容" in error for error in final_errors)) + self.assertTrue(any("尚未标记为“已归纳”" in error for error in final_errors)) + + def test_rendered_tex_has_four_sections_and_no_template_markers(self) -> None: + tex_path = self.root / "supporting_materials" / "AI 工具使用详情.tex" + render_tex(self.log_path, self.template, tex_path) + text = tex_path.read_text(encoding="utf-8") + self.assertNotIn("%%__", text) + self.assertNotIn("待队员确认", text) + self.assertIn("MathModelAgent 桌面 Agent", text) + self.assertIn("运行环境未提供具体版本", text) + self.assertIn("AI-001", text) + self.assertIn("EX-001", text) + self.assertIn("迭代修正:", text) + self.assertIn("核验情况:", text) + self.assertNotIn("人工修改:", text) + self.assertNotIn("人工核验:", text) + self.assertNotIn("未记录到额外人工修改", text) + self.assertIn("一、所用 AI 工具名称、版本或型号", text) + self.assertIn("四、对 AI 输出的采纳、人工修改和核验的主要情况", text) + + def test_language_polishing_is_excluded_from_part_four(self) -> None: + tex_path = self.root / "AI 工具使用详情.tex" + render_tex(self.log_path, self.template, tex_path) + part_four = tex_path.read_text(encoding="utf-8").split( + "四、对 AI 输出的采纳、人工修改和核验的主要情况", 1 + )[1] + self.assertIn("AI-001", part_four) + self.assertNotIn("AI-002", part_four) + + def test_in_progress_record_blocks_generation(self) -> None: + pending = VALID_MULTI_TOOL_LOG.replace( + "- 当前状态:已归纳", "- 当前状态:记录中", 1 + ) + self.log_path.write_text(pending, encoding="utf-8") + with self.assertRaises(GenerationError): + render_tex(self.log_path, self.template, self.root / "out.tex") + + def test_core_model_detail_is_rejected(self) -> None: + unsafe = VALID_MULTI_TOOL_LOG.replace( + "检查实现完整性", "检查 ARIMA 参数为 0.8 的实现完整性" + ) + self.log_path.write_text(unsafe, encoding="utf-8") + errors = validate(self.log_path, "finalize") + self.assertTrue(any("核心建模过程" in error for error in errors)) + + def test_optional_evidenced_human_edit_is_rendered(self) -> None: + with_human_edit = VALID_MULTI_TOOL_LOG.replace( + "- 对应证据文件或产物:reports/VERIFY_REPORT.md", + "- 人工修改内容:根据明确反馈调整了章节顺序\n" + "- 人工修改依据:当前会话中的明确修改要求\n" + "- 对应证据文件或产物:reports/VERIFY_REPORT.md", + ) + self.log_path.write_text(with_human_edit, encoding="utf-8") + tex_path = self.root / "AI 工具使用详情.tex" + render_tex(self.log_path, self.template, tex_path) + text = tex_path.read_text(encoding="utf-8") + self.assertIn("人工修改:", text) + self.assertIn("根据明确反馈调整了章节顺序", text) + + def test_forbidden_no_human_record_phrase_is_rejected(self) -> None: + unsafe = VALID_MULTI_TOOL_LOG.replace( + "程序运行发现的问题已修正,并重新生成相关产物", + "未记录到额外人工修改", + ) + self.log_path.write_text(unsafe, encoding="utf-8") + errors = validate(self.log_path, "finalize") + self.assertTrue(any("禁止的无人工记录提示" in error for error in errors)) + + def test_non_contiguous_tool_ids_fail_validation(self) -> None: + invalid = VALID_MULTI_TOOL_LOG.replace("TOOL-02", "TOOL-03") + self.log_path.write_text(invalid, encoding="utf-8") + errors = validate(self.log_path, "record") + self.assertIn("工具编号必须从 1 开始连续递增", errors) + + def test_missing_compilers_fail_with_actionable_error(self) -> None: + tex_path = self.root / "AI 工具使用详情.tex" + tex_path.write_text("test", encoding="utf-8") + with ExitStack() as stack: + stack.enter_context( + patch("generate_ai_usage_pdf.shutil.which", return_value=None) + ) + stack.enter_context( + self.assertRaisesRegex(GenerationError, "缺少 PDF 编译器") + ) + compile_pdf(tex_path) + + +if __name__ == "__main__": + unittest.main() diff --git a/skills/7ai-disclosure/scripts/validate_ai_usage_log.py b/skills/7ai-disclosure/scripts/validate_ai_usage_log.py new file mode 100644 index 00000000..f6cb804d --- /dev/null +++ b/skills/7ai-disclosure/scripts/validate_ai_usage_log.py @@ -0,0 +1,384 @@ +#!/usr/bin/env python3 +"""Validate the structured Markdown log used by 7ai-disclosure.""" + +from __future__ import annotations + +import argparse +import re +import sys +import tempfile +from pathlib import Path + +PENDING_MARKERS = ( + "待队员确认", + "请填写", + "TODO", + "PLACEHOLDER", + "示例工具", + "示例模型", + "示例型号", +) +NO_HUMAN_RECORD_PHRASES = ( + "未记录到额外人工修改", + "未记录人工修改", + "无人工修改记录", + "没有人工修改记录", + "未发现人工修改", +) +SECRET_PATTERNS = { + "OpenAI 风格密钥": re.compile(r"\bsk-[A-Za-z0-9_-]{12,}\b"), + "GitHub 令牌": re.compile(r"\b(?:ghp|github_pat)_[A-Za-z0-9_]{12,}\b"), + "Tavily 密钥": re.compile(r"\btvly-[A-Za-z0-9_-]{12,}\b"), + "Bearer 令牌": re.compile(r"\bBearer\s+[A-Za-z0-9._~+/-]{12,}", re.IGNORECASE), + "环境变量密钥": re.compile(r"\b[A-Z][A-Z0-9_]*(?:API_KEY|TOKEN|SECRET)\s*=\s*\S+"), +} +CORE_DETAIL_PATTERNS = { + "公式或变量表达式": re.compile( + r"(?:\\(?:frac|sum|int|begin)\b|\$[^$\n]+\$|\b[A-Za-z]\w*_\d+\b|" + r"\b[A-Za-z][A-Za-z0-9_]*\s*=\s*-?\d)" + ), + "具体数值结果": re.compile( + r"(?:最终结果|最优值|预测值|权重|参数|误差|准确率|目标函数值)" + r"[^。;\n]{0,16}-?\d+(?:\.\d+)?" + ), + "特定模型或算法名称": re.compile( + r"(?:AHP|TOPSIS|ARIMA|LSTM|XGBoost|NSGA(?:-?II)?|" + r"随机森林|层次分析法|熵权法|遗传算法|模拟退火|微分方程|" + r"线性规划|整数规划)", + re.IGNORECASE, + ), + "内部项目路径": re.compile( + r"(?:^|[\s((])(?:code|results|paper|figures|reports)[/\\][^\s,。;))]+", + re.IGNORECASE, + ), +} +HUMAN_CLAIM_PATTERN = re.compile( + r"(?:队员|手工|参赛者|团队成员|人工(?:复算|检查|核验|修改|审阅|调整|确认|验证))" +) + +REQUIRED_HEADINGS = ("AI 工具目录", "AI 使用记录") +REQUIRED_TOOL = ("工具名称", "版本或型号") +REQUIRED_RECORD = ( + "工具编号", + "使用环节", + "具体使用目的", + "是否仅用于语言润色", + "主要提示方式", + "使用过程说明", + "AI 输出采纳情况", + "对应证据文件或产物", + "当前状态", +) +REQUIRED_REVIEW = ("迭代修正情况", "核验方法与结果") +OPTIONAL_HUMAN = ("人工修改内容", "人工修改依据") +REQUIRED_EXAMPLE = ("对应记录", "代表性提示摘要", "AI 输出摘要", "处理结果") +PUBLIC_DISCLOSURE_FIELDS = ( + "使用环节", + "具体使用目的", + "主要提示方式", + "使用过程说明", + "AI 输出采纳情况", + "迭代修正情况", + "核验方法与结果", + "人工修改内容", + "代表性提示摘要", + "AI 输出摘要", + "处理结果", +) +ID_PATTERN = re.compile( + r"^###\s+((?:TOOL-\d{2})|(?:AI-\d{3})|(?:EX-\d{3}))\s*$", re.MULTILINE +) + + +def parse_fields(text: str) -> dict[str, list[str]]: + fields: dict[str, list[str]] = {} + for line in text.splitlines(): + match = re.match(r"^\s*-\s*([^::]+)[::]\s*(.*?)\s*$", line) + if match: + fields.setdefault(match.group(1).strip(), []).append(match.group(2).strip()) + return fields + + +def id_sections(text: str) -> list[tuple[str, str]]: + matches = list(ID_PATTERN.finditer(text)) + result: list[tuple[str, str]] = [] + for index, match in enumerate(matches): + end = matches[index + 1].start() if index + 1 < len(matches) else len(text) + result.append((match.group(1), text[match.end() : end])) + return result + + +def missing_fields( + fields: dict[str, list[str]], required: tuple[str, ...] +) -> list[str]: + return [name for name in required if name not in fields or not any(fields[name])] + + +def is_pending(value: str) -> bool: + return ( + not value.strip() + or any(marker.casefold() in value.casefold() for marker in PENDING_MARKERS) + or bool(re.search(r"【[^】]*】", value)) + ) + + +def expected_ids(prefix: str, count: int, width: int) -> list[str]: + """Build the required contiguous identifiers for one entry type.""" + return [f"{prefix}-{index:0{width}d}" for index in range(1, count + 1)] + + +def validate_public_content( + owner: str, fields: dict[str, list[str]], errors: list[str] +) -> None: + """Reject details or claims that must not enter the public disclosure.""" + for field_name in PUBLIC_DISCLOSURE_FIELDS: + for value in fields.get(field_name, []): + for phrase in NO_HUMAN_RECORD_PHRASES: + if phrase in value: + errors.append( + f"{owner} {field_name} 含禁止的无人工记录提示:{phrase}" + ) + if field_name not in OPTIONAL_HUMAN and HUMAN_CLAIM_PATTERN.search(value): + errors.append( + f"{owner} {field_name} 将过程表述为人工行为;仅有明确会话依据时" + "才可使用人工修改专用字段" + ) + for label, pattern in CORE_DETAIL_PATTERNS.items(): + if pattern.search(value): + errors.append( + f"{owner} {field_name} 含{label},请改为不泄露核心建模过程的阶段概括" + ) + + +def validate(path: Path, mode: str) -> list[str]: + if not path.is_file(): + return [f"日志不存在:{path}"] + + text = path.read_text(encoding="utf-8-sig") + errors: list[str] = [] + + for heading in REQUIRED_HEADINGS: + if not re.search(rf"^##\s+{re.escape(heading)}\s*$", text, flags=re.MULTILINE): + errors.append(f"缺少二级标题:{heading}") + + for label, pattern in SECRET_PATTERNS.items(): + if pattern.search(text): + errors.append(f"检测到疑似{label},请删除或脱敏") + + sections = id_sections(text) + tools = [ + (item_id, block) for item_id, block in sections if item_id.startswith("TOOL-") + ] + records = [ + (item_id, block) for item_id, block in sections if item_id.startswith("AI-") + ] + examples = [ + (item_id, block) for item_id, block in sections if item_id.startswith("EX-") + ] + tool_ids = [item_id for item_id, _ in tools] + record_ids = [item_id for item_id, _ in records] + example_ids = [item_id for item_id, _ in examples] + + if not tools: + errors.append("至少需要一个 TOOL-xx 工具条目") + if not records: + errors.append("至少需要一个 AI-xxx 使用记录") + for label, ids in ( + ("工具", tool_ids), + ("使用记录", record_ids), + ("典型交互", example_ids), + ): + if len(ids) != len(set(ids)): + errors.append(f"{label}编号重复") + for label, ids, prefix, width in ( + ("工具", tool_ids, "TOOL", 2), + ("使用记录", record_ids, "AI", 3), + ("典型交互", example_ids, "EX", 3), + ): + if ids and ids != expected_ids(prefix, len(ids), width): + errors.append(f"{label}编号必须从 1 开始连续递增") + + for tool_id, block in tools: + fields = parse_fields(block) + for name in missing_fields(fields, REQUIRED_TOOL): + errors.append(f"{tool_id} 缺少字段或值:{name}") + + for record_id, block in records: + fields = parse_fields(block) + for name in missing_fields(fields, REQUIRED_RECORD): + errors.append(f"{record_id} 缺少字段或值:{name}") + + references = re.findall(r"TOOL-\d{2}", " ".join(fields.get("工具编号", []))) + if not references: + errors.append(f"{record_id} 未引用 TOOL-xx") + for reference in references: + if reference not in tool_ids: + errors.append(f"{record_id} 引用了不存在的工具:{reference}") + + language_only = " ".join(fields.get("是否仅用于语言润色", [])) + if language_only and language_only not in {"是", "否"}: + errors.append(f"{record_id} “是否仅用于语言润色”只能是“是”或“否”") + if language_only != "是": + for name in missing_fields(fields, REQUIRED_REVIEW): + errors.append(f"{record_id} 非纯语言润色,缺少字段或值:{name}") + + human_edit = " ".join(fields.get("人工修改内容", [])).strip() + human_basis = " ".join(fields.get("人工修改依据", [])).strip() + if bool(human_edit) != bool(human_basis): + errors.append( + f"{record_id} 人工修改内容与人工修改依据必须同时填写或同时省略" + ) + + status = " ".join(fields.get("当前状态", [])) + if status and status not in {"记录中", "已归纳"}: + errors.append(f"{record_id} 当前状态只能是“记录中”或“已归纳”") + validate_public_content(record_id, fields, errors) + + for example_id, block in examples: + fields = parse_fields(block) + for name in missing_fields(fields, REQUIRED_EXAMPLE): + errors.append(f"{example_id} 缺少字段或值:{name}") + references = re.findall(r"AI-\d{3}", " ".join(fields.get("对应记录", []))) + if not references: + errors.append(f"{example_id} 未引用 AI-xxx") + for reference in references: + if reference not in record_ids: + errors.append(f"{example_id} 引用了不存在的记录:{reference}") + validate_public_content(example_id, fields, errors) + + if mode == "finalize": + for field_name, values in parse_fields(text).items(): + for value in values: + if is_pending(value): + errors.append(f"最终生成前仍有待确认或占位内容:{field_name}") + for record_id, block in records: + fields = parse_fields(block) + if " ".join(fields.get("当前状态", [])) != "已归纳": + errors.append(f"{record_id} 尚未标记为“已归纳”") + + return list(dict.fromkeys(errors)) + + +VALID_SAMPLE = """# AI 工具使用过程记录 +## AI 工具目录 +### TOOL-01 +- 工具名称:MathModelAgent 桌面 Agent +- 版本或型号:运行环境未提供具体版本 +## AI 使用记录 +### AI-001 +- 工具编号:TOOL-01 +- 使用环节:代码复核 +- 具体使用目的:检查边界条件 +- 是否仅用于语言润色:否 +- 主要提示方式:提供代码和约束,请列出边界风险 +- 使用过程说明:生成检查清单后与实际产物逐项对照 +- AI 输出采纳情况:经文件对照后选择性纳入最终稿 +- 迭代修正情况:程序运行发现的问题已修正并重新生成相关产物 +- 核验方法与结果:完成代码复现、异常检查和正文结果一致性核对,检查通过 +- 对应证据文件或产物:tests/test_boundary.py +- 当前状态:已归纳 +## 典型交互示例(可选) +### EX-001 +- 对应记录:AI-001 +- 代表性提示摘要:检查边界条件 +- AI 输出摘要:列出三类风险 +- 处理结果:与现有产物对照后,仅保留有运行记录支持的建议 +""" + + +def self_test() -> int: + language_sample = VALID_SAMPLE.replace( + "- 是否仅用于语言润色:否", "- 是否仅用于语言润色:是" + ) + language_sample = language_sample.replace( + "- 迭代修正情况:程序运行发现的问题已修正并重新生成相关产物\n", + "", + ) + language_sample = language_sample.replace( + "- 核验方法与结果:完成代码复现、异常检查和正文结果一致性核对,检查通过\n", + "", + ) + cases = [ + ("valid", VALID_SAMPLE, "finalize", False), + ("language_only", language_sample, "finalize", False), + ( + "pending", + VALID_SAMPLE.replace("- 当前状态:已归纳", "- 当前状态:记录中"), + "finalize", + True, + ), + ( + "bad_ref", + VALID_SAMPLE.replace("- 工具编号:TOOL-01", "- 工具编号:TOOL-99"), + "record", + True, + ), + ( + "secret", + VALID_SAMPLE + "\n- 备注:sk-abcdefghijklmnopqrstuvwxyz\n", + "record", + True, + ), + ( + "missing_review", + VALID_SAMPLE.replace( + "- 核验方法与结果:完成代码复现、异常检查和正文结果一致性核对,检查通过\n", + "", + ), + "record", + True, + ), + ( + "fabricated_human_claim", + VALID_SAMPLE.replace("完成代码复现", "队员手工复算并完成代码复现"), + "record", + True, + ), + ( + "core_detail", + VALID_SAMPLE.replace("检查边界条件", "检查 ARIMA 参数为 0.8 的结果"), + "record", + True, + ), + ( + "no_human_record_phrase", + VALID_SAMPLE.replace("程序运行发现的问题已修正", "未记录到额外人工修改"), + "record", + True, + ), + ("non_contiguous", VALID_SAMPLE.replace("TOOL-01", "TOOL-02"), "record", True), + ] + with tempfile.TemporaryDirectory() as tmpdir: + for name, content, mode, should_fail in cases: + path = Path(tmpdir) / f"{name}.md" + path.write_text(content, encoding="utf-8") + failed = bool(validate(path, mode)) + if failed != should_fail: + print(f"SELF-TEST FAIL: {name}") + return 1 + print("SELF-TEST PASS") + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description="校验 AI 工具使用过程记录") + parser.add_argument("path", nargs="?", type=Path, help="AI_USAGE_LOG.md 路径") + parser.add_argument("--mode", choices=("record", "finalize"), default="finalize") + parser.add_argument("--self-test", action="store_true") + args = parser.parse_args() + if args.self_test: + return self_test() + if args.path is None: + parser.error("除 --self-test 外,必须提供日志路径") + errors = validate(args.path, args.mode) + if errors: + for error in errors: + print(f"ERROR: {error}") + print(f"VALIDATION FAILED: {len(errors)} error(s)") + return 1 + print(f"VALIDATION PASS ({args.mode})") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git "a/skills/7ai-disclosure/templates/AI \345\267\245\345\205\267\344\275\277\347\224\250\350\257\246\346\203\205.pdf" "b/skills/7ai-disclosure/templates/AI \345\267\245\345\205\267\344\275\277\347\224\250\350\257\246\346\203\205.pdf" new file mode 100644 index 00000000..956e2304 Binary files /dev/null and "b/skills/7ai-disclosure/templates/AI \345\267\245\345\205\267\344\275\277\347\224\250\350\257\246\346\203\205.pdf" differ diff --git a/skills/7ai-disclosure/templates/AI_USAGE_LOG.md b/skills/7ai-disclosure/templates/AI_USAGE_LOG.md new file mode 100644 index 00000000..e46ebbd3 --- /dev/null +++ b/skills/7ai-disclosure/templates/AI_USAGE_LOG.md @@ -0,0 +1,30 @@ +# AI 工具使用过程记录 + +> 本记录由工作流根据会话、运行时元数据、文件产物和检查结果自动维护。只记录实际发生且可由任务证据支持的使用,不写入密钥、完整聊天记录、核心建模细节或模型私有思维链。 + +## AI 工具目录 + +### TOOL-01 + +- 工具名称:MathModelAgent 桌面 Agent +- 版本或型号:运行环境未提供具体版本 + +## AI 使用记录 + +### AI-001 + +- 工具编号:TOOL-01 +- 使用环节:任务启动与全流程编排 +- 具体使用目的:建立任务计划、组织阶段产物并维护检查记录 +- 是否仅用于语言润色:否 +- 主要提示方式:【自动概括本次任务的输入类型和输出要求,不写题目细节】 +- 使用过程说明:【自动概括实际执行阶段,不写模型、公式、参数或结果】 +- AI 输出采纳情况:【根据最终文件和修改记录自动归纳】 +- 迭代修正情况:【根据重新运行和文件修改证据自动归纳】 +- 核验方法与结果:【根据实际检查记录自动归纳】 +- 对应证据文件或产物:plan.md;todo.md +- 当前状态:记录中 + +## 典型交互示例(可选) + + diff --git "a/skills/7ai-disclosure/templates/AI\345\267\245\345\205\267\344\275\277\347\224\250\350\257\246\346\203\205\346\250\241\346\235\277.tex" "b/skills/7ai-disclosure/templates/AI\345\267\245\345\205\267\344\275\277\347\224\250\350\257\246\346\203\205\346\250\241\346\235\277.tex" new file mode 100644 index 00000000..9ec9f32e --- /dev/null +++ "b/skills/7ai-disclosure/templates/AI\345\267\245\345\205\267\344\275\277\347\224\250\350\257\246\346\203\205\346\250\241\346\235\277.tex" @@ -0,0 +1,85 @@ +% !TeX program = xelatex +% 由 generate_ai_usage_pdf.py 根据自动归纳日志填充;不要直接提交本源文件。 +\documentclass[UTF8,a4paper,11pt]{ctexart} +\usepackage[a4paper,top=2.0cm,bottom=2.0cm,left=2.15cm,right=2.15cm]{geometry} +\usepackage{array,booktabs,longtable,fancyhdr,lastpage} +\usepackage[table]{xcolor} +\usepackage[hidelinks]{hyperref} + +\definecolor{accent}{HTML}{315B7D} +\definecolor{sectionbg}{HTML}{EDF3F7} +\definecolor{tablehead}{HTML}{E4EDF3} +\definecolor{rulegray}{HTML}{AAB8C2} +\definecolor{bodytext}{HTML}{263746} +\definecolor{mutedtext}{HTML}{667580} +\newcolumntype{L}[1]{>{\raggedright\arraybackslash}p{#1}} +\newcommand{\sectiontitle}[1]{% + \par\vspace{0.65em}\noindent + \begingroup\setlength{\fboxsep}{7pt}% + \colorbox{sectionbg}{\parbox{\dimexpr\textwidth-2\fboxsep\relax}{% + \textcolor{accent}{\rule{3pt}{1.15em}\hspace{0.65em}\bfseries\zihao{-3}#1}}}% + \endgroup\par\vspace{0.5em}} +\newcommand{\recordheading}[2]{% + \vspace{0.25em}\noindent\textcolor{accent}{\bfseries 使用记录 #1(#2)}\par + \vspace{0.08em}{\color{rulegray}\hrule height 0.35pt}\vspace{0.2em}} +\renewcommand{\arraystretch}{1.3} +\setlength{\parindent}{0pt} +\setlength{\parskip}{0.35em} +\setlength{\LTpre}{0.2em} +\setlength{\LTpost}{0.25em} +\arrayrulecolor{rulegray} + +\pagestyle{fancy} +\setlength{\headheight}{24pt} +\addtolength{\topmargin}{-7pt} +\fancyhf{} +\fancyhead[L]{\textcolor{accent}{\bfseries AI 工具使用详情}} +\fancyfoot[C]{\textcolor{mutedtext}{第 \thepage\ 页,共 \pageref{LastPage} 页}} +\renewcommand{\headrulewidth}{0pt} +\renewcommand{\headrule}{\hbox to\headwidth{\color{accent}\leaders\hrule height 0.6pt\hfill}} +\renewcommand{\footrulewidth}{0pt} + +\begin{document} +\color{bodytext} + +\begin{center} + {\zihao{2}\bfseries\color{accent}AI 工具使用详情}\par + \vspace{0.3em} + {\color{rulegray}\rule{0.28\textwidth}{0.7pt}} +\end{center} + +\sectiontitle{一、所用 AI 工具名称、版本或型号} +\begin{longtable}{@{}L{2.1cm}L{6.2cm}L{7.1cm}@{}} +\toprule +\rowcolor{tablehead}\textcolor{accent}{\textbf{编号}} & \textcolor{accent}{\textbf{AI 工具名称}} & \textcolor{accent}{\textbf{版本或型号}} \\ +\midrule +\endfirsthead +\toprule +\rowcolor{tablehead}\textcolor{accent}{\textbf{编号}} & \textcolor{accent}{\textbf{AI 工具名称}} & \textcolor{accent}{\textbf{版本或型号}} \\ +\midrule +\endhead +%%__TOOLS_ROWS__%% +\bottomrule +\end{longtable} + +\sectiontitle{二、具体使用目的和环节} +\begin{longtable}{@{}L{3.0cm}L{4.2cm}L{8.2cm}@{}} +\toprule +\rowcolor{tablehead}\textcolor{accent}{\textbf{工具编号}} & \textcolor{accent}{\textbf{使用环节}} & \textcolor{accent}{\textbf{具体使用目的}} \\ +\midrule +\endfirsthead +\toprule +\rowcolor{tablehead}\textcolor{accent}{\textbf{工具编号}} & \textcolor{accent}{\textbf{使用环节}} & \textcolor{accent}{\textbf{具体使用目的}} \\ +\midrule +\endhead +%%__PURPOSE_ROWS__%% +\bottomrule +\end{longtable} + +\sectiontitle{三、主要提示方式与使用过程说明} +%%__PROCESS_BLOCKS__%% + +\sectiontitle{四、对 AI 输出的采纳、人工修改和核验的主要情况} +%%__ADOPTION_BLOCKS__%% + +\end{document} diff --git a/skills/7ai-disclosure/tests/fixtures/valid_multi_tool.md b/skills/7ai-disclosure/tests/fixtures/valid_multi_tool.md new file mode 100644 index 00000000..83bbb880 --- /dev/null +++ b/skills/7ai-disclosure/tests/fixtures/valid_multi_tool.md @@ -0,0 +1,50 @@ +# AI 工具使用过程记录 + +## AI 工具目录 + +### TOOL-01 + +- 工具名称:MathModelAgent 桌面 Agent +- 版本或型号:运行环境未提供具体版本 + +### TOOL-02 + +- 工具名称:ChatGPT +- 版本或型号:运行环境未提供具体版本 + +## AI 使用记录 + +### AI-001 + +- 工具编号:TOOL-01 +- 使用环节:方案梳理与实现检查 +- 具体使用目的:辅助梳理任务结构并检查实现完整性 +- 是否仅用于语言润色:否 +- 主要提示方式:提供任务材料类型与交付要求,要求给出可复现的阶段建议 +- 使用过程说明:先形成阶段草稿,再根据程序运行和文件检查结果迭代 +- AI 输出采纳情况:经文件对照后选择性纳入最终稿 +- 迭代修正情况:程序运行发现的问题已修正,并重新生成相关产物 +- 核验方法与结果:完成代码复现、异常检查和正文结果一致性核对,检查通过 +- 对应证据文件或产物:reports/VERIFY_REPORT.md +- 当前状态:已归纳 + +### AI-002 + +- 工具编号:TOOL-02 +- 使用环节:摘要语言润色 +- 具体使用目的:压缩摘要并统一术语 +- 是否仅用于语言润色:是 +- 主要提示方式:提供原摘要并限制字数 +- 使用过程说明:比较修改前后文本,保持原有信息范围不变 +- AI 输出采纳情况:部分采纳措辞 +- 对应证据文件或产物:paper/abstract.tex +- 当前状态:已归纳 + +## 典型交互示例(可选) + +### EX-001 + +- 对应记录:AI-001 +- 代表性提示摘要:检查实现完整性、结果一致性和表达缺漏 +- AI 输出摘要:给出风险项和修订建议摘要 +- 处理结果:与现有产物对照后,仅保留有运行记录支持的建议 diff --git a/skills/doctor/SKILL.md b/skills/doctor/SKILL.md index e8958e18..49ac8b77 100644 --- a/skills/doctor/SKILL.md +++ b/skills/doctor/SKILL.md @@ -16,6 +16,7 @@ allowed-tools: Bash(*), Read, Write | --- | --- | --- | | `typst` | 论文编译(5writing、6verity,Typst 引擎) | `command -v typst` | | `xelatex` | 论文编译(5writing、6verity,LaTeX 引擎,中文模板必需) | `command -v xelatex` | +| `tectonic` | AI 使用详情 PDF 的备用 LaTeX 编译器 | `command -v tectonic` | | `python3` | 数值计算与图表(3coding-visual) | `command -v python3` | | `drawio` / `draw.io` | DrawIO 流程图导出 PDF(4drawio) | `command -v drawio \|\| command -v draw.io` | | `pdftoppm` | PDF 转 PNG 视觉检查(6verity) | `command -v pdftoppm` | @@ -32,6 +33,7 @@ allowed-tools: Bash(*), Read, Write | `matplotlib` | 图表生成 | | `scikit-learn` | 机器学习建模 | | `openpyxl` | 读写 Excel 数据附件 | +| `PyMuPDF`(导入名 `fitz`) | 检查 AI 使用详情 PDF 的页数、A4 尺寸和文本层 | ## 工作流程 @@ -74,6 +76,7 @@ check_cmd() { check_cmd typst check_cmd xelatex +check_cmd tectonic check_cmd python3 || check_cmd python # Windows 上可能是 python command -v drawio >/dev/null 2>&1 || command -v draw.io >/dev/null 2>&1 \ && echo "OK drawio" || echo "MISS drawio" @@ -81,21 +84,27 @@ check_cmd pdftoppm check_cmd mutool check_cmd magick -python3 - <<'PYEOF' +PYTHON_BIN="$(command -v python3 || command -v python)" +if [ -n "$PYTHON_BIN" ]; then +"$PYTHON_BIN" - <<'PYEOF' import importlib -pkgs = ["numpy", "scipy", "pandas", "matplotlib", "sklearn", "openpyxl"] +pkgs = ["numpy", "scipy", "pandas", "matplotlib", "sklearn", "openpyxl", "fitz"] for p in pkgs: try: importlib.import_module(p) import importlib.metadata as meta try: - ver = meta.version(p if p != "sklearn" else "scikit-learn") + dist = {"sklearn": "scikit-learn", "fitz": "PyMuPDF"}.get(p, p) + ver = meta.version(dist) except Exception: ver = "?" print(f"OK {p} ({ver})") except ImportError: print(f"MISS {p}") PYEOF +else + echo "MISS python3/python" +fi ``` ### Step 3:输出检查报告 @@ -112,7 +121,9 @@ PYEOF ... ``` -**必须项:** typst 或 xelatex(至少一个论文编译器)、python3、numpy、pandas、matplotlib +**通用必须项:** typst 或 xelatex(至少一个论文编译器)、python3、numpy、pandas、matplotlib + +**国赛 AI 支撑材料附加必须项:** `7ai-disclosure/SKILL.md` 与其模板、脚本完整,xelatex 或 tectonic 至少一个,PyMuPDF 可导入。 **可选项:** drawio、pdftoppm/mutool/magick 三选一、scipy、scikit-learn、openpyxl ### Step 4:提供安装命令(按平台) @@ -220,6 +231,7 @@ Doctor 检查完成(macOS) 4drawio ⚠ drawio 未安装,PDF 导出将跳过 5writing ✓(typst ✓,xelatex ✓) 6verity ⚠ 无 PDF 转 PNG 工具,视觉检查将跳过 + 7ai-disclosure ✓(python ✓,xelatex ✓,PyMuPDF ✓) ``` ## 注意事项 @@ -228,4 +240,5 @@ Doctor 检查完成(macOS) - Windows 下建议在 PowerShell(管理员)或 Git Bash 中运行,部分命令需要管理员权限。 - Linux 的 `sudo` 命令会请求密码,执行前告知用户。 - drawio 和 PDF 转 PNG 工具缺失不影响核心工作流,仅影响导出质量。 +- 国赛任务缺少 `7ai-disclosure`、xelatex/tectonic 或 PyMuPDF 时,不得报告最终提交包已完成。 - 如平台检测为 unknown,打印所有平台命令供用户手动选择。 diff --git a/skills/skills.sh.json b/skills/skills.sh.json index 58d26a52..b3e7e2ef 100644 --- a/skills/skills.sh.json +++ b/skills/skills.sh.json @@ -10,7 +10,8 @@ "3coding-visual", "4drawio", "5writing", - "6verity" + "6verity", + "7ai-disclosure" ] }, {