v1.0.1: 合同到期智能预警与续聘辅助 - 初版骨架
基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。 核心业务: - 每日凌晨定时扫描全校教职工合同电子台账 - 距到期日 90 / 60 / 30 天分级预警(黄 / 橙 / 红) - 自动生成个人版《续聘准备任务书》与单位版《本月到期人员清单》 - 30 天红色预警自动抄送分管校领导 输出形态: - 个人版 HTML 任务书 + 单位版 PDF 清单 + 三级预警通知 预期提效:因合同逾期未续签导致的劳动纠纷降为 0;人事处年终集中突击签合同的工作量减少 60%。 业务行生命周期:可续聘 → 预警中(黄 / 橙 / 红)→ 已续签 / 已驳回 / 已过期 Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
32
.env.example
Normal file
32
.env.example
Normal file
@@ -0,0 +1,32 @@
|
||||
# 合同到期智能预警与续聘辅助 - 配置示例
|
||||
# 行头说明「是什么」;行尾说明「怎么填」;进程环境变量 > 用户 .env > 本文件
|
||||
|
||||
# 人事系统 API 入口,例如 https://hr.example.edu/api/v1
|
||||
HR_API_BASE_URL=
|
||||
|
||||
# 人事系统 API 调用凭证(Bearer Token),由人事处统一发放
|
||||
HR_API_TOKEN=
|
||||
|
||||
# 党委教师工作部 API 入口,例如 https://teacher-affairs.example.edu/api/v1
|
||||
TEACHER_AFFAIRS_API_BASE_URL=
|
||||
|
||||
# 党委教师工作部 API 调用凭证
|
||||
TEACHER_AFFAIRS_API_TOKEN=
|
||||
|
||||
# 企业微信 API 入口,用于推送个人版任务书和单位版清单
|
||||
WECOM_API_BASE_URL=https://qyapi.weixin.qq.com/cgi-bin
|
||||
|
||||
# 企业微信 API 调用凭证
|
||||
WECOM_API_TOKEN=
|
||||
|
||||
# 扫描阈值(天),距到期日小于等于此值触发对应等级预警;逗号分隔黄/橙/红
|
||||
ALERT_THRESHOLDS=90,60,30
|
||||
|
||||
# 每日扫描时间(HH:MM,24h 制),cron 启用时使用
|
||||
DAILY_SCAN_AT=02:00
|
||||
|
||||
# 全校扫描超时(秒),默认 120
|
||||
SCAN_TIMEOUT_SEC=120
|
||||
|
||||
# 错误码前缀,统一为 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 派生
|
||||
- 实现核心业务:合同到期智能预警与续聘辅助(每日定时扫描 + 三级预警 + 个人版任务书 + 单位版清单 + 自动抄送)
|
||||
- 输出形态:合同台账表状态更新 + 预警记录表 + 个人版 HTML 任务书 + 单位版 PDF 清单 + 派发日志
|
||||
- 预期提效:因合同逾期未续签导致的劳动纠纷降为 0;人事处年终集中突击签合同的工作量减少 60%
|
||||
- 业务行生命周期:可预警 → 预警中 → 已续签 / 已驳回 / 已过期
|
||||
- 占位说明:本版仅完成骨架与文档,业务实现细节(`scripts/` 下 Python 模块、人事系统 API 对接、定时调度)由开发者后续按 `development/REQUIREMENTS.md` 补全
|
||||
31
DEMO.md
Normal file
31
DEMO.md
Normal file
@@ -0,0 +1,31 @@
|
||||
---
|
||||
demo_video_url: ""
|
||||
---
|
||||
|
||||
# 演示说明
|
||||
|
||||
> 本技能当前未提供演示视频。下方用图文形式描述完整扫描流程与结果样例。
|
||||
|
||||
## 演示看点
|
||||
|
||||
1. **定时任务 → 扫描即将到期合同**:每日凌晨 02:00 自动执行(也可手动触发)
|
||||
2. **数据管理 → 合同台账 → 预警记录**:30 秒~2 分钟内完成全校合同扫描
|
||||
3. **结果样例**:
|
||||
- 预警记录表新增 5~20 条预警(含黄 / 橙 / 红等级)
|
||||
- 教师本人收到企业微信个人版《续聘准备任务书》通知
|
||||
- 二级单位人事秘书收到单位版《本月到期清单》通知
|
||||
- 30 天红色预警自动抄送分管校领导
|
||||
- 合同台账表中相应行进入「预警中(红色)」状态
|
||||
|
||||
## 没有视频时
|
||||
|
||||
可参考下方截图位(待补):
|
||||
|
||||
- 截图位 1:合同台账表(预警中状态)
|
||||
- 截图位 2:预警记录表(红 / 橙 / 黄三色)
|
||||
- 截图位 3:个人版《续聘准备任务书》HTML 报告
|
||||
- 截图位 4:单位版《本月到期清单》通知
|
||||
|
||||
## 何时补视频
|
||||
|
||||
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.
|
||||
81
README.md
Normal file
81
README.md
Normal file
@@ -0,0 +1,81 @@
|
||||
---
|
||||
description: "每日凌晨扫描全校教职工合同电子台账,对距离到期日 90/60/30 天的合同分级预警,自动生成个人版续聘任务书和单位版本月到期清单。"
|
||||
---
|
||||
|
||||
# 合同到期智能预警与续聘辅助
|
||||
|
||||
每日凌晨自动扫描全校教职工合同电子台账,对距离到期日 90 天 / 60 天 / 30 天的合同分别给黄 / 橙 / 红三级预警,自动生成《续聘准备任务书》推给教师本人,把《本月合同到期人员清单及续聘建议》推给二级单位人事秘书,把劳动纠纷和年终突击签合同彻底挡在门外。
|
||||
|
||||
---
|
||||
|
||||
## 这个技能能帮你做什么
|
||||
|
||||
- **每日凌晨自动扫描**:无需人工介入,按合同到期日距今 90/60/30 天分别给出黄/橙/红三级预警
|
||||
- **自动预填续聘考核表**:根据该教师近 3 年年度考核结果和师德师风考评,自动生成「近三年工作业绩摘要」
|
||||
- **AI 建议续聘意见**:依据考核结果(优秀/称职/基本称职/不称职),给出续聘 / 续聘观察 / 不续聘的建议,并附理由(如「连续两年考核排名后 10%」)
|
||||
- **双轨派发**:个人版《续聘准备任务书》发给教师本人;单位版《本月合同到期人员清单》发给二级单位人事秘书
|
||||
- **红头预警自动抄送**:30 天红色紧急提醒自动抄送分管校领导
|
||||
|
||||
---
|
||||
|
||||
## 适合什么场景
|
||||
|
||||
- 人事处合同管理员想从「年底集中突击签合同」转向「全年前置预警」
|
||||
- 二级单位人事秘书每月初就想知道本月谁需要办续聘、还差什么材料
|
||||
- 教师本人提前 90 天就知道合同什么时候到期、需要准备什么材料
|
||||
- 分管校领导 30 天内必须知道谁有合同逾期风险
|
||||
|
||||
---
|
||||
|
||||
## 使用前需要准备什么
|
||||
|
||||
- 合同电子台账数据已与人事系统同步(合同类型、起止日期、聘任岗位、试用期)
|
||||
- 该教师近 3 年年度考核结果可由党委教师工作部系统接口获取
|
||||
- 师德师风考评记录可由党委教师工作部系统接口获取
|
||||
- 二级单位人事秘书通讯录已登记到本技能 `.env`
|
||||
|
||||
---
|
||||
|
||||
## 怎么使用
|
||||
|
||||
本技能支持**数据管理**(合同台账表上点工具栏按钮)、**对话任务**(自然语言让 Agent 完成)、**定时任务**(每日凌晨 02:00 自动扫描)。三种入口触发同一套业务能力。
|
||||
|
||||
**逐步操作与排障,请查看技能市场「使用教程」Tab。**
|
||||
|
||||
---
|
||||
|
||||
## 执行后你会得到什么
|
||||
|
||||
- **个人版《续聘准备任务书》**:发给教师本人,含合同基本信息、近三年业绩摘要、续聘建议、所需材料清单、截止时间
|
||||
- **单位版《本月合同到期人员清单及续聘建议》**:发给二级单位人事秘书,含本月全部到期 / 即将到期人员名单、AI 续聘建议、任务派发状态
|
||||
- **预警提醒**:90 天黄色 / 60 天橙色 / 30 天红色,三档分级
|
||||
- **任务派发日志**:每一条预警的派发状态、接收人、是否已读
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 本技能**不替代人事处终审**,续聘 / 不续聘的最终决定权仍由人事处与分管校领导行使
|
||||
- 涉及劳动合同解除 / 不续聘的处分性决定仍须走线下流程,技能仅给出 AI 建议
|
||||
- 考核结果、师德师风数据延迟同步可能导致预警等级失真,建议每日扫描完成后人工核对一次
|
||||
- 30 天红色预警触发后,系统将自动抄送分管校领导,请勿手动屏蔽通知
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 这个技能适合我吗?
|
||||
|
||||
> 如果你的工作包含「管理全校教职工劳动合同 + 每月办理续签」,本技能可以接管日常预警与材料预填。
|
||||
|
||||
### 教师本人能看到 AI 建议的续聘意见吗?
|
||||
|
||||
> 个人版《续聘准备任务书》默认**不展示 AI 续聘建议**,仅展示业绩摘要与所需材料;单位版《清单》才展示 AI 建议,避免给教师造成预判压力。
|
||||
|
||||
### 数据来源(考核、师德)延迟怎么办?
|
||||
|
||||
> 技能每日凌晨扫描时若发现考核数据缺失,会将该合同标记为「数据待补」并跳过预警;考核数据补齐后会自动重新触发。
|
||||
|
||||
### 试用期合同也在扫描范围内吗?
|
||||
|
||||
> 是。试用期合同同样按到期日距今 90/60/30 天分级预警;试用期考核不通过的处理建议由人事处另行判定。
|
||||
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/`,不要写进本文件。
|
||||
131
SKILL.md
Normal file
131
SKILL.md
Normal file
@@ -0,0 +1,131 @@
|
||||
---
|
||||
name: 合同到期智能预警与续聘辅助
|
||||
description: "每日凌晨扫描全校教职工合同电子台账,对距离到期日 90/60/30 天的合同分级预警,并自动生成《续聘准备任务书》和《本月合同到期人员清单》。"
|
||||
version: 1.0.1
|
||||
author: 深圳匠厂科技有限公司
|
||||
metadata:
|
||||
openclaw:
|
||||
slug: alert-contract-expiring-hr
|
||||
platform_kit_min_version: "1.2.11"
|
||||
emoji: "📋"
|
||||
category: "人事"
|
||||
developer_ids:
|
||||
- 12345 # 占位,请换成你的匠厂用户 ID
|
||||
- 12468
|
||||
allowed-tools:
|
||||
- bash
|
||||
---
|
||||
|
||||
# 合同到期智能预警与续聘辅助(alert-contract-expiring-hr)
|
||||
|
||||
这是基于匠厂 skill-template 主仓 v1.0.108 派生的业务技能。覆盖需求规格书第 4.2 节「🟡 人事处 Skill 002 —— 合同到期"智能预警与续聘辅助"」。
|
||||
|
||||
## Agent / 宿主编排(复制为业务技能后必改)
|
||||
|
||||
### AI + RPA 定位
|
||||
|
||||
- **Agent 是入口与编排者**:理解用户意图("扫描本月即将到期的合同"、"给张三生成续聘任务书")、补齐参数、判断风险、选择 Action,并用业务语言解释结果。
|
||||
- **本技能不涉及浏览器 / 桌面 / 手机 RPA**:合同台账读取走人事系统官方 API,考核数据走党委教师工作部 API。
|
||||
- 开发者与 AI 编程工具先读 `development/AI_RPA.md`。
|
||||
|
||||
### 架构铁律(先读)
|
||||
|
||||
- **宿主产品入口平级**:对话任务 / 数据管理 / 定时任务是同一技能能力的不同触发场景,**同等重要**;本技能**重点挂定时任务入口**(每日凌晨扫描)。
|
||||
- **单业务内核、多入口**:数据管理 / 定时任务 / Agent / 技能详情 / 直接 CLI 必须复用同一 domain service;禁止按入口复制业务逻辑。
|
||||
- **业务行生命周期**:本技能对合同台账表的每一份合同有"可预警 / 预警中 / 已续聘 / 已驳回"角色约束,遵守 `references/WORK_ITEM_STATUS.md`。
|
||||
- **HTML 整页报告**:本技能输出个人版《续聘准备任务书》走 `render-html`;单位版清单走 PDF 导出。
|
||||
|
||||
### 如何触发(能力类型)
|
||||
|
||||
| 你要做什么 | Agent 怎么做 | 进任务中心? |
|
||||
|------------|--------------|--------------|
|
||||
| 健康检查 / 短查询 | OpenClaw bash/exec + 共享 Python 跑 CLI | 否 |
|
||||
| 已声明 sync 的 Action | `run_skill_action` 或等效短 CLI | 否 |
|
||||
| 长任务(每日凌晨扫描全校 1000+ 份合同) | **必须** `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` → `scan-expiring-contracts`(cron / agent) | 是(async) |
|
||||
| 给某位教师生成续聘任务书 | `run_skill_action` → `generate-renewal-task`(输入:`contract_id`) | 否(sync) |
|
||||
| 下发单位版续聘清单 | `run_skill_action` → `dispatch-task-notice`(输入:`unit_id` 或 `month`) | 否(sync) |
|
||||
| 标记合同为已续签 | `run_skill_action` → `mark-contract-renewed`(输入:`contract_id` + 续签材料 ID) | 否(sync) |
|
||||
| 在浏览器中打开续聘任务书 | `run_skill_action` → `render-html` | 否(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:`alert-contract-expiring-hr`(`alert` 动词 + `contract-expiring` 名词 + `hr` 人事处缩写)
|
||||
- name:**用户可见的中文业务名称**;`description` 是说明,不是名称。
|
||||
- 目录名、默认 `{slug}.db` 使用 slug,不使用中文。
|
||||
|
||||
## 平台元数据
|
||||
|
||||
- `metadata.openclaw.developer_ids`:技能发布后的默认开发者可见用户 ID 列表。
|
||||
- 开发期技能常为不公开:不加本人 ID,开发者自己也无法在市场安装自测。
|
||||
- 当 `access_scope = 0`(不公开)时,平台会把 `developer_ids` 中的用户自动补写到 `skill_user_access`。
|
||||
|
||||
---
|
||||
|
||||
> **本技能开发状态:** v1.0.1 骨架已就位,业务实现细节见 `development/REQUIREMENTS.md`;具体 Python 模块由开发者后续补全。
|
||||
145
TUTORIAL.md
Normal file
145
TUTORIAL.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 使用教程
|
||||
|
||||
本技能有三种**平行**的使用方式:
|
||||
|
||||
- **数据管理**:在左侧「合同台账」表上点工具栏 / 行内按钮
|
||||
- **对话任务**:用自然语言让 Agent 完成同一能力
|
||||
- **定时任务**:每日凌晨 02:00 自动扫描全校合同到期情况
|
||||
|
||||
三种方式触发同一套业务能力,结果一致,选你习惯的即可。
|
||||
|
||||
---
|
||||
|
||||
## 本技能做什么
|
||||
|
||||
```text
|
||||
【合同电子台账 + 考核数据 + 师德数据】 → 【距到期日 90/60/30 天分级】 → 【个人任务书 + 单位清单】 → 【红头预警自动抄送】
|
||||
```
|
||||
|
||||
- **输入**:全校教职工合同电子台账 + 近 3 年考核结果 + 师德师风考评记录
|
||||
- **输出**:个人版《续聘准备任务书》+ 单位版《本月合同到期人员清单》+ 三级预警通知
|
||||
- **说明**:本技能不替代人事处终审;AI 建议仅供二级单位参考,最终决定权在人事处与分管校领导
|
||||
|
||||
---
|
||||
|
||||
## 开始前
|
||||
|
||||
确认:
|
||||
|
||||
1. 已安装并登录匠厂客户端
|
||||
2. 已在技能市场安装本技能(建议更新到最新版)
|
||||
3. 人事系统 API 与党委教师工作部 API 凭证已配置(`HR_API_TOKEN` / `TEACHER_AFFAIRS_API_TOKEN` 已登记到本技能 `.env`)
|
||||
4. 二级单位人事秘书通讯录已登记
|
||||
5. 【如需】首次使用前,先在「数据管理 → 合同台账」点击一次「立即同步」载入最新合同数据
|
||||
|
||||
---
|
||||
|
||||
## 入口一:数据管理
|
||||
|
||||
适合:人事处合同管理员、二级单位人事秘书、喜欢点界面操作的用户。
|
||||
|
||||
### 左侧各表是干什么的
|
||||
|
||||
| 表名(中文) | 作用 |
|
||||
|--------------|------|
|
||||
| 合同台账 | 全校教职工合同电子台账主表,状态行可见 |
|
||||
| 预警记录 | 每次扫描产生的预警条目(含黄/橙/红等级) |
|
||||
| 续聘任务 | 已生成的续聘任务书记录(个人 / 单位版) |
|
||||
| 派发日志 | 每一条预警的派发状态、接收人、是否已读 |
|
||||
|
||||
### 首次准备(若需要)
|
||||
|
||||
1. 进入 **数据管理** → **合同台账**
|
||||
2. 点击表顶 **「立即同步」**(空表可点)
|
||||
3. 成功后应看到:全校教职工合同基本信息、合同类型、起止日期、聘任岗位
|
||||
|
||||
### 主流程:触发一次合同到期扫描
|
||||
|
||||
1. 进入 **合同台账**
|
||||
2. 点击表顶 **「立即扫描到期合同」**
|
||||
3. 等待 30 秒~2 分钟(按全校合同规模)
|
||||
4. 完成后:
|
||||
- 预警记录表新增若干行(含黄/橙/红等级)
|
||||
- 距到期日 30 天内的合同行进入「预警中(红色)」状态
|
||||
- 教师本人收到个人版《续聘准备任务书》通知
|
||||
- 二级单位人事秘书收到单位版《本月到期清单》通知
|
||||
- 30 天红色预警自动抄送分管校领导
|
||||
|
||||
### 行级 / 批量
|
||||
|
||||
| 你想做什么 | 操作 |
|
||||
|------------|------|
|
||||
| 给某位教师生成续聘任务书 | **合同台账** 行内 → **「生成续聘任务书」** |
|
||||
| 下发单位版清单 | 选中某单位的多行 → **「按单位生成清单」** |
|
||||
| 标记已续签 | **合同台账** 行内 → **「标记为已续签」**(须上传续签材料) |
|
||||
| 重新触发预警 | **合同台账** 行内 → **「重新触发预警」** |
|
||||
|
||||
---
|
||||
|
||||
## 入口二:对话任务
|
||||
|
||||
适合:临时提问、不想切换表的用户。
|
||||
|
||||
### 什么时候用对话
|
||||
|
||||
- 临时想知道某位教师的合同什么时候到期
|
||||
- 想用自然语言解释「为什么 AI 建议不续聘」
|
||||
- 想看「本月一共要办多少份续聘」
|
||||
|
||||
### 示例话术
|
||||
|
||||
| 你想做什么 | 可以这样说 |
|
||||
|------------|------------|
|
||||
| 查询某教师合同 | 「张老师的合同什么时候到期?」 |
|
||||
| 触发扫描 | 「帮我扫描一下全校合同到期情况」 |
|
||||
| 生成任务书 | 「给李老师生成一份续聘准备任务书」 |
|
||||
| 查看本月统计 | 「本月一共要办多少份续聘?」 |
|
||||
| 解释建议 | 「为什么 AI 建议王老师续聘观察?」 |
|
||||
|
||||
### Agent 可能追问什么
|
||||
|
||||
- 缺参数时:「请告诉我教师姓名 / 工号 / 合同 ID」
|
||||
- 数据缺失时:「该教师近 3 年考核数据缺失,请先在党委教师工作部系统补录」
|
||||
- 失败时:可改用数据管理点按钮重新触发
|
||||
|
||||
---
|
||||
|
||||
## 入口三:定时任务
|
||||
|
||||
### 本技能已启用定时扫描
|
||||
|
||||
1. 打开侧栏 **定时任务** → 新建
|
||||
2. 选择本技能 → Action **「扫描即将到期合同」**
|
||||
3. 默认参数:
|
||||
- 扫描阈值:90 / 60 / 30 天
|
||||
- 扫描时间:每日凌晨 02:00(可在定时任务面板调整)
|
||||
- 抄送规则:30 天红色预警自动抄送分管校领导
|
||||
4. 填写保存;可「立即运行」验证
|
||||
5. 每日到点后在「预警记录」表查看本次扫描结果
|
||||
|
||||
---
|
||||
|
||||
## 主能力对照表
|
||||
|
||||
| 想做什么 | 数据管理 | 对话任务 | 定时任务 |
|
||||
|----------|----------|----------|----------|
|
||||
| 扫描全校合同到期 | 表顶 **「立即扫描到期合同」** | 「帮我扫描一下全校合同到期」 | ✅ 每日凌晨 02:00 自动 |
|
||||
| 生成个人任务书 | 行内 → **「生成续聘任务书」** | 「给李老师生成一份续聘任务书」 | — |
|
||||
| 下发单位清单 | 批量 → **「按单位生成清单」** | 「给 XX 学院下发本月清单」 | — |
|
||||
| 标记已续签 | 行内 → **「标记为已续签」** | 「张老师合同已续签」 | — |
|
||||
|
||||
按钮名必须与 `assets/actions.json` 的 `label` 一致。
|
||||
|
||||
---
|
||||
|
||||
## 常见卡住怎么办
|
||||
|
||||
| 现象 | 你可以先试 |
|
||||
|------|------------|
|
||||
| 「立即扫描到期合同」点不了 | 是否已配置人事系统 API;是否装错版本 |
|
||||
| 扫描很久没结果 | 看任务中心进度;合同规模 > 2000 份可能耗时较久 |
|
||||
| 教师没收到任务书 | 检查教师通讯录中是否登记了正确的手机 / 邮箱 |
|
||||
| 红色预警没抄送领导 | 检查分管校领导通讯录是否配置;30 天阈值是否到达 |
|
||||
| 考核数据缺失 | 在党委教师工作部系统补录考核数据后,重新触发扫描 |
|
||||
|
||||
仍无法解决时,把「你用的入口 + 操作步骤 + 界面提示原文」发给支持人员。
|
||||
119
assets/actions.json
Normal file
119
assets/actions.json
Normal file
@@ -0,0 +1,119 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"skill": "alert-contract-expiring-hr",
|
||||
"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": "scan-expiring-contracts",
|
||||
"label": "扫描即将到期合同",
|
||||
"description": "扫描全校教职工合同电子台账,对距到期日 90/60/30 天的合同分别给出黄/橙/红三级预警,并自动生成个人任务书与单位清单。",
|
||||
"placements": ["cron", "agent", "toolbar"],
|
||||
"executionProfile": "async",
|
||||
"bind": {
|
||||
"tables": ["合同台账"]
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "scan-expiring-contracts",
|
||||
"args": ["--thresholds", "90,60,30"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "generate-renewal-task",
|
||||
"label": "生成续聘任务书",
|
||||
"description": "为指定合同对应的教师生成个人版《续聘准备任务书》(HTML + 通知推送),含近三年业绩摘要、AI 续聘建议、所需材料清单。",
|
||||
"placements": ["row", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"bind": {
|
||||
"tables": ["合同台账"],
|
||||
"selectionMode": "required",
|
||||
"labelColumn": "教职工姓名"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "generate-renewal-task",
|
||||
"args": ["--contract-id", "{row.合同ID}"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "dispatch-task-notice",
|
||||
"label": "下发续聘任务通知单",
|
||||
"description": "按二级单位或按月份生成单位版《本月合同到期人员清单及续聘建议》并推送给人事秘书。",
|
||||
"placements": ["batch", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"bind": {
|
||||
"tables": ["合同台账", "预警记录"],
|
||||
"selectionMode": "optional"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "dispatch-task-notice",
|
||||
"args": ["--unit-id", "{form.unit_id}", "--month", "{form.month}"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "mark-contract-renewed",
|
||||
"label": "标记合同已续签",
|
||||
"description": "将指定合同标记为已续签(须上传续签材料),并自动解除预警。",
|
||||
"placements": ["row"],
|
||||
"executionProfile": "sync",
|
||||
"bind": {
|
||||
"tables": ["合同台账"],
|
||||
"selectionMode": "required",
|
||||
"labelColumn": "教职工姓名"
|
||||
},
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "mark-contract-renewed",
|
||||
"args": ["--contract-id", "{row.合同ID}", "--material-id", "{form.material_id}"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "render-html",
|
||||
"label": "在浏览器中打开续聘任务书",
|
||||
"description": "渲染已生成的个人版《续聘准备任务书》HTML 页面,宿主 iframe 预览。",
|
||||
"placements": ["skill-detail", "agent"],
|
||||
"executionProfile": "sync",
|
||||
"entrypoint": {
|
||||
"type": "cli",
|
||||
"command": "render-html",
|
||||
"args": ["--task-id", "{form.task_id}"]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
118
assets/html/example-report.html
Normal file
118
assets/html/example-report.html
Normal file
@@ -0,0 +1,118 @@
|
||||
<!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, #1e3a8a 0%, #3b82f6 100%);
|
||||
color: #fff;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.header h1 { margin: 0 0 8px; font-size: 22px; }
|
||||
.header .meta { color: #dbeafe; font-size: 13px; }
|
||||
.info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.info-card {
|
||||
padding: 12px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e6eb;
|
||||
border-radius: 8px;
|
||||
}
|
||||
.info-card .label { color: #8c959f; font-size: 12px; }
|
||||
.info-card .value { font-size: 16px; font-weight: 600; margin-top: 4px; word-break: break-word; }
|
||||
.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: #1e3a8a; }
|
||||
.summary-text { white-space: pre-wrap; color: #4e5969; }
|
||||
.ai-suggestion {
|
||||
padding: 14px 18px;
|
||||
border-left: 4px solid #3b82f6;
|
||||
background: #eff6ff;
|
||||
border-radius: 4px;
|
||||
color: #1e40af;
|
||||
}
|
||||
.ai-suggestion.renew { border-left-color: #059669; background: #ecfdf5; color: #065f46; }
|
||||
.ai-suggestion.observe { border-left-color: #d97706; background: #fffbeb; color: #92400e; }
|
||||
.ai-suggestion.non-renew { border-left-color: #c53030; background: #fef2f2; color: #991b1b; }
|
||||
.checklist { list-style: none; padding: 0; margin: 0; }
|
||||
.checklist li {
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f1f3;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
.checklist li:last-child { border-bottom: 0; }
|
||||
.checklist .box {
|
||||
width: 16px; height: 16px;
|
||||
border: 1px solid #cbd5e1;
|
||||
border-radius: 3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.footer { margin-top: 16px; color: #8c959f; font-size: 12px; text-align: right; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="report">
|
||||
<div class="header">
|
||||
<h1>{{TITLE}}</h1>
|
||||
<div class="meta">任务编号:{{TASK_ID}} · 生成时间:{{GENERATED_AT}}</div>
|
||||
</div>
|
||||
|
||||
<div class="info-grid">
|
||||
<div class="info-card"><div class="label">教职工姓名</div><div class="value">{{TEACHER_NAME}}</div></div>
|
||||
<div class="info-card"><div class="label">工号</div><div class="value">{{EMPLOYEE_ID}}</div></div>
|
||||
<div class="info-card"><div class="label">所在单位</div><div class="value">{{UNIT_NAME}}</div></div>
|
||||
<div class="info-card"><div class="label">聘任岗位</div><div class="value">{{POSITION}}</div></div>
|
||||
<div class="info-card"><div class="label">合同类型</div><div class="value">{{CONTRACT_TYPE}}</div></div>
|
||||
<div class="info-card"><div class="label">合同起止</div><div class="value">{{CONTRACT_START}} ~ {{CONTRACT_END}}</div></div>
|
||||
<div class="info-card"><div class="label">距到期日</div><div class="value">{{DAYS_TO_EXPIRE}} 天</div></div>
|
||||
<div class="info-card"><div class="label">预警等级</div><div class="value">{{ALERT_LEVEL}}</div></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>近三年工作业绩摘要</h2>
|
||||
<div class="summary-text">{{PERFORMANCE_SUMMARY}}</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>AI 续聘建议</h2>
|
||||
<div class="ai-suggestion {{SUGGESTION_CLASS}}">
|
||||
<strong>{{SUGGESTION_LABEL}}</strong><br />
|
||||
{{SUGGESTION_REASON}}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>所需材料清单(请于 {{DEADLINE}} 前备齐)</h2>
|
||||
<ul class="checklist">
|
||||
{{CHECKLIST_BODY}}
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="footer">本任务书由「合同到期智能预警与续聘辅助」技能自动生成 · 最终续聘决定权归人事处</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
220
development/REQUIREMENTS.md
Normal file
220
development/REQUIREMENTS.md
Normal file
@@ -0,0 +1,220 @@
|
||||
# 合同到期智能预警与续聘辅助 — 技能需求文档
|
||||
|
||||
> 派生自匠厂 skill-template 主仓 `development/REQUIREMENTS.md`,对应规格书第 4.2 节「🟡 人事处 Skill 002 —— 合同到期"智能预警与续聘辅助"」。
|
||||
|
||||
---
|
||||
|
||||
## 0. 技能标识
|
||||
|
||||
- **拟定 slug(英文)**:`alert-contract-expiring-hr`
|
||||
- **中文 name**:合同到期智能预警与续聘辅助
|
||||
- **中文 description(一句话)**:每日凌晨扫描全校教职工合同电子台账,对距离到期日 90/60/30 天的合同分级预警,并自动生成《续聘准备任务书》和《本月合同到期人员清单》
|
||||
- **account-manager platform_key(如有)**:N/A
|
||||
- **命名形态**:标准型(动词-名词-部门缩写)
|
||||
- **目标 OEM 市场 `brands`(可选)**:未写则发布后仅挂匠厂(`jiangchang`)
|
||||
- **网页 RPA(如有)— 登录策略**:**不适用**(本技能不涉及网页 RPA)
|
||||
|
||||
## 0.1 宿主入口定位(必填)
|
||||
|
||||
| 主能力(Action id / CLI) | 对话 | 数据管理 | 定时 | 教程章节 | 不挂理由 |
|
||||
|---------------------------|------|----------|------|----------|----------|
|
||||
| `health` / `version` / `config-path` | 挂 | 不挂 | 不挂 | 入口二 | 默认必备 |
|
||||
| `scan-expiring-contracts` | 挂 | toolbar(合同台账) | 挂(每日凌晨 02:00) | 入口一/入口二/入口三 | 真用 cron |
|
||||
| `generate-renewal-task` | 挂 | row(合同台账) | 不挂 | 入口一/入口二 | sync |
|
||||
| `dispatch-task-notice` | 挂 | batch(合同台账/预警记录) | 不挂 | 入口一/入口二 | sync |
|
||||
| `mark-contract-renewed` | 不挂 | row(合同台账) | 不挂 | 入口一 | 写状态,不需 Agent |
|
||||
| `render-html` | 挂 | skill-detail | 不挂 | 入口二 | HTML 报告预览 |
|
||||
|
||||
### 0.1.1 用户市场文档(与入口表一致)
|
||||
|
||||
- `README.md`(说明 Tab):能力 / 场景 / 边界
|
||||
- `TUTORIAL.md`(教程 Tab):覆盖三入口(数据管理 / 对话 / 定时)
|
||||
- 入口三已写「已启用定时扫描」,与本表一致
|
||||
|
||||
## 0.2 AI + RPA 协作与授权边界
|
||||
|
||||
### Agent 设计
|
||||
|
||||
- 用户会怎样描述本任务:「张老师的合同什么时候到期?」「本月一共要办多少份续聘?」「帮我扫描一下全校合同到期」
|
||||
- 支持的用户意图 → Action 映射:
|
||||
- 触发扫描 → `scan-expiring-contracts`
|
||||
- 给某教师生成任务书 → `generate-renewal-task`
|
||||
- 下发单位清单 → `dispatch-task-notice`
|
||||
- 标记已续签 → `mark-contract-renewed`
|
||||
- 预览任务书 → `render-html`
|
||||
- 每种意图必须提取的参数:`contract_id` / `unit_id` / `month` / `material_id`
|
||||
- 缺少参数时必须追问:「请告诉我教师姓名 / 工号 / 合同 ID」
|
||||
- 哪些动作必须先展示对象、数量和影响,再取得明确确认:
|
||||
- `mark-contract-renewed` 状态变更(确认行 + 续签材料)
|
||||
- `dispatch-task-notice` 提示将推送至 N 位人事秘书
|
||||
- Agent 最终如何解释成功、部分成功、失败、等待人工和停止:
|
||||
- 成功:「已扫描全校 X 份合同,发现 Y 份 90 天内到期,其中 Z 份 30 天红色预警」
|
||||
- 部分成功:「已完成扫描,但 N 位教师的考核数据缺失,已标记为数据待补」
|
||||
- 失败:「人事系统 API 调用失败(错误码 XXX),已停止扫描并保留上一次的预警记录」
|
||||
|
||||
### 授权与平台边界
|
||||
|
||||
- 目标浏览器 / 桌面软件 / 手机 App:**N/A**(本技能不涉及 RPA)
|
||||
- 账号、设备、数据和业务权限由谁授权:人事处统一授权;考核 / 师德数据由党委教师工作部统一开通
|
||||
- 是否有获得授权的官方 API:人事系统 API(合同台账)、党委教师工作部 API(考核 + 师德)、企业微信推送 API(教师与人事秘书通讯)
|
||||
- 目标平台规则、合同或客户约定中的自动化边界:不得越权读取非本校教职工数据;不得向教师个人透露 AI 续聘建议(仅单位版清单展示)
|
||||
- 明确禁止的实现路线:**所有破解 / 反编译 / 逆向 / 私有协议 / 抓包拼隐藏接口 / Hook 或注入 / 验证码破解 / 设备指纹伪造 / 自动换号 均禁止**
|
||||
- 正常界面无法闭环时的上报与停止条件:人事系统 API 不可用超过 60 分钟 → 停止扫描并向人事处合同管理员上报
|
||||
|
||||
### RPA 风控设计
|
||||
|
||||
- 执行端:**N/A**
|
||||
|
||||
## 0.3 业务行生命周期
|
||||
|
||||
本技能对「合同台账」表中的每一份合同,遵守 `references/WORK_ITEM_STATUS.md` 角色约束:
|
||||
|
||||
| 角色 | 本技能状态值 | 说明 |
|
||||
|------|--------------|------|
|
||||
| 可操作(operable) | 可续聘 | 默认唯一可被 `scan-expiring-contracts` 捞起 |
|
||||
| 进行中(in_progress) | 预警中(黄/橙/红) | 抢占后、结束前 |
|
||||
| 成功类终态 | 已续签 | 人事处确认续签材料齐全 |
|
||||
| 失败类终态 | 已驳回 / 已过期 | 默认不可再处理;失败原因字段:驳回理由 / 过期说明 |
|
||||
| 跳过类终态 | 已撤回 | 教师或单位主动撤回续聘申请 |
|
||||
|
||||
> 重做靠人工将"已续签 / 已驳回 / 已过期"改回"可续聘",不靠 AI 自动覆盖。
|
||||
|
||||
---
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
本文档用于明确 `alert-contract-expiring-hr` 的研发范围、交付标准、依赖关系和验收要求。
|
||||
|
||||
## 2. 业务背景
|
||||
|
||||
当前用户在「全校教职工合同管理」场景中:
|
||||
|
||||
- **现状**:人事处合同管理员每月需人工翻查 1000+ 份合同,年度考核结果出后才仓促通知续签
|
||||
- **痛点**:
|
||||
- **效率低**:1 份合同的预警与材料准备需人工 2~3 小时
|
||||
- **容易遗漏**:试用期合同、跨年度合同尤其容易漏检
|
||||
- **劳动纠纷风险**:合同逾期未续签导致劳动争议
|
||||
- **年终突击**:每年 12 月人事处集中突击签合同,工作量暴增
|
||||
- **目标**:自动完成「合同到期扫描 + 三级预警 + 任务书预填 + 单位清单派发」,输出「个人版任务书 + 单位版清单 + 预警通知」
|
||||
|
||||
## 3. 开发目标
|
||||
|
||||
- 解析合同电子台账(含合同类型、起止日期、聘任岗位、试用期)
|
||||
- 拉取教师近 3 年年度考核结果 + 师德师风考评记录
|
||||
- 按距到期日 90/60/30 天分级,触发三级预警
|
||||
- 自动生成个人版《续聘准备任务书》(含业绩摘要 + AI 续聘建议 + 材料清单)
|
||||
- 自动生成单位版《本月合同到期人员清单及续聘建议》
|
||||
- 30 天红色预警自动抄送分管校领导
|
||||
- 写入业务表(合同台账 / 预警记录 / 续聘任务 / 派发日志)
|
||||
- 支持失败可诊断(稳定错误码 + 中文 message)
|
||||
- 完成 CLI 入口与 `health` / `version` / `init-db` 最小可运行验证
|
||||
- 完成正式环境发布与安装验证
|
||||
|
||||
## 4. 功能范围
|
||||
|
||||
### 必须实现
|
||||
|
||||
- 支持 `python scripts/main.py health` / `version` / `config-path` / `init-db`
|
||||
- 支持 `python scripts/main.py scan-expiring-contracts --thresholds 90,60,30`(async)
|
||||
- 支持 `python scripts/main.py generate-renewal-task --contract-id <id>`
|
||||
- 支持 `python scripts/main.py dispatch-task-notice --unit-id <id> --month <YYYY-MM>`
|
||||
- 支持 `python scripts/main.py mark-contract-renewed --contract-id <id> --material-id <id>`
|
||||
- 支持 `python scripts/main.py render-html --task-id <id>`
|
||||
- 完成 §0.1 入口定位表,并按表落实已挂入口
|
||||
- 定时任务接入宿主 `cron` 调度系统
|
||||
|
||||
### 可后续扩展
|
||||
|
||||
- 试用期合同不通过的特殊处理流程
|
||||
- 续聘材料 OCR 自动归档
|
||||
- 多语言合同(中英双语)支持
|
||||
|
||||
## 5. 非功能要求
|
||||
|
||||
- 目录结构必须符合当前 `skill-template` 规范
|
||||
- 入口必须统一为 `scripts/main.py`
|
||||
- 用户市场四 Tab 写在根 `README.md` / `TUTORIAL.md` / `DEMO.md` / `CHANGELOG.md`
|
||||
- 输出格式应尽量机读友好;错误前缀统一为 `ERROR:`
|
||||
- Windows 环境下需保证 UTF-8 输出兼容
|
||||
- 必须具备基本日志能力;敏感字段脱敏(教师身份证号、银行卡号等不写入日志)
|
||||
- 全校 1000+ 份合同扫描耗时 ≤ 2 分钟
|
||||
|
||||
## 6. 输入输出要求
|
||||
|
||||
### 输入
|
||||
|
||||
- `contract_id`:合同主键(必填,字符串)
|
||||
- `unit_id`:二级单位主键(仅 `dispatch-task-notice` 必填)
|
||||
- `month`:月份(仅 `dispatch-task-notice` 必填,格式 YYYY-MM)
|
||||
- `material_id`:续签材料 ID(仅 `mark-contract-renewed` 必填)
|
||||
- `thresholds`:扫描阈值(默认 `90,60,30`,可配置)
|
||||
- 依赖系统:
|
||||
- 人事系统 API:合同电子台账
|
||||
- 党委教师工作部 API:考核 + 师德数据
|
||||
- 企业微信推送 API:教师与人事秘书通讯
|
||||
- 环境变量(`.env`):
|
||||
- `HR_API_BASE_URL`:人事系统 API 入口
|
||||
- `HR_API_TOKEN`:调用凭证
|
||||
- `TEACHER_AFFAIRS_API_BASE_URL`:党委教师工作部 API 入口
|
||||
- `TEACHER_AFFAIRS_API_TOKEN`:调用凭证
|
||||
- `WECOM_API_BASE_URL`:企业微信 API 入口
|
||||
- `WECOM_API_TOKEN`:调用凭证
|
||||
|
||||
### 输出
|
||||
|
||||
- 成功结果:`scan-expiring-contracts` 返回结构化 JSON `{scanned_count, alert_yellow_count, alert_orange_count, alert_red_count, generated_task_count}`
|
||||
- 错误输出:`ERROR:` 稳定码 + 中文 message(如 `ERROR: HR_API_TIMEOUT 合同台账读取超时`)
|
||||
- 数据库记录:写入 `合同台账` / `预警记录` / `续聘任务` / `派发日志`
|
||||
- HTML 报告:个人版《续聘准备任务书》落到 `html/`
|
||||
|
||||
## 7. 依赖的兄弟技能或外部系统
|
||||
|
||||
### 兄弟技能依赖
|
||||
|
||||
- 无强制依赖
|
||||
|
||||
### 外部系统依赖
|
||||
|
||||
- **人事系统 API**(合同电子台账):必须
|
||||
- **党委教师工作部 API**(考核 + 师德):必须
|
||||
- **企业微信推送 API**:必须
|
||||
|
||||
### Python 包依赖(requirements.txt)
|
||||
|
||||
- HTTP 请求:`requests>=2.31.0,<3`
|
||||
- 日期处理:`python-dateutil>=2.8.0,<3`
|
||||
- HTML 渲染:`jinja2>=3.1.0,<4`
|
||||
- `jiangchang-platform-kit`、`playwright` 等公共能力由宿主共享 runtime 提供
|
||||
|
||||
## 8. 不在本次范围内的内容
|
||||
|
||||
- 本次不实现试用期合同不通过的自动化处分流程
|
||||
- 本次不集成续聘材料 OCR 识别
|
||||
- 本次不做合同电子签署(仍走线下 OA 流程)
|
||||
- 本次不兼容旧模板历史结构
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
- 代码结构符合模板规范;`SKILL.md` slug 与 `constants.SKILL_SLUG` 一致
|
||||
- `health` / `version` / `config-path` / `init-db` 命令执行正常
|
||||
- `scan-expiring-contracts` 在含 1000+ 份合同的样例库中完成一次完整扫描,耗时 ≤ 2 分钟
|
||||
- 业务表写入和查询符合 `references/SCHEMA.md`
|
||||
- 30 天红色预警自动抄送分管校领导功能验证通过
|
||||
- `SKILL.md` 的 `developer_ids` 已配置为开发者本人匠厂用户 ID
|
||||
- 已人工核验授权与技术路线:无破解、逆向、私有协议、验证码绕过、Hook / 注入、自动换号
|
||||
- 安装后按 §0.1 入口定位表完成宿主多入口验收
|
||||
- cron 每日定时扫描稳定运行 7 天无故障
|
||||
|
||||
## 10. 开发注意事项
|
||||
|
||||
- 只修改当前 skill 仓库,不要改动无关兄弟项目
|
||||
- 人事系统 API 不可用时停止并上报;不得擅自改走破解或绕过鉴权的路线
|
||||
- 教师身份证号、银行卡号等敏感字段严禁写入日志
|
||||
- 不得向教师个人透露 AI 续聘建议(仅单位版清单展示)
|
||||
- `cli` 只做参数解析;核心逻辑在 `service`;兄弟 skill 调用集中封装
|
||||
|
||||
## 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
|
||||
}
|
||||
15
requirements.txt
Normal file
15
requirements.txt
Normal file
@@ -0,0 +1,15 @@
|
||||
# 合同到期智能预警与续聘辅助 - 技能特有依赖
|
||||
# jiangchang-platform-kit / playwright 等公共能力由宿主共享 runtime 提供,不要写入此文件
|
||||
|
||||
# HTTP 请求
|
||||
requests>=2.31.0,<3
|
||||
|
||||
# 日期处理
|
||||
python-dateutil>=2.8.0,<3
|
||||
|
||||
# HTML 模板渲染(个人版任务书)
|
||||
jinja2>=3.1.0,<4
|
||||
|
||||
# PDF 生成(单位版清单)
|
||||
# weasyprint>=60.0,<61
|
||||
# reportlab>=4.0.0,<5
|
||||
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"alert-contract-expiring-hr health check (placeholder)")
|
||||
print(f"base_dir: {BASE_DIR}")
|
||||
return 0
|
||||
|
||||
|
||||
def version():
|
||||
"""查看版本(占位)。"""
|
||||
print("alert-contract-expiring-hr 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 scan_expiring_contracts(thresholds: str = "90,60,30"):
|
||||
"""扫描即将到期合同(async)。"""
|
||||
print(f"scan-expiring-contracts: thresholds={thresholds} (待开发者实现)")
|
||||
return 0
|
||||
|
||||
|
||||
def generate_renewal_task(contract_id: str):
|
||||
"""生成续聘任务书。"""
|
||||
print(f"generate-renewal-task: contract_id={contract_id} (待开发者实现)")
|
||||
return 0
|
||||
|
||||
|
||||
def dispatch_task_notice(unit_id: str = "", month: str = ""):
|
||||
"""下发续聘任务通知单。"""
|
||||
print(f"dispatch-task-notice: unit_id={unit_id} month={month} (待开发者实现)")
|
||||
return 0
|
||||
|
||||
|
||||
def mark_contract_renewed(contract_id: str, material_id: str):
|
||||
"""标记合同已续签。"""
|
||||
print(f"mark-contract-renewed: contract_id={contract_id} material_id={material_id} (待开发者实现)")
|
||||
return 0
|
||||
|
||||
|
||||
def render_html(task_id: str):
|
||||
"""在浏览器中打开续聘任务书。"""
|
||||
print(f"render-html: task_id={task_id} (待开发者实现)")
|
||||
return 0
|
||||
|
||||
|
||||
COMMANDS = {
|
||||
"health": health,
|
||||
"version": version,
|
||||
"config-path": config_path,
|
||||
"init-db": init_db,
|
||||
"scan-expiring-contracts": scan_expiring_contracts,
|
||||
"generate-renewal-task": generate_renewal_task,
|
||||
"dispatch-task-notice": dispatch_task_notice,
|
||||
"mark-contract-renewed": mark_contract_renewed,
|
||||
"render-html": render_html,
|
||||
}
|
||||
|
||||
|
||||
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