@stalfh233/omc-cli 0.4.60 → 0.4.62

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.
@@ -349,7 +349,7 @@
349
349
  "capability": "W7 S29 runtime workflow work-item query (search_workitems)",
350
350
  "status": "executable",
351
351
  "scope": "GET /api/api/runtime/workflow/search_workitems (WorkItemSearchVO via @ModelAttribute; the server injects userId so the result is the caller's todo) returns PageVO<WorkItemVO>; per item bizObjectId/schemaCode resolved via GET /api/api/runtime/workflow/getBizObjectByWfInstanceId. Wire shape pinned from WorkflowInstanceController.searchWorkItems + WorkflowInstanceRuntimeController.handleSearchWorkItems + RuntimeController.searchWorkItems/WorkflowInstanceFacadeImpl.queryWorkItems. WorkItemSearchVO.filterWorkflowInstanceId is a NOT-equal exclusion on instanceId (BizWorkitemServiceImpl.toSearchWorkItemsCriterion:400-402), so exact instance matching is applied client-side.",
352
- "excluded_or_unsupported_reason": "Live-proven 2026-09-11: startWorkflow -> workitem.list discovered workItemId 970fe802baba463d97c1d97f3695bb3a + bizObjectId c607b77af4ab4293a1f6ce314ec9327c -> approval submit -> list count 0 and record COMPLETED. Evidence in .scratch/omc-app-management/issues/06-workitem-list-capability.md.",
352
+ "excluded_or_unsupported_reason": "Live-proven 2026-09-11: startWorkflow -> workitem.list discovered workItemId 970fe802baba463d97c1d97f3695bb3a + bizObjectId c607b77af4ab4293a1f6ce314ec9327c -> approval submit -> list count 0 and record COMPLETED. Evidence in .scratch/omc-app-management/issues/06-workitem-list-capability.md. r4-training 2026-10-20: a stalled instance (data.submit -> PROCESSING, no next todo) exposed nothing; list_workflow_instance_activity (WorkflowMaintenanceAssembler.listActivateActivity) now backs workflow.instanceActivity with per-activity status/participants so \"where did it stop\" is a read, not a MySQL+container-log dig.",
353
353
  "version_family": "8.6",
354
354
  "anchor_version": "8.6.26",
355
355
  "evidence": [
@@ -360,14 +360,20 @@
360
360
  {
361
361
  "path": ".scratch/omc-app-management/issues/06-workitem-list-capability.md",
362
362
  "anchor": "静态取证"
363
+ },
364
+ {
365
+ "path": "packages/omc-cli/src/workflow-support.ts",
366
+ "anchor": "listInstanceActivities"
363
367
  }
364
368
  ],
365
369
  "lane": "W7-S29-greenfield",
366
370
  "tools": [
367
- "workitem.list"
371
+ "workitem.list",
372
+ "workflow.instanceActivity"
368
373
  ],
369
374
  "scopes": [
370
- "runtime workflow work-item query"
375
+ "runtime workflow work-item query",
376
+ "runtime workflow instance node-state diagnosis"
371
377
  ]
372
378
  },
