@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.
- package/LICENSE +391 -32
- package/README.md +137 -8
- package/bin/huicai.js +146 -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 -1
- 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 -1
- 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 -1
- 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 -1
- 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 -1
- 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 -1
- 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/visual.js +1 -1
- 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,602 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# 财务测算引擎:假设(JSON)→ 投资估算表 / 项目投资现金流量表 / 财务评价指标(NPV·IRR·回收期·总投资收益率)/
|
|
3
|
+
# 情景分析 / 敏感性分析(含临界点)/ 结论句 / 可直接交给 render_chart 的图表 spec。只用标准库。
|
|
4
|
+
#
|
|
5
|
+
# ── 为什么要有它 ──
|
|
6
|
+
# 可研报告最常见的硬伤是「数字对不上、口径不对」,而这恰恰是模型心算最容易错的地方。实测(HuiCai 可研基线):
|
|
7
|
+
# · 静态回收期拿「收入」去除总投资——没扣经营成本,回收期被严重低估;
|
|
8
|
+
# · 「内部模型调用节约 60 万元/年」,而整个项目的模型调用预算才 22 万元——节约额比被节约的支出还大;
|
|
9
|
+
# · 模板要求的净现值、内部收益率、敏感性分析一个都没有。
|
|
10
|
+
# 这些不是文笔问题,是计算问题。计算交给引擎:模型只给假设和依据,数字由这里算、表由这里排,
|
|
11
|
+
# 全文引用的每个数都来自同一次计算,前后不可能矛盾。
|
|
12
|
+
#
|
|
13
|
+
# ── 口径(按《建设项目经济评价方法与参数》(第三版)融资前分析;notes 里原样写给评审)──
|
|
14
|
+
# · 计算期 = 建设期 m 年 + 运营期 n 年,年序 1..m+n;现金流按年末发生(年末习惯法),折现到第 1 年初;
|
|
15
|
+
# · 所得税前净现金流量 = 营业收入 + 成本节约 + 回收余值 − 建设投资 − 经营成本;
|
|
16
|
+
# · 调整所得税 = 息税前利润 × 税率,息税前利润 = 收入 + 节约 − 经营成本 − 折旧摊销(建设投资按直线法摊销),
|
|
17
|
+
# 亏损向后结转 5 年弥补;所得税后净现金流量 = 税前 − 调整所得税;
|
|
18
|
+
# · 成本节约 = 被节约的那项支出 × 节约比例——**节约额永远不会超过被节约的支出本身**;
|
|
19
|
+
# · 投资回收期含建设期:T − 1 + |第 T−1 年累计净现金流量| ÷ 第 T 年净现金流量;
|
|
20
|
+
# · 敏感性:各因素单独 ±10%/±20%(折现率 ±2 个百分点),临界点 = 使 NPV 恰为 0 的变动幅度(即 IRR = 基准收益率)。
|
|
21
|
+
#
|
|
22
|
+
# 用法:python fin_model.py <assumptions.json | JSON 文本> 最后一行输出 JSON。
|
|
23
|
+
|
|
24
|
+
import json
|
|
25
|
+
import math
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ModelError(ValueError):
|
|
31
|
+
"""假设写错了:信息要能直接照着改。"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _num(x, where):
|
|
35
|
+
try:
|
|
36
|
+
v = float(x)
|
|
37
|
+
except (TypeError, ValueError):
|
|
38
|
+
raise ModelError(f'{where} 不是数字:{x!r}')
|
|
39
|
+
if math.isnan(v) or math.isinf(v):
|
|
40
|
+
raise ModelError(f'{where} 不是有限数字:{x!r}')
|
|
41
|
+
return v
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def fmt(v, d=2):
|
|
45
|
+
return '—' if v is None else f'{v:,.{d}f}'
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def pct(v, d=1):
|
|
49
|
+
return '—' if v is None else f'{v * 100:.{d}f}%'
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def yrs(v):
|
|
53
|
+
return '超出计算期' if v is None else f'{v:.2f} 年'
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def md(headers, rows):
|
|
57
|
+
out = ['| ' + ' | '.join(headers) + ' |', '|' + '|'.join(['---'] * len(headers)) + '|']
|
|
58
|
+
out += ['| ' + ' | '.join(str(c) for c in r) + ' |' for r in rows]
|
|
59
|
+
return '\n'.join(out)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
# ── 读假设 ──────────────────────────────────────────────────────────────────
|
|
63
|
+
def _len_hint(got, n, m):
|
|
64
|
+
"""逐年数组长度不对时,猜最常见的原因:把建设期那几年也写进去了。"""
|
|
65
|
+
if got == n + m:
|
|
66
|
+
return f'——看起来把建设期 {m} 年也算进去了:逐年数组只写运营期,去掉开头建设期的 {m} 个数'
|
|
67
|
+
return ''
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def series_of(item, n, where, is_cost=False, m=1):
|
|
71
|
+
"""一行收入/成本 → 运营期 n 年的逐年数组。支持 values / price×volume / per_year(+growth)。"""
|
|
72
|
+
if 'values' in item:
|
|
73
|
+
vals = item['values']
|
|
74
|
+
if not isinstance(vals, list) or len(vals) != n:
|
|
75
|
+
got = len(vals) if isinstance(vals, list) else 0
|
|
76
|
+
raise ModelError(f'{where}.values 要有 {n} 个数(运营期 {n} 年逐年),实际 {got} 个' + _len_hint(got, n, m))
|
|
77
|
+
return [_num(v, f'{where}.values[{i}]') for i, v in enumerate(vals)]
|
|
78
|
+
if 'price' in item and 'volume' in item:
|
|
79
|
+
price = _num(item['price'], f'{where}.price')
|
|
80
|
+
vol = item['volume']
|
|
81
|
+
if not isinstance(vol, list) or len(vol) != n:
|
|
82
|
+
got = len(vol) if isinstance(vol, list) else 0
|
|
83
|
+
raise ModelError(f'{where}.volume 要有 {n} 个数(运营期逐年销量/客户数),实际 {got} 个' + _len_hint(got, n, m))
|
|
84
|
+
return [price * _num(v, f'{where}.volume[{i}]') for i, v in enumerate(vol)]
|
|
85
|
+
if 'per_year' in item:
|
|
86
|
+
base = _num(item['per_year'], f'{where}.per_year')
|
|
87
|
+
g = _num(item.get('growth', 0), f'{where}.growth')
|
|
88
|
+
return [base * (1 + g) ** i for i in range(n)]
|
|
89
|
+
raise ModelError(f'{where} 要给 values,或 price+volume,或 per_year(+growth)'
|
|
90
|
+
+ (',或 pct_of_revenue' if is_cost else ''))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load(spec):
|
|
94
|
+
if not isinstance(spec, dict):
|
|
95
|
+
raise ModelError('假设要是一个 JSON 对象')
|
|
96
|
+
n = int(_num(spec.get('years', 5), 'years'))
|
|
97
|
+
m = int(_num(spec.get('construction_years', 1), 'construction_years'))
|
|
98
|
+
if not 1 <= n <= 30:
|
|
99
|
+
raise ModelError('years(运营期年数)要在 1~30 之间')
|
|
100
|
+
if not 1 <= m <= 5:
|
|
101
|
+
raise ModelError('construction_years(建设期年数)要在 1~5 之间')
|
|
102
|
+
r = _num(spec.get('discount_rate', 0.08), 'discount_rate')
|
|
103
|
+
if not 0 <= r < 1:
|
|
104
|
+
raise ModelError('discount_rate 写成小数(8% 写 0.08)')
|
|
105
|
+
tax = _num(spec.get('tax_rate', 0), 'tax_rate')
|
|
106
|
+
if not 0 <= tax < 1:
|
|
107
|
+
raise ModelError('tax_rate 写成小数(高新技术企业 15% 写 0.15;0 = 只做所得税前分析)')
|
|
108
|
+
dep_years = int(_num(spec.get('depreciation_years', n), 'depreciation_years'))
|
|
109
|
+
if dep_years < 1:
|
|
110
|
+
raise ModelError('depreciation_years 至少 1 年')
|
|
111
|
+
|
|
112
|
+
inv = spec.get('investment') or []
|
|
113
|
+
if not inv:
|
|
114
|
+
raise ModelError('缺 investment(建设投资明细):[{"name":"研发人员人工","amount":480,"basis":"12人×12月×3.3万元"}]')
|
|
115
|
+
investment = []
|
|
116
|
+
for i, it in enumerate(inv):
|
|
117
|
+
amt = _num(it.get('amount'), f'investment[{i}].amount')
|
|
118
|
+
if amt < 0:
|
|
119
|
+
raise ModelError(f'investment[{i}].amount 不能为负')
|
|
120
|
+
sch = it.get('schedule')
|
|
121
|
+
if sch is None:
|
|
122
|
+
sch = [1.0 / m] * m # 没写分年计划:建设期内平均投入
|
|
123
|
+
if not isinstance(sch, list) or len(sch) != m:
|
|
124
|
+
raise ModelError(f'investment[{i}].schedule 要有 {m} 个比例(建设期逐年投入占比)')
|
|
125
|
+
sch = [_num(x, f'investment[{i}].schedule') for x in sch]
|
|
126
|
+
if abs(sum(sch) - 1) > 1e-6:
|
|
127
|
+
raise ModelError(f'investment[{i}].schedule 各年比例之和要等于 1(现在是 {sum(sch):g})')
|
|
128
|
+
investment.append({'name': str(it.get('name') or f'投资项{i + 1}'), 'amount': amt, 'schedule': sch,
|
|
129
|
+
'basis': str(it.get('basis', '') or '')})
|
|
130
|
+
|
|
131
|
+
revenue = []
|
|
132
|
+
for i, it in enumerate(spec.get('revenue') or []):
|
|
133
|
+
revenue.append({'name': str(it.get('name') or f'收入{i + 1}'), 'values': series_of(it, n, f'revenue[{i}]', m=m),
|
|
134
|
+
'basis': str(it.get('basis', '') or '')})
|
|
135
|
+
savings = []
|
|
136
|
+
for i, it in enumerate(spec.get('savings') or []):
|
|
137
|
+
base = it.get('base_values')
|
|
138
|
+
if not isinstance(base, list) or len(base) != n:
|
|
139
|
+
got = len(base) if isinstance(base, list) else 0
|
|
140
|
+
raise ModelError(f'savings[{i}] 要给 base_values(被节约的那项支出,运营期逐年 {n} 个数,实际 {got} 个)和 rate——'
|
|
141
|
+
'节约额 = 支出 × 比例,没有支出就谈不上节约' + _len_hint(got, n, m))
|
|
142
|
+
rate = _num(it.get('rate'), f'savings[{i}].rate')
|
|
143
|
+
if not 0 <= rate <= 1:
|
|
144
|
+
raise ModelError(f'savings[{i}].rate 要在 0~1 之间(节约 30% 写 0.3)')
|
|
145
|
+
bv = [_num(v, f'savings[{i}].base_values[{t}]') for t, v in enumerate(base)]
|
|
146
|
+
savings.append({'name': str(it.get('name') or f'成本节约{i + 1}'), 'base': bv, 'rate': rate,
|
|
147
|
+
'values': [b * rate for b in bv], 'basis': str(it.get('basis', '') or '')})
|
|
148
|
+
if not revenue and not savings:
|
|
149
|
+
raise ModelError('revenue(营业收入)与 savings(成本节约)至少给一项。'
|
|
150
|
+
'既无收入也无可量化节约的公益/内部项目,应改做费用效果分析,不要硬编收入')
|
|
151
|
+
costs = []
|
|
152
|
+
for i, it in enumerate(spec.get('costs') or []):
|
|
153
|
+
p = it.get('pct_of_revenue')
|
|
154
|
+
if p is not None:
|
|
155
|
+
p = _num(p, f'costs[{i}].pct_of_revenue')
|
|
156
|
+
if not 0 <= p <= 1:
|
|
157
|
+
raise ModelError(f'costs[{i}].pct_of_revenue 要在 0~1 之间(收入的 20% 写 0.2)')
|
|
158
|
+
costs.append({'name': str(it.get('name') or f'成本{i + 1}'), 'pct': p,
|
|
159
|
+
'values': series_of(it, n, f'costs[{i}]', is_cost=True, m=m) if p is None else None,
|
|
160
|
+
'basis': str(it.get('basis', '') or '')})
|
|
161
|
+
scen = spec.get('scenarios') or {}
|
|
162
|
+
if not isinstance(scen, dict):
|
|
163
|
+
raise ModelError('scenarios 写成对象:{"乐观":{"revenue":1.2},"悲观":{"revenue":0.7,"costs":1.1}}')
|
|
164
|
+
for k, v in scen.items():
|
|
165
|
+
if not isinstance(v, dict) or not set(v) <= {'revenue', 'costs', 'investment', 'savings'}:
|
|
166
|
+
raise ModelError(f'情景「{k}」只能调 revenue/costs/investment/savings 四个乘数')
|
|
167
|
+
for kk, vv in v.items():
|
|
168
|
+
_num(vv, f'scenarios.{k}.{kk}')
|
|
169
|
+
start = spec.get('start_year')
|
|
170
|
+
return {
|
|
171
|
+
'unit': str(spec.get('unit', '万元')), 'n': n, 'm': m, 'r': r, 'tax': tax, 'dep_years': dep_years,
|
|
172
|
+
'start_year': int(_num(start, 'start_year')) if start is not None else None,
|
|
173
|
+
'investment': investment, 'revenue': revenue, 'costs': costs, 'savings': savings,
|
|
174
|
+
'residual': _num(spec.get('residual_value', 0), 'residual_value'), 'scenarios': scen,
|
|
175
|
+
'decimals': max(0, min(4, int(_num(spec.get('decimals', 2), 'decimals')))),
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ── 计算 ────────────────────────────────────────────────────────────────────
|
|
180
|
+
def cashflows(M, k_rev=1.0, k_cost=1.0, k_inv=1.0, k_sav=1.0, rev_line=None):
|
|
181
|
+
"""按乘数重算一遍现金流。基准、情景、敏感性、临界点全走这一个函数,口径不会分叉。"""
|
|
182
|
+
m, n = M['m'], M['n']
|
|
183
|
+
rev_lines = [[v * (k_rev if rev_line in (None, x['name']) else 1.0) for v in x['values']] for x in M['revenue']]
|
|
184
|
+
rev = [sum(l[j] for l in rev_lines) for j in range(n)]
|
|
185
|
+
cost_lines = [[(c['pct'] * rev[j] if c['pct'] is not None else c['values'][j]) * k_cost for j in range(n)]
|
|
186
|
+
for c in M['costs']]
|
|
187
|
+
cost = [sum(l[j] for l in cost_lines) for j in range(n)]
|
|
188
|
+
sav_lines = [[v * k_sav for v in s['values']] for s in M['savings']]
|
|
189
|
+
sav = [sum(l[j] for l in sav_lines) for j in range(n)]
|
|
190
|
+
inv_year = [sum(x['amount'] * x['schedule'][k] for x in M['investment']) * k_inv for k in range(m)]
|
|
191
|
+
inv = sum(inv_year)
|
|
192
|
+
dep = [inv / M['dep_years'] if j < M['dep_years'] else 0.0 for j in range(n)]
|
|
193
|
+
ebit = [rev[j] + sav[j] - cost[j] - dep[j] for j in range(n)]
|
|
194
|
+
tax, pool = [], [] # 亏损向后结转 5 年
|
|
195
|
+
for j, e in enumerate(ebit):
|
|
196
|
+
if e <= 0:
|
|
197
|
+
if e < 0:
|
|
198
|
+
pool.append([j, -e])
|
|
199
|
+
tax.append(0.0)
|
|
200
|
+
continue
|
|
201
|
+
taxable = e
|
|
202
|
+
for p in pool:
|
|
203
|
+
if j - p[0] > 5 or p[1] <= 0:
|
|
204
|
+
continue
|
|
205
|
+
use = min(p[1], taxable)
|
|
206
|
+
p[1] -= use
|
|
207
|
+
taxable -= use
|
|
208
|
+
if taxable <= 0:
|
|
209
|
+
break
|
|
210
|
+
tax.append(taxable * M['tax'])
|
|
211
|
+
resid = [M['residual'] if j == n - 1 else 0.0 for j in range(n)]
|
|
212
|
+
pre = [-v for v in inv_year] + [rev[j] + sav[j] + resid[j] - cost[j] for j in range(n)]
|
|
213
|
+
post = pre[:m] + [pre[m + j] - tax[j] for j in range(n)]
|
|
214
|
+
return {'rev': rev, 'rev_lines': rev_lines, 'cost': cost, 'cost_lines': cost_lines, 'sav': sav,
|
|
215
|
+
'sav_lines': sav_lines, 'inv_year': inv_year, 'inv': inv, 'dep': dep, 'ebit': ebit, 'tax': tax,
|
|
216
|
+
'resid': resid, 'pre': pre, 'post': post}
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def npv(ncf, r):
|
|
220
|
+
"""年末习惯法:第 t 年(t 从 1 起)的现金流折现 t 年。"""
|
|
221
|
+
return sum(v / (1 + r) ** (t + 1) for t, v in enumerate(ncf))
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def irr(ncf):
|
|
225
|
+
"""二分法求 NPV=0 的折现率;无根(如累计始终为负)返回 None——如实说「不存在」,不编数。"""
|
|
226
|
+
lo, hi = -0.99, 10.0
|
|
227
|
+
f_lo, f_hi = npv(ncf, lo), npv(ncf, hi)
|
|
228
|
+
if f_lo == 0:
|
|
229
|
+
return lo
|
|
230
|
+
if f_lo * f_hi > 0:
|
|
231
|
+
return None
|
|
232
|
+
for _ in range(300):
|
|
233
|
+
mid = (lo + hi) / 2
|
|
234
|
+
fm = npv(ncf, mid)
|
|
235
|
+
if abs(fm) < 1e-10 or hi - lo < 1e-12:
|
|
236
|
+
return mid
|
|
237
|
+
if (f_lo < 0) == (fm < 0):
|
|
238
|
+
lo, f_lo = mid, fm
|
|
239
|
+
else:
|
|
240
|
+
hi = mid
|
|
241
|
+
return (lo + hi) / 2
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def payback(ncf, r=None):
|
|
245
|
+
"""含建设期的投资回收期:T − 1 + |第 T−1 年累计| ÷ 第 T 年净现金流(T 为累计首次 ≥0 的年序)。收不回返回 None。"""
|
|
246
|
+
cum = 0.0
|
|
247
|
+
for t, v in enumerate(ncf):
|
|
248
|
+
dv = v if r is None else v / (1 + r) ** (t + 1)
|
|
249
|
+
prev, cum = cum, cum + dv
|
|
250
|
+
if t > 0 and prev < 0 <= cum:
|
|
251
|
+
return t + (-prev) / dv
|
|
252
|
+
return None
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
def indicators(ncf, r):
|
|
256
|
+
return {'npv': npv(ncf, r), 'irr': irr(ncf), 'payback': payback(ncf), 'payback_dynamic': payback(ncf, r)}
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def main_ncf(M, cf):
|
|
260
|
+
return cf['post'] if M['tax'] > 0 else cf['pre']
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
def switching(M, key):
|
|
264
|
+
"""临界点:使主口径 NPV 恰为 0 的因素变动幅度,在 [-99%, +300%] 内二分;找不到返回 None。"""
|
|
265
|
+
def f(k):
|
|
266
|
+
kw = {'k_rev': 1.0, 'k_cost': 1.0, 'k_inv': 1.0, 'k_sav': 1.0}
|
|
267
|
+
if key.startswith('rev:'):
|
|
268
|
+
kw['k_rev'] = 1 + k
|
|
269
|
+
return npv(main_ncf(M, cashflows(M, rev_line=key[4:], **kw)), M['r'])
|
|
270
|
+
kw[key] = 1 + k
|
|
271
|
+
return npv(main_ncf(M, cashflows(M, **kw)), M['r'])
|
|
272
|
+
if f(0.0) == 0:
|
|
273
|
+
return 0.0
|
|
274
|
+
for a, b in ((-0.99, 0.0), (0.0, 3.0)):
|
|
275
|
+
fa, fb = f(a), f(b)
|
|
276
|
+
if fa * fb < 0:
|
|
277
|
+
for _ in range(100):
|
|
278
|
+
mid = (a + b) / 2
|
|
279
|
+
fm = f(mid)
|
|
280
|
+
if (fm < 0) == (fa < 0):
|
|
281
|
+
a, fa = mid, fm
|
|
282
|
+
else:
|
|
283
|
+
b = mid
|
|
284
|
+
return (a + b) / 2
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def run(spec):
|
|
289
|
+
M = load(spec)
|
|
290
|
+
n, m, u, r, d = M['n'], M['m'], M['unit'], M['r'], M['decimals']
|
|
291
|
+
T = m + n
|
|
292
|
+
has_tax = M['tax'] > 0
|
|
293
|
+
basis = '所得税后' if has_tax else '所得税前'
|
|
294
|
+
base = cashflows(M)
|
|
295
|
+
ind_pre = indicators(base['pre'], r)
|
|
296
|
+
ind_post = indicators(base['post'], r) if has_tax else None
|
|
297
|
+
ind = ind_post or ind_pre
|
|
298
|
+
avg_ebit = sum(base['ebit']) / n
|
|
299
|
+
roi = avg_ebit / base['inv'] if base['inv'] else None
|
|
300
|
+
|
|
301
|
+
def f(v):
|
|
302
|
+
return fmt(v, d)
|
|
303
|
+
|
|
304
|
+
years = [str(M['start_year'] + t) if M['start_year'] else f'第{t + 1}年' for t in range(T)]
|
|
305
|
+
|
|
306
|
+
# ── 预警:不改数,只提醒哪里评审会追问 ──
|
|
307
|
+
warnings = []
|
|
308
|
+
if not M['costs']:
|
|
309
|
+
warnings.append('没有列经营成本:回收期与 NPV 会按「收入全是净收益」算,严重高估——至少列人员、云资源/算力、模型调用、运维与销售')
|
|
310
|
+
elif sum(base['rev']) > 0 and sum(base['cost']) < 0.15 * sum(base['rev']):
|
|
311
|
+
warnings.append(f'经营成本只占营业收入的 {pct(sum(base["cost"]) / sum(base["rev"]))},偏低——'
|
|
312
|
+
'AI 类产品的模型调用、算力、运维人员、销售与服务费用要逐项列全')
|
|
313
|
+
if ind['npv'] < 0:
|
|
314
|
+
warnings.append(f'{basis} NPV 为 {f(ind["npv"])} {u} < 0:按当前假设财务上不可行——结论写「不可行」或'
|
|
315
|
+
'「有条件可行」并写清条件,不要为了结论去改假设')
|
|
316
|
+
if ind['irr'] is None:
|
|
317
|
+
warnings.append('内部收益率不存在(累计净现金流始终为负):如实写「IRR 不存在」,不要编一个数')
|
|
318
|
+
elif ind['irr'] > 1:
|
|
319
|
+
warnings.append(f'IRR 高达 {pct(ind["irr"])}:通常是投资漏列或收入假设过于乐观,评审一定会追问依据')
|
|
320
|
+
if ind['payback'] is None:
|
|
321
|
+
warnings.append(f'计算期 {T} 年内收不回投资:回收期写「超出计算期」')
|
|
322
|
+
for s in M['savings']:
|
|
323
|
+
if s['rate'] > 0.5:
|
|
324
|
+
warnings.append(f'「{s["name"]}」节约比例 {pct(s["rate"], 0)},超过一半——要有实测或试点数据支撑,否则评审不会采信')
|
|
325
|
+
for x in M['revenue']:
|
|
326
|
+
v = x['values']
|
|
327
|
+
for j in range(1, n):
|
|
328
|
+
if v[j - 1] > 0 and v[j] / v[j - 1] > 3:
|
|
329
|
+
warnings.append(f'收入「{x["name"]}」运营第 {j + 1} 年是上一年的 {v[j] / v[j - 1]:.1f} 倍:要写清增长依据(渠道、签约、试点转化)')
|
|
330
|
+
break
|
|
331
|
+
if sum(base['sav']) > sum(base['rev']) > 0:
|
|
332
|
+
warnings.append('成本节约合计大于营业收入合计:效益主要靠「省钱」,文中要写清节约的来源与测量方法')
|
|
333
|
+
# 情景方向要自洽。实测模型不知道「按收入比例的成本会自动随收入变」,为了让上游成本跟着收入降,
|
|
334
|
+
# 给悲观情景设了 costs ×0.8——得到一个「收入降 40%、成本降 20%」的悲观情景,写进报告就是笑话。
|
|
335
|
+
has_pct = any(c['pct'] is not None for c in M['costs'])
|
|
336
|
+
for name, mult in M['scenarios'].items():
|
|
337
|
+
kr, kc = float(mult.get('revenue', 1)), float(mult.get('costs', 1))
|
|
338
|
+
bad = re.search(r'悲观|保守|下行|不利|压力', name)
|
|
339
|
+
good = re.search(r'乐观|上行|有利', name)
|
|
340
|
+
if bad and (kc < 1 or kr > 1):
|
|
341
|
+
warnings.append(f'「{name}」情景的方向不对(收入×{kr:g}、经营成本×{kc:g}):悲观情景应是收入 ≤1、成本 ≥1。'
|
|
342
|
+
+ ('按收入比例列的成本(pct_of_revenue)已经随收入自动下降,不用再把 costs 调低来「跟随」——' if has_pct and kc < 1 else '')
|
|
343
|
+
+ '改完重算,别把方向相反的情景写进报告')
|
|
344
|
+
if good and (kc > 1 or kr < 1):
|
|
345
|
+
warnings.append(f'「{name}」情景的方向不对(收入×{kr:g}、经营成本×{kc:g}):乐观情景应是收入 ≥1、成本 ≤1'
|
|
346
|
+
+ ('(按收入比例的成本会随收入自动上升,不必再调高 costs)' if has_pct and kc > 1 else '') + ',改完重算')
|
|
347
|
+
|
|
348
|
+
# ── 1) 投资估算表 ──
|
|
349
|
+
inv_total = base['inv']
|
|
350
|
+
head = ['序号', '工程或费用名称', f'金额({u})', '占比']
|
|
351
|
+
if m > 1:
|
|
352
|
+
head += [years[k] for k in range(m)]
|
|
353
|
+
head.append('估算依据')
|
|
354
|
+
rows = []
|
|
355
|
+
for i, x in enumerate(M['investment']):
|
|
356
|
+
row = [str(i + 1), x['name'], f(x['amount']), pct(x['amount'] / inv_total if inv_total else 0)]
|
|
357
|
+
if m > 1:
|
|
358
|
+
row += [f(x['amount'] * s) for s in x['schedule']]
|
|
359
|
+
rows.append(row + [x['basis'] or '—'])
|
|
360
|
+
tot = ['', '合计', f(inv_total), '100.0%']
|
|
361
|
+
if m > 1:
|
|
362
|
+
tot += [f(v) for v in base['inv_year']]
|
|
363
|
+
rows.append(tot + [''])
|
|
364
|
+
t_inv = md(head, rows)
|
|
365
|
+
|
|
366
|
+
# ── 2) 项目投资现金流量表(序号/项目/合计/逐年)──
|
|
367
|
+
def line(no, name, vals_full):
|
|
368
|
+
return [no, name, f(sum(vals_full))] + [f(v) if v != 0 else '—' for v in vals_full]
|
|
369
|
+
|
|
370
|
+
def ops(vals): # 运营期数组 → 计算期数组(建设期补 0)
|
|
371
|
+
return [0.0] * m + list(vals)
|
|
372
|
+
|
|
373
|
+
def cumsum(vals):
|
|
374
|
+
out, acc = [], 0.0
|
|
375
|
+
for v in vals:
|
|
376
|
+
acc += v
|
|
377
|
+
out.append(acc)
|
|
378
|
+
return out
|
|
379
|
+
|
|
380
|
+
rows = []
|
|
381
|
+
cin = [0.0] * m + [base['rev'][j] + base['sav'][j] + base['resid'][j] for j in range(n)]
|
|
382
|
+
rows.append(line('1', '现金流入', cin))
|
|
383
|
+
k = 1
|
|
384
|
+
for x, l in zip(M['revenue'], base['rev_lines']):
|
|
385
|
+
rows.append(line(f'1.{k}', f'营业收入:{x["name"]}', ops(l)))
|
|
386
|
+
k += 1
|
|
387
|
+
for s, l in zip(M['savings'], base['sav_lines']):
|
|
388
|
+
rows.append(line(f'1.{k}', f'成本节约:{s["name"]}', ops(l)))
|
|
389
|
+
k += 1
|
|
390
|
+
if M['residual']:
|
|
391
|
+
rows.append(line(f'1.{k}', '回收资产余值', ops(base['resid'])))
|
|
392
|
+
cout = [base['inv_year'][t] if t < m else base['cost'][t - m] for t in range(T)]
|
|
393
|
+
rows.append(line('2', '现金流出', cout))
|
|
394
|
+
rows.append(line('2.1', '建设投资', base['inv_year'] + [0.0] * n))
|
|
395
|
+
for i, (c, l) in enumerate(zip(M['costs'], base['cost_lines'])):
|
|
396
|
+
rows.append(line(f'2.{i + 2}', f'经营成本:{c["name"]}', ops(l)))
|
|
397
|
+
cum_pre = cumsum(base['pre'])
|
|
398
|
+
rows.append(line('3', '所得税前净现金流量(1−2)', base['pre']))
|
|
399
|
+
rows.append(['4', '累计所得税前净现金流量', '—'] + [f(v) for v in cum_pre])
|
|
400
|
+
cum_main = cum_pre
|
|
401
|
+
if has_tax:
|
|
402
|
+
cum_post = cumsum(base['post'])
|
|
403
|
+
rows.append(line('5', f'调整所得税({pct(M["tax"], 0)})', ops(base['tax'])))
|
|
404
|
+
rows.append(line('6', '所得税后净现金流量(3−5)', base['post']))
|
|
405
|
+
rows.append(['7', '累计所得税后净现金流量', '—'] + [f(v) for v in cum_post])
|
|
406
|
+
cum_main = cum_post
|
|
407
|
+
main = main_ncf(M, base)
|
|
408
|
+
disc = [v / (1 + r) ** (t + 1) for t, v in enumerate(main)]
|
|
409
|
+
cum_disc = cumsum(disc)
|
|
410
|
+
no = 8 if has_tax else 5
|
|
411
|
+
rows.append([str(no), f'折现系数(i={pct(r, 0)})', '—'] + [f'{1 / (1 + r) ** (t + 1):.4f}' for t in range(T)])
|
|
412
|
+
rows.append([str(no + 1), f'{basis}折现净现金流量', f(sum(disc))] + [f(v) for v in disc])
|
|
413
|
+
rows.append([str(no + 2), f'累计{basis}折现净现金流量', '—'] + [f(v) for v in cum_disc])
|
|
414
|
+
t_cf = md(['序号', '项目', '合计'] + years, rows)
|
|
415
|
+
|
|
416
|
+
# ── 3) 财务评价指标表 ──
|
|
417
|
+
def ind_rows(tag, ii):
|
|
418
|
+
return [
|
|
419
|
+
[f'项目投资财务净现值({tag},i={pct(r, 0)})', f'{f(ii["npv"])} {u}', 'FNPV ≥ 0 可行'],
|
|
420
|
+
[f'项目投资财务内部收益率({tag})', pct(ii['irr'], 2) if ii['irr'] is not None else '不存在',
|
|
421
|
+
f'FIRR ≥ 基准收益率 {pct(r, 0)} 可行'],
|
|
422
|
+
[f'静态投资回收期({tag},含建设期)', yrs(ii['payback']), f'计算期 {T} 年'],
|
|
423
|
+
[f'动态投资回收期({tag},含建设期)', yrs(ii['payback_dynamic']), '按折现后的累计净现金流量'],
|
|
424
|
+
]
|
|
425
|
+
rows = ind_rows('所得税前', ind_pre)
|
|
426
|
+
if has_tax:
|
|
427
|
+
rows += ind_rows('所得税后', ind_post)
|
|
428
|
+
rows.append(['总投资收益率(运营期年均息税前利润 ÷ 总投资)', pct(roi), '与行业参考值比较'])
|
|
429
|
+
t_ind = md(['指标', '数值', '判据'], rows)
|
|
430
|
+
|
|
431
|
+
# ── 4) 情景分析 ──
|
|
432
|
+
scen_def = {'基准': {}}
|
|
433
|
+
scen_def.update(M['scenarios'])
|
|
434
|
+
rows, scen_out, scen_desc = [], {}, {}
|
|
435
|
+
labels = (('revenue', '收入'), ('costs', '经营成本'), ('investment', '投资'), ('savings', '节约'))
|
|
436
|
+
for name, mult in scen_def.items():
|
|
437
|
+
kw = {'k_rev': float(mult.get('revenue', 1)), 'k_cost': float(mult.get('costs', 1)),
|
|
438
|
+
'k_inv': float(mult.get('investment', 1)), 'k_sav': float(mult.get('savings', 1))}
|
|
439
|
+
ii = indicators(main_ncf(M, cashflows(M, **kw)), r)
|
|
440
|
+
scen_out[name] = ii
|
|
441
|
+
desc = '、'.join(f'{lab}{(float(mult[key]) - 1) * 100:+.0f}%' for key, lab in labels
|
|
442
|
+
if key in mult and float(mult[key]) != 1) or '全部按基准假设'
|
|
443
|
+
scen_desc[name] = desc
|
|
444
|
+
rows.append([name, desc, f(ii['npv']), pct(ii['irr'], 2) if ii['irr'] is not None else '不存在',
|
|
445
|
+
yrs(ii['payback'])])
|
|
446
|
+
t_scen = md(['情景', '假设调整', f'NPV({basis},{u})', f'FIRR({basis})', '静态回收期'], rows)
|
|
447
|
+
|
|
448
|
+
# ── 5) 敏感性分析 ──
|
|
449
|
+
factors = []
|
|
450
|
+
if M['revenue']:
|
|
451
|
+
factors.append(('k_rev', '营业收入'))
|
|
452
|
+
if M['costs']:
|
|
453
|
+
factors.append(('k_cost', '经营成本'))
|
|
454
|
+
factors.append(('k_inv', '建设投资'))
|
|
455
|
+
if M['savings']:
|
|
456
|
+
factors.append(('k_sav', '成本节约'))
|
|
457
|
+
if len(M['revenue']) > 1:
|
|
458
|
+
factors += [(f'rev:{x["name"]}', f'其中:{x["name"]}') for x in M['revenue']]
|
|
459
|
+
steps = [-0.2, -0.1, 0.1, 0.2]
|
|
460
|
+
rows, tornado, sens = [], [], []
|
|
461
|
+
for key, lab in factors:
|
|
462
|
+
vals = []
|
|
463
|
+
for st in steps:
|
|
464
|
+
kw = {'k_rev': 1.0, 'k_cost': 1.0, 'k_inv': 1.0, 'k_sav': 1.0}
|
|
465
|
+
if key.startswith('rev:'):
|
|
466
|
+
kw['k_rev'] = 1 + st
|
|
467
|
+
cf = cashflows(M, rev_line=key[4:], **kw)
|
|
468
|
+
else:
|
|
469
|
+
kw[key] = 1 + st
|
|
470
|
+
cf = cashflows(M, **kw)
|
|
471
|
+
vals.append(npv(main_ncf(M, cf), r))
|
|
472
|
+
coef = ((vals[2] - vals[1]) / 0.2) / ind['npv'] if ind['npv'] else None
|
|
473
|
+
sw = switching(M, key)
|
|
474
|
+
sens.append({'factor': lab, 'coef': coef, 'switch': sw})
|
|
475
|
+
if sw is not None:
|
|
476
|
+
sw_txt = f'{sw * 100:+.1f}%'
|
|
477
|
+
elif (coef or 0) >= 0: # 正向因素:往下压到头 NPV 仍不变号
|
|
478
|
+
sw_txt = '减少 99% 仍不转负' if ind['npv'] >= 0 else '增加 300% 仍为负'
|
|
479
|
+
else:
|
|
480
|
+
sw_txt = '增加 300% 仍不转负' if ind['npv'] >= 0 else '减少 99% 仍为负'
|
|
481
|
+
rows.append([lab] + [f(v) for v in vals] + [f'{coef:.2f}' if coef is not None else '—', sw_txt])
|
|
482
|
+
if not key.startswith('rev:'): # 龙卷风只画总因素,分项留在表里
|
|
483
|
+
tornado.append({'name': lab, 'low': round(vals[0], d), 'high': round(vals[3], d),
|
|
484
|
+
'low_label': '-20%', 'high_label': '+20%'})
|
|
485
|
+
lo_r, hi_r = max(r - 0.02, 0.0), r + 0.02
|
|
486
|
+
dr_lo, dr_hi = npv(main, lo_r), npv(main, hi_r)
|
|
487
|
+
tornado.append({'name': '基准收益率', 'low': round(dr_lo, d), 'high': round(dr_hi, d),
|
|
488
|
+
'low_label': pct(lo_r, 0), 'high_label': pct(hi_r, 0)})
|
|
489
|
+
t_sens = md(['因素', '-20%', '-10%', '+10%', '+20%', '敏感度系数', '临界点(NPV=0)'], rows)
|
|
490
|
+
# 折现率不是「±10%」那种变动,单独一句,免得挤进同一列被误读
|
|
491
|
+
t_sens += (f'\n\n表中数值为{basis}财务净现值({u}),基准情景 {f(ind["npv"])} {u}。基准收益率取 {pct(lo_r, 0)} / {pct(hi_r, 0)} 时,'
|
|
492
|
+
f'NPV 分别为 {f(dr_lo)} / {f(dr_hi)} {u}'
|
|
493
|
+
+ (f';折现率升至 {pct(ind["irr"], 2)}(即 FIRR)时 NPV 为 0。' if ind['irr'] is not None else '。'))
|
|
494
|
+
|
|
495
|
+
# ── 图表 spec(原样交给 render_chart)──
|
|
496
|
+
charts = {
|
|
497
|
+
'investment_composition': {'type': 'pie', 'unit': u, 'categories': [x['name'] for x in M['investment']],
|
|
498
|
+
'series': [{'name': '建设投资', 'values': [round(x['amount'], d) for x in M['investment']]}]},
|
|
499
|
+
'investment_waterfall': {'type': 'waterfall', 'unit': u, 'total_label': '总投资',
|
|
500
|
+
'items': [{'name': x['name'], 'value': round(x['amount'], d)} for x in M['investment']]},
|
|
501
|
+
'cumulative_cashflow': {'type': 'line', 'unit': u, 'categories': years,
|
|
502
|
+
'series': [{'name': f'累计{basis}净现金流量', 'values': [round(v, d) for v in cum_main]},
|
|
503
|
+
{'name': f'累计{basis}折现净现金流量', 'values': [round(v, d) for v in cum_disc]}]},
|
|
504
|
+
'sensitivity_tornado': {'type': 'tornado', 'metric': f'{basis}NPV', 'unit': u, 'base': round(ind['npv'], d),
|
|
505
|
+
'items': tornado},
|
|
506
|
+
}
|
|
507
|
+
if M['revenue']:
|
|
508
|
+
op_years = years[m:]
|
|
509
|
+
if len(M['revenue']) > 1:
|
|
510
|
+
charts['revenue_by_line'] = {'type': 'stacked', 'unit': u, 'categories': op_years,
|
|
511
|
+
'series': [{'name': x['name'], 'values': [round(v, d) for v in l]}
|
|
512
|
+
for x, l in zip(M['revenue'], base['rev_lines'])]}
|
|
513
|
+
if M['costs']:
|
|
514
|
+
charts['revenue_vs_cost'] = {'type': 'bar', 'unit': u, 'categories': op_years,
|
|
515
|
+
'series': [{'name': '营业收入', 'values': [round(v, d) for v in base['rev']]},
|
|
516
|
+
{'name': '经营成本', 'values': [round(v, d) for v in base['cost']]}]}
|
|
517
|
+
|
|
518
|
+
# ── 口径说明(原样写进报告,评审能照着复核)──
|
|
519
|
+
notes = [
|
|
520
|
+
f'计算期 {T} 年:建设期 {m} 年({years[0]}{"–" + years[m - 1] if m > 1 else ""}),运营期 {n} 年'
|
|
521
|
+
f'({years[m]}{"–" + years[-1] if n > 1 else ""});金额单位 {u};现金流按年末发生,折现到计算期第 1 年初;'
|
|
522
|
+
f'基准收益率 {pct(r, 1)}。',
|
|
523
|
+
'所得税前净现金流量 = 营业收入 + 成本节约 + 回收资产余值 − 建设投资 − 经营成本。',
|
|
524
|
+
]
|
|
525
|
+
if has_tax:
|
|
526
|
+
notes.append(f'调整所得税 = 息税前利润 × {pct(M["tax"], 0)};息税前利润 = 营业收入 + 成本节约 − 经营成本 − 摊销,'
|
|
527
|
+
f'建设投资按 {M["dep_years"]} 年直线法摊销、残值为 0,亏损向后结转 5 年弥补。')
|
|
528
|
+
else:
|
|
529
|
+
notes.append('未计所得税(所得税前分析);如需所得税后指标,给出适用税率后重算。')
|
|
530
|
+
for x in M['revenue']:
|
|
531
|
+
if x['basis']:
|
|
532
|
+
notes.append(f'营业收入「{x["name"]}」:{x["basis"]}。')
|
|
533
|
+
for c in M['costs']:
|
|
534
|
+
if c['pct'] is not None:
|
|
535
|
+
notes.append(f'经营成本「{c["name"]}」按营业收入的 {pct(c["pct"], 0)} 计' + (f':{c["basis"]}' if c['basis'] else '') + '。')
|
|
536
|
+
elif c['basis']:
|
|
537
|
+
notes.append(f'经营成本「{c["name"]}」:{c["basis"]}。')
|
|
538
|
+
for s in M['savings']:
|
|
539
|
+
notes.append(f'成本节约「{s["name"]}」= 被节约的支出 × {pct(s["rate"], 0)}' + (f'({s["basis"]})' if s['basis'] else '')
|
|
540
|
+
+ f';被节约支出逐年为 {"、".join(f(v) for v in s["base"])} {u},节约额不超过支出本身。')
|
|
541
|
+
if any(c['pct'] is not None for c in M['costs']):
|
|
542
|
+
notes.append('按营业收入比例计的经营成本,在情景分析与敏感性分析中随营业收入同步变动;'
|
|
543
|
+
'「经营成本」因素的调整乘在全部经营成本之上。')
|
|
544
|
+
notes.append('敏感性分析每次只变动一个因素、其余保持基准;临界点为使 NPV 恰为 0 的变动幅度。')
|
|
545
|
+
|
|
546
|
+
# ── 结论句(正文直接引用,保证与表一致)──
|
|
547
|
+
top = max(sens, key=lambda s: abs(s['coef'] or 0)) if sens else None
|
|
548
|
+
feasible = ind['npv'] >= 0 and ind['irr'] is not None and ind['irr'] >= r
|
|
549
|
+
concl = (f'按基准假设,项目总投资 {f(inv_total)} {u},{basis}财务净现值 {f(ind["npv"])} {u}'
|
|
550
|
+
f'(i={pct(r, 0)}),财务内部收益率 {pct(ind["irr"], 2) if ind["irr"] is not None else "不存在"},'
|
|
551
|
+
f'静态投资回收期 {yrs(ind["payback"])}(含建设期 {m} 年),动态投资回收期 {yrs(ind["payback_dynamic"])};')
|
|
552
|
+
concl += '财务上可行。' if feasible else '按当前假设财务上不可行。'
|
|
553
|
+
if top and top['switch'] is not None and top['coef']:
|
|
554
|
+
concl += (f'{top["factor"]}是最敏感的因素(敏感度系数 {top["coef"]:.2f}),'
|
|
555
|
+
f'{"下降" if top["switch"] < 0 else "上升"} {abs(top["switch"]) * 100:.1f}% 时 NPV 降为 0。')
|
|
556
|
+
|
|
557
|
+
bad = [(k, v) for k, v in scen_out.items() if k != '基准' and v['npv'] < 0]
|
|
558
|
+
if bad and feasible: # 结论「可行」不能盖住下行情景转负
|
|
559
|
+
concl += ';'.join(f'在「{k}」情景({scen_desc[k]})下 NPV 为 {f(v["npv"])} {u},转为不可行' for k, v in bad) \
|
|
560
|
+
+ ',需在风险分析中给出应对措施。'
|
|
561
|
+
|
|
562
|
+
def rnd(ii):
|
|
563
|
+
return {kk: (round(v, 6) if isinstance(v, float) else v) for kk, v in ii.items()}
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
'ok': True, 'unit': u, 'basis': basis,
|
|
567
|
+
'indicators': {'pre_tax': rnd(ind_pre), 'post_tax': rnd(ind_post) if ind_post else None,
|
|
568
|
+
'roi': round(roi, 6) if roi is not None else None, 'total_investment': round(inv_total, 6)},
|
|
569
|
+
'conclusion': concl,
|
|
570
|
+
'scenarios': {kk: rnd(v) for kk, v in scen_out.items()},
|
|
571
|
+
'sensitivity': [{'factor': s['factor'], 'coef': round(s['coef'], 4) if s['coef'] is not None else None,
|
|
572
|
+
'switch': round(s['switch'], 4) if s['switch'] is not None else None} for s in sens],
|
|
573
|
+
'tables': {'投资估算表': t_inv, '项目投资现金流量表': t_cf, '财务评价指标表': t_ind,
|
|
574
|
+
'情景分析表': t_scen, '敏感性分析表': t_sens},
|
|
575
|
+
'notes': notes, 'charts': charts, 'warnings': warnings,
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
|
|
579
|
+
def main(argv):
|
|
580
|
+
if len(argv) < 2:
|
|
581
|
+
print(json.dumps({'ok': False, 'error': '用法:fin_model.py <assumptions.json>'}, ensure_ascii=False))
|
|
582
|
+
return 2
|
|
583
|
+
try:
|
|
584
|
+
src = argv[1]
|
|
585
|
+
if src.lstrip().startswith('{'): # 直接传 JSON(只读工具不往工作区写临时文件)
|
|
586
|
+
spec = json.loads(src)
|
|
587
|
+
else:
|
|
588
|
+
with open(src, encoding='utf-8') as fh:
|
|
589
|
+
spec = json.load(fh)
|
|
590
|
+
res = run(spec)
|
|
591
|
+
except ModelError as e:
|
|
592
|
+
res = {'ok': False, 'error': str(e)}
|
|
593
|
+
except json.JSONDecodeError as e:
|
|
594
|
+
res = {'ok': False, 'error': f'假设文件不是合法 JSON:{e}'}
|
|
595
|
+
except Exception as e: # 引擎自身出错:如实上报
|
|
596
|
+
res = {'ok': False, 'error': f'测算引擎出错:{type(e).__name__}: {e}'}
|
|
597
|
+
print(json.dumps(res, ensure_ascii=False))
|
|
598
|
+
return 0 if res.get('ok') else 1
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
if __name__ == '__main__':
|
|
602
|
+
sys.exit(main(sys.argv))
|