@stalfh233/omc-cli 0.4.62 → 0.4.64
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/cli.js +9 -0
- package/dist/formula-lint.js +73 -0
- package/dist/lanes/datarule.js +23 -1
- package/dist/lanes/rule-graph.js +116 -43
- package/dist/lanes/runtime-data.js +47 -6
- package/dist/lanes/workflow.js +115 -5
- package/package.json +3 -3
- package/skills/omc-write-hazards.md +26 -0
package/dist/cli.js
CHANGED
|
@@ -458,6 +458,15 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
458
458
|
if (options.summary) {
|
|
459
459
|
const record = content && typeof content === "object" && !Array.isArray(content) ? content : undefined;
|
|
460
460
|
const keys = record ? Object.entries(record).map(([key, value]) => ({ key, bytes: Buffer.byteLength(JSON.stringify(value) ?? "null", "utf8"), kind: Array.isArray(value) ? `array[${value.length}]` : value === null ? "null" : typeof value })).sort((left, right) => right.bytes - left.bytes) : [];
|
|
461
|
+
// `--summary --jq <path>`: shape AND the one value, in a single call. Without
|
|
462
|
+
// this the caller had to spend one round on the shape and another on the
|
|
463
|
+
// path (or pull the whole body); r5 hit that on 67 KB workflow templates.
|
|
464
|
+
if (options.jq !== undefined) {
|
|
465
|
+
const projected = projectJsonPath(artifact.content.toString("utf8") === "" ? undefined : content, options.jq);
|
|
466
|
+
return emitTransportResult(projected.missed
|
|
467
|
+
? { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available }, fetch: artifactFetchHint(id) }
|
|
468
|
+
: { status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, projection: { path: options.jq, content: projected.value ?? null } }, transport);
|
|
469
|
+
}
|
|
461
470
|
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, keys, fetch: artifactFetchHint(id) }, transport);
|
|
462
471
|
}
|
|
463
472
|
if (options.keys !== undefined) {
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared lint for the platform's formula engine (EXPRESSION actions and
|
|
3
|
+
* dataRuleType-2 CALCULATE carriers).
|
|
4
|
+
*
|
|
5
|
+
* Evidence (AI-Platform v1.1.25): the engine parses `formula` with Aviator and
|
|
6
|
+
* registers exactly the names in `CalculateMethodType` (cloudpivot-shared),
|
|
7
|
+
* one function per code, via `CalculateRuleExecutor`. A name outside that set
|
|
8
|
+
* fails at submit time with `errcode 101005 表达式格式错误`; text concatenation
|
|
9
|
+
* with `+` instead of `STRING(...)` does NOT fail — `+` is numeric addition and
|
|
10
|
+
* the target silently stores null (live 2026-10-20, r4-training: `{a}+"-X"`
|
|
11
|
+
* wrote null while `STRING({a},"-X")` wrote `CNAME-X`).
|
|
12
|
+
*
|
|
13
|
+
* The platform validates nothing at save time, so this lint runs before the
|
|
14
|
+
* wire. Severity is advisory: an unknown name is loud at submit, but the caller
|
|
15
|
+
* should see it at write time rather than after a data probe.
|
|
16
|
+
*/
|
|
17
|
+
export const FORMULA_FUNCTIONS = new Set([
|
|
18
|
+
"STRING", "SUBSTRING", "REPLACE", "UPPERMONEY", "IF", "CONTAINS", "GETADDRESS", "LEN",
|
|
19
|
+
"LOWER", "UPPER", "SEARCH", "STARTSWITH", "SUBSTITUTE", "TRIM", "GETUNITNAME", "LEFT",
|
|
20
|
+
"RIGHT", "TEXT", "UNION", "DATE", "COUNTIF", "SUM", "AVG", "MAX", "MIN", "COUNT", "ROUND",
|
|
21
|
+
"INTUP", "INTDOWN", "VALUE", "FIXED", "ABS", "CEILING", "FLOOR", "COS", "SIN", "LARGE",
|
|
22
|
+
"SMALL", "LOG", "MOD", "PI", "POWER", "RAND", "SQRT", "SUMIF", "DATEDIF", "YEAR", "MONTH",
|
|
23
|
+
"DAY", "WEEKNUM", "WEEKDAY", "QUARTER", "HOUR", "MINUTE", "SECOND", "ADDDAY", "ADDMONTH",
|
|
24
|
+
"ADDYEAR", "TODAY", "BETWEEN", "NOTBETWEEN", "ADDHOUR", "ADDMINUTE", "NOW", "CASE", "ISNULL",
|
|
25
|
+
]);
|
|
26
|
+
/** Names agents reach for that the engine does NOT register, with the real one. */
|
|
27
|
+
export const FORMULA_FUNCTION_ALIASES = Object.freeze({
|
|
28
|
+
CONCAT: "STRING",
|
|
29
|
+
CONCATENATE: "STRING",
|
|
30
|
+
STRCAT: "STRING",
|
|
31
|
+
JOIN: "STRING",
|
|
32
|
+
});
|
|
33
|
+
const TEXT_PLUS_CONCAT = /(\{[^{}]+\}|[A-Za-z_][A-Za-z0-9_]*\s*\([^()]*\))\s*\+\s*["']|["']\s*\+\s*(\{[^{}]+\}|[A-Za-z_][A-Za-z0-9_]*\s*\()/;
|
|
34
|
+
/**
|
|
35
|
+
* Lint one formula string. `where` names the carrier in the caller's own terms
|
|
36
|
+
* (e.g. `dataActions[0].value.formula`), so the message points at the key the
|
|
37
|
+
* caller has to fix.
|
|
38
|
+
*/
|
|
39
|
+
export function lintFormulaText(formula, where) {
|
|
40
|
+
if (typeof formula !== "string" || formula.trim().length === 0)
|
|
41
|
+
return [];
|
|
42
|
+
const entries = [];
|
|
43
|
+
for (const match of formula.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*\(/g)) {
|
|
44
|
+
const name = match[1].toUpperCase();
|
|
45
|
+
const alias = FORMULA_FUNCTION_ALIASES[name];
|
|
46
|
+
if (alias) {
|
|
47
|
+
entries.push({
|
|
48
|
+
code: "formula-function-alias",
|
|
49
|
+
severity: "warning",
|
|
50
|
+
functionName: match[1],
|
|
51
|
+
message: `${where}: 公式里的 ${match[1]}() 不是平台函数;文本合并用 ${alias}({a},"-",{b})。别名写法在提交时报 errcode 101005 表达式格式错误。`,
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (FORMULA_FUNCTIONS.has(name))
|
|
56
|
+
continue;
|
|
57
|
+
entries.push({
|
|
58
|
+
code: "formula-function-unknown",
|
|
59
|
+
severity: "warning",
|
|
60
|
+
functionName: match[1],
|
|
61
|
+
message: `${where}: 公式函数 ${match[1]}() 不在平台注册表(CalculateMethodType)——提交时会报 101005 表达式格式错误。文本合并用 STRING();函数表见 cpdocs BusinessRulesTrigger.md「表达式函数」。`,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
if (TEXT_PLUS_CONCAT.test(formula)) {
|
|
65
|
+
entries.push({
|
|
66
|
+
code: "formula-text-plus-concat",
|
|
67
|
+
severity: "warning",
|
|
68
|
+
message: `${where}: 公式用 + 拼接了字符串字面量——+ 是数值加法,文本目标上会静默写 null(真机 2026-10-20)。文本合并用 STRING({a},"-",{b})。`,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return entries;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=formula-lint.js.map
|
package/dist/lanes/datarule.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { lintFormulaText } from "../formula-lint.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { ENDPOINTS } from "../endpoints.js";
|
|
3
4
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
@@ -396,6 +397,20 @@ export function assertDataRuleOptions(dataRuleType, options, checkType) {
|
|
|
396
397
|
assertStitchEditorTextRoundTrips(options);
|
|
397
398
|
}
|
|
398
399
|
}
|
|
400
|
+
/**
|
|
401
|
+
* Advisory formula lint for the type-2 CALCULATE carrier. Same registry as the
|
|
402
|
+
* rule-graph EXPRESSION lint (src/formula-lint.ts): an unregistered function
|
|
403
|
+
* name fails at submit with 101005 and `+` on text silently writes null, and
|
|
404
|
+
* the platform validates neither at create time.
|
|
405
|
+
*/
|
|
406
|
+
export function dataRuleFormulaWarnings(dataRuleType, options) {
|
|
407
|
+
if (dataRuleType !== 2)
|
|
408
|
+
return [];
|
|
409
|
+
const formula = type2FormulaText(options);
|
|
410
|
+
if (formula === null)
|
|
411
|
+
return [];
|
|
412
|
+
return lintFormulaText(formula, "options.shortTextStitch.formula");
|
|
413
|
+
}
|
|
399
414
|
/** Extract the expression of a type-2 CALCULATE rule (mirrors calcExpressionShapeError's carrier choice). */
|
|
400
415
|
function type2FormulaText(options) {
|
|
401
416
|
const raw = typeof options.shortTextStitch === "string" ? options.shortTextStitch : typeof options.value === "string" ? options.value : undefined;
|
|
@@ -751,6 +766,7 @@ export function registerDataRuleLane(tools) {
|
|
|
751
766
|
matchedCount: rules.length,
|
|
752
767
|
rules: rules.map(decodeDataRule),
|
|
753
768
|
...(args.dataRuleType >= 3 ? { advisory: DATARULE_FORM_INTERACTION_ADVISORY } : {}),
|
|
769
|
+
...(() => { const w = dataRuleFormulaWarnings(args.dataRuleType, args.options); return w.length ? { lintWarnings: w } : {}; })(),
|
|
754
770
|
},
|
|
755
771
|
};
|
|
756
772
|
},
|
|
@@ -759,7 +775,13 @@ export function registerDataRuleLane(tools) {
|
|
|
759
775
|
assertDataRuleTarget(args);
|
|
760
776
|
assertDataRuleOptions(args.dataRuleType, args.options, args.checkType);
|
|
761
777
|
assertDataRuleCheckType(args.dataRuleType, args.checkType);
|
|
762
|
-
},
|
|
778
|
+
},
|
|
779
|
+
// Dry-run is the cheapest place to see the formula lint (the platform
|
|
780
|
+
// validates nothing at create time; the failure surfaces at submit).
|
|
781
|
+
async (_session, args) => {
|
|
782
|
+
const lintWarnings = dataRuleFormulaWarnings(args.dataRuleType, args.options);
|
|
783
|
+
return lintWarnings.length ? { lintWarnings } : {};
|
|
784
|
+
}, dataruleRollbackSpec("datarule.create"));
|
|
763
785
|
writeTool(tools, "datarule.update", DATARULE_LANE, DATARULE_TOOLS["datarule.update"].scope, DATARULE_TOOLS["datarule.update"].description, {
|
|
764
786
|
id: z.string().min(1).optional().describe("数据规则记录 id;省略时可改用业务键 schemaCode + propertyCode(+dataRuleType) 定位"),
|
|
765
787
|
schemaCode: z.string().min(1),
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
* call (dry lint) so a mis-shaped graph cannot reach the platform.
|
|
11
11
|
*/
|
|
12
12
|
export { DYNAMIC_CONDITION_SOURCE_KEY, DYNAMIC_ACTION_SOURCE_KEY } from "omc-core/wire-facts";
|
|
13
|
+
import { lintFormulaText } from "../formula-lint.js";
|
|
13
14
|
import { DYNAMIC_CONDITION_SOURCE_KEY, DYNAMIC_ACTION_SOURCE_KEY } from "omc-core/wire-facts";
|
|
14
15
|
import { decodeJsonColumn, JSON_COLUMN_NULL_COALESCE } from "../json-column.js";
|
|
15
16
|
export const RULE_VALUE_TYPES = ["DYNAMIC", "FIXED", "EMPTY", "VARIABLE", "EXPRESSION", "SYSTEM", "AGGREGATION"];
|
|
@@ -1092,32 +1093,8 @@ export function validateConditionLeaf(leaf, nodeCode, where, warnings, legacyTol
|
|
|
1092
1093
|
return blockers;
|
|
1093
1094
|
}
|
|
1094
1095
|
/**
|
|
1095
|
-
*
|
|
1096
|
-
*
|
|
1097
|
-
* function per code). A name outside this set makes the platform answer
|
|
1098
|
-
* `errcode 101005 表达式格式错误` at submit time (1.1.25 真机 2026-10-20).
|
|
1099
|
-
*/
|
|
1100
|
-
const FORMULA_FUNCTIONS = new Set([
|
|
1101
|
-
"STRING", "SUBSTRING", "REPLACE", "UPPERMONEY", "IF", "CONTAINS", "GETADDRESS", "LEN",
|
|
1102
|
-
"LOWER", "UPPER", "SEARCH", "STARTSWITH", "SUBSTITUTE", "TRIM", "GETUNITNAME", "LEFT",
|
|
1103
|
-
"RIGHT", "TEXT", "UNION", "DATE", "COUNTIF", "SUM", "AVG", "MAX", "MIN", "COUNT", "ROUND",
|
|
1104
|
-
"INTUP", "INTDOWN", "VALUE", "FIXED", "ABS", "CEILING", "FLOOR", "COS", "SIN", "LARGE",
|
|
1105
|
-
"SMALL", "LOG", "MOD", "PI", "POWER", "RAND", "SQRT", "SUMIF", "DATEDIF", "YEAR", "MONTH",
|
|
1106
|
-
"DAY", "WEEKNUM", "WEEKDAY", "QUARTER", "HOUR", "MINUTE", "SECOND", "ADDDAY", "ADDMONTH",
|
|
1107
|
-
"ADDYEAR", "TODAY", "BETWEEN", "NOTBETWEEN", "ADDHOUR", "ADDMINUTE", "NOW", "CASE", "ISNULL",
|
|
1108
|
-
]);
|
|
1109
|
-
/** Names agents reach for that the engine does NOT register, with the real one. */
|
|
1110
|
-
const FORMULA_FUNCTION_ALIASES = Object.freeze({
|
|
1111
|
-
CONCAT: "STRING",
|
|
1112
|
-
CONCATENATE: "STRING",
|
|
1113
|
-
STRCAT: "STRING",
|
|
1114
|
-
JOIN: "STRING",
|
|
1115
|
-
});
|
|
1116
|
-
/**
|
|
1117
|
-
* Advisory lint for EXPRESSION formulas. The platform validates formulas only at
|
|
1118
|
-
* submit time and the failure is loud for unknown functions (101005) but SILENT
|
|
1119
|
-
* for text arithmetic: `{a}+"-X"` on a text target stores null, because `+` is
|
|
1120
|
-
* numeric. Text concatenation must use `STRING(a,b,...)` (真机 2026-10-20).
|
|
1096
|
+
* Advisory lint for EXPRESSION formulas (shared with datarule so both carriers
|
|
1097
|
+
* use one function registry — see src/formula-lint.ts for the 1.1.25 evidence).
|
|
1121
1098
|
*/
|
|
1122
1099
|
export function formulaWarnings(action, nodeCode, index) {
|
|
1123
1100
|
if (String(action.ruleValueType ?? "") !== "EXPRESSION")
|
|
@@ -1128,20 +1105,13 @@ export function formulaWarnings(action, nodeCode, index) {
|
|
|
1128
1105
|
const formula = value.formula;
|
|
1129
1106
|
if (typeof formula !== "string" || !formula.trim())
|
|
1130
1107
|
return [];
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
continue;
|
|
1139
|
-
}
|
|
1140
|
-
if (FORMULA_FUNCTIONS.has(name))
|
|
1141
|
-
continue;
|
|
1142
|
-
warnings.push({ code: "rule-graph-formula-function-unknown", nodeCode, key: "value", severity: "warning", message: `${where}: 公式函数 ${match[1]}() 不在平台注册表(CalculateMethodType)——提交时会报 101005 表达式格式错误。可用函数见 omc help omc-business-configuration 或 cpdocs BusinessRulesTrigger.md「表达式函数」。` });
|
|
1143
|
-
}
|
|
1144
|
-
return warnings;
|
|
1108
|
+
return lintFormulaText(formula, `dataActions[${index}].value.formula`).map((entry) => ({
|
|
1109
|
+
code: entry.code,
|
|
1110
|
+
nodeCode,
|
|
1111
|
+
key: "value",
|
|
1112
|
+
severity: "warning",
|
|
1113
|
+
message: entry.message,
|
|
1114
|
+
}));
|
|
1145
1115
|
}
|
|
1146
1116
|
/** Validate one dataAction; returns blockers (empty = clean). */
|
|
1147
1117
|
export function validateDataAction(action, nodeCode, index) {
|
|
@@ -1414,6 +1384,56 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1414
1384
|
// The graph shape (DYNAMIC filter `currentSchemaDataItem` → target `id`;
|
|
1415
1385
|
// DYNAMIC action `currentDataItemValue`) is correct as-is. Surface the two
|
|
1416
1386
|
// prerequisites as a non-gating note rather than the false "unverified" claim.
|
|
1387
|
+
// A BRANCH that fans out to TWO cross-model UPDATE nodes answers 50000 at
|
|
1388
|
+
// write/run time (r5-asset 2026-10-20: the "完好/损坏 → 各自回写资产台账"
|
|
1389
|
+
// shape failed with 50000). Loud, but only after the whole rule is written, so
|
|
1390
|
+
// warn before the wire and name the supported alternative.
|
|
1391
|
+
if (schemaCode !== undefined && schemaCode.length > 0 && Array.isArray(routes)) {
|
|
1392
|
+
const updates = new Map();
|
|
1393
|
+
for (const node of nodeRecords) {
|
|
1394
|
+
if (String(node.nodeType ?? "") !== "UPDATE")
|
|
1395
|
+
continue;
|
|
1396
|
+
const target = String(node.targetObjectCode ?? "").trim();
|
|
1397
|
+
if (target === "" || target === schemaCode)
|
|
1398
|
+
continue;
|
|
1399
|
+
updates.set(String(node.nodeCode ?? ""), { nodeCode: String(node.nodeCode ?? ""), target });
|
|
1400
|
+
}
|
|
1401
|
+
const branches = nodeRecords.filter((node) => String(node.nodeType ?? "") === "BRANCH");
|
|
1402
|
+
const fanOut = [];
|
|
1403
|
+
for (const branch of branches) {
|
|
1404
|
+
const start = String(branch.nodeCode ?? "");
|
|
1405
|
+
const queue = [start];
|
|
1406
|
+
const seen = new Set();
|
|
1407
|
+
const hit = [];
|
|
1408
|
+
while (queue.length > 0) {
|
|
1409
|
+
const current = queue.shift();
|
|
1410
|
+
if (seen.has(current))
|
|
1411
|
+
continue;
|
|
1412
|
+
seen.add(current);
|
|
1413
|
+
if (current !== start && updates.has(current))
|
|
1414
|
+
hit.push(current);
|
|
1415
|
+
for (const route of routes) {
|
|
1416
|
+
if (!route || typeof route !== "object")
|
|
1417
|
+
continue;
|
|
1418
|
+
if (String(route.preNode ?? "") === current) {
|
|
1419
|
+
const post = String(route.postNode ?? "");
|
|
1420
|
+
if (post !== "")
|
|
1421
|
+
queue.push(post);
|
|
1422
|
+
}
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
if (new Set(hit).size > 1)
|
|
1426
|
+
fanOut.push(`${start} → ${[...new Set(hit)].join("、")}`);
|
|
1427
|
+
}
|
|
1428
|
+
if (fanOut.length > 0)
|
|
1429
|
+
blockers.push({
|
|
1430
|
+
code: "rule-graph-branch-multi-cross-model-update",
|
|
1431
|
+
severity: "warning",
|
|
1432
|
+
nodeCode: String(branches[0]?.nodeCode ?? ""),
|
|
1433
|
+
key: "routes",
|
|
1434
|
+
message: `分支节点后接多个跨模型 UPDATE(${fanOut.join(";")})——真机 2026-10-20 该组合在写/运行时报 50000。改成:① 单个跨模型 UPDATE + 条件动作(dataActions 内按条件区分),或 ② 通过 BIZ_ACTION 自调二开接口一次处理多行。`,
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1417
1437
|
if (schemaCode !== undefined && schemaCode.length > 0) {
|
|
1418
1438
|
const writeTypes = new Set(["UPDATE", "CREATE", "DELETE"]);
|
|
1419
1439
|
const crossModel = nodeRecords.filter((node) => {
|
|
@@ -1538,13 +1558,18 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1538
1558
|
// logged 「查找范围不存在数据」 and the field kept its old value; moving the
|
|
1539
1559
|
// same node after SYSTEM_CREATE wrote it. This is the one action type that
|
|
1540
1560
|
// must sit AFTER persistence (ASSIGN/BIZ_ACTION must sit before).
|
|
1541
|
-
if (String(systemNode.nodeType ?? "") === "SYSTEM_CREATE" && schemaCode) {
|
|
1561
|
+
if ((String(systemNode.nodeType ?? "") === "SYSTEM_CREATE" || String(systemNode.nodeType ?? "") === "SYSTEM_UPDATE") && schemaCode) {
|
|
1542
1562
|
const selfUpdateIndex = nodeRecords.findIndex((node) => {
|
|
1543
1563
|
const target = String(node.targetObjectCode ?? "");
|
|
1544
1564
|
return String(node.nodeType) === "UPDATE" && (target === "" || target === schemaCode);
|
|
1545
1565
|
});
|
|
1546
|
-
if (selfUpdateIndex >= 0 && selfUpdateIndex < systemIndex)
|
|
1547
|
-
|
|
1566
|
+
if (selfUpdateIndex >= 0 && selfUpdateIndex < systemIndex) {
|
|
1567
|
+
const systemType = String(systemNode.nodeType ?? "");
|
|
1568
|
+
const detail = systemType === "SYSTEM_CREATE"
|
|
1569
|
+
? "Create 触发时触发行尚未落库,按 id 查不到(日志「查找范围不存在数据」),字段静默不写回"
|
|
1570
|
+
: "Update 触发时随后的 SYSTEM_UPDATE 会用本次提交值覆盖刚写的值(真机 2026-10-20:UPDATE 在前 note 落回提交值,在后才保留 UPDATE 值)";
|
|
1571
|
+
blockers.push({ code: "rule-graph-update-before-persistence-order", nodeCode: String(nodeRecords[selfUpdateIndex].nodeCode ?? ""), key: "nodes", message: `自更新 UPDATE 必须排在 ${systemType} 之后(Start→${systemType}→UPDATE→End)——排在前面时规则能存能启用,但${detail}。把它挪到系统节点之后。` });
|
|
1572
|
+
}
|
|
1548
1573
|
}
|
|
1549
1574
|
}
|
|
1550
1575
|
// inputParam chaining: only "_input" or "_<existing nodeCode>_output".
|
|
@@ -1601,6 +1626,54 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1601
1626
|
const actions = Array.isArray(node.dataActions) ? node.dataActions : [];
|
|
1602
1627
|
if (actions.length === 0)
|
|
1603
1628
|
blockers.push({ code: "rule-graph-update-without-actions", nodeCode, key: "dataActions", message: "UPDATE 节点至少需要 1 个 dataAction" });
|
|
1629
|
+
// ---- cross-model UPDATE shapes (source: AI-Platform v1.1.25
|
|
1630
|
+
// UpdateServiceExecutor + ActionMapping, re-decompiled 2026-10-20) ----
|
|
1631
|
+
// targetObjectCode WITHOUT a dot = 目标主表; WITH a dot =
|
|
1632
|
+
// "<目标主表>.<目标子表>" (ActionMapping.targetIsMainSchema splits on '.').
|
|
1633
|
+
// Execution: getUpdatedMainBizObject() queries the TARGET main table once
|
|
1634
|
+
// with mainConditions ONLY; sheetConditions is read only by
|
|
1635
|
+
// doUpdateChildData() for the target child. So:
|
|
1636
|
+
// (a) a source child field (currentSchemaDataItem "child.field") is read
|
|
1637
|
+
// as a COLLECTION (DatasetImpl) and still produces ONE target query —
|
|
1638
|
+
// there is no per-source-child-row fan-out. Writing that shape looks
|
|
1639
|
+
// right, publishes, and silently matches nothing (r5-asset 2026-10-20:
|
|
1640
|
+
// id = asset_apply_items.asset updated zero rows).
|
|
1641
|
+
// (b) sheetConditions on a MAIN target is dead config.
|
|
1642
|
+
const targetCode = String(node.targetObjectCode ?? "").trim();
|
|
1643
|
+
const targetIsChild = targetCode.includes(".");
|
|
1644
|
+
const targetMain = targetIsChild ? targetCode.split(".")[0] : targetCode;
|
|
1645
|
+
const filterRecord = (node.filterCondition && typeof node.filterCondition === "object" ? node.filterCondition : {});
|
|
1646
|
+
const mainGroups = Array.isArray(filterRecord.mainConditions) ? filterRecord.mainConditions : [];
|
|
1647
|
+
const childSourcedLeaves = [];
|
|
1648
|
+
for (const group of mainGroups) {
|
|
1649
|
+
if (!Array.isArray(group))
|
|
1650
|
+
continue;
|
|
1651
|
+
for (const leaf of group) {
|
|
1652
|
+
if (!leaf || typeof leaf !== "object")
|
|
1653
|
+
continue;
|
|
1654
|
+
const record = leaf;
|
|
1655
|
+
if (String(record.ruleValueType ?? "").toUpperCase() !== "DYNAMIC")
|
|
1656
|
+
continue;
|
|
1657
|
+
const source = String(record.currentSchemaDataItem ?? "").trim();
|
|
1658
|
+
if (source.includes("."))
|
|
1659
|
+
childSourcedLeaves.push(`${String(record.targetSchemaDataItem ?? "?")} = ${source}`);
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
if (!targetIsChild && targetMain !== schemaCode && childSourcedLeaves.length > 0)
|
|
1663
|
+
blockers.push({
|
|
1664
|
+
code: "rule-graph-cross-model-child-source-fanout",
|
|
1665
|
+
nodeCode,
|
|
1666
|
+
key: "filterCondition.mainConditions",
|
|
1667
|
+
message: `${nodeCode}: 跨模型 UPDATE 用子表字段做定位条件(${childSourcedLeaves.join("、")})——1.1.25 把 "子表.字段" 解析成**集合**,目标主表仍只查询一次,不会按每条子表行各更新一次,运行结果是 0 行命中(r5-asset 2026-10-20 真机:保存/发布全绿、状态不变)。单行回写改用主表字段做 DYNAMIC 来源;一张单多行逐件回写需二开(BIZ_ACTION 自调接口一次处理多行)。`,
|
|
1668
|
+
});
|
|
1669
|
+
if (!targetIsChild && Array.isArray(filterRecord.sheetConditions) && filterRecord.sheetConditions.length > 0)
|
|
1670
|
+
blockers.push({
|
|
1671
|
+
code: "rule-graph-sheet-conditions-ignored-for-main-target",
|
|
1672
|
+
nodeCode,
|
|
1673
|
+
key: "filterCondition.sheetConditions",
|
|
1674
|
+
severity: "warning",
|
|
1675
|
+
message: `${nodeCode}: targetObjectCode "${targetCode}" 是主表目标,sheetConditions 只在目标是 "<主表>.<子表>" 时被 doUpdateChildData 读取——这里不会生效(UpdateServiceExecutor 只把 mainConditions 传给目标主表查询)。要筛目标子表请把 targetObjectCode 写成点号路径。`,
|
|
1676
|
+
});
|
|
1604
1677
|
actions.forEach((action, index) => {
|
|
1605
1678
|
if (action && typeof action === "object") {
|
|
1606
1679
|
blockers.push(...validateDataAction(action, nodeCode, index));
|
|
@@ -290,6 +290,25 @@ async function portalRequiredViolations(session, schemaCode, sheetCode, formData
|
|
|
290
290
|
* carrying its own `id` is "Modified", an id-less row is "Added"; an explicit
|
|
291
291
|
* rowStatus is preserved verbatim.
|
|
292
292
|
*/
|
|
293
|
+
/**
|
|
294
|
+
* Paths of non-empty child-table arrays anywhere inside a form payload. The
|
|
295
|
+
* platform appends every child row it finds on an approval submit, regardless
|
|
296
|
+
* of nesting, so detection must not stop at the top level.
|
|
297
|
+
*/
|
|
298
|
+
function collectChildArrayPaths(value, childCodes, prefix = "") {
|
|
299
|
+
if (value === null || typeof value !== "object")
|
|
300
|
+
return [];
|
|
301
|
+
if (Array.isArray(value))
|
|
302
|
+
return [];
|
|
303
|
+
const found = [];
|
|
304
|
+
for (const [key, child] of Object.entries(value)) {
|
|
305
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
306
|
+
if (childCodes.has(key) && Array.isArray(child) && child.length > 0)
|
|
307
|
+
found.push(path);
|
|
308
|
+
found.push(...collectChildArrayPaths(child, childCodes, path));
|
|
309
|
+
}
|
|
310
|
+
return found;
|
|
311
|
+
}
|
|
293
312
|
async function stampChildRowStatuses(session, schemaCode, formData) {
|
|
294
313
|
const childCodes = new Set();
|
|
295
314
|
try {
|
|
@@ -300,10 +319,10 @@ async function stampChildRowStatuses(session, schemaCode, formData) {
|
|
|
300
319
|
}
|
|
301
320
|
}
|
|
302
321
|
catch {
|
|
303
|
-
return formData;
|
|
322
|
+
return { formData, childCodes: [] };
|
|
304
323
|
}
|
|
305
324
|
if (childCodes.size === 0)
|
|
306
|
-
return formData;
|
|
325
|
+
return { formData, childCodes: [] };
|
|
307
326
|
let changed = false;
|
|
308
327
|
const next = { ...formData };
|
|
309
328
|
for (const code of childCodes) {
|
|
@@ -321,7 +340,7 @@ async function stampChildRowStatuses(session, schemaCode, formData) {
|
|
|
321
340
|
return { ...row, rowStatus: hasId ? "Modified" : "Added" };
|
|
322
341
|
});
|
|
323
342
|
}
|
|
324
|
-
return changed ? next : formData;
|
|
343
|
+
return { formData: changed ? next : formData, childCodes: [...childCodes] };
|
|
325
344
|
}
|
|
326
345
|
/** Runtime business-object data keyed by field code. */
|
|
327
346
|
export function registerRuntimeDataLane(tools) {
|
|
@@ -646,7 +665,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
646
665
|
message: "submit:true 与 workflowCode 互斥——workflowCode 是流程草稿两段式的第一段(返回 workItemId 后用 data.submit 提交);无流程模型的一调完成态用 submit:true 且不带 workflowCode",
|
|
647
666
|
});
|
|
648
667
|
const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
|
|
649
|
-
const formData = await stampChildRowStatuses(session, args.schemaCode, args.formData);
|
|
668
|
+
const formData = (await stampChildRowStatuses(session, args.schemaCode, args.formData)).formData;
|
|
650
669
|
if (typeof args.workflowCode === "string" && args.workflowCode.trim()) {
|
|
651
670
|
// Portal draft-save shape. The row id travels in data.id (the portal
|
|
652
671
|
// form SDK receives a pre-generated id from form/load and echoes it);
|
|
@@ -808,7 +827,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
808
827
|
message: "no live business object with this id; refusing to update (the platform would otherwise insert a new row)",
|
|
809
828
|
});
|
|
810
829
|
}
|
|
811
|
-
const formData = await stampChildRowStatuses(session, args.schemaCode, args.formData);
|
|
830
|
+
const formData = (await stampChildRowStatuses(session, args.schemaCode, args.formData)).formData;
|
|
812
831
|
const performed = await wireCall(session, {
|
|
813
832
|
path: ENDPOINTS.runtimeFormSave,
|
|
814
833
|
method: "POST",
|
|
@@ -907,6 +926,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
907
926
|
workItemId: z.string().optional(),
|
|
908
927
|
bizObjectId: z.string().optional().describe("existing record id; REQUIRED for workItem approval submits (formType=1 + workItemId) — the submit's bizObject must reference the in-flight record or the engine NPEs (DataChangeHandlerImpl.changeBizObject)"),
|
|
909
928
|
agree: z.boolean().optional(),
|
|
929
|
+
allowChildRowAppend: z.boolean().optional().describe("默认 false:审批提交(带 workItemId/workflowInstanceId)时 formData 含子表数组会被拒——平台按追加插行,每次提交重复一份。确实要追加子行时才置 true。"),
|
|
910
930
|
approval: z.record(z.string(), z.unknown()).optional(),
|
|
911
931
|
runMode: z.string().optional(),
|
|
912
932
|
isSimulative: z.boolean().optional(),
|
|
@@ -926,7 +946,28 @@ export function registerRuntimeDataLane(tools) {
|
|
|
926
946
|
});
|
|
927
947
|
}
|
|
928
948
|
const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
|
|
929
|
-
const
|
|
949
|
+
const stampedSubmit = await stampChildRowStatuses(session, args.schemaCode, args.formData);
|
|
950
|
+
const formData = stampedSubmit.formData;
|
|
951
|
+
// r5-asset 2026-10-20: the platform APPENDS child rows carried in an
|
|
952
|
+
// approval submit's formData (no upsert by row id, no error). Live: an
|
|
953
|
+
// approval that echoed the whole form twice turned 2 detail rows into 6
|
|
954
|
+
// and 30000 into 90000 while every call answered errcode 0 — node-level
|
|
955
|
+
// `editable:false` does not stop it server-side. Refuse before the wire.
|
|
956
|
+
if ((hasWorkItem || hasInstance) && args.allowChildRowAppend !== true) {
|
|
957
|
+
// Scan at ANY depth: callers wrap the form as {data:{...}} or
|
|
958
|
+
// {bizObject:{data:{...}}}, and a top-level-only check let those through
|
|
959
|
+
// (the platform appends whatever it finds). The child codes come from the
|
|
960
|
+
// model's own field list.
|
|
961
|
+
const carried = collectChildArrayPaths(args.formData, new Set(stampedSubmit.childCodes));
|
|
962
|
+
if (carried.length > 0)
|
|
963
|
+
throw new ToolRefusalError({
|
|
964
|
+
status: "refused",
|
|
965
|
+
blocker: "submit-approval-child-rows",
|
|
966
|
+
schemaCode: args.schemaCode,
|
|
967
|
+
childTables: carried,
|
|
968
|
+
message: `审批提交(workItemId/workflowInstanceId)的 formData 不要带子表数组:${carried.join("、")}。平台按「追加行」处理——每提交一次就再插一份(真机:2 行 → 6 行,30000 → 90000),且不报错、审批节点的 editable:false 也拦不住。只传主表字段;确实要在审批中追加子行时显式传 allowChildRowAppend:true。`,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
930
971
|
// Portal required-field semantics: the REST submit does NOT enforce them,
|
|
931
972
|
// so a missing required value must be REPORTED, not silently accepted as a
|
|
932
973
|
// portal-valid submit (dogfood R3). Best-effort read; a failure never
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -482,6 +482,84 @@ function mapCallerRules(callerRoutes, draftRules, activities) {
|
|
|
482
482
|
* `org.role.list` / `org.department.list` first. Advisory (never blocks): the
|
|
483
483
|
* platform accepts the save, so this is a readback lint, not a write gate.
|
|
484
484
|
*/
|
|
485
|
+
/**
|
|
486
|
+
* Participant FUNCTION names that the runtime does not register. Read-back and
|
|
487
|
+
* seeded templates carry them (the CRM seed uses FindUsersSpecByRoleFunction),
|
|
488
|
+
* and 1.1.25 only ships FindUserByRoleFunction — the others throw
|
|
489
|
+
* FunctionNotFoundException inside ParticipantActivityRuntime, so the instance
|
|
490
|
+
* stalls in PROCESSING with no work item while every API call answers errCode 0.
|
|
491
|
+
* A warning was not enough (r4 lost ~200 rounds to it), so publish refuses.
|
|
492
|
+
*/
|
|
493
|
+
export function unregisteredParticipantFunctions(template) {
|
|
494
|
+
const activities = Array.isArray(template.activities) ? template.activities : [];
|
|
495
|
+
const found = [];
|
|
496
|
+
for (const activity of activities) {
|
|
497
|
+
if (String(activity.activityType ?? "") !== "PARTICIPANT")
|
|
498
|
+
continue;
|
|
499
|
+
const participant = String(activity.participant ?? "").trim();
|
|
500
|
+
const match = /^(FindUsersByRoleFunction|FindUsersSpecByRoleFunction)\s*\(/.exec(participant);
|
|
501
|
+
if (match)
|
|
502
|
+
found.push({ activityCode: String(activity.activityCode ?? ""), functionName: match[1] });
|
|
503
|
+
}
|
|
504
|
+
return found;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* The designer default OMC backfills is `rejectToStart:true` on approval nodes
|
|
508
|
+
* (see backfillDesignerDefaults). With that flag, rejecting returns the instance
|
|
509
|
+
* to the fill node and the flow keeps running: endEventHandler and
|
|
510
|
+
* cancelEventHandler never fire (live 2026-10-20, r5-asset — instance stayed
|
|
511
|
+
* PROCESSING, todo back at A2, status still 审批中). A rule bound to `cancel`
|
|
512
|
+
* therefore silently never runs. Advisory: reject-to-start is a legitimate
|
|
513
|
+
* choice, only "I bound a cancel rule and expect it to run" is broken.
|
|
514
|
+
*/
|
|
515
|
+
/**
|
|
516
|
+
* A delta that omits `routes` keeps the draft's edges (mergeCallerDelta /
|
|
517
|
+
* completeWorkflowChain). That is the right default, but it is invisible: after
|
|
518
|
+
* 0.4.63's fix a branched draft survives an activities-only update, and a caller
|
|
519
|
+
* has no way to tell "I preserved 5 edges including the else fallback" from
|
|
520
|
+
* "the platform rebuilt a linear chain". Report the inherited branch edges.
|
|
521
|
+
*/
|
|
522
|
+
function inheritedBranchEdges(template) {
|
|
523
|
+
const rows = Array.isArray(template.activities) ? template.activities : [];
|
|
524
|
+
const rules = Array.isArray(template.rules) ? template.rules : [];
|
|
525
|
+
const outbound = new Map();
|
|
526
|
+
for (const rule of rules) {
|
|
527
|
+
const pre = String(rule.preActivityCode ?? "");
|
|
528
|
+
const post = String(rule.postActivityCode ?? "");
|
|
529
|
+
if (pre === "" || post === "")
|
|
530
|
+
continue;
|
|
531
|
+
outbound.set(pre, [...(outbound.get(pre) ?? []), post]);
|
|
532
|
+
}
|
|
533
|
+
const nameOf = new Map(rows.map((row) => [String(row.activityCode ?? ""), String(row.activityName ?? "")]));
|
|
534
|
+
return [...outbound.entries()]
|
|
535
|
+
.filter(([, targets]) => targets.length > 1)
|
|
536
|
+
.map(([pre, targets]) => `${pre}${nameOf.get(pre) ? `(${nameOf.get(pre)})` : ""} → ${targets.join(" | ")}`);
|
|
537
|
+
}
|
|
538
|
+
export function rejectToStartCancelWarnings(template) {
|
|
539
|
+
const activities = Array.isArray(template.activities) ? template.activities : [];
|
|
540
|
+
const blocking = activities
|
|
541
|
+
.filter((activity) => String(activity.activityType ?? "") === "PARTICIPANT")
|
|
542
|
+
.filter((activity) => {
|
|
543
|
+
const action = activity.permittedAction;
|
|
544
|
+
if (action === undefined || action === null || typeof action !== "object")
|
|
545
|
+
return true; // designer default -> true
|
|
546
|
+
return action.rejectToStart !== false;
|
|
547
|
+
})
|
|
548
|
+
.map((activity) => String(activity.activityCode ?? ""));
|
|
549
|
+
if (blocking.length === 0)
|
|
550
|
+
return [];
|
|
551
|
+
const cancel = template.cancelEventHandler;
|
|
552
|
+
const cancelRules = cancel && typeof cancel === "object" ? cancel.bizActions : undefined;
|
|
553
|
+
const bound = Array.isArray(cancelRules) ? cancelRules.length : 0;
|
|
554
|
+
if (bound === 0)
|
|
555
|
+
return [];
|
|
556
|
+
return [{
|
|
557
|
+
code: "workflow-reject-to-start-suppresses-cancel-handler",
|
|
558
|
+
severity: "warning",
|
|
559
|
+
message: `cancelEventHandler 绑了 ${bound} 条规则,但审批节点 ${blocking.join("、")} 的 rejectToStart 为 true(设计器默认):驳回会把实例退回填写节点并继续运行,end/cancel 处理器都不触发,这些规则不会执行(r5-asset 真机:实例仍 PROCESSING、待办回 A2)。` +
|
|
560
|
+
`两种改法:① 在这个节点的 permittedAction 里显式传 rejectToStart:false(驳回即结束,cancel 生效);② 驳回后的状态改成由发起人手工处理或二开监听。`,
|
|
561
|
+
}];
|
|
562
|
+
}
|
|
485
563
|
function participantReferenceWarnings(template) {
|
|
486
564
|
const activities = Array.isArray(template.activities) ? template.activities : [];
|
|
487
565
|
const warnings = [];
|
|
@@ -691,7 +769,7 @@ function validateWorkflowRender(template, tool) {
|
|
|
691
769
|
* chain order" — reusing any edge whose endpoints survived. A draft whose
|
|
692
770
|
* (possibly branching) graph is untouched passes the chain check verbatim.
|
|
693
771
|
*/
|
|
694
|
-
function completeWorkflowChain(rules, activities) {
|
|
772
|
+
export function completeWorkflowChain(rules, activities) {
|
|
695
773
|
const codes = activities.map((activity) => String(activity.activityCode ?? "")).filter((code) => code.length > 0);
|
|
696
774
|
if (codes.length < 2)
|
|
697
775
|
return rules;
|
|
@@ -712,13 +790,24 @@ function completeWorkflowChain(rules, activities) {
|
|
|
712
790
|
}
|
|
713
791
|
for (const code of codes) {
|
|
714
792
|
const type = typeOf(code);
|
|
715
|
-
if (type !== "START" && (inbound.get(code) ?? 0)
|
|
793
|
+
if (type !== "START" && (inbound.get(code) ?? 0) < 1)
|
|
716
794
|
valid = false;
|
|
717
|
-
if (type !== "END" && (outbound.get(code) ?? 0)
|
|
795
|
+
if (type !== "END" && (outbound.get(code) ?? 0) < 1)
|
|
718
796
|
valid = false;
|
|
719
797
|
}
|
|
720
798
|
if (valid)
|
|
721
799
|
return rules;
|
|
800
|
+
// r5-asset 2026-10-20: a node with TWO outgoing edges is a conditional
|
|
801
|
+
// branch (condition edge + utilizeElse fallback), NOT an invalid chain. The
|
|
802
|
+
// old rule demanded outbound === 1, so every branched graph fell through to
|
|
803
|
+
// the linear rebuild below — which maps consecutive activity codes and
|
|
804
|
+
// silently DROPS the else edge. Live symptom: a workflow.update that sent
|
|
805
|
+
// activities without routes erased A3→A5, every low-amount instance stalled
|
|
806
|
+
// in PROCESSING with errcode 0, and the only trace was a missing edge in
|
|
807
|
+
// get_published. Never linearize a graph that already branches.
|
|
808
|
+
const branched = [...outbound.values()].some((count) => count > 1);
|
|
809
|
+
if (branched)
|
|
810
|
+
return rules;
|
|
722
811
|
const byEdge = new Map(rules.map((rule) => [`${rule.preActivityCode}->${rule.postActivityCode}`, rule]));
|
|
723
812
|
return codes.slice(0, -1).map((code, index) => byEdge.get(`${code}->${codes[index + 1]}`) ?? { preActivityCode: code, postActivityCode: codes[index + 1] });
|
|
724
813
|
}
|
|
@@ -1740,8 +1829,16 @@ export function registerWorkflowLane(tools) {
|
|
|
1740
1829
|
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.update returned no draft" });
|
|
1741
1830
|
validateWorkflowRender(data, "workflow.update readback");
|
|
1742
1831
|
const participantWarnings = participantReferenceWarnings(data);
|
|
1832
|
+
// Evaluate on `body` (post-backfill): the readback may drop the handler
|
|
1833
|
+
// keys, and the backfilled permittedAction is exactly what will run.
|
|
1834
|
+
const rejectWarnings = rejectToStartCancelWarnings(body);
|
|
1743
1835
|
const renamedActivities = renamedActivityPairs(template, data);
|
|
1744
1836
|
const renamedInPlace = inPlaceRenames(data);
|
|
1837
|
+
// Caller sent no routes at all: the draft's edges (branches included) are
|
|
1838
|
+
// what rides the wire. Say so, and name the branch edges, because the only
|
|
1839
|
+
// other trace is a missing edge in a later getPublished readback.
|
|
1840
|
+
const callerSentRoutes = template.routes !== undefined || template.edges !== undefined || template.rules !== undefined;
|
|
1841
|
+
const branchEdges = callerSentRoutes ? [] : inheritedBranchEdges(data);
|
|
1745
1842
|
// 改名验收的权威读数:每个节点的当前显示名(platform may keep
|
|
1746
1843
|
// activityName or sync it; effectiveActivityName resolves both shapes).
|
|
1747
1844
|
const rawActivityRows = Array.isArray(data.activities)
|
|
@@ -1752,7 +1849,7 @@ export function registerWorkflowLane(tools) {
|
|
|
1752
1849
|
const activityNames = Object.fromEntries(rawActivityRows
|
|
1753
1850
|
.map((row) => [String(row.activityCode ?? ""), effectiveActivityName(row)])
|
|
1754
1851
|
.filter(([code]) => code !== ""));
|
|
1755
|
-
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : rawActivityRows.length, templateType: data.templateType ?? null, activityNames, ...(participantWarnings.length ? { participantWarnings } : {}), ...(renamedInPlace.length ? { renamedInPlace, renamedInPlaceHint: "改名已同 code 原地生效(载体 name_i18n,displayName 才是当前名;activityName 是首建值,平台不再变更)" } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "类型变更铸了新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
1852
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : rawActivityRows.length, templateType: data.templateType ?? null, activityNames, ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length ? { workflowWarnings: rejectWarnings } : {}), ...(branchEdges.length ? { routesInherited: { edges: branchEdges, hint: "本次未传 routes/edges:以上分支边沿用草稿(含 else 兜底)。要改连线请显式回传完整 routes" } } : {}), ...(renamedInPlace.length ? { renamedInPlace, renamedInPlaceHint: "改名已同 code 原地生效(载体 name_i18n,displayName 才是当前名;activityName 是首建值,平台不再变更)" } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "类型变更铸了新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
1756
1853
|
}, (args) => {
|
|
1757
1854
|
// Zero-wire refusal (ticket 77): fires in semanticGate BEFORE the
|
|
1758
1855
|
// governed pipeline — including the rollback before-capture read.
|
|
@@ -1771,6 +1868,18 @@ export function registerWorkflowLane(tools) {
|
|
|
1771
1868
|
// boundaries is a partial edit → delta merge (see activitiesLackBoundaries).
|
|
1772
1869
|
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, activitiesLackBoundaries(template));
|
|
1773
1870
|
await backfillDesignerDefaults(session, args.schemaCode, body, template);
|
|
1871
|
+
const unregistered = unregisteredParticipantFunctions(body);
|
|
1872
|
+
if (unregistered.length > 0)
|
|
1873
|
+
throw new ToolRefusalError({
|
|
1874
|
+
status: "refused",
|
|
1875
|
+
blocker: "workflow-participant-function-unregistered",
|
|
1876
|
+
workflowCode: args.code,
|
|
1877
|
+
activities: unregistered,
|
|
1878
|
+
message: `流程发布被拒:参与人函数 ${[...new Set(unregistered.map((entry) => entry.functionName + "()"))].join("、")} 在 AI-Platform v1.1.25 未注册(节点 ${unregistered.map((entry) => entry.activityCode).join("、")})。` +
|
|
1879
|
+
`发布能过,但运行期 ParticipantActivityRuntime 抛 FunctionNotFoundException:实例卡 PROCESSING、不生成待办,而 data.submit 仍回 errCode 0(r4-training 真机)。` +
|
|
1880
|
+
`内置函数只有 FindUserByRole({Originator.OU},"<roleCode>","random");跨部门/全局角色改用参与者对象数组(角色 id)。` +
|
|
1881
|
+
`这两个名字常见于 workflow.get_published 读回与种子流程,不要回抄。`,
|
|
1882
|
+
});
|
|
1774
1883
|
validateWorkflowRender(body, "workflow.publish");
|
|
1775
1884
|
const performed = await wireCall(session, { path: ENDPOINTS.workflowPublish, method: "POST", body });
|
|
1776
1885
|
// A refused publish answers errcode 0 — the ValidateMessage[] is the truth.
|
|
@@ -1795,7 +1904,8 @@ export function registerWorkflowLane(tools) {
|
|
|
1795
1904
|
});
|
|
1796
1905
|
validateWorkflowRender(data, "workflow.publish readback");
|
|
1797
1906
|
const participantWarnings = participantReferenceWarnings(data);
|
|
1798
|
-
|
|
1907
|
+
const rejectWarnings = rejectToStartCancelWarnings(data);
|
|
1908
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, published: isPublished, workflowVersion: data.workflowVersion ?? null, ...(warnings.length ? { warnings } : {}), ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length ? { workflowWarnings: rejectWarnings } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
1799
1909
|
}, (args) => {
|
|
1800
1910
|
assertTemplateNotMinimal(args.template ?? {}, "workflow.publish");
|
|
1801
1911
|
}, undefined, workflowRollbackSpec("workflow.publish"));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.64",
|
|
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.64",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.64",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -125,3 +125,29 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
125
125
|
## 固定验收顺序
|
|
126
126
|
|
|
127
127
|
执行阶段以 `omc-five-piece-flow` 为准;写面安全只额外强调两点:写前先 `commands/describe/contract` + dry-run 静态校验,写后权威 readback,必要时再用 CLI 触发运行验证。
|
|
128
|
+
|
|
129
|
+
## 审批提交回传子表 = 追加行(真机 2026-10-20)
|
|
130
|
+
|
|
131
|
+
带 `workItemId` / `workflowInstanceId` 的 `data.submit` 里放子表数组,平台按**追加**处理,不按行 `id` upsert、也不报错:
|
|
132
|
+
2 行明细回传两次 → 6 行,`totalValue` 30000 → 90000,审批节点的 `editable:false` 拦不住服务端写入。
|
|
133
|
+
**只传主表字段**;确需追加行时显式 `allowChildRowAppend:true`(0.4.63 起默认拒绝,blocker `submit-approval-child-rows`)。
|
|
134
|
+
收尾时前后各读一次子表行数确认未变。
|
|
135
|
+
|
|
136
|
+
## 跨表回写的真实语义(1.1.25 源码 + 真机 2026-10-20)
|
|
137
|
+
|
|
138
|
+
`targetObjectCode` 的**点号是目标侧路径**,不是来源字段路径:
|
|
139
|
+
|
|
140
|
+
| 写法 | 语义 | 执行 |
|
|
141
|
+
|------|------|------|
|
|
142
|
+
| `"asset_ledger"` | 目标主表 | 先按 `mainConditions` 查目标主表(**只查一次**),再逐行改 |
|
|
143
|
+
| `"asset_apply.asset_apply_items"` | 目标主表下的子表 | 先查目标主表,再用 `sheetConditions` 在**已查出的主表 ID 范围内**筛目标子表行 |
|
|
144
|
+
|
|
145
|
+
对应源码:`UpdateServiceExecutor.getUpdatedMainBizObject()` 只把 `mainConditions` 交给目标主表查询;`sheetConditions` 只在 `doUpdateChildData()` 里被读取。
|
|
146
|
+
|
|
147
|
+
由此得到四条使用纪律:
|
|
148
|
+
|
|
149
|
+
1. **主表目标上的 `sheetConditions` 是死配置**——不会生效(OMC 0.4.64 起给 warning)。
|
|
150
|
+
2. **来源子表字段是集合,不是逐行扇出**:`currentSchemaDataItem:"items.asset"` 会被解析成该子表所有行的值集合,目标主表仍只查询一次。写这个形状能存能发布,但运行 0 行命中(r5-asset 真机)。OMC 0.4.64 起 `rule-graph-cross-model-child-source-fanout` **拒绝**。
|
|
151
|
+
3. **一张单多行逐件回写**(每条明细各改一条另一模型的主表行)没有可用的纯配置形状——走 BIZ_ACTION 自调二开接口,一次处理多行;单行回写用主表字段做 DYNAMIC 来源。
|
|
152
|
+
4. **BRANCH 后接两个跨表 UPDATE** 真机报 50000;改用单个 UPDATE + 条件动作,或二开。
|
|
153
|
+
|