@stalfh233/omc-cli 0.4.85 → 0.4.87
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 +3 -1
- package/dist/endpoints.js +12 -0
- package/dist/goal-routes.js +96 -1
- package/dist/lanes/model.js +44 -0
- package/dist/lanes/runtime-data.js +6 -1
- package/dist/lanes/workflow.js +102 -15
- package/dist/model-scaffold.js +12 -1
- package/dist/runtime-support.js +91 -0
- package/dist/workflow-support.js +34 -0
- package/package.json +3 -3
package/README.md
CHANGED
|
@@ -130,10 +130,12 @@ 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.
|
|
133
|
+
## 近期能力(0.4.74 → 0.4.87)
|
|
134
134
|
|
|
135
135
|
| 版本 | 新增 / 修复 |
|
|
136
136
|
|---|---|
|
|
137
|
+
| 0.4.87 | **r14 真机探针修复**:`workitem.list` 的 50000 **不再是无解题**——按 `participant` 走 JPA 查询端点(能查**别人**名下的待办、且不解析流程模板),全局搜索失败时回 `workitem-search-unavailable` 带成因与三条绕行;`workflow.delete` 的 402508 改 `workflow-has-instances` 并说明**不要改用 model.delete 绕过**;`model.delete` 在有未完结待办时回 `workflowWarning`(级联会留脏待办 → 全局待办搜索整页 NPE)。另修两个 composer 模板参数名写错(`form-default --value`→`--default`、`rule-when --rule`→`--on`),并加**模板 vs `--help` 自动校验测试** |
|
|
138
|
+
| 0.4.86 | **r13 真机探针修复**:`omc next` 增加 **composer 路由**(含建模型+字段/审批流/计算/条件规则/表单默认值 → 直接给出对应一条命令;读面仍走 ladder,条件分支流程仍走 recipe)——r13 读了文档里那行 `model-scaffold` 却仍调 26 次 `field.create`,说明「写进文档」不等于「能被发现」。`data.submit` 审批提交**写前校验身份**:非本人在办的待办直接回 `submit-workitem-not-mine`(带待办持有者 + 三条出路),不再让平台回一个 `40005 权限不足`让 agent 猜 |
|
|
137
139
|
| 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
140
|
| 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
141
|
| 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`:带上该流程的权限行现状、两种成因(无权限行 / 发起人部门记录已失效)与可粘贴的修复命令,不再是「流程发起权限不足」五个字 |
|
package/dist/endpoints.js
CHANGED
|
@@ -165,6 +165,18 @@ export const ENDPOINTS = {
|
|
|
165
165
|
workflowCreatePermission: "/api/api/workflow/create_permission",
|
|
166
166
|
workflowDeletePermission: "/api/api/workflow/delete_permission",
|
|
167
167
|
workflowSearchWorkItems: "/api/api/runtime/workflow/search_workitems",
|
|
168
|
+
/**
|
|
169
|
+
* The JPA-spec work-item query (WorkflowInstanceController.listWorkItems).
|
|
170
|
+
*
|
|
171
|
+
* Distinct from `search_workitems` in a way that matters: search resolves each
|
|
172
|
+
* hit's workflow template (`buildTempEntity`) and NPEs the WHOLE page when one
|
|
173
|
+
* work item points at a template row that no longer exists (r14 live: a stale
|
|
174
|
+
* `inspHazardFlow` reference made every unscoped `workitem.list` answer 50000,
|
|
175
|
+
* eight times in a row). This endpoint builds a WorkItemQuerySpec and queries
|
|
176
|
+
* the table directly — no template resolution — and it accepts `participant`,
|
|
177
|
+
* so it can answer "what is pending for user X", not only "for me".
|
|
178
|
+
*/
|
|
179
|
+
workflowListWorkItems: "/api/api/runtime/workflow/list_workitems",
|
|
168
180
|
workflowGetBizObjectByWfInstanceId: "/api/api/runtime/workflow/getBizObjectByWfInstanceId",
|
|
169
181
|
workflowListInstanceActivity: "/api/api/runtime/workflow/list_workflow_instance_activity",
|
|
170
182
|
workflowListFinishedWorkItems: "/api/api/runtime/workflow/list_finished_workitems",
|
package/dist/goal-routes.js
CHANGED
|
@@ -321,7 +321,75 @@ 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
|
-
|
|
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
|
+
// `--on` is the trigger (Create/Update/…), not `--rule`: r14's template
|
|
357
|
+
// invented a flag, and the new template-vs---help test caught it offline.
|
|
358
|
+
command: "omc rule-when --schema <模型code> --on Create --when '<条件>' --then 'set:<字段code>=<值>'",
|
|
359
|
+
usage: "omc rule-when --help",
|
|
360
|
+
summary: "条件→动作规则一条命令(BRANCH + 赋值节点拼装),替代手拼 nodeGraph",
|
|
361
|
+
groups: [["规则", "条件"], ["如果", "当", "满足", "时"], ["更新", "赋值", "改为", "设为", "动作"]],
|
|
362
|
+
avoid: [],
|
|
363
|
+
topics: ["omc-business-configuration"],
|
|
364
|
+
},
|
|
365
|
+
{
|
|
366
|
+
id: "form-default",
|
|
367
|
+
// The flag is `--default`, not `--value` (r14: the template was wrong and the
|
|
368
|
+
// probe's copy-paste failed with `unknown option '--value'`). Composers must
|
|
369
|
+
// emit commands that actually parse — a wrong skeleton is worse than none.
|
|
370
|
+
command: "omc form-default --schema <模型code> --field <字段code> --default <值> --apply",
|
|
371
|
+
usage: "omc form-default --help",
|
|
372
|
+
summary: "表单字段默认值一条命令(建表单/复用已有表单 + 写默认值),无需手改 options",
|
|
373
|
+
groups: [["默认值", "默认"], ["表单", "字段"]],
|
|
374
|
+
avoid: [],
|
|
375
|
+
topics: ["omc-business-configuration"],
|
|
376
|
+
},
|
|
377
|
+
]);
|
|
378
|
+
/** Test seam: the composer registry, so a template typo can be caught offline. */
|
|
379
|
+
export const COMPOSER_ROUTES_FOR_TEST = COMPOSER_ROUTES;
|
|
380
|
+
function matchComposer(goal, mode) {
|
|
381
|
+
// A composer is a WRITE path: reads and scouting asks must keep their ladder.
|
|
382
|
+
if (mode !== "build" && mode !== "modify")
|
|
383
|
+
return undefined;
|
|
384
|
+
for (const composer of COMPOSER_ROUTES) {
|
|
385
|
+
if (composer.avoid?.some((phrase) => goal.includes(phrase)))
|
|
386
|
+
continue;
|
|
387
|
+
if (!composer.groups.every((group) => group.some((cue) => goal.includes(cue))))
|
|
388
|
+
continue;
|
|
389
|
+
return composer;
|
|
390
|
+
}
|
|
391
|
+
return undefined;
|
|
392
|
+
}
|
|
325
393
|
function isWholePackageWrite(mode, goal, capabilityCount) {
|
|
326
394
|
return (mode === "build" || mode === "modify") && looksWholePackage(goal, capabilityCount);
|
|
327
395
|
}
|
|
@@ -400,6 +468,33 @@ export function goalRoute(registry, goal) {
|
|
|
400
468
|
route: { write: false, readback: false, firstCommands: ["omc capabilities", "omc capability <id>", "omc help omc-capability-scouting"] },
|
|
401
469
|
};
|
|
402
470
|
}
|
|
471
|
+
// Composer before the capability solver: a recurring write SHAPE has a single
|
|
472
|
+
// command that does the job with preflight + idempotency, which beats a ladder
|
|
473
|
+
// of atomic tools even when the ladder also matches (r13: 26 field.create).
|
|
474
|
+
const composer = matchComposer(goal, mode);
|
|
475
|
+
if (composer) {
|
|
476
|
+
const topics = (composer.topics ?? []).filter((topic) => availableTopics.has(topic));
|
|
477
|
+
return {
|
|
478
|
+
status: "ok",
|
|
479
|
+
goal,
|
|
480
|
+
confident: true,
|
|
481
|
+
mode,
|
|
482
|
+
write: true,
|
|
483
|
+
match: { kind: "composer", ref: composer.id, title: composer.summary, command: composer.command },
|
|
484
|
+
route: {
|
|
485
|
+
composer: composer.id,
|
|
486
|
+
command: composer.command,
|
|
487
|
+
summary: composer.summary,
|
|
488
|
+
// The composer IS the first step; readiness still comes first because
|
|
489
|
+
// the command needs a bound environment to run.
|
|
490
|
+
firstCommands: [DISCOVERY_AUTHORITY.readiness, composer.command, composer.usage],
|
|
491
|
+
write: true,
|
|
492
|
+
readback: true,
|
|
493
|
+
...(topics.length > 0 ? { readCommands: topics.map((topic) => `omc help ${topic} --section "<节>"`), readHint: "只在 composer 的 --help 不够用时再读这些 topic 的对应节" } : {}),
|
|
494
|
+
},
|
|
495
|
+
...(topics.length > 0 ? { topics } : {}),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
403
498
|
const scores = [...capabilityScores(registry, goal).entries()].sort((left, right) => right[1] - left[1]);
|
|
404
499
|
const fallback = {
|
|
405
500
|
status: "ok",
|
package/dist/lanes/model.js
CHANGED
|
@@ -17,6 +17,32 @@ import { wireCall, wireRows, WireError } from "../wire.js";
|
|
|
17
17
|
import { MODEL_CREATE_BODY_DEFAULTS, MODEL_LANE_CONTRACT } from "./model-contract.js";
|
|
18
18
|
const MODEL_LANE = MODEL_LANE_CONTRACT.lane;
|
|
19
19
|
const MODEL_TOOLS = MODEL_LANE_CONTRACT.tools;
|
|
20
|
+
/**
|
|
21
|
+
* Pending work items for every workflow attached to this model.
|
|
22
|
+
*
|
|
23
|
+
* Best-effort (empty on any failure) — it exists to warn before a cascade that
|
|
24
|
+
* orphans todos, never to block the delete.
|
|
25
|
+
*/
|
|
26
|
+
async function pendingWorkflowItems(session, schemaCode) {
|
|
27
|
+
try {
|
|
28
|
+
const isWorkflow = (await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode } })).data === true;
|
|
29
|
+
if (!isWorkflow)
|
|
30
|
+
return [];
|
|
31
|
+
const template = (await wireCall(session, { path: ENDPOINTS.workflowGetBySchema, query: { schemaCode } })).data;
|
|
32
|
+
const workflowCode = template && typeof template.workflowCode === "string" ? template.workflowCode : "";
|
|
33
|
+
if (!workflowCode)
|
|
34
|
+
return [];
|
|
35
|
+
const data = (await wireCall(session, {
|
|
36
|
+
path: ENDPOINTS.workflowListWorkItems,
|
|
37
|
+
query: { workflowCode, status: "PENDING", page: "0", size: "1" },
|
|
38
|
+
})).data;
|
|
39
|
+
const total = Number(data?.totalElements ?? 0);
|
|
40
|
+
return Number.isFinite(total) && total > 0 ? [{ workflowCode, pending: total }] : [];
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
}
|
|
20
46
|
function modelRollbackSpec(toolName) {
|
|
21
47
|
return {
|
|
22
48
|
target: (args) => `model:${String(args.schemaCode ?? args.code ?? "")}`,
|
|
@@ -586,6 +612,12 @@ export function registerModelLane(tools) {
|
|
|
586
612
|
message: `${args.schemaCode} 被模型 ${referencingModels.map((model) => model.schemaCode).join("、")} 的关联字段引用,不能删除(服务端 deleteFunctions 的异步拒绝已提前到调用时)。依赖倒序清理:先删引用方模型(或先删其关联字段),再删被引用模型;引用方物理删除的逐列异步级联需 40-60s,完成后再删本模型。`,
|
|
587
613
|
});
|
|
588
614
|
}
|
|
615
|
+
// Model deletion CASCADES the workflow template away but leaves
|
|
616
|
+
// `biz_workitem` rows pointing at it. The platform's global todo search
|
|
617
|
+
// resolves every hit's template, so ONE orphan NPEs the whole page for
|
|
618
|
+
// everyone (r14 live: 8 consecutive 50000s; cleanup needed a maintainer
|
|
619
|
+
// with DB access). Warn while it is still actionable.
|
|
620
|
+
const liveWorkflows = await pendingWorkflowItems(session, args.schemaCode);
|
|
589
621
|
const performed = await wireCall(session, {
|
|
590
622
|
path: ENDPOINTS.appPackagePhysicallyDelete,
|
|
591
623
|
method: "POST",
|
|
@@ -608,6 +640,18 @@ export function registerModelLane(tools) {
|
|
|
608
640
|
readback: {
|
|
609
641
|
absent,
|
|
610
642
|
treeNodePending,
|
|
643
|
+
...(liveWorkflows.length > 0
|
|
644
|
+
? {
|
|
645
|
+
workflowWarning: {
|
|
646
|
+
code: "model-deleted-with-pending-work-items",
|
|
647
|
+
severity: "warning",
|
|
648
|
+
workflows: liveWorkflows,
|
|
649
|
+
message: `删除前该模型的流程还有未完结待办(${liveWorkflows.map((row) => `${row.workflowCode}:${row.pending}`).join("、")})。` +
|
|
650
|
+
`模型删除会级联掉流程模板,但**不会**清掉这些待办;它们从此指向空模板,而平台全局待办搜索会给每条待办解析模板 —— **一条缺失就整页 NPE(50000),所有用户的待办列表一起坏掉**(r14 实测 8 连击,只能靠维护者清库恢复)。` +
|
|
651
|
+
`若 now 已经删了:用 workitem.list 的 \`workflowCode\` 或 \`participant\` 参数绕开全局搜索,并请维护者清理这些脏待办。下次请先完结实例再删模型。`,
|
|
652
|
+
},
|
|
653
|
+
}
|
|
654
|
+
: {}),
|
|
611
655
|
...(absent
|
|
612
656
|
? {}
|
|
613
657
|
: {
|
|
@@ -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",
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -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 { diagnoseInstanceActivity, getInstanceBaseInfo, listInstanceActivities, resolveBizObjectIdByInstance, searchWorkItems, wireString } from "../workflow-support.js";
|
|
11
|
+
import { diagnoseInstanceActivity, getInstanceBaseInfo, listInstanceActivities, listWorkItemsByParticipant, resolveBizObjectIdByInstance, searchWorkItems, wireString } from "../workflow-support.js";
|
|
12
12
|
import { readSchemaFormButtons } from "../form-support.js";
|
|
13
13
|
import { WORKFLOW_EVENT_HANDLER_FIELDS, WORKFLOW_NODE_EVENT_HANDLER_FIELDS, WORKFLOW_LANE_CONTRACT, workflowTemplateArg } from "./workflow-contract.js";
|
|
14
14
|
import { PLATFORM_TRIGGER_RULE_CODES } from "./rule-graph.js";
|
|
@@ -1646,6 +1646,25 @@ function workflowRollbackSpec(toolName) {
|
|
|
1646
1646
|
},
|
|
1647
1647
|
};
|
|
1648
1648
|
}
|
|
1649
|
+
/**
|
|
1650
|
+
* How many workflow instances for this model are still unfinished.
|
|
1651
|
+
*
|
|
1652
|
+
* Best-effort (0 on any failure): it exists to WARN before a delete that would
|
|
1653
|
+
* orphan todos, never to block one.
|
|
1654
|
+
*/
|
|
1655
|
+
async function countPendingWorkItems(session, workflowCode) {
|
|
1656
|
+
try {
|
|
1657
|
+
const data = (await wireCall(session, {
|
|
1658
|
+
path: ENDPOINTS.workflowListWorkItems,
|
|
1659
|
+
query: { workflowCode, status: "PENDING", page: "0", size: "1" },
|
|
1660
|
+
})).data;
|
|
1661
|
+
const total = Number(data?.totalElements ?? 0);
|
|
1662
|
+
return Number.isFinite(total) && total > 0 ? total : 0;
|
|
1663
|
+
}
|
|
1664
|
+
catch {
|
|
1665
|
+
return 0;
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1649
1668
|
async function mergedTemplateBody(session, schemaCode, code, template, merge = false) {
|
|
1650
1669
|
const draft = await getWorkflowDraftOrNull(session, code);
|
|
1651
1670
|
if (draft == null)
|
|
@@ -2518,11 +2537,29 @@ export function registerWorkflowLane(tools) {
|
|
|
2518
2537
|
};
|
|
2519
2538
|
}, undefined, undefined, workflowRollbackSpec("workflow.update"));
|
|
2520
2539
|
writeTool(tools, "workflow.delete", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.delete"].scope, WORKFLOW_TOOLS["workflow.delete"].description, { schemaCode: z.string().min(1), code: z.string().min(1) }, async (session, args) => {
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
method: "POST",
|
|
2524
|
-
|
|
2525
|
-
|
|
2540
|
+
let performed;
|
|
2541
|
+
try {
|
|
2542
|
+
performed = await wireCall(session, { path: ENDPOINTS.workflowDelete, method: "POST", query: { workflowCode: args.code } });
|
|
2543
|
+
}
|
|
2544
|
+
catch (error) {
|
|
2545
|
+
// 402508 流程实例已存在 — the platform refuses on purpose. Previously it
|
|
2546
|
+
// arrived as a bare wire code, so the caller's next move was to delete the
|
|
2547
|
+
// MODEL instead (which cascades the template away and leaves orphan todos
|
|
2548
|
+
// pointing at nothing — r14's 8× 50000). Name the order.
|
|
2549
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 402508) {
|
|
2550
|
+
const pending = await countPendingWorkItems(session, args.code);
|
|
2551
|
+
throw new ToolRefusalError({
|
|
2552
|
+
status: "refused",
|
|
2553
|
+
blocker: "workflow-has-instances",
|
|
2554
|
+
workflowCode: args.code,
|
|
2555
|
+
...(pending > 0 ? { pendingWorkItems: pending } : {}),
|
|
2556
|
+
message: `流程 ${args.code} 还有流程实例,平台拒绝删除(402508)。**不要改用 model.delete 绕过**——那会把流程模板级联删掉,` +
|
|
2557
|
+
`留下指向空模板的待办,而平台全局待办搜索会给每条待办解析模板,**一条缺失就整页 NPE(50000),所有用户的待办列表一起坏掉**(r14 实测 8 连击,需维护者清库)。` +
|
|
2558
|
+
`正确顺序:① 先完结/清理实例的待办(workitem.list --workflowCode ${args.code} 看还有哪些在办);② 再 workflow.delete;③ 最后才 model.delete。`,
|
|
2559
|
+
});
|
|
2560
|
+
}
|
|
2561
|
+
throw error;
|
|
2562
|
+
}
|
|
2526
2563
|
const header = await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode: args.code } }).catch(() => ({ data: null }));
|
|
2527
2564
|
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, absent: header.data === null || header.data === undefined } };
|
|
2528
2565
|
}, undefined, undefined, workflowRollbackSpec("workflow.delete"));
|
|
@@ -2607,6 +2644,14 @@ export function registerWorkflowLane(tools) {
|
|
|
2607
2644
|
.describe("exact-match client-side filter on the returned items' instanceId (the server filterWorkflowInstanceId field is a NOT-equal exclusion, so it is not used)"),
|
|
2608
2645
|
workflowCode: z.string().optional(),
|
|
2609
2646
|
schemaCode: z.string().optional(),
|
|
2647
|
+
participant: z
|
|
2648
|
+
.string()
|
|
2649
|
+
.optional()
|
|
2650
|
+
.describe("按**指定用户**的待办查询(用户 id)。省略=当前登录人。用它查别人名下的待办——全局搜索会按登录人过滤,查不到别人的(审批走不通时的第一个诊断手段)"),
|
|
2651
|
+
state: z
|
|
2652
|
+
.enum(["PENDING", "NOTSTARTED", "INPROGRESS", "DRAFT", "PAUSE", "FINISHED"])
|
|
2653
|
+
.optional()
|
|
2654
|
+
.describe("待办状态,配合 participant 使用;默认 PENDING(在办)"),
|
|
2610
2655
|
// r35: an agent sent page:1 expecting the first page and read
|
|
2611
2656
|
// `total:1 / count:0` as a contradiction — the page index is ZERO-based
|
|
2612
2657
|
// (like data.list). Say so at the parameter itself.
|
|
@@ -2621,13 +2666,53 @@ export function registerWorkflowLane(tools) {
|
|
|
2621
2666
|
capability: capabilityArg,
|
|
2622
2667
|
},
|
|
2623
2668
|
handler: async (ctx, args, self) => governedRead(ctx, self, args, async (session) => {
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2669
|
+
// `participant` takes the JPA-spec endpoint: it can answer "what is
|
|
2670
|
+
// pending for user X" and it does not resolve templates, so a dangling
|
|
2671
|
+
// reference elsewhere cannot take it down.
|
|
2672
|
+
let page;
|
|
2673
|
+
if (args.participant !== undefined) {
|
|
2674
|
+
page = await listWorkItemsByParticipant(session, {
|
|
2675
|
+
participant: args.participant,
|
|
2676
|
+
...(args.state !== undefined ? { status: args.state } : {}),
|
|
2677
|
+
...(args.workflowCode !== undefined ? { workflowCode: args.workflowCode } : {}),
|
|
2678
|
+
page: args.page ?? 0,
|
|
2679
|
+
size: args.size ?? 20,
|
|
2680
|
+
});
|
|
2681
|
+
}
|
|
2682
|
+
else {
|
|
2683
|
+
try {
|
|
2684
|
+
page = await searchWorkItems(session, {
|
|
2685
|
+
...(args.workflowInstanceId ? { instanceId: args.workflowInstanceId } : {}),
|
|
2686
|
+
...(args.workflowCode ? { workflowCode: args.workflowCode } : {}),
|
|
2687
|
+
...(args.schemaCode ? { schemaCode: args.schemaCode } : {}),
|
|
2688
|
+
page: args.page ?? 0,
|
|
2689
|
+
size: args.size ?? (args.workflowInstanceId ? 200 : 20),
|
|
2690
|
+
});
|
|
2691
|
+
}
|
|
2692
|
+
catch (error) {
|
|
2693
|
+
// r14: one work item pointing at a missing workflow-template row makes
|
|
2694
|
+
// the WHOLE search NPE (buildTempEntity → BeanUtils.copyProperties
|
|
2695
|
+
// (null,…)). The probe hit this eight times consecutively because the
|
|
2696
|
+
// message said only "50000 服务异常". Name the cause and the way round
|
|
2697
|
+
// it instead of leaving a dead end.
|
|
2698
|
+
if (!(error instanceof WireError) || error.kind !== "business" || error.errcode !== 50000)
|
|
2699
|
+
throw error;
|
|
2700
|
+
throw new ToolRefusalError({
|
|
2701
|
+
status: "refused",
|
|
2702
|
+
blocker: "workitem-search-unavailable",
|
|
2703
|
+
retryable: false,
|
|
2704
|
+
cause: "平台全局待办搜索会给每条待办解析流程模板(buildTempEntity);只要有一条待办指向已删除的 h_workflow_template 行,整页查询就抛 NPE → 50000。这是平台侧脏引用,不是本工具的入参问题。",
|
|
2705
|
+
workaround: [
|
|
2706
|
+
"① 按流程收窄(这一步不解析模板,最可靠):omc call workitem.list --env <env> --args '{\"workflowCode\":\"<流程code>\"}'",
|
|
2707
|
+
"② 按人查(能查别人名下的待办):omc call workitem.list --env <env> --args '{\"participant\":\"<用户id>\"}'",
|
|
2708
|
+
"③ 只看某实例走到哪:omc call workflow.instanceActivity --env <env> --args '{\"workflowInstanceId\":\"<实例id>\"}'",
|
|
2709
|
+
],
|
|
2710
|
+
message: "平台全局待办搜索当前不可用(50000):有一条待办指向已删除的流程模板,整页解析直接 NPE——**与你传的参数无关,重试不会变好**。" +
|
|
2711
|
+
"改用带范围的查询:按 `workflowCode` 收窄,或按 `participant` 查某人的待办(这两条走 JPA 查询,不做模板解析)。" +
|
|
2712
|
+
"若要彻底恢复全局搜索,需要维护者清掉那条脏待办(平台侧数据修复)。",
|
|
2713
|
+
});
|
|
2714
|
+
}
|
|
2715
|
+
}
|
|
2631
2716
|
const selected = page.items;
|
|
2632
2717
|
// Resolve the in-flight bizObject id per distinct instance. Authoritative
|
|
2633
2718
|
// source: getBizObjectByWfInstanceId -> BizObjectCreatedModel.id/schemaCode.
|
|
@@ -2667,10 +2752,12 @@ export function registerWorkflowLane(tools) {
|
|
|
2667
2752
|
: undefined;
|
|
2668
2753
|
return {
|
|
2669
2754
|
total: page.total,
|
|
2670
|
-
|
|
2755
|
+
// The JPA-spec path reports no page count; only emit it when known.
|
|
2756
|
+
...("totalPages" in page && page.totalPages !== undefined ? { totalPages: page.totalPages } : {}),
|
|
2671
2757
|
count: items.length,
|
|
2672
2758
|
...(purposeHint ? { purposeHint } : {}),
|
|
2673
|
-
...(
|
|
2759
|
+
...(args.participant !== undefined ? { participant: args.participant, state: args.state ?? "PENDING", source: "list_workitems" } : {}),
|
|
2760
|
+
...("unfilteredTotal" in page && page.unfilteredTotal !== undefined ? { unfilteredTotal: page.unfilteredTotal } : {}),
|
|
2674
2761
|
items,
|
|
2675
2762
|
};
|
|
2676
2763
|
}),
|
package/dist/model-scaffold.js
CHANGED
|
@@ -227,7 +227,18 @@ export async function cmdModelScaffold(args) {
|
|
|
227
227
|
steps.push({ step: "child:attach", status: "failed", detail: { parent: code, child: child.code, message: reason } });
|
|
228
228
|
continue;
|
|
229
229
|
}
|
|
230
|
-
steps.push({
|
|
230
|
+
steps.push({
|
|
231
|
+
step: "child:attach",
|
|
232
|
+
status: parentOutcome.ok ? "ok" : "skipped",
|
|
233
|
+
// r14: `skipped` on both the attach and the child columns read as "the
|
|
234
|
+
// sub-columns never got created", when in fact attaching the parent
|
|
235
|
+
// CHILD_TABLE creates them. Say so on the step itself.
|
|
236
|
+
detail: {
|
|
237
|
+
parent: code,
|
|
238
|
+
child: child.code,
|
|
239
|
+
note: parentOutcome.ok ? "子表字段与子列在这一次 attach 中一起建立;随后的 child:field.create 返回 already-present 是正常的" : "子表已存在(含子列),跳过",
|
|
240
|
+
},
|
|
241
|
+
});
|
|
231
242
|
await createFields(child.code, scaffoldChildrenOf(child), "child");
|
|
232
243
|
}
|
|
233
244
|
// 4) 发布字段:半成品不发布(比不发布更难查)。
|
package/dist/runtime-support.js
CHANGED
|
@@ -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/dist/workflow-support.js
CHANGED
|
@@ -182,4 +182,38 @@ export function diagnoseInstanceActivity(input) {
|
|
|
182
182
|
warnings: fallbackSuspect ? ["participant-fallback-to-originator"] : [],
|
|
183
183
|
};
|
|
184
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Work items for a SPECIFIC participant, from the JPA-spec endpoint.
|
|
187
|
+
*
|
|
188
|
+
* Two jobs the global search cannot do (r14, 2026-10-24):
|
|
189
|
+
* 1. survive a dangling template reference — `search_workitems` resolves each
|
|
190
|
+
* hit's template and NPEs the entire page when one is missing (a stale
|
|
191
|
+
* `inspHazardFlow` made every unscoped call answer 50000, 8× in a row);
|
|
192
|
+
* 2. answer "what is pending for user X". `search_workitems` filters to the
|
|
193
|
+
* logged-in user server-side, so an approval held by someone else is
|
|
194
|
+
* invisible — which is exactly the question the approval flow needs.
|
|
195
|
+
*/
|
|
196
|
+
export async function listWorkItemsByParticipant(session, filter) {
|
|
197
|
+
const query = {
|
|
198
|
+
participant: filter.participant,
|
|
199
|
+
status: filter.status ?? "PENDING",
|
|
200
|
+
page: String(filter.page ?? 0),
|
|
201
|
+
size: String(filter.size ?? 200),
|
|
202
|
+
participantUnitType: "3",
|
|
203
|
+
};
|
|
204
|
+
if (filter.workflowCode)
|
|
205
|
+
query.workflowCode = filter.workflowCode;
|
|
206
|
+
const data = (await wireCall(session, { path: ENDPOINTS.workflowListWorkItems, query })).data;
|
|
207
|
+
const page = data?.content !== undefined ? data : data;
|
|
208
|
+
const rows = Array.isArray(page?.content)
|
|
209
|
+
? page.content
|
|
210
|
+
: wireRows(page);
|
|
211
|
+
const total = Number(data?.totalElements ?? page?.totalElements ?? rows.length);
|
|
212
|
+
const totalPages = Number(data?.totalPages ?? page?.totalPages);
|
|
213
|
+
return {
|
|
214
|
+
total: Number.isFinite(total) ? total : rows.length,
|
|
215
|
+
...(Number.isFinite(totalPages) ? { totalPages } : {}),
|
|
216
|
+
items: rows,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
185
219
|
//# 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.
|
|
3
|
+
"version": "0.4.87",
|
|
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.87",
|
|
54
54
|
"commander": "14.0.3",
|
|
55
|
-
"omc-core": "0.4.
|
|
55
|
+
"omc-core": "0.4.87",
|
|
56
56
|
"proper-lockfile": "4.1.2",
|
|
57
57
|
"write-file-atomic": "6.0.0",
|
|
58
58
|
"zod": "^3.23.8",
|