@stalfh233/omc-cli 0.6.15 → 0.6.16
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/batch-call.js +55 -0
- package/dist/capability-overview.js +1 -1
- package/dist/cli.js +21 -7
- package/dist/goal-routes.js +23 -0
- package/dist/graph.js +11 -1
- package/dist/lanes/businessrule.js +22 -0
- package/dist/lanes/field.js +9 -0
- package/dist/lanes/list-view.js +0 -27
- package/dist/lanes/model.js +20 -2
- package/dist/lanes/runtime-data.js +4 -0
- package/dist/lanes/workflow-contract.js +2 -2
- package/dist/lanes/workflow.js +19 -1
- package/dist/read-shapes.js +4 -1
- package/dist/read.js +4 -1
- package/dist/recipes/field.js +39 -1
- package/dist/skills.js +13 -0
- package/dist/tool-types.js +21 -3
- package/package.json +3 -3
package/dist/batch-call.js
CHANGED
|
@@ -20,6 +20,54 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import { refusalText } from "./cli-contract.js";
|
|
22
22
|
import { runScript } from "./script.js";
|
|
23
|
+
function inputBusinessKey(args) {
|
|
24
|
+
const keys = ["code", "name", "bizObjectId", "id"];
|
|
25
|
+
const out = {};
|
|
26
|
+
for (const key of keys)
|
|
27
|
+
if (args[key] !== undefined)
|
|
28
|
+
out[key] = args[key];
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
function failureDetails(entry, args) {
|
|
32
|
+
const message = refusalText(entry.result);
|
|
33
|
+
const formData = args.formData && typeof args.formData === "object" && !Array.isArray(args.formData)
|
|
34
|
+
? args.formData
|
|
35
|
+
: args;
|
|
36
|
+
const numericName = Object.keys(formData).find((key) => /amount|qty|quantity|price|number|count/i.test(key));
|
|
37
|
+
const numericValue = numericName === undefined ? undefined : formData[numericName];
|
|
38
|
+
if (numericName !== undefined && typeof numericValue === "string" && /50000|服务异常|wire-business/i.test(message)) {
|
|
39
|
+
return {
|
|
40
|
+
fieldCode: numericName,
|
|
41
|
+
value: numericValue,
|
|
42
|
+
reason: `NUMERICAL 字段收到字符串 "${numericValue}"`,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
return { hint: "平台回了无明细 50000,常见成因见 help omc-refusals;请用该行输入与已发布字段逐项核对" };
|
|
46
|
+
}
|
|
47
|
+
export function batchWarnings(argsList) {
|
|
48
|
+
const warnings = [];
|
|
49
|
+
const seenCodes = new Map();
|
|
50
|
+
argsList.forEach((args, index) => {
|
|
51
|
+
const row = index + 1;
|
|
52
|
+
const data = args.formData && typeof args.formData === "object" && !Array.isArray(args.formData) ? args.formData : args;
|
|
53
|
+
if (data.code === undefined || data.code === null || data.code === "")
|
|
54
|
+
warnings.push({ row, fieldCode: "code", code: "required", message: "必填字段 code 缺失" });
|
|
55
|
+
else if (typeof data.code === "string") {
|
|
56
|
+
const previous = seenCodes.get(data.code);
|
|
57
|
+
if (previous !== undefined)
|
|
58
|
+
warnings.push({ row, fieldCode: "code", code: "duplicate", message: `code 与第 ${previous} 行重复` });
|
|
59
|
+
else
|
|
60
|
+
seenCodes.set(data.code, row);
|
|
61
|
+
}
|
|
62
|
+
for (const [fieldCode, value] of Object.entries(data)) {
|
|
63
|
+
if (typeof value === "string" && value.length > 255)
|
|
64
|
+
warnings.push({ row, fieldCode, code: "maxLength", message: `${fieldCode} 长度 ${value.length} 超过常见上限 255` });
|
|
65
|
+
if (typeof value === "number" && !Number.isInteger(value) && /qty|quantity|count|level/i.test(fieldCode))
|
|
66
|
+
warnings.push({ row, fieldCode, code: "scale-exceeded", message: `${fieldCode} 含小数,但字段可能是 0 位数值字段` });
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
return warnings;
|
|
70
|
+
}
|
|
23
71
|
/** Cap mirrors `omc script`: a runaway list should be a refusal, not a 20-min hang. */
|
|
24
72
|
export const BATCH_MAX_ENTRIES = 200;
|
|
25
73
|
/**
|
|
@@ -69,9 +117,12 @@ export async function runBatchCall(options) {
|
|
|
69
117
|
.filter((entry) => entry.status !== "ok")
|
|
70
118
|
.map((entry) => ({
|
|
71
119
|
index: entry.index,
|
|
120
|
+
row: entry.index + 1,
|
|
121
|
+
indexBase: 0,
|
|
72
122
|
status: String(entry.result.status ?? "failed"),
|
|
73
123
|
blocker: entry.result.blocker ?? null,
|
|
74
124
|
message: refusalText(entry.result),
|
|
125
|
+
...failureDetails(entry, options.argsList[entry.index] ?? {}),
|
|
75
126
|
...(Array.isArray(entry.result.issues) ? { issues: entry.result.issues } : {}),
|
|
76
127
|
}));
|
|
77
128
|
const executed = entries.length;
|
|
@@ -92,7 +143,10 @@ export async function runBatchCall(options) {
|
|
|
92
143
|
const skippedMarker = performed?.skipped;
|
|
93
144
|
return {
|
|
94
145
|
index: entry.index,
|
|
146
|
+
row: entry.index + 1,
|
|
147
|
+
indexBase: 0,
|
|
95
148
|
ok: entry.status === "ok",
|
|
149
|
+
...inputBusinessKey(options.argsList[entry.index] ?? {}),
|
|
96
150
|
...(bizObjectId !== undefined ? { bizObjectId } : {}),
|
|
97
151
|
...(entry.status === "ok" ? {} : { status: String(entry.result.status ?? "failed"), blocker: entry.result.blocker ?? null }),
|
|
98
152
|
...(skippedMarker !== undefined ? { skipped: String(skippedMarker) } : {}),
|
|
@@ -112,6 +166,7 @@ export async function runBatchCall(options) {
|
|
|
112
166
|
// 每条一行摘要(index/ok/bizObjectId)——不是完整信封,但足以回答
|
|
113
167
|
// 「第 3 条建成了吗、它的 id 是什么」。失败项另有 failures 带完整原因。
|
|
114
168
|
entries: entriesInline,
|
|
169
|
+
...(batchWarnings(options.argsList).length > 0 ? { warnings: batchWarnings(options.argsList) } : {}),
|
|
115
170
|
...(entries.length > entriesInline.length
|
|
116
171
|
? { entriesOmitted: entries.length - entriesInline.length, entriesHint: `条目超过 ${ENTRIES_INLINE_LIMIT} 条,只内联前 ${ENTRIES_INLINE_LIMIT} 条摘要;完整结果走 detailArtifactId / omc artifact get` }
|
|
117
172
|
: {}),
|
|
@@ -252,12 +252,12 @@ export const CAPABILITY_CUES = Object.freeze({
|
|
|
252
252
|
programming: ["在线开发", "在线编码", "后端源码", "java源码", "编码模式", "handler", "programming", "源码编译"],
|
|
253
253
|
bizservice: ["集成", "接口", "服务", "回调", "对接", "外部系统", "datasql", "restful"],
|
|
254
254
|
report: ["报表", "图表", "看板", "bi", "dashboard"],
|
|
255
|
+
data: ["运行数据", "提交数据", "保存记录", "查记录", "造数据", "测试数据", "测试单据", "造单据", "造单", "录数据", "填数据", "模拟数据", "业务数据", "造几条", "造两张", "造几张", "验数据", "批量导入", "批量写入", "批量删除", "导入数据"],
|
|
255
256
|
// r18 (2026-10-26): "造数据 / 造测试单据 / 录数据" is the RUN-DATA capability, but
|
|
256
257
|
// the goal usually also says 子表/明细 (field cues) — without these compound
|
|
257
258
|
// cues the router sent the probe to `field` in read mode (write:false), so it
|
|
258
259
|
// never reached data.submit. Compound (not bare 数据) to avoid stealing
|
|
259
260
|
// ordinary model/field asks that merely mention data.
|
|
260
|
-
data: ["运行数据", "提交数据", "保存记录", "查记录", "造数据", "测试数据", "测试单据", "造单据", "造单", "录数据", "填数据", "模拟数据", "业务数据", "造几条", "造两张", "造几张", "验数据"],
|
|
261
261
|
});
|
|
262
262
|
/**
|
|
263
263
|
* Boundary cues — authored alongside CAPABILITY_CUES (same runtime authority).
|
package/dist/cli.js
CHANGED
|
@@ -463,9 +463,10 @@ async function cmdBatchCall(store, toolName, rawList, options) {
|
|
|
463
463
|
failures.push({ index, blocker: payload.blocker ?? null, message: refusalText(payload), ...(Array.isArray(payload.validation) ? { validation: payload.validation } : {}), ...(payload.citation !== undefined ? { citation: payload.citation } : {}) });
|
|
464
464
|
}
|
|
465
465
|
});
|
|
466
|
-
|
|
466
|
+
let result = {
|
|
467
467
|
status: failures.length > 0 ? "partial" : "ok",
|
|
468
468
|
mode: "preflight",
|
|
469
|
+
shapeOnly: true,
|
|
469
470
|
tool: toolName,
|
|
470
471
|
requested: entries.length,
|
|
471
472
|
shapeChecked: entries.length,
|
|
@@ -473,8 +474,16 @@ async function cmdBatchCall(store, toolName, rawList, options) {
|
|
|
473
474
|
writes: 0,
|
|
474
475
|
// On failure the caller has to change the JSON first, so there is no
|
|
475
476
|
// runnable command yet; on success, hand over the exact execution command.
|
|
476
|
-
...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list
|
|
477
|
+
...(failures.length > 0 ? { failures } : { nextCommand: `omc call ${toolName} --args-list '<file|stdin>'` }),
|
|
477
478
|
};
|
|
479
|
+
const preflightPaths = [...argvValues(options.args, "--extract"), ...argvValues(options.args, "--jq")]
|
|
480
|
+
.flatMap((path) => path.split(",")).map((path) => path.trim()).filter(Boolean);
|
|
481
|
+
const preflightKeys = options.args.includes("--extract-keys") || options.args.includes("--keys") || options.args.some((token, index) => token === "--jq" && (options.args[index + 1] === undefined || options.args[index + 1].startsWith("--")));
|
|
482
|
+
if (preflightPaths.length > 0 || preflightKeys) {
|
|
483
|
+
const projected = projectResultExtract(result, preflightPaths, preflightKeys);
|
|
484
|
+
if (projected !== undefined)
|
|
485
|
+
result = projected;
|
|
486
|
+
}
|
|
478
487
|
const classified = classifyResult(result);
|
|
479
488
|
writeEnvelope(envelope(classified.status, result, request, classified.error));
|
|
480
489
|
return failures.length > 0 ? 1 : 0;
|
|
@@ -525,7 +534,7 @@ async function cmdCall(store, args) {
|
|
|
525
534
|
return emitTransportResult({ status: "ok", ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, { request, maxOutputBytes });
|
|
526
535
|
}
|
|
527
536
|
}
|
|
528
|
-
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file
|
|
537
|
+
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径(必须位于配置根内;跨目录临时文件用 --args -(stdin))>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组> [--preflight]]");
|
|
529
538
|
console.error("输出预算:--max-output-bytes 默认 65536(只约束最终 stdout 信封;读面 diet ~2048 独立外置 detailArtifactId);artifact get 内联上限 8192");
|
|
530
539
|
console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
|
|
531
540
|
return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
|
|
@@ -1632,17 +1641,22 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1632
1641
|
return emitThrown(error, transport);
|
|
1633
1642
|
}
|
|
1634
1643
|
}
|
|
1635
|
-
function cmdGraphReferences(kind, code, transport) {
|
|
1644
|
+
function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
1636
1645
|
if (!["service", "model", "field"].includes(kind))
|
|
1637
1646
|
return emitInvalid(transport, { message: "kind 必须是 service / model / field(同 rule.references 第一版边界)" });
|
|
1638
1647
|
try {
|
|
1639
1648
|
const result = queryGraphReferences(kind, code);
|
|
1649
|
+
if (requestedEnvironment !== undefined && requestedEnvironment !== result.index.environmentName)
|
|
1650
|
+
return emitResult({ status: "refused", blocker: "graph-environment-mismatch", message: `索引已绑定环境 ${result.index.environmentName},不是 ${requestedEnvironment};请先 omc graph pull --env ${requestedEnvironment}`, index: result.index }, transport);
|
|
1640
1651
|
return emitResult({
|
|
1641
1652
|
status: "ok",
|
|
1642
1653
|
kind: result.kind,
|
|
1643
1654
|
code: result.code,
|
|
1644
1655
|
matches: result.matches,
|
|
1645
1656
|
index: result.index,
|
|
1657
|
+
// r35a-F4:覆盖声明必须透传——CLI 这里是白名单式重建,漏加就等于没修
|
|
1658
|
+
// (matches:[] 曾被当成可安全删除的依据)。
|
|
1659
|
+
...(result.coverage !== undefined ? { coverage: result.coverage } : {}),
|
|
1646
1660
|
...(result.note !== undefined ? { note: result.note } : {}),
|
|
1647
1661
|
...(result.gapWarning !== undefined ? { gapWarning: result.gapWarning } : {}),
|
|
1648
1662
|
}, transport);
|
|
@@ -2145,7 +2159,7 @@ async function main() {
|
|
|
2145
2159
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2146
2160
|
});
|
|
2147
2161
|
command.command("doctor").option("--environment <environment>").option("--env <environment>").option("--service <serviceCode>", "可选:体检单个业务集成服务的鉴权参数形状(掩码/占位/全 0 检出,WARN 非致命)").action(async (options) => { commandExitCode = await cmdDoctor(new ConfigStore(defaultPaths()), options.environment ?? options.env, options.service); });
|
|
2148
|
-
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
2162
|
+
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象);逐条执行、默认不中断,最后回 succeeded/failed 汇总。适合批量建字段等').option("--preflight", "与 --args-list 一起用:只跑参数校验 + 写前语义闸(零 wire、零写入),一次报全所有形状问题").option("--keys", "与 artifact get 的 --keys 同义(= --extract-keys):列出本次结果 artifact 的顶层键与体积").addOption(new Option("--extract <path>", "在本次结果的 artifact 内取一个路径的值(可重复;省掉一次 artifact get)").argParser((value, previous = []) => [...previous, value]).default([])).addOption(new Option("--jq [path]", "同 --extract,并支持 [] 映射(如 items[].fields.planNo);裸 --jq 列出结果的顶层键").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract-keys", "列出本次结果 artifact 的顶层键与体积(省掉一次 artifact get --keys)").option("--max-output-bytes <bytes>", "最终 stdout 信封预算;默认 65536(投影/--jq 之后再判);读面 diet(~2048) 独立外置 detailArtifactId,抬高本参数不会把已 diet 的大表重新内联;artifact get 内联上限 8192").option("--request-id <requestId>").option("--timeout-ms <milliseconds>").option("--wait <milliseconds>").option("--dry-run").addHelpText("after", `\n${onboardingHelp()}\n`).action(async (tool, options) => {
|
|
2149
2163
|
const rest = [tool, ...(options.env ? ["--env", options.env] : []), ...(options.environment ? ["--environment", options.environment] : []), ...(options.args !== undefined ? ["--args", options.args] : []), ...(options.argsFile !== undefined ? ["--args-file", options.argsFile] : []), ...(options.argsList !== undefined ? ["--args-list", options.argsList] : []), ...(options.preflight ? ["--preflight"] : []), ...(Array.isArray(options.extract) ? options.extract.flatMap((path) => ["--extract", path]) : []), ...(Array.isArray(options.jq) ? options.jq.flatMap((path) => path === true ? ["--jq"] : ["--jq", path]) : []), ...(options.extractKeys ? ["--extract-keys"] : []),
|
|
2150
2164
|
...(options.keys ? ["--keys"] : []), ...(options.maxOutputBytes !== undefined ? ["--max-output-bytes", options.maxOutputBytes] : []), ...(options.requestId ? ["--request-id", options.requestId] : []), ...(options.timeoutMs !== undefined ? ["--timeout-ms", options.timeoutMs] : []), ...(options.wait !== undefined ? ["--wait", options.wait] : []), ...(options.dryRun ? ["--dry-run"] : [])];
|
|
2151
2165
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
@@ -2513,8 +2527,8 @@ async function main() {
|
|
|
2513
2527
|
const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
|
|
2514
2528
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport));
|
|
2515
2529
|
});
|
|
2516
|
-
graphCommand.command("references <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references
|
|
2517
|
-
commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport));
|
|
2530
|
+
graphCommand.command("references <kind> <code>").option("--env <environment>", "本子命令不接触环境;该参数仅用于提示索引绑定环境").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references 第一版边界;结果带索引新鲜度与扫描缺口(有缺口时零命中不构成「无引用」证明)。覆盖:规则图/数据规则/视图;不含表单、workflow.permissions、流程绑定、在线 JS。在线版 rule.references 不含数据规则;两者都不含上述未扫描面。\n").action(async (kind, code, options) => {
|
|
2531
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdGraphReferences(kind, code, transport, options.environment ?? options.env));
|
|
2518
2532
|
});
|
|
2519
2533
|
graphCommand.command("impact <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n影响面(离线):跳1=引用该编码的规则(同 references),跳2=这些规则还读写哪些 service/model/field——改前看爆炸半径。截断上限 50 规则×每类 20 码。\n").action(async (kind, code, options) => {
|
|
2520
2534
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
|
package/dist/goal-routes.js
CHANGED
|
@@ -672,6 +672,29 @@ export function goalRoute(registry, goal) {
|
|
|
672
672
|
firstAnswer: { state: "blocked", why: boundary.note },
|
|
673
673
|
};
|
|
674
674
|
}
|
|
675
|
+
if (/批量导入|批量写入|批量删除|导入数据/.test(goal)) {
|
|
676
|
+
const action = /批量删除/.test(goal) ? "data.delete" : "data.save";
|
|
677
|
+
return {
|
|
678
|
+
status: "ok", goal, confident: true, mode: "modify", write: true,
|
|
679
|
+
match: { kind: "capability", ref: "data", title: "运行数据批量写入(--args-list)" },
|
|
680
|
+
route: { capability: "data", lane: "runtime-data", note: "运行数据批量操作逐条执行,失败继续;先用 --preflight 做零写入形状校验", readiness: DISCOVERY_AUTHORITY.readiness, firstCommands: [`omc call ${action} --args-list '<file|stdin>' --preflight`, `omc call ${action} --args-list '<file|stdin>'`], write: true },
|
|
681
|
+
firstAnswer: { state: "needs-input", question: "目标模型 schemaCode 和输入文件/JSON 数组是什么?示例:omc call data.save --args-list '<file|stdin>' --preflight", action: LIST_MODELS_ACTION, pendingCommand: `omc call ${action} --args-list '<file|stdin>'`, why: "批量导入/写入/删除使用 --args-list;先预检形状,再逐条执行" },
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
// Cross-application model moves/copies are not an intent migration. There
|
|
685
|
+
// is no model.move/model.copy path across app boundaries: model.move only
|
|
686
|
+
// reparents a model within its current application. Keep the useful
|
|
687
|
+
// declarative route for rebuilding the target model, but put this fact in
|
|
688
|
+
// the route itself so callers cannot mistake intent for an in-place move.
|
|
689
|
+
if (/模型/.test(goal) && /(跨应用|一个应用.*另一个应用|从一个应用.*到另一个应用)/.test(goal) && /(移动|复制|迁移|搬)/.test(goal)) {
|
|
690
|
+
const routed = wholePackageRoute(registry, goal, index, ["app", "model"]);
|
|
691
|
+
const clarification = "事实澄清:没有跨应用迁移/复制模型的专用工具;model.move 仅支持同一应用内换目录。跨应用移植需要在新应用重建模型,并手工搬运数据。";
|
|
692
|
+
return {
|
|
693
|
+
...routed,
|
|
694
|
+
route: { ...routed.route, note: clarification },
|
|
695
|
+
firstAnswer: { ...routed.firstAnswer, why: `${clarification} ${routed.firstAnswer.why}` },
|
|
696
|
+
};
|
|
697
|
+
}
|
|
675
698
|
// Composer before the capability solver: a recurring write SHAPE has a single
|
|
676
699
|
// command that does the job with preflight + idempotency, which beats a ladder
|
|
677
700
|
// of atomic tools even when the ladder also matches (r13: 26 field.create).
|
package/dist/graph.js
CHANGED
|
@@ -607,16 +607,26 @@ export function queryGraphReferences(kind, code) {
|
|
|
607
607
|
via: row.via,
|
|
608
608
|
}));
|
|
609
609
|
const gapped = summary.gaps.count > 0;
|
|
610
|
+
// r35a-F4(真机 2026-11-06):探针拿 matches:[] 差点把一个**被表单引用**的
|
|
611
|
+
// 字段判成「可安全删除」——note 只说了「不含规则条件叶子/流程绑定」,从没
|
|
612
|
+
// 明说表单与流程权限表不在扫描范围。散文字段不承担安全契约:这里给结构化
|
|
613
|
+
// coverage,让「没人引用」与「这一面没扫」在机器可读层面可分。
|
|
614
|
+
const coverage = {
|
|
615
|
+
scanned: ["businessrule", "datarule", "listview"],
|
|
616
|
+
notScanned: ["form", "workflow.permissions", "workflow.bindings", "onlinejs", "rule.conditions"],
|
|
617
|
+
note: "matches:[] 只代表 scanned 面零命中;notScanned 面(尤其 form 与 workflow.permissions)未扫描,不可据此判定可安全删除",
|
|
618
|
+
};
|
|
610
619
|
return {
|
|
611
620
|
kind,
|
|
612
621
|
code,
|
|
613
622
|
matches,
|
|
623
|
+
coverage,
|
|
614
624
|
index: summary,
|
|
615
625
|
...(matches.length === 0
|
|
616
626
|
? {
|
|
617
627
|
note: (gapped
|
|
618
628
|
? "零命中,但索引存在扫描缺口——不可作为「无引用」证明;先 omc graph pull 重建再查"
|
|
619
|
-
: "
|
|
629
|
+
: "零命中=仅 scanned 面(规则/数据规则/视图)没有引用它;form(表单控件)与 workflow.permissions 未扫描,见 coverage.notScanned——不可据此判定可安全删除") +
|
|
620
630
|
(zeroHitKindHint(db, kind, code, "references") ?? ""),
|
|
621
631
|
}
|
|
622
632
|
: {}),
|
|
@@ -141,12 +141,25 @@ export function registerBusinessRuleLane(tools) {
|
|
|
141
141
|
// key (schemaCode+ruleCode), so the row id is rarely needed.
|
|
142
142
|
const fullRows = args.full === true;
|
|
143
143
|
const visible = args.onlyCustom === true ? rows.filter((row) => row.defaultRule !== true) : rows;
|
|
144
|
+
const statusLag = visible.some((row) => row.enabled === true && String(row.status ?? "") !== "ENABLED");
|
|
144
145
|
return {
|
|
145
146
|
schemaCode: args.schemaCode,
|
|
146
147
|
modelPresent: true,
|
|
147
148
|
// r28g F18:list 投影的是 PUBLISH 行;运行时可执行性以 rule.get / rule.enable.verified 为准。
|
|
148
149
|
source: "published-list",
|
|
149
150
|
executableHint: "rules[].enabled/status 来自 PUBLISH 投影;运行时判定读 DRAFT(rule.get 的 enabled+ruleStatus,或 rule.enable 回执的 executable/verified)。不要仅凭本面判定可 rule.run。",
|
|
151
|
+
...(statusLag
|
|
152
|
+
? {
|
|
153
|
+
statusLag: true,
|
|
154
|
+
statusNote: "本面 status 是 PUBLISH 投影列,与 rule.get 的 ruleStatus 可不同;检测到 enabled=true 而 status 非 ENABLED 的滞后行——enabled=true 才是分发真相,运行时以 rule.get/enable 复核",
|
|
155
|
+
}
|
|
156
|
+
: {
|
|
157
|
+
// r35a-F7(真机 2026-11-06):`status` 与 rule.get 的 `ruleStatus` 是
|
|
158
|
+
// **不同 wire 列**,可一个 ENABLED 一个 DISABLED(探针实测
|
|
159
|
+
// list status=ENABLED/ruleVersion=1 vs get ruleStatus=DISABLED/0)。
|
|
160
|
+
// 常态化给一句短注(~90B/次),避免把本面 status 当运行时真相。
|
|
161
|
+
statusNote: "本面 status 是 PUBLISH 投影列,与 rule.get 的 ruleStatus 可不同;运行时是否生效以 rule.get/enable 为准",
|
|
162
|
+
}),
|
|
150
163
|
...(args.onlyCustom === true ? { filtered: { onlyCustom: true, total: rows.length, returned: visible.length } } : {}),
|
|
151
164
|
...(endpointFamily ? { endpointFamily } : {}),
|
|
152
165
|
rules: visible.map((row) => ({
|
|
@@ -154,6 +167,7 @@ export function registerBusinessRuleLane(tools) {
|
|
|
154
167
|
name: row.name,
|
|
155
168
|
enabled: row.enabled,
|
|
156
169
|
status: row.status,
|
|
170
|
+
...(row.enabled === true && String(row.status ?? "") !== "ENABLED" ? { statusLag: true } : {}),
|
|
157
171
|
...(row.ruleVersion !== undefined ? { ruleVersion: row.ruleVersion } : {}),
|
|
158
172
|
...(row.defaultRule !== undefined ? { defaultRule: row.defaultRule, preset: row.defaultRule === true } : {}),
|
|
159
173
|
// ticket-80's update-mode readback teaches following the same-code
|
|
@@ -270,6 +284,14 @@ export function registerBusinessRuleLane(tools) {
|
|
|
270
284
|
code: wanted,
|
|
271
285
|
scanned: { models: scoped.length, rules: scannedRules, ...(parseFailures > 0 ? { parseFailures } : {}) },
|
|
272
286
|
matches,
|
|
287
|
+
// r35a-F4(真机 2026-11-06):matches:[] 曾被当成「可安全删除」的依据,
|
|
288
|
+
// 而探针实测该字段**被表单引用**(form.get draftControlCodes)且出现在
|
|
289
|
+
// 流程节点权限里——本面两者都没扫。散文字段不承担安全契约:结构化声明。
|
|
290
|
+
coverage: {
|
|
291
|
+
scanned: ["businessrule"],
|
|
292
|
+
notScanned: ["datarule", "form", "workflow.permissions", "workflow.bindings", "onlinejs", "listview"],
|
|
293
|
+
note: "matches:[] 只代表 businessrule 面零命中;datarule/form/流程权限/视图等未扫描(离线 graph references 覆盖更广,见其 coverage),不可据此判定可安全删除",
|
|
294
|
+
},
|
|
273
295
|
...(scope !== undefined ? { applicationResolution: applicationResolutionOf(scope) } : {}),
|
|
274
296
|
...(matches.length === 0
|
|
275
297
|
? {
|
package/dist/lanes/field.js
CHANGED
|
@@ -732,10 +732,12 @@ async function fieldUpdateDryRun(session, args) {
|
|
|
732
732
|
}),
|
|
733
733
|
};
|
|
734
734
|
}
|
|
735
|
+
const impact = await collectFieldDeleteImpact(session, args, { full: true });
|
|
735
736
|
return {
|
|
736
737
|
wouldChange: changes.length > 0,
|
|
737
738
|
changes,
|
|
738
739
|
...(dataImpact !== undefined ? { dataImpact } : {}),
|
|
740
|
+
impact,
|
|
739
741
|
schemaCode: args.schemaCode,
|
|
740
742
|
code: args.code,
|
|
741
743
|
};
|
|
@@ -951,6 +953,11 @@ async function collectFieldDeleteImpact(session, args, options) {
|
|
|
951
953
|
...(counted.note ? { dataRowsNote: counted.note } : counted.dataRows === "unknown" ? { dataRowsNote: "拿不到行数:data.list total 不可用" } : {}),
|
|
952
954
|
references,
|
|
953
955
|
dependentUpdates: surfaces.dependentUpdates,
|
|
956
|
+
coverage: {
|
|
957
|
+
scanned: options.full ? ["businessrule", "form", "listview", "onlinejs", "data"] : ["form", "listview", "onlinejs", "data"],
|
|
958
|
+
notScanned: options.full ? ["workflow.permissions", "workflow.bindings", "rule.conditions"] : ["businessrule", "workflow.permissions", "workflow.bindings", "rule.conditions"],
|
|
959
|
+
coveredSurfaces: options.full ? ["businessrule", "form", "listview", "onlinejs", "data"] : ["form", "listview", "onlinejs", "data"],
|
|
960
|
+
},
|
|
954
961
|
...(unknowns.length > 0 ? { unknown: unknowns } : {}),
|
|
955
962
|
note: options.full
|
|
956
963
|
? "完整影响面(规则+表单+视图+在线JS+行数)。删除不会自动清理依赖面残留。"
|
|
@@ -1062,6 +1069,7 @@ async function runFieldUpdate(session, args) {
|
|
|
1062
1069
|
...(args.propertyEmpty !== undefined ? ["propertyEmpty"] : []),
|
|
1063
1070
|
];
|
|
1064
1071
|
const unverifiable = submittedPropertyEmpty !== undefined && !hasReadBackPropertyEmpty ? ["propertyEmpty"] : [];
|
|
1072
|
+
const impact = await collectFieldDeleteImpact(session, args, { full: false });
|
|
1065
1073
|
return {
|
|
1066
1074
|
performed: { errcode: performed.errcode },
|
|
1067
1075
|
...(prepared.conditionCompiled !== undefined && prepared.conditionCompiled.length > 0
|
|
@@ -1069,6 +1077,7 @@ async function runFieldUpdate(session, args) {
|
|
|
1069
1077
|
: {}),
|
|
1070
1078
|
readback: {
|
|
1071
1079
|
face: "draft",
|
|
1080
|
+
impact,
|
|
1072
1081
|
fieldPresent: after !== undefined,
|
|
1073
1082
|
...(after?.name !== undefined ? { name: after.name } : {}),
|
|
1074
1083
|
...(typeof after?.name_i18n === "string" ? { name_i18n: after.name_i18n } : {}),
|
package/dist/lanes/list-view.js
CHANGED
|
@@ -476,23 +476,6 @@ export function registerListViewLane(tools) {
|
|
|
476
476
|
schemaCode: z.string().min(1),
|
|
477
477
|
name: z.string().min(1),
|
|
478
478
|
code: z.string().optional().describe("视图 code;省略时**默认取 schemaCode**(与 listview.get / listview.list / rollback 的默认视图口径一致)。平台侧 code 必填,本工具会替你填,不要再依赖不回执的猜测(r26 真机:不传直接被 errcode 10005「编码不能为空」拒绝,白跑一轮线上往返)"),
|
|
479
|
-
// r29b F5a: create 没有列参数。接受 fieldCodes 只是为了给可执行 nextCommand,
|
|
480
|
-
// 避免未知键算法给出「fieldCodes→code」这种南辕北辙的建议。
|
|
481
|
-
fieldCodes: z.array(z.string()).optional().describe("本工具不配列——若传了会拒绝并指引 listview.configure"),
|
|
482
|
-
},
|
|
483
|
-
// dry-run 不跑 execute——fieldCodes 必须在 preflight 拦,否则 dry-run 仍会放行。
|
|
484
|
-
preflight: (args) => {
|
|
485
|
-
if (Array.isArray(args.fieldCodes) && args.fieldCodes.length > 0) {
|
|
486
|
-
const code = args.code ?? args.schemaCode;
|
|
487
|
-
throw new ToolInputError({
|
|
488
|
-
status: "error",
|
|
489
|
-
blocker: "tool-input-invalid",
|
|
490
|
-
tool: "listview.create",
|
|
491
|
-
message: "listview.create 不接受列参数:先 create(schemaCode/name/code),再 listview.configure 配 fieldCodes。" +
|
|
492
|
-
"不要把 fieldCodes 当成 create 的拼写错误去改成 code。",
|
|
493
|
-
nextCommand: `omc call listview.configure --args '${JSON.stringify({ schemaCode: args.schemaCode, code, fieldCodes: args.fieldCodes })}'`,
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
479
|
},
|
|
497
480
|
execute: async (session, args) => {
|
|
498
481
|
// r26 真机:`code` 在 schema 里可选,但三处读面/rollback 都用
|
|
@@ -500,16 +483,6 @@ export function registerListViewLane(tools) {
|
|
|
500
483
|
// `errcode 10005 编码不能为空`,探针白跑一轮。默认视图 code 就是
|
|
501
484
|
// schemaCode(listview.list 的 hint 也这么写),这里对齐。
|
|
502
485
|
const code = args.code ?? args.schemaCode;
|
|
503
|
-
if (Array.isArray(args.fieldCodes) && args.fieldCodes.length > 0) {
|
|
504
|
-
throw new ToolInputError({
|
|
505
|
-
status: "error",
|
|
506
|
-
blocker: "tool-input-invalid",
|
|
507
|
-
tool: "listview.create",
|
|
508
|
-
message: "listview.create 不接受列参数:先 create(schemaCode/name/code),再 listview.configure 配 fieldCodes。" +
|
|
509
|
-
"不要把 fieldCodes 当成 create 的拼写错误去改成 code。",
|
|
510
|
-
nextCommand: `omc call listview.configure --args '${JSON.stringify({ schemaCode: args.schemaCode, code, fieldCodes: args.fieldCodes })}'`,
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
486
|
// Ticket 81: read the header FIRST — a same-code retry short-circuits
|
|
514
487
|
// with the existing view and sends ZERO create wires (the live
|
|
515
488
|
// post-timeout retry otherwise answers 303002 编码重复).
|
package/dist/lanes/model.js
CHANGED
|
@@ -393,7 +393,11 @@ export function registerModelLane(tools) {
|
|
|
393
393
|
// delete). Same SYSTEM_FIELD_CODES set field.list uses.
|
|
394
394
|
const visibleColumns = all ? listviewColumns : listviewColumns.filter((column) => !SYSTEM_FIELD_CODES.has(String(column.propertyCode)));
|
|
395
395
|
const listview = listviewFace && listviewFace.columns
|
|
396
|
-
? {
|
|
396
|
+
? {
|
|
397
|
+
code: listviewFace.code ?? schemaCode,
|
|
398
|
+
columnCount: visibleColumns.length,
|
|
399
|
+
columns: visibleColumns.map((column) => ({ propertyCode: column.propertyCode, propertyType: column.propertyType, visible: column.visible })),
|
|
400
|
+
}
|
|
397
401
|
: undefined;
|
|
398
402
|
const ruleRows = Array.isArray(ruleFace?.rules) ? ruleFace.rules : [];
|
|
399
403
|
// All rules are kept: the platform pre-creates trigger rows (Create/Update/
|
|
@@ -433,6 +437,7 @@ export function registerModelLane(tools) {
|
|
|
433
437
|
const workflow = draftFace?.exists === false || (draftFace === undefined && publishedFace === undefined)
|
|
434
438
|
? (draftFace && draftFace.schemaCode ? { exists: false } : undefined)
|
|
435
439
|
: {
|
|
440
|
+
exists: true,
|
|
436
441
|
workflowCode: summary?.workflowCode ?? workflowCode,
|
|
437
442
|
published: publishedFace?.published === true || pubSummary?.templateType === "PUBLISH",
|
|
438
443
|
activities: Array.isArray(summary?.activities) ? summary.activities.map((activity) => ({ code: activity.code, type: activity.type, name: activity.name, participant: activity.participant })) : [],
|
|
@@ -447,6 +452,7 @@ export function registerModelLane(tools) {
|
|
|
447
452
|
const absent = fieldRefusalErrcode === 300004 || faceErrcodes.get("field.list") === 300004;
|
|
448
453
|
const modelPresent = absent ? false : fieldFace !== undefined && !fieldRefused ? true : undefined;
|
|
449
454
|
return {
|
|
455
|
+
code: schemaCode,
|
|
450
456
|
schemaCode,
|
|
451
457
|
...(modelName !== undefined ? { name: modelName } : {}),
|
|
452
458
|
...(modelPresent !== undefined ? { modelPresent } : {}),
|
|
@@ -482,12 +488,24 @@ export function registerModelLane(tools) {
|
|
|
482
488
|
return result;
|
|
483
489
|
return {
|
|
484
490
|
schemaCode: result.schemaCode,
|
|
491
|
+
// r35a-F3:compact 投影必须保留 code(model.list 有 code,inspect 此前
|
|
492
|
+
// 只剩 schemaCode,`--jq '[].code'` 必 pathMiss 白跑一轮)。
|
|
493
|
+
...(result.code !== undefined ? { code: result.code } : {}),
|
|
485
494
|
fieldCodes: (result.fields ?? []).map((field) => field.code),
|
|
486
495
|
form: result.form,
|
|
487
496
|
listview: result.listview ? { code: result.listview.code, columnCount: result.listview.columnCount } : undefined,
|
|
488
497
|
ruleCodes: (result.rules ?? []).map((rule) => rule.code),
|
|
489
498
|
datarules: result.datarules,
|
|
490
|
-
|
|
499
|
+
// r35a-F2:workflow 摘要必须恒有 exists 布尔——compact 投影此前把它丢了,
|
|
500
|
+
// 真机(走 compact 的分支)于是永远读不到 exists(探针只能靠
|
|
501
|
+
// missedEntries 反推「缺失=有流程」,语义反直觉且有安全风险)。
|
|
502
|
+
workflow: result.workflow
|
|
503
|
+
? {
|
|
504
|
+
exists: result.workflow.exists !== false,
|
|
505
|
+
workflowCode: result.workflow.workflowCode,
|
|
506
|
+
published: result.workflow.published,
|
|
507
|
+
}
|
|
508
|
+
: { exists: false },
|
|
491
509
|
hint: result.hint,
|
|
492
510
|
};
|
|
493
511
|
},
|
|
@@ -2238,6 +2238,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2238
2238
|
...readback,
|
|
2239
2239
|
workflowInstanceId: effectiveInstanceId,
|
|
2240
2240
|
objectId,
|
|
2241
|
+
// The submit response often has no workItemId; when the engine created
|
|
2242
|
+
// the next todo, its first item is the authoritative continuation id.
|
|
2241
2243
|
workItemId: typeof performedData.workItemId === "string" ? performedData.workItemId : null,
|
|
2242
2244
|
sequenceStatus,
|
|
2243
2245
|
submittedData: approvalDelta ? projectApprovalReadback(loadedRecord, Object.keys(formData ?? {})) : projectReadbackRecord(loadedRecord, args),
|
|
@@ -2290,6 +2292,8 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2290
2292
|
}
|
|
2291
2293
|
if (nextWorkItems.length > 0) {
|
|
2292
2294
|
readback.nextWorkItems = nextWorkItems;
|
|
2295
|
+
if (readback.workItemId === null)
|
|
2296
|
+
readback.workItemId = nextWorkItems[0].workItemId;
|
|
2293
2297
|
const nextName = nextWorkItems[0].activityName ?? nextWorkItems[0].activityCode ?? "下一节点";
|
|
2294
2298
|
if (rejected) {
|
|
2295
2299
|
readback.returnedTo = {
|
|
@@ -208,11 +208,11 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
208
208
|
}),
|
|
209
209
|
"workitem.list": Object.freeze({
|
|
210
210
|
scope: "runtime workflow work-item query",
|
|
211
|
-
description: "查询当前登录人的待办(`GET /api/api/runtime/workflow/search_workitems`)。WorkItemSearchVO 经 @ModelAttribute 传参,服务端以登录态注入 userId
|
|
211
|
+
description: "查询当前登录人的待办(`GET /api/api/runtime/workflow/search_workitems`)。WorkItemSearchVO 经 @ModelAttribute 传参,服务端以登录态注入 userId,故无需传处理人——结果天然是「我的待办」。返回每项完整键集:`workItemId, workflowInstanceId, workflowCode, activityCode, activityName, state, sequenceNo, originator, originatorName, participant, participantName, bizObjectId, schemaCode`;外置大结果另带 `itemsCount, ids, itemsPreview, truncated, truncatedKeys, omittedKeys, detailFile/detailSha256`,其中 itemsPreview 为前 3 个 `{id}` 引用。并尽力经 `GET /api/api/runtime/workflow/getBizObjectByWfInstanceId` 解析 bizObjectId/schemaCode 供 data.submit 审批。`workflowInstanceId` 为客户端精确过滤(服务端 WorkItemSearchVO.filterWorkflowInstanceId 实为 notEqualsTo 排除,不能用于命中实例);带该过滤时 total 为过滤后的条数,原服务端总数在 unfilteredTotal。**想知道「流程停在哪」,用 `workflow.instanceActivity`,不要靠反复 workitem.list**——待办查询只返回在办项,实例停住时永远是空数组。",
|
|
212
212
|
}),
|
|
213
213
|
"workflow.instanceActivity": Object.freeze({
|
|
214
214
|
scope: "runtime workflow instance node-state diagnosis",
|
|
215
|
-
description: "按实例查全部节点状态(`GET /api/api/runtime/workflow/list_workflow_instance_activity`,WorkflowMaintenanceAssembler.listActivateActivity
|
|
215
|
+
description: "按实例查全部节点状态(`activities[].status` 数字枚举:`1=当前激活/持有待办`、`0=已完成或尚未到达`,别与实例级 `instanceStatus:\"PROCESSING\"` 混用——后者只表示流程仍在运行)。返回每个节点 `{activityCode,activityName,activityType,status,participantCount,participants:[{name,id}],sheetTrigger?}`(`GET /api/api/runtime/workflow/list_workflow_instance_activity`,WorkflowMaintenanceAssembler.listActivateActivity)。用于「data.submit 回 PROCESSING 但没有下一待办」的自诊断:例子 `omc call workflow.instanceActivity --args '{\"workflowInstanceId\":\"<id>\"}'`。当没有任何节点 status=1 时,readback 的 diagnosis.code=no-active-activity 并列出排查顺序(参与人为空的审批节点 → 参与人写法 → 容器日志 GetParticipantInfo error)。审批推进正常时该工具不需要——data.submit 的 readback.nextSubmit 已给出下一级参数。",
|
|
216
216
|
}),
|
|
217
217
|
}),
|
|
218
218
|
});
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -19,6 +19,24 @@ const WORKFLOW_LANE = WORKFLOW_LANE_CONTRACT.lane;
|
|
|
19
19
|
const WORKFLOW_TOOLS = WORKFLOW_LANE_CONTRACT.tools;
|
|
20
20
|
const WORKFLOW_ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
|
|
21
21
|
const WORKFLOW_POINT = /^-?\d+(?:\.\d+)?,\s-?\d+(?:\.\d+)?$/;
|
|
22
|
+
/** Keep workitem.list's external envelope aligned with data.list. */
|
|
23
|
+
export function workItemListCompactProjection(result) {
|
|
24
|
+
const compact = {};
|
|
25
|
+
for (const [key, value] of Object.entries(result)) {
|
|
26
|
+
if (key !== "items" || !Array.isArray(value)) {
|
|
27
|
+
if (key !== "ids" && key !== "itemsPreview" && key !== "listArtifactHint")
|
|
28
|
+
compact[key] = value;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const rows = value;
|
|
32
|
+
const ids = rows.map((row) => row.workItemId).filter((id) => typeof id === "string" && id.length > 0);
|
|
33
|
+
compact.itemsCount = rows.length;
|
|
34
|
+
compact.ids = ids;
|
|
35
|
+
compact.itemsPreview = ids.slice(0, 3).map((id) => ({ id }));
|
|
36
|
+
compact.listArtifactHint = "items 已外置(truncatedKeys/omittedKeys 含 items):完整待办见 artifact;ids=本页全部 workItemId,itemsPreview=前 3 个 id 引用。";
|
|
37
|
+
}
|
|
38
|
+
return compact;
|
|
39
|
+
}
|
|
22
40
|
/**
|
|
23
41
|
* Conditional branch lines (round7). `routes[]` maps to the platform `Rule`
|
|
24
42
|
* DTO (Rule.java:28-59), which carries a structured branch condition
|
|
@@ -3205,7 +3223,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3205
3223
|
...("unfilteredTotal" in page && page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
|
|
3206
3224
|
items,
|
|
3207
3225
|
};
|
|
3208
|
-
}),
|
|
3226
|
+
}, { compact: workItemListCompactProjection }),
|
|
3209
3227
|
}));
|
|
3210
3228
|
tools.push(defineTool({
|
|
3211
3229
|
name: "workflow.instanceActivity",
|
package/dist/read-shapes.js
CHANGED
|
@@ -94,7 +94,10 @@ const READ_SHAPES = {
|
|
|
94
94
|
},
|
|
95
95
|
"workitem.list": {
|
|
96
96
|
// Flat rows — no fields bag. Wire id is projected as workItemId.
|
|
97
|
-
|
|
97
|
+
// r34c-F4(真机 2026-11-06):此前 `{workItemId,activityCode,...}` 的省略号让
|
|
98
|
+
// 探针必须实调一次才敢确定标题键叫 activityName(--full 也一样省略)。
|
|
99
|
+
// 展开真实键集(真机 item 实测);外置信封的 ids/itemsPreview 由 F7 补。
|
|
100
|
+
keys: ["items:array<{workItemId,workflowInstanceId,workflowCode,activityCode,activityName,state,sequenceNo,originator,originatorName,participant,participantName,bizObjectId,schemaCode}>", "total", "count"],
|
|
98
101
|
notes: ["待办 id 键是 workItemId,不是 id(平铺,不套 fields)"],
|
|
99
102
|
},
|
|
100
103
|
"rule.list": {
|
package/dist/read.js
CHANGED
|
@@ -121,7 +121,10 @@ function externalizedShapePlaceholders(result, dropped) {
|
|
|
121
121
|
// 自相矛盾的标记会让调用方以为还得去 artifact 取。
|
|
122
122
|
if (dropped.length === 0)
|
|
123
123
|
return {};
|
|
124
|
-
|
|
124
|
+
// Keep the A1 empty/null placeholder shape, but explicitly distinguish it
|
|
125
|
+
// from a genuine empty payload. `omittedKeys` is the machine-readable face;
|
|
126
|
+
// `truncatedKeys` remains the legacy list of payload keys moved to artifact.
|
|
127
|
+
const placeholders = { truncatedKeys: [...dropped], omittedKeys: [...dropped], truncated: true };
|
|
125
128
|
for (const key of dropped)
|
|
126
129
|
placeholders[key] = Array.isArray(result[key]) ? [] : null;
|
|
127
130
|
return placeholders;
|
package/dist/recipes/field.js
CHANGED
|
@@ -291,6 +291,44 @@ const formulaAggregationCrossModel = Object.freeze({
|
|
|
291
291
|
requiresEnvironmentDerivedIds: true,
|
|
292
292
|
prerequisiteData: { source: "field.list(published:true) ×2(来源模型 + 目标模型)", fields: ["source.model", "source.dataItemCode", "source.matchField", "target.matchField"] },
|
|
293
293
|
});
|
|
294
|
+
const formulaExpressionSameModel = Object.freeze({
|
|
295
|
+
id: "formula-expression-same-model",
|
|
296
|
+
title: "同模型基础公式字段:FORMULA + EXPRESSION(amount×2)",
|
|
297
|
+
aliases: ["同模型公式", "基础公式字段", "表达式字段", "公式字段", "amount*2"],
|
|
298
|
+
status: "offline-valid",
|
|
299
|
+
tool: "field.create",
|
|
300
|
+
args: {
|
|
301
|
+
environment,
|
|
302
|
+
schemaCode: model,
|
|
303
|
+
code: "total",
|
|
304
|
+
name: "合计",
|
|
305
|
+
propertyType: "FORMULA",
|
|
306
|
+
options: {
|
|
307
|
+
formulaConfig: JSON.stringify({
|
|
308
|
+
resultType: 2,
|
|
309
|
+
formulaType: "EXPRESSION",
|
|
310
|
+
expression: JSON.stringify({ formula: "{amount}*2" }),
|
|
311
|
+
}),
|
|
312
|
+
persistent: true,
|
|
313
|
+
},
|
|
314
|
+
},
|
|
315
|
+
preconditions: ["目标模型已存在;amount 是同一模型上已存在并已发布的数值字段;total 尚不存在"],
|
|
316
|
+
conditionalRequirements: [
|
|
317
|
+
"formulaConfig 是 JSON 字符串,内层 expression 仍是 JSON 字符串:CLI recipe 已代做两层序列化,直接把 args 传给 field.create,不要再手工转义。",
|
|
318
|
+
"这是同模型的基础 EXPRESSION;跨模型汇总请改用 formula-aggregation-cross-model。",
|
|
319
|
+
],
|
|
320
|
+
priorToolReferences: [
|
|
321
|
+
{ tool: "field.list", args: { environment, schemaCode: model, published: true }, purpose: "确认 amount 已发布并确认目标 code 未占用" },
|
|
322
|
+
],
|
|
323
|
+
dryRun: { supported: true, command: `omc call field.create --env ${environment} --dry-run --args '<recipe.args>'`, notes: "dry-run 校验 FORMULA/EXPRESSION 序列化形状,不创建字段" },
|
|
324
|
+
expectedReadback: { readback: { fieldPresent: true, options: { formulaConfig: "<EXPRESSION: {amount}*2>" } }, next: ["field.publish", "form.draft", "form.publish"] },
|
|
325
|
+
refusalRecovery: [
|
|
326
|
+
{ code: "duplicate-field", action: "字段已存在改用 field.update 或换 code" },
|
|
327
|
+
{ code: "formula-expression-source-missing", action: "先创建并发布同模型的 amount 数值字段,再创建 total" },
|
|
328
|
+
...commonRecovery,
|
|
329
|
+
],
|
|
330
|
+
requiresEnvironmentDerivedIds: false,
|
|
331
|
+
});
|
|
294
332
|
/**
|
|
295
333
|
* r29(2026-10-29 中冶终轮)+ 用户领域知识:外部数据(企查查/NC 等)在表单上
|
|
296
334
|
* 「填表时即带出」的标准模式——两段式组合。此前三轮探针(r24/r28b/r29)都发明
|
|
@@ -346,6 +384,6 @@ const externalLookupModel = Object.freeze({
|
|
|
346
384
|
});
|
|
347
385
|
export const FIELD_METADATA = Object.freeze({
|
|
348
386
|
"field.update": Object.freeze({ recipes: [fieldSerialNumber, formConditionalRequired, formAlwaysReadonly, formConditionalReadonly], relatedTools: ["field.list", "field.publish", "form.draft", "form.publish"] }),
|
|
349
|
-
"field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, dictionaryMultiSelection, childTable, businessModelDropdown, formulaAggregationCrossModel, externalLookupModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
|
|
387
|
+
"field.create": Object.freeze({ recipes: [numericTwoDecimals, dictionarySelection, dictionaryMultiSelection, childTable, businessModelDropdown, formulaAggregationCrossModel, formulaExpressionSameModel, externalLookupModel], relatedTools: ["dictionary.names", "dictionary.records", "discover_environment", "field.list", "field.publish", "form.draft", "configure_form", "form.publish"] }),
|
|
350
388
|
});
|
|
351
389
|
//# sourceMappingURL=field.js.map
|
package/dist/skills.js
CHANGED
|
@@ -102,6 +102,19 @@ const SYNTHETIC_SKILLS = Object.freeze({
|
|
|
102
102
|
"",
|
|
103
103
|
].join("\n"),
|
|
104
104
|
},
|
|
105
|
+
"batch-write": {
|
|
106
|
+
name: "batch-write",
|
|
107
|
+
description: "批量运行数据写入、形状预检与逐条失败定位",
|
|
108
|
+
content: [
|
|
109
|
+
"# 批量写",
|
|
110
|
+
"",
|
|
111
|
+
"通用批量入口是 `omc call <tool> --args-list '<json数组>'`,逐条执行且默认不中断。",
|
|
112
|
+
"先做零写入形状校验:`omc call data.save --args-list '<file|stdin>' --preflight`;回执 `shapeOnly:true`,只校验 JSON 形状与 schema 必填键,不做业务语义校验;业务校验要真写一次才知道。",
|
|
113
|
+
"支持 `data.save`、`data.delete`、`field.create` 等同一工具逐条批量调用;没有专用 `data.import`,导入数据就是逐条 `data.save`。",
|
|
114
|
+
"失败项回 `row`(1 基)、`index`(0 基)和尽力推断的字段/原因;成功项回输入中的 `code` 等业务标识。",
|
|
115
|
+
"",
|
|
116
|
+
].join("\n"),
|
|
117
|
+
},
|
|
105
118
|
});
|
|
106
119
|
/** Every methodology topic exposed by `omc help`. */
|
|
107
120
|
export function listSkills() {
|
package/dist/tool-types.js
CHANGED
|
@@ -171,6 +171,22 @@ function contextualClosestKey(toolName, key, schemaKeys) {
|
|
|
171
171
|
return "bizObjectIds";
|
|
172
172
|
return undefined;
|
|
173
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* r35a-F8(真机 2026-11-06):`field.update` 传 `propertyType` 时,编辑距离
|
|
176
|
+
* 建议到 `propertyEmpty`(风马牛不相及)——探针要「改字段类型」却拿不到任何
|
|
177
|
+
* 可行动信息。字段 type/code 在平台设计态不可原地修改,只能删重建(且会波及
|
|
178
|
+
* 引用),所以这里给的是**替代路径**而不是一个错误参数名。
|
|
179
|
+
*/
|
|
180
|
+
const CONTEXTUAL_KEY_GUIDANCE = Object.freeze({
|
|
181
|
+
"field.update": Object.freeze({
|
|
182
|
+
propertyType: "平台不支持原地改字段类型:需 field.delete(先看其 impact)→ field.create;同名 code 的改 type 会丢该列存量值",
|
|
183
|
+
type: "同上:改类型需 field.delete → field.create;改名(显示名)用 name,改 code 无工具(等同删+建)",
|
|
184
|
+
code: "字段 code 不可改:只能 field.delete → field.create(引用面会断,先看 field.delete 的 impact)",
|
|
185
|
+
}),
|
|
186
|
+
"field.create": Object.freeze({
|
|
187
|
+
precision: "precision→options.scale(数值小数位,不是顶层 precision)",
|
|
188
|
+
}),
|
|
189
|
+
});
|
|
174
190
|
function closestSchemaKey(key, schemaKeys) {
|
|
175
191
|
if (schemaKeys.has(key))
|
|
176
192
|
return key;
|
|
@@ -218,6 +234,8 @@ export function parseToolInput(tool, input) {
|
|
|
218
234
|
const unknown = Object.keys(normalized).filter((key) => !schemaKeys.has(key));
|
|
219
235
|
if (unknown.length > 0) {
|
|
220
236
|
const suggestions = unknown.map((key) => `${key}→${KEY_SEMANTIC_HINTS[key] ?? contextualClosestKey(tool.name, key, schemaKeys) ?? closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
|
|
237
|
+
// r35a-F8:有替代路径指引时单独列(不是「最接近的合法键」,而是「该走哪条路」)。
|
|
238
|
+
const guidanceLines = unknown.map((key) => CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key]).filter((hint) => typeof hint === "string" && hint.length > 0);
|
|
221
239
|
// r25 实证:`publish→(无相近键)` 等于没说——配置与发布是两步,用户要的是
|
|
222
240
|
// 另一个工具。有语义指引时优先给可执行的下一步。
|
|
223
241
|
const twoStep = unknown
|
|
@@ -228,7 +246,7 @@ export function parseToolInput(tool, input) {
|
|
|
228
246
|
// 漏掉发布指引(报告原文:「若用程序消费回执,会选 issues[],就会漏掉」)。
|
|
229
247
|
// 两条通道必须同源,否则等于修了一半。
|
|
230
248
|
const issueHintFor = (key) => {
|
|
231
|
-
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key];
|
|
249
|
+
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key] ?? CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key];
|
|
232
250
|
if (guidance !== undefined)
|
|
233
251
|
return guidance;
|
|
234
252
|
const semantic = KEY_SEMANTIC_HINTS[key];
|
|
@@ -247,8 +265,8 @@ export function parseToolInput(tool, input) {
|
|
|
247
265
|
status: "error",
|
|
248
266
|
blocker: "tool-input-unknown-keys",
|
|
249
267
|
tool: tool.name,
|
|
250
|
-
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `最接近的合法键:${suggestions.join(";")}。参数表:${paramCommand}`}`,
|
|
251
|
-
...(twoStep.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
|
|
268
|
+
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `${guidanceLines.length > 0 ? guidanceLines.join(";") : `最接近的合法键:${suggestions.join(";")}`}。参数表:${paramCommand}`}`,
|
|
269
|
+
...(twoStep.length > 0 || guidanceLines.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
|
|
252
270
|
issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: issueHintFor(key) })),
|
|
253
271
|
});
|
|
254
272
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.16",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
57
|
"commander": "14.0.3",
|
|
58
|
-
"omc-core": "0.6.
|
|
58
|
+
"omc-core": "0.6.16",
|
|
59
59
|
"proper-lockfile": "4.1.2",
|
|
60
60
|
"write-file-atomic": "6.0.0",
|
|
61
61
|
"zod": "^3.23.8",
|
|
@@ -73,6 +73,6 @@
|
|
|
73
73
|
"directory": "packages/omc-cli"
|
|
74
74
|
},
|
|
75
75
|
"optionalDependencies": {
|
|
76
|
-
"@stalfh233/omc-fake-platform": "0.6.
|
|
76
|
+
"@stalfh233/omc-fake-platform": "0.6.16"
|
|
77
77
|
}
|
|
78
78
|
}
|