@stalfh233/omc-cli 0.6.6 → 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.
Files changed (68) hide show
  1. package/README.md +2 -1
  2. package/dist/approval-flow.js +5 -3
  3. package/dist/bizservice-config.js +26 -10
  4. package/dist/call.js +47 -0
  5. package/dist/capability-overview.js +47 -24
  6. package/dist/cli-contract.js +90 -17
  7. package/dist/cli-output.js +41 -2
  8. package/dist/cli-stdout.js +74 -0
  9. package/dist/cli.js +162 -54
  10. package/dist/command-transport.js +8 -1
  11. package/dist/config-store.js +17 -2
  12. package/dist/discovery.js +62 -3
  13. package/dist/field-families.js +35 -6
  14. package/dist/goal-routes.js +118 -11
  15. package/dist/human-summary.js +11 -0
  16. package/dist/lanes/app-contract.js +12 -5
  17. package/dist/lanes/app.js +221 -134
  18. package/dist/lanes/apply-changes.js +2 -1
  19. package/dist/lanes/b2-registration-contract.js +3 -3
  20. package/dist/lanes/b2-registration.js +890 -693
  21. package/dist/lanes/businessrule.js +75 -51
  22. package/dist/lanes/custom-page.js +166 -115
  23. package/dist/lanes/datarule-contract.js +2 -2
  24. package/dist/lanes/datarule.js +238 -154
  25. package/dist/lanes/dictionary.js +253 -110
  26. package/dist/lanes/environment.js +57 -9
  27. package/dist/lanes/field.js +160 -107
  28. package/dist/lanes/folder-contract.js +7 -3
  29. package/dist/lanes/folder.js +249 -118
  30. package/dist/lanes/form-contract.js +1 -1
  31. package/dist/lanes/form.js +188 -64
  32. package/dist/lanes/list-view.js +405 -369
  33. package/dist/lanes/meta.js +31 -6
  34. package/dist/lanes/model-contract.js +1 -1
  35. package/dist/lanes/model.js +610 -512
  36. package/dist/lanes/online-js-contract.js +1 -1
  37. package/dist/lanes/online-js.js +160 -146
  38. package/dist/lanes/organization.js +905 -640
  39. package/dist/lanes/programming.js +274 -136
  40. package/dist/lanes/report-contract.js +2 -2
  41. package/dist/lanes/report.js +354 -198
  42. package/dist/lanes/rule-lifecycle.js +294 -140
  43. package/dist/lanes/runtime-data-contract.js +4 -4
  44. package/dist/lanes/runtime-data.js +869 -745
  45. package/dist/lanes/workflow-contract.js +3 -3
  46. package/dist/lanes/workflow.js +732 -605
  47. package/dist/model-scaffold.js +26 -5
  48. package/dist/path-projection.js +196 -14
  49. package/dist/plan.js +25 -0
  50. package/dist/programming-workflow.js +55 -3
  51. package/dist/read-shapes.js +146 -0
  52. package/dist/read.js +40 -9
  53. package/dist/recipes/businessrule.js +74 -24
  54. package/dist/recipes/dictionary.js +7 -4
  55. package/dist/recipes/form.js +46 -0
  56. package/dist/recipes/runtime-data.js +3 -2
  57. package/dist/refusal-guidance.js +29 -5
  58. package/dist/rule-support.js +14 -1
  59. package/dist/rule-when.js +2 -2
  60. package/dist/tool-types.js +9 -3
  61. package/dist/version-family-gaps.js +101 -0
  62. package/dist/write.js +2 -1
  63. package/package.json +6 -4
  64. package/skills/omc-glossary.md +13 -0
  65. package/skills/omc-refusals.md +13 -5
  66. package/skills/omc-start-here.md +2 -2
  67. package/skills/omc-tool-selection.md +2 -1
  68. package/skills/omc-write-hazards.md +5 -2
@@ -7,6 +7,7 @@ import { defineTool, ToolRefusalError } from "../tool-types.js";
7
7
  import { wireCall, WireError } from "../wire.js";
8
8
  import { biCall, DEFAULT_BI_CORP_ID } from "../bi-wire.js";
9
9
  import { REPORT_LANE_CONTRACT } from "./report-contract.js";
10
+ import { readAppTree } from "./folder.js";
10
11
  const LANE = REPORT_LANE_CONTRACT.lane;
11
12
  const TOOLS = REPORT_LANE_CONTRACT.tools;
