commit 1d78e542cac5bc6444195ab5931d48ca83e9a81b Author: taojianmin Date: Mon Sep 14 12:19:12 2026 +0800 v1.0.1: 课程教学大纲合规性体检 - 初版骨架 基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。 核心业务: - 教师在教务系统提交新开课或修订大纲时自动触发 - 格式结构 + 课程目标与毕业要求矩阵匹配 + 课程思政融合度三维体检 - 以批注模式嵌入原 Word 文档,文末给出百分制评分 输出形态: - Word 文档红 / 黄 / 绿三色批注 - 百分制体检评分 + HTML 体检报告 + 修改建议清单 预期提效:教务处培养方案审核环节的返工次数减少 70%;大纲编制的规范性达到 95% 以上。 业务行生命周期:无(一次性体检结果存档,不重复处理) Co-Authored-By: Claude Code diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..0d12dc2 --- /dev/null +++ b/.env.example @@ -0,0 +1,26 @@ +# 课程教学大纲合规性体检 - 配置示例 +# 行头说明「是什么」;行尾说明「怎么填」;进程环境变量 > 用户 .env > 本文件 + +# 教务系统 API 入口,例如 https://aa.example.edu/api/v1 +AA_API_BASE_URL= + +# 教务系统 API 调用凭证(Bearer Token),由教务处统一发放 +AA_API_TOKEN= + +# 课程思政关键词列表(JSON 数组),如 ["家国情怀","科学伦理","工匠精神","社会责任"] +IDEOLOGY_KEYWORDS=["家国情怀","科学伦理","工匠精神","社会责任"] + +# 建议直接送审分数阈值,默认 85 +SCORE_PASS_THRESHOLD=85 + +# 建议修改后送审分数阈值,默认 60 +SCORE_MODIFY_THRESHOLD=60 + +# 单份大纲体检超时(秒),默认 60 +AUDIT_TIMEOUT_SEC=60 + +# 语义相似度阈值,课程目标与毕业要求指标点匹配判定,默认 0.7 +MATRIX_MATCH_THRESHOLD=0.7 + +# 错误码前缀,统一为 ERROR: 起头 +LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08d4ec6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.py[cod] +.env +*.env.local +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ \ No newline at end of file diff --git a/.openclaw-skill-template b/.openclaw-skill-template new file mode 100644 index 0000000..5464d21 --- /dev/null +++ b/.openclaw-skill-template @@ -0,0 +1,5 @@ +# 此文件仅存在于 skill-template 源仓库。 +# 用 tools/scaffold_skill 创建新技能时,脚本会从目标目录删除本文件。 +# 若业务技能仓库中仍存在本文件,说明未正确 scaffold,tests 会失败。 +template_slug: your-skill-slug +template_version: "1.0.32" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a1c96bb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +本文件对应技能市场 **「更新日志」** Tab。 + +## 1.0.1 + +- 初版:基于匠厂 skill-template 主仓 v1.0.108 派生 +- 实现核心业务:课程教学大纲合规性体检(格式结构 + 课程目标与毕业要求矩阵匹配 + 课程思政融合度) +- 输出形态:原 Word 文档红黄绿三色批注 + 百分制体检评分 + HTML 体检报告 + 修改建议清单 +- 预期提效:教务处培养方案审核环节的返工次数减少 70%;大纲编制的规范性达到 95% 以上(一次性通过校级抽查) +- 业务行生命周期:无(一次性体检结果存档,不重复处理) +- 占位说明:本版仅完成骨架与文档,业务实现细节(`scripts/` 下 Python 模块、NLP 语义相似度模型选型、Word 批注回写策略)由开发者后续按 `development/REQUIREMENTS.md` 补全 diff --git a/DEMO.md b/DEMO.md new file mode 100644 index 0000000..b425bae --- /dev/null +++ b/DEMO.md @@ -0,0 +1,28 @@ +--- +demo_video_url: "" +--- + +# 演示说明 + +> 本技能当前未提供演示视频。下方用图文形式描述完整体检流程与结果样例。 + +## 演示看点 + +1. **数据管理 → 课程大纲 → 体检这份大纲**:选中待体检的行,30 秒~1 分钟内完成 +2. **结果样例**: + - 原 Word 文档出现红 / 黄 / 绿三色批注(红 = 不达标、黄 = 边缘、绿 = 通过) + - 文末给出百分制评分(如「82 分,建议补充课程思政元素后重新提交」) + - 体检记录表新增一行,含「不达标项清单 + 评分细则」 + - 大纲表状态自动更新为「待修改」 + +## 没有视频时 + +可参考下方截图位(待补): + +- 截图位 1:Word 文档红黄绿三色批注 +- 截图位 2:体检记录表(评分 + 不达标项清单) +- 截图位 3:HTML 体检报告(综合评分 + 修改建议) + +## 何时补视频 + +v1.1.0 起,按 `development/TUTORIAL_SURFACES.md` §3 录制 1~2 分钟演示视频,覆盖上传大纲 → 触发体检 → 查看批注 → 查看评分报告的全流程。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb90574 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 深圳匠厂科技有限公司 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..d6de3d4 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +--- +description: "教师在教务系统提交新开课或修订大纲时,自动检查格式结构、课程目标与毕业要求矩阵匹配、课程思政融合度,以批注模式嵌入 Word 文档并给出百分制体检评分。" +--- + +# 课程教学大纲合规性体检 + +教师在教务系统提交新开课或修订大纲时,技能会自动按《课程大纲编制规范》逐章检查格式结构,把课程目标与人才培养方案中的毕业要求指标点做语义匹配,并统计课程思政关键词的融入度。检查结果以批注模式直接嵌入原 Word 文档,文末给出百分制评分和修改建议清单,让教师在送审前就能自纠自改,把教务处审核环节的返工次数减少 70%。 + +--- + +## 这个技能能帮你做什么 + +- **格式与结构审查**:检查大纲是否包含「课程简介、教学目标、教学内容、教学方法、考核方式、推荐教材」等必备章节,并检测各章节字数是否严重失衡 +- **课程目标与毕业要求矩阵匹配**:用 NLP 语义相似度把课程目标与人才培养方案中的毕业要求指标点一一对齐,发现"脱节"项 +- **课程思政融合度检测**:统计「家国情怀、科学伦理、工匠精神、社会责任」等思政关键词的出现频次与上下文语境,识别"仅在后记提及"的伪融合 +- **批注 + 评分双输出**:以批注模式直接嵌入原 Word 文档,文末给出百分制体检评分 +- **修改建议清单**:列出每项不达标项的具体修改建议与规则引用超链接 + +--- + +## 适合什么场景 + +- 课程负责人新开课或修订课程大纲,需要在送审前自检 +- 教研室主任在二级学院内审时,需要批量体检本专业所有大纲 +- 二级学院教学秘书需要快速识别"问题大纲",优先安排复审 +- 教务处需要在 9 月开学前对全校新开课大纲做一次集中体检 + +--- + +## 使用前需要准备什么 + +- 教务系统 API 凭证(`AA_API_TOKEN`)已配置到本技能 `.env` +- 教师撰写的《课程教学大纲》Word 文档(doc/docx) +- 对应专业最新版《人才培养方案》PDF(含毕业要求指标点) +- 学校《课程大纲编制规范》红头文件(含字数、格式、课程思政比例等模板要求) + +--- + +## 怎么使用 + +本技能支持**数据管理**(大纲表上点工具栏按钮)与**对话任务**(自然语言让 Agent 完成)。定时任务为占位,按需启用。 + +**逐步操作与排障,请查看技能市场「使用教程」Tab。** + +--- + +## 执行后你会得到什么 + +- 原 Word 文档的批注模式修改建议(红 / 黄 / 绿三色批注) +- 文末的百分制体检评分(如「82 分,建议补充课程思政元素后重新提交」) +- 一份《教学大纲体检报告》HTML 报告,可下载或直接预览 +- 大纲表状态更新(体检中 → 已通过 / 待修改 / 驳回) + +--- + +## 注意事项 + +- 本技能**不替代教务处终审**,最终审核仍由教务处教学科与教学委员会负责 +- 体检评分仅供参考;评分 ≥ 85 分建议直接送审,60~85 分建议修改后送审,< 60 分必须修改 +- OCR 识别对扫描版人才培养方案 PDF 准确率约 92%,关键指标点可能漏检,建议上传电子版 +- 课程思政关键词列表需与学校马克思主义学院对齐;如有新增主题词,请同步更新配置 + +--- + +## 常见问题 + +### 这个技能适合我吗? + +> 如果你的工作包含「课程大纲撰写或修订」,本技能可以在送审前自动兜住常见格式与对齐问题。 + +### 体检评分多少分能直接送审? + +> 建议 ≥ 85 分直接送审;60~85 分建议先修改再送审;< 60 分必须修改。建议教研室主任对 60~85 分区间做重点复核。 + +### 课程思政关键词不够怎么办? + +> 体检报告会列出缺失的关键词类别(如「家国情怀 0 次」「工匠精神 0 次」),并给出融入建议(如「在第 3 章教学内容中增加 XX 案例」)。 + +### 已经体检过的大纲能再体检一次吗? + +> 可以。修订完成后再次点击「重新体检」,会以最新版本覆盖原结果。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..657e660 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# 安全漏洞披露 + +本仓库是匠厂技能开发模板。请把**未公开的安全漏洞**发给深圳匠厂科技有限公司,不要在公开 Issue、PR 或讨论区里贴完整复现步骤、PoC 或利用细节。 + +## 怎么报告 + +请通过匠厂官网联系渠道提交:[https://www.jc2009.com/](https://www.jc2009.com/) + +建议在联系时注明: + +- 标题或开头带 `[Security]` +- 影响的仓库 / 版本(例如 `skill-template`、tag `v1.0.x`) +- 问题类型、影响范围、复现步骤 +- 若已有修复思路,可一并说明 + +我们会确认收悉,并在有结论后告知是否修复、是否公开披露。 + +## 不在本页范围 + +本页只说明**如何向我们报告漏洞**。技能运行时的权限、RPA 合规、加密发布等产品安全设计,见 `development/` 与 `references/`,不要写进本文件。 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..7881059 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,129 @@ +--- +name: 课程教学大纲合规性体检 +description: "教师在教务系统提交新开课/修订大纲时,自动检查格式结构、课程目标与毕业要求矩阵匹配、课程思政融合度,以批注模式嵌入原 Word 文档并给出百分制体检评分。" +version: 1.0.1 +author: 深圳匠厂科技有限公司 +metadata: + openclaw: + slug: check-course-syllabus-aa + platform_kit_min_version: "1.2.11" + emoji: "📚" + category: "教务" + developer_ids: + - 12345 # 占位,请换成你的匠厂用户 ID + - 12468 +allowed-tools: + - bash +--- + +# 课程教学大纲合规性体检(check-course-syllabus-aa) + +这是基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。覆盖需求规格书第 4.2 节「🟣 教务处 Skill 001 —— 课程教学大纲"合规性体检"」。 + +## Agent / 宿主编排(复制为业务技能后必改) + +### AI + RPA 定位 + +- **Agent 是入口与编排者**:理解用户意图("给《数据结构》大纲做体检"、"查看本月体检合格率")、补齐参数、判断风险、选择 Action,并用业务语言解释结果。 +- **本技能不涉及浏览器 / 桌面 / 手机 RPA**:大纲 Word 文件读取、教学大纲编制规范 PDF 解析、人才培养方案匹配均走教务系统官方 API + 本地 NLP 处理。 +- 开发者与 AI 编程工具先读 `development/AI_RPA.md`。 + +### 架构铁律(先读) + +- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**;本技能**重点挂数据管理 + 对话入口**(教师提交大纲时强制过 AI 体检)。 +- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service。 +- **业务行生命周期**:本技能对「大纲体检记录」无角色约束(一次性体检结果存档,不重复处理)。 +- **HTML 整页报告**:本技能输出体检报告走 `render-html`,并把批注回写到原 Word 文档。 + +### 如何触发(能力类型) + +| 你要做什么 | Agent 怎么做 | 进任务中心? | +|------------|--------------|--------------| +| 健康检查 / 短查询 | OpenClaw bash/exec + 共享 Python 跑 CLI | 否 | +| 已声明 sync 的 Action | `run_skill_action` 或等效短 CLI | 否 | +| 长任务(向量化比对 + Word 回写) | **必须** `run_skill_action` | 是(async) | + +### 用户意图路由表(业务技能复制后必改) + +| 用户意图(本技能自填) | Agent 怎么做 | 进任务中心? | +|------------------------|--------------|--------------| +| 检查运行环境 | 短 CLI:`python {baseDir}/scripts/main.py health` | 否 | +| 查看版本 | 短 CLI:`python {baseDir}/scripts/main.py version` | 否 | +| 查看配置位置 | 短 CLI:`python {baseDir}/scripts/main.py config-path` | 否 | +| 对一份大纲做合规体检 | `run_skill_action` → `audit-syllabus`(输入:`syllabus_id`) | 否(sync) | +| 批量体检某专业所有大纲 | `run_skill_action` → `batch-audit-syllabus`(输入:`major_id`) | 是(async) | +| 查看体检报告 | `run_skill_action` → `render-html`(输入:`audit_id`) | 否(sync) | +| 体检规范文件库更新 | `run_skill_action` → `sync-norm-library`(cron 占位) | 是(async) | + +## 共享 Python(必读) + +宿主共享 runtime 在 `{JIANGCHANG_DATA_ROOT}/python-runtime/.venv`,并已前置到 PATH(同时设 `VIRTUAL_ENV`)。短 CLI 一条命令直接跑: + +```bash +python {baseDir}/scripts/main.py health +python {baseDir}/scripts/main.py version +python {baseDir}/scripts/main.py config-path +python {baseDir}/scripts/main.py init-db +``` + +- 公共库来自共享 venv 的 `jiangchang-platform-kit>=1.2.11`(包名路径 `jiangchang_skill_core`);**不要 vendor** `scripts/jiangchang_skill_core/`。 +- 根目录 `requirements.txt` **只声明技能特有**依赖;不要写 `jiangchang-platform-kit` / `playwright`。 +- `metadata.openclaw.platform_kit_min_version` 是宿主兼容声明,**不是** pip 依赖。 +- `health` 用 `collect_runtime_diagnostics` 做只读诊断。 + +配置:仓库 `.env.example` 为模板;用户 `.env` 在 `{JIANGCHANG_DATA_ROOT}/{JIANGCHANG_USER_ID}/{slug}/.env`。优先级:进程环境变量 > 用户 `.env` > `.env.example`。 + +## 面向用户问答(LLM 规则) + +- 本文(`SKILL.md`)是 LLM / OpenClaw 平台读取的技能入口,**不是**用户市场说明。 +- 根目录市场材料面向普通用户(见下方「文档分工」),**不是** Agent 调用契约。 + +当用户询问以下问题时,**必须优先读取**根目录 `README.md`(能力说明)与 `TUTORIAL.md`(逐步用法): + +- 这个技能是做什么的 +- 这个技能怎么用 +- 适合什么场景 +- 使用前要准备什么 +- 执行后会得到什么 +- 能不能帮我完成某个业务任务 + +**回答「怎么用 / 能做什么」时的要求:** + +1. 「能做什么 / 场景 / 概念类 FAQ」以 `README.md`(说明 Tab)为主;「点哪里 / 说什么 / 排障」以 `TUTORIAL.md`(教程 Tab)为主。 +2. **禁止**在回答中粘贴 `python …/scripts/main.py …` 或其它命令行。 +3. **禁止**为答用法而去 `dir` / `ls` / 列技能目录。 +4. **不要**向普通用户暴露 Playwright、DOM、Python、RPA、库表名、`error_code` 等实现细节。 +5. 用户问「怎么用」时:**优先读 `TUTORIAL.md`**。 + +## 文档分工 + +| 文档 | 读者 | 用途 | +|------|------|------| +| 根目录 `README.md` | 普通用户 | 市场 **说明** Tab | +| 根目录 `TUTORIAL.md` | 普通用户 | 市场 **教程** Tab | +| 根目录 `DEMO.md` | 普通用户 | 市场 **演示** Tab | +| 根目录 `CHANGELOG.md` | 普通用户 | 市场 **更新日志** Tab | +| `SKILL.md`(本文) | LLM / OpenClaw 平台 | 技能入口、触发与运行契约摘要 | +| `references/` | Agent 编排 / 调用 | CLI、Action、schema 契约 | +| `development/` | 开发者 / AI 编程代理 | 需求、开发教程、测试、深度技术规范 | + +## 目录约定 + +- `assets/`、`development/`、`evals/`、`references/`、`scripts/`、`tests/` +- CLI 入口固定为 `scripts/main.py` +- 业务逻辑按 `cli / db / service / util` 分层 + +## 技能命名与 slug + +- slug:`check-course-syllabus-aa`(`check` 动词 + `course-syllabus` 名词 + `aa` academic affairs 教务处缩写) +- name:**用户可见的中文业务名称**;`description` 是说明,不是名称。 +- 目录名、默认 `{slug}.db` 使用 slug,不使用中文。 + +## 平台元数据 + +- `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表。 +- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。 + +--- + +> **本技能开发状态:** v1.0.1 骨架已就位,业务实现细节见 `development/REQUIREMENTS.md`;具体 Python 模块由开发者后续补全。 diff --git a/TUTORIAL.md b/TUTORIAL.md new file mode 100644 index 0000000..5efbda7 --- /dev/null +++ b/TUTORIAL.md @@ -0,0 +1,146 @@ +# 使用教程 + +本技能有两种**平行**的使用方式: + +- **数据管理**:在左侧「课程大纲」表上点工具栏 / 行内按钮 +- **对话任务**:用自然语言让 Agent 完成同一能力 + +定时任务是否提供:**见下方「入口三」**(当前为占位,按需启用)。 + +--- + +## 本技能做什么 + +```text +【教师 Word 大纲 + 专业培养方案 + 编制规范】 → 【格式结构 + 矩阵匹配 + 思政融合度】 → 【Word 批注 + 体检报告】 → 【百分制评分 + 修改建议】 +``` + +- **输入**:教师撰写的《课程教学大纲》Word + 对应专业最新版《人才培养方案》PDF + 学校《课程大纲编制规范》红头文件 +- **输出**:原 Word 文档的批注 + 百分制体检评分 + 修改建议清单 + HTML 体检报告 +- **说明**:本技能不替代教务处终审;评分 ≥ 85 分建议直接送审,60~85 分建议修改后送审,< 60 分必须修改 + +--- + +## 开始前 + +确认: + +1. 已安装并登录匠厂客户端 +2. 已在技能市场安装本技能(建议更新到最新版) +3. 教务系统 API 凭证已配置(`AA_API_TOKEN` 已登记到本技能 `.env`) +4. 教师撰写的《课程教学大纲》Word 文档已上传到教务系统 +5. 对应专业最新版《人才培养方案》PDF 已入库(一次性导入,后续自动匹配) +6. 学校《课程大纲编制规范》红头文件已入库 + +--- + +## 入口一:数据管理 + +适合:课程负责人、教研室主任、二级学院教学秘书。 + +### 左侧各表是干什么的 + +| 表名(中文) | 作用 | +|--------------|------| +| 课程大纲 | 全部课程大纲主表(含体检状态) | +| 体检记录 | 每次体检的结果(含评分、不达标项) | +| 培养方案 | 各专业最新版《人才培养方案》PDF 索引 | +| 编制规范 | 《课程大纲编制规范》红头文件索引 | +| 课程思政关键词 | 当前启用的思政关键词列表与权重 | + +### 首次准备(若需要) + +1. 进入 **数据管理** → **培养方案** +2. 点击表顶 **「导入培养方案」**(空表可点) +3. 成功后应看到:本校全部专业的培养方案 PDF 文件、对应年级、毕业要求指标点摘要 + +### 主流程:对一份大纲做合规体检 + +1. 进入 **课程大纲** +2. 选中待体检的行 → 点击 **「体检这份大纲」** +3. 等待 30 秒~1 分钟(向量化 + Word 回写) +4. 完成后: + - 原 Word 文档出现红 / 黄 / 绿三色批注 + - 体检记录表新增一行(含评分 + 不达标项) + - 大纲表状态自动更新为「已通过 / 待修改 / 驳回」 + +### 行级 / 批量 + +| 你想做什么 | 操作 | +|------------|------| +| 体检单份大纲 | **课程大纲** 行内 → **「体检这份大纲」** | +| 批量体检某专业所有大纲 | 选中多行 → **「批量体检」** | +| 查看体检报告 | **体检记录** 行内 → **「查看报告」** | +| 重新体检 | **课程大纲** 行内 → **「重新体检」** | +| 同步编制规范 | **编制规范** 表顶 → **「立即同步」** | + +--- + +## 入口二:对话任务 + +适合:临时提问、不想切换表的用户。 + +### 什么时候用对话 + +- 临时想给某份大纲做体检,但不想打开数据管理 +- 想用自然语言解释「为什么这份大纲 82 分」 +- 想看「本月一共体检了多少份大纲 / 平均分是多少」 + +### 示例话术 + +| 你想做什么 | 可以这样说 | +|------------|------------| +| 体检一份大纲 | 「帮我给《数据结构》大纲做一下体检」 | +| 批量体检 | 「请把计算机专业所有大纲都体检一遍」 | +| 查看评分 | 「《高等数学》大纲现在多少分?」 | +| 解释评分 | 「为什么《XX》大纲 82 分?」 | +| 同步规范 | 「同步一下课程大纲编制规范」 | + +### Agent 可能追问什么 + +- 缺参数时:「请告诉我课程名称 / 大纲 ID / 教师姓名」 +- 培养方案缺失时:「该课程对应专业的培养方案尚未导入,请先到数据管理导入」 +- 失败时:可改用数据管理点按钮重新触发 + +--- + +## 入口三:定时任务 + +### 本技能当前为占位(按需启用) + +本技能**当前未启用定时任务入口**。如未来启用,建议场景: + +- 开学前 1 个月(每年 8 月)自动对全校新开课大纲做集中体检 +- 编制规范文件更新后自动重检存量大纲 + +启用条件:教务系统提供体检排期 API,并经教务处教学科确认自动化边界。 + +当前请使用 **数据管理** 或 **对话任务**。 + +--- + +## 主能力对照表 + +| 想做什么 | 数据管理 | 对话任务 | +|----------|----------|----------| +| 体检一份大纲 | **课程大纲** 行内 → **「体检这份大纲」** | 「帮我给《数据结构》大纲做体检」 | +| 批量体检 | 勾选多行 → **「批量体检」** | 「请把计算机专业所有大纲都体检一遍」 | +| 查看体检报告 | **体检记录** 行内 → **「查看报告」** | 「《XX》大纲现在多少分?」 | +| 重新体检 | **课程大纲** 行内 → **「重新体检」** | 「再体检一次《XX》大纲」 | +| 同步编制规范 | **编制规范** 表顶 → **「立即同步」** | 「同步一下课程大纲编制规范」 | + +按钮名必须与 `assets/actions.json` 的 `label` 一致。 + +--- + +## 常见卡住怎么办 + +| 现象 | 你可以先试 | +|------|------------| +| 「体检这份大纲」点不了 | 是否已配置教务系统 API;是否装错版本 | +| 体检很久没结果 | 看任务中心进度;大纲过大(>3MB)可能耗时较久 | +| 评分明显偏低 | 检查培养方案是否已正确导入;课程目标是否含完整段落(仅一行字会判为不达标) | +| Word 批注没出现 | 检查教务系统是否支持批注回写;如不支持,仅生成 HTML 报告 | +| 课程思政关键词全为 0 | 关键词库为空或关键词文件未同步;先到「课程思政关键词」表配置 | + +仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。 diff --git a/assets/actions.json b/assets/actions.json new file mode 100644 index 0000000..ddeb587 --- /dev/null +++ b/assets/actions.json @@ -0,0 +1,107 @@ +{ + "schemaVersion": 1, + "skill": "check-course-syllabus-aa", + "actions": [ + { + "id": "health", + "label": "运行检查", + "description": "检查技能运行环境、教务系统 API 连通性、培养方案库完整性。", + "placements": ["skill-detail", "agent"], + "executionProfile": "sync", + "entrypoint": { + "type": "cli", + "command": "health", + "args": [] + } + }, + { + "id": "version", + "label": "查看版本", + "description": "返回技能版本和机器标识。", + "placements": ["skill-detail"], + "executionProfile": "sync", + "entrypoint": { + "type": "cli", + "command": "version", + "args": [] + } + }, + { + "id": "config-path", + "label": "查看配置位置", + "description": "返回技能配置文件位置。", + "placements": ["skill-detail"], + "executionProfile": "sync", + "entrypoint": { + "type": "cli", + "command": "config-path", + "args": [] + } + }, + { + "id": "audit-syllabus", + "label": "体检这份大纲", + "description": "对指定课程大纲执行格式结构 + 矩阵匹配 + 思政融合度体检,30 秒~1 分钟内返回百分制评分与不达标项清单。", + "placements": ["row", "agent"], + "executionProfile": "sync", + "bind": { + "tables": ["课程大纲"], + "selectionMode": "required", + "labelColumn": "课程名称" + }, + "entrypoint": { + "type": "cli", + "command": "audit-syllabus", + "args": ["--syllabus-id", "{row.大纲ID}"] + } + }, + { + "id": "batch-audit-syllabus", + "label": "批量体检", + "description": "批量体检某专业或某教研室下所有大纲,按顺序逐份执行并汇总结果。", + "placements": ["batch", "agent"], + "executionProfile": "async", + "bind": { + "tables": ["课程大纲"], + "selectionMode": "optional" + }, + "entrypoint": { + "type": "cli", + "command": "batch-audit-syllabus", + "args": ["--major-id", "{form.major_id}", "--course-ids", "{form.course_ids}"] + } + }, + { + "id": "render-html", + "label": "查看体检报告", + "description": "渲染指定体检记录的 HTML 报告,宿主 iframe 预览。", + "placements": ["row", "skill-detail", "agent"], + "executionProfile": "sync", + "bind": { + "tables": ["体检记录"], + "selectionMode": "required", + "labelColumn": "课程名称" + }, + "entrypoint": { + "type": "cli", + "command": "render-html", + "args": ["--audit-id", "{row.体检ID}"] + } + }, + { + "id": "sync-norm-library", + "label": "同步编制规范库", + "description": "从教务系统重新拉取《课程大纲编制规范》红头文件并更新索引。占位 Action,cron 启用后由定时调度。", + "placements": ["cron", "agent", "toolbar"], + "executionProfile": "async", + "bind": { + "tables": ["编制规范"] + }, + "entrypoint": { + "type": "cli", + "command": "sync-norm-library", + "args": [] + } + } + ] +} diff --git a/assets/html/example-report.html b/assets/html/example-report.html new file mode 100644 index 0000000..389a1e9 --- /dev/null +++ b/assets/html/example-report.html @@ -0,0 +1,116 @@ + + + + + {{TITLE}} + + + +
+
+
+

