From 3f87f223f99327614de9fd66a145c1c3b50c19a5 Mon Sep 17 00:00:00 2001 From: taojianmin Date: Mon, 14 Sep 2026 12:19:13 +0800 Subject: [PATCH] =?UTF-8?q?v1.0.1:=20=E5=A5=96=E5=8A=A9=E5=AD=A6=E9=87=91?= =?UTF-8?q?=E8=AF=84=E5=AE=9A=E9=80=BB=E8=BE=91=E9=98=B2=E4=BC=AA=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=20-=20=E5=88=9D=E7=89=88=E9=AA=A8=E6=9E=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。 核心业务: - 辅导员在汇总表中批量勾选候选人后自动触发 - 多源数据一致性校验(自报家庭年收入 vs 一卡通消费 vs 生源地 GDP) - 横向排名可疑检测(综测 vs 成绩排名差值过大) - 历史重复资助排查(本学年累计金额超限) 输出形态: - 《奖助评定复核建议清单》HTML 报告 - 高风险个案推送至辅导员工作台 - 复核结果留痕(核查说明 + 终审结论) 预期提效:资助中心抽查工作量减少 80%;因认定不公引发的学生投诉事件下降 60%。 业务行生命周期:待审 → 已标注 → 已复核(保留 / 取消 / 调减)/ 已驳回 / 已撤回 Co-Authored-By: Claude Code --- .env.example | 38 +++ .gitignore | 10 + .openclaw-skill-template | 5 + CHANGELOG.md | 12 + DEMO.md | 29 +++ LICENSE | 21 ++ README.md | 82 ++++++ SECURITY.md | 20 ++ SKILL.md | 129 ++++++++++ TUTORIAL.md | 147 +++++++++++ assets/actions.json | 153 ++++++++++++ assets/html/example-report.html | 122 +++++++++ development/REQUIREMENTS.md | 213 ++++++++++++++++ pytest.ini | 8 + release.ps1 | 424 ++++++++++++++++++++++++++++++++ requirements.txt | 11 + scripts/cli/__init__.py | 1 + scripts/db/__init__.py | 8 + scripts/main.py | 105 ++++++++ scripts/service/__init__.py | 9 + scripts/util/__init__.py | 1 + 21 files changed, 1548 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .openclaw-skill-template create mode 100644 CHANGELOG.md create mode 100644 DEMO.md create mode 100644 LICENSE create mode 100644 README.md create mode 100644 SECURITY.md create mode 100644 SKILL.md create mode 100644 TUTORIAL.md create mode 100644 assets/actions.json create mode 100644 assets/html/example-report.html create mode 100644 development/REQUIREMENTS.md create mode 100644 pytest.ini create mode 100644 release.ps1 create mode 100644 requirements.txt create mode 100644 scripts/cli/__init__.py create mode 100644 scripts/db/__init__.py create mode 100644 scripts/main.py create mode 100644 scripts/service/__init__.py create mode 100644 scripts/util/__init__.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..d073627 --- /dev/null +++ b/.env.example @@ -0,0 +1,38 @@ +# 奖助学金评定逻辑防伪校验 - 配置示例 +# 行头说明「是什么」;行尾说明「怎么填」;进程环境变量 > 用户 .env > 本文件 + +# 学生工作部 API 入口,例如 https://sas.example.edu/api/v1 +SAS_API_BASE_URL= + +# 学生工作部 API 调用凭证 +SAS_API_TOKEN= + +# 一卡通中心 API 入口 +CARD_API_BASE_URL= + +# 一卡通中心 API 调用凭证 +CARD_API_TOKEN= + +# 教务系统 API 入口 +AA_API_BASE_URL= + +# 教务系统 API 调用凭证 +AA_API_TOKEN= + +# 公开统计来源(生源地 GDP 数据),例如国家统计局接口 +GDP_STAT_API_BASE_URL=https://data.stats.gov.cn + +# 年度累计资助上限(元),由学校资助管理中心按年度发布 +ANNUAL_AID_LIMIT=8000 + +# 成绩排名与综测排名差值阈值(超过即触发预警),默认 10 +SCORE_GAP_THRESHOLD=10 + +# 消费 / 年收入比值阈值(超过即触发预警),默认 0.5 +CONSUMPTION_INCOME_RATIO=0.5 + +# 单候选人校验超时(秒),默认 5 +VERIFY_TIMEOUT_SEC=5 + +# 错误码前缀,统一为 ERROR: 起头 +LOG_LEVEL=INFO diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08d4ec6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.py[cod] +.env +*.env.local +.pytest_cache/ +.coverage +htmlcov/ +dist/ +build/ +*.egg-info/ \ No newline at end of file diff --git a/.openclaw-skill-template b/.openclaw-skill-template new file mode 100644 index 0000000..5464d21 --- /dev/null +++ b/.openclaw-skill-template @@ -0,0 +1,5 @@ +# 此文件仅存在于 skill-template 源仓库。 +# 用 tools/scaffold_skill 创建新技能时,脚本会从目标目录删除本文件。 +# 若业务技能仓库中仍存在本文件,说明未正确 scaffold,tests 会失败。 +template_slug: your-skill-slug +template_version: "1.0.32" diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e738a67 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog + +本文件对应技能市场 **「更新日志」** Tab。 + +## 1.0.1 + +- 初版:基于匠厂 skill-template 主仓 v1.0.108 派生 +- 实现核心业务:奖助学金评定逻辑防伪校验(多源数据一致性 + 横向排名可疑 + 历史重复资助排查) +- 输出形态:复核建议清单 HTML 报告 + 高风险个案推送 + 异常标注 + 复核结果留痕 +- 预期提效:资助中心抽查工作量减少 80%;因认定不公引发的学生投诉事件下降 60% +- 业务行生命周期:待审 → 已标注 → 已复核 / 已驳回 +- 占位说明:本版仅完成骨架与文档,业务实现细节(`scripts/` 下 Python 模块、异常检测阈值、跨源数据对账逻辑)由开发者后续按 `development/REQUIREMENTS.md` 补全 diff --git a/DEMO.md b/DEMO.md new file mode 100644 index 0000000..e5c7fed --- /dev/null +++ b/DEMO.md @@ -0,0 +1,29 @@ +--- +demo_video_url: "" +--- + +# 演示说明 + +> 本技能当前未提供演示视频。下方用图文形式描述完整校验流程与结果样例。 + +## 演示看点 + +1. **数据管理 → 候选人汇总表 → 批量勾选 + AI 校验**:辅导员批量勾选某班级候选人,10~30 秒内完成多源数据比对 +2. **结果样例**: + - 复核建议清单表新增若干高风险个案,每行含「异常类型 + 异常详情 + 建议处理」 + - 异常候选人行状态变为「已标注」 + - 辅导员工作台收到待复核通知 + - 学生处资助中心科员收到复核个案汇总 + +## 没有视频时 + +可参考下方截图位(待补): + +- 截图位 1:候选人汇总表(已标注状态) +- 截图位 2:复核建议清单表(异常类型分类) +- 截图位 3:HTML 复核清单(高风险个案 + 异常详情) +- 截图位 4:辅导员工作台待复核通知 + +## 何时补视频 + +v1.1.0 起,按 `development/TUTORIAL_SURFACES.md` §3 录制 1~2 分钟演示视频,覆盖勾选候选人 → 触发校验 → 查看复核清单 → 提交复核结果的全流程。 diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bb90574 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 深圳匠厂科技有限公司 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..dfeec3b --- /dev/null +++ b/README.md @@ -0,0 +1,82 @@ +--- +description: "辅导员在汇总表中批量勾选候选人后,自动比对自报家庭年收入与一卡通消费、综测排名与成绩排名、本学年累计资助金额,生成《奖助评定复核建议清单》。" +--- + +# 奖助学金评定逻辑防伪校验 + +辅导员在奖助学金评审期批量勾选候选人后,技能会自动比对四类异常:自报家庭年收入与一卡通月均消费是否吻合、家庭住址所在县域人均 GDP 与致贫原因是否合理、综合素质测评排名与成绩排名差值是否过大、本学年累计已获资助是否超限。生成《奖助评定复核建议清单》,把高风险个案推到辅导员工作台,要求人工复核并填写核查说明后才能提交终审。 + +--- + +## 这个技能能帮你做什么 + +- **多源数据一致性校验**:自报家庭年收入 vs 一卡通消费水平 vs 生源地人均 GDP,全方位交叉比对 +- **横向排名可疑检测**:同一班级 / 专业内,成绩排名与综测排名差值过大自动标注 +- **历史重复资助排查**:自动检索本学年已获得的其他资助,累计金额超学校上限自动预警 +- **高风险个案推送**:对存在异常的候选人推到辅导员工作台,要求人工复核 +- **复核结果留痕**:辅导员的核查说明 + 终审结论全部入库,可追溯 + +--- + +## 适合什么场景 + +- 辅导员在国家助学金 / 国家励志奖学金 / 校级奖学金评定期,需要快速筛选高风险个案 +- 学生处资助中心需要在终审前集中复核所有待复核个案 +- 二级学院学生工作副书记需要在评定会上快速决策 + +--- + +## 使用前需要准备什么 + +- 学生工作部 API 凭证(`SAS_API_TOKEN`)已配置到本技能 `.env` +- 一卡通中心 API 凭证(`CARD_API_TOKEN`)已配置到本技能 `.env` +- 教务系统 API 凭证(`AA_API_TOKEN`)已配置到本技能 `.env` +- 学生提交的《家庭经济困难认定表》已录入学生工作部系统 +- 一卡通消费数据已同步到本技能 +- 生源地人均 GDP 数据(最新年度公开统计)已下载到本技能 + +--- + +## 怎么使用 + +本技能支持**数据管理**(候选人表上点工具栏按钮)与**对话任务**(自然语言让 Agent 完成)。定时任务为占位,按需启用。 + +**逐步操作与排障,请查看技能市场「使用教程」Tab。** + +--- + +## 执行后你会得到什么 + +- 一份《奖助评定复核建议清单》HTML 报告,含高风险个案清单与异常详情 +- 候选人表状态更新(待审 → 已标注 / 已复核) +- 辅导员工作台收到待复核通知(如有异常) +- 学生处资助中心科员收到待复核个案汇总 + +--- + +## 注意事项 + +- 本技能**不替代辅导员终审**,所有异常仍需人工填写核查说明 +- 一卡通消费数据可能受学生校外消费、转账等影响,异常仅作预警,不作"造假"定性 +- 生源地人均 GDP 为公开统计数据,可能与实际家庭状况有差异,技能仅做粗筛 +- 学生家庭隐私数据(年收入 / 致贫原因)受严格保密,技能不写入日志,导出报告脱敏 + +--- + +## 常见问题 + +### 这个技能适合我吗? + +> 如果你的工作包含「奖助学金评审期集中复核」,本技能可以一气呵成完成多源数据交叉比对。 + +### 异常一定会被认定造假吗? + +> 不会。异常仅作预警,最终仍由辅导员人工复核;如确有合理解释(如家庭临时大额汇款、亲属转账),可填写核查说明后通过。 + +### 累计资助上限是多少? + +> 由学校资助管理中心按年度发布;本技能默认读取 `ANNUAL_AID_LIMIT` 配置项,可按需调整。 + +### 生源地 GDP 数据多久更新一次? + +> 建议每年评定季前更新一次;技能支持手动触发同步,也支持 cron 占位。 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..657e660 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,20 @@ +# 安全漏洞披露 + +本仓库是匠厂技能开发模板。请把**未公开的安全漏洞**发给深圳匠厂科技有限公司,不要在公开 Issue、PR 或讨论区里贴完整复现步骤、PoC 或利用细节。 + +## 怎么报告 + +请通过匠厂官网联系渠道提交:[https://www.jc2009.com/](https://www.jc2009.com/) + +建议在联系时注明: + +- 标题或开头带 `[Security]` +- 影响的仓库 / 版本(例如 `skill-template`、tag `v1.0.x`) +- 问题类型、影响范围、复现步骤 +- 若已有修复思路,可一并说明 + +我们会确认收悉,并在有结论后告知是否修复、是否公开披露。 + +## 不在本页范围 + +本页只说明**如何向我们报告漏洞**。技能运行时的权限、RPA 合规、加密发布等产品安全设计,见 `development/` 与 `references/`,不要写进本文件。 diff --git a/SKILL.md b/SKILL.md new file mode 100644 index 0000000..38cb204 --- /dev/null +++ b/SKILL.md @@ -0,0 +1,129 @@ +--- +name: 奖助学金评定逻辑防伪校验 +description: "辅导员在汇总表中批量勾选候选人后,自动比对自报家庭年收入与一卡通消费、综测排名与成绩排名、本学年累计资助金额,生成《奖助评定复核建议清单》。" +version: 1.0.1 +author: 深圳匠厂科技有限公司 +metadata: + openclaw: + slug: verify-scholarship-fraud-sas + platform_kit_min_version: "1.2.11" + emoji: "🛡" + category: "学生" + developer_ids: + - 12345 # 占位,请换成你的匠厂用户 ID + - 12468 +allowed-tools: + - bash +--- + +# 奖助学金评定逻辑防伪校验(verify-scholarship-fraud-sas) + +这是基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。覆盖需求规格书第 4.3 节「🟠 学生处 Skill 001 —— 奖助学金评定"逻辑防伪校验"」。 + +## Agent / 宿主编排(复制为业务技能后必改) + +### AI + RPA 定位 + +- **Agent 是入口与编排者**:理解用户意图("校验国家助学金候选人"、"标记这位学生为待复核")、补齐参数、判断风险、选择 Action,并用业务语言解释结果。 +- **本技能不涉及浏览器 / 桌面 / 手机 RPA**:一卡通消费数据、综测成绩、资助台账均走官方 API;生源地人均 GDP 从公开统计数据抓取。 + +### 架构铁律(先读) + +- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**。 +- **单业务内核、多入口**:所有入口必须复用同一 domain service。 +- **业务行生命周期**:本技能对「候选人行」有"待审 / 已标注 / 已复核 / 已驳回"角色约束,遵守 `references/WORK_ITEM_STATUS.md`。 +- **HTML 整页报告**:本技能输出《奖助评定复核建议清单》走 `render-html`。 + +### 如何触发(能力类型) + +| 你要做什么 | 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` → `verify-candidate`(输入:`candidate_id`) | 否(sync) | +| 批量校验某班级所有候选人 | `run_skill_action` → `batch-verify-class`(输入:`class_id`) | 是(async) | +| 标记候选人为待复核 | `run_skill_action` → `mark-needs-review`(输入:`candidate_id` + 复核原因) | 否(sync) | +| 提交复核结果 | `run_skill_action` → `submit-review-result`(输入:`candidate_id` + 结论 + 核查说明) | 否(sync) | +| 查看复核建议清单 | `run_skill_action` → `render-html`(输入:`list_id`) | 否(sync) | + +## 共享 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:`verify-scholarship-fraud-sas`(`verify` 动词 + `scholarship-fraud` 名词 + `sas` student affairs 学生处缩写) +- name:**用户可见的中文业务名称**;`description` 是说明,不是名称。 +- 目录名、默认 `{slug}.db` 使用 slug,不使用中文。 + +## 平台元数据 + +- `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表。 +- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。 + +--- + +> **本技能开发状态:** v1.0.1 骨架已就位,业务实现细节见 `development/REQUIREMENTS.md`;具体 Python 模块由开发者后续补全。 diff --git a/TUTORIAL.md b/TUTORIAL.md new file mode 100644 index 0000000..38139f2 --- /dev/null +++ b/TUTORIAL.md @@ -0,0 +1,147 @@ +# 使用教程 + +本技能有两种**平行**的使用方式: + +- **数据管理**:在左侧「候选人汇总表」上点工具栏 / 行内按钮 +- **对话任务**:用自然语言让 Agent 完成同一能力 + +定时任务是否提供:**见下方「入口三」**(当前为占位,按需启用)。 + +--- + +## 本技能做什么 + +```text +【候选人汇总表 + 一卡通消费 + 综测排名 + 历史资助】 → 【四类异常检测】 → 【复核建议清单】 → 【辅导员人工复核 → 终审】 +``` + +- **输入**:辅导员批量勾选的候选人 + 一卡通消费数据 + 综测成绩排名 + 本学年已获资助记录 + 生源地人均 GDP +- **输出**:《奖助评定复核建议清单》HTML 报告 + 高风险个案推送 +- **说明**:本技能不替代辅导员终审;所有异常仍需人工填写核查说明 + +--- + +## 开始前 + +确认: + +1. 已安装并登录匠厂客户端 +2. 已在技能市场安装本技能(建议更新到最新版) +3. 学生工作部 API / 一卡通中心 API / 教务系统 API 凭证已配置 +4. 【如需】首次使用前先在「数据管理 → 一卡通消费」点击「立即同步」 +5. 【如需】首次使用前先在「数据管理 → 生源地 GDP」点击「同步最新公开数据」 + +--- + +## 入口一:数据管理 + +适合:辅导员、学生处资助中心科员。 + +### 左侧各表是干什么的 + +| 表名(中文) | 作用 | +|--------------|------| +| 候选人汇总表 | 各班级辅导员批量勾选的国家助学金 / 励志奖学金 / 校级奖学金候选人 | +| 复核建议清单 | 防伪校验产出的高风险个案清单(含异常详情) | +| 困难认定 | 学生《家庭经济困难认定表》数据(只读) | +| 一卡通消费 | 学生食堂 / 超市月均消费额(只读) | +| 历史资助 | 本学年已获资助记录(用于累计金额比对) | + +### 首次准备(若需要) + +1. 进入 **数据管理** → **一卡通消费** +2. 点击表顶 **「立即同步」**(空表可点) +3. 成功后应看到:全校学生的一卡通月均消费额 + +### 主流程:批量校验某班级候选人 + +1. 进入 **候选人汇总表** +2. 勾选某班级所有候选人 → 点击 **「AI 校验」** +3. 等待 10~30 秒(按班级规模) +4. 完成后: + - 复核建议清单表新增若干行(含高风险个案) + - 异常候选人行状态变为「已标注」 + - 辅导员工作台收到待复核通知 + - 学生处资助中心科员收到复核个案汇总 + +### 行级 / 批量 + +| 你想做什么 | 操作 | +|------------|------| +| 校验一位候选人 | **候选人汇总表** 行内 → **「AI 校验」** | +| 批量校验某班级 | 勾选多行 → **「批量校验」** | +| 标记为待复核 | **候选人汇总表** 行内 → **「标记为待复核」** | +| 提交复核结果 | **复核建议清单** 行内 → **「提交复核结果」** | +| 查看复核清单 | **复核建议清单** 行内 → **「查看清单」** | +| 同步一卡通数据 | **一卡通消费** 表顶 → **「立即同步」** | + +--- + +## 入口二:对话任务 + +适合:临时提问、不想切换表的用户。 + +### 什么时候用对话 + +- 临时想校验一位候选人,但不想打开数据管理 +- 想用自然语言解释「为什么这位学生被标为异常」 +- 想看「本月一共多少位高风险个案」 + +### 示例话术 + +| 你想做什么 | 可以这样说 | +|------------|------------| +| 校验一位候选人 | 「帮我校验张同学的助学金申请」 | +| 批量校验 | 「请把 XX 班所有候选人都校验一遍」 | +| 标记待复核 | 「把李同学标记为待复核」 | +| 提交复核 | 「张同学的核查说明已写好,提交复核」 | +| 查看本月统计 | 「本月一共多少位高风险个案?」 | + +### Agent 可能追问什么 + +- 缺参数时:「请告诉我学生姓名 / 学号 / 班级」 +- 一卡通数据缺失时:「该生一卡通消费数据未同步,请先到数据管理同步」 +- 失败时:可改用数据管理点按钮重新触发 + +--- + +## 入口三:定时任务 + +### 本技能当前为占位(按需启用) + +本技能**当前未启用定时任务入口**。如未来启用,建议场景: + +- 每学期评定季开始前 7 天自动同步一卡通数据 +- 评定季期间每日自动扫描已提交候选人的异常 + +启用条件:学生工作部系统提供定时回调 API,并经学生处资助中心确认自动化边界。 + +当前请使用 **数据管理** 或 **对话任务**。 + +--- + +## 主能力对照表 + +| 想做什么 | 数据管理 | 对话任务 | +|----------|----------|----------| +| 校验一位候选人 | **候选人汇总表** 行内 → **「AI 校验」** | 「帮我校验张同学的助学金申请」 | +| 批量校验 | 勾选多行 → **「批量校验」** | 「请把 XX 班所有候选人都校验一遍」 | +| 标记待复核 | 行内 → **「标记为待复核」** | 「把李同学标记为待复核」 | +| 提交复核结果 | **复核建议清单** 行内 → **「提交复核结果」** | 「张同学的核查说明已写好,提交复核」 | +| 查看复核清单 | **复核建议清单** 行内 → **「查看清单」** | 「把本月复核清单发给我」 | + +按钮名必须与 `assets/actions.json` 的 `label` 一致。 + +--- + +## 常见卡住怎么办 + +| 现象 | 你可以先试 | +|------|------------| +| 「AI 校验」点不了 | 是否已配置三套 API;是否装错版本 | +| 校验很久没结果 | 看任务中心进度;候选人 > 500 可能耗时较久 | +| 异常过多 | 检查一卡通同步是否完整;本源 GDP 数据是否最新 | +| 复核提交失败 | 是否填写完整核查说明;是否在审核期内 | +| 学生一卡通数据缺失 | 学生可能近期未消费;可手动标记为「数据缺失」并跳过此项 | + +仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。 diff --git a/assets/actions.json b/assets/actions.json new file mode 100644 index 0000000..f806adc --- /dev/null +++ b/assets/actions.json @@ -0,0 +1,153 @@ +{ + "schemaVersion": 1, + "skill": "verify-scholarship-fraud-sas", + "actions": [ + { + "id": "health", + "label": "运行检查", + "description": "检查技能运行环境、学生工作部 / 一卡通中心 / 教务系统 API 连通性、生源地 GDP 库完整性。", + "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": "verify-candidate", + "label": "AI 校验", + "description": "对一位候选人执行四类异常检测(多源数据一致性 + 横向排名 + 历史重复资助),返回异常标签与详情。", + "placements": ["row", "agent"], + "executionProfile": "sync", + "bind": { + "tables": ["候选人汇总表"], + "selectionMode": "required", + "labelColumn": "学生姓名" + }, + "entrypoint": { + "type": "cli", + "command": "verify-candidate", + "args": ["--candidate-id", "{row.候选人ID}"] + } + }, + { + "id": "batch-verify-class", + "label": "批量校验", + "description": "批量校验某班级或某专业下所有候选人,按顺序逐位执行并汇总结果。", + "placements": ["batch", "agent"], + "executionProfile": "async", + "bind": { + "tables": ["候选人汇总表"], + "selectionMode": "optional" + }, + "entrypoint": { + "type": "cli", + "command": "batch-verify-class", + "args": ["--class-id", "{form.class_id}"] + } + }, + { + "id": "mark-needs-review", + "label": "标记为待复核", + "description": "手动将候选人标记为待复核,推送通知给辅导员工作台。", + "placements": ["row"], + "executionProfile": "sync", + "bind": { + "tables": ["候选人汇总表"], + "selectionMode": "required", + "labelColumn": "学生姓名" + }, + "entrypoint": { + "type": "cli", + "command": "mark-needs-review", + "args": ["--candidate-id", "{row.候选人ID}", "--reason", "{form.reason}"] + } + }, + { + "id": "submit-review-result", + "label": "提交复核结果", + "description": "辅导员填写核查说明后提交复核结果(含最终结论:保留 / 取消 / 调减资助)。", + "placements": ["row"], + "executionProfile": "sync", + "bind": { + "tables": ["复核建议清单"], + "selectionMode": "required", + "labelColumn": "学生姓名" + }, + "entrypoint": { + "type": "cli", + "command": "submit-review-result", + "args": ["--candidate-id", "{row.候选人ID}", "--conclusion", "{form.conclusion}", "--review-note", "{form.review_note}"] + } + }, + { + "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": ["--list-id", "{row.清单ID}"] + } + }, + { + "id": "sync-card-data", + "label": "同步一卡通数据", + "description": "从一卡通中心重新拉取消费数据。占位 Action,cron 启用后由定时调度。", + "placements": ["cron", "agent", "toolbar"], + "executionProfile": "async", + "bind": { + "tables": ["一卡通消费"] + }, + "entrypoint": { + "type": "cli", + "command": "sync-card-data", + "args": [] + } + }, + { + "id": "sync-gdp-data", + "label": "同步生源地 GDP 数据", + "description": "从公开统计来源拉取最新生源地人均 GDP 数据。占位 Action,cron 启用后由定时调度。", + "placements": ["cron", "agent"], + "executionProfile": "async", + "entrypoint": { + "type": "cli", + "command": "sync-gdp-data", + "args": [] + } + } + ] +} diff --git a/assets/html/example-report.html b/assets/html/example-report.html new file mode 100644 index 0000000..e6ec767 --- /dev/null +++ b/assets/html/example-report.html @@ -0,0 +1,122 @@ + + + + + {{TITLE}} + + + +
+
+

