@stalfh233/omc-cli 0.0.0-stage → 0.4.10
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/LICENSE +21 -0
- package/README.md +293 -3
- package/coverage/m1-coverage-manifest-v1.json +1278 -0
- package/dist/approval-token.js +102 -0
- package/dist/args.js +25 -0
- package/dist/artifacts.js +119 -0
- package/dist/bench/call-face-eval.js +256 -0
- package/dist/bench/context-attribution.js +151 -0
- package/dist/bench/discovery-cost-eval.js +230 -0
- package/dist/bench/driver.js +81 -0
- package/dist/bench/evals.js +236 -0
- package/dist/bench/fake-http-server.js +65 -0
- package/dist/bench/instrument.js +87 -0
- package/dist/bench/intent-face-eval.js +343 -0
- package/dist/bench/run.js +166 -0
- package/dist/bench/scenario.js +343 -0
- package/dist/bench/types.js +76 -0
- package/dist/bi-wire.js +41 -0
- package/dist/bizservice-config.js +581 -0
- package/dist/call.js +153 -0
- package/dist/capability-absences.js +23 -0
- package/dist/capability-overview.js +492 -0
- package/dist/capability-shape.js +154 -0
- package/dist/cli-contract.js +70 -0
- package/dist/cli-output.js +73 -0
- package/dist/cli.js +1418 -0
- package/dist/code-rules.js +69 -0
- package/dist/command-transport.js +155 -0
- package/dist/config-store.js +195 -0
- package/dist/context.js +21 -0
- package/dist/contract-consistency.js +66 -0
- package/dist/contract-resources.js +62 -0
- package/dist/coverage-consistency.js +62 -0
- package/dist/coverage-registry.js +76 -0
- package/dist/coverage.js +130 -0
- package/dist/data-list-filter.js +114 -0
- package/dist/discovery.js +390 -0
- package/dist/endpoints.js +154 -0
- package/dist/environment-policy.js +26 -0
- package/dist/execution-metadata.js +1092 -0
- package/dist/fake/app.js +45 -0
- package/dist/fake/b2-registration.js +594 -0
- package/dist/fake/businessrule.js +242 -0
- package/dist/fake/datarule.js +72 -0
- package/dist/fake/dictionary.js +108 -0
- package/dist/fake/environment.js +30 -0
- package/dist/fake/field.js +167 -0
- package/dist/fake/form.js +101 -0
- package/dist/fake/index.js +121 -0
- package/dist/fake/list-view.js +289 -0
- package/dist/fake/model.js +136 -0
- package/dist/fake/online-js.js +18 -0
- package/dist/fake/report.js +253 -0
- package/dist/fake/routes.js +47 -0
- package/dist/fake/rule-lifecycle.js +37 -0
- package/dist/fake/runtime-data.js +367 -0
- package/dist/fake/state.js +67 -0
- package/dist/fake/workflow.js +364 -0
- package/dist/field-change.js +200 -0
- package/dist/field-families.js +896 -0
- package/dist/form-layout.js +111 -0
- package/dist/form-support.js +821 -0
- package/dist/goal-routes.js +468 -0
- package/dist/governed-execution.js +87 -0
- package/dist/human-summary.js +212 -0
- package/dist/identity.js +62 -0
- package/dist/intent/baseline.js +57 -0
- package/dist/intent/capabilities/bizservice.js +274 -0
- package/dist/intent/capabilities/businessrule.js +493 -0
- package/dist/intent/capabilities/datarule.js +187 -0
- package/dist/intent/capabilities/field.js +545 -0
- package/dist/intent/capabilities/form.js +136 -0
- package/dist/intent/capabilities/index.js +64 -0
- package/dist/intent/capabilities/listview.js +157 -0
- package/dist/intent/capabilities/model.js +100 -0
- package/dist/intent/capabilities/onlinejs.js +108 -0
- package/dist/intent/capabilities/report.js +355 -0
- package/dist/intent/capabilities/workflow.js +458 -0
- package/dist/intent/capability.js +6 -0
- package/dist/intent/cli.js +91 -0
- package/dist/intent/compare.js +56 -0
- package/dist/intent/compiler.js +79 -0
- package/dist/intent/dsl.js +129 -0
- package/dist/intent/plan-file.js +63 -0
- package/dist/intent/readback.js +65 -0
- package/dist/intent/schema.js +158 -0
- package/dist/intent/validation.js +30 -0
- package/dist/intent/yaml.js +315 -0
- package/dist/json-column.js +68 -0
- package/dist/lanes/app-contract.js +95 -0
- package/dist/lanes/app-coverage.js +16 -0
- package/dist/lanes/app.js +174 -0
- package/dist/lanes/apply-changes.js +231 -0
- package/dist/lanes/b2-registration-contract.js +292 -0
- package/dist/lanes/b2-registration-coverage.js +48 -0
- package/dist/lanes/b2-registration.js +1187 -0
- package/dist/lanes/businessrule-contract.js +232 -0
- package/dist/lanes/businessrule-coverage.js +16 -0
- package/dist/lanes/businessrule.js +221 -0
- package/dist/lanes/contract-support.js +65 -0
- package/dist/lanes/coverage-declaration.js +9 -0
- package/dist/lanes/datarule-contract.js +155 -0
- package/dist/lanes/datarule-coverage.js +19 -0
- package/dist/lanes/datarule-protocol.js +308 -0
- package/dist/lanes/datarule.js +818 -0
- package/dist/lanes/dictionary-contract.js +78 -0
- package/dist/lanes/dictionary-coverage.js +24 -0
- package/dist/lanes/dictionary.js +235 -0
- package/dist/lanes/environment-contract.js +70 -0
- package/dist/lanes/environment-coverage.js +14 -0
- package/dist/lanes/environment.js +163 -0
- package/dist/lanes/field-contract.js +223 -0
- package/dist/lanes/field-coverage.js +27 -0
- package/dist/lanes/field.js +374 -0
- package/dist/lanes/form-contract.js +97 -0
- package/dist/lanes/form-coverage.js +16 -0
- package/dist/lanes/form.js +185 -0
- package/dist/lanes/lane-ids.js +34 -0
- package/dist/lanes/list-view-contract.js +175 -0
- package/dist/lanes/list-view-coverage.js +20 -0
- package/dist/lanes/list-view-shapes.js +1207 -0
- package/dist/lanes/list-view.js +578 -0
- package/dist/lanes/meta-contract.js +85 -0
- package/dist/lanes/meta.js +255 -0
- package/dist/lanes/model-contract.js +168 -0
- package/dist/lanes/model-coverage.js +20 -0
- package/dist/lanes/model.js +986 -0
- package/dist/lanes/online-js-contract.js +99 -0
- package/dist/lanes/online-js-coverage.js +28 -0
- package/dist/lanes/online-js.js +127 -0
- package/dist/lanes/report-contract.js +144 -0
- package/dist/lanes/report-coverage.js +21 -0
- package/dist/lanes/report.js +476 -0
- package/dist/lanes/rule-graph.js +1846 -0
- package/dist/lanes/rule-lifecycle-contract.js +92 -0
- package/dist/lanes/rule-lifecycle-coverage.js +20 -0
- package/dist/lanes/rule-lifecycle.js +176 -0
- package/dist/lanes/runtime-data-contract.js +264 -0
- package/dist/lanes/runtime-data-coverage.js +25 -0
- package/dist/lanes/runtime-data.js +1054 -0
- package/dist/lanes/workflow-contract.js +223 -0
- package/dist/lanes/workflow-coverage.js +40 -0
- package/dist/lanes/workflow.js +1813 -0
- package/dist/online-js-layout.js +58 -0
- package/dist/online-js-source.js +276 -0
- package/dist/package-tool.js +51 -0
- package/dist/package.js +73 -0
- package/dist/plan.js +73 -0
- package/dist/read.js +144 -0
- package/dist/redact.js +28 -0
- package/dist/rule-support.js +587 -0
- package/dist/runtime-support.js +134 -0
- package/dist/server.js +92 -0
- package/dist/session-manager.js +30 -0
- package/dist/session.js +149 -0
- package/dist/skills.js +112 -0
- package/dist/support.js +98 -0
- package/dist/tool-types.js +127 -0
- package/dist/tools.js +59 -0
- package/dist/usage-log.js +197 -0
- package/dist/wire.js +287 -0
- package/dist/workflow-support.js +99 -0
- package/dist/write-lease.js +26 -0
- package/dist/write-lock.js +109 -0
- package/dist/write.js +176 -0
- package/dist/zip.js +156 -0
- package/docs/tool-surface-map.md +42 -0
- package/package.json +71 -6
- package/skills/omc-acceptance-criteria.md +47 -0
- package/skills/omc-business-configuration.md +257 -0
- package/skills/omc-capabilities.md +197 -0
- package/skills/omc-capability-scouting.md +55 -0
- package/skills/omc-config-draft-review.md +171 -0
- package/skills/omc-five-piece-flow.md +35 -0
- package/skills/omc-glossary.md +86 -0
- package/skills/omc-refusals.md +110 -0
- package/skills/omc-requirement-analysis.md +161 -0
- package/skills/omc-requirement-vocabulary.md +51 -0
- package/skills/omc-start-here.md +82 -0
- package/skills/omc-tool-selection.md +119 -0
- package/skills/omc-write-hazards.md +87 -0
|
@@ -0,0 +1,1813 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ENDPOINTS } from "../endpoints.js";
|
|
3
|
+
import { capabilityArg, environmentArg } from "../args.js";
|
|
4
|
+
import { governedRead } from "../read.js";
|
|
5
|
+
import { ToolRefusalError } from "../tool-types.js";
|
|
6
|
+
import { writeTool } from "../write.js";
|
|
7
|
+
import { registerPackageTool } from "../package-tool.js";
|
|
8
|
+
import { writeSummary } from "../human-summary.js";
|
|
9
|
+
import { defineTool, ToolInputError } from "../tool-types.js";
|
|
10
|
+
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
11
|
+
import { resolveBizObjectIdByInstance, searchWorkItems, wireString } from "../workflow-support.js";
|
|
12
|
+
import { WORKFLOW_EVENT_HANDLER_FIELDS, WORKFLOW_LANE_CONTRACT, workflowTemplateArg } from "./workflow-contract.js";
|
|
13
|
+
import { PLATFORM_TRIGGER_RULE_CODES } from "./rule-graph.js";
|
|
14
|
+
import { executeRuleEnableWrite } from "./rule-lifecycle.js";
|
|
15
|
+
const WORKFLOW_LANE = WORKFLOW_LANE_CONTRACT.lane;
|
|
16
|
+
const WORKFLOW_TOOLS = WORKFLOW_LANE_CONTRACT.tools;
|
|
17
|
+
const WORKFLOW_ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
|
|
18
|
+
const WORKFLOW_POINT = /^-?\d+(?:\.\d+)?,\s-?\d+(?:\.\d+)?$/;
|
|
19
|
+
/**
|
|
20
|
+
* Conditional branch lines (round7). `routes[]` maps to the platform `Rule`
|
|
21
|
+
* DTO (Rule.java:28-59), which carries a structured branch condition
|
|
22
|
+
* `ruleConditionGroup = List<List<RuleConditionItem>>` (outer OR / inner AND).
|
|
23
|
+
* ActivityContext.getNextFormulaActivities:82-99 only consults it when
|
|
24
|
+
* popupType == EXPRESSION; a rule with utilizeElse=true is the else/fallback
|
|
25
|
+
* line and is never a condition line. These enums mirror ConditionType.java and
|
|
26
|
+
* MatchType.java (shared 8.6.26); a typo silently no-ops on the platform, so the
|
|
27
|
+
* write gate refuses it instead.
|
|
28
|
+
*/
|
|
29
|
+
const WORKFLOW_CONDITION_TYPES = new Set(["EQ", "NEQ", "GT", "LT", "GTEQ", "LTEQ", "CT", "NCT", "EP", "NEP", "OF", "NOF", "BETWEEN", "NOTBETWEEN", "CT_ALL", "NCT_ALL", "CT_ANY", "NCT_ANY", "EQ_ANY", "NEQ_ANY"]);
|
|
30
|
+
const WORKFLOW_MATCH_TYPES = new Set(["ALL", "ANY"]);
|
|
31
|
+
/**
|
|
32
|
+
* `RuleConditionItem` (RuleConditionItem.java, shared 8.6.26) has EXACTLY four
|
|
33
|
+
* fields: currentDataItem, bizPropertyType, conditionType, value. Any extra key
|
|
34
|
+
* makes the platform's workflow/update deserialization fail and answer a
|
|
35
|
+
* **misleading `401001 流程不存在!`** — not a key error (live 2026-09-25: adding
|
|
36
|
+
* the business-rule condition keys targetDataItem/ruleValueType to a workflow
|
|
37
|
+
* route condition poisoned the whole update). Fail closed on unknown keys before
|
|
38
|
+
* the wire so the agent gets the real fix instead of chasing a phantom workflow.
|
|
39
|
+
*/
|
|
40
|
+
const WORKFLOW_RULE_CONDITION_ITEM_KEYS = new Set(["currentDataItem", "bizPropertyType", "conditionType", "value"]);
|
|
41
|
+
/**
|
|
42
|
+
* RuleConditionItem.bizPropertyType is a BizPropertyType whose fastjson
|
|
43
|
+
* deserializer (BizPropertyTypeDeserializer.deserialze) reads an INT index
|
|
44
|
+
* (`BizPropertyType.get(TypeUtils.castToInt(value))`) and whose serializer writes
|
|
45
|
+
* the same index. Sending the enum NAME ("NUMERICAL") makes castToInt coerce it
|
|
46
|
+
* to 0 (the wrong type) and the template conversion then fails with a MISLEADING
|
|
47
|
+
* `流程名称为空!` (live 101.96.203.196 / AI-Platform 1.1.22 — cost the sandbox
|
|
48
|
+
* agent the whole workflow write). Accept the name for ergonomics but emit the
|
|
49
|
+
* index on the wire.
|
|
50
|
+
*/
|
|
51
|
+
const WORKFLOW_BIZ_PROPERTY_TYPE_INDEX = Object.freeze({
|
|
52
|
+
SHORT_TEXT: 0, LONG_TEXT: 1, NUMERICAL: 2, DATE: 3, LOGICAL: 4, SELECTION: 5,
|
|
53
|
+
ATTACHMENT: 6, COMMENT: 7, CHILD_TABLE: 8, WORK_SHEET: 9, ADDRESS: 10, MULT_WORK_SHEET: 11,
|
|
54
|
+
RADIO: 12, CHECKBOX: 13, DROPDOWN_BOX: 14, DROPDOWN_MULTI_BOX: 15, RELEVANCE_DATA: 16,
|
|
55
|
+
HYPER_LINK: 17, ID: 20, TIME: 28, STAFF_SELECTOR: 50, STAFF_MULTI_SELECTOR: 51,
|
|
56
|
+
DEPARTMENT_SELECTOR: 60, DEPARTMENT_MULTI_SELECTOR: 61, FORMULA: 100, OCR: 250,
|
|
57
|
+
ELECTRONIC_SIGN: 251, APPROVAL_INFO: 254,
|
|
58
|
+
});
|
|
59
|
+
const WORKFLOW_BIZ_PROPERTY_INDEX_SET = new Set(Object.values(WORKFLOW_BIZ_PROPERTY_TYPE_INDEX));
|
|
60
|
+
/** Resolve a caller-supplied bizPropertyType (index or enum name) to the wire index, or null if invalid. */
|
|
61
|
+
function resolveBizPropertyTypeIndex(value) {
|
|
62
|
+
if (typeof value === "number" && Number.isInteger(value))
|
|
63
|
+
return WORKFLOW_BIZ_PROPERTY_INDEX_SET.has(value) ? value : null;
|
|
64
|
+
if (typeof value === "string")
|
|
65
|
+
return WORKFLOW_BIZ_PROPERTY_TYPE_INDEX[value.trim().toUpperCase()] ?? null;
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
/** Rewrite each RuleConditionItem.bizPropertyType to the numeric index the platform deserializes. */
|
|
69
|
+
function normalizeRuleConditionGroup(groups) {
|
|
70
|
+
if (!Array.isArray(groups))
|
|
71
|
+
return groups;
|
|
72
|
+
return groups.map((group) => Array.isArray(group)
|
|
73
|
+
? group.map((item) => {
|
|
74
|
+
const record = (item ?? {});
|
|
75
|
+
const index = resolveBizPropertyTypeIndex(record.bizPropertyType);
|
|
76
|
+
return index === null ? record : { ...record, bizPropertyType: index };
|
|
77
|
+
})
|
|
78
|
+
: group);
|
|
79
|
+
}
|
|
80
|
+
/** Route fields that describe the line's condition/behaviour (as opposed to geometry). */
|
|
81
|
+
const WORKFLOW_ROUTE_CONDITION_KEYS = ["text", "formula", "utilizeElse", "fixedPoint", "alternativeFlag", "childMatchType", "ruleConditionGroup"];
|
|
82
|
+
/**
|
|
83
|
+
* Fail closed on a malformed structured branch condition BEFORE any wire call —
|
|
84
|
+
* an unknown conditionType or childMatchType deserializes to null on the platform
|
|
85
|
+
* and the line silently never fires (the same class of silent no-op that bit
|
|
86
|
+
* checkType=2). Only caller-supplied `routes`/`rules` are checked.
|
|
87
|
+
*/
|
|
88
|
+
function validateWorkflowRouteConditions(template, tool) {
|
|
89
|
+
const raw = template.routes ?? template.rules ?? template.edges;
|
|
90
|
+
if (!Array.isArray(raw))
|
|
91
|
+
return;
|
|
92
|
+
for (const [index, entry] of raw.entries()) {
|
|
93
|
+
const route = (entry ?? {});
|
|
94
|
+
// `null` is the platform's stored form for an absent optional — a caller
|
|
95
|
+
// legitimately round-tripping get_draft sends `text:null`/`formula:null`. Treat
|
|
96
|
+
// null like undefined, not as a wrong type.
|
|
97
|
+
for (const booleanKey of ["utilizeElse", "fixedPoint", "alternativeFlag"])
|
|
98
|
+
if (route[booleanKey] !== undefined && route[booleanKey] !== null && typeof route[booleanKey] !== "boolean")
|
|
99
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].${booleanKey} must be boolean` });
|
|
100
|
+
for (const stringKey of ["text", "formula"])
|
|
101
|
+
if (route[stringKey] !== undefined && route[stringKey] !== null && typeof route[stringKey] !== "string")
|
|
102
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].${stringKey} must be a string` });
|
|
103
|
+
if (route.childMatchType !== undefined && route.childMatchType !== null && !(typeof route.childMatchType === "string" && WORKFLOW_MATCH_TYPES.has(route.childMatchType)))
|
|
104
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].childMatchType must be ALL or ANY` });
|
|
105
|
+
if (route.ruleConditionGroup === undefined || route.ruleConditionGroup === null)
|
|
106
|
+
continue;
|
|
107
|
+
const groups = route.ruleConditionGroup;
|
|
108
|
+
if (!Array.isArray(groups) || groups.length === 0)
|
|
109
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].ruleConditionGroup must be a non-empty array of AND-groups (each an array of conditions)` });
|
|
110
|
+
for (const [groupIndex, group] of groups.entries()) {
|
|
111
|
+
if (!Array.isArray(group) || group.length === 0)
|
|
112
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].ruleConditionGroup[${groupIndex}] must be a non-empty array of RuleConditionItem` });
|
|
113
|
+
for (const [itemIndex, item] of group.entries()) {
|
|
114
|
+
const condition = (item ?? {});
|
|
115
|
+
if (typeof condition.currentDataItem !== "string" || condition.currentDataItem.trim() === "")
|
|
116
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].ruleConditionGroup[${groupIndex}][${itemIndex}].currentDataItem must be a non-empty field code (child fields use "childSchemaCode.fieldCode")` });
|
|
117
|
+
if (!(typeof condition.conditionType === "string" && WORKFLOW_CONDITION_TYPES.has(condition.conditionType)))
|
|
118
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].ruleConditionGroup[${groupIndex}][${itemIndex}].conditionType must be a ConditionType (${[...WORKFLOW_CONDITION_TYPES].join("/")})` });
|
|
119
|
+
if (condition.bizPropertyType !== undefined && condition.bizPropertyType !== null && resolveBizPropertyTypeIndex(condition.bizPropertyType) === null)
|
|
120
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-route-condition-invalid", message: `${tool}: routes[${index}].ruleConditionGroup[${groupIndex}][${itemIndex}].bizPropertyType must be a BizPropertyType index (NUMERICAL=2, DATE=3, SHORT_TEXT=0, STAFF_SELECTOR=50, …) or a known enum name` });
|
|
121
|
+
const unknownKeys = Object.keys(condition).filter((key) => !WORKFLOW_RULE_CONDITION_ITEM_KEYS.has(key));
|
|
122
|
+
if (unknownKeys.length > 0)
|
|
123
|
+
throw new ToolRefusalError({
|
|
124
|
+
status: "refused",
|
|
125
|
+
blocker: "workflow-route-condition-invalid",
|
|
126
|
+
message: `${tool}: routes[${index}].ruleConditionGroup[${groupIndex}][${itemIndex}] 含未知键 ${unknownKeys.join("/")}——RuleConditionItem 只接受 ${[...WORKFLOW_RULE_CONDITION_ITEM_KEYS].join("/")};未知键会让平台 workflow/update 返回**误导性的 401001 流程不存在!**(真机 2026-09-25)。业务规则条件组的 targetDataItem/ruleValueType 等键不能照抄到流程连线条件里。`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Fail closed on an unknown bindRule handler BEFORE any wire call. The MCP
|
|
134
|
+
* transport validates the zod schema, but `omc call` (the first-class live
|
|
135
|
+
* driver) invokes the handler directly, so the semantic gate lives here.
|
|
136
|
+
*/
|
|
137
|
+
function resolveEventHandlerField(handler) {
|
|
138
|
+
const key = handler ?? "end";
|
|
139
|
+
const field = WORKFLOW_EVENT_HANDLER_FIELDS[key];
|
|
140
|
+
if (!field) {
|
|
141
|
+
throw new ToolRefusalError({
|
|
142
|
+
status: "refused",
|
|
143
|
+
blocker: "workflow-invalid-handler",
|
|
144
|
+
handler,
|
|
145
|
+
message: `handler must be one of ${Object.keys(WORKFLOW_EVENT_HANDLER_FIELDS).join("|")} (omit for end)`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
return { key: key, field };
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Live 8.6.26 fidelity — a REFUSED workflow write still answers HTTP 200 with
|
|
152
|
+
* `errcode 0`; the refusal rides `data` as a `ValidateMessage[]`
|
|
153
|
+
* ({errCode, errContent, errorLevel}; S29 live contract, publish refusal errmsg
|
|
154
|
+
* is 发布流程失败). Trusting `errcode` alone reports a phantom success: live, a
|
|
155
|
+
* PARTICIPANT node whose originate participant is `"admin"` (not the platform
|
|
156
|
+
* keyword `"{Originator}"`) answers `401035 填写申请单 ERROR` while errcode stays
|
|
157
|
+
* 0 — the tool said publish:ok and the flow never went live. Surface every
|
|
158
|
+
* ERROR-level message as a structured refusal so the caller can fix the
|
|
159
|
+
* template instead of believing the flow is running. WARN-level messages are
|
|
160
|
+
* returned as evidence but do not block (live: `{Creator}` answers 401062 WARN).
|
|
161
|
+
*/
|
|
162
|
+
function workflowValidationErrors(data) {
|
|
163
|
+
if (!Array.isArray(data))
|
|
164
|
+
return [];
|
|
165
|
+
return data.filter((message) => message !== null && typeof message === "object" && message.errorLevel === "ERROR");
|
|
166
|
+
}
|
|
167
|
+
function workflowValidationWarnings(data) {
|
|
168
|
+
if (!Array.isArray(data))
|
|
169
|
+
return [];
|
|
170
|
+
return data.filter((message) => message !== null && typeof message === "object" && message.errorLevel !== "ERROR");
|
|
171
|
+
}
|
|
172
|
+
/** Refuse after a workflow write when the platform answered ValidateMessage[] ERRORs (errcode 0 notwithstanding). */
|
|
173
|
+
function assertWorkflowWriteAccepted(performed, tool, workflowCode) {
|
|
174
|
+
const errors = workflowValidationErrors(performed.data);
|
|
175
|
+
if (errors.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
throw new ToolRefusalError({
|
|
178
|
+
status: "refused",
|
|
179
|
+
blocker: "workflow-publish-invalid",
|
|
180
|
+
workflowCode,
|
|
181
|
+
validationErrors: errors,
|
|
182
|
+
message: `${tool}: 平台以 ValidateMessage[] 拒绝(errcode 0 但不落库):${errors.map((error) => `${error.errCode} ${error.errContent}`).join(";")}。常见根因:审批(PARTICIPANT)节点的 sheetCode 为空(=模型默认表单槽位,通常等于 schemaCode;401026 WORKFLOW_ACTIVITY_NO_SHEET——工具已自动回填,仍报则检查端点是否漏),或新增节点未接入链路(END 无入边)。修正模板后重试,不要反复 publish`,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Ticket 77 — a START/END-only template cannot overwrite the platform's
|
|
187
|
+
* auto-generated full draft (PARTICIPANT 填写申请单/审批 nodes); the platform
|
|
188
|
+
* answers a stable 401001 流程不存在. Refuse pre-wire and point at the
|
|
189
|
+
* get_draft-based editing path (or workflow.bindRule for rule binding).
|
|
190
|
+
*/
|
|
191
|
+
/**
|
|
192
|
+
* Caller activities may arrive as an array OR as an index-keyed object
|
|
193
|
+
* (`{"0":{…},"1":{…}}` — the shape the published workflow recipe teaches and
|
|
194
|
+
* the shape get_draft-adjacent callers reach for). Every downstream merge/lint
|
|
195
|
+
* step keys on an ARRAY; before this coercion an object-shaped `activities`
|
|
196
|
+
* was destructured away and silently DROPPED, so `workflow.update` answered
|
|
197
|
+
* `ok` while writing the unchanged draft — bench10/11: an agent's two-level
|
|
198
|
+
* approval template vanished and it could never land the second approval,
|
|
199
|
+
* burning 7 get_draft + 7 update round-trips. Coerce object → array in numeric
|
|
200
|
+
* key order; a non-array/non-object value is left to the element checks.
|
|
201
|
+
*/
|
|
202
|
+
function callerActivities(template) {
|
|
203
|
+
const activities = template?.activities;
|
|
204
|
+
if (Array.isArray(activities))
|
|
205
|
+
return activities;
|
|
206
|
+
if (activities !== null && typeof activities === "object") {
|
|
207
|
+
return Object.keys(activities)
|
|
208
|
+
.sort((a, b) => Number(a) - Number(b))
|
|
209
|
+
.map((key) => activities[key]);
|
|
210
|
+
}
|
|
211
|
+
return undefined;
|
|
212
|
+
}
|
|
213
|
+
function assertTemplateNotMinimal(template, tool) {
|
|
214
|
+
// An absent template (publish the stored draft as-is) is the platform's own
|
|
215
|
+
// path and is out of the guard's scope — the bench golden evals drive it.
|
|
216
|
+
if (template === undefined || template === null)
|
|
217
|
+
return;
|
|
218
|
+
const activities = callerActivities(template);
|
|
219
|
+
const types = activities
|
|
220
|
+
? activities
|
|
221
|
+
.map((activity) => (activity && typeof activity === "object" ? String(activity.type ?? activity.activityType ?? "") : ""))
|
|
222
|
+
.filter((type) => type.length > 0)
|
|
223
|
+
: [];
|
|
224
|
+
if (types.length > 0 && types.every((type) => type === "START" || type === "END")) {
|
|
225
|
+
throw new ToolRefusalError({
|
|
226
|
+
status: "refused",
|
|
227
|
+
blocker: "workflow-template-minimal",
|
|
228
|
+
message: `${tool} 收到仅含 START/END 的极简模板——平台为流程头自动生成含 PARTICIPANT 节点(填写申请单/审批)的完整草稿,极简模板无法覆盖,平台会稳定返回 401001 流程不存在。以 workflow.get_draft 返回的完整模板为基础修改后再 update/publish;只绑规则用 workflow.bindRule`,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Ticket 112 — dry-lint the caller template against the platform's update
|
|
234
|
+
* contract (8.6.26 decompiled source), zero wire:
|
|
235
|
+
* - `templateType:"BusinessProcess"` is NOT a WorkflowTemplateStatus member
|
|
236
|
+
* (WorkflowTemplateUtil.java:136 valueOf; the converter swallows the
|
|
237
|
+
* exception into a partial model and the save chain then fails with errcode
|
|
238
|
+
* 50000) — omit it and the draft's value rides along.
|
|
239
|
+
* - every activities element needs a type (platform keys on activityType;
|
|
240
|
+
* ActivityType.valueOf("null") throws the same swallowed way).
|
|
241
|
+
*/
|
|
242
|
+
function lintWorkflowTemplate(template, tool) {
|
|
243
|
+
if (template === undefined || template === null)
|
|
244
|
+
return;
|
|
245
|
+
if (template.templateType === "BusinessProcess")
|
|
246
|
+
throw new ToolRefusalError({
|
|
247
|
+
status: "refused",
|
|
248
|
+
blocker: "workflow-template-type-invalid",
|
|
249
|
+
message: `${tool}: templateType "BusinessProcess" 不是 WorkflowTemplateStatus 枚举成员(WorkflowTemplateUtil.java:136 valueOf——转换异常被吞成残缺模型后保存链 50000)。省略 templateType,由 get_draft 草稿值回显`,
|
|
250
|
+
});
|
|
251
|
+
const activities = callerActivities(template);
|
|
252
|
+
if (activities) {
|
|
253
|
+
for (const activity of activities) {
|
|
254
|
+
const record = activity;
|
|
255
|
+
const type = record != null && typeof record === "object" ? (record.type ?? record.activityType) : undefined;
|
|
256
|
+
if (type === undefined || type === null || String(type).trim() === "")
|
|
257
|
+
throw new ToolRefusalError({
|
|
258
|
+
status: "refused",
|
|
259
|
+
blocker: "workflow-activity-type-missing",
|
|
260
|
+
message: `${tool}: activities 元素缺少 type/activityType(平台 ActivityType.valueOf 对 "null" 抛异常被吞成残缺模型 → 50000);每个活动必须带类型`,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
/** Caller-side Activity aliases (input + workflowSummary spelling) that must never ride the wire. */
|
|
266
|
+
const ACTIVITY_INPUT_ALIAS_KEYS = ["id", "type", "name", "code", "index"];
|
|
267
|
+
/**
|
|
268
|
+
* Ticket 112 — normalize the caller template onto the platform DTO:
|
|
269
|
+
* - `routes[]` is NOT a WorkflowTemplateModel field (WorkflowTemplateModel
|
|
270
|
+
* .java:51-66 declares List<Rule> rules; Rule.java:40-57
|
|
271
|
+
* preActivityCode/postActivityCode). Caller routes are mapped to rules
|
|
272
|
+
* elements — with derived string `points` (the workflow designer canvas
|
|
273
|
+
* keys on them exactly like the rule graph does) — and the routes key
|
|
274
|
+
* never rides the wire.
|
|
275
|
+
* - caller activities may use the repo shorthand {id,type,name,participant};
|
|
276
|
+
* the platform wire keys are activityCode/activityType/activityName
|
|
277
|
+
* (WorkflowTemplateController update @ApiImplicitParam example;
|
|
278
|
+
* ActivityType.valueOf("null") makes the converter fail → 401001). Each
|
|
279
|
+
* caller activity is merged onto the matching DRAFT activity so the
|
|
280
|
+
* platform-required subfaces (todoDataItems, permittedAction,
|
|
281
|
+
* preCondition, geometry) echo instead of holing the model.
|
|
282
|
+
*/
|
|
283
|
+
function mapCallerActivities(callerActivities, draftActivities) {
|
|
284
|
+
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
285
|
+
const draftByType = (type) => draftActivities.find((activity) => String(activity.activityType ?? "") === type);
|
|
286
|
+
return callerActivities.map((entry) => {
|
|
287
|
+
const record = (entry ?? {});
|
|
288
|
+
const code = activityCodeOf(record);
|
|
289
|
+
const rawType = String(record.activityType ?? record.type ?? "");
|
|
290
|
+
const type = rawType === "APPROVAL" ? "PARTICIPANT" : rawType;
|
|
291
|
+
const base = draftByCode.get(code) ??
|
|
292
|
+
(type === "START" ? draftByType("START") : type === "END" ? draftByType("END") : type === "PARTICIPANT" ? draftByType("PARTICIPANT") : undefined) ??
|
|
293
|
+
{};
|
|
294
|
+
// Drop the input aliases AND the readback-only keys (`code`/`index` come
|
|
295
|
+
// from workflowSummary). Stray keys on an Activity/Rule make the platform
|
|
296
|
+
// answer a misleading 401001; only canonical keys may ride the wire. The
|
|
297
|
+
// participant is normalized as part of the record so the trailing spread
|
|
298
|
+
// cannot re-introduce a shorthand the platform cannot resolve.
|
|
299
|
+
const wireRecord = {
|
|
300
|
+
...Object.fromEntries(Object.entries(record).filter(([key]) => !ACTIVITY_INPUT_ALIAS_KEYS.includes(key))),
|
|
301
|
+
...(record.participant !== undefined ? { participant: normalizeParticipantForWire(record.participant) } : {}),
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
...base,
|
|
305
|
+
activityCode: code,
|
|
306
|
+
...(type ? { activityType: type } : {}),
|
|
307
|
+
...(record.name !== undefined ? { activityName: record.name } : {}),
|
|
308
|
+
...wireRecord,
|
|
309
|
+
};
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
function validateCallerActivities(template, tool) {
|
|
313
|
+
const activities = callerActivities(template);
|
|
314
|
+
if (!activities)
|
|
315
|
+
return;
|
|
316
|
+
const codes = new Set();
|
|
317
|
+
for (const [index, raw] of activities.entries()) {
|
|
318
|
+
const activity = raw;
|
|
319
|
+
const code = String(activity?.activityCode ?? activity?.id ?? activity?.code ?? "").trim();
|
|
320
|
+
const rawType = String(activity?.activityType ?? activity?.type ?? "").trim();
|
|
321
|
+
const type = rawType === "APPROVAL" ? "PARTICIPANT" : rawType;
|
|
322
|
+
if (!code)
|
|
323
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activity-code-invalid", message: `${tool}: activities[${index}] requires a non-empty activityCode/id` });
|
|
324
|
+
if (codes.has(code))
|
|
325
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activity-code-duplicate", message: `${tool}: duplicate activityCode ${code}` });
|
|
326
|
+
if (!rawType)
|
|
327
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-activity-type-missing", message: `${tool}: activities[${index}] requires type/activityType` });
|
|
328
|
+
if (!WORKFLOW_ACTIVITY_TYPES.has(type))
|
|
329
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activity-type-invalid", message: `${tool}: activities[${index}] has an illegal ActivityType` });
|
|
330
|
+
codes.add(code);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Workflow canonical node layout — the same lesson as the rule graph
|
|
335
|
+
* (CFG-26), freshly observed live 2026-09-17: mapCallerActivities merges an
|
|
336
|
+
* activity onto the draft row by code, else by TYPE. A caller who names TWO
|
|
337
|
+
* PARTICIPANT nodes has neither code in the draft, so BOTH inherit the single
|
|
338
|
+
* draft PARTICIPANT's box (x=327, y=116) and overlap — the designer then draws
|
|
339
|
+
* the fill→approve edge as a horizontal stub and the chain is unreadable. When
|
|
340
|
+
* the merged activities lack geometry or collide, lay the chain out in the
|
|
341
|
+
* platform's single column (x=327, 158×40, 80px apart) and re-derive the rule
|
|
342
|
+
* points. A graph the caller laid out deliberately (no missing box, no
|
|
343
|
+
* overlap) is preserved verbatim.
|
|
344
|
+
*/
|
|
345
|
+
function linearWorkflowOrder(activities, rules) {
|
|
346
|
+
const codes = activities.map((activity) => String(activity.activityCode ?? ""));
|
|
347
|
+
if (codes.some((code) => !code) || new Set(codes).size !== codes.length)
|
|
348
|
+
return [];
|
|
349
|
+
const outgoing = new Map();
|
|
350
|
+
for (const rule of rules) {
|
|
351
|
+
const pre = String(rule.preActivityCode ?? "");
|
|
352
|
+
const post = String(rule.postActivityCode ?? "");
|
|
353
|
+
if (!codes.includes(pre) || !codes.includes(post))
|
|
354
|
+
continue;
|
|
355
|
+
outgoing.set(pre, [...(outgoing.get(pre) ?? []), post]);
|
|
356
|
+
}
|
|
357
|
+
const starts = activities.filter((activity) => activity.activityType === "START").map((activity) => String(activity.activityCode));
|
|
358
|
+
if (starts.length !== 1 || [...outgoing.values()].some((targets) => targets.length > 1))
|
|
359
|
+
return [];
|
|
360
|
+
const order = [];
|
|
361
|
+
let cursor = starts[0];
|
|
362
|
+
while (cursor !== undefined && !order.includes(cursor)) {
|
|
363
|
+
order.push(cursor);
|
|
364
|
+
cursor = outgoing.get(cursor)?.[0];
|
|
365
|
+
}
|
|
366
|
+
return order.length === codes.length ? order : [];
|
|
367
|
+
}
|
|
368
|
+
function canonicalizeWorkflowLayout(activities, rules) {
|
|
369
|
+
const numberOr = (value, fallback) => (Number.isFinite(Number(value)) ? Number(value) : fallback);
|
|
370
|
+
const boxOf = (activity) => ({
|
|
371
|
+
x: numberOr(activity.x, Number.NaN),
|
|
372
|
+
y: numberOr(activity.y, Number.NaN),
|
|
373
|
+
width: numberOr(activity.width, 158),
|
|
374
|
+
height: numberOr(activity.height, 40),
|
|
375
|
+
});
|
|
376
|
+
const boxes = activities.map(boxOf);
|
|
377
|
+
const missing = boxes.some((box) => Number.isNaN(box.x) || Number.isNaN(box.y));
|
|
378
|
+
const overlaps = boxes.some((a, ai) => boxes.some((b, bi) => bi > ai && !Number.isNaN(a.x) && !Number.isNaN(b.x) && a.x < b.x + b.width && b.x < a.x + a.width && a.y < b.y + b.height && b.y < a.y + a.height));
|
|
379
|
+
if (!missing && !overlaps)
|
|
380
|
+
return { activities, changed: false };
|
|
381
|
+
const order = linearWorkflowOrder(activities, rules);
|
|
382
|
+
const position = (code, index) => (order.length > 0 ? order.indexOf(code) : index);
|
|
383
|
+
return {
|
|
384
|
+
changed: true,
|
|
385
|
+
activities: activities.map((activity, index) => ({
|
|
386
|
+
...activity,
|
|
387
|
+
x: 327,
|
|
388
|
+
y: 36 + 80 * position(String(activity.activityCode ?? ""), index),
|
|
389
|
+
width: 158,
|
|
390
|
+
height: 40,
|
|
391
|
+
})),
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
/** Ticket 112 — workflow rules carry designer-canvas `points` (same lesson as the rule graph): derive from the activity boxes. */
|
|
395
|
+
function deriveRulePoints(pre, post, activities) {
|
|
396
|
+
const numberOr = (value, fallback) => (Number.isFinite(Number(value)) ? Number(value) : fallback);
|
|
397
|
+
const boxOf = (code, index) => {
|
|
398
|
+
const activity = activities.find((candidate) => String(candidate.activityCode ?? "") === code) ?? activities[index];
|
|
399
|
+
return {
|
|
400
|
+
x: numberOr(activity?.x, 327),
|
|
401
|
+
y: numberOr(activity?.y, 36 + 80 * index),
|
|
402
|
+
width: numberOr(activity?.width, 158),
|
|
403
|
+
height: numberOr(activity?.height, 40),
|
|
404
|
+
};
|
|
405
|
+
};
|
|
406
|
+
const preIndex = Math.max(0, activities.findIndex((candidate) => String(candidate.activityCode ?? "") === pre));
|
|
407
|
+
const postIndex = Math.max(0, activities.findIndex((candidate) => String(candidate.activityCode ?? "") === post));
|
|
408
|
+
const s = boxOf(pre, preIndex);
|
|
409
|
+
const t = boxOf(post, postIndex);
|
|
410
|
+
const scx = s.x + s.width / 2;
|
|
411
|
+
const tcx = t.x + t.width / 2;
|
|
412
|
+
if (t.y > s.y)
|
|
413
|
+
return [`${scx}, ${s.y + s.height}`, `${tcx}, ${t.y}`];
|
|
414
|
+
if (t.y < s.y)
|
|
415
|
+
return [`${scx}, ${s.y}`, `${tcx}, ${t.y + t.height}`];
|
|
416
|
+
if (t.x >= s.x)
|
|
417
|
+
return [`${s.x + s.width}, ${s.y + s.height / 2}`, `${t.x}, ${t.y + t.height / 2}`];
|
|
418
|
+
return [`${s.x}, ${s.y + s.height / 2}`, `${t.x + t.width}, ${t.y + t.height / 2}`];
|
|
419
|
+
}
|
|
420
|
+
function mapCallerRules(callerRoutes, draftRules, activities) {
|
|
421
|
+
const baseRule = draftRules[0] ?? {};
|
|
422
|
+
return callerRoutes.map((entry) => {
|
|
423
|
+
const record = (entry ?? {});
|
|
424
|
+
const pre = String(record.preActivityCode ?? record.preNode ?? record.pre ?? record.from ?? "");
|
|
425
|
+
const post = String(record.postActivityCode ?? record.postNode ?? record.post ?? record.to ?? "");
|
|
426
|
+
const points = Array.isArray(record.points) && record.points.length > 0 ? record.points : deriveRulePoints(pre, post, activities);
|
|
427
|
+
const mapped = {
|
|
428
|
+
...baseRule,
|
|
429
|
+
...Object.fromEntries(Object.entries(record).filter(([key]) => !["preNode", "postNode", "pre", "post", "from", "to"].includes(key))),
|
|
430
|
+
preActivityCode: pre,
|
|
431
|
+
postActivityCode: post,
|
|
432
|
+
points,
|
|
433
|
+
};
|
|
434
|
+
// A structured branch line (round7) only fires when popupType is EXPRESSION
|
|
435
|
+
// and it is NOT the else/fallback line (ActivityContext
|
|
436
|
+
// .getNextFormulaActivities:82-99). The platform defaults are popupType
|
|
437
|
+
// FUNCTION (Rule.java:49) and utilizeElse true (Rule.java:37) — either would
|
|
438
|
+
// silently turn a supplied ruleConditionGroup into a no-op, and both ride in
|
|
439
|
+
// via the draft baseRule. So force them unless the caller set them itself.
|
|
440
|
+
if (record.ruleConditionGroup !== undefined) {
|
|
441
|
+
if (record.popupType === undefined)
|
|
442
|
+
mapped.popupType = "EXPRESSION";
|
|
443
|
+
if (record.utilizeElse === undefined)
|
|
444
|
+
mapped.utilizeElse = false;
|
|
445
|
+
// The platform deserializes BizPropertyType from an INT index; normalize a
|
|
446
|
+
// caller's enum name before the wire (see WORKFLOW_BIZ_PROPERTY_TYPE_INDEX).
|
|
447
|
+
mapped.ruleConditionGroup = normalizeRuleConditionGroup(record.ruleConditionGroup);
|
|
448
|
+
}
|
|
449
|
+
return mapped;
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
/**
|
|
453
|
+
* Participant references must be platform-resolvable. Real published workflows
|
|
454
|
+
* (2026-09-23 production observation) use exactly three forms:
|
|
455
|
+
* (a) a keyword — `{Originator}`;
|
|
456
|
+
* (b) a JSON array of participant objects —
|
|
457
|
+
* `[{"code","corpId","id","name","type"}]` (type 2 = role, 3 = user),
|
|
458
|
+
* joined with `+` for several;
|
|
459
|
+
* (c) a function — `FindUserByRole({Originator.OU},"<roleCode>","random")`.
|
|
460
|
+
* Free text (a bare role name like `部门负责人`) publishes but resolves to
|
|
461
|
+
* nobody and creates no workitem, so warn — the caller must resolve names via
|
|
462
|
+
* `org.role.list` / `org.department.list` first. Advisory (never blocks): the
|
|
463
|
+
* platform accepts the save, so this is a readback lint, not a write gate.
|
|
464
|
+
*/
|
|
465
|
+
function participantReferenceWarnings(template) {
|
|
466
|
+
const activities = Array.isArray(template.activities) ? template.activities : [];
|
|
467
|
+
const warnings = [];
|
|
468
|
+
for (const activity of activities) {
|
|
469
|
+
if (String(activity.activityType ?? "") !== "PARTICIPANT")
|
|
470
|
+
continue;
|
|
471
|
+
const raw = activity.participant;
|
|
472
|
+
if (raw === undefined || raw === null || String(raw).trim().length === 0)
|
|
473
|
+
continue; // platform may fill later
|
|
474
|
+
const participant = String(raw).trim();
|
|
475
|
+
if (isPlatformParticipantReference(participant))
|
|
476
|
+
continue;
|
|
477
|
+
warnings.push({
|
|
478
|
+
activityCode: String(activity.activityCode ?? ""),
|
|
479
|
+
participant,
|
|
480
|
+
warning: `参与者「${participant}」不是平台可解析的引用(自由文本解析不到人,不生成待办)。真实语法:关键字 {Originator};参与者对象数组 [{"code":"<角色code>","corpId":"<corpId|other>","id":"<roleId>","name":"<角色名>","type":2}](type 2=角色,3=用户,多个用 + 连接);或函数 FindUserByRole({Originator.OU},"<roleCode>","random")。先用 org.role.list / org.department.list / org.user.search 解析 code/id。`,
|
|
481
|
+
});
|
|
482
|
+
}
|
|
483
|
+
return warnings;
|
|
484
|
+
}
|
|
485
|
+
function isPlatformParticipantReference(participant) {
|
|
486
|
+
if (/^\{[^}]+\}$/.test(participant))
|
|
487
|
+
return true; // {Originator} keyword
|
|
488
|
+
if (/^[A-Za-z_$][\w$]*\s*\(/.test(participant))
|
|
489
|
+
return true; // FindUserByRole(...)
|
|
490
|
+
if (participant.startsWith("[") || participant.startsWith("+")) {
|
|
491
|
+
// one or more `[...]` segments joined by +
|
|
492
|
+
const segments = participant.split("+").map((segment) => segment.trim());
|
|
493
|
+
return segments.every((segment) => {
|
|
494
|
+
if (segment.length === 0)
|
|
495
|
+
return true;
|
|
496
|
+
try {
|
|
497
|
+
const parsed = JSON.parse(segment);
|
|
498
|
+
return Array.isArray(parsed) && parsed.every((entry) => entry !== null && typeof entry === "object" && ("id" in entry || "code" in entry));
|
|
499
|
+
}
|
|
500
|
+
catch {
|
|
501
|
+
return false;
|
|
502
|
+
}
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
return false;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Normalize a participant reference to a platform-resolvable form BEFORE it rides
|
|
509
|
+
* the wire. Live 2026-09-28: an intent that declares `participant: {type: Originator}`
|
|
510
|
+
* (or a bare `participant: Originator`) is stored as the JSON string
|
|
511
|
+
* `"{\"type\":\"Originator\"}"`, and `workflow.publish` refuses with
|
|
512
|
+
* `401035 填写申请单` — the originate participant never resolves, so the whole
|
|
513
|
+
* workflow will not publish. The platform understands three forms (see
|
|
514
|
+
* participantReferenceWarnings); this maps the natural shorthands onto them:
|
|
515
|
+
* - a lone originator (bare `Originator`, or `{type/participantType: "Originator"}`
|
|
516
|
+
* with only display keys) → the keyword `"{Originator}"`
|
|
517
|
+
* - a role/user object `{code|id: …}` → a one-element JSON array `[obj]`
|
|
518
|
+
* - an array → JSON string; a string is left verbatim (already a valid form)
|
|
519
|
+
* Existing `"{Originator}"`/function/role-array strings pass through unchanged.
|
|
520
|
+
*/
|
|
521
|
+
export function normalizeParticipantForWire(participant) {
|
|
522
|
+
if (participant === undefined || participant === null)
|
|
523
|
+
return participant;
|
|
524
|
+
if (typeof participant === "string") {
|
|
525
|
+
return /^originator$/i.test(participant.trim()) ? "{Originator}" : participant;
|
|
526
|
+
}
|
|
527
|
+
if (Array.isArray(participant))
|
|
528
|
+
return participant.length === 0 ? "" : JSON.stringify(participant);
|
|
529
|
+
if (typeof participant === "object") {
|
|
530
|
+
const record = participant;
|
|
531
|
+
const keys = Object.keys(record);
|
|
532
|
+
const typeValue = record.type ?? record.participantType;
|
|
533
|
+
const displayOnly = keys.every((key) => ["type", "participantType", "name", "i18nMessage", "label", "displayName"].includes(key));
|
|
534
|
+
if (displayOnly && typeof typeValue === "string" && /^originator$/i.test(typeValue.trim()))
|
|
535
|
+
return "{Originator}";
|
|
536
|
+
// A role/user descriptor must be a JSON ARRAY string; a bare object fails
|
|
537
|
+
// resolution too.
|
|
538
|
+
if ("code" in record || "id" in record)
|
|
539
|
+
return JSON.stringify([record]);
|
|
540
|
+
return JSON.stringify(record);
|
|
541
|
+
}
|
|
542
|
+
return participant;
|
|
543
|
+
}
|
|
544
|
+
function workflowRenderCompat(template) {
|
|
545
|
+
const activities = Array.isArray(template.activities) ? template.activities : [];
|
|
546
|
+
const rules = Array.isArray(template.rules) ? template.rules : [];
|
|
547
|
+
const activityCodes = new Set(activities.map((activity) => String(activity.activityCode ?? "")));
|
|
548
|
+
const danglingViolations = rules.flatMap((rule, index) => {
|
|
549
|
+
const missing = ["preActivityCode", "postActivityCode"].filter((key) => !activityCodes.has(String(rule[key] ?? "")));
|
|
550
|
+
return missing.length > 0 ? [{ ruleIndex: index, missing }] : [];
|
|
551
|
+
});
|
|
552
|
+
// 2026-09-25 cost pass: geometry + route points are deterministic for the
|
|
553
|
+
// canonical single column (x=327, 158×40, 80px apart). Echoing all of them on
|
|
554
|
+
// every activity/edge was ~880 B per workflow write. Only a DEVIATION from the
|
|
555
|
+
// canonical seat/edge is meaningful (a caller-placed layout, or a collision the
|
|
556
|
+
// canonicalizer fixed), so only deviations are echoed; their absence proves the
|
|
557
|
+
// graph is canonical. This keeps the render-compat capability (ticket 109:
|
|
558
|
+
// catch a broken/off-column layout) at a fraction of the bytes.
|
|
559
|
+
const numberOr = (value, fallback) => (Number.isFinite(Number(value)) ? Number(value) : fallback);
|
|
560
|
+
const order = linearWorkflowOrder(activities, rules);
|
|
561
|
+
const seat = (activity, index) => ({
|
|
562
|
+
x: 327,
|
|
563
|
+
y: 36 + 80 * (order.length > 0 ? order.indexOf(String(activity.activityCode ?? "")) : index),
|
|
564
|
+
width: 158,
|
|
565
|
+
height: 40,
|
|
566
|
+
});
|
|
567
|
+
const canonicalGeometry = (activity, index) => {
|
|
568
|
+
const expected = seat(activity, index);
|
|
569
|
+
return numberOr(activity.x, Number.NaN) === expected.x && numberOr(activity.y, Number.NaN) === expected.y && numberOr(activity.width, expected.width) === expected.width && numberOr(activity.height, expected.height) === expected.height;
|
|
570
|
+
};
|
|
571
|
+
return {
|
|
572
|
+
activities: activities.map((activity, index) => {
|
|
573
|
+
const geometry = { x: activity.x, y: activity.y, width: activity.width, height: activity.height };
|
|
574
|
+
return { activityCode: String(activity.activityCode), activityType: String(activity.activityType), ...(canonicalGeometry(activity, index) ? {} : { geometry }) };
|
|
575
|
+
}),
|
|
576
|
+
rules: rules.map((rule) => {
|
|
577
|
+
const pre = String(rule.preActivityCode);
|
|
578
|
+
const post = String(rule.postActivityCode);
|
|
579
|
+
const stored = Array.isArray(rule.points) ? rule.points : [];
|
|
580
|
+
const derived = deriveRulePoints(pre, post, activities);
|
|
581
|
+
const canonicalPoints = stored.length === derived.length && stored.every((point, index) => point === derived[index]);
|
|
582
|
+
return { preActivityCode: pre, postActivityCode: post, ...(canonicalPoints ? {} : { points: rule.points }), ...workflowRouteConditionView(rule) };
|
|
583
|
+
}),
|
|
584
|
+
startEnd: {
|
|
585
|
+
start: activities.filter((activity) => activity.activityType === "START").map((activity) => String(activity.activityCode)),
|
|
586
|
+
end: activities.filter((activity) => activity.activityType === "END").map((activity) => String(activity.activityCode)),
|
|
587
|
+
},
|
|
588
|
+
danglingViolations,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Round7 — the branch-relevant fields of a `Rule`, for readback. Without this
|
|
593
|
+
* the route condition is invisible: an agent could write a branch and never
|
|
594
|
+
* confirm which edge carries which condition.
|
|
595
|
+
*
|
|
596
|
+
* 2026-09-25 cost pass: the platform stores every optional rule key (`text`,
|
|
597
|
+
* `formula`, `childMatchType`, `ruleConditionGroup` as null; `utilizeElse`,
|
|
598
|
+
* `fixedPoint`, `alternativeFlag` as false; `popupType` as "FUNCTION"). Echoing
|
|
599
|
+
* those defaults repeated ~8 keys on EVERY edge of every workflow read (a 4-edge
|
|
600
|
+
* workflow carried ~890 B of edges, mostly defaults). Only meaningful values
|
|
601
|
+
* survive here; a default/absent key stays out. `full:true` still returns the
|
|
602
|
+
* raw stored row.
|
|
603
|
+
*/
|
|
604
|
+
function workflowRouteConditionView(rule) {
|
|
605
|
+
const view = {};
|
|
606
|
+
for (const key of ["text", "formula", "childMatchType", "ruleConditionGroup"]) {
|
|
607
|
+
const value = rule[key];
|
|
608
|
+
if (value !== undefined && value !== null)
|
|
609
|
+
view[key] = value;
|
|
610
|
+
}
|
|
611
|
+
for (const key of ["utilizeElse", "fixedPoint", "alternativeFlag"]) {
|
|
612
|
+
if (rule[key] === true)
|
|
613
|
+
view[key] = true;
|
|
614
|
+
}
|
|
615
|
+
if (rule.popupType !== undefined && rule.popupType !== null && rule.popupType !== "FUNCTION")
|
|
616
|
+
view.popupType = rule.popupType;
|
|
617
|
+
return view;
|
|
618
|
+
}
|
|
619
|
+
function validateWorkflowRender(template, tool) {
|
|
620
|
+
const activities = template.activities;
|
|
621
|
+
if (!Array.isArray(activities))
|
|
622
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activities-invalid", message: `${tool}: activities must be an array` });
|
|
623
|
+
const codes = new Set();
|
|
624
|
+
for (const [index, raw] of activities.entries()) {
|
|
625
|
+
const activity = raw;
|
|
626
|
+
const code = String(activity?.activityCode ?? "").trim();
|
|
627
|
+
const type = String(activity?.activityType ?? "").trim();
|
|
628
|
+
if (!code)
|
|
629
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activity-code-invalid", message: `${tool}: activities[${index}].activityCode must be non-empty` });
|
|
630
|
+
if (codes.has(code))
|
|
631
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activity-code-duplicate", message: `${tool}: duplicate activityCode ${code}` });
|
|
632
|
+
if (!WORKFLOW_ACTIVITY_TYPES.has(type))
|
|
633
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-activity-type-invalid", message: `${tool}: activities[${index}].activityType is not an 8.6.26 ActivityType` });
|
|
634
|
+
codes.add(code);
|
|
635
|
+
}
|
|
636
|
+
const rules = template.rules;
|
|
637
|
+
if (!Array.isArray(rules))
|
|
638
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-rules-invalid", message: `${tool}: rules must be an array` });
|
|
639
|
+
for (const [index, raw] of rules.entries()) {
|
|
640
|
+
const rule = raw;
|
|
641
|
+
const pre = String(rule?.preActivityCode ?? "").trim();
|
|
642
|
+
const post = String(rule?.postActivityCode ?? "").trim();
|
|
643
|
+
if (!pre || !post || !codes.has(pre) || !codes.has(post))
|
|
644
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-dangling-rule", message: `${tool}: rules[${index}] references an unknown activity` });
|
|
645
|
+
const points = rule?.points;
|
|
646
|
+
if (!Array.isArray(points) || points.length === 0 || points.some((point) => typeof point !== "string" || !WORKFLOW_POINT.test(point)))
|
|
647
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-rules-invalid", message: `${tool}: rules[${index}].points must be a non-empty string[] of "x, y" coordinates` });
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* The draft rules remapped by index cannot describe an activity the caller
|
|
652
|
+
* ADDED (an extra approval node): the remapped edges stop at the old chain, an
|
|
653
|
+
* extra END sits with no inbound edge, and the platform publish validator
|
|
654
|
+
* answers 401026/401027/401017 on 结束 (bench12, live). When the remapped rules
|
|
655
|
+
* are not a clean linear chain over the mapped activities, rebuild the chain in
|
|
656
|
+
* the caller's array order — the recipe contract is "array order = approval
|
|
657
|
+
* chain order" — reusing any edge whose endpoints survived. A draft whose
|
|
658
|
+
* (possibly branching) graph is untouched passes the chain check verbatim.
|
|
659
|
+
*/
|
|
660
|
+
function completeWorkflowChain(rules, activities) {
|
|
661
|
+
const codes = activities.map((activity) => String(activity.activityCode ?? "")).filter((code) => code.length > 0);
|
|
662
|
+
if (codes.length < 2)
|
|
663
|
+
return rules;
|
|
664
|
+
const typeOf = (code) => String(activities.find((activity) => String(activity.activityCode ?? "") === code)?.activityType ?? "");
|
|
665
|
+
const codeSet = new Set(codes);
|
|
666
|
+
const inbound = new Map();
|
|
667
|
+
const outbound = new Map();
|
|
668
|
+
let valid = true;
|
|
669
|
+
for (const rule of rules) {
|
|
670
|
+
const pre = String(rule.preActivityCode ?? "");
|
|
671
|
+
const post = String(rule.postActivityCode ?? "");
|
|
672
|
+
if (!codeSet.has(pre) || !codeSet.has(post)) {
|
|
673
|
+
valid = false;
|
|
674
|
+
continue;
|
|
675
|
+
}
|
|
676
|
+
outbound.set(pre, (outbound.get(pre) ?? 0) + 1);
|
|
677
|
+
inbound.set(post, (inbound.get(post) ?? 0) + 1);
|
|
678
|
+
}
|
|
679
|
+
for (const code of codes) {
|
|
680
|
+
const type = typeOf(code);
|
|
681
|
+
if (type !== "START" && (inbound.get(code) ?? 0) !== 1)
|
|
682
|
+
valid = false;
|
|
683
|
+
if (type !== "END" && (outbound.get(code) ?? 0) !== 1)
|
|
684
|
+
valid = false;
|
|
685
|
+
}
|
|
686
|
+
if (valid)
|
|
687
|
+
return rules;
|
|
688
|
+
const byEdge = new Map(rules.map((rule) => [`${rule.preActivityCode}->${rule.postActivityCode}`, rule]));
|
|
689
|
+
return codes.slice(0, -1).map((code, index) => byEdge.get(`${code}->${codes[index + 1]}`) ?? { preActivityCode: code, postActivityCode: codes[index + 1] });
|
|
690
|
+
}
|
|
691
|
+
/** Activity type resolution shared by the merge + code reconcile (APPROVAL is a legacy alias). */
|
|
692
|
+
function activityTypeOf(record) {
|
|
693
|
+
const rawType = String(record.activityType ?? record.type ?? "");
|
|
694
|
+
return rawType === "APPROVAL" ? "PARTICIPANT" : rawType;
|
|
695
|
+
}
|
|
696
|
+
/** Display name the caller asked for, in any accepted spelling. */
|
|
697
|
+
function activityNameOf(record) {
|
|
698
|
+
const value = record.activityName ?? record.i18nMessage ?? record.name;
|
|
699
|
+
if (typeof value === "string" && value.length > 0)
|
|
700
|
+
return value;
|
|
701
|
+
// The participant shorthand `{type: Originator, name: 部门负责人}` carries the
|
|
702
|
+
// intended NODE label inside `participant.name`. The platform only resolves
|
|
703
|
+
// the participant keyword and defaults the activity name to 填写申请单, so
|
|
704
|
+
// without this the declared node name is silently lost (bench62: both approval
|
|
705
|
+
// nodes read back as 填写申请单). Promote it when the participant is the lone
|
|
706
|
+
// originator shorthand (the role-descriptor form keeps its own semantics).
|
|
707
|
+
const participant = record.participant;
|
|
708
|
+
if (participant && typeof participant === "object" && !Array.isArray(participant)) {
|
|
709
|
+
const descriptor = participant;
|
|
710
|
+
const typeValue = descriptor.type ?? descriptor.participantType;
|
|
711
|
+
if (typeof typeValue === "string" && /^originator$/i.test(typeValue.trim()) && typeof descriptor.name === "string" && descriptor.name.trim().length > 0)
|
|
712
|
+
return descriptor.name;
|
|
713
|
+
}
|
|
714
|
+
return undefined;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* Activity code in any accepted spelling. `code` is what `workflowSummary`
|
|
718
|
+
* (get_draft/get_published compact) emits, so `get_draft → edit → update` — the
|
|
719
|
+
* documented "read back then send the delta" flow — round-trips: an activity the
|
|
720
|
+
* readback handed you is directly re-sendable.
|
|
721
|
+
*/
|
|
722
|
+
function activityCodeOf(record) {
|
|
723
|
+
return String(record.activityCode ?? record.id ?? record.code ?? "");
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Live 2026-09-23: the platform's workflow/update CANNOT RENAME an existing
|
|
727
|
+
* activityCode — the stored name and type win. A caller list that reuses the
|
|
728
|
+
* auto-draft's `ActivityN` codes therefore corrupts: the caller's
|
|
729
|
+
* `Activity3` PARTICIPANT keeps the draft's name ("审批"), and a caller's
|
|
730
|
+
* `Activity4` PARTICIPANT merges onto the draft's END node (name "结束",
|
|
731
|
+
* type END) — reproduced on bench12/13/15, where the second approval node
|
|
732
|
+
* showed as "结束". Only a FRESH code receives the caller's name/type.
|
|
733
|
+
*
|
|
734
|
+
* Reconcile the caller list against the draft: keep a code when the caller
|
|
735
|
+
* re-sends the same type + name (idempotent re-update), otherwise mint a
|
|
736
|
+
* collision-free code so the change actually lands. Returns the rewritten
|
|
737
|
+
* activities plus the original→final code map for route remapping.
|
|
738
|
+
*/
|
|
739
|
+
function reconcileCallerActivityCodes(callerActivities, draftActivities) {
|
|
740
|
+
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
741
|
+
const used = new Set(draftActivities.map((activity) => String(activity.activityCode ?? "")));
|
|
742
|
+
const codeMap = new Map();
|
|
743
|
+
const activities = callerActivities.map((record) => {
|
|
744
|
+
const original = activityCodeOf(record);
|
|
745
|
+
const type = activityTypeOf(record);
|
|
746
|
+
const name = activityNameOf(record);
|
|
747
|
+
const draft = draftByCode.get(original);
|
|
748
|
+
let code = original;
|
|
749
|
+
if (draft !== undefined) {
|
|
750
|
+
const draftType = String(draft.activityType ?? "");
|
|
751
|
+
const draftName = activityNameOf(draft);
|
|
752
|
+
const changed = draftType !== type || (name !== undefined && draftName !== undefined && name !== draftName);
|
|
753
|
+
if (changed) {
|
|
754
|
+
let suffix = 2;
|
|
755
|
+
while (used.has(`${original}_${suffix}`))
|
|
756
|
+
suffix += 1;
|
|
757
|
+
code = `${original}_${suffix}`;
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
used.add(code);
|
|
761
|
+
codeMap.set(original, code);
|
|
762
|
+
return { ...record, activityCode: code, ...(name !== undefined ? { activityName: name, i18nMessage: name } : {}) };
|
|
763
|
+
});
|
|
764
|
+
return { activities, codeMap };
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Context-cost (round 15): a caller template that declares activities but NOT
|
|
768
|
+
* both boundaries is necessarily a partial edit — a workflow without START+END
|
|
769
|
+
* cannot stand alone on the platform (create always mints them). Treating it as
|
|
770
|
+
* a full replace dropped the draft's boundary nodes and answered a misleading
|
|
771
|
+
* `50000 服务异常` (bench43: a targeted `propertyPermissions` update on two
|
|
772
|
+
* PARTICIPANT nodes lost start/end, and the agent's recovery was to read the
|
|
773
|
+
* whole 43.7 KB body back and post it verbatim — 93 KB, 38% of the task).
|
|
774
|
+
* Route such a template through the delta merge instead, so the caller can send
|
|
775
|
+
* only the node(s) it touches. A template that DOES carry both boundaries keeps
|
|
776
|
+
* replace semantics (an explicit full-graph rewrite).
|
|
777
|
+
*/
|
|
778
|
+
function activitiesLackBoundaries(template) {
|
|
779
|
+
const activities = callerActivities(template);
|
|
780
|
+
if (!activities || activities.length === 0)
|
|
781
|
+
return false;
|
|
782
|
+
const types = activities.map((raw) => {
|
|
783
|
+
const record = raw;
|
|
784
|
+
return record != null && typeof record === "object" ? String(record.activityType ?? record.type ?? "").trim().toUpperCase() : "";
|
|
785
|
+
});
|
|
786
|
+
return !(types.includes("START") && types.includes("END"));
|
|
787
|
+
}
|
|
788
|
+
/**
|
|
789
|
+
* Deep-merge a caller activity row onto its draft row, with `propertyPermissions`
|
|
790
|
+
* (and each entry's `subPropertyPermissions`) merged BY `propertyCode` rather
|
|
791
|
+
* than replaced. A targeted field-permission edit sends one or two permission
|
|
792
|
+
* entries; a raw array replace would silently drop every field the caller did
|
|
793
|
+
* not re-send. Only arrays deep-merge; an explicit non-array (e.g. null) is
|
|
794
|
+
* taken as the caller's intent to replace.
|
|
795
|
+
*/
|
|
796
|
+
function mergePermissionsByCode(base, caller) {
|
|
797
|
+
if (!Array.isArray(base) || !Array.isArray(caller))
|
|
798
|
+
return caller;
|
|
799
|
+
const byCode = new Map();
|
|
800
|
+
for (const entry of base) {
|
|
801
|
+
if (entry && typeof entry === "object") {
|
|
802
|
+
const row = entry;
|
|
803
|
+
byCode.set(String(row.propertyCode ?? ""), row);
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
for (const entry of caller) {
|
|
807
|
+
if (!entry || typeof entry !== "object")
|
|
808
|
+
continue;
|
|
809
|
+
const row = entry;
|
|
810
|
+
const key = String(row.propertyCode ?? "");
|
|
811
|
+
const prev = byCode.get(key);
|
|
812
|
+
byCode.set(key, prev
|
|
813
|
+
? {
|
|
814
|
+
...prev,
|
|
815
|
+
...row,
|
|
816
|
+
...(row.subPropertyPermissions !== undefined ? { subPropertyPermissions: mergePermissionsByCode(prev.subPropertyPermissions, row.subPropertyPermissions) } : {}),
|
|
817
|
+
}
|
|
818
|
+
: row);
|
|
819
|
+
}
|
|
820
|
+
return [...byCode.values()];
|
|
821
|
+
}
|
|
822
|
+
function deepMergeActivityRow(base, caller) {
|
|
823
|
+
const merged = { ...base, ...caller };
|
|
824
|
+
if (Array.isArray(caller.propertyPermissions))
|
|
825
|
+
merged.propertyPermissions = mergePermissionsByCode(base.propertyPermissions, caller.propertyPermissions);
|
|
826
|
+
return merged;
|
|
827
|
+
}
|
|
828
|
+
/**
|
|
829
|
+
* Incremental (`merge:true`) workflow edit: the caller sends ONLY the activities/
|
|
830
|
+
* routes it adds or changes; the draft is the base. Draft activities survive
|
|
831
|
+
* unless overridden by `activityCode`, caller-only codes append (usually before
|
|
832
|
+
* END via `completeWorkflowChain`), and route edges union (caller wins on the
|
|
833
|
+
* same edge). This is the "read back, then send the delta" path — without it a
|
|
834
|
+
* subset silently REPLACED the whole activity list and dropped nodes.
|
|
835
|
+
*/
|
|
836
|
+
function mergeCallerDelta(template, draft) {
|
|
837
|
+
const { routes: routesRaw, activities: _raw, edges: edgesRaw, ...rest } = template;
|
|
838
|
+
const routes = routesRaw ?? edgesRaw;
|
|
839
|
+
const draftActivities = Array.isArray(draft.activities) ? draft.activities : [];
|
|
840
|
+
const draftRules = Array.isArray(draft.rules) ? draft.rules : [];
|
|
841
|
+
const caller = (callerActivities(template) ?? []).map((raw) => {
|
|
842
|
+
const type = activityTypeOf(raw);
|
|
843
|
+
const name = activityNameOf(raw);
|
|
844
|
+
return {
|
|
845
|
+
// Drop the input aliases AND the readback-only keys (`code`/`index` come
|
|
846
|
+
// from workflowSummary): the merge override is spread onto the draft row,
|
|
847
|
+
// so an unstripped alias would ride the wire and make the platform answer
|
|
848
|
+
// a misleading 401001 (same policy as mapCallerActivities).
|
|
849
|
+
...Object.fromEntries(Object.entries(raw).filter(([key]) => !ACTIVITY_INPUT_ALIAS_KEYS.includes(key))),
|
|
850
|
+
activityCode: activityCodeOf(raw),
|
|
851
|
+
...(type ? { activityType: type } : {}),
|
|
852
|
+
...(name !== undefined ? { activityName: name, i18nMessage: name } : {}),
|
|
853
|
+
// Normalize a participant shorthand to a platform-resolvable form (the
|
|
854
|
+
// merge path bypasses mapCallerActivities; see normalizeParticipantForWire).
|
|
855
|
+
...(raw.participant !== undefined ? { participant: normalizeParticipantForWire(raw.participant) } : {}),
|
|
856
|
+
};
|
|
857
|
+
});
|
|
858
|
+
// Live 2026-09-23: the platform cannot rename an existing activityCode — the
|
|
859
|
+
// stored name/type win (same constraint the replace path reconciles). A
|
|
860
|
+
// caller that re-sends the readback with a changed name/type must get a FRESH
|
|
861
|
+
// code, and the old node must be DROPPED so it is a rename, not a duplicate.
|
|
862
|
+
const { activities: reconciledCaller, codeMap } = reconcileCallerActivityCodes(caller, draftActivities);
|
|
863
|
+
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
864
|
+
const originalOf = new Map();
|
|
865
|
+
for (const [original, final] of codeMap)
|
|
866
|
+
if (String(original) !== String(final))
|
|
867
|
+
originalOf.set(String(final), String(original));
|
|
868
|
+
// Map each caller node back to the DRAFT code it targets, so an override
|
|
869
|
+
// (including a rename) replaces that draft row IN PLACE instead of moving
|
|
870
|
+
// the node to the end of the chain. A renamed node inherits the old row's
|
|
871
|
+
// subfaces (geometry, sheetCode, todoDataItems, …) — only code/name/type change.
|
|
872
|
+
const callerByOriginal = new Map();
|
|
873
|
+
for (const activity of reconciledCaller) {
|
|
874
|
+
const final = String(activity.activityCode);
|
|
875
|
+
const original = originalOf.get(final) ?? final;
|
|
876
|
+
const base = draftByCode.get(original);
|
|
877
|
+
callerByOriginal.set(original, base ? deepMergeActivityRow(base, activity) : activity);
|
|
878
|
+
}
|
|
879
|
+
const mergedActivities = draftActivities.map((activity) => {
|
|
880
|
+
const key = String(activity.activityCode ?? "");
|
|
881
|
+
const override = callerByOriginal.get(key);
|
|
882
|
+
if (!override)
|
|
883
|
+
return activity;
|
|
884
|
+
callerByOriginal.delete(key);
|
|
885
|
+
return { ...activity, ...override };
|
|
886
|
+
});
|
|
887
|
+
const newActivities = [...callerByOriginal.values()];
|
|
888
|
+
// Insert added nodes BEFORE the terminal END, so the auto-chain reads
|
|
889
|
+
// START → …existing… → new → END (appending after END produced a chain that
|
|
890
|
+
// ended on the new node instead of END).
|
|
891
|
+
if (newActivities.length > 0) {
|
|
892
|
+
let endIndex = mergedActivities.length;
|
|
893
|
+
for (let index = mergedActivities.length - 1; index >= 0; index -= 1) {
|
|
894
|
+
if (String(mergedActivities[index]?.activityType ?? "") === "END") {
|
|
895
|
+
endIndex = index;
|
|
896
|
+
break;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
mergedActivities.splice(endIndex, 0, ...newActivities);
|
|
900
|
+
}
|
|
901
|
+
const remapCode = (value) => codeMap.get(String(value ?? "")) ?? value;
|
|
902
|
+
// Both draft edges and caller edges follow a renamed node; the caller may use
|
|
903
|
+
// any endpoint spelling, so remap every alias.
|
|
904
|
+
const remapRuleEndpoints = (rule) => {
|
|
905
|
+
const mapped = { ...rule };
|
|
906
|
+
for (const key of ["preActivityCode", "preNode", "pre", "from"])
|
|
907
|
+
if (rule[key] !== undefined)
|
|
908
|
+
mapped[key] = remapCode(rule[key]);
|
|
909
|
+
for (const key of ["postActivityCode", "postNode", "post", "to"])
|
|
910
|
+
if (rule[key] !== undefined)
|
|
911
|
+
mapped[key] = remapCode(rule[key]);
|
|
912
|
+
return mapped;
|
|
913
|
+
};
|
|
914
|
+
const edgeKey = (rule) => `${rule.preActivityCode ?? rule.preNode ?? rule.pre ?? rule.from}->${rule.postActivityCode ?? rule.postNode ?? rule.post ?? rule.to}`;
|
|
915
|
+
const remappedDraftRules = draftRules.map(remapRuleEndpoints);
|
|
916
|
+
const mappedCallerRoutes = Array.isArray(routes) ? mapCallerRules(routes.map((entry) => remapRuleEndpoints((entry ?? {}))), remappedDraftRules, mergedActivities) : [];
|
|
917
|
+
const byEdge = new Map();
|
|
918
|
+
for (const rule of remappedDraftRules)
|
|
919
|
+
byEdge.set(edgeKey(rule), rule);
|
|
920
|
+
for (const rule of mappedCallerRoutes)
|
|
921
|
+
byEdge.set(edgeKey(rule), { ...(byEdge.get(edgeKey(rule)) ?? {}), ...rule });
|
|
922
|
+
const mergedRules = completeWorkflowChain([...byEdge.values()], mergedActivities);
|
|
923
|
+
// Same layout + points derivation as the replace path: a chain-completion edge
|
|
924
|
+
// synthesized above carries no points, and the designer canvas needs them.
|
|
925
|
+
const laidOut = canonicalizeWorkflowLayout(mergedActivities, mergedRules);
|
|
926
|
+
const rules = mergedRules.map((rule) => {
|
|
927
|
+
const absent = rule.points === undefined || rule.points === null;
|
|
928
|
+
return laidOut.changed || absent
|
|
929
|
+
? { ...rule, points: deriveRulePoints(String(rule.preActivityCode ?? ""), String(rule.postActivityCode ?? ""), laidOut.activities) }
|
|
930
|
+
: rule;
|
|
931
|
+
});
|
|
932
|
+
return { ...rest, activities: laidOut.activities, rules };
|
|
933
|
+
}
|
|
934
|
+
function normalizeWorkflowTemplate(template, draft) {
|
|
935
|
+
if (template === undefined || template === null)
|
|
936
|
+
return {};
|
|
937
|
+
const { routes: routesRaw, activities: rawActivities, edges: edgesRaw, ...rest } = template;
|
|
938
|
+
const routes = routesRaw ?? edgesRaw;
|
|
939
|
+
void rawActivities;
|
|
940
|
+
// Array OR index-keyed object; `rest` must not carry the raw form or an
|
|
941
|
+
// object-shaped activities would ride the wire unmerged.
|
|
942
|
+
const rawCaller = callerActivities(template);
|
|
943
|
+
const draftActivities = Array.isArray(draft.activities) ? draft.activities : [];
|
|
944
|
+
const draftRules = Array.isArray(draft.rules) ? draft.rules : [];
|
|
945
|
+
// The platform cannot rename an existing activityCode (see the reconcile
|
|
946
|
+
// comment); give every changed/new node a fresh code, and remap caller routes
|
|
947
|
+
// so they follow.
|
|
948
|
+
const reconciled = rawCaller ? reconcileCallerActivityCodes(rawCaller, draftActivities) : undefined;
|
|
949
|
+
const activities = reconciled?.activities;
|
|
950
|
+
const codeMap = reconciled?.codeMap ?? new Map();
|
|
951
|
+
const remapCode = (value) => codeMap.get(String(value ?? "")) ?? value;
|
|
952
|
+
const remappedRoutes = Array.isArray(routes)
|
|
953
|
+
? routes.map((entry) => {
|
|
954
|
+
const record = (entry ?? {});
|
|
955
|
+
return {
|
|
956
|
+
...record,
|
|
957
|
+
...(record.preNode !== undefined ? { preNode: remapCode(record.preNode) } : {}),
|
|
958
|
+
...(record.postNode !== undefined ? { postNode: remapCode(record.postNode) } : {}),
|
|
959
|
+
...(record.pre !== undefined ? { pre: remapCode(record.pre) } : {}),
|
|
960
|
+
...(record.post !== undefined ? { post: remapCode(record.post) } : {}),
|
|
961
|
+
};
|
|
962
|
+
})
|
|
963
|
+
: routes;
|
|
964
|
+
const normalized = { ...rest };
|
|
965
|
+
if (activities)
|
|
966
|
+
normalized.activities = mapCallerActivities(activities, draftActivities);
|
|
967
|
+
if (Array.isArray(remappedRoutes)) {
|
|
968
|
+
const mapped = mapCallerRules(remappedRoutes, draftRules, normalized.activities ?? draftActivities);
|
|
969
|
+
normalized.rules = Array.isArray(rest.rules) ? [...rest.rules, ...mapped] : mapped;
|
|
970
|
+
}
|
|
971
|
+
if (Array.isArray(rest.rules) && !Array.isArray(remappedRoutes))
|
|
972
|
+
normalized.rules = mapCallerRules(rest.rules, draftRules, normalized.activities ?? draftActivities);
|
|
973
|
+
if (activities && !Array.isArray(remappedRoutes) && !Array.isArray(rest.rules)) {
|
|
974
|
+
const mappedActivities = normalized.activities;
|
|
975
|
+
const remapped = draftRules.map((rule) => {
|
|
976
|
+
const preIndex = draftActivities.findIndex((activity) => String(activity.activityCode ?? "") === String(rule.preActivityCode ?? ""));
|
|
977
|
+
const postIndex = draftActivities.findIndex((activity) => String(activity.activityCode ?? "") === String(rule.postActivityCode ?? ""));
|
|
978
|
+
const pre = mappedActivities[preIndex]?.activityCode ?? rule.preActivityCode;
|
|
979
|
+
const post = mappedActivities[postIndex]?.activityCode ?? rule.postActivityCode;
|
|
980
|
+
return { ...rule, preActivityCode: pre, postActivityCode: post };
|
|
981
|
+
});
|
|
982
|
+
normalized.rules = completeWorkflowChain(remapped, mappedActivities);
|
|
983
|
+
}
|
|
984
|
+
// Live 2026-09-17: merged activities may collide (two PARTICIPANT nodes both
|
|
985
|
+
// inheriting the draft PARTICIPANT's box) — lay the chain out canonically,
|
|
986
|
+
// then re-derive every rule's points from the final geometry so the designer
|
|
987
|
+
// lines follow the boxes instead of drawing a horizontal stub.
|
|
988
|
+
if (Array.isArray(normalized.activities)) {
|
|
989
|
+
const laidOut = canonicalizeWorkflowLayout(normalized.activities, Array.isArray(normalized.rules) ? normalized.rules : []);
|
|
990
|
+
normalized.activities = laidOut.activities;
|
|
991
|
+
// Caller-authored points on an already-renderable layout stay verbatim;
|
|
992
|
+
// points are re-derived when the geometry was repaired — or when a rule
|
|
993
|
+
// carries none at all (a chain-completion edge synthesized above). A
|
|
994
|
+
// provided-but-malformed points value is left for validateWorkflowRender to
|
|
995
|
+
// refuse (never silently repaired).
|
|
996
|
+
if (Array.isArray(normalized.rules))
|
|
997
|
+
normalized.rules = normalized.rules.map((rule) => {
|
|
998
|
+
const absent = rule.points === undefined || rule.points === null;
|
|
999
|
+
return laidOut.changed || absent
|
|
1000
|
+
? { ...rule, points: deriveRulePoints(String(rule.preActivityCode ?? ""), String(rule.postActivityCode ?? ""), laidOut.activities) }
|
|
1001
|
+
: rule;
|
|
1002
|
+
});
|
|
1003
|
+
}
|
|
1004
|
+
return normalized;
|
|
1005
|
+
}
|
|
1006
|
+
/**
|
|
1007
|
+
* Ticket 112 — merge the caller template onto the FULL get_draft row before
|
|
1008
|
+
* the update/publish wire. The platform save is a full-row write; posting a
|
|
1009
|
+
* minimal template leaves converter-swallowed holes and answers errcode
|
|
1010
|
+
* 50000 (live incident). Caller keys override; everything else echoes the
|
|
1011
|
+
* draft verbatim (the same merge pattern workflow.bindRule uses).
|
|
1012
|
+
*/
|
|
1013
|
+
async function mergedTemplateBody(session, schemaCode, code, template, merge = false) {
|
|
1014
|
+
const draft = await getWorkflowDraftOrNull(session, code);
|
|
1015
|
+
if (draft == null)
|
|
1016
|
+
throw new ToolRefusalError({
|
|
1017
|
+
status: "refused",
|
|
1018
|
+
blocker: "workflow-not-found",
|
|
1019
|
+
workflowCode: code,
|
|
1020
|
+
message: `流程 ${code} 不存在(get_draft 401001 缺席)——先 workflow.create 再 update/publish`,
|
|
1021
|
+
});
|
|
1022
|
+
const normalized = merge ? mergeCallerDelta(template, draft) : normalizeWorkflowTemplate(template, draft);
|
|
1023
|
+
// S29 contract: a PARTICIPANT activity requires sheetCode = the schema's
|
|
1024
|
+
// sheet, else validate/publish answers 401026 WORKFLOW_ACTIVITY_NO_SHEET. The
|
|
1025
|
+
// platform's own draft sets each participant's sheetCode to the model code;
|
|
1026
|
+
// an activity the caller ADDED has no draft row to inherit it from, so
|
|
1027
|
+
// backfill it (the default-form slot is the model code — same rule as
|
|
1028
|
+
// configure_form's omitted sheetCode).
|
|
1029
|
+
if (Array.isArray(normalized.activities))
|
|
1030
|
+
normalized.activities = normalized.activities.map((activity) => String(activity.activityType ?? "") === "PARTICIPANT" && (activity.sheetCode === undefined || activity.sheetCode === null || activity.sheetCode === "")
|
|
1031
|
+
? { ...activity, sheetCode: schemaCode }
|
|
1032
|
+
: activity);
|
|
1033
|
+
return { ...draft, ...normalized, schemaCode, workflowCode: code };
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* Round-2 BUG-05 — live `workflow/get_header` and `workflow/get_draft` answer
|
|
1037
|
+
* `401001 流程不存在!` for an ABSENT workflow rather than a null payload. Both
|
|
1038
|
+
* are ABSENCE signals, not failures: the pre-create probe and the post-create
|
|
1039
|
+
* readback must treat them as "no workflow" so workflow.create can proceed
|
|
1040
|
+
* instead of stopping at the probe's 401001.
|
|
1041
|
+
*/
|
|
1042
|
+
function isWorkflowAbsent(error) {
|
|
1043
|
+
return error instanceof WireError && error.kind === "business" && error.errcode === 401001;
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Identity seam: a caller may hold only the model's `schemaCode`. Resolve the
|
|
1047
|
+
* workflow it backs via the platform link (workflow/isworkflow →
|
|
1048
|
+
* workflow/getWorkflow); return null when the model has no workflow.
|
|
1049
|
+
*/
|
|
1050
|
+
async function resolveWorkflowArg(session, args) {
|
|
1051
|
+
if (args.workflowCode)
|
|
1052
|
+
return args.workflowCode;
|
|
1053
|
+
if (!args.schemaCode)
|
|
1054
|
+
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "workflow.*", message: "需要 workflowCode,或 schemaCode(工具内部解析该模型的流程)" });
|
|
1055
|
+
const code = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode: args.schemaCode } })).data === true
|
|
1056
|
+
? (await wireCall(session, { path: ENDPOINTS.workflowGetBySchema, query: { schemaCode: args.schemaCode } })).data
|
|
1057
|
+
: null;
|
|
1058
|
+
const workflowCode = code && typeof code.workflowCode === "string" && code.workflowCode ? code.workflowCode : null;
|
|
1059
|
+
return workflowCode;
|
|
1060
|
+
}
|
|
1061
|
+
async function getWorkflowHeaderOrNull(session, workflowCode) {
|
|
1062
|
+
try {
|
|
1063
|
+
return ((await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode } })).data ?? null);
|
|
1064
|
+
}
|
|
1065
|
+
catch (error) {
|
|
1066
|
+
if (isWorkflowAbsent(error))
|
|
1067
|
+
return null;
|
|
1068
|
+
throw error;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
async function getWorkflowDraftOrNull(session, workflowCode) {
|
|
1072
|
+
try {
|
|
1073
|
+
return ((await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode } })).data ?? null);
|
|
1074
|
+
}
|
|
1075
|
+
catch (error) {
|
|
1076
|
+
if (isWorkflowAbsent(error))
|
|
1077
|
+
return null;
|
|
1078
|
+
throw error;
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
/**
|
|
1082
|
+
* Round 4 (context-cost): keep the node structure in the default workflow
|
|
1083
|
+
* readback. The full template row is 30–75 KB (activities ×4, originateActivity,
|
|
1084
|
+
* notify config); without this projection the agent fetches the whole artifact
|
|
1085
|
+
* just to see the node list. Reproduced benchmark: `artifact get` was 56% of a
|
|
1086
|
+
* complex task's stdout, dominated by workflow.get_draft/get_published (75 KB +
|
|
1087
|
+
* 52 KB). Full body stays reachable via detailArtifactId, `--jq`, or `--full`.
|
|
1088
|
+
*/
|
|
1089
|
+
/**
|
|
1090
|
+
* Round 16: compact, non-default field-permission summary for one activity.
|
|
1091
|
+
* The full permission row carries ~10 keys per field (`propertyName`,
|
|
1092
|
+
* `encryptVisible`, `decryptAble`, …); the caller only needs to confirm who can
|
|
1093
|
+
* see/edit/require what, so keep `{code, visible, editable, required}` and drop
|
|
1094
|
+
* the all-false rows that dominate the full body. Returns undefined when the
|
|
1095
|
+
* activity declares no permission face or has no non-default row.
|
|
1096
|
+
*/
|
|
1097
|
+
function activityPermissions(activity) {
|
|
1098
|
+
const rows = activity.propertyPermissions;
|
|
1099
|
+
if (!Array.isArray(rows))
|
|
1100
|
+
return undefined;
|
|
1101
|
+
const permissions = rows
|
|
1102
|
+
.filter((row) => Boolean(row && typeof row === "object" && !Array.isArray(row)))
|
|
1103
|
+
.map((row) => ({ code: row.propertyCode ?? null, visible: row.visible === true, editable: row.editable === true, required: row.required === true }))
|
|
1104
|
+
.filter((row) => row.visible || row.editable || row.required);
|
|
1105
|
+
return permissions.length > 0 ? { permissions } : undefined;
|
|
1106
|
+
}
|
|
1107
|
+
export function workflowCompactProjection(result) {
|
|
1108
|
+
const data = (result.data ?? null);
|
|
1109
|
+
if (data === null)
|
|
1110
|
+
return {};
|
|
1111
|
+
const activities = (data.activities ?? {});
|
|
1112
|
+
const nodes = Object.entries(activities).map(([index, activity]) => ({
|
|
1113
|
+
index: Number(index),
|
|
1114
|
+
code: activity.activityCode ?? null,
|
|
1115
|
+
type: activity.activityType ?? null,
|
|
1116
|
+
name: activity.activityName ?? activity.i18nMessage ?? null,
|
|
1117
|
+
...(activity.participant !== undefined ? { participant: activity.participant } : {}),
|
|
1118
|
+
// Round 16 (bench65 feedback): a participant node's field permissions were
|
|
1119
|
+
// only visible in the 30–45 KB full body, so acceptance (and any "did my
|
|
1120
|
+
// permission edit land?") required the artifact. Surface only the
|
|
1121
|
+
// NON-DEFAULT rows (any of visible/editable/required true) so the summary
|
|
1122
|
+
// stays small and names exactly what a readback should confirm.
|
|
1123
|
+
...(activityPermissions(activity) ?? {}),
|
|
1124
|
+
}));
|
|
1125
|
+
const rules = data.rules;
|
|
1126
|
+
const edges = Array.isArray(rules)
|
|
1127
|
+
? rules.map((rule) => {
|
|
1128
|
+
const record = rule;
|
|
1129
|
+
return { from: record.preActivityCode ?? null, to: record.postActivityCode ?? null, ...workflowRouteConditionView(record) };
|
|
1130
|
+
})
|
|
1131
|
+
: [];
|
|
1132
|
+
// Round 15 (bench61 feedback): a count ("ruleBindings: 3") cannot be verified —
|
|
1133
|
+
// show WHICH event handler binds WHICH rule codes, so a readback confirms the
|
|
1134
|
+
// binding without pulling the full body. The platform's own originate node
|
|
1135
|
+
// (填写申请单) and any injected participant are named too, so a caller does not
|
|
1136
|
+
// mistake them for their declared nodes.
|
|
1137
|
+
const ruleBindings = [];
|
|
1138
|
+
for (const [handler, field] of Object.entries(WORKFLOW_EVENT_HANDLER_FIELDS)) {
|
|
1139
|
+
const config = data[field];
|
|
1140
|
+
const codes = Array.isArray(config?.bizActions) ? config.bizActions.filter((value) => typeof value === "string") : [];
|
|
1141
|
+
if (codes.length > 0 && config?.bizActionType === "BIZ_RULE")
|
|
1142
|
+
ruleBindings.push({ handler, field, ruleCodes: codes });
|
|
1143
|
+
}
|
|
1144
|
+
const originate = data.originateActivity;
|
|
1145
|
+
return {
|
|
1146
|
+
workflowSummary: {
|
|
1147
|
+
workflowCode: data.workflowCode ?? result.workflowCode ?? null,
|
|
1148
|
+
templateType: data.templateType ?? null,
|
|
1149
|
+
activities: nodes,
|
|
1150
|
+
edges,
|
|
1151
|
+
ruleBindings,
|
|
1152
|
+
ruleBindingCount: ruleBindings.reduce((sum, binding) => sum + binding.ruleCodes.length, 0),
|
|
1153
|
+
...(originate ? { originateActivity: { code: originate.activityCode ?? null, type: originate.activityType ?? null, name: originate.activityName ?? originate.i18nMessage ?? null, participant: originate.participant ?? null } } : {}),
|
|
1154
|
+
},
|
|
1155
|
+
// Editing needs only `activities` (array or index object) + `routes`: the tool
|
|
1156
|
+
// merges onto the stored draft, connects added nodes through to END, fills
|
|
1157
|
+
// sheetCode and geometry. Reading the 30–75 KB full body is NOT required —
|
|
1158
|
+
// bench17 pulled 61 KB `full:true` just to edit two node names.
|
|
1159
|
+
summaryHint: "改流程只需把要改/新增的 activities(数组)+ routes 传给 workflow.update:缺 START/END 的部分模板会自动按增量 merge 到草稿(草稿为基,自动补 sheetCode/连线/坐标),无需读回整模板或 --full 正文;字段权限也只需在对应 PARTICIPANT 上给 propertyPermissions:[{propertyCode,...}],工具按 propertyCode 合并、未列的字段权限保留。只有要整图替换时才给全 START..END 的完整模板;要看通知等完整节点正文才用 detailArtifactId 或 --full",
|
|
1160
|
+
};
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* A workflow event handler (start/cancel/activate/end) binds a business rule by
|
|
1164
|
+
* CODE. The platform's designer rule picker only offers CUSTOM rules — the
|
|
1165
|
+
* preset trigger rows (Create/Update/Delete/Load/GetList/Available/Cancel, all
|
|
1166
|
+
* `defaultRule:true`) are NOT selectable there, because they already run on the
|
|
1167
|
+
* data event itself. Binding one (2026-09-28: bench59 bound `Create` to `end`)
|
|
1168
|
+
* produces a handler the designer cannot represent. Refuse before any write.
|
|
1169
|
+
*/
|
|
1170
|
+
async function assertBindableWorkflowRules(session, schemaCode, codes) {
|
|
1171
|
+
const presets = codes.filter((code) => PLATFORM_TRIGGER_RULE_CODES.has(code));
|
|
1172
|
+
if (presets.length > 0)
|
|
1173
|
+
throw new ToolRefusalError({
|
|
1174
|
+
status: "refused",
|
|
1175
|
+
blocker: "workflow-bind-preset-rule",
|
|
1176
|
+
ruleCodes: presets,
|
|
1177
|
+
message: `流程事件只能绑定【自定义】业务规则,不能绑定平台预置触发规则(${presets.join("、")})——预置规则已在数据事件上运行,设计器的流程事件规则选择器不提供它们。要「审批通过后执行某逻辑」,请新建一条自定义规则(自定义 ruleCode),把动作节点写进它,再 bindRule 到对应 handler;或在预置 Create/Update 规则里直接追加节点。`,
|
|
1178
|
+
});
|
|
1179
|
+
// Cross-check the authoritative `defaultRule` flag (covers preset codes not in
|
|
1180
|
+
// the local set, and a schema that renamed one).
|
|
1181
|
+
let rows;
|
|
1182
|
+
try {
|
|
1183
|
+
const list = await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } });
|
|
1184
|
+
rows = wireRows(list.data);
|
|
1185
|
+
}
|
|
1186
|
+
catch {
|
|
1187
|
+
return new Map(); // a read failure must not block an otherwise-valid bind
|
|
1188
|
+
}
|
|
1189
|
+
const byCode = new Map(rows.map((row) => [String(row.code ?? ""), row]));
|
|
1190
|
+
const defaults = codes.filter((code) => byCode.get(code)?.defaultRule === true && !PLATFORM_TRIGGER_RULE_CODES.has(code));
|
|
1191
|
+
if (defaults.length > 0)
|
|
1192
|
+
throw new ToolRefusalError({
|
|
1193
|
+
status: "refused",
|
|
1194
|
+
blocker: "workflow-bind-preset-rule",
|
|
1195
|
+
ruleCodes: defaults,
|
|
1196
|
+
message: `流程事件只能绑定自定义业务规则;${defaults.join("、")} 是平台预置默认规则(defaultRule:true),设计器不可选。改用自定义规则。`,
|
|
1197
|
+
});
|
|
1198
|
+
return byCode;
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* A rule bound to a workflow event handler is INERT while disabled — the
|
|
1202
|
+
* designer shows it 失效/未启用 and the event handler will not run it. Binding a
|
|
1203
|
+
* workflow rule therefore GUARANTEES the bound custom rule is enabled: a
|
|
1204
|
+
* disabled/never-published rule (the state `rule.save` leaves) is published +
|
|
1205
|
+
* enabled here via the same `rule.enable` chain. Live 2026-10-08 (SCM sandbox):
|
|
1206
|
+
* rules created with `rule.save` (DRAFT/DISABLED) and bound with
|
|
1207
|
+
* `workflow.bindRule` read back 「失效·未启用」 in the workflow designer — a
|
|
1208
|
+
* workflow-bound rule MUST be enabled.
|
|
1209
|
+
*/
|
|
1210
|
+
async function ensureWorkflowRulesEnabled(session, schemaCode, codes, byCode) {
|
|
1211
|
+
const enabledRuleCodes = [];
|
|
1212
|
+
const alreadyEnabledRuleCodes = [];
|
|
1213
|
+
if (byCode.size === 0)
|
|
1214
|
+
return { enabledRuleCodes, alreadyEnabledRuleCodes }; // list read failed → cannot judge
|
|
1215
|
+
for (const code of codes) {
|
|
1216
|
+
const row = byCode.get(code);
|
|
1217
|
+
if (!row)
|
|
1218
|
+
throw new ToolRefusalError({
|
|
1219
|
+
status: "refused",
|
|
1220
|
+
blocker: "workflow-bind-rule-absent",
|
|
1221
|
+
schemaCode,
|
|
1222
|
+
ruleCodes: [code],
|
|
1223
|
+
message: `流程事件要绑定的规则 "${code}" 不存在(${schemaCode} 的 list_by_schemacode 无此 code)——流程事件只能绑已存在的【自定义】规则。先用 rule.save 建该规则(CUSTOM_NODE + 动作节点),再 bindRule。`,
|
|
1224
|
+
});
|
|
1225
|
+
if (row.enabled === true) {
|
|
1226
|
+
alreadyEnabledRuleCodes.push(code);
|
|
1227
|
+
continue;
|
|
1228
|
+
}
|
|
1229
|
+
// Disabled/DRAFT: publish + enable so the event handler actually runs it.
|
|
1230
|
+
await executeRuleEnableWrite(session, { schemaCode, ruleCode: code });
|
|
1231
|
+
enabledRuleCodes.push(code);
|
|
1232
|
+
}
|
|
1233
|
+
return { enabledRuleCodes, alreadyEnabledRuleCodes };
|
|
1234
|
+
}
|
|
1235
|
+
export function registerWorkflowLane(tools) {
|
|
1236
|
+
// Sibling registry for the high-level `configure_workflow` (reuses each atomic
|
|
1237
|
+
// tool's own writeExecute so every gate + readback is identical).
|
|
1238
|
+
const siblings = tools;
|
|
1239
|
+
tools.push(defineTool({
|
|
1240
|
+
name: "workflow.get_draft",
|
|
1241
|
+
layer: "low",
|
|
1242
|
+
kind: "read",
|
|
1243
|
+
lane: WORKFLOW_LANE,
|
|
1244
|
+
scope: WORKFLOW_TOOLS["workflow.get_draft"].scope,
|
|
1245
|
+
description: WORKFLOW_TOOLS["workflow.get_draft"].description,
|
|
1246
|
+
inputSchema: {
|
|
1247
|
+
environment: environmentArg,
|
|
1248
|
+
workflowCode: z.string().optional().describe("流程 code;也可改用 schemaCode(工具内部按 workflow/isworkflow + workflow/getWorkflow 解析)"),
|
|
1249
|
+
schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型已发布的流程;模型没有流程则 exists:false"),
|
|
1250
|
+
full: z.boolean().optional().describe("内联完整工作流模板;默认返回摘要与 detailFile"),
|
|
1251
|
+
capability: capabilityArg,
|
|
1252
|
+
},
|
|
1253
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
1254
|
+
// Identity seam: callers often hold only the model's schemaCode; resolve the
|
|
1255
|
+
// workflow it backs rather than forcing an external mapping.
|
|
1256
|
+
const workflowCode = await resolveWorkflowArg(session, args);
|
|
1257
|
+
if (!workflowCode)
|
|
1258
|
+
return { schemaCode: args.schemaCode, exists: false, note: "该模型没有已发布流程(workflow/isworkflow=false);先 workflow.create" };
|
|
1259
|
+
// Round-2 BUG-05: a missing workflow answers 401001 — surface absence as
|
|
1260
|
+
// a clean readback (exists:false) instead of a confusing error.
|
|
1261
|
+
const draft = await getWorkflowDraftOrNull(session, workflowCode);
|
|
1262
|
+
return draft == null
|
|
1263
|
+
? { workflowCode, exists: false, note: "流程不存在(平台 401001 = 缺席信号);先 workflow.create" }
|
|
1264
|
+
: { workflowCode, exists: true, data: draft };
|
|
1265
|
+
}, { full: args.full === true, compact: workflowCompactProjection, target: `workflow.get_draft:${args.workflowCode ?? args.schemaCode}` }),
|
|
1266
|
+
}));
|
|
1267
|
+
tools.push(defineTool({
|
|
1268
|
+
name: "workflow.get_published",
|
|
1269
|
+
layer: "low",
|
|
1270
|
+
kind: "read",
|
|
1271
|
+
lane: WORKFLOW_LANE,
|
|
1272
|
+
scope: WORKFLOW_TOOLS["workflow.get_published"].scope,
|
|
1273
|
+
description: WORKFLOW_TOOLS["workflow.get_published"].description,
|
|
1274
|
+
inputSchema: {
|
|
1275
|
+
environment: environmentArg,
|
|
1276
|
+
workflowCode: z.string().optional().describe("流程 code;也可改用 schemaCode(工具内部按 workflow/isworkflow + workflow/getWorkflow 解析)"),
|
|
1277
|
+
schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型已发布的流程;模型没有流程则 exists:false"),
|
|
1278
|
+
full: z.boolean().optional().describe("内联完整工作流模板;默认返回摘要与 detailFile"),
|
|
1279
|
+
capability: capabilityArg,
|
|
1280
|
+
},
|
|
1281
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
1282
|
+
const workflowCode = await resolveWorkflowArg(session, args);
|
|
1283
|
+
if (!workflowCode)
|
|
1284
|
+
return { schemaCode: args.schemaCode, exists: false, published: false, note: "该模型没有已发布流程(workflow/isworkflow=false);先 workflow.create 再 publish" };
|
|
1285
|
+
let pub;
|
|
1286
|
+
try {
|
|
1287
|
+
pub = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode } });
|
|
1288
|
+
}
|
|
1289
|
+
catch (error) {
|
|
1290
|
+
// Live `workflow/get_published` also answers 401001 流程不存在! for an
|
|
1291
|
+
// absent workflow. Mirror workflow.get_draft: surface absence as a
|
|
1292
|
+
// clean exists:false readback so a greenfield probe does not have to
|
|
1293
|
+
// create the workflow first.
|
|
1294
|
+
if (isWorkflowAbsent(error))
|
|
1295
|
+
return { workflowCode, exists: false, published: false, note: "流程不存在(平台 401001 = 缺席信号);先 workflow.create 再 publish" };
|
|
1296
|
+
throw error;
|
|
1297
|
+
}
|
|
1298
|
+
// Ticket 77: the platform answers the DRAFT shape for an unpublished
|
|
1299
|
+
// workflow — data presence is NOT publish evidence. The published face
|
|
1300
|
+
// is identified by its markers (templateType PUBLISH / published flag).
|
|
1301
|
+
const data = (pub.data ?? null);
|
|
1302
|
+
const published = data !== null && (data.templateType === "PUBLISH" || data.published === true);
|
|
1303
|
+
return {
|
|
1304
|
+
workflowCode,
|
|
1305
|
+
exists: true,
|
|
1306
|
+
published,
|
|
1307
|
+
...(data && !published ? { note: "平台对未发布流程仍返回草稿形态——此读回不能当作已发布证据;以 published:true / templateType:PUBLISH 为准(先 workflow.publish)" } : {}),
|
|
1308
|
+
data: pub.data,
|
|
1309
|
+
};
|
|
1310
|
+
}, { full: args.full === true, compact: workflowCompactProjection, target: `workflow.get_published:${args.workflowCode ?? args.schemaCode}` }),
|
|
1311
|
+
}));
|
|
1312
|
+
tools.push(defineTool({
|
|
1313
|
+
name: "workflow.validate",
|
|
1314
|
+
layer: "low",
|
|
1315
|
+
kind: "read",
|
|
1316
|
+
lane: WORKFLOW_LANE,
|
|
1317
|
+
scope: WORKFLOW_TOOLS["workflow.validate"].scope,
|
|
1318
|
+
description: WORKFLOW_TOOLS["workflow.validate"].description,
|
|
1319
|
+
inputSchema: {
|
|
1320
|
+
environment: environmentArg,
|
|
1321
|
+
workflowCode: z.string().optional().describe("要校验的流程;省略则用 schemaCode 解析"),
|
|
1322
|
+
schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型的流程"),
|
|
1323
|
+
template: workflowTemplateArg.optional().describe("可选:先按 workflow.update 的 merge 语义合成该模板再校验;省略则校验当前草稿"),
|
|
1324
|
+
capability: capabilityArg,
|
|
1325
|
+
},
|
|
1326
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
1327
|
+
const workflowCode = await resolveWorkflowArg(session, args);
|
|
1328
|
+
if (!workflowCode)
|
|
1329
|
+
return { schemaCode: args.schemaCode, exists: false, note: "该模型没有流程可校验" };
|
|
1330
|
+
const template = (args.template ?? {});
|
|
1331
|
+
const body = await mergedTemplateBody(session, args.schemaCode ?? "", workflowCode, template, activitiesLackBoundaries(template));
|
|
1332
|
+
const result = await wireCall(session, { path: ENDPOINTS.workflowValidate, method: "POST", body });
|
|
1333
|
+
const messages = Array.isArray(result.data) ? result.data : [];
|
|
1334
|
+
const errors = messages.filter((message) => message.errorLevel === "ERROR");
|
|
1335
|
+
const warnings = messages.filter((message) => message.errorLevel !== "ERROR");
|
|
1336
|
+
return { workflowCode, valid: errors.length === 0, errorCount: errors.length, warningCount: warnings.length, errors, warnings };
|
|
1337
|
+
}, { target: `workflow.validate:${args.workflowCode ?? args.schemaCode}` }),
|
|
1338
|
+
}));
|
|
1339
|
+
writeTool(tools, "workflow.create", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.create"].scope, WORKFLOW_TOOLS["workflow.create"].description, { schemaCode: z.string().min(1), name: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
|
|
1340
|
+
// Ticket 76 idempotent retry: get_header FIRST — a post-timeout
|
|
1341
|
+
// re-create of the same code short-circuits with the existing header
|
|
1342
|
+
// and ZERO create wires. On the happy path this read REPLACES the
|
|
1343
|
+
// post-create header read (same wire count), because a successful
|
|
1344
|
+
// create implies the header.
|
|
1345
|
+
const existingHeader = await getWorkflowHeaderOrNull(session, args.code);
|
|
1346
|
+
if (existingHeader != null) {
|
|
1347
|
+
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existingHeader } };
|
|
1348
|
+
}
|
|
1349
|
+
let performed;
|
|
1350
|
+
try {
|
|
1351
|
+
performed = await wireCall(session, {
|
|
1352
|
+
path: ENDPOINTS.workflowCreate,
|
|
1353
|
+
method: "POST",
|
|
1354
|
+
body: { workflowCode: args.code, workflowName: args.name, schemaCode: args.schemaCode, icon: "h-icon-all-logic" },
|
|
1355
|
+
});
|
|
1356
|
+
}
|
|
1357
|
+
catch (error) {
|
|
1358
|
+
// Ticket 76 live: the platform answers an envelope with NO numeric
|
|
1359
|
+
// errcode when the SCHEMA already has a workflow (a second, differently
|
|
1360
|
+
// coded one is refused). Decode it as a duplicate-create — same-code
|
|
1361
|
+
// presence was already excluded by the pre-check, so this is the
|
|
1362
|
+
// different-code case — instead of surfacing a malformed-envelope
|
|
1363
|
+
// transport-looking failure on a business duplicate.
|
|
1364
|
+
if (error instanceof WireError && error.kind === "malformed-envelope") {
|
|
1365
|
+
const has = await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode: args.schemaCode } });
|
|
1366
|
+
if (has.data === true) {
|
|
1367
|
+
throw new ToolRefusalError({
|
|
1368
|
+
status: "refused",
|
|
1369
|
+
blocker: "workflow-already-exists",
|
|
1370
|
+
schemaCode: args.schemaCode,
|
|
1371
|
+
workflowCode: args.code,
|
|
1372
|
+
message: `模型 ${args.schemaCode} 已有流程(平台对重复创建返回非数字 errcode 的 envelope)——一个模型只能有一个流程。要改现有流程用 workflow.update/workflow.publish(先 workflow.get_draft 取当前模板),不要另建新 code`,
|
|
1373
|
+
});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
throw error;
|
|
1377
|
+
}
|
|
1378
|
+
const draft = await getWorkflowDraftOrNull(session, args.code);
|
|
1379
|
+
return {
|
|
1380
|
+
performed: { errcode: performed.errcode },
|
|
1381
|
+
readback: { workflowCode: args.code, headerPresent: true, draftPresent: draft != null, templateId: draft?.id ?? null },
|
|
1382
|
+
};
|
|
1383
|
+
});
|
|
1384
|
+
writeTool(tools, "workflow.update", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.update"].scope, WORKFLOW_TOOLS["workflow.update"].description, { schemaCode: z.string().min(1), code: z.string().min(1), template: workflowTemplateArg, merge: z.boolean().optional().describe("增量:只发改动/新增的活动与路由,工具按 activityCode 合并到草稿(草稿为基;新增节点自动接链),不必先读回整模板再重发。默认自动:模板缺 START 或 END 时按增量 merge(部分模板),给了完整 START..END 才整模板替换;显式 true 强制增量、false 强制替换") }, async (session, args) => {
|
|
1385
|
+
const template = args.template ?? {};
|
|
1386
|
+
validateCallerActivities(template, "workflow.update");
|
|
1387
|
+
validateWorkflowRouteConditions(template, "workflow.update");
|
|
1388
|
+
assertTemplateNotMinimal(template, "workflow.update");
|
|
1389
|
+
lintWorkflowTemplate(template, "workflow.update");
|
|
1390
|
+
// Ticket 112: merge onto the full draft row — a verbatim minimal post
|
|
1391
|
+
// leaves converter holes and answers errcode 50000 on the platform.
|
|
1392
|
+
// Delta semantics: explicit `true` forces merge, explicit `false` forces
|
|
1393
|
+
// replace, and the default auto-detects a partial template (missing
|
|
1394
|
+
// START/END) as a delta — see activitiesLackBoundaries.
|
|
1395
|
+
const merge = args.merge === true || (args.merge !== false && activitiesLackBoundaries(template));
|
|
1396
|
+
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, merge);
|
|
1397
|
+
validateWorkflowRender(body, "workflow.update");
|
|
1398
|
+
// Live: workflow/update does NOT run the template validator — it stores a
|
|
1399
|
+
// refused participant silently and only /publish answers the
|
|
1400
|
+
// ValidateMessage[]. So the guard belongs on publish/bindRule, not here.
|
|
1401
|
+
const performed = await wireCall(session, { path: ENDPOINTS.workflowUpdate, method: "POST", body });
|
|
1402
|
+
const draft = await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode: args.code } });
|
|
1403
|
+
const data = (draft.data ?? null);
|
|
1404
|
+
if (!data)
|
|
1405
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.update returned no draft" });
|
|
1406
|
+
validateWorkflowRender(data, "workflow.update readback");
|
|
1407
|
+
const participantWarnings = participantReferenceWarnings(data);
|
|
1408
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : 0, templateType: data.templateType ?? null, ...(participantWarnings.length ? { participantWarnings } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
1409
|
+
});
|
|
1410
|
+
writeTool(tools, "workflow.publish", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.publish"].scope, WORKFLOW_TOOLS["workflow.publish"].description, { schemaCode: z.string().min(1), code: z.string().min(1), template: workflowTemplateArg.optional() }, async (session, args) => {
|
|
1411
|
+
// An absent template publishes the stored draft as-is — the platform's own
|
|
1412
|
+
// path. The schema is optional so the contract no longer contradicts it.
|
|
1413
|
+
const template = args.template ?? {};
|
|
1414
|
+
validateCallerActivities(template, "workflow.publish");
|
|
1415
|
+
validateWorkflowRouteConditions(template, "workflow.publish");
|
|
1416
|
+
assertTemplateNotMinimal(template, "workflow.publish");
|
|
1417
|
+
lintWorkflowTemplate(template, "workflow.publish");
|
|
1418
|
+
// Ticket 112: same merge-before-write as update. A template without both
|
|
1419
|
+
// boundaries is a partial edit → delta merge (see activitiesLackBoundaries).
|
|
1420
|
+
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, activitiesLackBoundaries(template));
|
|
1421
|
+
validateWorkflowRender(body, "workflow.publish");
|
|
1422
|
+
const performed = await wireCall(session, { path: ENDPOINTS.workflowPublish, method: "POST", body });
|
|
1423
|
+
// A refused publish answers errcode 0 — the ValidateMessage[] is the truth.
|
|
1424
|
+
assertWorkflowWriteAccepted(performed, "workflow.publish", args.code);
|
|
1425
|
+
const published = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.code } });
|
|
1426
|
+
const data = (published.data ?? null);
|
|
1427
|
+
// Ticket 77: publish evidence is the published-face MARKER, not data
|
|
1428
|
+
// presence (the endpoint answers the draft shape when unpublished).
|
|
1429
|
+
const isPublished = data !== null && (data.templateType === "PUBLISH" || data.published === true);
|
|
1430
|
+
if (!data)
|
|
1431
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.publish returned no published template" });
|
|
1432
|
+
const warnings = workflowValidationWarnings(performed.data);
|
|
1433
|
+
// The platform accepted the write (no ERROR) yet the published face still
|
|
1434
|
+
// shows the draft marker: surface that honestly instead of implying the
|
|
1435
|
+
// flow went live.
|
|
1436
|
+
if (!isPublished)
|
|
1437
|
+
throw new ToolRefusalError({
|
|
1438
|
+
status: "refused",
|
|
1439
|
+
blocker: "workflow-publish-not-applied",
|
|
1440
|
+
workflowCode: args.code,
|
|
1441
|
+
message: `workflow.publish: 平台接受写入(errcode 0)但读回仍是草稿形态(templateType=${String(data.templateType ?? "null")}, published=${String(data.published ?? "null")})——流程未生效。先 workflow.validate 复核模板,修正后重试`,
|
|
1442
|
+
});
|
|
1443
|
+
validateWorkflowRender(data, "workflow.publish readback");
|
|
1444
|
+
const participantWarnings = participantReferenceWarnings(data);
|
|
1445
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, published: isPublished, workflowVersion: data.workflowVersion ?? null, ...(warnings.length ? { warnings } : {}), ...(participantWarnings.length ? { participantWarnings } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
1446
|
+
});
|
|
1447
|
+
writeTool(tools, "workflow.bindRule", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.bindRule"].scope, WORKFLOW_TOOLS["workflow.bindRule"].description, {
|
|
1448
|
+
schemaCode: z.string().min(1),
|
|
1449
|
+
workflowCode: z.string().min(1),
|
|
1450
|
+
ruleCode: z.string().min(1).optional().describe("single-rule form: the rule to bind (mutually exclusive with ruleCodes)"),
|
|
1451
|
+
ruleCodes: z.array(z.string().min(1)).min(1).optional().describe("batch form: several rules bound in ONE update + publish; MERGE semantics — the handler's existing bizActions are preserved and the listed rules appended (deduplicated)"),
|
|
1452
|
+
handler: z
|
|
1453
|
+
.string()
|
|
1454
|
+
.optional()
|
|
1455
|
+
.describe("event handler to bind: start | cancel | activate | end (default end)"),
|
|
1456
|
+
publish: z.boolean().optional(),
|
|
1457
|
+
}, async (session, args) => {
|
|
1458
|
+
const { key: handlerKey, field: handlerField } = resolveEventHandlerField(args.handler);
|
|
1459
|
+
const codes = (args.ruleCodes ?? [args.ruleCode]).map(String);
|
|
1460
|
+
// Only CUSTOM rules are bindable to workflow events — the designer's picker
|
|
1461
|
+
// does not offer preset trigger rows (bench59 bound `Create` to `end`).
|
|
1462
|
+
const bindableByCode = await assertBindableWorkflowRules(session, args.schemaCode, codes);
|
|
1463
|
+
// A bound workflow rule MUST be enabled (else 失效/未启用). Enable any
|
|
1464
|
+
// disabled bound custom rule before writing the binding.
|
|
1465
|
+
const { enabledRuleCodes, alreadyEnabledRuleCodes } = await ensureWorkflowRulesEnabled(session, args.schemaCode, codes, bindableByCode);
|
|
1466
|
+
const shouldPublish = args.publish !== false;
|
|
1467
|
+
const draft = (await getWorkflowDraftOrNull(session, args.workflowCode)) ?? {};
|
|
1468
|
+
if (Object.keys(draft).length === 0) {
|
|
1469
|
+
throw new ToolRefusalError({
|
|
1470
|
+
status: "refused",
|
|
1471
|
+
blocker: "workflow-not-found",
|
|
1472
|
+
workflowCode: args.workflowCode,
|
|
1473
|
+
message: `流程 ${args.workflowCode} 不存在(get_draft 401001 缺席)——先 workflow.create,再 bindRule`,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
const existing = (draft[handlerField] ?? {});
|
|
1477
|
+
const priorActions = Array.isArray(existing.bizActions)
|
|
1478
|
+
? existing.bizActions.filter((value) => typeof value === "string")
|
|
1479
|
+
: [];
|
|
1480
|
+
// MERGE: existing bizActions are preserved; the listed rules are
|
|
1481
|
+
// appended in order, deduplicated (same semantics the single-rule form
|
|
1482
|
+
// has always had — replace is NOT the contract).
|
|
1483
|
+
const nextActions = [...priorActions];
|
|
1484
|
+
for (const code of codes)
|
|
1485
|
+
if (!nextActions.includes(code))
|
|
1486
|
+
nextActions.push(code);
|
|
1487
|
+
const nextTemplate = {
|
|
1488
|
+
...draft,
|
|
1489
|
+
[handlerField]: {
|
|
1490
|
+
...(typeof existing === "object" && existing ? existing : {}),
|
|
1491
|
+
bizActions: nextActions,
|
|
1492
|
+
bizActionType: "BIZ_RULE",
|
|
1493
|
+
},
|
|
1494
|
+
workflowCode: args.workflowCode,
|
|
1495
|
+
schemaCode: args.schemaCode,
|
|
1496
|
+
};
|
|
1497
|
+
validateWorkflowRender(nextTemplate, "workflow.bindRule");
|
|
1498
|
+
const performed = await wireCall(session, {
|
|
1499
|
+
path: ENDPOINTS.workflowUpdate,
|
|
1500
|
+
method: "POST",
|
|
1501
|
+
body: nextTemplate,
|
|
1502
|
+
});
|
|
1503
|
+
let publishedReadback = null;
|
|
1504
|
+
let publishMarked = false;
|
|
1505
|
+
if (shouldPublish) {
|
|
1506
|
+
const publishPerformed = await wireCall(session, {
|
|
1507
|
+
path: ENDPOINTS.workflowPublish,
|
|
1508
|
+
method: "POST",
|
|
1509
|
+
body: nextTemplate,
|
|
1510
|
+
});
|
|
1511
|
+
// A refused publish answers errcode 0 with the truth in ValidateMessage[].
|
|
1512
|
+
assertWorkflowWriteAccepted(publishPerformed, "workflow.bindRule", args.workflowCode);
|
|
1513
|
+
const pub = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.workflowCode } });
|
|
1514
|
+
publishedReadback = (pub.data ?? null);
|
|
1515
|
+
// Authoritative marker, not the request echo (ticket 77 semantics).
|
|
1516
|
+
publishMarked = publishedReadback !== null && (publishedReadback.templateType === "PUBLISH" || publishedReadback.published === true);
|
|
1517
|
+
}
|
|
1518
|
+
const draftReadback = await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode: args.workflowCode } });
|
|
1519
|
+
const data = (draftReadback.data ?? null);
|
|
1520
|
+
if (!data)
|
|
1521
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.bindRule returned no draft" });
|
|
1522
|
+
validateWorkflowRender(data, "workflow.bindRule readback");
|
|
1523
|
+
const handler = data ? data[handlerField] : undefined;
|
|
1524
|
+
const bizActions = Array.isArray(handler?.bizActions) ? handler.bizActions.filter((value) => typeof value === "string") : [];
|
|
1525
|
+
const publishedHandler = publishedReadback ? publishedReadback[handlerField] : undefined;
|
|
1526
|
+
const publishedBizActions = Array.isArray(publishedHandler?.bizActions)
|
|
1527
|
+
? publishedHandler.bizActions.filter((value) => typeof value === "string")
|
|
1528
|
+
: [];
|
|
1529
|
+
return {
|
|
1530
|
+
performed: { errcode: performed.errcode, handler: handlerKey, addedRuleCodes: codes, bizActions, publishRequested: shouldPublish },
|
|
1531
|
+
readback: {
|
|
1532
|
+
workflowCode: args.workflowCode,
|
|
1533
|
+
handler: handlerKey,
|
|
1534
|
+
handlerField,
|
|
1535
|
+
semantics: "merge",
|
|
1536
|
+
bound: codes.every((code) => bizActions.includes(code)),
|
|
1537
|
+
bizActions,
|
|
1538
|
+
// Workflow-bound rules must be enabled; report the enable delta.
|
|
1539
|
+
enabledRuleCodes,
|
|
1540
|
+
alreadyEnabledRuleCodes,
|
|
1541
|
+
bizActionType: typeof handler?.bizActionType === "string" ? handler.bizActionType : null,
|
|
1542
|
+
templateFieldsPreserved: Object.keys(draft).every((key) => key === handlerField || data[key] !== undefined),
|
|
1543
|
+
// Authoritative published-face marker, NOT the request echo: a
|
|
1544
|
+
// refused publish keeps errcode 0 and the draft marker (live 8.6.26).
|
|
1545
|
+
published: publishMarked,
|
|
1546
|
+
publishedBound: codes.every((code) => publishedBizActions.includes(code)),
|
|
1547
|
+
publishedBizActions,
|
|
1548
|
+
publishedBizActionType: typeof publishedHandler?.bizActionType === "string" ? publishedHandler.bizActionType : null,
|
|
1549
|
+
...(participantReferenceWarnings(data).length ? { participantWarnings: participantReferenceWarnings(data) } : {}),
|
|
1550
|
+
...(bizActions.length > 1
|
|
1551
|
+
? { advisory: `同一事件处理器绑定了 ${bizActions.length} 条规则,它们会**全部执行**(没有互斥):审批通过/驳回这类互斥结果不要用两条各自 FIXED 赋值的规则——两次都会写、后者覆盖前者(bench22 病灶:通过→已通过、驳回→已驳回 两条各自 FIXED 赋值都绑 endEventHandler)。正确做法二选一:① **绑定到不同处理器/触发**——通过绑流程结束处理器 \`end\`(endEventHandler)或流程生效规则 \`Available\`,驳回绑流程作废处理器 \`cancel\`(cancelEventHandler)或规则 \`Cancel\`;② **一条规则内部按条件分支**(规则图用 BRANCH/filterCondition/dataCondition 读审批结果,多分支各写各的)。真实生产(2026-09-23,case_info_update)只在 endEventHandler 绑一条多分支图(updateData),由该图内部分支完成回写。` }
|
|
1552
|
+
: {}),
|
|
1553
|
+
workflowRenderCompat: workflowRenderCompat(data),
|
|
1554
|
+
},
|
|
1555
|
+
};
|
|
1556
|
+
}, (args) => {
|
|
1557
|
+
resolveEventHandlerField(args.handler);
|
|
1558
|
+
const hasSingle = typeof args.ruleCode === "string" && args.ruleCode.length > 0;
|
|
1559
|
+
const hasMany = Array.isArray(args.ruleCodes);
|
|
1560
|
+
if (hasSingle && hasMany)
|
|
1561
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-bindrule-codes-exclusive", message: "ruleCode 与 ruleCodes 互斥:单规则用 ruleCode,多规则用 ruleCodes[]" });
|
|
1562
|
+
if (!hasSingle && !hasMany)
|
|
1563
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-bindrule-codes-invalid", message: "需要 ruleCode(单个)或 ruleCodes(数组)之一" });
|
|
1564
|
+
if (hasMany) {
|
|
1565
|
+
const codes = args.ruleCodes;
|
|
1566
|
+
if (codes.length === 0 || new Set(codes.map(String)).size !== codes.length || codes.some((code) => typeof code !== "string" || code.length === 0))
|
|
1567
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-bindrule-codes-invalid", message: "ruleCodes 必须是非空、去重的规则 code 数组" });
|
|
1568
|
+
}
|
|
1569
|
+
});
|
|
1570
|
+
writeTool(tools, "workflow.delete", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.delete"].scope, WORKFLOW_TOOLS["workflow.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
|
|
1571
|
+
const performed = await wireCall(session, {
|
|
1572
|
+
path: ENDPOINTS.workflowDelete,
|
|
1573
|
+
method: "POST",
|
|
1574
|
+
query: { workflowCode: args.code },
|
|
1575
|
+
});
|
|
1576
|
+
const header = await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode: args.code } }).catch(() => ({ data: null }));
|
|
1577
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, absent: header.data === null || header.data === undefined } };
|
|
1578
|
+
});
|
|
1579
|
+
// ---- high-level greenfield orchestration (round 10 ergonomics) --------
|
|
1580
|
+
// The survey's write-face gap: a first workflow took create → update →
|
|
1581
|
+
// publish (+ bindRule for rules), 3–4 separate CLI calls. configure_workflow
|
|
1582
|
+
// runs the SAME atomic steps through each tool's own writeExecute, so the
|
|
1583
|
+
// gates/readbacks are identical, in one governed package.
|
|
1584
|
+
registerPackageTool(tools, {
|
|
1585
|
+
name: "configure_workflow",
|
|
1586
|
+
lane: WORKFLOW_LANE,
|
|
1587
|
+
scope: WORKFLOW_TOOLS["configure_workflow"].scope,
|
|
1588
|
+
description: WORKFLOW_TOOLS["configure_workflow"].description,
|
|
1589
|
+
inputSchema: {
|
|
1590
|
+
schemaCode: z.string().min(1),
|
|
1591
|
+
name: z.string().min(1),
|
|
1592
|
+
code: z.string().min(1).describe("workflowCode"),
|
|
1593
|
+
template: workflowTemplateArg.describe("完整模板(activities + routes);工具内部合并到草稿,含条件分支写法见 workflow.update"),
|
|
1594
|
+
bindRules: z
|
|
1595
|
+
.array(z.object({ handler: z.string().optional(), ruleCodes: z.array(z.string().min(1)).min(1) }))
|
|
1596
|
+
.optional()
|
|
1597
|
+
.describe("可选:发布后绑定业务规则;每项 {handler?,ruleCodes[]},handler 取 start|cancel|activate|end(缺省 end)"),
|
|
1598
|
+
},
|
|
1599
|
+
plan: (args) => {
|
|
1600
|
+
const actions = [
|
|
1601
|
+
{ tool: "workflow.create", args: { schemaCode: args.schemaCode, name: args.name, code: args.code } },
|
|
1602
|
+
{ tool: "workflow.update", args: { schemaCode: args.schemaCode, code: args.code, template: args.template } },
|
|
1603
|
+
{ tool: "workflow.publish", args: { schemaCode: args.schemaCode, code: args.code } },
|
|
1604
|
+
];
|
|
1605
|
+
for (const binding of args.bindRules ?? [])
|
|
1606
|
+
actions.push({ tool: "workflow.bindRule", args: { schemaCode: args.schemaCode, workflowCode: args.code, ruleCodes: binding.ruleCodes, ...(binding.handler ? { handler: binding.handler } : {}) } });
|
|
1607
|
+
return { summary: writeSummary("configure_workflow", args, undefined, "plan"), actions };
|
|
1608
|
+
},
|
|
1609
|
+
execute: async (session, args) => {
|
|
1610
|
+
const steps = [];
|
|
1611
|
+
const run = async (toolName, toolArgs) => {
|
|
1612
|
+
const target = siblings.find((tool) => tool.name === toolName);
|
|
1613
|
+
if (!target?.writeExecute)
|
|
1614
|
+
throw new ToolRefusalError({ status: "refused", blocker: "configure-workflow-step-unavailable", message: `configure_workflow 需要 ${toolName},但它未注册写执行` });
|
|
1615
|
+
try {
|
|
1616
|
+
const outcome = await target.writeExecute(session, toolArgs);
|
|
1617
|
+
steps.push({ step: toolName, ...outcome });
|
|
1618
|
+
return outcome.readback;
|
|
1619
|
+
}
|
|
1620
|
+
catch (error) {
|
|
1621
|
+
const envelope = error instanceof ToolRefusalError ? { ...error.payload } : error instanceof WireError ? { blocker: `wire-${error.kind}`, errcode: error.errcode, message: error.message } : { blocker: "configure-workflow-step-failed", message: String(error) };
|
|
1622
|
+
return { __failed: true, tool: toolName, index: steps.length, envelope };
|
|
1623
|
+
}
|
|
1624
|
+
};
|
|
1625
|
+
for (const [toolName, toolArgs] of [
|
|
1626
|
+
["workflow.create", { schemaCode: args.schemaCode, name: args.name, code: args.code }],
|
|
1627
|
+
["workflow.update", { schemaCode: args.schemaCode, code: args.code, template: args.template }],
|
|
1628
|
+
["workflow.publish", { schemaCode: args.schemaCode, code: args.code }],
|
|
1629
|
+
]) {
|
|
1630
|
+
const readback = await run(toolName, toolArgs);
|
|
1631
|
+
if (readback && readback.__failed)
|
|
1632
|
+
return { status: "refused", steps, failedStep: { index: steps.length, tool: toolName }, envelope: readback.envelope };
|
|
1633
|
+
}
|
|
1634
|
+
for (const binding of args.bindRules ?? []) {
|
|
1635
|
+
const readback = await run("workflow.bindRule", { schemaCode: args.schemaCode, workflowCode: args.code, ruleCodes: binding.ruleCodes, ...(binding.handler ? { handler: binding.handler } : {}) });
|
|
1636
|
+
if (readback && readback.__failed)
|
|
1637
|
+
return { status: "refused", steps, failedStep: { index: steps.length, tool: "workflow.bindRule" }, envelope: readback.envelope };
|
|
1638
|
+
}
|
|
1639
|
+
return { steps, readback: { workflowCode: args.code, published: true, boundHandlers: (args.bindRules ?? []).map((binding) => binding.handler ?? "end") } };
|
|
1640
|
+
},
|
|
1641
|
+
summarize: (args, _outcome, phase) => writeSummary("configure_workflow", args, undefined, phase),
|
|
1642
|
+
});
|
|
1643
|
+
tools.push(defineTool({
|
|
1644
|
+
name: "workitem.list",
|
|
1645
|
+
layer: "low",
|
|
1646
|
+
kind: "read",
|
|
1647
|
+
lane: WORKFLOW_LANE,
|
|
1648
|
+
scope: WORKFLOW_TOOLS["workitem.list"].scope,
|
|
1649
|
+
description: WORKFLOW_TOOLS["workitem.list"].description,
|
|
1650
|
+
inputSchema: {
|
|
1651
|
+
environment: environmentArg,
|
|
1652
|
+
workflowInstanceId: z
|
|
1653
|
+
.string()
|
|
1654
|
+
.optional()
|
|
1655
|
+
.describe("exact-match client-side filter on the returned items' instanceId (the server filterWorkflowInstanceId field is a NOT-equal exclusion, so it is not used)"),
|
|
1656
|
+
workflowCode: z.string().optional(),
|
|
1657
|
+
schemaCode: z.string().optional(),
|
|
1658
|
+
page: z.number().int().min(0).optional(),
|
|
1659
|
+
size: z
|
|
1660
|
+
.number()
|
|
1661
|
+
.int()
|
|
1662
|
+
.min(1)
|
|
1663
|
+
.max(200)
|
|
1664
|
+
.optional()
|
|
1665
|
+
.describe("page size; defaults to 200 when workflowInstanceId is set (single-page scan) else 20"),
|
|
1666
|
+
capability: capabilityArg,
|
|
1667
|
+
},
|
|
1668
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
1669
|
+
const page = await searchWorkItems(session, {
|
|
1670
|
+
...(args.workflowInstanceId ? { instanceId: args.workflowInstanceId } : {}),
|
|
1671
|
+
...(args.workflowCode ? { workflowCode: args.workflowCode } : {}),
|
|
1672
|
+
...(args.schemaCode ? { schemaCode: args.schemaCode } : {}),
|
|
1673
|
+
page: args.page ?? 0,
|
|
1674
|
+
size: args.size ?? (args.workflowInstanceId ? 200 : 20),
|
|
1675
|
+
});
|
|
1676
|
+
const selected = page.items;
|
|
1677
|
+
// Resolve the in-flight bizObject id per distinct instance. Authoritative
|
|
1678
|
+
// source: getBizObjectByWfInstanceId -> BizObjectCreatedModel.id/schemaCode.
|
|
1679
|
+
// Best-effort: a failed lookup leaves the identity null, never drops the todo.
|
|
1680
|
+
const objectByInstance = new Map();
|
|
1681
|
+
for (const item of selected) {
|
|
1682
|
+
const instanceId = wireString(item.instanceId);
|
|
1683
|
+
if (!instanceId || objectByInstance.has(instanceId))
|
|
1684
|
+
continue;
|
|
1685
|
+
objectByInstance.set(instanceId, await resolveBizObjectIdByInstance(session, instanceId));
|
|
1686
|
+
}
|
|
1687
|
+
const items = selected.map((item) => {
|
|
1688
|
+
const instanceId = wireString(item.instanceId);
|
|
1689
|
+
const linked = instanceId ? objectByInstance.get(instanceId) : undefined;
|
|
1690
|
+
return {
|
|
1691
|
+
workItemId: wireString(item.id),
|
|
1692
|
+
workflowInstanceId: instanceId,
|
|
1693
|
+
workflowCode: wireString(item.workflowCode),
|
|
1694
|
+
activityCode: wireString(item.activityCode),
|
|
1695
|
+
activityName: wireString(item.activityName),
|
|
1696
|
+
state: wireString(item.state),
|
|
1697
|
+
sequenceNo: wireString(item.sequenceNo),
|
|
1698
|
+
originator: wireString(item.originator),
|
|
1699
|
+
originatorName: wireString(item.originatorName),
|
|
1700
|
+
participant: wireString(item.participant),
|
|
1701
|
+
participantName: wireString(item.participantName),
|
|
1702
|
+
bizObjectId: linked?.bizObjectId ?? null,
|
|
1703
|
+
schemaCode: linked?.schemaCode ?? wireString(item.schemaCode),
|
|
1704
|
+
};
|
|
1705
|
+
});
|
|
1706
|
+
return {
|
|
1707
|
+
total: page.total,
|
|
1708
|
+
totalPages: page.totalPages,
|
|
1709
|
+
count: items.length,
|
|
1710
|
+
items,
|
|
1711
|
+
};
|
|
1712
|
+
}),
|
|
1713
|
+
}));
|
|
1714
|
+
tools.push(defineTool({
|
|
1715
|
+
name: "org.role.list",
|
|
1716
|
+
layer: "low",
|
|
1717
|
+
kind: "read",
|
|
1718
|
+
lane: WORKFLOW_LANE,
|
|
1719
|
+
scope: WORKFLOW_TOOLS["org.role.list"].scope,
|
|
1720
|
+
description: WORKFLOW_TOOLS["org.role.list"].description,
|
|
1721
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
1722
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
1723
|
+
const data = (await wireCall(session, { path: ENDPOINTS.organizationRoleList })).data;
|
|
1724
|
+
const roles = [];
|
|
1725
|
+
for (const group of wireRows(data)) {
|
|
1726
|
+
const children = Array.isArray(group.children) ? group.children : [];
|
|
1727
|
+
if (children.length === 0)
|
|
1728
|
+
roles.push(roleOf(group, null));
|
|
1729
|
+
else
|
|
1730
|
+
for (const child of children)
|
|
1731
|
+
roles.push(roleOf(child, String(group.name ?? "")));
|
|
1732
|
+
}
|
|
1733
|
+
return { count: roles.length, roles };
|
|
1734
|
+
}),
|
|
1735
|
+
}));
|
|
1736
|
+
tools.push(defineTool({
|
|
1737
|
+
name: "org.department.list",
|
|
1738
|
+
layer: "low",
|
|
1739
|
+
kind: "read",
|
|
1740
|
+
lane: WORKFLOW_LANE,
|
|
1741
|
+
scope: WORKFLOW_TOOLS["org.department.list"].scope,
|
|
1742
|
+
description: WORKFLOW_TOOLS["org.department.list"].description,
|
|
1743
|
+
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
1744
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
1745
|
+
const data = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentTree })).data;
|
|
1746
|
+
const list = Array.isArray(data?.departmentList) ? data.departmentList : wireRows(data);
|
|
1747
|
+
const departments = list.map((dept) => ({
|
|
1748
|
+
id: dept.id ?? null,
|
|
1749
|
+
name: dept.name ?? null,
|
|
1750
|
+
code: dept.code ?? null,
|
|
1751
|
+
corpId: dept.corpId ?? null,
|
|
1752
|
+
parentId: dept.parentId ?? null,
|
|
1753
|
+
}));
|
|
1754
|
+
return { count: departments.length, departments };
|
|
1755
|
+
}),
|
|
1756
|
+
}));
|
|
1757
|
+
tools.push(defineTool({
|
|
1758
|
+
name: "org.user.search",
|
|
1759
|
+
layer: "low",
|
|
1760
|
+
kind: "read",
|
|
1761
|
+
lane: WORKFLOW_LANE,
|
|
1762
|
+
scope: WORKFLOW_TOOLS["org.user.search"].scope,
|
|
1763
|
+
description: WORKFLOW_TOOLS["org.user.search"].description,
|
|
1764
|
+
inputSchema: {
|
|
1765
|
+
environment: environmentArg,
|
|
1766
|
+
// `wd` is the platform's own key; `keyword`/`q` are accepted aliases so a
|
|
1767
|
+
// caller that reaches for the natural word is not bounced with
|
|
1768
|
+
// "wd: Required" (round 10: an agent sent `keyword` and burned a retry).
|
|
1769
|
+
wd: z.string().min(1).optional().describe("搜索关键字(用户姓名或账号用户名);平台 \"关键字不能为空\" 即缺它。可用 keyword/q 作别名"),
|
|
1770
|
+
keyword: z.string().min(1).optional().describe("`wd` 的别名"),
|
|
1771
|
+
q: z.string().min(1).optional().describe("`wd` 的别名"),
|
|
1772
|
+
page: z.number().int().min(0).optional().describe("页号,默认 0"),
|
|
1773
|
+
size: z.number().int().min(1).optional().describe("每页条数,默认 100"),
|
|
1774
|
+
filterType: z.string().optional().describe("admin=只搜当前应用管理员可见范围(可省)"),
|
|
1775
|
+
capability: capabilityArg,
|
|
1776
|
+
},
|
|
1777
|
+
handler: async (ctx, args, self) => {
|
|
1778
|
+
const keyword = args.wd ?? args.keyword ?? args.q;
|
|
1779
|
+
if (!keyword)
|
|
1780
|
+
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "org.user.search", message: "需要 wd(关键字);keyword/q 亦可" });
|
|
1781
|
+
return governedRead(ctx, self, { ...args, wd: keyword }, async (session) => {
|
|
1782
|
+
const body = { wd: keyword, page: args.page ?? 0, size: args.size ?? 100 };
|
|
1783
|
+
if (args.filterType !== undefined)
|
|
1784
|
+
body.filterType = args.filterType;
|
|
1785
|
+
const data = (await wireCall(session, { path: ENDPOINTS.organizationUserSearch, method: "POST", body })).data;
|
|
1786
|
+
const rows = Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
1787
|
+
const users = rows.map((user) => ({
|
|
1788
|
+
id: user.id ?? null,
|
|
1789
|
+
name: user.name ?? null,
|
|
1790
|
+
username: user.username ?? null,
|
|
1791
|
+
userId: user.userId ?? null,
|
|
1792
|
+
code: user.code ?? null,
|
|
1793
|
+
departmentId: user.departmentId ?? null,
|
|
1794
|
+
corpId: user.corpId ?? null,
|
|
1795
|
+
status: user.status ?? null,
|
|
1796
|
+
}));
|
|
1797
|
+
return { keyword, count: users.length, total: data?.totalElements ?? users.length, users };
|
|
1798
|
+
});
|
|
1799
|
+
},
|
|
1800
|
+
}));
|
|
1801
|
+
}
|
|
1802
|
+
/** Flatten a role-group/role row into the participant-construction fields. */
|
|
1803
|
+
function roleOf(row, group) {
|
|
1804
|
+
return {
|
|
1805
|
+
id: row.id ?? null,
|
|
1806
|
+
name: row.name ?? null,
|
|
1807
|
+
code: row.code ?? null,
|
|
1808
|
+
roleType: row.roleType ?? null,
|
|
1809
|
+
corpId: row.corpId ?? null,
|
|
1810
|
+
...(group !== null ? { group } : {}),
|
|
1811
|
+
};
|
|
1812
|
+
}
|
|
1813
|
+
//# sourceMappingURL=workflow.js.map
|