@stalfh233/omc-cli 0.4.53 → 0.4.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approval-flow.js +5 -4
- package/dist/args.js +14 -0
- package/dist/cli-output.js +15 -2
- package/dist/cli.js +11 -13
- package/dist/code-rules.js +17 -0
- package/dist/compute.js +2 -4
- package/dist/form-default.js +2 -4
- package/dist/lanes/app.js +4 -0
- package/dist/lanes/b2-registration-contract.js +2 -2
- package/dist/lanes/businessrule.js +1 -1
- package/dist/lanes/model-contract.js +2 -2
- package/dist/lanes/model.js +50 -2
- package/dist/lanes/report.js +12 -0
- package/dist/lanes/runtime-data.js +5 -0
- package/dist/lanes/workflow-contract.js +1 -1
- package/dist/lanes/workflow.js +92 -19
- package/dist/rule-when.js +5 -4
- package/dist/session.js +26 -3
- package/dist/write.js +23 -2
- package/package.json +3 -3
package/dist/approval-flow.js
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
import { runToolCall } from "./call.js";
|
|
20
20
|
import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contract.js";
|
|
21
21
|
import { writeEnvelope } from "./cli-output.js";
|
|
22
|
+
import { argvValue } from "./args.js";
|
|
22
23
|
/** 解析步骤 DSL 为 {name, participant}(导出供测试)。 */
|
|
23
24
|
export function parseStep(raw) {
|
|
24
25
|
const match = /^(.+?)@(role|user):([A-Za-z0-9_.:-]+)$/.exec(raw.trim());
|
|
@@ -55,15 +56,15 @@ export function composeApprovalTemplate(options) {
|
|
|
55
56
|
}
|
|
56
57
|
export async function cmdApprovalFlow(args) {
|
|
57
58
|
const request = requestId();
|
|
58
|
-
const flag = (name) =>
|
|
59
|
-
const index = args.indexOf(name);
|
|
60
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
61
|
-
};
|
|
59
|
+
const flag = (name) => argvValue(args, name);
|
|
62
60
|
const multi = (name) => {
|
|
63
61
|
const values = [];
|
|
62
|
+
const prefix = `${name}=`;
|
|
64
63
|
for (let i = 0; i < args.length; i++) {
|
|
65
64
|
if (args[i] === name && args[i + 1] !== undefined)
|
|
66
65
|
values.push(args[i + 1]);
|
|
66
|
+
else if (args[i] !== undefined && args[i].startsWith(prefix))
|
|
67
|
+
values.push(args[i].slice(prefix.length));
|
|
67
68
|
}
|
|
68
69
|
return values;
|
|
69
70
|
};
|
package/dist/args.js
CHANGED
|
@@ -22,4 +22,18 @@ export const confirmProductionArg = z
|
|
|
22
22
|
* review (omc-config-draft-review).
|
|
23
23
|
*/
|
|
24
24
|
export const WRITE_PROTOCOL = "写入由环境 writable 决定(只读环境拒写);生产环境的写必须带 confirmProduction:true(高危确认,记入审计);写前仍走对话层草稿确认(omc-config-draft-review)。";
|
|
25
|
+
/**
|
|
26
|
+
* CLI argv flag reader. Accepts BOTH `--name value` and `--name=value` — a
|
|
27
|
+
* sandbox probe (r4, 2026-10-17) passed `--steps='审批@role:x'` and the
|
|
28
|
+
* separate-token-only parser answered "至少一个 --steps 审批步骤", pushing the
|
|
29
|
+
* agent to bypass the composer and hand-build the workflow.
|
|
30
|
+
*/
|
|
31
|
+
export function argvValue(args, name) {
|
|
32
|
+
const prefix = `${name}=`;
|
|
33
|
+
const inline = args.find((token) => token.startsWith(prefix));
|
|
34
|
+
if (inline !== undefined)
|
|
35
|
+
return inline.slice(prefix.length);
|
|
36
|
+
const index = args.indexOf(name);
|
|
37
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
38
|
+
}
|
|
25
39
|
//# sourceMappingURL=args.js.map
|
package/dist/cli-output.js
CHANGED
|
@@ -26,9 +26,22 @@ export function configureOutput(argv) {
|
|
|
26
26
|
* are consumed once here instead of being declared on every subcommand.
|
|
27
27
|
*/
|
|
28
28
|
export function stripOutputFlags(argv) {
|
|
29
|
+
// Normalize `--flag=value` into two tokens BEFORE commander parses: the
|
|
30
|
+
// dispatcher's variadic options (`--steps`, `--when`, `--then`) drop the
|
|
31
|
+
// equals form (a real sandbox call `--steps=x` answered "至少一个 --steps
|
|
32
|
+
// 审批步骤"), and every other subcommand gets the same courtesy for free.
|
|
33
|
+
const normalized = [];
|
|
34
|
+
for (const token of argv) {
|
|
35
|
+
if (token.startsWith("--") && token.includes("=")) {
|
|
36
|
+
const at = token.indexOf("=");
|
|
37
|
+
normalized.push(token.slice(0, at), token.slice(at + 1));
|
|
38
|
+
}
|
|
39
|
+
else
|
|
40
|
+
normalized.push(token);
|
|
41
|
+
}
|
|
29
42
|
const out = [];
|
|
30
|
-
for (let index = 0; index <
|
|
31
|
-
const token =
|
|
43
|
+
for (let index = 0; index < normalized.length; index += 1) {
|
|
44
|
+
const token = normalized[index];
|
|
32
45
|
if (token === "--quiet" || token === "--verbose")
|
|
33
46
|
continue;
|
|
34
47
|
if (token === "--out") {
|
package/dist/cli.js
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// T04:SQLite 警告过滤器必须保持第一个 import(先于任何拉起 omc-core/node:sqlite 的模块)。
|
|
3
3
|
import "./sqlite-warning-filter.js";
|
|
4
|
+
import { argvValue } from "./args.js";
|
|
4
5
|
import { spawnSync } from "node:child_process";
|
|
5
6
|
import { createInterface } from "node:readline/promises";
|
|
6
7
|
import { stdin, argv } from "node:process";
|
|
7
8
|
import { createRequire } from "node:module";
|
|
8
|
-
import { Command, CommanderError } from "commander";
|
|
9
|
+
import { Command, CommanderError, Option } from "commander";
|
|
9
10
|
import { ConfigStore, defaultPaths, removedProfileKeys } from "./config-store.js";
|
|
10
11
|
import { loadCoverageManifest, versionFamilyNotice } from "./coverage.js";
|
|
11
12
|
import { redactText } from "./redact.js";
|
|
@@ -64,10 +65,7 @@ async function askSecret(question) {
|
|
|
64
65
|
});
|
|
65
66
|
}
|
|
66
67
|
async function cmdSetup(store, args) {
|
|
67
|
-
const flag = (name) =>
|
|
68
|
-
const index = args.indexOf(name);
|
|
69
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
70
|
-
};
|
|
68
|
+
const flag = (name) => argvValue(args, name);
|
|
71
69
|
if (args.includes("--list")) {
|
|
72
70
|
for (const { name, profile } of store.listEnvironments())
|
|
73
71
|
console.error(`${name}${profile.isDefault ? "(默认)" : ""} [${profile.production ? "生产环境" : "测试环境"},${environmentWritable(profile) ? "允许写入" : "只读"}${profile.production ? "(生产写需逐笔 confirmProduction)" : ""}${removedProfileKeys(profile).length > 0 ? `,⚠ 含已移除键:${removedProfileKeys(profile).join("、")}` : ""}] -> ${profile.baseUrl} 应用=${profile.applicationCode}${profile.engineCode ? ` 租户=${profile.engineCode}` : ""}`);
|
|
@@ -233,8 +231,7 @@ function doctorAnchor() {
|
|
|
233
231
|
].join("\n");
|
|
234
232
|
}
|
|
235
233
|
function flag0(args, name) {
|
|
236
|
-
|
|
237
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
234
|
+
return argvValue(args, name);
|
|
238
235
|
}
|
|
239
236
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
240
237
|
async function cmdCall(store, args) {
|
|
@@ -255,10 +252,7 @@ async function cmdCall(store, args) {
|
|
|
255
252
|
writeEnvelope(envelope(status, boundResult(result, maxOutputBytes), request, classified.error));
|
|
256
253
|
return exitCode;
|
|
257
254
|
};
|
|
258
|
-
const flag = (name) =>
|
|
259
|
-
const index = args.indexOf(name);
|
|
260
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
261
|
-
};
|
|
255
|
+
const flag = (name) => argvValue(args, name);
|
|
262
256
|
const positional = args.filter((token, index) => !token.startsWith("--") && (index === 0 || !args[index - 1]?.startsWith("--env") && !args[index - 1]?.startsWith("--environment") && !args[index - 1]?.startsWith("--args")));
|
|
263
257
|
const toolName = positional[0];
|
|
264
258
|
if (!toolName || toolName === "help" || args.includes("--help")) {
|
|
@@ -950,11 +944,15 @@ async function main() {
|
|
|
950
944
|
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"] : [])];
|
|
951
945
|
commandExitCode = await cmdCompute(rest);
|
|
952
946
|
});
|
|
953
|
-
|
|
947
|
+
// Commander 14 needs a real variadic Option; the old `{variadic:true}` cast
|
|
948
|
+
// landed in the argParser slot and silently dropped every --steps value
|
|
949
|
+
// (live 2026-10-18: `--steps "审批@role:x"` answered "至少一个 --steps").
|
|
950
|
+
const collectValues = (value, previous = []) => [...previous, value];
|
|
951
|
+
command.command("rule-when").option("--schema <schemaCode>").option("--schema-code <schemaCode>").option("--on <trigger>").addOption(new Option("--when <condition>", "条件,可重复").argParser(collectValues).default([])).addOption(new Option("--then <action>", "动作,可重复").argParser(collectValues).default([])).option("--name <name>").option("--env <environment>").option("--environment <environment>").option("--apply").addHelpText("after", "\n条件-动作规则拼装器:--when 条件(可多个 AND)→ --then set:字段=值/引用/公式。编译为设计器验证形状(预置触发+UPDATE filter/赋值)。默认 dry-run,--apply 保存并发布;前台生效按输出清单人工验证。\n").action(async (options) => {
|
|
954
952
|
const rest = ["rule-when", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(options.on ? ["--on", options.on] : []), ...(Array.isArray(options.when) ? options.when.flatMap((w) => ["--when", w]) : []), ...(Array.isArray(options.then) ? options.then.flatMap((a) => ["--then", a]) : []), ...(options.name ? ["--name", options.name] : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : [])];
|
|
955
953
|
commandExitCode = await cmdRuleWhen(rest);
|
|
956
954
|
});
|
|
957
|
-
command.command("approval-flow").option("--schema <schemaCode>").option("--schema-code <schemaCode>").
|
|
955
|
+
command.command("approval-flow").option("--schema <schemaCode>").option("--schema-code <schemaCode>").addOption(new Option("--steps <step>", "审批步骤,可重复").argParser(collectValues).default([])).option("--env <environment>").option("--environment <environment>").option("--apply").option("--no-originator-node").option("--no-reject-to-start").addHelpText("after", "\n审批流拼装器:--steps 名称@role:<角色code>(多级给多个)。编译为验证模板(START→填写→审批链→END,审批节点字段只读),--apply 建流+权限+发布。\n").action(async (options) => {
|
|
958
956
|
const rest = ["approval-flow", ...(options.schema ? ["--schema", options.schema] : []), ...(options.schemaCode ? ["--schema-code", options.schemaCode] : []), ...(Array.isArray(options.steps) ? options.steps.flatMap((s) => ["--steps", s]) : []), ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.apply ? ["--apply"] : []), ...(options.originatorNode === false ? ["--no-originator-node"] : []), ...(options.rejectToStart === false ? ["--no-reject-to-start"] : [])];
|
|
959
957
|
commandExitCode = await cmdApprovalFlow(rest);
|
|
960
958
|
});
|
package/dist/code-rules.js
CHANGED
|
@@ -33,6 +33,17 @@ export const RESERVED_CODES = new Set([
|
|
|
33
33
|
"ownerDeptQueryCode",
|
|
34
34
|
"version",
|
|
35
35
|
].map((code) => code.toLowerCase()));
|
|
36
|
+
/**
|
|
37
|
+
* SQL 关键字(MySQL 未加引号时的保留字)。平台在 wire 才拒绝(10033
|
|
38
|
+
* 编码[order]为SQL关键字),这里前置到与 code 格式同层,省一轮建了再改。
|
|
39
|
+
*/
|
|
40
|
+
export const SQL_KEYWORD_CODES = new Set([
|
|
41
|
+
"order", "group", "key", "index", "desc", "asc", "select", "from", "where", "join",
|
|
42
|
+
"union", "all", "and", "or", "not", "null", "true", "false", "case", "when", "then",
|
|
43
|
+
"else", "end", "default", "primary", "foreign", "references", "constraint", "create",
|
|
44
|
+
"drop", "alter", "insert", "delete", "update", "into", "values", "table", "column",
|
|
45
|
+
"check", "distinct", "having", "limit", "offset", "by", "left", "right", "inner", "outer",
|
|
46
|
+
]);
|
|
36
47
|
/** Infer the style a code is written in (underscores → snake, else camel). */
|
|
37
48
|
export function inferCodeStyle(code) {
|
|
38
49
|
if (code.includes("_"))
|
|
@@ -60,6 +71,12 @@ export function validateBusinessCode(rawCode, options) {
|
|
|
60
71
|
}
|
|
61
72
|
if (RESERVED_CODES.has(code.toLowerCase()))
|
|
62
73
|
violations.push({ code, classification: "code_reserved", message: `code "${code}" 是平台系统/保留字段编码,业务模型与字段不得使用(会与平台自身列冲突)` });
|
|
74
|
+
if (SQL_KEYWORD_CODES.has(code.toLowerCase()))
|
|
75
|
+
violations.push({
|
|
76
|
+
code,
|
|
77
|
+
classification: "code_reserved",
|
|
78
|
+
message: `code "${code}" 是 SQL 关键字(平台会在落库时报 10033 编码[${code}]为SQL关键字);改成语义后缀,如 ${code}Ref / ${code}No`,
|
|
79
|
+
});
|
|
63
80
|
return violations;
|
|
64
81
|
}
|
|
65
82
|
/** Convenience for the dry-lint call sites: true when validateBusinessCode has no violations. */
|
package/dist/compute.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
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
|
+
import { argvValue } from "./args.js";
|
|
19
20
|
/** 编译 formula → editorText:每个 {token} 引用包成 <field-mark>裸token</field-mark>。 */
|
|
20
21
|
export function compileEditorText(formula, editorMark) {
|
|
21
22
|
return formula.replace(/\{([^{}]+)\}/g, (_match, token) => `<${editorMark}>${token.trim()}</${editorMark}>`);
|
|
@@ -40,10 +41,7 @@ export function composeComputeArgs(options) {
|
|
|
40
41
|
}
|
|
41
42
|
export async function cmdCompute(args) {
|
|
42
43
|
const request = requestId();
|
|
43
|
-
const flag = (name) =>
|
|
44
|
-
const index = args.indexOf(name);
|
|
45
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
46
|
-
};
|
|
44
|
+
const flag = (name) => argvValue(args, name);
|
|
47
45
|
const schema = flag("--schema") ?? flag("--schema-code");
|
|
48
46
|
const target = flag("--target") ?? flag("--property");
|
|
49
47
|
const formula = flag("--formula");
|
package/dist/form-default.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
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
|
+
import { argvValue } from "./args.js";
|
|
19
20
|
/** 从 form.get 结果提取控件数组(draft 优先,published 兜底)。 */
|
|
20
21
|
function toControlList(value) {
|
|
21
22
|
if (Array.isArray(value))
|
|
@@ -36,10 +37,7 @@ function extractControls(formGet) {
|
|
|
36
37
|
}
|
|
37
38
|
export async function cmdFormDefault(args) {
|
|
38
39
|
const request = requestId();
|
|
39
|
-
const flag = (name) =>
|
|
40
|
-
const index = args.indexOf(name);
|
|
41
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
42
|
-
};
|
|
40
|
+
const flag = (name) => argvValue(args, name);
|
|
43
41
|
const schema = flag("--schema") ?? flag("--schema-code");
|
|
44
42
|
const field = flag("--field");
|
|
45
43
|
const value = flag("--default");
|
package/dist/lanes/app.js
CHANGED
|
@@ -131,6 +131,10 @@ export function registerAppLane(tools) {
|
|
|
131
131
|
portalHidden,
|
|
132
132
|
...(warning ? { warning } : {}),
|
|
133
133
|
publishBoundary: { inScope: false, nextAction: "publish-in-designer", reason: "app publish is outside the CLI configuration boundary" },
|
|
134
|
+
manualActions: [
|
|
135
|
+
{ action: "publish-in-designer", detail: "在设计器发布应用,否则门户不可见" },
|
|
136
|
+
{ action: "portal-visibility-check", detail: "发布后核对应用位于具名分组并在门户可见" },
|
|
137
|
+
],
|
|
134
138
|
recovery: [{ code: "publish-in-designer", action: "在设计器发布应用并核对其位于具名分组;再做门户可见性验证" }],
|
|
135
139
|
note: "应用发布属边界外:建成后需人工在设计器发布并核对该分组,否则门户不可见",
|
|
136
140
|
},
|
|
@@ -117,7 +117,7 @@ export const B2_LANE_CONTRACT = Object.freeze({
|
|
|
117
117
|
}),
|
|
118
118
|
"service.register": Object.freeze({
|
|
119
119
|
scope: B2_SCOPE,
|
|
120
|
-
description: "
|
|
120
|
+
description: "**URL 前缀 × 鉴权(混用即 401)**:方法 url `/openapi/...` → `/api/openapi/...` 单层 + `xclient`(OAuth2 Client Credentials);`/api/...` → `/api/api/...` 双层 + 用户 token(client_id=api)。排障先 `service.list`→`service.get` 找同 adapter 的既有成功服务照抄形状;禁止用 `/externalLink/...` 等免鉴权前缀承载二开接口。注册业务集成服务(`/bizservice/service/create`,body `{code,name,adapterCode,description,configJson?}`;adapterCode 空 → 500005)+ 可选方法(`/bizservice/method/create`,可带 `configJson`/`inputParametersJson`/`outputParametersJson`),并回读。serviceConfig 明文序列化(RESTFUL/INTERNAL/SOAP 需 auth+commonlist.url;DATA_SQL/DATA_TABLE 需 commonlist.databaseConnectionPoolCode;SAP 需 commonlist.sapConnectionPoolCode;KAFKA 需 commonlist.kafkaConnectionPoolCode),methodConfig 由工具 AES-128-ECB+Base64 加密(SOAP 方法由 WSDL 动态发现,无固定方法 configJson)。参数面结构化序列化;RESTFUL 每参数需 configJson.position,URL_VARIABLE 拒绝;required 三态透传。设计器同款必填:name(≤50)、adapterCode、serviceCategoryId;除 SOAP 动态方法外,各原生适配器带 methodName 时必带 methodConfig;INTERNAL 方法创建走二开扩展包导入,不在本写面;仅名字/参数面的注册壳不可绑定。",
|
|
121
121
|
}),
|
|
122
122
|
"service.update": Object.freeze({
|
|
123
123
|
scope: "update readback",
|
|
@@ -133,7 +133,7 @@ export const B2_LANE_CONTRACT = Object.freeze({
|
|
|
133
133
|
}),
|
|
134
134
|
"method.create": Object.freeze({
|
|
135
135
|
scope: "method create readback",
|
|
136
|
-
description: "在**已存在的业务服务**下新增方法(`/bizservice/method/create`):新写一个扩展接口通常不是新建服务,而是往已有服务里加方法。服务不存在 → refused(新增服务用 `service.register`);方法已存在 → refused(改用 `method.update`)。configJson 由工具 AES
|
|
136
|
+
description: "在**已存在的业务服务**下新增方法(`/bizservice/method/create`):新写一个扩展接口通常不是新建服务,而是往已有服务里加方法。服务不存在 → refused(新增服务用 `service.register`);方法已存在 → refused(改用 `method.update`)。configJson 由工具 AES 加密,参数面结构化序列化并回读。**RESTFUL 参数面**:入参与出参的每个参数都要 `configJson.position`,合法值只有 `QUERY_PARAMS | URL_VARIABLE | REQUEST_HEADER | REQUEST_BODY | PATH_PARAMS`(`REQUEST_PARAM` 不存在;`URL_VARIABLE` 运行时 501007 被拒)。平台注入的常量头(如 X-LowCode-Corpid)标 `required:false`,否则 method.test 会按缺参 fail-closed。写入用枚举名,读回是整数索引(0 QUERY_PARAMS / 1 URL_VARIABLE / 2 REQUEST_HEADER / 3 REQUEST_BODY / 4 PATH_PARAMS)。",
|
|
137
137
|
}),
|
|
138
138
|
"method.delete": Object.freeze({
|
|
139
139
|
scope: "method delete readback",
|
|
@@ -93,7 +93,7 @@ export function registerBusinessRuleLane(tools) {
|
|
|
93
93
|
enabled: row.enabled,
|
|
94
94
|
status: row.status,
|
|
95
95
|
...(row.ruleVersion !== undefined ? { ruleVersion: row.ruleVersion } : {}),
|
|
96
|
-
...(row.defaultRule !== undefined ? { defaultRule: row.defaultRule } : {}),
|
|
96
|
+
...(row.defaultRule !== undefined ? { defaultRule: row.defaultRule, preset: row.defaultRule === true } : {}),
|
|
97
97
|
// ticket-80's update-mode readback teaches following the same-code
|
|
98
98
|
// latestPublished row — keep the flag on the default face (5 B).
|
|
99
99
|
...(row.latestPublished !== undefined ? { latestPublished: row.latestPublished } : {}),
|
|
@@ -8,7 +8,7 @@ import { LANES } from "./lane-ids.js";
|
|
|
8
8
|
export const MODEL_WIRE = Object.freeze({
|
|
9
9
|
createBody: "bizmodels/create carries appCode, parentId (folder), modelType, bizSchemaCode, icon, name_i18n, pcAble/mobileAble, options{statisticEnabled:false}",
|
|
10
10
|
readback: "exits_bizschema readback via apppackage/trees (type==='BizModel' && code===schemaCode)",
|
|
11
|
-
folderResolution: "explicit folderId
|
|
11
|
+
folderResolution: "explicit folderId(树行 id 或目录 code,经 applicationTree 归一为树行 id;未命中 → model-create-folder-unresolved)> first non-BizModel tree node (auto-default-folder);none found → 302001。注意 apppackage/list 的目录键是 code、trees 的键是 id,跨读面传值必须归一",
|
|
12
12
|
deleteBody: "apppackage/physically_delete body [{id,code,type:'BizModel'}] where id MUST be the app-tree node id (AppFunction id)",
|
|
13
13
|
deleteIdNamespace: "physically_delete resolves each row id as an AppFunction id (removeBizModel → getAppFunction(id), AppIntegrationFacadeImpl) and answers 601003 业务模型不存在 for a biz-model record id; the two id spaces are NOT interchangeable. model.delete/deleteMany resolve the node across applications (default app first, then applicationList per-app trees) and refuse fail-closed with model-application-unresolved when the record exists but no tree node does — never guessing a record id",
|
|
14
14
|
deleteTolerance: "already-absent short-circuits; the authoritative record face polls to verified absence; the tree node is eventually consistent and reported as treeNodePending",
|
|
@@ -108,7 +108,7 @@ export const MODEL_LANE_CONTRACT = Object.freeze({
|
|
|
108
108
|
}),
|
|
109
109
|
"model.create": Object.freeze({
|
|
110
110
|
scope: MODEL_SCOPE,
|
|
111
|
-
description: "Create a design-time model (bizmodels/create) with exits_bizschema readback. folderId optional
|
|
111
|
+
description: "Create a design-time model (bizmodels/create) with exits_bizschema readback. folderId optional — accepts BOTH the tree row id and the folder code and is normalized to the row id (apppackage/list exposes codes; trees expose ids); an unresolved folder is refused with candidates. applicationCode optional — defaults to the configured app, else the tenant's first application. A tree-miss does not prove absence: the record face is checked too (model-create-duplicate-orphan). readback.present=false downgrades deliveryState to WRITE_UNVERIFIED.",
|
|
112
112
|
}),
|
|
113
113
|
"model.delete": Object.freeze({
|
|
114
114
|
scope: MODEL_SCOPE,
|
package/dist/lanes/model.js
CHANGED
|
@@ -301,6 +301,8 @@ export function registerModelLane(tools) {
|
|
|
301
301
|
form,
|
|
302
302
|
listview,
|
|
303
303
|
rules,
|
|
304
|
+
rulesPreset: rules.filter((rule) => rule.defaultRule === true).map((rule) => rule.code),
|
|
305
|
+
rulesCustom: rules.filter((rule) => rule.defaultRule !== true).map((rule) => rule.code),
|
|
304
306
|
datarules,
|
|
305
307
|
workflow,
|
|
306
308
|
hint: "model.inspect 聚合设计时六面(fields/form/listview/rules/datarules/workflow,摘要)。modelPresent 是存在性判据(field 面 300004=false);字段 options 细节用 field.list --code;规则图用 rule.get schemaCode+ruleCode;运行数据用 data.list(不在此面)。",
|
|
@@ -345,10 +347,56 @@ export function registerModelLane(tools) {
|
|
|
345
347
|
message: `模型 ${args.code} 已存在于应用 ${existing.applicationCode} 的应用树中;create 拒绝重复 POST,不能把既有模型证明为本次创建结果`,
|
|
346
348
|
});
|
|
347
349
|
}
|
|
350
|
+
// A tree-miss does NOT prove absence: an orphan mount (row exists in
|
|
351
|
+
// h_biz_schema but its h_app_function.parentId is invalid) is invisible to
|
|
352
|
+
// the tree scan but real on the record face. Refuse instead of creating a
|
|
353
|
+
// second row and leaving the first orphaned (live 2026-10-17).
|
|
354
|
+
try {
|
|
355
|
+
// bizmodels/get_all is keyed by the query, so a non-null object IS the
|
|
356
|
+
// model (an absent one answers a business errcode — wireCall throws and
|
|
357
|
+
// we fall through to create).
|
|
358
|
+
const record = (await wireCall(session, { path: ENDPOINTS.modelGet, query: { schemaCode: args.code } })).data;
|
|
359
|
+
if (record !== null && typeof record === "object") {
|
|
360
|
+
throw new ToolRefusalError({
|
|
361
|
+
status: "refused",
|
|
362
|
+
blocker: "model-create-duplicate-orphan",
|
|
363
|
+
schemaCode: args.code,
|
|
364
|
+
applicationCode: args.applicationCode ?? null,
|
|
365
|
+
message: `模型 ${args.code} 在册(bizmodels/get_all 可读)但不在应用树中——疑似挂载孤儿(h_app_function.parentId 无效)。请修 parentId 为目录行 id,不要 create;否则会再建一条孤儿`,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
if (error instanceof ToolRefusalError)
|
|
371
|
+
throw error;
|
|
372
|
+
// A record-face read failure must not be read as absence; let create
|
|
373
|
+
// proceed and rely on the platform's unique key.
|
|
374
|
+
}
|
|
375
|
+
// Cross-face key trap (live 2026-10-17, AI-Platform 1.1.7): apppackage/list
|
|
376
|
+
// projects a Folder node's h_app_function.CODE, while apppackage/trees rows
|
|
377
|
+
// carry the function ID. Passing the former straight through as
|
|
378
|
+
// bizmodels/create.parentId writes an orphan row that the tree assembler
|
|
379
|
+
// drops — the model reads fine on schema faces but vanishes from the menu,
|
|
380
|
+
// model.list(tree) and duplicate checks. Resolve BOTH keys and always send
|
|
381
|
+
// the tree row id.
|
|
382
|
+
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
348
383
|
let folderId = args.folderId;
|
|
349
384
|
let resolvedFrom = args.folderId ? "explicit" : undefined;
|
|
350
|
-
if (
|
|
351
|
-
const
|
|
385
|
+
if (folderId) {
|
|
386
|
+
const folder = tree.find((row) => row.type !== "BizModel" && typeof row.id === "string" && (String(row.id) === folderId || String(row.code ?? "") === folderId));
|
|
387
|
+
if (!folder) {
|
|
388
|
+
const candidates = tree.filter((row) => row.type !== "BizModel" && typeof row.id === "string").map((row) => `${String(row.id)}|${String(row.code ?? "")}|${String(row.name ?? "")}`);
|
|
389
|
+
throw new ToolRefusalError({
|
|
390
|
+
status: "refused",
|
|
391
|
+
blocker: "model-create-folder-unresolved",
|
|
392
|
+
message: `folderId ${folderId} 未命中应用 ${applicationCode} 树中任何目录(id/code 双口径)——不要把 apppackage/list 的目录 code 当 id 传;候选(id|code|name):${candidates.join(";") || "(无目录)"}`,
|
|
393
|
+
applicationCode,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
resolvedFrom = String(folder.id) === folderId ? "explicit" : "explicit-code-resolved";
|
|
397
|
+
folderId = String(folder.id);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
352
400
|
const folder = tree.find((row) => row.type !== "BizModel" && typeof row.id === "string");
|
|
353
401
|
if (!folder)
|
|
354
402
|
throw new WireError("business", "no folder found for model creation", 302001);
|
package/dist/lanes/report.js
CHANGED
|
@@ -339,6 +339,18 @@ export function registerReportLane(tools) {
|
|
|
339
339
|
const infos = args.dataSourceInfos.map((info) => ({ dataSourceId: info.dataSourceId, useType: info.useType ?? 1 }));
|
|
340
340
|
const linked = (await biCall(session, { path: ENDPOINTS.biDatasourceModelLink, body: { dataSourceInfos: infos }, query: { lang: "cn" }, auth }));
|
|
341
341
|
const models = Array.isArray(linked) ? linked : [];
|
|
342
|
+
// Live 2026-10-18: the platform answers success but links NOTHING when the
|
|
343
|
+
// model has not synced into BI yet (cloudpivot_report.i_datasource /
|
|
344
|
+
// i_modelschema empty). DELIVERED with linkedCount:0 is a false success —
|
|
345
|
+
// refuse it with the prerequisite instead (designer must open the report
|
|
346
|
+
// once to trigger the sync).
|
|
347
|
+
if (infos.length > 0 && models.length === 0)
|
|
348
|
+
throw new ToolRefusalError({
|
|
349
|
+
status: "refused",
|
|
350
|
+
blocker: "report-datasource-link-not-synced",
|
|
351
|
+
message: "平台返回成功但未绑定任何数据集(linkedCount:0):模型尚未同步进 BI。先在设计器打开一次报表/触发同步,再重试;不要以 DELIVERED 判定成功。",
|
|
352
|
+
dataSourceIds: infos.map((info) => info.dataSourceId),
|
|
353
|
+
});
|
|
342
354
|
return {
|
|
343
355
|
performed: { errcode: 0, linked: infos.map((info) => info.dataSourceId) },
|
|
344
356
|
readback: { linkedCount: models.length, models: models.map((model) => ({ dataSourceId: model.dataSourceId, displayName: model.displayName, tableName: model.schema?.tableName })) },
|
|
@@ -503,6 +503,11 @@ export function registerRuntimeDataLane(tools) {
|
|
|
503
503
|
schemaCode: args.schemaCode,
|
|
504
504
|
...(args.queryCode !== undefined ? { queryCode: args.queryCode } : {}),
|
|
505
505
|
total: args.showTotal === false ? null : page.total,
|
|
506
|
+
// This branch is the non-`all` read: the platform's default filter
|
|
507
|
+
// hides DRAFTs and the total excludes them too — say so explicitly
|
|
508
|
+
// (live 2026-10-18: total:0 was read as "no residue" while 9 drafts
|
|
509
|
+
// existed).
|
|
510
|
+
filter: { excludedDraft: true, hint: "默认不含 DRAFT(total 亦仅计非草稿);核对/清理残留请加 all:true" },
|
|
506
511
|
totalPages: page.totalPages,
|
|
507
512
|
page: page.page ?? args.page ?? 0,
|
|
508
513
|
size: page.size ?? args.size ?? 20,
|
|
@@ -120,7 +120,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
120
120
|
}),
|
|
121
121
|
"workflow.update": Object.freeze({
|
|
122
122
|
scope: WORKFLOW_SCOPE,
|
|
123
|
-
description: "写入工作流模板草稿(`/api/api/workflow/update`,raw template JSON body)。cli 以当前草稿为基线做 merge,未列出的既有节点保留:**模板缺 START 或 END 时自动按增量 merge(部分模板),给全 START..END 才整模板替换**;也可显式 `merge:true/false`。**节点字段权限也走这里**:不必读 `get_draft --full`——直接发 `activities:[{activityCode, propertyPermissions:[{propertyCode, visible, editable, required, summary, subPropertyPermissions}]}]`(**type 可省**:activityCode 命中草稿节点时类型/名称继承草稿,不会铸 `_2`
|
|
123
|
+
description: "写入工作流模板草稿(`/api/api/workflow/update`,raw template JSON body)。cli 以当前草稿为基线做 merge,未列出的既有节点保留:**模板缺 START 或 END 时自动按增量 merge(部分模板),给全 START..END 才整模板替换**;也可显式 `merge:true/false`。**节点字段权限也走这里**:不必读 `get_draft --full`——直接发 `activities:[{activityCode, propertyPermissions:[{propertyCode, visible, editable, required, summary, subPropertyPermissions}]}]`(**type 可省**:activityCode 命中草稿节点时类型/名称继承草稿,不会铸 `_2` 新码;**改名=同 code 原地生效**——工具把新名写入 `name_i18n`(zh/zh_TW/en),activityName 平台侧保持首建值,读回以 `renamedInPlace.displayName` 为准;只有**类型变更**才铸新码并弃旧码),工具按 propertyCode 合并到草稿对应节点(未列的字段权限保留;`merge:true` 也可同用)——没有单独的权限工具,别把「找不到入口」当成不支持(dogfood 实测该误判会假阻塞)。`summary` **无需配置**:工具合并时自动补 `summary:true`(平台怪癖——缺省进不了运行时 `activitySummaries`、`form/load` 读不到你的 `editable:false`;显式传 `false` 才保留);子表字段用 `subPropertyPermissions`(同样自动补)。**子表属性不可设 `required:true`**(接口形状接受但设计器节点字段配置不支持,存入后无法渲染——工具直接拒 `workflow-child-table-not-required`);子表整行只配 visible/editable,字段级必填配到子表内部字段所在节点。**新增节点用语义 code**(如 `approval_finance`),不要模仿草稿的 `ActivityN` 再加 `_1/_2` 后缀。**设计器默认自动补齐**(调用方显式传值优先):填写节点({Originator})自动生成子表操作按钮 `buttonPermission.childTableBtns`(每个子表 ADD/DELETE/IMPORT/EXPORT 启用、BATCH_UPDATE 禁用——API 建的流程缺这份模板,设计器里子表按钮会全部不勾);`permittedAction` 归一为设计器默认(填写节点 showReject:false 不能驳回自己,审批节点 rejectToStart:true 驳回回发起节点)。用 `npm run test:live-permission`(非管理员参与者)验收,管理员会被后端强制 `e:true` 掩盖。",
|
|
124
124
|
}),
|
|
125
125
|
"workflow.publish": Object.freeze({
|
|
126
126
|
scope: WORKFLOW_SCOPE,
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -751,11 +751,14 @@ function activityCodeOf(record) {
|
|
|
751
751
|
* type END) — reproduced on bench12/13/15, where the second approval node
|
|
752
752
|
* showed as "结束". Only a FRESH code receives the caller's name/type.
|
|
753
753
|
*
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
754
|
+
* Reconcile the caller list against the draft: keep a code when the caller
|
|
755
|
+
* re-sends the same type + name (idempotent re-update). A NAME change is an
|
|
756
|
+
* IN-PLACE rename — the new name rides `name_i18n` (zh/zh_TW/en) on the same
|
|
757
|
+
* code (live 2026-10-17: that is exactly what the designer's rename posts;
|
|
758
|
+
* activityName stays the first-created value server-side). Only a TYPE
|
|
759
|
+
* change still mints a collision-free fresh code. Returns the rewritten
|
|
760
|
+
* activities plus the original→final code map for route remapping.
|
|
761
|
+
*/
|
|
759
762
|
function reconcileCallerActivityCodes(callerActivities, draftActivities) {
|
|
760
763
|
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
761
764
|
const used = new Set(draftActivities.map((activity) => String(activity.activityCode ?? "")));
|
|
@@ -766,6 +769,7 @@ function reconcileCallerActivityCodes(callerActivities, draftActivities) {
|
|
|
766
769
|
const name = activityNameOf(record);
|
|
767
770
|
const draft = draftByCode.get(original);
|
|
768
771
|
let code = original;
|
|
772
|
+
let renameInPlace = false;
|
|
769
773
|
if (draft !== undefined) {
|
|
770
774
|
const draftType = String(draft.activityType ?? "").trim().toUpperCase() === "APPROVAL" ? "PARTICIPANT" : String(draft.activityType ?? "").trim().toUpperCase();
|
|
771
775
|
const draftName = activityNameOf(draft);
|
|
@@ -776,17 +780,31 @@ function reconcileCallerActivityCodes(callerActivities, draftActivities) {
|
|
|
776
780
|
// Activity3_2_2). An omitted type is "keep the stored one", not "changed
|
|
777
781
|
// to empty".
|
|
778
782
|
const callerHasType = record.activityType !== undefined || record.type !== undefined;
|
|
779
|
-
const
|
|
780
|
-
|
|
783
|
+
const typeChanged = callerHasType && draftType !== type;
|
|
784
|
+
const nameChanged = name !== undefined && draftName !== undefined && name !== draftName;
|
|
785
|
+
// Live 2026-10-17 (designer capture + raw replay, 8.6.26): a NAME change
|
|
786
|
+
// DOES land in place — the writable carrier is `name_i18n` (locale map;
|
|
787
|
+
// the designer's own rename posts exactly that and activityName stays
|
|
788
|
+
// the first-created value). Only a TYPE change still needs a fresh code
|
|
789
|
+
// (activityType is pinned for existing codes).
|
|
790
|
+
if (typeChanged) {
|
|
781
791
|
let suffix = 2;
|
|
782
792
|
while (used.has(`${original}_${suffix}`))
|
|
783
793
|
suffix += 1;
|
|
784
794
|
code = `${original}_${suffix}`;
|
|
785
795
|
}
|
|
796
|
+
else if (nameChanged) {
|
|
797
|
+
renameInPlace = true;
|
|
798
|
+
}
|
|
786
799
|
}
|
|
787
800
|
used.add(code);
|
|
788
801
|
codeMap.set(original, code);
|
|
789
|
-
return {
|
|
802
|
+
return {
|
|
803
|
+
...record,
|
|
804
|
+
activityCode: code,
|
|
805
|
+
...(name !== undefined ? { activityName: name, i18nMessage: name } : {}),
|
|
806
|
+
...(renameInPlace ? { name_i18n: { zh: name, zh_TW: name, en: name } } : {}),
|
|
807
|
+
};
|
|
790
808
|
});
|
|
791
809
|
return { activities, codeMap };
|
|
792
810
|
}
|
|
@@ -968,10 +986,12 @@ async function backfillDesignerDefaults(session, schemaCode, body, callerTemplat
|
|
|
968
986
|
}
|
|
969
987
|
}
|
|
970
988
|
/**
|
|
971
|
-
* Rename pairs surfaced in the readback
|
|
972
|
-
* `<original>_<n>` and DROPS the original code —
|
|
973
|
-
* reads as a brand-new node and APPENDS it,
|
|
974
|
-
* readback names the pairs so the caller
|
|
989
|
+
* Rename pairs surfaced in the readback: a TYPE change (the only remaining
|
|
990
|
+
* mint trigger) mints `<original>_<n>` and DROPS the original code —
|
|
991
|
+
* re-sending the OLD code then reads as a brand-new node and APPENDS it,
|
|
992
|
+
* silently growing the flow. The readback names the pairs so the caller
|
|
993
|
+
* switches to the new code. (A pure NAME rename no longer mints — it rides
|
|
994
|
+
* name_i18n in place; see inPlaceRenames.)
|
|
975
995
|
*/
|
|
976
996
|
function renamedActivityPairs(template, data) {
|
|
977
997
|
const callerCodes = new Set((callerActivities(template) ?? []).map((row) => activityCodeOf(row)).filter((code) => code !== ""));
|
|
@@ -984,6 +1004,30 @@ function renamedActivityPairs(template, data) {
|
|
|
984
1004
|
}
|
|
985
1005
|
return pairs;
|
|
986
1006
|
}
|
|
1007
|
+
/**
|
|
1008
|
+
* In-place renames in a readback: rows whose name_i18n (the rename carrier)
|
|
1009
|
+
* diverges from the first-created activityName. Display name =
|
|
1010
|
+
* name_i18n.zh ?? name_i18n.en ?? activityName.
|
|
1011
|
+
*/
|
|
1012
|
+
function inPlaceRenames(data) {
|
|
1013
|
+
// The platform's draft face carries activities as an INDEX-KEYED OBJECT (the
|
|
1014
|
+
// compact projection already handles both shapes); accept either.
|
|
1015
|
+
const raw = data.activities;
|
|
1016
|
+
const rows = Array.isArray(raw)
|
|
1017
|
+
? raw
|
|
1018
|
+
: raw && typeof raw === "object"
|
|
1019
|
+
? Object.values(raw)
|
|
1020
|
+
: [];
|
|
1021
|
+
const out = [];
|
|
1022
|
+
for (const row of rows) {
|
|
1023
|
+
const i18n = row.name_i18n;
|
|
1024
|
+
const zh = i18n && typeof i18n === "object" ? String(i18n.zh ?? i18n.en ?? "") : "";
|
|
1025
|
+
const base = String(row.activityName ?? "");
|
|
1026
|
+
if (zh !== "" && zh !== base)
|
|
1027
|
+
out.push({ activityCode: String(row.activityCode ?? ""), activityName: base, displayName: zh });
|
|
1028
|
+
}
|
|
1029
|
+
return out;
|
|
1030
|
+
}
|
|
987
1031
|
/**
|
|
988
1032
|
* Live 2026-10-14: a workflow node's propertyPermissions ACCEPTS
|
|
989
1033
|
* `required:true` on a CHILD_TABLE (子表) property at the wire level, but the
|
|
@@ -1039,10 +1083,13 @@ function mergeCallerDelta(template, draft) {
|
|
|
1039
1083
|
...(raw.participant !== undefined ? { participant: normalizeParticipantForWire(raw.participant) } : {}),
|
|
1040
1084
|
};
|
|
1041
1085
|
});
|
|
1042
|
-
// Live 2026-
|
|
1043
|
-
//
|
|
1044
|
-
//
|
|
1045
|
-
//
|
|
1086
|
+
// Live 2026-10-17 (designer traffic capture + raw replay on 8.6.26): for an
|
|
1087
|
+
// EXISTING activityCode the platform pins activityName/i18nMessage/
|
|
1088
|
+
// activityType at their first-created values — but `name_i18n` (locale map)
|
|
1089
|
+
// is writable and IS the rename carrier (the designer's rename posts exactly
|
|
1090
|
+
// that under the same code). reconcileCallerActivityCodes keeps the code for
|
|
1091
|
+
// a name change and stamps name_i18n; only a type change still mints a fresh
|
|
1092
|
+
// code and DROPS the old node (a retype, not a rename).
|
|
1046
1093
|
const { activities: reconciledCaller, codeMap } = reconcileCallerActivityCodes(caller, draftActivities);
|
|
1047
1094
|
const draftByCode = new Map(draftActivities.map((activity) => [String(activity.activityCode ?? ""), activity]));
|
|
1048
1095
|
const originalOf = new Map();
|
|
@@ -1356,7 +1403,7 @@ export function workflowCompactProjection(result) {
|
|
|
1356
1403
|
index: Number(index),
|
|
1357
1404
|
code: activity.activityCode ?? null,
|
|
1358
1405
|
type: activity.activityType ?? null,
|
|
1359
|
-
name: activity
|
|
1406
|
+
name: effectiveActivityName(activity),
|
|
1360
1407
|
...(activity.participant !== undefined ? { participant: activity.participant } : {}),
|
|
1361
1408
|
// Round 16 (bench65 feedback): a participant node's field permissions were
|
|
1362
1409
|
// only visible in the 30–45 KB full body, so acceptance (and any "did my
|
|
@@ -1393,7 +1440,7 @@ export function workflowCompactProjection(result) {
|
|
|
1393
1440
|
edges,
|
|
1394
1441
|
ruleBindings,
|
|
1395
1442
|
ruleBindingCount: ruleBindings.reduce((sum, binding) => sum + binding.ruleCodes.length, 0),
|
|
1396
|
-
...(originate ? { originateActivity: { code: originate.activityCode ?? null, type: originate.activityType ?? null, name: originate
|
|
1443
|
+
...(originate ? { originateActivity: { code: originate.activityCode ?? null, type: originate.activityType ?? null, name: effectiveActivityName(originate), participant: originate.participant ?? null } } : {}),
|
|
1397
1444
|
},
|
|
1398
1445
|
// Editing needs only `activities` (array or index object) + `routes`: the tool
|
|
1399
1446
|
// merges onto the stored draft, connects added nodes through to END, fills
|
|
@@ -1402,6 +1449,21 @@ export function workflowCompactProjection(result) {
|
|
|
1402
1449
|
summaryHint: "改流程只需把要改/新增的 activities(数组)+ routes 传给 workflow.update:缺 START/END 的部分模板会自动按增量 merge 到草稿(草稿为基,自动补 sheetCode/连线/坐标),无需读回整模板或 --full 正文;字段权限也只需在对应 PARTICIPANT 上给 propertyPermissions:[{propertyCode,...}],工具按 propertyCode 合并、未列的字段权限保留。只有要整图替换时才给全 START..END 的完整模板;要看通知等完整节点正文才用 detailArtifactId 或 --full",
|
|
1403
1450
|
};
|
|
1404
1451
|
}
|
|
1452
|
+
/**
|
|
1453
|
+
* A node's DISPLAY name: `name_i18n` (the rename carrier — the designer's
|
|
1454
|
+
* rename writes the locale map and activityName keeps its first-created value,
|
|
1455
|
+
* live 2026-10-17) wins over the first-created activityName/i18nMessage.
|
|
1456
|
+
*/
|
|
1457
|
+
function effectiveActivityName(activity) {
|
|
1458
|
+
const i18n = activity.name_i18n;
|
|
1459
|
+
if (i18n && typeof i18n === "object") {
|
|
1460
|
+
const map = i18n;
|
|
1461
|
+
const localized = map.zh ?? map.zh_TW ?? map.en;
|
|
1462
|
+
if (localized !== undefined && String(localized) !== "")
|
|
1463
|
+
return String(localized);
|
|
1464
|
+
}
|
|
1465
|
+
return (activity.activityName ?? activity.i18nMessage ?? null);
|
|
1466
|
+
}
|
|
1405
1467
|
/**
|
|
1406
1468
|
* A workflow event handler (start/cancel/activate/end) binds a business rule by
|
|
1407
1469
|
* CODE. The platform's designer rule picker only offers CUSTOM rules — the
|
|
@@ -1651,7 +1713,18 @@ export function registerWorkflowLane(tools) {
|
|
|
1651
1713
|
validateWorkflowRender(data, "workflow.update readback");
|
|
1652
1714
|
const participantWarnings = participantReferenceWarnings(data);
|
|
1653
1715
|
const renamedActivities = renamedActivityPairs(template, data);
|
|
1654
|
-
|
|
1716
|
+
const renamedInPlace = inPlaceRenames(data);
|
|
1717
|
+
// 改名验收的权威读数:每个节点的当前显示名(platform may keep
|
|
1718
|
+
// activityName or sync it; effectiveActivityName resolves both shapes).
|
|
1719
|
+
const rawActivityRows = Array.isArray(data.activities)
|
|
1720
|
+
? data.activities
|
|
1721
|
+
: data.activities && typeof data.activities === "object"
|
|
1722
|
+
? Object.values(data.activities)
|
|
1723
|
+
: [];
|
|
1724
|
+
const activityNames = Object.fromEntries(rawActivityRows
|
|
1725
|
+
.map((row) => [String(row.activityCode ?? ""), effectiveActivityName(row)])
|
|
1726
|
+
.filter(([code]) => code !== ""));
|
|
1727
|
+
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) } };
|
|
1655
1728
|
}, (args) => {
|
|
1656
1729
|
// Zero-wire refusal (ticket 77): fires in semanticGate BEFORE the
|
|
1657
1730
|
// governed pipeline — including the rollback before-capture read.
|
package/dist/rule-when.js
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
import { runToolCall } from "./call.js";
|
|
22
22
|
import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contract.js";
|
|
23
23
|
import { writeEnvelope } from "./cli-output.js";
|
|
24
|
+
import { argvValue } from "./args.js";
|
|
24
25
|
const WHEN_OPS = { "=": "EQ", "==": "EQ", "!=": "NEQ", ">": "GT", "<": "LT", ">=": "GTEQ", "<=": "LTEQ", "~=": "CT", "!~": "NCT" };
|
|
25
26
|
/** 编译 --when/--then DSL 为设计器验证形状的规则图(导出供测试断言)。 */
|
|
26
27
|
export function composeRuleWhenGraph(options) {
|
|
@@ -93,15 +94,15 @@ export function composeRuleWhenArgs(options) {
|
|
|
93
94
|
}
|
|
94
95
|
export async function cmdRuleWhen(args) {
|
|
95
96
|
const request = requestId();
|
|
96
|
-
const flag = (name) =>
|
|
97
|
-
const index = args.indexOf(name);
|
|
98
|
-
return index >= 0 ? args[index + 1] : undefined;
|
|
99
|
-
};
|
|
97
|
+
const flag = (name) => argvValue(args, name);
|
|
100
98
|
const multi = (name) => {
|
|
101
99
|
const values = [];
|
|
100
|
+
const prefix = `${name}=`;
|
|
102
101
|
for (let i = 0; i < args.length; i++) {
|
|
103
102
|
if (args[i] === name && args[i + 1] !== undefined)
|
|
104
103
|
values.push(args[i + 1]);
|
|
104
|
+
else if (args[i] !== undefined && args[i].startsWith(prefix))
|
|
105
|
+
values.push(args[i].slice(prefix.length));
|
|
105
106
|
}
|
|
106
107
|
return values;
|
|
107
108
|
};
|
package/dist/session.js
CHANGED
|
@@ -58,8 +58,28 @@ export class CloudPivotSession {
|
|
|
58
58
|
administrator: cached.administrator,
|
|
59
59
|
...(cached.accountIdentity !== undefined ? { accountIdentity: cached.accountIdentity } : {}),
|
|
60
60
|
};
|
|
61
|
-
if (cached.version !== undefined)
|
|
61
|
+
if (cached.version !== undefined) {
|
|
62
62
|
this.version = cached.version;
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
// Legacy cache written while version evidence was unavailable: probe the
|
|
66
|
+
// public system config once and backfill the cache, otherwise every
|
|
67
|
+
// later write stays fail-closed even after the probe itself is fixed
|
|
68
|
+
// (live 2026-10-17: AI-Platform deployment, pcServerUrl null).
|
|
69
|
+
const systemConfig = await core
|
|
70
|
+
.fetchCloudPivotSystemConfig(this.profile.baseUrl, undefined, fetch, core.cloudPivotTenantHeaders(this.profile.engineCode))
|
|
71
|
+
.catch(() => undefined);
|
|
72
|
+
this.version = detectVersion(systemConfig);
|
|
73
|
+
if (this.version !== undefined) {
|
|
74
|
+
this.tokenCache?.save({
|
|
75
|
+
token: cached.token,
|
|
76
|
+
enterpriseId: cached.enterpriseId,
|
|
77
|
+
administrator: cached.administrator,
|
|
78
|
+
...(cached.accountIdentity !== undefined ? { accountIdentity: cached.accountIdentity } : {}),
|
|
79
|
+
version: this.version,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
63
83
|
return this.loginResult;
|
|
64
84
|
}
|
|
65
85
|
const core = await omcCore();
|
|
@@ -67,9 +87,12 @@ export class CloudPivotSession {
|
|
|
67
87
|
this.fingerprintOf = core.canonicalCloudPivotEnvironmentFingerprint;
|
|
68
88
|
let systemConfig = this.systemConfig;
|
|
69
89
|
if (!systemConfig) {
|
|
90
|
+
// Version evidence and login need the public system config only; the
|
|
91
|
+
// authoritative-origin resolver stays strict for callers that need an
|
|
92
|
+
// origin (AI-Platform answers pcServerUrl:null — live 2026-10-17).
|
|
70
93
|
systemConfig = await core
|
|
71
|
-
.
|
|
72
|
-
.
|
|
94
|
+
.fetchCloudPivotSystemConfig(this.profile.baseUrl, undefined, fetch, core.cloudPivotTenantHeaders(this.profile.engineCode))
|
|
95
|
+
.catch(() => undefined);
|
|
73
96
|
}
|
|
74
97
|
const authenticator = new core.CloudPivotAuthenticator(await this.http());
|
|
75
98
|
this.loginResult = await authenticator.login({
|
package/dist/write.js
CHANGED
|
@@ -110,6 +110,11 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
110
110
|
return { status: executed.status, steps: executed.steps, failedStep: executed.failedStep, ...executed.envelope, ...(args.requestId ? { requestId: args.requestId } : {}), artifactFingerprint: artifactRef.detailSha256 };
|
|
111
111
|
}
|
|
112
112
|
const { performed, readback, steps } = executed;
|
|
113
|
+
// Readback gate (live 2026-10-17): a tool that reports `readback.present:false`
|
|
114
|
+
// has NOT proven its own write — the platform accepted the request but the
|
|
115
|
+
// object is absent from the authoritative face. "Called without error" must
|
|
116
|
+
// never be stamped DELIVERED; downgrade to WRITE_UNVERIFIED and surface why.
|
|
117
|
+
const { deliveryState, readbackWarning } = deliveryStateFor(readback);
|
|
113
118
|
// Every production write echoes the explicit acknowledgement into the audit
|
|
114
119
|
// trail, so the record can always answer "was this a confirmed production
|
|
115
120
|
// write?".
|
|
@@ -133,7 +138,8 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
133
138
|
...(rollbackBefore !== undefined ? { before: rollbackBefore } : {}),
|
|
134
139
|
}
|
|
135
140
|
: {}),
|
|
136
|
-
deliveryState
|
|
141
|
+
deliveryState,
|
|
142
|
+
...(readbackWarning ? { readbackWarning } : {}),
|
|
137
143
|
recordedAt: new Date().toISOString(),
|
|
138
144
|
};
|
|
139
145
|
const artifactRef = writeArtifact(ctx, `${tool.name}-${randomUUID().slice(0, 8)}.json`, artifact);
|
|
@@ -156,9 +162,24 @@ async function governedWrite(ctx, tool, args, execute, dryRunPayload) {
|
|
|
156
162
|
});
|
|
157
163
|
rollbackRef = { seq: entry.seq, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})` };
|
|
158
164
|
}
|
|
159
|
-
return { status: "ok", deliveryState
|
|
165
|
+
return { status: "ok", deliveryState, performed, readback, ...(readbackWarning ? { readbackWarning } : {}), ...(steps !== undefined ? { steps } : {}), ...(args.requestId ? { requestId: args.requestId, idempotency: idempotencyDeclaration(tool.name) } : {}), ...(productionConfirmed ? { productionConfirmed: true } : {}), ...(rollbackRef ? { rollback: rollbackRef } : {}), summary: writeSummary(tool.name, args, { performed, readback }, "done"), detailFile: artifactRef.detailFile, detailSha256: artifactRef.detailSha256, artifactFingerprint: artifactRef.detailSha256 };
|
|
160
166
|
});
|
|
161
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Readback gate: a tool whose authoritative readback says the target is ABSENT
|
|
170
|
+
* has not proven its write. "Called without error" must never be stamped
|
|
171
|
+
* DELIVERED (live 2026-10-17: model.create returned errcode 0 + present:false
|
|
172
|
+
* while the menu row was orphaned; the AI read DELIVERED as success).
|
|
173
|
+
*/
|
|
174
|
+
export function deliveryStateFor(readback) {
|
|
175
|
+
const presentFalse = readback !== null && typeof readback === "object" && readback.present === false;
|
|
176
|
+
return presentFalse
|
|
177
|
+
? {
|
|
178
|
+
deliveryState: "WRITE_UNVERIFIED",
|
|
179
|
+
readbackWarning: "读回显示目标不存在(readback.present=false):平台接受了请求但权威面查不到——不要当作最终一致性豁免,按失败排查(模型挂载/路径/权限)",
|
|
180
|
+
}
|
|
181
|
+
: { deliveryState: "DELIVERED" };
|
|
182
|
+
}
|
|
162
183
|
function idempotencyDeclaration(tool) {
|
|
163
184
|
if (tool.includes("delete"))
|
|
164
185
|
return { key: "requestId", replay: "completed", duplicate: "already-absent-no-op" };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.54",
|
|
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.54",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.54",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|