12
13
  function reportRollbackSpec(toolName) {
@@ -115,15 +116,22 @@ function collectReports(node, out) {
115
116
  * immediate folder). /api/report/update 500s ("服务异常") when parentId is
116
117
  * absent, so this is required rather than cosmetic. The fake seeds a plain
117
118
  * `parentId`, which is preferred when present.
119
+ *
120
+ * r28i F7 live: folderList's last entry can disagree with the AppFunction
121
+ * parent on the application tree (create receipt showed「固定资产管理」while
122
+ * folder.tree had the node under「默认分组」). Prefer an authoritative tree
123
+ * parent when the caller already resolved one.
118
124
  */
119
- function pageParentId(page) {
125
+ function pageParentId(page, treeParentId) {
126
+ if (typeof treeParentId === "string" && treeParentId)
127
+ return treeParentId;
120
128
  if (typeof page.parentId === "string" && page.parentId)
121
129
  return page.parentId;
122
130
  const list = Array.isArray(page.folderList) ? page.folderList : [];
123
131
  const last = list[list.length - 1];
124
132
  return last && typeof last.id === "string" ? last.id : undefined;
125
133
  }
126
- function reportPageView(page) {
134
+ function reportPageView(page, treeParentId) {
127
135
  return {
128
136
  id: page.id,
129
137
  reportObjectId: page.reportObjectId ?? page.id,
@@ -136,9 +144,44 @@ function reportPageView(page) {
136
144
  description: page.description,
137
145
  pcAble: page.pcAble,
138
146
  mobileAble: page.mobileAble,
139
- parentId: pageParentId(page),
147
+ parentId: pageParentId(page, treeParentId),
140
148
  };
141
149
  }
150
+ /** Resolve the AppFunction node for a report code from the application tree. */
151
+ async function resolveReportTreeNode(session, appCode, code) {
152
+ try {
153
+ const rows = await readAppTree(session, appCode);
154
+ const hit = rows.find((row) => String(row.code ?? "") === code && String(row.type ?? "") === "Report");
155
+ if (!hit)
156
+ return null;
157
+ return {
158
+ id: String(hit.id ?? ""),
159
+ parentId: hit.parentId == null ? null : String(hit.parentId),
160
+ path: hit.path == null ? null : String(hit.path),
161
+ };
162
+ }
163
+ catch {
164
+ return null;
165
+ }
166
+ }
167
+ function parentMismatchFields(requestedParentId, treeNode) {
168
+ const actualParentId = treeNode?.parentId ?? null;
169
+ const mismatch = actualParentId !== null && actualParentId !== requestedParentId;
170
+ return {
171
+ requestedParentId,
172
+ actualParentId,
173
+ ...(treeNode?.path ? { actualPath: treeNode.path } : {}),
174
+ ...(mismatch
175
+ ? {
176
+ parentMismatch: true,
177
+ parentMismatchNote: "入参 parentId 与应用树实际父节点不一致(或 report/get.folderList 投影曾误导)。以 folder.tree / actualParentId 为准;parentId 必须是 type=Folder 的目录 id",
178
+ }
179
+ : {}),
180
+ };
181
+ }
182
+ function reportVerifyCommand(code, appCode) {
183
+ return `omc call folder.tree --args '{"applicationCode":"${appCode}"}' --jq 'nodes' | 按 code=${code} 查 type=Report 的 parentId/path(BI 不可用时 report.get/report.tree 不可信)`;
184
+ }
142
185
  function chartView(chart) {
143
186
  return {
144
187
  uuid: chart.uuid,
@@ -329,204 +372,317 @@ export function registerReportLane(tools) {
329
372
  return { kind: args.kind, count: merged.length, datasources: merged };
330
373
  }),
331
374
  }));
332
- writeTool(tools, "report.datasource.link", LANE, TOOLS["report.datasource.link"].scope, TOOLS["report.datasource.link"].description, {
333
- reportCode: z.string().min(1),
334
- appCode: z.string().min(1),
335
- dataSourceInfos: z.array(z.object({ dataSourceId: z.string().min(1), useType: z.number().int().optional() })).min(1),
336
- corpId: z.string().optional(),
337
- }, async (session, args) => {
338
- const auth = authOf(args.corpId, args.reportCode, args.appCode);
339
- const infos = args.dataSourceInfos.map((info) => ({ dataSourceId: info.dataSourceId, useType: info.useType ?? 1 }));
340
- const linked = (await biCall(session, { path: ENDPOINTS.biDatasourceModelLink, body: { dataSourceInfos: infos }, query: { lang: "cn" }, auth }));
341
- const models = Array.isArray(linked) ? linked : [];
342
- // Live 2026-10-18: the platform answers success but links NOTHING when the
343
- // model has not synced into BI yet (cloudpivot_report.i_datasource /
344
- // i_modelschema empty). DELIVERED with linkedCount:0 is a false success —
345
- // refuse it with the prerequisite instead (designer must open the report
346
- // once to trigger the sync).
347
- if (infos.length > 0 && models.length === 0)
348
- throw new ToolRefusalError({
349
- status: "refused",
350
- blocker: "report-datasource-link-not-synced",
351
- message: "平台返回成功但未绑定任何数据集(linkedCount:0):模型尚未同步进 BI。先在设计器打开一次报表/触发同步,再重试;不要以 DELIVERED 判定成功。",
352
- dataSourceIds: infos.map((info) => info.dataSourceId),
353
- });
354
- return {
355
- performed: { errcode: 0, linked: infos.map((info) => info.dataSourceId) },
356
- readback: { linkedCount: models.length, models: models.map((model) => ({ dataSourceId: model.dataSourceId, displayName: model.displayName, tableName: model.schema?.tableName })) },
357
- };
358
- });
359
- writeTool(tools, "report.create", LANE, TOOLS["report.create"].scope, TOOLS["report.create"].description, {
360
- // r33: the platform refuses report codes longer than 28 chars (errcode
361
- // 10005) — enforce offline so the shape never costs a wire round trip.
362
- code: z.string().min(1).max(28).describe("报表 code;平台上限 28 字符(10005 实证 r33)——超长在提交前直接被拒"),
363
- name: z.string().min(1).max(60),
364
- appCode: z.string().min(1),
365
- parentId: z.string().min(1),
366
- icon: z.string().default("h-icon-all-Report"),
367
- nameI18n: z.union([z.record(z.string(), z.string()), z.string()]).optional(),
368
- pcAble: z.boolean().default(true),
369
- mobileAble: z.boolean().default(true),
370
- corpId: z.string().optional(),
371
- }, async (session, args) => {
372
- const code = requireCode(args.code);
373
- const created = (await wireCall(session, {
374
- path: ENDPOINTS.reportCreate,
375
- method: "POST",
376
- body: {
377
- code,
378
- name: args.name,
379
- parentId: args.parentId,
380
- icon: args.icon,
381
- name_i18n: typeof args.nameI18n === "string" ? args.nameI18n : JSON.stringify(args.nameI18n ?? { zh: args.name }),
382
- pcAble: args.pcAble,
383
- mobileAble: args.mobileAble,
384
- appCode: args.appCode,
385
- },
386
- })).data;
387
- const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code } })).data;
388
- const objectId = String(page.id ?? page.reportObjectId ?? "");
389
- if (objectId) {
390
- try {
391
- await biCall(session, {
392
- path: ENDPOINTS.biReportPageSave,
393
- body: { report: { objectId, title: args.name, attributes: "{}", global: JSON.stringify(DEFAULT_REPORT_GLOBAL) } },
394
- auth: authOf(args.corpId, code, args.appCode),
375
+ writeTool({
376
+ tools,
377
+ name: "report.datasource.link",
378
+ lane: LANE,
379
+ scope: TOOLS["report.datasource.link"].scope,
380
+ description: TOOLS["report.datasource.link"].description,
381
+ inputSchema: {
382
+ reportCode: z.string().min(1),
383
+ appCode: z.string().min(1),
384
+ dataSourceInfos: z.array(z.object({ dataSourceId: z.string().min(1), useType: z.number().int().optional() })).min(1),
385
+ corpId: z.string().optional(),
386
+ },
387
+ execute: async (session, args) => {
388
+ const auth = authOf(args.corpId, args.reportCode, args.appCode);
389
+ const infos = args.dataSourceInfos.map((info) => ({ dataSourceId: info.dataSourceId, useType: info.useType ?? 1 }));
390
+ const linked = (await biCall(session, { path: ENDPOINTS.biDatasourceModelLink, body: { dataSourceInfos: infos }, query: { lang: "cn" }, auth }));
391
+ const models = Array.isArray(linked) ? linked : [];
392
+ // Live 2026-10-18: the platform answers success but links NOTHING when the
393
+ // model has not synced into BI yet (cloudpivot_report.i_datasource /
394
+ // i_modelschema empty). DELIVERED with linkedCount:0 is a false success —
395
+ // refuse it with the prerequisite instead (designer must open the report
396
+ // once to trigger the sync).
397
+ if (infos.length > 0 && models.length === 0)
398
+ throw new ToolRefusalError({
399
+ status: "refused",
400
+ blocker: "report-datasource-link-not-synced",
401
+ message: "平台返回成功但未绑定任何数据集(linkedCount:0):模型尚未同步进 BI。先在设计器打开一次报表/触发同步,再重试;不要以 DELIVERED 判定成功。",
402
+ dataSourceIds: infos.map((info) => info.dataSourceId),
395
403
  });
404
+ return {
405
+ performed: { errcode: 0, linked: infos.map((info) => info.dataSourceId) },
406
+ readback: { linkedCount: models.length, models: models.map((model) => ({ dataSourceId: model.dataSourceId, displayName: model.displayName, tableName: model.schema?.tableName })) },
407
+ };
408
+ },
409
+ });
410
+ writeTool({
411
+ tools,
412
+ name: "report.create",
413
+ lane: LANE,
414
+ scope: TOOLS["report.create"].scope,
415
+ description: TOOLS["report.create"].description,
416
+ inputSchema: {
417
+ // r33: the platform refuses report codes longer than 28 chars (errcode
418
+ // 10005) — enforce offline so the shape never costs a wire round trip.
419
+ code: z.string().min(1).max(28).describe("报表 code;平台上限 28 字符(10005 实证 r33)——超长在提交前直接被拒"),
420
+ name: z.string().min(1).max(60),
421
+ appCode: z.string().min(1),
422
+ parentId: z
423
+ .string()
424
+ .min(1)
425
+ .describe("必须是 folder.tree 里 type=Folder 的目录 id(不能挂应用根/报表节点)。平台可能静默改挂或 report/get.folderList 投影与入参不一致——create 会按 folder.tree 读回实际 parentId/path,不一致时标 parentMismatch:true;务必用 folder.tree 或 report.tree 核对落点"),
426
+ icon: z.string().default("h-icon-all-Report"),
427
+ nameI18n: z.union([z.record(z.string(), z.string()), z.string()]).optional(),
428
+ pcAble: z.boolean().default(true),
429
+ mobileAble: z.boolean().default(true),
430
+ corpId: z.string().optional(),
431
+ },
432
+ execute: async (session, args) => {
433
+ const code = requireCode(args.code);
434
+ const created = (await wireCall(session, {
435
+ path: ENDPOINTS.reportCreate,
436
+ method: "POST",
437
+ body: {
438
+ code,
439
+ name: args.name,
440
+ parentId: args.parentId,
441
+ icon: args.icon,
442
+ name_i18n: typeof args.nameI18n === "string" ? args.nameI18n : JSON.stringify(args.nameI18n ?? { zh: args.name }),
443
+ pcAble: args.pcAble,
444
+ mobileAble: args.mobileAble,
445
+ appCode: args.appCode,
446
+ },
447
+ })).data;
448
+ const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code } })).data;
449
+ const objectId = String(page.id ?? page.reportObjectId ?? "");
450
+ // r28i F7: authoritative parent comes from the AppFunction tree, not folderList.
451
+ const treeNode = await resolveReportTreeNode(session, args.appCode, code);
452
+ const pageView = reportPageView(page, treeNode?.parentId ?? undefined);
453
+ const parentFields = parentMismatchFields(args.parentId, treeNode);
454
+ const verifyCommand = reportVerifyCommand(code, args.appCode);
455
+ if (objectId) {
456
+ try {
457
+ await biCall(session, {
458
+ path: ENDPOINTS.biReportPageSave,
459
+ body: { report: { objectId, title: args.name, attributes: "{}", global: JSON.stringify(DEFAULT_REPORT_GLOBAL) } },
460
+ auth: authOf(args.corpId, code, args.appCode),
461
+ });
462
+ }
463
+ catch (error) {
464
+ // r19 p14(p9 实证)+ r27 F8:报表服务未启动时 BI 初始化统一 502,
465
+ // 但平台层的报表页行**已经建了**。裸抛 uncertain 会掩盖这个残留——
466
+ // 半成功必须显式。
467
+ //
468
+ // deliveryState:合法集合只有 DELIVERED | WRITE_UNVERIFIED(见
469
+ // write.deliveryStateFor)。BI 失败时权威树/门户读不到该报表,不能
470
+ // 盖 DELIVERED;这里把 readback.present 置 false,让共享写门把
471
+ // deliveryState 降为 WRITE_UNVERIFIED(半成功 ≠ 已交付)。平台行
472
+ // 仍在——看 halfSuccess / objectId / report,不要把 present:false
473
+ // 读成「啥也没写」。
474
+ // r28i F8: WRITE_UNVERIFIED must carry one verifiable read command.
475
+ return {
476
+ performed: { errcode: 0, code, appCode: args.appCode, objectId, biInit: "failed" },
477
+ readback: {
478
+ // present:false → write.ts deliveryStateFor → WRITE_UNVERIFIED
479
+ present: false,
480
+ report: pageView,
481
+ biInitialized: false,
482
+ // 平台侧行已建,但报表树/门户看不到(与 report.tree 读回 0 对齐)
483
+ treeVisibility: "not-listed",
484
+ platformRowCreated: true,
485
+ treeNodeId: treeNode?.id || null,
486
+ verifyCommand,
487
+ ...parentFields,
488
+ },
489
+ halfSuccess: {
490
+ message: `平台层报表页已建(code=${code},objectId=${objectId}),但 BI 初始化失败(${error instanceof Error ? error.message : String(error)})——元数据已残留;报表树/门户看不到(treeVisibility=not-listed)`,
491
+ cleanup: `omc call report.delete --args '{"code":"${code}"}'(或保留,待报表服务启动后重试 BI 配置)`,
492
+ boundary: "报表服务未启动时 BI 层(/dashboard/v1/*)统一 502——这是环境边界,不要原样重试",
493
+ verifyCommand,
494
+ },
495
+ };
496
+ }
396
497
  }
397
- catch (error) {
398
- // r19 p14(p9 实证):报表服务未启动时 BI 初始化统一 502,但平台层的
399
- // 报表页行**已经建了**。裸抛 uncertain 会掩盖这个残留——半成功必须
400
- // 显式:回执带 halfSuccess 警示 + 清理命令,让 agent 下一调用就能收拾。
401
- return {
402
- performed: { errcode: 0, code, appCode: args.appCode, objectId, biInit: "failed" },
403
- readback: { present: page.code === code, report: reportPageView(page), biInitialized: false },
404
- halfSuccess: {
405
- message: `平台层报表页已建(code=${code},objectId=${objectId}),但 BI 初始化失败(${error instanceof Error ? error.message : String(error)})——元数据已残留`,
406
- cleanup: `omc call report.delete --args '{"code":"${code}"}'(或保留,待报表服务启动后重试 BI 配置)`,
407
- boundary: "报表服务未启动时 BI 层(/dashboard/v1/*)统一 502——这是环境边界,不要原样重试",
408
- },
409
- };
410
- }
411
- }
412
- void created;
413
- return {
414
- performed: { errcode: 0, code, appCode: args.appCode, objectId },
415
- readback: { present: page.code === code, report: reportPageView(page), biInitialized: objectId !== "" },
416
- };
417
- }, (args) => requireCode(args.code), undefined, reportRollbackSpec("report.create"));
418
- writeTool(tools, "report.update", LANE, TOOLS["report.update"].scope, TOOLS["report.update"].description, {
419
- code: z.string().min(1),
420
- name: z.string().min(1).max(60).optional(),
421
- icon: z.string().optional(),
422
- nameI18n: z.union([z.record(z.string(), z.string()), z.string()]).optional(),
423
- pcAble: z.boolean().optional(),
424
- mobileAble: z.boolean().optional(),
425
- parentId: z.string().optional(),
426
- }, async (session, args) => {
427
- const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.code } })).data;
428
- if (!page || !page.id)
429
- throw new ToolRefusalError({ status: "refused", blocker: "report-not-found", message: `报表页 ${args.code} 不存在` });
430
- const name = args.name ?? String(page.name ?? "");
431
- const performed = await wireCall(session, {
432
- path: ENDPOINTS.reportUpdate,
433
- method: "POST",
434
- body: {
435
- id: page.id,
436
- code: page.code ?? args.code,
437
- appCode: page.appCode,
438
- name,
439
- parentId: args.parentId ?? pageParentId(page),
440
- icon: args.icon ?? page.icon,
441
- name_i18n: typeof args.nameI18n === "string" ? args.nameI18n : JSON.stringify(args.nameI18n ?? parseJsonText(page.name_i18n) ?? { zh: name }),
442
- pcAble: args.pcAble ?? page.pcAble,
443
- mobileAble: args.mobileAble ?? page.mobileAble,
444
- },
445
- });
446
- const after = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.code } })).data;
447
- return { performed: { errcode: performed.errcode }, readback: { present: Boolean(after && after.code), report: reportPageView(after) } };
448
- }, undefined, undefined, reportRollbackSpec("report.update"));
449
- writeTool(tools, "report.delete", LANE, TOOLS["report.delete"].scope, TOOLS["report.delete"].description, {
450
- code: z.string().min(1),
451
- corpId: z.string().optional(),
452
- }, async (session, args) => {
453
- const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.code } })).data;
454
- let dashboardRemoved = false;
455
- if (page && page.id) {
456
- try {
457
- await biCall(session, {
458
- path: ENDPOINTS.biReportRemove,
459
- body: { objectId: page.id },
460
- auth: authOf(args.corpId, String(page.code ?? args.code), String(page.appCode ?? "")),
461
- });
462
- dashboardRemoved = true;
498
+ void created;
499
+ return {
500
+ performed: { errcode: 0, code, appCode: args.appCode, objectId },
501
+ readback: {
502
+ present: page.code === code,
503
+ report: pageView,
504
+ biInitialized: objectId !== "",
505
+ treeNodeId: treeNode?.id || null,
506
+ ...parentFields,
507
+ },
508
+ };
509
+ },
510
+ preflight: (args) => requireCode(args.code),
511
+ rollback: reportRollbackSpec("report.create"),
512
+ });
513
+ writeTool({
514
+ tools,
515
+ name: "report.update",
516
+ lane: LANE,
517
+ scope: TOOLS["report.update"].scope,
518
+ description: TOOLS["report.update"].description,
519
+ inputSchema: {
520
+ code: z.string().min(1),
521
+ name: z.string().min(1).max(60).optional(),
522
+ icon: z.string().optional(),
523
+ nameI18n: z.union([z.record(z.string(), z.string()), z.string()]).optional(),
524
+ pcAble: z.boolean().optional(),
525
+ mobileAble: z.boolean().optional(),
526
+ parentId: z.string().optional(),
527
+ },
528
+ execute: async (session, args) => {
529
+ const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.code } })).data;
530
+ if (!page || !page.id)
531
+ throw new ToolRefusalError({ status: "refused", blocker: "report-not-found", message: `报表页 ${args.code} 不存在` });
532
+ const name = args.name ?? String(page.name ?? "");
533
+ const performed = await wireCall(session, {
534
+ path: ENDPOINTS.reportUpdate,
535
+ method: "POST",
536
+ body: {
537
+ id: page.id,
538
+ code: page.code ?? args.code,
539
+ appCode: page.appCode,
540
+ name,
541
+ parentId: args.parentId ?? pageParentId(page),
542
+ icon: args.icon ?? page.icon,
543
+ name_i18n: typeof args.nameI18n === "string" ? args.nameI18n : JSON.stringify(args.nameI18n ?? parseJsonText(page.name_i18n) ?? { zh: name }),
544
+ pcAble: args.pcAble ?? page.pcAble,
545
+ mobileAble: args.mobileAble ?? page.mobileAble,
546
+ },
547
+ });
548
+ const after = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.code } })).data;
549
+ return { performed: { errcode: performed.errcode }, readback: { present: Boolean(after && after.code), report: reportPageView(after) } };
550
+ },
551
+ rollback: reportRollbackSpec("report.update"),
552
+ });
553
+ writeTool({
554
+ tools,
555
+ name: "report.delete",
556
+ lane: LANE,
557
+ scope: TOOLS["report.delete"].scope,
558
+ description: TOOLS["report.delete"].description,
559
+ inputSchema: {
560
+ code: z.string().min(1),
561
+ corpId: z.string().optional(),
562
+ },
563
+ execute: async (session, args) => {
564
+ const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.code } })).data;
565
+ // r27 F9: BI 不可用(502 / 服务未部署)时**不能**卡死整条删除链——
566
+ // 否则平台侧 reportDelete 根本不跑,报表节点残留在应用树,app.delete
567
+ // 回 600006「应用存在下属节点」。BI 失败降级继续删平台节点;只有平台
568
+ // 侧也失败才 throw。
569
+ let biRemoved = false;
570
+ let dashboardError;
571
+ if (page && page.id) {
572
+ try {
573
+ await biCall(session, {
574
+ path: ENDPOINTS.biReportRemove,
575
+ body: { objectId: page.id },
576
+ auth: authOf(args.corpId, String(page.code ?? args.code), String(page.appCode ?? "")),
577
+ });
578
+ biRemoved = true;
579
+ }
580
+ catch (error) {
581
+ if (isMissingDashboard(error)) {
582
+ // dashboard 本就不在 → 视为 BI 侧无需再删,不算 degraded。
583
+ biRemoved = false;
584
+ }
585
+ else {
586
+ dashboardError = error instanceof Error ? error.message.slice(0, 240) : String(error).slice(0, 240);
587
+ biRemoved = false;
588
+ }
589
+ }
463
590
  }
