weaver-work-cli 0.1.5 → 0.1.7

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 (225) hide show
  1. package/README.md +14 -9
  2. package/dist/cmd/skills/index.js +60 -0
  3. package/dist/internal/e10/auth/commands.js +48 -32
  4. package/dist/internal/e10/auth/session.js +44 -3
  5. package/dist/internal/e10/auth/xiaoe.js +363 -11
  6. package/dist/internal/e10/context.js +4 -2
  7. package/dist/internal/skills/detector.js +42 -0
  8. package/dist/internal/skills/install.js +50 -3
  9. package/dist/shortcuts/archive/render/search-format.js +12 -3
  10. package/dist/shortcuts/ebuilder-form/continuation.js +70 -0
  11. package/dist/shortcuts/ebuilder-form/errors.js +54 -0
  12. package/dist/shortcuts/ebuilder-form/host.js +1238 -0
  13. package/dist/shortcuts/ebuilder-form/index.js +108 -0
  14. package/dist/shortcuts/ebuilder-form/manifest.js +347 -0
  15. package/dist/shortcuts/ebuilder-form/operations/read.js +157 -0
  16. package/dist/shortcuts/ebuilder-form/operations/registry.js +56 -0
  17. package/dist/shortcuts/ebuilder-form/operations/shared.js +132 -0
  18. package/dist/shortcuts/ebuilder-form/operations/types.js +19 -0
  19. package/dist/shortcuts/ebuilder-form/operations/write.js +277 -0
  20. package/dist/shortcuts/ebuilder-form/operations.js +8 -0
  21. package/dist/shortcuts/ehr/operations/salary.js +2 -140
  22. package/dist/shortcuts/im/continuation.js +70 -0
  23. package/dist/shortcuts/{yimiaoban → im}/errors.js +14 -14
  24. package/dist/shortcuts/{yimiaoban → im}/host.js +26 -26
  25. package/dist/shortcuts/{yimiaoban → im}/index.js +42 -42
  26. package/dist/shortcuts/{yimiaoban → im}/manifest.js +7 -6
  27. package/dist/shortcuts/{yimiaoban → im}/operations/file-user.js +54 -41
  28. package/dist/shortcuts/{yimiaoban → im}/operations/group-read.js +46 -44
  29. package/dist/shortcuts/{yimiaoban → im}/operations/group-write.js +113 -66
  30. package/dist/shortcuts/{yimiaoban → im}/operations/msg-sync.js +164 -141
  31. package/dist/shortcuts/{yimiaoban → im}/operations/msg-write.js +47 -47
  32. package/dist/shortcuts/{yimiaoban → im}/operations/person.js +10 -10
  33. package/dist/shortcuts/{yimiaoban → im}/operations/registry.js +10 -8
  34. package/dist/shortcuts/{yimiaoban → im}/operations/session.js +82 -83
  35. package/dist/shortcuts/im/operations/shared.js +442 -0
  36. package/dist/shortcuts/{yimiaoban → im}/operations/types.js +1 -1
  37. package/dist/shortcuts/im/operations/user-remind.js +201 -0
  38. package/dist/shortcuts/{yimiaoban → im}/operations/write-util.js +7 -7
  39. package/dist/shortcuts/{yimiaoban → im}/operations.js +3 -3
  40. package/dist/shortcuts/index.js +10 -2
  41. package/dist/shortcuts/invoice/manifest.js +113 -10
  42. package/dist/shortcuts/invoice/operations/import.js +39 -14
  43. package/dist/shortcuts/invoice/operations/reim.js +38 -18
  44. package/dist/shortcuts/invoice/operations/shared.js +35 -6
  45. package/dist/shortcuts/jiuchuanhui/manifest.js +3 -3
  46. package/dist/shortcuts/jiuchuanhui/operations/registry.js +2 -2
  47. package/dist/shortcuts/{yimiaoban → okr}/continuation.js +10 -10
  48. package/dist/shortcuts/okr/errors.js +70 -0
  49. package/dist/shortcuts/okr/host.js +169 -0
  50. package/dist/shortcuts/okr/index.js +155 -0
  51. package/dist/shortcuts/okr/manifest.js +70 -0
  52. package/dist/shortcuts/okr/operations/link.js +305 -0
  53. package/dist/shortcuts/okr/operations/read.js +481 -0
  54. package/dist/shortcuts/okr/operations/registry.js +31 -0
  55. package/dist/shortcuts/okr/operations/shared.js +381 -0
  56. package/dist/shortcuts/okr/operations/types.js +88 -0
  57. package/dist/shortcuts/okr/operations/write.js +838 -0
  58. package/dist/shortcuts/okr/operations.js +8 -0
  59. package/dist/shortcuts/project/continuation.js +70 -0
  60. package/dist/shortcuts/project/errors.js +53 -0
  61. package/dist/shortcuts/project/host.js +160 -0
  62. package/dist/shortcuts/project/index.js +106 -0
  63. package/dist/shortcuts/project/manifest.js +36 -0
  64. package/dist/shortcuts/project/operations/read.js +551 -0
  65. package/dist/shortcuts/project/operations/registry.js +31 -0
  66. package/dist/shortcuts/project/operations/shared.js +68 -0
  67. package/dist/shortcuts/project/operations/types.js +39 -0
  68. package/dist/shortcuts/project/operations/write.js +266 -0
  69. package/dist/shortcuts/project/operations.js +8 -0
  70. package/dist/shortcuts/workflow/continuation.js +508 -0
  71. package/dist/shortcuts/workflow/endpoints.js +149 -0
  72. package/dist/shortcuts/workflow/errors.js +112 -0
  73. package/dist/shortcuts/workflow/host.js +224 -0
  74. package/dist/shortcuts/workflow/index.js +116 -0
  75. package/dist/shortcuts/workflow/manifest.js +30 -0
  76. package/dist/shortcuts/workflow/operations/form.js +4159 -0
  77. package/dist/shortcuts/workflow/operations/read.js +3778 -0
  78. package/dist/shortcuts/workflow/operations/registry.js +33 -0
  79. package/dist/shortcuts/workflow/operations/shared.js +167 -0
  80. package/dist/shortcuts/workflow/operations/types.js +44 -0
  81. package/dist/shortcuts/workflow/operations/write.js +1991 -0
  82. package/dist/shortcuts/workflow/operations.js +119 -0
  83. package/dist/shortcuts/workflow/state.js +182 -0
  84. package/docs/SKILL.md +5 -3
  85. package/docs/_catalog.md +10 -1
  86. package/docs/agent-invoice.md +2 -2
  87. package/docs/agent-skill-install.md +2 -2
  88. package/docs/e10-auth.md +3 -1
  89. package/docs/ebuilder-form.md +38 -0
  90. package/docs/im.md +104 -0
  91. package/docs/invoice.md +5 -5
  92. package/docs/okr.md +122 -0
  93. package/docs/operation-manual.md +10 -7
  94. package/docs/project.md +69 -0
  95. package/docs/skill-review-2026-09-19.md +68 -0
  96. package/package.json +3 -2
  97. package/scripts/package-skill.mjs +38 -11
  98. package/scripts/sync-connector-skills.mjs +172 -0
  99. package/skill-template/business-info.json +357 -16
  100. package/skill-template/domains/ebuilder-form.md +5 -0
  101. package/skill-template/domains/okr.md +18 -0
  102. package/skill-template/domains/shijingran.md +3 -0
  103. package/skill-template/domains/skill-maker.md +4 -4
  104. package/skill-template/domains/workflow.md +22 -0
  105. package/skill-template/domains/yepiaotong.md +4 -2
  106. package/skill-template/domains/yimiaoban.md +13 -10
  107. package/skill-template/product.json +5 -0
  108. package/skill-template/skill-template.md +1 -0
  109. package/skills/weaver-e10-calendar/SKILL.md +5 -1
  110. package/skills/weaver-e10-calendar/references/calendar-query.md +7 -0
  111. package/skills/weaver-e10-calendar/references/source-manifest.json +314 -32
  112. package/skills/weaver-e10-ebuilder-form/SKILL.md +123 -0
  113. package/skills/weaver-e10-ebuilder-form/references/context-routing.md +90 -0
  114. package/skills/weaver-e10-ebuilder-form/references/custom-api.md +86 -0
  115. package/skills/weaver-e10-ebuilder-form/references/disabled-capabilities.md +50 -0
  116. package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +100 -0
  117. package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +91 -0
  118. package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +74 -0
  119. package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +86 -0
  120. package/skills/weaver-e10-ebuilder-form/references/source-manifest.json +107 -0
  121. package/skills/weaver-e10-esb/SKILL.md +1 -1
  122. package/skills/weaver-e10-esb/references/source-manifest.json +15 -14
  123. package/skills/weaver-e10-hrm/SKILL.md +1 -1
  124. package/skills/weaver-e10-hrm/references/source-manifest.json +357 -38
  125. package/skills/weaver-e10-jiuchuanhui/SKILL.md +22 -4
  126. package/skills/weaver-e10-jiuchuanhui/references/clue.md +4 -2
  127. package/skills/weaver-e10-jiuchuanhui/references/contact.md +1 -1
  128. package/skills/weaver-e10-jiuchuanhui/references/customer.md +3 -3
  129. package/skills/weaver-e10-jiuchuanhui/references/disabled-capabilities.md +1 -1
  130. package/skills/weaver-e10-jiuchuanhui/references/discovery-config.md +10 -10
  131. package/skills/weaver-e10-jiuchuanhui/references/general-helpers.md +3 -3
  132. package/skills/weaver-e10-jiuchuanhui/references/sale.md +7 -7
  133. package/skills/weaver-e10-jiuchuanhui/references/source-manifest.json +3 -3
  134. package/skills/weaver-e10-jucailin/SKILL.md +2 -2
  135. package/skills/weaver-e10-mail/SKILL.md +22 -6
  136. package/skills/weaver-e10-meeting/SKILL.md +22 -1
  137. package/skills/weaver-e10-okr/SKILL.md +139 -0
  138. package/skills/weaver-e10-okr/references/okr-align.md +59 -0
  139. package/skills/weaver-e10-okr/references/okr-keyresult.md +68 -0
  140. package/skills/weaver-e10-okr/references/okr-link-routing.md +67 -0
  141. package/skills/weaver-e10-okr/references/okr-query.md +105 -0
  142. package/skills/weaver-e10-okr/references/okr-write.md +94 -0
  143. package/skills/weaver-e10-okr/references/source-manifest.json +672 -0
  144. package/skills/weaver-e10-plan/SKILL.md +3 -3
  145. package/skills/weaver-e10-plan/references/plan-report-write.md +1 -1
  146. package/skills/weaver-e10-plan/references/source-manifest.json +20 -20
  147. package/skills/weaver-e10-qiyecheng/SKILL.md +21 -1
  148. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +17 -17
  149. package/skills/weaver-e10-shared/SKILL.md +79 -11
  150. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +41 -8
  151. package/skills/weaver-e10-shared/references/non-interactive-environment.md +78 -0
  152. package/skills/weaver-e10-shared/references/weaver-e10-installation.md +39 -0
  153. package/skills/weaver-e10-shijingran/SKILL.md +83 -0
  154. package/skills/weaver-e10-shijingran/references/field-resolution.md +105 -0
  155. package/skills/weaver-e10-shijingran/references/operations.md +154 -0
  156. package/skills/weaver-e10-shijingran/references/source-manifest.json +1380 -0
  157. package/skills/weaver-e10-shijingran/references/write-operations.md +90 -0
  158. package/skills/weaver-e10-skill-maker/SKILL.md +26 -9
  159. package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +39 -8
  160. package/skills/weaver-e10-skill-maker/references/detector-validation.md +25 -8
  161. package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +66 -5
  162. package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +86 -0
  163. package/skills/weaver-e10-skill-maker/references/source-update-detection.md +59 -5
  164. package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +16 -8
  165. package/skills/weaver-e10-wenshuding/SKILL.md +28 -4
  166. package/skills/weaver-e10-wenshuding/references/archive-search.md +14 -0
  167. package/skills/weaver-e10-wenshuding/references/source-manifest.json +27 -26
  168. package/skills/weaver-e10-workflow/SKILL.md +250 -0
  169. package/skills/weaver-e10-workflow/references/source-manifest.json +1062 -0
  170. package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +96 -0
  171. package/skills/weaver-e10-workflow/references/workflow-batch.md +275 -0
  172. package/skills/weaver-e10-workflow/references/workflow-create.md +182 -0
  173. package/skills/weaver-e10-workflow/references/workflow-edit.md +106 -0
  174. package/skills/weaver-e10-workflow/references/workflow-memory.md +165 -0
  175. package/skills/weaver-e10-workflow/references/workflow-operate.md +153 -0
  176. package/skills/weaver-e10-workflow/references/workflow-query.md +821 -0
  177. package/skills/weaver-e10-workflow/references/workflow-share.md +117 -0
  178. package/skills/weaver-e10-workflow/references/workflow-view.md +189 -0
  179. package/skills/weaver-e10-yepiaotong/SKILL.md +28 -28
  180. package/skills/weaver-e10-yepiaotong/references/invoice-disabled-capabilities.md +11 -5
  181. package/skills/weaver-e10-yepiaotong/references/invoice-import.md +14 -8
  182. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +9 -43
  183. package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +8 -0
  184. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +13 -17
  185. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +6 -7
  186. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +2 -2
  187. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +1155 -553
  188. package/skills/weaver-e10-yimiaoban/SKILL.md +56 -38
  189. package/skills/weaver-e10-yimiaoban/references/ding-write.md +8 -8
  190. package/skills/weaver-e10-yimiaoban/references/error-codes.md +16 -0
  191. package/skills/weaver-e10-yimiaoban/references/field-resolution.md +10 -10
  192. package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +12 -12
  193. package/skills/weaver-e10-yimiaoban/references/group-read.md +16 -16
  194. package/skills/weaver-e10-yimiaoban/references/group-write.md +17 -15
  195. package/skills/weaver-e10-yimiaoban/references/msg-read.md +21 -17
  196. package/skills/weaver-e10-yimiaoban/references/msg-write.md +10 -10
  197. package/skills/weaver-e10-yimiaoban/references/person.md +6 -6
  198. package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +15 -11
  199. package/skills/weaver-e10-yimiaoban/references/session-sysmsg.md +11 -10
  200. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +2164 -154
  201. package/skills/weaver-e10-yimiaoban/references/user-remind.md +75 -0
  202. package/skills/weaver-e10-ziguanjia/SKILL.md +4 -2
  203. package/skills/weaver-e10-ziguanjia/references/source-manifest.json +99 -29
  204. package/tools/weaver-skill-detector-1.0.13/SKILL.md +253 -0
  205. package/tools/weaver-skill-detector-1.0.13/_meta.json +6 -0
  206. package/tools/weaver-skill-detector-1.0.13/references/checklist.md +298 -0
  207. package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +4190 -0
  208. package/dist/shortcuts/yimiaoban/operations/shared.js +0 -236
  209. package/docs/yimiaoban.md +0 -86
  210. package/skills/weaver-e10-calendar/product.json +0 -8
  211. package/skills/weaver-e10-esb/product.json +0 -8
  212. package/skills/weaver-e10-hrm/product.json +0 -8
  213. package/skills/weaver-e10-jiuchuanhui/product.json +0 -8
  214. package/skills/weaver-e10-jucailin/product.json +0 -8
  215. package/skills/weaver-e10-mail/product.json +0 -8
  216. package/skills/weaver-e10-meeting/product.json +0 -8
  217. package/skills/weaver-e10-plan/product.json +0 -8
  218. package/skills/weaver-e10-qiyecheng/product.json +0 -8
  219. package/skills/weaver-e10-skill-maker/product.json +0 -8
  220. package/skills/weaver-e10-wenshuding/product.json +0 -8
  221. package/skills/weaver-e10-yepiaotong/product.json +0 -8
  222. package/skills/weaver-e10-yepiaotong/references/invoice-red.md +0 -261
  223. package/skills/weaver-e10-yimiaoban/product.json +0 -8
  224. package/skills/weaver-e10-ziguanjia/product.json +0 -8
  225. /package/dist/shortcuts/{yimiaoban → im}/operations/msg-parse.js +0 -0
