@stalfh233/omc-cli 0.6.14 → 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 +47 -16
- package/dist/discovery.js +7 -1
- package/dist/goal-routes.js +23 -0
- package/dist/graph.js +11 -1
- package/dist/human-summary.js +4 -1
- package/dist/lanes/businessrule.js +22 -0
- package/dist/lanes/datarule-contract.js +2 -1
- package/dist/lanes/datarule.js +60 -10
- package/dist/lanes/field-contract.js +2 -2
- 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/organization.js +45 -16
- package/dist/lanes/runtime-data.js +4 -0
- package/dist/lanes/workflow-contract.js +2 -2
- package/dist/lanes/workflow.js +29 -4
- 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 +46 -6
- 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);
|
|
@@ -536,6 +545,11 @@ async function cmdCall(store, args) {
|
|
|
536
545
|
const argsList = flag("--args-list");
|
|
537
546
|
if (argsList !== undefined)
|
|
538
547
|
return cmdBatchCall(store, toolName, argsList, { environment, requestValue, args, preflight: args.includes("--preflight") });
|
|
548
|
+
if (args.includes("--preflight")) {
|
|
549
|
+
const message = "--preflight 仅用于 --args-list 批量形状;单次调用没有批量预检语义。请改用 --args-list,或移除 --preflight 后执行单次调用。";
|
|
550
|
+
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message, retryable: false }));
|
|
551
|
+
return CLI_EXIT_CODES.invalidInput;
|
|
552
|
+
}
|
|
539
553
|
// 1.2 of issue 2026-10-23: read faces answer {detailArtifactId, sha256} and the
|
|
540
554
|
// caller then needs a SECOND call to see one value (16 two-step chains in one
|
|
541
555
|
// session; 15 more were pure --keys probes). `--extract <path>` (repeatable)
|
|
@@ -685,7 +699,7 @@ async function cmdCall(store, args) {
|
|
|
685
699
|
const otherUnknown = unknownKeys.filter((key) => !APP_SCOPE_ARG_KEYS.has(key));
|
|
686
700
|
if (tool.kind !== "write" && otherUnknown.length > 0) {
|
|
687
701
|
if (!lenientArgs) {
|
|
688
|
-
const suggestions = otherUnknown.map((key) => ({ key, ...suggestArgumentKey(key, [...schemaKeys]) }));
|
|
702
|
+
const suggestions = otherUnknown.map((key) => ({ key, ...suggestArgumentKey(key, [...schemaKeys], toolName) }));
|
|
689
703
|
const pairs = suggestions.map((entry) => {
|
|
690
704
|
if (entry.closest)
|
|
691
705
|
return `${entry.key}→${entry.closest}`;
|
|
@@ -705,7 +719,7 @@ async function cmdCall(store, args) {
|
|
|
705
719
|
return emit("error", result, CLI_EXIT_CODES.invalidInput);
|
|
706
720
|
}
|
|
707
721
|
for (const key of otherUnknown) {
|
|
708
|
-
ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys]) });
|
|
722
|
+
ignoredReadKeys.push({ key, ...suggestArgumentKey(key, [...schemaKeys], toolName) });
|
|
709
723
|
}
|
|
710
724
|
}
|
|
711
725
|
}
|
|
@@ -781,6 +795,10 @@ async function cmdCall(store, args) {
|
|
|
781
795
|
const projected = projectResultExtract(result, extractPaths, extractKeys);
|
|
782
796
|
if (projected !== undefined)
|
|
783
797
|
result = projected;
|
|
798
|
+
// Projected reads are represented by extract[] (and rows[] for multi-path).
|
|
799
|
+
// Do not also copy the single-path value to top-level content.
|
|
800
|
+
if (extractPaths.length > 0 && !extractKeys && typeof result === "object" && result !== null)
|
|
801
|
+
delete result.content;
|
|
784
802
|
}
|
|
785
803
|
// 读面未知键的非阻断回显:放在投影**之后**,否则会被 --jq 的窄投影吞掉。
|
|
786
804
|
if (ignoredReadKeys.length > 0 && typeof result === "object" && result !== null) {
|
|
@@ -828,8 +846,15 @@ const APP_SCOPE_ARG_KEYS = new Set(["appCode", "applicationCode", "application"]
|
|
|
828
846
|
* 短键(≤5 字符)靠距离算不准,这里先查显式缩写表(与 tool-types 的 KEY_ALIAS_HINTS
|
|
829
847
|
* 同源),命中且参数真实存在才用;否则退回 closestMatches。
|
|
830
848
|
*/
|
|
831
|
-
export function suggestArgumentKey(key, schemaKeys) {
|
|
849
|
+
export function suggestArgumentKey(key, schemaKeys, toolName) {
|
|
832
850
|
const known = new Set(schemaKeys);
|
|
851
|
+
const normalizedTool = toolName?.toLowerCase() ?? "";
|
|
852
|
+
if ((normalizedTool.startsWith("workflow.") || normalizedTool.startsWith("workitem.")) && key === "code" && known.has("workflowCode"))
|
|
853
|
+
return { closest: "workflowCode" };
|
|
854
|
+
if (normalizedTool.startsWith("data.") && key === "id" && known.has("bizObjectId"))
|
|
855
|
+
return { closest: "bizObjectId" };
|
|
856
|
+
if (normalizedTool.startsWith("data.") && key === "ids" && known.has("bizObjectIds"))
|
|
857
|
+
return { closest: "bizObjectIds" };
|
|
833
858
|
const aliases = KEY_ALIAS_HINTS[key.toLowerCase()] ?? [];
|
|
834
859
|
for (const candidate of aliases) {
|
|
835
860
|
if (known.has(candidate))
|
|
@@ -1616,17 +1641,22 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1616
1641
|
return emitThrown(error, transport);
|
|
1617
1642
|
}
|
|
1618
1643
|
}
|
|
1619
|
-
function cmdGraphReferences(kind, code, transport) {
|
|
1644
|
+
function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
1620
1645
|
if (!["service", "model", "field"].includes(kind))
|
|
1621
1646
|
return emitInvalid(transport, { message: "kind 必须是 service / model / field(同 rule.references 第一版边界)" });
|
|
1622
1647
|
try {
|
|
1623
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);
|
|
1624
1651
|
return emitResult({
|
|
1625
1652
|
status: "ok",
|
|
1626
1653
|
kind: result.kind,
|
|
1627
1654
|
code: result.code,
|
|
1628
1655
|
matches: result.matches,
|
|
1629
1656
|
index: result.index,
|
|
1657
|
+
// r35a-F4:覆盖声明必须透传——CLI 这里是白名单式重建,漏加就等于没修
|
|
1658
|
+
// (matches:[] 曾被当成可安全删除的依据)。
|
|
1659
|
+
...(result.coverage !== undefined ? { coverage: result.coverage } : {}),
|
|
1630
1660
|
...(result.note !== undefined ? { note: result.note } : {}),
|
|
1631
1661
|
...(result.gapWarning !== undefined ? { gapWarning: result.gapWarning } : {}),
|
|
1632
1662
|
}, transport);
|
|
@@ -2129,7 +2159,7 @@ async function main() {
|
|
|
2129
2159
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2130
2160
|
});
|
|
2131
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); });
|
|
2132
|
-
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) => {
|
|
2133
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"] : []),
|
|
2134
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"] : [])];
|
|
2135
2165
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
@@ -2412,18 +2442,19 @@ async function main() {
|
|
|
2412
2442
|
console.error(`已清理 ${result.removed} 个读回产物,释放 ${formatBytes(result.freedBytes)};保留 ${result.kept} 个(${formatBytes(result.keptBytes)})`);
|
|
2413
2443
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2414
2444
|
});
|
|
2415
|
-
|
|
2416
|
-
// 语义完全相同。探针先试 `--extract` 拿到 `unknown option`(白花一次往返),
|
|
2417
|
-
// 才想起两者不同名。别名照收,help 里也标出来。
|
|
2418
|
-
artifactCommand.command("get <id>").option("--jq [path]").option("--extract <path>", "同 --jq(与 omc call 的 --extract 同名,省一次命名切换)").option("--path-only").option("--summary").option("--keys [a,b]").option("--full", "整包返回正文(默认:正文超过 8KB 时只回形状 outline + fetch,按需 --jq/--keys 定向取值)").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
2445
|
+
artifactCommand.command("get <id>").addOption(new Option("--jq [path]").argParser((value, previous = []) => [...previous, value]).default([])).option("--extract <path>", "同 --jq(与 omc call 的 --extract 同名,省一次命名切换)").option("--path-only").option("--summary").option("--keys [a,b]").option("--full", "整包返回正文(默认:正文超过 8KB 时只回形状 outline + fetch,按需 --jq/--keys 定向取值)").option("--max-output-bytes <bytes>").action(async (id, options) => {
|
|
2419
2446
|
// A bare `--jq`/`--keys` is a key-inventory request, not a missing argument:
|
|
2420
2447
|
// commander's "argument missing" (invalid-input + retry) is a dead end the
|
|
2421
2448
|
// caller cannot fix without already knowing the flag takes a value (r11
|
|
2422
2449
|
// burned six rounds on `artifact get <id> --jq` before trying `--summary`).
|
|
2423
2450
|
// --extract 是 --jq 的同义别名(r28h F3)。
|
|
2424
|
-
|
|
2451
|
+
// 重复 --jq 的多路径合并为逗号路径透传(r34c-F8:此前静默只留最后一个)。
|
|
2452
|
+
const jqPaths = [
|
|
2453
|
+
...(Array.isArray(options.jq) ? options.jq.filter((v) => typeof v === "string") : []),
|
|
2454
|
+
...(typeof options.extract === "string" ? [options.extract] : []),
|
|
2455
|
+
];
|
|
2425
2456
|
const bareJq = options.jq === true;
|
|
2426
|
-
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(
|
|
2457
|
+
commandExitCode = await withTransport(options, request, (transport) => cmdArtifactGet(id, transport, { ...(jqPaths.length > 0 ? { jq: jqPaths.join(",") } : {}), pathOnly: options.pathOnly === true, summary: options.summary === true || options.keys === true || bareJq,
|
|
2427
2458
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2428
2459
|
});
|
|
2429
2460
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
@@ -2496,8 +2527,8 @@ async function main() {
|
|
|
2496
2527
|
const parsed = options.concurrency === undefined ? undefined : Number.parseInt(String(options.concurrency), 10);
|
|
2497
2528
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphPull(options.environment ?? options.env, parsed, transport));
|
|
2498
2529
|
});
|
|
2499
|
-
graphCommand.command("references <kind> <code>").option("--max-output-bytes <bytes>").addHelpText("after", "\n离线查询(零 wire):kind = service|model|field,语义同 rule.references
|
|
2500
|
-
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));
|
|
2501
2532
|
});
|
|
2502
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) => {
|
|
2503
2534
|
commandExitCode = await withTransport(options, request, (transport) => cmdGraphImpact(kind, code, transport));
|
package/dist/discovery.js
CHANGED
|
@@ -588,7 +588,13 @@ export function describeParam(registry, name, param) {
|
|
|
588
588
|
// r28e 真机(2026-11-05):调用方看到 `status:"ok"` + 一堆键,误以为"参数存在、
|
|
589
589
|
// 只是描述为空"。`found:false` 是机器信号,但人读时需要一句话点明——否则
|
|
590
590
|
// 会照着 `available` 之外的名字继续试。
|
|
591
|
-
const
|
|
591
|
+
const availableKeys = Object.keys(schema.properties ?? {});
|
|
592
|
+
// Prefer a same-tool semantic alias over a cross-tool same-name suggestion.
|
|
593
|
+
// `datarule.create --param type` means the rule discriminator, whose wire
|
|
594
|
+
// name is dataRuleType; recommending custompage.list's `type` sends the
|
|
595
|
+
// caller to the wrong tool family.
|
|
596
|
+
const semantic = name.startsWith("datarule.") && param === "type" && availableKeys.includes("dataRuleType") ? "dataRuleType" : undefined;
|
|
597
|
+
const nearest = semantic ?? closestMatch(param, availableKeys);
|
|
592
598
|
return {
|
|
593
599
|
name: tool.name,
|
|
594
600
|
param,
|
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
|
: {}),
|
package/dist/human-summary.js
CHANGED
|
@@ -167,7 +167,10 @@ function phraseOf(tool, args, outcome) {
|
|
|
167
167
|
case "datarule.create":
|
|
168
168
|
case "datarule.update": {
|
|
169
169
|
const type = DATA_RULE_TYPE_NAMES_ZH[Number(args.dataRuleType)] ?? `类型${String(args.dataRuleType ?? "?")}`;
|
|
170
|
-
|
|
170
|
+
const readbackRule = outcome?.readback?.after ?? outcome?.readback?.before;
|
|
171
|
+
const ruleName = readbackRule && typeof readbackRule === "object" ? stringArg(readbackRule, "name") : undefined;
|
|
172
|
+
const identity = ruleName ?? stringArg(args, "propertyCode") ?? (readbackRule && typeof readbackRule === "object" ? stringArg(readbackRule, "id")?.slice(0, 8) : undefined) ?? "?";
|
|
173
|
+
return `${tool === "datarule.create" ? "创建" : "更新"}字段规则(${identity},${type}),挂在 ${schemaCode ?? "?"}`;
|
|
171
174
|
}
|
|
172
175
|
case "datarule.delete":
|
|
173
176
|
return `删除字段规则 ${stringArg(args, "id") ?? "?"}(${schemaCode ?? "?"})`;
|
|
@@ -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
|
? {
|
|
@@ -17,7 +17,7 @@ export const DATARULE_WIRE = Object.freeze({
|
|
|
17
17
|
delete: "bizdatarule/delete?id=<ruleId> (@RequestParam String id) with list exact-absence readback",
|
|
18
18
|
dataRuleTypes: "DataRuleType: 1 CHECK_RULE (校验), 2 CALCULATE_RULE (计算/拼接), 3 DISPLAY_RULE (显示), 4 OTHER_RULE (必填), 5 READ_ONLY_RULE (只读); 8 EXTENSION_RULE is out of scope",
|
|
19
19
|
dataRuleCheckTypes: "dataRuleType 1 (CHECK_RULE 提交校验规则) 的 checkType 判别(cloudpivot-shared DataRuleCheckType.java)——**校验类需求走这里,不要写在线 JS**:1 REGEX_CHECK 正则校验 / 2 SUBMIT_CHECK 提交校验(含跨字段比较)/ 3 EMPTY_ROW_CHECK 空行校验 / 4 MAX_LENGTH_CHECK 最大长度校验 / 5 REPEAT_CHECK 重复校验;checkType 只配 dataRuleType 1,缺省或非法值由工具拒绝。**数值上下限没有专用类型——用 checkType 2 提交校验表达(如 数量≥0:options 提交校验条件 {quantity}>=0,r21 实证)**",
|
|
20
|
-
optionsByCheckType: "各 checkType 的 options
|
|
20
|
+
optionsByCheckType: "各 checkType 的 options 白名单(工具会拒绝 unknownOptionKeys,避免平台静默忽略;运行版 1.1.25 SubmitCheckValidator/RepeatCheckValidator 反编译实证 2026-11-06):1 REGEX_CHECK{regexp,regexpText};2 SUBMIT_CHECK{conditions,verifyType,prompt}(verifyType 的拒用取值=submitDenied——它是 verifyType 的值,不是独立键);3 EMPTY_ROW_CHECK{isEmptyRow,isEmptyRowTips};4 MAX_LENGTH_CHECK{maxLength};5 REPEAT_CHECK{noRepeat,properties,isSubTableValidationAllData,prompt}。checkType-2 的 prompt 是纯文本(门户 raw 渲染 options.prompt,不做富文本解析)。重复提示逐项/数组解析,RepeatCheckValidator:258-272:isDataItem=0 → code 是字面文案;isDataItem=1 → code 是字段 code。checkType=5 的唯一去重键是 options.noRepeat(配合 properties),平台 REST 也会拦重复;conditions/regexp/maxLength 等只在门户前端提交面执行。",
|
|
21
21
|
optionsByType: "options is a JSON string read per type: 1 check keys (verifyFormula/regexp/maxLength/isEmptyRow/noRepeat/conditions), 2 FormulaConfig — `shortTextStitch` (falling back to `value`) MUST be a JSON OBJECT STRING whose `formula` key holds the expression, e.g. {\"shortTextStitch\":\"{\\\"formula\\\":\\\"{field}\\\",\\\"editorText\\\":\\\"{字段名}\\\"}\"}; a bare \"{field}\" string fails JSONUtil.isJson → getCalRuleExpression returns null → false 305003 dead-cycle; `formulaType:\"AGGREGATION\"` instead reads `aggregation`; a top-level `expression` is NEVER read (it is only a key INSIDE that nested JSON), 3 displayFormula, 4 requiredFormula, 5 readonlyCondition",
|
|
22
22
|
formulaReference: "类型2 公式引用按位置解析:裸 {字段}=**主记录**字段;带点 {子表code.字段}=**子表**字段(FormulaProcessor.constructSubExprParamsValueMap 要求点前等于子表 schema)。所以**主表公式**用裸 {字段}(带 {主表code.字段} 会按子表解析 → 值空/静默 null),**子表公式**(childRule:true)引用同级子字段必须带点 {子表code.字段}(裸引用被当主记录字段 → 值缺失 → 提交 50000)。工具在写前按子表已发布字段判定并拒绝裸引用(blocker datarule-formula-child-bare-reference),读回对存量子表公式加 formulaHint 提示复核。",
|
|
23
23
|
optionsEncoding: "the engine JSON-parses the `options` column and reads requiredFormula / readonlyCondition / displayFormula / verifyFormula / shortTextStitch (whose VALUE is itself a JSON string carrying `formula`); send options as a stringified object",
|
|
@@ -85,6 +85,7 @@ export const DATARULE_LANE_CONTRACT = Object.freeze({
|
|
|
85
85
|
scope: DATARULE_SCOPE,
|
|
86
86
|
description: describeTool("Create a field-level data rule (bizdatarule/create) and read it back from the authoritative list. **表单运行时条件优先 `field.update`**(本工具=字段级引擎/设计器读面)。", [
|
|
87
87
|
"`dataRuleType` → options 键:1→conditions/prompt/regexp/maxLength(+checkType);2→shortTextStitch|expression;3→displayFormula;4→requiredFormula(无条件必填=`\"true\"`);5→readonlyCondition。8 EXTENSION out of scope.",
|
|
88
|
+
"`childRule:true` 仅用于已发布的子表列(propertyCode/propertyCodes 必须是子表列 code;主表字段省略)。子表列在发布面不可见时先用 field.create + field.publish 生成列,不要直接 POST datarule.create(r34a-F12 真机:childRule 三种形态全被发布面校验拒)。",
|
|
88
89
|
"**⚠️ type3/4/5 行的两个读面**:门户**运行时**读**字段 options**(`requiredFormula`/`readonlyCondition`/`readonlyFormula`/`displayFormula`,`field.create/update` 写);表单设计器**设计态** Set/Unset 读 **bizdatarule/list**(本行,network capture 2026-09-23)。**实测:type3/4/5 规则单独不会让门户表单控件变必填/只读**(中文或符号操作符都一样),符号式经 `historyOpt` 镜像还会让表单渲染崩溃——运行时条件请用中文操作符。表单条件优先走 `field.update`:它会**自动配对**本行(readback.formConditionSync)。本工具用于字段级引擎规则或独立增删改。",
|
|
89
90
|
"**类型3/4/5 条件串协议(平台 create 不校验,坏形状落库即雷,工具按此协议拒绝)**:`{字段code} 操作符 [值]`。操作符取 **bizdatarule 词表**(与字段 options 的表单交互引擎不同——那是符号式):**中文** 等于/不等于/大于/小于/大于等于/小于等于/包含/不包含/包含全部/不包含全部/包含任意一个/不包含任意一个/等于任意一个/属于/不属于/介于/为空/不为空(实测 146 条真实规则以此为主),也接受符号 `==` `!=` `>` `<` `>=` `<=` `Contains` `IsEmpty` 等。为空/不为空**不带值**(`{field} 不为空`);其余带值(JSON 对象或标量)。多条件 ` && `、多组 ` || `。示例 type4:`options:{requiredFormula:\"{amount} 等于 {\\\"valueType\\\":\\\"specifyOption\\\",\\\"value\\\":\\\"A\\\"}\"}`;无条件必填用字面量 \"true\"。**字段绑定用 `propertyCodes`(数组)**,工具自动把 `options.propertyCodes` 也补上;存量的坏规则由 datarule.list 以 shapeWarning 标出。",
|
|
90
91
|
"`options` is a JSON object sent to the wire as a JSON string; the engine reads `requiredFormula` (4), `readonlyCondition` (5), `displayFormula` (3), `verifyFormula` (1) and `shortTextStitch` (2).",
|
package/dist/lanes/datarule.js
CHANGED
|
@@ -50,6 +50,15 @@ export const DATARULE_CHECK_TYPES = Object.freeze({
|
|
|
50
50
|
4: "MAX_LENGTH_CHECK 最大长度校验(options.maxLength)",
|
|
51
51
|
5: "REPEAT_CHECK 重复校验(options.properties 数组 + isSubTableValidationAllData + prompt)",
|
|
52
52
|
});
|
|
53
|
+
/** Options the 8.6.26 engine actually reads for each data-rule discriminator. */
|
|
54
|
+
export const DATARULE_CHECK_OPTION_KEYS = Object.freeze({
|
|
55
|
+
1: Object.freeze(["regexp", "regexpText"]),
|
|
56
|
+
2: Object.freeze(["conditions", "verifyType", "prompt"]),
|
|
57
|
+
3: Object.freeze(["isEmptyRow", "isEmptyRowTips"]),
|
|
58
|
+
4: Object.freeze(["maxLength"]),
|
|
59
|
+
5: Object.freeze(["noRepeat", "properties", "isSubTableValidationAllData", "prompt"]),
|
|
60
|
+
});
|
|
61
|
+
const DATARULE_CALCULATE_MIRROR_KEYS = ["formulaType", "resultType"];
|
|
53
62
|
const checkTypeArg = z
|
|
54
63
|
.number()
|
|
55
64
|
.int()
|
|
@@ -120,6 +129,16 @@ function decodeDataRule(row) {
|
|
|
120
129
|
sortKey: row.sortKey ?? null,
|
|
121
130
|
options,
|
|
122
131
|
};
|
|
132
|
+
const optionKeys = effectiveOptionKeys(Number(row.dataRuleType), Number(row.checkType));
|
|
133
|
+
if (options !== null && typeof options === "object" && !Array.isArray(options)) {
|
|
134
|
+
const storedKeys = Object.keys(options).filter((key) => key !== "propertyCodes");
|
|
135
|
+
decoded.effectiveKeys = storedKeys.filter((key) => optionKeys.includes(key));
|
|
136
|
+
decoded.ignoredKeys = storedKeys.filter((key) => !optionKeys.includes(key));
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
decoded.effectiveKeys = [];
|
|
140
|
+
decoded.ignoredKeys = [];
|
|
141
|
+
}
|
|
123
142
|
// Round 4 (ADR 0005): read-side annotation. The platform accepted and stored
|
|
124
143
|
// malformed option shapes for years; the same protocol validator that gates
|
|
125
144
|
// writes marks stored landmines here. Report-only — stored data is never
|
|
@@ -159,6 +178,21 @@ function decodeDataRule(row) {
|
|
|
159
178
|
}
|
|
160
179
|
return decoded;
|
|
161
180
|
}
|
|
181
|
+
function effectiveOptionKeys(dataRuleType, checkType) {
|
|
182
|
+
if (dataRuleType === 1) {
|
|
183
|
+
if (checkType !== undefined)
|
|
184
|
+
return DATARULE_CHECK_OPTION_KEYS[checkType] ?? [];
|
|
185
|
+
return [...new Set(Object.values(DATARULE_CHECK_OPTION_KEYS).flat())];
|
|
186
|
+
}
|
|
187
|
+
return dataRuleType === 2 ? [...(DATARULE_TYPE_OPTION_KEYS[2] ?? []), ...DATARULE_CALCULATE_MIRROR_KEYS] : DATARULE_TYPE_OPTION_KEYS[dataRuleType] ?? [];
|
|
188
|
+
}
|
|
189
|
+
function dataRuleExecutionWarning(checkType) {
|
|
190
|
+
if (checkType === 5)
|
|
191
|
+
return "重复校验 noRepeat 由服务端拦截,REST `data.submit` 亦会拒绝重复数据(errcode 800150006);properties 指定参与去重的字段。";
|
|
192
|
+
if (checkType === 1 || checkType === 2 || checkType === 3 || checkType === 4)
|
|
193
|
+
return "该校验规则仅由门户前端提交校验执行;REST `data.submit` 不拦截违规数据。需要服务端拦截请改用业务规则(BIZ_ACTION/条件)。";
|
|
194
|
+
return "校验规则的执行面取决于 checkType;请在门户提交面验收,未提供 checkType 时不要把 REST 行为视为已验证。";
|
|
195
|
+
}
|
|
162
196
|
/**
|
|
163
197
|
* Fail closed when the options object cannot carry the key the engine reads
|
|
164
198
|
* for this type (RequiredDataRuleValidator / ReadOnlyValidator /
|
|
@@ -354,6 +388,20 @@ export function assertDataRuleOptions(dataRuleType, options, checkType) {
|
|
|
354
388
|
message: `dataRuleType ${dataRuleType} requires at least one of ${required.join(", ")} in options (those are the keys the 8.6.26 engine reads)`,
|
|
355
389
|
});
|
|
356
390
|
}
|
|
391
|
+
const ignored = Object.keys(options).filter((key) => key !== "propertyCodes" && !effectiveOptionKeys(dataRuleType, checkType).includes(key));
|
|
392
|
+
if (ignored.length > 0) {
|
|
393
|
+
const allowed = effectiveOptionKeys(dataRuleType, checkType);
|
|
394
|
+
const checkLabel = dataRuleType === 1 ? `checkType=${checkType ?? "?"}` : `dataRuleType=${dataRuleType}`;
|
|
395
|
+
throw new ToolRefusalError({
|
|
396
|
+
status: "refused",
|
|
397
|
+
blocker: "datarule-unknown-option-keys",
|
|
398
|
+
dataRuleType,
|
|
399
|
+
...(checkType === undefined ? {} : { checkType }),
|
|
400
|
+
unknownOptionKeys: ignored,
|
|
401
|
+
effectiveKeys: allowed,
|
|
402
|
+
message: `${checkLabel} 不读取 options 键:${ignored.join(", ")};本类型有效键:${allowed.join(", ") || "(无)"}。平台会静默忽略这些键,拒绝写入以避免误以为规则生效`,
|
|
403
|
+
});
|
|
404
|
+
}
|
|
357
405
|
// Round 4 (ADR 0005): types 3/4/5 gate on the reproduced condition protocol.
|
|
358
406
|
// The platform itself does not validate these shapes at create time, so an
|
|
359
407
|
// unguarded write stores a portal-renderer landmine that only fires when a
|
|
@@ -818,7 +866,7 @@ export function registerDataRuleLane(tools) {
|
|
|
818
866
|
dataRuleType: dataRuleTypeArg,
|
|
819
867
|
options: dataRuleOptionsArg,
|
|
820
868
|
checkType: checkTypeArg,
|
|
821
|
-
childRule: z.boolean().optional(),
|
|
869
|
+
childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
|
|
822
870
|
name: z.string().optional(),
|
|
823
871
|
sortKey: z.number().int().optional(),
|
|
824
872
|
},
|
|
@@ -832,20 +880,21 @@ export function registerDataRuleLane(tools) {
|
|
|
832
880
|
if (published !== null) {
|
|
833
881
|
const missing = codes.filter((code) => !published.has(code));
|
|
834
882
|
if (missing.length > 0)
|
|
835
|
-
throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST
|
|
883
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-property-not-published", schemaCode: args.schemaCode, propertyCodes: codes, missing, message: `字段 ${args.schemaCode}.${missing.join(",")} 不在发布面上;拒绝 datarule.create POST。子表列规则请随 field.create 生成/发布;datarule.create 的发布面校验不含子表列(childRule 不改变此行为,r34a-F12 真机三形态全拒)` });
|
|
836
884
|
}
|
|
837
885
|
const existingRules = await listDataRules(session, args.schemaCode);
|
|
838
886
|
// dataRuleType 1 (CHECK_RULE) fans out into 5 checkTypes that COEXIST on one
|
|
839
887
|
// field (the platform auto-creates a checkType 4 maxLength row per text
|
|
840
888
|
// field), so the clash key there is field + `checkType`, not field + type.
|
|
841
|
-
|
|
889
|
+
const duplicate = existingRules.find((row) => {
|
|
842
890
|
if (Number(row.dataRuleType) !== args.dataRuleType)
|
|
843
891
|
return false;
|
|
844
892
|
if (!storedTargetCodes(row).some((code) => codes.includes(code)))
|
|
845
893
|
return false;
|
|
846
894
|
return args.dataRuleType === 1 ? (row.checkType ?? null) === (args.checkType ?? null) : true;
|
|
847
|
-
})
|
|
848
|
-
|
|
895
|
+
});
|
|
896
|
+
if (duplicate)
|
|
897
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", schemaCode: args.schemaCode, propertyCodes: codes, dataRuleType: args.dataRuleType, ...(args.dataRuleType === 1 ? { checkType: args.checkType } : {}), nextCommand: `omc call datarule.update --args '${JSON.stringify({ id: String(duplicate.id ?? "<existingId>"), schemaCode: args.schemaCode, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes.length > 1 ? codes : undefined, dataRuleType: args.dataRuleType, checkType: args.checkType, options: args.options })}'`, message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝重复 POST;如需修改请使用该规则 id 更新` });
|
|
849
898
|
const createRule = () => wireCall(session, {
|
|
850
899
|
path: ENDPOINTS.dataRuleCreate,
|
|
851
900
|
method: "POST",
|
|
@@ -872,7 +921,7 @@ export function registerDataRuleLane(tools) {
|
|
|
872
921
|
// 提交违规数据(-50 / 超阈值)errcode 0 放行、原值落库——校验在门户前端
|
|
873
922
|
// 提交管线执行,REST 不拦。先告知执行面,省掉「先实测两轮才发现不生效」。
|
|
874
923
|
...(args.dataRuleType === 1
|
|
875
|
-
? { executionWarning:
|
|
924
|
+
? { executionWarning: dataRuleExecutionWarning(args.checkType) }
|
|
876
925
|
: {}),
|
|
877
926
|
...(() => { const w = dataRuleFormulaWarnings(args.dataRuleType, args.options); return w.length ? { lintWarnings: w } : {}; })(),
|
|
878
927
|
},
|
|
@@ -906,7 +955,7 @@ export function registerDataRuleLane(tools) {
|
|
|
906
955
|
dataRuleType: dataRuleTypeArg,
|
|
907
956
|
options: dataRuleOptionsArg,
|
|
908
957
|
checkType: checkTypeArg,
|
|
909
|
-
childRule: z.boolean().optional(),
|
|
958
|
+
childRule: z.boolean().optional().describe("true=子表列规则,propertyCode 为子表列 code;主表字段忽略"),
|
|
910
959
|
name: z.string().optional(),
|
|
911
960
|
sortKey: z.number().int().optional(),
|
|
912
961
|
},
|
|
@@ -926,7 +975,7 @@ export function registerDataRuleLane(tools) {
|
|
|
926
975
|
const requested = dataRuleTargetCodes(patchArgs);
|
|
927
976
|
const codes = requested.length > 0 ? requested : storedTargetCodes(current);
|
|
928
977
|
const boundArgs = { ...patchArgs, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes };
|
|
929
|
-
|
|
978
|
+
const duplicate = before.find((row) => {
|
|
930
979
|
if (row.id === id)
|
|
931
980
|
return false;
|
|
932
981
|
if (Number(row.dataRuleType) !== args.dataRuleType)
|
|
@@ -934,8 +983,9 @@ export function registerDataRuleLane(tools) {
|
|
|
934
983
|
if (!storedTargetCodes(row).some((code) => codes.includes(code)))
|
|
935
984
|
return false;
|
|
936
985
|
return args.dataRuleType === 1 ? (row.checkType ?? null) === (args.checkType ?? null) : true;
|
|
937
|
-
})
|
|
938
|
-
|
|
986
|
+
});
|
|
987
|
+
if (duplicate)
|
|
988
|
+
throw new ToolRefusalError({ status: "refused", blocker: "datarule-duplicate-property-type", nextCommand: `omc call datarule.update --args '${JSON.stringify({ id: String(duplicate.id ?? "<existingId>"), schemaCode: args.schemaCode, propertyCode: codes.length === 1 ? codes[0] : undefined, propertyCodes: codes.length > 1 ? codes : undefined, dataRuleType: args.dataRuleType, checkType: args.checkType, options: args.options })}'`, message: `字段 ${args.schemaCode}.${codes.join(",")} 已存在同类型数据规则${args.dataRuleType === 1 ? `(checkType ${args.checkType})` : ""};拒绝 update POST;如需修改请使用该规则 id 更新` });
|
|
939
989
|
const beforeDecoded = decodeDataRule(current);
|
|
940
990
|
const performed = await wireCall(session, {
|
|
941
991
|
path: ENDPOINTS.dataRuleUpdate,
|
|
@@ -172,7 +172,7 @@ export const FIELD_LANE_CONTRACT = Object.freeze({
|
|
|
172
172
|
"For single/multi-select pass `choices` (e.g. [\"A\",\"B\",\"C\"]) or `options.optionsSet` with optionsType custom/dictionary/businessModel; dictionary uses checkedDictionary/checkedDictionaryCode, businessModel uses schemaCode/sheetDataItem.",
|
|
173
173
|
"For a relation pass `relativeCode` and `options` (schemaCode/displayField/...). `searchFormula` and `conditions` are JSON strings for the relation data restriction/query conditions.",
|
|
174
174
|
"For a relation display field the tool writes the field-row top-level `relativePropertyCode` = options.displayField (the runtime reads THAT key, not options.displayField — QueryRuntimeController.java:1214-1226) and defaults options.displayFieldType:0 + displayFieldFormat:\"\".",
|
|
175
|
-
"For FORMULA pass `options.formulaConfig` (nested JSON string) + `options.persistent:true`; for RELEVANCE_DATA pass `options.relevanceField` + `options.referenceField` + `options.referenceFieldPropertyType`.",
|
|
175
|
+
"For FORMULA pass `options.formulaConfig` (nested JSON string) + `options.persistent:true`; for RELEVANCE_DATA pass `options.relevanceField` + `options.referenceField` + `options.referenceFieldPropertyType`. 数值字段的小数位写 `options.scale`;顶层 `precision` 不被读取,若误传请改为 `precision→options.scale(数值小数位)`。",
|
|
176
176
|
"org 选人族(STAFF_SELECTOR 等 5/50/51/60/61)字段行 options 自动写成实例观测的发布安全形状(yst/fwb 发布面 2656 行:null 或 {defaultValue, mappings, virtualField[, noRepeat]};单选 defaultValue \"\",多选 [])。deptVisible/multi 是**表单控件面**判别键(disposeUpdateType/updateSelectionType 读 control.options,票 69 已由表单脚手架写入控件面),不出现在字段行。",
|
|
177
177
|
]),
|
|
178
178
|
}),
|
|
@@ -217,7 +217,7 @@ export function assertFieldContract() {
|
|
|
217
217
|
throw new Error("field contract must carry the blocked families");
|
|
218
218
|
if (FIELD_LANE_CONTRACT.wire.publishScope !== "model")
|
|
219
219
|
throw new Error("field contract pins the publish scope to model");
|
|
220
|
-
assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.create", ["bizproperty/create", "OCR/ELECTRONIC_SIGN", "`choices`", "options.optionsSet", "`relativeCode`", "searchFormula", "`options.formulaConfig`", "`options.relevanceField`", "code_reserved", "WORK_SHEET"]);
|
|
220
|
+
assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.create", ["bizproperty/create", "OCR/ELECTRONIC_SIGN", "`choices`", "options.optionsSet", "`relativeCode`", "searchFormula", "`options.formulaConfig`", "`options.relevanceField`", "precision→options.scale", "code_reserved", "WORK_SHEET"]);
|
|
221
221
|
assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.update", ["bizproperty/update", "`options`", "field.publish", "cascadeRepublish", "form.draft(rebuild)", "form.publish(force)", "published fields", "non-structural", "partial"]);
|
|
222
222
|
assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.publish", ["bizproperty/publish", "published", "Model-scoped", "publishedCodes", "unpublishedRequested", "nonexistentCodes", "stillDraftCodes"]);
|
|
223
223
|
assertDescriptionCarries(FIELD_LANE_CONTRACT, "field.delete", ["bizproperty/force_delete", "absence"]);
|
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
|
},
|
|
@@ -392,19 +392,36 @@ export function registerOrganizationLane(tools) {
|
|
|
392
392
|
return scope.id;
|
|
393
393
|
return null;
|
|
394
394
|
};
|
|
395
|
-
const members = rows.map((row) =>
|
|
396
|
-
id
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
395
|
+
const members = await Promise.all(rows.map(async (row) => {
|
|
396
|
+
const id = roleUserId(row);
|
|
397
|
+
let username = row.username ?? null;
|
|
398
|
+
if (username == null && id !== "") {
|
|
399
|
+
try {
|
|
400
|
+
const user = (await wireCall(session, {
|
|
401
|
+
path: ENDPOINTS.organizationUserInfo,
|
|
402
|
+
query: { ...orgQuery(session), id },
|
|
403
|
+
})).data;
|
|
404
|
+
username = user?.username ?? null;
|
|
405
|
+
}
|
|
406
|
+
catch (error) {
|
|
407
|
+
if (!(error instanceof WireError) || error.errcode !== 10011)
|
|
408
|
+
throw error;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
return {
|
|
412
|
+
id,
|
|
413
|
+
// r32 探针:把角色 id 当成员 id 传给 userScope/addUsers → 查询为空。
|
|
414
|
+
// 明示这个 id 的身份与正确来源,防角色/用户 id 混用。
|
|
415
|
+
idType: "user",
|
|
416
|
+
name: row.name ?? null,
|
|
417
|
+
username,
|
|
418
|
+
departmentId: memberDepartment(row),
|
|
419
|
+
departmentName: row.departmentName ?? null,
|
|
420
|
+
// `ouScope` decides whether a role member can be resolved as a workflow
|
|
421
|
+
// participant. Surface the count so an empty scope is visible here.
|
|
422
|
+
scopeCount: Array.isArray(row.ouScope) ? row.ouScope.length : null,
|
|
423
|
+
unitType: row.unitType ?? null,
|
|
424
|
+
};
|
|
408
425
|
}));
|
|
409
426
|
return {
|
|
410
427
|
roleId,
|
|
@@ -426,7 +443,7 @@ export function registerOrganizationLane(tools) {
|
|
|
426
443
|
environment: environmentArg,
|
|
427
444
|
roleId: z.string().min(1).optional().describe("角色 id(32 位 hex);与 roleCode 二选一"),
|
|
428
445
|
roleCode: z.string().min(1).optional().describe("角色 code(org.role.list 的 roles 码表)或名称;与 roleId 二选一"),
|
|
429
|
-
userId: z.string().min(1).describe("成员的用户记录 id(recId
|
|
446
|
+
userId: z.string().min(1).optional().describe("成员的用户记录 id(recId);roleId/roleCode 二者其一必填"),
|
|
430
447
|
capability: capabilityArg,
|
|
431
448
|
},
|
|
432
449
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
@@ -976,8 +993,20 @@ export function registerOrganizationLane(tools) {
|
|
|
976
993
|
if (!(error instanceof WireError) || error.errcode !== 10011)
|
|
977
994
|
throw error;
|
|
978
995
|
}
|
|
979
|
-
|
|
980
|
-
|
|
996
|
+
// r34b-F6(真机 2026-11-06,1.1.25):死 id 平台**不回 null 也不回 10011**,
|
|
997
|
+
// 而是 200 + 空壳对象——`!data` 分支真机永远走不到,exists:true + 全 null
|
|
998
|
+
// 把「不存在」伪装成「存在但没数据」。真机形状:name/username/mobile/email/
|
|
999
|
+
// departmentId/status 全空且 roleIds 无。全身份字段空 = 按 exists:false 处理。
|
|
1000
|
+
const emptyShell = data !== null && [
|
|
1001
|
+
data.name, data.username, data.mobile, data.email, data.departmentId, data.status,
|
|
1002
|
+
].every((v) => v == null || v === "") && (!Array.isArray(data.roleIds) || data.roleIds.length === 0);
|
|
1003
|
+
if (!data || emptyShell)
|
|
1004
|
+
return {
|
|
1005
|
+
userId: String(args.userId), exists: false,
|
|
1006
|
+
existsNote: emptyShell ? "平台回 200 空壳(身份字段全空)——按不存在处理(r34b-F6 真机)" : undefined,
|
|
1007
|
+
name: null, username: null, departmentId: null, mobile: null, email: null,
|
|
1008
|
+
active: null, userStatus: null, roleIds: [],
|
|
1009
|
+
};
|
|
981
1010
|
return {
|
|
982
1011
|
userId: String(args.userId), exists: true,
|
|
983
1012
|
name: data.name ?? null, username: data.username ?? null, departmentId: data.departmentId ?? null,
|
|
@@ -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
|
|
@@ -3199,13 +3217,13 @@ export function registerWorkflowLane(tools) {
|
|
|
3199
3217
|
total: page.total,
|
|
3200
3218
|
// The JPA-spec path reports no page count; only emit it when known.
|
|
3201
3219
|
...("totalPages" in page && page.totalPages !== undefined ? { totalPages: page.totalPages } : {}),
|
|
3202
|
-
|
|
3220
|
+
itemsCount: items.length,
|
|
3203
3221
|
...(purposeHint ? { purposeHint } : {}),
|
|
3204
3222
|
...(args.participant !== undefined ? { participant: args.participant, state: args.state ?? "PENDING", source: "list_workitems" } : {}),
|
|
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",
|
|
@@ -3678,6 +3696,12 @@ export function registerWorkflowLane(tools) {
|
|
|
3678
3696
|
};
|
|
3679
3697
|
}
|
|
3680
3698
|
return flat;
|
|
3699
|
+
}, {
|
|
3700
|
+
compact: (result) => ({
|
|
3701
|
+
...result,
|
|
3702
|
+
roles: Array.isArray(result.roles) ? result.roles : [],
|
|
3703
|
+
groups: Array.isArray(result.groups) ? result.groups : [],
|
|
3704
|
+
}),
|
|
3681
3705
|
}),
|
|
3682
3706
|
}));
|
|
3683
3707
|
tools.push(defineTool({
|
|
@@ -3729,7 +3753,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3729
3753
|
kind: "read",
|
|
3730
3754
|
lane: WORKFLOW_LANE,
|
|
3731
3755
|
scope: WORKFLOW_TOOLS["org.user.search"].scope,
|
|
3732
|
-
description: WORKFLOW_TOOLS["org.user.search"].description
|
|
3756
|
+
description: `${WORKFLOW_TOOLS["org.user.search"].description} 返回 count=本页 users 条数、total=总命中数;没有本页结果时两者均为 0。`,
|
|
3733
3757
|
inputSchema: {
|
|
3734
3758
|
environment: environmentArg,
|
|
3735
3759
|
// `wd` is the platform's own key; `keyword`/`q` are accepted aliases so a
|
|
@@ -3826,7 +3850,8 @@ export function registerWorkflowLane(tools) {
|
|
|
3826
3850
|
corpId: user.corpId ?? null,
|
|
3827
3851
|
status: user.status ?? null,
|
|
3828
3852
|
}));
|
|
3829
|
-
|
|
3853
|
+
const totalHits = users.length === 0 ? 0 : (typeof data?.totalElements === "number" ? data.totalElements : users.length);
|
|
3854
|
+
return { keyword, count: users.length, total: totalHits, users };
|
|
3830
3855
|
}, { compact: userSearchCompact });
|
|
3831
3856
|
},
|
|
3832
3857
|
}));
|
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
|
@@ -152,6 +152,41 @@ export const KEY_ALIAS_HINTS = Object.freeze({
|
|
|
152
152
|
order: ["orders"],
|
|
153
153
|
orderby: ["orders"],
|
|
154
154
|
});
|
|
155
|
+
/** Semantic directions for keys whose wire name is not an edit-distance match. */
|
|
156
|
+
export const KEY_SEMANTIC_HINTS = Object.freeze({
|
|
157
|
+
precision: "options.scale(数值小数位)",
|
|
158
|
+
});
|
|
159
|
+
/**
|
|
160
|
+
* r34a-F18(batch2 挂账收尾,2026-11-06 真机):`data.delete` 传 `id` 时写面
|
|
161
|
+
* preflight 建议到无关的 `requestId`(编辑距离子串命中)。读面路径(cli.ts 的
|
|
162
|
+
* suggestArgumentKey)已按工具族给 `id→bizObjectId`,写面必须同一语义,否则
|
|
163
|
+
* 两条通道互相矛盾。**语境而非全局**:`id` 在多数工具是合法键,不能进
|
|
164
|
+
* KEY_SEMANTIC_HINTS(那是无条件的)。
|
|
165
|
+
*/
|
|
166
|
+
function contextualClosestKey(toolName, key, schemaKeys) {
|
|
167
|
+
const t = toolName.toLowerCase();
|
|
168
|
+
if (t.startsWith("data.") && key === "id" && schemaKeys.has("bizObjectId"))
|
|
169
|
+
return "bizObjectId";
|
|
170
|
+
if (t.startsWith("data.") && key === "ids" && schemaKeys.has("bizObjectIds"))
|
|
171
|
+
return "bizObjectIds";
|
|
172
|
+
return undefined;
|
|
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
|
+
});
|
|
155
190
|
function closestSchemaKey(key, schemaKeys) {
|
|
156
191
|
if (schemaKeys.has(key))
|
|
157
192
|
return key;
|
|
@@ -198,7 +233,9 @@ export function parseToolInput(tool, input) {
|
|
|
198
233
|
const schemaKeys = new Set(Object.keys(tool.inputSchema));
|
|
199
234
|
const unknown = Object.keys(normalized).filter((key) => !schemaKeys.has(key));
|
|
200
235
|
if (unknown.length > 0) {
|
|
201
|
-
const suggestions = unknown.map((key) => `${key}→${closestSchemaKey(key, schemaKeys) ?? "(无相近键,omc describe <tool> 看参数表)"}`);
|
|
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);
|
|
202
239
|
// r25 实证:`publish→(无相近键)` 等于没说——配置与发布是两步,用户要的是
|
|
203
240
|
// 另一个工具。有语义指引时优先给可执行的下一步。
|
|
204
241
|
const twoStep = unknown
|
|
@@ -209,24 +246,27 @@ export function parseToolInput(tool, input) {
|
|
|
209
246
|
// 漏掉发布指引(报告原文:「若用程序消费回执,会选 issues[],就会漏掉」)。
|
|
210
247
|
// 两条通道必须同源,否则等于修了一半。
|
|
211
248
|
const issueHintFor = (key) => {
|
|
212
|
-
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key];
|
|
249
|
+
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key] ?? CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key];
|
|
213
250
|
if (guidance !== undefined)
|
|
214
251
|
return guidance;
|
|
252
|
+
const semantic = KEY_SEMANTIC_HINTS[key];
|
|
253
|
+
const contextual = contextualClosestKey(tool.name, key, schemaKeys);
|
|
215
254
|
const closest = closestSchemaKey(key, schemaKeys);
|
|
216
255
|
// 语言规则(HANDOFF §三.3-8):人读句子中文、机器字段/键名英文。issues[]
|
|
217
256
|
// 与人读 message 同源(B4),所以这里也是中文句子 + 英文键名。
|
|
218
|
-
return closest === undefined ? "未知键;本工具的合法键里没有相近的" : `未知键;最接近的合法键:${closest}`;
|
|
257
|
+
return semantic !== undefined ? `未知键;语义上应改用:${semantic}` : contextual !== undefined ? `未知键;本工具语境最接近的合法键:${contextual}` : closest === undefined ? "未知键;本工具的合法键里没有相近的" : `未知键;最接近的合法键:${closest}`;
|
|
219
258
|
};
|
|
220
259
|
// 未知键的下一步应指向**正确的那个参数**,不是整份 describe(4–17 KB)。
|
|
221
260
|
// 有近邻时 `--param <nearKey>` 是 150–900 B,且直接回答「本来想写哪个键」。
|
|
222
|
-
|
|
261
|
+
// 语境建议优先于编辑距离(r34a-F18:data.delete 的 id 指 requestId 是噪声)。
|
|
262
|
+
const nearestKey = contextualClosestKey(tool.name, unknown[0], schemaKeys) ?? closestSchemaKey(unknown[0], schemaKeys);
|
|
223
263
|
const paramCommand = nearestKey !== undefined ? `omc describe ${tool.name} --param ${nearestKey}` : `omc describe ${tool.name}`;
|
|
224
264
|
throw new ToolInputError({
|
|
225
265
|
status: "error",
|
|
226
266
|
blocker: "tool-input-unknown-keys",
|
|
227
267
|
tool: tool.name,
|
|
228
|
-
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : `最接近的合法键:${suggestions.join(";")}。参数表:${paramCommand}`}`,
|
|
229
|
-
...(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 }),
|
|
230
270
|
issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: issueHintFor(key) })),
|
|
231
271
|
});
|
|
232
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
|
}
|