weaver-e10-builder 1.6.10 → 1.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.
Files changed (155) hide show
  1. package/dist/commands/report-admin.d.ts.map +1 -1
  2. package/dist/commands/report-admin.js +378 -2
  3. package/dist/commands/report-admin.js.map +1 -1
  4. package/dist/commands/report-validate.d.ts.map +1 -1
  5. package/dist/commands/report-validate.js +1066 -3
  6. package/dist/commands/report-validate.js.map +1 -1
  7. package/dist/commands/report.d.ts.map +1 -1
  8. package/dist/commands/report.js +341 -3
  9. package/dist/commands/report.js.map +1 -1
  10. package/dist/commands/workflow-after-submit-comment.d.ts +3 -0
  11. package/dist/commands/workflow-after-submit-comment.d.ts.map +1 -0
  12. package/dist/commands/workflow-after-submit-comment.js +56 -0
  13. package/dist/commands/workflow-after-submit-comment.js.map +1 -0
  14. package/dist/commands/workflow-setting.d.ts.map +1 -1
  15. package/dist/commands/workflow-setting.js +2 -67
  16. package/dist/commands/workflow-setting.js.map +1 -1
  17. package/dist/services/report/blueprint.d.ts +109 -0
  18. package/dist/services/report/blueprint.d.ts.map +1 -0
  19. package/dist/services/report/blueprint.js +322 -0
  20. package/dist/services/report/blueprint.js.map +1 -0
  21. package/dist/services/report/build.d.ts.map +1 -1
  22. package/dist/services/report/build.js +8 -3
  23. package/dist/services/report/build.js.map +1 -1
  24. package/dist/services/report/card.d.ts +65 -0
  25. package/dist/services/report/card.d.ts.map +1 -0
  26. package/dist/services/report/card.js +151 -0
  27. package/dist/services/report/card.js.map +1 -0
  28. package/dist/services/report/copy-health.d.ts +31 -0
  29. package/dist/services/report/copy-health.d.ts.map +1 -0
  30. package/dist/services/report/copy-health.js +87 -0
  31. package/dist/services/report/copy-health.js.map +1 -0
  32. package/dist/services/report/dataset-heal.d.ts +64 -0
  33. package/dist/services/report/dataset-heal.d.ts.map +1 -0
  34. package/dist/services/report/dataset-heal.js +89 -0
  35. package/dist/services/report/dataset-heal.js.map +1 -0
  36. package/dist/services/report/dataset-ids.d.ts +70 -0
  37. package/dist/services/report/dataset-ids.d.ts.map +1 -0
  38. package/dist/services/report/dataset-ids.js +149 -0
  39. package/dist/services/report/dataset-ids.js.map +1 -0
  40. package/dist/services/report/dataset.d.ts.map +1 -1
  41. package/dist/services/report/dataset.js +3 -1
  42. package/dist/services/report/dataset.js.map +1 -1
  43. package/dist/services/report/empty-dataset.d.ts +51 -0
  44. package/dist/services/report/empty-dataset.d.ts.map +1 -0
  45. package/dist/services/report/empty-dataset.js +213 -0
  46. package/dist/services/report/empty-dataset.js.map +1 -0
  47. package/dist/services/report/field-binding.d.ts +75 -0
  48. package/dist/services/report/field-binding.d.ts.map +1 -0
  49. package/dist/services/report/field-binding.js +160 -0
  50. package/dist/services/report/field-binding.js.map +1 -0
  51. package/dist/services/report/forms.d.ts +32 -0
  52. package/dist/services/report/forms.d.ts.map +1 -1
  53. package/dist/services/report/forms.js +90 -0
  54. package/dist/services/report/forms.js.map +1 -1
  55. package/dist/services/report/formula-parent.d.ts +25 -0
  56. package/dist/services/report/formula-parent.d.ts.map +1 -0
  57. package/dist/services/report/formula-parent.js +116 -0
  58. package/dist/services/report/formula-parent.js.map +1 -0
  59. package/dist/services/report/locate.d.ts +40 -0
  60. package/dist/services/report/locate.d.ts.map +1 -0
  61. package/dist/services/report/locate.js +122 -0
  62. package/dist/services/report/locate.js.map +1 -0
  63. package/dist/services/report/reports.d.ts +4 -0
  64. package/dist/services/report/reports.d.ts.map +1 -1
  65. package/dist/services/report/reports.js +4 -2
  66. package/dist/services/report/reports.js.map +1 -1
  67. package/dist/services/report/scene.d.ts +22 -0
  68. package/dist/services/report/scene.d.ts.map +1 -0
  69. package/dist/services/report/scene.js +145 -0
  70. package/dist/services/report/scene.js.map +1 -0
  71. package/dist/services/report/types.d.ts +28 -0
  72. package/dist/services/report/types.d.ts.map +1 -1
  73. package/dist/services/report/verify.d.ts +24 -0
  74. package/dist/services/report/verify.d.ts.map +1 -1
  75. package/dist/services/report/verify.js +115 -2
  76. package/dist/services/report/verify.js.map +1 -1
  77. package/dist/services/report/workbook.d.ts +12 -1
  78. package/dist/services/report/workbook.d.ts.map +1 -1
  79. package/dist/services/report/workbook.js +107 -8
  80. package/dist/services/report/workbook.js.map +1 -1
  81. package/dist/services/workflow-after-submit-comment.d.ts +20 -0
  82. package/dist/services/workflow-after-submit-comment.d.ts.map +1 -0
  83. package/dist/services/workflow-after-submit-comment.js +81 -0
  84. package/dist/services/workflow-after-submit-comment.js.map +1 -0
  85. package/ecodecmd/src/build.js +36 -12
  86. package/package.json +1 -1
  87. package/skill-docs/SKILL.md +4 -4
  88. package/skill-docs/_catalog.md +10 -1
  89. package/skill-docs/references/coding/guide-page-code.md +95 -0
  90. package/skill-docs/references/components/ref-component-common.md +98 -0
  91. package/skill-docs/references/report/add-dataset-registration-guide.md +44 -22
  92. package/skill-docs/references/report/advanced-crosstab-tree-guide.md +82 -0
  93. package/skill-docs/references/report/ai-report-workflow.md +100 -100
  94. package/skill-docs/references/report/api-auth-headers.md +129 -0
  95. package/skill-docs/references/report/api-report-build-fast-path.md +274 -275
  96. package/skill-docs/references/report/app-datasource-lookup-guide.md +230 -230
  97. package/skill-docs/references/report/branch-scene-guide.md +293 -0
  98. package/skill-docs/references/report/build-resilience-guide.md +99 -0
  99. package/skill-docs/references/report/card-report-guide.md +110 -0
  100. package/skill-docs/references/report/cell-config-echo-guide.md +44 -9
  101. package/skill-docs/references/report/cell-transform-guide.md +9 -9
  102. package/skill-docs/references/report/column-split-report-guide.md +96 -0
  103. package/skill-docs/references/report/condition-format-guide.md +49 -24
  104. package/skill-docs/references/report/cross-dataset-filter-guide.md +180 -69
  105. package/skill-docs/references/report/crosstab-raw-workbook-guide.md +265 -0
  106. package/skill-docs/references/report/dataset-query-filter-guide.md +231 -235
  107. package/skill-docs/references/report/dataset-sidebar-ops-guide.md +75 -34
  108. package/skill-docs/references/report/dataset-table-field-contract-guide.md +196 -46
  109. package/skill-docs/references/report/datasource-metadata-guide.md +128 -119
  110. package/skill-docs/references/report/datasource-selection-and-parent-filter-guide.md +183 -148
  111. package/skill-docs/references/report/date-range-query-reference.md +97 -102
  112. package/skill-docs/references/report/default-header-style-guide.md +8 -8
  113. package/skill-docs/references/report/designer-blank-prevention-guide.md +8 -8
  114. package/skill-docs/references/report/dossier-report-guide.md +69 -0
  115. package/skill-docs/references/report/drilldown-report-guide.md +59 -0
  116. package/skill-docs/references/report/event-hreflink-guide.md +53 -47
  117. package/skill-docs/references/report/expansion-block-parent-config-guide.md +208 -0
  118. package/skill-docs/references/report/failure-cleanup-and-nonblocking-guide.md +183 -0
  119. package/skill-docs/references/report/field-bind-exact-match-guide.md +208 -208
  120. package/skill-docs/references/report/financial-stats-report-guide.md +89 -0
  121. package/skill-docs/references/report/formula-cell-api-guide.md +325 -258
  122. package/skill-docs/references/report/golden-case-summary-sheet.md +200 -0
  123. package/skill-docs/references/report/guide-report-build.md +6 -1
  124. package/skill-docs/references/report/hidden-row-and-header-merge-guide.md +10 -10
  125. package/skill-docs/references/report/layout-showlocation-freeze-guide.md +39 -10
  126. package/skill-docs/references/report/multi-report-crosstab-casebook.md +263 -0
  127. package/skill-docs/references/report/paged-group-report-guide.md +68 -0
  128. package/skill-docs/references/report/preaggregate-auto-disable-guide.md +24 -13
  129. package/skill-docs/references/report/query-condition-api-guide.md +145 -125
  130. package/skill-docs/references/report/ref-report-commands.md +41 -3
  131. package/skill-docs/references/report/report-baseinfo-defaults-guide.md +117 -117
  132. package/skill-docs/references/report/report-building-guide.md +52 -52
  133. package/skill-docs/references/report/report-case-prompts.md +104 -0
  134. package/skill-docs/references/report/report-config-schema.md +281 -281
  135. package/skill-docs/references/report/report-create-modes-guide.md +73 -35
  136. package/skill-docs/references/report/report-data-model.md +131 -131
  137. package/skill-docs/references/report/report-delivery-contract.md +23 -3
  138. package/skill-docs/references/report/report-designer-feature-map.md +30 -35
  139. package/skill-docs/references/report/report-help-guide.md +304 -0
  140. package/skill-docs/references/report/report-internal-error-guide.md +71 -0
  141. package/skill-docs/references/report/report-knowledge-base.md +27 -27
  142. package/skill-docs/references/report/report-lifecycle-api-guide.md +126 -94
  143. package/skill-docs/references/report/report-list-page-ops-guide.md +54 -8
  144. package/skill-docs/references/report/report-permission-guide.md +99 -102
  145. package/skill-docs/references/report/report-requirement-analysis-template.md +93 -29
  146. package/skill-docs/references/report/report-self-check-guide.md +164 -164
  147. package/skill-docs/references/report/report-verification-discipline-guide.md +203 -0
  148. package/skill-docs/references/report/safe-partial-save-guide.md +151 -151
  149. package/skill-docs/references/report/shard-freeze-report-guide.md +97 -0
  150. package/skill-docs/references/report/snowflake-id-precision-guide.md +84 -84
  151. package/skill-docs/references/report/spec-build-schema.md +71 -71
  152. package/skill-docs/references/report/targeted-fix-guide.md +155 -97
  153. package/skill-docs/references/report/user-request-scope-only-guide.md +23 -4
  154. package/skill-docs/references/workflow/guide-after-submit-comment.md +58 -0
  155. package/skill-docs/references/workflow/guide-workflow.md +6 -0