{{TITLE}}

+
清单编号:{{LIST_ID}} · 批次:{{BATCH_NAME}} · 评定项目:{{AWARD_NAME}}
+
辅导员:{{COUNSELOR_NAME}} · 生成时间:{{GENERATED_AT}}
+
+ +
+ 🔒 本报告含学生家庭隐私数据,已脱敏处理(学号 / 姓名末位)。请按学校保密规定保管,不得外传。 +
+ +
+
校验候选人总数
{{TOTAL_COUNT}}
+
🔴 高风险个案
{{HIGH_RISK_COUNT}}
+
🟡 边缘个案
{{EDGE_COUNT}}
+
🟢 无异常
{{CLEAN_COUNT}}
+
+ +
+

高风险个案复核清单

+ + + + + + + + + + + + {{BODY}} + +
学生(脱敏)异常类型异常详情建议处理复核状态
+
+ + +
+ + diff --git a/development/REQUIREMENTS.md b/development/REQUIREMENTS.md new file mode 100644 index 0000000..c6604ef --- /dev/null +++ b/development/REQUIREMENTS.md @@ -0,0 +1,213 @@ +# 奖助学金评定逻辑防伪校验 — 技能需求文档 + +> 派生自匠厂 skill-template 主仓 `development/REQUIREMENTS.md`,对应规格书第 4.3 节「🟠 学生处 Skill 001 —— 奖助学金评定"逻辑防伪校验"」。 + +--- + +## 0. 技能标识 + +- **拟定 slug(英文)**:`verify-scholarship-fraud-sas` +- **中文 name**:奖助学金评定逻辑防伪校验 +- **中文 description(一句话)**:辅导员在汇总表中批量勾选候选人后,自动比对自报家庭年收入与一卡通消费、综测排名与成绩排名、本学年累计资助金额,生成《奖助评定复核建议清单》 +- **account-manager platform_key(如有)**:N/A +- **命名形态**:标准型 +- **目标 OEM 市场 `brands`(可选)**:未写则仅挂匠厂 +- **网页 RPA(如有)— 登录策略**:**不适用** + +## 0.1 宿主入口定位(必填) + +| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 教程章节 | 不挂理由 | +|---------------------------|------|----------|------|----------|----------| +| `health` / `version` / `config-path` | 挂 | 不挂 | 不挂 | 入口二 | 默认必备 | +| `verify-candidate` | 挂 | row(候选人汇总表) | 不挂 | 入口一/入口二 | sync | +| `batch-verify-class` | 挂 | batch(候选人汇总表) | 不挂 | 入口一/入口二 | async | +| `mark-needs-review` | 不挂 | row(候选人汇总表) | 不挂 | 入口一 | 写状态 | +| `submit-review-result` | 不挂 | row(复核建议清单) | 不挂 | 入口一 | 写状态 | +| `render-html` | 挂 | row(复核建议清单) | 不挂 | 入口一/入口二 | sync | +| `sync-card-data` | 挂 | toolbar(一卡通消费) | 挂(占位) | 入口一/入口三 | cron 占位 | +| `sync-gdp-data` | 挂 | 不挂 | 挂(占位) | 入口三 | cron 占位 | + +### 0.1.1 用户市场文档 + +- `README.md`:能力 / 场景 / 边界 +- `TUTORIAL.md`:覆盖两入口,入口三写「占位」 + +## 0.2 AI + RPA 协作与授权边界 + +### Agent 设计 + +- 用户意图:校验候选人 / 批量校验 / 标记待复核 / 提交复核 / 查看清单 / 同步数据 +- 意图 → Action 映射: + - 单个校验 → `verify-candidate` + - 批量校验 → `batch-verify-class` + - 标记待复核 → `mark-needs-review` + - 提交复核结果 → `submit-review-result` + - 查看清单 → `render-html` + - 同步一卡通 / GDP → `sync-card-data` / `sync-gdp-data` +- 缺参数追问:学生姓名 / 学号 / 班级 +- 必须确认的动作: + - `mark-needs-review` 标记前提示复核原因 + - `submit-review-result` 提交前提示最终结论 +- 结果解释:成功(异常标签 + 详情)、部分成功(一卡通缺失但其他维度校验完成)、失败(数据源全部不可用 → 仅记录到日志) + +### 授权与平台边界 + +- 学生工作部 / 一卡通中心 / 教务系统 API 凭证由学生处统一开通 +- 学生家庭隐私数据(年收入 / 致贫原因)受严格保密 +- 严禁逆向任何系统、破解私有协议 +- 学生数据严禁写入日志 + +### RPA 风控设计 + +- 执行端:**N/A** + +## 0.3 业务行生命周期 + +本技能对「候选人汇总表」中的每一行,遵守 `references/WORK_ITEM_STATUS.md` 角色约束: + +| 角色 | 本技能状态值 | 说明 | +|------|--------------|------| +| 可操作(operable) | 待审 | 默认唯一可被 `verify-candidate` 捞起 | +| 进行中(in_progress) | 已标注 | AI 已发现异常,等待辅导员复核 | +| 成功类终态 | 已复核(保留) | 辅导员填写核查说明并通过 | +| 失败类终态 | 已复核(取消 / 调减) / 已驳回 | 默认不可再处理;失败原因字段:核查说明 | +| 跳过类终态 | 已撤回 | 学生主动撤回申请 | + +> 重做靠人工将"已复核 / 已驳回"改回"待审",不靠 AI 自动覆盖。 + +--- + +## 1. 文档目标 + +本文档用于明确 `verify-scholarship-fraud-sas` 的研发范围、交付标准、依赖关系和验收要求。 + +## 2. 业务背景 + +- **现状**:辅导员在奖助学金评审期人工翻查每位候选人的家庭资料与历史资助,效率低且容易遗漏 +- **痛点**: + - **效率低**:每位候选人需 15~20 分钟人工核查 + - **容易遗漏**:跨年度累计资助、生源地差异等难以全面比对 + - **认定争议**:抽查不到位引发学生投诉 +- **目标**:自动完成「多源数据交叉比对 + 横向排名可疑 + 历史重复资助排查」,输出《复核建议清单》 + +## 3. 开发目标 + +- 拉取学生《家庭经济困难认定表》数据 +- 拉取一卡通月均消费额(食堂 / 超市) +- 拉取综测成绩排名 +- 拉取本学年已获资助记录 +- 比对生源地人均 GDP(公开统计) +- 四类异常检测: + - 多源数据一致性(年收入 vs 消费 vs GDP) + - 横向排名可疑(成绩 vs 综测) + - 历史重复资助(累计金额超限) + - 致贫原因逻辑合理性(住址 vs GDP) +- 输出《复核建议清单》HTML 报告 + 高风险个案推送 +- 写入业务表(候选人汇总表 / 复核建议清单 / 困难认定 / 一卡通消费 / 历史资助) + +## 4. 功能范围 + +### 必须实现 + +- 支持 `python scripts/main.py health` / `version` / `config-path` / `init-db` +- 支持 `python scripts/main.py verify-candidate --candidate-id ` +- 支持 `python scripts/main.py batch-verify-class --class-id `(async) +- 支持 `python scripts/main.py mark-needs-review --candidate-id --reason ` +- 支持 `python scripts/main.py submit-review-result --candidate-id --conclusion <保留/取消/调减> --review-note ` +- 支持 `python scripts/main.py render-html --list-id ` +- 支持 `python scripts/main.py sync-card-data` / `sync-gdp-data`(async,cron 占位) +- 完成 §0.1 入口定位表 + +### 可后续扩展 + +- AI 智能撰写核查说明草稿 +- 校级 / 院级差异化学位资助规则 +- 历史投诉数据联动(如某生此前有认定争议) + +## 5. 非功能要求 + +- 目录结构符合 skill-template 规范 +- 入口统一为 `scripts/main.py` +- 用户市场四 Tab +- 输出机读友好;错误前缀 `ERROR:` +- Windows UTF-8 兼容 +- 学生家庭隐私数据严禁写入日志 +- 单候选人校验耗时 ≤ 5 秒;批量校验 500 人 ≤ 30 秒 + +## 6. 输入输出要求 + +### 输入 + +- `candidate_id`:候选人主键(必填) +- `class_id`:班级主键(仅 batch 必填) +- `reason`:复核原因(mark-needs-review 必填) +- `conclusion`:最终结论(submit-review-result 必填,枚举:保留 / 取消 / 调减) +- `review_note`:核查说明(submit-review-result 必填) +- `list_id`:复核清单主键(仅 render 必填) +- 依赖系统:学生工作部 API / 一卡通中心 API / 教务系统 API +- 环境变量(`.env`): + - `SAS_API_BASE_URL` / `SAS_API_TOKEN`:学生工作部 + - `CARD_API_BASE_URL` / `CARD_API_TOKEN`:一卡通中心 + - `AA_API_BASE_URL` / `AA_API_TOKEN`:教务系统 + - `ANNUAL_AID_LIMIT`:年度累计资助上限(元) + - `SCORE_GAP_THRESHOLD`:成绩排名与综测排名差值阈值(默认 10) + - `CONSUMPTION_INCOME_RATIO`:消费 / 年收入比值阈值(默认 0.5) + +### 输出 + +- 成功结果:`verify-candidate` 返回 `{risk_level, anomaly_types, anomaly_details}` +- 错误输出:`ERROR:` 稳定码 + 中文 message +- 数据库记录:写入 `复核建议清单` 表 +- HTML 报告:落到 `html/` + +## 7. 依赖的兄弟技能或外部系统 + +### 兄弟技能依赖 + +- 无强制依赖 + +### 外部系统依赖 + +- **学生工作部 API**(困难认定 + 候选人):必须 +- **一卡通中心 API**(消费数据):必须 +- **教务系统 API**(成绩 + 综测排名):必须 +- **公开统计来源**(生源地 GDP):必须 + +### Python 包依赖(requirements.txt) + +- HTTP 请求:`requests>=2.31.0,<3` +- 数据处理:`pandas>=2.0.0,<3` +- 行政区划映射:`cpca>=0.5.0` +- `jiangchang-platform-kit` 由宿主提供 + +## 8. 不在本次范围内的内容 + +- 本次不实现 AI 智能撰写核查说明草稿 +- 本次不集成历史投诉数据 +- 本次不做 cron 启用(占位 Action 已就位) +- 本次不兼容旧模板历史结构 + +## 9. 验收标准 + +- 代码结构符合模板规范 +- `health` / `version` / `config-path` / `init-db` 命令执行正常 +- `verify-candidate` 在 100 人样例库中完成一次校验,平均耗时 ≤ 5 秒 +- 业务表写入和查询符合 `references/SCHEMA.md` +- 异常识别准确率 ≥ 90%(基于样例库) +- 学生隐私数据 0 写入日志(`test_logging_contract.py` 守护) +- `SKILL.md` 的 `developer_ids` 已配置 +- 安装后按 §0.1 完成宿主多入口验收 + +## 10. 开发注意事项 + +- 只修改当前 skill 仓库 +- 学生家庭隐私数据严禁写入日志;导出报告脱敏 +- 严禁逆向任何系统或绕过数据访问权限 +- 异常检测阈值可在 `runtime/config` 调参 +- `cli` 只做参数解析;核心逻辑在 `service` + +## 11. 变更记录 + +| 日期 | 版本 | 变更人 | 变更内容 | +|------|------|--------|----------| +| 2026-09-14 | v1.0.1 | 匠厂 | 初版需求文档,骨架就位 | diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..d4413ff --- /dev/null +++ b/pytest.ini @@ -0,0 +1,8 @@ +[pytest] +testpaths = tests +python_files = test_*.py *_test.py +python_classes = Test* +python_functions = test_* +norecursedirs = .git __pycache__ .pytest_cache integration desktop samples fixtures artifacts diagnostics +filterwarnings = + ignore::DeprecationWarning diff --git a/release.ps1 b/release.ps1 new file mode 100644 index 0000000..db77b94 --- /dev/null +++ b/release.ps1 @@ -0,0 +1,424 @@ +<# +.SYNOPSIS + Local git release for skill-template repo. + +.DESCRIPTION + Handles git commit / push / semver tag / push tag only. + Encryption, ZIP packaging, and marketplace sync are done by CI + (.github/workflows/release_skill.yaml → reusable-release-skill.yaml@main). + + POLICY-RELEASE-001: before tagging, requires SKILL.md version and + scripts/util/constants.py SKILL_VERSION to equal the next tag (without prefix). + release.ps1 does not auto-edit those fields. + + Requires: git, PowerShell 5+ + + After fetch, runs default tests against the current working tree (before auto-commit): + python tests/run_tests.py -v + Use -SkipTests only for emergency diagnostics. -DryRun still runs tests. +#> + +[CmdletBinding()] +param( + [string]$Prefix = "v", + [string]$Message, + [switch]$AutoCommit, + [switch]$RequireClean, + [string]$CommitMessage, + [switch]$DryRun, + [switch]$SkipTests +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Default tag message is 「正式发布」 from UTF-8 bytes (not a Chinese literal in +# this file). Windows PowerShell 5.1 parses UTF-8 scripts without BOM as the +# system ANSI code page, which would turn a source literal into 「姝e紡鍙戝竷」. +if (-not $PSBoundParameters.ContainsKey("Message") -or [string]::IsNullOrWhiteSpace($Message)) { + $Message = [System.Text.Encoding]::UTF8.GetString([byte[]](0xE6, 0xAD, 0xA3, 0xE5, 0xBC, 0x8F, 0xE5, 0x8F, 0x91, 0xE5, 0xB8, 0x83)) +} + +function Remove-GitNoise { + param([string[]]$Lines) + return @($Lines | Where-Object { + $_ -and ($_ -notmatch '^warning:\s+unable to access ') + }) +} + +function Convert-GitOutputLines { + param($Output) + return @( + @($Output) | ForEach-Object { + if ($_ -is [System.Management.Automation.ErrorRecord]) { $_.ToString() } + else { "$_" } + } + ) +} + +# Call git.exe directly. Never wrap with `cmd /c`: on Chinese Windows, cmd +# re-encodes the command line through the OEM code page. +# Also never pass Chinese via `git -m`: Windows PowerShell 5.1 encodes native +# argv using the console code page, so 「正式发布」 becomes 「姝e紡鍙戝竷」 +# in the annotated tag. Commit/tag messages go through a UTF-8 file + `-F`. +function Invoke-GitRaw { + param([Parameter(Mandatory = $true)][string[]]$GitArgs) + $prev = $ErrorActionPreference + $ErrorActionPreference = "Continue" + try { + $output = & git @GitArgs 2>&1 + $code = $LASTEXITCODE + } finally { + $ErrorActionPreference = $prev + } + return [pscustomobject]@{ + ExitCode = $code + Lines = @(Remove-GitNoise -Lines @(Convert-GitOutputLines -Output $output)) + } +} + +function Invoke-Git { + param([Parameter(Mandatory = $true)][string[]]$GitArgs) + Write-Host (">> git " + ($GitArgs -join " ")) -ForegroundColor DarkGray + $r = Invoke-GitRaw -GitArgs $GitArgs + if ($r.ExitCode -ne 0) { + throw "git command failed: git $($GitArgs -join ' ')" + } +} + +function Invoke-GitWithMessageFile { + param( + [Parameter(Mandatory = $true)][string[]]$GitArgsPrefix, + [Parameter(Mandatory = $true)][string]$Message + ) + $msgFile = Join-Path ([System.IO.Path]::GetTempPath()) ("jiangchang-git-msg-" + [guid]::NewGuid().ToString("N") + ".txt") + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + [System.IO.File]::WriteAllText($msgFile, ($Message.TrimEnd() + "`n"), $utf8NoBom) + try { + Invoke-Git -GitArgs (@("-c", "i18n.commitEncoding=utf-8") + $GitArgsPrefix + @("-F", $msgFile)) + } finally { + Remove-Item -LiteralPath $msgFile -Force -ErrorAction SilentlyContinue + } +} + +function Get-GitOutput { + param([Parameter(Mandatory = $true)][string[]]$GitArgs) + $r = Invoke-GitRaw -GitArgs $GitArgs + if ($r.ExitCode -ne 0) { + throw "git command failed: git $($GitArgs -join ' ')" + } + return @($r.Lines) +} + +function Test-Repo { + $r = Invoke-GitRaw -GitArgs @("rev-parse", "--is-inside-work-tree") + if ($r.ExitCode -ne 0) { + return $false + } + return (($r.Lines | Select-Object -First 1) -eq "true") +} + +function Get-CurrentBranch { + $b = (Get-GitOutput -GitArgs @("branch", "--show-current") | Select-Object -First 1).Trim() + return $b +} + +function Get-StatusPorcelain { + $lines = @(Get-GitOutput -GitArgs @("status", "--porcelain")) + return $lines +} + +function Parse-SemVerTag { + param( + [string]$Tag, + [string]$TagPrefix + ) + $escaped = [regex]::Escape($TagPrefix) + $m = [regex]::Match($Tag, "^${escaped}(\d+)\.(\d+)\.(\d+)$") + if (-not $m.Success) { return $null } + + return [pscustomobject]@{ + Raw = $Tag + Major = [int]$m.Groups[1].Value + Minor = [int]$m.Groups[2].Value + Patch = [int]$m.Groups[3].Value + } +} + +function Get-NextTag { + param([string]$TagPrefix) + + $tags = Get-GitOutput -GitArgs @("tag", "--list") + $parsed = @() + + foreach ($t in $tags) { + $t = $t.Trim() + if (-not $t) { continue } + $obj = Parse-SemVerTag -Tag $t -TagPrefix $TagPrefix + if ($null -ne $obj) { $parsed += $obj } + } + + if ($parsed.Count -eq 0) { + return "${TagPrefix}1.0.1" + } + + $latest = $parsed | Sort-Object Major, Minor, Patch | Select-Object -Last 1 + return "$TagPrefix$($latest.Major).$($latest.Minor).$([int]$latest.Patch + 1)" +} + +function Read-SkillMdVersion { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $path = Join-Path $SkillRoot "SKILL.md" + if (-not (Test-Path -LiteralPath $path)) { + return $null + } + $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + $m = [regex]::Match($text, '(?m)^version:\s*["'']?([0-9]+\.[0-9]+\.[0-9]+)["'']?\s*$') + if (-not $m.Success) { + return $null + } + return $m.Groups[1].Value.Trim() +} + +function Read-SkillConstantsVersion { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $path = Join-Path $SkillRoot "scripts\util\constants.py" + if (-not (Test-Path -LiteralPath $path)) { + return $null + } + $text = Get-Content -LiteralPath $path -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return $null + } + $m = [regex]::Match($text, '(?m)^SKILL_VERSION\s*=\s*["'']([0-9]+\.[0-9]+\.[0-9]+)["'']') + if (-not $m.Success) { + return $null + } + return $m.Groups[1].Value.Trim() +} + +# POLICY-RELEASE-001: marketplace latest (git tag) must equal package manifest versions. +function Assert-SkillReleaseVersionAlignment { + param( + [Parameter(Mandatory = $true)][string]$SkillRoot, + [Parameter(Mandatory = $true)][string]$NextTag, + [Parameter(Mandatory = $true)][string]$TagPrefix + ) + + $skillMdPath = Join-Path $SkillRoot "SKILL.md" + if (-not (Test-Path -LiteralPath $skillMdPath)) { + return + } + + $expected = $NextTag + if ($TagPrefix -and $expected.StartsWith($TagPrefix)) { + $expected = $expected.Substring($TagPrefix.Length) + } + $expected = $expected.Trim() + if ($expected -notmatch '^\d+\.\d+\.\d+$') { + throw "Release check failed (POLICY-RELEASE-001): cannot derive semver from Next tag '$NextTag'." + } + + $mdVersion = Read-SkillMdVersion -SkillRoot $SkillRoot + $constVersion = Read-SkillConstantsVersion -SkillRoot $SkillRoot + + $problems = @() + if ([string]::IsNullOrWhiteSpace($mdVersion)) { + $problems += "SKILL.md missing top-level version: X.Y.Z" + } + elseif ($mdVersion -ne $expected) { + $problems += "SKILL.md version=$mdVersion (want $expected)" + } + if ([string]::IsNullOrWhiteSpace($constVersion)) { + $problems += "scripts/util/constants.py missing SKILL_VERSION = `"X.Y.Z`"" + } + elseif ($constVersion -ne $expected) { + $problems += "SKILL_VERSION=$constVersion (want $expected)" + } + + if ($problems.Count -gt 0) { + throw ( + "Release aborted (POLICY-RELEASE-001): Next tag=$NextTag requires manifest version $expected, but " + + ($problems -join "; ") + + ". Bump SKILL.md version and scripts/util/constants.py SKILL_VERSION to $expected (and CHANGELOG), then re-run release.ps1. " + + "release.ps1 does not auto-edit these fields." + ) + } + + Write-Host "Version alignment OK: tag=$NextTag == SKILL.md=$mdVersion == SKILL_VERSION=$constVersion" -ForegroundColor DarkGray +} + +function Assert-SkillReleasePackagingSources { + param([Parameter(Mandatory = $true)][string]$SkillRoot) + + $scriptsDir = Join-Path $SkillRoot "scripts" + $mainPy = Join-Path $scriptsDir "main.py" + if (-not (Test-Path -LiteralPath $mainPy)) { + return + } + + $text = Get-Content -LiteralPath $mainPy -Raw -Encoding UTF8 -ErrorAction SilentlyContinue + if ([string]::IsNullOrWhiteSpace($text)) { + return + } + + $need = @() + if ($text -match '(?m)^\s*from\s+cli\.') { $need += 'cli' } + if ($text -match '(?m)^\s*import\s+cli\b') { $need += 'cli' } + if ($text -match '(?m)^\s*from\s+service\.') { $need += 'service' } + if ($text -match '(?m)^\s*import\s+service\b') { $need += 'service' } + if ($text -match '(?m)^\s*from\s+db\.') { $need += 'db' } + if ($text -match '(?m)^\s*import\s+db\b') { $need += 'db' } + if ($text -match '(?m)^\s*from\s+util\.') { $need += 'util' } + if ($text -match '(?m)^\s*import\s+util\b') { $need += 'util' } + + foreach ($p in ($need | Select-Object -Unique)) { + $folder = Join-Path $scriptsDir $p + if (-not (Test-Path -LiteralPath $folder)) { + throw "Release check failed: scripts/main.py imports from '$p' but folder is missing: $folder" + } + } + + $pyFiles = @(Get-ChildItem -LiteralPath $scriptsDir -Filter *.py -Recurse -File -ErrorAction SilentlyContinue) + Write-Host "Packaging check: $($pyFiles.Count) Python file(s) under scripts/ (CI will obfuscate all recursively)." -ForegroundColor DarkGray +} + +function Invoke-DefaultTests { + param( + [Parameter(Mandatory = $true)][string]$SkillRoot, + [switch]$Skip + ) + + if ($Skip) { + Write-Host "WARNING: Skipping tests before release. This should only be used for emergency diagnostics." -ForegroundColor Yellow + return + } + + $runTests = Join-Path $SkillRoot "tests\run_tests.py" + if (-not (Test-Path -LiteralPath $runTests)) { + throw "Release check failed: tests/run_tests.py not found." + } + + Write-Host ">> python tests/run_tests.py -v" -ForegroundColor DarkGray + & python $runTests -v + if ($LASTEXITCODE -ne 0) { + throw "Release aborted: default tests failed (python tests/run_tests.py -v)." + } +} + +function Ensure-CleanOrAutoCommit { + param( + [switch]$DoAutoCommit, + [switch]$NeedClean, + [switch]$IsDryRun, + [string]$Msg + ) + + $status = @(Get-StatusPorcelain) + if ($status.Length -eq 0) { return } + + if ($NeedClean) { + Write-Host "Working tree is not clean and -RequireClean is enabled." -ForegroundColor Yellow + (Invoke-GitRaw -GitArgs @("status", "--short")).Lines | ForEach-Object { Write-Host $_ } + throw "Abort: dirty working tree." + } + + $commitMsg = $Msg + if ([string]::IsNullOrWhiteSpace($commitMsg)) { + $commitMsg = "chore: auto release commit ($(Get-Date -Format 'yyyy-MM-dd HH:mm:ss'))" + } + + if (-not $DoAutoCommit) { + Write-Host "Detected uncommitted changes, auto-committing before release..." -ForegroundColor Yellow + } + + if ($IsDryRun) { + Write-Host "[DryRun] Would run: git add -A" -ForegroundColor Yellow + Write-Host "[DryRun] Would run: git commit -m `"$commitMsg`"" -ForegroundColor Yellow + return + } + + Invoke-Git -GitArgs @("add", "-A") + Invoke-GitWithMessageFile -GitArgsPrefix @("commit") -Message $commitMsg +} + +try { + Write-Host "=== Release Script Start ===" -ForegroundColor Cyan + + if (-not (Test-Repo)) { + throw "Current directory is not a git repository." + } + + $branch = Get-CurrentBranch + if ([string]::IsNullOrWhiteSpace($branch)) { + throw "Unable to determine current branch." + } + + if ($branch -notin @("main", "master")) { + throw "Current branch is '$branch'. Release is only allowed from main/master." + } + + if ($DryRun) { + Write-Host "[DryRun] Would run: git fetch --tags --prune origin" -ForegroundColor Yellow + } else { + Invoke-Git -GitArgs @("fetch", "--tags", "--prune", "origin") + } + + $skillRoot = (Get-GitOutput -GitArgs @("rev-parse", "--show-toplevel") | Select-Object -First 1).Trim() + if (Test-Path -LiteralPath (Join-Path $skillRoot "SKILL.md")) { + Assert-SkillReleasePackagingSources -SkillRoot $skillRoot + } + + $nextTag = Get-NextTag -TagPrefix $Prefix + Write-Host "Next tag: $nextTag" -ForegroundColor Green + Assert-SkillReleaseVersionAlignment -SkillRoot $skillRoot -NextTag $nextTag -TagPrefix $Prefix + + $existing = @(Get-GitOutput -GitArgs @("tag", "--list", $nextTag)) + if ($existing.Length -gt 0) { + throw "Tag already exists: $nextTag" + } + + Invoke-DefaultTests -SkillRoot $skillRoot -Skip:$SkipTests + + Ensure-CleanOrAutoCommit -DoAutoCommit:$AutoCommit -NeedClean:$RequireClean -IsDryRun:$DryRun -Msg $CommitMessage + + $upstream = Invoke-GitRaw -GitArgs @("rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") + $hasUpstream = ($upstream.ExitCode -eq 0) + + if ($DryRun) { + if ($hasUpstream) { + Write-Host "[DryRun] Would run: git push" -ForegroundColor Yellow + } else { + Write-Host "[DryRun] Would run: git push -u origin $branch" -ForegroundColor Yellow + } + } else { + if ($hasUpstream) { + Invoke-Git -GitArgs @("push") + } else { + Invoke-Git -GitArgs @("push", "-u", "origin", $branch) + } + } + + if ($DryRun) { + Write-Host "[DryRun] Would run: git tag -a $nextTag -F ($Message)" -ForegroundColor Yellow + Write-Host "[DryRun] Would run: git push origin $nextTag" -ForegroundColor Yellow + Write-Host "=== DryRun Complete ===" -ForegroundColor Cyan + exit 0 + } + + Invoke-GitWithMessageFile -GitArgsPrefix @("tag", "-a", $nextTag) -Message $Message + Invoke-Git -GitArgs @("push", "origin", $nextTag) + + Write-Host "Release success: $nextTag" -ForegroundColor Green + Write-Host "=== Release Script Done ===" -ForegroundColor Cyan + exit 0 +} +catch { + Write-Host "Release failed: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..96b8487 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +# 奖助学金评定逻辑防伪校验 - 技能特有依赖 +# jiangchang-platform-kit / playwright 等公共能力由宿主共享 runtime 提供,不要写入此文件 + +# HTTP 请求 +requests>=2.31.0,<3 + +# 数据处理 +pandas>=2.0.0,<3 + +# 行政区划映射(生源地 GDP 对账) +cpca>=0.5.0 diff --git a/scripts/cli/__init__.py b/scripts/cli/__init__.py new file mode 100644 index 0000000..9dc01a4 --- /dev/null +++ b/scripts/cli/__init__.py @@ -0,0 +1 @@ +"""scripts.cli - CLI 参数解析层(占位)""" diff --git a/scripts/db/__init__.py b/scripts/db/__init__.py new file mode 100644 index 0000000..4bc7684 --- /dev/null +++ b/scripts/db/__init__.py @@ -0,0 +1,8 @@ +"""scripts.db - 数据库访问层(占位) + +按 references/SCHEMA.md 实现表与字段: +- 制度送审稿 +- 冲突条款清单 +- 制度库索引 +- 审查任务日志 +""" diff --git a/scripts/main.py b/scripts/main.py new file mode 100644 index 0000000..59aa966 --- /dev/null +++ b/scripts/main.py @@ -0,0 +1,105 @@ +#!/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"verify-scholarship-fraud-sas health check (placeholder)") + print(f"base_dir: {BASE_DIR}") + return 0 + + +def version(): + """查看版本(占位)。""" + print("verify-scholarship-fraud-sas 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 verify_candidate(candidate_id: str): + """对一位候选人做防伪校验。""" + print(f"verify-candidate: candidate_id={candidate_id} (待开发者实现)") + return 0 + + +def batch_verify_class(class_id: str): + """批量校验某班级候选人。""" + print(f"batch-verify-class: class_id={class_id} (待开发者实现)") + return 0 + + +def mark_needs_review(candidate_id: str, reason: str): + """标记候选人为待复核。""" + print(f"mark-needs-review: candidate_id={candidate_id} reason={reason} (待开发者实现)") + return 0 + + +def submit_review_result(candidate_id: str, conclusion: str, review_note: str): + """提交复核结果。""" + print(f"submit-review-result: candidate_id={candidate_id} conclusion={conclusion} (待开发者实现)") + return 0 + + +def render_html(list_id: str): + """查看复核清单。""" + print(f"render-html: list_id={list_id} (待开发者实现)") + return 0 + + +def sync_card_data(): + """同步一卡通数据。""" + print("sync-card-data (待开发者实现)") + return 0 + + +def sync_gdp_data(): + """同步生源地 GDP 数据。""" + print("sync-gdp-data (待开发者实现)") + return 0 + + +COMMANDS = { + "health": health, + "version": version, + "config-path": config_path, + "init-db": init_db, + "verify-candidate": verify_candidate, + "batch-verify-class": batch_verify_class, + "mark-needs-review": mark_needs_review, + "submit-review-result": submit_review_result, + "render-html": render_html, + "sync-card-data": sync_card_data, + "sync-gdp-data": sync_gdp_data, +} + + +def main(argv=None): + argv = argv or sys.argv[1:] + if not argv or argv[0] not in COMMANDS: + print("Usage: python scripts/main.py ") + print("Commands:", ", ".join(COMMANDS.keys())) + return 1 + return COMMANDS[argv[0]](*argv[1:]) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/service/__init__.py b/scripts/service/__init__.py new file mode 100644 index 0000000..deea28f --- /dev/null +++ b/scripts/service/__init__.py @@ -0,0 +1,9 @@ +"""scripts.service - 业务核心逻辑层(占位) + +按 development/REQUIREMENTS.md §3 开发目标实现: +- 条款解析与向量化 +- 制度库语义比对 +- 程序合规体检 +- 重复发文检测 +- 报告生成与批注回写 +""" diff --git a/scripts/util/__init__.py b/scripts/util/__init__.py new file mode 100644 index 0000000..29fb478 --- /dev/null +++ b/scripts/util/__init__.py @@ -0,0 +1 @@ +"""scripts.util - 工具函数层(占位)"""