@stalfh233/omc-cli 0.6.7 → 0.6.10
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 +2 -1
- package/dist/approval-flow.js +5 -3
- package/dist/bizservice-config.js +26 -10
- package/dist/call.js +47 -0
- package/dist/capability-overview.js +47 -24
- package/dist/cli-contract.js +73 -26
- package/dist/cli-output.js +41 -2
- package/dist/cli-stdout.js +74 -0
- package/dist/cli.js +135 -52
- package/dist/command-transport.js +8 -1
- package/dist/config-store.js +17 -2
- package/dist/discovery.js +62 -3
- package/dist/field-families.js +35 -6
- package/dist/goal-routes.js +118 -11
- package/dist/human-summary.js +11 -0
- package/dist/lanes/app-contract.js +12 -5
- package/dist/lanes/app.js +221 -134
- package/dist/lanes/apply-changes.js +2 -1
- package/dist/lanes/b2-registration-contract.js +3 -3
- package/dist/lanes/b2-registration.js +890 -693
- package/dist/lanes/businessrule.js +75 -51
- package/dist/lanes/custom-page.js +166 -115
- package/dist/lanes/datarule-contract.js +2 -2
- package/dist/lanes/datarule.js +238 -154
- package/dist/lanes/dictionary.js +253 -110
- package/dist/lanes/environment.js +57 -9
- package/dist/lanes/field.js +160 -107
- package/dist/lanes/folder-contract.js +7 -3
- package/dist/lanes/folder.js +249 -118
- package/dist/lanes/form-contract.js +1 -1
- package/dist/lanes/form.js +188 -64
- package/dist/lanes/list-view.js +405 -369
- package/dist/lanes/meta.js +31 -6
- package/dist/lanes/model-contract.js +1 -1
- package/dist/lanes/model.js +610 -512
- package/dist/lanes/online-js-contract.js +1 -1
- package/dist/lanes/online-js.js +160 -146
- package/dist/lanes/organization.js +905 -640
- package/dist/lanes/programming.js +274 -136
- package/dist/lanes/report-contract.js +2 -2
- package/dist/lanes/report.js +354 -198
- package/dist/lanes/rule-lifecycle.js +294 -140
- package/dist/lanes/runtime-data-contract.js +4 -4
- package/dist/lanes/runtime-data.js +869 -745
- package/dist/lanes/workflow-contract.js +3 -3
- package/dist/lanes/workflow.js +732 -605
- package/dist/model-scaffold.js +26 -5
- package/dist/path-projection.js +196 -14
- package/dist/plan.js +25 -0
- package/dist/programming-workflow.js +55 -3
- package/dist/read-shapes.js +146 -0
- package/dist/read.js +40 -9
- package/dist/recipes/businessrule.js +74 -24
- package/dist/recipes/dictionary.js +7 -4
- package/dist/recipes/form.js +46 -0
- package/dist/recipes/runtime-data.js +3 -2
- package/dist/refusal-guidance.js +29 -5
- package/dist/rule-support.js +14 -1
- package/dist/rule-when.js +2 -2
- package/dist/tool-types.js +9 -3
- package/dist/version-family-gaps.js +101 -0
- package/dist/write.js +2 -1
- package/package.json +6 -4
- package/skills/omc-glossary.md +13 -0
- package/skills/omc-refusals.md +13 -5
- package/skills/omc-tool-selection.md +2 -1
- package/skills/omc-write-hazards.md +5 -2
package/dist/lanes/workflow.js
CHANGED
|
@@ -2226,386 +2226,425 @@ export function registerWorkflowLane(tools) {
|
|
|
2226
2226
|
return { workflowCode, valid: errors.length === 0, errorCount: errors.length, warningCount: warnings.length, errors, warnings };
|
|
2227
2227
|
}, { target: `workflow.validate:${args.workflowCode ?? args.schemaCode}` }),
|
|
2228
2228
|
}));
|
|
2229
|
-
writeTool(
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
});
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2229
|
+
writeTool({
|
|
2230
|
+
tools,
|
|
2231
|
+
name: "workflow.create",
|
|
2232
|
+
lane: WORKFLOW_LANE,
|
|
2233
|
+
scope: WORKFLOW_TOOLS["workflow.create"].scope,
|
|
2234
|
+
description: WORKFLOW_TOOLS["workflow.create"].description,
|
|
2235
|
+
inputSchema: {
|
|
2236
|
+
schemaCode: z.string().min(1),
|
|
2237
|
+
// 2026-10-27 用户需求:流程名默认与表单名一致。省略时的解析链见
|
|
2238
|
+
// resolveWorkflowName——表单名 → 模型名 → 拒绝(不拿 schemaCode 冒充)。
|
|
2239
|
+
name: z.string().min(1).optional().describe("流程名称;省略时默认 = 该模型默认表单的名字(无表单草稿时用模型名),与设计器行为一致"),
|
|
2240
|
+
code: z.string().min(1),
|
|
2241
|
+
},
|
|
2242
|
+
execute: async (session, args) => {
|
|
2243
|
+
// Ticket 76 idempotent retry: get_header FIRST — a post-timeout
|
|
2244
|
+
// re-create of the same code short-circuits with the existing header
|
|
2245
|
+
// and ZERO create wires. On the happy path this read REPLACES the
|
|
2246
|
+
// post-create header read (same wire count), because a successful
|
|
2247
|
+
// create implies the header.
|
|
2248
|
+
const existingHeader = await getWorkflowHeaderOrNull(session, args.code);
|
|
2249
|
+
if (existingHeader != null) {
|
|
2250
|
+
return { performed: { skipped: "already-present" }, readback: { alreadyPresent: true, header: existingHeader } };
|
|
2251
|
+
}
|
|
2252
|
+
const { name: workflowName, resolvedFrom } = await resolveWorkflowName(session, args.name, args.schemaCode);
|
|
2253
|
+
let performed;
|
|
2254
|
+
try {
|
|
2255
|
+
performed = await wireCall(session, {
|
|
2256
|
+
path: ENDPOINTS.workflowCreate,
|
|
2257
|
+
method: "POST",
|
|
2258
|
+
body: { workflowCode: args.code, workflowName, schemaCode: args.schemaCode, icon: "h-icon-all-logic" },
|
|
2259
|
+
});
|
|
2260
|
+
}
|
|
2261
|
+
catch (error) {
|
|
2262
|
+
// Ticket 76 live: the platform answers an envelope with NO numeric
|
|
2263
|
+
// errcode when the SCHEMA already has a workflow (a second, differently
|
|
2264
|
+
// coded one is refused). Decode it as a duplicate-create — same-code
|
|
2265
|
+
// presence was already excluded by the pre-check, so this is the
|
|
2266
|
+
// different-code case — instead of surfacing a malformed-envelope
|
|
2267
|
+
// transport-looking failure on a business duplicate.
|
|
2268
|
+
if (error instanceof WireError && error.kind === "malformed-envelope") {
|
|
2269
|
+
const has = await wireCall(session, { path: ENDPOINTS.workflowIsWorkflow, query: { schemaCode: args.schemaCode } });
|
|
2270
|
+
if (has.data === true) {
|
|
2271
|
+
throw new ToolRefusalError({
|
|
2272
|
+
status: "refused",
|
|
2273
|
+
blocker: "workflow-already-exists",
|
|
2274
|
+
schemaCode: args.schemaCode,
|
|
2275
|
+
workflowCode: args.code,
|
|
2276
|
+
message: `模型 ${args.schemaCode} 已有流程(平台对重复创建返回非数字 errcode 的 envelope)——一个模型只能有一个流程。要改现有流程用 workflow.update/workflow.publish(先 workflow.get_draft 取当前模板),不要另建新 code`,
|
|
2277
|
+
});
|
|
2278
|
+
}
|
|
2271
2279
|
}
|
|
2280
|
+
throw error;
|
|
2272
2281
|
}
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
}
|
|
2281
|
-
writeTool(
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2308
|
-
|
|
2309
|
-
|
|
2310
|
-
|
|
2311
|
-
|
|
2312
|
-
|
|
2313
|
-
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2282
|
+
const draft = await getWorkflowDraftOrNull(session, args.code);
|
|
2283
|
+
return {
|
|
2284
|
+
performed: { errcode: performed.errcode },
|
|
2285
|
+
readback: { workflowCode: args.code, name: workflowName, nameResolvedFrom: resolvedFrom, headerPresent: true, draftPresent: draft != null, templateId: draft?.id ?? null },
|
|
2286
|
+
};
|
|
2287
|
+
},
|
|
2288
|
+
rollback: workflowRollbackSpec("workflow.create"),
|
|
2289
|
+
});
|
|
2290
|
+
writeTool({
|
|
2291
|
+
tools,
|
|
2292
|
+
name: "workflow.update",
|
|
2293
|
+
lane: WORKFLOW_LANE,
|
|
2294
|
+
scope: WORKFLOW_TOOLS["workflow.update"].scope,
|
|
2295
|
+
description: WORKFLOW_TOOLS["workflow.update"].description,
|
|
2296
|
+
inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code,传 workflowCode 会被 schema 拒绝"), template: workflowTemplateArg, merge: z.boolean().optional().describe("增量:只发改动/新增的活动与路由,工具按 activityCode 合并到草稿(草稿为基;新增节点自动接链),不必先读回整模板再重发。默认自动:模板缺 START 或 END 时按增量 merge(部分模板),给了完整 START..END 才整模板替换;显式 true 强制增量、false 强制替换。merge 是并集、绝不删边:结果比草稿少边会被拒(workflow-merge-would-drop-routes);--dry-run 会返回 routesBefore/routesAfter/routesAdded/routesDropped") },
|
|
2297
|
+
execute: async (session, args) => {
|
|
2298
|
+
const template = args.template ?? {};
|
|
2299
|
+
validateCallerActivities(template, "workflow.update");
|
|
2300
|
+
validateWorkflowRouteConditions(template, "workflow.update");
|
|
2301
|
+
assertTemplateNotMinimal(template, "workflow.update");
|
|
2302
|
+
lintWorkflowTemplate(template, "workflow.update");
|
|
2303
|
+
await refuseRequiredOnChildTableFields(session, args.schemaCode, template, "workflow.update");
|
|
2304
|
+
// Ticket 112: merge onto the full draft row — a verbatim minimal post
|
|
2305
|
+
// leaves converter holes and answers errcode 50000 on the platform.
|
|
2306
|
+
// Delta semantics: explicit `true` forces merge, explicit `false` forces
|
|
2307
|
+
// replace, and the default auto-detects a partial template (missing
|
|
2308
|
+
// START/END) as a delta — see activitiesLackBoundaries.
|
|
2309
|
+
const merge = args.merge === true || (args.merge !== false && activitiesLackBoundaries(template));
|
|
2310
|
+
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, merge);
|
|
2311
|
+
await backfillDesignerDefaults(session, args.schemaCode, body, template);
|
|
2312
|
+
assertFirstActivityIsOriginator(template, "workflow.update", body);
|
|
2313
|
+
validateWorkflowRender(body, "workflow.update");
|
|
2314
|
+
await assertRoleParticipantScopes(session, body);
|
|
2315
|
+
// Live: workflow/update does NOT run the template validator — it stores a
|
|
2316
|
+
// refused participant silently and only /publish answers the
|
|
2317
|
+
// ValidateMessage[]. So the guard belongs on publish/bindRule, not here.
|
|
2318
|
+
const performed = await wireCall(session, { path: ENDPOINTS.workflowUpdate, method: "POST", body });
|
|
2319
|
+
const draft = await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode: args.code } });
|
|
2320
|
+
const data = (draft.data ?? null);
|
|
2321
|
+
if (!data)
|
|
2322
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.update returned no draft" });
|
|
2323
|
+
validateWorkflowRender(data, "workflow.update readback");
|
|
2324
|
+
const participantWarnings = participantReferenceWarnings(data);
|
|
2325
|
+
// Evaluate on `body` (post-backfill): the readback may drop the handler
|
|
2326
|
+
// keys, and the backfilled permittedAction is exactly what will run.
|
|
2327
|
+
const rejectWarnings = rejectToStartCancelWarnings(body);
|
|
2328
|
+
const renamedActivities = renamedActivityPairs(template, data);
|
|
2329
|
+
const renamedInPlace = inPlaceRenames(data);
|
|
2330
|
+
// Caller sent no routes at all: the draft's edges (branches included) are
|
|
2331
|
+
// what rides the wire. Say so, and name the branch edges, because the only
|
|
2332
|
+
// other trace is a missing edge in a later getPublished readback.
|
|
2333
|
+
const callerSentRoutes = template.routes !== undefined || template.edges !== undefined || template.rules !== undefined;
|
|
2334
|
+
const branchEdges = callerSentRoutes ? [] : inheritedBranchEdges(data);
|
|
2335
|
+
// 改名验收的权威读数:每个节点的当前显示名(platform may keep
|
|
2336
|
+
// activityName or sync it; effectiveActivityName resolves both shapes).
|
|
2337
|
+
const rawActivityRows = Array.isArray(data.activities)
|
|
2338
|
+
? data.activities
|
|
2339
|
+
: data.activities && typeof data.activities === "object"
|
|
2340
|
+
? Object.values(data.activities)
|
|
2341
|
+
: [];
|
|
2342
|
+
const activityNames = Object.fromEntries(rawActivityRows
|
|
2343
|
+
.map((row) => [String(row.activityCode ?? ""), effectiveActivityName(row)])
|
|
2344
|
+
.filter(([code]) => code !== ""));
|
|
2345
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, activities: Array.isArray(data.activities) ? data.activities.length : rawActivityRows.length, templateType: data.templateType ?? null, activityNames, rollbackHint: `本次改动的 before 快照已记录:omc rollback plan --target workflow:${args.schemaCode}/${args.code} 查看可回退项,omc rollback apply --target … --step <n> 执行(草稿回退后仍需 workflow.publish 才生效)`, ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length ? { workflowWarnings: rejectWarnings } : {}), ...(branchEdges.length ? { routesInherited: { edges: branchEdges, hint: "本次未传 routes/edges:以上分支边沿用草稿(含 else 兜底)。要改连线请显式回传完整 routes" } } : {}), ...(renamedInPlace.length ? { renamedInPlace, renamedInPlaceHint: "改名已同 code 原地生效(载体 name_i18n,displayName 才是当前名;activityName 是首建值,平台不再变更)" } : {}), ...(renamedActivities.length ? { renamedActivities, renamedHint: "类型变更铸了新码(上表 from→to)且旧码已失效——后续读写一律用 to;再用旧码 from 会被当作新节点追加" } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
2346
|
+
},
|
|
2347
|
+
preflight: (args) => {
|
|
2348
|
+
// Zero-wire refusal (ticket 77): fires in semanticGate BEFORE the
|
|
2349
|
+
// governed pipeline — including the rollback before-capture read.
|
|
2350
|
+
assertTemplateNotMinimal(args.template ?? {}, "workflow.update");
|
|
2351
|
+
},
|
|
2352
|
+
dryRunPayload: // Dry-run must actually look at the merge result: shape-only previews said
|
|
2353
|
+
// "wouldChange" while the write silently dropped 15 of 46 edges (issue
|
|
2354
|
+
// 2026-10-22 §1.2). The payload runs the SAME merge as the write (reads
|
|
2355
|
+
// only) and reports the edge delta; the contraction refusal fires here too.
|
|
2356
|
+
async (session, args) => {
|
|
2357
|
+
const template = args.template ?? {};
|
|
2358
|
+
const merge = args.merge === true || (args.merge !== false && activitiesLackBoundaries(template));
|
|
2359
|
+
const draft = await getWorkflowDraftOrNull(session, args.code);
|
|
2360
|
+
if (draft === null)
|
|
2361
|
+
throw new ToolRefusalError({
|
|
2362
|
+
status: "refused",
|
|
2363
|
+
blocker: "workflow-not-found",
|
|
2364
|
+
workflowCode: args.code,
|
|
2365
|
+
message: `流程 ${args.code} 不存在(get_draft 401001 缺席)——先 workflow.create 再 update/publish`,
|
|
2366
|
+
});
|
|
2367
|
+
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, merge);
|
|
2368
|
+
validateWorkflowRender(body, "workflow.update");
|
|
2369
|
+
const edgeKeys = (rules) => (Array.isArray(rules) ? rules : []).map((rule) => `${String(rule.preActivityCode ?? "")}->${String(rule.postActivityCode ?? "")}`);
|
|
2370
|
+
const beforeEdges = edgeKeys(draft.rules);
|
|
2371
|
+
const afterEdges = edgeKeys(body.rules);
|
|
2372
|
+
// Multiset difference: two conditional lines can share one node pair, so a
|
|
2373
|
+
// Set would hide a real drop.
|
|
2374
|
+
const remaining = [...afterEdges];
|
|
2375
|
+
const routesDropped = [];
|
|
2376
|
+
for (const edge of beforeEdges) {
|
|
2377
|
+
const index = remaining.indexOf(edge);
|
|
2378
|
+
if (index >= 0)
|
|
2379
|
+
remaining.splice(index, 1);
|
|
2380
|
+
else
|
|
2381
|
+
routesDropped.push(edge);
|
|
2382
|
+
}
|
|
2383
|
+
const beforeLeft = [...beforeEdges];
|
|
2384
|
+
const routesAdded = [];
|
|
2385
|
+
for (const edge of afterEdges) {
|
|
2386
|
+
const index = beforeLeft.indexOf(edge);
|
|
2387
|
+
if (index >= 0)
|
|
2388
|
+
beforeLeft.splice(index, 1);
|
|
2389
|
+
else
|
|
2390
|
+
routesAdded.push(edge);
|
|
2391
|
+
}
|
|
2392
|
+
const activityCodes = (rows) => {
|
|
2393
|
+
const list = Array.isArray(rows) ? rows : rows && typeof rows === "object" ? Object.values(rows) : [];
|
|
2394
|
+
return list.map((row) => String(row?.activityCode ?? "")).filter((code) => code !== "");
|
|
2395
|
+
};
|
|
2396
|
+
const callerSentRoutes = template.routes !== undefined || template.edges !== undefined || template.rules !== undefined;
|
|
2397
|
+
return {
|
|
2398
|
+
merge,
|
|
2399
|
+
callerSentRoutes,
|
|
2400
|
+
activitiesBefore: activityCodes(draft.activities).length,
|
|
2401
|
+
activitiesAfter: activityCodes(body.activities).length,
|
|
2402
|
+
routesBefore: beforeEdges.length,
|
|
2403
|
+
routesAfter: afterEdges.length,
|
|
2404
|
+
routesAdded,
|
|
2405
|
+
routesDropped,
|
|
2406
|
+
hint: callerSentRoutes
|
|
2407
|
+
? "本次显式给了连线:merge 是并集(草稿边 + 你的边),不会删边"
|
|
2408
|
+
: "本次未给 routes/edges/rules:草稿连线原样沿用(新增节点才会自动接链)",
|
|
2409
|
+
};
|
|
2410
|
+
},
|
|
2411
|
+
rollback: workflowRollbackSpec("workflow.update"),
|
|
2412
|
+
});
|
|
2413
|
+
writeTool({
|
|
2414
|
+
tools,
|
|
2415
|
+
name: "workflow.publish",
|
|
2416
|
+
lane: WORKFLOW_LANE,
|
|
2417
|
+
scope: WORKFLOW_TOOLS["workflow.publish"].scope,
|
|
2418
|
+
description: WORKFLOW_TOOLS["workflow.publish"].description,
|
|
2419
|
+
inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code,传 workflowCode 会被 schema 拒绝"), template: workflowTemplateArg.optional() },
|
|
2420
|
+
execute: async (session, args) => {
|
|
2421
|
+
// An absent template publishes the stored draft as-is — the platform's own
|
|
2422
|
+
// path. The schema is optional so the contract no longer contradicts it.
|
|
2423
|
+
const template = args.template ?? {};
|
|
2424
|
+
validateCallerActivities(template, "workflow.publish");
|
|
2425
|
+
validateWorkflowRouteConditions(template, "workflow.publish");
|
|
2426
|
+
assertTemplateNotMinimal(template, "workflow.publish");
|
|
2427
|
+
await refuseRequiredOnChildTableFields(session, args.schemaCode, template, "workflow.publish");
|
|
2428
|
+
lintWorkflowTemplate(template, "workflow.publish");
|
|
2429
|
+
// Ticket 112: same merge-before-write as update. A template without both
|
|
2430
|
+
// boundaries is a partial edit → delta merge (see activitiesLackBoundaries).
|
|
2431
|
+
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, activitiesLackBoundaries(template));
|
|
2432
|
+
await backfillDesignerDefaults(session, args.schemaCode, body, template);
|
|
2433
|
+
assertFirstActivityIsOriginator(template, "workflow.publish", body);
|
|
2434
|
+
const unregistered = unregisteredParticipantFunctions(body);
|
|
2435
|
+
if (unregistered.length > 0)
|
|
2436
|
+
throw new ToolRefusalError({
|
|
2437
|
+
status: "refused",
|
|
2438
|
+
blocker: "workflow-participant-function-unregistered",
|
|
2439
|
+
workflowCode: args.code,
|
|
2440
|
+
activities: unregistered,
|
|
2441
|
+
message: `流程发布被拒:参与人函数 ${[...new Set(unregistered.map((entry) => entry.functionName + "()"))].join("、")} 在 AI-Platform v1.1.25 未注册(节点 ${unregistered.map((entry) => entry.activityCode).join("、")})。` +
|
|
2442
|
+
`发布能过,但运行期 ParticipantActivityRuntime 抛 FunctionNotFoundException:实例卡 PROCESSING、不生成待办,而 data.submit 仍回 errCode 0(r4-training 真机)。` +
|
|
2443
|
+
`内置函数只有 FindUserByRole({Originator.OU},"<roleCode>","random");跨部门/全局角色改用参与者对象数组(角色 id)。` +
|
|
2444
|
+
`这两个名字常见于 workflow.get_published 读回与种子流程,不要回抄。`,
|
|
2445
|
+
});
|
|
2446
|
+
validateWorkflowRender(body, "workflow.publish");
|
|
2447
|
+
const performed = await wireCall(session, { path: ENDPOINTS.workflowPublish, method: "POST", body });
|
|
2448
|
+
// A refused publish answers errcode 0 — the ValidateMessage[] is the truth.
|
|
2449
|
+
assertWorkflowWriteAccepted(performed, "workflow.publish", args.code);
|
|
2450
|
+
const published = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.code } });
|
|
2451
|
+
const data = (published.data ?? null);
|
|
2452
|
+
// Ticket 77: publish evidence is the published-face MARKER, not data
|
|
2453
|
+
// presence (the endpoint answers the draft shape when unpublished).
|
|
2454
|
+
const isPublished = data !== null && (data.templateType === "PUBLISH" || data.published === true);
|
|
2455
|
+
if (!data)
|
|
2456
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.publish returned no published template" });
|
|
2457
|
+
const warnings = workflowValidationWarnings(performed.data);
|
|
2458
|
+
// The platform accepted the write (no ERROR) yet the published face still
|
|
2459
|
+
// shows the draft marker: surface that honestly instead of implying the
|
|
2460
|
+
// flow went live.
|
|
2461
|
+
if (!isPublished)
|
|
2462
|
+
throw new ToolRefusalError({
|
|
2463
|
+
status: "refused",
|
|
2464
|
+
blocker: "workflow-publish-not-applied",
|
|
2465
|
+
workflowCode: args.code,
|
|
2466
|
+
message: `workflow.publish: 平台接受写入(errcode 0)但读回仍是草稿形态(templateType=${String(data.templateType ?? "null")}, published=${String(data.published ?? "null")})——流程未生效。先 workflow.validate 复核模板,修正后重试`,
|
|
2467
|
+
});
|
|
2468
|
+
validateWorkflowRender(data, "workflow.publish readback");
|
|
2469
|
+
const participantWarnings = participantReferenceWarnings(data);
|
|
2470
|
+
const rejectWarnings = rejectToStartCancelWarnings(data);
|
|
2471
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, published: isPublished, workflowVersion: data.workflowVersion ?? null, ...(warnings.length ? { warnings } : {}), ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length ? { workflowWarnings: rejectWarnings } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
2472
|
+
},
|
|
2473
|
+
preflight: (args) => {
|
|
2474
|
+
assertTemplateNotMinimal(args.template ?? {}, "workflow.publish");
|
|
2475
|
+
},
|
|
2476
|
+
rollback: workflowRollbackSpec("workflow.publish"),
|
|
2477
|
+
});
|
|
2478
|
+
writeTool({
|
|
2479
|
+
tools,
|
|
2480
|
+
name: "workflow.bindRule",
|
|
2481
|
+
lane: WORKFLOW_LANE,
|
|
2482
|
+
scope: WORKFLOW_TOOLS["workflow.bindRule"].scope,
|
|
2483
|
+
description: WORKFLOW_TOOLS["workflow.bindRule"].description,
|
|
2484
|
+
inputSchema: {
|
|
2485
|
+
schemaCode: z.string().min(1),
|
|
2486
|
+
workflowCode: z.string().min(1),
|
|
2487
|
+
ruleCode: z.string().min(1).optional().describe("single-rule form: the rule to bind (mutually exclusive with ruleCodes)"),
|
|
2488
|
+
ruleCodes: z.array(z.string().min(1)).min(1).optional().describe("batch form: several rules bound in ONE update + publish; MERGE semantics — the handler's existing bizActions are preserved and the listed rules appended (deduplicated)"),
|
|
2489
|
+
handler: z
|
|
2490
|
+
.string()
|
|
2491
|
+
.optional()
|
|
2492
|
+
.describe("FLOW-level event handler to bind: start | cancel | activate | end (default end). 与 activityCode+nodeEvent 互斥"),
|
|
2493
|
+
activityCode: z
|
|
2494
|
+
.string()
|
|
2495
|
+
.min(1)
|
|
2496
|
+
.optional()
|
|
2497
|
+
.describe("NODE-level target activity (草稿里的 activityCode,如 Activity2). 必须与 nodeEvent 同时给;节点不存在时报 workflow-activity-not-found 并列出可用 code"),
|
|
2498
|
+
nodeEvent: z
|
|
2499
|
+
.enum(["beforeActivate", "afterActivate", "endActivity", "cancelActivity", "activityApproved", "activityRejected", "jobSubmitted", "jobRejected"])
|
|
2500
|
+
.optional()
|
|
2501
|
+
.describe("NODE-level event on activityCode: beforeActivate|afterActivate|endActivity|cancelActivity|activityApproved|activityRejected|jobSubmitted|jobRejected(如填写节点 endActivity = 申请人提交离开该节点时). 与 handler 互斥"),
|
|
2502
|
+
publish: z.boolean().optional(),
|
|
2503
|
+
},
|
|
2504
|
+
execute: async (session, args) => {
|
|
2505
|
+
const target = resolveBindTarget(args.handler, args.nodeEvent, args.activityCode);
|
|
2506
|
+
const codes = (args.ruleCodes ?? [args.ruleCode]).map(String);
|
|
2507
|
+
// Only CUSTOM rules are bindable to workflow events — the designer's picker
|
|
2508
|
+
// does not offer preset trigger rows (bench59 bound `Create` to `end`).
|
|
2509
|
+
const bindableByCode = await assertBindableWorkflowRules(session, args.schemaCode, codes);
|
|
2510
|
+
// A bound workflow rule MUST be enabled (else 失效/未启用). Enable any
|
|
2511
|
+
// disabled bound custom rule before writing the binding.
|
|
2512
|
+
const { enabledRuleCodes, alreadyEnabledRuleCodes } = await ensureWorkflowRulesEnabled(session, args.schemaCode, codes, bindableByCode);
|
|
2513
|
+
const shouldPublish = args.publish !== false;
|
|
2514
|
+
const draft = (await getWorkflowDraftOrNull(session, args.workflowCode)) ?? {};
|
|
2515
|
+
if (Object.keys(draft).length === 0) {
|
|
2516
|
+
throw new ToolRefusalError({
|
|
2517
|
+
status: "refused",
|
|
2518
|
+
blocker: "workflow-not-found",
|
|
2519
|
+
workflowCode: args.workflowCode,
|
|
2520
|
+
message: `流程 ${args.workflowCode} 不存在(get_draft 401001 缺席)——先 workflow.create,再 bindRule`,
|
|
2521
|
+
});
|
|
2522
|
+
}
|
|
2523
|
+
// Node-level: fail closed BEFORE the wire when the activity code is not in
|
|
2524
|
+
// the draft, and name the codes that ARE (the designer's own codes like
|
|
2525
|
+
// Activity2 are the common miss).
|
|
2526
|
+
if (target.scope === "activity" && !findTemplateActivity(draft, target.activityCode)) {
|
|
2527
|
+
throw new ToolRefusalError({
|
|
2528
|
+
status: "refused",
|
|
2529
|
+
blocker: "workflow-activity-not-found",
|
|
2530
|
+
workflowCode: args.workflowCode,
|
|
2531
|
+
activityCode: target.activityCode,
|
|
2532
|
+
activities: templateActivityCodes(draft),
|
|
2533
|
+
message: `流程 ${args.workflowCode} 的草稿里没有活动 ${target.activityCode}——节点级事件必须指向已存在的活动。可用活动:${templateActivityCodes(draft).join("、") || "(无)"}(先用 workflow.get_draft 看 code,别用节点名)`,
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
const handlerOf = (template) => {
|
|
2537
|
+
if (target.scope === "workflow")
|
|
2538
|
+
return (template?.[target.field] ?? {}) ?? {};
|
|
2539
|
+
return (findTemplateActivity(template, target.activityCode)?.[target.field] ?? {}) ?? {};
|
|
2540
|
+
};
|
|
2541
|
+
const existing = handlerOf(draft);
|
|
2542
|
+
const priorActions = Array.isArray(existing.bizActions)
|
|
2543
|
+
? existing.bizActions.filter((value) => typeof value === "string")
|
|
2325
2544
|
: [];
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
-
|
|
2350
|
-
|
|
2351
|
-
validateWorkflowRender(body, "workflow.update");
|
|
2352
|
-
const edgeKeys = (rules) => (Array.isArray(rules) ? rules : []).map((rule) => `${String(rule.preActivityCode ?? "")}->${String(rule.postActivityCode ?? "")}`);
|
|
2353
|
-
const beforeEdges = edgeKeys(draft.rules);
|
|
2354
|
-
const afterEdges = edgeKeys(body.rules);
|
|
2355
|
-
// Multiset difference: two conditional lines can share one node pair, so a
|
|
2356
|
-
// Set would hide a real drop.
|
|
2357
|
-
const remaining = [...afterEdges];
|
|
2358
|
-
const routesDropped = [];
|
|
2359
|
-
for (const edge of beforeEdges) {
|
|
2360
|
-
const index = remaining.indexOf(edge);
|
|
2361
|
-
if (index >= 0)
|
|
2362
|
-
remaining.splice(index, 1);
|
|
2363
|
-
else
|
|
2364
|
-
routesDropped.push(edge);
|
|
2365
|
-
}
|
|
2366
|
-
const beforeLeft = [...beforeEdges];
|
|
2367
|
-
const routesAdded = [];
|
|
2368
|
-
for (const edge of afterEdges) {
|
|
2369
|
-
const index = beforeLeft.indexOf(edge);
|
|
2370
|
-
if (index >= 0)
|
|
2371
|
-
beforeLeft.splice(index, 1);
|
|
2372
|
-
else
|
|
2373
|
-
routesAdded.push(edge);
|
|
2374
|
-
}
|
|
2375
|
-
const activityCodes = (rows) => {
|
|
2376
|
-
const list = Array.isArray(rows) ? rows : rows && typeof rows === "object" ? Object.values(rows) : [];
|
|
2377
|
-
return list.map((row) => String(row?.activityCode ?? "")).filter((code) => code !== "");
|
|
2378
|
-
};
|
|
2379
|
-
const callerSentRoutes = template.routes !== undefined || template.edges !== undefined || template.rules !== undefined;
|
|
2380
|
-
return {
|
|
2381
|
-
merge,
|
|
2382
|
-
callerSentRoutes,
|
|
2383
|
-
activitiesBefore: activityCodes(draft.activities).length,
|
|
2384
|
-
activitiesAfter: activityCodes(body.activities).length,
|
|
2385
|
-
routesBefore: beforeEdges.length,
|
|
2386
|
-
routesAfter: afterEdges.length,
|
|
2387
|
-
routesAdded,
|
|
2388
|
-
routesDropped,
|
|
2389
|
-
hint: callerSentRoutes
|
|
2390
|
-
? "本次显式给了连线:merge 是并集(草稿边 + 你的边),不会删边"
|
|
2391
|
-
: "本次未给 routes/edges/rules:草稿连线原样沿用(新增节点才会自动接链)",
|
|
2392
|
-
};
|
|
2393
|
-
}, workflowRollbackSpec("workflow.update"));
|
|
2394
|
-
writeTool(tools, "workflow.publish", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.publish"].scope, WORKFLOW_TOOLS["workflow.publish"].description, { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code,传 workflowCode 会被 schema 拒绝"), template: workflowTemplateArg.optional() }, async (session, args) => {
|
|
2395
|
-
// An absent template publishes the stored draft as-is — the platform's own
|
|
2396
|
-
// path. The schema is optional so the contract no longer contradicts it.
|
|
2397
|
-
const template = args.template ?? {};
|
|
2398
|
-
validateCallerActivities(template, "workflow.publish");
|
|
2399
|
-
validateWorkflowRouteConditions(template, "workflow.publish");
|
|
2400
|
-
assertTemplateNotMinimal(template, "workflow.publish");
|
|
2401
|
-
await refuseRequiredOnChildTableFields(session, args.schemaCode, template, "workflow.publish");
|
|
2402
|
-
lintWorkflowTemplate(template, "workflow.publish");
|
|
2403
|
-
// Ticket 112: same merge-before-write as update. A template without both
|
|
2404
|
-
// boundaries is a partial edit → delta merge (see activitiesLackBoundaries).
|
|
2405
|
-
const body = await mergedTemplateBody(session, args.schemaCode, args.code, template, activitiesLackBoundaries(template));
|
|
2406
|
-
await backfillDesignerDefaults(session, args.schemaCode, body, template);
|
|
2407
|
-
assertFirstActivityIsOriginator(template, "workflow.publish", body);
|
|
2408
|
-
const unregistered = unregisteredParticipantFunctions(body);
|
|
2409
|
-
if (unregistered.length > 0)
|
|
2410
|
-
throw new ToolRefusalError({
|
|
2411
|
-
status: "refused",
|
|
2412
|
-
blocker: "workflow-participant-function-unregistered",
|
|
2413
|
-
workflowCode: args.code,
|
|
2414
|
-
activities: unregistered,
|
|
2415
|
-
message: `流程发布被拒:参与人函数 ${[...new Set(unregistered.map((entry) => entry.functionName + "()"))].join("、")} 在 AI-Platform v1.1.25 未注册(节点 ${unregistered.map((entry) => entry.activityCode).join("、")})。` +
|
|
2416
|
-
`发布能过,但运行期 ParticipantActivityRuntime 抛 FunctionNotFoundException:实例卡 PROCESSING、不生成待办,而 data.submit 仍回 errCode 0(r4-training 真机)。` +
|
|
2417
|
-
`内置函数只有 FindUserByRole({Originator.OU},"<roleCode>","random");跨部门/全局角色改用参与者对象数组(角色 id)。` +
|
|
2418
|
-
`这两个名字常见于 workflow.get_published 读回与种子流程,不要回抄。`,
|
|
2419
|
-
});
|
|
2420
|
-
validateWorkflowRender(body, "workflow.publish");
|
|
2421
|
-
const performed = await wireCall(session, { path: ENDPOINTS.workflowPublish, method: "POST", body });
|
|
2422
|
-
// A refused publish answers errcode 0 — the ValidateMessage[] is the truth.
|
|
2423
|
-
assertWorkflowWriteAccepted(performed, "workflow.publish", args.code);
|
|
2424
|
-
const published = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.code } });
|
|
2425
|
-
const data = (published.data ?? null);
|
|
2426
|
-
// Ticket 77: publish evidence is the published-face MARKER, not data
|
|
2427
|
-
// presence (the endpoint answers the draft shape when unpublished).
|
|
2428
|
-
const isPublished = data !== null && (data.templateType === "PUBLISH" || data.published === true);
|
|
2429
|
-
if (!data)
|
|
2430
|
-
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.publish returned no published template" });
|
|
2431
|
-
const warnings = workflowValidationWarnings(performed.data);
|
|
2432
|
-
// The platform accepted the write (no ERROR) yet the published face still
|
|
2433
|
-
// shows the draft marker: surface that honestly instead of implying the
|
|
2434
|
-
// flow went live.
|
|
2435
|
-
if (!isPublished)
|
|
2436
|
-
throw new ToolRefusalError({
|
|
2437
|
-
status: "refused",
|
|
2438
|
-
blocker: "workflow-publish-not-applied",
|
|
2439
|
-
workflowCode: args.code,
|
|
2440
|
-
message: `workflow.publish: 平台接受写入(errcode 0)但读回仍是草稿形态(templateType=${String(data.templateType ?? "null")}, published=${String(data.published ?? "null")})——流程未生效。先 workflow.validate 复核模板,修正后重试`,
|
|
2441
|
-
});
|
|
2442
|
-
validateWorkflowRender(data, "workflow.publish readback");
|
|
2443
|
-
const participantWarnings = participantReferenceWarnings(data);
|
|
2444
|
-
const rejectWarnings = rejectToStartCancelWarnings(data);
|
|
2445
|
-
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, published: isPublished, workflowVersion: data.workflowVersion ?? null, ...(warnings.length ? { warnings } : {}), ...(participantWarnings.length ? { participantWarnings } : {}), ...(rejectWarnings.length ? { workflowWarnings: rejectWarnings } : {}), workflowRenderCompat: workflowRenderCompat(data) } };
|
|
2446
|
-
}, (args) => {
|
|
2447
|
-
assertTemplateNotMinimal(args.template ?? {}, "workflow.publish");
|
|
2448
|
-
}, undefined, workflowRollbackSpec("workflow.publish"));
|
|
2449
|
-
writeTool(tools, "workflow.bindRule", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.bindRule"].scope, WORKFLOW_TOOLS["workflow.bindRule"].description, {
|
|
2450
|
-
schemaCode: z.string().min(1),
|
|
2451
|
-
workflowCode: z.string().min(1),
|
|
2452
|
-
ruleCode: z.string().min(1).optional().describe("single-rule form: the rule to bind (mutually exclusive with ruleCodes)"),
|
|
2453
|
-
ruleCodes: z.array(z.string().min(1)).min(1).optional().describe("batch form: several rules bound in ONE update + publish; MERGE semantics — the handler's existing bizActions are preserved and the listed rules appended (deduplicated)"),
|
|
2454
|
-
handler: z
|
|
2455
|
-
.string()
|
|
2456
|
-
.optional()
|
|
2457
|
-
.describe("FLOW-level event handler to bind: start | cancel | activate | end (default end). 与 activityCode+nodeEvent 互斥"),
|
|
2458
|
-
activityCode: z
|
|
2459
|
-
.string()
|
|
2460
|
-
.min(1)
|
|
2461
|
-
.optional()
|
|
2462
|
-
.describe("NODE-level target activity (草稿里的 activityCode,如 Activity2). 必须与 nodeEvent 同时给;节点不存在时报 workflow-activity-not-found 并列出可用 code"),
|
|
2463
|
-
nodeEvent: z
|
|
2464
|
-
.enum(["beforeActivate", "afterActivate", "endActivity", "cancelActivity", "activityApproved", "activityRejected", "jobSubmitted", "jobRejected"])
|
|
2465
|
-
.optional()
|
|
2466
|
-
.describe("NODE-level event on activityCode: beforeActivate|afterActivate|endActivity|cancelActivity|activityApproved|activityRejected|jobSubmitted|jobRejected(如填写节点 endActivity = 申请人提交离开该节点时). 与 handler 互斥"),
|
|
2467
|
-
publish: z.boolean().optional(),
|
|
2468
|
-
}, async (session, args) => {
|
|
2469
|
-
const target = resolveBindTarget(args.handler, args.nodeEvent, args.activityCode);
|
|
2470
|
-
const codes = (args.ruleCodes ?? [args.ruleCode]).map(String);
|
|
2471
|
-
// Only CUSTOM rules are bindable to workflow events — the designer's picker
|
|
2472
|
-
// does not offer preset trigger rows (bench59 bound `Create` to `end`).
|
|
2473
|
-
const bindableByCode = await assertBindableWorkflowRules(session, args.schemaCode, codes);
|
|
2474
|
-
// A bound workflow rule MUST be enabled (else 失效/未启用). Enable any
|
|
2475
|
-
// disabled bound custom rule before writing the binding.
|
|
2476
|
-
const { enabledRuleCodes, alreadyEnabledRuleCodes } = await ensureWorkflowRulesEnabled(session, args.schemaCode, codes, bindableByCode);
|
|
2477
|
-
const shouldPublish = args.publish !== false;
|
|
2478
|
-
const draft = (await getWorkflowDraftOrNull(session, args.workflowCode)) ?? {};
|
|
2479
|
-
if (Object.keys(draft).length === 0) {
|
|
2480
|
-
throw new ToolRefusalError({
|
|
2481
|
-
status: "refused",
|
|
2482
|
-
blocker: "workflow-not-found",
|
|
2483
|
-
workflowCode: args.workflowCode,
|
|
2484
|
-
message: `流程 ${args.workflowCode} 不存在(get_draft 401001 缺席)——先 workflow.create,再 bindRule`,
|
|
2485
|
-
});
|
|
2486
|
-
}
|
|
2487
|
-
// Node-level: fail closed BEFORE the wire when the activity code is not in
|
|
2488
|
-
// the draft, and name the codes that ARE (the designer's own codes like
|
|
2489
|
-
// Activity2 are the common miss).
|
|
2490
|
-
if (target.scope === "activity" && !findTemplateActivity(draft, target.activityCode)) {
|
|
2491
|
-
throw new ToolRefusalError({
|
|
2492
|
-
status: "refused",
|
|
2493
|
-
blocker: "workflow-activity-not-found",
|
|
2494
|
-
workflowCode: args.workflowCode,
|
|
2495
|
-
activityCode: target.activityCode,
|
|
2496
|
-
activities: templateActivityCodes(draft),
|
|
2497
|
-
message: `流程 ${args.workflowCode} 的草稿里没有活动 ${target.activityCode}——节点级事件必须指向已存在的活动。可用活动:${templateActivityCodes(draft).join("、") || "(无)"}(先用 workflow.get_draft 看 code,别用节点名)`,
|
|
2498
|
-
});
|
|
2499
|
-
}
|
|
2500
|
-
const handlerOf = (template) => {
|
|
2501
|
-
if (target.scope === "workflow")
|
|
2502
|
-
return (template?.[target.field] ?? {}) ?? {};
|
|
2503
|
-
return (findTemplateActivity(template, target.activityCode)?.[target.field] ?? {}) ?? {};
|
|
2504
|
-
};
|
|
2505
|
-
const existing = handlerOf(draft);
|
|
2506
|
-
const priorActions = Array.isArray(existing.bizActions)
|
|
2507
|
-
? existing.bizActions.filter((value) => typeof value === "string")
|
|
2508
|
-
: [];
|
|
2509
|
-
// MERGE: existing bizActions are preserved; the listed rules are
|
|
2510
|
-
// appended in order, deduplicated (same semantics the single-rule form
|
|
2511
|
-
// has always had — replace is NOT the contract).
|
|
2512
|
-
const nextActions = [...priorActions];
|
|
2513
|
-
for (const code of codes)
|
|
2514
|
-
if (!nextActions.includes(code))
|
|
2515
|
-
nextActions.push(code);
|
|
2516
|
-
const mergedHandler = {
|
|
2517
|
-
...(typeof existing === "object" && existing ? existing : {}),
|
|
2518
|
-
bizActions: nextActions,
|
|
2519
|
-
bizActionType: "BIZ_RULE",
|
|
2520
|
-
};
|
|
2521
|
-
let nextTemplate;
|
|
2522
|
-
if (target.scope === "workflow") {
|
|
2523
|
-
nextTemplate = { ...draft, [target.field]: mergedHandler, workflowCode: args.workflowCode, schemaCode: args.schemaCode };
|
|
2524
|
-
}
|
|
2525
|
-
else {
|
|
2526
|
-
// Keep the draft's activities container shape (array or index-keyed
|
|
2527
|
-
// object) — only the selected row's handler field changes.
|
|
2528
|
-
nextTemplate = mapTemplateActivity(draft, target.activityCode, (row) => ({ ...row, [target.field]: mergedHandler })).template;
|
|
2529
|
-
nextTemplate = { ...nextTemplate, workflowCode: args.workflowCode, schemaCode: args.schemaCode };
|
|
2530
|
-
}
|
|
2531
|
-
validateWorkflowRender(nextTemplate, "workflow.bindRule");
|
|
2532
|
-
const performed = await wireCall(session, {
|
|
2533
|
-
path: ENDPOINTS.workflowUpdate,
|
|
2534
|
-
method: "POST",
|
|
2535
|
-
body: nextTemplate,
|
|
2536
|
-
});
|
|
2537
|
-
let publishedReadback = null;
|
|
2538
|
-
let publishMarked = false;
|
|
2539
|
-
if (shouldPublish) {
|
|
2540
|
-
const publishPerformed = await wireCall(session, {
|
|
2541
|
-
path: ENDPOINTS.workflowPublish,
|
|
2545
|
+
// MERGE: existing bizActions are preserved; the listed rules are
|
|
2546
|
+
// appended in order, deduplicated (same semantics the single-rule form
|
|
2547
|
+
// has always had — replace is NOT the contract).
|
|
2548
|
+
const nextActions = [...priorActions];
|
|
2549
|
+
for (const code of codes)
|
|
2550
|
+
if (!nextActions.includes(code))
|
|
2551
|
+
nextActions.push(code);
|
|
2552
|
+
const mergedHandler = {
|
|
2553
|
+
...(typeof existing === "object" && existing ? existing : {}),
|
|
2554
|
+
bizActions: nextActions,
|
|
2555
|
+
bizActionType: "BIZ_RULE",
|
|
2556
|
+
};
|
|
2557
|
+
let nextTemplate;
|
|
2558
|
+
if (target.scope === "workflow") {
|
|
2559
|
+
nextTemplate = { ...draft, [target.field]: mergedHandler, workflowCode: args.workflowCode, schemaCode: args.schemaCode };
|
|
2560
|
+
}
|
|
2561
|
+
else {
|
|
2562
|
+
// Keep the draft's activities container shape (array or index-keyed
|
|
2563
|
+
// object) — only the selected row's handler field changes.
|
|
2564
|
+
nextTemplate = mapTemplateActivity(draft, target.activityCode, (row) => ({ ...row, [target.field]: mergedHandler })).template;
|
|
2565
|
+
nextTemplate = { ...nextTemplate, workflowCode: args.workflowCode, schemaCode: args.schemaCode };
|
|
2566
|
+
}
|
|
2567
|
+
validateWorkflowRender(nextTemplate, "workflow.bindRule");
|
|
2568
|
+
const performed = await wireCall(session, {
|
|
2569
|
+
path: ENDPOINTS.workflowUpdate,
|
|
2542
2570
|
method: "POST",
|
|
2543
2571
|
body: nextTemplate,
|
|
2544
2572
|
});
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
:
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2594
|
-
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
|
|
2573
|
+
let publishedReadback = null;
|
|
2574
|
+
let publishMarked = false;
|
|
2575
|
+
if (shouldPublish) {
|
|
2576
|
+
const publishPerformed = await wireCall(session, {
|
|
2577
|
+
path: ENDPOINTS.workflowPublish,
|
|
2578
|
+
method: "POST",
|
|
2579
|
+
body: nextTemplate,
|
|
2580
|
+
});
|
|
2581
|
+
// A refused publish answers errcode 0 with the truth in ValidateMessage[].
|
|
2582
|
+
assertWorkflowWriteAccepted(publishPerformed, "workflow.bindRule", args.workflowCode);
|
|
2583
|
+
const pub = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.workflowCode } });
|
|
2584
|
+
publishedReadback = (pub.data ?? null);
|
|
2585
|
+
// Authoritative marker, not the request echo (ticket 77 semantics).
|
|
2586
|
+
publishMarked = publishedReadback !== null && (publishedReadback.templateType === "PUBLISH" || publishedReadback.published === true);
|
|
2587
|
+
}
|
|
2588
|
+
const draftReadback = await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode: args.workflowCode } });
|
|
2589
|
+
const data = (draftReadback.data ?? null);
|
|
2590
|
+
if (!data)
|
|
2591
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-render-readback-invalid", message: "workflow.bindRule returned no draft" });
|
|
2592
|
+
validateWorkflowRender(data, "workflow.bindRule readback");
|
|
2593
|
+
const handler = handlerOf(data);
|
|
2594
|
+
const bizActions = Array.isArray(handler?.bizActions) ? handler.bizActions.filter((value) => typeof value === "string") : [];
|
|
2595
|
+
const publishedHandler = publishedReadback ? handlerOf(publishedReadback) : undefined;
|
|
2596
|
+
const publishedBizActions = Array.isArray(publishedHandler?.bizActions)
|
|
2597
|
+
? publishedHandler.bizActions.filter((value) => typeof value === "string")
|
|
2598
|
+
: [];
|
|
2599
|
+
const targetShape = target.scope === "workflow"
|
|
2600
|
+
? { handler: target.key, handlerField: target.field }
|
|
2601
|
+
: { activityCode: target.activityCode, nodeEvent: target.key, nodeEventField: target.field };
|
|
2602
|
+
return {
|
|
2603
|
+
performed: { errcode: performed.errcode, scope: target.scope, ...targetShape, addedRuleCodes: codes, bizActions, publishRequested: shouldPublish },
|
|
2604
|
+
readback: {
|
|
2605
|
+
workflowCode: args.workflowCode,
|
|
2606
|
+
scope: target.scope,
|
|
2607
|
+
...targetShape,
|
|
2608
|
+
semantics: "merge",
|
|
2609
|
+
bound: codes.every((code) => bizActions.includes(code)),
|
|
2610
|
+
bizActions,
|
|
2611
|
+
// Workflow-bound rules must be enabled; report the enable delta.
|
|
2612
|
+
enabledRuleCodes,
|
|
2613
|
+
alreadyEnabledRuleCodes,
|
|
2614
|
+
bizActionType: typeof handler?.bizActionType === "string" ? handler.bizActionType : null,
|
|
2615
|
+
templateFieldsPreserved: target.scope === "workflow"
|
|
2616
|
+
? Object.keys(draft).every((key) => key === target.field || data[key] !== undefined)
|
|
2617
|
+
: Object.keys(draft).every((key) => data[key] !== undefined),
|
|
2618
|
+
// Authoritative published-face marker, NOT the request echo: a
|
|
2619
|
+
// refused publish keeps errcode 0 and the draft marker (live 8.6.26).
|
|
2620
|
+
published: publishMarked,
|
|
2621
|
+
publishedBound: codes.every((code) => publishedBizActions.includes(code)),
|
|
2622
|
+
publishedBizActions,
|
|
2623
|
+
publishedBizActionType: typeof publishedHandler?.bizActionType === "string" ? publishedHandler.bizActionType : null,
|
|
2624
|
+
...(participantReferenceWarnings(data).length ? { participantWarnings: participantReferenceWarnings(data) } : {}),
|
|
2625
|
+
...(bizActions.length > 1
|
|
2626
|
+
? { advisory: `同一事件处理器绑定了 ${bizActions.length} 条规则,它们会**全部执行**(没有互斥):审批通过/驳回这类互斥结果不要用两条各自 FIXED 赋值的规则——两次都会写、后者覆盖前者(bench22 病灶:通过→已通过、驳回→已驳回 两条各自 FIXED 赋值都绑 endEventHandler)。正确做法二选一:① **绑定到不同处理器/触发**——通过绑流程结束处理器 \`end\`(endEventHandler)或流程生效规则 \`Available\`,驳回绑流程作废处理器 \`cancel\`(cancelEventHandler)或规则 \`Cancel\`;② **一条规则内部按条件分支**(规则图用 BRANCH/filterCondition/dataCondition 读审批结果,多分支各写各的)。真实生产(2026-09-23,case_info_update)只在 endEventHandler 绑一条多分支图(updateData),由该图内部分支完成回写。` }
|
|
2627
|
+
: {}),
|
|
2628
|
+
workflowRenderCompat: workflowRenderCompat(data),
|
|
2629
|
+
},
|
|
2630
|
+
};
|
|
2631
|
+
},
|
|
2632
|
+
preflight: (args) => {
|
|
2633
|
+
resolveBindTarget(args.handler, args.nodeEvent, args.activityCode);
|
|
2634
|
+
const hasSingle = typeof args.ruleCode === "string" && args.ruleCode.length > 0;
|
|
2635
|
+
const hasMany = Array.isArray(args.ruleCodes);
|
|
2636
|
+
if (hasSingle && hasMany)
|
|
2637
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-bindrule-codes-exclusive", message: "ruleCode 与 ruleCodes 互斥:单规则用 ruleCode,多规则用 ruleCodes[]" });
|
|
2638
|
+
if (!hasSingle && !hasMany)
|
|
2639
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-bindrule-codes-invalid", message: "需要 ruleCode(单个)或 ruleCodes(数组)之一" });
|
|
2640
|
+
if (hasMany) {
|
|
2641
|
+
const codes = args.ruleCodes;
|
|
2642
|
+
if (codes.length === 0 || new Set(codes.map(String)).size !== codes.length || codes.some((code) => typeof code !== "string" || code.length === 0))
|
|
2643
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-bindrule-codes-invalid", message: "ruleCodes 必须是非空、去重的规则 code 数组" });
|
|
2644
|
+
}
|
|
2645
|
+
},
|
|
2646
|
+
rollback: workflowRollbackSpec("workflow.bindRule"),
|
|
2647
|
+
});
|
|
2609
2648
|
/**
|
|
2610
2649
|
* Friendly writer for the designer's 「表单操作」 checkboxes: each PARTICIPANT
|
|
2611
2650
|
* activity carries `buttonPermission.customBtns[]` = the form-toolbar buttons
|
|
@@ -2616,148 +2655,166 @@ export function registerWorkflowLane(tools) {
|
|
|
2616
2655
|
* childTableBtns, which is exactly how a caller drops the child-table
|
|
2617
2656
|
* defaults — this tool merges per node.
|
|
2618
2657
|
*/
|
|
2619
|
-
writeTool(
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
.min(1),
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
}
|
|
2638
|
-
|
|
2639
|
-
const availableCodes = Object.keys(available);
|
|
2640
|
-
const draftCodes = templateActivityCodes(draft);
|
|
2641
|
-
const patches = [];
|
|
2642
|
-
const names = {};
|
|
2643
|
-
for (const node of args.nodes) {
|
|
2644
|
-
const row = findTemplateActivity(draft, node.activityCode);
|
|
2645
|
-
if (!row) {
|
|
2658
|
+
writeTool({
|
|
2659
|
+
tools,
|
|
2660
|
+
name: "workflow.setButtons",
|
|
2661
|
+
lane: WORKFLOW_LANE,
|
|
2662
|
+
scope: WORKFLOW_TOOLS["workflow.setButtons"].scope,
|
|
2663
|
+
description: WORKFLOW_TOOLS["workflow.setButtons"].description,
|
|
2664
|
+
inputSchema: {
|
|
2665
|
+
schemaCode: z.string().min(1),
|
|
2666
|
+
workflowCode: z.string().min(1),
|
|
2667
|
+
nodes: z
|
|
2668
|
+
.array(z.object({
|
|
2669
|
+
activityCode: z.string().min(1).describe("活动 code(workflow.get_draft 的 activities[].code)"),
|
|
2670
|
+
buttonCodes: z.array(z.string().min(1)).describe("该节点要**启用**的表单按钮 code(按 form.get 的 formButtonCodes);未列出的置为 enabled:false,不删除子表按钮配置"),
|
|
2671
|
+
}))
|
|
2672
|
+
.min(1),
|
|
2673
|
+
publish: z.boolean().optional().describe("默认 true:写草稿后发布,表单页立即生效;false 只写草稿"),
|
|
2674
|
+
},
|
|
2675
|
+
execute: async (session, args) => {
|
|
2676
|
+
const draft = (await getWorkflowDraftOrNull(session, args.workflowCode)) ?? {};
|
|
2677
|
+
if (Object.keys(draft).length === 0)
|
|
2646
2678
|
throw new ToolRefusalError({
|
|
2647
2679
|
status: "refused",
|
|
2648
|
-
blocker: "workflow-
|
|
2680
|
+
blocker: "workflow-not-found",
|
|
2649
2681
|
workflowCode: args.workflowCode,
|
|
2650
|
-
|
|
2651
|
-
activities: draftCodes,
|
|
2652
|
-
message: `流程 ${args.workflowCode} 没有活动 ${node.activityCode};可用:${draftCodes.join("、") || "(无)"}`,
|
|
2682
|
+
message: `流程 ${args.workflowCode} 不存在(get_draft 401001 缺席)`,
|
|
2653
2683
|
});
|
|
2654
|
-
|
|
2655
|
-
const
|
|
2656
|
-
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2684
|
+
const available = await readSchemaFormButtons(session, args.schemaCode);
|
|
2685
|
+
const availableCodes = Object.keys(available);
|
|
2686
|
+
const draftCodes = templateActivityCodes(draft);
|
|
2687
|
+
const patches = [];
|
|
2688
|
+
const names = {};
|
|
2689
|
+
for (const node of args.nodes) {
|
|
2690
|
+
const row = findTemplateActivity(draft, node.activityCode);
|
|
2691
|
+
if (!row) {
|
|
2692
|
+
throw new ToolRefusalError({
|
|
2693
|
+
status: "refused",
|
|
2694
|
+
blocker: "workflow-activity-not-found",
|
|
2695
|
+
workflowCode: args.workflowCode,
|
|
2696
|
+
activityCode: node.activityCode,
|
|
2697
|
+
activities: draftCodes,
|
|
2698
|
+
message: `流程 ${args.workflowCode} 没有活动 ${node.activityCode};可用:${draftCodes.join("、") || "(无)"}`,
|
|
2699
|
+
});
|
|
2700
|
+
}
|
|
2701
|
+
const unknown = node.buttonCodes.filter((code) => available[code] === undefined);
|
|
2702
|
+
if (unknown.length > 0) {
|
|
2703
|
+
throw new ToolRefusalError({
|
|
2704
|
+
status: "refused",
|
|
2705
|
+
blocker: "form-button-absent",
|
|
2706
|
+
schemaCode: args.schemaCode,
|
|
2707
|
+
activityCode: node.activityCode,
|
|
2708
|
+
buttonCodes: unknown,
|
|
2709
|
+
available: availableCodes,
|
|
2710
|
+
message: `按钮 ${unknown.join("、")} 不在模型 ${args.schemaCode} 的表单按钮里;先 form.publish {buttons:[…]} 生成。可用:${availableCodes.join("、") || "(无)"}`,
|
|
2711
|
+
});
|
|
2712
|
+
}
|
|
2713
|
+
const existing = (row.buttonPermission ?? {});
|
|
2714
|
+
const existingBtns = Array.isArray(existing.customBtns) ? existing.customBtns : [];
|
|
2715
|
+
const wanted = new Set(node.buttonCodes);
|
|
2716
|
+
const merged = [];
|
|
2717
|
+
for (const button of existingBtns) {
|
|
2718
|
+
const code = String(button.code ?? "");
|
|
2719
|
+
if (code === "")
|
|
2720
|
+
continue;
|
|
2721
|
+
merged.push({ ...button, enabled: wanted.has(code) });
|
|
2722
|
+
wanted.delete(code);
|
|
2723
|
+
}
|
|
2724
|
+
for (const code of wanted) {
|
|
2725
|
+
const source = available[code];
|
|
2726
|
+
names[code] = String(source.name ?? code);
|
|
2727
|
+
merged.push({ code, name: String(source.name ?? code), name_i18n: source.name_i18n ?? null, enabled: true, type: "form" });
|
|
2728
|
+
}
|
|
2729
|
+
patches.push({
|
|
2661
2730
|
activityCode: node.activityCode,
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2731
|
+
buttonPermission: {
|
|
2732
|
+
...existing,
|
|
2733
|
+
containerCode: existing.containerCode ?? args.schemaCode,
|
|
2734
|
+
customBtns: merged,
|
|
2735
|
+
childTableBtns: existing.childTableBtns ?? [],
|
|
2736
|
+
},
|
|
2665
2737
|
});
|
|
2666
2738
|
}
|
|
2667
|
-
const
|
|
2668
|
-
|
|
2669
|
-
const
|
|
2670
|
-
const
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
for (const code of wanted) {
|
|
2679
|
-
const source = available[code];
|
|
2680
|
-
names[code] = String(source.name ?? code);
|
|
2681
|
-
merged.push({ code, name: String(source.name ?? code), name_i18n: source.name_i18n ?? null, enabled: true, type: "form" });
|
|
2739
|
+
const body = await mergedTemplateBody(session, args.schemaCode, args.workflowCode, { activities: patches }, true);
|
|
2740
|
+
validateWorkflowRender(body, "workflow.setButtons");
|
|
2741
|
+
const performed = await wireCall(session, { path: ENDPOINTS.workflowUpdate, method: "POST", body });
|
|
2742
|
+
const shouldPublish = args.publish !== false;
|
|
2743
|
+
let publishMarked = false;
|
|
2744
|
+
if (shouldPublish) {
|
|
2745
|
+
const publishPerformed = await wireCall(session, { path: ENDPOINTS.workflowPublish, method: "POST", body });
|
|
2746
|
+
assertWorkflowWriteAccepted(publishPerformed, "workflow.setButtons", args.workflowCode);
|
|
2747
|
+
const pub = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.workflowCode } });
|
|
2748
|
+
const published = (pub.data ?? null);
|
|
2749
|
+
publishMarked = published !== null && (published.templateType === "PUBLISH" || published.published === true);
|
|
2682
2750
|
}
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2751
|
+
const readback = (await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode: args.workflowCode } })).data;
|
|
2752
|
+
const nodeButtons = (readback ? readback.activities : draft.activities);
|
|
2753
|
+
const rows = Array.isArray(nodeButtons)
|
|
2754
|
+
? nodeButtons
|
|
2755
|
+
: nodeButtons && typeof nodeButtons === "object"
|
|
2756
|
+
? Object.values(nodeButtons)
|
|
2757
|
+
: [];
|
|
2758
|
+
const state = args.nodes.map((node) => {
|
|
2759
|
+
const row = rows.find((entry) => String(entry.activityCode ?? "") === node.activityCode) ?? {};
|
|
2760
|
+
const bp = (row.buttonPermission ?? {});
|
|
2761
|
+
const btns = Array.isArray(bp.customBtns) ? bp.customBtns : [];
|
|
2762
|
+
return {
|
|
2763
|
+
activityCode: node.activityCode,
|
|
2764
|
+
enabled: btns.filter((button) => button.enabled === true).map((button) => String(button.code ?? "")),
|
|
2765
|
+
disabled: btns.filter((button) => button.enabled !== true).map((button) => String(button.code ?? "")),
|
|
2766
|
+
};
|
|
2691
2767
|
});
|
|
2692
|
-
}
|
|
2693
|
-
const body = await mergedTemplateBody(session, args.schemaCode, args.workflowCode, { activities: patches }, true);
|
|
2694
|
-
validateWorkflowRender(body, "workflow.setButtons");
|
|
2695
|
-
const performed = await wireCall(session, { path: ENDPOINTS.workflowUpdate, method: "POST", body });
|
|
2696
|
-
const shouldPublish = args.publish !== false;
|
|
2697
|
-
let publishMarked = false;
|
|
2698
|
-
if (shouldPublish) {
|
|
2699
|
-
const publishPerformed = await wireCall(session, { path: ENDPOINTS.workflowPublish, method: "POST", body });
|
|
2700
|
-
assertWorkflowWriteAccepted(publishPerformed, "workflow.setButtons", args.workflowCode);
|
|
2701
|
-
const pub = await wireCall(session, { path: ENDPOINTS.workflowGetPublished, query: { workflowCode: args.workflowCode } });
|
|
2702
|
-
const published = (pub.data ?? null);
|
|
2703
|
-
publishMarked = published !== null && (published.templateType === "PUBLISH" || published.published === true);
|
|
2704
|
-
}
|
|
2705
|
-
const readback = (await wireCall(session, { path: ENDPOINTS.workflowGetDraft, query: { workflowCode: args.workflowCode } })).data;
|
|
2706
|
-
const nodeButtons = (readback ? readback.activities : draft.activities);
|
|
2707
|
-
const rows = Array.isArray(nodeButtons)
|
|
2708
|
-
? nodeButtons
|
|
2709
|
-
: nodeButtons && typeof nodeButtons === "object"
|
|
2710
|
-
? Object.values(nodeButtons)
|
|
2711
|
-
: [];
|
|
2712
|
-
const state = args.nodes.map((node) => {
|
|
2713
|
-
const row = rows.find((entry) => String(entry.activityCode ?? "") === node.activityCode) ?? {};
|
|
2714
|
-
const bp = (row.buttonPermission ?? {});
|
|
2715
|
-
const btns = Array.isArray(bp.customBtns) ? bp.customBtns : [];
|
|
2716
2768
|
return {
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2769
|
+
performed: { errcode: performed.errcode, publishRequested: shouldPublish, nodes: args.nodes.map((node) => node.activityCode) },
|
|
2770
|
+
readback: {
|
|
2771
|
+
workflowCode: args.workflowCode,
|
|
2772
|
+
nodes: state,
|
|
2773
|
+
published: publishMarked,
|
|
2774
|
+
buttonNames: names,
|
|
2775
|
+
note: "节点未列出的按钮被置为 enabled:false(不删除子表按钮配置);表单页只在 enabled:true 的节点显示该按钮",
|
|
2776
|
+
},
|
|
2720
2777
|
};
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
try {
|
|
2736
|
-
performed = await wireCall(session, { path: ENDPOINTS.workflowDelete, method: "POST", query: { workflowCode: args.code } });
|
|
2737
|
-
}
|
|
2738
|
-
catch (error) {
|
|
2739
|
-
// 402508 流程实例已存在 — the platform refuses on purpose. Previously it
|
|
2740
|
-
// arrived as a bare wire code, so the caller's next move was to delete the
|
|
2741
|
-
// MODEL instead (which cascades the template away and leaves orphan todos
|
|
2742
|
-
// pointing at nothing — r14's 8× 50000). Name the order.
|
|
2743
|
-
if (error instanceof WireError && error.kind === "business" && error.errcode === 402508) {
|
|
2744
|
-
const pending = await countPendingWorkItems(session, args.code);
|
|
2745
|
-
throw new ToolRefusalError({
|
|
2746
|
-
status: "refused",
|
|
2747
|
-
blocker: "workflow-has-instances",
|
|
2748
|
-
workflowCode: args.code,
|
|
2749
|
-
...(pending > 0 ? { pendingWorkItems: pending } : {}),
|
|
2750
|
-
nextCommand: `omc call workitem.list --args '${JSON.stringify({ workflowCode: args.code })}'`,
|
|
2751
|
-
message: `流程 ${args.code} 还有流程实例,平台拒绝删除(402508)。**不要改用 model.delete 绕过**——那会把流程模板级联删掉,` +
|
|
2752
|
-
`留下指向空模板的待办,而平台全局待办搜索会给每条待办解析模板,**一条缺失就整页 NPE(50000),所有用户的待办列表一起坏掉**(r14 实测 8 连击,需维护者清库)。` +
|
|
2753
|
-
`正确顺序:① 先完结/清理实例的待办(workitem.list --workflowCode ${args.code} 看还有哪些在办);② 再 workflow.delete;③ 最后才 model.delete。`,
|
|
2754
|
-
});
|
|
2778
|
+
},
|
|
2779
|
+
rollback: workflowRollbackSpec("workflow.update"),
|
|
2780
|
+
});
|
|
2781
|
+
writeTool({
|
|
2782
|
+
tools,
|
|
2783
|
+
name: "workflow.delete",
|
|
2784
|
+
lane: WORKFLOW_LANE,
|
|
2785
|
+
scope: WORKFLOW_TOOLS["workflow.delete"].scope,
|
|
2786
|
+
description: WORKFLOW_TOOLS["workflow.delete"].description,
|
|
2787
|
+
inputSchema: { schemaCode: z.string().min(1), code: z.string().min(1).describe("流程编码——本参数名是 code") },
|
|
2788
|
+
execute: async (session, args) => {
|
|
2789
|
+
let performed;
|
|
2790
|
+
try {
|
|
2791
|
+
performed = await wireCall(session, { path: ENDPOINTS.workflowDelete, method: "POST", query: { workflowCode: args.code } });
|
|
2755
2792
|
}
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2793
|
+
catch (error) {
|
|
2794
|
+
// 402508 流程实例已存在 — the platform refuses on purpose. Previously it
|
|
2795
|
+
// arrived as a bare wire code, so the caller's next move was to delete the
|
|
2796
|
+
// MODEL instead (which cascades the template away and leaves orphan todos
|
|
2797
|
+
// pointing at nothing — r14's 8× 50000). Name the order.
|
|
2798
|
+
if (error instanceof WireError && error.kind === "business" && error.errcode === 402508) {
|
|
2799
|
+
const pending = await countPendingWorkItems(session, args.code);
|
|
2800
|
+
throw new ToolRefusalError({
|
|
2801
|
+
status: "refused",
|
|
2802
|
+
blocker: "workflow-has-instances",
|
|
2803
|
+
workflowCode: args.code,
|
|
2804
|
+
...(pending > 0 ? { pendingWorkItems: pending } : {}),
|
|
2805
|
+
nextCommand: `omc call workitem.list --args '${JSON.stringify({ workflowCode: args.code })}'`,
|
|
2806
|
+
message: `流程 ${args.code} 还有流程实例,平台拒绝删除(402508)。**不要改用 model.delete 绕过**——那会把流程模板级联删掉,` +
|
|
2807
|
+
`留下指向空模板的待办,而平台全局待办搜索会给每条待办解析模板,**一条缺失就整页 NPE(50000),所有用户的待办列表一起坏掉**(r14 实测 8 连击,需维护者清库)。` +
|
|
2808
|
+
`正确顺序:① 先完结/清理实例的待办(workitem.list --workflowCode ${args.code} 看还有哪些在办);② 再 workflow.delete;③ 最后才 model.delete。`,
|
|
2809
|
+
});
|
|
2810
|
+
}
|
|
2811
|
+
throw error;
|
|
2812
|
+
}
|
|
2813
|
+
const header = await wireCall(session, { path: ENDPOINTS.workflowGetHeader, query: { workflowCode: args.code } }).catch(() => ({ data: null }));
|
|
2814
|
+
return { performed: { errcode: performed.errcode }, readback: { workflowCode: args.code, absent: header.data === null || header.data === undefined } };
|
|
2815
|
+
},
|
|
2816
|
+
rollback: workflowRollbackSpec("workflow.delete"),
|
|
2817
|
+
});
|
|
2761
2818
|
// ---- high-level greenfield orchestration (round 10 ergonomics) --------
|
|
2762
2819
|
// The survey's write-face gap: a first workflow took create → update →
|
|
2763
2820
|
// publish (+ bindRule for rules), 3–4 separate CLI calls. configure_workflow
|
|
@@ -3101,103 +3158,119 @@ export function registerWorkflowLane(tools) {
|
|
|
3101
3158
|
};
|
|
3102
3159
|
}),
|
|
3103
3160
|
}));
|
|
3104
|
-
writeTool(
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
// Send the unit's own name/code too: `list_permission` reads them back
|
|
3122
|
-
// (it echoes `name`/`code`), which is the only way `workflow.permissions`
|
|
3123
|
-
// can describe a row by more than its opaque permission id.
|
|
3124
|
-
const identity = await unitIdentity(session, unitType, args.unitId);
|
|
3125
|
-
await wireCall(session, {
|
|
3126
|
-
path: ENDPOINTS.workflowCreatePermission,
|
|
3127
|
-
method: "POST",
|
|
3128
|
-
body: {
|
|
3129
|
-
workflowCode,
|
|
3130
|
-
unitId: args.unitId,
|
|
3131
|
-
unitType,
|
|
3132
|
-
corpId: args.corpId ?? session.enterpriseId(),
|
|
3133
|
-
...(identity.name ? { name: identity.name } : {}),
|
|
3134
|
-
...(identity.code ? { code: identity.code } : {}),
|
|
3135
|
-
},
|
|
3136
|
-
});
|
|
3137
|
-
}
|
|
3138
|
-
catch (error) {
|
|
3139
|
-
const errcode = error.details?.errcode ?? error.errcode;
|
|
3140
|
-
if (String(errcode ?? "") !== "402003")
|
|
3141
|
-
throw new ToolRefusalError({
|
|
3142
|
-
status: "refused",
|
|
3143
|
-
blocker: "workflow-permission-write-failed",
|
|
3144
|
-
workflowCode,
|
|
3145
|
-
unitId: args.unitId,
|
|
3146
|
-
unitType,
|
|
3147
|
-
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)}`,
|
|
3148
|
-
});
|
|
3149
|
-
return {
|
|
3150
|
-
performed: { skipped: "already-present" },
|
|
3151
|
-
readback: { workflowCode, alreadyPresent: true, unitId: args.unitId, unitType, count: before.length, note: "该主体已有发起权限(平台 402003),未重复写入" },
|
|
3152
|
-
};
|
|
3153
|
-
}
|
|
3154
|
-
const after = await permissionRows(session, workflowCode);
|
|
3155
|
-
const created = after.find((row) => !before.some((old) => String(old.id ?? "") === String(row.id ?? ""))) ?? after[after.length - 1];
|
|
3156
|
-
return {
|
|
3157
|
-
performed: { errcode: 0 },
|
|
3158
|
-
readback: {
|
|
3159
|
-
workflowCode,
|
|
3160
|
-
count: after.length,
|
|
3161
|
-
permissionId: created?.id ?? null,
|
|
3162
|
-
unitType: created?.unitType ?? unitType,
|
|
3163
|
-
name: created?.name ?? null,
|
|
3164
|
-
note: "发起权限已授予。若之前 data.submit 回 40009,现在应可发起;仍失败先 workflow.permissions 确认行存在,并核对发起人所属部门/角色。",
|
|
3165
|
-
},
|
|
3166
|
-
};
|
|
3167
|
-
});
|
|
3168
|
-
writeTool(tools, "workflow.removePermission", WORKFLOW_LANE, WORKFLOW_TOOLS["workflow.removePermission"].scope, WORKFLOW_TOOLS["workflow.removePermission"].description, {
|
|
3169
|
-
permissionId: z.string().min(1).optional().describe("首选:workflow.permissions 返回的 id(唯一确定)"),
|
|
3170
|
-
workflowCode: z.string().optional().describe("配合 unitId+unitType 使用(平台的 list_permission 不回 unitId,需按显示名解析)"),
|
|
3171
|
-
unitId: z.string().min(1).optional().describe("部门/角色/用户 id;按 unitType 解析显示名后匹配权限行"),
|
|
3172
|
-
unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).optional().describe("unitId 的类型:1=部门 2=角色 3=用户"),
|
|
3173
|
-
schemaCode: z.string().optional(),
|
|
3174
|
-
}, async (session, args) => {
|
|
3175
|
-
let permissionId = args.permissionId;
|
|
3176
|
-
if (!permissionId) {
|
|
3177
|
-
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode });
|
|
3178
|
-
if (!workflowCode || !args.unitId || args.unitType === undefined)
|
|
3179
|
-
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "workflow.removePermission", message: "需要 permissionId(推荐),或 workflowCode(/schemaCode)+ unitId + unitType" });
|
|
3161
|
+
writeTool({
|
|
3162
|
+
tools,
|
|
3163
|
+
name: "workflow.addPermission",
|
|
3164
|
+
lane: WORKFLOW_LANE,
|
|
3165
|
+
scope: WORKFLOW_TOOLS["workflow.addPermission"].scope,
|
|
3166
|
+
description: WORKFLOW_TOOLS["workflow.addPermission"].description,
|
|
3167
|
+
inputSchema: {
|
|
3168
|
+
workflowCode: z.string().optional().describe("流程 code;省略则用 schemaCode 解析"),
|
|
3169
|
+
schemaCode: z.string().optional().describe("模型 code:省略 workflowCode 时用它解析该模型的流程"),
|
|
3170
|
+
unitId: z.string().min(1).describe("部门 id / 角色 id / 用户 id(看 unitType)"),
|
|
3171
|
+
unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).describe("1=部门 2=角色 3=用户;也接受 DEPARTMENT/ROLE/USER"),
|
|
3172
|
+
corpId: z.string().optional().describe("企业 id;省略则用当前会话企业"),
|
|
3173
|
+
},
|
|
3174
|
+
execute: async (session, args) => {
|
|
3175
|
+
const workflowCode = await resolveWorkflowArg(session, args);
|
|
3176
|
+
if (!workflowCode)
|
|
3177
|
+
throw new ToolRefusalError({ status: "refused", blocker: "workflow-not-found", message: `模型 ${String(args.schemaCode ?? "")} 没有流程;先 workflow.create` });
|
|
3180
3178
|
const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3179
|
+
// Idempotent retry: the platform answers 402003 WORKFLOW_PERMISSION_EXIST
|
|
3180
|
+
// for a duplicate unitId, which would read as a failure. `list_permission`
|
|
3181
|
+
// does NOT echo unitId (only id/name/unitType/fullName/code), so identity
|
|
3182
|
+
// is decided by the platform's own refusal rather than a guessed pre-read.
|
|
3183
|
+
const before = await permissionRows(session, workflowCode);
|
|
3184
|
+
try {
|
|
3185
|
+
// Send the unit's own name/code too: `list_permission` reads them back
|
|
3186
|
+
// (it echoes `name`/`code`), which is the only way `workflow.permissions`
|
|
3187
|
+
// can describe a row by more than its opaque permission id.
|
|
3188
|
+
const identity = await unitIdentity(session, unitType, args.unitId);
|
|
3189
|
+
await wireCall(session, {
|
|
3190
|
+
path: ENDPOINTS.workflowCreatePermission,
|
|
3191
|
+
method: "POST",
|
|
3192
|
+
body: {
|
|
3191
3193
|
workflowCode,
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3194
|
+
unitId: args.unitId,
|
|
3195
|
+
unitType,
|
|
3196
|
+
corpId: args.corpId ?? session.enterpriseId(),
|
|
3197
|
+
...(identity.name ? { name: identity.name } : {}),
|
|
3198
|
+
...(identity.code ? { code: identity.code } : {}),
|
|
3195
3199
|
},
|
|
3200
|
+
});
|
|
3201
|
+
}
|
|
3202
|
+
catch (error) {
|
|
3203
|
+
const errcode = error.details?.errcode ?? error.errcode;
|
|
3204
|
+
if (String(errcode ?? "") !== "402003")
|
|
3205
|
+
throw new ToolRefusalError({
|
|
3206
|
+
status: "refused",
|
|
3207
|
+
blocker: "workflow-permission-write-failed",
|
|
3208
|
+
workflowCode,
|
|
3209
|
+
unitId: args.unitId,
|
|
3210
|
+
unitType,
|
|
3211
|
+
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
|
+
});
|
|
3213
|
+
return {
|
|
3214
|
+
performed: { skipped: "already-present" },
|
|
3215
|
+
readback: { workflowCode, alreadyPresent: true, unitId: args.unitId, unitType, count: before.length, note: "该主体已有发起权限(平台 402003),未重复写入" },
|
|
3196
3216
|
};
|
|
3197
|
-
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3217
|
+
}
|
|
3218
|
+
const after = await permissionRows(session, workflowCode);
|
|
3219
|
+
const created = after.find((row) => !before.some((old) => String(old.id ?? "") === String(row.id ?? ""))) ?? after[after.length - 1];
|
|
3220
|
+
return {
|
|
3221
|
+
performed: { errcode: 0 },
|
|
3222
|
+
readback: {
|
|
3223
|
+
workflowCode,
|
|
3224
|
+
count: after.length,
|
|
3225
|
+
permissionId: created?.id ?? null,
|
|
3226
|
+
unitType: created?.unitType ?? unitType,
|
|
3227
|
+
name: created?.name ?? null,
|
|
3228
|
+
note: "发起权限已授予。若之前 data.submit 回 40009,现在应可发起;仍失败先 workflow.permissions 确认行存在,并核对发起人所属部门/角色。",
|
|
3229
|
+
},
|
|
3230
|
+
};
|
|
3231
|
+
},
|
|
3232
|
+
});
|
|
3233
|
+
writeTool({
|
|
3234
|
+
tools,
|
|
3235
|
+
name: "workflow.removePermission",
|
|
3236
|
+
lane: WORKFLOW_LANE,
|
|
3237
|
+
scope: WORKFLOW_TOOLS["workflow.removePermission"].scope,
|
|
3238
|
+
description: WORKFLOW_TOOLS["workflow.removePermission"].description,
|
|
3239
|
+
inputSchema: {
|
|
3240
|
+
permissionId: z.string().min(1).optional().describe("首选:workflow.permissions 返回的 id(唯一确定)"),
|
|
3241
|
+
workflowCode: z.string().optional().describe("配合 unitId+unitType 使用(平台的 list_permission 不回 unitId,需按显示名解析)"),
|
|
3242
|
+
unitId: z.string().min(1).optional().describe("部门/角色/用户 id;按 unitType 解析显示名后匹配权限行"),
|
|
3243
|
+
unitType: z.union([z.literal(1), z.literal(2), z.literal(3), z.literal("DEPARTMENT"), z.literal("ROLE"), z.literal("USER")]).optional().describe("unitId 的类型:1=部门 2=角色 3=用户"),
|
|
3244
|
+
schemaCode: z.string().optional(),
|
|
3245
|
+
},
|
|
3246
|
+
execute: async (session, args) => {
|
|
3247
|
+
let permissionId = args.permissionId;
|
|
3248
|
+
if (!permissionId) {
|
|
3249
|
+
const workflowCode = await resolveWorkflowArg(session, { workflowCode: args.workflowCode, schemaCode: args.schemaCode });
|
|
3250
|
+
if (!workflowCode || !args.unitId || args.unitType === undefined)
|
|
3251
|
+
throw new ToolInputError({ status: "error", blocker: "tool-input-invalid", tool: "workflow.removePermission", message: "需要 permissionId(推荐),或 workflowCode(/schemaCode)+ unitId + unitType" });
|
|
3252
|
+
const unitType = args.unitType === 1 || args.unitType === "DEPARTMENT" ? 1 : args.unitType === 2 || args.unitType === "ROLE" ? 2 : 3;
|
|
3253
|
+
const identity = await unitIdentity(session, unitType, args.unitId);
|
|
3254
|
+
const rows = await permissionRows(session, workflowCode);
|
|
3255
|
+
const wantedType = String(unitType);
|
|
3256
|
+
const hit = rows.find((row) => String(row.unitType ?? "") === wantedType && (String(row.id ?? "") === args.unitId ||
|
|
3257
|
+
(identity.name !== null && String(row.name ?? "") === identity.name) ||
|
|
3258
|
+
(identity.code !== null && String(row.code ?? "") === identity.code)));
|
|
3259
|
+
if (!hit)
|
|
3260
|
+
return {
|
|
3261
|
+
performed: { skipped: "absent" },
|
|
3262
|
+
readback: {
|
|
3263
|
+
workflowCode,
|
|
3264
|
+
removed: false,
|
|
3265
|
+
note: `没有匹配该主体的权限行(幂等)。注意 list_permission 不回 unitId,匹配靠解析出的显示名「${identity.name ?? "?"}」;不确定时用 workflow.permissions 拿 id 再按 permissionId 删。`,
|
|
3266
|
+
permissions: rows.map((row) => ({ id: row.id ?? null, name: row.name ?? null, unitType: row.unitType ?? null })),
|
|
3267
|
+
},
|
|
3268
|
+
};
|
|
3269
|
+
permissionId = String(hit.id ?? "");
|
|
3270
|
+
}
|
|
3271
|
+
await wireCall(session, { path: ENDPOINTS.workflowDeletePermission, method: "POST", query: { permissionId } });
|
|
3272
|
+
return { performed: { errcode: 0 }, readback: { permissionId, removed: true } };
|
|
3273
|
+
},
|
|
3201
3274
|
});
|
|
3202
3275
|
tools.push(defineTool({
|
|
3203
3276
|
name: "org.role.list",
|
|
@@ -3261,6 +3334,23 @@ export function registerWorkflowLane(tools) {
|
|
|
3261
3334
|
parentId: dept.parentId ?? null,
|
|
3262
3335
|
}));
|
|
3263
3336
|
return { count: departments.length, departments };
|
|
3337
|
+
}, {
|
|
3338
|
+
// r28h F2: default CODE_NAME_LIST_KEYS collapses departments to
|
|
3339
|
+
// `id|name` and drops parentId — the one field this tool exists for.
|
|
3340
|
+
// Keep a dedicated slim shape that preserves hierarchy.
|
|
3341
|
+
compact: (result) => {
|
|
3342
|
+
const rows = Array.isArray(result.departments) ? result.departments : [];
|
|
3343
|
+
return {
|
|
3344
|
+
count: result.count ?? rows.length,
|
|
3345
|
+
departments: rows.map((row) => ({
|
|
3346
|
+
id: row.id ?? null,
|
|
3347
|
+
name: row.name ?? null,
|
|
3348
|
+
code: row.code ?? null,
|
|
3349
|
+
parentId: row.parentId ?? null,
|
|
3350
|
+
})),
|
|
3351
|
+
departmentsCount: rows.length,
|
|
3352
|
+
};
|
|
3353
|
+
},
|
|
3264
3354
|
}),
|
|
3265
3355
|
}));
|
|
3266
3356
|
tools.push(defineTool({
|
|
@@ -3290,6 +3380,29 @@ export function registerWorkflowLane(tools) {
|
|
|
3290
3380
|
// `wd` = 0..9 to discover users — 11 calls, ~16 KB, to learn a roster the
|
|
3291
3381
|
// platform can already answer. Omitted keyword now aggregates
|
|
3292
3382
|
// `department/list_user` over the department tree (one tool call).
|
|
3383
|
+
// r28h F4: ≤50 users inline as {id,name,username,departmentId}; larger
|
|
3384
|
+
// rosters fall back to count + artifact. Default summarizeValue was folding
|
|
3385
|
+
// a 12-person roster into `{count,sha256,omitted:true}`.
|
|
3386
|
+
const userSearchCompact = (result) => {
|
|
3387
|
+
const rows = Array.isArray(result.users) ? result.users : [];
|
|
3388
|
+
const slim = rows.map((row) => ({
|
|
3389
|
+
id: row.id ?? null,
|
|
3390
|
+
name: row.name ?? null,
|
|
3391
|
+
username: row.username ?? null,
|
|
3392
|
+
departmentId: row.departmentId ?? null,
|
|
3393
|
+
}));
|
|
3394
|
+
return {
|
|
3395
|
+
keyword: result.keyword ?? null,
|
|
3396
|
+
count: result.count ?? rows.length,
|
|
3397
|
+
total: result.total ?? rows.length,
|
|
3398
|
+
...(result.departmentsScanned !== undefined ? { departmentsScanned: result.departmentsScanned } : {}),
|
|
3399
|
+
...(result.truncated === true ? { truncated: true } : {}),
|
|
3400
|
+
...(typeof result.note === "string" ? { note: result.note } : {}),
|
|
3401
|
+
...(typeof result.hint === "string" ? { hint: result.hint } : {}),
|
|
3402
|
+
users: slim.length <= 50 ? slim : { count: slim.length, omitted: true },
|
|
3403
|
+
...(slim.length > 50 ? { usersCount: slim.length } : {}),
|
|
3404
|
+
};
|
|
3405
|
+
};
|
|
3293
3406
|
if (!keyword) {
|
|
3294
3407
|
return governedRead(ctx, self, args, async (session) => {
|
|
3295
3408
|
const orgQuery = { corpId: session.enterpriseId() };
|
|
@@ -3325,7 +3438,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3325
3438
|
users,
|
|
3326
3439
|
hint: "无关键字=列全员(按部门树聚合)。平台 user/search 的 wd 不支持空串(10005),所以这条路径走 department/list_user;只要某部门的人用 org.department.users。",
|
|
3327
3440
|
};
|
|
3328
|
-
});
|
|
3441
|
+
}, { compact: userSearchCompact });
|
|
3329
3442
|
}
|
|
3330
3443
|
return governedRead(ctx, self, { ...args, wd: keyword }, async (session) => {
|
|
3331
3444
|
const body = { wd: keyword, page: args.page ?? 0, size: args.size ?? 100 };
|
|
@@ -3344,7 +3457,7 @@ export function registerWorkflowLane(tools) {
|
|
|
3344
3457
|
status: user.status ?? null,
|
|
3345
3458
|
}));
|
|
3346
3459
|
return { keyword, count: users.length, total: data?.totalElements ?? users.length, users };
|
|
3347
|
-
});
|
|
3460
|
+
}, { compact: userSearchCompact });
|
|
3348
3461
|
},
|
|
3349
3462
|
}));
|
|
3350
3463
|
}
|
|
@@ -3380,25 +3493,30 @@ function isRoleGroup(row) {
|
|
|
3380
3493
|
export function flattenRoleTree(rows) {
|
|
3381
3494
|
const roles = [];
|
|
3382
3495
|
const groups = [];
|
|
3383
|
-
const walk = (nodes,
|
|
3496
|
+
const walk = (nodes, containers) => {
|
|
3384
3497
|
for (const node of nodes) {
|
|
3385
3498
|
if (!isRoleGroup(node)) {
|
|
3386
|
-
|
|
3499
|
+
const parent = containers.length > 0 ? containers[containers.length - 1] : null;
|
|
3500
|
+
roles.push(roleOf(node, parent));
|
|
3387
3501
|
continue;
|
|
3388
3502
|
}
|
|
3389
3503
|
const name = String(node.name ?? "");
|
|
3390
|
-
const
|
|
3504
|
+
const id = String(node.id ?? "");
|
|
3505
|
+
const code = node.code == null || node.code === "" ? null : String(node.code);
|
|
3506
|
+
const path = [...containers.map((container) => container.name), name].filter((part) => part.length > 0).join(" / ");
|
|
3391
3507
|
const children = roleChildren(node);
|
|
3508
|
+
const parent = containers.length > 0 ? containers[containers.length - 1] : null;
|
|
3392
3509
|
groups.push({
|
|
3393
3510
|
id: node.id ?? null,
|
|
3394
3511
|
name: node.name ?? null,
|
|
3395
3512
|
code: node.code ?? null,
|
|
3396
3513
|
corpId: node.corpId ?? null,
|
|
3397
|
-
|
|
3514
|
+
parentId: parent?.id ?? null,
|
|
3515
|
+
path,
|
|
3398
3516
|
childCount: children.length,
|
|
3399
3517
|
});
|
|
3400
3518
|
if (children.length > 0)
|
|
3401
|
-
walk(children,
|
|
3519
|
+
walk(children, [...containers, { id, code, name }]);
|
|
3402
3520
|
}
|
|
3403
3521
|
};
|
|
3404
3522
|
walk(rows, []);
|
|
@@ -3418,13 +3536,22 @@ export function flattenRoleTree(rows) {
|
|
|
3418
3536
|
};
|
|
3419
3537
|
}
|
|
3420
3538
|
function roleOf(row, group) {
|
|
3539
|
+
// r28h F6: roles used to carry only `group:<name>` — write faces need the
|
|
3540
|
+
// group id/code. Keep `group` (name) for back-compat and add explicit FKs.
|
|
3421
3541
|
return {
|
|
3422
3542
|
id: row.id ?? null,
|
|
3423
3543
|
name: row.name ?? null,
|
|
3424
3544
|
code: row.code ?? null,
|
|
3425
3545
|
roleType: row.roleType ?? null,
|
|
3426
3546
|
corpId: row.corpId ?? null,
|
|
3427
|
-
...(group !== null
|
|
3547
|
+
...(group !== null
|
|
3548
|
+
? {
|
|
3549
|
+
group: group.name,
|
|
3550
|
+
groupId: group.id || null,
|
|
3551
|
+
groupCode: group.code,
|
|
3552
|
+
groupName: group.name,
|
|
3553
|
+
}
|
|
3554
|
+
: {}),
|
|
3428
3555
|
};
|
|
3429
3556
|
}
|
|
3430
3557
|
//# sourceMappingURL=workflow.js.map
|