weaver-work-cli 0.1.8 → 0.1.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/README.md +11 -11
  2. package/dist/cmd/action.js +76 -0
  3. package/dist/cmd/index.js +2 -0
  4. package/dist/cmd/refs/index.js +103 -0
  5. package/dist/cmd/register.js +31 -0
  6. package/dist/cmd/root.js +1 -0
  7. package/dist/cmd/setup/index.js +25 -5
  8. package/dist/cmd/skills/index.js +37 -5
  9. package/dist/core/connector.js +54 -0
  10. package/dist/core/context.js +2 -1
  11. package/dist/core/input.js +48 -0
  12. package/dist/core/output.js +298 -3
  13. package/dist/core/refs.js +432 -0
  14. package/dist/core/state.js +26 -0
  15. package/dist/internal/e10/auth/commands.js +2 -2
  16. package/dist/internal/e10/auth/session.js +14 -0
  17. package/dist/internal/e10/auth/xiaoe.js +116 -51
  18. package/dist/internal/e10/request-runtime.js +5 -0
  19. package/dist/internal/skills/install.js +148 -28
  20. package/dist/internal/skills/reader.js +12 -9
  21. package/dist/shortcuts/blog/continuation.js +74 -0
  22. package/dist/shortcuts/blog/errors.js +60 -0
  23. package/dist/shortcuts/blog/host.js +175 -0
  24. package/dist/shortcuts/blog/index.js +324 -0
  25. package/dist/shortcuts/blog/manifest.js +415 -0
  26. package/dist/shortcuts/blog/operations/comment.js +125 -0
  27. package/dist/shortcuts/blog/operations/comments.js +31 -0
  28. package/dist/shortcuts/blog/operations/edit.js +131 -0
  29. package/dist/shortcuts/blog/operations/export.js +46 -0
  30. package/dist/shortcuts/blog/operations/like.js +80 -0
  31. package/dist/shortcuts/blog/operations/publish.js +157 -0
  32. package/dist/shortcuts/blog/operations/query-user.js +58 -0
  33. package/dist/shortcuts/blog/operations/query.js +96 -0
  34. package/dist/shortcuts/blog/operations/registry.js +54 -0
  35. package/dist/shortcuts/blog/operations/remind.js +176 -0
  36. package/dist/shortcuts/blog/operations/shared.js +330 -0
  37. package/dist/shortcuts/blog/operations/types.js +16 -0
  38. package/dist/shortcuts/blog/operations.js +9 -0
  39. package/dist/shortcuts/caizhilian/continuation.js +70 -0
  40. package/dist/shortcuts/caizhilian/errors.js +53 -0
  41. package/dist/shortcuts/caizhilian/host.js +190 -0
  42. package/dist/shortcuts/caizhilian/index.js +186 -0
  43. package/dist/shortcuts/caizhilian/manifest.js +42 -0
  44. package/dist/shortcuts/caizhilian/operations/api.js +801 -0
  45. package/dist/shortcuts/caizhilian/operations/confirm.js +75 -0
  46. package/dist/shortcuts/caizhilian/operations/files.js +306 -0
  47. package/dist/shortcuts/caizhilian/operations/form.js +170 -0
  48. package/dist/shortcuts/caizhilian/operations/handlers/collab.js +611 -0
  49. package/dist/shortcuts/caizhilian/operations/handlers/document.js +890 -0
  50. package/dist/shortcuts/caizhilian/operations/handlers/file-collection.js +346 -0
  51. package/dist/shortcuts/caizhilian/operations/handlers/file-upload.js +192 -0
  52. package/dist/shortcuts/caizhilian/operations/handlers/fileops.js +570 -0
  53. package/dist/shortcuts/caizhilian/operations/handlers/folder.js +969 -0
  54. package/dist/shortcuts/caizhilian/operations/handlers/search.js +318 -0
  55. package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +294 -0
  56. package/dist/shortcuts/caizhilian/operations/handlers/setting-backup.js +423 -0
  57. package/dist/shortcuts/caizhilian/operations/handlers/setting-encrypt.js +238 -0
  58. package/dist/shortcuts/caizhilian/operations/handlers/setting-storage.js +364 -0
  59. package/dist/shortcuts/caizhilian/operations/handlers/system.js +106 -0
  60. package/dist/shortcuts/caizhilian/operations/registry.js +151 -0
  61. package/dist/shortcuts/caizhilian/operations/shared.js +408 -0
  62. package/dist/shortcuts/caizhilian/operations/specs.js +623 -0
  63. package/dist/shortcuts/caizhilian/operations/types.js +57 -0
  64. package/dist/shortcuts/caizhilian/operations.js +21 -0
  65. package/dist/shortcuts/contract/continuation.js +70 -0
  66. package/dist/shortcuts/contract/errors.js +62 -0
  67. package/dist/shortcuts/contract/host.js +205 -0
  68. package/dist/shortcuts/contract/index.js +318 -0
  69. package/dist/shortcuts/contract/manifest.js +596 -0
  70. package/dist/shortcuts/contract/operations/clause-reference.js +78 -0
  71. package/dist/shortcuts/contract/operations/form-info.js +371 -0
  72. package/dist/shortcuts/contract/operations/read.js +118 -0
  73. package/dist/shortcuts/contract/operations/registry.js +43 -0
  74. package/dist/shortcuts/contract/operations/request.js +300 -0
  75. package/dist/shortcuts/contract/operations/shared.js +133 -0
  76. package/dist/shortcuts/contract/operations/subject.js +157 -0
  77. package/dist/shortcuts/contract/operations/template.js +81 -0
  78. package/dist/shortcuts/contract/operations/types.js +7 -0
  79. package/dist/shortcuts/contract/operations/workflow.js +52 -0
  80. package/dist/shortcuts/contract/operations/write.js +131 -0
  81. package/dist/shortcuts/contract/operations.js +8 -0
  82. package/dist/shortcuts/hrm/index.js +83 -2
  83. package/dist/shortcuts/hrm/operations/employee.js +160 -30
  84. package/dist/shortcuts/hrm/operations/org.js +17 -3
  85. package/dist/shortcuts/hrm/operations/position.js +18 -2
  86. package/dist/shortcuts/index.js +5 -1
  87. package/dist/shortcuts/invoice/index.js +74 -36
  88. package/dist/shortcuts/invoice/manifest.js +674 -4
  89. package/dist/shortcuts/invoice/markdown.js +172 -0
  90. package/dist/shortcuts/invoice/operations/add.js +3 -3
  91. package/dist/shortcuts/invoice/operations/delete.js +2 -2
  92. package/dist/shortcuts/invoice/operations/get.js +10 -2
  93. package/dist/shortcuts/invoice/operations/import.js +2 -2
  94. package/dist/shortcuts/invoice/operations/registry.js +8 -1
  95. package/dist/shortcuts/invoice/operations/reim-auto.js +719 -0
  96. package/dist/shortcuts/invoice/operations/reim.js +85 -2
  97. package/dist/shortcuts/invoice/operations/shared.js +75 -8
  98. package/dist/shortcuts/invoice/operations/update.js +2 -2
  99. package/dist/shortcuts/invoice/operations/upload-and-validate.js +93 -0
  100. package/dist/shortcuts/invoice/operations/validate-preview.js +2 -1
  101. package/dist/shortcuts/invoice/projection.js +328 -0
  102. package/dist/shortcuts/meeting/host.js +18 -1
  103. package/dist/shortcuts/meeting/manifest.js +30 -18
  104. package/dist/shortcuts/meeting/operations/eb/catalog.js +390 -0
  105. package/dist/shortcuts/meeting/operations/eb/conflict.js +389 -0
  106. package/dist/shortcuts/meeting/operations/eb/factory.js +389 -0
  107. package/dist/shortcuts/meeting/operations/eb/index.js +22 -0
  108. package/dist/shortcuts/meeting/operations/env.js +8 -1
  109. package/dist/shortcuts/meeting/operations/registry.js +2 -0
  110. package/dist/shortcuts/meeting/operations/signs.js +216 -2
  111. package/dist/shortcuts/workflow/endpoints.js +2 -1
  112. package/dist/shortcuts/workflow/errors.js +8 -2
  113. package/dist/shortcuts/workflow/host.js +6 -3
  114. package/dist/shortcuts/workflow/operations/form.js +931 -589
  115. package/dist/shortcuts/workflow/operations/read.js +128 -205
  116. package/dist/shortcuts/workflow/operations/shared.js +21 -0
  117. package/dist/shortcuts/workflow/operations/write.js +750 -52
  118. package/dist/shortcuts/workflow/operations.js +36 -10
  119. package/docs/_catalog.md +6 -0
  120. package/docs/agent-invoice.md +5 -1
  121. package/docs/agent-io-channel-design.md +468 -0
  122. package/docs/agent-skill-install.md +9 -4
  123. package/docs/blog.md +123 -0
  124. package/docs/caizhilian.md +304 -0
  125. package/docs/contract.md +91 -0
  126. package/docs/e10-auth.md +2 -2
  127. package/docs/hrm.md +19 -9
  128. package/docs/invoice.md +5 -1
  129. package/docs/meeting.md +1 -1
  130. package/docs/operation-manual.md +15 -3
  131. package/docs/skill-review-2026-09-19.md +2 -2
  132. package/package.json +2 -2
  133. package/scripts/package-skill.mjs +32 -20
  134. package/skill-template/business-info.json +113 -14
  135. package/skill-template/domains/blog.md +24 -0
  136. package/skill-template/domains/caizhilian.md +22 -0
  137. package/skill-template/domains/jinchengda.md +23 -0
  138. package/skill-template/domains/meeting.md +1 -1
  139. package/skill-template/domains/workflow.md +1 -1
  140. package/skill-template/skill-template.md +1 -1
  141. package/skills/weaver-e10-blog/SKILL.md +111 -0
  142. package/skills/weaver-e10-blog/references/blog-comment.md +51 -0
  143. package/skills/weaver-e10-blog/references/blog-comments.md +39 -0
  144. package/skills/weaver-e10-blog/references/blog-default-permission.md +38 -0
  145. package/skills/weaver-e10-blog/references/blog-disabled-capabilities.md +11 -0
  146. package/skills/weaver-e10-blog/references/blog-edit.md +51 -0
  147. package/skills/weaver-e10-blog/references/blog-export.md +42 -0
  148. package/skills/weaver-e10-blog/references/blog-like.md +50 -0
  149. package/skills/weaver-e10-blog/references/blog-publish.md +52 -0
  150. package/skills/weaver-e10-blog/references/blog-query-user.md +45 -0
  151. package/skills/weaver-e10-blog/references/blog-query.md +49 -0
  152. package/skills/weaver-e10-blog/references/blog-remind.md +51 -0
  153. package/skills/weaver-e10-blog/references/blog-resolve-users.md +38 -0
  154. package/skills/weaver-e10-blog/references/source-manifest.json +1540 -0
  155. package/skills/weaver-e10-caizhilian/SKILL.md +216 -0
  156. package/skills/weaver-e10-caizhilian/references/agent-entry.md +106 -0
  157. package/skills/weaver-e10-caizhilian/references/collaboration.md +112 -0
  158. package/skills/weaver-e10-caizhilian/references/document.md +111 -0
  159. package/skills/weaver-e10-caizhilian/references/file-collection.md +136 -0
  160. package/skills/weaver-e10-caizhilian/references/file-upload.md +309 -0
  161. package/skills/weaver-e10-caizhilian/references/files.md +120 -0
  162. package/skills/weaver-e10-caizhilian/references/folder.md +118 -0
  163. package/skills/weaver-e10-caizhilian/references/org-browser.md +104 -0
  164. package/skills/weaver-e10-caizhilian/references/setting-attach.md +410 -0
  165. package/skills/weaver-e10-caizhilian/references/setting-backup.md +372 -0
  166. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +399 -0
  167. package/skills/weaver-e10-caizhilian/references/setting-storage.md +603 -0
  168. package/skills/weaver-e10-caizhilian/references/source-manifest.json +5079 -0
  169. package/skills/weaver-e10-caizhilian/references/withheld.md +93 -0
  170. package/skills/weaver-e10-caizhilian/references/write-protocol.md +120 -0
  171. package/skills/weaver-e10-calendar/SKILL.md +4 -3
  172. package/skills/weaver-e10-ebuilder-form/SKILL.md +4 -9
  173. package/skills/weaver-e10-ebuilder-form/references/context-routing.md +0 -9
  174. package/skills/weaver-e10-ebuilder-form/references/custom-api.md +1 -5
  175. package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +1 -5
  176. package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +1 -5
  177. package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +1 -5
  178. package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +1 -5
  179. package/skills/weaver-e10-esb/SKILL.md +2 -2
  180. package/skills/weaver-e10-esb/references/source-manifest.json +9 -9
  181. package/skills/weaver-e10-hrm/SKILL.md +54 -6
  182. package/skills/weaver-e10-hrm/references/employee.md +89 -15
  183. package/skills/weaver-e10-hrm/references/org.md +7 -4
  184. package/skills/weaver-e10-hrm/references/position.md +14 -7
  185. package/skills/weaver-e10-hrm/references/source-manifest.json +1 -1
  186. package/skills/weaver-e10-jinchengda/SKILL.md +166 -0
  187. package/skills/weaver-e10-jinchengda/references/contract-agent-entry.md +82 -0
  188. package/skills/weaver-e10-jinchengda/references/contract-clause-recommender.md +69 -0
  189. package/skills/weaver-e10-jinchengda/references/contract-clause-reference-save.md +78 -0
  190. package/skills/weaver-e10-jinchengda/references/contract-disabled-capabilities.md +27 -0
  191. package/skills/weaver-e10-jinchengda/references/contract-request-status.md +71 -0
  192. package/skills/weaver-e10-jinchengda/references/contract-request-write.md +94 -0
  193. package/skills/weaver-e10-jinchengda/references/contract-signing-subjects.md +89 -0
  194. package/skills/weaver-e10-jinchengda/references/contract-subject-create.md +77 -0
  195. package/skills/weaver-e10-jinchengda/references/contract-template-selector.md +66 -0
  196. package/skills/weaver-e10-jinchengda/references/contract-workflow-discovery.md +94 -0
  197. package/skills/weaver-e10-jinchengda/references/source-manifest.json +259 -0
  198. package/skills/weaver-e10-jiuchuanhui/SKILL.md +5 -11
  199. package/skills/weaver-e10-jucailin/SKILL.md +4 -3
  200. package/skills/weaver-e10-mail/SKILL.md +4 -11
  201. package/skills/weaver-e10-meeting/SKILL.md +66 -32
  202. package/skills/weaver-e10-meeting/references/meeting-change.md +5 -2
  203. package/skills/weaver-e10-meeting/references/meeting-create.md +2 -1
  204. package/skills/weaver-e10-meeting/references/meeting-eb.md +114 -0
  205. package/skills/weaver-e10-meeting/references/meeting-lifecycle.md +9 -8
  206. package/skills/weaver-e10-meeting/references/meeting-receipts-signs.md +35 -7
  207. package/skills/weaver-e10-meeting/references/meeting-update.md +1 -1
  208. package/skills/weaver-e10-okr/SKILL.md +6 -11
  209. package/skills/weaver-e10-okr/references/source-manifest.json +3 -3
  210. package/skills/weaver-e10-plan/SKILL.md +3 -3
  211. package/skills/weaver-e10-plan/references/plan-collect-and-compose.md +1 -1
  212. package/skills/weaver-e10-plan/references/plan-overview-and-routing.md +1 -1
  213. package/skills/weaver-e10-plan/references/source-manifest.json +13 -13
  214. package/skills/weaver-e10-qiyecheng/SKILL.md +4 -10
  215. package/skills/weaver-e10-qiyecheng/references/source-manifest.json +9 -9
  216. package/skills/weaver-e10-shared/SKILL.md +36 -25
  217. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +10 -31
  218. package/skills/weaver-e10-shared/references/json-output-contract.md +20 -11
  219. package/skills/weaver-e10-shijingran/SKILL.md +2 -1
  220. package/skills/weaver-e10-shijingran/references/source-manifest.json +9 -9
  221. package/skills/weaver-e10-skill-maker/SKILL.md +4 -10
  222. package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +5 -3
  223. package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +12 -27
  224. package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +17 -5
  225. package/skills/weaver-e10-skill-maker/references/source-update-detection.md +4 -27
  226. package/skills/weaver-e10-wenshuding/SKILL.md +5 -16
  227. package/skills/weaver-e10-wenshuding/references/source-manifest.json +7 -7
  228. package/skills/weaver-e10-workflow/SKILL.md +32 -37
  229. package/skills/weaver-e10-workflow/references/source-manifest.json +39 -75
  230. package/skills/weaver-e10-workflow/references/workflow-batch.md +90 -29
  231. package/skills/weaver-e10-workflow/references/workflow-create.md +79 -109
  232. package/skills/weaver-e10-workflow/references/workflow-memory.md +19 -9
  233. package/skills/weaver-e10-workflow/references/workflow-operate.md +2 -2
  234. package/skills/weaver-e10-workflow/references/workflow-query.md +38 -20
  235. package/skills/weaver-e10-workflow/references/workflow-share.md +18 -2
  236. package/skills/weaver-e10-workflow/references/workflow-view.md +3 -4
  237. package/skills/weaver-e10-yepiaotong/SKILL.md +75 -39
  238. package/skills/weaver-e10-yepiaotong/references/invoice-detail.md +22 -1
  239. package/skills/weaver-e10-yepiaotong/references/invoice-enterprise-list.md +3 -1
  240. package/skills/weaver-e10-yepiaotong/references/invoice-output-view.md +135 -0
  241. package/skills/weaver-e10-yepiaotong/references/invoice-personal-list.md +15 -3
  242. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +1 -1
  243. package/skills/weaver-e10-yepiaotong/references/invoice-summarize.py +217 -0
  244. package/skills/weaver-e10-yepiaotong/references/invoice-upload-and-validate.md +88 -0
  245. package/skills/weaver-e10-yepiaotong/references/invoice-validation-preview.md +7 -2
  246. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +89 -9
  247. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +1 -0
  248. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +70 -49
  249. package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +119 -0
  250. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +55 -36
  251. package/skills/weaver-e10-yepiaotong/references/reim-matching-rules.md +4 -0
  252. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +149 -0
  253. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +125 -5
  254. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +430 -63
  255. package/skills/weaver-e10-yimiaoban/SKILL.md +4 -12
  256. package/skills/weaver-e10-yimiaoban/references/source-manifest.json +3 -3
  257. package/skills/weaver-e10-ziguanjia/SKILL.md +2 -1
  258. package/skills/weaver-e10-ziguanjia/references/ziguanjia-agent-entry.md +3 -3
  259. package/tools/weaver-skill-detector-1.0.13/SKILL.md +9 -9
  260. package/tools/weaver-skill-detector-1.0.13/references/checklist.md +3 -3
  261. package/tools/weaver-skill-detector-1.0.13/scripts/__pycache__/validate_skill.cpython-313.pyc +0 -0
  262. package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +98 -91
  263. package/scripts/sync-connector-skills.mjs +0 -172
