@stalfh233/omc-cli 0.6.7 → 0.6.11
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/model.js
CHANGED
|
@@ -16,6 +16,7 @@ import { defineTool, ToolInputError, ToolRefusalError } from "../tool-types.js";
|
|
|
16
16
|
import { wireCall, wireRows, WireError } from "../wire.js";
|
|
17
17
|
import { readModelRecord } from "../model-record.js";
|
|
18
18
|
import { MODEL_CREATE_BODY_DEFAULTS, MODEL_LANE_CONTRACT } from "./model-contract.js";
|
|
19
|
+
import { flattenFolders } from "./folder.js";
|
|
19
20
|
const MODEL_LANE = MODEL_LANE_CONTRACT.lane;
|
|
20
21
|
const MODEL_TOOLS = MODEL_LANE_CONTRACT.tools;
|
|
21
22
|
/**
|
|
@@ -413,97 +414,132 @@ export function registerModelLane(tools) {
|
|
|
413
414
|
},
|
|
414
415
|
}),
|
|
415
416
|
}));
|
|
416
|
-
writeTool(
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
const
|
|
437
|
-
|
|
417
|
+
writeTool({
|
|
418
|
+
tools,
|
|
419
|
+
name: "model.create",
|
|
420
|
+
lane: MODEL_LANE,
|
|
421
|
+
scope: MODEL_TOOLS["model.create"].scope,
|
|
422
|
+
description: MODEL_TOOLS["model.create"].description,
|
|
423
|
+
inputSchema: {
|
|
424
|
+
name: z.string().min(1),
|
|
425
|
+
code: z.string().min(1),
|
|
426
|
+
folderId: z
|
|
427
|
+
.string()
|
|
428
|
+
.optional()
|
|
429
|
+
.describe("目标目录 id(必须是 folder.tree 里 type=Folder 的 id;也接受该目录 code,工具归一为 id)。省略则落到应用树第一个目录(常为平台默认分组)。不要传目录名。"),
|
|
430
|
+
applicationCode: z
|
|
431
|
+
.string()
|
|
432
|
+
.optional()
|
|
433
|
+
.describe("模型所属应用 code(应用包,不是目录)。省略用配置默认应用;folder.* 的 appCode 别名本工具不认,请用 applicationCode。"),
|
|
434
|
+
modelType: z.enum(["LIST", "TREE"]).default("LIST"),
|
|
435
|
+
},
|
|
436
|
+
execute: async (session, args) => {
|
|
437
|
+
const applicationCode = await resolveApplicationCode(session, args.applicationCode);
|
|
438
|
+
const existing = await resolveModelTreeNode(session, args.code, args.applicationCode);
|
|
439
|
+
if (existing.node) {
|
|
438
440
|
throw new ToolRefusalError({
|
|
439
441
|
status: "refused",
|
|
440
|
-
blocker: "model-create-duplicate
|
|
442
|
+
blocker: "model-create-duplicate",
|
|
441
443
|
schemaCode: args.code,
|
|
442
|
-
applicationCode:
|
|
443
|
-
|
|
444
|
-
message: `模型 ${args.code} 在册(bizmodels/get_all 可读)但不在应用树中。**先跑一次 model.delete**——它现在会把两种成因分开:` +
|
|
445
|
-
`① 读缓存滞后(记录其实已删)→ 回 already-absent/stale-record-cache,随后可直接 create;` +
|
|
446
|
-
`② 真孤儿(删除级联中断留下的残骸)→ 回 model-orphaned-record 并给平台侧出路(回收站物理删除 / 换 code),因为 8.6 平台无法删除没有 AppFunction 行的业务模型。` +
|
|
447
|
-
`不要重复 create(会再造一条孤儿),也不要只信这一条拒绝信息。`,
|
|
444
|
+
applicationCode: existing.applicationCode,
|
|
445
|
+
message: `模型 ${args.code} 已存在于应用 ${existing.applicationCode} 的应用树中;create 拒绝重复 POST,不能把既有模型证明为本次创建结果`,
|
|
448
446
|
});
|
|
449
447
|
}
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
status: "refused",
|
|
473
|
-
blocker: "model-create-folder-unresolved",
|
|
474
|
-
message: `folderId ${folderId} 未命中应用 ${applicationCode} 树中任何目录(id/code 双口径)——不要把 apppackage/list 的目录 code 当 id 传;候选(id|code|name):${candidates.join(";") || "(无目录)"}`,
|
|
475
|
-
applicationCode,
|
|
476
|
-
});
|
|
448
|
+
// A tree-miss does NOT prove absence: an orphan mount (row exists in
|
|
449
|
+
// h_biz_schema but its h_app_function.parentId is invalid) is invisible to
|
|
450
|
+
// the tree scan but real on the record face. Refuse instead of creating a
|
|
451
|
+
// second row and leaving the first orphaned (live 2026-10-17).
|
|
452
|
+
try {
|
|
453
|
+
// bizmodels/get_all is keyed by the query, so a non-null object IS the
|
|
454
|
+
// model (an absent one answers a business errcode — wireCall throws and
|
|
455
|
+
// we fall through to create).
|
|
456
|
+
const record = await readModelRecord(session, args.code);
|
|
457
|
+
if (record !== null && typeof record === "object") {
|
|
458
|
+
throw new ToolRefusalError({
|
|
459
|
+
status: "refused",
|
|
460
|
+
blocker: "model-create-duplicate-orphan",
|
|
461
|
+
schemaCode: args.code,
|
|
462
|
+
applicationCode: args.applicationCode ?? null,
|
|
463
|
+
nextCommand: `omc call model.delete --env <env> --args '{"schemaCode":"${args.code}"}'`,
|
|
464
|
+
message: `模型 ${args.code} 在册(bizmodels/get_all 可读)但不在应用树中。**先跑一次 model.delete**——它现在会把两种成因分开:` +
|
|
465
|
+
`① 读缓存滞后(记录其实已删)→ 回 already-absent/stale-record-cache,随后可直接 create;` +
|
|
466
|
+
`② 真孤儿(删除级联中断留下的残骸)→ 回 model-orphaned-record 并给平台侧出路(回收站物理删除 / 换 code),因为 8.6 平台无法删除没有 AppFunction 行的业务模型。` +
|
|
467
|
+
`不要重复 create(会再造一条孤儿),也不要只信这一条拒绝信息。`,
|
|
468
|
+
});
|
|
469
|
+
}
|
|
477
470
|
}
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
471
|
+
catch (error) {
|
|
472
|
+
if (error instanceof ToolRefusalError)
|
|
473
|
+
throw error;
|
|
474
|
+
// A record-face read failure must not be read as absence; let create
|
|
475
|
+
// proceed and rely on the platform's unique key.
|
|
476
|
+
}
|
|
477
|
+
// Cross-face key trap (live 2026-10-17, AI-Platform 1.1.7): apppackage/list
|
|
478
|
+
// projects a Folder node's h_app_function.CODE, while apppackage/trees rows
|
|
479
|
+
// carry the function ID. Passing the former straight through as
|
|
480
|
+
// bizmodels/create.parentId writes an orphan row that the tree assembler
|
|
481
|
+
// drops — the model reads fine on schema faces but vanishes from the menu,
|
|
482
|
+
// model.list(tree) and duplicate checks. Resolve BOTH keys and always send
|
|
483
|
+
// the tree row id.
|
|
484
|
+
const tree = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
485
|
+
let folderId = args.folderId;
|
|
486
|
+
let resolvedFrom = args.folderId ? "explicit" : undefined;
|
|
487
|
+
if (folderId) {
|
|
488
|
+
const folder = tree.find((row) => row.type !== "BizModel" && typeof row.id === "string" && (String(row.id) === folderId || String(row.code ?? "") === folderId));
|
|
489
|
+
if (!folder) {
|
|
490
|
+
const candidates = tree.filter((row) => row.type !== "BizModel" && typeof row.id === "string").map((row) => `${String(row.id)}|${String(row.code ?? "")}|${String(row.name ?? "")}`);
|
|
491
|
+
throw new ToolRefusalError({
|
|
492
|
+
status: "refused",
|
|
493
|
+
blocker: "model-create-folder-unresolved",
|
|
494
|
+
message: `folderId ${folderId} 未命中应用 ${applicationCode} 树中任何目录(id/code 双口径)——不要把 apppackage/list 的目录 code 当 id 传;候选(id|code|name):${candidates.join(";") || "(无目录)"}`,
|
|
495
|
+
applicationCode,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
resolvedFrom = String(folder.id) === folderId ? "explicit" : "explicit-code-resolved";
|
|
499
|
+
folderId = String(folder.id);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
const folder = tree.find((row) => row.type !== "BizModel" && typeof row.id === "string");
|
|
503
|
+
if (!folder)
|
|
504
|
+
throw new WireError("business", "no folder found for model creation", 302001);
|
|
505
|
+
folderId = String(folder.id);
|
|
506
|
+
resolvedFrom = "auto-default-folder";
|
|
507
|
+
}
|
|
508
|
+
const performed = await wireCall(session, {
|
|
509
|
+
path: ENDPOINTS.modelCreate,
|
|
510
|
+
method: "POST",
|
|
511
|
+
body: {
|
|
512
|
+
appCode: applicationCode,
|
|
513
|
+
name: args.name,
|
|
514
|
+
parentId: folderId,
|
|
515
|
+
modelType: args.modelType ?? MODEL_CREATE_BODY_DEFAULTS.modelType,
|
|
516
|
+
bizSchemaCode: args.code,
|
|
517
|
+
icon: MODEL_CREATE_BODY_DEFAULTS.icon,
|
|
518
|
+
name_i18n: nameI18nJson(args.name),
|
|
519
|
+
pcAble: MODEL_CREATE_BODY_DEFAULTS.pcAble,
|
|
520
|
+
mobileAble: MODEL_CREATE_BODY_DEFAULTS.mobileAble,
|
|
521
|
+
options: MODEL_CREATE_BODY_DEFAULTS.options,
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
// r28d F4: prove placement with the same path face folder.create uses
|
|
525
|
+
// (flattenFolders), not a bare present:true.
|
|
526
|
+
const afterTree = flattenFolders(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data));
|
|
527
|
+
const createdNode = afterTree.find((row) => row.type === "BizModel" && row.code === args.code);
|
|
528
|
+
const present = createdNode !== undefined;
|
|
529
|
+
return {
|
|
530
|
+
performed: { errcode: performed.errcode, folderId, resolvedFrom, applicationCode },
|
|
531
|
+
readback: {
|
|
532
|
+
present,
|
|
533
|
+
schemaCode: args.code,
|
|
534
|
+
folderId: createdNode ? String(createdNode.parentId ?? folderId) : folderId,
|
|
535
|
+
path: createdNode ? String(createdNode.path ?? "") : null,
|
|
536
|
+
...(createdNode?.displayPath ? { displayPath: createdNode.displayPath } : {}),
|
|
537
|
+
applicationCode,
|
|
538
|
+
},
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
rollback: modelRollbackSpec("model.create"),
|
|
542
|
+
});
|
|
507
543
|
/** model.delete writeTool extra arg: a session-aware dry-run that scans inbound references (no write). */
|
|
508
544
|
async function modelDeleteDryRun(session, args) {
|
|
509
545
|
const record = await (async () => {
|
|
@@ -547,6 +583,9 @@ export function registerModelLane(tools) {
|
|
|
547
583
|
referencingModels: referencing,
|
|
548
584
|
treeNodeResolved: nodeResolved,
|
|
549
585
|
applicationCode: resolved.applicationCode,
|
|
586
|
+
// r28g F30:plan 以前只报对外引用,不说本模型内部从属资源会被级联清掉。
|
|
587
|
+
// 计数走轻量读面;失败不挡 dry-run(仍给定性 cascadeNote)。
|
|
588
|
+
cascadeNote: "删除模型会级联清掉其字段/表单/列表视图/字段级数据规则/业务规则(含平台预置触发行)及运行数据;其它模型对本模型的关联字段不会自动拆——见 mayBeReferenced/referencingModels。预置规则在幻影 schema 上 list 会重灌,故删前勿对已删 schemaCode 再 rule.list。",
|
|
550
589
|
...(referencing.length > 0
|
|
551
590
|
? { blocker: "model-referenced-by", nextAction: "先删引用方模型或先删其关联字段,再删本模型(依赖倒序)" }
|
|
552
591
|
: !nodeResolved
|
|
@@ -554,455 +593,514 @@ export function registerModelLane(tools) {
|
|
|
554
593
|
: {}),
|
|
555
594
|
};
|
|
556
595
|
}
|
|
557
|
-
writeTool(
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
//
|
|
576
|
-
//
|
|
577
|
-
//
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
596
|
+
writeTool({
|
|
597
|
+
tools,
|
|
598
|
+
name: "model.delete",
|
|
599
|
+
lane: MODEL_LANE,
|
|
600
|
+
scope: MODEL_TOOLS["model.delete"].scope,
|
|
601
|
+
description: MODEL_TOOLS["model.delete"].description,
|
|
602
|
+
inputSchema: {
|
|
603
|
+
schemaCode: z.string().min(1),
|
|
604
|
+
applicationCode: z
|
|
605
|
+
.string()
|
|
606
|
+
.optional()
|
|
607
|
+
.describe("模型所属应用 code(应用包)。省略时先查配置默认应用,再扫其它应用树定位节点;跨应用删除建议显式传入。"),
|
|
608
|
+
},
|
|
609
|
+
execute: async (session, args) => {
|
|
610
|
+
// authoritative model record (bizmodels/get_all); the app-tree node id
|
|
611
|
+
// may lag behind and is cleaned best-effort.
|
|
612
|
+
const readRecord = () => readModelRecord(session, args.schemaCode);
|
|
613
|
+
const record = await readRecord();
|
|
614
|
+
// Ticket 68: resolve the app-tree node across applications (the tree row
|
|
615
|
+
// id IS the AppFunction id physically_delete needs). Never fall back to
|
|
616
|
+
// the biz-model record id — the platform answers 601003 业务模型不存在
|
|
617
|
+
// for it (removeBizModel looks the id up as an AppFunction id).
|
|
618
|
+
const resolved = await resolveModelTreeNode(session, args.schemaCode, args.applicationCode);
|
|
619
|
+
const node = resolved.node;
|
|
620
|
+
const applicationCode = resolved.applicationCode;
|
|
621
|
+
const treeRows = async () => applicationCode ? flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: applicationCode } })).data)) : [];
|
|
622
|
+
if (!node) {
|
|
623
|
+
if (record === null)
|
|
624
|
+
return { performed: { skipped: "already-absent" }, readback: { absent: true } };
|
|
625
|
+
// r12 (2026-10-24): "record readable but no tree node" has TWO causes and
|
|
626
|
+
// the caller could not tell them apart — create said "go fix parentId",
|
|
627
|
+
// model.move said "there is no node", model.delete said "no node means
|
|
628
|
+
// no delete": a closed loop with no exit.
|
|
629
|
+
//
|
|
630
|
+
// Cause A: a stale read face. `bizmodels/get_all` is served through a
|
|
631
|
+
// cache, so a record already removed by a previous delete (or by an
|
|
632
|
+
// interrupted cascade) keeps answering for a while. Re-read after a
|
|
633
|
+
// short wait before declaring anything.
|
|
634
|
+
// ~7s total: long enough to clear a warm read cache, short enough that
|
|
635
|
+
// a genuine orphan is reported without a long stall. Tests can shrink it
|
|
636
|
+
// with OMC_MODEL_ORPHAN_RECHECK_MS.
|
|
637
|
+
const recheckDelay = Number(process.env.OMC_MODEL_ORPHAN_RECHECK_MS ?? 1000);
|
|
638
|
+
let stillPresent = record;
|
|
639
|
+
for (let attempt = 0; attempt < 4 && stillPresent !== null; attempt += 1) {
|
|
640
|
+
await new Promise((resolve) => setTimeout(resolve, Number.isFinite(recheckDelay) ? recheckDelay : 1000));
|
|
641
|
+
stillPresent = await readRecord();
|
|
642
|
+
}
|
|
643
|
+
if (stillPresent === null) {
|
|
644
|
+
return {
|
|
645
|
+
performed: { skipped: "stale-record-cache" },
|
|
646
|
+
readback: {
|
|
647
|
+
absent: true,
|
|
648
|
+
note: "业务模型记录读取面(bizmodels/get_all)先前仍返回该 code,复查后已消失——这是读缓存滞后的残留,模型实际已删除。若刚跑过 model.create 又被拒「已在册」,现在可以重试。",
|
|
649
|
+
},
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
// Cause B: a genuine orphan. The platform has no path: physically_delete
|
|
653
|
+
// resolves the id through AppFunction and `removeBizModel` throws
|
|
654
|
+
// APP_FUNCTION_MODEL_NOTEXIST when the row is gone
|
|
655
|
+
// (AppIntegrationFacadeImpl:1905-1910), so no id can satisfy it.
|
|
656
|
+
throw new ToolRefusalError({
|
|
657
|
+
status: "refused",
|
|
658
|
+
blocker: "model-orphaned-record",
|
|
659
|
+
schemaCode: args.schemaCode,
|
|
660
|
+
retryable: false,
|
|
661
|
+
platformLimitation: "removeBizModel(AppIntegrationFacadeImpl:1905) 先 getAppFunction(id),找不到即抛 APP_FUNCTION_MODEL_NOTEXIST——没有 AppFunction 行的业务模型记录在 8.6 平台上无法通过 physically_delete 删除。",
|
|
662
|
+
message: `${args.schemaCode} 的业务模型记录仍在(bizmodels/get_all 持续返回),但应用树里没有它的节点(AppFunction)。` +
|
|
663
|
+
`**这是平台侧的不可删状态**:physically_delete 用 AppFunction id 定位,而平台 removeBizModel 找不到 AppFunction 行时抛 APP_FUNCTION_MODEL_NOTEXIST——本工具不猜测 id(传业务记录 id 只会得到 601003 业务模型不存在),因此无法通过 physically_delete 删除。` +
|
|
664
|
+
`已连续复查 4 次、间隔约 1s,记录仍在,因此不是读缓存滞后。` +
|
|
665
|
+
`已知成因:删除级联被中断(逐列异步 40-60s,中途失败会留下「记录在、节点没了」的残骸)。` +
|
|
666
|
+
`可行出路(按代价排序):① 用平台「回收站 → 物理删除」清理该残留(apppackage/physically_deleteAll 同一路径);` +
|
|
667
|
+
`② 换一个未占用的 schema code 继续(r11/r12 探针均采用此法);` +
|
|
668
|
+
`③ 若你有平台数据库权限,删除 h_biz_schema/h_biz_property/h_biz_sheet 对应行并重启 launcher 清缓存——但这是维护者操作,不在工具权限内。` +
|
|
669
|
+
`**不要再重复调用 model.delete / model.move / model.create**:三方都会给出同样的答复。`,
|
|
670
|
+
});
|
|
599
671
|
}
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
//
|
|
603
|
-
//
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
const treeRowList = resolved.treeRows.length > 0 ? resolved.treeRows : await treeRows();
|
|
622
|
-
// Ticket 23 inbound-reference precheck: the platform's sync face answers
|
|
623
|
-
// errcode 0 while the async deleteFunctions dispatcher refuses with
|
|
624
|
-
// "xxx被模型 yyy 引用,不能删除" — surface that refusal at call time with
|
|
625
|
-
// the referencing models, before any write reaches the environment.
|
|
626
|
-
const siblings = treeRowList
|
|
627
|
-
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code !== args.schemaCode)
|
|
628
|
-
.map((row) => ({ schemaCode: String(row.code), name: typeof row.name === "string" && row.name ? row.name : String(row.code) }));
|
|
629
|
-
const referencingModels = [];
|
|
630
|
-
for (const sibling of siblings) {
|
|
631
|
-
const referencedFields = [];
|
|
632
|
-
for (const isPublish of ["false", "true"]) {
|
|
633
|
-
const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: sibling.schemaCode, isPublish } });
|
|
634
|
-
for (const row of wireRows(listed.data)) {
|
|
635
|
-
if (relationTargetOf(row) !== args.schemaCode)
|
|
636
|
-
continue;
|
|
637
|
-
const code = row.code ?? row.propertyCode;
|
|
638
|
-
if (typeof code === "string" && !referencedFields.some((field) => field.code === code)) {
|
|
639
|
-
referencedFields.push({ code, type: row.type ?? row.propertyType });
|
|
672
|
+
const targetId = node.id;
|
|
673
|
+
const treeRowList = resolved.treeRows.length > 0 ? resolved.treeRows : await treeRows();
|
|
674
|
+
// Ticket 23 inbound-reference precheck: the platform's sync face answers
|
|
675
|
+
// errcode 0 while the async deleteFunctions dispatcher refuses with
|
|
676
|
+
// "xxx被模型 yyy 引用,不能删除" — surface that refusal at call time with
|
|
677
|
+
// the referencing models, before any write reaches the environment.
|
|
678
|
+
const siblings = treeRowList
|
|
679
|
+
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && row.code !== args.schemaCode)
|
|
680
|
+
.map((row) => ({ schemaCode: String(row.code), name: typeof row.name === "string" && row.name ? row.name : String(row.code) }));
|
|
681
|
+
const referencingModels = [];
|
|
682
|
+
for (const sibling of siblings) {
|
|
683
|
+
const referencedFields = [];
|
|
684
|
+
for (const isPublish of ["false", "true"]) {
|
|
685
|
+
const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: sibling.schemaCode, isPublish } });
|
|
686
|
+
for (const row of wireRows(listed.data)) {
|
|
687
|
+
if (relationTargetOf(row) !== args.schemaCode)
|
|
688
|
+
continue;
|
|
689
|
+
const code = row.code ?? row.propertyCode;
|
|
690
|
+
if (typeof code === "string" && !referencedFields.some((field) => field.code === code)) {
|
|
691
|
+
referencedFields.push({ code, type: row.type ?? row.propertyType });
|
|
692
|
+
}
|
|
640
693
|
}
|
|
641
694
|
}
|
|
695
|
+
if (referencedFields.length > 0)
|
|
696
|
+
referencingModels.push({ schemaCode: sibling.schemaCode, name: sibling.name, fields: referencedFields });
|
|
642
697
|
}
|
|
643
|
-
if (
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
698
|
+
if (referencingModels.length > 0) {
|
|
699
|
+
throw new ToolRefusalError({
|
|
700
|
+
status: "refused",
|
|
701
|
+
blocker: "model-referenced-by",
|
|
702
|
+
schemaCode: args.schemaCode,
|
|
703
|
+
referencingModels,
|
|
704
|
+
deletionOrder: [...referencingModels.map((model) => model.schemaCode), args.schemaCode],
|
|
705
|
+
nextCommand: `omc call model.delete --args '${JSON.stringify({ schemaCode: referencingModels[0].schemaCode })}'`,
|
|
706
|
+
message: `${args.schemaCode} 被模型 ${referencingModels.map((model) => model.schemaCode).join("、")} 的关联字段引用,不能删除(服务端 deleteFunctions 的异步拒绝已提前到调用时)。依赖倒序清理:先删引用方模型(或先删其关联字段),再删被引用模型;引用方物理删除的逐列异步级联需 40-60s,完成后再删本模型。`,
|
|
707
|
+
});
|
|
708
|
+
}
|
|
709
|
+
// Model deletion CASCADES the workflow template away but leaves
|
|
710
|
+
// `biz_workitem` rows pointing at it. The platform's global todo search
|
|
711
|
+
// resolves every hit's template, so ONE orphan NPEs the whole page for
|
|
712
|
+
// everyone (r14 live: 8 consecutive 50000s; cleanup needed a maintainer
|
|
713
|
+
// with DB access). Warn while it is still actionable.
|
|
714
|
+
const liveWorkflows = await pendingWorkflowItems(session, args.schemaCode);
|
|
715
|
+
const performed = await wireCall(session, {
|
|
716
|
+
path: ENDPOINTS.appPackagePhysicallyDelete,
|
|
717
|
+
method: "POST",
|
|
718
|
+
body: [{ id: targetId, code: args.schemaCode, type: "BizModel" }],
|
|
655
719
|
});
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
body: [{ id: targetId, code: args.schemaCode, type: "BizModel" }],
|
|
667
|
-
});
|
|
668
|
-
// Both the model record and the app-tree node are eventually consistent
|
|
669
|
-
// (the per-column async cascade takes 3-4s per column, 40-60s for a
|
|
670
|
-
// 12-column model). Poll the authoritative record face for verified
|
|
671
|
-
// absence (~60s budget), then report the tree-node state as a pending
|
|
672
|
-
// hint with explicit wait-and-recheck guidance instead of a failure.
|
|
673
|
-
let absent = false;
|
|
674
|
-
for (let attempt = 0; attempt < 16 && !absent; attempt += 1) {
|
|
675
|
-
if (attempt > 0)
|
|
676
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * attempt, 4000)));
|
|
677
|
-
absent = (await readRecord()) === null;
|
|
678
|
-
}
|
|
679
|
-
const treeNodePending = (await treeRows()).some((row) => row.type === "BizModel" && row.code === args.schemaCode);
|
|
680
|
-
return {
|
|
681
|
-
performed: { errcode: performed.errcode, deletedId: targetId, applicationCode },
|
|
682
|
-
readback: {
|
|
683
|
-
absent,
|
|
684
|
-
treeNodePending,
|
|
685
|
-
...(liveWorkflows.length > 0
|
|
686
|
-
? {
|
|
687
|
-
workflowWarning: {
|
|
688
|
-
code: "model-deleted-with-pending-work-items",
|
|
689
|
-
severity: "warning",
|
|
690
|
-
workflows: liveWorkflows,
|
|
691
|
-
message: `删除前该模型的流程还有未完结待办(${liveWorkflows.map((row) => `${row.workflowCode}:${row.pending}`).join("、")})。` +
|
|
692
|
-
`模型删除会级联掉流程模板,但**不会**清掉这些待办;它们从此指向空模板,而平台全局待办搜索会给每条待办解析模板 —— **一条缺失就整页 NPE(50000),所有用户的待办列表一起坏掉**(r14 实测 8 连击,只能靠维护者清库恢复)。` +
|
|
693
|
-
`若 now 已经删了:用 workitem.list 的 \`workflowCode\` 或 \`participant\` 参数绕开全局搜索,并请维护者清理这些脏待办。下次请先完结实例再删模型。`,
|
|
694
|
-
},
|
|
695
|
-
}
|
|
696
|
-
: {}),
|
|
697
|
-
...(absent
|
|
698
|
-
? {}
|
|
699
|
-
: {
|
|
700
|
-
nextAction: "wait-and-recheck",
|
|
701
|
-
note: "异步逐列级联实测 40-60s:absent:false + treeNodePending:true 表示删除级联仍在进行,不是失败。等待约 60s 后重跑 model.delete(幂等,already-absent 短路)复核;若刚删除引用方模型,其级联可能仍在拆除关联列,必须等它完成后再删本模型。",
|
|
702
|
-
}),
|
|
703
|
-
},
|
|
704
|
-
};
|
|
705
|
-
}, undefined, modelDeleteDryRun, modelRollbackSpec("model.delete"));
|
|
706
|
-
writeTool(tools, "model.deleteMany", MODEL_LANE, MODEL_TOOLS["model.deleteMany"].scope, MODEL_TOOLS["model.deleteMany"].description, { schemaCodes: z.array(z.string().min(1)).min(1), applicationCode: z.string().optional() }, async (session, args) => {
|
|
707
|
-
// Platform evidence: physically_delete takes the WHOLE list in ONE
|
|
708
|
-
// request (AppPackageController.java:1424 @RequestBody
|
|
709
|
-
// List<AppDeleteParamVO>, 8.6.26 decompiled-full). The batch tool sends
|
|
710
|
-
// every existing model in that single request after prechecking each
|
|
711
|
-
// target's inbound references — a referenced model refuses the whole
|
|
712
|
-
// batch with per-target evidence and ZERO side effects.
|
|
713
|
-
const readRecord = (schemaCode) => readModelRecord(session, schemaCode);
|
|
714
|
-
const applicationCode = await resolveApplicationCode(session, args.applicationCode).catch(() => "");
|
|
715
|
-
const targets = [...new Set(args.schemaCodes)];
|
|
716
|
-
const alreadyAbsent = [];
|
|
717
|
-
const entries = [];
|
|
718
|
-
const unresolved = [];
|
|
719
|
-
// Ticket 68: resolve each model's app-tree node across applications; the
|
|
720
|
-
// tree id (AppFunction id) is what physically_delete needs. Collect the
|
|
721
|
-
// resolved trees for the sibling scan (cross-application references too).
|
|
722
|
-
const resolvedTrees = [];
|
|
723
|
-
const seenTrees = new Set();
|
|
724
|
-
const appsToScan = new Set();
|
|
725
|
-
for (const code of targets) {
|
|
726
|
-
const resolved = await resolveModelTreeNode(session, code, args.applicationCode);
|
|
727
|
-
appsToScan.add(resolved.applicationCode);
|
|
728
|
-
if (!seenTrees.has(resolved.applicationCode) && resolved.treeRows.length > 0) {
|
|
729
|
-
seenTrees.add(resolved.applicationCode);
|
|
730
|
-
resolvedTrees.push(...resolved.treeRows);
|
|
720
|
+
// Both the model record and the app-tree node are eventually consistent
|
|
721
|
+
// (the per-column async cascade takes 3-4s per column, 40-60s for a
|
|
722
|
+
// 12-column model). Poll the authoritative record face for verified
|
|
723
|
+
// absence (~60s budget), then report the tree-node state as a pending
|
|
724
|
+
// hint with explicit wait-and-recheck guidance instead of a failure.
|
|
725
|
+
let absent = false;
|
|
726
|
+
for (let attempt = 0; attempt < 16 && !absent; attempt += 1) {
|
|
727
|
+
if (attempt > 0)
|
|
728
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * attempt, 4000)));
|
|
729
|
+
absent = (await readRecord()) === null;
|
|
731
730
|
}
|
|
732
|
-
|
|
733
|
-
|
|
731
|
+
const treeNodePending = (await treeRows()).some((row) => row.type === "BizModel" && row.code === args.schemaCode);
|
|
732
|
+
return {
|
|
733
|
+
performed: { errcode: performed.errcode, deletedId: targetId, applicationCode },
|
|
734
|
+
readback: {
|
|
735
|
+
absent,
|
|
736
|
+
treeNodePending,
|
|
737
|
+
...(liveWorkflows.length > 0
|
|
738
|
+
? {
|
|
739
|
+
workflowWarning: {
|
|
740
|
+
code: "model-deleted-with-pending-work-items",
|
|
741
|
+
severity: "warning",
|
|
742
|
+
workflows: liveWorkflows,
|
|
743
|
+
message: `删除前该模型的流程还有未完结待办(${liveWorkflows.map((row) => `${row.workflowCode}:${row.pending}`).join("、")})。` +
|
|
744
|
+
`模型删除会级联掉流程模板,但**不会**清掉这些待办;它们从此指向空模板,而平台全局待办搜索会给每条待办解析模板 —— **一条缺失就整页 NPE(50000),所有用户的待办列表一起坏掉**(r14 实测 8 连击,只能靠维护者清库恢复)。` +
|
|
745
|
+
`若 now 已经删了:用 workitem.list 的 \`workflowCode\` 或 \`participant\` 参数绕开全局搜索,并请维护者清理这些脏待办。下次请先完结实例再删模型。`,
|
|
746
|
+
},
|
|
747
|
+
}
|
|
748
|
+
: {}),
|
|
749
|
+
...(absent
|
|
750
|
+
? {}
|
|
751
|
+
: {
|
|
752
|
+
nextAction: "wait-and-recheck",
|
|
753
|
+
note: "异步逐列级联实测 40-60s:absent:false + treeNodePending:true 表示删除级联仍在进行,不是失败。等待约 60s 后重跑 model.delete(幂等,already-absent 短路)复核;若刚删除引用方模型,其级联可能仍在拆除关联列,必须等它完成后再删本模型。",
|
|
754
|
+
}),
|
|
755
|
+
},
|
|
756
|
+
};
|
|
757
|
+
},
|
|
758
|
+
dryRunPayload: modelDeleteDryRun,
|
|
759
|
+
rollback: modelRollbackSpec("model.delete"),
|
|
760
|
+
});
|
|
761
|
+
writeTool({
|
|
762
|
+
tools,
|
|
763
|
+
name: "model.deleteMany",
|
|
764
|
+
lane: MODEL_LANE,
|
|
765
|
+
scope: MODEL_TOOLS["model.deleteMany"].scope,
|
|
766
|
+
description: MODEL_TOOLS["model.deleteMany"].description,
|
|
767
|
+
inputSchema: { schemaCodes: z.array(z.string().min(1)).min(1), applicationCode: z.string().optional() },
|
|
768
|
+
execute: async (session, args) => {
|
|
769
|
+
// Platform evidence: physically_delete takes the WHOLE list in ONE
|
|
770
|
+
// request (AppPackageController.java:1424 @RequestBody
|
|
771
|
+
// List<AppDeleteParamVO>, 8.6.26 decompiled-full). The batch tool sends
|
|
772
|
+
// every existing model in that single request after prechecking each
|
|
773
|
+
// target's inbound references — a referenced model refuses the whole
|
|
774
|
+
// batch with per-target evidence and ZERO side effects.
|
|
775
|
+
const readRecord = (schemaCode) => readModelRecord(session, schemaCode);
|
|
776
|
+
const applicationCode = await resolveApplicationCode(session, args.applicationCode).catch(() => "");
|
|
777
|
+
const targets = [...new Set(args.schemaCodes)];
|
|
778
|
+
const alreadyAbsent = [];
|
|
779
|
+
const entries = [];
|
|
780
|
+
const unresolved = [];
|
|
781
|
+
// Ticket 68: resolve each model's app-tree node across applications; the
|
|
782
|
+
// tree id (AppFunction id) is what physically_delete needs. Collect the
|
|
783
|
+
// resolved trees for the sibling scan (cross-application references too).
|
|
784
|
+
const resolvedTrees = [];
|
|
785
|
+
const seenTrees = new Set();
|
|
786
|
+
const appsToScan = new Set();
|
|
787
|
+
for (const code of targets) {
|
|
788
|
+
const resolved = await resolveModelTreeNode(session, code, args.applicationCode);
|
|
789
|
+
appsToScan.add(resolved.applicationCode);
|
|
790
|
+
if (!seenTrees.has(resolved.applicationCode) && resolved.treeRows.length > 0) {
|
|
791
|
+
seenTrees.add(resolved.applicationCode);
|
|
792
|
+
resolvedTrees.push(...resolved.treeRows);
|
|
793
|
+
}
|
|
794
|
+
if (resolved.node?.id) {
|
|
795
|
+
entries.push({ id: resolved.node.id, code, type: "BizModel" });
|
|
796
|
+
}
|
|
797
|
+
else if ((await readRecord(code)) === null) {
|
|
798
|
+
alreadyAbsent.push(code);
|
|
799
|
+
}
|
|
800
|
+
else {
|
|
801
|
+
unresolved.push(code);
|
|
802
|
+
}
|
|
734
803
|
}
|
|
735
|
-
|
|
736
|
-
|
|
804
|
+
if (unresolved.length > 0) {
|
|
805
|
+
throw new ToolRefusalError({
|
|
806
|
+
status: "refused",
|
|
807
|
+
blocker: "model-application-unresolved",
|
|
808
|
+
unresolved,
|
|
809
|
+
message: `批量删除被拒(零副作用):${unresolved.join("、")} 的业务模型记录仍存在但在任何应用树中都找不到节点(AppFunction)——physically_delete 需要 AppFunction id,不能拿业务模型记录 id 猜测。请显式传 applicationCode,或先用 discover_environment 确认归属应用。`,
|
|
810
|
+
});
|
|
737
811
|
}
|
|
738
|
-
|
|
739
|
-
|
|
812
|
+
const treeRowList = resolvedTrees;
|
|
813
|
+
// Per-target inbound-reference precheck (ticket 23 semantics, scanned
|
|
814
|
+
// once per sibling across all batch targets). Siblings INSIDE the batch
|
|
815
|
+
// are being deleted together and therefore do not block.
|
|
816
|
+
const batchSet = new Set(targets);
|
|
817
|
+
const siblings = treeRowList
|
|
818
|
+
.filter((row) => row.type === "BizModel" && typeof row.code === "string" && !batchSet.has(String(row.code)))
|
|
819
|
+
.map((row) => ({ schemaCode: String(row.code), name: typeof row.name === "string" && row.name ? row.name : String(row.code) }));
|
|
820
|
+
const blockedTargets = [];
|
|
821
|
+
for (const sibling of siblings) {
|
|
822
|
+
const hitsByTarget = new Map();
|
|
823
|
+
for (const isPublish of ["false", "true"]) {
|
|
824
|
+
const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: sibling.schemaCode, isPublish } });
|
|
825
|
+
for (const row of wireRows(listed.data)) {
|
|
826
|
+
const target = relationTargetOf(row);
|
|
827
|
+
if (target === undefined || !batchSet.has(target))
|
|
828
|
+
continue;
|
|
829
|
+
const code = row.code ?? row.propertyCode;
|
|
830
|
+
if (typeof code !== "string")
|
|
831
|
+
continue;
|
|
832
|
+
const hits = hitsByTarget.get(target) ?? [];
|
|
833
|
+
if (!hits.some((field) => field.code === code))
|
|
834
|
+
hits.push({ code, type: row.type ?? row.propertyType });
|
|
835
|
+
hitsByTarget.set(target, hits);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
for (const [target, fields] of hitsByTarget) {
|
|
839
|
+
const blocked = blockedTargets.find((entry) => entry.schemaCode === target) ?? { schemaCode: target, referencingModels: [] };
|
|
840
|
+
blocked.referencingModels.push({ schemaCode: sibling.schemaCode, name: sibling.name, fields });
|
|
841
|
+
if (!blockedTargets.includes(blocked))
|
|
842
|
+
blockedTargets.push(blocked);
|
|
843
|
+
}
|
|
740
844
|
}
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
845
|
+
if (blockedTargets.length > 0) {
|
|
846
|
+
throw new ToolRefusalError({
|
|
847
|
+
status: "refused",
|
|
848
|
+
blocker: "model-referenced-by",
|
|
849
|
+
blockedTargets,
|
|
850
|
+
message: `批量删除被拒(零副作用):${blockedTargets.map((blocked) => `${blocked.schemaCode} 被模型 ${blocked.referencingModels.map((model) => model.schemaCode).join("、")} 的关联字段引用`).join(";")}。依赖倒序清理:把引用方模型一并纳入 schemaCodes(或先删其关联字段);引用方物理删除的逐列异步级联需 40-60s,完成后再删被引用模型。`,
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
if (entries.length === 0) {
|
|
854
|
+
return { performed: { skipped: "already-absent", alreadyAbsent }, readback: { perCode: Object.fromEntries(targets.map((code) => [code, { absent: true }])), allAbsent: true } };
|
|
855
|
+
}
|
|
856
|
+
const performed = await wireCall(session, {
|
|
857
|
+
path: ENDPOINTS.appPackagePhysicallyDelete,
|
|
858
|
+
method: "POST",
|
|
859
|
+
body: entries,
|
|
748
860
|
});
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
const blockedTargets = [];
|
|
759
|
-
for (const sibling of siblings) {
|
|
760
|
-
const hitsByTarget = new Map();
|
|
761
|
-
for (const isPublish of ["false", "true"]) {
|
|
762
|
-
const listed = await wireCall(session, { path: ENDPOINTS.fieldList, query: { schemaCode: sibling.schemaCode, isPublish } });
|
|
763
|
-
for (const row of wireRows(listed.data)) {
|
|
764
|
-
const target = relationTargetOf(row);
|
|
765
|
-
if (target === undefined || !batchSet.has(target))
|
|
861
|
+
// Per-model absence polling on the authoritative record face (the async
|
|
862
|
+
// per-column cascade takes 40-60s per model); pending codes carry the
|
|
863
|
+
// same wait-and-recheck guidance as model.delete.
|
|
864
|
+
const absentSet = new Set();
|
|
865
|
+
for (let attempt = 0; attempt < 16 && absentSet.size < entries.length; attempt += 1) {
|
|
866
|
+
if (attempt > 0)
|
|
867
|
+
await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * attempt, 4000)));
|
|
868
|
+
for (const entry of entries) {
|
|
869
|
+
if (absentSet.has(entry.code))
|
|
766
870
|
continue;
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
continue;
|
|
770
|
-
const hits = hitsByTarget.get(target) ?? [];
|
|
771
|
-
if (!hits.some((field) => field.code === code))
|
|
772
|
-
hits.push({ code, type: row.type ?? row.propertyType });
|
|
773
|
-
hitsByTarget.set(target, hits);
|
|
871
|
+
if ((await readRecord(entry.code)) === null)
|
|
872
|
+
absentSet.add(entry.code);
|
|
774
873
|
}
|
|
775
874
|
}
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
if (blockedTargets.length > 0) {
|
|
784
|
-
throw new ToolRefusalError({
|
|
785
|
-
status: "refused",
|
|
786
|
-
blocker: "model-referenced-by",
|
|
787
|
-
blockedTargets,
|
|
788
|
-
message: `批量删除被拒(零副作用):${blockedTargets.map((blocked) => `${blocked.schemaCode} 被模型 ${blocked.referencingModels.map((model) => model.schemaCode).join("、")} 的关联字段引用`).join(";")}。依赖倒序清理:把引用方模型一并纳入 schemaCodes(或先删其关联字段);引用方物理删除的逐列异步级联需 40-60s,完成后再删被引用模型。`,
|
|
789
|
-
});
|
|
790
|
-
}
|
|
791
|
-
if (entries.length === 0) {
|
|
792
|
-
return { performed: { skipped: "already-absent", alreadyAbsent }, readback: { perCode: Object.fromEntries(targets.map((code) => [code, { absent: true }])), allAbsent: true } };
|
|
793
|
-
}
|
|
794
|
-
const performed = await wireCall(session, {
|
|
795
|
-
path: ENDPOINTS.appPackagePhysicallyDelete,
|
|
796
|
-
method: "POST",
|
|
797
|
-
body: entries,
|
|
798
|
-
});
|
|
799
|
-
// Per-model absence polling on the authoritative record face (the async
|
|
800
|
-
// per-column cascade takes 40-60s per model); pending codes carry the
|
|
801
|
-
// same wait-and-recheck guidance as model.delete.
|
|
802
|
-
const absentSet = new Set();
|
|
803
|
-
for (let attempt = 0; attempt < 16 && absentSet.size < entries.length; attempt += 1) {
|
|
804
|
-
if (attempt > 0)
|
|
805
|
-
await new Promise((resolve) => setTimeout(resolve, Math.min(1000 * attempt, 4000)));
|
|
806
|
-
for (const entry of entries) {
|
|
807
|
-
if (absentSet.has(entry.code))
|
|
875
|
+
const afterTree = (await Promise.all([...appsToScan]
|
|
876
|
+
.filter((appCode) => appCode)
|
|
877
|
+
.map(async (appCode) => flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data))))).flat();
|
|
878
|
+
const perCode = {};
|
|
879
|
+
for (const code of targets) {
|
|
880
|
+
if (alreadyAbsent.includes(code)) {
|
|
881
|
+
perCode[code] = { absent: true };
|
|
808
882
|
continue;
|
|
809
|
-
|
|
810
|
-
|
|
883
|
+
}
|
|
884
|
+
const absent = absentSet.has(code);
|
|
885
|
+
const treeNodePending = afterTree.some((row) => row.type === "BizModel" && row.code === code);
|
|
886
|
+
perCode[code] = absent ? { absent, ...(treeNodePending ? { treeNodePending } : {}) } : { absent, treeNodePending, nextAction: "wait-and-recheck", note: "异步逐列级联实测 40-60s:absent:false 表示该模型的删除级联仍在进行,不是失败。等待约 60s 后重跑 model.deleteMany(幂等,already-absent 短路)复核。" };
|
|
811
887
|
}
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
888
|
+
const allAbsent = targets.every((code) => perCode[code]?.absent === true);
|
|
889
|
+
return {
|
|
890
|
+
performed: { errcode: performed.errcode, deletedIds: entries.map((entry) => entry.id), alreadyAbsent, requested: targets, applicationCode },
|
|
891
|
+
readback: {
|
|
892
|
+
perCode,
|
|
893
|
+
allAbsent,
|
|
894
|
+
...(allAbsent ? {} : { nextAction: "wait-and-recheck", note: "部分模型仍在异步级联中;按 perCode 逐项查看,等待约 60s 后重跑(幂等)。" }),
|
|
895
|
+
},
|
|
896
|
+
};
|
|
897
|
+
},
|
|
898
|
+
preflight: (args) => {
|
|
899
|
+
const codes = args.schemaCodes;
|
|
900
|
+
if (!Array.isArray(codes) || codes.length === 0 || new Set(codes).size !== codes.length || codes.some((code) => typeof code !== "string" || code.length === 0)) {
|
|
901
|
+
throw new ToolRefusalError({ status: "refused", blocker: "model-delete-many-invalid", message: "schemaCodes 必须是非空、去重的模型 code 数组" });
|
|
821
902
|
}
|
|
822
|
-
|
|
823
|
-
const treeNodePending = afterTree.some((row) => row.type === "BizModel" && row.code === code);
|
|
824
|
-
perCode[code] = absent ? { absent, ...(treeNodePending ? { treeNodePending } : {}) } : { absent, treeNodePending, nextAction: "wait-and-recheck", note: "异步逐列级联实测 40-60s:absent:false 表示该模型的删除级联仍在进行,不是失败。等待约 60s 后重跑 model.deleteMany(幂等,already-absent 短路)复核。" };
|
|
825
|
-
}
|
|
826
|
-
const allAbsent = targets.every((code) => perCode[code]?.absent === true);
|
|
827
|
-
return {
|
|
828
|
-
performed: { errcode: performed.errcode, deletedIds: entries.map((entry) => entry.id), alreadyAbsent, requested: targets, applicationCode },
|
|
829
|
-
readback: {
|
|
830
|
-
perCode,
|
|
831
|
-
allAbsent,
|
|
832
|
-
...(allAbsent ? {} : { nextAction: "wait-and-recheck", note: "部分模型仍在异步级联中;按 perCode 逐项查看,等待约 60s 后重跑(幂等)。" }),
|
|
833
|
-
},
|
|
834
|
-
};
|
|
835
|
-
}, (args) => {
|
|
836
|
-
const codes = args.schemaCodes;
|
|
837
|
-
if (!Array.isArray(codes) || codes.length === 0 || new Set(codes).size !== codes.length || codes.some((code) => typeof code !== "string" || code.length === 0)) {
|
|
838
|
-
throw new ToolRefusalError({ status: "refused", blocker: "model-delete-many-invalid", message: "schemaCodes 必须是非空、去重的模型 code 数组" });
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
writeTool(tools, "model.setSummary", MODEL_LANE, MODEL_TOOLS["model.setSummary"].scope, MODEL_TOOLS["model.setSummary"].description, {
|
|
842
|
-
schemaCode: z.string().min(1),
|
|
843
|
-
summary: z.string().min(1).describe('数据标题表达式,线上存储形状是 Summary[] JSON(isDataItem 1=表单字段、2=查询字段),如 [{"isDataItem":1,"code":"subject"}];也接受纯 {字段} 占位串(自动转换为 Summary[])。含字面文本的模板无法表示会被拒(model-summary-invalid)'),
|
|
844
|
-
}, async (session, args) => {
|
|
845
|
-
// 8.6.26 BizSchemaController.updateSummary (decompiled :700-736):
|
|
846
|
-
// POST bizmodels/update_summary {schemaCode, summary}; the platform
|
|
847
|
-
// itself requires a non-empty summary (validateNotEmpty) and answers
|
|
848
|
-
// 300014 BIZ_SCHEMA_NOT_PUBLISHED for a missing schema. Readback is the
|
|
849
|
-
// sibling GET get_summary face.
|
|
850
|
-
const summary = normalizeSummaryInput(args.summary);
|
|
851
|
-
const performed = await wireCall(session, {
|
|
852
|
-
path: ENDPOINTS.modelUpdateSummary,
|
|
853
|
-
method: "POST",
|
|
854
|
-
body: { schemaCode: args.schemaCode, summary },
|
|
855
|
-
});
|
|
856
|
-
const readback = await wireCall(session, { path: ENDPOINTS.modelGetSummary, query: { schemaCode: args.schemaCode } });
|
|
857
|
-
const stored = readback.data ?? "";
|
|
858
|
-
const storedValid = isCanonicalSummary(stored);
|
|
859
|
-
return {
|
|
860
|
-
performed: { errcode: performed.errcode },
|
|
861
|
-
readback: {
|
|
862
|
-
summary: stored,
|
|
863
|
-
normalized: summary,
|
|
864
|
-
...(storedValid ? {} : { warning: "读回的 summary 不是合法 Summary[](平台运行时标题解析会抛反序列化异常 → 该模型 data.save/submit 恒 50000);用本工具以 Summary[] 或纯 {字段} 串重写修复" }),
|
|
865
|
-
},
|
|
866
|
-
};
|
|
867
|
-
}, (args) => {
|
|
868
|
-
if (typeof args.summary !== "string" || args.summary.trim().length === 0)
|
|
869
|
-
throw new ToolRefusalError({ status: "refused", blocker: "model-summary-empty", message: "数据标题 summary 不允许为空(平台 validateNotEmpty 同语义)" });
|
|
870
|
-
normalizeSummaryInput(args.summary);
|
|
903
|
+
},
|
|
871
904
|
});
|
|
872
|
-
writeTool(
|
|
873
|
-
|
|
874
|
-
name:
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
});
|
|
894
|
-
if (args.name === undefined)
|
|
895
|
-
throw new ToolRefusalError({
|
|
896
|
-
status: "refused",
|
|
897
|
-
blocker: "model-update-empty-patch",
|
|
898
|
-
schemaCode: args.schemaCode,
|
|
899
|
-
message: "没有可应用的补丁字段——至少提供 name(改名节点显示);数据标题用 model.setSummary",
|
|
900
|
-
});
|
|
901
|
-
const nodeId = typeof node.id === "string" ? node.id : String(node.id ?? "");
|
|
902
|
-
const parentId = typeof node.parentId === "string" ? node.parentId : String(node.parentId ?? "");
|
|
903
|
-
if (!nodeId || !parentId)
|
|
904
|
-
throw new WireError("business", `app tree node for ${args.schemaCode} lacks id/parentId (bizmodels/update requires both)`, 10005);
|
|
905
|
-
const performed = await wireCall(session, {
|
|
906
|
-
path: ENDPOINTS.modelUpdate,
|
|
907
|
-
method: "POST",
|
|
908
|
-
body: { bizSchemaCode: args.schemaCode, id: nodeId, parentId, name: args.name },
|
|
909
|
-
});
|
|
910
|
-
// Authoritative readback: the tree NODE name (the patched face). The
|
|
911
|
-
// get_all record face is read informationally — the schema record is
|
|
912
|
-
// NOT migrated by this endpoint (probe evidence).
|
|
913
|
-
const after = await resolveModelTreeNode(session, args.schemaCode);
|
|
914
|
-
const nodeAfter = after.node;
|
|
915
|
-
// Informational only: the app-tree node is the authoritative face for a
|
|
916
|
-
// rename, so an unreadable record face must not fail the whole call.
|
|
917
|
-
const record = await readModelRecord(session, args.schemaCode).catch(() => null);
|
|
918
|
-
const recordName = record?.name ?? null;
|
|
919
|
-
const nodeName = typeof nodeAfter?.name === "string" ? nodeAfter.name : null;
|
|
920
|
-
return {
|
|
921
|
-
performed: { errcode: performed.errcode },
|
|
922
|
-
readback: { schemaCode: args.schemaCode, name: nodeName, renamed: nodeName === args.name, recordName, authoritativeFace: "app-tree-node" },
|
|
923
|
-
};
|
|
924
|
-
}, (args) => {
|
|
925
|
-
// `omc call` bypasses zod, so an extra modelType key still reaches here.
|
|
926
|
-
const raw = args;
|
|
927
|
-
// Probe evidence (yunshu ledger + ticket 09): bizmodels/update only
|
|
928
|
-
// updates the app-menu node display — the underlying schema (type, tree
|
|
929
|
-
// system fields) is FIXED at create. A modelType patch here would look
|
|
930
|
-
// accepted while get_all keeps the original type: refuse it.
|
|
931
|
-
if (raw.modelType !== undefined)
|
|
932
|
-
throw new ToolRefusalError({
|
|
933
|
-
status: "refused",
|
|
934
|
-
blocker: "model-type-immutable",
|
|
935
|
-
schemaCode: raw.schemaCode,
|
|
936
|
-
message: "模型类型在创建时固化——bizmodels/update 只更新应用菜单节点显示,不能把底层 schema 从 LIST 迁移为 TREE(get_all 仍返回原类型与树系统字段,live 取证)。要树模型请建新模型",
|
|
905
|
+
writeTool({
|
|
906
|
+
tools,
|
|
907
|
+
name: "model.setSummary",
|
|
908
|
+
lane: MODEL_LANE,
|
|
909
|
+
scope: MODEL_TOOLS["model.setSummary"].scope,
|
|
910
|
+
description: MODEL_TOOLS["model.setSummary"].description,
|
|
911
|
+
inputSchema: {
|
|
912
|
+
schemaCode: z.string().min(1),
|
|
913
|
+
summary: z.string().min(1).describe('数据标题表达式,线上存储形状是 Summary[] JSON(isDataItem 1=表单字段、2=查询字段),如 [{"isDataItem":1,"code":"subject"}];也接受纯 {字段} 占位串(自动转换为 Summary[])。含字面文本的模板无法表示会被拒(model-summary-invalid)'),
|
|
914
|
+
},
|
|
915
|
+
execute: async (session, args) => {
|
|
916
|
+
// 8.6.26 BizSchemaController.updateSummary (decompiled :700-736):
|
|
917
|
+
// POST bizmodels/update_summary {schemaCode, summary}; the platform
|
|
918
|
+
// itself requires a non-empty summary (validateNotEmpty) and answers
|
|
919
|
+
// 300014 BIZ_SCHEMA_NOT_PUBLISHED for a missing schema. Readback is the
|
|
920
|
+
// sibling GET get_summary face.
|
|
921
|
+
const summary = normalizeSummaryInput(args.summary);
|
|
922
|
+
const performed = await wireCall(session, {
|
|
923
|
+
path: ENDPOINTS.modelUpdateSummary,
|
|
924
|
+
method: "POST",
|
|
925
|
+
body: { schemaCode: args.schemaCode, summary },
|
|
937
926
|
});
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
927
|
+
const readback = await wireCall(session, { path: ENDPOINTS.modelGetSummary, query: { schemaCode: args.schemaCode } });
|
|
928
|
+
const stored = readback.data ?? "";
|
|
929
|
+
const storedValid = isCanonicalSummary(stored);
|
|
930
|
+
return {
|
|
931
|
+
performed: { errcode: performed.errcode },
|
|
932
|
+
readback: {
|
|
933
|
+
summary: stored,
|
|
934
|
+
normalized: summary,
|
|
935
|
+
...(storedValid ? {} : { warning: "读回的 summary 不是合法 Summary[](平台运行时标题解析会抛反序列化异常 → 该模型 data.save/submit 恒 50000);用本工具以 Summary[] 或纯 {字段} 串重写修复" }),
|
|
936
|
+
},
|
|
937
|
+
};
|
|
938
|
+
},
|
|
939
|
+
preflight: (args) => {
|
|
940
|
+
if (typeof args.summary !== "string" || args.summary.trim().length === 0)
|
|
941
|
+
throw new ToolRefusalError({ status: "refused", blocker: "model-summary-empty", message: "数据标题 summary 不允许为空(平台 validateNotEmpty 同语义)" });
|
|
942
|
+
normalizeSummaryInput(args.summary);
|
|
943
|
+
},
|
|
944
|
+
});
|
|
945
|
+
writeTool({
|
|
946
|
+
tools,
|
|
947
|
+
name: "model.update",
|
|
948
|
+
lane: MODEL_LANE,
|
|
949
|
+
scope: MODEL_TOOLS["model.update"].scope,
|
|
950
|
+
description: MODEL_TOOLS["model.update"].description,
|
|
951
|
+
inputSchema: {
|
|
952
|
+
schemaCode: z.string().min(1),
|
|
953
|
+
name: z.string().min(1).optional().describe("新显示名(bizmodels/update 补丁语义,读回 get_all + 应用树节点)"),
|
|
954
|
+
modelType: z.enum(["LIST", "TREE"]).optional().describe("恒拒(model-type-immutable):模型类型创建时固化,bizmodels/update 只更新菜单节点显示"),
|
|
955
|
+
},
|
|
956
|
+
execute: async (session, args) => {
|
|
957
|
+
// Source-pinned contract (BizSchemaController.update, 8.6.26
|
|
958
|
+
// decompiled :266-305): POST/PUT bizmodels/update with AppFunctionVO —
|
|
959
|
+
// bizSchemaCode, id (AppFunction node id), name, parentId are ALL
|
|
960
|
+
// validateNotEmpty-required; the endpoint patches the AppFunction MENU
|
|
961
|
+
// NODE (setName/setIcon/setModelType...), it does not migrate the
|
|
962
|
+
// underlying schema (probe evidence: get_all keeps the original type).
|
|
963
|
+
// So: resolve the tree node for id + parentId, patch the name, and read
|
|
964
|
+
// the NODE face back (the schema record name may stay unchanged).
|
|
965
|
+
const resolved = await resolveModelTreeNode(session, args.schemaCode);
|
|
966
|
+
const node = resolved.node;
|
|
967
|
+
if (!node)
|
|
968
|
+
throw new ToolRefusalError({
|
|
969
|
+
status: "refused",
|
|
970
|
+
blocker: "model-update-target-absent",
|
|
971
|
+
schemaCode: args.schemaCode,
|
|
972
|
+
message: `模型 ${args.schemaCode} 无应用树节点(bizmodels/update 只更新 AppFunction 菜单节点,id/parentId 均必需)——确认 schemaCode 或先用 model.create`,
|
|
973
|
+
});
|
|
974
|
+
if (args.name === undefined)
|
|
975
|
+
throw new ToolRefusalError({
|
|
976
|
+
status: "refused",
|
|
977
|
+
blocker: "model-update-empty-patch",
|
|
978
|
+
schemaCode: args.schemaCode,
|
|
979
|
+
message: "没有可应用的补丁字段——至少提供 name(改名节点显示);数据标题用 model.setSummary",
|
|
980
|
+
});
|
|
981
|
+
const nodeId = typeof node.id === "string" ? node.id : String(node.id ?? "");
|
|
982
|
+
const parentId = typeof node.parentId === "string" ? node.parentId : String(node.parentId ?? "");
|
|
983
|
+
if (!nodeId || !parentId)
|
|
984
|
+
throw new WireError("business", `app tree node for ${args.schemaCode} lacks id/parentId (bizmodels/update requires both)`, 10005);
|
|
985
|
+
const performed = await wireCall(session, {
|
|
986
|
+
path: ENDPOINTS.modelUpdate,
|
|
987
|
+
method: "POST",
|
|
988
|
+
body: { bizSchemaCode: args.schemaCode, id: nodeId, parentId, name: args.name },
|
|
951
989
|
});
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
//
|
|
958
|
-
//
|
|
959
|
-
const
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
990
|
+
// Authoritative readback: the tree NODE name (the patched face). The
|
|
991
|
+
// get_all record face is read informationally — the schema record is
|
|
992
|
+
// NOT migrated by this endpoint (probe evidence).
|
|
993
|
+
const after = await resolveModelTreeNode(session, args.schemaCode);
|
|
994
|
+
const nodeAfter = after.node;
|
|
995
|
+
// Informational only: the app-tree node is the authoritative face for a
|
|
996
|
+
// rename, so an unreadable record face must not fail the whole call.
|
|
997
|
+
const record = await readModelRecord(session, args.schemaCode).catch(() => null);
|
|
998
|
+
const recordName = record?.name ?? null;
|
|
999
|
+
const nodeName = typeof nodeAfter?.name === "string" ? nodeAfter.name : null;
|
|
1000
|
+
return {
|
|
1001
|
+
performed: { errcode: performed.errcode },
|
|
1002
|
+
readback: { schemaCode: args.schemaCode, name: nodeName, renamed: nodeName === args.name, recordName, authoritativeFace: "app-tree-node" },
|
|
1003
|
+
};
|
|
1004
|
+
},
|
|
1005
|
+
preflight: (args) => {
|
|
1006
|
+
// `omc call` bypasses zod, so an extra modelType key still reaches here.
|
|
1007
|
+
const raw = args;
|
|
1008
|
+
// Probe evidence (yunshu ledger + ticket 09): bizmodels/update only
|
|
1009
|
+
// updates the app-menu node display — the underlying schema (type, tree
|
|
1010
|
+
// system fields) is FIXED at create. A modelType patch here would look
|
|
1011
|
+
// accepted while get_all keeps the original type: refuse it.
|
|
1012
|
+
if (raw.modelType !== undefined)
|
|
1013
|
+
throw new ToolRefusalError({
|
|
1014
|
+
status: "refused",
|
|
1015
|
+
blocker: "model-type-immutable",
|
|
1016
|
+
schemaCode: raw.schemaCode,
|
|
1017
|
+
message: "模型类型在创建时固化——bizmodels/update 只更新应用菜单节点显示,不能把底层 schema 从 LIST 迁移为 TREE(get_all 仍返回原类型与树系统字段,live 取证)。要树模型请建新模型",
|
|
1018
|
+
});
|
|
1019
|
+
},
|
|
1020
|
+
rollback: modelRollbackSpec("model.update"),
|
|
1021
|
+
});
|
|
1022
|
+
writeTool({
|
|
1023
|
+
tools,
|
|
1024
|
+
name: "model.move",
|
|
1025
|
+
lane: MODEL_LANE,
|
|
1026
|
+
scope: "move a model's app-tree node to another folder (bizmodels/update parentId)",
|
|
1027
|
+
description: "把模型移动到指定应用目录(分组):`POST bizmodels/update` 的 `parentId`(与 model.update 同一端点,AppFunctionController 补丁语义:bizSchemaCode/id/name/parentId 全量必填,工具从应用树解析)。`folderId` 必须是目录节点 id(`folder.tree` 的 id),**不能直接给目录名**——重名目录会定位错。移动后按应用树回读 `parentId/path` 确认;模型的 schema、字段、表单、视图都不动,只换菜单节点归属。目标目录不存在时拒绝 `folder-not-found` 并列出目录树。",
|
|
1028
|
+
inputSchema: {
|
|
1029
|
+
schemaCode: z.string().min(1),
|
|
1030
|
+
folderId: z
|
|
1031
|
+
.string()
|
|
1032
|
+
.min(1)
|
|
1033
|
+
.describe("目标目录 id(必须是 folder.tree 里 type=Folder 的 id;顶层目录也是真实 id,不要传空或目录名)"),
|
|
1034
|
+
applicationCode: z
|
|
1035
|
+
.string()
|
|
1036
|
+
.optional()
|
|
1037
|
+
.describe("模型所在应用 code(应用包,跨应用时显式给);folder.* 的 appCode 别名本工具不认。"),
|
|
1038
|
+
},
|
|
1039
|
+
execute: async (session, args) => {
|
|
1040
|
+
const resolved = await resolveModelTreeNode(session, args.schemaCode, args.applicationCode);
|
|
1041
|
+
if (!resolved.node)
|
|
1042
|
+
throw new ToolRefusalError({
|
|
1043
|
+
status: "refused",
|
|
1044
|
+
blocker: "model-update-target-absent",
|
|
1045
|
+
schemaCode: args.schemaCode,
|
|
1046
|
+
message: `模型 ${args.schemaCode} 无应用树节点——确认 schemaCode/应用 code(model.move 只改 AppFunction 节点归属)`,
|
|
1047
|
+
});
|
|
1048
|
+
const appCode = resolved.applicationCode;
|
|
1049
|
+
const tree = resolved.treeRows.length > 0 ? resolved.treeRows : flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode } })).data));
|
|
1050
|
+
const target = tree.find((row) => String(row.id ?? "") === args.folderId);
|
|
1051
|
+
if (!target || String(target.type ?? "") !== "Folder") {
|
|
1052
|
+
// A folder from ANOTHER application is the common miss (the model and
|
|
1053
|
+
// the folder must live in the same app tree). Name that instead of a
|
|
1054
|
+
// bare "not found".
|
|
1055
|
+
const apps = wireRows((await wireCall(session, { path: ENDPOINTS.applicationList, query: {} })).data);
|
|
1056
|
+
let otherApp;
|
|
1057
|
+
for (const app of apps) {
|
|
1058
|
+
const candidate = String(app.appCode ?? app.code ?? "");
|
|
1059
|
+
if (!candidate || candidate === appCode)
|
|
1060
|
+
continue;
|
|
1061
|
+
const rows = flattenTreeRows(wireRows((await wireCall(session, { path: ENDPOINTS.applicationTree, query: { appCode: candidate } })).data));
|
|
1062
|
+
if (rows.some((row) => String(row.id ?? "") === args.folderId && String(row.type ?? "") === "Folder")) {
|
|
1063
|
+
otherApp = candidate;
|
|
1064
|
+
break;
|
|
1065
|
+
}
|
|
969
1066
|
}
|
|
1067
|
+
throw new ToolRefusalError({
|
|
1068
|
+
status: "refused",
|
|
1069
|
+
blocker: otherApp ? "folder-other-application" : "folder-not-found",
|
|
1070
|
+
folderId: args.folderId,
|
|
1071
|
+
applicationCode: appCode,
|
|
1072
|
+
...(otherApp ? { folderApplicationCode: otherApp } : {}),
|
|
1073
|
+
available: tree.filter((row) => row.type === "Folder").map((row) => ({ id: row.id, name: row.name })).slice(0, 40),
|
|
1074
|
+
message: otherApp
|
|
1075
|
+
? `目录 ${args.folderId} 属于应用 ${otherApp},而模型 ${args.schemaCode} 在应用 ${appCode}——模型的目录必须同应用;跨应用请先在 ${appCode} 建同名目录再移动`
|
|
1076
|
+
: `应用 ${appCode} 没有目录 ${args.folderId}——folderId 必须用 folder.tree 的目录节点 id(不是目录名)`,
|
|
1077
|
+
});
|
|
970
1078
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
message: otherApp
|
|
979
|
-
? `目录 ${args.folderId} 属于应用 ${otherApp},而模型 ${args.schemaCode} 在应用 ${appCode}——模型的目录必须同应用;跨应用请先在 ${appCode} 建同名目录再移动`
|
|
980
|
-
: `应用 ${appCode} 没有目录 ${args.folderId}——folderId 必须用 folder.tree 的目录节点 id(不是目录名)`,
|
|
1079
|
+
const node = resolved.node;
|
|
1080
|
+
const nodeId = String(node.id ?? "");
|
|
1081
|
+
const beforeParent = String(node.parentId ?? "");
|
|
1082
|
+
const performed = await wireCall(session, {
|
|
1083
|
+
path: ENDPOINTS.modelUpdate,
|
|
1084
|
+
method: "POST",
|
|
1085
|
+
body: { bizSchemaCode: args.schemaCode, id: nodeId, parentId: args.folderId, name: String(node.name ?? args.schemaCode) },
|
|
981
1086
|
});
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
beforeParentId: beforeParent,
|
|
1000
|
-
afterParentId: parentNow || null,
|
|
1001
|
-
folder: { id: args.folderId, name: target.name ?? null },
|
|
1002
|
-
note: "只改菜单节点归属;schema/字段/表单/视图与发布状态都不动",
|
|
1003
|
-
},
|
|
1004
|
-
};
|
|
1005
|
-
}, undefined, undefined, modelRollbackSpec("model.move"));
|
|
1087
|
+
const after = await resolveModelTreeNode(session, args.schemaCode, appCode);
|
|
1088
|
+
const parentNow = String(after.node?.parentId ?? "");
|
|
1089
|
+
return {
|
|
1090
|
+
performed: { errcode: performed.errcode },
|
|
1091
|
+
readback: {
|
|
1092
|
+
schemaCode: args.schemaCode,
|
|
1093
|
+
applicationCode: appCode,
|
|
1094
|
+
moved: parentNow === args.folderId,
|
|
1095
|
+
beforeParentId: beforeParent,
|
|
1096
|
+
afterParentId: parentNow || null,
|
|
1097
|
+
folder: { id: args.folderId, name: target.name ?? null },
|
|
1098
|
+
note: "只改菜单节点归属;schema/字段/表单/视图与发布状态都不动",
|
|
1099
|
+
},
|
|
1100
|
+
};
|
|
1101
|
+
},
|
|
1102
|
+
rollback: modelRollbackSpec("model.move"),
|
|
1103
|
+
});
|
|
1006
1104
|
registerPackageTool(tools, {
|
|
1007
1105
|
name: "configure_model",
|
|
1008
1106
|
lane: MODEL_LANE,
|