v1.0.1: 课程成绩异常诊断与报告生成 - 初版骨架

基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。

核心业务:
- 教师在教务系统录完成绩后自动触发
- 统计指标计算 + 三种异常模式识别(高分低能 / 两极分化 / 挂科异常)
- 生成三段式 AI 评语(总体 / 问题 / 建议)

输出形态:
- HTML 报告(含统计表 + 正态分布图 + AI 评语)
- 高风险课程自动标记 + 异常预警通知

预期提效:教师撰写成绩分析报告的时间从半天 → 10 分钟。

业务行生命周期:无(一次性报告存档,不重复处理)

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
2026-09-14 12:19:12 +08:00
commit 1d20cfa708
21 changed files with 1482 additions and 0 deletions

26
.env.example Normal file
View File

@@ -0,0 +1,26 @@
# 课程成绩异常诊断与报告生成 - 配置示例
# 行头说明「是什么」;行尾说明「怎么填」;进程环境变量 > 用户 .env > 本文件
# 教务系统 API 入口,例如 https://aa.example.edu/api/v1
AA_API_BASE_URL=
# 教务系统 API 调用凭证
AA_API_TOKEN=
# 挂科异常阈值(不及格率超过此值触发预警),默认 0.30
HIGH_FAIL_RATE_THRESHOLD=0.30
# 高分低能阈值(平时分 - 期末分 差值超过此值触发预警),默认 20
SCORE_GAP_THRESHOLD=20
# 两极分化阈值(标准差 / 历史均值 超过此值触发预警),默认 1.5
STD_DEV_RATIO_THRESHOLD=1.5
# 单门课程分析超时(秒),默认 30
ANALYZE_TIMEOUT_SEC=30
# 历史成绩对比窗口(学期数),默认 6 个学期(约 3 年)
HISTORY_WINDOW_SEMESTERS=6
# 错误码前缀,统一为 ERROR: 起头
LOG_LEVEL=INFO

10
.gitignore vendored Normal file
View File

@@ -0,0 +1,10 @@
__pycache__/
*.py[cod]
.env
*.env.local
.pytest_cache/
.coverage
htmlcov/
dist/
build/
*.egg-info/

5
.openclaw-skill-template Normal file
View File

@@ -0,0 +1,5 @@
# 此文件仅存在于 skill-template 源仓库。
# 用 tools/scaffold_skill 创建新技能时,脚本会从目标目录删除本文件。
# 若业务技能仓库中仍存在本文件,说明未正确 scaffold,tests 会失败。
template_slug: your-skill-slug
template_version: "1.0.32"

12
CHANGELOG.md Normal file
View File

@@ -0,0 +1,12 @@
# Changelog
本文件对应技能市场 **「更新日志」** Tab。
## 1.0.1
- 初版:基于匠厂 skill-template 主仓 v1.0.108 派生
- 实现核心业务:课程成绩异常诊断与报告生成(统计指标计算 + 高分低能/两极分化/挂科异常识别 + AI 三段式评语)
- 输出形态:HTML 报告(含统计表 + 正态分布图 + AI 评语)+ 高风险课程标记 + 异常预警通知
- 预期提效:教师撰写成绩分析报告的时间从半天 → 10 分钟;教学督导能够精准锁定"高风险课程"进行提前干预
- 业务行生命周期:无(一次性报告存档,不重复处理)
- 占位说明:本版仅完成骨架与文档,业务实现细节(`scripts/` 下 Python 模块、异常检测算法调参、AI 评语模板)由开发者后续按 `development/REQUIREMENTS.md` 补全

27
DEMO.md Normal file
View File

@@ -0,0 +1,27 @@
---
demo_video_url: ""
---
# 演示说明
> 本技能当前未提供演示视频。下方用图文形式描述完整分析流程与结果样例。
## 演示看点
1. **数据管理 → 课程成绩 → 分析这份成绩**:选中待分析的行,10~30 秒内完成
2. **结果样例**:
- 分析报告表新增一行,含「统计指标表 + 正态分布图 + AI 三段式评语」
- 异常课程自动推送给教学督导与学院教学副院长
- 课程行状态变为「已分析」或「高风险」
## 没有视频时
可参考下方截图位(待补):
- 截图位 1:分析报告表(统计指标 + 异常模式标签)
- 截图位 2:HTML 分析报告(统计表 + 正态分布图)
- 截图位 3:AI 三段式评语(总体 / 问题 / 建议)
## 何时补视频
v1.1.0 起,按 `development/TUTORIAL_SURFACES.md` §3 录制 1~2 分钟演示视频,覆盖录完成绩 → 触发分析 → 查看报告 → 标记高风险的全流程。