@@ -0,0 +1,1991 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { WorkflowCliError, workflowConfirmationError, workflowPartialError, workflowValidationError, } from '../errors.js';
3
+ import { continuationReplayError } from '../continuation.js';
4
+ import { rethrowAuthOrNetworkError } from './read.js';
5
+ import { CONTINUATION_INPUT, SCHEMA_CATEGORY, SCHEMA_CONFIRM, SCHEMA_INDEX, SCHEMA_REQUEST_ID, schemaOf, workflowSuccess, } from './types.js';
6
+ import { asInt, asString, asStringArray, compact, responseData, toArray, resolveMemoryAppId } from './shared.js';
7
+ import { WORKFLOW_ENDPOINTS } from '../endpoints.js';
8
+ import { findIndexedItem, loadSearchIndex, } from '../state.js';
9
+ // ===========================================================================
10
+ // 工作流写操作(operate / share / batch / memory)
11
+ //
12
+ // 移植自 scripts/e10_workflow.py。所有成对的 prepare/apply 严格遵循
13
+ // 「prepare 只解析/取数/校验/预览,apply 校验 continuation 后才写」的确认链;
14
+ // **批量类写操作(batchSubmit / batchRead / batchSupervise / batchShare / batchForward /
15
+ // batchReject)是单命令**:一次调用内完成门禁校验、名称解析与写入,防重复写入由
16
+ // 「确定性幂等键 + 有界保护窗口」的一次性闸门承担(见 batchIdempotencyKey / claimBatchWrite)。
17
+ // 写请求遇网络中断/超时(结果未知)一律抛 workflowPartialError,禁止自动重试。
18
+ // ===========================================================================
19
+ const CONTINUATION_TTL_SECONDS = 600;
20
+ const BATCH_SIZE_DEFAULT = 100;
21
+ const READ_REMARK_VALUES = new Set(['60', '61', '70', '71', '80', '81']);
22
+ // 共享对象类型 → addRequestShare2 的 shareType
23
+ const SHARE_TYPE_MAP = {
24
+ user: '1',
25
+ dept: '2',
26
+ subcompany: '5',
27
+ role: '10',
28
+ group: '14',
29
+ position: '1311',
30
+ all: '11',
31
+ };
32
+ // 转发接收人类型(batchForward receiveOperatorInfos 的 _entityType)
33
+ const FORWARD_RECEIVER_TYPES = new Set(['user', 'dept', 'group', 'all']);
34
+ // RESOURCE_BROWSER 名称转 ID 的额外固定参数(与 Python SYSTEM_BROWSER_COMPLETE['creator'] 一致)
35
+ const RESOURCE_BROWSER_EXTRA = {
36
+ browserMultiple: true,
37
+ authWecahtBook: false,
38
+ wxDepNeedConvertCount: 0,
39
+ wxEmpNeedConvertCount: 0,
40
+ wxTotalNeedConvertCount: 0,
41
+ };
42
+ // ---------------------------------------------------------------------------
43
+ // 通用工具
44
+ // ---------------------------------------------------------------------------
45
+ function chunkArray(items, size) {
46
+ const out = [];
47
+ const n = Math.max(1, size);
48
+ for (let i = 0; i < items.length; i += n)
49
+ out.push(items.slice(i, i + n));
50
+ return out;
51
+ }
52
+ function splitInlineId(value) {
53
+ const text = (value || '').trim();
54
+ if (!text)
55
+ return { id: '', name: null };
56
+ const match = /^(.*?)\(([^()]+)\)\s*$/.exec(text);
57
+ if (match) {
58
+ const name = match[1].trim();
59
+ const id = match[2].trim();
60
+ if (id && /^[0-9A-Za-z_\-]+$/.test(id))
61
+ return { id, name: name || null };
62
+ }
63
+ return { id: text, name: null };
64
+ }
65
+ function parseJsonObject(value) {
66
+ if (value === undefined || value === null || value === '')
67
+ return undefined;
68
+ if (typeof value === 'object')
69
+ return value;
70
+ const text = String(value).trim();
71
+ try {
72
+ const parsed = JSON.parse(text);
73
+ return parsed && typeof parsed === 'object' ? parsed : undefined;
74
+ }
75
+ catch {
76
+ throw workflowValidationError('json_invalid', `无法解析 JSON 参数:${text.slice(0, 80)}`);
77
+ }
78
+ }
79
+ /** 走浏览按钮把关键字解析成候选 [{id,name}];任何异常按 0 命中处理(保守,不抛)。 */
80
+ async function browserResolve(env, path, keyword, extra = {}) {
81
+ const kw = (keyword || '').trim();
82
+ if (!kw)
83
+ return [];
84
+ const body = compact({ browserMultiple: true, newly: 1, searchValue: kw, ...extra });
85
+ let response;
86
+ try {
87
+ response = await env.host.workflowRequest({ method: 'POST', path, operation: 'name.resolve', body });
88
+ }
89
+ catch {
90
+ return [];
91
+ }
92
+ const data = responseData(response);
93
+ const list = data?.data?.data ?? data?.data ?? data;
94
+ if (!Array.isArray(list))
95
+ return [];
96
+ const out = [];
97
+ const seen = new Set();
98
+ for (const item of list) {
99
+ if (!item || typeof item !== 'object')
100
+ continue;
101
+ const id = asString(item.id);
102
+ if (!id || seen.has(id))
103
+ continue;
104
+ seen.add(id);
105
+ out.push({ id, name: asString(item.name) ?? asString(item.content) ?? '' });
106
+ }
107
+ return out;
108
+ }
109
+ /** 把多个(逗号分隔的)接收人名称解析为已解析的 ID 列表;歧义/0 命中抛错。 */
110
+ async function resolveReceiverNames(env, receiverName) {
111
+ const parts = String(receiverName || '')
112
+ .split(',')
113
+ .map((p) => p.trim())
114
+ .filter(Boolean);
115
+ if (!parts.length)
116
+ throw workflowValidationError('receiver_required', '接收人名称不能为空');
117
+ const ids = [];
118
+ for (const part of parts) {
119
+ const inline = splitInlineId(part);
120
+ if (inline.id !== part) {
121
+ ids.push(inline.id);
122
+ continue;
123
+ }
124
+ const candidates = await browserResolve(env, WORKFLOW_ENDPOINTS.RESOURCE_BROWSER, part, RESOURCE_BROWSER_EXTRA);
125
+ if (candidates.length === 1) {
126
+ ids.push(candidates[0].id);
127
+ }
128
+ else if (candidates.length > 1) {
129
+ throw workflowValidationError('receiver_ambiguous', `接收人「${part}」匹配到多个候选,请确认后回传 "名称(ID)"`, {
130
+ candidates: candidates.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })),
131
+ });
132
+ }
133
+ else {
134
+ throw workflowValidationError('receiver_not_found', `未匹配到接收人「${part}」,请提供准确姓名或 "名称(ID)"`, {
135
+ receiverName: part,
136
+ });
137
+ }
138
+ }
139
+ return ids;
140
+ }
141
+ // ---------------------------------------------------------------------------
142
+ // 序号定位:requestId 或 group+index(来源:resolve_request_id_from_args)
143
+ // ---------------------------------------------------------------------------
144
+ function collectAllIndexItems(index) {
145
+ const flat = index.items ?? [];
146
+ const grouped = Object.values(index.groups ?? {}).flatMap((g) => g.items ?? []);
147
+ const byId = new Map();
148
+ for (const it of [...flat, ...grouped]) {
149
+ if (it && it.requestId)
150
+ byId.set(String(it.requestId), it);
151
+ }
152
+ return [...byId.values()];
153
+ }
154
+ function findIndexItemByRequestId(index, requestId) {
155
+ return collectAllIndexItems(index).find((it) => String(it.requestId) === String(requestId));
156
+ }
157
+ /** 解析出目标流程条目;显式 requestId 时优先用索引元数据,索引缺失则回退拉详情校验(对应 Python 写前门禁)。 */
158
+ async function resolveTargetItem(env, args) {
159
+ const explicit = asString(args.requestId);
160
+ const index = await loadSearchIndex(env.ctx);
161
+ let item;
162
+ if (explicit) {
163
+ const fromIndex = findIndexItemByRequestId(index, explicit);
164
+ item = fromIndex ?? (await resolveTargetViaDetail(env, explicit, asString(args.action)));
165
+ }
166
+ else {
167
+ const position = asInt(args.index);
168
+ if (position === undefined) {
169
+ throw workflowValidationError('locate_required', '必须提供 requestId 或 group+index 定位目标流程');
170
+ }
171
+ const group = asString(args.group);
172
+ const located = findIndexedItem(index, group, position);
173
+ if (!located || !located.requestId) {
174
+ throw workflowValidationError('index_stale', '序号定位失败:本地检索索引已失效或该序号不存在,请重新执行查询(workflow.search)后再操作。', { group: group ?? null, index: position });
175
+ }
176
+ item = located;
177
+ }
178
+ // 写前可操作性门禁:canSubmitRejectTransfer 不可操作时拒绝 agree/submit/reject/return/transfer/turnTodo
179
+ // (对应 Python cmd_workflow_operate 的 map_item 校验;isRemark 10/11 或 60/61/70/71/80/81 走特殊分支不拦截)
180
+ applyOperabilityGate(item, asString(args.action));
181
+ return item;
182
+ }
183
+ /** 显式 requestId 且本地索引无元数据时,拉 REQUEST_DETAIL 校验流程当前是否可操作。 */
184
+ async function resolveTargetViaDetail(env, requestId, action) {
185
+ const session = await env.host.getSession();
186
+ let detail;
187
+ try {
188
+ detail = await env.host.workflowRequest({
189
+ method: 'POST',
190
+ path: WORKFLOW_ENDPOINTS.REQUEST_DETAIL,
191
+ operation: 'request.detail',
192
+ body: { requestId, isGetRequestBaseInfo: true, isGetRequestLog: false, isGetFormData: false, userId: session.userId },
193
+ });
194
+ }
195
+ catch {
196
+ throw workflowValidationError('request_detail_unavailable', `无法获取流程 ${requestId} 详情以校验操作权限,已停止操作(请通过列表序号操作)`, { requestId });
197
+ }
198
+ const data = responseData(detail)?.data;
199
+ const mapItem = (data && typeof data === 'object' && data.mapItem && typeof data.mapItem === 'object' ? data.mapItem : undefined)
200
+ ?? (data && typeof data === 'object' ? data : {});
201
+ const isRemark = String(mapItem.isremark ?? mapItem.isRemark ?? '');
202
+ const allowed = mapItem.canSubmitRejectTransfer;
203
+ const isSpecialSubmit = ['agree', 'submit'].includes(String(action ?? '')) &&
204
+ (isRemark === '10' || isRemark === '11' || READ_REMARK_VALUES.has(isRemark));
205
+ // 门禁只在服务端明确返回 false 时拦截(来源:Python `if allowed is False and not is_special_submit`)。
206
+ // 不要把 0/'' 之类历史返回一并当"不允许"——会把可操作流程误拦成 operate_not_allowed。
207
+ if (allowed === false && !isSpecialSubmit) {
208
+ throw workflowValidationError('operate_not_allowed', `流程 ${requestId} 当前不支持「${String(action ?? '')}」操作,已停止操作`, { requestId, isRemark, canSubmitRejectTransfer: allowed });
209
+ }
210
+ return {
211
+ requestId,
212
+ isRemark,
213
+ nodeid: String(mapItem.nodeid ?? mapItem.nodeId ?? ''),
214
+ workflowId: String(mapItem.workflowid ?? mapItem.workflowId ?? ''),
215
+ userMasterId: String(mapItem.usermasterid ?? mapItem.userMasterId ?? mapItem.secondempid ?? ''),
216
+ userid: String(mapItem.userid ?? ''),
217
+ canSubmitRejectTransfer: allowed,
218
+ };
219
+ }
220
+ /** 单条操作可操作性门禁(对应 Python cmd_workflow_operate 的 canSubmitRejectTransfer 校验)。 */
221
+ function applyOperabilityGate(item, action) {
222
+ const needsGate = ['agree', 'submit', 'reject', 'return', 'transfer', 'turnTodo'].includes(String(action ?? ''));
223
+ if (!needsGate)
224
+ return;
225
+ const isRemark = String(item.isRemark ?? '');
226
+ const allowed = item.canSubmitRejectTransfer;
227
+ const isSpecialSubmit = ['agree', 'submit'].includes(String(action ?? '')) &&
228
+ (isRemark === '10' || isRemark === '11' || READ_REMARK_VALUES.has(isRemark));
229
+ // 同 detail 门禁:只在明确 false 时拦截(V11.3 语义)
230
+ if (allowed === false && !isSpecialSubmit) {
231
+ throw workflowValidationError('operate_not_allowed', `流程 ${String(item.requestId)} 当前不支持「${String(action ?? '')}」操作,已停止操作`, { requestId: item.requestId, isRemark, canSubmitRejectTransfer: allowed });
232
+ }
233
+ }
234
+ /** apply 阶段校验:confirm + continuation(且与 prepare 时登记的操作名一致)。 */
235
+ async function verifyConfirm(env, args, expectedOperation) {
236
+ if (args.confirm !== true) {
237
+ throw workflowConfirmationError(expectedOperation, {
238
+ hint: '请重新调用并传入 confirm=true 与 prepare 返回的 continuation',
239
+ });
240
+ }
241
+ const lease = await env.continuation.verify(args.continuation, expectedOperation);
242
+ const session = await env.host.getSession();
243
+ if (lease.baseUrl !== session.baseUrl || lease.profile !== session.profile || lease.userId !== session.userId) {
244
+ throw workflowValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 prepare');
245
+ }
246
+ // verifyConfirm 仅 apply 阶段调用:校验通过后**原子抢占**该 continuation(一次性防重放)。
247
+ // 抢占判据是落盘 `wx` 创建本身的成败,而不是"先查存在、再写标记"——后者在两次执行交错时
248
+ // 会双双通过校验并各自写入,构成真实重复提交(2026-09-16 发现的 TOCTOU 窗口)。
249
+ // 抢不到(已被另一次执行占用)时**不再执行任何写入**,直接把上一次的执行结果回报出去:
250
+ // 已成功 → 幂等成功信封(ALREADY_APPLIED);结果未确认 → continuation_consumed。
251
+ const claim = await env.continuation.consume(args.continuation, expectedOperation);
252
+ if (!claim.claimed) {
253
+ throw continuationReplayError(claim.previous ?? { state: 'unknown' }, expectedOperation);
254
+ }
255
+ // 登记"本次执行正持有这个句柄":后续每一个写请求发出去之前都要据此推进阶段(见 runWrite)。
256
+ env.activeContinuation = String(args.continuation ?? '');
257
+ return lease;
258
+ }
259
+ /**
260
+ * 读取 prepare 阶段签发的 prepared 载荷(apply 阶段的唯一入口)。
261
+ *
262
+ * 兼容两种结构:**扁平**(本文件全部 prepare 实际签发的形态:`{prepared, operation, baseUrl, ...}`)
263
+ * 与**嵌套**(`{payload: {prepared}}`,表单系形态)。两种都取不到时抛显式 validation,
264
+ * **不允许**退化成 `undefined.prepared` 的 TypeError——契约错配若伪装成 internal/unexpected,
265
+ * 排查成本极高(2026-09-15 线上事故:8 个 apply 全崩且报错指向"未知内部错误")。
266
+ */
267
+ function readPreparedPayload(lease, operation) {
268
+ const prepared = lease?.prepared ?? lease?.payload?.prepared;
269
+ if (!prepared || typeof prepared !== 'object') {
270
+ throw workflowValidationError('continuation_payload_missing', `continuation 中没有 ${operation} 的 prepare 数据,无法执行。请重新执行对应的 Prepare 命令后再 apply。`, { retryAllowed: true, operation });
271
+ }
272
+ return prepared;
273
+ }
274
+ /**
275
+ * 包装写请求:发请求前把 continuation 阶段推进到 `saving`;网络中断/超时(结果未知)→
276
+ * workflowPartialError;其余错误原样抛出。
277
+ *
278
+ * 为什么必须在**写请求之前**推进阶段:`consume` 落下的 `claimed` 表示"只抢到句柄、还没写入",
279
+ * 是允许安全重占(同一句柄重试)的唯一依据。若"已经发过写请求"也停在 `claimed`,进程被杀后
280
+ * 重放就会把它当成"上次没写成"而重新提交 ⇒ 真实重复写入。推进失败则**不发请求**(宁可失败,
281
+ * 也不留一次无法判定结果的高风险写入)。
282
+ */
283
+ async function runWrite(stage, env, attempt, partialData) {
284
+ const active = env.activeContinuation;
285
+ if (active)
286
+ await env.continuation.markStage(active, 'saving');
287
+ try {
288
+ return await attempt();
289
+ }
290
+ catch (error) {
291
+ if (error instanceof WorkflowCliError && error.type === 'network') {
292
+ throw workflowPartialError('写操作请求已发出但响应中断/超时,操作结果未知。禁止自动重试,请先通过只读接口回查流程状态确认是否已生效。', partialData, { stage });
293
+ }
294
+ throw error;
295
+ }
296
+ }
297
+ /** 给写请求体附加当前操作人 userId(对应 Python with_userid)。 */
298
+ async function withUserId(env, payload) {
299
+ const session = await env.host.getSession();
300
+ return { ...payload, userId: session.userId };
301
+ }
302
+ // ---------------------------------------------------------------------------
303
+ // 节点字段校验(来源:run_node_field_verification L14294)
304
+ // 提交/批准/确认 triggerOperation=0,退回 triggerOperation=1。
305
+ // 仅作写前硬性门禁:blocked 时 apply 应停止。
306
+ // ---------------------------------------------------------------------------
307
+ async function runNodeFieldVerification(env, requestId, nodeId, triggerOperation) {
308
+ const result = { enabled: false, blocked: false, passed: true, reminders: [], results: [], passPopUpPrompt: 0, noPassPrompt: 0 };
309
+ const rid = String(requestId || '').trim();
310
+ const nid = String(nodeId || '').trim();
311
+ if (!rid || !nid)
312
+ return result;
313
+ let conf;
314
+ try {
315
+ conf = await env.host.workflowRequest({
316
+ method: 'GET',
317
+ path: WORKFLOW_ENDPOINTS.CONFORMANCE_SET,
318
+ operation: 'nodeVerification',
319
+ query: { requestId: rid, nodeId: nid, entryMode: 2, triggerOperation, delete: 'false' },
320
+ });
321
+ }
322
+ catch {
323
+ return result;
324
+ }
325
+ const confData = responseData(conf)?.data;
326
+ const cptBase = confData?.cptBase;
327
+ const cptSetList = confData?.cptSetList;
328
+ if (!cptBase || !Array.isArray(cptSetList) || !cptSetList.length)
329
+ return result;
330
+ if (Number(cptBase.openNodeCheck) !== 1)
331
+ return result;
332
+ result.enabled = true;
333
+ const ruleSet = cptSetList.map((rule) => ({
334
+ ruleName: String(rule.ruleName || ''),
335
+ verificationResult: '检测中…',
336
+ rowId: `${Math.random().toString(16).slice(2)}`,
337
+ conditionKey: String(rule.conditionKey || ''),
338
+ rulePassTip: String(rule.rulePassTip || ''),
339
+ ruleNoPassTip: String(rule.ruleNoPassTip || ''),
340
+ verificationStatus: 0,
341
+ processType: Number(rule.processType || 1),
342
+ cptSetId: String(rule.id || ''),
343
+ ruleType: Number(rule.ruleType || 0),
344
+ }));
345
+ let dataResult;
346
+ try {
347
+ // testUserId 传当前操作人 userId(来源:Python runtime.get("userId")),
348
+ // 保证依赖操作人身份的校验规则不因空串而漏拦/误拦。
349
+ const session = await env.host.getSession();
350
+ dataResult = await env.host.workflowRequest({
351
+ method: 'POST',
352
+ path: WORKFLOW_ENDPOINTS.VERIFICATION_REQUEST_DATA,
353
+ operation: 'nodeVerification',
354
+ body: { requestId: rid, nodeId: nid, ruleSet, isCooperate: 0, isTest: 0, testUserId: String(session.userId ?? '').trim() },
355
+ });
356
+ }
357
+ catch {
358
+ return result;
359
+ }
360
+ const dataPayload = responseData(dataResult)?.data;
361
+ if (!dataPayload || typeof dataPayload !== 'object')
362
+ return result;
363
+ const requestData = dataPayload.requestData ?? [];
364
+ const systemContents = dataPayload.systemContents ?? [];
365
+ for (const rule of ruleSet) {
366
+ let verdict;
367
+ try {
368
+ verdict = await env.host.workflowRequest({
369
+ method: 'POST',
370
+ path: WORKFLOW_ENDPOINTS.VERIFICATION_CONDITION_RULE,
371
+ operation: 'nodeVerification',
372
+ body: {
373
+ requestId: rid,
374
+ nodeId: nid,
375
+ identityId: '',
376
+ cptSetId: rule.cptSetId,
377
+ conditionKey: rule.conditionKey,
378
+ requestData,
379
+ systemContents,
380
+ },
381
+ });
382
+ }
383
+ catch {
384
+ continue;
385
+ }
386
+ const vd = responseData(verdict)?.data;
387
+ const status = Number(vd?.status || 0);
388
+ const ruleType = Number(rule.ruleType || 0);
389
+ const reason = String(vd?.reason || '').trim();
390
+ const tip = ruleType === 2 ? reason : String(vd?.result || '').trim();
391
+ const item = { ruleName: rule.ruleName, status, result: tip, reason, ruleType };
392
+ result.results.push(item);
393
+ if (status === 3)
394
+ result.reminders.push(item);
395
+ }
396
+ const ruleRelation = Number(cptBase.ruleRelation || 0);
397
+ const statuses = result.results.map((r) => r.status);
398
+ result.blocked = ruleRelation === 1 ? !statuses.some((s) => s === 1) : statuses.some((s) => s === 2);
399
+ result.passed = !result.blocked;
400
+ // 校验提示开关(来源:Python cpt_base.get("passPopUpPrompt"/"noPassPrompt"))——
401
+ // 0 时不显示校验结果直接操作;提醒(status=3)提示由 noPassPrompt=1 控制;
402
+ // 纯通过提示由 passPopUpPrompt=1 控制。
403
+ result.passPopUpPrompt = Number(cptBase.passPopUpPrompt || 0);
404
+ result.noPassPrompt = Number(cptBase.noPassPrompt || 0);
405
+ return result;
406
+ }
407
+ /** 节点字段校验提示文本(来源:Python post_operation 把 reminders 拼进 userMessage 的逻辑)。
408
+ * reminder(status=3 仅提醒)不阻止操作,但须回传给用户,否则「建议补充某字段」被吞。 */
409
+ function verificationNotice(verification) {
410
+ if (!verification?.enabled)
411
+ return undefined;
412
+ const reminders = verification.reminders || [];
413
+ if (reminders.length && verification.noPassPrompt === 1) {
414
+ const lines = reminders.map((r) => `${r.ruleName} 校验未通过(${r.result})`).join(';');
415
+ return `节点字段校验提醒:${lines}`;
416
+ }
417
+ if (verification.passed && !reminders.length && verification.passPopUpPrompt === 1) {
418
+ const lines = (verification.results || []).map((r) => `${r.ruleName} 校验通过(${r.result})`).join(';');
419
+ return `节点字段校验:${lines}`;
420
+ }
421
+ return undefined;
422
+ }
423
+ function parseShareObjects(raw) {
424
+ const arr = asStringArray(raw, 'shareObjects');
425
+ if (!arr)
426
+ return [];
427
+ const out = [];
428
+ for (const item of arr) {
429
+ const obj = parseJsonObject(item);
430
+ if (!obj || typeof obj !== 'object')
431
+ throw workflowValidationError('share_objects_invalid', '--share-objects 每项必须是对象');
432
+ const type = asString(obj.type);
433
+ if (!type)
434
+ throw workflowValidationError('share_type_missing', '共享对象缺少 type');
435
+ out.push({ type: type.trim().toLowerCase(), name: asString(obj.name) });
436
+ }
437
+ return out;
438
+ }
439
+ /** 共享对象多命中 → 以成功态返回候选澄清(来源:Python cmd_workflow_share 的 clarification 分支)。 */
440
+ class ShareClarificationSignal extends Error {
441
+ payload;
442
+ constructor(payload) {
443
+ super('share_clarification');
444
+ this.name = 'ShareClarificationSignal';
445
+ this.payload = payload;
446
+ }
447
+ }
448
+ /** 解析单个共享目标 → addRequestShare2 shareTargetList 项;歧义/0 命中抛错。 */
449
+ async function resolveShareTarget(env, obj, minSec, maxSec) {
450
+ const scope = (obj.type || '').trim().toLowerCase();
451
+ const shareType = SHARE_TYPE_MAP[scope];
452
+ if (!shareType) {
453
+ throw workflowValidationError('share_type_unsupported', `共享对象类型「${scope}」当前环境未提供对应浏览接口,仅支持 user/dept/subcompany/group/role/position/all`, { shareType: scope });
454
+ }
455
+ if (scope === 'all') {
456
+ return {
457
+ shareType: '11',
458
+ shareObjId: '0',
459
+ permissionTypeList: [0, 1, 2],
460
+ minSeclevel: minSec && /^\d+$/.test(minSec) ? Number(minSec) : -10,
461
+ maxSeclevel: maxSec && /^\d+$/.test(maxSec) ? Number(maxSec) : 900,
462
+ };
463
+ }
464
+ const name = (obj.name || '').trim();
465
+ if (!name)
466
+ throw workflowValidationError('share_target_name_missing', `共享对象(${scope})缺少 name`);
467
+ // 名称转 ID 复用人员范围(EmployeeScope)浏览接口:各类对象各有端点
468
+ // (来源:Python EMPLOYEE_SCOPE_BROWSER_TYPES,module=hrm)
469
+ const browserPaths = {
470
+ user: WORKFLOW_ENDPOINTS.HRM_RESOURCE_BROWSER,
471
+ dept: WORKFLOW_ENDPOINTS.DEPT_BROWSER,
472
+ subcompany: WORKFLOW_ENDPOINTS.SUBCOMPANY_BROWSER,
473
+ group: WORKFLOW_ENDPOINTS.GROUP_BROWSER,
474
+ role: WORKFLOW_ENDPOINTS.ROLE_BROWSER,
475
+ position: WORKFLOW_ENDPOINTS.POSITION_BROWSER,
476
+ };
477
+ const path = browserPaths[scope];
478
+ if (!path) {
479
+ throw workflowValidationError('share_type_unsupported', `共享对象类型「${scope}」当前环境未提供对应浏览接口,仅支持 user/dept/subcompany/group/role/position/all`, { shareType: scope });
480
+ }
481
+ // 共享对象的公共修饰(安全级别范围、角色/岗位级别)——「名称(ID) 直取」与「名称搜索」两条路径共用
482
+ const decorate = (shareObjId) => {
483
+ const built = { shareType, shareObjId, permissionTypeList: [0, 1, 2] };
484
+ // 部门/分部/角色/岗位/所有人带安全级别范围(来源:Python resolve_share_target)
485
+ if (scope === 'dept' || scope === 'subcompany' || scope === 'role' || scope === 'position') {
486
+ built.minSeclevel = minSec && /^\d+$/.test(minSec) ? Number(minSec) : -10;
487
+ built.maxSeclevel = maxSec && /^\d+$/.test(maxSec) ? Number(maxSec) : 900;
488
+ }
489
+ // 角色级别:0 总部(默认);岗位级别:0 所有组织下的岗位(默认)
490
+ if (scope === 'role' || scope === 'position')
491
+ built.shareTragetType = '0';
492
+ return built;
493
+ };
494
+ // 显式「名称(ID)」:直接采用括号里的 ID,不再拿整串去搜名称。与 resolveReceiverNames 同一口径
495
+ // (V11.3 的 split_name_with_inline_id 亦然)。缺这条分支时,澄清与报错里指引的「改用 名称(ID)」
496
+ // 是死路——整串会被当成姓名去搜,必然 0 命中,用户按提示回传反而再报一次 not_found。
497
+ const inline = splitInlineId(name);
498
+ if (inline.id !== name)
499
+ return decorate(inline.id);
500
+ const candidates = await browserResolve(env, path, name);
501
+ const exact = candidates.filter((c) => c.name.trim().toLowerCase() === name.toLowerCase());
502
+ const selected = exact.length === 1 ? exact : candidates;
503
+ if (!selected.length) {
504
+ throw workflowValidationError('share_target_not_found', `共享对象「${name}(${scope})」未匹配到任何候选,请核对名称或改用 "名称(ID)"`, { shareType: scope, name });
505
+ }
506
+ if (selected.length !== 1) {
507
+ // 多命中不是失败而是"需要澄清":按 V11.3 语义以成功态返回候选(模型展示给用户后按
508
+ // "名称(ID)" 回传续跑)。原先直接抛校验错误会让模型把它当失败中止、不给候选。
509
+ throw new ShareClarificationSignal({
510
+ name,
511
+ scope,
512
+ candidates: selected.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })),
513
+ });
514
+ }
515
+ return decorate(selected[0].id);
516
+ }
517
+ /** 取共享安全级别默认值(getAddShareForm);失败返回空串(不阻断)。 */
518
+ async function shareDefaultSecLevels(env) {
519
+ try {
520
+ const res = await env.host.workflowRequest({
521
+ method: 'GET',
522
+ path: WORKFLOW_ENDPOINTS.SHARE_ADD_FORM,
523
+ operation: 'share.form',
524
+ });
525
+ // 解包层数:responseData 已解一层(res.data),此处只在未直接拿到 items 时再解一层
526
+ // ——原先固定多写一层 `.data`,实测导致 safeLevelObj 恒取不到,安全级别默认值退化成
527
+ // -10/900(比系统默认更宽),与 Python `result["data"]["items"]["safeLevelObj"]["value"]` 不符。
528
+ const body = responseData(res);
529
+ const data = body && typeof body === 'object' && body.items
530
+ ? body
531
+ : (body && typeof body === 'object' ? body.data ?? {} : {});
532
+ const value = data?.items?.safeLevelObj?.value ?? {};
533
+ return [String(value?.min ?? '').trim(), String(value?.max ?? '').trim()];
534
+ }
535
+ catch {
536
+ return ['', ''];
537
+ }
538
+ }
539
+ function mergeSecLevel(userValue, defaultValue, takeMax) {
540
+ const userStr = String(userValue ?? '').trim();
541
+ if (!userStr || !/^\d+$/.test(userStr))
542
+ return (defaultValue ?? '').trim();
543
+ const userInt = Number(userStr);
544
+ if (!/^\d+$/.test((defaultValue ?? '').trim()))
545
+ return userStr;
546
+ const defInt = Number(defaultValue);
547
+ return String(takeMax ? Math.max(userInt, defInt) : Math.min(userInt, defInt));
548
+ }
549
+ /** 转发接收人定义:{type, name?};type=all 不需要 name。 */
550
+ function parseReceiveTargets(raw) {
551
+ const arr = asStringArray(raw, 'receiveTargets');
552
+ if (!arr)
553
+ return [];
554
+ const out = [];
555
+ for (const item of arr) {
556
+ const obj = parseJsonObject(item);
557
+ if (!obj || typeof obj !== 'object')
558
+ throw workflowValidationError('receive_targets_invalid', '--receive-targets 每项必须是对象');
559
+ const type = asString(obj.type);
560
+ if (!type)
561
+ throw workflowValidationError('forward_type_missing', '转发接收人缺少 type');
562
+ out.push({ type: type.trim().toLowerCase(), name: asString(obj.name) });
563
+ }
564
+ return out;
565
+ }
566
+ /** 解析单个转发接收人 → batchForward receiveOperatorInfos 项;歧义/0 命中抛错。 */
567
+ async function resolveForwardReceiver(env, obj) {
568
+ const rtype = (obj.type || '').trim().toLowerCase();
569
+ if (!FORWARD_RECEIVER_TYPES.has(rtype)) {
570
+ throw workflowValidationError('forward_receiver_unsupported', `转发接收人类型「${rtype}」当前环境仅支持 user/dept/group/all(人员/部门/群组/所有人)`, { receiverType: rtype });
571
+ }
572
+ if (rtype === 'all')
573
+ return { _entityType: 'all', id: '0' };
574
+ const name = (obj.name || '').trim();
575
+ if (!name)
576
+ throw workflowValidationError('forward_receiver_name_missing', `转发接收人(${rtype})缺少 name`);
577
+ const path = rtype === 'dept' ? WORKFLOW_ENDPOINTS.DEPT_BROWSER : WORKFLOW_ENDPOINTS.RESOURCE_BROWSER;
578
+ // 显式「名称(ID)」:直接采用括号里的 ID(与 resolveShareTarget 同名分支同一理由)。
579
+ const inline = splitInlineId(name);
580
+ if (inline.id !== name)
581
+ return { _entityType: rtype, id: inline.id };
582
+ const candidates = await browserResolve(env, path, name);
583
+ const exact = candidates.filter((c) => c.name.trim().toLowerCase() === name.toLowerCase());
584
+ const selected = exact.length === 1 ? exact : candidates;
585
+ if (selected.length !== 1) {
586
+ throw workflowValidationError('forward_receiver_ambiguous', `转发接收人「${name}(${rtype})」匹配到 ${selected.length} 个候选,请确认后回传 "名称(ID)"`, { candidates: selected.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })) });
587
+ }
588
+ return { _entityType: rtype, id: selected[0].id };
589
+ }
590
+ /** 判断系统是否允许转发给所有人(getReceiverAllowAllConfig)。 */
591
+ async function forwardAllowAllEnabled(env) {
592
+ try {
593
+ const res = await env.host.workflowRequest({
594
+ method: 'GET',
595
+ path: WORKFLOW_ENDPOINTS.FORWARD_ALLOW_ALL,
596
+ operation: 'forward.allowAll',
597
+ });
598
+ return Boolean(responseData(res)?.data?.allowAll);
599
+ }
600
+ catch {
601
+ return false;
602
+ }
603
+ }
604
+ /** 拉取某类批量可执行待办数据(对应 Python workflow-search --batch-type 的批量端点查询)。 */
605
+ async function fetchBatchTodoItems(env, suffix, pageSize) {
606
+ const listKey = `TODO_LIST_BATCH_${suffix}`;
607
+ const totalKey = `TODO_TOTAL_BATCH_${suffix}`;
608
+ const listPath = WORKFLOW_ENDPOINTS[listKey];
609
+ const totalPath = WORKFLOW_ENDPOINTS[totalKey];
610
+ if (!listPath || !totalPath) {
611
+ throw workflowValidationError('batch_type_unsupported', `暂不支持的批量类型:${suffix}`);
612
+ }
613
+ const body = {
614
+ params: { tabid: '0', dimension: 'todo', flowSpareType: 1, outType: 0 },
615
+ searchParams: { formDatas: {}, type: 'and', initSearch: false },
616
+ customDisplayPage: { current: 1, size: Math.max(1, pageSize) },
617
+ };
618
+ const totalBody = { ...body };
619
+ delete totalBody.customDisplayPage;
620
+ const [totalResponse, listResponse] = await Promise.all([
621
+ env.host.workflowRequest({ method: 'POST', path: totalPath, operation: 'batch.fetchTotal', body: totalBody }),
622
+ env.host.workflowRequest({ method: 'POST', path: listPath, operation: 'batch.fetch', body }),
623
+ ]);
624
+ return { items: extractBatchItems(listResponse), total: extractBatchTotal(totalResponse) };
625
+ }
626
+ function extractBatchItems(response) {
627
+ const record = responseData(response);
628
+ const data = record?.data && typeof record.data === 'object' ? record.data : {};
629
+ const records = toArray(data?.displayData ?? record?.displayData ?? data?.list ?? record?.list ?? []);
630
+ const out = [];
631
+ for (const rec of records) {
632
+ const o = rec && typeof rec === 'object' ? rec : {};
633
+ const requestId = String(o.requestid ?? o.requestId ?? '');
634
+ if (!requestId)
635
+ continue;
636
+ out.push({
637
+ requestId,
638
+ isRemark: String(o.isremark ?? o.isRemark ?? ''),
639
+ userMasterId: String(o.usermasterid ?? o.userMasterId ?? o.secondempid ?? o.secondEmpId ?? ''),
640
+ userid: String(o.userid ?? o.usermasterid ?? o.userMasterId ?? ''),
641
+ nodeid: String(o.nodeid ?? o.nodeId ?? ''),
642
+ workflowId: String(o.workflowid ?? o.workflowId ?? ''),
643
+ });
644
+ }
645
+ return out;
646
+ }
647
+ function extractBatchTotal(response) {
648
+ const record = responseData(response);
649
+ const candidates = [
650
+ record?.total,
651
+ record?.count,
652
+ record?.totalCount,
653
+ record?.data?.total,
654
+ record?.data?.totalCount,
655
+ ];
656
+ for (const c of candidates) {
657
+ const n = asInt(c);
658
+ if (n !== undefined)
659
+ return n;
660
+ }
661
+ return 0;
662
+ }
663
+ /** 快照条目 → 批量候选:元数据全部取自「最近一次查询结果」(与 V11.3 会话映射一致)。 */
664
+ function indexItemToBatchCandidate(item) {
665
+ const record = item;
666
+ return {
667
+ requestId: String(record.requestId ?? ''),
668
+ isRemark: String(record.isRemark ?? ''),
669
+ userMasterId: String(record.userMasterId ?? record.secondEmpId ?? ''),
670
+ userid: String(record.userid ?? ''),
671
+ nodeid: String(record.nodeid ?? record.nodeId ?? ''),
672
+ workflowId: String(record.workflowId ?? ''),
673
+ };
674
+ }
675
+ /**
676
+ * 用实时批量列表补齐**快照里缺的字段**(典型是 `userid`:列表归一化不产出它,而批量转发要用
677
+ * 它拼 operateUserIds)。
678
+ *
679
+ * 只补空值——不覆盖快照已有字段、不改变 isRemark 分类、**不参与放行判据**。因此实时列表用哪套
680
+ * 取数口径(含 `tabid`)与"能不能操作"无关。认证/网络错误原样抛出;其余失败静默忽略(快照已
681
+ * 足够支撑写请求,门禁在上游已通过)。
682
+ */
683
+ async function enrichBatchMetadata(env, suffix, items, pageSize) {
684
+ const pending = items.filter((it) => !it.userid || !it.userMasterId || !it.nodeid || !it.workflowId);
685
+ if (!pending.length)
686
+ return;
687
+ const live = new Map();
688
+ try {
689
+ const fetched = await fetchBatchTodoItems(env, suffix, Math.max(pageSize, items.length));
690
+ for (const it of fetched.items)
691
+ live.set(String(it.requestId), it);
692
+ }
693
+ catch (error) {
694
+ rethrowAuthOrNetworkError(error);
695
+ return;
696
+ }
697
+ for (const target of pending) {
698
+ const source = live.get(String(target.requestId));
699
+ if (!source)
700
+ continue;
701
+ if (!target.userid && source.userid)
702
+ target.userid = source.userid;
703
+ if (!target.userMasterId && source.userMasterId)
704
+ target.userMasterId = source.userMasterId;
705
+ if (!target.nodeid && source.nodeid)
706
+ target.nodeid = source.nodeid;
707
+ if (!target.workflowId && source.workflowId)
708
+ target.workflowId = source.workflowId;
709
+ }
710
+ }
711
+ /**
712
+ * 门禁失败文案:只指向**唯一的取数口径**——最近一次查询结果。
713
+ *
714
+ * 旧文案把失败归因为"刚被处理掉"或"取了别的 `--tabid`",并把"必要时带 `--tabid` 重新取数"
715
+ * 写成恢复路径;而后者本身就是导致误判的原因(见 `locateBatchItems`),模型会顺着它去换口径
716
+ * 反复重查。这里同时保留「先只读核对是否已被处理」的指引,避免把"上一次已成功"误判成需要重试。
717
+ */
718
+ function batchGateMessage(missing) {
719
+ const preview = `${missing.slice(0, 5).join(',')}${missing.length > 5 ? ',…' : ''}`;
720
+ return '批量操作需校验流程来自最近一次查询结果(isRemark < 100 才可批量操作)。'
721
+ + `以下 ${missing.length} 条不在最近一次查询结果里:${preview}。`
722
+ + '正确流程:先用 workflow.search --batchType <类型> --pageSize 100 查询一页,'
723
+ + '再立即用该页返回的 requestId 调用批量命令(不要先收集全部 requestId 再一次性提交)。'
724
+ + `若这些流程其实已经被处理过,请先只读核对:workflow.search --category done --requestId ${preview};`
725
+ + '已处理就按成功汇报,不要重复执行同一条批量命令(也不要为此重跑 Prepare)。';
726
+ }
727
+ /**
728
+ * 定位批量 requestIds:在**最近一次查询结果的快照**(本地索引 `search-index.json`,对应 Python
729
+ * `session_maps.json` 的 `latestQueryId.items`)中校验并取回元数据 —— V11.3 会话映射门禁语义。
730
+ *
731
+ * 为什么必须回到快照(2026-09-16 裁决):V11.3 的门禁是
732
+ * `cmd_batch_submit → load_session_maps() → queries[latestQueryId].items`,**从不在门禁时再查
733
+ * 一次服务端**,因此不存在"查询一个口径、门禁另一个口径"的分叉。V12 一度改成实时查
734
+ * `batchSubmit` 列表、且请求体硬编码 `tabid:'0'`,而文档指引的查询
735
+ * (`workflow.search --batchType`)解析出的 tabid 可能是别的值——两批数据不相交时,模型按文档
736
+ * 查出来的 requestId 会被门禁判成"不存在",只能反复重查反复试(真实事故:3 条一起提交全部
737
+ * `batch_gate_required`)。回到快照后,判据只依赖"这些流程确实是模型刚查到的"。
738
+ *
739
+ * "重复执行同一批 requestId"的风险由 continuation 一次性门禁承担(句柄只能消费一次;已消费且
740
+ * 成功时 apply 直接返回 `ALREADY_APPLIED` 成功信封),不再依赖门禁多查一次服务端兜底。
741
+ */
742
+ async function locateBatchItems(env, suffix, requestIds, pageSize) {
743
+ const index = await loadSearchIndex(env.ctx);
744
+ const byId = new Map();
745
+ for (const item of collectAllIndexItems(index)) {
746
+ const requestId = String(item.requestId ?? '');
747
+ if (requestId && !byId.has(requestId))
748
+ byId.set(requestId, item);
749
+ }
750
+ const located = [];
751
+ const missing = [];
752
+ for (const rid of requestIds) {
753
+ const hit = byId.get(String(rid));
754
+ if (hit)
755
+ located.push(indexItemToBatchCandidate(hit));
756
+ else
757
+ missing.push(String(rid));
758
+ }
759
+ if (missing.length) {
760
+ throw workflowValidationError('batch_gate_required', batchGateMessage(missing), {
761
+ missingRequestIds: missing,
762
+ });
763
+ }
764
+ await enrichBatchMetadata(env, suffix, located, pageSize);
765
+ return { items: located, total: located.length };
766
+ }
767
+ function collectRequestIds(args) {
768
+ const fromJson = asStringArray(args.requestIds, 'requestIds');
769
+ if (fromJson)
770
+ return fromJson.map((s) => String(s).trim()).filter(Boolean);
771
+ const raw = asString(args.requestIds);
772
+ if (raw)
773
+ return raw.split(',').map((s) => s.trim()).filter(Boolean);
774
+ return [];
775
+ }
776
+ // ===========================================================================
777
+ // 1. workflow.operatePrepare / workflow.operateApply
778
+ // ===========================================================================
779
+ const OPERATE_LOCATE_SCHEMA = schemaOf({
780
+ requestId: SCHEMA_REQUEST_ID,
781
+ group: { type: 'string', description: '检索索引分组标识(与 index 配合)' },
782
+ index: SCHEMA_INDEX,
783
+ action: {
784
+ type: 'string',
785
+ enum: [
786
+ 'agree', 'submit', 'reject', 'return', 'transfer', 'turnTodo', 'delegate',
787
+ 'forward', 'cc', 'addreview', 'forceDrawBack', 'forceOver', 'consul',
788
+ 'deleteRequest', 'supervise',
789
+ ],
790
+ description: '动作,按用户原话选:agree/submit 同意提交,reject/return 退回,turnTodo 转办,forward 转发,delegate 委托,cc 抄送,addreview 加审,forceDrawBack 强制收回,forceOver 强制结束,consul 意见征询,deleteRequest 删除,supervise 催办',
791
+ },
792
+ opinion: { type: 'string', description: '签字/操作意见(同意/退回/强制结束/意见征询/催办等使用)' },
793
+ receiverName: { type: 'string', description: '接收人类动作(transfer/forward/cc/addreview/consul)接收人名称,逗号分隔,支持 "名称(ID)"' },
794
+ agent: { type: 'string', description: 'delegate 代理人用户 ID' },
795
+ rejectType: { type: 'string', description: 'reject 退回类型 0自由/1逐级/2指定范围/3按出口' },
796
+ nodeid: { type: 'string', description: 'reject 退回目标节点 ID' },
797
+ deleteType: { type: 'string', description: 'deleteRequest 删除方式 0回收站(默认)/1彻底删除' },
798
+ otherParamJson: { type: 'string', description: '附加参数 JSON(forceDrawBack/forceOver/consul 的 otherParam/otherParams)' },
799
+ selectFlowParamJson: { type: 'string', description: 'submit/agree 多出口节点提交时的指定流转参数 JSON(selectFlowParam)' },
800
+ exceptionHandleInfoJson: { type: 'string', description: 'submit/agree 提交时的异常处理参数 JSON(exceptionHandleInfo)' },
801
+ formdataJson: { type: 'string', description: 'submit/agree 随提交携带的表单数据 JSON(formdata)' },
802
+ }, { required: ['action'] });
803
+ const workflowOperatePrepare = {
804
+ name: 'workflow.operatePrepare',
805
+ resource: 'workflow',
806
+ method: 'operatePrepare',
807
+ action: 'workflow.operatePrepare',
808
+ summary: '准备单条流程操作:解析定位、接收人名称转 ID、预览,不写入;产出 continuation 供 apply 使用',
809
+ capability: 'operate',
810
+ risk: 'read-before-write',
811
+ requiresConfirmation: true,
812
+ inputSchema: OPERATE_LOCATE_SCHEMA,
813
+ handler: async (args, env) => {
814
+ const action = asString(args.action);
815
+ if (!action)
816
+ throw workflowValidationError('action_required', 'action 必填');
817
+ const item = await resolveTargetItem(env, args);
818
+ const remark = asString(args.opinion) ?? asString(args.remark) ?? '';
819
+ const prepared = {
820
+ requestId: String(item.requestId),
821
+ action,
822
+ remark,
823
+ isRemark: String(item.isRemark ?? ''),
824
+ nodeid: String(item.nodeid ?? item.nodeId ?? ''),
825
+ // 置为已办(doBatchRead)必需:缺 workflowId/secondEmpId 时接口报错
826
+ // (来源:Python cmd_workflow_operate 注入 read_items / cmd_batch_read 取 nodeid+workflowId+userMasterId)
827
+ workflowId: String(item.workflowId ?? ''),
828
+ userMasterId: String(item.userMasterId ?? item.secondEmpId ?? ''),
829
+ canSubmitRejectTransfer: item.canSubmitRejectTransfer,
830
+ };
831
+ const needsReceiver = ['transfer', 'turnTodo', 'forward', 'cc', 'addreview', 'consul'].includes(action);
832
+ if (needsReceiver) {
833
+ const receiverName = asString(args.receiverName);
834
+ if (!receiverName)
835
+ throw workflowValidationError('receiver_required', `动作 ${action} 必须提供 --receiver-name`);
836
+ prepared.receivers = await resolveReceiverNames(env, receiverName);
837
+ }
838
+ if (action === 'delegate') {
839
+ const agent = asString(args.agent);
840
+ if (!agent)
841
+ throw workflowValidationError('agent_required', 'delegate 必须提供 --agent 代理人用户 ID');
842
+ prepared.agent = agent;
843
+ }
844
+ if (action === 'reject') {
845
+ if (args.rejectType !== undefined)
846
+ prepared.rejectType = asString(args.rejectType);
847
+ if (args.nodeid !== undefined)
848
+ prepared.rejectNodeid = asString(args.nodeid);
849
+ }
850
+ if (action === 'deleteRequest') {
851
+ prepared.deleteType = asString(args.deleteType) ?? '0';
852
+ }
853
+ if (['forceDrawBack', 'forceOver', 'consul', 'supervise'].includes(action) && args.otherParamJson !== undefined) {
854
+ const op = parseJsonObject(args.otherParamJson);
855
+ if (op)
856
+ prepared.otherParam = op;
857
+ }
858
+ // 多出口节点提交:透传 selectFlowParam / exceptionHandleInfo / formdata(来源:Python cmd_submit
859
+ // add_optional_payload)——否则多出口流程会取默认/首分支或报「请选择出口」,走错审批路径。
860
+ if (action === 'agree' || action === 'submit') {
861
+ const selectFlowParam = parseJsonObject(args.selectFlowParamJson);
862
+ if (selectFlowParam)
863
+ prepared.selectFlowParam = selectFlowParam;
864
+ const exceptionHandleInfo = parseJsonObject(args.exceptionHandleInfoJson);
865
+ if (exceptionHandleInfo)
866
+ prepared.exceptionHandleInfo = exceptionHandleInfo;
867
+ const formdata = parseJsonObject(args.formdataJson);
868
+ if (formdata)
869
+ prepared.formdata = formdata;
870
+ }
871
+ const session = await env.host.getSession();
872
+ const token = await env.continuation.issue({
873
+ operation: 'workflow.operateApply',
874
+ baseUrl: session.baseUrl,
875
+ profile: session.profile,
876
+ userId: session.userId,
877
+ prepared,
878
+ }, CONTINUATION_TTL_SECONDS * 1000);
879
+ return workflowSuccess('workflow.operatePrepare', {
880
+ status: 'AWAITING_CONFIRMATION',
881
+ preview: {
882
+ requestId: prepared.requestId,
883
+ action,
884
+ remark,
885
+ receivers: prepared.receivers,
886
+ agent: prepared.agent,
887
+ },
888
+ continuation: token,
889
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
890
+ });
891
+ },
892
+ };
893
+ const workflowOperateApply = {
894
+ name: 'workflow.operateApply',
895
+ resource: 'workflow',
896
+ method: 'operateApply',
897
+ action: 'workflow.operateApply',
898
+ summary: '提交单条流程操作;必须 confirm=true 且携带 operatePrepare 的 continuation',
899
+ capability: 'operate',
900
+ risk: 'high-risk-write',
901
+ requiresConfirmation: true,
902
+ inputSchema: schemaOf({
903
+ ...OPERATE_LOCATE_SCHEMA.properties,
904
+ ...CONTINUATION_INPUT,
905
+ }, { required: ['action', 'confirm', 'continuation'] }),
906
+ handler: async (args, env) => {
907
+ const lease = await verifyConfirm(env, args, 'workflow.operateApply');
908
+ const prepared = readPreparedPayload(lease, 'workflow.operateApply');
909
+ if (!prepared || !prepared.requestId) {
910
+ throw workflowValidationError('continuation_invalid', 'continuation 缺少操作数据');
911
+ }
912
+ const action = prepared.action;
913
+ const requestId = String(prepared.requestId);
914
+ const remark = prepared.remark ?? '';
915
+ // 不需批示/不需提交/不需批注(isremark 60/61/70/71/80/81)走置为已办;10/11 走征询回复
916
+ const isRemark = String(prepared.isRemark ?? '');
917
+ if ((action === 'agree' || action === 'submit')) {
918
+ if (isRemark === '10' || isRemark === '11') {
919
+ const payload = await withUserId(env, { requestId, remark });
920
+ const response = await runWrite('consulReply', env, () => env.host.workflowRequest({
921
+ method: 'POST', path: WORKFLOW_ENDPOINTS.CONSUL_REPLY, operation: 'workflow.operateApply', body: payload,
922
+ }), payload);
923
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'consulReply', requestId, result: response });
924
+ }
925
+ if (READ_REMARK_VALUES.has(isRemark)) {
926
+ const umid = String(prepared.userMasterId ?? '');
927
+ const readPayload = [{
928
+ requestId,
929
+ workflowId: String(prepared.workflowId ?? ''),
930
+ nodeId: String(prepared.nodeid ?? ''),
931
+ dataSrc: 0,
932
+ module: 0,
933
+ requestCode: '',
934
+ workflowCode: '',
935
+ isRemark: Number(isRemark) || 0,
936
+ secondEmpId: umid,
937
+ }];
938
+ const response = await runWrite('batchRead', env, () => env.host.workflowRequest({
939
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_READ, operation: 'workflow.operateApply', body: readPayload,
940
+ }), readPayload);
941
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'batchRead', requestId, result: response });
942
+ }
943
+ }
944
+ const receivers = prepared.receivers ?? [];
945
+ const receiveOperators = receivers.map((id) => ({ id, entityType: 'user' }));
946
+ switch (action) {
947
+ case 'agree':
948
+ case 'submit': {
949
+ const nodeId = String(prepared.nodeid ?? '');
950
+ const verification = await runNodeFieldVerification(env, requestId, nodeId, '0');
951
+ if (verification.blocked) {
952
+ const lines = verification.results
953
+ .filter((r) => r.status !== 1)
954
+ .map((r) => `${r.ruleName} 校验未通过(${r.result})`)
955
+ .join(';');
956
+ throw workflowValidationError('node_field_verification_blocked', `节点字段校验未通过,已停止提交:${lines}`, {
957
+ verification,
958
+ });
959
+ }
960
+ const payload = await withUserId(env, { requestId, remark });
961
+ if (nodeId)
962
+ payload.userCurrentNodeId = nodeId;
963
+ if (prepared.requestName)
964
+ payload.requestName = prepared.requestName;
965
+ // 多出口节点提交透传(来源:Python cmd_submit add_optional_payload)
966
+ if (prepared.selectFlowParam)
967
+ payload.selectFlowParam = prepared.selectFlowParam;
968
+ if (prepared.exceptionHandleInfo)
969
+ payload.exceptionHandleInfo = prepared.exceptionHandleInfo;
970
+ if (prepared.formdata)
971
+ payload.formdata = prepared.formdata;
972
+ const response = await runWrite('submit', env, () => env.host.workflowRequest({
973
+ method: 'POST', path: WORKFLOW_ENDPOINTS.SUBMIT_REQUEST, operation: 'workflow.operateApply', body: payload,
974
+ }), payload);
975
+ const notice = verificationNotice(verification);
976
+ return workflowSuccess('workflow.operateApply', {
977
+ status: 'COMPLETE', action: 'submit', requestId, result: response, verification,
978
+ ...(notice ? { notice } : {}),
979
+ });
980
+ }
981
+ case 'reject':
982
+ case 'return': {
983
+ const nodeId = String(prepared.nodeid ?? '');
984
+ const verification = await runNodeFieldVerification(env, requestId, nodeId, '1');
985
+ if (verification.blocked) {
986
+ const lines = verification.results
987
+ .filter((r) => r.status !== 1)
988
+ .map((r) => `${r.ruleName} 校验未通过(${r.result})`)
989
+ .join(';');
990
+ throw workflowValidationError('node_field_verification_blocked', `节点字段校验未通过,已停止退回:${lines}`, {
991
+ verification,
992
+ });
993
+ }
994
+ const payload = await withUserId(env, {
995
+ requestId,
996
+ RejectToType: prepared.rejectType ?? '0',
997
+ remark,
998
+ });
999
+ if (prepared.rejectNodeid)
1000
+ payload.RejectToNodeid = prepared.rejectNodeid;
1001
+ const response = await runWrite('reject', env, () => env.host.workflowRequest({
1002
+ method: 'POST', path: WORKFLOW_ENDPOINTS.REJECT_REQUEST, operation: 'workflow.operateApply', body: payload,
1003
+ }), payload);
1004
+ const notice = verificationNotice(verification);
1005
+ return workflowSuccess('workflow.operateApply', {
1006
+ status: 'COMPLETE', action: 'reject', requestId, result: response, verification,
1007
+ ...(notice ? { notice } : {}),
1008
+ });
1009
+ }
1010
+ case 'transfer':
1011
+ case 'turnTodo': {
1012
+ const payload = await withUserId(env, { requestId, receiveOperators, remark });
1013
+ const response = await runWrite('transfer', env, () => env.host.workflowRequest({
1014
+ method: 'POST', path: WORKFLOW_ENDPOINTS.TRANSFER_REQUEST, operation: 'workflow.operateApply', body: payload,
1015
+ }), payload);
1016
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'transfer', requestId, result: response });
1017
+ }
1018
+ case 'delegate': {
1019
+ const payload = await withUserId(env, { requestId, agentUserId: prepared.agent, remark });
1020
+ const response = await runWrite('delegate', env, () => env.host.workflowRequest({
1021
+ method: 'POST', path: WORKFLOW_ENDPOINTS.DELEGATE_REQUEST, operation: 'workflow.operateApply', body: payload,
1022
+ }), payload);
1023
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'delegate', requestId, result: response });
1024
+ }
1025
+ case 'forward': {
1026
+ const payload = await withUserId(env, { requestId, receiveOperators, remark });
1027
+ if (prepared.otherParam)
1028
+ payload.otherParam = prepared.otherParam;
1029
+ const response = await runWrite('forward', env, () => env.host.workflowRequest({
1030
+ method: 'POST', path: WORKFLOW_ENDPOINTS.FORWARD_REQUEST, operation: 'workflow.operateApply', body: payload,
1031
+ }), payload);
1032
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'forward', requestId, result: response });
1033
+ }
1034
+ case 'cc': {
1035
+ const payload = await withUserId(env, { requestId, receiveOperators, remark });
1036
+ if (prepared.otherParam)
1037
+ payload.otherParam = prepared.otherParam;
1038
+ const response = await runWrite('cc', env, () => env.host.workflowRequest({
1039
+ method: 'POST', path: WORKFLOW_ENDPOINTS.CC_REQUEST, operation: 'workflow.operateApply', body: payload,
1040
+ }), payload);
1041
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'cc', requestId, result: response });
1042
+ }
1043
+ case 'addreview': {
1044
+ const payload = await withUserId(env, { requestId, receiveOperators, remark });
1045
+ if (prepared.otherParam)
1046
+ payload.otherParam = prepared.otherParam;
1047
+ const response = await runWrite('addReview', env, () => env.host.workflowRequest({
1048
+ method: 'POST', path: WORKFLOW_ENDPOINTS.ADD_REVIEW_REQUEST, operation: 'workflow.operateApply', body: payload,
1049
+ }), payload);
1050
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'addreview', requestId, result: response });
1051
+ }
1052
+ case 'forceDrawBack': {
1053
+ const payload = { requestId };
1054
+ if (prepared.otherParam)
1055
+ payload.otherParams = prepared.otherParam;
1056
+ const response = await runWrite('forceDrawBack', env, () => env.host.workflowRequest({
1057
+ method: 'POST', path: WORKFLOW_ENDPOINTS.FORCE_DRAW_BACK, operation: 'workflow.operateApply', body: payload,
1058
+ }), payload);
1059
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'forceDrawBack', requestId, result: response });
1060
+ }
1061
+ case 'forceOver': {
1062
+ const payload = await withUserId(env, { requestId, remark });
1063
+ if (prepared.otherParam)
1064
+ payload.otherParams = prepared.otherParam;
1065
+ const response = await runWrite('forceOver', env, () => env.host.workflowRequest({
1066
+ method: 'POST', path: WORKFLOW_ENDPOINTS.FORCE_OVER, operation: 'workflow.operateApply', body: payload,
1067
+ }), payload);
1068
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'forceOver', requestId, result: response });
1069
+ }
1070
+ case 'consul': {
1071
+ const payload = await withUserId(env, { requestId, receiveOperators, remark });
1072
+ if (prepared.otherParam)
1073
+ payload.otherParam = prepared.otherParam;
1074
+ const response = await runWrite('consul', env, () => env.host.workflowRequest({
1075
+ method: 'POST', path: WORKFLOW_ENDPOINTS.CONSUL, operation: 'workflow.operateApply', body: payload,
1076
+ }), payload);
1077
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'consul', requestId, result: response });
1078
+ }
1079
+ case 'deleteRequest': {
1080
+ const payload = await withUserId(env, {
1081
+ requestId,
1082
+ deleteType: Number(prepared.deleteType ?? 0) || 0,
1083
+ });
1084
+ if (prepared.otherParam)
1085
+ payload.otherParams = prepared.otherParam;
1086
+ const response = await runWrite('deleteRequest', env, () => env.host.workflowRequest({
1087
+ method: 'POST', path: WORKFLOW_ENDPOINTS.DELETE_REQUEST, operation: 'workflow.operateApply', body: payload,
1088
+ }), payload);
1089
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'deleteRequest', requestId, result: response });
1090
+ }
1091
+ case 'supervise': {
1092
+ // 默认提醒当前审批人;otherParamJson 可覆盖提醒对象(来源:Python cmd_supervise
1093
+ // `other_param.update(override)`——提醒谁的几个属性至少填 1 个)
1094
+ const otherParam = {
1095
+ currentOperator: 1,
1096
+ messageRemind: 0,
1097
+ doneOperator: 0,
1098
+ submitOperator: 0,
1099
+ ccOperator: 0,
1100
+ emailRemind: 0,
1101
+ };
1102
+ if (prepared.otherParam && typeof prepared.otherParam === 'object') {
1103
+ Object.assign(otherParam, prepared.otherParam);
1104
+ }
1105
+ const payload = await withUserId(env, { requestId, remark, otherParam });
1106
+ const response = await runWrite('supervise', env, () => env.host.workflowRequest({
1107
+ method: 'POST', path: WORKFLOW_ENDPOINTS.SUPERVISE, operation: 'workflow.operateApply', body: payload,
1108
+ }), payload);
1109
+ return workflowSuccess('workflow.operateApply', { status: 'COMPLETE', action: 'supervise', requestId, result: response });
1110
+ }
1111
+ default:
1112
+ throw workflowValidationError('action_unsupported', `不支持的审批动作:${action}`);
1113
+ }
1114
+ },
1115
+ };
1116
+ // ===========================================================================
1117
+ // 2. workflow.sharePrepare / workflow.shareApply
1118
+ // ===========================================================================
1119
+ const SHARE_INPUT = schemaOf({
1120
+ requestId: SCHEMA_REQUEST_ID,
1121
+ group: { type: 'string' },
1122
+ index: SCHEMA_INDEX,
1123
+ shareObjects: {
1124
+ type: 'array',
1125
+ items: { type: 'string' },
1126
+ description: '共享对象 JSON 数组字符串,每项 {"type":"user|dept|subcompany|group|role|position|all","name":"名称"};all 不需要 name',
1127
+ },
1128
+ minSeclevel: { type: 'string', description: '最小安全级别(与默认值取大)' },
1129
+ maxSeclevel: { type: 'string', description: '最大安全级别(与默认值取小)' },
1130
+ });
1131
+ const workflowSharePrepare = {
1132
+ name: 'workflow.sharePrepare',
1133
+ resource: 'workflow',
1134
+ method: 'sharePrepare',
1135
+ action: 'workflow.sharePrepare',
1136
+ summary: '准备流程共享:解析共享对象名称转 ID、取安全级别默认值,不写入;产出 continuation',
1137
+ capability: 'share',
1138
+ risk: 'read-before-write',
1139
+ requiresConfirmation: true,
1140
+ inputSchema: SHARE_INPUT,
1141
+ handler: async (args, env) => {
1142
+ const item = await resolveTargetItem(env, args);
1143
+ const objects = parseShareObjects(args.shareObjects);
1144
+ if (!objects.length)
1145
+ throw workflowValidationError('share_objects_required', '必须提供 --share-objects 共享对象列表');
1146
+ const [defMin, defMax] = await shareDefaultSecLevels(env);
1147
+ const minSec = mergeSecLevel(args.minSeclevel, defMin, true);
1148
+ const maxSec = mergeSecLevel(args.maxSeclevel, defMax, false);
1149
+ const targets = [];
1150
+ for (const obj of objects) {
1151
+ try {
1152
+ targets.push(await resolveShareTarget(env, obj, minSec, maxSec));
1153
+ }
1154
+ catch (err) {
1155
+ if (err instanceof ShareClarificationSignal) {
1156
+ return workflowSuccess('workflow.sharePrepare', {
1157
+ status: 'NEED_CLARIFICATION',
1158
+ needClarification: true,
1159
+ clarificationType: 'share_target',
1160
+ clarificationQuestion: `共享对象「${err.payload.name}」匹配到多个候选,请确认要共享给哪一个。`,
1161
+ candidates: err.payload.candidates,
1162
+ displayRules: [
1163
+ 'needClarification=true 时原样展示候选,等用户确认后用 "名称(ID)" 重新调用 workflow.sharePrepare',
1164
+ '候选序号与显示名称保持原样,不要自行重建查询条件',
1165
+ ],
1166
+ });
1167
+ }
1168
+ throw err;
1169
+ }
1170
+ }
1171
+ const prepared = {
1172
+ requestId: String(item.requestId),
1173
+ userMasterId: String(item.userMasterId ?? item.secondEmpId ?? ''),
1174
+ targets,
1175
+ minSec,
1176
+ maxSec,
1177
+ };
1178
+ const session = await env.host.getSession();
1179
+ const token = await env.continuation.issue({
1180
+ operation: 'workflow.shareApply',
1181
+ baseUrl: session.baseUrl,
1182
+ profile: session.profile,
1183
+ userId: session.userId,
1184
+ prepared,
1185
+ }, CONTINUATION_TTL_SECONDS * 1000);
1186
+ return workflowSuccess('workflow.sharePrepare', {
1187
+ status: 'AWAITING_CONFIRMATION',
1188
+ preview: { requestId: prepared.requestId, targetCount: targets.length, minSec, maxSec },
1189
+ continuation: token,
1190
+ expiresInSeconds: CONTINUATION_TTL_SECONDS,
1191
+ });
1192
+ },
1193
+ };
1194
+ const workflowShareApply = {
1195
+ name: 'workflow.shareApply',
1196
+ resource: 'workflow',
1197
+ method: 'shareApply',
1198
+ action: 'workflow.shareApply',
1199
+ summary: '提交流程共享(addRequestShare2);必须 confirm=true 且携带 sharePrepare 的 continuation',
1200
+ capability: 'share',
1201
+ risk: 'high-risk-write',
1202
+ requiresConfirmation: true,
1203
+ inputSchema: schemaOf({ ...SHARE_INPUT.properties, ...CONTINUATION_INPUT }, { required: ['confirm', 'continuation'] }),
1204
+ handler: async (args, env) => {
1205
+ const lease = await verifyConfirm(env, args, 'workflow.shareApply');
1206
+ const prepared = readPreparedPayload(lease, 'workflow.shareApply');
1207
+ if (!prepared || !prepared.requestId)
1208
+ throw workflowValidationError('continuation_invalid', 'continuation 缺少共享数据');
1209
+ const payload = {
1210
+ otherParams: {},
1211
+ shareSourceList: [
1212
+ {
1213
+ requestid: String(prepared.requestId),
1214
+ workflowid: '0',
1215
+ isCreate: 0,
1216
+ createWfId: '0',
1217
+ secondEmpId: String(prepared.userMasterId ?? ''),
1218
+ },
1219
+ ],
1220
+ shareTargetList: prepared.targets,
1221
+ };
1222
+ const response = await runWrite('shareAdd', env, () => env.host.workflowRequest({
1223
+ method: 'POST', path: WORKFLOW_ENDPOINTS.SHARE_ADD, operation: 'workflow.shareApply', body: payload,
1224
+ }), payload);
1225
+ return workflowSuccess('workflow.shareApply', { status: 'COMPLETE', requestId: prepared.requestId, result: response });
1226
+ },
1227
+ };
1228
+ // ===========================================================================
1229
+ // 3. workflow.batchSubmit(单命令:门禁 → 置已办 → 提交)
1230
+ // ===========================================================================
1231
+ const BATCH_REQUEST_IDS_INPUT = {
1232
+ oneOf: [{ type: 'array', items: SCHEMA_REQUEST_ID }, { type: 'string' }],
1233
+ description: '待批量操作的 requestId(数组或与查询索引一致的逗号分隔串)',
1234
+ };
1235
+ const BATCH_SIZE_INPUT = {
1236
+ type: 'integer',
1237
+ minimum: 1,
1238
+ maximum: 100,
1239
+ description: '单批最大条数,默认 100',
1240
+ };
1241
+ const BATCH_COMMON_INPUT = schemaOf({
1242
+ requestIds: BATCH_REQUEST_IDS_INPUT,
1243
+ remark: { type: 'string', description: '批量操作统一意见' },
1244
+ batchSize: BATCH_SIZE_INPUT,
1245
+ async: { type: 'boolean', description: '批量提交是否异步执行(isAsync,仅 batchSubmit 使用)' },
1246
+ category: SCHEMA_CATEGORY,
1247
+ });
1248
+ /** 单命令批量写入的统一入参:操作自身字段 + `confirm`(必填,与 requestIds 在同一次调用里传)。 */
1249
+ function batchWriteSchema(input) {
1250
+ return schemaOf({ ...input.properties, confirm: SCHEMA_CONFIRM }, { required: ['confirm'] });
1251
+ }
1252
+ /**
1253
+ * 单命令批量操作的**幂等键**:由「哪个操作、谁、在哪个租户、要写哪一批流程、写向哪些对象」决定,
1254
+ * 与 `remark` / `batchSize` / `confirm` 无关(同一批同一对象换个意见再执行一次,仍然是重复执行)。
1255
+ *
1256
+ * 为什么"写向哪些对象"必须进键:换对象是**另一次操作**——把同一批流程先转给张三、再转给李四,
1257
+ * 两次都合法;若键只看 requestIds,第二次会被误判成"上一次已成功",于是**不写入却报成功**,
1258
+ * 用户会以为李四收到了。remark 相反:同一批同一对象换句话再执行一次没有任何正当理由。
1259
+ *
1260
+ * 为什么用确定性键而不是 prepare 句柄:单命令没有可供模型搬运的句柄(这正是本次改造的目的),
1261
+ * 而防重复写入不能因此缺位。键相同 ⇒ 同一次写入意图 ⇒ 由 `consume` 原子抢占 + `saving` 阶段
1262
+ * 推进 + `settle` 回写结果组成的一次性闸门拦下第二次执行。
1263
+ */
1264
+ function batchIdempotencyKey(operation, session, requestIds, scope = {}) {
1265
+ // workflow.batchRead → batch-read(batchSubmit 仍是 batch-submit)
1266
+ const prefix = operation.replace(/^workflow\./u, '').replace(/[A-Z]/gu, (char) => `-${char.toLowerCase()}`);
1267
+ const fingerprint = [
1268
+ operation,
1269
+ session.baseUrl,
1270
+ session.profile,
1271
+ session.userId,
1272
+ [...requestIds].sort().join(','),
1273
+ canonicalJson(scope),
1274
+ ].join('|');
1275
+ return `${prefix}:${createHash('sha256').update(fingerprint).digest('hex')}`;
1276
+ }
1277
+ /** 稳定序列化:对象按键排序、数组保持顺序,保证同一组入参在任何书写顺序下得到同一个幂等键。 */
1278
+ function canonicalJson(value) {
1279
+ if (value === null || typeof value !== 'object')
1280
+ return JSON.stringify(value ?? null);
1281
+ if (Array.isArray(value))
1282
+ return `[${value.map((item) => canonicalJson(item)).join(',')}]`;
1283
+ const record = value;
1284
+ const keys = Object.keys(record).filter((key) => record[key] !== undefined).sort();
1285
+ return `{${keys.map((key) => `${JSON.stringify(key)}:${canonicalJson(record[key])}`).join(',')}}`;
1286
+ }
1287
+ /**
1288
+ * 批量写入的幂等保护窗口:窗口内「同一操作 + 同一批 requestId + 同一组写向对象」只允许写入一次;
1289
+ * 窗口过后同一批可以再次执行。
1290
+ *
1291
+ * 为什么必须有窗口(不能永久留痕):同一条流程被再次处理**是合法操作**——被退回后再次提交、
1292
+ * 被再次转办后再次催办、同一批流程换个人再转发一次。永久留痕会把它永久挡成 `ALREADY_APPLIED`。
1293
+ *
1294
+ * 为什么取 60 秒(而不是沿用句柄的 10 分钟):窗口要盖住的是**机械式重复执行**——宿主把同一条
1295
+ * 命令重跑(秒级)、或模型在没做只读核对前顺着报错直接重试(一般也在一个回合内)。而
1296
+ * `write_uncertain` 之后的**合法**恢复路径是"先只读核对 → 确认未生效 → 重试",若窗口设成
1297
+ * 10 分钟,核对完反而要干等十分钟,恢复路径被堵死。
1298
+ */
1299
+ const BATCH_IDEMPOTENCY_WINDOW_MS = 60_000;
1300
+ /** 单命令批量操作的统一确认门禁:必须显式 `confirm: true`。 */
1301
+ function requireBatchConfirm(args, operation) {
1302
+ if (args.confirm !== true) {
1303
+ throw workflowConfirmationError(operation, { hint: '请重新调用并传入 confirm=true' });
1304
+ }
1305
+ }
1306
+ /**
1307
+ * 抢占本次批量写入的一次性闸门(见 batchIdempotencyKey / BATCH_IDEMPOTENCY_WINDOW_MS)。
1308
+ *
1309
+ * 抢不到 ⇒ 上一次同键执行的状态决定回报形态(已成功 → ALREADY_APPLIED;写入未确认 →
1310
+ * write_uncertain;仅被并发占用 → continuation_busy),**本次一个写请求都不发**。
1311
+ *
1312
+ * 调用位置固定:必须放在「全部校验与名称解析都已通过、即将发出第一个写请求」之处——放早了,
1313
+ * 一次失败的校验会白白占掉窗口,让校正参数后的重试被误判成重复执行。
1314
+ */
1315
+ async function claimBatchWrite(env, operation, requestIds, scope = {}) {
1316
+ const session = await env.host.getSession();
1317
+ const key = batchIdempotencyKey(operation, session, requestIds, scope);
1318
+ const claim = await env.continuation.consume(key, operation, BATCH_IDEMPOTENCY_WINDOW_MS);
1319
+ if (!claim.claimed) {
1320
+ throw continuationReplayError(claim.previous ?? { state: 'unknown' }, operation);
1321
+ }
1322
+ // 登记本次执行的闸门:写请求发出前据此把阶段推进到 saving(见 runWrite)。
1323
+ env.activeContinuation = key;
1324
+ return key;
1325
+ }
1326
+ const workflowBatchSubmit = {
1327
+ name: 'workflow.batchSubmit',
1328
+ resource: 'workflow',
1329
+ method: 'batchSubmit',
1330
+ action: 'workflow.batchSubmit',
1331
+ summary: '批量提交:一次调用完成门禁校验、置已办(BATCH_READ)与提交(BATCH_SUBMIT);必须 confirm=true',
1332
+ capability: 'batch',
1333
+ risk: 'high-risk-write',
1334
+ requiresConfirmation: true,
1335
+ inputSchema: schemaOf({ ...BATCH_COMMON_INPUT.properties, confirm: SCHEMA_CONFIRM }, { required: ['confirm'] }),
1336
+ handler: async (args, env) => {
1337
+ requireBatchConfirm(args, 'workflow.batchSubmit');
1338
+ const requestIds = collectRequestIds(args);
1339
+ if (!requestIds.length)
1340
+ throw workflowValidationError('request_ids_required', '必须提供 requestIds');
1341
+ const batchSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
1342
+ const { items } = await locateBatchItems(env, 'SUBMIT', requestIds, batchSize);
1343
+ const submitItems = [];
1344
+ const readItems = [];
1345
+ const blockedItems = [];
1346
+ for (const it of items) {
1347
+ const isRemark = String(it.isRemark ?? '');
1348
+ const entry = {
1349
+ requestId: String(it.requestId),
1350
+ userid: String(it.userid ?? it.userMasterId ?? ''),
1351
+ userMasterId: String(it.userMasterId ?? it.secondEmpId ?? ''),
1352
+ nodeid: String(it.nodeid ?? it.nodeId ?? ''),
1353
+ workflowId: String(it.workflowId ?? ''),
1354
+ isRemark,
1355
+ };
1356
+ if (READ_REMARK_VALUES.has(isRemark))
1357
+ readItems.push(entry);
1358
+ else if (Number(isRemark) < 100)
1359
+ submitItems.push(entry);
1360
+ else
1361
+ blockedItems.push(entry);
1362
+ }
1363
+ // isRemark=100 不可批量提交:全部被拦时直接失败,不做"静默成功"(来源:Python cmd_workflow_batch_submit 门禁)
1364
+ if (!submitItems.length && !readItems.length) {
1365
+ throw workflowValidationError('batch_gate_required', '批量提交需校验流程可操作性(isRemark < 100 才可批量提交)。正确流程:先用 workflow.search --batchType submit --pageSize 100 查询一页可批量操作的数据,再立即用该页返回的 requestId 调用批量提交;isRemark=100 的流程不可批量提交。', { blockedRequestIds: blockedItems.map((item) => item.requestId), doNotRetry: true });
1366
+ }
1367
+ // 一次性写入闸门(见 batchIdempotencyKey / BATCH_IDEMPOTENCY_WINDOW_MS)。
1368
+ const key = await claimBatchWrite(env, 'workflow.batchSubmit', requestIds);
1369
+ let readResult = null;
1370
+ if (readItems.length) {
1371
+ const readPayload = readItems.map((it) => ({
1372
+ requestId: it.requestId,
1373
+ workflowId: it.workflowId,
1374
+ nodeId: it.nodeid,
1375
+ dataSrc: 0,
1376
+ module: 0,
1377
+ requestCode: '',
1378
+ workflowCode: '',
1379
+ isRemark: Number(it.isRemark) || 0,
1380
+ secondEmpId: it.userMasterId,
1381
+ }));
1382
+ readResult = await runWrite('batchRead', env, () => env.host.workflowRequest({
1383
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_READ, operation: 'workflow.batchSubmit', body: readPayload,
1384
+ }), readPayload);
1385
+ }
1386
+ let submitResult = null;
1387
+ let submittedCount = 0;
1388
+ if (submitItems.length) {
1389
+ // 按 batchSize 分批(契约:batchSize 1–100,CLI 自动分批循环)——
1390
+ // 原先把所有 requestId 拼成一次请求,超出单批上限会被服务端拒绝
1391
+ for (const batch of chunkArray(submitItems, batchSize)) {
1392
+ const ids = batch.map((it) => it.requestId).join(',');
1393
+ const payload = await withUserId(env, { requestIds: ids, remark: asString(args.remark) ?? '', isAsync: args.async === true });
1394
+ submitResult = await runWrite('batchSubmit', env, () => env.host.workflowRequest({
1395
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_SUBMIT, operation: 'workflow.batchSubmit', body: payload,
1396
+ }), payload);
1397
+ submittedCount += batch.length;
1398
+ }
1399
+ }
1400
+ const data = {
1401
+ status: 'COMPLETE',
1402
+ readCount: readItems.length,
1403
+ submittedCount,
1404
+ batchRead: readResult,
1405
+ submitResponse: submitResult,
1406
+ ...(blockedItems.length
1407
+ ? {
1408
+ skippedRequestIds: blockedItems.map((item) => item.requestId),
1409
+ skippedReason: 'isRemark=100 不可批量提交,已跳过',
1410
+ }
1411
+ : {}),
1412
+ };
1413
+ // 回写执行结果(best-effort):重复执行时据此回报"上一次已成功",而不是一句无法分辨的"已使用"。
1414
+ await env.continuation.settle(key, data, 'workflow.batchSubmit');
1415
+ return workflowSuccess('workflow.batchSubmit', data);
1416
+ },
1417
+ };
1418
+ // ===========================================================================
1419
+ // 4. workflow.batchRead(单命令:门禁 → 元数据校验 → 置已读 / 已办)
1420
+ // ===========================================================================
1421
+ const BATCH_READ_INPUT = schemaOf({
1422
+ requestIds: BATCH_REQUEST_IDS_INPUT,
1423
+ batchSize: BATCH_SIZE_INPUT,
1424
+ });
1425
+ const workflowBatchRead = {
1426
+ name: 'workflow.batchRead',
1427
+ resource: 'workflow',
1428
+ method: 'batchRead',
1429
+ action: 'workflow.batchRead',
1430
+ summary: '批量置已读/已办(doBatchRead):一次调用完成门禁校验与写入;必须 confirm=true',
1431
+ capability: 'batch',
1432
+ risk: 'high-risk-write',
1433
+ requiresConfirmation: true,
1434
+ inputSchema: batchWriteSchema(BATCH_READ_INPUT),
1435
+ handler: async (args, env) => {
1436
+ requireBatchConfirm(args, 'workflow.batchRead');
1437
+ const requestIds = collectRequestIds(args);
1438
+ if (!requestIds.length)
1439
+ throw workflowValidationError('request_ids_required', '必须提供 requestIds');
1440
+ const batchSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
1441
+ const { items } = await locateBatchItems(env, 'READ', requestIds, batchSize);
1442
+ const readItems = items.map((it) => ({
1443
+ requestId: String(it.requestId),
1444
+ workflowId: String(it.workflowId ?? ''),
1445
+ nodeid: String(it.nodeid ?? it.nodeId ?? ''),
1446
+ isRemark: String(it.isRemark ?? '0'),
1447
+ userMasterId: String(it.userMasterId ?? it.secondEmpId ?? ''),
1448
+ }));
1449
+ // 元数据门禁(来源:Python cmd_batch_read:缺 workflowId/nodeId 直接失败并提示需来自最近一次批量查询)
1450
+ const missingMeta = readItems.filter((it) => !it.workflowId || !it.nodeid).map((it) => it.requestId);
1451
+ if (missingMeta.length) {
1452
+ throw workflowValidationError('batch_read_meta_missing', `以下流程缺少批量置已读所需元数据(workflowId/节点),无法执行:${missingMeta.join(', ')}。目标需来自最近一次 workflow.search(批量查询)结果,请重新查询后再试`, { requestIds: missingMeta });
1453
+ }
1454
+ const key = await claimBatchWrite(env, 'workflow.batchRead', requestIds);
1455
+ let readCount = 0;
1456
+ let lastResult = null;
1457
+ let unverified = false;
1458
+ for (const batch of chunkArray(readItems, batchSize)) {
1459
+ const payload = batch.map((it) => ({
1460
+ requestId: it.requestId,
1461
+ workflowId: it.workflowId,
1462
+ nodeId: it.nodeid,
1463
+ dataSrc: 0,
1464
+ module: 0,
1465
+ requestCode: '',
1466
+ workflowCode: '',
1467
+ isRemark: Number(it.isRemark) || 0,
1468
+ secondEmpId: it.userMasterId,
1469
+ }));
1470
+ const response = await runWrite('batchRead', env, () => env.host.workflowRequest({
1471
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_READ, operation: 'workflow.batchRead', body: payload,
1472
+ }), payload);
1473
+ lastResult = response;
1474
+ // 成功校验(来源:Python cmd_batch_read_command:`data is not True` 即失败停止)——
1475
+ // 只在明确失败时中止,避免把静默失败当成功上报
1476
+ if (response?.data === false || response?.success === false) {
1477
+ throw workflowValidationError('batch_read_failed', `批量置已读失败(已成功 ${readCount} 条,本批 ${batch.length} 条未生效),已停止`, { readCount, failedBatch: batch.map((it) => it.requestId), response });
1478
+ }
1479
+ if (response?.data !== true && response?.success !== true)
1480
+ unverified = true;
1481
+ readCount += batch.length;
1482
+ }
1483
+ const data = {
1484
+ status: 'COMPLETE',
1485
+ readCount,
1486
+ response: lastResult,
1487
+ ...(unverified
1488
+ ? { resultUnverified: true, resultUnverifiedNotice: '接口未返回明确成功标志,已按提交成功处理;如需确认请重新查询流程状态' }
1489
+ : {}),
1490
+ };
1491
+ await env.continuation.settle(key, data, 'workflow.batchRead');
1492
+ return workflowSuccess('workflow.batchRead', data);
1493
+ },
1494
+ };
1495
+ // ===========================================================================
1496
+ // 5. workflow.batchSupervise(单命令:提醒人解析 → 催办)
1497
+ // ===========================================================================
1498
+ const BATCH_SUPERVISE_INPUT = schemaOf({
1499
+ requestIds: BATCH_REQUEST_IDS_INPUT,
1500
+ remark: { type: 'string' },
1501
+ currentOperator: { type: 'integer', enum: [0, 1], description: '催办当前节点未操作者,默认 1' },
1502
+ messageRemind: { type: 'integer', enum: [0, 1], description: '短信提醒,默认 0' },
1503
+ emailRemind: { type: 'integer', enum: [0, 1], description: '邮件提醒,默认 0' },
1504
+ doneOperator: { type: 'integer', enum: [0, 1], description: '已审批人,默认 0' },
1505
+ ccOperator: { type: 'integer', enum: [0, 1], description: '抄送人,默认 0' },
1506
+ submitOperator: { type: 'integer', enum: [0, 1], description: '发起人,默认 0' },
1507
+ otherReminder: { type: 'string', description: '其他提醒人姓名/ID,逗号分隔,支持 "姓名(ID)"' },
1508
+ batchSize: BATCH_SIZE_INPUT,
1509
+ });
1510
+ /** 解析「其他提醒人」名单:`姓名(ID)` 直取 ID,纯姓名走浏览器候选解析(歧义 / 0 命中直接报错)。 */
1511
+ async function resolveOtherReminders(env, raw) {
1512
+ const value = asString(raw);
1513
+ const ids = [];
1514
+ if (!value)
1515
+ return ids;
1516
+ for (const part of value.split(',').map((item) => item.trim()).filter(Boolean)) {
1517
+ const inline = splitInlineId(part);
1518
+ if (inline.id !== part) {
1519
+ ids.push(inline.id);
1520
+ continue;
1521
+ }
1522
+ const candidates = await browserResolve(env, WORKFLOW_ENDPOINTS.RESOURCE_BROWSER, part, RESOURCE_BROWSER_EXTRA);
1523
+ if (candidates.length === 1)
1524
+ ids.push(candidates[0].id);
1525
+ else if (candidates.length > 1) {
1526
+ throw workflowValidationError('reminder_ambiguous', `提醒人「${part}」匹配到多个候选,请回传 "姓名(ID)"`, {
1527
+ candidates: candidates.map((c, i) => ({ index: i + 1, id: c.id, name: c.name })),
1528
+ });
1529
+ }
1530
+ else {
1531
+ throw workflowValidationError('reminder_not_found', `未匹配到提醒人「${part}」`, { reminderName: part });
1532
+ }
1533
+ }
1534
+ return ids;
1535
+ }
1536
+ const workflowBatchSupervise = {
1537
+ name: 'workflow.batchSupervise',
1538
+ resource: 'workflow',
1539
+ method: 'batchSupervise',
1540
+ action: 'workflow.batchSupervise',
1541
+ summary: '批量催办(batchSupervise):一次调用完成提醒人解析与写入;必须 confirm=true',
1542
+ capability: 'batch',
1543
+ risk: 'high-risk-write',
1544
+ requiresConfirmation: true,
1545
+ inputSchema: batchWriteSchema(BATCH_SUPERVISE_INPUT),
1546
+ handler: async (args, env) => {
1547
+ requireBatchConfirm(args, 'workflow.batchSupervise');
1548
+ const requestIds = collectRequestIds(args);
1549
+ if (!requestIds.length)
1550
+ throw workflowValidationError('request_ids_required', '必须提供 requestIds');
1551
+ const batchSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
1552
+ // 与其它 5 个批量命令**同一门禁**:requestIds 必须来自「最近一次查询结果」快照
1553
+ // (后缀 URGE —— 与 workflow.search --batchType urge 共用同一套批量端点)。
1554
+ // 刻意放在 claimBatchWrite 之前:门禁判不通过时不得占用一次性写入窗口。
1555
+ await locateBatchItems(env, 'URGE', requestIds, batchSize);
1556
+ const flags = {
1557
+ currentOperator: asInt(args.currentOperator) ?? 1,
1558
+ noViewOperator: 0,
1559
+ viewOperator: 0,
1560
+ messageRemind: asInt(args.messageRemind) ?? 0,
1561
+ emailRemind: asInt(args.emailRemind) ?? 0,
1562
+ doneOperator: asInt(args.doneOperator) ?? 0,
1563
+ ccOperator: asInt(args.ccOperator) ?? 0,
1564
+ submitOperator: asInt(args.submitOperator) ?? 0,
1565
+ };
1566
+ const otherReminderIds = await resolveOtherReminders(env, args.otherReminder);
1567
+ const remark = asString(args.remark) ?? '';
1568
+ const session = await env.host.getSession();
1569
+ const key = await claimBatchWrite(env, 'workflow.batchSupervise', requestIds, {
1570
+ // 「催给谁」进键:换提醒对象是另一次操作;remark 不进键(换句意见再催一次仍是重复执行)。
1571
+ flags,
1572
+ otherReminder: otherReminderIds,
1573
+ });
1574
+ const batches = chunkArray(requestIds, batchSize);
1575
+ const result = {
1576
+ success: true,
1577
+ requestIdCount: requestIds.length,
1578
+ batchCount: batches.length,
1579
+ successNum: 0,
1580
+ failNum: 0,
1581
+ batches: [],
1582
+ };
1583
+ for (let i = 0; i < batches.length; i++) {
1584
+ const batch = batches[i];
1585
+ const payload = await withUserId(env, {
1586
+ requestIds: batch.join(','),
1587
+ operateUserIds: session.userId,
1588
+ otherReminder: otherReminderIds.join(','),
1589
+ ...flags,
1590
+ identityId: '0',
1591
+ identityType: 0,
1592
+ remark,
1593
+ });
1594
+ const submitResult = await runWrite('batchSupervise', env, () => env.host.workflowRequest({
1595
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_SUPERVISE, operation: 'workflow.batchSupervise', body: payload,
1596
+ }), payload);
1597
+ const inner = submitResult?.data?.data ?? submitResult?.data ?? submitResult;
1598
+ const successNum = Number(inner?.successNum ?? 0);
1599
+ const failNum = Number(inner?.failNum ?? 0);
1600
+ result.successNum += successNum;
1601
+ result.failNum += failNum;
1602
+ result.batches.push({ batchIndex: i + 1, requestIds: batch, successNum, failNum, resultMessage: String(inner?.resultMessage ?? '') });
1603
+ if (failNum > 0) {
1604
+ result.success = false;
1605
+ result.batchIndex = i + 1;
1606
+ result.userMessage = `批量催办第 ${i + 1}/${batches.length} 批部分失败:成功 ${successNum} 条,失败 ${failNum} 条`;
1607
+ await env.continuation.settle(key, result, 'workflow.batchSupervise');
1608
+ return workflowSuccess('workflow.batchSupervise', result);
1609
+ }
1610
+ }
1611
+ result.userMessage = `已批量催办 ${result.successNum} 条流程。` + (result.failNum ? `失败 ${result.failNum} 条。` : '');
1612
+ await env.continuation.settle(key, result, 'workflow.batchSupervise');
1613
+ return workflowSuccess('workflow.batchSupervise', result);
1614
+ },
1615
+ };
1616
+ // ===========================================================================
1617
+ // 6. workflow.batchShare(单命令:门禁 → 共享对象解析 → 共享)
1618
+ // ===========================================================================
1619
+ const BATCH_SHARE_INPUT = schemaOf({
1620
+ requestIds: BATCH_REQUEST_IDS_INPUT,
1621
+ shareObjects: {
1622
+ type: 'array',
1623
+ items: { type: 'string' },
1624
+ description: '共享对象 JSON 数组字符串,每项 {"type":"user|dept|subcompany|group|role|position|all","name":"名称"}',
1625
+ },
1626
+ minSeclevel: { type: 'string' },
1627
+ maxSeclevel: { type: 'string' },
1628
+ batchSize: BATCH_SIZE_INPUT,
1629
+ });
1630
+ const workflowBatchShare = {
1631
+ name: 'workflow.batchShare',
1632
+ resource: 'workflow',
1633
+ method: 'batchShare',
1634
+ action: 'workflow.batchShare',
1635
+ summary: '批量共享(addRequestShare2,分批):一次调用完成门禁校验与共享对象解析;必须 confirm=true',
1636
+ capability: 'batch',
1637
+ risk: 'high-risk-write',
1638
+ requiresConfirmation: true,
1639
+ inputSchema: batchWriteSchema(BATCH_SHARE_INPUT),
1640
+ handler: async (args, env) => {
1641
+ requireBatchConfirm(args, 'workflow.batchShare');
1642
+ const requestIds = collectRequestIds(args);
1643
+ if (!requestIds.length)
1644
+ throw workflowValidationError('request_ids_required', '必须提供 requestIds');
1645
+ const batchSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
1646
+ const { items } = await locateBatchItems(env, 'SHARE', requestIds, batchSize);
1647
+ const objects = parseShareObjects(args.shareObjects);
1648
+ if (!objects.length)
1649
+ throw workflowValidationError('share_objects_required', '必须提供 --share-objects 共享对象列表');
1650
+ const [defMin, defMax] = await shareDefaultSecLevels(env);
1651
+ const minSec = mergeSecLevel(args.minSeclevel, defMin, true);
1652
+ const maxSec = mergeSecLevel(args.maxSeclevel, defMax, false);
1653
+ const targets = [];
1654
+ for (const obj of objects)
1655
+ targets.push(await resolveShareTarget(env, obj, minSec, maxSec));
1656
+ const sourceItems = items.map((it) => ({
1657
+ requestId: String(it.requestId),
1658
+ userMasterId: String(it.userMasterId ?? it.secondEmpId ?? ''),
1659
+ }));
1660
+ const key = await claimBatchWrite(env, 'workflow.batchShare', requestIds, {
1661
+ // 「共享给谁」进键:换对象是另一次操作,不能被当成重复执行拦下。
1662
+ targets,
1663
+ minSec,
1664
+ maxSec,
1665
+ });
1666
+ const sharedIds = [];
1667
+ let lastResult = null;
1668
+ for (const batch of chunkArray(sourceItems, batchSize)) {
1669
+ const payload = {
1670
+ otherParams: {},
1671
+ shareSourceList: batch.map((it) => ({
1672
+ requestid: String(it.requestId),
1673
+ workflowid: '0',
1674
+ isCreate: 0,
1675
+ createWfId: '0',
1676
+ secondEmpId: String(it.userMasterId ?? ''),
1677
+ })),
1678
+ shareTargetList: targets,
1679
+ };
1680
+ const response = await runWrite('batchShare', env, () => env.host.workflowRequest({
1681
+ method: 'POST', path: WORKFLOW_ENDPOINTS.SHARE_ADD, operation: 'workflow.batchShare', body: payload,
1682
+ }), payload);
1683
+ lastResult = response;
1684
+ sharedIds.push(...batch.map((it) => String(it.requestId)));
1685
+ }
1686
+ const data = { status: 'COMPLETE', sharedCount: sharedIds.length, requestIds: sharedIds, response: lastResult };
1687
+ await env.continuation.settle(key, data, 'workflow.batchShare');
1688
+ return workflowSuccess('workflow.batchShare', data);
1689
+ },
1690
+ };
1691
+ // ===========================================================================
1692
+ // 7. workflow.batchForward(单命令:门禁 → 接收人解析 → 转发)
1693
+ // ===========================================================================
1694
+ const BATCH_FORWARD_INPUT = schemaOf({
1695
+ requestIds: BATCH_REQUEST_IDS_INPUT,
1696
+ receiveTargets: {
1697
+ type: 'array',
1698
+ items: { type: 'string' },
1699
+ description: '转发接收人 JSON 数组字符串,每项 {"type":"user|dept|group|all","name":"名称"};all 不需要 name',
1700
+ },
1701
+ remark: { type: 'string' },
1702
+ batchSize: BATCH_SIZE_INPUT,
1703
+ });
1704
+ const workflowBatchForward = {
1705
+ name: 'workflow.batchForward',
1706
+ resource: 'workflow',
1707
+ method: 'batchForward',
1708
+ action: 'workflow.batchForward',
1709
+ summary: '批量转发(batchForward,分批):一次调用完成门禁校验、接收人解析与转发所有人配置检查;必须 confirm=true',
1710
+ capability: 'batch',
1711
+ risk: 'high-risk-write',
1712
+ requiresConfirmation: true,
1713
+ inputSchema: batchWriteSchema(BATCH_FORWARD_INPUT),
1714
+ handler: async (args, env) => {
1715
+ requireBatchConfirm(args, 'workflow.batchForward');
1716
+ const requestIds = collectRequestIds(args);
1717
+ if (!requestIds.length)
1718
+ throw workflowValidationError('request_ids_required', '必须提供 requestIds');
1719
+ const batchSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
1720
+ const { items } = await locateBatchItems(env, 'FORWARD', requestIds, batchSize);
1721
+ const receivers = parseReceiveTargets(args.receiveTargets);
1722
+ if (!receivers.length)
1723
+ throw workflowValidationError('forward_receivers_required', '必须提供 --receive-targets 转发接收人');
1724
+ if (receivers.some((r) => r.type === 'all') && !await forwardAllowAllEnabled(env)) {
1725
+ throw workflowValidationError('forward_all_not_allowed', '根据系统配置您不可转发给所有人,请联系系统管理员', { doNotRetry: true });
1726
+ }
1727
+ const receiverInfos = [];
1728
+ for (const r of receivers)
1729
+ receiverInfos.push(await resolveForwardReceiver(env, r));
1730
+ const sourceItems = items.map((it) => ({
1731
+ requestId: String(it.requestId),
1732
+ userid: String(it.userid ?? it.userMasterId ?? ''),
1733
+ }));
1734
+ const remark = asString(args.remark) ?? '';
1735
+ const key = await claimBatchWrite(env, 'workflow.batchForward', requestIds, {
1736
+ // 「转给谁」进键:把同一批流程先转给张三、再转给李四,是两次都合法的操作。
1737
+ receiverInfos,
1738
+ });
1739
+ const forwardedIds = [];
1740
+ let lastResult = null;
1741
+ for (const batch of chunkArray(sourceItems, batchSize)) {
1742
+ const payload = {
1743
+ src: 'forward',
1744
+ receiveOperatorInfos: receiverInfos,
1745
+ remark,
1746
+ attachments: [],
1747
+ fileData: [],
1748
+ locationData: {},
1749
+ comment: {
1750
+ relevanceList: [],
1751
+ imAtContent: remark,
1752
+ atLinkList: [],
1753
+ content: remark,
1754
+ originContent: remark,
1755
+ },
1756
+ quoteData: [],
1757
+ fullTextAnnotationData: [],
1758
+ signatureData: { signatureImgId: '' },
1759
+ userSignatureData: { signatureImgId: '' },
1760
+ identityId: '0',
1761
+ identityType: 0,
1762
+ requestIds: batch.map((it) => it.requestId).join(','),
1763
+ operateUserIds: batch.map((it) => it.userid).join(','),
1764
+ clientType: 0,
1765
+ };
1766
+ const response = await runWrite('batchForward', env, () => env.host.workflowRequest({
1767
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_FORWARD, operation: 'workflow.batchForward', body: payload,
1768
+ }), payload);
1769
+ lastResult = response;
1770
+ const submitResults = responseData(response)?.data?.submitResult ?? [];
1771
+ if (Array.isArray(submitResults) && submitResults.length) {
1772
+ const failures = submitResults.filter((sr) => sr?.status !== true);
1773
+ if (failures.length) {
1774
+ const partial = {
1775
+ status: 'PARTIAL',
1776
+ success: false,
1777
+ forwardedCount: forwardedIds.length,
1778
+ failedRequestIds: failures.map((f) => String(f.requestId ?? '')),
1779
+ failures: failures.map((f) => ({ requestId: String(f.requestId ?? ''), resultMessage: String(f.resultMessage ?? '转发失败') })),
1780
+ response,
1781
+ };
1782
+ await env.continuation.settle(key, partial, 'workflow.batchForward');
1783
+ return workflowSuccess('workflow.batchForward', partial);
1784
+ }
1785
+ }
1786
+ forwardedIds.push(...batch.map((it) => it.requestId));
1787
+ }
1788
+ const data = { status: 'COMPLETE', forwardedCount: forwardedIds.length, requestIds: forwardedIds, response: lastResult };
1789
+ await env.continuation.settle(key, data, 'workflow.batchForward');
1790
+ return workflowSuccess('workflow.batchForward', data);
1791
+ },
1792
+ };
1793
+ // ===========================================================================
1794
+ // 8. workflow.batchReject(单命令:门禁 → 退回)
1795
+ // ===========================================================================
1796
+ const BATCH_REJECT_INPUT = schemaOf({
1797
+ requestIds: BATCH_REQUEST_IDS_INPUT,
1798
+ remark: { type: 'string' },
1799
+ batchSize: BATCH_SIZE_INPUT,
1800
+ });
1801
+ const workflowBatchReject = {
1802
+ name: 'workflow.batchReject',
1803
+ resource: 'workflow',
1804
+ method: 'batchReject',
1805
+ action: 'workflow.batchReject',
1806
+ summary: '批量退回(batchReject,分批;不传 batchSubmitListParams):一次调用完成门禁校验与退回;必须 confirm=true',
1807
+ capability: 'batch',
1808
+ risk: 'high-risk-write',
1809
+ requiresConfirmation: true,
1810
+ inputSchema: batchWriteSchema(BATCH_REJECT_INPUT),
1811
+ handler: async (args, env) => {
1812
+ requireBatchConfirm(args, 'workflow.batchReject');
1813
+ const requestIds = collectRequestIds(args);
1814
+ if (!requestIds.length)
1815
+ throw workflowValidationError('request_ids_required', '必须提供 requestIds');
1816
+ const batchSize = asInt(args.batchSize) ?? BATCH_SIZE_DEFAULT;
1817
+ const { items } = await locateBatchItems(env, 'REJECT', requestIds, batchSize);
1818
+ const sourceItems = items.map((it) => ({
1819
+ requestId: String(it.requestId),
1820
+ userid: String(it.userid ?? it.userMasterId ?? ''),
1821
+ }));
1822
+ const remark = asString(args.remark) ?? '';
1823
+ const key = await claimBatchWrite(env, 'workflow.batchReject', requestIds);
1824
+ const rejectedIds = [];
1825
+ let lastResult = null;
1826
+ for (const batch of chunkArray(sourceItems, batchSize)) {
1827
+ const payload = {
1828
+ isMobile: false,
1829
+ isAsync: false,
1830
+ conditionId: '',
1831
+ requestIds: batch.map((it) => it.requestId).join(','),
1832
+ operateUserIds: batch.map((it) => it.userid).join(','),
1833
+ remark,
1834
+ handWrittenSign: '',
1835
+ comment: {
1836
+ relevanceList: [],
1837
+ imAtContent: remark,
1838
+ atLinkList: [],
1839
+ content: remark,
1840
+ originContent: remark,
1841
+ },
1842
+ };
1843
+ const response = await runWrite('batchReject', env, () => env.host.workflowRequest({
1844
+ method: 'POST', path: WORKFLOW_ENDPOINTS.BATCH_REJECT, operation: 'workflow.batchReject', body: payload,
1845
+ }), payload);
1846
+ lastResult = response;
1847
+ const operateResults = responseData(response)?.data?.operateResult ?? [];
1848
+ if (Array.isArray(operateResults) && operateResults.length) {
1849
+ const failures = operateResults.filter((or) => or?.status !== true);
1850
+ if (failures.length) {
1851
+ const partial = {
1852
+ status: 'PARTIAL',
1853
+ success: false,
1854
+ rejectedCount: rejectedIds.length,
1855
+ failedRequestIds: failures.map((f) => String(f.requestId ?? '')),
1856
+ failures: failures.map((f) => ({ requestId: String(f.requestId ?? ''), resultMessage: String(f.resultMessage ?? '退回失败') })),
1857
+ response,
1858
+ };
1859
+ await env.continuation.settle(key, partial, 'workflow.batchReject');
1860
+ return workflowSuccess('workflow.batchReject', partial);
1861
+ }
1862
+ }
1863
+ rejectedIds.push(...batch.map((it) => it.requestId));
1864
+ }
1865
+ const data = { status: 'COMPLETE', rejectedCount: rejectedIds.length, requestIds: rejectedIds, response: lastResult };
1866
+ await env.continuation.settle(key, data, 'workflow.batchReject');
1867
+ return workflowSuccess('workflow.batchReject', data);
1868
+ },
1869
+ };
1870
+ // ===========================================================================
1871
+ // 9. workflow.memoryExtract(requiresConfirmation = false,对话结束静默保存)
1872
+ // ===========================================================================
1873
+ const MEMORY_INPUT = schemaOf({
1874
+ msgList: {
1875
+ type: 'array',
1876
+ items: { type: 'object' },
1877
+ description: '记忆消息数组 [{msgRole:"user"|"assistant", msgValue:"..."}]',
1878
+ },
1879
+ userMessage: { type: 'array', items: { type: 'string' }, description: '用户消息列表(与 msgList 二选一)' },
1880
+ assistantMessage: { type: 'array', items: { type: 'string' }, description: '模型消息列表(与 msgList 二选一)' },
1881
+ dynamicContext: { type: 'object', description: '动态上下文参数(合并进 dynamicContextParams)' },
1882
+ workflowId: { type: 'string', description: '关联工作流 ID' },
1883
+ memoryAppId: { type: 'string', description: '记忆应用 ID' },
1884
+ async: { type: 'boolean', description: '语义为同步发送并返回真实结果(禁止 fire-and-forget)' },
1885
+ });
1886
+ function normalizeMemoryMsgList(value) {
1887
+ const arr = Array.isArray(value) ? value : [];
1888
+ const out = [];
1889
+ const aliases = { 用户: 'user', 模型: 'assistant', ai: 'assistant' };
1890
+ for (const item of arr) {
1891
+ if (!item || typeof item !== 'object')
1892
+ throw workflowValidationError('msg_list_invalid', 'msgList 数组项必须是对象');
1893
+ const it = item;
1894
+ let role = asString(it.msgRole) ?? asString(it.role);
1895
+ if (!role)
1896
+ throw workflowValidationError('msg_role_required', 'msgList 项缺少 msgRole/role');
1897
+ role = aliases[role.toLowerCase()] ?? role;
1898
+ if (role !== 'user' && role !== 'assistant') {
1899
+ throw workflowValidationError('msg_role_invalid', `msgList.msgRole 只能是 user 或 assistant(收到 ${role})`);
1900
+ }
1901
+ const val = asString(it.msgValue) ?? asString(it.content) ?? asString(it.message);
1902
+ if (!val)
1903
+ continue;
1904
+ out.push({ msgRole: role, msgValue: val });
1905
+ }
1906
+ if (!out.length)
1907
+ throw workflowValidationError('msg_list_empty', 'msgList 不能为空');
1908
+ return out;
1909
+ }
1910
+ const workflowMemoryExtract = {
1911
+ name: 'workflow.memoryExtract',
1912
+ resource: 'workflow',
1913
+ method: 'memoryExtract',
1914
+ action: 'workflow.memoryExtract',
1915
+ summary: '抽取并保存对话记忆(agentMemoryExtract);对话结束静默保存,返回真实 saved 结果,禁止 fire-and-forget',
1916
+ capability: 'memory',
1917
+ risk: 'write',
1918
+ requiresConfirmation: false,
1919
+ inputSchema: MEMORY_INPUT,
1920
+ handler: async (args, env) => {
1921
+ let msgList = normalizeMemoryMsgList(args.msgList);
1922
+ const userMessages = asStringArray(args.userMessage, 'userMessage');
1923
+ const assistantMessages = asStringArray(args.assistantMessage, 'assistantMessage');
1924
+ if (!msgList.length && (userMessages || assistantMessages)) {
1925
+ const built = [];
1926
+ for (const m of userMessages ?? [])
1927
+ if (m)
1928
+ built.push({ msgRole: 'user', msgValue: m });
1929
+ for (const m of assistantMessages ?? [])
1930
+ if (m)
1931
+ built.push({ msgRole: 'assistant', msgValue: m });
1932
+ msgList = normalizeMemoryMsgList(built);
1933
+ }
1934
+ // 末尾补 remember 保存触发信号词
1935
+ if (!msgList.length || msgList[msgList.length - 1].msgValue !== 'remember') {
1936
+ msgList = [...msgList, { msgRole: 'user', msgValue: 'remember' }];
1937
+ }
1938
+ const workflowId = asString(args.workflowId);
1939
+ const extra = (args.dynamicContext && typeof args.dynamicContext === 'object' ? args.dynamicContext : {});
1940
+ const dynamicContextParams = { ...extra };
1941
+ if (workflowId)
1942
+ dynamicContextParams.workflowId = workflowId;
1943
+ const chatParams = {
1944
+ // appid 必须始终携带(缺省时记忆接口判为无效应用、静默不保存)
1945
+ appid: resolveMemoryAppId(asString(args.memoryAppId)),
1946
+ dynamicContextParams,
1947
+ };
1948
+ const payload = { chatParams, aiMsgEntityList: msgList };
1949
+ let saved = false;
1950
+ let result = null;
1951
+ try {
1952
+ result = await env.host.workflowRequest({
1953
+ method: 'POST',
1954
+ path: WORKFLOW_ENDPOINTS.MEMORY_EXTRACT,
1955
+ operation: 'workflow.memoryExtract',
1956
+ body: payload,
1957
+ });
1958
+ // 记忆接口业务标志是 status/fail(不是 success);只在明确失败时判未保存
1959
+ const body = (result && typeof result === 'object' ? result : {});
1960
+ const explicitFalse = body.saved === false || body.success === false || body.status === false || body.fail === true;
1961
+ saved = !explicitFalse && (body.saved === true || body.success === true || body.status === true);
1962
+ if (!saved && !explicitFalse)
1963
+ saved = body.code === 200 || body.fail === false;
1964
+ }
1965
+ catch {
1966
+ saved = false;
1967
+ }
1968
+ return workflowSuccess('workflow.memoryExtract', {
1969
+ saved,
1970
+ messageCount: msgList.length,
1971
+ workflowId: workflowId ?? '',
1972
+ result,
1973
+ });
1974
+ },
1975
+ };
1976
+ // ===========================================================================
1977
+ // 导出
1978
+ // ===========================================================================
1979
+ export const workflowWriteOperations = [
1980
+ workflowOperatePrepare,
1981
+ workflowOperateApply,
1982
+ workflowSharePrepare,
1983
+ workflowShareApply,
1984
+ workflowBatchSubmit,
1985
+ workflowBatchRead,
1986
+ workflowBatchSupervise,
1987
+ workflowBatchShare,
1988
+ workflowBatchForward,
1989
+ workflowBatchReject,
1990
+ workflowMemoryExtract,
1991
+ ];