464
- catch (error) {
465
- if (!isMissingDashboard(error))
466
- throw error;
591
+ const performed = await wireCall(session, { path: ENDPOINTS.reportDelete, method: "POST", query: { code: args.code } });
592
+ const tree = (await wireCall(session, { path: ENDPOINTS.reportTreeByApp, query: { appCode: String(page?.appCode ?? "") } })).data;
593
+ const remaining = [];
594
+ collectReports(tree, remaining);
595
+ const stillListed = remaining.some((report) => report.code === args.code);
596
+ const platformRemoved = !stillListed;
597
+ const degraded = dashboardError !== undefined ? "bi-unavailable" : undefined;
598
+ return {
599
+ performed: {
600
+ errcode: performed.errcode,
601
+ code: args.code,
602
+ // 兼容旧键:dashboardRemoved ≡ biRemoved(BI 看板摘除是否成功)
603
+ dashboardRemoved: biRemoved,
604
+ biRemoved,
605
+ platformRemoved,
606
+ ...(dashboardError !== undefined ? { dashboardError } : {}),
607
+ ...(degraded !== undefined ? { degraded } : {}),
608
+ ...(degraded !== undefined
609
+ ? { hint: "BI 报表页未摘除,但平台侧节点已删;应用可继续删除(app.delete 不再被 600006 卡住)" }
610
+ : {}),
611
+ },
612
+ readback: {
613
+ absent: platformRemoved,
614
+ remainingInTree: stillListed,
615
+ biRemoved,
616
+ platformRemoved,
617
+ ...(degraded !== undefined ? { degraded } : {}),
618
+ },
619
+ };
620
+ },
621
+ rollback: reportRollbackSpec("report.delete"),
622
+ });
623
+ writeTool({
624
+ tools,
625
+ name: "report.configure",
626
+ lane: LANE,
627
+ scope: TOOLS["report.configure"].scope,
628
+ description: TOOLS["report.configure"].description,
629
+ inputSchema: {
630
+ reportCode: z.string().min(1),
631
+ appCode: z.string().min(1),
632
+ global: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
633
+ attributes: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
634
+ charts: z.array(chartObject).optional(),
635
+ removeUnlistedCharts: z.boolean().default(false),
636
+ corpId: z.string().optional(),
637
+ },
638
+ execute: async (session, args) => {
639
+ const auth = authOf(args.corpId, args.reportCode, args.appCode);
640
+ const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.reportCode } })).data;
641
+ const objectId = String(page?.id ?? page?.reportObjectId ?? "");
642
+ if (!objectId)
643
+ throw new ToolRefusalError({ status: "refused", blocker: "report-not-found", message: `报表页 ${args.reportCode} 不存在` });
644
+ const current = await readBiConfig(session, auth, objectId);
645
+ if (!current.present)
646
+ throw new ToolRefusalError({ status: "refused", blocker: "report-dashboard-absent", message: `报表页 ${args.reportCode} 的 BI dashboard 不存在;先 report.create 初始化或用 report.get 确认 present` });
647
+ const existing = new Map(current.charts.map((chart) => [String(chart.uuid), chart]));
648
+ const declared = args.charts ?? [];
649
+ const toAdd = [];
650
+ const toUpdate = [];
651
+ const kept = new Set();
652
+ for (const chart of declared) {
653
+ const uuid = chart.uuid;
654
+ if (typeof uuid !== "string" || !uuid)
655
+ throw new ToolRefusalError({ status: "refused", blocker: "report-chart-uuid-required", message: "report.configure 的每个图表必须带 uuid(既有=更新,新图表自带 uuid)" });
656
+ kept.add(uuid);
657
+ // The BI write face scopes charts by their parent report objectId; stamp
658
+ // it so callers (and the intent compiler) need not know it.
659
+ const stamped = { ...chart, objectId };
660
+ (existing.has(uuid) ? toUpdate : toAdd).push(stamped);
467
661
  }