21
LICENSE Normal file
View File

@@ -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.

82
README.md Normal file
View File

@@ -0,0 +1,82 @@
---
description: "教师在教务系统录完成绩后,自动计算统计指标、识别高分低能/两极分化/挂科异常三种模式、生成三段式评语,输出图文并茂的《课程成绩分析报告》。"
---
# 课程成绩异常诊断与报告生成
教师在教务系统录完全班学生成绩后,技能会自动计算最高分 / 最低分 / 平均分 / 中位数 / 标准差 / 及格率 / 优秀率,绘制正态分布曲线,并识别三种典型异常模式——平时分远高于期末分的「高分低能」、标准差异常偏大的「两极分化」、挂科率超 30% 的「挂科异常」。最后生成一份含统计图表与 AI 诊断评语的《课程成绩分析报告》,把教师半天的工作压到 10 分钟。
---
## 这个技能能帮你做什么
- **统计指标自动计算**:最高 / 最低 / 平均 / 中位 / 标准差 / 及格率 / 优秀率 + 正态分布曲线
- **「高分低能」预警**:平时分远高于期末分(差值 > 20 分),提示可能存在平时分"放水"或期末试卷难度过大
- **「两极分化」预警**:标准差超过该课程历史平均标准差的 1.5 倍,提示教学方法可能不适合部分学生
- **「挂科异常」预警**:不及格率超过 30% 或较去年同课程上升超过 15 个百分点,自动触发"试卷难度/教学有效性"复核建议
- **三段式 AI 评语**:总体描述 + 存在问题 + 改进建议,供教师修改后用于课程总结存档
- **高风险课程标记**:自动把异常课程推送给教学督导与学院教学副院长
---
## 适合什么场景
- 任课教师每学期末需要撰写课程总结与成绩分析报告
- 教学督导需要锁定"高风险课程"进行提前干预
- 学院教学副院长需要在学期末做一次整体教学质量回顾
- 教务处需要在学期结束后汇总全校成绩分布用于年度教学质量报告
---
## 使用前需要准备什么
- 教务系统 API 凭证(`AA_API_TOKEN`)已配置到本技能 `.env`
- 该课程全班学生的各项成绩明细(平时分 / 期中分 / 实验分 / 期末分 / 总评)
- 该课程的教学大纲(含各考核项权重)
- 该班级的前置课程历史成绩数据(如高数 / 英语 / 计算机基础等)
---
## 怎么使用
本技能支持**数据管理**(课程成绩表上点工具栏按钮)与**对话任务**(自然语言让 Agent 完成)。定时任务为占位,按需启用。
**逐步操作与排障,请查看技能市场「使用教程」Tab。**
---
## 执行后你会得到什么
- 一份《课程成绩分析报告》(HTML),含统计表 + 正态分布图 + AI 三段式评语
- 教学督导与学院教学副院长收到"高风险课程"通知(如有异常)
- 课程成绩表状态更新(已分析 / 高风险)
- 报告可一键导出 PDF 或嵌入课程总结存档
---
## 注意事项
- 本技能**不替代教学督导终审**,最终干预决定权仍在督导与学院
- "高分低能"等异常仅作预警,教师可补充说明(如"本课程平时分含小组项目评分,与期末卷面难度可比性较低")
- 历史成绩对比基于该校该课程近 3 年的均值;新课程或首次开课课程无对比数据时,仅做统计不预警
- 涉及学生个人成绩明细的导出仍受教务系统权限控制,技能仅输出聚合统计与匿名分布
---
## 常见问题
### 这个技能适合我吗?
> 如果你的工作包含「学期末成绩分析 + 课程总结撰写」,本技能可以一气呵成地生成报告草稿。
### 高风险课程的标准是什么?
> 同时满足下列任一条件:(a) 不及格率 > 30%;(b) 不及格率较去年同课程上升 > 15 个百分点;(c) 标准差 > 历史均值的 1.5 倍;(d) 平时分与期末分差值 > 20 分。
### AI 评语能直接用于课程总结吗?
> 评语为草稿,建议教师修改后再用于存档。AI 评语仅作为参考,不能完全替代教师对课程教学过程的反思。
### 历史成绩数据不足怎么办?
> 历史不足 3 期的课程,仅做统计指标计算,不做"两极分化"预警。

20
SECURITY.md Normal file
View File

