@stalfh233/omc-cli 0.4.85 → 0.4.86

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/README.md CHANGED
@@ -130,10 +130,11 @@ omc call discover_environment --env <env> --args '{}' # 环境就绪(真实
130
130
  - **intent**:`partial` 表示该能力可声明式(`omc intent`)覆盖一部分——通常 create/update;
131
131
  `none` 表示只走 `omc call`。删除类、方法面、分类/连接池等仍走 `omc call` 逃生舱。
132
132
 
133
- ## 近期能力(0.4.74 → 0.4.84)
133
+ ## 近期能力(0.4.74 → 0.4.86)
134
134
 
135
135
  | 版本 | 新增 / 修复 |
136
136
  |---|---|
137
+ | 0.4.86 | **r13 真机探针修复**:`omc next` 增加 **composer 路由**(含建模型+字段/审批流/计算/条件规则/表单默认值 → 直接给出对应一条命令;读面仍走 ladder,条件分支流程仍走 recipe)——r13 读了文档里那行 `model-scaffold` 却仍调 26 次 `field.create`,说明「写进文档」不等于「能被发现」。`data.submit` 审批提交**写前校验身份**:非本人在办的待办直接回 `submit-workitem-not-mine`(带待办持有者 + 三条出路),不再让平台回一个 `40005 权限不足`让 agent 猜 |
137
138
  | 0.4.84 | **回合数优化(从省字节转向省往返)**:新增 `omc model-scaffold`(一条命令建模型+N 字段+子表+发布+表单,先 dry-run 预检零写入)与 `omc call <tool> --args-list`(同工具 N 次、逐条执行、回汇总)。修三个真 bug:键名提示自相矛盾(`workflow.delete` 两边互指)、`--dry-run` 与真写对 NUMERICAL 判定不一致(两套真相)、`runToolCall` 的 dry-run 被判失败 |
138
139
  | 0.4.83 | **r12 真机探针修正(最重要:文档纠错)**:字段计算规则(`datarule` type2)**由服务端在 `data.submit`/`data.update` 时计算并回填,且覆盖传入值**——此前 skills/describe 六处写的「API 不回填、服务端聚合需二开」是错的(旧证据只覆盖 `FORMULA(100)` 字段)。另:`omc compute` 新增 `--where "<子列> = <值>"`(编译 COUNTIF/SUMIF)并**拒绝无条件子表聚合**(`--allow-unfiltered` 显式放行);`model.delete` 把「孤儿模型」死锁变成一次明确回答——先复查读缓存,仍存在则回 `model-orphaned-record` 并给平台侧出路 |
139
140
  | 0.4.82 | **流程发起权限(40009)**:新增 `workflow.permissions` / `workflow.addPermission` / `workflow.removePermission`(`/workflow/list_permission`、`create_permission`、`delete_permission`,unitType 线上为枚举序号 1=部门 2=角色 3=用户)。`data.submit`/`data.save+workflowCode` 的 40009 现在直接回 `workflow-start-permission-denied`:带上该流程的权限行现状、两种成因(无权限行 / 发起人部门记录已失效)与可粘贴的修复命令,不再是「流程发起权限不足」五个字 |
@@ -321,7 +321,68 @@ function orderedCapabilities(ids, index) {
321
321
  * runtime/exact patches), not a second main path.
322
322
  */
323
323
  const WHOLE_PACKAGE_CAPABILITIES = ["model", "field", "form", "listview", "datarule", "businessrule", "workflow", "onlinejs"];
