@stalfh233/omc-cli 0.4.92 → 0.4.94
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/dist/approval-flow.js +21 -5
- package/dist/cli-contract.js +35 -0
- package/dist/cli.js +30 -6
- package/dist/lanes/custom-page.js +6 -2
- package/dist/lanes/rule-graph.js +57 -2
- package/dist/lanes/runtime-data.js +141 -5
- package/dist/model-purge.js +4 -0
- package/package.json +3 -3
package/dist/approval-flow.js
CHANGED
|
@@ -93,7 +93,7 @@ export async function cmdApprovalFlow(args) {
|
|
|
93
93
|
return 0;
|
|
94
94
|
}
|
|
95
95
|
// 1) 确保流程存在(一个模型只能有一个:已存在则直接 update)
|
|
96
|
-
const createOutcome = await runToolCall("workflow.create", { args: { schemaCode: schema, code: compiled.workflowCode, name: `${schema} 审批流`, ...(environment ? { environment } : {}) }, requestId: request, environment });
|
|
96
|
+
const createOutcome = await runToolCall("workflow.create", { args: { schemaCode: schema, code: compiled.workflowCode, name: `${schema} 审批流`, ...(environment ? { environment } : {}) }, requestId: request, environment, print: () => undefined });
|
|
97
97
|
if (createOutcome.exitCode !== 0) {
|
|
98
98
|
const result = createOutcome.result;
|
|
99
99
|
const blocker = String(result.blocker ?? "");
|
|
@@ -102,8 +102,24 @@ export async function cmdApprovalFlow(args) {
|
|
|
102
102
|
return createOutcome.exitCode;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
// r14 U1: `<schema>_wf` is THIS composer's own naming convention (not the
|
|
106
|
+
// platform's), and `workflow.create` is idempotent — when that code already
|
|
107
|
+
// exists it answers alreadyPresent and writes nothing, so the following
|
|
108
|
+
// update() merges into whatever template was left behind. A residual from an
|
|
109
|
+
// earlier round then silently inherits the new nodes (r14 chased exactly this:
|
|
110
|
+
// a leftover inspHazardFlow made the next run's behaviour inexplicable).
|
|
111
|
+
const createReadback = createOutcome.result?.readback;
|
|
112
|
+
const preexistingTemplate = createReadback?.alreadyPresent === true;
|
|
113
|
+
const preexistingSection = preexistingTemplate
|
|
114
|
+
? {
|
|
115
|
+
preexistingTemplate: true,
|
|
116
|
+
preexistingWarning: `流程 code ${compiled.workflowCode} 在本轮之前就已存在(workflow.create 幂等跳过,未新建)。` +
|
|
117
|
+
`本次 update 是**合并**进这条既有模板,旧节点/旧连线会保留——它们可能属于上一次运行或另一个模型。` +
|
|
118
|
+
`如果不是有意复用,先 workflow.delete(有实例会 402508,用 model-purge)或换一个 workflowCode 再跑。`,
|
|
119
|
+
}
|
|
120
|
+
: {};
|
|
105
121
|
// 2) 取字段清单构建审批节点只读权限(r75 修正:审批节点 editable:false)
|
|
106
|
-
const fieldList = await runToolCall("field.list", { args: { schemaCode: schema, published: true, ...(environment ? { environment } : {}) }, requestId: request, environment });
|
|
122
|
+
const fieldList = await runToolCall("field.list", { args: { schemaCode: schema, published: true, ...(environment ? { environment } : {}) }, requestId: request, environment, print: () => undefined });
|
|
107
123
|
const fieldRows = (fieldList.result?.fields ?? fieldList.result?.items ?? []);
|
|
108
124
|
const propertyCodes = fieldRows.map((row) => String(row.code ?? row.propertyCode ?? "")).filter(Boolean);
|
|
109
125
|
const approvalNodes = compiled.activities.filter((a) => a.activityType === "PARTICIPANT" && a.participant !== "{Originator}");
|
|
@@ -113,16 +129,16 @@ export async function cmdApprovalFlow(args) {
|
|
|
113
129
|
// 3) update 模板(merge 语义;工具自动补 sheetCode/链路/设计器默认)
|
|
114
130
|
const updateOutcome = await runToolCall("workflow.update", {
|
|
115
131
|
args: { schemaCode: schema, code: compiled.workflowCode, template: { workflowCode: compiled.workflowCode, activities: activitiesWithPermissions }, ...(environment ? { environment } : {}) },
|
|
116
|
-
requestId: request, environment,
|
|
132
|
+
requestId: request, environment, print: () => undefined,
|
|
117
133
|
});
|
|
118
134
|
if (updateOutcome.exitCode !== 0) {
|
|
119
135
|
writeEnvelope(envelope(classifyResult(updateOutcome.result).status, updateOutcome.result, request, classifyResult(updateOutcome.result).error));
|
|
120
136
|
return updateOutcome.exitCode;
|
|
121
137
|
}
|
|
122
138
|
// 4) publish
|
|
123
|
-
const publishOutcome = await runToolCall("workflow.publish", { args: { schemaCode: schema, code: compiled.workflowCode, ...(environment ? { environment } : {}) }, requestId: request, environment });
|
|
139
|
+
const publishOutcome = await runToolCall("workflow.publish", { args: { schemaCode: schema, code: compiled.workflowCode, ...(environment ? { environment } : {}) }, requestId: request, environment, print: () => undefined });
|
|
124
140
|
const classified = classifyResult(publishOutcome.result);
|
|
125
|
-
writeEnvelope(envelope(classified.status, { schemaCode: schema, workflowCode: compiled.workflowCode, steps: steps.length, publish: publishOutcome.result }, request, classified.error));
|
|
141
|
+
writeEnvelope(envelope(classified.status, { schemaCode: schema, workflowCode: compiled.workflowCode, steps: steps.length, ...preexistingSection, publish: publishOutcome.result }, request, classified.error));
|
|
126
142
|
if (publishOutcome.exitCode === 0)
|
|
127
143
|
process.stderr.write("[omc] 前台生效验证清单:1) 造一条数据提交走流程;2) 每级审批人在门户有待办(角色用 FindUserByRole 需审批人挂在发起人同部门,否则 PENDING 不来);3) 审批节点字段只读(不可改单);4) 驳回退发起人核对;5) 设计器打开流程确认节点链显示正常。\n");
|
|
128
144
|
return publishOutcome.exitCode;
|
package/dist/cli-contract.js
CHANGED
|
@@ -75,6 +75,36 @@ export function serializeEnvelope(value) {
|
|
|
75
75
|
export function requestId() {
|
|
76
76
|
return randomUUID();
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* r16 P2: `rule-graph-validation-failed` always named the same example
|
|
80
|
+
* (`business-rule-branch-conditional`) no matter WHICH lint code fired, so a
|
|
81
|
+
* QUERY-shape failure sent the caller to a BRANCH example (2 wasted calls, 0
|
|
82
|
+
* benefit). Route by the first code's prefix and only fall back to the example.
|
|
83
|
+
*/
|
|
84
|
+
export function ruleGraphRecovery(validation) {
|
|
85
|
+
if (!Array.isArray(validation))
|
|
86
|
+
return undefined;
|
|
87
|
+
const codes = validation
|
|
88
|
+
.filter((entry) => entry !== null && typeof entry === "object")
|
|
89
|
+
.filter((entry) => entry.severity !== "warning")
|
|
90
|
+
.map((entry) => String(entry.code ?? ""));
|
|
91
|
+
const first = codes[0];
|
|
92
|
+
if (first === undefined)
|
|
93
|
+
return undefined;
|
|
94
|
+
if (first.startsWith("rule-graph-query-"))
|
|
95
|
+
return "omc describe rule.save --param nodeGraph";
|
|
96
|
+
if (first === "rule-graph-node-output-not-addressable")
|
|
97
|
+
return "omc compute --help";
|
|
98
|
+
if (first.startsWith("rule-graph-assign-"))
|
|
99
|
+
return "omc describe rule.save --param nodeGraph";
|
|
100
|
+
if (first.startsWith("rule-graph-preset-system-node") || first === "rule-graph-system-node")
|
|
101
|
+
return "omc describe rule.save --example standard-business-rule-graph";
|
|
102
|
+
if (first.startsWith("rule-graph-branch") || first.startsWith("rule-graph-condition-"))
|
|
103
|
+
return "omc describe rule.save --example business-rule-branch-conditional";
|
|
104
|
+
if (first.startsWith("rule-graph-create") || first.startsWith("rule-graph-query"))
|
|
105
|
+
return "omc describe rule.save --example rule-cross-model-create";
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
78
108
|
/**
|
|
79
109
|
* Derive a runnable next command from a refusal payload. Tools that already
|
|
80
110
|
* carry a structured recovery (an explicit `nextCommand`, an artifact `fetch`
|
|
@@ -94,6 +124,11 @@ export function nextCommandFor(result) {
|
|
|
94
124
|
const index = trimmed.indexOf("omc ");
|
|
95
125
|
return index > 0 ? trimmed.slice(index).trim() : undefined;
|
|
96
126
|
};
|
|
127
|
+
// A tool that already diagnosed the shape wins over its generic recovery: the
|
|
128
|
+
// validation codes say WHAT is wrong, so they pick the right example.
|
|
129
|
+
const routed = ruleGraphRecovery(result.validation) ?? ruleGraphRecovery(result.nodeChecks);
|
|
130
|
+
if (routed !== undefined)
|
|
131
|
+
return routed;
|
|
97
132
|
const explicit = result.nextCommand;
|
|
98
133
|
if (typeof explicit === "string" && explicit.trim().length > 0)
|
|
99
134
|
return explicit.trim();
|
package/dist/cli.js
CHANGED
|
@@ -419,7 +419,9 @@ async function cmdCall(store, args) {
|
|
|
419
419
|
// `--jq` is the same path projection plus `[]` fan-out; a bare `--jq` is the
|
|
420
420
|
// key inventory (the same muscle memory as `artifact get <id> --jq`).
|
|
421
421
|
const extractPaths = [...argvValues(args, "--extract"), ...argvValues(args, "--jq")];
|
|
422
|
-
|
|
422
|
+
// `artifact get` uses `--keys`; `call` only had `--extract-keys`, so the muscle
|
|
423
|
+
// memory failed with "unknown option '--keys'" (r16 P7). Accept both.
|
|
424
|
+
const extractKeys = args.includes("--extract-keys") || args.includes("--keys") || args.some((token, index) => token === "--jq" && (args[index + 1] === undefined || args[index + 1].startsWith("--")));
|
|
423
425
|
const dryRun = args.includes("--dry-run");
|
|
424
426
|
const waitValue = flag("--wait");
|
|
425
427
|
let waitMs = 0;
|
|
@@ -1086,7 +1088,7 @@ function cmdHelp(topic, commandNames, transport, section, sections = false, all
|
|
|
1086
1088
|
for (const requested of requestedSections) {
|
|
1087
1089
|
const sliced = readSkillSection(topic, requested);
|
|
1088
1090
|
if (sliced?.section)
|
|
1089
|
-
found.push({ section: requested, content: sliced.section });
|
|
1091
|
+
found.push({ section: requested, content: sliced.section, chars: sliced.section.length, bytes: Buffer.byteLength(sliced.section, "utf8") });
|
|
1090
1092
|
else
|
|
1091
1093
|
missing.push(requested);
|
|
1092
1094
|
}
|
|
@@ -1113,7 +1115,18 @@ function cmdHelp(topic, commandNames, transport, section, sections = false, all
|
|
|
1113
1115
|
}
|
|
1114
1116
|
if (!sliced.section)
|
|
1115
1117
|
return emitInvalid(transport, { message: `topic ${topic} 没有 section "${singleSection}";可用:${sliced.headings.join("、")}`, blocker: "help-section-not-found" });
|
|
1116
|
-
|
|
1118
|
+
// r16 cost finding: the same section was re-read SIX times (46 KB) because the
|
|
1119
|
+
// receipt only gave `bytes` while a character count was compared against it.
|
|
1120
|
+
// Emit both, plus `complete:true` so "is this truncated?" never needs a retry.
|
|
1121
|
+
return emitTransportResult({
|
|
1122
|
+
status: "ok",
|
|
1123
|
+
topic: sliced.document.name,
|
|
1124
|
+
section: singleSection,
|
|
1125
|
+
chars: sliced.section.length,
|
|
1126
|
+
bytes: Buffer.byteLength(sliced.section, "utf8"),
|
|
1127
|
+
complete: true,
|
|
1128
|
+
content: sliced.section,
|
|
1129
|
+
}, transport);
|
|
1117
1130
|
}
|
|
1118
1131
|
const document = readSkill(topic);
|
|
1119
1132
|
if (document === undefined) {
|
|
@@ -1282,8 +1295,9 @@ async function main() {
|
|
|
1282
1295
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
1283
1296
|
});
|
|
1284
1297
|
command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
|
|
1285
|
-
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
1286
|
-
const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
|
|
1298
|
+
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
1299
|
+
const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
|
|
1300
|
+
...(options.keys ? ["--keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
|
|
1287
1301
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
1288
1302
|
});
|
|
1289
1303
|
command.command("compute").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--target <propertyCode>").option("--property <propertyCode>").option("--formula <formula>").option("--where <condition>", "条件聚合:\"<子列code> = <值>\",与 --formula \"{<子表code>.<子列code>}\" 配合编译为 COUNTIF/SUMIF").option("--aggregate <count|sum>", "条件聚合用哪种:count(默认,→COUNTIF)/ sum(→SUMIF)").option("--allow-unfiltered", "显式接受「对整个子表列无条件聚合」;不给则拒绝(会把不符合条件的行也算进去)").option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n字段计算规则拼装器:公式 {字段} 引用 → datarule type2 完整形状(editorText/editorMark 自动生成)。默认 dry-run,--apply 写入。条件聚合用 --formula \"{子表.子列}\" --where \"子列 = 值\"。复杂场景(条件分支/跨模型)走 omc next 的 recipe 路径。\n").action(async (options) => {
|
|
@@ -1335,7 +1349,17 @@ async function main() {
|
|
|
1335
1349
|
.option("--no-form", "跳过表单发布")
|
|
1336
1350
|
.option("--force-publish", "有字段失败也发布(默认停在发布前)")
|
|
1337
1351
|
.option("--apply")
|
|
1338
|
-
.addHelpText("after",
|
|
1352
|
+
.addHelpText("after", `\n一条命令建模型:模型 → N 字段(含子表挂载与子列)→ 发布字段 → 发布表单。默认 dry-run 只列计划。\n\n` +
|
|
1353
|
+
`字段族名(propertyType,必须逐字匹配;--apply 才校验,dry-run 不校验):\n` +
|
|
1354
|
+
` SHORT_TEXT LONG_TEXT RICH_TEXT NUMERICAL DATE TIME LOGICAL\n` +
|
|
1355
|
+
` RADIO CHECKBOX DROPDOWN_BOX DROPDOWN_MULTI_BOX\n` +
|
|
1356
|
+
` STAFF_SELECTOR STAFF_MULTI_SELECTOR DEPARTMENT_SELECTOR DEPARTMENT_MULTI_SELECTOR\n` +
|
|
1357
|
+
` WORK_SHEET MULT_WORK_SHEET RELEVANCE_DATA HYPER_LINK ATTACHMENT\n` +
|
|
1358
|
+
` CHILD_TABLE FORMULA ADDRESS\n` +
|
|
1359
|
+
` 常见错写:DEPARTMENT_MULTI→DEPARTMENT_MULTI_SELECTOR、STAFF_SINGLE→STAFF_SELECTOR、DROP_DOWN_BOX→DROPDOWN_BOX\n\n` +
|
|
1360
|
+
`choices(RADIO/CHECKBOX/DROPDOWN 系列):**字符串数组**,如 [\"待处理\",\"已完成\"]——不是 [{key,value}]\n` +
|
|
1361
|
+
`关联字段(WORK_SHEET/MULT_WORK_SHEET):options:{\"schemaCode\":\"<目标模型>\"} + relativeCode:\"<目标模型>\"\n` +
|
|
1362
|
+
`子表:childFields:[…](每条同字段形状)\n`)
|
|
1339
1363
|
.action(async (options) => {
|
|
1340
1364
|
const rest = ["model-scaffold",
|
|
1341
1365
|
...(options.code ? ["--code", options.code] : []),
|
|
@@ -209,8 +209,12 @@ export function registerCustomPageTools(tools) {
|
|
|
209
209
|
code: args.code,
|
|
210
210
|
existDraft: body.existDraft ?? null,
|
|
211
211
|
published: body.published ?? null,
|
|
212
|
-
...(part !== "published" ? { draftContent: draft, draftBytes: draft === null ? 0 : Buffer.byteLength(draft, "utf8") } : {}),
|
|
213
|
-
...(part !== "draft" ? { publishedContent: published, publishedBytes: published === null ? 0 : Buffer.byteLength(published, "utf8") } : {}),
|
|
212
|
+
...(part !== "published" ? { draftContent: draft, draftChars: draft === null ? 0 : draft.length, draftBytes: draft === null ? 0 : Buffer.byteLength(draft, "utf8") } : {}),
|
|
213
|
+
...(part !== "draft" ? { publishedContent: published, publishedChars: published === null ? 0 : published.length, publishedBytes: published === null ? 0 : Buffer.byteLength(published, "utf8") } : {}),
|
|
214
|
+
// r16 P8: `--jq content` returned null because the key is publishedContent.
|
|
215
|
+
// Keep the canonical keys and point at them instead of letting the miss
|
|
216
|
+
// read as "the body is empty".
|
|
217
|
+
contentKeyHint: part === "draft" ? "正文键是 draftContent(不是 content)" : part === "published" ? "正文键是 publishedContent(不是 content)" : "正文键是 draftContent / publishedContent(不是 content)",
|
|
214
218
|
};
|
|
215
219
|
}),
|
|
216
220
|
}));
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -1163,7 +1163,7 @@ export function validateConditionLeaf(leaf, nodeCode, where, warnings, legacyTol
|
|
|
1163
1163
|
else if (String(leaf.ruleConditionType) === "CHANGED")
|
|
1164
1164
|
warnings?.push({ code: "rule-graph-legacy-operator-roundtrip", nodeCode, key: "ruleConditionType", severity: "warning", message: `${where}: ruleConditionType "CHANGED" 是设计器 UI 独有变更检测算子——按原样保留回传(round-trip 保真),OMC 不支持新建此类算子;新增条件请用 ${RULE_CONDITION_TYPES.join("/")}` });
|
|
1165
1165
|
else
|
|
1166
|
-
blockers.push({ code: "rule-graph-condition-type", nodeCode, key: "ruleConditionType", message: `${where}: ruleConditionType "${String(leaf.ruleConditionType)}
|
|
1166
|
+
blockers.push({ code: "rule-graph-condition-type", nodeCode, key: "ruleConditionType", message: `${where}: ruleConditionType "${String(leaf.ruleConditionType)}" 不在 ${RULE_CONDITION_TYPES.join("/")}` });
|
|
1167
1167
|
}
|
|
1168
1168
|
if (leaf[BRANCH_EXIT_OPERATOR_KEY] !== undefined && !RULE_CONDITION_TYPES.includes(String(leaf[BRANCH_EXIT_OPERATOR_KEY]))) {
|
|
1169
1169
|
if (legacyTolerant)
|
|
@@ -1555,6 +1555,54 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1555
1555
|
message: `分支节点后接多个跨模型 UPDATE(${fanOut.join(";")})——真机 2026-10-20 该组合在写/运行时报 50000。改成:① 单个跨模型 UPDATE + 条件动作(dataActions 内按条件区分),或 ② 通过 BIZ_ACTION 自调二开接口一次处理多行。`,
|
|
1556
1556
|
});
|
|
1557
1557
|
}
|
|
1558
|
+
// r16 P1 (costliest finding of the round: 21 rule.save calls, 14 refusals):
|
|
1559
|
+
// a caller who wants "count the child rows that match X and write the number to
|
|
1560
|
+
// a main field" naturally invents a QUERY node output like `Q1_count` and
|
|
1561
|
+
// references it downstream. The platform has NO such addressable output —
|
|
1562
|
+
// RuleVirtualPropertyType only exposes `var_judgment_status/_total/
|
|
1563
|
+
// _match_count/_non_match_count`, which belong to a BRANCH's own judgment, not
|
|
1564
|
+
// to a QUERY result set. The save passes and the PUBLISH self-check then fails
|
|
1565
|
+
// with「字段不存在」naming the invented code, which sends the caller looking for
|
|
1566
|
+
// a missing field. Refuse before the wire and name the two real paths.
|
|
1567
|
+
const nodeCodes = new Set(nodeRecords.map((node) => String(node.nodeCode ?? "")).filter((code) => code.length > 0));
|
|
1568
|
+
const inventedOutputs = [];
|
|
1569
|
+
const referenceKeys = ["currentSchemaDataItem", "currentDataItemValue", "targetSchemaDataItem", "value"];
|
|
1570
|
+
for (const node of nodeRecords) {
|
|
1571
|
+
const nodeCode = String(node.nodeCode ?? "");
|
|
1572
|
+
const walk = (value, where) => {
|
|
1573
|
+
if (Array.isArray(value)) {
|
|
1574
|
+
value.forEach((entry, index) => walk(entry, `${where}[${index}]`));
|
|
1575
|
+
return;
|
|
1576
|
+
}
|
|
1577
|
+
if (value === null || typeof value !== "object")
|
|
1578
|
+
return;
|
|
1579
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1580
|
+
if (referenceKeys.includes(key) && typeof entry === "string") {
|
|
1581
|
+
// `Q1_count` / `SearchData_1_rows` where Q1/SearchData_1 IS a node code.
|
|
1582
|
+
const match = /^([A-Za-z][\w]*?)_(count|rows|total|result|output|size|length)$/.exec(entry.trim());
|
|
1583
|
+
if (match && nodeCodes.has(match[1]) && match[1] !== nodeCode)
|
|
1584
|
+
inventedOutputs.push({ nodeCode, reference: entry.trim(), where: `${where}.${key}` });
|
|
1585
|
+
}
|
|
1586
|
+
walk(entry, `${where}.${key}`);
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
walk(node, nodeCode || "node");
|
|
1590
|
+
}
|
|
1591
|
+
if (inventedOutputs.length > 0) {
|
|
1592
|
+
const first = inventedOutputs[0];
|
|
1593
|
+
blockers.push({
|
|
1594
|
+
code: "rule-graph-node-output-not-addressable",
|
|
1595
|
+
nodeCode: first.nodeCode,
|
|
1596
|
+
key: first.where,
|
|
1597
|
+
message: `引用 ${inventedOutputs.map((entry) => `${entry.reference}(${entry.where})`).join("、")} 试图当"某个节点的输出变量"——平台**没有**这种可寻址输出。` +
|
|
1598
|
+
`QUERY 节点的结果集只在它自己的下游节点里以 inputParam:"_<节点code>_output" 被消费,不能被条件/赋值按名字取值;` +
|
|
1599
|
+
`平台仅有的虚拟属性是 BRANCH 自身的判断统计(var_judgment_status / var_judgment_total / var_judgment_match_count / var_judgment_non_match_count),` +
|
|
1600
|
+
`那是"分支判定的命中数",不是 QUERY 结果行的计数(RuleVirtualPropertyType.java,8.6.26)。` +
|
|
1601
|
+
`要「统计子表符合条件的行数写入主表字段」,用字段计算规则:` +
|
|
1602
|
+
`omc compute --schema <模型> --target <目标字段> --formula '{<子表schemaCode>.<子列code>}' --where '<子列code> = <值>' --aggregate count(编译成 COUNTIF,服务端在 submit/update 时算)。` +
|
|
1603
|
+
`要「按查询结果逐条新增」才用 QUERY + CREATE(N→N 扇出,recipe: rule-cross-model-create)。`,
|
|
1604
|
+
});
|
|
1605
|
+
}
|
|
1558
1606
|
if (schemaCode !== undefined && schemaCode.length > 0) {
|
|
1559
1607
|
const writeTypes = new Set(["UPDATE", "CREATE", "DELETE"]);
|
|
1560
1608
|
const crossModel = nodeRecords.filter((node) => {
|
|
@@ -1843,7 +1891,14 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1843
1891
|
code: "rule-graph-query-conditions-empty",
|
|
1844
1892
|
nodeCode,
|
|
1845
1893
|
key: "targetConditionGroups",
|
|
1846
|
-
message: `QUERY 的 ${mainEmpty ? "mainConditions" : "sheetConditions"} 没有任何条件叶——引擎 getPage 会直接返回空列表且不写日志(QueryActivityBehavior:78-86,1.1.25),运行期表现为「节点执行开始」后静默无输出。要查全表请显式写一条恒真条件叶(如 targetSchemaDataItem:"id" + ruleConditionType:"NOT_EMPTY
|
|
1894
|
+
message: `QUERY 的 ${mainEmpty ? "mainConditions" : "sheetConditions"} 没有任何条件叶——引擎 getPage 会直接返回空列表且不写日志(QueryActivityBehavior:78-86,1.1.25),运行期表现为「节点执行开始」后静默无输出。要查全表请显式写一条恒真条件叶(如 targetSchemaDataItem:"id" + ruleConditionType:"NEP";非空枚举是 EP/NEP,没有 NOT_EMPTY),否则补上真正的筛选条件。` +
|
|
1895
|
+
// r16 P2: the four QUERY requirements used to arrive one refusal at a
|
|
1896
|
+
// time (targetSchemaCode → condition leaf → group shape → 550018).
|
|
1897
|
+
// List them together so one edit satisfies all of them.
|
|
1898
|
+
`\nQUERY 节点的完整要求(一次核对,别一条条试):① targetSchemaCode 必填(子表写 <schemaCode>.<子表code>);` +
|
|
1899
|
+
`② targetConditionGroups 必须是对象 {mainConditions:[[…]],sheetConditions:[[…]]},不能是裸数组;` +
|
|
1900
|
+
`③ 主表条件与子表条件都要至少一条叶(查全表就写 id + NEP 恒真叶);` +
|
|
1901
|
+
`④ 结果集不能被下游按名字引用——下游消费写 inputParam:"_${String(node.nodeCode ?? "")}_output",要汇总计数改用 omc compute。`,
|
|
1847
1902
|
});
|
|
1848
1903
|
}
|
|
1849
1904
|
if (node.dataCondition !== undefined && node.dataCondition !== null && countConditionLeaves(node.dataCondition) === 0) {
|
|
@@ -288,6 +288,66 @@ async function loadExistingBizObject(session, schemaCode, sheetCode, objectId) {
|
|
|
288
288
|
throw error;
|
|
289
289
|
}
|
|
290
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Is this work item still the caller's open todo? `false` = it was already
|
|
293
|
+
* submitted (the usual meaning of 100005, since the platform rate-limits per
|
|
294
|
+
* work item). `null` = could not tell.
|
|
295
|
+
*/
|
|
296
|
+
async function workItemStillOpen(session, workflowInstanceId, workItemId) {
|
|
297
|
+
if (typeof workflowInstanceId !== "string" || workflowInstanceId.length === 0)
|
|
298
|
+
return null;
|
|
299
|
+
try {
|
|
300
|
+
const data = (await wireCall(session, {
|
|
301
|
+
path: ENDPOINTS.workflowListWorkItems,
|
|
302
|
+
query: { workflowInstanceId, status: "PENDING", page: "0", size: "200" },
|
|
303
|
+
})).data;
|
|
304
|
+
const rows = Array.isArray(data?.content) ? data.content : wireRows(data);
|
|
305
|
+
if (typeof workItemId !== "string" || workItemId.length === 0)
|
|
306
|
+
return rows.length > 0;
|
|
307
|
+
return rows.some((row) => String(row.workItemId ?? row.id ?? "") === workItemId);
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
return null;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* People-picker values ride the wire as a bare user id (or an array of ids), but
|
|
315
|
+
* they COME BACK as objects. Writing the readback value verbatim trips
|
|
316
|
+
* `UserBizField.fillClientContent` in the engine (Assertion Failed → bare 50000,
|
|
317
|
+
* r16 P5). Detect the object/array shape on the way IN, against the published
|
|
318
|
+
* field types, so the refusal names the field instead of letting the platform
|
|
319
|
+
* answer 服务异常.
|
|
320
|
+
*/
|
|
321
|
+
async function personFieldShapeViolations(session, schemaCode, formData) {
|
|
322
|
+
const STAFF_TYPES = new Set([50, 51, 52]);
|
|
323
|
+
let rows;
|
|
324
|
+
try {
|
|
325
|
+
rows = wireRows((await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode, isPublish: "true" } })).data);
|
|
326
|
+
}
|
|
327
|
+
catch {
|
|
328
|
+
return [];
|
|
329
|
+
}
|
|
330
|
+
const staffCodes = new Map();
|
|
331
|
+
for (const row of rows) {
|
|
332
|
+
const type = Number(row.type ?? row.propertyType);
|
|
333
|
+
const code = String(row.code ?? row.propertyCode ?? "");
|
|
334
|
+
if (code && STAFF_TYPES.has(type))
|
|
335
|
+
staffCodes.set(code, String(row.typeName ?? type));
|
|
336
|
+
}
|
|
337
|
+
const violations = [];
|
|
338
|
+
for (const [code, value] of Object.entries(formData)) {
|
|
339
|
+
if (!staffCodes.has(code))
|
|
340
|
+
continue;
|
|
341
|
+
if (value === null || value === undefined || value === "")
|
|
342
|
+
continue;
|
|
343
|
+
if (typeof value === "string")
|
|
344
|
+
continue;
|
|
345
|
+
if (Array.isArray(value) && value.every((entry) => typeof entry === "string"))
|
|
346
|
+
continue;
|
|
347
|
+
violations.push({ code, got: Array.isArray(value) ? `array[${value.length}]` : typeof value });
|
|
348
|
+
}
|
|
349
|
+
return violations;
|
|
350
|
+
}
|
|
291
351
|
/**
|
|
292
352
|
* Portal-required fields the published form declares (`control.options.required`
|
|
293
353
|
* or `requiredFormula`). The portal enforces these client-side; the REST
|
|
@@ -1032,6 +1092,22 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1032
1092
|
// so a missing required value must be REPORTED, not silently accepted as a
|
|
1033
1093
|
// portal-valid submit (dogfood R3). Best-effort read; a failure never
|
|
1034
1094
|
// blocks the submit.
|
|
1095
|
+
// r16 P5:人员字段读回来是对象数组 `[{id,type,name,…}]`,写回去必须是**字符串 id**
|
|
1096
|
+
// ——写成对象会让 UserBizField.fillClientContent 断言失败,平台只回 50000。
|
|
1097
|
+
// 读写不对称且 schema 无法表达(formData 是 additionalProperties),所以在
|
|
1098
|
+
// 写前按已发布字段类型做一次浅校验,把这类形状错误变成一条指名道姓的拒绝。
|
|
1099
|
+
const staffShaped = await personFieldShapeViolations(session, args.schemaCode, formData).catch(() => []);
|
|
1100
|
+
if (staffShaped.length > 0)
|
|
1101
|
+
throw new ToolRefusalError({
|
|
1102
|
+
status: "refused",
|
|
1103
|
+
blocker: "submit-person-field-shape",
|
|
1104
|
+
schemaCode: args.schemaCode,
|
|
1105
|
+
fields: staffShaped,
|
|
1106
|
+
message: `这些人员字段的取值是对象/数组:${staffShaped.map((entry) => `${entry.code}(${entry.got})`).join("、")}。` +
|
|
1107
|
+
`人员字段**写入只接受字符串 id**(多个用逗号或数组的字符串 id)——读回来是 \`[{id,type,name,fullDepartment,…}]\`,` +
|
|
1108
|
+
`把读回值原样写回会让平台 UserBizField.fillClientContent 断言失败、只回 50000 服务异常。` +
|
|
1109
|
+
`写成 "\"<用户id>\"" 或 ["<id1>","<id2>"]。`,
|
|
1110
|
+
});
|
|
1035
1111
|
const validation = await portalRequiredViolations(session, args.schemaCode, sheetCode, args.formData).catch(() => ({ requiredFields: [], missingRequired: [] }));
|
|
1036
1112
|
// Directive: a workflow-backed model's submit must START the workflow, not
|
|
1037
1113
|
// merely save a sheet row. When the caller names no workflow params, detect
|
|
@@ -1039,6 +1115,38 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1039
1115
|
// with no workflow stays a direct sheet submit (formType=2 -> COMPLETED).
|
|
1040
1116
|
let explicitWorkflowCode = typeof args.workflowCode === "string" && args.workflowCode.trim().length > 0 ? args.workflowCode.trim() : "";
|
|
1041
1117
|
let autoStarted = false;
|
|
1118
|
+
// r16 P4: a second submit on the same work item answers 100005
|
|
1119
|
+
// SYS_REPLAY_ERROR「频繁重复提交!」(ErrorCode.java:68 — the SAME numeric code
|
|
1120
|
+
// as TEST_CONNECTION_FAIL, so the text is the only discriminator). The
|
|
1121
|
+
// platform rate-limits per work item, not per request body, so retrying
|
|
1122
|
+
// with a different payload changes nothing; the caller needs to know
|
|
1123
|
+
// whether the FIRST submit actually landed. Diagnose before the wire.
|
|
1124
|
+
if (hasWorkItem) {
|
|
1125
|
+
try {
|
|
1126
|
+
const activity = (await wireCall(session, {
|
|
1127
|
+
path: ENDPOINTS.workflowListInstanceActivity,
|
|
1128
|
+
query: { workflowInstanceId: String(args.workflowInstanceId ?? "") },
|
|
1129
|
+
})).data;
|
|
1130
|
+
const rows = Array.isArray(activity?.activities) ? activity.activities : wireRows(activity);
|
|
1131
|
+
const mine = rows.filter((row) => String(row.workItemId ?? "") === args.workItemId);
|
|
1132
|
+
const finished = mine.length > 0 && mine.every((row) => /FINISHED|COMPLETED/i.test(String(row.state ?? row.status ?? "")));
|
|
1133
|
+
if (finished)
|
|
1134
|
+
throw new ToolRefusalError({
|
|
1135
|
+
status: "refused",
|
|
1136
|
+
blocker: "submit-workitem-already-finished",
|
|
1137
|
+
schemaCode: args.schemaCode,
|
|
1138
|
+
workItemId: args.workItemId,
|
|
1139
|
+
nextCommand: `omc call workitem.list --args '${JSON.stringify({ workflowInstanceId: String(args.workflowInstanceId ?? "") })}'`,
|
|
1140
|
+
message: `待办 ${String(args.workItemId)} 已经是完成状态——本次提交**没有发生**,平台会回 100005「频繁重复提交!」(SYS_REPLAY_ERROR,按 workItem 维度限流,换载荷/等待都无效)。` +
|
|
1141
|
+
`这不是失败,是"已经提交过了"。用 workitem.list 看下一级待办,或 data.load 看当前 sequenceStatus,不要重复提交。`,
|
|
1142
|
+
});
|
|
1143
|
+
}
|
|
1144
|
+
catch (error) {
|
|
1145
|
+
if (error instanceof ToolRefusalError)
|
|
1146
|
+
throw error;
|
|
1147
|
+
// A read failure must not block the submit (fail open).
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1042
1150
|
if (!explicitWorkflowCode && !hasWorkItem && !hasInstance) {
|
|
1043
1151
|
const resolved = await resolvePublishedWorkflowCode(session, args.schemaCode);
|
|
1044
1152
|
if (resolved) {
|
|
@@ -1150,11 +1258,39 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1150
1258
|
// platform answer "权限不足", which reads like a misconfiguration.
|
|
1151
1259
|
if (hasWorkItem)
|
|
1152
1260
|
await diagnoseApprovalSubmitIdentity(session, { workflowInstanceId: args.workflowInstanceId, workItemId: args.workItemId });
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1261
|
+
let performed;
|
|
1262
|
+
try {
|
|
1263
|
+
performed = await runtimeWriteWithAppMapping(session, args.schemaCode, 50000, "data.submit 运行时提交稳定失败(50000)", () => wireCall(session, {
|
|
1264
|
+
path: ENDPOINTS.runtimeFormSubmit,
|
|
1265
|
+
method: "POST",
|
|
1266
|
+
body: payload,
|
|
1267
|
+
}), undefined, hasWorkflowCode ? explicitWorkflowCode : undefined);
|
|
1268
|
+
}
|
|
1269
|
+
catch (error) {
|
|
1270
|
+
// r16 P4: 100005 is SYS_REPLAY_ERROR「频繁重复提交!」(ErrorCode.java:68 —
|
|
1271
|
+
// the same numeric code as TEST_CONNECTION_FAIL, so only the text
|
|
1272
|
+
// separates them). The platform rate-limits per WORK ITEM, so a different
|
|
1273
|
+
// payload or a longer wait changes nothing. The actionable question is
|
|
1274
|
+
// "did the earlier submit land?", which the todo list answers.
|
|
1275
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === RUNTIME_DATA_WIRE.replayTokenErrcode) {
|
|
1276
|
+
const stillMine = await workItemStillOpen(session, args.workflowInstanceId, args.workItemId).catch(() => null);
|
|
1277
|
+
throw new ToolRefusalError({
|
|
1278
|
+
status: "refused",
|
|
1279
|
+
blocker: stillMine === false ? "submit-workitem-already-finished" : "submit-rate-limited",
|
|
1280
|
+
schemaCode: args.schemaCode,
|
|
1281
|
+
workItemId: args.workItemId ?? null,
|
|
1282
|
+
workflowInstanceId: args.workflowInstanceId ?? null,
|
|
1283
|
+
nextCommand: `omc call workitem.list --args '${JSON.stringify({ workflowInstanceId: String(args.workflowInstanceId ?? "") })}'`,
|
|
1284
|
+
message: stillMine === false
|
|
1285
|
+
? `100005「频繁重复提交!」在这里的真实含义:待办 ${String(args.workItemId ?? "(未提供)")} **已经提交过了**。限流按 workItem 维度,换载荷、等待、换 requestId 都无效——这不是失败。用 workitem.list 看下一级待办,或 data.load 看 sequenceStatus,不要重试本次提交。`
|
|
1286
|
+
: `100005 = 平台 SYS_REPLAY_ERROR「频繁重复提交!」(ErrorCode.java:68,与"测试连接不通过"共用该数值码,只能靠文案区分)。` +
|
|
1287
|
+
`两种成因:① 该待办已经提交过;② startWorkflow 路径的 replayToken 未刷新。` +
|
|
1288
|
+
`${stillMine === null ? "(待办状态读取失败,无法判定)" : stillMine ? "该待办仍在——多半是限流窗口未过。" : ""}` +
|
|
1289
|
+
`先 workitem.list 核对当前待办,确认没提交成功再等一会儿重试;改 payload 不会绕过它。`,
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
throw error;
|
|
1293
|
+
}
|
|
1158
1294
|
const performedData = (performed.data ?? {});
|
|
1159
1295
|
let readback = { formType };
|
|
1160
1296
|
if (formType === "2") {
|
package/dist/model-purge.js
CHANGED
|
@@ -165,7 +165,11 @@ export async function planPurge(options, injected) {
|
|
|
165
165
|
businessRules,
|
|
166
166
|
...(workflowCode !== undefined ? { workflowCode } : {}),
|
|
167
167
|
instanceCount,
|
|
168
|
+
// Renamed intent (r16 P9): this counts WORK ITEMS in the workflow (one per
|
|
169
|
+
// open activity, including ones that are not the caller's todo), not "my
|
|
170
|
+
// todos". r16 saw 2 here while workitem.list showed 0 and read it as a bug.
|
|
168
171
|
pendingWorkItems,
|
|
172
|
+
pendingWorkItemsNote: "这是该流程的在办工作项总数(每个未完结活动一条,不等于「我的待办」);看自己的待办用 workitem.list",
|
|
169
173
|
instanceIds: [...instanceIds].slice(0, 50),
|
|
170
174
|
steps,
|
|
171
175
|
blockers,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.94",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.94",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.94",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|