weaver-work-cli 0.1.5 → 0.1.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -9
- package/dist/cmd/skills/index.js +60 -0
- package/dist/internal/e10/auth/commands.js +48 -32
- package/dist/internal/e10/auth/session.js +44 -3
- package/dist/internal/e10/auth/xiaoe.js +363 -11
- package/dist/internal/e10/context.js +4 -2
- package/dist/internal/skills/detector.js +42 -0
- package/dist/internal/skills/install.js +50 -3
- package/dist/shortcuts/archive/render/search-format.js +12 -3
- package/dist/shortcuts/ebuilder-form/continuation.js +70 -0
- package/dist/shortcuts/ebuilder-form/errors.js +54 -0
- package/dist/shortcuts/ebuilder-form/host.js +1238 -0
- package/dist/shortcuts/ebuilder-form/index.js +108 -0
- package/dist/shortcuts/ebuilder-form/manifest.js +347 -0
- package/dist/shortcuts/ebuilder-form/operations/read.js +157 -0
- package/dist/shortcuts/ebuilder-form/operations/registry.js +56 -0
- package/dist/shortcuts/ebuilder-form/operations/shared.js +132 -0
- package/dist/shortcuts/ebuilder-form/operations/types.js +19 -0
- package/dist/shortcuts/ebuilder-form/operations/write.js +277 -0
- package/dist/shortcuts/ebuilder-form/operations.js +8 -0
- package/dist/shortcuts/ehr/operations/salary.js +2 -140
- package/dist/shortcuts/im/continuation.js +70 -0
- package/dist/shortcuts/{yimiaoban → im}/errors.js +14 -14
- package/dist/shortcuts/{yimiaoban → im}/host.js +26 -26
- package/dist/shortcuts/{yimiaoban → im}/index.js +42 -42
- package/dist/shortcuts/{yimiaoban → im}/manifest.js +7 -6
- package/dist/shortcuts/{yimiaoban → im}/operations/file-user.js +54 -41
- package/dist/shortcuts/{yimiaoban → im}/operations/group-read.js +46 -44
- package/dist/shortcuts/{yimiaoban → im}/operations/group-write.js +113 -66
- package/dist/shortcuts/{yimiaoban → im}/operations/msg-sync.js +164 -141
- package/dist/shortcuts/{yimiaoban → im}/operations/msg-write.js +47 -47
- package/dist/shortcuts/{yimiaoban → im}/operations/person.js +10 -10
- package/dist/shortcuts/{yimiaoban → im}/operations/registry.js +10 -8
- package/dist/shortcuts/{yimiaoban → im}/operations/session.js +82 -83
- package/dist/shortcuts/im/operations/shared.js +442 -0
- package/dist/shortcuts/{yimiaoban → im}/operations/types.js +1 -1
- package/dist/shortcuts/im/operations/user-remind.js +201 -0
- package/dist/shortcuts/{yimiaoban → im}/operations/write-util.js +7 -7
- package/dist/shortcuts/{yimiaoban → im}/operations.js +3 -3
- package/dist/shortcuts/index.js +10 -2
- package/dist/shortcuts/invoice/manifest.js +113 -10
- package/dist/shortcuts/invoice/operations/import.js +39 -14
- package/dist/shortcuts/invoice/operations/reim.js +38 -18
- package/dist/shortcuts/invoice/operations/shared.js +35 -6
- package/dist/shortcuts/jiuchuanhui/manifest.js +3 -3
- package/dist/shortcuts/jiuchuanhui/operations/registry.js +2 -2
- package/dist/shortcuts/{yimiaoban → okr}/continuation.js +10 -10
- package/dist/shortcuts/okr/errors.js +70 -0
- package/dist/shortcuts/okr/host.js +169 -0
- package/dist/shortcuts/okr/index.js +155 -0
- package/dist/shortcuts/okr/manifest.js +70 -0
- package/dist/shortcuts/okr/operations/link.js +305 -0
- package/dist/shortcuts/okr/operations/read.js +481 -0
- package/dist/shortcuts/okr/operations/registry.js +31 -0
- package/dist/shortcuts/okr/operations/shared.js +381 -0
- package/dist/shortcuts/okr/operations/types.js +88 -0
- package/dist/shortcuts/okr/operations/write.js +838 -0
- package/dist/shortcuts/okr/operations.js +8 -0
- package/dist/shortcuts/project/continuation.js +70 -0
- package/dist/shortcuts/project/errors.js +53 -0
- package/dist/shortcuts/project/host.js +160 -0
- package/dist/shortcuts/project/index.js +106 -0
- package/dist/shortcuts/project/manifest.js +36 -0
- package/dist/shortcuts/project/operations/read.js +551 -0
- package/dist/shortcuts/project/operations/registry.js +31 -0
- package/dist/shortcuts/project/operations/shared.js +68 -0
- package/dist/shortcuts/project/operations/types.js +39 -0
- package/dist/shortcuts/project/operations/write.js +266 -0
- package/dist/shortcuts/project/operations.js +8 -0
- package/dist/shortcuts/workflow/continuation.js +508 -0
- package/dist/shortcuts/workflow/endpoints.js +149 -0
- package/dist/shortcuts/workflow/errors.js +112 -0
- package/dist/shortcuts/workflow/host.js +224 -0
- package/dist/shortcuts/workflow/index.js +116 -0
- package/dist/shortcuts/workflow/manifest.js +30 -0
- package/dist/shortcuts/workflow/operations/form.js +4159 -0
- package/dist/shortcuts/workflow/operations/read.js +3778 -0
- package/dist/shortcuts/workflow/operations/registry.js +33 -0
- package/dist/shortcuts/workflow/operations/shared.js +167 -0
- package/dist/shortcuts/workflow/operations/types.js +44 -0
- package/dist/shortcuts/workflow/operations/write.js +1991 -0
- package/dist/shortcuts/workflow/operations.js +119 -0
- package/dist/shortcuts/workflow/state.js +182 -0
- package/docs/SKILL.md +5 -3
- package/docs/_catalog.md +10 -1
- package/docs/agent-invoice.md +2 -2
- package/docs/agent-skill-install.md +2 -2
- package/docs/e10-auth.md +3 -1
- package/docs/ebuilder-form.md +38 -0
- package/docs/im.md +104 -0
- package/docs/invoice.md +5 -5
- package/docs/okr.md +122 -0
- package/docs/operation-manual.md +10 -7
- package/docs/project.md +69 -0
- package/docs/skill-review-2026-09-19.md +68 -0
- package/package.json +3 -2
- package/scripts/package-skill.mjs +38 -11
- package/scripts/sync-connector-skills.mjs +172 -0
- package/skill-template/business-info.json +357 -16
- package/skill-template/domains/ebuilder-form.md +5 -0
- package/skill-template/domains/okr.md +18 -0
- package/skill-template/domains/shijingran.md +3 -0
- package/skill-template/domains/skill-maker.md +4 -4
- package/skill-template/domains/workflow.md +22 -0
- package/skill-template/domains/yepiaotong.md +4 -2
- package/skill-template/domains/yimiaoban.md +13 -10
- package/skill-template/product.json +5 -0
- package/skill-template/skill-template.md +1 -0
- package/skills/weaver-e10-calendar/SKILL.md +5 -1
- package/skills/weaver-e10-calendar/references/calendar-query.md +7 -0
- package/skills/weaver-e10-calendar/references/source-manifest.json +314 -32
- package/skills/weaver-e10-ebuilder-form/SKILL.md +123 -0
- package/skills/weaver-e10-ebuilder-form/references/context-routing.md +90 -0
- package/skills/weaver-e10-ebuilder-form/references/custom-api.md +86 -0
- package/skills/weaver-e10-ebuilder-form/references/disabled-capabilities.md +50 -0
- package/skills/weaver-e10-ebuilder-form/references/fields-browser.md +100 -0
- package/skills/weaver-e10-ebuilder-form/references/list-nlist.md +91 -0
- package/skills/weaver-e10-ebuilder-form/references/openapi-read.md +74 -0
- package/skills/weaver-e10-ebuilder-form/references/openapi-write.md +86 -0
- package/skills/weaver-e10-ebuilder-form/references/source-manifest.json +107 -0
- package/skills/weaver-e10-esb/SKILL.md +1 -1
- package/skills/weaver-e10-esb/references/source-manifest.json +15 -14
- package/skills/weaver-e10-hrm/SKILL.md +1 -1
- package/skills/weaver-e10-hrm/references/source-manifest.json +357 -38
- package/skills/weaver-e10-jiuchuanhui/SKILL.md +22 -4
- package/skills/weaver-e10-jiuchuanhui/references/clue.md +4 -2
- package/skills/weaver-e10-jiuchuanhui/references/contact.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/references/customer.md +3 -3
- package/skills/weaver-e10-jiuchuanhui/references/disabled-capabilities.md +1 -1
- package/skills/weaver-e10-jiuchuanhui/references/discovery-config.md +10 -10
- package/skills/weaver-e10-jiuchuanhui/references/general-helpers.md +3 -3
- package/skills/weaver-e10-jiuchuanhui/references/sale.md +7 -7
- package/skills/weaver-e10-jiuchuanhui/references/source-manifest.json +3 -3
- package/skills/weaver-e10-jucailin/SKILL.md +2 -2
- package/skills/weaver-e10-mail/SKILL.md +22 -6
- package/skills/weaver-e10-meeting/SKILL.md +22 -1
- package/skills/weaver-e10-okr/SKILL.md +139 -0
- package/skills/weaver-e10-okr/references/okr-align.md +59 -0
- package/skills/weaver-e10-okr/references/okr-keyresult.md +68 -0
- package/skills/weaver-e10-okr/references/okr-link-routing.md +67 -0
- package/skills/weaver-e10-okr/references/okr-query.md +105 -0
- package/skills/weaver-e10-okr/references/okr-write.md +94 -0
- package/skills/weaver-e10-okr/references/source-manifest.json +672 -0
- package/skills/weaver-e10-plan/SKILL.md +3 -3
- package/skills/weaver-e10-plan/references/plan-report-write.md +1 -1
- package/skills/weaver-e10-plan/references/source-manifest.json +20 -20
- package/skills/weaver-e10-qiyecheng/SKILL.md +21 -1
- package/skills/weaver-e10-qiyecheng/references/source-manifest.json +17 -17
- package/skills/weaver-e10-shared/SKILL.md +79 -11
- package/skills/weaver-e10-shared/references/e10-auth-and-session.md +41 -8
- package/skills/weaver-e10-shared/references/non-interactive-environment.md +78 -0
- package/skills/weaver-e10-shared/references/weaver-e10-installation.md +39 -0
- package/skills/weaver-e10-shijingran/SKILL.md +83 -0
- package/skills/weaver-e10-shijingran/references/field-resolution.md +105 -0
- package/skills/weaver-e10-shijingran/references/operations.md +154 -0
- package/skills/weaver-e10-shijingran/references/source-manifest.json +1380 -0
- package/skills/weaver-e10-shijingran/references/write-operations.md +90 -0
- package/skills/weaver-e10-skill-maker/SKILL.md +26 -9
- package/skills/weaver-e10-skill-maker/references/business-skill-generation.md +39 -8
- package/skills/weaver-e10-skill-maker/references/detector-validation.md +25 -8
- package/skills/weaver-e10-skill-maker/references/module-cli-generation.md +66 -5
- package/skills/weaver-e10-skill-maker/references/module-gap-audit-and-repair.md +86 -0
- package/skills/weaver-e10-skill-maker/references/source-update-detection.md +59 -5
- package/skills/weaver-e10-skill-maker/references/weaver-skill-style.md +16 -8
- package/skills/weaver-e10-wenshuding/SKILL.md +28 -4
- package/skills/weaver-e10-wenshuding/references/archive-search.md +14 -0
- package/skills/weaver-e10-wenshuding/references/source-manifest.json +27 -26
- package/skills/weaver-e10-workflow/SKILL.md +250 -0
- package/skills/weaver-e10-workflow/references/source-manifest.json +1062 -0
- package/skills/weaver-e10-workflow/references/workflow-agent-entry.md +96 -0
- package/skills/weaver-e10-workflow/references/workflow-batch.md +275 -0
- package/skills/weaver-e10-workflow/references/workflow-create.md +182 -0
- package/skills/weaver-e10-workflow/references/workflow-edit.md +106 -0
- package/skills/weaver-e10-workflow/references/workflow-memory.md +165 -0
- package/skills/weaver-e10-workflow/references/workflow-operate.md +153 -0
- package/skills/weaver-e10-workflow/references/workflow-query.md +821 -0
- package/skills/weaver-e10-workflow/references/workflow-share.md +117 -0
- package/skills/weaver-e10-workflow/references/workflow-view.md +189 -0
- package/skills/weaver-e10-yepiaotong/SKILL.md +28 -28
- package/skills/weaver-e10-yepiaotong/references/invoice-disabled-capabilities.md +11 -5
- package/skills/weaver-e10-yepiaotong/references/invoice-import.md +14 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing-examples.md +9 -43
- package/skills/weaver-e10-yepiaotong/references/invoice-issuing.md +8 -0
- package/skills/weaver-e10-yepiaotong/references/invoice-reim.md +13 -17
- package/skills/weaver-e10-yepiaotong/references/reim-api-reference.md +6 -7
- package/skills/weaver-e10-yepiaotong/references/reim-examples.md +2 -2
- package/skills/weaver-e10-yepiaotong/references/source-manifest.json +1155 -553
- package/skills/weaver-e10-yimiaoban/SKILL.md +56 -38
- package/skills/weaver-e10-yimiaoban/references/ding-write.md +8 -8
- package/skills/weaver-e10-yimiaoban/references/error-codes.md +16 -0
- package/skills/weaver-e10-yimiaoban/references/field-resolution.md +10 -10
- package/skills/weaver-e10-yimiaoban/references/file-user-i18n.md +12 -12
- package/skills/weaver-e10-yimiaoban/references/group-read.md +16 -16
- package/skills/weaver-e10-yimiaoban/references/group-write.md +17 -15
- package/skills/weaver-e10-yimiaoban/references/msg-read.md +21 -17
- package/skills/weaver-e10-yimiaoban/references/msg-write.md +10 -10
- package/skills/weaver-e10-yimiaoban/references/person.md +6 -6
- package/skills/weaver-e10-yimiaoban/references/safety-boundaries.md +15 -11
- package/skills/weaver-e10-yimiaoban/references/session-sysmsg.md +11 -10
- package/skills/weaver-e10-yimiaoban/references/source-manifest.json +2164 -154
- package/skills/weaver-e10-yimiaoban/references/user-remind.md +75 -0
- package/skills/weaver-e10-ziguanjia/SKILL.md +4 -2
- package/skills/weaver-e10-ziguanjia/references/source-manifest.json +99 -29
- package/tools/weaver-skill-detector-1.0.13/SKILL.md +253 -0
- package/tools/weaver-skill-detector-1.0.13/_meta.json +6 -0
- package/tools/weaver-skill-detector-1.0.13/references/checklist.md +298 -0
- package/tools/weaver-skill-detector-1.0.13/scripts/validate_skill.py +4190 -0
- package/dist/shortcuts/yimiaoban/operations/shared.js +0 -236
- package/docs/yimiaoban.md +0 -86
- package/skills/weaver-e10-calendar/product.json +0 -8
- package/skills/weaver-e10-esb/product.json +0 -8
- package/skills/weaver-e10-hrm/product.json +0 -8
- package/skills/weaver-e10-jiuchuanhui/product.json +0 -8
- package/skills/weaver-e10-jucailin/product.json +0 -8
- package/skills/weaver-e10-mail/product.json +0 -8
- package/skills/weaver-e10-meeting/product.json +0 -8
- package/skills/weaver-e10-plan/product.json +0 -8
- package/skills/weaver-e10-qiyecheng/product.json +0 -8
- package/skills/weaver-e10-skill-maker/product.json +0 -8
- package/skills/weaver-e10-wenshuding/product.json +0 -8
- package/skills/weaver-e10-yepiaotong/product.json +0 -8
- package/skills/weaver-e10-yepiaotong/references/invoice-red.md +0 -261
- package/skills/weaver-e10-yimiaoban/product.json +0 -8
- package/skills/weaver-e10-ziguanjia/product.json +0 -8
- /package/dist/shortcuts/{yimiaoban → im}/operations/msg-parse.js +0 -0
|
@@ -0,0 +1,4159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* workflow/form.ts - 发起流程(create)与修改流程表单(edit)的 7 个 operation。
|
|
3
|
+
*
|
|
4
|
+
* 移植自 Python skill e10_workflow_create.py(4,963 行),行为须与该脚本逐条对齐。
|
|
5
|
+
* 认证由 CLI 托管(host 注入会话),本文件不实现登录/缓存(verify_login_auth /
|
|
6
|
+
* atrust_cookie_diagnosis / get_runtime / state/create_contexts 一律丢弃)。
|
|
7
|
+
*
|
|
8
|
+
* 原 Python 用磁盘文件保存「上下文(context)」,本 TS 模块改用 continuation 令牌承载上下文:
|
|
9
|
+
* createForm -> 发出 continuation(operation=workflow.createForm) 内嵌表单上下文
|
|
10
|
+
* createPrepare -> 校验该 continuation,产出预览 + 发出 continuation(operation=workflow.createPrepare)
|
|
11
|
+
* createApply -> 校验该 continuation,复核只读字段后 save+create
|
|
12
|
+
* editLoad / editPrepare / editSave 同理(operation 分别为 workflow.editLoad / workflow.editPrepare)。
|
|
13
|
+
*/
|
|
14
|
+
import { randomUUID } from 'node:crypto';
|
|
15
|
+
import { workflowSuccess, schemaOf, SCHEMA_REQUEST_ID, SCHEMA_CONTINUATION, SCHEMA_CONFIRM, } from './types.js';
|
|
16
|
+
import { responseData, toArray, withLeadingMeta, stdoutBytesOf, STDOUT_SAFE_BYTES, } from './shared.js';
|
|
17
|
+
import { workflowValidationError, workflowApiError, workflowPartialError, workflowPolicyError, WorkflowCliError, } from '../errors.js';
|
|
18
|
+
import { FORM_ENDPOINTS, AJAX_QUERY, formApiUrl } from '../endpoints.js';
|
|
19
|
+
import { continuationReplayError } from '../continuation.js';
|
|
20
|
+
import { writeStateFileBestEffort } from '../state.js';
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// 常量(与 Python 源逐条对齐)
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
const TEXTAREA_COMPONENTS = new Set(['textarea', 'richtext', 'editor', 'html', 'htmltext']);
|
|
25
|
+
const SELECT_COMPONENTS = new Set(['select', 'selectmultiple', 'radiobox', 'checkbox', 'radio', 'multiselect']);
|
|
26
|
+
const BROWSER_COMPONENTS = new Set([
|
|
27
|
+
'employee', 'department', 'subcompany', 'jobtitle', 'job', 'relatebrowser',
|
|
28
|
+
'browser', 'workflow', 'ebuilder', 'customer', 'project',
|
|
29
|
+
]);
|
|
30
|
+
const NON_INPUT_COMPONENTS = new Set([
|
|
31
|
+
'paragraph', 'label', 'divider', 'option', 'detailtable', 'group', 'layout', 'internaltitle', 'multilingualtag',
|
|
32
|
+
]);
|
|
33
|
+
const READONLY_COMPONENTS = new Set([
|
|
34
|
+
'dividingline', 'paragraph', 'slidercomponent', 'button', 'subtablecontrol', 'mobileinteraction',
|
|
35
|
+
]);
|
|
36
|
+
/** 系统字段键:流程标题/紧急程度/密级;流程编号 REQUESTMARK 忽略不校验。 */
|
|
37
|
+
const SYSTEM_FIELD_KEYS = {
|
|
38
|
+
REQUEST_NAMEL: '流程标题',
|
|
39
|
+
REQUEST_LEVEL: '紧急程度',
|
|
40
|
+
SECLEVEL: '密级',
|
|
41
|
+
};
|
|
42
|
+
/** 附件大小档位 0-5 对应 1/5/10/20/50/100MB;6/9 取同层 customAttmentSize(已修复 bug)。 */
|
|
43
|
+
const ATTACH_SIZE_LIMIT_STEPS_MB = [1, 5, 10, 20, 50, 100];
|
|
44
|
+
const CONTINUATION_TTL_MS = 10 * 60 * 1000;
|
|
45
|
+
/**
|
|
46
|
+
* `fields` 的紧凑编码(通用)。
|
|
47
|
+
*
|
|
48
|
+
* 字段多的表单里,`fields` 的键名要重复 N 次;pretty 输出下每个字段还会占掉 4~6 行缩进
|
|
49
|
+
* —— 实测某 278 字段的表单 `fields` 占 35.8KB,其中键名 + 缩进比真实数据还多。这里改成
|
|
50
|
+
* "一行一个字段、列用 `|` 分隔"的字符串数组:省掉重复键名,也只占一行缩进。
|
|
51
|
+
*
|
|
52
|
+
* "columns": ["id","name","subFormId","componentKey"],
|
|
53
|
+
* "rows": ["856266610411798602|Cost Bearer||MultiContent", …],
|
|
54
|
+
* "extra": { "<字段id>": { "required": true, "isBrowser": true, "options": [ … ] } }
|
|
55
|
+
*
|
|
56
|
+
* 非列信息(required / readOnly / isBrowser / options / fileUploadParams…)条目少,
|
|
57
|
+
* 仍按字段 id 登记在 `extra`。信息不丢:完整结构写在 resultFile,这里只是换一种编码。
|
|
58
|
+
*/
|
|
59
|
+
function encodeFieldsCompact(fields) {
|
|
60
|
+
const columns = ['id', 'name', 'subFormId', 'componentKey'];
|
|
61
|
+
const cell = (value) => String(value === undefined || value === null ? '' : value).replace(/[|\r\n]/g, ' ');
|
|
62
|
+
const rows = fields.map((field) => columns.map((col) => cell(field?.[col])).join('|'));
|
|
63
|
+
const extra = {};
|
|
64
|
+
for (const field of fields) {
|
|
65
|
+
if (!field || typeof field !== 'object')
|
|
66
|
+
continue;
|
|
67
|
+
const rest = {};
|
|
68
|
+
for (const [key, value] of Object.entries(field)) {
|
|
69
|
+
if (!columns.includes(key))
|
|
70
|
+
rest[key] = value;
|
|
71
|
+
}
|
|
72
|
+
if (Object.keys(rest).length)
|
|
73
|
+
extra[String(field.id ?? '')] = rest;
|
|
74
|
+
}
|
|
75
|
+
return { columns, rows, rowCount: rows.length, extra };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* `fields` 被整体移出 stdout 时的补偿索引(V12.1.34)。
|
|
79
|
+
*
|
|
80
|
+
* 极端档(`fieldsOmitted`)此前只留 `requiredFields` / `subFormRequired` / `editableFields`
|
|
81
|
+
* 三个**纯 id 数组**,模型拿不到「字段名 ↔ 字段 id」的对应关系,只能自己写脚本去 resultFile 里挖
|
|
82
|
+
* (实测某次会话为此多写 3 个脚本、3 次被 head 截断,白花 6 次工具往返)。
|
|
83
|
+
* 这里补一份**只含可写字段**的紧凑索引(行 = `id|name|subFormId|componentKey|flags`),
|
|
84
|
+
* 体积很小(某 278 字段表单实测 3.7KB,其余字段 17.5KB,合计 21.2KB < 安全线),
|
|
85
|
+
* 足以让模型直接构造 `data` 的键;更细的元数据(browserModule / fileUploadParams / 完整 options)
|
|
86
|
+
* 仍按字段 id 去 resultFile 取。纯结构裁剪,不做任何语义判断。
|
|
87
|
+
*
|
|
88
|
+
* @param writableIds 可写字段 id(含系统字段),顺序即输出顺序的同构子集
|
|
89
|
+
* @param sourceFields 字段来源(createForm 传可见字段原对象;editLoad 传 slimFields)
|
|
90
|
+
* @param systemNames 系统字段 id → 中文名(不在 sourceFields 里的兜底)
|
|
91
|
+
*/
|
|
92
|
+
function encodeWritableFieldsCompact(writableIds, sourceFields, systemNames) {
|
|
93
|
+
const byId = new Map();
|
|
94
|
+
for (const f of sourceFields)
|
|
95
|
+
byId.set(String(f?.id ?? ''), f);
|
|
96
|
+
const cell = (value) => String(value === undefined || value === null ? '' : value).replace(/[|\r\n]/g, ' ');
|
|
97
|
+
const flagsOf = (f) => {
|
|
98
|
+
const component = String(f.componentKey ?? '').toLowerCase();
|
|
99
|
+
let flags = '';
|
|
100
|
+
if (f.required)
|
|
101
|
+
flags += 'R';
|
|
102
|
+
if (f.isBrowser)
|
|
103
|
+
flags += 'B';
|
|
104
|
+
if (f.isSelect)
|
|
105
|
+
flags += 'S';
|
|
106
|
+
if (f.isMultiple)
|
|
107
|
+
flags += 'M';
|
|
108
|
+
if (component === 'employeescope')
|
|
109
|
+
flags += 'E';
|
|
110
|
+
if (component === 'einvoicecomponent')
|
|
111
|
+
flags += 'V';
|
|
112
|
+
if (f.folderDependsOnOption)
|
|
113
|
+
flags += 'D';
|
|
114
|
+
if (f.fileUploadParams)
|
|
115
|
+
flags += 'F';
|
|
116
|
+
if (Array.isArray(f.options) && f.options.length)
|
|
117
|
+
flags += 'O';
|
|
118
|
+
if (String(f.format ?? '').trim())
|
|
119
|
+
flags += 'T';
|
|
120
|
+
return flags;
|
|
121
|
+
};
|
|
122
|
+
const rows = [];
|
|
123
|
+
const optionIndex = {};
|
|
124
|
+
const emitted = new Set();
|
|
125
|
+
const push = (id) => {
|
|
126
|
+
if (emitted.has(id))
|
|
127
|
+
return;
|
|
128
|
+
emitted.add(id);
|
|
129
|
+
const f = byId.get(id);
|
|
130
|
+
if (!f) {
|
|
131
|
+
rows.push([id, systemNames[id] || '', '', '', ''].map(cell).join('|'));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
rows.push([id, f.name, f.subFormId, f.componentKey, flagsOf(f)].map(cell).join('|'));
|
|
135
|
+
const options = Array.isArray(f.options) ? f.options : [];
|
|
136
|
+
if (options.length) {
|
|
137
|
+
optionIndex[id] = options.slice(0, 30).map((o) => (o && typeof o === 'object' ? String(o.name ?? o.content ?? '') : String(o)));
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const wanted = new Set(writableIds);
|
|
141
|
+
for (const f of sourceFields) {
|
|
142
|
+
const id = String(f?.id ?? '');
|
|
143
|
+
if (wanted.has(id))
|
|
144
|
+
push(id);
|
|
145
|
+
}
|
|
146
|
+
for (const id of writableIds)
|
|
147
|
+
push(id);
|
|
148
|
+
const out = {
|
|
149
|
+
columns: ['id', 'name', 'subFormId', 'componentKey', 'flags'],
|
|
150
|
+
rows,
|
|
151
|
+
rowCount: rows.length,
|
|
152
|
+
flagLegend: 'R=必填 B=浏览按钮 S=选项 M=多选 E=人员范围 V=发票 D=附件目录随选项 '
|
|
153
|
+
+ 'F=有上传参数(见 resultFile) O=有选项(见 optionIndex) T=有日期/时间格式要求',
|
|
154
|
+
};
|
|
155
|
+
if (Object.keys(optionIndex).length)
|
|
156
|
+
out.optionIndex = optionIndex;
|
|
157
|
+
return out;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* 大表单输出保护(通用,不针对任何具体工作流/表单)。
|
|
161
|
+
*
|
|
162
|
+
* 字段多的表单,`createForm` / `editLoad` 的输出能轻松超过 60KB,会被宿主截断成 2KB 预览;
|
|
163
|
+
* 模型于是写脚本读回、读回又被截断,白花好几轮工具往返(实测某次会话为此多花 5 次)。这里
|
|
164
|
+
* 对齐 `todoStat` 的做法:超安全线时把**完整结果**写入 resultFile,stdout 只回精简版并前置
|
|
165
|
+
* `resultFile`(放最前,被截断也看得见),同时把 `fields` 换成列式编码。
|
|
166
|
+
*
|
|
167
|
+
* 落盘不可用时保持全量输出(宁大不丢数据),只补一句说明。
|
|
168
|
+
*
|
|
169
|
+
* V12.1.34:入口可传 `fieldsOmittedCompensation`(可写字段紧凑索引)。它**只在 `fields` 真的被
|
|
170
|
+
* 整体移出 stdout 时**才随摘要一起产出 —— 紧凑编码那一档 `fields` 仍在,模型本来就能读到字段名,
|
|
171
|
+
* 不需要重复体积;小表单(未超线)完全不产出,避免无谓地把输出顶到安全线上。
|
|
172
|
+
*/
|
|
173
|
+
/** 读取本输出的统一叮嘱(紧凑版与兜底版共用)。 */
|
|
174
|
+
const READ_WHOLE_OUTPUT_HINT = '读取本输出请一次读完:不要用 head/tail 等管道截断'
|
|
175
|
+
+ '(截断会破坏 JSON,并导致重复执行同一条命令);本输出被截断读不全时直接读 resultFile,不要重跑本命令。';
|
|
176
|
+
async function compactFormOutput(result, env, resultFileName, fieldsOmittedCompensation) {
|
|
177
|
+
const bytes = stdoutBytesOf(result);
|
|
178
|
+
if (bytes <= STDOUT_SAFE_BYTES)
|
|
179
|
+
return result;
|
|
180
|
+
const fields = toArray(result.fields);
|
|
181
|
+
const resultFile = await writeStateFileBestEffort(resultFileName, JSON.stringify(result, null, 2), env.ctx);
|
|
182
|
+
if (!resultFile) {
|
|
183
|
+
return {
|
|
184
|
+
...result,
|
|
185
|
+
outputBytes: bytes,
|
|
186
|
+
largeOutputNotice: `表单字段较多(${fields.length} 个)且本地落盘不可用,已按原样返回全量 fields。`
|
|
187
|
+
+ READ_WHOLE_OUTPUT_HINT,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
const compact = withLeadingMeta({
|
|
191
|
+
resultFile,
|
|
192
|
+
persistedResult: true,
|
|
193
|
+
outputBytes: bytes,
|
|
194
|
+
largeOutputNotice: `表单字段较多(${fields.length} 个),完整结果已写入 resultFile。`
|
|
195
|
+
+ 'stdout 里的 fields 是紧凑编码:columns 为列名,rows 每行一个字段、各列用 "|" 分隔'
|
|
196
|
+
+ '(顺序与 columns 一致),extra 按字段 id 存放非列信息'
|
|
197
|
+
+ '(required / readOnly / disableEdit / isBrowser / isSelect / isMultiple / browserModule /'
|
|
198
|
+
+ ' browserType / options / fileUploadParams 等;未出现在 extra 里的字段即为默认值)。'
|
|
199
|
+
+ '需要某字段的完整原始元数据时,从 resultFile 里按该字段 id 取。'
|
|
200
|
+
+ 'resultFile 是完整 pretty JSON、体积很大,请用脚本按需提取,不要整份读入上下文。'
|
|
201
|
+
+ READ_WHOLE_OUTPUT_HINT,
|
|
202
|
+
}, {
|
|
203
|
+
...result,
|
|
204
|
+
fields: encodeFieldsCompact(fields),
|
|
205
|
+
});
|
|
206
|
+
if (stdoutBytesOf(compact) <= STDOUT_SAFE_BYTES)
|
|
207
|
+
return compact;
|
|
208
|
+
// 极端兜底(字段极多的表单,紧凑编码也压不进安全线):stdout 只留指针 + 摘要,
|
|
209
|
+
// 字段清单去 resultFile 按需提取 —— 否则整段输出会被宿主截成 2KB 预览,等于什么都没拿到。
|
|
210
|
+
// V12.1.34:随摘要附上**可写字段补偿索引**,模型不必再自己写脚本挖 resultFile。
|
|
211
|
+
// V12.1.35(实测 278 字段表单:美化 37,887 B → ≈17 KB,落进宿主 28 KB 内联区):
|
|
212
|
+
// ① 安全线改用**美化口径** `stdoutBytesOf()`(与实际 stdout 一致)。此前量压缩 JSON,**低估 1.79 倍**
|
|
213
|
+
// ⇒ 这一档从未触发,且 `writableFields` 被追加在末尾(起始偏移 31,093),宿主只给 2048 B 预览
|
|
214
|
+
// ⇒ 模型永远看不到它,只能自写脚本挖 resultFile(实测白烧 4 轮工具往返、约 54 s)。
|
|
215
|
+
// ② `conditionalRequired`(278 字段表单实测压缩 10.9 KB / 美化 ≈19.5 KB)与 `editableFields`
|
|
216
|
+
// (与 writableFields 冗余)移出 stdout,只留条数 —— 二者合起来才够把体积压进宿主内联区。
|
|
217
|
+
// ③ `writableFields` 前置到载荷最前,任何截断场景下都先被看到。
|
|
218
|
+
const condCount = Array.isArray(result.conditionalRequired) ? result.conditionalRequired.length : 0;
|
|
219
|
+
const summary = { ...result };
|
|
220
|
+
delete summary.fields;
|
|
221
|
+
delete summary.conditionalRequired;
|
|
222
|
+
delete summary.editableFields;
|
|
223
|
+
const ordered = {};
|
|
224
|
+
if (fieldsOmittedCompensation)
|
|
225
|
+
ordered.writableFields = fieldsOmittedCompensation;
|
|
226
|
+
for (const [key, value] of Object.entries(summary)) {
|
|
227
|
+
if (value !== undefined)
|
|
228
|
+
ordered[key] = value;
|
|
229
|
+
}
|
|
230
|
+
const fieldsOmittedNotice = `表单字段过多(${fields.length} 个),字段清单已整体移出 stdout,`
|
|
231
|
+
+ '完整结果(含全部 fields 原始元数据)已写入 resultFile。'
|
|
232
|
+
+ (fieldsOmittedCompensation
|
|
233
|
+
? 'stdout **最前面**已给出全部可写字段的紧凑索引 writableFields:columns 为列名、rows 每行一个字段'
|
|
234
|
+
+ '(`id|name|subFormId|componentKey|flags`,flags 含义见 flagLegend;带选项的字段,'
|
|
235
|
+
+ '其选项名见 optionIndex)。**直接用 writableFields 里的字段 id 构造 data 的键**,'
|
|
236
|
+
+ '不要为了拿字段清单再写脚本解析 resultFile;只有需要某字段更细的元数据'
|
|
237
|
+
+ '(browserModule / fileUploadParams / 完整 options 结构)时,才按该字段 id 从 resultFile 取。'
|
|
238
|
+
: '构造表单数据前,请按字段 id 从 resultFile 里提取所需键,按需只取几个键、'
|
|
239
|
+
+ '不要把整份 resultFile 读入上下文。')
|
|
240
|
+
+ (condCount
|
|
241
|
+
? `为压进体积安全线,条件必填规则(conditionalRequired,${condCount} 条)与可写白名单`
|
|
242
|
+
+ '(editableFields)也已移出 stdout(条数见 conditionalRequiredCount);'
|
|
243
|
+
+ '需要时按同一个 resultFile 取。'
|
|
244
|
+
: '')
|
|
245
|
+
+ READ_WHOLE_OUTPUT_HINT;
|
|
246
|
+
const leadingMeta = {
|
|
247
|
+
resultFile,
|
|
248
|
+
persistedResult: true,
|
|
249
|
+
outputBytes: bytes,
|
|
250
|
+
fieldsOmitted: true,
|
|
251
|
+
fieldCount: fields.length,
|
|
252
|
+
...(condCount ? { conditionalRequiredCount: condCount } : {}),
|
|
253
|
+
largeOutputNotice: fieldsOmittedNotice,
|
|
254
|
+
};
|
|
255
|
+
const withIndex = withLeadingMeta(leadingMeta, ordered);
|
|
256
|
+
if (stdoutBytesOf(withIndex) <= STDOUT_SAFE_BYTES)
|
|
257
|
+
return withIndex;
|
|
258
|
+
// 索引也放不进安全线(字段数量极端):退回只留指针,并显式标注已省略。
|
|
259
|
+
delete ordered.writableFields;
|
|
260
|
+
return withLeadingMeta({
|
|
261
|
+
...leadingMeta,
|
|
262
|
+
...(fieldsOmittedCompensation ? { writableFieldsOmitted: true } : {}),
|
|
263
|
+
}, ordered);
|
|
264
|
+
}
|
|
265
|
+
async function requestJson(env, method, path, query, body) {
|
|
266
|
+
const response = await env.host.workflowRequest({ method, path, query, body, operation: path });
|
|
267
|
+
return responseData(response);
|
|
268
|
+
}
|
|
269
|
+
async function getSessionContext(env) {
|
|
270
|
+
const session = await env.host.getSession();
|
|
271
|
+
return { baseUrl: session.baseUrl, profile: session.profile, userId: session.userId };
|
|
272
|
+
}
|
|
273
|
+
/** 校验 continuation 的上下文绑定与当前会话一致(baseUrl/profile/userId)。 */
|
|
274
|
+
async function verifyContextLease(env, token, expectedOperation, consumeOnVerify = false) {
|
|
275
|
+
const lease = await env.continuation.verify(token, expectedOperation);
|
|
276
|
+
const session = await getSessionContext(env);
|
|
277
|
+
if (lease.baseUrl !== session.baseUrl || lease.profile !== session.profile || lease.userId !== session.userId) {
|
|
278
|
+
throw workflowValidationError('context_mismatch', '确认上下文已变化(租户或登录账号不同),请重新执行 createForm/editLoad');
|
|
279
|
+
}
|
|
280
|
+
// apply 阶段才消费(一次性防重放);prepare 阶段可重试、不写业务数据,不消费。
|
|
281
|
+
if (consumeOnVerify) {
|
|
282
|
+
const claim = await env.continuation.consume(token, expectedOperation);
|
|
283
|
+
// 抢不到说明已有(并发的)执行占用该句柄:**不得再写入**,并按占用状态给出可执行的处置说明。
|
|
284
|
+
if (!claim.claimed)
|
|
285
|
+
throw continuationReplayError(claim.previous ?? { state: 'unknown' }, expectedOperation);
|
|
286
|
+
// 登记本次执行的句柄:写请求发出前据此推进阶段,避免"死在写入途中"被误判成可安全重占。
|
|
287
|
+
env.activeContinuation = token;
|
|
288
|
+
}
|
|
289
|
+
return lease;
|
|
290
|
+
}
|
|
291
|
+
function isNetworkError(error) {
|
|
292
|
+
return error instanceof WorkflowCliError && error.type === 'network';
|
|
293
|
+
}
|
|
294
|
+
// ---------------------------------------------------------------------------
|
|
295
|
+
// 原始布局 walk(收集 {fieldId: raw} 供附件限制解析,独立于 extractFields)
|
|
296
|
+
// ---------------------------------------------------------------------------
|
|
297
|
+
function walkLayoutRawFields(layoutRoot) {
|
|
298
|
+
const result = {};
|
|
299
|
+
const walk = (value) => {
|
|
300
|
+
if (Array.isArray(value)) {
|
|
301
|
+
for (const child of value)
|
|
302
|
+
walk(child);
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
if (!value || typeof value !== 'object')
|
|
306
|
+
return;
|
|
307
|
+
const fieldId = String(value.fieldId ?? '').trim();
|
|
308
|
+
const component = String(value.componentKey ?? value.type ?? '').trim();
|
|
309
|
+
if (fieldId && component) {
|
|
310
|
+
const previous = result[fieldId];
|
|
311
|
+
if (previous === undefined || Object.keys(value).length > Object.keys(previous).length) {
|
|
312
|
+
result[fieldId] = value;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const child of Object.values(value))
|
|
316
|
+
walk(child);
|
|
317
|
+
};
|
|
318
|
+
walk(layoutRoot);
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
// ---------------------------------------------------------------------------
|
|
322
|
+
// runtime / frame / minor 解析
|
|
323
|
+
// ---------------------------------------------------------------------------
|
|
324
|
+
function parseRuntime(data, workflowId) {
|
|
325
|
+
if (!data || typeof data !== 'object')
|
|
326
|
+
throw workflowApiError('format_error', 'loadBaseParam 未返回 data');
|
|
327
|
+
const common = data.commonParam;
|
|
328
|
+
if (!common || typeof common !== 'object')
|
|
329
|
+
throw workflowApiError('format_error', 'loadBaseParam 未返回 commonParam');
|
|
330
|
+
const currentUser = (common.currentUser && typeof common.currentUser === 'object') ? common.currentUser : {};
|
|
331
|
+
const employeeId = String(currentUser.userId ?? '');
|
|
332
|
+
if (!employeeId)
|
|
333
|
+
throw workflowApiError('format_error', '缺少 commonParam.currentUser.userId');
|
|
334
|
+
const requestInfo = (data.requestInfo && typeof data.requestInfo === 'object') ? data.requestInfo : {};
|
|
335
|
+
const requestName = String(requestInfo.requestName ?? '').trim();
|
|
336
|
+
const requestLevel = requestInfo.requestLevel === null || requestInfo.requestLevel === undefined ? '' : String(requestInfo.requestLevel).trim();
|
|
337
|
+
const secLevel = requestInfo.secLevel === null || requestInfo.secLevel === undefined ? '' : String(requestInfo.secLevel).trim();
|
|
338
|
+
return {
|
|
339
|
+
request_id: String(common.requestId ?? ''),
|
|
340
|
+
node_id: String(common.userCurrentNodeId ?? common.fixedNodeId ?? ''),
|
|
341
|
+
employee_id: employeeId,
|
|
342
|
+
identity_id: String(common.identityId ?? employeeId),
|
|
343
|
+
identity_type: Number(common.identityType ?? 0),
|
|
344
|
+
current_user_name: String(currentUser.userName ?? ''),
|
|
345
|
+
auth_str: String(common.authStr ?? ''),
|
|
346
|
+
auth_signature: String(common.authSignatureStr ?? ''),
|
|
347
|
+
workflow_id: workflowId,
|
|
348
|
+
workflow_name: String(requestInfo.workflowName ?? ''),
|
|
349
|
+
workflow_description: String(requestInfo.workflowDesc ?? ''),
|
|
350
|
+
workflow_type_name: String(requestInfo.workflowTypeName ?? ''),
|
|
351
|
+
default_request_name: requestName,
|
|
352
|
+
default_request_level: requestLevel,
|
|
353
|
+
default_sec_level: secLevel,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
function parseFrame(data, nodeId) {
|
|
357
|
+
if (!data || typeof data !== 'object')
|
|
358
|
+
throw workflowApiError('format_error', 'loadFrameParam 未返回 data');
|
|
359
|
+
const configs = data.formConfig;
|
|
360
|
+
if (!Array.isArray(configs) || configs.length !== 1 || !configs[0] || typeof configs[0] !== 'object') {
|
|
361
|
+
throw workflowApiError('format_error', 'loadFrameParam 返回的表单配置不是唯一项');
|
|
362
|
+
}
|
|
363
|
+
const config = configs[0];
|
|
364
|
+
const formInfo = (config.formInfo && typeof config.formInfo === 'object') ? config.formInfo : {};
|
|
365
|
+
const formId = String(formInfo.relateKey ?? '');
|
|
366
|
+
if (!formId)
|
|
367
|
+
throw workflowApiError('format_error', '缺少 formConfig.formInfo.relateKey');
|
|
368
|
+
const relateType = String(formInfo.relateType ?? '').trim() || 'WEA_FORM';
|
|
369
|
+
const viewNodeId = String(config.viewFormNodeId ?? nodeId);
|
|
370
|
+
if (!viewNodeId)
|
|
371
|
+
throw workflowApiError('format_error', '缺少 formConfig.viewFormNodeId');
|
|
372
|
+
if (viewNodeId !== nodeId)
|
|
373
|
+
throw workflowApiError('format_error', 'loadBaseParam 与 loadFrameParam 返回了不同节点');
|
|
374
|
+
const ts = String(config.timestamp ?? '').match(/^\d+$/) ? Number(config.timestamp) : Date.now();
|
|
375
|
+
return {
|
|
376
|
+
form_id: formId,
|
|
377
|
+
relate_type: relateType,
|
|
378
|
+
permission_id: String(config.permissionId ?? '').trim(),
|
|
379
|
+
permission_type: Number(config.permissionType ?? 1),
|
|
380
|
+
layout_multi_id: String(config.layoutMultiId ?? config.defaultLayoutId ?? ''),
|
|
381
|
+
new_layout: config.newLayout !== false,
|
|
382
|
+
sys_field_new_mode: config.sysFieldNewMode !== false,
|
|
383
|
+
view_form_node_id: viewNodeId,
|
|
384
|
+
timestamp: ts,
|
|
385
|
+
// enableNodePermission=false ⇒ 该表单未启用节点权限:字段属性只由表单设计决定,
|
|
386
|
+
// 不带顶层 permissionId 才能拿到 viewRelateConfig(静态必填/只读/禁止编辑)。
|
|
387
|
+
// 缺省(服务端未返回该字段)视为启用,保持既有行为。
|
|
388
|
+
enable_node_permission: config.enableNodePermission !== false,
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
function parseMinorInfo(data) {
|
|
392
|
+
if (!data || typeof data !== 'object') {
|
|
393
|
+
return { permissions: {}, level_options: [], sec_level_options: [], sec_default_id: '' };
|
|
394
|
+
}
|
|
395
|
+
const permissions = {};
|
|
396
|
+
const sysPermission = (data.sysFieldPermission && typeof data.sysFieldPermission === 'object') ? data.sysFieldPermission : {};
|
|
397
|
+
for (const key of Object.keys(SYSTEM_FIELD_KEYS)) {
|
|
398
|
+
const item = sysPermission[key];
|
|
399
|
+
if (item && typeof item === 'object') {
|
|
400
|
+
permissions[key] = {
|
|
401
|
+
isview: Boolean(item.isview),
|
|
402
|
+
isedit: Boolean(item.isedit),
|
|
403
|
+
ismandatory: Boolean(item.ismandatory),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
const levelOptions = [];
|
|
408
|
+
for (const item of data.customLevelList || []) {
|
|
409
|
+
if (!item || typeof item !== 'object')
|
|
410
|
+
continue;
|
|
411
|
+
if (item.closed === true)
|
|
412
|
+
continue;
|
|
413
|
+
const customName = String(item.customName ?? '').trim();
|
|
414
|
+
const defaultName = String(item.defaultName ?? '').trim();
|
|
415
|
+
const display = customName || defaultName;
|
|
416
|
+
if (display)
|
|
417
|
+
levelOptions.push({ code: item.code, name: display });
|
|
418
|
+
}
|
|
419
|
+
const secLevelOptions = [];
|
|
420
|
+
let defaultSecLevelId = '';
|
|
421
|
+
const params = (data.params && typeof data.params === 'object') ? data.params : {};
|
|
422
|
+
for (const item of params.secLevelOption || []) {
|
|
423
|
+
if (!item || typeof item === 'object')
|
|
424
|
+
continue;
|
|
425
|
+
const id = String(item.id ?? '').trim();
|
|
426
|
+
if (!id)
|
|
427
|
+
continue;
|
|
428
|
+
secLevelOptions.push({ id, name: String(item.name ?? '').trim() });
|
|
429
|
+
if (!defaultSecLevelId) {
|
|
430
|
+
const isDefault = item.isDefault === true || String(item.isDefault ?? '').trim().toLowerCase() === '1' || String(item.isDefault ?? '').trim().toLowerCase() === 'true';
|
|
431
|
+
if (isDefault)
|
|
432
|
+
defaultSecLevelId = id;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return { permissions, level_options: levelOptions, sec_level_options: secLevelOptions, sec_default_id: defaultSecLevelId };
|
|
436
|
+
}
|
|
437
|
+
function parseTenantSetting(data) {
|
|
438
|
+
if (!data || typeof data !== 'object')
|
|
439
|
+
return {};
|
|
440
|
+
const maxSizeText = String(data.sysMaxSingleUploadSize ?? '').trim();
|
|
441
|
+
let maxSizeMb = 0;
|
|
442
|
+
try {
|
|
443
|
+
maxSizeMb = parseInt(maxSizeText, 10);
|
|
444
|
+
if (Number.isNaN(maxSizeMb))
|
|
445
|
+
maxSizeMb = 0;
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
maxSizeMb = 0;
|
|
449
|
+
}
|
|
450
|
+
const whitesEnable = data.sysFormatWhitesEnable === true;
|
|
451
|
+
const formatText = String(data[whitesEnable ? 'sysFormatWhites' : 'sysFormatBlacks'] ?? '');
|
|
452
|
+
const formats = formatText.split(',').map((p) => p.trim().toLowerCase()).filter((p) => p);
|
|
453
|
+
if (maxSizeMb <= 0 && formats.length === 0)
|
|
454
|
+
return {};
|
|
455
|
+
return { max_size_mb: maxSizeMb, format_mode: whitesEnable ? 'white' : 'black', formats };
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* build_layout_request:customParam 逐字段对齐。
|
|
459
|
+
*
|
|
460
|
+
* 顶层 permissionId 的带与不带,由 `loadFrameParam.formConfig[0].enableNodePermission` 决定
|
|
461
|
+
* (2026-09-17 实测定案,同一租户 OA 10.0.2607.01):
|
|
462
|
+
* - 该表单**启用了节点权限**(`true`/缺省)→ 带上 `permissionId`,拿到的是该节点的真实字段权限;
|
|
463
|
+
* 不带时业务字段会被返回成 `readOnly=true`(mulinquan 公文zc 实测:5 字段全只读、`editableFields: []`)。
|
|
464
|
+
* - 该表单**未启用节点权限**(`false`)→ 不带。此时带上(服务端给的哨兵值 `-1`)会让服务端把
|
|
465
|
+
* `viewRelateConfig` 返回成空数组、布局里只剩 1 个 `required=true`(流程标题),
|
|
466
|
+
* 静态必填/只读/禁止编辑信息全丢(实测:`requiredFields` 由 11 个业务字段掉到 `["REQUEST_NAMEL"]`,
|
|
467
|
+
* 只读字段 35→0);不带时拿到的才是表单设计级的真实属性。
|
|
468
|
+
*/
|
|
469
|
+
function buildLayoutRequest(workflowId, runtime, frame) {
|
|
470
|
+
const custom = {
|
|
471
|
+
permissionType: frame.permission_type,
|
|
472
|
+
requestId: runtime.request_id,
|
|
473
|
+
workflowId,
|
|
474
|
+
nodeId: runtime.node_id,
|
|
475
|
+
authSignatureStr: runtime.auth_signature,
|
|
476
|
+
authStr: runtime.auth_str,
|
|
477
|
+
readOnly: false,
|
|
478
|
+
identityId: runtime.identity_id,
|
|
479
|
+
identityType: runtime.identity_type,
|
|
480
|
+
isTest: false,
|
|
481
|
+
create: true,
|
|
482
|
+
mobile: false,
|
|
483
|
+
formId: frame.form_id,
|
|
484
|
+
source: 'view',
|
|
485
|
+
isAgent: false,
|
|
486
|
+
beAgentId: '0',
|
|
487
|
+
hideSysFieldArea: true,
|
|
488
|
+
viewFormNodeId: frame.view_form_node_id,
|
|
489
|
+
timestamp: frame.timestamp,
|
|
490
|
+
turnTodoUser: [],
|
|
491
|
+
sysFieldNewMode: frame.sys_field_new_mode,
|
|
492
|
+
};
|
|
493
|
+
const permissionId = String(frame.permission_id ?? '').trim();
|
|
494
|
+
// 未启用节点权限的表单(enableNodePermission=false)不带 permissionId:带了会丢静态必填/只读属性。
|
|
495
|
+
const nodePermissionEnabled = frame.enable_node_permission !== false;
|
|
496
|
+
return {
|
|
497
|
+
terminalType: 'PC',
|
|
498
|
+
formId: frame.form_id,
|
|
499
|
+
...(nodePermissionEnabled && permissionId ? { permissionId } : {}),
|
|
500
|
+
module: 'workflow',
|
|
501
|
+
customParam: custom,
|
|
502
|
+
cacheUid: randomUUID(),
|
|
503
|
+
newLayout: frame.new_layout,
|
|
504
|
+
layoutMultiId: frame.layout_multi_id,
|
|
505
|
+
dataCrossLayout: false,
|
|
506
|
+
employeeId: runtime.employee_id,
|
|
507
|
+
refId: workflowId,
|
|
508
|
+
initialDatas: [
|
|
509
|
+
{ keyType: null, key: 'isCreate', value: 1 },
|
|
510
|
+
{ keyType: null, key: 'isMobile', value: 0 },
|
|
511
|
+
],
|
|
512
|
+
cost: false,
|
|
513
|
+
effectValues: [workflowId, runtime.node_id],
|
|
514
|
+
orgAsyncPageQuery: true,
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
// ---------------------------------------------------------------------------
|
|
518
|
+
// 字段提取(extractFields)
|
|
519
|
+
// ---------------------------------------------------------------------------
|
|
520
|
+
function extractFields(layoutData, requiredMap, disableEditMap, displayMap) {
|
|
521
|
+
const candidates = {};
|
|
522
|
+
const walk = (value) => {
|
|
523
|
+
if (Array.isArray(value)) {
|
|
524
|
+
for (const child of value)
|
|
525
|
+
walk(child);
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
if (!value || typeof value !== 'object')
|
|
529
|
+
return;
|
|
530
|
+
const fieldId = String(value.fieldId ?? '').trim();
|
|
531
|
+
const component = String(value.componentKey ?? value.type ?? '').trim();
|
|
532
|
+
if (fieldId && component) {
|
|
533
|
+
const previous = candidates[fieldId];
|
|
534
|
+
if (previous === undefined || Object.keys(value).length > Object.keys(previous).length) {
|
|
535
|
+
candidates[fieldId] = value;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
for (const child of Object.values(value))
|
|
539
|
+
walk(child);
|
|
540
|
+
};
|
|
541
|
+
walk(layoutData.formLayout);
|
|
542
|
+
const result = [];
|
|
543
|
+
for (const fieldId of Object.keys(candidates)) {
|
|
544
|
+
const raw = candidates[fieldId];
|
|
545
|
+
const component = String(raw.componentKey ?? raw.type ?? '');
|
|
546
|
+
const normalized = component.toLowerCase();
|
|
547
|
+
if (NON_INPUT_COMPONENTS.has(normalized) || normalized.startsWith('flowpagesysfield'))
|
|
548
|
+
continue;
|
|
549
|
+
const browserType = (raw.browserType && typeof raw.browserType === 'object') ? raw.browserType : {};
|
|
550
|
+
const options = [];
|
|
551
|
+
for (const option of raw.options || []) {
|
|
552
|
+
if (!option || typeof option !== 'object')
|
|
553
|
+
continue;
|
|
554
|
+
const optionId = String(option.valueKey ?? option.selectionId ?? option.id ?? '').trim();
|
|
555
|
+
const name = String(option.name ?? option.content ?? '').trim();
|
|
556
|
+
if (optionId && name)
|
|
557
|
+
options.push({ id: optionId, name });
|
|
558
|
+
}
|
|
559
|
+
// 注意:browserType 上面已归一化成对象(缺省 `{}`),而 **JS 的 `Boolean({})` 恒为 true**
|
|
560
|
+
// (Python 的 `bool({})` 为 False)——直接照抄 Python 的 `bool(browser_type)` 会让**每个字段**
|
|
561
|
+
// 都被判成浏览按钮字段,文本/富文本字段随之走 name→ID 解析并报 BROWSER NOT_FOUND,
|
|
562
|
+
// 内容永远写不进去(2026-09-16「内部留言」留言内容字段实证)。必须判"非空对象"。
|
|
563
|
+
const isBrowser = Object.keys(browserType).length > 0 || BROWSER_COMPONENTS.has(normalized);
|
|
564
|
+
const isSelect = SELECT_COMPONENTS.has(normalized) || options.length > 0;
|
|
565
|
+
result.push({
|
|
566
|
+
id: fieldId,
|
|
567
|
+
name: String(raw.title ?? raw.name ?? raw.dataKey ?? fieldId),
|
|
568
|
+
componentKey: component,
|
|
569
|
+
required: (requiredMap[fieldId] !== undefined) ? requiredMap[fieldId] : Boolean(raw.required || raw.mustInput),
|
|
570
|
+
disableEdit: (disableEditMap[fieldId] !== undefined) ? disableEditMap[fieldId] : Boolean(raw.disableEdit),
|
|
571
|
+
readOnly: Boolean(READONLY_COMPONENTS.has(normalized) || raw.readOnly),
|
|
572
|
+
display: (displayMap[fieldId] !== undefined) ? displayMap[fieldId] : (raw.display !== undefined ? raw.display : true),
|
|
573
|
+
formId: String(raw.formId ?? (layoutData.form && layoutData.form.id) ?? ''),
|
|
574
|
+
subFormId: String(raw.subFormId ?? ''),
|
|
575
|
+
isBrowser,
|
|
576
|
+
isSelect,
|
|
577
|
+
isMultiple: Boolean(SELECT_COMPONENTS.has(normalized) && (normalized === 'selectmultiple' || normalized === 'checkbox' || normalized === 'multiselect'))
|
|
578
|
+
|| (isBrowser && raw.isSingle === false),
|
|
579
|
+
browserModule: String(browserType.module ?? raw.browserModule ?? ''),
|
|
580
|
+
browserType: String(browserType.type ?? browserType.id ?? raw.browserTypeCode ?? ''),
|
|
581
|
+
options,
|
|
582
|
+
maxLength: raw.maxLen !== undefined ? raw.maxLen : raw.maxlen,
|
|
583
|
+
richEditor: Boolean(raw.richEditor),
|
|
584
|
+
showOrder: raw.showOrder !== undefined ? raw.showOrder : 0,
|
|
585
|
+
format: String(raw.format ?? raw.formatPattern ?? ''),
|
|
586
|
+
placeholder: String(raw.placeholder ?? ''),
|
|
587
|
+
defaultExtend: (raw.defaultExtend && typeof raw.defaultExtend === 'object') ? raw.defaultExtend : null,
|
|
588
|
+
typeLen: raw.typeLen !== undefined ? raw.typeLen : raw.typeLength,
|
|
589
|
+
columnType: String(raw.columnType ?? ''),
|
|
590
|
+
});
|
|
591
|
+
}
|
|
592
|
+
result.sort((a, b) => {
|
|
593
|
+
if (Boolean(a.subFormId) !== Boolean(b.subFormId))
|
|
594
|
+
return a.subFormId ? 1 : -1;
|
|
595
|
+
if (a.subFormId !== b.subFormId)
|
|
596
|
+
return a.subFormId < b.subFormId ? -1 : 1;
|
|
597
|
+
const sa = Number(a.showOrder || 0);
|
|
598
|
+
const sb = Number(b.showOrder || 0);
|
|
599
|
+
if (sa !== sb)
|
|
600
|
+
return sa - sb;
|
|
601
|
+
if (a.name !== b.name)
|
|
602
|
+
return a.name < b.name ? -1 : 1;
|
|
603
|
+
return a.id < b.id ? -1 : 1;
|
|
604
|
+
});
|
|
605
|
+
return result;
|
|
606
|
+
}
|
|
607
|
+
function normalizeDetail(raw) {
|
|
608
|
+
if (!raw || typeof raw !== 'object')
|
|
609
|
+
return {};
|
|
610
|
+
const formField = (raw.formField && typeof raw.formField === 'object') ? raw.formField : {};
|
|
611
|
+
const fieldId = String(formField.id ?? raw.fieldId ?? '').trim();
|
|
612
|
+
if (!fieldId)
|
|
613
|
+
return {};
|
|
614
|
+
const detail = { formField: { id: fieldId } };
|
|
615
|
+
if (raw.content !== undefined && raw.content !== null)
|
|
616
|
+
detail.content = String(raw.content);
|
|
617
|
+
const dataText = raw.dataText;
|
|
618
|
+
if (dataText && typeof dataText === 'object' && dataText.content !== undefined && dataText.content !== null) {
|
|
619
|
+
detail.dataText = { content: String(dataText.content) };
|
|
620
|
+
}
|
|
621
|
+
const options = raw.dataOptions;
|
|
622
|
+
if (Array.isArray(options)) {
|
|
623
|
+
const normalizedOptions = [];
|
|
624
|
+
for (const item of options) {
|
|
625
|
+
if (!item || typeof item !== 'object')
|
|
626
|
+
continue;
|
|
627
|
+
const option = {
|
|
628
|
+
optionId: String(item.optionId ?? item.id ?? ''),
|
|
629
|
+
content: String(item.content ?? item.name ?? ''),
|
|
630
|
+
};
|
|
631
|
+
const scopeType = String(item.type ?? '').trim();
|
|
632
|
+
if (scopeType)
|
|
633
|
+
option.type = scopeType;
|
|
634
|
+
if (item.optionObj && typeof item.optionObj === 'object')
|
|
635
|
+
option.optionObj = item.optionObj;
|
|
636
|
+
normalizedOptions.push(option);
|
|
637
|
+
}
|
|
638
|
+
detail.dataOptions = normalizedOptions;
|
|
639
|
+
}
|
|
640
|
+
for (const key of ['dataIndex', 'rowId']) {
|
|
641
|
+
if (raw[key] !== undefined && raw[key] !== null)
|
|
642
|
+
detail[key] = raw[key];
|
|
643
|
+
}
|
|
644
|
+
const subForm = (raw.subForm && typeof raw.subForm === 'object') ? raw.subForm : null;
|
|
645
|
+
const subFormId = String(raw.subFormId ?? (subForm && subForm.id) ?? '').trim();
|
|
646
|
+
if (subFormId)
|
|
647
|
+
detail.subForm = { id: subFormId };
|
|
648
|
+
return detail;
|
|
649
|
+
}
|
|
650
|
+
function hasDetailValue(detail) {
|
|
651
|
+
if (String(detail.content ?? '').trim())
|
|
652
|
+
return true;
|
|
653
|
+
if (String((detail.dataText && detail.dataText.content) ?? '').trim())
|
|
654
|
+
return true;
|
|
655
|
+
return (detail.dataOptions || []).some((item) => String(item.optionId ?? '').trim());
|
|
656
|
+
}
|
|
657
|
+
function normalizeDefaults(value) {
|
|
658
|
+
if (!Array.isArray(value))
|
|
659
|
+
return [];
|
|
660
|
+
const result = [];
|
|
661
|
+
for (const raw of value) {
|
|
662
|
+
const detail = normalizeDetail(raw);
|
|
663
|
+
if (detail && hasDetailValue(detail))
|
|
664
|
+
result.push(detail);
|
|
665
|
+
}
|
|
666
|
+
return result;
|
|
667
|
+
}
|
|
668
|
+
function slimFieldInfo(field) {
|
|
669
|
+
let options = field.options || [];
|
|
670
|
+
let truncated = false;
|
|
671
|
+
if (options.length > 30) {
|
|
672
|
+
options = options.slice(0, 30);
|
|
673
|
+
truncated = true;
|
|
674
|
+
}
|
|
675
|
+
const slim = {
|
|
676
|
+
id: field.id,
|
|
677
|
+
name: field.name,
|
|
678
|
+
componentKey: field.componentKey,
|
|
679
|
+
required: field.required,
|
|
680
|
+
isBrowser: field.isBrowser,
|
|
681
|
+
isSelect: field.isSelect,
|
|
682
|
+
isMultiple: field.isMultiple,
|
|
683
|
+
browserType: field.browserType || '',
|
|
684
|
+
browserModule: field.browserModule || '',
|
|
685
|
+
options,
|
|
686
|
+
format: field.format || '',
|
|
687
|
+
placeholder: field.placeholder || '',
|
|
688
|
+
typeLen: field.typeLen,
|
|
689
|
+
};
|
|
690
|
+
if (slim.format && ['datecomponent', 'datetimecomponent', 'timecomponent', 'dateinterval'].includes(String(field.componentKey ?? '').toLowerCase())) {
|
|
691
|
+
slim.formatHint = `格式: ${slim.format},必须按此格式传值`;
|
|
692
|
+
}
|
|
693
|
+
if (truncated)
|
|
694
|
+
slim.optionsTruncated = true;
|
|
695
|
+
return slim;
|
|
696
|
+
}
|
|
697
|
+
function requiredFieldsWithSystem(formRequired, runtime, sysPermissions) {
|
|
698
|
+
const ids = formRequired.map((f) => String(f.id ?? ''));
|
|
699
|
+
for (const sysKey of Object.keys(SYSTEM_FIELD_KEYS)) {
|
|
700
|
+
const perm = sysPermissions[sysKey] || {};
|
|
701
|
+
if (perm.isview && perm.ismandatory)
|
|
702
|
+
ids.push(sysKey);
|
|
703
|
+
}
|
|
704
|
+
return ids;
|
|
705
|
+
}
|
|
706
|
+
// ---------------------------------------------------------------------------
|
|
707
|
+
// 附件上传限制解析
|
|
708
|
+
// ---------------------------------------------------------------------------
|
|
709
|
+
function _attachDirLayer(rawFileField, attachConfig) {
|
|
710
|
+
if (rawFileField && typeof rawFileField === 'object' && String(rawFileField.folderType ?? '').trim() === '1') {
|
|
711
|
+
return [rawFileField, true];
|
|
712
|
+
}
|
|
713
|
+
return [(attachConfig && typeof attachConfig === 'object') ? attachConfig : {}, false];
|
|
714
|
+
}
|
|
715
|
+
function _rawUploadFieldsMap(rawFieldsById, attachConfigs) {
|
|
716
|
+
const result = {};
|
|
717
|
+
if (!rawFieldsById || typeof rawFieldsById !== 'object')
|
|
718
|
+
return result;
|
|
719
|
+
attachConfigs = (attachConfigs && typeof attachConfigs === 'object') ? attachConfigs : {};
|
|
720
|
+
for (const fid of Object.keys(rawFieldsById)) {
|
|
721
|
+
const raw = rawFieldsById[fid];
|
|
722
|
+
if (!raw || typeof raw !== 'object')
|
|
723
|
+
continue;
|
|
724
|
+
if (String(raw.componentKey ?? '').toLowerCase() !== 'filecomponent')
|
|
725
|
+
continue;
|
|
726
|
+
const [layer] = _attachDirLayer(raw, attachConfigs[fid]);
|
|
727
|
+
const optionFid = String(layer.optionId ?? '').trim();
|
|
728
|
+
if (String(layer.folderSelectType ?? '').trim() !== '1' || !optionFid)
|
|
729
|
+
continue;
|
|
730
|
+
result[fid] = raw;
|
|
731
|
+
if (optionFid in rawFieldsById)
|
|
732
|
+
result[optionFid] = rawFieldsById[optionFid];
|
|
733
|
+
}
|
|
734
|
+
return result;
|
|
735
|
+
}
|
|
736
|
+
function _valueTokens(value) {
|
|
737
|
+
const tokens = [];
|
|
738
|
+
const add = (raw) => {
|
|
739
|
+
if (raw === undefined || raw === null)
|
|
740
|
+
return;
|
|
741
|
+
const text = String(raw).trim();
|
|
742
|
+
if (text && !tokens.includes(text))
|
|
743
|
+
tokens.push(text);
|
|
744
|
+
};
|
|
745
|
+
if (value && typeof value === 'object') {
|
|
746
|
+
for (const item of value.dataOptions || []) {
|
|
747
|
+
if (!item || typeof item !== 'object')
|
|
748
|
+
continue;
|
|
749
|
+
for (const key of ['optionId', 'id', 'valueKey', 'selectionId'])
|
|
750
|
+
add(item[key]);
|
|
751
|
+
for (const key of ['content', 'name'])
|
|
752
|
+
add(item[key]);
|
|
753
|
+
}
|
|
754
|
+
for (const key of ['content', 'name', 'value', 'optionId', 'id', 'valueKey', 'selectionId'])
|
|
755
|
+
add(value[key]);
|
|
756
|
+
}
|
|
757
|
+
else {
|
|
758
|
+
add(value);
|
|
759
|
+
}
|
|
760
|
+
return tokens;
|
|
761
|
+
}
|
|
762
|
+
function resolveOptionFolderId(optionFieldRaw, selectedValue) {
|
|
763
|
+
if (!optionFieldRaw || typeof optionFieldRaw !== 'object')
|
|
764
|
+
return null;
|
|
765
|
+
const rawOptions = optionFieldRaw.options;
|
|
766
|
+
if (!Array.isArray(rawOptions))
|
|
767
|
+
return null;
|
|
768
|
+
const tokens = _valueTokens(selectedValue);
|
|
769
|
+
if (!tokens.length)
|
|
770
|
+
return null;
|
|
771
|
+
const flat = [];
|
|
772
|
+
const collect = (items) => {
|
|
773
|
+
if (!Array.isArray(items))
|
|
774
|
+
return;
|
|
775
|
+
for (const item of items) {
|
|
776
|
+
if (item && typeof item === 'object') {
|
|
777
|
+
flat.push(item);
|
|
778
|
+
collect(item.children);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
collect(rawOptions);
|
|
783
|
+
const catalogueId = (option) => {
|
|
784
|
+
const catalogue = option.catalogue;
|
|
785
|
+
if (catalogue && typeof catalogue === 'object') {
|
|
786
|
+
const id = String(catalogue.catalogueId ?? '').trim();
|
|
787
|
+
if (id)
|
|
788
|
+
return id;
|
|
789
|
+
}
|
|
790
|
+
return null;
|
|
791
|
+
};
|
|
792
|
+
const idKeys = (option) => {
|
|
793
|
+
const keys = [];
|
|
794
|
+
for (const key of ['valueKey', 'selectionId', 'optionId', 'id']) {
|
|
795
|
+
if (option[key] !== undefined && option[key] !== null && String(option[key]).trim())
|
|
796
|
+
keys.push(String(option[key]).trim());
|
|
797
|
+
}
|
|
798
|
+
return keys;
|
|
799
|
+
};
|
|
800
|
+
const idMatches = [];
|
|
801
|
+
const nameMatches = [];
|
|
802
|
+
for (const option of flat) {
|
|
803
|
+
const iks = idKeys(option);
|
|
804
|
+
const names = [];
|
|
805
|
+
for (const key of ['content', 'name']) {
|
|
806
|
+
if (option[key] !== undefined && option[key] !== null && String(option[key]).trim())
|
|
807
|
+
names.push(String(option[key]).trim());
|
|
808
|
+
}
|
|
809
|
+
if (iks.some((k) => tokens.includes(k)))
|
|
810
|
+
idMatches.push(option);
|
|
811
|
+
else if (names.some((n) => tokens.includes(n)))
|
|
812
|
+
nameMatches.push(option);
|
|
813
|
+
}
|
|
814
|
+
if (idMatches.length === 1)
|
|
815
|
+
return catalogueId(idMatches[0]);
|
|
816
|
+
if (!idMatches.length && nameMatches.length === 1)
|
|
817
|
+
return catalogueId(nameMatches[0]);
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
function _attachmentFolderIds(rawUploadFields, attachConfigs, fields, valueByFid) {
|
|
821
|
+
const result = {};
|
|
822
|
+
if (!rawUploadFields || typeof rawUploadFields !== 'object' || !Array.isArray(fields))
|
|
823
|
+
return result;
|
|
824
|
+
attachConfigs = (attachConfigs && typeof attachConfigs === 'object') ? attachConfigs : {};
|
|
825
|
+
for (const field of fields) {
|
|
826
|
+
if (!field || typeof field !== 'object')
|
|
827
|
+
continue;
|
|
828
|
+
if (String(field.subFormId ?? '').trim())
|
|
829
|
+
continue;
|
|
830
|
+
if (String(field.componentKey ?? '').toLowerCase() !== 'filecomponent')
|
|
831
|
+
continue;
|
|
832
|
+
const fid = String(field.id ?? '').trim();
|
|
833
|
+
const raw = rawUploadFields[fid];
|
|
834
|
+
if (!raw || typeof raw !== 'object')
|
|
835
|
+
continue;
|
|
836
|
+
const [layer] = _attachDirLayer(raw, attachConfigs[fid]);
|
|
837
|
+
const optionFieldId = String(layer.optionId ?? '').trim();
|
|
838
|
+
if (String(layer.folderSelectType ?? '').trim() !== '1' || !optionFieldId)
|
|
839
|
+
continue;
|
|
840
|
+
const optionRaw = rawUploadFields[optionFieldId];
|
|
841
|
+
const selected = valueByFid[optionFieldId];
|
|
842
|
+
result[fid] = resolveOptionFolderId(optionRaw, selected);
|
|
843
|
+
}
|
|
844
|
+
return result;
|
|
845
|
+
}
|
|
846
|
+
function resolveFileUploadConstraints(rawField, tenantSetting, attachConfig, optionFieldRaw, selectedOptionValue) {
|
|
847
|
+
const txt = (key) => {
|
|
848
|
+
const v = rawField[key];
|
|
849
|
+
return v !== undefined && v !== null ? String(v).trim() : '';
|
|
850
|
+
};
|
|
851
|
+
const txtIn = (d, key) => {
|
|
852
|
+
const v = d && typeof d === 'object' ? d[key] : undefined;
|
|
853
|
+
return v !== undefined && v !== null ? String(v).trim() : '';
|
|
854
|
+
};
|
|
855
|
+
const [dirLayer] = _attachDirLayer(rawField, attachConfig);
|
|
856
|
+
let resolvedFolderId = null;
|
|
857
|
+
if (txtIn(dirLayer, 'folderSelectType') === '1') {
|
|
858
|
+
resolvedFolderId = resolveOptionFolderId(optionFieldRaw, selectedOptionValue);
|
|
859
|
+
}
|
|
860
|
+
else {
|
|
861
|
+
const folderIdText = txtIn(dirLayer, 'folderId');
|
|
862
|
+
if (folderIdText)
|
|
863
|
+
resolvedFolderId = folderIdText;
|
|
864
|
+
}
|
|
865
|
+
const sizeLayers = [];
|
|
866
|
+
if (txt('sizeType') === '1')
|
|
867
|
+
sizeLayers.push(rawField);
|
|
868
|
+
if (attachConfig && typeof attachConfig === 'object')
|
|
869
|
+
sizeLayers.push(attachConfig);
|
|
870
|
+
let sizeLimitMb = null;
|
|
871
|
+
for (const layer of sizeLayers) {
|
|
872
|
+
const idx = txtIn(layer, 'attmentSizeLimit');
|
|
873
|
+
if (['0', '1', '2', '3', '4', '5'].includes(idx)) {
|
|
874
|
+
sizeLimitMb = ATTACH_SIZE_LIMIT_STEPS_MB[Number(idx)];
|
|
875
|
+
break;
|
|
876
|
+
}
|
|
877
|
+
if (idx === '6' || idx === '9') {
|
|
878
|
+
const customText = txtIn(layer, 'customAttmentSize');
|
|
879
|
+
let customMb = 0;
|
|
880
|
+
try {
|
|
881
|
+
customMb = parseInt(customText, 10);
|
|
882
|
+
}
|
|
883
|
+
catch {
|
|
884
|
+
customMb = 0;
|
|
885
|
+
}
|
|
886
|
+
if (customMb > 0) {
|
|
887
|
+
sizeLimitMb = customMb;
|
|
888
|
+
break;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
if (sizeLimitMb === null) {
|
|
893
|
+
const maxSize = tenantSetting && tenantSetting.max_size_mb;
|
|
894
|
+
if (typeof maxSize === 'number' && maxSize > 0)
|
|
895
|
+
sizeLimitMb = maxSize;
|
|
896
|
+
}
|
|
897
|
+
let formatLimit = null;
|
|
898
|
+
const customLayers = [];
|
|
899
|
+
if (txt('limitType') === '1')
|
|
900
|
+
customLayers.push(rawField);
|
|
901
|
+
else if (attachConfig && typeof attachConfig === 'object')
|
|
902
|
+
customLayers.push(attachConfig);
|
|
903
|
+
for (const layer of customLayers) {
|
|
904
|
+
const customFormatText = txtIn(layer, 'customLimitFormat');
|
|
905
|
+
const customFormats = customFormatText ? customFormatText.split(',').map((p) => p.trim().toLowerCase()).filter((p) => p) : [];
|
|
906
|
+
if (customFormats.length) {
|
|
907
|
+
formatLimit = { mode: 'white', formats: customFormats };
|
|
908
|
+
break;
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
if (formatLimit === null && tenantSetting && typeof tenantSetting === 'object') {
|
|
912
|
+
const mode = tenantSetting.format_mode;
|
|
913
|
+
const formats = tenantSetting.formats;
|
|
914
|
+
if ((mode === 'white' || mode === 'black') && Array.isArray(formats) && formats.length) {
|
|
915
|
+
formatLimit = { mode, formats };
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
let maxNum = null;
|
|
919
|
+
const maxNumText = txt('maxNum');
|
|
920
|
+
if (maxNumText) {
|
|
921
|
+
let n = 0;
|
|
922
|
+
try {
|
|
923
|
+
n = parseInt(maxNumText, 10);
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
n = 0;
|
|
927
|
+
}
|
|
928
|
+
if (n > 0)
|
|
929
|
+
maxNum = n;
|
|
930
|
+
}
|
|
931
|
+
return [resolvedFolderId, sizeLimitMb, formatLimit, maxNum];
|
|
932
|
+
}
|
|
933
|
+
function buildFileUploadParams(runtime, frame, fieldId, isCreate, secretLevelId, folderId, sizeLimitMb, formatLimit, maxNum, readOnly) {
|
|
934
|
+
const moduleValue = String(frame.relate_type ?? '').trim() === 'WEA_EBUILDER' ? 'ebuildercard' : 'workflow';
|
|
935
|
+
const params = {
|
|
936
|
+
customParam: {
|
|
937
|
+
requestId: runtime.request_id,
|
|
938
|
+
workflowId: runtime.workflow_id,
|
|
939
|
+
nodeId: runtime.node_id,
|
|
940
|
+
readOnly: Boolean(readOnly),
|
|
941
|
+
identityId: runtime.identity_id,
|
|
942
|
+
identityType: runtime.identity_type,
|
|
943
|
+
isTest: false,
|
|
944
|
+
create: isCreate,
|
|
945
|
+
mobile: false,
|
|
946
|
+
formId: frame.form_id,
|
|
947
|
+
authSignatureStr: runtime.auth_signature,
|
|
948
|
+
authStr: runtime.auth_str,
|
|
949
|
+
fieldId,
|
|
950
|
+
},
|
|
951
|
+
source: 'form',
|
|
952
|
+
module: moduleValue,
|
|
953
|
+
authModule: moduleValue,
|
|
954
|
+
sourceRefId: runtime.request_id,
|
|
955
|
+
refId: runtime.request_id,
|
|
956
|
+
};
|
|
957
|
+
if (secretLevelId)
|
|
958
|
+
params.secretLevel = String(secretLevelId);
|
|
959
|
+
if (folderId)
|
|
960
|
+
params.folderId = String(folderId);
|
|
961
|
+
if (typeof sizeLimitMb === 'number' && sizeLimitMb > 0)
|
|
962
|
+
params.sizeLimit = sizeLimitMb;
|
|
963
|
+
if (formatLimit && typeof formatLimit === 'object' && (formatLimit.mode === 'white' || formatLimit.mode === 'black')) {
|
|
964
|
+
const formats = formatLimit.formats;
|
|
965
|
+
if (Array.isArray(formats) && formats.length)
|
|
966
|
+
params.formatLimit = { mode: formatLimit.mode, formats: [...formats] };
|
|
967
|
+
}
|
|
968
|
+
if (typeof maxNum === 'number' && maxNum > 0)
|
|
969
|
+
params.maxNum = maxNum;
|
|
970
|
+
return params;
|
|
971
|
+
}
|
|
972
|
+
function parseLayoutMeta(layoutData) {
|
|
973
|
+
const hiddenCellIds = new Set();
|
|
974
|
+
const compsFieldIds = new Set();
|
|
975
|
+
const layoutSubformIds = new Set();
|
|
976
|
+
let page0 = null;
|
|
977
|
+
const pageLayouts = layoutData && layoutData.formLayout ? layoutData.formLayout.pageLayouts : undefined;
|
|
978
|
+
if (Array.isArray(pageLayouts) && pageLayouts.length)
|
|
979
|
+
page0 = pageLayouts[0];
|
|
980
|
+
if (page0 && typeof page0 === 'object') {
|
|
981
|
+
const wb = page0.config && page0.config.workbook ? page0.config.workbook : {};
|
|
982
|
+
const sheet1 = wb.Sheet1;
|
|
983
|
+
const cells = sheet1 ? sheet1.cells : null;
|
|
984
|
+
const hidePositions = new Set();
|
|
985
|
+
if (Array.isArray(cells)) {
|
|
986
|
+
for (const cell of cells) {
|
|
987
|
+
if (!cell || typeof cell !== 'object')
|
|
988
|
+
continue;
|
|
989
|
+
const attrs = cell.tag && cell.tag.attrs ? cell.tag.attrs : {};
|
|
990
|
+
if (attrs.hide) {
|
|
991
|
+
try {
|
|
992
|
+
hidePositions.add(`${cell.col}:${cell.row}`);
|
|
993
|
+
}
|
|
994
|
+
catch {
|
|
995
|
+
/* ignore */
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
if (hidePositions.size) {
|
|
1001
|
+
for (const comp of page0.comps || []) {
|
|
1002
|
+
if (!comp || typeof comp !== 'object')
|
|
1003
|
+
continue;
|
|
1004
|
+
const cfg = comp.config || {};
|
|
1005
|
+
const fid = String(cfg.fieldId ?? '').trim();
|
|
1006
|
+
if (!fid)
|
|
1007
|
+
continue;
|
|
1008
|
+
const excel = cfg.excel || {};
|
|
1009
|
+
try {
|
|
1010
|
+
const pos = `${excel.col}:${excel.row}`;
|
|
1011
|
+
if (hidePositions.has(pos))
|
|
1012
|
+
hiddenCellIds.add(fid);
|
|
1013
|
+
}
|
|
1014
|
+
catch {
|
|
1015
|
+
/* ignore */
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
for (const comp of page0.comps || []) {
|
|
1020
|
+
if (!comp || typeof comp !== 'object')
|
|
1021
|
+
continue;
|
|
1022
|
+
const cfg = comp.config || {};
|
|
1023
|
+
const fid = String(cfg.fieldId ?? '').trim();
|
|
1024
|
+
if (fid)
|
|
1025
|
+
compsFieldIds.add(fid);
|
|
1026
|
+
if (String(cfg.type ?? '').toLowerCase() === 'detailtable' || String(cfg.type ?? '').toLowerCase() === 'datatable') {
|
|
1027
|
+
const sid = String(cfg.subFormId ?? '').trim();
|
|
1028
|
+
if (sid)
|
|
1029
|
+
layoutSubformIds.add(sid);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
return { hiddenCellIds, compsFieldIds, layoutSubformIds };
|
|
1034
|
+
}
|
|
1035
|
+
function applyViewRelateHidden(layoutSubformIds, viewRelateConfig) {
|
|
1036
|
+
if (Array.isArray(viewRelateConfig)) {
|
|
1037
|
+
for (const item of viewRelateConfig) {
|
|
1038
|
+
if (!item || typeof item !== 'object')
|
|
1039
|
+
continue;
|
|
1040
|
+
const fid = String(item.fieldId ?? '').trim();
|
|
1041
|
+
if (layoutSubformIds.has(fid) && item.display === false)
|
|
1042
|
+
layoutSubformIds.delete(fid);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* 「OA 未下发静态必填标记」的显式告警(V12.1.22 引入,V12.1.27 纠正措辞)。
|
|
1048
|
+
*
|
|
1049
|
+
* 静态必填的权威来源是 `getFormLayout` 的 `viewRelateConfig[]`(fieldId → required/disableEdit/display),
|
|
1050
|
+
* 回退来源是布局节点自身的 `required`/`mustInput`。**2026-09-17 更正**:此前观测到的"三个表单
|
|
1051
|
+
* `viewRelateConfig` 长度为 0"并非服务端没配置,而是 `getFormLayout` 请求多带了顶层 `permissionId`
|
|
1052
|
+
* (未启用节点权限的表单会因此拿不到字段级属性;见 `buildLayoutRequest`)。该请求口径修正后,
|
|
1053
|
+
* 本告警只在服务端**确实**不下发字段属性时才出现(如启用节点权限的表单)。
|
|
1054
|
+
* 真正未下发时:`requiredFields` 只剩系统字段、`subFormRequired` 恒为 `{}`,**不代表表单没有必填项**。
|
|
1055
|
+
*
|
|
1056
|
+
* V12.1.27 纠正(此前措辞有硬伤):原文案把 `missingFields` 也写成"为空不代表没有必填/无法据此校验业务必填",
|
|
1057
|
+
* 与事实相反——本租户的业务必填是**条件必填**,由 `viewLinkages` 的 `required` 规则承载,
|
|
1058
|
+
* prepare 阶段按实际所填数据**实时求值**,`missingFields` 完全可信(2026-09-17 真机验证:总部费用报销
|
|
1059
|
+
* `报销方式=银行(付公司)` → `missingFields=[账户名称,账号,开户机构名称]`)。原文案会诱导模型对用户说
|
|
1060
|
+
* "系统不会校验必填项",属错误陈述。现改为:静态标记未下发 + 条件必填清单(`conditionalRequired`)+
|
|
1061
|
+
* **missingFields 可信、照常 prepare**。
|
|
1062
|
+
*
|
|
1063
|
+
* 返回 null 表示静态必填信息可用,无需告警。
|
|
1064
|
+
*/
|
|
1065
|
+
function requiredInfoWarningText(context) {
|
|
1066
|
+
const ctx = context || {};
|
|
1067
|
+
const vrc = Array.isArray(ctx.viewRelateConfig) ? ctx.viewRelateConfig : [];
|
|
1068
|
+
if (vrc.length)
|
|
1069
|
+
return null; // OA 下发了字段级权限配置 → 静态必填信息可信
|
|
1070
|
+
const fields = Array.isArray(ctx.fields) ? ctx.fields : [];
|
|
1071
|
+
const business = fields.filter((f) => f && f.id
|
|
1072
|
+
&& !String(f.id).startsWith('REQUEST_') && !String(f.id).startsWith('SECLEVEL')
|
|
1073
|
+
&& f.display !== false && !f.readOnly && !f.disableEdit);
|
|
1074
|
+
// 没有业务字段(纯系统字段表单)→ 空清单是正常结果
|
|
1075
|
+
if (!business.length)
|
|
1076
|
+
return null;
|
|
1077
|
+
// 有任何字段带必填标记 → 至少拿到了部分真实信息,不必告警
|
|
1078
|
+
if (business.some((f) => f.required))
|
|
1079
|
+
return null;
|
|
1080
|
+
return 'OA 未下发该表单的**静态**必填标记,本次 requiredFields / subFormRequired 只含系统字段(流程标题等),'
|
|
1081
|
+
+ '但这**不代表该表单没有业务必填项**:业务必填见 conditionalRequired。'
|
|
1082
|
+
+ 'missingFields 由 prepare 按实际所填数据实时计算、**可信**——照常 prepare,'
|
|
1083
|
+
+ '并把 missingFields 与 conditionalRequired 一次性向用户问齐。'
|
|
1084
|
+
+ '不要对用户说"系统不会校验必填项"。';
|
|
1085
|
+
}
|
|
1086
|
+
// ---------------------------------------------------------------------------
|
|
1087
|
+
// createForm:loadBase/loadFrame/loadMinor/formLayout + 字段输出 + fileUploadParams
|
|
1088
|
+
// ---------------------------------------------------------------------------
|
|
1089
|
+
async function commandCreateForm(args, env) {
|
|
1090
|
+
const workflowId = String(args.workflowId ?? '').trim();
|
|
1091
|
+
if (!workflowId)
|
|
1092
|
+
throw workflowValidationError('workflow_id_required', 'workflowId 必填');
|
|
1093
|
+
const query = { workflowId, isCreate: 1, ...AJAX_QUERY };
|
|
1094
|
+
const body = { workflowId, isCreate: 1, isMobile: 0 };
|
|
1095
|
+
const baseResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadBase, query, body);
|
|
1096
|
+
const frameResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadFrame, query, body);
|
|
1097
|
+
let minorInfo = {};
|
|
1098
|
+
let minorWarning = null;
|
|
1099
|
+
try {
|
|
1100
|
+
const minorResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadMinor, query, { workflowId, isCreate: 1, isMobile: 0 });
|
|
1101
|
+
minorInfo = parseMinorInfo(minorResp);
|
|
1102
|
+
}
|
|
1103
|
+
catch {
|
|
1104
|
+
minorInfo = {};
|
|
1105
|
+
minorWarning = 'loadMinorInfo 获取系统字段权限失败,跳过系统字段(流程标题/紧急程度/密级)必填校验';
|
|
1106
|
+
}
|
|
1107
|
+
const base = baseResp;
|
|
1108
|
+
const frameData = frameResp;
|
|
1109
|
+
const runtime = parseRuntime(base, workflowId);
|
|
1110
|
+
const frame = parseFrame(frameData, runtime.node_id);
|
|
1111
|
+
let tenantSetting = {};
|
|
1112
|
+
try {
|
|
1113
|
+
const settingResp = await requestJson(env, 'POST', FORM_ENDPOINTS.tenantSetting, undefined, { setType: [] });
|
|
1114
|
+
tenantSetting = parseTenantSetting(settingResp);
|
|
1115
|
+
}
|
|
1116
|
+
catch {
|
|
1117
|
+
tenantSetting = {};
|
|
1118
|
+
}
|
|
1119
|
+
const layoutResp = await requestJson(env, 'POST', formApiUrl(FORM_ENDPOINTS.formLayout, frame.relate_type), { formId: frame.form_id, ...AJAX_QUERY }, buildLayoutRequest(workflowId, runtime, frame));
|
|
1120
|
+
const layoutData = layoutResp;
|
|
1121
|
+
if (!layoutData || typeof layoutData !== 'object')
|
|
1122
|
+
throw workflowApiError('format_error', 'getFormLayout 未返回有效 data');
|
|
1123
|
+
const returnedFormId = String((layoutData.form && layoutData.form.id) ?? '');
|
|
1124
|
+
if (returnedFormId && returnedFormId !== frame.form_id)
|
|
1125
|
+
throw workflowApiError('format_error', 'getFormLayout 返回了不同的 formId');
|
|
1126
|
+
const layoutId = String((layoutData.formLayout && layoutData.formLayout.id) ?? '');
|
|
1127
|
+
if (!layoutId)
|
|
1128
|
+
throw workflowApiError('format_error', '缺少 formLayout.id');
|
|
1129
|
+
const requiredMap = {};
|
|
1130
|
+
const disableEditMap = {};
|
|
1131
|
+
const displayMap = {};
|
|
1132
|
+
const viewRelateConfig = Array.isArray(layoutData.viewRelateConfig) ? layoutData.viewRelateConfig : [];
|
|
1133
|
+
for (const item of viewRelateConfig) {
|
|
1134
|
+
if (!item || typeof item !== 'object')
|
|
1135
|
+
continue;
|
|
1136
|
+
const fid = String(item.fieldId ?? '').trim();
|
|
1137
|
+
if (!fid)
|
|
1138
|
+
continue;
|
|
1139
|
+
requiredMap[fid] = Boolean(item.required);
|
|
1140
|
+
disableEditMap[fid] = Boolean(item.disableEdit);
|
|
1141
|
+
displayMap[fid] = item.display !== undefined ? Boolean(item.display) : true;
|
|
1142
|
+
}
|
|
1143
|
+
const fields = extractFields(layoutData, requiredMap, disableEditMap, displayMap);
|
|
1144
|
+
const rawFieldsById = walkLayoutRawFields(layoutData.formLayout);
|
|
1145
|
+
const attachConfigs = (layoutData.attachConfigs && typeof layoutData.attachConfigs === 'object') ? layoutData.attachConfigs : {};
|
|
1146
|
+
const rawUploadFields = _rawUploadFieldsMap(rawFieldsById, attachConfigs);
|
|
1147
|
+
const meta = parseLayoutMeta(layoutData);
|
|
1148
|
+
const hiddenCellIds = meta.hiddenCellIds;
|
|
1149
|
+
let compsFieldIds = meta.compsFieldIds;
|
|
1150
|
+
const layoutSubformIds = meta.layoutSubformIds;
|
|
1151
|
+
applyViewRelateHidden(layoutSubformIds, viewRelateConfig);
|
|
1152
|
+
const visibleFields = [];
|
|
1153
|
+
for (const f of fields) {
|
|
1154
|
+
const fid = String(f.id ?? '');
|
|
1155
|
+
if (hiddenCellIds.has(fid))
|
|
1156
|
+
continue;
|
|
1157
|
+
if (compsFieldIds.size && !String(f.subFormId ?? '').trim() && !compsFieldIds.has(fid))
|
|
1158
|
+
continue;
|
|
1159
|
+
visibleFields.push(f);
|
|
1160
|
+
}
|
|
1161
|
+
const defaults = normalizeDefaults(layoutData.defaultDetails);
|
|
1162
|
+
// 计算必填/可编辑/明细必填(按默认值 + onLoadForm 联动)
|
|
1163
|
+
const systemFieldValues = _normalizeSystemFieldValues(layoutData.businessOperatorObj);
|
|
1164
|
+
let linkageActions = Array.isArray(layoutData.linkageActions) ? layoutData.linkageActions : [];
|
|
1165
|
+
let onLoadLinkage = [];
|
|
1166
|
+
if (linkageActions.length) {
|
|
1167
|
+
const loadEntries = _entriesFromDefaults(defaults);
|
|
1168
|
+
try {
|
|
1169
|
+
onLoadLinkage = await runFieldLinkage(env, buildLinkageContext({ runtime, frame, layout: { form_id: frame.form_id }, linkageActions }), loadEntries, new Set(['onLoadForm']), defaults);
|
|
1170
|
+
}
|
|
1171
|
+
catch {
|
|
1172
|
+
onLoadLinkage = [];
|
|
1173
|
+
}
|
|
1174
|
+
if (onLoadLinkage.length) {
|
|
1175
|
+
const merged = mergeLinkageDetails(defaults, onLoadLinkage);
|
|
1176
|
+
defaults.length = 0;
|
|
1177
|
+
for (const d of merged)
|
|
1178
|
+
defaults.push(d);
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(layoutData.viewLinkages, [], defaults, onLoadLinkage, systemFieldValues);
|
|
1182
|
+
const mainHidden = new Set(rowHidden.main || []);
|
|
1183
|
+
const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
|
|
1184
|
+
if (initialHidden.size) {
|
|
1185
|
+
for (const id of initialHidden)
|
|
1186
|
+
mainHidden.add(id);
|
|
1187
|
+
rowHidden.main = mainHidden;
|
|
1188
|
+
}
|
|
1189
|
+
const vrcFieldIds = new Set(viewRelateConfig.filter((i) => i && typeof i === 'object' && String(i.fieldId ?? '').trim()).map((i) => String(i.fieldId)));
|
|
1190
|
+
const mainDisabled = new Set(rowDisabled.main || []);
|
|
1191
|
+
const mainExtraRequired = new Set(rowExtraRequired.main || []);
|
|
1192
|
+
const requiredFieldsList = [];
|
|
1193
|
+
const editableFieldsList = [];
|
|
1194
|
+
const subFormRequired = {};
|
|
1195
|
+
const subFormLinkageRequired = {};
|
|
1196
|
+
for (const key of Object.keys(rowExtraRequired)) {
|
|
1197
|
+
if (key === 'main' || !key.includes(':'))
|
|
1198
|
+
continue;
|
|
1199
|
+
const subFormId = key.split(':', 1)[0];
|
|
1200
|
+
for (const fid of rowExtraRequired[key]) {
|
|
1201
|
+
const target = visibleFields.find((f) => String(f.id ?? '') === fid);
|
|
1202
|
+
if (target)
|
|
1203
|
+
subFormLinkageRequired[subFormId] = subFormLinkageRequired[subFormId] || [];
|
|
1204
|
+
if (target)
|
|
1205
|
+
subFormLinkageRequired[subFormId].push(slimFieldInfo(target));
|
|
1206
|
+
}
|
|
1207
|
+
}
|
|
1208
|
+
const folderOptionValues = _mainValueMap(defaults);
|
|
1209
|
+
for (const field of visibleFields) {
|
|
1210
|
+
const fieldId = String(field.id ?? '');
|
|
1211
|
+
const subFormId = String(field.subFormId ?? '');
|
|
1212
|
+
if (field.readOnly || field.disableEdit || mainDisabled.has(fieldId))
|
|
1213
|
+
continue;
|
|
1214
|
+
if (mainHidden.has(fieldId) || (subFormId && hiddenSubforms.has(subFormId)))
|
|
1215
|
+
continue;
|
|
1216
|
+
if (fieldId && vrcFieldIds.size && !vrcFieldIds.has(fieldId))
|
|
1217
|
+
continue;
|
|
1218
|
+
if (field.required || mainExtraRequired.has(fieldId)) {
|
|
1219
|
+
const slim = slimFieldInfo(field);
|
|
1220
|
+
if (subFormId) {
|
|
1221
|
+
subFormRequired[subFormId] = subFormRequired[subFormId] || [];
|
|
1222
|
+
subFormRequired[subFormId].push(slim);
|
|
1223
|
+
continue;
|
|
1224
|
+
}
|
|
1225
|
+
requiredFieldsList.push(slim);
|
|
1226
|
+
continue;
|
|
1227
|
+
}
|
|
1228
|
+
editableFieldsList.push(slimFieldInfo(field));
|
|
1229
|
+
}
|
|
1230
|
+
for (const subFormId of Object.keys(subFormLinkageRequired)) {
|
|
1231
|
+
const existing = new Set((subFormRequired[subFormId] || []).map((f) => String(f.id ?? '')));
|
|
1232
|
+
for (const slim of subFormLinkageRequired[subFormId]) {
|
|
1233
|
+
if (!existing.has(String(slim.id ?? ''))) {
|
|
1234
|
+
subFormRequired[subFormId] = subFormRequired[subFormId] || [];
|
|
1235
|
+
subFormRequired[subFormId].push(slim);
|
|
1236
|
+
existing.add(String(slim.id ?? ''));
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
const slimFields = [];
|
|
1241
|
+
for (const field of visibleFields) {
|
|
1242
|
+
const item = {
|
|
1243
|
+
id: field.id,
|
|
1244
|
+
name: field.name,
|
|
1245
|
+
subFormId: field.subFormId,
|
|
1246
|
+
componentKey: field.componentKey,
|
|
1247
|
+
};
|
|
1248
|
+
if (field.required)
|
|
1249
|
+
item.required = true;
|
|
1250
|
+
if (field.readOnly || field.disableEdit) {
|
|
1251
|
+
item.readOnly = Boolean(field.readOnly);
|
|
1252
|
+
item.disableEdit = Boolean(field.disableEdit);
|
|
1253
|
+
}
|
|
1254
|
+
for (const key of ['isBrowser', 'isSelect', 'isMultiple', 'browserType', 'browserModule']) {
|
|
1255
|
+
if (field[key])
|
|
1256
|
+
item[key] = field[key];
|
|
1257
|
+
}
|
|
1258
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'employeescope')
|
|
1259
|
+
item.isEmployeeScope = true;
|
|
1260
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'einvoicecomponent')
|
|
1261
|
+
item.isEinvoice = true;
|
|
1262
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'filecomponent') {
|
|
1263
|
+
const fileFieldId = String(field.id ?? '');
|
|
1264
|
+
if (fileFieldId && !(field.readOnly || field.disableEdit)) {
|
|
1265
|
+
const rawFileField = rawFieldsById[fileFieldId] || {};
|
|
1266
|
+
const attachCfg = attachConfigs[fileFieldId];
|
|
1267
|
+
const [dirLayer] = _attachDirLayer(rawFileField, attachCfg);
|
|
1268
|
+
let optionFieldRaw = null;
|
|
1269
|
+
let optionValue = null;
|
|
1270
|
+
if (String(dirLayer.folderSelectType ?? '').trim() === '1') {
|
|
1271
|
+
item.folderDependsOnOption = true;
|
|
1272
|
+
const optionFieldId = String(dirLayer.optionId ?? '').trim();
|
|
1273
|
+
if (optionFieldId) {
|
|
1274
|
+
optionFieldRaw = rawFieldsById[optionFieldId];
|
|
1275
|
+
optionValue = folderOptionValues[optionFieldId];
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
const [folderId, sizeLimitMb, formatLimit, maxNum] = resolveFileUploadConstraints(rawFileField, tenantSetting, attachCfg || null, optionFieldRaw, optionValue);
|
|
1279
|
+
item.fileUploadParams = buildFileUploadParams(runtime, frame, fileFieldId, true, minorInfo.sec_default_id || null, folderId, sizeLimitMb, formatLimit, maxNum, Boolean(field.readOnly || field.disableEdit));
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'textarea' && field.richEditor)
|
|
1283
|
+
item.componentKey = 'RichText';
|
|
1284
|
+
if (String(field.format ?? '').trim())
|
|
1285
|
+
item.format = String(field.format).trim();
|
|
1286
|
+
const options = field.options || [];
|
|
1287
|
+
if (options.length)
|
|
1288
|
+
item.options = options;
|
|
1289
|
+
slimFields.push(item);
|
|
1290
|
+
}
|
|
1291
|
+
const fieldsById = new Map();
|
|
1292
|
+
for (const f of fields)
|
|
1293
|
+
fieldsById.set(String(f.id ?? ''), f);
|
|
1294
|
+
const initValues = [];
|
|
1295
|
+
for (const d of defaults) {
|
|
1296
|
+
const fid = String((d.formField && d.formField.id) ?? '').trim();
|
|
1297
|
+
if (!fid || hiddenCellIds.has(fid))
|
|
1298
|
+
continue;
|
|
1299
|
+
const sf = String((d.subForm && d.subForm.id) ?? '').trim();
|
|
1300
|
+
if (sf) {
|
|
1301
|
+
if (layoutSubformIds.size && !layoutSubformIds.has(sf))
|
|
1302
|
+
continue;
|
|
1303
|
+
}
|
|
1304
|
+
else if (compsFieldIds.size && !compsFieldIds.has(fid)) {
|
|
1305
|
+
continue;
|
|
1306
|
+
}
|
|
1307
|
+
const disp = _detailDisplayText(d);
|
|
1308
|
+
if (!disp)
|
|
1309
|
+
continue;
|
|
1310
|
+
const f = fieldsById.get(fid) || {};
|
|
1311
|
+
const iv = { fieldId: fid, name: f.name ?? fid, value: disp };
|
|
1312
|
+
if (sf)
|
|
1313
|
+
iv.subFormId = sf;
|
|
1314
|
+
initValues.push(iv);
|
|
1315
|
+
}
|
|
1316
|
+
const session = await getSessionContext(env);
|
|
1317
|
+
const context = {
|
|
1318
|
+
kind: 'CREATE',
|
|
1319
|
+
version: 1,
|
|
1320
|
+
baseUrl: session.baseUrl,
|
|
1321
|
+
profile: session.profile,
|
|
1322
|
+
userId: session.userId,
|
|
1323
|
+
oaBaseUrl: session.baseUrl,
|
|
1324
|
+
workflow: {
|
|
1325
|
+
id: workflowId,
|
|
1326
|
+
name: runtime.workflow_name,
|
|
1327
|
+
description: runtime.workflow_description,
|
|
1328
|
+
type_name: runtime.workflow_type_name,
|
|
1329
|
+
},
|
|
1330
|
+
runtime,
|
|
1331
|
+
frame,
|
|
1332
|
+
layout: { form_id: frame.form_id, layout_id: layoutId },
|
|
1333
|
+
fields,
|
|
1334
|
+
defaults,
|
|
1335
|
+
rawUploadFields,
|
|
1336
|
+
attachConfigs,
|
|
1337
|
+
viewLinkages: Array.isArray(layoutData.viewLinkages) ? layoutData.viewLinkages : null,
|
|
1338
|
+
systemFieldValues,
|
|
1339
|
+
linkageActions,
|
|
1340
|
+
viewRelateConfig,
|
|
1341
|
+
hiddenCellFieldIds: [...hiddenCellIds],
|
|
1342
|
+
compsFieldIds: [...compsFieldIds],
|
|
1343
|
+
layoutSubformIds: [...layoutSubformIds],
|
|
1344
|
+
systemFields: {
|
|
1345
|
+
permissions: minorInfo.permissions || {},
|
|
1346
|
+
level_options: minorInfo.level_options || [],
|
|
1347
|
+
sec_level_options: minorInfo.sec_level_options || [],
|
|
1348
|
+
defaults: {
|
|
1349
|
+
requestName: runtime.default_request_name || '',
|
|
1350
|
+
requestLevel: runtime.default_request_level || '',
|
|
1351
|
+
secLevel: runtime.default_sec_level || '',
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
minorInfo,
|
|
1355
|
+
tenantSetting,
|
|
1356
|
+
onLoadLinkage,
|
|
1357
|
+
};
|
|
1358
|
+
const token = await env.continuation.issue({ operation: 'workflow.createForm', baseUrl: session.baseUrl, profile: session.profile, userId: session.userId, payload: context }, CONTINUATION_TTL_MS);
|
|
1359
|
+
// 条件必填清单(V12.1.27):viewLinkages 里的 required 规则原样结构化,form 阶段即可主动问齐必填项。
|
|
1360
|
+
const conditionalRequired = conditionalRequiredFields(layoutData.viewLinkages, rawFieldsById);
|
|
1361
|
+
const requiredFieldIds = requiredFieldsWithSystem(requiredFieldsList, runtime, minorInfo.permissions || {});
|
|
1362
|
+
// 可写字段 = 必填(主表 + 明细,含系统字段)+ 其余可编辑字段。
|
|
1363
|
+
// 只在 `fields` 真被整体移出 stdout 时才会随输出(由 compactFormOutput 决定)。
|
|
1364
|
+
const writableFieldIds = [];
|
|
1365
|
+
for (const id of requiredFieldIds)
|
|
1366
|
+
writableFieldIds.push(id);
|
|
1367
|
+
for (const list of Object.values(subFormRequired)) {
|
|
1368
|
+
for (const f of list)
|
|
1369
|
+
writableFieldIds.push(String(f?.id ?? ''));
|
|
1370
|
+
}
|
|
1371
|
+
for (const f of editableFieldsList)
|
|
1372
|
+
writableFieldIds.push(String(f?.id ?? ''));
|
|
1373
|
+
const writableFieldsCompact = encodeWritableFieldsCompact([...new Set(writableFieldIds)], visibleFields, SYSTEM_FIELD_KEYS);
|
|
1374
|
+
return workflowSuccess('workflow.createForm', await compactFormOutput({
|
|
1375
|
+
status: 'FORM_READY',
|
|
1376
|
+
contextId: token,
|
|
1377
|
+
requestId: runtime.request_id,
|
|
1378
|
+
requestName: runtime.default_request_name || '',
|
|
1379
|
+
workflow: context.workflow,
|
|
1380
|
+
form: { id: frame.form_id, layout_id: layoutId },
|
|
1381
|
+
requiredFields: requiredFieldIds,
|
|
1382
|
+
subFormRequired: Object.fromEntries(Object.entries(subFormRequired).map(([k, v]) => [k, v.map((f) => String(f.id ?? ''))])),
|
|
1383
|
+
editableFields: editableFieldsList.map((f) => String(f.id ?? '')),
|
|
1384
|
+
fields: slimFields,
|
|
1385
|
+
initialValues: initValues,
|
|
1386
|
+
systemFields: {
|
|
1387
|
+
permissions: minorInfo.permissions || {},
|
|
1388
|
+
defaults: context.systemFields.defaults,
|
|
1389
|
+
levelOptions: minorInfo.level_options || [],
|
|
1390
|
+
secLevelOptions: minorInfo.sec_level_options || [],
|
|
1391
|
+
},
|
|
1392
|
+
systemFieldWarning: minorWarning,
|
|
1393
|
+
requiredInfoWarning: requiredInfoWarningText(context),
|
|
1394
|
+
...(conditionalRequired.length ? { conditionalRequired } : {}),
|
|
1395
|
+
// 原 Python 把完整 context 落盘;此处通过 continuation 承载,返回结构保持文档契约。
|
|
1396
|
+
continuation: token,
|
|
1397
|
+
}, env, 'last_create_form.json', writableFieldsCompact));
|
|
1398
|
+
}
|
|
1399
|
+
// ---------------------------------------------------------------------------
|
|
1400
|
+
// 输入拆分 / 字段绑定 / 只读校验
|
|
1401
|
+
// ---------------------------------------------------------------------------
|
|
1402
|
+
function splitInput(value) {
|
|
1403
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1404
|
+
throw workflowValidationError('input_invalid', '表单数据必须是 JSON 对象');
|
|
1405
|
+
let main;
|
|
1406
|
+
let details;
|
|
1407
|
+
if ('main' in value || 'details' in value) {
|
|
1408
|
+
main = value.main || {};
|
|
1409
|
+
details = value.details || {};
|
|
1410
|
+
}
|
|
1411
|
+
else {
|
|
1412
|
+
main = value;
|
|
1413
|
+
details = {};
|
|
1414
|
+
}
|
|
1415
|
+
if (!main || typeof main !== 'object' || !details || typeof details !== 'object')
|
|
1416
|
+
throw workflowValidationError('input_invalid', 'main 和 details 必须是 JSON 对象');
|
|
1417
|
+
const normalizedDetails = {};
|
|
1418
|
+
for (const subFormId of Object.keys(details)) {
|
|
1419
|
+
const rows = details[subFormId];
|
|
1420
|
+
if (!Array.isArray(rows) || !rows.every((r) => r && typeof r === 'object')) {
|
|
1421
|
+
throw workflowValidationError('input_invalid', `明细表 ${subFormId} 必须是对象数组`);
|
|
1422
|
+
}
|
|
1423
|
+
normalizedDetails[String(subFormId)] = rows;
|
|
1424
|
+
}
|
|
1425
|
+
return [main, normalizedDetails];
|
|
1426
|
+
}
|
|
1427
|
+
function bindInputFields(main, details, fields) {
|
|
1428
|
+
const entries = [];
|
|
1429
|
+
const ambiguities = [];
|
|
1430
|
+
const resolve = (key, subFormId) => {
|
|
1431
|
+
const scope = fields.filter((f) => String(f.subFormId ?? '') === subFormId);
|
|
1432
|
+
const idMatches = scope.filter((f) => String(f.id) === key);
|
|
1433
|
+
const nameMatches = scope.filter((f) => String(f.name ?? '').toLowerCase() === key.toLowerCase());
|
|
1434
|
+
const matches = idMatches.length ? idMatches : nameMatches;
|
|
1435
|
+
if (matches.length === 1)
|
|
1436
|
+
return matches[0];
|
|
1437
|
+
ambiguities.push({
|
|
1438
|
+
kind: 'FIELD',
|
|
1439
|
+
input: key,
|
|
1440
|
+
subFormId,
|
|
1441
|
+
candidates: matches.map((m) => ({ id: m.id, name: m.name })),
|
|
1442
|
+
reason: matches.length ? 'DUPLICATE_NAME' : 'NOT_FOUND',
|
|
1443
|
+
});
|
|
1444
|
+
return null;
|
|
1445
|
+
};
|
|
1446
|
+
for (const key of Object.keys(main)) {
|
|
1447
|
+
const field = resolve(String(key), '');
|
|
1448
|
+
if (field)
|
|
1449
|
+
entries.push({ field, value: main[key] });
|
|
1450
|
+
}
|
|
1451
|
+
const rowSeed = Date.now() * 100000 + Math.floor(Math.random() * 70000) + 10000;
|
|
1452
|
+
let rowOffset = 0;
|
|
1453
|
+
for (const subFormId of Object.keys(details)) {
|
|
1454
|
+
const rows = details[subFormId];
|
|
1455
|
+
rows.forEach((row, index) => {
|
|
1456
|
+
const rowId = String(rowSeed + rowOffset);
|
|
1457
|
+
rowOffset += 1;
|
|
1458
|
+
const rowMeta = { dataIndex: index + 1, rowId, subForm: { id: subFormId } };
|
|
1459
|
+
for (const key of Object.keys(row)) {
|
|
1460
|
+
const field = resolve(String(key), subFormId);
|
|
1461
|
+
if (field)
|
|
1462
|
+
entries.push({ field, value: row[key], row: rowMeta });
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
return [entries, ambiguities];
|
|
1467
|
+
}
|
|
1468
|
+
function readonlyEntryViolations(entries) {
|
|
1469
|
+
const hits = [];
|
|
1470
|
+
for (const entry of entries) {
|
|
1471
|
+
const field = entry.field || {};
|
|
1472
|
+
if (field.readOnly || field.disableEdit) {
|
|
1473
|
+
hits.push({ fieldId: String(field.id ?? ''), name: String(field.name ?? ''), subFormId: String(field.subFormId ?? '') });
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1476
|
+
return hits;
|
|
1477
|
+
}
|
|
1478
|
+
// ---------------------------------------------------------------------------
|
|
1479
|
+
// 人员范围字段(componentKey=EmployeeScope)名称→ID
|
|
1480
|
+
// 来源:V11.3 `e10_workflow.py` 的 `EMPLOYEE_SCOPE_BROWSER_TYPES` /
|
|
1481
|
+
// `employee_scope_browser_candidates` / `_employee_scope_strip_suffix`。
|
|
1482
|
+
// 人员范围同一字段内可混合 8 类对象,各类走各自 /api/hrm/common/browser/complete/{type};
|
|
1483
|
+
// all / allExternal 是特殊类,不调接口(optionId 固定 "0")。
|
|
1484
|
+
// ---------------------------------------------------------------------------
|
|
1485
|
+
const EMPLOYEE_SCOPE_MODULE = 'hrm';
|
|
1486
|
+
const EMPLOYEE_SCOPE_BROWSER_TYPES = {
|
|
1487
|
+
user: 'resource',
|
|
1488
|
+
dept: 'department',
|
|
1489
|
+
subcompany: 'subcompany',
|
|
1490
|
+
group: 'channel',
|
|
1491
|
+
role: 'role',
|
|
1492
|
+
position: 'position',
|
|
1493
|
+
external: 'external',
|
|
1494
|
+
};
|
|
1495
|
+
const EMPLOYEE_SCOPE_SPECIAL_TYPES = { all: '所有人', allExternal: '所有外部联系人' };
|
|
1496
|
+
/** 人员范围名称常见口语后缀(出现在名称末尾时剥离后重试搜索)。 */
|
|
1497
|
+
const EMPLOYEE_SCOPE_NAME_SUFFIXES = ['部门', '分部', '岗位', '群组', '群', '角色', '人员'];
|
|
1498
|
+
function isEmployeeScopeField(field) {
|
|
1499
|
+
return Boolean(field?.isEmployeeScope)
|
|
1500
|
+
|| String(field?.componentKey ?? '').toLowerCase() === 'employeescope';
|
|
1501
|
+
}
|
|
1502
|
+
/** 认证/网络错误必须原样抛出,不得降级成"未匹配到候选"(读路径红线)。 */
|
|
1503
|
+
function rethrowAuthOrNetwork(error) {
|
|
1504
|
+
const type = error?.type;
|
|
1505
|
+
if (type === 'authentication' || type === 'network')
|
|
1506
|
+
throw error;
|
|
1507
|
+
}
|
|
1508
|
+
function stripEmployeeScopeSuffix(keyword) {
|
|
1509
|
+
for (const suffix of EMPLOYEE_SCOPE_NAME_SUFFIXES) {
|
|
1510
|
+
if (keyword.endsWith(suffix) && keyword.length > suffix.length)
|
|
1511
|
+
return keyword.slice(0, -suffix.length);
|
|
1512
|
+
}
|
|
1513
|
+
return keyword;
|
|
1514
|
+
}
|
|
1515
|
+
/** all / allExternal 的固定结构(optionId 恒为 "0",optionObj 为固定字段)。 */
|
|
1516
|
+
function employeeScopeSpecialCandidate(scopeType) {
|
|
1517
|
+
const name = EMPLOYEE_SCOPE_SPECIAL_TYPES[scopeType];
|
|
1518
|
+
if (!name)
|
|
1519
|
+
return null;
|
|
1520
|
+
const raw = scopeType === 'all'
|
|
1521
|
+
? {
|
|
1522
|
+
content: name,
|
|
1523
|
+
type: 'all',
|
|
1524
|
+
icon: 'Icon-all02',
|
|
1525
|
+
_entityContent: '所有人',
|
|
1526
|
+
_secretLevel: '0-100',
|
|
1527
|
+
secretLevel: '0-100',
|
|
1528
|
+
}
|
|
1529
|
+
: { content: name, type: 'allExternal', icon: 'Icon-all02', _entityContent: '所有外部联系人' };
|
|
1530
|
+
return { id: '0', name, raw };
|
|
1531
|
+
}
|
|
1532
|
+
/**
|
|
1533
|
+
* 人员范围字段单类对象名称转 ID:`POST /api/hrm/common/browser/complete/{browserType}`,
|
|
1534
|
+
* 入参统一 `{browserMultiple:true, newly:1, searchValue:keyword}`,返回 `[{id,name,raw}]`
|
|
1535
|
+
* (raw 为接口原始对象,保存时作为 optionObj)。0 命中时按 V11.3 剥离口语后缀重试一次。
|
|
1536
|
+
*/
|
|
1537
|
+
async function employeeScopeCandidates(env, scopeType, keyword) {
|
|
1538
|
+
const special = employeeScopeSpecialCandidate(scopeType);
|
|
1539
|
+
if (special)
|
|
1540
|
+
return [special];
|
|
1541
|
+
const browserType = EMPLOYEE_SCOPE_BROWSER_TYPES[scopeType];
|
|
1542
|
+
const kw = String(keyword ?? '').trim();
|
|
1543
|
+
if (!browserType || !kw)
|
|
1544
|
+
return [];
|
|
1545
|
+
const fetchOnce = async (searchValue) => {
|
|
1546
|
+
let response;
|
|
1547
|
+
try {
|
|
1548
|
+
response = await env.host.workflowRequest({
|
|
1549
|
+
method: 'POST',
|
|
1550
|
+
path: `/api/${EMPLOYEE_SCOPE_MODULE}/common/browser/complete/${browserType}`,
|
|
1551
|
+
operation: 'name.resolve',
|
|
1552
|
+
body: { browserMultiple: true, newly: 1, searchValue },
|
|
1553
|
+
});
|
|
1554
|
+
}
|
|
1555
|
+
catch (error) {
|
|
1556
|
+
// V11.3 把请求失败一律当 0 命中;本项目红线要求认证/网络错误原样抛出,其余按 0 命中。
|
|
1557
|
+
rethrowAuthOrNetwork(error);
|
|
1558
|
+
return [];
|
|
1559
|
+
}
|
|
1560
|
+
const data = responseData(response);
|
|
1561
|
+
const list = data?.data?.data ?? data?.data;
|
|
1562
|
+
if (!Array.isArray(list))
|
|
1563
|
+
return [];
|
|
1564
|
+
const out = [];
|
|
1565
|
+
const seen = new Set();
|
|
1566
|
+
for (const item of list) {
|
|
1567
|
+
if (!item || typeof item !== 'object')
|
|
1568
|
+
continue;
|
|
1569
|
+
const id = String(item.id ?? '').trim();
|
|
1570
|
+
if (!id || seen.has(id))
|
|
1571
|
+
continue;
|
|
1572
|
+
seen.add(id);
|
|
1573
|
+
out.push({ id, name: String(item.name ?? item.content ?? '').trim(), raw: item });
|
|
1574
|
+
}
|
|
1575
|
+
return out;
|
|
1576
|
+
};
|
|
1577
|
+
let candidates = await fetchOnce(kw);
|
|
1578
|
+
if (!candidates.length) {
|
|
1579
|
+
const stripped = stripEmployeeScopeSuffix(kw);
|
|
1580
|
+
if (stripped && stripped !== kw)
|
|
1581
|
+
candidates = await fetchOnce(stripped);
|
|
1582
|
+
}
|
|
1583
|
+
return candidates;
|
|
1584
|
+
}
|
|
1585
|
+
// ---------------------------------------------------------------------------
|
|
1586
|
+
// 值序列化(serializeValue:文本/选项/浏览按钮/附件/发票/人员范围)
|
|
1587
|
+
// ---------------------------------------------------------------------------
|
|
1588
|
+
class ValueAmbiguity extends Error {
|
|
1589
|
+
details;
|
|
1590
|
+
constructor(details) {
|
|
1591
|
+
super(details.reason || 'AMBIGUOUS');
|
|
1592
|
+
this.details = details;
|
|
1593
|
+
}
|
|
1594
|
+
}
|
|
1595
|
+
function listValues(value) {
|
|
1596
|
+
return Array.isArray(value) ? value : [value];
|
|
1597
|
+
}
|
|
1598
|
+
function assertMultiplicity(field, options) {
|
|
1599
|
+
if (!field.isMultiple && options.length > 1) {
|
|
1600
|
+
throw new ValueAmbiguity({ kind: 'MULTIPLICITY', fieldId: field.id, fieldName: field.name, reason: '该字段是单选,但输入了多个值', candidates: options });
|
|
1601
|
+
}
|
|
1602
|
+
}
|
|
1603
|
+
function normalizeRelateCandidate(value) {
|
|
1604
|
+
if (!value || typeof value !== 'object')
|
|
1605
|
+
return null;
|
|
1606
|
+
const id = String(value.relateId ?? '').trim();
|
|
1607
|
+
const name = String(value.name ?? '').trim();
|
|
1608
|
+
return id && name ? { id, name } : null;
|
|
1609
|
+
}
|
|
1610
|
+
function serializeValue(field, value, mappings, row, browserHints = {}) {
|
|
1611
|
+
const detail = { ...(row || {}), formField: { id: field.id } };
|
|
1612
|
+
const isEmployeeScope = field.isEmployeeScope || String(field.componentKey ?? '').toLowerCase() === 'employeescope';
|
|
1613
|
+
if (isEmployeeScope) {
|
|
1614
|
+
const options = [];
|
|
1615
|
+
for (const item of listValues(value)) {
|
|
1616
|
+
if (!item || typeof item !== 'object') {
|
|
1617
|
+
throw new ValueAmbiguity({ kind: 'EMPLOYEE_SCOPE', fieldId: field.id, fieldName: field.name, reason: '人员范围字段的值必须是对象数组,每项含 name 和 type', resolution: '格式如 [{"name":"袁湘云","type":"user"}]' });
|
|
1618
|
+
}
|
|
1619
|
+
const optionId = String(item.optionId ?? item.id ?? '').trim();
|
|
1620
|
+
const content = String(item.content ?? item.name ?? '').trim();
|
|
1621
|
+
const scopeType = String(item.type ?? '').trim();
|
|
1622
|
+
if (optionId && content && scopeType) {
|
|
1623
|
+
const option = { optionId, content, type: scopeType };
|
|
1624
|
+
if (item.optionObj && typeof item.optionObj === 'object')
|
|
1625
|
+
option.optionObj = item.optionObj;
|
|
1626
|
+
options.push(option);
|
|
1627
|
+
continue;
|
|
1628
|
+
}
|
|
1629
|
+
if (!content || !scopeType) {
|
|
1630
|
+
throw new ValueAmbiguity({ kind: 'EMPLOYEE_SCOPE', fieldId: field.id, fieldName: field.name, input: item, reason: '缺少名称或类型', resolution: '每项必须含 name/content 与 type(user/dept/subcompany/group/role/position/external/all/allExternal)' });
|
|
1631
|
+
}
|
|
1632
|
+
// 名称/类型齐了却没拿到 ID:说明名称→ID 解析没成功(预解析失败或未走预解析)。
|
|
1633
|
+
// **绝不静默丢弃**——空 optionId 会让 hasDetailValue 判为无值,整条值从保存体里消失,
|
|
1634
|
+
// 用户看到"已准备"、实际收件人为空(2026-09-16「内部留言」被留言人字段的实测现象)。
|
|
1635
|
+
throw new ValueAmbiguity({
|
|
1636
|
+
kind: 'EMPLOYEE_SCOPE',
|
|
1637
|
+
fieldId: field.id,
|
|
1638
|
+
fieldName: field.name,
|
|
1639
|
+
scopeType,
|
|
1640
|
+
input: content,
|
|
1641
|
+
reason: 'NOT_RESOLVED',
|
|
1642
|
+
resolution: `名称「${content}(${scopeType})」未能解析为 ID,请核对名称,或改用 {"optionId":"候选id","content":"候选名称","type":"${scopeType}"} 后重新 prepare。`,
|
|
1643
|
+
});
|
|
1644
|
+
}
|
|
1645
|
+
detail.dataOptions = options;
|
|
1646
|
+
return [detail, options.map((o) => ({ id: o.optionId, name: o.content, type: o.type }))];
|
|
1647
|
+
}
|
|
1648
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'einvoicecomponent') {
|
|
1649
|
+
const options = [];
|
|
1650
|
+
for (const item of listValues(value)) {
|
|
1651
|
+
if (!item || typeof item !== 'object') {
|
|
1652
|
+
throw new ValueAmbiguity({ kind: 'EINVOICE', fieldId: field.id, fieldName: field.name, input: item, reason: '发票字段的值必须是发票对象数组', resolution: '每项传发票对象(含 id/fid)' });
|
|
1653
|
+
}
|
|
1654
|
+
let optionObj;
|
|
1655
|
+
let optionId;
|
|
1656
|
+
if (item.optionObj && typeof item.optionObj === 'object') {
|
|
1657
|
+
optionObj = item.optionObj;
|
|
1658
|
+
optionId = String(item.optionId ?? optionObj.optionId ?? optionObj.id ?? optionObj.fid ?? '').trim();
|
|
1659
|
+
}
|
|
1660
|
+
else {
|
|
1661
|
+
optionObj = item;
|
|
1662
|
+
optionId = String(item.optionId ?? item.id ?? item.fid ?? '').trim();
|
|
1663
|
+
}
|
|
1664
|
+
if (!optionId) {
|
|
1665
|
+
throw new ValueAmbiguity({ kind: 'EINVOICE', fieldId: field.id, fieldName: field.name, input: item, reason: '发票对象缺少 optionId/id/fid', resolution: '传入完整发票对象(含 id 或 fid)' });
|
|
1666
|
+
}
|
|
1667
|
+
options.push({ optionId, type: 'einvoice', optionObj });
|
|
1668
|
+
}
|
|
1669
|
+
assertMultiplicity(field, options);
|
|
1670
|
+
detail.dataOptions = options;
|
|
1671
|
+
return [detail, options.map((o) => ({ id: o.optionId, name: String((o.optionObj && (o.optionObj.name || o.optionObj.content)) || o.optionId) }))];
|
|
1672
|
+
}
|
|
1673
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'filecomponent') {
|
|
1674
|
+
const options = [];
|
|
1675
|
+
for (const item of listValues(value)) {
|
|
1676
|
+
if (!item || typeof item !== 'object') {
|
|
1677
|
+
throw new ValueAmbiguity({ kind: 'ATTACHMENT', fieldId: field.id, fieldName: field.name, input: item, reason: '附件字段的值必须是附件项数组', resolution: '每项固定格式 {"optionId":"上传返回的file_id","content":"文件名","type":"file"}' });
|
|
1678
|
+
}
|
|
1679
|
+
const optionId = String(item.optionId ?? '').trim();
|
|
1680
|
+
const content = String(item.content ?? '').trim();
|
|
1681
|
+
const ftype = String(item.type ?? '').trim();
|
|
1682
|
+
if (!optionId || !content || ftype !== 'file') {
|
|
1683
|
+
throw new ValueAmbiguity({ kind: 'ATTACHMENT', fieldId: field.id, fieldName: field.name, input: item, reason: '附件项缺少 optionId/content,或 type 不是 file', resolution: '固定格式 {"optionId":"上传返回的file_id","content":"文件名","type":"file"}' });
|
|
1684
|
+
}
|
|
1685
|
+
options.push({ optionId, content, type: 'file' });
|
|
1686
|
+
}
|
|
1687
|
+
assertMultiplicity(field, options);
|
|
1688
|
+
detail.dataOptions = options;
|
|
1689
|
+
return [detail, options.map((o) => ({ id: o.optionId, name: o.content }))];
|
|
1690
|
+
}
|
|
1691
|
+
if (field.isBrowser) {
|
|
1692
|
+
const options = [];
|
|
1693
|
+
for (const item of listValues(value)) {
|
|
1694
|
+
if (item && typeof item === 'object' && item.id) {
|
|
1695
|
+
options.push({ optionId: String(item.id), content: String(item.name ?? item.content ?? item.id) });
|
|
1696
|
+
continue;
|
|
1697
|
+
}
|
|
1698
|
+
const keyword = String(item).trim();
|
|
1699
|
+
const rowIndex = String((row && row.dataIndex) ?? '0');
|
|
1700
|
+
const lookupKey = `${field.id}_${rowIndex}_${keyword}`;
|
|
1701
|
+
const candidates = mappings[lookupKey] || [];
|
|
1702
|
+
const exact = candidates.filter((c) => c.name.toLowerCase() === keyword.toLowerCase());
|
|
1703
|
+
const selected = exact.length === 1 ? exact : candidates;
|
|
1704
|
+
if (selected.length !== 1) {
|
|
1705
|
+
// 未命中时带上候选澄清清单(该浏览控件首页合法取值);一个候选都没有时如实给空数组,
|
|
1706
|
+
// 由文档要求模型"一次明确告知用户并请其给出准确名称",禁止换相近词反复 prepare。
|
|
1707
|
+
const hints = selected.length ? selected : (browserHints[lookupKey] || []);
|
|
1708
|
+
throw new ValueAmbiguity({
|
|
1709
|
+
kind: 'BROWSER',
|
|
1710
|
+
fieldId: field.id,
|
|
1711
|
+
fieldName: field.name,
|
|
1712
|
+
input: keyword,
|
|
1713
|
+
reason: selected.length ? 'MULTIPLE_MATCHES' : 'NOT_FOUND',
|
|
1714
|
+
candidates: hints,
|
|
1715
|
+
candidatesAvailable: hints.length > 0,
|
|
1716
|
+
resolution: hints.length
|
|
1717
|
+
? '从 candidates 里挑出用户确认的那个,按 {"id":"候选id","name":"候选名称"} 重新 prepare。'
|
|
1718
|
+
: '将该值改为 {"id":"候选id","name":"候选名称"} 后重新 prepare。',
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1721
|
+
options.push({ optionId: selected[0].id, content: selected[0].name });
|
|
1722
|
+
}
|
|
1723
|
+
assertMultiplicity(field, options);
|
|
1724
|
+
detail.dataOptions = options;
|
|
1725
|
+
return [detail, options.map((o) => ({ id: o.optionId, name: o.content }))];
|
|
1726
|
+
}
|
|
1727
|
+
if (field.isSelect) {
|
|
1728
|
+
const selectedOptions = [];
|
|
1729
|
+
const choices = field.options || [];
|
|
1730
|
+
for (const item of listValues(value)) {
|
|
1731
|
+
if (item && typeof item === 'object' && item.id) {
|
|
1732
|
+
selectedOptions.push({ optionId: String(item.id), content: String(item.name ?? item.content ?? item.id) });
|
|
1733
|
+
continue;
|
|
1734
|
+
}
|
|
1735
|
+
const keyword = String(item).trim();
|
|
1736
|
+
const matches = choices.filter((c) => String(c.id) === keyword || String(c.name ?? '').toLowerCase() === keyword.toLowerCase());
|
|
1737
|
+
if (matches.length !== 1) {
|
|
1738
|
+
throw new ValueAmbiguity({ kind: 'OPTION', fieldId: field.id, fieldName: field.name, input: keyword, reason: matches.length ? 'MULTIPLE_MATCHES' : 'NOT_FOUND', candidates: matches || choices, resolution: '将该值改为 {"id":"选项id","name":"选项名称"} 后重新 prepare。' });
|
|
1739
|
+
}
|
|
1740
|
+
selectedOptions.push({ optionId: matches[0].id, content: matches[0].name });
|
|
1741
|
+
}
|
|
1742
|
+
assertMultiplicity(field, selectedOptions);
|
|
1743
|
+
detail.dataOptions = selectedOptions;
|
|
1744
|
+
return [detail, selectedOptions.map((o) => ({ id: o.optionId, name: o.content }))];
|
|
1745
|
+
}
|
|
1746
|
+
const component = String(field.componentKey ?? '').toLowerCase();
|
|
1747
|
+
if (value && typeof value === 'object') {
|
|
1748
|
+
throw new ValueAmbiguity({ kind: 'VALUE_FORMAT', fieldId: field.id, fieldName: field.name, reason: '文本字段只接受字符串、数字或布尔值', candidates: [] });
|
|
1749
|
+
}
|
|
1750
|
+
const content = value === null || value === undefined ? '' : String(value);
|
|
1751
|
+
const dateFormat = String(field.format ?? '');
|
|
1752
|
+
const needsTime = dateFormat && /(HH|hh)[::.]?mm|H:mm|h:mm/i.test(dateFormat);
|
|
1753
|
+
if (needsTime && /^\d{4}-\d{1,2}-\d{1,2}$/.test(content)) {
|
|
1754
|
+
throw new ValueAmbiguity({ kind: 'DATETIME_FORMAT', fieldId: field.id, fieldName: field.name, input: content, reason: `该字段是日期+时间控件(format=${dateFormat}),只传日期不完整;请按 format 传完整值(如 2026-08-21 09:00)后重新 prepare,不要省略时间。`, candidates: [] });
|
|
1755
|
+
}
|
|
1756
|
+
const maxLength = field.maxLength;
|
|
1757
|
+
const richUnlimited = component === 'textarea' && Boolean(field.richEditor);
|
|
1758
|
+
if (!richUnlimited && maxLength !== undefined && maxLength !== null && String(maxLength).match(/^\d+$/) && content.length > Number(maxLength)) {
|
|
1759
|
+
throw new ValueAmbiguity({ kind: 'VALUE_TOO_LONG', fieldId: field.id, fieldName: field.name, reason: `长度超过 ${maxLength}`, candidates: [] });
|
|
1760
|
+
}
|
|
1761
|
+
if (TEXTAREA_COMPONENTS.has(component))
|
|
1762
|
+
detail.dataText = { content };
|
|
1763
|
+
else
|
|
1764
|
+
detail.content = content;
|
|
1765
|
+
return [detail, content];
|
|
1766
|
+
}
|
|
1767
|
+
/**
|
|
1768
|
+
* 人员范围字段的名称→ID 预解析:把 `{"name":"刘艳","type":"user"}` 就地换成
|
|
1769
|
+
* `{"optionId":"<解析出的ID>","content":"刘艳","type":"user","optionObj":<接口原始对象>}`,
|
|
1770
|
+
* 交给 `serializeValue` 的"已解析"分支直接透传。解析不到(0 命中/多命中)时把歧义挂到该条目上,
|
|
1771
|
+
* 由主循环原样报给模型(带候选),**不允许退化成一个空 optionId 被静默丢掉**。
|
|
1772
|
+
*/
|
|
1773
|
+
async function preResolveEmployeeScopeEntries(env, entries) {
|
|
1774
|
+
const failures = new Map();
|
|
1775
|
+
for (const entry of entries) {
|
|
1776
|
+
const field = entry.field;
|
|
1777
|
+
if (!isEmployeeScopeField(field))
|
|
1778
|
+
continue;
|
|
1779
|
+
const items = listValues(entry.value);
|
|
1780
|
+
const resolved = [];
|
|
1781
|
+
let failure = null;
|
|
1782
|
+
for (const item of items) {
|
|
1783
|
+
if (!item || typeof item !== 'object') {
|
|
1784
|
+
resolved.push(item); // 非对象由 serializeValue 报"必须是对象数组"
|
|
1785
|
+
continue;
|
|
1786
|
+
}
|
|
1787
|
+
const optionId = String(item.optionId ?? item.id ?? '').trim();
|
|
1788
|
+
const content = String(item.content ?? item.name ?? '').trim();
|
|
1789
|
+
const scopeType = String(item.type ?? '').trim();
|
|
1790
|
+
// 已解析 / 缺名称或类型:原样回填,交回 serializeValue 处理(保持单一报错出口)
|
|
1791
|
+
if (optionId || !content || !scopeType) {
|
|
1792
|
+
resolved.push(item);
|
|
1793
|
+
continue;
|
|
1794
|
+
}
|
|
1795
|
+
const candidates = await employeeScopeCandidates(env, scopeType, content);
|
|
1796
|
+
const exact = candidates.filter((c) => String(c.name).toLowerCase() === content.toLowerCase());
|
|
1797
|
+
const selected = exact.length === 1 ? exact : candidates;
|
|
1798
|
+
if (selected.length !== 1) {
|
|
1799
|
+
failure = {
|
|
1800
|
+
kind: 'EMPLOYEE_SCOPE',
|
|
1801
|
+
fieldId: field.id,
|
|
1802
|
+
fieldName: field.name,
|
|
1803
|
+
...(entry.row ? { dataIndex: entry.row.dataIndex } : {}),
|
|
1804
|
+
scopeType,
|
|
1805
|
+
input: content,
|
|
1806
|
+
reason: selected.length ? 'MULTIPLE_MATCHES' : 'NOT_FOUND',
|
|
1807
|
+
candidates: selected.map((c) => ({ id: c.id, name: c.name })),
|
|
1808
|
+
resolution: `将该值改为 {"optionId":"候选id","content":"候选名称","type":"${scopeType}"} 后重新 prepare。`,
|
|
1809
|
+
};
|
|
1810
|
+
break;
|
|
1811
|
+
}
|
|
1812
|
+
const chosen = selected[0];
|
|
1813
|
+
resolved.push({
|
|
1814
|
+
optionId: String(chosen.id),
|
|
1815
|
+
content: String(chosen.name),
|
|
1816
|
+
type: scopeType,
|
|
1817
|
+
...(chosen.raw && typeof chosen.raw === 'object' ? { optionObj: chosen.raw } : {}),
|
|
1818
|
+
});
|
|
1819
|
+
}
|
|
1820
|
+
if (failure) {
|
|
1821
|
+
failures.set(entry, failure);
|
|
1822
|
+
continue;
|
|
1823
|
+
}
|
|
1824
|
+
entry.value = Array.isArray(entry.value) ? resolved : resolved[0];
|
|
1825
|
+
}
|
|
1826
|
+
return failures;
|
|
1827
|
+
}
|
|
1828
|
+
/**
|
|
1829
|
+
* 浏览按钮字段「名称→ID 未命中」时的候选澄清(V12.1.22 定稿)。
|
|
1830
|
+
*
|
|
1831
|
+
* 名称→ID 走的是 `nameToId`(`/api/ai/chat/fieldMapping/getRelateData`),它**一个关键词只回一个
|
|
1832
|
+
* "最佳匹配"**,未命中时给不出任何候选 —— 于是 `ambiguities[].candidates` 只能是空数组,模型只能
|
|
1833
|
+
* 自己换相近词瞎猜(2026-09-16「费用类型=手机话费」实证:模型猜了"手机费",最后干脆把字段删了)。
|
|
1834
|
+
* 同文件里 **select 分支** `candidates: matches || choices` 是会回退给出**该字段全部可选项**的,
|
|
1835
|
+
* 浏览按钮这一支缺了这个兜底。
|
|
1836
|
+
*
|
|
1837
|
+
* 这里改用字段自带的浏览接口(与人员范围同一套协议)
|
|
1838
|
+
* `POST /api/{browserModule}/common/browser/complete/{browserType}`:
|
|
1839
|
+
* ① 先按关键词查一次(该接口与 nameToId 不是同一实现,可能命中 nameToId 命不中的值);
|
|
1840
|
+
* ② 仍为空 → **再用空关键词**查一次,把该浏览控件首页的合法取值当候选返回,让模型/用户有据可依。
|
|
1841
|
+
* 只对**真的没命中**的关键词发请求,命中路径零额外开销。
|
|
1842
|
+
*/
|
|
1843
|
+
async function browserFieldCandidates(env, field, keyword) {
|
|
1844
|
+
const module = String(field?.browserModule ?? '').trim();
|
|
1845
|
+
const btype = String(field?.browserType ?? '').trim();
|
|
1846
|
+
if (!module || !btype)
|
|
1847
|
+
return [];
|
|
1848
|
+
const fetchOnce = async (searchValue) => {
|
|
1849
|
+
let response;
|
|
1850
|
+
try {
|
|
1851
|
+
response = await env.host.workflowRequest({
|
|
1852
|
+
method: 'POST',
|
|
1853
|
+
path: `/api/${module}/common/browser/complete/${btype}`,
|
|
1854
|
+
operation: 'name.resolve',
|
|
1855
|
+
body: { browserMultiple: true, newly: 1, searchValue },
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
catch (error) {
|
|
1859
|
+
// 与人员范围同口径:认证/网络错误原样抛出,其余(含接口不支持空关键词)按 0 命中处理。
|
|
1860
|
+
rethrowAuthOrNetwork(error);
|
|
1861
|
+
return [];
|
|
1862
|
+
}
|
|
1863
|
+
const data = responseData(response);
|
|
1864
|
+
const list = data?.data?.data ?? data?.data;
|
|
1865
|
+
if (!Array.isArray(list))
|
|
1866
|
+
return [];
|
|
1867
|
+
const out = [];
|
|
1868
|
+
const seen = new Set();
|
|
1869
|
+
for (const item of list) {
|
|
1870
|
+
if (!item || typeof item !== 'object')
|
|
1871
|
+
continue;
|
|
1872
|
+
const id = String(item.id ?? '').trim();
|
|
1873
|
+
if (!id || seen.has(id))
|
|
1874
|
+
continue;
|
|
1875
|
+
seen.add(id);
|
|
1876
|
+
const name = String(item.name ?? item.content ?? '').trim();
|
|
1877
|
+
if (!name)
|
|
1878
|
+
continue;
|
|
1879
|
+
out.push({ id, name });
|
|
1880
|
+
if (out.length >= 20)
|
|
1881
|
+
break; // 候选只用于澄清,截断防输出膨胀
|
|
1882
|
+
}
|
|
1883
|
+
return out;
|
|
1884
|
+
};
|
|
1885
|
+
const kw = String(keyword ?? '').trim();
|
|
1886
|
+
const byKeyword = kw ? await fetchOnce(kw) : [];
|
|
1887
|
+
if (byKeyword.length)
|
|
1888
|
+
return byKeyword;
|
|
1889
|
+
// 关键词查不到 → 取该浏览控件的合法取值清单(空关键词枚举首页)
|
|
1890
|
+
const all = await fetchOnce('');
|
|
1891
|
+
return all.filter((c) => c.name.toLowerCase() !== kw.toLowerCase());
|
|
1892
|
+
}
|
|
1893
|
+
async function resolveEntryValues(env, entries, formId) {
|
|
1894
|
+
// 人员范围先做名称→ID(它不走 nameToId,也不该被当成普通浏览按钮字段)
|
|
1895
|
+
const employeeScopeFailures = await preResolveEmployeeScopeEntries(env, entries);
|
|
1896
|
+
const fieldDatas = [];
|
|
1897
|
+
const seenLookups = new Set();
|
|
1898
|
+
// 待查关键词 → {字段, 原始关键词}:名称→ID 未命中时据此补一次候选澄清(见 browserFieldCandidates)
|
|
1899
|
+
const lookupSpecs = new Map();
|
|
1900
|
+
for (const entry of entries) {
|
|
1901
|
+
const field = entry.field;
|
|
1902
|
+
if (!field.isBrowser)
|
|
1903
|
+
continue;
|
|
1904
|
+
// 人员范围已在上一步解析完,不能再塞进 nameToId 的 fieldDatas(否则会把对象字面量当关键词送出去)
|
|
1905
|
+
if (isEmployeeScopeField(field))
|
|
1906
|
+
continue;
|
|
1907
|
+
const row = (entry.row && typeof entry.row === 'object') ? entry.row : null;
|
|
1908
|
+
const rowIndex = String((row && row.dataIndex) ?? '0');
|
|
1909
|
+
const isDetail = Boolean(row);
|
|
1910
|
+
for (const item of listValues(entry.value)) {
|
|
1911
|
+
if (item && typeof item === 'object' && (item.id || item.optionId))
|
|
1912
|
+
continue;
|
|
1913
|
+
const keyword = String(item).trim();
|
|
1914
|
+
if (!keyword)
|
|
1915
|
+
continue;
|
|
1916
|
+
const lookupKey = `${field.id}_${rowIndex}_${keyword}`;
|
|
1917
|
+
if (seenLookups.has(lookupKey))
|
|
1918
|
+
continue;
|
|
1919
|
+
seenLookups.add(lookupKey);
|
|
1920
|
+
lookupSpecs.set(lookupKey, { field, keyword });
|
|
1921
|
+
fieldDatas.push({
|
|
1922
|
+
fieldId: String(field.id),
|
|
1923
|
+
componentKey: field.componentKey || '',
|
|
1924
|
+
fieldValue: keyword,
|
|
1925
|
+
rowIndex,
|
|
1926
|
+
operateType: 'add',
|
|
1927
|
+
isDetail,
|
|
1928
|
+
subFormId: String(field.subFormId || '0'),
|
|
1929
|
+
browserModule: field.browserModule || '',
|
|
1930
|
+
browserType: field.browserType || '',
|
|
1931
|
+
});
|
|
1932
|
+
}
|
|
1933
|
+
}
|
|
1934
|
+
const mappings = {};
|
|
1935
|
+
if (fieldDatas.length) {
|
|
1936
|
+
const response = await requestJson(env, 'POST', FORM_ENDPOINTS.nameToId, AJAX_QUERY, { formId: String(formId), fieldDatas });
|
|
1937
|
+
const relateData = (response && typeof response === 'object') ? response.relateData : null;
|
|
1938
|
+
if (!relateData || typeof relateData !== 'object')
|
|
1939
|
+
throw workflowApiError('format_error', 'getRelateData 未返回 data.relateData 对象');
|
|
1940
|
+
for (const resultKey of Object.keys(relateData)) {
|
|
1941
|
+
if (!resultKey.includes('_'))
|
|
1942
|
+
continue;
|
|
1943
|
+
const keywordMapping = relateData[resultKey];
|
|
1944
|
+
if (!keywordMapping || typeof keywordMapping !== 'object')
|
|
1945
|
+
continue;
|
|
1946
|
+
const idx = resultKey.lastIndexOf('_');
|
|
1947
|
+
const fieldId = resultKey.slice(0, idx);
|
|
1948
|
+
const rowIndex = resultKey.slice(idx + 1);
|
|
1949
|
+
for (const keyword of Object.keys(keywordMapping)) {
|
|
1950
|
+
const normalized = normalizeRelateCandidate(keywordMapping[keyword]);
|
|
1951
|
+
mappings[`${fieldId}_${rowIndex}_${keyword}`] = normalized ? [normalized] : [];
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
// 名称→ID 未命中的浏览按钮:补一次候选澄清(只对真的没命中的关键词发请求)。
|
|
1956
|
+
// 有了候选清单,模型的 `ambiguities[].resolution` 才有据可依(把合法取值列给用户选),
|
|
1957
|
+
// 而不是拿到空数组后自己换相近词硬猜(见 browserFieldCandidates 的说明)。
|
|
1958
|
+
const browserHints = {};
|
|
1959
|
+
for (const [key, spec] of lookupSpecs) {
|
|
1960
|
+
if (mappings[key] && mappings[key].length)
|
|
1961
|
+
continue;
|
|
1962
|
+
if (!spec.field.browserModule || !spec.field.browserType)
|
|
1963
|
+
continue;
|
|
1964
|
+
browserHints[key] = await browserFieldCandidates(env, spec.field, spec.keyword);
|
|
1965
|
+
}
|
|
1966
|
+
const details = [];
|
|
1967
|
+
const ambiguities = [];
|
|
1968
|
+
const preview = [];
|
|
1969
|
+
for (const entry of entries) {
|
|
1970
|
+
const field = entry.field;
|
|
1971
|
+
const scopeFailure = employeeScopeFailures.get(entry);
|
|
1972
|
+
if (scopeFailure) {
|
|
1973
|
+
ambiguities.push(scopeFailure);
|
|
1974
|
+
continue;
|
|
1975
|
+
}
|
|
1976
|
+
let detail;
|
|
1977
|
+
let display;
|
|
1978
|
+
try {
|
|
1979
|
+
[detail, display] = serializeValue(field, entry.value, mappings, entry.row, browserHints);
|
|
1980
|
+
}
|
|
1981
|
+
catch (err) {
|
|
1982
|
+
if (err instanceof ValueAmbiguity) {
|
|
1983
|
+
ambiguities.push(err.details);
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
throw err;
|
|
1987
|
+
}
|
|
1988
|
+
if (detail && hasDetailValue(detail)) {
|
|
1989
|
+
details.push(detail);
|
|
1990
|
+
const pv = {
|
|
1991
|
+
fieldId: field.id,
|
|
1992
|
+
fieldName: field.name,
|
|
1993
|
+
subFormId: field.subFormId || '',
|
|
1994
|
+
value: display,
|
|
1995
|
+
};
|
|
1996
|
+
if (entry.row)
|
|
1997
|
+
pv.dataIndex = entry.row.dataIndex;
|
|
1998
|
+
preview.push(pv);
|
|
1999
|
+
if (detail && display !== entry.value)
|
|
2000
|
+
entry.value = display;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
return [details, ambiguities, preview];
|
|
2004
|
+
}
|
|
2005
|
+
// ---------------------------------------------------------------------------
|
|
2006
|
+
// 字段联动 / 显示属性联动 / 必填校验
|
|
2007
|
+
// ---------------------------------------------------------------------------
|
|
2008
|
+
function linkageContextFromPayload(payload) {
|
|
2009
|
+
return {
|
|
2010
|
+
layout: payload.layout,
|
|
2011
|
+
runtime: payload.runtime,
|
|
2012
|
+
frame: payload.frame,
|
|
2013
|
+
linkageActions: payload.linkageActions,
|
|
2014
|
+
};
|
|
2015
|
+
}
|
|
2016
|
+
function buildLinkageContext(base) {
|
|
2017
|
+
return {
|
|
2018
|
+
layout: base.layout,
|
|
2019
|
+
runtime: base.runtime,
|
|
2020
|
+
frame: base.frame,
|
|
2021
|
+
linkageActions: base.linkageActions,
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function _buildLinkageRunData(linkageActions, changedFieldIds, actionTypes) {
|
|
2025
|
+
const changed = new Set([...changedFieldIds].map((f) => String(f).trim()).filter(Boolean));
|
|
2026
|
+
const actionTypeSet = actionTypes ? new Set([...actionTypes].map((s) => String(s))) : null;
|
|
2027
|
+
const runData = [];
|
|
2028
|
+
if (!Array.isArray(linkageActions))
|
|
2029
|
+
return runData;
|
|
2030
|
+
for (const action of linkageActions) {
|
|
2031
|
+
if (!action || typeof action !== 'object')
|
|
2032
|
+
continue;
|
|
2033
|
+
const actionType = String(action.actionType ?? 'changeField');
|
|
2034
|
+
const targetField = String(action.targetField ?? '').trim();
|
|
2035
|
+
if (actionType === 'onblur')
|
|
2036
|
+
continue;
|
|
2037
|
+
if (actionType === 'changeField') {
|
|
2038
|
+
if (actionTypeSet !== null && !actionTypeSet.has('changeField'))
|
|
2039
|
+
continue;
|
|
2040
|
+
if (!targetField || !changed.has(targetField))
|
|
2041
|
+
continue;
|
|
2042
|
+
}
|
|
2043
|
+
else {
|
|
2044
|
+
if (actionTypeSet === null || !actionTypeSet.has(actionType))
|
|
2045
|
+
continue;
|
|
2046
|
+
}
|
|
2047
|
+
const ids = action.idList;
|
|
2048
|
+
if (!Array.isArray(ids) || !ids.length)
|
|
2049
|
+
continue;
|
|
2050
|
+
runData.push({ linkageIds: ids.map(String), actionType, targetField });
|
|
2051
|
+
}
|
|
2052
|
+
return runData;
|
|
2053
|
+
}
|
|
2054
|
+
function _entriesFromDefaults(defaults) {
|
|
2055
|
+
const entries = [];
|
|
2056
|
+
for (const detail of defaults || []) {
|
|
2057
|
+
if (!detail || typeof detail !== 'object')
|
|
2058
|
+
continue;
|
|
2059
|
+
const formField = (detail.formField && typeof detail.formField === 'object') ? detail.formField : {};
|
|
2060
|
+
const fieldId = String(formField.id ?? '').trim();
|
|
2061
|
+
if (!fieldId)
|
|
2062
|
+
continue;
|
|
2063
|
+
const options = detail.dataOptions;
|
|
2064
|
+
let value = null;
|
|
2065
|
+
if (Array.isArray(options) && options.length) {
|
|
2066
|
+
value = options.filter((o) => o && typeof o === 'object' && String(o.optionId ?? '').trim()).map((o) => ({ id: String(o.optionId ?? ''), name: String(o.content ?? o.optionId ?? '') }));
|
|
2067
|
+
}
|
|
2068
|
+
else if (detail.dataText && typeof detail.dataText === 'object') {
|
|
2069
|
+
const t = String(detail.dataText.content ?? '').trim();
|
|
2070
|
+
if (t)
|
|
2071
|
+
value = t;
|
|
2072
|
+
}
|
|
2073
|
+
else {
|
|
2074
|
+
const t = String(detail.content ?? '').trim();
|
|
2075
|
+
if (t)
|
|
2076
|
+
value = t;
|
|
2077
|
+
}
|
|
2078
|
+
if (value === null || (Array.isArray(value) && !value.length))
|
|
2079
|
+
continue;
|
|
2080
|
+
const entry = { field: { id: fieldId, componentKey: String(formField.componentKey ?? ''), isBrowser: Array.isArray(options) && Boolean(options.length) }, value };
|
|
2081
|
+
const subForm = (detail.subForm && typeof detail.subForm === 'object') ? detail.subForm : null;
|
|
2082
|
+
const subFormId = String(detail.subFormId ?? (subForm && subForm.id) ?? '').trim();
|
|
2083
|
+
if (subFormId || detail.dataIndex !== undefined || detail.rowId) {
|
|
2084
|
+
const row = { subForm: { id: subFormId || '' } };
|
|
2085
|
+
if (detail.dataIndex !== undefined)
|
|
2086
|
+
row.dataIndex = Number(detail.dataIndex);
|
|
2087
|
+
if (detail.rowId)
|
|
2088
|
+
row.rowId = String(detail.rowId);
|
|
2089
|
+
entry.row = row;
|
|
2090
|
+
}
|
|
2091
|
+
entries.push(entry);
|
|
2092
|
+
}
|
|
2093
|
+
return entries;
|
|
2094
|
+
}
|
|
2095
|
+
function _neededLinkageFieldIds(linkageActions, runDataList) {
|
|
2096
|
+
const needed = new Set();
|
|
2097
|
+
if (!Array.isArray(linkageActions) || !runDataList.length)
|
|
2098
|
+
return needed;
|
|
2099
|
+
for (const run of runDataList) {
|
|
2100
|
+
if (!run || typeof run !== 'object')
|
|
2101
|
+
continue;
|
|
2102
|
+
const runActionType = String(run.actionType ?? '');
|
|
2103
|
+
const runIds = new Set((run.linkageIds || []).map(String));
|
|
2104
|
+
const runTarget = String(run.targetField ?? '').trim();
|
|
2105
|
+
for (const action of linkageActions) {
|
|
2106
|
+
if (!action || typeof action !== 'object')
|
|
2107
|
+
continue;
|
|
2108
|
+
if (String(action.actionType ?? '') !== runActionType)
|
|
2109
|
+
continue;
|
|
2110
|
+
if (runTarget !== String(action.targetField ?? '').trim())
|
|
2111
|
+
continue;
|
|
2112
|
+
const actionIds = new Set((action.idList || []).map(String));
|
|
2113
|
+
if (runIds.size && [...runIds].every((x) => actionIds.has(x))) {
|
|
2114
|
+
const tf = String(action.targetField ?? '').trim();
|
|
2115
|
+
if (tf)
|
|
2116
|
+
needed.add(tf);
|
|
2117
|
+
for (const cf of action.conditionFieldList || []) {
|
|
2118
|
+
const c = String(cf ?? '').trim();
|
|
2119
|
+
if (c)
|
|
2120
|
+
needed.add(c);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
}
|
|
2124
|
+
}
|
|
2125
|
+
return needed;
|
|
2126
|
+
}
|
|
2127
|
+
function _entryValueText(value) {
|
|
2128
|
+
for (const item of listValues(value)) {
|
|
2129
|
+
if (item && typeof item === 'object') {
|
|
2130
|
+
for (const key of ['id', 'optionId']) {
|
|
2131
|
+
if (item[key] !== undefined && item[key] !== null && String(item[key]).trim())
|
|
2132
|
+
return String(item[key]).trim();
|
|
2133
|
+
}
|
|
2134
|
+
for (const key of ['name', 'content']) {
|
|
2135
|
+
if (item[key] !== undefined && item[key] !== null && String(item[key]).trim())
|
|
2136
|
+
return String(item[key]).trim();
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
else {
|
|
2140
|
+
const t = String(item).trim();
|
|
2141
|
+
if (t)
|
|
2142
|
+
return t;
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
return '';
|
|
2146
|
+
}
|
|
2147
|
+
function _entryValueIds(value) {
|
|
2148
|
+
const ids = [];
|
|
2149
|
+
for (const item of listValues(value)) {
|
|
2150
|
+
if (item && typeof item === 'object') {
|
|
2151
|
+
for (const key of ['id', 'optionId', 'value']) {
|
|
2152
|
+
if (item[key] !== undefined && item[key] !== null && String(item[key]).trim()) {
|
|
2153
|
+
ids.push(String(item[key]).trim());
|
|
2154
|
+
break;
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
}
|
|
2158
|
+
else {
|
|
2159
|
+
const t = String(item).trim();
|
|
2160
|
+
if (t)
|
|
2161
|
+
ids.push(t);
|
|
2162
|
+
}
|
|
2163
|
+
}
|
|
2164
|
+
return ids;
|
|
2165
|
+
}
|
|
2166
|
+
function _detailValueIds(detail) {
|
|
2167
|
+
const ids = [];
|
|
2168
|
+
if (detail.content !== undefined && detail.content !== null) {
|
|
2169
|
+
const t = String(detail.content).trim();
|
|
2170
|
+
if (t)
|
|
2171
|
+
ids.push(t);
|
|
2172
|
+
}
|
|
2173
|
+
if (detail.dataText && typeof detail.dataText === 'object' && detail.dataText.content !== undefined && detail.dataText.content !== null) {
|
|
2174
|
+
const t = String(detail.dataText.content).trim();
|
|
2175
|
+
if (t)
|
|
2176
|
+
ids.push(t);
|
|
2177
|
+
}
|
|
2178
|
+
for (const option of detail.dataOptions || []) {
|
|
2179
|
+
if (option && typeof option === 'object' && option.optionId !== undefined && option.optionId !== null) {
|
|
2180
|
+
const t = String(option.optionId).trim();
|
|
2181
|
+
if (t)
|
|
2182
|
+
ids.push(t);
|
|
2183
|
+
}
|
|
2184
|
+
}
|
|
2185
|
+
return ids;
|
|
2186
|
+
}
|
|
2187
|
+
function _linkageOptions(sourceContent) {
|
|
2188
|
+
if (!sourceContent || typeof sourceContent !== 'string' || !sourceContent.trim())
|
|
2189
|
+
return [];
|
|
2190
|
+
let data;
|
|
2191
|
+
try {
|
|
2192
|
+
data = JSON.parse(sourceContent);
|
|
2193
|
+
}
|
|
2194
|
+
catch {
|
|
2195
|
+
return [];
|
|
2196
|
+
}
|
|
2197
|
+
if (!data || typeof data !== 'object')
|
|
2198
|
+
return [];
|
|
2199
|
+
const result = [];
|
|
2200
|
+
for (const item of data.dataOptions || []) {
|
|
2201
|
+
if (item && typeof item === 'object') {
|
|
2202
|
+
const id = String(item.optionId ?? '').trim();
|
|
2203
|
+
const content = String(item.content ?? '').trim();
|
|
2204
|
+
if (id)
|
|
2205
|
+
result.push([id, content]);
|
|
2206
|
+
}
|
|
2207
|
+
}
|
|
2208
|
+
for (const item of data.optionValues || []) {
|
|
2209
|
+
if (item && typeof item === 'object') {
|
|
2210
|
+
const tv = String(item.textValue ?? '').trim();
|
|
2211
|
+
const name = String(item.name ?? '').trim();
|
|
2212
|
+
if (tv)
|
|
2213
|
+
result.push([tv, name]);
|
|
2214
|
+
}
|
|
2215
|
+
}
|
|
2216
|
+
return result;
|
|
2217
|
+
}
|
|
2218
|
+
/**
|
|
2219
|
+
* 联动条件里的"期望取值"(`sourceContent` 是 JSON 字符串)。
|
|
2220
|
+
*
|
|
2221
|
+
* 与 `_linkageOptions`(只服务条件命中判定,取 id/name 两元组)分开:本函数保留 **id + content**,
|
|
2222
|
+
* 并在没有任何选项列表时回退到 `dataOptions`/`content` 标量(如 `{"formField":{"id":"…"},"content":"1"}`),
|
|
2223
|
+
* 供 `conditionalRequired` 原样展示条件取值用。
|
|
2224
|
+
*/
|
|
2225
|
+
function _linkageConditionValues(sourceContent) {
|
|
2226
|
+
if (!sourceContent || typeof sourceContent !== 'string' || !sourceContent.trim())
|
|
2227
|
+
return [];
|
|
2228
|
+
let data;
|
|
2229
|
+
try {
|
|
2230
|
+
data = JSON.parse(sourceContent);
|
|
2231
|
+
}
|
|
2232
|
+
catch {
|
|
2233
|
+
return [];
|
|
2234
|
+
}
|
|
2235
|
+
if (!data || typeof data !== 'object')
|
|
2236
|
+
return [];
|
|
2237
|
+
const result = [];
|
|
2238
|
+
for (const item of data.dataOptions || []) {
|
|
2239
|
+
if (!item || typeof item !== 'object')
|
|
2240
|
+
continue;
|
|
2241
|
+
const id = String(item.optionId ?? '').trim();
|
|
2242
|
+
const content = String(item.content ?? '').trim();
|
|
2243
|
+
if (id)
|
|
2244
|
+
result.push(content ? { id, content } : { id });
|
|
2245
|
+
else if (content)
|
|
2246
|
+
result.push({ content });
|
|
2247
|
+
}
|
|
2248
|
+
for (const item of data.optionValues || []) {
|
|
2249
|
+
if (!item || typeof item !== 'object')
|
|
2250
|
+
continue;
|
|
2251
|
+
const id = String(item.textValue ?? '').trim();
|
|
2252
|
+
const content = String(item.name ?? '').trim();
|
|
2253
|
+
if (id)
|
|
2254
|
+
result.push(content ? { id, content } : { id });
|
|
2255
|
+
else if (content)
|
|
2256
|
+
result.push({ content });
|
|
2257
|
+
}
|
|
2258
|
+
if (!result.length && data.content !== undefined && data.content !== null) {
|
|
2259
|
+
const content = String(data.content).trim();
|
|
2260
|
+
if (content)
|
|
2261
|
+
result.push({ content });
|
|
2262
|
+
}
|
|
2263
|
+
return result;
|
|
2264
|
+
}
|
|
2265
|
+
/**
|
|
2266
|
+
* 「条件必填」清单(V12.1.27 新增能力)。
|
|
2267
|
+
*
|
|
2268
|
+
* 背景:本租户 OA 的 `getFormLayout` 不下发静态必填标记(`viewRelateConfig` 为空、布局组件 `required`
|
|
2269
|
+
* 全为 false),业务必填**只以「显示属性联动」形式存在**——`viewLinkages[]` 中 `linkageProp='required'`
|
|
2270
|
+
* 的规则:条件命中时该字段才变必填。
|
|
2271
|
+
*
|
|
2272
|
+
* 这些规则原先只在 prepare 阶段按"实际所填数据"参与必填计算(命中才出现在 `missingFields`),
|
|
2273
|
+
* form 阶段模型完全看不到 ⇒ 模型无法在发起前主动向用户问齐必填项,只能等用户恰巧填对触发字段
|
|
2274
|
+
* (2026-09-17 分享会话实证:模型只能说"拿不到必填信息",用户此前也反复反馈"没有必填字段")。
|
|
2275
|
+
*
|
|
2276
|
+
* 本函数把规则**按 OA 原样结构化输出**(不评价、不做语义改写、不做条件求值):`match` 取
|
|
2277
|
+
* `condition.detailType`(AND/OR,缺省 AND),`term` 取 `detailSymbol` 原值
|
|
2278
|
+
* (EQUAL/UN_EQUAL/IN/NOT_IN/NULL/NOT_NULL),`values` 为该条件的期望取值。
|
|
2279
|
+
* **条件是否命中仍以 prepare 的 `missing_fields` 为准**。
|
|
2280
|
+
* 无 required 规则时返回空数组(调用方按需省略该键)。
|
|
2281
|
+
*/
|
|
2282
|
+
function conditionalRequiredFields(linkages, rawFieldsById) {
|
|
2283
|
+
if (!Array.isArray(linkages) || !linkages.length)
|
|
2284
|
+
return [];
|
|
2285
|
+
const fieldMeta = (fieldId) => {
|
|
2286
|
+
const raw = rawFieldsById[fieldId];
|
|
2287
|
+
if (!raw || typeof raw !== 'object')
|
|
2288
|
+
return { name: '', subFormId: '' };
|
|
2289
|
+
return {
|
|
2290
|
+
name: String(raw.title ?? raw.name ?? raw.dataKey ?? '').trim(),
|
|
2291
|
+
subFormId: String(raw.subFormId ?? '').trim(),
|
|
2292
|
+
};
|
|
2293
|
+
};
|
|
2294
|
+
const result = [];
|
|
2295
|
+
const seen = new Set();
|
|
2296
|
+
for (const linkage of linkages) {
|
|
2297
|
+
if (!linkage || typeof linkage !== 'object')
|
|
2298
|
+
continue;
|
|
2299
|
+
const condition = (linkage.condition && typeof linkage.condition === 'object') ? linkage.condition : null;
|
|
2300
|
+
if (!condition)
|
|
2301
|
+
continue;
|
|
2302
|
+
const conditionDetails = Array.isArray(condition.conditionDetails) ? condition.conditionDetails : [];
|
|
2303
|
+
const when = [];
|
|
2304
|
+
for (const cd of conditionDetails) {
|
|
2305
|
+
if (!cd || typeof cd !== 'object')
|
|
2306
|
+
continue;
|
|
2307
|
+
const conditionFieldId = String(cd.targetId ?? '').trim();
|
|
2308
|
+
if (!conditionFieldId)
|
|
2309
|
+
continue;
|
|
2310
|
+
const item = { fieldId: conditionFieldId };
|
|
2311
|
+
const meta = fieldMeta(conditionFieldId);
|
|
2312
|
+
if (meta.name)
|
|
2313
|
+
item.fieldName = meta.name;
|
|
2314
|
+
item.term = String(cd.detailSymbol ?? 'EQUAL').trim().toUpperCase();
|
|
2315
|
+
const sourceType = String(cd.sourceType ?? 'CONSTANT').trim().toUpperCase();
|
|
2316
|
+
if (sourceType !== 'CONSTANT') {
|
|
2317
|
+
item.sourceType = sourceType;
|
|
2318
|
+
const sourceFieldId = String(cd.sourceId ?? '').trim();
|
|
2319
|
+
if (sourceFieldId)
|
|
2320
|
+
item.sourceFieldId = sourceFieldId;
|
|
2321
|
+
}
|
|
2322
|
+
const values = _linkageConditionValues(cd.sourceContent);
|
|
2323
|
+
if (values.length)
|
|
2324
|
+
item.values = values;
|
|
2325
|
+
when.push(item);
|
|
2326
|
+
}
|
|
2327
|
+
if (!when.length)
|
|
2328
|
+
continue;
|
|
2329
|
+
const match = String(condition.detailType ?? 'AND').trim().toUpperCase() || 'AND';
|
|
2330
|
+
const details = Array.isArray(linkage.linkageDetails) ? linkage.linkageDetails : [];
|
|
2331
|
+
for (const ld of details) {
|
|
2332
|
+
if (!ld || typeof ld !== 'object')
|
|
2333
|
+
continue;
|
|
2334
|
+
if (String(ld.linkageProp ?? '').trim().toLowerCase() !== 'required')
|
|
2335
|
+
continue;
|
|
2336
|
+
for (const target of String(ld.linkageValue ?? '').split(',')) {
|
|
2337
|
+
const fieldId = target.trim();
|
|
2338
|
+
if (!fieldId)
|
|
2339
|
+
continue;
|
|
2340
|
+
const dedupeKey = [fieldId, match, JSON.stringify(when)].join('\u0000');
|
|
2341
|
+
if (seen.has(dedupeKey))
|
|
2342
|
+
continue;
|
|
2343
|
+
seen.add(dedupeKey);
|
|
2344
|
+
const meta = fieldMeta(fieldId);
|
|
2345
|
+
const item = { fieldId };
|
|
2346
|
+
if (meta.name)
|
|
2347
|
+
item.fieldName = meta.name;
|
|
2348
|
+
if (meta.subFormId)
|
|
2349
|
+
item.subFormId = meta.subFormId;
|
|
2350
|
+
item.match = match;
|
|
2351
|
+
item.when = when;
|
|
2352
|
+
result.push(item);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
}
|
|
2356
|
+
return result;
|
|
2357
|
+
}
|
|
2358
|
+
function _normalizeSystemFieldValues(businessOperatorObj) {
|
|
2359
|
+
const result = {};
|
|
2360
|
+
if (!businessOperatorObj || typeof businessOperatorObj !== 'object')
|
|
2361
|
+
return result;
|
|
2362
|
+
for (const key of Object.keys(businessOperatorObj)) {
|
|
2363
|
+
const value = businessOperatorObj[key];
|
|
2364
|
+
const ids = systemFieldValueIds(value);
|
|
2365
|
+
if (ids.length)
|
|
2366
|
+
result[String(key).trim()] = ids;
|
|
2367
|
+
}
|
|
2368
|
+
return result;
|
|
2369
|
+
}
|
|
2370
|
+
function systemFieldValueIds(value) {
|
|
2371
|
+
const ids = [];
|
|
2372
|
+
if (value && typeof value === 'object') {
|
|
2373
|
+
const optionValues = value.optionValues;
|
|
2374
|
+
if (Array.isArray(optionValues)) {
|
|
2375
|
+
for (const item of optionValues) {
|
|
2376
|
+
if (item && typeof item === 'object') {
|
|
2377
|
+
for (const raw of [item.textValue, item.name]) {
|
|
2378
|
+
if (raw !== undefined && raw !== null && String(raw).trim())
|
|
2379
|
+
ids.push(String(raw).trim());
|
|
2380
|
+
}
|
|
2381
|
+
}
|
|
2382
|
+
else {
|
|
2383
|
+
const t = String(item).trim();
|
|
2384
|
+
if (t)
|
|
2385
|
+
ids.push(t);
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
}
|
|
2389
|
+
else {
|
|
2390
|
+
for (const key of ['textValue', 'content', 'value']) {
|
|
2391
|
+
if (value[key] !== undefined && value[key] !== null && String(value[key]).trim())
|
|
2392
|
+
ids.push(String(value[key]).trim());
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2396
|
+
else if (Array.isArray(value)) {
|
|
2397
|
+
for (const item of value)
|
|
2398
|
+
ids.push(...systemFieldValueIds(item));
|
|
2399
|
+
}
|
|
2400
|
+
else {
|
|
2401
|
+
const t = String(value).trim();
|
|
2402
|
+
if (t)
|
|
2403
|
+
ids.push(t);
|
|
2404
|
+
}
|
|
2405
|
+
return ids;
|
|
2406
|
+
}
|
|
2407
|
+
function rowKeyFor(subFormId, dataIndex) {
|
|
2408
|
+
const sub = String(subFormId ?? '').trim();
|
|
2409
|
+
if (!sub)
|
|
2410
|
+
return 'main';
|
|
2411
|
+
return `${sub}:${dataIndex ?? 1}`;
|
|
2412
|
+
}
|
|
2413
|
+
function _detailDisplayText(detail) {
|
|
2414
|
+
if (detail.content !== undefined && detail.content !== null && String(detail.content).trim())
|
|
2415
|
+
return String(detail.content).trim();
|
|
2416
|
+
if (detail.dataText && typeof detail.dataText === 'object' && detail.dataText.content !== undefined && detail.dataText.content !== null && String(detail.dataText.content).trim()) {
|
|
2417
|
+
return String(detail.dataText.content).trim();
|
|
2418
|
+
}
|
|
2419
|
+
const options = detail.dataOptions;
|
|
2420
|
+
if (Array.isArray(options) && options.length) {
|
|
2421
|
+
return options.map((o) => String(o.content ?? o.optionId ?? '')).filter(Boolean).join('、');
|
|
2422
|
+
}
|
|
2423
|
+
return '';
|
|
2424
|
+
}
|
|
2425
|
+
function _mainValueMap(details) {
|
|
2426
|
+
const result = {};
|
|
2427
|
+
for (const detail of details || []) {
|
|
2428
|
+
if (!detail || typeof detail !== 'object')
|
|
2429
|
+
continue;
|
|
2430
|
+
const subForm = (detail.subForm && typeof detail.subForm === 'object') ? detail.subForm : null;
|
|
2431
|
+
if (subForm && String(subForm.id ?? '').trim())
|
|
2432
|
+
continue;
|
|
2433
|
+
if (String(detail.subFormId ?? '').trim())
|
|
2434
|
+
continue;
|
|
2435
|
+
const fid = String((detail.formField && detail.formField.id) ?? '').trim();
|
|
2436
|
+
if (fid)
|
|
2437
|
+
result[fid] = detail;
|
|
2438
|
+
}
|
|
2439
|
+
return result;
|
|
2440
|
+
}
|
|
2441
|
+
async function runFieldLinkage(env, ctx, entries, actionTypes, allDetails, isEdit = false) {
|
|
2442
|
+
const changedIds = entries.filter((e) => e.field && typeof e.field === 'object' && String(e.field.id ?? '').trim())
|
|
2443
|
+
.map((e) => String(e.field.id));
|
|
2444
|
+
const runDataList = _buildLinkageRunData(ctx.linkageActions, changedIds, actionTypes);
|
|
2445
|
+
const neededIds = _neededLinkageFieldIds(ctx.linkageActions, runDataList);
|
|
2446
|
+
const dataList = [];
|
|
2447
|
+
const collectedIds = new Set();
|
|
2448
|
+
const pushItem = (field, value, row) => {
|
|
2449
|
+
const fieldId = String(field.id ?? '').trim();
|
|
2450
|
+
if (!fieldId || !neededIds.has(fieldId))
|
|
2451
|
+
return;
|
|
2452
|
+
const valueText = _entryValueText(value);
|
|
2453
|
+
const item = { fieldId, componentKey: String(field.componentKey ?? ''), content: valueText };
|
|
2454
|
+
if (row && typeof row === 'object') {
|
|
2455
|
+
const subFormId = String((row.subForm && row.subForm.id) ?? '').trim();
|
|
2456
|
+
if (subFormId)
|
|
2457
|
+
item.subFormId = subFormId;
|
|
2458
|
+
if (row.dataIndex !== undefined)
|
|
2459
|
+
item.dataIndex = Number(row.dataIndex);
|
|
2460
|
+
if (row.rowId)
|
|
2461
|
+
item.rowId = String(row.rowId);
|
|
2462
|
+
}
|
|
2463
|
+
const options = [];
|
|
2464
|
+
for (const vi of listValues(value)) {
|
|
2465
|
+
if (vi && typeof vi === 'object' && vi.id)
|
|
2466
|
+
options.push({ id: String(vi.id), name: String(vi.name ?? vi.content ?? vi.id) });
|
|
2467
|
+
}
|
|
2468
|
+
if (options.length)
|
|
2469
|
+
item.optionList = options;
|
|
2470
|
+
dataList.push(item);
|
|
2471
|
+
collectedIds.add(fieldId);
|
|
2472
|
+
};
|
|
2473
|
+
for (const entry of entries) {
|
|
2474
|
+
if (!entry.field || typeof entry.field !== 'object')
|
|
2475
|
+
continue;
|
|
2476
|
+
pushItem(entry.field, entry.value, entry.row);
|
|
2477
|
+
}
|
|
2478
|
+
if (allDetails) {
|
|
2479
|
+
for (const extra of _entriesFromDefaults(allDetails)) {
|
|
2480
|
+
const fld = extra.field;
|
|
2481
|
+
if (!fld || typeof fld !== 'object')
|
|
2482
|
+
continue;
|
|
2483
|
+
const eid = String(fld.id ?? '').trim();
|
|
2484
|
+
if (!eid || !neededIds.has(eid) || collectedIds.has(eid))
|
|
2485
|
+
continue;
|
|
2486
|
+
pushItem(fld, extra.value, extra.row);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (!dataList.length && !runDataList.length)
|
|
2490
|
+
return [];
|
|
2491
|
+
const executeParam = {
|
|
2492
|
+
formId: ctx.layout.form_id,
|
|
2493
|
+
module: 'workflow',
|
|
2494
|
+
dataList,
|
|
2495
|
+
runDataList,
|
|
2496
|
+
moduleDataList: [],
|
|
2497
|
+
};
|
|
2498
|
+
if (isEdit) {
|
|
2499
|
+
const runtime = ctx.runtime || {};
|
|
2500
|
+
const frame = ctx.frame || {};
|
|
2501
|
+
executeParam.customParam = {
|
|
2502
|
+
permissionType: Number(frame.permission_type ?? 1),
|
|
2503
|
+
requestId: ctx.requestId ?? runtime.request_id,
|
|
2504
|
+
workflowId: (ctx.workflow && ctx.workflow.id) ?? runtime.workflow_id,
|
|
2505
|
+
nodeId: runtime.node_id ?? '',
|
|
2506
|
+
dataSrc: 'NEW',
|
|
2507
|
+
authSignatureStr: runtime.auth_signature ?? '',
|
|
2508
|
+
authStr: runtime.auth_str ?? '',
|
|
2509
|
+
readOnly: false,
|
|
2510
|
+
identityId: runtime.identity_id ?? '',
|
|
2511
|
+
identityType: Number(runtime.identity_type ?? 0),
|
|
2512
|
+
isTest: false,
|
|
2513
|
+
create: false,
|
|
2514
|
+
mobile: false,
|
|
2515
|
+
formId: ctx.formId ?? ctx.layout.form_id,
|
|
2516
|
+
dataId: ctx.dataId ?? runtime.request_id,
|
|
2517
|
+
source: 'view',
|
|
2518
|
+
isAgent: false,
|
|
2519
|
+
beAgentId: '0',
|
|
2520
|
+
hideSysFieldArea: true,
|
|
2521
|
+
viewFormNodeId: frame.view_form_node_id ?? runtime.node_id ?? '',
|
|
2522
|
+
timestamp: Date.now(),
|
|
2523
|
+
turnTodoUser: [],
|
|
2524
|
+
sysFieldNewMode: true,
|
|
2525
|
+
isOverrideViewConfig: true,
|
|
2526
|
+
};
|
|
2527
|
+
executeParam.businessFields = ctx.businessFields || [];
|
|
2528
|
+
}
|
|
2529
|
+
else {
|
|
2530
|
+
const runtime = ctx.runtime || {};
|
|
2531
|
+
if (runtime.request_id)
|
|
2532
|
+
executeParam.refId = runtime.request_id;
|
|
2533
|
+
const frame = ctx.frame || {};
|
|
2534
|
+
if (frame.layout_multi_id)
|
|
2535
|
+
executeParam.layoutId = frame.layout_multi_id;
|
|
2536
|
+
}
|
|
2537
|
+
const payload = { formId: ctx.layout.form_id, module: 'workflow', executeParam };
|
|
2538
|
+
const relateType = (ctx.frame && ctx.frame.relate_type) || '';
|
|
2539
|
+
const endpoint = formApiUrl(FORM_ENDPOINTS.fdLinkage, relateType);
|
|
2540
|
+
const response = await requestJson(env, 'POST', endpoint, AJAX_QUERY, payload);
|
|
2541
|
+
// requestJson 内部已解包一层(responseData),此处不可再取 .data
|
|
2542
|
+
const data = (response && typeof response === 'object') ? response : null;
|
|
2543
|
+
const linkageList = (data && typeof data === 'object') ? data.linkageDataList : null;
|
|
2544
|
+
if (!Array.isArray(linkageList))
|
|
2545
|
+
return [];
|
|
2546
|
+
return linkageList.filter((i) => i && typeof i === 'object').map((i) => normalizeDetail(i));
|
|
2547
|
+
}
|
|
2548
|
+
function mergeLinkageDetails(merged, linkageDetails) {
|
|
2549
|
+
const keyOf = (detail) => {
|
|
2550
|
+
const formField = (detail.formField && typeof detail.formField === 'object') ? detail.formField : {};
|
|
2551
|
+
const subForm = (detail.subForm && typeof detail.subForm === 'object') ? detail.subForm : {};
|
|
2552
|
+
return [String(subForm.id ?? '').trim(), String(detail.dataIndex ?? '').trim(), String(formField.id ?? '').trim()];
|
|
2553
|
+
};
|
|
2554
|
+
if (!linkageDetails || !linkageDetails.length)
|
|
2555
|
+
return [...merged];
|
|
2556
|
+
const result = [...merged];
|
|
2557
|
+
const indexByKey = new Map();
|
|
2558
|
+
result.forEach((d, i) => indexByKey.set(keyOf(d).join('\u0000'), i));
|
|
2559
|
+
for (const linkageDetail of linkageDetails) {
|
|
2560
|
+
const key = keyOf(linkageDetail);
|
|
2561
|
+
if (!key[2])
|
|
2562
|
+
continue;
|
|
2563
|
+
const joined = key.join('\u0000');
|
|
2564
|
+
if (indexByKey.has(joined)) {
|
|
2565
|
+
result[indexByKey.get(joined)] = linkageDetail;
|
|
2566
|
+
}
|
|
2567
|
+
else {
|
|
2568
|
+
result.push(linkageDetail);
|
|
2569
|
+
indexByKey.set(joined, result.length - 1);
|
|
2570
|
+
}
|
|
2571
|
+
}
|
|
2572
|
+
return result;
|
|
2573
|
+
}
|
|
2574
|
+
function computeLinkageVisibility(linkages, entries, defaults, linkageDetails, systemFieldValues) {
|
|
2575
|
+
const hiddenSubforms = new Set();
|
|
2576
|
+
const rowHidden = {};
|
|
2577
|
+
const rowExtraRequired = {};
|
|
2578
|
+
const rowDisabled = {};
|
|
2579
|
+
if (!Array.isArray(linkages))
|
|
2580
|
+
return [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled];
|
|
2581
|
+
const rowAdd = (target, rowKey, fieldId) => {
|
|
2582
|
+
(target[rowKey] || (target[rowKey] = new Set())).add(fieldId);
|
|
2583
|
+
};
|
|
2584
|
+
const rowDiscard = (target, rowKey, fieldId) => {
|
|
2585
|
+
if (target[rowKey])
|
|
2586
|
+
target[rowKey].delete(fieldId);
|
|
2587
|
+
};
|
|
2588
|
+
const fieldValues = {};
|
|
2589
|
+
for (const detail of defaults || []) {
|
|
2590
|
+
if (!detail || typeof detail !== 'object')
|
|
2591
|
+
continue;
|
|
2592
|
+
const formField = (detail.formField && typeof detail.formField === 'object') ? detail.formField : {};
|
|
2593
|
+
const fieldId = String(formField.id ?? '').trim();
|
|
2594
|
+
if (!fieldId)
|
|
2595
|
+
continue;
|
|
2596
|
+
const subFormId = String((detail.subForm && detail.subForm.id) ?? '').trim();
|
|
2597
|
+
const dataIndex = detail.dataIndex;
|
|
2598
|
+
const rowKey = rowKeyFor(subFormId, dataIndex);
|
|
2599
|
+
(fieldValues[rowKey] || (fieldValues[rowKey] = {}))[fieldId] = (fieldValues[rowKey][fieldId] || []).concat(_detailValueIds(detail));
|
|
2600
|
+
}
|
|
2601
|
+
for (const detail of linkageDetails || []) {
|
|
2602
|
+
if (!detail || typeof detail !== 'object')
|
|
2603
|
+
continue;
|
|
2604
|
+
const formField = (detail.formField && typeof detail.formField === 'object') ? detail.formField : {};
|
|
2605
|
+
const fieldId = String(formField.id ?? '').trim();
|
|
2606
|
+
if (!fieldId)
|
|
2607
|
+
continue;
|
|
2608
|
+
const subFormId = String((detail.subForm && detail.subForm.id) ?? '').trim();
|
|
2609
|
+
const dataIndex = detail.dataIndex;
|
|
2610
|
+
const rowKey = rowKeyFor(subFormId, dataIndex);
|
|
2611
|
+
(fieldValues[rowKey] || (fieldValues[rowKey] = {}))[fieldId] = _detailValueIds(detail);
|
|
2612
|
+
}
|
|
2613
|
+
for (const entry of entries) {
|
|
2614
|
+
const field = entry.field;
|
|
2615
|
+
if (!field || typeof field !== 'object')
|
|
2616
|
+
continue;
|
|
2617
|
+
const fieldId = String(field.id ?? '').trim();
|
|
2618
|
+
if (!fieldId)
|
|
2619
|
+
continue;
|
|
2620
|
+
const row = (entry.row && typeof entry.row === 'object') ? entry.row : null;
|
|
2621
|
+
const subFormId = row ? String((row.subForm && row.subForm.id) ?? '').trim() : String(field.subFormId ?? '').trim();
|
|
2622
|
+
const dataIndex = row ? row.dataIndex : null;
|
|
2623
|
+
const rowKey = rowKeyFor(subFormId, dataIndex);
|
|
2624
|
+
(fieldValues[rowKey] || (fieldValues[rowKey] = {}))[fieldId] = _entryValueIds(entry.value);
|
|
2625
|
+
}
|
|
2626
|
+
if (systemFieldValues) {
|
|
2627
|
+
const mainValues = (fieldValues.main || (fieldValues.main = {}));
|
|
2628
|
+
for (const sysKey of Object.keys(systemFieldValues)) {
|
|
2629
|
+
if (systemFieldValues[sysKey].length)
|
|
2630
|
+
(mainValues[sysKey] || (mainValues[sysKey] = [])).push(...systemFieldValues[sysKey]);
|
|
2631
|
+
}
|
|
2632
|
+
}
|
|
2633
|
+
if (!fieldValues.main)
|
|
2634
|
+
fieldValues.main = {};
|
|
2635
|
+
for (const linkage of linkages) {
|
|
2636
|
+
if (!linkage || typeof linkage !== 'object')
|
|
2637
|
+
continue;
|
|
2638
|
+
const condition = linkage.condition;
|
|
2639
|
+
if (!condition || typeof condition !== 'object')
|
|
2640
|
+
continue;
|
|
2641
|
+
const conditionDetails = condition.conditionDetails;
|
|
2642
|
+
if (!Array.isArray(conditionDetails) || !conditionDetails.length)
|
|
2643
|
+
continue;
|
|
2644
|
+
const detailType = String(condition.detailType ?? 'AND').trim().toUpperCase();
|
|
2645
|
+
const cdHitSets = [];
|
|
2646
|
+
for (const cd of conditionDetails) {
|
|
2647
|
+
if (!cd || typeof cd !== 'object')
|
|
2648
|
+
continue;
|
|
2649
|
+
const triggerId = String(cd.targetId ?? '').trim();
|
|
2650
|
+
if (!triggerId)
|
|
2651
|
+
continue;
|
|
2652
|
+
const detailSymbol = String(cd.detailSymbol ?? 'EQUAL').trim().toUpperCase();
|
|
2653
|
+
const sourceType = String(cd.sourceType ?? 'CONSTANT').trim().toUpperCase();
|
|
2654
|
+
const sourceIsVariable = sourceType === 'VARIABLE';
|
|
2655
|
+
let condValues;
|
|
2656
|
+
if (sourceIsVariable) {
|
|
2657
|
+
const sourceId = String(cd.sourceId ?? '').trim();
|
|
2658
|
+
if (!sourceId)
|
|
2659
|
+
continue;
|
|
2660
|
+
condValues = [];
|
|
2661
|
+
}
|
|
2662
|
+
else {
|
|
2663
|
+
condValues = _linkageOptions(cd.sourceContent).flatMap((p) => [p[0], p[1]]);
|
|
2664
|
+
}
|
|
2665
|
+
if (detailSymbol !== 'NULL' && detailSymbol !== 'NOT_NULL' && !sourceIsVariable && !condValues.length)
|
|
2666
|
+
continue;
|
|
2667
|
+
const matchedRows = new Set();
|
|
2668
|
+
for (const rowKey of Object.keys(fieldValues)) {
|
|
2669
|
+
const values = fieldValues[rowKey][triggerId] || [];
|
|
2670
|
+
let condVals = condValues;
|
|
2671
|
+
if (sourceIsVariable) {
|
|
2672
|
+
condVals = fieldValues[rowKey][String(cd.sourceId ?? '').trim()] || [];
|
|
2673
|
+
if (detailSymbol !== 'NULL' && detailSymbol !== 'NOT_NULL' && !condVals.length)
|
|
2674
|
+
continue;
|
|
2675
|
+
}
|
|
2676
|
+
if (detailSymbol === 'NULL') {
|
|
2677
|
+
if (!values.length)
|
|
2678
|
+
matchedRows.add(rowKey);
|
|
2679
|
+
}
|
|
2680
|
+
else if (detailSymbol === 'NOT_NULL') {
|
|
2681
|
+
if (values.length)
|
|
2682
|
+
matchedRows.add(rowKey);
|
|
2683
|
+
}
|
|
2684
|
+
else if (detailSymbol === 'UN_EQUAL' || detailSymbol === 'NOT_IN') {
|
|
2685
|
+
if (values.length && !values.some((v) => condVals.includes(v)))
|
|
2686
|
+
matchedRows.add(rowKey);
|
|
2687
|
+
}
|
|
2688
|
+
else {
|
|
2689
|
+
if (values.some((v) => condVals.includes(v)))
|
|
2690
|
+
matchedRows.add(rowKey);
|
|
2691
|
+
}
|
|
2692
|
+
}
|
|
2693
|
+
cdHitSets.push(matchedRows);
|
|
2694
|
+
}
|
|
2695
|
+
if (!cdHitSets.length)
|
|
2696
|
+
continue;
|
|
2697
|
+
let hitRows;
|
|
2698
|
+
if (detailType === 'AND') {
|
|
2699
|
+
hitRows = new Set(cdHitSets[0]);
|
|
2700
|
+
for (const rows of cdHitSets.slice(1))
|
|
2701
|
+
hitRows = new Set([...hitRows].filter((r) => rows.has(r)));
|
|
2702
|
+
}
|
|
2703
|
+
else {
|
|
2704
|
+
hitRows = new Set();
|
|
2705
|
+
for (const rows of cdHitSets)
|
|
2706
|
+
for (const r of rows)
|
|
2707
|
+
hitRows.add(r);
|
|
2708
|
+
}
|
|
2709
|
+
if (!hitRows.size)
|
|
2710
|
+
continue;
|
|
2711
|
+
for (const ld of linkage.linkageDetails || []) {
|
|
2712
|
+
if (!ld || typeof ld !== 'object')
|
|
2713
|
+
continue;
|
|
2714
|
+
const linkageObj = String(ld.linkageObj ?? '').trim().toLowerCase();
|
|
2715
|
+
const linkageProp = String(ld.linkageProp ?? '').trim().toLowerCase();
|
|
2716
|
+
for (const target of String(ld.linkageValue ?? '').split(',')) {
|
|
2717
|
+
const t = target.trim();
|
|
2718
|
+
if (!t)
|
|
2719
|
+
continue;
|
|
2720
|
+
if (linkageObj === 'detail' && (linkageProp === 'hide' || linkageProp === 'hideline')) {
|
|
2721
|
+
hiddenSubforms.add(t);
|
|
2722
|
+
continue;
|
|
2723
|
+
}
|
|
2724
|
+
if (linkageProp === 'show' && linkageObj === 'detail') {
|
|
2725
|
+
hiddenSubforms.delete(t);
|
|
2726
|
+
continue;
|
|
2727
|
+
}
|
|
2728
|
+
for (const rowKey of hitRows) {
|
|
2729
|
+
if (linkageProp === 'hide' || linkageProp === 'hideline')
|
|
2730
|
+
rowAdd(rowHidden, rowKey, t);
|
|
2731
|
+
else if (linkageProp === 'show')
|
|
2732
|
+
rowDiscard(rowHidden, rowKey, t);
|
|
2733
|
+
else if (linkageProp === 'required')
|
|
2734
|
+
rowAdd(rowExtraRequired, rowKey, t);
|
|
2735
|
+
else if (linkageProp === 'disabled')
|
|
2736
|
+
rowAdd(rowDisabled, rowKey, t);
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
}
|
|
2741
|
+
return [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled];
|
|
2742
|
+
}
|
|
2743
|
+
function mergeDetails(defaults, supplied) {
|
|
2744
|
+
const result = new Map();
|
|
2745
|
+
const keyOf = (detail) => {
|
|
2746
|
+
const formField = (detail.formField && typeof detail.formField === 'object') ? detail.formField : {};
|
|
2747
|
+
const subForm = (detail.subForm && typeof detail.subForm === 'object') ? detail.subForm : {};
|
|
2748
|
+
return [String(subForm.id ?? '').trim(), String(detail.dataIndex ?? '').trim(), String(formField.id ?? '').trim()].join('\u0000');
|
|
2749
|
+
};
|
|
2750
|
+
for (const detail of [...(defaults || []), ...(supplied || [])]) {
|
|
2751
|
+
const normalized = normalizeDetail(detail);
|
|
2752
|
+
if (!normalized)
|
|
2753
|
+
continue;
|
|
2754
|
+
const key = keyOf(normalized);
|
|
2755
|
+
if (hasDetailValue(normalized))
|
|
2756
|
+
result.set(key, normalized);
|
|
2757
|
+
else
|
|
2758
|
+
result.delete(key);
|
|
2759
|
+
}
|
|
2760
|
+
return [...result.values()];
|
|
2761
|
+
}
|
|
2762
|
+
/** 用户本轮**已建的明细行**:subFormId → `[{dataIndex, rowId}]`(取自入参 entries 的明细行)。 */
|
|
2763
|
+
function userDetailRows(entries) {
|
|
2764
|
+
const rows = {};
|
|
2765
|
+
for (const entry of entries || []) {
|
|
2766
|
+
if (!entry || !entry.row)
|
|
2767
|
+
continue;
|
|
2768
|
+
const sub = String((entry.row.subForm && entry.row.subForm.id) ?? '').trim();
|
|
2769
|
+
if (!sub)
|
|
2770
|
+
continue;
|
|
2771
|
+
const list = rows[sub] || (rows[sub] = []);
|
|
2772
|
+
const di = entry.row.dataIndex;
|
|
2773
|
+
if (list.some((r) => String(r.dataIndex) === String(di)))
|
|
2774
|
+
continue;
|
|
2775
|
+
list.push({ dataIndex: di, rowId: entry.row.rowId });
|
|
2776
|
+
}
|
|
2777
|
+
return rows;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* 把「**没有行号的明细默认值**」补到用户已建的行上(V12.1.35)。
|
|
2781
|
+
*
|
|
2782
|
+
* OA `defaultDetails` 里的明细类默认值**不带 `dataIndex`**(实测 6/6,形如
|
|
2783
|
+
* `{formField:{id}, dataOptions:[{optionId,content}], subForm:{id}}`),而明细字段的
|
|
2784
|
+
* **判缺失**与**写载荷**都按 `subFormId + dataIndex + fieldId` 索引 ⇒ 不补就会两处踩:
|
|
2785
|
+
* ① 判缺失查的是 `sub\0行号\0field`(行号来自用户入参),默认值的键却是 `sub\0空\0field`
|
|
2786
|
+
* ⇒ **永远比不中** ⇒ 「明细里已有默认值的必填字段」被**误报缺失**
|
|
2787
|
+
* (实例:`币种` 默认「人民币」、明在 `initialValues` 里,仍进 `missingFields`);主表键固定为
|
|
2788
|
+
* `\0\0field`、与默认值同键,所以主表 18 个默认值都没问题,**只有明细会踩**;
|
|
2789
|
+
* ② 载荷里的默认值因没有行号而落不到行上。
|
|
2790
|
+
*
|
|
2791
|
+
* **只补缺口、不做任何语义判断**:某个 `(subFormId, dataIndex, fieldId)` 已经有值(用户填的、或联动
|
|
2792
|
+
* 带出的)就原样留着不覆盖;**有行号的默认值原样保留**;该明细没有用户行时也原样保留。
|
|
2793
|
+
*/
|
|
2794
|
+
function fillDetailDefaultsByRow(details, detailDefaults, rowsBySubform) {
|
|
2795
|
+
const out = [...(details || [])];
|
|
2796
|
+
const present = new Set(out.map((d) => [
|
|
2797
|
+
String((d && d.subForm && d.subForm.id) ?? '').trim(),
|
|
2798
|
+
String(d && d.dataIndex !== undefined && d.dataIndex !== null ? d.dataIndex : '').trim(),
|
|
2799
|
+
String((d && d.formField && d.formField.id) ?? '').trim(),
|
|
2800
|
+
].join('\u0000')));
|
|
2801
|
+
for (const def of detailDefaults || []) {
|
|
2802
|
+
if (!def)
|
|
2803
|
+
continue;
|
|
2804
|
+
const sub = String((def.subForm && def.subForm.id) ?? '').trim();
|
|
2805
|
+
const fid = String((def.formField && def.formField.id) ?? '').trim();
|
|
2806
|
+
const hasRow = def.dataIndex !== undefined && def.dataIndex !== null
|
|
2807
|
+
&& String(def.dataIndex).trim() !== '';
|
|
2808
|
+
const rows = sub ? rowsBySubform[sub] : undefined;
|
|
2809
|
+
if (!sub || !fid || hasRow || !rows || !rows.length)
|
|
2810
|
+
continue;
|
|
2811
|
+
for (const row of rows) {
|
|
2812
|
+
const key = [sub, String(row.dataIndex).trim(), fid].join('\u0000');
|
|
2813
|
+
if (present.has(key))
|
|
2814
|
+
continue;
|
|
2815
|
+
const copy = { ...def, dataIndex: row.dataIndex };
|
|
2816
|
+
if (row.rowId !== undefined && row.rowId !== null)
|
|
2817
|
+
copy.rowId = row.rowId;
|
|
2818
|
+
out.push(copy);
|
|
2819
|
+
present.add(key);
|
|
2820
|
+
}
|
|
2821
|
+
}
|
|
2822
|
+
return out;
|
|
2823
|
+
}
|
|
2824
|
+
function missingRequiredFields(fields, entries, details, hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled, hiddenFieldIds, compsFieldIds, layoutSubformIds) {
|
|
2825
|
+
const present = new Set((details || []).filter((d) => hasDetailValue(d)).map((d) => {
|
|
2826
|
+
const formField = (d.formField && typeof d.formField === 'object') ? d.formField : {};
|
|
2827
|
+
const subForm = (d.subForm && typeof d.subForm === 'object') ? d.subForm : {};
|
|
2828
|
+
return [String(subForm.id ?? '').trim(), String(d.dataIndex ?? '').trim(), String(formField.id ?? '').trim()].join('\u0000');
|
|
2829
|
+
}));
|
|
2830
|
+
const rowsBySubform = {};
|
|
2831
|
+
for (const entry of entries) {
|
|
2832
|
+
if (entry.row) {
|
|
2833
|
+
const sub = String(entry.row.subForm.id);
|
|
2834
|
+
(rowsBySubform[sub] || (rowsBySubform[sub] = new Set())).add(String(entry.row.dataIndex));
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
const missing = [];
|
|
2838
|
+
for (const field of fields) {
|
|
2839
|
+
if (field.readOnly || field.disableEdit)
|
|
2840
|
+
continue;
|
|
2841
|
+
const fieldId = String(field.id ?? '');
|
|
2842
|
+
if (hiddenFieldIds.has(fieldId))
|
|
2843
|
+
continue;
|
|
2844
|
+
const subFormId = String(field.subFormId ?? '');
|
|
2845
|
+
if (subFormId && hiddenSubforms.has(subFormId))
|
|
2846
|
+
continue;
|
|
2847
|
+
if (subFormId && layoutSubformIds.size && !layoutSubformIds.has(subFormId))
|
|
2848
|
+
continue;
|
|
2849
|
+
if (!subFormId) {
|
|
2850
|
+
if (compsFieldIds.size && !compsFieldIds.has(fieldId))
|
|
2851
|
+
continue;
|
|
2852
|
+
const rowKey = rowKeyFor('');
|
|
2853
|
+
if ((rowHidden[rowKey] || new Set()).has(fieldId) || (rowDisabled[rowKey] || new Set()).has(fieldId))
|
|
2854
|
+
continue;
|
|
2855
|
+
if (!(field.required || (rowExtraRequired[rowKey] || new Set()).has(fieldId)))
|
|
2856
|
+
continue;
|
|
2857
|
+
if (!present.has(['', '', fieldId].join('\u0000')))
|
|
2858
|
+
missing.push({ id: fieldId, name: field.name, subFormId: '' });
|
|
2859
|
+
continue;
|
|
2860
|
+
}
|
|
2861
|
+
for (const dataIndex of [...(rowsBySubform[subFormId] || new Set())].sort()) {
|
|
2862
|
+
const rowKey = rowKeyFor(subFormId, dataIndex);
|
|
2863
|
+
if ((rowHidden[rowKey] || new Set()).has(fieldId) || (rowDisabled[rowKey] || new Set()).has(fieldId))
|
|
2864
|
+
continue;
|
|
2865
|
+
if (!(field.required || (rowExtraRequired[rowKey] || new Set()).has(fieldId)))
|
|
2866
|
+
continue;
|
|
2867
|
+
if (!present.has([subFormId, dataIndex, fieldId].join('\u0000'))) {
|
|
2868
|
+
missing.push({ id: fieldId, name: field.name, subFormId, dataIndex: Number(dataIndex) });
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
}
|
|
2872
|
+
return missing;
|
|
2873
|
+
}
|
|
2874
|
+
function buildPreviewDetails(preview) {
|
|
2875
|
+
const groups = {};
|
|
2876
|
+
for (const item of preview || []) {
|
|
2877
|
+
const subFormId = String(item.subFormId ?? '').trim();
|
|
2878
|
+
if (!subFormId)
|
|
2879
|
+
continue;
|
|
2880
|
+
const fieldId = String(item.fieldId ?? '').trim();
|
|
2881
|
+
const fieldName = String(item.fieldName ?? '').trim();
|
|
2882
|
+
if (!fieldId || !fieldName)
|
|
2883
|
+
continue;
|
|
2884
|
+
const group = groups[subFormId] || (groups[subFormId] = { subFormId, name: '', columns: [], row_map: {} });
|
|
2885
|
+
if (!group.columns.some((c) => c.fieldName === fieldName))
|
|
2886
|
+
group.columns.push({ fieldId, fieldName });
|
|
2887
|
+
const dataIndex = item.dataIndex;
|
|
2888
|
+
const rowKey = String(dataIndex !== undefined && dataIndex !== null ? dataIndex : '0');
|
|
2889
|
+
const row = group.row_map[rowKey] || (group.row_map[rowKey] = { dataIndex, values: {} });
|
|
2890
|
+
row.values[fieldId] = previewValueText(item.value);
|
|
2891
|
+
}
|
|
2892
|
+
const result = [];
|
|
2893
|
+
for (const subFormId of Object.keys(groups).sort()) {
|
|
2894
|
+
const group = groups[subFormId];
|
|
2895
|
+
const rows = Object.keys(group.row_map).sort().map((rk) => ({
|
|
2896
|
+
dataIndex: group.row_map[rk].dataIndex,
|
|
2897
|
+
values: group.columns.map((c) => group.row_map[rk].values[c.fieldId] ?? ''),
|
|
2898
|
+
}));
|
|
2899
|
+
result.push({ subFormId, name: group.name, columns: group.columns, rows });
|
|
2900
|
+
}
|
|
2901
|
+
return result;
|
|
2902
|
+
}
|
|
2903
|
+
function previewValueText(value) {
|
|
2904
|
+
if (Array.isArray(value) && value.length && value.every((i) => i && typeof i === 'object')) {
|
|
2905
|
+
const names = value.map((i) => String(i.name ?? i.content ?? '').trim()).filter(Boolean);
|
|
2906
|
+
if (names.length)
|
|
2907
|
+
return names.join('、');
|
|
2908
|
+
}
|
|
2909
|
+
return value;
|
|
2910
|
+
}
|
|
2911
|
+
function linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, hiddenFieldIds, compsFieldIds, entries, layoutSubformIds) {
|
|
2912
|
+
if (!linkageDetails || !linkageDetails.length)
|
|
2913
|
+
return '';
|
|
2914
|
+
const nameById = new Map();
|
|
2915
|
+
for (const f of fields)
|
|
2916
|
+
nameById.set(String(f.id ?? ''), f.name || '');
|
|
2917
|
+
const entryFieldIds = new Set(entries.filter((e) => e.field && typeof e.field === 'object').map((e) => String(e.field.id)));
|
|
2918
|
+
const brought = [];
|
|
2919
|
+
const cleared = [];
|
|
2920
|
+
for (const detail of linkageDetails) {
|
|
2921
|
+
const fieldId = String((detail.formField && detail.formField.id) ?? '').trim();
|
|
2922
|
+
if (!fieldId)
|
|
2923
|
+
continue;
|
|
2924
|
+
if (hiddenFieldIds.has(fieldId))
|
|
2925
|
+
continue;
|
|
2926
|
+
const subFormId = String((detail.subForm && detail.subForm.id) ?? '').trim();
|
|
2927
|
+
if (!subFormId && compsFieldIds.size && !compsFieldIds.has(fieldId))
|
|
2928
|
+
continue;
|
|
2929
|
+
if (subFormId && layoutSubformIds.size && !layoutSubformIds.has(subFormId))
|
|
2930
|
+
continue;
|
|
2931
|
+
const dataIndex = detail.dataIndex;
|
|
2932
|
+
const rowKey = rowKeyFor(subFormId, dataIndex);
|
|
2933
|
+
if ((rowHidden[rowKey] || new Set()).has(fieldId))
|
|
2934
|
+
continue;
|
|
2935
|
+
const field = fields.find((f) => String(f.id ?? '') === fieldId);
|
|
2936
|
+
if (field && hiddenSubforms.has(String(field.subFormId ?? '')))
|
|
2937
|
+
continue;
|
|
2938
|
+
const name = nameById.get(fieldId) || fieldId;
|
|
2939
|
+
const display = _detailDisplayText(detail);
|
|
2940
|
+
if (!display) {
|
|
2941
|
+
if (entryFieldIds.has(fieldId))
|
|
2942
|
+
cleared.push(name);
|
|
2943
|
+
continue;
|
|
2944
|
+
}
|
|
2945
|
+
brought.push(`${name}=${display}`);
|
|
2946
|
+
}
|
|
2947
|
+
let result = '';
|
|
2948
|
+
if (brought.length)
|
|
2949
|
+
result += '字段联动带出:' + brought.join('、') + '。';
|
|
2950
|
+
if (cleared.length)
|
|
2951
|
+
result += '字段联动清空:' + cleared.join('、') + '。';
|
|
2952
|
+
return result;
|
|
2953
|
+
}
|
|
2954
|
+
// ---------------------------------------------------------------------------
|
|
2955
|
+
// createPrepare / createApply
|
|
2956
|
+
// ---------------------------------------------------------------------------
|
|
2957
|
+
async function commandCreatePrepare(args, env) {
|
|
2958
|
+
const token = String(args.context ?? '');
|
|
2959
|
+
if (!token)
|
|
2960
|
+
throw workflowValidationError('context_required', '需要 createForm 返回的 continuation(context)');
|
|
2961
|
+
const lease = await verifyContextLease(env, token, 'workflow.createForm');
|
|
2962
|
+
const context = lease.payload;
|
|
2963
|
+
const [main, detailValues] = splitInput(args.data);
|
|
2964
|
+
const fields = Array.isArray(context.fields) ? context.fields : [];
|
|
2965
|
+
const [entries, fieldAmbiguities] = bindInputFields(main, detailValues, fields);
|
|
2966
|
+
if (fieldAmbiguities.length) {
|
|
2967
|
+
return workflowSuccess('workflow.createPrepare', {
|
|
2968
|
+
status: 'NEEDS_CONFIRMATION',
|
|
2969
|
+
contextId: token,
|
|
2970
|
+
message: '存在重名或无法识别的表单字段,请改用字段 id。',
|
|
2971
|
+
ambiguities: fieldAmbiguities,
|
|
2972
|
+
});
|
|
2973
|
+
}
|
|
2974
|
+
const readonlyHits = readonlyEntryViolations(entries);
|
|
2975
|
+
if (readonlyHits.length) {
|
|
2976
|
+
return workflowSuccess('workflow.createPrepare', {
|
|
2977
|
+
status: 'NEEDS_CONFIRMATION',
|
|
2978
|
+
contextId: token,
|
|
2979
|
+
message: '以下字段是只读/禁止手工编辑字段,不能填写:' + readonlyHits.map((h) => h.name || h.fieldId).join('、'),
|
|
2980
|
+
notEditable: readonlyHits,
|
|
2981
|
+
});
|
|
2982
|
+
}
|
|
2983
|
+
const [details, valueAmbiguities, preview] = await resolveEntryValues(env, entries, context.layout.form_id);
|
|
2984
|
+
const merged = mergeDetails(context.defaults || [], details);
|
|
2985
|
+
const linkageDetails = await runFieldLinkage(env, linkageContextFromPayload(context), entries, null, merged, false);
|
|
2986
|
+
// 明细默认值无行号,而判缺失/写载荷都按行号索引 ⇒ 按用户已建行补齐缺口(V12.1.35,
|
|
2987
|
+
// 详见 fillDetailDefaultsByRow 注释)。放在联动之后合并,联动带出的值仍然优先。
|
|
2988
|
+
const merged2 = fillDetailDefaultsByRow(mergeLinkageDetails(merged, linkageDetails), context.defaults || [], userDetailRows(entries));
|
|
2989
|
+
const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
|
|
2990
|
+
const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
|
|
2991
|
+
const [hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.defaults || [], linkageDetails, context.systemFieldValues);
|
|
2992
|
+
const initialHidden = new Set(fields.filter((f) => f.display === false).map((f) => String(f.id ?? '')));
|
|
2993
|
+
if (initialHidden.size) {
|
|
2994
|
+
const mh = new Set(rowHidden.main || []);
|
|
2995
|
+
for (const id of initialHidden)
|
|
2996
|
+
mh.add(id);
|
|
2997
|
+
rowHidden.main = mh;
|
|
2998
|
+
}
|
|
2999
|
+
const missing = missingRequiredFields(fields, entries, merged2, hiddenSubforms, rowHidden, rowExtraRequired, rowDisabled, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), new Set(context.layoutSubformIds || []));
|
|
3000
|
+
const sysPermissions = (context.systemFields && context.systemFields.permissions) || {};
|
|
3001
|
+
const sysDefaults = (context.systemFields && context.systemFields.defaults) || {};
|
|
3002
|
+
const requestLevelArg = String(args.requestLevel ?? '').trim();
|
|
3003
|
+
const secLevelArg = String(args.secLevel ?? '').trim();
|
|
3004
|
+
const requestNameValue = String(args.requestName ?? sysDefaults.requestName ?? '').trim();
|
|
3005
|
+
const requestLevelValue = requestLevelArg || String(sysDefaults.requestLevel ?? '').trim();
|
|
3006
|
+
const secLevelValue = secLevelArg || String(sysDefaults.secLevel ?? '').trim();
|
|
3007
|
+
const systemMissing = [];
|
|
3008
|
+
for (const sysKey of Object.keys(SYSTEM_FIELD_KEYS)) {
|
|
3009
|
+
const perm = sysPermissions[sysKey] || {};
|
|
3010
|
+
if (!perm.isview)
|
|
3011
|
+
continue;
|
|
3012
|
+
const value = { REQUEST_NAMEL: requestNameValue, REQUEST_LEVEL: requestLevelValue, SECLEVEL: secLevelValue }[sysKey];
|
|
3013
|
+
if (perm.ismandatory && !value)
|
|
3014
|
+
systemMissing.push({ key: sysKey, name: SYSTEM_FIELD_KEYS[sysKey], subFormId: '', systemField: true });
|
|
3015
|
+
}
|
|
3016
|
+
const intent = String(args.intent ?? 'submit');
|
|
3017
|
+
if (valueAmbiguities.length || ((missing.length || systemMissing.length) && intent !== 'draft')) {
|
|
3018
|
+
return workflowSuccess('workflow.createPrepare', {
|
|
3019
|
+
status: valueAmbiguities.length ? 'NEEDS_CONFIRMATION' : 'NEEDS_INPUT',
|
|
3020
|
+
contextId: token,
|
|
3021
|
+
message: valueAmbiguities.length ? '存在无法唯一解析的选项,或无法匹配的浏览按钮值,请让用户确认。' : '缺少必填字段。',
|
|
3022
|
+
// 已解析成功的字段回显(V12.1.34):入参给的是名称时,这里给出**服务端最终解析结果**
|
|
3023
|
+
// (选项给名称、浏览/人员/部门给 {id,name} 数组),模型据此自查"我传的值到底变成了什么对象",
|
|
3024
|
+
// 不必把不确定性转嫁给用户确认。READY 时同一内容在 `fields`(确认预览),此处不再重复。
|
|
3025
|
+
// 之前这条短路返回完全没有回显,模型因此抱怨"prepare 输出里看不到字段预览"。
|
|
3026
|
+
resolvedValues: preview,
|
|
3027
|
+
ambiguities: valueAmbiguities,
|
|
3028
|
+
missingFields: [...missing, ...systemMissing],
|
|
3029
|
+
linkageResult: linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), entries, new Set(context.layoutSubformIds || [])),
|
|
3030
|
+
...folderPayload,
|
|
3031
|
+
});
|
|
3032
|
+
}
|
|
3033
|
+
const requestName = String(args.requestName ?? sysDefaults.requestName ?? context.runtime.default_request_name ?? context.workflow.name ?? '新建流程').trim();
|
|
3034
|
+
if (!requestName)
|
|
3035
|
+
throw workflowValidationError('input_invalid', 'request_name 不能为空');
|
|
3036
|
+
const inputFieldIds = [...new Set(entries.map((e) => String((e.field && e.field.id) ?? '')).filter(Boolean))];
|
|
3037
|
+
const prepared = {
|
|
3038
|
+
dataDetails: merged2,
|
|
3039
|
+
requestName,
|
|
3040
|
+
requestLevel: requestLevelValue,
|
|
3041
|
+
secLevel: secLevelValue,
|
|
3042
|
+
remark: String(args.remark ?? ''),
|
|
3043
|
+
preview,
|
|
3044
|
+
previewDetails: buildPreviewDetails(preview),
|
|
3045
|
+
intent,
|
|
3046
|
+
missingFields: (intent === 'draft') ? [...missing, ...systemMissing] : [],
|
|
3047
|
+
inputFieldIds,
|
|
3048
|
+
};
|
|
3049
|
+
const session = await getSessionContext(env);
|
|
3050
|
+
const outToken = await env.continuation.issue({
|
|
3051
|
+
operation: 'workflow.createPrepare',
|
|
3052
|
+
baseUrl: session.baseUrl,
|
|
3053
|
+
profile: session.profile,
|
|
3054
|
+
userId: session.userId,
|
|
3055
|
+
payload: { context, prepared },
|
|
3056
|
+
}, CONTINUATION_TTL_MS);
|
|
3057
|
+
return workflowSuccess('workflow.createPrepare', {
|
|
3058
|
+
status: 'READY',
|
|
3059
|
+
contextId: outToken,
|
|
3060
|
+
requestId: context.runtime.request_id,
|
|
3061
|
+
workflow: context.workflow,
|
|
3062
|
+
requestName,
|
|
3063
|
+
requestLevel: requestLevelValue,
|
|
3064
|
+
secLevel: secLevelValue,
|
|
3065
|
+
fields: preview,
|
|
3066
|
+
previewDetails: buildPreviewDetails(preview),
|
|
3067
|
+
confirmationRequired: true,
|
|
3068
|
+
linkageResult: linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), entries, new Set(context.layoutSubformIds || [])),
|
|
3069
|
+
missingFields: (intent === 'draft') ? [...missing, ...systemMissing] : [],
|
|
3070
|
+
...folderPayload,
|
|
3071
|
+
message: (!missing.length && !systemMissing.length)
|
|
3072
|
+
? '尚未调用 saveFormData 和 flow/create。请向用户展示预览并获得明确确认。'
|
|
3073
|
+
: `仅发起保存(draft)模式:以下必填字段未填写:${[...missing, ...systemMissing].map((m) => m.name).join('、')}。可继续 create 保存草稿,或补全字段后重新 prepare。`,
|
|
3074
|
+
continuation: outToken,
|
|
3075
|
+
});
|
|
3076
|
+
}
|
|
3077
|
+
function formatRemark(value) {
|
|
3078
|
+
const text = String(value ?? '').trim();
|
|
3079
|
+
if (!text)
|
|
3080
|
+
return '_weaverMte_';
|
|
3081
|
+
if (text.startsWith('_weaverMte_'))
|
|
3082
|
+
return text;
|
|
3083
|
+
return `_weaverMte_<p>${text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>')}</p>`;
|
|
3084
|
+
}
|
|
3085
|
+
function buildSaveBody(context, prepared) {
|
|
3086
|
+
const runtime = context.runtime;
|
|
3087
|
+
return {
|
|
3088
|
+
module: 'workflow',
|
|
3089
|
+
customParam: {
|
|
3090
|
+
isCreate: true,
|
|
3091
|
+
requestId: runtime.request_id,
|
|
3092
|
+
isTest: false,
|
|
3093
|
+
secretLevel: '0',
|
|
3094
|
+
secretLevelValidity: '',
|
|
3095
|
+
nodeId: runtime.node_id,
|
|
3096
|
+
workflowId: context.workflow.id,
|
|
3097
|
+
type: 23,
|
|
3098
|
+
authSignatureStr: runtime.auth_signature,
|
|
3099
|
+
authStr: runtime.auth_str,
|
|
3100
|
+
identityId: runtime.identity_id,
|
|
3101
|
+
identityType: runtime.identity_type,
|
|
3102
|
+
monitorEditFormData: false,
|
|
3103
|
+
},
|
|
3104
|
+
employeeId: runtime.employee_id,
|
|
3105
|
+
formData: {
|
|
3106
|
+
module: 'workflow',
|
|
3107
|
+
presetDataId: runtime.request_id,
|
|
3108
|
+
dataDetails: prepared.dataDetails,
|
|
3109
|
+
client: 'pc',
|
|
3110
|
+
dataStatus: 'temporary',
|
|
3111
|
+
form: { id: context.layout.form_id },
|
|
3112
|
+
formLayout: { id: context.layout.layout_id },
|
|
3113
|
+
},
|
|
3114
|
+
};
|
|
3115
|
+
}
|
|
3116
|
+
/**
|
|
3117
|
+
* 取出 prepare 阶段算好的载荷。
|
|
3118
|
+
*
|
|
3119
|
+
* `createPrepare`/`editPrepare` 签发的结构是 **`payload: {context, prepared}`** —— `prepared` 与
|
|
3120
|
+
* `context` **平级**,并不在 `context` 里面。历史上这里直接读 `context.prepared`,于是:
|
|
3121
|
+
* - 发起(createApply):`flow/create` 之前构造请求体时 `context.prepared` 为 undefined,
|
|
3122
|
+
* 读 `requestLevel` 直接抛 TypeError(internal/unexpected)⇒ **流程根本创建不出来**;
|
|
3123
|
+
* - 修改(editApply):退化成 `{}` ⇒ `dataDetails` 恒为空数组 ⇒ **改了半天一个字段都没存进去**。
|
|
3124
|
+
* 现在统一走这一个出口:优先用调用方传下来的平级 `prepared`,同时兼容旧的嵌套形态。
|
|
3125
|
+
*/
|
|
3126
|
+
function resolvePrepared(context, prepared) {
|
|
3127
|
+
if (prepared && typeof prepared === 'object' && Object.keys(prepared).length)
|
|
3128
|
+
return prepared;
|
|
3129
|
+
const nested = context?.prepared;
|
|
3130
|
+
return (nested && typeof nested === 'object') ? nested : {};
|
|
3131
|
+
}
|
|
3132
|
+
function buildCreateBody(context, prepared, saved, src) {
|
|
3133
|
+
const runtime = context.runtime;
|
|
3134
|
+
return {
|
|
3135
|
+
workflowId: context.workflow.id,
|
|
3136
|
+
isCreate: true,
|
|
3137
|
+
apiModule: 'workflow',
|
|
3138
|
+
identityId: runtime.identity_id,
|
|
3139
|
+
identityType: runtime.identity_type,
|
|
3140
|
+
requestId: runtime.request_id,
|
|
3141
|
+
userCurrentNodeId: runtime.node_id,
|
|
3142
|
+
fixedNodeId: runtime.node_id,
|
|
3143
|
+
isAgent: false,
|
|
3144
|
+
beAgentId: '0',
|
|
3145
|
+
authStr: runtime.auth_str,
|
|
3146
|
+
authSignatureStr: runtime.auth_signature,
|
|
3147
|
+
isTest: false,
|
|
3148
|
+
pageKey: `create_${context.workflow.id}_${Date.now()}`,
|
|
3149
|
+
requestLevel: String(prepared.requestLevel ?? '0').trim(),
|
|
3150
|
+
secLevel: String(prepared.secLevel ?? '0').trim(),
|
|
3151
|
+
secValidity: '',
|
|
3152
|
+
needWfBack: true,
|
|
3153
|
+
sameOperateId: null,
|
|
3154
|
+
customBtnId: null,
|
|
3155
|
+
confirmSecondSubmit: false,
|
|
3156
|
+
selectFlowParam: null,
|
|
3157
|
+
freeChoiceNodeIds: null,
|
|
3158
|
+
nodeAddOperators: null,
|
|
3159
|
+
requestName: prepared.requestName,
|
|
3160
|
+
dataId: saved.data_id,
|
|
3161
|
+
recordId: saved.record_id,
|
|
3162
|
+
saveFormTime: saved.save_form_time,
|
|
3163
|
+
voteSubmit: false,
|
|
3164
|
+
sealId: null,
|
|
3165
|
+
unNeedVerifyPrivacyAgreement: 1,
|
|
3166
|
+
remark: formatRemark(prepared.remark),
|
|
3167
|
+
attachments: [],
|
|
3168
|
+
fileData: [],
|
|
3169
|
+
locationData: {},
|
|
3170
|
+
comment: {
|
|
3171
|
+
relevanceList: [],
|
|
3172
|
+
imAtContent: formatRemark(prepared.remark) !== '_weaverMte_' ? formatRemark(prepared.remark) : '',
|
|
3173
|
+
originContent: formatRemark(prepared.remark) !== '_weaverMte_' ? formatRemark(prepared.remark) : '',
|
|
3174
|
+
atLinkList: [],
|
|
3175
|
+
content: formatRemark(prepared.remark),
|
|
3176
|
+
},
|
|
3177
|
+
quoteData: [],
|
|
3178
|
+
fullTextAnnotationData: [],
|
|
3179
|
+
signatureData: { signatureImgId: '' },
|
|
3180
|
+
userSignatureData: { signatureImgId: '' },
|
|
3181
|
+
src,
|
|
3182
|
+
clientType: 0,
|
|
3183
|
+
};
|
|
3184
|
+
}
|
|
3185
|
+
function parseSaveResponse(response) {
|
|
3186
|
+
const data = responseData(response);
|
|
3187
|
+
if (!data || typeof data !== 'object')
|
|
3188
|
+
throw workflowPartialError('saveFormData 成功但返回恢复标识不完整,禁止自动重试', { stage: 'saveFormData' });
|
|
3189
|
+
const values = {
|
|
3190
|
+
data_id: String(data.id ?? '').trim(),
|
|
3191
|
+
record_id: String(data.recordId ?? '').trim(),
|
|
3192
|
+
save_form_time: String(data.logDate ?? '').trim(),
|
|
3193
|
+
};
|
|
3194
|
+
if (!values.data_id || !values.record_id || !values.save_form_time) {
|
|
3195
|
+
throw workflowPartialError('saveFormData 成功但返回恢复标识不完整,禁止自动重试', { stage: 'saveFormData', ...values });
|
|
3196
|
+
}
|
|
3197
|
+
return values;
|
|
3198
|
+
}
|
|
3199
|
+
function parseCreateResponse(response, expectedRequestId) {
|
|
3200
|
+
if (!response || typeof response !== 'object')
|
|
3201
|
+
throw workflowPartialError('flow/create 返回格式错误,禁止自动重试', { stage: 'flow/create' });
|
|
3202
|
+
const success = response.status === true || [0, 200, '0', '200'].includes(response.code) || [0, 200, '0', '200'].includes(response.status);
|
|
3203
|
+
if (!success || response.flowIsCreateSuccess === false || response.fail === true) {
|
|
3204
|
+
throw workflowPartialError(String(response.resultMessage ?? response.msg ?? 'flow/create 未确认成功'), { stage: 'flow/create' });
|
|
3205
|
+
}
|
|
3206
|
+
const sources = [response];
|
|
3207
|
+
let cursor = 0;
|
|
3208
|
+
while (cursor < sources.length) {
|
|
3209
|
+
const source = sources[cursor++];
|
|
3210
|
+
for (const key of ['data', 'result', 'workflowBaseInfo', 'requestInfo']) {
|
|
3211
|
+
const nested = source[key];
|
|
3212
|
+
if (nested && typeof nested === 'object' && !sources.includes(nested))
|
|
3213
|
+
sources.push(nested);
|
|
3214
|
+
}
|
|
3215
|
+
}
|
|
3216
|
+
const firstText = (...keys) => {
|
|
3217
|
+
for (const source of sources) {
|
|
3218
|
+
for (const key of keys) {
|
|
3219
|
+
const v = String(source[key] ?? '').trim();
|
|
3220
|
+
if (v)
|
|
3221
|
+
return v;
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
return '';
|
|
3225
|
+
};
|
|
3226
|
+
const returnedId = firstText('requestId', 'requestid');
|
|
3227
|
+
if (returnedId && returnedId !== expectedRequestId) {
|
|
3228
|
+
throw workflowPartialError('flow/create 返回了不同的 requestId,禁止自动重试', { stage: 'flow/create' });
|
|
3229
|
+
}
|
|
3230
|
+
return {
|
|
3231
|
+
requestId: returnedId || expectedRequestId,
|
|
3232
|
+
requestMark: firstText('requestMark', 'requestmark', 'requestCode'),
|
|
3233
|
+
requestName: firstText('requestName', 'requestname'),
|
|
3234
|
+
};
|
|
3235
|
+
}
|
|
3236
|
+
async function runCreateNodeFieldVerification(env, context) {
|
|
3237
|
+
const runtime = context.runtime;
|
|
3238
|
+
const requestId = String(runtime.request_id ?? '').trim();
|
|
3239
|
+
const nodeId = String(runtime.node_id ?? '').trim();
|
|
3240
|
+
const result = { enabled: false, results: [], blocked: false, passed: true, reminders: [], error: '' };
|
|
3241
|
+
if (!requestId || !nodeId) {
|
|
3242
|
+
result.error = '缺少 requestId 或 nodeId,跳过节点字段校验';
|
|
3243
|
+
return result;
|
|
3244
|
+
}
|
|
3245
|
+
const baseUrl = (await getSessionContext(env)).baseUrl;
|
|
3246
|
+
const referer = `${baseUrl}/sp/workflow/flowpage/create/${context.workflow.id}?workflowId=${context.workflow.id}&isCreate=1`;
|
|
3247
|
+
const conf = await requestJson(env, 'GET', FORM_ENDPOINTS.conformanceSet, {
|
|
3248
|
+
requestId, nodeId, entryMode: 2, triggerOperation: '0', delete: 'false',
|
|
3249
|
+
}, undefined);
|
|
3250
|
+
// requestJson 内部已解包一层(responseData),此处不可再取 .data
|
|
3251
|
+
const confData = (conf && typeof conf === 'object') ? conf : null;
|
|
3252
|
+
const cptBase = (confData && confData.cptBase) ? confData.cptBase : null;
|
|
3253
|
+
const cptSetList = (confData && confData.cptSetList) ? confData.cptSetList : null;
|
|
3254
|
+
if (!cptBase || typeof cptBase !== 'object' || !Array.isArray(cptSetList) || !cptSetList.length)
|
|
3255
|
+
return result;
|
|
3256
|
+
if (Number(cptBase.openNodeCheck ?? 0) !== 1)
|
|
3257
|
+
return result;
|
|
3258
|
+
result.enabled = true;
|
|
3259
|
+
const ruleSet = [];
|
|
3260
|
+
for (const rule of cptSetList) {
|
|
3261
|
+
if (!rule || typeof rule !== 'object')
|
|
3262
|
+
continue;
|
|
3263
|
+
ruleSet.push({
|
|
3264
|
+
ruleName: String(rule.ruleName ?? ''),
|
|
3265
|
+
verificationResult: '检测中…',
|
|
3266
|
+
rowId: randomUUID().replace(/-/g, ''),
|
|
3267
|
+
conditionKey: String(rule.conditionKey ?? ''),
|
|
3268
|
+
rulePassTip: String(rule.rulePassTip ?? ''),
|
|
3269
|
+
ruleNoPassTip: String(rule.ruleNoPassTip ?? ''),
|
|
3270
|
+
verificationStatus: 0,
|
|
3271
|
+
processType: Number(rule.processType ?? 1),
|
|
3272
|
+
cptSetId: String(rule.id ?? ''),
|
|
3273
|
+
ruleType: Number(rule.ruleType ?? 0),
|
|
3274
|
+
});
|
|
3275
|
+
}
|
|
3276
|
+
const dataResult = await requestJson(env, 'POST', FORM_ENDPOINTS.verificationRequestData, undefined, {
|
|
3277
|
+
requestId, nodeId, ruleSet, isCooperate: 0, isTest: 0, testUserId: String(runtime.employee_id ?? ''),
|
|
3278
|
+
});
|
|
3279
|
+
// requestJson 内部已解包一层(responseData),此处不可再取 .data
|
|
3280
|
+
const dataPayload = (dataResult && typeof dataResult === 'object') ? dataResult : null;
|
|
3281
|
+
if (!dataPayload || typeof dataPayload !== 'object') {
|
|
3282
|
+
result.error = 'getRequestDataByRequestId 未返回有效数据';
|
|
3283
|
+
return result;
|
|
3284
|
+
}
|
|
3285
|
+
const requestData = dataPayload.requestData || [];
|
|
3286
|
+
const systemContents = dataPayload.systemContents || [];
|
|
3287
|
+
for (const rule of ruleSet) {
|
|
3288
|
+
const verdict = await requestJson(env, 'POST', FORM_ENDPOINTS.verificationConditionRule, undefined, {
|
|
3289
|
+
requestId, nodeId, identityId: '', cptSetId: rule.cptSetId, conditionKey: rule.conditionKey, requestData, systemContents,
|
|
3290
|
+
});
|
|
3291
|
+
// requestJson 内部已解包一层(responseData),此处不可再取 .data
|
|
3292
|
+
const vdata = (verdict && typeof verdict === 'object') ? verdict : null;
|
|
3293
|
+
const status = Number((vdata && vdata.status) ?? 0);
|
|
3294
|
+
const ruleType = Number(rule.ruleType ?? 0);
|
|
3295
|
+
const reason = String((vdata && vdata.reason) ?? '').trim();
|
|
3296
|
+
const ruleResult = String((vdata && vdata.result) ?? '').trim();
|
|
3297
|
+
const tip = ruleType === 2 ? reason : ruleResult;
|
|
3298
|
+
const item = { ruleName: rule.ruleName, status, result: tip, reason, ruleType };
|
|
3299
|
+
result.results.push(item);
|
|
3300
|
+
if (status === 3)
|
|
3301
|
+
result.reminders.push(item);
|
|
3302
|
+
}
|
|
3303
|
+
const ruleRelation = Number(cptBase.ruleRelation ?? 0);
|
|
3304
|
+
const statuses = result.results.map((r) => r.status);
|
|
3305
|
+
result.blocked = ruleRelation === 1 ? !statuses.some((s) => s === 1) : statuses.some((s) => s === 2);
|
|
3306
|
+
result.ruleRelation = ruleRelation;
|
|
3307
|
+
result.passPopUpPrompt = Number(cptBase.passPopUpPrompt ?? 0);
|
|
3308
|
+
result.noPassPrompt = Number(cptBase.noPassPrompt ?? 0);
|
|
3309
|
+
result.passed = !result.blocked;
|
|
3310
|
+
return result;
|
|
3311
|
+
}
|
|
3312
|
+
async function commandCreateApply(args, env) {
|
|
3313
|
+
if (args.confirm !== true)
|
|
3314
|
+
throw workflowValidationError('confirm_required', '必须在用户明确确认后传入 confirm=true');
|
|
3315
|
+
const token = String(args.context ?? '');
|
|
3316
|
+
if (!token)
|
|
3317
|
+
throw workflowValidationError('context_required', '需要 createPrepare 返回的 continuation(context)');
|
|
3318
|
+
const lease = await verifyContextLease(env, token, 'workflow.createPrepare', true);
|
|
3319
|
+
const { context, prepared } = lease.payload;
|
|
3320
|
+
// 只读硬拦截兜底:复核 prepare 记录的输入字段此刻是否仍可写
|
|
3321
|
+
const inputIds = prepared.inputFieldIds || [];
|
|
3322
|
+
if (inputIds.length) {
|
|
3323
|
+
const fieldsById = new Map((context.fields || []).map((f) => [String(f.id ?? ''), f]));
|
|
3324
|
+
const readonlyHits = inputIds.filter((fid) => {
|
|
3325
|
+
const f = fieldsById.get(fid);
|
|
3326
|
+
return f && (f.readOnly || f.disableEdit);
|
|
3327
|
+
}).map((fid) => ({ fieldId: fid, name: (fieldsById.get(fid) || {}).name ?? fid }));
|
|
3328
|
+
if (readonlyHits.length) {
|
|
3329
|
+
throw workflowPolicyError('field_readonly', '提交数据包含只读/禁止手工编辑字段,已停止保存:' + readonlyHits.map((h) => h.name || h.fieldId).join('、'), { notEditable: readonlyHits });
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
// 保存时字段联动(EVENT_SAVE_DATA)
|
|
3333
|
+
if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
|
|
3334
|
+
const saveDetails = prepared.dataDetails || [];
|
|
3335
|
+
const saveEntries = _entriesFromDefaults(saveDetails);
|
|
3336
|
+
if (saveEntries.length) {
|
|
3337
|
+
try {
|
|
3338
|
+
const saveLinkage = await runFieldLinkage(env, linkageContextFromPayload(context), saveEntries, new Set(['EVENT_SAVE_DATA']), saveDetails, false);
|
|
3339
|
+
if (saveLinkage.length)
|
|
3340
|
+
prepared.dataDetails = mergeLinkageDetails(saveDetails, saveLinkage);
|
|
3341
|
+
}
|
|
3342
|
+
catch {
|
|
3343
|
+
/* 写操作红线:不确定不重试,仅跳过 */
|
|
3344
|
+
}
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
// 阶段推进到"要发写入请求了":此后再被中止/杀掉,重放只能按**结果不确定**处理(先只读核对),
|
|
3348
|
+
// 而在此之前被中止则允许安全重试同一句柄。写不进阶段就中止(宁可不发,也不留无法判定的一次性写入)。
|
|
3349
|
+
await env.continuation.markStage(token, 'saving');
|
|
3350
|
+
let saveData;
|
|
3351
|
+
try {
|
|
3352
|
+
const saveResponse = await env.host.workflowRequest({
|
|
3353
|
+
method: 'POST',
|
|
3354
|
+
path: formApiUrl(FORM_ENDPOINTS.save, context.frame.relate_type),
|
|
3355
|
+
query: { formId: context.layout.form_id, ...AJAX_QUERY },
|
|
3356
|
+
body: buildSaveBody(context, prepared),
|
|
3357
|
+
operation: 'workflow.createApply',
|
|
3358
|
+
});
|
|
3359
|
+
saveData = parseSaveResponse(saveResponse);
|
|
3360
|
+
}
|
|
3361
|
+
catch (error) {
|
|
3362
|
+
if (isNetworkError(error)) {
|
|
3363
|
+
throw workflowPartialError('saveFormData 请求发送后连接中断,是否已创建不确定,禁止自动重试,请通过 workflow.list 复核', { attempted: { requestId: context.runtime.request_id } }, { stage: 'saveFormData' });
|
|
3364
|
+
}
|
|
3365
|
+
throw error;
|
|
3366
|
+
}
|
|
3367
|
+
const submitIntent = String(args.intent ?? 'submit') === 'submit';
|
|
3368
|
+
let verification = {};
|
|
3369
|
+
let needConfirm = false;
|
|
3370
|
+
if (submitIntent) {
|
|
3371
|
+
verification = await runCreateNodeFieldVerification(env, context);
|
|
3372
|
+
const reminders = verification.reminders || [];
|
|
3373
|
+
needConfirm = verification.enabled
|
|
3374
|
+
? (Boolean(reminders.length && verification.noPassPrompt === 1) || (!reminders.length && verification.passPopUpPrompt === 1))
|
|
3375
|
+
: false;
|
|
3376
|
+
}
|
|
3377
|
+
const createSrc = (submitIntent && !verification.blocked && !(needConfirm && args.verificationConfirmed !== true)) ? 'submit' : 'save';
|
|
3378
|
+
// 提交时字段联动(EVENT_SUBMIT_OR_AGREE)
|
|
3379
|
+
if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
|
|
3380
|
+
const submitDetails = prepared.dataDetails || [];
|
|
3381
|
+
const submitEntries = _entriesFromDefaults(submitDetails);
|
|
3382
|
+
if (submitEntries.length) {
|
|
3383
|
+
try {
|
|
3384
|
+
const submitLinkage = await runFieldLinkage(env, linkageContextFromPayload(context), submitEntries, new Set(['EVENT_SUBMIT_OR_AGREE']), submitDetails, false);
|
|
3385
|
+
if (submitLinkage.length)
|
|
3386
|
+
prepared.dataDetails = mergeLinkageDetails(submitDetails, submitLinkage);
|
|
3387
|
+
}
|
|
3388
|
+
catch {
|
|
3389
|
+
/* 跳过 */
|
|
3390
|
+
}
|
|
3391
|
+
}
|
|
3392
|
+
}
|
|
3393
|
+
let created;
|
|
3394
|
+
try {
|
|
3395
|
+
const createResponse = await env.host.workflowRequest({
|
|
3396
|
+
method: 'POST',
|
|
3397
|
+
path: FORM_ENDPOINTS.create,
|
|
3398
|
+
query: AJAX_QUERY,
|
|
3399
|
+
body: buildCreateBody(context, resolvePrepared(context, prepared), saveData, createSrc),
|
|
3400
|
+
operation: 'workflow.createApply',
|
|
3401
|
+
});
|
|
3402
|
+
// parseCreateResponse 需读外层 status/code 并向下遍历 data/result,必须传原始响应
|
|
3403
|
+
created = parseCreateResponse(createResponse, context.runtime.request_id);
|
|
3404
|
+
// flow/create 已返回成功:阶段推进到 committed。若进程在"回写成功结果"之前被中止,
|
|
3405
|
+
// 重放可据此报幂等成功(ALREADY_APPLIED + requestId),而不是只报一句"已使用"让人反查。
|
|
3406
|
+
await env.continuation
|
|
3407
|
+
.markStage(token, 'committed', { requestId: context.runtime.request_id })
|
|
3408
|
+
.catch(() => undefined);
|
|
3409
|
+
}
|
|
3410
|
+
catch (error) {
|
|
3411
|
+
if (isNetworkError(error)) {
|
|
3412
|
+
throw workflowPartialError('saveFormData 已成功,但 flow/create 结果失败或不确定;禁止自动重试。', { stage: 'flow/create', ...saveData }, { stage: 'flow/create' });
|
|
3413
|
+
}
|
|
3414
|
+
throw error;
|
|
3415
|
+
}
|
|
3416
|
+
const requestId = context.runtime.request_id;
|
|
3417
|
+
const requestName = prepared.requestName;
|
|
3418
|
+
const baseUrl = (await getSessionContext(env)).baseUrl;
|
|
3419
|
+
const result = {
|
|
3420
|
+
status: 'CREATED',
|
|
3421
|
+
message: '流程已创建并保存为草稿。',
|
|
3422
|
+
contextId: token,
|
|
3423
|
+
requestId,
|
|
3424
|
+
requestName,
|
|
3425
|
+
requestId_alt: requestId,
|
|
3426
|
+
requestMark: created.requestMark || null,
|
|
3427
|
+
requestName_alt: created.requestName || requestName,
|
|
3428
|
+
workflow: context.workflow,
|
|
3429
|
+
url: `${baseUrl}/sp/workflow/flowpage/view/${requestId}?operResultType=SUCCESS_RELOAD_CURRENT_PAGE&userSpecifiedNodeId=0&autoMaxDisplay=0`,
|
|
3430
|
+
};
|
|
3431
|
+
if (submitIntent) {
|
|
3432
|
+
if (verification.blocked) {
|
|
3433
|
+
result.status = 'CREATED';
|
|
3434
|
+
result.message = '流程已创建,但因节点字段校验未通过未提交审批,请调整后重新提交。';
|
|
3435
|
+
result.verification = verification;
|
|
3436
|
+
}
|
|
3437
|
+
else if (needConfirm && args.verificationConfirmed !== true) {
|
|
3438
|
+
result.status = 'CREATED';
|
|
3439
|
+
result.message = '流程已创建,节点字段校验通过。是否继续提交审批?(确认后请告知,将用 submit 命令提交)';
|
|
3440
|
+
result.needVerificationConfirm = true;
|
|
3441
|
+
result.verification = verification;
|
|
3442
|
+
result.nodeId = context.runtime.node_id;
|
|
3443
|
+
}
|
|
3444
|
+
else {
|
|
3445
|
+
result.status = 'CREATED_SUBMITTED';
|
|
3446
|
+
result.message = '流程已发起并提交审批。';
|
|
3447
|
+
result.verification = verification;
|
|
3448
|
+
}
|
|
3449
|
+
}
|
|
3450
|
+
return workflowSuccess('workflow.createApply', result);
|
|
3451
|
+
}
|
|
3452
|
+
// ---------------------------------------------------------------------------
|
|
3453
|
+
// editLoad / editPrepare / editSave
|
|
3454
|
+
// ---------------------------------------------------------------------------
|
|
3455
|
+
function buildEditDataLayoutRequest(requestId, workflowId, nodeId, formId, dataId, layoutMultiId, viewFormNodeId, permissionType, runtime) {
|
|
3456
|
+
return {
|
|
3457
|
+
terminalType: 'PC',
|
|
3458
|
+
dataId,
|
|
3459
|
+
formId,
|
|
3460
|
+
module: 'workflow',
|
|
3461
|
+
newLayout: true,
|
|
3462
|
+
cacheUid: null,
|
|
3463
|
+
layoutMultiId,
|
|
3464
|
+
dataCrossLayout: false,
|
|
3465
|
+
employeeId: runtime.employee_id,
|
|
3466
|
+
refId: workflowId,
|
|
3467
|
+
effectValues: [workflowId, nodeId],
|
|
3468
|
+
allReadOnly: false,
|
|
3469
|
+
initialDatas: [],
|
|
3470
|
+
dynamicData: false,
|
|
3471
|
+
cost: false,
|
|
3472
|
+
customParam: {
|
|
3473
|
+
permissionType,
|
|
3474
|
+
requestId,
|
|
3475
|
+
workflowId,
|
|
3476
|
+
nodeId,
|
|
3477
|
+
dataSrc: 'NEW',
|
|
3478
|
+
authSignatureStr: runtime.auth_signature,
|
|
3479
|
+
authStr: runtime.auth_str,
|
|
3480
|
+
readOnly: false,
|
|
3481
|
+
identityId: runtime.identity_id,
|
|
3482
|
+
identityType: Number(runtime.identity_type ?? 0),
|
|
3483
|
+
isTest: false,
|
|
3484
|
+
create: false,
|
|
3485
|
+
mobile: false,
|
|
3486
|
+
formId,
|
|
3487
|
+
dataId,
|
|
3488
|
+
source: 'view',
|
|
3489
|
+
isAgent: false,
|
|
3490
|
+
beAgentId: '0',
|
|
3491
|
+
hideSysFieldArea: true,
|
|
3492
|
+
viewFormNodeId,
|
|
3493
|
+
timestamp: Date.now(),
|
|
3494
|
+
turnTodoUser: [],
|
|
3495
|
+
sysFieldNewMode: true,
|
|
3496
|
+
isOverrideViewConfig: true,
|
|
3497
|
+
},
|
|
3498
|
+
orgAsyncPageQuery: true,
|
|
3499
|
+
};
|
|
3500
|
+
}
|
|
3501
|
+
async function commandEditLoad(args, env) {
|
|
3502
|
+
const requestId = String(args.requestId ?? '').trim();
|
|
3503
|
+
if (!requestId)
|
|
3504
|
+
throw workflowValidationError('request_id_required', 'requestId 必填');
|
|
3505
|
+
const query = { operResultType: 'SUCCESS_RELOAD_CURRENT_PAGE', userSpecifiedNodeId: '0', autoMaxDisplay: '0', isCreate: 0, requestId, ...AJAX_QUERY };
|
|
3506
|
+
const body = { operResultType: 'SUCCESS_RELOAD_CURRENT_PAGE', userSpecifiedNodeId: '0', autoMaxDisplay: '0', isCreate: 0, requestId };
|
|
3507
|
+
const baseResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadBase, query, body);
|
|
3508
|
+
const frameResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadFrame, query, body);
|
|
3509
|
+
const base = baseResp;
|
|
3510
|
+
const frameData = frameResp;
|
|
3511
|
+
const requestInfo = (base.requestInfo && typeof base.requestInfo === 'object') ? base.requestInfo : {};
|
|
3512
|
+
const runtime = parseRuntime(base, requestId);
|
|
3513
|
+
if (String(requestInfo.workflowId ?? '').trim())
|
|
3514
|
+
runtime.workflow_id = String(requestInfo.workflowId).trim();
|
|
3515
|
+
const frame = parseFrame(frameData, runtime.node_id);
|
|
3516
|
+
const workflowId = runtime.workflow_id;
|
|
3517
|
+
let minorInfo = {};
|
|
3518
|
+
try {
|
|
3519
|
+
const minorResp = await requestJson(env, 'POST', FORM_ENDPOINTS.loadMinor, { workflowId, isCreate: 1, ...AJAX_QUERY }, { workflowId, isCreate: 1, isMobile: 0 });
|
|
3520
|
+
minorInfo = parseMinorInfo(minorResp);
|
|
3521
|
+
}
|
|
3522
|
+
catch {
|
|
3523
|
+
minorInfo = {};
|
|
3524
|
+
}
|
|
3525
|
+
let tenantSetting = {};
|
|
3526
|
+
try {
|
|
3527
|
+
const settingResp = await requestJson(env, 'POST', FORM_ENDPOINTS.tenantSetting, undefined, { setType: [] });
|
|
3528
|
+
tenantSetting = parseTenantSetting(settingResp);
|
|
3529
|
+
}
|
|
3530
|
+
catch {
|
|
3531
|
+
tenantSetting = {};
|
|
3532
|
+
}
|
|
3533
|
+
const dataId = requestId;
|
|
3534
|
+
const dlResp = await requestJson(env, 'POST', formApiUrl(FORM_ENDPOINTS.formDataAndLayout, frame.relate_type), { formId: frame.form_id, dataId, ...AJAX_QUERY }, buildEditDataLayoutRequest(requestId, workflowId, runtime.node_id, frame.form_id, dataId, frame.layout_multi_id, frame.view_form_node_id, frame.permission_type, runtime));
|
|
3535
|
+
const dlData = dlResp;
|
|
3536
|
+
if (!dlData || typeof dlData !== 'object')
|
|
3537
|
+
throw workflowApiError('format_error', 'getFormDataAndLayout 未返回有效 data');
|
|
3538
|
+
const vrc = Array.isArray(dlData.viewRelateConfig) ? dlData.viewRelateConfig : [];
|
|
3539
|
+
const requiredMap = {};
|
|
3540
|
+
const disableEditMap = {};
|
|
3541
|
+
const displayMap = {};
|
|
3542
|
+
for (const item of vrc) {
|
|
3543
|
+
if (!item || typeof item !== 'object')
|
|
3544
|
+
continue;
|
|
3545
|
+
const fid = String(item.fieldId ?? '').trim();
|
|
3546
|
+
if (!fid)
|
|
3547
|
+
continue;
|
|
3548
|
+
requiredMap[fid] = Boolean(item.required);
|
|
3549
|
+
disableEditMap[fid] = Boolean(item.disableEdit);
|
|
3550
|
+
displayMap[fid] = item.display !== undefined ? Boolean(item.display) : true;
|
|
3551
|
+
}
|
|
3552
|
+
const fields = extractFields(dlData, requiredMap, disableEditMap, displayMap);
|
|
3553
|
+
const rawFieldsById = walkLayoutRawFields(dlData.formLayout);
|
|
3554
|
+
const attachConfigs = (dlData.attachConfigs && typeof dlData.attachConfigs === 'object') ? dlData.attachConfigs : {};
|
|
3555
|
+
const rawUploadFields = _rawUploadFieldsMap(rawFieldsById, attachConfigs);
|
|
3556
|
+
const formLayout = (dlData.formLayout && typeof dlData.formLayout === 'object') ? dlData.formLayout : {};
|
|
3557
|
+
const compReadonlyIds = new Set();
|
|
3558
|
+
const compDisableIds = new Set();
|
|
3559
|
+
const compsFieldIds = new Set();
|
|
3560
|
+
for (const page of formLayout.pageLayouts || []) {
|
|
3561
|
+
if (!page || typeof page !== 'object')
|
|
3562
|
+
continue;
|
|
3563
|
+
for (const comp of page.comps || []) {
|
|
3564
|
+
if (!comp || typeof comp !== 'object')
|
|
3565
|
+
continue;
|
|
3566
|
+
const cfg = comp.config || {};
|
|
3567
|
+
const fid = String(cfg.fieldId ?? '').trim();
|
|
3568
|
+
if (!fid)
|
|
3569
|
+
continue;
|
|
3570
|
+
compsFieldIds.add(fid);
|
|
3571
|
+
if (comp.readOnly)
|
|
3572
|
+
compReadonlyIds.add(fid);
|
|
3573
|
+
if (comp.disableEdit || cfg.disableEdit)
|
|
3574
|
+
compDisableIds.add(fid);
|
|
3575
|
+
}
|
|
3576
|
+
}
|
|
3577
|
+
const currentDetails = (dlData.formData && dlData.formData.dataDetails ? dlData.formData.dataDetails : [])
|
|
3578
|
+
.filter((i) => i && typeof i === 'object').map((i) => normalizeDetail(i)).filter(Boolean);
|
|
3579
|
+
const fd = (dlData.formData && typeof dlData.formData === 'object') ? dlData.formData : {};
|
|
3580
|
+
const newDataId = String(fd.id ?? dataId).trim();
|
|
3581
|
+
const updateTimeStamp = String(fd.updateTimeStamp ?? '').trim();
|
|
3582
|
+
const formObj = (dlData.form && typeof dlData.form === 'object') ? dlData.form : {};
|
|
3583
|
+
const layoutId = String(formLayout.id ?? '').trim();
|
|
3584
|
+
const editableIds = [];
|
|
3585
|
+
for (const field of fields) {
|
|
3586
|
+
const fid = String(field.id ?? '');
|
|
3587
|
+
const subFid = String(field.subFormId ?? '');
|
|
3588
|
+
if (subFid) {
|
|
3589
|
+
if (!compReadonlyIds.has(fid) && !compDisableIds.has(fid))
|
|
3590
|
+
editableIds.push(fid);
|
|
3591
|
+
continue;
|
|
3592
|
+
}
|
|
3593
|
+
if (field.readOnly || field.disableEdit)
|
|
3594
|
+
continue;
|
|
3595
|
+
if (field.display === false)
|
|
3596
|
+
continue;
|
|
3597
|
+
if (compsFieldIds.size && !compsFieldIds.has(fid))
|
|
3598
|
+
continue;
|
|
3599
|
+
editableIds.push(fid);
|
|
3600
|
+
}
|
|
3601
|
+
const session = await getSessionContext(env);
|
|
3602
|
+
const context = {
|
|
3603
|
+
kind: 'EDIT',
|
|
3604
|
+
version: 2,
|
|
3605
|
+
baseUrl: session.baseUrl,
|
|
3606
|
+
profile: session.profile,
|
|
3607
|
+
userId: session.userId,
|
|
3608
|
+
oaBaseUrl: session.baseUrl,
|
|
3609
|
+
requestId,
|
|
3610
|
+
workflow: {
|
|
3611
|
+
id: workflowId,
|
|
3612
|
+
name: runtime.workflow_name,
|
|
3613
|
+
description: runtime.workflow_description,
|
|
3614
|
+
type_name: runtime.workflow_type_name,
|
|
3615
|
+
},
|
|
3616
|
+
runtime,
|
|
3617
|
+
frame,
|
|
3618
|
+
form_id: String(formObj.id ?? frame.form_id),
|
|
3619
|
+
data_id: newDataId,
|
|
3620
|
+
layout_id: layoutId,
|
|
3621
|
+
update_time_stamp: updateTimeStamp,
|
|
3622
|
+
employee_id: runtime.employee_id,
|
|
3623
|
+
fields,
|
|
3624
|
+
currentDetails,
|
|
3625
|
+
rawUploadFields,
|
|
3626
|
+
attachConfigs,
|
|
3627
|
+
viewRelateConfig: vrc,
|
|
3628
|
+
viewLinkages: Array.isArray(dlData.viewLinkages) ? dlData.viewLinkages : [],
|
|
3629
|
+
linkageActions: Array.isArray(dlData.linkageActions) ? dlData.linkageActions : [],
|
|
3630
|
+
businessFields: Array.isArray(dlData.businessFields) ? dlData.businessFields : [],
|
|
3631
|
+
defaultDetails: (dlData.defaultDetails || []).filter((i) => i && typeof i === 'object').map((i) => normalizeDetail(i)).filter(Boolean),
|
|
3632
|
+
editableIds: [...new Set(editableIds)],
|
|
3633
|
+
requestInfo: {
|
|
3634
|
+
requestName: runtime.default_request_name || '',
|
|
3635
|
+
requestLevel: runtime.default_request_level || '',
|
|
3636
|
+
secLevel: runtime.default_sec_level || '',
|
|
3637
|
+
currentNodeName: String((base.requestInfo || {}).currentNodeName ?? ''),
|
|
3638
|
+
},
|
|
3639
|
+
minorInfo,
|
|
3640
|
+
tenantSetting,
|
|
3641
|
+
};
|
|
3642
|
+
const token = await env.continuation.issue({ operation: 'workflow.editLoad', baseUrl: session.baseUrl, profile: session.profile, userId: session.userId, payload: context }, CONTINUATION_TTL_MS);
|
|
3643
|
+
const fieldsById = new Map();
|
|
3644
|
+
for (const f of fields)
|
|
3645
|
+
fieldsById.set(String(f.id ?? ''), f);
|
|
3646
|
+
const slimFields = [];
|
|
3647
|
+
const folderOptionValues = _mainValueMap(currentDetails);
|
|
3648
|
+
for (const field of fields) {
|
|
3649
|
+
const item = { id: field.id, name: field.name, subFormId: field.subFormId, componentKey: field.componentKey };
|
|
3650
|
+
if (field.required)
|
|
3651
|
+
item.required = true;
|
|
3652
|
+
for (const key of ['isBrowser', 'isSelect', 'isMultiple', 'browserType', 'browserModule']) {
|
|
3653
|
+
if (field[key])
|
|
3654
|
+
item[key] = field[key];
|
|
3655
|
+
}
|
|
3656
|
+
if (field.readOnly || field.disableEdit) {
|
|
3657
|
+
item.readOnly = Boolean(field.readOnly);
|
|
3658
|
+
item.disableEdit = Boolean(field.disableEdit);
|
|
3659
|
+
}
|
|
3660
|
+
if (String(field.componentKey ?? '').toLowerCase() === 'filecomponent') {
|
|
3661
|
+
const fileFieldId = String(field.id ?? '');
|
|
3662
|
+
if (fileFieldId && !(field.readOnly || field.disableEdit)) {
|
|
3663
|
+
const rawFileField = rawFieldsById[fileFieldId] || {};
|
|
3664
|
+
const attachCfg = attachConfigs[fileFieldId];
|
|
3665
|
+
const [dirLayer] = _attachDirLayer(rawFileField, attachCfg);
|
|
3666
|
+
let optionFieldRaw = null;
|
|
3667
|
+
let optionValue = null;
|
|
3668
|
+
if (String(dirLayer.folderSelectType ?? '').trim() === '1') {
|
|
3669
|
+
item.folderDependsOnOption = true;
|
|
3670
|
+
const optionFieldId = String(dirLayer.optionId ?? '').trim();
|
|
3671
|
+
if (optionFieldId) {
|
|
3672
|
+
optionFieldRaw = rawFieldsById[optionFieldId];
|
|
3673
|
+
optionValue = folderOptionValues[optionFieldId];
|
|
3674
|
+
}
|
|
3675
|
+
}
|
|
3676
|
+
const [folderId, sizeLimitMb, formatLimit, maxNum] = resolveFileUploadConstraints(rawFileField, tenantSetting, attachCfg || null, optionFieldRaw, optionValue);
|
|
3677
|
+
item.fileUploadParams = buildFileUploadParams(runtime, frame, fileFieldId, false, minorInfo.sec_default_id || null, folderId, sizeLimitMb, formatLimit, maxNum, Boolean(field.readOnly || field.disableEdit));
|
|
3678
|
+
}
|
|
3679
|
+
}
|
|
3680
|
+
if (String(field.format ?? '').trim())
|
|
3681
|
+
item.format = String(field.format).trim();
|
|
3682
|
+
const options = field.options || [];
|
|
3683
|
+
if (options.length)
|
|
3684
|
+
item.options = options;
|
|
3685
|
+
slimFields.push(item);
|
|
3686
|
+
}
|
|
3687
|
+
const currentValues = [];
|
|
3688
|
+
for (const detail of currentDetails) {
|
|
3689
|
+
const fid = String((detail.formField && detail.formField.id) ?? '').trim();
|
|
3690
|
+
const field = fieldsById.get(fid);
|
|
3691
|
+
const row = {
|
|
3692
|
+
fieldId: fid,
|
|
3693
|
+
name: (field || {}).name ?? fid,
|
|
3694
|
+
subFormId: (field || {}).subFormId || (detail.subForm && detail.subForm.id) || '',
|
|
3695
|
+
value: _detailDisplayText(detail),
|
|
3696
|
+
};
|
|
3697
|
+
if (detail.dataIndex !== undefined)
|
|
3698
|
+
row.dataIndex = detail.dataIndex;
|
|
3699
|
+
if (detail.rowId)
|
|
3700
|
+
row.rowId = detail.rowId;
|
|
3701
|
+
currentValues.push(row);
|
|
3702
|
+
}
|
|
3703
|
+
// 可写字段补偿索引(同 createForm,见 encodeWritableFieldsCompact):只在 `fields`
|
|
3704
|
+
// 被整体移出 stdout 时随输出。只保留确实在本次字段清单里的 id,保证每行都有真实字段名。
|
|
3705
|
+
const slimFieldIdSet = new Set(slimFields.map((f) => String(f.id ?? '')));
|
|
3706
|
+
const editWritableIds = [...new Set((context.editableIds || []).map((x) => String(x)))]
|
|
3707
|
+
.filter((id) => slimFieldIdSet.has(id));
|
|
3708
|
+
const writableFieldsCompact = encodeWritableFieldsCompact(editWritableIds, slimFields, SYSTEM_FIELD_KEYS);
|
|
3709
|
+
return workflowSuccess('workflow.editLoad', await compactFormOutput({
|
|
3710
|
+
status: 'EDIT_READY',
|
|
3711
|
+
contextId: token,
|
|
3712
|
+
requestId,
|
|
3713
|
+
workflow: context.workflow,
|
|
3714
|
+
requestInfo: context.requestInfo,
|
|
3715
|
+
form: { id: context.form_id, layout_id: layoutId, data_id: newDataId },
|
|
3716
|
+
editableFields: context.editableIds,
|
|
3717
|
+
currentValues,
|
|
3718
|
+
fields: slimFields,
|
|
3719
|
+
continuation: token,
|
|
3720
|
+
}, env, 'last_edit_load.json', writableFieldsCompact));
|
|
3721
|
+
}
|
|
3722
|
+
function buildEditLinkageParam(context, dataList, runDataList) {
|
|
3723
|
+
const runtime = context.runtime || {};
|
|
3724
|
+
const frame = context.frame || {};
|
|
3725
|
+
return {
|
|
3726
|
+
formId: context.form_id,
|
|
3727
|
+
module: 'workflow',
|
|
3728
|
+
dataId: context.data_id,
|
|
3729
|
+
employeeId: context.employee_id || runtime.employee_id || '',
|
|
3730
|
+
customParam: {
|
|
3731
|
+
permissionType: Number(frame.permission_type ?? 1),
|
|
3732
|
+
requestId: context.request_id,
|
|
3733
|
+
workflowId: context.workflow.id,
|
|
3734
|
+
nodeId: runtime.node_id || '',
|
|
3735
|
+
dataSrc: 'NEW',
|
|
3736
|
+
authSignatureStr: runtime.auth_signature || '',
|
|
3737
|
+
authStr: runtime.auth_str || '',
|
|
3738
|
+
readOnly: false,
|
|
3739
|
+
identityId: runtime.identity_id || '',
|
|
3740
|
+
identityType: Number(runtime.identity_type ?? 0),
|
|
3741
|
+
isTest: false,
|
|
3742
|
+
create: false,
|
|
3743
|
+
mobile: false,
|
|
3744
|
+
formId: context.form_id,
|
|
3745
|
+
dataId: context.data_id,
|
|
3746
|
+
source: 'view',
|
|
3747
|
+
isAgent: false,
|
|
3748
|
+
beAgentId: '0',
|
|
3749
|
+
hideSysFieldArea: true,
|
|
3750
|
+
viewFormNodeId: frame.view_form_node_id || runtime.node_id || '',
|
|
3751
|
+
timestamp: Date.now(),
|
|
3752
|
+
turnTodoUser: [],
|
|
3753
|
+
sysFieldNewMode: true,
|
|
3754
|
+
isOverrideViewConfig: true,
|
|
3755
|
+
},
|
|
3756
|
+
businessFields: context.businessFields || [],
|
|
3757
|
+
moduleDataList: [],
|
|
3758
|
+
dataList,
|
|
3759
|
+
runDataList,
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
async function commandEditPrepare(args, env) {
|
|
3763
|
+
const token = String(args.context ?? '');
|
|
3764
|
+
if (!token)
|
|
3765
|
+
throw workflowValidationError('context_required', '需要 editLoad 返回的 continuation(context)');
|
|
3766
|
+
const lease = await verifyContextLease(env, token, 'workflow.editLoad');
|
|
3767
|
+
const context = lease.payload;
|
|
3768
|
+
if (context.kind !== 'EDIT')
|
|
3769
|
+
throw workflowValidationError('context_kind_invalid', '该上下文不是 edit 链路上下文(需先 edit-load)');
|
|
3770
|
+
const [main, detailValues] = splitInput(args.data);
|
|
3771
|
+
const fields = Array.isArray(context.fields) ? context.fields : [];
|
|
3772
|
+
const [entries, fieldAmbiguities] = bindInputFields(main, detailValues, fields);
|
|
3773
|
+
if (fieldAmbiguities.length) {
|
|
3774
|
+
return workflowSuccess('workflow.editPrepare', {
|
|
3775
|
+
status: 'NEEDS_CONFIRMATION',
|
|
3776
|
+
contextId: token,
|
|
3777
|
+
message: '存在重名或无法识别的表单字段,请改用字段 id。',
|
|
3778
|
+
ambiguities: fieldAmbiguities,
|
|
3779
|
+
});
|
|
3780
|
+
}
|
|
3781
|
+
const editableIds = new Set(context.editableIds || []);
|
|
3782
|
+
for (const entry of entries) {
|
|
3783
|
+
const fid = String((entry.field && entry.field.id) ?? '');
|
|
3784
|
+
if (fid && !editableIds.has(fid)) {
|
|
3785
|
+
return workflowSuccess('workflow.editPrepare', {
|
|
3786
|
+
status: 'NEEDS_CONFIRMATION',
|
|
3787
|
+
contextId: token,
|
|
3788
|
+
message: `字段 ${(entry.field && entry.field.name) || fid} 不可编辑(只读/禁用/不可见),不能修改。`,
|
|
3789
|
+
notEditable: { fieldId: fid, name: (entry.field && entry.field.name) || '' },
|
|
3790
|
+
});
|
|
3791
|
+
}
|
|
3792
|
+
}
|
|
3793
|
+
const [details, valueAmbiguities, preview] = await resolveEntryValues(env, entries, context.form_id);
|
|
3794
|
+
if (valueAmbiguities.length) {
|
|
3795
|
+
return workflowSuccess('workflow.editPrepare', {
|
|
3796
|
+
status: 'NEEDS_CONFIRMATION',
|
|
3797
|
+
contextId: token,
|
|
3798
|
+
message: '存在无法唯一解析的选项,或无法匹配的浏览按钮值,请让用户确认。',
|
|
3799
|
+
ambiguities: valueAmbiguities,
|
|
3800
|
+
linkageResult: '',
|
|
3801
|
+
});
|
|
3802
|
+
}
|
|
3803
|
+
const merged = mergeDetails(context.currentDetails || [], details);
|
|
3804
|
+
const linkageDetails = await runFieldLinkage(env, { ...linkageContextFromPayload(context), requestId: context.requestId, formId: context.form_id, dataId: context.data_id, workflow: context.workflow, businessFields: context.businessFields }, entries, null, merged, true);
|
|
3805
|
+
const merged2 = mergeLinkageDetails(merged, linkageDetails);
|
|
3806
|
+
const attachmentFolderIds = _attachmentFolderIds(context.rawUploadFields || {}, context.attachConfigs || {}, fields, _mainValueMap(merged2));
|
|
3807
|
+
const folderPayload = Object.keys(attachmentFolderIds).length ? { attachmentFolderIds } : {};
|
|
3808
|
+
const [hiddenSubforms, rowHidden, , rowDisabled] = computeLinkageVisibility(context.viewLinkages, entries, context.currentDetails || [], linkageDetails, context.systemFieldValues);
|
|
3809
|
+
const previewAll = [...preview];
|
|
3810
|
+
const fieldsById = new Map();
|
|
3811
|
+
for (const f of fields)
|
|
3812
|
+
fieldsById.set(String(f.id ?? ''), f);
|
|
3813
|
+
for (const detail of linkageDetails) {
|
|
3814
|
+
const fid = String((detail.formField && detail.formField.id) ?? '').trim();
|
|
3815
|
+
const field = fieldsById.get(fid);
|
|
3816
|
+
if (!field)
|
|
3817
|
+
continue;
|
|
3818
|
+
const subFid = String((detail.subForm && detail.subForm.id) ?? '').trim();
|
|
3819
|
+
if (subFid)
|
|
3820
|
+
continue;
|
|
3821
|
+
const displayText = _detailDisplayText(detail) || (detail.dataOptions || []).map((o) => String(o.content ?? '')).filter(Boolean).join('、');
|
|
3822
|
+
if (!displayText)
|
|
3823
|
+
continue;
|
|
3824
|
+
previewAll.push({ fieldId: fid, fieldName: field.name || fid, subFormId: '', value: displayText, linkage: true });
|
|
3825
|
+
}
|
|
3826
|
+
const session = await getSessionContext(env);
|
|
3827
|
+
const outToken = await env.continuation.issue({ operation: 'workflow.editPrepare', baseUrl: session.baseUrl, profile: session.profile, userId: session.userId, payload: { context, prepared: { dataDetails: merged2, preview: previewAll } } }, CONTINUATION_TTL_MS);
|
|
3828
|
+
return workflowSuccess('workflow.editPrepare', {
|
|
3829
|
+
status: 'READY',
|
|
3830
|
+
contextId: outToken,
|
|
3831
|
+
requestId: context.requestId,
|
|
3832
|
+
workflow: context.workflow,
|
|
3833
|
+
message: '表单修改已准备;确认后调用 edit-save 保存(不校验必填,仅保存表单字段)。',
|
|
3834
|
+
changedFields: preview,
|
|
3835
|
+
linkageResult: linkageResultNotice(linkageDetails, fields, hiddenSubforms, rowHidden, new Set(context.hiddenCellFieldIds || []), new Set(context.compsFieldIds || []), entries, new Set(context.layoutSubformIds || [])),
|
|
3836
|
+
...folderPayload,
|
|
3837
|
+
confirmationRequired: true,
|
|
3838
|
+
continuation: outToken,
|
|
3839
|
+
});
|
|
3840
|
+
}
|
|
3841
|
+
function buildEditSaveBody(context, prepared) {
|
|
3842
|
+
const runtime = context.runtime || {};
|
|
3843
|
+
return {
|
|
3844
|
+
module: 'workflow',
|
|
3845
|
+
customParam: {
|
|
3846
|
+
isCreate: false,
|
|
3847
|
+
requestId: context.request_id,
|
|
3848
|
+
isTest: false,
|
|
3849
|
+
secretLevel: '0',
|
|
3850
|
+
secretLevelValidity: '',
|
|
3851
|
+
nodeId: runtime.node_id || '',
|
|
3852
|
+
workflowId: context.workflow.id,
|
|
3853
|
+
type: 23,
|
|
3854
|
+
authSignatureStr: runtime.auth_signature || '',
|
|
3855
|
+
authStr: runtime.auth_str || '',
|
|
3856
|
+
identityId: runtime.identity_id || '',
|
|
3857
|
+
identityType: Number(runtime.identity_type ?? 0),
|
|
3858
|
+
monitorEditFormData: false,
|
|
3859
|
+
},
|
|
3860
|
+
employeeId: context.employee_id || runtime.employee_id || '',
|
|
3861
|
+
formData: {
|
|
3862
|
+
id: context.data_id,
|
|
3863
|
+
updateTimeStamp: context.update_time_stamp || '',
|
|
3864
|
+
module: 'workflow',
|
|
3865
|
+
dataDelRows: [],
|
|
3866
|
+
dataDetails: prepared.dataDetails || [],
|
|
3867
|
+
client: 'pc',
|
|
3868
|
+
dataStatus: 'temporary',
|
|
3869
|
+
form: { id: context.form_id },
|
|
3870
|
+
formLayout: { id: context.layout_id || '' },
|
|
3871
|
+
},
|
|
3872
|
+
};
|
|
3873
|
+
}
|
|
3874
|
+
async function commandEditSave(args, env) {
|
|
3875
|
+
if (args.confirm !== true)
|
|
3876
|
+
throw workflowValidationError('confirm_required', 'edit-save 需要 confirm=true(用户已明确确认本次修改)');
|
|
3877
|
+
const token = String(args.context ?? '');
|
|
3878
|
+
if (!token)
|
|
3879
|
+
throw workflowValidationError('context_required', '需要 editPrepare 返回的 continuation(context)');
|
|
3880
|
+
const lease = await verifyContextLease(env, token, 'workflow.editPrepare', true);
|
|
3881
|
+
const { context, prepared } = lease.payload;
|
|
3882
|
+
if (context.kind !== 'EDIT')
|
|
3883
|
+
throw workflowValidationError('context_kind_invalid', '该上下文不是 edit 链路上下文');
|
|
3884
|
+
if (Array.isArray(context.linkageActions) && context.linkageActions.length) {
|
|
3885
|
+
const saveDetails = prepared.dataDetails || [];
|
|
3886
|
+
const saveEntries = _entriesFromDefaults(saveDetails);
|
|
3887
|
+
if (saveEntries.length) {
|
|
3888
|
+
try {
|
|
3889
|
+
const saveLinkage = await runFieldLinkage(env, { ...linkageContextFromPayload(context), requestId: context.requestId, formId: context.form_id, dataId: context.data_id, workflow: context.workflow, businessFields: context.businessFields }, saveEntries, new Set(['EVENT_SAVE_DATA']), saveDetails, true);
|
|
3890
|
+
if (saveLinkage.length)
|
|
3891
|
+
prepared.dataDetails = mergeLinkageDetails(saveDetails, saveLinkage);
|
|
3892
|
+
}
|
|
3893
|
+
catch {
|
|
3894
|
+
/* 跳过 */
|
|
3895
|
+
}
|
|
3896
|
+
}
|
|
3897
|
+
}
|
|
3898
|
+
// 阶段推进到"要发写入请求了":此后再被中止/杀掉,重放只能按**结果不确定**处理(先只读核对),
|
|
3899
|
+
// 而在此之前被中止则允许安全重试同一句柄(与 createApply 对称)。
|
|
3900
|
+
await env.continuation.markStage(token, 'saving');
|
|
3901
|
+
let data;
|
|
3902
|
+
try {
|
|
3903
|
+
const saveResponse = await env.host.workflowRequest({
|
|
3904
|
+
method: 'POST',
|
|
3905
|
+
path: formApiUrl(FORM_ENDPOINTS.save, context.frame.relate_type),
|
|
3906
|
+
query: { formId: context.form_id, ...AJAX_QUERY },
|
|
3907
|
+
body: buildEditSaveBody(context, resolvePrepared(context, prepared)),
|
|
3908
|
+
operation: 'workflow.editSave',
|
|
3909
|
+
});
|
|
3910
|
+
data = responseData(saveResponse);
|
|
3911
|
+
}
|
|
3912
|
+
catch (error) {
|
|
3913
|
+
if (isNetworkError(error)) {
|
|
3914
|
+
throw workflowPartialError('saveFormData 请求发送后连接中断,是否已保存不确定,禁止自动重试,请重新查询流程表单确认', { stage: 'saveFormData' }, { stage: 'saveFormData' });
|
|
3915
|
+
}
|
|
3916
|
+
throw error;
|
|
3917
|
+
}
|
|
3918
|
+
// responseData 已解包一层,此处不可再取 .data
|
|
3919
|
+
const saveData = (data && typeof data === 'object') ? data : null;
|
|
3920
|
+
if (!saveData || typeof saveData !== 'object' || !String(saveData.id ?? '').trim()) {
|
|
3921
|
+
throw workflowPartialError('saveFormData 返回异常,禁止自动重试', { stage: 'saveFormData' });
|
|
3922
|
+
}
|
|
3923
|
+
const newTimeStamp = String(saveData.updateTimeStamp ?? context.update_time_stamp ?? '');
|
|
3924
|
+
// saveFormData 已返回成功:阶段推进到 committed。若进程在"回写成功结果"之前被中止,
|
|
3925
|
+
// 重放可据此报幂等成功(ALREADY_APPLIED + requestId),而不是只报一句"已使用"让人反查。
|
|
3926
|
+
await env.continuation
|
|
3927
|
+
.markStage(token, 'committed', { requestId: context.request_id })
|
|
3928
|
+
.catch(() => undefined);
|
|
3929
|
+
return workflowSuccess('workflow.editSave', {
|
|
3930
|
+
status: 'EDIT_SAVED',
|
|
3931
|
+
contextId: token,
|
|
3932
|
+
requestId: context.request_id,
|
|
3933
|
+
workflow: context.workflow,
|
|
3934
|
+
message: '流程表单内容已保存(仅保存表单字段,未提交流程)。',
|
|
3935
|
+
saved: { data_id: String(saveData.id ?? ''), updateTimeStamp: newTimeStamp, recordId: String(saveData.recordId ?? '') },
|
|
3936
|
+
saveLinkage: Boolean(resolvePrepared(context, prepared).dataDetails?.length),
|
|
3937
|
+
});
|
|
3938
|
+
}
|
|
3939
|
+
// ---------------------------------------------------------------------------
|
|
3940
|
+
// createList(查询可发起工作流)
|
|
3941
|
+
// ---------------------------------------------------------------------------
|
|
3942
|
+
async function commandCreateList(args, env) {
|
|
3943
|
+
const response = await requestJson(env, 'POST', FORM_ENDPOINTS.workflowCreateList, AJAX_QUERY, { wfpBelongTypeCode: '1', loadOfsFlow: '0', clientCtrl: 1 });
|
|
3944
|
+
// requestJson 内部已解包一层(responseData),此处不可再取 .data
|
|
3945
|
+
const data = (response && typeof response === 'object') ? response : null;
|
|
3946
|
+
if (!data || typeof data !== 'object')
|
|
3947
|
+
throw workflowApiError('format_error', '工作流列表返回 data 格式错误');
|
|
3948
|
+
const workflowId = String(args.workflowId ?? '').trim();
|
|
3949
|
+
if (workflowId) {
|
|
3950
|
+
const authorized = simplifyWorkflows(data);
|
|
3951
|
+
const exact = authorized.filter((item) => String(item.id ?? '').trim() === workflowId);
|
|
3952
|
+
if (exact.length) {
|
|
3953
|
+
return workflowSuccess('workflow.createList', {
|
|
3954
|
+
status: 'SELECTED',
|
|
3955
|
+
workflow: exact[0],
|
|
3956
|
+
selectionSource: 'workflowId',
|
|
3957
|
+
message: '已按记忆中的 workflowId 精确确认可发起工作流,无需用户选择。',
|
|
3958
|
+
});
|
|
3959
|
+
}
|
|
3960
|
+
return workflowSuccess('workflow.createList', {
|
|
3961
|
+
status: 'NOT_FOUND',
|
|
3962
|
+
query: args.name || '',
|
|
3963
|
+
workflowId,
|
|
3964
|
+
message: '记忆中的 workflowId 不在当前用户可发起工作流列表中,可能已无发起权限或工作流已停用。',
|
|
3965
|
+
workflows: [],
|
|
3966
|
+
});
|
|
3967
|
+
}
|
|
3968
|
+
const result = simplifyWorkflows(data, String(args.name ?? ''));
|
|
3969
|
+
if (!result.length) {
|
|
3970
|
+
return workflowSuccess('workflow.createList', { status: 'NOT_FOUND', query: args.name || '', workflows: [] });
|
|
3971
|
+
}
|
|
3972
|
+
if (result.length === 1) {
|
|
3973
|
+
return workflowSuccess('workflow.createList', { status: 'SELECTED', workflow: result[0] });
|
|
3974
|
+
}
|
|
3975
|
+
// 多命中时**先按全名精确匹配优选**:候选里恰好有一个名字与用户所写完全相同的(如"总部费用报销"
|
|
3976
|
+
// 对上「总部费用报销 / 总部费用报销流程 / 总部费用报销(自定义)」),就直接选中它,不让用户
|
|
3977
|
+
// 为"是不是要多打几个字"再选一次。这与查询侧 `resolveTerm` 的候选优选是**同一套判据**。
|
|
3978
|
+
// 只有在"没有任何候选与所写完全一致"(或完全一致的有多个、仍是歧义)时才请用户选择。
|
|
3979
|
+
const keyword = String(args.name ?? '').trim().toLowerCase();
|
|
3980
|
+
const exact = keyword
|
|
3981
|
+
? result.filter((item) => String(item.workflowname ?? '').trim().toLowerCase() === keyword)
|
|
3982
|
+
: [];
|
|
3983
|
+
if (exact.length === 1) {
|
|
3984
|
+
return workflowSuccess('workflow.createList', {
|
|
3985
|
+
status: 'SELECTED',
|
|
3986
|
+
workflow: exact[0],
|
|
3987
|
+
selectionSource: 'exactName',
|
|
3988
|
+
message: `名称与「${args.name}」完全一致的工作流只有 1 个,已直接选中,无需用户选择。`,
|
|
3989
|
+
});
|
|
3990
|
+
}
|
|
3991
|
+
const selected = exact.length ? exact : result;
|
|
3992
|
+
return workflowSuccess('workflow.createList', {
|
|
3993
|
+
status: 'NEEDS_SELECTION',
|
|
3994
|
+
message: '匹配到多个工作流,且其中没有与所写名称完全一致的,请让用户选择后再继续。',
|
|
3995
|
+
query: args.name || '',
|
|
3996
|
+
workflows: selected,
|
|
3997
|
+
});
|
|
3998
|
+
}
|
|
3999
|
+
function simplifyWorkflows(data, name = '') {
|
|
4000
|
+
const groups = Array.isArray(data.listDataGroup) ? data.listDataGroup : [];
|
|
4001
|
+
const typeNames = {};
|
|
4002
|
+
for (const item of groups) {
|
|
4003
|
+
if (item && typeof item === 'object')
|
|
4004
|
+
typeNames[String(item.id)] = String(item.typename ?? '');
|
|
4005
|
+
}
|
|
4006
|
+
const items = Array.isArray(data.listData) ? data.listData : [];
|
|
4007
|
+
const keyword = name.trim().toLowerCase();
|
|
4008
|
+
const result = [];
|
|
4009
|
+
const seen = new Set();
|
|
4010
|
+
for (const item of items) {
|
|
4011
|
+
if (!item || typeof item !== 'object')
|
|
4012
|
+
continue;
|
|
4013
|
+
const workflowId = String(item.workflowid ?? item.id ?? '').trim();
|
|
4014
|
+
const wfName = String(item.workflowname ?? item.workflowName ?? '').trim();
|
|
4015
|
+
if (!workflowId || !wfName || seen.has(workflowId))
|
|
4016
|
+
continue;
|
|
4017
|
+
if (keyword && !wfName.toLowerCase().includes(keyword))
|
|
4018
|
+
continue;
|
|
4019
|
+
seen.add(workflowId);
|
|
4020
|
+
result.push({
|
|
4021
|
+
id: workflowId,
|
|
4022
|
+
workflowname: wfName,
|
|
4023
|
+
workflowdesc: String(item.workflowdesc ?? ''),
|
|
4024
|
+
typename: typeNames[String(item.type)] ?? '',
|
|
4025
|
+
});
|
|
4026
|
+
}
|
|
4027
|
+
result.sort((a, b) => (a.typename + a.workflowname + a.id).localeCompare(b.typename + b.workflowname + b.id));
|
|
4028
|
+
return result;
|
|
4029
|
+
}
|
|
4030
|
+
// ---------------------------------------------------------------------------
|
|
4031
|
+
// operation 定义
|
|
4032
|
+
// ---------------------------------------------------------------------------
|
|
4033
|
+
const workflowCreateList = {
|
|
4034
|
+
name: 'workflow.createList',
|
|
4035
|
+
resource: 'workflow',
|
|
4036
|
+
method: 'createList',
|
|
4037
|
+
action: 'workflow.createList',
|
|
4038
|
+
summary: '查询当前用户可发起的工作流列表(按名称或记忆 workflowId 精确确认)',
|
|
4039
|
+
capability: 'create',
|
|
4040
|
+
risk: 'read',
|
|
4041
|
+
requiresConfirmation: false,
|
|
4042
|
+
inputSchema: schemaOf({
|
|
4043
|
+
name: { type: 'string', description: '工作流名称关键字(模糊匹配)' },
|
|
4044
|
+
workflowId: { type: 'string', description: '记忆中已确认的工作流 ID;传入时按当前用户可发起列表精确校验并优先选中' },
|
|
4045
|
+
}, { required: [] }),
|
|
4046
|
+
handler: (args, env) => commandCreateList(args, env),
|
|
4047
|
+
};
|
|
4048
|
+
const workflowCreateForm = {
|
|
4049
|
+
name: 'workflow.createForm',
|
|
4050
|
+
resource: 'workflow',
|
|
4051
|
+
method: 'createForm',
|
|
4052
|
+
action: 'workflow.createForm',
|
|
4053
|
+
summary: '初始化发起流程表单:loadBase/loadFrame/loadMinor/formLayout 合并为 runtime 与 frame,产出字段清单 + fileUploadParams(不落业务数据)',
|
|
4054
|
+
capability: 'create',
|
|
4055
|
+
risk: 'read',
|
|
4056
|
+
requiresConfirmation: false,
|
|
4057
|
+
inputSchema: schemaOf({
|
|
4058
|
+
workflowId: SCHEMA_REQUEST_ID,
|
|
4059
|
+
relatesTo: { type: 'string', description: '可选关联对象 id(仅透传,不影响表单初始化)' },
|
|
4060
|
+
}, { required: ['workflowId'] }),
|
|
4061
|
+
handler: (args, env) => commandCreateForm(args, env),
|
|
4062
|
+
};
|
|
4063
|
+
const workflowCreatePrepare = {
|
|
4064
|
+
name: 'workflow.createPrepare',
|
|
4065
|
+
resource: 'workflow',
|
|
4066
|
+
method: 'createPrepare',
|
|
4067
|
+
action: 'workflow.createPrepare',
|
|
4068
|
+
summary: '收集并准备表单数据:绑定字段、解析选项/浏览按钮、校验只读字段、触发字段联动,产出确认预览 + continuation(不落业务数据)',
|
|
4069
|
+
capability: 'create',
|
|
4070
|
+
risk: 'read-before-write',
|
|
4071
|
+
requiresConfirmation: true,
|
|
4072
|
+
inputSchema: schemaOf({
|
|
4073
|
+
context: SCHEMA_CONTINUATION,
|
|
4074
|
+
data: {
|
|
4075
|
+
type: 'object',
|
|
4076
|
+
description: '表单字段值,结构 {"main":{...},"details":{"<subFormId>":[{...}]}}',
|
|
4077
|
+
additionalProperties: true,
|
|
4078
|
+
},
|
|
4079
|
+
requestName: { type: 'string', description: '流程标题(form 返回 requestName 非空时直接用其值;为空时由模型按内容生成)' },
|
|
4080
|
+
requestLevel: { type: 'string', description: '紧急程度(取 systemFields.levelOptions 的 code)' },
|
|
4081
|
+
secLevel: { type: 'string', description: '密级(取 systemFields.secLevelOptions 的 id)' },
|
|
4082
|
+
remark: { type: 'string', description: '签字意见' },
|
|
4083
|
+
intent: { type: 'string', enum: ['submit', 'draft'], description: '意图:submit=提交审批(必填缺失阻断);draft=仅保存草稿(必填缺失不阻断)' },
|
|
4084
|
+
}, { required: ['context', 'data', 'intent'] }),
|
|
4085
|
+
handler: (args, env) => commandCreatePrepare(args, env),
|
|
4086
|
+
};
|
|
4087
|
+
const workflowCreateApply = {
|
|
4088
|
+
name: 'workflow.createApply',
|
|
4089
|
+
resource: 'workflow',
|
|
4090
|
+
method: 'createApply',
|
|
4091
|
+
action: 'workflow.createApply',
|
|
4092
|
+
summary: '明确确认后发起流程:写前复核只读字段 → build_save_body → fdLinkage → saveFormData → flow/create;结果不确定抛 partial error 禁止重试',
|
|
4093
|
+
capability: 'create',
|
|
4094
|
+
risk: 'high-risk-write',
|
|
4095
|
+
requiresConfirmation: true,
|
|
4096
|
+
inputSchema: schemaOf({
|
|
4097
|
+
context: SCHEMA_CONTINUATION,
|
|
4098
|
+
confirm: SCHEMA_CONFIRM,
|
|
4099
|
+
intent: { type: 'string', enum: ['submit', 'draft'], description: '意图(必填,独立于 prepare 的 intent):submit=创建并提交审批;draft=仅保存草稿' },
|
|
4100
|
+
question: { type: 'string', description: '用户发起时的原话(记忆保存参考)' },
|
|
4101
|
+
verificationConfirmed: { type: 'boolean', description: '节点字段校验需用户确认时显式确认继续提交' },
|
|
4102
|
+
}, { required: ['context', 'confirm', 'intent'] }),
|
|
4103
|
+
handler: (args, env) => commandCreateApply(args, env),
|
|
4104
|
+
};
|
|
4105
|
+
const workflowEditLoad = {
|
|
4106
|
+
name: 'workflow.editLoad',
|
|
4107
|
+
resource: 'workflow',
|
|
4108
|
+
method: 'editLoad',
|
|
4109
|
+
action: 'workflow.editLoad',
|
|
4110
|
+
summary: '加载已存在流程表单:formDataAndLayout 返回当前值 + 可编辑字段 + 只读标记',
|
|
4111
|
+
capability: 'edit',
|
|
4112
|
+
risk: 'read',
|
|
4113
|
+
requiresConfirmation: false,
|
|
4114
|
+
inputSchema: schemaOf({ requestId: SCHEMA_REQUEST_ID }, { required: ['requestId'] }),
|
|
4115
|
+
handler: (args, env) => commandEditLoad(args, env),
|
|
4116
|
+
};
|
|
4117
|
+
const workflowEditPrepare = {
|
|
4118
|
+
name: 'workflow.editPrepare',
|
|
4119
|
+
resource: 'workflow',
|
|
4120
|
+
method: 'editPrepare',
|
|
4121
|
+
action: 'workflow.editPrepare',
|
|
4122
|
+
summary: '修改流程表单字段并触发字段联动(不校验必填),产出预览 + continuation',
|
|
4123
|
+
capability: 'edit',
|
|
4124
|
+
risk: 'read-before-write',
|
|
4125
|
+
requiresConfirmation: true,
|
|
4126
|
+
inputSchema: schemaOf({
|
|
4127
|
+
context: SCHEMA_CONTINUATION,
|
|
4128
|
+
data: {
|
|
4129
|
+
type: 'object',
|
|
4130
|
+
description: '要修改的字段值,结构 {"main":{...},"details":{"<subFormId>":[{...}]}}',
|
|
4131
|
+
additionalProperties: true,
|
|
4132
|
+
},
|
|
4133
|
+
}, { required: ['context', 'data'] }),
|
|
4134
|
+
handler: (args, env) => commandEditPrepare(args, env),
|
|
4135
|
+
};
|
|
4136
|
+
const workflowEditApply = {
|
|
4137
|
+
name: 'workflow.editApply',
|
|
4138
|
+
resource: 'workflow',
|
|
4139
|
+
method: 'editApply',
|
|
4140
|
+
action: 'workflow.editApply',
|
|
4141
|
+
summary: '明确确认后保存修改后的表单字段(仅 saveFormData,不调 flow/save/submit);不确定结果抛 partial error',
|
|
4142
|
+
capability: 'edit',
|
|
4143
|
+
risk: 'high-risk-write',
|
|
4144
|
+
requiresConfirmation: true,
|
|
4145
|
+
inputSchema: schemaOf({
|
|
4146
|
+
context: SCHEMA_CONTINUATION,
|
|
4147
|
+
confirm: SCHEMA_CONFIRM,
|
|
4148
|
+
}, { required: ['context', 'confirm'] }),
|
|
4149
|
+
handler: (args, env) => commandEditSave(args, env),
|
|
4150
|
+
};
|
|
4151
|
+
export const workflowFormOperations = [
|
|
4152
|
+
workflowCreateList,
|
|
4153
|
+
workflowCreateForm,
|
|
4154
|
+
workflowCreatePrepare,
|
|
4155
|
+
workflowCreateApply,
|
|
4156
|
+
workflowEditLoad,
|
|
4157
|
+
workflowEditPrepare,
|
|
4158
|
+
workflowEditApply,
|
|
4159
|
+
];
|