@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;
@@ -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
- return { kind: "calls", calls: [{ tool: "dictionary.save", args: { code: h.code, name: h.name, ...(h.classificationId ? { classificationId: h.classificationId } : {}), records: snapshot.records } }] };
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: [] };
@@ -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
- return model && model.id ? model : null;
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 = { schemaCode, code, fieldCodes: readArray(row, "queryColumns").map((column) => String(column.propertyCode ?? "")).filter(Boolean), ...(row.querySorts !== undefined ? { sorts: row.querySorts } : {}), ...(row.queryConditions !== undefined ? { conditions: row.queryConditions } : {}), ...(row.queryActions !== undefined ? { actions: row.queryActions, replaceActions: true } : {}), ...(row.queryPresentation !== undefined ? { presentation: row.queryPresentation } : {}), ...(row.options !== undefined ? { options: parseConditionOptions(row.options) } : {}), ...(row.queryPresentationType !== undefined ? { presentationType: row.queryPresentationType } : {}), ...(row.queryGanttModel !== undefined ? { gantt: row.queryGanttModel } : {}), ...(row.queryLayer !== undefined ? { queryLayer: row.queryLayer } : {}), ...(row.multiTableHeaderConfig !== undefined ? { multiTableHeaderConfig: parseConditionOptions(row.multiTableHeaderConfig) } : {}) };
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.43",
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.43",
53
+ "@stalfh233/omc-fake-platform": "0.4.44",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.43",
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`: