weaver-work-cli 0.1.7 → 0.1.20

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 (264) hide show
  1. package/README.md +11 -11
  2. package/dist/cmd/action.js +76 -0
  3. package/dist/cmd/index.js +2 -0
  4. package/dist/cmd/refs/index.js +103 -0
  5. package/dist/cmd/register.js +31 -0
  6. package/dist/cmd/root.js +1 -0
  7. package/dist/cmd/setup/index.js +25 -5
  8. package/dist/cmd/skills/index.js +37 -5
  9. package/dist/core/connector.js +54 -0
  10. package/dist/core/context.js +2 -1
  11. package/dist/core/input.js +48 -0
  12. package/dist/core/output.js +298 -3
  13. package/dist/core/refs.js +432 -0
  14. package/dist/core/state.js +26 -0
  15. package/dist/internal/e10/auth/commands.js +2 -2
  16. package/dist/internal/e10/auth/session.js +14 -0
  17. package/dist/internal/e10/auth/xiaoe.js +116 -51
  18. package/dist/internal/e10/request-runtime.js +5 -0
  19. package/dist/internal/skills/install.js +148 -28
  20. package/dist/internal/skills/reader.js +12 -9
  21. package/dist/shortcuts/blog/continuation.js +74 -0
  22. package/dist/shortcuts/blog/errors.js +60 -0
  23. package/dist/shortcuts/blog/host.js +175 -0
  24. package/dist/shortcuts/blog/index.js +324 -0
  25. package/dist/shortcuts/blog/manifest.js +415 -0
  26. package/dist/shortcuts/blog/operations/comment.js +125 -0
  27. package/dist/shortcuts/blog/operations/comments.js +31 -0
  28. package/dist/shortcuts/blog/operations/edit.js +131 -0
  29. package/dist/shortcuts/blog/operations/export.js +46 -0
  30. package/dist/shortcuts/blog/operations/like.js +80 -0
  31. package/dist/shortcuts/blog/operations/publish.js +157 -0
  32. package/dist/shortcuts/blog/operations/query-user.js +58 -0
  33. package/dist/shortcuts/blog/operations/query.js +96 -0
  34. package/dist/shortcuts/blog/operations/registry.js +54 -0
  35. package/dist/shortcuts/blog/operations/remind.js +176 -0
  36. package/dist/shortcuts/blog/operations/shared.js +330 -0
  37. package/dist/shortcuts/blog/operations/types.js +16 -0
  38. package/dist/shortcuts/blog/operations.js +9 -0
  39. package/dist/shortcuts/caizhilian/continuation.js +70 -0
  40. package/dist/shortcuts/caizhilian/errors.js +53 -0
  41. package/dist/shortcuts/caizhilian/host.js +190 -0
  42. package/dist/shortcuts/caizhilian/index.js +186 -0
  43. package/dist/shortcuts/caizhilian/manifest.js +42 -0
  44. package/dist/shortcuts/caizhilian/operations/api.js +801 -0
  45. package/dist/shortcuts/caizhilian/operations/confirm.js +75 -0
  46. package/dist/shortcuts/caizhilian/operations/files.js +306 -0
  47. package/dist/shortcuts/caizhilian/operations/form.js +170 -0
  48. package/dist/shortcuts/caizhilian/operations/handlers/collab.js +611 -0
  49. package/dist/shortcuts/caizhilian/operations/handlers/document.js +890 -0
  50. package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +346 -0
  51. package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +192 -0
  52. package/dist/shortcuts/caizhilian/operations/handlers/fileops.js +570 -0
  53. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +969 -0
  54. package/dist/shortcuts/caizhilian/operations/handlers/search.js +318 -0
  55. package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +294 -0
  56. package/dist/shortcuts/caizhilian/operations/handlers/setting-backup.js +423 -0
  57. package/dist/shortcuts/caizhilian/operations/handlers/setting-encrypt.js +238 -0
  58. package/dist/shortcuts/caizhilian/operations/handlers/setting-storage.js +364 -0
  59. package/dist/shortcuts/caizhilian/operations/handlers/system.js +106 -0
  60. package/dist/shortcuts/caizhilian/operations/registry.js +151 -0
  61. package/dist/shortcuts/caizhilian/operations/shared.js +408 -0
  62. package/dist/shortcuts/caizhilian/operations/specs.js +623 -0
  63. package/dist/shortcuts/caizhilian/operations/types.js +57 -0
  64. package/dist/shortcuts/caizhilian/operations.js +21 -0
  65. package/dist/shortcuts/contract/continuation.js +70 -0
  66. package/dist/shortcuts/contract/errors.js +62 -0
  67. package/dist/shortcuts/contract/host.js +205 -0
  68. package/dist/shortcuts/contract/index.js +318 -0
  69. package/dist/shortcuts/contract/manifest.js +596 -0
  70. package/dist/shortcuts/contract/operations/clause-reference.js +78 -0
  71. package/dist/shortcuts/contract/operations/form-info.js +371 -0
  72. package/dist/shortcuts/contract/operations/read.js +118 -0
  73. package/dist/shortcuts/contract/operations/registry.js +43 -0
  74. package/dist/shortcuts/contract/operations/request.js +300 -0
  75. package/dist/shortcuts/contract/operations/shared.js +133 -0
  76. package/dist/shortcuts/contract/operations/subject.js +157 -0
  77. package/dist/shortcuts/contract/operations/template.js +81 -0
  78. package/dist/shortcuts/contract/operations/types.js +7 -0
  79. package/dist/shortcuts/contract/operations/workflow.js +52 -0
  80. package/dist/shortcuts/contract/operations/write.js +131 -0
  81. package/dist/shortcuts/contract/operations.js +8 -0
  82. package/dist/shortcuts/hrm/index.js +83 -2
  83. package/dist/shortcuts/hrm/operations/employee.js +160 -30
  84. package/dist/shortcuts/hrm/operations/org.js +17 -3
  85. package/dist/shortcuts/hrm/operations/position.js +18 -2
  86. package/dist/shortcuts/index.js +5 -1
  87. package/dist/shortcuts/invoice/index.js +74 -36
  88. package/dist/shortcuts/invoice/manifest.js +674 -4
  89. package/dist/shortcuts/invoice/markdown.js +172 -0
  90. package/dist/shortcuts/invoice/operations/add.js +3 -3
  91. package/dist/shortcuts/invoice/operations/delete.js +2 -2
  92. package/dist/shortcuts/invoice/operations/get.js +10 -2
  93. package/dist/shortcuts/invoice/operations/import.js +2 -2
  94. package/dist/shortcuts/invoice/operations/registry.js +8 -1
  95. package/dist/shortcuts/invoice/operations/reim-auto.js +719 -0
  96. package/dist/shortcuts/invoice/operations/reim.js +85 -2
  97. package/dist/shortcuts/invoice/operations/shared.js +75 -8
  98. package/dist/shortcuts/invoice/operations/update.js +2 -2
  99. package/dist/shortcuts/invoice/operations/upload-and-validate.js +93 -0
  100. package/dist/shortcuts/invoice/operations/validate-preview.js +2 -1
  101. package/dist/shortcuts/invoice/projection.js +328 -0
  102. package/dist/shortcuts/meeting/host.js +18 -1
  103. package/dist/shortcuts/meeting/manifest.js +30 -18
  104. package/dist/shortcuts/meeting/operations/eb/catalog.js +390 -0
  105. package/dist/shortcuts/meeting/operations/eb/conflict.js +389 -0
  106. package/dist/shortcuts/meeting/operations/eb/factory.js +389 -0
  107. package/dist/shortcuts/meeting/operations/eb/index.js +22 -0
  108. package/dist/shortcuts/meeting/operations/env.js +8 -1
  109. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  110. package/dist/shortcuts/meeting/operations/signs.js +216 -2
  111. package/dist/shortcuts/workflow/endpoints.js +2 -1
  112. package/dist/shortcuts/workflow/errors.js +8 -2
  113. package/dist/shortcuts/workflow/host.js +6 -3
  114. package/dist/shortcuts/workflow/operations/form.js +931 -533
  115. package/dist/shortcuts/workflow/operations/read.js +462 -81
  116. package/dist/shortcuts/workflow/operations/shared.js +44 -0
  117. package/dist/shortcuts/workflow/operations/write.js +760 -56
  118. package/dist/shortcuts/workflow/operations.js +36 -10
  119. package/docs/_catalog.md +6 -0
  120. package/docs/agent-invoice.md +5 -1
  121. package/docs/agent-io-channel-design.md +468 -0
  122. package/docs/agent-skill-install.md +9 -4
  123. package/docs/blog.md +123 -0
  124. package/docs/caizhilian.md +304 -0
  125. package/docs/contract.md +91 -0
  126. package/docs/e10-auth.md +2 -2
  127. package/docs/hrm.md +19 -9
  128. package/docs/invoice.md +5 -1
  129. package/docs/meeting.md +1 -1
  130. package/docs/operation-manual.md +15 -3
  131. package/docs/skill-review-2026-09-19.md +2 -2
  132. package/package.json +2 -2
  133. package/scripts/package-skill.mjs +33 -20
  134. package/skill-template/business-info.json +113 -14
  135. package/skill-template/domains/blog.md +24 -0
  136. package/skill-template/domains/caizhilian.md +22 -0
  137. package/skill-template/domains/jinchengda.md +23 -0
  138. package/skill-template/domains/meeting.md +1 -1
  139. package/skill-template/domains/workflow.md +1 -1
  140. package/skill-template/skill-template.md +1 -1
  141. package/skills/weaver-e10-blog/SKILL.md +111 -0
  142. package/skills/weaver-e10-blog/references/blog-comment.md +51 -0
  143. package/skills/weaver-e10-blog/references/blog-comments.md +39 -0
  144. package/skills/weaver-e10-blog/references/blog-default-permission.md +38 -0
  145. package/skills/weaver-e10-blog/references/blog-disabled-capabilities.md +11 -0
  146. package/skills/weaver-e10-blog/references/blog-edit.md +51 -0
  147. package/skills/weaver-e10-blog/references/blog-export.md +42 -0
  148. package/skills/weaver-e10-blog/references/blog-like.md +50 -0
  149. package/skills/weaver-e10-blog/references/blog-publish.md +52 -0
  150. package/skills/weaver-e10-blog/references/blog-query-user.md +45 -0
  151. package/skills/weaver-e10-blog/references/blog-query.md +49 -0
  152. package/skills/weaver-e10-blog/references/blog-remind.md +51 -0
  153. package/skills/weaver-e10-blog/references/blog-resolve-users.md +38 -0
  154. package/skills/weaver-e10-blog/references/source-manifest.json +1540 -0
  155. package/skills/weaver-e10-caizhilian/SKILL.md +216 -0
  156. package/skills/weaver-e10-caizhilian/references/agent-entry.md +106 -0
  157. package/skills/weaver-e10-caizhilian/references/collaboration.md +112 -0
  158. package/skills/weaver-e10-caizhilian/references/document.md +111 -0
  159. package/skills/weaver-e10-caizhilian/references/file-collection.md +136 -0
  160. package/skills/weaver-e10-caizhilian/references/file-upload.md +309 -0
  161. package/skills/weaver-e10-caizhilian/references/files.md +120 -0
  162. package/skills/weaver-e10-caizhilian/references/folder.md +118 -0
  163. package/skills/weaver-e10-caizhilian/references/org-browser.md +104 -0
  164. package/skills/weaver-e10-caizhilian/references/setting-attach.md +410 -0
  165. package/skills/weaver-e10-caizhilian/references/setting-backup.md +372 -0
  166. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +399 -0
  167. package/skills/weaver-e10-caizhilian/references/setting-storage.md +603 -0
  168. package/skills/weaver-e10-caizhilian/references/source-manifest.json +5079 -0
  169. package/skills/weaver-e10-caizhilian/references/withheld.md +93 -0
  170. package/skills/weaver-e10-caizhilian/references/write-protocol.md +120 -0
  171. package/skills/weaver-e10-calendar/SKILL.md +4 -3
  172. package/skills/weaver-e10-ebuilder-form/SKILL.md +4 -9
  173. package/skills/weaver-e10-ebuilder-form/references/context-routing.md +0 -9
  174. package/skills/weaver-e10-ebuilder-form/references/custom-api.md +1 -5
  175. package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +1 -5
  176. package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +1 -5
  177. package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +1 -5
  178. package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +1 -5
  179. package/skills/weaver-e10-esb/SKILL.md +2 -2
  180. package/skills/weaver-e10-esb/references/source-manifest.json +9 -9
  181. package/skills/weaver-e10-hrm/SKILL.md +54 -6
  182. package/skills/weaver-e10-hrm/references/employee.md +89 -15
  183. package/skills/weaver-e10-hrm/references/org.md +7 -4
  184. package/skills/weaver-e10-hrm/references/position.md +14 -7
  185. package/skills/weaver-e10-hrm/references/source-manifest.json +1 -1
  186. package/skills/weaver-e10-jinchengda/SKILL.md +166 -0
  187. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +82 -0
  188. package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +69 -0
  189. package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +78 -0
  190. package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +27 -0
  191. package/skills/weaver-e10-jinchengda/references/contract-request-status.md +71 -0
  192. package/skills/weaver-e10-jinchengda/references/contract-request-write.md +94 -0
  193. package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +89 -0
  194. package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +77 -0
  195. package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +66 -0
  196. package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +94 -0
  197. package/skills/weaver-e10-jinchengda/references/source-manifest.json +259 -0
  198. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -11
  199. package/skills/weaver-e10-jucailin/SKILL.md +4 -3
  200. package/skills/weaver-e10-mail/SKILL.md +4 -11
  201. package/skills/weaver-e10-meeting/SKILL.md +66 -32
  202. package/skills/weaver-e10-meeting/references/meeting-change.md +5 -2
  203. package/skills/weaver-e10-meeting/references/meeting-create.md +2 -1
  204. package/skills/weaver-e10-meeting/references/meeting-eb.md +114 -0
  205. package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +9 -8
  206. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +35 -7
  207. package/skills/weaver-e10-meeting/references/meeting-update.md +1 -1
  208. package/skills/weaver-e10-okr/SKILL.md +6 -11
  209. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  210. package/skills/weaver-e10-plan/SKILL.md +3 -3
  211. package/skills/weaver-e10-plan/references/plan-collect-and-compose.md +1 -1
  212. package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +1 -1
  213. package/skills/weaver-e10-plan/references/source-manifest.json +13 -13
  214. package/skills/weaver-e10-qiyecheng/SKILL.md +4 -10
  215. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +9 -9
  216. package/skills/weaver-e10-shared/SKILL.md +36 -25
  217. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +10 -31
  218. package/skills/weaver-e10-shared/references/json-output-contract.md +20 -11
  219. package/skills/weaver-e10-shijingran/SKILL.md +2 -1
  220. package/skills/weaver-e10-shijingran/references/source-manifest.json +9 -9
  221. package/skills/weaver-e10-skill-maker/SKILL.md +4 -10
  222. package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +5 -3
  223. package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +12 -27
  224. package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +17 -5
  225. package/skills/weaver-e10-skill-maker/references/source-update-detection.md +4 -27
  226. package/skills/weaver-e10-wenshuding/SKILL.md +5 -16
  227. package/skills/weaver-e10-wenshuding/references/source-manifest.json +7 -7
  228. package/skills/weaver-e10-workflow/SKILL.md +32 -37
  229. package/skills/weaver-e10-workflow/references/source-manifest.json +40 -77
  230. package/skills/weaver-e10-workflow/references/workflow-batch.md +90 -29
  231. package/skills/weaver-e10-workflow/references/workflow-create.md +79 -109
  232. package/skills/weaver-e10-workflow/references/workflow-memory.md +19 -9
  233. package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -2
  234. package/skills/weaver-e10-workflow/references/workflow-query.md +37 -20
  235. package/skills/weaver-e10-workflow/references/workflow-share.md +18 -2
  236. package/skills/weaver-e10-workflow/references/workflow-view.md +4 -4
  237. package/skills/weaver-e10-yepiaotong/SKILL.md +75 -39
  238. package/skills/weaver-e10-yepiaotong/references/invoice-detail.md +22 -1
  239. package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +3 -1
  240. package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +5 -5
  241. package/skills/weaver-e10-yepiaotong/references/invoice-output-view.md +135 -0
  242. package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +15 -3
  243. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +1 -1
  244. package/skills/weaver-e10-yepiaotong/references/invoice-summarize.py +217 -0
  245. package/skills/weaver-e10-yepiaotong/references/invoice-upload-and-validate.md +88 -0
  246. package/skills/weaver-e10-yepiaotong/references/invoice-validation-preview.md +7 -2
  247. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +90 -10
  248. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +1 -0
  249. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +70 -49
  250. package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +119 -0
  251. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +55 -36
  252. package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +4 -0
  253. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +149 -0
  254. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +125 -5
  255. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +430 -63
  256. package/skills/weaver-e10-yimiaoban/SKILL.md +4 -12
  257. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
  258. package/skills/weaver-e10-ziguanjia/SKILL.md +2 -1
  259. package/skills/weaver-e10-ziguanjia/references/ziguanjia-agent-entry.md +3 -3
  260. package/tools/weaver-skill-detector-1.0.13/SKILL.md +9 -9
  261. package/tools/weaver-skill-detector-1.0.13/references/checklist.md +3 -3
  262. package/tools/weaver-skill-detector-1.0.13/scripts/__pycache__/validate_skill.cpython-313.pyc +0 -0
  263. package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +98 -91
  264. package/scripts/sync-connector-skills.mjs +0 -172
