@stalfh233/omc-cli 0.6.11 → 0.6.13
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/coverage/m1-coverage-manifest-v1.json +2 -0
- package/dist/approval-flow.js +84 -3
- package/dist/batch-call.js +32 -2
- package/dist/bench/call-face-eval.js +2 -2
- package/dist/call.js +35 -2
- package/dist/capability-overview.js +21 -7
- package/dist/cli-contract.js +34 -4
- package/dist/cli-output.js +6 -2
- package/dist/cli.js +303 -69
- package/dist/describe-cache.js +8 -4
- package/dist/environment-policy.js +20 -0
- package/dist/field-families.js +62 -4
- package/dist/form-support.js +293 -6
- package/dist/goal-routes.js +137 -22
- package/dist/identity.js +63 -11
- package/dist/lanes/app-contract.js +4 -4
- package/dist/lanes/app.js +19 -1
- package/dist/lanes/dictionary-contract.js +16 -5
- package/dist/lanes/dictionary-coverage.js +1 -0
- package/dist/lanes/dictionary.js +200 -8
- package/dist/lanes/environment-contract.js +18 -6
- package/dist/lanes/environment.js +129 -16
- package/dist/lanes/field-contract.js +2 -1
- package/dist/lanes/field.js +707 -59
- package/dist/lanes/form.js +52 -6
- package/dist/lanes/list-view-contract.js +2 -2
- package/dist/lanes/list-view.js +98 -2
- package/dist/lanes/meta.js +36 -10
- package/dist/lanes/model-contract.js +7 -6
- package/dist/lanes/model.js +154 -10
- package/dist/lanes/organization.js +67 -7
- package/dist/lanes/runtime-data-contract.js +4 -2
- package/dist/lanes/runtime-data.js +395 -41
- package/dist/lanes/workflow-contract.js +2 -2
- package/dist/lanes/workflow.js +381 -11
- package/dist/model-scaffold.js +11 -3
- package/dist/path-projection.js +192 -32
- package/dist/read-shapes.js +31 -11
- package/dist/recipes/field.js +16 -3
- package/dist/recipes/form.js +2 -1
- package/dist/recipes/runtime-data.js +58 -8
- package/dist/recipes/workflow.js +86 -1
- package/dist/refusal-guidance.js +18 -6
- package/dist/rollback-spec.js +41 -2
- package/dist/rollback.js +49 -11
- package/dist/script.js +72 -12
- package/dist/skills.js +91 -3
- package/dist/tools.js +113 -1
- package/dist/wire.js +13 -1
- package/dist/write-lock.js +92 -4
- package/dist/write.js +129 -16
- package/package.json +5 -3
- package/skills/omc-business-configuration.md +1 -1
- package/skills/omc-capabilities.md +5 -4
- package/skills/omc-playbook.md +41 -0
- package/skills/omc-refusals.md +58 -6
- package/skills/omc-tool-selection.md +2 -0
- package/skills/omc-write-hazards.md +3 -2
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"dictionary.save",
|
|
49
49
|
"dictionary.status",
|
|
50
50
|
"dictionary.usage",
|
|
51
|
+
"dictionary.references",
|
|
51
52
|
"dictionary.classes",
|
|
52
53
|
"dictionary.class.save",
|
|
53
54
|
"dictionary.class.delete"
|
|
@@ -62,6 +63,7 @@
|
|
|
62
63
|
"dictionary save",
|
|
63
64
|
"dictionary status",
|
|
64
65
|
"dictionary record usage",
|
|
66
|
+
"dictionary refs",
|
|
65
67
|
"dictionary classes",
|
|
66
68
|
"dictionary class save",
|
|
67
69
|
"dictionary class delete"
|
package/dist/approval-flow.js
CHANGED
|
@@ -20,15 +20,64 @@ import { runToolCall } from "./call.js";
|
|
|
20
20
|
import { envelope, requestId, classifyResult, CLI_EXIT_CODES } from "./cli-contract.js";
|
|
21
21
|
import { writeEnvelope } from "./cli-output.js";
|
|
22
22
|
import { argvValue } from "./args.js";
|
|
23
|
+
/**
|
|
24
|
+
* r31c F20: participant object arrays only accept `{code,corpId,id,name,type}`.
|
|
25
|
+
* Intuitive keys (`participantType`/`roleId`/`participantName`) are silently
|
|
26
|
+
* dropped by the platform → readback `[{}]`, validate still valid, publish only
|
|
27
|
+
* warns. Refuse here so the composer never emits a broken shape.
|
|
28
|
+
*/
|
|
29
|
+
export function assertParticipantShape(participant, at) {
|
|
30
|
+
if (participant === undefined || participant === null)
|
|
31
|
+
return;
|
|
32
|
+
if (typeof participant === "string") {
|
|
33
|
+
const trimmed = participant.trim();
|
|
34
|
+
if (trimmed === "{Originator}")
|
|
35
|
+
return;
|
|
36
|
+
if (/^FindUserByRole\s*\(/.test(trimmed))
|
|
37
|
+
return;
|
|
38
|
+
if (trimmed.startsWith("[")) {
|
|
39
|
+
let parsed;
|
|
40
|
+
try {
|
|
41
|
+
parsed = JSON.parse(trimmed);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
throw new Error(`${at}: participant JSON 解析失败;对象数组形如 [{"code":"<角色code>","corpId":"other","id":"<roleId>","name":"<名>","type":2}]`);
|
|
45
|
+
}
|
|
46
|
+
assertParticipantShape(parsed, at);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
throw new Error(`${at}: participant 自由文本「${trimmed}」不可解析(不生成待办)。用 {Originator} / FindUserByRole(...) / 对象数组 {code,corpId,id,name,type}`);
|
|
50
|
+
}
|
|
51
|
+
if (!Array.isArray(participant)) {
|
|
52
|
+
throw new Error(`${at}: participant 必须是字符串或对象数组`);
|
|
53
|
+
}
|
|
54
|
+
for (const [index, entry] of participant.entries()) {
|
|
55
|
+
if (entry === null || typeof entry !== "object" || Array.isArray(entry)) {
|
|
56
|
+
throw new Error(`${at}[${index}]: 参与者条目必须是对象`);
|
|
57
|
+
}
|
|
58
|
+
const record = entry;
|
|
59
|
+
const badKeys = ["participantType", "roleId", "participantName", "userId"].filter((key) => key in record);
|
|
60
|
+
if (badKeys.length > 0) {
|
|
61
|
+
throw new Error(`${at}[${index}]: 不认 ${badKeys.join("/")}——平台会静默丢掉,读回变成 [{}]。正确键:code/corpId/id/name/type(type 2=角色 3=用户)`);
|
|
62
|
+
}
|
|
63
|
+
if (record.code === undefined || record.type === undefined) {
|
|
64
|
+
throw new Error(`${at}[${index}]: 参与者对象缺 code/type(必填)。正确形状:{"code":"<角色或用户code>","corpId":"other","id":"<id>","name":"<名>","type":2}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
23
68
|
/** 解析步骤 DSL 为 {name, participant}(导出供测试)。 */
|
|
24
69
|
export function parseStep(raw) {
|
|
25
70
|
const match = /^(.+?)@(role|user):([A-Za-z0-9_.:-]+)$/.exec(raw.trim());
|
|
26
71
|
if (!match)
|
|
27
72
|
throw new Error(`--steps "${raw}" 解析失败:形如 名称@role:<角色code> 或 名称@user:<用户id>(自由文本角色名能发布但解析不到人、不生成待办)`);
|
|
28
73
|
const [, name, kind, code] = match;
|
|
74
|
+
// Role steps stay on FindUserByRole (department-scoped). User steps emit the
|
|
75
|
+
// canonical object array — never participantType/roleId.
|
|
29
76
|
if (kind === "role")
|
|
30
77
|
return { name: name.trim(), participant: `FindUserByRole({Originator.OU},"${code}","random")` };
|
|
31
|
-
|
|
78
|
+
const participant = JSON.stringify([{ code, corpId: "other", id: code, name: name.trim(), type: 3 }]);
|
|
79
|
+
assertParticipantShape(participant, `--steps ${name}`);
|
|
80
|
+
return { name: name.trim(), participant };
|
|
32
81
|
}
|
|
33
82
|
/** 编译完整模板(recipe 验证形状)。 */
|
|
34
83
|
export function composeApprovalTemplate(options) {
|
|
@@ -44,6 +93,7 @@ export function composeApprovalTemplate(options) {
|
|
|
44
93
|
}
|
|
45
94
|
for (const raw of options.steps) {
|
|
46
95
|
const { name, participant } = parseStep(raw);
|
|
96
|
+
assertParticipantShape(participant, `steps[${name}]`);
|
|
47
97
|
activities.push({
|
|
48
98
|
activityCode: `Activity${n}`, activityType: "PARTICIPANT", activityName: name, i18nMessage: name,
|
|
49
99
|
participant,
|
|
@@ -74,13 +124,26 @@ export async function cmdApprovalFlow(args) {
|
|
|
74
124
|
const apply = args.includes("--apply");
|
|
75
125
|
const originatorNode = !args.includes("--no-originator-node");
|
|
76
126
|
const rejectToStart = !args.includes("--no-reject-to-start");
|
|
77
|
-
|
|
127
|
+
// r31c F5:本拼装器只做线性审批。条件分支必须退回 workflow.update;行级数据
|
|
128
|
+
// 可见范围当前工具面不支持——别把「申请人只看自己」误当成 permissions。
|
|
129
|
+
const usage = [
|
|
130
|
+
'用法:omc approval-flow --schema <模型> --steps "节点名@role:<角色code>" [--steps ...多级] [--no-originator-node] [--no-reject-to-start] [--apply]',
|
|
131
|
+
"步骤也可用 名称@user:<用户id> 绑定具体审批人。默认 dry-run(编译模板预览);--apply 建/改流程+字段权限+发布。",
|
|
132
|
+
"⚠️ 本工具只做线性流程(START→填写→N 级审批→END),没有 --condition。",
|
|
133
|
+
"条件分支请用:omc describe workflow.update --example workflow-conditional-branch",
|
|
134
|
+
'最小条件流示例:routes 上加 ruleConditionGroup:[[{currentDataItem:"amount",bizPropertyType:2,conditionType:"GT",value:"50000"}]],兜底线加 utilizeElse:true。',
|
|
135
|
+
"⚠️ workflow.permissions / org.role.setUserScope 都不是行级数据权限;「申请人只能看自己的单据」当前工具面不支持。",
|
|
136
|
+
].join("\n");
|
|
78
137
|
const invalid = (message) => {
|
|
79
138
|
writeEnvelope(envelope("error", {}, request, { code: "invalid-input", message: `${message}\n${usage}`, retryable: false }));
|
|
80
139
|
return CLI_EXIT_CODES.invalidInput;
|
|
81
140
|
};
|
|
82
141
|
if (!schema)
|
|
83
142
|
return invalid("--schema 必填");
|
|
143
|
+
// Explicit refuse of a flag we do not implement, so callers do not assume silent ignore.
|
|
144
|
+
if (args.includes("--condition") || args.includes("--branch") || args.some((arg) => arg.startsWith("--condition=") || arg.startsWith("--branch="))) {
|
|
145
|
+
return invalid("本工具不支持 --condition/--branch(只做线性审批)。条件分流请:omc describe workflow.update --example workflow-conditional-branch");
|
|
146
|
+
}
|
|
84
147
|
let compiled;
|
|
85
148
|
try {
|
|
86
149
|
compiled = composeApprovalTemplate({ schema, steps, originatorNode, rejectToStart, environment, apply });
|
|
@@ -90,8 +153,26 @@ export async function cmdApprovalFlow(args) {
|
|
|
90
153
|
}
|
|
91
154
|
// F1(r27):边界前置——成功回执就标出跨账号审批在单会话 CLI 内不可用。
|
|
92
155
|
const crossAccountApproval = "unsupported: single-session CLI";
|
|
156
|
+
const linearOnlyNote = "本工具只做线性审批;条件分支用 omc describe workflow.update --example workflow-conditional-branch。" +
|
|
157
|
+
"workflow.permissions 只设谁能发起,不是行级可见范围(行级数据权限当前工具面不支持)。";
|
|
93
158
|
if (!apply) {
|
|
94
|
-
writeEnvelope(envelope("ok", {
|
|
159
|
+
writeEnvelope(envelope("ok", {
|
|
160
|
+
schemaCode: schema,
|
|
161
|
+
...compiled,
|
|
162
|
+
dryRun: true,
|
|
163
|
+
crossAccountApproval,
|
|
164
|
+
linearOnly: true,
|
|
165
|
+
rowLevelDataPermission: "unsupported",
|
|
166
|
+
note: `dry-run 仅编译预览;--apply 执行 workflow.create(无流程时)→update(模板+字段权限)→publish。边界:非发起人身份的审批无法在单会话 CLI 内推进(单 token);若要验跨账号审批,用第二账号另建环境,或把参与人设为 {Originator}。${linearOnlyNote}`,
|
|
167
|
+
conditionExample: {
|
|
168
|
+
tool: "workflow.update",
|
|
169
|
+
example: "workflow-conditional-branch",
|
|
170
|
+
routesSnippet: [
|
|
171
|
+
{ preNode: "A2", postNode: "A3", ruleConditionGroup: [[{ currentDataItem: "amount", bizPropertyType: 2, conditionType: "GT", value: "50000" }]] },
|
|
172
|
+
{ preNode: "A2", postNode: "A4", utilizeElse: true },
|
|
173
|
+
],
|
|
174
|
+
},
|
|
175
|
+
}, request));
|
|
95
176
|
return 0;
|
|
96
177
|
}
|
|
97
178
|
// 1) 确保流程存在(一个模型只能有一个:已存在则直接 update)
|
package/dist/batch-call.js
CHANGED
|
@@ -76,6 +76,29 @@ export async function runBatchCall(options) {
|
|
|
76
76
|
}));
|
|
77
77
|
const executed = entries.length;
|
|
78
78
|
const skipped = options.argsList.length - executed;
|
|
79
|
+
// 2026-11-05(r30c F3/F10/F13):全成功时旧回执只有 4 个计数,调用方拿不到
|
|
80
|
+
// **每条的 id**——想确认「这 5 条到底进没进库」必须再发一次 data.list,而批量
|
|
81
|
+
// 存在的理由正是省掉那一次往返。反过来把 8 份完整信封塞回来又会重建本命令要
|
|
82
|
+
// 消除的那堵墙。折中:**每条一行摘要**(几十字节),超过阈值才截断。
|
|
83
|
+
const ENTRIES_INLINE_LIMIT = 20;
|
|
84
|
+
const entriesSummary = entries.map((entry) => {
|
|
85
|
+
const readback = entry.result.readback;
|
|
86
|
+
const performed = entry.result.performed;
|
|
87
|
+
const bizObjectId = typeof performed?.bizObjectId === "string"
|
|
88
|
+
? String(performed.bizObjectId)
|
|
89
|
+
: typeof readback?.authoritative?.id === "string"
|
|
90
|
+
? String(readback.authoritative.id)
|
|
91
|
+
: undefined;
|
|
92
|
+
const skippedMarker = performed?.skipped;
|
|
93
|
+
return {
|
|
94
|
+
index: entry.index,
|
|
95
|
+
ok: entry.status === "ok",
|
|
96
|
+
...(bizObjectId !== undefined ? { bizObjectId } : {}),
|
|
97
|
+
...(entry.status === "ok" ? {} : { status: String(entry.result.status ?? "failed"), blocker: entry.result.blocker ?? null }),
|
|
98
|
+
...(skippedMarker !== undefined ? { skipped: String(skippedMarker) } : {}),
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
const entriesInline = entriesSummary.slice(0, ENTRIES_INLINE_LIMIT);
|
|
79
102
|
return {
|
|
80
103
|
exitCode: failed > 0 ? 1 : 0,
|
|
81
104
|
result: {
|
|
@@ -86,8 +109,15 @@ export async function runBatchCall(options) {
|
|
|
86
109
|
succeeded,
|
|
87
110
|
failed,
|
|
88
111
|
...(skipped > 0 ? { notExecuted: skipped } : {}),
|
|
89
|
-
//
|
|
90
|
-
//
|
|
112
|
+
// 每条一行摘要(index/ok/bizObjectId)——不是完整信封,但足以回答
|
|
113
|
+
// 「第 3 条建成了吗、它的 id 是什么」。失败项另有 failures 带完整原因。
|
|
114
|
+
entries: entriesInline,
|
|
115
|
+
...(entries.length > entriesInline.length
|
|
116
|
+
? { entriesOmitted: entries.length - entriesInline.length, entriesHint: `条目超过 ${ENTRIES_INLINE_LIMIT} 条,只内联前 ${ENTRIES_INLINE_LIMIT} 条摘要;完整结果走 detailArtifactId / omc artifact get` }
|
|
117
|
+
: {}),
|
|
118
|
+
// Only failures carry full per-entry detail: echoing 8 full success
|
|
119
|
+
// envelopes would rebuild exactly the wall of text this command exists
|
|
120
|
+
// to remove.
|
|
91
121
|
...(failures.length > 0 ? { failures } : {}),
|
|
92
122
|
...(skipped > 0 ? { notExecutedNote: "未执行的条目通常是因为解析阶段就失败(如缺少必填参数);先修 failures 里的第一条" } : {}),
|
|
93
123
|
},
|
|
@@ -163,8 +163,8 @@ export const CALL_FACE_TASKS = [
|
|
|
163
163
|
ok(replay.code === 0 && replay.envelope.result?.status === "ok" && replay.envelope.result?.replay === "already-completed", `replay=${JSON.stringify(replay.envelope).slice(0, 260)}`);
|
|
164
164
|
ok(server.fake.bodiesFor("/api/api/app/bizmodels/create").length === 1, "replay must not send a second create wire");
|
|
165
165
|
const diverged = await agent.run(["call", "model.create", "--env", "eval", "--args", JSON.stringify({ name: "幂等模型2", code: "eval_idem2" }), "--request-id", "eval-idem-1"]);
|
|
166
|
-
ok(diverged.code ===
|
|
167
|
-
ok(server.fake.bodiesFor("/api/api/app/bizmodels/create").length ===
|
|
166
|
+
ok(diverged.code === 12 && diverged.envelope.result?.blocker === "request-id-args-mismatch", `diverged args must refuse mismatch, got ${JSON.stringify(diverged.envelope).slice(0, 260)}`);
|
|
167
|
+
ok(server.fake.bodiesFor("/api/api/app/bizmodels/create").length === 1, "diverged args must not write a second create");
|
|
168
168
|
},
|
|
169
169
|
},
|
|
170
170
|
{
|
package/dist/call.js
CHANGED
|
@@ -9,6 +9,7 @@ import { parseToolInput, ToolInputError } from "./tool-types.js";
|
|
|
9
9
|
import { absencePointer } from "./capability-absences.js";
|
|
10
10
|
import { closestMatches } from "./closest-match.js";
|
|
11
11
|
import { attachRefusalRecovery } from "./refusal-guidance.js";
|
|
12
|
+
import { lockOwner } from "./write-lock.js";
|
|
12
13
|
export const CALL_EXIT_CODES = { ok: 0, failed: 1, unknownTool: 2, invalidInput: 10 };
|
|
13
14
|
export const CLI_ARGS_MAX_BYTES = 1024 * 1024;
|
|
14
15
|
function readBounded(fd, label) {
|
|
@@ -168,7 +169,33 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
168
169
|
try {
|
|
169
170
|
return await Promise.race([
|
|
170
171
|
invoke(),
|
|
171
|
-
new Promise((resolve) => {
|
|
172
|
+
new Promise((resolve) => {
|
|
173
|
+
timer = setTimeout(() => {
|
|
174
|
+
// r30a F4/F5: lock is deliberately kept on cli-timeout (cli.ts),
|
|
175
|
+
// so retryable must NOT claim "retry now". Put lock state into
|
|
176
|
+
// the JSON envelope and give a pasteable request.status command.
|
|
177
|
+
const envName = typeof args.environment === "string" && args.environment.length > 0 ? args.environment : options.environment;
|
|
178
|
+
const lockHeldBy = typeof envName === "string" && envName.length > 0 ? lockOwner(envName) : null;
|
|
179
|
+
const requestId = typeof args.requestId === "string" ? args.requestId : options.requestId;
|
|
180
|
+
const envFlag = typeof envName === "string" && envName.length > 0 ? ` --env ${envName}` : "";
|
|
181
|
+
const statusCmd = typeof requestId === "string" && requestId.length > 0
|
|
182
|
+
? `omc call request.status${envFlag} --args '{"requestId":"${requestId}"}'`
|
|
183
|
+
: `omc call request.status${envFlag} --args '{"requestId":"<本次 requestId>"}'`;
|
|
184
|
+
resolve({
|
|
185
|
+
status: "uncertain",
|
|
186
|
+
blocker: "cli-timeout",
|
|
187
|
+
frozen: true,
|
|
188
|
+
// Lock still held by this process until exit → not immediately retryable.
|
|
189
|
+
retryable: false,
|
|
190
|
+
retryableAfter: "process-exit",
|
|
191
|
+
retryAfter: lockHeldBy?.pid !== undefined ? `pid ${lockHeldBy.pid} exits` : "current process exits (stale lock auto-recovers)",
|
|
192
|
+
lockHeldBy,
|
|
193
|
+
nextAction: `${statusCmd};确认结果后再决定是否换 requestId 重试(锁未放前立刻重试会撞 write-lock-held)`,
|
|
194
|
+
nextCommand: statusCmd,
|
|
195
|
+
message: "outcome is uncertain: cli-timeout;写可能仍在飞,环境写锁故意未释放",
|
|
196
|
+
});
|
|
197
|
+
}, options.timeoutMs);
|
|
198
|
+
}),
|
|
172
199
|
]);
|
|
173
200
|
}
|
|
174
201
|
finally {
|
|
@@ -209,9 +236,15 @@ export async function runToolCall(toolName, options = {}) {
|
|
|
209
236
|
* one raw + one escaped, read as "奇奇怪怪的乱码").
|
|
210
237
|
*/
|
|
211
238
|
function humanResultLine(result) {
|
|
239
|
+
const status = String(result?.status ?? "ok");
|
|
240
|
+
// dry-run 的正文(含 summary)已经完整在信封里,再往 stderr 印一行就是重复载荷
|
|
241
|
+
// ——这正是 2026-10-14 Windows 那次「两行 JSON、一行还被转义」的根因。
|
|
242
|
+
// 2026-11-05 回归:`model.create` 的会话级 dry-run 探测开始在顶层带 summary,
|
|
243
|
+
// 于是这一行又被印出来(round16 的「dry-run 不打印人话」断言失败)。
|
|
244
|
+
if (status === "dry-run")
|
|
245
|
+
return null;
|
|
212
246
|
if (typeof result.summary === "string" && result.summary.trim().length > 0)
|
|
213
247
|
return result.summary;
|
|
214
|
-
const status = String(result?.status ?? "ok");
|
|
215
248
|
if (status === "ok" || status === "dry-run")
|
|
216
249
|
return null;
|
|
217
250
|
if (result?.blocker === undefined && status !== "error" && status !== "uncertain")
|
|
@@ -24,7 +24,7 @@ const SURFACES = [
|
|
|
24
24
|
operations: ["discover", "fingerprint"],
|
|
25
25
|
requires: [],
|
|
26
26
|
intent: { level: "none", note: "治理面,不进 intent;用 omc setup / discover_environment。" },
|
|
27
|
-
notes: "只读;写入开关与生产授权在 omc setup。",
|
|
27
|
+
notes: "只读;写入开关与生产授权在 omc setup。discover/fingerprint 回 detectedVersion/inAnchorFamily/versionSource(与 doctor 同源)。",
|
|
28
28
|
},
|
|
29
29
|
{
|
|
30
30
|
id: "app",
|
|
@@ -88,6 +88,8 @@ const SURFACES = [
|
|
|
88
88
|
description: "列表视图设计时表头与列生命周期。",
|
|
89
89
|
operations: ["create", "configure", "delete"],
|
|
90
90
|
intent: { level: "partial", note: "create/configure 与 delete(absent:true)已声明式。" },
|
|
91
|
+
// r30f F5:partial 不能只剩一个词——说清「哪一半可用 / 哪一半要实跑」。
|
|
92
|
+
notes: "coverage=partial:create/configure/get/list/delete 可用;1.1.x 上 delete/absent 与默认视图短路需实跑,勿当 full。",
|
|
91
93
|
},
|
|
92
94
|
{
|
|
93
95
|
id: "datarule",
|
|
@@ -97,7 +99,7 @@ const SURFACES = [
|
|
|
97
99
|
operations: ["create", "update", "delete"],
|
|
98
100
|
requires: ["field"],
|
|
99
101
|
intent: { level: "partial", note: "声明式支持字段级规则 create/update 与 delete(absent:true);规则类型 1..5 与 options 按平台校验器闭环。" },
|
|
100
|
-
notes: "目标字段必须已发布。",
|
|
102
|
+
notes: "目标字段必须已发布。coverage=partial:list/create/update/delete 可用;type3/4/5 设计态行≠运行时 options(运行时优先 field.update);跨版本请实跑。",
|
|
101
103
|
},
|
|
102
104
|
{
|
|
103
105
|
id: "businessrule",
|
|
@@ -166,7 +168,7 @@ const SURFACES = [
|
|
|
166
168
|
description: "报表页 + BI 图表:报表树/元数据读取、报表页建改删、BI dashboard 配置、图表增改删、图表数据查询、数据集读取/关联。",
|
|
167
169
|
operations: ["tree", "get", "chartdata", "datasource", "create", "update", "delete", "configure"],
|
|
168
170
|
intent: { level: "partial", note: "顶层 reports[] 声明式 create/update + dashboard configure(global/attributes/charts diff,removeUnlistedCharts 删未声明图表);delete 走 omc call report.delete,datasource.link/chartdata 走 omc call。" },
|
|
169
|
-
notes: "
|
|
171
|
+
notes: "coverage=partial:tree/get/create/update/configure/delete 与 datasource.link/chartdata 可用;BI(/dashboard/v1/*) 缺 config 会静默 no-op,跨版本请实跑。报表页走 /api/api/report/*;BI body 必带 config:{token,reportCode,appCode}。BI 外层 corpId 是真实查询键(错值→dashboard.not.exist,空→corpid.empty),默认 \"a\",可用 corpId 覆盖。",
|
|
170
172
|
},
|
|
171
173
|
{
|
|
172
174
|
id: "data",
|
|
@@ -297,6 +299,16 @@ export const BOUNDARY_CUES = Object.freeze({
|
|
|
297
299
|
cues: ["子表套子表", "嵌套子表", "多级子表"],
|
|
298
300
|
note: "嵌套子表(子表套子表)在 W6-S27 为 blocked;一级子表可用,嵌套不可",
|
|
299
301
|
},
|
|
302
|
+
/**
|
|
303
|
+
* r31c F27: four near-synonyms for row-level data scope all returned 0 help
|
|
304
|
+
* hits. Surface as a boundary so `omc next "申请人只看自己的单据"` answers
|
|
305
|
+
* honestly instead of routing to workflow.permissions (who may originate).
|
|
306
|
+
*/
|
|
307
|
+
"row-level-data-permission": {
|
|
308
|
+
status: "unsupported",
|
|
309
|
+
cues: ["只看自己", "只能看自己", "本人单据", "行级权限", "行级数据权限", "数据权限", "可见范围", "dataScope", "按登录人过滤"],
|
|
310
|
+
note: "行级数据权限 / 可见范围 / 申请人只看自己的单据不在工具覆盖内(没有按登录人动态过滤的读面/写面)。workflow.permissions 只管谁能发起;org.role.setUserScope 是角色成员部门管理范围;listview.conditions 是全局过滤。如实报告能力缺口,不要用发起权限/列表条件伪造",
|
|
311
|
+
},
|
|
300
312
|
/**
|
|
301
313
|
* r19 p14(2026-10-26 用户裁决:报表**暂不优先支持、先隐藏**,不是「平台
|
|
302
314
|
* 不支持」):报表分两层——平台层 `/api/api/report/*`(树/改名/存在性)在
|
|
@@ -322,15 +334,17 @@ export function boundaryOf(goal) {
|
|
|
322
334
|
}
|
|
323
335
|
const CAPABILITY_TOPICS = Object.freeze({
|
|
324
336
|
environment: ["omc-start-here", "omc-write-hazards"],
|
|
325
|
-
app
|
|
337
|
+
// r29a F4:应用参数矩阵挂 app-scope(合成 topic,见 skills.ts)。
|
|
338
|
+
app: ["app-scope", "omc-business-configuration", "omc-write-hazards"],
|
|
339
|
+
folder: ["app-scope", "omc-business-configuration"],
|
|
326
340
|
dictionary: ["omc-business-configuration", "omc-tool-selection"],
|
|
327
|
-
model: ["omc-business-configuration", "omc-config-draft-review"],
|
|
341
|
+
model: ["app-scope", "omc-business-configuration", "omc-config-draft-review"],
|
|
328
342
|
field: ["omc-business-configuration", "omc-tool-selection"],
|
|
329
343
|
form: ["omc-business-configuration", "omc-config-draft-review"],
|
|
330
344
|
listview: ["omc-business-configuration", "omc-tool-selection"],
|
|
331
345
|
datarule: ["omc-business-configuration", "omc-refusals"],
|
|
332
|
-
businessrule: ["omc-business-configuration", "omc-refusals"],
|
|
333
|
-
"rule-lifecycle": ["omc-business-configuration", "omc-refusals"],
|
|
346
|
+
businessrule: ["app-scope", "omc-business-configuration", "omc-refusals"],
|
|
347
|
+
"rule-lifecycle": ["app-scope", "omc-business-configuration", "omc-refusals"],
|
|
334
348
|
workflow: ["omc-business-configuration", "omc-refusals"],
|
|
335
349
|
onlinejs: ["omc-business-configuration", "omc-write-hazards"],
|
|
336
350
|
bizservice: ["omc-business-configuration", "omc-write-hazards"],
|
package/dist/cli-contract.js
CHANGED
|
@@ -46,7 +46,28 @@ export function asciiStdout() {
|
|
|
46
46
|
// ever answer "not a Windows code page here".
|
|
47
47
|
if (process.platform !== "win32")
|
|
48
48
|
return false;
|
|
49
|
-
return asciiStdoutFor(process.platform, process.stdout.isTTY === true, activeCodePage());
|
|
49
|
+
return asciiStdoutFor(process.platform, process.stdout.isTTY === true, activeCodePage(), process.env);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Hosts that are known to decode UTF-8 from a pipe regardless of the console
|
|
53
|
+
* code page. Escaping inside them is **pure loss**: the reader is a UTF-8
|
|
54
|
+
* consumer (Windows Terminal, VS Code, an agent harness), so `\u6295\u5f71`
|
|
55
|
+
* reads as garbage to the human skimming the transcript while the JSON was
|
|
56
|
+
* parseable either way.
|
|
57
|
+
*
|
|
58
|
+
* 2026-11-05 用户实测(Windows + agent harness):整屏 `\uXXXX`,人眼无法阅读。
|
|
59
|
+
* 之前只判「TTY 与否 + 代码页」,把「现代终端里被 harness 捕获」也当成了旧管道。
|
|
60
|
+
*/
|
|
61
|
+
export function utf8CapableHost(env) {
|
|
62
|
+
// Windows Terminal 与任何设置了 TERM_PROGRAM 的宿主(VS Code / WezTerm / …)。
|
|
63
|
+
if (typeof env.WT_SESSION === "string" && env.WT_SESSION !== "")
|
|
64
|
+
return true;
|
|
65
|
+
if (typeof env.TERM_PROGRAM === "string" && env.TERM_PROGRAM !== "")
|
|
66
|
+
return true;
|
|
67
|
+
// 与上面同理:显式声明了 TERM 的现代终端/PTY。
|
|
68
|
+
if (typeof env.TERM === "string" && env.TERM !== "" && env.TERM !== "dumb")
|
|
69
|
+
return true;
|
|
70
|
+
return false;
|
|
50
71
|
}
|
|
51
72
|
/**
|
|
52
73
|
* T2(GitLab #17,2026-11-04):`--utf8` 是「人类可读文本按 UTF-8 原文输出」的
|
|
@@ -110,11 +131,14 @@ function activeCodePage() {
|
|
|
110
131
|
return cachedCodePage;
|
|
111
132
|
}
|
|
112
133
|
/** Pure core of asciiStdout for tests: platform × tty × active code page. */
|
|
113
|
-
export function asciiStdoutFor(platform, isTty, codePage) {
|
|
134
|
+
export function asciiStdoutFor(platform, isTty, codePage, env = {}) {
|
|
114
135
|
if (platform !== "win32")
|
|
115
136
|
return false;
|
|
116
137
|
if (isTty)
|
|
117
138
|
return false;
|
|
139
|
+
// 已知 UTF-8 宿主:代码页是旧的不代表**消费者**解不了 UTF-8(harness / 现代终端)。
|
|
140
|
+
if (utf8CapableHost(env))
|
|
141
|
+
return false;
|
|
118
142
|
// Unknown code page (chcp unavailable — modern terminal hosts): trust
|
|
119
143
|
// UTF-8. Windows Terminal defaults to it, and \u escapes are unreadable to
|
|
120
144
|
// humans either way.
|
|
@@ -240,10 +264,12 @@ export function nextCommandFor(result) {
|
|
|
240
264
|
* accidents (0.6.6/0.6.7): `meta.get_status` → `"failed"`, `rollback apply` →
|
|
241
265
|
* `"stopped"` both used to land in `internal` before they were added here.
|
|
242
266
|
*/
|
|
243
|
-
export const RESULT_STATUSES = ["ok", "dry-run", "partial", "unknown-tool", "uncertain", "refused", "failed", "stopped", "blocked", "error"];
|
|
267
|
+
export const RESULT_STATUSES = ["ok", "dry-run", "partial", "unknown-tool", "uncertain", "refused", "failed", "stopped", "blocked", "skipped", "error"];
|
|
244
268
|
export const RESULT_STATUS_ROUTES = Object.freeze({
|
|
245
269
|
ok: { kind: "success", envelopeStatus: "ok", exitCode: CLI_EXIT_CODES.ok },
|
|
246
270
|
"dry-run": { kind: "success", envelopeStatus: "dry-run", exitCode: CLI_EXIT_CODES.ok },
|
|
271
|
+
// r30a F12: irreversible rollback apply is an expected answer, not an error.
|
|
272
|
+
skipped: { kind: "success", envelopeStatus: "skipped", exitCode: CLI_EXIT_CODES.ok },
|
|
247
273
|
partial: { kind: "partial", envelopeStatus: "partial", code: "partial-batch", exitCode: 1 },
|
|
248
274
|
"unknown-tool": { kind: "unknown-tool", envelopeStatus: "error", code: "invalid-input", exitCode: CLI_EXIT_CODES.invalidInput },
|
|
249
275
|
uncertain: { kind: "uncertain", envelopeStatus: "uncertain", code: "uncertain", exitCode: CLI_EXIT_CODES.uncertain },
|
|
@@ -339,9 +365,13 @@ export function classifyResult(result) {
|
|
|
339
365
|
};
|
|
340
366
|
}
|
|
341
367
|
if (route?.kind === "uncertain") {
|
|
368
|
+
// r30a F5: honour the producer's retryable flag (cli-timeout keeps the
|
|
369
|
+
// write lock → retryable:false). Default remains true for other uncertains
|
|
370
|
+
// that do not carry an explicit flag.
|
|
371
|
+
const retryable = result.retryable === false ? false : true;
|
|
342
372
|
return {
|
|
343
373
|
status: route.envelopeStatus,
|
|
344
|
-
error: withNext({ code: route.code, message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable
|
|
374
|
+
error: withNext({ code: route.code, message: String(result.message ?? result.reason ?? "outcome is uncertain"), retryable }),
|
|
345
375
|
exitCode: route.exitCode,
|
|
346
376
|
};
|
|
347
377
|
}
|
package/dist/cli-output.js
CHANGED
|
@@ -132,8 +132,12 @@ function emitAsciiBannerOnce() {
|
|
|
132
132
|
// 注意:ascii 模式下 stderr 本身也会被转义,所以横幅必须是纯 ASCII。
|
|
133
133
|
if (process.env.OMC_ASCII_STDOUT !== undefined || process.env.OMC_HUMAN_UTF8 !== undefined)
|
|
134
134
|
return;
|
|
135
|
-
|
|
136
|
-
|
|
135
|
+
// 2026-11-05:旧横幅只给了 `--utf8`,而它**只管 stderr**——用户要的正是不想再
|
|
136
|
+
// 看 `\uXXXX`,照它做没有效果。给出真正能关掉转义的两个办法。
|
|
137
|
+
process.stderr.write("[omc] legacy console code page detected: the JSON envelope is \\u-escaped (e.g. \\u6295\\u5f71) so it stays parseable.\n"
|
|
138
|
+
+ " If your consumer decodes UTF-8 (agent harness / VS Code / Windows Terminal), turn it off:\n"
|
|
139
|
+
+ " set OMC_ASCII_STDOUT=0 (Windows cmd / PowerShell: $env:OMC_ASCII_STDOUT=0)\n"
|
|
140
|
+
+ " or switch the console itself: chcp 65001\n");
|
|
137
141
|
}
|
|
138
142
|
/**
|
|
139
143
|
* Attach a repeat warning to a refusal that repeats the previous one. Refusal
|