@huicai/cli 2026.804.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.
- package/LICENSE +391 -32
- package/README.md +176 -10
- package/bin/huicai.js +166 -73
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -0
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -0
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -0
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/errkind.js +1 -0
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -0
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -0
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/vision.js +1 -0
- package/src/visual.js +1 -0
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- 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:]))
|