@@ -0,0 +1,176 @@
1
+ import { blogConfirmationError, blogPartialError, blogValidationError, isBlogNetworkError } from '../errors.js';
2
+ import { asDate, asInteger, objectFingerprint, remindContent, remindYear, searchUsersByName, sessionMatches, } from './shared.js';
3
+ import { workflow } from './types.js';
4
+ const MAX_TARGETS = 50;
5
+ /** user_ids(数组或逗号分隔字符串)与 names(经 HRM 解析)二选一 */
6
+ async function resolveRemindTargets(input, host) {
7
+ if (input.user_ids !== undefined && input.names !== undefined) {
8
+ throw blogValidationError('input_ambiguous', 'user_ids 与 names 只能提供一个');
9
+ }
10
+ if (input.user_ids !== undefined) {
11
+ const raw = typeof input.user_ids === 'string'
12
+ ? input.user_ids.split(',').map((item) => item.trim()).filter(Boolean)
13
+ : input.user_ids;
14
+ if (!Array.isArray(raw) || raw.length === 0) {
15
+ throw blogValidationError('input_invalid', 'user_ids 必须是非空数组或逗号分隔字符串');
16
+ }
17
+ if (raw.length > MAX_TARGETS)
18
+ throw blogValidationError('input_invalid', `user_ids 单次最多 ${MAX_TARGETS} 人`);
19
+ return raw.map((item, index) => {
20
+ const id = String(item);
21
+ if (!/^[1-9]\d*$/u.test(id)) {
22
+ throw blogValidationError('input_invalid', `user_ids[${index}] 必须是正整数文本`);
23
+ }
24
+ return { user_id: id, name: null };
25
+ });
26
+ }
27
+ if (input.names !== undefined) {
28
+ const raw = input.names;
29
+ if (!Array.isArray(raw) || raw.length === 0) {
30
+ throw blogValidationError('input_invalid', 'names 必须是非空字符串数组');
31
+ }
32
+ if (raw.length > MAX_TARGETS)
33
+ throw blogValidationError('input_invalid', `names 单次最多 ${MAX_TARGETS} 人`);
34
+ const targets = [];
35
+ for (const item of raw) {
36
+ const name = typeof item === 'string' ? item.trim() : '';
37
+ if (!name)
38
+ throw blogValidationError('input_invalid', 'names 不能包含空值');
39
+ const users = await searchUsersByName(host, name);
40
+ if (users.length === 0)
41
+ throw blogValidationError('name_not_found', `未找到名为「${name}」的员工`);
42
+ if (users.length > 1) {
43
+ throw blogValidationError('name_ambiguous', `「${name}」命中 ${users.length} 名员工,请改用 user_ids 指定`, { candidates: users });
44
+ }
45
+ targets.push({ user_id: users[0].id, name: users[0].name });
46
+ }
47
+ return targets;
48
+ }
49
+ throw blogValidationError('input_invalid', 'user_ids 与 names 必须提供一个');
50
+ }
51
+ /** 自定义文案自检:必须出现「日」,避免提醒指向错误周期;缺省时使用默认文案 */
52
+ function normalizeCustomContent(content, date) {
53
+ if (content === undefined || content === null)
54
+ return undefined;
55
+ const text = typeof content === 'string' ? content.trim() : '';
56
+ if (!text)
57
+ return undefined;
58
+ if (!/日/u.test(text)) {
59
+ throw blogValidationError('input_invalid', `自定义提醒文案应包含具体日期,例如「${remindContent(date)}」;或省略 content 使用默认文案`);
60
+ }
61
+ return text;
62
+ }
63
+ /** 提醒 prepare:解析被提醒人 + 生成默认文案 + 签发 continuation(不发送) */
64
+ export class BlogRemindPrepareOperation {
65
+ name = 'blog.remind.prepare';
66
+ async execute(input, { host, continuation }) {
67
+ const date = asDate(input.date, 'date');
68
+ const targets = await resolveRemindTargets(input, host);
69
+ const custom = normalizeCustomContent(input.content, date);
70
+ const content = custom ?? remindContent(date);
71
+ const year = asInteger(input.year, 'year', { min: 2000, max: 2100, dflt: remindYear(date) });
72
+ const session = await host.getSession();
73
+ const continuationToken = await continuation.issue({
74
+ operation: 'blog.remind.apply',
75
+ workflow: workflow('blog.remind', 'AWAITING_CONFIRMATION'),
76
+ baseUrl: session.baseUrl,
77
+ profile: session.profile,
78
+ userId: session.userId,
79
+ date,
80
+ content,
81
+ year,
82
+ targets,
83
+ fingerprint: objectFingerprint({ date, content, year, targets }),
84
+ });
85
+ return {
86
+ status: 'AWAITING_CONFIRMATION',
87
+ data: {
88
+ targets: targets.map((target) => ({ user_id: target.user_id, name: target.name })),
89
+ date,
90
+ content,
91
+ year,
92
+ continuation: continuationToken,
93
+ expires_in_seconds: 600,
94
+ workflow: workflow('blog.remind', 'AWAITING_CONFIRMATION'),
95
+ next: '向用户复述被提醒人与文案,用户确认后调用 blog.remind.apply 并传 confirm=true',
96
+ },
97
+ };
98
+ }
99
+ }
100
+ /** 提醒 apply:逐人 POST sendUrge;部分失败返回 PARTIAL,中断返回 partial/write_uncertain */
101
+ export class BlogRemindApplyOperation {
102
+ name = 'blog.remind.apply';
103
+ async execute(input, { host, continuation }) {
104
+ if (input.confirm !== true) {
105
+ throw blogConfirmationError('blog.remind.apply', { hint: '重新调用时传入 confirm=true' });
106
+ }
107
+ const lease = await continuation.verify(input.continuation, 'blog.remind.apply');
108
+ if (!Array.isArray(lease.targets) || lease.targets.length === 0 || typeof lease.content !== 'string') {
109
+ throw blogValidationError('continuation_invalid', '提醒 continuation 缺少有效目标或文案');
110
+ }
111
+ const session = await host.getSession();
112
+ if (!sessionMatches(lease, session)) {
113
+ throw blogValidationError('context_mismatch', '确认上下文已变化,请重新准备提醒');
114
+ }
115
+ if (objectFingerprint({ date: lease.date, content: lease.content, year: lease.year, targets: lease.targets }) !== lease.fingerprint) {
116
+ throw blogValidationError('continuation_invalid', '提醒 continuation 指纹校验失败');
117
+ }
118
+ const succeeded = [];
119
+ const failed = [];
120
+ // 多人逐个 POST;已发送后中断不自动重试,返回 partial
121
+ for (const target of lease.targets) {
122
+ try {
123
+ await host.blogRequest({
124
+ path: '/api/blog/message/sendUrge',
125
+ body: {
126
+ checkStr: '',
127
+ content: lease.content,
128
+ definiteType: 'blog',
129
+ ids: target.user_id,
130
+ targetId: null,
131
+ type: 'blog',
132
+ year: lease.year,
133
+ },
134
+ operation: 'blog.remind.apply',
135
+ });
136
+ succeeded.push({ user_id: target.user_id, name: target.name });
137
+ }
138
+ catch (error) {
139
+ failed.push({
140
+ user_id: target.user_id,
141
+ name: target.name,
142
+ message: error instanceof Error ? error.message : String(error),
143
+ });
144
+ if (isBlogNetworkError(error)) {
145
+ throw blogPartialError('提醒发送过程中连接中断,结果不确定,禁止自动重试', {
146
+ succeeded,
147
+ failed,
148
+ workflow: workflow('blog.remind', 'EXECUTING'),
149
+ }, { stage: 'urge' });
150
+ }
151
+ }
152
+ }
153
+ if (failed.length > 0) {
154
+ return {
155
+ status: 'PARTIAL',
156
+ data: {
157
+ content: lease.content,
158
+ date: lease.date,
159
+ succeeded,
160
+ failed,
161
+ workflow: workflow('blog.remind', failed.length === lease.targets.length ? 'FAILED' : 'PARTIAL_COMPLETE'),
162
+ },
163
+ };
164
+ }
165
+ return {
166
+ status: 'COMPLETE',
167
+ data: {
168
+ content: lease.content,
169
+ date: lease.date,
170
+ succeeded,
171
+ failed,
172
+ workflow: workflow('blog.remind', 'COMPLETE'),
173
+ },
174
+ };
175
+ }
176
+ }
@@ -0,0 +1,330 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { stat, writeFile } from 'node:fs/promises';
4
+ import { dirname, resolve } from 'node:path';
5
+ import { blogValidationError } from '../errors.js';
6
+ const DATE_PATTERN = /^\d{4}-\d{2}-\d{2}$/u;
7
+ /** 单人日报逐日扫描(detailsByDate)的区间上限,防止一次调用打爆接口 */
8
+ export const MAX_RANGE_DAYS = 62;
9
+ const FORBIDDEN_INPUT_KEYS = new Set([
10
+ 'access_token',
11
+ 'app_key',
12
+ 'app_secret',
13
+ 'cookie',
14
+ 'eteamsid',
15
+ 'token',
16
+ 'authorization',
17
+ ]);
18
+ const FORBIDDEN_NORMALIZED_KEYS = new Set([...FORBIDDEN_INPUT_KEYS].map((key) => key.toLowerCase()));
19
+ export const USER_TYPES = ['directSubordinate', 'indirectSubordinate', 'allSubordinate', 'myFollow'];
20
+ export function asObject(value, name = '业务输入') {
21
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
22
+ throw blogValidationError('input_invalid', `${name}必须是 JSON 对象`);
23
+ }
24
+ return value;
25
+ }
26
+ export function asNonEmptyString(value, name) {
27
+ if (typeof value !== 'string' || !value.trim()) {
28
+ throw blogValidationError('input_invalid', `${name} 必须是非空字符串`);
29
+ }
30
+ return value.trim();
31
+ }
32
+ export function asPositiveString(value, name) {
33
+ if (typeof value !== 'string' || !/^\d+$/u.test(value) || value === '0') {
34
+ throw blogValidationError('input_invalid', `${name} 必须是正整数文本`);
35
+ }
36
+ return value;
37
+ }
38
+ export function asInteger(value, name, options) {
39
+ const { min, max, dflt } = options;
40
+ if (value === undefined || value === null || value === '') {
41
+ if (dflt !== undefined)
42
+ return dflt;
43
+ throw blogValidationError('input_invalid', `${name} 必须是整数`);
44
+ }
45
+ const parsed = typeof value === 'number' ? value : Number(value);
46
+ if (!Number.isInteger(parsed) || parsed < min || parsed > max) {
47
+ throw blogValidationError('input_invalid', `${name} 必须是 ${min}~${max} 的整数`);
48
+ }
49
+ return parsed;
50
+ }
51
+ export function asDate(value, name) {
52
+ const text = typeof value === 'string' ? value.trim() : '';
53
+ if (!DATE_PATTERN.test(text) || Number.isNaN(Date.parse(`${text}T00:00:00+08:00`))) {
54
+ throw blogValidationError('input_invalid', `${name} 必须是 YYYY-MM-DD 格式日期`);
55
+ }
56
+ return text;
57
+ }
58
+ export function normalizeUserType(value) {
59
+ if (typeof value !== 'string' || !USER_TYPES.includes(value.trim())) {
60
+ throw blogValidationError('input_invalid', `user_type 必须是 ${USER_TYPES.join(' | ')} 之一`);
61
+ }
62
+ return value.trim();
63
+ }
64
+ /** 认证凭据禁止出现在业务输入中(CLI 全权托管 Cookie / ETEAMSID / Token) */
65
+ export function assertNoForbiddenKeys(value, location = 'input') {
66
+ if (!value || typeof value !== 'object')
67
+ return;
68
+ if (Array.isArray(value)) {
69
+ value.forEach((item, index) => assertNoForbiddenKeys(item, `${location}[${index}]`));
70
+ return;
71
+ }
72
+ for (const [key, child] of Object.entries(value)) {
73
+ if (FORBIDDEN_INPUT_KEYS.has(key) || FORBIDDEN_NORMALIZED_KEYS.has(key.toLowerCase())) {
74
+ throw blogValidationError('field_forbidden', `禁止业务输入字段:${location}.${key}`);
75
+ }
76
+ assertNoForbiddenKeys(child, `${location}.${key}`);
77
+ }
78
+ }
79
+ function pad(n) {
80
+ return String(n).padStart(2, '0');
81
+ }
82
+ /** E10 日报日期是本地时区 0 点的 Unix 毫秒;必须按本地时区格式化,用 toISOString 会差一天 */
83
+ export function localDateStr(ms) {
84
+ const d = new Date(ms);
85
+ return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`;
86
+ }
87
+ export function localDateTimeStr(ms) {
88
+ const d = new Date(ms);
89
+ return `${localDateStr(ms)} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
90
+ }
91
+ export function dateRange(startInput, endInput, startName = 'start_date', endName = 'end_date') {
92
+ const start = asDate(startInput, startName);
93
+ const end = asDate(endInput, endName);
94
+ const days = [];
95
+ for (let d = new Date(`${start}T00:00:00`); d <= new Date(`${end}T00:00:00`); d.setDate(d.getDate() + 1)) {
96
+ days.push(`${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}`);
97
+ }
98
+ if (days.length === 0)
99
+ throw blogValidationError('input_invalid', `${startName} 不能晚于 ${endName}`);
100
+ if (days.length > MAX_RANGE_DAYS) {
101
+ throw blogValidationError('range_too_large', `日期区间共 ${days.length} 天,超过 ${MAX_RANGE_DAYS} 天上限;请缩小范围后重试`);
102
+ }
103
+ return days;
104
+ }
105
+ /** 提醒默认文案(固定以「请填写」开头,日期与 date 入参一致) */
106
+ export function remindContent(date) {
107
+ const d = new Date(`${date}T00:00:00`);
108
+ return `请填写 ${d.getFullYear()}年 ${d.getMonth() + 1}月 ${d.getDate()}日 的日报!`;
109
+ }
110
+ export function remindYear(date) {
111
+ return new Date(`${date}T00:00:00`).getFullYear();
112
+ }
113
+ export function htmlToText(html) {
114
+ return html.replace(/<[^>]+>/g, '').trim();
115
+ }
116
+ /** 纯文本自动包裹 <p>;HTML 原样保留 */
117
+ export function ensureHtml(content) {
118
+ return /^\s*</u.test(content) ? content : `<p>${content}</p>`;
119
+ }
120
+ function canonicalJson(value) {
121
+ if (Array.isArray(value))
122
+ return `[${value.map(canonicalJson).join(',')}]`;
123
+ if (value && typeof value === 'object') {
124
+ return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(',')}}`;
125
+ }
126
+ return JSON.stringify(value);
127
+ }
128
+ /** 业务对象指纹:canonical JSON 的 sha256,用于 prepare/apply 之间的目标一致性校验 */
129
+ export function objectFingerprint(value) {
130
+ return createHash('sha256').update(canonicalJson(value)).digest('hex');
131
+ }
132
+ export function sessionMatches(lease, session) {
133
+ return lease.baseUrl === session.baseUrl
134
+ && (lease.profile ?? '') === (session.profile ?? '')
135
+ && lease.userId === session.userId;
136
+ }
137
+ /** detailsByDate 原始响应无 found 字段,且无日报时返回空壳 blog 对象 —— 判定看 blog.id */
138
+ export function detailsFound(value) {
139
+ return Boolean(value?.blog && typeof value.blog === 'object' && value.blog.id);
140
+ }
141
+ /** 补交判定统一用 createTime vs blogTime 是否同日(blogs 接口的 later 字段不可靠) */
142
+ export function submitStatus(blog) {
143
+ if (blog.create_time === null || blog.blog_time === null)
144
+ return '补交';
145
+ return localDateStr(blog.create_time) === localDateStr(blog.blog_time) ? '正常' : '补交';
146
+ }
147
+ export function normalizeBlog(raw) {
148
+ if (!raw || typeof raw !== 'object' || !raw.id)
149
+ return null;
150
+ const createTime = typeof raw.createTime === 'number' ? raw.createTime : null;
151
+ const blogTime = typeof raw.blogTime === 'number' ? raw.blogTime : null;
152
+ const content = typeof raw.content === 'string' ? raw.content : '';
153
+ return {
154
+ id: String(raw.id),
155
+ create_time: createTime,
156
+ create_time_str: createTime === null ? null : localDateTimeStr(createTime),
157
+ blog_time: blogTime,
158
+ blog_time_str: blogTime === null ? null : localDateStr(blogTime),
159
+ content,
160
+ content_text: htmlToText(content),
161
+ comment_count: typeof raw.commentCount === 'number' ? raw.commentCount : null,
162
+ praise_count: typeof raw.praiseCount === 'number' ? raw.praiseCount : null,
163
+ later: typeof raw.later === 'number' ? raw.later : null,
164
+ };
165
+ }
166
+ /** blogs 接口返回每人每天一行;未提交的日子是空占位行(blog 为空/无 id) */
167
+ export function normalizeBlogListItem(item) {
168
+ if (!item || typeof item !== 'object')
169
+ return null;
170
+ const date = typeof item.date === 'number' ? item.date : Number(item.date);
171
+ if (!Number.isFinite(date))
172
+ return null;
173
+ const userId = String(item.user?.id ?? '');
174
+ const username = String(item.user?.username ?? item.user?.name ?? '');
175
+ const blog = normalizeBlog(item.blog);
176
+ return {
177
+ date,
178
+ date_str: localDateStr(date),
179
+ user_id: userId,
180
+ username,
181
+ blog,
182
+ submitted: Boolean(blog),
183
+ submit_status: blog ? submitStatus(blog) : '未提交',
184
+ };
185
+ }
186
+ /**
187
+ * HRM 人员搜索(body 为实测必填模板,缺字段会返回 0 行)。
188
+ * 关键:结果取 `id` 字段,不是 `userid` —— E10 业务接口对 `userid` 全部报 unknowUser。
189
+ */
190
+ export async function searchUsersByName(host, name) {
191
+ const body = {
192
+ browserMultiple: true,
193
+ authWecahtBook: false,
194
+ wxDepNeedConvertCount: 0,
195
+ wxEmpNeedConvertCount: 0,
196
+ wxTotalNeedConvertCount: 0,
197
+ cmd: 'all',
198
+ current: 1,
199
+ pageSize: 30,
200
+ formDatas: {
201
+ username: [{ usernameOpt: 'eq', usernameInput: name }],
202
+ status: [{ statusSelect: ['normal'] }],
203
+ subcompany: [{ subcompanyOpt: 'in' }],
204
+ department: [{ departmentOpt: 'in' }],
205
+ role: [{ roleOpt: 'in' }],
206
+ position: [{ positionOpt: 'in' }],
207
+ superior: [{ superiorOpt: 'in' }],
208
+ },
209
+ quickSearchDatas: {},
210
+ type: 'and',
211
+ containsNoneAccount: false,
212
+ containsPartTimeEmployee: false,
213
+ };
214
+ const countResp = await host.blogRequest({ path: '/api/hrm/common/browser/count/resource', body, operation: 'blog.resolve_users' });
215
+ const total = countResp?.data?.total ?? 0;
216
+ if (!total)
217
+ return [];
218
+ const dataResp = await host.blogRequest({ path: '/api/hrm/common/browser/data/resource', body, operation: 'blog.resolve_users' });
219
+ const rows = Array.isArray(dataResp?.data?.data) ? dataResp.data.data : [];
220
+ return rows
221
+ .map((row) => ({
222
+ id: String(row.id ?? ''),
223
+ name: String(row.name ?? row.username ?? ''),
224
+ department: String(row.department ?? ''),
225
+ }))
226
+ .filter((user) => user.id);
227
+ }
228
+ /** 按姓名解析唯一 userId;同名多命中抛错并列出候选 */
229
+ export async function resolveUserId(input, host) {
230
+ if (input.user_id !== undefined && input.name !== undefined) {
231
+ throw blogValidationError('input_ambiguous', 'user_id 与 name 只能提供一个');
232
+ }
233
+ if (input.user_id !== undefined) {
234
+ return { userId: asPositiveString(input.user_id, 'user_id'), userName: null };
235
+ }
236
+ if (input.name !== undefined) {
237
+ const name = asNonEmptyString(input.name, 'name');
238
+ const users = await searchUsersByName(host, name);
239
+ if (users.length === 0) {
240
+ throw blogValidationError('name_not_found', `未找到名为「${name}」的员工`);
241
+ }
242
+ if (users.length > 1) {
243
+ throw blogValidationError('name_ambiguous', `「${name}」命中 ${users.length} 名员工,请改用 user_id 指定`, { candidates: users });
244
+ }
245
+ return { userId: users[0].id, userName: users[0].name };
246
+ }
247
+ throw blogValidationError('input_invalid', 'user_id 与 name 必须提供一个');
248
+ }
249
+ /** 目标人员解析:只传 date 不传 user_id/name 时默认当前登录人 */
250
+ export async function resolveTargetUser(input, host) {
251
+ if (input.user_id === undefined && input.name === undefined) {
252
+ const session = await host.getSession();
253
+ return { userId: session.userId, userName: null };
254
+ }
255
+ return resolveUserId(input, host);
256
+ }
257
+ /** 查某人某天日报(detailsByDate);found=false 表示当天无日报 */
258
+ export async function fetchDetailsByDate(host, date, userId) {
259
+ const value = await host.blogRequest({
260
+ method: 'GET',
261
+ path: '/api/blog/prior/detailsByDate',
262
+ query: { date, userId },
263
+ operation: 'blog.query_user',
264
+ });
265
+ const found = detailsFound(value);
266
+ return { found, raw: value, blog: found ? normalizeBlog(value.blog) : null };
267
+ }
268
+ /** 目标日报定位:blog_id 直传,或 date + user_id/name 经 detailsByDate 解析 */
269
+ export async function resolveTargetBlog(input, host, action) {
270
+ if (input.blog_id !== undefined && (input.date !== undefined || input.user_id !== undefined || input.name !== undefined)) {
271
+ throw blogValidationError('input_ambiguous', 'blog_id 与 date/user_id/name 只能提供一个');
272
+ }
273
+ if (input.blog_id !== undefined) {
274
+ return { blogId: asPositiveString(input.blog_id, 'blog_id'), blog: null, date: null, userId: null };
275
+ }
276
+ const date = asDate(input.date, 'date');
277
+ const { userId, userName } = await resolveTargetUser(input, host);
278
+ const detail = await fetchDetailsByDate(host, date, userId);
279
+ if (!detail.found || !detail.blog) {
280
+ throw blogValidationError('target_missing', `对应日期 ${date} 人员 ${userName ?? userId} 无日报,无法${action}`);
281
+ }
282
+ return { blogId: detail.blog.id, blog: detail.blog, date, userId };
283
+ }
284
+ export function normalizeIdList(value, name) {
285
+ const raw = typeof value === 'string'
286
+ ? value.split(',').map((item) => item.trim()).filter(Boolean)
287
+ : value;
288
+ if (!Array.isArray(raw) || raw.length === 0) {
289
+ throw blogValidationError('input_invalid', `${name} 必须是非空数组或逗号分隔字符串`);
290
+ }
291
+ if (raw.length > 50)
292
+ throw blogValidationError('input_invalid', `${name} 单次最多支持 50 项`);
293
+ const ids = raw.map((item, index) => asPositiveString(String(item), `${name}[${index}]`));
294
+ if (new Set(ids).size !== ids.length)
295
+ throw blogValidationError('input_invalid', `${name} 不能包含重复 ID`);
296
+ return ids;
297
+ }
298
+ /** 导出落点校验:父目录必须存在,且拒绝覆盖已存在文件 */
299
+ export async function resolveExportOutput(output) {
300
+ if (typeof output !== 'string' || !output.trim()) {
301
+ throw blogValidationError('output_required', 'output 必须是明确的本地文件路径');
302
+ }
303
+ const resolved = resolve(output);
304
+ let parent;
305
+ try {
306
+ parent = await stat(dirname(resolved));
307
+ }
308
+ catch {
309
+ throw blogValidationError('output_parent_missing', `输出目录不存在:${dirname(resolved)}`);
310
+ }
311
+ if (!parent.isDirectory()) {
312
+ throw blogValidationError('output_parent_invalid', '输出文件的父路径不是目录');
313
+ }
314
+ if (existsSync(resolved)) {
315
+ throw blogValidationError('output_exists', `输出文件已存在,拒绝覆盖:${resolved}`);
316
+ }
317
+ return resolved;
318
+ }
319
+ /** 以 wx 写入,避免 check-then-write 竞态覆盖用户文件 */
320
+ export async function writeExportFile(resolved, content) {
321
+ try {
322
+ await writeFile(resolved, content, { flag: 'wx', mode: 0o600 });
323
+ }
324
+ catch (error) {
325
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') {
326
+ throw blogValidationError('output_exists', `输出文件已存在,拒绝覆盖:${resolved}`);
327
+ }
328
+ throw error;
329
+ }
330
+ }
@@ -0,0 +1,16 @@
1
+ export const READ = 'read';
2
+ /** 构造「对象类型 + 禁止额外字段」的 JSON Schema。 */
3
+ export function schemaOf(properties, options = {}) {
4
+ return {
5
+ type: 'object',
6
+ additionalProperties: false,
7
+ properties,
8
+ ...(options.required?.length ? { required: options.required } : {}),
9
+ ...(options.oneOf ? { oneOf: options.oneOf } : {}),
10
+ ...(options.anyOf ? { anyOf: options.anyOf } : {}),
11
+ ...(options.description ? { description: options.description } : {}),
12
+ };
13
+ }
14
+ export function workflow(name, state) {
15
+ return { name, state };
16
+ }
@@ -0,0 +1,9 @@
1
+ import { getBlogOperation } from './operations/registry.js';
2
+ import { asObject, assertNoForbiddenKeys } from './operations/shared.js';
3
+ /** `run <operation>` 统一入口:选 handler、校验输入对象与禁用字段,再交给 handler。 */
4
+ export async function executeBlogOperation(operation, rawInput, context) {
5
+ const handler = getBlogOperation(operation);
6
+ const input = asObject(rawInput);
7
+ assertNoForbiddenKeys(input);
8
+ return handler.execute(input, context);
9
+ }
@@ -0,0 +1,70 @@
1
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
2
+ import { existsSync } from 'node:fs';
3
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises';
4
+ import { homedir } from 'node:os';
5
+ import { resolve } from 'node:path';
6
+ import { caizhilianValidationError } from './errors.js';
7
+ function stateRoot(ctx) {
8
+ const configured = ctx?.config?.path();
9
+ if (configured)
10
+ return resolve(configured, '..');
11
+ return process.env.WEAVER_WORK_CLI_HOME || process.env.WORK_CLI_HOME || resolve(homedir(), '.weaver-work-cli');
12
+ }
13
+ async function loadKey(ctx) {
14
+ const keysDir = resolve(stateRoot(ctx), 'keys');
15
+ const filename = resolve(keysDir, 'caizhilian-continuation.key');
16
+ await mkdir(keysDir, { recursive: true, mode: 0o700 });
17
+ if (existsSync(filename))
18
+ return readFile(filename);
19
+ const key = randomBytes(32);
20
+ try {
21
+ await writeFile(filename, key, { flag: 'wx', mode: 0o600 });
22
+ if (process.platform !== 'win32')
23
+ await chmod(filename, 0o600).catch(() => undefined);
24
+ return key;
25
+ }
26
+ catch {
27
+ return readFile(filename);
28
+ }
29
+ }
30
+ function encode(value) {
31
+ return Buffer.from(JSON.stringify(value)).toString('base64url');
32
+ }
33
+ function signature(payload, key) {
34
+ return createHmac('sha256', key).update(payload).digest('base64url');
35
+ }
36
+ export function createCaizhilianContinuationSigner(ctx) {
37
+ return {
38
+ async issue(payload, ttlMs = 10 * 60_000) {
39
+ const issuedAt = Date.now();
40
+ const body = encode({ ...payload, issuedAt, expiresAt: issuedAt + ttlMs });
41
+ const sig = signature(body, await loadKey(ctx));
42
+ return `${body}.${sig}`;
43
+ },
44
+ async verify(token, expectedOperation) {
45
+ if (typeof token !== 'string' || !token.includes('.')) {
46
+ throw caizhilianValidationError('continuation_invalid', 'continuation 无效');
47
+ }
48
+ const [body, provided] = token.split('.');
49
+ const expected = signature(body, await loadKey(ctx));
50
+ const valid = provided.length === expected.length
51
+ && timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
52
+ if (!valid)
53
+ throw caizhilianValidationError('continuation_invalid', 'continuation 签名校验失败');
54
+ let value;
55
+ try {
56
+ value = JSON.parse(Buffer.from(body, 'base64url').toString('utf8'));
57
+ }
58
+ catch {
59
+ throw caizhilianValidationError('continuation_invalid', 'continuation 内容无法解析');
60
+ }
61
+ if (value.operation !== expectedOperation) {
62
+ throw caizhilianValidationError('continuation_mismatch', 'continuation 不属于当前操作');
63
+ }
64
+ if (!Number.isSafeInteger(value.expiresAt) || value.expiresAt < Date.now()) {
65
+ throw caizhilianValidationError('continuation_expired', 'continuation 已过期');
66
+ }
67
+ return value;
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,53 @@
1
+ import { CliError, ExitCode } from '../../core/errors.js';
2
+ export class CaizhilianCliError extends CliError {
3
+ type;
4
+ subtype;
5
+ code;
6
+ retryable;
7
+ stage;
8
+ details;
9
+ partialData;
10
+ constructor(type, subtype, message, options = {}) {
11
+ super(message, options.exitCode ?? ExitCode.InternalError);
12
+ this.name = 'CaizhilianCliError';
13
+ this.type = type;
14
+ this.subtype = subtype;
15
+ this.code = options.code;
16
+ this.retryable = options.retryable ?? false;
17
+ this.stage = options.stage;
18
+ this.details = options.details;
19
+ this.partialData = options.partialData;
20
+ }
21
+ }
22
+ export function caizhilianValidationError(subtype, message, details) {
23
+ return new CaizhilianCliError('validation', subtype, message, {
24
+ details,
25
+ exitCode: ExitCode.UsageError,
26
+ });
27
+ }
28
+ export function caizhilianAuthError(subtype, message, details) {
29
+ return new CaizhilianCliError('authentication', subtype, message, {
30
+ details,
31
+ exitCode: ExitCode.AuthError,
32
+ });
33
+ }
34
+ export function caizhilianNetworkError(subtype, message, options = {}) {
35
+ return new CaizhilianCliError('network', subtype, message, {
36
+ ...options,
37
+ retryable: options.retryable ?? true,
38
+ exitCode: ExitCode.NetworkError,
39
+ });
40
+ }
41
+ export function caizhilianConfirmationError(action, details) {
42
+ return new CaizhilianCliError('confirmation', 'required', '该操作需要明确确认', {
43
+ details: { action, ...details },
44
+ exitCode: ExitCode.ConfirmationRequired,
45
+ });
46
+ }
47
+ export function caizhilianPartialError(message, partialData, details) {
48
+ return new CaizhilianCliError('partial', 'write_uncertain', message, {
49
+ details,
50
+ partialData,
51
+ exitCode: ExitCode.Partial,
52
+ });
53
+ }