@lark-people/cli 0.0.0-beta.3 → 0.0.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. package/README.md +8 -17
  2. package/README.zh.md +8 -17
  3. package/bin/people-cli_darwin_amd64 +0 -0
  4. package/bin/people-cli_darwin_arm64 +0 -0
  5. package/bin/people-cli_linux_amd64 +0 -0
  6. package/bin/people-cli_linux_arm64 +0 -0
  7. package/bin/people-cli_windows_amd64.exe +0 -0
  8. package/bin/people-cli_windows_arm64.exe +0 -0
  9. package/package.json +4 -1
  10. package/skills/people-attendance/SKILL.md +0 -667
  11. package/skills/people-attendance/references/people-attendance-submission.md +0 -426
  12. package/skills/people-attendance/scripts/resolve_date.py +0 -894
  13. package/skills/people-hire/SKILL.md +0 -255
  14. package/skills/people-hire/references/people-hire-application.md +0 -334
  15. package/skills/people-hire/references/people-hire-evaluation-task-list.md +0 -233
  16. package/skills/people-hire/references/people-hire-evaluation.md +0 -127
  17. package/skills/people-hire/references/people-hire-interview-record.md +0 -109
  18. package/skills/people-hire/references/people-hire-interview-task-list.md +0 -250
  19. package/skills/people-hire/references/people-hire-interview.md +0 -146
  20. package/skills/people-hire/references/people-hire-job-process.md +0 -93
  21. package/skills/people-hire/references/people-hire-job-requirement.md +0 -227
  22. package/skills/people-hire/references/people-hire-job.md +0 -277
  23. package/skills/people-hire/references/people-hire-minutes.md +0 -91
  24. package/skills/people-hire/references/people-hire-talent.md +0 -105
  25. package/skills/people-hire/references/people-hire-test.md +0 -197
  26. package/skills/people-overtime/SKILL.md +0 -992
  27. package/skills/people-overtime/references/people-attendance-overtime-apply.md +0 -491
  28. package/skills/people-overtime/references/people-attendance-overtime.md +0 -51
  29. package/skills/people-overtime/references/people-attendance-submission.md +0 -203
  30. package/skills/people-overtime/scripts/resolve_date.py +0 -894
