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:
26
.env.example
Normal file
26
.env.example
Normal 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
10
.gitignore
vendored
Normal 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
5
.openclaw-skill-template
Normal 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
12
CHANGELOG.md
Normal 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
27
DEMO.md
Normal 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
21
LICENSE
Normal 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
82
README.md
Normal 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
20
SECURITY.md
Normal 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
129
SKILL.md
Normal 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
144
TUTORIAL.md
Normal 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
123
assets/actions.json
Normal 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": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
145
assets/html/example-report.html
Normal file
145
assets/html/example-report.html
Normal 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
182
development/REQUIREMENTS.md
Normal 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
8
pytest.ini
Normal 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
424
release.ps1
Normal 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
14
requirements.txt
Normal 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
1
scripts/cli/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""scripts.cli - CLI 参数解析层(占位)"""
|
||||||
8
scripts/db/__init__.py
Normal file
8
scripts/db/__init__.py
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
"""scripts.db - 数据库访问层(占位)
|
||||||
|
|
||||||
|
按 references/SCHEMA.md 实现表与字段:
|
||||||
|
- 制度送审稿
|
||||||
|
- 冲突条款清单
|
||||||
|
- 制度库索引
|
||||||
|
- 审查任务日志
|
||||||
|
"""
|
||||||
91
scripts/main.py
Normal file
91
scripts/main.py
Normal 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())
|
||||||
9
scripts/service/__init__.py
Normal file
9
scripts/service/__init__.py
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
"""scripts.service - 业务核心逻辑层(占位)
|
||||||
|
|
||||||
|
按 development/REQUIREMENTS.md §3 开发目标实现:
|
||||||
|
- 条款解析与向量化
|
||||||
|
- 制度库语义比对
|
||||||
|
- 程序合规体检
|
||||||
|
- 重复发文检测
|
||||||
|
- 报告生成与批注回写
|
||||||
|
"""
|
||||||
1
scripts/util/__init__.py
Normal file
1
scripts/util/__init__.py
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"""scripts.util - 工具函数层(占位)"""
|
||||||
Reference in New Issue
Block a user