324
- /** A whole-package ask (build OR modify) is a declarative-intent candidate. */
324
+ const COMPOSER_ROUTES = Object.freeze([
325
+ {
326
+ id: "model-scaffold",
327
+ command: "omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON数组>' --apply",
328
+ usage: "omc model-scaffold --help",
329
+ summary: "一条命令建模型:模型 → N 字段(subFields 子表+子列)→ 发布字段 → 发布表单;apply 前逐字段 dry-run 预检、零写入",
330
+ // (build verb) × (model noun) × (field/child-table noun)
331
+ groups: [["建", "创建", "新建", "搭建", "从零"], ["模型"], ["字段", "子表", "列", "属性", "明细"]],
332
+ avoid: ["嵌套子表", "子表套子表"],
333
+ topics: ["omc-business-configuration"],
334
+ },
335
+ {
336
+ id: "approval-flow",
337
+ command: "omc approval-flow --schema <模型code> --steps '<节点名>@role:<角色code>' --apply",
338
+ usage: "omc approval-flow --help",
339
+ summary: "线性审批流一条命令:建流程 → 模板+字段只读权限 → 发布(多级用重复 --steps)",
340
+ groups: [["审批", "流程"], ["建", "创建", "新建", "配置", "加", "搭"]],
341
+ // Branching / countersign shapes are NOT this composer's business.
342
+ avoid: ["条件分支", "分支", "会签", "并行", "或签", "网关", "子流程", "动态审批人"],
343
+ topics: ["omc-five-piece-flow"],
344
+ },
345
+ {
346
+ id: "compute",
347
+ command: "omc compute --schema <模型code> --target <目标字段> --formula '<公式>' [--where '<子列> = <值>'] --apply",
348
+ usage: "omc compute --help",
349
+ summary: "字段计算规则一条命令(datarule type2):普通表达式、子表求和、条件计数 COUNTIF/SUMIF;服务端 submit/update 时回填",
350
+ groups: [["计算", "统计", "汇总", "求和", "聚合", "计数", "合计"], ["字段", "子表", "列", "回主表", "写入"]],
351
+ avoid: [],
352
+ topics: ["omc-write-hazards"],
353
+ },
354
+ {
355
+ id: "rule-when",
356
+ command: "omc rule-when --schema <模型code> --rule <规则code> --when '<条件>' --then '<动作>'",
357
+ usage: "omc rule-when --help",
358
+ summary: "条件→动作规则一条命令(BRANCH + 赋值节点拼装),替代手拼 nodeGraph",
359
+ groups: [["规则", "条件"], ["如果", "当", "满足", "时"], ["更新", "赋值", "改为", "设为", "动作"]],
360
+ avoid: [],
361
+ topics: ["omc-business-configuration"],
362
+ },
363
+ {
364
+ id: "form-default",
365
+ command: "omc form-default --schema <模型code> --field <字段code> --value <值>",
366
+ usage: "omc form-default --help",
367
+ summary: "表单字段默认值一条命令(建表单/复用已有表单 + 写默认值),无需手改 options",
368
+ groups: [["默认值", "默认"], ["表单", "字段"]],
369
+ avoid: [],
370
+ topics: ["omc-business-configuration"],
371
+ },
372
+ ]);
373
+ function matchComposer(goal, mode) {
374
+ // A composer is a WRITE path: reads and scouting asks must keep their ladder.
375
+ if (mode !== "build" && mode !== "modify")
376
+ return undefined;
377
+ for (const composer of COMPOSER_ROUTES) {
378
+ if (composer.avoid?.some((phrase) => goal.includes(phrase)))
379
+ continue;
380
+ if (!composer.groups.every((group) => group.some((cue) => goal.includes(cue))))
381
+ continue;
382
+ return composer;
383
+ }
384
+ return undefined;
385
+ }
325
386
  function isWholePackageWrite(mode, goal, capabilityCount) {
326
387
  return (mode === "build" || mode === "modify") && looksWholePackage(goal, capabilityCount);
327
388
  }
@@ -400,6 +461,33 @@ export function goalRoute(registry, goal) {
400
461
  route: { write: false, readback: false, firstCommands: ["omc capabilities", "omc capability <id>", "omc help omc-capability-scouting"] },
401
462
  };
402
463
  }
464
+ // Composer before the capability solver: a recurring write SHAPE has a single
465
+ // command that does the job with preflight + idempotency, which beats a ladder
466
+ // of atomic tools even when the ladder also matches (r13: 26 field.create).
467
+ const composer = matchComposer(goal, mode);
468
+ if (composer) {
469
+ const topics = (composer.topics ?? []).filter((topic) => availableTopics.has(topic));
470
+ return {
471
+ status: "ok",
472
+ goal,
473
+ confident: true,
474
+ mode,
475
+ write: true,
476
+ match: { kind: "composer", ref: composer.id, title: composer.summary, command: composer.command },
477
+ route: {
478
+ composer: composer.id,
479
+ command: composer.command,
480
+ summary: composer.summary,
481
+ // The composer IS the first step; readiness still comes first because
482
+ // the command needs a bound environment to run.
483
+ firstCommands: [DISCOVERY_AUTHORITY.readiness, composer.command, composer.usage],
484
+ write: true,
485
+ readback: true,
486
+ ...(topics.length > 0 ? { readCommands: topics.map((topic) => `omc help ${topic} --section "<节>"`), readHint: "只在 composer 的 --help 不够用时再读这些 topic 的对应节" } : {}),
487
+ },
488
+ ...(topics.length > 0 ? { topics } : {}),
489
+ };
490
+ }
403
491
  const scores = [...capabilityScores(registry, goal).entries()].sort((left, right) => right[1] - left[1]);
