@stalfh233/omc-cli 0.4.81 → 0.4.82

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.81)
133
+ ## 近期能力(0.4.74 → 0.4.82)
134
134
 
135
135
  | 版本 | 新增 / 修复 |
136
136
  |---|---|
137
+ | 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`:带上该流程的权限行现状、两种成因(无权限行 / 发起人部门记录已失效)与可粘贴的修复命令,不再是「流程发起权限不足」五个字 |
137
138
  | 0.4.81 | **r11 真机探针修复**:`rule.save` 对 `AGGREGATION` 规则节点**写前 fail-closed**(平台会存进去、运行时 50000,真实路径是 `omc compute`/datarule type2);`omc next "统计子表行数"` 路由补 `datarule` 阶梯与 `field-compute-subtable-sum` 配方;`org.user.search` 省略关键字时**按部门树列全员**(平台 `wd` 不支持空串,此前只能枚举 0–9);`omc artifact get --jq` 无值回键清单(不再 commander 报错);`usage` 的 `faces` 纳入 `--jq/--summary/--keys/--path-only/--extract`(此前读面投影完全不记账) |
138
139
  | 0.4.80 | **省 token 与用量账本**:`omc help <topic>` 默认只回节索引(约 1KB,含每节字节数),`--all` 才读整篇;`omc artifact get` 正文 >8KB 默认只回形状(`--full` 才整包);`omc usage` 默认只回汇总(`byTool`/`redundancy`/`estimatedTokens`),`--records` 才展开;`omc script` 的**每行工具调用单独记账**(`script-call` + `parentId`);未绑定环境的目录回落 `$OMC_CONFIG_HOME/usage/<cwd-hash>.jsonl` |
139
140
  | 0.4.79 | README 对齐 0.4.79 实际工具面(142 工具 / 16 能力) |
@@ -339,6 +339,9 @@
339
339
  "workflow.bindRule",
340
340
  "workflow.setButtons",
341
341
  "workflow.delete",
342
+ "workflow.permissions",
343
+ "workflow.addPermission",
344
+ "workflow.removePermission",
342
345
  "configure_workflow",
343
346
  "workflow.validate",
344
347
  "org.role.users",
@@ -365,6 +368,7 @@
365
368
  "scopes": [
366
369
  "greenfield workflow authoring",
367
370
  "workflow node form-button checkboxes",
371
+ "workflow start permission (who may originate)",
368
372
  "organization role member read (role/users)",
369
373
  "organization role member management scope read (role/get_ouscope)",
370
374
  "organization role group create (role/add_role_group)",
package/dist/endpoints.js CHANGED
@@ -152,6 +152,18 @@ export const ENDPOINTS = {
152
152
  workflowValidate: "/api/api/workflow/validate",
153
153
  workflowPublish: "/api/api/workflow/publish",
154
154
  workflowDelete: "/api/api/workflow/delete",
155
+ // 发起权限 (who may START the flow). Without a row here — or with a role/
156
+ // department row the caller is not in — `data.submit`/`data.save+workflowCode`
157
+ // answers 40009 流程发起权限不足 (WorkflowAspect.validateWorkflowPermission →
158
+ // WorkflowManagementFacadeImpl.getWorkflowTemplateHeadersBySchemaCode). Endpoints
159
+ // from WorkflowTemplateController (cloudpivot-engine 8.6.26; live 1.1.25):
160
+ // GET /workflow/list_permission?workflowCode= → [{id,name,unitType,fullName,corpId,code}]
161
+ // POST /workflow/create_permission body {workflowCode,unitId,unitType,corpId?}
162
+ // unitType is the ENUM INDEX on the wire: 1=部门 2=角色 3=用户 (UnitType @JsonValue)
163
+ // POST/DELETE /workflow/delete_permission?permissionId=
164
+ workflowListPermission: "/api/api/workflow/list_permission",
165
+ workflowCreatePermission: "/api/api/workflow/create_permission",
166
+ workflowDeletePermission: "/api/api/workflow/delete_permission",
155
167
  workflowSearchWorkItems: "/api/api/runtime/workflow/search_workitems",
156
168
  workflowGetBizObjectByWfInstanceId: "/api/api/runtime/workflow/getBizObjectByWfInstanceId",
157
169
  workflowListInstanceActivity: "/api/api/runtime/workflow/list_workflow_instance_activity",
@@ -699,7 +699,7 @@ export function registerRuntimeDataLane(tools) {
699
699
  ...(replayToken ? { replayToken } : {}),
700
700
  bizObject: { schemaCode: args.schemaCode, sheetCode, data: { ...formData, id: objectId, sequenceStatus: "DRAFT" } },
701
701
  },
702
- }), formData);
702
+ }), formData, args.workflowCode);
703
703
  const vo = (performed.data ?? {});
704
704
  const returnedId = typeof vo.objectId === "string" && vo.objectId ? vo.objectId : typeof vo.bizObjectId === "string" && vo.bizObjectId ? vo.bizObjectId : objectId;
705
705
  const workflowInstanceId = typeof vo.workflowInstanceId === "string" ? vo.workflowInstanceId : null;
@@ -1087,7 +1087,7 @@ export function registerRuntimeDataLane(tools) {
1087
1087
  path: ENDPOINTS.runtimeFormSubmit,
1088
1088
  method: "POST",
1089
1089
  body: payload,
1090
- }));
1090
+ }), undefined, hasWorkflowCode ? explicitWorkflowCode : undefined);
1091
1091
  const performedData = (performed.data ?? {});
1092
1092
  let readback = { formType };
1093
1093
  if (formType === "2") {
@@ -174,6 +174,18 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
174
174
  scope: WORKFLOW_SCOPE,
175
175
  description: "删除工作流模板(`/api/api/workflow/delete`),回读确认缺失。",
176
176
  }),
177
+ "workflow.permissions": Object.freeze({
178
+ scope: "workflow start permission (who may originate)",
179
+ description: "列出谁能发起该流程(`GET /api/api/workflow/list_permission?workflowCode=` → {id,name,unitType,fullName,corpId,code};1=部门 2=角色 3=用户)。**`data.submit` 报 40009 时先看这里**:PART_VISIABLE 且本表无命中行 ⇒ 除平台管理员(`h_perm_admin` 的 ADMIN/SYS_MNG,非登录 isAdmin)外无人能发起;发布流程不会自动建这一行。",
180
+ }),
181
+ "workflow.addPermission": Object.freeze({
182
+ scope: "workflow start permission (who may originate)",
183
+ description: "授予发起权限(`POST /api/api/workflow/create_permission`,body `{workflowCode,unitId,unitType,corpId}`;unitType 取枚举序号 1=部门 2=角色 3=用户)。unitId 用 org.department.list / org.role.list / org.user.search 取。重复授予按 402003 幂等跳过,写后回读确认。",
184
+ }),
185
+ "workflow.removePermission": Object.freeze({
186
+ scope: "workflow start permission (who may originate)",
187
+ description: "撤销发起权限(`POST /api/api/workflow/delete_permission?permissionId=`)。首选传 `permissionId`(`workflow.permissions` 的 id);也可传 `workflowCode`+`unitId`+`unitType`,工具解析该主体的显示名后匹配(`list_permission` 不回 unitId)。缺失即幂等成功。",
188
+ }),
177
189
  "workflow.validate": Object.freeze({
178
190
  scope: WORKFLOW_SCOPE,
179
191
  description: "只读预检(`POST /api/api/workflow/validate`):合成草稿模板(或可选 `template` 按 merge 语义)后返回 ValidateMessage[],拆出 errors/warnings,`valid = 无 ERROR`。用于 publish 前先看平台会不会拒——平台对 publish 常回 errcode:0 但 ValidateMessage[] 里带 ERROR,本工具把它显式暴露,避免「publish 看似成功却没落库」。不写任何状态。",
@@ -223,6 +235,9 @@ export function assertWorkflowContract() {
223
235
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.publish", ["workflow/publish", "template"]);
224
236
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.setButtons", ["buttonPermission", "customBtns", "childTableBtns", "formButtonCodes", "form-button-absent", "publish:false"]);
225
237
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.bindRule", ["startEventHandler", "cancelEventHandler", "activateEventHandler", "endEventHandler", "bizActions", "bizActionType", "BIZ_RULE", "publish:false", "enabledRuleCodes", "workflow-bind-rule-absent", "activityCode", "nodeEvent", "endActivity", "activityApproved"]);
238
+ assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.permissions", ["list_permission", "40009", "PART_VISIABLE"]);
239
+ assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.addPermission", ["create_permission", "unitType", "unitId"]);
240
+ assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.removePermission", ["delete_permission", "permissionId"]);
226
241
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.role.list", ["organization/role/list", "type", "Originator"]);
227
242
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.department.list", ["organization/department/tree", "parentId"]);
228
243
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "org.user.search", ["organization/user/search", "wd", "type 3"]);
@@ -18,6 +18,9 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
18
18
  claim("workflow.bindRule", T["workflow.bindRule"].scope),
19
19
  claim("workflow.setButtons", T["workflow.setButtons"].scope),
20
20
  claim("workflow.delete", T["workflow.delete"].scope),
21
+ claim("workflow.permissions", T["workflow.permissions"].scope),
22
+ claim("workflow.addPermission", T["workflow.addPermission"].scope),
23
+ claim("workflow.removePermission", T["workflow.removePermission"].scope),
21
24
  claim("configure_workflow", T["configure_workflow"].scope),
22
25
  claim("workflow.validate", T["workflow.validate"].scope),
23
26
  // Organization writes (role group / role / members / management scope).
@@ -2724,6 +2724,194 @@ export function registerWorkflowLane(tools) {
2724
2724
  };
2725
2725
  }),
2726
2726
  }));
2727
+ // ── 发起权限 (who may originate) ────────────────────────────────────────────
2728
+ //
2729
+ // 40009 流程发起权限不足 is the single most opaque refusal on this platform:
2730
+ // `data.submit` (and `data.save` with workflowCode) answers it, the message
2731
+ // names no permission, and there is no CLI face to inspect or grant one. r11
2732
+ // (2026-10-24) burned 7 submit attempts and could not deliver the whole
2733
+ // workflow section; the platform source shows the check is concrete:
2734
+ // `WorkflowAspect.validateWorkflowPermission` → template header must exist and
2735
+ // be published → `getWorkflowTemplateHeadersBySchemaCode(userId, schemaCode)`:
2736
+ // • the user's root department must resolve (`getAllParentIds` non-empty),
2737
+ // • `visibleType=ALL_VISIABLE` templates are open to everyone,
2738
+ // • otherwise a `h_workflow_permission` row must match the user, their
2739
+ // department (or any ancestor), or one of their roles,
2740
+ // • platform admins (`h_perm_admin` ADMIN/SYS_MNG) bypass all of it.
2741
+ // Publishing a flow creates NO permission row, so a freshly published flow is
2742
+ // unusable by anyone who is not a platform admin.
2743
+ const permissionRows = async (session, workflowCode) => wireRows((await wireCall(session, { path: ENDPOINTS.workflowListPermission, query: { workflowCode } })).data);
2744
+ // `list_permission` echoes the PERMISSION row id plus the unit's display name
2745
+ // and code — never the unitId (WorkflowTemplateController.getPermissionMap).
2746
+ // So identity for readback/removal is resolved from the unit's own face.
2747
+ const unitIdentity = async (session, unitType, unitId) => {
2748
+ try {
2749
+ if (unitType === 1) {
2750
+ const row = (await wireCall(session, { path: ENDPOINTS.organizationDepartmentGet, query: { deptId: unitId } })).data;
2751
+ return { name: row ? String(row.name ?? "") || null : null, code: row ? (String(row.code ?? "") || null) : null };
2752
+ }
2753
+ if (unitType === 2) {
2754
+ const rows = wireRows((await wireCall(session, { path: ENDPOINTS.organizationRoleList, query: { expandAll: "true" } })).data);
2755
+ const walk = (entries) => {
2756
+ for (const entry of entries) {
2757
+ if (String(entry.id ?? "") === unitId)
2758
+ return entry;
2759
+ const children = Array.isArray(entry.children) ? entry.children : [];
2760
+ const hit = walk(children);
2761
+ if (hit)
2762
+ return hit;
2763
+ }
2764
+ return undefined;
2765
+ };
2766
+ const hit = walk(rows);
2767
+ return { name: hit ? String(hit.name ?? "") || null : null, code: hit ? (String(hit.code ?? "") || null) : null };
2768
+ }
2769
+ const row = (await wireCall(session, { path: ENDPOINTS.organizationUserInfo, query: { id: unitId } })).data;
2770
+ return { name: row ? String(row.name ?? "") || null : null, code: row ? (String(row.username ?? "") || null) : null };
2771
+ }
2772
+ catch {
2773
+ return { name: null, code: null };
2774
+ }
2775
+ };
2776
+ tools.push(defineTool({
2777
+ name: "workflow.permissions",
2778
+ layer: "low",
2779
+ kind: "read",
2780
+ lane: WORKFLOW_LANE,
2781
+ scope: WORKFLOW_TOOLS["workflow.permissions"].scope,
2782
+ description: WORKFLOW_TOOLS["workflow.permissions"].description,
2783
+ inputSchema: {
2784
+ environment: environmentArg,
2785
+ workflowCode: z.string().optional().describe("流程 code;省略则用 schemaCode 解析"),
2786
+ schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型的流程"),
2787
+ capability: capabilityArg,
2788
+ },
2789
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
2790
+ const workflowCode = await resolveWorkflowArg(session, args);
2791
+ if (!workflowCode)
2792
+ return { schemaCode: args.schemaCode, exists: false, permissions: [], note: "该模型没有流程" };
2793
+ const rows = await permissionRows(session, workflowCode);
2794
+ const permissions = rows.map((row) => ({
2795
+ id: row.id ?? null,
2796
+ name: row.name ?? null,
2797
+ unitType: row.unitType ?? null,
2798
+ unitTypeName: row.unitType === 1 || row.unitType === "1" ? "部门" : row.unitType === 2 || row.unitType === "2" ? "角色" : row.unitType === 3 || row.unitType === "3" ? "用户" : null,
2799
+ fullName: row.fullName ?? null,
2800
+ corpId: row.corpId ?? null,
2801
+ code: row.code ?? null,
2802
+ }));
2803
+ return {
2804
+ workflowCode,
2805
+ count: permissions.length,
2806
+ permissions,
2807
+ ...(permissions.length === 0
2808
+ ? {
2809
+ blocker: "workflow-start-permission-empty",
2810
+ note: "该流程没有任何发起权限行:除平台管理员(h_perm_admin 里 ADMIN/SYS_MNG)外**没有人能发起**,data.submit 会回 40009「流程发起权限不足」。发布流程不会自动建这一行,用 workflow.addPermission 授给部门/角色/用户(或把模板 visibleType 改成 ALL_VISIABLE)。",
2811
+ nextCommand: `omc call workflow.addPermission --env <env> --args '{"workflowCode":"${workflowCode}","unitId":"<部门|角色|用户 id>","unitType":1}'`,
2812
+ }
2813
+ : {}),
2814
+ };
2815
+ }),
2816
+ }));
2817
+ writeTool(tools, "workflow.addPermission", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.addPermission"].scope, WORKFLOW_TOOLS["workflow.addPermission"].description, {
2818
+ workflowCode: z.string().optional().describe("流程 code;省略则用 schemaCode 解析"),
2819
+ schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型的流程"),
2820
+ unitId: z.string().min(1).describe("部门 id / 角色 id / 用户 id(看 unitType)"),
2821
+ unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).describe("1=部门 2=角色 3=用户;也接受 DEPARTMENT/ROLE/USER"),
2822
+ corpId: z.string().optional().describe("企业 id;省略则用当前会话企业"),
2823
+ }, async (session, args) => {
2824
+ const workflowCode = await resolveWorkflowArg(session, args);
2825
+ if (!workflowCode)
2826
+ throw new ToolRefusalError({ status: "refused", blocker: "workflow-not-found", message: `模型 ${String(args.schemaCode ?? "")} 没有流程;先 workflow.create` });
2827
+ const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
2828
+ // Idempotent retry: the platform answers 402003 WORKFLOW_PERMISSION_EXIST
2829
+ // for a duplicate unitId, which would read as a failure. `list_permission`
2830
+ // does NOT echo unitId (only id/name/unitType/fullName/code), so identity
2831
+ // is decided by the platform's own refusal rather than a guessed pre-read.
2832
+ const before = await permissionRows(session, workflowCode);
2833
+ try {
2834
+ // Send the unit's own name/code too: `list_permission` reads them back
2835
+ // (it echoes `name`/`code`), which is the only way `workflow.permissions`
2836
+ // can describe a row by more than its opaque permission id.
2837
+ const identity = await unitIdentity(session, unitType, args.unitId);
2838
+ await wireCall(session, {
2839
+ path: ENDPOINTS.workflowCreatePermission,
2840
+ method: "POST",
2841
+ body: {
2842
+ workflowCode,
2843
+ unitId: args.unitId,
2844
+ unitType,
2845
+ corpId: args.corpId ?? session.enterpriseId(),
2846
+ ...(identity.name ? { name: identity.name } : {}),
2847
+ ...(identity.code ? { code: identity.code } : {}),
2848
+ },
2849
+ });
2850
+ }
2851
+ catch (error) {
2852
+ const errcode = error.details?.errcode ?? error.errcode;
2853
+ if (String(errcode ?? "") !== "402003")
2854
+ throw new ToolRefusalError({
2855
+ status: "refused",
2856
+ blocker: "workflow-permission-write-failed",
2857
+ workflowCode,
2858
+ unitId: args.unitId,
2859
+ unitType,
2860
+ message: `create_permission 被平台拒绝(err=${String(errcode ?? "n/a")})。核对 unitId 是否真实存在(部门用 org.department.list、角色用 org.role.list、用户用 org.user.search),以及发起权限是否被企业策略禁用。原始错误:${error instanceof Error ? error.message : String(error)}`,
2861
+ });
2862
+ return {
2863
+ performed: { skipped: "already-present" },
2864
+ readback: { workflowCode, alreadyPresent: true, unitId: args.unitId, unitType, count: before.length, note: "该主体已有发起权限(平台 402003),未重复写入" },
2865
+ };
2866
+ }
2867
+ const after = await permissionRows(session, workflowCode);
2868
+ const created = after.find((row) => !before.some((old) => String(old.id ?? "") === String(row.id ?? ""))) ?? after[after.length - 1];
2869
+ return {
2870
+ performed: { errcode: 0 },
2871
+ readback: {
2872
+ workflowCode,
2873
+ count: after.length,
2874
+ permissionId: created?.id ?? null,
2875
+ unitType: created?.unitType ?? unitType,
2876
+ name: created?.name ?? null,
2877
+ note: "发起权限已授予。若之前 data.submit 回 40009,现在应可发起;仍失败先 workflow.permissions 确认行存在,并核对发起人所属部门/角色。",
2878
+ },
2879
+ };
2880
+ });
2881
+ writeTool(tools, "workflow.removePermission", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.removePermission"].scope, WORKFLOW_TOOLS["workflow.removePermission"].description, {
2882
+ permissionId: z.string().min(1).optional().describe("首选:workflow.permissions 返回的 id(唯一确定)"),
2883
+ workflowCode: z.string().optional().describe("配合 unitId+unitType 使用(平台的 list_permission 不回 unitId,需按显示名解析)"),
2884
+ unitId: z.string().min(1).optional().describe("部门/角色/用户 id;按 unitType 解析显示名后匹配权限行"),
2885
+ unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).optional().describe("unitId 的类型:1=部门 2=角色 3=用户"),
2886
+ schemaCode: z.string().optional(),
2887
+ }, async (session, args) => {
2888
+ let permissionId = args.permissionId;
2889
+ if (!permissionId) {
2890
+ const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode });
2891
+ if (!workflowCode || !args.unitId || args.unitType === undefined)
2892
+ throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "workflow.removePermission", message: "需要 permissionId(推荐),或 workflowCode(/schemaCode)+ unitId + unitType" });
2893
+ const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
2894
+ const identity = await unitIdentity(session, unitType, args.unitId);
2895
+ const rows = await permissionRows(session, workflowCode);
2896
+ const wantedType = String(unitType);
2897
+ const hit = rows.find((row) => String(row.unitType ?? "") === wantedType && (String(row.id ?? "") === args.unitId ||
2898
+ (identity.name !== null && String(row.name ?? "") === identity.name) ||
2899
+ (identity.code !== null && String(row.code ?? "") === identity.code)));
2900
+ if (!hit)
2901
+ return {
2902
+ performed: { skipped: "absent" },
2903
+ readback: {
2904
+ workflowCode,
2905
+ removed: false,
2906
+ note: `没有匹配该主体的权限行(幂等)。注意 list_permission 不回 unitId,匹配靠解析出的显示名「${identity.name ?? "?"}」;不确定时用 workflow.permissions 拿 id 再按 permissionId 删。`,
2907
+ permissions: rows.map((row) => ({ id: row.id ?? null, name: row.name ?? null, unitType: row.unitType ?? null })),
2908
+ },
2909
+ };
2910
+ permissionId = String(hit.id ?? "");
2911
+ }
2912
+ await wireCall(session, { path: ENDPOINTS.workflowDeletePermission, method: "POST", query: { permissionId } });
2913
+ return { performed: { errcode: 0 }, readback: { permissionId, removed: true } };
2914
+ });
2727
2915
  tools.push(defineTool({
2728
2916
  name: "org.role.list",
2729
2917
  layer: "low",
@@ -54,11 +54,17 @@ export async function mapUnpublishedAppFailure(session, schemaCode, errcode, roo
54
54
  * unpublished-app failure. Used by data.save/data.submit (errcode 50000) and
55
55
  * datarule.create type2 (errcode 305003) so the try/catch wiring lives once.
56
56
  */
57
- export async function runtimeWriteWithAppMapping(session, schemaCode, errcode, rootCause, call, formData) {
57
+ export async function runtimeWriteWithAppMapping(session, schemaCode, errcode, rootCause, call, formData, workflowCode) {
58
58
  try {
59
59
  return await call();
60
60
  }
61
61
  catch (error) {
62
+ // 40009 is diagnosed FIRST: it names a cause the app/relation mappings
63
+ // cannot see, and it is the one refusal whose fix is a concrete next
64
+ // command (r11: seven blind submit attempts). Guarded by the errcode so
65
+ // every other failure keeps its existing mapping.
66
+ if (error instanceof WireError && error.kind === "business" && error.errcode === 40009)
67
+ await diagnoseWorkflowStartPermission(session, schemaCode, workflowCode, error);
62
68
  let mapped;
63
69
  try {
64
70
  mapped = await mapUnpublishedAppFailure(session, schemaCode, errcode, rootCause, error);
@@ -131,4 +137,89 @@ export async function diagnoseRelationShapeFailure(session, schemaCode, errcode,
131
137
  message: `errcode ${errcode}——根因:单选关联字段 ${culprits.join(", ")} 传了数组(WORK_SHEET/propertyType 9 只接受 id 字符串或 {id};数组会稳定 ${errcode})。多选关联(11)与人员/组织(50/51)才用数组`,
132
138
  });
133
139
  }
140
+ /**
141
+ * 40009 流程发起权限不足 — the most opaque refusal on this platform.
142
+ *
143
+ * r11 (2026-10-24, 1.1.25) could not deliver an entire workflow section: seven
144
+ * `data.submit` attempts all answered the same two words, and neither the
145
+ * message nor any CLI face said WHY. The platform source
146
+ * (`WorkflowAspect.validateWorkflowPermission` →
147
+ * `WorkflowManagementFacadeImpl.getWorkflowTemplateHeadersBySchemaCode`) shows
148
+ * three independent causes, all of which produce the identical code:
149
+ *
150
+ * 1. `h_workflow_template` header missing / not published;
151
+ * 2. the caller's root department does not resolve (`getAllParentIds` empty →
152
+ * the allowed-template list is short-circuited to EMPTY, so a user whose
153
+ * department row was deleted/moved can never start anything);
154
+ * 3. no `h_workflow_permission` row matching the user, their department (or an
155
+ * ancestor), or one of their roles — and `visibleType != ALL_VISIABLE`.
156
+ * Publishing a flow creates NO permission row, so a freshly published flow
157
+ * is unusable by everyone except a `h_perm_admin` ADMIN/SYS_MNG.
158
+ *
159
+ * The diagnosis here reads only the cheap, authoritative faces (permission list
160
+ * + header presence) and names the recovery command instead of leaving the
161
+ * caller to guess. Anything that fails is swallowed: a diagnosis must never
162
+ * replace the original refusal.
163
+ */
164
+ export async function diagnoseWorkflowStartPermission(session, schemaCode, workflowCode, error) {
165
+ if (!(error instanceof WireError && error.kind === "business" && error.errcode === 40009))
166
+ throw error;
167
+ const resolvedCode = workflowCode ?? (await resolvePublishedWorkflowCodeQuiet(session, schemaCode));
168
+ if (!resolvedCode)
169
+ throw error;
170
+ let permissions = [];
171
+ let headerPresent = true;
172
+ try {
173
+ headerPresent = (await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode: resolvedCode } })).data != null;
174
+ }
175
+ catch {
176
+ headerPresent = true;
177
+ }
178
+ try {
179
+ permissions = wireRows((await wireCall(session, { path: ENDPOINTS.workflowListPermission, query: { workflowCode: resolvedCode } })).data);
180
+ }
181
+ catch {
182
+ permissions = [];
183
+ }
184
+ const headerHint = headerPresent ? "" : ` 流程头 ${resolvedCode} 不存在或未发布(先 workflow.publish)。`;
185
+ const message = permissions.length === 0
186
+ ? `流程 ${resolvedCode} 没有任何发起权限行。§ 发布流程不会自动建发起权限§:` +
187
+ `除平台管理员(平台 h_perm_admin 里的 ADMIN/SYS_MNG,注意这与登录接口返回的 isAdmin 不是一回事)外,任何人都无法发起,` +
188
+ `data.submit / data.save+workflowCode 一律回 40009。${headerHint}` +
189
+ ` 修复:把发起权限授给一个部门/角色/用户 —— ` +
190
+ `omc call workflow.addPermission --env <env> --args '{"workflowCode":"${resolvedCode}","unitId":"<org.department.list|org.role.list|org.user.search 的 id>","unitType":1}'` +
191
+ `(1=部门 2=角色 3=用户;同一行也可用 workflow.permissions 复核)。` +
192
+ `另一种原因同样报 40009 且无法从权限行看出来:发起人所属部门记录已被删除/移动(平台 getRootDeptId 解析为空时直接短路成"无任何可发起模板")——` +
193
+ `用 org.user.search 看自己的 departmentId,再用 org.department.list 确认该部门仍在。`
194
+ : `流程 ${resolvedCode} 已有 ${permissions.length} 条发起权限行(${permissions.map((row) => `${String(row.name ?? row.code ?? "?")}/unitType=${String(row.unitType ?? "?")}`).join("、")}),` +
195
+ `但当前调用者不在其中。${headerHint}` +
196
+ ` 核对顺序:① 发起人的 departmentId(org.user.search)是否等于某条 unitType=1 的部门,或是其祖先链上的一级;` +
197
+ `② 发起人是否属于某条 unitType=2 的角色(org.role.users);③ 是否该 unitType=3 的用户本人;` +
198
+ `④ 若部门记录已被删除/移动,平台会把可发起列表短路成空——同样报 40009,但与权限行无关。` +
199
+ ` 补授权:omc call workflow.addPermission --env <env> --args '{"workflowCode":"${resolvedCode}","unitId":"<id>","unitType":1}';查看现状用 omc call workflow.permissions。`;
200
+ throw new ToolRefusalError({
201
+ status: "refused",
202
+ blocker: "workflow-start-permission-denied",
203
+ schemaCode,
204
+ workflowCode: resolvedCode,
205
+ errcode: 40009,
206
+ permissionCount: permissions.length,
207
+ permissions,
208
+ message,
209
+ });
210
+ }
211
+ /** The published workflow code for a schema, or undefined (never throws). */
212
+ async function resolvePublishedWorkflowCodeQuiet(session, schemaCode) {
213
+ try {
214
+ const isWorkflow = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode } })).data === true;
215
+ if (!isWorkflow)
216
+ return undefined;
217
+ const template = (await wireCall(session, { path: ENDPOINTS.workflowGetBySchema, query: { schemaCode } })).data;
218
+ const code = template && typeof template.workflowCode === "string" && template.workflowCode.length > 0 ? template.workflowCode : null;
219
+ return code ?? undefined;
220
+ }
221
+ catch {
222
+ return undefined;
223
+ }
224
+ }
134
225
  //# 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.81",
3
+ "version": "0.4.82",
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.81",
53
+ "@stalfh233/omc-fake-platform": "0.4.82",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.81",
55
+ "omc-core": "0.4.82",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -148,7 +148,7 @@ only the section for the capability you need —
148
148
  ## 流程模板 `workflow`
149
149
 
150
150
  - **能做哪些事**:流程模板 get_draft/get_published、create、update、publish、bindRule(事件处理器规则绑定)、delete、workitem.list、workflow.instanceActivity(实例节点状态诊断);参与人用组织对象。
151
- - **工具**:`workflow.get_draft`、`workflow.setButtons`(节点「表单操作」按钮勾选)、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`configure_workflow`、`workitem.list`、`workflow.instanceActivity`、`org.role.list`、`org.department.list`、`org.user.search`、`org.role.users`、`org.role.userScope`、`org.roleGroup.create`、`org.role.create`、`org.role.update`、`org.role.addUsers`、`org.role.removeUsers`、`org.role.setUserScope`、`org.department.create`、`org.department.update`、`org.department.delete`、`org.department.users`、`org.user.create`、`org.user.update`、`org.user.delete`、`org.user.info`、`org.user.departments`、`org.role.delete`、`org.roleGroup.delete`、`org.user.transferDepartment`
151
+ - **工具**:`workflow.get_draft`、`workflow.setButtons`(节点「表单操作」按钮勾选)、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`workflow.permissions`、`workflow.addPermission`、`workflow.removePermission`(发起权限:发布流程不会自动建这一行,缺它一律 40009)、`configure_workflow`、`workitem.list`、`workflow.instanceActivity`、`org.role.list`、`org.department.list`、`org.user.search`、`org.role.users`、`org.role.userScope`、`org.roleGroup.create`、`org.role.create`、`org.role.update`、`org.role.addUsers`、`org.role.removeUsers`、`org.role.setUserScope`、`org.department.create`、`org.department.update`、`org.department.delete`、`org.department.users`、`org.user.create`、`org.user.update`、`org.user.delete`、`org.user.info`、`org.user.departments`、`org.role.delete`、`org.roleGroup.delete`、`org.user.transferDepartment`
152
152
  - **recipe**:`omc describe workflow.bindRule --example workflow-handler-binding`
153
153
  - **边界 / 陷阱**:greenfield 首版;`publish`/`bindRule` 不得在 `ValidateMessage[]` 拒绝时报告成功。运行时 workitem(提交/审批)走 `data.submit`;想知道实例停在哪/是否错派,用 `workflow.instanceActivity`(无 status=1=真停,参与人全是发起人=解析为空退回)。
154
154