373
379
  {
package/dist/endpoints.js CHANGED
@@ -113,6 +113,9 @@ export const ENDPOINTS = {
113
113
  workflowDelete: "/api/api/workflow/delete",
114
114
  workflowSearchWorkItems: "/api/api/runtime/workflow/search_workitems",
115
115
  workflowGetBizObjectByWfInstanceId: "/api/api/runtime/workflow/getBizObjectByWfInstanceId",
116
+ workflowListInstanceActivity: "/api/api/runtime/workflow/list_workflow_instance_activity",
117
+ workflowListFinishedWorkItems: "/api/api/runtime/workflow/list_finished_workitems",
118
+ workflowGetInstanceBaseInfo: "/api/api/runtime/workflow/get_instance_baseinfo",
116
119
  organizationRoleList: "/api/api/organization/role/list",
117
120
  organizationDepartmentTree: "/api/api/organization/department/tree",
118
121
  organizationUserSearch: "/api/api/organization/user/search",
@@ -570,6 +570,7 @@ export function registerRuntimeDataLane(tools) {
570
570
  schemaCode: z.string(),
571
571
  bizObjectId: z.string({ required_error: "bizObjectId 必填(参数名就是 bizObjectId——不是 id 也不是 objectId,r44 两个 agent 都猜了 objectId;取值来自 data.submit 读回的 objectId/sequenceNo 或 data.list 的 row.bizObjectId)" }),
572
572
  full: z.boolean().optional().describe("返回未裁剪的完整业务对象(含 owner/creater 等系统字段;默认返回业务字段+身份的精简记录)"),
573
+ fields: z.array(z.string().min(1)).optional().describe("只保留列出的字段——核验状态时传 [\"sequenceStatus\",\"<业务状态字段>\"],别整条 data.load(r4-training 2026-10-20:同一对象被读 74 次);缺失字段回在 missingFields"),
573
574
  capability: capabilityArg,
574
575
  },
575
576
  handler: async (ctx, args, self) => {
@@ -603,7 +604,27 @@ export function registerRuntimeDataLane(tools) {
603
604
  }
604
605
  if (includeSchema)
605
606
  ctx.schemaCache.add(args.schemaCode);
606
- return { schemaCode: args.schemaCode, bizObjectId: args.bizObjectId, exists: data != null, ...(includeSchema ? {} : { schemaCached: true }), data };
607
+ // r4-training 2026-10-20: most repeat loads only ask "did it move?".
608
+ // Trim on the CLI side so the caller pays for the state bits, not the
609
+ // whole record (74 reads of one object in the r4 session).
610
+ let missingFields;
611
+ if (Array.isArray(args.fields) && args.fields.length > 0) {
612
+ if (data && typeof data === "object" && !Array.isArray(data)) {
613
+ const source = data;
614
+ const projected = {};
615
+ const missing = [];
616
+ for (const field of args.fields) {
617
+ if (field in source)
618
+ projected[field] = source[field];
619
+ else
620
+ missing.push(field);
621
+ }
622
+ data = projected;
623
+ if (missing.length > 0)
624
+ missingFields = missing;
625
+ }
626
+ }
627
+ return { schemaCode: args.schemaCode, bizObjectId: args.bizObjectId, exists: data != null, ...(missingFields ? { missingFields, availableFields: data && typeof data === "object" ? Object.keys(data) : [] } : {}), ...(includeSchema ? {} : { schemaCached: true }), data };
607
628
  }, { full: args.full === true, target: `data.load:${args.schemaCode}:${args.bizObjectId}` });
608
629
  },
609
630
  }));
@@ -1152,7 +1173,7 @@ export function registerRuntimeDataLane(tools) {
1152
1173
  code: "flow-no-next-workitem",
1153
1174
  message: "提交成功但该实例没有生成下一节点待办,流程停在 PROCESSING。常见原因:审批参与人解析为空(角色无成员 / 成员未挂发起人部门 / 参与人函数未注册)。",
1154
1175
  checks: [
1155
- `omc call workitem.list --args '{"workflowInstanceId":"${effectiveInstanceId}"}'(稍后重查,排除异步派发延迟)`,
1176
+ `omc call workflow.instanceActivity --args '{"workflowInstanceId":"${effectiveInstanceId}"}'(看每个节点的 status 与 participants;无 status=1 即停住)`,
1156
1177
  "容器日志搜 GetParticipantInfo error / CustomParticipleMethod{...} -> []",
1157
1178
  "参与人优先用参与者对象数组(角色 id);内置函数只认 FindUserByRole(org, roleCode[, order])",
1158
1179
  ],
@@ -47,7 +47,7 @@ export const WORKFLOW_WIRE = Object.freeze({
47
47
  /** Workflow template object (workflow/update|publish raw body). */
48
48
  export const workflowTemplateArg = z
49
49
  .record(z.string(), z.unknown())
50
- .describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空时实例同样卡 PROCESSING 且无待办,验收看 workitem.list 而不是 data.submit 返回(r4-training 实证 2026-10-20)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template.`);
50
+ .describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. Add approval activities between start/end (each with id/type/participant) and routes[] as needed. **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空(零成员/未挂部门/自由文本/角色码不存在)时引擎**把任务退回发起人**、流程继续,症状是「审批人成了我自己」,不是卡住;真正卡住的是参与人函数未注册(FunctionNotFoundException,无待办、PROCESSING、接口仍 errCode 0)。两者都用 workflow.instanceActivity 区分:无 status=1 节点=卡住;status=1 且参与人全是发起人=静默错派(r4-training 实证 2026-10-20)。 **START 之后必须保留发起节点**(如「填写申请单」participant={Originator},删掉/缺失则 publish 401035——r5-crm 实证 2026-10-09);流程模型的数据入口=直接 data.submit(创建+启流程+单据号生成,勿用 save+submit 两步)。 A route entry MAY carry the platform's structured branch condition ruleConditionGroup (outer OR / inner AND) plus childMatchType/utilizeElse/alternativeFlag/text — see the workflow contract's routeBranch: ${WORKFLOW_WIRE.routeBranch}. endEventHandler{bizActions,bizActionType} binds business rules — prefer workflow.bindRule instead of replacing the whole template.`);
51
51
  const WORKFLOW_SCOPE = "greenfield workflow authoring";
52
52
  export const WORKFLOW_LANE_CONTRACT = Object.freeze({
53
53
  lane: LANES.workflow,
@@ -156,7 +156,11 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
156
156
  }),
157
157
  "workitem.list": Object.freeze({
158
158
  scope: "runtime workflow work-item query",
159
- description: "查询当前登录人的待办(`GET /api/api/runtime/workflow/search_workitems`)。WorkItemSearchVO 经 @ModelAttribute 传参,服务端以登录态注入 userId,故无需传处理人——结果天然是「我的待办」。返回每项 workItemId(id) + workflowInstanceId(instanceId) + activityName + state;并尽力经 `GET /api/api/runtime/workflow/getBizObjectByWfInstanceId` 解析 bizObjectId/schemaCode 供 data.submit 审批。`workflowInstanceId` 为客户端精确过滤(服务端 WorkItemSearchVO.filterWorkflowInstanceId 实为 notEqualsTo 排除,不能用于命中实例)。",
159
+ description: "查询当前登录人的待办(`GET /api/api/runtime/workflow/search_workitems`)。WorkItemSearchVO 经 @ModelAttribute 传参,服务端以登录态注入 userId,故无需传处理人——结果天然是「我的待办」。返回每项 workItemId(id) + workflowInstanceId(instanceId) + activityName + state;并尽力经 `GET /api/api/runtime/workflow/getBizObjectByWfInstanceId` 解析 bizObjectId/schemaCode 供 data.submit 审批。`workflowInstanceId` 为客户端精确过滤(服务端 WorkItemSearchVO.filterWorkflowInstanceId 实为 notEqualsTo 排除,不能用于命中实例);带该过滤时 total 为过滤后的条数,原服务端总数在 unfilteredTotal。**想知道「流程停在哪」,用 `workflow.instanceActivity`,不要靠反复 workitem.list**——待办查询只返回在办项,实例停住时永远是空数组。",
160
+ }),
161
+ "workflow.instanceActivity": Object.freeze({
162
+ scope: "runtime workflow instance node-state diagnosis",
163
+ description: "按实例查全部节点状态(`GET /api/api/runtime/workflow/list_workflow_instance_activity`,WorkflowMaintenanceAssembler.listActivateActivity)。返回模板里每个活动节点的 activityCode/activityName/activityType + status(1=当前激活,0=已完成或未到达)+ 解析出的 participants(含姓名/id)。用于「data.submit 回 PROCESSING 但没有下一待办」的自诊断:例子 `omc call workflow.instanceActivity --args '{\"workflowInstanceId\":\"<id>\"}'`。当没有任何节点 status=1 时,readback 的 diagnosis.code=no-active-activity 并列出排查顺序(参与人为空的审批节点 → 参与人写法 → 容器日志 GetParticipantInfo error)。审批推进正常时该工具不需要——data.submit 的 readback.nextSubmit 已给出下一级参数。",
160
164
  }),
161
165
  }),
162
166
  });
@@ -191,6 +195,7 @@ export function assertWorkflowContract() {
191
195
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.validate", ["workflow/validate", "ValidateMessage", "valid"]);
192
196
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "configure_workflow", ["workflow.create", "workflow.update", "workflow.publish", "workflow.bindRule", "bindRules"]);
193
197
  assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workitem.list", ["search_workitems", "workItemId", "workflowInstanceId", "bizObjectId", "getBizObjectByWfInstanceId", "filterWorkflowInstanceId"]);
