@stalfh233/omc-cli 0.4.81 → 0.4.83
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -1
- package/coverage/m1-coverage-manifest-v1.json +4 -0
- package/dist/capability-overview.js +1 -1
- package/dist/cli.js +2 -2
- package/dist/compute.js +87 -5
- package/dist/endpoints.js +12 -0
- package/dist/field-families.js +1 -1
- package/dist/lanes/field-contract.js +2 -2
- package/dist/lanes/model.js +47 -3
- package/dist/lanes/rule-graph.js +1 -1
- package/dist/lanes/runtime-data.js +2 -2
- package/dist/lanes/workflow-contract.js +15 -0
- package/dist/lanes/workflow-coverage.js +3 -0
- package/dist/lanes/workflow.js +188 -0
- package/dist/recipes/datarule.js +3 -3
- package/dist/runtime-support.js +92 -1
- package/package.json +3 -3
- package/skills/omc-capabilities.md +3 -3
- package/skills/omc-requirement-vocabulary.md +3 -2
- package/skills/omc-write-hazards.md +7 -3
package/README.md
CHANGED
|
@@ -130,10 +130,12 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
|
|
|
130
130
|
- **intent**:`partial` 表示该能力可声明式(`omc intent`)覆盖一部分——通常 create/update;
|
|
131
131
|
`none` 表示只走 `omc call`。删除类、方法面、分类/连接池等仍走 `omc call` 逃生舱。
|
|
132
132
|
|
|
133
|
-
## 近期能力(0.4.74 → 0.4.
|
|
133
|
+
## 近期能力(0.4.74 → 0.4.83)
|
|
134
134
|
|
|
135
135
|
| 版本 | 新增 / 修复 |
|
|
136
136
|
|---|---|
|
|
137
|
+
| 0.4.83 | **r12 真机探针修正(最重要:文档纠错)**:字段计算规则(`datarule` type2)**由服务端在 `data.submit`/`data.update` 时计算并回填,且覆盖传入值**——此前 skills/describe 六处写的「API 不回填、服务端聚合需二开」是错的(旧证据只覆盖 `FORMULA(100)` 字段)。另:`omc compute` 新增 `--where "<子列> = <值>"`(编译 COUNTIF/SUMIF)并**拒绝无条件子表聚合**(`--allow-unfiltered` 显式放行);`model.delete` 把「孤儿模型」死锁变成一次明确回答——先复查读缓存,仍存在则回 `model-orphaned-record` 并给平台侧出路 |
|
|
138
|
+
| 0.4.82 | **流程发起权限(40009)**:新增 `workflow.permissions` / `workflow.addPermission` / `workflow.removePermission`(`/workflow/list_permission`、`create_permission`、`delete_permission`,unitType 线上为枚举序号 1=部门 2=角色 3=用户)。`data.submit`/`data.save+workflowCode` 的 40009 现在直接回 `workflow-start-permission-denied`:带上该流程的权限行现状、两种成因(无权限行 / 发起人部门记录已失效)与可粘贴的修复命令,不再是「流程发起权限不足」五个字 |
|
|
137
139
|
| 0.4.81 | **r11 真机探针修复**:`rule.save` 对 `AGGREGATION` 规则节点**写前 fail-closed**(平台会存进去、运行时 50000,真实路径是 `omc compute`/datarule type2);`omc next "统计子表行数"` 路由补 `datarule` 阶梯与 `field-compute-subtable-sum` 配方;`org.user.search` 省略关键字时**按部门树列全员**(平台 `wd` 不支持空串,此前只能枚举 0–9);`omc artifact get --jq` 无值回键清单(不再 commander 报错);`usage` 的 `faces` 纳入 `--jq/--summary/--keys/--path-only/--extract`(此前读面投影完全不记账) |
|
|
138
140
|
| 0.4.80 | **省 token 与用量账本**:`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数),`--all` 才读整篇;`omc artifact get` 正文 >8KB 默认只回形状(`--full` 才整包);`omc usage` 默认只回汇总(`byTool`/`redundancy`/`estimatedTokens`),`--records` 才展开;`omc script` 的**每行工具调用单独记账**(`script-call` + `parentId`);未绑定环境的目录回落 `$OMC_CONFIG_HOME/usage/<cwd-hash>.jsonl` |
|
|
139
141
|
| 0.4.79 | README 对齐 0.4.79 实际工具面(142 工具 / 16 能力) |
|
|
@@ -339,6 +339,9 @@
|
|
|
339
339
|
"workflow.bindRule",
|
|
340
340
|
"workflow.setButtons",
|
|
341
341
|
"workflow.delete",
|
|
342
|
+
"workflow.permissions",
|
|
343
|
+
"workflow.addPermission",
|
|
344
|
+
"workflow.removePermission",
|
|
342
345
|
"configure_workflow",
|
|
343
346
|
"workflow.validate",
|
|
344
347
|
"org.role.users",
|
|
@@ -365,6 +368,7 @@
|
|
|
365
368
|
"scopes": [
|
|
366
369
|
"greenfield workflow authoring",
|
|
367
370
|
"workflow node form-button checkboxes",
|
|
371
|
+
"workflow start permission (who may originate)",
|
|
368
372
|
"organization role member read (role/users)",
|
|
369
373
|
"organization role member management scope read (role/get_ouscope)",
|
|
370
374
|
"organization role group create (role/add_role_group)",
|
|
@@ -71,7 +71,7 @@ const SURFACES = [
|
|
|
71
71
|
description: "字段族全链:create→publish→表单绑定;CHILD_TABLE 子列;FORMULA 计算(子表求和回主表、子表列同行运算)。",
|
|
72
72
|
operations: ["create", "update", "publish", "delete"],
|
|
73
73
|
intent: { level: "partial", note: "create/update(含 CHILD_TABLE subFields、FORMULA 计算)、publish 与 delete(absent:true)已声明式。" },
|
|
74
|
-
notes: "子表列是 schemaCode=父字段 code 上的普通字段;field.update 不接受 subFields。子列支持标量族 + WORK_SHEET 关联列(需 relativeCode + options)。**计算类已覆盖(默认走计算规则,非 FORMULA 字段)**:子表求和、子表列同行运算用「普通字段 + 计算规则」(`describe datarule.create --example field-compute-rule|field-compute-subtable-sum|field-compute-child-column
|
|
74
|
+
notes: "子表列是 schemaCode=父字段 code 上的普通字段;field.update 不接受 subFields。子列支持标量族 + WORK_SHEET 关联列(需 relativeCode + options)。**计算类已覆盖(默认走计算规则,非 FORMULA 字段)**:子表求和、子表列同行运算用「普通字段 + 计算规则」(`describe datarule.create --example field-compute-rule|field-compute-subtable-sum|field-compute-child-column`);计算规则由**服务端**在 data.submit/data.update 时计算并回填且覆盖传入值(真机 1.1.25 复核)。嵌套子表/MULT_WORK_SHEET/RELEVANCE_DATA 不覆盖。",
|
|
75
75
|
},
|
|
76
76
|
{
|
|
77
77
|
id: "form",
|
package/dist/cli.js
CHANGED
|
@@ -1146,8 +1146,8 @@ async function main() {
|
|
|
1146
1146
|
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] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(options.extractKeys ? ["--extract-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"] : [])];
|
|
1147
1147
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
1148
1148
|
});
|
|
1149
|
-
command.command("compute").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--target <propertyCode>").option("--property <propertyCode>").option("--formula <formula>").option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n字段计算规则拼装器:公式 {字段} 引用 → datarule type2 完整形状(editorText/editorMark 自动生成)。默认 dry-run,--apply
|
|
1150
|
-
const rest = ["compute", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(options.target ? ["--target", options.target] : []), ...(options.property ? ["--property", options.property] : []), ...(options.formula !== undefined ? ["--formula", options.formula] : []), ...(options.name ? ["--name", options.name] : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : [])];
|
|
1149
|
+
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) => {
|
|
1150
|
+
const rest = ["compute", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(options.target ? ["--target", options.target] : []), ...(options.property ? ["--property", options.property] : []), ...(options.formula !== undefined ? ["--formula", options.formula] : []), ...(options.where !== undefined ? ["--where", options.where] : []), ...(options.aggregate !== undefined ? ["--aggregate", options.aggregate] : []), ...(options.allowUnfiltered ? ["--allow-unfiltered"] : []), ...(options.name ? ["--name", options.name] : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : [])];
|
|
1151
1151
|
commandExitCode = await cmdCompute(rest);
|
|
1152
1152
|
});
|
|
1153
1153
|
// Commander 14 needs a real variadic Option; the old `{variadic:true}` cast
|
package/dist/compute.js
CHANGED
|
@@ -17,23 +17,90 @@ import { runToolCall } from "./call.js";
|
|
|
17
17
|
import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contract.js";
|
|
18
18
|
import { writeEnvelope } from "./cli-output.js";
|
|
19
19
|
import { argvValue } from "./args.js";
|
|
20
|
+
/**
|
|
21
|
+
* The offending aggregate call when a formula aggregates a whole child column
|
|
22
|
+
* with no filter function, else undefined. COUNTIF/SUMIF/IF-wrapped calls are
|
|
23
|
+
* filtered by construction and pass.
|
|
24
|
+
*/
|
|
25
|
+
export function childAggregateWithoutFilter(formula) {
|
|
26
|
+
const upper = formula.toUpperCase();
|
|
27
|
+
if (upper.includes("COUNTIF") || upper.includes("SUMIF"))
|
|
28
|
+
return undefined;
|
|
29
|
+
// An IF(...) around the aggregate is the hand-written filtered form.
|
|
30
|
+
if (upper.trimStart().startsWith("IF("))
|
|
31
|
+
return undefined;
|
|
32
|
+
for (const fn of CHILD_AGGREGATES) {
|
|
33
|
+
const pattern = new RegExp(`${fn}\\(\\s*\\{([^{}]+\\.[^{}]+)\\}\\s*\\)`, "i");
|
|
34
|
+
const match = pattern.exec(formula);
|
|
35
|
+
if (match)
|
|
36
|
+
return `${fn}(${match[1]})`;
|
|
37
|
+
}
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
20
40
|
/** 编译 formula → editorText:每个 {token} 引用包成 <field-mark>裸token</field-mark>。 */
|
|
21
41
|
export function compileEditorText(formula, editorMark) {
|
|
22
42
|
return formula.replace(/\{([^{}]+)\}/g, (_match, token) => `<${editorMark}>${token.trim()}</${editorMark}>`);
|
|
23
43
|
}
|
|
44
|
+
/** Aggregation functions that consume a whole child column. */
|
|
45
|
+
const CHILD_AGGREGATES = ["SUM", "COUNT", "MAX", "MIN", "AVG", "UNION"];
|
|
46
|
+
/**
|
|
47
|
+
* Compile `--where "<子列> = <值>"` into the platform's conditional aggregate.
|
|
48
|
+
*
|
|
49
|
+
* Shape: `COUNTIF({<child>.<column>}, "<value>")` / `SUMIF({<child>.<column>}, "<value>")`.
|
|
50
|
+
* Only equality is supported: the platform's aggregator takes a value, not a
|
|
51
|
+
* predicate, and inventing operator support would silently emit a formula the
|
|
52
|
+
* engine reads differently.
|
|
53
|
+
*/
|
|
54
|
+
export function compileConditionalAggregate(options) {
|
|
55
|
+
const source = options.formula.trim();
|
|
56
|
+
const where = options.where.trim();
|
|
57
|
+
const requested = (options.aggregate ?? "count").toLowerCase();
|
|
58
|
+
// The formula must already be a bare child-column reference: `{child.column}`.
|
|
59
|
+
const reference = /^\{([^{}]+)\}$/.exec(source);
|
|
60
|
+
if (!reference)
|
|
61
|
+
throw new Error(`--where 需要 --formula 是单个子表列引用,如 --formula "{<子表code>.<子列code>}"(当前:"${source}")`);
|
|
62
|
+
const token = reference[1].trim();
|
|
63
|
+
if (!token.includes("."))
|
|
64
|
+
throw new Error(`--where 的 --formula 必须是「子表code.子列code」形式(当前:"${token}")`);
|
|
65
|
+
const columnCode = token.split(".").pop().trim();
|
|
66
|
+
const condition = /^([^=<>!]+?)\s*=\s*(.+)$/.exec(where);
|
|
67
|
+
if (!condition)
|
|
68
|
+
throw new Error(`--where 只支持「<子列code> = <值>」(平台聚合器接受的是值,不是谓词;当前:"${where}")`);
|
|
69
|
+
const whereColumn = condition[1].trim().split(".").pop().trim();
|
|
70
|
+
const expected = condition[2].trim().replace(/^["']|["']$/g, "");
|
|
71
|
+
if (whereColumn !== columnCode)
|
|
72
|
+
throw new Error(`--where 的列(${whereColumn})与 --formula 引用列(${columnCode})不一致;条件聚合只作用于同一列,如 --formula "{items.checkResult}" --where "checkResult = 异常"`);
|
|
73
|
+
if (requested !== "count" && requested !== "sum")
|
|
74
|
+
throw new Error(`--aggregate 只支持 count/sum(条件聚合映射为 COUNTIF/SUMIF,当前 "${String(options.aggregate)}")`);
|
|
75
|
+
return `${requested === "sum" ? "SUMIF" : "COUNTIF"}({${token}},${JSON.stringify(expected)})`;
|
|
76
|
+
}
|
|
24
77
|
/** 组装 datarule.create 的完整 args(导出供测试直接断言形状)。 */
|
|
25
78
|
export function composeComputeArgs(options) {
|
|
26
79
|
const editorMark = "field-omc0001";
|
|
27
|
-
|
|
28
|
-
|
|
80
|
+
// Conditional aggregation REPLACES the formula: the caller describes the
|
|
81
|
+
// intent ("count where X = Y") and the composer emits the platform's IF form.
|
|
82
|
+
const effectiveFormula = options.where !== undefined ? compileConditionalAggregate(options) : options.formula;
|
|
83
|
+
const editorText = compileEditorText(effectiveFormula, editorMark);
|
|
84
|
+
if (!/\{[^{}]+\}/.test(effectiveFormula))
|
|
29
85
|
throw new Error("--formula 至少含一个字段引用(如 {amount});纯常量没有计算规则意义");
|
|
30
86
|
if (editorText.includes("{") || editorText.includes("}"))
|
|
31
87
|
throw new Error("公式内出现嵌套花括号({{...}}),拼装器暂不支持;请展开为简单引用");
|
|
88
|
+
// r12: an unfiltered COUNT/SUM over a child column dry-runs clean and then
|
|
89
|
+
// counts EVERY row. That is occasionally what you want, but "统计子表中满足
|
|
90
|
+
// 条件的行数" is the common ask, and silently aggregating everything is the
|
|
91
|
+
// wrong answer written to the platform. Refuse instead of warning: `--apply`
|
|
92
|
+
// makes this a data defect, not a hint.
|
|
93
|
+
const unfilteredChild = childAggregateWithoutFilter(effectiveFormula);
|
|
94
|
+
if (unfilteredChild !== undefined && options.where === undefined && options.allowUnfiltered !== true)
|
|
95
|
+
throw new Error(`公式 ${unfilteredChild} 对整个子表列聚合,没有任何过滤条件——会把不符合条件的行也算进去。` +
|
|
96
|
+
`若要「统计满足条件的行数」,用 --where:omc compute --schema ${options.schema} --target ${options.target} ` +
|
|
97
|
+
`--formula "{<子表code>.<子列code>}" --where "<子列code> = <值>"(编译为 COUNTIF / SUMIF)。` +
|
|
98
|
+
`确实要无条件聚合时,加 --allow-unfiltered 显式确认。`);
|
|
32
99
|
return {
|
|
33
100
|
schemaCode: options.schema,
|
|
34
101
|
propertyCode: options.target,
|
|
35
102
|
dataRuleType: 2,
|
|
36
|
-
options: { shortTextStitch: JSON.stringify({ formula:
|
|
103
|
+
options: { shortTextStitch: JSON.stringify({ formula: effectiveFormula, editorText, editorMark }) },
|
|
37
104
|
name: options.name ?? `${options.target} 计算规则`,
|
|
38
105
|
...(options.environment ? { environment: options.environment } : {}),
|
|
39
106
|
dryRun: !options.apply,
|
|
@@ -48,7 +115,15 @@ export async function cmdCompute(args) {
|
|
|
48
115
|
const name = flag("--name");
|
|
49
116
|
const environment = flag("--env") ?? flag("--environment");
|
|
50
117
|
const apply = args.includes("--apply");
|
|
51
|
-
const
|
|
118
|
+
const where = flag("--where");
|
|
119
|
+
const aggregateFlag = flag("--aggregate");
|
|
120
|
+
const allowUnfiltered = args.includes("--allow-unfiltered");
|
|
121
|
+
const usage = [
|
|
122
|
+
"用法:omc compute --schema <模型code> --target <目标字段> --formula \"<公式,{字段} 引用>\" [--name 规则名] [--env 环境] [--apply]",
|
|
123
|
+
"条件聚合:--formula \"{<子表code>.<子列code>}\" --where \"<子列code> = <值>\" [--aggregate count|sum] → 编译为 COUNTIF / SUMIF",
|
|
124
|
+
"公式语法:同类字段 {字段code};子表列 {子表code.子列code};无条件聚合 SUM/COUNT/MAX/MIN/AVG({子表.子列}) 需加 --allow-unfiltered 显式确认。",
|
|
125
|
+
"默认 dry-run,--apply 才写入。",
|
|
126
|
+
].join("\n");
|
|
52
127
|
const invalid = (message) => {
|
|
53
128
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
54
129
|
return CLI_EXIT_CODES.invalidInput;
|
|
@@ -57,7 +132,14 @@ export async function cmdCompute(args) {
|
|
|
57
132
|
return invalid("--schema/--target/--formula 均必填");
|
|
58
133
|
let createArgs;
|
|
59
134
|
try {
|
|
60
|
-
|
|
135
|
+
if (aggregateFlag !== undefined && !["count", "sum"].includes(aggregateFlag))
|
|
136
|
+
return invalid(`--aggregate 只支持 count 或 sum(当前 "${aggregateFlag}")`);
|
|
137
|
+
createArgs = composeComputeArgs({
|
|
138
|
+
schema, target, formula, name, environment, apply,
|
|
139
|
+
...(where !== undefined ? { where } : {}),
|
|
140
|
+
...(aggregateFlag !== undefined ? { aggregate: aggregateFlag } : {}),
|
|
141
|
+
...(allowUnfiltered ? { allowUnfiltered: true } : {}),
|
|
142
|
+
});
|
|
61
143
|
}
|
|
62
144
|
catch (error) {
|
|
63
145
|
return invalid(error instanceof Error ? error.message : String(error));
|
package/dist/endpoints.js
CHANGED
|
@@ -152,6 +152,18 @@ export const ENDPOINTS = {
|
|
|
152
152
|
workflowValidate: "/api/api/workflow/validate",
|
|
153
153
|
workflowPublish: "/api/api/workflow/publish",
|
|
154
154
|
workflowDelete: "/api/api/workflow/delete",
|
|
155
|
+
// 发起权限 (who may START the flow). Without a row here — or with a role/
|
|
156
|
+
// department row the caller is not in — `data.submit`/`data.save+workflowCode`
|
|
157
|
+
// answers 40009 流程发起权限不足 (WorkflowAspect.validateWorkflowPermission →
|
|
158
|
+
// WorkflowManagementFacadeImpl.getWorkflowTemplateHeadersBySchemaCode). Endpoints
|
|
159
|
+
// from WorkflowTemplateController (cloudpivot-engine 8.6.26; live 1.1.25):
|
|
160
|
+
// GET /workflow/list_permission?workflowCode= → [{id,name,unitType,fullName,corpId,code}]
|
|
161
|
+
// POST /workflow/create_permission body {workflowCode,unitId,unitType,corpId?}
|
|
162
|
+
// unitType is the ENUM INDEX on the wire: 1=部门 2=角色 3=用户 (UnitType @JsonValue)
|
|
163
|
+
// POST/DELETE /workflow/delete_permission?permissionId=
|
|
164
|
+
workflowListPermission: "/api/api/workflow/list_permission",
|
|
165
|
+
workflowCreatePermission: "/api/api/workflow/create_permission",
|
|
166
|
+
workflowDeletePermission: "/api/api/workflow/delete_permission",
|
|
155
167
|
workflowSearchWorkItems: "/api/api/runtime/workflow/search_workitems",
|
|
156
168
|
workflowGetBizObjectByWfInstanceId: "/api/api/runtime/workflow/getBizObjectByWfInstanceId",
|
|
157
169
|
workflowListInstanceActivity: "/api/api/runtime/workflow/list_workflow_instance_activity",
|
package/dist/field-families.js
CHANGED
|
@@ -171,7 +171,7 @@ export const fieldOptionsArg = z
|
|
|
171
171
|
searchFormula: jsonString().optional().describe("WORK_SHEET / MULT_WORK_SHEET data restriction range JSON"),
|
|
172
172
|
conditions: jsonString().optional().describe("WORK_SHEET / MULT_WORK_SHEET query conditions JSON"),
|
|
173
173
|
optionsSet: optionsSetArg.optional().describe("selection source: custom, dictionary, or businessModel"),
|
|
174
|
-
formulaConfig: z.string().optional().describe('FORMULA(计算公式)字段 required: nested JSON string. **计算类需求默认不要建本字段类型**,改用普通字段(数值/单行文本)+计算规则(datarule.create dataRuleType 2, 裸 {字段} 引用)。本字段类型用于明确的「计算公式」列。形状 {"resultType":0|2,"resultFormat":""|"percentile","formulaType":"EXPRESSION","expression":"<JSON string {formula,editorText,editorMark}>"}
|
|
174
|
+
formulaConfig: z.string().optional().describe('FORMULA(计算公式)字段 required: nested JSON string. **计算类需求默认不要建本字段类型**,改用普通字段(数值/单行文本)+计算规则(datarule.create dataRuleType 2, 裸 {字段} 引用)。本字段类型用于明确的「计算公式」列。形状 {"resultType":0|2,"resultFormat":""|"percentile","formulaType":"EXPRESSION","expression":"<JSON string {formula,editorText,editorMark}>"};本类型字段由门户前端计算、API 不回填(计算规则 datarule type2 则是服务端回填)。'),
|
|
175
175
|
persistent: z.boolean().optional().describe("FORMULA(100): must be true; persistent=false is outside the evidenced contract"),
|
|
176
176
|
relevanceField: z.string().optional().describe("RELEVANCE_DATA(16) required: local WORK_SHEET field code the reference resolves through"),
|
|
177
177
|
referenceField: z.string().optional().describe("RELEVANCE_DATA(16) required: referenced field code on the related model"),
|
|
@@ -90,9 +90,9 @@ export const FIELD_FORMULA_WIRE = Object.freeze({
|
|
|
90
90
|
outerOptions: '{"formulaConfig":"<JSON string>","persistent":true[, "format":""|"percentile" — readback-only mirror, fwb §100]}',
|
|
91
91
|
innerConfig: '{"resultType":0|2,"resultFormat":""|"percentile","formulaType":"EXPRESSION","expression":"<JSON string {formula,editorText,editorMark}>"}',
|
|
92
92
|
expressionFormula: '<fieldCode> placeholders, e.g. {omc_a} + {omc_b}',
|
|
93
|
-
aggregationConfig: 'AGGREGATION nested {schemaCode,dataItemCode,calculateType,formula,queryConditions} = 表单「计算」控件的「聚合计算」(UI 实测 2026-10-07:跨模型聚合,求和/最大/最小/…). FormulaProcessor.executeMainAggregation 用 queryConditions 查该 schema、对匹配行的 dataItemCode 求值 formula. shape-validated & allowed. **计算类需求默认不用 FORMULA 字段**:子表求和回主表、子表列同行运算一律「普通字段 + 计算规则」(`datarule.create` dataRuleType:2,recipes `field-compute-subtable-sum` / `field-compute-child-column`,前台真机算得 7.00/350);FORMULA
|
|
93
|
+
aggregationConfig: 'AGGREGATION nested {schemaCode,dataItemCode,calculateType,formula,queryConditions} = 表单「计算」控件的「聚合计算」(UI 实测 2026-10-07:跨模型聚合,求和/最大/最小/…). FormulaProcessor.executeMainAggregation 用 queryConditions 查该 schema、对匹配行的 dataItemCode 求值 formula. shape-validated & allowed. **计算类需求默认不用 FORMULA 字段**:子表求和回主表、子表列同行运算一律「普通字段 + 计算规则」(`datarule.create` dataRuleType:2,recipes `field-compute-subtable-sum` / `field-compute-child-column`,前台真机算得 7.00/350);FORMULA 字段仅作已存量读取。计算规则(`datarule.create` dataRuleType:2)**由服务端在 data.submit/data.update 时计算并回填,且覆盖传入值**(真机 1.1.25 复核 2026-10-24);本 FORMULA 类型字段才是仅前端计算、API 不回填。',
|
|
94
94
|
legacyRejected: "resultType/expression top-level keys are not read by the platform and are refused",
|
|
95
|
-
runtimeVerification: "FORMULA
|
|
95
|
+
runtimeVerification: "本 FORMULA(100) 字段类型:计算由门户**前端**执行,API save/update/submit 不回填/重算(真机 2026-09-25/2026-10-07,仅覆盖该字段类型)。**不要**把这条套到字段计算规则(datarule dataRuleType:2)上——后者由服务端在 data.submit/data.update 时计算并回填,且覆盖传入值(真机 1.1.25 复核 2026-10-24,含删除规则后传入值生效的对照组)。带子表计算字段的模型 API data.save/update 可能 50000(服务端重算 NPE),前台正常。",
|
|
96
96
|
});
|
|
97
97
|
/** RELEVANCE_DATA(16) options wire (8.6.26 RelevanceDataOptions + admin field serializer). */
|
|
98
98
|
export const FIELD_RELEVANCE_WIRE = Object.freeze({
|
package/dist/lanes/model.js
CHANGED
|
@@ -364,7 +364,11 @@ export function registerModelLane(tools) {
|
|
|
364
364
|
blocker: "model-create-duplicate-orphan",
|
|
365
365
|
schemaCode: args.code,
|
|
366
366
|
applicationCode: args.applicationCode ?? null,
|
|
367
|
-
|
|
367
|
+
nextCommand: `omc call model.delete --env <env> --args '{"schemaCode":"${args.code}"}'`,
|
|
368
|
+
message: `模型 ${args.code} 在册(bizmodels/get_all 可读)但不在应用树中。**先跑一次 model.delete**——它现在会把两种成因分开:` +
|
|
369
|
+
`① 读缓存滞后(记录其实已删)→ 回 already-absent/stale-record-cache,随后可直接 create;` +
|
|
370
|
+
`② 真孤儿(删除级联中断留下的残骸)→ 回 model-orphaned-record 并给平台侧出路(回收站物理删除 / 换 code),因为 8.6 平台无法删除没有 AppFunction 行的业务模型。` +
|
|
371
|
+
`不要重复 create(会再造一条孤儿),也不要只信这一条拒绝信息。`,
|
|
368
372
|
});
|
|
369
373
|
}
|
|
370
374
|
}
|
|
@@ -500,11 +504,51 @@ export function registerModelLane(tools) {
|
|
|
500
504
|
if (!node) {
|
|
501
505
|
if (record === null)
|
|
502
506
|
return { performed: { skipped: "already-absent" }, readback: { absent: true } };
|
|
507
|
+
// r12 (2026-10-24): "record readable but no tree node" has TWO causes and
|
|
508
|
+
// the caller could not tell them apart — create said "go fix parentId",
|
|
509
|
+
// model.move said "there is no node", model.delete said "no node means
|
|
510
|
+
// no delete": a closed loop with no exit.
|
|
511
|
+
//
|
|
512
|
+
// Cause A: a stale read face. `bizmodels/get_all` is served through a
|
|
513
|
+
// cache, so a record already removed by a previous delete (or by an
|
|
514
|
+
// interrupted cascade) keeps answering for a while. Re-read after a
|
|
515
|
+
// short wait before declaring anything.
|
|
516
|
+
// ~7s total: long enough to clear a warm read cache, short enough that
|
|
517
|
+
// a genuine orphan is reported without a long stall. Tests can shrink it
|
|
518
|
+
// with OMC_MODEL_ORPHAN_RECHECK_MS.
|
|
519
|
+
const recheckDelay = Number(process.env.OMC_MODEL_ORPHAN_RECHECK_MS ?? 1000);
|
|
520
|
+
let stillPresent = record;
|
|
521
|
+
for (let attempt = 0; attempt < 4 && stillPresent !== null; attempt += 1) {
|
|
522
|
+
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(recheckDelay) ? recheckDelay : 1000));
|
|
523
|
+
stillPresent = await readRecord();
|
|
524
|
+
}
|
|
525
|
+
if (stillPresent === null) {
|
|
526
|
+
return {
|
|
527
|
+
performed: { skipped: "stale-record-cache" },
|
|
528
|
+
readback: {
|
|
529
|
+
absent: true,
|
|
530
|
+
note: "业务模型记录读取面(bizmodels/get_all)先前仍返回该 code,复查后已消失——这是读缓存滞后的残留,模型实际已删除。若刚跑过 model.create 又被拒「已在册」,现在可以重试。",
|
|
531
|
+
},
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
// Cause B: a genuine orphan. The platform has no path: physically_delete
|
|
535
|
+
// resolves the id through AppFunction and `removeBizModel` throws
|
|
536
|
+
// APP_FUNCTION_MODEL_NOTEXIST when the row is gone
|
|
537
|
+
// (AppIntegrationFacadeImpl:1905-1910), so no id can satisfy it.
|
|
503
538
|
throw new ToolRefusalError({
|
|
504
539
|
status: "refused",
|
|
505
|
-
blocker: "model-
|
|
540
|
+
blocker: "model-orphaned-record",
|
|
506
541
|
schemaCode: args.schemaCode,
|
|
507
|
-
|
|
542
|
+
retryable: false,
|
|
543
|
+
platformLimitation: "removeBizModel(AppIntegrationFacadeImpl:1905) 先 getAppFunction(id),找不到即抛 APP_FUNCTION_MODEL_NOTEXIST——没有 AppFunction 行的业务模型记录在 8.6 平台上无法通过 physically_delete 删除。",
|
|
544
|
+
message: `${args.schemaCode} 的业务模型记录仍在(bizmodels/get_all 持续返回),但应用树里没有它的节点(AppFunction)。` +
|
|
545
|
+
`**这是平台侧的不可删状态**:physically_delete 用 AppFunction id 定位,而平台 removeBizModel 找不到 AppFunction 行时抛 APP_FUNCTION_MODEL_NOTEXIST——本工具不猜测 id(传业务记录 id 只会得到 601003 业务模型不存在),因此无法通过 physically_delete 删除。` +
|
|
546
|
+
`已连续复查 4 次、间隔约 1s,记录仍在,因此不是读缓存滞后。` +
|
|
547
|
+
`已知成因:删除级联被中断(逐列异步 40-60s,中途失败会留下「记录在、节点没了」的残骸)。` +
|
|
548
|
+
`可行出路(按代价排序):① 用平台「回收站 → 物理删除」清理该残留(apppackage/physically_deleteAll 同一路径);` +
|
|
549
|
+
`② 换一个未占用的 schema code 继续(r11/r12 探针均采用此法);` +
|
|
550
|
+
`③ 若你有平台数据库权限,删除 h_biz_schema/h_biz_property/h_biz_sheet 对应行并重启 launcher 清缓存——但这是维护者操作,不在工具权限内。` +
|
|
551
|
+
`**不要再重复调用 model.delete / model.move / model.create**:三方都会给出同样的答复。`,
|
|
508
552
|
});
|
|
509
553
|
}
|
|
510
554
|
const targetId = node.id;
|
package/dist/lanes/rule-graph.js
CHANGED
|
@@ -1237,7 +1237,7 @@ export function validateDataAction(action, nodeCode, index) {
|
|
|
1237
1237
|
code: "rule-graph-aggregation-is-a-field-feature",
|
|
1238
1238
|
nodeCode,
|
|
1239
1239
|
key: `dataActions[${index}].ruleValueType`,
|
|
1240
|
-
message: `${nodeCode}: 规则节点不支持 AGGREGATION 赋值——平台会存进去并启用,但运行期聚合结果是集合(ArrayList),标量目标收到集合即 50000「服务异常」(DecimalBizField.fillClientContent)。**子表计数/求和/取最值回主表用字段计算规则**:目标字段建成普通数值字段,然后 \`omc compute --schema <主表> --target <目标字段> --formula 'COUNTIF({<子表code>.<子列code>},"异常")'\`(默认 dry-run,--apply 写入;等价 datarule.create dataRuleType:2,配方 \`field-compute-subtable-sum\`)。规则图里只做 FIXED/DYNAMIC/EXPRESSION
|
|
1240
|
+
message: `${nodeCode}: 规则节点不支持 AGGREGATION 赋值——平台会存进去并启用,但运行期聚合结果是集合(ArrayList),标量目标收到集合即 50000「服务异常」(DecimalBizField.fillClientContent)。**子表计数/求和/取最值回主表用字段计算规则**:目标字段建成普通数值字段,然后 \`omc compute --schema <主表> --target <目标字段> --formula 'COUNTIF({<子表code>.<子列code>},"异常")'\`(默认 dry-run,--apply 写入;等价 datarule.create dataRuleType:2,配方 \`field-compute-subtable-sum\`)。规则图里只做 FIXED/DYNAMIC/EXPRESSION 的直接赋值。该计算规则由**服务端**在 data.submit/data.update 时计算并回填(真机 1.1.25 复核),写完可直接 data.load 断言结果;注意它会**覆盖**你传入的同名字段值。来源:r11 巡检沙盒 2026-10-24(1.1.25)。`,
|
|
1241
1241
|
}];
|
|
1242
1242
|
if (!RULE_VALUE_TYPES.includes(valueType))
|
|
1243
1243
|
blockers.push({ code: "rule-graph-action-value-type", nodeCode, key: "ruleValueType", message: `${where}: ruleValueType "${valueType}" 不在 ${RULE_VALUE_TYPES.join("/")}` });
|
|
@@ -699,7 +699,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
699
699
|
...(replayToken ? { replayToken } : {}),
|
|
700
700
|
bizObject: { schemaCode: args.schemaCode, sheetCode, data: { ...formData, id: objectId, sequenceStatus: "DRAFT" } },
|
|
701
701
|
},
|
|
702
|
-
}), formData);
|
|
702
|
+
}), formData, args.workflowCode);
|
|
703
703
|
const vo = (performed.data ?? {});
|
|
704
704
|
const returnedId = typeof vo.objectId === "string" && vo.objectId ? vo.objectId : typeof vo.bizObjectId === "string" && vo.bizObjectId ? vo.bizObjectId : objectId;
|
|
705
705
|
const workflowInstanceId = typeof vo.workflowInstanceId === "string" ? vo.workflowInstanceId : null;
|
|
@@ -1087,7 +1087,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1087
1087
|
path: ENDPOINTS.runtimeFormSubmit,
|
|
1088
1088
|
method: "POST",
|
|
1089
1089
|
body: payload,
|
|
1090
|
-
}));
|
|
1090
|
+
}), undefined, hasWorkflowCode ? explicitWorkflowCode : undefined);
|
|
1091
1091
|
const performedData = (performed.data ?? {});
|
|
1092
1092
|
let readback = { formType };
|
|
1093
1093
|
if (formType === "2") {
|
|
@@ -174,6 +174,18 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
174
174
|
scope: WORKFLOW_SCOPE,
|
|
175
175
|
description: "删除工作流模板(`/api/api/workflow/delete`),回读确认缺失。",
|
|
176
176
|
}),
|
|
177
|
+
"workflow.permissions": Object.freeze({
|
|
178
|
+
scope: "workflow start permission (who may originate)",
|
|
179
|
+
description: "列出谁能发起该流程(`GET /api/api/workflow/list_permission?workflowCode=` → {id,name,unitType,fullName,corpId,code};1=部门 2=角色 3=用户)。**`data.submit` 报 40009 时先看这里**:PART_VISIABLE 且本表无命中行 ⇒ 除平台管理员(`h_perm_admin` 的 ADMIN/SYS_MNG,非登录 isAdmin)外无人能发起;发布流程不会自动建这一行。",
|
|
180
|
+
}),
|
|
181
|
+
"workflow.addPermission": Object.freeze({
|
|
182
|
+
scope: "workflow start permission (who may originate)",
|
|
183
|
+
description: "授予发起权限(`POST /api/api/workflow/create_permission`,body `{workflowCode,unitId,unitType,corpId}`;unitType 取枚举序号 1=部门 2=角色 3=用户)。unitId 用 org.department.list / org.role.list / org.user.search 取。重复授予按 402003 幂等跳过,写后回读确认。",
|
|
184
|
+
}),
|
|
185
|
+
"workflow.removePermission": Object.freeze({
|
|
186
|
+
scope: "workflow start permission (who may originate)",
|
|
187
|
+
description: "撤销发起权限(`POST /api/api/workflow/delete_permission?permissionId=`)。首选传 `permissionId`(`workflow.permissions` 的 id);也可传 `workflowCode`+`unitId`+`unitType`,工具解析该主体的显示名后匹配(`list_permission` 不回 unitId)。缺失即幂等成功。",
|
|
188
|
+
}),
|
|
177
189
|
"workflow.validate": Object.freeze({
|
|
178
190
|
scope: WORKFLOW_SCOPE,
|
|
179
191
|
description: "只读预检(`POST /api/api/workflow/validate`):合成草稿模板(或可选 `template` 按 merge 语义)后返回 ValidateMessage[],拆出 errors/warnings,`valid = 无 ERROR`。用于 publish 前先看平台会不会拒——平台对 publish 常回 errcode:0 但 ValidateMessage[] 里带 ERROR,本工具把它显式暴露,避免「publish 看似成功却没落库」。不写任何状态。",
|
|
@@ -223,6 +235,9 @@ export function assertWorkflowContract() {
|
|
|
223
235
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.publish", ["workflow/publish", "template"]);
|
|
224
236
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.setButtons", ["buttonPermission", "customBtns", "childTableBtns", "formButtonCodes", "form-button-absent", "publish:false"]);
|
|
225
237
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.bindRule", ["startEventHandler", "cancelEventHandler", "activateEventHandler", "endEventHandler", "bizActions", "bizActionType", "BIZ_RULE", "publish:false", "enabledRuleCodes", "workflow-bind-rule-absent", "activityCode", "nodeEvent", "endActivity", "activityApproved"]);
|
|
238
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.permissions", ["list_permission", "40009", "PART_VISIABLE"]);
|
|
239
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.addPermission", ["create_permission", "unitType", "unitId"]);
|
|
240
|
+
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.removePermission", ["delete_permission", "permissionId"]);
|
|
226
241
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.role.list", ["organization/role/list", "type", "Originator"]);
|
|
227
242
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.department.list", ["organization/department/tree", "parentId"]);
|
|
228
243
|
assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.user.search", ["organization/user/search", "wd", "type 3"]);
|
|
@@ -18,6 +18,9 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
|
|
|
18
18
|
claim("workflow.bindRule", T["workflow.bindRule"].scope),
|
|
19
19
|
claim("workflow.setButtons", T["workflow.setButtons"].scope),
|
|
20
20
|
claim("workflow.delete", T["workflow.delete"].scope),
|
|
21
|
+
claim("workflow.permissions", T["workflow.permissions"].scope),
|
|
22
|
+
claim("workflow.addPermission", T["workflow.addPermission"].scope),
|
|
23
|
+
claim("workflow.removePermission", T["workflow.removePermission"].scope),
|
|
21
24
|
claim("configure_workflow", T["configure_workflow"].scope),
|
|
22
25
|
claim("workflow.validate", T["workflow.validate"].scope),
|
|
23
26
|
// Organization writes (role group / role / members / management scope).
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -2724,6 +2724,194 @@ export function registerWorkflowLane(tools) {
|
|
|
2724
2724
|
};
|
|
2725
2725
|
}),
|
|
2726
2726
|
}));
|
|
2727
|
+
// ── 发起权限 (who may originate) ────────────────────────────────────────────
|
|
2728
|
+
//
|
|
2729
|
+
// 40009 流程发起权限不足 is the single most opaque refusal on this platform:
|
|
2730
|
+
// `data.submit` (and `data.save` with workflowCode) answers it, the message
|
|
2731
|
+
// names no permission, and there is no CLI face to inspect or grant one. r11
|
|
2732
|
+
// (2026-10-24) burned 7 submit attempts and could not deliver the whole
|
|
2733
|
+
// workflow section; the platform source shows the check is concrete:
|
|
2734
|
+
// `WorkflowAspect.validateWorkflowPermission` → template header must exist and
|
|
2735
|
+
// be published → `getWorkflowTemplateHeadersBySchemaCode(userId, schemaCode)`:
|
|
2736
|
+
// • the user's root department must resolve (`getAllParentIds` non-empty),
|
|
2737
|
+
// • `visibleType=ALL_VISIABLE` templates are open to everyone,
|
|
2738
|
+
// • otherwise a `h_workflow_permission` row must match the user, their
|
|
2739
|
+
// department (or any ancestor), or one of their roles,
|
|
2740
|
+
// • platform admins (`h_perm_admin` ADMIN/SYS_MNG) bypass all of it.
|
|
2741
|
+
// Publishing a flow creates NO permission row, so a freshly published flow is
|
|
2742
|
+
// unusable by anyone who is not a platform admin.
|
|
2743
|
+
const permissionRows = async (session, workflowCode) => wireRows((await wireCall(session, { path: ENDPOINTS.workflowListPermission, query: { workflowCode } })).data);
|
|
2744
|
+
// `list_permission` echoes the PERMISSION row id plus the unit's display name
|
|
2745
|
+
// and code — never the unitId (WorkflowTemplateController.getPermissionMap).
|
|
2746
|
+
// So identity for readback/removal is resolved from the unit's own face.
|
|
2747
|
+
const unitIdentity = async (session, unitType, unitId) => {
|
|
2748
|
+
try {
|
|
2749
|
+
if (unitType === 1) {
|
|
2750
|
+
const row = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentGet, query: { deptId: unitId } })).data;
|
|
2751
|
+
return { name: row ? String(row.name ?? "") || null : null, code: row ? (String(row.code ?? "") || null) : null };
|
|
2752
|
+
}
|
|
2753
|
+
if (unitType === 2) {
|
|
2754
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.organizationRoleList, query: { expandAll: "true" } })).data);
|
|
2755
|
+
const walk = (entries) => {
|
|
2756
|
+
for (const entry of entries) {
|
|
2757
|
+
if (String(entry.id ?? "") === unitId)
|
|
2758
|
+
return entry;
|
|
2759
|
+
const children = Array.isArray(entry.children) ? entry.children : [];
|
|
2760
|
+
const hit = walk(children);
|
|
2761
|
+
if (hit)
|
|
2762
|
+
return hit;
|
|
2763
|
+
}
|
|
2764
|
+
return undefined;
|
|
2765
|
+
};
|
|
2766
|
+
const hit = walk(rows);
|
|
2767
|
+
return { name: hit ? String(hit.name ?? "") || null : null, code: hit ? (String(hit.code ?? "") || null) : null };
|
|
2768
|
+
}
|
|
2769
|
+
const row = (await wireCall(session, { path: ENDPOINTS.organizationUserInfo, query: { id: unitId } })).data;
|
|
2770
|
+
return { name: row ? String(row.name ?? "") || null : null, code: row ? (String(row.username ?? "") || null) : null };
|
|
2771
|
+
}
|
|
2772
|
+
catch {
|
|
2773
|
+
return { name: null, code: null };
|
|
2774
|
+
}
|
|
2775
|
+
};
|
|
2776
|
+
tools.push(defineTool({
|
|
2777
|
+
name: "workflow.permissions",
|
|
2778
|
+
layer: "low",
|
|
2779
|
+
kind: "read",
|
|
2780
|
+
lane: WORKFLOW_LANE,
|
|
2781
|
+
scope: WORKFLOW_TOOLS["workflow.permissions"].scope,
|
|
2782
|
+
description: WORKFLOW_TOOLS["workflow.permissions"].description,
|
|
2783
|
+
inputSchema: {
|
|
2784
|
+
environment: environmentArg,
|
|
2785
|
+
workflowCode: z.string().optional().describe("流程 code;省略则用 schemaCode 解析"),
|
|
2786
|
+
schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型的流程"),
|
|
2787
|
+
capability: capabilityArg,
|
|
2788
|
+
},
|
|
2789
|
+
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
2790
|
+
const workflowCode = await resolveWorkflowArg(session, args);
|
|
2791
|
+
if (!workflowCode)
|
|
2792
|
+
return { schemaCode: args.schemaCode, exists: false, permissions: [], note: "该模型没有流程" };
|
|
2793
|
+
const rows = await permissionRows(session, workflowCode);
|
|
2794
|
+
const permissions = rows.map((row) => ({
|
|
2795
|
+
id: row.id ?? null,
|
|
2796
|
+
name: row.name ?? null,
|
|
2797
|
+
unitType: row.unitType ?? null,
|
|
2798
|
+
unitTypeName: row.unitType === 1 || row.unitType === "1" ? "部门" : row.unitType === 2 || row.unitType === "2" ? "角色" : row.unitType === 3 || row.unitType === "3" ? "用户" : null,
|
|
2799
|
+
fullName: row.fullName ?? null,
|
|
2800
|
+
corpId: row.corpId ?? null,
|
|
2801
|
+
code: row.code ?? null,
|
|
2802
|
+
}));
|
|
2803
|
+
return {
|
|
2804
|
+
workflowCode,
|
|
2805
|
+
count: permissions.length,
|
|
2806
|
+
permissions,
|
|
2807
|
+
...(permissions.length === 0
|
|
2808
|
+
? {
|
|
2809
|
+
blocker: "workflow-start-permission-empty",
|
|
2810
|
+
note: "该流程没有任何发起权限行:除平台管理员(h_perm_admin 里 ADMIN/SYS_MNG)外**没有人能发起**,data.submit 会回 40009「流程发起权限不足」。发布流程不会自动建这一行,用 workflow.addPermission 授给部门/角色/用户(或把模板 visibleType 改成 ALL_VISIABLE)。",
|
|
2811
|
+
nextCommand: `omc call workflow.addPermission --env <env> --args '{"workflowCode":"${workflowCode}","unitId":"<部门|角色|用户 id>","unitType":1}'`,
|
|
2812
|
+
}
|
|
2813
|
+
: {}),
|
|
2814
|
+
};
|
|
2815
|
+
}),
|
|
2816
|
+
}));
|
|
2817
|
+
writeTool(tools, "workflow.addPermission", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.addPermission"].scope, WORKFLOW_TOOLS["workflow.addPermission"].description, {
|
|
2818
|
+
workflowCode: z.string().optional().describe("流程 code;省略则用 schemaCode 解析"),
|
|
2819
|
+
schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型的流程"),
|
|
2820
|
+
unitId: z.string().min(1).describe("部门 id / 角色 id / 用户 id(看 unitType)"),
|
|
2821
|
+
unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).describe("1=部门 2=角色 3=用户;也接受 DEPARTMENT/ROLE/USER"),
|
|
2822
|
+
corpId: z.string().optional().describe("企业 id;省略则用当前会话企业"),
|
|
2823
|
+
}, async (session, args) => {
|
|
2824
|
+
const workflowCode = await resolveWorkflowArg(session, args);
|
|
2825
|
+
if (!workflowCode)
|
|
2826
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-not-found", message: `模型 ${String(args.schemaCode ?? "")} 没有流程;先 workflow.create` });
|
|
2827
|
+
const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
|
|
2828
|
+
// Idempotent retry: the platform answers 402003 WORKFLOW_PERMISSION_EXIST
|
|
2829
|
+
// for a duplicate unitId, which would read as a failure. `list_permission`
|
|
2830
|
+
// does NOT echo unitId (only id/name/unitType/fullName/code), so identity
|
|
2831
|
+
// is decided by the platform's own refusal rather than a guessed pre-read.
|
|
2832
|
+
const before = await permissionRows(session, workflowCode);
|
|
2833
|
+
try {
|
|
2834
|
+
// Send the unit's own name/code too: `list_permission` reads them back
|
|
2835
|
+
// (it echoes `name`/`code`), which is the only way `workflow.permissions`
|
|
2836
|
+
// can describe a row by more than its opaque permission id.
|
|
2837
|
+
const identity = await unitIdentity(session, unitType, args.unitId);
|
|
2838
|
+
await wireCall(session, {
|
|
2839
|
+
path: ENDPOINTS.workflowCreatePermission,
|
|
2840
|
+
method: "POST",
|
|
2841
|
+
body: {
|
|
2842
|
+
workflowCode,
|
|
2843
|
+
unitId: args.unitId,
|
|
2844
|
+
unitType,
|
|
2845
|
+
corpId: args.corpId ?? session.enterpriseId(),
|
|
2846
|
+
...(identity.name ? { name: identity.name } : {}),
|
|
2847
|
+
...(identity.code ? { code: identity.code } : {}),
|
|
2848
|
+
},
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
catch (error) {
|
|
2852
|
+
const errcode = error.details?.errcode ?? error.errcode;
|
|
2853
|
+
if (String(errcode ?? "") !== "402003")
|
|
2854
|
+
throw new ToolRefusalError({
|
|
2855
|
+
status: "refused",
|
|
2856
|
+
blocker: "workflow-permission-write-failed",
|
|
2857
|
+
workflowCode,
|
|
2858
|
+
unitId: args.unitId,
|
|
2859
|
+
unitType,
|
|
2860
|
+
message: `create_permission 被平台拒绝(err=${String(errcode ?? "n/a")})。核对 unitId 是否真实存在(部门用 org.department.list、角色用 org.role.list、用户用 org.user.search),以及发起权限是否被企业策略禁用。原始错误:${error instanceof Error ? error.message : String(error)}`,
|
|
2861
|
+
});
|
|
2862
|
+
return {
|
|
2863
|
+
performed: { skipped: "already-present" },
|
|
2864
|
+
readback: { workflowCode, alreadyPresent: true, unitId: args.unitId, unitType, count: before.length, note: "该主体已有发起权限(平台 402003),未重复写入" },
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
const after = await permissionRows(session, workflowCode);
|
|
2868
|
+
const created = after.find((row) => !before.some((old) => String(old.id ?? "") === String(row.id ?? ""))) ?? after[after.length - 1];
|
|
2869
|
+
return {
|
|
2870
|
+
performed: { errcode: 0 },
|
|
2871
|
+
readback: {
|
|
2872
|
+
workflowCode,
|
|
2873
|
+
count: after.length,
|
|
2874
|
+
permissionId: created?.id ?? null,
|
|
2875
|
+
unitType: created?.unitType ?? unitType,
|
|
2876
|
+
name: created?.name ?? null,
|
|
2877
|
+
note: "发起权限已授予。若之前 data.submit 回 40009,现在应可发起;仍失败先 workflow.permissions 确认行存在,并核对发起人所属部门/角色。",
|
|
2878
|
+
},
|
|
2879
|
+
};
|
|
2880
|
+
});
|
|
2881
|
+
writeTool(tools, "workflow.removePermission", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.removePermission"].scope, WORKFLOW_TOOLS["workflow.removePermission"].description, {
|
|
2882
|
+
permissionId: z.string().min(1).optional().describe("首选:workflow.permissions 返回的 id(唯一确定)"),
|
|
2883
|
+
workflowCode: z.string().optional().describe("配合 unitId+unitType 使用(平台的 list_permission 不回 unitId,需按显示名解析)"),
|
|
2884
|
+
unitId: z.string().min(1).optional().describe("部门/角色/用户 id;按 unitType 解析显示名后匹配权限行"),
|
|
2885
|
+
unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).optional().describe("unitId 的类型:1=部门 2=角色 3=用户"),
|
|
2886
|
+
schemaCode: z.string().optional(),
|
|
2887
|
+
}, async (session, args) => {
|
|
2888
|
+
let permissionId = args.permissionId;
|
|
2889
|
+
if (!permissionId) {
|
|
2890
|
+
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode });
|
|
2891
|
+
if (!workflowCode || !args.unitId || args.unitType === undefined)
|
|
2892
|
+
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "workflow.removePermission", message: "需要 permissionId(推荐),或 workflowCode(/schemaCode)+ unitId + unitType" });
|
|
2893
|
+
const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
|
|
2894
|
+
const identity = await unitIdentity(session, unitType, args.unitId);
|
|
2895
|
+
const rows = await permissionRows(session, workflowCode);
|
|
2896
|
+
const wantedType = String(unitType);
|
|
2897
|
+
const hit = rows.find((row) => String(row.unitType ?? "") === wantedType && (String(row.id ?? "") === args.unitId ||
|
|
2898
|
+
(identity.name !== null && String(row.name ?? "") === identity.name) ||
|
|
2899
|
+
(identity.code !== null && String(row.code ?? "") === identity.code)));
|
|
2900
|
+
if (!hit)
|
|
2901
|
+
return {
|
|
2902
|
+
performed: { skipped: "absent" },
|
|
2903
|
+
readback: {
|
|
2904
|
+
workflowCode,
|
|
2905
|
+
removed: false,
|
|
2906
|
+
note: `没有匹配该主体的权限行(幂等)。注意 list_permission 不回 unitId,匹配靠解析出的显示名「${identity.name ?? "?"}」;不确定时用 workflow.permissions 拿 id 再按 permissionId 删。`,
|
|
2907
|
+
permissions: rows.map((row) => ({ id: row.id ?? null, name: row.name ?? null, unitType: row.unitType ?? null })),
|
|
2908
|
+
},
|
|
2909
|
+
};
|
|
2910
|
+
permissionId = String(hit.id ?? "");
|
|
2911
|
+
}
|
|
2912
|
+
await wireCall(session, { path: ENDPOINTS.workflowDeletePermission, method: "POST", query: { permissionId } });
|
|
2913
|
+
return { performed: { errcode: 0 }, readback: { permissionId, removed: true } };
|
|
2914
|
+
});
|
|
2727
2915
|
tools.push(defineTool({
|
|
2728
2916
|
name: "org.role.list",
|
|
2729
2917
|
layer: "low",
|
package/dist/recipes/datarule.js
CHANGED
|
@@ -54,7 +54,7 @@ const dataruleCalcRule = Object.freeze({
|
|
|
54
54
|
},
|
|
55
55
|
name: "c=a+b",
|
|
56
56
|
},
|
|
57
|
-
preconditions: ["
|
|
57
|
+
preconditions: ["**拼装器优先**:简单表达式用 `omc compute --formula '{a}*0.4+{b}*0.3'`;**条件聚合**用 `omc compute --formula '{<子表>.<子列>}' --where '<子列> = <值>'`(自动编译 COUNTIF/SUMIF)——两者都默认 dry-run、`--apply` 写入。本配方仅用于拼装器表达不了的条件分支等复杂场景。",
|
|
58
58
|
"**计算类需求:建普通业务字段(数值优先,文本亦可)+ 本计算规则;不要用「计算公式」字段类型(FORMULA)**——平台约定是普通字段顺带配计算规则(真机 2026-10-08 复核:普通数值字段 + type2 计算规则,门户算出 7.00,设计器「计算规则」弹窗显示公式)。",
|
|
59
59
|
"参与计算的字段已发布",
|
|
60
60
|
],
|
|
@@ -95,7 +95,7 @@ const dataruleSubtableSum = Object.freeze({
|
|
|
95
95
|
},
|
|
96
96
|
name: "合计=明细金额之和",
|
|
97
97
|
},
|
|
98
|
-
preconditions: ["
|
|
98
|
+
preconditions: ["**拼装器优先**:简单表达式用 `omc compute --formula '{a}*0.4+{b}*0.3'`;**条件聚合**用 `omc compute --formula '{<子表>.<子列>}' --where '<子列> = <值>'`(自动编译 COUNTIF/SUMIF)——两者都默认 dry-run、`--apply` 写入。本配方仅用于拼装器表达不了的条件分支等复杂场景。",
|
|
99
99
|
"**目标 `total` 是普通数值字段(不是「计算公式」字段)**;`<childSchemaCode>` 是子表字段的 code(如 `prop_repair_detail` / `Sub_item_information_table`),`amount` 是其子列 code",
|
|
100
100
|
"子表与其列已发布",
|
|
101
101
|
],
|
|
@@ -131,7 +131,7 @@ const dataruleChildColumn = Object.freeze({
|
|
|
131
131
|
},
|
|
132
132
|
name: "金额=数量×单价",
|
|
133
133
|
},
|
|
134
|
-
preconditions: ["
|
|
134
|
+
preconditions: ["**拼装器优先**:简单表达式用 `omc compute --formula '{a}*0.4+{b}*0.3'`;**条件聚合**用 `omc compute --formula '{<子表>.<子列>}' --where '<子列> = <值>'`(自动编译 COUNTIF/SUMIF)——两者都默认 dry-run、`--apply` 写入。本配方仅用于拼装器表达不了的条件分支等复杂场景。",
|
|
135
135
|
"**子表列 `amount` 是普通数值字段(不是「计算公式」字段)**,且是子表内的列(schemaCode=子表字段 code)"],
|
|
136
136
|
conditionalRequirements: [
|
|
137
137
|
"**子表内列引用用 `{子表schemaCode.子列code}` 点号全限定**(真机 2026-10-08 设计器产出:`{prop_repair_detail.qty}*{prop_repair_detail.unit_price}`);不要写裸 `{qty}`。",
|
package/dist/runtime-support.js
CHANGED
|
@@ -54,11 +54,17 @@ export async function mapUnpublishedAppFailure(session, schemaCode, errcode, roo
|
|
|
54
54
|
* unpublished-app failure. Used by data.save/data.submit (errcode 50000) and
|
|
55
55
|
* datarule.create type2 (errcode 305003) so the try/catch wiring lives once.
|
|
56
56
|
*/
|
|
57
|
-
export async function runtimeWriteWithAppMapping(session, schemaCode, errcode, rootCause, call, formData) {
|
|
57
|
+
export async function runtimeWriteWithAppMapping(session, schemaCode, errcode, rootCause, call, formData, workflowCode) {
|
|
58
58
|
try {
|
|
59
59
|
return await call();
|
|
60
60
|
}
|
|
61
61
|
catch (error) {
|
|
62
|
+
// 40009 is diagnosed FIRST: it names a cause the app/relation mappings
|
|
63
|
+
// cannot see, and it is the one refusal whose fix is a concrete next
|
|
64
|
+
// command (r11: seven blind submit attempts). Guarded by the errcode so
|
|
65
|
+
// every other failure keeps its existing mapping.
|
|
66
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 40009)
|
|
67
|
+
await diagnoseWorkflowStartPermission(session, schemaCode, workflowCode, error);
|
|
62
68
|
let mapped;
|
|
63
69
|
try {
|
|
64
70
|
mapped = await mapUnpublishedAppFailure(session, schemaCode, errcode, rootCause, error);
|
|
@@ -131,4 +137,89 @@ export async function diagnoseRelationShapeFailure(session, schemaCode, errcode,
|
|
|
131
137
|
message: `errcode ${errcode}——根因:单选关联字段 ${culprits.join(", ")} 传了数组(WORK_SHEET/propertyType 9 只接受 id 字符串或 {id};数组会稳定 ${errcode})。多选关联(11)与人员/组织(50/51)才用数组`,
|
|
132
138
|
});
|
|
133
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* 40009 流程发起权限不足 — the most opaque refusal on this platform.
|
|
142
|
+
*
|
|
143
|
+
* r11 (2026-10-24, 1.1.25) could not deliver an entire workflow section: seven
|
|
144
|
+
* `data.submit` attempts all answered the same two words, and neither the
|
|
145
|
+
* message nor any CLI face said WHY. The platform source
|
|
146
|
+
* (`WorkflowAspect.validateWorkflowPermission` →
|
|
147
|
+
* `WorkflowManagementFacadeImpl.getWorkflowTemplateHeadersBySchemaCode`) shows
|
|
148
|
+
* three independent causes, all of which produce the identical code:
|
|
149
|
+
*
|
|
150
|
+
* 1. `h_workflow_template` header missing / not published;
|
|
151
|
+
* 2. the caller's root department does not resolve (`getAllParentIds` empty →
|
|
152
|
+
* the allowed-template list is short-circuited to EMPTY, so a user whose
|
|
153
|
+
* department row was deleted/moved can never start anything);
|
|
154
|
+
* 3. no `h_workflow_permission` row matching the user, their department (or an
|
|
155
|
+
* ancestor), or one of their roles — and `visibleType != ALL_VISIABLE`.
|
|
156
|
+
* Publishing a flow creates NO permission row, so a freshly published flow
|
|
157
|
+
* is unusable by everyone except a `h_perm_admin` ADMIN/SYS_MNG.
|
|
158
|
+
*
|
|
159
|
+
* The diagnosis here reads only the cheap, authoritative faces (permission list
|
|
160
|
+
* + header presence) and names the recovery command instead of leaving the
|
|
161
|
+
* caller to guess. Anything that fails is swallowed: a diagnosis must never
|
|
162
|
+
* replace the original refusal.
|
|
163
|
+
*/
|
|
164
|
+
export async function diagnoseWorkflowStartPermission(session, schemaCode, workflowCode, error) {
|
|
165
|
+
if (!(error instanceof WireError && error.kind === "business" && error.errcode === 40009))
|
|
166
|
+
throw error;
|
|
167
|
+
const resolvedCode = workflowCode ?? (await resolvePublishedWorkflowCodeQuiet(session, schemaCode));
|
|
168
|
+
if (!resolvedCode)
|
|
169
|
+
throw error;
|
|
170
|
+
let permissions = [];
|
|
171
|
+
let headerPresent = true;
|
|
172
|
+
try {
|
|
173
|
+
headerPresent = (await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode: resolvedCode } })).data != null;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
headerPresent = true;
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
permissions = wireRows((await wireCall(session, { path: ENDPOINTS.workflowListPermission, query: { workflowCode: resolvedCode } })).data);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
permissions = [];
|
|
183
|
+
}
|
|
184
|
+
const headerHint = headerPresent ? "" : ` 流程头 ${resolvedCode} 不存在或未发布(先 workflow.publish)。`;
|
|
185
|
+
const message = permissions.length === 0
|
|
186
|
+
? `流程 ${resolvedCode} 没有任何发起权限行。§ 发布流程不会自动建发起权限§:` +
|
|
187
|
+
`除平台管理员(平台 h_perm_admin 里的 ADMIN/SYS_MNG,注意这与登录接口返回的 isAdmin 不是一回事)外,任何人都无法发起,` +
|
|
188
|
+
`data.submit / data.save+workflowCode 一律回 40009。${headerHint}` +
|
|
189
|
+
` 修复:把发起权限授给一个部门/角色/用户 —— ` +
|
|
190
|
+
`omc call workflow.addPermission --env <env> --args '{"workflowCode":"${resolvedCode}","unitId":"<org.department.list|org.role.list|org.user.search 的 id>","unitType":1}'` +
|
|
191
|
+
`(1=部门 2=角色 3=用户;同一行也可用 workflow.permissions 复核)。` +
|
|
192
|
+
`另一种原因同样报 40009 且无法从权限行看出来:发起人所属部门记录已被删除/移动(平台 getRootDeptId 解析为空时直接短路成"无任何可发起模板")——` +
|
|
193
|
+
`用 org.user.search 看自己的 departmentId,再用 org.department.list 确认该部门仍在。`
|
|
194
|
+
: `流程 ${resolvedCode} 已有 ${permissions.length} 条发起权限行(${permissions.map((row) => `${String(row.name ?? row.code ?? "?")}/unitType=${String(row.unitType ?? "?")}`).join("、")}),` +
|
|
195
|
+
`但当前调用者不在其中。${headerHint}` +
|
|
196
|
+
` 核对顺序:① 发起人的 departmentId(org.user.search)是否等于某条 unitType=1 的部门,或是其祖先链上的一级;` +
|
|
197
|
+
`② 发起人是否属于某条 unitType=2 的角色(org.role.users);③ 是否该 unitType=3 的用户本人;` +
|
|
198
|
+
`④ 若部门记录已被删除/移动,平台会把可发起列表短路成空——同样报 40009,但与权限行无关。` +
|
|
199
|
+
` 补授权:omc call workflow.addPermission --env <env> --args '{"workflowCode":"${resolvedCode}","unitId":"<id>","unitType":1}';查看现状用 omc call workflow.permissions。`;
|
|
200
|
+
throw new ToolRefusalError({
|
|
201
|
+
status: "refused",
|
|
202
|
+
blocker: "workflow-start-permission-denied",
|
|
203
|
+
schemaCode,
|
|
204
|
+
workflowCode: resolvedCode,
|
|
205
|
+
errcode: 40009,
|
|
206
|
+
permissionCount: permissions.length,
|
|
207
|
+
permissions,
|
|
208
|
+
message,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
/** The published workflow code for a schema, or undefined (never throws). */
|
|
212
|
+
async function resolvePublishedWorkflowCodeQuiet(session, schemaCode) {
|
|
213
|
+
try {
|
|
214
|
+
const isWorkflow = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode } })).data === true;
|
|
215
|
+
if (!isWorkflow)
|
|
216
|
+
return undefined;
|
|
217
|
+
const template = (await wireCall(session, { path: ENDPOINTS.workflowGetBySchema, query: { schemaCode } })).data;
|
|
218
|
+
const code = template && typeof template.workflowCode === "string" && template.workflowCode.length > 0 ? template.workflowCode : null;
|
|
219
|
+
return code ?? undefined;
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return undefined;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
134
225
|
//# sourceMappingURL=runtime-support.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.83",
|
|
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.83",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.83",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -91,8 +91,8 @@ only the section for the capability you need —
|
|
|
91
91
|
- **边界 / 陷阱**:
|
|
92
92
|
- **计算类需求默认走「普通字段 + 计算规则」,不要建「计算公式」字段类型(FORMULA)**:建一个**数值/单行文本**业务字段,再配一条**字段级计算规则**(`datarule.create` `dataRuleType:2`,`options.shortTextStitch={"formula","editorText","editorMark"}`)。理由:平台约定用正常业务字段承载、顺带配计算规则(真机 2026-10-08 production 表单核验 + 设计器弹窗显示)。「计算公式」字段类型不是默认,仅当确需一列专用公式时才用。
|
|
93
93
|
- **计算规则公式引用**:同类字段裸 `{字段code}`(`{a}+{b}`);**子表列用 `{子表字段code.子列code}`**(`{Sub_item_information_table.taxInclusiveAmount}`);子表求和 `SUM({子表.子列})`;支持 `IF(cond,a,b)`、`CASE(v,c1,r1,c2,r2)`、`VALUE()`、`AND`、比较 `=`、字符串字面量与运算符空格。recipe:`omc describe datarule.create --example field-compute-rule|field-compute-subtable-sum|field-compute-child-column`。
|
|
94
|
-
-
|
|
95
|
-
-
|
|
94
|
+
- 计算规则(`dataRuleType:2`)**由服务端在 `data.submit`/`data.update` 时计算并回填,且覆盖传入值**(真机 1.1.25,2026-10-24 复核;对照组:删规则后传入值生效)。可直接 `data.load` 读回验证,无需前台人工确认。
|
|
95
|
+
- 只有 `FORMULA`(计算公式,`propertyType:100`)字段是**门户前端**计算、API 不回填(旧证据 2026-09-25/10-07,仅覆盖该字段类型)。
|
|
96
96
|
- 子表列支持标量族 + WORK_SHEET 关联列(需 `relativeCode`+options);**嵌套子表 / MULT_WORK_SHEET / RELEVANCE_DATA 不覆盖**。
|
|
97
97
|
|
|
98
98
|
## 默认表单 `form`
|
|
@@ -148,7 +148,7 @@ only the section for the capability you need —
|
|
|
148
148
|
## 流程模板 `workflow`
|
|
149
149
|
|
|
150
150
|
- **能做哪些事**:流程模板 get_draft/get_published、create、update、publish、bindRule(事件处理器规则绑定)、delete、workitem.list、workflow.instanceActivity(实例节点状态诊断);参与人用组织对象。
|
|
151
|
-
- **工具**:`workflow.get_draft`、`workflow.setButtons`(节点「表单操作」按钮勾选)、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`configure_workflow`、`workitem.list`、`workflow.instanceActivity`、`org.role.list`、`org.department.list`、`org.user.search`、`org.role.users`、`org.role.userScope`、`org.roleGroup.create`、`org.role.create`、`org.role.update`、`org.role.addUsers`、`org.role.removeUsers`、`org.role.setUserScope`、`org.department.create`、`org.department.update`、`org.department.delete`、`org.department.users`、`org.user.create`、`org.user.update`、`org.user.delete`、`org.user.info`、`org.user.departments`、`org.role.delete`、`org.roleGroup.delete`、`org.user.transferDepartment`
|
|
151
|
+
- **工具**:`workflow.get_draft`、`workflow.setButtons`(节点「表单操作」按钮勾选)、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`workflow.permissions`、`workflow.addPermission`、`workflow.removePermission`(发起权限:发布流程不会自动建这一行,缺它一律 40009)、`configure_workflow`、`workitem.list`、`workflow.instanceActivity`、`org.role.list`、`org.department.list`、`org.user.search`、`org.role.users`、`org.role.userScope`、`org.roleGroup.create`、`org.role.create`、`org.role.update`、`org.role.addUsers`、`org.role.removeUsers`、`org.role.setUserScope`、`org.department.create`、`org.department.update`、`org.department.delete`、`org.department.users`、`org.user.create`、`org.user.update`、`org.user.delete`、`org.user.info`、`org.user.departments`、`org.role.delete`、`org.roleGroup.delete`、`org.user.transferDepartment`
|
|
152
152
|
- **recipe**:`omc describe workflow.bindRule --example workflow-handler-binding`
|
|
153
153
|
- **边界 / 陷阱**:greenfield 首版;`publish`/`bindRule` 不得在 `ValidateMessage[]` 拒绝时报告成功。运行时 workitem(提交/审批)走 `data.submit`;想知道实例停在哪/是否错派,用 `workflow.instanceActivity`(无 status=1=真停,参与人全是发起人=解析为空退回)。
|
|
154
154
|
|
|
@@ -45,11 +45,12 @@ Coverage status and boundaries for every capability (including 权限/子表/聚
|
|
|
45
45
|
上线/内联/OCR) live in **`omc-capabilities`** — its 能力索引 table and
|
|
46
46
|
[边界] section are the authority; read the relevant section there rather than a
|
|
47
47
|
copy here. In particular, **跨模型/子表聚合、汇总、求和是配置能力**,不要当二开。
|
|
48
|
-
|
|
48
|
+
落点是**字段计算规则**:条件计数/求和用拼装器的 `--where`——
|
|
49
|
+
`omc compute --schema <主表> --target <目标字段> --formula '{<子表code>.<子列code>}' --where "<子列code> = <值>"`(编译为 COUNTIF/SUMIF;等价手写 `--formula 'COUNTIF({<子表code>.<子列code>},"异常")'`)
|
|
49
50
|
(等价 `datarule.create` `dataRuleType:2`,配方 `field-compute-subtable-sum` / `field-compute-child-column`)。
|
|
50
51
|
**不要**在规则图(`rule.save`)里用 `ruleValueType:"AGGREGATION"` —— 平台会存进去并启用,运行期聚合结果是集合,
|
|
51
52
|
标量目标收到集合即 50000「服务异常」(r11 巡检沙盒 2026-10-24,1.1.25;OMC 现已写前 fail-closed)。
|
|
52
|
-
|
|
53
|
+
字段计算规则(`dataRuleType:2`)由**服务端**在 `data.submit`/`data.update` 时计算并回填(且覆盖传入值,真机 1.1.25 复核):写完直接 `data.load` 读目标字段即可验证。只有 `FORMULA`(100) 字段是前端计算、API 不回填。
|
|
53
54
|
|
|
54
55
|
Cue → capability is a starting point, never a substitute for 定面. A cue that
|
|
55
56
|
could name two faces is resolved by the 定面表 in the parent skill.
|
|
@@ -97,9 +97,13 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
97
97
|
|
|
98
98
|
## 计算规则(datarule)的取值时机
|
|
99
99
|
|
|
100
|
-
-
|
|
101
|
-
|
|
102
|
-
-
|
|
100
|
+
- **字段计算规则(`datarule.create` `dataRuleType:2`)由服务端在写入时计算并回填**(真机 1.1.25 复核 2026-10-24):
|
|
101
|
+
`data.submit` 与 `data.update` 都会算,且**计算值覆盖调用方传入的值**。
|
|
102
|
+
- 实测:`COUNTIF({子表.checkResult},"异常")` → submit 4 行(3 异常)回读 `3`;update 传 `9` 回读仍是重算的 `2`;删除该规则后同一 update 传 `7` 则回读 `7`(对照组)。
|
|
103
|
+
- 所以「计算规则是否生效」**可以用 API 判定**:写一条带子表的数据后 `data.load` 读目标字段。
|
|
104
|
+
- 服务端聚合(子表计数/求和/回主表)**不需要二开**,用普通字段 + type2 计算规则即可参与流程分流、报表、后续 update。
|
|
105
|
+
- **区别:`FORMULA`(计算公式,`propertyType:100`)字段仍只在前端算** —— 旧证据(2026-09-25 / 2026-10-07)只覆盖该字段类型,API 写入不回填。计算类需求默认不要建 FORMULA 字段(见 `omc-capabilities` §字段)。
|
|
106
|
+
- ⚠️ **计算字段不要靠 `data.update` 主动赋值**:规则在时会重算并丢弃你传的值。要让某字段取特定值,先确认它没有计算规则(或删规则),再写。
|
|
103
107
|
|
|
104
108
|
## 规则与业务集成
|
|
105
109
|
|