weaver-e10-builder 1.6.10 → 1.6.12

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