@@ -1,21 +1,25 @@
1
1
  /**
2
- * workflow/form.ts - 发起流程(create)与修改流程表单(edit)的 7 个 operation。
2
+ * workflow/form.ts - 发起流程(create)与修改流程表单(edit)的 operation 集合。
3
3
  *
4
4
  * 移植自 Python skill e10_workflow_create.py(4,963 行),行为须与该脚本逐条对齐。
5
5
  * 认证由 CLI 托管(host 注入会话),本文件不实现登录/缓存(verify_login_auth /
6
- * atrust_cookie_diagnosis / get_runtime / state/create_contexts 一律丢弃)。
6
+ * atrust_cookie_ diagnosis / get_runtime / state/create_contexts 一律丢弃)。
7
7
  *
8
8
  * 原 Python 用磁盘文件保存「上下文(context)」,本 TS 模块改用 continuation 令牌承载上下文:
9
- * createForm -> 发出 continuation(operation=workflow.createForm) 内嵌表单上下文
10
- * createPrepare -> 校验该 continuation,产出预览 + 发出 continuation(operation=workflow.createPrepare)
11
- * createApply -> 校验该 continuation,复核只读字段后 save+create
9
+ * createForm -> 解析可发起工作流(名称→ID,原 createList 的职责)→ 载入表单上下文,
10
+ * 发出 continuation(operation=workflow.createForm) 内嵌该上下文
11
+ * createRequest -> 校验该 continuation,按 action 一次完成「联动 + 必填校验 + 保存 + 发起」
12
12
  * editLoad / editPrepare / editSave 同理(operation 分别为 workflow.editLoad / workflow.editPrepare)。
13
+ *
14
+ * 发起链路**只有两条命令**(2026-09-21 用户裁定,从原 4 条 createList/createForm/createPrepare/
15
+ * createApply 合并而来):模型只需「取表单 → 填字段 → createRequest」,不必自己判断必填是否缺失
16
+ * ——必填判定完全在 CLI 内部按实际所填数据(含联动带出值)实时计算。
13
17
  */
14
18
  import { randomUUID } from 'node:crypto';
15
19
  import { workflowSuccess, schemaOf, SCHEMA_REQUEST_ID, SCHEMA_CONTINUATION, SCHEMA_CONFIRM, } from './types.js';
16
- import { responseData, toArray, withLeadingMeta, stdoutBytesOf, STDOUT_SAFE_BYTES, } from './shared.js';
17
- import { workflowValidationError, workflowApiError, workflowPartialError, workflowPolicyError, WorkflowCliError, } from '../errors.js';
18
- import { FORM_ENDPOINTS, AJAX_QUERY, formApiUrl } from '../endpoints.js';
20
+ import { responseData, toArray, withLeadingMeta, stdoutBytesOf, STDOUT_SAFE_BYTES, parseNameWithId, resolveMemoryAppId, } from './shared.js';
21
+ import { workflowValidationError, workflowApiError, workflowPartialError, workflowPolicyError, workflowClarificationError, WorkflowCliError, } from '../errors.js';
22
+ import { FORM_ENDPOINTS, WORKFLOW_ENDPOINTS, AJAX_QUERY, formApiUrl } from '../endpoints.js';
19
23
  import { continuationReplayError } from '../continuation.js';
20
24
  import { writeStateFileBestEffort } from '../state.js';
21
25
  // ---------------------------------------------------------------------------
