@stalfh233/omc-cli 0.4.43 → 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.
|
@@ -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/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/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",
|
|
@@ -146,6 +146,11 @@ rules:
|
|
|
146
146
|
- 配置默认视图后即可用 `data.list`;否则部分 8.6.26 读面业务列可能为 null,单行用 `data.load` 核对。
|
|
147
147
|
- 读回视图用 `listview.get`(默认回列/排序/条件/动作紧凑清单)与 `listview.list`。
|
|
148
148
|
|
|
149
|
+
## 流程
|
|
150
|
+
|
|
151
|
+
- **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
|
|
152
|
+
- 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
|
|
153
|
+
|
|
149
154
|
## 流程字段权限
|
|
150
155
|
|
|
151
156
|
新增字段后,必须重新检查每个参与节点的 `propertyPermissions`:
|