@stalfh233/omc-cli 0.4.67 → 0.4.69
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/lanes/runtime-data.js +7 -15
- package/dist/lanes/workflow.js +8 -0
- package/dist/recipes/businessrule.js +1 -1
- package/dist/usage-log.js +75 -1
- package/package.json +3 -3
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();
|
|
@@ -1048,22 +1048,14 @@ export function registerRuntimeDataLane(tools) {
|
|
|
1048
1048
|
draftObjectId = targetObjectId;
|
|
1049
1049
|
}
|
|
1050
1050
|
else if (formType === "2") {
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
if (!draftObjectId)
|
|
1059
|
-
throw new WireError("business", "submit chain: draft save returned no bizObjectId", 304001);
|
|
1060
|
-
// No post-save form/load (ticket 18): the submit persists
|
|
1061
|
-
// bizObject.data itself and updateBizObject merges patch-style onto
|
|
1062
|
-
// the stored row, and initSubmitBizObject.setOwner re-reads the
|
|
1063
|
-
// stored row's owner server-side — so formData + the saved id is the
|
|
1064
|
-
// complete submit payload.
|
|
1051
|
+
// Live 1.1.25 (r7-vehicle 2026-10-21): the previous chain did
|
|
1052
|
+
// form/save (creating a DRAFT row) and then submitted with that id —
|
|
1053
|
+
// the platform INSERTED A SECOND ROW instead of completing the first,
|
|
1054
|
+
// leaving one COMPLETED + one orphan DRAFT per submit (verified in
|
|
1055
|
+
// ia38g_veh_vehicle: same plateNo, same second, statuses COMPLETED and
|
|
1056
|
+
// DRAFT). form/submit persists bizObject.data itself (ticket 18), so
|
|
1057
|
+
// skip the pre-save and let the platform create+complete in one call.
|
|
1065
1058
|
payload.bizObject = {
|
|
1066
|
-
id: draftObjectId,
|
|
1067
1059
|
schemaCode: args.schemaCode,
|
|
1068
1060
|
sheetCode,
|
|
1069
1061
|
data: stripEngineOwnedKeys(formData),
|
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
|
};
|
|
@@ -77,7 +77,7 @@ const ruleBranchGraph = Object.freeze({
|
|
|
77
77
|
conditionalRequirements: [
|
|
78
78
|
"**系统节点顺序:BRANCH 必须排在 `SYSTEM_CREATE` 之后**——BRANCH 直接接在 START 后会运行时静默不生效、动作不回写(工具 fail closed)。注意这与 BIZ_ACTION 回写场景相反(那里系统节点要在动作之后)——本 recipe 是分支场景。**`SYSTEM_CREATE` 必须显式写**(工具对分支图的自动注入不可靠)。",
|
|
79
79
|
"**出口条件叶只 4 个键** `{currentSchemaDataItem(左值字段),ruleDataConditionType(操作符),ruleValueType,value}`;用 UI 键 `targetSchemaDataItem`+`ruleConditionType` 会 save+publish 过但运行时 50000(工具 fail closed `rule-graph-branch-condition-keys`)。",
|
|
80
|
-
"**下列键可省、工具自动补**(省 token;真机验证极简图 1170B vs 全显式 1690B,−31%):每个节点的 `nodeName`;BRANCH 的 `inputParam:\"_input\"`/`judgeMethod:\"DEFAULT\"`;自更新 UPDATE(`targetObjectCode`=本模型)的 `inputParam:\"_input\"`/`filterCondition`(身份匹配);非兜底路由的 `routeCondition:true`/`matchType`。仅 BRANCH 的 `sourceInputMethod` 与兜底 else 的 `routeCondition:false`
|
|
80
|
+
"**下列键可省、工具自动补**(省 token;真机验证极简图 1170B vs 全显式 1690B,−31%):每个节点的 `nodeName`;BRANCH 的 `inputParam:\"_input\"`/`judgeMethod:\"DEFAULT\"`;自更新 UPDATE(`targetObjectCode`=本模型)的 `inputParam:\"_input\"`/`filterCondition`(身份匹配);非兜底路由的 `routeCondition:true`/`matchType`。仅 BRANCH 的 `sourceInputMethod` 与兜底 else 的 `routeCondition:false` 需显式。**跨模型写(`targetObjectCode`≠规则模型)已真机验证可用**(1.1.25,2026-10-21:FIXED id / 文本字段 / 单跳关联字段三种 DYNAMIC 来源均命中)——前提是 ① 承载规则真的触发(Create 在提交时、Update 在 data.update、或绑流程事件);② **目标行必须已提交(COMPLETED)**,DRAFT 行不参与查询(日志「查找范围不存在数据」)。点号路径按子表解析:`关联字段.id` 与两跳 `a.b` 不可用。",
|
|
81
81
|
"预置规则的系统节点类型必须与 ruleCode 匹配(Create→SYSTEM_CREATE);放 SYSTEM_UPDATE 会 data.submit 50000(工具 fail closed)。",
|
|
82
82
|
],
|
|
83
83
|
priorToolReferences: [
|
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.69",
|
|
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.69",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.69",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|