weaver-work-cli 0.1.24 → 0.1.25
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.
- package/README.md +10 -1
- package/dist/core/output.js +53 -1
- package/dist/internal/e10/auth/commands.js +17 -2
- package/dist/internal/e10/auth/crypto.js +52 -12
- package/dist/internal/e10/auth/xiaoe.js +112 -39
- package/dist/shortcuts/caizhilian/operations/handlers/fileops.js +67 -1
- package/dist/shortcuts/caizhilian/operations/handlers/setting-attach.js +198 -0
- package/dist/shortcuts/caizhilian/operations/shared.js +32 -0
- package/dist/shortcuts/caizhilian/operations/specs.js +15 -0
- package/dist/shortcuts/workflow/index.js +55 -1
- package/dist/shortcuts/workflow/operations/form.js +1252 -109
- package/dist/shortcuts/workflow/operations/read.js +75 -35
- package/dist/shortcuts/workflow/operations/shared.js +25 -0
- package/dist/shortcuts/workflow/stdout-compact.js +33 -0
- package/docs/caizhilian.md +4 -3
- package/docs/e10-auth.md +8 -1
- package/node_modules/@napi-rs/keyring/LICENSE +21 -0
- package/node_modules/@napi-rs/keyring/README.md +19 -0
- package/node_modules/@napi-rs/keyring/index.d.ts +166 -0
- package/node_modules/@napi-rs/keyring/index.js +514 -0
- package/node_modules/@napi-rs/keyring/keytar.d.ts +53 -0
- package/node_modules/@napi-rs/keyring/keytar.js +26 -0
- package/node_modules/@napi-rs/keyring/package.json +125 -0
- package/node_modules/@napi-rs/keyring-darwin-arm64/README.md +3 -0
- package/node_modules/@napi-rs/keyring-darwin-arm64/keyring.darwin-arm64.node +0 -0
- package/node_modules/@napi-rs/keyring-darwin-arm64/package.json +32 -0
- package/node_modules/commander/LICENSE +22 -0
- package/node_modules/commander/Readme.md +1149 -0
- package/node_modules/commander/esm.mjs +16 -0
- package/node_modules/commander/index.js +24 -0
- package/node_modules/commander/lib/argument.js +149 -0
- package/node_modules/commander/lib/command.js +2662 -0
- package/node_modules/commander/lib/error.js +39 -0
- package/node_modules/commander/lib/help.js +709 -0
- package/node_modules/commander/lib/option.js +367 -0
- package/node_modules/commander/lib/suggestSimilar.js +101 -0
- package/node_modules/commander/package-support.json +16 -0
- package/node_modules/commander/package.json +82 -0
- package/node_modules/commander/typings/esm.d.mts +3 -0
- package/node_modules/commander/typings/index.d.ts +1045 -0
- package/package.json +5 -1
- package/skills/weaver-e10-builder/SKILL.md +281 -0
- package/skills/weaver-e10-builder/_catalog.md +94 -0
- package/skills/weaver-e10-builder/actionflows/CONTEXT.md +71 -0
- package/skills/weaver-e10-builder/actionflows/SKILL.md +176 -0
- package/skills/weaver-e10-builder/actionflows/docs/adr/0001-generated-actionflow-cleanup-identity.md +5 -0
- package/skills/weaver-e10-builder/actionflows/docs/adr/0002-preserve-existing-actionflow-on-update.md +36 -0
- package/skills/weaver-e10-builder/actionflows/references/components/calendar/MeetingCreate.md +19 -0
- package/skills/weaver-e10-builder/actionflows/references/components/calendar/ScheduleCreate.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/database/DMLExecute.md +38 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/AttachmentCopy.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/AttachmentMove.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentCopy.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentDelete.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentEdit.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentMove.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentPermissionChange.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentRenewal.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentRevert.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSave.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscription.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscriptionApprove.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscriptionSendBack.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/DocumentSubscriptionWithdraw.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/FileFolderSave.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/GetAttachmentInfo.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/GetDocumentInfo.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/ModifyDocumentStatus.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/ReplaceAttachment.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/document/UpdatePrintNum.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbAdd.md +100 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbDelete.md +23 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbRecover.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbSaveVersion.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/ActionEbUpdate.md +67 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/DateTrigger.md +32 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EbGetDataById.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EnquiryForm.md +107 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EventButton.md +33 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/EventFormData.md +19 -0
- package/skills/weaver-e10-builder/actionflows/references/components/ebuilder/TimeBatchTrigger.md +19 -0
- package/skills/weaver-e10-builder/actionflows/references/components/http/Http.md +77 -0
- package/skills/weaver-e10-builder/actionflows/references/components/logic/Condition.md +23 -0
- package/skills/weaver-e10-builder/actionflows/references/components/logic/LogicFlowSet.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/logic/MultipleExecution.md +38 -0
- package/skills/weaver-e10-builder/actionflows/references/components/logic/Output.md +37 -0
- package/skills/weaver-e10-builder/actionflows/references/components/message/InnerMQSend.md +50 -0
- package/skills/weaver-e10-builder/actionflows/references/components/message/SendMessage.md +30 -0
- package/skills/weaver-e10-builder/actionflows/references/components/personnel/AlterPersonStatus.md +30 -0
- package/skills/weaver-e10-builder/actionflows/references/components/personnel/HrmEmployeeUpdate.md +34 -0
- package/skills/weaver-e10-builder/actionflows/references/components/personnel/PersonnelAlterType.md +38 -0
- package/skills/weaver-e10-builder/actionflows/references/components/personnel/PersonnelOnboarding.md +36 -0
- package/skills/weaver-e10-builder/actionflows/references/components/serverless/ServerlessAction.md +42 -0
- package/skills/weaver-e10-builder/actionflows/references/components/trigger/CustomTrigger.md +82 -0
- package/skills/weaver-e10-builder/actionflows/references/components/trigger/InnerMQListener.md +41 -0
- package/skills/weaver-e10-builder/actionflows/references/components/trigger/SimpleScheduler.md +19 -0
- package/skills/weaver-e10-builder/actionflows/references/components/trigger/WebHookTrigger.md +91 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveAddSign.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveAgreeRejectApply.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveCommit.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveConsulTurnTodo.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveCreate.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDelete.md +19 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDoCirculate.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDoForward.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveDoTurnTodo.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveEntrust.md +19 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveIntervention.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveMandatoryArchiving.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveManualCC.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveRefuseRejectApply.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveRejectApply.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveSave.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveSendBack.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveShareEntry.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveTrigger.md +99 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/ApproveWithdraw.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/WorkFlowDocEmpowerment.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/components/workflow/WorkflowToDoc.md +17 -0
- package/skills/weaver-e10-builder/actionflows/references/condition_rules.md +100 -0
- package/skills/weaver-e10-builder/actionflows/references/dynamic_capability_matrix.md +39 -0
- package/skills/weaver-e10-builder/actionflows/references/dynamic_resources.md +41 -0
- package/skills/weaver-e10-builder/actionflows/references/examples/enquiry-form-batch-insert-scene2.json +1043 -0
- package/skills/weaver-e10-builder/actionflows/references/function_rules.md +15 -0
- package/skills/weaver-e10-builder/actionflows/references/trigger_runtime_notes.md +53 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Bar.json +341 -0
- package/skills/weaver-e10-builder/assets/dom-structure/BarStack.json +341 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Bubble.json +323 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Calendar.json +515 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Circular.json +337 -0
- package/skills/weaver-e10-builder/assets/dom-structure/CollapsePanel.json +289 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Comment.json +617 -0
- package/skills/weaver-e10-builder/assets/dom-structure/CountDown.json +755 -0
- package/skills/weaver-e10-builder/assets/dom-structure/DigitalPanel.json +191 -0
- package/skills/weaver-e10-builder/assets/dom-structure/EbButton.json +387 -0
- package/skills/weaver-e10-builder/assets/dom-structure/FastCreate.json +242 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Gauge.json +282 -0
- package/skills/weaver-e10-builder/assets/dom-structure/HBar.json +336 -0
- package/skills/weaver-e10-builder/assets/dom-structure/HBarStack.json +337 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Line.json +341 -0
- package/skills/weaver-e10-builder/assets/dom-structure/NList.json +712 -0
- package/skills/weaver-e10-builder/assets/dom-structure/NavBar.json +274 -0
- package/skills/weaver-e10-builder/assets/dom-structure/NavPanel.json +390 -0
- package/skills/weaver-e10-builder/assets/dom-structure/NewTab.json +606 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Nightingale.json +336 -0
- package/skills/weaver-e10-builder/assets/dom-structure/PageHeader.json +205 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Picture.json +209 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Pie.json +338 -0
- package/skills/weaver-e10-builder/assets/dom-structure/ProgressChart.json +299 -0
- package/skills/weaver-e10-builder/assets/dom-structure/SearchGlobal.json +628 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Slide.json +489 -0
- package/skills/weaver-e10-builder/assets/dom-structure/Treemap.json +324 -0
- package/skills/weaver-e10-builder/assets/factory/chart-dimensions.json +536 -0
- package/skills/weaver-e10-builder/assets/factory/example-btn-progress-collapse-configured.json +2515 -0
- package/skills/weaver-e10-builder/assets/factory/example-configured-2.json +13805 -0
- package/skills/weaver-e10-builder/assets/factory/example-configured.json +6735 -0
- package/skills/weaver-e10-builder/assets/factory/factory-bar-stack.json +326 -0
- package/skills/weaver-e10-builder/assets/factory/factory-bar.json +222 -0
- package/skills/weaver-e10-builder/assets/factory/factory-bubble.json +322 -0
- package/skills/weaver-e10-builder/assets/factory/factory-calendar.json +325 -0
- package/skills/weaver-e10-builder/assets/factory/factory-circular.json +320 -0
- package/skills/weaver-e10-builder/assets/factory/factory-collapse-panel.json +132 -0
- package/skills/weaver-e10-builder/assets/factory/factory-comment.json +388 -0
- package/skills/weaver-e10-builder/assets/factory/factory-count-down.json +198 -0
- package/skills/weaver-e10-builder/assets/factory/factory-digital-panel.json +108 -0
- package/skills/weaver-e10-builder/assets/factory/factory-eb-button.json +150 -0
- package/skills/weaver-e10-builder/assets/factory/factory-fast-create.json +62 -0
- package/skills/weaver-e10-builder/assets/factory/factory-gauge.json +312 -0
- package/skills/weaver-e10-builder/assets/factory/factory-h-bar-stack.json +328 -0
- package/skills/weaver-e10-builder/assets/factory/factory-h-bar.json +326 -0
- package/skills/weaver-e10-builder/assets/factory/factory-line.json +212 -0
- package/skills/weaver-e10-builder/assets/factory/factory-nav-bar.json +167 -0
- package/skills/weaver-e10-builder/assets/factory/factory-nav-menu.json +144 -0
- package/skills/weaver-e10-builder/assets/factory/factory-nav-panel.json +338 -0
- package/skills/weaver-e10-builder/assets/factory/factory-new-tab.json +163 -0
- package/skills/weaver-e10-builder/assets/factory/factory-nightingale.json +312 -0
- package/skills/weaver-e10-builder/assets/factory/factory-nlist.json +112 -0
- package/skills/weaver-e10-builder/assets/factory/factory-page-header.json +171 -0
- package/skills/weaver-e10-builder/assets/factory/factory-picture.json +169 -0
- package/skills/weaver-e10-builder/assets/factory/factory-pie.json +212 -0
- package/skills/weaver-e10-builder/assets/factory/factory-progress-chart.json +162 -0
- package/skills/weaver-e10-builder/assets/factory/factory-search-global.json +104 -0
- package/skills/weaver-e10-builder/assets/factory/factory-slide.json +164 -0
- package/skills/weaver-e10-builder/assets/factory/factory-treemap.json +322 -0
- package/skills/weaver-e10-builder/assets/icons.db +0 -0
- package/skills/weaver-e10-builder/assets/sample/.claude/settings.local.json +8 -0
- package/skills/weaver-e10-builder/assets/sample/api/formApi.js +510 -0
- package/skills/weaver-e10-builder/assets/sample/config/config.js +0 -0
- package/skills/weaver-e10-builder/assets/sample/config/configLoad.js +0 -0
- package/skills/weaver-e10-builder/assets/sample/config/configLoad_default.js +0 -0
- package/skills/weaver-e10-builder/assets/sample/config/config_default.js +0 -0
- package/skills/weaver-e10-builder/assets/sample/entry.js +22 -0
- package/skills/weaver-e10-builder/assets/sample/eslint.config.cjs +48 -0
- package/skills/weaver-e10-builder/assets/sample/memo.md +0 -0
- package/skills/weaver-e10-builder/assets/sample/readme.js +8 -0
- package/skills/weaver-e10-builder/references/app/guide-app-publish.md +92 -0
- package/skills/weaver-e10-builder/references/auth/guide-auth.md +88 -0
- package/skills/weaver-e10-builder/references/coding/guide-edit-backfill.md +189 -0
- package/skills/weaver-e10-builder/references/coding/guide-form-crud.md +262 -0
- package/skills/weaver-e10-builder/references/coding/guide-page-code.md +244 -0
- package/skills/weaver-e10-builder/references/coding/guide-page-export.md +119 -0
- package/skills/weaver-e10-builder/references/coding/ref-compile-rules.md +130 -0
- package/skills/weaver-e10-builder/references/common/guide-plan-template.md +164 -0
- package/skills/weaver-e10-builder/references/common/map.md +153 -0
- package/skills/weaver-e10-builder/references/common/ref-cli-commands.md +635 -0
- package/skills/weaver-e10-builder/references/components/@weapp-Button.md +104 -0
- package/skills/weaver-e10-builder/references/components/base-Badge.md +100 -0
- package/skills/weaver-e10-builder/references/components/base-Dialog.md +358 -0
- package/skills/weaver-e10-builder/references/components/base-Empty.md +58 -0
- package/skills/weaver-e10-builder/references/components/base-Tag.md +112 -0
- package/skills/weaver-e10-builder/references/components/display-Alert.md +64 -0
- package/skills/weaver-e10-builder/references/components/display-Avatar.md +150 -0
- package/skills/weaver-e10-builder/references/components/display-Calendar.md +263 -0
- package/skills/weaver-e10-builder/references/components/display-Carousel.md +115 -0
- package/skills/weaver-e10-builder/references/components/display-Collapse.md +299 -0
- package/skills/weaver-e10-builder/references/components/display-FilePreview.md +49 -0
- package/skills/weaver-e10-builder/references/components/display-Icon.md +66 -0
- package/skills/weaver-e10-builder/references/components/display-List.md +320 -0
- package/skills/weaver-e10-builder/references/components/display-Pagination.md +41 -0
- package/skills/weaver-e10-builder/references/components/display-Spin.md +34 -0
- package/skills/weaver-e10-builder/references/components/display-Steps.md +89 -0
- package/skills/weaver-e10-builder/references/components/display-Table.md +160 -0
- package/skills/weaver-e10-builder/references/components/display-Title.md +117 -0
- package/skills/weaver-e10-builder/references/components/form-Checkbox.md +161 -0
- package/skills/weaver-e10-builder/references/components/form-DepartmentBrowser-api.md +52 -0
- package/skills/weaver-e10-builder/references/components/form-EmployeeBrowser-api.md +51 -0
- package/skills/weaver-e10-builder/references/components/form-EmployeeBrowser.md +51 -0
- package/skills/weaver-e10-builder/references/components/form-Input.md +123 -0
- package/skills/weaver-e10-builder/references/components/form-Radio.md +145 -0
- package/skills/weaver-e10-builder/references/components/form-Select.md +191 -0
- package/skills/weaver-e10-builder/references/components/form-Switch.md +46 -0
- package/skills/weaver-e10-builder/references/components/form-Upload.md +53 -0
- package/skills/weaver-e10-builder/references/components/generic-Echarts.md +102 -0
- package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-components.md +130 -0
- package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-css.md +127 -0
- package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-factory.md +460 -0
- package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer-version.md +111 -0
- package/skills/weaver-e10-builder/references/ebuilder-designer/guide-designer.md +288 -0
- package/skills/weaver-e10-builder/references/esb-interface/guide-import.md +34 -0
- package/skills/weaver-e10-builder/references/form/guide-create-form.md +248 -0
- package/skills/weaver-e10-builder/references/form/guide-ebuilder-filter.md +73 -0
- package/skills/weaver-e10-builder/references/form/guide-ebuilder-scope.md +82 -0
- package/skills/weaver-e10-builder/references/form/guide-field-linkage.md +123 -0
- package/skills/weaver-e10-builder/references/form/guide-form-archive-permission.md +175 -0
- package/skills/weaver-e10-builder/references/form/guide-form-association.md +124 -0
- package/skills/weaver-e10-builder/references/form/guide-form-button.md +169 -0
- package/skills/weaver-e10-builder/references/form/guide-form-data.md +78 -0
- package/skills/weaver-e10-builder/references/form/guide-form-formula-functions.md +343 -0
- package/skills/weaver-e10-builder/references/form/guide-form-layout-incremental.md +90 -0
- package/skills/weaver-e10-builder/references/form/guide-form-layout-planning.md +68 -0
- package/skills/weaver-e10-builder/references/form/guide-form-linkage.md +269 -0
- package/skills/weaver-e10-builder/references/form/guide-form-message.md +74 -0
- package/skills/weaver-e10-builder/references/form/guide-form-physical-table.md +146 -0
- package/skills/weaver-e10-builder/references/form/guide-form-serial-mark.md +47 -0
- package/skills/weaver-e10-builder/references/form/guide-form-sub-detail.md +82 -0
- package/skills/weaver-e10-builder/references/form/guide-form-view-permission.md +38 -0
- package/skills/weaver-e10-builder/references/form/guide-form-view.md +371 -0
- package/skills/weaver-e10-builder/references/form/guide-formula-linkage.md +281 -0
- package/skills/weaver-e10-builder/references/form/guide-list-columns.md +52 -0
- package/skills/weaver-e10-builder/references/form/guide-list-custom-text.md +52 -0
- package/skills/weaver-e10-builder/references/form/guide-list-stats.md +136 -0
- package/skills/weaver-e10-builder/references/form/guide-virtual-form.md +156 -0
- package/skills/weaver-e10-builder/references/form/ref-form-fields.md +293 -0
- package/skills/weaver-e10-builder/references/form/style-templates.md +97 -0
- package/skills/weaver-e10-builder/references/hrm/guide-matrix.md +245 -0
- package/skills/weaver-e10-builder/references/hrm/guide-org-employee.md +155 -0
- package/skills/weaver-e10-builder/references/menu/system-menus.md +75 -0
- package/skills/weaver-e10-builder/references/report/add-dataset-registration-guide.md +139 -0
- package/skills/weaver-e10-builder/references/report/ai-report-workflow.md +412 -0
- package/skills/weaver-e10-builder/references/report/api-report-build-fast-path.md +275 -0
- package/skills/weaver-e10-builder/references/report/app-datasource-lookup-guide.md +230 -0
- package/skills/weaver-e10-builder/references/report/cell-config-echo-guide.md +163 -0
- package/skills/weaver-e10-builder/references/report/cell-transform-guide.md +147 -0
- package/skills/weaver-e10-builder/references/report/condition-format-guide.md +140 -0
- package/skills/weaver-e10-builder/references/report/cross-dataset-filter-guide.md +304 -0
- package/skills/weaver-e10-builder/references/report/dataset-query-filter-guide.md +235 -0
- package/skills/weaver-e10-builder/references/report/dataset-sidebar-ops-guide.md +280 -0
- package/skills/weaver-e10-builder/references/report/dataset-table-field-contract-guide.md +339 -0
- package/skills/weaver-e10-builder/references/report/datasource-metadata-guide.md +365 -0
- package/skills/weaver-e10-builder/references/report/datasource-selection-and-parent-filter-guide.md +148 -0
- package/skills/weaver-e10-builder/references/report/date-range-query-reference.md +102 -0
- package/skills/weaver-e10-builder/references/report/default-header-style-guide.md +70 -0
- package/skills/weaver-e10-builder/references/report/designer-blank-prevention-guide.md +111 -0
- package/skills/weaver-e10-builder/references/report/event-hreflink-guide.md +184 -0
- package/skills/weaver-e10-builder/references/report/field-bind-exact-match-guide.md +208 -0
- package/skills/weaver-e10-builder/references/report/formula-cell-api-guide.md +258 -0
- package/skills/weaver-e10-builder/references/report/guide-report-build.md +131 -0
- package/skills/weaver-e10-builder/references/report/guide-report-modify.md +100 -0
- package/skills/weaver-e10-builder/references/report/guide-report-multisource.md +143 -0
- package/skills/weaver-e10-builder/references/report/guide-report-query.md +98 -0
- package/skills/weaver-e10-builder/references/report/hidden-row-and-header-merge-guide.md +100 -0
- package/skills/weaver-e10-builder/references/report/layout-showlocation-freeze-guide.md +167 -0
- package/skills/weaver-e10-builder/references/report/preaggregate-auto-disable-guide.md +107 -0
- package/skills/weaver-e10-builder/references/report/query-condition-api-guide.md +890 -0
- package/skills/weaver-e10-builder/references/report/ref-report-commands.md +122 -0
- package/skills/weaver-e10-builder/references/report/ref-report-pitfalls.md +226 -0
- package/skills/weaver-e10-builder/references/report/report-baseinfo-defaults-guide.md +117 -0
- package/skills/weaver-e10-builder/references/report/report-building-guide.md +511 -0
- package/skills/weaver-e10-builder/references/report/report-config-schema.md +445 -0
- package/skills/weaver-e10-builder/references/report/report-create-modes-guide.md +182 -0
- package/skills/weaver-e10-builder/references/report/report-data-model.md +447 -0
- package/skills/weaver-e10-builder/references/report/report-delivery-contract.md +101 -0
- package/skills/weaver-e10-builder/references/report/report-designer-feature-map.md +500 -0
- package/skills/weaver-e10-builder/references/report/report-internal-error-guide.md +78 -0
- package/skills/weaver-e10-builder/references/report/report-knowledge-base.md +389 -0
- package/skills/weaver-e10-builder/references/report/report-lifecycle-api-guide.md +392 -0
- package/skills/weaver-e10-builder/references/report/report-list-page-ops-guide.md +191 -0
- package/skills/weaver-e10-builder/references/report/report-permission-guide.md +365 -0
- package/skills/weaver-e10-builder/references/report/report-requirement-analysis-template.md +202 -0
- package/skills/weaver-e10-builder/references/report/report-self-check-guide.md +164 -0
- package/skills/weaver-e10-builder/references/report/safe-partial-save-guide.md +151 -0
- package/skills/weaver-e10-builder/references/report/snowflake-id-precision-guide.md +84 -0
- package/skills/weaver-e10-builder/references/report/spec-build-schema.md +294 -0
- package/skills/weaver-e10-builder/references/report/targeted-fix-guide.md +97 -0
- package/skills/weaver-e10-builder/references/report/user-request-scope-only-guide.md +82 -0
- package/skills/weaver-e10-builder/references/workflow/guide-function-management.md +154 -0
- package/skills/weaver-e10-builder/references/workflow/guide-opinion-settings.md +126 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow-action.md +174 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow-auth-resp.md +224 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow-condition.md +447 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow-node-layout.md +181 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow-odoc-setting.md +248 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow-odoc.md +68 -0
- package/skills/weaver-e10-builder/references/workflow/guide-workflow.md +527 -0
- package/skills/weaver-e10-builder/references/workflow/operator-matrix.md +76 -0
- package/skills/weaver-e10-caizhilian/SKILL.md +7 -2
- package/skills/weaver-e10-caizhilian/references/files.md +15 -0
- package/skills/weaver-e10-caizhilian/references/setting-attach.md +48 -3
- package/skills/weaver-e10-caizhilian/references/source-manifest.json +99 -1
- package/skills/weaver-e10-shared/SKILL.md +8 -4
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +7 -5
- package/skills/weaver-e10-workflow/SKILL.md +23 -44
- package/skills/weaver-e10-workflow/references/source-manifest.json +27 -22
- package/skills/weaver-e10-workflow/references/workflow-create.md +238 -85
- package/skills/weaver-e10-workflow/references/workflow-edit.md +1 -1
- package/skills/weaver-e10-workflow/references/workflow-memory.md +1 -4
- package/skills/weaver-e10-workflow/references/workflow-query.md +2 -2
|
@@ -17,12 +17,14 @@
|
|
|
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 { compactStdoutPaths } from '../stdout-compact.js';
|
|
23
24
|
import { continuationReplayError } from '../continuation.js';
|
|
24
25
|
import { writeStateFileBestEffort } from '../state.js';
|
|
25
|
-
import { buildHistoryRequestData, filterHistoryRequestData } from './read.js';
|
|
26
|
+
import { buildHistoryRequestData, fetchHistoryListSnapshot, filterHistoryRequestData } from './read.js';
|
|
27
|
+
import { hasXiaoEEnv } from '../../../internal/e10/auth/xiaoe.js';
|
|
26
28
|
// ---------------------------------------------------------------------------
|
|
27
29
|
// 常量(与 Python 源逐条对齐)
|
|
28
30
|
// ---------------------------------------------------------------------------
|
|
@@ -58,10 +60,24 @@ const CONTINUATION_TTL_MS = 10 * 60 * 1000;
|
|
|
58
60
|
* 实测反而把模型带进"先看看手里有什么、再决定读不读文件"的分叉:某次真机会话里落的是**只剩指针**
|
|
59
61
|
* 那一档(`fields` / `writableFields` / `editableFields` 全被移出 stdout),模型于是先 grep 找
|
|
60
62
|
* `writableFields`、再读 `requiredFields`、发现 `fields` 是完整对象后又按 17 个字段 id 批量 grep,
|
|
61
|
-
*
|
|
63
|
+
* **从头到尾没读那个文件**。于是改成"超线只回提示"。
|
|
62
64
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
+
* ★ 2026-10-13(P0)修正**过度裁剪**:真机会话里"只回提示"的代价是模型**必然**去读那份落盘文件
|
|
66
|
+
* (`Read(..., limit: 2691)`,79,799 字符 ≈ 20k tokens),而它其实只需要两样东西——"要填哪些字段
|
|
67
|
+
* (含各自的默认值)"和"下一步用哪个 `contextId`"。现在超线时**连同 `contextId` /
|
|
68
|
+
* `historyRequestData` 一起回**,`fields` 则在"挂上后仍不超线"时一并回(见 `compactFormOutput`
|
|
69
|
+
* 的 `always` / `ifFits`),提示也随之从"照抄读命令"改成"不必读、需要运行时/系统字段时才读",
|
|
70
|
+
* 把那次 3.9 s 读取 + ~20k tokens 上下文整个省掉。
|
|
71
|
+
*
|
|
72
|
+
* 2026-10-14:`createForm` 的字段清单合并成唯一一份 `fields` 后,`fields` 自己成了"超线与否"的
|
|
73
|
+
* 主要变量 —— 于是它被归入 `ifFits`:挂得下就挂(模型免读文件),挂不下才在提示里点名要读。
|
|
74
|
+
*
|
|
75
|
+
* ★ 2026-10-14 晚(真机反证,改动 A+B):**宿主截断不受这条 24,000 线约束**。真机会话里 24,417 B
|
|
76
|
+
* 的 `createForm` 输出被宿主**中段截掉 9,330 字符**(头尾保留、中间省略),丢的正是 `fields` 前半段;
|
|
77
|
+
* 而紧凑档判定的结论是"挂得下" ⇒ 提示说"不必读取"、模型却必须去读,**两笔成本都付了**。两处修正:
|
|
78
|
+
* ① 回挂键从 `{always, ifFits}` 改成**有序清单**(`FormOutputKeepEntry[]`),把 `fields` 提到
|
|
79
|
+
* `historyRequestData` 之前 —— 中段截断保留头部,先保"要填哪些字段";② 提示语从断言改成**条件式**
|
|
80
|
+
* ("输出完整就不必读取;若被截断……读一次补齐",见 `resultFileNotice`)。
|
|
65
81
|
*
|
|
66
82
|
* 落盘不可用时没有文件可指,只能保持全量输出(宁大不丢数据),只补一句说明。
|
|
67
83
|
*/
|
|
@@ -69,36 +85,154 @@ const CONTINUATION_TTL_MS = 10 * 60 * 1000;
|
|
|
69
85
|
const READ_WHOLE_OUTPUT_HINT = '读取本输出请一次读完:不要用 head/tail 等管道截断'
|
|
70
86
|
+ '(截断会破坏 JSON,并导致重复执行同一条命令)。';
|
|
71
87
|
/**
|
|
72
|
-
*
|
|
88
|
+
* `createForm` 单次输出的**字符**安全线(2026-10-15 用户裁定)。
|
|
89
|
+
*
|
|
90
|
+
* 与 `STDOUT_SAFE_BYTES`(24,000 **字节**,`editLoad` / 查询类共用)是两个独立口径,不可互相套用:
|
|
91
|
+
* 本线按**字符**计,中文 1 字符 = 3 字节 ⇒ 同样一份表单输出按本线更容易触线。
|
|
92
|
+
* 用户给定的宿主侧限制实现(`terminal_tool.py` 的 `MAX_OUTPUT_LEN`)也按字符计,故取整 20,000。
|
|
73
93
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
94
|
+
* ★ 量的对象是**最终打出去的那份文本**(`finalStdoutChars`:信封 + 紧凑路径),不是美化 JSON
|
|
95
|
+
* —— 后者在 `fields` / `historyRequestData` / `systemFields` 走紧凑路径后能高估约 30%。
|
|
96
|
+
*/
|
|
97
|
+
const CREATE_FORM_STDOUT_SAFE_CHARS = 20_000;
|
|
98
|
+
/**
|
|
99
|
+
* `durationMs` 进判线时用的**位数哨兵**(见 `FormOutputCharBudget.reserve`)。
|
|
100
|
+
*
|
|
101
|
+
* 该值只参与判线的宽度估算、**不会**被下发:真实耗时在压缩之后才求值,判线那一刻只能用位数把它先
|
|
102
|
+
* 占住。8 位 ≈ 27.8 小时,远超单条命令的合理上限 —— 哨兵比真值多几个字符时判线会略微高估,代价是
|
|
103
|
+
* 边界上的表单**提前**落盘;反过来低估才会让宿主按 `MAX_OUTPUT_LEN = 20000` 真截断,那才是要避免的。
|
|
104
|
+
*/
|
|
105
|
+
const DURATION_RESERVE_SENTINEL = 99_999_999;
|
|
106
|
+
/**
|
|
107
|
+
* `createForm` 超线时的**唯一**回执(2026-10-15 用户裁定,措辞由用户逐字给定)。
|
|
108
|
+
*
|
|
109
|
+
* 超线就**不再回 JSON**:完整结果落盘,stdout 只留这一句。`contextId` / `fields` /
|
|
110
|
+
* `historyRequestData` 也随之**一律不回挂** —— 它们在落盘文件里都是完整的,模型读一次文件即可取到
|
|
111
|
+
* (含下一步 `createRequest` 要用的 `contextId`)。这与 `editLoad` 那一档(同为"只回提示")形态一致。
|
|
112
|
+
*/
|
|
113
|
+
function createFormResultFileNotice(resultFile) {
|
|
114
|
+
return `当前表单内容较大,已临时写入文件 ${resultFile},请直接读取 ${resultFile} 获取完整结果`;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* 超线时的**提示行**(措辞由用户 2026-09-29 首定,2026-10-13 P0 改为"不必读"口径)。
|
|
118
|
+
*
|
|
119
|
+
* 用户 2026-09-29 的原始要求:提示里**直接给出可执行的读命令**(工具 + `file_path` + 精确 `limit`),
|
|
120
|
+
* 避免模型自己把 `limit` 给小了、于是分段读好几次 —— 这条保留。
|
|
121
|
+
*
|
|
122
|
+
* P0 起:`contextId` / `historyRequestData` 已随超线档一起返回,所以 **Read 不是必经步骤**,
|
|
123
|
+
* 只在需要运行时 / 系统字段这类细节时才用;首句先说清"不必读取",否则模型仍会照抄去读(那正是要省的)。
|
|
124
|
+
*
|
|
125
|
+
* 2026-10-14:`fields`(字段清单 + 默认值)也可能被挂在超线档里(`ifFits` 口径)。**挂上才有资格说
|
|
126
|
+
* "不必读取"**——挂不上时提示语要反过来明确要模型去读,否则它会拿着一份没有字段清单的 stdout 硬拼
|
|
127
|
+
* `data`。`fieldsIncluded` 就是按"最终到底挂没挂上 `fields`"传进来的。
|
|
128
|
+
*
|
|
129
|
+
* ★ 2026-10-14 晚(改动 A):上述"挂上就说不必读取"是**断言**,而它的隐含前提——"内联出去的
|
|
130
|
+
* ~24 KB 一定能被模型完整看到"——被真机会话证伪:宿主把 24,417 B 的输出**中段截掉 9,330 字符**,
|
|
131
|
+
* 挂在里面的 `fields` 前半段整段消失。于是 `fieldsIncluded` 为真时不再断言"不必读取",改成**条件式**:
|
|
132
|
+
* 输出完整就不必读,**若被截断**(出现「已截断…字符」提示 / 字段清单不完整)则读一次补齐。
|
|
133
|
+
* 判据本身仍保留——它决定的是"该不该说可以省掉这次读取",不是"输出一定完整"。
|
|
77
134
|
*
|
|
78
135
|
* `lines` 是落盘文件的总行数,正好是一次读完所需的 `limit`。文件读取工具不受宿主 stdout 那条
|
|
79
136
|
* ~30 KB 内联线约束(实测 76 KB / 238 KB 都能一次读回)。
|
|
80
137
|
*/
|
|
81
|
-
function resultFileNotice(resultFile, lines) {
|
|
82
|
-
|
|
83
|
-
|
|
138
|
+
function resultFileNotice(resultFile, lines, fieldsIncluded = true) {
|
|
139
|
+
const head = `表单内容较大,完整表单已写入临时文件(resultFile:${resultFile})。`;
|
|
140
|
+
const advice = fieldsIncluded
|
|
141
|
+
? '**输出完整就不必读取**:字段清单(含 `initialValue`)与历史参考值都已随 stdout 下发,下一步 createRequest 用 contextId 即可;'
|
|
142
|
+
+ '**若本输出被截断**(出现「已截断…字符」之类提示,或字段清单明显不完整),说明前部被省略的正是拼 `data` 需要的字段 —— 按下面命令读一次补齐(不要分段读):'
|
|
143
|
+
: '**字段清单未随 stdout 下发**(它本身就超了内联线)——拼 `data` 前必须把它读一次(不要分段读):';
|
|
144
|
+
return head + advice + `\nRead(file_path: "${resultFile}", limit: ${lines})`;
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* 量「这份 `data` 落进 stdout 后,宿主会看到多少字符」——**与最终输出同源的那一份文本**。
|
|
148
|
+
*
|
|
149
|
+
* 为什么不能沿用"美化 JSON 的 `.length`":紧凑路径(`fields` / `historyRequestData` /
|
|
150
|
+
* `systemFields`,见 `stdout-compact.ts`)会把这几块的缩进空白整段压掉,实测 `createForm`
|
|
151
|
+
* 整份实测小 **约 30%**(`hugeLayout(120)` 桩 23,522 → 16,613 字符)。判线若还按美化量就会系统性
|
|
152
|
+
* 高估 —— 该内联的表单被判超线落盘。
|
|
153
|
+
*
|
|
154
|
+
* 信封形状必须与 `shortcuts/workflow/index.ts` 的 `successEnvelope` **同形**(`createForm` 不带
|
|
155
|
+
* `meta`,故此处没有该键):测试用真的 `successEnvelope` + `stringifyForStdout` 复算 `outputChars`
|
|
156
|
+
* 把它锁住,`successEnvelope` 一改形状这条用例就会红。
|
|
157
|
+
*/
|
|
158
|
+
function finalStdoutChars(data, operation) {
|
|
159
|
+
const envelope = { schemaVersion: 1, ok: true, operation, data, warnings: [] };
|
|
160
|
+
return stdoutCharsOf(envelope, compactStdoutPaths(operation));
|
|
84
161
|
}
|
|
85
|
-
|
|
162
|
+
/**
|
|
163
|
+
* 大表单输出裁剪:超安全线就落盘 + 只回指针。
|
|
164
|
+
*
|
|
165
|
+
* `keep` 是**回挂**给调用方的口子(2026-10-14 起;当晚从 `{always, ifFits}` 两桶改成**有序清单**):
|
|
166
|
+
* - 不给 `ifFits`:无论多大都必须留在 stdout —— 它们是"不读那份文件也能继续干活"的全部依据;
|
|
167
|
+
* - 给 `ifFits: true`:**挂上后仍在安全线内才留**(如 `fields`)。真·巨型表单挂不回去,此时只能让
|
|
168
|
+
* 模型去读文件,提示语也随最终是否挂上 `fields` 改写(见 `resultFileNotice`)。
|
|
169
|
+
*
|
|
170
|
+
* ★ 顺序为什么是契约(2026-10-14 晚,真机反证):宿主可能**中段截断**(头尾保留、中间省略),
|
|
171
|
+
* 所以"排在前面的键先保"。真机那次把 `fields` 排在 `historyRequestData` **之后**,截断正好吃掉
|
|
172
|
+
* `fields` 前半段 ⇒ 现在规定 `contextId` → `fields` → `historyRequestData` 的次序。
|
|
173
|
+
*
|
|
174
|
+
* 传 `charBudget` 则进**严格档**(`createForm` 专用,2026-10-15 用户裁定):改用**字符**口径判线,
|
|
175
|
+
* 且超线时**只回那一句提示**、`keep` 整份失效 —— 与上面默认档「回挂干活必需键」的口径**正好相反**,
|
|
176
|
+
* 两者不要互相套用。`editLoad` 走默认档(字节口径 + 无 keep),行为不变。
|
|
177
|
+
*/
|
|
178
|
+
async function compactFormOutput(result, env, resultFileName, keep = [], charBudget) {
|
|
179
|
+
const entries = keep.filter((entry) => entry && entry.value !== undefined);
|
|
180
|
+
const entriesObject = (list) => {
|
|
181
|
+
const out = {};
|
|
182
|
+
for (const entry of list)
|
|
183
|
+
out[entry.key] = entry.value;
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
// ★ 严格档(`createForm`):**字符**口径判线;超线时 stdout 只留那一句提示。
|
|
187
|
+
if (charBudget) {
|
|
188
|
+
// `chars` 是**原值**:`outputChars` 自述的就是"这份 `data` 有多大",口径必须与落盘文件里那份
|
|
189
|
+
// 载荷严格一致(用例 `outputChars 必须等于「最终 stdout 文本」的字符数` 锁着这一条)。
|
|
190
|
+
const chars = finalStdoutChars(result, charBudget.operation);
|
|
191
|
+
// `measured` 只在阈值比较里用:`reserve` 里的键(`durationMs`)"结果里会有、判线时还没有值",
|
|
192
|
+
// 按位数用哨兵把它占住(见 `FormOutputCharBudget.reserve`)——宁可高估提前落盘,不可低估被截断。
|
|
193
|
+
const measured = charBudget.reserve
|
|
194
|
+
? finalStdoutChars({ ...result, ...charBudget.reserve }, charBudget.operation)
|
|
195
|
+
: chars;
|
|
196
|
+
// 未超线 ⇒ 原样返回完整结果(与默认档同形,`keep` 本就是 `result` 里已有的键)。
|
|
197
|
+
if (measured <= charBudget.safeChars)
|
|
198
|
+
return { ...result, ...entriesObject(entries) };
|
|
199
|
+
const payload = JSON.stringify(result, null, 2);
|
|
200
|
+
const resultFile = await writeStateFileBestEffort(resultFileName, payload, env.ctx);
|
|
201
|
+
if (!resultFile) {
|
|
202
|
+
// 落盘不可用 ⇒ 没有文件可指,只能保持全量输出(宁大不丢数据),与默认档同口径。
|
|
203
|
+
return {
|
|
204
|
+
...result,
|
|
205
|
+
...entriesObject(entries),
|
|
206
|
+
outputChars: chars,
|
|
207
|
+
largeOutputNotice: READ_WHOLE_OUTPUT_HINT,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// 只回那一句(`resultFile` 排最前,被截断也看得见)。
|
|
211
|
+
return withLeadingMeta({
|
|
212
|
+
resultFile,
|
|
213
|
+
persistedResult: true,
|
|
214
|
+
outputChars: chars,
|
|
215
|
+
largeOutputNotice: createFormResultFileNotice(resultFile),
|
|
216
|
+
}, {});
|
|
217
|
+
}
|
|
86
218
|
const bytes = stdoutBytesOf(result);
|
|
87
219
|
// 未超本地安全线 ⇒ 原样返回,不做任何裁剪、也不落盘。
|
|
88
220
|
if (bytes <= STDOUT_SAFE_BYTES)
|
|
89
|
-
return result;
|
|
221
|
+
return { ...result, ...entriesObject(entries) };
|
|
90
222
|
const payload = JSON.stringify(result, null, 2);
|
|
91
223
|
const resultFile = await writeStateFileBestEffort(resultFileName, payload, env.ctx);
|
|
92
224
|
if (!resultFile) {
|
|
93
|
-
return { ...result, outputBytes: bytes, largeOutputNotice: READ_WHOLE_OUTPUT_HINT };
|
|
225
|
+
return { ...result, ...entriesObject(entries), outputBytes: bytes, largeOutputNotice: READ_WHOLE_OUTPUT_HINT };
|
|
94
226
|
}
|
|
227
|
+
const lines = payload.split('\n').length;
|
|
95
228
|
// 只回提示:路径说明 + 读命令(`resultFile` 排最前,被截断也看得见)。
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
229
|
+
const pointer = () => withLeadingMeta({ resultFile, persistedResult: true, outputBytes: bytes, largeOutputNotice: '' }, {});
|
|
230
|
+
const withOptional = { ...pointer(), ...entriesObject(entries) };
|
|
231
|
+
const final = entries.some((entry) => entry.ifFits) && stdoutBytesOf(withOptional) > STDOUT_SAFE_BYTES
|
|
232
|
+
? { ...pointer(), ...entriesObject(entries.filter((entry) => !entry.ifFits)) }
|
|
233
|
+
: withOptional;
|
|
234
|
+
final.largeOutputNotice = resultFileNotice(resultFile, lines, 'fields' in final && Boolean(final.fields));
|
|
235
|
+
return final;
|
|
102
236
|
}
|
|
103
237
|
async function requestJson(env, method, path, query, body, options = {}) {
|
|
104
238
|
const response = await env.host.workflowRequest({
|
|
@@ -583,15 +717,6 @@ function slimFieldInfo(field) {
|
|
|
583
717
|
slim.optionsTruncated = true;
|
|
584
718
|
return slim;
|
|
585
719
|
}
|
|
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
720
|
// ---------------------------------------------------------------------------
|
|
596
721
|
// 附件上传限制解析
|
|
597
722
|
// ---------------------------------------------------------------------------
|
|
@@ -858,6 +983,240 @@ function buildFileUploadParams(runtime, frame, fieldId, isCreate, secretLevelId,
|
|
|
858
983
|
params.maxNum = maxNum;
|
|
859
984
|
return params;
|
|
860
985
|
}
|
|
986
|
+
/**
|
|
987
|
+
* 布局里**隐藏行**上的字段(2026-10-14 用户口径,V12.1.112)。
|
|
988
|
+
*
|
|
989
|
+
* 用户在表单上看到的是一张 Excel 表:`config.workbook` 每个「节点」都有自己的行列网格——
|
|
990
|
+
* `Sheet1` 是**页面根网格**,其余键是**容器组件自己的网格**(如明细表 `DetailTable` 的内部网格)。
|
|
991
|
+
* `sheet.rows[i].visible === false` ⇒ **该整行内容在前台一律不可见**。
|
|
992
|
+
*
|
|
993
|
+
* 为什么必须补这条:`流程类型` / `打印状态` 在 `fields[]` 与 `viewRelateConfig` 里跟普通业务
|
|
994
|
+
* Select(如 `报销方式`)**逐键同形**——`readOnly`/`disableEdit` 都是 false、`display` 也是 true、
|
|
995
|
+
* 连默认值都有,载荷里**没有任何"这是隐藏字段"的标记**。它们唯一的不同就是**落在隐藏行上**
|
|
996
|
+
* (真机 总部费用报销:两者都在 `Sheet1.rows` 第 76 行,该行 `visible:false`,整块 74~83 行全隐藏)。
|
|
997
|
+
*
|
|
998
|
+
* 判定必须**沿容器链往上走**:组件的 `config.excel.row` 是**相对其父容器**的坐标
|
|
999
|
+
* (`excel.parent === 'ROOT'` 才是页面根网格),父容器本身还可能是一个组件——真机
|
|
1000
|
+
* `单据状态` / `收单日期` / `到达出纳日期` / `分部` 这些字段就挂在 `MultiContent` 组合控件里,
|
|
1001
|
+
* 是组合控件本体(第 76 行)被藏了 ⇒ 它内部所有字段用户同样看不到(用户原话:"该行的所有内容
|
|
1002
|
+
* 在表单上都是隐藏的")。任一层落在隐藏行上即判隐藏。
|
|
1003
|
+
*
|
|
1004
|
+
* 查不到对应网格时(老布局 / 节点缺失 / `workbook` 整个没有)**不判隐藏**——fail-open,
|
|
1005
|
+
* 与 `compsFieldIds` 为空时不筛同一降级口径:宁多返字段,不可误删用户真能看到的字段。
|
|
1006
|
+
*/
|
|
1007
|
+
function collectHiddenRowFieldIds(page0, comps) {
|
|
1008
|
+
const hiddenRowFieldIds = new Set();
|
|
1009
|
+
const workbook = page0?.config?.workbook;
|
|
1010
|
+
if (!workbook || typeof workbook !== 'object')
|
|
1011
|
+
return hiddenRowFieldIds;
|
|
1012
|
+
const rootSheet = workbook.Sheet1;
|
|
1013
|
+
// nodeId → 组件:组件的 `excel.parent` 指向父容器的 nodeId,靠它才能找回父容器自己的网格。
|
|
1014
|
+
const compByNodeId = new Map();
|
|
1015
|
+
for (const comp of toArray(comps)) {
|
|
1016
|
+
if (!comp || typeof comp !== 'object')
|
|
1017
|
+
continue;
|
|
1018
|
+
const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
|
|
1019
|
+
if (nodeId)
|
|
1020
|
+
compByNodeId.set(nodeId, comp);
|
|
1021
|
+
}
|
|
1022
|
+
const rowHidden = (sheet, row) => {
|
|
1023
|
+
const rows = sheet && Array.isArray(sheet.rows) ? sheet.rows : [];
|
|
1024
|
+
const index = Number(row);
|
|
1025
|
+
if (!Number.isInteger(index) || index < 0 || index >= rows.length)
|
|
1026
|
+
return false;
|
|
1027
|
+
const entry = rows[index];
|
|
1028
|
+
return Boolean(entry) && typeof entry === 'object' && entry.visible === false;
|
|
1029
|
+
};
|
|
1030
|
+
for (const comp of toArray(comps)) {
|
|
1031
|
+
if (!comp || typeof comp !== 'object')
|
|
1032
|
+
continue;
|
|
1033
|
+
const fieldId = String(comp.config?.fieldId ?? '').trim();
|
|
1034
|
+
if (!fieldId)
|
|
1035
|
+
continue;
|
|
1036
|
+
let cursor = comp;
|
|
1037
|
+
while (cursor) {
|
|
1038
|
+
const excel = cursor.config?.excel;
|
|
1039
|
+
if (!excel || typeof excel !== 'object')
|
|
1040
|
+
break;
|
|
1041
|
+
const parentId = String(excel.parent ?? '').trim();
|
|
1042
|
+
if (!parentId || parentId === 'ROOT') {
|
|
1043
|
+
if (rowHidden(rootSheet, excel.row))
|
|
1044
|
+
hiddenRowFieldIds.add(fieldId);
|
|
1045
|
+
break;
|
|
1046
|
+
}
|
|
1047
|
+
const owner = compByNodeId.get(parentId);
|
|
1048
|
+
const sheet = owner ? workbook[String(owner.id ?? '')] : null;
|
|
1049
|
+
if (sheet && rowHidden(sheet, excel.row)) {
|
|
1050
|
+
hiddenRowFieldIds.add(fieldId);
|
|
1051
|
+
break;
|
|
1052
|
+
}
|
|
1053
|
+
cursor = owner;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
return hiddenRowFieldIds;
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* 明细表**隐藏列**上的字段(2026-10-14 用户口径,V12.1.116)。
|
|
1060
|
+
*
|
|
1061
|
+
* 明细表在 `comps[]` 里是 `type === 'DetailTable'` 的组件:
|
|
1062
|
+
* `comp.id` 是它在 `pageLayouts[0].config.workbook` 里的**网格 key**(用户口径里的「标签页 key」),
|
|
1063
|
+
* `comp.config.subFormId`(真机同 `comp.refBusinessId`)是该明细表自己的 id。
|
|
1064
|
+
* 该网格 `columns[n].visible === false` ⇒ **第 n 列整列在表单上不显示**,列上的字段用户在前台
|
|
1065
|
+
* 看不到;载荷里与隐藏行一样**没有任何"隐藏"标记**(只有布局知道)。真机实例(总部费用报销):
|
|
1066
|
+
* 一张 34 列的明细表有 22 列 `visible:false`,涉及 27 个字段。
|
|
1067
|
+
*
|
|
1068
|
+
* 列 → 字段的寻址与隐藏行同一套:字段组件的 `config.excel.parent` 指向**明细表组件的
|
|
1069
|
+
* `excel.nodeId`**、`config.excel.col` 是**列号**(0 起,与 `columns` 下标同口径)。沿容器链往上走,
|
|
1070
|
+
* 任一层落在明细网格的隐藏列上即判隐藏(明细表内的组合控件落在隐藏列时,其内部字段同样看不到)。
|
|
1071
|
+
*
|
|
1072
|
+
* **只认明细表网格**:页面根网格(`Sheet1`)的列隐藏不在本口径内(真机无此态)。
|
|
1073
|
+
* 查不到明细网格 / `columns` 缺失时**不判隐藏**——fail-open,与隐藏行同一降级口径。
|
|
1074
|
+
*/
|
|
1075
|
+
function collectHiddenDetailColumnFieldIds(page0, comps) {
|
|
1076
|
+
const hiddenColFieldIds = new Set();
|
|
1077
|
+
const workbook = page0?.config?.workbook;
|
|
1078
|
+
if (!workbook || typeof workbook !== 'object')
|
|
1079
|
+
return hiddenColFieldIds;
|
|
1080
|
+
const sheets = workbook;
|
|
1081
|
+
// nodeId → 组件(组件的 `excel.parent` 指向父容器的 nodeId);同时挑出明细表的网格 key。
|
|
1082
|
+
const compByNodeId = new Map();
|
|
1083
|
+
const detailSheetKeys = new Set();
|
|
1084
|
+
for (const comp of toArray(comps)) {
|
|
1085
|
+
if (!comp || typeof comp !== 'object')
|
|
1086
|
+
continue;
|
|
1087
|
+
const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
|
|
1088
|
+
if (nodeId)
|
|
1089
|
+
compByNodeId.set(nodeId, comp);
|
|
1090
|
+
const type = String(comp.type ?? comp.config?.type ?? '').trim().toLowerCase();
|
|
1091
|
+
if (type !== 'detailtable')
|
|
1092
|
+
continue;
|
|
1093
|
+
const sheetKey = String(comp.id ?? '').trim();
|
|
1094
|
+
if (sheetKey && sheets[sheetKey] && typeof sheets[sheetKey] === 'object')
|
|
1095
|
+
detailSheetKeys.add(sheetKey);
|
|
1096
|
+
}
|
|
1097
|
+
if (!detailSheetKeys.size)
|
|
1098
|
+
return hiddenColFieldIds;
|
|
1099
|
+
const colHidden = (sheet, col) => {
|
|
1100
|
+
const columns = sheet && Array.isArray(sheet.columns) ? sheet.columns : [];
|
|
1101
|
+
const index = Number(col);
|
|
1102
|
+
if (!Number.isInteger(index) || index < 0 || index >= columns.length)
|
|
1103
|
+
return false;
|
|
1104
|
+
const entry = columns[index];
|
|
1105
|
+
return Boolean(entry) && typeof entry === 'object' && entry.visible === false;
|
|
1106
|
+
};
|
|
1107
|
+
for (const comp of toArray(comps)) {
|
|
1108
|
+
if (!comp || typeof comp !== 'object')
|
|
1109
|
+
continue;
|
|
1110
|
+
const fieldId = String(comp.config?.fieldId ?? '').trim();
|
|
1111
|
+
if (!fieldId)
|
|
1112
|
+
continue;
|
|
1113
|
+
let cursor = comp;
|
|
1114
|
+
while (cursor) {
|
|
1115
|
+
const excel = cursor.config?.excel;
|
|
1116
|
+
if (!excel || typeof excel !== 'object')
|
|
1117
|
+
break;
|
|
1118
|
+
const parentId = String(excel.parent ?? '').trim();
|
|
1119
|
+
if (!parentId || parentId === 'ROOT')
|
|
1120
|
+
break; // 页面根网格不参与本口径
|
|
1121
|
+
const owner = compByNodeId.get(parentId);
|
|
1122
|
+
if (!owner)
|
|
1123
|
+
break;
|
|
1124
|
+
const ownerId = String(owner.id ?? '').trim();
|
|
1125
|
+
if (detailSheetKeys.has(ownerId)) {
|
|
1126
|
+
if (colHidden(sheets[ownerId], excel.col))
|
|
1127
|
+
hiddenColFieldIds.add(fieldId);
|
|
1128
|
+
break;
|
|
1129
|
+
}
|
|
1130
|
+
cursor = owner;
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
return hiddenColFieldIds;
|
|
1134
|
+
}
|
|
1135
|
+
/**
|
|
1136
|
+
* 多内容单元格(`MultiContent`)内部**被隐藏的单元格**上的字段(2026-10-14 用户口径,V12.1.117)。
|
|
1137
|
+
*
|
|
1138
|
+
* `comps[]` 里 `type === 'MultiContent'` 是「多内容单元格」组合控件:它把若干子字段摆在自己
|
|
1139
|
+
* **内嵌的**单元格网格里——`config.excel.multiCells`,形如 `[row][col]` 的二维数组(空格子为 `null`),
|
|
1140
|
+
* 每个单元格形如 `{ nodeId, value, style, tag }`。`cell.nodeId` 与子字段组件的
|
|
1141
|
+
* `config.excel.nodeId` 一一对应(真机 41/41 命中,且 `multiCells` 的行列下标与子字段
|
|
1142
|
+
* `excel.row` / `excel.col` 也 41/41 一致)。单元格 `cell.tag.attrs.hide === true`
|
|
1143
|
+
* ⇒ **该单元格里的字段内容在前台不可见**;载荷里与隐藏行 / 隐藏列一样**没有任何"隐藏"标记**,
|
|
1144
|
+
* 只有布局知道。真机实例(总部费用报销):`部门;部门ID` 这个多内容单元格里第二个格子
|
|
1145
|
+
* (`部门ID`,`fieldId 100003720000001374`)就是 `tag.attrs.hide === true`,而同格的
|
|
1146
|
+
* `部门`(`100003720000000074`)照常可见。
|
|
1147
|
+
*
|
|
1148
|
+
* **`MultiContent` 不在 `config.workbook` 里**(它没有自己的 sheet),所以隐藏行 / 隐藏列的
|
|
1149
|
+
* 网格口径天然覆盖不到它——必须单独读内嵌的 `multiCells`。
|
|
1150
|
+
*
|
|
1151
|
+
* 寻址与隐藏行 / 隐藏列同一套:沿容器链往上走,任一层的组件若是 `MultiContent` 且其 `multiCells` 里
|
|
1152
|
+
* 有指向当前 cursor 的 `nodeId` 且该单元格 `hide === true` 即判隐藏(多内容里再嵌组合控件时,
|
|
1153
|
+
* 其内部字段同样看不到)。`multiCells` 缺失 / 结构异常时**不判隐藏**——fail-open,同一降级口径。
|
|
1154
|
+
*/
|
|
1155
|
+
function collectHiddenMultiContentFieldIds(comps) {
|
|
1156
|
+
const hiddenFieldIds = new Set();
|
|
1157
|
+
const list = toArray(comps);
|
|
1158
|
+
// nodeId → 组件:组件的 `excel.parent` 指向父容器的 nodeId,靠它才能沿容器链往上走。
|
|
1159
|
+
const compByNodeId = new Map();
|
|
1160
|
+
// **被隐藏的单元格**所对应的子组件 nodeId 集合(`multiCells` 里 `tag.attrs.hide === true` 的那批)。
|
|
1161
|
+
const hiddenCellNodeIds = new Set();
|
|
1162
|
+
for (const comp of list) {
|
|
1163
|
+
if (!comp || typeof comp !== 'object')
|
|
1164
|
+
continue;
|
|
1165
|
+
const nodeId = String(comp.config?.excel?.nodeId ?? '').trim();
|
|
1166
|
+
if (nodeId)
|
|
1167
|
+
compByNodeId.set(nodeId, comp);
|
|
1168
|
+
}
|
|
1169
|
+
for (const comp of list) {
|
|
1170
|
+
if (!comp || typeof comp !== 'object')
|
|
1171
|
+
continue;
|
|
1172
|
+
const type = String(comp.type ?? comp.config?.type ?? '').trim().toLowerCase();
|
|
1173
|
+
if (type !== 'multicontent')
|
|
1174
|
+
continue;
|
|
1175
|
+
const cells = comp.config?.excel?.multiCells;
|
|
1176
|
+
if (!Array.isArray(cells))
|
|
1177
|
+
continue;
|
|
1178
|
+
for (const row of cells) {
|
|
1179
|
+
// `multiCells` 是 `[row][col]` 二维数组,空格子是 `null`。
|
|
1180
|
+
if (!Array.isArray(row))
|
|
1181
|
+
continue;
|
|
1182
|
+
for (const cell of row) {
|
|
1183
|
+
if (!cell || typeof cell !== 'object')
|
|
1184
|
+
continue;
|
|
1185
|
+
const attrs = cell.tag?.attrs;
|
|
1186
|
+
if (!attrs || typeof attrs !== 'object' || attrs.hide !== true)
|
|
1187
|
+
continue;
|
|
1188
|
+
const cellNodeId = String(cell.nodeId ?? '').trim();
|
|
1189
|
+
if (cellNodeId)
|
|
1190
|
+
hiddenCellNodeIds.add(cellNodeId);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
}
|
|
1194
|
+
if (!hiddenCellNodeIds.size)
|
|
1195
|
+
return hiddenFieldIds;
|
|
1196
|
+
for (const comp of list) {
|
|
1197
|
+
if (!comp || typeof comp !== 'object')
|
|
1198
|
+
continue;
|
|
1199
|
+
const fieldId = String(comp.config?.fieldId ?? '').trim();
|
|
1200
|
+
if (!fieldId)
|
|
1201
|
+
continue;
|
|
1202
|
+
let cursor = comp;
|
|
1203
|
+
while (cursor) {
|
|
1204
|
+
const excel = cursor.config?.excel;
|
|
1205
|
+
if (!excel || typeof excel !== 'object')
|
|
1206
|
+
break;
|
|
1207
|
+
const nodeId = String(excel.nodeId ?? '').trim();
|
|
1208
|
+
if (nodeId && hiddenCellNodeIds.has(nodeId)) {
|
|
1209
|
+
hiddenFieldIds.add(fieldId);
|
|
1210
|
+
break;
|
|
1211
|
+
}
|
|
1212
|
+
const parentId = String(excel.parent ?? '').trim();
|
|
1213
|
+
if (!parentId || parentId === 'ROOT')
|
|
1214
|
+
break;
|
|
1215
|
+
cursor = compByNodeId.get(parentId) ?? null;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
return hiddenFieldIds;
|
|
1219
|
+
}
|
|
861
1220
|
function parseLayoutMeta(layoutData) {
|
|
862
1221
|
const hiddenCellIds = new Set();
|
|
863
1222
|
const compsFieldIds = new Set();
|
|
@@ -918,6 +1277,15 @@ function parseLayoutMeta(layoutData) {
|
|
|
918
1277
|
layoutSubformIds.add(sid);
|
|
919
1278
|
}
|
|
920
1279
|
}
|
|
1280
|
+
// 行隐藏 / 单元格隐藏 / 明细表隐藏列 / 多内容单元格隐藏**同义**(都是"用户在表单上看不到"),
|
|
1281
|
+
// 并成一份 `hiddenCellIds`:下游 `visibleFields`/`initialValue`/`missingRequiredFields`/「已填内容」
|
|
1282
|
+
// /联动提示全部只认这一份,且随句柄以 `hiddenCellFieldIds` 传给 `createRequest`,两边口径天然一致。
|
|
1283
|
+
for (const fid of collectHiddenRowFieldIds(page0, page0.comps))
|
|
1284
|
+
hiddenCellIds.add(fid);
|
|
1285
|
+
for (const fid of collectHiddenDetailColumnFieldIds(page0, page0.comps))
|
|
1286
|
+
hiddenCellIds.add(fid);
|
|
1287
|
+
for (const fid of collectHiddenMultiContentFieldIds(page0.comps))
|
|
1288
|
+
hiddenCellIds.add(fid);
|
|
921
1289
|
}
|
|
922
1290
|
return { hiddenCellIds, compsFieldIds, layoutSubformIds };
|
|
923
1291
|
}
|
|
@@ -940,7 +1308,8 @@ function applyViewRelateHidden(layoutSubformIds, viewRelateConfig) {
|
|
|
940
1308
|
* `viewRelateConfig` 长度为 0"并非服务端没配置,而是 `getFormLayout` 请求多带了顶层 `permissionId`
|
|
941
1309
|
* (未启用节点权限的表单会因此拿不到字段级属性;见 `buildLayoutRequest`)。该请求口径修正后,
|
|
942
1310
|
* 本告警只在服务端**确实**不下发字段属性时才出现(如启用节点权限的表单)。
|
|
943
|
-
*
|
|
1311
|
+
* 真正未下发时:服务端没有字段级的 required / disableEdit / display 配置,`fields` 里的只读 / 禁止编辑
|
|
1312
|
+
* 判定与「标记未下发」都因此失真,**不代表表单没有必填项**。
|
|
944
1313
|
*
|
|
945
1314
|
* V12.1.27 纠正(此前措辞有硬伤):原文案把 `missingFields` 也写成"为空不代表没有必填/无法据此校验业务必填",
|
|
946
1315
|
* 与事实相反——本租户的业务必填多为**条件必填**(`viewLinkages` 的 `required` 规则),
|
|
@@ -950,13 +1319,16 @@ function applyViewRelateHidden(layoutSubformIds, viewRelateConfig) {
|
|
|
950
1319
|
*
|
|
951
1320
|
* 2026-09-23:`createForm` 不再下发联动规则(原 `conditionalRequired`),必填判断一律以 `missingFields` 为准。
|
|
952
1321
|
*
|
|
953
|
-
*
|
|
1322
|
+
* 2026-10-14:`createForm` 不再下发任何必填清单(`requiredFields` / `subFormRequired` / `fillPlan` 全撤),
|
|
1323
|
+
* 本告警的措辞随之改成"静态权限信息没拿到",不再点名那三个已不存在的键。
|
|
1324
|
+
*
|
|
1325
|
+
* 返回 null 表示静态权限信息可用,无需告警。
|
|
954
1326
|
*/
|
|
955
1327
|
function requiredInfoWarningText(context) {
|
|
956
1328
|
const ctx = context || {};
|
|
957
1329
|
const vrc = Array.isArray(ctx.viewRelateConfig) ? ctx.viewRelateConfig : [];
|
|
958
1330
|
if (vrc.length)
|
|
959
|
-
return null; // OA 下发了字段级权限配置 →
|
|
1331
|
+
return null; // OA 下发了字段级权限配置 → 字段的只读 / 可见判定可信
|
|
960
1332
|
const fields = Array.isArray(ctx.fields) ? ctx.fields : [];
|
|
961
1333
|
const business = fields.filter((f) => f && f.id
|
|
962
1334
|
&& !String(f.id).startsWith('REQUEST_') && !String(f.id).startsWith('SECLEVEL')
|
|
@@ -967,10 +1339,10 @@ function requiredInfoWarningText(context) {
|
|
|
967
1339
|
// 有任何字段带必填标记 → 至少拿到了部分真实信息,不必告警
|
|
968
1340
|
if (business.some((f) => f.required))
|
|
969
1341
|
return null;
|
|
970
|
-
return 'OA
|
|
1342
|
+
return 'OA 未下发该表单的**静态**字段权限配置:本次 `fields` 里的只读 / 禁止编辑判定可能偏保守,'
|
|
971
1343
|
+ '但这**不代表该表单没有业务必填项**——业务必填多为「条件必填」,只有按实际所填数据才判定得出。'
|
|
972
|
-
+ '
|
|
973
|
-
+ '
|
|
1344
|
+
+ '必填一律以 createRequest 返回的 `missingFields` 为准(按实际所填数据实时计算、**可信**):'
|
|
1345
|
+
+ '照常发起,返回 NEEDS_INPUT 时把 missingFields 一次性向用户问齐。'
|
|
974
1346
|
+ '不要对用户说"系统不会校验必填项"。';
|
|
975
1347
|
}
|
|
976
1348
|
// ---------------------------------------------------------------------------
|
|
@@ -1281,6 +1653,10 @@ async function loadCreateFormPack(workflowId, env) {
|
|
|
1281
1653
|
return { data, launchPreferences };
|
|
1282
1654
|
}
|
|
1283
1655
|
async function commandCreateForm(args, env) {
|
|
1656
|
+
// ★ 2026-10-16 用户裁定:返回值里固定带上**本命令自身的耗时**(毫秒)。
|
|
1657
|
+
// 起点取在函数入口(含下面那次会话惰性初始化),终点取在结果产出之后 —— 即"从命令开始执行到
|
|
1658
|
+
// 执行完成"。取值不做开关、不进 skill 文档(用户明确要求"直接默认加上")。
|
|
1659
|
+
const startedAt = Date.now();
|
|
1284
1660
|
// 会话是**惰性**加载的(`ensureSession` 只在首次调用时填充)。本命令随后会并发发起多个请求,
|
|
1285
1661
|
// 先在这里显式取一次,避免并发首调各自走一遍会话初始化。
|
|
1286
1662
|
await env.host.getSession();
|
|
@@ -1299,11 +1675,38 @@ async function commandCreateForm(args, env) {
|
|
|
1299
1675
|
target = await resolveCreateWorkflow(args.workflow, env);
|
|
1300
1676
|
pack = await loadCreateFormPack(target.id, env);
|
|
1301
1677
|
}
|
|
1302
|
-
|
|
1678
|
+
// ★ 2026-10-15 用户裁定:`createForm` 改用**字符口径 20,000** 的**严格档**(见
|
|
1679
|
+
// `CREATE_FORM_STDOUT_SAFE_CHARS` / `compactFormOutput` 的 `charBudget`)。
|
|
1680
|
+
//
|
|
1681
|
+
// 与 2026-10-13/14 那两轮「超线仍回挂 `contextId` / `fields` / `historyRequestData`」的口径**相反**:
|
|
1682
|
+
// 超线时 stdout **只回那一句提示**,一个键都不回挂 —— 用户明确要求"不用再返回 json",理由是那几样
|
|
1683
|
+
// 在落盘文件里都能取到(含下一步 `createRequest` 要用的 `contextId`)。因此这里**不再传 `keep` 清单**,
|
|
1684
|
+
// 键序契约(`contextId` → `fields` → `historyRequestData`)也随之失效、不必再维护。
|
|
1685
|
+
//
|
|
1686
|
+
// 判线量的是**最终打出去的那份文本**(`finalStdoutChars`:信封 + `fields` / `historyRequestData` /
|
|
1687
|
+
// `systemFields` 的紧凑路径),不是美化 JSON —— 那三个键压成单行后整份实测小约 30%,用美化口径判线
|
|
1688
|
+
// 会让本可内联的表单被判落盘。
|
|
1689
|
+
//
|
|
1690
|
+
// 未超线时 `fullPayload` 原样返回,行为与从前一致(只是 stdout 上那几个键改单行);落盘那份**始终
|
|
1691
|
+
// 完整**(pretty JSON,未紧凑),所以模型按提示读回来拿到的是与 stdout 同口径的全量内容。
|
|
1692
|
+
//
|
|
1693
|
+
// 显式标注为 `Record<string, any>`:`pack.data` 是带索引签名的宽松对象,而对象字面量里又显式写了
|
|
1694
|
+
// `workflow` / `launchPreferences`,TS 的推断会**丢掉索引签名**、只留这两个显式键。
|
|
1695
|
+
const fullPayload = {
|
|
1303
1696
|
...pack.data,
|
|
1304
1697
|
workflow: { ...(pack.data.workflow || {}), selectionSource: target.source },
|
|
1305
1698
|
launchPreferences: pack.launchPreferences,
|
|
1306
|
-
}
|
|
1699
|
+
};
|
|
1700
|
+
const compacted = await compactFormOutput(fullPayload, env, 'last_create_form.json', [], {
|
|
1701
|
+
safeChars: CREATE_FORM_STDOUT_SAFE_CHARS,
|
|
1702
|
+
operation: 'workflow.createForm',
|
|
1703
|
+
// 判线要量"最终打出去的那份文本",而 `durationMs` 的真值只能在压缩**之后**求(它统计的正是
|
|
1704
|
+
// 整条命令),故此处按位数用哨兵值先把它算进判线 —— 高估只让边界上的表单提前落盘(安全方向)。
|
|
1705
|
+
reserve: { durationMs: DURATION_RESERVE_SENTINEL },
|
|
1706
|
+
});
|
|
1707
|
+
// 耗时并在 `data` 里(与 `outputChars` / `persistedResult` 这些运行期属性同处);**两条分支都带上**
|
|
1708
|
+
// —— 超线落盘那一档 `data` 只剩一句提示,并进去它才不会恰好在"大表单"时消失。
|
|
1709
|
+
return workflowSuccess('workflow.createForm', { ...compacted, durationMs: Date.now() - startedAt });
|
|
1307
1710
|
}
|
|
1308
1711
|
// ---------------------------------------------------------------------------
|
|
1309
1712
|
// createForm:loadBase/loadFrame/loadMinor/formLayout + 字段输出 + fileUploadParams
|
|
@@ -1317,6 +1720,15 @@ async function commandCreateForm(args, env) {
|
|
|
1317
1720
|
async function buildCreateFormData(workflowId, env) {
|
|
1318
1721
|
const query = { workflowId, isCreate: 1, ...AJAX_QUERY };
|
|
1319
1722
|
const body = { workflowId, isCreate: 1, isMobile: 0 };
|
|
1723
|
+
/**
|
|
1724
|
+
* 历史参考值的**列表**请求提前发起(P2 提速,2026-10-13):它只依赖 `workflowId`,与下面四个
|
|
1725
|
+
* 表单载入接口没有任何数据依赖,所以放在这里和它们**并发**跑,不再等字段解析完再串行补一次列表
|
|
1726
|
+
* (真机该请求 1–2 s,全落在关键路径上)。
|
|
1727
|
+
*
|
|
1728
|
+
* `.catch(() => null)` 是必须的:这个 Promise 可能因为表单后续抛错而**没人 await**,
|
|
1729
|
+
* 兜住之后就不会留下未处理的拒绝;真要暴露的认证/网络错误由同批的载入请求抛出,不会被吞掉。
|
|
1730
|
+
*/
|
|
1731
|
+
const historyListSnapshot = fetchHistoryListSnapshot(env, { workflowId }).catch(() => null);
|
|
1320
1732
|
// 2026-09-22:这四个接口**彼此没有数据依赖**(入参只需要 workflowId,见各自的 query/body),
|
|
1321
1733
|
// 真机实测各 171–429 ms、串行合计约 1.1 s;并行后只受最慢的一个约束。
|
|
1322
1734
|
// `loadMinorInfo` / `tenant/setting` 仍是 best-effort(失败只降级、不阻断),所以各自 catch 成
|
|
@@ -1428,7 +1840,23 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1428
1840
|
const systemFieldValues = _normalizeSystemFieldValues(layoutData.businessOperatorObj);
|
|
1429
1841
|
let linkageActions = Array.isArray(layoutData.linkageActions) ? layoutData.linkageActions : [];
|
|
1430
1842
|
let onLoadLinkage = [];
|
|
1431
|
-
|
|
1843
|
+
/**
|
|
1844
|
+
* 小e 环境下**不跑 `onLoadForm` 字段联动**(2026-10-16 用户裁定)。
|
|
1845
|
+
*
|
|
1846
|
+
* 理由:小e 这一档的真正填单动作由宿主浏览器页面完成,`createForm` 只是给模型一份
|
|
1847
|
+
* 「有哪些字段、默认值是多少」的快照;onLoadForm 联动**带出的值**在这一档用不上
|
|
1848
|
+
* (真到表单页里,页面自己会把联动跑一遍)。省掉这一趟 `fdLinkage` 请求。
|
|
1849
|
+
*
|
|
1850
|
+
* ⚠️ **显示属性联动照跑**:紧接其后的 `computeLinkageVisibility` 数据源是响应里本地的
|
|
1851
|
+
* `layoutData.viewLinkages`,不依赖本次联动结果;跳过字段联动后它按「表单默认值 + 系统字段值」
|
|
1852
|
+
* 求值,`fields` 里的隐藏 / 只读 / 条件必填判定仍然成立。
|
|
1853
|
+
*
|
|
1854
|
+
* `linkageActions` 仍原样随句柄带给 `createRequest`(非小e 环境下那一侧自己会跑联动,见
|
|
1855
|
+
* `commandCreateRequest`),本处跳过的只是 `createForm` 自己发起的这一次 onLoadForm 调用。
|
|
1856
|
+
* 小e 环境下两处都不跑(`createRequest` 同档也跳过字段联动与显示属性联动,由浏览器页面自己算)。
|
|
1857
|
+
* 非小e 环境行为不变。
|
|
1858
|
+
*/
|
|
1859
|
+
if (!hasXiaoEEnv(env.ctx.env) && linkageActions.length) {
|
|
1432
1860
|
const loadEntries = _entriesFromDefaults(defaults);
|
|
1433
1861
|
try {
|
|
1434
1862
|
onLoadLinkage = await runFieldLinkage(env, buildLinkageContext({ runtime, frame, layout: { form_id: frame.form_id }, linkageActions }), loadEntries, new Set(['onLoadForm']), defaults);
|
|
@@ -1502,8 +1930,60 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1502
1930
|
}
|
|
1503
1931
|
}
|
|
1504
1932
|
}
|
|
1933
|
+
/**
|
|
1934
|
+
* 模型真正需要关心的字段 = **必填字段** ∪ **用户实际能在表单上看到的可编辑字段**。
|
|
1935
|
+
*
|
|
1936
|
+
* 2026-10-14 用户裁定:`createForm` 下发的字段信息(`fields`)与历史数据参考
|
|
1937
|
+
* (`historyRequestData`)都只保留这两类。此前 `fields` 下发的是"布局级字段全集"
|
|
1938
|
+
* (真机 277 条,其中 145 条是只读 / 不可编辑 / 不在当前布局上的字段),模型既看不到也填不了,
|
|
1939
|
+
* 只能逐条扫过去白花上下文。
|
|
1940
|
+
*
|
|
1941
|
+
* 口径来源就是上面两轮循环已经算好的四份清单(主表必填 / 主表可写 / 明细必填 / 明细联动必填)——
|
|
1942
|
+
* 它们本身已过「非只读且可写」「未被联动隐藏」「在当前布局的展示配置内」三道筛,不另立判据。
|
|
1943
|
+
*/
|
|
1944
|
+
const modelFieldIds = new Set();
|
|
1945
|
+
for (const list of [requiredFieldsList, editableFieldsList, ...Object.values(subFormRequired), ...Object.values(subFormLinkageRequired)]) {
|
|
1946
|
+
for (const item of list) {
|
|
1947
|
+
const id = String(item?.id ?? '');
|
|
1948
|
+
if (id)
|
|
1949
|
+
modelFieldIds.add(id);
|
|
1950
|
+
}
|
|
1951
|
+
}
|
|
1952
|
+
/**
|
|
1953
|
+
* 字段默认值(`initialValue`)——「有默认值的字段直接把值挂在字段属性上」(2026-10-14 用户裁定)。
|
|
1954
|
+
*
|
|
1955
|
+
* 取代此前两条输出面:独立的 `initialValues` 数组、以及 `fillPlan` 里的 `defaulted` 标记。
|
|
1956
|
+
* 口径不变(`hiddenCellIds` / `modelFieldIds` / 布局展示配置三道筛,与 `fields` 同源),
|
|
1957
|
+
* 只是换了落点:模型看某个字段时,默认值就在它旁边,不必再跨键对照两份清单。
|
|
1958
|
+
* 同一字段有多条默认值时取第一条(与旧 `initialValues` 的逐条追加相比,「哪个是新值」旧口径
|
|
1959
|
+
* 本就无法判定,取首条至少是确定性的)。
|
|
1960
|
+
*/
|
|
1961
|
+
const initialValueByFieldId = new Map();
|
|
1962
|
+
for (const d of defaults) {
|
|
1963
|
+
const fid = String((d.formField && d.formField.id) ?? '').trim();
|
|
1964
|
+
if (!fid || hiddenCellIds.has(fid) || initialValueByFieldId.has(fid))
|
|
1965
|
+
continue;
|
|
1966
|
+
if (!modelFieldIds.has(fid))
|
|
1967
|
+
continue;
|
|
1968
|
+
const sf = String((d.subForm && d.subForm.id) ?? '').trim();
|
|
1969
|
+
if (sf) {
|
|
1970
|
+
if (layoutSubformIds.size && !layoutSubformIds.has(sf))
|
|
1971
|
+
continue;
|
|
1972
|
+
}
|
|
1973
|
+
else if (compsFieldIds.size && !compsFieldIds.has(fid)) {
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
const disp = _detailDisplayText(d);
|
|
1977
|
+
if (!disp)
|
|
1978
|
+
continue;
|
|
1979
|
+
initialValueByFieldId.set(fid, disp);
|
|
1980
|
+
}
|
|
1505
1981
|
const slimFields = [];
|
|
1506
1982
|
for (const field of visibleFields) {
|
|
1983
|
+
// 只下发模型需要关心的字段(可编辑 ∪ 必填且可见,见 modelFieldIds 注释);只读 / 联动隐藏 /
|
|
1984
|
+
// 不在当前布局上的字段不下发——模型既填不了也看不到。
|
|
1985
|
+
if (!modelFieldIds.has(String(field.id ?? '')))
|
|
1986
|
+
continue;
|
|
1507
1987
|
// 键序 id|name|subFormId|componentKey 是契约;`subFormId` **为空就整个省略**(缺该键=主表字段)
|
|
1508
1988
|
// —— 主表字段占绝大多数,写 `""` 恒白占一条键(真机 278 字段表单:100 个主表字段 × 22 B ≈ 2.2 KB)。
|
|
1509
1989
|
const slimSubFormId = String(field.subFormId ?? '');
|
|
@@ -1511,9 +1991,15 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1511
1991
|
if (slimSubFormId)
|
|
1512
1992
|
item.subFormId = slimSubFormId;
|
|
1513
1993
|
item.componentKey = field.componentKey;
|
|
1514
|
-
//
|
|
1515
|
-
// `
|
|
1516
|
-
//
|
|
1994
|
+
// 表单初始化时就有的值(OA 预填 + onLoadForm 联动带出)——有才出现该键;**没有默认值的字段不带**。
|
|
1995
|
+
// CLI 拼 `data` 时会自动并入,模型**不必显式传**;用户本次明确改写了它才按用户说的覆盖(见
|
|
1996
|
+
// `workflow-create.md` 的「填值纪律」)。
|
|
1997
|
+
const initialValue = initialValueByFieldId.get(String(field.id ?? ''));
|
|
1998
|
+
if (initialValue !== undefined)
|
|
1999
|
+
item.initialValue = initialValue;
|
|
2000
|
+
// `fields` **不返回** `required`(2026-10-14 起也不再区分必填/可写):本清单就是「可编辑 ∪ 必填且
|
|
2001
|
+
// 可见」的全集,能出现在这里就是能填的字段;必填与否最终只由 `createRequest` 的 `missingFields`
|
|
2002
|
+
// 说了算。标 `required` 只会让模型拿它去推演"哪些是必填",与 `missingFields` 形成双份口径。
|
|
1517
2003
|
if (field.readOnly || field.disableEdit) {
|
|
1518
2004
|
item.readOnly = Boolean(field.readOnly);
|
|
1519
2005
|
item.disableEdit = Boolean(field.disableEdit);
|
|
@@ -1558,28 +2044,6 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1558
2044
|
const fieldsById = new Map();
|
|
1559
2045
|
for (const f of fields)
|
|
1560
2046
|
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
2047
|
/**
|
|
1584
2048
|
* 必填字段 → 历史可参考值(V12.1.77:取数点从 `createRequest` **前移到这里**)。
|
|
1585
2049
|
*
|
|
@@ -1602,7 +2066,9 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1602
2066
|
...Object.values(subFormRequired).flatMap((list) => list.map((f) => String(f?.id ?? ''))),
|
|
1603
2067
|
].filter(Boolean))];
|
|
1604
2068
|
const historyCache = fields.length
|
|
1605
|
-
? await buildHistoryRequestData(env, fields, historyRequiredFieldIds.map((id) => ({ id, name: String((fieldsById.get(id) || {}).name ?? id) })), { allFormFields: true, workflowId }
|
|
2069
|
+
? await buildHistoryRequestData(env, fields, historyRequiredFieldIds.map((id) => ({ id, name: String((fieldsById.get(id) || {}).name ?? id) })), { allFormFields: true, workflowId },
|
|
2070
|
+
// 本命令开头就已并发发起的列表请求:这里只补「详情」那几步(P2)。
|
|
2071
|
+
historyListSnapshot)
|
|
1606
2072
|
: null;
|
|
1607
2073
|
const session = await getSessionContext(env);
|
|
1608
2074
|
const context = {
|
|
@@ -1651,31 +2117,25 @@ async function buildCreateFormData(workflowId, env) {
|
|
|
1651
2117
|
historyRequiredFieldIds,
|
|
1652
2118
|
};
|
|
1653
2119
|
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
2120
|
return {
|
|
1656
2121
|
data: {
|
|
1657
2122
|
status: 'FORM_READY',
|
|
1658
2123
|
contextId: token,
|
|
1659
2124
|
requestId: runtime.request_id,
|
|
1660
|
-
// 显式点名语义:createForm 阶段并没有建单,这个 ID 是**预生成**的流程请求 ID,
|
|
1661
|
-
// 只作为该单据的身份标识随 context 带到 createRequest;不写清楚模型会误当成"已存在的流程编号/草稿号"。
|
|
1662
|
-
requestIdDesc: '预生成的流程请求 ID:本阶段尚未创建流程,它只是这次将要创建的单据的身份标识(用于后续 createRequest),不是已存在的流程编号或草稿号。',
|
|
1663
2125
|
requestName: runtime.default_request_name || '',
|
|
1664
2126
|
workflow: context.workflow,
|
|
1665
2127
|
form: { id: frame.form_id, layout_id: layoutId },
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
// `
|
|
1669
|
-
//
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
...requiredFieldIds.map((id) => String(id ?? '')),
|
|
1673
|
-
].filter(Boolean))],
|
|
2128
|
+
// 字段清单(**唯一**一份,2026-10-14 用户裁定):可编辑字段 ∪ 必填且可见字段;有默认值的字段
|
|
2129
|
+
// 直接带 `initialValue`。此前并列的 `requiredFields` / `subFormRequired` / `editableFields` /
|
|
2130
|
+
// `initialValues` 四份清单与 `fillPlan` 一并取消——它们与 `fields` 同源、只是换个维度再切一刀,
|
|
2131
|
+
// 模型却要跨五份清单对照同一个字段。现在的口径只有两句:
|
|
2132
|
+
// 「哪些字段可以落值」= 本清单(能出现在这里就是能填的);
|
|
2133
|
+
// 「哪个必填还没值」= `createRequest` 的 `missingFields`(唯一权威,本命令不下发必填标记)。
|
|
1674
2134
|
fields: slimFields,
|
|
1675
|
-
|
|
1676
|
-
//
|
|
1677
|
-
//
|
|
1678
|
-
historyRequestData: filterHistoryRequestData(historyCache,
|
|
2135
|
+
// 历史参考值(近一年、非草稿、最近 5 条「我发起的」)——填值前先看它,能直接照用的字段就别去
|
|
2136
|
+
// 问用户。字段范围与 `fields` **同一口径**(见 modelFieldIds 注释);完整结果(本表单全部字段)
|
|
2137
|
+
// 仍随句柄留给 `createRequest` 用(条件必填要到那边才现身)。
|
|
2138
|
+
historyRequestData: filterHistoryRequestData(historyCache, modelFieldIds),
|
|
1679
2139
|
systemFields: {
|
|
1680
2140
|
permissions: minorInfo.permissions || {},
|
|
1681
2141
|
defaults: context.systemFields.defaults,
|
|
@@ -2000,6 +2460,119 @@ function normalizeRelateCandidate(value) {
|
|
|
2000
2460
|
const name = String(value.name ?? '').trim();
|
|
2001
2461
|
return id && name ? { id, name } : null;
|
|
2002
2462
|
}
|
|
2463
|
+
/** 需要按 `format` 归一化取值的日期 / 时间类控件(componentKey 已小写)。 */
|
|
2464
|
+
const DATE_COMPONENTS = new Set(['datecomponent', 'datetimecomponent', 'timecomponent', 'dateinterval']);
|
|
2465
|
+
/**
|
|
2466
|
+
* 从模型给的文本里解析日期 / 时间分量。认得下面这些写法(全角数字先转半角):
|
|
2467
|
+
* `2026-08-21` / `2026/8/21` / `2026.8.21` / `2026年8月21日` / `20260821`、
|
|
2468
|
+
* `09:00` / `9:5` / `9时30分` / `2026-08-21 09:00:30` / `20260821090030`、单挂一个裸数字(按小时)。
|
|
2469
|
+
* 解析不出任何分量时返回 `null`(调用方原样透传,不猜)。
|
|
2470
|
+
*/
|
|
2471
|
+
function parseDateParts(raw) {
|
|
2472
|
+
const text = String(raw ?? '')
|
|
2473
|
+
.replace(/[0-9]/g, (c) => String.fromCharCode(c.charCodeAt(0) - 0xfee0))
|
|
2474
|
+
.trim();
|
|
2475
|
+
if (!text)
|
|
2476
|
+
return null;
|
|
2477
|
+
const parts = {};
|
|
2478
|
+
// ① 紧凑写法:20260821 / 20260821093000
|
|
2479
|
+
const compact = /^(\d{4})(\d{2})(\d{2})(?:[T\s]?(\d{2})(\d{2})(\d{2})?)?$/.exec(text);
|
|
2480
|
+
if (compact) {
|
|
2481
|
+
parts.y = Number(compact[1]);
|
|
2482
|
+
parts.m = Number(compact[2]);
|
|
2483
|
+
parts.d = Number(compact[3]);
|
|
2484
|
+
if (compact[4] !== undefined) {
|
|
2485
|
+
parts.hh = Number(compact[4]);
|
|
2486
|
+
parts.mm = Number(compact[5]);
|
|
2487
|
+
}
|
|
2488
|
+
if (compact[6] !== undefined)
|
|
2489
|
+
parts.ss = Number(compact[6]);
|
|
2490
|
+
return parts;
|
|
2491
|
+
}
|
|
2492
|
+
// ② 日期段:年月日之间允许 - / . 或「年」「月」,日期后允许「日」「号」
|
|
2493
|
+
let rest = text;
|
|
2494
|
+
const dateMatch = /(\d{4})\s*[-/.年]\s*(\d{1,2})\s*[-/.月]\s*(\d{1,2})\s*[日号]?/.exec(rest);
|
|
2495
|
+
if (dateMatch) {
|
|
2496
|
+
parts.y = Number(dateMatch[1]);
|
|
2497
|
+
parts.m = Number(dateMatch[2]);
|
|
2498
|
+
parts.d = Number(dateMatch[3]);
|
|
2499
|
+
rest = rest.slice(0, dateMatch.index) + rest.slice(dateMatch.index + dateMatch[0].length);
|
|
2500
|
+
}
|
|
2501
|
+
// ③ 时间段:时[:时分][:秒][.毫秒],分隔符认 : : 时 点 ,秒可带「分」「秒」
|
|
2502
|
+
const timeMatch = /(\d{1,2})\s*[::时点]\s*(\d{1,2})(?:\s*[::分]\s*(\d{1,2}))?(?:\s*[..]\s*(\d{1,3}))?/.exec(rest);
|
|
2503
|
+
if (timeMatch) {
|
|
2504
|
+
parts.hh = Number(timeMatch[1]);
|
|
2505
|
+
parts.mm = Number(timeMatch[2]);
|
|
2506
|
+
if (timeMatch[3] !== undefined)
|
|
2507
|
+
parts.ss = Number(timeMatch[3]);
|
|
2508
|
+
if (timeMatch[4] !== undefined)
|
|
2509
|
+
parts.ms = Number(timeMatch[4]);
|
|
2510
|
+
}
|
|
2511
|
+
if (parts.y === undefined && parts.hh === undefined) {
|
|
2512
|
+
// 兜底:整串只剩一个裸数字 ⇒ 当小时用(`format: HH` 场景,如 "9")
|
|
2513
|
+
const bare = /^(\d{1,2})$/.exec(rest.trim());
|
|
2514
|
+
if (!bare)
|
|
2515
|
+
return null;
|
|
2516
|
+
parts.hh = Number(bare[1]);
|
|
2517
|
+
}
|
|
2518
|
+
return parts;
|
|
2519
|
+
}
|
|
2520
|
+
/**
|
|
2521
|
+
* 按字段 `format` 归一化日期 / 时间文本(V12.1.115,2026-10-14 用户裁定)。
|
|
2522
|
+
*
|
|
2523
|
+
* 目标:**模型不必关心 `format`**。`2026-8-21`、`2026/08/21`、`2026年8月21日 9:5`、
|
|
2524
|
+
* `2026-08-21 09:00:00` 都会落到 `format` 要求的形状——
|
|
2525
|
+
* - `format` 里有的分量由解析结果补零后填(`yyyy-MM-dd` ⇒ `2026-08-21`);
|
|
2526
|
+
* - 时间分量没解析出来时补 `00`(`yyyy-MM-dd HH:mm` 收到纯日期 ⇒ `2026-08-21 00:00`),
|
|
2527
|
+
* 这也是此前 `DATETIME_FORMAT` 报错那条路径的替代:不再要求模型补时间;
|
|
2528
|
+
* - `format` 里没有的分量直接丢掉(`yyyy-MM-dd` 收到带时分秒的写法 ⇒ 只留日期)。
|
|
2529
|
+
*
|
|
2530
|
+
* 两条**不猜**的边界(宁可原样透传给服务端报错,也不编造用户没给的信息):
|
|
2531
|
+
* ① 解析不出任何分量(不是日期/时间文本);② `format` 要日期、但文本里连年月日都没凑齐。
|
|
2532
|
+
*/
|
|
2533
|
+
function normalizeDateValue(raw, format) {
|
|
2534
|
+
const text = String(raw ?? '').trim();
|
|
2535
|
+
if (!text || !format)
|
|
2536
|
+
return raw;
|
|
2537
|
+
// 日期区间(`2026-08-01~2026-08-31` / 「…至…」):两半各自归一化后按原分隔符接回。
|
|
2538
|
+
for (const sep of ['~', '至']) {
|
|
2539
|
+
if (text.includes(sep)) {
|
|
2540
|
+
const halves = text.split(sep);
|
|
2541
|
+
if (halves.length === 2)
|
|
2542
|
+
return `${normalizeDateValue(halves[0], format)}${sep}${normalizeDateValue(halves[1], format)}`;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
const parts = parseDateParts(text);
|
|
2546
|
+
if (!parts)
|
|
2547
|
+
return text;
|
|
2548
|
+
// `format` 要哪个日期分量,文本里就得有哪个——缺了不猜(补 `01` 等于编造用户没给的日期)。
|
|
2549
|
+
if ((/y{2,4}/.test(format) && parts.y === undefined)
|
|
2550
|
+
|| (/M/.test(format) && parts.m === undefined)
|
|
2551
|
+
|| (/d/.test(format) && parts.d === undefined)) {
|
|
2552
|
+
return text;
|
|
2553
|
+
}
|
|
2554
|
+
const pad = (n, len = 2) => String(n).padStart(len, '0');
|
|
2555
|
+
return format.replace(/yyyy|yy|MM|dd|HH|hh|mm|ss|fff|M|d|H|h|m|s|f/g, (token) => {
|
|
2556
|
+
switch (token) {
|
|
2557
|
+
case 'yyyy': return pad(parts.y ?? 0, 4);
|
|
2558
|
+
case 'yy': return pad((parts.y ?? 0) % 100);
|
|
2559
|
+
case 'MM': return pad(parts.m ?? 0);
|
|
2560
|
+
case 'M': return String(parts.m ?? 0);
|
|
2561
|
+
case 'dd': return pad(parts.d ?? 0);
|
|
2562
|
+
case 'd': return String(parts.d ?? 0);
|
|
2563
|
+
case 'HH':
|
|
2564
|
+
case 'hh': return pad(parts.hh ?? 0);
|
|
2565
|
+
case 'H':
|
|
2566
|
+
case 'h': return String(parts.hh ?? 0);
|
|
2567
|
+
case 'mm': return pad(parts.mm ?? 0);
|
|
2568
|
+
case 'm': return String(parts.mm ?? 0);
|
|
2569
|
+
case 'ss': return pad(parts.ss ?? 0);
|
|
2570
|
+
case 's': return String(parts.ss ?? 0);
|
|
2571
|
+
case 'fff': return pad(parts.ms ?? 0, 3);
|
|
2572
|
+
default: return String(parts.ms ?? 0); // 'f'
|
|
2573
|
+
}
|
|
2574
|
+
});
|
|
2575
|
+
}
|
|
2003
2576
|
function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
2004
2577
|
const detail = { ...(row || {}), formField: { id: field.id } };
|
|
2005
2578
|
const isEmployeeScope = field.isEmployeeScope || String(field.componentKey ?? '').toLowerCase() === 'employeescope';
|
|
@@ -2122,7 +2695,16 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
|
2122
2695
|
continue;
|
|
2123
2696
|
}
|
|
2124
2697
|
const keyword = String(item).trim();
|
|
2125
|
-
|
|
2698
|
+
// 选项值允许「名称(ID)」形态:模型会把澄清候选 / 历史参考值里的 `名称(ID)` 原样回传,
|
|
2699
|
+
// 而契约(`workflow-create.md`「字段值格式」)只要求传 name ⇒ 必须容错,不能报 NOT_FOUND。
|
|
2700
|
+
// **ID 取最后一个括号内的内容**:选项名自身可能带括号——真机如 `银行(个人报销)(7)`,
|
|
2701
|
+
// 取第一个括号会得到 `个人报销` 这种非数字片段。口径复用 `parseNameWithId`(同一份正则,
|
|
2702
|
+
// 避免"查询侧认、发起侧不认"的口径分叉),显式 ID 命中即直取,否则回落到名称精确比对。
|
|
2703
|
+
const parsed = parseNameWithId(keyword);
|
|
2704
|
+
const explicit = parsed.id ? choices.filter((c) => String(c.id) === parsed.id) : [];
|
|
2705
|
+
const matches = explicit.length
|
|
2706
|
+
? explicit
|
|
2707
|
+
: choices.filter((c) => String(c.id) === keyword || String(c.name ?? '').toLowerCase() === parsed.name.toLowerCase());
|
|
2126
2708
|
if (matches.length !== 1) {
|
|
2127
2709
|
throw new ValueAmbiguity({
|
|
2128
2710
|
kind: 'OPTION',
|
|
@@ -2146,12 +2728,13 @@ function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
|
2146
2728
|
if (value && typeof value === 'object') {
|
|
2147
2729
|
throw new ValueAmbiguity({ kind: 'VALUE_FORMAT', fieldId: field.id, fieldName: field.name, reason: '文本字段只接受字符串、数字或布尔值', candidates: [] });
|
|
2148
2730
|
}
|
|
2149
|
-
const
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2731
|
+
const rawContent = value === null || value === undefined ? '' : String(value);
|
|
2732
|
+
// 日期 / 时间控件:模型给的写法一律在 CLI 内按字段 `format` 归一化(2026-10-14 用户裁定)。
|
|
2733
|
+
// 此前这里在"日期+时间控件只收到纯日期"时抛 `DATETIME_FORMAT` 让模型补时间——把格式这件事
|
|
2734
|
+
// 外包给了模型,实测它会为 `100` 还是 `100.00`、`09:00` 还是 `9:00` 反复纠结。
|
|
2735
|
+
const content = DATE_COMPONENTS.has(component)
|
|
2736
|
+
? normalizeDateValue(rawContent, String(field.format ?? ''))
|
|
2737
|
+
: rawContent;
|
|
2155
2738
|
const maxLength = field.maxLength;
|
|
2156
2739
|
const richUnlimited = component === 'textarea' && Boolean(field.richEditor);
|
|
2157
2740
|
if (!richUnlimited && maxLength !== undefined && maxLength !== null && String(maxLength).match(/^\d+$/) && content.length > Number(maxLength)) {
|
|
@@ -2865,6 +3448,16 @@ function mergeLinkageDetails(merged, linkageDetails) {
|
|
|
2865
3448
|
}
|
|
2866
3449
|
return result;
|
|
2867
3450
|
}
|
|
3451
|
+
/**
|
|
3452
|
+
* `computeLinkageVisibility` 的空结果(四个集合都是空)。
|
|
3453
|
+
*
|
|
3454
|
+
* 用途:小e 档的 `createRequest` **不跑显示属性联动**(2026-10-16 用户裁定,见
|
|
3455
|
+
* `commandCreateRequest`),此时直接取这份空结果顶替,避免调用点写四个字面量再补类型标注。
|
|
3456
|
+
* 语义与 `computeLinkageVisibility` 的早退分支(`linkages` 不是数组时)完全一致。
|
|
3457
|
+
*/
|
|
3458
|
+
function emptyLinkageVisibility() {
|
|
3459
|
+
return [new Set(), {}, {}, {}];
|
|
3460
|
+
}
|
|
2868
3461
|
function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues, fields) {
|
|
2869
3462
|
const hiddenSubforms = new Set();
|
|
2870
3463
|
const rowHidden = {};
|
|
@@ -3256,6 +3849,289 @@ function previewValueText(value) {
|
|
|
3256
3849
|
}
|
|
3257
3850
|
return value;
|
|
3258
3851
|
}
|
|
3852
|
+
/** markdown 表格单元格:不能出现裸 `|` 与换行,否则列会错位(与 invoice 渲染档同口径)。 */
|
|
3853
|
+
function markdownCell(value) {
|
|
3854
|
+
const raw = String(value ?? '').trim();
|
|
3855
|
+
if (!raw)
|
|
3856
|
+
return '—';
|
|
3857
|
+
return raw.replace(/\|/gu, '\\|').replace(/\r?\n/gu, ' ');
|
|
3858
|
+
}
|
|
3859
|
+
/**
|
|
3860
|
+
* 缺失必填字段的**历史推荐值**(2026-10-13 用户口径)。
|
|
3861
|
+
*
|
|
3862
|
+
* 口径(用户原话:"该字段历史数据填写值都一样,或者最近三条历史流程该字段值都一致"):
|
|
3863
|
+
* - 只看该字段**有值**的历史记录,顺序即 `historyCache` 的**发起时间倒序**;
|
|
3864
|
+
* - 有值记录取值**全部一致** ⇒ 推荐;
|
|
3865
|
+
* - 否则取**最近 3 条**有值记录,三者一致 ⇒ 推荐;
|
|
3866
|
+
* - 其余一律**不给推荐**(宁缺勿错——让用户自己填,不猜)。
|
|
3867
|
+
*
|
|
3868
|
+
* 数据源是 `createForm` 随句柄留下的**宽缓存**(本表单全部字段),所以**联动命中后才变必填**的字段
|
|
3869
|
+
* (真机样本里的「相关流程」)也能命中;句柄来自旧版本 / 缓存缺失时静默返回 `null`,不拖累主链路。
|
|
3870
|
+
*
|
|
3871
|
+
* **一致性判断完全在命令侧算完**:模型既不拿原始历史、也不做任何推断,只负责原样粘贴。
|
|
3872
|
+
*/
|
|
3873
|
+
function historyRecommendation(records, fieldId) {
|
|
3874
|
+
const id = String(fieldId ?? '').trim();
|
|
3875
|
+
if (!id)
|
|
3876
|
+
return null;
|
|
3877
|
+
const values = [];
|
|
3878
|
+
for (const record of toArray(records)) {
|
|
3879
|
+
// `fieldValues` 是 `{字段id: 值}` 映射(2026-10-15 起;旧形态是数组)。
|
|
3880
|
+
const fieldValues = record?.fieldValues;
|
|
3881
|
+
if (!fieldValues || typeof fieldValues !== 'object' || Array.isArray(fieldValues))
|
|
3882
|
+
continue;
|
|
3883
|
+
if (!Object.prototype.hasOwnProperty.call(fieldValues, id))
|
|
3884
|
+
continue;
|
|
3885
|
+
// 附件类字段的值是**结构化对象**(见 `formatHistoryFieldValue`):`String()` 会把它压成
|
|
3886
|
+
// `[object Object]`,于是 N 条互不相同的附件"看起来完全一致"⇒ 凭空给出一条假推荐。
|
|
3887
|
+
// 非字符串一律不参与一致性判定(宁缺勿错)。
|
|
3888
|
+
const raw = fieldValues[id];
|
|
3889
|
+
if (typeof raw !== 'string')
|
|
3890
|
+
continue;
|
|
3891
|
+
const text = raw.trim();
|
|
3892
|
+
if (text)
|
|
3893
|
+
values.push(text);
|
|
3894
|
+
}
|
|
3895
|
+
if (!values.length)
|
|
3896
|
+
return null;
|
|
3897
|
+
if (values.every((value) => value === values[0])) {
|
|
3898
|
+
return { value: values[0], note: `近 ${values.length} 条历史流程该字段取值一致` };
|
|
3899
|
+
}
|
|
3900
|
+
const latest = values.slice(0, 3);
|
|
3901
|
+
if (latest.length === 3 && latest.every((value) => value === latest[0])) {
|
|
3902
|
+
return { value: latest[0], note: '最近 3 条历史流程该字段取值一致' };
|
|
3903
|
+
}
|
|
3904
|
+
return null;
|
|
3905
|
+
}
|
|
3906
|
+
/** 选项清单里反查展示名(`紧急程度` / `密级` 的代码→中文名)。查不到返回空串。 */
|
|
3907
|
+
function optionDisplayName(options, value, idKeys) {
|
|
3908
|
+
for (const item of toArray(options)) {
|
|
3909
|
+
if (!item || typeof item !== 'object')
|
|
3910
|
+
continue;
|
|
3911
|
+
for (const key of idKeys) {
|
|
3912
|
+
if (String(item[key] ?? '').trim() !== value)
|
|
3913
|
+
continue;
|
|
3914
|
+
return String(item.name ?? item.content ?? '').trim();
|
|
3915
|
+
}
|
|
3916
|
+
}
|
|
3917
|
+
return '';
|
|
3918
|
+
}
|
|
3919
|
+
/**
|
|
3920
|
+
* 系统字段(流程标题 / 紧急程度 / 密级)的「已填内容」行(2026-10-14 用户口径)。
|
|
3921
|
+
*
|
|
3922
|
+
* 流程标题**只要该次发起有值就列**(与上一版单独那行 `- **流程标题**:…` 同口径,只是换个位置)。
|
|
3923
|
+
*
|
|
3924
|
+
* 紧急程度 / 密级是本次新增,两条门槛**缺一不可**,否则一个字都不给(宁缺勿错):
|
|
3925
|
+
* ① 服务端明确说该字段 `isview` —— `permissions` 里没有这一项时按"用户在表单上看不到"处理,
|
|
3926
|
+
* 不给没有依据的行;
|
|
3927
|
+
* ② 该值能在 `level_options` / `sec_level_options` 里反查出中文名 —— 这两个控件在表单上是下拉框,
|
|
3928
|
+
* 命令拿到的是**代码**(如密级 `1`);直接印代码用户看不懂,宁可省略这一行。
|
|
3929
|
+
*/
|
|
3930
|
+
function systemFilledRows(params) {
|
|
3931
|
+
const rows = [];
|
|
3932
|
+
const title = String(params.title ?? '').trim();
|
|
3933
|
+
if (title)
|
|
3934
|
+
rows.push({ name: SYSTEM_FIELD_KEYS.REQUEST_NAMEL, value: title });
|
|
3935
|
+
const push = (key, value, options, idKeys) => {
|
|
3936
|
+
const perm = params.permissions[key] || {};
|
|
3937
|
+
if (!perm.isview)
|
|
3938
|
+
return;
|
|
3939
|
+
const raw = String(value ?? '').trim();
|
|
3940
|
+
if (!raw)
|
|
3941
|
+
return;
|
|
3942
|
+
const name = optionDisplayName(options, raw, idKeys);
|
|
3943
|
+
if (!name)
|
|
3944
|
+
return;
|
|
3945
|
+
rows.push({ name: SYSTEM_FIELD_KEYS[key], value: name });
|
|
3946
|
+
};
|
|
3947
|
+
push('REQUEST_LEVEL', params.requestLevel, params.levelOptions, ['code', 'name']);
|
|
3948
|
+
push('SECLEVEL', params.secLevel, params.secLevelOptions, ['id', 'name']);
|
|
3949
|
+
return rows;
|
|
3950
|
+
}
|
|
3951
|
+
/**
|
|
3952
|
+
* 「已填内容」**主表行**的唯一来源(2026-10-14 用户口径):**用户在表单上能看到的、有值的、
|
|
3953
|
+
* 可编辑的主表字段**,按**表单布局顺序**列出。
|
|
3954
|
+
*
|
|
3955
|
+
* 为什么不再只列"模型这次传了什么":真的按 `resolvedValues`(= 仅本次入参)列时,用户看到的截图里
|
|
3956
|
+
* 「已填内容」只剩下模型显式传的那两项 —— 而这张单在表单上其实是**已填状态**的:表单默认值、
|
|
3957
|
+
* 系统字段一个都没列出来,用户根本无法据此确认"这张单到底填成什么样了"。
|
|
3958
|
+
*
|
|
3959
|
+
* 数据源是 `merged2` = 表单默认值 + 本次入参 + 联动带出的值,**就是将来真正落库的那一份**,
|
|
3960
|
+
* 所以列出来的每个值都是"用户会在表单上看到的值"。
|
|
3961
|
+
*
|
|
3962
|
+
* 不列的五类(都按"用户在表单上看到的"口径筛):
|
|
3963
|
+
* - 明细字段(另由明细表承载)、没值的字段(那是「还需要你提供」的活);
|
|
3964
|
+
* - 只读 / 禁止编辑的字段(用户改不了,不算"可编辑字段");
|
|
3965
|
+
* - 联动隐藏、联动禁用、布局隐藏的字段;
|
|
3966
|
+
* - **布局隐藏行上的字段**(`Sheet1.rows[].visible === false`,见 `collectHiddenRowFieldIds`);
|
|
3967
|
+
* - **不在这张表单组件清单里的字段**(`compsFieldIds` 是**白名单**:它列的是表单上真实摆着的
|
|
3968
|
+
* 组件。E10 的老表单会在定义里留着"已废弃 / 换了控件"的历史字段,服务端照样下发它们的默认值,
|
|
3969
|
+
* 但这些字段用户在表单上根本看不到 ⇒ 不进「已填内容」。这份名单与 `initialValues` 用的是同一份
|
|
3970
|
+
* ——`buildCreateFormData` 里的 `compsFieldIds.size && !compsFieldIds.has(fid)`,两边必须同口径,
|
|
3971
|
+
* 否则模型看到的 `initialValues` 与命令渲染的「已填内容」会互相打架)。
|
|
3972
|
+
*
|
|
3973
|
+
* ⚠️ 2026-10-14 修正:本函数初版把这处写成 `compsFieldIds.has(fieldId) ⇒ 跳过`(白名单当黑名单用),
|
|
3974
|
+
* 真机上结果**正好反过来**——列出来的全是用户看不到的老/隐藏字段(`币种(主表)2`、
|
|
3975
|
+
* `账号企业个人类型(废弃)`、`创建人直接上级` …),真正填了的 `报销事由` 一个都没列。
|
|
3976
|
+
* 单测没兜住是因为用例的 `compsFieldIds` 恒为 `[]`(空名单不筛),缺陷恰好落在空集上。
|
|
3977
|
+
* **名单为空时(老句柄 / 布局解析失败)不筛**,与 `initialValues` 的降级口径一致。
|
|
3978
|
+
*
|
|
3979
|
+
* ⚠️ 2026-10-14 二次修正(用户口径):`流程类型` 曾按**字段名**硬排除(`MAIN_FILLED_EXCLUDED_NAMES`),
|
|
3980
|
+
* 那是因为当时只看到"它与普通业务 Select 逐键同形、载荷里无标记"。真实原因是**它落在隐藏行上**
|
|
3981
|
+
* ——用户明确指出:这两个字段在表单上确实是显示的组件,但**所在行隐藏**,所以前台看不到。
|
|
3982
|
+
* 按名排除已撤除,改由 `hiddenCellFieldIds`(含行隐藏)统一承载——换一张表单、换一个字段名同样成立。
|
|
3983
|
+
*/
|
|
3984
|
+
function mainFilledRows(fields, values, hiddenIds, disabledIds, compsFieldIds) {
|
|
3985
|
+
const rows = [];
|
|
3986
|
+
for (const field of toArray(fields)) {
|
|
3987
|
+
if (!field || typeof field !== 'object')
|
|
3988
|
+
continue;
|
|
3989
|
+
const fieldId = String(field.id ?? '').trim();
|
|
3990
|
+
if (!fieldId)
|
|
3991
|
+
continue;
|
|
3992
|
+
if (String(field.subFormId ?? '').trim())
|
|
3993
|
+
continue;
|
|
3994
|
+
if (field.readOnly || field.disableEdit)
|
|
3995
|
+
continue;
|
|
3996
|
+
if (hiddenIds.has(fieldId) || disabledIds.has(fieldId))
|
|
3997
|
+
continue;
|
|
3998
|
+
if (compsFieldIds.size && !compsFieldIds.has(fieldId))
|
|
3999
|
+
continue;
|
|
4000
|
+
const detail = values.get(fieldId);
|
|
4001
|
+
if (!detail)
|
|
4002
|
+
continue;
|
|
4003
|
+
// 值必须走 `_detailDisplayText`(与 `initialValues` 的展示口径同源):浏览/选项类在载荷里是
|
|
4004
|
+
// `dataOptions`、富文本是 `dataText`,直接 `String()` 会印出 `[object Object]`。
|
|
4005
|
+
const value = _detailDisplayText(detail);
|
|
4006
|
+
if (!value)
|
|
4007
|
+
continue;
|
|
4008
|
+
rows.push({ name: String(field.name ?? fieldId), value });
|
|
4009
|
+
}
|
|
4010
|
+
return rows;
|
|
4011
|
+
}
|
|
4012
|
+
/**
|
|
4013
|
+
* `createRequest` **已落地结果**的渲染档(与查询侧 `finalMarkdown` 同口径):调用方
|
|
4014
|
+
* **原样粘贴**即可——不必自己挑字段、拼表格,也不必判断该展示什么。
|
|
4015
|
+
*
|
|
4016
|
+
* 只在流程**已经落地**时渲染(`CREATED` / `CREATED_SUBMITTED` / `CREATED_SUBMIT_FAILED`)。
|
|
4017
|
+
* `NEEDS_INPUT`(必填还没填齐)由 `renderCreatePendingMarkdown` 单独渲染——**刻意与本文本同一套
|
|
4018
|
+
* 表格口径**,只换结论句与末段(那边要说清"还没发起 + 还差哪几项");`NEEDS_CONFIRMATION` 不给
|
|
4019
|
+
* 文本(要先解决 `ambiguities` 再重发)。
|
|
4020
|
+
*
|
|
4021
|
+
* 内容分四段:结论(`message`,命令已写好的中文)→ 编号/链接 → 主表「已填内容」两列表
|
|
4022
|
+
* (系统字段如流程标题在前,业务字段按表单布局顺序随后;行由 `mainFilledRows` + `systemFilledRows`
|
|
4023
|
+
* 算好传进来,命令侧只有这一处口径)→ 各明细表(Excel 列形态)→ 提交失败原因 / 草稿缺项提醒(有才出现)。
|
|
4024
|
+
*/
|
|
4025
|
+
function renderCreateResultMarkdown(result, mainRows) {
|
|
4026
|
+
const lines = [];
|
|
4027
|
+
const message = String(result.message ?? '').trim();
|
|
4028
|
+
if (message)
|
|
4029
|
+
lines.push(message, '');
|
|
4030
|
+
// 流程标题已并入「已填内容」第一行(2026-10-14 用户口径):同一个值不在一份文本里出现两次。
|
|
4031
|
+
const header = [
|
|
4032
|
+
['流程编号', result.requestMark || result.requestId],
|
|
4033
|
+
['流程链接', result.url],
|
|
4034
|
+
];
|
|
4035
|
+
for (const [label, value] of header) {
|
|
4036
|
+
const text = String(value ?? '').trim();
|
|
4037
|
+
if (text)
|
|
4038
|
+
lines.push(`- **${label}**:${text}`);
|
|
4039
|
+
}
|
|
4040
|
+
if (mainRows.length) {
|
|
4041
|
+
lines.push('', '**已填内容**', '', '| 字段 | 值 |', '| --- | --- |');
|
|
4042
|
+
for (const row of mainRows)
|
|
4043
|
+
lines.push(`| ${markdownCell(row.name)} | ${markdownCell(row.value)} |`);
|
|
4044
|
+
}
|
|
4045
|
+
// 明细表:按列展示(Excel 列形态),多个子表单各成一表。
|
|
4046
|
+
const groups = (result.previewDetails || []).filter((g) => (g?.columns || []).length);
|
|
4047
|
+
groups.forEach((group, index) => {
|
|
4048
|
+
const columns = group.columns;
|
|
4049
|
+
lines.push('', groups.length > 1 ? `**明细表 ${index + 1}**` : '**明细表**', '', `| 序号 | ${columns.map((c) => markdownCell(c.fieldName)).join(' | ')} |`, `| --- | ${columns.map(() => '---').join(' | ')} |`);
|
|
4050
|
+
(group.rows || []).forEach((row, rowIndex) => {
|
|
4051
|
+
const values = (row?.values || []).map((v) => markdownCell(v));
|
|
4052
|
+
lines.push(`| ${rowIndex + 1} | ${values.join(' | ')} |`);
|
|
4053
|
+
});
|
|
4054
|
+
});
|
|
4055
|
+
const failReason = String(result.submitFailReason ?? '').trim();
|
|
4056
|
+
if (failReason)
|
|
4057
|
+
lines.push('', `**提交失败原因**:${failReason}`);
|
|
4058
|
+
const notice = String(result.missingFieldsNotice ?? '').trim();
|
|
4059
|
+
if (notice)
|
|
4060
|
+
lines.push('', notice);
|
|
4061
|
+
return lines.join('\n').trim();
|
|
4062
|
+
}
|
|
4063
|
+
/**
|
|
4064
|
+
* `createRequest` **尚未发起**(`NEEDS_INPUT`:必填还没填齐)的渲染档:告诉用户"已填了什么、还差
|
|
4065
|
+
* 哪几项"。与落地档(`renderCreateResultMarkdown`)**同一套表格口径**,调用方原样粘贴即可。
|
|
4066
|
+
*
|
|
4067
|
+
* 为什么要在命令里渲染(2026-10-13 真机):让模型自己拿 `resolvedValues` 拼主表 + 明细表、再拼
|
|
4068
|
+
* 待补清单,既慢又容易漂移;而且那样必须把 `resolvedValues` 一起返回,白占体积。渲染完就**不再
|
|
4069
|
+
* 返回**那些中间结构(`resolvedValues` / `previewDetails`)。
|
|
4070
|
+
*
|
|
4071
|
+
* 内容分四段:结论(还差几项)→ 主表「已填内容」两列表(系统字段如流程标题在前、业务字段按表单
|
|
4072
|
+
* 布局顺序随后,含表单默认值 —— 行由调用方算好传进来,与落地档共用同一份口径)→ 各明细表
|
|
4073
|
+
* (Excel 列形态)→ 待补必填项清单(每项标出在主表还是明细第几行)。明细与主表**分开列**,与落地档同口径。
|
|
4074
|
+
*
|
|
4075
|
+
* 待补清单里,命令会为**历史取值一致**的字段补一行「历史推荐值」(口径见 `historyRecommendation`)——
|
|
4076
|
+
* 用户据此可以一句话确认,不必先自己回想;推荐值由命令算好,模型不参与推断。
|
|
4077
|
+
*/
|
|
4078
|
+
function renderCreatePendingMarkdown(pending, mainRows, previewDetails, historyRecords, fieldMeta) {
|
|
4079
|
+
const lines = [];
|
|
4080
|
+
const missing = (pending.missingFields || []).filter(Boolean);
|
|
4081
|
+
lines.push(`还没提交:还有 ${missing.length} 项必填没填,给齐后我直接提交。`);
|
|
4082
|
+
if (mainRows.length) {
|
|
4083
|
+
lines.push('', '**已填内容**', '', '| 字段 | 值 |', '| --- | --- |');
|
|
4084
|
+
for (const row of mainRows) {
|
|
4085
|
+
lines.push(`| ${markdownCell(row.name)} | ${markdownCell(row.value)} |`);
|
|
4086
|
+
}
|
|
4087
|
+
}
|
|
4088
|
+
const groups = (previewDetails || []).filter((g) => (g?.columns || []).length);
|
|
4089
|
+
groups.forEach((group, index) => {
|
|
4090
|
+
const columns = group.columns;
|
|
4091
|
+
lines.push('', groups.length > 1 ? `**明细表 ${index + 1}**` : '**明细表**', '', `| 序号 | ${columns.map((c) => markdownCell(c.fieldName)).join(' | ')} |`, `| --- | ${columns.map(() => '---').join(' | ')} |`);
|
|
4092
|
+
(group.rows || []).forEach((row, rowIndex) => {
|
|
4093
|
+
const values = (row?.values || []).map((v) => markdownCell(v));
|
|
4094
|
+
lines.push(`| ${rowIndex + 1} | ${values.join(' | ')} |`);
|
|
4095
|
+
});
|
|
4096
|
+
});
|
|
4097
|
+
if (missing.length) {
|
|
4098
|
+
lines.push('', '**还需要你提供**', '');
|
|
4099
|
+
missing.forEach((item, index) => {
|
|
4100
|
+
const subFormId = String(item?.subFormId ?? '').trim();
|
|
4101
|
+
const rowIndex = Number(item?.dataIndex);
|
|
4102
|
+
const where = subFormId
|
|
4103
|
+
? `明细表第 ${Number.isFinite(rowIndex) && rowIndex > 0 ? rowIndex : 1} 行`
|
|
4104
|
+
: '主表';
|
|
4105
|
+
lines.push(`${index + 1}. **${markdownCell(item?.name ?? item?.id)}**(${where})`);
|
|
4106
|
+
const recommendation = historyRecommendation(historyRecords, String(item?.id ?? ''));
|
|
4107
|
+
if (recommendation) {
|
|
4108
|
+
lines.push(` - 历史推荐值:${markdownCell(recommendation.value)}(${recommendation.note})`);
|
|
4109
|
+
}
|
|
4110
|
+
});
|
|
4111
|
+
// 缺项类型提示(2026-10-14 用户令):缺项是**发票类 / 附件类**时,把"怎么把它给到"在渲染档里
|
|
4112
|
+
// 一次说清。真机现场:这两类缺项会让模型在"要不要补一句提醒用户怎么给"上反复称量(思考链里
|
|
4113
|
+
// 最长的尾巴就出在这里)——按本项目既有原则,把容易让模型绕圈的判断留在命令侧。
|
|
4114
|
+
// 缺项可能不在 `fieldMeta` 里(联动命中后才新增的必填,不在 createForm 的静态快照里):
|
|
4115
|
+
// 判不出类型就不加,宁可少一句也不猜。
|
|
4116
|
+
const metaById = new Map();
|
|
4117
|
+
for (const meta of fieldMeta || [])
|
|
4118
|
+
metaById.set(String(meta?.id ?? ''), meta);
|
|
4119
|
+
const missingHas = (match) => missing.some((item) => {
|
|
4120
|
+
const meta = metaById.get(String(item?.id ?? ''));
|
|
4121
|
+
return !!meta && match(meta);
|
|
4122
|
+
});
|
|
4123
|
+
if (missingHas((meta) => meta.componentKey === 'EinvoiceComponent' || meta.isEinvoice === true)) {
|
|
4124
|
+
lines.push('', '其中发票可以直接从你的票夹里选,也可以上传本地发票文件。');
|
|
4125
|
+
}
|
|
4126
|
+
if (missingHas((meta) => !!meta.fileUploadParams)) {
|
|
4127
|
+
lines.push('', '附件需要上传本地文件。');
|
|
4128
|
+
}
|
|
4129
|
+
}
|
|
4130
|
+
const linkage = String(pending.linkageResult ?? '').trim();
|
|
4131
|
+
if (linkage)
|
|
4132
|
+
lines.push('', linkage);
|
|
4133
|
+
return lines.join('\n').trim();
|
|
4134
|
+
}
|
|
3259
4135
|
function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, hiddenFieldIds, compsFieldIds, entries, layoutSubformIds) {
|
|
3260
4136
|
if (!linkageDetails || !linkageDetails.length)
|
|
3261
4137
|
return '';
|
|
@@ -3300,6 +4176,167 @@ function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden,
|
|
|
3300
4176
|
return result;
|
|
3301
4177
|
}
|
|
3302
4178
|
// ---------------------------------------------------------------------------
|
|
4179
|
+
// 小e 环境:createRequest 改交浏览器页面填单(不写服务端)
|
|
4180
|
+
// ---------------------------------------------------------------------------
|
|
4181
|
+
/**
|
|
4182
|
+
* 小e 环境下 `createRequest` 不写服务端,改为把「本次要填的字段值」渲染成 `<browsertool>` 块,
|
|
4183
|
+
* 由宿主的浏览器页面把值带进新建页 —— 2026-10-15 用户裁定。
|
|
4184
|
+
*
|
|
4185
|
+
* 边界(用户口径,逐条对齐):
|
|
4186
|
+
* - **无条件直出**:`action=draft` / `submit` 都一样,既不请求 `saveData` / `flow/create` / `submit`,
|
|
4187
|
+
* 也不做必填缺失判定(不返回 `NEEDS_INPUT`);缺什么由浏览器页面里补。
|
|
4188
|
+
* - 「字段重名 / 只读字段 / 值解析歧义」三类**连值都拼不出来**的结构性拦截**照旧生效**(在下面走
|
|
4189
|
+
* 的就是与普通链路同一段代码),它们不是必填判定。
|
|
4190
|
+
* - 句柄**不消费**:本分支在任何写请求之前返回,同一个 `context` 之后仍可正常重发。
|
|
4191
|
+
* - `title` 取**工作流名称**;`src` 与两处 `routerProps_*` 的 `initialEntries` 用**工作流 id**;
|
|
4192
|
+
* `params.customParams` 取 `createRequest` **入参**经解析后的字段值(不含表单默认值、不含联动带出的值),
|
|
4193
|
+
* 同段 `fieldAssignKeys` 列出这次要填的**全部字段 id**(`,` 连接)。
|
|
4194
|
+
*/
|
|
4195
|
+
const XIAOE_FILL_FORM_TOOL_ID = 'fill_form';
|
|
4196
|
+
/** 直出 `<browsertool>` 时写死的骨架(除 title / src / `routerProps_*` / `params.customParams` 外的全部内容)。 */
|
|
4197
|
+
const XIAOE_FILL_FORM_SDK_OPTIONS = {
|
|
4198
|
+
compName: 'ui.CorsComponent',
|
|
4199
|
+
routerType: 'MemoryRouter',
|
|
4200
|
+
// 用户 2026-10-15 裁定:PC / 移动各一份 params,app 写死 `@weapp/workflow`。
|
|
4201
|
+
// 2026-10-16 用户澄清:`@` 后面**没有**斜线,`weapp` 与 `workflow` 之间**一个**斜线
|
|
4202
|
+
// ——聊天输入框会把这个值转义显示成 `@/weapp/workflow`,一律以本条为准。
|
|
4203
|
+
// 路由组件分别取 `Comp.WffpRoute`(PC)/ `Comp.MWffpRoute`(移动,带 noMDialog)。
|
|
4204
|
+
params_pc: {
|
|
4205
|
+
app: '@weapp/workflow',
|
|
4206
|
+
compName: 'Comp.WffpRoute',
|
|
4207
|
+
},
|
|
4208
|
+
params_mobile: {
|
|
4209
|
+
app: '@weapp/workflow',
|
|
4210
|
+
compName: 'Comp.MWffpRoute',
|
|
4211
|
+
noMDialog: true,
|
|
4212
|
+
},
|
|
4213
|
+
};
|
|
4214
|
+
/**
|
|
4215
|
+
* `params.customParams` 字段值的两个分隔符 —— 口径来自用户 2026-10-15 给的实机截图:
|
|
4216
|
+
* - **同一字段多值用 `,`**:`?<字段id>=0,1,2`(下拉复选框三选全中)
|
|
4217
|
+
* - **明细多行用 `;`**:`?<明细字段id>=第一行;第二行;第三行`
|
|
4218
|
+
* - 两者组合:`?<明细字段id>=0,1;2,0,2`(行内多值用 `,`、行与行之间用 `;`)
|
|
4219
|
+
*/
|
|
4220
|
+
const XIAOE_INITIAL_VALUES_MULTI_SEPARATOR = ',';
|
|
4221
|
+
const XIAOE_INITIAL_VALUES_ROW_SEPARATOR = ';';
|
|
4222
|
+
/** 单个标量 → URL 传参里的那一段文本。对象按「id 优先」取标识(附件取到的就是 fileid)。 */
|
|
4223
|
+
function xiaoEUrlParamScalar(value) {
|
|
4224
|
+
if (value === null || value === undefined)
|
|
4225
|
+
return '';
|
|
4226
|
+
if (typeof value === 'object') {
|
|
4227
|
+
const obj = value;
|
|
4228
|
+
// 解析后的字段值是 `[{id, name}]` 形态(附件 / 选项 / 浏览按钮 / 人员范围都走这一份),
|
|
4229
|
+
// `id` 就是可直接落库的标识:选项类是选项值(0/1/2)、附件类是 fileid、浏览按钮是记录 id。
|
|
4230
|
+
// `name` / `content` 一律不用 —— 附件传文件名等于传一个指向不存在文件的坏 ID。
|
|
4231
|
+
for (const key of ['id', 'optionId', 'fileId', 'fileid', 'fid', 'value']) {
|
|
4232
|
+
const picked = obj[key];
|
|
4233
|
+
if (picked === null || picked === undefined)
|
|
4234
|
+
continue;
|
|
4235
|
+
const text = String(picked).trim();
|
|
4236
|
+
if (text)
|
|
4237
|
+
return text;
|
|
4238
|
+
}
|
|
4239
|
+
return '';
|
|
4240
|
+
}
|
|
4241
|
+
return String(value).trim();
|
|
4242
|
+
}
|
|
4243
|
+
/** 单个字段的值 → URL 传参里的那一段文本(数组按「同字段多值」用 `,` 连接)。 */
|
|
4244
|
+
function xiaoEUrlParamValue(value) {
|
|
4245
|
+
if (value === null || value === undefined)
|
|
4246
|
+
return '';
|
|
4247
|
+
if (Array.isArray(value)) {
|
|
4248
|
+
return value
|
|
4249
|
+
.map((item) => xiaoEUrlParamScalar(item))
|
|
4250
|
+
.filter(Boolean)
|
|
4251
|
+
.join(XIAOE_INITIAL_VALUES_MULTI_SEPARATOR);
|
|
4252
|
+
}
|
|
4253
|
+
return xiaoEUrlParamScalar(value);
|
|
4254
|
+
}
|
|
4255
|
+
/**
|
|
4256
|
+
* `params.customParams` 里**字段值**那一份 = `{字段id: 值}`(用户裁定用**字段 id**,不用数据 key);
|
|
4257
|
+
* 同时回一份**字段 id 顺序表**,供同段 `fieldAssignKeys`(`,` 连接)用。
|
|
4258
|
+
*
|
|
4259
|
+
* 数据源是 `createRequest` **入参**经解析后的结果(`resolveEntryValues` 的 `preview`)——只含
|
|
4260
|
+
* 模型本次真的传了的字段,不含表单默认值、不含联动带出的值。
|
|
4261
|
+
*
|
|
4262
|
+
* 明细字段(`preview` 里带 `dataIndex`)按**行号升序**取值、用 `;` 连接;主表字段就是它自己的值。
|
|
4263
|
+
* 值为空(没解析出内容)的字段整个省略,不生成空参数。
|
|
4264
|
+
*/
|
|
4265
|
+
function xiaoECustomParams(preview) {
|
|
4266
|
+
const ordered = [];
|
|
4267
|
+
const mainValues = new Map();
|
|
4268
|
+
const detailValues = new Map();
|
|
4269
|
+
for (const item of toArray(preview)) {
|
|
4270
|
+
const fieldId = String(item?.fieldId ?? '').trim();
|
|
4271
|
+
if (!fieldId)
|
|
4272
|
+
continue;
|
|
4273
|
+
const text = xiaoEUrlParamValue(item?.value);
|
|
4274
|
+
if (!text)
|
|
4275
|
+
continue;
|
|
4276
|
+
const isDetail = item?.dataIndex !== undefined && item?.dataIndex !== null;
|
|
4277
|
+
if (!isDetail) {
|
|
4278
|
+
if (!mainValues.has(fieldId) && !detailValues.has(fieldId))
|
|
4279
|
+
ordered.push(fieldId);
|
|
4280
|
+
mainValues.set(fieldId, text);
|
|
4281
|
+
continue;
|
|
4282
|
+
}
|
|
4283
|
+
if (!detailValues.has(fieldId)) {
|
|
4284
|
+
if (!mainValues.has(fieldId))
|
|
4285
|
+
ordered.push(fieldId);
|
|
4286
|
+
detailValues.set(fieldId, []);
|
|
4287
|
+
}
|
|
4288
|
+
const index = Number(item.dataIndex);
|
|
4289
|
+
detailValues.get(fieldId).push({ index: Number.isFinite(index) ? index : 0, text });
|
|
4290
|
+
}
|
|
4291
|
+
const values = {};
|
|
4292
|
+
for (const fieldId of ordered) {
|
|
4293
|
+
const rows = detailValues.get(fieldId);
|
|
4294
|
+
if (rows && rows.length) {
|
|
4295
|
+
rows.sort((a, b) => a.index - b.index);
|
|
4296
|
+
values[fieldId] = rows.map((row) => row.text).join(XIAOE_INITIAL_VALUES_ROW_SEPARATOR);
|
|
4297
|
+
continue;
|
|
4298
|
+
}
|
|
4299
|
+
values[fieldId] = mainValues.get(fieldId) ?? '';
|
|
4300
|
+
}
|
|
4301
|
+
return { values, keys: ordered };
|
|
4302
|
+
}
|
|
4303
|
+
/**
|
|
4304
|
+
* 渲染 `<browsertool>` 块(该块**整体**就是小e 环境下 `createRequest` 的 `finalMarkdown`)。
|
|
4305
|
+
*
|
|
4306
|
+
* 除 `title` / `src` / 两处 `routerProps_*` 的 `initialEntries` / `params.customParams` 外全部写死。
|
|
4307
|
+
*/
|
|
4308
|
+
function renderXiaoEFillFormMarkdown(params) {
|
|
4309
|
+
const base = String(params.baseUrl ?? '').replace(/\/+$/u, '');
|
|
4310
|
+
const workflowId = String(params.workflowId ?? '').trim();
|
|
4311
|
+
const createPath = `/flowpage/create/${workflowId}`;
|
|
4312
|
+
const payload = {
|
|
4313
|
+
version: 1,
|
|
4314
|
+
callId: 'call_001',
|
|
4315
|
+
toolId: XIAOE_FILL_FORM_TOOL_ID,
|
|
4316
|
+
arguments: {
|
|
4317
|
+
type: 'e10',
|
|
4318
|
+
title: String(params.title ?? ''),
|
|
4319
|
+
src: `${base}/sp/workflow${createPath}?workflowId=${workflowId}&isCreate=1`,
|
|
4320
|
+
sdkOptions: {
|
|
4321
|
+
compName: XIAOE_FILL_FORM_SDK_OPTIONS.compName,
|
|
4322
|
+
routerType: XIAOE_FILL_FORM_SDK_OPTIONS.routerType,
|
|
4323
|
+
routerProps_pc: { initialEntries: [createPath] },
|
|
4324
|
+
routerProps_mobile: { initialEntries: [createPath] },
|
|
4325
|
+
params_pc: { ...XIAOE_FILL_FORM_SDK_OPTIONS.params_pc },
|
|
4326
|
+
params_mobile: { ...XIAOE_FILL_FORM_SDK_OPTIONS.params_mobile },
|
|
4327
|
+
params: {
|
|
4328
|
+
customParams: {
|
|
4329
|
+
// 字段 id → 值(要填写的字段及值),随后是这次要填的**全部字段 id**。
|
|
4330
|
+
...params.customParams.values,
|
|
4331
|
+
fieldAssignKeys: params.customParams.keys.join(','),
|
|
4332
|
+
},
|
|
4333
|
+
},
|
|
4334
|
+
},
|
|
4335
|
+
},
|
|
4336
|
+
};
|
|
4337
|
+
return `<browsertool>\n${JSON.stringify(payload, null, 2)}\n</browsertool>`;
|
|
4338
|
+
}
|
|
4339
|
+
// ---------------------------------------------------------------------------
|
|
3303
4340
|
// createRequest(合并原 createPrepare + createApply)
|
|
3304
4341
|
// ---------------------------------------------------------------------------
|
|
3305
4342
|
/**
|
|
@@ -3346,13 +4383,30 @@ async function commandCreateRequest(args, env) {
|
|
|
3346
4383
|
}
|
|
3347
4384
|
const [details, valueAmbiguities, preview] = await resolveEntryValues(env, entries, context.layout.form_id);
|
|
3348
4385
|
const merged = mergeDetails(context.defaults || [], details);
|
|
3349
|
-
|
|
4386
|
+
/**
|
|
4387
|
+
* 小e 环境(2026-10-16 用户裁定):**字段联动与显示属性联动都不跑**。
|
|
4388
|
+
*
|
|
4389
|
+
* 这一档真正的填单动作由宿主浏览器页面完成(见下方 `BROWSER_FILL` 分支),本命令只负责把
|
|
4390
|
+
* 「本次要填的字段值」交给页面:联动**带出的值**、以及联动求出来的隐藏 / 条件必填 / 禁填,
|
|
4391
|
+
* 到了表单页里都会由页面自己重算一遍。在这里跑一是白费一趟 `fdLinkage` 请求,二是这些结果
|
|
4392
|
+
* 全都进不了本档的输出(`customParams` 取的是 `preview`,即入参解析后的值)。
|
|
4393
|
+
*
|
|
4394
|
+
* ⚠️ 与同一次裁定里 `createForm` 的差别:那边是「不跑字段联动、**显示属性联动照跑**」
|
|
4395
|
+
* (`buildCreateFormData`,那边的联动结果要用来算字段的必填 / 可编辑清单);这里两者都不跑 ——
|
|
4396
|
+
* 本档既不做必填判定、也不发写请求。非小e 环境行为不变。
|
|
4397
|
+
*/
|
|
4398
|
+
const isXiaoE = hasXiaoEEnv(env.ctx.env);
|
|
4399
|
+
const linkageDetails = isXiaoE
|
|
4400
|
+
? []
|
|
4401
|
+
: await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false);
|
|
3350
4402
|
// 明细默认值无行号,而判缺失/写载荷都按行号索引 ⇒ 按用户已建行补齐缺口(V12.1.35,
|
|
3351
4403
|
// 详见 fillDetailDefaultsByRow 注释)。放在联动之后合并,联动带出的值仍然优先。
|
|
3352
4404
|
const merged2 = fillDetailDefaultsByRow(mergeLinkageDetails(merged, linkageDetails), context.defaults || [], userDetailRows(entries));
|
|
3353
4405
|
const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
|
|
3354
4406
|
const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
|
|
3355
|
-
const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] =
|
|
4407
|
+
const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = isXiaoE
|
|
4408
|
+
? emptyLinkageVisibility()
|
|
4409
|
+
: computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues, fields);
|
|
3356
4410
|
const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
|
|
3357
4411
|
if (initialHidden.size) {
|
|
3358
4412
|
const mh = new Set(rowHidden.main || []);
|
|
@@ -3390,25 +4444,52 @@ async function commandCreateRequest(args, env) {
|
|
|
3390
4444
|
}
|
|
3391
4445
|
const missingAll = [...missing, ...systemMissing];
|
|
3392
4446
|
/**
|
|
3393
|
-
*
|
|
4447
|
+
* 历史参考值在 `createRequest` 一侧**不再返回**(V12.1.102 撤出;V12.1.77 曾在这里从句柄缓存
|
|
4448
|
+
* 筛"这次新出现的必填字段"的历史值一并给回)。
|
|
3394
4449
|
*
|
|
3395
|
-
*
|
|
3396
|
-
*
|
|
3397
|
-
*
|
|
4450
|
+
* 撤出理由(2026-10-13 真机复核):文档口径早已明令 `NEEDS_INPUT` / `NEEDS_CONFIRMATION` 时
|
|
4451
|
+
* `historyRequestData` **一律不看、不用**(必填缺失直接一次问齐用户,不做任何值推断)——这份数据
|
|
4452
|
+
* 于是只剩副作用:既白占返回体积(真机 3,649 B 里占 2,173 B=60%,且 5 条记录还是同一组值的
|
|
4453
|
+
* 重复),又逼模型多烧一轮说服自己"不要看",返回值与规则自相矛盾。
|
|
3398
4454
|
*
|
|
3399
|
-
* `createForm
|
|
3400
|
-
*
|
|
3401
|
-
*
|
|
4455
|
+
* 历史参考值的唯一入口回到 `createForm`(那边给的仍是"本表单静态必填字段"的参考值)。
|
|
4456
|
+
*
|
|
4457
|
+
* 2026-10-13 晚补(用户口径):原始历史**仍然不返回**给模型,但命令**内部**继续用它——
|
|
4458
|
+
* 为缺失的必填字段算一条「历史取值一致」的**推荐值**渲染进 `finalMarkdown`(见
|
|
4459
|
+
* `historyRecommendation`)。这样用户能一句话确认,而"一致性判断"这件容易让模型绕圈的事
|
|
4460
|
+
* 全部留在命令侧,模型拿到的仍是一份可直接粘贴的文本。
|
|
3402
4461
|
*/
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
4462
|
+
/**
|
|
4463
|
+
* 「已填内容」的两组行(2026-10-14 用户口径):**系统字段(流程标题等)在前、业务字段按表单
|
|
4464
|
+
* 布局顺序随后**,两份渲染档(必填缺失 / 已落地)共用同一份口径。值取自 `merged2`
|
|
4465
|
+
* (表单默认值 + 本次入参 + 联动带出的值),也就是"用户在表单上看到的那一份"。
|
|
4466
|
+
*
|
|
4467
|
+
* 真机现场:用户看到的「已填内容」只有模型显式传的两项,而表单上明明还有一批**已填状态**的
|
|
4468
|
+
* 字段(表单默认值、流程标题)——用户无法据此确认"这张单到底填成什么样了"。
|
|
4469
|
+
*/
|
|
4470
|
+
const mainValueById = new Map();
|
|
4471
|
+
for (const detail of merged2) {
|
|
4472
|
+
if (!detail || typeof detail !== 'object')
|
|
3408
4473
|
continue;
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
4474
|
+
const subFormId = String((detail.subForm && detail.subForm.id) ?? detail.subFormId ?? '').trim();
|
|
4475
|
+
if (subFormId)
|
|
4476
|
+
continue;
|
|
4477
|
+
const fieldId = String((detail.formField && detail.formField.id) ?? '').trim();
|
|
4478
|
+
if (fieldId)
|
|
4479
|
+
mainValueById.set(fieldId, detail);
|
|
4480
|
+
}
|
|
4481
|
+
const filledFieldRows = mainFilledRows(fields, mainValueById,
|
|
4482
|
+
// `rowHidden.main` 已并入布局隐藏(见上面 `initialHidden` 的合流),再补 `hiddenCellFieldIds`。
|
|
4483
|
+
new Set([...(rowHidden.main || []), ...hiddenFieldIds]), new Set(rowDisabled.main || []), compsFieldIds);
|
|
4484
|
+
const filledTitleRows = (title) => systemFilledRows({
|
|
4485
|
+
title,
|
|
4486
|
+
requestLevel: requestLevelValue,
|
|
4487
|
+
secLevel: secLevelValue,
|
|
4488
|
+
permissions: sysPermissions,
|
|
4489
|
+
// 老句柄可能没有这两个选项清单(键名也随版本变过一次),取不到就退化成"不列系统字段"。
|
|
4490
|
+
levelOptions: (context.systemFields && (context.systemFields.level_options || context.systemFields.levelOptions)) || [],
|
|
4491
|
+
secLevelOptions: (context.systemFields && (context.systemFields.sec_level_options || context.systemFields.secLevelOptions)) || [],
|
|
4492
|
+
});
|
|
3412
4493
|
if (valueAmbiguities.length) {
|
|
3413
4494
|
return workflowSuccess('workflow.createRequest', {
|
|
3414
4495
|
status: 'NEEDS_CONFIRMATION',
|
|
@@ -3420,20 +4501,76 @@ async function commandCreateRequest(args, env) {
|
|
|
3420
4501
|
resolvedValues: preview,
|
|
3421
4502
|
ambiguities: valueAmbiguities,
|
|
3422
4503
|
missingFields: missingAll,
|
|
3423
|
-
historyRequestData,
|
|
3424
4504
|
linkageResult,
|
|
3425
4505
|
...folderPayload,
|
|
3426
4506
|
});
|
|
3427
4507
|
}
|
|
4508
|
+
/**
|
|
4509
|
+
* 小e 环境(2026-10-15 用户裁定):**一个写请求都不发**,直接把本次要填的字段值渲染成
|
|
4510
|
+
* `<browsertool>` 交给宿主的浏览器页面 —— 因此这里**不判必填缺失**(不返回 `NEEDS_INPUT`),
|
|
4511
|
+
* `draft` / `submit` 走的都是这一条。
|
|
4512
|
+
*
|
|
4513
|
+
* 放置位置:在所有"连值都拼不出来"的结构性拦截(字段重名 / 只读字段 / 值解析歧义,都在本行之上)
|
|
4514
|
+
* **之后**、必填缺失分支与写入路径(`verifyContextLease(token, …, true)` 消费句柄、`saveData`、
|
|
4515
|
+
* `flow/create`、`submit`)**之前**。于是:句柄不被消费、一个写请求都不发,同一个 `context` 之后
|
|
4516
|
+
* 仍可正常重发。
|
|
4517
|
+
*
|
|
4518
|
+
* 渲染档(2026-10-15 用户裁定):`finalMarkdown` **整体就是一段 `<browsertool>` 标签块**;本轮回复正文
|
|
4519
|
+
* = 这段内容本身(从 `<browsertool>` 到 `</browsertool>`),原样输出、**不要包在代码块里**。
|
|
4520
|
+
*
|
|
4521
|
+
* **stdout 形态**(同日第二次裁定「xiaoe 下应该直接输出 `<browsertool>` 标签」):这一档的 stdout
|
|
4522
|
+
* **就是那段标签块本身,不套 JSON 信封** —— 判定与落点都在 `shortcuts/workflow/index.ts` 的
|
|
4523
|
+
* `rawStdoutOf()` / `executeAndPrint()`(放在命令层是因为"打到 stdout 的形态"属命令层职责)。
|
|
4524
|
+
* 所以下面的 `message` / `workflow` 等键**不会出现在 stdout 上**,它们只对 `--input-ref` 这类
|
|
4525
|
+
* 程序化消费者有意义;模型侧看到的就是标签块,读法见 `workflow-create.md` 第 3 步与处置表。
|
|
4526
|
+
*
|
|
4527
|
+
* 判据:`auth/xiaoe.ts` 的 `hasXiaoEEnv(env.ctx.env)`(2026-10-16 用户裁定,统一走公共判据)。
|
|
4528
|
+
* 它已覆盖宿主的两种形态 —— 网页端(`XIAOE_USER_TOKEN` + `XIAOE_DOMAIN` 平台凭证)与桌面客户端
|
|
4529
|
+
* (`XIAOE_DATA_ROOT` / `XIAOE_ROBOT_DATA_ROOT` / `XIAOE_USER_SETTINGS_PATH` / `XIAOE_ROBOT_ROOT`
|
|
4530
|
+
* 四个运行期路径变量),同时仍对 `WORKBUDDY_*` / `CODEX_*` 做排他短路。本处复用上面算好的
|
|
4531
|
+
* `isXiaoE`(同一判据,只求值一次)。
|
|
4532
|
+
*
|
|
4533
|
+
* 本档**不跑字段联动、也不跑显示属性联动**(2026-10-16 用户裁定,见上方 `isXiaoE` 的注释):
|
|
4534
|
+
* 所以这里既没有「字段联动带出」的提示,`hiddenSubforms` / `rowExtraRequired` / `rowDisabled`
|
|
4535
|
+
* 也都是空集 —— 该由浏览器页面在表单里自己算。
|
|
4536
|
+
*/
|
|
4537
|
+
if (isXiaoE) {
|
|
4538
|
+
// `src` 要的是绝对地址:句柄里带走的那份 baseUrl 为准(与句柄同源,避免"换了域名后旧句柄拼出别的站"),
|
|
4539
|
+
// 旧版本句柄可能没带 ⇒ 回落到当前会话的 baseUrl。
|
|
4540
|
+
const xiaoEBaseUrl = String(context.baseUrl ?? '') || (await getSessionContext(env)).baseUrl;
|
|
4541
|
+
return workflowSuccess('workflow.createRequest', {
|
|
4542
|
+
status: 'BROWSER_FILL',
|
|
4543
|
+
contextId: token,
|
|
4544
|
+
message: '当前是小e环境:本次不向服务端写入,改由浏览器页面完成填单。命令已把 <browsertool> 标签块直接输出到 stdout(本档不打 JSON 信封),把那段输出原样作为本轮回复正文即可(不要包在代码块里、不要加任何前后说明)。',
|
|
4545
|
+
workflow: context.workflow,
|
|
4546
|
+
finalMarkdown: renderXiaoEFillFormMarkdown({
|
|
4547
|
+
baseUrl: xiaoEBaseUrl,
|
|
4548
|
+
title: String(context.workflow?.name ?? ''),
|
|
4549
|
+
workflowId: String(context.workflow?.id ?? context.runtime?.workflow_id ?? ''),
|
|
4550
|
+
customParams: xiaoECustomParams(preview),
|
|
4551
|
+
}),
|
|
4552
|
+
});
|
|
4553
|
+
}
|
|
3428
4554
|
if (action === 'submit' && missingAll.length) {
|
|
3429
|
-
// submit
|
|
4555
|
+
// submit 且有必填缺失:**不调用创建接口**,把缺失项一次交回给模型/用户。
|
|
4556
|
+
//
|
|
4557
|
+
// 同时给一份可直接原样粘贴的渲染档(V12.1.102):结论 + 主表「已填内容」两列表(含流程标题与
|
|
4558
|
+
// 表单默认值,2026-10-14 起)+ 明细 Excel 列 + 待补必填项清单。模型不必自己拼这些表格,也不必再
|
|
4559
|
+
// 遍历 `resolvedValues` 拼待补清单;因此这里**不再返回** `resolvedValues`(渲染素材只在命令内部用)。
|
|
4560
|
+
const pendingRequestName = String(args.requestName ?? sysDefaults.requestName ?? context.runtime.default_request_name ?? '').trim();
|
|
3430
4561
|
return workflowSuccess('workflow.createRequest', {
|
|
3431
4562
|
status: 'NEEDS_INPUT',
|
|
3432
4563
|
contextId: token,
|
|
3433
4564
|
message: '缺少必填字段,本次未发起流程。请把下面这些字段一次问齐后,带同一 context 重发 action=submit。',
|
|
3434
|
-
resolvedValues: preview,
|
|
3435
4565
|
missingFields: missingAll,
|
|
3436
|
-
|
|
4566
|
+
finalMarkdown: renderCreatePendingMarkdown({ missingFields: missingAll, linkageResult },
|
|
4567
|
+
// 标题行按"当前还没填到标题"的口径取(可能为空 ⇒ 不出现该行),与落地档的 `requestName` 兜底分开。
|
|
4568
|
+
[...filledTitleRows(pendingRequestName), ...filledFieldRows], buildPreviewDetails(preview),
|
|
4569
|
+
// 历史推荐值的数据源:`createForm` 随句柄留下的**宽缓存**(本表单全部字段)。
|
|
4570
|
+
// 只用来算"这个缺失字段历史取值是否一致",**不返回**给模型——一致性判断不外包给模型。
|
|
4571
|
+
context.historyCache,
|
|
4572
|
+
// 缺项类型提示用:缺项是发票 / 附件类时把"怎么给"写在渲染档里(见函数内注释)。
|
|
4573
|
+
fields),
|
|
3437
4574
|
linkageResult,
|
|
3438
4575
|
...folderPayload,
|
|
3439
4576
|
});
|
|
@@ -3637,6 +4774,12 @@ async function commandCreateRequest(args, env) {
|
|
|
3637
4774
|
: '。');
|
|
3638
4775
|
}
|
|
3639
4776
|
}
|
|
4777
|
+
// 渲染档:流程**已落地**时给一份可直接原样粘贴的最终回复文本(与查询侧 `finalMarkdown` 同口径)。
|
|
4778
|
+
// `NEEDS_INPUT` 的那份在上面分支里就已挂上(同口径、只换结论句与末段);`NEEDS_CONFIRMATION` 不给。
|
|
4779
|
+
// 标题行用**本次真正落库的** `requestName`(与 `prepared.requestName` 同源),不再是"可能为空"的口径。
|
|
4780
|
+
if (['CREATED', 'CREATED_SUBMITTED', 'CREATED_SUBMIT_FAILED'].includes(String(result.status))) {
|
|
4781
|
+
result.finalMarkdown = renderCreateResultMarkdown(result, [...filledTitleRows(requestName), ...filledFieldRows]);
|
|
4782
|
+
}
|
|
3640
4783
|
return workflowSuccess('workflow.createRequest', result);
|
|
3641
4784
|
}
|
|
3642
4785
|
function formatRemark(value) {
|