赢时胜加班单提交助手
创建人:jiya1996 最后实测验证:2026-05-12 版本:2.1(补充单日超过 8h 的按开始时间拆分 + 分批提交)
分发给同事使用:见
references/installation_for_colleagues.md本 skill 遵循 agentskills.io 开放标准,可装在 Claude Code / OpenClaw / Cursor / Goose 等 30+ 兼容客户端里。
这个 skill 解决什么问题
赢时胜员工每周需要把加班时长提交到 SHR 系统(金蝶 s-HR Cloud)的「我要奋斗」模块。整个流程涉及:
- 从禅道(pm.example.com:8071)拉打卡数据
- 按公司规则算出每天该报多少加班时长
- 从 SHR(oa.example.com:5887)查已提交的奋斗单做差集
- 从禅道工时确认页拉当天做的事(用作奋斗内容)
- 在 SHR「多条创建」里逐条填表 + 提交
整个流程做成「人在环(HITL)的半自动化」:计算和填表自动化,提交前用户人工 review。
何时使用本 skill
触发条件(任一即可):
- 用户提到"提加班单"、"提奋斗单"、"算加班"、"我要奋斗"
- 用户问"这周加班几小时"、"还有哪几天没提加班"
- 用户提到 SHR / 金蝶 / <your-company> 加班相关
- 用户说"帮我把上周加班补一下"
⚠️ 必读前置警告
1. 节假日库每年要更新一次
本 skill 用 chinese-calendar 库识别中国大陆法定节假日 + 调休补班。该库每年初待国务院公布次年放假安排后由维护者更新。
每年元旦后第一次跑 skill 前,运行:
python -m pip install -U chinese-calendar
否则跨年的日期判断会回落到「周一~周五 = 工作日」简单逻辑,调休补班和法定假日全算错。scripts/holidays.py 里已经做了 NotImplementedError 兜底 + warning,但你不会想要那个。
2. 公司可能禁用 RPA
金蝶 s-HR 可能埋点检测自动化操作。本次(2026-05-08)实测可用,不代表长期安全。用前最好和 IT/HR 确认一句——哪怕用自己账号操作。
3. Windows 控制台编码
Windows cmd / git-bash 默认 cp936,撞中文 + emoji 必崩。所有 Python 脚本必须加:
PYTHONIOENCODING=utf-8 python ...
4. 永远不要全自动提交
submit_overtime.py 必须传 --confirm 才会真正点提交按钮。orchestrator.py 走完所有阶段会暂停问 yes 才提交。
整体架构
┌────────────────────────────────────────────────────┐
│ 用户保持登录的 Chrome(手动过验证码 + 记密码) │
│ (启动方式见 references/chrome_setup.md) │
│ ├─ 禅道 tab https://pm.example.com:8071 │
│ └─ SHR tab https://oa.example.com:5887 │
└─────────────────────┬──────────────────────────────┘
│ Chrome DevTools Protocol (CDP, port 9222)
▼
┌────────────────────────────────────────────────────┐
│ Playwright(connect_over_cdp,复用登录态) │
│ ├─ 抓禅道考勤打卡 (extract_attendance.py) │
│ ├─ 抓 SHR 已提交奋斗单 (extract_submitted.py) │
│ ├─ 抓禅道工时确认(奋斗内容)(extract_effort.py) │
│ └─ 多条创建网格填表 + 提交 (submit_overtime.py) │
└─────────────────────┬──────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ 计算 + 节假日识别 │
│ ├─ calculator.py:工作日/非工作日规则、向下取整 │
│ └─ holidays.py:chinese_calendar 调休补班识别 │
└────────────────────────────────────────────────────┘
SHR 的三个关键 URL(金蝶 s-HR 是 SPA,每个页面 uipk 不一样):
- 默认入口:
uipk=...AtsOverTimeBillForm← 空白单条新建表单(干扰页) - 列表页:
uipk=...AtsOverTimeBillList← 抓已提交清单 + 入口枢纽 - 多条创建:
uipk=...AtsOverTimeBillForm.PersonnalBatch&method=addNew← 真正提交的网格
标准使用流程
阶段 0:一次性环境准备
- 按
references/chrome_setup.md启动带--remote-debugging-port=9222的 Chrome - 安装依赖:
python -m pip install -r requirements.txt python -m playwright install chromium - 在 Chrome 里手动登录禅道 + SHR(过验证码、勾「记住密码」)
- 自检:日常浏览器访问
http://localhost:9222,能看到禅道和 SHR 两个 tab
阶段 1:算时长 + 查差集(dry-run)
PYTHONIOENCODING=utf-8 python scripts/orchestrator.py \
--start 2026-05-04 --end 2026-05-10 \
--content "<你这周加班期间做的事>" \
--dry-run-only
orchestrator 会:
- 抓禅道考勤打卡
- 抓 SHR 已提交奋斗单
- 计算应提交差集;单日超过 8h 时自动按开始时间拆分多条(如 09:44-18:44 8h + 18:45-21:45 3h)
- 填奋斗内容(优先级:
--content命令行 > 用户终端输入 > 禅道工时确认页拉建议) - 把待提交清单存到
plan.json,dry-run 输出后退出
奋斗内容来源策略(无默认占位,必须三选一拿到,否则报错退出):
- 跑命令时加
--content "..."一次性给所有单子填同样内容 - 不加
--content时,会对每条单子先尝试从禅道工时确认页拉当天的任务名作为建议 - 禅道工时为空 → 终端会要求你手输(必填)
输出格式(示例):
=== DRY RUN:以下 N 条**不会**真实提交 ===
• 2026-MM-DD 周X HH:MM-HH:MM 报 X.Xh [工作日/非工作日] 使用方式=... 分段=1/2 提交时段=HH:MM-HH:MM | <奋斗内容>
阶段 2:确认无误后真正提交
PYTHONIOENCODING=utf-8 python scripts/submit_overtime.py \
--plan plan.json --confirm
或一键:
PYTHONIOENCODING=utf-8 python scripts/orchestrator.py \
--start 2026-05-04 --end 2026-05-10 \
--content "<填你这周加班做的事>" --confirm
提交流程:
- goto
SHR_BILL_MULTI_CREATE_URL直达多条创建网格 - 对每条单子:点
#addRow_entries→ 填日期 → 等金蝶自动带类型/开始时间/使用方式 → 选积分下拉 → 填备注- 若该日总时长超过 8h,脚本会把同一天拆成多条,并覆盖 SHR 自动带出的奋斗开始/结束时间、休息时长和隐藏申请时长缓存。
- 后续分段的开始时间必须和前一段不同(小时、分钟都建议错开 1 分钟),否则 SHR 会报「奋斗时间不能超过8个小时」。
- 点顶部
#submit按钮 → 弹金蝶 messenger 确认 → 点「确认」 a 标签 - URL 跳到
AtsOverTimeBillList= 提交成功
成功后到 SHR 列表页二次核对。
计算规则速查
详见 references/calculation_rules.md。
工作日(含调休补班):
- 仅可申请奋斗积分
- 加班 = 下班打卡 − 19:00
- 必须 ≥ 1h 才能提交(exact 1h 可以提)
- 向下取整到 0.5h
非工作日(含法定假日):
- 默认调休(可改奋斗积分)
- 加班 = 下班打卡 − 上班打卡 − 午晚餐扣除
- 午餐扣除:上班打卡 < 12:00 时扣 1h
- 晚餐扣除:下班打卡 ≥ 19:00 时扣 1h
- 单日净时长 > 8h:自动拆分多条,默认第一条最多 8h,第二条从上一条结束后 1 分钟开始
文件结构
yss-overtime-submission/
├── SKILL.md # 本文件
├── requirements.txt # playwright + chinese-calendar
├── references/
│ ├── calculation_rules.md # 计算规则全集
│ ├── chrome_setup.md # 启动 Chrome debug 模式
│ └── system_navigation.md # 禅道 / SHR 真实 URL + selector 对照表
└── scripts/
├── calculator.py # 纯计算逻辑(带 doctest)
├── holidays.py # chinese_calendar 包装 + 调休补班识别
├── selectors.py # 集中管理 CSS selectors(UI 改版改这里)
├── connect_chrome.py # Playwright CDP 连接工具
├── extract_attendance.py # 抓禅道打卡
├── extract_submitted.py # 抓 SHR 已提交单
├── extract_effort.py # 抓禅道工时确认(奋斗内容候选)
├── submit_overtime.py # 提交加班单(dry-run / confirm)
├── orchestrator.py # 总入口
├── simulate.py # 不连浏览器的逻辑模拟(mock 数据)
└── debug/ # selector 漂移时复用的 dump 脚本
├── debug_shr.py # 列表页 dump
├── debug_shr_list.py # 点奋斗列表按钮 + dump
├── debug_shr_filter.py # 展开筛选 + dump 日期 input
├── debug_shr_query.py # 查询 + dump 表格行
├── debug_shr_multi_create.py # dump 多条创建网格
├── debug_zt_effort.py # dump 禅道工时确认页
└── debug_zt_effort_5_6.py # 拉某天工时(输出 demo)
Selector 失效时的修复流程
UI 改版让脚本失效是迟早的事。修复步骤:
- 在 debug Chrome 里访问出问题的页面
- 跑
scripts/debug/里对应的 dump 脚本(5 个 dump 脚本对应 5 个关键页面) - 比对输出,定位错位的 selector
- 改
scripts/selectors.py里对应的常量 - 跑
python scripts/orchestrator.py --start ... --end ... --dry-run-only自测
所有 selector 集中在 scripts/selectors.py——一处出错只改一处。
已知坑(实跑踩过)
| 坑 | 解决 |
|---|---|
goto AtsOverTimeBillForm 直接进了一个空白表单干扰页,不是奋斗列表 | 用 AtsOverTimeBillList 直达列表页 |
| 列表页默认收起筛选,要先点「展开筛选」按钮才有日期 input | 脚本里先 get_by_text("展开筛选").click() |
顶部「奋斗列表」按钮 #returnToOverTimeBillList 是 not visible(被父容器遮住),普通 click 失效 | 用 page.evaluate("...click()") JS 兜底 |
列表页的「查询」按钮用 get_by_text("查询") 会误匹配到「未查询到结果」div | 用 #filter-search |
多条创建网格的 cell selector 不是 data-field,是 jqGrid 的 aria-describedby="entries_*" | 用 td[aria-describedby="entries_otDate"] 等 |
提交后弹的不是 native dialog,是金蝶 messenger 容器(page.on("dialog") 抓不到) | 找 [class*="messenger"] 里的「确认」 a 标签 click |
禅道工时页的「名称」「描述」是 <input value="..."> 不是 td 文本 | 用 td.querySelector('input').value |
禅道工时 URL 不是 m=effort&f=mywork,是 m=todo&f=confirmuserconsumed | 已修正 selectors.py |
| 单日加班超过 8h 时,同一张单里第二条如果沿用 SHR 自动带出的同一开始时间,会报「奋斗时间不能超过8个小时」 | 按起始时间拆分多条,第二段从上一段结束后 1 分钟开始,并同步更新 jqGrid data / row currentData / storeValue 三处缓存 |