weaver-work-cli 0.1.24 → 0.1.26

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 (401) hide show
  1. package/README.md +30 -21
  2. package/dist/cmd/root.js +1 -0
  3. package/dist/core/output.js +53 -1
  4. package/dist/core/runtime-agent.js +19 -0
  5. package/dist/internal/e10/auth/commands.js +58 -23
  6. package/dist/internal/e10/auth/crypto.js +52 -12
  7. package/dist/internal/e10/auth/xiaoe.js +145 -53
  8. package/dist/internal/e10/context.js +5 -27
  9. package/dist/shortcuts/archive-oa/constants.js +66 -0
  10. package/dist/shortcuts/archive-oa/continuation.js +70 -0
  11. package/dist/shortcuts/archive-oa/errors.js +53 -0
  12. package/dist/shortcuts/archive-oa/host.js +157 -0
  13. package/dist/shortcuts/archive-oa/index.js +225 -0
  14. package/dist/shortcuts/archive-oa/manifest.js +62 -0
  15. package/dist/shortcuts/archive-oa/operations/borrow-car.js +159 -0
  16. package/dist/shortcuts/archive-oa/operations/borrow-url.js +108 -0
  17. package/dist/shortcuts/archive-oa/operations/confirm.js +48 -0
  18. package/dist/shortcuts/archive-oa/operations/contract.js +120 -0
  19. package/dist/shortcuts/archive-oa/operations/locate.js +50 -0
  20. package/dist/shortcuts/archive-oa/operations/registry.js +40 -0
  21. package/dist/shortcuts/archive-oa/operations/shared.js +181 -0
  22. package/dist/shortcuts/archive-oa/operations/types.js +45 -0
  23. package/dist/shortcuts/archive-oa/operations.js +13 -0
  24. package/dist/shortcuts/caizhilian/operations/handlers/fileops.js +67 -1
  25. package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +198 -0
  26. package/dist/shortcuts/caizhilian/operations/shared.js +32 -0
  27. package/dist/shortcuts/caizhilian/operations/specs.js +15 -0
  28. package/dist/shortcuts/index.js +2 -0
  29. package/dist/shortcuts/invoice/host.js +2 -1
  30. package/dist/shortcuts/invoice/manifest.js +111 -0
  31. package/dist/shortcuts/invoice/operations/issuing-qys.js +361 -0
  32. package/dist/shortcuts/invoice/operations/issuing.js +5 -5
  33. package/dist/shortcuts/invoice/operations/registry.js +4 -0
  34. package/dist/shortcuts/workflow/endpoints.js +11 -0
  35. package/dist/shortcuts/workflow/host.js +2 -2
  36. package/dist/shortcuts/workflow/index.js +25 -1
  37. package/dist/shortcuts/workflow/operations/chatscene.js +270 -0
  38. package/dist/shortcuts/workflow/operations/form.js +2128 -244
  39. package/dist/shortcuts/workflow/operations/read.js +186 -41
  40. package/dist/shortcuts/workflow/operations/shared.js +25 -0
  41. package/dist/shortcuts/workflow/operations/sign-input.js +106 -0
  42. package/dist/shortcuts/workflow/operations/write.js +9 -1
  43. package/dist/shortcuts/workflow/stdout-compact.js +41 -0
  44. package/docs/archive-oa.md +79 -0
  45. package/docs/caizhilian.md +4 -3
  46. package/docs/e10-auth.md +13 -5
  47. package/node_modules/@napi-rs/keyring/LICENSE +21 -0
  48. package/node_modules/@napi-rs/keyring/README.md +19 -0
  49. package/node_modules/@napi-rs/keyring/index.d.ts +166 -0
  50. package/node_modules/@napi-rs/keyring/index.js +514 -0
  51. package/node_modules/@napi-rs/keyring/keytar.d.ts +53 -0
  52. package/node_modules/@napi-rs/keyring/keytar.js +26 -0
  53. package/node_modules/@napi-rs/keyring/package.json +125 -0
  54. package/node_modules/@napi-rs/keyring-darwin-arm64/README.md +3 -0
  55. package/node_modules/@napi-rs/keyring-darwin-arm64/keyring.darwin-arm64.node +0 -0
  56. package/node_modules/@napi-rs/keyring-darwin-arm64/package.json +32 -0
  57. package/node_modules/commander/LICENSE +22 -0
  58. package/node_modules/commander/Readme.md +1149 -0
  59. package/node_modules/commander/esm.mjs +16 -0
  60. package/node_modules/commander/index.js +24 -0
  61. package/node_modules/commander/lib/argument.js +149 -0
  62. package/node_modules/commander/lib/command.js +2662 -0
  63. package/node_modules/commander/lib/error.js +39 -0
  64. package/node_modules/commander/lib/help.js +709 -0
  65. package/node_modules/commander/lib/option.js +367 -0
  66. package/node_modules/commander/lib/suggestSimilar.js +101 -0
  67. package/node_modules/commander/package-support.json +16 -0
  68. package/node_modules/commander/package.json +82 -0
  69. package/node_modules/commander/typings/esm.d.mts +3 -0
  70. package/node_modules/commander/typings/index.d.ts +1045 -0
  71. package/package.json +5 -1
  72. package/skill-template/business-info.json +16 -0
  73. package/skill-template/domains/archive-oa.md +19 -0
  74. package/skill-template/domains/yepiaotong.md +2 -2
  75. package/skills/weaver-e10-archive-oa/SKILL.md +159 -0
  76. package/skills/weaver-e10-archive-oa/references/archive-locate.md +49 -0
  77. package/skills/weaver-e10-archive-oa/references/borrow-car.md +76 -0
  78. package/skills/weaver-e10-archive-oa/references/borrow-flow.md +53 -0
  79. package/skills/weaver-e10-archive-oa/references/contract-search.md +63 -0
  80. package/skills/weaver-e10-archive-oa/references/disabled-capabilities.md +29 -0
  81. package/skills/weaver-e10-archive-oa/references/qys-contract-search.md +47 -0
  82. package/skills/weaver-e10-archive-oa/references/source-manifest.json +400 -0
  83. package/skills/weaver-e10-builder/SKILL.md +303 -0
  84. package/skills/weaver-e10-builder/_catalog.md +94 -0
  85. package/skills/weaver-e10-builder/actionflows/CONTEXT.md +71 -0
  86. package/skills/weaver-e10-builder/actionflows/SKILL.md +176 -0
  87. package/skills/weaver-e10-builder/actionflows/docs/adr/0001-generated-actionflow-cleanup-identity.md +5 -0
  88. package/skills/weaver-e10-builder/actionflows/docs/adr/0002-preserve-existing-actionflow-on-update.md +36 -0
  89. package/skills/weaver-e10-builder/actionflows/references/components/calendar/MeetingCreate.md +19 -0
  90. package/skills/weaver-e10-builder/actionflows/references/components/calendar/ScheduleCreate.md +17 -0
  91. package/skills/weaver-e10-builder/actionflows/references/components/database/DMLExecute.md +38 -0
  92. package/skills/weaver-e10-builder/actionflows/references/components/document/AttachmentCopy.md +17 -0
  93. package/skills/weaver-e10-builder/actionflows/references/components/document/AttachmentMove.md +17 -0
  94. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentCopy.md +17 -0
  95. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentDelete.md +17 -0
  96. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentEdit.md +17 -0
  97. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentMove.md +17 -0
  98. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentPermissionChange.md +17 -0
  99. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentRenewal.md +17 -0
  100. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentRevert.md +17 -0
  101. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSave.md +17 -0
  102. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscription.md +17 -0
  103. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscriptionApprove.md +17 -0
  104. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscriptionSendBack.md +17 -0
  105. package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscriptionWithdraw.md +17 -0
  106. package/skills/weaver-e10-builder/actionflows/references/components/document/FileFolderSave.md +17 -0
  107. package/skills/weaver-e10-builder/actionflows/references/components/document/GetAttachmentInfo.md +17 -0
  108. package/skills/weaver-e10-builder/actionflows/references/components/document/GetDocumentInfo.md +17 -0
  109. package/skills/weaver-e10-builder/actionflows/references/components/document/ModifyDocumentStatus.md +17 -0
  110. package/skills/weaver-e10-builder/actionflows/references/components/document/ReplaceAttachment.md +17 -0
  111. package/skills/weaver-e10-builder/actionflows/references/components/document/UpdatePrintNum.md +17 -0
  112. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbAdd.md +100 -0
  113. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbDelete.md +23 -0
  114. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbRecover.md +17 -0
  115. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbSaveVersion.md +17 -0
  116. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbUpdate.md +67 -0
  117. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/DateTrigger.md +32 -0
  118. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EbGetDataById.md +17 -0
  119. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EnquiryForm.md +107 -0
  120. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EventButton.md +33 -0
  121. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EventFormData.md +19 -0
  122. package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/TimeBatchTrigger.md +19 -0
  123. package/skills/weaver-e10-builder/actionflows/references/components/http/Http.md +77 -0
  124. package/skills/weaver-e10-builder/actionflows/references/components/logic/Condition.md +23 -0
  125. package/skills/weaver-e10-builder/actionflows/references/components/logic/LogicFlowSet.md +17 -0
  126. package/skills/weaver-e10-builder/actionflows/references/components/logic/MultipleExecution.md +38 -0
  127. package/skills/weaver-e10-builder/actionflows/references/components/logic/Output.md +37 -0
  128. package/skills/weaver-e10-builder/actionflows/references/components/message/InnerMQSend.md +50 -0
  129. package/skills/weaver-e10-builder/actionflows/references/components/message/SendMessage.md +30 -0
  130. package/skills/weaver-e10-builder/actionflows/references/components/personnel/AlterPersonStatus.md +30 -0
  131. package/skills/weaver-e10-builder/actionflows/references/components/personnel/HrmEmployeeUpdate.md +34 -0
  132. package/skills/weaver-e10-builder/actionflows/references/components/personnel/PersonnelAlterType.md +38 -0
  133. package/skills/weaver-e10-builder/actionflows/references/components/personnel/PersonnelOnboarding.md +36 -0
  134. package/skills/weaver-e10-builder/actionflows/references/components/serverless/ServerlessAction.md +42 -0
  135. package/skills/weaver-e10-builder/actionflows/references/components/trigger/CustomTrigger.md +82 -0
  136. package/skills/weaver-e10-builder/actionflows/references/components/trigger/InnerMQListener.md +41 -0
  137. package/skills/weaver-e10-builder/actionflows/references/components/trigger/SimpleScheduler.md +19 -0
  138. package/skills/weaver-e10-builder/actionflows/references/components/trigger/WebHookTrigger.md +91 -0
  139. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveAddSign.md +17 -0
  140. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveAgreeRejectApply.md +17 -0
  141. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveCommit.md +17 -0
  142. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveConsulTurnTodo.md +17 -0
  143. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveCreate.md +17 -0
  144. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDelete.md +19 -0
  145. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDoCirculate.md +17 -0
  146. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDoForward.md +17 -0
  147. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDoTurnTodo.md +17 -0
  148. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveEntrust.md +19 -0
  149. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveIntervention.md +17 -0
  150. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveMandatoryArchiving.md +17 -0
  151. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveManualCC.md +17 -0
  152. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveRefuseRejectApply.md +17 -0
  153. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveRejectApply.md +17 -0
  154. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveSave.md +17 -0
  155. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveSendBack.md +17 -0
  156. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveShareEntry.md +17 -0
  157. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveTrigger.md +99 -0
  158. package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveWithdraw.md +17 -0
  159. package/skills/weaver-e10-builder/actionflows/references/components/workflow/WorkFlowDocEmpowerment.md +17 -0
  160. package/skills/weaver-e10-builder/actionflows/references/components/workflow/WorkflowToDoc.md +17 -0
  161. package/skills/weaver-e10-builder/actionflows/references/condition_rules.md +100 -0
  162. package/skills/weaver-e10-builder/actionflows/references/dynamic_capability_matrix.md +39 -0
  163. package/skills/weaver-e10-builder/actionflows/references/dynamic_resources.md +41 -0
  164. package/skills/weaver-e10-builder/actionflows/references/examples/enquiry-form-batch-insert-scene2.json +1043 -0
  165. package/skills/weaver-e10-builder/actionflows/references/function_rules.md +15 -0
  166. package/skills/weaver-e10-builder/actionflows/references/trigger_runtime_notes.md +53 -0
  167. package/skills/weaver-e10-builder/assets/dom-structure/Bar.json +341 -0
  168. package/skills/weaver-e10-builder/assets/dom-structure/BarStack.json +341 -0
  169. package/skills/weaver-e10-builder/assets/dom-structure/Bubble.json +323 -0
  170. package/skills/weaver-e10-builder/assets/dom-structure/Calendar.json +515 -0
  171. package/skills/weaver-e10-builder/assets/dom-structure/Circular.json +337 -0
  172. package/skills/weaver-e10-builder/assets/dom-structure/CollapsePanel.json +289 -0
  173. package/skills/weaver-e10-builder/assets/dom-structure/Comment.json +617 -0
  174. package/skills/weaver-e10-builder/assets/dom-structure/CountDown.json +755 -0
  175. package/skills/weaver-e10-builder/assets/dom-structure/DigitalPanel.json +191 -0
  176. package/skills/weaver-e10-builder/assets/dom-structure/EbButton.json +387 -0
  177. package/skills/weaver-e10-builder/assets/dom-structure/FastCreate.json +242 -0
  178. package/skills/weaver-e10-builder/assets/dom-structure/Gauge.json +282 -0
  179. package/skills/weaver-e10-builder/assets/dom-structure/HBar.json +336 -0
  180. package/skills/weaver-e10-builder/assets/dom-structure/HBarStack.json +337 -0
  181. package/skills/weaver-e10-builder/assets/dom-structure/Line.json +341 -0
  182. package/skills/weaver-e10-builder/assets/dom-structure/NList.json +712 -0
  183. package/skills/weaver-e10-builder/assets/dom-structure/NavBar.json +274 -0
  184. package/skills/weaver-e10-builder/assets/dom-structure/NavPanel.json +390 -0
  185. package/skills/weaver-e10-builder/assets/dom-structure/NewTab.json +606 -0
  186. package/skills/weaver-e10-builder/assets/dom-structure/Nightingale.json +336 -0
  187. package/skills/weaver-e10-builder/assets/dom-structure/PageHeader.json +205 -0
  188. package/skills/weaver-e10-builder/assets/dom-structure/Picture.json +209 -0
  189. package/skills/weaver-e10-builder/assets/dom-structure/Pie.json +338 -0
  190. package/skills/weaver-e10-builder/assets/dom-structure/ProgressChart.json +299 -0
  191. package/skills/weaver-e10-builder/assets/dom-structure/SearchGlobal.json +628 -0
  192. package/skills/weaver-e10-builder/assets/dom-structure/Slide.json +489 -0
  193. package/skills/weaver-e10-builder/assets/dom-structure/Treemap.json +324 -0
  194. package/skills/weaver-e10-builder/assets/factory/chart-dimensions.json +536 -0
  195. package/skills/weaver-e10-builder/assets/factory/example-btn-progress-collapse-configured.json +2515 -0
  196. package/skills/weaver-e10-builder/assets/factory/example-configured-2.json +13805 -0
  197. package/skills/weaver-e10-builder/assets/factory/example-configured.json +6735 -0
  198. package/skills/weaver-e10-builder/assets/factory/factory-bar-stack.json +326 -0
  199. package/skills/weaver-e10-builder/assets/factory/factory-bar.json +222 -0
  200. package/skills/weaver-e10-builder/assets/factory/factory-bubble.json +322 -0
  201. package/skills/weaver-e10-builder/assets/factory/factory-calendar.json +325 -0
  202. package/skills/weaver-e10-builder/assets/factory/factory-circular.json +320 -0
  203. package/skills/weaver-e10-builder/assets/factory/factory-collapse-panel.json +132 -0
  204. package/skills/weaver-e10-builder/assets/factory/factory-comment.json +388 -0
  205. package/skills/weaver-e10-builder/assets/factory/factory-count-down.json +198 -0
  206. package/skills/weaver-e10-builder/assets/factory/factory-digital-panel.json +108 -0
  207. package/skills/weaver-e10-builder/assets/factory/factory-eb-button.json +150 -0
  208. package/skills/weaver-e10-builder/assets/factory/factory-fast-create.json +62 -0
  209. package/skills/weaver-e10-builder/assets/factory/factory-gauge.json +312 -0
  210. package/skills/weaver-e10-builder/assets/factory/factory-h-bar-stack.json +328 -0
  211. package/skills/weaver-e10-builder/assets/factory/factory-h-bar.json +326 -0
  212. package/skills/weaver-e10-builder/assets/factory/factory-line.json +212 -0
  213. package/skills/weaver-e10-builder/assets/factory/factory-nav-bar.json +167 -0
  214. package/skills/weaver-e10-builder/assets/factory/factory-nav-menu.json +144 -0
  215. package/skills/weaver-e10-builder/assets/factory/factory-nav-panel.json +338 -0
  216. package/skills/weaver-e10-builder/assets/factory/factory-new-tab.json +163 -0
  217. package/skills/weaver-e10-builder/assets/factory/factory-nightingale.json +312 -0
  218. package/skills/weaver-e10-builder/assets/factory/factory-nlist.json +112 -0
  219. package/skills/weaver-e10-builder/assets/factory/factory-page-header.json +171 -0
  220. package/skills/weaver-e10-builder/assets/factory/factory-picture.json +169 -0
  221. package/skills/weaver-e10-builder/assets/factory/factory-pie.json +212 -0
  222. package/skills/weaver-e10-builder/assets/factory/factory-progress-chart.json +162 -0
  223. package/skills/weaver-e10-builder/assets/factory/factory-search-global.json +104 -0
  224. package/skills/weaver-e10-builder/assets/factory/factory-slide.json +164 -0
  225. package/skills/weaver-e10-builder/assets/factory/factory-treemap.json +322 -0
  226. package/skills/weaver-e10-builder/assets/icons.db +0 -0
  227. package/skills/weaver-e10-builder/assets/sample/.claude/settings.local.json +8 -0
  228. package/skills/weaver-e10-builder/assets/sample/api/formApi.js +510 -0
  229. package/skills/weaver-e10-builder/assets/sample/config/config.js +0 -0
  230. package/skills/weaver-e10-builder/assets/sample/config/configLoad.js +0 -0
  231. package/skills/weaver-e10-builder/assets/sample/config/configLoad_default.js +0 -0
  232. package/skills/weaver-e10-builder/assets/sample/config/config_default.js +0 -0
  233. package/skills/weaver-e10-builder/assets/sample/entry.js +22 -0
  234. package/skills/weaver-e10-builder/assets/sample/eslint.config.cjs +48 -0
  235. package/skills/weaver-e10-builder/assets/sample/memo.md +0 -0
  236. package/skills/weaver-e10-builder/assets/sample/readme.js +8 -0
  237. package/skills/weaver-e10-builder/references/app/guide-app-publish.md +92 -0
  238. package/skills/weaver-e10-builder/references/auth/guide-auth.md +88 -0
  239. package/skills/weaver-e10-builder/references/coding/guide-edit-backfill.md +189 -0
  240. package/skills/weaver-e10-builder/references/coding/guide-form-crud.md +262 -0
  241. package/skills/weaver-e10-builder/references/coding/guide-page-code.md +244 -0
  242. package/skills/weaver-e10-builder/references/coding/guide-page-export.md +119 -0
  243. package/skills/weaver-e10-builder/references/coding/ref-compile-rules.md +130 -0
  244. package/skills/weaver-e10-builder/references/common/guide-plan-template.md +164 -0
  245. package/skills/weaver-e10-builder/references/common/map.md +153 -0
  246. package/skills/weaver-e10-builder/references/common/ref-cli-commands.md +635 -0
  247. package/skills/weaver-e10-builder/references/components/@weapp-Button.md +104 -0
  248. package/skills/weaver-e10-builder/references/components/base-Badge.md +100 -0
  249. package/skills/weaver-e10-builder/references/components/base-Dialog.md +358 -0
  250. package/skills/weaver-e10-builder/references/components/base-Empty.md +58 -0
  251. package/skills/weaver-e10-builder/references/components/base-Tag.md +112 -0
  252. package/skills/weaver-e10-builder/references/components/display-Alert.md +64 -0
  253. package/skills/weaver-e10-builder/references/components/display-Avatar.md +150 -0
  254. package/skills/weaver-e10-builder/references/components/display-Calendar.md +263 -0
  255. package/skills/weaver-e10-builder/references/components/display-Carousel.md +115 -0
  256. package/skills/weaver-e10-builder/references/components/display-Collapse.md +299 -0
  257. package/skills/weaver-e10-builder/references/components/display-FilePreview.md +49 -0
  258. package/skills/weaver-e10-builder/references/components/display-Icon.md +66 -0
  259. package/skills/weaver-e10-builder/references/components/display-List.md +320 -0
  260. package/skills/weaver-e10-builder/references/components/display-Pagination.md +41 -0
  261. package/skills/weaver-e10-builder/references/components/display-Spin.md +34 -0
  262. package/skills/weaver-e10-builder/references/components/display-Steps.md +89 -0
  263. package/skills/weaver-e10-builder/references/components/display-Table.md +160 -0
  264. package/skills/weaver-e10-builder/references/components/display-Title.md +117 -0
  265. package/skills/weaver-e10-builder/references/components/form-Checkbox.md +161 -0
  266. package/skills/weaver-e10-builder/references/components/form-DepartmentBrowser-api.md +52 -0
  267. package/skills/weaver-e10-builder/references/components/form-EmployeeBrowser-api.md +51 -0
  268. package/skills/weaver-e10-builder/references/components/form-EmployeeBrowser.md +51 -0
  269. package/skills/weaver-e10-builder/references/components/form-Input.md +123 -0
  270. package/skills/weaver-e10-builder/references/components/form-Radio.md +145 -0
  271. package/skills/weaver-e10-builder/references/components/form-Select.md +191 -0
  272. package/skills/weaver-e10-builder/references/components/form-Switch.md +46 -0
  273. package/skills/weaver-e10-builder/references/components/form-Upload.md +53 -0
  274. package/skills/weaver-e10-builder/references/components/generic-Echarts.md +102 -0
  275. package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-components.md +130 -0
  276. package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-css.md +127 -0
  277. package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-factory.md +460 -0
  278. package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-version.md +111 -0
  279. package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer.md +288 -0
  280. package/skills/weaver-e10-builder/references/esb-interface/guide-import.md +34 -0
  281. package/skills/weaver-e10-builder/references/form/guide-create-form.md +248 -0
  282. package/skills/weaver-e10-builder/references/form/guide-ebuilder-filter.md +73 -0
  283. package/skills/weaver-e10-builder/references/form/guide-ebuilder-scope.md +82 -0
  284. package/skills/weaver-e10-builder/references/form/guide-field-linkage.md +123 -0
  285. package/skills/weaver-e10-builder/references/form/guide-form-archive-permission.md +175 -0
  286. package/skills/weaver-e10-builder/references/form/guide-form-association.md +124 -0
  287. package/skills/weaver-e10-builder/references/form/guide-form-button.md +169 -0
  288. package/skills/weaver-e10-builder/references/form/guide-form-data.md +78 -0
  289. package/skills/weaver-e10-builder/references/form/guide-form-formula-functions.md +343 -0
  290. package/skills/weaver-e10-builder/references/form/guide-form-layout-incremental.md +90 -0
  291. package/skills/weaver-e10-builder/references/form/guide-form-layout-planning.md +68 -0
  292. package/skills/weaver-e10-builder/references/form/guide-form-linkage.md +269 -0
  293. package/skills/weaver-e10-builder/references/form/guide-form-message.md +74 -0
  294. package/skills/weaver-e10-builder/references/form/guide-form-physical-table.md +146 -0
  295. package/skills/weaver-e10-builder/references/form/guide-form-serial-mark.md +47 -0
  296. package/skills/weaver-e10-builder/references/form/guide-form-sub-detail.md +82 -0
  297. package/skills/weaver-e10-builder/references/form/guide-form-view-permission.md +38 -0
  298. package/skills/weaver-e10-builder/references/form/guide-form-view.md +371 -0
  299. package/skills/weaver-e10-builder/references/form/guide-formula-linkage.md +281 -0
  300. package/skills/weaver-e10-builder/references/form/guide-list-columns.md +52 -0
  301. package/skills/weaver-e10-builder/references/form/guide-list-custom-text.md +52 -0
  302. package/skills/weaver-e10-builder/references/form/guide-list-stats.md +136 -0
  303. package/skills/weaver-e10-builder/references/form/guide-virtual-form.md +156 -0
  304. package/skills/weaver-e10-builder/references/form/ref-form-fields.md +293 -0
  305. package/skills/weaver-e10-builder/references/form/style-templates.md +97 -0
  306. package/skills/weaver-e10-builder/references/hrm/guide-matrix.md +245 -0
  307. package/skills/weaver-e10-builder/references/hrm/guide-org-employee.md +155 -0
  308. package/skills/weaver-e10-builder/references/menu/system-menus.md +75 -0
  309. package/skills/weaver-e10-builder/references/report/add-dataset-registration-guide.md +139 -0
  310. package/skills/weaver-e10-builder/references/report/ai-report-workflow.md +412 -0
  311. package/skills/weaver-e10-builder/references/report/api-report-build-fast-path.md +275 -0
  312. package/skills/weaver-e10-builder/references/report/app-datasource-lookup-guide.md +230 -0
  313. package/skills/weaver-e10-builder/references/report/cell-config-echo-guide.md +163 -0
  314. package/skills/weaver-e10-builder/references/report/cell-transform-guide.md +147 -0
  315. package/skills/weaver-e10-builder/references/report/condition-format-guide.md +140 -0
  316. package/skills/weaver-e10-builder/references/report/cross-dataset-filter-guide.md +304 -0
  317. package/skills/weaver-e10-builder/references/report/dataset-query-filter-guide.md +235 -0
  318. package/skills/weaver-e10-builder/references/report/dataset-sidebar-ops-guide.md +280 -0
  319. package/skills/weaver-e10-builder/references/report/dataset-table-field-contract-guide.md +339 -0
  320. package/skills/weaver-e10-builder/references/report/datasource-metadata-guide.md +365 -0
  321. package/skills/weaver-e10-builder/references/report/datasource-selection-and-parent-filter-guide.md +148 -0
  322. package/skills/weaver-e10-builder/references/report/date-range-query-reference.md +102 -0
  323. package/skills/weaver-e10-builder/references/report/default-header-style-guide.md +70 -0
  324. package/skills/weaver-e10-builder/references/report/designer-blank-prevention-guide.md +111 -0
  325. package/skills/weaver-e10-builder/references/report/event-hreflink-guide.md +184 -0
  326. package/skills/weaver-e10-builder/references/report/field-bind-exact-match-guide.md +208 -0
  327. package/skills/weaver-e10-builder/references/report/formula-cell-api-guide.md +258 -0
  328. package/skills/weaver-e10-builder/references/report/guide-report-build.md +131 -0
  329. package/skills/weaver-e10-builder/references/report/guide-report-modify.md +100 -0
  330. package/skills/weaver-e10-builder/references/report/guide-report-multisource.md +143 -0
  331. package/skills/weaver-e10-builder/references/report/guide-report-query.md +98 -0
  332. package/skills/weaver-e10-builder/references/report/hidden-row-and-header-merge-guide.md +100 -0
  333. package/skills/weaver-e10-builder/references/report/layout-showlocation-freeze-guide.md +167 -0
  334. package/skills/weaver-e10-builder/references/report/preaggregate-auto-disable-guide.md +107 -0
  335. package/skills/weaver-e10-builder/references/report/query-condition-api-guide.md +890 -0
  336. package/skills/weaver-e10-builder/references/report/ref-report-commands.md +122 -0
  337. package/skills/weaver-e10-builder/references/report/ref-report-pitfalls.md +226 -0
  338. package/skills/weaver-e10-builder/references/report/report-baseinfo-defaults-guide.md +117 -0
  339. package/skills/weaver-e10-builder/references/report/report-building-guide.md +511 -0
  340. package/skills/weaver-e10-builder/references/report/report-config-schema.md +445 -0
  341. package/skills/weaver-e10-builder/references/report/report-create-modes-guide.md +182 -0
  342. package/skills/weaver-e10-builder/references/report/report-data-model.md +447 -0
  343. package/skills/weaver-e10-builder/references/report/report-delivery-contract.md +101 -0
  344. package/skills/weaver-e10-builder/references/report/report-designer-feature-map.md +500 -0
  345. package/skills/weaver-e10-builder/references/report/report-internal-error-guide.md +78 -0
  346. package/skills/weaver-e10-builder/references/report/report-knowledge-base.md +389 -0
  347. package/skills/weaver-e10-builder/references/report/report-lifecycle-api-guide.md +392 -0
  348. package/skills/weaver-e10-builder/references/report/report-list-page-ops-guide.md +191 -0
  349. package/skills/weaver-e10-builder/references/report/report-permission-guide.md +365 -0
  350. package/skills/weaver-e10-builder/references/report/report-requirement-analysis-template.md +202 -0
  351. package/skills/weaver-e10-builder/references/report/report-self-check-guide.md +164 -0
  352. package/skills/weaver-e10-builder/references/report/safe-partial-save-guide.md +151 -0
  353. package/skills/weaver-e10-builder/references/report/snowflake-id-precision-guide.md +84 -0
  354. package/skills/weaver-e10-builder/references/report/spec-build-schema.md +294 -0
  355. package/skills/weaver-e10-builder/references/report/targeted-fix-guide.md +97 -0
  356. package/skills/weaver-e10-builder/references/report/user-request-scope-only-guide.md +82 -0
  357. package/skills/weaver-e10-builder/references/workflow/guide-function-management.md +154 -0
  358. package/skills/weaver-e10-builder/references/workflow/guide-opinion-settings.md +126 -0
  359. package/skills/weaver-e10-builder/references/workflow/guide-workflow-action.md +174 -0
  360. package/skills/weaver-e10-builder/references/workflow/guide-workflow-auth-resp.md +224 -0
  361. package/skills/weaver-e10-builder/references/workflow/guide-workflow-condition.md +447 -0
  362. package/skills/weaver-e10-builder/references/workflow/guide-workflow-node-layout.md +181 -0
  363. package/skills/weaver-e10-builder/references/workflow/guide-workflow-odoc-setting.md +248 -0
  364. package/skills/weaver-e10-builder/references/workflow/guide-workflow-odoc.md +68 -0
  365. package/skills/weaver-e10-builder/references/workflow/guide-workflow.md +527 -0
  366. package/skills/weaver-e10-builder/references/workflow/operator-matrix.md +76 -0
  367. package/skills/weaver-e10-caizhilian/SKILL.md +43 -16
  368. package/skills/weaver-e10-caizhilian/references/agent-entry.md +7 -3
  369. package/skills/weaver-e10-caizhilian/references/collaboration.md +5 -3
  370. package/skills/weaver-e10-caizhilian/references/document.md +33 -11
  371. package/skills/weaver-e10-caizhilian/references/file-collection.md +4 -3
  372. package/skills/weaver-e10-caizhilian/references/file-upload.md +2 -3
  373. package/skills/weaver-e10-caizhilian/references/files.md +23 -2
  374. package/skills/weaver-e10-caizhilian/references/folder.md +48 -16
  375. package/skills/weaver-e10-caizhilian/references/setting-attach.md +61 -42
  376. package/skills/weaver-e10-caizhilian/references/setting-backup.md +13 -51
  377. package/skills/weaver-e10-caizhilian/references/setting-encrypt.md +20 -51
  378. package/skills/weaver-e10-caizhilian/references/setting-storage.md +82 -147
  379. package/skills/weaver-e10-caizhilian/references/source-manifest.json +99 -1
  380. package/skills/weaver-e10-caizhilian/references/withheld.md +20 -44
  381. package/skills/weaver-e10-shared/SKILL.md +24 -18
  382. package/skills/weaver-e10-shared/references/e10-auth-and-session.md +16 -15
  383. package/skills/weaver-e10-shared/references/non-interactive-environment.md +10 -10
  384. package/skills/weaver-e10-workflow/SKILL.md +23 -44
  385. package/skills/weaver-e10-workflow/references/source-manifest.json +40 -25
  386. package/skills/weaver-e10-workflow/references/workflow-create.md +278 -85
  387. package/skills/weaver-e10-workflow/references/workflow-edit.md +1 -1
  388. package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -4
  389. package/skills/weaver-e10-workflow/references/workflow-query.md +2 -2
  390. package/skills/weaver-e10-yepiaotong/SKILL.md +10 -7
  391. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +49 -1
  392. package/skills/weaver-e10-yepiaotong/references/invoice-issuing-qys-ledger.md +66 -0
  393. package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +59 -36
  394. package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +54 -509
  395. package/skills/weaver-e10-yepiaotong/references/reim-examples.md +182 -345
  396. package/skills/weaver-e10-yepiaotong/references/reim-expense-subject-rules.md +4 -5
  397. package/skills/weaver-e10-yepiaotong/references/reim-form-fill-rules.md +153 -345
  398. package/skills/weaver-e10-yepiaotong/references/reim-phone-subsidy-rules.md +5 -5
  399. package/skills/weaver-e10-yepiaotong/references/reim-workflow.md +182 -154
  400. package/skills/weaver-e10-yepiaotong/references/source-manifest.json +96 -0
  401. package/skills/weaver-e10-yepiaotong/references/reim-fast-path.md +0 -119
@@ -17,12 +17,16 @@
17
17
  */
18
18
  import { randomUUID } from 'node:crypto';
19
19
  import { workflowSuccess, schemaOf, SCHEMA_REQUEST_ID, SCHEMA_CONTINUATION, SCHEMA_CONFIRM, } from './types.js';
20
- import { asBool, responseData, toArray, withLeadingMeta, stdoutBytesOf, STDOUT_SAFE_BYTES, parseNameWithId, resolveMemoryAppId, } from './shared.js';
20
+ import { asBool, responseData, toArray, withLeadingMeta, stdoutBytesOf, stdoutCharsOf, STDOUT_SAFE_BYTES, parseNameWithId, resolveMemoryAppId, } from './shared.js';
21
21
  import { workflowValidationError, workflowApiError, workflowPartialError, workflowPolicyError, workflowClarificationError, WorkflowCliError, } from '../errors.js';
22
22
  import { FORM_ENDPOINTS, WORKFLOW_ENDPOINTS, AJAX_QUERY, formApiUrl } from '../endpoints.js';
23
+ import { resolveSignRemark } from './sign-input.js';
24
+ import { compactStdoutPaths } from '../stdout-compact.js';
23
25
  import { continuationReplayError } from '../continuation.js';
24
26
  import { writeStateFileBestEffort } from '../state.js';
25
- import { buildHistoryRequestData, filterHistoryRequestData } from './read.js';
27
+ import { buildHistoryRequestData, fetchHistoryListSnapshot, filterHistoryRequestData, pickUniqueMineWorkflow } from './read.js';
28
+ import { hasXiaoEEnv } from '../../../internal/e10/auth/xiaoe.js';
29
+ import { buildChatSceneInput, formatLocalNow, requestFormFill } from './chatscene.js';
26
30
  // ---------------------------------------------------------------------------
27
31
  // 常量(与 Python 源逐条对齐)
28
32
  // ---------------------------------------------------------------------------
@@ -58,10 +62,24 @@ const CONTINUATION_TTL_MS = 10 * 60 * 1000;
58
62
  * 实测反而把模型带进"先看看手里有什么、再决定读不读文件"的分叉:某次真机会话里落的是**只剩指针**
59
63
  * 那一档(`fields` / `writableFields` / `editableFields` 全被移出 stdout),模型于是先 grep 找
60
64
  * `writableFields`、再读 `requiredFields`、发现 `fields` 是完整对象后又按 17 个字段 id 批量 grep,
61
- * **从头到尾没读那个文件**。现在超线只有一条路:照抄命令读文件。
65
+ * **从头到尾没读那个文件**。于是改成"超线只回提示"。
62
66
  *
63
- * **返回体里不再有任何表单属性** —— 字段清单、必填、历史参考值、`contextId` / `continuation`
64
- * 全部只在 resultFile 里,所以提示必须把"不读就没法继续"说透。
67
+ * ★ 2026-10-13(P0)修正**过度裁剪**:真机会话里"只回提示"的代价是模型**必然**去读那份落盘文件
68
+ * (`Read(..., limit: 2691)`,79,799 字符 ≈ 20k tokens),而它其实只需要两样东西——"要填哪些字段
69
+ * (含各自的默认值)"和"下一步用哪个 `contextId`"。现在超线时**连同 `contextId` /
70
+ * `historyRequestData` 一起回**,`fields` 则在"挂上后仍不超线"时一并回(见 `compactFormOutput`
71
+ * 的 `always` / `ifFits`),提示也随之从"照抄读命令"改成"不必读、需要运行时/系统字段时才读",
72
+ * 把那次 3.9 s 读取 + ~20k tokens 上下文整个省掉。
73
+ *
74
+ * 2026-10-14:`createForm` 的字段清单合并成唯一一份 `fields` 后,`fields` 自己成了"超线与否"的
75
+ * 主要变量 —— 于是它被归入 `ifFits`:挂得下就挂(模型免读文件),挂不下才在提示里点名要读。
76
+ *
77
+ * ★ 2026-10-14 晚(真机反证,改动 A+B):**宿主截断不受这条 24,000 线约束**。真机会话里 24,417 B
78
+ * 的 `createForm` 输出被宿主**中段截掉 9,330 字符**(头尾保留、中间省略),丢的正是 `fields` 前半段;
79
+ * 而紧凑档判定的结论是"挂得下" ⇒ 提示说"不必读取"、模型却必须去读,**两笔成本都付了**。两处修正:
80
+ * ① 回挂键从 `{always, ifFits}` 改成**有序清单**(`FormOutputKeepEntry[]`),把 `fields` 提到
81
+ * `historyRequestData` 之前 —— 中段截断保留头部,先保"要填哪些字段";② 提示语从断言改成**条件式**
82
+ * ("输出完整就不必读取;若被截断……读一次补齐",见 `resultFileNotice`)。
65
83
  *
66
84
  * 落盘不可用时没有文件可指,只能保持全量输出(宁大不丢数据),只补一句说明。
67
85
  */
@@ -69,36 +87,154 @@ const CONTINUATION_TTL_MS = 10 * 60 * 1000;
69
87
  const READ_WHOLE_OUTPUT_HINT = '读取本输出请一次读完:不要用 head/tail 等管道截断'
70
88
  + '(截断会破坏 JSON,并导致重复执行同一条命令)。';
71
89
  /**
72
- * 超线时的**唯一**返回内容:落盘路径 + 一条可直接照抄的读文件命令(措辞由用户 2026-09-29 指定)。
90
+ * `createForm` 单次输出的**字符**安全线(2026-10-15 用户裁定)。
91
+ *
92
+ * 与 `STDOUT_SAFE_BYTES`(24,000 **字节**,`editLoad` / 查询类共用)是两个独立口径,不可互相套用:
93
+ * 本线按**字符**计,中文 1 字符 = 3 字节 ⇒ 同样一份表单输出按本线更容易触线。
94
+ * 用户给定的宿主侧限制实现(`terminal_tool.py` 的 `MAX_OUTPUT_LEN`)也按字符计,故取整 20,000。
95
+ *
96
+ * ★ 量的对象是**最终打出去的那份文本**(`finalStdoutChars`:信封 + 紧凑路径),不是美化 JSON
97
+ * —— 后者在 `fields` / `historyRequestData` / `systemFields` 走紧凑路径后能高估约 30%。
98
+ */
99
+ const CREATE_FORM_STDOUT_SAFE_CHARS = 20_000;
100
+ /**
101
+ * `durationMs` 进判线时用的**位数哨兵**(见 `FormOutputCharBudget.reserve`)。
102
+ *
103
+ * 该值只参与判线的宽度估算、**不会**被下发:真实耗时在压缩之后才求值,判线那一刻只能用位数把它先
104
+ * 占住。8 位 ≈ 27.8 小时,远超单条命令的合理上限 —— 哨兵比真值多几个字符时判线会略微高估,代价是
105
+ * 边界上的表单**提前**落盘;反过来低估才会让宿主按 `MAX_OUTPUT_LEN = 20000` 真截断,那才是要避免的。
106
+ */
107
+ const DURATION_RESERVE_SENTINEL = 99_999_999;
108
+ /**
109
+ * `createForm` 超线时的**唯一**回执(2026-10-15 用户裁定,措辞由用户逐字给定)。
110
+ *
111
+ * 超线就**不再回 JSON**:完整结果落盘,stdout 只留这一句。`contextId` / `fields` /
112
+ * `historyRequestData` 也随之**一律不回挂** —— 它们在落盘文件里都是完整的,模型读一次文件即可取到
113
+ * (含下一步 `createRequest` 要用的 `contextId`)。这与 `editLoad` 那一档(同为"只回提示")形态一致。
114
+ */
115
+ function createFormResultFileNotice(resultFile) {
116
+ return `当前表单内容较大,已临时写入文件 ${resultFile},请直接读取 ${resultFile} 获取完整结果`;
117
+ }
118
+ /**
119
+ * 超线时的**提示行**(措辞由用户 2026-09-29 首定,2026-10-13 P0 改为"不必读"口径)。
120
+ *
121
+ * 用户 2026-09-29 的原始要求:提示里**直接给出可执行的读命令**(工具 + `file_path` + 精确 `limit`),
122
+ * 避免模型自己把 `limit` 给小了、于是分段读好几次 —— 这条保留。
123
+ *
124
+ * P0 起:`contextId` / `historyRequestData` 已随超线档一起返回,所以 **Read 不是必经步骤**,
125
+ * 只在需要运行时 / 系统字段这类细节时才用;首句先说清"不必读取",否则模型仍会照抄去读(那正是要省的)。
73
126
  *
74
- * 用户明确要求:① 超线**只回提示**,不再回任何精简属性 —— 否则模型会先思考一段再决定读不读;
75
- * ② 提示里**直接给出可执行的读命令**(工具 + `file_path` + 精确 `limit`),避免模型自己把 `limit`
76
- * 给小了、于是分段读好几次;③ **除这两行外一个字都不要**(原话:"其余内容都去掉")。
127
+ * 2026-10-14:`fields`(字段清单 + 默认值)也可能被挂在超线档里(`ifFits` 口径)。**挂上才有资格说
128
+ * "不必读取"**——挂不上时提示语要反过来明确要模型去读,否则它会拿着一份没有字段清单的 stdout 硬拼
129
+ * `data`。`fieldsIncluded` 就是按"最终到底挂没挂上 `fields`"传进来的。
130
+ *
131
+ * ★ 2026-10-14 晚(改动 A):上述"挂上就说不必读取"是**断言**,而它的隐含前提——"内联出去的
132
+ * ~24 KB 一定能被模型完整看到"——被真机会话证伪:宿主把 24,417 B 的输出**中段截掉 9,330 字符**,
133
+ * 挂在里面的 `fields` 前半段整段消失。于是 `fieldsIncluded` 为真时不再断言"不必读取",改成**条件式**:
134
+ * 输出完整就不必读,**若被截断**(出现「已截断…字符」提示 / 字段清单不完整)则读一次补齐。
135
+ * 判据本身仍保留——它决定的是"该不该说可以省掉这次读取",不是"输出一定完整"。
77
136
  *
78
137
  * `lines` 是落盘文件的总行数,正好是一次读完所需的 `limit`。文件读取工具不受宿主 stdout 那条
79
138
  * ~30 KB 内联线约束(实测 76 KB / 238 KB 都能一次读回)。
80
139
  */