@@ -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/`,不要写进本文件。

129
SKILL.md Normal file
View File

@@ -0,0 +1,129 @@
---
name: 课程成绩异常诊断与报告生成
description: "教师在教务系统录完成绩后,自动计算统计指标、识别"高分低能 / 两极分化 / 挂科异常"三种异常模式、生成三段式评语,输出图文并茂的《课程成绩分析报告》。"
version: 1.0.1
author: 深圳匠厂科技有限公司
metadata:
openclaw:
slug: analyze-grade-distribution-aa
platform_kit_min_version: "1.2.11"
emoji: "📊"
category: "教务"
developer_ids:
- 12345 # 占位,请换成你的匠厂用户 ID
- 12468
allowed-tools:
- bash
---
# 课程成绩异常诊断与报告生成(analyze-grade-distribution-aa)
这是基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。覆盖需求规格书第 4.2 节「🟣 教务处 Skill 002 —— 成绩分析"异常诊断与报告生成"」。
## Agent / 宿主编排(复制为业务技能后必改)
### AI + RPA 定位
- **Agent 是入口与编排者**:理解用户意图("分析《高等数学》成绩"、"查看本月高风险课程")、补齐参数、判断风险、选择 Action,并用业务语言解释结果。
- **本技能不涉及浏览器 / 桌面 / 手机 RPA**:成绩数据读取与回写走教务系统官方 API。
### 架构铁律(先读)
- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**;本技能**重点挂数据管理 + 对话入口**。
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service。
- **业务行生命周期**:本技能对「成绩分析报告」无角色约束(一次性报告存档)。
- **HTML 整页报告**:本技能输出《课程成绩分析报告》走 `render-html`,含统计图表 + AI 诊断评语。
### 如何触发(能力类型)
| 你要做什么 | Agent 怎么做 | 进任务中心? |
|------------|--------------|--------------|
| 健康检查 / 短查询 | OpenClaw bash/exec + 共享 Python 跑 CLI | 否 |
| 已声明 sync 的 Action | `run_skill_action` 或等效短 CLI | 否 |
| 长任务(批量分析多门课程) | **必须** `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` → `analyze-grade-distribution`(输入:`course_id`) | 否(sync) |
| 批量分析某学期所有课程 | `run_skill_action` → `batch-analyze-courses`(输入:`semester_id`) | 是(async) |
| 查看分析报告 | `run_skill_action` → `render-html`(输入:`report_id`) | 否(sync) |
| 标记高风险课程 | `run_skill_action` → `flag-high-risk-course`(输入:`course_id`) | 否(sync) |
| 同步历史成绩数据 | `run_skill_action` → `sync-historical-grades`(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:`analyze-grade-distribution-aa`(`analyze` 动词 + `grade-distribution` 名词 + `aa` academic affairs 教务处缩写)
- name:**用户可见的中文业务名称**;`description` 是说明,不是名称。
- 目录名、默认 `{slug}.db` 使用 slug,不使用中文。
## 平台元数据
- `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表。
- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。
---
> **本技能开发状态:** v1.0.1 骨架已就位,业务实现细节见 `development/REQUIREMENTS.md`;具体 Python 模块由开发者后续补全。

144
TUTORIAL.md Normal file
View File