404
492
  const fallback = {
405
493
  status: "ok",
@@ -8,7 +8,7 @@ import { ToolInputError, ToolRefusalError } from "../tool-types.js";
8
8
  import { writeTool } from "../write.js";
9
9
  import { defineTool } from "../tool-types.js";
10
10
  import { wireCall, wireRows, WireError } from "../wire.js";
11
- import { CHILD_TABLE_PROPERTY_TYPE, runtimeWriteWithAppMapping } from "../runtime-support.js";
11
+ import { CHILD_TABLE_PROPERTY_TYPE, diagnoseApprovalSubmitIdentity, runtimeWriteWithAppMapping } from "../runtime-support.js";
12
12
  import { getReplayToken, searchWorkItems, wireString } from "../workflow-support.js";
13
13
  import { ENGINE_OWNED_KEYS, RUNTIME_DATA_LANE_CONTRACT, RUNTIME_DATA_WIRE, runtimeFormDataArg, } from "./runtime-data-contract.js";
14
14
  import { SYSTEM_FIELD_CODES } from "../field-families.js";
@@ -1083,6 +1083,11 @@ export function registerRuntimeDataLane(tools) {
1083
1083
  payload.runMode = args.runMode;
1084
1084
  if (typeof args.isSimulative === "boolean")
1085
1085
  payload.isSimulative = args.isSimulative;
1086
+ // Approval submits are resolved against the LOGGED-IN user (r13: six
1087
+ // blind 40005s). Name the boundary before the write instead of letting the
1088
+ // platform answer "权限不足", which reads like a misconfiguration.
1089
+ if (hasWorkItem)
1090
+ await diagnoseApprovalSubmitIdentity(session, { workflowInstanceId: args.workflowInstanceId, workItemId: args.workItemId });
1086
1091
  const performed = await runtimeWriteWithAppMapping(session, args.schemaCode, 50000, "data.submit 运行时提交稳定失败(50000)", () => wireCall(session, {
1087
1092
  path: ENDPOINTS.runtimeFormSubmit,
1088
1093
  method: "POST",
@@ -2,6 +2,7 @@ import { wireCall, wireRows, WireError } from "./wire.js";
2
2
  import { ENDPOINTS } from "./endpoints.js";
3
3
  import { ToolRefusalError } from "./tool-types.js";
4
4
  import { resolveModelTreeNode } from "./lanes/model.js";
5
+ import { listInstanceActivities, wireString } from "./workflow-support.js";
5
6
  /**
6
7
  * Ticket 78 — map the two same-root-cause platform failures of an
7
8
  * UNPUBLISHED application to the explicit `app-not-published` blocker:
@@ -222,4 +223,94 @@ async function resolvePublishedWorkflowCodeQuiet(session, schemaCode) {
222
223
  return undefined;
223
224
  }
224
225
  }
226
+ /**
227
+ * 40005 权限不足 on an APPROVAL submit — the caller is not a participant of the
228
+ * pending work item.
229
+ *
230
+ * r13 (2026-10-24, 1.1.25) could not verify "审批通过" at all: the todo existed,
231
+ * `workflow.instanceActivity` showed the node active with the right participant,
232
+ * and `data.submit` with `workItemId` + `agree:true` answered a bare
233
+ * `40005 权限不足`. Six attempts, plus two round trips spent trying to add an
234
+ * approval node as a workaround, before a control experiment (clearing the role
235
+ * membership so the participant fell back to the originator) proved the cause:
236
+ * **the platform submits as the logged-in user**, and OMC holds exactly one
237
+ * session, so an approval addressed to anyone else can never be driven from the
238
+ * CLI.
239
+ *
240
+ * This is a capability boundary, not a config defect — but a boundary the CLI
241
+ * must NAME, because "权限不足" reads like a permission misconfiguration. Fail
242
+ * open whenever the participant set is unknown (no active activity, empty
243
+ * participantors, or a read failure): a diagnosis must never invent a refusal.
244
+ */
245
+ export async function diagnoseApprovalSubmitIdentity(session, args) {
246
+ const instanceId = typeof args.workflowInstanceId === "string" ? args.workflowInstanceId : "";
247
+ if (!instanceId)
248
+ return;
249
+ let activities;
250
+ let me;
251
+ try {
252
+ activities = await listInstanceActivities(session, instanceId);
253
+ // `organization/user/info` needs an `id`; resolve "me" from the login
254
+ // identity (the username the token was issued for) through user/search.
255
+ const identity = (await session.login()).accountIdentity;
256
+ me = null;
257
+ if (identity !== undefined && identity.length > 0) {
258
+ const searchData = (await wireCall(session, {
259
+ path: ENDPOINTS.organizationUserSearch,
260
+ method: "POST",
261
+ body: { wd: identity, page: 0, size: 20 },
262
+ })).data;
263
+ // user/search answers a page envelope ({content:[…]}), not a bare array —
264
+ // `wireRows` refuses that shape (same decode as `org.user.search`).
265
+ const found = Array.isArray(searchData?.content) ? searchData.content : wireRows(searchData);
266
+ const exact = found.find((row) => [row.username, row.userId, row.name].some((value) => String(value ?? "") === identity)) ?? found[0];
267
+ if (exact) {
268
+ const id = wireString(exact.id) ?? wireString(exact.userId);
269
+ if (id)
270
+ me = (await wireCall(session, { path: ENDPOINTS.organizationUserInfo, query: { id } })).data;
271
+ if (!me && exact)
272
+ me = exact;
273
+ }
274
+ }
275
+ }
276
+ catch {
277
+ return; // Unreadable faces: let the platform answer.
278
+ }
279
+ const myId = me ? wireString(me.id) ?? wireString(me.userId) : undefined;
280
+ if (!myId)
281
+ return;
282
+ const myName = me ? wireString(me.name) ?? myId : myId;
283
+ // Only an activity that currently HOLDS tasks can be submitted against.
284
+ const active = activities.filter((row) => String(row.status ?? "") === "1");
285
+ if (active.length === 0)
286
+ return;
287
+ const participantIds = (row) => {
288
+ const list = Array.isArray(row.participantors) ? row.participantors : [];
289
+ return list.map((person) => wireString(person.id) ?? wireString(person.userId) ?? "").filter((id) => id.length > 0);
290
+ };
291
+ const known = active.filter((row) => participantIds(row).length > 0);
292
+ // Nobody publishes a participant list (auto/service tasks): stay silent.
293
+ if (known.length === 0)
294
+ return;
295
+ if (known.some((row) => participantIds(row).includes(myId)))
296
+ return;
297
+ const awaiting = known
298
+ .map((row) => ({ activityName: wireString(row.activityName) ?? null, activityCode: wireString(row.activityCode) ?? null, participants: participantIds(row) }))
299
+ .filter((row) => row.participants.length > 0);
300
+ throw new ToolRefusalError({
301
+ status: "refused",
302
+ blocker: "submit-workitem-not-mine",
303
+ workflowInstanceId: instanceId,
304
+ ...(typeof args.workItemId === "string" ? { workItemId: args.workItemId } : {}),
305
+ currentUser: { id: myId, name: myName },
306
+ awaiting,
307
+ message: `当前登录人 ${myName}(${myId})不是该实例待办节点的参与者,平台会以 **40005 权限不足** 拒绝这次提交。` +
308
+ `待办持有者:${awaiting.map((row) => `${row.activityName ?? row.activityCode ?? "?"} → ${row.participants.join("/")}`).join(";")}。` +
309
+ `**这是 OMC 的能力边界**:平台按登录态鉴权,而 OMC 一次只持有一个会话(一份 token 缓存),` +
310
+ `因此「非发起人身份的审批」无法在 CLI 内推进。可选做法:① 由该参与人自己在门户审批;` +
311
+ `② 用该参与人的账号另建一个环境(omc env add)再执行同一条 data.submit;` +
312
+ `③ 若只是为了验证流程能走通,把该节点参与人临时改为发起人(org.role.addUsers / workflow.update),验证后改回。` +
313
+ `不要重复盲试——同一个 workItemId 会一直回 40005。`,
314
+ });
315
+ }
225
316
  //# sourceMappingURL=runtime-support.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.85",
3
+ "version": "0.4.86",
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.85",
53
+ "@stalfh233/omc-fake-platform": "0.4.86",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.85",
55
+ "omc-core": "0.4.86",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",