@@ -1,10 +1,20 @@
1
1
  import { readFileSync } from 'node:fs';
2
2
  import { withReport } from './report.js';
3
- import { getBaseInfo, getReportDevInfo, saveReportDevInfo } from '../services/report/reports.js';
3
+ import { getBaseInfo, getReportDevInfo, listReports, saveReportDevInfo } from '../services/report/reports.js';
4
+ import { applyFieldBindings, scanFieldBindings } from '../services/report/field-binding.js';
5
+ import { foreignRefs, planDatasetIdRemap } from '../services/report/dataset-ids.js';
6
+ import { fixEmptyDatasets, inspectEmptyDatasets } from '../services/report/empty-dataset.js';
7
+ import { assertAllowedDatasetIds, buildBlueprint, verifyBlueprintReadback, } from '../services/report/blueprint.js';
4
8
  import { listDatasets, fieldsKeyOf } from '../services/report/dataset.js';
5
9
  import { checkCellLimit, validateScrollPage, validateShowLocation, validateSnapshotPatch, validateWorkbookGridline, validateSheetPropGridLine, } from '../services/report/guard.js';
6
- import { mergeWorkbookSafe, fieldToCell, fieldStyle } from '../services/report/safe-save.js';
7
- import { checkCrossFilters, checkTableNameContract, readCrossFilters, verifyAll, verifyFieldBind, } from '../services/report/verify.js';
10
+ import { buildWorkbookFromFields, fieldStyle, fieldToCell, mergeWorkbookSafe, } from '../services/report/safe-save.js';
11
+ import { constCell } from '../services/report/workbook.js';
12
+ import { checkFormulaParents } from '../services/report/formula-parent.js';
13
+ import { checkScene, detectScenes, jumpHref, rulesText } from '../services/report/scene.js';
14
+ import { checkPreaggregate } from '../services/report/analyze.js';
15
+ import { renameDataset } from '../services/report/dataset.js';
16
+ import { saveReportWithCacheConfig } from '../services/report/reports.js';
17
+ import { cellMeta, checkAggregateAnchors, checkCrossFilters, checkTableNameContract, readCrossFilters, verifyAll, verifyFieldBind, } from '../services/report/verify.js';
8
18
  import { hrefCatalog, mapNaturalPhrase, normalizeFont, normalizeGroupType, normalizeHref, normalizeNumberFormat, normalizeTransform, styleCatalog, validateBaseMeta, validateCellPatch, validateConditionRule, FORMAT_CATALOG, GROUP_TYPE_ALIAS, } from '../services/report/validate.js';
9
19
  function readInput(raw) {
10
20
  if (raw.inline)
@@ -26,6 +36,15 @@ function printValidation(r, json, payload) {
26
36
  if (!r.ok)
27
37
  process.exitCode = 1;
28
38
  }
39
+ /** Field 绑定补丁的可读描述(dry-run 与结果共用)。 */
40
+ function patchText(p) {
41
+ const seg = [];
42
+ if (p.text)
43
+ seg.push(`text ${JSON.stringify(p.text.from)}→${JSON.stringify(p.text.to)}`);
44
+ if (p.tableName)
45
+ seg.push(`tableName ${JSON.stringify(p.tableName.from)}→${JSON.stringify(p.tableName.to)}`);
46
+ return seg.join(' | ');
47
+ }
29
48
  /** 取报表的字段记录 + 应用 id。 */
30
49
  async function loadDev(http, reportId, appIdArg) {
31
50
  const base = await getBaseInfo(http, reportId);
@@ -377,6 +396,32 @@ export function registerReportValidateCommands(report, program) {
377
396
  process.exitCode = 1;
378
397
  });
379
398
  });
399
+ verify
400
+ .command('anchors')
401
+ .argument('<reportId>', '报表 ID')
402
+ .option('--app-id <id>', '应用 ID')
403
+ .description('汇总格锚点体检:三件套(纵向/汇总/分组 none)与左父格是否直指行维度入口格')
404
+ .action(async (reportId, raw) => {
405
+ await withReport(program, raw.appId || '', async (http, json) => {
406
+ const { fields } = await loadDev(http, reportId, raw.appId);
407
+ const items = checkAggregateAnchors(fields);
408
+ const aggs = fields.filter((f) => {
409
+ const m = cellMeta(f);
410
+ return ['sum', 'avg', 'max', 'min'].includes(m.countType.toLowerCase()) || m.displayType.toLowerCase() === 'count';
411
+ }).length;
412
+ if (json) {
413
+ console.log(JSON.stringify({ aggregateCells: aggs, items }, null, 2));
414
+ }
415
+ else {
416
+ console.log(`汇总格 ${aggs} 个`);
417
+ items.forEach((e) => console.log(`[FAIL] ${e.message}`));
418
+ if (!items.length)
419
+ console.log('PASS — 汇总格三件套与锚点均合规');
420
+ }
421
+ if (items.length)
422
+ process.exitCode = 1;
423
+ });
424
+ });
380
425
  verify
381
426
  .command('meta')
382
427
  .argument('<reportId>', '报表 ID')
@@ -622,6 +667,332 @@ export function registerReportValidateCommands(report, program) {
622
667
  process.exitCode = 2;
623
668
  });
624
669
  });