@@ -0,0 +1,144 @@
# 使用教程
本技能有两种**平行**的使用方式:
- **数据管理**:在左侧「课程成绩」表上点工具栏 / 行内按钮
- **对话任务**:用自然语言让 Agent 完成同一能力
定时任务是否提供:**见下方「入口三」**(当前为占位,按需启用)。
---
## 本技能做什么
```text
【课程成绩明细 + 教学大纲权重 + 历史成绩】 → 【统计指标计算 + 三种异常模式识别】 → 【AI 三段式评语】 → 【HTML 报告 + 高风险课程标记】
```
- **输入**:某门课程全班学生成绩明细 + 教学大纲(考核项权重)+ 前置课程历史成绩
- **输出**:HTML 报告(含统计表 + 正态分布图 + AI 评语)+ 异常预警通知
- **说明**:本技能不替代教学督导终审;AI 评语为草稿,教师修改后用于存档
---
## 开始前
确认:
1. 已安装并登录匠厂客户端
2. 已在技能市场安装本技能(建议更新到最新版)
3. 教务系统 API 凭证已配置(`AA_API_TOKEN` 已登记到本技能 `.env`)
4. 【如需】教师录完成绩后才能触发分析
5. 【如需】历史成绩数据已入库(首次使用需先在「数据管理 → 历史成绩」点击一次「立即同步」)
---
## 入口一:数据管理
适合:任课教师、教学督导、学院教学副院长。
### 左侧各表是干什么的
| 表名(中文) | 作用 |
|--------------|------|
| 课程成绩 | 各学期各课程成绩主表(含分析状态) |
| 分析报告 | 每次成绩分析的产物(含统计指标、AI 评语) |
| 历史成绩 | 该课程近 3 年的成绩数据(用于对比) |
| 高风险课程 | 被标记为高风险的课程列表 |
### 首次准备(若需要)
1. 进入 **数据管理** → **历史成绩**
2. 点击表顶 **「立即同步」**(空表可点)
3. 成功后应看到:本校该课程近 3 年的历史成绩数据
### 主流程:分析一门课程的成绩
1. 进入 **课程成绩**
2. 选中待分析的行 → 点击 **「分析这份成绩」**
3. 等待 10~30 秒(统计 + 异常识别 + AI 评语生成)
4. 完成后:
- 分析报告表新增一行(含统计指标 + AI 评语)
- 如触发预警,课程行状态变为「高风险」
- 教学督导 / 学院教学副院长收到"高风险课程"通知
### 行级 / 批量
| 你想做什么 | 操作 |
|------------|------|
| 分析单门课程成绩 | **课程成绩** 行内 → **「分析这份成绩」** |
| 批量分析某学期所有课程 | 表顶 → **「批量分析本学期」** |
| 查看分析报告 | **分析报告** 行内 → **「查看报告」** |
| 标记为高风险 | **分析报告** 行内 → **「标记为高风险」** |
| 同步历史成绩 | **历史成绩** 表顶 → **「立即同步」** |
---
## 入口二:对话任务
适合:临时提问、不想切换表的用户。
### 什么时候用对话
- 临时想分析一份成绩,但不想打开数据管理
- 想用自然语言解释「为什么这份成绩被标为高风险」
- 想看「本学期一共多少门课程被标为高风险」
### 示例话术
| 你想做什么 | 可以这样说 |
|------------|------------|
| 分析一门课程 | 「帮我分析一下《高等数学 A》本学期的成绩」 |
| 批量分析 | 「请把本学期计算机学院所有课程都分析一遍」 |
| 查看报告 | 「《XX 课程》成绩分析报告出来了吗?」 |
| 解释预警 | 「为什么《XX 课程》被标为高风险?」 |
| 标记高风险 | 「把《XX 课程》标记为高风险」 |
### Agent 可能追问什么
- 缺参数时:「请告诉我课程名称 / 学期 / 课程 ID」
- 历史数据缺失时:「该课程历史成绩尚未同步,请先到数据管理同步」
- 失败时:可改用数据管理点按钮重新触发
---
## 入口三:定时任务
### 本技能当前为占位(按需启用)
本技能**当前未启用定时任务入口**。如未来启用,建议场景:
- 每学期末(考试结束后第 7 天)自动对全校课程做一次批量分析
- 历史成绩数据每日同步一次
启用条件:教务系统提供定时回调 API,并经教务处教学科确认自动化边界。
当前请使用 **数据管理** 或 **对话任务**。
---
## 主能力对照表
| 想做什么 | 数据管理 | 对话任务 |
|----------|----------|----------|
| 分析一门课程成绩 | **课程成绩** 行内 → **「分析这份成绩」** | 「帮我分析一下《XX》成绩」 |
| 批量分析 | 表顶 → **「批量分析本学期」** | 「请把本学期所有课程都分析一遍」 |
| 查看分析报告 | **分析报告** 行内 → **「查看报告」** | 「《XX 课程》报告出来了吗?」 |
| 标记高风险 | **分析报告** 行内 → **「标记为高风险」** | 「把《XX 课程》标记为高风险」 |
| 同步历史成绩 | **历史成绩** 表顶 → **「立即同步」** | 「同步一下历史成绩」 |
按钮名必须与 `assets/actions.json` 的 `label` 一致。
---
## 常见卡住怎么办
| 现象 | 你可以先试 |
|------|------------|
| 「分析这份成绩」点不了 | 是否已配置教务系统 API;是否装错版本 |
| 分析很久没结果 | 看任务中心进度;学生数 > 200 可能耗时较久 |
| 评分异常被标为高风险 | 检查历史成绩是否同步;阈值是否配置过高 |
| 历史成绩数据缺失 | 先到「数据管理 → 历史成绩」点击「立即同步」 |
| AI 评语明显失真 | 可手动编辑后保存;评语仅供草稿参考 |
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。

123
assets/actions.json Normal file
View File

@@ -0,0 +1,123 @@
{
"schemaVersion": 1,
"skill": "analyze-grade-distribution-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": "analyze-grade-distribution",
"label": "分析这份成绩",
"description": "对指定课程的成绩执行统计指标计算 + 三种异常模式识别 + AI 三段式评语生成,10~30 秒内返回报告。",
"placements": ["row", "agent"],
"executionProfile": "sync",
"bind": {
"tables": ["课程成绩"],
"selectionMode": "required",
"labelColumn": "课程名称"
},
"entrypoint": {
"type": "cli",
"command": "analyze-grade-distribution",
"args": ["--course-id", "{row.课程ID}"]
}
},
{
"id": "batch-analyze-courses",
"label": "批量分析本学期",
"description": "批量分析某学期所有课程的成绩,按顺序逐门执行并汇总结果。",
"placements": ["toolbar", "agent"],
"executionProfile": "async",
"bind": {
"tables": ["课程成绩"]
},
"entrypoint": {
"type": "cli",
"command": "batch-analyze-courses",
"args": ["--semester-id", "{form.semester_id}"]
}
},
{
"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": ["--report-id", "{row.报告ID}"]
}
},
{
"id": "flag-high-risk-course",
"label": "标记为高风险课程",
"description": "手动将某门课程标记为高风险课程,推送通知给教学督导与学院教学副院长。",
"placements": ["row"],
"executionProfile": "sync",
"bind": {
"tables": ["分析报告"],
"selectionMode": "required",
"labelColumn": "课程名称"
},
"entrypoint": {
"type": "cli",
"command": "flag-high-risk-course",
"args": ["--report-id", "{row.报告ID}"]
}
},
{
"id": "sync-historical-grades",
"label": "同步历史成绩",
"description": "从教务系统重新拉取历史成绩数据。占位 Action,cron 启用后由定时调度。",
"placements": ["cron", "agent", "toolbar"],
"executionProfile": "async",
"bind": {
"tables": ["历史成绩"]
},
"entrypoint": {
"type": "cli",
"command": "sync-historical-grades",
"args": []
}
}
]
}

View File

