@stalfh233/omc-cli 0.6.17 → 0.6.19
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 +21 -2
- package/dist/cli-stdout.js +1 -0
- package/dist/cli.js +24 -14
- package/dist/field-change.js +7 -1
- package/dist/field-families.js +67 -2
- package/dist/graph.js +31 -7
- package/dist/lanes/app.js +17 -4
- package/dist/lanes/field.js +120 -5
- package/dist/lanes/folder.js +29 -2
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +51 -29
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +41 -9
- package/dist/lanes/programming.js +67 -14
- package/dist/lanes/runtime-data.js +153 -16
- package/dist/mcp-graph.js +32 -13
- package/dist/programming-workflow.js +16 -3
- package/dist/read-shapes.js +3 -3
- package/dist/residue.js +116 -0
- package/dist/rollback.js +54 -19
- package/dist/write.js +6 -2
- package/package.json +3 -3
package/dist/batch-call.js
CHANGED
|
@@ -46,7 +46,12 @@ function failureDetails(entry, args) {
|
|
|
46
46
|
}
|
|
47
47
|
return { hint: "平台回了无明细 50000,常见成因见 help omc-refusals;请用该行输入与已发布字段逐项核对" };
|
|
48
48
|
}
|
|
49
|
-
export function batchWarnings(argsList) {
|
|
49
|
+
export function batchWarnings(argsList, tool) {
|
|
50
|
+
// Runtime data validation belongs to the runtime-data lane, which has the
|
|
51
|
+
// published field metadata. The old generic `code` requirement fabricated
|
|
52
|
+
// ghost warnings for data.save (models commonly have no code field).
|
|
53
|
+
if (tool === "data.save")
|
|
54
|
+
return [];
|
|
50
55
|
const warnings = [];
|
|
51
56
|
const seenCodes = new Map();
|
|
52
57
|
argsList.forEach((args, index) => {
|
|
@@ -143,6 +148,11 @@ export async function runBatchCall(options) {
|
|
|
143
148
|
? String(readback.authoritative.id)
|
|
144
149
|
: undefined;
|
|
145
150
|
const skippedMarker = performed?.skipped;
|
|
151
|
+
const applicationCode = typeof performed?.applicationCode === "string"
|
|
152
|
+
? String(performed.applicationCode)
|
|
153
|
+
: typeof entry.result.readback?.applicationCode === "string"
|
|
154
|
+
? String(entry.result.readback.applicationCode)
|
|
155
|
+
: undefined;
|
|
146
156
|
return {
|
|
147
157
|
index: entry.index,
|
|
148
158
|
row: entry.index + 1,
|
|
@@ -150,12 +160,20 @@ export async function runBatchCall(options) {
|
|
|
150
160
|
ok: entry.status === "ok",
|
|
151
161
|
...inputBusinessKey(options.argsList[entry.index] ?? {}),
|
|
152
162
|
...(bizObjectId !== undefined ? { bizObjectId } : {}),
|
|
163
|
+
...(applicationCode !== undefined ? { applicationCode } : {}),
|
|
153
164
|
...(entry.status === "ok" ? {} : { status: String(entry.result.status ?? "failed"), blocker: entry.result.blocker ?? null }),
|
|
154
165
|
...(skippedMarker !== undefined ? { skipped: String(skippedMarker) } : {}),
|
|
155
166
|
};
|
|
156
167
|
});
|
|
157
168
|
const entriesInline = entriesSummary.slice(0, ENTRIES_INLINE_LIMIT);
|
|
158
|
-
const
|
|
169
|
+
const laneWarnings = entries.flatMap((entry) => {
|
|
170
|
+
const warnings = entry.result.readback?.warnings;
|
|
171
|
+
return Array.isArray(warnings)
|
|
172
|
+
? warnings.map((warning) => ({ ...warning, row: entry.index + 1 }))
|
|
173
|
+
: [];
|
|
174
|
+
});
|
|
175
|
+
const warnings = options.tool === "data.save" ? laneWarnings : batchWarnings(options.argsList, options.tool);
|
|
176
|
+
const applicationCode = entries.map((entry) => entry.result.performed?.applicationCode).find((value) => typeof value === "string");
|
|
159
177
|
const warningsInline = warnings.slice(0, ENTRIES_INLINE_LIMIT);
|
|
160
178
|
const detailOmitted = entriesSummary.length > entriesInline.length || warnings.length > warningsInline.length;
|
|
161
179
|
const detailArtifact = detailOmitted
|
|
@@ -170,6 +188,7 @@ export async function runBatchCall(options) {
|
|
|
170
188
|
executed,
|
|
171
189
|
succeeded,
|
|
172
190
|
failed,
|
|
191
|
+
...(applicationCode !== undefined ? { applicationCode } : {}),
|
|
173
192
|
...(skipped > 0 ? { notExecuted: skipped } : {}),
|
|
174
193
|
// 每条一行摘要(index/ok/bizObjectId)——不是完整信封,但足以回答
|
|
175
194
|
// 「第 3 条建成了吗、它的 id 是什么」。失败项另有 failures 带完整原因。
|
package/dist/cli-stdout.js
CHANGED
package/dist/cli.js
CHANGED
|
@@ -57,6 +57,7 @@ import { listSkills, readSkill, readSkillOutline, readSkillSection, searchSkills
|
|
|
57
57
|
import { parseToolInput, ToolInputError } from "./tool-types.js";
|
|
58
58
|
import { WireError, wireCall } from "./wire.js";
|
|
59
59
|
import { DEFAULT_PULL_CONCURRENCY, GraphIndexAbsentError, GraphPullLockHeldError, MAX_PULL_CONCURRENCY, impactGraph, publishGraphPull, queryGraphReferences, readGraphStatus, searchGraphRules, } from "./graph.js";
|
|
60
|
+
import { cmdResidue } from "./residue.js";
|
|
60
61
|
import { ENDPOINTS } from "./endpoints.js";
|
|
61
62
|
import { makeToolContext } from "./context.js";
|
|
62
63
|
import { cmdRollbackList, cmdRollbackPlan, cmdRollbackApply } from "./rollback.js";
|
|
@@ -368,7 +369,7 @@ function parseDoctorStep(line) {
|
|
|
368
369
|
function doctorAnchor() {
|
|
369
370
|
const topics = listSkills().map((entry) => entry.name).join("、");
|
|
370
371
|
return [
|
|
371
|
-
"最小路径 omc next \"<目标>\"(目标→工具一步定位,不确定会明说)→ omc help omc-start-here → omc call discover_environment --env <env> --args '{}'(<env> 名以 doctor 的「环境=」或项目 .omc-env
|
|
372
|
+
"最小路径 omc next \"<目标>\"(目标→工具一步定位,不确定会明说)→ omc help omc-start-here → omc call discover_environment --env <env> --args '{}'(<env> 名以 doctor 的「环境=」或项目 .omc-env 为准,勿猜;项目环境白名单按 cwd 解析,须在含 .omc-env 的目录内运行)→ omc capabilities / omc describe <tool>",
|
|
372
373
|
`方法论 omc help <topic>(或 --topic <name>)--sections / --section "<标题>"(按节读,随包 skills/);topic:${topics}`,
|
|
373
374
|
"大输出 超预算结果只回 artifactId/sha256/fetch;用 fetch 里的 omc artifact get <id>(可加 --jq <path>)取回,勿全量读入",
|
|
374
375
|
// r29f F9:只读面不碰远端,但仍会本地落盘——写清,避免「零写入」核对误判。
|
|
@@ -534,7 +535,7 @@ async function cmdCall(store, args) {
|
|
|
534
535
|
return emitTransportResult({ status: "ok", ...described, helpNote: "来自 omc describe;完整面用 omc describe <tool> [--full|--param|--example]" }, { request, maxOutputBytes });
|
|
535
536
|
}
|
|
536
537
|
}
|
|
537
|
-
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file
|
|
538
|
+
console.error("用法:omc call <tool> [--env <环境>] [--args '<json>' | --args - (stdin) | --args-file <配置根内路径(必须位于配置根内;数组批量请改用 --args-list;跨目录临时文件用 --args -(stdin))>] [--extract <path>]... [--jq [path]] [--extract-keys] [--args-list <json数组>(最多 200 条;超出请分批或用 omc script 读文件) [--preflight]]");
|
|
538
539
|
console.error("输出预算:--max-output-bytes 默认 65536(只约束最终 stdout 信封;读面 diet ~2048 独立外置 detailArtifactId);artifact get 内联上限 8192");
|
|
539
540
|
console.error("写入:由环境 writable 决定(只读环境拒写);生产环境的写必须在 args 里带 confirmProduction:true。");
|
|
540
541
|
return emit(toolName && toolName !== "help" ? "error" : "ok", {}, toolName && toolName !== "help" ? CLI_EXIT_CODES.invalidInput : 0);
|
|
@@ -632,18 +633,20 @@ async function cmdCall(store, args) {
|
|
|
632
633
|
// 原来只在末尾附一句「若问题在引号转义…」,容易被当成补充说明;这里把它
|
|
633
634
|
// 提成主建议,并给出 PS 可粘贴的两条命令。
|
|
634
635
|
const looksLikeShellSplitting = /Unexpected (token|end of JSON)|Expected property name|position \d+/i.test(detail);
|
|
635
|
-
const message =
|
|
636
|
-
?
|
|
637
|
-
:
|
|
638
|
-
? `--args
|
|
639
|
-
:
|
|
636
|
+
const message = argsFile !== undefined && /must be a JSON object/.test(detail)
|
|
637
|
+
? `${detail};--args-file 只收单个 JSON 对象,批量数组请改用 --args-list(最多 200 条,可分批执行)`
|
|
638
|
+
: outOfRoot
|
|
639
|
+
? `--args-file 越界:${detail}。可执行做法:把参数文件放到 ${configRoot} 之下(如 ${join(configRoot, "args.json")}),再用 --args-file 引用;不要把参数文件放在系统临时目录(配置根之外一律拒绝)`
|
|
640
|
+
: looksLikeShellSplitting
|
|
641
|
+
? `--args 不像合法 JSON(${detail})——这个形态通常是 shell 把内联 JSON 拆坏了(Windows PowerShell 的引号/转义规则),不是你写错了字段。改法二选一:① 把 JSON 写进 ${join(configRoot, "args.json")} 再用 --args-file;② \`omc call <tool> --args - \` 从 stdin 读(PS: \`'{"k":"v"}' | omc call <tool> --args -\`)。POSIX 文档里的单引号写法不能照抄到 PS`
|
|
642
|
+
: `--args 必须是合法 JSON:${detail}。若问题在引号转义(Windows PowerShell),改用 --args-file <配置根内的路径> 或 --args -(stdin)`;
|
|
640
643
|
console.error(message);
|
|
641
644
|
// The MACHINE envelope must carry the same actionable text: agents read
|
|
642
645
|
// stdout, and the old envelope only repeated the English detail.
|
|
643
646
|
// Keep the REJECTED path on stderr only: the machine envelope must not leak
|
|
644
647
|
// host layout (ticket 103), so it carries the root plus the bare file name.
|
|
645
648
|
const rejectedName = typeof argsFile === "string" ? basename(resolve(configRoot, argsFile)) : "";
|
|
646
|
-
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: outOfRoot ? `--args-file 必须位于配置根内(配置根 ${configRoot}${rejectedName ? `;本次文件 ${rejectedName}` : ""})。免落盘替代:--args - 从 stdin 读 JSON(echo '<json>' | omc call <tool> --args -),或把文件放进配置根后再引用;限制是为了防参数文件携带凭据被收进会话产物` : detail, retryable: false, ...(outOfRoot ? { configurationRoot: configRoot } : {}) }));
|
|
649
|
+
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: outOfRoot ? `--args-file 必须位于配置根内(配置根 ${configRoot}${rejectedName ? `;本次文件 ${rejectedName}` : ""})。免落盘替代:--args - 从 stdin 读 JSON(echo '<json>' | omc call <tool> --args -),或把文件放进配置根后再引用;限制是为了防参数文件携带凭据被收进会话产物` : argsFile !== undefined && /must be a JSON object/.test(detail) ? `${detail};批量数组请改用 --args-list(最多 200 条,可分批执行)` : detail, retryable: false, ...(outOfRoot ? { configurationRoot: configRoot } : {}) }));
|
|
647
650
|
return CLI_EXIT_CODES.invalidInput;
|
|
648
651
|
}
|
|
649
652
|
const registry = buildToolRegistry();
|
|
@@ -984,7 +987,6 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
984
987
|
...artifactProjectionMetadata(content),
|
|
985
988
|
keys,
|
|
986
989
|
path: options.jq,
|
|
987
|
-
content: projected.content ?? null,
|
|
988
990
|
projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }],
|
|
989
991
|
...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}),
|
|
990
992
|
...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}),
|
|
@@ -1036,7 +1038,7 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
1036
1038
|
const projected = projectArtifactPath(artifactContent, options.jq);
|
|
1037
1039
|
return emitTransportResult(projected.missed
|
|
1038
1040
|
? { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, unanswered: true, hint: artifactPathMissHint(projected.at) }
|
|
1039
|
-
: { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq,
|
|
1041
|
+
: { status: "ok", artifactId: id, sha256: artifact.sha256, ...artifactProjectionMetadata(content), path: options.jq, projections: [{ path: options.jq, content: projected.content ?? null, ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}) }], ...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}), ...(projected.missedEntries !== undefined ? { missedEntries: projected.missedEntries } : {}), ...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}), ...(artifactNullHint(projected.content, projected.nullEntries) !== undefined ? { hint: artifactNullHint(projected.content, projected.nullEntries) } : {}) }, transport);
|
|
1040
1042
|
}
|
|
1041
1043
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
1042
1044
|
// large ones (up to 55 KB) pulled whole workflow templates just to read one
|
|
@@ -1624,6 +1626,7 @@ async function cmdGraphPull(environment, concurrency, transport) {
|
|
|
1624
1626
|
identityFingerprint: data.meta.identityFingerprint,
|
|
1625
1627
|
pulledAtIso: data.meta.pulledAtIso,
|
|
1626
1628
|
scope: data.meta.scope,
|
|
1629
|
+
coverage: { applicationCodes: [data.meta.applicationCode] },
|
|
1627
1630
|
freshness: data.gaps.length === 0 ? "complete" : "partial",
|
|
1628
1631
|
scanned: { models: data.scanned.models, rules: data.scanned.rules, datarules: data.scanned.datarules, views: data.scanned.views, ...(data.scanned.parseFailures > 0 ? { parseFailures: data.scanned.parseFailures } : {}) },
|
|
1629
1632
|
edges: edgesByKind,
|
|
@@ -2162,7 +2165,7 @@ async function main() {
|
|
|
2162
2165
|
commandExitCode = CLI_EXIT_CODES.ok;
|
|
2163
2166
|
});
|
|
2164
2167
|
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); });
|
|
2165
|
-
command.command("call <tool>").option("--env <environment>").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON
|
|
2168
|
+
command.command("call <tool>").option("--env <environment>", "环境名;项目白名单按当前 cwd 解析,须在含 .omc-env 的目录内运行").option("--environment <environment>").option("--args <json>").option("--args-file <path>", "从文件读参数 JSON。**必须位于配置根内**(数组批量请改用 --args-list;跨目录/系统临时文件会被拒);免落盘用 `--args -`(stdin)").option("--args-list <json-array>", '同一工具跑 N 次(JSON 数组,每条一个 args 对象,最多 200 条);超过上限请分批,或用 omc script 读文件。逐条执行、默认不中断,最后回 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) => {
|
|
2166
2169
|
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"] : []),
|
|
2167
2170
|
...(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"] : [])];
|
|
2168
2171
|
commandExitCode = await cmdCall(new ConfigStore(defaultPaths()), rest);
|
|
@@ -2408,15 +2411,21 @@ async function main() {
|
|
|
2408
2411
|
script: { lines: lines.length, skipped: parsed.skipped.length },
|
|
2409
2412
|
}, 0);
|
|
2410
2413
|
});
|
|
2411
|
-
const rollbackCommand = command.command("rollback").description("配置回滚(git-revert 语义):list 列写时间线 / plan 预览恢复(diff+后续写覆盖警告)/ apply 执行恢复——恢复走完整写闸门并记 journal
|
|
2414
|
+
const rollbackCommand = command.command("rollback").description("配置回滚(git-revert 语义):list 列写时间线 / plan 预览恢复(diff+后续写覆盖警告)/ apply 执行恢复——恢复走完整写闸门并记 journal(回滚可再回滚)。apply 支持 --wait <毫秒> 有界排队;list/plan 为只读面不接写锁。接入全部配置写面(workflow/规则/数据规则/视图/表单草稿/在线JS/字典/字段/模型/应用/报表);publish 类生命周期推进不可逆(plan 会说明);journal 与 before 快照为本机边界(不跨机器)");
|
|
2412
2415
|
rollbackCommand.command("list").option("--env <environment>").option("--environment <environment>").option("--target <substr>").option("--tool <name>").option("--limit <n>").action(async (options) => {
|
|
2413
2416
|
commandExitCode = await withTransport(options, "rollback list", (transport) => cmdRollbackList({ environment: options.env ?? options.environment, target: options.target, tool: options.tool, limit: options.limit }, transport));
|
|
2414
2417
|
});
|
|
2415
2418
|
rollbackCommand.command("plan <ref>").option("--env <environment>").option("--environment <environment>").action(async (ref, options) => {
|
|
2416
2419
|
commandExitCode = await withTransport(options, "rollback plan", (transport) => cmdRollbackPlan(ref, { environment: options.env ?? options.environment }, transport));
|
|
2417
2420
|
});
|
|
2418
|
-
rollbackCommand.command("apply <ref>").option("--env <environment>").option("--environment <environment>").option("--confirm-production").action(async (ref, options) => {
|
|
2419
|
-
|
|
2421
|
+
rollbackCommand.command("apply <ref>").option("--env <environment>").option("--environment <environment>").option("--confirm-production").option("--wait <milliseconds>", "写锁冲突时的有界排队毫秒数(默认 0=立刻拒绝)").action(async (ref, options) => {
|
|
2422
|
+
const waitMs = options.wait === undefined ? 0 : Number(options.wait);
|
|
2423
|
+
if (!Number.isSafeInteger(waitMs) || waitMs < 0 || waitMs > 600_000) {
|
|
2424
|
+
console.error("--wait must be an integer number of milliseconds from 0 to 600000");
|
|
2425
|
+
commandExitCode = CLI_EXIT_CODES.invalidInput;
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
commandExitCode = await withTransport(options, "rollback apply", (transport) => cmdRollbackApply(ref, { environment: options.env ?? options.environment, confirmProduction: options.confirmProduction === true, waitMs }, transport));
|
|
2420
2429
|
});
|
|
2421
2430
|
const artifactCommand = command.command("artifact");
|
|
2422
2431
|
artifactCommand.command("prune").option("--older-than <duration>", "删除超过该时长的读回产物(如 7d/12h/30m)").option("--max-bytes <bytes>", "产物目录预算;超出时按最旧优先删除").option("--json").action((options) => {
|
|
@@ -2461,6 +2470,7 @@ async function main() {
|
|
|
2461
2470
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2462
2471
|
});
|
|
2463
2472
|
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 即零残留。只读零写入;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)); });
|
|
2464
2474
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2465
2475
|
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 } : {}) })); });
|
|
2466
2476
|
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/field-change.js
CHANGED
|
@@ -59,7 +59,12 @@ export function prepareFieldChange(input) {
|
|
|
59
59
|
assertFormConditions(raw);
|
|
60
60
|
const options = ensureRelationDisplayFieldOptions(propertyType, raw);
|
|
61
61
|
const displayCode = relationDisplayFieldCode(propertyType, options);
|
|
62
|
-
|
|
62
|
+
const optionRelationTarget = typeof options.schemaCode === "string" ? options.schemaCode.trim() : "";
|
|
63
|
+
const normalizedRelation = (propertyType === FIELD_PROPERTY_TYPES.WORK_SHEET || propertyType === FIELD_PROPERTY_TYPES.MULT_WORK_SHEET) &&
|
|
64
|
+
optionRelationTarget.length > 0 && !(typeof input.relativeCode === "string" && input.relativeCode.trim().length > 0)
|
|
65
|
+
? { relationTarget: "options.schemaCode→relativeCode" }
|
|
66
|
+
: undefined;
|
|
67
|
+
return { propertyType, isChildTable, options, ...(displayCode ? { displayCode } : {}), ...(normalizedRelation ? { normalized: normalizedRelation } : {}) };
|
|
63
68
|
}
|
|
64
69
|
/** The single platform wire body for a field create. */
|
|
65
70
|
export function fieldChangeWireBody(input, prepared) {
|
|
@@ -74,6 +79,7 @@ export function fieldChangeWireBody(input, prepared) {
|
|
|
74
79
|
? { defaultValue: null, subSchema: prepared.subSchema }
|
|
75
80
|
: { defaultValue: "", ...(prepared.displayCode ? { relativePropertyCode: prepared.displayCode } : {}), options: JSON.stringify(prepared.options) }),
|
|
76
81
|
...(input.relativeCode ? { relativeCode: input.relativeCode } : {}),
|
|
82
|
+
...(!input.relativeCode && prepared.normalized ? { relativeCode: String(prepared.options.schemaCode) } : {}),
|
|
77
83
|
};
|
|
78
84
|
}
|
|
79
85
|
/**
|
package/dist/field-families.js
CHANGED
|
@@ -15,7 +15,7 @@ export const propertyTypeArg = z
|
|
|
15
15
|
export const relativeCodeArg = z
|
|
16
16
|
.string()
|
|
17
17
|
.optional()
|
|
18
|
-
.describe("
|
|
18
|
+
.describe("关联目标 code;租户唯一、按 code 全局解析,可跨应用");
|
|
19
19
|
/**
|
|
20
20
|
* Ticket 59 (P0): the wire `name_i18n` column must be a *parseable JSON object
|
|
21
21
|
* string*, never a bare display string. The admin designer runs an unguarded
|
|
@@ -167,7 +167,7 @@ export const subFieldArg = z
|
|
|
167
167
|
*/
|
|
168
168
|
export const fieldOptionsArg = z
|
|
169
169
|
.object({
|
|
170
|
-
schemaCode: z.string().optional().describe("
|
|
170
|
+
schemaCode: z.string().optional().describe("关联目标 code;租户唯一、按 code 全局解析,可跨应用"),
|
|
171
171
|
queryCode: z.string().optional().describe("relation query code (usually the target model code)"),
|
|
172
172
|
displayField: z.string().optional().describe("relation display field (usually 'name')"),
|
|
173
173
|
mappings: z
|
|
@@ -270,6 +270,13 @@ export const FIELD_TYPE_SYNONYMS = Object.freeze({
|
|
|
270
270
|
});
|
|
271
271
|
/** A "did you mean" hint for a family name that is actually a Chinese concept. */
|
|
272
272
|
export function fieldTypeHint(type) {
|
|
273
|
+
const commonAliases = {
|
|
274
|
+
SINGLE_TEXT: "SHORT_TEXT",
|
|
275
|
+
NUMERIC: "NUMERICAL",
|
|
276
|
+
};
|
|
277
|
+
const alias = commonAliases[type.toUpperCase()];
|
|
278
|
+
if (alias)
|
|
279
|
+
return `「${type}」不是族名;你要找的可能是 ${alias}`;
|
|
273
280
|
for (const [concept, family] of Object.entries(FIELD_TYPE_SYNONYMS))
|
|
274
281
|
if (type.includes(concept) || concept.includes(type))
|
|
275
282
|
return `「${type}」看起来是概念名而非族名;最接近的是 ${family}`;
|
|
@@ -281,6 +288,64 @@ export function fieldTypeHint(type) {
|
|
|
281
288
|
return `「${type}」不是族名;你要找的可能是 ${closest}`;
|
|
282
289
|
return undefined;
|
|
283
290
|
}
|
|
291
|
+
/**
|
|
292
|
+
* Validate a model-relation target before bizproperty/create. The platform
|
|
293
|
+
* accepts the row and only fails at model publish (300010), which leaves the
|
|
294
|
+
* whole model poisoned. Callers supply the already-read model list so this
|
|
295
|
+
* helper remains usable by both field entry points without owning transport.
|
|
296
|
+
*/
|
|
297
|
+
export function assertRelationTargetExists(args, candidates) {
|
|
298
|
+
const raw = args.propertyType;
|
|
299
|
+
const type = typeof raw === "number" && Number.isInteger(raw)
|
|
300
|
+
? raw
|
|
301
|
+
: typeof raw === "string" && raw in FIELD_PROPERTY_TYPES ? FIELD_PROPERTY_TYPES[raw] : undefined;
|
|
302
|
+
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
303
|
+
return;
|
|
304
|
+
const relativeCode = typeof args.relativeCode === "string" ? args.relativeCode.trim() : "";
|
|
305
|
+
const optionsCode = typeof args.options?.schemaCode === "string" ? args.options.schemaCode.trim() : "";
|
|
306
|
+
const target = relativeCode || optionsCode;
|
|
307
|
+
// Some offline/fake transports do not expose a model-list face. An empty
|
|
308
|
+
// candidate set is therefore "unknown", not proof of absence; live model
|
|
309
|
+
// lists are non-empty and get the strict pre-write gate.
|
|
310
|
+
if (!target || candidates.length === 0 || candidates.includes(target))
|
|
311
|
+
return;
|
|
312
|
+
const visibleCandidates = [...candidates].sort().slice(0, 5);
|
|
313
|
+
throw new ToolRefusalError({
|
|
314
|
+
status: "refused",
|
|
315
|
+
blocker: "relation-target-not-found",
|
|
316
|
+
relationTarget: target,
|
|
317
|
+
candidates: visibleCandidates,
|
|
318
|
+
message: `关联字段 ${String(args.code ?? "(未命名)")} 的关联目标 ${target} 不存在;已在写入前拒绝。可选模型:${visibleCandidates.length > 0 ? visibleCandidates.join("、") : "(无)"}`,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
/** Construct the field-specific explanation when an older platform leaks 300010. */
|
|
322
|
+
export function relationPublishFailureMessage(fields, candidates) {
|
|
323
|
+
for (const field of fields) {
|
|
324
|
+
let options;
|
|
325
|
+
if (field.options && typeof field.options === "object" && !Array.isArray(field.options))
|
|
326
|
+
options = field.options;
|
|
327
|
+
else if (typeof field.options === "string") {
|
|
328
|
+
try {
|
|
329
|
+
const parsed = JSON.parse(field.options);
|
|
330
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed))
|
|
331
|
+
options = parsed;
|
|
332
|
+
}
|
|
333
|
+
catch { /* malformed options will be reported by the platform */ }
|
|
334
|
+
}
|
|
335
|
+
const raw = field.propertyType ?? field.type;
|
|
336
|
+
const type = typeof raw === "number" ? raw : typeof raw === "string" && raw in FIELD_PROPERTY_TYPES ? FIELD_PROPERTY_TYPES[raw] : undefined;
|
|
337
|
+
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
338
|
+
continue;
|
|
339
|
+
const relativeCode = typeof field.relativeCode === "string" ? field.relativeCode.trim() : "";
|
|
340
|
+
const target = relativeCode || (typeof options?.schemaCode === "string" ? options.schemaCode.trim() : "");
|
|
341
|
+
if (typeof options?.schemaCode === "string" && options.schemaCode.trim() && (!field.relativeCode || String(field.relativeCode).trim() === "")) {
|
|
342
|
+
return `字段 ${String(field.code ?? "(未知)")} 的关联目标 ${options.schemaCode} 仅写在 options.schemaCode;发布面不解析该位置(平台原始 errcode 300010「数据模型编码不能为空!」)。请重建/归一化为顶层 relativeCode=${options.schemaCode}`;
|
|
343
|
+
}
|
|
344
|
+
if (target && candidates.length > 0 && !candidates.includes(target))
|
|
345
|
+
return `字段 ${String(field.code ?? "(未知)")} 的关联目标 ${target} 不存在(平台原始 errcode 300010「数据模型编码不能为空!」);可选模型:${candidates.slice(0, 5).join("、")}`;
|
|
346
|
+
}
|
|
347
|
+
return undefined;
|
|
348
|
+
}
|
|
284
349
|
/** Case/underscore-insensitive closest match over the family list (shared helper, see closest-match.ts). */
|
|
285
350
|
function closestFamilyName(type) {
|
|
286
351
|
return closestMatch(type, Object.keys(FIELD_PROPERTY_TYPES));
|
package/dist/graph.js
CHANGED
|
@@ -52,12 +52,19 @@ export const DEFAULT_PULL_CONCURRENCY = 16;
|
|
|
52
52
|
export const MAX_PULL_CONCURRENCY = 32;
|
|
53
53
|
/** 索引超过这个天数就提示刷新(配置类查询,1 天足够新)。 */
|
|
54
54
|
export const STALE_INDEX_DAYS = 1;
|
|
55
|
+
export const STALE_AFTER_MINUTES = STALE_INDEX_DAYS * 24 * 60;
|
|
55
56
|
export function indexAgeDays(pulledAtIso, now = Date.now()) {
|
|
56
57
|
const pulled = Date.parse(pulledAtIso);
|
|
57
58
|
if (Number.isNaN(pulled))
|
|
58
59
|
return 0;
|
|
59
60
|
return Math.max(0, Math.floor((now - pulled) / 86_400_000));
|
|
60
61
|
}
|
|
62
|
+
export function indexAgeMinutes(pulledAtIso, now = Date.now()) {
|
|
63
|
+
const pulled = Date.parse(pulledAtIso);
|
|
64
|
+
if (Number.isNaN(pulled))
|
|
65
|
+
return 0;
|
|
66
|
+
return Math.max(0, Math.floor((now - pulled) / 60_000));
|
|
67
|
+
}
|
|
61
68
|
export class GraphIndexAbsentError extends Error {
|
|
62
69
|
constructor(message) {
|
|
63
70
|
super(message);
|
|
@@ -404,6 +411,7 @@ export async function pullRuleGraph(session, concurrency = DEFAULT_PULL_CONCURRE
|
|
|
404
411
|
identityFingerprint: identity,
|
|
405
412
|
environmentName: session.environmentName,
|
|
406
413
|
applicationCode,
|
|
414
|
+
coverage: { applicationCodes: [applicationCode] },
|
|
407
415
|
...(scope !== undefined ? { applicationResolution: applicationResolutionOf(scope) } : {}),
|
|
408
416
|
pulledAtIso,
|
|
409
417
|
},
|
|
@@ -611,11 +619,14 @@ function openGraphIndex() {
|
|
|
611
619
|
}
|
|
612
620
|
const pulledAtIso = meta.pulledAtIso ?? "";
|
|
613
621
|
const ageDays = indexAgeDays(pulledAtIso);
|
|
622
|
+
const ageMinutes = indexAgeMinutes(pulledAtIso);
|
|
623
|
+
const coverage = { applicationCodes: meta.applicationCode ? [meta.applicationCode] : [] };
|
|
614
624
|
return {
|
|
615
625
|
db,
|
|
616
626
|
summary: {
|
|
617
627
|
environmentName: meta.environmentName ?? "",
|
|
618
628
|
applicationCode: meta.applicationCode ?? "",
|
|
629
|
+
coverage,
|
|
619
630
|
// Persisted as JSON by the writer; a legacy index (pre-0.5.4) has no
|
|
620
631
|
// such key, so this stays optional rather than failing the read.
|
|
621
632
|
...(meta.applicationResolution ? { applicationResolution: parseApplicationResolution(meta.applicationResolution) } : {}),
|
|
@@ -623,6 +634,8 @@ function openGraphIndex() {
|
|
|
623
634
|
pulledAtIso,
|
|
624
635
|
freshness: gapCount === 0 ? "complete" : "partial",
|
|
625
636
|
ageDays,
|
|
637
|
+
ageMinutes,
|
|
638
|
+
staleAfterMinutes: STALE_AFTER_MINUTES,
|
|
626
639
|
// r31a-F16:旧索引 + complete 标签 = 静默错答。给可直接照抄的刷新命令,
|
|
627
640
|
// 并明确「complete 只指扫描无缺口」。
|
|
628
641
|
...(ageDays >= STALE_INDEX_DAYS
|
|
@@ -696,7 +709,7 @@ export function queryGraphReferences(kind, code) {
|
|
|
696
709
|
? {
|
|
697
710
|
note: (gapped
|
|
698
711
|
? "零命中,但索引存在扫描缺口——不可作为「无引用」证明;先 omc graph pull 重建再查"
|
|
699
|
-
:
|
|
712
|
+
: `零命中=仅 scanned 面(规则/数据规则/视图)没有引用它;本索引覆盖应用:${summary.coverage.applicationCodes.join("、") || "(未知)"},不代表其他应用没有引用;form(表单控件)与 workflow.permissions 未扫描,见 coverage.notScanned——不可据此判定可安全删除`) +
|
|
700
713
|
(zeroHitKindHint(db, kind, code, "references") ?? ""),
|
|
701
714
|
}
|
|
702
715
|
: {}),
|
|
@@ -719,6 +732,10 @@ export function readGraphStatus() {
|
|
|
719
732
|
dbBytes: statSync(dbPath).size,
|
|
720
733
|
dbPath,
|
|
721
734
|
dbId: sha256(dbPath).slice(0, 12),
|
|
735
|
+
ageMinutes: summary.ageMinutes,
|
|
736
|
+
staleAfterMinutes: summary.staleAfterMinutes,
|
|
737
|
+
...(summary.staleWarning ? { warning: summary.staleWarning } : {}),
|
|
738
|
+
coverage: summary.coverage,
|
|
722
739
|
gaps,
|
|
723
740
|
};
|
|
724
741
|
}
|
|
@@ -733,7 +750,7 @@ export function readGraphStatus() {
|
|
|
733
750
|
* a prefix match (`引用*`), which is also what makes CJK usable: unicode61
|
|
734
751
|
* stores a CJK run as one token, so a full-run query hits only by prefix.
|
|
735
752
|
*/
|
|
736
|
-
export function searchGraphRules(term, limit = 20) {
|
|
753
|
+
export function searchGraphRules(term, limit = 20, offset = 0) {
|
|
737
754
|
const { db, summary } = openGraphIndex();
|
|
738
755
|
try {
|
|
739
756
|
const tokens = term.trim().split(/\s+/).filter((token) => token !== "");
|
|
@@ -748,8 +765,8 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
748
765
|
const rows = db
|
|
749
766
|
.prepare(`SELECT r.schema_code, r.rule_code, r.name, r.enabled, r.status
|
|
750
767
|
FROM rules_fts JOIN rules r ON r.rowid = rules_fts.rowid
|
|
751
|
-
WHERE rules_fts MATCH ? ORDER BY bm25(rules_fts), r.schema_code, r.rule_code LIMIT ?`)
|
|
752
|
-
.all(quoted, limit);
|
|
768
|
+
WHERE rules_fts MATCH ? ORDER BY bm25(rules_fts), r.schema_code, r.rule_code LIMIT ? OFFSET ?`)
|
|
769
|
+
.all(quoted, limit, offset);
|
|
753
770
|
const refsFor = db.prepare("SELECT kind, code FROM edges WHERE schema_code = ? AND rule_code = ? ORDER BY kind, code");
|
|
754
771
|
matches = rows.map((row) => {
|
|
755
772
|
const refs = { service: [], model: [], field: [] };
|
|
@@ -767,8 +784,15 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
767
784
|
});
|
|
768
785
|
const searchEntity = (countSql, rowsSql, params, map) => {
|
|
769
786
|
const total = Number(db.prepare(countSql).get(...params).n);
|
|
770
|
-
const rows = db.prepare(rowsSql).all(...params, limit);
|
|
771
|
-
return {
|
|
787
|
+
const rows = db.prepare(rowsSql.replace("LIMIT ?", "LIMIT ? OFFSET ?")).all(...params, limit, offset);
|
|
788
|
+
return {
|
|
789
|
+
count: rows.length,
|
|
790
|
+
total,
|
|
791
|
+
items: rows.map(map),
|
|
792
|
+
offset,
|
|
793
|
+
limit,
|
|
794
|
+
...(offset + rows.length < total ? { truncated: true, nextOffset: offset + rows.length } : {}),
|
|
795
|
+
};
|
|
772
796
|
};
|
|
773
797
|
entityMatches.model = searchEntity("SELECT COUNT(*) AS n FROM models WHERE instr(name, ?) > 0 OR instr(schema_code, ?) > 0", "SELECT schema_code, name FROM models WHERE instr(name, ?) > 0 OR instr(schema_code, ?) > 0 ORDER BY schema_code LIMIT ?", [term.trim(), term.trim()], (row) => ({ schemaCode: row.schema_code, name: row.name }));
|
|
774
798
|
entityMatches.field = searchEntity("SELECT COUNT(*) AS n FROM fields WHERE instr(name, ?) > 0 OR instr(code, ?) > 0", "SELECT schema_code, code, name FROM fields WHERE instr(name, ?) > 0 OR instr(code, ?) > 0 ORDER BY schema_code, code LIMIT ?", [term.trim(), term.trim()], (row) => ({ schemaCode: row.schema_code, code: row.code, name: row.name }));
|
|
@@ -789,7 +813,7 @@ export function searchGraphRules(term, limit = 20) {
|
|
|
789
813
|
// 实测摩擦(2026-10-30,zlt测试):中文子串 0 命中后没有下一步提示,
|
|
790
814
|
// 调用方在「换个词再试」里打转。把两个出口都写进 note:CJK 词首
|
|
791
815
|
// 通配 + 找引用走 references/impact。
|
|
792
|
-
:
|
|
816
|
+
: `实体检索:没有命中模型/字段/服务的 name 或 code;本索引覆盖应用:${summary.coverage.applicationCodes.join("、") || "(未知)"},不代表其他应用没有该实体。\n规则检索:没有命中规则 code/name;规则搜索只匹配词首(例如「增加预算」是一个词,「预算」匹配不到),请给全词、词首或英文 code 前缀。找"谁引用了 X"请走 references/impact <kind> <code>。`,
|
|
793
817
|
}
|
|
794
818
|
: {}),
|
|
795
819
|
...(gapped ? { gapWarning: `索引有 ${summary.gaps.count} 个扫描缺口;命中列表可能不完整` } : {}),
|
package/dist/lanes/app.js
CHANGED
|
@@ -5,10 +5,22 @@ import { governedRead } from "../read.js";
|
|
|
5
5
|
import { writeTool } from "../write.js";
|
|
6
6
|
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
7
7
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
8
|
+
import { resolveApplicationScope } from "../support.js";
|
|
8
9
|
import { APP_LANE_CONTRACT } from "./app-contract.js";
|
|
9
10
|
import { readAppTree } from "./folder.js";
|
|
10
11
|
const APP_LANE = APP_LANE_CONTRACT.lane;
|
|
11
12
|
const APP_TOOLS = APP_LANE_CONTRACT.tools;
|
|
13
|
+
export async function resolveValidatedApplicationScope(session, requested) {
|
|
14
|
+
const scope = await resolveApplicationScope(session, requested);
|
|
15
|
+
const availableApps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
|
|
16
|
+
.map((row) => String(row.appCode ?? row.code ?? "")).filter((code) => code !== "");
|
|
17
|
+
if (availableApps.length > 0 && !availableApps.includes(scope.code)) {
|
|
18
|
+
throw new ToolRefusalError({ status: "refused", blocker: "app-not-found", applicationCode: scope.code,
|
|
19
|
+
applicationResolution: { status: "unresolved", source: scope.source, requested: scope.requested ?? scope.code, available: availableApps.slice(0, 40) },
|
|
20
|
+
message: `应用「${scope.code}」不在租户 app.list 里(不是「该应用没有模型」)。可用:${availableApps.slice(0, 12).join("、")}${availableApps.length > 12 ? " 等" : ""};先 app.list 取正确 applicationCode。` });
|
|
21
|
+
}
|
|
22
|
+
return scope;
|
|
23
|
+
}
|
|
12
24
|
function appRollbackSpec(toolName) {
|
|
13
25
|
return {
|
|
14
26
|
target: (args) => `app:${String(args.code ?? "")}`,
|
|
@@ -48,10 +60,11 @@ export function registerAppLane(tools) {
|
|
|
48
60
|
description: APP_TOOLS["app.list"].description,
|
|
49
61
|
inputSchema: { environment: environmentArg, capability: capabilityArg },
|
|
50
62
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
51
|
-
const applications = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data).map((row) =>
|
|
52
|
-
code
|
|
53
|
-
|
|
54
|
-
|
|
63
|
+
const applications = await Promise.all(wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data).map(async (row) => {
|
|
64
|
+
const code = String(row.appCode ?? row.code);
|
|
65
|
+
const tree = await readAppTree(session, code);
|
|
66
|
+
const modelCount = tree.reduce((count, node) => count + (node.type === "BizModel" ? 1 : 0), 0);
|
|
67
|
+
return { code, name: row.appName ?? row.name, groupId: row.groupId, modelCount };
|
|
55
68
|
}));
|
|
56
69
|
// r29f F2:与 discover_environment 同构——一眼判断「内联是否全集」。
|
|
57
70
|
// 应用通常个位数,默认内联 + omitted:false;大租户由 governedRead 外置时
|