670
+ fix
671
+ .command('field-binding')
672
+ .argument('[reportId]', '报表 ID;省略则扫描该应用内全部报表')
673
+ .requiredOption('--app-id <id>', '应用 ID')
674
+ .option('--table-name <name>', '强制指定单元格 tableName(默认按数据集推导)')
675
+ .option('--apply', '真的落库')
676
+ .description('回补 Field 格的字段显示名(text)与数据集回显名(tableName);换数据集 / heal 后必跑')
677
+ .action(async (reportId, raw) => {
678
+ await withReport(program, raw.appId, async (http, json) => {
679
+ const opts = { forceTableName: raw.tableName ? String(raw.tableName) : undefined };
680
+ // ── 应用级批量 ──
681
+ if (!reportId) {
682
+ const rows = await listReports(http, raw.appId);
683
+ const out = [];
684
+ for (const r of rows) {
685
+ const rid = String(r.id || '');
686
+ if (!rid)
687
+ continue;
688
+ try {
689
+ const scan = await scanFieldBindings(http, rid, raw.appId, opts);
690
+ out.push({
691
+ reportId: rid,
692
+ name: r.name || '',
693
+ fieldCells: scan.fieldCells,
694
+ patches: scan.patches,
695
+ blankConst: scan.blankConst,
696
+ });
697
+ }
698
+ catch (e) {
699
+ out.push({ reportId: rid, name: r.name || '', error: e.message });
700
+ }
701
+ }
702
+ const dirty = out.filter((x) => !x.error && x.patches.length);
703
+ const errors = out.filter((x) => x.error);
704
+ if (json && !raw.apply) {
705
+ console.log(JSON.stringify({ total: out.length, damaged: dirty.length, rows: out }, null, 2));
706
+ return;
707
+ }
708
+ console.log(`应用内共 ${out.length} 张报表,回显受损 ${dirty.length} 张。`);
709
+ for (const x of out) {
710
+ if (x.error) {
711
+ console.log(` ✗ ${x.name || x.reportId} (${x.reportId}) —— 扫描失败:${x.error}`);
712
+ continue;
713
+ }
714
+ if (!x.patches.length) {
715
+ console.log(` ✓ ${x.name || x.reportId} (${x.reportId}) —— Field ${x.fieldCells} 格回显正常`);
716
+ continue;
717
+ }
718
+ console.log(` ✗ ${x.name || x.reportId} (${x.reportId}) —— ${x.patches.length} 格待补`);
719
+ x.patches.slice(0, 4).forEach((p) => console.log(` [${p.at}] ${patchText(p)}`));
720
+ if (x.patches.length > 4)
721
+ console.log(` … 另有 ${x.patches.length - 4} 格`);
722
+ }
723
+ if (!raw.apply) {
724
+ console.log('(预演,未落库。加 --apply 执行)');
725
+ return;
726
+ }
727
+ if (!dirty.length) {
728
+ console.log('无需修复。');
729
+ return;
730
+ }
731
+ let okN = 0;
732
+ for (const x of dirty) {
733
+ try {
734
+ const scan = await scanFieldBindings(http, x.reportId, raw.appId, opts);
735
+ const res = await applyFieldBindings(http, scan);
736
+ if (res.ok)
737
+ okN += 1;
738
+ console.log(` ${res.ok ? '✓' : '✗'} ${x.name || x.reportId} —— ${scan.patches.length} 格回补;` +
739
+ `fields ${res.cells}/${res.target},Formula ${res.formula},残留不合规 ${res.residual}`);
740
+ }
741
+ catch (e) {
742
+ console.log(` ✗ ${x.name || x.reportId} —— ${e.message}`);
743
+ }
744
+ }
745
+ console.log(`完成:${okN}/${dirty.length} 张修复通过。`);
746
+ if (okN !== dirty.length || errors.length)
747
+ process.exitCode = 2;
748
+ return;
749
+ }
750
+ // ── 单张 ──
751
+ const scan = await scanFieldBindings(http, reportId, raw.appId, opts);
752
+ if (json && !raw.apply) {
753
+ console.log(JSON.stringify({ reportId, fieldCells: scan.fieldCells, patches: scan.patches }, null, 2));
754
+ return;
755
+ }
756
+ if (!scan.patches.length) {
757
+ console.log(`Field 格 ${scan.fieldCells} 个,text/tableName 均已合规,无需修复。`);
758
+ if (scan.blankConst.length) {
759
+ console.log(`(旁证:Const 格文字为空 ${scan.blankConst.length} 个:${scan.blankConst.slice(0, 8).join(', ')})`);
760
+ }
761
+ return;
762
+ }
763
+ console.log(`报表 ${reportId}:Field 格 ${scan.fieldCells} 个,待补 ${scan.patches.length} 个:`);
764
+ scan.patches.forEach((p) => console.log(` [${p.at}] ${patchText(p)}`));
765
+ if (scan.blankConst.length) {
766
+ console.log(`(旁证:Const 格文字为空 ${scan.blankConst.length} 个:${scan.blankConst.slice(0, 8).join(', ')})`);
767
+ }
768
+ if (!raw.apply) {
769
+ console.log('(预演,未落库。加 --apply 执行)');
770
+ return;
771
+ }
772
+ const res = await applyFieldBindings(http, scan);
773
+ console.log(`回读:fields ${res.cells}/${res.target} ${JSON.stringify(res.counts)} ` +
774
+ `残留不合规 Field=${res.residual} Formula=${res.formula}`);
775
+ if (res.ok) {
776
+ console.log(`已落库 —— ${scan.patches.length} 格回补完成。`);
777
+ }
778
+ else {
779
+ console.error('回读校验未通过:请检查该报表是否被其它操作同时改写。');
780
+ process.exitCode = 2;
781
+ }
782
+ });
783
+ });
784
+ fix
785
+ .command('dataset-id')
786
+ .argument('[reportId]', '报表 ID;省略则扫描该应用内全部报表')
787
+ .requiredOption('--app-id <id>', '应用 ID')
788
+ .option('--apply', '真的落库')
789
+ .description('把外来数据集 id 换回本报表同 sourceId 的 id(设计器字段下拉空白时用)')
790
+ .action(async (reportId, raw) => {
791
+ const ownOf = async (http, rid) => {
792
+ const rows = await listDatasets(http, rid, raw.appId);
793
+ return rows.map((d) => ({
794
+ id: String(d.id),
795
+ sourceId: String(d.sourceId || ''),
796
+ name: d.name,
797
+ type: d.sourceType,
798
+ }));
799
+ };
800
+ const applyOne = async (http, rid) => {
801
+ const dev = await getReportDevInfo(http, rid);
802
+ const fields = dev.fields || [];
803
+ const own = await ownOf(http, rid);
804
+ const plan = planDatasetIdRemap(fields, own);
805
+ return { dev, fields, own, plan };
806
+ };
807
+ await withReport(program, raw.appId, async (http, json) => {
808
+ // ── 应用级批量 ──
809
+ if (!reportId) {
810
+ const rows = await listReports(http, raw.appId);
811
+ const out = [];
812
+ for (const r of rows) {
813
+ const rid = String(r.id || '');
814
+ if (!rid)
815
+ continue;
816
+ try {
817
+ const { plan } = await applyOne(http, rid);
818
+ out.push({
819
+ reportId: rid,
820
+ name: r.name || '',
821
+ changes: plan.changes.length,
822
+ unresolved: plan.unresolved,
823
+ conflicts: plan.conflicts,
824
+ });
825
+ }
826
+ catch (e) {
827
+ out.push({ reportId: rid, name: r.name || '', error: e.message });
828
+ }
829
+ }
830
+ const dirty = out.filter((x) => !x.error && x.changes);
831
+ if (json && !raw.apply) {
832
+ console.log(JSON.stringify({ total: out.length, dirty: dirty.length, rows: out }, null, 2));
833
+ return;
834
+ }
835
+ console.log(`应用内共 ${out.length} 张报表,含外来数据集 id 的 ${dirty.length} 张。`);
836
+ out.forEach((x) => {
837
+ if (x.error)
838
+ console.log(` ✗ ${x.name || x.reportId} —— ${x.error}`);
839
+ else if (!x.changes)
840
+ console.log(` ✓ ${x.name || x.reportId} —— 无外来 id`);
841
+ else
842
+ console.log(` ✗ ${x.name || x.reportId} —— ${x.changes} 处引用待换` +
843
+ (x.unresolved.length ? `,${x.unresolved.length} 处无法解析` : '') +
844
+ (x.conflicts.length ? `,${x.conflicts.length} 处冲突` : ''));
845
+ });
846
+ if (!raw.apply) {
847
+ console.log('(预演,未落库。加 --apply 执行)');
848
+ return;
849
+ }
850
+ if (!dirty.length) {
851
+ console.log('无需修复。');
852
+ return;
853
+ }
854
+ let okN = 0;
855
+ for (const x of dirty) {
856
+ try {
857
+ const { dev, fields, plan } = await applyOne(http, x.reportId);
858
+ if (plan.unresolved.length || plan.conflicts.length) {
859
+ console.log(` ✗ ${x.name || x.reportId} —— 有无法解析/冲突的引用,跳过`);
860
+ continue;
861
+ }
862
+ const wb = buildWorkbookFromFields(plan.next, (dev.sheetProps || [])[0] || {});
863
+ const base = await getBaseInfo(http, x.reportId);
864
+ const fresh = await getReportDevInfo(http, x.reportId);
865
+ await saveReportDevInfo(http, {
866
+ appId: raw.appId,
867
+ reportId: x.reportId,
868
+ name: String(base.name || ''),
869
+ workbook: wb,
870
+ designKey: String(fresh.designKey || ''),
871
+ groupId: String(base.groupId ?? '-1'),
872
+ nameLabelId: String(base.nameLabelId ?? '-1'),
873
+ });
874
+ const after = await getReportDevInfo(http, x.reportId);
875
+ const own = await ownOf(http, x.reportId);
876
+ const left = foreignRefs(after.fields || [], own);
877
+ if (left.length) {
878
+ console.log(` ✗ ${x.name || x.reportId} —— 回读仍有 ${left.length} 处外来 id`);
879
+ }
880
+ else {
881
+ okN++;
882
+ console.log(` ✓ ${x.name || x.reportId} —— ${plan.changes.length} 处已换`);
883
+ }
884
+ }
885
+ catch (e) {
886
+ console.log(` ✗ ${x.name || x.reportId} —— ${e.message}`);
887
+ }
888
+ }
889
+ console.log(`完成:${okN}/${dirty.length} 张修复通过。`);
890
+ if (okN !== dirty.length)
891
+ process.exitCode = 2;
892
+ return;
893
+ }
894
+ // ── 单张 ──
895
+ const { dev, fields, own, plan } = await applyOne(http, reportId);
896
+ if (json && !raw.apply) {
897
+ console.log(JSON.stringify({ reportId, ...plan, next: undefined }, null, 2));
898
+ return;
899
+ }
900
+ if (!plan.changes.length && !plan.unresolved.length) {
901
+ console.log(`PASS —— ${fields.length} 格里没有外来数据集 id。`);
902
+ return;
903
+ }
904
+ console.log(`报表 ${reportId}:${plan.refCount} 处数据集引用,外来 ${plan.changes.length} 处` +
905
+ (plan.unresolved.length ? `,无法解析 ${plan.unresolved.length} 处` : '') +
906
+ (plan.conflicts.length ? `,冲突 ${plan.conflicts.length} 处` : ''));
907
+ plan.changes.forEach((c) => console.log(` [(${c.x},${c.y})${c.text ? ' ' + c.text : ''}] ${c.key}: ${c.from} → ${c.to}`));
908
+ plan.unresolved.forEach((u) => console.log(` [?] (${u.x},${u.y}) ${u.key}=${u.id} —— ${u.reason}`));
909
+ plan.conflicts.forEach((c) => console.log(` [!] ${c.foreignId} 同时映射到 ${c.a} 与 ${c.b}((${c.x},${c.y}))`));
910
+ if (plan.unresolved.length || plan.conflicts.length) {
911
+ console.error('存在无法确定目标数据集的引用,已跳过落库。');
912
+ process.exitCode = 3;
913
+ return;
914
+ }
915
+ if (!raw.apply) {
916
+ console.log('(预演,未落库。加 --apply 执行)');
917
+ return;
918
+ }
919
+ const wb = buildWorkbookFromFields(plan.next, (dev.sheetProps || [])[0] || {});
920
+ const base = await getBaseInfo(http, reportId);
921
+ const fresh = await getReportDevInfo(http, reportId);
922
+ await saveReportDevInfo(http, {
923
+ appId: raw.appId,
924
+ reportId,
925
+ name: String(base.name || ''),
926
+ workbook: wb,
927
+ designKey: String(fresh.designKey || ''),
928
+ groupId: String(base.groupId ?? '-1'),
929
+ nameLabelId: String(base.nameLabelId ?? '-1'),
930
+ });
931
+ const after = await getReportDevInfo(http, reportId);
932
+ const left = foreignRefs(after.fields || [], own);
933
+ const af = after.fields || [];
934
+ const mismatch = af.filter((f) => f.cellType === 'Field' && String(f.fieldKey || '').split('.')[0] !== String(f.dataSetId)).length;
935
+ console.log(`回读:fields ${af.length}/${fields.length} 外来 id ${left.length} 处 fieldKey 首段≠dataSetId ${mismatch}`);
936
+ if (!left.length && !mismatch) {
937
+ console.log(`VERIFY OK —— ${plan.changes.length} 处引用已换回本报表数据集。`);
938
+ }
939
+ else {
940
+ console.error('VERIFY FAIL');
941
+ process.exitCode = 2;
942
+ }
943
+ });
944
+ });
945
+ fix
946
+ .command('empty-dataset')
947
+ .argument('<reportId>', '报表 ID')
948
+ .requiredOption('--app-id <id>', '应用 ID')
949
+ .option('--dataset-id <id>', '只处理指定数据集(默认全部空壳)')
950
+ .option('--source-type <type>', '重新挂载用的 sourceType', 'eteams')
951
+ .option('--source-group <g>', '重新挂载用的 sourceGroup', 'sql')
952
+ .option('--table-name <name>', '单元格 tableName(默认取数据集名)')
953
+ .option('--keep-old', '不删除旧空壳')
954
+ .option('--apply', '真的落库')
955
+ .description('治理「挂上了但字段为 0」的空壳数据集(SQL 视图被按业务模块挂载)')
956
+ .action(async (reportId, raw) => {
957
+ await withReport(program, raw.appId, async (http, json) => {
958
+ const ins = await inspectEmptyDatasets(http, reportId, raw.appId);
959
+ if (json && !raw.apply) {
960
+ console.log(JSON.stringify(ins, null, 2));
961
+ return;
962
+ }
963
+ console.log(`数据集 ${ins.rows.length} 条:`);
964
+ ins.rows.forEach((d) => console.log(` [${d.rawType}] ${d.name} id=${d.id} group=${d.sourceGroup || '-'} 字段=${d.fieldCount} 引用格=${d.refs}` +
965
+ (d.fieldCount === 0 ? ' ← 空壳' : '')));
966
+ const targets = raw.datasetId ? ins.empties.filter((d) => d.id === String(raw.datasetId)) : ins.empties;
967
+ if (!targets.length) {
968
+ console.log('\n无空壳数据集,无需修复。');
969
+ return;
970
+ }
971
+ console.log(`\n将处理 ${targets.length} 条空壳(按 sourceType=${raw.sourceType} / sourceGroup=${raw.sourceGroup} 重新挂载)`);
972
+ if (!raw.apply) {
973
+ console.log('[dry-run] 未落库。确认后加 --apply。');
974
+ return;
975
+ }
976
+ const res = await fixEmptyDatasets(http, reportId, raw.appId, {
977
+ datasetId: raw.datasetId,
978
+ sourceType: raw.sourceType,
979
+ sourceGroup: raw.sourceGroup,
980
+ tableName: raw.tableName,
981
+ keepOld: !!raw.keepOld,
982
+ });
983
+ res.targets.forEach((t) => console.log(` ✓ ${t.oldId} → ${t.newId}:重映射 ${t.cells} 格,新数据集字段 ${t.fields}`));
984
+ res.removed.forEach((r) => console.log(` ${r.ok ? '✓' : 'SKIP'} 删除旧空壳 ${r.datasetId}${r.reason ? `:${r.reason}` : ''}`));
985
+ const ok = res.cellsAfter === res.cellsBefore && res.staleLeft === 0 && res.mismatch === 0;
986
+ console.log(`回读:${res.cellsAfter} 格(落库前 ${res.cellsBefore});旧 id 残留 ${res.staleLeft};fieldKey 不自洽 ${res.mismatch}`);
987
+ if (ok) {
988
+ console.log('结论:通过。');
989
+ }
990
+ else {
991
+ console.error('结论:未通过。');
992
+ process.exitCode = 3;
993
+ }
994
+ });
995
+ });
625
996
  fix