{{TITLE}}

+
体检编号:{{AUDIT_ID}} · 课程:{{COURSE_NAME}} · 任课教师:{{TEACHER_NAME}}
+
体检完成时间:{{GENERATED_AT}}
+
+
+
{{SCORE}}
+
百分制评分
+
+
+ +
+

体检概览

+
+
格式结构
{{FORMAT_SCORE}} 分
+
课程目标与毕业要求匹配
{{MATRIX_SCORE}} 分
+
课程思政融合度
{{IDEOLOGY_SCORE}} 分
+
不达标项
{{ISSUE_COUNT}} 项
+
+
+ +
+

体检结论与建议

+

{{CONCLUSION}}

+
+ +
+

不达标项清单(按重要性排序)

+
    + {{CHECKLIST_BODY}} +
+
+ + +
+ + diff --git a/development/REQUIREMENTS.md b/development/REQUIREMENTS.md new file mode 100644 index 0000000..1c09ad6 --- /dev/null +++ b/development/REQUIREMENTS.md @@ -0,0 +1,182 @@ +# 课程教学大纲合规性体检 — 技能需求文档 + +> 派生自匠厂 skill-template 主仓 `development/REQUIREMENTS.md`,对应规格书第 4.2 节「🟣 教务处 Skill 001 —— 课程教学大纲"合规性体检"」。 + +--- + +## 0. 技能标识 + +- **拟定 slug(英文)**:`check-course-syllabus-aa` +- **中文 name**:课程教学大纲合规性体检 +- **中文 description(一句话)**:教师在教务系统提交新开课或修订大纲时,自动检查格式结构、课程目标与毕业要求矩阵匹配、课程思政融合度,以批注模式嵌入原 Word 文档并给出百分制体检评分 +- **account-manager platform_key(如有)**:N/A +- **命名形态**:标准型(动词-名词-部门缩写) +- **目标 OEM 市场 `brands`(可选)**:未写则发布后仅挂匠厂(`jiangchang`) +- **网页 RPA(如有)— 登录策略**:**不适用**(本技能不涉及网页 RPA) + +## 0.1 宿主入口定位(必填) + +| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 教程章节 | 不挂理由 | +|---------------------------|------|----------|------|----------|----------| +| `health` / `version` / `config-path` | 挂 | 不挂 | 不挂 | 入口二 | 默认必备 | +| `audit-syllabus` | 挂 | row(课程大纲) | 不挂 | 入口一/入口二 | sync | +| `batch-audit-syllabus` | 挂 | batch(课程大纲) | 不挂 | 入口一/入口二 | async | +| `render-html` | 挂 | row(体检记录) | 不挂 | 入口一/入口二 | sync | +| `sync-norm-library` | 挂 | toolbar(编制规范) | 挂(占位) | 入口一/入口三 | cron 占位 | + +### 0.1.1 用户市场文档 + +- `README.md`:能力 / 场景 / 边界 +- `TUTORIAL.md`:覆盖两入口(数据管理 / 对话),入口三写「占位」 + +## 0.2 AI + RPA 协作与授权边界 + +### Agent 设计 + +- 用户意图:体检单份大纲 / 批量体检 / 查看报告 / 解释评分 / 同步规范 +- 支持的用户意图 → Action 映射: + - 体检单份大纲 → `audit-syllabus` + - 批量体检 → `batch-audit-syllabus` + - 查看报告 → `render-html` + - 同步规范 → `sync-norm-library` +- 缺参数追问:课程名称 / 大纲 ID / 教师姓名 +- 必须确认的动作: + - `batch-audit-syllabus` 批量体检前确认数量与教研室范围 +- 结果解释:成功(评分 + 不达标项)、部分成功(Word 批注回写失败但报告生成成功)、失败(教务 API 超时) + +### 授权与平台边界 + +- 教务系统 API 凭证由教务处统一开通 +- 仅读取教师本人或教务处授权的大纲文件 +- 严禁逆向教务系统、破解私有协议、自动绕过审批 +- OCR / NLP 模型失败时停止并上报,不擅自重试 + +### RPA 风控设计 + +- 执行端:**N/A** + +## 0.3 业务行生命周期 + +本技能对「体检记录」表中的每一条体检记录,无角色约束(一次性结果存档,不重复处理)。体检记录表仅作为审计追溯依据。 + +--- + +## 1. 文档目标 + +本文档用于明确 `check-course-syllabus-aa` 的研发范围、交付标准、依赖关系和验收要求。 + +## 2. 业务背景 + +- **现状**:教师新开课或修订大纲后送审,教务处审核时常见格式不规范、课程目标与毕业要求脱节、课程思政元素流于形式等问题 +- **痛点**:教务处人工审核一份大纲平均 40 分钟;返工率 30%;课程思政融合度难以量化评估 +- **目标**:自动完成「格式结构 + 矩阵匹配 + 思政融合度」三维体检,输出「Word 批注 + 百分制评分 + 修改建议」 + +## 3. 开发目标 + +- 解析 Word 大纲文件(doc/docx),提取章节结构与文字 +- 解析 PDF 培养方案,提取毕业要求指标点 +- NLP 语义相似度匹配课程目标与毕业要求指标点 +- 关键词检测 + 上下文语境分析课程思政融合度 +- 输出 Word 批注(红黄绿三色)+ 百分制评分 + HTML 体检报告 +- 写入业务表(课程大纲 / 体检记录 / 培养方案 / 编制规范 / 课程思政关键词) +- 完成 CLI 入口与 `health` / `version` / `init-db` 最小可运行验证 + +## 4. 功能范围 + +### 必须实现 + +- 支持 `python scripts/main.py health` / `version` / `config-path` / `init-db` +- 支持 `python scripts/main.py audit-syllabus --syllabus-id ` +- 支持 `python scripts/main.py batch-audit-syllabus --major-id `(async) +- 支持 `python scripts/main.py render-html --audit-id ` +- 支持 `python scripts/main.py sync-norm-library`(async,cron 占位) +- 完成 §0.1 入口定位表,并按表落实已挂入口 + +### 可后续扩展 + +- 多语言教学大纲(中英双语)支持 +- 课程思政案例库推荐(在评分偏低时给出融入建议) +- 跨专业毕业要求指标点合并匹配 + +## 5. 非功能要求 + +- 目录结构必须符合当前 `skill-template` 规范 +- 入口必须统一为 `scripts/main.py` +- 用户市场四 Tab 写在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md` +- 输出格式应尽量机读友好;错误前缀统一为 `ERROR:` +- Windows 环境下需保证 UTF-8 输出兼容 +- 单份大纲体检耗时 ≤ 60 秒(含 Word 批注回写) + +## 6. 输入输出要求 + +### 输入 + +- `syllabus_id`:大纲主键(必填,字符串) +- `major_id`:专业主键(仅 batch 必填) +- `course_ids`:课程 ID 列表(仅 batch 必填) +- `audit_id`:体检主键(仅 render-html 必填) +- 依赖系统:教务系统 API(大纲读写 + 批注回写) +- 环境变量(`.env`): + - `AA_API_BASE_URL`:教务系统 API 入口 + - `AA_API_TOKEN`:调用凭证 + - `IDEOLOGY_KEYWORDS`:课程思政关键词列表(JSON 数组) + - `SCORE_PASS_THRESHOLD`:建议直接送审分数阈值,默认 85 + - `SCORE_MODIFY_THRESHOLD`:建议修改后送审分数阈值,默认 60 + +### 输出 + +- 成功结果:`audit-syllabus` 返回 `{score, format_score, matrix_score, ideology_score, issue_count, audit_status}` +- 错误输出:`ERROR:` 稳定码 + 中文 message +- 数据库记录:写入 `体检记录` 表 +- HTML 报告:体检报告落到 `html/` +- Word 批注:红 / 黄 / 绿三色批注回写到原 Word 文档 + +## 7. 依赖的兄弟技能或外部系统 + +### 兄弟技能依赖 + +- 无强制依赖 + +### 外部系统依赖 + +- **教务系统 API**(大纲读写 + 批注回写):必须 +- **培养方案库**(PDF 文件):必须 + +### Python 包依赖(requirements.txt) + +- Word 文档解析:`python-docx>=1.0.0,<2` +- PDF 解析:`pdfplumber>=0.10.0` +- NLP 语义相似度:`sentence-transformers>=2.2.0,<3` +- HTTP 请求:`requests>=2.31.0,<3` +- `jiangchang-platform-kit` 由宿主共享 runtime 提供 + +## 8. 不在本次范围内的内容 + +- 本次不实现课程思政案例库自动推荐(仅做关键词检测) +- 本次不实现跨专业合并匹配 +- 本次不做 cron 启用(占位 Action 已就位) +- 本次不兼容旧模板历史结构 + +## 9. 验收标准 + +- 代码结构符合模板规范;`SKILL.md` slug 与 `constants.SKILL_SLUG` 一致 +- `health` / `version` / `config-path` / `init-db` 命令执行正常 +- `audit-syllabus` 在含 100 份大纲的样例库中完成一次完整体检,耗时 ≤ 60 秒 +- 业务表写入和查询符合 `references/SCHEMA.md` +- Word 批注回写成功率 ≥ 95% +- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID +- 已人工核验授权与技术路线 +- 安装后按 §0.1 入口定位表完成宿主多入口验收 + +## 10. 开发注意事项 + +- 只修改当前 skill 仓库 +- 教务系统 API 不可用时停止并上报 +- NLP 模型权重文件较大,应通过 `assets/` 或运行时下载,不入库 +- `cli` 只做参数解析;核心逻辑在 `service` + +## 11. 变更记录 + +| 日期 | 版本 | 变更人 | 变更内容 | +|------|------|--------|----------| +| 2026-09-14 | v1.0.1 | 匠厂 | 初版需求文档,骨架就位 | diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..d4413ff --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +testpaths = tests +python_files = test_*.py *_test.py +python_classes = Test* +python_functions = test_* +norecursedirs = .git __pycache__ .pytest_cache integration desktop samples fixtures artifacts diagnostics +filterwarnings = + ignore::DeprecationWarning diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..db77b94 --- /dev/null +++ b/release.ps1 @@ -0,0 +1,424 @@ +<# +.SYNOPSIS + Local git release for skill-template repo. + +.DESCRIPTION + Handles git commit / push / semver tag / push tag only. + Encryption, ZIP packaging, and marketplace sync are done by CI + (.github/workflows/release_skill.yaml → reusable-release-skill.yaml@main). + + POLICY-RELEASE-001: before tagging, requires SKILL.md version and + scripts/util/constants.py SKILL_VERSION to equal the next tag (without prefix). + release.ps1 does not auto-edit those fields. + + Requires: git, PowerShell 5+ + + After fetch, runs default tests against the current working tree (before auto-commit): + python tests/run_tests.py -v + Use -SkipTests only for emergency diagnostics. -DryRun still runs tests. +#> + +[CmdletBinding()] +param( + [string]$Prefix = "v", + [string]$Message, + [switch]$AutoCommit, + [switch]$RequireClean, + [string]$CommitMessage, + [switch]$DryRun, + [switch]$SkipTests +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Default tag message is 「正式发布」 from UTF-8 bytes (not a Chinese literal in +# this file). Windows PowerShell 5.1 parses UTF-8 scripts without BOM as the +# system ANSI code page, which would turn a source literal into 「姝e紡鍙戝竷」. +if (-not $PSBoundParameters.ContainsKey("Message") -or [string]::IsNullOrWhiteSpace($Message)) { + $Message = [System.Text.Encoding]::UTF8.GetString([byte[]](0xE6, 0xAD, 0xA3, 0xE5, 0xBC, 0x8F, 0xE5, 0x8F, 0x91, 0xE5, 0xB8, 0x83)) +} + +function Remove-GitNoise { + param([string[]]$Lines) + return @($Lines | Where-Object { + $_ -and ($_ -notmatch '^warning:\s+unable to access ') + }) +} + +function Convert-GitOutputLines { + param($Output) + return @( + @($Output) | ForEach-Object { + if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } + else { "$_" } + } + ) +} + +# Call git.exe directly. Never wrap with `cmd /c`: on Chinese Windows, cmd +# re-encodes the command line through the OEM code page. +# Also never pass Chinese via `git -m`: Windows PowerShell 5.1 encodes native +# argv using the console code page, so 「正式发布」 becomes 「姝e紡鍙戝竷」 +# in the annotated tag. Commit/tag messages go through a UTF-8 file + `-F`. +function Invoke-GitRaw { + param([Parameter(Mandatory = $true)][string[]]$GitArgs) + $prev = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = & git @GitArgs 2>&1 + $code = $LASTEXITCODE + } finally { + $ErrorActionPreference = $prev + } + return [pscustomobject]@{ + ExitCode = $code + Lines = @(Remove-GitNoise -Lines @(Convert-GitOutputLines -Output $output)) + } +} + +function Invoke-Git { + param([Parameter(Mandatory = $true)][string[]]$GitArgs) + Write-Host (">> git " + ($GitArgs -join " ")) -ForegroundColor DarkGray + $r = Invoke-GitRaw -GitArgs $GitArgs + if ($r.ExitCode -ne 0) { + throw "git command failed: git $($GitArgs -join ' ')" + } +} + +function Invoke-GitWithMessageFile { + param( + [Parameter(Mandatory = $true)][string[]]$GitArgsPrefix, + [Parameter(Mandatory = $true)][string]$Message + ) + $msgFile = Join-Path ([System.IO.Path]::GetTempPath()) ("jiangchang-git-msg-" + [guid]::NewGuid().ToString("N") + ".txt") + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + [System.IO.File]::WriteAllText($msgFile, ($Message.TrimEnd() + "`n"), $utf8NoBom) + try { + Invoke-Git -GitArgs (@("-c", "i18n.commitEncoding=utf-8") + $GitArgsPrefix + @("-F", $msgFile)) + } finally { + Remove-Item -LiteralPath $msgFile -Force -ErrorAction SilentlyContinue + } +} + +function Get-GitOutput { + param([Parameter(Mandatory = $true)][string[]]$GitArgs) + $r = Invoke-GitRaw -GitArgs $GitArgs + if ($r.ExitCode -ne 0) { + throw "git command failed: git $($GitArgs -join ' ')" + } + return @($r.Lines) +} + +function Test-Repo { + $r = Invoke-GitRaw -GitArgs @("rev-parse", "--is-inside-work-tree") + if ($r.ExitCode -ne 0) { + return $false + } + return (($r.Lines | Select-Object -First 1) -eq "true") +} + +function Get-CurrentBranch { + $b = (Get-GitOutput -GitArgs @("branch", "--show-current") | Select-Object -First 1).Trim() + return $b +} + +function Get-StatusPorcelain { + $lines = @(Get-GitOutput -GitArgs @("status", "--porcelain")) + return $lines +} + +function Parse-SemVerTag { + param( + [string]$Tag, + [string]$TagPrefix + ) + $escaped = [regex]::Escape($TagPrefix) + $m = [regex]::Match($Tag, "^${escaped}(\d+)\.(\d+)\.(\d+)$") + if (-not $m.Success) { return $null } + + return [pscustomobject]@{ + Raw = $Tag + Major = [int]$m.Groups[1].Value + Minor = [int]$m.Groups[2].Value + Patch = [int]$m.Groups[3].Value + } +} + +function Get-NextTag { + param([string]$TagPrefix) + + $tags = Get-GitOutput -GitArgs @("tag", "--list") + $parsed = @() + + foreach ($t in $tags) { + $t = $t.Trim() + if (-not $t) { continue } + $obj = Parse-SemVerTag -Tag $t -TagPrefix $TagPrefix + if ($null -ne $obj) { $parsed += $obj } + } + + if ($parsed.Count -eq 0) { + return "${TagPrefix}1.0.1" + } + + $latest = $parsed | Sort-Object Major, Minor, Patch | Select-Object -Last 1 + return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)" +} + +function Read-SkillMdVersion { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $path = Join-Path $SkillRoot "SKILL.md" + if (-not (Test-Path -LiteralPath $path)) { + return $null + } + $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + $m = [regex]::Match($text, '(?m)^version:\s*["'']?([0-9]+\.[0-9]+\.[0-9]+)["'']?\s*$') + if (-not $m.Success) { + return $null + } + return $m.Groups[1].Value.Trim() +} + +function Read-SkillConstantsVersion { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $path = Join-Path $SkillRoot "scripts\util\constants.py" + if (-not (Test-Path -LiteralPath $path)) { + return $null + } + $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + $m = [regex]::Match($text, '(?m)^SKILL_VERSION\s*=\s*["'']([0-9]+\.[0-9]+\.[0-9]+)["'']') + if (-not $m.Success) { + return $null + } + return $m.Groups[1].Value.Trim() +} + +# POLICY-RELEASE-001: marketplace latest (git tag) must equal package manifest versions. +function Assert-SkillReleaseVersionAlignment { + param( + [Parameter(Mandatory = $true)][string]$SkillRoot, + [Parameter(Mandatory = $true)][string]$NextTag, + [Parameter(Mandatory = $true)][string]$TagPrefix + ) + + $skillMdPath = Join-Path $SkillRoot "SKILL.md" + if (-not (Test-Path -LiteralPath $skillMdPath)) { + return + } + + $expected = $NextTag + if ($TagPrefix -and $expected.StartsWith($TagPrefix)) { + $expected = $expected.Substring($TagPrefix.Length) + } + $expected = $expected.Trim() + if ($expected -notmatch '^\d+\.\d+\.\d+$') { + throw "Release check failed (POLICY-RELEASE-001): cannot derive semver from Next tag '$NextTag'." + } + + $mdVersion = Read-SkillMdVersion -SkillRoot $SkillRoot + $constVersion = Read-SkillConstantsVersion -SkillRoot $SkillRoot + + $problems = @() + if ([string]::IsNullOrWhiteSpace($mdVersion)) { + $problems += "SKILL.md missing top-level version: X.Y.Z" + } + elseif ($mdVersion -ne $expected) { + $problems += "SKILL.md version=$mdVersion (want $expected)" + } + if ([string]::IsNullOrWhiteSpace($constVersion)) { + $problems += "scripts/util/constants.py missing SKILL_VERSION = `"X.Y.Z`"" + } + elseif ($constVersion -ne $expected) { + $problems += "SKILL_VERSION=$constVersion (want $expected)" + } + + if ($problems.Count -gt 0) { + throw ( + "Release aborted (POLICY-RELEASE-001): Next tag=$NextTag requires manifest version $expected, but " + + ($problems -join "; ") + + ". Bump SKILL.md version and scripts/util/constants.py SKILL_VERSION to $expected (and CHANGELOG), then re-run release.ps1. " + + "release.ps1 does not auto-edit these fields." + ) + } + + Write-Host "Version alignment OK: tag=$NextTag == SKILL.md=$mdVersion == SKILL_VERSION=$constVersion" -ForegroundColor DarkGray +} + +function Assert-SkillReleasePackagingSources { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $scriptsDir = Join-Path $SkillRoot "scripts" + $mainPy = Join-Path $scriptsDir "main.py" + if (-not (Test-Path -LiteralPath $mainPy)) { + return + } + + $text = Get-Content -LiteralPath $mainPy -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return + } + + $need = @() + if ($text -match '(?m)^\s*from\s+cli\.') { $need += 'cli' } + if ($text -match '(?m)^\s*import\s+cli\b') { $need += 'cli' } + if ($text -match '(?m)^\s*from\s+service\.') { $need += 'service' } + if ($text -match '(?m)^\s*import\s+service\b') { $need += 'service' } + if ($text -match '(?m)^\s*from\s+db\.') { $need += 'db' } + if ($text -match '(?m)^\s*import\s+db\b') { $need += 'db' } + if ($text -match '(?m)^\s*from\s+util\.') { $need += 'util' } + if ($text -match '(?m)^\s*import\s+util\b') { $need += 'util' } + + foreach ($p in ($need | Select-Object -Unique)) { + $folder = Join-Path $scriptsDir $p + if (-not (Test-Path -LiteralPath $folder)) { + throw "Release check failed: scripts/main.py imports from '$p' but folder is missing: $folder" + } + } + + $pyFiles = @(Get-ChildItem -LiteralPath $scriptsDir -Filter *.py -Recurse -File -ErrorAction SilentlyContinue) + Write-Host "Packaging check: $($pyFiles.Count) Python file(s) under scripts/ (CI will obfuscate all recursively)." -ForegroundColor DarkGray +} + +function Invoke-DefaultTests { + param( + [Parameter(Mandatory = $true)][string]$SkillRoot, + [switch]$Skip + ) + + if ($Skip) { + Write-Host "WARNING: Skipping tests before release. This should only be used for emergency diagnostics." -ForegroundColor Yellow + return + } + + $runTests = Join-Path $SkillRoot "tests\run_tests.py" + if (-not (Test-Path -LiteralPath $runTests)) { + throw "Release check failed: tests/run_tests.py not found." + } + + Write-Host ">> python tests/run_tests.py -v" -ForegroundColor DarkGray + & python $runTests -v + if ($LASTEXITCODE -ne 0) { + throw "Release aborted: default tests failed (python tests/run_tests.py -v)." + } +} + +function Ensure-CleanOrAutoCommit { + param( + [switch]$DoAutoCommit, + [switch]$NeedClean, + [switch]$IsDryRun, + [string]$Msg + ) + + $status = @(Get-StatusPorcelain) + if ($status.Length -eq 0) { return } + + if ($NeedClean) { + Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow + (Invoke-GitRaw -GitArgs @("status", "--short")).Lines | ForEach-Object { Write-Host $_ } + throw "Abort: dirty working tree." + } + + $commitMsg = $Msg + if ([string]::IsNullOrWhiteSpace($commitMsg)) { + $commitMsg = "chore: auto release commit ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))" + } + + if (-not $DoAutoCommit) { + Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow + } + + if ($IsDryRun) { + Write-Host "[DryRun] Would run: git add -A" -ForegroundColor Yellow + Write-Host "[DryRun] Would run: git commit -m `"$commitMsg`"" -ForegroundColor Yellow + return + } + + Invoke-Git -GitArgs @("add", "-A") + Invoke-GitWithMessageFile -GitArgsPrefix @("commit") -Message $commitMsg +} + +try { + Write-Host "=== Release Script Start ===" -ForegroundColor Cyan + + if (-not (Test-Repo)) { + throw "Current directory is not a git repository." + } + + $branch = Get-CurrentBranch + if ([string]::IsNullOrWhiteSpace($branch)) { + throw "Unable to determine current branch." + } + + if ($branch -notin @("main", "master")) { + throw "Current branch is '$branch'. Release is only allowed from main/master." + } + + if ($DryRun) { + Write-Host "[DryRun] Would run: git fetch --tags --prune origin" -ForegroundColor Yellow + } else { + Invoke-Git -GitArgs @("fetch", "--tags", "--prune", "origin") + } + + $skillRoot = (Get-GitOutput -GitArgs @("rev-parse", "--show-toplevel") | Select-Object -First 1).Trim() + if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) { + Assert-SkillReleasePackagingSources -SkillRoot $skillRoot + } + + $nextTag = Get-NextTag -TagPrefix $Prefix + Write-Host "Next tag: $nextTag" -ForegroundColor Green + Assert-SkillReleaseVersionAlignment -SkillRoot $skillRoot -NextTag $nextTag -TagPrefix $Prefix + + $existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag)) + if ($existing.Length -gt 0) { + throw "Tag already exists: $nextTag" + } + + Invoke-DefaultTests -SkillRoot $skillRoot -Skip:$SkipTests + + Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage + + $upstream = Invoke-GitRaw -GitArgs @("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") + $hasUpstream = ($upstream.ExitCode -eq 0) + + if ($DryRun) { + if ($hasUpstream) { + Write-Host "[DryRun] Would run: git push" -ForegroundColor Yellow + } else { + Write-Host "[DryRun] Would run: git push -u origin $branch" -ForegroundColor Yellow + } + } else { + if ($hasUpstream) { + Invoke-Git -GitArgs @("push") + } else { + Invoke-Git -GitArgs @("push", "-u", "origin", $branch) + } + } + + if ($DryRun) { + Write-Host "[DryRun] Would run: git tag -a $nextTag -F ($Message)" -ForegroundColor Yellow + Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow + Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan + exit 0 + } + + Invoke-GitWithMessageFile -GitArgsPrefix @("tag", "-a", $nextTag) -Message $Message + Invoke-Git -GitArgs @("push", "origin", $nextTag) + + Write-Host "Release success: $nextTag" -ForegroundColor Green + Write-Host "=== Release Script Done ===" -ForegroundColor Cyan + exit 0 +} +catch { + Write-Host "Release failed: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7f03847 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,14 @@ +# 课程教学大纲合规性体检 - 技能特有依赖 +# jiangchang-platform-kit / playwright 等公共能力由宿主共享 runtime 提供,不要写入此文件 + +# Word 文档解析(大纲 doc/docx) +python-docx>=1.0.0,<2 + +# PDF 解析(培养方案 PDF) +pdfplumber>=0.10.0 + +# NLP 语义相似度(中文) +sentence-transformers>=2.2.0,<3 + +# HTTP 请求 +requests>=2.31.0,<3 diff --git a/scripts/cli/__init__.py b/scripts/cli/__init__.py new file mode 100644 index 0000000..9dc01a4 --- /dev/null +++ b/scripts/cli/__init__.py @@ -0,0 +1 @@ +"""scripts.cli - CLI 参数解析层(占位)""" diff --git a/scripts/db/__init__.py b/scripts/db/__init__.py new file mode 100644 index 0000000..4bc7684 --- /dev/null +++ b/scripts/db/__init__.py @@ -0,0 +1,8 @@ +"""scripts.db - 数据库访问层(占位) + +按 references/SCHEMA.md 实现表与字段: +- 制度送审稿 +- 冲突条款清单 +- 制度库索引 +- 审查任务日志 +""" diff --git a/scripts/main.py b/scripts/main.py new file mode 100644 index 0000000..af4e2d8 --- /dev/null +++ b/scripts/main.py @@ -0,0 +1,84 @@ +#!/usr/bin/env python3 +"""课程教学大纲合规性体检 - CLI 入口(占位骨架)""" + +import sys +from pathlib import Path + +# 占位:开发者后续接入 jiangchang_skill_core +# from jiangchang_skill_core import ... + +BASE_DIR = Path(__file__).resolve().parent.parent + + +def health(): + """运行检查(占位)。""" + print(f"check-course-syllabus-aa health check (placeholder)") + print(f"base_dir: {BASE_DIR}") + return 0 + + +def version(): + """查看版本(占位)。""" + print("check-course-syllabus-aa v1.0.1") + return 0 + + +def config_path(): + """查看配置位置(占位)。""" + print(f"{BASE_DIR / '.env'}") + return 0 + + +def init_db(): + """初始化数据库(占位)。""" + print("init-db: 后续由开发者按 references/SCHEMA.md 实现") + return 0 + + +def audit_syllabus(syllabus_id: str): + """体检这份大纲。""" + print(f"audit-syllabus: syllabus_id={syllabus_id} (待开发者实现)") + return 0 + + +def batch_audit_syllabus(major_id: str = "", course_ids: str = ""): + """批量体检。""" + print(f"batch-audit-syllabus: major_id={major_id} course_ids={course_ids} (待开发者实现)") + return 0 + + +def render_html(audit_id: str): + """查看体检报告。""" + print(f"render-html: audit_id={audit_id} (待开发者实现)") + return 0 + + +def sync_norm_library(): + """同步编制规范库。""" + print("sync-norm-library (待开发者实现)") + return 0 + + +COMMANDS = { + "health": health, + "version": version, + "config-path": config_path, + "init-db": init_db, + "audit-syllabus": audit_syllabus, + "batch-audit-syllabus": batch_audit_syllabus, + "render-html": render_html, + "sync-norm-library": sync_norm_library, +} + + +def main(argv=None): + argv = argv or sys.argv[1:] + if not argv or argv[0] not in COMMANDS: + print("Usage: python scripts/main.py ") + print("Commands:", ", ".join(COMMANDS.keys())) + return 1 + return COMMANDS[argv[0]](*argv[1:]) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/service/__init__.py b/scripts/service/__init__.py new file mode 100644 index 0000000..deea28f --- /dev/null +++ b/scripts/service/__init__.py @@ -0,0 +1,9 @@ +"""scripts.service - 业务核心逻辑层(占位) + +按 development/REQUIREMENTS.md §3 开发目标实现: +- 条款解析与向量化 +- 制度库语义比对 +- 程序合规体检 +- 重复发文检测 +- 报告生成与批注回写 +""" diff --git a/scripts/util/__init__.py b/scripts/util/__init__.py new file mode 100644 index 0000000..29fb478 --- /dev/null +++ b/scripts/util/__init__.py @@ -0,0 +1 @@ +"""scripts.util - 工具函数层(占位)"""