@stalfh233/omc-cli 0.6.18 → 0.6.20
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-stdout.js +1 -0
- package/dist/cli.js +7 -6
- package/dist/config-store.js +20 -1
- package/dist/field-families.js +6 -5
- package/dist/lanes/field.js +11 -5
- package/dist/lanes/folder.js +29 -2
- package/dist/mcp-graph.js +108 -18
- package/dist/residue.js +116 -0
- package/dist/tool-types.js +14 -2
- package/package.json +3 -3
- package/skills/omc-refusals.md +5 -0
- package/skills/omc-start-here.md +5 -0
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-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";
|
|
@@ -986,7 +987,6 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
986
987
|
...artifactProjectionMetadata(content),
|
|
987
988
|
keys,
|
|
988
989
|
path: options.jq,
|
|
989
|
-
content: projected.content ?? null,
|
|
990
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 } : {}) }],
|
|
991
991
|
...(projected.mapped !== undefined ? { mapped: projected.mapped } : {}),
|
|
992
992
|
...(projected.nullEntries !== undefined && projected.nullEntries > 0 ? { nullEntries: projected.nullEntries } : {}),
|
|
@@ -1038,7 +1038,7 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
1038
1038
|
const projected = projectArtifactPath(artifactContent, options.jq);
|
|
1039
1039
|
return emitTransportResult(projected.missed
|
|
1040
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) }
|
|
1041
|
-
: { 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);
|
|
1042
1042
|
}
|
|
1043
1043
|
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
1044
1044
|
// large ones (up to 55 KB) pulled whole workflow templates just to read one
|
|
@@ -1666,7 +1666,7 @@ function cmdGraphReferences(kind, code, transport, requestedEnvironment) {
|
|
|
1666
1666
|
}
|
|
1667
1667
|
catch (error) {
|
|
1668
1668
|
if (error instanceof GraphIndexAbsentError)
|
|
1669
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1669
|
+
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
1670
|
return emitThrown(error, transport);
|
|
1671
1671
|
}
|
|
1672
1672
|
}
|
|
@@ -1680,7 +1680,7 @@ function cmdGraphStatus(transport) {
|
|
|
1680
1680
|
}
|
|
1681
1681
|
catch (error) {
|
|
1682
1682
|
if (error instanceof GraphIndexAbsentError)
|
|
1683
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1683
|
+
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
1684
|
return emitThrown(error, transport);
|
|
1685
1685
|
}
|
|
1686
1686
|
}
|
|
@@ -1693,7 +1693,7 @@ function cmdGraphImpact(kind, code, transport) {
|
|
|
1693
1693
|
}
|
|
1694
1694
|
catch (error) {
|
|
1695
1695
|
if (error instanceof GraphIndexAbsentError)
|
|
1696
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1696
|
+
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
1697
|
return emitThrown(error, transport);
|
|
1698
1698
|
}
|
|
1699
1699
|
}
|
|
@@ -1704,7 +1704,7 @@ function cmdGraphSearch(term, transport) {
|
|
|
1704
1704
|
}
|
|
1705
1705
|
catch (error) {
|
|
1706
1706
|
if (error instanceof GraphIndexAbsentError)
|
|
1707
|
-
return emitResult({ status: "refused", blocker: "graph-index-absent", message: error.message
|
|
1707
|
+
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
1708
|
return emitThrown(error, transport);
|
|
1709
1709
|
}
|
|
1710
1710
|
}
|
|
@@ -2470,6 +2470,7 @@ async function main() {
|
|
|
2470
2470
|
pureKeys: (options.keys === true || bareJq) && options.summary !== true, ...(typeof options.keys === "string" ? { keys: options.keys } : {}), full: options.full === true }));
|
|
2471
2471
|
});
|
|
2472
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)); });
|
|
2473
2474
|
const jqOption = () => new Option("--jq [path]", "路径投影(与 call --jq 同 DSL;可逗号多路径;裸 --jq 列顶层键)").argParser((value, previous = []) => [...previous, value]).default([]);
|
|
2474
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 } : {}) })); });
|
|
2475
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/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/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
|
|
@@ -301,9 +301,9 @@ export function assertRelationTargetExists(args, candidates) {
|
|
|
301
301
|
: typeof raw === "string" && raw in FIELD_PROPERTY_TYPES ? FIELD_PROPERTY_TYPES[raw] : undefined;
|
|
302
302
|
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
303
303
|
return;
|
|
304
|
-
const optionsCode = typeof args.options?.schemaCode === "string" ? args.options.schemaCode.trim() : "";
|
|
305
304
|
const relativeCode = typeof args.relativeCode === "string" ? args.relativeCode.trim() : "";
|
|
306
|
-
const
|
|
305
|
+
const optionsCode = typeof args.options?.schemaCode === "string" ? args.options.schemaCode.trim() : "";
|
|
306
|
+
const target = relativeCode || optionsCode;
|
|
307
307
|
// Some offline/fake transports do not expose a model-list face. An empty
|
|
308
308
|
// candidate set is therefore "unknown", not proof of absence; live model
|
|
309
309
|
// lists are non-empty and get the strict pre-write gate.
|
|
@@ -336,7 +336,8 @@ export function relationPublishFailureMessage(fields, candidates) {
|
|
|
336
336
|
const type = typeof raw === "number" ? raw : typeof raw === "string" && raw in FIELD_PROPERTY_TYPES ? FIELD_PROPERTY_TYPES[raw] : undefined;
|
|
337
337
|
if (type !== FIELD_PROPERTY_TYPES.WORK_SHEET && type !== FIELD_PROPERTY_TYPES.MULT_WORK_SHEET)
|
|
338
338
|
continue;
|
|
339
|
-
const
|
|
339
|
+
const relativeCode = typeof field.relativeCode === "string" ? field.relativeCode.trim() : "";
|
|
340
|
+
const target = relativeCode || (typeof options?.schemaCode === "string" ? options.schemaCode.trim() : "");
|
|
340
341
|
if (typeof options?.schemaCode === "string" && options.schemaCode.trim() && (!field.relativeCode || String(field.relativeCode).trim() === "")) {
|
|
341
342
|
return `字段 ${String(field.code ?? "(未知)")} 的关联目标 ${options.schemaCode} 仅写在 options.schemaCode;发布面不解析该位置(平台原始 errcode 300010「数据模型编码不能为空!」)。请重建/归一化为顶层 relativeCode=${options.schemaCode}`;
|
|
342
343
|
}
|
package/dist/lanes/field.js
CHANGED
|
@@ -12,16 +12,21 @@ import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
|
12
12
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
13
13
|
import { executeFormDraftWrite, executeFormPublishWrite, readFormOrNull } from "../form-support.js";
|
|
14
14
|
import { FIELD_LANE_CONTRACT } from "./field-contract.js";
|
|
15
|
-
import { flattenTreeRows, mapBounded
|
|
15
|
+
import { flattenTreeRows, mapBounded } from "../support.js";
|
|
16
16
|
import { extractBizObjectPage } from "./runtime-data.js";
|
|
17
17
|
const FIELD_LANE = FIELD_LANE_CONTRACT.lane;
|
|
18
18
|
const FIELD_TOOLS = FIELD_LANE_CONTRACT.tools;
|
|
19
19
|
/** Read actual model schema codes; bare modelList may expose internal node ids. */
|
|
20
20
|
export async function modelSchemaCodes(session) {
|
|
21
21
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
22
|
+
// Relation codes are tenant-global on the platform, not application-scoped.
|
|
23
|
+
// Enumerate every application for this call only; deliberately do not cache
|
|
24
|
+
// this merged candidate set across calls because model writes can change it.
|
|
25
|
+
const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
|
|
26
|
+
.map((row) => row.appCode ?? row.code)
|
|
27
|
+
.filter((code) => typeof code === "string" && code.length > 0);
|
|
28
|
+
const trees = await mapBounded(apps, 4, async (appCode) => flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data)));
|
|
29
|
+
const codes = trees.flat()
|
|
25
30
|
.filter((row) => row.type === "BizModel")
|
|
26
31
|
.map((row) => String(row.code ?? row.schemaCode ?? row.bizSchemaCode ?? ""))
|
|
27
32
|
.filter((code) => code.length > 0);
|
|
@@ -29,7 +34,8 @@ export async function modelSchemaCodes(session) {
|
|
|
29
34
|
return codes;
|
|
30
35
|
}
|
|
31
36
|
catch {
|
|
32
|
-
// Fall through to the compatibility endpoint below
|
|
37
|
+
// Fall through to the compatibility endpoint below when the tenant-wide
|
|
38
|
+
// discovery face is unavailable (older/fake transports).
|
|
33
39
|
}
|
|
34
40
|
return wireRows((await wireCall(session, { path: ENDPOINTS.modelList, query: {} })).data)
|
|
35
41
|
.map((row) => String(row.schemaCode ?? row.bizSchemaCode ?? row.code ?? ""))
|
package/dist/lanes/folder.js
CHANGED
|
@@ -5,7 +5,7 @@ import { writeTool } from "../write.js";
|
|
|
5
5
|
import { wireCall, wireRows } from "../wire.js";
|
|
6
6
|
import { ENDPOINTS } from "../endpoints.js";
|
|
7
7
|
import { capabilityArg, environmentArg } from "../args.js";
|
|
8
|
-
import { resolveApplicationCode } from "../support.js";
|
|
8
|
+
import { jsonBytes, resolveApplicationCode } from "../support.js";
|
|
9
9
|
import { LANES } from "./lane-ids.js";
|
|
10
10
|
import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
|
|
11
11
|
/**
|
|
@@ -38,6 +38,33 @@ import { FOLDER_LANE_CONTRACT } from "./folder-contract.js";
|
|
|
38
38
|
*/
|
|
39
39
|
const LANE = LANES.folder;
|
|
40
40
|
const T = FOLDER_LANE_CONTRACT.tools;
|
|
41
|
+
/**
|
|
42
|
+
* folder.tree is a node inventory, so its useful inline budget is node-count
|
|
43
|
+
* first rather than the generic read compact threshold. Keep the existing
|
|
44
|
+
* byte guard as a second gate: a pathological 50-node tree still belongs in
|
|
45
|
+
* the artifact when the node payload itself is too large.
|
|
46
|
+
*/
|
|
47
|
+
export const FOLDER_TREE_INLINE_NODE_LIMIT = 50;
|
|
48
|
+
export const FOLDER_TREE_NODE_INLINE_CEILING_BYTES = 8192;
|
|
49
|
+
export function compactFolderTree(result) {
|
|
50
|
+
const nodes = Array.isArray(result.nodes) ? result.nodes : undefined;
|
|
51
|
+
if (nodes === undefined)
|
|
52
|
+
return { ...result };
|
|
53
|
+
if (nodes.length <= FOLDER_TREE_INLINE_NODE_LIMIT && jsonBytes(nodes) <= FOLDER_TREE_NODE_INLINE_CEILING_BYTES) {
|
|
54
|
+
return { ...result, nodes };
|
|
55
|
+
}
|
|
56
|
+
const countGate = nodes.length > FOLDER_TREE_INLINE_NODE_LIMIT;
|
|
57
|
+
const { nodes: _nodes, ...withoutNodes } = result;
|
|
58
|
+
return {
|
|
59
|
+
...withoutNodes,
|
|
60
|
+
nodesPreview: nodes.slice(0, 5),
|
|
61
|
+
note: `${String(result.note ?? "")} ${countGate
|
|
62
|
+
? `nodes 超过 ${FOLDER_TREE_INLINE_NODE_LIMIT} 条,完整节点已外置到 detailArtifactId;nodesPreview 仅展示前 5 条。`
|
|
63
|
+
: `nodes 正文超过 ${FOLDER_TREE_NODE_INLINE_CEILING_BYTES}B 字节闸,完整节点已外置到 detailArtifactId;这是字节闸,不是节点数限制。`}`.trim(),
|
|
64
|
+
// Deliberately omit nodes so governedRead adds the standard empty
|
|
65
|
+
// placeholder/truncatedKeys while the preview remains available.
|
|
66
|
+
};
|
|
67
|
+
}
|
|
41
68
|
/**
|
|
42
69
|
* Live 1.1.25 (r28d F2): platform `apppackage/create` seeds one top-level Folder
|
|
43
70
|
* whose `name_i18n.en` is the stable token `defaultGroup` (zh display is「默认分组」).
|
|
@@ -202,7 +229,7 @@ function folderTools(tools) {
|
|
|
202
229
|
: " applicationExists:false——应用不在 apppackage/list;空树不等于「空应用」。") +
|
|
203
230
|
" nodeCount/folderCount/modelCount 是全应用聚合,共享环境含其它会话资源;核对自己的资源请按 code 前缀过滤 nodes[],不要只比计数。",
|
|
204
231
|
};
|
|
205
|
-
}),
|
|
232
|
+
}, { compact: compactFolderTree }),
|
|
206
233
|
}));
|
|
207
234
|
writeTool({
|
|
208
235
|
tools,
|
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/residue.js
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ENDPOINTS } from "./endpoints.js";
|
|
3
|
+
import { wireCall, wireRows } from "./wire.js";
|
|
4
|
+
import { flattenTreeRows } from "./support.js";
|
|
5
|
+
import { ConfigStore, defaultPaths } from "./config-store.js";
|
|
6
|
+
import { SessionManager } from "./session-manager.js";
|
|
7
|
+
import { makeToolContext } from "./context.js";
|
|
8
|
+
import { emitInvalid, emitResult } from "./command-transport.js";
|
|
9
|
+
const MAX_ITEMS = 20;
|
|
10
|
+
function face(faceName, rows, extra = {}) {
|
|
11
|
+
return { face: faceName, ...extra, count: rows.length, items: rows.slice(0, MAX_ITEMS) };
|
|
12
|
+
}
|
|
13
|
+
function codeOf(row) {
|
|
14
|
+
return String(row.code ?? row.schemaCode ?? row.bizSchemaCode ?? "");
|
|
15
|
+
}
|
|
16
|
+
function nameOf(row, code) {
|
|
17
|
+
return String(row.name ?? row.displayName ?? row.title ?? code);
|
|
18
|
+
}
|
|
19
|
+
function codeStarts(row, prefix) {
|
|
20
|
+
return codeOf(row).startsWith(prefix);
|
|
21
|
+
}
|
|
22
|
+
function folderStarts(row, prefix) {
|
|
23
|
+
return codeStarts(row, prefix) || String(row.name ?? "").startsWith(prefix);
|
|
24
|
+
}
|
|
25
|
+
function scriptPresent(raw) {
|
|
26
|
+
if (typeof raw !== "string")
|
|
27
|
+
return false;
|
|
28
|
+
try {
|
|
29
|
+
const parsed = JSON.parse(raw);
|
|
30
|
+
if (!Array.isArray(parsed))
|
|
31
|
+
return false;
|
|
32
|
+
return parsed.some((element) => {
|
|
33
|
+
const row = element;
|
|
34
|
+
return row.id === "customScript" && typeof row.innerHTML === "string" && row.innerHTML.trim() !== "";
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function tree(session, app) {
|
|
42
|
+
return flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: app } })).data));
|
|
43
|
+
}
|
|
44
|
+
async function modelRows(session, app) {
|
|
45
|
+
return (await tree(session, app)).filter((row) => row.type === "BizModel");
|
|
46
|
+
}
|
|
47
|
+
async function customPages(session, appSet) {
|
|
48
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.customPageList, query: { modeType: "VUE_MODE" } })).data);
|
|
49
|
+
return rows.filter((row) => appSet.has(String(row.appCode ?? "")));
|
|
50
|
+
}
|
|
51
|
+
async function deepFaces(session, model) {
|
|
52
|
+
const schemaCode = codeOf(model);
|
|
53
|
+
const rules = wireRows((await wireCall(session, { path: ENDPOINTS.businessRuleListBySchemaCode, query: { schemaCode } })).data);
|
|
54
|
+
const datarules = wireRows((await wireCall(session, { path: ENDPOINTS.dataRuleList, query: { schemaCode } })).data);
|
|
55
|
+
let online = [];
|
|
56
|
+
try {
|
|
57
|
+
const sheet = (await wireCall(session, { path: ENDPOINTS.sheetGet, query: { schemaCode, sheetCode: schemaCode } })).data;
|
|
58
|
+
if (sheet && (scriptPresent(sheet.draftHtmlJson) || scriptPresent(sheet.publishedHtmlJson))) {
|
|
59
|
+
const raw = typeof sheet.publishedHtmlJson === "string" && scriptPresent(sheet.publishedHtmlJson) ? sheet.publishedHtmlJson : sheet.draftHtmlJson;
|
|
60
|
+
online = [{ present: true, ...(typeof raw === "string" ? { sha256: createHash("sha256").update(raw).digest("hex") } : {}) }];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// A missing form is an absent online-js face, not a scan failure.
|
|
65
|
+
}
|
|
66
|
+
return [
|
|
67
|
+
face("rule", rules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
68
|
+
face("datarule", datarules.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { model: schemaCode }),
|
|
69
|
+
face("onlinejs", online, { model: schemaCode }),
|
|
70
|
+
];
|
|
71
|
+
}
|
|
72
|
+
export async function scanResidues(session, prefix, options = {}) {
|
|
73
|
+
const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data)
|
|
74
|
+
.map((row) => String(row.appCode ?? row.code ?? ""))
|
|
75
|
+
.filter(Boolean);
|
|
76
|
+
const selected = options.apps && options.apps !== "all" ? options.apps.split(",").map((app) => app.trim()).filter(Boolean) : apps;
|
|
77
|
+
const appSet = new Set(selected);
|
|
78
|
+
const faces = [];
|
|
79
|
+
const models = [];
|
|
80
|
+
for (const app of selected) {
|
|
81
|
+
const rows = await tree(session, app);
|
|
82
|
+
const matchingModels = rows.filter((row) => row.type === "BizModel" && codeStarts(row, prefix));
|
|
83
|
+
models.push(...matchingModels.map((row) => ({ ...row, appCode: app })));
|
|
84
|
+
faces.push(face("model", matchingModels.map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
|
|
85
|
+
faces.push(face("folder", rows.filter((row) => row.type === "Folder" && folderStarts(row, prefix)).map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
|
|
86
|
+
}
|
|
87
|
+
const pages = (await customPages(session, appSet)).filter((row) => codeStarts(row, prefix));
|
|
88
|
+
for (const app of selected) {
|
|
89
|
+
faces.push(face("custompage", pages.filter((row) => String(row.appCode ?? "") === app).map((row) => ({ code: codeOf(row), name: nameOf(row, codeOf(row)) })), { app }));
|
|
90
|
+
}
|
|
91
|
+
if (options.deep !== false) {
|
|
92
|
+
for (const model of models)
|
|
93
|
+
faces.push(...await deepFaces(session, model));
|
|
94
|
+
}
|
|
95
|
+
const totalResidues = faces.reduce((sum, item) => sum + item.count, 0);
|
|
96
|
+
return {
|
|
97
|
+
prefix,
|
|
98
|
+
faces,
|
|
99
|
+
totalResidues,
|
|
100
|
+
clean: totalResidues === 0,
|
|
101
|
+
note: "report 面暂停维护未扫描;workflow 模板跟模型走(模型删即删)",
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
export async function cmdResidue(prefix, options, transport) {
|
|
105
|
+
if (!prefix || prefix.trim() === "")
|
|
106
|
+
return emitInvalid(transport, { message: "residue 的 prefix 必填" });
|
|
107
|
+
try {
|
|
108
|
+
const context = makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
109
|
+
const session = await context.sessions.session(options.env);
|
|
110
|
+
return emitResult(await scanResidues(session, prefix, { apps: options.apps, deep: options.noDeep !== true }), transport);
|
|
111
|
+
}
|
|
112
|
+
catch (error) {
|
|
113
|
+
return emitResult({ status: "error", blocker: "residue-scan-failed", message: error instanceof Error ? error.message : String(error) }, transport);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=residue.js.map
|
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.20",
|
|
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.20",
|
|
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.20"
|
|
77
77
|
}
|
|
78
78
|
}
|
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
|
@@ -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`。
|