@stalfh233/omc-cli 0.6.12 → 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 +8 -2
- package/dist/cli.js +279 -67
- 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 +168 -31
- 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 +3 -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
package/dist/refusal-guidance.js
CHANGED
|
@@ -15,12 +15,18 @@ const SHAPE_GATES = Object.freeze({
|
|
|
15
15
|
recipeCommand: { tool: "data.submit", recipe: "plain-data-submit" },
|
|
16
16
|
doNot: "不要传毫秒数(如 1790000000000)——不报错但落库 NULL,读回才发现",
|
|
17
17
|
},
|
|
18
|
+
"child-rows-missing-id": {
|
|
19
|
+
whatWentWrong: "更新已有记录时,子表行没带 id",
|
|
20
|
+
correctShape: "三合一:既有行带 id(改);追加行 {…, rowStatus:\"Added\"};删除行 {id, rowStatus:\"Deleted\"}(只给 id)。少传行不会删",
|
|
21
|
+
recipeCommand: { tool: "data.update", recipe: "child-rows-upsert" },
|
|
22
|
+
doNot: "不要用同一批无 id 子行重试 update——每次都会再插一遍(r17 实测 2 行变 8 行);也不要靠少传行删行",
|
|
23
|
+
},
|
|
24
|
+
// 旧名兼容(r29b F7 前叫 submit-…,data.update 也会抛)
|
|
18
25
|
"submit-child-rows-missing-id": {
|
|
19
26
|
whatWentWrong: "更新已有记录时,子表行没带 id",
|
|
20
|
-
correctShape: "
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
doNot: "不要用同一批无 id 子行重试 update——每次都会再插一遍(r17 实测 2 行变 8 行)",
|
|
27
|
+
correctShape: "三合一:既有行带 id(改);追加行 {…, rowStatus:\"Added\"};删除行 {id, rowStatus:\"Deleted\"}(只给 id)。少传行不会删",
|
|
28
|
+
recipeCommand: { tool: "data.update", recipe: "child-rows-upsert" },
|
|
29
|
+
doNot: "不要用同一批无 id 子行重试 update——每次都会再插一遍(r17 实测 2 行变 8 行);也不要靠少传行删行",
|
|
24
30
|
},
|
|
25
31
|
"submit-approval-agree-misplaced": {
|
|
26
32
|
whatWentWrong: "agree 放在了 approval 对象里,平台只读顶层 agree",
|
|
@@ -105,7 +111,9 @@ const SHAPE_GATES = Object.freeze({
|
|
|
105
111
|
* retry command — a command that would fail again is worse than none.
|
|
106
112
|
*/
|
|
107
113
|
const FIX_FIRST_GATES = Object.freeze({
|
|
108
|
-
"workflow-start-permission-denied": "40009 有三个独立同码原因:① 发起人角色没挂该流程的发起权限(ouScope ∩ 发起人部门链,全空会静默回退发起人);② 流程未启用/未发布;③ 模型未绑定流程。先 omc call workflow.get_published
|
|
114
|
+
"workflow-start-permission-denied": "40009 有三个独立同码原因:① 发起人角色没挂该流程的发起权限(ouScope ∩ 发起人部门链,全空会静默回退发起人);② 流程未启用/未发布;③ 模型未绑定流程。先 omc call workflow.permissions / workflow.get_published 核对;管理员发起成功带 bypassReason 时不证明白名单生效——换非管理员环境验证。不要反复换参数重试 data.submit",
|
|
115
|
+
"workflow-bound-to-listview": "303024 列表绑定了流程:直接粘贴回执 nextCommand(listview.configure:actions:[] + replaceActions:true + 现成 fieldCodes)解绑,再 workflow.delete;整体清理用 model-purge",
|
|
116
|
+
"workflow-start-permission-empty": "发起权限表为空:除平台管理员外无人能发起。用 workflow.addPermission 授给部门/角色/用户;管理员 data.submit 成功不证明白名单生效",
|
|
109
117
|
"environment-write-disabled": "当前环境 profile 不可写:换可写环境,或让维护者在配置里开 writable——CLI 侧没有绕过开关",
|
|
110
118
|
"production-confirmation-required": "生产环境的写需要显式 confirmProduction:true——确认这次写入真的要落生产再加该参数,不要靠重试",
|
|
111
119
|
"app-not-published": "目标应用未发布:先 workflow/form/field 各面发布(或 app 级发布),再重试运行面操作",
|
|
@@ -191,7 +199,11 @@ export function refusalRecovery(blocker, payload, options = {}) {
|
|
|
191
199
|
// message/detail, so both refused and frozen (uncertain) envelopes teach it.
|
|
192
200
|
if (mentionsDashboardChannel(payload))
|
|
193
201
|
return dashboardBoundaryGuidance();
|
|
194
|
-
|
|
202
|
+
// r29b F7: blockerAliases 让旧名也能命中新引导。
|
|
203
|
+
const aliasBlockers = Array.isArray(payload.blockerAliases)
|
|
204
|
+
? payload.blockerAliases.filter((entry) => typeof entry === "string")
|
|
205
|
+
: [];
|
|
206
|
+
const shape = SHAPE_GATES[blocker] ?? aliasBlockers.map((alias) => SHAPE_GATES[alias]).find((entry) => entry !== undefined);
|
|
195
207
|
if (shape) {
|
|
196
208
|
const command = resolveCommand(shape, options.registry);
|
|
197
209
|
const { recipeCommand: _drop, ...rest } = shape;
|
package/dist/rollback-spec.js
CHANGED
|
@@ -1,7 +1,21 @@
|
|
|
1
|
+
/** Business-key extractors for array alignment in deepDiff (r30a F14/F15). */
|
|
2
|
+
const ARRAY_ALIGN_KEYS = ["code", "id", "propertyCode", "activityCode", "ruleCode", "nodeCode"];
|
|
3
|
+
function arrayEntryKey(entry) {
|
|
4
|
+
if (entry === null || typeof entry !== "object")
|
|
5
|
+
return undefined;
|
|
6
|
+
const record = entry;
|
|
7
|
+
for (const key of ARRAY_ALIGN_KEYS) {
|
|
8
|
+
const value = record[key];
|
|
9
|
+
if (value !== undefined && value !== null && String(value) !== "")
|
|
10
|
+
return `${key}:${String(value)}`;
|
|
11
|
+
}
|
|
12
|
+
return undefined;
|
|
13
|
+
}
|
|
1
14
|
/**
|
|
2
15
|
* Field-level deep diff for the plan surface. Returns at most `limit`
|
|
3
|
-
* changed paths plus the total count
|
|
4
|
-
* (
|
|
16
|
+
* changed paths plus the total count. Arrays of objects with a business key
|
|
17
|
+
* (`code`/`id`/…) are aligned by that key; index-only comparison is the
|
|
18
|
+
* fallback for scalars / unkeyed rows (r30a F14).
|
|
5
19
|
*/
|
|
6
20
|
export function deepDiff(before, current, path = "", limit = 40, acc = []) {
|
|
7
21
|
const walk = (b, c, p) => {
|
|
@@ -33,6 +47,31 @@ export function deepDiff(before, current, path = "", limit = 40, acc = []) {
|
|
|
33
47
|
const cA = c;
|
|
34
48
|
if (bA.length !== cA.length)
|
|
35
49
|
acc.push({ path: p, before: `<array×${bA.length}>`, current: `<array×${cA.length}>` });
|
|
50
|
+
const bKeys = bA.map(arrayEntryKey);
|
|
51
|
+
const cKeys = cA.map(arrayEntryKey);
|
|
52
|
+
const canAlign = bKeys.every((key) => key !== undefined) && cKeys.every((key) => key !== undefined)
|
|
53
|
+
&& new Set(bKeys).size === bKeys.length && new Set(cKeys).size === cKeys.length;
|
|
54
|
+
if (canAlign) {
|
|
55
|
+
const bMap = new Map(bKeys.map((key, index) => [key, bA[index]]));
|
|
56
|
+
const cMap = new Map(cKeys.map((key, index) => [key, cA[index]]));
|
|
57
|
+
for (const key of new Set([...bMap.keys(), ...cMap.keys()])) {
|
|
58
|
+
if (acc.length >= limit)
|
|
59
|
+
return;
|
|
60
|
+
const inB = bMap.has(key);
|
|
61
|
+
const inC = cMap.has(key);
|
|
62
|
+
const short = key.includes(":") ? key.slice(key.indexOf(":") + 1) : key;
|
|
63
|
+
if (!inB) {
|
|
64
|
+
acc.push({ path: `${p}[+${short}]`, before: "<absent>", current: cMap.get(key) });
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (!inC) {
|
|
68
|
+
acc.push({ path: `${p}[-${short}]`, before: bMap.get(key), current: "<absent>" });
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
walk(bMap.get(key), cMap.get(key), `${p}[${short}]`);
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
36
75
|
const shared = Math.min(bA.length, cA.length);
|
|
37
76
|
for (let i = 0; i < shared && acc.length < limit; i += 1)
|
|
38
77
|
walk(bA[i], cA[i], `${p}[${i}]`);
|
package/dist/rollback.js
CHANGED
|
@@ -43,8 +43,19 @@ export async function listRollback(options, context) {
|
|
|
43
43
|
entries = entries.filter((entry) => entry.target.includes(options.target));
|
|
44
44
|
if (options.tool)
|
|
45
45
|
entries = entries.filter((entry) => entry.tool === options.tool || entry.tool.startsWith(`${options.tool}.`));
|
|
46
|
+
// r30a F22: hide documented no-ops from the default timeline.
|
|
47
|
+
const actionable = entries.filter((entry) => entry.noop !== true);
|
|
46
48
|
const limit = options.limit ? Number(options.limit) : 40;
|
|
47
|
-
const timeline = [...
|
|
49
|
+
const timeline = [...actionable].reverse().slice(0, Number.isSafeInteger(limit) && limit > 0 ? limit : 40);
|
|
50
|
+
// r30a F9/F11/F20: surface which writes in this window cannot be undone.
|
|
51
|
+
const unrollbackable = timeline
|
|
52
|
+
.filter((entry) => entry.rollbackable === false)
|
|
53
|
+
.map((entry) => ({
|
|
54
|
+
seq: entry.seq,
|
|
55
|
+
tool: entry.tool,
|
|
56
|
+
target: entry.target,
|
|
57
|
+
reason: entry.note ?? "rollbackable:false",
|
|
58
|
+
}));
|
|
48
59
|
return {
|
|
49
60
|
status: "ok",
|
|
50
61
|
environment,
|
|
@@ -57,9 +68,11 @@ export async function listRollback(options, context) {
|
|
|
57
68
|
target: entry.target,
|
|
58
69
|
rollbackable: entry.rollbackable,
|
|
59
70
|
...(entry.note ? { note: entry.note } : {}),
|
|
71
|
+
...(entry.steps !== undefined ? { steps: entry.steps } : {}),
|
|
60
72
|
artifact: basename(entry.artifactFile),
|
|
61
73
|
})),
|
|
62
|
-
|
|
74
|
+
...(unrollbackable.length > 0 ? { unrollbackable } : {}),
|
|
75
|
+
hint: "omc rollback plan <seq> 预览恢复(不写);omc rollback apply <seq> 执行恢复;unrollbackable[] 是本窗口内撤不了的写",
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
78
|
async function buildPlan(ref, environmentOption, context) {
|
|
@@ -68,13 +81,20 @@ async function buildPlan(ref, environmentOption, context) {
|
|
|
68
81
|
const entry = resolveJournalEntry(environment, ref, ctx.artifactsDir);
|
|
69
82
|
if (!entry)
|
|
70
83
|
throw new Error(`journal(${environment})中找不到 ${ref}:先 omc rollback list 核对 seq`);
|
|
84
|
+
// r30a F9/F12: compound writes are journaled with rollbackable:false and may
|
|
85
|
+
// have NO RollbackSpec on the tool (configure_model). Do not call findSpec
|
|
86
|
+
// for the unrollbackable short-circuit — that used to throw and become exit 15.
|
|
71
87
|
const unrollbackable = (reason) => ({
|
|
72
88
|
entry,
|
|
73
|
-
spec:
|
|
89
|
+
spec: {
|
|
90
|
+
target: () => entry.target,
|
|
91
|
+
read: async () => null,
|
|
92
|
+
inverse: () => ({ kind: "irreversible", reason }),
|
|
93
|
+
},
|
|
74
94
|
before: undefined,
|
|
75
95
|
artifactArgs: {},
|
|
76
96
|
context: ctx,
|
|
77
|
-
plan: { environmentMatch: true, diff: { changes: [], total: 0 }, laterWrites:
|
|
97
|
+
plan: { environmentMatch: true, diff: { changes: [], total: 0 }, laterWrites: laterWritesOnTarget(environment, ctx.artifactsDir, entry.seq, entry.target), inverse: { kind: "irreversible", reason } },
|
|
78
98
|
});
|
|
79
99
|
if (!entry.rollbackable)
|
|
80
100
|
return unrollbackable(entry.note ?? "该写的 before 快照捕获失败,无法回滚");
|
|
@@ -122,13 +142,17 @@ export async function planRollback(ref, options, context) {
|
|
|
122
142
|
};
|
|
123
143
|
const script = plan.inverse.kind === "calls" ? plan.inverse.calls.map((call) => JSON.stringify({ tool: call.tool, args: { ...call.args, environment: entry.environment } })) : [];
|
|
124
144
|
const scriptOmitted = script.join("").length > 6000;
|
|
145
|
+
const irreversible = plan.inverse.kind === "irreversible";
|
|
125
146
|
return {
|
|
126
147
|
status: "ok",
|
|
127
148
|
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target, ts: entry.ts },
|
|
128
149
|
diff: { changedPaths: plan.diff.changes.map((change) => ({ path: change.path, before: preview(change.before), current: preview(change.current) })), total: plan.diff.total },
|
|
129
|
-
|
|
150
|
+
// r30a F12: do not claim "will overwrite later writes" when inverse is irreversible.
|
|
151
|
+
...(plan.laterWrites.length > 0 && !irreversible
|
|
130
152
|
? { warning: `该目标在此写之后还有 ${plan.laterWrites.length} 次写(seq ${plan.laterWrites.map((w) => w.seq).join(", ")});本次回滚会用 before 参数覆盖这些后续变更`, laterWrites: plan.laterWrites.map((w) => ({ seq: w.seq, tool: w.tool, ts: w.ts })) }
|
|
131
|
-
:
|
|
153
|
+
: plan.laterWrites.length > 0
|
|
154
|
+
? { laterWrites: plan.laterWrites.map((w) => ({ seq: w.seq, tool: w.tool, ts: w.ts })), laterWritesNote: "本写 irreversible,不会覆盖后续写;若要恢复后续变更请对那些 seq 分别 plan/apply" }
|
|
155
|
+
: {}),
|
|
132
156
|
inverse: plan.inverse.kind === "calls"
|
|
133
157
|
? {
|
|
134
158
|
kind: "calls",
|
|
@@ -139,19 +163,33 @@ export async function planRollback(ref, options, context) {
|
|
|
139
163
|
: { script }),
|
|
140
164
|
}
|
|
141
165
|
: { kind: "irreversible", reason: plan.inverse.reason, ...(plan.inverse.partial ? { partial: plan.inverse.partial } : {}) },
|
|
142
|
-
hint:
|
|
166
|
+
hint: irreversible
|
|
167
|
+
? `本写不可逆(apply 会 skipped/exit 0);${plan.laterWrites.length > 0 ? `下一步可试 omc rollback plan ${plan.laterWrites[0].seq} --env ${entry.environment}` : "无后续可恢复写"}`
|
|
168
|
+
: "预览不写入;确认后 omc rollback apply <seq>(恢复走完整写闸门,回滚本身也会记 journal,可再回滚)",
|
|
143
169
|
};
|
|
144
170
|
}
|
|
145
171
|
export async function applyRollback(ref, options, context) {
|
|
146
172
|
const loaded = await buildPlan(ref, options.environment, context);
|
|
147
173
|
const { entry, plan, spec, artifactArgs, before, context: ctx } = loaded;
|
|
148
|
-
if (plan.inverse.kind !== "calls" || plan.inverse.calls.length === 0)
|
|
174
|
+
if (plan.inverse.kind !== "calls" || plan.inverse.calls.length === 0) {
|
|
175
|
+
// r30a F12: irreversible is an expected answer → status:"skipped" + exit 0
|
|
176
|
+
// (was unclassified-status / exit 15). Keep reason + next step pasteable.
|
|
177
|
+
const inverse = plan.inverse;
|
|
178
|
+
const irreversible = inverse.kind === "irreversible";
|
|
179
|
+
const reason = irreversible ? inverse.reason : "无可执行恢复调用";
|
|
180
|
+
const later = plan.laterWrites;
|
|
149
181
|
return {
|
|
150
|
-
status:
|
|
182
|
+
status: irreversible ? "skipped" : "ok",
|
|
183
|
+
applied: false,
|
|
184
|
+
reason,
|
|
151
185
|
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target },
|
|
152
|
-
inverse
|
|
153
|
-
message:
|
|
186
|
+
inverse,
|
|
187
|
+
message: reason,
|
|
188
|
+
...(later.length > 0
|
|
189
|
+
? { nextCommand: `omc rollback plan ${later[0].seq} --env ${entry.environment}`, hint: `本写不可逆;可对后续写 seq ${later.map((w) => w.seq).join(", ")} 再 plan/apply` }
|
|
190
|
+
: {}),
|
|
154
191
|
};
|
|
192
|
+
}
|
|
155
193
|
const results = [];
|
|
156
194
|
for (const [index, call] of plan.inverse.calls.entries()) {
|
|
157
195
|
const { exitCode, result } = await runToolCall(call.tool, {
|
package/dist/script.js
CHANGED
|
@@ -5,44 +5,80 @@ import { SessionManager } from "./session-manager.js";
|
|
|
5
5
|
import { makeToolContext } from "./tools.js";
|
|
6
6
|
import { runToolCall } from "./call.js";
|
|
7
7
|
import { currentUsageId, recordScriptCall } from "./usage-log.js";
|
|
8
|
+
function isCommentOrBlank(line) {
|
|
9
|
+
return line.length === 0 || line.startsWith("#") || line.startsWith("//");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Parse NDJSON. Blank / `#` / `//` comment lines are ignored. Other illegal
|
|
13
|
+
* lines are collected as `skipped` (r31b F16/F17) so a trailing `// total 32`
|
|
14
|
+
* no longer aborts the whole batch before any call runs.
|
|
15
|
+
*/
|
|
8
16
|
export function parseScript(text) {
|
|
9
17
|
const lines = [];
|
|
18
|
+
const skipped = [];
|
|
10
19
|
const raw = text.split("\n");
|
|
11
20
|
for (const [index, untrimmed] of raw.entries()) {
|
|
12
21
|
const line = untrimmed.trim();
|
|
13
|
-
if (line
|
|
22
|
+
if (isCommentOrBlank(line))
|
|
14
23
|
continue;
|
|
15
24
|
let parsed;
|
|
16
25
|
try {
|
|
17
26
|
parsed = JSON.parse(line);
|
|
18
27
|
}
|
|
19
28
|
catch (error) {
|
|
20
|
-
|
|
29
|
+
skipped.push({
|
|
30
|
+
sourceLine: index + 1,
|
|
31
|
+
reason: `不是合法 JSON:${error instanceof Error ? error.message : String(error)}`,
|
|
32
|
+
text: line.slice(0, 120),
|
|
33
|
+
});
|
|
34
|
+
continue;
|
|
21
35
|
}
|
|
22
36
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed) || typeof parsed.tool !== "string") {
|
|
23
|
-
|
|
37
|
+
skipped.push({
|
|
38
|
+
sourceLine: index + 1,
|
|
39
|
+
reason: `必须是 {"tool":"<名称>","args":{...}} 形状(tool 必填)`,
|
|
40
|
+
text: line.slice(0, 120),
|
|
41
|
+
});
|
|
42
|
+
continue;
|
|
24
43
|
}
|
|
25
44
|
const entry = parsed;
|
|
26
|
-
if (entry.args !== undefined && (entry.args === null || typeof entry.args !== "object" || Array.isArray(entry.args)))
|
|
27
|
-
|
|
45
|
+
if (entry.args !== undefined && (entry.args === null || typeof entry.args !== "object" || Array.isArray(entry.args))) {
|
|
46
|
+
skipped.push({
|
|
47
|
+
sourceLine: index + 1,
|
|
48
|
+
reason: "args 必须是 JSON 对象",
|
|
49
|
+
text: line.slice(0, 120),
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
28
53
|
lines.push({
|
|
29
54
|
tool: entry.tool,
|
|
30
55
|
...(typeof entry.args === "object" && entry.args !== null ? { args: entry.args } : {}),
|
|
31
56
|
...(typeof entry.env === "string" ? { env: entry.env } : {}),
|
|
32
57
|
...(entry.dryRun === true ? { dryRun: true } : {}),
|
|
58
|
+
sourceLine: index + 1,
|
|
33
59
|
});
|
|
34
60
|
}
|
|
35
|
-
return lines;
|
|
61
|
+
return { lines, skipped };
|
|
36
62
|
}
|
|
37
63
|
export async function runScript(lines, options = {}) {
|
|
38
64
|
const print = options.print ?? ((line) => console.log(line));
|
|
39
65
|
const context = options.context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
40
|
-
// The outer `script` invocation is already recorded by completeUsage; these
|
|
41
|
-
// inner records are what make a batch measurable at all. Before this, all 23
|
|
42
|
-
// `script -` calls in round 10 logged stdoutBytes=0 and the actual work was
|
|
43
|
-
// invisible (the outer record says "script -", the log says nothing else).
|
|
44
66
|
const parentId = currentUsageId();
|
|
67
|
+
const skipped = options.skipped ?? [];
|
|
68
|
+
for (const skip of skipped) {
|
|
69
|
+
print(JSON.stringify({
|
|
70
|
+
kind: "script-skipped",
|
|
71
|
+
sourceLine: skip.sourceLine,
|
|
72
|
+
reason: skip.reason,
|
|
73
|
+
text: skip.text,
|
|
74
|
+
note: "非法行已跳过(# 与 // 开头的注释/空行会被忽略;其它非法行计入 summary.skipped)",
|
|
75
|
+
}));
|
|
76
|
+
}
|
|
45
77
|
let failed = false;
|
|
78
|
+
let ok = 0;
|
|
79
|
+
let failedCount = 0;
|
|
80
|
+
const failedIndexes = [];
|
|
81
|
+
let stoppedAt;
|
|
46
82
|
for (const [index, line] of lines.entries()) {
|
|
47
83
|
const startedMs = Date.now();
|
|
48
84
|
const { exitCode, result } = await runToolCall(line.tool, {
|
|
@@ -67,12 +103,36 @@ export async function runScript(lines, options = {}) {
|
|
|
67
103
|
print(rendered);
|
|
68
104
|
if (exitCode !== 0) {
|
|
69
105
|
failed = true;
|
|
106
|
+
failedCount += 1;
|
|
107
|
+
failedIndexes.push(index);
|
|
70
108
|
if (options.continueOnError !== true) {
|
|
71
|
-
|
|
72
|
-
|
|
109
|
+
stoppedAt = index;
|
|
110
|
+
print(JSON.stringify({
|
|
111
|
+
kind: "script-stopped",
|
|
112
|
+
stoppedAtIndex: index,
|
|
113
|
+
tool: line.tool,
|
|
114
|
+
remaining: lines.length - index - 1,
|
|
115
|
+
note: `第 ${index + 1} 行失败,后续 ${lines.length - index - 1} 行未执行;加 --continue 跑完全部(默认首败即停)`,
|
|
116
|
+
}));
|
|
117
|
+
break;
|
|
73
118
|
}
|
|
74
119
|
}
|
|
120
|
+
else {
|
|
121
|
+
ok += 1;
|
|
122
|
+
}
|
|
75
123
|
}
|
|
124
|
+
print(JSON.stringify({
|
|
125
|
+
summary: {
|
|
126
|
+
total: lines.length,
|
|
127
|
+
ok,
|
|
128
|
+
failed: failedCount,
|
|
129
|
+
skipped: skipped.length,
|
|
130
|
+
...(failedIndexes.length > 0 ? { failedIndexes } : {}),
|
|
131
|
+
...(stoppedAt !== undefined ? { stoppedAt } : {}),
|
|
132
|
+
},
|
|
133
|
+
}));
|
|
134
|
+
if (lines.length === 0 && skipped.length > 0)
|
|
135
|
+
return 10; // invalid-input: nothing runnable
|
|
76
136
|
return failed ? 1 : 0;
|
|
77
137
|
}
|
|
78
138
|
export function readScriptInput(path) {
|
package/dist/skills.js
CHANGED
|
@@ -34,9 +34,78 @@ function skillFiles() {
|
|
|
34
34
|
return [];
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
|
+
/** r29a F4:合成 topic——跨 app/folder/model/rule 的应用参数矩阵(不落盘 md,省预算)。 */
|
|
38
|
+
/** r31b F4:合成 topic——并发语义(真机实测结论,不落盘 md)。 */
|
|
39
|
+
const SYNTHETIC_SKILLS = Object.freeze({
|
|
40
|
+
"app-scope": {
|
|
41
|
+
name: "app-scope",
|
|
42
|
+
description: "跨 app/folder/model/rule 的应用范围参数名矩阵(主名/别名)",
|
|
43
|
+
content: [
|
|
44
|
+
"# 应用范围参数(app-scope)",
|
|
45
|
+
"",
|
|
46
|
+
"同一能力族里应用参数名不完全统一。一次看完,别逐个 `describe --full`。",
|
|
47
|
+
"",
|
|
48
|
+
"## 矩阵",
|
|
49
|
+
"",
|
|
50
|
+
"| 工具面 | 主参数 | 别名 | 备注 |",
|
|
51
|
+
"| --- | --- | --- | --- |",
|
|
52
|
+
"| `app.*` | (多数无;create/delete 用 `code`) | — | 管应用包本身 |",
|
|
53
|
+
"| `folder.*` | `applicationCode` | `appCode` | 双别名;冲突报 `app-code-conflict` |",
|
|
54
|
+
"| `model.*` | `applicationCode` | **不认** `appCode` | 传 `appCode` → `unknown-app-scope-arg` |",
|
|
55
|
+
"| `rule.*` | **无** | — | 按 `schemaCode` 定位;传应用键 → `tool-has-no-app-scope` |",
|
|
56
|
+
"| `report.*` | `appCode` | — | 报表面主用短名 |",
|
|
57
|
+
"",
|
|
58
|
+
"## 怎么查",
|
|
59
|
+
"",
|
|
60
|
+
"- 单能力:`omc capability model` / `folder`(回执带 `appScopeArgs`)",
|
|
61
|
+
"- 单工具:`omc describe <tool> --param applicationCode`",
|
|
62
|
+
"- 本 topic:`omc help app-scope`",
|
|
63
|
+
"",
|
|
64
|
+
"## 易踩坑",
|
|
65
|
+
"",
|
|
66
|
+
"1. `folder.tree` 能用 `appCode`,不能据此推断 `model.list` 也认。",
|
|
67
|
+
"2. `rule.list` 传 `applicationCode` 会被拒——规则面没有应用维度。",
|
|
68
|
+
"3. 不存在的 `applicationCode` 在 `model.list` 会 `app-not-found`(不是空列表)。",
|
|
69
|
+
"",
|
|
70
|
+
].join("\n"),
|
|
71
|
+
},
|
|
72
|
+
concurrency: {
|
|
73
|
+
name: "concurrency",
|
|
74
|
+
description: "环境写锁 / 记录级 last-writer-wins / version 非乐观锁(r31 真机)",
|
|
75
|
+
content: [
|
|
76
|
+
"# 并发语义(r31 真机)",
|
|
77
|
+
"",
|
|
78
|
+
"写锁是**环境级**;记录级**无乐观锁**。CLI 不提供 `ifVersion`。以下全部双进程实测。",
|
|
79
|
+
"",
|
|
80
|
+
"| 操作 | 保护 | 结果 |",
|
|
81
|
+
"| --- | --- | --- |",
|
|
82
|
+
"| 并行写 | 环境写锁 | 输家 `write-lock-held`(`retryable`/`lockOwner`/`nextCommand`) |",
|
|
83
|
+
"| 同记录 `data.update` 不同字段 +`--wait` | 写锁串行+patch | 两字段都保住;**同字段**后写覆盖 |",
|
|
84
|
+
"| 读-改-写(旧值算新值) | **无** | 后写覆盖 |",
|
|
85
|
+
"| 行 `version` | 只读计数器 | update 成功 +1;`formData.version` 静默忽略(**不是乐观锁**) |",
|
|
86
|
+
"| 并行同 code 字段/模型 | 写锁+存在性闸 | 一成功;一 `duplicate-field`/`model-create-duplicate` |",
|
|
87
|
+
"| `data.list` 翻页 vs 写 | 读无快照 | 无 `snapshotAt`;可重复/漏行,`total` 可漂移 |",
|
|
88
|
+
"| 同 id `data.delete` | 写锁串行 | 第二次 `performed.skipped:\"already-absent\"` |",
|
|
89
|
+
"",
|
|
90
|
+
"`data.list` 唯一一致性键是 `filter.draftPolicy`(草稿是否计入)。",
|
|
91
|
+
"",
|
|
92
|
+
"读面数组键:`field.list`→`fields`;`model.list`→`models`;`data.list`→`items`(别用错 `--jq`)。",
|
|
93
|
+
"",
|
|
94
|
+
"```bash",
|
|
95
|
+
"(omc call data.save --env local --args '{\"schemaCode\":\"<m>\",\"formData\":{\"code\":\"a\",\"value\":1}}') &",
|
|
96
|
+
"(omc call data.save --env local --args '{\"schemaCode\":\"<m>\",\"formData\":{\"code\":\"b\",\"value\":2}}') & wait",
|
|
97
|
+
"```",
|
|
98
|
+
"",
|
|
99
|
+
"- 写串行或 `--wait`;**不要** shell 重试循环。",
|
|
100
|
+
"- 批量用 `omc script`(同进程更快);**测写锁必须多进程 `omc call`**。",
|
|
101
|
+
"- 详见 `omc help omc-write-hazards --section \"写调用纪律\"`。",
|
|
102
|
+
"",
|
|
103
|
+
].join("\n"),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
37
106
|
/** Every methodology topic exposed by `omc help`. */
|
|
38
107
|
export function listSkills() {
|
|
39
|
-
|
|
108
|
+
const files = skillFiles().map((file) => {
|
|
40
109
|
const name = file.replace(/\.md$/, "");
|
|
41
110
|
let description = `OMC methodology: ${name}`;
|
|
42
111
|
try {
|
|
@@ -47,6 +116,8 @@ export function listSkills() {
|
|
|
47
116
|
}
|
|
48
117
|
return { name, description };
|
|
49
118
|
});
|
|
119
|
+
const synthetic = Object.values(SYNTHETIC_SKILLS).map(({ name, description }) => ({ name, description }));
|
|
120
|
+
return [...files, ...synthetic].sort((left, right) => left.name.localeCompare(right.name));
|
|
50
121
|
}
|
|
51
122
|
/**
|
|
52
123
|
* r43 后 backlog:`omc help 编号` 曾零命中(高频中文业务词不是 topic 名)。
|
|
@@ -76,6 +147,11 @@ const HELP_TOPIC_SYNONYMS = Object.freeze({
|
|
|
76
147
|
验收: ["omc-acceptance-criteria"],
|
|
77
148
|
入门: ["omc-start-here"],
|
|
78
149
|
拒绝: ["omc-refusals"],
|
|
150
|
+
并发: ["concurrency", "omc-write-hazards"],
|
|
151
|
+
竞争: ["concurrency"],
|
|
152
|
+
乐观锁: ["concurrency"],
|
|
153
|
+
写锁: ["concurrency", "omc-write-hazards"],
|
|
154
|
+
快照: ["concurrency"],
|
|
79
155
|
});
|
|
80
156
|
export function suggestHelpTopics(query) {
|
|
81
157
|
if (query === undefined || query.length === 0)
|
|
@@ -99,15 +175,25 @@ export function suggestHelpTopics(query) {
|
|
|
99
175
|
}
|
|
100
176
|
return [...hits].slice(0, 5);
|
|
101
177
|
}
|
|
178
|
+
/** Appended to every `--all` body so the probe learns the cheap face (r31c F2). */
|
|
179
|
+
const HELP_ALL_SECTION_HINT = '\n\n> hint:"下次可按节读:omc help <topic> --section <标题>"\n';
|
|
102
180
|
/** Read one methodology topic for `omc help <topic>`. */
|
|
103
181
|
export function readSkill(name) {
|
|
104
182
|
if (!/^[a-z0-9-]+$/.test(name))
|
|
105
183
|
return undefined;
|
|
184
|
+
if (SYNTHETIC_SKILLS[name] !== undefined) {
|
|
185
|
+
const synthetic = SYNTHETIC_SKILLS[name];
|
|
186
|
+
return { ...synthetic, content: `${synthetic.content.trimEnd()}${HELP_ALL_SECTION_HINT}` };
|
|
187
|
+
}
|
|
106
188
|
const entry = listSkills().find((skill) => skill.name === name);
|
|
107
189
|
if (entry === undefined)
|
|
108
190
|
return undefined;
|
|
109
191
|
try {
|
|
110
|
-
|
|
192
|
+
const content = normalizeLineEndings(readFileSync(join(skillsDir, `${name}.md`), "utf8"));
|
|
193
|
+
// r31c F2: `--all` used to dump 5–14KB with no "use --section next time"
|
|
194
|
+
// hint (that tip lived only on the index face / cli.ts). Append one line so
|
|
195
|
+
// the expensive face teaches the cheap face. Index blurbs stay ≤40 chars.
|
|
196
|
+
return { ...entry, content: `${content.trimEnd()}${HELP_ALL_SECTION_HINT}` };
|
|
111
197
|
}
|
|
112
198
|
catch {
|
|
113
199
|
return undefined;
|
|
@@ -210,7 +296,9 @@ export function readSkillSection(name, requested) {
|
|
|
210
296
|
*/
|
|
211
297
|
/** First meaningful body line of a section: skip headings/blank/fence/list markers. */
|
|
212
298
|
function sectionBlurb(body) {
|
|
213
|
-
|
|
299
|
+
// r31c F2: index blurbs must stay ≤40 chars so a bare `omc help <topic>`
|
|
300
|
+
// is enough to pick the right section (probe had to open --all / second round).
|
|
301
|
+
const limit = 40;
|
|
214
302
|
// sliceSectionText includes the section's own heading line — skip it.
|
|
215
303
|
for (const raw of body.split(/\r?\n/).slice(1)) {
|
|
216
304
|
const line = raw.trim();
|
package/dist/tools.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
import { z } from "zod";
|
|
1
3
|
import { loadCoverageManifest } from "./coverage.js";
|
|
2
4
|
import { makeToolContext } from "./context.js";
|
|
3
5
|
import { registerMetaLane } from "./lanes/meta.js";
|
|
@@ -23,9 +25,18 @@ import { registerReportLane } from "./lanes/report.js";
|
|
|
23
25
|
import { registerApplyChangesLane } from "./lanes/apply-changes.js";
|
|
24
26
|
import { executionMetadataFor } from "./execution-metadata.js";
|
|
25
27
|
import { attachRefusalRecovery } from "./refusal-guidance.js";
|
|
26
|
-
import { ToolRefusalError } from "./tool-types.js";
|
|
28
|
+
import { defineTool, ToolRefusalError } from "./tool-types.js";
|
|
29
|
+
import { environmentArg } from "./args.js";
|
|
30
|
+
import { appendJournalEntry } from "./journal.js";
|
|
31
|
+
import { listRequestArtifacts } from "./write.js";
|
|
27
32
|
export { ToolRefusalError, ToolUncertainError } from "./tool-types.js";
|
|
28
33
|
export { makeToolContext };
|
|
34
|
+
/** Compound package writes that need an honest non-rollbackable journal line (r30a F9/F11). */
|
|
35
|
+
const COMPOUND_JOURNAL_TOOLS = new Set(["configure_model", "configure_form"]);
|
|
36
|
+
const COMPOUND_IRREVERSIBLE = Object.freeze({
|
|
37
|
+
configure_model: "复合写无单一逆操作,只记 journal(rollbackable:false);清理请 model-purge / 逐项回滚子步骤",
|
|
38
|
+
configure_form: "复合写无单一逆操作,只记 journal(rollbackable:false);表单无删除端点,请用 form.draft 覆盖",
|
|
39
|
+
});
|
|
29
40
|
/**
|
|
30
41
|
* Composition root: assemble the governed tool surface from one adapter
|
|
31
42
|
* module per configuration lane. The lane modules own their tool
|
|
@@ -58,11 +69,61 @@ export function buildToolRegistry() {
|
|
|
58
69
|
// LAST: apply_changes looks up sibling write tools at call time, so it must
|
|
59
70
|
// capture the fully-populated registry array.
|
|
60
71
|
registerApplyChangesLane(tools);
|
|
72
|
+
// r30a F4: read-only requestId lookup over local write artifacts (no new store).
|
|
73
|
+
registerRequestStatusTool(tools);
|
|
61
74
|
for (const tool of tools) {
|
|
62
75
|
const metadata = executionMetadataFor(tool);
|
|
63
76
|
if (metadata)
|
|
64
77
|
tool.executionMetadata = metadata;
|
|
65
78
|
}
|
|
79
|
+
// r30a F9/F11/F23: wrap compound package writes so they leave one honest
|
|
80
|
+
// journal line (rollbackable:false) + receipt.rollback.seq + irreversible hint.
|
|
81
|
+
for (const tool of tools) {
|
|
82
|
+
if (!COMPOUND_JOURNAL_TOOLS.has(tool.name))
|
|
83
|
+
continue;
|
|
84
|
+
const inner = tool.handler;
|
|
85
|
+
const toolName = tool.name;
|
|
86
|
+
tool.handler = async (ctx, args, self) => {
|
|
87
|
+
const result = await inner(ctx, args, self);
|
|
88
|
+
if (!result || typeof result !== "object")
|
|
89
|
+
return result;
|
|
90
|
+
const record = result;
|
|
91
|
+
if (record.status !== "ok" || record.deliveryState !== "DELIVERED")
|
|
92
|
+
return result;
|
|
93
|
+
if (record.rollback && typeof record.rollback === "object")
|
|
94
|
+
return result;
|
|
95
|
+
const environment = typeof args.environment === "string" && args.environment.length > 0
|
|
96
|
+
? args.environment
|
|
97
|
+
: "local";
|
|
98
|
+
let environmentFingerprint = "compound-write";
|
|
99
|
+
try {
|
|
100
|
+
const session = await ctx.sessions.session(args.environment);
|
|
101
|
+
environmentFingerprint = session.environmentFingerprint();
|
|
102
|
+
}
|
|
103
|
+
catch { /* journal still useful without fingerprint */ }
|
|
104
|
+
const code = typeof args.code === "string" ? args.code : typeof args.schemaCode === "string" ? args.schemaCode : toolName;
|
|
105
|
+
const steps = Array.isArray(record.steps) ? record.steps : undefined;
|
|
106
|
+
const entry = appendJournalEntry(ctx.artifactsDir, {
|
|
107
|
+
v: 1,
|
|
108
|
+
environment,
|
|
109
|
+
environmentFingerprint,
|
|
110
|
+
tool: toolName,
|
|
111
|
+
target: `${toolName}:${code}`,
|
|
112
|
+
beforeSha: null,
|
|
113
|
+
rollbackable: false,
|
|
114
|
+
note: "复合写无逆操作,只记录(不假装可回滚)",
|
|
115
|
+
...(steps !== undefined ? { steps: steps.map((step) => (step && typeof step === "object" ? { step: step.step, skipped: step.skipped } : step)) } : {}),
|
|
116
|
+
artifactFile: typeof record.detailFile === "string" ? record.detailFile : `${toolName}.compound.json`,
|
|
117
|
+
artifactId: typeof record.detailFile === "string" ? basename(record.detailFile) : `${toolName}-compound`,
|
|
118
|
+
artifactSha: typeof record.artifactFingerprint === "string" ? record.artifactFingerprint : typeof record.detailSha256 === "string" ? record.detailSha256 : "compound",
|
|
119
|
+
});
|
|
120
|
+
return {
|
|
121
|
+
...record,
|
|
122
|
+
rollback: { seq: entry.seq, rollbackable: false, hint: `omc rollback plan ${entry.seq}(--env ${entry.environment})→ irreversible/skipped` },
|
|
123
|
+
irreversible: COMPOUND_IRREVERSIBLE[toolName] ?? "复合写无逆操作,只记 journal",
|
|
124
|
+
};
|
|
125
|
+
};
|
|
126
|
+
}
|
|
66
127
|
// r19 ticket 06 — 拒绝即教学,收口在最深的一层:每个 handler 返回/抛出的
|
|
67
128
|
// refused 结果在这里统一附加 recovery(同源配方命令、正确形状、不要做什么)。
|
|
68
129
|
// 收口在 registry 而不是 CLI 出口,是因为 runToolCall、测试 harness、MCP
|
|
@@ -98,5 +159,56 @@ export function buildToolRegistry() {
|
|
|
98
159
|
}
|
|
99
160
|
return tools;
|
|
100
161
|
}
|
|
162
|
+
/** Local-only requestId readback (r30a F4) — exposes findCompleted's store. */
|
|
163
|
+
function registerRequestStatusTool(tools) {
|
|
164
|
+
tools.push(defineTool({
|
|
165
|
+
name: "request.status",
|
|
166
|
+
layer: "meta",
|
|
167
|
+
kind: "read",
|
|
168
|
+
description: "按 requestId 查本机写产物;uncertain 后确认,不查远端。",
|
|
169
|
+
inputSchema: {
|
|
170
|
+
environment: environmentArg,
|
|
171
|
+
requestId: z.string().min(1),
|
|
172
|
+
tool: z.string().optional(),
|
|
173
|
+
},
|
|
174
|
+
handler: async (ctx, args) => {
|
|
175
|
+
const requestId = String(args.requestId);
|
|
176
|
+
const environment = typeof args.environment === "string" && args.environment.length > 0 ? args.environment : undefined;
|
|
177
|
+
const tool = typeof args.tool === "string" && args.tool.length > 0 ? args.tool : undefined;
|
|
178
|
+
const hits = listRequestArtifacts(ctx.artifactsDir, requestId, { ...(tool ? { tool } : {}), ...(environment ? { environment } : {}) });
|
|
179
|
+
if (hits.length === 0) {
|
|
180
|
+
return {
|
|
181
|
+
status: "ok",
|
|
182
|
+
found: false,
|
|
183
|
+
requestId,
|
|
184
|
+
...(environment ? { environment } : {}),
|
|
185
|
+
hint: "本机无此 requestId;可按业务键 data.list/model.list 再确认",
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
const latest = hits[hits.length - 1];
|
|
189
|
+
const artifact = latest.artifact;
|
|
190
|
+
const performed = (artifact.performed ?? {});
|
|
191
|
+
return {
|
|
192
|
+
status: "ok",
|
|
193
|
+
found: true,
|
|
194
|
+
requestId,
|
|
195
|
+
tool: artifact.tool,
|
|
196
|
+
environment: artifact.environment,
|
|
197
|
+
deliveryState: artifact.deliveryState,
|
|
198
|
+
completedAt: artifact.recordedAt,
|
|
199
|
+
requestFingerprint: artifact.requestFingerprint,
|
|
200
|
+
result: {
|
|
201
|
+
...(typeof performed.bizObjectId === "string" ? { bizObjectId: performed.bizObjectId } : {}),
|
|
202
|
+
...(performed.skipped !== undefined ? { skipped: performed.skipped } : {}),
|
|
203
|
+
...(artifact.readback !== undefined ? { readback: artifact.readback } : {}),
|
|
204
|
+
performed,
|
|
205
|
+
},
|
|
206
|
+
artifactFile: latest.file,
|
|
207
|
+
matches: hits.length,
|
|
208
|
+
...(hits.length > 1 ? { hint: `同 id ${hits.length} 条;以上为最后一条` } : {}),
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
}));
|
|
212
|
+
}
|
|
101
213
|
export { loadCoverageManifest };
|
|
102
214
|
//# sourceMappingURL=tools.js.map
|
package/dist/wire.js
CHANGED
|
@@ -235,7 +235,19 @@ export async function wireCall(session, call) {
|
|
|
235
235
|
data: envelope.data ?? null,
|
|
236
236
|
readbackFingerprint: "",
|
|
237
237
|
};
|
|
238
|
-
|
|
238
|
+
// 2026-11-05(ticket-110 回归):**否定读不缓存**。
|
|
239
|
+
//
|
|
240
|
+
// `sheetGet` 对「还没有表单」的模型回 errcode 0 + data:null——这不是错误,
|
|
241
|
+
// 是被缓存的合法响应。于是同一会话里一旦有人读过「表单不存在」(例如
|
|
242
|
+
// `field.publish` 的 formBinding 咨询),后面 60s 内的所有 sheetGet 都命中
|
|
243
|
+
// 这份**空**缓存:设计器刚建好的表单 / 测试直接种进去的布局都读不到,
|
|
244
|
+
// `onlinejs.compose` 于是把既有 toolbar/template 当不存在,重建空骨架,
|
|
245
|
+
// 设计器写的控件属性(className 等)被永久覆盖。
|
|
246
|
+
//
|
|
247
|
+
// 负面结果(data 为 null / 空数组)恰恰是最容易在下一秒被写操作推翻的状态,
|
|
248
|
+
// 缓存它省不了往返,只会制造陈旧读。
|
|
249
|
+
const cacheableResult = result.data !== null && !(Array.isArray(result.data) && result.data.length === 0);
|
|
250
|
+
if (state && cacheableResult)
|
|
239
251
|
state.entries.set(key, { result: structuredClone(result), query: call.query ?? {}, storedAt: clock() });
|
|
240
252
|
return result;
|
|
241
253
|
}
|