@huicai/cli 2026.814.0 → 2026.1019.0

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 (485) hide show
  1. package/LICENSE +391 -32
  2. package/README.md +137 -8
  3. package/bin/huicai.js +146 -73
  4. package/package.json +27 -8
  5. package/skills/ab-testing/trigger-cases.json +30 -0
  6. package/skills/academic-writing/trigger-cases.json +30 -0
  7. package/skills/accounting-ops/trigger-cases.json +30 -0
  8. package/skills/ai-agent/trigger-cases.json +30 -0
  9. package/skills/ai-aiops/trigger-cases.json +30 -0
  10. package/skills/ai-coding/trigger-cases.json +30 -0
  11. package/skills/ai-compliance/trigger-cases.json +30 -0
  12. package/skills/ai-eval/trigger-cases.json +30 -0
  13. package/skills/ai-llm-app/trigger-cases.json +30 -0
  14. package/skills/ai-mlops/trigger-cases.json +30 -0
  15. package/skills/ai-multimodal/trigger-cases.json +30 -0
  16. package/skills/ai-prompt/trigger-cases.json +30 -0
  17. package/skills/ai-rag/trigger-cases.json +30 -0
  18. package/skills/anomaly-detection/trigger-cases.json +30 -0
  19. package/skills/api-design/trigger-cases.json +31 -0
  20. package/skills/api-integration/trigger-cases.json +31 -0
  21. package/skills/arch-design/trigger-cases.json +12 -0
  22. package/skills/arch-diagram/SKILL.md +118 -0
  23. package/skills/arch-diagram/trigger-cases.json +30 -0
  24. package/skills/arch-distributed/trigger-cases.json +30 -0
  25. package/skills/arch-multitenant/trigger-cases.json +30 -0
  26. package/skills/audio-brief/SKILL.md +65 -0
  27. package/skills/audio-brief/trigger-cases.json +30 -0
  28. package/skills/audit-assist/trigger-cases.json +30 -0
  29. package/skills/audit-chain/trigger-cases.json +31 -0
  30. package/skills/banking-credit/trigger-cases.json +30 -0
  31. package/skills/brand-design/trigger-cases.json +30 -0
  32. package/skills/business-plan/trigger-cases.json +30 -0
  33. package/skills/capital-ops/trigger-cases.json +30 -0
  34. package/skills/cicd-pipeline/trigger-cases.json +31 -0
  35. package/skills/code-archaeology/SKILL.md +55 -0
  36. package/skills/code-archaeology/trigger-cases.json +9 -0
  37. package/skills/code-quality/SKILL.md +138 -0
  38. package/skills/code-quality/references/languages.md +107 -0
  39. package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
  40. package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
  41. package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
  42. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
  43. package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
  44. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
  45. package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
  46. package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
  47. package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
  48. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
  49. package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
  50. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
  51. package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
  52. package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
  53. package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
  54. package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
  55. package/skills/code-quality/scripts/code_battery.py +1072 -0
  56. package/skills/code-quality/scripts/code_check.py +1269 -0
  57. package/skills/code-quality/scripts/code_csharp.py +371 -0
  58. package/skills/code-quality/scripts/code_fix.py +201 -0
  59. package/skills/code-quality/scripts/code_java.py +695 -0
  60. package/skills/code-quality/scripts/code_jsts.py +313 -0
  61. package/skills/code-quality/scripts/code_lens.py +1079 -0
  62. package/skills/code-quality/scripts/code_python.py +536 -0
  63. package/skills/code-quality/scripts/code_testplan.py +239 -0
  64. package/skills/code-quality/trigger-cases.json +45 -0
  65. package/skills/code-review/SKILL.md +24 -0
  66. package/skills/code-review/trigger-cases.json +35 -0
  67. package/skills/commit-and-pr/trigger-cases.json +31 -0
  68. package/skills/content-creation/trigger-cases.json +30 -0
  69. package/skills/corporate-governance/trigger-cases.json +30 -0
  70. package/skills/crawl-chain/trigger-cases.json +31 -0
  71. package/skills/customer-service/trigger-cases.json +30 -0
  72. package/skills/data-analysis/trigger-cases.json +30 -0
  73. package/skills/data-chain/SKILL.md +1 -1
  74. package/skills/data-chain/trigger-cases.json +31 -0
  75. package/skills/data-governance/trigger-cases.json +30 -0
  76. package/skills/data-report-pro/trigger-cases.json +30 -0
  77. package/skills/data-streaming/trigger-cases.json +30 -0
  78. package/skills/data-wrangling/SKILL.md +1 -1
  79. package/skills/data-wrangling/trigger-cases.json +30 -0
  80. package/skills/db-es/trigger-cases.json +30 -0
  81. package/skills/db-migration/trigger-cases.json +30 -0
  82. package/skills/db-mysql/trigger-cases.json +30 -0
  83. package/skills/db-postgres/trigger-cases.json +30 -0
  84. package/skills/db-redis/trigger-cases.json +30 -0
  85. package/skills/debug-root-cause/trigger-cases.json +30 -0
  86. package/skills/dependency-audit/SKILL.md +56 -0
  87. package/skills/dependency-audit/trigger-cases.json +7 -0
  88. package/skills/dependency-upgrade/trigger-cases.json +31 -0
  89. package/skills/dev-cpp/trigger-cases.json +30 -0
  90. package/skills/dev-cross-platform/trigger-cases.json +30 -0
  91. package/skills/dev-csharp/SKILL.md +23 -0
  92. package/skills/dev-csharp/trigger-cases.json +30 -0
  93. package/skills/dev-frontend/SKILL.md +19 -0
  94. package/skills/dev-frontend/trigger-cases.json +30 -0
  95. package/skills/dev-go/trigger-cases.json +30 -0
  96. package/skills/dev-harmony/trigger-cases.json +30 -0
  97. package/skills/dev-java/SKILL.md +20 -0
  98. package/skills/dev-java/trigger-cases.json +30 -0
  99. package/skills/dev-mobile/trigger-cases.json +30 -0
  100. package/skills/dev-node/trigger-cases.json +30 -0
  101. package/skills/dev-php/trigger-cases.json +30 -0
  102. package/skills/dev-python/SKILL.md +21 -0
  103. package/skills/dev-python/trigger-cases.json +30 -0
  104. package/skills/dev-rust/trigger-cases.json +30 -0
  105. package/skills/dev-toolchain/trigger-cases.json +31 -0
  106. package/skills/doc-chain/trigger-cases.json +31 -0
  107. package/skills/doc-compare/trigger-cases.json +30 -0
  108. package/skills/docx-author/SKILL.md +127 -25
  109. package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
  110. package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
  111. package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
  112. package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
  113. package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
  114. package/skills/docx-author/scripts/docx_battery.py +586 -0
  115. package/skills/docx-author/scripts/docx_build.py +995 -0
  116. package/skills/docx-author/scripts/docx_check.py +1306 -0
  117. package/skills/docx-author/scripts/docx_doc.py +14 -154
  118. package/skills/docx-author/scripts/docx_layout.py +490 -0
  119. package/skills/docx-author/scripts/zh_prose.py +639 -0
  120. package/skills/docx-author/trigger-cases.json +58 -0
  121. package/skills/echo-brief/SKILL.md +84 -0
  122. package/skills/echo-brief/references/echo-format.md +68 -0
  123. package/skills/echo-brief/trigger-cases.json +40 -0
  124. package/skills/ecommerce-ops/trigger-cases.json +30 -0
  125. package/skills/education-training/trigger-cases.json +30 -0
  126. package/skills/ehs-safety/trigger-cases.json +30 -0
  127. package/skills/email-writing/trigger-cases.json +30 -0
  128. package/skills/eng-ai/trigger-cases.json +30 -0
  129. package/skills/eng-architecture/trigger-cases.json +31 -0
  130. package/skills/eng-automation/trigger-cases.json +31 -0
  131. package/skills/eng-collaboration/trigger-cases.json +30 -0
  132. package/skills/eng-complex-env/trigger-cases.json +30 -0
  133. package/skills/eng-data-platform/trigger-cases.json +31 -0
  134. package/skills/eng-database/trigger-cases.json +31 -0
  135. package/skills/eng-develop/trigger-cases.json +30 -0
  136. package/skills/eng-docs/SKILL.md +24 -0
  137. package/skills/eng-docs/trigger-cases.json +30 -0
  138. package/skills/eng-efficiency/trigger-cases.json +30 -0
  139. package/skills/eng-infra/trigger-cases.json +30 -0
  140. package/skills/eng-integration/trigger-cases.json +31 -0
  141. package/skills/eng-legacy/trigger-cases.json +31 -0
  142. package/skills/eng-observability/trigger-cases.json +31 -0
  143. package/skills/eng-ops/trigger-cases.json +31 -0
  144. package/skills/eng-performance/trigger-cases.json +31 -0
  145. package/skills/eng-planning/trigger-cases.json +31 -0
  146. package/skills/eng-quality/SKILL.md +8 -0
  147. package/skills/eng-quality/trigger-cases.json +31 -0
  148. package/skills/eng-release/trigger-cases.json +31 -0
  149. package/skills/eng-reliability/trigger-cases.json +31 -0
  150. package/skills/eng-requirements/trigger-cases.json +12 -0
  151. package/skills/eng-research/trigger-cases.json +30 -0
  152. package/skills/eng-security/trigger-cases.json +31 -0
  153. package/skills/eng-ux/trigger-cases.json +30 -0
  154. package/skills/engineering-chain/trigger-cases.json +30 -0
  155. package/skills/env-offline/trigger-cases.json +31 -0
  156. package/skills/env-private-deploy/trigger-cases.json +31 -0
  157. package/skills/env-xinchuang/trigger-cases.json +31 -0
  158. package/skills/equipment-tpm/trigger-cases.json +30 -0
  159. package/skills/esg-report/trigger-cases.json +30 -0
  160. package/skills/event-management/trigger-cases.json +30 -0
  161. package/skills/excel-analysis-chain/SKILL.md +17 -4
  162. package/skills/excel-analysis-chain/trigger-cases.json +31 -0
  163. package/skills/explain-codebase/trigger-cases.json +11 -0
  164. package/skills/file-ingest/trigger-cases.json +30 -0
  165. package/skills/finance-analysis/trigger-cases.json +31 -0
  166. package/skills/finance-chain/trigger-cases.json +31 -0
  167. package/skills/financial-modeling/trigger-cases.json +31 -0
  168. package/skills/find-simplifications/SKILL.md +43 -0
  169. package/skills/find-simplifications/trigger-cases.json +23 -0
  170. package/skills/foreign-trade/trigger-cases.json +30 -0
  171. package/skills/funnel-cohort/trigger-cases.json +30 -0
  172. package/skills/gov-document/trigger-cases.json +30 -0
  173. package/skills/gui-change-evidence/SKILL.md +47 -0
  174. package/skills/gui-change-evidence/trigger-cases.json +23 -0
  175. package/skills/hr-chain/trigger-cases.json +31 -0
  176. package/skills/hr-operations/trigger-cases.json +30 -0
  177. package/skills/info-disclosure/trigger-cases.json +31 -0
  178. package/skills/infra-gateway/trigger-cases.json +31 -0
  179. package/skills/infra-k8s/trigger-cases.json +31 -0
  180. package/skills/infra-mq/trigger-cases.json +30 -0
  181. package/skills/insurance-advisory/trigger-cases.json +30 -0
  182. package/skills/internal-audit/trigger-cases.json +30 -0
  183. package/skills/ip-management/trigger-cases.json +31 -0
  184. package/skills/ipo-listing/trigger-cases.json +31 -0
  185. package/skills/it-audit/trigger-cases.json +31 -0
  186. package/skills/itsm-ops/trigger-cases.json +31 -0
  187. package/skills/knowledge-sop/trigger-cases.json +31 -0
  188. package/skills/legal-chain/trigger-cases.json +31 -0
  189. package/skills/legal-contract/trigger-cases.json +30 -0
  190. package/skills/litigation-support/trigger-cases.json +31 -0
  191. package/skills/logistics-warehouse/trigger-cases.json +30 -0
  192. package/skills/long-doc-analysis/trigger-cases.json +31 -0
  193. package/skills/management-accounting/trigger-cases.json +30 -0
  194. package/skills/manufacturing-chain/trigger-cases.json +31 -0
  195. package/skills/manufacturing-quality/trigger-cases.json +30 -0
  196. package/skills/marketing-analytics/trigger-cases.json +31 -0
  197. package/skills/marketing-chain/trigger-cases.json +31 -0
  198. package/skills/meeting-minutes/trigger-cases.json +30 -0
  199. package/skills/metrics-system/trigger-cases.json +31 -0
  200. package/skills/negotiation-prep/trigger-cases.json +30 -0
  201. package/skills/obs-prometheus/trigger-cases.json +31 -0
  202. package/skills/office-automation/trigger-cases.json +31 -0
  203. package/skills/okr-review/trigger-cases.json +30 -0
  204. package/skills/ops-fleet/trigger-cases.json +31 -0
  205. package/skills/ops-linux/trigger-cases.json +31 -0
  206. package/skills/optimization-solver/trigger-cases.json +30 -0
  207. package/skills/patent-disclosure/SKILL.md +105 -0
  208. package/skills/patent-disclosure/references/figures.md +110 -0
  209. package/skills/patent-disclosure/references/outline.md +162 -0
  210. package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
  211. package/skills/patent-disclosure/trigger-cases.json +31 -0
  212. package/skills/patent-drafting/SKILL.md +108 -0
  213. package/skills/patent-drafting/trigger-cases.json +31 -0
  214. package/skills/patent-portfolio/SKILL.md +81 -0
  215. package/skills/patent-portfolio/trigger-cases.json +31 -0
  216. package/skills/pdf-toolkit/trigger-cases.json +31 -0
  217. package/skills/perf-optimize/trigger-cases.json +31 -0
  218. package/skills/pivot-crosstab/trigger-cases.json +30 -0
  219. package/skills/policy-analysis/trigger-cases.json +31 -0
  220. package/skills/portfolio-analysis/trigger-cases.json +31 -0
  221. package/skills/ppt-chain/SKILL.md +14 -6
  222. package/skills/ppt-chain/trigger-cases.json +31 -0
  223. package/skills/pptx-author/SKILL.md +86 -33
  224. package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
  225. package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
  226. package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
  227. package/skills/pptx-author/scripts/pptx_battery.py +178 -0
  228. package/skills/pptx-author/scripts/pptx_build.py +119 -0
  229. package/skills/pptx-author/scripts/pptx_check.py +1182 -0
  230. package/skills/pptx-author/scripts/pptx_deck.py +736 -145
  231. package/skills/pptx-author/scripts/pptx_layout.py +384 -0
  232. package/skills/pptx-author/trigger-cases.json +15 -0
  233. package/skills/pr-comms/trigger-cases.json +30 -0
  234. package/skills/procurement-supply/trigger-cases.json +31 -0
  235. package/skills/product-design/trigger-cases.json +31 -0
  236. package/skills/production-planning/trigger-cases.json +31 -0
  237. package/skills/project-management/trigger-cases.json +31 -0
  238. package/skills/project-proposal/trigger-cases.json +31 -0
  239. package/skills/prose-standard/SKILL.md +53 -0
  240. package/skills/prose-standard/trigger-cases.json +23 -0
  241. package/skills/qa-api-automation/trigger-cases.json +31 -0
  242. package/skills/qa-mobile-automation/trigger-cases.json +31 -0
  243. package/skills/qa-ui-automation/trigger-cases.json +31 -0
  244. package/skills/quant-analysis/trigger-cases.json +31 -0
  245. package/skills/rag-triage/SKILL.md +64 -0
  246. package/skills/rag-triage/evals.json +57 -0
  247. package/skills/rag-triage/references/triage-playbook.md +75 -0
  248. package/skills/rag-triage/trigger-cases.json +31 -0
  249. package/skills/real-estate-property/trigger-cases.json +30 -0
  250. package/skills/refactor-safely/SKILL.md +1 -1
  251. package/skills/refactor-safely/trigger-cases.json +30 -0
  252. package/skills/regression-forecast/trigger-cases.json +31 -0
  253. package/skills/release-checklist/trigger-cases.json +31 -0
  254. package/skills/report-craft/SKILL.md +76 -0
  255. package/skills/report-craft/references/feasibility.md +118 -0
  256. package/skills/report-craft/references/review-checklist.md +51 -0
  257. package/skills/report-craft/references/visuals.md +114 -0
  258. package/skills/report-craft/references/writing.md +80 -0
  259. package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
  260. package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
  261. package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
  262. package/skills/report-craft/scripts/chart_render.py +1222 -0
  263. package/skills/report-craft/scripts/fin_model.py +602 -0
  264. package/skills/report-craft/scripts/project_facts.py +580 -0
  265. package/skills/report-craft/scripts/report_battery.py +349 -0
  266. package/skills/report-craft/trigger-cases.json +15 -0
  267. package/skills/retail-store-ops/trigger-cases.json +30 -0
  268. package/skills/risk-compliance/trigger-cases.json +31 -0
  269. package/skills/scope-creep/SKILL.md +45 -0
  270. package/skills/scope-creep/trigger-cases.json +7 -0
  271. package/skills/sec-auth/trigger-cases.json +31 -0
  272. package/skills/sec-compliance/trigger-cases.json +31 -0
  273. package/skills/securities-chain/trigger-cases.json +31 -0
  274. package/skills/securities-research/trigger-cases.json +31 -0
  275. package/skills/security-audit/trigger-cases.json +31 -0
  276. package/skills/slides-html/SKILL.md +25 -10
  277. package/skills/slides-html/trigger-cases.json +11 -0
  278. package/skills/sql-analysis/SKILL.md +110 -0
  279. package/skills/sql-analysis/trigger-cases.json +34 -0
  280. package/skills/sql-tuning/trigger-cases.json +31 -0
  281. package/skills/statistical-testing/trigger-cases.json +31 -0
  282. package/skills/strategy-planning/trigger-cases.json +30 -0
  283. package/skills/supply-chain/trigger-cases.json +31 -0
  284. package/skills/tax-compliance/trigger-cases.json +30 -0
  285. package/skills/tech-doc/SKILL.md +262 -0
  286. package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
  287. package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
  288. package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
  289. package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
  290. package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
  291. package/skills/tech-doc/scripts/archive_battery.py +860 -0
  292. package/skills/tech-doc/scripts/archive_build.py +599 -0
  293. package/skills/tech-doc/scripts/archive_check.py +1307 -0
  294. package/skills/tech-doc/scripts/archive_spec.py +499 -0
  295. package/skills/tech-doc/scripts/doc_templates.py +624 -0
  296. package/skills/tech-doc/trigger-cases.json +57 -0
  297. package/skills/tech-research/trigger-cases.json +31 -0
  298. package/skills/tender-bidding/trigger-cases.json +31 -0
  299. package/skills/translation-pro/trigger-cases.json +30 -0
  300. package/skills/treasury-mgmt/trigger-cases.json +31 -0
  301. package/skills/trim-cot-leakage/SKILL.md +58 -0
  302. package/skills/trim-cot-leakage/trigger-cases.json +23 -0
  303. package/skills/web-craft/SKILL.md +103 -0
  304. package/skills/web-craft/references/craft-checklist.md +75 -0
  305. package/skills/web-craft/references/design-direction.md +122 -0
  306. package/skills/web-craft/references/h5-mobile.md +100 -0
  307. package/skills/web-craft/references/motion.md +136 -0
  308. package/skills/web-craft/references/recipes.md +74 -0
  309. package/skills/web-craft/templates/wedding-invitation.html +1180 -0
  310. package/skills/web-craft/trigger-cases.json +14 -0
  311. package/skills/web-scraping/trigger-cases.json +34 -0
  312. package/skills/web-search/SKILL.md +11 -10
  313. package/skills/web-search/trigger-cases.json +31 -0
  314. package/skills/weekly-report/SKILL.md +134 -0
  315. package/skills/weekly-report/trigger-cases.json +11 -0
  316. package/skills/write-tests/trigger-cases.json +31 -0
  317. package/skills/xlsx-analysis/SKILL.md +143 -77
  318. package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
  319. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
  320. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
  321. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
  322. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
  323. package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
  324. package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
  325. package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
  326. package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
  327. package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
  328. package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
  329. package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
  330. package/skills/xlsx-analysis/trigger-cases.json +59 -0
  331. package/squads/hc-defense.md +47 -0
  332. package/squads/hc-rdarchive.md +110 -0
  333. package/squads/hc-redteam.md +50 -0
  334. package/squads/hc-weekly.md +125 -0
  335. package/src/account.js +1 -1
  336. package/src/acp.js +1 -0
  337. package/src/advisor.js +1 -1
  338. package/src/agent.js +1 -1
  339. package/src/api.js +1 -1
  340. package/src/archaeology.js +1 -0
  341. package/src/ask-ui.js +1 -0
  342. package/src/assets-import.js +1 -1
  343. package/src/autofix.js +1 -1
  344. package/src/automation-tools.js +1 -1
  345. package/src/automation.js +1 -1
  346. package/src/autorun.js +1 -1
  347. package/src/banner.js +1 -1
  348. package/src/capability-probe.js +1 -0
  349. package/src/checkpoints.js +1 -1
  350. package/src/code-tools.js +1 -0
  351. package/src/codeindex.js +1 -1
  352. package/src/codemode.js +1 -0
  353. package/src/command-bridge.js +1 -0
  354. package/src/command-ui.js +1 -0
  355. package/src/commands.js +1 -1
  356. package/src/compaction.js +1 -1
  357. package/src/computer.js +1 -1
  358. package/src/config.js +1 -1
  359. package/src/consult.js +1 -1
  360. package/src/context-report.js +1 -1
  361. package/src/context-window.js +1 -0
  362. package/src/context.js +1 -1
  363. package/src/crew.js +1 -0
  364. package/src/crush.js +1 -0
  365. package/src/daemon.js +1 -1
  366. package/src/decisions.js +1 -1
  367. package/src/depaudit.js +1 -0
  368. package/src/device.js +1 -1
  369. package/src/diagram-draw.js +1 -0
  370. package/src/diagram-layouts.js +1 -0
  371. package/src/diagram-raster.js +1 -0
  372. package/src/diagram.js +1 -0
  373. package/src/dictation.js +1 -0
  374. package/src/diffview.js +1 -1
  375. package/src/doctor.js +1 -1
  376. package/src/docx-tools.js +1 -0
  377. package/src/errkind.js +1 -1
  378. package/src/evidence.js +1 -0
  379. package/src/executors.js +1 -1
  380. package/src/expert-catalog.js +1 -1
  381. package/src/experts-store.js +1 -1
  382. package/src/experts.js +1 -1
  383. package/src/external-agents.js +1 -0
  384. package/src/forks.js +1 -1
  385. package/src/fs-observe.js +1 -0
  386. package/src/gitflow.js +1 -1
  387. package/src/goalround.js +1 -0
  388. package/src/hooks.js +1 -1
  389. package/src/input.js +1 -1
  390. package/src/insights.js +1 -1
  391. package/src/jobs.js +1 -0
  392. package/src/kbsync.js +1 -1
  393. package/src/lsp.js +1 -0
  394. package/src/mcp-apps.js +1 -0
  395. package/src/mcp.js +1 -1
  396. package/src/memory.js +1 -1
  397. package/src/notify.js +1 -1
  398. package/src/permissions.js +1 -1
  399. package/src/plan-limits.js +1 -0
  400. package/src/policy.js +1 -0
  401. package/src/pptx-tools.js +1 -0
  402. package/src/presence.js +1 -1
  403. package/src/privatedir.js +1 -0
  404. package/src/pyruntime.js +1 -1
  405. package/src/radar.js +1 -0
  406. package/src/render.js +1 -1
  407. package/src/repl.js +1 -1
  408. package/src/report-tools.js +1 -0
  409. package/src/routing.js +1 -0
  410. package/src/sandbox-probe.js +1 -0
  411. package/src/sandbox-win-launcher.mjs +156 -0
  412. package/src/sandbox.js +1 -0
  413. package/src/scan.js +1 -1
  414. package/src/schedule.js +1 -1
  415. package/src/scopecreep.js +1 -0
  416. package/src/sdk.js +1 -0
  417. package/src/selection.js +1 -0
  418. package/src/session-events.js +1 -0
  419. package/src/session.js +1 -1
  420. package/src/sessionindex.js +1 -0
  421. package/src/sessionlog.js +1 -0
  422. package/src/skill-catalog.js +1 -0
  423. package/src/skill-lint.js +1 -0
  424. package/src/skill-optimize.js +1 -0
  425. package/src/skill-scan.js +1 -0
  426. package/src/skills.js +1 -1
  427. package/src/spill.js +1 -0
  428. package/src/squad.js +1 -1
  429. package/src/subagent.js +1 -1
  430. package/src/surface-state.js +1 -0
  431. package/src/tapd.js +1 -0
  432. package/src/terminal-select.js +1 -0
  433. package/src/terminal.js +1 -0
  434. package/src/test-runner.js +1 -0
  435. package/src/todo.js +1 -1
  436. package/src/toolpipe.js +1 -0
  437. package/src/tools.js +1 -1
  438. package/src/toon.js +1 -0
  439. package/src/transcript.js +1 -1
  440. package/src/tui/app.js +1 -1
  441. package/src/tui/command-menu.js +1 -0
  442. package/src/tui/progress.js +1 -0
  443. package/src/tui/scroll.js +1 -0
  444. package/src/tui/welcome.js +1 -0
  445. package/src/tui/workbench.js +1 -0
  446. package/src/ui-spec.js +1 -0
  447. package/src/uitree.js +1 -0
  448. package/src/uninstall.js +1 -1
  449. package/src/visual.js +1 -1
  450. package/src/voice-form.js +1 -0
  451. package/src/voice.js +1 -0
  452. package/src/watch.js +1 -1
  453. package/src/web-audit.js +1 -0
  454. package/src/web-cdp.js +1 -0
  455. package/src/web-inspect.js +1 -0
  456. package/src/websearch.js +1 -0
  457. package/src/weekly.js +1 -0
  458. package/src/workflow.js +1 -0
  459. package/src/worklog.js +1 -0
  460. package/src/xlsx-tools.js +1 -0
  461. package/ui/command-picker.js +79 -0
  462. package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
  463. package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
  464. package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
  465. package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
  466. package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
  467. package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
  468. package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
  469. package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
  470. package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
  471. package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
  472. package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
  473. package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
  474. package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
  475. package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
  476. package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
  477. package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
  478. package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
  479. package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
  480. package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
  481. package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
  482. package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
  483. package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
  484. package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
  485. package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