626
997
  .command('freeze')
627
998
  .argument('<reportId>', '报表 ID')
@@ -715,5 +1086,697 @@ export function registerReportValidateCommands(report, program) {
715
1086
  process.exitCode = 2;
716
1087
  });
717
1088
  });
1089
+ // ── 公式格父格(锚点) ──────────────────────────────────
1090
+ const formula = report
1091
+ .command('formula')
1092
+ .description('公式格父格:check 只读体检 / align 左父格对齐 / top 上父格定点修正');
1093
+ const fparent = formula.command('parent').description('公式格左父格:check 只读 / align 对齐');
1094
+ fparent
1095
+ .command('check')
1096
+ .argument('<reportId>', '报表 ID')
1097
+ .option('--app-id <id>', '应用 ID')
1098
+ .description('检查公式格左父格是否等于被引用字段的区域根格(不一致 exit 3)')
1099
+ .action(async (reportId, raw) => {
1100
+ await withReport(program, raw.appId || '', async (http, json) => {
1101
+ const { fields } = await loadDev(http, reportId, raw.appId);
1102
+ const rows = checkFormulaParents(fields);
1103
+ const bad = rows.filter((r) => r.issues.length);
1104
+ if (json) {
1105
+ console.log(JSON.stringify({ formulas: rows.length, issues: bad }, null, 2));
1106
+ }
1107
+ else {
1108
+ console.log(`公式格 ${rows.length} 个`);
1109
+ bad.forEach((r) => {
1110
+ console.log(`[FAIL] (${r.cell[0]},${r.cell[1]})`);
1111
+ r.issues.forEach((m) => console.log(` ${m}`));
1112
+ });
1113
+ if (!bad.length)
1114
+ console.log('PASS — 公式格父格与被引用字段一致');
1115
+ }
1116
+ if (bad.length)
1117
+ process.exitCode = 3;
1118
+ });
1119
+ });
1120
+ fparent
1121
+ .command('align')
1122
+ .argument('<reportId>', '报表 ID')
1123
+ .option('--app-id <id>', '应用 ID')
1124
+ .option('--apply', '真的落库')
1125
+ .description('把公式格左父格对齐到被引用字段的区域根格,并把 refX/refY 归 -1')
1126
+ .action(async (reportId, raw) => {
1127
+ await withReport(program, raw.appId || '', async (http, json) => {
1128
+ const { base, appId, fields } = await loadDev(http, reportId, raw.appId);
1129
+ const rows = checkFormulaParents(fields);
1130
+ const targets = rows.filter((r) => r.expected && (r.current[0] !== r.expected[0] || r.current[1] !== r.expected[1] || r.refX !== -1 || r.refY !== -1));
1131
+ const manual = rows.filter((r) => !r.expected && r.issues.length);
1132
+ if (json) {
1133
+ console.log(JSON.stringify({ toFix: targets, manual }, null, 2));
1134
+ }
1135
+ else {
1136
+ targets.forEach((r) => console.log(`(${r.cell[0]},${r.cell[1]}) 左父格 (${r.current[0]},${r.current[1]}) → (${r.expected[0]},${r.expected[1]})`));
1137
+ manual.forEach((r) => console.log(`[需人工] (${r.cell[0]},${r.cell[1]}):${r.issues.join(';')}`));
1138
+ if (!targets.length)
1139
+ console.log('没有需要对齐的公式格。');
1140
+ }
1141
+ if (!raw.apply || !targets.length) {
1142
+ if (!json && targets.length)
1143
+ console.log('(预演,未落库。加 --apply 执行)');
1144
+ return;
1145
+ }
1146
+ const patch = new Map(targets.map((r) => [`${r.cell[0]},${r.cell[1]}`, r.expected]));
1147
+ const wb = mergeWorkbookSafe({ fields }, {
1148
+ patchFn: (w) => {
1149
+ for (const sheet of Object.values(w.sheets || {})) {
1150
+ const dt = sheet?.data?.dataTable || {};
1151
+ for (const [key, [ex, ey]] of patch) {
1152
+ const [x, y] = key.split(',').map(Number);
1153
+ const ct = dt[String(x)]?.[String(y)]?.style?.cellType;
1154
+ if (!ct)
1155
+ continue;
1156
+ ct.leftX = ex;
1157
+ ct.leftY = ey;
1158
+ ct.refX = -1;
1159
+ ct.refY = -1;
1160
+ }
1161
+ }
1162
+ },
1163
+ });
1164
+ const fresh = await getReportDevInfo(http, reportId);
1165
+ await saveReportDevInfo(http, {
1166
+ appId,
1167
+ reportId,
1168
+ name: String(base.name || ''),
1169
+ workbook: wb,
1170
+ designKey: String(fresh.designKey || ''),
1171
+ groupId: String(base.groupId ?? '0'),
1172
+ nameLabelId: String(base.nameLabelId ?? '-1'),
1173
+ });
1174
+ const after = checkFormulaParents(((await getReportDevInfo(http, reportId)).fields || []));
1175
+ const still = after.filter((r) => r.issues.length);
1176
+ console.log(still.length ? `⚠️ 回读仍有 ${still.length} 个公式格不一致` : `已对齐 ${targets.length} 个公式格`);
1177
+ if (still.length)
1178
+ process.exitCode = 2;
1179
+ });
1180
+ });
1181
+ formula
1182
+ .command('top')
1183
+ .argument('<reportId>', '报表 ID')
1184
+ .option('--app-id <id>', '应用 ID')
1185
+ .option('--set-top-parent <pair...>', '定点修正上父格:x,y=topX,topY(可重复)')
1186
+ .option('--apply', '真的落库')
1187
+ .description('上父格定点修正(交叉表「合计列 → 列头格」场景)')
1188
+ .action(async (reportId, raw) => {
1189
+ await withReport(program, raw.appId || '', async (http, json) => {
1190
+ const pairs = (raw.setTopParent || []);
1191
+ const patch = new Map();
1192
+ for (const p of pairs) {
1193
+ const m = String(p).match(/^(-?\d+),(-?\d+)=(-?\d+),(-?\d+)$/);
1194
+ if (!m)
1195
+ throw new Error(`--set-top-parent 需为 x,y=topX,topY,收到「${p}」`);
1196
+ patch.set(`${Number(m[1])},${Number(m[2])}`, [Number(m[3]), Number(m[4])]);
1197
+ }
1198
+ if (!patch.size)
1199
+ throw new Error('至少要给一个 --set-top-parent "x,y=topX,topY"');
1200
+ const { base, appId, fields } = await loadDev(http, reportId, raw.appId);
1201
+ const missing = [...patch.keys()].filter((k) => !fields.some((f) => `${Number(f.x)},${Number(f.y)}` === k));
1202
+ if (missing.length) {
1203
+ console.error(`这些坐标上没有单元格:${missing.join(' ')}`);
1204
+ process.exitCode = 1;
1205
+ return;
1206
+ }
1207
+ if (json)
1208
+ console.log(JSON.stringify({ patch: Object.fromEntries(patch) }, null, 2));
1209
+ else
1210
+ patch.forEach(([tx, ty], k) => console.log(`${k} 上父格 → (${tx},${ty})`));
1211
+ if (!raw.apply) {
1212
+ if (!json)
1213
+ console.log('(预演,未落库。加 --apply 执行)');
1214
+ return;
1215
+ }
1216
+ const wb = mergeWorkbookSafe({ fields }, {
1217
+ patchFn: (w) => {
1218
+ for (const sheet of Object.values(w.sheets || {})) {
1219
+ const dt = sheet?.data?.dataTable || {};
1220
+ for (const [key, [tx, ty]] of patch) {
1221
+ const [x, y] = key.split(',').map(Number);
1222
+ const ct = dt[String(x)]?.[String(y)]?.style?.cellType;
1223
+ if (!ct)
1224
+ continue;
1225
+ ct.topX = tx;
1226
+ ct.topY = ty;
1227
+ }
1228
+ }
1229
+ },
1230
+ });
1231
+ const fresh = await getReportDevInfo(http, reportId);
1232
+ await saveReportDevInfo(http, {
1233
+ appId,
1234
+ reportId,
1235
+ name: String(base.name || ''),
1236
+ workbook: wb,
1237
+ designKey: String(fresh.designKey || ''),
1238
+ groupId: String(base.groupId ?? '0'),
1239
+ nameLabelId: String(base.nameLabelId ?? '-1'),
1240
+ });
1241
+ console.log(`已落库 ${patch.size} 个上父格`);
1242
+ });
1243
+ });
1244
+ // ── 左父格定点修正 ──────────────────────────────────────
1245
+ fix
1246
+ .command('parent')
1247
+ .argument('<reportId>', '报表 ID')
1248
+ .option('--app-id <id>', '应用 ID')
1249
+ .option('--set-parent <pair...>', '定点修正左父格:x,y=leftX,leftY(可重复)')
1250
+ .option('--apply', '真的落库')
1251
+ .description('左父格定点修正(默认只读预演)')
1252
+ .action(async (reportId, raw) => {
1253
+ await withReport(program, raw.appId || '', async (http, json) => {
1254
+ const pairs = (raw.setParent || []);
1255
+ const patch = new Map();
1256
+ for (const p of pairs) {
1257
+ const m = String(p).match(/^(-?\d+),(-?\d+)=(-?\d+),(-?\d+)$/);
1258
+ if (!m)
1259
+ throw new Error(`--set-parent 需为 x,y=leftX,leftY,收到「${p}」`);
1260
+ patch.set(`${Number(m[1])},${Number(m[2])}`, [Number(m[3]), Number(m[4])]);
1261
+ }
1262
+ if (!patch.size)
1263
+ throw new Error('至少要给一个 --set-parent "x,y=leftX,leftY"');
1264
+ const { base, appId, fields } = await loadDev(http, reportId, raw.appId);
1265
+ const missing = [...patch.keys()].filter((k) => !fields.some((f) => `${Number(f.x)},${Number(f.y)}` === k));
1266
+ if (missing.length) {
1267
+ console.error(`这些坐标上没有单元格:${missing.join(' ')}`);
1268
+ process.exitCode = 1;
1269
+ return;
1270
+ }
1271
+ if (json)
1272
+ console.log(JSON.stringify({ patch: Object.fromEntries(patch) }, null, 2));
1273
+ else
1274
+ patch.forEach(([lx, ly], k) => console.log(`${k} 左父格 → (${lx},${ly})`));
1275
+ if (!raw.apply) {
1276
+ if (!json)
1277
+ console.log('(预演,未落库。加 --apply 执行)');
1278
+ return;
1279
+ }
1280
+ const wb = mergeWorkbookSafe({ fields }, {
1281
+ patchFn: (w) => {
1282
+ for (const sheet of Object.values(w.sheets || {})) {
1283
+ const dt = sheet?.data?.dataTable || {};
1284
+ for (const [key, [lx, ly]] of patch) {
1285
+ const [x, y] = key.split(',').map(Number);
1286
+ const ct = dt[String(x)]?.[String(y)]?.style?.cellType;
1287
+ if (!ct)
1288
+ continue;
1289
+ ct.leftX = lx;
1290
+ ct.leftY = ly;
1291
+ }
1292
+ }
1293
+ },
1294
+ });
1295
+ const fresh = await getReportDevInfo(http, reportId);
1296
+ await saveReportDevInfo(http, {
1297
+ appId,
1298
+ reportId,
1299
+ name: String(base.name || ''),
1300
+ workbook: wb,
1301
+ designKey: String(fresh.designKey || ''),
1302
+ groupId: String(base.groupId ?? '0'),
1303
+ nameLabelId: String(base.nameLabelId ?? '-1'),
1304
+ });
1305
+ console.log(`已落库 ${patch.size} 个左父格`);
1306
+ });
1307
+ });
1308
+ // ── 数据集 id 归属体检(只读) ──────────────────────────
1309
+ report
1310
+ .command('check-ids')
1311
+ .argument('<reportId>', '报表 ID')
1312
+ .option('--app-id <id>', '应用 ID')
1313
+ .description('只读:单元格里的数据集 id 是否都来自本报表(外来 id 会让设计器字段下拉空白)')
1314
+ .action(async (reportId, raw) => {
1315
+ await withReport(program, raw.appId || '', async (http, json) => {
1316
+ const { appId, fields } = await loadDev(http, reportId, raw.appId);
1317
+ const datasets = await listDatasets(http, reportId, appId);
1318
+ const own = datasets.map((d) => ({
1319
+ id: d.id,
1320
+ sourceId: String(d.sourceId || ''),
1321
+ name: d.name,
1322
+ type: d.sourceType,
1323
+ }));
1324
+ const issues = [];
1325
+ for (const r of foreignRefs(fields, own)) {
1326
+ issues.push(`(${r.x},${r.y}) ${r.key} ${r.id} 不属于本报表`);
1327
+ }
1328
+ for (const f of fields) {
1329
+ if (f.cellType !== 'Field')
1330
+ continue;
1331
+ const dsId = String(f.dataSetId ?? '');
1332
+ const first = String(f.fieldKey ?? '').split('.')[0];
1333
+ if (dsId && first && first !== dsId) {
1334
+ issues.push(`(${f.x},${f.y}) fieldKey 首段 ${first} ≠ dataSetId ${dsId}`);
1335
+ }
1336
+ }
1337
+ if (json) {
1338
+ console.log(JSON.stringify({ datasets: datasets.map((d) => d.id), issues }, null, 2));
1339
+ }
1340
+ else {
1341
+ console.log(`本报表数据集 ${datasets.length} 个:${datasets.map((d) => `${d.name}(${d.id})`).join('、')}`);
1342
+ issues.forEach((m) => console.log(`[FAIL] ${m}`));
1343
+ if (!issues.length)
1344
+ console.log('PASS — 所有单元格的数据集 id 都来自本报表');
1345
+ }
1346
+ if (issues.length)
1347
+ process.exitCode = 3;
1348
+ });
1349
+ });
1350
+ // ── 从蓝图整表重建 ──────────────────────────────────────
1351
+ const rebuild = report.command('rebuild').description('整表重建(灾后救援)');
1352
+ rebuild
1353
+ .command('from-blueprint')
1354
+ .requiredOption('--blueprint <file>', '蓝图 JSON 文件')
1355
+ .option('--apply', '真的落库')
1356
+ .option('--allow-ids <ids>', '额外允许的数据集 id(逗号分隔)')
1357
+ .description('按蓝图重建整张报表;落库前跑克隆纪律与数据集 id 白名单门禁')
1358
+ .action(async (raw) => {
1359
+ const bp = JSON.parse(readFileSync(raw.blueprint, 'utf-8').replace(/^/, ''));
1360
+ await withReport(program, String(bp.appId || ''), async (http, json) => {
1361
+ const built = await buildBlueprint(http, bp);
1362
+ const extra = String(raw['allowIds'] || '')
1363
+ .split(',')
1364
+ .map((s) => s.trim())
1365
+ .filter(Boolean);
1366
+ const allowedIds = built.allowedIds.concat(extra);
1367
+ // 落库前硬门禁:任何白名单外的数据集 id 都拒绝
1368
+ const bad = assertAllowedDatasetIds(built.fields, allowedIds);
1369
+ if (bad.length) {
1370
+ console.error(`数据集 id 门禁未通过(${bad.length} 处白名单外引用):`);
1371
+ bad.slice(0, 12).forEach((b) => console.error(` ✗ ${b}`));
1372
+ process.exitCode = 3;
1373
+ return;
1374
+ }
1375
+ const summary = {
1376
+ reportId: bp.reportId,
1377
+ appId: bp.appId,
1378
+ sheets: built.sheets.map((s) => ({ name: s.name, cells: s.fields.length })),
1379
+ counts: built.counts,
1380
+ datasets: built.datasets.map((d) => ({ key: d.key, id: d.id, name: d.tableName })),
1381
+ allowedIds,
1382
+ cells: built.fields.length,
1383
+ };
1384
+ if (json && !raw.apply) {
1385
+ console.log(JSON.stringify(summary, null, 2));
1386
+ return;
1387
+ }
1388
+ console.log(`蓝图重建 报表 ${bp.reportId}(appId=${bp.appId}):${built.fields.length} 格 ${JSON.stringify(built.counts)}`);
1389
+ console.log('克隆纪律自检:0 坐标冲突 / 0 META 残留 / 0 旧 id ✅');
1390
+ console.log(`数据集白名单:${allowedIds.join(' / ')}`);
1391
+ if (bp.styleSource)
1392
+ console.log(`样式/键模板来源:${bp.styleSource.reportId}`);
1393
+ for (const s of built.sheets) {
1394
+ console.log(` sheet「${s.name}」${s.fields.length} 格`);
1395
+ s.fields.slice(0, 40).forEach((f) => {
1396
+ const extraTxt = f.cellType === 'Field'
1397
+ ? ` ← ${f.fieldKey}`
1398
+ : f.cellType === 'Formula'
1399
+ ? ` = ${f.cellText}`
1400
+ : ` "${f.cellText}"`;
1401
+ console.log(` [${Number(f.x) + 1}行${String.fromCharCode(65 + Number(f.y))}列] ${f.cellType} left=(${f.leftX},${f.leftY})${extraTxt}`);
1402
+ });
1403
+ if (s.fields.length > 40)
1404
+ console.log(` … 另有 ${s.fields.length - 40} 格`);
1405
+ }
1406
+ const dev = await getReportDevInfo(http, bp.reportId);
1407
+ if (!raw.apply) {
1408
+ console.log('\n[dry-run] 未落库。确认后加 --apply。');
1409
+ console.log(`当前线上该表格数:${(dev.fields || []).length}(蓝图将替换为 ${built.fields.length})`);
1410
+ return;
1411
+ }
1412
+ // 逐 sheet 重建后合并(不同 sheet 同坐标互不覆盖)
1413
+ const workbook = { version: '11.1.0', sheetCount: built.sheets.length, newTabVisible: false, tabEditable: false, sheets: {} };
1414
+ built.sheets.forEach((s, i) => {
1415
+ const sp = (dev.sheetProps || [])[s.index] || (dev.sheetProps || [])[0] || {};
1416
+ const wb = buildWorkbookFromFields(s.fields, { ...sp, name: s.name });
1417
+ const sheetObj = Object.values(wb.sheets)[0];
1418
+ sheetObj.name = s.name;
1419
+ sheetObj.index = i;
1420
+ workbook.sheets[s.name] = sheetObj;
1421
+ });
1422
+ const base = await getBaseInfo(http, bp.reportId);
1423
+ await saveReportDevInfo(http, {
1424
+ appId: String(bp.appId),
1425
+ reportId: String(bp.reportId),
1426
+ name: String(bp.name || base.name || ''),
1427
+ workbook,
1428
+ designKey: String(dev.designKey || ''),
1429
+ groupId: String(base.groupId ?? '-1'),
1430
+ nameLabelId: String(base.nameLabelId ?? '-1'),
1431
+ });
1432
+ const after = await getReportDevInfo(http, bp.reportId);
1433
+ const check = verifyBlueprintReadback(after.fields || [], built);
1434
+ console.log(`\n回读:fields ${(after.fields || []).length}/${built.fields.length} ${JSON.stringify(check.counts)}`);
1435
+ if (check.ok) {
1436
+ console.log(`VERIFY OK —— 蓝图 ${built.fields.length} 格已落库并回读一致。`);
1437
+ }
1438
+ else {
1439
+ console.error('VERIFY FAIL');
1440
+ check.issues.forEach((m) => console.error(` ✗ ${m}`));
1441
+ process.exitCode = 2;
1442
+ }
1443
+ });
1444
+ });
1445
+ // ── 数据预聚合 ──────────────────────────────────────────
1446
+ const preagg = report.command('preagg').description('数据预聚合(解析优化):check 只读 / disable 关闭');
1447
+ preagg
1448
+ .command('check')
1449
+ .argument('<reportId>', '报表 ID')
1450
+ .option('--app-id <id>', '应用 ID')
1451
+ .description('判定该报表是否应关闭预聚合(只读)')
1452
+ .action(async (reportId, raw) => {
1453
+ await withReport(program, raw.appId || '', async (http, json) => {
1454
+ const { fields } = await loadDev(http, reportId, raw.appId);
1455
+ const r = checkPreaggregate([], fields);
1456
+ if (json)
1457
+ console.log(JSON.stringify(r, null, 2));
1458
+ else {
1459
+ console.log(`建议 dataPreAgg = ${r.dataPreAgg}`);
1460
+ r.reasons.forEach((m) => console.log(` · ${m}`));
1461
+ if (r.dataPreAgg)
1462
+ console.log('未命中阻断条件,可保持开启。');
1463
+ }
1464
+ if (!r.dataPreAgg)
1465
+ process.exitCode = 2;
1466
+ });
1467
+ });
1468
+ preagg
1469
+ .command('disable')
1470
+ .argument('<reportId>', '报表 ID')
1471
+ .option('--app-id <id>', '应用 ID')
1472
+ .option('--apply', '真的落库')
1473
+ .description('关闭预聚合(写 sheetConfig dataPreAgg=false)')
1474
+ .action(async (reportId, raw) => {
1475
+ await withReport(program, raw.appId || '', async (http, json) => {
1476
+ const { base, appId, dev, fields } = await loadDev(http, reportId, raw.appId);
1477
+ if (!raw.apply) {
1478
+ console.log('将关闭预聚合:sheetConfig = {"dataPreAgg":false,"simpleList":false}');
1479
+ console.log('(预演,未落库。加 --apply 执行)');
1480
+ return;
1481
+ }
1482
+ const wb = mergeWorkbookSafe({ ...dev, fields });
1483
+ const datasets = await listDatasets(http, reportId, appId);
1484
+ await saveReportWithCacheConfig(http, {
1485
+ appId,
1486
+ reportId,
1487
+ name: String(base.name || ''),
1488
+ workbook: wb,
1489
+ datasets,
1490
+ sheetConfig: JSON.stringify({ dataPreAgg: false, simpleList: false }),
1491
+ });
1492
+ console.log('已关闭预聚合。');
1493
+ });
1494
+ });
1495
+ // ── 数据集名去重后缀 ────────────────────────────────────
1496
+ fix
1497
+ .command('dataset-name')
1498
+ .argument('<reportId>', '报表 ID')
1499
+ .option('--app-id <id>', '应用 ID')
1500
+ .option('--apply', '真的落库')
1501
+ .description('去掉数据集名里重复拼的类型后缀(X(业务模块)(业务模块) → X(业务模块))')
1502
+ .action(async (reportId, raw) => {
1503
+ await withReport(program, raw.appId || '', async (http, json) => {
1504
+ const { appId } = await loadDev(http, reportId, raw.appId);
1505
+ const datasets = await listDatasets(http, reportId, appId);
1506
+ const plan = datasets
1507
+ .map((d) => {
1508
+ const fixed = String(d.name || '').replace(/(\([^()]+\))\1+$/, '$1');
1509
+ return { id: d.id, before: d.name, after: fixed };
1510
+ })
1511
+ .filter((p) => p.before !== p.after);
1512
+ if (json)
1513
+ console.log(JSON.stringify({ plan, applied: !!raw.apply }, null, 2));
1514
+ else {
1515
+ plan.forEach((p) => console.log(`${p.id} ${p.before} → ${p.after}`));
1516
+ if (!plan.length)
1517
+ console.log('没有重复后缀的数据集名。');
1518
+ }
1519
+ if (!raw.apply) {
1520
+ if (!json && plan.length)
1521
+ console.log('(预演,未落库。加 --apply 执行)');
1522
+ return;
1523
+ }
1524
+ for (const p of plan)
1525
+ await renameDataset(http, p.id, p.after);
1526
+ console.log(`已改名 ${plan.length} 个数据集。`);
1527
+ });
1528
+ });
1529
+ // ── 分支场景 A/B/C ──────────────────────────────────────
1530
+ const scene = report.command('scene').description('分支场景:同 sheet 多报表 / 自增序号 / 单元格跳转 Sheet');
1531
+ scene
1532
+ .command('rules')
1533
+ .description('打印三个场景的判定条件与强制配置')
1534
+ .action(() => console.log(rulesText()));
1535
+ scene
1536
+ .command('detect')
1537
+ .requiredOption('--req <file>', '需求文本文件(UTF-8)')
1538
+ .description('从需求文本识别属于哪个场景(只读,不落库)')
1539
+ .action((raw) => {
1540
+ const text = readFileSync(String(raw.req), 'utf8');
1541
+ const r = detectScenes(text);
1542
+ if (!r.scenes.length) {
1543
+ console.log('未识别到 A/B/C 场景。');
1544
+ return;
1545
+ }
1546
+ r.scenes.forEach((s) => console.log(`场景 ${s}:${r.evidence[s].join(';')}`));
1547
+ });
1548
+ const parseSeqCells = (v) => String(v || '')
1549
+ .split(';')
1550
+ .map((p) => p.trim())
1551
+ .filter(Boolean)
1552
+ .map((p) => {
1553
+ const [x, y] = p.split(',').map(Number);
1554
+ if (!Number.isFinite(x) || !Number.isFinite(y))
1555
+ throw new Error(`--seq-cells 需为 "x,y;x,y",收到「${p}」`);
1556
+ return [x, y];
1557
+ });
1558
+ const parseJumpCells = (v) => String(v || '')
1559
+ .split(';')
1560
+ .map((p) => p.trim())
1561
+ .filter(Boolean)
1562
+ .map((p) => {
1563
+ const [xy, n] = p.split('=');
1564
+ const [x, y] = String(xy).split(',').map(Number);
1565
+ const sheetNo = Number(n);
1566
+ if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(sheetNo)) {
1567
+ throw new Error(`--jump-cells 需为 "x,y=N"(N 为 sheet 序号,从 1 起),收到「${p}」`);
1568
+ }
1569
+ return [x, y, sheetNo];
1570
+ });
1571
+ scene
1572
+ .command('check')
1573
+ .argument('<reportId>', '报表 ID')
1574
+ .option('--app-id <id>', '应用 ID')
1575
+ .option('--scene <s>', '只查某一个场景(A/B/C);缺省全查')
1576
+ .option('--seq-cells <cells>', '序号格坐标 "x,y;x,y"(场景 B)')
1577
+ .option('--jump-cells <cells>', '跳转格 "x,y=N"(场景 C,N 为 sheet 序号)')
1578
+ .description('场景体检(只读):公共父格 / 序号格 / 跳转链接')
1579
+ .action(async (reportId, raw) => {
1580
+ await withReport(program, raw.appId || '', async (http, json) => {
1581
+ const { fields } = await loadDev(http, reportId, raw.appId);
1582
+ const scenes = raw.scene ? [String(raw.scene).toUpperCase()] : ['A', 'B', 'C'];
1583
+ const items = scenes.flatMap((s) => checkScene(fields, s, {
1584
+ reportId,
1585
+ seqCells: parseSeqCells(raw.seqCells),
1586
+ jumpCells: parseJumpCells(raw.jumpCells),
1587
+ }));
1588
+ const errors = items.filter((i) => i.level === 'error');
1589
+ if (json)
1590
+ console.log(JSON.stringify({ scenes, items }, null, 2));
1591
+ else {
1592
+ items.forEach((i) => console.log(`[${i.level === 'error' ? 'FAIL' : 'WARN'}] ${i.message}`));
1593
+ if (!errors.length)
1594
+ console.log('PASS — 已检查场景均无阻断问题');
1595
+ }
1596
+ if (errors.length)
1597
+ process.exitCode = 1;
1598
+ });
1599
+ });
1600
+ scene
1601
+ .command('apply')
1602
+ .argument('<reportId>', '报表 ID')
1603
+ .option('--app-id <id>', '应用 ID')
1604
+ .requiredOption('--scene <s>', '场景 A/B/C')
1605
+ .option('--at <x,y>', '场景 A 公共父格坐标(默认 0,0)')
1606
+ .option('--anchor <cells>', '场景 A 需要挂到公共父格的块锚点 "x,y;x,y"')
1607
+ .option('--seq-cells <cells>', '场景 B 序号格 "x,y;x,y"')
1608
+ .option('--jump-cells <cells>', '场景 C 跳转格 "x,y=N"')
1609
+ .option('--base <url>', '场景 C 基础地址(缺省走相对路径)')
1610
+ .option('--open-type <t>', '场景 C 打开方式:blank(默认)/ self / jsscript')
1611
+ .option('--apply', '真的落库')
1612
+ .description('按场景写入配置(默认只读预演)')
1613
+ .action(async (reportId, raw) => {
1614
+ await withReport(program, raw.appId || '', async (http, json) => {
1615
+ const s = String(raw.scene).toUpperCase();
1616
+ if (!['A', 'B', 'C'].includes(s))
1617
+ throw new Error('--scene 只能是 A / B / C');
1618
+ const { base, appId, fields } = await loadDev(http, reportId, raw.appId);
1619
+ const at = raw.at
1620
+ ? String(raw.at).split(',').map(Number)
1621
+ : [0, 0];
1622
+ const anchors = String(raw.anchor || '')
1623
+ .split(';')
1624
+ .map((p) => p.trim())
1625
+ .filter(Boolean)
1626
+ .map((p) => p.split(',').map(Number));
1627
+ const seqCells = parseSeqCells(raw.seqCells);
1628
+ const jumpCells = parseJumpCells(raw.jumpCells);
1629
+ if (s === 'B' && !seqCells.length)
1630
+ throw new Error('场景 B 需要 --seq-cells "x,y"');
1631
+ if (s === 'C' && !jumpCells.length)
1632
+ throw new Error('场景 C 需要 --jump-cells "x,y=N"');
1633
+ if (json) {
1634
+ console.log(JSON.stringify({ scene: s, at, anchors, seqCells, jumpCells, applied: !!raw.apply }, null, 2));
1635
+ }
1636
+ else {
1637
+ if (s === 'A') {
1638
+ console.log(`公共父格 (${at[0]},${at[1]});块锚点 ${anchors.length ? anchors.map((a) => `(${a[0]},${a[1]})`).join(' ') : '(未指定)'}`);
1639
+ }
1640
+ else if (s === 'B') {
1641
+ console.log(`序号格:${seqCells.map((c) => `(${c[0]},${c[1]})`).join(' ')}`);
1642
+ }
1643
+ else {
1644
+ jumpCells.forEach(([x, y, n]) => console.log(`(${x},${y}) → ${jumpHref(reportId, n, raw.base)}`));
1645
+ }
1646
+ }
1647
+ if (!raw.apply) {
1648
+ if (!json)
1649
+ console.log('(预演,未落库。加 --apply 执行)');
1650
+ return;
1651
+ }
1652
+ const wb = mergeWorkbookSafe({ fields }, {
1653
+ patchFn: (w) => {
1654
+ for (const sheet of Object.values(w.sheets || {})) {
1655
+ const dt = sheet?.data?.dataTable || {};
1656
+ if (s === 'A') {
1657
+ // 公共父格:坐标上没有就新建一个 Const
1658
+ if (!dt[String(at[0])])
1659
+ dt[String(at[0])] = {};
1660
+ const atCell = dt[String(at[0])][String(at[1])];
1661
+ if (atCell?.style?.cellType) {
1662
+ atCell.style.cellType.isActiveParent = true;
1663
+ atCell.style.cellType.leftX = -1;
1664
+ atCell.style.cellType.leftY = -1;
1665
+ }
1666
+ else {
1667
+ const created = constCell('统计区', undefined, { left: [-1, -1] });
1668
+ created.style.cellType.isActiveParent = true;
1669
+ dt[String(at[0])][String(at[1])] = created;
1670
+ }
1671
+ for (const [ax, ay] of anchors) {
1672
+ const ct = dt[String(ax)]?.[String(ay)]?.style?.cellType;
1673
+ if (!ct)
1674
+ continue;
1675
+ ct.leftX = at[0];
1676
+ ct.leftY = at[1];
1677
+ }
1678
+ }
1679
+ if (s === 'B') {
1680
+ for (const [x, y] of seqCells) {
1681
+ const ct = dt[String(x)]?.[String(y)]?.style?.cellType;
1682
+ if (!ct)
1683
+ continue;
1684
+ ct.typeName = 'SequenceNumberCellType';
1685
+ ct.cellType = 'SequenceNumber';
1686
+ ct.cellText = '{seq}';
1687
+ ct.expandType = 'Vertical';
1688
+ ct.displayType = 'Group';
1689
+ ct.groupType = null;
1690
+ }
1691
+ }
1692
+ if (s === 'C') {
1693
+ for (const [x, y, n] of jumpCells) {
1694
+ const ct = dt[String(x)]?.[String(y)]?.style?.cellType;
1695
+ if (!ct)
1696
+ continue;
1697
+ const href = jumpHref(reportId, n, raw.base);
1698
+ ct.pcHreflink = href;
1699
+ ct.h5Hreflink = href;
1700
+ ct.openType = String(raw.openType || 'blank');
1701
+ }
1702
+ }
1703
+ }
1704
+ },
1705
+ });
1706
+ const fresh = await getReportDevInfo(http, reportId);
1707
+ await saveReportDevInfo(http, {
1708
+ appId,
1709
+ reportId,
1710
+ name: String(base.name || ''),
1711
+ workbook: wb,
1712
+ designKey: String(fresh.designKey || ''),
1713
+ groupId: String(base.groupId ?? '0'),
1714
+ nameLabelId: String(base.nameLabelId ?? '-1'),
1715
+ });
1716
+ const after = ((await getReportDevInfo(http, reportId)).fields || []);
1717
+ const items = checkScene(after, s, { reportId, seqCells, jumpCells });
1718
+ const errors = items.filter((i) => i.level === 'error');
1719
+ errors.forEach((e) => console.log(`[FAIL] ${e.message}`));
1720
+ console.log(errors.length ? `⚠️ 回读仍有 ${errors.length} 处问题` : `场景 ${s} 已落库并通过回读校验`);
1721
+ if (errors.length)
1722
+ process.exitCode = 2;
1723
+ });
1724
+ });
1725
+ // ── 网格原样打印(只读) ────────────────────────────────
1726
+ const dump = report.command('dump').description('只读:原样打印单元格网格,看清坐标与空缺');
1727
+ dump
1728
+ .command('cells')
1729
+ .argument('<reportId>', '报表 ID')
1730
+ .option('--app-id <id>', '应用 ID')
1731
+ .description('按行打印每个格子的类型与内容,并标出空缺')
1732
+ .action(async (reportId, raw) => {
1733
+ await withReport(program, raw.appId || '', async (http, json) => {
1734
+ const { fields } = await loadDev(http, reportId, raw.appId);
1735
+ if (!fields.length) {
1736
+ console.log('报表没有任何单元格。');
1737
+ return;
1738
+ }
1739
+ const cellText = (f) => {
1740
+ const meta = cellMeta(f);
1741
+ if (f.cellType === 'Formula')
1742
+ return `=${meta.cellText || ''}`;
1743
+ return String(meta.cellText || f.text || '');
1744
+ };
1745
+ const maxX = Math.max(...fields.map((f) => Number(f.x)));
1746
+ const maxY = Math.max(...fields.map((f) => Number(f.y)));
1747
+ const at = new Map(fields.map((f) => [`${Number(f.x)},${Number(f.y)}`, f]));
1748
+ if (json) {
1749
+ console.log(JSON.stringify({
1750
+ rows: maxX + 1,
1751
+ cols: maxY + 1,
1752
+ cells: fields.map((f) => ({
1753
+ x: Number(f.x),
1754
+ y: Number(f.y),
1755
+ type: f.cellType,
1756
+ text: cellText(f),
1757
+ })),
1758
+ }, null, 2));
1759
+ return;
1760
+ }
1761
+ const gaps = [];
1762
+ for (let x = 0; x <= maxX; x++) {
1763
+ const parts = [];
1764
+ for (let y = 0; y <= maxY; y++) {
1765
+ const f = at.get(`${x},${y}`);
1766
+ if (!f) {
1767
+ parts.push(`${y}:·`);
1768
+ gaps.push(`(${x},${y})`);
1769
+ continue;
1770
+ }
1771
+ const kind = String(f.cellType || '').slice(0, 3);
1772
+ parts.push(`${y}:${kind}|${cellText(f)}`);
1773
+ }
1774
+ console.log(`行 ${x} ${parts.join(' ')}`);
1775
+ }
1776
+ console.log(`\n共 ${fields.length} 格,网格 ${maxX + 1} 行 × ${maxY + 1} 列`);
1777
+ if (gaps.length)
1778
+ console.log(`空缺 ${gaps.length} 处:${gaps.slice(0, 20).join(' ')}${gaps.length > 20 ? ' …' : ''}`);
1779
+ });
1780
+ });
718
1781
  }
719
1782
  //# sourceMappingURL=report-validate.js.map