@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>{{TITLE}}</title>
<style>
/* 数据管理 iframe 预览布局基线 */
html, body {
margin: 0;
padding: 16px;
font-family: -apple-system, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
color: #1f2329;
background: #fafbfc;
line-height: 1.6;
}
.report { max-width: 100%; }
.header {
padding: 20px;
background: linear-gradient(135deg, #7c3aed 0%, #a855f7 100%);
color: #fff;
border-radius: 8px;
margin-bottom: 16px;
}
.header h1 { margin: 0 0 8px; font-size: 22px; }
.header .meta { color: #ede9fe; font-size: 13px; }
.risk-badge {
display: inline-block;
padding: 4px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
margin-left: 8px;
}
.risk-badge.high { background: #fef2f2; color: #c53030; }
.risk-badge.normal { background: #ecfdf5; color: #059669; }
.section {
padding: 16px 20px;
background: #fff;
border: 1px solid #e5e6eb;
border-radius: 8px;
margin-bottom: 16px;
}
.section h2 { margin: 0 0 12px; font-size: 16px; color: #6d28d9; }
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 12px;
}
.stat-card {
padding: 12px 16px;
background: #faf5ff;
border: 1px solid #e9d5ff;
border-radius: 8px;
}
.stat-card .label { color: #8b5cf6; font-size: 12px; }
.stat-card .value { font-size: 20px; font-weight: 700; margin-top: 4px; color: #4c1d95; }
.stat-card .unit { font-size: 12px; color: #8b5cf6; margin-left: 4px; }
.distribution {
padding: 16px;
background: #faf5ff;
border: 1px dashed #c4b5fd;
border-radius: 8px;
text-align: center;
color: #6d28d9;
font-style: italic;
margin-top: 8px;
}
.anomaly-list {
list-style: none;
padding: 0;
margin: 0;
}
.anomaly-list li {
padding: 12px 16px;
border-left: 4px solid #cbd5e1;
background: #f8fafc;
border-radius: 4px;
margin-bottom: 8px;
}
.anomaly-list li.high { border-left-color: #c53030; background: #fef2f2; color: #991b1b; }
.anomaly-list li.normal { border-left-color: #059669; background: #ecfdf5; color: #065f46; }
.anomaly-list li strong { display: block; margin-bottom: 4px; }
.comment-block {
padding: 14px 18px;
background: #faf5ff;
border-left: 4px solid #a855f7;
border-radius: 4px;
color: #4c1d95;
margin-bottom: 8px;
white-space: pre-wrap;
}
.comment-block strong { display: block; margin-bottom: 6px; color: #6d28d9; }
.footer { margin-top: 16px; color: #8c959f; font-size: 12px; text-align: right; }
</style>
</head>
<body>
<div class="report">
<div class="header">
<h1>{{TITLE}}<span class="risk-badge {{RISK_CLASS}}">{{RISK_LABEL}}</span></h1>
<div class="meta">报告编号:{{REPORT_ID}} · 课程:{{COURSE_NAME}} · 任课教师:{{TEACHER_NAME}}</div>
<div class="meta">学期:{{SEMESTER}} · 分析完成时间:{{GENERATED_AT}}</div>
</div>
<div class="section">
<h2>统计指标</h2>
<div class="stat-grid">
<div class="stat-card"><div class="label">最高分</div><div class="value">{{MAX_SCORE}}<span class="unit">分</span></div></div>
<div class="stat-card"><div class="label">最低分</div><div class="value">{{MIN_SCORE}}<span class="unit">分</span></div></div>
<div class="stat-card"><div class="label">平均分</div><div class="value">{{AVG_SCORE}}<span class="unit">分</span></div></div>
<div class="stat-card"><div class="label">中位数</div><div class="value">{{MEDIAN_SCORE}}<span class="unit">分</span></div></div>
<div class="stat-card"><div class="label">标准差</div><div class="value">{{STD_SCORE}}</div></div>
<div class="stat-card"><div class="label">及格率</div><div class="value">{{PASS_RATE}}<span class="unit">%</span></div></div>
<div class="stat-card"><div class="label">优秀率</div><div class="value">{{EXCELLENT_RATE}}<span class="unit">%</span></div></div>
<div class="stat-card"><div class="label">学生数</div><div class="value">{{STUDENT_COUNT}}</div></div>
</div>
<div class="distribution">{{DISTRIBUTION_PLACEHOLDER}}</div>
</div>
<div class="section">
<h2>异常模式识别</h2>
<ul class="anomaly-list">
{{ANOMALY_BODY}}
</ul>
</div>
<div class="section">
<h2>AI 评语(草稿,请教师修改后存档)</h2>
<div class="comment-block">
<strong>📊 总体描述</strong>
{{COMMENT_OVERVIEW}}
</div>
<div class="comment-block">
<strong>⚠️ 存在问题</strong>
{{COMMENT_ISSUES}}
</div>
<div class="comment-block">
<strong>💡 改进建议</strong>
{{COMMENT_SUGGESTIONS}}
</div>
</div>
<div class="footer">本报告由「课程成绩异常诊断与报告生成」技能自动生成 · 最终评价权归教学督导与学院</div>
</div>
</body>
</html>

182
development/REQUIREMENTS.md Normal file
View File

@@ -0,0 +1,182 @@
# 课程成绩异常诊断与报告生成 — 技能需求文档
> 派生自匠厂 skill-template 主仓 `development/REQUIREMENTS.md`,对应规格书第 4.2 节「🟣 教务处 Skill 002 —— 成绩分析"异常诊断与报告生成"」。
---
## 0. 技能标识
- **拟定 slug(英文)**:`analyze-grade-distribution-aa`
- **中文 name**:课程成绩异常诊断与报告生成
- **中文 description(一句话)**:教师在教务系统录完成绩后,自动计算统计指标、识别"高分低能 / 两极分化 / 挂科异常"三种异常模式、生成三段式评语,输出图文并茂的《课程成绩分析报告》
- **account-manager platform_key(如有)**:N/A
- **命名形态**:标准型
- **目标 OEM 市场 `brands`(可选)**:未写则仅挂匠厂
- **网页 RPA(如有)— 登录策略**:**不适用**
## 0.1 宿主入口定位(必填)
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 教程章节 | 不挂理由 |
|---------------------------|------|----------|------|----------|----------|
| `health` / `version` / `config-path` | 挂 | 不挂 | 不挂 | 入口二 | 默认必备 |
| `analyze-grade-distribution` | 挂 | row(课程成绩) | 不挂 | 入口一/入口二 | sync |
| `batch-analyze-courses` | 挂 | toolbar(课程成绩) | 不挂 | 入口一/入口二 | async |
| `render-html` | 挂 | row(分析报告) | 不挂 | 入口一/入口二 | sync |
| `flag-high-risk-course` | 不挂 | row(分析报告) | 不挂 | 入口一 | 写状态 |
| `sync-historical-grades` | 挂 | toolbar(历史成绩) | 挂(占位) | 入口一/入口三 | cron 占位 |
### 0.1.1 用户市场文档
- `README.md`:能力 / 场景 / 边界
- `TUTORIAL.md`:覆盖两入口,入口三写「占位」
## 0.2 AI + RPA 协作与授权边界
### Agent 设计
- 用户意图:分析单门课程 / 批量分析 / 查看报告 / 解释预警 / 标记高风险 / 同步历史成绩
- 意图 → Action 映射:
- 分析单门课程 → `analyze-grade-distribution`
- 批量分析 → `batch-analyze-courses`
- 查看报告 → `render-html`
- 标记高风险 → `flag-high-risk-course`
- 同步历史 → `sync-historical-grades`
- 缺参数追问:课程名称 / 学期 / 课程 ID
- 必须确认的动作:
- `batch-analyze-courses` 批量分析前确认范围与数量
- `flag-high-risk-course` 标记前提示推送对象
- 结果解释:成功(评分 + 异常模式 + 评语)、部分成功(评语生成失败但统计完成)、失败(历史数据缺失 → 仅做统计不预警)
### 授权与平台边界
- 教务系统 API 由教务处统一开通
- 学生个人成绩明细受教务系统权限控制,技能仅输出聚合统计与匿名分布
- 严禁逆向教务系统、破解私有协议
- 异常检测算法调参时停止并上报
### RPA 风控设计
- 执行端:**N/A**
## 0.3 业务行生命周期
本技能对「分析报告」无角色约束(一次性报告存档)。
---
## 1. 文档目标
本文档用于明确 `analyze-grade-distribution-aa` 的研发范围、交付标准、依赖关系和验收要求。
## 2. 业务背景
- **现状**:教师每学期末需撰写课程总结与成绩分析报告,平均耗时半天
- **痛点**:人工计算统计指标效率低;"高分低能 / 两极分化 / 挂科异常"难以识别;督导难以及时锁定高风险课程
- **目标**:自动完成「统计指标计算 + 三种异常识别 + AI 评语生成」,输出「HTML 报告 + 高风险课程标记」
## 3. 开发目标
- 拉取某课程全班成绩明细(平时分 / 期中分 / 实验分 / 期末分 / 总评)
- 拉取该课程的教学大纲(考核项权重)
- 拉取历史成绩数据(前置课程 / 同课程历史)
- 计算统计指标 + 正态分布 + 三种异常模式识别
- 生成 AI 三段式评语
- 输出 HTML 报告 + 异常预警通知
- 写入业务表(课程成绩 / 分析报告 / 历史成绩 / 高风险课程)
## 4. 功能范围
### 必须实现
- 支持 `python scripts/main.py health` / `version` / `config-path` / `init-db`
- 支持 `python scripts/main.py analyze-grade-distribution --course-id <id>`
- 支持 `python scripts/main.py batch-analyze-courses --semester-id <id>`(async)
- 支持 `python scripts/main.py render-html --report-id <id>`
- 支持 `python scripts/main.py flag-high-risk-course --report-id <id>`
- 支持 `python scripts/main.py sync-historical-grades`(async,cron 占位)
- 完成 §0.1 入口定位表
### 可后续扩展
- 多课程横向对比报告
- 学生个人学习轨迹预警
- 与学校教学质量监控平台对接
## 5. 非功能要求
- 目录结构符合 skill-template 规范
- 入口统一为 `scripts/main.py`
- 用户市场四 Tab
- 输出机读友好;错误前缀 `ERROR:`
- Windows UTF-8 兼容
- 单门课程分析耗时 ≤ 30 秒(学生数 ≤ 200)
## 6. 输入输出要求
### 输入
- `course_id`:课程主键(必填)
- `semester_id`:学期主键(仅 batch 必填)
- `report_id`:报告主键(仅 render / flag 必填)
- 依赖系统:教务系统 API(成绩读写)
- 环境变量(`.env`):
- `AA_API_BASE_URL`:教务系统 API 入口
- `AA_API_TOKEN`:调用凭证
- `HIGH_FAIL_RATE_THRESHOLD`:挂科异常阈值(默认 0.30)
- `SCORE_GAP_THRESHOLD`:高分低能阈值(默认 20)
- `STD_DEV_RATIO_THRESHOLD`:两极分化阈值(默认 1.5)
### 输出
- 成功结果:`analyze-grade-distribution` 返回 `{max_score, min_score, avg, median, std, pass_rate, excellent_rate, anomaly_flags, ai_comment}`
- 错误输出:`ERROR:` 稳定码 + 中文 message
- 数据库记录:写入 `分析报告` 表
- HTML 报告:落到 `html/`
## 7. 依赖的兄弟技能或外部系统
### 兄弟技能依赖
- 无强制依赖
### 外部系统依赖
- **教务系统 API**(成绩读写):必须
### Python 包依赖(requirements.txt)
- HTTP 请求:`requests>=2.31.0,<3`
- 数值计算:`numpy>=1.24.0,<2`
- 数据处理:`pandas>=2.0.0,<3`
- 统计可视化(图表占位):`matplotlib>=3.7.0,<4`
- `jiangchang-platform-kit` 由宿主提供
## 8. 不在本次范围内的内容
- 本次不实现学生个人学习轨迹预警
- 本次不集成教学质量监控平台
- 本次不做 cron 启用(占位 Action 已就位)
- 本次不兼容旧模板历史结构
## 9. 验收标准
- 代码结构符合模板规范
- `health` / `version` / `config-path` / `init-db` 命令执行正常
- `analyze-grade-distribution` 在含 100 门课程的样例库中完成一次分析,耗时 ≤ 30 秒
- 业务表写入和查询符合 `references/SCHEMA.md`
- 异常识别准确率 ≥ 90%(基于样例库)
- `SKILL.md` 的 `developer_ids` 已配置
- 安装后按 §0.1 完成宿主多入口验收
## 10. 开发注意事项
- 只修改当前 skill 仓库
- 学生个人成绩明细受教务系统权限控制
- 异常检测阈值可在 `runtime/config` 调参
- `cli` 只做参数解析;核心逻辑在 `service`
## 11. 变更记录
| 日期 | 版本 | 变更人 | 变更内容 |
|------|------|--------|----------|
| 2026-09-14 | v1.0.1 | 匠厂 | 初版需求文档,骨架就位 |

8
pytest.ini Normal file
View File

@@ -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

424
release.ps1 Normal file
View File

@@ -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 <utf8-msg-file> ($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
}

14
requirements.txt Normal file
View File

@@ -0,0 +1,14 @@
# 课程成绩异常诊断与报告生成 - 技能特有依赖
# jiangchang-platform-kit / playwright 等公共能力由宿主共享 runtime 提供,不要写入此文件
# HTTP 请求
requests>=2.31.0,<3
# 数值计算
numpy>=1.24.0,<2
# 数据处理
pandas>=2.0.0,<3
# 统计可视化(图表占位,可选)
matplotlib>=3.7.0,<4

1
scripts/cli/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""scripts.cli - CLI 参数解析层(占位)"""

8
scripts/db/__init__.py Normal file
View File

@@ -0,0 +1,8 @@
"""scripts.db - 数据库访问层(占位)
按 references/SCHEMA.md 实现表与字段:
- 制度送审稿
- 冲突条款清单
- 制度库索引
- 审查任务日志
"""

91
scripts/main.py Normal file
View File

@@ -0,0 +1,91 @@
#!/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"analyze-grade-distribution-aa health check (placeholder)")
print(f"base_dir: {BASE_DIR}")
return 0
def version():
"""查看版本(占位)。"""
print("analyze-grade-distribution-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 analyze_grade_distribution(course_id: str):
"""分析这份成绩。"""
print(f"analyze-grade-distribution: course_id={course_id} (待开发者实现)")
return 0
def batch_analyze_courses(semester_id: str):
"""批量分析本学期。"""
print(f"batch-analyze-courses: semester_id={semester_id} (待开发者实现)")
return 0
def render_html(report_id: str):
"""查看分析报告。"""
print(f"render-html: report_id={report_id} (待开发者实现)")
return 0
def flag_high_risk_course(report_id: str):
"""标记为高风险课程。"""
print(f"flag-high-risk-course: report_id={report_id} (待开发者实现)")
return 0
def sync_historical_grades():
"""同步历史成绩。"""
print("sync-historical-grades (待开发者实现)")
return 0
COMMANDS = {
"health": health,
"version": version,
"config-path": config_path,
"init-db": init_db,
"analyze-grade-distribution": analyze_grade_distribution,
"batch-analyze-courses": batch_analyze_courses,
"render-html": render_html,
"flag-high-risk-course": flag_high_risk_course,
"sync-historical-grades": sync_historical_grades,
}
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 <command>")
print("Commands:", ", ".join(COMMANDS.keys()))
return 1
return COMMANDS[argv[0]](*argv[1:])
if __name__ == "__main__":
sys.exit(main())

View File

@@ -0,0 +1,9 @@
"""scripts.service - 业务核心逻辑层(占位)
按 development/REQUIREMENTS.md §3 开发目标实现:
- 条款解析与向量化
- 制度库语义比对
- 程序合规体检
- 重复发文检测
- 报告生成与批注回写
"""

1
scripts/util/__init__.py Normal file
View File

@@ -0,0 +1 @@
"""scripts.util - 工具函数层(占位)"""