@stalfh233/omc-cli 0.4.66 → 0.4.68
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-output.js +32 -2
- package/dist/cli.js +9 -2
- package/dist/lanes/workflow.js +8 -0
- package/dist/usage-log.js +75 -1
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +2 -0
package/dist/cli-output.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { writeFileSync } from "node:fs";
|
|
2
2
|
import { asciiStdout, serializeEnvelope } from "./cli-contract.js";
|
|
3
|
-
import { recordUsageOutput } from "./usage-log.js";
|
|
3
|
+
import { currentUsageCommand, priorRepeatedFailures, recordUsageOutput } from "./usage-log.js";
|
|
4
4
|
let options = { quiet: false, verbose: false };
|
|
5
5
|
/** Parse `--quiet`, `--verbose` and `--out <file>` from the raw argv once, before dispatch. */
|
|
6
6
|
export function configureOutput(argv) {
|
|
@@ -85,8 +85,38 @@ function emitAsciiBannerOnce() {
|
|
|
85
85
|
// 横幅走 stderr(不进管道、不破坏单信封契约),在痛点时刻给出两个解法。
|
|
86
86
|
process.stderr.write("[omc] 当前控制台代码页非 UTF-8,中文已 ASCII 转义防乱码。要原样中文:本次调用加 --utf8,或 chcp 65001 后重跑,或设 OMC_ASCII_STDOUT=0。\n");
|
|
87
87
|
}
|
|
88
|
+
/**
|
|
89
|
+
* Attach a repeat warning to a refusal that repeats the previous one. Refusal
|
|
90
|
+
* envelopes already say what is wrong; a third identical failure means the
|
|
91
|
+
* caller is tuning arguments inside a dead end (r6-onboarding 2026-10-21: 16
|
|
92
|
+
* repeat runs, 27 avoidable calls), so say so where it will be read.
|
|
93
|
+
*/
|
|
94
|
+
function withRepeatWarning(value) {
|
|
95
|
+
if (!value || typeof value !== "object")
|
|
96
|
+
return value;
|
|
97
|
+
const envelope = value;
|
|
98
|
+
const status = String(envelope.status ?? "");
|
|
99
|
+
if (status !== "refused" && status !== "error" && status !== "uncertain")
|
|
100
|
+
return value;
|
|
101
|
+
const result = envelope.result && typeof envelope.result === "object" ? envelope.result : undefined;
|
|
102
|
+
const blocker = typeof result?.blocker === "string" ? result.blocker : typeof envelope.blocker === "string" ? envelope.blocker : undefined;
|
|
103
|
+
const prior = priorRepeatedFailures(currentUsageCommand(), blocker);
|
|
104
|
+
if (prior.count < 2)
|
|
105
|
+
return value;
|
|
106
|
+
const repeatWarning = {
|
|
107
|
+
sameFailureCount: prior.count + 1,
|
|
108
|
+
blocker: blocker ?? null,
|
|
109
|
+
...(prior.requestIds.length > 0 ? { previousRequestIds: prior.requestIds } : {}),
|
|
110
|
+
hint: status === "uncertain"
|
|
111
|
+
? `同一调用已连续返回 uncertain ${prior.count + 1} 次。uncertain 表示结果未确定:先按 requestId 回读真实状态再决定,不要盲目重发写请求。`
|
|
112
|
+
: `同一调用已连续失败 ${prior.count + 1} 次(blocker ${blocker ?? "unknown"})。不要再微调参数重试:换形状或换工具——先 describe/配方确认契约,或按 blocker 指向的替代路径处理。`,
|
|
113
|
+
};
|
|
114
|
+
if (result)
|
|
115
|
+
return { ...envelope, result: { ...result, repeatWarning } };
|
|
116
|
+
return { ...envelope, repeatWarning };
|
|
117
|
+
}
|
|
88
118
|
export function writeEnvelope(value) {
|
|
89
|
-
const outputValue = compactForOutput(value);
|
|
119
|
+
const outputValue = compactForOutput(withRepeatWarning(value));
|
|
90
120
|
const text = serializeEnvelope(outputValue);
|
|
91
121
|
if (asciiStdout())
|
|
92
122
|
emitAsciiBannerOnce();
|
package/dist/cli.js
CHANGED
|
@@ -499,9 +499,16 @@ async function cmdArtifactGet(id, transport, options = {}) {
|
|
|
499
499
|
? { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: null, pathMiss: true, missedAt: projected.at, available: projected.available, hint: `路径未命中段 "${projected.at}"——available 是该层可用的键/范围;方括号下标(a[0])与点式(a.0)等价` }
|
|
500
500
|
: { status: "ok", artifactId: id, sha256: artifact.sha256, path: options.jq, content: projected.value ?? null }, transport);
|
|
501
501
|
}
|
|
502
|
-
|
|
502
|
+
// r6-onboarding 2026-10-21: 47 artifact fetches, 0 used a projection; the 10
|
|
503
|
+
// large ones (up to 55 KB) pulled whole workflow templates just to read one
|
|
504
|
+
// nested key. Nudge once, in the same response, instead of hoping the hint
|
|
505
|
+
// in `fetch` is noticed.
|
|
506
|
+
const unprojected = artifact.content.length > 8192
|
|
507
|
+
? { hint: `artifact ${Math.round(artifact.content.length / 1024)}KB 已整包返回。只要其中一段时用 --jq '<path>'(或 --summary 看形状、--keys 取顶层键),不要把整包带进上下文。` }
|
|
508
|
+
: {};
|
|
509
|
+
const result = { status: "ok", artifactId: id, sha256: artifact.sha256, content, ...unprojected };
|
|
503
510
|
if (Buffer.byteLength(JSON.stringify(result), "utf8") > transport.maxOutputBytes)
|
|
504
|
-
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, fetch: artifactFetchHint(id) }, transport);
|
|
511
|
+
return emitTransportResult({ status: "ok", artifactId: id, sha256: artifact.sha256, bytes: artifact.content.length, ...unprojected, fetch: artifactFetchHint(id) }, transport);
|
|
505
512
|
return emitTransportResult(result, transport);
|
|
506
513
|
}
|
|
507
514
|
catch (error) {
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -2226,10 +2226,18 @@ export function registerWorkflowLane(tools) {
|
|
|
2226
2226
|
schemaCode: linked?.schemaCode ?? wireString(item.schemaCode),
|
|
2227
2227
|
};
|
|
2228
2228
|
});
|
|
2229
|
+
// r6-onboarding 2026-10-21: 89 of 117 calls used this tool with only a
|
|
2230
|
+
// schemaCode — mostly to ask "where did the flow go", which the todo query
|
|
2231
|
+
// cannot answer (it lists pending items only). Say what this call is for
|
|
2232
|
+
// at the moment it is used, and name the two substitutes.
|
|
2233
|
+
const purposeHint = args.workflowInstanceId === undefined && args.workflowCode === undefined
|
|
2234
|
+
? "本调用用于发现待办并取 workItemId 做审批;只想看流程走到哪,用 workflow.instanceActivity;提交/审批后优先复用 data.submit 的 readback.nextSubmit,不要重复查询。"
|
|
2235
|
+
: undefined;
|
|
2229
2236
|
return {
|
|
2230
2237
|
total: page.total,
|
|
2231
2238
|
totalPages: page.totalPages,
|
|
2232
2239
|
count: items.length,
|
|
2240
|
+
...(purposeHint ? { purposeHint } : {}),
|
|
2233
2241
|
...(page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
|
|
2234
2242
|
items,
|
|
2235
2243
|
};
|
package/dist/usage-log.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
1
|
+
import { appendFileSync, existsSync, mkdirSync, openSync, readFileSync, readSync, closeSync, statSync } from "node:fs";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { dirname, resolve } from "node:path";
|
|
4
4
|
let pending;
|
|
@@ -9,6 +9,47 @@ function logPath() {
|
|
|
9
9
|
const projectEnv = resolve(process.cwd(), ".omc-env");
|
|
10
10
|
return existsSync(projectEnv) ? resolve(process.cwd(), ".omc", "usage.jsonl") : undefined;
|
|
11
11
|
}
|
|
12
|
+
/** The command string of the in-flight invocation (for diagnostics). */
|
|
13
|
+
export function currentUsageCommand() {
|
|
14
|
+
return pending?.command;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* How many consecutive PRIOR invocations failed the same way. A refusal tells
|
|
18
|
+
* the caller what is wrong; three identical refusals in a row mean the caller is
|
|
19
|
+
* tuning arguments inside a dead end instead of changing approach (r6-onboarding
|
|
20
|
+
* 2026-10-21: 16 such runs, 43 calls, 27 of them avoidable).
|
|
21
|
+
*/
|
|
22
|
+
export function priorRepeatedFailures(command, blocker, limit = 40) {
|
|
23
|
+
if (!command)
|
|
24
|
+
return { count: 0, requestIds: [] };
|
|
25
|
+
const path = logPath();
|
|
26
|
+
if (!path)
|
|
27
|
+
return { count: 0, requestIds: [] };
|
|
28
|
+
// Read only the tail: this runs on every refusal, and the log grows with the
|
|
29
|
+
// session (r6: 1528 records). Parsing the last ~64 KB is enough for `limit`.
|
|
30
|
+
const tail = readUsageTail(path, 65536).slice(-limit);
|
|
31
|
+
const matches = (record) => {
|
|
32
|
+
if (String(record.command ?? "") !== command)
|
|
33
|
+
return false;
|
|
34
|
+
const status = String(record.status ?? "");
|
|
35
|
+
if (status === "ok" || status === "dry-run" || status === "no-envelope")
|
|
36
|
+
return false;
|
|
37
|
+
if (blocker === undefined)
|
|
38
|
+
return true;
|
|
39
|
+
return String(record.blocker ?? "") === blocker;
|
|
40
|
+
};
|
|
41
|
+
let count = 0;
|
|
42
|
+
const requestIds = [];
|
|
43
|
+
for (let index = tail.length - 1; index >= 0; index -= 1) {
|
|
44
|
+
if (!matches(tail[index]))
|
|
45
|
+
break;
|
|
46
|
+
count += 1;
|
|
47
|
+
const requestId = tail[index].requestId;
|
|
48
|
+
if (typeof requestId === "string" && requestIds.length < 3)
|
|
49
|
+
requestIds.push(requestId);
|
|
50
|
+
}
|
|
51
|
+
return { count, requestIds };
|
|
52
|
+
}
|
|
12
53
|
function commandOf(argv) {
|
|
13
54
|
const values = [];
|
|
14
55
|
for (let index = 2; index < argv.length && values.length < 2; index += 1) {
|
|
@@ -91,6 +132,12 @@ export function recordUsageOutput(value, stdoutBytes, fileBytes = 0) {
|
|
|
91
132
|
pending.status = String(envelope.status ?? "unknown");
|
|
92
133
|
if (typeof error?.code === "string")
|
|
93
134
|
pending.errorCode = error.code;
|
|
135
|
+
// The blocker is the actionable half of a refusal (policy-refused alone says
|
|
136
|
+
// nothing); record it so repeat detection and accounting can group by cause.
|
|
137
|
+
const resultEnvelope = envelope.result && typeof envelope.result === "object" ? envelope.result : undefined;
|
|
138
|
+
const blocker = typeof resultEnvelope?.blocker === "string" ? resultEnvelope.blocker : typeof envelope.blocker === "string" ? envelope.blocker : undefined;
|
|
139
|
+
if (blocker)
|
|
140
|
+
pending.blocker = blocker;
|
|
94
141
|
// 0.4.16:requestId 落盘(信封瘦身把它从 stdout 移除后,排障关联的唯一落点)
|
|
95
142
|
if (typeof envelope.requestId === "string")
|
|
96
143
|
pending.requestId = envelope.requestId;
|
|
@@ -118,10 +165,37 @@ export function completeUsage(exitCode) {
|
|
|
118
165
|
status: current.status,
|
|
119
166
|
exitCode,
|
|
120
167
|
...(current.errorCode ? { errorCode: current.errorCode } : {}),
|
|
168
|
+
...(current.blocker ? { blocker: current.blocker } : {}),
|
|
121
169
|
...(current.requestId ? { requestId: current.requestId } : {}),
|
|
122
170
|
};
|
|
123
171
|
appendFileSync(path, `${JSON.stringify(record)}\n`, "utf8");
|
|
124
172
|
}
|
|
173
|
+
function readUsageTail(path, maxBytes) {
|
|
174
|
+
if (!existsSync(path))
|
|
175
|
+
return [];
|
|
176
|
+
const size = statSync(path).size;
|
|
177
|
+
const start = Math.max(0, size - maxBytes);
|
|
178
|
+
const fd = openSync(path, "r");
|
|
179
|
+
try {
|
|
180
|
+
const buffer = Buffer.alloc(size - start);
|
|
181
|
+
readSync(fd, buffer, 0, buffer.length, start);
|
|
182
|
+
const text = buffer.toString("utf8");
|
|
183
|
+
// Drop a leading partial line when the tail started mid-record.
|
|
184
|
+
const body = start > 0 ? text.slice(text.indexOf("\n") + 1) : text;
|
|
185
|
+
return body.split(/\r?\n/).filter(Boolean).flatMap((line) => {
|
|
186
|
+
try {
|
|
187
|
+
const value = JSON.parse(line);
|
|
188
|
+
return value && typeof value === "object" ? [value] : [];
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
return [];
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
finally {
|
|
196
|
+
closeSync(fd);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
125
199
|
function readUsage(path) {
|
|
126
200
|
if (!existsSync(path))
|
|
127
201
|
return [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.68",
|
|
4
4
|
"description": "CloudPivot 8.6 configuration capability CLI — governed, fail-closed, coverage-manifest bounded.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"access": "public"
|
|
51
51
|
},
|
|
52
52
|
"dependencies": {
|
|
53
|
-
"@stalfh233/omc-fake-platform": "0.4.
|
|
53
|
+
"@stalfh233/omc-fake-platform": "0.4.68",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.68",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -150,6 +150,8 @@ rules:
|
|
|
150
150
|
|
|
151
151
|
- **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
|
|
152
152
|
- 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
|
|
153
|
+
- **清理测试数据用批量删除**:`data.delete --args '{"schemaCode":"<code>","bizObjectIds":["id1","id2"]}'` 一次往返删多条(真机 r6:207 次逐条 `data.delete` 占 13.5% 调用)。
|
|
154
|
+
- **取 artifact 先投影**:`artifact get <id> --summary` 看形状 → `--jq '<path>'` 取那一段;只有确实要整份时才裸取(r6:47 次整包拉取 650KB,0 次用投影)。
|
|
153
155
|
- **核验状态用投影,别重复整条读**:`data.load --args '{"schemaCode":..,"bizObjectId":..,"fields":["sequenceStatus","<业务状态字段>"]}'` 只回指定字段(缺失项在 `missingFields`)——r4 实测同一对象被整条读 74 次;大结果先 `artifact get <id> --summary` → `--keys/--jq`,别整包内联进推理。
|
|
154
156
|
- **流程第一个节点必须是 `{Originator}` 填写节点**:平台 `WorkflowTemplateValidateUtility.validateStartActivity` 对「START 后直接接审批」报 401035「发起节点的参与者只能是{Originator}」,publish 回 errcode 0 但一个 ValidateMessage[] 就把模板挡在门外、不落库(真机 2026-10-21)。OMC 0.4.65 起在 `workflow.update`/`publish` 出门前拒绝(`workflow-first-activity-not-originator`)。
|
|
155
157
|
- **跨模型回写的目标行必须已提交(COMPLETED)**:DRAFT 行不参与目标查询,运行 0 行命中且日志只有「查找范围不存在数据」。单跳关联字段可直接作 DYNAMIC 来源(引擎取关联对象 id);点号路径按子表解析,`关联字段.id` / 两跳路径都不支持。
|