@@ -1,667 +0,0 @@
1
- ---
2
- name: people-attendance
3
- version: 1.3.0
4
- description: "飞书出勤提报。支持查询出勤提报记录、出勤月度汇总。涉及查询出勤、出勤记录、出勤时长、出勤天数、出勤状态、出勤计算标签、出勤汇总等关键词时必须使用本 skill。"
5
- metadata:
6
- requires:
7
- bins: ["people-cli"]
8
- cliHelp: "people-cli attendance --help"
9
- ---
10
-
11
- # attendance (v1)
12
-
13
- ## 🚨 PR0:评测固化的高频错误(每次进入本 skill 必须先扫一遍)
14
-
15
- > 历史评测人工原因列里多次出现的"低级错误"已固化为本节硬规则,违反任意一条都直接导致评分不通过。所有规则在 SKILL 其他段落都有更详细的展开,这里是**入口检查清单**。
16
-
17
- 1. **走错 skill(误路由到 people-overtime)**
18
- - 用户 query **主词** = `attendance` / `出勤` / `attendance summary` / `attendance records` / `submit attendance` / `attendance hours` 等 → **必须**在本 skill 处理。
19
- - 即使 query 同时引用 `Overtime Pay` / `Working Day Overtime` / `Time off in Lieu (Overtime)` / `Saturday OT` / `Sunday OT` / `Public Holiday OT` / `Recorded Overtime` / `1.5x Daily Salary` / `2x Daily Salary` / `1.5 Hourly` 等含 `OT` / `Overtime` 子串的 calculation_tag 字面值,**仍属本 skill**。calculation_tag 只是过滤条件,绝不构成加班路由信号。
20
- - 反例(评测真实命中):`"How many hours of attendance were tagged as Overtime Pay and status 更正审批中 last month?"` —— 主词 `attendance` 必须走 `submission.get` (`time_type=1`),不要切到 `people-overtime`。
21
-
22
- 2. **英文 query → 英文回复(严格语种镜像)**
23
- - 用户 query 整句英文 / 主词英文 / 含英文日期表达(`today` / `last week` / `May 5`)→ 回复给用户的**所有可见文字**(结论、字段名、表格表头、status / unit / calculation_tag、reason、星期、日期回显、空结果话术、拒绝话术)**必须全部英文**,**禁止**任何中文字符出现在回复中。
24
- - 即使接口返回中文(CN 分支不支持 `accept_language`,或服务端偶发回退),LLM **必须**按下方「字段值映射表」手动翻译成英文再展示。
25
- - **空结果**:英文用户 → `No attendance records for {date}.`;**不要**写成 `2026-06-15: 无记录` / `2026-06-15: No record (duration=0, unit=hour)`,这两种都是评测命中的反例。
26
-
27
- 3. **`accept_language` 必须显式传**
28
- - 英文 query → `accept_language="en-US"`(区分大小写、必须中划线);中文 query → `"zh-CN"`。**禁止**省略,**禁止** `en` / `en_US` / `en-us` / `en_us` / `english` 等任何变体。
29
- - 仅 `submission.get` / `submission.query_month_summary` 支持本字段。
30
-
31
- 4. **日期解析必须走脚本,不许 LLM 心算**
32
- - 所有相对日期表达(`today` / `yesterday` / `the day before yesterday` / `this week` / `last week` / `this month` / `last month` / `last Monday` / `this Tuesday` / `May 5` / 中文同等表达)必须先调用 `scripts/resolve_date.py` 拿 `resolved_date` / `resolved_weekday`。
33
- - **禁止** LLM 自行口算星期(评测真实错误:anchor=2026-06-22(Mon) 时把"本周二"算成 06-16 / 06-24,正确是 06-23;把"本周"算成"06-15 起 1 天",正确是 06-22 起 1 天)。
34
- - 回显星期一律用脚本返回的 `anchor_weekday` / `resolved_weekday`,不要凭脑算。
35
-
36
- 5. **整周 / 整月 fan-out 必须完整覆盖区间内每一天**
37
- - `last month` / `2026-05` → 必须 fan-out **31 天**,不许漏头漏尾、不许跳过周末(除非用户明确写 `weekdays / workdays / 工作日`)。
38
- - `this week`(anchor=Monday)→ 仅当天 1 天;`this week`(anchor=Wednesday)→ Mon~Wed 3 天;不要扩成整周或退到上周。
39
- - `last week`(默认)→ 上周一 ~ 上周日 共 7 天;`last week's workdays` 才是上周一 ~ 上周五 5 天。
40
- - fan-out 之前**必须**先回显「Today is …. The range is X (Mon) ~ Y (Sun), N days in total.」让用户即时纠错。
41
-
42
- 6. **submission.get 单日数据:有 children 必须以 children 为准(最高优先级硬规则,唯一逻辑)**
43
-
44
- > ⚠️ **本规则在评测中已多次翻车,是 children 取不准的根因。请按下方"强制预处理流程"逐步执行,禁止跳步、禁止凭印象写聚合代码。**
45
-
46
- - **核心总则**:使用 `submission.get` 查询单日数据时,**只要 `rows[i]` 含非空 `children` 字段,就必须完全以 `children[].column` 为准**(包含 `calculation_tag` / `status` / `duration` 全部字段),**外层 `column` 一律不再读取**——外层只是被拆分前的汇总壳子,`calculation_tag` 通常是空字符串,直接拿外层会得到错误结果。
47
-
48
- - **强制预处理流程(每次拿到 `submission.get` 响应后必须执行的 3 步)**:
49
- 1. **Step 1 — 行级展开**:对每个 `rows[i]`,先判断 `children` 是否非空:
50
- - `len(rows[i].children) > 0` → 把当行替换为 `rows[i].children`(即用 children 列表代替自身),**外层 column 整个丢弃**
51
- - 否则 → 保留 `[rows[i]]`(用外层 column 当作单条明细)
52
- 2. **Step 2 — 字段提取**:在展开后的明细列表中,对每条 `column[]` 数组按 `code` 抽取 `attendance_date / status / duration / unit / calculation_tag`,转成扁平 dict(每条明细一份)。
53
- 3. **Step 3 — 多标签 split**:每条明细的 `calculation_tag` 是**逗号分隔的多个标签字符串**(例如 `"【SGP】常规出勤,【SGP】2 倍日薪"` 或 `"Regular,2 Daily"`),必须按英文逗号 `,` split 成 `tags = [t.strip() for t in calc_tag.split(",") if t.strip()]`,**用列表成员精确匹配**目标标签,而不是子串包含匹配。
54
-
55
- - **唯一判定逻辑(不可拆例外、不可放宽)**:
56
- ```
57
- # Step 1 — 行级展开
58
- items = []
59
- for r in rows:
60
- if r.children and len(r.children) > 0:
61
- items.extend(r.children) # 完全走 children
62
- else:
63
- items.append(r) # 没有 children 才取外层
64
-
65
- # Step 2 — 字段提取(展开后 items 是扁平的明细列表)
66
- records = []
67
- for it in items:
68
- col = it.column # 注意:children[i].column 同样是数组结构
69
- records.append({
70
- "attendance_date": col_get(col, "attendance_date"),
71
- "status": col_get(col, "status"),
72
- "duration": float(col_get(col, "duration") or 0),
73
- "unit": col_get(col, "unit"),
74
- "calculation_tag": col_get(col, "calculation_tag") or "",
75
- })
76
-
77
- # Step 3 — 命中过滤 / 求和(必须 split 后用列表成员匹配)
78
- def hit(rec, target_tag):
79
- tags = [t.strip() for t in rec["calculation_tag"].split(",") if t.strip()]
80
- return target_tag in tags # ✅ 列表成员精确匹配,不是子串 in 字符串
81
- ```
82
-
83
- - **多标签 split 命中规则(重要)**:
84
- - ✅ `tags = ["【SGP】常规出勤", "【SGP】2 倍日薪"]`,`"【SGP】2 倍日薪" in tags` → 命中
85
- - ❌ **禁止**直接 `"2 倍日薪" in calculation_tag`(子串匹配)——会把 `"【SGP】2 倍日薪"` 与 `"【SGP】1.2 倍日薪"`(如有)混淆,且会把外层空字符串当作"未命中"漏判。
86
- - ✅ 中英对照:CN 环境多标签如 `"【SGP】常规出勤,【SGP】2 倍日薪"`;EN 环境如 `"Regular,2 Daily"` / `"Regular,2x Daily Salary"`。两种环境下都必须先 split 再精确匹配。
87
- - ✅ 同义标签**等价集合**(命中时任一即可):
88
- - 「2 倍日薪 / 2 Daily / 2x Daily Salary」属于同一组
89
- - 「1.5 倍日薪 / 1.5 Daily / 1.5x Daily Salary」属于同一组
90
- - 「1.5 倍时薪 / 1.5 Hourly / 1.5x Hourly」属于同一组
91
- - 「常规出勤 / Regular」属于同一组
92
- - 「正常工作时间 / Normal Working Hours」属于同一组
93
- - 中英文环境分别返回上述对应字面值,命中时**只比对当前响应语种的字面值**即可(已传 `accept_language="zh-CN"` 就比中文,已传 `"en-US"` 就比英文),不要混用两种语言做联合命中。
94
-
95
- - **三类典型查询的取值映射(统一逻辑,不再分场景)**:
96
- | 用户问 | 取值口径 |
97
- |---|---|
98
- | 当天总时长 | `sum(records[].duration)`(**不再读外层 `column.duration`**,避免与 children 重复) |
99
- | 当天 status / 多状态明细 | 遍历 `records[].status`,分别列出(避免合并不同状态的记录) |
100
- | 按 calculation_tag 过滤 / 求和 / 计数 | 对 `records[]` 做 Step 3 split + 列表成员匹配,再按 `duration` 求和或按日计数 |
101
-
102
- - **状态过滤的常用值(已传 `accept_language` 后才会出现对应语种)**:
103
- - 中文:`已通过 / 审批中 / 已拒绝 / 已撤回 / 更正审批中 / 撤销审批中 / 已取消`
104
- - 英文:`Approved / Approving / Rejected / Withdrawn / Pending correction / Withdraw approving / Cancelled`
105
-
106
- - **绝对禁止(每条都是评测命中的低级错误)**:
107
- - ❌ 因为外层 `column.calculation_tag=""` 就回报「无标签 / 无该标签」(标签在 children 里)。
108
- - ❌ 同时把外层 `column` 与 `children[]` 都计入聚合(duration 重复、命中重复)。
109
- - ❌ 跳过 children 直接读外层 column 来做过滤 / 求和。
110
- - ❌ 把 `calculation_tag` 当作单一字符串做 substring 匹配(必须 split 成数组后精确比对)。
111
- - ❌ 「不涉及 calculation_tag 时优先外层 column」——例外分支已废弃,**所有字段统一 children 优先**。
112
- - ❌ 在中文响应里去匹配英文标签(或反之),导致全部漏命中。
113
-
114
- - **自检清单(提交答案前必须逐条 ✅)**:
115
- - ☐ 每个 `rows[i]` 都用 `len(children) > 0` 判断过分支
116
- - ☐ children 非空时,外层 column 整个被丢弃,没有再读取
117
- - ☐ `calculation_tag` 已经按 `","` split 成列表
118
- - ☐ 命中判断是 `target in tags`(列表成员)而非 `target in tag_str`(子串)
119
- - ☐ 当天总时长用了 children duration 求和,没有读外层 duration
120
- - ☐ 命中标签的语种与 `accept_language` 对应(中文响应不去匹配英文标签)
121
-
122
- - **典型样例(2026-06-07,必背)**:外层 `column` = `{ duration:9, status:已通过, calculation_tag:"" }`,`children[2]` 分别为:
123
- - `{ duration:8, status:已通过, calculation_tag:"【SGP】常规出勤,【SGP】2 倍日薪" }`
124
- - `{ duration:1, status:已通过, calculation_tag:"【SGP】1.5 倍时薪,【SGP】常规出勤" }`
125
-
126
- | 用户问 | 正确回答 | 错误回答 |
127
- |---|---|---|
128
- | 当天出勤总时长 | `8+1=9 小时`(children 求和) | ❌ 直接读外层 9(虽巧合相同,但逻辑错) |
129
- | 含「2 倍日薪」标签的时长 | split 后第 1 条命中 → `8 小时` | ❌ "外层标签为空 → 无该标签 → 0 小时" |
130
- | 含「常规出勤」标签的天数 | 两条都命中 → 当天 1 天 | ❌ 仅看外层 → 当天 0 天 |
131
- | 含「常规出勤」标签的时长 | 两条都命中 → `8+1=9 小时` | ❌ 仅命中其中一条 / 仅读外层 |
132
- | calculation_tag 是什么 | 列出 `["【SGP】常规出勤","【SGP】2 倍日薪"]` 与 `["【SGP】1.5 倍时薪","【SGP】常规出勤"]` 两条 | ❌ "外层为空 → 无标签" |
133
-
134
- 7. **接口选择硬规则:哪些月度问题禁止用 `query_month_summary`,必须强制走 `submission.get` 逐日 fan-out**
135
-
136
- > ⚠️ **本规则是 children 取不准的另一根因**。评测真实复盘:用户问「本月 2 倍日薪时长」时,模型图省事直接调用 `query_month_summary`,该接口**只返回顶层折算明细**(如「常规出勤 156 小时」),**不返回 children 拆分**,导致漏掉 `2 倍日薪` 标签的真实数据,错误回报「本月没有 2 倍日薪」。事实上 `2 倍日薪` 只存在于 `submission.get` 的 `children[].column.calculation_tag` 中。
137
-
138
- - **核心总则**:`query_month_summary` **只能用于「不涉及标签 / 严格状态过滤的月度总时长」**。**只要用户问题涉及 `calculation_tag` 过滤、严格 `status` 过滤、按标签拆分天数 / 时长,就必须强制走 `submission.get` 逐日 fan-out 并按 children 解析**,**严禁**用 `query_month_summary` 替代。
139
-
140
- - **接口能力对照**:
141
- | 能力 | `query_month_summary` | `submission.get` |
142
- |---|---|---|
143
- | 月度总时长(笼统) | ✅ 一次调用即可 | 需逐日 fan-out + children 求和 |
144
- | calculation_tag 拆分(含 children) | ❌ **不返回 children**,只有顶层折算明细 | ✅ children 优先解析 |
145
- | 严格 status 过滤(仅已通过 / 仅已拒绝) | ❌ **已含审批中**,无法严格分离 | ✅ 按 children status 精确过滤 |
146
- | 多标签 split 命中 | ❌ 不支持 | ✅ split + 列表成员匹配 |
147
- | 单日 / 周 / 区间查询 | ❌ 仅支持单月 | ✅ 单日,区间需 fan-out |
148
-
149
- - **必须强制走 `submission.get` 逐日 fan-out 的月度问题(禁用 `query_month_summary`)**:
150
- - ❌ 「本月 2 倍日薪 / 1.5 倍时薪 / 常规出勤 / 正常工作时间 / 调休假 时长是多少」→ 涉及 calculation_tag 拆分
151
- - ❌ 「本月 calculation_tag = X 的天数 / 命中天数 / 命中时长」→ 涉及 calculation_tag 过滤
152
- - ❌ 「本月已通过的出勤是多少 / 仅审批中 / 仅已拒绝」→ 涉及严格 status 过滤
153
- - ❌ 「本月含 X 标签且 status = Y 的出勤」→ 涉及 calculation_tag + status 联合过滤
154
- - ❌ 「How many hours of 2x daily salary attendance do I have this month?」→ 同上(英文同理)
155
- - ❌ 「本月 calculation_tag 是 Regular 且状态为已通过的出勤天数」→ 同上
156
-
157
- - **可以用 `query_month_summary` 的月度问题(无需 fan-out)**:
158
- - ✅ 「本月 / 上月出勤总时长是多少」(不涉及任何标签或严格 status 过滤)
159
- - ✅ 「本月共出勤了多少小时」「May 的出勤累计」
160
- - ⚠️ 但**回复时必须主动声明「数值已含审批中」**,详见下文 `query_month_summary` 章节。
161
-
162
- - **决策流程(拿到月度问题后的强制思考链)**:
163
- ```
164
- Step A:用户的月度问题里是否含 calculation_tag 名称(2 倍日薪 / Regular / 1.5 Hourly / Overtime Pay …)?
165
- → 是 → 强制走 submission.get fan-out(不允许尝试 query_month_summary)
166
- → 否 → Step B
167
- Step B:用户是否要严格按某个 status 过滤(仅已通过 / 仅已拒绝 / 仅更正审批中)?
168
- → 是 → 强制走 submission.get fan-out
169
- → 否 → Step C
170
- Step C:用户只是问「本月总时长 / 累计多少」?
171
- → 是 → 用 query_month_summary(一次调用),但回复必须带「已含审批中」说明
172
- ```
173
-
174
- - **绝对禁止(评测真实命中的低级错误)**:
175
- - ❌ 用 `query_month_summary` 的「常规出勤 156 小时」当作「本月所有标签的总时长」回复用户。
176
- - ❌ 因为 `query_month_summary` 顶层没有出现某标签(如 `2 倍日薪`),就回报「本月没有该标签的出勤」——该接口本就不返回 children 明细。
177
- - ❌ 「为了避免 30 次限流」而把 calculation_tag / status 过滤的月度问题硬塞给 `query_month_summary`——遇到限流应按 PR0 #99991400 退避重试策略,而不是退化到错误接口。
178
- - ❌ 把 `query_month_summary` 与 `submission.get` 的结果混合聚合(口径不一致:前者含审批中、无 children;后者可严格按 status / children 拆分)。
179
-
180
- - **典型反例对照(必背)**:
181
- | 用户问 | 错误做法 | 正确做法 |
182
- |---|---|---|
183
- | 本月 2 倍日薪时长 | 调 `query_month_summary` 看顶层 → 没有 2 倍日薪 → 回报「本月没有 2 倍日薪」 | 逐日 fan-out `submission.get` → children 拆分 → split `calculation_tag` → 命中「【SGP】2 倍日薪」 → 06-07 当天 8 小时 |
184
- | 本月 Regular 标签且已通过的出勤天数 | 调 `query_month_summary` 看顶层「常规出勤 156 小时」 → 折算成 19 天 | 逐日 fan-out → children 拆分 → split + 列表成员命中「Regular / 【SGP】常规出勤」+ status="已通过" → 7 天 |
185
- | 本月已通过的出勤是多少 | 调 `query_month_summary` 直接回 156 小时 | 逐日 fan-out → 严格按 children status="已通过" 求和(避免把审批中算进去) |
186
- | 本月出勤总时长 | ✅ `query_month_summary` 一次拿走 + 声明「已含审批中」 | ✅ 同左 |
187
-
188
- > **入口检查口令**:① 主词(attendance vs overtime) → ② 语种(英文/中文) → ③ accept_language(en-US / zh-CN) → ④ 日期(resolve_date.py) → ⑤ 完整 fan-out → ⑥ children 优先 → ⑦ **接口选择**(涉及 calculation_tag / 严格 status 过滤的月度问题禁用 `query_month_summary`,强制走 `submission.get` fan-out) → 调接口 → 用对应语种回复。
189
-
190
- > [!IMPORTANT]
191
- > **本 skill 只查询「出勤提报(Attendance)」数据**,所有查询都必须显式指定 `time_type=1`。
192
- > - 用户问「加班/补加班/加班审批/调休/加班费/OT/overtime」等**业务主词**:**不要**调用本 skill(避免误查出勤数据),应切到 `people-overtime`。
193
- > - 不要依赖返回字段里的 `display_name`(可能出现"出勤/加班"字样)来"猜"要查什么;以用户意图为准。
194
- >
195
- > ⚠️ **极重要原则:calculation_tag 的字面值不是加班路由信号**。`Overtime Pay` / `Working Day Overtime` / `1.5x Daily Salary` / `2x Daily Salary` / `Time off in Lieu` / `Time off in Lieu (Overtime)` / `Saturday OT` / `Sunday OT` / `Public Holiday OT` / `Rest Day OT` / `1.5 Hourly` / `1.5x Hourly` / `Recorded Overtime` / `Recorded Attendance` / `Normal Working Hours` / `Regular Hours` / `Regular` / `by_duration` / `by_time` 等都只是**出勤提报记录里的计算标签 / 出勤类型字段值**,CN 与海外员工的出勤数据中都可能出现(中文环境下的 `【SGP】1.5 倍时薪 / 【SGP】2 倍日薪 / 【SGP】常规出勤 / 【SGP】调休假` 同理)。**禁止**仅凭 query 中包含这些英文标签字面值(哪怕含 `OT` / `Overtime` 子串)就把意图判定为「加班」并切到 `people-overtime`,必须以用户显式表达的**业务主词**判定数据域:
196
- > - 主词是「出勤 / attendance / 上班时长 / 出勤提报 / 出勤汇总 / 出勤记录」 → **本 skill**(即使 query 里同时引用了 `1.5x Daily Salary` / `Overtime Pay` / `Time off in Lieu (Overtime)` / `Recorded Overtime` 等含 OT/Overtime 子串的标签字面值),逐日 `submission.get` + `time_type=1` 后按 `rows[].(children[]?).column.calculation_tag` 过滤聚合即可。
197
- > - 主词是「加班 / overtime / OT / 加班审批 / 调休 / 加班费」 → 切到 `people-overtime`。
198
- > - 同时混用两个主词或仅给标签字面值而无主词时 → 默认按**出勤**处理,并在回复中明确"本次查询的是出勤提报 (time_type=1),如需查加班请明确说明",**禁止**直接走加班路径。
199
-
200
- > [!CAUTION]
201
- > **英文环境下的反例(评测高频翻车点,必须杜绝)**:
202
- > - ❌ 用户问 "What is the total **attendance** duration with the `1.5x Hourly` tag last week?" → 主词是 `attendance`,`1.5x Hourly` 只是 calculation_tag 字面值 → 必须走本 skill 的 `submission.get` (`time_type=1`),不要因为标签里有 "Hourly" / "OT" 子串切到 `people-overtime`。
203
- > - ❌ 用户问 "How many hours of **attendance** were tagged as `Working Day Overtime` this month?" → 主词是 `attendance`,标签虽含 `Overtime` 单词但仍是出勤记录的计算标签 → 必须走本 skill。
204
- > - ❌ 用户问 "How much of my **attendance** last week was `Time off in Lieu (Overtime)` / `Saturday OT` / `Sunday OT` / `Public Holiday OT`?" → 主词是 `attendance`,标签虽含 OT 字样但属于出勤记录的口径标签 → 必须走本 skill。
205
- > - ❌ 用户问 "Which days last week did my **attendance** include `Recorded Overtime` rows?" → `Recorded Overtime` 是 `display_name` 字段值,仍属出勤记录 → 必须走本 skill。
206
- > - ❌ 用户问 "本月 calculation_tag=`1.5x Daily Salary` 的**出勤**汇总" / "SGP 本月 `2x Daily Salary` **出勤**汇总" → 主词「出勤」,必须走本 skill。
207
- > - ✅ 正确判定的关键:先看 query 里的**主词**(attendance / 出勤 / submitted attendance / attendance hours / attendance summary / attendance records / attendance with tag …),主词决定走 attendance;calculation_tag 字面值仅是过滤条件,不改变路由。
208
-
209
- > [!CAUTION]
210
- > **Non-goals(明确不支持,遇到这类意图立即拒绝,不要调用 CLI)**
211
- > - ❌ 提交 / 补提交 / 修改 / 撤回 / 删除 出勤记录(本 skill 仅只读查询)
212
- > - ❌ 查询他人(工号 / employee_id / open_id / user_id / 下属);只查当前登录账号本人
213
- > - ❌ **显式未来意图**:明天 / 明日 / 后天 / 大后天 / 下周X / 下个月X号 / 明年X月 / N天后 / N周后 等"明确指向未来"的表达 → 直接拒绝
214
- > - ❌ 非法日期(如 2 月 30 日 / 13 月)
215
- > - ❌ 加班审批 / 加班申请 → 改用 `people-overtime` skill
216
- > - ❌ 出勤打卡 / 补卡 / 排班修改
217
-
218
- > [!NOTE]
219
- > **重要:单日查询若日期"本身合法但碰巧落在未来"(如今天周一查"本周三/本周五"、本月 1 日查"本月 30 号"),仍应正常调用接口,由后端返回"无记录"。这一类不属于"显式未来意图",**不要**直接拒绝。
220
- > - 判断口径:依据 `resolve_date.py` 输出的 `is_future` 字段 + 表达式语义共同决定:
221
- > - 表达式以「明天/后天/下周/下月/明年/N 天后」等开头 → 即使 `is_future=true` 也拒绝
222
- > - 表达式属于「本周X/本月N号/今年X月X日」等,且 `is_future=true` → **正常查询**,输出"无记录"或当前实际状态
223
- > - 范围查询(本周/本月/今年/上半年)若 `partially_in_future=true`,必须使用 `--clamp-to-anchor` 把结束裁剪到今天;切勿把未来日期也拿去查。
224
-
225
- ## Intent → Command 速查(高频场景一表导航)
226
-
227
- | 用户意图 | 推荐命令 | 关键参数 |
228
- |---|---|---|
229
- | 今天 / 昨天 / 某一天的出勤记录 | `attendance submission get` | `date=YYYYMMDD`, `time_type=1` |
230
- | 本月 / 上月 / 某月累计**总时长**(不涉及 calculation_tag、status 过滤) | `attendance submission query_month_summary` | `month=YYYYMM` |
231
- | 本月 / 上月 / 某月**按 calculation_tag 过滤**(如「2 倍日薪 / 1.5 倍时薪 / 常规出勤」时长 / 天数) | **必须**逐日 `submission get` 后 children 拆分聚合 | `time_type=1`;**禁止**用 `query_month_summary` |
232
- | 本月 / 上月 / 某月**按严格 status 过滤**(如「已通过 / 已拒绝 / 更正审批中」) | **必须**逐日 `submission get` 后按 children status 过滤 | `time_type=1`;`query_month_summary` 已含审批中,不能严格 status 过滤 |
233
- | 本周 / 上周 / 区间累计出勤 | 逐日 `submission get` 后聚合 | 一日一次,多日见下文聚合规则 |
234
- | 出勤记录按 calculation_tag 过滤(Regular / Normal Working Hours / 1.5x Daily Salary / 2x Daily Salary / Overtime Pay / Working Day Overtime / Time off in Lieu / Recorded Overtime …) | 逐日 `submission get` 后**有 children 走 children[].column**,无 children 才用外层 `column` | `time_type=1`;**前提:query 主词是出勤**;详见下文「children 拆分行规则」 |
235
- | 按 status 过滤(已通过 / 审批中 / 已拒绝 / 更正审批中) | 逐日 `submission get` 后**有 children 走 children[].column.status**,无 children 才用外层 `column.status` | `time_type=1` |
236
- | 查加班 / 加班审批 / 调休(**业务主词**为加班,而不是仅出现 calculation_tag 字面值) | **改用 `people-overtime` skill** | — |
237
-
238
- > [!IMPORTANT]
239
- > **`submission.get` 返回结构里的 `children` 拆分行规则(硬规则,唯一逻辑,与 PR0 #6 完全一致)**:
240
- >
241
- > **一句话总则**:使用 `submission.get` 查询单日数据时,**只要返回的 `rows[i]` 含非空 `children` 字段,就完全以 `children[].column` 为准**(包含 `calculation_tag` / `status` / `duration` 全部字段,外层 `column` 一律不再读取);**只有当 `rows[i]` 没有 `children`(或 `children=[]`)时,才取外层 `column`**。
242
- >
243
- > - `data.rows[i]` 形如 `{ "column": [...], "children": [ { "column": [...] }, ... ] }`。
244
- > - 当某一天的出勤被服务端**拆分过**(例如同一天既有「常规 8 小时」又有「加班 1 小时」),**真实拆分明细在 `children[].column`**(每条 `duration` 是分量、`calculation_tag` 是真实标签、`status` 是真实状态);外层 `column` 只是被拆分前的汇总壳子,**一律忽略**。
245
- > - 当某一天没有拆分时,`rows[i]` 不带 `children` 字段(或 `children=[]`),此时直接用外层 `column` 即可。
246
- > - **统一规则(不区分查询场景)**:
247
- > - **若 `rows[i]` 含 `children` 且非空**:**必须**完全走 `children[].column`,包括但不限于:
248
- > - `calculation_tag` 过滤 / 命中 / 求和 / 计数 → 遍历 `children[].column.calculation_tag`
249
- > - `status` 过滤 / 命中 / 拆分明细 → 遍历 `children[].column.status`
250
- > - 当天总时长 → `sum(children[].column.duration)`(**不再读外层 `column.duration`**)
251
- > - 同一天多状态 / 多标签的明细列举 → 分别列出每条 `children[].column`
252
- > - **若 `rows[i]` 不含 `children` 或 `children=[]`**:才用外层 `column.calculation_tag` / `column.status` / `column.duration`。
253
- > - **禁止**对同一天同时把外层 `column` 与 `children[]` 都计入聚合(会重复计数、错误命中)。
254
- > - **禁止**「不涉及 calculation_tag 时优先外层 column」的例外分支——已废弃。只要有 children,就完全走 children。
255
- >
256
- > - **多标签 split 规则(命中判断时必须执行)**:
257
- > - 每条明细的 `calculation_tag` 是**逗号分隔的多标签字符串**(如 `"【SGP】常规出勤,【SGP】2 倍日薪"` / `"Regular,2 Daily"`)。
258
- > - 命中前必须先 `tags = [t.strip() for t in calc_tag.split(",") if t.strip()]`,再用**列表成员精确匹配**(`target in tags`)。
259
- > - **禁止**直接 `target in calc_tag`(子串匹配)——会混淆相似标签(如 `"2 倍日薪"` 误命中 `"1.2 倍日薪"`),也会把外层空字符串当未命中。
260
- > - **同义标签等价集合(命中时任一即可,但只比对当前响应语种的字面值,不要跨语种联合命中)**:
261
- > - 「2 倍日薪 / 2 Daily / 2x Daily Salary」
262
- > - 「1.5 倍日薪 / 1.5 Daily / 1.5x Daily Salary」
263
- > - 「1.5 倍时薪 / 1.5 Hourly / 1.5x Hourly」
264
- > - 「常规出勤 / Regular」
265
- > - 「正常工作时间 / Normal Working Hours」
266
- > - 「调休假 / Time off in Lieu (Overtime)」
267
- >
268
- > - **伪代码(必须照此实现,不可凭印象写聚合)**:
269
- > ```
270
- > def col_get(col_array, code):
271
- > for c in col_array or []:
272
- > if c.code == code: return c.value
273
- > return None
274
- >
275
- > # Step 1 — 行级展开
276
- > items = []
277
- > for r in data.rows:
278
- > if r.children and len(r.children) > 0:
279
- > items.extend(r.children) # 完全走 children
280
- > else:
281
- > items.append(r) # 没有 children 才取外层
282
- >
283
- > # Step 2 — 字段提取(统一扁平化)
284
- > records = []
285
- > for it in items:
286
- > col = it.column
287
- > records.append({
288
- > "attendance_date": col_get(col, "attendance_date"),
289
- > "status": col_get(col, "status"),
290
- > "duration": float(col_get(col, "duration") or 0),
291
- > "unit": col_get(col, "unit"),
292
- > "calculation_tag": col_get(col, "calculation_tag") or "",
293
- > })
294
- >
295
- > # Step 3 — 命中过滤 / 求和(split 后用列表成员精确匹配)
296
- > def hit(rec, target_tag):
297
- > tags = [t.strip() for t in rec["calculation_tag"].split(",") if t.strip()]
298
- > return target_tag in tags
299
- > ```
300
- >
301
- > - **自检清单(提交答案前必须逐条 ✅)**:
302
- > - ☐ 每个 `rows[i]` 都按 `len(children) > 0` 走过分支
303
- > - ☐ children 非空时,外层 column 整个被丢弃,没有再读取
304
- > - ☐ `calculation_tag` 已经按 `","` split 成列表
305
- > - ☐ 命中判断是 `target in tags`(列表成员),不是 `target in tag_str`(子串)
306
- > - ☐ 当天总时长用 children duration 求和,没有读外层 duration
307
- > - ☐ 命中标签的语种与 `accept_language` 对应(中文响应不去匹配英文标签)
308
- >
309
- > - **样例(2026-06-07)**:外层 `column` 含 `duration=9, status=已通过, calculation_tag=""`,`children` 中两条分别为 `8 小时(【SGP】常规出勤,【SGP】2 倍日薪, 已通过)` 与 `1 小时(【SGP】1.5 倍时薪,【SGP】常规出勤, 已通过)`。
310
- > - 用户问「2026-06-07 出勤总时长」→ 走 children,`8+1=9 小时`(**不再**直接读外层 9 小时)。
311
- > - 用户问「2026-06-07 含 `2 倍日薪` 标签的出勤多少小时」→ split 后第 1 条 `tags=["【SGP】常规出勤","【SGP】2 倍日薪"]` 命中 → `8 小时`;**绝不能**因外层 `calculation_tag=""` 就回答「无该标签」。
312
- > - 用户问「2026-06-07 含 `常规出勤` 标签的出勤天数 / 时长」→ split 后两条都命中 `"【SGP】常规出勤"` → 当天 1 天 / 合计 `8+1=9 小时`,**不能**仅命中其中一条。
313
- > - 用户问「2026-06-07 calculation_tag 是什么」→ 列出 children 里两条标签 `["【SGP】常规出勤","【SGP】2 倍日薪"]` / `["【SGP】1.5 倍时薪","【SGP】常规出勤"]`,**禁止**回报「外层 column.calculation_tag 为空,无标签」。
314
-
315
-
316
- ### 相对时间窗口语义(必须按下表统一解析)
317
-
318
- | 用户表达 | 解析口径 |
319
- |----------|----------|
320
- | 今天 / today | `Now` 日期 |
321
- | 昨天 / yesterday | `Now - 1 day` |
322
- | 本周 / this week | ISO 周一 ~ ISO 周日(**包含周六周日**);做范围查询时必须裁剪到今天(`--clamp-to-anchor`) |
323
- | 上周 / last week | 上一个 ISO 周一 ~ 上一个 ISO 周日(**完整 7 天,包含上周六、上周日**),不得默认裁剪为周一~周五 |
324
- | 本周X(如本周三) | ISO 周一开始第 X 天,**即使是未来也照常单日查询,由后端返回无记录** |
325
- | 上周X | 上一 ISO 周对应日 |
326
- | 下周 / 下周X / 明天 | **显式未来 → 直接拒绝** |
327
- | 本月 / this month | 本月 1 日 ~ `Now`(包含周末,只到今天,不包含未来日期,必须 `--clamp-to-anchor`) |
328
- | 上个月 / last month | 上月 1 日 ~ 上月最后一天(**完整自然月,包含周末**) |
329
- | 上半年 / 下半年 | 1-6 月 / 7-12 月,需按自然月拆分逐月调用;下半年若全部在未来,拒绝 |
330
- | 某月(如 `5月` / `5月份` / `May`) | 默认当年;若用户给的月份 > `Now` 月份(跨年情形),回退为去年同月 |
331
- | `今年X月` / `去年X月` / `YYYY年X月` | 严格按年份解析(脚本支持) |
332
-
333
- > ⚠️ **「上周」/「本周」的范围查询必须包含周末**:
334
- > - 用户问「上周哪几天出勤超过 9 小时」「上周 calculation_tag = X 的有多少」「which days last week did I work more than 9 hours」时,**fan-out 必须覆盖 7 天**(周一 ~ 周日)。
335
- > - 历史评测里出现过「上周」被错误裁剪成「上周一 ~ 上周五」(漏掉上周六、上周日)的低级错误,**严禁**。
336
- > - **只有当用户明确说出「上周工作日 / weekdays / 工作日」时**,才裁剪到周一 ~ 周五;其它情况一律 7 天 fan-out。
337
- > - 同理「本周」范围 = 本周一 ~ `Now`(若 `Now` 落在周六、周日,必须包含到当日,不要主动剔除周末)。
338
-
339
- > [!CAUTION]
340
- > **英文环境日期解析强校验(评测高频翻车,必须严格执行)**
341
- >
342
- > 历史评测「人工原因分析」中已固化的错误案例:
343
- > - ❌ `this week` 被解析为 **「上周一 ~ 本周一」**(错把 anchor 当成周日/周末算 ISO 边界)。正解:以 ISO 周一为周首日,`this week` = `ISO_Monday(anchor) ~ ISO_Sunday(anchor)`;做范围查询时再用 `--clamp-to-anchor` 裁剪到今天。anchor=2026-06-22(Monday)时 `this week` = `2026-06-22 ~ 2026-06-28`,clamp 后 = `2026-06-22 ~ 2026-06-22`。
344
- > - ❌ `last week's workdays` 被错解为本周一~本周五 / 上周二~上周六。正解:anchor=2026-06-22(Mon)时 `last week's weekdays / last week 工作日` = **2026-06-15 (Mon) ~ 2026-06-19 (Fri)** 共 5 天。`last week`(无 weekdays 限定)= **2026-06-15 ~ 2026-06-21** 共 7 天。
345
- > - ❌ `yesterday` / `day before yesterday` 被 LLM 心算错(如把"前天"理解为"昨天",或没按 anchor 减法)。正解:**严格 `anchor − 1` / `anchor − 2`**。anchor=2026-06-22 时 `yesterday=2026-06-21`、`day before yesterday=2026-06-20`;anchor=2026-06-17 时 `yesterday=2026-06-16`、`day before yesterday=2026-06-15`。
346
- >
347
- > **强制行为(不可由 LLM 心算替代)**
348
- >
349
- > | 英文表达 | 必须使用的脚本调用 | 期望返回字段 | 严格回显模板 |
350
- > |---|---|---|---|
351
- > | `today` | `resolve_date.py --expr "today" --anchor <Now>` | `resolved_date` | `Today is <anchor> (<anchor_weekday>). The date you're asking about is <resolved_date> (<resolved_weekday>).` |
352
- > | `yesterday` | `--expr "yesterday"` | `resolved_date = anchor - 1` | 同上 |
353
- > | `the day before yesterday` / `day before yesterday` | `--expr "day before yesterday"` | `resolved_date = anchor - 2` | 同上 |
354
- > | `tomorrow` / `next Monday` / `next week` … | `--expr <原文>` | `is_future=true` | 直接拒绝(显式未来);不要尝试调接口 |
355
- > | `this week` | `--expr "this week" --anchor <Now> --clamp-to-anchor` | `type=range, resolved_date_from=ISO_Mon, resolved_date_to=clamped_to_anchor` | `Today is <anchor>. This week is <date_from> (<from_wd>) ~ <date_to> (<to_wd>), <N> days in total.` |
356
- > | `last week` | `--expr "last week" --anchor <Now>` | `type=range, resolved_date_from=上ISO_Mon, resolved_date_to=上ISO_Sun` | `Today is <anchor>. Last week is <date_from> (Mon) ~ <date_to> (Sun), 7 days in total.` |
357
- > | `last week's workdays` / `last week weekdays` | `--expr "last week" --anchor <Now>`,**结果再按本地 weekday 0-4 (Mon-Fri) 过滤** | `date_from = 上Mon, date_to = 上Fri` | `Today is <anchor>. Last week's weekdays are <Mon> (Mon) ~ <Fri> (Fri), 5 days in total.` |
358
- > | `this month` | `--expr "this month" --anchor <Now> --clamp-to-anchor` | `type=range, from=本月1日, to=anchor` | 同上 |
359
- > | `last month` | `--expr "last month" --anchor <Now>` | `type=range, from=上月1日, to=上月末` | 同上 |
360
- > | `last Monday` / `last Friday` / `last Sunday` | `--expr "last Monday/Friday/Sunday" --anchor <Now>` | `resolved_date = 上ISO周对应日` | 注意 `last Sunday` 是上周日(不是本周日) |
361
- > | `this Wednesday` / `this Friday` | `--expr "this Wednesday" --anchor <Now>` | `resolved_date = 本ISO周对应日`(即使未来也按规则返回) | 单日查询:未来日期由后端返回无记录,**不要直接拒绝** |
362
- >
363
- > **回显时必须使用脚本返回的字段值**:
364
- > - `resolved_weekday` / `resolved_date_from_weekday` / `resolved_date_to_weekday` 必须直接复用,**禁止** LLM 根据日期口算星期(评测里出现过 2026-06-15 被口算成「Sunday」、2026-06-12 被口算成「Thursday」的错误)。
365
- > - `anchor_weekday` 是 today 真实星期,回显「Today is …」时也必须用脚本返回值,不要自行口算。
366
- >
367
- > **多日 fan-out 时的边界检查**:
368
- > - 解析得到 `resolved_date_from` / `resolved_date_to` 后,先逐日列出 `YYYYMMDD` 列表再调接口;列表必须严格落在区间内、不可漏头漏尾。
369
- > - 周末过滤只在 query 显式包含 `weekdays / workdays / 工作日` 时执行(用脚本返回的 weekday 字段过滤,不要自行口算)。
370
- > - 多日 fan-out 前必须先回显「Today is …. The range is X (Mon) ~ Y (Sun), 7 days in total. Proceeding.」,让用户能即时纠错。
371
-
372
-
373
- ### 日期解析三层兜底(必须严格遵守)
374
-
375
- > [!IMPORTANT]
376
- > **任何形式的相对/绝对日期表达,都必须先调用 `resolve_date.py`,禁止 LLM 自行推算日期或星期几**。
377
- > 历史测评显示模型在"本周三/上周五/last Wednesday"这类表达上经常算错星期或日期,必须用脚本兜底。
378
-
379
- ```
380
- Level 1: 调用 resolve_date.py 脚本(确定性计算,优先且强制使用)
381
- ├─ success=true → 使用 resolved_date / resolved_date_from~to
382
- │ ├─ type=single 且 is_future=true:
383
- │ │ ├─ 表达式属于"显式未来"(明天/下周X/下月X号/明年/N天后…) → 拒绝
384
- │ │ └─ 表达式属于"周内/月内某天恰好未来"(本周X/本月N号/今年X月X日) → 正常查询
385
- │ └─ type=range 且 partially_in_future=true:使用 --clamp-to-anchor 重新解析
386
- └─ success=false → 进入 Level 2
387
-
388
- Level 2: Agent 基于 ISO 8601 规则语义推导(周一=第一天)
389
- ├─ 有信心推出日期 → 使用推理结果,但仍需向用户在回复中明确日期与星期,便于纠错
390
- └─ 无信心 → 进入 Level 3
391
-
392
- Level 3: 主动追问用户"请给出具体日期(如 2026-06-13)"
393
- ```
394
-
395
- > [!IMPORTANT]
396
- > **范围/整周/整月等"区间"意图**,必须用脚本返回的区间,并加 `--clamp-to-anchor` 把结束日期裁剪到 `Now`:
397
- >
398
- > ```bash
399
- > python3 <skill_path>/scripts/resolve_date.py \
400
- > --expr "本周" --anchor "<YYYY-MM-DD>" --clamp-to-anchor
401
- > # → type=range, resolved_date_from=周一, resolved_date_to=今天
402
- > ```
403
-
404
- #### 脚本支持的常见表达(已覆盖测评常见 case)
405
-
406
- - 中文相对:`今天/明天/昨天/前天/后天/大前天/大后天/三天前/两周后/N个月前/N个月后`
407
- - 中文周:`本周X / 这周X / 这礼拜X / 礼拜X / 周3 / 上周X / 下周X / 上上周X / 下下周X / 上2周一 / 下3周一`
408
- - 中文月日:`6月13号 / 5月份 / 今年5月 / 去年12月 / 2025年12月 / 月初 / 月末 / 月底 / 上月底 / 上月初 / 下月初`
409
- - 中文范围:`本周 / 上周 / 本月 / 上个月 / 今年 / 去年 / 上半年 / 下半年 / 6月 / 上周一到上周五`
410
- - 英文相对天:`today / tomorrow / yesterday / day before yesterday / day after tomorrow / 3 days ago / two days ago / in 2 days / 5 weeks ago / two months later`
411
- - 英文周:`Monday / last Wednesday / this Friday / next Monday`
412
- - 英文范围:`this week / last week / next week / this month / last month / next month / this year / last year / next year / first half / second half / H1 / H2`
413
- - 英文月日:`May / June / Jan / May 5 / May 5th / May 5, 2026 / 5 May / 5th May 2026 / Jan 31 / Dec 25 2025`
414
- - 绝对:`2026-06-13 / 2026.6.13 / 2026/6/13 / 6.13 / 6-13 / 2026年6月13日 / 20260613`
415
-
416
- > [!IMPORTANT]
417
- > **英文环境下务必把表达式原样喂给 `resolve_date.py`,不要 LLM 自己心算**。脚本已支持 `last week / this month / 3 days ago / May 5 / May 5, 2026 / first half` 等常见英文表达;若脚本返回 `success=false`,再走 Level 2 / Level 3。
418
-
419
- ## 核心场景
420
-
421
- ### 1. 查询出勤提报记录(单日 / 多日聚合)
422
- **绝对优先使用 [`submission`](references/people-attendance-submission.md)**(`time_type=1`)。
423
-
424
- > [!NOTE]
425
- > 接口仅支持**单日**查询(`date` 必须 `YYYYMMDD` 字符串,例如 `"20260408"`)。
426
- > 「本周/上周/本月/上月/最近两周/某周N天」这类范围需求,必须先把范围解析为日期列表,逐日 `submission.get` 后在内存中聚合。
427
- > 当区间 ≥ 一整月时,**优先**用 `query_month_summary` 兜底(一次调用即可),不要硬刷 30 次单日,容易触发 99991400 限流。
428
-
429
- 场景示例:
430
- - "今天/昨天/前天/三天前/5 天前的出勤记录" → 单日 `submission.get`
431
- - "5 月 15 号那天我出勤了多少小时" / "what was my attendance on June 2nd 2026?" → 单日 `submission.get`
432
- - "本周/上周/这周到现在出勤了多久" → 推理日期范围后**逐日**调用 `submission.get`,再聚合 `duration`
433
- - "上周每天的出勤时长 / 哪几天 duration > 8 / 不到 8 小时" → 逐日 `submission.get`,按 `rows[].column.duration` 过滤
434
- - "上周 calculation_tag 包含 Regular / Normal Working Hours / 1.5x Daily Salary / Overtime Pay 的记录" → 逐日 `submission.get` 后按 `rows[].column.calculation_tag` 过滤/汇总
435
- - "上周/今天审批中 / 已拒绝 / 已通过 / 更正审批中的出勤" → 逐日 `submission.get` 后按 `rows[].column.status` 过滤
436
- - "本月第三个工作日 / 月初第一个工作日的出勤" → 排除周六周日推算工作日,再单日 `submission.get`
437
- - "下周一 / 明天 / 下个月 3 号的出勤" → **未来日期,直接拒绝**,不调用 CLI
438
- - "帮我提交今天 8 小时 / submit my attendance for today" / "补提交昨天 7.5 小时" → **不支持写操作**,直接告知用户当前 skill 仅支持查询,不能提交/修改/撤回/删除
439
- - "帮我查张三 / 工号 E12345 / 我下属的出勤" → **不支持**,直接告知「只支持查询当前登录账号本人的出勤提报记录」
440
- - "导出/删除/更新出勤记录" → **不支持**
441
-
442
- ### 2. 出勤月度汇总
443
- **绝对优先使用 [`submission.query_month_summary`](references/people-attendance-submission.md)**。
444
-
445
- 只支持**单月**汇总;多月对比 / 跨年累计 / 半年汇总需逐月调用后聚合。
446
-
447
- > [!IMPORTANT]
448
- > **接口数据口径(必须主动告知用户,不可省略)**:`submission.query_month_summary` 返回的月度汇总**包含「审批中」状态的记录**(含「审批中 / 撤销审批中 / 更正审批中」等所有审批中变体),即数值是已通过加上各类审批中的合计。
449
- >
450
- > **强制行为**:
451
- > - **每次**调用本接口给用户回报结果时,**必须**在数据后**显式声明**该口径——告知用户「数值已包含审批中的部分」,不要让用户误以为是仅"已通过"。
452
- > - 用户问「本月已通过的出勤」「approved attendance for this month」等**严格只看某一个 status** 的诉求时,**不能**直接拿本接口的结果当成"已通过"返回;要么明确告知"此数值已包含审批中",要么改用 `submission.get` 逐日 fan-out + 按 `status` 严格过滤后求和。
453
- > - 用户问「本月总共出勤多少」「How many attendance hours this month」等**通用**月度统计 → **默认**走 `query_month_summary`(更高效,避免 30 次 fan-out 限流),**但回复中必须带上口径说明**。
454
- >
455
- > **回复模板(必带口径说明,语种镜像)**:
456
- > - 中文回复尾部追加:
457
- > ```
458
- > 说明:本月度汇总包含「审批中」状态的记录(含撤销审批中、更正审批中等),即数值为已通过与各类审批中的合计。
459
- > ```
460
- > - 英文回复尾部追加:
461
- > ```
462
- > Note: This monthly summary includes records with the Approving / Pending status (covering Approving, Withdraw approving, Pending correction, etc.). The total is the sum of Approved plus all in-approval records.
463
- > ```
464
-
465
- 场景示例:
466
- - "我这个月出勤了多长时间" → 单次 `query_month_summary` (本月);回复必须附上「数值已包含审批中」的说明
467
- - "上个月的出勤情况" / "May 的出勤统计" / "今年 1 月份出勤统计" → 单次 `query_month_summary`;附口径说明
468
- - "去年 12 月我出勤了多少小时" → 注意跨年,`month=202512`;附口径说明
469
- - "4 月和 5 月的出勤总时长分别是多少" → 分别调用 `query_month_summary` 后比较;每月数据都附口径说明
470
- - "本月已通过的出勤汇总" / "上月已拒绝的出勤" / "更正审批中的出勤" → **不要**直接用 `query_month_summary` 当作答案(该数值还含审批中),改逐日 `submission.get` 后按 status 过滤
471
- - "How many attendance hours did I have last month?" → 单次 `query_month_summary`;英文回复中必须带 `Note: This monthly summary includes records with the Approving / Pending status ...`
472
-
473
- ## Few-shot 示例(直接照抄,避免试错)
474
-
475
- > 当前日期 = 2026-06-10(示例参考,实际以系统当前日期为准)
476
-
477
- ```bash
478
- # A. 本月累计出勤 → 一次调用解决
479
- people-cli attendance submission query_month_summary \
480
- --params '{"month":"202606","accept_language":"zh-CN"}' --as user
481
-
482
- # B. 上个月累计出勤
483
- people-cli attendance submission query_month_summary \
484
- --params '{"month":"202605","accept_language":"zh-CN"}' --as user
485
-
486
- # C. 去年 12 月(跨年)
487
- people-cli attendance submission query_month_summary \
488
- --params '{"month":"202512","accept_language":"zh-CN"}' --as user
489
-
490
- # D. 查指定一天的出勤详情
491
- people-cli attendance submission get \
492
- --params '{"date":"20260609","time_type":"1","accept_language":"zh-CN"}' --as user
493
-
494
- # E. 查上周累计出勤 → 逐日调用后求和(注意 time_type=1 必填)
495
- for d in 20260601 20260602 20260603 20260604 20260605; do
496
- people-cli attendance submission get \
497
- --params "{\"date\":\"$d\",\"time_type\":\"1\",\"accept_language\":\"zh-CN\"}" --as user
498
- sleep 1 # 串行+小退避,避免触发 99991400 限流
499
- done
500
- # 在内存中按 rows[].column.duration 求和;按 calculation_tag / status 过滤
501
-
502
- # F. 英文环境:accept_language 必须传 "en-US"(不是 en / en_US / en-us / en_us)
503
- # 用户问:"How many hours did I work this month?" → resolve_date 解析后逐日 / 月度汇总
504
- people-cli attendance submission query_month_summary \
505
- --params '{"month":"202606","accept_language":"en-US"}' --as user
506
-
507
- # G. 英文环境:last week 的逐日记录(accept_language="en-US" → 返回英文 status / calculation_tag)
508
- for d in 20260615 20260616 20260617 20260618 20260619 20260620 20260621; do
509
- people-cli attendance submission get \
510
- --params "{\"date\":\"$d\",\"time_type\":\"1\",\"accept_language\":\"en-US\"}" --as user
511
- sleep 1
512
- done
513
-
514
- # H. 英文环境:日期表达直接交给 resolve_date.py(不要 LLM 心算)
515
- python3 <skill_path>/scripts/resolve_date.py --expr "last week" --anchor "2026-06-22"
516
- # → {"success": true, "type": "range", "resolved_date_from": "2026-06-15", "resolved_date_to": "2026-06-21", ...}
517
- python3 <skill_path>/scripts/resolve_date.py --expr "May 5, 2026" --anchor "2026-06-22"
518
- # → {"success": true, "type": "single", "resolved_date": "2026-05-05", ...}
519
- ```
520
-
521
- ## 限流(99991400)退避重试策略(必须按此执行)
522
-
523
- > **背景**:评测发现多日 fan-out 时(如本月 6.1 ~ 6.16)会高概率触发 `99991400`,导致大量"中段日期"数据缺失,最终给用户的汇总只算到很少的几天,严重失真。**必须按下面的退避重试策略执行**,不要碰到一次限流就整体放弃或裁剪范围。
524
-
525
- ### 1. 调用前预防(首选)
526
-
527
- - **整月累计** → **优先**用 `query_month_summary` 一次拿走,不要逐日 fan-out 30 次。
528
- - **多日 fan-out** → 串行执行,每次调用之间 `sleep ≥ 1s`;当跨度 ≥ 7 天时建议 `sleep 1.5s`;跨度 ≥ 15 天必须先评估是否能用 `query_month_summary` 兜底。
529
- - **禁止并行**:同一次范围查询绝对不能并发 fan-out,并发会立即触发 99991400。
530
-
531
- ### 2. 命中 `99991400` 的退避重试(强制)
532
-
533
- 每次调用后必须先用 jq 检查响应顶层 `.code`:若 `code == 99991400`,按下表退避重试:
534
-
535
- | 第 N 次失败 | 等待时长 | 备注 |
536
- |-------------|---------|------|
537
- | 1 | `sleep 2s` | 单日重试 1 次 |
538
- | 2 | `sleep 4s` | 单日重试 2 次 |
539
- | 3 | `sleep 8s` | 单日重试 3 次(最后一次,仍失败再放弃该日) |
540
-
541
- > ⚠️ **重要变更**:以前文档建议"最多重试 2 次",实测仍然会大段缺失数据。现强制升级为**最多 3 次指数退避(2s→4s→8s)**,并且**所有命中限流的日期必须全部走完这 3 次重试**,不允许只重试第一个失败日就放弃后续。
542
-
543
- ### 3. 多日聚合的限流恢复
544
-
545
- - 多日 fan-out 时维护一份 `failed_dates` 列表;每个 `99991400` 当场退避重试 3 次后仍失败的日期才入列。
546
- - 主流程跑完后,若 `failed_dates` 非空:**必须**额外做一轮"补抓"——`sleep 5s` 后串行重跑这些日期一次(间隔 ≥ 2s)。
547
- - 补抓仍失败的日期才允许标记为「数据缺失」。
548
-
549
- ### 4. 限流命中后的回报口径(绝对不能当作 0 或空数据)
550
-
551
- - **禁止**把 `code == 99991400` 的日期按 0 小时合并求和,或回复"无记录"。
552
- - 多日聚合最终回复必须显式标注:
553
- - 「以下日期数据未取回(限流,已重试 3 次仍失败):YYYY-MM-DD ×N」
554
- - 「已成功取回的日期累计:duration=X 小时(不含上述缺失日期)」
555
- - 单日查询命中限流后回复:「查询 `<日期>` 时触发了出勤接口限流(99991400),已重试 3 次仍未取回,请稍后再试」。
556
-
557
- ### 5. Few-shot:本月累计 fan-out 的安全脚手架
558
-
559
- ```bash
560
- # 本月 1 日到今天逐日抓取,包含限流退避
561
- fail_list=()
562
- for d in 20260601 20260602 20260603 ... 20260617; do
563
- for attempt in 1 2 3; do
564
- resp=$(people-cli attendance submission get \
565
- --params "{\"date\":\"$d\",\"time_type\":\"1\",\"accept_language\":\"zh-CN\"}" --as user)
566
- code=$(echo "$resp" | jq -r '.code // 0')
567
- if [ "$code" != "99991400" ]; then break; fi
568
- sleep $((2 ** attempt)) # 2s -> 4s -> 8s
569
- done
570
- if [ "$code" = "99991400" ]; then fail_list+=("$d"); fi
571
- sleep 1.5
572
- done
573
- # 补抓
574
- sleep 5
575
- for d in "${fail_list[@]}"; do
576
- people-cli attendance submission get --params "{\"date\":\"$d\",\"time_type\":\"1\"}" --as user
577
- sleep 2
578
- done
579
- ```
580
-
581
- ## 异常意图必须进入 skill 拒绝(严禁绕开)
582
-
583
- > **背景**:评测中出现过「帮我提交今天出勤25个小时」这类**写操作 + 超时长**异常意图被模型直接绕开 skill 自由作答的情况。**所有出勤相关意图都必须进入本 skill**,由 skill 的能力边界统一拒绝。
584
-
585
- | 异常意图示例 | 处理 |
586
- |--------------|------|
587
- | 「帮我提交/补提交今天出勤 X 小时」「submit my attendance for today」 | 进入 skill → 直接回复「当前 skill 仅支持查询出勤记录,不支持提交、补提交、修改、撤回或删除」;**禁止** LLM 在 skill 外自行回应 |
588
- | 「帮我提交今天出勤 25 小时 / -3 小时 / 0 小时」(超时长 / 零 / 负值) | 进入 skill → 先告知不支持写操作,**不要**先就时长合法性给建议;写操作能力边界优先 |
589
- | 「撤回 / 取消 / 修改 / 删除 / 导出出勤记录」 | 进入 skill → 直接拒绝 |
590
- | 「帮我查张三 / 工号 E12345 / 我下属的出勤」 | 进入 skill → 直接告知「只支持查询当前登录账号本人」 |
591
- | 「明天 / 下周 X / 下个月 X 号的出勤」 | 进入 skill → Step 0 解析出未来日期后由 skill 统一拒绝 |
592
- | 「我的出勤够不够抵扣迟到 / 加班补贴怎么算」 | 进入 skill → 拒绝联动场景 |
593
-
594
- > 关键原则:**只要 query 出现「出勤 / attendance / 上班时长 / 出勤汇总 / 提交出勤」等关键字,就必须进入本 skill**。即使最终结论是"不支持",也由 skill 来给固定话术,不允许 LLM 在 skill 外自由发挥。
595
-
596
- ## 关键约束(绝对不允许放宽)
597
-
598
- - **只支持查询当前登录账号本人**;不支持通过工号/employee_id/open_id/user_id 等查询他人。
599
- - **只读**;不支持提交、补提交、修改、撤回、删除、导出出勤记录。任何写操作意图直接拒绝。
600
- - **不支持未来日期**;当解析后的日期 > 当前日期,直接告知「不支持查询未来日期的出勤记录」。
601
- - **日期合法性校验**;非法日期(如 2 月 30 日、13 月)直接拒绝。
602
- - **`time_type=1` 代表出勤提报记录**;查加班记录请改用 `people-overtime` skill。
603
- - **日期/月份格式严格**:`date` 必须 `YYYYMMDD` 字符串(如 `"20260408"`,不是 `2026-04-08`、不是 整数 `20260408`);`month` 必须 `YYYYMM` 字符串(如 `"202605"`,不是 `2026-05`)。
604
- - **`accept_language` 取值严格枚举(强制规则,不可放宽)**:`submission.get` / `submission.query_month_summary` 仅接受 `"zh-CN"` 与 `"en-US"` **两个字面值**(区分大小写、必须中划线、地区码必须为 `CN` / `US`)。**禁止**传 `en` / `en_US` / `en-us` / `en_us` / `english` / `EN-US` / `zh` / `zh_CN` / `cn` 等任何变体——服务端会**默默回退到中文**输出,导致英文用户看到中文 `display_name`、中文 `status`、中文 `calculation_tag` 等字段。
605
- - **❗英文查询的硬性约束**:**只要用户用英文提问**(query 任意位置出现英文业务词如 `attendance` / `submit` / `overtime` / `hours` / `last week` / `this month` / `Regular` / `Approved` / `1.5x Hourly` / `2x Daily Salary` / `Working Day Overtime` / `Time off in Lieu` 等,或整句为英文,或 query 含纯英文日期表达 `today`/`yesterday`/`May 5`),**`accept_language` 必须传 `"en-US"`,不允许省略、不允许默认、不允许传任何变体**。即使查询对象是 SGP 员工(中文 `display_name` 是 `【SGP】常规出勤`),英文上下文也必须传 `"en-US"` 让服务端返回 `Regular Hours` / `Approved` / `Normal Working Hours` 等英文字段,禁止依赖 LLM 自行翻译中文返回。
606
- - **判定 LLM 当前对话是英文环境**(用户使用英文提问、系统语言为英文、或 query 主词为 `attendance` / `submit` / `overtime` 等英文词)→ **必须**显式传 `accept_language="en-US"`;不允许省略或随手写 `en`。
607
- - **中文环境**(用户中文提问 / query 含「出勤、加班、汇总」等中文词)→ 显式传 `accept_language="zh-CN"`;不省略以避免依赖默认值。
608
- - **混合判断口径(按优先级)**:① query 整句为英文 → `en-US`;② query 含中文主词(出勤/加班/汇总等) → `zh-CN`;③ query 含英文主词(attendance / overtime 等)但同时夹杂少量中文标签字面值(如「上周 attendance 的 1.5 倍时薪」)→ 仍按 query 整体语种判定(主体英文用 `en-US`,主体中文用 `zh-CN`);④ 无法判断时按 `zh-CN`。
609
- - 归一化映射(LLM 收到任意变体后必须先做映射再传参):
610
- | 用户/上下文 | 实际可传值 |
611
- |---|---|
612
- | `en` / `en_US` / `en-us` / `en_us` / `english` / `EN-US` | `"en-US"` |
613
- | `zh` / `zh_CN` / `zh-cn` / `cn` / `chinese` / `中文` | `"zh-CN"` |
614
- - **历史评测复现**:用户问 `"How many hours of 2x daily salary attendance do I have this month?"`,LLM 默认未传 `accept_language` 或传了 `en` → 服务端返回中文 `【SGP】2 倍日薪`,匹配过滤时还需 LLM 自行做中英映射,容易漏匹配。**必须**传 `en-US` 直接拿到 `2 Daily` / `2x Daily Salary` 的英文标签做精确匹配。
615
- - **❗英文查询 → 英文回复(语种镜像,强制规则)**:用户用英文提问 → 面向用户的最终回复(结论文字、字段标签、表格表头、状态枚举、calculation_tag、unit、reason、星期、回显日期模板等)**必须全部用英文**,**禁止**任何中文字符出现在回复中。
616
- - 字段值映射(即使 query 已传 `en-US`,仍需校对避免回退中文混入):
617
- | 中文(不允许出现在英文回复中) | 英文等价 |
618
- |---|---|
619
- | 已通过 / 审批中 / 已拒绝 / 已撤回 / 更正审批中 / 已取消 | Approved / Approving / Rejected / Withdrawn / Pending correction / Cancelled |
620
- | 小时 / 天 / 半天 / 半小时 | Hour / Day / Half day / Half hour |
621
- | 出勤 / 加班 / 出勤记录 / 月度汇总 | Attendance / Overtime / Attendance record / Monthly summary |
622
- | 星期一 / 星期二 / … / 星期日 | Monday / Tuesday / … / Sunday |
623
- | 【SGP】常规出勤 / 【SGP】1 倍日薪 / 【SGP】1.5 倍时薪 / 【SGP】2 倍日薪 / 【SGP】调休假 | Regular Hours / 1 Daily / 1.5 Hourly / 2 Daily / Time off in Lieu (Overtime) |
624
- | 无加班记录 / 无出勤记录 / 无记录 | No overtime records / No attendance records / No records |
625
- - **回退保护**:服务端因任何原因仍返回中文字段(极少数 corner case)→ LLM **必须**手动按上表映射成英文后再展示,不允许把中文原文直接抛给用户。
626
- - **回显模板(英文)**:
627
- - 单日:`Today is {YYYY-MM-DD} ({weekday}). The date you're asking about is {resolved_date} ({resolved_weekday}). Proceed.`
628
- - 范围:`Today is {YYYY-MM-DD} ({weekday}). The range you're asking about is {date_from} ({from_weekday}) ~ {date_to} ({to_weekday}), {N} days in total. Proceed.`
629
- - 拒绝(未来):`Future dates are not supported for attendance queries.`
630
- - 拒绝(写操作):`This skill only supports querying attendance records. Submitting / amending / withdrawing / deleting attendance records is not supported.`
631
- - 拒绝(他人):`Only the current logged-in user's attendance records are supported.`
632
- - 空结果:`No attendance records for {date or range}.`(**禁止**追加 `(duration=0, unit=hour)` 之类的零值结构化尾巴)
633
- - **混合 query 时**:query 整句英文 / 主词为英文 → 全英文回复;query 主词中文 → 全中文回复;混合时按整句主体语种判定。
634
- - **限流码 99991400 必须按 3 次指数退避(2s→4s→8s)逐日重试**,并在主流程结束后对剩余失败日期做一轮 `sleep 5s` 后的补抓;详见上方「限流(99991400)退避重试策略」章节。**严禁**:① 把 99991400 当成「无记录 / 0 小时」合并求和;② 命中一次就整体放弃后续日期;③ 隐式裁剪查询范围回避限流。多日聚合时若部分日期最终仍失败,必须显式标注「数据缺失(限流,已重试 3 次)」。
635
- - **`上周` / `last week` 默认 7 天 fan-out**(包含上周六、上周日),只有用户明确说「工作日 / weekdays」时才裁剪到周一~周五;同理「本周」必须包含到 `Now` 当日(哪怕 `Now` 是周六/周日)。
636
- - **所有出勤相关异常意图必须进入本 skill 处理**(写操作、查他人、未来日期、联动诉求等),由 skill 的能力边界统一拒绝;**禁止** LLM 在 skill 外自由作答。
637
- - **面向用户的回复中禁止暴露 skill 名称及切换提示**:不得出现 `people-attendance` / `people-overtime` 等 skill 名称,也不得出现「将切到 …」「改用 … skill」「切换到 … skill」「如需查加班请切换到 people-overtime」之类话术。skill 之间的路由是内部行为,对用户透明执行即可;如确需澄清意图,使用业务措辞反问(例如「您想查询的是出勤记录还是加班记录?」),不要提及 skill 标识。
638
-
639
- ## 防误用:出勤 vs 加班(必须先判定)
640
-
641
- > [!IMPORTANT]
642
- > **路由判定只看「业务主词」,不看 calculation_tag 字面值**。
643
- > 评测发现高频误判:用户问「本月按 calculation_tag=1.5x Daily Salary 汇总**出勤**」「SGP 本月 2x Daily Salary **出勤**汇总」「查询 Overtime Pay 标签且更正审批中的**出勤记录**」时,模型仅凭 query 出现了 `1.5x` / `2x` / `Overtime Pay` 等英文标签,就直接判定为"加班"而切到 `people-overtime`,导致回复跑偏。**严禁此类反推**。
644
-
645
- 在调用任何 CLI 之前,先做一次"数据域"判定:
646
-
647
- 1. **明确出勤**(本 skill 可处理):
648
- - 业务主词:`出勤` / `attendance` / `上班时长` / `出勤提报` / `出勤汇总` / `出勤记录` / `迟到/早退/缺勤`(仅查询记录,不做规则推断)
649
- - **即使** query 同时出现 `1.5x Daily Salary` / `2x Daily Salary` / `Overtime Pay` / `Working Day Overtime` / `Time off in Lieu` / `Saturday OT` / `Sunday OT` 等 calculation_tag 字面值,**仍属于本 skill**——这些是出勤提报记录里的过滤条件,不是加班路由信号。
650
- - 调用:`people-cli attendance submission get/query_month_summary` 且 **必须** `time_type="1"`,再按 `rows[].column.calculation_tag` 过滤聚合。
651
- 2. **明确加班**(本 skill 不处理):
652
- - 业务主词:`加班` / `overtime`(独立词,**不**包含 `Working Day Overtime` / `Overtime Pay` 这类标签内嵌的 overtime) / `OT` / `补加班` / `加班审批` / `调休` / `加班费` / `发起加班` / `申请加班`
653
- - 处理:直接提示用户切换 `people-overtime`(不要尝试在本 skill 内用 `time_type=2` 查)
654
- 3. **含糊不清**(如只问"我今天工时/时长",或仅给标签字面值无任何业务主词):
655
- - 默认按"出勤"处理,并在回复里明确:本次查询的是**出勤提报(time_type=1)**;如用户要查加班请说明。
656
- - **禁止**默认按"加班"处理。
657
-
658
- ## 命令速查
659
-
660
- | 需求 | 推荐命令 |
661
- |------|---------|
662
- | 查指定日期的出勤记录(出勤提报) | `people-cli attendance submission get` (`time_type=1`) |
663
- | 查指定月份的出勤累计汇总 | `people-cli attendance submission query_month_summary` |
664
-
665
- ## 参考文档
666
-
667
- - [people-attendance-submission.md](references/people-attendance-submission.md) — 出勤提报记录 / 月度汇总查询(含字段映射、错误码、jq 模板)