468
- }
469
- const performed = await wireCall(session, { path: ENDPOINTS.reportDelete, method: "POST", query: { code: args.code } });
470
- const tree = (await wireCall(session, { path: ENDPOINTS.reportTreeByApp, query: { appCode: String(page?.appCode ?? "") } })).data;
471
- const remaining = [];
472
- collectReports(tree, remaining);
473
- const stillListed = remaining.some((report) => report.code === args.code);
474
- return { performed: { errcode: performed.errcode, code: args.code, dashboardRemoved }, readback: { absent: !stillListed, remainingInTree: stillListed } };
475
- }, undefined, undefined, reportRollbackSpec("report.delete"));
476
- writeTool(tools, "report.configure", LANE, TOOLS["report.configure"].scope, TOOLS["report.configure"].description, {
477
- reportCode: z.string().min(1),
478
- appCode: z.string().min(1),
479
- global: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
480
- attributes: z.union([z.record(z.string(), z.unknown()), z.string()]).optional(),
481
- charts: z.array(chartObject).optional(),
482
- removeUnlistedCharts: z.boolean().default(false),
483
- corpId: z.string().optional(),
484
- }, async (session, args) => {
485
- const auth = authOf(args.corpId, args.reportCode, args.appCode);
486
- const page = (await wireCall(session, { path: ENDPOINTS.reportGet, query: { code: args.reportCode } })).data;
487
- const objectId = String(page?.id ?? page?.reportObjectId ?? "");
488
- if (!objectId)
489
- throw new ToolRefusalError({ status: "refused", blocker: "report-not-found", message: `报表页 ${args.reportCode} 不存在` });
490
- const current = await readBiConfig(session, auth, objectId);
491
- if (!current.present)
492
- throw new ToolRefusalError({ status: "refused", blocker: "report-dashboard-absent", message: `报表页 ${args.reportCode} 的 BI dashboard 不存在;先 report.create 初始化或用 report.get 确认 present` });
493
- const existing = new Map(current.charts.map((chart) => [String(chart.uuid), chart]));
494
- const declared = args.charts ?? [];
495
- const toAdd = [];
496
- const toUpdate = [];
497
- const kept = new Set();
498
- for (const chart of declared) {
499
- const uuid = chart.uuid;
500
- if (typeof uuid !== "string" || !uuid)
501
- throw new ToolRefusalError({ status: "refused", blocker: "report-chart-uuid-required", message: "report.configure 的每个图表必须带 uuid(既有=更新,新图表自带 uuid)" });
502
- kept.add(uuid);
503
- // The BI write face scopes charts by their parent report objectId; stamp
504
- // it so callers (and the intent compiler) need not know it.
505
- const stamped = { ...chart, objectId };
506
- (existing.has(uuid) ? toUpdate : toAdd).push(stamped);
507
- }
508
- const toRemove = args.removeUnlistedCharts ? [...existing.keys()].filter((uuid) => !kept.has(uuid)) : [];
509
- const performed = {};
510
- if (toAdd.length)
511
- performed.added = (await biCall(session, { path: ENDPOINTS.biChartAdd, body: { charts: toAdd }, auth }));
512
- if (toUpdate.length)
513
- performed.updated = (await biCall(session, { path: ENDPOINTS.biChartUpdate, body: { charts: toUpdate }, auth }));
514
- if (toRemove.length)
515
- performed.removed = (await biCall(session, { path: ENDPOINTS.biChartRemoveMany, body: { objectIds: toRemove }, auth }));
516
- if (args.global !== undefined)
517
- performed.global = (await biCall(session, { path: ENDPOINTS.biReportSaveGlobal, body: { objectId, global: jsonString(args.global, {}) }, auth }));
518
- if (args.attributes !== undefined)
519
- performed.attributes = (await biCall(session, { path: ENDPOINTS.biReportSaveAttributes, body: { objectId, attributes: jsonString(args.attributes, {}) }, auth }));
520
- const after = await readBiConfig(session, auth, objectId);
521
- return {
522
- performed: { ...performed, added: toAdd.length, updated: toUpdate.length, removed: toRemove.length },
523
- readback: {
524
- chartCount: after.charts.length,
525
- charts: after.charts.map(chartView),
526
- globalPresent: after.global !== undefined && JSON.stringify(after.global) !== "{}",
527
- attributesPresent: after.attributes !== undefined && JSON.stringify(after.attributes) !== "{}",
528
- },
529
- };
530
- }, undefined, undefined, reportRollbackSpec("report.configure"));
662
+ const toRemove = args.removeUnlistedCharts ? [...existing.keys()].filter((uuid) => !kept.has(uuid)) : [];
663
+ const performed = {};
664
+ if (toAdd.length)
665
+ performed.added = (await biCall(session, { path: ENDPOINTS.biChartAdd, body: { charts: toAdd }, auth }));
666
+ if (toUpdate.length)
667
+ performed.updated = (await biCall(session, { path: ENDPOINTS.biChartUpdate, body: { charts: toUpdate }, auth }));
668
+ if (toRemove.length)
669
+ performed.removed = (await biCall(session, { path: ENDPOINTS.biChartRemoveMany, body: { objectIds: toRemove }, auth }));
670
+ if (args.global !== undefined)
671
+ performed.global = (await biCall(session, { path: ENDPOINTS.biReportSaveGlobal, body: { objectId, global: jsonString(args.global, {}) }, auth }));
672
+ if (args.attributes !== undefined)
673
+ performed.attributes = (await biCall(session, { path: ENDPOINTS.biReportSaveAttributes, body: { objectId, attributes: jsonString(args.attributes, {}) }, auth }));
674
+ const after = await readBiConfig(session, auth, objectId);
675
+ return {
676
+ performed: { ...performed, added: toAdd.length, updated: toUpdate.length, removed: toRemove.length },
677
+ readback: {
678
+ chartCount: after.charts.length,
679
+ charts: after.charts.map(chartView),
680
+ globalPresent: after.global !== undefined && JSON.stringify(after.global) !== "{}",
681
+ attributesPresent: after.attributes !== undefined && JSON.stringify(after.attributes) !== "{}",
682
+ },
683
+ };
684
+ },
685
+ rollback: reportRollbackSpec("report.configure"),
686
+ });
531
687
  }
532
688
  //# sourceMappingURL=report.js.map