81
- function resultFileNotice(resultFile, lines) {
82
- return `表单内容较大,已写入临时文件(resultFile:${resultFile}),无需思考,直接运行下面的命令读取完整表单内容:`
83
- + `\nRead(file_path: "${resultFile}", limit: ${lines})`;
140
+ function resultFileNotice(resultFile, lines, fieldsIncluded = true) {
141
+ const head = `表单内容较大,完整表单已写入临时文件(resultFile:${resultFile})。`;
142
+ const advice = fieldsIncluded
143
+ ? '**输出完整就不必读取**:字段清单(含 `initialValue`)与历史参考值都已随 stdout 下发,下一步 createRequest 用 contextId 即可;'
144
+ + '**若本输出被截断**(出现「已截断…字符」之类提示,或字段清单明显不完整),说明前部被省略的正是拼 `data` 需要的字段 —— 按下面命令读一次补齐(不要分段读):'
145
+ : '**字段清单未随 stdout 下发**(它本身就超了内联线)——拼 `data` 前必须把它读一次(不要分段读):';
146
+ return head + advice + `\nRead(file_path: "${resultFile}", limit: ${lines})`;
147
+ }
148
+ /**
149
+ * 量「这份 `data` 落进 stdout 后,宿主会看到多少字符」——**与最终输出同源的那一份文本**。
150
+ *
151
+ * 为什么不能沿用"美化 JSON 的 `.length`":紧凑路径(`fields` / `historyRequestData` /
152
+ * `systemFields`,见 `stdout-compact.ts`)会把这几块的缩进空白整段压掉,实测 `createForm`
153
+ * 整份实测小 **约 30%**(`hugeLayout(120)` 桩 23,522 → 16,613 字符)。判线若还按美化量就会系统性
154
+ * 高估 —— 该内联的表单被判超线落盘。
155
+ *
156
+ * 信封形状必须与 `shortcuts/workflow/index.ts` 的 `successEnvelope` **同形**(`createForm` 不带
157
+ * `meta`,故此处没有该键):测试用真的 `successEnvelope` + `stringifyForStdout` 复算 `outputChars`
158
+ * 把它锁住,`successEnvelope` 一改形状这条用例就会红。
159
+ */
160
+ function finalStdoutChars(data, operation) {
161
+ const envelope = { schemaVersion: 1, ok: true, operation, data, warnings: [] };
162
+ return stdoutCharsOf(envelope, compactStdoutPaths(operation));
84
163
  }
85
- async function compactFormOutput(result, env, resultFileName) {
164
+ /**
165
+ * 大表单输出裁剪:超安全线就落盘 + 只回指针。
166
+ *
167
+ * `keep` 是**回挂**给调用方的口子(2026-10-14 起;当晚从 `{always, ifFits}` 两桶改成**有序清单**):
168
+ * - 不给 `ifFits`:无论多大都必须留在 stdout —— 它们是"不读那份文件也能继续干活"的全部依据;
169
+ * - 给 `ifFits: true`:**挂上后仍在安全线内才留**(如 `fields`)。真·巨型表单挂不回去,此时只能让
170
+ * 模型去读文件,提示语也随最终是否挂上 `fields` 改写(见 `resultFileNotice`)。
171
+ *
172
+ * ★ 顺序为什么是契约(2026-10-14 晚,真机反证):宿主可能**中段截断**(头尾保留、中间省略),
173
+ * 所以"排在前面的键先保"。真机那次把 `fields` 排在 `historyRequestData` **之后**,截断正好吃掉
174
+ * `fields` 前半段 ⇒ 现在规定 `contextId` → `fields` → `historyRequestData` 的次序。
175
+ *
176
+ * 传 `charBudget` 则进**严格档**(`createForm` 专用,2026-10-15 用户裁定):改用**字符**口径判线,
177
+ * 且超线时**只回那一句提示**、`keep` 整份失效 —— 与上面默认档「回挂干活必需键」的口径**正好相反**,
178
+ * 两者不要互相套用。`editLoad` 走默认档(字节口径 + 无 keep),行为不变。
179
+ */
180
+ async function compactFormOutput(result, env, resultFileName, keep = [], charBudget) {
181
+ const entries = keep.filter((entry) => entry && entry.value !== undefined);
182
+ const entriesObject = (list) => {
183
+ const out = {};
184
+ for (const entry of list)
185
+ out[entry.key] = entry.value;
186
+ return out;
187
+ };
188
+ // ★ 严格档(`createForm`):**字符**口径判线;超线时 stdout 只留那一句提示。
189
+ if (charBudget) {
190
+ // `chars` 是**原值**:`outputChars` 自述的就是"这份 `data` 有多大",口径必须与落盘文件里那份
191
+ // 载荷严格一致(用例 `outputChars 必须等于「最终 stdout 文本」的字符数` 锁着这一条)。
192
+ const chars = finalStdoutChars(result, charBudget.operation);
193
+ // `measured` 只在阈值比较里用:`reserve` 里的键(`durationMs`)"结果里会有、判线时还没有值",
194
+ // 按位数用哨兵把它占住(见 `FormOutputCharBudget.reserve`)——宁可高估提前落盘,不可低估被截断。
195
+ const measured = charBudget.reserve
196
+ ? finalStdoutChars({ ...result, ...charBudget.reserve }, charBudget.operation)
197
+ : chars;
198
+ // 未超线 ⇒ 原样返回完整结果(与默认档同形,`keep` 本就是 `result` 里已有的键)。
199
+ if (measured <= charBudget.safeChars)
200
+ return { ...result, ...entriesObject(entries) };
201
+ const payload = JSON.stringify(result, null, 2);
202
+ const resultFile = await writeStateFileBestEffort(resultFileName, payload, env.ctx);
203
+ if (!resultFile) {
204
+ // 落盘不可用 ⇒ 没有文件可指,只能保持全量输出(宁大不丢数据),与默认档同口径。
205
+ return {
206
+ ...result,
207
+ ...entriesObject(entries),
208
+ outputChars: chars,
209
+ largeOutputNotice: READ_WHOLE_OUTPUT_HINT,
210
+ };
211
+ }
212
+ // 只回那一句(`resultFile` 排最前,被截断也看得见)。
213
+ return withLeadingMeta({
214
+ resultFile,
215
+ persistedResult: true,
216
+ outputChars: chars,
217
+ largeOutputNotice: createFormResultFileNotice(resultFile),
218
+ }, {});
219
+ }
86
220
  const bytes = stdoutBytesOf(result);
87
221
  // 未超本地安全线 ⇒ 原样返回,不做任何裁剪、也不落盘。
88
222
  if (bytes <= STDOUT_SAFE_BYTES)
89
- return result;
223
+ return { ...result, ...entriesObject(entries) };
90
224
  const payload = JSON.stringify(result, null, 2);
91
225
  const resultFile = await writeStateFileBestEffort(resultFileName, payload, env.ctx);
92
226
  if (!resultFile) {
93
- return { ...result, outputBytes: bytes, largeOutputNotice: READ_WHOLE_OUTPUT_HINT };
227
+ return { ...result, ...entriesObject(entries), outputBytes: bytes, largeOutputNotice: READ_WHOLE_OUTPUT_HINT };
94
228
  }
229
+ const lines = payload.split('\n').length;
95
230
  // 只回提示:路径说明 + 读命令(`resultFile` 排最前,被截断也看得见)。
96
- return withLeadingMeta({
97
- resultFile,
98
- persistedResult: true,
99
- outputBytes: bytes,
100
- largeOutputNotice: resultFileNotice(resultFile, payload.split('\n').length),
101
- }, {});
231
+ const pointer = () => withLeadingMeta({ resultFile, persistedResult: true, outputBytes: bytes, largeOutputNotice: '' }, {});
232
+ const withOptional = { ...pointer(), ...entriesObject(entries) };
233
+ const final = entries.some((entry) => entry.ifFits) && stdoutBytesOf(withOptional) > STDOUT_SAFE_BYTES
234
+ ? { ...pointer(), ...entriesObject(entries.filter((entry) => !entry.ifFits)) }
235
+ : withOptional;
236
+ final.largeOutputNotice = resultFileNotice(resultFile, lines, 'fields' in final && Boolean(final.fields));
237
+ return final;
102
238
  }
103
239
  async function requestJson(env, method, path, query, body, options = {}) {
104
240
  const response = await env.host.workflowRequest({
@@ -583,15 +719,6 @@ function slimFieldInfo(field) {
583
719
  slim.optionsTruncated = true;
584
720
  return slim;
585
721
  }
586
- function requiredFieldsWithSystem(formRequired, runtime, sysPermissions) {
587
- const ids = formRequired.map((f) => String(f.id ?? ''));
588
- for (const sysKey of Object.keys(SYSTEM_FIELD_KEYS)) {
589
- const perm = sysPermissions[sysKey] || {};
590
- if (perm.isview && perm.ismandatory)
591
- ids.push(sysKey);
592
- }
593
- return ids;
594
- }
595
722
  // ---------------------------------------------------------------------------
596
723
  // 附件上传限制解析
597
724
  // ---------------------------------------------------------------------------
@@ -858,6 +985,320 @@ function buildFileUploadParams(runtime, frame, fieldId, isCreate, secretLevelId,
858
985
  params.maxNum = maxNum;
859
986
  return params;
860
987
  }
988
+ /**
989
+ * 布局里**隐藏行**上的字段(2026-10-14 用户口径,V12.1.112)。
990
+ *
991
+ * 用户在表单上看到的是一张 Excel 表:`config.workbook` 每个「节点」都有自己的行列网格——
992
+ * `Sheet1` 是**页面根网格**,其余键是**容器组件自己的网格**(如明细表 `DetailTable` 的内部网格)。
993
+ * `sheet.rows[i].visible === false` ⇒ **该整行内容在前台一律不可见**。
994
+ *
995
+ * 为什么必须补这条:`流程类型` / `打印状态` 在 `fields[]` 与 `viewRelateConfig` 里跟普通业务
996
+ * Select(如 `报销方式`)**逐键同形**——`readOnly`/`disableEdit` 都是 false、`display` 也是 true、
997
+ * 连默认值都有,载荷里**没有任何"这是隐藏字段"的标记**。它们唯一的不同就是**落在隐藏行上**
998
+ * (真机 总部费用报销:两者都在 `Sheet1.rows` 第 76 行,该行 `visible:false`,整块 74~83 行全隐藏)。
999
+ *
1000
+ * 判定必须**沿容器链往上走**:组件的 `config.excel.row` 是**相对其父容器**的坐标
1001
+ * (`excel.parent === 'ROOT'` 才是页面根网格),父容器本身还可能是一个组件——真机
1002
+ * `单据状态` / `收单日期` / `到达出纳日期` / `分部` 这些字段就挂在 `MultiContent` 组合控件里,
1003
+ * 是组合控件本体(第 76 行)被藏了 ⇒ 它内部所有字段用户同样看不到(用户原话:"该行的所有内容
1004
+ * 在表单上都是隐藏的")。任一层落在隐藏行上即判隐藏。
1005
+ *
1006
+ * 查不到对应网格时(老布局 / 节点缺失 / `workbook` 整个没有)**不判隐藏**——fail-open,
1007
+ * 与 `compsFieldIds` 为空时不筛同一降级口径:宁多返字段,不可误删用户真能看到的字段。
1008
+ */
1009
+ function collectHiddenRowFieldIds(page0, comps) {
1010
+ const hiddenRowFieldIds = new Set();
1011
+ const workbook = page0?.config?.workbook;
1012
+ if (!workbook || typeof workbook !== 'object')
1013
+ return hiddenRowFieldIds;
1014
+ const rootSheet = workbook.Sheet1;
1015
+ // nodeId → 组件:组件的 `excel.parent` 指向父容器的 nodeId,靠它才能找回父容器自己的网格。
1016
+ const compByNodeId = new Map();
1017
+ for (const comp of toArray(comps)) {
1018
+ if (!comp || typeof comp !== 'object')
1019
+ continue;
1020
+ const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
1021
+ if (nodeId)
1022
+ compByNodeId.set(nodeId, comp);
1023
+ }
1024
+ const rowHidden = (sheet, row) => {
1025
+ const rows = sheet && Array.isArray(sheet.rows) ? sheet.rows : [];
1026
+ const index = Number(row);
1027
+ if (!Number.isInteger(index) || index < 0 || index >= rows.length)
1028
+ return false;
1029
+ const entry = rows[index];
1030
+ return Boolean(entry) && typeof entry === 'object' && entry.visible === false;
1031
+ };
1032
+ for (const comp of toArray(comps)) {
1033
+ if (!comp || typeof comp !== 'object')
1034
+ continue;
1035
+ const fieldId = String(comp.config?.fieldId ?? '').trim();
1036
+ if (!fieldId)
1037
+ continue;
1038
+ let cursor = comp;
1039
+ while (cursor) {
1040
+ const excel = cursor.config?.excel;
1041
+ if (!excel || typeof excel !== 'object')
1042
+ break;
1043
+ const parentId = String(excel.parent ?? '').trim();
1044
+ if (!parentId || parentId === 'ROOT') {
1045
+ if (rowHidden(rootSheet, excel.row))
1046
+ hiddenRowFieldIds.add(fieldId);
1047
+ break;
1048
+ }
1049
+ const owner = compByNodeId.get(parentId);
1050
+ const sheet = owner ? workbook[String(owner.id ?? '')] : null;
1051
+ if (sheet && rowHidden(sheet, excel.row)) {
1052
+ hiddenRowFieldIds.add(fieldId);
1053
+ break;
1054
+ }
1055
+ cursor = owner;
1056
+ }
1057
+ }
1058
+ return hiddenRowFieldIds;
1059
+ }
1060
+ /**
1061
+ * 明细表**隐藏列**上的字段(2026-10-14 用户口径,V12.1.116)。
1062
+ *
1063
+ * 明细表在 `comps[]` 里是 `type === 'DetailTable'` 的组件:
1064
+ * `comp.id` 是它在 `pageLayouts[0].config.workbook` 里的**网格 key**(用户口径里的「标签页 key」),
1065
+ * `comp.config.subFormId`(真机同 `comp.refBusinessId`)是该明细表自己的 id。
1066
+ * 该网格 `columns[n].visible === false` ⇒ **第 n 列整列在表单上不显示**,列上的字段用户在前台
1067
+ * 看不到;载荷里与隐藏行一样**没有任何"隐藏"标记**(只有布局知道)。真机实例(总部费用报销):
1068
+ * 一张 34 列的明细表有 22 列 `visible:false`,涉及 27 个字段。
1069
+ *
1070
+ * 列 → 字段的寻址与隐藏行同一套:字段组件的 `config.excel.parent` 指向**明细表组件的
1071
+ * `excel.nodeId`**、`config.excel.col` 是**列号**(0 起,与 `columns` 下标同口径)。沿容器链往上走,
1072
+ * 任一层落在明细网格的隐藏列上即判隐藏(明细表内的组合控件落在隐藏列时,其内部字段同样看不到)。
1073
+ *
1074
+ * **只认明细表网格**:页面根网格(`Sheet1`)的列隐藏不在本口径内(真机无此态)。
1075
+ * 查不到明细网格 / `columns` 缺失时**不判隐藏**——fail-open,与隐藏行同一降级口径。
1076
+ */
1077
+ function collectHiddenDetailColumnFieldIds(page0, comps) {
1078
+ const hiddenColFieldIds = new Set();
1079
+ const workbook = page0?.config?.workbook;
1080
+ if (!workbook || typeof workbook !== 'object')
1081
+ return hiddenColFieldIds;
1082
+ const sheets = workbook;
1083
+ // nodeId → 组件(组件的 `excel.parent` 指向父容器的 nodeId);同时挑出明细表的网格 key。
1084
+ const compByNodeId = new Map();
1085
+ const detailSheetKeys = new Set();
1086
+ for (const comp of toArray(comps)) {
1087
+ if (!comp || typeof comp !== 'object')
1088
+ continue;
1089
+ const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
1090
+ if (nodeId)
1091
+ compByNodeId.set(nodeId, comp);
1092
+ const type = String(comp.type ?? comp.config?.type ?? '').trim().toLowerCase();
1093
+ if (type !== 'detailtable')
1094
+ continue;
1095
+ const sheetKey = String(comp.id ?? '').trim();
1096
+ if (sheetKey && sheets[sheetKey] && typeof sheets[sheetKey] === 'object')
1097
+ detailSheetKeys.add(sheetKey);
1098
+ }
1099
+ if (!detailSheetKeys.size)
1100
+ return hiddenColFieldIds;
1101
+ const colHidden = (sheet, col) => {
1102
+ const columns = sheet && Array.isArray(sheet.columns) ? sheet.columns : [];
1103
+ const index = Number(col);
1104
+ if (!Number.isInteger(index) || index < 0 || index >= columns.length)
1105
+ return false;
1106
+ const entry = columns[index];
1107
+ return Boolean(entry) && typeof entry === 'object' && entry.visible === false;
1108
+ };
1109
+ for (const comp of toArray(comps)) {
1110
+ if (!comp || typeof comp !== 'object')
1111
+ continue;
1112
+ const fieldId = String(comp.config?.fieldId ?? '').trim();
1113
+ if (!fieldId)
1114
+ continue;
1115
+ let cursor = comp;
1116
+ while (cursor) {
1117
+ const excel = cursor.config?.excel;
1118
+ if (!excel || typeof excel !== 'object')
1119
+ break;
1120
+ const parentId = String(excel.parent ?? '').trim();
1121
+ if (!parentId || parentId === 'ROOT')
1122
+ break; // 页面根网格不参与本口径
1123
+ const owner = compByNodeId.get(parentId);
1124
+ if (!owner)
1125
+ break;
1126
+ const ownerId = String(owner.id ?? '').trim();
1127
+ if (detailSheetKeys.has(ownerId)) {
1128
+ if (colHidden(sheets[ownerId], excel.col))
1129
+ hiddenColFieldIds.add(fieldId);
1130
+ break;
1131
+ }
1132
+ cursor = owner;
1133
+ }
1134
+ }
1135
+ return hiddenColFieldIds;
1136
+ }
1137
+ /**
1138
+ * 多内容单元格(`MultiContent`)内部**被隐藏的单元格**上的字段(2026-10-14 用户口径,V12.1.117)。
1139
+ *
1140
+ * `comps[]` 里 `type === 'MultiContent'` 是「多内容单元格」组合控件:它把若干子字段摆在自己
1141
+ * **内嵌的**单元格网格里——`config.excel.multiCells`,形如 `[row][col]` 的二维数组(空格子为 `null`),
1142
+ * 每个单元格形如 `{ nodeId, value, style, tag }`。`cell.nodeId` 与子字段组件的
1143
+ * `config.excel.nodeId` 一一对应(真机 41/41 命中,且 `multiCells` 的行列下标与子字段
1144
+ * `excel.row` / `excel.col` 也 41/41 一致)。单元格 `cell.tag.attrs.hide === true`
1145
+ * ⇒ **该单元格里的字段内容在前台不可见**;载荷里与隐藏行 / 隐藏列一样**没有任何"隐藏"标记**,
1146
+ * 只有布局知道。真机实例(总部费用报销):`部门;部门ID` 这个多内容单元格里第二个格子
1147
+ * (`部门ID`,`fieldId 100003720000001374`)就是 `tag.attrs.hide === true`,而同格的
1148
+ * `部门`(`100003720000000074`)照常可见。
1149
+ *
1150
+ * **`MultiContent` 不在 `config.workbook` 里**(它没有自己的 sheet),所以隐藏行 / 隐藏列的
1151
+ * 网格口径天然覆盖不到它——必须单独读内嵌的 `multiCells`。
1152
+ *
1153
+ * 寻址与隐藏行 / 隐藏列同一套:沿容器链往上走,任一层的组件若是 `MultiContent` 且其 `multiCells` 里
1154
+ * 有指向当前 cursor 的 `nodeId` 且该单元格 `hide === true` 即判隐藏(多内容里再嵌组合控件时,
1155
+ * 其内部字段同样看不到)。`multiCells` 缺失 / 结构异常时**不判隐藏**——fail-open,同一降级口径。
1156
+ */
1157
+ function collectHiddenMultiContentFieldIds(comps) {
1158
+ const hiddenFieldIds = new Set();
1159
+ const list = toArray(comps);
1160
+ // nodeId → 组件:组件的 `excel.parent` 指向父容器的 nodeId,靠它才能沿容器链往上走。
1161
+ const compByNodeId = new Map();
1162
+ // **被隐藏的单元格**所对应的子组件 nodeId 集合(`multiCells` 里 `tag.attrs.hide === true` 的那批)。
1163
+ const hiddenCellNodeIds = new Set();
1164
+ for (const comp of list) {
1165
+ if (!comp || typeof comp !== 'object')
1166
+ continue;
1167
+ const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
1168
+ if (nodeId)
1169
+ compByNodeId.set(nodeId, comp);
1170
+ }
1171
+ for (const comp of list) {
1172
+ if (!comp || typeof comp !== 'object')
1173
+ continue;
1174
+ const type = String(comp.type ?? comp.config?.type ?? '').trim().toLowerCase();
1175
+ if (type !== 'multicontent')
1176
+ continue;
1177
+ const cells = comp.config?.excel?.multiCells;
1178
+ if (!Array.isArray(cells))
1179
+ continue;
1180
+ for (const row of cells) {
1181
+ // `multiCells` 是 `[row][col]` 二维数组,空格子是 `null`。
1182
+ if (!Array.isArray(row))
1183
+ continue;
1184
+ for (const cell of row) {
1185
+ if (!cell || typeof cell !== 'object')
1186
+ continue;
1187
+ const attrs = cell.tag?.attrs;
1188
+ if (!attrs || typeof attrs !== 'object' || attrs.hide !== true)
1189
+ continue;
1190
+ const cellNodeId = String(cell.nodeId ?? '').trim();
1191
+ if (cellNodeId)
1192
+ hiddenCellNodeIds.add(cellNodeId);
1193
+ }
1194
+ }
1195
+ }
1196
+ if (!hiddenCellNodeIds.size)
1197
+ return hiddenFieldIds;
1198
+ for (const comp of list) {
1199
+ if (!comp || typeof comp !== 'object')
1200
+ continue;
1201
+ const fieldId = String(comp.config?.fieldId ?? '').trim();
1202
+ if (!fieldId)
1203
+ continue;
1204
+ let cursor = comp;
1205
+ while (cursor) {
1206
+ const excel = cursor.config?.excel;
1207
+ if (!excel || typeof excel !== 'object')
1208
+ break;
1209
+ const nodeId = String(excel.nodeId ?? '').trim();
1210
+ if (nodeId && hiddenCellNodeIds.has(nodeId)) {
1211
+ hiddenFieldIds.add(fieldId);
1212
+ break;
1213
+ }
1214
+ const parentId = String(excel.parent ?? '').trim();
1215
+ if (!parentId || parentId === 'ROOT')
1216
+ break;
1217
+ cursor = compByNodeId.get(parentId) ?? null;
1218
+ }
1219
+ }
1220
+ return hiddenFieldIds;
1221
+ }
1222
+ /**
1223
+ * 页面根网格(Sheet1)上**被隐藏的单元格**里的字段(2026-10-20 修复)。
1224
+ *
1225
+ * `pageLayouts[0].config.workbook.Sheet1.cells[]` 里 `cell.tag.attrs.hide === true`
1226
+ * ⇒ 该单元格(`col:row`)在表单上不显示,格子里的字段用户看不到。
1227
+ *
1228
+ * 寻址必须**沿容器链往上走**(与隐藏行 / 隐藏列 / 多内容格同一套):组件的 `config.excel.col`/`row`
1229
+ * 是**相对其父容器**的坐标,只有 `excel.parent === 'ROOT'` 那一层才是 Sheet1 的行列号。
1230
+ * 真机反例(总部费用报销,「报销人直接上级」`fieldId 100003720000076596`):字段自身的坐标是
1231
+ * `col:0,row:0`(相对它所在的 `MultiContent` 容器),真正被藏起来的是 Sheet1 上 `col:3,row:4`
1232
+ * 那个格子里的**容器本体**——只比字段自己的坐标就永远不可能命中,值于是漏进「已填内容」。
1233
+ *
1234
+ * 之前这里的实现只拿 comp 自己的 `excel.col:row` 与 Sheet1 的隐藏格直接比对,组合控件(尤其是
1235
+ * `MultiContent`)内部的字段一概漏判;改成沿链上溯后,容器被藏 ⇒ 它内部所有字段一并判隐藏。
1236
+ *
1237
+ * 查不到 `cells` / 没有隐藏格时**不判隐藏**——fail-open,与其它隐藏口径同一降级原则。
1238
+ */
1239
+ function collectHiddenSheetCellFieldIds(page0, comps) {
1240
+ const hiddenFieldIds = new Set();
1241
+ const sheet1 = page0?.config?.workbook?.Sheet1;
1242
+ const cells = sheet1 && Array.isArray(sheet1.cells) ? sheet1.cells : [];
1243
+ if (!cells.length)
1244
+ return hiddenFieldIds;
1245
+ /** `col:row`(两边同口径归一成数字,避免 `"3"` 与 `3` 对不上);坐标不可解析时返回 null。 */
1246
+ const posOf = (col, row) => {
1247
+ const c = Number(col);
1248
+ const r = Number(row);
1249
+ if (!Number.isFinite(c) || !Number.isFinite(r))
1250
+ return null;
1251
+ return `${c}:${r}`;
1252
+ };
1253
+ const hidePositions = new Set();
1254
+ for (const cell of cells) {
1255
+ if (!cell || typeof cell !== 'object')
1256
+ continue;
1257
+ const attrs = cell.tag?.attrs;
1258
+ if (!attrs || typeof attrs !== 'object' || !attrs.hide)
1259
+ continue;
1260
+ const pos = posOf(cell.col, cell.row);
1261
+ if (pos)
1262
+ hidePositions.add(pos);
1263
+ }
1264
+ if (!hidePositions.size)
1265
+ return hiddenFieldIds;
1266
+ // nodeId → 组件:组件的 `excel.parent` 指向父容器的 nodeId,靠它才能找回父容器自己的坐标。
1267
+ const compByNodeId = new Map();
1268
+ for (const comp of toArray(comps)) {
1269
+ if (!comp || typeof comp !== 'object')
1270
+ continue;
1271
+ const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
1272
+ if (nodeId)
1273
+ compByNodeId.set(nodeId, comp);
1274
+ }
1275
+ for (const comp of toArray(comps)) {
1276
+ if (!comp || typeof comp !== 'object')
1277
+ continue;
1278
+ const fieldId = String(comp.config?.fieldId ?? '').trim();
1279
+ if (!fieldId)
1280
+ continue;
1281
+ let cursor = comp;
1282
+ while (cursor) {
1283
+ const excel = cursor.config?.excel;
1284
+ if (!excel || typeof excel !== 'object')
1285
+ break;
1286
+ const parentId = String(excel.parent ?? '').trim();
1287
+ if (!parentId || parentId === 'ROOT') {
1288
+ // 到根网格这一层才是 Sheet1 的真实行列号(容器内的坐标是相对的,比不得)。
1289
+ const pos = posOf(excel.col, excel.row);
1290
+ if (pos && hidePositions.has(pos))
1291
+ hiddenFieldIds.add(fieldId);
1292
+ break;
1293
+ }
1294
+ const owner = compByNodeId.get(parentId);
1295
+ if (!owner)
1296
+ break;
1297
+ cursor = owner;
1298
+ }
1299
+ }
1300
+ return hiddenFieldIds;
1301
+ }
861
1302
  function parseLayoutMeta(layoutData) {
862
1303
  const hiddenCellIds = new Set();
863
1304
  const compsFieldIds = new Set();
@@ -867,44 +1308,6 @@ function parseLayoutMeta(layoutData) {
867
1308
  if (Array.isArray(pageLayouts) && pageLayouts.length)
868
1309
  page0 = pageLayouts[0];
869
1310
  if (page0 && typeof page0 === 'object') {
870
- const wb = page0.config && page0.config.workbook ? page0.config.workbook : {};
871
- const sheet1 = wb.Sheet1;
872
- const cells = sheet1 ? sheet1.cells : null;
873
- const hidePositions = new Set();
874
- if (Array.isArray(cells)) {
875
- for (const cell of cells) {
876
- if (!cell || typeof cell !== 'object')
877
- continue;
878
- const attrs = cell.tag && cell.tag.attrs ? cell.tag.attrs : {};
879
- if (attrs.hide) {
880
- try {
881
- hidePositions.add(`${cell.col}:${cell.row}`);
882
- }
883
- catch {
884
- /* ignore */
885
- }
886
- }
887
- }
888
- }
889
- if (hidePositions.size) {
890
- for (const comp of page0.comps || []) {
891
- if (!comp || typeof comp !== 'object')
892
- continue;
893
- const cfg = comp.config || {};
894
- const fid = String(cfg.fieldId ?? '').trim();
895
- if (!fid)
896
- continue;
897
- const excel = cfg.excel || {};
898
- try {
899
- const pos = `${excel.col}:${excel.row}`;
900
- if (hidePositions.has(pos))
901
- hiddenCellIds.add(fid);
902
- }
903
- catch {
904
- /* ignore */
905
- }
906
- }
907
- }
908
1311
  for (const comp of page0.comps || []) {
909
1312
  if (!comp || typeof comp !== 'object')
910
1313
  continue;
@@ -918,6 +1321,18 @@ function parseLayoutMeta(layoutData) {
918
1321
  layoutSubformIds.add(sid);
919
1322
  }
920
1323
  }
1324
+ // 行隐藏 / 单元格隐藏 / 明细表隐藏列 / 多内容单元格隐藏**同义**(都是"用户在表单上看不到"),
1325
+ // 并成一份 `hiddenCellIds`:下游 `visibleFields`/`initialValue`/`missingRequiredFields`/「已填内容」
1326
+ // /联动提示全部只认这一份,且随句柄以 `hiddenCellFieldIds` 传给 `createRequest`,两边口径天然一致。
1327
+ // 单元格隐藏**必须沿容器链上溯**(2026-10-20 修复):容器的 `excel` 坐标才是 Sheet1 上的位置。
1328
+ for (const fid of collectHiddenSheetCellFieldIds(page0, page0.comps))
1329
+ hiddenCellIds.add(fid);
1330
+ for (const fid of collectHiddenRowFieldIds(page0, page0.comps))
1331
+ hiddenCellIds.add(fid);
1332
+ for (const fid of collectHiddenDetailColumnFieldIds(page0, page0.comps))
1333
+ hiddenCellIds.add(fid);
1334
+ for (const fid of collectHiddenMultiContentFieldIds(page0.comps))
1335
+ hiddenCellIds.add(fid);
921
1336
  }
922
1337
  return { hiddenCellIds, compsFieldIds, layoutSubformIds };
923
1338
  }
@@ -940,7 +1355,8 @@ function applyViewRelateHidden(layoutSubformIds, viewRelateConfig) {
940
1355
  * `viewRelateConfig` 长度为 0"并非服务端没配置,而是 `getFormLayout` 请求多带了顶层 `permissionId`
941
1356
  * (未启用节点权限的表单会因此拿不到字段级属性;见 `buildLayoutRequest`)。该请求口径修正后,
942
1357
  * 本告警只在服务端**确实**不下发字段属性时才出现(如启用节点权限的表单)。
943
- * 真正未下发时:`requiredFields` 只剩系统字段、`subFormRequired` 恒为 `{}`,**不代表表单没有必填项**。
1358
+ * 真正未下发时:服务端没有字段级的 required / disableEdit / display 配置,`fields` 里的只读 / 禁止编辑
1359
+ * 判定与「标记未下发」都因此失真,**不代表表单没有必填项**。
944
1360
  *
945
1361
  * V12.1.27 纠正(此前措辞有硬伤):原文案把 `missingFields` 也写成"为空不代表没有必填/无法据此校验业务必填",
946
1362
  * 与事实相反——本租户的业务必填多为**条件必填**(`viewLinkages` 的 `required` 规则),
@@ -950,13 +1366,16 @@ function applyViewRelateHidden(layoutSubformIds, viewRelateConfig) {
950
1366
  *
951
1367
  * 2026-09-23:`createForm` 不再下发联动规则(原 `conditionalRequired`),必填判断一律以 `missingFields` 为准。
952
1368
  *
953
- * 返回 null 表示静态必填信息可用,无需告警。
1369
+ * 2026-10-14:`createForm` 不再下发任何必填清单(`requiredFields` / `subFormRequired` / `fillPlan` 全撤),
1370
+ * 本告警的措辞随之改成"静态权限信息没拿到",不再点名那三个已不存在的键。
1371
+ *
1372
+ * 返回 null 表示静态权限信息可用,无需告警。
954
1373
  */
955
1374
  function requiredInfoWarningText(context) {
956
1375
  const ctx = context || {};
957
1376
  const vrc = Array.isArray(ctx.viewRelateConfig) ? ctx.viewRelateConfig : [];
958
1377
  if (vrc.length)
959
- return null; // OA 下发了字段级权限配置 → 静态必填信息可信
1378
+ return null; // OA 下发了字段级权限配置 → 字段的只读 / 可见判定可信
960
1379
  const fields = Array.isArray(ctx.fields) ? ctx.fields : [];
961
1380
  const business = fields.filter((f) => f && f.id
962
1381
  && !String(f.id).startsWith('REQUEST_') && !String(f.id).startsWith('SECLEVEL')
@@ -967,27 +1386,39 @@ function requiredInfoWarningText(context) {
967
1386
  // 有任何字段带必填标记 → 至少拿到了部分真实信息,不必告警
968
1387
  if (business.some((f) => f.required))
969
1388
  return null;
970
- return 'OA 未下发该表单的**静态**必填标记,本次 requiredFields / subFormRequired 只含系统字段(流程标题等),'
1389
+ return 'OA 未下发该表单的**静态**字段权限配置:本次 `fields` 里的只读 / 禁止编辑判定可能偏保守,'
971
1390
  + '但这**不代表该表单没有业务必填项**——业务必填多为「条件必填」,只有按实际所填数据才判定得出。'
972
- + 'missingFields 由 createRequest 按实际所填数据实时计算、**可信**:照常发起,'
973
- + '返回 NEEDS_INPUT 时把 missingFields 一次性向用户问齐。'
1391
+ + '必填一律以 createRequest 返回的 `missingFields` 为准(按实际所填数据实时计算、**可信**):'
1392
+ + '照常发起,返回 NEEDS_INPUT 时把 missingFields 一次性向用户问齐。'
974
1393
  + '不要对用户说"系统不会校验必填项"。';
975
1394
  }
976
1395
  // ---------------------------------------------------------------------------
977
1396
  // 发起侧工作流解析(原 createList 的职责;候选池 = 当前用户「可发起的工作流」)
978
1397
  // ---------------------------------------------------------------------------
979
1398
  /**
980
- * 把一个「纯名称」切成多个匹配关键词(2026-09-23 用户裁定)。
1399
+ * 名称关键词的**填充词**(按长词优先依次脱壳)。
1400
+ *
1401
+ * 用户裁定:「去掉一些词:工作流、流程」(2026-09-23);「现在要**额外去掉申请**关键字」
1402
+ * (2026-10-21)。顺序有讲究:`工作流程` 必须排在 `工作流` / `流程` 之前
1403
+ * (`工作流` 与 `流程` 在 `流` 处重叠,先剥短的会漏判)。
1404
+ */
1405
+ const CREATE_KEYWORD_FILLERS = ['工作流程', '工作流', '流程', '申请'];
1406
+ /**
1407
+ * 把一个「纯名称」切成多个名称关键词(2026-09-23 用户裁定 + 2026-10-21 追加净词)。
981
1408
  *
982
1409
  * 单个格内可用逗号分隔多个关键词(中英文逗号都认),也可用数组多元素表达;命中结果做**并集**,
983
- * 目的是**提高候选召回** —— 用户首次要发起某个流程时,多给几个说法更容易命中他真正要发起的那个,
984
- * 从而少让用户回来补全名称。
985
- *
986
- * 去词规则(用户裁定,逐字):「**去掉一些词:工作流、流程**」。落到实现上是——
987
- * **只丢弃"整格就是填充词"的关键词**(`流程` / `工作流` / `工作流程`),其余**原样保留**:
988
- * - `"报销,报销流程,流程"` → 关键词 = `报销`、`报销流程`(**两个**,`流程` 被丢掉);
989
- * - `报销流程` **不在词内剥离** —— 它是有效关键词(剥成 `报销` 会让"多关键词"退化成单关键词)。
990
- * 匹配不到的关键词在并集里自然贡献为空,无害,所以不必额外清理。
1410
+ * 目的是**提高候选召回** —— 用户首次要发起某个流程时,多给几个说法更容易命中他真正要发起的那个。
1411
+ *
1412
+ * 每个关键词带**两副面孔**(2026-10-21 用户裁定):
1413
+ * - `raw` = 原样(`请假流程`、`请假申请`);
1414
+ * - `core` = 去掉填充词后的净词(`请假流程` → `请假`、`请假申请` → `请假`、`总部请假` → `总部请假`)。
1415
+ *
1416
+ * 两者**都参与本地模糊过滤**(候选取并集),于是「请假申请」既按它自己、也按「请假」去找,
1417
+ * 能命中「请假申请单」这类名字;净词与原词相同的词天然只算一次。
1418
+ *
1419
+ * **只丢弃"整格全是填充词"的关键词**(`流程` / `工作流` / `申请` …):它们不代表任何业务含义,
1420
+ * 留着会命中一大批无关流程。`报销申请` 这类**含填充词但本身有效**的词不会被丢 ——
1421
+ * 它的净词 `报销` 才是干活的那个。
991
1422
  */
992
1423
  function splitCreateKeywords(raw) {
993
1424
  const out = [];
@@ -996,15 +1427,18 @@ function splitCreateKeywords(raw) {
996
1427
  const token = piece.trim();
997
1428
  if (!token)
998
1429
  continue;
999
- // 填充词按**长词优先**脱壳:`工作流程` → 空(`工作流`+`流程` 在 `流` 处重叠,顺序错会漏判)
1000
- const residue = token.split('工作流程').join('').split('工作流').join('').split('流程').join('');
1001
- if (!residue.trim())
1430
+ let core = token;
1431
+ for (const filler of CREATE_KEYWORD_FILLERS)
1432
+ core = core.split(filler).join('');
1433
+ core = core.trim();
1434
+ // 整格都是填充词 ⇒ 丢弃(留着会让 `流程` 命中「请假流程」「合同流程」等一大批)
1435
+ if (!core)
1002
1436
  continue;
1003
1437
  const key = token.toLowerCase();
1004
1438
  if (seen.has(key))
1005
1439
  continue;
1006
1440
  seen.add(key);
1007
- out.push(token);
1441
+ out.push({ raw: token, core });
1008
1442
  }
1009
1443
  return out;
1010
1444
  }
@@ -1018,10 +1452,17 @@ function splitCreateKeywords(raw) {
1018
1452
  * - **0 命中 → 澄清**:不能把「没听懂你的条件」伪装成「没有这条流程」,否则模型会如实转述成
1019
1453
  * "没有该工作流",用户拿不到可操作的信息;
1020
1454
  * - 1 命中 → 直接用;
1021
- * - 多命中但候选中**恰有一个与所写名称完全一致** → 直接用,不为"要不要多打几个字"再问一次;
1022
- * - 多命中且没有完全一致的 → 澄清并列出候选(`display` 即 `名称(ID)`,用户选定后原样回传)。
1455
+ * - 多命中时,**全名一致必须唯一**才直接用(2026-10-21 用户二次裁定,撤销同日"按词序先到先得"):
1456
+ * 连"净词 + 原词"一起算,若与候选全名一致的说法只指向**同一个**候选 ⇒ 用它;若指向**多个不同**
1457
+ * 候选(如 `费用报销,报销流程审批` 恰好各自对上一条)⇒ 这是用户给的条件本身有歧义,仍回来澄清;
1458
+ * - 多命中且没有任何全名一致的 → 澄清并列出候选(`display` 即 `名称(ID)`,用户选定后原样回传)。
1459
+ * 这一档在澄清**之前**先做一次消歧(2026-10-20 用户裁定):把候选拿去查「我发起的」工作流树,
1460
+ * 用户近一年只发起过其中**一个**就直接用它(`from:'mineRecentUnique'`);否则/失败则照旧澄清。
1461
+ * **只有这一档会消歧**:0 命中、以及"只给了填充词"那两档都不查(前者本就没候选可比,
1462
+ * 后者在发任何远端请求前就澄清了)。
1023
1463
  *
1024
- * 名称格支持**多关键词**(`"报销,报销流程"`,见 `splitCreateKeywords`):各关键词命中做并集去重后,
1464
+ * 名称格支持**多关键词**(`"报销,报销流程"`,见 `splitCreateKeywords`):每个关键词同时贡献
1465
+ * **净词**与**原词**(`请假申请` → `请假` + `请假申请`),两组词各自命中后**并集去重**,
1025
1466
  * 再走上面同一套判据。**只发一次** `getNewFlowListData` 而不是每个关键词并行各发一次 —— 该接口
1026
1467
  * 请求体固定为 `{wfpBelongTypeCode, loadOfsFlow, clientCtrl}`、**没有任何关键词入参**(V11.3 Python
1027
1468
  * 基线 `e10_workflow_create.py:395-436` 同样是取全量后本地过滤;真机单笔 ≈1,649 ms / 228,929 B),
@@ -1044,8 +1485,17 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
1044
1485
  // 一个关键词都没解析出来(如只传了 `流程`)时**连可发起列表都不去拉**
1045
1486
  // (该接口真机单笔 ≈1,649 ms / 228,929 B),与"完全不传 workflow"同一口径。
1046
1487
  const explicitIds = new Set();
1047
- const nameTerms = [];
1048
- const keywordSeen = new Set();
1488
+ /** 参与**本地模糊过滤**的词:每个关键词先净词、再原词(同一词只留一个)。 */
1489
+ const filterTerms = [];
1490
+ /**
1491
+ * 参与**全名精确匹配**的词集合:每个关键词先净词、再原词(如 `请假申请` → `请假` + `请假申请`)。
1492
+ *
1493
+ * 只做集合比对、**不看先后次序**(2026-10-21 二次裁定:全名一致必须唯一才直接用,不允许按词序
1494
+ * 先到先得)。
1495
+ */
1496
+ const exactTerms = [];
1497
+ const filterSeen = new Set();
1498
+ const exactSeen = new Set();
1049
1499
  for (const term of rawTerms) {
1050
1500
  const parsed = parseNameWithId(term);
1051
1501
  // 纯数字同样按 ID 处理(`parseNameWithId` 只认 "名称(数字)" 形态)
@@ -1055,14 +1505,29 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
1055
1505
  continue;
1056
1506
  }
1057
1507
  for (const keyword of splitCreateKeywords(parsed.name)) {
1058
- const key = keyword.toLowerCase();
1059
- if (keywordSeen.has(key))
1060
- continue;
1061
- keywordSeen.add(key);
1062
- nameTerms.push(keyword);
1508
+ const coreKey = keyword.core.toLowerCase();
1509
+ const rawKey = keyword.raw.toLowerCase();
1510
+ // 模糊过滤:**单字净词不参与**(`申请单` 剥成 `单` 会把整张清单拉进来);
1511
+ // 原词永远参与,所以"只说得出一个怪名字"时召回不受影响。
1512
+ if (keyword.core.length >= 2 && !filterSeen.has(coreKey)) {
1513
+ filterSeen.add(coreKey);
1514
+ filterTerms.push(keyword.core);
1515
+ }
1516
+ if (!filterSeen.has(rawKey)) {
1517
+ filterSeen.add(rawKey);
1518
+ filterTerms.push(keyword.raw);
1519
+ }
1520
+ if (!exactSeen.has(coreKey)) {
1521
+ exactSeen.add(coreKey);
1522
+ exactTerms.push(keyword.core);
1523
+ }
1524
+ if (!exactSeen.has(rawKey)) {
1525
+ exactSeen.add(rawKey);
1526
+ exactTerms.push(keyword.raw);
1527
+ }
1063
1528
  }
1064
1529
  }
1065
- if (!explicitIds.size && !nameTerms.length) {
1530
+ if (!explicitIds.size && !filterTerms.length) {
1066
1531
  throw workflowClarificationError('workflow', queryText, '要发起哪个流程?请提供该流程的完整名称(或其它可用于检索的关键词)后再继续。');
1067
1532
  }
1068
1533
  const response = await requestJson(env, 'POST', FORM_ENDPOINTS.workflowCreateList, AJAX_QUERY, {
@@ -1083,7 +1548,7 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
1083
1548
  }
1084
1549
  }
1085
1550
  // 名称关键词:逐个取命中,**并集去重**(多给几个说法只扩大候选面,不改变下面的命中判据)
1086
- for (const keyword of nameTerms) {
1551
+ for (const keyword of filterTerms) {
1087
1552
  for (const item of simplifyWorkflows(data, keyword)) {
1088
1553
  if (seen.has(String(item.id)))
1089
1554
  continue;
@@ -1098,13 +1563,30 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
1098
1563
  return { id, name: String(only.workflowname ?? ''), source: explicitIds.has(id) ? 'workflowId' : 'single' };
1099
1564
  }
1100
1565
  if (picked.length > 1) {
1101
- const wanted = new Set(nameTerms.map((t) => t.trim().toLowerCase()).filter(Boolean));
1566
+ // ★ 2026-10-21 用户二次裁定:**退回「全名命中必须唯一」**——同日早先那版"按词序循环、第一个
1567
+ // 全名一致的就用"已撤销。理由:多个说法各自全名命中**不同**候选时,用户给的条件本身就有歧义
1568
+ // (两条互相竞争的精确证据),按词序替用户先到先得等于擅自替他裁决,宁可回来问一句。
1569
+ //
1570
+ // 仍算"唯一"、可直接用的两档:
1571
+ // - 只有一个候选与某个说法全名一致,其余说法只是陪跑(命中的是别的、或没命中);
1572
+ // - 多个说法全名命中的是**同一个**候选(去重后仍是一条)。
1573
+ // "说法"包含每个关键词的**净词**与**原词**(见 `exactTerms`),两者都参与比对。
1574
+ //
1575
+ // 某个说法命中**多条同名候选**(不同 id、名字一字不差)同样算不唯一 ⇒ 一并澄清。
1576
+ const wanted = new Set(exactTerms.map((t) => t.trim().toLowerCase()).filter(Boolean));
1102
1577
  const exact = picked.filter((item) => wanted.has(String(item.workflowname ?? '').trim().toLowerCase()));
1103
- // 多命中时有唯一全名精确匹配 → 直接选中(与查询侧 exact_candidates 优选同一判据)
1104
1578
  if (exact.length === 1) {
1105
1579
  return { id: String(exact[0].id), name: String(exact[0].workflowname ?? ''), source: 'exactName' };
1106
1580
  }
1581
+ // 澄清候选:能对上的那几条优先(比全量 picked 精确),一条都对不上才退回全量。
1107
1582
  const list = exact.length ? exact : picked;
1583
+ // ★ 2026-10-20 用户裁定:澄清前先按「用户近一年实际发起过哪些」消歧。
1584
+ // 用户只发起过其中某一个 ⇒ 直接用,不再问一遍(`from:mineRecentUnique` 标明这是推断的,
1585
+ // 不是用户明说的)。查不到 / 不唯一 / 请求失败 ⇒ 一律回落下面原样的多候选澄清。
1586
+ const inferred = await pickUniqueMineWorkflow(list.map((item) => ({ id: String(item.id), name: String(item.workflowname ?? '') })), env);
1587
+ if (inferred) {
1588
+ return { id: inferred.id, name: inferred.name, source: 'single', from: 'mineRecentUnique' };
1589
+ }
1108
1590
  // `clarificationQuestion` 是**用户口径**:它会被 `note` 要求「原样交给用户确认」,因此
1109
1591
  // 只能写"发生了什么 + 要用户做什么"。**禁止**把本机判定依据(如"其中没有与所写名称完全
1110
1592
  // 一致的")写进这句——那属于 CLI 内部实现,外传会让用户在确认卡片上看到与己无关的判据
@@ -1124,9 +1606,9 @@ async function resolveCreateWorkflow(rawWorkflow, env) {
1124
1606
  *
1125
1607
  * 背景(2026-09-23 实测 + 用户裁定):`POST …memoryPrompt` 带 `workflowId` 时返回的是**全集** ——
1126
1608
  * `--- user.md ---` / `--- memory.md ---`(全局画像 + 全局别名映射)+ `--- memory_<workflowId>.md ---`
1127
- * (该工作流维度)。于是 `launchPreferences` 与入口处读过的无参 `workflow.memoryPrompt`(纯全局)
1609
+ * (该工作流维度)。于是这条命令取回的记忆与入口处读过的无参 `workflow.memoryPrompt`(纯全局)
1128
1610
  * **内容重叠**:真机实测同一租户 `promptText` 2,467 字符 = 全局 543 + 工作流维度 1,924,
1129
- * 同一段全局记忆会在上下文里出现两次。而 `launchPreferences` 的语义是「**该工作流的**发起偏好」,
1611
+ * 同一段全局记忆会在上下文里出现两次。而这里要的是「**该工作流的**发起偏好」,
1130
1612
  * 全局那份已由入口读取覆盖,不应重复下发。
1131
1613
  *
1132
1614
  * 故只保留标记 `--- memory_<workflowId>.md ---` 及其**之后**的内容:
@@ -1149,16 +1631,17 @@ function extractWorkflowMemorySection(promptText, workflowId) {
1149
1631
  return text.slice(at).trim();
1150
1632
  }
1151
1633
  /**
1152
- * 顺带取回用户在这条工作流上的发起偏好(`buildAgentMemoryPrompt`,带 `workflowId`)。
1634
+ * 取回用户在这条工作流上的记忆(`buildAgentMemoryPrompt`,带 `workflowId`)。
1153
1635
  *
1154
- * `promptText` **只含该工作流维度的记忆**(全局画像/全局别名由入口的无参 `workflow.memoryPrompt`
1155
- * 覆盖,这里经 `extractWorkflowMemorySection` 裁掉,避免同一段全局记忆在上下文里出现两次)。
1636
+ * 返回值**只含该工作流维度的那一段**(全局画像 / 全局别名由入口的无参
1637
+ * `workflow.memoryPrompt` 覆盖,这里经 `extractWorkflowMemorySection` 裁掉,
1638
+ * 避免同一段全局记忆在上下文里出现两次)。
1156
1639
  *
1157
- * best-effort:读记忆失败(未开通记忆 / 接口不可用)不是错误——一律按新用户处理,
1158
- * 只体现在 `launchPreferences.available` 上,绝不阻断表单载入(与 `workflow.memoryPrompt` 同口径)。
1640
+ * best-effort:读不到(没开通记忆 / 404 / 接口故障)一律返回空串 —— 对模型而言
1641
+ * 「没有记忆」和「读取失败」的处理完全一样(按新用户处理、不要重试),不需要再分档。
1642
+ * 绝不阻断表单载入(与 `workflow.memoryPrompt` 同口径)。
1159
1643
  */
1160
- async function fetchLaunchPreferences(workflowId, env) {
1161
- const result = { available: false, workflowId, promptText: '' };
1644
+ async function fetchWorkflowMemory(workflowId, env) {
1162
1645
  try {
1163
1646
  const response = await env.host.workflowRequest({
1164
1647
  method: 'POST',
@@ -1168,38 +1651,21 @@ async function fetchLaunchPreferences(workflowId, env) {
1168
1651
  });
1169
1652
  // 该接口原始返回是 {code,msg,status,data:"<markdown>",fail},业务成功标志是 status/fail
1170
1653
  // **不是 success**——这里**不能**用 responseData 解包(会把 data 里的 markdown 当主体,
1171
- // 导致 promptText 恒空)。与 workflow.memoryPrompt 同源。
1172
- // 只认 `data`(字符串)作为偏好正文:**不回退 `msg`**——`msg` 里是「接口返回成功」这类
1173
- // 通用提示,回退它会让"没配记忆"的环境被读成有一条内容是"接口返回成功"的偏好。
1654
+ // 导致正文恒空)。与 workflow.memoryPrompt 同源。
1655
+ // 只认 `data`(字符串)作为正文:**不回退 `msg`**——`msg` 里是「接口返回成功」这类
1656
+ // 通用提示,回退它会让"没配记忆"的环境被读成有一条内容是"接口返回成功"的记忆。
1174
1657
  const body = (response && typeof response === 'object' ? response : {});
1175
- const rawPromptText = typeof body.data === 'string' ? body.data.trim() : '';
1176
- const promptText = extractWorkflowMemorySection(rawPromptText, workflowId);
1177
1658
  const businessOk = body.fail !== true && body.status !== false && body.success !== false;
1178
- result.available = Boolean(businessOk && promptText);
1179
- result.promptText = promptText;
1659
+ if (!businessOk)
1660
+ return '';
1661
+ const rawPromptText = typeof body.data === 'string' ? body.data.trim() : '';
1662
+ return extractWorkflowMemorySection(rawPromptText, workflowId);
1180
1663
  }
1181
- catch (error) {
1182
- // 404 = 该智能体/工作流还没有记忆条目(未开通记忆),属**正常的新用户状态**,不是失败:
1183
- // 保持 `available:false` + `promptText:''`,**不加任何"失败"提示**、也不阻断任务。
1184
- // 其它错误才提示"读取失败、按新用户处理"。
1185
- if (!isNotFoundError(error)) {
1186
- result.note = '该工作流的记忆偏好读取失败,按新用户处理,不要重试。';
1187
- }
1664
+ catch {
1665
+ // 404 = 该工作流还没有记忆条目(未开通记忆)、其它错误 = 接口不可用;
1666
+ // 两者都按「没有记忆」处理,既不加提示也不阻断任务。
1667
+ return '';
1188
1668
  }
1189
- return result;
1190
- }
1191
- /**
1192
- * 是否是「资源不存在」类错误(HTTP 404)。
1193
- *
1194
- * 真机上该错误由 host 统一抛成 `workflowApiError('http_error', 'OA 接口 HTTP 404', {code:404, details:{status:404}})`,
1195
- * 故同时认 `code` 与 `details.status` 两处。不依赖类实例判断,避免把跨包包装过的错误漏掉。
1196
- */
1197
- function isNotFoundError(error) {
1198
- const record = (error && typeof error === 'object') ? error : {};
1199
- if (Number(record.code) === 404)
1200
- return true;
1201
- const details = record.details;
1202
- return Boolean(details && typeof details === 'object' && Number(details.status) === 404);
1203
1669
  }
1204
1670
  // ---------------------------------------------------------------------------
1205
1671
  // createForm:解析工作流 + loadBase/loadFrame/loadMinor/formLayout + 字段输出
@@ -1271,19 +1737,99 @@ function singleExplicitWorkflowId(rawWorkflow) {
1271
1737
  const parsed = parseNameWithId(terms[0]);
1272
1738
  return parsed.id ?? (/^[0-9]+$/.test(terms[0]) ? terms[0] : '');
1273
1739
  }
1274
- /** 表单载入 + 该工作流的发起偏好:两者只共享 `workflowId`,可以并行。 */
1740
+ /** 表单载入 + 该工作流的记忆:两者只共享 `workflowId`,可以并行。 */
1275
1741
  async function loadCreateFormPack(workflowId, env) {
1276
- const [{ data }, launchPreferences] = await Promise.all([
1742
+ const [{ data }, workflowMemory] = await Promise.all([
1277
1743
  buildCreateFormData(workflowId, env),
1278
1744
  // 记忆读取是 best-effort(内部全 catch),不会把表单载入的错误顶掉。
1279
- fetchLaunchPreferences(workflowId, env),
1745
+ fetchWorkflowMemory(workflowId, env),
1280
1746
  ]);
1281
- return { data, launchPreferences };
1747
+ return { data, workflowMemory };
1282
1748
  }
1283
- async function commandCreateForm(args, env) {
1749
+ /**
1750
+ * 智能填参:把「用户意图 + 表单字段上下文」交给独立智能体(`chatScene`),换回可直接使用的 `data`。
1751
+ *
1752
+ * ## 什么时候调
1753
+ *
1754
+ * 两个条件**同时**成立才调,缺一就原样返回(= 老行为):
1755
+ * 1. 调用方给了 `userQuestion`(非空)—— 空串时智能体没有可依据的意图,接口也回不出有效结果,
1756
+ * 所以仍以它为前置(2026-10-20 用户裁定);
1757
+ * 2. 拿到了 `baseUrl`(真连上了 OA)。
1758
+ *
1759
+ * **不看小e 档**(2026-10-20 用户裁定):以前这一档被跳过,理由是"填单发生在宿主页面、算了也没人用";
1760
+ * 现在照走 —— 走通时 `createForm` 直接把算好的 `data` 交给模型,页面填单同样能用上。
1761
+ *
1762
+ * ## 失败怎么处理
1763
+ *
1764
+ * `requestFormFill` 内部吞掉所有异常并返回 `ok:false`,这里**直接不当回事**:返回
1765
+ * `applied:false`,调用方照旧回 `historyRequestData` / `systemFields`。智能体挂掉不能把
1766
+ * 发起流程打死 —— 这是本功能唯一的硬约束。
1767
+ *
1768
+ * ## 传什么记忆
1769
+ *
1770
+ * `userMemory` 用该工作流的记忆(`workflowMemory`)。它是
1771
+ * **一段 Markdown 文本**,不是键值对象(2026-10-19 用户裁定)。全局画像由入口处的
1772
+ * `workflow.memoryPrompt` 覆盖,这里不重复取。
1773
+ */
1774
+ async function resolveIntentFill(pack, userQuestion, baseUrl, env) {
1775
+ const question = String(userQuestion ?? '').trim();
1776
+ // ★ 2026-10-20 用户裁定(口径修正):前置**只有两条** ——「`userQuestion` 非空」+「连上了 OA」。
1777
+ // `userQuestion` 为空时智能体没有可依据的意图、接口也回不出有效结果,故照旧不调;
1778
+ // **小e 档不再豁免**(此前 `hasXiaoEEnv` 直接关掉这条链路,现已按用户要求去掉)。
1779
+ // 走通与否仍由接口结果决定:回不出可用的 `data.chatMessage.content`(JSON)就 `applied:false`。
1780
+ if (!question || !baseUrl)
1781
+ return { applied: false };
1782
+ const fields = Array.isArray(pack.data.fields) ? pack.data.fields : [];
1783
+ if (!fields.length)
1784
+ return { applied: false };
1785
+ const input = buildChatSceneInput({
1786
+ userQuestion: question,
1787
+ workflow: {
1788
+ id: String(pack.data.workflow?.id ?? ''),
1789
+ name: String(pack.data.workflow?.name ?? ''),
1790
+ },
1791
+ fields,
1792
+ historyRequestData: pack.data.historyRequestData ?? null,
1793
+ systemFields: pack.data.systemFields ?? {},
1794
+ userMemory: String(pack.workflowMemory ?? ''),
1795
+ now: formatLocalNow(),
1796
+ });
1797
+ const outcome = await requestFormFill(env, input);
1798
+ if (!outcome.ok || !outcome.data)
1799
+ return { applied: false };
1800
+ return {
1801
+ applied: true,
1802
+ data: outcome.data,
1803
+ // 智能体的标题规则:有默认值时它只会给空串(沿用默认);给了值才是用户明确要求的标题
1804
+ requestName: String(outcome.systemData?.requestName ?? '').trim(),
1805
+ // 供 `createRequestDirect` 把「紧急程度 / 密级 / 签字意见」一并透传给 `createRequest`;
1806
+ // `createForm` 自己不消费这几个键(返回面一个字不变)。
1807
+ systemData: outcome.systemData ?? {},
1808
+ // ★ 2026-10-19 用户裁定:把「生成表单参数」这段的耗时单独带出去(只在真走通 chatScene 时才有)。
1809
+ // 口径 = `requestFormFill` 内部这一趟(发请求 → 拿回 content → parse → 归一化),
1810
+ // 不含本函数上游的会话初始化与表单取数 —— 那些已由 `durationMs` 计入,两者是「整条命令」与
1811
+ // 「其中智能体这一段」的关系,不是并列的两笔开销。
1812
+ durationMs: outcome.durationMs,
1813
+ };
1814
+ }
1815
+ /**
1816
+ * `createForm` 的**取数阶段**(原 `commandCreateForm` 前半段,逐行搬过来,行为一字不改)。
1817
+ *
1818
+ * 抽出来的唯一目的:`createRequestDirect` 需要「未经压缩、未经落盘」的原始 payload —— 那条命令要把
1819
+ * `contextId` 与智能体算出的 `data` 直接喂给 `createRequest`,而 `commandCreateForm` 的输出可能
1820
+ * 因为超线只剩一句"结果已落盘"的提示,取不到这两个键。
1821
+ */
1822
+ async function prepareCreateFormPayload(args, env) {
1284
1823
  // 会话是**惰性**加载的(`ensureSession` 只在首次调用时填充)。本命令随后会并发发起多个请求,
1285
1824
  // 先在这里显式取一次,避免并发首调各自走一遍会话初始化。
1286
- await env.host.getSession();
1825
+ // 顺带留住返回的上下文:智能填参要用它的 `baseUrl` 判「是不是真连了 OA」。
1826
+ //
1827
+ // 顺带计时:登录取凭据(小e 环境下是 `tokens.py` 换 eteamId + `teamsCheck` 换登录人这一整套)
1828
+ // 走的是 auth 层自己的 fetch,不经 `host.workflowRequest` ⇒ 天然落在 `apiDurations` 之外。
1829
+ // 这一段的耗时单独带出去(`authMs`),否则「总耗时 − 各接口」会凭空多出几百毫秒对不上账。
1830
+ const authStartedAt = Date.now();
1831
+ const sessionCtx = await env.host.getSession();
1832
+ const authMs = Date.now() - authStartedAt;
1287
1833
  let target;
1288
1834
  let pack;
1289
1835
  const explicitId = singleExplicitWorkflowId(args.workflow);
@@ -1299,11 +1845,99 @@ async function commandCreateForm(args, env) {
1299
1845
  target = await resolveCreateWorkflow(args.workflow, env);
1300
1846
  pack = await loadCreateFormPack(target.id, env);
1301
1847
  }
1302
- return workflowSuccess('workflow.createForm', await compactFormOutput({
1848
+ // ★ 2026-10-15 用户裁定:`createForm` 改用**字符口径 20,000** 的**严格档**(见
1849
+ // `CREATE_FORM_STDOUT_SAFE_CHARS` / `compactFormOutput` 的 `charBudget`)。
1850
+ //
1851
+ // 与 2026-10-13/14 那两轮「超线仍回挂 `contextId` / `fields` / `historyRequestData`」的口径**相反**:
1852
+ // 超线时 stdout **只回那一句提示**,一个键都不回挂 —— 用户明确要求"不用再返回 json",理由是那几样
1853
+ // 在落盘文件里都能取到(含下一步 `createRequest` 要用的 `contextId`)。因此这里**不再传 `keep` 清单**,
1854
+ // 键序契约(`contextId` → `fields` → `historyRequestData`)也随之失效、不必再维护。
1855
+ //
1856
+ // 判线量的是**最终打出去的那份文本**(`finalStdoutChars`:信封 + `fields` / `historyRequestData` /
1857
+ // `systemFields` 的紧凑路径),不是美化 JSON —— 那三个键压成单行后整份实测小约 30%,用美化口径判线
1858
+ // 会让本可内联的表单被判落盘。
1859
+ //
1860
+ // 未超线时 `fullPayload` 原样返回,行为与从前一致(只是 stdout 上那几个键改单行);落盘那份**始终
1861
+ // 完整**(pretty JSON,未紧凑),所以模型按提示读回来拿到的是与 stdout 同口径的全量内容。
1862
+ //
1863
+ // 显式标注为 `Record<string, any>`:`pack.data` 是带索引签名的宽松对象,而对象字面量里又显式写了
1864
+ // `workflow`,TS 的推断会**丢掉索引签名**、只留这个显式键。
1865
+ const fullPayload = {
1303
1866
  ...pack.data,
1304
- workflow: { ...(pack.data.workflow || {}), selectionSource: target.source },
1305
- launchPreferences: pack.launchPreferences,
1306
- }, env, 'last_create_form.json'));
1867
+ workflow: {
1868
+ ...(pack.data.workflow || {}),
1869
+ selectionSource: target.source,
1870
+ // ★ 2026-10-20 用户裁定:`from` **只在**「多候选按近一年发起记录推断出来」这一种场景出现,
1871
+ // 其余场景一个字都不多(模型据此知道这个工作流是推断的,而不是用户明说的)。
1872
+ ...(target.from ? { from: target.from } : {}),
1873
+ },
1874
+ };
1875
+ // 该工作流的记忆:**一段 Markdown 原文**,读不到就不出这个键(模型见到"没有这个键"就按新用户处理)。
1876
+ if (pack.workflowMemory)
1877
+ fullPayload.workflowMemory = pack.workflowMemory;
1878
+ // ★ 2026-10-19 用户裁定:`createForm` 在返回前调 `chatScene`,由独立智能体把用户意图直接算成
1879
+ // `data` 参数。成功时返回面收窄为「记忆(workflowMemory)+ fields + data」:
1880
+ // - `historyRequestData` 删除(历史值已由智能体消化进 data,再发一份只会让模型二选一)
1881
+ // - `systemFields` / `systemFieldWarning` 删除(智能体已按默认值 + 选项约束算过标题与紧急程度)
1882
+ // - `requiredInfoWarning` **保留**:它不是 systemFields 的内容,而是「OA 没下发静态字段权限,
1883
+ // 照常按 missingFields 补齐」这条**行为指引**;删掉会让模型在未下发权限的表单上误判。
1884
+ // 失败(含小e 档、未传 userQuestion)⇒ 一个字都不改,原样走老口径。
1885
+ const fill = await resolveIntentFill(pack, String(args.userQuestion ?? ''), sessionCtx.baseUrl, env);
1886
+ if (fill.applied) {
1887
+ delete fullPayload.historyRequestData;
1888
+ delete fullPayload.systemFields;
1889
+ delete fullPayload.systemFieldWarning;
1890
+ fullPayload.data = fill.data;
1891
+ // 智能体只在「用户明确要求标题」时才给非空值;它给了才覆盖 createForm 的默认标题
1892
+ if (fill.requestName)
1893
+ fullPayload.requestName = fill.requestName;
1894
+ }
1895
+ return { payload: fullPayload, fill, authMs };
1896
+ }
1897
+ async function commandCreateForm(args, env) {
1898
+ // ★ 2026-10-16 用户裁定:返回值里固定带上**本命令自身的耗时**(毫秒)。
1899
+ // 起点取在函数入口(含下面那次会话惰性初始化),终点取在结果产出之后 —— 即"从命令开始执行到
1900
+ // 执行完成"。取值不做开关、不进 skill 文档(用户明确要求"直接默认加上")。
1901
+ const startedAt = Date.now();
1902
+ // ★ 2026-10-21 用户裁定:耗时口径与 `createRequestDirect` **完全一致** —— 除总耗时外再给
1903
+ // 「每个接口多久」(`apiDurations`)与「登录取凭据多久」(`authMs`)。所以这里也把 host 包一层
1904
+ // 逐接口计时,整条命令(含智能填参那一趟)的请求都会被记上。
1905
+ const spans = [];
1906
+ const timedEnv = { ...env, host: withApiTiming(env.host, spans) };
1907
+ const { payload: fullPayload, fill, authMs } = await prepareCreateFormPayload(args, timedEnv);
1908
+ // 取数阶段已全部结束 ⇒ 这里拿到的是完整的一份;它既是判线要预留的键,也是最终返回的值。
1909
+ const apiDurations = summarizeApiSpans(spans);
1910
+ // 判线的位数预留:`durationMs` / `apiDurations` / `authMs` 一定会有;`fillDurationMs` **只有走通
1911
+ // chatScene 才有**,所以只在它会出现的这一轮才占位 —— 否则没走智能体时也白占十几个字符,边界上
1912
+ // 会让本可内联的表单提前落盘。`apiDurations` 此刻已经求出,直接按真值占(不必用哨兵估)。
1913
+ const reserve = {
1914
+ durationMs: DURATION_RESERVE_SENTINEL,
1915
+ apiDurations,
1916
+ authMs: DURATION_RESERVE_SENTINEL,
1917
+ };
1918
+ if (fill.durationMs !== undefined)
1919
+ reserve.fillDurationMs = DURATION_RESERVE_SENTINEL;
1920
+ const compacted = await compactFormOutput(fullPayload, timedEnv, 'last_create_form.json', [], {
1921
+ safeChars: CREATE_FORM_STDOUT_SAFE_CHARS,
1922
+ operation: 'workflow.createForm',
1923
+ // 判线要量"最终打出去的那份文本",而 `durationMs` / `fillDurationMs` 的真值只能在压缩**之后**求
1924
+ // (它们统计的正是这段时间),故此处按位数用哨兵值先把它算进判线 —— 高估只让边界上的表单提前落盘
1925
+ // (安全方向)。
1926
+ reserve,
1927
+ });
1928
+ // 耗时并在 `data` 里(与 `outputChars` / `persistedResult` 这些运行期属性同处);**两条分支都带上**
1929
+ // —— 超线落盘那一档 `data` 只剩一句提示,并进去它才不会恰好在"大表单"时消失。
1930
+ const result = {
1931
+ ...compacted,
1932
+ durationMs: Date.now() - startedAt,
1933
+ apiDurations,
1934
+ authMs,
1935
+ };
1936
+ // `fillDurationMs`(生成表单参数的耗时)**只在真走通 chatScene 时出现**:失败降级走的是老口径,
1937
+ // 返回面必须一个字都不多(对应需求「不符合预期则仍按原本逻辑执行和返回」)。
1938
+ if (fill.durationMs !== undefined)
1939
+ result.fillDurationMs = fill.durationMs;
1940
+ return workflowSuccess('workflow.createForm', result);
1307
1941
  }
1308
1942
  // ---------------------------------------------------------------------------
1309
1943
  // createForm:loadBase/loadFrame/loadMinor/formLayout + 字段输出 + fileUploadParams
@@ -1317,6 +1951,15 @@ async function commandCreateForm(args, env) {
1317
1951
  async function buildCreateFormData(workflowId, env) {
1318
1952
  const query = { workflowId, isCreate: 1, ...AJAX_QUERY };
1319
1953
  const body = { workflowId, isCreate: 1, isMobile: 0 };
1954
+ /**
1955
+ * 历史参考值的**列表**请求提前发起(P2 提速,2026-10-13):它只依赖 `workflowId`,与下面四个
1956
+ * 表单载入接口没有任何数据依赖,所以放在这里和它们**并发**跑,不再等字段解析完再串行补一次列表
1957
+ * (真机该请求 1–2 s,全落在关键路径上)。
1958
+ *
1959
+ * `.catch(() => null)` 是必须的:这个 Promise 可能因为表单后续抛错而**没人 await**,
1960
+ * 兜住之后就不会留下未处理的拒绝;真要暴露的认证/网络错误由同批的载入请求抛出,不会被吞掉。
1961
+ */
1962
+ const historyListSnapshot = fetchHistoryListSnapshot(env, { workflowId }).catch(() => null);
1320
1963
  // 2026-09-22:这四个接口**彼此没有数据依赖**(入参只需要 workflowId,见各自的 query/body),
1321
1964
  // 真机实测各 171–429 ms、串行合计约 1.1 s;并行后只受最慢的一个约束。
1322
1965
  // `loadMinorInfo` / `tenant/setting` 仍是 best-effort(失败只降级、不阻断),所以各自 catch 成
@@ -1428,7 +2071,23 @@ async function buildCreateFormData(workflowId, env) {
1428
2071
  const systemFieldValues = _normalizeSystemFieldValues(layoutData.businessOperatorObj);
1429
2072
  let linkageActions = Array.isArray(layoutData.linkageActions) ? layoutData.linkageActions : [];
1430
2073
  let onLoadLinkage = [];
1431
- if (linkageActions.length) {
2074
+ /**
2075
+ * 小e 环境下**不跑 `onLoadForm` 字段联动**(2026-10-16 用户裁定)。
2076
+ *
2077
+ * 理由:小e 这一档的真正填单动作由宿主浏览器页面完成,`createForm` 只是给模型一份
2078
+ * 「有哪些字段、默认值是多少」的快照;onLoadForm 联动**带出的值**在这一档用不上
2079
+ * (真到表单页里,页面自己会把联动跑一遍)。省掉这一趟 `fdLinkage` 请求。
2080
+ *
2081
+ * ⚠️ **显示属性联动照跑**:紧接其后的 `computeLinkageVisibility` 数据源是响应里本地的
2082
+ * `layoutData.viewLinkages`,不依赖本次联动结果;跳过字段联动后它按「表单默认值 + 系统字段值」
2083
+ * 求值,`fields` 里的隐藏 / 只读 / 条件必填判定仍然成立。
2084
+ *
2085
+ * `linkageActions` 仍原样随句柄带给 `createRequest`(非小e 环境下那一侧自己会跑联动,见
2086
+ * `commandCreateRequest`),本处跳过的只是 `createForm` 自己发起的这一次 onLoadForm 调用。
2087
+ * 小e 环境下两处都不跑(`createRequest` 同档也跳过字段联动与显示属性联动,由浏览器页面自己算)。
2088
+ * 非小e 环境行为不变。
2089
+ */
2090
+ if (!hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.()) && linkageActions.length) {
1432
2091
  const loadEntries = _entriesFromDefaults(defaults);
1433
2092
  try {
1434
2093
  onLoadLinkage = await runFieldLinkage(env, buildLinkageContext({ runtime, frame, layout: { form_id: frame.form_id }, linkageActions }), loadEntries, new Set(['onLoadForm']), defaults);
@@ -1502,8 +2161,60 @@ async function buildCreateFormData(workflowId, env) {
1502
2161
  }
1503
2162
  }
1504
2163
  }
2164
+ /**
2165
+ * 模型真正需要关心的字段 = **必填字段** ∪ **用户实际能在表单上看到的可编辑字段**。
2166
+ *
2167
+ * 2026-10-14 用户裁定:`createForm` 下发的字段信息(`fields`)与历史数据参考
2168
+ * (`historyRequestData`)都只保留这两类。此前 `fields` 下发的是"布局级字段全集"
2169
+ * (真机 277 条,其中 145 条是只读 / 不可编辑 / 不在当前布局上的字段),模型既看不到也填不了,
2170
+ * 只能逐条扫过去白花上下文。
2171
+ *
2172
+ * 口径来源就是上面两轮循环已经算好的四份清单(主表必填 / 主表可写 / 明细必填 / 明细联动必填)——
2173
+ * 它们本身已过「非只读且可写」「未被联动隐藏」「在当前布局的展示配置内」三道筛,不另立判据。
2174
+ */
2175
+ const modelFieldIds = new Set();
2176
+ for (const list of [requiredFieldsList, editableFieldsList, ...Object.values(subFormRequired), ...Object.values(subFormLinkageRequired)]) {
2177
+ for (const item of list) {
2178
+ const id = String(item?.id ?? '');
2179
+ if (id)
2180
+ modelFieldIds.add(id);
2181
+ }
2182
+ }
2183
+ /**
2184
+ * 字段默认值(`initialValue`)——「有默认值的字段直接把值挂在字段属性上」(2026-10-14 用户裁定)。
2185
+ *
2186
+ * 取代此前两条输出面:独立的 `initialValues` 数组、以及 `fillPlan` 里的 `defaulted` 标记。
2187
+ * 口径不变(`hiddenCellIds` / `modelFieldIds` / 布局展示配置三道筛,与 `fields` 同源),
2188
+ * 只是换了落点:模型看某个字段时,默认值就在它旁边,不必再跨键对照两份清单。
2189
+ * 同一字段有多条默认值时取第一条(与旧 `initialValues` 的逐条追加相比,「哪个是新值」旧口径
2190
+ * 本就无法判定,取首条至少是确定性的)。
2191
+ */
2192
+ const initialValueByFieldId = new Map();
2193
+ for (const d of defaults) {
2194
+ const fid = String((d.formField && d.formField.id) ?? '').trim();
2195
+ if (!fid || hiddenCellIds.has(fid) || initialValueByFieldId.has(fid))
2196
+ continue;
2197
+ if (!modelFieldIds.has(fid))
2198
+ continue;
2199
+ const sf = String((d.subForm && d.subForm.id) ?? '').trim();
2200
+ if (sf) {
2201
+ if (layoutSubformIds.size && !layoutSubformIds.has(sf))
2202
+ continue;
2203
+ }
2204
+ else if (compsFieldIds.size && !compsFieldIds.has(fid)) {
2205
+ continue;
2206
+ }
2207
+ const disp = _detailDisplayText(d);
2208
+ if (!disp)
2209
+ continue;
2210
+ initialValueByFieldId.set(fid, disp);
2211
+ }
1505
2212
  const slimFields = [];
1506
2213
  for (const field of visibleFields) {
2214
+ // 只下发模型需要关心的字段(可编辑 ∪ 必填且可见,见 modelFieldIds 注释);只读 / 联动隐藏 /
2215
+ // 不在当前布局上的字段不下发——模型既填不了也看不到。
2216
+ if (!modelFieldIds.has(String(field.id ?? '')))
2217
+ continue;
1507
2218
  // 键序 id|name|subFormId|componentKey 是契约;`subFormId` **为空就整个省略**(缺该键=主表字段)
1508
2219
  // —— 主表字段占绝大多数,写 `""` 恒白占一条键(真机 278 字段表单:100 个主表字段 × 22 B ≈ 2.2 KB)。
1509
2220
  const slimSubFormId = String(field.subFormId ?? '');
@@ -1511,9 +2222,15 @@ async function buildCreateFormData(workflowId, env) {
1511
2222
  if (slimSubFormId)
1512
2223
  item.subFormId = slimSubFormId;
1513
2224
  item.componentKey = field.componentKey;
1514
- // `fields` **不返回** `required`:必填只从 `requiredFields`(主表)/ `subFormRequired`(明细)取。
1515
- // `fields` 是布局级字段全集(含联动隐藏、未在当前页展示的字段),在这里标必填会让模型
1516
- // 拿它去推演"能填什么 / 哪些是必填",与真实可填集合(`editableFields`)混淆。
2225
+ // 表单初始化时就有的值(OA 预填 + onLoadForm 联动带出)——有才出现该键;**没有默认值的字段不带**。
2226
+ // CLI 拼 `data` 时会自动并入,模型**不必显式传**;用户本次明确改写了它才按用户说的覆盖(见
2227
+ // `workflow-create.md` 的「填值纪律」)。
2228
+ const initialValue = initialValueByFieldId.get(String(field.id ?? ''));
2229
+ if (initialValue !== undefined)
2230
+ item.initialValue = initialValue;
2231
+ // `fields` **不返回** `required`(2026-10-14 起也不再区分必填/可写):本清单就是「可编辑 ∪ 必填且
2232
+ // 可见」的全集,能出现在这里就是能填的字段;必填与否最终只由 `createRequest` 的 `missingFields`
2233
+ // 说了算。标 `required` 只会让模型拿它去推演"哪些是必填",与 `missingFields` 形成双份口径。
1517
2234
  if (field.readOnly || field.disableEdit) {
1518
2235
  item.readOnly = Boolean(field.readOnly);
1519
2236
  item.disableEdit = Boolean(field.disableEdit);
@@ -1558,28 +2275,6 @@ async function buildCreateFormData(workflowId, env) {
1558
2275
  const fieldsById = new Map();
1559
2276
  for (const f of fields)
1560
2277
  fieldsById.set(String(f.id ?? ''), f);
1561
- const initValues = [];
1562
- for (const d of defaults) {
1563
- const fid = String((d.formField && d.formField.id) ?? '').trim();
1564
- if (!fid || hiddenCellIds.has(fid))
1565
- continue;
1566
- const sf = String((d.subForm && d.subForm.id) ?? '').trim();
1567
- if (sf) {
1568
- if (layoutSubformIds.size && !layoutSubformIds.has(sf))
1569
- continue;
1570
- }
1571
- else if (compsFieldIds.size && !compsFieldIds.has(fid)) {
1572
- continue;
1573
- }
1574
- const disp = _detailDisplayText(d);
1575
- if (!disp)
1576
- continue;
1577
- const f = fieldsById.get(fid) || {};
1578
- const iv = { fieldId: fid, name: f.name ?? fid, value: disp };
1579
- if (sf)
1580
- iv.subFormId = sf;
1581
- initValues.push(iv);
1582
- }
1583
2278
  /**
1584
2279
  * 必填字段 → 历史可参考值(V12.1.77:取数点从 `createRequest` **前移到这里**)。
1585
2280
  *
@@ -1602,7 +2297,9 @@ async function buildCreateFormData(workflowId, env) {
1602
2297
  ...Object.values(subFormRequired).flatMap((list) => list.map((f) => String(f?.id ?? ''))),
1603
2298
  ].filter(Boolean))];
1604
2299
  const historyCache = fields.length
1605
- ? await buildHistoryRequestData(env, fields, historyRequiredFieldIds.map((id) => ({ id, name: String((fieldsById.get(id) || {}).name ?? id) })), { allFormFields: true, workflowId })
2300
+ ? await buildHistoryRequestData(env, fields, historyRequiredFieldIds.map((id) => ({ id, name: String((fieldsById.get(id) || {}).name ?? id) })), { allFormFields: true, workflowId },
2301
+ // 本命令开头就已并发发起的列表请求:这里只补「详情」那几步(P2)。
2302
+ historyListSnapshot)
1606
2303
  : null;
1607
2304
  const session = await getSessionContext(env);
1608
2305
  const context = {
@@ -1651,31 +2348,25 @@ async function buildCreateFormData(workflowId, env) {
1651
2348
  historyRequiredFieldIds,
1652
2349
  };
1653
2350
  const token = await env.continuation.issue({ operation: 'workflow.createForm', baseUrl: session.baseUrl, profile: session.profile, userId: session.userId, payload: context }, CONTINUATION_TTL_MS);
1654
- const requiredFieldIds = requiredFieldsWithSystem(requiredFieldsList, runtime, minorInfo.permissions || {});
1655
2351
  return {
1656
2352
  data: {
1657
2353
  status: 'FORM_READY',
1658
2354
  contextId: token,
1659
2355
  requestId: runtime.request_id,
1660
- // 显式点名语义:createForm 阶段并没有建单,这个 ID 是**预生成**的流程请求 ID,
1661
- // 只作为该单据的身份标识随 context 带到 createRequest;不写清楚模型会误当成"已存在的流程编号/草稿号"。
1662
- requestIdDesc: '预生成的流程请求 ID:本阶段尚未创建流程,它只是这次将要创建的单据的身份标识(用于后续 createRequest),不是已存在的流程编号或草稿号。',
1663
2356
  requestName: runtime.default_request_name || '',
1664
2357
  workflow: context.workflow,
1665
2358
  form: { id: frame.form_id, layout_id: layoutId },
1666
- requiredFields: requiredFieldIds,
1667
- subFormRequired: Object.fromEntries(Object.entries(subFormRequired).map(([k, v]) => [k, v.map((f) => String(f.id ?? ''))])),
1668
- // `editableFields` = 「非必填的可写字段」∪「`requiredFields`」——必填字段同样是可写字段,
1669
- // 模型按本清单拼 `data` 时不必再去 `requiredFields` 里二次查一遍(明细必填见 `subFormRequired`)。
1670
- editableFields: [...new Set([
1671
- ...editableFieldsList.map((f) => String(f.id ?? '')),
1672
- ...requiredFieldIds.map((id) => String(id ?? '')),
1673
- ].filter(Boolean))],
2359
+ // 字段清单(**唯一**一份,2026-10-14 用户裁定):可编辑字段 ∪ 必填且可见字段;有默认值的字段
2360
+ // 直接带 `initialValue`。此前并列的 `requiredFields` / `subFormRequired` / `editableFields` /
2361
+ // `initialValues` 四份清单与 `fillPlan` 一并取消——它们与 `fields` 同源、只是换个维度再切一刀,
2362
+ // 模型却要跨五份清单对照同一个字段。现在的口径只有两句:
2363
+ // 「哪些字段可以落值」= 本清单(能出现在这里就是能填的);
2364
+ // 「哪个必填还没值」= `createRequest` 的 `missingFields`(唯一权威,本命令不下发必填标记)。
1674
2365
  fields: slimFields,
1675
- initialValues: initValues,
1676
- // 本表单**必填字段**的历史参考值(近一年、非草稿、最近 5 条「我发起的」)——填值前先看它,
1677
- // 能直接照用的字段就别去问用户。完整结果(含非必填字段)已随句柄留给 `createRequest` 用。
1678
- historyRequestData: filterHistoryRequestData(historyCache, historyRequiredFieldIds),
2366
+ // 历史参考值(近一年、非草稿、最近 5 条「我发起的」)——填值前先看它,能直接照用的字段就别去
2367
+ // 问用户。字段范围与 `fields` **同一口径**(见 modelFieldIds 注释);完整结果(本表单全部字段)
2368
+ // 仍随句柄留给 `createRequest` 用(条件必填要到那边才现身)。
2369
+ historyRequestData: filterHistoryRequestData(historyCache, modelFieldIds),
1679
2370
  systemFields: {
1680
2371
  permissions: minorInfo.permissions || {},
1681
2372
  defaults: context.systemFields.defaults,
@@ -1912,6 +2603,23 @@ function splitValueAliases(raw) {
1912
2603
  }
1913
2604
  return out;
1914
2605
  }
2606
+ /**
2607
+ * 一个格子要送去查候选的「说法」清单——**按字段是单选还是多选分叉**:
2608
+ *
2609
+ * - **单选**:整串与各说法一并登记,由 `resolveBrowserAliases` 的「整串优先」先试整串
2610
+ * (名称本身自带逗号时不会被拆错);
2611
+ * - **多选**:逗号是**值分隔符**,只拆不拼——整串既不做「整串优先」,也**不进候选池**。
2612
+ * 否则「张三,李四」会被整串当成一个名称,恰好命中就静默吞成一个值(真机问题:
2613
+ * 多选浏览字段只写进去第一个对象),而且还会白花一次 complete / nameToId 往返。
2614
+ *
2615
+ * 多选里若单个对象名真的自带逗号,由 `名称(ID)` 显式指定(`resolveBrowserAliases` 认它)。
2616
+ */
2617
+ function lookupTermsOf(field, raw) {
2618
+ const aliases = splitValueAliases(raw);
2619
+ if (field && field.isMultiple)
2620
+ return aliases.length ? aliases : [raw];
2621
+ return aliases.length > 1 ? [raw, ...aliases] : [raw];
2622
+ }
1915
2623
  /** 浏览按钮字段解析不出来的统一出口。`candidates` **只含关键词真实命中**,绝不用首页枚举冒充。 */
1916
2624
  function browserAmbiguity(field, input, hits) {
1917
2625
  return new ValueAmbiguity({
@@ -1961,8 +2669,11 @@ function resolveBrowserAliases(field, raw, lookup) {
1961
2669
  return out;
1962
2670
  };
1963
2671
  const aliases = splitValueAliases(raw);
1964
- // 整串与各说法**并行**登记:整串命中(名称自带逗号)与切段命中进同一个池子判定
1965
- const terms = aliases.length > 1 ? [raw, ...aliases] : [raw];
2672
+ // 整串与各说法**并行**登记:整串命中(名称自带逗号)与切段命中进同一个池子判定。
2673
+ // 多选字段只登记切段结果(见 lookupTermsOf)。
2674
+ const terms = lookupTermsOf(field, raw);
2675
+ /** 「整串优先」只对**单选**成立:多选里逗号是值分隔符,整串本身不是候选名称。 */
2676
+ const wholeTermFirst = !(field && field.isMultiple) && aliases.length > 1;
1966
2677
  const pool = [];
1967
2678
  /** 每个词各自「字面同名」的候选(显式 ID 也算字面命中,它本来就是精确值)。 */
1968
2679
  const literalByTerm = new Map();
@@ -1975,8 +2686,8 @@ function resolveBrowserAliases(field, raw, lookup) {
1975
2686
  literalByTerm.set(term, candidates.filter((c) => String(c?.name ?? '') === term));
1976
2687
  }
1977
2688
  const poolHits = dedupe(pool);
1978
- // ① **整串优先**:原串本身就是一个名称 ⇒ 用它、不拆(名称自带逗号也不会被拆错);真重名则澄清
1979
- if (aliases.length > 1) {
2689
+ // ① **整串优先**(仅单选):原串本身就是一个名称 ⇒ 用它、不拆(名称自带逗号也不会被拆错);真重名则澄清
2690
+ if (wholeTermFirst) {
1980
2691
  const wholeHits = dedupe(literalByTerm.get(raw) || []);
1981
2692
  if (wholeHits.length === 1)
1982
2693
  return wholeHits[0];
@@ -2000,6 +2711,119 @@ function normalizeRelateCandidate(value) {
2000
2711
  const name = String(value.name ?? '').trim();
2001
2712
  return id && name ? { id, name } : null;
2002
2713
  }
2714
+ /** 需要按 `format` 归一化取值的日期 / 时间类控件(componentKey 已小写)。 */
2715
+ const DATE_COMPONENTS = new Set(['datecomponent', 'datetimecomponent', 'timecomponent', 'dateinterval']);
2716
+ /**
2717
+ * 从模型给的文本里解析日期 / 时间分量。认得下面这些写法(全角数字先转半角):
2718
+ * `2026-08-21` / `2026/8/21` / `2026.8.21` / `2026年8月21日` / `20260821`、
2719
+ * `09:00` / `9:5` / `9时30分` / `2026-08-21 09:00:30` / `20260821090030`、单挂一个裸数字(按小时)。
2720
+ * 解析不出任何分量时返回 `null`(调用方原样透传,不猜)。
2721
+ */
2722
+ function parseDateParts(raw) {
2723
+ const text = String(raw ?? '')
2724
+ .replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
2725
+ .trim();
2726
+ if (!text)
2727
+ return null;
2728
+ const parts = {};
2729
+ // ① 紧凑写法:20260821 / 20260821093000
2730
+ const compact = /^(\d{4})(\d{2})(\d{2})(?:[T\s]?(\d{2})(\d{2})(\d{2})?)?$/.exec(text);
2731
+ if (compact) {
2732
+ parts.y = Number(compact[1]);
2733
+ parts.m = Number(compact[2]);
2734
+ parts.d = Number(compact[3]);
2735
+ if (compact[4] !== undefined) {
2736
+ parts.hh = Number(compact[4]);
2737
+ parts.mm = Number(compact[5]);
2738
+ }
2739
+ if (compact[6] !== undefined)
2740
+ parts.ss = Number(compact[6]);
2741
+ return parts;
2742
+ }
2743
+ // ② 日期段:年月日之间允许 - / . 或「年」「月」,日期后允许「日」「号」
2744
+ let rest = text;
2745
+ const dateMatch = /(\d{4})\s*[-/.年]\s*(\d{1,2})\s*[-/.月]\s*(\d{1,2})\s*[日号]?/.exec(rest);
2746
+ if (dateMatch) {
2747
+ parts.y = Number(dateMatch[1]);
2748
+ parts.m = Number(dateMatch[2]);
2749
+ parts.d = Number(dateMatch[3]);
2750
+ rest = rest.slice(0, dateMatch.index) + rest.slice(dateMatch.index + dateMatch[0].length);
2751
+ }
2752
+ // ③ 时间段:时[:时分][:秒][.毫秒],分隔符认 : : 时 点 ,秒可带「分」「秒」
2753
+ const timeMatch = /(\d{1,2})\s*[::时点]\s*(\d{1,2})(?:\s*[::分]\s*(\d{1,2}))?(?:\s*[..]\s*(\d{1,3}))?/.exec(rest);
2754
+ if (timeMatch) {
2755
+ parts.hh = Number(timeMatch[1]);
2756
+ parts.mm = Number(timeMatch[2]);
2757
+ if (timeMatch[3] !== undefined)
2758
+ parts.ss = Number(timeMatch[3]);
2759
+ if (timeMatch[4] !== undefined)
2760
+ parts.ms = Number(timeMatch[4]);
2761
+ }
2762
+ if (parts.y === undefined && parts.hh === undefined) {
2763
+ // 兜底:整串只剩一个裸数字 ⇒ 当小时用(`format: HH` 场景,如 "9")
2764
+ const bare = /^(\d{1,2})$/.exec(rest.trim());
2765
+ if (!bare)
2766
+ return null;
2767
+ parts.hh = Number(bare[1]);
2768
+ }
2769
+ return parts;
2770
+ }
2771
+ /**
2772
+ * 按字段 `format` 归一化日期 / 时间文本(V12.1.115,2026-10-14 用户裁定)。
2773
+ *
2774
+ * 目标:**模型不必关心 `format`**。`2026-8-21`、`2026/08/21`、`2026年8月21日 9:5`、
2775
+ * `2026-08-21 09:00:00` 都会落到 `format` 要求的形状——
2776
+ * - `format` 里有的分量由解析结果补零后填(`yyyy-MM-dd` ⇒ `2026-08-21`);
2777
+ * - 时间分量没解析出来时补 `00`(`yyyy-MM-dd HH:mm` 收到纯日期 ⇒ `2026-08-21 00:00`),
2778
+ * 这也是此前 `DATETIME_FORMAT` 报错那条路径的替代:不再要求模型补时间;
2779
+ * - `format` 里没有的分量直接丢掉(`yyyy-MM-dd` 收到带时分秒的写法 ⇒ 只留日期)。
2780
+ *
2781
+ * 两条**不猜**的边界(宁可原样透传给服务端报错,也不编造用户没给的信息):
2782
+ * ① 解析不出任何分量(不是日期/时间文本);② `format` 要日期、但文本里连年月日都没凑齐。
2783
+ */
2784
+ function normalizeDateValue(raw, format) {
2785
+ const text = String(raw ?? '').trim();
2786
+ if (!text || !format)
2787
+ return raw;
2788
+ // 日期区间(`2026-08-01~2026-08-31` / 「…至…」):两半各自归一化后按原分隔符接回。
2789
+ for (const sep of ['~', '至']) {
2790
+ if (text.includes(sep)) {
2791
+ const halves = text.split(sep);
2792
+ if (halves.length === 2)
2793
+ return `${normalizeDateValue(halves[0], format)}${sep}${normalizeDateValue(halves[1], format)}`;
2794
+ }
2795
+ }
2796
+ const parts = parseDateParts(text);
2797
+ if (!parts)
2798
+ return text;
2799
+ // `format` 要哪个日期分量,文本里就得有哪个——缺了不猜(补 `01` 等于编造用户没给的日期)。
2800
+ if ((/y{2,4}/.test(format) && parts.y === undefined)
2801
+ || (/M/.test(format) && parts.m === undefined)
2802
+ || (/d/.test(format) && parts.d === undefined)) {
2803
+ return text;
2804
+ }
2805
+ const pad = (n, len = 2) => String(n).padStart(len, '0');
2806
+ return format.replace(/yyyy|yy|MM|dd|HH|hh|mm|ss|fff|M|d|H|h|m|s|f/g, (token) => {
2807
+ switch (token) {
2808
+ case 'yyyy': return pad(parts.y ?? 0, 4);
2809
+ case 'yy': return pad((parts.y ?? 0) % 100);
2810
+ case 'MM': return pad(parts.m ?? 0);
2811
+ case 'M': return String(parts.m ?? 0);
2812
+ case 'dd': return pad(parts.d ?? 0);
2813
+ case 'd': return String(parts.d ?? 0);
2814
+ case 'HH':
2815
+ case 'hh': return pad(parts.hh ?? 0);
2816
+ case 'H':
2817
+ case 'h': return String(parts.hh ?? 0);
2818
+ case 'mm': return pad(parts.mm ?? 0);
2819
+ case 'm': return String(parts.mm ?? 0);
2820
+ case 'ss': return pad(parts.ss ?? 0);
2821
+ case 's': return String(parts.ss ?? 0);
2822
+ case 'fff': return pad(parts.ms ?? 0, 3);
2823
+ default: return String(parts.ms ?? 0); // 'f'
2824
+ }
2825
+ });
2826
+ }
2003
2827
  function serializeValue(field, value, mappings, row, browserHints = {}) {
2004
2828
  const detail = { ...(row || {}), formField: { id: field.id } };
2005
2829
  const isEmployeeScope = field.isEmployeeScope || String(field.componentKey ?? '').toLowerCase() === 'employeescope';
@@ -2106,6 +2930,18 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
2106
2930
  const key = `${field.id}_${rowIndex}_${term}`;
2107
2931
  return { mapped: mappings[key] || [], completed: browserHints[key] || [] };
2108
2932
  };
2933
+ if (field.isMultiple) {
2934
+ // 多选:一个格子里的逗号是**值分隔符**,逐说法各解析出一条 `dataOptions`
2935
+ // (整串不当名称,见 lookupTermsOf)。某个说法有歧义时整条值都不落地,
2936
+ // 由该说法自己的候选澄清——已解析出的兄弟值**不丢**,因为澄清后会整条重发。
2937
+ for (const term of lookupTermsOf(field, raw)) {
2938
+ const pickedOne = resolveBrowserAliases(field, term, lookup);
2939
+ if (options.some((o) => o.optionId === pickedOne.id))
2940
+ continue;
2941
+ options.push({ optionId: pickedOne.id, content: pickedOne.name });
2942
+ }
2943
+ continue;
2944
+ }
2109
2945
  const picked = resolveBrowserAliases(field, raw, lookup);
2110
2946
  options.push({ optionId: picked.id, content: picked.name });
2111
2947
  }
@@ -2122,7 +2958,16 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
2122
2958
  continue;
2123
2959
  }
2124
2960
  const keyword = String(item).trim();
2125
- const matches = choices.filter((c) => String(c.id) === keyword || String(c.name ?? '').toLowerCase() === keyword.toLowerCase());
2961
+ // 选项值允许「名称(ID)」形态:模型会把澄清候选 / 历史参考值里的 `名称(ID)` 原样回传,
2962
+ // 而契约(`workflow-create.md`「字段值格式」)只要求传 name ⇒ 必须容错,不能报 NOT_FOUND。
2963
+ // **ID 取最后一个括号内的内容**:选项名自身可能带括号——真机如 `银行(个人报销)(7)`,
2964
+ // 取第一个括号会得到 `个人报销` 这种非数字片段。口径复用 `parseNameWithId`(同一份正则,
2965
+ // 避免"查询侧认、发起侧不认"的口径分叉),显式 ID 命中即直取,否则回落到名称精确比对。
2966
+ const parsed = parseNameWithId(keyword);
2967
+ const explicit = parsed.id ? choices.filter((c) => String(c.id) === parsed.id) : [];
2968
+ const matches = explicit.length
2969
+ ? explicit
2970
+ : choices.filter((c) => String(c.id) === keyword || String(c.name ?? '').toLowerCase() === parsed.name.toLowerCase());
2126
2971
  if (matches.length !== 1) {
2127
2972
  throw new ValueAmbiguity({
2128
2973
  kind: 'OPTION',
@@ -2146,12 +2991,13 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
2146
2991
  if (value && typeof value === 'object') {
2147
2992
  throw new ValueAmbiguity({ kind: 'VALUE_FORMAT', fieldId: field.id, fieldName: field.name, reason: '文本字段只接受字符串、数字或布尔值', candidates: [] });
2148
2993
  }
2149
- const content = value === null || value === undefined ? '' : String(value);
2150
- const dateFormat = String(field.format ?? '');
2151
- const needsTime = dateFormat && /(HH|hh)[::.]?mm|H:mm|h:mm/i.test(dateFormat);
2152
- if (needsTime && /^\d{4}-\d{1,2}-\d{1,2}$/.test(content)) {
2153
- throw new ValueAmbiguity({ kind: 'DATETIME_FORMAT', fieldId: field.id, fieldName: field.name, input: content, reason: `该字段是日期+时间控件(format=${dateFormat}),只传日期不完整;请按 format 传完整值(如 2026-08-21 09:00)后重新 prepare,不要省略时间。`, candidates: [] });
2154
- }
2994
+ const rawContent = value === null || value === undefined ? '' : String(value);
2995
+ // 日期 / 时间控件:模型给的写法一律在 CLI 内按字段 `format` 归一化(2026-10-14 用户裁定)。
2996
+ // 此前这里在"日期+时间控件只收到纯日期"时抛 `DATETIME_FORMAT` 让模型补时间——把格式这件事
2997
+ // 外包给了模型,实测它会为 `100` 还是 `100.00`、`09:00` 还是 `9:00` 反复纠结。
2998
+ const content = DATE_COMPONENTS.has(component)
2999
+ ? normalizeDateValue(rawContent, String(field.format ?? ''))
3000
+ : rawContent;
2155
3001
  const maxLength = field.maxLength;
2156
3002
  const richUnlimited = component === 'textarea' && Boolean(field.richEditor);
2157
3003
  if (!richUnlimited && maxLength !== undefined && maxLength !== null && String(maxLength).match(/^\d+$/) && content.length > Number(maxLength)) {
@@ -2314,8 +3160,8 @@ async function resolveEntryValues(env, entries, formId) {
2314
3160
  continue;
2315
3161
  // 一个格子可写多个「说法」(逗号分隔):**整串与各说法一起登记**——这样"整串优先"要用的
2316
3162
  // 整串候选、以及各说法各自的候选,都在同一批里拿到(nameToId 一把批量发,未命中的再并行补)。
2317
- const aliases = splitValueAliases(raw);
2318
- const terms = aliases.length > 1 ? [raw, ...aliases] : [raw];
3163
+ // 多选字段只登记切段结果(逗号是值分隔符,见 lookupTermsOf)。
3164
+ const terms = lookupTermsOf(field, raw);
2319
3165
  // 候选来源二选一:控件自带 `browserModule`+`browserType` ⇒ 只走 complete(下一步补查),
2320
3166
  // **不进 nameToId 批次**;否则只走 nameToId。这样池子里永远只有一个来源,不会互相污染。
2321
3167
  const hasBrowserChannel = Boolean(field.browserModule && field.browserType);
@@ -2599,14 +3445,155 @@ function _detailValueIds(detail) {
2599
3445
  if (t)
2600
3446
  ids.push(t);
2601
3447
  }
2602
- for (const option of detail.dataOptions || []) {
2603
- if (option && typeof option === 'object' && option.optionId !== undefined && option.optionId !== null) {
2604
- const t = String(option.optionId).trim();
2605
- if (t)
2606
- ids.push(t);
2607
- }
3448
+ for (const option of detail.dataOptions || []) {
3449
+ if (option && typeof option === 'object' && option.optionId !== undefined && option.optionId !== null) {
3450
+ const t = String(option.optionId).trim();
3451
+ if (t)
3452
+ ids.push(t);
3453
+ }
3454
+ }
3455
+ return ids;
3456
+ }
3457
+ /**
3458
+ * 把一个值拆成「可比 token」集合:`id` / `optionId` / `name` / `content`,以及去掉尾部
3459
+ * `(id)` / `(id)` 后缀的纯文本。
3460
+ *
3461
+ * 为什么要拆:入参是**原始串**(`"银行(个人报销)(7)"`、`"20260999"`),基线是 **detail**
3462
+ * (`dataOptions:[{optionId:'7',content:'银行(个人报销)'}]`、`{content:'20260999'}`),
3463
+ * 直接比 `_entryValueIds` 与 `_detailValueIds` 会得到 `["银行(个人报销)(7)"]` vs
3464
+ * `["银行(个人报销)","7"]` 这种必不相等的形状。取交集判等才不会误判成"变了"。
3465
+ */
3466
+ function _pushComparableToken(tokens, raw) {
3467
+ const text = String(raw ?? '').trim();
3468
+ if (!text)
3469
+ return;
3470
+ tokens.add(text);
3471
+ const matched = text.match(/^(.*?)\s*[((]\s*([^()()]*?)\s*[))]$/);
3472
+ if (matched) {
3473
+ // 尾部括号里是 id 时(`银行(个人报销)(7)`、`泛微上海(100504500000311504)`)也要能对上
3474
+ if (matched[1].trim())
3475
+ tokens.add(matched[1].trim());
3476
+ if (matched[2].trim())
3477
+ tokens.add(matched[2].trim());
3478
+ }
3479
+ }
3480
+ function _entryComparableTokens(source) {
3481
+ const tokens = new Set();
3482
+ for (const item of listValues(source)) {
3483
+ if (item && typeof item === 'object') {
3484
+ for (const key of ['id', 'optionId', 'value', 'name', 'content']) {
3485
+ if (item[key] !== undefined && item[key] !== null)
3486
+ _pushComparableToken(tokens, item[key]);
3487
+ }
3488
+ }
3489
+ else {
3490
+ _pushComparableToken(tokens, item);
3491
+ }
3492
+ }
3493
+ return tokens;
3494
+ }
3495
+ /** 基线 detail 的可比 token(与 `_entryComparableTokens` 同一套口径)。 */
3496
+ function _baselineComparableTokens(detail) {
3497
+ const tokens = new Set();
3498
+ if (detail && typeof detail === 'object') {
3499
+ if (detail.content !== undefined && detail.content !== null)
3500
+ _pushComparableToken(tokens, detail.content);
3501
+ if (detail.dataText && typeof detail.dataText === 'object' && detail.dataText.content !== undefined) {
3502
+ _pushComparableToken(tokens, detail.dataText.content);
3503
+ }
3504
+ for (const option of detail.dataOptions || []) {
3505
+ if (!option || typeof option !== 'object')
3506
+ continue;
3507
+ if (option.optionId !== undefined && option.optionId !== null)
3508
+ _pushComparableToken(tokens, option.optionId);
3509
+ if (option.content !== undefined && option.content !== null)
3510
+ _pushComparableToken(tokens, option.content);
3511
+ if (option.name !== undefined && option.name !== null)
3512
+ _pushComparableToken(tokens, option.name);
3513
+ }
3514
+ }
3515
+ return tokens;
3516
+ }
3517
+ /**
3518
+ * 入参这条值是否**与基线相同**(即"用户没动过")。
3519
+ *
3520
+ * 判松不判严(任一 token 命中即算相同):漏判"变了"只会少触发一次联动(页面语义本来就是
3521
+ * 没变不触发);误判"没变"则会把用户真实修改的字段跳过,风险更大,所以这里宁可宽。
3522
+ * 基线里没有该字段 / 基线没值时一律视为"变了"(首次填写必须触发)。
3523
+ */
3524
+ function _sameAsBaseline(entry, detail) {
3525
+ const base = _baselineComparableTokens(detail);
3526
+ if (!base.size)
3527
+ return false;
3528
+ const incoming = _entryComparableTokens(entry.value);
3529
+ if (!incoming.size)
3530
+ return false;
3531
+ for (const token of incoming) {
3532
+ if (base.has(token))
3533
+ return true;
3534
+ }
3535
+ return false;
3536
+ }
3537
+ /**
3538
+ * 「值真的变了的字段 id」——`changeField` 类联动的触发依据(2026-10-20 用户裁定)。
3539
+ *
3540
+ * 旧口径把**入参里出现的字段**一律当成变更字段(模拟页面 onchange),于是"把表单默认值原样
3541
+ * 回传"会伪造出一次"用户修改":真机坑是 `合同号=20260999`(= 该字段 `initialValue`)被
3542
+ * `createForm.data` 带出来、又被整份回传给 `createRequest`,触发合同号的 changeField 联动,
3543
+ * 服务端按合同重算 `相关客户` 等 7 个字段把 `相关客户` 置空 ⇒ 必填缺失。
3544
+ *
3545
+ * 现在的口径:**只有值与基线不同才算变更**。
3546
+ * - 发起流程:基线 = 表单默认值(`context.defaults`)
3547
+ * - 编辑流程:基线 = 编辑前的字段值(`context.currentDetails`,来自 edit-load 的 `dataDetails`)
3548
+ *
3549
+ * 明细行按 `(subFormId, dataIndex)` 定位基线;明细默认值常常没有行号,此时回落到"同字段任一基线"
3550
+ * (见 `byField`)——宁可判"没变",也不要让无行号的默认值把每一行都判成变更。
3551
+ */
3552
+ function _changedFieldIds(entries, baseline) {
3553
+ const byRow = new Map();
3554
+ const byField = new Map();
3555
+ const pushTo = (map, key, item) => {
3556
+ const bucket = map.get(key);
3557
+ if (bucket)
3558
+ bucket.push(item);
3559
+ else
3560
+ map.set(key, [item]);
3561
+ };
3562
+ for (const detail of baseline || []) {
3563
+ if (!detail || typeof detail !== 'object')
3564
+ continue;
3565
+ const fieldId = String((detail.formField && detail.formField.id) ?? '').trim();
3566
+ if (!fieldId)
3567
+ continue;
3568
+ const subFormId = String((detail.subForm && detail.subForm.id) ?? '').trim();
3569
+ const hasRow = detail.dataIndex !== undefined && detail.dataIndex !== null;
3570
+ const dataIndex = hasRow ? String(detail.dataIndex) : '';
3571
+ pushTo(byField, fieldId, detail);
3572
+ if (hasRow)
3573
+ pushTo(byRow, `${subFormId}\u0000${fieldId}\u0000${dataIndex}`, detail);
3574
+ }
3575
+ const changed = [];
3576
+ const seen = new Set();
3577
+ for (const entry of entries) {
3578
+ if (!entry.field || typeof entry.field !== 'object')
3579
+ continue;
3580
+ const fieldId = String(entry.field.id ?? '').trim();
3581
+ if (!fieldId || seen.has(fieldId))
3582
+ continue;
3583
+ const row = (entry.row && typeof entry.row === 'object') ? entry.row : null;
3584
+ const subFormId = row ? String((row.subForm && row.subForm.id) ?? '').trim() : String(entry.field.subFormId ?? '').trim();
3585
+ const hasRow = Boolean(row) && row.dataIndex !== undefined && row.dataIndex !== null;
3586
+ const candidates = hasRow
3587
+ ? (byRow.get(`${subFormId}\u0000${fieldId}\u0000${String(row.dataIndex)}`) || [])
3588
+ : (byField.get(fieldId) || []);
3589
+ const fallback = hasRow ? (byField.get(fieldId) || []) : [];
3590
+ const unchanged = [...candidates, ...fallback].some((detail) => _sameAsBaseline(entry, detail));
3591
+ if (unchanged)
3592
+ continue;
3593
+ seen.add(fieldId);
3594
+ changed.push(fieldId);
2608
3595
  }
2609
- return ids;
3596
+ return changed;
2610
3597
  }
2611
3598
  function _linkageOptions(sourceContent) {
2612
3599
  if (!sourceContent || typeof sourceContent !== 'string' || !sourceContent.trim())
@@ -2732,9 +3719,10 @@ function _mainValueMap(details) {
2732
3719
  }
2733
3720
  return result;
2734
3721
  }
2735
- async function runFieldLinkage(env, ctx, entries, actionTypes, allDetails, isEdit = false) {
2736
- const changedIds = entries.filter((e) => e.field && typeof e.field === 'object' && String(e.field.id ?? '').trim())
2737
- .map((e) => String(e.field.id));
3722
+ async function runFieldLinkage(env, ctx, entries, actionTypes, allDetails, isEdit = false, baseline) {
3723
+ // 只有「值与基线(发起=表单默认值 / 编辑=编辑前值)不同」的字段才算变更字段,
3724
+ // 见 `_changedFieldIds` 的注释(2026-10-20 用户裁定:值没变就不该触发 changeField 联动)。
3725
+ const changedIds = _changedFieldIds(entries, baseline);
2738
3726
  const runDataList = _buildLinkageRunData(ctx.linkageActions, changedIds, actionTypes);
2739
3727
  const neededIds = _neededLinkageFieldIds(ctx.linkageActions, runDataList);
2740
3728
  const dataList = [];
@@ -2865,6 +3853,16 @@ function mergeLinkageDetails(merged, linkageDetails) {
2865
3853
  }
2866
3854
  return result;
2867
3855
  }
3856
+ /**
3857
+ * `computeLinkageVisibility` 的空结果(四个集合都是空)。
3858
+ *
3859
+ * 用途:小e 档的 `createRequest` **不跑显示属性联动**(2026-10-16 用户裁定,见
3860
+ * `commandCreateRequest`),此时直接取这份空结果顶替,避免调用点写四个字面量再补类型标注。
3861
+ * 语义与 `computeLinkageVisibility` 的早退分支(`linkages` 不是数组时)完全一致。
3862
+ */
3863
+ function emptyLinkageVisibility() {
3864
+ return [new Set(), {}, {}, {}];
3865
+ }
2868
3866
  function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues, fields) {
2869
3867
  const hiddenSubforms = new Set();
2870
3868
  const rowHidden = {};
@@ -3256,6 +4254,290 @@ function previewValueText(value) {
3256
4254
  }
3257
4255
  return value;
3258
4256
  }
4257
+ /** markdown 表格单元格:不能出现裸 `|` 与换行,否则列会错位(与 invoice 渲染档同口径)。 */
4258
+ function markdownCell(value) {
4259
+ const raw = String(value ?? '').trim();
4260
+ if (!raw)
4261
+ return '—';
4262
+ return raw.replace(/\|/gu, '\\|').replace(/\r?\n/gu, ' ');
4263
+ }
4264
+ /**
4265
+ * 缺失必填字段的**历史推荐值**(2026-10-13 用户口径)。
4266
+ *
4267
+ * 口径(用户原话:"该字段历史数据填写值都一样,或者最近三条历史流程该字段值都一致"):
4268
+ * - 只看该字段**有值**的历史记录,顺序即 `historyCache` 的**发起时间倒序**;
4269
+ * - 有值记录取值**全部一致** ⇒ 推荐;
4270
+ * - 否则取**最近 3 条**有值记录,三者一致 ⇒ 推荐;
4271
+ * - 其余一律**不给推荐**(宁缺勿错——让用户自己填,不猜)。
4272
+ *
4273
+ * 数据源是 `createForm` 随句柄留下的**宽缓存**(本表单全部字段),所以**联动命中后才变必填**的字段
4274
+ * (真机样本里的「相关流程」)也能命中;句柄来自旧版本 / 缓存缺失时静默返回 `null`,不拖累主链路。
4275
+ *
4276
+ * **一致性判断完全在命令侧算完**:模型既不拿原始历史、也不做任何推断,只负责原样粘贴。
4277
+ */
4278
+ function historyRecommendation(records, fieldId) {
4279
+ const id = String(fieldId ?? '').trim();
4280
+ if (!id)
4281
+ return null;
4282
+ const values = [];
4283
+ for (const record of toArray(records)) {
4284
+ // `fieldValues` 是 `{字段id: 值}` 映射(2026-10-15 起;旧形态是数组)。
4285
+ const fieldValues = record?.fieldValues;
4286
+ if (!fieldValues || typeof fieldValues !== 'object' || Array.isArray(fieldValues))
4287
+ continue;
4288
+ if (!Object.prototype.hasOwnProperty.call(fieldValues, id))
4289
+ continue;
4290
+ // 附件类字段的值是**结构化对象**(见 `formatHistoryFieldValue`):`String()` 会把它压成
4291
+ // `[object Object]`,于是 N 条互不相同的附件"看起来完全一致"⇒ 凭空给出一条假推荐。
4292
+ // 非字符串一律不参与一致性判定(宁缺勿错)。
4293
+ const raw = fieldValues[id];
4294
+ if (typeof raw !== 'string')
4295
+ continue;
4296
+ const text = raw.trim();
4297
+ if (text)
4298
+ values.push(text);
4299
+ }
4300
+ if (!values.length)
4301
+ return null;
4302
+ if (values.every((value) => value === values[0])) {
4303
+ return { value: values[0], note: `近 ${values.length} 条历史流程该字段取值一致` };
4304
+ }
4305
+ const latest = values.slice(0, 3);
4306
+ if (latest.length === 3 && latest.every((value) => value === latest[0])) {
4307
+ return { value: latest[0], note: '最近 3 条历史流程该字段取值一致' };
4308
+ }
4309
+ return null;
4310
+ }
4311
+ /** 选项清单里反查展示名(`紧急程度` / `密级` 的代码→中文名)。查不到返回空串。 */
4312
+ function optionDisplayName(options, value, idKeys) {
4313
+ for (const item of toArray(options)) {
4314
+ if (!item || typeof item !== 'object')
4315
+ continue;
4316
+ for (const key of idKeys) {
4317
+ if (String(item[key] ?? '').trim() !== value)
4318
+ continue;
4319
+ return String(item.name ?? item.content ?? '').trim();
4320
+ }
4321
+ }
4322
+ return '';
4323
+ }
4324
+ /**
4325
+ * 系统字段(流程标题 / 紧急程度 / 密级)的「已填内容」行(2026-10-14 用户口径)。
4326
+ *
4327
+ * 流程标题**只要该次发起有值就列**(与上一版单独那行 `- **流程标题**:…` 同口径,只是换个位置)。
4328
+ *
4329
+ * 紧急程度 / 密级是本次新增,两条门槛**缺一不可**,否则一个字都不给(宁缺勿错):
4330
+ * ① 服务端明确说该字段 `isview` —— `permissions` 里没有这一项时按"用户在表单上看不到"处理,
4331
+ * 不给没有依据的行;
4332
+ * ② 该值能在 `level_options` / `sec_level_options` 里反查出中文名 —— 这两个控件在表单上是下拉框,
4333
+ * 命令拿到的是**代码**(如密级 `1`);直接印代码用户看不懂,宁可省略这一行。
4334
+ */
4335
+ function systemFilledRows(params) {
4336
+ const rows = [];
4337
+ const title = String(params.title ?? '').trim();
4338
+ if (title)
4339
+ rows.push({ name: SYSTEM_FIELD_KEYS.REQUEST_NAMEL, value: title });
4340
+ const push = (key, value, options, idKeys) => {
4341
+ const perm = params.permissions[key] || {};
4342
+ if (!perm.isview)
4343
+ return;
4344
+ const raw = String(value ?? '').trim();
4345
+ if (!raw)
4346
+ return;
4347
+ const name = optionDisplayName(options, raw, idKeys);
4348
+ if (!name)
4349
+ return;
4350
+ rows.push({ name: SYSTEM_FIELD_KEYS[key], value: name });
4351
+ };
4352
+ push('REQUEST_LEVEL', params.requestLevel, params.levelOptions, ['code', 'name']);
4353
+ push('SECLEVEL', params.secLevel, params.secLevelOptions, ['id', 'name']);
4354
+ return rows;
4355
+ }
4356
+ /**
4357
+ * 「已填内容」**主表行**的唯一来源(2026-10-14 用户口径):**用户在表单上能看到的、有值的、
4358
+ * 可编辑的主表字段**,按**表单布局顺序**列出。
4359
+ *
4360
+ * 为什么不再只列"模型这次传了什么":真的按 `resolvedValues`(= 仅本次入参)列时,用户看到的截图里
4361
+ * 「已填内容」只剩下模型显式传的那两项 —— 而这张单在表单上其实是**已填状态**的:表单默认值、
4362
+ * 系统字段一个都没列出来,用户根本无法据此确认"这张单到底填成什么样了"。
4363
+ *
4364
+ * 数据源是 `merged2` = 表单默认值 + 本次入参 + 联动带出的值,**就是将来真正落库的那一份**,
4365
+ * 所以列出来的每个值都是"用户会在表单上看到的值"。
4366
+ *
4367
+ * 不列的五类(都按"用户在表单上看到的"口径筛):
4368
+ * - 明细字段(另由明细表承载)、没值的字段(那是「还需要你提供」的活);
4369
+ * - 只读 / 禁止编辑的字段(用户改不了,不算"可编辑字段");
4370
+ * - 联动隐藏、联动禁用、布局隐藏的字段;
4371
+ * - **布局隐藏行上的字段**(`Sheet1.rows[].visible === false`,见 `collectHiddenRowFieldIds`);
4372
+ * - **不在这张表单组件清单里的字段**(`compsFieldIds` 是**白名单**:它列的是表单上真实摆着的
4373
+ * 组件。E10 的老表单会在定义里留着"已废弃 / 换了控件"的历史字段,服务端照样下发它们的默认值,
4374
+ * 但这些字段用户在表单上根本看不到 ⇒ 不进「已填内容」。这份名单与 `initialValues` 用的是同一份
4375
+ * ——`buildCreateFormData` 里的 `compsFieldIds.size && !compsFieldIds.has(fid)`,两边必须同口径,
4376
+ * 否则模型看到的 `initialValues` 与命令渲染的「已填内容」会互相打架)。
4377
+ *
4378
+ * ⚠️ 2026-10-14 修正:本函数初版把这处写成 `compsFieldIds.has(fieldId) ⇒ 跳过`(白名单当黑名单用),
4379
+ * 真机上结果**正好反过来**——列出来的全是用户看不到的老/隐藏字段(`币种(主表)2`、
4380
+ * `账号企业个人类型(废弃)`、`创建人直接上级` …),真正填了的 `报销事由` 一个都没列。
4381
+ * 单测没兜住是因为用例的 `compsFieldIds` 恒为 `[]`(空名单不筛),缺陷恰好落在空集上。
4382
+ * **名单为空时(老句柄 / 布局解析失败)不筛**,与 `initialValues` 的降级口径一致。
4383
+ *
4384
+ * ⚠️ 2026-10-14 二次修正(用户口径):`流程类型` 曾按**字段名**硬排除(`MAIN_FILLED_EXCLUDED_NAMES`),
4385
+ * 那是因为当时只看到"它与普通业务 Select 逐键同形、载荷里无标记"。真实原因是**它落在隐藏行上**
4386
+ * ——用户明确指出:这两个字段在表单上确实是显示的组件,但**所在行隐藏**,所以前台看不到。
4387
+ * 按名排除已撤除,改由 `hiddenCellFieldIds`(含行隐藏)统一承载——换一张表单、换一个字段名同样成立。
4388
+ */
4389
+ function mainFilledRows(fields, values, hiddenIds, disabledIds, compsFieldIds) {
4390
+ const rows = [];
4391
+ for (const field of toArray(fields)) {
4392
+ if (!field || typeof field !== 'object')
4393
+ continue;
4394
+ const fieldId = String(field.id ?? '').trim();
4395
+ if (!fieldId)
4396
+ continue;
4397
+ if (String(field.subFormId ?? '').trim())
4398
+ continue;
4399
+ if (field.readOnly || field.disableEdit)
4400
+ continue;
4401
+ if (hiddenIds.has(fieldId) || disabledIds.has(fieldId))
4402
+ continue;
4403
+ if (compsFieldIds.size && !compsFieldIds.has(fieldId))
4404
+ continue;
4405
+ const detail = values.get(fieldId);
4406
+ if (!detail)
4407
+ continue;
4408
+ // 值必须走 `_detailDisplayText`(与 `initialValues` 的展示口径同源):浏览/选项类在载荷里是
4409
+ // `dataOptions`、富文本是 `dataText`,直接 `String()` 会印出 `[object Object]`。
4410
+ const value = _detailDisplayText(detail);
4411
+ if (!value)
4412
+ continue;
4413
+ rows.push({ name: String(field.name ?? fieldId), value });
4414
+ }
4415
+ return rows;
4416
+ }
4417
+ /**
4418
+ * `createRequest` **已落地结果**的渲染档(与查询侧 `finalMarkdown` 同口径):调用方
4419
+ * **原样粘贴**即可——不必自己挑字段、拼表格,也不必判断该展示什么。
4420
+ *
4421
+ * 只在流程**已经落地**时渲染(`CREATED` / `CREATED_SUBMITTED` / `CREATED_SUBMIT_FAILED`)。
4422
+ * `NEEDS_INPUT`(必填还没填齐)由 `renderCreatePendingMarkdown` 单独渲染——**刻意与本文本同一套
4423
+ * 表格口径**,只换结论句与末段(那边要说清"还没发起 + 还差哪几项");`NEEDS_CONFIRMATION` 不给
4424
+ * 文本(要先解决 `ambiguities` 再重发)。
4425
+ *
4426
+ * 内容分四段:结论(`message`,命令已写好的中文;**只用来渲染,渲染后这个键不再返回**)→ 编号/链接
4427
+ * → 主表「已填内容」两列表
4428
+ * (系统字段如流程标题在前,业务字段按表单布局顺序随后;行由 `mainFilledRows` + `systemFilledRows`
4429
+ * 算好传进来,命令侧只有这一处口径)→ 各明细表(Excel 列形态)→ 提交失败原因 / 草稿缺项提醒(有才出现)。
4430
+ */
4431
+ function renderCreateResultMarkdown(result, mainRows) {
4432
+ const lines = [];
4433
+ const message = String(result.message ?? '').trim();
4434
+ if (message)
4435
+ lines.push(message, '');
4436
+ // 流程标题已并入「已填内容」第一行(2026-10-14 用户口径):同一个值不在一份文本里出现两次。
4437
+ const header = [
4438
+ ['流程编号', result.requestMark || result.requestId],
4439
+ ['流程链接', result.url],
4440
+ ];
4441
+ for (const [label, value] of header) {
4442
+ const text = String(value ?? '').trim();
4443
+ if (text)
4444
+ lines.push(`- **${label}**:${text}`);
4445
+ }
4446
+ if (mainRows.length) {
4447
+ lines.push('', '**已填内容**', '', '| 字段 | 值 |', '| --- | --- |');
4448
+ for (const row of mainRows)
4449
+ lines.push(`| ${markdownCell(row.name)} | ${markdownCell(row.value)} |`);
4450
+ }
4451
+ // 明细表:按列展示(Excel 列形态),多个子表单各成一表。
4452
+ const groups = (result.previewDetails || []).filter((g) => (g?.columns || []).length);
4453
+ groups.forEach((group, index) => {
4454
+ const columns = group.columns;
4455
+ lines.push('', groups.length > 1 ? `**明细表 ${index + 1}**` : '**明细表**', '', `| 序号 | ${columns.map((c) => markdownCell(c.fieldName)).join(' | ')} |`, `| --- | ${columns.map(() => '---').join(' | ')} |`);
4456
+ (group.rows || []).forEach((row, rowIndex) => {
4457
+ const values = (row?.values || []).map((v) => markdownCell(v));
4458
+ lines.push(`| ${rowIndex + 1} | ${values.join(' | ')} |`);
4459
+ });
4460
+ });
4461
+ const failReason = String(result.submitFailReason ?? '').trim();
4462
+ if (failReason)
4463
+ lines.push('', `**提交失败原因**:${failReason}`);
4464
+ const notice = String(result.missingFieldsNotice ?? '').trim();
4465
+ if (notice)
4466
+ lines.push('', notice);
4467
+ return lines.join('\n').trim();
4468
+ }
4469
+ /**
4470
+ * `createRequest` **尚未发起**(`NEEDS_INPUT`:必填还没填齐)的渲染档:告诉用户"已填了什么、还差
4471
+ * 哪几项"。与落地档(`renderCreateResultMarkdown`)**同一套表格口径**,调用方原样粘贴即可。
4472
+ *
4473
+ * 为什么要在命令里渲染(2026-10-13 真机):让模型自己拿 `resolvedValues` 拼主表 + 明细表、再拼
4474
+ * 待补清单,既慢又容易漂移;而且那样必须把 `resolvedValues` 一起返回,白占体积。渲染完就**不再
4475
+ * 返回**那些中间结构(`resolvedValues` / `previewDetails`)。
4476
+ *
4477
+ * 内容分四段:结论(还差几项)→ 主表「已填内容」两列表(系统字段如流程标题在前、业务字段按表单
4478
+ * 布局顺序随后,含表单默认值 —— 行由调用方算好传进来,与落地档共用同一份口径)→ 各明细表
4479
+ * (Excel 列形态)→ 待补必填项清单(每项标出在主表还是明细第几行)。明细与主表**分开列**,与落地档同口径。
4480
+ *
4481
+ * 待补清单里,命令会为**历史取值一致**的字段补一行「历史推荐值」(口径见 `historyRecommendation`)——
4482
+ * 用户据此可以一句话确认,不必先自己回想;推荐值由命令算好,模型不参与推断。
4483
+ */
4484
+ function renderCreatePendingMarkdown(pending, mainRows, previewDetails, historyRecords, fieldMeta) {
4485
+ const lines = [];
4486
+ const missing = (pending.missingFields || []).filter(Boolean);
4487
+ lines.push(`还没提交:还有 ${missing.length} 项必填没填,给齐后我直接提交。`);
4488
+ if (mainRows.length) {
4489
+ lines.push('', '**已填内容**', '', '| 字段 | 值 |', '| --- | --- |');
4490
+ for (const row of mainRows) {
4491
+ lines.push(`| ${markdownCell(row.name)} | ${markdownCell(row.value)} |`);
4492
+ }
4493
+ }
4494
+ const groups = (previewDetails || []).filter((g) => (g?.columns || []).length);
4495
+ groups.forEach((group, index) => {
4496
+ const columns = group.columns;
4497
+ lines.push('', groups.length > 1 ? `**明细表 ${index + 1}**` : '**明细表**', '', `| 序号 | ${columns.map((c) => markdownCell(c.fieldName)).join(' | ')} |`, `| --- | ${columns.map(() => '---').join(' | ')} |`);
4498
+ (group.rows || []).forEach((row, rowIndex) => {
4499
+ const values = (row?.values || []).map((v) => markdownCell(v));
4500
+ lines.push(`| ${rowIndex + 1} | ${values.join(' | ')} |`);
4501
+ });
4502
+ });
4503
+ if (missing.length) {
4504
+ lines.push('', '**还需要你提供**', '');
4505
+ missing.forEach((item, index) => {
4506
+ const subFormId = String(item?.subFormId ?? '').trim();
4507
+ const rowIndex = Number(item?.dataIndex);
4508
+ const where = subFormId
4509
+ ? `明细表第 ${Number.isFinite(rowIndex) && rowIndex > 0 ? rowIndex : 1} 行`
4510
+ : '主表';
4511
+ lines.push(`${index + 1}. **${markdownCell(item?.name ?? item?.id)}**(${where})`);
4512
+ const recommendation = historyRecommendation(historyRecords, String(item?.id ?? ''));
4513
+ if (recommendation) {
4514
+ lines.push(` - 历史推荐值:${markdownCell(recommendation.value)}(${recommendation.note})`);
4515
+ }
4516
+ });
4517
+ // 缺项类型提示(2026-10-14 用户令):缺项是**发票类 / 附件类**时,把"怎么把它给到"在渲染档里
4518
+ // 一次说清。真机现场:这两类缺项会让模型在"要不要补一句提醒用户怎么给"上反复称量(思考链里
4519
+ // 最长的尾巴就出在这里)——按本项目既有原则,把容易让模型绕圈的判断留在命令侧。
4520
+ // 缺项可能不在 `fieldMeta` 里(联动命中后才新增的必填,不在 createForm 的静态快照里):
4521
+ // 判不出类型就不加,宁可少一句也不猜。
4522
+ const metaById = new Map();
4523
+ for (const meta of fieldMeta || [])
4524
+ metaById.set(String(meta?.id ?? ''), meta);
4525
+ const missingHas = (match) => missing.some((item) => {
4526
+ const meta = metaById.get(String(item?.id ?? ''));
4527
+ return !!meta && match(meta);
4528
+ });
4529
+ if (missingHas((meta) => meta.componentKey === 'EinvoiceComponent' || meta.isEinvoice === true)) {
4530
+ lines.push('', '其中发票可以直接从你的票夹里选,也可以上传本地发票文件。');
4531
+ }
4532
+ if (missingHas((meta) => !!meta.fileUploadParams)) {
4533
+ lines.push('', '附件需要上传本地文件。');
4534
+ }
4535
+ }
4536
+ const linkage = String(pending.linkageResult ?? '').trim();
4537
+ if (linkage)
4538
+ lines.push('', linkage);
4539
+ return lines.join('\n').trim();
4540
+ }
3259
4541
  function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, hiddenFieldIds, compsFieldIds, entries, layoutSubformIds) {
3260
4542
  if (!linkageDetails || !linkageDetails.length)
3261
4543
  return '';
@@ -3300,8 +4582,251 @@ function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden,
3300
4582
  return result;
3301
4583
  }
3302
4584
  // ---------------------------------------------------------------------------
3303
- // createRequest(合并原 createPrepare + createApply)
4585
+ // 小e 环境:createRequest 改交浏览器页面填单(不写服务端)
3304
4586
  // ---------------------------------------------------------------------------
4587
+ /**
4588
+ * 小e 环境下 `createRequest` 不写服务端,改为把「本次要填的字段值」渲染成 `<browsertool>` 块,
4589
+ * 由宿主的浏览器页面把值带进新建页 —— 2026-10-15 用户裁定。
4590
+ *
4591
+ * 边界(用户口径,逐条对齐):
4592
+ * - **无条件直出**:`action=draft` / `submit` 都一样,既不请求 `saveData` / `flow/create` / `submit`,
4593
+ * 也不做必填缺失判定(不返回 `NEEDS_INPUT`);缺什么由浏览器页面里补。
4594
+ * - 「字段重名 / 只读字段 / 值解析歧义」三类**连值都拼不出来**的结构性拦截**照旧生效**(在下面走
4595
+ * 的就是与普通链路同一段代码),它们不是必填判定。
4596
+ * - 句柄**不消费**:本分支在任何写请求之前返回,同一个 `context` 之后仍可正常重发。
4597
+ * - `title` 取**工作流名称**;`src` 与两处 `routerProps_*` 的 `initialEntries` 用**工作流 id**;
4598
+ * `params.customParams` 取 `createRequest` **入参**经解析后的字段值(不含表单默认值、不含联动带出的值),
4599
+ * 同段 `fieldAssignKeys` 列出这次要填的**全部字段 id**(`,` 连接)。
4600
+ */
4601
+ const XIAOE_FILL_FORM_TOOL_ID = 'fill_form';
4602
+ /** 直出 `<browsertool>` 时写死的骨架(除 title / src / `routerProps_*` / `params.customParams` 外的全部内容)。 */
4603
+ const XIAOE_FILL_FORM_SDK_OPTIONS = {
4604
+ compName: 'ui.CorsComponent',
4605
+ routerType: 'MemoryRouter',
4606
+ // 用户 2026-10-15 裁定:PC / 移动各一份 params,app 写死 `@weapp/workflow`。
4607
+ // 2026-10-16 用户澄清:`@` 后面**没有**斜线,`weapp` 与 `workflow` 之间**一个**斜线
4608
+ // ——聊天输入框会把这个值转义显示成 `@/weapp/workflow`,一律以本条为准。
4609
+ // 路由组件分别取 `Comp.WffpRoute`(PC)/ `Comp.MWffpRoute`(移动,带 noMDialog)。
4610
+ params_pc: {
4611
+ app: '@weapp/workflow',
4612
+ compName: 'Comp.WffpRoute',
4613
+ },
4614
+ params_mobile: {
4615
+ app: '@weapp/workflow',
4616
+ compName: 'Comp.MWffpRoute',
4617
+ noMDialog: true,
4618
+ },
4619
+ };
4620
+ /**
4621
+ * `params.customParams` 字段值的两个分隔符 —— 口径来自用户 2026-10-15 给的实机截图:
4622
+ * - **同一字段多值用 `,`**:`?<字段id>=0,1,2`(下拉复选框三选全中)
4623
+ * - **明细多行用 `;`**:`?<明细字段id>=第一行;第二行;第三行`
4624
+ * - 两者组合:`?<明细字段id>=0,1;2,0,2`(行内多值用 `,`、行与行之间用 `;`)
4625
+ */
4626
+ const XIAOE_INITIAL_VALUES_MULTI_SEPARATOR = ',';
4627
+ const XIAOE_INITIAL_VALUES_ROW_SEPARATOR = ';';
4628
+ /** 单个标量 → URL 传参里的那一段文本。对象按「id 优先」取标识(附件取到的就是 fileid)。 */
4629
+ function xiaoEUrlParamScalar(value) {
4630
+ if (value === null || value === undefined)
4631
+ return '';
4632
+ if (typeof value === 'object') {
4633
+ const obj = value;
4634
+ // 解析后的字段值是 `[{id, name}]` 形态(附件 / 选项 / 浏览按钮 / 人员范围都走这一份),
4635
+ // `id` 就是可直接落库的标识:选项类是选项值(0/1/2)、附件类是 fileid、浏览按钮是记录 id。
4636
+ // `name` / `content` 一律不用 —— 附件传文件名等于传一个指向不存在文件的坏 ID。
4637
+ for (const key of ['id', 'optionId', 'fileId', 'fileid', 'fid', 'value']) {
4638
+ const picked = obj[key];
4639
+ if (picked === null || picked === undefined)
4640
+ continue;
4641
+ const text = String(picked).trim();
4642
+ if (text)
4643
+ return text;
4644
+ }
4645
+ return '';
4646
+ }
4647
+ return String(value).trim();
4648
+ }
4649
+ /** 单个字段的值 → URL 传参里的那一段文本(数组按「同字段多值」用 `,` 连接)。 */
4650
+ function xiaoEUrlParamValue(value) {
4651
+ if (value === null || value === undefined)
4652
+ return '';
4653
+ if (Array.isArray(value)) {
4654
+ return value
4655
+ .map((item) => xiaoEUrlParamScalar(item))
4656
+ .filter(Boolean)
4657
+ .join(XIAOE_INITIAL_VALUES_MULTI_SEPARATOR);
4658
+ }
4659
+ return xiaoEUrlParamScalar(value);
4660
+ }
4661
+ /**
4662
+ * `params.customParams` 里**字段值**那一份 = `{字段id: 值}`(用户裁定用**字段 id**,不用数据 key);
4663
+ * 同时回一份**字段 id 顺序表**,供同段 `fieldAssignKeys`(`,` 连接)用。
4664
+ *
4665
+ * 数据源是 `createRequest` **入参**经解析后的结果(`resolveEntryValues` 的 `preview`)——只含
4666
+ * 模型本次真的传了的字段,不含表单默认值、不含联动带出的值。
4667
+ *
4668
+ * 明细字段(`preview` 里带 `dataIndex`)按**行号升序**取值、用 `;` 连接;主表字段就是它自己的值。
4669
+ * 值为空(没解析出内容)的字段整个省略,不生成空参数。
4670
+ */
4671
+ /**
4672
+ * 小e 档交给浏览器页面的字段值:只留**可见且可编辑**的那些(2026-10-21 用户裁定)。
4673
+ *
4674
+ * 依据是 `computeLinkageVisibility` 用句柄里本地 `viewLinkages` 求出来的隐藏 / 禁用,再加上字段
4675
+ * 自身的只读、禁填、初始隐藏(`display === false`)与布局隐藏(`hiddenCellFieldIds`)。
4676
+ *
4677
+ * 过滤的**只有值**,不改页面结构:这些字段在表单页里本来就填不进去(页面自己会拦),把值塞过去
4678
+ * 只会让页面报错或让用户看到"填了但没生效"。
4679
+ */
4680
+ function editablePreviewForXiaoE(preview, fields, visibility) {
4681
+ const fieldById = new Map();
4682
+ for (const field of toArray(fields)) {
4683
+ const id = String(field?.id ?? '').trim();
4684
+ if (id)
4685
+ fieldById.set(id, field);
4686
+ }
4687
+ const kept = [];
4688
+ for (const item of toArray(preview)) {
4689
+ const fieldId = String(item?.fieldId ?? '').trim();
4690
+ if (!fieldId)
4691
+ continue;
4692
+ const field = fieldById.get(fieldId) || {};
4693
+ if (field.readOnly || field.disableEdit || field.display === false)
4694
+ continue;
4695
+ if (visibility.hiddenFieldIds.has(fieldId))
4696
+ continue;
4697
+ const subFormId = String(field.subFormId ?? '').trim();
4698
+ if (subFormId && visibility.hiddenSubforms.has(subFormId))
4699
+ continue;
4700
+ // 行键口径与 `computeLinkageVisibility` 完全一致('main' 或 `<subFormId>:<dataIndex>`,
4701
+ // 明细行没给行号时按第 1 行算)。
4702
+ const hasRow = item?.dataIndex !== undefined && item?.dataIndex !== null;
4703
+ const rowKey = rowKeyFor(subFormId, hasRow ? item.dataIndex : undefined);
4704
+ if (visibility.rowHidden[rowKey]?.has(fieldId))
4705
+ continue;
4706
+ if (visibility.rowDisabled[rowKey]?.has(fieldId))
4707
+ continue;
4708
+ kept.push(item);
4709
+ }
4710
+ return kept;
4711
+ }
4712
+ function xiaoECustomParams(preview) {
4713
+ const ordered = [];
4714
+ const mainValues = new Map();
4715
+ const detailValues = new Map();
4716
+ for (const item of toArray(preview)) {
4717
+ const fieldId = String(item?.fieldId ?? '').trim();
4718
+ if (!fieldId)
4719
+ continue;
4720
+ const text = xiaoEUrlParamValue(item?.value);
4721
+ if (!text)
4722
+ continue;
4723
+ const isDetail = item?.dataIndex !== undefined && item?.dataIndex !== null;
4724
+ if (!isDetail) {
4725
+ if (!mainValues.has(fieldId) && !detailValues.has(fieldId))
4726
+ ordered.push(fieldId);
4727
+ mainValues.set(fieldId, text);
4728
+ continue;
4729
+ }
4730
+ if (!detailValues.has(fieldId)) {
4731
+ if (!mainValues.has(fieldId))
4732
+ ordered.push(fieldId);
4733
+ detailValues.set(fieldId, []);
4734
+ }
4735
+ const index = Number(item.dataIndex);
4736
+ detailValues.get(fieldId).push({ index: Number.isFinite(index) ? index : 0, text });
4737
+ }
4738
+ const values = {};
4739
+ for (const fieldId of ordered) {
4740
+ const rows = detailValues.get(fieldId);
4741
+ if (rows && rows.length) {
4742
+ rows.sort((a, b) => a.index - b.index);
4743
+ values[fieldId] = rows.map((row) => row.text).join(XIAOE_INITIAL_VALUES_ROW_SEPARATOR);
4744
+ continue;
4745
+ }
4746
+ values[fieldId] = mainValues.get(fieldId) ?? '';
4747
+ }
4748
+ return { values, keys: ordered };
4749
+ }
4750
+ /**
4751
+ * 渲染 `<browsertool>` 块(该块**整体**就是小e 环境下 `createRequest` 的 `finalMarkdown`)。
4752
+ *
4753
+ * 除 `title` / `src` / 两处 `routerProps_*` 的 `initialEntries` / `params.customParams` 外全部写死。
4754
+ */
4755
+ function renderXiaoEFillFormMarkdown(params) {
4756
+ const base = String(params.baseUrl ?? '').replace(/\/+$/u, '');
4757
+ const workflowId = String(params.workflowId ?? '').trim();
4758
+ const createPath = `/flowpage/create/${workflowId}`;
4759
+ const payload = {
4760
+ version: 1,
4761
+ callId: 'call_001',
4762
+ toolId: XIAOE_FILL_FORM_TOOL_ID,
4763
+ arguments: {
4764
+ type: 'e10',
4765
+ title: String(params.title ?? ''),
4766
+ src: `${base}/sp/workflow${createPath}?workflowId=${workflowId}&isCreate=1`,
4767
+ sdkOptions: {
4768
+ compName: XIAOE_FILL_FORM_SDK_OPTIONS.compName,
4769
+ routerType: XIAOE_FILL_FORM_SDK_OPTIONS.routerType,
4770
+ routerProps_pc: { initialEntries: [createPath] },
4771
+ routerProps_mobile: { initialEntries: [createPath] },
4772
+ params_pc: { ...XIAOE_FILL_FORM_SDK_OPTIONS.params_pc },
4773
+ params_mobile: { ...XIAOE_FILL_FORM_SDK_OPTIONS.params_mobile },
4774
+ params: {
4775
+ customParams: {
4776
+ // 字段 id → 值(要填写的字段及值),随后是这次要填的**全部字段 id**。
4777
+ ...params.customParams.values,
4778
+ fieldAssignKeys: params.customParams.keys.join(','),
4779
+ },
4780
+ },
4781
+ },
4782
+ },
4783
+ };
4784
+ return `<browsertool>\n${JSON.stringify(payload, null, 2)}\n</browsertool>`;
4785
+ }
4786
+ /**
4787
+ * 把命令耗时并回 `data` 顶层 —— `createRequest` 与 `createRequestDirect` **共用这一份口径**
4788
+ * (2026-10-21 用户裁定:`createRequest` 也要给耗时,且与 `createRequestDirect` 一致):
4789
+ * - `durationMs` = 命令入口 → 结果产出;
4790
+ * - `apiDurations` = 每个接口(并行批次合成一个 key,取墙钟、不叠加);
4791
+ * - `authMs` = 登录取凭据(首次会话解析)的墙钟 —— 这一段走 auth 层自己的 fetch、不经
4792
+ * `host.workflowRequest`,`apiDurations` 统计不到,单独给一个数才能对上账。
4793
+ *
4794
+ * `data` 不是普通对象时原样返回(被替换成字符串 / 数组的返回面一个字都不动)。
4795
+ */
4796
+ function stampCommandTimings(envelope, timing) {
4797
+ const data = envelope?.data;
4798
+ if (!data || typeof data !== 'object' || Array.isArray(data))
4799
+ return envelope;
4800
+ const mutable = data;
4801
+ mutable.durationMs = Date.now() - timing.startedAt;
4802
+ mutable.apiDurations = summarizeApiSpans(timing.spans);
4803
+ mutable.authMs = timing.authMs;
4804
+ return envelope;
4805
+ }
4806
+ /**
4807
+ * `createRequest` 的耗时外壳:建计时通道 + 量登录耗时 + 把三个耗时键并回结果。
4808
+ *
4809
+ * 参数明显不合法(`action` 不对 / 没 `context`)时**不先登录** —— 那两种情况内层会直接抛
4810
+ * 参数错,为一个注定失败的调用白走一趟统一登录不值当;真正的报错措辞仍由内层给出。
4811
+ */
4812
+ async function commandCreateRequest(args, env, options = {}) {
4813
+ const startedAt = Date.now();
4814
+ const spans = options.apiSpans ?? [];
4815
+ // `createRequestDirect` 已经把 host 包过一层(它的片段数组覆盖到表单载入那一段)⇒ 这里不能再包,
4816
+ // 否则每个请求会被记两遍(key 里会出现重名)。
4817
+ const timedEnv = options.apiSpans ? env : { ...env, host: withApiTiming(env.host, spans) };
4818
+ const action = String(args.action ?? '').trim();
4819
+ let authMs = 0;
4820
+ if ((action === 'draft' || action === 'submit') && String(args.context ?? '')) {
4821
+ // 会话是惰性的:内层的 `verifyContextLease` 本来也会取一次,这里只是把它**提前并单独计时**,
4822
+ // 拿到的仍是同一份缓存,不额外多发请求。
4823
+ const authStartedAt = Date.now();
4824
+ await timedEnv.host.getSession();
4825
+ authMs = Date.now() - authStartedAt;
4826
+ }
4827
+ const envelope = await runCreateRequest(args, timedEnv, options);
4828
+ return stampCommandTimings(envelope, { startedAt, spans, authMs });
4829
+ }
3305
4830
  /**
3306
4831
  * `workflow.createRequest` —— **一条命令**完成「字段解析 → 联动 → 必填校验 → 保存 → 发起」。
3307
4832
  *
@@ -3313,7 +4838,7 @@ function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden,
3313
4838
  * 返回**不消费**句柄,于是模型补齐字段后可以带**同一个 context** 重发,不必回头重跑 createForm;
3314
4839
  * 一旦消费,就只能按句柄状态分流(`ALREADY_APPLIED` / 结果不确定 / 并发占用)。
3315
4840
  */
3316
- async function commandCreateRequest(args, env) {
4841
+ async function runCreateRequest(args, env, options = {}) {
3317
4842
  const action = String(args.action ?? '').trim();
3318
4843
  if (action !== 'draft' && action !== 'submit') {
3319
4844
  throw workflowValidationError('action_required', 'action 必填,且只能是 draft(仅存草稿)或 submit(提交审批)');
@@ -3346,13 +4871,35 @@ async function commandCreateRequest(args, env) {
3346
4871
  }
3347
4872
  const [details, valueAmbiguities, preview] = await resolveEntryValues(env, entries, context.layout.form_id);
3348
4873
  const merged = mergeDetails(context.defaults || [], details);
3349
- const linkageDetails = await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false);
4874
+ /**
4875
+ * 小e 环境(2026-10-16 用户裁定):**字段联动与显示属性联动都不跑**。
4876
+ *
4877
+ * 这一档真正的填单动作由宿主浏览器页面完成(见下方 `BROWSER_FILL` 分支),本命令只负责把
4878
+ * 「本次要填的字段值」交给页面:联动**带出的值**、以及联动求出来的隐藏 / 条件必填 / 禁填,
4879
+ * 到了表单页里都会由页面自己重算一遍。在这里跑一是白费一趟 `fdLinkage` 请求,二是这些结果
4880
+ * 全都进不了本档的输出(`customParams` 取的是 `preview`,即入参解析后的值)。
4881
+ *
4882
+ * ⚠️ 与同一次裁定里 `createForm` 的差别:那边是「不跑字段联动、**显示属性联动照跑**」
4883
+ * (`buildCreateFormData`,那边的联动结果要用来算字段的必填 / 可编辑清单);这里两者都不跑 ——
4884
+ * 本档既不做必填判定、也不发写请求。非小e 环境行为不变。
4885
+ */
4886
+ const isXiaoE = hasXiaoEEnv(env.ctx.env, env.ctx.getGlobalOptions?.());
4887
+ const linkageDetails = isXiaoE
4888
+ ? []
4889
+ : await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false, context.defaults || []);
3350
4890
  // 明细默认值无行号,而判缺失/写载荷都按行号索引 ⇒ 按用户已建行补齐缺口(V12.1.35,
3351
4891
  // 详见 fillDetailDefaultsByRow 注释)。放在联动之后合并,联动带出的值仍然优先。
3352
4892
  const merged2 = fillDetailDefaultsByRow(mergeLinkageDetails(merged, linkageDetails), context.defaults || [], userDetailRows(entries));
3353
4893
  const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
3354
4894
  const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
3355
- const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues, fields);
4895
+ const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = isXiaoE
4896
+ ? (options.xiaoEViewLinkageFilter
4897
+ // 2026-10-21 用户裁定(`createRequestDirect` 这一档):小e 档**照跑显示属性联动**,只是不跑字段联动
4898
+ // —— 它要据此把页面本来就填不进去的字段(隐藏 / 禁用)从要填的值里剔掉。
4899
+ // 第三参传 `[]`(本次没有联动带出的值)时,求值只用表单默认值 + 本次入参 + 系统字段值。
4900
+ ? computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], [], context.systemFieldValues, fields)
4901
+ : emptyLinkageVisibility())
4902
+ : computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues, fields);
3356
4903
  const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
3357
4904
  if (initialHidden.size) {
3358
4905
  const mh = new Set(rowHidden.main || []);
@@ -3390,25 +4937,52 @@ async function commandCreateRequest(args, env) {
3390
4937
  }
3391
4938
  const missingAll = [...missing, ...systemMissing];
3392
4939
  /**
3393
- * 历史参考值(V12.1.77):取数已在 `createForm` 做完,这里**只从句柄里的缓存筛**,不再联网。
4940
+ * 历史参考值在 `createRequest` 一侧**不再返回**(V12.1.102 撤出;V12.1.77 曾在这里从句柄缓存
4941
+ * 筛"这次新出现的必填字段"的历史值一并给回)。
4942
+ *
4943
+ * 撤出理由(2026-10-13 真机复核):文档口径早已明令 `NEEDS_INPUT` / `NEEDS_CONFIRMATION` 时
4944
+ * `historyRequestData` **一律不看、不用**(必填缺失直接一次问齐用户,不做任何值推断)——这份数据
4945
+ * 于是只剩副作用:既白占返回体积(真机 3,649 B 里占 2,173 B=60%,且 5 条记录还是同一组值的
4946
+ * 重复),又逼模型多烧一轮说服自己"不要看",返回值与规则自相矛盾。
3394
4947
  *
3395
- * 只筛「**这次新出现**的必填字段」——即 `createForm` 当时还没被判成必填、模型因此没见过的那些
3396
- * (绝大多数是联动命中后才变成必填的字段)。`createForm` 已经给过历史值的字段不再重复返回:
3397
- * 那些值模型本来就拿到了,重复给只是白占上下文。
4948
+ * 历史参考值的唯一入口回到 `createForm`(那边给的仍是"本表单静态必填字段"的参考值)。
4949
+ *
4950
+ * 2026-10-13 晚补(用户口径):原始历史**仍然不返回**给模型,但命令**内部**继续用它——
4951
+ * 为缺失的必填字段算一条「历史取值一致」的**推荐值**渲染进 `finalMarkdown`(见
4952
+ * `historyRecommendation`)。这样用户能一句话确认,而"一致性判断"这件容易让模型绕圈的事
4953
+ * 全部留在命令侧,模型拿到的仍是一份可直接粘贴的文本。
4954
+ */
4955
+ /**
4956
+ * 「已填内容」的两组行(2026-10-14 用户口径):**系统字段(流程标题等)在前、业务字段按表单
4957
+ * 布局顺序随后**,两份渲染档(必填缺失 / 已落地)共用同一份口径。值取自 `merged2`
4958
+ * (表单默认值 + 本次入参 + 联动带出的值),也就是"用户在表单上看到的那一份"。
3398
4959
  *
3399
- * `createForm` 缓存是"本表单全部字段"的宽结果(`allFormFields`),所以新字段也能命中;
3400
- * 缓存取不到 / 一个值都没命中 ⇒ `null`(不塞空数组)。句柄不是本机签发、或会话跨版本升级时
3401
- * 缓存可能缺失,此时退化为 `null`——纯辅助信息,不阻断任何流程。
4960
+ * 真机现场:用户看到的「已填内容」只有模型显式传的两项,而表单上明明还有一批**已填状态**的
4961
+ * 字段(表单默认值、流程标题)——用户无法据此确认"这张单到底填成什么样了"。
3402
4962
  */
3403
- const knownRequiredFieldIds = new Set(toArray(context.historyRequiredFieldIds).map((id) => String(id ?? '').trim()).filter(Boolean));
3404
- const newMissingFieldIds = new Set();
3405
- for (const item of missingAll) {
3406
- const fieldId = String(item?.id ?? '').trim();
3407
- if (!fieldId || item?.systemField || knownRequiredFieldIds.has(fieldId))
4963
+ const mainValueById = new Map();
4964
+ for (const detail of merged2) {
4965
+ if (!detail || typeof detail !== 'object')
3408
4966
  continue;
3409
- newMissingFieldIds.add(fieldId);
3410
- }
3411
- const historyRequestData = filterHistoryRequestData(context.historyCache, newMissingFieldIds);
4967
+ const subFormId = String((detail.subForm && detail.subForm.id) ?? detail.subFormId ?? '').trim();
4968
+ if (subFormId)
4969
+ continue;
4970
+ const fieldId = String((detail.formField && detail.formField.id) ?? '').trim();
4971
+ if (fieldId)
4972
+ mainValueById.set(fieldId, detail);
4973
+ }
4974
+ const filledFieldRows = mainFilledRows(fields, mainValueById,
4975
+ // `rowHidden.main` 已并入布局隐藏(见上面 `initialHidden` 的合流),再补 `hiddenCellFieldIds`。
4976
+ new Set([...(rowHidden.main || []), ...hiddenFieldIds]), new Set(rowDisabled.main || []), compsFieldIds);
4977
+ const filledTitleRows = (title) => systemFilledRows({
4978
+ title,
4979
+ requestLevel: requestLevelValue,
4980
+ secLevel: secLevelValue,
4981
+ permissions: sysPermissions,
4982
+ // 老句柄可能没有这两个选项清单(键名也随版本变过一次),取不到就退化成"不列系统字段"。
4983
+ levelOptions: (context.systemFields && (context.systemFields.level_options || context.systemFields.levelOptions)) || [],
4984
+ secLevelOptions: (context.systemFields && (context.systemFields.sec_level_options || context.systemFields.secLevelOptions)) || [],
4985
+ });
3412
4986
  if (valueAmbiguities.length) {
3413
4987
  return workflowSuccess('workflow.createRequest', {
3414
4988
  status: 'NEEDS_CONFIRMATION',
@@ -3420,20 +4994,84 @@ async function commandCreateRequest(args, env) {
3420
4994
  resolvedValues: preview,
3421
4995
  ambiguities: valueAmbiguities,
3422
4996
  missingFields: missingAll,
3423
- historyRequestData,
3424
4997
  linkageResult,
3425
4998
  ...folderPayload,
3426
4999
  });
3427
5000
  }
5001
+ /**
5002
+ * 小e 环境(2026-10-15 用户裁定):**一个写请求都不发**,直接把本次要填的字段值渲染成
5003
+ * `<browsertool>` 交给宿主的浏览器页面 —— 因此这里**不判必填缺失**(不返回 `NEEDS_INPUT`),
5004
+ * `draft` / `submit` 走的都是这一条。
5005
+ *
5006
+ * 放置位置:在所有"连值都拼不出来"的结构性拦截(字段重名 / 只读字段 / 值解析歧义,都在本行之上)
5007
+ * **之后**、必填缺失分支与写入路径(`verifyContextLease(token, …, true)` 消费句柄、`saveData`、
5008
+ * `flow/create`、`submit`)**之前**。于是:句柄不被消费、一个写请求都不发,同一个 `context` 之后
5009
+ * 仍可正常重发。
5010
+ *
5011
+ * 渲染档(2026-10-15 用户裁定):`finalMarkdown` **整体就是一段 `<browsertool>` 标签块**;本轮回复正文
5012
+ * = 这段内容本身(从 `<browsertool>` 到 `</browsertool>`),原样输出、**不要包在代码块里**。
5013
+ *
5014
+ * **stdout 形态**(2026-10-21 用户裁定,**改口径**):这一档同样**走 JSON 信封**,标签块放在
5015
+ * `data.finalMarkdown` 里 —— 与 `createRequestDirect` 统一,模型侧只有一条读法;`durationMs` /
5016
+ * `apiDurations` / `authMs` 三个耗时键也因此在这一档可见。原来的「stdout 直接就是标签块、不套
5017
+ * 信封」已作废(`shortcuts/workflow/index.ts` 的直出通道随之删除)。
5018
+ * 所以下面的 `message` 要明确告诉模型:从 `data.finalMarkdown` 取内容、原样作为本轮回复正文。
5019
+ *
5020
+ * 判据:桌面客户端可由 Agent 根据宿主明确上下文传入全局参数 `--runtime-agent xiaoe`;
5021
+ * 传入环境中存在任意 `XIAOE_*` 变量时也直接判定为小e环境。
5022
+ * 历史会话和本地文件本身不参与宿主身份判断。
5023
+ * 本处复用上面算好的 `isXiaoE`(同一判据,只求值一次)。
5024
+ *
5025
+ * 本档**不跑字段联动、也不跑显示属性联动**(2026-10-16 用户裁定,见上方 `isXiaoE` 的注释):
5026
+ * 所以这里既没有「字段联动带出」的提示,`hiddenSubforms` / `rowExtraRequired` / `rowDisabled`
5027
+ * 也都是空集 —— 该由浏览器页面在表单里自己算。
5028
+ */
5029
+ if (isXiaoE) {
5030
+ // `src` 要的是绝对地址:句柄里带走的那份 baseUrl 为准(与句柄同源,避免"换了域名后旧句柄拼出别的站"),
5031
+ // 旧版本句柄可能没带 ⇒ 回落到当前会话的 baseUrl。
5032
+ const xiaoEBaseUrl = String(context.baseUrl ?? '') || (await getSessionContext(env)).baseUrl;
5033
+ // 交给页面的值:`createRequestDirect` 这一档先按显示属性联动过滤掉不可编辑字段(见上方 options 注释),
5034
+ // 其余入口(`createRequest`)原样传 `preview`。
5035
+ const fillPreview = options.xiaoEViewLinkageFilter
5036
+ ? editablePreviewForXiaoE(preview, fields, { hiddenSubforms, rowHidden, rowDisabled, hiddenFieldIds })
5037
+ : preview;
5038
+ return workflowSuccess('workflow.createRequest', {
5039
+ status: 'BROWSER_FILL',
5040
+ contextId: token,
5041
+ // 本档只有一种输出形态(JSON 信封),指引也就只有一条:从 `data.finalMarkdown` 取内容。
5042
+ message: '当前是小e环境:本次不向服务端写入,改由浏览器页面完成填单。本档输出的是 JSON 信封(额外带 durationMs / apiDurations / authMs 耗时),`data.finalMarkdown` 就是那段 <browsertool> 标签块(其中的换行在 JSON 里以 \\n 转义)。把 `data.finalMarkdown` 的内容原样作为本轮回复正文:从 <browsertool> 到 </browsertool>、换行还原成真实换行,不要包在代码块里、不要加任何前后说明;JSON 信封本身不要回给用户。',
5043
+ workflow: context.workflow,
5044
+ finalMarkdown: renderXiaoEFillFormMarkdown({
5045
+ baseUrl: xiaoEBaseUrl,
5046
+ title: String(context.workflow?.name ?? ''),
5047
+ workflowId: String(context.workflow?.id ?? context.runtime?.workflow_id ?? ''),
5048
+ customParams: xiaoECustomParams(fillPreview),
5049
+ }),
5050
+ });
5051
+ }
3428
5052
  if (action === 'submit' && missingAll.length) {
3429
- // submit 且有必填缺失:**不调用创建接口**,把缺失项一次交回给模型/用户
5053
+ // submit 且有必填缺失:**不调用创建接口**,把缺失项一次交回给模型/用户。
5054
+ //
5055
+ // 同时给一份可直接原样粘贴的渲染档(V12.1.102):结论 + 主表「已填内容」两列表(含流程标题与
5056
+ // 表单默认值,2026-10-14 起)+ 明细 Excel 列 + 待补必填项清单。模型不必自己拼这些表格,也不必再
5057
+ // 遍历 `resolvedValues` 拼待补清单;因此这里**不再返回** `resolvedValues`(渲染素材只在命令内部用)。
5058
+ const pendingRequestName = String(args.requestName ?? sysDefaults.requestName ?? context.runtime.default_request_name ?? '').trim();
5059
+ // ★ 2026-10-20 用户裁定:**有 `finalMarkdown` 的状态不再返回 `message`**。
5060
+ // 这条路一定有渲染档(`renderCreatePendingMarkdown` 至少输出结论句 + 待补清单),
5061
+ // 而 `message` 说的是同一件事、口气还是"对模型下指令"("带同一 context 重发 action=submit"),
5062
+ // 模型看到就会在 `finalMarkdown` 之外多输出一段、顺带把内部机制漏给用户。
3430
5063
  return workflowSuccess('workflow.createRequest', {
3431
5064
  status: 'NEEDS_INPUT',
3432
5065
  contextId: token,
3433
- message: '缺少必填字段,本次未发起流程。请把下面这些字段一次问齐后,带同一 context 重发 action=submit。',
3434
- resolvedValues: preview,
3435
5066
  missingFields: missingAll,
3436
- historyRequestData,
5067
+ finalMarkdown: renderCreatePendingMarkdown({ missingFields: missingAll, linkageResult },
5068
+ // 标题行按"当前还没填到标题"的口径取(可能为空 ⇒ 不出现该行),与落地档的 `requestName` 兜底分开。
5069
+ [...filledTitleRows(pendingRequestName), ...filledFieldRows], buildPreviewDetails(preview),
5070
+ // 历史推荐值的数据源:`createForm` 随句柄留下的**宽缓存**(本表单全部字段)。
5071
+ // 只用来算"这个缺失字段历史取值是否一致",**不返回**给模型——一致性判断不外包给模型。
5072
+ context.historyCache,
5073
+ // 缺项类型提示用:缺项是发票 / 附件类时把"怎么给"写在渲染档里(见函数内注释)。
5074
+ fields),
3437
5075
  linkageResult,
3438
5076
  ...folderPayload,
3439
5077
  });
@@ -3637,8 +5275,206 @@ async function commandCreateRequest(args, env) {
3637
5275
  : '。');
3638
5276
  }
3639
5277
  }
5278
+ // 渲染档:流程**已落地**时给一份可直接原样粘贴的最终回复文本(与查询侧 `finalMarkdown` 同口径)。
5279
+ // `NEEDS_INPUT` 的那份在上面分支里就已挂上(同口径、只换结论句与末段);`NEEDS_CONFIRMATION` 不给。
5280
+ // 标题行用**本次真正落库的** `requestName`(与 `prepared.requestName` 同源),不再是"可能为空"的口径。
5281
+ if (['CREATED', 'CREATED_SUBMITTED', 'CREATED_SUBMIT_FAILED'].includes(String(result.status))) {
5282
+ result.finalMarkdown = renderCreateResultMarkdown(result, [...filledTitleRows(requestName), ...filledFieldRows]);
5283
+ // ★ 渲染档的**结论句就是 `message`**(`renderCreateResultMarkdown` 第一行取的它),所以只能在
5284
+ // 渲染之后再摘键——不是不用它,是**不单独返回它**:返回面里 `message` 与 `finalMarkdown` 说的
5285
+ // 是同一句话,模型只会挑一个输出,留着就是给自己制造"两个声音"(2026-10-20 用户裁定)。
5286
+ delete result.message;
5287
+ }
3640
5288
  return workflowSuccess('workflow.createRequest', result);
3641
5289
  }
5290
+ // ---------------------------------------------------------------------------
5291
+ // 接口耗时(`workflow.createRequestDirect`)
5292
+ // ---------------------------------------------------------------------------
5293
+ /**
5294
+ * 接口**简称**:`path` → 模型看得懂的短名。
5295
+ *
5296
+ * 只登记这条命令真会打到的接口(解析工作流 / 载入表单 / 联动 / 写库 / 校验 / 浏览按钮);
5297
+ * 没登记的按路径最后一段兜底,不会因为漏登记就把计时丢掉。
5298
+ */
5299
+ const API_SHORT_NAMES = {
5300
+ [FORM_ENDPOINTS.workflowCreateList]: 'newFlowList',
5301
+ [FORM_ENDPOINTS.loadBase]: 'loadBase',
5302
+ [FORM_ENDPOINTS.loadFrame]: 'loadFrame',
5303
+ [FORM_ENDPOINTS.loadMinor]: 'loadMinor',
5304
+ [FORM_ENDPOINTS.formLayout]: 'formLayout',
5305
+ [FORM_ENDPOINTS.tenantSetting]: 'tenantSetting',
5306
+ [FORM_ENDPOINTS.fdLinkage]: 'fdLinkage',
5307
+ [FORM_ENDPOINTS.save]: 'saveFormData',
5308
+ [FORM_ENDPOINTS.create]: 'flowCreate',
5309
+ [FORM_ENDPOINTS.flowSubmit]: 'flowSubmit',
5310
+ [FORM_ENDPOINTS.signInputInfo]: 'signInputInfo',
5311
+ [FORM_ENDPOINTS.submit]: 'submitRequest',
5312
+ [FORM_ENDPOINTS.nameToId]: 'relateName2Id',
5313
+ [FORM_ENDPOINTS.verifyNodeFieldByList]: 'verifyNodeField',
5314
+ [FORM_ENDPOINTS.conformanceSet]: 'conformanceSet',
5315
+ [FORM_ENDPOINTS.verificationRequestData]: 'verificationRequestData',
5316
+ [FORM_ENDPOINTS.verificationConditionRule]: 'verificationConditionRule',
5317
+ [WORKFLOW_ENDPOINTS.MINE_TREE]: 'mineTree',
5318
+ [WORKFLOW_ENDPOINTS.MEMORY_PROMPT]: 'memoryPrompt',
5319
+ [WORKFLOW_ENDPOINTS.WORKFLOW_PATH_BROWSER]: 'workflowPathBrowser',
5320
+ [WORKFLOW_ENDPOINTS.RESOURCE_BROWSER]: 'resourceBrowser',
5321
+ [WORKFLOW_ENDPOINTS.HRM_RESOURCE_BROWSER]: 'hrmResourceBrowser',
5322
+ [WORKFLOW_ENDPOINTS.DEPT_BROWSER]: 'deptBrowser',
5323
+ [WORKFLOW_ENDPOINTS.SUBCOMPANY_BROWSER]: 'subcompanyBrowser',
5324
+ [WORKFLOW_ENDPOINTS.GROUP_BROWSER]: 'groupBrowser',
5325
+ [WORKFLOW_ENDPOINTS.ROLE_BROWSER]: 'roleBrowser',
5326
+ [WORKFLOW_ENDPOINTS.POSITION_BROWSER]: 'positionBrowser',
5327
+ [WORKFLOW_ENDPOINTS.NODE_BROWSER]: 'nodeBrowser',
5328
+ [WORKFLOW_ENDPOINTS.FIELD_OPTION_BROWSER]: 'fieldOptionBrowser',
5329
+ [WORKFLOW_ENDPOINTS.REQUEST_DETAIL]: 'getWorkflowRequest',
5330
+ // 智能填参(非 workflow 前缀,靠 `aiappuniquekey` 头定位智能体应用,见 chatscene.ts)
5331
+ '/api/ai/application/chat/chatScene': 'chatScene',
5332
+ };
5333
+ function apiShortName(path) {
5334
+ const text = String(path ?? '');
5335
+ const known = API_SHORT_NAMES[text];
5336
+ if (known)
5337
+ return known;
5338
+ const segments = text.split('/').filter(Boolean);
5339
+ return segments.length ? segments[segments.length - 1] : text || 'unknown';
5340
+ }
5341
+ /**
5342
+ * 给 host 套一层**逐接口计时**(只包请求,不改返回、不吞异常)。
5343
+ *
5344
+ * `finally` 里落片段:请求失败(超时 / 业务错误 / 抛错)同样要记 —— 恰恰是这类请求最需要看到耗时。
5345
+ */
5346
+ function withApiTiming(host, spans) {
5347
+ const record = async (path, run) => {
5348
+ const start = Date.now();
5349
+ try {
5350
+ return await run();
5351
+ }
5352
+ finally {
5353
+ spans.push({ name: apiShortName(path), start, end: Date.now() });
5354
+ }
5355
+ };
5356
+ return {
5357
+ ...host,
5358
+ workflowRequest: (request) => record(request?.path, () => host.workflowRequest(request)),
5359
+ workflowBinaryRequest: (request) => record(request?.path, () => host.workflowBinaryRequest(request)),
5360
+ };
5361
+ }
5362
+ /**
5363
+ * 把逐次请求的片段收成「一条命令里每个接口花了多久」。
5364
+ *
5365
+ * 口径(2026-10-21 用户裁定):
5366
+ * - **并行**(时间区间重叠)的接口合成**一个 key**:各简称用 `_` 拼起来(多个时按字母序,保证同一批
5367
+ * 接口每次输出同一个 key),**值取这一批的墙钟耗时**(最早开始 → 最晚结束),**不是**把批内每个
5368
+ * 接口的耗时相加;
5369
+ * - 串行接口各自一个 key,值就是它自己的耗时;
5370
+ * - 同一个 key 出现多次(同一接口被调了两轮)按累加计 —— 那确实是两笔独立开销。
5371
+ *
5372
+ * 重叠判定用「区间扫描」而不是推断代码里的并发结构:只要两个请求在时间上真重叠就算一批,
5373
+ * 批量接口内部再起并发也照样能反映出来。
5374
+ */
5375
+ function summarizeApiSpans(spans) {
5376
+ const sorted = [...spans].sort((a, b) => (a.start - b.start) || (a.end - b.end));
5377
+ const groups = [];
5378
+ let current = [];
5379
+ let reach = Number.NEGATIVE_INFINITY;
5380
+ for (const span of sorted) {
5381
+ // 端点相接(`start === reach`)按串行算:前一个已经返回,后一个才开始。
5382
+ if (current.length && span.start < reach) {
5383
+ current.push(span);
5384
+ reach = Math.max(reach, span.end);
5385
+ }
5386
+ else {
5387
+ if (current.length)
5388
+ groups.push(current);
5389
+ current = [span];
5390
+ reach = span.end;
5391
+ }
5392
+ }
5393
+ if (current.length)
5394
+ groups.push(current);
5395
+ const out = {};
5396
+ for (const group of groups) {
5397
+ const names = group.map((item) => item.name);
5398
+ const key = names.length === 1 ? names[0] : [...names].sort().join('_');
5399
+ const cost = Math.max(0, Math.round(Math.max(...group.map((item) => item.end)) - Math.min(...group.map((item) => item.start))));
5400
+ out[key] = (out[key] ?? 0) + cost;
5401
+ }
5402
+ return out;
5403
+ }
5404
+ /**
5405
+ * `workflow.createRequestDirect` —— **临时命令**:把「载入表单 + 智能填参」和「发起」串成一步。
5406
+ *
5407
+ * 等价于「先跑 `createForm`(带 `userQuestion`),再把它的 `contextId` 与 `data` 原样交给
5408
+ * `createRequest`」,区别只在一次进程内完成、不给模型中间态。
5409
+ *
5410
+ * ## 四条口径
5411
+ * 1. **取数阶段复用** `prepareCreateFormPayload`(`createForm` 的同一段代码),所以澄清 / 表单不可发起
5412
+ * 这类错误**原样上抛**,新命令一条都不会吞,也不会在澄清时硬着头皮发写请求。
5413
+ * 2. 智能填参走通 ⇒ 用它算出的 `data`;**没走通(超时 / 返回体不合法 / 未传 userQuestion)⇒ 按
5414
+ * `data` 为空继续** —— 交给 `createRequest` 自己的必填校验去判定(`submit` 缺必填不写库、
5415
+ * `draft` 直接建草稿),与"模型没填任何字段就调 createRequest"完全等价。
5416
+ * 3. **小e 档**:不跑字段联动(页面自己会重算),但**跑显示属性联动并把不可编辑字段过滤掉**
5417
+ * (`xiaoEViewLinkageFilter`),交出去的值只含页面真能填的字段。
5418
+ * 4. 最终返回 **`createRequest` 的结果**(`data` 与各状态语义完全一致,`action` 仍是
5419
+ * `workflow.createRequest`),只额外挂三个耗时键(见下)。JSON 信封的 `operation` 是
5420
+ * `workflow.createRequestDirect`(由命令层盖,见 `successEnvelope`)。
5421
+ *
5422
+ * ## 小e 档也走 JSON(2026-10-21 用户裁定,已推广到原命令)
5423
+ * 原命令在小e 下曾直出 `<browsertool>`,那条路会把耗时藏掉。现在**两条命令统一走 JSON 信封**:
5424
+ * `data.finalMarkdown` 仍是同一段标签块,`data.durationMs` / `data.apiDurations` / `data.authMs`
5425
+ * 一并可见;`message` 告知模型从 `finalMarkdown` 取内容原样回复。
5426
+ *
5427
+ * ## 耗时
5428
+ * - `data.durationMs` = 命令总耗时;
5429
+ * - `data.apiDurations` = 每个接口(并行批次合并)的耗时;
5430
+ * - `data.authMs` = 登录取凭据(首次会话解析)的耗时 —— 这段走 auth 层自己的 fetch,不经
5431
+ * `host.workflowRequest`,`apiDurations` 统计不到,单独给一个数才能对上账
5432
+ * (`durationMs` ≈ `apiDurations` 合计 + `authMs` + 本地解析/渲染的零头)。
5433
+ * 三者都只在 `data` 是普通对象时挂;本命令与 `createRequest` 用的是**同一个** `stampCommandTimings`,
5434
+ * 差别只在时间区间(这里从命令入口起算、覆盖表单载入那一段)。
5435
+ */
5436
+ async function commandCreateRequestDirect(args, env) {
5437
+ const startedAt = Date.now();
5438
+ const action = String(args.action ?? '').trim();
5439
+ // 先校验 action 再取数:参数错就没必要白跑一趟表单载入(那是最贵的一段)。
5440
+ if (action !== 'draft' && action !== 'submit') {
5441
+ throw workflowValidationError('action_required', 'action 必填,且只能是 draft(仅存草稿)或 submit(提交审批)');
5442
+ }
5443
+ // 计时从**命令入口**开始算(含会话解析、工作流消歧、表单载入、智能填参、写库、校验)。
5444
+ const spans = [];
5445
+ const timedEnv = { ...env, host: withApiTiming(env.host, spans) };
5446
+ const { payload, fill, authMs } = await prepareCreateFormPayload(args, timedEnv);
5447
+ const contextId = String(payload.contextId ?? '');
5448
+ if (!contextId) {
5449
+ // `prepareCreateFormPayload` 正常返回时 contextId 必然存在;这里只是不带着空句柄去调写接口。
5450
+ throw workflowValidationError('context_missing', '表单载入未产出可用句柄,本次未发起任何写入,请重试');
5451
+ }
5452
+ // ★ 智能体没给出可用 data ⇒ `{}`(不是老口径的 historyRequestData:本命令不做二次推断,
5453
+ // 有值就用、没有就交给必填校验)。
5454
+ const data = (fill.applied && fill.data && typeof fill.data === 'object')
5455
+ ? fill.data
5456
+ : {};
5457
+ const systemData = fill.systemData ?? {};
5458
+ const requestName = String(payload.requestName ?? '').trim();
5459
+ const requestLevel = String(systemData.requestLevel ?? '').trim();
5460
+ const secLevel = String(systemData.secLevel ?? '').trim();
5461
+ const remark = String(systemData.remark ?? '').trim();
5462
+ const envelope = await commandCreateRequest({
5463
+ context: contextId,
5464
+ data,
5465
+ action,
5466
+ // ★ 空串一律**不传键**:`createRequest` 的兜底链是 `args.x ?? sysDefaults.x ?? 句柄里的默认值`,
5467
+ // 而 `??` 挡不住空串 —— 传 `''` 会把后面的默认值全部短路掉(`request_name 不能为空` 就是这么来的)。
5468
+ ...(requestName ? { requestName } : {}),
5469
+ ...(requestLevel ? { requestLevel } : {}),
5470
+ ...(secLevel ? { secLevel } : {}),
5471
+ ...(remark ? { remark } : {}),
5472
+ verificationConfirmed: args.verificationConfirmed,
5473
+ }, timedEnv, { xiaoEViewLinkageFilter: true, apiSpans: spans });
5474
+ // 把耗时并回结果:与 `createRequest` 同一个函数、同一个口径,只是时间区间是整条命令
5475
+ // (含会话解析、工作流消歧、表单载入、智能填参、写库、校验)。
5476
+ return stampCommandTimings(envelope, { startedAt, spans, authMs });
5477
+ }
3642
5478
  function formatRemark(value) {
3643
5479
  const text = String(value ?? '').trim();
3644
5480
  if (!text)
@@ -3757,10 +5593,17 @@ function buildCreateBody(context, prepared, saved, src) {
3757
5593
  * - 多带 `operResultType`/`userSpecifiedNodeId`/`needAutoOpen`/`autoMaxDisplay`/`testParam`/
3758
5594
  * `seconedSubmit`(字段名本来就少一个 n,照抄不改)。
3759
5595
  */
3760
- function buildSubmitBody(context, prepared, view) {
5596
+ function buildSubmitBody(context, prepared, view,
5597
+ /**
5598
+ * 已经按默认样式包好的签字意见(见 `sign-input.ts`)。
5599
+ *
5600
+ * 不传则退回 `formatRemark` 的老形态 —— 只有"确实要提交、且内容非空"的那一档才会传,
5601
+ * 空意见与降级路径仍是 `_weaverMte_`,与历史完全一致。
5602
+ */
5603
+ wrappedRemark) {
3761
5604
  const runtime = context.runtime;
3762
5605
  const requestId = String(runtime.request_id ?? '');
3763
- const remark = formatRemark(prepared.remark);
5606
+ const remark = wrappedRemark ?? formatRemark(prepared.remark);
3764
5607
  return {
3765
5608
  operResultType: 'SUCCESS_RELOAD_CURRENT_PAGE',
3766
5609
  userSpecifiedNodeId: '0',
@@ -4008,13 +5851,18 @@ async function loadCreateSubmitContext(env, context, requestLevel) {
4008
5851
  * 连接中断同样按"提交结果不确定"如实回报;**认证错误照规则原样抛**,不降级成数据结论。
4009
5852
  */
4010
5853
  async function submitCreatedRequest(env, context, prepared, view) {
5854
+ // 提交前先按租户配置把签字意见包成带默认字体/字号的形态(发起流程 ⇒ `isCreate:1` + `workflowId`)。
5855
+ // 取不到样式时 `resolveSignRemark` 内部已退回老形态,不会阻断提交。
5856
+ const signRemark = await resolveSignRemark(env, prepared.remark, {
5857
+ workflowId: String(context.workflow?.id ?? ''),
5858
+ });
4011
5859
  let response;
4012
5860
  try {
4013
5861
  response = await env.host.workflowRequest({
4014
5862
  method: 'POST',
4015
5863
  path: FORM_ENDPOINTS.flowSubmit,
4016
5864
  query: { requestId: String(context.runtime.request_id ?? '') },
4017
- body: buildSubmitBody(context, prepared, view),
5865
+ body: buildSubmitBody(context, prepared, view, signRemark),
4018
5866
  operation: 'workflow.createRequest',
4019
5867
  });
4020
5868
  }
@@ -4505,7 +6353,7 @@ async function commandEditPrepare(args, env) {
4505
6353
  });
4506
6354
  }
4507
6355
  const merged = mergeDetails(context.currentDetails || [], details);
4508
- const linkageDetails = await runFieldLinkage(env, { ...linkageContextFromPayload(context), requestId: context.requestId, formId: context.form_id, dataId: context.data_id, workflow: context.workflow, businessFields: context.businessFields }, entries, null, merged, true);
6356
+ const linkageDetails = await runFieldLinkage(env, { ...linkageContextFromPayload(context), requestId: context.requestId, formId: context.form_id, dataId: context.data_id, workflow: context.workflow, businessFields: context.businessFields }, entries, null, merged, true, context.currentDetails || []);
4509
6357
  const merged2 = mergeLinkageDetails(merged, linkageDetails);
4510
6358
  const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
4511
6359
  const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
@@ -4679,7 +6527,7 @@ const workflowCreateForm = {
4679
6527
  resource: 'workflow',
4680
6528
  method: 'createForm',
4681
6529
  action: 'workflow.createForm',
4682
- summary: '按工作流参数解析可发起的工作流并载入发起表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出 context 句柄 + 字段清单 + fileUploadParams + 该工作流的发起偏好 + 本表单必填字段的历史参考值(不落业务数据)',
6530
+ summary: '按工作流参数解析可发起的工作流并载入发起表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出 context 句柄 + 字段清单 + fileUploadParams + 该工作流的发起偏好。传了 userQuestion 时额外调智能体把意图算成 data 参数(此时不返回 historyRequestData / systemFields)',
4683
6531
  capability: 'create',
4684
6532
  risk: 'read',
4685
6533
  requiresConfirmation: false,
@@ -4687,9 +6535,16 @@ const workflowCreateForm = {
4687
6535
  workflow: {
4688
6536
  type: 'array',
4689
6537
  items: { type: 'string' },
4690
- description: '工作流条件:支持 "名称(ID)"、名称(可传多个,用英文逗号分隔,如 "报销,报销流程")、纯数字 ID;按当前用户**可发起**的工作流模糊匹配;0 命中或多命中且无全名精确匹配时返回澄清(候选见 candidates),确认后由你按「名称(ID)」重发(只问用户完整名称或其它可用于检索的关键词,不要向用户索取 ID)',
6538
+ description: '工作流条件:支持 "名称(ID)"、名称(可传多个,用中英文逗号分隔,如 "请假,请假申请")、纯数字 ID;按当前用户**可发起**的工作流模糊匹配;未能唯一确定目标时返回澄清(候选见 candidates),确认后由你按「名称(ID)」重发(只问用户完整名称或其它可用于检索的关键词,不要向用户索取 ID)',
4691
6539
  },
4692
6540
  relatesTo: { type: 'string', description: '可选关联对象 id(仅透传,不影响表单初始化)' },
6541
+ userQuestion: {
6542
+ type: 'string',
6543
+ description: '用户的填单意图(用一句话概括,可以是多轮对话汇总后的最终需求)。'
6544
+ + '**传了才启用智能填参**:命令会在返回前把它连同字段清单交给智能体,换回已填好的 data 参数'
6545
+ + '(此时不再返回 historyRequestData / systemFields)。'
6546
+ + '不传 = 关闭这条链路,按原口径返回 historyRequestData 等参考值(老调用方零改动)',
6547
+ },
4693
6548
  }, { required: ['workflow'] }),
4694
6549
  handler: (args, env) => commandCreateForm(args, env),
4695
6550
  };
@@ -4718,6 +6573,34 @@ const workflowCreateRequest = {
4718
6573
  }, { required: ['context', 'data', 'action'] }),
4719
6574
  handler: (args, env) => commandCreateRequest(args, env),
4720
6575
  };
6576
+ const workflowCreateRequestDirect = {
6577
+ name: 'workflow.createRequestDirect',
6578
+ resource: 'workflow',
6579
+ method: 'createRequestDirect',
6580
+ action: 'workflow.createRequestDirect',
6581
+ summary: '一步发起(直接给 workflow,不必先调 createForm 取句柄;仍按 requiresConfirmation 先取得用户确认):先按 createForm 解析工作流并载入表单、把 userQuestion 交给智能体算成 data,然后直接接着执行 createRequest(保存 → 创建 → 可选提交);智能体没给出可用 data 时按 data 为空继续。小e 环境不跑字段联动但照跑显示属性联动、只交可编辑字段。返回结果与 createRequest 一致,另带 durationMs(总耗时)、apiDurations(每接口耗时)与 authMs(登录取凭据耗时)',
6582
+ capability: 'create',
6583
+ risk: 'high-risk-write',
6584
+ requiresConfirmation: true,
6585
+ inputSchema: schemaOf({
6586
+ workflow: {
6587
+ type: 'array',
6588
+ items: { type: 'string' },
6589
+ description: '工作流条件:支持 "名称(ID)"、名称(可传多个,用英文逗号分隔)、纯数字 ID;语义与 createForm 的 workflow 完全一致(含澄清规则)',
6590
+ },
6591
+ userQuestion: {
6592
+ type: 'string',
6593
+ description: '用户的填单意图(一句话概括,可多轮汇总)。命令会把它交给智能体算出要填的字段值后直接发起;为空或智能体没给出可用结果时,按"一个字段都没填"继续发起',
6594
+ },
6595
+ action: {
6596
+ type: 'string',
6597
+ enum: ['draft', 'submit'],
6598
+ description: 'draft=仅保存草稿;submit=提交审批(必填缺失时不写库、一次性回报缺失项)',
6599
+ },
6600
+ verificationConfirmed: { type: 'boolean', description: '节点字段校验需用户确认时显式确认继续提交' },
6601
+ }, { required: ['workflow', 'userQuestion', 'action'] }),
6602
+ handler: (args, env) => commandCreateRequestDirect(args, env),
6603
+ };
4721
6604
  const workflowEditLoad = {
4722
6605
  name: 'workflow.editLoad',
4723
6606
  resource: 'workflow',
@@ -4767,6 +6650,7 @@ const workflowEditApply = {
4767
6650
  export const workflowFormOperations = [
4768
6651
  workflowCreateForm,
4769
6652
  workflowCreateRequest,
6653
+ workflowCreateRequestDirect,
4770
6654
  workflowEditLoad,
4771
6655
  workflowEditPrepare,
4772
6656
  workflowEditApply,