@@ -0,0 +1,522 @@
1
+ # Excel 报表建表引擎:结构化 spec → 成品 xlsx(列宽先量后画、合计走公式、打印一页宽)。
2
+ #
3
+ # 与 docx_build / pptx_deck 同一条纪律:**模型只给「哪张表放什么」,尺寸与格式由引擎算**。
4
+ # 人手写 openpyxl 会犯的五类错,这里从写法上就不可能发生:
5
+ # · 合计行写成 Python 算好的常量 → 改一行明细,合计还是旧数,而且**没有任何报错**;
6
+ # 引擎一律写 SUBTOTAL/SUM 公式,改明细自动更新(这是 Excel 交付件最经典的事故);
7
+ # · 列宽拍脑袋 → 数字列不够宽就显示成 ####,打印出来一片井号;引擎按内容量(与体检同一把尺);
8
+ # · 订单号/身份证不设文本格式 → 前导零消失、18 位变科学计数法;引擎按列名判并强制 '@';
9
+ # · 不设打印区域 → 一张宽表打成 6 页、翻页看不见表头;引擎默认缩放到一页宽 + 重复标题行;
10
+ # · 分组小计与总计都用 SUM → 总计把小计又加了一遍,数字翻倍;引擎一律 SUBTOTAL(9,…),
11
+ # 它会自动忽略嵌套的 SUBTOTAL,这正是这个函数存在的理由。
12
+ #
13
+ # 落盘后自动跑一遍 xlsx_check 体检,报告随返回值给出。
14
+
15
+ import datetime
16
+ import json
17
+ import os
18
+ import re
19
+ import sys
20
+
21
+ from openpyxl import Workbook
22
+ from openpyxl.chart import BarChart, LineChart, PieChart, Reference
23
+ from openpyxl.formatting.rule import CellIsRule, ColorScaleRule, DataBarRule
24
+ from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
25
+ from openpyxl.utils import get_column_letter
26
+ from openpyxl.worksheet.datavalidation import DataValidation
27
+ from openpyxl.worksheet.properties import PageSetupProperties
28
+
29
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
30
+ import xlsx_layout as L # noqa: E402
31
+
32
+ ALIGN = {'left': 'left', 'center': 'center', 'right': 'right'}
33
+
34
+
35
+ def _side(color):
36
+ return Side(style='thin', color=color)
37
+
38
+
39
+ class SheetBuilder:
40
+ """单张工作表建表。行号一律由引擎推,调用方不需要(也不应该)自己数行。"""
41
+
42
+ def __init__(self, ws, prof, spec):
43
+ self.ws, self.prof, self.spec = ws, prof, spec
44
+ self.c = prof['colors']
45
+ self.warnings = []
46
+ self.cols = self._norm_columns()
47
+ self.rows = self._norm_rows()
48
+ self.header_row = 1
49
+ self.first_data = 2
50
+ self.last_data = 1
51
+ self.total_row = None
52
+ self.group_rows = []
53
+
54
+ # ── 规范化输入 ────────────────────────────────────────────────────────
55
+ def _norm_columns(self):
56
+ cols = self.spec.get('columns')
57
+ recs = self.spec.get('records')
58
+ if not cols and recs:
59
+ keys = []
60
+ for r in recs:
61
+ for k in r:
62
+ if k not in keys:
63
+ keys.append(k)
64
+ cols = [{'key': k, 'header': k} for k in keys]
65
+ if not cols and self.spec.get('rows'):
66
+ head = self.spec['rows'][0]
67
+ cols = [{'key': i, 'header': str(h)} for i, h in enumerate(head)]
68
+ self.spec = {**self.spec, 'rows': self.spec['rows'][1:]}
69
+ out = []
70
+ for i, c in enumerate(cols or []):
71
+ c = dict(c) if isinstance(c, dict) else {'key': i, 'header': str(c)}
72
+ c.setdefault('key', i)
73
+ c.setdefault('header', str(c['key']))
74
+ out.append(c)
75
+ return out
76
+
77
+ def _norm_rows(self):
78
+ if self.spec.get('records') is not None:
79
+ keys = [c['key'] for c in self.cols]
80
+ return [[r.get(k) for k in keys] for r in self.spec['records']]
81
+ return [list(r) for r in (self.spec.get('rows') or [])]
82
+
83
+ @staticmethod
84
+ def _coerce(v, fmt):
85
+ """按列格式把值写成**正确的类型**。
86
+
87
+ 引擎不能一边给列标上 yyyy-mm-dd,一边往里写字符串——那样 Excel 既算不了工期,
88
+ 排序也按字典序走,而且下游数据体检会如实报「日期被存成文本」。
89
+ 「先量后画」在类型上的对应物就是这一句。"""
90
+ if not isinstance(v, str) or not v.strip():
91
+ return v
92
+ t = v.strip()
93
+ if 'yyyy' in str(fmt):
94
+ m = re.match(r'^(\d{4})\s*[-/.年]\s*(\d{1,2})(?:\s*[-/.月]\s*(\d{1,2})日?)?$', t)
95
+ if m:
96
+ y, mo, d = int(m.group(1)), int(m.group(2)), int(m.group(3) or 1)
97
+ if 1900 <= y <= 2200 and 1 <= mo <= 12 and 1 <= d <= 31:
98
+ try:
99
+ return datetime.date(y, mo, d)
100
+ except ValueError:
101
+ return v
102
+ return v
103
+ if fmt in (L.FORMATS['text'], 'General', None):
104
+ return v
105
+ if re.fullmatch(r'[-+]?[\d,]+(\.\d+)?', t):
106
+ try:
107
+ return float(t.replace(',', '')) if '.' in t else int(t.replace(',', ''))
108
+ except ValueError:
109
+ return v
110
+ return v
111
+
112
+ def _fmt(self, col, j):
113
+ if col.get('format'):
114
+ return L.FORMATS.get(col['format'], col['format'])
115
+ vals = [r[j] for r in self.rows[:200] if j < len(r)]
116
+ f = L.guess_format(col['header'], vals)
117
+ if f == L.FORMATS['amount'] and self.prof.get('default_amount'):
118
+ return L.FORMATS[self.prof['default_amount']]
119
+ return f
120
+
121
+ # ── 建表 ──────────────────────────────────────────────────────────────
122
+ def build(self):
123
+ ws, p, c = self.ws, self.prof, self.c
124
+ ncol = max(1, len(self.cols))
125
+ r = 1
126
+ if self.spec.get('title'):
127
+ ws.cell(r, 1, self.spec['title'])
128
+ cell = ws.cell(r, 1)
129
+ cell.font = Font(name=p['font_west'], size=p['title_size'], bold=True, color=c['title_fg'])
130
+ cell.alignment = Alignment(horizontal='left', vertical='center')
131
+ self._ea(cell)
132
+ if ncol > 1:
133
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=ncol)
134
+ ws.row_dimensions[r].height = p['title_size'] * 2
135
+ r += 1
136
+ if self.spec.get('note'):
137
+ ws.cell(r, 1, self.spec['note'])
138
+ cell = ws.cell(r, 1)
139
+ cell.font = Font(name=p['font_west'], size=p['size'] - 1, italic=False, color='808080')
140
+ self._ea(cell)
141
+ if ncol > 1:
142
+ ws.merge_cells(start_row=r, start_column=1, end_row=r, end_column=ncol)
143
+ r += 1
144
+ if self.spec.get('title') or self.spec.get('note'):
145
+ r += 0
146
+ self.header_row = r
147
+ self._write_header(r)
148
+ r += 1
149
+ self.first_data = r
150
+ formats = [self._fmt(col, j) for j, col in enumerate(self.cols)]
151
+ group_by = self.spec.get('group_by')
152
+ if group_by is not None:
153
+ r = self._write_grouped(r, formats, group_by)
154
+ else:
155
+ r = self._write_rows(r, self.rows, formats)
156
+ self.last_data = r - 1
157
+ if self.spec.get('total') and self.rows:
158
+ self._write_total(r, formats)
159
+ self.total_row = r
160
+ r += 1
161
+ self._widths(formats)
162
+ self._finish(r)
163
+ return self
164
+
165
+ def _ea(self, cell):
166
+ """中文字体:openpyxl 只有一个 font.name,中文得靠字体本身支持——用「等线」这类中西文兼备的。"""
167
+ cell.font = Font(name=self.prof['font'], size=cell.font.size, bold=cell.font.bold,
168
+ color=cell.font.color, italic=cell.font.italic)
169
+
170
+ def _write_header(self, r):
171
+ p, c = self.prof, self.c
172
+ fill = PatternFill('solid', fgColor=c['head_bg'])
173
+ for j, col in enumerate(self.cols, start=1):
174
+ cell = self.ws.cell(r, j, col['header'])
175
+ cell.font = Font(name=p['font'], size=p['head_size'], bold=p['head_bold'], color=c['head_fg'])
176
+ cell.fill = fill
177
+ cell.alignment = Alignment(horizontal='center', vertical='center', wrap_text=True)
178
+ if p['borders']:
179
+ cell.border = Border(*(_side(c['border']),) * 4)
180
+ self.ws.row_dimensions[r].height = p['head_height']
181
+
182
+ def _write_rows(self, r, rows, formats, indent_first=False):
183
+ p, c = self.prof, self.c
184
+ stripe = PatternFill('solid', fgColor=c['stripe']) if p['stripe'] else None
185
+ border = Border(*(_side(c['border']),) * 4) if p['borders'] else None
186
+ for i, row in enumerate(rows):
187
+ for j, col in enumerate(self.cols, start=1):
188
+ v = row[j - 1] if j - 1 < len(row) else None
189
+ v = self._coerce(v, formats[j - 1])
190
+ cell = self.ws.cell(r, j, v)
191
+ cell.font = Font(name=p['font'], size=p['size'])
192
+ cell.number_format = formats[j - 1]
193
+ cell.alignment = Alignment(
194
+ horizontal=col.get('align') or self._auto_align(formats[j - 1]),
195
+ vertical='center', wrap_text=bool(col.get('wrap')))
196
+ if border:
197
+ cell.border = border
198
+ if stripe and i % 2 == 1:
199
+ cell.fill = stripe
200
+ self.ws.row_dimensions[r].height = p['row_height']
201
+ r += 1
202
+ return r
203
+
204
+ @staticmethod
205
+ def _auto_align(fmt):
206
+ if fmt == L.FORMATS['text'] or fmt == 'General':
207
+ return 'left'
208
+ if 'yyyy' in str(fmt):
209
+ return 'center'
210
+ return 'right'
211
+
212
+ def _write_grouped(self, r, formats, group_by):
213
+ """分组小计。小计与总计都用 SUBTOTAL(9,…)——它忽略嵌套 SUBTOTAL,总计不会把小计又加一遍。"""
214
+ keys = [c['key'] for c in self.cols]
215
+ gj = keys.index(group_by) if group_by in keys else (
216
+ group_by if isinstance(group_by, int) else 0)
217
+ sum_cols = self._sum_cols()
218
+ order, buckets = [], {}
219
+ for row in self.rows:
220
+ k = row[gj] if gj < len(row) else ''
221
+ if k not in buckets:
222
+ order.append(k)
223
+ buckets[k] = []
224
+ buckets[k].append(row)
225
+ for k in order:
226
+ start = r
227
+ r = self._write_rows(r, buckets[k], formats)
228
+ end = r - 1
229
+ self._write_subtotal(r, formats, start, end, f'{k} 小计', sum_cols)
230
+ self.group_rows.append(r)
231
+ r += 1
232
+ return r
233
+
234
+ def _sum_cols(self):
235
+ t = self.spec.get('total') or {}
236
+ want = t.get('cols')
237
+ keys = [c['key'] for c in self.cols]
238
+ if want:
239
+ return [keys.index(w) + 1 for w in want if w in keys]
240
+ out = []
241
+ for j, col in enumerate(self.cols, start=1):
242
+ f = self._fmt(col, j - 1)
243
+ if f in (L.FORMATS['amount'], L.FORMATS['amount0'], L.FORMATS['int'],
244
+ L.FORMATS['accounting']):
245
+ out.append(j)
246
+ return out
247
+
248
+ def _write_subtotal(self, r, formats, start, end, label, sum_cols):
249
+ p, c = self.prof, self.c
250
+ fill = PatternFill('solid', fgColor=c['total_bg'])
251
+ border = Border(top=_side('808080'), bottom=_side(c['border']),
252
+ left=_side(c['border']), right=_side(c['border']))
253
+ for j in range(1, len(self.cols) + 1):
254
+ cell = self.ws.cell(r, j)
255
+ cell.font = Font(name=p['font'], size=p['size'], bold=True)
256
+ cell.fill = fill
257
+ cell.border = border
258
+ cell.number_format = formats[j - 1]
259
+ if j == 1:
260
+ cell.value = label
261
+ cell.alignment = Alignment(horizontal='left', vertical='center')
262
+ elif j in sum_cols and end >= start:
263
+ letter = get_column_letter(j)
264
+ cell.value = f'=SUBTOTAL(9,{letter}{start}:{letter}{end})'
265
+ cell.alignment = Alignment(horizontal='right', vertical='center')
266
+ self.ws.row_dimensions[r].height = p['row_height']
267
+
268
+ def _write_total(self, r, formats):
269
+ t = self.spec.get('total') or {}
270
+ label = t.get('label', '合计')
271
+ self._write_subtotal(r, formats, self.first_data, self.last_data, label, self._sum_cols())
272
+
273
+ def _widths(self, formats):
274
+ for j, col in enumerate(self.cols, start=1):
275
+ if col.get('width'):
276
+ w = float(col['width'])
277
+ else:
278
+ vals = [r[j - 1] for r in self.rows if j - 1 < len(r)]
279
+ w = L.col_width(vals, col['header'], formats[j - 1])
280
+ self.ws.column_dimensions[get_column_letter(j)].width = w
281
+
282
+ def _finish(self, next_row):
283
+ ws, p, spec = self.ws, self.prof, self.spec
284
+ last = (self.total_row or self.last_data)
285
+ ncol = max(1, len(self.cols))
286
+ if spec.get('freeze', p['freeze']) and self.rows:
287
+ ws.freeze_panes = f'A{self.header_row + 1}'
288
+ if spec.get('autofilter', p['autofilter']) and self.rows:
289
+ ws.auto_filter.ref = (f'A{self.header_row}:'
290
+ f'{get_column_letter(ncol)}{self.last_data}')
291
+ self._conditional()
292
+ self._validation()
293
+ self._chart(next_row)
294
+ self._print_setup()
295
+
296
+ def _conditional(self):
297
+ keys = [c['key'] for c in self.cols]
298
+ for rule in (self.spec.get('conditional') or []):
299
+ col = rule.get('col')
300
+ if col not in keys:
301
+ self.warnings.append(f'条件格式跳过:找不到列「{col}」')
302
+ continue
303
+ letter = get_column_letter(keys.index(col) + 1)
304
+ ref = f'{letter}{self.first_data}:{letter}{self.last_data}'
305
+ kind = str(rule.get('type', 'data_bar'))
306
+ try:
307
+ if kind == 'data_bar':
308
+ self.ws.conditional_formatting.add(
309
+ ref, DataBarRule(start_type='min', end_type='max',
310
+ color=rule.get('color', '638EC6')))
311
+ elif kind == 'color_scale':
312
+ self.ws.conditional_formatting.add(
313
+ ref, ColorScaleRule(start_type='min', start_color='F8696B',
314
+ mid_type='percentile', mid_value=50, mid_color='FFEB84',
315
+ end_type='max', end_color='63BE7B'))
316
+ else:
317
+ op = {'gt': 'greaterThan', 'lt': 'lessThan', 'ge': 'greaterThanOrEqual',
318
+ 'le': 'lessThanOrEqual', 'eq': 'equal'}.get(kind, 'greaterThan')
319
+ fill = PatternFill('solid', fgColor=rule.get('color', 'FFC7CE'))
320
+ self.ws.conditional_formatting.add(
321
+ ref, CellIsRule(operator=op, formula=[str(rule.get('value', 0))], fill=fill))
322
+ except Exception as e:
323
+ self.warnings.append(f'条件格式失败({col}):{e}')
324
+
325
+ def _validation(self):
326
+ keys = [c['key'] for c in self.cols]
327
+ for v in (self.spec.get('validation') or []):
328
+ col = v.get('col')
329
+ if col not in keys or not v.get('list'):
330
+ continue
331
+ letter = get_column_letter(keys.index(col) + 1)
332
+ items = ','.join(str(x) for x in v['list'])
333
+ if len(items) > 250:
334
+ self.warnings.append(f'数据验证跳过({col}):候选项超 250 字符,Excel 内联清单放不下')
335
+ continue
336
+ dv = DataValidation(type='list', formula1=f'"{items}"', allow_blank=True, showErrorMessage=True)
337
+ dv.error, dv.errorTitle = '请从下拉列表中选择', '取值不在允许范围'
338
+ self.ws.add_data_validation(dv)
339
+ dv.add(f'{letter}{self.first_data}:{letter}{max(self.last_data, self.first_data)}')
340
+
341
+ def _chart(self, at_row):
342
+ ch = self.spec.get('chart')
343
+ if not ch or not self.rows:
344
+ return
345
+ keys = [c['key'] for c in self.cols]
346
+ cat = ch.get('cat')
347
+ vals = [v for v in (ch.get('values') or []) if v in keys]
348
+ if cat not in keys or not vals:
349
+ self.warnings.append('图表跳过:cat 或 values 指向的列不存在')
350
+ return
351
+ kind = str(ch.get('type', 'bar'))
352
+ chart = {'bar': BarChart, 'line': LineChart, 'pie': PieChart}.get(kind, BarChart)()
353
+ chart.title = ch.get('title') or self.spec.get('title') or ''
354
+ chart.height, chart.width = ch.get('height', 8), ch.get('width', 16)
355
+ cats = Reference(self.ws, min_col=keys.index(cat) + 1,
356
+ min_row=self.first_data, max_row=self.last_data)
357
+ for v in vals:
358
+ j = keys.index(v) + 1
359
+ data = Reference(self.ws, min_col=j, min_row=self.header_row, max_row=self.last_data)
360
+ chart.add_data(data, titles_from_data=True)
361
+ chart.set_categories(cats)
362
+ self.ws.add_chart(chart, f'A{at_row + 2}')
363
+
364
+ def _print_setup(self):
365
+ ws, ps = self.ws, dict(self.prof['print'])
366
+ ps.update(self.spec.get('print') or {})
367
+ ws.page_setup.orientation = 'landscape' if ps.get('landscape') else 'portrait'
368
+ ws.page_setup.paperSize = ws.PAPERSIZE_A4 if str(ps.get('page', 'A4')).upper() == 'A4' else ws.PAPERSIZE_A3
369
+ if ps.get('fit_width', True):
370
+ ws.sheet_properties.pageSetUpPr = PageSetupProperties(fitToPage=True)
371
+ ws.page_setup.fitToWidth = 1
372
+ ws.page_setup.fitToHeight = 0
373
+ if ps.get('repeat_head', True) and self.rows:
374
+ ws.print_title_rows = f'{self.header_row}:{self.header_row}'
375
+ m = float(ps.get('margin_cm', 1.8)) / 2.54
376
+ ws.page_margins.left = ws.page_margins.right = m
377
+ ws.page_margins.top = ws.page_margins.bottom = m
378
+ if ps.get('footer_page', True):
379
+ ws.oddFooter.center.text = '第 &P 页 / 共 &N 页'
380
+ ws.oddFooter.center.size = 9
381
+
382
+
383
+ def _cover(wb, spec, prof):
384
+ ws = wb.create_sheet('说明', 0)
385
+ ws.sheet_view.showGridLines = False
386
+ c = prof['colors']
387
+ ws.column_dimensions['A'].width = 18
388
+ ws.column_dimensions['B'].width = 60
389
+ ws.cell(1, 1, spec.get('title') or '报表说明')
390
+ ws.cell(1, 1).font = Font(name=prof['font'], size=18, bold=True, color=c['title_fg'])
391
+ ws.merge_cells('A1:B1')
392
+ ws.row_dimensions[1].height = 34
393
+ rows = list(spec.get('fields') or [])
394
+ rows.append(['生成日期', spec.get('date') or datetime.date.today().strftime('%Y-%m-%d')])
395
+ r = 3
396
+ for k, v in rows:
397
+ ws.cell(r, 1, str(k)).font = Font(name=prof['font'], size=11, bold=True)
398
+ ws.cell(r, 1).fill = PatternFill('solid', fgColor=c['total_bg'])
399
+ ws.cell(r, 1).alignment = Alignment(horizontal='center', vertical='center')
400
+ ws.cell(r, 2, str(v)).font = Font(name=prof['font'], size=11)
401
+ ws.cell(r, 2).alignment = Alignment(horizontal='left', vertical='center', wrap_text=True)
402
+ for j in (1, 2):
403
+ ws.cell(r, j).border = Border(*(_side(c['border']),) * 4)
404
+ ws.row_dimensions[r].height = 22
405
+ r += 1
406
+ return ws
407
+
408
+
409
+ def build(spec, out_path):
410
+ """spec(dict)→ xlsx。返回 (path, 体检结果, warnings)。"""
411
+ prof = L.profile(spec.get('profile', 'report'))
412
+ if spec.get('theme'):
413
+ prof['colors'] = dict(L.THEMES.get(spec['theme'], prof['colors']))
414
+ prof['theme'] = spec['theme']
415
+ # 父目录不存在就建——直接调 API 的场景(如归档生成器批量落盘到子目录)
416
+ # 拿不到工具层的 resolveOut 兜底,漏了这一句就会在第一个子目录上炸。
417
+ d = os.path.dirname(os.path.abspath(out_path))
418
+ if d:
419
+ os.makedirs(d, exist_ok=True)
420
+ wb = Workbook()
421
+ wb.remove(wb.active)
422
+ warns = []
423
+ sheets = spec.get('sheets') or []
424
+ if not sheets:
425
+ raise ValueError('spec 至少要有一张 sheet(sheets: [{name, columns, rows}])')
426
+ for s in sheets:
427
+ name = str(s.get('name') or f'Sheet{len(wb.sheetnames)+1}')
428
+ safe = re.sub(r'[\\/:*?\[\]]', '_', name)[:31]
429
+ if safe != name:
430
+ warns.append(f'工作表名「{name}」含非法字符或超 31 字,已改为「{safe}」')
431
+ if safe in wb.sheetnames:
432
+ safe = f'{safe[:28]}_{len(wb.sheetnames)}'
433
+ ws = wb.create_sheet(safe)
434
+ b = SheetBuilder(ws, prof, s).build()
435
+ warns.extend(f'[{safe}] {w}' for w in b.warnings)
436
+ if spec.get('cover'):
437
+ _cover(wb, spec['cover'], prof)
438
+ wb.save(out_path)
439
+ res = None
440
+ if spec.get('inspect', True):
441
+ try:
442
+ import xlsx_check
443
+ res = xlsx_check.inspect(out_path, prof['name'])
444
+ except Exception as e:
445
+ warns.append(f'体检未能运行:{e}')
446
+ return out_path, res, warns
447
+
448
+
449
+ def from_frames(frames, out_path, profile='report', **kw):
450
+ """{sheet名: DataFrame} → 成品 xlsx(write_styled 的升级替代:带合计公式与打印设置)。"""
451
+ import pandas as pd
452
+ if hasattr(frames, 'columns'):
453
+ frames = {'Sheet1': frames}
454
+ sheets = []
455
+ for name, df in frames.items():
456
+ cols = [{'key': str(c), 'header': str(c)} for c in df.columns]
457
+ rows = df.where(pd.notna(df), None).values.tolist()
458
+ sheets.append({'name': str(name), 'columns': cols, 'rows': rows,
459
+ **{k: v for k, v in kw.items() if k in
460
+ ('title', 'note', 'total', 'group_by', 'chart', 'conditional', 'validation')}})
461
+ if kw.get('total') is True:
462
+ for s in sheets:
463
+ s['total'] = {'label': '合计'}
464
+ return build({'profile': profile, 'sheets': sheets,
465
+ **{k: v for k, v in kw.items() if k in ('theme', 'cover', 'inspect')}}, out_path)
466
+
467
+
468
+ def main(argv):
469
+ if len(argv) < 2:
470
+ print('用法:python xlsx_build.py spec.json 输出.xlsx | python xlsx_build.py --demo 出.xlsx')
471
+ return 2
472
+ if argv[0] == '--demo':
473
+ return _demo(argv[1])
474
+ spec = json.load(open(argv[0], encoding='utf-8'))
475
+ try:
476
+ path, res, warns = build(spec, argv[1])
477
+ except Exception as e:
478
+ print(json.dumps({'error': f'{type(e).__name__}: {e}'}, ensure_ascii=False))
479
+ return 1
480
+ out = {'path': path, 'warnings': warns}
481
+ if res:
482
+ import xlsx_check
483
+ out['check'] = {'counts': res['counts'], 'stats': res['stats']}
484
+ out['report'] = xlsx_check.report(res, show_info=False)
485
+ print(json.dumps(out, ensure_ascii=False, default=str))
486
+ return 0
487
+
488
+
489
+ def _demo(out):
490
+ rows = [['华东', '上海', 12000.0, 9800.0, 0.224],
491
+ ['华东', '江苏', 8600.5, 7200.0, 0.194],
492
+ ['华南', '广东', 15300.0, 13100.0, 0.168],
493
+ ['华南', '福建', 4200.0, 3900.0, 0.077],
494
+ ['西南', '四川', 6100.0, 5600.0, 0.089]]
495
+ spec = {
496
+ 'profile': 'report',
497
+ 'cover': {'title': '2026 年 8 月经营报表', 'fields': [
498
+ ['编制单位', '数据一部'], ['数据口径', '含税,截至 2026-08-31'], ['数据来源', 'ERP 销售出库单']]},
499
+ 'sheets': [{
500
+ 'name': '分区域明细', 'title': '2026 年 8 月分区域销售明细',
501
+ 'note': '口径:含税金额,单位万元;毛利率=毛利/销售额',
502
+ 'columns': [{'key': 'region', 'header': '大区'}, {'key': 'prov', 'header': '省份'},
503
+ {'key': 'amt', 'header': '销售额', 'format': 'amount'},
504
+ {'key': 'cost', 'header': '成本', 'format': 'amount'},
505
+ {'key': 'rate', 'header': '毛利率', 'format': 'percent'}],
506
+ 'rows': rows, 'group_by': 'region', 'total': {'label': '总计'},
507
+ 'conditional': [{'col': 'amt', 'type': 'data_bar'}],
508
+ 'chart': {'type': 'bar', 'cat': 'prov', 'values': ['amt'], 'title': '各省销售额'},
509
+ }],
510
+ }
511
+ path, res, warns = build(spec, out)
512
+ print(f'demo → {path}')
513
+ if warns:
514
+ print('提示:' + ';'.join(warns))
515
+ if res:
516
+ import xlsx_check
517
+ print(xlsx_check.report(res))
518
+ return 0
519
+
520
+
521
+ if __name__ == '__main__':
522
+ sys.exit(main(sys.argv[1:]))