@stalfh233/omc-cli 0.6.19 → 0.6.21
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/cli-contract.js +11 -0
- package/dist/cli.js +23 -5
- package/dist/config-store.js +20 -1
- package/dist/lanes/online-js.js +26 -5
- package/dist/lanes/runtime-data-contract.js +1 -1
- package/dist/lanes/runtime-data.js +34 -17
- package/dist/lanes/workflow.js +8 -0
- package/dist/mcp-graph.js +108 -18
- package/dist/tool-types.js +14 -2
- package/package.json +3 -3
- package/skills/omc-capabilities.md +1 -0
- package/skills/omc-config-draft-review.md +2 -3
- package/skills/omc-glossary.md +2 -0
- package/skills/omc-playbook.md +5 -3
- package/skills/omc-refusals.md +5 -0
- package/skills/omc-start-here.md +7 -2
- package/skills/omc-write-hazards.md +9 -2
package/dist/cli-contract.js
CHANGED
|
@@ -386,6 +386,17 @@ export function classifyResult(result) {
|
|
|
386
386
|
const nextCommand = nextCommandFor(result) ?? (auth ? undefined : "omc help omc-refusals");
|
|
387
387
|
return { status: route.envelopeStatus, error: nextCommand === undefined ? error : { ...error, nextCommand }, exitCode: auth ? CLI_EXIT_CODES.authConfig : CLI_EXIT_CODES.policyRefused };
|
|
388
388
|
}
|
|
389
|
+
// Cross-project environment isolation is a local policy gate. It is emitted
|
|
390
|
+
// as an error-shaped result by the call seam so ordinary ConfigurationError
|
|
391
|
+
// failures (unknown env, missing credentials, malformed config) retain their
|
|
392
|
+
// historical classification.
|
|
393
|
+
if (result.blocker === "cross-environment-policy" || /^跨环境访问被拒绝:/u.test(String(result.message ?? ""))) {
|
|
394
|
+
return {
|
|
395
|
+
status: "error",
|
|
396
|
+
error: withNext({ code: "policy-refused", message: String(result.message ?? "cross-environment access refused"), retryable: false }),
|
|
397
|
+
exitCode: CLI_EXIT_CODES.policyRefused,
|
|
398
|
+
};
|
|
399
|
+
}
|
|
389
400
|
// r25 沙盒(2026-11-04,B1 的同类):`meta.get_status` 返回 `status:"failed"`、
|
|
390
401
|
// `rollback apply` 返回 `status:"stopped"`——两个都曾掉进 internal(exit 15):
|
|
391
402
|
// 调用方看到「CLI 内部故障」,其实是「配置/凭据读回失败」或「恢复链在 N 步停下」。
|
package/dist/cli.js
CHANGED
|
@@ -802,6 +802,24 @@ async function cmdCall(store, args) {
|
|
|
802
802
|
// Do not also copy the single-path value to top-level content.
|
|
803
803
|
if (extractPaths.length > 0 && !extractKeys && typeof result === "object" && result !== null)
|
|
804
804
|
delete result.content;
|
|
805
|
+
// r40a: unsupported jq language is an input error, never an `ok:true`
|
|
806
|
+
// projection with a null value. Keep the path engine's detailed hint but
|
|
807
|
+
// make the CLI contract actionable and machine-classifiable.
|
|
808
|
+
if (typeof result === "object" && result !== null && Array.isArray(result.extract)) {
|
|
809
|
+
for (const entry of result.extract)
|
|
810
|
+
delete entry.autoStripped;
|
|
811
|
+
const unsupported = result.extract.filter((entry) => entry.unsupportedSyntax === true);
|
|
812
|
+
if (unsupported.length > 0) {
|
|
813
|
+
const first = unsupported[0];
|
|
814
|
+
const message = `--jq 只接受路径选择器,不支持对象构造或管道语法(${String(first?.path ?? "")})。可运行示例:--jq 'fields[].code,fields[].typeName'`;
|
|
815
|
+
const invalid = { status: "error", blocker: "tool-input-invalid", unsupportedSyntax: true, paths: unsupported.map((entry) => entry.path), message, nextCommand: "omc call <tool> --args '<json>' --jq 'fields[].code,fields[].typeName'" };
|
|
816
|
+
return emit("error", invalid, CLI_EXIT_CODES.invalidInput);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
// `extract[]` is the canonical multi-path result. Do not emit the second
|
|
820
|
+
// row-alignment copy: callers can align the explicitly named path values.
|
|
821
|
+
if (typeof result === "object" && result !== null && extractPaths.length > 1)
|
|
822
|
+
delete result.rows;
|
|
805
823
|
}
|
|
806
824
|
// 读面未知键的非阻断回显:放在投影**之后**,否则会被 --jq 的窄投影吞掉。
|
|
807
825
|
if (ignoredReadKeys.length > 0 && typeof result === "object" && result !== null) {
|
|
@@ -1666,7 +1684,7 @@ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
|
1666
1684
|
}
|
|
1667
1685
|
catch (error) {
|
|
1668
1686
|
if (error instanceof GraphIndexAbsentError)
|
|
1669
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1687
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1670
1688
|
return emitThrown(error, transport);
|
|
1671
1689
|
}
|
|
1672
1690
|
}
|
|
@@ -1680,7 +1698,7 @@ function cmdGraphStatus(transport) {
|
|
|
1680
1698
|
}
|
|
1681
1699
|
catch (error) {
|
|
1682
1700
|
if (error instanceof GraphIndexAbsentError)
|
|
1683
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1701
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1684
1702
|
return emitThrown(error, transport);
|
|
1685
1703
|
}
|
|
1686
1704
|
}
|
|
@@ -1693,7 +1711,7 @@ function cmdGraphImpact(kind, code, transport) {
|
|
|
1693
1711
|
}
|
|
1694
1712
|
catch (error) {
|
|
1695
1713
|
if (error instanceof GraphIndexAbsentError)
|
|
1696
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1714
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1697
1715
|
return emitThrown(error, transport);
|
|
1698
1716
|
}
|
|
1699
1717
|
}
|
|
@@ -1704,7 +1722,7 @@ function cmdGraphSearch(term, transport) {
|
|
|
1704
1722
|
}
|
|
1705
1723
|
catch (error) {
|
|
1706
1724
|
if (error instanceof GraphIndexAbsentError)
|
|
1707
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1725
|
+
return emitResult({ status: "refused", blocker: "graph-index-absent", message: `${error.message} 只读核查(不写索引):omc graph status --env <env>(当前配置根);MCP 面工具读的可能是另一配置根,比对 dbId/dbPath。`, nextCommand: "omc graph pull --env <env>" }, transport);
|
|
1708
1726
|
return emitThrown(error, transport);
|
|
1709
1727
|
}
|
|
1710
1728
|
}
|
|
@@ -2470,7 +2488,7 @@ async function main() {
|
|
|
2470
2488
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2471
2489
|
});
|
|
2472
2490
|
command.command("bench").option("--live-sample").option("--env <environment>").action(async (options) => { commandExitCode = await cmdBench([...(options.liveSample ? ["--live-sample"] : []), ...(options.env ? ["--env", options.env] : [])]); });
|
|
2473
|
-
command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true
|
|
2491
|
+
command.command("residue <prefix>").description("跨面前缀残留扫描(model 全租户/folder/custompage;默认 deep 再查命中模型的 rule/datarule/onlinejs)——收尾「删净」一次证明,clean:true 即零残留。清理入口:omc model-purge --code <schemaCode>(按依赖倒序,默认 dry-run)。只读零写入;report 面暂停维护未扫描").option("--env <environment>", "环境名(默认按 cwd 的 .omc-env 解析)").option("--apps <codes>", "all(默认,全租户)或逗号分隔应用码").option("--no-deep", "只扫 model/folder/custompage,不深查命中模型").option("--max-output-bytes <bytes>").action(async (prefix, options) => { commandExitCode = await withTransport(options, request, (transport) => cmdResidue(prefix, { env: options.env, apps: options.apps, ...(options.deep === false ? { noDeep: true } : {}) }, transport)); });
|
|
2474
2492
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2475
2493
|
command.command("commands").option("--json").option("--compact").option("--filter <keyword>").option("--lane <id>").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (options) => { commandExitCode = await withTransport(options, request, (transport) => cmdDiscovery("commands", transport, undefined, { json: options.json === true, compact: options.compact === true, ...(options.filter ? { filter: options.filter } : {}), ...(options.lane ? { lane: options.lane } : {}), ...(options.jq !== undefined ? { jq: options.jq } : {}) })); });
|
|
2476
2494
|
command.command("describe <tool>").option("--full").option("--cache-ok", "允许 --full 在同会话重复读时短路为 artifact 指针(默认每次回全文)").option("--schema-only").option("--param <name>").option("--example [id]").option("--max-output-bytes <bytes>").addOption(jqOption()).action(async (tool, options) => {
|
package/dist/config-store.js
CHANGED
|
@@ -14,6 +14,12 @@ export function removedProfileKeys(profile) {
|
|
|
14
14
|
return REMOVED_PROFILE_KEYS.filter((key) => raw[key] !== undefined);
|
|
15
15
|
}
|
|
16
16
|
export class ConfigurationError extends Error {
|
|
17
|
+
policyRefused;
|
|
18
|
+
constructor(message, options = {}) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ConfigurationError";
|
|
21
|
+
this.policyRefused = options.policyRefused === true;
|
|
22
|
+
}
|
|
17
23
|
}
|
|
18
24
|
export function defaultPaths(projectDir = process.cwd()) {
|
|
19
25
|
const configRoot = process.env.OMC_CONFIG_HOME
|
|
@@ -290,12 +296,25 @@ export class ConfigStore {
|
|
|
290
296
|
const allowText = allowlist.length === 0
|
|
291
297
|
? `.omc-envs=(无或不存在;准入回退到 .omc-env ∪ 全局 default)`
|
|
292
298
|
: `.omc-envs=${allowlist.join(",")}`;
|
|
299
|
+
const configuredEnvironments = this.readConfig().environments;
|
|
300
|
+
const candidates = Object.entries(configuredEnvironments)
|
|
301
|
+
.filter(([, profile]) => profile.applicationCode === name);
|
|
302
|
+
// If several environment names point at the same application, recommend
|
|
303
|
+
// the one the caller is already working in before falling back to the
|
|
304
|
+
// machine-wide configuration order. This avoids steering an agent from a
|
|
305
|
+
// project-local `local` environment to an unrelated `prod` entry merely
|
|
306
|
+
// because `prod` appears first in omc.config.json.
|
|
307
|
+
const applicationHint = candidates.find(([candidate]) => candidate === bound)?.[0]
|
|
308
|
+
?? candidates.find(([candidate]) => allowlist.includes(candidate))?.[0]
|
|
309
|
+
?? candidates.find(([, profile]) => profile.isDefault)?.[0]
|
|
310
|
+
?? candidates[0]?.[0];
|
|
293
311
|
throw new ConfigurationError(`跨环境访问被拒绝:环境 "${name}" 不属于本项目(本项目允许:${own.join("、") || "(none)"})。` +
|
|
294
312
|
`解析到的项目根=${projectRoot};${boundText};${allowText}。` +
|
|
295
313
|
`准入由「当前工作目录」下的 .omc-envs(白名单)+ .omc-env(默认绑定)共同决定,不是由 --env 参数单独决定;` +
|
|
296
314
|
`在无声明的目录里 --env local 会落到机器全局 default(常见是 test)。` +
|
|
297
315
|
`请改到声明了该环境的项目根再跑,或先向用户报告并取得同意后执行 \`omc env allow ${name}\`(写入项目 .omc-envs),` +
|
|
298
|
-
`或临时设置 OMC_ALLOW_CROSS_ENV=1 单次放行。`
|
|
316
|
+
`或临时设置 OMC_ALLOW_CROSS_ENV=1 单次放行。` +
|
|
317
|
+
(applicationHint === undefined ? "" : `\n也许你指的是 --env ${applicationHint}(app=${name})。`), { policyRefused: true });
|
|
299
318
|
}
|
|
300
319
|
bindProjectEnvironment(name) {
|
|
301
320
|
const config = this.readConfig();
|
package/dist/lanes/online-js.js
CHANGED
|
@@ -5,7 +5,7 @@ import { assertHtmlRender, executeFormDraftWrite, executeFormPublishWrite, readF
|
|
|
5
5
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
6
6
|
import { writeTool } from "../write.js";
|
|
7
7
|
import { governedRead } from "../read.js";
|
|
8
|
-
import { defineTool } from "../tool-types.js";
|
|
8
|
+
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
9
9
|
import { wireCall } from "../wire.js";
|
|
10
10
|
import { composeCustomScript, lintOnlineJsSource } from "../online-js-source.js";
|
|
11
11
|
import { composeLayoutElements } from "../online-js-layout.js";
|
|
@@ -51,7 +51,9 @@ function readCustomScriptSource(rawHtml) {
|
|
|
51
51
|
// real script when the find falls back to the tag name.
|
|
52
52
|
const element = elements.find((candidate) => candidate.id === "customScript");
|
|
53
53
|
const script = typeof element?.innerHTML === "string" && element.innerHTML.trim() !== "" ? element.innerHTML : null;
|
|
54
|
-
return
|
|
54
|
+
return script !== null
|
|
55
|
+
? { present: true, script, sha256: createHash("sha256").update(script, "utf8").digest("hex"), elementCount: elements.length }
|
|
56
|
+
: { present: false, script: null, sha256: "", elementCount: elements.length, notCollected: true, notCollectedReason: "customScript 不存在,无法计算脚本哈希" };
|
|
55
57
|
}
|
|
56
58
|
export function registerOnlineJsLane(tools) {
|
|
57
59
|
tools.push(defineTool({
|
|
@@ -81,7 +83,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
81
83
|
sheetPublished: sheet.published === true,
|
|
82
84
|
draft: { ...draft, ...(draft.script === null ? { note: "草稿无 customScript 脚本(尚未 compose 或被清空)" } : {}) },
|
|
83
85
|
published: { ...published, ...(published.script === null ? { note: "发布面无 customScript 脚本" } : {}) },
|
|
84
|
-
...(draft.sha256 !==
|
|
86
|
+
...(draft.sha256 !== "" && published.sha256 !== ""
|
|
85
87
|
? { draftPublishedInSync: draft.sha256 === published.sha256 }
|
|
86
88
|
: {}),
|
|
87
89
|
hint: "脚本是 innerHTML 原文(含平台 (function(form){…}) 生命周期包装——那就是平台存储形态);改脚本用 onlinejs.compose(默认 INCREMENTAL 并入,replace:true 整段替换);同文件修订直接 replace:true 传完整脚本",
|
|
@@ -109,6 +111,7 @@ export function registerOnlineJsLane(tools) {
|
|
|
109
111
|
sheetCode: z.string().min(1).optional().describe("表单槽位码;省略时默认 = schemaCode(与 form.draft/form.publish 同口径)"),
|
|
110
112
|
source: z.string().optional().describe("在线 JS 代码。默认 INCREMENTAL:并入平台 (function(form){…}) 模板的既有脚本,模板与既有注册全部保留;配 replace:true 才是整段替换。脚本不得使用 export/import;事件名必须是平台事件(onLoad/onRendered/onValidate/onPreAction/onCustomAction/onActionDone/onPrintFormDataHandle/onPrintWorkflowLogDataHandle)。与 remove:true 二选一;source 为空字符串也表示移除 customScript"),
|
|
111
113
|
replace: z.boolean().optional().describe("true = 以 source 整体替换 customScript(含完整 (function(form){…}) 脚本)。默认 false = 增量并入既有脚本,保留平台模板与既有逻辑"),
|
|
114
|
+
confirmReplaceSha256: z.string().regex(/^[a-f0-9]{64}$/i).optional().describe("replace:true 覆盖既有脚本时必须等于当前脚本 SHA-256,防止静默覆盖线上改动"),
|
|
112
115
|
remove: z.boolean().optional().describe("true = 显式移除 customScript,使 onlinejs.get 返回 present:false;会写入并按 publish 设置发布。与 source 二选一"),
|
|
113
116
|
publish: z.boolean().optional().describe("默认 true:写草稿并推发布面;false = 只写草稿不推发布面"),
|
|
114
117
|
summary: z.boolean().optional().describe("true = 只回同步状态(sha/字节数/是否同步/渲染兼容),不回任何 HTML/JS 原文。确认「改动是否生效」用这个;要看原文用 onlinejs.get"),
|
|
@@ -126,6 +129,20 @@ export function registerOnlineJsLane(tools) {
|
|
|
126
129
|
const current = await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode: args.schemaCode, sheetCode: targetSheetCode } });
|
|
127
130
|
const existingSheet = (current.data ?? null);
|
|
128
131
|
const existingRaw = typeof existingSheet?.draftHtmlJson === "string" ? existingSheet.draftHtmlJson : undefined;
|
|
132
|
+
const existingPublishedRaw = typeof existingSheet?.publishedHtmlJson === "string" ? existingSheet.publishedHtmlJson : undefined;
|
|
133
|
+
const existingScriptState = readCustomScriptSource(existingRaw ?? existingPublishedRaw);
|
|
134
|
+
if (args.replace === true && existingScriptState.present && args.confirmReplaceSha256 !== existingScriptState.sha256) {
|
|
135
|
+
throw new ToolRefusalError({
|
|
136
|
+
status: "refused",
|
|
137
|
+
blocker: "onlinejs-replace-confirmation-required",
|
|
138
|
+
schemaCode: args.schemaCode,
|
|
139
|
+
sheetCode: targetSheetCode,
|
|
140
|
+
previousSha256: existingScriptState.sha256,
|
|
141
|
+
...(args.confirmReplaceSha256 !== undefined ? { providedSha256: args.confirmReplaceSha256 } : {}),
|
|
142
|
+
message: "replace:true 将覆盖已有线上脚本;必须传 confirmReplaceSha256 且与当前脚本 SHA-256 完全一致。未写入任何内容。",
|
|
143
|
+
nextCommand: `omc call onlinejs.get --args '${JSON.stringify({ schemaCode: args.schemaCode, sheetCode: targetSheetCode })}'`,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
129
146
|
const existingCompat = validateHtmlRender(existingRaw);
|
|
130
147
|
// A persisted legacy face may contain an object or malformed elements.
|
|
131
148
|
// Do not pass any of it through: discard the unproven face and rebuild
|
|
@@ -203,7 +220,9 @@ export function registerOnlineJsLane(tools) {
|
|
|
203
220
|
draftPublishedInSync,
|
|
204
221
|
publishedElementCount: publishedElements.length,
|
|
205
222
|
scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
|
|
206
|
-
|
|
223
|
+
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
224
|
+
composeMode: args.replace === true ? "replace" : "incremental",
|
|
225
|
+
...(removing ? { removed: true } : {}),
|
|
207
226
|
customScriptSha256,
|
|
208
227
|
customScriptBytes: Buffer.byteLength(composedScript, "utf8"),
|
|
209
228
|
composedDraftBytes: Buffer.byteLength(composedDraftHtml, "utf8"),
|
|
@@ -230,9 +249,11 @@ export function registerOnlineJsLane(tools) {
|
|
|
230
249
|
draftPublishedInSync,
|
|
231
250
|
publishedElementCount: publishedElements.length,
|
|
232
251
|
scriptElementPresent: publishedElements.some((element) => element.id === "customScript"),
|
|
252
|
+
scriptElementMeaning: "skeleton 空槽不等于存在脚本",
|
|
233
253
|
customScript: composedScript,
|
|
234
254
|
customScriptMatchesPublished: draftPublishedInSync,
|
|
235
|
-
|
|
255
|
+
composeMode: args.replace === true ? "replace" : "incremental",
|
|
256
|
+
...(removing ? { removed: true } : {}),
|
|
236
257
|
htmlRenderCompat,
|
|
237
258
|
portalVerificationPlan: ["在门户打开该表单", "触发已绑定的在线 JS 事件", "检查事件产生的副作用字段或校验提示"],
|
|
238
259
|
},
|
|
@@ -38,7 +38,7 @@ export function dataSubmitDescription() {
|
|
|
38
38
|
const wire = RUNTIME_DATA_WIRE;
|
|
39
39
|
return describeTool(`M2 runtime data SUBMIT (runtime/form/submit, WorkItemSubmitVO).`, [
|
|
40
40
|
"Distinct from data.save: data.save persists as DRAFT; data.submit advances the record to COMPLETED (no workflow) or drives the workflow instance.",
|
|
41
|
-
"
|
|
41
|
+
"**审批提交(带 workItemId)可省略 `formData`**:平台使用已存业务数据,且不要携带子表数组;新建记录(无 bizObjectId/workItemId)才必须传 formData。此前把两种场景混成 Required 是错误提示(2026-11-07 r40)。",
|
|
42
42
|
`就地语义:${wire.submitInPlace};无流程 readback 必须确认 objectId 与传入 bizObjectId 相同;无 bizObjectId 的 workflowCode 新建单 → PROCESSING + workflowInstanceId,无流程 → 该行 COMPLETED。`,
|
|
43
43
|
`既有草稿发起流程的唯一支持形状是两段式:先 data.save 带 workflowCode 暂存(${wire.workflowDraftSave}),再以返回的 workItemId 提交(issue#11 P6:带流程模型提交**必须**带 bizObjectId+workflowInstanceId+workItemId 三元——只带 bizObjectId 会答「流程实例无待办」、只缺 workItemId 会 50000;完整形状见 omc describe data.submit --example workflow-backed-data-submit,待办查询用 workitem.list)。`,
|
|
44
44
|
`仅无 workflowCode/workItemId/workflowInstanceId 且无 bizObjectId 时保留 save→submit 便捷链(新建记录);只要存在 workItemId 或 workflowInstanceId 就 fail-closed 拒绝(blocker submit-biz-object-id-required),零 runtime/form/submit wire。`,
|
|
@@ -33,8 +33,9 @@ function dataListItemPreview(items) {
|
|
|
33
33
|
return { ids, itemsPreview };
|
|
34
34
|
}
|
|
35
35
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
36
|
+
* r40a B:`data.list` 的大结果被外置后不能回 `items:[]`,否则调用方会把
|
|
37
|
+
* 「有数据但在 artifact」误读成「空结果」。保留一个明确的 items 外置指针,
|
|
38
|
+
* 让通用读门不会再补回空数组占位。
|
|
38
39
|
*
|
|
39
40
|
* **只在确实要丢弃 items 时才补 `ids`/`itemsPreview`**:这段只在 governedRead
|
|
40
41
|
* 判定 `large`(>2048B)后才会被调用。早期版本把 ids/preview 无条件塞进 handler
|
|
@@ -55,8 +56,14 @@ function dataListCompactProjection(result) {
|
|
|
55
56
|
compact.itemsPreview = preview.ids.slice(0, 3).map((id) => ({ id }));
|
|
56
57
|
if (value.length > 0)
|
|
57
58
|
compact.listArtifactHint =
|
|
58
|
-
"items
|
|
59
|
-
|
|
59
|
+
"items 已外置:items 是指针而非空结果;完整行见 artifact(detailArtifactId / fetch)。ids=本页全部 id;itemsPreview=ids 前 3 个的引用(仅 {id},不是第二份行)。";
|
|
60
|
+
// The generic read gate adds [] for dropped array keys. Keep an explicit
|
|
61
|
+
// pointer under the original key so it cannot manufacture that false
|
|
62
|
+
// empty-result shape. The full rows remain in detailArtifactId.
|
|
63
|
+
compact.items = { externalized: true, count: value.length };
|
|
64
|
+
compact.truncatedKeys = ["items"];
|
|
65
|
+
compact.truncated = true;
|
|
66
|
+
compact.itemsExternalized = { count: value.length };
|
|
60
67
|
continue;
|
|
61
68
|
}
|
|
62
69
|
// 这两个键由本函数按 items 现算,handler 不再重复内联。
|
|
@@ -1867,9 +1874,9 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1867
1874
|
* gone, not that the model is clean — a submit can leave a separate draft row
|
|
1868
1875
|
* behind. After a delete, read the model's residue so "cleaned up" is a
|
|
1869
1876
|
* model-level fact (`total:0`). Best-effort: a query failure never fails the
|
|
1870
|
-
|
|
1877
|
+
* delete, but it IS surfaced as an explicit readback error.
|
|
1871
1878
|
*/
|
|
1872
|
-
async function
|
|
1879
|
+
async function remainingRows(session, schemaCode, sheetCode) {
|
|
1873
1880
|
try {
|
|
1874
1881
|
const result = await wireCall(session, {
|
|
1875
1882
|
path: ENDPOINTS.runtimeQueryList,
|
|
@@ -1877,12 +1884,12 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1877
1884
|
body: { schemaCode, queryCode: schemaCode, filters: [], mobile: false, page: 0, size: 1, queryVersion: 1, showTotal: true },
|
|
1878
1885
|
});
|
|
1879
1886
|
const page = extractBizObjectPage(result.data);
|
|
1880
|
-
return {
|
|
1887
|
+
return { remainingRows: page.total, clean: page.total === 0, sheetCode, cleanNote: "clean 表示删除该行后该表是否已为空" };
|
|
1881
1888
|
}
|
|
1882
1889
|
catch (error) {
|
|
1883
1890
|
if (error instanceof WireError && error.kind === "business" && error.errcode === RUNTIME_DATA_WIRE.emptyModelErrcode)
|
|
1884
|
-
return {
|
|
1885
|
-
return {
|
|
1891
|
+
return { remainingRows: 0, clean: true, sheetCode, cleanNote: "clean 表示删除该行后该表是否已为空" };
|
|
1892
|
+
return { remainingRows: null, clean: null, sheetCode, cleanNote: "clean 表示删除该行后该表是否已为空", error: error instanceof Error ? error.message : String(error) };
|
|
1886
1893
|
}
|
|
1887
1894
|
}
|
|
1888
1895
|
writeTool({
|
|
@@ -1981,13 +1988,14 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1981
1988
|
noop: true,
|
|
1982
1989
|
recoveryHint: "若另一进程刚刚删除,这是正常并发结果(already-absent),不是 id 拿错。否则:真实 id 必须取自 data.list 的 items[].id;工作流中心/门户页面回显的 id 会脱敏。",
|
|
1983
1990
|
thisRow: { absent: true, deleted: false },
|
|
1984
|
-
|
|
1991
|
+
thisRowDeleted: true,
|
|
1992
|
+
...(await remainingRows(session, args.schemaCode, sheetCode)),
|
|
1985
1993
|
},
|
|
1986
1994
|
};
|
|
1987
1995
|
}
|
|
1988
1996
|
return {
|
|
1989
1997
|
performed: { errcode: one.errcode, deleted: one.deleted === true, noop: false, sheetCode, resolvedFrom },
|
|
1990
|
-
readback: { absent: one.absent, deleted: one.deleted === true, thisRow: { absent: one.absent, deleted: one.deleted === true }, ...(await
|
|
1998
|
+
readback: { absent: one.absent, deleted: one.deleted === true, thisRowDeleted: one.deleted === true, thisRow: { absent: one.absent, deleted: one.deleted === true }, ...(await remainingRows(session, args.schemaCode, sheetCode)) },
|
|
1991
1999
|
};
|
|
1992
2000
|
}
|
|
1993
2001
|
const deleted = [];
|
|
@@ -1995,7 +2003,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1995
2003
|
deleted.push(await deleteOne(id));
|
|
1996
2004
|
// `deleted`/`count` ride `readback` — the write envelope surfaces only
|
|
1997
2005
|
// performed/readback inline (extra top-level keys are artifact-only).
|
|
1998
|
-
return { performed: { sheetCode, resolvedFrom }, readback: { absent: true, count: deleted.length, deleted, ...(await
|
|
2006
|
+
return { performed: { sheetCode, resolvedFrom }, readback: { absent: true, count: deleted.length, deleted, ...(await remainingRows(session, args.schemaCode, sheetCode)) } };
|
|
1999
2007
|
},
|
|
2000
2008
|
});
|
|
2001
2009
|
writeTool({
|
|
@@ -2007,7 +2015,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2007
2015
|
inputSchema: {
|
|
2008
2016
|
schemaCode: z.string().min(1),
|
|
2009
2017
|
sheetCode: z.string().optional(),
|
|
2010
|
-
formData: runtimeFormDataArg,
|
|
2018
|
+
formData: runtimeFormDataArg.optional().describe("business-object data keyed by field code;新建记录必填,审批提交(带 workItemId)可省略——平台使用已存业务数据"),
|
|
2011
2019
|
workflowCode: z.string().optional(),
|
|
2012
2020
|
workflowInstanceId: z.string().optional(),
|
|
2013
2021
|
workItemId: z.string().optional(),
|
|
@@ -2077,7 +2085,16 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2077
2085
|
});
|
|
2078
2086
|
}
|
|
2079
2087
|
const { sheetCode, resolvedFrom } = await resolveFormSheetCode(session, { schemaCode: args.schemaCode, sheetCode: args.sheetCode });
|
|
2080
|
-
|
|
2088
|
+
if (!hasWorkItem && args.formData === undefined) {
|
|
2089
|
+
throw new ToolInputError({
|
|
2090
|
+
status: "error",
|
|
2091
|
+
blocker: "tool-input-invalid",
|
|
2092
|
+
tool: "data.submit",
|
|
2093
|
+
message: "新建提交缺少 formData;审批提交可省略 formData(子表数组不要带),新建记录才必须传 formData。",
|
|
2094
|
+
nextCommand: "omc call data.submit --args '{\"schemaCode\":\"<schemaCode>\",\"formData\":{...}}'",
|
|
2095
|
+
});
|
|
2096
|
+
}
|
|
2097
|
+
const stampedSubmit = await prepareRuntimeFormData(session, args.schemaCode, (args.formData ?? {}), { isUpdate: targetObjectId !== null });
|
|
2081
2098
|
const formData = stampedSubmit.formData;
|
|
2082
2099
|
// r5-asset 2026-10-20: the platform APPENDS child rows carried in an
|
|
2083
2100
|
// approval submit's formData (no upsert by row id, no error). Live: an
|
|
@@ -2089,7 +2106,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2089
2106
|
// {bizObject:{data:{...}}}, and a top-level-only check let those through
|
|
2090
2107
|
// (the platform appends whatever it finds). The child codes come from the
|
|
2091
2108
|
// model's own field list.
|
|
2092
|
-
const carried = collectChildArrayPaths(args.formData, new Set(stampedSubmit.childCodes));
|
|
2109
|
+
const carried = collectChildArrayPaths((args.formData ?? {}), new Set(stampedSubmit.childCodes));
|
|
2093
2110
|
if (carried.length > 0)
|
|
2094
2111
|
throw new ToolRefusalError({
|
|
2095
2112
|
status: "refused",
|
|
@@ -2124,7 +2141,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2124
2141
|
`确要在提交携带人员字段(个别平台构建接受),显式带 allowStaffOnSubmit:true 自担。`,
|
|
2125
2142
|
});
|
|
2126
2143
|
}
|
|
2127
|
-
const validation = await portalRequiredViolations(session, args.schemaCode, sheetCode, args.formData).catch(() => ({ requiredFields: [], missingRequired: [] }));
|
|
2144
|
+
const validation = await portalRequiredViolations(session, args.schemaCode, sheetCode, args.formData ?? {}).catch(() => ({ requiredFields: [], missingRequired: [] }));
|
|
2128
2145
|
// Directive: a workflow-backed model's submit must START the workflow, not
|
|
2129
2146
|
// merely save a sheet row. When the caller names no workflow params, detect
|
|
2130
2147
|
// the model's workflow and start it (formType=1 + workflowCode); a model
|
|
@@ -2178,7 +2195,7 @@ export function registerRuntimeDataLane(tools) {
|
|
|
2178
2195
|
bizObject: {
|
|
2179
2196
|
schemaCode: args.schemaCode,
|
|
2180
2197
|
sheetCode,
|
|
2181
|
-
data: args.formData,
|
|
2198
|
+
data: args.formData ?? {},
|
|
2182
2199
|
},
|
|
2183
2200
|
agree: args.agree !== false,
|
|
2184
2201
|
};
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -3314,12 +3314,20 @@ export function registerWorkflowLane(tools) {
|
|
|
3314
3314
|
const purposeHint = args.workflowInstanceId === undefined && args.workflowCode === undefined
|
|
3315
3315
|
? "本调用用于发现待办并取 workItemId 做审批;只想看流程走到哪,用 workflow.instanceActivity;提交/审批后优先复用 data.submit 的 readback.nextSubmit,不要重复查询。"
|
|
3316
3316
|
: undefined;
|
|
3317
|
+
// r40a-E(2026-11-07,E2E F29):按实例查且本人口径 total:0 时,与「流程没生成」
|
|
3318
|
+
// 不可区分——探针差点误判「审批没生成」。存在范围外待办时显式说明口径差。
|
|
3319
|
+
const scopedToInstance = args.workflowInstanceId !== undefined && args.participant === undefined;
|
|
3320
|
+
const unfiltered = page.unfilteredTotal;
|
|
3321
|
+
const othersScopeNote = scopedToInstance && page.total === 0 && typeof unfiltered === "number" && unfiltered > 0
|
|
3322
|
+
? `total:0 是当前登录人口径——查询范围(unfilteredTotal=${unfiltered})内存在他人的待办。若该实例应有待办:待办按持有人过滤,用 participant:<持有人id> 复查(持有人见 data.submit 回执 flowWarning);不要据此判定「流程未生成」。`
|
|
3323
|
+
: undefined;
|
|
3317
3324
|
return {
|
|
3318
3325
|
total: page.total,
|
|
3319
3326
|
// The JPA-spec path reports no page count; only emit it when known.
|
|
3320
3327
|
...("totalPages" in page && page.totalPages !== undefined ? { totalPages: page.totalPages } : {}),
|
|
3321
3328
|
itemsCount: items.length,
|
|
3322
3329
|
...(purposeHint ? { purposeHint } : {}),
|
|
3330
|
+
...(othersScopeNote ? { scopeNote: othersScopeNote } : {}),
|
|
3323
3331
|
...(args.participant !== undefined ? { participant: args.participant, state: args.state ?? "PENDING", source: "list_workitems" } : {}),
|
|
3324
3332
|
...("unfilteredTotal" in page && page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
|
|
3325
3333
|
items,
|
package/dist/mcp-graph.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
3
|
import { existsSync } from "node:fs";
|
|
3
4
|
import { resolve } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline";
|
|
@@ -60,7 +61,7 @@ const EXPLORE_TOOL = {
|
|
|
60
61
|
"一次查全一个目标的引用与影响面(本地索引,毫秒级)。",
|
|
61
62
|
"输入 query 可为业务名(实体 name+code 子串)或规则 code+name 词首;也可直接给字段/模型/服务 code。kind 可选 field|model|service,不给则三种都查。",
|
|
62
63
|
"返回 {index(freshness), references:{kind:[{schemaCode,ruleCode,nodeCode,nodeType,via}]}, impact:{kind:totals}, search:{matches:{model,field,service},rules}}。",
|
|
63
|
-
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset
|
|
64
|
+
"references/impact 支持 offset+limit 分页;truncated 时按返回的 nextOffset 继续调用。实体分页结果带 nextCall;分页不会改变 resolved。字段同名时请用 schemaCode 消歧。",
|
|
64
65
|
"改/删/重命名任何字段、模型、服务之前必查;找\"关于 X 的规则\"拿 code 也走这里。",
|
|
65
66
|
"索引不含规则条件叶子与流程绑定;freshness=partial 时命中可能不完整(答案里有 gapWarning)。",
|
|
66
67
|
].join(" "),
|
|
@@ -69,6 +70,7 @@ const EXPLORE_TOOL = {
|
|
|
69
70
|
properties: {
|
|
70
71
|
query: { type: "string", description: "业务名(实体 name+code 子串)、字段/模型/服务 code,或规则检索词(规则 code+name 词首匹配)" },
|
|
71
72
|
kind: { type: "string", enum: [...KINDS], description: "限定一种引用种类;省略则 field/model/service 全查" },
|
|
73
|
+
schemaCode: { type: "string", description: "限定字段所属模型;也可用于校验模型。" },
|
|
72
74
|
env: { type: "string", description: "期望的环境名;与索引所属环境不一致时会拒绝(防跨环境误答)" },
|
|
73
75
|
offset: { type: "integer", minimum: 0, description: "引用/实体结果起始偏移;truncated 时使用 nextOffset" },
|
|
74
76
|
limit: { type: "integer", minimum: 1, maximum: MATCH_CAP, description: "每类结果最多返回多少条(默认 50)" },
|
|
@@ -78,7 +80,7 @@ const EXPLORE_TOOL = {
|
|
|
78
80
|
};
|
|
79
81
|
const STATUS_TOOL = {
|
|
80
82
|
name: "omc_graph_status",
|
|
81
|
-
description: "
|
|
83
|
+
description: "索引完整性与新鲜度(所有身份与新鲜度字段同层):environmentName、freshness(complete|partial)、模型/规则计数、pulledAtIso、ageMinutes、staleAfterMinutes、dbId、dbPath、coverage、gaps。超过阈值会给 warning;索引缺失会给出 pull 指引。",
|
|
82
84
|
inputSchema: { type: "object", properties: {} },
|
|
83
85
|
};
|
|
84
86
|
export function graphToolDefinitions() {
|
|
@@ -90,6 +92,57 @@ function toolResult(payload, isError = false) {
|
|
|
90
92
|
function toolError(message) {
|
|
91
93
|
return toolResult({ status: "error", message }, true);
|
|
92
94
|
}
|
|
95
|
+
function withMatchedOn(row, query) {
|
|
96
|
+
const name = row.name ?? "";
|
|
97
|
+
const code = row.code ?? row.schemaCode ?? "";
|
|
98
|
+
const on = name.includes(query) ? "name" : "code";
|
|
99
|
+
const value = on === "name" ? name : code;
|
|
100
|
+
const at = value.toLocaleLowerCase().indexOf(query.toLocaleLowerCase());
|
|
101
|
+
if (at < 0)
|
|
102
|
+
return { ...row, matchedOn: { on, fragment: query } };
|
|
103
|
+
let start = at;
|
|
104
|
+
let end = at + query.length;
|
|
105
|
+
while (start > 0 && /[\p{L}\p{N}]/u.test(value[start - 1]))
|
|
106
|
+
start--;
|
|
107
|
+
while (end < value.length && /[\p{L}\p{N}]/u.test(value[end]))
|
|
108
|
+
end++;
|
|
109
|
+
return { ...row, matchedOn: { on, fragment: value.slice(start, end) } };
|
|
110
|
+
}
|
|
111
|
+
function levenshtein(a, b) {
|
|
112
|
+
const prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
113
|
+
for (let i = 1; i <= a.length; i++) {
|
|
114
|
+
let corner = prev[0];
|
|
115
|
+
prev[0] = i;
|
|
116
|
+
for (let j = 1; j <= b.length; j++) {
|
|
117
|
+
const old = prev[j];
|
|
118
|
+
prev[j] = Math.min(prev[j] + 1, prev[j - 1] + 1, corner + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
119
|
+
corner = old;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return prev[b.length];
|
|
123
|
+
}
|
|
124
|
+
function entityInventory() {
|
|
125
|
+
const db = new DatabaseSync(graphDbPath(), { readOnly: true });
|
|
126
|
+
try {
|
|
127
|
+
return [
|
|
128
|
+
...db.prepare("SELECT schema_code, name FROM models").all().map((r) => ({ kind: "model", schemaCode: r.schema_code, code: r.schema_code, name: r.name ?? "" })),
|
|
129
|
+
...db.prepare("SELECT schema_code, code, name FROM fields").all().map((r) => ({ kind: "field", schemaCode: r.schema_code, code: r.code, name: r.name ?? "" })),
|
|
130
|
+
...db.prepare("SELECT code, name FROM services").all().map((r) => ({ kind: "service", code: r.code, name: r.name ?? "" })),
|
|
131
|
+
];
|
|
132
|
+
}
|
|
133
|
+
finally {
|
|
134
|
+
db.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
function suggestions(query) {
|
|
138
|
+
return entityInventory().map((row) => {
|
|
139
|
+
const q = query.toLocaleLowerCase();
|
|
140
|
+
const values = [row.code, row.name].map((v) => v.toLocaleLowerCase());
|
|
141
|
+
const prefix = values.some((v) => v.startsWith(q)) ? 0 : values.some((v) => v.includes(q)) ? 1 : 2;
|
|
142
|
+
const distance = Math.min(...values.map((v) => levenshtein(q, v)));
|
|
143
|
+
return { kind: row.kind, code: row.code, name: row.name, score: prefix * 1000 + distance };
|
|
144
|
+
}).sort((a, b) => a.score - b.score || a.code.localeCompare(b.code)).slice(0, 3);
|
|
145
|
+
}
|
|
93
146
|
function cap(rows, offset = 0, limit = MATCH_CAP) {
|
|
94
147
|
const page = rows.slice(offset, offset + limit);
|
|
95
148
|
return { rows: page, truncated: offset + page.length < rows.length, offset, limit, ...(offset + page.length < rows.length ? { nextOffset: offset + page.length } : {}), total: rows.length };
|
|
@@ -100,7 +153,9 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
100
153
|
try {
|
|
101
154
|
if (name === STATUS_TOOL.name) {
|
|
102
155
|
const status = readGraphStatus();
|
|
103
|
-
|
|
156
|
+
// Keep a tiny compatibility locator for older MCP clients; all identity,
|
|
157
|
+
// freshness, coverage and gaps fields live at the top level.
|
|
158
|
+
return toolResult({ status: "ok", ...status, ...status.index, index: { environmentName: status.index.environmentName } });
|
|
104
159
|
}
|
|
105
160
|
if (name === EXPLORE_TOOL.name) {
|
|
106
161
|
const query = typeof args.query === "string" ? args.query.trim() : "";
|
|
@@ -109,6 +164,9 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
109
164
|
const kind = args.kind === undefined ? undefined : String(args.kind);
|
|
110
165
|
if (kind !== undefined && !KINDS.includes(kind))
|
|
111
166
|
return toolError(`kind 必须是 ${KINDS.join("|")} 之一`);
|
|
167
|
+
const schemaCode = typeof args.schemaCode === "string" && args.schemaCode.trim() ? args.schemaCode.trim() : undefined;
|
|
168
|
+
if (schemaCode && kind !== "field" && kind !== "model")
|
|
169
|
+
return toolError("schemaCode 仅适用于 kind=field|model");
|
|
112
170
|
const status = readGraphStatus();
|
|
113
171
|
const index = status.index;
|
|
114
172
|
if (typeof args.env === "string" && args.env.trim() && args.env.trim() !== index.environmentName) {
|
|
@@ -119,14 +177,32 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
119
177
|
const limit = typeof args.limit === "number" && Number.isSafeInteger(args.limit) && args.limit > 0 && args.limit <= MATCH_CAP ? args.limit : MATCH_CAP;
|
|
120
178
|
const references = {};
|
|
121
179
|
const impact = {};
|
|
180
|
+
// Resolve from an unpaged search. The result page is presentation only;
|
|
181
|
+
// otherwise offset=2 can silently change the subject being inspected.
|
|
182
|
+
const search = searchGraphRules(query, limit, offset);
|
|
183
|
+
const resolvedSearch = searchGraphRules(query, MATCH_CAP, 0);
|
|
184
|
+
const loweredQuery = query.toLocaleLowerCase();
|
|
185
|
+
const allCandidates = entityInventory().filter((r) => (r.code.toLocaleLowerCase().includes(loweredQuery) || r.name.toLocaleLowerCase().includes(loweredQuery)) &&
|
|
186
|
+
(!schemaCode || (r.kind !== "service" && r.schemaCode === schemaCode)));
|
|
187
|
+
const typedCandidates = kind ? allCandidates.filter((r) => r.kind === kind) : allCandidates;
|
|
188
|
+
const fieldCandidates = kind === "field" ? typedCandidates : allCandidates.filter((r) => r.kind === "field");
|
|
189
|
+
const exactSame = fieldCandidates.filter((r) => r.code === query);
|
|
190
|
+
const ambiguous = kind === "field" && !schemaCode && exactSame.length > 1;
|
|
191
|
+
const resolvedEntity = ambiguous ? undefined : typedCandidates[0] ?? (kind === undefined ? allCandidates[0] : undefined);
|
|
192
|
+
const subjectByName = query !== (resolvedEntity?.code ?? "") && resolvedEntity;
|
|
193
|
+
const subjects = resolvedEntity
|
|
194
|
+
? [{ kind: resolvedEntity.kind, code: resolvedEntity.kind === "model" ? (resolvedEntity.schemaCode ?? resolvedEntity.code) : resolvedEntity.code }]
|
|
195
|
+
: [];
|
|
122
196
|
for (const one of kinds) {
|
|
123
|
-
const
|
|
124
|
-
const
|
|
197
|
+
const subject = subjects.find((s) => s.kind === one);
|
|
198
|
+
const ref = queryGraphReferences(one, subject?.code ?? query);
|
|
199
|
+
const scopedMatches = subject && one === "field" && schemaCode ? ref.matches.filter((row) => row.schemaCode === schemaCode) : ref.matches;
|
|
200
|
+
const capped = cap(scopedMatches, offset, limit);
|
|
125
201
|
// r35a-F4c(真机 2026-11-06):**MCP 面是第三个消费者**——CLI 的 references
|
|
126
202
|
// 与 impact 都带 coverage 了,这里若不显式透传,MCP 用户看到的仍是
|
|
127
203
|
// 「matches:[] + 一句旧 note」,照样会把「表单没扫」误读成「没人引用」。
|
|
128
204
|
references[one] = {
|
|
129
|
-
count:
|
|
205
|
+
count: scopedMatches.length,
|
|
130
206
|
rows: capped.rows,
|
|
131
207
|
offset: capped.offset,
|
|
132
208
|
limit: capped.limit,
|
|
@@ -134,35 +210,49 @@ export async function executeGraphTool(name, rawArgs) {
|
|
|
134
210
|
...(ref.coverage ? { coverage: ref.coverage } : {}),
|
|
135
211
|
...(ref.gapWarning ? { gapWarning: ref.gapWarning } : {}),
|
|
136
212
|
};
|
|
137
|
-
if (
|
|
138
|
-
const imp = impactGraph(one, query);
|
|
213
|
+
if (scopedMatches.length > 0 || kind !== undefined || subject !== undefined) {
|
|
214
|
+
const imp = impactGraph(one, subject?.code ?? query);
|
|
215
|
+
const scopedRules = subject && one === "field" && schemaCode ? imp.rules.filter((row) => row.schemaCode === schemaCode) : imp.rules;
|
|
139
216
|
impact[one] = {
|
|
140
|
-
totals: imp.totals,
|
|
141
|
-
rules: cap(
|
|
217
|
+
totals: scopedRules.length === imp.rules.length ? imp.totals : { ...imp.totals, rules: scopedRules.length },
|
|
218
|
+
rules: cap(scopedRules, offset, limit),
|
|
142
219
|
datarules: cap(imp.datarules, offset, limit),
|
|
143
220
|
listviews: cap(imp.listviews, offset, limit),
|
|
221
|
+
...(one === "model" ? {
|
|
222
|
+
fieldsReferenced: [...imp.rules.flatMap((r) => r.touches.field.map((code) => ({ code, ruleCode: r.ruleCode }))).reduce((m, x) => {
|
|
223
|
+
const item = m.get(x.code) ?? { code: x.code, touchedByRules: [] };
|
|
224
|
+
if (!item.touchedByRules.includes(x.ruleCode))
|
|
225
|
+
item.touchedByRules.push(x.ruleCode);
|
|
226
|
+
m.set(x.code, item);
|
|
227
|
+
return m;
|
|
228
|
+
}, new Map()).values()],
|
|
229
|
+
} : {}),
|
|
144
230
|
...(imp.coverage ? { coverage: imp.coverage } : {}),
|
|
145
231
|
};
|
|
146
232
|
}
|
|
147
233
|
}
|
|
148
|
-
const
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
search.rules[0] ? { kind: "rule", schemaCode: search.rules[0].schemaCode, code: search.rules[0].ruleCode } : undefined;
|
|
234
|
+
const decorate = (row) => withMatchedOn({ ...row, ...(row.schemaCode && row.code === undefined ? { code: row.schemaCode } : {}) }, query);
|
|
235
|
+
const decoratedMatches = { ...search.matches, model: { ...search.matches.model, items: search.matches.model.items.map(decorate) }, field: { ...search.matches.field, items: search.matches.field.items.map(decorate) }, service: { ...search.matches.service, items: search.matches.service.items.map(decorate) } };
|
|
236
|
+
const noEntityHit = search.matches.model.total === 0 && search.matches.field.total === 0 && search.matches.service.total === 0;
|
|
237
|
+
const composite = /\s|\+|,|\b(?:AND|OR)\b/i.test(query);
|
|
153
238
|
const searchNote = "实体检索:业务名/实体 name+code 使用子串匹配。\n规则检索:规则 code+name 使用词首匹配(中文子串不命中)。";
|
|
154
239
|
const entityTruncated = Object.values(search.matches).some((match) => match.truncated === true);
|
|
155
|
-
const
|
|
240
|
+
const nextCall = entityTruncated ? Object.fromEntries(Object.entries(search.matches).filter(([, m]) => m.truncated).map(([k, m]) => [k, { query, kind: k, offset: m.nextOffset, limit }])) : undefined;
|
|
241
|
+
const paginationNote = entityTruncated ? `\n结果已截断:请使用 nextCall 中对应调用继续。` : "";
|
|
156
242
|
return toolResult({
|
|
157
243
|
status: "ok",
|
|
158
244
|
query,
|
|
159
245
|
found: resolvedEntity !== undefined,
|
|
160
|
-
...(resolvedEntity ? { resolved: resolvedEntity } : {}),
|
|
246
|
+
...(resolvedEntity ? { resolved: { kind: resolvedEntity.kind, schemaCode: "schemaCode" in resolvedEntity ? resolvedEntity.schemaCode : "", code: resolvedEntity.code, matchedOn: { on: resolvedEntity.name.includes(query) ? "name" : "code", fragment: query } } } : {}),
|
|
247
|
+
...(ambiguous ? { ambiguous: true, candidates: exactSame.map((r) => ({ schemaCode: r.schemaCode, code: r.code, name: r.name })) } : {}),
|
|
161
248
|
...(kind !== undefined ? { kind } : {}),
|
|
162
249
|
index,
|
|
163
250
|
references,
|
|
164
251
|
impact,
|
|
165
|
-
search: { count: search.rules.length, rules: search.rules, matches:
|
|
252
|
+
search: { count: search.rules.length, rules: search.rules, matches: decoratedMatches, coverage: search.coverage },
|
|
253
|
+
...(nextCall ? { nextCall } : {}),
|
|
254
|
+
...(noEntityHit && composite ? { unsupportedQuerySyntax: true, hint: "组合检索暂不支持,请拆词单查后求交" } : {}),
|
|
255
|
+
...(noEntityHit ? { suggestions: suggestions(query), hint: "可尝试 suggestions 中的实体 code/name;引用明细请使用匹配 code 查 references/impact" } : {}),
|
|
166
256
|
searchNote: `${searchNote}${paginationNote}`,
|
|
167
257
|
...(search.note ? { searchNote: `${searchNote}\n${search.note}` } : {}),
|
|
168
258
|
// 旧句「索引不含规则条件叶子与流程绑定」漏了表单与流程权限(r35a-F4 实证
|
package/dist/tool-types.js
CHANGED
|
@@ -187,6 +187,14 @@ const CONTEXTUAL_KEY_GUIDANCE = Object.freeze({
|
|
|
187
187
|
precision: "precision→options.scale(数值小数位,不是顶层 precision)",
|
|
188
188
|
}),
|
|
189
189
|
});
|
|
190
|
+
// A key can be valid for a sibling tool while still being invalid here. That
|
|
191
|
+
// is a capability-routing problem, not a spelling problem, so do not present
|
|
192
|
+
// the misleading "no close key" sentence. Keep this explicit and narrow: an
|
|
193
|
+
// arbitrary key must continue to use the ordinary closest-key hint.
|
|
194
|
+
const SIBLING_TOOL_KEY_HINTS = Object.freeze({
|
|
195
|
+
"app.update": Object.freeze({ name: '"name" 是 app.create 的入参;app.update 不收它' }),
|
|
196
|
+
"model.create": Object.freeze({ fields: '"fields" 是 configure_model 的入参;model.create 不收它' }),
|
|
197
|
+
});
|
|
190
198
|
function closestSchemaKey(key, schemaKeys) {
|
|
191
199
|
if (schemaKeys.has(key))
|
|
192
200
|
return key;
|
|
@@ -249,6 +257,9 @@ export function parseToolInput(tool, input) {
|
|
|
249
257
|
const guidance = TWO_STEP_GUIDANCE[tool.name]?.[key] ?? CONTEXTUAL_KEY_GUIDANCE[tool.name]?.[key];
|
|
250
258
|
if (guidance !== undefined)
|
|
251
259
|
return guidance;
|
|
260
|
+
const sibling = SIBLING_TOOL_KEY_HINTS[tool.name]?.[key];
|
|
261
|
+
if (sibling !== undefined)
|
|
262
|
+
return sibling;
|
|
252
263
|
const semantic = KEY_SEMANTIC_HINTS[key];
|
|
253
264
|
const contextual = contextualClosestKey(tool.name, key, schemaKeys);
|
|
254
265
|
const closest = closestSchemaKey(key, schemaKeys);
|
|
@@ -259,14 +270,15 @@ export function parseToolInput(tool, input) {
|
|
|
259
270
|
// 未知键的下一步应指向**正确的那个参数**,不是整份 describe(4–17 KB)。
|
|
260
271
|
// 有近邻时 `--param <nearKey>` 是 150–900 B,且直接回答「本来想写哪个键」。
|
|
261
272
|
// 语境建议优先于编辑距离(r34a-F18:data.delete 的 id 指 requestId 是噪声)。
|
|
273
|
+
const siblingHints = unknown.map((key) => SIBLING_TOOL_KEY_HINTS[tool.name]?.[key]).filter((hint) => typeof hint === "string");
|
|
262
274
|
const nearestKey = contextualClosestKey(tool.name, unknown[0], schemaKeys) ?? closestSchemaKey(unknown[0], schemaKeys);
|
|
263
275
|
const paramCommand = nearestKey !== undefined ? `omc describe ${tool.name} --param ${nearestKey}` : `omc describe ${tool.name}`;
|
|
264
276
|
throw new ToolInputError({
|
|
265
277
|
status: "error",
|
|
266
278
|
blocker: "tool-input-unknown-keys",
|
|
267
279
|
tool: tool.name,
|
|
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 }),
|
|
280
|
+
message: `写面工具不接受未知键(会被静默丢弃):${unknown.join("、")}。${twoStep.length > 0 ? twoStep.join(" ") : siblingHints.length > 0 ? siblingHints.join(";") : `${guidanceLines.length > 0 ? guidanceLines.join(";") : `最接近的合法键:${suggestions.join(";")}`}。参数表:${paramCommand}`}`,
|
|
281
|
+
...(twoStep.length > 0 || guidanceLines.length > 0 || siblingHints.length > 0 ? { nextCommand: `omc describe ${tool.name}` } : { nextCommand: paramCommand }),
|
|
270
282
|
issues: unknown.map((key) => ({ path: [key], code: "unrecognized_key", message: issueHintFor(key) })),
|
|
271
283
|
});
|
|
272
284
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.21",
|
|
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.21",
|
|
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.21"
|
|
77
77
|
}
|
|
78
78
|
}
|
|
@@ -117,6 +117,7 @@ only the section for the capability you need —
|
|
|
117
117
|
- **按钮脚本(JS)读写**:`listview.get --buttonCode <code>`(或 `--buttons`)直接回 `scriptData`/`scriptBytes`/`scriptSha256`,不用翻 artifact;写用 `listview.configure --buttonScripts [{code,script}]` 只改脚本(保留其它字段),或在 `buttons[]` 里用 `script` 简写(= `actionConfig.scriptData`)。
|
|
118
118
|
- **recipe**:`omc describe listview.configure --example configure-default-list`
|
|
119
119
|
- **边界 / 陷阱**:默认视图已存在,`listview.configure` 省略 code 即配置默认视图。**按字段分组(queryLayer)未取证**——别猜写。
|
|
120
|
+
- **行级条件标红当前不支持**:`rowColorSetting` 在 8.6.26 没有平台类映射,配置会被拒绝。替代方案是增加/展示状态字段,再用视图筛选或分组表达重点记录;不要把列表行颜色当作已覆盖能力承诺。
|
|
120
121
|
|
|
121
122
|
## 字段级规则 `datarule`
|
|
122
123
|
|
|
@@ -144,10 +144,9 @@ you can wait for.
|
|
|
144
144
|
范围以任务书为界,任务书之外的增删改仍需新草稿。指纹重核与写后读回照旧。
|
|
145
145
|
此豁免**要求任务书显式声明预授权**;agent 不得自行把交互会话当成自治上下文。
|
|
146
146
|
|
|
147
|
-
## 7. Cleanup
|
|
147
|
+
## 7. Cleanup / 非交互 run
|
|
148
148
|
|
|
149
|
-
|
|
150
|
-
**清单外的对象仍要单独出草稿。** 清理只碰自有 `omc_mcp_*` 夹具。
|
|
149
|
+
清理授权捆在创建授权里:创建草稿写明「验收后删除这些」,确认即一并授权;**清单外对象仍单独出草稿**;清理只碰自有 `omc_mcp_*` 夹具。非交互 run 无 question 通道时按上文「任务书即草稿即批准」执行,另加两条:推定内容逐项写入草稿 `assumptions[]`;**超出任务书范围的写入不能推定**——停下,收口报告标红等新授权。
|
|
151
150
|
|
|
152
151
|
## 8. Canonical configuration order(规范配置顺序)
|
|
153
152
|
|
package/skills/omc-glossary.md
CHANGED
|
@@ -76,6 +76,8 @@ Terminology for the `omc` configuration CLI. Built on CloudPivot 8.6 (anchor
|
|
|
76
76
|
tools `data.save` / `data.load` / `data.submit` / `data.update` / `data.list`).
|
|
77
77
|
`data.load` is the authoritative custom-field readback; list projection
|
|
78
78
|
is a pinned boundary (custom fields keys-present-values-null).
|
|
79
|
+
- **记录状态 `sequenceStatus`** — 系统流转态枚举:`DRAFT`(草稿)、`PROCESSING`(流程处理中)、`COMPLETED`(已完成/提交态)。判态用 `data.load`,不要用 `data.list` 的入口统计;`data.list` 的 `draftPolicy` 只影响列表计入口径。无流程模型提交用 `data.submit`;有流程模型在 workitem 审批通过后才进入 `COMPLETED`。它不是业务状态字段,业务状态应另建字段并由规则维护。
|
|
80
|
+
- **业务状态** — 平台无内置业务状态字段;需求里的「状态改成大额维修」要显式建业务字段(如 `repairStatus`)+ `rule-when --then 'set:<状态字段>=<值>'` 维护;不要拿 `sequenceStatus` 承载业务枚举。
|
|
79
81
|
- **B2 service (bizservice)** — service + method registration face.
|
|
80
82
|
- **List view (query)** — design-time list-view header lifecycle with
|
|
81
83
|
forced LIST/showOnPc/unpublished defaults.
|
package/skills/omc-playbook.md
CHANGED
|
@@ -13,11 +13,13 @@ description: 一页纸业务场景步骤清单(采购申请 / 请假审批 /
|
|
|
13
13
|
|
|
14
14
|
1. `omc next "从零做采购申请:主子表、合计、条件审批、列表、按钮"`
|
|
15
15
|
2. `omc intent schema`(或直接按下方拼装器链)
|
|
16
|
-
3. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '[{"code":"title","name":"标题","propertyType":"SHORT_TEXT"},{"code":"amount","name":"合计","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"
|
|
17
|
-
|
|
16
|
+
3. `omc model-scaffold --app <app> --code <code> --name 采购申请 --fields '[{"code":"title","name":"标题","propertyType":"SHORT_TEXT"},{"code":"amount","name":"合计","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"<主表code>_details","name":"明细","propertyType":"CHILD_TABLE","subFields":[{"code":"item","name":"品名","propertyType":"SHORT_TEXT"},{"code":"qty","name":"数量","propertyType":"NUMERICAL","options":{"scale":0}},{"code":"price","name":"单价","propertyType":"NUMERICAL","options":{"scale":2}},{"code":"lineAmount","name":"金额","propertyType":"NUMERICAL","options":{"scale":2}}]}]' --apply`
|
|
17
|
+
子表 code 是全局子 schema code,必须全局唯一;不要使用 `details`、`items` 这类通用名,按 `<主表code>_details` 这类带主表前缀的形式命名,避免撞名 40000。
|
|
18
|
+
4. `omc compute --schema <code> --target amount --formula 'SUM({<主表code>_details.lineAmount})' --apply`(⚠️ 只在门户算,REST 不回填)
|
|
18
19
|
5. 条件审批(金额>50000 两级):**不要** `approval-flow --condition`(无此参数)。线性先:`omc approval-flow --schema <code> --steps '经理@role:<mgr>' --steps '总监@role:<dir>' --apply`;条件分流改 `omc describe workflow.update --example workflow-conditional-branch` 后 `workflow.update`。
|
|
19
20
|
6. 发起权限:`omc call workflow.addPermission --args '{"workflowCode":"<code>_wf","unitId":"<deptOrRoleId>","unitType":1}'`(这是**谁能发起**,不是行级可见范围)
|
|
20
|
-
7. 列表:`omc call listview.configure --args '{"schemaCode":"<code>","fieldCodes":["title","amount"]}'`
|
|
21
|
+
7. 列表:`omc call listview.configure --args '{"schemaCode":"<code>","fieldCodes":["title","amount"],"conditions":[{"propertyCode":"assetNo","filterType":"Eq","defaultValue":"<设备编号>"}]}'`
|
|
22
|
+
按设备编号等条件查询时用 `conditions`;形状以 `omc describe listview.configure --param conditions` 为准(至少提供 `propertyCode`,再按字段补 `filterType`/`defaultValue`)。
|
|
21
23
|
8. 表单按钮:`omc call listview.configure --args '{"schemaCode":"<code>","buttons":[{"name":"提交审批","useLocation":"FORM","bindAction":"FLOW"}]}'`(形状以 describe 为准)
|
|
22
24
|
9. ⚠️ **行级「申请人只看自己」当前工具面不支持**(`workflow.permissions` / `org.role.setUserScope` 都不是行级数据权限)
|
|
23
25
|
|
package/skills/omc-refusals.md
CHANGED
|
@@ -11,6 +11,11 @@ Grouped by family. For a specific tool's exact wire and valid shapes, follow the
|
|
|
11
11
|
Read `result.blocker` (refused) or `result.status`+`result.reason` (uncertain).
|
|
12
12
|
`result.retryable` is advisory only — the family decides what to do.
|
|
13
13
|
|
|
14
|
+
`status:"refused"` means a local preflight or policy gate stopped the call:
|
|
15
|
+
it necessarily sent zero platform writes; there is no need to second-guess
|
|
16
|
+
whether anything was written. A platform/wire error (`wire-business` or an
|
|
17
|
+
`errcode`) is different: the outcome can be uncertain, so use readback.
|
|
18
|
+
|
|
14
19
|
## 1. Write permission declined
|
|
15
20
|
|
|
16
21
|
- `environment-write-disabled` — the environment profile has `writable:false`.
|
package/skills/omc-start-here.md
CHANGED
|
@@ -12,9 +12,9 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
12
12
|
|
|
13
13
|
## 最小路径(先读这段就能开始)
|
|
14
14
|
|
|
15
|
-
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON
|
|
15
|
+
0. **调用与检索(跨平台)**:`omc call ... --args '<json>'` 的内联 JSON 在 Windows PowerShell 下因引号转义不可靠——**PS 环境优先 `--args-file <path>`**(把 JSON 写临时文件再引用;解析失败的报错里也有此提示)。长 JSON、含引号或中文换行的负载统一可走 stdin:`echo '<json>' | omc call <tool> --args -`;`--args-file <path>` 则必须位于项目配置根内。读命令输出默认把中文转义为 `\uXXXX`(防乱码)——**这是有意为之**:stdout 恒为「恰好一个合法 JSON envelope」,在任何代码页下都能被 `JSON.parse`/`ConvertFrom-Json` 还原(GitLab #17 T2 实证:曾让 `--utf8` 关掉转义,GBK 管道把信封结构解码坏、`ConvertFrom-Json` 中途失败)。`--utf8` 只让**人类诊断文本**(stderr/散文)按 UTF-8 原文输出;要连 stdout 也是中文明文,必须先让管道按 UTF-8 解码(`chcp 65001`)——只设 `OMC_ASCII_STDOUT=0` 是关写出端转义、不改管道解码,在 cp936 管道下会把信封解码坏。对输出做内容检索时**先解 JSON 再检索**(或 `--jq` 取值),"grep 搜不到中文"≠"没有配置"(静默假阴性坑)。stdout 恒为纯 JSON、人读进度在 stderr——脚本解析勿 `2>&1` 合流。
|
|
16
16
|
|
|
17
|
-
0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true
|
|
17
|
+
0. **拼装器优先(高频写面一条命令)**:整模型建模(模型+N 字段+子表+发布+表单)`omc model-scaffold`;**存量模型加字段(建→发布→重建表单→列表加列)`omc field-add`**;字段计算 `omc compute`;条件→动作 `omc rule-when`;线性审批流 `omc approval-flow`;表单默认值 `omc form-default`;存量盘点 `data.list all:true`;收尾清理 `omc model-purge --code <schema>`(依赖倒序,默认 dry-run)+ `omc residue <prefix>` 证删净——命中这些形态先看 `--help`,比 recipe 手拼省 10+ 轮。
|
|
18
18
|
|
|
19
19
|
0d. **改线上已有配置:按范围选路**——单点(字段/视图/规则/布局)用 `omc call` 只传要改的参数;存量加字段用 `omc field-add`;整包收敛写**最小** `intent.yaml`(只声明要管的)再 `validate`/`plan`/`apply`;整面正文用 `form.get` + `artifact get --jq` 取回再写。`intent readback` 只做**等价校验**、不是导出可编辑草稿;结构化配置没有 pull/apply(仅规则源码走 `omc programming`)。
|
|
20
20
|
|
|
@@ -28,6 +28,11 @@ be treated as a second source. Read this, then follow the topics it names.
|
|
|
28
28
|
阶梯步骤已内联工具必填参数(`route.ladder[].tools`/`readTools`)与命中 recipe 的 `describe <tool> --example <id>`;据此直接执行——**不要再逐能力 `commands --lane`**,也不要先扫 `commands --json`/全部 `contract`/全部 `describe --full`。
|
|
29
29
|
方法论按 `route.readSections` 逐节读:对每条 `{topic, section}` 执行 `omc help <topic> --section "<section>"`(单节 0.4–3.4KB)。`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数);**不要 `--all` 读整篇**(5–14KB,读一次就留在后续每一轮窗口)。**带着具体问题(错误码/跨能力语义/某个词)直接 `omc help --search "<词>"`**——跨全部 topic 全文检索,命中节的正文随结果内联返回,一次调用就是答案;**严禁自己写 `for t in …; do omc help $t --all | grep …` 循环**(r18 实测 12 调用/176KB,检索一次 8KB 搞定)。
|
|
30
30
|
1b. **改/删/重命名既有对象前先看影响面(本地规则图索引)**:`omc graph pull --env <env>` 一次拉全平台建本地索引(增量、秒级、带锁与 gap 报告),之后 `omc graph references <service|model|field> <code>` 离线答「谁引用了它」——一调替代逐模型 `rule.get` 枚举(r30 实测影响评估手工枚举 8×rule.get,graph 一调可达);`impact <kind> <code>` 看爆炸半径、`search <词>` 找规则 code、`status` 看索引新鲜度。索引不含规则条件叶子与流程绑定,命中后仍以 `rule.get` 原文为准。无索引时会拒绝并指路 pull。宿主已接 MCP 时用 `omc mcp-graph --path <项目根>`(工具 `omc_graph_explore`/`omc_graph_status`)。
|
|
31
|
+
|
|
32
|
+
**图谱只覆盖单应用时怎么按应用盘点(只读)**:先用
|
|
33
|
+
`omc call app.list` 列出应用,再用
|
|
34
|
+
`omc call model.list --args '{"applicationCode":"<code>"}'` 按应用盘点模型。
|
|
35
|
+
graph 索引 coverage 默认只含配置应用;coverage 未命中不等于应用或模型不存在。
|
|
31
36
|
1c. **写后端 Java 源码(在线开发)走工作区而不是一条条 call**:`omc programming pull --env <env> --app <appCode>` 把业务规则源码拉进 `.omc-programming/`(git 可管),改文件后 `omc programming status` → `omc programming apply [--publish]`(逐文件走编程面治理);图形化规则转编码用 `omc programming adopt --schema <s> --rule <r>`;验证用 `omc programming test --schema <s> --rule <r>`(fixture 驱动:compile→publish→触发→断言→清理)。**平台运行时只执行已发布版**:只 save 不 publish 等于没改;publish 内部会编译并把规则切 CODING+enable。包路径/类名不得改。
|
|
32
37
|
2. 写前只需读 `omc-config-draft-review`(人审门)与 `omc-five-piece-flow`(发现→草稿→权限→执行→回读);`omc-business-configuration` 按命中的能力分节读(见 `next` 的 `readSections`),不要整篇预取。
|
|
33
38
|
3. 环境名以 `omc doctor` 的「环境=」或 `.omc-env` 为准;工具不确定先用默认 `describe`/`--example`,只有单个工具缺形状才用 `--full`;`describe` 带 `exampleHint`/`recipeHint` 时优先 `--example`。
|
|
@@ -80,6 +80,10 @@ omc help omc-write-hazards
|
|
|
80
80
|
- 新模型先配列表视图再依赖 `data.list`。部分 8.6.26 读面在没有 query/view 配置时会把业务列投影为 null;单行核对可用 `data.load`。1.1.25 复证(2026-10-31):脚手架默认视图只含 createdTime/creater/name/sequenceNo 四个系统列,`data.list` 对不在列面的业务字段一律回 null——**null ≠ 数据丢失**,先 `listview.get` 看列面;修复=`listview.configure {fieldCodes:[…业务列…], replaceColumns:true}`,配完立即回值。
|
|
81
81
|
- **默认列表视图不能单独 `listview.delete`**(平台回 `303025 列表是默认列表`);要清掉它就删模型(`model.delete` 级联)。清理时别把它当失败。
|
|
82
82
|
|
|
83
|
+
## 删除链
|
|
84
|
+
|
|
85
|
+
`model.delete` 带走:默认表单/在线 JS/流程/列表视图/平台预置规则与数据规则;**不带走**:自定义业务规则、业务数据(须先删)。顺序:`onlinejs` → `data.delete` 逐条清数据 → 删自定义规则 → `omc model-purge --code <schema>`(依赖倒序,默认 dry-run)。漏传 `bizObjectId(s)` 会被 `tool-input-invalid` 拒绝并指路 `data.list` 取 id(1.1.25 批量 `bizObjectIds` 被拒,须逐个单删)——无「漏传=全删」暗门(真机实测)。清完 `data.load` 复核。
|
|
86
|
+
|
|
83
87
|
## 按钮
|
|
84
88
|
|
|
85
89
|
- **列表/行级按钮**用 `listview.configure` 的 `buttons`(`useLocation: LIST|ROW`)。
|
|
@@ -164,9 +168,13 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
164
168
|
|
|
165
169
|
- **本 CLI 只负责发布,不负责写 JS**。脚本内容的创作与增量合并不在本 CLI:若当前项目自带在线 JS 编写规范(例如其 `.cpdocs`/技能或项目文档),遵循它;否则向用户/设计者索取已定稿 `source`。不要在 omc 技能里手写/臆造 JS 逻辑。
|
|
166
170
|
- 经 omc 发布时用 `onlinejs.compose`:把已定稿的 `source` 组合进平台 skeleton 的 `customScript`,再 `publish`,并回读 `publishedHtmlJson` + SHA-256。不要用裸脚本覆盖平台 skeleton。
|
|
167
|
-
- **`replace
|
|
171
|
+
- **`replace`/`merge` 拒绝形状是设计**:`replace` 必须传完整 `(function(form){…})`(裸代码会写坏脚本槽);`merge` 传完整外层脚本也被拒(避免嵌套丢内容)。远端脚本为空时:完整脚本 + `replace:true`。
|
|
168
172
|
- 源码不能有 import/export,事件名必须在支持集合内;compose 后读回 draft/published HTML,浏览器不是默认步骤。
|
|
169
173
|
|
|
174
|
+
### 规范查找顺序
|
|
175
|
+
|
|
176
|
+
项目规范:沙盒/项目 `.cpdocs/` → 项目根技能/文档 → 任务书指定路径。cpdocs 的 `--confirm-sha256` 流水线与 omc 互不替代:compose 已内置 dry-run 指纹,等价=replace 前用 `onlinejs.get` 记当前 SHA。
|
|
177
|
+
|
|
170
178
|
## 固定验收顺序
|
|
171
179
|
|
|
172
180
|
执行阶段以 `omc-five-piece-flow` 为准;写面安全只额外强调两点:写前先 `commands/describe/contract` + dry-run 静态校验,写后权威 readback,必要时再用 CLI 触发运行验证。
|
|
@@ -237,4 +245,3 @@ omc call field.publish --env local --args '{"schemaCode":"expense"}'
|
|
|
237
245
|
2. **来源子表字段是集合,不是逐行扇出**:`currentSchemaDataItem:"items.asset"` 会被解析成该子表所有行的值集合,目标主表仍只查询一次。写这个形状能存能发布,但运行 0 行命中(r5-asset 真机)。OMC 0.4.64 起 `rule-graph-cross-model-child-source-fanout` **拒绝**。
|
|
238
246
|
3. **一张单多行逐件回写**(每条明细各改一条另一模型的主表行)没有可用的纯配置形状——走 BIZ_ACTION 自调二开接口,一次处理多行;单行回写用主表字段做 DYNAMIC 来源。
|
|
239
247
|
4. **BRANCH 后接两个跨表 UPDATE** 真机报 50000;改用单个 UPDATE + 条件动作,或二开。
|
|
240
|
-
|