@@ -82,11 +86,20 @@ function encodeFieldsCompact(fields) {
82
86
  * (实测某次会话为此多写 3 个脚本、3 次被 head 截断,白花 6 次工具往返)。
83
87
  * 这里补一份**只含可写字段**的紧凑索引(行 = `id|name|subFormId|componentKey|flags`),
84
88
  * 体积很小(某 278 字段表单实测 3.7KB,其余字段 17.5KB,合计 21.2KB < 安全线),
85
- * 足以让模型直接构造 `data` 的键;更细的元数据(browserModule / fileUploadParams / 完整 options)
86
- * 仍按字段 id 去 resultFile 取。纯结构裁剪,不做任何语义判断。
89
+ * 足以让模型直接构造 `data` 的键。
90
+ *
91
+ * V12.1.37(2026-09-23):把两类**填值前就要核对、又恰好不在行内**的属性随索引下发 ——
92
+ * 它们此前只能去 resultFile 取,实测就是那次「二次读取」的主要动因:
93
+ * - `formatIndex`(字段 id → `format`):日期/时间字段的格式(`yyyy-MM-dd` 等)。只有 `T` 标记时
94
+ * 模型知道"有格式要求"却不知道要求什么,只能读 resultFile;实测某会话为它多跑一轮 ≈11 s。
95
+ * - `uploadLimits`(字段 id → `大小上限|格式限制|数量上限`):附件字段的上传限制,见 `uploadLimitOf`。
96
+ *
97
+ * 仍需按字段 id 去 resultFile 取的只剩 `browserModule` / `browserType` / 完整 `options` 结构等
98
+ * 低频元数据。纯结构裁剪,不做任何语义判断。
87
99
  *
88
100
  * @param writableIds 可写字段 id(含系统字段),顺序即输出顺序的同构子集
89
- * @param sourceFields 字段来源(createForm 传可见字段原对象;editLoad 传 slimFields)
101
+ * @param sourceFields 字段来源。**必须传已挂上 `fileUploadParams` / `folderDependsOnOption` 的那份**
102
+ * (createForm 传 `slimFields`;传早期的 `visibleFields` 会让 F/D 两个标记恒不出现)
90
103
  * @param systemNames 系统字段 id → 中文名(不在 sourceFields 里的兜底)
91
104
  */
92
105
  function encodeWritableFieldsCompact(writableIds, sourceFields, systemNames) {
@@ -121,6 +134,8 @@ function encodeWritableFieldsCompact(writableIds, sourceFields, systemNames) {
121
134
  };
122
135
  const rows = [];
123
136
  const optionIndex = {};
137
+ const formatIndex = {};
138
+ const uploadLimits = {};
124
139
  const emitted = new Set();
125
140
  const push = (id) => {
126
141
  if (emitted.has(id))
@@ -136,6 +151,12 @@ function encodeWritableFieldsCompact(writableIds, sourceFields, systemNames) {
136
151
  if (options.length) {
137
152
  optionIndex[id] = options.slice(0, 30).map((o) => (o && typeof o === 'object' ? String(o.name ?? o.content ?? '') : String(o)));
138
153
  }
154
+ const format = String(f.format ?? '').trim();
155
+ if (format)
156
+ formatIndex[id] = format;
157
+ const limits = uploadLimitOf(f);
158
+ if (limits)
159
+ uploadLimits[id] = limits;
139
160
  };
140
161
  const wanted = new Set(writableIds);
141
162
  for (const f of sourceFields) {
@@ -150,12 +171,50 @@ function encodeWritableFieldsCompact(writableIds, sourceFields, systemNames) {
150
171
  rows,
151
172
  rowCount: rows.length,
152
173
  flagLegend: 'R=必填 B=浏览按钮 S=选项 M=多选 E=人员范围 V=发票 D=附件目录随选项 '
153
- + 'F=有上传参数(见 resultFile) O=有选项(见 optionIndex) T=有日期/时间格式要求',
174
+ + 'F=有上传参数(见 uploadLimits) O=有选项(见 optionIndex) '
175
+ + 'T=有日期/时间格式要求(见 formatIndex)',
154
176
  };
155
177
  if (Object.keys(optionIndex).length)
156
178
  out.optionIndex = optionIndex;
179
+ if (Object.keys(formatIndex).length)
180
+ out.formatIndex = formatIndex;
181
+ if (Object.keys(uploadLimits).length)
182
+ out.uploadLimits = uploadLimits;
157
183
  return out;
158
184
  }
185
+ /**
186
+ * 附件字段的上传限制 → 一行紧凑描述(`大小上限|格式限制|数量上限`)。
187
+ *
188
+ * 只摘模型填值前要核对的三项,**丢掉服务器侧管道**:`customParam`(`authStr`/`authSignatureStr`/
189
+ * `requestId`/`nodeId`/`identityId`…)、`folderId`/`refId`/`sourceRefId`/`source`/`module` 等,
190
+ * 单字段原块 ≈700 B 而其中三项限制合计不到 120 B。
191
+ *
192
+ * - `sizeLimit` 单位 MB;`formatLimit.mode='white'` 记「白名单」、其余记「黑名单」;
193
+ * `formats=['*.*']`(或 `['*']`)记「不限格式」。
194
+ * - 缺项一律写 `-`,**不要留空**:留空会被读成"限制为 0/不允许"。
195
+ * - 无 `fileUploadParams` 的字段返回空串(调用方不产出该条目)。
196
+ */
197
+ function uploadLimitOf(f) {
198
+ const params = (f && typeof f === 'object') ? f.fileUploadParams : null;
199
+ if (!params || typeof params !== 'object')
200
+ return '';
201
+ const size = Number(params.sizeLimit);
202
+ const maxNum = Number(params.maxNum);
203
+ const limit = (params.formatLimit && typeof params.formatLimit === 'object') ? params.formatLimit : null;
204
+ const formats = Array.isArray(limit?.formats)
205
+ ? limit.formats.map((v) => String(v).trim()).filter(Boolean)
206
+ : [];
207
+ const formatText = !formats.length
208
+ ? '-'
209
+ : (formats.length === 1 && (formats[0] === '*.*' || formats[0] === '*'))
210
+ ? '不限格式'
211
+ : `${String(limit?.mode ?? '').toLowerCase() === 'white' ? '白名单' : '黑名单'}:${formats.join(',')}`;
212
+ return [
213
+ Number.isFinite(size) && size > 0 ? `${size}MB` : '-',
214
+ formatText,
215
+ Number.isFinite(maxNum) && maxNum > 0 ? `${maxNum}个` : '-',
216
+ ].join('|');
217
+ }
159
218
  /**
160
219
  * 大表单输出保护(通用,不针对任何具体工作流/表单)。
161
220
  *
@@ -212,58 +271,73 @@ async function compactFormOutput(result, env, resultFileName, fieldsOmittedCompe
212
271
  // ① 安全线改用**美化口径** `stdoutBytesOf()`(与实际 stdout 一致)。此前量压缩 JSON,**低估 1.79 倍**
213
272
  // ⇒ 这一档从未触发,且 `writableFields` 被追加在末尾(起始偏移 31,093),宿主只给 2048 B 预览
214
273
  // ⇒ 模型永远看不到它,只能自写脚本挖 resultFile(实测白烧 4 轮工具往返、约 54 s)。
215
- // ② `conditionalRequired`(278 字段表单实测压缩 10.9 KB / 美化 ≈19.5 KB)与 `editableFields`
216
- // (与 writableFields 冗余)移出 stdout,只留条数 —— 二者合起来才够把体积压进宿主内联区。
274
+ // ② `editableFields`(与 writableFields 冗余)移出 stdout,只留可写索引 — 二者合起来才够把体积压进宿主内联区。
217
275
  // ③ `writableFields` 前置到载荷最前,任何截断场景下都先被看到。
218
- const condCount = Array.isArray(result.conditionalRequired) ? result.conditionalRequired.length : 0;
219
276
  const summary = { ...result };
220
277
  delete summary.fields;
221
- delete summary.conditionalRequired;
222
278
  delete summary.editableFields;
223
- const ordered = {};
224
- if (fieldsOmittedCompensation)
225
- ordered.writableFields = fieldsOmittedCompensation;
226
- for (const [key, value] of Object.entries(summary)) {
227
- if (value !== undefined)
228
- ordered[key] = value;
229
- }
230
- const fieldsOmittedNotice = `表单字段过多(${fields.length} 个),字段清单已整体移出 stdout,`
279
+ /**
280
+ * "字段清单去哪了"的文案——`withIndex` 表示**本档是否真的带了** `writableFields`。
281
+ *
282
+ * 2026-09-22 修正:只留指针那一档此前复用同一段文案,于是在**没给索引的档位里声称"已给出索引"**,
283
+ * 既与同档的 `writableFieldsOmitted: true` 标记互相矛盾,又把模型指向一个不存在的键。
284
+ *
285
+ * 2026-09-23 修正:文案此前只提 `optionIndex`,还把 `fileUploadParams` 列进"要读 resultFile"的清单 ——
286
+ * 而 `formatIndex` / `uploadLimits` 其实**已经随索引下发**,本可覆盖"日期格式""上传限制"这两个最常见的
287
+ * 回读动因。文案不提它们,等于默许模型为这两项去读 resultFile(实测就是这么白跑一轮)。现按实际下发的
288
+ * 三类索引(`optionIndex` / `formatIndex` / `uploadLimits`,均按需出现)改写,并删掉 `fileUploadParams`。
289
+ */
290
+ const fieldsOmittedNoticeOf = (withIndex) => `表单字段过多(${fields.length} 个),字段清单已整体移出 stdout,`
231
291
  + '完整结果(含全部 fields 原始元数据)已写入 resultFile。'
232
- + (fieldsOmittedCompensation
292
+ + (withIndex
233
293
  ? 'stdout **最前面**已给出全部可写字段的紧凑索引 writableFields:columns 为列名、rows 每行一个字段'
234
- + '(`id|name|subFormId|componentKey|flags`,flags 含义见 flagLegend;带选项的字段,'
235
- + '其选项名见 optionIndex)。**直接用 writableFields 里的字段 id 构造 data 的键**,'
236
- + '不要为了拿字段清单再写脚本解析 resultFile;只有需要某字段更细的元数据'
237
- + '(browserModule / fileUploadParams / 完整 options 结构)时,才按该字段 id 从 resultFile 取。'
238
- : '构造表单数据前,请按字段 id 从 resultFile 里提取所需键,按需只取几个键、'
239
- + '不要把整份 resultFile 读入上下文。')
240
- + (condCount
241
- ? `为压进体积安全线,条件必填规则(conditionalRequired,${condCount} 条)与可写白名单`
242
- + '(editableFields)也已移出 stdout(条数见 conditionalRequiredCount);'
243
- + '需要时按同一个 resultFile 取。'
244
- : '')
245
- + READ_WHOLE_OUTPUT_HINT;
246
- const leadingMeta = {
294
+ + '(`id|name|subFormId|componentKey|flags`,flags 含义见 flagLegend)。填值前要核对的映射'
295
+ + '也随同一索引下发:选项名见 optionIndex、日期/时间格式见 formatIndex、附件上传限制见 uploadLimits'
296
+ + '(三者按需出现,没有该类字段时没有该键)。**直接用 writableFields 里的字段 id 构造 data 的键**,'
297
+ + '选项、日期格式、上传限制一律照上述索引填 —— 不要为了拿字段清单、选项、格式或上传限制再写脚本解析 resultFile;'
298
+ + '只有需要这里没有的字段元数据(browserModule / browserType / 完整 options 结构)时,'
299
+ + '才按该字段 id 从 resultFile 取。'
300
+ : '本档体积太紧,**可写字段索引也放不进 stdout**——构造表单数据前,请按字段 id 从 resultFile 里提取所需键,'
301
+ + '按需只取几个键、不要把整份 resultFile 读入上下文。');
302
+ const metaOf = (withIndex = true) => ({
247
303
  resultFile,
248
304
  persistedResult: true,
249
305
  outputBytes: bytes,
250
306
  fieldsOmitted: true,
251
307
  fieldCount: fields.length,
252
- ...(condCount ? { conditionalRequiredCount: condCount } : {}),
253
- largeOutputNotice: fieldsOmittedNotice,
308
+ largeOutputNotice: fieldsOmittedNoticeOf(withIndex) + READ_WHOLE_OUTPUT_HINT,
309
+ });
310
+ const orderedBody = (includeWritable = true) => {
311
+ const out = {};
312
+ if (includeWritable && fieldsOmittedCompensation)
313
+ out.writableFields = fieldsOmittedCompensation;
314
+ for (const [key, value] of Object.entries(summary)) {
315
+ if (value !== undefined)
316
+ out[key] = value;
317
+ }
318
+ return out;
254
319
  };
255
- const withIndex = withLeadingMeta(leadingMeta, ordered);
320
+ // 第一档:可写字段索引 + 摘要(正常的大表单都落在这里)
321
+ const withIndex = withLeadingMeta(metaOf(true), orderedBody(true));
256
322
  if (stdoutBytesOf(withIndex) <= STDOUT_SAFE_BYTES)
257
323
  return withIndex;
258
- // 索引也放不进安全线(字段数量极端):退回只留指针,并显式标注已省略。
259
- delete ordered.writableFields;
324
+ // 第二档(字段数量极端):索引也放不进安全线,退回只留指针,并显式标注索引被省略了。
325
+ // 2026-09-22 修正:这一档**必须真的把 writableFields 去掉**——此前仍经 `orderedBody(true)` 把它塞回去,
326
+ // 结果既没压进安全线,又与 `writableFieldsOmitted: true` 自相矛盾、文案还在说"已给出索引"。
260
327
  return withLeadingMeta({
261
- ...leadingMeta,
328
+ ...metaOf(false),
262
329
  ...(fieldsOmittedCompensation ? { writableFieldsOmitted: true } : {}),
263
- }, ordered);
330
+ }, orderedBody(false));
264
331
  }
265
- async function requestJson(env, method, path, query, body) {
266
- const response = await env.host.workflowRequest({ method, path, query, body, operation: path });
332
+ async function requestJson(env, method, path, query, body, options = {}) {
333
+ const response = await env.host.workflowRequest({
334
+ method,
335
+ path,
336
+ query,
337
+ body,
338
+ operation: path,
339
+ ...(options.tolerateBusinessError ? { tolerateBusinessError: true } : {}),
340
+ });
267
341
  return responseData(response);
268
342
  }
269
343
  async function getSessionContext(env) {
@@ -1054,11 +1128,12 @@ function applyViewRelateHidden(layoutSubformIds, viewRelateConfig) {
1054
1128
  * 真正未下发时:`requiredFields` 只剩系统字段、`subFormRequired` 恒为 `{}`,**不代表表单没有必填项**。
1055
1129
  *
1056
1130
  * V12.1.27 纠正(此前措辞有硬伤):原文案把 `missingFields` 也写成"为空不代表没有必填/无法据此校验业务必填",
1057
- * 与事实相反——本租户的业务必填是**条件必填**,由 `viewLinkages` 的 `required` 规则承载,
1058
- * prepare 阶段按实际所填数据**实时求值**,`missingFields` 完全可信(2026-09-17 真机验证:总部费用报销
1131
+ * 与事实相反——本租户的业务必填多为**条件必填**(`viewLinkages` 的 `required` 规则),
1132
+ * createRequest 阶段按实际所填数据**实时求值**,`missingFields` 完全可信(2026-09-17 真机验证:总部费用报销
1059
1133
  * `报销方式=银行(付公司)` → `missingFields=[账户名称,账号,开户机构名称]`)。原文案会诱导模型对用户说
1060
- * "系统不会校验必填项",属错误陈述。现改为:静态标记未下发 + 条件必填清单(`conditionalRequired`)+
1061
- * **missingFields 可信、照常 prepare**。
1134
+ * "系统不会校验必填项",属错误陈述。现改为:静态标记未下发 + **missingFields 可信、照常 prepare**。
1135
+ *
1136
+ * 2026-09-23:`createForm` 不再下发联动规则(原 `conditionalRequired`),必填判断一律以 `missingFields` 为准。
1062
1137
  *
1063
1138
  * 返回 null 表示静态必填信息可用,无需告警。
1064
1139
  */
@@ -1078,43 +1153,415 @@ function requiredInfoWarningText(context) {
1078
1153
  if (business.some((f) => f.required))
1079
1154
  return null;
1080
1155
  return 'OA 未下发该表单的**静态**必填标记,本次 requiredFields / subFormRequired 只含系统字段(流程标题等),'
1081
- + '但这**不代表该表单没有业务必填项**:业务必填见 conditionalRequired。'
1082
- + 'missingFields 由 prepare 按实际所填数据实时计算、**可信**——照常 prepare,'
1083
- + '并把 missingFields 与 conditionalRequired 一次性向用户问齐。'
1156
+ + '但这**不代表该表单没有业务必填项**——业务必填多为「条件必填」,只有按实际所填数据才判定得出。'
1157
+ + 'missingFields 由 createRequest 按实际所填数据实时计算、**可信**:照常发起,'
1158
+ + '返回 NEEDS_INPUT 时把 missingFields 一次性向用户问齐。'
1084
1159
  + '不要对用户说"系统不会校验必填项"。';
1085
1160
  }
1086
1161
  // ---------------------------------------------------------------------------
1087
- // createForm:loadBase/loadFrame/loadMinor/formLayout + 字段输出 + fileUploadParams
1162
+ // 发起侧工作流解析(原 createList 的职责;候选池 = 当前用户「可发起的工作流」)
1163
+ // ---------------------------------------------------------------------------
1164
+ /**
1165
+ * 把一个「纯名称」切成多个匹配关键词(2026-09-23 用户裁定)。
1166
+ *
1167
+ * 单个格内可用逗号分隔多个关键词(中英文逗号都认),也可用数组多元素表达;命中结果做**并集**,
1168
+ * 目的是**提高候选召回** —— 用户首次要发起某个流程时,多给几个说法更容易命中他真正要发起的那个,
1169
+ * 从而少让用户回来补全名称。
1170
+ *
1171
+ * 去词规则(用户裁定,逐字):「**去掉一些词:工作流、流程**」。落到实现上是——
1172
+ * **只丢弃"整格就是填充词"的关键词**(`流程` / `工作流` / `工作流程`),其余**原样保留**:
1173
+ * - `"报销,报销流程,流程"` → 关键词 = `报销`、`报销流程`(**两个**,`流程` 被丢掉);
1174
+ * - `报销流程` **不在词内剥离** —— 它是有效关键词(剥成 `报销` 会让"多关键词"退化成单关键词)。
1175
+ * 匹配不到的关键词在并集里自然贡献为空,无害,所以不必额外清理。
1176
+ */
1177
+ function splitCreateKeywords(raw) {
1178
+ const out = [];
1179
+ const seen = new Set();
1180
+ for (const piece of String(raw ?? '').split(/[,,]/u)) {
1181
+ const token = piece.trim();
1182
+ if (!token)
1183
+ continue;
1184
+ // 填充词按**长词优先**脱壳:`工作流程` → 空(`工作流`+`流程` 在 `流` 处重叠,顺序错会漏判)
1185
+ const residue = token.split('工作流程').join('').split('工作流').join('').split('流程').join('');
1186
+ if (!residue.trim())
1187
+ continue;
1188
+ const key = token.toLowerCase();
1189
+ if (seen.has(key))
1190
+ continue;
1191
+ seen.add(key);
1192
+ out.push(token);
1193
+ }
1194
+ return out;
1195
+ }
1196
+ /**
1197
+ * 发起侧名称→ID。
1198
+ *
1199
+ * 与查询侧 `resolveTerm` 的关键差别在**候选池**:这里必须落在「当前用户可发起的工作流」上
1200
+ * (`getNewFlowListData`),而不是流程路径浏览按钮——后者包含用户看得见、但没有发起权限的流程。
1201
+ *
1202
+ * 判据(与查询侧同一套,避免出现"查询能选中、发起选不中"的错位):
1203
+ * - **0 命中 → 澄清**:不能把「没听懂你的条件」伪装成「没有这条流程」,否则模型会如实转述成
1204
+ * "没有该工作流",用户拿不到可操作的信息;
1205
+ * - 1 命中 → 直接用;
1206
+ * - 多命中但候选中**恰有一个与所写名称完全一致** → 直接用,不为"要不要多打几个字"再问一次;
1207
+ * - 多命中且没有完全一致的 → 澄清并列出候选(`display` 即 `名称(ID)`,用户选定后原样回传)。
1208
+ *
1209
+ * 名称格支持**多关键词**(`"报销,报销流程"`,见 `splitCreateKeywords`):各关键词命中做并集去重后,
1210
+ * 再走上面同一套判据。**只发一次** `getNewFlowListData` 而不是每个关键词并行各发一次 —— 该接口
1211
+ * 请求体固定为 `{wfpBelongTypeCode, loadOfsFlow, clientCtrl}`、**没有任何关键词入参**(V11.3 Python
1212
+ * 基线 `e10_workflow_create.py:395-436` 同样是取全量后本地过滤;真机单笔 ≈1,649 ms / 228,929 B),
1213
+ * 并行 N 次只会拿到 N 份相同的全量列表,结果与"单次请求 + 本地多关键词过滤"完全等价却要付 N 倍开销。
1214
+ */
1215
+ async function resolveCreateWorkflow(rawWorkflow, env) {
1216
+ const rawTerms = [];
1217
+ for (const item of toArray(rawWorkflow)) {
1218
+ const text = String(item ?? '').trim();
1219
+ if (text)
1220
+ rawTerms.push(text);
1221
+ }
1222
+ // 澄清文案里回显的是**用户原话**(不是切分后的关键词)——用户要能认出自己给的条件
1223
+ const queryText = rawTerms.join('、');
1224
+ if (!rawTerms.length) {
1225
+ throw workflowClarificationError('workflow', '', '要发起哪个流程?请提供可发起工作流的名称、`名称(ID)` 或工作流 ID 后再继续。');
1226
+ }
1227
+ // 先做**纯本地**的解析:显式 ID(`名称(ID)` / 纯数字)与名称关键词(一格可含多个,见
1228
+ // `splitCreateKeywords`)。这一步不依赖远端结果,于是能先判"到底有没有可用条件"——
1229
+ // 一个关键词都没解析出来(如只传了 `流程`)时**连可发起列表都不去拉**
1230
+ // (该接口真机单笔 ≈1,649 ms / 228,929 B),与"完全不传 workflow"同一口径。
1231
+ const explicitIds = new Set();
1232
+ const nameTerms = [];
1233
+ const keywordSeen = new Set();
1234
+ for (const term of rawTerms) {
1235
+ const parsed = parseNameWithId(term);
1236
+ // 纯数字同样按 ID 处理(`parseNameWithId` 只认 "名称(数字)" 形态)
1237
+ const explicitId = parsed.id ?? (/^[0-9]+$/.test(term) ? term : '');
1238
+ if (explicitId) {
1239
+ explicitIds.add(explicitId);
1240
+ continue;
1241
+ }
1242
+ for (const keyword of splitCreateKeywords(parsed.name)) {
1243
+ const key = keyword.toLowerCase();
1244
+ if (keywordSeen.has(key))
1245
+ continue;
1246
+ keywordSeen.add(key);
1247
+ nameTerms.push(keyword);
1248
+ }
1249
+ }
1250
+ if (!explicitIds.size && !nameTerms.length) {
1251
+ throw workflowClarificationError('workflow', queryText, '要发起哪个流程?请提供可发起工作流的名称、`名称(ID)` 或工作流 ID 后再继续。');
1252
+ }
1253
+ const response = await requestJson(env, 'POST', FORM_ENDPOINTS.workflowCreateList, AJAX_QUERY, {
1254
+ wfpBelongTypeCode: '1', loadOfsFlow: '0', clientCtrl: 1,
1255
+ });
1256
+ // requestJson 内部已解包一层(responseData),此处不可再取 .data
1257
+ const data = (response && typeof response === 'object') ? response : null;
1258
+ if (!data || typeof data !== 'object')
1259
+ throw workflowApiError('format_error', '工作流列表返回 data 格式错误');
1260
+ const all = simplifyWorkflows(data);
1261
+ const picked = [];
1262
+ const seen = new Set();
1263
+ for (const explicitId of explicitIds) {
1264
+ const hit = all.find((item) => String(item.id ?? '') === explicitId);
1265
+ if (hit && !seen.has(String(hit.id))) {
1266
+ seen.add(String(hit.id));
1267
+ picked.push(hit);
1268
+ }
1269
+ }
1270
+ // 名称关键词:逐个取命中,**并集去重**(多给几个说法只扩大候选面,不改变下面的命中判据)
1271
+ for (const keyword of nameTerms) {
1272
+ for (const item of simplifyWorkflows(data, keyword)) {
1273
+ if (seen.has(String(item.id)))
1274
+ continue;
1275
+ seen.add(String(item.id));
1276
+ picked.push(item);
1277
+ }
1278
+ }
1279
+ picked.sort((a, b) => (a.typename + a.workflowname + a.id).localeCompare(b.typename + b.workflowname + b.id));
1280
+ if (picked.length === 1) {
1281
+ const only = picked[0];
1282
+ const id = String(only.id);
1283
+ return { id, name: String(only.workflowname ?? ''), source: explicitIds.has(id) ? 'workflowId' : 'single' };
1284
+ }
1285
+ if (picked.length > 1) {
1286
+ const wanted = new Set(nameTerms.map((t) => t.trim().toLowerCase()).filter(Boolean));
1287
+ const exact = picked.filter((item) => wanted.has(String(item.workflowname ?? '').trim().toLowerCase()));
1288
+ // 多命中时有唯一全名精确匹配 → 直接选中(与查询侧 exact_candidates 优选同一判据)
1289
+ if (exact.length === 1) {
1290
+ return { id: String(exact[0].id), name: String(exact[0].workflowname ?? ''), source: 'exactName' };
1291
+ }
1292
+ const list = exact.length ? exact : picked;
1293
+ throw workflowClarificationError('workflow', queryText, `「${queryText}」匹配到 ${list.length} 个可发起的工作流,且其中没有与所写名称完全一致的,`
1294
+ + '请确认要发起的具体工作流。', list.slice(0, 10).map((item, index) => ({
1295
+ index: index + 1,
1296
+ id: String(item.id),
1297
+ name: String(item.workflowname ?? ''),
1298
+ display: `${String(item.workflowname ?? '')}(${String(item.id)})`,
1299
+ })));
1300
+ }
1301
+ throw workflowClarificationError('workflow', queryText, `「${queryText}」在当前账号下没有匹配到可发起的工作流(也可能已停用或无发起权限)。`
1302
+ + '请核对名称,或给出准确的完整名称 / 工作流 ID 后再继续。');
1303
+ }
1304
+ /**
1305
+ * 从「全局 + 该工作流维度」的合并记忆里**只取工作流维度那一段**。
1306
+ *
1307
+ * 背景(2026-09-23 实测 + 用户裁定):`POST …memoryPrompt` 带 `workflowId` 时返回的是**全集** ——
1308
+ * `--- user.md ---` / `--- memory.md ---`(全局画像 + 全局别名映射)+ `--- memory_<workflowId>.md ---`
1309
+ * (该工作流维度)。于是 `launchPreferences` 与入口处读过的无参 `workflow.memoryPrompt`(纯全局)
1310
+ * **内容重叠**:真机实测同一租户 `promptText` 2,467 字符 = 全局 543 + 工作流维度 1,924,
1311
+ * 同一段全局记忆会在上下文里出现两次。而 `launchPreferences` 的语义是「**该工作流的**发起偏好」,
1312
+ * 全局那份已由入口读取覆盖,不应重复下发。
1313
+ *
1314
+ * 故只保留标记 `--- memory_<workflowId>.md ---` 及其**之后**的内容:
1315
+ * - 找得到该标记 ⇒ 从标记处截断(标记行本身保留 —— 它自带 workflowId 作用域,便于模型判断归属);
1316
+ * - 找不到 ⇒ **整体丢弃**(说明这次返回里没有该工作流维度的内容,只剩与入口重复的全局部分)。
1317
+ *
1318
+ * ⚠️ 这是**服务端支持前的兼容裁剪**:等服务端改为「传 workflowId 只回工作流维度记忆」后,
1319
+ * 若返回里仍带 `--- memory_<workflowId>.md ---` 分隔头,本函数行为不变、删掉也等价;
1320
+ * 若不再带该分隔头,**这段会把正常内容整体丢掉** —— 届时必须一并移除本函数与它的调用。
1321
+ */
1322
+ function extractWorkflowMemorySection(promptText, workflowId) {
1323
+ const text = String(promptText ?? '');
1324
+ const id = String(workflowId ?? '').trim();
1325
+ if (!text.trim() || !id)
1326
+ return '';
1327
+ const marker = `--- memory_${id}.md ---`;
1328
+ const at = text.indexOf(marker);
1329
+ if (at < 0)
1330
+ return '';
1331
+ return text.slice(at).trim();
1332
+ }
1333
+ /**
1334
+ * 顺带取回用户在这条工作流上的发起偏好(`buildAgentMemoryPrompt`,带 `workflowId`)。
1335
+ *
1336
+ * `promptText` **只含该工作流维度的记忆**(全局画像/全局别名由入口的无参 `workflow.memoryPrompt`
1337
+ * 覆盖,这里经 `extractWorkflowMemorySection` 裁掉,避免同一段全局记忆在上下文里出现两次)。
1338
+ *
1339
+ * best-effort:读记忆失败(未开通记忆 / 接口不可用)不是错误——一律按新用户处理,
1340
+ * 只体现在 `launchPreferences.available` 上,绝不阻断表单载入(与 `workflow.memoryPrompt` 同口径)。
1341
+ */
1342
+ async function fetchLaunchPreferences(workflowId, env) {
1343
+ const result = { available: false, workflowId, promptText: '' };
1344
+ try {
1345
+ const response = await env.host.workflowRequest({
1346
+ method: 'POST',
1347
+ path: WORKFLOW_ENDPOINTS.MEMORY_PROMPT,
1348
+ operation: 'workflow.createForm',
1349
+ body: { appid: resolveMemoryAppId(), dynamicContextParams: { workflowId } },
1350
+ });
1351
+ // 该接口原始返回是 {code,msg,status,data:"<markdown>",fail},业务成功标志是 status/fail
1352
+ // **不是 success**——这里**不能**用 responseData 解包(会把 data 里的 markdown 当主体,
1353
+ // 导致 promptText 恒空)。与 workflow.memoryPrompt 同源。
1354
+ // 只认 `data`(字符串)作为偏好正文:**不回退 `msg`**——`msg` 里是「接口返回成功」这类
1355
+ // 通用提示,回退它会让"没配记忆"的环境被读成有一条内容是"接口返回成功"的偏好。
1356
+ const body = (response && typeof response === 'object' ? response : {});
1357
+ const rawPromptText = typeof body.data === 'string' ? body.data.trim() : '';
1358
+ const promptText = extractWorkflowMemorySection(rawPromptText, workflowId);
1359
+ const businessOk = body.fail !== true && body.status !== false && body.success !== false;
1360
+ result.available = Boolean(businessOk && promptText);
1361
+ result.promptText = promptText;
1362
+ }
1363
+ catch (error) {
1364
+ // 404 = 该智能体/工作流还没有记忆条目(未开通记忆),属**正常的新用户状态**,不是失败:
1365
+ // 保持 `available:false` + `promptText:''`,**不加任何"失败"提示**、也不阻断任务。
1366
+ // 其它错误才提示"读取失败、按新用户处理"。
1367
+ if (!isNotFoundError(error)) {
1368
+ result.note = '该工作流的记忆偏好读取失败,按新用户处理,不要重试。';
1369
+ }
1370
+ }
1371
+ return result;
1372
+ }
1373
+ /**
1374
+ * 是否是「资源不存在」类错误(HTTP 404)。
1375
+ *
1376
+ * 真机上该错误由 host 统一抛成 `workflowApiError('http_error', 'OA 接口 HTTP 404', {code:404, details:{status:404}})`,
1377
+ * 故同时认 `code` 与 `details.status` 两处。不依赖类实例判断,避免把跨包包装过的错误漏掉。
1378
+ */
1379
+ function isNotFoundError(error) {
1380
+ const record = (error && typeof error === 'object') ? error : {};
1381
+ if (Number(record.code) === 404)
1382
+ return true;
1383
+ const details = record.details;
1384
+ return Boolean(details && typeof details === 'object' && Number(details.status) === 404);
1385
+ }
1088
1386
  // ---------------------------------------------------------------------------
1387
+ // createForm:解析工作流 + loadBase/loadFrame/loadMinor/formLayout + 字段输出
1388
+ // ---------------------------------------------------------------------------
1389
+ /**
1390
+ * `workflow.createForm` —— **一条命令**完成「解析可发起工作流 → 载入发起表单 → 取回该工作流发起偏好」。
1391
+ *
1392
+ * 入参只需要工作流参数(`名称(ID)` / `名称` / `id`,与 `workflow.search` 的 `workflow` 同形态)。
1393
+ * 解析不出唯一目标时走澄清(`ok:true` + `status:NEEDS_CLARIFICATION`,与查询侧同一套约定),
1394
+ * 此时候选之外**不返回任何表单数据**。
1395
+ */
1396
+ /**
1397
+ * 从 `loadBaseParam` 的返回里读出**「为什么进不去这张表单」**的结论。
1398
+ *
1399
+ * 真机实测(2026-09-22,三种形态都取过原文):
1400
+ * - 正常可发起:`data.commonParam.authInfo = {canView:true, verifyRight:"VIEW_RIGHT", ...}`,且有 `requestInfo.workflowName`;
1401
+ * - **ID 不存在**:`canView:false`、`verifyRight:"WORKFLOW_NOT_EXIST"`、`exceptionType.desc:"当前工作流不存在,请联系管理员"`;
1402
+ * - **存在但无发起权限**:`canView:false`、`verifyRight:"NO_CREATE_RIGHT"`、`exceptionType.desc:"无流程创建权限"`、`mainPrompt:"无发起权限"`。
1403
+ *
1404
+ * 这三种情况下 `data` 里**只有** `cmdExeLogs`/`PLExecuteInfo`/`commonParam`——没有 `requestInfo`、
1405
+ * 更没有表单框架,所以必须在 `parseRuntime` 之前先判,否则只会得到一个语焉不详的格式错误。
1406
+ *
1407
+ * 判据保守:只认 `authInfo` 存在且 `canView === false`;老响应没有 `authInfo` 时不设闸(照常往下走)。
1408
+ */
1409
+ function readCreateFormBlock(base) {
1410
+ const data = (base && typeof base === 'object') ? base : null;
1411
+ const common = (data?.commonParam && typeof data.commonParam === 'object') ? data.commonParam : null;
1412
+ const auth = (common?.authInfo && typeof common.authInfo === 'object') ? common.authInfo : null;
1413
+ if (!auth)
1414
+ return null;
1415
+ if (auth.canView !== false)
1416
+ return null;
1417
+ const exc = (auth.exceptionType && typeof auth.exceptionType === 'object') ? auth.exceptionType : {};
1418
+ const detail = String(exc.exceptionDesc ?? exc.desc ?? auth.noRightTip ?? '').trim();
1419
+ const right = String(auth.verifyRight ?? '').trim().toUpperCase();
1420
+ if (right === 'WORKFLOW_NOT_EXIST')
1421
+ return { reason: 'workflow_not_exist', detail };
1422
+ return { reason: 'no_create_right', detail };
1423
+ }
1424
+ /**
1425
+ * 把「进不去这张表单」翻成**可判别**的澄清。
1426
+ *
1427
+ * 两句话必须分开写:「ID 不存在」与「存在但没有发起权限」是完全不同的事实,
1428
+ * 混成一句就会让模型把后者说成前者(把"没权限"伪装成"没有这条流程")。
1429
+ * `clarificationReason` 随信封下发,模型据此选话术(文案见 `operations.ts` 的 `clarificationNote`)。
1430
+ */
1431
+ function createFormBlockedError(workflowId, block) {
1432
+ if (block.reason === 'workflow_not_exist') {
1433
+ return workflowClarificationError('workflow', workflowId, `工作流 ID「${workflowId}」不存在(可能已删除或被停用)。请核对 ID,或直接给出流程名称重新指定。`, [], { clarificationReason: 'workflow_not_exist' });
1434
+ }
1435
+ return workflowClarificationError('workflow', workflowId, `工作流 ID「${workflowId}」**存在**,但当前账号没有发起权限`
1436
+ + `${block.detail ? `(${block.detail})` : ''}。`
1437
+ + '请改用其他可发起的流程,或联系管理员为该流程授予发起权限。', [], { clarificationReason: 'no_create_right' });
1438
+ }
1439
+ /**
1440
+ * 入参是否已是「单条、且自带显式 ID」的工作流参数(`名称(ID)` 或纯 ID)。
1441
+ *
1442
+ * 该形态下**完全不需要**可发起工作流列表:权威名称由 `loadBaseParam` 的
1443
+ * `requestInfo.workflowName` 给出,可发起性由同一接口的 `commonParam.authInfo` 给出
1444
+ * (见 `readCreateFormBlock`)。因此这条路径会**跳过** `getNewFlowListData`
1445
+ * ——真机实测该接口单笔 **1.65 s / 228,929 B**,是本命令最贵的一笔。
1446
+ *
1447
+ * 不满足该形态时返回 null,调用方退回「靠列表解析出 id」的原口径。
1448
+ */
1449
+ function singleExplicitWorkflowId(rawWorkflow) {
1450
+ const terms = toArray(rawWorkflow).map((item) => String(item ?? '').trim()).filter(Boolean);
1451
+ if (terms.length !== 1)
1452
+ return null;
1453
+ const parsed = parseNameWithId(terms[0]);
1454
+ return parsed.id ?? (/^[0-9]+$/.test(terms[0]) ? terms[0] : '');
1455
+ }
1456
+ /** 表单载入 + 该工作流的发起偏好:两者只共享 `workflowId`,可以并行。 */
1457
+ async function loadCreateFormPack(workflowId, env) {
1458
+ const [{ data, writableFieldsCompact }, launchPreferences] = await Promise.all([
1459
+ buildCreateFormData(workflowId, env),
1460
+ // 记忆读取是 best-effort(内部全 catch),不会把表单载入的错误顶掉。
1461
+ fetchLaunchPreferences(workflowId, env),
1462
+ ]);
1463
+ return { data, writableFieldsCompact, launchPreferences };
1464
+ }
1089
1465
  async function commandCreateForm(args, env) {
1090
- const workflowId = String(args.workflowId ?? '').trim();
1091
- if (!workflowId)
1092
- throw workflowValidationError('workflow_id_required', 'workflowId 必填');
1466
+ // 会话是**惰性**加载的(`ensureSession` 只在首次调用时填充)。本命令随后会并发发起多个请求,
1467
+ // 先在这里显式取一次,避免并发首调各自走一遍会话初始化。
1468
+ await env.host.getSession();
1469
+ let target;
1470
+ let pack;
1471
+ const explicitId = singleExplicitWorkflowId(args.workflow);
1472
+ if (explicitId) {
1473
+ // 自带显式 ID ⇒ **一个列表请求都不发**(真机该接口 1.65 s / 228,929 B,是本命令最贵的一笔)。
1474
+ // 权威名称由 loadBaseParam 的 `requestInfo.workflowName` 给出、可发起性由同接口的
1475
+ // `commonParam.authInfo` 给出;该 ID 不可用时由载入表单一侧的 `readCreateFormBlock`
1476
+ // 抛出**可判别**的澄清(不是把它伪装成"没有匹配到可发起的工作流")。
1477
+ target = { id: explicitId, name: '', source: 'workflowId' };
1478
+ pack = await loadCreateFormPack(explicitId, env);
1479
+ }
1480
+ else {
1481
+ target = await resolveCreateWorkflow(args.workflow, env);
1482
+ pack = await loadCreateFormPack(target.id, env);
1483
+ }
1484
+ return workflowSuccess('workflow.createForm', await compactFormOutput({
1485
+ ...pack.data,
1486
+ workflow: { ...(pack.data.workflow || {}), selectionSource: target.source },
1487
+ launchPreferences: pack.launchPreferences,
1488
+ }, env, 'last_create_form.json', pack.writableFieldsCompact));
1489
+ }
1490
+ // ---------------------------------------------------------------------------
1491
+ // createForm:loadBase/loadFrame/loadMinor/formLayout + 字段输出 + fileUploadParams
1492
+ // ---------------------------------------------------------------------------
1493
+ /**
1494
+ * 载入发起表单(原 `createForm` 的全部取数逻辑,逐行保留)——产出表单上下文与输出契约。
1495
+ *
1496
+ * 与旧版的差别只有一处:`workflowId` 由调用方(`resolveCreateWorkflow`)解析好后传进来,
1497
+ * 不再是本函数的入参校验点。
1498
+ */
1499
+ async function buildCreateFormData(workflowId, env) {
1093
1500
  const query = { workflowId, isCreate: 1, ...AJAX_QUERY };
1094
1501
  const body = { workflowId, isCreate: 1, isMobile: 0 };
1095
- const baseResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadBase, query, body);
1096
- const frameResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadFrame, query, body);
1502
+ // 2026-09-22:这四个接口**彼此没有数据依赖**(入参只需要 workflowId,见各自的 query/body),
1503
+ // 真机实测各 171–429 ms、串行合计约 1.1 s;并行后只受最慢的一个约束。
1504
+ // `loadMinorInfo` / `tenant/setting` 仍是 best-effort(失败只降级、不阻断),所以各自 catch 成
1505
+ // 结果对象再判定 —— 不能让它们的失败经由 Promise.all 把整个命令带崩(后者语义与串行版一致)。
1506
+ // `loadBaseParam` / `loadFrameParam` 也改成"先收着、后判定":目的是让 base 的权限结论
1507
+ // **优先于** frame 的泛化错误浮出(理由见下面的判定顺序)。
1508
+ const [baseOutcome, frameOutcome, minorOutcome, tenantOutcome] = await Promise.all([
1509
+ // `tolerateBusinessError`:该接口用 `status:false` + `commonParam.authInfo` 表达
1510
+ // 「不存在 / 无发起权限」,走带业务过滤的通道只会拿到"接口返回状态异常,code=200"。
1511
+ requestJson(env, 'POST', FORM_ENDPOINTS.loadBase, query, body, { tolerateBusinessError: true })
1512
+ .then((value) => ({ ok: true, value }))
1513
+ .catch((error) => ({ ok: false, error })),
1514
+ requestJson(env, 'POST', FORM_ENDPOINTS.loadFrame, query, body)
1515
+ .then((value) => ({ ok: true, value }))
1516
+ .catch((error) => ({ ok: false, error })),
1517
+ requestJson(env, 'POST', FORM_ENDPOINTS.loadMinor, query, { workflowId, isCreate: 1, isMobile: 0 })
1518
+ .then((value) => ({ ok: true, value }))
1519
+ .catch(() => ({ ok: false, value: null })),
1520
+ requestJson(env, 'POST', FORM_ENDPOINTS.tenantSetting, undefined, { setType: [] })
1521
+ .then((value) => ({ ok: true, value }))
1522
+ .catch(() => ({ ok: false, value: null })),
1523
+ ]);
1524
+ // 「为什么进不去这张表单」必须**先于**其它接口的错误判定:
1525
+ // ID 不存在 / 无发起权限时,真机上 `loadFrameParam`、`loadMinorInfo` 会返回
1526
+ // `code:500「无流程查看权限」`——那是**后果**不是**原因**。若让它先外溢,模型只能对着
1527
+ // "无流程查看权限"猜,说不出到底是「ID 不存在」还是「存在但没权限」。
1528
+ if (baseOutcome.ok) {
1529
+ const blocked = readCreateFormBlock(baseOutcome.value);
1530
+ if (blocked)
1531
+ throw createFormBlockedError(workflowId, blocked);
1532
+ }
1533
+ if (!baseOutcome.ok)
1534
+ throw baseOutcome.error;
1535
+ if (!frameOutcome.ok)
1536
+ throw frameOutcome.error;
1537
+ const minorWarningText = 'loadMinorInfo 获取系统字段权限失败,跳过系统字段(流程标题/紧急程度/密级)必填校验';
1097
1538
  let minorInfo = {};
1098
1539
  let minorWarning = null;
1099
- try {
1100
- const minorResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadMinor, query, { workflowId, isCreate: 1, isMobile: 0 });
1101
- minorInfo = parseMinorInfo(minorResp);
1540
+ if (minorOutcome.ok) {
1541
+ try {
1542
+ minorInfo = parseMinorInfo(minorOutcome.value);
1543
+ }
1544
+ catch {
1545
+ minorInfo = {};
1546
+ minorWarning = minorWarningText;
1547
+ }
1102
1548
  }
1103
- catch {
1104
- minorInfo = {};
1105
- minorWarning = 'loadMinorInfo 获取系统字段权限失败,跳过系统字段(流程标题/紧急程度/密级)必填校验';
1549
+ else {
1550
+ minorWarning = minorWarningText;
1106
1551
  }
1107
- const base = baseResp;
1108
- const frameData = frameResp;
1552
+ const base = baseOutcome.value;
1553
+ const frameData = frameOutcome.value;
1109
1554
  const runtime = parseRuntime(base, workflowId);
1110
1555
  const frame = parseFrame(frameData, runtime.node_id);
1556
+ // tenantSetting 取不到时按 {} 处理(与串行版的 catch 分支同口径)。
1111
1557
  let tenantSetting = {};
1112
- try {
1113
- const settingResp = await requestJson(env, 'POST', FORM_ENDPOINTS.tenantSetting, undefined, { setType: [] });
1114
- tenantSetting = parseTenantSetting(settingResp);
1115
- }
1116
- catch {
1117
- tenantSetting = {};
1558
+ if (tenantOutcome.ok) {
1559
+ try {
1560
+ tenantSetting = parseTenantSetting(tenantOutcome.value);
1561
+ }
1562
+ catch {
1563
+ tenantSetting = {};
1564
+ }
1118
1565
  }
1119
1566
  const layoutResp = await requestJson(env, 'POST', formApiUrl(FORM_ENDPOINTS.formLayout, frame.relate_type), { formId: frame.form_id, ...AJAX_QUERY }, buildLayoutRequest(workflowId, runtime, frame));
1120
1567
  const layoutData = layoutResp;
@@ -1178,7 +1625,7 @@ async function commandCreateForm(args, env) {
1178
1625
  defaults.push(d);
1179
1626
  }
1180
1627
  }
1181
- const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(layoutData.viewLinkages, [], defaults, onLoadLinkage, systemFieldValues);
1628
+ const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(layoutData.viewLinkages, [], defaults, onLoadLinkage, systemFieldValues, fields);
1182
1629
  const mainHidden = new Set(rowHidden.main || []);
1183
1630
  const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
1184
1631
  if (initialHidden.size) {
@@ -1356,8 +1803,6 @@ async function commandCreateForm(args, env) {
1356
1803
  onLoadLinkage,
1357
1804
  };
1358
1805
  const token = await env.continuation.issue({ operation: 'workflow.createForm', baseUrl: session.baseUrl, profile: session.profile, userId: session.userId, payload: context }, CONTINUATION_TTL_MS);
1359
- // 条件必填清单(V12.1.27):viewLinkages 里的 required 规则原样结构化,form 阶段即可主动问齐必填项。
1360
- const conditionalRequired = conditionalRequiredFields(layoutData.viewLinkages, rawFieldsById);
1361
1806
  const requiredFieldIds = requiredFieldsWithSystem(requiredFieldsList, runtime, minorInfo.permissions || {});
1362
1807
  // 可写字段 = 必填(主表 + 明细,含系统字段)+ 其余可编辑字段。
1363
1808
  // 只在 `fields` 真被整体移出 stdout 时才会随输出(由 compactFormOutput 决定)。
@@ -1370,31 +1815,38 @@ async function commandCreateForm(args, env) {
1370
1815
  }
1371
1816
  for (const f of editableFieldsList)
1372
1817
  writableFieldIds.push(String(f?.id ?? ''));
1373
- const writableFieldsCompact = encodeWritableFieldsCompact([...new Set(writableFieldIds)], visibleFields, SYSTEM_FIELD_KEYS);
1374
- return workflowSuccess('workflow.createForm', await compactFormOutput({
1375
- status: 'FORM_READY',
1376
- contextId: token,
1377
- requestId: runtime.request_id,
1378
- requestName: runtime.default_request_name || '',
1379
- workflow: context.workflow,
1380
- form: { id: frame.form_id, layout_id: layoutId },
1381
- requiredFields: requiredFieldIds,
1382
- subFormRequired: Object.fromEntries(Object.entries(subFormRequired).map(([k, v]) => [k, v.map((f) => String(f.id ?? ''))])),
1383
- editableFields: editableFieldsList.map((f) => String(f.id ?? '')),
1384
- fields: slimFields,
1385
- initialValues: initValues,
1386
- systemFields: {
1387
- permissions: minorInfo.permissions || {},
1388
- defaults: context.systemFields.defaults,
1389
- levelOptions: minorInfo.level_options || [],
1390
- secLevelOptions: minorInfo.sec_level_options || [],
1818
+ // 字段来源必须是 slimFields(它才带 fileUploadParams / folderDependsOnOption):传 visibleFields
1819
+ // 会让 F/D 两个标记恒不出现,uploadLimits 也整块缺失。
1820
+ const writableFieldsCompact = encodeWritableFieldsCompact([...new Set(writableFieldIds)], slimFields, SYSTEM_FIELD_KEYS);
1821
+ return {
1822
+ data: {
1823
+ status: 'FORM_READY',
1824
+ contextId: token,
1825
+ requestId: runtime.request_id,
1826
+ // 显式点名语义:createForm 阶段并没有建单,这个 ID 是**预生成**的流程请求 ID,
1827
+ // 只作为该单据的身份标识随 context 带到 createRequest;不写清楚模型会误当成"已存在的流程编号/草稿号"。
1828
+ requestIdDesc: '预生成的流程请求 ID:本阶段尚未创建流程,它只是这次将要创建的单据的身份标识(用于后续 createRequest),不是已存在的流程编号或草稿号。',
1829
+ requestName: runtime.default_request_name || '',
1830
+ workflow: context.workflow,
1831
+ form: { id: frame.form_id, layout_id: layoutId },
1832
+ requiredFields: requiredFieldIds,
1833
+ subFormRequired: Object.fromEntries(Object.entries(subFormRequired).map(([k, v]) => [k, v.map((f) => String(f.id ?? ''))])),
1834
+ editableFields: editableFieldsList.map((f) => String(f.id ?? '')),
1835
+ fields: slimFields,
1836
+ initialValues: initValues,
1837
+ systemFields: {
1838
+ permissions: minorInfo.permissions || {},
1839
+ defaults: context.systemFields.defaults,
1840
+ levelOptions: minorInfo.level_options || [],
1841
+ secLevelOptions: minorInfo.sec_level_options || [],
1842
+ },
1843
+ systemFieldWarning: minorWarning,
1844
+ requiredInfoWarning: requiredInfoWarningText(context),
1845
+ // 原 Python 把完整 context 落盘;此处通过 continuation 承载,返回结构保持文档契约。
1846
+ continuation: token,
1391
1847
  },
1392
- systemFieldWarning: minorWarning,
1393
- requiredInfoWarning: requiredInfoWarningText(context),
1394
- ...(conditionalRequired.length ? { conditionalRequired } : {}),
1395
- // 原 Python 把完整 context 落盘;此处通过 continuation 承载,返回结构保持文档契约。
1396
- continuation: token,
1397
- }, env, 'last_create_form.json', writableFieldsCompact));
1848
+ writableFieldsCompact,
1849
+ };
1398
1850
  }
1399
1851
  // ---------------------------------------------------------------------------
1400
1852
  // 输入拆分 / 字段绑定 / 只读校验
@@ -2213,145 +2665,15 @@ function _linkageOptions(sourceContent) {
2213
2665
  result.push([tv, name]);
2214
2666
  }
2215
2667
  }
2216
- return result;
2217
- }
2218
- /**
2219
- * 联动条件里的"期望取值"(`sourceContent` 是 JSON 字符串)。
2220
- *
2221
- * 与 `_linkageOptions`(只服务条件命中判定,取 id/name 两元组)分开:本函数保留 **id + content**,
2222
- * 并在没有任何选项列表时回退到 `dataOptions`/`content` 标量(如 `{"formField":{"id":"…"},"content":"1"}`),
2223
- * 供 `conditionalRequired` 原样展示条件取值用。
2224
- */
2225
- function _linkageConditionValues(sourceContent) {
2226
- if (!sourceContent || typeof sourceContent !== 'string' || !sourceContent.trim())
2227
- return [];
2228
- let data;
2229
- try {
2230
- data = JSON.parse(sourceContent);
2231
- }
2232
- catch {
2233
- return [];
2234
- }
2235
- if (!data || typeof data !== 'object')
2236
- return [];
2237
- const result = [];
2238
- for (const item of data.dataOptions || []) {
2239
- if (!item || typeof item !== 'object')
2240
- continue;
2241
- const id = String(item.optionId ?? '').trim();
2242
- const content = String(item.content ?? '').trim();
2243
- if (id)
2244
- result.push(content ? { id, content } : { id });
2245
- else if (content)
2246
- result.push({ content });
2247
- }
2248
- for (const item of data.optionValues || []) {
2249
- if (!item || typeof item !== 'object')
2250
- continue;
2251
- const id = String(item.textValue ?? '').trim();
2252
- const content = String(item.name ?? '').trim();
2253
- if (id)
2254
- result.push(content ? { id, content } : { id });
2255
- else if (content)
2256
- result.push({ content });
2257
- }
2668
+ // 标量取值回退(2026-09-22):形如 `{"formField":{"id":"…"},"content":"1"}` 的条件(本租户
2669
+ // 「总部费用报销」的 `发票扫描` 就是这一形态)既没有 dataOptions 也没有 optionValues,旧实现返回 []
2670
+ // ⇒ 调用处 `!condValues.length` 直接 `continue` ⇒ **整条条件被静默丢弃**(既不算成立也不算不成立)。
2671
+ // 后果:`发票扫描 ≠ 1` 从规则里消失 ⇒ 用户**已经扫描过发票**(`发票扫描="1"`)时该规则照样命中,误报必填。
2672
+ // 标量取值的统一口径就是回退 `content`。
2258
2673
  if (!result.length && data.content !== undefined && data.content !== null) {
2259
- const content = String(data.content).trim();
2260
- if (content)
2261
- result.push({ content });
2262
- }
2263
- return result;
2264
- }
2265
- /**
2266
- * 「条件必填」清单(V12.1.27 新增能力)。
2267
- *
2268
- * 背景:本租户 OA 的 `getFormLayout` 不下发静态必填标记(`viewRelateConfig` 为空、布局组件 `required`
2269
- * 全为 false),业务必填**只以「显示属性联动」形式存在**——`viewLinkages[]` 中 `linkageProp='required'`
2270
- * 的规则:条件命中时该字段才变必填。
2271
- *
2272
- * 这些规则原先只在 prepare 阶段按"实际所填数据"参与必填计算(命中才出现在 `missingFields`),
2273
- * form 阶段模型完全看不到 ⇒ 模型无法在发起前主动向用户问齐必填项,只能等用户恰巧填对触发字段
2274
- * (2026-09-17 分享会话实证:模型只能说"拿不到必填信息",用户此前也反复反馈"没有必填字段")。
2275
- *
2276
- * 本函数把规则**按 OA 原样结构化输出**(不评价、不做语义改写、不做条件求值):`match` 取
2277
- * `condition.detailType`(AND/OR,缺省 AND),`term` 取 `detailSymbol` 原值
2278
- * (EQUAL/UN_EQUAL/IN/NOT_IN/NULL/NOT_NULL),`values` 为该条件的期望取值。
2279
- * **条件是否命中仍以 prepare 的 `missing_fields` 为准**。
2280
- * 无 required 规则时返回空数组(调用方按需省略该键)。
2281
- */
2282
- function conditionalRequiredFields(linkages, rawFieldsById) {
2283
- if (!Array.isArray(linkages) || !linkages.length)
2284
- return [];
2285
- const fieldMeta = (fieldId) => {
2286
- const raw = rawFieldsById[fieldId];
2287
- if (!raw || typeof raw !== 'object')
2288
- return { name: '', subFormId: '' };
2289
- return {
2290
- name: String(raw.title ?? raw.name ?? raw.dataKey ?? '').trim(),
2291
- subFormId: String(raw.subFormId ?? '').trim(),
2292
- };
2293
- };
2294
- const result = [];
2295
- const seen = new Set();
2296
- for (const linkage of linkages) {
2297
- if (!linkage || typeof linkage !== 'object')
2298
- continue;
2299
- const condition = (linkage.condition && typeof linkage.condition === 'object') ? linkage.condition : null;
2300
- if (!condition)
2301
- continue;
2302
- const conditionDetails = Array.isArray(condition.conditionDetails) ? condition.conditionDetails : [];
2303
- const when = [];
2304
- for (const cd of conditionDetails) {
2305
- if (!cd || typeof cd !== 'object')
2306
- continue;
2307
- const conditionFieldId = String(cd.targetId ?? '').trim();
2308
- if (!conditionFieldId)
2309
- continue;
2310
- const item = { fieldId: conditionFieldId };
2311
- const meta = fieldMeta(conditionFieldId);
2312
- if (meta.name)
2313
- item.fieldName = meta.name;
2314
- item.term = String(cd.detailSymbol ?? 'EQUAL').trim().toUpperCase();
2315
- const sourceType = String(cd.sourceType ?? 'CONSTANT').trim().toUpperCase();
2316
- if (sourceType !== 'CONSTANT') {
2317
- item.sourceType = sourceType;
2318
- const sourceFieldId = String(cd.sourceId ?? '').trim();
2319
- if (sourceFieldId)
2320
- item.sourceFieldId = sourceFieldId;
2321
- }
2322
- const values = _linkageConditionValues(cd.sourceContent);
2323
- if (values.length)
2324
- item.values = values;
2325
- when.push(item);
2326
- }
2327
- if (!when.length)
2328
- continue;
2329
- const match = String(condition.detailType ?? 'AND').trim().toUpperCase() || 'AND';
2330
- const details = Array.isArray(linkage.linkageDetails) ? linkage.linkageDetails : [];
2331
- for (const ld of details) {
2332
- if (!ld || typeof ld !== 'object')
2333
- continue;
2334
- if (String(ld.linkageProp ?? '').trim().toLowerCase() !== 'required')
2335
- continue;
2336
- for (const target of String(ld.linkageValue ?? '').split(',')) {
2337
- const fieldId = target.trim();
2338
- if (!fieldId)
2339
- continue;
2340
- const dedupeKey = [fieldId, match, JSON.stringify(when)].join('\u0000');
2341
- if (seen.has(dedupeKey))
2342
- continue;
2343
- seen.add(dedupeKey);
2344
- const meta = fieldMeta(fieldId);
2345
- const item = { fieldId };
2346
- if (meta.name)
2347
- item.fieldName = meta.name;
2348
- if (meta.subFormId)
2349
- item.subFormId = meta.subFormId;
2350
- item.match = match;
2351
- item.when = when;
2352
- result.push(item);
2353
- }
2354
- }
2674
+ const scalar = String(data.content).trim();
2675
+ if (scalar)
2676
+ result.push([scalar, scalar]);
2355
2677
  }
2356
2678
  return result;
2357
2679
  }
@@ -2571,7 +2893,7 @@ function mergeLinkageDetails(merged, linkageDetails) {
2571
2893
  }
2572
2894
  return result;
2573
2895
  }
2574
- function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues) {
2896
+ function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues, fields) {
2575
2897
  const hiddenSubforms = new Set();
2576
2898
  const rowHidden = {};
2577
2899
  const rowExtraRequired = {};
@@ -2632,6 +2954,47 @@ function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, s
2632
2954
  }
2633
2955
  if (!fieldValues.main)
2634
2956
  fieldValues.main = {};
2957
+ const allRowKeys = Object.keys(fieldValues);
2958
+ /**
2959
+ * 主表作用域字段判定 —— 用于把「主表条件」与「明细行条件」分开求值。
2960
+ *
2961
+ * 背景(2026-09-20 真机 + 配置双证):本租户「总部费用报销」有一条 required 规则是
2962
+ * `相关流程`(明细) ⇐ `发票扫描`(主表) UN_EQUAL "1" AND `费用类型(科目)`(明细) EQUAL 5301手机话费
2963
+ * AND `申请人(废弃)`(主表) NOT_IN […9 人]。旧实现把每个条件都按 `rowKey` 求命中行再求交,
2964
+ * 而主表条件只能落在 `'main'` 桶、明细条件落在 `'<subFormId>:<idx>'` 桶 ⇒ **AND 交集恒空
2965
+ * ⇒ 规则永不命中**,明细行的条件必填被**静默漏报**(`missingFields` 里从不出现 `相关流程`,
2966
+ * `linkageResult` 恒空),模型据此认为"没有新增必填"。
2967
+ *
2968
+ * 口径:主表条件与"具体哪一明细行"无关 ⇒ 它成立时**对所有行放行**(不限制),不成立时给空集
2969
+ * (AND 判死)。只含主表条件的规则因此行为不变(`'main'` 仍在放行集合里),只含明细条件的规则
2970
+ * 完全不变。
2971
+ *
2972
+ * **补记(2026-09-22 真机复核)**:上面这个作用域修复只解决了"交集恒空"的一半。这条规则在修好作用域后
2973
+ * **仍然**判死,原因在另一处:它的第一条必要条件 `发票扫描 UN_EQUAL "1"` 的前提字段在本租户从来拿不到值,
2974
+ * 而求值时要求 `values.length`。现已按「空值即 null ⇒ 与常量不相等」统一口径修正(见下方
2975
+ * `UN_EQUAL`/`NOT_IN` 分支),并补上标量 `content` 条件的解析回退(见 `_linkageOptions`),
2976
+ * 否则该条件会被整条丢弃、导致"已扫描发票"时反而误报必填。
2977
+ */
2978
+ const mainScopeFieldIds = new Set();
2979
+ for (const f of Array.isArray(fields) ? fields : []) {
2980
+ if (!f || typeof f !== 'object')
2981
+ continue;
2982
+ const id = String(f.id ?? '').trim();
2983
+ if (!id)
2984
+ continue;
2985
+ if (!String(f.subFormId ?? '').trim())
2986
+ mainScopeFieldIds.add(id);
2987
+ }
2988
+ /** 无字段元数据时的兜底:只要该字段在任一明细行桶里出现过,就按明细作用域处理 */
2989
+ const isMainScopeField = (fieldId) => {
2990
+ if (mainScopeFieldIds.has(fieldId))
2991
+ return true;
2992
+ if (mainScopeFieldIds.size)
2993
+ return false;
2994
+ if (/^(REQUEST_|SECLEVEL|system_)/u.test(fieldId))
2995
+ return true;
2996
+ return !allRowKeys.some((key) => key !== 'main' && Object.prototype.hasOwnProperty.call(fieldValues[key], fieldId));
2997
+ };
2635
2998
  for (const linkage of linkages) {
2636
2999
  if (!linkage || typeof linkage !== 'object')
2637
3000
  continue;
@@ -2664,8 +3027,9 @@ function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, s
2664
3027
  }
2665
3028
  if (detailSymbol !== 'NULL' && detailSymbol !== 'NOT_NULL' && !sourceIsVariable && !condValues.length)
2666
3029
  continue;
3030
+ const mainScope = isMainScopeField(triggerId);
2667
3031
  const matchedRows = new Set();
2668
- for (const rowKey of Object.keys(fieldValues)) {
3032
+ for (const rowKey of mainScope ? ['main'] : allRowKeys) {
2669
3033
  const values = fieldValues[rowKey][triggerId] || [];
2670
3034
  let condVals = condValues;
2671
3035
  if (sourceIsVariable) {
@@ -2682,7 +3046,11 @@ function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, s
2682
3046
  matchedRows.add(rowKey);
2683
3047
  }
2684
3048
  else if (detailSymbol === 'UN_EQUAL' || detailSymbol === 'NOT_IN') {
2685
- if (values.length && !values.some((v) => condVals.includes(v)))
3049
+ // 空值即 null(2026-09-22 口径):null 与任何常量都不相等,也不落在任何 NOT_IN 名单里 ⇒ 条件成立。
3050
+ // 旧实现额外要求 `values.length`(前提字段必须有值),等于把「字段没填值」当成「条件不成立」,
3051
+ // 使「未扫描发票(`发票扫描 ≠ "1"`,前提字段通常空着)⇒ 相关流程必填」这类规则在**自然场景下稳定漏报**
3052
+ // (真机实例见上方主表作用域注释)。注意与 `NULL` / `NOT_NULL` 分工:那两个比较符才是专门判"空/非空"的。
3053
+ if (!values.some((v) => condVals.includes(v)))
2686
3054
  matchedRows.add(rowKey);
2687
3055
  }
2688
3056
  else {
@@ -2690,6 +3058,14 @@ function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, s
2690
3058
  matchedRows.add(rowKey);
2691
3059
  }
2692
3060
  }
3061
+ // 主表条件成立 ⇒ 对所有行放行(它不针对某一明细行);不成立 ⇒ 空集,AND 时直接把该规则判死。
3062
+ if (mainScope) {
3063
+ const holds = matchedRows.size > 0;
3064
+ matchedRows.clear();
3065
+ if (holds)
3066
+ for (const key of allRowKeys)
3067
+ matchedRows.add(key);
3068
+ }
2693
3069
  cdHitSets.push(matchedRows);
2694
3070
  }
2695
3071
  if (!cdHitSets.length)
@@ -2952,19 +3328,110 @@ function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden,
2952
3328
  return result;
2953
3329
  }
2954
3330
  // ---------------------------------------------------------------------------
2955
- // createPrepare / createApply
3331
+ // createRequest(合并原 createPrepare + createApply)
2956
3332
  // ---------------------------------------------------------------------------
2957
- async function commandCreatePrepare(args, env) {
3333
+ /**
3334
+ * 把 preview 里的字段值渲染成人类可读文本(写进记忆偏好用)。
3335
+ *
3336
+ * `preview[].value` 的形态随字段类型而变:文本是字符串、选项是字符串、浏览/人员/部门是
3337
+ * `{id,name}`(或数组)。这里只取可读的名字部分,避免把 `[object Object]` 写进记忆。
3338
+ */
3339
+ function preferenceTextOf(value) {
3340
+ if (value === undefined || value === null)
3341
+ return '';
3342
+ if (Array.isArray(value)) {
3343
+ return value
3344
+ .map((item) => String((item && typeof item === 'object') ? (item.name ?? item.content ?? '') : item).trim())
3345
+ .filter(Boolean)
3346
+ .join('、');
3347
+ }
3348
+ if (typeof value === 'object')
3349
+ return '';
3350
+ return String(value).trim();
3351
+ }
3352
+ /**
3353
+ * 保存成功后把「本次必填字段的取值」沉淀为该工作流的发起偏好(`agentMemoryExtract`)。
3354
+ *
3355
+ * **放在 CLI 内部做**(模型无感):这是「这次必填项填了什么」这类确定性事实,不是需要语义抽取的
3356
+ * 自由文本——由命令自己记下来最可靠,也省掉「模型任务结束时再补一次 memoryExtract」这一轮往返。
3357
+ * best-effort:失败只返回 `false`,绝不影响发起结果。
3358
+ *
3359
+ * 2026-09-23:**静默化** —— 调用方**不再把返回值写进任何输出字段**(原先回 `memorySaved`)。
3360
+ * 返回值仍是 `boolean`,仅保留给单元测试/内部判断用;路由层不得再把它暴露给模型。
3361
+ */
3362
+ async function saveLaunchPreferences(workflow, requiredFieldIds, preview, env) {
3363
+ const pairs = [];
3364
+ const seen = new Set();
3365
+ for (const item of preview || []) {
3366
+ const fieldId = String(item?.fieldId ?? '').trim();
3367
+ if (!fieldId || !requiredFieldIds.has(fieldId) || seen.has(fieldId))
3368
+ continue;
3369
+ const text = preferenceTextOf(item.value);
3370
+ if (!text)
3371
+ continue;
3372
+ seen.add(fieldId);
3373
+ pairs.push(`${String(item.fieldName ?? fieldId).trim()}=${text}`);
3374
+ }
3375
+ if (!pairs.length)
3376
+ return false;
3377
+ const workflowId = String(workflow.id ?? '').trim();
3378
+ const workflowName = String(workflow.name ?? '').trim();
3379
+ try {
3380
+ const response = await env.host.workflowRequest({
3381
+ method: 'POST',
3382
+ path: WORKFLOW_ENDPOINTS.MEMORY_EXTRACT,
3383
+ operation: 'workflow.createRequest',
3384
+ body: {
3385
+ chatParams: {
3386
+ // appid 必须始终携带(缺省时记忆接口判为无效应用、静默不保存)
3387
+ appid: resolveMemoryAppId(),
3388
+ dynamicContextParams: { ...(workflowId ? { workflowId } : {}) },
3389
+ },
3390
+ // 末尾补 remember 保存触发信号词(与 workflow.memoryExtract 同口径)
3391
+ aiMsgEntityList: [
3392
+ { msgRole: 'user', msgValue: `记住:发起「${workflowName || workflowId}」流程时,这些必填项的常用值为 ${pairs.join(';')}。` },
3393
+ { msgRole: 'user', msgValue: 'remember' },
3394
+ ],
3395
+ },
3396
+ });
3397
+ // 记忆接口业务标志是 status/fail(不是 success);只在明确失败时判未保存
3398
+ const body = (response && typeof response === 'object' ? response : {});
3399
+ if (body.saved === false || body.success === false || body.status === false || body.fail === true)
3400
+ return false;
3401
+ return body.saved === true || body.success === true || body.status === true || body.code === 200 || body.fail === false;
3402
+ }
3403
+ catch {
3404
+ return false;
3405
+ }
3406
+ }
3407
+ /**
3408
+ * `workflow.createRequest` —— **一条命令**完成「字段解析 → 联动 → 必填校验 → 保存 → 发起」。
3409
+ *
3410
+ * 模型只需按用户意图把字段填好直接调用,**不需要自己判断必填是否缺失**:
3411
+ * - `action=submit` 且仍有必填缺失 → **一个写请求都不发**,把全部缺失项一次交回(`NEEDS_INPUT`);
3412
+ * - `action=draft` → 直接创建草稿,缺失项仅作提醒随结果返回(**含联动后才变成必填的字段**);
3413
+ * - 成功保存后,CLI **内部**把必填字段的本次取值沉淀为该工作流的偏好(模型无感)。
3414
+ *
3415
+ * 句柄消费时机(关键):**只在真正要发写入请求之前**消费。缺必填 / 需澄清这类「还没写」的
3416
+ * 返回**不消费**句柄,于是模型补齐字段后可以带**同一个 context** 重发,不必回头重跑 createForm;
3417
+ * 一旦消费,就只能按句柄状态分流(`ALREADY_APPLIED` / 结果不确定 / 并发占用)。
3418
+ */
3419
+ async function commandCreateRequest(args, env) {
3420
+ const action = String(args.action ?? '').trim();
3421
+ if (action !== 'draft' && action !== 'submit') {
3422
+ throw workflowValidationError('action_required', 'action 必填,且只能是 draft(仅存草稿)或 submit(提交审批)');
3423
+ }
2958
3424
  const token = String(args.context ?? '');
2959
3425
  if (!token)
2960
3426
  throw workflowValidationError('context_required', '需要 createForm 返回的 continuation(context)');
3427
+ // 只读取载荷、**不消费**:见上方「句柄消费时机」
2961
3428
  const lease = await verifyContextLease(env, token, 'workflow.createForm');
2962
3429
  const context = lease.payload;
2963
3430
  const [main, detailValues] = splitInput(args.data);
2964
3431
  const fields = Array.isArray(context.fields) ? context.fields : [];
2965
3432
  const [entries, fieldAmbiguities] = bindInputFields(main, detailValues, fields);
2966
3433
  if (fieldAmbiguities.length) {
2967
- return workflowSuccess('workflow.createPrepare', {
3434
+ return workflowSuccess('workflow.createRequest', {
2968
3435
  status: 'NEEDS_CONFIRMATION',
2969
3436
  contextId: token,
2970
3437
  message: '存在重名或无法识别的表单字段,请改用字段 id。',
@@ -2973,7 +3440,7 @@ async function commandCreatePrepare(args, env) {
2973
3440
  }
2974
3441
  const readonlyHits = readonlyEntryViolations(entries);
2975
3442
  if (readonlyHits.length) {
2976
- return workflowSuccess('workflow.createPrepare', {
3443
+ return workflowSuccess('workflow.createRequest', {
2977
3444
  status: 'NEEDS_CONFIRMATION',
2978
3445
  contextId: token,
2979
3446
  message: '以下字段是只读/禁止手工编辑字段,不能填写:' + readonlyHits.map((h) => h.name || h.fieldId).join('、'),
@@ -2988,7 +3455,7 @@ async function commandCreatePrepare(args, env) {
2988
3455
  const merged2 = fillDetailDefaultsByRow(mergeLinkageDetails(merged, linkageDetails), context.defaults || [], userDetailRows(entries));
2989
3456
  const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
2990
3457
  const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
2991
- const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues);
3458
+ const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues, fields);
2992
3459
  const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
2993
3460
  if (initialHidden.size) {
2994
3461
  const mh = new Set(rowHidden.main || []);
@@ -2996,7 +3463,10 @@ async function commandCreatePrepare(args, env) {
2996
3463
  mh.add(id);
2997
3464
  rowHidden.main = mh;
2998
3465
  }
2999
- const missing = missingRequiredFields(fields, entries, merged2, hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), new Set(context.layoutSubformIds || []));
3466
+ const hiddenFieldIds = new Set(context.hiddenCellFieldIds || []);
3467
+ const compsFieldIds = new Set(context.compsFieldIds || []);
3468
+ const layoutSubformIds = new Set(context.layoutSubformIds || []);
3469
+ const missing = missingRequiredFields(fields, entries, merged2, hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled, hiddenFieldIds, compsFieldIds, layoutSubformIds);
3000
3470
  const sysPermissions = (context.systemFields && context.systemFields.permissions) || {};
3001
3471
  const sysDefaults = (context.systemFields && context.systemFields.defaults) || {};
3002
3472
  const requestLevelArg = String(args.requestLevel ?? '').trim();
@@ -3013,20 +3483,39 @@ async function commandCreatePrepare(args, env) {
3013
3483
  if (perm.ismandatory && !value)
3014
3484
  systemMissing.push({ key: sysKey, name: SYSTEM_FIELD_KEYS[sysKey], subFormId: '', systemField: true });
3015
3485
  }
3016
- const intent = String(args.intent ?? 'submit');
3017
- if (valueAmbiguities.length || ((missing.length || systemMissing.length) && intent !== 'draft')) {
3018
- return workflowSuccess('workflow.createPrepare', {
3019
- status: valueAmbiguities.length ? 'NEEDS_CONFIRMATION' : 'NEEDS_INPUT',
3486
+ const linkageResult = linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, hiddenFieldIds, compsFieldIds, entries, layoutSubformIds);
3487
+ // 「显示属性联动」实际命中后新变成必填的字段(rowExtraRequired 就是条件必填的求值结果)。
3488
+ // draft 只做提醒,必须把它们点出来——否则用户拿到的是一份"看着挺完整、提交时才被拦"的草稿。
3489
+ const linkageRequiredIds = new Set();
3490
+ for (const key of Object.keys(rowExtraRequired)) {
3491
+ for (const fieldId of rowExtraRequired[key])
3492
+ linkageRequiredIds.add(String(fieldId));
3493
+ }
3494
+ const missingAll = [...missing, ...systemMissing];
3495
+ if (valueAmbiguities.length) {
3496
+ return workflowSuccess('workflow.createRequest', {
3497
+ status: 'NEEDS_CONFIRMATION',
3020
3498
  contextId: token,
3021
- message: valueAmbiguities.length ? '存在无法唯一解析的选项,或无法匹配的浏览按钮值,请让用户确认。' : '缺少必填字段。',
3499
+ message: '存在无法唯一解析的选项,或无法匹配的浏览按钮值,请让用户确认。',
3022
3500
  // 已解析成功的字段回显(V12.1.34):入参给的是名称时,这里给出**服务端最终解析结果**
3023
3501
  // (选项给名称、浏览/人员/部门给 {id,name} 数组),模型据此自查"我传的值到底变成了什么对象",
3024
- // 不必把不确定性转嫁给用户确认。READY 时同一内容在 `fields`(确认预览),此处不再重复。
3025
- // 之前这条短路返回完全没有回显,模型因此抱怨"prepare 输出里看不到字段预览"。
3502
+ // 不必把不确定性转嫁给用户确认。
3026
3503
  resolvedValues: preview,
3027
3504
  ambiguities: valueAmbiguities,
3028
- missingFields: [...missing, ...systemMissing],
3029
- linkageResult: linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), entries, new Set(context.layoutSubformIds || [])),
3505
+ missingFields: missingAll,
3506
+ linkageResult,
3507
+ ...folderPayload,
3508
+ });
3509
+ }
3510
+ if (action === 'submit' && missingAll.length) {
3511
+ // submit 且有必填缺失:**不调用创建接口**,把缺失项一次交回给模型/用户
3512
+ return workflowSuccess('workflow.createRequest', {
3513
+ status: 'NEEDS_INPUT',
3514
+ contextId: token,
3515
+ message: '缺少必填字段,本次未发起流程。请把下面这些字段一次问齐后,带同一 context 重发 action=submit。',
3516
+ resolvedValues: preview,
3517
+ missingFields: missingAll,
3518
+ linkageResult,
3030
3519
  ...folderPayload,
3031
3520
  });
3032
3521
  }
@@ -3042,37 +3531,176 @@ async function commandCreatePrepare(args, env) {
3042
3531
  remark: String(args.remark ?? ''),
3043
3532
  preview,
3044
3533
  previewDetails: buildPreviewDetails(preview),
3045
- intent,
3046
- missingFields: (intent === 'draft') ? [...missing, ...systemMissing] : [],
3534
+ intent: action,
3535
+ missingFields: missingAll,
3047
3536
  inputFieldIds,
3048
3537
  };
3049
- const session = await getSessionContext(env);
3050
- const outToken = await env.continuation.issue({
3051
- operation: 'workflow.createPrepare',
3052
- baseUrl: session.baseUrl,
3053
- profile: session.profile,
3054
- userId: session.userId,
3055
- payload: { context, prepared },
3056
- }, CONTINUATION_TTL_MS);
3057
- return workflowSuccess('workflow.createPrepare', {
3058
- status: 'READY',
3059
- contextId: outToken,
3060
- requestId: context.runtime.request_id,
3538
+ // ── 以下进入写入路径 ──────────────────────────────────────────────────────
3539
+ // 先把句柄**消费**掉(原子抢占,一次性防重放),再发任何写请求:抢不到说明已有(并发的)
3540
+ // 执行占用该句柄,此时**绝不能写入**,按占用状态给出可执行的处置说明。
3541
+ await verifyContextLease(env, token, 'workflow.createForm', true);
3542
+ // 只读硬拦截兜底:复核本次入参字段此刻是否仍可写
3543
+ if (inputFieldIds.length) {
3544
+ const fieldsById = new Map((context.fields || []).map((f) => [String(f.id ?? ''), f]));
3545
+ const hits = inputFieldIds.filter((fid) => {
3546
+ const f = fieldsById.get(fid);
3547
+ return f && (f.readOnly || f.disableEdit);
3548
+ }).map((fid) => ({ fieldId: fid, name: (fieldsById.get(fid) || {}).name ?? fid }));
3549
+ if (hits.length) {
3550
+ throw workflowPolicyError('field_readonly', '提交数据包含只读/禁止手工编辑字段,已停止保存:' + hits.map((h) => h.name || h.fieldId).join('、'), { notEditable: hits });
3551
+ }
3552
+ }
3553
+ // 保存时字段联动(EVENT_SAVE_DATA)
3554
+ if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
3555
+ const saveDetails = prepared.dataDetails || [];
3556
+ const saveEntries = _entriesFromDefaults(saveDetails);
3557
+ if (saveEntries.length) {
3558
+ try {
3559
+ const saveLinkage = await runFieldLinkage(env, linkageContextFromPayload(context), saveEntries, new Set(['EVENT_SAVE_DATA']), saveDetails, false);
3560
+ if (saveLinkage.length)
3561
+ prepared.dataDetails = mergeLinkageDetails(saveDetails, saveLinkage);
3562
+ }
3563
+ catch {
3564
+ /* 写操作红线:不确定不重试,仅跳过 */
3565
+ }
3566
+ }
3567
+ }
3568
+ // 阶段推进到"要发写入请求了":此后再被中止/杀掉,重放只能按**结果不确定**处理(先只读核对),
3569
+ // 而在此之前被中止则允许安全重试同一句柄。写不进阶段就中止(宁可不发,也不留无法判定的一次性写入)。
3570
+ await env.continuation.markStage(token, 'saving');
3571
+ let saveData;
3572
+ try {
3573
+ const saveResponse = await env.host.workflowRequest({
3574
+ method: 'POST',
3575
+ path: formApiUrl(FORM_ENDPOINTS.save, context.frame.relate_type),
3576
+ query: { formId: context.layout.form_id, ...AJAX_QUERY },
3577
+ body: buildSaveBody(context, prepared),
3578
+ operation: 'workflow.createRequest',
3579
+ });
3580
+ saveData = parseSaveResponse(saveResponse);
3581
+ }
3582
+ catch (error) {
3583
+ if (isNetworkError(error)) {
3584
+ throw workflowPartialError('saveFormData 请求发送后连接中断,是否已创建不确定,禁止自动重试,请通过 workflow.list 复核', { attempted: { requestId: context.runtime.request_id } }, { stage: 'saveFormData' });
3585
+ }
3586
+ throw error;
3587
+ }
3588
+ const submitIntent = action === 'submit';
3589
+ let verification = {};
3590
+ let needConfirm = false;
3591
+ if (submitIntent) {
3592
+ verification = await runCreateNodeFieldVerification(env, context);
3593
+ const reminders = verification.reminders || [];
3594
+ needConfirm = verification.enabled
3595
+ ? (Boolean(reminders.length && verification.noPassPrompt === 1) || (!reminders.length && verification.passPopUpPrompt === 1))
3596
+ : false;
3597
+ }
3598
+ const createSrc = (submitIntent && !verification.blocked && !(needConfirm && args.verificationConfirmed !== true)) ? 'submit' : 'save';
3599
+ // 提交时字段联动(EVENT_SUBMIT_OR_AGREE)
3600
+ if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
3601
+ const submitDetails = prepared.dataDetails || [];
3602
+ const submitEntries = _entriesFromDefaults(submitDetails);
3603
+ if (submitEntries.length) {
3604
+ try {
3605
+ const submitLinkage = await runFieldLinkage(env, linkageContextFromPayload(context), submitEntries, new Set(['EVENT_SUBMIT_OR_AGREE']), submitDetails, false);
3606
+ if (submitLinkage.length)
3607
+ prepared.dataDetails = mergeLinkageDetails(submitDetails, submitLinkage);
3608
+ }
3609
+ catch {
3610
+ /* 跳过 */
3611
+ }
3612
+ }
3613
+ }
3614
+ let created;
3615
+ try {
3616
+ const createResponse = await env.host.workflowRequest({
3617
+ method: 'POST',
3618
+ path: FORM_ENDPOINTS.create,
3619
+ query: AJAX_QUERY,
3620
+ body: buildCreateBody(context, resolvePrepared(context, prepared), saveData, createSrc),
3621
+ operation: 'workflow.createRequest',
3622
+ });
3623
+ // parseCreateResponse 需读外层 status/code 并向下遍历 data/result,必须传原始响应
3624
+ created = parseCreateResponse(createResponse, context.runtime.request_id);
3625
+ // flow/create 已返回成功:阶段推进到 committed。若进程在"回写成功结果"之前被中止,
3626
+ // 重放可据此报幂等成功(ALREADY_APPLIED + requestId),而不是只报一句"已使用"让人反查。
3627
+ await env.continuation
3628
+ .markStage(token, 'committed', { requestId: context.runtime.request_id })
3629
+ .catch(() => undefined);
3630
+ }
3631
+ catch (error) {
3632
+ if (isNetworkError(error)) {
3633
+ throw workflowPartialError('saveFormData 已成功,但 flow/create 结果失败或不确定;禁止自动重试。', { stage: 'flow/create', ...saveData }, { stage: 'flow/create' });
3634
+ }
3635
+ throw error;
3636
+ }
3637
+ const requestId = context.runtime.request_id;
3638
+ const baseUrl = (await getSessionContext(env)).baseUrl;
3639
+ const result = {
3640
+ status: 'CREATED',
3641
+ message: '流程已创建并保存为草稿。',
3642
+ contextId: token,
3643
+ requestId,
3644
+ requestName: prepared.requestName,
3645
+ requestId_alt: requestId,
3646
+ requestMark: created.requestMark || null,
3647
+ requestName_alt: created.requestName || prepared.requestName,
3061
3648
  workflow: context.workflow,
3062
- requestName,
3063
- requestLevel: requestLevelValue,
3064
- secLevel: secLevelValue,
3649
+ url: `${baseUrl}/sp/workflow/flowpage/view/${requestId}?operResultType=SUCCESS_RELOAD_CURRENT_PAGE&userSpecifiedNodeId=0&autoMaxDisplay=0`,
3650
+ linkageResult,
3651
+ // 缺必填清单**恒有值**(不再"只在 draft 时才有"):submit 成功时为空数组,draft 时是
3652
+ // 「提交前还需补齐」的提醒。字段恒定存在,模型不需要按 status 分支去猜它有没有。
3653
+ missingFields: missingAll,
3654
+ // 字段落库结果回显:写完之后模型唯一能看到"我填的值到底存成了什么"的地方
3655
+ // (按返回值汇报即可,不必为了核对再查一次流程详情)。
3065
3656
  fields: preview,
3066
- previewDetails: buildPreviewDetails(preview),
3067
- confirmationRequired: true,
3068
- linkageResult: linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), entries, new Set(context.layoutSubformIds || [])),
3069
- missingFields: (intent === 'draft') ? [...missing, ...systemMissing] : [],
3070
- ...folderPayload,
3071
- message: (!missing.length && !systemMissing.length)
3072
- ? '尚未调用 saveFormData 和 flow/create。请向用户展示预览并获得明确确认。'
3073
- : `仅发起保存(draft)模式:以下必填字段未填写:${[...missing, ...systemMissing].map((m) => m.name).join('、')}。可继续 create 保存草稿,或补全字段后重新 prepare。`,
3074
- continuation: outToken,
3075
- });
3657
+ previewDetails: prepared.previewDetails,
3658
+ };
3659
+ if (submitIntent) {
3660
+ if (verification.blocked) {
3661
+ result.message = '流程已创建,但因节点字段校验未通过未提交审批,请调整后重新提交。';
3662
+ result.verification = verification;
3663
+ }
3664
+ else if (needConfirm && args.verificationConfirmed !== true) {
3665
+ result.message = '流程已创建,节点字段校验通过。是否继续提交审批?(确认后请告知,将用 submit 命令提交)';
3666
+ result.needVerificationConfirm = true;
3667
+ result.verification = verification;
3668
+ result.nodeId = context.runtime.node_id;
3669
+ }
3670
+ else {
3671
+ result.status = 'CREATED_SUBMITTED';
3672
+ result.message = '流程已发起并提交审批。';
3673
+ result.verification = verification;
3674
+ }
3675
+ }
3676
+ else {
3677
+ // draft:缺失项**只作提醒**(提交前需补齐);联动后才新出现的必填单独点出来。
3678
+ result.missingFields = missingAll;
3679
+ if (missingAll.length) {
3680
+ const linkageMissed = missingAll.filter((m) => !m.systemField && linkageRequiredIds.has(String(m.id)));
3681
+ result.missingFieldsNotice = '草稿已保存。以下必填字段尚未填写,提交前请补齐:'
3682
+ + missingAll.map((m) => m.name).join('、')
3683
+ + (linkageMissed.length
3684
+ ? `。其中 ${linkageMissed.map((m) => m.name).join('、')} 是联动后才变成必填的字段(当前所填数据已命中其触发条件)。`
3685
+ : '。');
3686
+ }
3687
+ }
3688
+ // 必填字段的本次取值 → 该工作流的发起偏好(CLI 内部完成、**静默**,见下方调用点注释)
3689
+ const requiredFieldIds = new Set();
3690
+ for (const field of fields) {
3691
+ if (field.readOnly || field.disableEdit)
3692
+ continue;
3693
+ if (field.required)
3694
+ requiredFieldIds.add(String(field.id ?? ''));
3695
+ }
3696
+ for (const fieldId of linkageRequiredIds)
3697
+ requiredFieldIds.add(fieldId);
3698
+ // 2026-09-23 用户裁定:必填偏好的沉淀必须**静默** —— 只调接口,**不写进任何返回字段**
3699
+ // (此前会回 `memorySaved`,等于把这个内部副作用暴露给模型:可被复述、可被据以推断"已经记过了")。
3700
+ // 它的用途只是「后续发起同类流程时,可直接用历史偏好补齐必填项」——减少对用户的打扰、提升发起速度,
3701
+ // 属于纯 CLI 内部行为,因此**输出里不得出现任何痕迹**。best-effort:失败也不影响发起结果。
3702
+ await saveLaunchPreferences(context.workflow || {}, requiredFieldIds, preview, env);
3703
+ return workflowSuccess('workflow.createRequest', result);
3076
3704
  }
3077
3705
  function formatRemark(value) {
3078
3706
  const text = String(value ?? '').trim();
@@ -3309,146 +3937,6 @@ async function runCreateNodeFieldVerification(env, context) {
3309
3937
  result.passed = !result.blocked;
3310
3938
  return result;
3311
3939
  }
3312
- async function commandCreateApply(args, env) {
3313
- if (args.confirm !== true)
3314
- throw workflowValidationError('confirm_required', '必须在用户明确确认后传入 confirm=true');
3315
- const token = String(args.context ?? '');
3316
- if (!token)
3317
- throw workflowValidationError('context_required', '需要 createPrepare 返回的 continuation(context)');
3318
- const lease = await verifyContextLease(env, token, 'workflow.createPrepare', true);
3319
- const { context, prepared } = lease.payload;
3320
- // 只读硬拦截兜底:复核 prepare 记录的输入字段此刻是否仍可写
3321
- const inputIds = prepared.inputFieldIds || [];
3322
- if (inputIds.length) {
3323
- const fieldsById = new Map((context.fields || []).map((f) => [String(f.id ?? ''), f]));
3324
- const readonlyHits = inputIds.filter((fid) => {
3325
- const f = fieldsById.get(fid);
3326
- return f && (f.readOnly || f.disableEdit);
3327
- }).map((fid) => ({ fieldId: fid, name: (fieldsById.get(fid) || {}).name ?? fid }));
3328
- if (readonlyHits.length) {
3329
- throw workflowPolicyError('field_readonly', '提交数据包含只读/禁止手工编辑字段,已停止保存:' + readonlyHits.map((h) => h.name || h.fieldId).join('、'), { notEditable: readonlyHits });
3330
- }
3331
- }
3332
- // 保存时字段联动(EVENT_SAVE_DATA)
3333
- if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
3334
- const saveDetails = prepared.dataDetails || [];
3335
- const saveEntries = _entriesFromDefaults(saveDetails);
3336
- if (saveEntries.length) {
3337
- try {
3338
- const saveLinkage = await runFieldLinkage(env, linkageContextFromPayload(context), saveEntries, new Set(['EVENT_SAVE_DATA']), saveDetails, false);
3339
- if (saveLinkage.length)
3340
- prepared.dataDetails = mergeLinkageDetails(saveDetails, saveLinkage);
3341
- }
3342
- catch {
3343
- /* 写操作红线:不确定不重试,仅跳过 */
3344
- }
3345
- }
3346
- }
3347
- // 阶段推进到"要发写入请求了":此后再被中止/杀掉,重放只能按**结果不确定**处理(先只读核对),
3348
- // 而在此之前被中止则允许安全重试同一句柄。写不进阶段就中止(宁可不发,也不留无法判定的一次性写入)。
3349
- await env.continuation.markStage(token, 'saving');
3350
- let saveData;
3351
- try {
3352
- const saveResponse = await env.host.workflowRequest({
3353
- method: 'POST',
3354
- path: formApiUrl(FORM_ENDPOINTS.save, context.frame.relate_type),
3355
- query: { formId: context.layout.form_id, ...AJAX_QUERY },
3356
- body: buildSaveBody(context, prepared),
3357
- operation: 'workflow.createApply',
3358
- });
3359
- saveData = parseSaveResponse(saveResponse);
3360
- }
3361
- catch (error) {
3362
- if (isNetworkError(error)) {
3363
- throw workflowPartialError('saveFormData 请求发送后连接中断,是否已创建不确定,禁止自动重试,请通过 workflow.list 复核', { attempted: { requestId: context.runtime.request_id } }, { stage: 'saveFormData' });
3364
- }
3365
- throw error;
3366
- }
3367
- const submitIntent = String(args.intent ?? 'submit') === 'submit';
3368
- let verification = {};
3369
- let needConfirm = false;
3370
- if (submitIntent) {
3371
- verification = await runCreateNodeFieldVerification(env, context);
3372
- const reminders = verification.reminders || [];
3373
- needConfirm = verification.enabled
3374
- ? (Boolean(reminders.length && verification.noPassPrompt === 1) || (!reminders.length && verification.passPopUpPrompt === 1))
3375
- : false;
3376
- }
3377
- const createSrc = (submitIntent && !verification.blocked && !(needConfirm && args.verificationConfirmed !== true)) ? 'submit' : 'save';
3378
- // 提交时字段联动(EVENT_SUBMIT_OR_AGREE)
3379
- if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
3380
- const submitDetails = prepared.dataDetails || [];
3381
- const submitEntries = _entriesFromDefaults(submitDetails);
3382
- if (submitEntries.length) {
3383
- try {
3384
- const submitLinkage = await runFieldLinkage(env, linkageContextFromPayload(context), submitEntries, new Set(['EVENT_SUBMIT_OR_AGREE']), submitDetails, false);
3385
- if (submitLinkage.length)
3386
- prepared.dataDetails = mergeLinkageDetails(submitDetails, submitLinkage);
3387
- }
3388
- catch {
3389
- /* 跳过 */
3390
- }
3391
- }
3392
- }
3393
- let created;
3394
- try {
3395
- const createResponse = await env.host.workflowRequest({
3396
- method: 'POST',
3397
- path: FORM_ENDPOINTS.create,
3398
- query: AJAX_QUERY,
3399
- body: buildCreateBody(context, resolvePrepared(context, prepared), saveData, createSrc),
3400
- operation: 'workflow.createApply',
3401
- });
3402
- // parseCreateResponse 需读外层 status/code 并向下遍历 data/result,必须传原始响应
3403
- created = parseCreateResponse(createResponse, context.runtime.request_id);
3404
- // flow/create 已返回成功:阶段推进到 committed。若进程在"回写成功结果"之前被中止,
3405
- // 重放可据此报幂等成功(ALREADY_APPLIED + requestId),而不是只报一句"已使用"让人反查。
3406
- await env.continuation
3407
- .markStage(token, 'committed', { requestId: context.runtime.request_id })
3408
- .catch(() => undefined);
3409
- }
3410
- catch (error) {
3411
- if (isNetworkError(error)) {
3412
- throw workflowPartialError('saveFormData 已成功,但 flow/create 结果失败或不确定;禁止自动重试。', { stage: 'flow/create', ...saveData }, { stage: 'flow/create' });
3413
- }
3414
- throw error;
3415
- }
3416
- const requestId = context.runtime.request_id;
3417
- const requestName = prepared.requestName;
3418
- const baseUrl = (await getSessionContext(env)).baseUrl;
3419
- const result = {
3420
- status: 'CREATED',
3421
- message: '流程已创建并保存为草稿。',
3422
- contextId: token,
3423
- requestId,
3424
- requestName,
3425
- requestId_alt: requestId,
3426
- requestMark: created.requestMark || null,
3427
- requestName_alt: created.requestName || requestName,
3428
- workflow: context.workflow,
3429
- url: `${baseUrl}/sp/workflow/flowpage/view/${requestId}?operResultType=SUCCESS_RELOAD_CURRENT_PAGE&userSpecifiedNodeId=0&autoMaxDisplay=0`,
3430
- };
3431
- if (submitIntent) {
3432
- if (verification.blocked) {
3433
- result.status = 'CREATED';
3434
- result.message = '流程已创建,但因节点字段校验未通过未提交审批,请调整后重新提交。';
3435
- result.verification = verification;
3436
- }
3437
- else if (needConfirm && args.verificationConfirmed !== true) {
3438
- result.status = 'CREATED';
3439
- result.message = '流程已创建,节点字段校验通过。是否继续提交审批?(确认后请告知,将用 submit 命令提交)';
3440
- result.needVerificationConfirm = true;
3441
- result.verification = verification;
3442
- result.nodeId = context.runtime.node_id;
3443
- }
3444
- else {
3445
- result.status = 'CREATED_SUBMITTED';
3446
- result.message = '流程已发起并提交审批。';
3447
- result.verification = verification;
3448
- }
3449
- }
3450
- return workflowSuccess('workflow.createApply', result);
3451
- }
3452
3940
  // ---------------------------------------------------------------------------
3453
3941
  // editLoad / editPrepare / editSave
3454
3942
  // ---------------------------------------------------------------------------
@@ -3805,7 +4293,7 @@ async function commandEditPrepare(args, env) {
3805
4293
  const merged2 = mergeLinkageDetails(merged, linkageDetails);
3806
4294
  const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
3807
4295
  const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
3808
- const [hiddenSubforms, rowHidden, , rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.currentDetails || [], linkageDetails, context.systemFieldValues);
4296
+ const [hiddenSubforms, rowHidden, , rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.currentDetails || [], linkageDetails, context.systemFieldValues, fields);
3809
4297
  const previewAll = [...preview];
3810
4298
  const fieldsById = new Map();
3811
4299
  for (const f of fields)
@@ -3896,7 +4384,7 @@ async function commandEditSave(args, env) {
3896
4384
  }
3897
4385
  }
3898
4386
  // 阶段推进到"要发写入请求了":此后再被中止/杀掉,重放只能按**结果不确定**处理(先只读核对),
3899
- // 而在此之前被中止则允许安全重试同一句柄(与 createApply 对称)。
4387
+ // 而在此之前被中止则允许安全重试同一句柄(与 createRequest 对称)。
3900
4388
  await env.continuation.markStage(token, 'saving');
3901
4389
  let data;
3902
4390
  try {
@@ -3936,122 +4424,6 @@ async function commandEditSave(args, env) {
3936
4424
  saveLinkage: Boolean(resolvePrepared(context, prepared).dataDetails?.length),
3937
4425
  });
3938
4426
  }
3939
- // ---------------------------------------------------------------------------
3940
- // createList(查询可发起工作流)
3941
- // ---------------------------------------------------------------------------
3942
- /**
3943
- * `createList` 大输出保护。
3944
- *
3945
- * 未传 `name` / `workflowId` 时本命令返回**当前用户全部可发起工作流**,流程多的租户动辄数千个
3946
- * (线上实测某租户 3,862 个、美化后约 460 KB),远超宿主的单次输出线(≈28 KB)⇒ 会被截成
3947
- * 2048 B 预览,模型只能自己写脚本去读文件(线上会话即如此绕行)。
3948
- *
3949
- * 对齐同文件的 `compactFormOutput` 与 `read.ts` 的 `enforceSearchStdoutBudget`:超安全线时把
3950
- * 完整清单落盘,stdout 只保留前 `CREATE_LIST_INLINE_LIMIT` 条,并在**最前面**给出 `resultFile`
3951
- * (被截断也看得见);落盘不可用时保持全量(宁大不丢数据),只补一句说明。
3952
- *
3953
- * 注意:本命令的 `SELECTED`(1 条)/ `NOT_FOUND`(0 条)分支体积天然很小,无需过此函数。
3954
- */
3955
- const CREATE_LIST_INLINE_LIMIT = 100;
3956
- async function compactCreateListOutput(payload, env) {
3957
- const bytes = stdoutBytesOf(payload);
3958
- if (bytes <= STDOUT_SAFE_BYTES)
3959
- return payload;
3960
- const workflows = toArray(payload.workflows);
3961
- const resultFile = await writeStateFileBestEffort('last_create_list.json', JSON.stringify(payload, null, 2), env.ctx);
3962
- if (!resultFile) {
3963
- return {
3964
- ...payload,
3965
- outputBytes: bytes,
3966
- largeOutputNotice: `可发起工作流较多(${workflows.length} 个)且本地落盘不可用,已按原样返回全量 workflows。`
3967
- + '请改用 name 或 workflowId 精确定位,不要整份读入上下文。',
3968
- };
3969
- }
3970
- return withLeadingMeta({
3971
- resultFile,
3972
- persistedResult: true,
3973
- outputBytes: bytes,
3974
- workflowsTotal: workflows.length,
3975
- workflowsTruncated: true,
3976
- largeOutputNotice: `可发起工作流较多(${workflows.length} 个),完整清单已写入 resultFile;`
3977
- + `stdout 只保留前 ${CREATE_LIST_INLINE_LIMIT} 条。`
3978
- + '需要更多候选时从 resultFile 按需提取(不要整份读入上下文)——更推荐直接传 `name`(按名称匹配)'
3979
- + '或 `workflowId`(精确校验)来定位,避免拉全量。读取本输出请一次读完,不要用 head/tail 截断。',
3980
- }, {
3981
- ...payload,
3982
- workflows: workflows.slice(0, CREATE_LIST_INLINE_LIMIT),
3983
- });
3984
- }
3985
- async function commandCreateList(args, env) {
3986
- const workflowId = String(args.workflowId ?? '').trim();
3987
- const name = String(args.name ?? '').trim();
3988
- // `name` 与 `workflowId` 都不传时**直接返回提示、不发请求**:这种调用(线上会话出现过
3989
- // `--input-json '{}'`)只会拿到当前用户的全部可发起工作流(线上实测 3,862 条),既定位不到
3990
- // 用户要的流程,体积也必然超输出线 —— 与其拉下来再落盘/截断兜底,不如根本不发这一次请求。
3991
- if (!workflowId && !name) {
3992
- return workflowSuccess('workflow.createList', {
3993
- status: 'MISSING_QUERY',
3994
- message: '未提供 name 或 workflowId,已跳过查询。不传这两个参数时只能拉取当前用户的全部可发起工作流,'
3995
- + '既无法定位到用户要的流程、体积也远超单次输出上限。'
3996
- + '请先用业务流程名称(name)做模糊匹配,或直接传已知的 workflowId 做精确校验。',
3997
- workflows: [],
3998
- });
3999
- }
4000
- const response = await requestJson(env, 'POST', FORM_ENDPOINTS.workflowCreateList, AJAX_QUERY, { wfpBelongTypeCode: '1', loadOfsFlow: '0', clientCtrl: 1 });
4001
- // requestJson 内部已解包一层(responseData),此处不可再取 .data
4002
- const data = (response && typeof response === 'object') ? response : null;
4003
- if (!data || typeof data !== 'object')
4004
- throw workflowApiError('format_error', '工作流列表返回 data 格式错误');
4005
- if (workflowId) {
4006
- const authorized = simplifyWorkflows(data);
4007
- const exact = authorized.filter((item) => String(item.id ?? '').trim() === workflowId);
4008
- if (exact.length) {
4009
- return workflowSuccess('workflow.createList', {
4010
- status: 'SELECTED',
4011
- workflow: exact[0],
4012
- selectionSource: 'workflowId',
4013
- message: '已按记忆中的 workflowId 精确确认可发起工作流,无需用户选择。',
4014
- });
4015
- }
4016
- return workflowSuccess('workflow.createList', {
4017
- status: 'NOT_FOUND',
4018
- query: name,
4019
- workflowId,
4020
- message: '记忆中的 workflowId 不在当前用户可发起工作流列表中,可能已无发起权限或工作流已停用。',
4021
- workflows: [],
4022
- });
4023
- }
4024
- const result = simplifyWorkflows(data, name);
4025
- if (!result.length) {
4026
- return workflowSuccess('workflow.createList', { status: 'NOT_FOUND', query: name, workflows: [] });
4027
- }
4028
- if (result.length === 1) {
4029
- return workflowSuccess('workflow.createList', { status: 'SELECTED', workflow: result[0] });
4030
- }
4031
- // 多命中时**先按全名精确匹配优选**:候选里恰好有一个名字与用户所写完全相同的(如"总部费用报销"
4032
- // 对上「总部费用报销 / 总部费用报销流程 / 总部费用报销(自定义)」),就直接选中它,不让用户
4033
- // 为"是不是要多打几个字"再选一次。这与查询侧 `resolveTerm` 的候选优选是**同一套判据**。
4034
- // 只有在"没有任何候选与所写完全一致"(或完全一致的有多个、仍是歧义)时才请用户选择。
4035
- const keyword = name.toLowerCase();
4036
- const exact = keyword
4037
- ? result.filter((item) => String(item.workflowname ?? '').trim().toLowerCase() === keyword)
4038
- : [];
4039
- if (exact.length === 1) {
4040
- return workflowSuccess('workflow.createList', {
4041
- status: 'SELECTED',
4042
- workflow: exact[0],
4043
- selectionSource: 'exactName',
4044
- message: `名称与「${name}」完全一致的工作流只有 1 个,已直接选中,无需用户选择。`,
4045
- });
4046
- }
4047
- const selected = exact.length ? exact : result;
4048
- return workflowSuccess('workflow.createList', await compactCreateListOutput({
4049
- status: 'NEEDS_SELECTION',
4050
- message: '匹配到多个工作流,且其中没有与所写名称完全一致的,请让用户选择后再继续。',
4051
- query: name,
4052
- workflows: selected,
4053
- }, env));
4054
- }
4055
4427
  function simplifyWorkflows(data, name = '') {
4056
4428
  const groups = Array.isArray(data.listDataGroup) ? data.listDataGroup : [];
4057
4429
  const typeNames = {};
@@ -4086,44 +4458,33 @@ function simplifyWorkflows(data, name = '') {
4086
4458
  // ---------------------------------------------------------------------------
4087
4459
  // operation 定义
4088
4460
  // ---------------------------------------------------------------------------
4089
- const workflowCreateList = {
4090
- name: 'workflow.createList',
4091
- resource: 'workflow',
4092
- method: 'createList',
4093
- action: 'workflow.createList',
4094
- summary: '查询当前用户可发起的工作流列表(按名称或记忆 workflowId 精确确认)',
4095
- capability: 'create',
4096
- risk: 'read',
4097
- requiresConfirmation: false,
4098
- inputSchema: schemaOf({
4099
- name: { type: 'string', description: '工作流名称关键字(模糊匹配)。**与 workflowId 至少传一个**——两个都不传时本命令不做查询,直接返回 status=MISSING_QUERY 的提示(不传参数只能拉取全部可发起工作流,无法定位用户要的流程)' },
4100
- workflowId: { type: 'string', description: '记忆中已确认的工作流 ID;传入时按当前用户可发起列表精确校验并优先选中。**与 name 至少传一个**' },
4101
- }, { required: [] }),
4102
- handler: (args, env) => commandCreateList(args, env),
4103
- };
4104
4461
  const workflowCreateForm = {
4105
4462
  name: 'workflow.createForm',
4106
4463
  resource: 'workflow',
4107
4464
  method: 'createForm',
4108
4465
  action: 'workflow.createForm',
4109
- summary: '初始化发起流程表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出字段清单 + fileUploadParams(不落业务数据)',
4466
+ summary: '按工作流参数解析可发起的工作流并载入发起表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出 context 句柄 + 字段清单 + fileUploadParams + 该工作流的发起偏好(不落业务数据)',
4110
4467
  capability: 'create',
4111
4468
  risk: 'read',
4112
4469
  requiresConfirmation: false,
4113
4470
  inputSchema: schemaOf({
4114
- workflowId: SCHEMA_REQUEST_ID,
4471
+ workflow: {
4472
+ type: 'array',
4473
+ items: { type: 'string' },
4474
+ description: '工作流条件:支持 "名称(ID)"、名称(可传多个,用英文逗号分隔,如 "报销,报销流程")、纯数字 ID;按当前用户**可发起**的工作流模糊匹配;0 命中或多命中且无全名精确匹配时返回澄清(候选见 candidates),确认后用「名称(ID)」重发',
4475
+ },
4115
4476
  relatesTo: { type: 'string', description: '可选关联对象 id(仅透传,不影响表单初始化)' },
4116
- }, { required: ['workflowId'] }),
4477
+ }, { required: ['workflow'] }),
4117
4478
  handler: (args, env) => commandCreateForm(args, env),
4118
4479
  };
4119
- const workflowCreatePrepare = {
4120
- name: 'workflow.createPrepare',
4480
+ const workflowCreateRequest = {
4481
+ name: 'workflow.createRequest',
4121
4482
  resource: 'workflow',
4122
- method: 'createPrepare',
4123
- action: 'workflow.createPrepare',
4124
- summary: '收集并准备表单数据:绑定字段、解析选项/浏览按钮、校验只读字段、触发字段联动,产出确认预览 + continuation(不落业务数据)',
4483
+ method: 'createRequest',
4484
+ action: 'workflow.createRequest',
4485
+ summary: '按 action 一次完成发起:绑定字段 → 解析选项/浏览按钮 → 触发字段联动与显示属性联动 → 必填校验 →(submit 缺必填则只回报缺失、不写)→ saveFormData → flow/create',
4125
4486
  capability: 'create',
4126
- risk: 'read-before-write',
4487
+ risk: 'high-risk-write',
4127
4488
  requiresConfirmation: true,
4128
4489
  inputSchema: schemaOf({
4129
4490
  context: SCHEMA_CONTINUATION,
@@ -4132,31 +4493,14 @@ const workflowCreatePrepare = {
4132
4493
  description: '表单字段值,结构 {"main":{...},"details":{"<subFormId>":[{...}]}}',
4133
4494
  additionalProperties: true,
4134
4495
  },
4496
+ action: { type: 'string', enum: ['draft', 'submit'], description: 'draft=仅保存草稿(必填缺失不阻断,缺失项随结果作为提醒返回);submit=提交审批(仍有必填缺失时不调用创建接口,改为一次性回报全部缺失项)' },
4135
4497
  requestName: { type: 'string', description: '流程标题(form 返回 requestName 非空时直接用其值;为空时由模型按内容生成)' },
4136
4498
  requestLevel: { type: 'string', description: '紧急程度(取 systemFields.levelOptions 的 code)' },
4137
4499
  secLevel: { type: 'string', description: '密级(取 systemFields.secLevelOptions 的 id)' },
4138
4500
  remark: { type: 'string', description: '签字意见' },
4139
- intent: { type: 'string', enum: ['submit', 'draft'], description: '意图:submit=提交审批(必填缺失阻断);draft=仅保存草稿(必填缺失不阻断)' },
4140
- }, { required: ['context', 'data', 'intent'] }),
4141
- handler: (args, env) => commandCreatePrepare(args, env),
4142
- };
4143
- const workflowCreateApply = {
4144
- name: 'workflow.createApply',
4145
- resource: 'workflow',
4146
- method: 'createApply',
4147
- action: 'workflow.createApply',
4148
- summary: '明确确认后发起流程:写前复核只读字段 → build_save_body → fdLinkage → saveFormData → flow/create;结果不确定抛 partial error 禁止重试',
4149
- capability: 'create',
4150
- risk: 'high-risk-write',
4151
- requiresConfirmation: true,
4152
- inputSchema: schemaOf({
4153
- context: SCHEMA_CONTINUATION,
4154
- confirm: SCHEMA_CONFIRM,
4155
- intent: { type: 'string', enum: ['submit', 'draft'], description: '意图(必填,独立于 prepare 的 intent):submit=创建并提交审批;draft=仅保存草稿' },
4156
- question: { type: 'string', description: '用户发起时的原话(记忆保存参考)' },
4157
4501
  verificationConfirmed: { type: 'boolean', description: '节点字段校验需用户确认时显式确认继续提交' },
4158
- }, { required: ['context', 'confirm', 'intent'] }),
4159
- handler: (args, env) => commandCreateApply(args, env),
4502
+ }, { required: ['context', 'data', 'action'] }),
4503
+ handler: (args, env) => commandCreateRequest(args, env),
4160
4504
  };
4161
4505
  const workflowEditLoad = {
4162
4506
  name: 'workflow.editLoad',
@@ -4205,10 +4549,8 @@ const workflowEditApply = {
4205
4549
  handler: (args, env) => commandEditSave(args, env),
4206
4550
  };
4207
4551
  export const workflowFormOperations = [
4208
- workflowCreateList,
4209
4552
  workflowCreateForm,
4210
- workflowCreatePrepare,
4211
- workflowCreateApply,
4553
+ workflowCreateRequest,
4212
4554
  workflowEditLoad,
4213
4555
  workflowEditPrepare,
4214
4556
  workflowEditApply,