@stalfh233/omc-cli 0.4.42 → 0.4.44
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/lanes/b2-registration.js +94 -4
- package/dist/lanes/dictionary.js +22 -1
- package/dist/lanes/field.js +2 -2
- package/dist/lanes/list-view.js +59 -4
- package/dist/lanes/workflow.js +19 -1
- package/dist/recipes/online-js.js +3 -3
- package/dist/rollback-spec.js +22 -0
- package/dist/rollback.js +37 -5
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +6 -0
|
@@ -148,6 +148,96 @@ function dbConnPoolCodeOf(serviceConfig) {
|
|
|
148
148
|
function isServiceAbsent(error) {
|
|
149
149
|
return error instanceof WireError && error.kind === "business" && error.errcode === 500001;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Rollback specs (round 17): BIZSERVICE faces split by the secret boundary —
|
|
153
|
+
* service.update replays its PLAINTEXT configJson as serviceConfig;
|
|
154
|
+
* method.update/method.delete carry CIPHERTEXT configJson (tool-encrypted,
|
|
155
|
+
* write face accepts only plaintext methodConfig) so config-carrying changes
|
|
156
|
+
* restore honestly as irreversible; name/parameter-only writes replay
|
|
157
|
+
* cleanly; method.create rolls back via method.delete. service.register /
|
|
158
|
+
* service.import / service.delete / dbconnpool.* / method.test stay OUT of
|
|
159
|
+
* the journal (batch+cipher or probe faces).
|
|
160
|
+
*/
|
|
161
|
+
function b2ServiceRollbackSpec(toolName) {
|
|
162
|
+
return {
|
|
163
|
+
target: (args) => (toolName.startsWith("service") ? `bizservice:${String(args.serviceCode ?? "")}` : `bizmethod:${String(args.serviceCode ?? "")}/${String(args.methodCode ?? "")}`),
|
|
164
|
+
read: async (session, args) => {
|
|
165
|
+
const serviceCode = String(args.serviceCode ?? "");
|
|
166
|
+
if (toolName === "service.update") {
|
|
167
|
+
const row = (await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode } })).data;
|
|
168
|
+
return row && Object.keys(row).length > 0 ? row : null;
|
|
169
|
+
}
|
|
170
|
+
const rows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode, filterFlag: "false" } })).data);
|
|
171
|
+
const row = rows.find((candidate) => String(candidate.code ?? "") === String(args.methodCode ?? ""));
|
|
172
|
+
if (!row)
|
|
173
|
+
return null;
|
|
174
|
+
const { configJson, ...rest } = row;
|
|
175
|
+
return { ...rest, hadConfig: typeof configJson === "string" && configJson.trim() !== "" };
|
|
176
|
+
},
|
|
177
|
+
inverse: ({ before, args }) => {
|
|
178
|
+
const serviceCode = String(args.serviceCode ?? "");
|
|
179
|
+
if (toolName === "service.update") {
|
|
180
|
+
if (before == null)
|
|
181
|
+
return { kind: "calls", calls: [], notes: ["服务写入前不存在(幂等跳过),无需回滚"] };
|
|
182
|
+
const row = before;
|
|
183
|
+
let serviceConfig;
|
|
184
|
+
if (typeof row.configJson === "string" && row.configJson.trim() !== "") {
|
|
185
|
+
try {
|
|
186
|
+
const parsed = JSON.parse(row.configJson);
|
|
187
|
+
if (parsed !== null && typeof parsed === "object")
|
|
188
|
+
serviceConfig = parsed;
|
|
189
|
+
}
|
|
190
|
+
catch {
|
|
191
|
+
/* unparsable config: replay without it and note */
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return {
|
|
195
|
+
kind: "calls",
|
|
196
|
+
calls: [{ tool: "service.update", args: { serviceCode, ...(row.name != null ? { name: String(row.name) } : {}), ...(row.description != null ? { description: String(row.description) } : {}), ...(serviceConfig !== undefined ? { serviceConfig } : {}) } }],
|
|
197
|
+
...(serviceConfig === undefined && typeof row.configJson === "string" && row.configJson.trim() !== "" ? { notes: ["before configJson 无法解析为对象,未随回滚回放——请人工核对服务配置"] } : {}),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
const methodCode = String(args.methodCode ?? "");
|
|
201
|
+
if (toolName === "method.create") {
|
|
202
|
+
if (before == null)
|
|
203
|
+
return { kind: "calls", calls: [{ tool: "method.delete", args: { serviceCode, methodCode } }] };
|
|
204
|
+
return { kind: "calls", calls: [], notes: ["方法写入前已存在(幂等跳过),无需回滚"] };
|
|
205
|
+
}
|
|
206
|
+
if (before == null)
|
|
207
|
+
return { kind: "calls", calls: [], notes: ["目标写入前不存在,无需回滚"] };
|
|
208
|
+
const row = before;
|
|
209
|
+
const parseParams = (value) => {
|
|
210
|
+
if (typeof value !== "string")
|
|
211
|
+
return value !== undefined ? value : undefined;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(value);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
return undefined;
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
if (toolName === "method.update") {
|
|
220
|
+
if (args.methodConfig !== undefined)
|
|
221
|
+
return { kind: "irreversible", reason: "该写携带 methodConfig(平台以密文存储,工具只接受明文结构化配置)——密文无法安全回放;请用原明文 methodConfig 重新 method.update,或人工重配" };
|
|
222
|
+
const inputParameters = parseParams(row.inputParametersJson);
|
|
223
|
+
const outputParameters = parseParams(row.outputParametersJson);
|
|
224
|
+
return {
|
|
225
|
+
kind: "calls",
|
|
226
|
+
calls: [{ tool: "method.update", args: { serviceCode, methodCode, ...(row.name != null ? { name: String(row.name) } : {}), ...(inputParameters !== undefined ? { methodInputParameters: inputParameters } : {}), ...(outputParameters !== undefined ? { methodOutputParameters: outputParameters } : {}) } }],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (row.hadConfig === true)
|
|
230
|
+
return { kind: "irreversible", reason: "被删除的方法携带密文 configJson——重建需要明文 methodConfig,密文无法安全回放;请用原配置重新 method.create" };
|
|
231
|
+
const inputParameters = parseParams(row.inputParametersJson);
|
|
232
|
+
const outputParameters = parseParams(row.outputParametersJson);
|
|
233
|
+
return {
|
|
234
|
+
kind: "calls",
|
|
235
|
+
calls: [{ tool: "method.create", args: { serviceCode, methodCode, name: String(row.name ?? methodCode), ...(inputParameters !== undefined ? { methodInputParameters: inputParameters } : {}), ...(outputParameters !== undefined ? { methodOutputParameters: outputParameters } : {}) } }],
|
|
236
|
+
notes: ["方法已重建;被规则/按钮引用处请回读核对"],
|
|
237
|
+
};
|
|
238
|
+
},
|
|
239
|
+
};
|
|
240
|
+
}
|
|
151
241
|
export function registerB2Lane(tools) {
|
|
152
242
|
tools.push(defineTool({
|
|
153
243
|
name: "service.get",
|
|
@@ -324,7 +414,7 @@ export function registerB2Lane(tools) {
|
|
|
324
414
|
return { performed: { errcode: performed.errcode }, readback: { methodPresent: after != null, nameMatches: args.name === undefined || after?.name === args.name, configMatches: methodConfigPlain === undefined || after?.configJson === methodConfigPlain, inputParameters: decodeJsonColumn(after?.inputParametersJson), outputParameters: decodeJsonColumn(after?.outputParametersJson) } };
|
|
325
415
|
}, (args) => {
|
|
326
416
|
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
327
|
-
});
|
|
417
|
+
}, undefined, b2ServiceRollbackSpec("method.update"));
|
|
328
418
|
writeTool(tools, "method.create", B2_LANE, B2_TOOLS["method.create"].scope, B2_TOOLS["method.create"].description, {
|
|
329
419
|
serviceCode: z.string().min(1).describe("已存在的业务服务编码(在其下新增方法)"),
|
|
330
420
|
methodCode: z.string().min(1),
|
|
@@ -371,7 +461,7 @@ export function registerB2Lane(tools) {
|
|
|
371
461
|
return { performed: { errcode: performed.errcode }, readback: { methodPresent: after != null, nameMatches: after?.name === args.name, configMatches: methodConfigPlain === undefined || after?.configJson === methodConfigPlain, inputParameters: decodeJsonColumn(after?.inputParametersJson), outputParameters: decodeJsonColumn(after?.outputParametersJson) } };
|
|
372
462
|
}, (args) => {
|
|
373
463
|
assertStructuredConfig(args.methodConfig, "methodConfig");
|
|
374
|
-
});
|
|
464
|
+
}, undefined, b2ServiceRollbackSpec("method.create"));
|
|
375
465
|
writeTool(tools, "method.delete", B2_LANE, B2_TOOLS["method.delete"].scope, B2_TOOLS["method.delete"].description, { serviceCode: z.string().min(1), methodCode: z.string().min(1) }, async (session, args) => {
|
|
376
466
|
// Id resolution mirrors service.delete: the delete endpoint keys on the
|
|
377
467
|
// row id (query, comma-separated on the platform face — this tool stays
|
|
@@ -389,7 +479,7 @@ export function registerB2Lane(tools) {
|
|
|
389
479
|
const afterRows = wireRows((await wireCall(session, { path: ENDPOINTS.bizServiceMethodList, query: { serviceCode: args.serviceCode, filterFlag: "false" } })).data);
|
|
390
480
|
const after = afterRows.find((row) => row.code === args.methodCode);
|
|
391
481
|
return { performed: { errcode: performed.errcode }, readback: { absent: after == null, ...(after ? { method: after } : {}) } };
|
|
392
|
-
});
|
|
482
|
+
}, undefined, undefined, b2ServiceRollbackSpec("method.delete"));
|
|
393
483
|
tools.push(defineTool({
|
|
394
484
|
name: "service.category.list",
|
|
395
485
|
layer: "low",
|
|
@@ -870,7 +960,7 @@ export function registerB2Lane(tools) {
|
|
|
870
960
|
// and therefore runs after the read-only id resolution — still before
|
|
871
961
|
// any update write (see the execute body).
|
|
872
962
|
assertStructuredConfig(args.serviceConfig, "serviceConfig");
|
|
873
|
-
});
|
|
963
|
+
}, undefined, b2ServiceRollbackSpec("service.update"));
|
|
874
964
|
writeTool(tools, "service.delete", B2_LANE, B2_TOOLS["service.delete"].scope, B2_TOOLS["service.delete"].description, { serviceCode: z.string().min(1) }, async (session, args) => {
|
|
875
965
|
const found = await wireCall(session, { path: ENDPOINTS.bizServiceGetByCode, query: { serviceCode: args.serviceCode } });
|
|
876
966
|
const id = found.data?.id;
|
package/dist/lanes/dictionary.js
CHANGED
|
@@ -30,7 +30,28 @@ function dictionaryRollbackSpec(toolName) {
|
|
|
30
30
|
return { kind: "irreversible", reason: "平台无字典删除端点;可回滚内容:对后续 dictionary.save 的 before 执行 plan/apply" };
|
|
31
31
|
const snapshot = before;
|
|
32
32
|
const h = snapshot.header;
|
|
33
|
-
|
|
33
|
+
// Wire rows store name_i18n as a JSON STRING and dictionaryType may
|
|
34
|
+
// come back as a string; the tool's input schema takes an OBJECT and
|
|
35
|
+
// the numeric literal — project before replaying (round-trip finding
|
|
36
|
+
// 2026-10-14: raw replay died as tool-input-invalid).
|
|
37
|
+
const projectI18n = (value, fallbackName) => {
|
|
38
|
+
if (value !== null && value !== undefined && typeof value === "object")
|
|
39
|
+
return value;
|
|
40
|
+
if (typeof value === "string") {
|
|
41
|
+
try {
|
|
42
|
+
const parsed = JSON.parse(value);
|
|
43
|
+
if (parsed !== null && typeof parsed === "object")
|
|
44
|
+
return parsed;
|
|
45
|
+
}
|
|
46
|
+
catch { /* fall through to the synthesized map */ }
|
|
47
|
+
}
|
|
48
|
+
return { "zh-CN": fallbackName, en: fallbackName };
|
|
49
|
+
};
|
|
50
|
+
const records = (Array.isArray(snapshot.records) ? snapshot.records : []).map((record) => {
|
|
51
|
+
const row = record;
|
|
52
|
+
return { ...row, name_i18n: projectI18n(row.name_i18n, row.name) };
|
|
53
|
+
});
|
|
54
|
+
return { kind: "calls", calls: [{ tool: "dictionary.save", args: { id: h.id ?? undefined, code: h.code, name: h.name, dictionaryType: Number(h.dictionaryType ?? 0), ...(h.classificationId ? { classificationId: h.classificationId } : {}), records } }] };
|
|
34
55
|
}
|
|
35
56
|
if (before == null)
|
|
36
57
|
return { kind: "calls", calls: [] };
|
package/dist/lanes/field.js
CHANGED
|
@@ -36,8 +36,8 @@ function fieldRollbackSpec(toolName) {
|
|
|
36
36
|
const row = before;
|
|
37
37
|
const options = row.options !== undefined ? decodeJsonColumn(row.options, JSON_COLUMN_PASSTHROUGH) : undefined;
|
|
38
38
|
if (toolName === "field.delete")
|
|
39
|
-
return { kind: "calls", calls: [{ tool: "field.create", args: { schemaCode, code, name: String(row.name ?? code), propertyType: String(row.propertyType ?? row.type), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode
|
|
40
|
-
return { kind: "calls", calls: [{ tool: "field.update", args: { schemaCode, code, ...(row.name !== undefined ? { name: row.name } : {}), ...(row.name_i18n
|
|
39
|
+
return { kind: "calls", calls: [{ tool: "field.create", args: { schemaCode, code, name: String(row.name ?? code), propertyType: String(row.propertyType ?? row.type), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode != null ? { relativeCode: row.relativeCode } : {}), ...(row.subFields !== undefined ? { subFields: row.subFields } : {}) } }] };
|
|
40
|
+
return { kind: "calls", calls: [{ tool: "field.update", args: { schemaCode, code, ...(row.name !== undefined ? { name: row.name } : {}), ...(row.name_i18n != null ? { name_i18n: row.name_i18n } : {}), ...(options !== undefined ? { options } : {}), ...(row.choices !== undefined ? { choices: row.choices } : {}), ...(row.relativeCode != null ? { relativeCode: row.relativeCode } : {}), ...(row.subFields !== undefined ? { subFields: row.subFields } : {}), ...(row.propertyEmpty !== undefined ? { propertyEmpty: row.propertyEmpty } : {}) } }] };
|
|
41
41
|
},
|
|
42
42
|
};
|
|
43
43
|
}
|
package/dist/lanes/list-view.js
CHANGED
|
@@ -6,6 +6,7 @@ import { writeTool } from "../write.js";
|
|
|
6
6
|
import { encodeJsonColumn } from "../json-column.js";
|
|
7
7
|
import { defineTool, ToolRefusalError } from "../tool-types.js";
|
|
8
8
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
9
|
+
import { QUERY_ACTION_TYPE } from "omc-core/list-view-wire";
|
|
9
10
|
import { resolveModelTreeNode } from "./model.js";
|
|
10
11
|
import { SYSTEM_FIELD_CODES } from "../field-families.js";
|
|
11
12
|
import { LIST_VIEW_LANE_CONTRACT } from "./list-view-contract.js";
|
|
@@ -263,13 +264,65 @@ export function registerListViewLane(tools) {
|
|
|
263
264
|
};
|
|
264
265
|
}),
|
|
265
266
|
}));
|
|
267
|
+
/** Reverse enum map: the wire stores numeric queryActionType, the tool arg takes the enum name. */
|
|
268
|
+
const QUERY_ACTION_TYPE_NAME = new Map(Object.entries(QUERY_ACTION_TYPE.index).map(([name, index]) => [index, name]));
|
|
269
|
+
/**
|
|
270
|
+
* Project the BEFORE view model back into listview.configure args, replaying
|
|
271
|
+
* ONLY the keys the original write touched (configure semantics: omitted key =
|
|
272
|
+
* untouched, so a faithful restore never needs to resend untouched faces).
|
|
273
|
+
* Live round-trip finding 2026-10-14: replaying the RAW row (numeric
|
|
274
|
+
* queryActionType, platform-injected action keys, empty column sets) failed
|
|
275
|
+
* the tool's own input schema.
|
|
276
|
+
*/
|
|
277
|
+
function projectBeforeModelToConfigureArgs(row, originalArgs, schemaCode, code) {
|
|
278
|
+
const notes = [];
|
|
279
|
+
const fieldCodes = readArray(row, "queryColumns").map((column) => String(column.propertyCode ?? "")).filter(Boolean);
|
|
280
|
+
if (fieldCodes.length === 0)
|
|
281
|
+
return { ok: false, reason: "before 视图无显式业务列(平台默认视图列集)——listview.configure 的 fieldCodes 至少 1 列,无法表达空列集;恢复平台默认列请人工在设计器重置视图" };
|
|
282
|
+
// A restore must RECLAIM the before column set as the authoritative whole:
|
|
283
|
+
// without replaceColumns the tool MERGES fieldCodes into the existing set
|
|
284
|
+
// and a column added by the rolled-back write survives the rollback.
|
|
285
|
+
const args = { schemaCode, code, fieldCodes, replaceColumns: true };
|
|
286
|
+
if (originalArgs.hideDefaultSystemFields !== undefined)
|
|
287
|
+
args.hideDefaultSystemFields = originalArgs.hideDefaultSystemFields;
|
|
288
|
+
if (originalArgs.sorts !== undefined)
|
|
289
|
+
args.sorts = row.querySorts ?? [];
|
|
290
|
+
if (originalArgs.conditions !== undefined)
|
|
291
|
+
args.conditions = row.queryConditions ?? [];
|
|
292
|
+
if (originalArgs.actions !== undefined)
|
|
293
|
+
notes.push("动作集不随回滚恢复:读面注入平台默认动作且 configure 读改写回存会累积(快照不稳定);如需恢复动作请按原 args 显式重配");
|
|
294
|
+
if (originalArgs.defaultActions !== undefined)
|
|
295
|
+
args.defaultActions = originalArgs.defaultActions;
|
|
296
|
+
if (originalArgs.presentation !== undefined)
|
|
297
|
+
args.presentation = row.queryPresentation;
|
|
298
|
+
if (originalArgs.options !== undefined)
|
|
299
|
+
args.options = parseConditionOptions(row.options);
|
|
300
|
+
if (originalArgs.presentationType !== undefined)
|
|
301
|
+
args.presentationType = row.queryPresentationType;
|
|
302
|
+
if (originalArgs.gantt !== undefined)
|
|
303
|
+
args.gantt = row.queryGanttModel;
|
|
304
|
+
if (originalArgs.queryLayer !== undefined)
|
|
305
|
+
args.queryLayer = row.queryLayer;
|
|
306
|
+
if (originalArgs.multiTableHeaderConfig !== undefined)
|
|
307
|
+
args.multiTableHeaderConfig = parseConditionOptions(row.multiTableHeaderConfig);
|
|
308
|
+
return { ok: true, args, notes };
|
|
309
|
+
}
|
|
266
310
|
const listviewRollbackSpec = (toolName) => ({
|
|
267
311
|
target: (args) => `listview:${String(args.schemaCode ?? "")}/${String(args.code ?? args.schemaCode ?? "")}`,
|
|
268
312
|
read: async (session, args) => {
|
|
269
313
|
const code = String(args.code ?? args.schemaCode);
|
|
270
314
|
const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode: String(args.schemaCode), code, clientType: "PC" } });
|
|
271
315
|
const model = (got.data ?? null);
|
|
272
|
-
|
|
316
|
+
if (!model || !model.id)
|
|
317
|
+
return null;
|
|
318
|
+
// STABLE snapshot projection: the read face injects platform default
|
|
319
|
+
// queryActions (BizQueryFacadeImpl) and the configure RMW writes them
|
|
320
|
+
// back, so the raw action list grows on every write and can never
|
|
321
|
+
// byte-compare. Columns project to the authorable identity (sortKey/id
|
|
322
|
+
// are re-minted by every configure). Actions are therefore OUT of the
|
|
323
|
+
// rollback snapshot (and out of the restore) by design.
|
|
324
|
+
const columns = (Array.isArray(model.queryColumns) ? model.queryColumns : []).map((column) => ({ propertyCode: column.propertyCode, name: column.name, visible: column.visible }));
|
|
325
|
+
return { ...model, queryColumns: columns, queryActions: undefined };
|
|
273
326
|
},
|
|
274
327
|
inverse: ({ before, args }) => {
|
|
275
328
|
const schemaCode = String(args.schemaCode);
|
|
@@ -279,10 +332,12 @@ export function registerListViewLane(tools) {
|
|
|
279
332
|
if (before == null)
|
|
280
333
|
return { kind: "calls", calls: [], notes: ["目标在写入前不存在,无法恢复"] };
|
|
281
334
|
const row = before;
|
|
282
|
-
const configure =
|
|
335
|
+
const configure = projectBeforeModelToConfigureArgs(row, args, schemaCode, code);
|
|
336
|
+
if (configure.ok === false)
|
|
337
|
+
return { kind: "irreversible", reason: configure.reason };
|
|
283
338
|
if (toolName === "listview.configure")
|
|
284
|
-
return { kind: "calls", calls: [{ tool: "listview.configure", args: configure }] };
|
|
285
|
-
return { kind: "calls", calls: [{ tool: "listview.create", args: { schemaCode, code, name: String(row.name ?? code) } }, { tool: "listview.configure", args: configure }], notes: ["删除后重建视图,平台 id 不保证一致"] };
|
|
339
|
+
return { kind: "calls", calls: [{ tool: "listview.configure", args: configure.args }], ...(configure.notes.length > 0 ? { notes: configure.notes } : {}) };
|
|
340
|
+
return { kind: "calls", calls: [{ tool: "listview.create", args: { schemaCode, code, name: String(row.name ?? code) } }, { tool: "listview.configure", args: configure.args }], notes: ["删除后重建视图,平台 id 不保证一致", ...configure.notes] };
|
|
286
341
|
},
|
|
287
342
|
});
|
|
288
343
|
tools.push(defineTool({
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -967,6 +967,23 @@ async function backfillDesignerDefaults(session, schemaCode, body, callerTemplat
|
|
|
967
967
|
}
|
|
968
968
|
}
|
|
969
969
|
}
|
|
970
|
+
/**
|
|
971
|
+
* Rename pairs surfaced in the readback (live r51 2026-10-14): a rename mints
|
|
972
|
+
* `<original>_<n>` and DROPS the original code — re-sending the OLD code then
|
|
973
|
+
* reads as a brand-new node and APPENDS it, silently growing the flow. The
|
|
974
|
+
* readback names the pairs so the caller switches to the new code.
|
|
975
|
+
*/
|
|
976
|
+
function renamedActivityPairs(template, data) {
|
|
977
|
+
const callerCodes = new Set((callerActivities(template) ?? []).map((row) => activityCodeOf(row)).filter((code) => code !== ""));
|
|
978
|
+
const bodyCodes = new Set((Array.isArray(data.activities) ? data.activities : []).map((row) => String(row.activityCode ?? "")));
|
|
979
|
+
const pairs = [];
|
|
980
|
+
for (const code of bodyCodes) {
|
|
981
|
+
const match = /^(.+)_\d+$/.exec(code);
|
|
982
|
+
if (match && callerCodes.has(match[1]) && !bodyCodes.has(match[1]))
|
|
983
|
+
pairs.push({ from: match[1], to: code });
|
|
984
|
+
}
|
|
985
|
+
return pairs;
|
|
986
|
+
}
|
|
970
987
|
/**
|
|
971
988
|
* Live 2026-10-14: a workflow node's propertyPermissions ACCEPTS
|
|
972
989
|
* `required:true` on a CHILD_TABLE (子表) property at the wire level, but the
|
|
@@ -1633,7 +1650,8 @@ export function registerWorkflowLane(tools) {
|
|
|
1633
1650
|
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.update returned no draft" });
|
|
1634
1651
|
validateWorkflowRender(data, "workflow.update readback");
|
|
1635
1652
|
const participantWarnings = participantReferenceWarnings(data);
|
|
1636
|
-
|
|
1653
|
+
const renamedActivities = renamedActivityPairs(template, data);
|
|
1654
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : 0, templateType: data.templateType ?? null, ...(participantWarnings.length ? { participantWarnings } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "改名会铸新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
1637
1655
|
}, (args) => {
|
|
1638
1656
|
// Zero-wire refusal (ticket 77): fires in semanticGate BEFORE the
|
|
1639
1657
|
// governed pipeline — including the rollback before-capture read.
|
|
@@ -8,12 +8,12 @@ const onlinejsHighlightScript = Object.freeze({
|
|
|
8
8
|
environment,
|
|
9
9
|
schemaCode: model,
|
|
10
10
|
sheetCode: "<默认表单 code == 模型 code>",
|
|
11
|
-
|
|
11
|
+
replace: true,
|
|
12
12
|
source: "(function(form){ var amt = form.getControl('amountCode'); var party = form.getControl('targetCode'); function refresh(v){ party.options.style = Number(v) > 100000 ? 'color: red;' : ''; } form.on('onRendered', function(){ refresh(amt.getValue()); amt.valueChange.subscribe(function(change){ refresh(change.value); }); }); })",
|
|
13
13
|
},
|
|
14
14
|
preconditions: [
|
|
15
15
|
"控件编码先 model.inspect/field.list 确认(示例 amountCode/targetCode 换成真实字段 code)",
|
|
16
|
-
"完整 `(function(form){…})`
|
|
16
|
+
"完整 `(function(form){…})` 生命周期外壳必须配 `replace:true`(默认 INCREMENTAL merge 收外壳会被拒并指路 replace——r38 实证)",
|
|
17
17
|
],
|
|
18
18
|
conditionalRequirements: [
|
|
19
19
|
"事件模型:`form.on('onRendered', …)` 初始化 + `<控件>.valueChange.subscribe(function(change){ change.value })` 监听;改样式用 `control.options.style`",
|
|
@@ -23,7 +23,7 @@ const onlinejsHighlightScript = Object.freeze({
|
|
|
23
23
|
priorToolReferences: [{ tool: "form.get", args: { environment, schemaCode: model }, purpose: "确认表单已发布与控件编码" }],
|
|
24
24
|
dryRun: { supported: true, command: `omc call onlinejs.compose --env ${environment} --dry-run --args-file compose.json`, notes: "dry-run 校验外壳/合并模式,不写平台" },
|
|
25
25
|
expectedReadback: { readback: { deliveryState: "DELIVERED", published: true, scriptElementPresent: true, htmlRenderCompat: { draft: { ok: true }, published: { ok: true } } } },
|
|
26
|
-
refusalRecovery: [{ code: "
|
|
26
|
+
refusalRecovery: [{ code: "onlinejs-source-invalid", action: "完整外壳改 `replace:true`(六元素模板保留,仅整体替换 customScript)" }],
|
|
27
27
|
requiresEnvironmentDerivedIds: false,
|
|
28
28
|
});
|
|
29
29
|
export const ONLINEJS_METADATA = Object.freeze({
|
package/dist/rollback-spec.js
CHANGED
|
@@ -59,6 +59,28 @@ export function deepDiff(before, current, path = "", limit = 40, acc = []) {
|
|
|
59
59
|
walk(before, current, path);
|
|
60
60
|
return { changes: acc, total: acc.length };
|
|
61
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Drop null/undefined values from inverse-call args, recursively (live r55
|
|
64
|
+
* 2026-10-14): the wire rows carry explicit nulls (`relativeCode:null`) and
|
|
65
|
+
* `!== undefined` guards let them through — the target tool's input schema then
|
|
66
|
+
* rejects the replay as input-invalid (exit 10) and the restore stops. A null
|
|
67
|
+
* in the before row means "no value", and for every replayable config tool an
|
|
68
|
+
* ABSENT key is the same meaning with a schema the tool accepts.
|
|
69
|
+
*/
|
|
70
|
+
export function pruneNullish(value) {
|
|
71
|
+
if (Array.isArray(value))
|
|
72
|
+
return value.map(pruneNullish);
|
|
73
|
+
if (value !== null && typeof value === "object") {
|
|
74
|
+
const out = {};
|
|
75
|
+
for (const [key, item] of Object.entries(value)) {
|
|
76
|
+
if (item === null || item === undefined)
|
|
77
|
+
continue;
|
|
78
|
+
out[key] = pruneNullish(item);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
62
84
|
/**
|
|
63
85
|
* Canonical projection for restore verification: a replayed restore will
|
|
64
86
|
* NEVER be byte-equal to the before snapshot — the platform stamps
|
package/dist/rollback.js
CHANGED
|
@@ -5,13 +5,20 @@ import { SessionManager } from "./session-manager.js";
|
|
|
5
5
|
import { makeToolContext, buildToolRegistry } from "./tools.js";
|
|
6
6
|
import { runToolCall } from "./call.js";
|
|
7
7
|
import { readJournal, resolveJournalEntry, laterWritesOnTarget } from "./journal.js";
|
|
8
|
-
import { canonicalForVerify, deepDiff } from "./rollback-spec.js";
|
|
8
|
+
import { canonicalForVerify, deepDiff, pruneNullish } from "./rollback-spec.js";
|
|
9
9
|
import { sha256 } from "./support.js";
|
|
10
10
|
import { emitTransportResult } from "./command-transport.js";
|
|
11
11
|
/** Pure runtime assembly: an injected context (tests) or the default one. */
|
|
12
12
|
function runtime(context) {
|
|
13
13
|
return context ?? makeToolContext({ sessions: new SessionManager(new ConfigStore(defaultPaths())) });
|
|
14
14
|
}
|
|
15
|
+
/** Nullish-prune every call arg set in an inverse plan (see pruneNullish). */
|
|
16
|
+
function pruneInversePlan(plan) {
|
|
17
|
+
const pruneCalls = (calls) => calls.map((call) => ({ tool: call.tool, args: pruneNullish(call.args) }));
|
|
18
|
+
if (plan.kind === "calls")
|
|
19
|
+
return { ...plan, calls: pruneCalls(plan.calls) };
|
|
20
|
+
return plan.partial ? { ...plan, partial: pruneCalls(plan.partial) } : plan;
|
|
21
|
+
}
|
|
15
22
|
function resolvedEnvironment(environment) {
|
|
16
23
|
return new ConfigStore(defaultPaths()).resolveEnvironment(environment).name;
|
|
17
24
|
}
|
|
@@ -94,17 +101,31 @@ async function buildPlan(ref, environmentOption, context) {
|
|
|
94
101
|
currentFingerprint,
|
|
95
102
|
diff: deepDiff(before, current),
|
|
96
103
|
laterWrites,
|
|
97
|
-
|
|
104
|
+
// Every inverse call's args are nullish-pruned centrally: wire-row nulls
|
|
105
|
+
// must never reach a tool's input schema (r55 field.update relativeCode:null).
|
|
106
|
+
inverse: pruneInversePlan(spec.inverse({ before, args: artifactArgs, readback: (artifact.readback ?? {}) })),
|
|
98
107
|
},
|
|
99
108
|
};
|
|
100
109
|
}
|
|
101
110
|
export async function planRollback(ref, options, context) {
|
|
102
111
|
const loaded = await buildPlan(ref, options.environment, context);
|
|
103
112
|
const { entry, plan } = loaded;
|
|
113
|
+
// Preview values are for READING — full before templates run to tens of KB
|
|
114
|
+
// and used to blow the envelope budget (live r51: plan had to be fetched
|
|
115
|
+
// back from an artifact). Truncate previews; `apply` executes the full
|
|
116
|
+
// parameters regardless.
|
|
117
|
+
const preview = (value) => {
|
|
118
|
+
if (value === undefined || value === null)
|
|
119
|
+
return value;
|
|
120
|
+
const text = typeof value === "string" ? value : JSON.stringify(value);
|
|
121
|
+
return text !== undefined && text.length > 200 ? `${text.slice(0, 200)}…(省略 ${text.length - 200}B,apply 用完整参数)` : value;
|
|
122
|
+
};
|
|
123
|
+
const script = plan.inverse.kind === "calls" ? plan.inverse.calls.map((call) => JSON.stringify({ tool: call.tool, args: { ...call.args, environment: entry.environment } })) : [];
|
|
124
|
+
const scriptOmitted = script.join("").length > 6000;
|
|
104
125
|
return {
|
|
105
126
|
status: "ok",
|
|
106
127
|
rollbackOf: { seq: entry.seq, tool: entry.tool, target: entry.target, ts: entry.ts },
|
|
107
|
-
diff: { changedPaths: plan.diff.changes, total: plan.diff.total },
|
|
128
|
+
diff: { changedPaths: plan.diff.changes.map((change) => ({ path: change.path, before: preview(change.before), current: preview(change.current) })), total: plan.diff.total },
|
|
108
129
|
...(plan.laterWrites.length > 0
|
|
109
130
|
? { 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 })) }
|
|
110
131
|
: {}),
|
|
@@ -113,7 +134,9 @@ export async function planRollback(ref, options, context) {
|
|
|
113
134
|
kind: "calls",
|
|
114
135
|
count: plan.inverse.calls.length,
|
|
115
136
|
...(plan.inverse.notes ? { notes: plan.inverse.notes } : {}),
|
|
116
|
-
|
|
137
|
+
...(scriptOmitted
|
|
138
|
+
? { scriptOmitted: true, scriptHint: "恢复调用参数较大(含完整 before 模板)已省略——直接 omc rollback apply <seq> 执行即用完整参数" }
|
|
139
|
+
: { script }),
|
|
117
140
|
}
|
|
118
141
|
: { kind: "irreversible", reason: plan.inverse.reason, ...(plan.inverse.partial ? { partial: plan.inverse.partial } : {}) },
|
|
119
142
|
hint: "预览不写入;确认后 omc rollback apply <seq>(恢复走完整写闸门,回滚本身也会记 journal,可再回滚)",
|
|
@@ -137,7 +160,16 @@ export async function applyRollback(ref, options, context) {
|
|
|
137
160
|
context: ctx,
|
|
138
161
|
print: () => undefined,
|
|
139
162
|
});
|
|
140
|
-
results.push({
|
|
163
|
+
results.push({
|
|
164
|
+
tool: call.tool,
|
|
165
|
+
exitCode,
|
|
166
|
+
status: result.status,
|
|
167
|
+
...(result.deliveryState ? { deliveryState: result.deliveryState } : {}),
|
|
168
|
+
...(result.summary ? { summary: result.summary } : {}),
|
|
169
|
+
// r55: exit-code-only results hid the reason a restore call failed
|
|
170
|
+
// (input-invalid showed as a bare "status=error"). Surface the shape.
|
|
171
|
+
...(exitCode === 0 ? {} : { blocker: result.blocker ?? null, message: result.message ?? null, ...(result.issues !== undefined ? { issues: result.issues } : {}), ...(result.diagnostic !== undefined ? { diagnostic: result.diagnostic } : {}) }),
|
|
172
|
+
});
|
|
141
173
|
if (exitCode !== 0)
|
|
142
174
|
return { status: "stopped", rollbackOf: { seq: entry.seq, tool: entry.tool }, step: index + 1, results, message: `恢复调用 ${call.tool} 未成功(status=${result.status});已执行的调用已各自记 journal,可对已完成步骤再 plan` };
|
|
143
175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stalfh233/omc-cli",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.44",
|
|
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.44",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.44",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|
|
@@ -42,6 +42,7 @@ node "$CLI" help omc-five-piece-flow
|
|
|
42
42
|
|
|
43
43
|
## 模型与字段
|
|
44
44
|
|
|
45
|
+
- **code 长度平台硬限**:模型 code ≤26 字符、字段 code ≤28(`business-code-invalid` 出门校验;长业务名先起短 code,长名放 name)。
|
|
45
46
|
- 模型层字段默认可空;必填、只读、可见性在表单和流程节点配置。
|
|
46
47
|
- `propertyEmpty` 默认 `false`,不要把业务必填硬编码在数据模型。
|
|
47
48
|
- 关联单选优先读目标模型已发布字段,明确显示字段;无映射不要写空数组字符串。
|
|
@@ -145,6 +146,11 @@ rules:
|
|
|
145
146
|
- 配置默认视图后即可用 `data.list`;否则部分 8.6.26 读面业务列可能为 null,单行用 `data.load` 核对。
|
|
146
147
|
- 读回视图用 `listview.get`(默认回列/排序/条件/动作紧凑清单)与 `listview.list`。
|
|
147
148
|
|
|
149
|
+
## 流程
|
|
150
|
+
|
|
151
|
+
- **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
|
|
152
|
+
- 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
|
|
153
|
+
|
|
148
154
|
## 流程字段权限
|
|
149
155
|
|
|
150
156
|
新增字段后,必须重新检查每个参与节点的 `propertyPermissions`:
|