@stalfh233/omc-cli 0.6.12 → 0.6.14
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/coverage/m1-coverage-manifest-v1.json +2 -0
- package/dist/approval-flow.js +84 -3
- package/dist/batch-call.js +32 -2
- package/dist/bench/call-face-eval.js +2 -2
- package/dist/call.js +35 -2
- package/dist/capability-overview.js +21 -7
- package/dist/cli-contract.js +8 -2
- package/dist/cli.js +313 -72
- package/dist/describe-cache.js +8 -4
- package/dist/discovery.js +4 -0
- package/dist/environment-policy.js +20 -0
- package/dist/field-families.js +62 -4
- package/dist/form-support.js +293 -6
- package/dist/goal-routes.js +137 -22
- package/dist/identity.js +63 -11
- package/dist/lanes/app-contract.js +4 -4
- package/dist/lanes/app.js +20 -2
- package/dist/lanes/dictionary-contract.js +16 -5
- package/dist/lanes/dictionary-coverage.js +1 -0
- package/dist/lanes/dictionary.js +200 -8
- package/dist/lanes/environment-contract.js +18 -6
- package/dist/lanes/environment.js +129 -16
- package/dist/lanes/field-contract.js +3 -2
- package/dist/lanes/field.js +785 -63
- package/dist/lanes/form.js +52 -6
- package/dist/lanes/list-view-contract.js +3 -3
- package/dist/lanes/list-view.js +143 -12
- package/dist/lanes/meta.js +36 -10
- package/dist/lanes/model-contract.js +7 -6
- package/dist/lanes/model.js +154 -10
- package/dist/lanes/organization.js +67 -7
- package/dist/lanes/runtime-data-contract.js +4 -2
- package/dist/lanes/runtime-data.js +395 -41
- package/dist/lanes/workflow-contract.js +2 -2
- package/dist/lanes/workflow.js +381 -11
- package/dist/model-scaffold.js +11 -3
- package/dist/path-projection.js +181 -31
- package/dist/read-shapes.js +31 -11
- package/dist/recipes/b2-registration.js +1 -1
- package/dist/recipes/field.js +16 -3
- package/dist/recipes/form.js +4 -3
- package/dist/recipes/runtime-data.js +58 -8
- package/dist/recipes/workflow.js +86 -1
- package/dist/refusal-guidance.js +18 -6
- package/dist/rollback-spec.js +41 -2
- package/dist/rollback.js +49 -11
- package/dist/script.js +72 -12
- package/dist/skills.js +91 -3
- package/dist/tools.js +113 -1
- package/dist/wire.js +13 -1
- package/dist/write-lock.js +92 -4
- package/dist/write.js +129 -16
- package/package.json +3 -3
- package/skills/omc-business-configuration.md +1 -1
- package/skills/omc-capabilities.md +5 -4
- package/skills/omc-playbook.md +41 -0
- package/skills/omc-refusals.md +58 -6
- package/skills/omc-tool-selection.md +2 -0
- package/skills/omc-write-hazards.md +3 -2
|
@@ -83,7 +83,7 @@ export const WORKFLOW_WIRE = Object.freeze({
|
|
|
83
83
|
/** Workflow template object (workflow/update|publish raw body). */
|
|
84
84
|
export const workflowTemplateArg = z
|
|
85
85
|
.record(z.string(), z.unknown())
|
|
86
|
-
.describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}.
|
|
86
|
+
.describe(`workflow template object. Minimal valid shape (create via workflow.create first, then workflow.update with this, then workflow.publish): ${WORKFLOW_WIRE.templateShape}. **连线写法**:routes(recipe)/ edges(同义)/ rules(平台 DTO 字段名,get_draft 原样读回)三选一,同传两种会被拒。**新节点必须带 type/activityType**(activityCode 命中草稿既有节点时可省)。**体积**:整模板含 propertyPermissions 常到 1.0–1.4MB,而 --args-file 上限 1MB(CLI_ARGS_MAX_BYTES=1048576)——只发改动节点/权限行,别回传整份正文。 **参与人解析陷阱(r5-crm 实证 2026-10-10)**:内置 FindUserByRole({Originator.OU},code,...) 按部门范围找人——目标角色**不挂部门**(如全公司唯一的总监)时 ParticipantActivityRuntime.initParticipants 抛 EngineRuntimeException、流程卡 PROCESSING 无待办;跨部门/全局角色**不要写 FindUsersSpecByRoleFunction(code)**——1.1.25 运行期未注册该函数(FunctionNotFoundException,接口仍回 errCode 0),改用参与者对象数组(角色 id)或给角色补上可覆盖的部门范围成员。解析为空(零成员/未挂部门/自由文本/角色码不存在)时引擎**把任务退回发起人**、流程继续,症状「审批人成了我自己」;真正卡住是参与人函数未注册(无待办/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 两步)。 routeBranch: ${WORKFLOW_WIRE.routeBranch}。endEventHandler{bizActions,bizActionType} 绑业务规则——用 workflow.bindRule(activityCode+nodeEvent),别替换整个模板。`);
|
|
87
87
|
const WORKFLOW_SCOPE = "greenfield workflow authoring";
|
|
88
88
|
export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
89
89
|
lane: LANES.workflow,
|
|
@@ -176,7 +176,7 @@ export const WORKFLOW_LANE_CONTRACT = Object.freeze({
|
|
|
176
176
|
}),
|
|
177
177
|
"workflow.permissions": Object.freeze({
|
|
178
178
|
scope: "workflow start permission (who may originate)",
|
|
179
|
-
description: "列出谁能发起该流程(`GET /api/api/workflow/list_permission
|
|
179
|
+
description: "列出谁能发起该流程(`GET /api/api/workflow/list_permission` → {id,name,unitType,fullName,corpId,code};1=部门 2=角色 3=用户;平台权限枚举 PART_VISIABLE)。回执带 `actor`/`effectiveFor`:管理员绕过白名单时 reason 标明,发起成功≠白名单生效。40009 先看这里;发布不会自动建行。",
|
|
180
180
|
}),
|
|
181
181
|
"workflow.addPermission": Object.freeze({
|
|
182
182
|
scope: "workflow start permission (who may originate)",
|
package/dist/lanes/workflow.js
CHANGED
|
@@ -14,6 +14,7 @@ import { readSchemaFormButtons } from "../form-support.js";
|
|
|
14
14
|
import { WORKFLOW_EVENT_HANDLER_FIELDS, WORKFLOW_NODE_EVENT_HANDLER_FIELDS, WORKFLOW_LANE_CONTRACT, workflowTemplateArg } from "./workflow-contract.js";
|
|
15
15
|
import { PLATFORM_TRIGGER_RULE_CODES } from "./rule-graph.js";
|
|
16
16
|
import { executeRuleEnableWrite } from "./rule-lifecycle.js";
|
|
17
|
+
import { PLATFORM_ADMIN_START_BYPASS, resolvePermissionActor } from "../identity.js";
|
|
17
18
|
const WORKFLOW_LANE = WORKFLOW_LANE_CONTRACT.lane;
|
|
18
19
|
const WORKFLOW_TOOLS = WORKFLOW_LANE_CONTRACT.tools;
|
|
19
20
|
const WORKFLOW_ACTIVITY_TYPES = new Set(["START", "END", "CONNECTION", "PARTICIPANT", "SUB_INSTANCE", "CIRCULATE", "SYSTEM_ACTIVITY", "AGENT_ACTIVITY"]);
|
|
@@ -1015,6 +1016,106 @@ function pickRouteSpelling(spellings, tool) {
|
|
|
1015
1016
|
* chain order" — reusing any edge whose endpoints survived. A draft whose
|
|
1016
1017
|
* (possibly branching) graph is untouched passes the chain check verbatim.
|
|
1017
1018
|
*/
|
|
1019
|
+
/**
|
|
1020
|
+
* r31c F23: local reachability / else-coverage warnings for workflow.validate.
|
|
1021
|
+
* Does NOT change `valid` (still ERROR-only). A node with only conditional
|
|
1022
|
+
* outbounds and no utilizeElse/unconditional edge is a dead-end when the
|
|
1023
|
+
* condition fails — platform validate still says valid:true.
|
|
1024
|
+
*/
|
|
1025
|
+
export function workflowReachabilityWarnings(template) {
|
|
1026
|
+
const activitiesRaw = template.activities;
|
|
1027
|
+
const activities = Array.isArray(activitiesRaw)
|
|
1028
|
+
? activitiesRaw
|
|
1029
|
+
: activitiesRaw && typeof activitiesRaw === "object"
|
|
1030
|
+
? Object.values(activitiesRaw)
|
|
1031
|
+
: [];
|
|
1032
|
+
if (activities.length === 0)
|
|
1033
|
+
return [];
|
|
1034
|
+
const rulesRaw = template.rules ?? template.routes ?? template.edges;
|
|
1035
|
+
const rules = Array.isArray(rulesRaw) ? rulesRaw : [];
|
|
1036
|
+
const typeOf = new Map();
|
|
1037
|
+
const nameOf = new Map();
|
|
1038
|
+
for (const activity of activities) {
|
|
1039
|
+
const code = String(activity.activityCode ?? "").trim();
|
|
1040
|
+
if (!code)
|
|
1041
|
+
continue;
|
|
1042
|
+
typeOf.set(code, String(activity.activityType ?? "").trim());
|
|
1043
|
+
nameOf.set(code, String(activity.activityName ?? activity.name ?? code));
|
|
1044
|
+
}
|
|
1045
|
+
const outbound = new Map();
|
|
1046
|
+
for (const rule of rules) {
|
|
1047
|
+
const pre = String(rule.preActivityCode ?? rule.preNode ?? rule.pre ?? rule.from ?? "").trim();
|
|
1048
|
+
const post = String(rule.postActivityCode ?? rule.postNode ?? rule.post ?? rule.to ?? "").trim();
|
|
1049
|
+
if (!pre || !post)
|
|
1050
|
+
continue;
|
|
1051
|
+
const hasGroup = Array.isArray(rule.ruleConditionGroup) && rule.ruleConditionGroup.length > 0;
|
|
1052
|
+
const utilizeElse = rule.utilizeElse === true;
|
|
1053
|
+
const list = outbound.get(pre) ?? [];
|
|
1054
|
+
list.push({ post, conditional: hasGroup && !utilizeElse, utilizeElse });
|
|
1055
|
+
outbound.set(pre, list);
|
|
1056
|
+
}
|
|
1057
|
+
const warnings = [];
|
|
1058
|
+
// 1) Nodes with conditional outbounds must also have an else / unconditional exit.
|
|
1059
|
+
for (const [code, edges] of outbound) {
|
|
1060
|
+
if (typeOf.get(code) === "END")
|
|
1061
|
+
continue;
|
|
1062
|
+
const hasConditional = edges.some((edge) => edge.conditional);
|
|
1063
|
+
if (!hasConditional)
|
|
1064
|
+
continue;
|
|
1065
|
+
const hasFallback = edges.some((edge) => edge.utilizeElse || !edge.conditional);
|
|
1066
|
+
if (!hasFallback) {
|
|
1067
|
+
warnings.push({
|
|
1068
|
+
activityCode: code,
|
|
1069
|
+
name: nameOf.get(code) ?? code,
|
|
1070
|
+
problem: `节点 ${code} 在条件不满足时无出路(流程断头):有条件出边但没有 utilizeElse/无条件兜底`,
|
|
1071
|
+
});
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
// 2) Every non-END node must be able to reach some END along ANY outbound edge.
|
|
1075
|
+
const endCodes = [...typeOf.entries()].filter(([, type]) => type === "END").map(([code]) => code);
|
|
1076
|
+
if (endCodes.length === 0)
|
|
1077
|
+
return warnings;
|
|
1078
|
+
const canReachEnd = (start) => {
|
|
1079
|
+
const seen = new Set();
|
|
1080
|
+
const stack = [start];
|
|
1081
|
+
while (stack.length > 0) {
|
|
1082
|
+
const current = stack.pop();
|
|
1083
|
+
if (typeOf.get(current) === "END")
|
|
1084
|
+
return true;
|
|
1085
|
+
if (seen.has(current))
|
|
1086
|
+
continue;
|
|
1087
|
+
seen.add(current);
|
|
1088
|
+
for (const edge of outbound.get(current) ?? []) {
|
|
1089
|
+
if (!seen.has(edge.post))
|
|
1090
|
+
stack.push(edge.post);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
return false;
|
|
1094
|
+
};
|
|
1095
|
+
for (const [code, type] of typeOf) {
|
|
1096
|
+
if (type === "END")
|
|
1097
|
+
continue;
|
|
1098
|
+
if ((outbound.get(code) ?? []).length === 0) {
|
|
1099
|
+
// START with no edges is already a platform ERROR; still warn for PARTICIPANT dead-ends.
|
|
1100
|
+
if (type === "PARTICIPANT" || type === "SUB_INSTANCE" || type === "SYSTEM") {
|
|
1101
|
+
warnings.push({
|
|
1102
|
+
activityCode: code,
|
|
1103
|
+
name: nameOf.get(code) ?? code,
|
|
1104
|
+
problem: `节点 ${code} 没有任何出向边,无法到达 END(流程断头)`,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
if (!canReachEnd(code)) {
|
|
1110
|
+
warnings.push({
|
|
1111
|
+
activityCode: code,
|
|
1112
|
+
name: nameOf.get(code) ?? code,
|
|
1113
|
+
problem: `节点 ${code} 沿现有出向边无法到达 END(流程断头)`,
|
|
1114
|
+
});
|
|
1115
|
+
}
|
|
1116
|
+
}
|
|
1117
|
+
return warnings;
|
|
1118
|
+
}
|
|
1018
1119
|
export function completeWorkflowChain(rules, activities) {
|
|
1019
1120
|
const codes = activities.map((activity) => String(activity.activityCode ?? "")).filter((code) => code.length > 0);
|
|
1020
1121
|
if (codes.length < 2)
|
|
@@ -1977,6 +2078,23 @@ export function workflowCompactProjection(result) {
|
|
|
1977
2078
|
}
|
|
1978
2079
|
}
|
|
1979
2080
|
const originate = data.originateActivity;
|
|
2081
|
+
// Count ALL propertyPermissions rows on the native template (including
|
|
2082
|
+
// all-false), so --full/--jq can answer "did my visibility edit land?" even
|
|
2083
|
+
// when the compact permissions face filters defaults out.
|
|
2084
|
+
const propertyPermissionsCount = Object.values(activities).reduce((sum, activity) => {
|
|
2085
|
+
const rows = activity.propertyPermissions;
|
|
2086
|
+
return sum + (Array.isArray(rows) ? rows.length : 0);
|
|
2087
|
+
}, 0);
|
|
2088
|
+
// r30d F14: --full externalizes the native body; keep a projectable top-level
|
|
2089
|
+
// activities summary so callers do not need four hops (describe → get --full
|
|
2090
|
+
// → pathMiss → artifact get) just to see node codes / participants.
|
|
2091
|
+
const activitySummaries = nodes.map((node) => ({
|
|
2092
|
+
code: node.code,
|
|
2093
|
+
name: node.name,
|
|
2094
|
+
type: node.type,
|
|
2095
|
+
...(node.participant !== undefined ? { participants: node.participant } : {}),
|
|
2096
|
+
...(node.permissions !== undefined ? { propertyPermissions: node.permissions } : {}),
|
|
2097
|
+
}));
|
|
1980
2098
|
return {
|
|
1981
2099
|
workflowSummary: {
|
|
1982
2100
|
workflowCode: data.workflowCode ?? result.workflowCode ?? null,
|
|
@@ -1988,11 +2106,15 @@ export function workflowCompactProjection(result) {
|
|
|
1988
2106
|
ruleBindingCount: ruleBindings.reduce((sum, binding) => sum + binding.ruleCodes.length, 0),
|
|
1989
2107
|
...(originate ? { originateActivity: { code: originate.activityCode ?? null, type: originate.activityType ?? null, name: effectiveActivityName(originate), participant: originate.participant ?? null } } : {}),
|
|
1990
2108
|
},
|
|
2109
|
+
// Projectable top-level face for --full / --jq (r30d F9/F14).
|
|
2110
|
+
activities: activitySummaries,
|
|
2111
|
+
propertyPermissionsCount,
|
|
2112
|
+
version: data.workflowVersion ?? data.version ?? null,
|
|
1991
2113
|
// Editing needs only `activities` (array or index object) + `routes`: the tool
|
|
1992
2114
|
// merges onto the stored draft, connects added nodes through to END, fills
|
|
1993
2115
|
// sheetCode and geometry. Reading the 30–75 KB full body is NOT required —
|
|
1994
2116
|
// bench17 pulled 61 KB `full:true` just to edit two node names.
|
|
1995
|
-
summaryHint: "改流程只需把要改/新增的 activities(数组)+ routes 传给 workflow.update:缺 START/END 的部分模板会自动按增量 merge 到草稿(草稿为基,自动补 sheetCode/连线/坐标),无需读回整模板或 --full 正文;字段权限也只需在对应 PARTICIPANT 上给 propertyPermissions:[{propertyCode,...}],工具按 propertyCode 合并、未列的字段权限保留。只有要整图替换时才给全 START..END 的完整模板;要看通知等完整节点正文才用 detailArtifactId 或 --full",
|
|
2117
|
+
summaryHint: "改流程只需把要改/新增的 activities(数组)+ routes 传给 workflow.update:缺 START/END 的部分模板会自动按增量 merge 到草稿(草稿为基,自动补 sheetCode/连线/坐标),无需读回整模板或 --full 正文;字段权限也只需在对应 PARTICIPANT 上给 propertyPermissions:[{propertyCode,...}],工具按 propertyCode 合并、未列的字段权限保留。只有要整图替换时才给全 START..END 的完整模板;要看通知等完整节点正文才用 detailArtifactId 或 --full。--full 外置时顶层仍有 activities[{code,name,participants}] + propertyPermissionsCount 可直接投影。",
|
|
1996
2118
|
};
|
|
1997
2119
|
}
|
|
1998
2120
|
/**
|
|
@@ -2223,7 +2345,28 @@ export function registerWorkflowLane(tools) {
|
|
|
2223
2345
|
const messages = Array.isArray(result.data) ? result.data : [];
|
|
2224
2346
|
const errors = messages.filter((message) => message.errorLevel === "ERROR");
|
|
2225
2347
|
const warnings = messages.filter((message) => message.errorLevel !== "ERROR");
|
|
2226
|
-
|
|
2348
|
+
// r31c F23: local reachability / else-coverage — warning only, never flips valid.
|
|
2349
|
+
const reachability = workflowReachabilityWarnings(body);
|
|
2350
|
+
const allWarnings = [
|
|
2351
|
+
...warnings,
|
|
2352
|
+
...reachability.map((entry) => ({
|
|
2353
|
+
errorLevel: "WARNING",
|
|
2354
|
+
errCode: "omc-reachability",
|
|
2355
|
+
errContent: entry.problem,
|
|
2356
|
+
activityCode: entry.activityCode,
|
|
2357
|
+
name: entry.name,
|
|
2358
|
+
problem: entry.problem,
|
|
2359
|
+
})),
|
|
2360
|
+
];
|
|
2361
|
+
return {
|
|
2362
|
+
workflowCode,
|
|
2363
|
+
valid: errors.length === 0,
|
|
2364
|
+
errorCount: errors.length,
|
|
2365
|
+
warningCount: allWarnings.length,
|
|
2366
|
+
errors,
|
|
2367
|
+
warnings: allWarnings,
|
|
2368
|
+
...(reachability.length > 0 ? { reachabilityWarnings: reachability } : {}),
|
|
2369
|
+
};
|
|
2227
2370
|
}, { target: `workflow.validate:${args.workflowCode ?? args.schemaCode}` }),
|
|
2228
2371
|
}));
|
|
2229
2372
|
writeTool({
|
|
@@ -2808,6 +2951,51 @@ export function registerWorkflowLane(tools) {
|
|
|
2808
2951
|
`正确顺序:① 先完结/清理实例的待办(workitem.list --workflowCode ${args.code} 看还有哪些在办);② 再 workflow.delete;③ 最后才 model.delete。`,
|
|
2809
2952
|
});
|
|
2810
2953
|
}
|
|
2954
|
+
// 303024 列表绑定了流程 — workflow.create auto-binds the default list
|
|
2955
|
+
// view; delete refuses until that WORKFLOW association is removed
|
|
2956
|
+
// (r30d F12/F13; r31 F48). Name the bound view + a pasteable unbind
|
|
2957
|
+
// command that already carries real fieldCodes (no listview.get dig).
|
|
2958
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 303024) {
|
|
2959
|
+
const bound = await findListviewsBoundToWorkflow(session, args.schemaCode, args.code);
|
|
2960
|
+
const primary = bound[0];
|
|
2961
|
+
const viewCode = primary?.code ?? args.schemaCode;
|
|
2962
|
+
const fieldCodes = primary?.fieldCodes?.length
|
|
2963
|
+
? primary.fieldCodes
|
|
2964
|
+
: ["createdTime", "creater", "name", "sequenceNo"];
|
|
2965
|
+
// Live-proven unbind (sandbox-r31f 2026-11-05): empty actions +
|
|
2966
|
+
// replaceActions:true clears the WORKFLOW add binding; platform then
|
|
2967
|
+
// re-injects non-workflow defaults. Prefer this over rewriting ADD→BIZSHEET
|
|
2968
|
+
// so the pasted command needs no action surgery.
|
|
2969
|
+
const detachArgs = {
|
|
2970
|
+
schemaCode: args.schemaCode,
|
|
2971
|
+
code: viewCode,
|
|
2972
|
+
fieldCodes,
|
|
2973
|
+
actions: [],
|
|
2974
|
+
replaceActions: true,
|
|
2975
|
+
};
|
|
2976
|
+
const detachExample = `omc call listview.configure --env <env> --args '${JSON.stringify(detachArgs)}'`;
|
|
2977
|
+
throw new ToolRefusalError({
|
|
2978
|
+
status: "refused",
|
|
2979
|
+
blocker: "workflow-bound-to-listview",
|
|
2980
|
+
errcode: 303024,
|
|
2981
|
+
workflowCode: args.code,
|
|
2982
|
+
schemaCode: args.schemaCode,
|
|
2983
|
+
boundListviews: bound.map(({ code, name, actionCode, associationCode, fieldCodes: cols }) => ({
|
|
2984
|
+
code,
|
|
2985
|
+
name,
|
|
2986
|
+
actionCode,
|
|
2987
|
+
associationCode,
|
|
2988
|
+
...(cols.length > 0 ? { fieldCodes: cols } : {}),
|
|
2989
|
+
})),
|
|
2990
|
+
nextCommand: detachExample,
|
|
2991
|
+
message: `流程 ${args.code} 仍被列表视图绑定(303024「列表绑定了流程」)。` +
|
|
2992
|
+
(bound.length > 0
|
|
2993
|
+
? `绑在:${bound.map((view) => `${view.code}(${view.name})action=${view.actionCode ?? "?"}`).join("、")}。`
|
|
2994
|
+
: `默认视图多半是 ${args.schemaCode}(workflow.create 会自动绑)。`) +
|
|
2995
|
+
`先解绑再删:粘贴 nextCommand(listview.configure:actions:[] + replaceActions:true + 现有 fieldCodes),再重试 workflow.delete。` +
|
|
2996
|
+
`整体清理也可用 omc model-purge --code ${args.schemaCode}(级联带走流程模板与视图)。`,
|
|
2997
|
+
});
|
|
2998
|
+
}
|
|
2811
2999
|
throw error;
|
|
2812
3000
|
}
|
|
2813
3001
|
const header = await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode: args.code } }).catch(() => ({ data: null }));
|
|
@@ -3085,6 +3273,54 @@ export function registerWorkflowLane(tools) {
|
|
|
3085
3273
|
// Publishing a flow creates NO permission row, so a freshly published flow is
|
|
3086
3274
|
// unusable by anyone who is not a platform admin.
|
|
3087
3275
|
const permissionRows = async (session, workflowCode) => wireRows((await wireCall(session, { path: ENDPOINTS.workflowListPermission, query: { workflowCode } })).data);
|
|
3276
|
+
/** Best-effort: which list views still bind this workflow (303024 recovery). */
|
|
3277
|
+
async function findListviewsBoundToWorkflow(session, schemaCode, workflowCode) {
|
|
3278
|
+
const bound = [];
|
|
3279
|
+
try {
|
|
3280
|
+
const listed = wireRows((await wireCall(session, { path: ENDPOINTS.listQueryList, query: { schemaCode, clientType: "PC" } })).data);
|
|
3281
|
+
for (const row of listed) {
|
|
3282
|
+
const code = String(row.code ?? "");
|
|
3283
|
+
if (!code)
|
|
3284
|
+
continue;
|
|
3285
|
+
try {
|
|
3286
|
+
const got = await wireCall(session, { path: ENDPOINTS.listQueryGet, query: { schemaCode, code, clientType: "PC" } });
|
|
3287
|
+
const model = (got.data ?? null);
|
|
3288
|
+
if (!model)
|
|
3289
|
+
continue;
|
|
3290
|
+
const actions = Array.isArray(model.queryActions) ? model.queryActions : [];
|
|
3291
|
+
const fieldCodes = (Array.isArray(model.queryColumns) ? model.queryColumns : [])
|
|
3292
|
+
.map((column) => String(column.propertyCode ?? ""))
|
|
3293
|
+
.filter(Boolean);
|
|
3294
|
+
for (const action of actions) {
|
|
3295
|
+
const association = action.associationType ?? action.queryActionRelativeType;
|
|
3296
|
+
const associationCode = String(action.associationCode ?? action.relativeCode ?? "");
|
|
3297
|
+
const isWorkflow = association === 1 ||
|
|
3298
|
+
association === "1" ||
|
|
3299
|
+
association === "WORKFLOW" ||
|
|
3300
|
+
String(association).toUpperCase() === "WORKFLOW";
|
|
3301
|
+
if (!isWorkflow)
|
|
3302
|
+
continue;
|
|
3303
|
+
if (associationCode && associationCode !== workflowCode && associationCode !== schemaCode)
|
|
3304
|
+
continue;
|
|
3305
|
+
bound.push({
|
|
3306
|
+
code,
|
|
3307
|
+
name: String(row.name ?? model.name ?? code),
|
|
3308
|
+
actionCode: action.actionCode === undefined || action.actionCode === null ? null : String(action.actionCode),
|
|
3309
|
+
associationCode: associationCode || null,
|
|
3310
|
+
fieldCodes,
|
|
3311
|
+
});
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
catch {
|
|
3315
|
+
// one view unreadable — keep scanning
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
}
|
|
3319
|
+
catch {
|
|
3320
|
+
// list unreadable — caller still gets a generic detach recipe
|
|
3321
|
+
}
|
|
3322
|
+
return bound;
|
|
3323
|
+
}
|
|
3088
3324
|
// `list_permission` echoes the PERMISSION row id plus the unit's display name
|
|
3089
3325
|
// and code — never the unitId (WorkflowTemplateController.getPermissionMap).
|
|
3090
3326
|
// So identity for readback/removal is resolved from the unit's own face.
|
|
@@ -3144,17 +3380,35 @@ export function registerWorkflowLane(tools) {
|
|
|
3144
3380
|
corpId: row.corpId ?? null,
|
|
3145
3381
|
code: row.code ?? null,
|
|
3146
3382
|
}));
|
|
3383
|
+
const actor = await resolvePermissionActor(session);
|
|
3384
|
+
const effectiveFor = {
|
|
3385
|
+
userId: actor.userId,
|
|
3386
|
+
name: actor.name,
|
|
3387
|
+
allowed: actor.isAdmin || permissions.length > 0,
|
|
3388
|
+
reason: actor.isAdmin
|
|
3389
|
+
? PLATFORM_ADMIN_START_BYPASS
|
|
3390
|
+
: permissions.length === 0
|
|
3391
|
+
? "no-start-permission-row"
|
|
3392
|
+
: "whitelist-configured-not-evaluated-for-membership",
|
|
3393
|
+
note: actor.isAdmin
|
|
3394
|
+
? "当前登录人是平台管理员:发起成功不证明白名单生效。要用非管理员账号另建环境验证。"
|
|
3395
|
+
: "本读面只回配置行;membership 命中需非管理员实发 data.submit 才能证伪。",
|
|
3396
|
+
};
|
|
3147
3397
|
return {
|
|
3148
3398
|
workflowCode,
|
|
3149
3399
|
count: permissions.length,
|
|
3150
3400
|
permissions,
|
|
3401
|
+
actor,
|
|
3402
|
+
effectiveFor,
|
|
3151
3403
|
...(permissions.length === 0
|
|
3152
3404
|
? {
|
|
3153
3405
|
blocker: "workflow-start-permission-empty",
|
|
3154
|
-
note: "该流程没有任何发起权限行:除平台管理员(h_perm_admin 里 ADMIN/SYS_MNG)外**没有人能发起**,data.submit 会回 40009「流程发起权限不足」。发布流程不会自动建这一行,用 workflow.addPermission 授给部门/角色/用户(或把模板 visibleType 改成 ALL_VISIABLE
|
|
3406
|
+
note: "该流程没有任何发起权限行:除平台管理员(h_perm_admin 里 ADMIN/SYS_MNG)外**没有人能发起**,data.submit 会回 40009「流程发起权限不足」。发布流程不会自动建这一行,用 workflow.addPermission 授给部门/角色/用户(或把模板 visibleType 改成 ALL_VISIABLE)。管理员发起成功 ≠ 白名单生效。",
|
|
3155
3407
|
nextCommand: `omc call workflow.addPermission --env <env> --args '{"workflowCode":"${workflowCode}","unitId":"<部门|角色|用户 id>","unitType":1}'`,
|
|
3156
3408
|
}
|
|
3157
|
-
: {
|
|
3409
|
+
: {
|
|
3410
|
+
note: "permissions 是配置白名单。若 actor.isAdmin=true,data.submit 成功可能只是管理员绕过,不证明白名单对普通用户生效。",
|
|
3411
|
+
}),
|
|
3158
3412
|
};
|
|
3159
3413
|
}),
|
|
3160
3414
|
}));
|
|
@@ -3210,13 +3464,15 @@ export function registerWorkflowLane(tools) {
|
|
|
3210
3464
|
unitType,
|
|
3211
3465
|
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)}`,
|
|
3212
3466
|
});
|
|
3467
|
+
const actor = await resolvePermissionActor(session);
|
|
3213
3468
|
return {
|
|
3214
3469
|
performed: { skipped: "already-present" },
|
|
3215
|
-
readback: { workflowCode, alreadyPresent: true, unitId: args.unitId, unitType, count: before.length, note: "该主体已有发起权限(平台 402003),未重复写入" },
|
|
3470
|
+
readback: { workflowCode, alreadyPresent: true, unitId: args.unitId, unitType, count: before.length, note: "该主体已有发起权限(平台 402003),未重复写入", actor },
|
|
3216
3471
|
};
|
|
3217
3472
|
}
|
|
3218
3473
|
const after = await permissionRows(session, workflowCode);
|
|
3219
3474
|
const created = after.find((row) => !before.some((old) => String(old.id ?? "") === String(row.id ?? ""))) ?? after[after.length - 1];
|
|
3475
|
+
const actor = await resolvePermissionActor(session);
|
|
3220
3476
|
return {
|
|
3221
3477
|
performed: { errcode: 0 },
|
|
3222
3478
|
readback: {
|
|
@@ -3225,10 +3481,28 @@ export function registerWorkflowLane(tools) {
|
|
|
3225
3481
|
permissionId: created?.id ?? null,
|
|
3226
3482
|
unitType: created?.unitType ?? unitType,
|
|
3227
3483
|
name: created?.name ?? null,
|
|
3228
|
-
|
|
3484
|
+
actor,
|
|
3485
|
+
note: "发起权限已授予。若之前 data.submit 回 40009,现在应可发起;仍失败先 workflow.permissions 确认行存在,并核对发起人所属部门/角色。管理员发起成功不证明白名单对普通人生效。",
|
|
3229
3486
|
},
|
|
3230
3487
|
};
|
|
3231
3488
|
},
|
|
3489
|
+
rollback: {
|
|
3490
|
+
target: (args) => `workflow-permission:${String(args.workflowCode ?? args.schemaCode ?? "")}`,
|
|
3491
|
+
read: async (session, args) => {
|
|
3492
|
+
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode });
|
|
3493
|
+
if (!workflowCode)
|
|
3494
|
+
return null;
|
|
3495
|
+
return permissionRows(session, workflowCode);
|
|
3496
|
+
},
|
|
3497
|
+
inverse: ({ before, args, readback }) => {
|
|
3498
|
+
if (readback.alreadyPresent === true)
|
|
3499
|
+
return { kind: "calls", calls: [], notes: ["权限行已存在(幂等),未写入,无需回滚"] };
|
|
3500
|
+
const permissionId = readback.permissionId;
|
|
3501
|
+
if (typeof permissionId === "string" && permissionId.length > 0)
|
|
3502
|
+
return { kind: "calls", calls: [{ tool: "workflow.removePermission", args: { permissionId } }] };
|
|
3503
|
+
return { kind: "irreversible", reason: "addPermission 回执缺少 permissionId,无法自动撤权;用 workflow.permissions 找 id 后手工 removePermission" };
|
|
3504
|
+
},
|
|
3505
|
+
},
|
|
3232
3506
|
});
|
|
3233
3507
|
writeTool({
|
|
3234
3508
|
tools,
|
|
@@ -3245,8 +3519,11 @@ export function registerWorkflowLane(tools) {
|
|
|
3245
3519
|
},
|
|
3246
3520
|
execute: async (session, args) => {
|
|
3247
3521
|
let permissionId = args.permissionId;
|
|
3522
|
+
let workflowCodeForActor;
|
|
3523
|
+
let beforeRow = null;
|
|
3248
3524
|
if (!permissionId) {
|
|
3249
3525
|
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode });
|
|
3526
|
+
workflowCodeForActor = workflowCode ?? undefined;
|
|
3250
3527
|
if (!workflowCode || !args.unitId || args.unitType === undefined)
|
|
3251
3528
|
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "workflow.removePermission", message: "需要 permissionId(推荐),或 workflowCode(/schemaCode)+ unitId + unitType" });
|
|
3252
3529
|
const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
|
|
@@ -3254,22 +3531,115 @@ export function registerWorkflowLane(tools) {
|
|
|
3254
3531
|
const rows = await permissionRows(session, workflowCode);
|
|
3255
3532
|
const wantedType = String(unitType);
|
|
3256
3533
|
const hit = rows.find((row) => String(row.unitType ?? "") === wantedType && (String(row.id ?? "") === args.unitId ||
|
|
3534
|
+
// list_permission echoes `code` (unit business code or, on some faces, the
|
|
3535
|
+
// unitId itself) but never unitId — accept both spellings so remove by
|
|
3536
|
+
// unitId does not silently miss (r30d F11 / fake+live).
|
|
3537
|
+
String(row.code ?? "") === args.unitId ||
|
|
3257
3538
|
(identity.name !== null && String(row.name ?? "") === identity.name) ||
|
|
3258
|
-
(identity.code !== null && String(row.code ?? "") === identity.code)
|
|
3259
|
-
|
|
3539
|
+
(identity.code !== null && String(row.code ?? "") === identity.code) ||
|
|
3540
|
+
(identity.name !== null && String(row.fullName ?? "") === identity.name)));
|
|
3541
|
+
if (!hit) {
|
|
3542
|
+
const actor = await resolvePermissionActor(session);
|
|
3260
3543
|
return {
|
|
3261
3544
|
performed: { skipped: "absent" },
|
|
3262
3545
|
readback: {
|
|
3263
3546
|
workflowCode,
|
|
3264
3547
|
removed: false,
|
|
3265
|
-
note: `没有匹配该主体的权限行(幂等)。注意 list_permission 不回 unitId,匹配靠解析出的显示名「${identity.name ?? "?"}
|
|
3266
|
-
permissions: rows.map((row) => ({ id: row.id ?? null, name: row.name ?? null, unitType: row.unitType ?? null })),
|
|
3548
|
+
note: `没有匹配该主体的权限行(幂等)。注意 list_permission 不回 unitId,匹配靠解析出的显示名「${identity.name ?? "?"}」/code;不确定时用 workflow.permissions 拿 id 再按 permissionId 删。`,
|
|
3549
|
+
permissions: rows.map((row) => ({ id: row.id ?? null, name: row.name ?? null, unitType: row.unitType ?? null, code: row.code ?? null })),
|
|
3550
|
+
actor,
|
|
3267
3551
|
},
|
|
3268
3552
|
};
|
|
3553
|
+
}
|
|
3554
|
+
beforeRow = {
|
|
3555
|
+
id: hit.id ?? null,
|
|
3556
|
+
name: hit.name ?? null,
|
|
3557
|
+
unitType: hit.unitType ?? unitType,
|
|
3558
|
+
fullName: hit.fullName ?? null,
|
|
3559
|
+
code: hit.code ?? null,
|
|
3560
|
+
corpId: hit.corpId ?? null,
|
|
3561
|
+
unitId: args.unitId,
|
|
3562
|
+
};
|
|
3269
3563
|
permissionId = String(hit.id ?? "");
|
|
3270
3564
|
}
|
|
3565
|
+
else {
|
|
3566
|
+
// Capture the row being removed so the receipt has a rebuildable before
|
|
3567
|
+
// face even when the caller only passed permissionId (r30d F11).
|
|
3568
|
+
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode }).catch(() => null);
|
|
3569
|
+
workflowCodeForActor = workflowCode ?? undefined;
|
|
3570
|
+
if (workflowCode) {
|
|
3571
|
+
const rows = await permissionRows(session, workflowCode);
|
|
3572
|
+
const hit = rows.find((row) => String(row.id ?? "") === permissionId);
|
|
3573
|
+
if (hit)
|
|
3574
|
+
beforeRow = {
|
|
3575
|
+
id: hit.id ?? null,
|
|
3576
|
+
name: hit.name ?? null,
|
|
3577
|
+
unitType: hit.unitType ?? null,
|
|
3578
|
+
fullName: hit.fullName ?? null,
|
|
3579
|
+
code: hit.code ?? null,
|
|
3580
|
+
corpId: hit.corpId ?? null,
|
|
3581
|
+
};
|
|
3582
|
+
}
|
|
3583
|
+
}
|
|
3271
3584
|
await wireCall(session, { path: ENDPOINTS.workflowDeletePermission, method: "POST", query: { permissionId } });
|
|
3272
|
-
|
|
3585
|
+
const actor = await resolvePermissionActor(session);
|
|
3586
|
+
return {
|
|
3587
|
+
performed: { errcode: 0 },
|
|
3588
|
+
readback: {
|
|
3589
|
+
permissionId,
|
|
3590
|
+
removed: true,
|
|
3591
|
+
...(workflowCodeForActor ? { workflowCode: workflowCodeForActor } : {}),
|
|
3592
|
+
before: beforeRow,
|
|
3593
|
+
actor,
|
|
3594
|
+
...(beforeRow
|
|
3595
|
+
? {}
|
|
3596
|
+
: { rollbackable: false, rollbackNote: "删除前未捕获到权限行详情(只给了 permissionId 且未能解析所属流程);撤错需手工按 id 复建。" }),
|
|
3597
|
+
},
|
|
3598
|
+
};
|
|
3599
|
+
},
|
|
3600
|
+
rollback: {
|
|
3601
|
+
target: (args) => `workflow-permission:${String(args.workflowCode ?? args.schemaCode ?? args.permissionId ?? "")}`,
|
|
3602
|
+
read: async (session, args) => {
|
|
3603
|
+
// Prefer resolving the concrete row so inverse can re-add it.
|
|
3604
|
+
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode }).catch(() => null);
|
|
3605
|
+
if (!workflowCode)
|
|
3606
|
+
return null;
|
|
3607
|
+
const rows = await permissionRows(session, workflowCode);
|
|
3608
|
+
if (typeof args.permissionId === "string" && args.permissionId) {
|
|
3609
|
+
return rows.find((row) => String(row.id ?? "") === args.permissionId) ?? null;
|
|
3610
|
+
}
|
|
3611
|
+
if (typeof args.unitId === "string" && args.unitType !== undefined) {
|
|
3612
|
+
const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
|
|
3613
|
+
const identity = await unitIdentity(session, unitType, args.unitId);
|
|
3614
|
+
return rows.find((row) => String(row.unitType ?? "") === String(unitType) && (String(row.id ?? "") === args.unitId ||
|
|
3615
|
+
String(row.code ?? "") === args.unitId ||
|
|
3616
|
+
(identity.name !== null && String(row.name ?? "") === identity.name) ||
|
|
3617
|
+
(identity.code !== null && String(row.code ?? "") === identity.code) ||
|
|
3618
|
+
(identity.name !== null && String(row.fullName ?? "") === identity.name))) ?? null;
|
|
3619
|
+
}
|
|
3620
|
+
return null;
|
|
3621
|
+
},
|
|
3622
|
+
inverse: ({ before, args }) => {
|
|
3623
|
+
if (before == null)
|
|
3624
|
+
return { kind: "irreversible", reason: "removePermission 前未捕获到权限行(before 为空),无法自动复建;用 workflow.addPermission 手工补回" };
|
|
3625
|
+
const row = before;
|
|
3626
|
+
const workflowCode = String(args.workflowCode ?? "");
|
|
3627
|
+
const unitId = typeof args.unitId === "string" && args.unitId ? args.unitId : null;
|
|
3628
|
+
const unitType = args.unitType !== undefined
|
|
3629
|
+
? (args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3)
|
|
3630
|
+
: (row.unitType === 1 || row.unitType === "1" ? 1 : row.unitType === 2 || row.unitType === "2" ? 2 : row.unitType === 3 || row.unitType === "3" ? 3 : null);
|
|
3631
|
+
if (!workflowCode || !unitId || unitType === null)
|
|
3632
|
+
return {
|
|
3633
|
+
kind: "irreversible",
|
|
3634
|
+
reason: `before 有行(name=${String(row.name ?? "?")} unitType=${String(row.unitType ?? "?")}),但缺少可重放的 workflowCode+unitId+unitType;请手工:omc call workflow.addPermission --args '{"workflowCode":"<code>","unitId":"<id>","unitType":${unitType ?? 2}}'`,
|
|
3635
|
+
partial: [],
|
|
3636
|
+
};
|
|
3637
|
+
return {
|
|
3638
|
+
kind: "calls",
|
|
3639
|
+
calls: [{ tool: "workflow.addPermission", args: { workflowCode, unitId, unitType } }],
|
|
3640
|
+
notes: [`复建被删行:name=${String(row.name ?? "?")} unitType=${unitType}`],
|
|
3641
|
+
};
|
|
3642
|
+
},
|
|
3273
3643
|
},
|
|
3274
3644
|
});
|
|
3275
3645
|
tools.push(defineTool({
|
package/dist/model-scaffold.js
CHANGED
|
@@ -73,9 +73,11 @@ export async function cmdModelScaffold(args) {
|
|
|
73
73
|
const forcePublish = args.includes("--force-publish");
|
|
74
74
|
const usage = [
|
|
75
75
|
"用法:omc model-scaffold --code <模型code> --name <模型名> --fields '<JSON 数组>' [--folder <目录id>] [--apply]",
|
|
76
|
-
" --fields 每条是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...};子表加 \"
|
|
76
|
+
" --fields 每条是一个 field.create 的参数:{\"code\",\"name\",\"propertyType\",...};子表加 \"subFields\":[...]。",
|
|
77
|
+
" 子表键名统一用 subFields(field.create 平台键);childFields 仍作别名接受,但 --help 只展示 subFields。",
|
|
77
78
|
" 默认 dry-run(列出将要做什么);--apply 依次执行 建模型→建字段→建子列→发布字段→发布表单。",
|
|
78
79
|
" 幂等:模型/字段已存在记为 skipped 继续;有真失败时默认不发布(--force-publish 可强发)。",
|
|
80
|
+
" 每个子调用使用独立 requestId(避免同 id 不同参数被幂等门拒绝);重入已存在字段记 skipped。",
|
|
79
81
|
" 从文件读:--fields-file <配置根内路径>。",
|
|
80
82
|
].join("\n");
|
|
81
83
|
const invalid = (message) => {
|
|
@@ -121,7 +123,7 @@ export async function cmdModelScaffold(args) {
|
|
|
121
123
|
for (const field of preflightTargets) {
|
|
122
124
|
const probe = await runToolCall("field.create", {
|
|
123
125
|
args: scaffoldFieldArgs(code, field),
|
|
124
|
-
requestId: request
|
|
126
|
+
requestId: `${request}:preflight:${field.code}`,
|
|
125
127
|
dryRun: true,
|
|
126
128
|
...(environment !== undefined ? { environment } : {}),
|
|
127
129
|
print: () => undefined,
|
|
@@ -168,11 +170,17 @@ export async function cmdModelScaffold(args) {
|
|
|
168
170
|
// lets that through writes N extra lines to stdout and breaks the
|
|
169
171
|
// one-envelope contract (the classic trap recorded in HANDOFF §6). Swallow the
|
|
170
172
|
// per-call lines and surface the useful ones in `steps[]` instead.
|
|
173
|
+
// r31c F9: each sub-call needs its own requestId. Reusing the composer-level
|
|
174
|
+
// id makes the 2nd+ field.create look like "same id, different args" and the
|
|
175
|
+
// idempotency gate refuses them (probe: created:1 failed:6).
|
|
176
|
+
let subCallSeq = 0;
|
|
171
177
|
const call = async (tool, toolArgs) => {
|
|
172
178
|
const lines = [];
|
|
179
|
+
subCallSeq += 1;
|
|
180
|
+
const perCallRequestId = `${request}:${tool}:${subCallSeq}`;
|
|
173
181
|
const outcome = await runToolCall(tool, {
|
|
174
182
|
args: toolArgs,
|
|
175
|
-
requestId:
|
|
183
|
+
requestId: perCallRequestId,
|
|
176
184
|
...(environment !== undefined ? { environment } : {}),
|
|
177
185
|
print: (line) => lines.push(line),
|
|
178
186
|
});
|