@stalfh233/omc-cli 0.4.58 → 0.4.60
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/compute.js +21 -1
- package/dist/lanes/list-view-shapes.js +10 -2
- package/dist/lanes/rule-graph.js +77 -4
- package/dist/lanes/runtime-data.js +17 -0
- package/dist/lanes/workflow-contract.js +1 -1
- package/dist/lanes/workflow.js +14 -0
- package/dist/recipes/businessrule.js +9 -6
- package/dist/recipes/workflow.js +2 -0
- package/package.json +3 -3
- package/skills/omc-acceptance-criteria.md +1 -0
- package/skills/omc-business-configuration.md +1 -0
- package/skills/omc-write-hazards.md +12 -0
package/dist/compute.js
CHANGED
|
@@ -62,7 +62,27 @@ export async function cmdCompute(args) {
|
|
|
62
62
|
catch (error) {
|
|
63
63
|
return invalid(error instanceof Error ? error.message : String(error));
|
|
64
64
|
}
|
|
65
|
-
|
|
65
|
+
// Live 2026-10-20 (purchase-request app): a child-table target
|
|
66
|
+
// (schemaCode = 子表 code) made datarule.create throw, and the uncaught error
|
|
67
|
+
// surfaced as `{"code":"internal","message":"command failed"}` — no way to tell
|
|
68
|
+
// whether the formula, the schema, or the tool was at fault. Catch it and hand
|
|
69
|
+
// back an actionable hint; the same shape succeeds via datarule.create.
|
|
70
|
+
let result;
|
|
71
|
+
let exitCode;
|
|
72
|
+
try {
|
|
73
|
+
({ result, exitCode } = await runToolCall("datarule.create", { args: createArgs, requestId: request, environment }));
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
77
|
+
writeEnvelope(envelope("error", {}, request, {
|
|
78
|
+
code: "compute-write-failed",
|
|
79
|
+
message: `compute 调用 datarule.create 失败:${message}\n` +
|
|
80
|
+
"若 --schema 是**子表 code**(子表列计算),改用可跑样例直接发:omc describe datarule.create --example field-compute-child-column," +
|
|
81
|
+
"或 omc call datarule.create --args '<该样例 args>'(含 resultType/formulaType/expression 的嵌套 JSON)。",
|
|
82
|
+
retryable: false,
|
|
83
|
+
}));
|
|
84
|
+
return CLI_EXIT_CODES.internal;
|
|
85
|
+
}
|
|
66
86
|
const classified = classifyResult(result);
|
|
67
87
|
writeEnvelope(envelope(classified.status, result, request, classified.error));
|
|
68
88
|
return exitCode;
|
|
@@ -45,7 +45,15 @@ const QUERY_FILTER_TYPES = new Set(QUERY_FILTER_TYPE);
|
|
|
45
45
|
* (`QueryFilterType.getDefaultQueryFilterType`, QueryFilterType.java) — used
|
|
46
46
|
* when a condition omits `filterType`, so the stored row names its comparison.
|
|
47
47
|
*/
|
|
48
|
-
function defaultQueryFilterType(propertyType) {
|
|
48
|
+
function defaultQueryFilterType(propertyType, displayType) {
|
|
49
|
+
// Live 2026-10-20 (purchase-request app): a SHORT_TEXT field holding 是/否 was
|
|
50
|
+
// configured with `displayType:"SELECT"` but no `filterType`, and the property
|
|
51
|
+
// type table answered "Like" — a fuzzy match on an enum-valued column ("否"
|
|
52
|
+
// also matched text containing that character). An enumerated display control
|
|
53
|
+
// (SELECT/RADIO/CHECKBOX) is a closed value set, so its operator is Eq.
|
|
54
|
+
const display = typeof displayType === "string" ? displayType.toUpperCase() : undefined;
|
|
55
|
+
if (display === "SELECT" || display === "RADIO" || display === "CHECKBOX")
|
|
56
|
+
return "Eq";
|
|
49
57
|
switch (propertyType) {
|
|
50
58
|
case FIELD_PROPERTY_TYPES.LOGICAL:
|
|
51
59
|
case FIELD_PROPERTY_TYPES.WORK_SHEET:
|
|
@@ -818,7 +826,7 @@ export function normalizeConditions(conditions, fieldRows, schemaCode) {
|
|
|
818
826
|
value: requestedFilterType,
|
|
819
827
|
message: `conditions 的 filterType "${String(requestedFilterType)}" 不是平台 QueryFilterType;可选:${QUERY_FILTER_TYPE.join(" / ")}`,
|
|
820
828
|
});
|
|
821
|
-
row.filterType = requestedFilterType !== undefined ? String(requestedFilterType) : defaultQueryFilterType(propertyType);
|
|
829
|
+
row.filterType = requestedFilterType !== undefined ? String(requestedFilterType) : defaultQueryFilterType(propertyType, condition.displayType);
|
|
822
830
|
if (condition.displayType !== undefined)
|
|
823
831
|
row.displayType = DISPLAY_TYPE.index[condition.displayType] ?? null;
|
|
824
832
|
if (condition.defaultValue !== undefined)
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -823,7 +823,7 @@ export const RULE_NODE_DEFAULT_NAMES = Object.freeze({
|
|
|
823
823
|
/** Canonical node shapes per nodeType, transcribed from the production capture. */
|
|
824
824
|
export const RULE_NODE_TEMPLATES = Object.freeze({
|
|
825
825
|
UPDATE: {
|
|
826
|
-
description: "数据更新:`filterCondition` = **查找满足条件的主表数据**(要更新哪条;DYNAMIC 过滤来源=currentSchemaDataItem,value 不出现);**空 mainConditions = 无条件下更新整表**(bench23 病灶,干 lint 给 warning)。`filterSubCondition` = 查找满足条件的子表数据。`dataActions` 赋值(DYNAMIC 来源=currentDataItemValue,value 留空;**EXPRESSION 公式赋值**:`ruleValueType:\"EXPRESSION\"` + `value` 为含非空 `formula` 的对象(可带 `editorText`/`editorMark`,平台编辑器解析要用到)。字段引用语法是 `{字段code}`,如 `{\"formula\":\"{amount}-{cost}\"}
|
|
826
|
+
description: "数据更新:`filterCondition` = **查找满足条件的主表数据**(要更新哪条;DYNAMIC 过滤来源=currentSchemaDataItem,value 不出现);**空 mainConditions = 无条件下更新整表**(bench23 病灶,干 lint 给 warning)。`filterSubCondition` = 查找满足条件的子表数据。`dataActions` 赋值(DYNAMIC 来源=currentDataItemValue,value 留空;**EXPRESSION 公式赋值**:`ruleValueType:\"EXPRESSION\"` + `value` 为含非空 `formula` 的对象(可带 `editorText`/`editorMark`,平台编辑器解析要用到)。字段引用语法是 `{字段code}`,如 `{\"formula\":\"{amount}-{cost}\"}`;**文本合并只能写 `STRING({a},\"-\",{b})`**(真机 2026-10-20:`{a}+\"-X\"` 在文本目标上静默写 null,`+` 是数值加法;`CONCAT/CONCATENATE/STRCAT/JOIN` 未注册,提交报 101005 表达式格式错误);日期差用 **`DATEDIF({end},{begin},\"<unit>\")`**(unit 小写:`y` 年 / `M` 月 / `d` 日 / `h` 时 / `m` 分 / `s` 秒;两个日期字段直接相减不成立)。公式语言**支持 IF/比较运算符**(真机 2026-10-07:`IF(DATEDIF({actual},{eta},\"h\")>0,INTDOWN(DATEDIF({actual},{eta},\"h\")),0)` 计算正确);取整用 `INTDOWN`(floor)/`INTUP`(ceil)/`ROUND`;**`MAX`/`MIN` 是集合聚合函数,不是逐值函数**——`MAX(0,x)` 报 `Wrong number of args (2) passed to: MAX` 50000,两值取大用 `IF(x>y,x,y)`。无效公式会让整个 rule.save/update 50000(非静默)。公式只在**数据更新(Update)** 触发链持久化,Create 触发的自更新本 build 不落库;同批更新内公式读**更新前**值——`UPDATE 算 X → BRANCH 判 X` 这类同链两步也读旧值,不成立);`dataCondition` = 输入数据满足条件时才执行;`childTriggerConditionType` = 子表触发范围 ADD/MODIFY/DELETE。inputParam 必须是 \"_input\"(或 \"_<QUERY节点>_output\")。平台标签证据:i18n 模块 196233 节点属性表 + evidence/form-conditions-and-rules-truth.md §4。",
|
|
827
827
|
template: {
|
|
828
828
|
nodeCode: "UPDATE_1", nodeName: "数据更新", nodeType: "UPDATE", targetObjectCode: "<target schemaCode>", inputParam: "_input", updateNodeType: "UPDATE_MODEL", execFormMapping: false,
|
|
829
829
|
filterCondition: { mainConditions: [[{ targetSchemaDataItem: "<target field>", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "<trigger field>" }]], sheetConditions: [] },
|
|
@@ -1091,6 +1091,58 @@ export function validateConditionLeaf(leaf, nodeCode, where, warnings, legacyTol
|
|
|
1091
1091
|
}
|
|
1092
1092
|
return blockers;
|
|
1093
1093
|
}
|
|
1094
|
+
/**
|
|
1095
|
+
* The formula engine's registered function names — the `CalculateMethodType`
|
|
1096
|
+
* enum in cloudpivot-shared (`CalculateRuleExecutor` registers one Aviator
|
|
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).
|
|
1121
|
+
*/
|
|
1122
|
+
export function formulaWarnings(action, nodeCode, index) {
|
|
1123
|
+
if (String(action.ruleValueType ?? "") !== "EXPRESSION")
|
|
1124
|
+
return [];
|
|
1125
|
+
const value = action.value;
|
|
1126
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
1127
|
+
return [];
|
|
1128
|
+
const formula = value.formula;
|
|
1129
|
+
if (typeof formula !== "string" || !formula.trim())
|
|
1130
|
+
return [];
|
|
1131
|
+
const warnings = [];
|
|
1132
|
+
const where = `dataActions[${index}]`;
|
|
1133
|
+
for (const match of formula.matchAll(/([A-Za-z_][A-Za-z0-9_]*)\s*\(/g)) {
|
|
1134
|
+
const name = match[1].toUpperCase();
|
|
1135
|
+
const alias = FORMULA_FUNCTION_ALIASES[name];
|
|
1136
|
+
if (alias) {
|
|
1137
|
+
warnings.push({ code: "rule-graph-formula-function-alias", nodeCode, key: "value", severity: "warning", message: `${where}: 公式里的 ${match[1]}() 不是平台函数;文本合并用 ${alias}({a},"-",{b})。别名写法在提交时报 errcode 101005 表达式格式错误。` });
|
|
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;
|
|
1145
|
+
}
|
|
1094
1146
|
/** Validate one dataAction; returns blockers (empty = clean). */
|
|
1095
1147
|
export function validateDataAction(action, nodeCode, index) {
|
|
1096
1148
|
const blockers = [];
|
|
@@ -1479,6 +1531,21 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1479
1531
|
if (assignIndex >= 0 && assignIndex > systemIndex)
|
|
1480
1532
|
blockers.push({ code: "rule-graph-assign-persistence-order", nodeCode: String(systemNode.nodeCode ?? ""), key: "nodes", message: `ASSIGN(CURRENT) 的赋值必须排在 ${String(systemNode.nodeType)} 之前(Start→ASSIGN→${String(systemNode.nodeType)}→End),否则赋值合并进 _input 的时机晚于持久化,读回无变化(真机 2026-09-28)。将 ASSIGN 移到系统持久化节点前。` });
|
|
1481
1533
|
}
|
|
1534
|
+
// A SELF-update in a Create rule must run AFTER SYSTEM_CREATE: the UPDATE
|
|
1535
|
+
// node queries its target rows, and before the trigger row is persisted that
|
|
1536
|
+
// query matches nothing. Observed live 2026-10-20: UPDATE_1 before
|
|
1537
|
+
// SYSTEM_CREATE saved/published/enabled fine, but UpdateServiceExecutor
|
|
1538
|
+
// logged 「查找范围不存在数据」 and the field kept its old value; moving the
|
|
1539
|
+
// same node after SYSTEM_CREATE wrote it. This is the one action type that
|
|
1540
|
+
// must sit AFTER persistence (ASSIGN/BIZ_ACTION must sit before).
|
|
1541
|
+
if (String(systemNode.nodeType ?? "") === "SYSTEM_CREATE" && schemaCode) {
|
|
1542
|
+
const selfUpdateIndex = nodeRecords.findIndex((node) => {
|
|
1543
|
+
const target = String(node.targetObjectCode ?? "");
|
|
1544
|
+
return String(node.nodeType) === "UPDATE" && (target === "" || target === schemaCode);
|
|
1545
|
+
});
|
|
1546
|
+
if (selfUpdateIndex >= 0 && selfUpdateIndex < systemIndex)
|
|
1547
|
+
blockers.push({ code: "rule-graph-update-before-persistence-order", nodeCode: String(nodeRecords[selfUpdateIndex].nodeCode ?? ""), key: "nodes", message: `Create 规则里的自更新 UPDATE 必须排在 SYSTEM_CREATE 之后(Start→SYSTEM_CREATE→UPDATE→End)——排在前面时规则能存能启用,但运行时查不到触发行(日志「查找范围不存在数据」),字段静默不写回(真机 2026-10-20 前后对照)。把它挪到系统节点之后。` });
|
|
1548
|
+
}
|
|
1482
1549
|
}
|
|
1483
1550
|
// inputParam chaining: only "_input" or "_<existing nodeCode>_output".
|
|
1484
1551
|
for (const node of nodeRecords) {
|
|
@@ -1535,8 +1602,10 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1535
1602
|
if (actions.length === 0)
|
|
1536
1603
|
blockers.push({ code: "rule-graph-update-without-actions", nodeCode, key: "dataActions", message: "UPDATE 节点至少需要 1 个 dataAction" });
|
|
1537
1604
|
actions.forEach((action, index) => {
|
|
1538
|
-
if (action && typeof action === "object")
|
|
1605
|
+
if (action && typeof action === "object") {
|
|
1539
1606
|
blockers.push(...validateDataAction(action, nodeCode, index));
|
|
1607
|
+
warnings.push(...formulaWarnings(action, nodeCode, index));
|
|
1608
|
+
}
|
|
1540
1609
|
else
|
|
1541
1610
|
blockers.push({ code: "rule-graph-action-not-object", nodeCode, key: `dataActions[${index}]`, message: "dataAction 必须是对象" });
|
|
1542
1611
|
});
|
|
@@ -1690,14 +1759,18 @@ export function validateRuleNodeGraph(graph, ruleCode, schemaCode, options = {})
|
|
|
1690
1759
|
}
|
|
1691
1760
|
if (Array.isArray(node.dataActions))
|
|
1692
1761
|
node.dataActions.forEach((action, index) => {
|
|
1693
|
-
if (action && typeof action === "object")
|
|
1762
|
+
if (action && typeof action === "object") {
|
|
1694
1763
|
blockers.push(...validateDataAction(action, nodeCode, index));
|
|
1764
|
+
warnings.push(...formulaWarnings(action, nodeCode, index));
|
|
1765
|
+
}
|
|
1695
1766
|
});
|
|
1696
1767
|
}
|
|
1697
1768
|
if (nodeType === "ASSIGN" && Array.isArray(node.dataActions)) {
|
|
1698
1769
|
node.dataActions.forEach((action, index) => {
|
|
1699
|
-
if (action && typeof action === "object")
|
|
1770
|
+
if (action && typeof action === "object") {
|
|
1700
1771
|
blockers.push(...validateDataAction(action, nodeCode, index));
|
|
1772
|
+
warnings.push(...formulaWarnings(action, nodeCode, index));
|
|
1773
|
+
}
|
|
1701
1774
|
});
|
|
1702
1775
|
}
|
|
1703
1776
|
// ASSIGN.filterCondition = 「满足条件时触发赋值」(与 UPDATE 同构的定位/触发条件),
|
|
@@ -1141,6 +1141,23 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1141
1141
|
note: "下一级审批:用这些字段调 data.submit(同意传 agree:true / formData 回填)。",
|
|
1142
1142
|
};
|
|
1143
1143
|
}
|
|
1144
|
+
else if (sequenceStatus === "PROCESSING") {
|
|
1145
|
+
// r4-training 2026-10-20: an unresolvable participant (role with no
|
|
1146
|
+
// members, member not attached to the originator's department, or a
|
|
1147
|
+
// participant function that is not registered) makes the engine
|
|
1148
|
+
// create NO next work item while the submit still answers errCode 0.
|
|
1149
|
+
// Without this line the caller cannot tell "flow advanced" from
|
|
1150
|
+
// "flow silently stalled" — surface it instead of staying quiet.
|
|
1151
|
+
readback.flowWarning = {
|
|
1152
|
+
code: "flow-no-next-workitem",
|
|
1153
|
+
message: "提交成功但该实例没有生成下一节点待办,流程停在 PROCESSING。常见原因:审批参与人解析为空(角色无成员 / 成员未挂发起人部门 / 参与人函数未注册)。",
|
|
1154
|
+
checks: [
|
|
1155
|
+
`omc call workitem.list --args '{"workflowInstanceId":"${effectiveInstanceId}"}'(稍后重查,排除异步派发延迟)`,
|
|
1156
|
+
"容器日志搜 GetParticipantInfo error / CustomParticipleMethod{...} -> []",
|
|
1157
|
+
"参与人优先用参与者对象数组(角色 id);内置函数只认 FindUserByRole(org, roleCode[, order])",
|
|
1158
|
+
],
|
|
1159
|
+
};
|
|
1160
|
+
}
|
|
1144
1161
|
}
|
|
1145
1162
|
catch {
|
|
1146
1163
|
// best-effort
|
|
@@ -47,7 +47,7 @@ export const WORKFLOW_WIRE = Object.freeze({
|
|
|
47
47
|
/** Workflow template object (workflow/update|publish raw body). */
|
|
48
48
|
export const workflowTemplateArg = z
|
|
49
49
|
.record(z.string(), z.unknown())
|
|
50
|
-
.describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING
|
|
50
|
+
.describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空时实例同样卡 PROCESSING 且无待办,验收看 workitem.list 而不是 data.submit 返回(r4-training 实证 2026-10-20)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template.`);
|
|
51
51
|
const WORKFLOW_SCOPE = "greenfield workflow authoring";
|
|
52
52
|
export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
53
53
|
lane: LANES.workflow,
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -492,6 +492,20 @@ function participantReferenceWarnings(template) {
|
|
|
492
492
|
if (raw === undefined || raw === null || String(raw).trim().length === 0)
|
|
493
493
|
continue; // platform may fill later
|
|
494
494
|
const participant = String(raw).trim();
|
|
495
|
+
// r4-training 2026-10-20: two function names that show up in read-back /
|
|
496
|
+
// seeded templates are NOT registered at runtime in AI-Platform v1.1.25 —
|
|
497
|
+
// they publish fine, then ParticipantActivityRuntime throws
|
|
498
|
+
// FunctionNotFoundException, the instance stalls in PROCESSING and the
|
|
499
|
+
// submit still answers errCode 0. Flag them by name (advisory, not a block).
|
|
500
|
+
const unregistered = /^(FindUsersByRoleFunction|FindUsersSpecByRoleFunction)\s*\(/.exec(participant);
|
|
501
|
+
if (unregistered) {
|
|
502
|
+
warnings.push({
|
|
503
|
+
activityCode: String(activity.activityCode ?? ""),
|
|
504
|
+
participant,
|
|
505
|
+
warning: `参与者函数 ${unregistered[1]}(...) 在 AI-Platform v1.1.25 未注册(运行期 FunctionNotFoundException,实例卡 PROCESSING 且提交仍回 errCode 0)。内置写法只有 FindUserByRole(org, roleCode[, order]);跨部门/全局角色优先用参与者对象数组(角色 id)。不要从 workflow.get_published 或种子流程回抄这个函数名。`,
|
|
506
|
+
});
|
|
507
|
+
continue;
|
|
508
|
+
}
|
|
495
509
|
if (isPlatformParticipantReference(participant))
|
|
496
510
|
continue;
|
|
497
511
|
warnings.push({
|
|
@@ -12,25 +12,28 @@ const ruleGraph = Object.freeze({
|
|
|
12
12
|
nodeGraph: {
|
|
13
13
|
nodes: [
|
|
14
14
|
{ nodeCode: "Start", nodeType: "START" },
|
|
15
|
-
{ nodeCode: "UPDATE_1", nodeType: "UPDATE", targetObjectCode: model, inputParam: "_input", filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] }, dataActions: [{ targetDataItem: "status", ruleValueType: "FIXED", value: "待处理", ruleActionType: "EQUALS" }] },
|
|
16
15
|
{ nodeCode: "Create", nodeName: "新增数据", nodeType: "SYSTEM_CREATE", isSystemNode: true },
|
|
16
|
+
{ nodeCode: "UPDATE_1", nodeType: "UPDATE", targetObjectCode: model, inputParam: "_input", filterCondition: { mainConditions: [[{ targetSchemaDataItem: "id", ruleConditionType: "EQ", ruleValueType: "DYNAMIC", currentSchemaDataItem: "id" }]], sheetConditions: [] }, dataActions: [{ targetDataItem: "status", ruleValueType: "FIXED", value: "待处理", ruleActionType: "EQUALS" }] },
|
|
17
17
|
{ nodeCode: "End", nodeType: "END" },
|
|
18
18
|
],
|
|
19
19
|
routes: [
|
|
20
|
-
{ preNode: "Start", postNode: "
|
|
21
|
-
{ preNode: "
|
|
22
|
-
{ preNode: "
|
|
20
|
+
{ preNode: "Start", postNode: "Create" },
|
|
21
|
+
{ preNode: "Create", postNode: "UPDATE_1" },
|
|
22
|
+
{ preNode: "UPDATE_1", postNode: "End" },
|
|
23
23
|
],
|
|
24
24
|
},
|
|
25
25
|
publish: true,
|
|
26
26
|
},
|
|
27
27
|
preconditions: ["模型和 target field 已发布;Create 是平台预置触发规则;目标字段可由规则动作写入"],
|
|
28
|
-
conditionalRequirements: [
|
|
28
|
+
conditionalRequirements: [
|
|
29
|
+
"**自更新 UPDATE 必须排在 SYSTEM_CREATE 之后**(Start→SYSTEM_CREATE→UPDATE→End)——排在前面时规则能存能启用,运行时 `UpdateServiceExecutor` 查不到触发行(日志「查找范围不存在数据,不再做修改数据动作」),字段静默不写回。真机对照 2026-10-20:UPDATE 在 SYSTEM_CREATE 前 → 值不变;挪到其后 → 值写入成功。注意与 ASSIGN/BIZ_ACTION 相反,那两类要排在系统节点之前。",
|
|
30
|
+
"BIZ_ACTION 的 outputMappings 必须在 SYSTEM_CREATE/SYSTEM_UPDATE 前被消费;自定义 ruleCode 不会被数据事件自动分发",
|
|
31
|
+
],
|
|
29
32
|
priorToolReferences: [
|
|
30
33
|
{ tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认 status 已发布" },
|
|
31
34
|
],
|
|
32
35
|
dryRun: { supported: true, command: `omc call rule.save --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 执行 rule graph dry lint;不会保存/发布规则" },
|
|
33
|
-
expectedReadback: { performed: { published: true, enabled: false }, readback: { published: true, next: "rule.enable", inputParam: "_input", systemNodeOrder: "
|
|
36
|
+
expectedReadback: { performed: { published: true, enabled: false }, readback: { published: true, next: "rule.enable", inputParam: "_input", systemNodeOrder: "SYSTEM_CREATE → UPDATE_1" } },
|
|
34
37
|
refusalRecovery: [
|
|
35
38
|
{ code: "rule-graph-validation-failed", action: "按 validation 修正 nodeType、路线、_input 和系统节点顺序后重新 dry-run" },
|
|
36
39
|
{ code: "rule-schema-unresolved", action: "先确认模型存在,不要让 list/save 为幻影 schema 补建预置规则" },
|
package/dist/recipes/workflow.js
CHANGED
|
@@ -27,6 +27,8 @@ const workflowTemplateUpdate = Object.freeze({
|
|
|
27
27
|
"多级审批:为每个审批节点各写一个 PARTICIPANT 活动(activityCode 唯一),END 放最后;数组里按顺序就是审批链顺序;新增的审批节点由工具自动接入链路(补全到 END 的连线),无需自己写 routes;",
|
|
28
28
|
"**{Originator}=发起人自己**:审批节点用它=提交人自审自己——提交人与审批人同账号时平台可能直接自动通过(COMPLETED,无待办;r40 实证,与节点结构有关)。需要独立审批人时绑定具体用户 id 或 `FindUserByRole({Originator.OU},\"<角色code>\",\"random\")`;验收要 PENDING 待办时先确认审批人≠提交人;",
|
|
29
29
|
"**participant 必须是平台可解析的三种形态之一(真实生产 2026-09-23 观察)**:(a) 关键字 `{Originator}`;(b) 参与者对象数组 JSON `[{\"code\":\"<角色code>\",\"corpId\":\"<corpId|other>\",\"id\":\"<roleId>\",\"name\":\"<角色名>\",\"type\":2}]`(type 2=角色,3=用户;多个用 `+` 连接);(c) 函数 `FindUserByRole({Originator.OU},\"<roleCode>\",\"random\")`。**自由文本角色名(如 `部门负责人`)能 publish 但解析不到人、不生成待办**——先用 `org.role.list` / `org.department.list` 拿 code/id 再构造。工具会在 readback 里以 `participantWarnings` 标出自由文本。",
|
|
30
|
+
"**不要从读回/种子配置回抄函数式参与人**:`workflow.get_published` 的摘要与平台种子流程(如 crm_contract_flow)会显示 `FindUsersByRoleFunction(...)` / `FindUsersSpecByRoleFunction(...)`,1.1.25 运行期**未注册这两个名字**,原样写回会在 ParticipantActivityRuntime 抛 `FunctionNotFoundException`;参与人表达式只认 `FindUserByRole(org, roleCode[, order])`(r4-training 实证 2026-10-20,`cloudpivot-workflow-AI-Platform-v1.1.25.jar` 只有 `FindUserByRoleFunction`)。",
|
|
31
|
+
"**参与人解析为空同样是静默失败**:`FindUserByRole({Originator.OU},...)` 只找该部门范围内的角色成员;角色成员未挂部门(或角色零成员)时返回 `[]`,实例卡 PROCESSING、无待办、接口仍回 errCode 0。验收必须看 `workitem.list` 或容器日志 `CustomParticipleMethod{...} -> []`,不能只看 `data.submit` 返回;需要跨部门/全局角色时优先用参与者对象数组(角色 id),或先给角色补齐部门范围成员。",
|
|
30
32
|
"**审批节点必须带 sheetCode(=模型默认表单槽位,通常等于 schemaCode)——S29 契约:participant sheetCode 为空 publish 会 401026 WORKFLOW_ACTIVITY_NO_SHEET。工具已自动回填,正常无需手填**;",
|
|
31
33
|
"节点类型 START / PARTICIPANT / END;省略的既有节点由 merge 保留;",
|
|
32
34
|
"**平台 workflow.create 会自动生成含「填写申请单」发起节点的草稿(START → 填写申请单 → 审批 → END);workflow.update 是 merge,不删除既有节点——要严格三节点需在设计器删该节点,或如实接受该发起节点**",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.60",
|
|
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.60",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.60",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -13,6 +13,7 @@ A change is accepted only when every gate below holds.
|
|
|
13
13
|
2. **Authoritative readback** — prove the intended state from the authoritative face (`field.list`, `form.get`, `data.load`, `onlinejs.get` for Online JS, `model.inspect` for workflow/rules), pairing each write with its same-family reader (`onlinejs.compose`→`onlinejs.get`; `report.datasource.link`→`linkedCount>0`). "Called without error" is not acceptance.
|
|
14
14
|
2a. **Judge must be decidable** — prove the field exists first; all-same values (`false`/`null`/`0`) mean a wrong judge, not a pass. `form.get` has no `customScript` key — use `onlinejs.get`.
|
|
15
15
|
2b. **Reachable ≠ correct** — `method.test success:true` proves reachability only; without a contract report "接口连通,业务对接未验证(契约缺失)".
|
|
16
|
+
2c. **Allowed ≠ advanced** — `data.submit` → `PROCESSING` only proves the instance exists. Prove the next task via `workitem.list` or a later submit; a stalled instance still answers `errCode 0`.
|
|
16
17
|
3. **Delivery state recorded** — the `.omc/` artifact exists with `deliveryState: DELIVERED` and the environment fingerprint.
|
|
17
18
|
4. **Zero residue** — fixtures removed and exact absence proven on every face (draft and published); final state CLEANED.
|
|
18
19
|
|
|
@@ -150,6 +150,7 @@ rules:
|
|
|
150
150
|
|
|
151
151
|
- **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
|
|
152
152
|
- 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
|
|
153
|
+
- **参与人解析失败是静默的**:角色零成员、成员未挂部门(`FindUserByRole({Originator.OU},...)` 只找部门范围)、或写了未注册函数(`FindUsersByRoleFunction` / `FindUsersSpecByRoleFunction`,1.1.25 未内置)时,`data.submit` 仍回 `errCode 0`,实例停在 PROCESSING 且 `biz_workitem` 无下一节点。验收看 `workitem.list` 是否有新待办,或容器日志的 `GetParticipantInfo error` / `CustomParticipleMethod{...} -> []`;不要从 `workflow.get_published` 的摘要回抄函数式参与人。内置写法只有 `FindUserByRole(org, roleCode[, order])`,跨部门/全局角色优先用参与者对象数组(角色 id)。
|
|
153
154
|
|
|
154
155
|
## 流程字段权限
|
|
155
156
|
|
|
@@ -89,6 +89,18 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
89
89
|
|
|
90
90
|
父 `CHILD_TABLE` 行不接受 `options/choices/subFields` 更新;子列直接走上面的 CRUD 面。
|
|
91
91
|
|
|
92
|
+
- **提交/审批都不要再回传子表数组**(真机 2026-10-19/20):`data.save` 建草稿后 `data.submit` 若带子表行(无 `id`),平台按新增追加(3→9);**审批提交同样**——`data.submit` 带 `workItemId`+`agree` 推进一步时回传子表,每级再 +1 行(1→2→3)。金额若显式传了合计不会翻倍,但行数与 SUM 口径已被污染,且没有报错。审批只回传标量字段(`agree`/意见/被改标量);确需改子行时带 `id`+`rowStatus:"Modified"`。验收加一条"审批后子表行数不变"。
|
|
93
|
+
|
|
94
|
+
## 写调用纪律
|
|
95
|
+
|
|
96
|
+
- **同一 `<env>` 的写调用必须串行发起**:`write-lock-held` 不只在"并行重放失败请求"时出现,也会在**并行发起两个无关写**时触发(实测:并行两条 `dictionary.save`、并行 `field.update`+`field.publish` 各失败一次)。读可并行,写不要。
|
|
97
|
+
|
|
98
|
+
## 计算规则(datarule)的取值时机
|
|
99
|
+
|
|
100
|
+
- **计算由门户前端执行,API 写入不回填**:`datarule.create` 接受 `IF({实际日期}>{需求日期},"是","否")` 这类比较式并落库,但 `data.save`/`data.submit` 不计算结果值,读回仍是 null。
|
|
101
|
+
- 所以"计算规则是否生效"**无法用 API 判定**,必须在门户录一条看结果;验收清单要写"计算类字段前台确认"。
|
|
102
|
+
- 需要服务端就有值(参与汇总、流程分流、超期判定)时不要依赖 datarule:改用二开接口在保存/审批时算好并回写标量。
|
|
103
|
+
|
|
92
104
|
## 规则与业务集成
|
|
93
105
|
|
|
94
106
|
- 规则节点缺 `nodeName` 时 CLI 按平台 `RuleNodeType` 补默认名并给 warning;业务节点优先显式命名。
|