198
+ assertDescriptionCarries(WORKFLOW_LANE_CONTRACT, "workflow.instanceActivity", ["list_workflow_instance_activity", "status", "participants", "no-active-activity"]);
194
199
  if (!workflowTemplateArg.description?.includes("routes"))
195
200
  throw new Error("workflow template arg must document the routes->rules mapping");
196
201
  if (workflowTemplateArg.description?.includes('\\"templateType\\":\\"BusinessProcess\\"') || workflowTemplateArg.description?.includes('"templateType":"BusinessProcess"'))
@@ -26,7 +26,7 @@ export const WORKFLOW_COVERAGE = declareLaneCoverage(WORKFLOW_LANE_CONTRACT.lane
26
26
  "packages/omc-cli/src/lanes/workflow.ts",
27
27
  ".scratch/omc-app-management/issues/06-workitem-list-capability.md",
28
28
  ],
29
- tools: [claim("workitem.list", T["workitem.list"].scope)],
29
+ tools: [claim("workitem.list", T["workitem.list"].scope), claim("workflow.instanceActivity", T["workflow.instanceActivity"].scope)],
30
30
  },
31
31
  {
32
32
  capability: "workflow participant resolution (org roles/departments)",
@@ -8,7 +8,7 @@ import { registerPackageTool } from "../package-tool.js";
8
8
  import { writeSummary } from "../human-summary.js";
9
9
  import { defineTool, ToolInputError } from "../tool-types.js";
10
10
  import { wireCall, wireRows, WireError } from "../wire.js";
11
- import { resolveBizObjectIdByInstance, searchWorkItems, wireString } from "../workflow-support.js";
11
+ import { diagnoseInstanceActivity, getInstanceBaseInfo, listInstanceActivities, resolveBizObjectIdByInstance, searchWorkItems, wireString } from "../workflow-support.js";
12
12
  import { WORKFLOW_EVENT_HANDLER_FIELDS, WORKFLOW_LANE_CONTRACT, workflowTemplateArg } from "./workflow-contract.js";
13
13
  import { PLATFORM_TRIGGER_RULE_CODES } from "./rule-graph.js";
14
14
  import { executeRuleEnableWrite } from "./rule-lifecycle.js";
@@ -2067,10 +2067,60 @@ export function registerWorkflowLane(tools) {
2067
2067
  total: page.total,
2068
2068
  totalPages: page.totalPages,
2069
2069
  count: items.length,
2070
+ ...(page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
2070
2071
  items,
2071
2072
  };
2072
2073
  }),
2073
2074
  }));
