@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.
@@ -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
- const result = { status: "ok", artifactId: id, sha256: artifact.sha256, content };
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) {
@@ -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.66",
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.66",
53
+ "@stalfh233/omc-fake-platform": "0.4.68",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.66",
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` / 两跳路径都不支持。