2075
+ tools.push(defineTool({
2076
+ name: "workflow.instanceActivity",
2077
+ layer: "low",
2078
+ kind: "read",
2079
+ lane: WORKFLOW_LANE,
2080
+ scope: WORKFLOW_TOOLS["workflow.instanceActivity"].scope,
2081
+ description: WORKFLOW_TOOLS["workflow.instanceActivity"].description,
2082
+ inputSchema: {
2083
+ environment: environmentArg,
2084
+ workflowInstanceId: z.string().min(1).describe("流程实例 id(data.submit / data.load 读回的 workflowInstanceId)"),
2085
+ capability: capabilityArg,
2086
+ },
2087
+ handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
2088
+ const rows = await listInstanceActivities(session, args.workflowInstanceId);
2089
+ const header = await getInstanceBaseInfo(session, args.workflowInstanceId);
2090
+ const instanceStatus = header ? wireString(header.status) : null;
2091
+ const activities = rows.map((row) => {
2092
+ const participants = Array.isArray(row.participantors) ? row.participantors : [];
2093
+ return {
2094
+ activityCode: wireString(row.activityCode),
2095
+ activityName: wireString(row.activityName),
2096
+ activityType: row.activityType === undefined ? null : row.activityType,
2097
+ status: row.status === undefined ? null : row.status,
2098
+ participantCount: participants.length,
2099
+ participants: participants.slice(0, 10).map((person) => ({
2100
+ name: wireString(person.name) ?? wireString(person.userName) ?? null,
2101
+ id: wireString(person.id) ?? wireString(person.userId) ?? null,
2102
+ })),
2103
+ ...(row.sheetTrigger !== undefined ? { sheetTrigger: row.sheetTrigger } : {}),
2104
+ };
2105
+ });
2106
+ const active = activities.filter((activity) => activity.status === 1);
2107
+ const reached = activities.filter((activity) => activity.status === 1 || activity.participantCount > 0);
2108
+ // Diagnosis, not inference: the platform's own signal is status 1 = an
2109
+ // activity that currently holds tasks. No active activity while the
2110
+ // instance is still PROCESSING is exactly the silent-stall shape.
2111
+ const diagnosis = diagnoseInstanceActivity({ instanceStatus, endStatus: header ? wireString(header.endStatus) : null, originator: header ? wireString(header.originator) : null, activities });
2112
+ return {
2113
+ workflowInstanceId: args.workflowInstanceId,
2114
+ ...(instanceStatus ? { instanceStatus } : {}),
2115
+ ...(header && wireString(header.endStatus) ? { endStatus: wireString(header.endStatus) } : {}),
2116
+ activityCount: activities.length,
2117
+ activeCount: active.length,
2118
+ reachedCount: reached.length,
2119
+ activities,
2120
+ diagnosis,
2121
+ };
2122
+ }),
2123
+ }));
2074
2124
  tools.push(defineTool({
2075
2125
  name: "org.role.list",
2076
2126
  layer: "low",
@@ -28,7 +28,7 @@ const workflowTemplateUpdate = Object.freeze({
28
28
  "**{Originator}=发起人自己**:审批节点用它=提交人自审自己——提交人与审批人同账号时平台可能直接自动通过(COMPLETED,无待办;r40 实证,与节点结构有关)。需要独立审批人时绑定具体用户 id 或 `FindUserByRole({Originator.OU},\"<角色code>\",\"random\")`;验收要 PENDING 待办时先确认审批人≠提交人;",
29
29
  "**participant 必须是平台可解析的三种形态之一(真实生产 2026-09-23 观察)**:(a) 关键字 `{Originator}`;(b) 参与者对象数组 JSON `[{\"code\":\"<角色code>\",\"corpId\":\"<corpId|other>\",\"id\":\"<roleId>\",\"name\":\"<角色名>\",\"type\":2}]`(type 2=角色,3=用户;多个用 `+` 连接);(c) 函数 `FindUserByRole({Originator.OU},\"<roleCode>\",\"random\")`。**自由文本角色名(如 `部门负责人`)能 publish 但解析不到人、不生成待办**——先用 `org.role.list` / `org.department.list` 拿 code/id 再构造。工具会在 readback 里以 `participantWarnings` 标出自由文本。",
30
30
  "**不要从读回/种子配置回抄函数式参与人**:`workflow.get_published` 的摘要与平台种子流程(如 crm_contract_flow)会显示 `FindUsersByRoleFunction(...)` / `FindUsersSpecByRoleFunction(...)`,1.1.25 运行期**未注册这两个名字**,原样写回会在 ParticipantActivityRuntime 抛 `FunctionNotFoundException`;参与人表达式只认 `FindUserByRole(org, roleCode[, order])`(r4-training 实证 2026-10-20,`cloudpivot-workflow-AI-Platform-v1.1.25.jar` 只有 `FindUserByRoleFunction`)。",
31
- "**参与人解析为空同样是静默失败**:`FindUserByRole({Originator.OU},...)` 只找该部门范围内的角色成员;角色成员未挂部门(或角色零成员)时返回 `[]`,实例卡 PROCESSING、无待办、接口仍回 errCode 0。验收必须看 `workitem.list` 或容器日志 `CustomParticipleMethod{...} -> []`,不能只看 `data.submit` 返回;需要跨部门/全局角色时优先用参与者对象数组(角色 id),或先给角色补齐部门范围成员。",
31
+ "**参与人解析为空有两种后果,别混为一谈(1.1.25 实测 2026-10-20)**:① 解析成空数组(角色零成员、成员未挂发起人部门、自由文本、角色码不存在)时引擎**不报错也不卡住**,而是把任务退回**发起人**——`biz_workitem.participant` 就是发起人,接口回进程正常,唯一症状是「审批人显示成我自己」。② 参与人**函数未注册**(`FindUsersByRoleFunction` / `FindUsersSpecByRoleFunction`)才真停:`FunctionNotFoundException`、不生成待办、实例卡 PROCESSING 而接口仍回 errCode 0。要区分用 `omc call workflow.instanceActivity --args '{\"workflowInstanceId\":\"<id>\"}'`:无 status=1 节点=真停;有 status=1 且参与人全是发起人=静默错派(readback 的 diagnosis.warnings 会给 participant-fallback-to-originator)。",
32
32
  "**审批节点必须带 sheetCode(=模型默认表单槽位,通常等于 schemaCode)——S29 契约:participant sheetCode 为空 publish 会 401026 WORKFLOW_ACTIVITY_NO_SHEET。工具已自动回填,正常无需手填**;",
33
33
  "节点类型 START / PARTICIPANT / END;省略的既有节点由 merge 保留;",
34
34
  "**平台 workflow.create 会自动生成含「填写申请单」发起节点的草稿(START → 填写申请单 → 审批 → END);workflow.update 是 merge,不删除既有节点——要严格三节点需在设计器删该节点,或如实接受该发起节点**",
@@ -1,5 +1,5 @@
1
1
  import { ENDPOINTS } from "./endpoints.js";
2
- import { wireCall } from "./wire.js";
2
+ import { wireCall, wireRows } from "./wire.js";
3
3
  /**
4
4
  * Shared workflow RUNTIME endpoint knowledge (ticket 21): merges the two
5
5
  * parallel protocol implementations that used to live inside the runtime-data
@@ -62,7 +62,47 @@ export async function searchWorkItems(session, filter = {}) {
62
62
  const items = filter.instanceId
63
63
  ? page.content.filter((item) => wireString(item.instanceId) === filter.instanceId)
64
64
  : page.content;
65
- return { total: page.total, totalPages: page.totalPages, items };
65
+ // r4-training 2026-10-20: `total` is the server's count BEFORE the client-side
66
+ // instance filter, so a completed instance answered `total:7, count:0` — which
67
+ // reads as a contradiction. When the caller filtered by instance, report the
68
+ // filtered total and keep the raw number under unfilteredTotal.
69
+ return filter.instanceId
70
+ ? { total: items.length, totalPages: page.totalPages, unfilteredTotal: page.total, items }
71
+ : { total: page.total, totalPages: page.totalPages, items };
72
+ }
73
+ /**
74
+ * Node-by-node state of one workflow instance (r4-training 2026-10-20).
75
+ *
76
+ * A stalled instance (`data.submit` → PROCESSING with no next todo) answers
77
+ * errCode 0 and exposes nothing through `workitem.list` (the todo query only
78
+ * returns pending items). `list_workflow_instance_activity` returns every
79
+ * template activity with `status` (1 = active, 0 = done or never reached) and
80
+ * the resolved participants, which is the difference between "flow advanced"
81
+ * and "flow silently stopped" (WorkflowMaintenanceAssembler.listActivateActivity).
82
+ */
83
+ export async function listInstanceActivities(session, instanceId) {
84
+ const data = (await wireCall(session, {
85
+ path: ENDPOINTS.workflowListInstanceActivity,
86
+ query: { workflowInstanceId: instanceId },
87
+ })).data;
88
+ return wireRows(data);
89
+ }
90
+ /**
91
+ * Instance header (WorkflowIntanceBaseInfoVO): `status` is the run state, so a
92
+ * COMPLETED instance with zero active activities is NOT a stall. Best-effort —
93
+ * a header lookup failure must not hide the activity list.
94
+ */
95
+ export async function getInstanceBaseInfo(session, instanceId) {
96
+ try {
97
+ const data = (await wireCall(session, {
98
+ path: ENDPOINTS.workflowGetInstanceBaseInfo,
99
+ query: { workflowInstanceId: instanceId },
100
+ })).data;
101
+ return data && typeof data === "object" && !Array.isArray(data) ? data : null;
102
+ }
103
+ catch {
104
+ return null;
105
+ }
66
106
  }
67
107
  /**
68
108
  * Resolve the in-flight bizObject identity for a workflow instance.
@@ -96,4 +136,50 @@ export async function getReplayToken(session) {
96
136
  const token = await wireCall(session, { path: ENDPOINTS.runtimeFormReplayToken, query: {} });
97
137
  return typeof token.data === "string" && token.data ? token.data : undefined;
98
138
  }
139
+ /**
140
+ * Turn (instance status, per-activity status) into the one-line verdict a caller
141
+ * needs after `data.submit` returned PROCESSING. A finished instance legitimately
142
+ * has no active activity; a still-running one with none has silently stalled
143
+ * (r4-training 2026-10-20 — the stall answers errCode 0 and shows nothing).
144
+ */
145
+ export function diagnoseInstanceActivity(input) {
146
+ const active = input.activities.filter((activity) => activity.status === 1);
147
+ if (input.instanceStatus !== null && !/RUNNING|PROCESSING|ACTIVE/i.test(input.instanceStatus))
148
+ return {
149
+ code: "instance-finished",
150
+ message: `实例状态 ${input.instanceStatus},没有激活节点属正常结束${input.endStatus ? `(endStatus=${input.endStatus})` : ""}。`,
151
+ checks: [],
152
+ warnings: [],
153
+ };
154
+ if (active.length === 0) {
155
+ const emptyParticipants = input.activities.filter((activity) => activity.participantCount === 0).map((activity) => activity.activityCode ?? "?");
156
+ return {
157
+ code: "no-active-activity",
158
+ message: `该实例仍在运行,但没有任何活动节点处于激活状态(status=1)——流程已停住:可能是参与人解析为空、节点被取消,或下一节点未创建${emptyParticipants.length ? `。无参与人的节点:${emptyParticipants.join("、")}` : ""}。`,
159
+ checks: [
160
+ "看上面 participants 为空的审批节点——参与人解析为空是 1.1.25 最常见的静默停滞原因",
161
+ "参与人优先用参与者对象数组(角色 id);内置函数只认 FindUserByRole(org, roleCode[, order])",
162
+ "需要平台日志时看容器日志的 GetParticipantInfo error / CustomParticipleMethod{...} -> []",
163
+ ],
164
+ warnings: [],
165
+ };
166
+ }
167
+ // 1.1.25 fallback shape (r4-training 2026-10-20 live): when a participant
168
+ // expression resolves to nobody (empty role, free text, unknown role code) the
169
+ // engine does NOT stall — it hands the task to the originator, so the only
170
+ // visible symptom is "the approver is me". Flag that instead of calling it a
171
+ // stall: a real stall (unregistered participant function / engine exception)
172
+ // produces no work item at all.
173
+ const originator = input.originator ?? null;
174
+ const fallbackSuspect = originator !== null &&
175
+ active.some((activity) => activity.participantCount > 0 && activity.participants.every((person) => person.id === originator));
176
+ return {
177
+ code: "has-active-activity",
178
+ message: `当前激活节点:${active.map((activity) => `${activity.activityCode}(${activity.activityName ?? ""})`).join("、")}`,
179
+ checks: fallbackSuspect
180
+ ? ["若这些节点本应流转给角色或他人,现在却落在发起人身上:参与人解析为空时平台会退回发起人(1.1.25 实测),流程看似推进实为错派。核对参与人写法(参与者对象数组 / FindUserByRole(org, roleCode[, order]))与角色成员。"]
181
+ : [],
182
+ warnings: fallbackSuspect ? ["participant-fallback-to-originator"] : [],
183
+ };
184
+ }
99
185
  //# sourceMappingURL=workflow-support.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stalfh233/omc-cli",
3
- "version": "0.4.60",
3
+ "version": "0.4.62",
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.60",
53
+ "@stalfh233/omc-fake-platform": "0.4.62",
54
54
  "commander": "14.0.3",
55
- "omc-core": "0.4.60",
55
+ "omc-core": "0.4.62",
56
56
  "proper-lockfile": "4.1.2",
57
57
  "write-file-atomic": "6.0.0",
58
58
  "zod": "^3.23.8",
@@ -150,7 +150,8 @@ rules:
150
150
 
151
151
  - **两条路**:模型+表单+流程一揽子交付走**意图**(`intent` 的 `workflow` 能力,声明式一次 apply);只动流程或想逐步控制走**低层链** `workflow.create → workflow.update(权限/节点)→ workflow.publish`,标准链配方见 `omc describe workflow.update`(含多级审批/participant 三形态/自动回填说明)。流程权限细节见下节。
152
152
  - 一个模型只能有一个流程;已有流程的模型直接 `workflow.update`,不要另建。
153
- - **参与人解析失败是静默的**:角色零成员、成员未挂部门(`FindUserByRole({Originator.OU},...)` 只找部门范围)、或写了未注册函数(`FindUsersByRoleFunction` / `FindUsersSpecByRoleFunction`,1.1.25 未内置)时,`data.submit` 仍回 `errCode 0`,实例停在 PROCESSING 且 `biz_workitem` 无下一节点。验收看 `workitem.list` 是否有新待办,或容器日志的 `GetParticipantInfo error` / `CustomParticipleMethod{...} -> []`;不要从 `workflow.get_published` 的摘要回抄函数式参与人。内置写法只有 `FindUserByRole(org, roleCode[, order])`,跨部门/全局角色优先用参与者对象数组(角色 id)。
153
+ - **核验状态用投影,别重复整条读**:`data.load --args '{"schemaCode":..,"bizObjectId":..,"fields":["sequenceStatus","<业务状态字段>"]}'` 只回指定字段(缺失项在 `missingFields`)——r4 实测同一对象被整条读 74 次;大结果先 `artifact get <id> --summary` → `--keys/--jq`,别整包内联进推理。
154
+ - **参与人解析失败分两种,别当成同一种(1.1.25 实测)**:① **解析为空**(角色零成员、成员未挂部门、自由文本、角色码不存在)→ 引擎把任务**退回发起人**,流程继续,症状是「审批人显示成我自己」,`data.submit` 与 `workitem.list` 都正常;② **参与人函数未注册**(`FindUsersByRoleFunction` / `FindUsersSpecByRoleFunction`,1.1.25 未内置)→ `FunctionNotFoundException`、不生成待办、实例卡 PROCESSING 而接口仍回 `errCode 0`。区分用 `omc call workflow.instanceActivity --args '{"workflowInstanceId":"<id>"}'`:无 `status:1` 节点=卡住;`status:1` 且参与人全是发起人=`diagnosis.warnings` 给 `participant-fallback-to-originator`。不要从 `workflow.get_published` 的摘要回抄函数式参与人;内置写法只有 `FindUserByRole(org, roleCode[, order])`,跨部门/全局角色优先用参与者对象数组(角色 id)。
154
155
 
155
156
  ## 流程字段权限
156
157
 
@@ -137,10 +137,10 @@ only the section for the capability you need —
137
137
 
138
138
  ## 流程模板 `workflow`
139
139
 
140
- - **能做哪些事**:流程模板 get_draft/get_published、create、update、publish、bindRule(事件处理器规则绑定)、delete、workitem.list;参与人用组织对象。
141
- - **工具**:`workflow.get_draft`、`workflow.get_published`、`workflow.validate`、`workflow.create`、`workflow.update`、`workflow.publish`、`workflow.bindRule`、`workflow.delete`、`configure_workflow`、`workitem.list`、`org.role.list`、`org.department.list`、`org.user.search`
140
+ - **能做哪些事**:流程模板 get_draft/get_published、create、update、publish、bindRule(事件处理器规则绑定)、delete、workitem.list、workflow.instanceActivity(实例节点状态诊断);参与人用组织对象。
141
+ - **工具**:`workflow.get_draft`、`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`
142
142
  - **recipe**:`omc describe workflow.bindRule --example workflow-handler-binding`
143
- - **边界 / 陷阱**:greenfield 首版;`publish`/`bindRule` 不得在 `ValidateMessage[]` 拒绝时报告成功。运行时 workitem(提交/审批)走 `data.submit`。
143
+ - **边界 / 陷阱**:greenfield 首版;`publish`/`bindRule` 不得在 `ValidateMessage[]` 拒绝时报告成功。运行时 workitem(提交/审批)走 `data.submit`;想知道实例停在哪/是否错派,用 `workflow.instanceActivity`(无 status=1=真停,参与人全是发起人=解析为空退回)。
144
144
 
145
145
  ## 表单在线 JS `onlinejs`
146
146