@huicai/cli 2026.714.0 → 2026.721.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (335) hide show
  1. package/README.md +2 -1
  2. package/bin/huicai.js +83 -0
  3. package/package.json +2 -2
  4. package/skills/ab-testing/SKILL.md +58 -0
  5. package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
  6. package/skills/ab-testing/scripts/ab_test.py +71 -0
  7. package/skills/academic-writing/SKILL.md +54 -0
  8. package/skills/accounting-ops/SKILL.md +68 -0
  9. package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
  10. package/skills/accounting-ops/scripts/acct_tools.py +89 -0
  11. package/skills/ai-agent/SKILL.md +85 -0
  12. package/skills/ai-agent/references/tool-design-checklist.md +51 -0
  13. package/skills/ai-aiops/SKILL.md +73 -0
  14. package/skills/ai-aiops/references/aiops-rollout-checklist.md +36 -0
  15. package/skills/ai-coding/SKILL.md +89 -0
  16. package/skills/ai-coding/references/task-brief-template.md +48 -0
  17. package/skills/ai-compliance/SKILL.md +74 -0
  18. package/skills/ai-compliance/references/genai-compliance-checklist.md +40 -0
  19. package/skills/ai-eval/SKILL.md +80 -0
  20. package/skills/ai-eval/scripts/eval-runner.py +150 -0
  21. package/skills/ai-llm-app/SKILL.md +90 -0
  22. package/skills/ai-llm-app/references/llm-integration-checklist.md +39 -0
  23. package/skills/ai-mlops/SKILL.md +72 -0
  24. package/skills/ai-mlops/scripts/token-cost-calc.py +108 -0
  25. package/skills/ai-multimodal/SKILL.md +86 -0
  26. package/skills/ai-multimodal/scripts/field-validate.py +182 -0
  27. package/skills/ai-prompt/SKILL.md +75 -0
  28. package/skills/ai-prompt/references/prompt-template.md +46 -0
  29. package/skills/ai-rag/SKILL.md +76 -0
  30. package/skills/ai-rag/scripts/chunk-inspect.py +129 -0
  31. package/skills/anomaly-detection/SKILL.md +54 -0
  32. package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
  33. package/skills/anomaly-detection/scripts/anomaly.py +99 -0
  34. package/skills/api-integration/SKILL.md +74 -0
  35. package/skills/api-integration/references/signing-recipes.md +141 -0
  36. package/skills/arch-design/SKILL.md +57 -0
  37. package/skills/arch-distributed/SKILL.md +81 -0
  38. package/skills/arch-distributed/references/idempotency-review.md +43 -0
  39. package/skills/arch-multitenant/SKILL.md +73 -0
  40. package/skills/arch-multitenant/references/tenant-isolation-checklist.md +34 -0
  41. package/skills/audit-assist/SKILL.md +63 -0
  42. package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
  43. package/skills/audit-assist/scripts/audit_sampling.py +93 -0
  44. package/skills/audit-chain/SKILL.md +60 -0
  45. package/skills/banking-credit/SKILL.md +59 -0
  46. package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
  47. package/skills/banking-credit/scripts/credit_metrics.py +53 -0
  48. package/skills/brand-design/SKILL.md +56 -0
  49. package/skills/business-plan/SKILL.md +56 -0
  50. package/skills/capital-ops/SKILL.md +52 -0
  51. package/skills/cicd-pipeline/SKILL.md +85 -0
  52. package/skills/cicd-pipeline/references/pipeline-review-checklist.md +39 -0
  53. package/skills/content-creation/SKILL.md +56 -0
  54. package/skills/corporate-governance/SKILL.md +56 -0
  55. package/skills/crawl-chain/SKILL.md +61 -0
  56. package/skills/customer-service/SKILL.md +51 -0
  57. package/skills/data-analysis/SKILL.md +46 -0
  58. package/skills/data-analysis/scripts/cjk_font.py +56 -0
  59. package/skills/data-chain/SKILL.md +54 -0
  60. package/skills/data-governance/SKILL.md +54 -0
  61. package/skills/data-report-pro/SKILL.md +65 -0
  62. package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
  63. package/skills/data-report-pro/scripts/bigdata.py +79 -0
  64. package/skills/data-streaming/SKILL.md +87 -0
  65. package/skills/data-streaming/references/job-checklist.md +36 -0
  66. package/skills/data-wrangling/SKILL.md +52 -0
  67. package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
  68. package/skills/data-wrangling/scripts/join_tools.py +99 -0
  69. package/skills/db-es/SKILL.md +68 -0
  70. package/skills/db-es/scripts/es-health.sh +72 -0
  71. package/skills/db-migration/SKILL.md +72 -0
  72. package/skills/db-migration/scripts/checksum-mysql.sql +46 -0
  73. package/skills/db-migration/scripts/checksum-postgres.sql +39 -0
  74. package/skills/db-migration/scripts/compare-reports.py +98 -0
  75. package/skills/db-mysql/SKILL.md +78 -0
  76. package/skills/db-mysql/scripts/mysql-health.sql +80 -0
  77. package/skills/db-postgres/SKILL.md +86 -0
  78. package/skills/db-postgres/scripts/pg-health.sql +67 -0
  79. package/skills/db-redis/SKILL.md +83 -0
  80. package/skills/db-redis/scripts/redis-health.sh +92 -0
  81. package/skills/dev-cpp/SKILL.md +94 -0
  82. package/skills/dev-cpp/references/cmake-sanitizer-snippets.md +56 -0
  83. package/skills/dev-cross-platform/SKILL.md +73 -0
  84. package/skills/dev-cross-platform/references/capability-matrix-template.md +39 -0
  85. package/skills/dev-csharp/SKILL.md +96 -0
  86. package/skills/dev-csharp/references/review-checklist.md +37 -0
  87. package/skills/dev-frontend/SKILL.md +91 -0
  88. package/skills/dev-frontend/scripts/bundle-budget.py +106 -0
  89. package/skills/dev-go/SKILL.md +88 -0
  90. package/skills/dev-go/scripts/go-triage.sh +54 -0
  91. package/skills/dev-harmony/SKILL.md +64 -0
  92. package/skills/dev-harmony/references/harmony-checklist.md +40 -0
  93. package/skills/dev-java/SKILL.md +77 -0
  94. package/skills/dev-java/scripts/jvm-triage.sh +84 -0
  95. package/skills/dev-mobile/SKILL.md +82 -0
  96. package/skills/dev-mobile/references/store-compliance-checklist.md +33 -0
  97. package/skills/dev-node/SKILL.md +82 -0
  98. package/skills/dev-node/scripts/node-triage.sh +78 -0
  99. package/skills/dev-php/SKILL.md +86 -0
  100. package/skills/dev-php/scripts/fpm-calc.py +48 -0
  101. package/skills/dev-python/SKILL.md +81 -0
  102. package/skills/dev-python/scripts/venv-doctor.sh +75 -0
  103. package/skills/dev-rust/SKILL.md +84 -0
  104. package/skills/dev-rust/references/cargo-ci-snippets.md +50 -0
  105. package/skills/dev-toolchain/SKILL.md +78 -0
  106. package/skills/dev-toolchain/scripts/secret-scan.sh +83 -0
  107. package/skills/doc-chain/SKILL.md +58 -0
  108. package/skills/doc-compare/SKILL.md +44 -0
  109. package/skills/doc-compare/scripts/compare.py +93 -0
  110. package/skills/docx-author/SKILL.md +39 -0
  111. package/skills/docx-author/scripts/docx_doc.py +161 -0
  112. package/skills/ecommerce-ops/SKILL.md +54 -0
  113. package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
  114. package/skills/ecommerce-ops/scripts/gmv_decompose.py +61 -0
  115. package/skills/education-training/SKILL.md +53 -0
  116. package/skills/ehs-safety/SKILL.md +63 -0
  117. package/skills/email-writing/SKILL.md +52 -0
  118. package/skills/eng-ai/SKILL.md +74 -0
  119. package/skills/eng-ai/references/ai-feature-checklist.md +36 -0
  120. package/skills/eng-architecture/SKILL.md +85 -0
  121. package/skills/eng-architecture/scripts/capacity-calc.py +100 -0
  122. package/skills/eng-automation/SKILL.md +84 -0
  123. package/skills/eng-automation/references/automation-inventory.md +38 -0
  124. package/skills/eng-automation/references/routine-presets.md +73 -0
  125. package/skills/eng-collaboration/SKILL.md +76 -0
  126. package/skills/eng-collaboration/references/collab-templates.md +43 -0
  127. package/skills/eng-complex-env/SKILL.md +84 -0
  128. package/skills/eng-complex-env/references/env-dossier-template.md +52 -0
  129. package/skills/eng-complex-env/scripts/env-recon.sh +152 -0
  130. package/skills/eng-data-platform/SKILL.md +81 -0
  131. package/skills/eng-data-platform/references/table-spec-template.md +39 -0
  132. package/skills/eng-database/SKILL.md +79 -0
  133. package/skills/eng-database/references/schema-review-checklist.md +37 -0
  134. package/skills/eng-develop/SKILL.md +76 -0
  135. package/skills/eng-develop/references/dod-checklist.md +43 -0
  136. package/skills/eng-docs/SKILL.md +71 -0
  137. package/skills/eng-docs/references/runbook-template.md +57 -0
  138. package/skills/eng-efficiency/SKILL.md +75 -0
  139. package/skills/eng-efficiency/scripts/dora-calc.py +104 -0
  140. package/skills/eng-infra/SKILL.md +81 -0
  141. package/skills/eng-infra/references/production-readiness.md +46 -0
  142. package/skills/eng-integration/SKILL.md +82 -0
  143. package/skills/eng-integration/references/integration-dossier-template.md +38 -0
  144. package/skills/eng-legacy/SKILL.md +81 -0
  145. package/skills/eng-legacy/references/modernization-workbook.md +46 -0
  146. package/skills/eng-observability/SKILL.md +78 -0
  147. package/skills/eng-observability/scripts/slo-calc.py +82 -0
  148. package/skills/eng-ops/SKILL.md +74 -0
  149. package/skills/eng-ops/references/postmortem-template.md +67 -0
  150. package/skills/eng-performance/SKILL.md +73 -0
  151. package/skills/eng-performance/scripts/perf-baseline.py +110 -0
  152. package/skills/eng-planning/SKILL.md +79 -0
  153. package/skills/eng-planning/scripts/schedule-calc.py +101 -0
  154. package/skills/eng-quality/SKILL.md +74 -0
  155. package/skills/eng-quality/references/test-plan-templates.md +46 -0
  156. package/skills/eng-release/SKILL.md +78 -0
  157. package/skills/eng-release/references/release-plan-template.md +36 -0
  158. package/skills/eng-reliability/SKILL.md +72 -0
  159. package/skills/eng-reliability/references/resilience-review.md +43 -0
  160. package/skills/eng-requirements/SKILL.md +86 -0
  161. package/skills/eng-requirements/references/story-templates.md +47 -0
  162. package/skills/eng-research/SKILL.md +73 -0
  163. package/skills/eng-research/references/research-templates.md +42 -0
  164. package/skills/eng-security/SKILL.md +73 -0
  165. package/skills/eng-security/references/security-review-checklist.md +33 -0
  166. package/skills/eng-ux/SKILL.md +85 -0
  167. package/skills/eng-ux/references/ux-review-checklist.md +36 -0
  168. package/skills/engineering-chain/SKILL.md +101 -0
  169. package/skills/engineering-chain/references/eng-health-review.md +37 -0
  170. package/skills/env-offline/SKILL.md +81 -0
  171. package/skills/env-offline/scripts/bundle-manifest.sh +94 -0
  172. package/skills/env-private-deploy/SKILL.md +72 -0
  173. package/skills/env-private-deploy/references/delivery-templates.md +35 -0
  174. package/skills/env-xinchuang/SKILL.md +73 -0
  175. package/skills/env-xinchuang/references/adaptation-matrix-template.md +41 -0
  176. package/skills/equipment-tpm/SKILL.md +56 -0
  177. package/skills/equipment-tpm/scripts/oee.py +58 -0
  178. package/skills/esg-report/SKILL.md +58 -0
  179. package/skills/esg-report/scripts/carbon_calc.py +61 -0
  180. package/skills/event-management/SKILL.md +51 -0
  181. package/skills/excel-analysis-chain/SKILL.md +73 -0
  182. package/skills/file-ingest/SKILL.md +51 -0
  183. package/skills/file-ingest/scripts/__pycache__/file_toolkit.cpython-312.pyc +0 -0
  184. package/skills/file-ingest/scripts/file_toolkit.py +404 -0
  185. package/skills/finance-analysis/SKILL.md +60 -0
  186. package/skills/finance-analysis/references/ratios.md +54 -0
  187. package/skills/finance-analysis/scripts/fin_ratios.py +132 -0
  188. package/skills/finance-chain/SKILL.md +59 -0
  189. package/skills/financial-modeling/SKILL.md +58 -0
  190. package/skills/financial-modeling/scripts/fin_model.py +123 -0
  191. package/skills/foreign-trade/SKILL.md +54 -0
  192. package/skills/funnel-cohort/SKILL.md +53 -0
  193. package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
  194. package/skills/funnel-cohort/scripts/funnel.py +105 -0
  195. package/skills/gov-document/SKILL.md +57 -0
  196. package/skills/hr-chain/SKILL.md +51 -0
  197. package/skills/hr-operations/SKILL.md +53 -0
  198. package/skills/info-disclosure/SKILL.md +56 -0
  199. package/skills/infra-gateway/SKILL.md +85 -0
  200. package/skills/infra-gateway/references/golive-checklist.md +36 -0
  201. package/skills/infra-k8s/SKILL.md +78 -0
  202. package/skills/infra-k8s/scripts/k8s-triage.sh +84 -0
  203. package/skills/infra-mq/SKILL.md +87 -0
  204. package/skills/infra-mq/references/mq-review-checklist.md +36 -0
  205. package/skills/insurance-advisory/SKILL.md +57 -0
  206. package/skills/internal-audit/SKILL.md +66 -0
  207. package/skills/ip-management/SKILL.md +54 -0
  208. package/skills/ipo-listing/SKILL.md +60 -0
  209. package/skills/it-audit/SKILL.md +56 -0
  210. package/skills/itsm-ops/SKILL.md +55 -0
  211. package/skills/knowledge-sop/SKILL.md +51 -0
  212. package/skills/legal-chain/SKILL.md +55 -0
  213. package/skills/legal-contract/SKILL.md +58 -0
  214. package/skills/legal-contract/references/contract-checklists.md +59 -0
  215. package/skills/litigation-support/SKILL.md +53 -0
  216. package/skills/logistics-warehouse/SKILL.md +53 -0
  217. package/skills/logistics-warehouse/scripts/freight_check.py +71 -0
  218. package/skills/long-doc-analysis/SKILL.md +52 -0
  219. package/skills/management-accounting/SKILL.md +57 -0
  220. package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
  221. package/skills/management-accounting/scripts/costing.py +90 -0
  222. package/skills/manufacturing-chain/SKILL.md +57 -0
  223. package/skills/manufacturing-quality/SKILL.md +57 -0
  224. package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
  225. package/skills/manufacturing-quality/scripts/spc.py +86 -0
  226. package/skills/marketing-analytics/SKILL.md +59 -0
  227. package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
  228. package/skills/marketing-analytics/scripts/rfm.py +60 -0
  229. package/skills/marketing-chain/SKILL.md +57 -0
  230. package/skills/meeting-minutes/SKILL.md +55 -0
  231. package/skills/metrics-system/SKILL.md +55 -0
  232. package/skills/negotiation-prep/SKILL.md +56 -0
  233. package/skills/obs-prometheus/SKILL.md +74 -0
  234. package/skills/obs-prometheus/references/promql-cheatsheet.md +78 -0
  235. package/skills/office-automation/SKILL.md +54 -0
  236. package/skills/okr-review/SKILL.md +44 -0
  237. package/skills/ops-fleet/SKILL.md +69 -0
  238. package/skills/ops-fleet/scripts/batch-exec-guard.sh +119 -0
  239. package/skills/ops-linux/SKILL.md +69 -0
  240. package/skills/ops-linux/scripts/quick-triage.sh +138 -0
  241. package/skills/optimization-solver/SKILL.md +53 -0
  242. package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
  243. package/skills/optimization-solver/scripts/solver.py +107 -0
  244. package/skills/pdf-toolkit/SKILL.md +43 -0
  245. package/skills/pdf-toolkit/scripts/pdf_ops.py +137 -0
  246. package/skills/pivot-crosstab/SKILL.md +44 -0
  247. package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
  248. package/skills/pivot-crosstab/scripts/pivot_tools.py +90 -0
  249. package/skills/policy-analysis/SKILL.md +47 -0
  250. package/skills/portfolio-analysis/SKILL.md +56 -0
  251. package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
  252. package/skills/portfolio-analysis/scripts/portfolio.py +78 -0
  253. package/skills/ppt-chain/SKILL.md +61 -0
  254. package/skills/pptx-author/SKILL.md +48 -0
  255. package/skills/pptx-author/scripts/pptx_deck.py +193 -0
  256. package/skills/pr-comms/SKILL.md +58 -0
  257. package/skills/pr-comms/references/pr-templates.md +54 -0
  258. package/skills/procurement-supply/SKILL.md +47 -0
  259. package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
  260. package/skills/procurement-supply/scripts/supply.py +71 -0
  261. package/skills/product-design/SKILL.md +66 -0
  262. package/skills/product-design/references/prd-framework.md +60 -0
  263. package/skills/production-planning/SKILL.md +56 -0
  264. package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
  265. package/skills/production-planning/scripts/mrp_capacity.py +70 -0
  266. package/skills/project-management/SKILL.md +53 -0
  267. package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
  268. package/skills/project-management/scripts/cpm.py +84 -0
  269. package/skills/project-proposal/SKILL.md +54 -0
  270. package/skills/project-proposal/scripts/budget_sheet.py +90 -0
  271. package/skills/qa-api-automation/SKILL.md +67 -0
  272. package/skills/qa-api-automation/references/three-layer-skeleton.md +61 -0
  273. package/skills/qa-mobile-automation/SKILL.md +70 -0
  274. package/skills/qa-mobile-automation/references/device-matrix-template.md +34 -0
  275. package/skills/qa-ui-automation/SKILL.md +80 -0
  276. package/skills/qa-ui-automation/references/stability-conventions.md +41 -0
  277. package/skills/quant-analysis/SKILL.md +60 -0
  278. package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
  279. package/skills/quant-analysis/scripts/quant.py +102 -0
  280. package/skills/real-estate-property/SKILL.md +51 -0
  281. package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
  282. package/skills/real-estate-property/scripts/property_calc.py +106 -0
  283. package/skills/regression-forecast/SKILL.md +52 -0
  284. package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
  285. package/skills/regression-forecast/scripts/regress.py +106 -0
  286. package/skills/retail-store-ops/SKILL.md +57 -0
  287. package/skills/retail-store-ops/scripts/store_ops.py +57 -0
  288. package/skills/risk-compliance/SKILL.md +51 -0
  289. package/skills/sec-auth/SKILL.md +89 -0
  290. package/skills/sec-auth/references/auth-review-checklist.md +38 -0
  291. package/skills/sec-compliance/SKILL.md +74 -0
  292. package/skills/sec-compliance/references/dengbao-checklist.md +43 -0
  293. package/skills/securities-chain/SKILL.md +69 -0
  294. package/skills/securities-research/SKILL.md +58 -0
  295. package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
  296. package/skills/securities-research/scripts/valuation.py +93 -0
  297. package/skills/slides-html/SKILL.md +50 -0
  298. package/skills/slides-html/scripts/template.html +117 -0
  299. package/skills/statistical-testing/SKILL.md +51 -0
  300. package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
  301. package/skills/statistical-testing/scripts/stat_tests.py +131 -0
  302. package/skills/strategy-planning/SKILL.md +53 -0
  303. package/skills/supply-chain/SKILL.md +53 -0
  304. package/skills/tax-compliance/SKILL.md +57 -0
  305. package/skills/tax-compliance/scripts/tax_calc.py +90 -0
  306. package/skills/tech-research/SKILL.md +55 -0
  307. package/skills/tech-research/references/research-framework.md +50 -0
  308. package/skills/tender-bidding/SKILL.md +65 -0
  309. package/skills/tender-bidding/references/bid-checklist.md +44 -0
  310. package/skills/translation-pro/SKILL.md +44 -0
  311. package/skills/treasury-mgmt/SKILL.md +56 -0
  312. package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
  313. package/skills/treasury-mgmt/scripts/cashflow_forecast.py +51 -0
  314. package/skills/web-scraping/SKILL.md +81 -0
  315. package/skills/web-search/SKILL.md +67 -0
  316. package/skills/xlsx-analysis/SKILL.md +96 -0
  317. package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
  318. package/skills/xlsx-analysis/scripts/xlsx_style.py +71 -0
  319. package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +335 -0
  320. package/src/agent.js +1 -1
  321. package/src/api.js +1 -1
  322. package/src/codeindex.js +1 -1
  323. package/src/commands.js +1 -1
  324. package/src/config.js +1 -1
  325. package/src/context.js +1 -1
  326. package/src/doccache.js +1 -0
  327. package/src/doctor.js +1 -1
  328. package/src/executors.js +1 -1
  329. package/src/input.js +1 -1
  330. package/src/permissions.js +1 -1
  331. package/src/pyruntime.js +1 -0
  332. package/src/repl.js +1 -1
  333. package/src/scan.js +1 -1
  334. package/src/tools.js +1 -1
  335. package/src/tui/app.js +1 -1
@@ -0,0 +1,404 @@
1
+ # -*- coding: utf-8 -*-
2
+ """通用文件读取工具箱:read_any(path) 把「任意办公文件」变成可用文本+元信息,本地全类型分发、优雅降级。
3
+
4
+ 经 run_python 使用(pandas/openpyxl/xlrd/pypdf/python-docx/python-pptx 已随托管运行时就绪)。
5
+ 覆盖:Excel(xlsx/xlsm/xls)、CSV/TSV、PDF、Word(docx/旧版 doc)、PPT(pptx)、OFD(电子发票/公文)、
6
+ ZIP、HTML/MHTML、RTF、图片(png/jpg 尺寸元信息)、纯文本。绝不抛裸异常——不支持/加密/损坏都回可读原因。
7
+
8
+ r = read_any('attachments/某文件.pdf')
9
+ if r['ok']: print(r['meta']); print(r['text'][:2000])
10
+ else: print('无法直接读取:', r['reason']) # 例如扫描件需 OCR、旧 .doc 需转档
11
+ """
12
+ from __future__ import annotations
13
+ import os, re, io, zipfile, warnings
14
+ warnings.simplefilter('ignore')
15
+
16
+ TEXT_CAP = 200_000 # 单文件返回文本上限,避免超大文件撑爆
17
+
18
+
19
+ def _result(kind, ok, text='', meta='', reason=''):
20
+ return {'kind': kind, 'ok': bool(ok), 'text': (text or '')[:TEXT_CAP], 'meta': meta, 'reason': reason}
21
+
22
+
23
+ def _ext(p):
24
+ b = os.path.basename(p); i = b.rfind('.')
25
+ return b[i:].lower() if i > 0 else ''
26
+
27
+
28
+ def _sniff(path):
29
+ """按**内容**判类(治扩展名错配:.doc 实为 rtf、.rtf 实为 ole2、.xls 实为 html 等常见乱象)。"""
30
+ try:
31
+ n = os.path.getsize(path)
32
+ except OSError:
33
+ return 'missing'
34
+ if n == 0:
35
+ return 'empty'
36
+ with open(path, 'rb') as fh:
37
+ h = fh.read(16)
38
+ if h[:4] == b'PK\x03\x04':
39
+ return 'zip'
40
+ if h[:4] == b'\xd0\xcf\x11\xe0':
41
+ return 'ole2'
42
+ if h[:8] == b'\x89PNG\r\n\x1a\n':
43
+ return 'png'
44
+ if h[:3] == b'\xff\xd8\xff':
45
+ return 'jpeg'
46
+ if h[:5] == b'%PDF-':
47
+ return 'pdf'
48
+ lead = h.lstrip(b'\xef\xbb\xbf \t\r\n').lower() # 去 BOM/空白后看文本签名
49
+ if lead[:5] == b'{\\rtf':
50
+ return 'rtf'
51
+ if lead[:5] in (b'<html', b'<!doc') or lead[:6] == b'<table' or lead[:5] == b'<?xml':
52
+ return 'html'
53
+ return 'other'
54
+
55
+
56
+ # ── 各类型处理器 ─────────────────────────────────────────────────────────────
57
+ def _read_excel(path):
58
+ import pandas as pd
59
+ engines = {'ole2': ['xlrd', None], 'zip': ['openpyxl', None]}.get(_sniff(path), [None, 'openpyxl', 'xlrd'])
60
+ xf, last = None, ''
61
+ for eng in engines:
62
+ try:
63
+ xf = pd.ExcelFile(path) if eng is None else pd.ExcelFile(path, engine=eng); break
64
+ except Exception as ex:
65
+ last = f'{type(ex).__name__}: {str(ex)[:80]}'
66
+ if xf is None:
67
+ if 'Content_Types' in last:
68
+ return _result('excel', False, reason='xlsx 压缩包损坏(缺 [Content_Types].xml),请重新导出')
69
+ if any(k in last.lower() for k in ('ole2', 'encrypt', 'workbook')):
70
+ return _result('excel', False, reason='疑似加密或损坏的旧格式(.xls),需密码或另存为 .xlsx')
71
+ return _result('excel', False, reason=last or '无法识别的表格格式')
72
+ parts = []
73
+ for s in xf.sheet_names[:12]:
74
+ try:
75
+ raw = xf.parse(s, header=None, nrows=6, dtype=object)
76
+ parts.append(f'# 工作表 {s}(约 {xf.parse(s, header=None).shape[0]} 行)\n'
77
+ + raw.dropna(how='all').head(5).to_string(index=False, header=False))
78
+ except Exception as ex:
79
+ parts.append(f'# 工作表 {s} 读取失败:{type(ex).__name__}')
80
+ return _result('excel', True, '\n\n'.join(parts), meta=f'{len(xf.sheet_names)} 个工作表')
81
+
82
+
83
+ def _read_csv(path):
84
+ import pandas as pd
85
+ for enc in ('utf-8', 'gbk', 'utf-8-sig', 'latin-1'):
86
+ try:
87
+ df = pd.read_csv(path, nrows=200, encoding=enc)
88
+ return _result('csv', True, df.head(30).to_string(index=False), meta=f'{df.shape[1]} 列(前 200 行采样,{enc})')
89
+ except UnicodeDecodeError:
90
+ continue
91
+ except Exception as ex:
92
+ return _result('csv', False, reason=f'{type(ex).__name__}: {str(ex)[:80]}')
93
+ return _result('csv', False, reason='无法识别 CSV 编码(试过 utf-8/gbk/latin-1)')
94
+
95
+
96
+ def _read_pdf(path):
97
+ import pypdf
98
+ try:
99
+ r = pypdf.PdfReader(path)
100
+ if r.is_encrypted:
101
+ try:
102
+ r.decrypt('')
103
+ except Exception:
104
+ return _result('pdf', False, meta=f'{len(r.pages)} 页', reason='PDF 已加密,需要密码')
105
+ txt = []
106
+ for pg in r.pages:
107
+ txt.append(pg.extract_text() or '')
108
+ if sum(len(t) for t in txt) > TEXT_CAP:
109
+ break
110
+ body = '\n'.join(txt).strip()
111
+ if body:
112
+ return _result('pdf', True, body, meta=f'{len(r.pages)} 页')
113
+ return _result('pdf', False, meta=f'{len(r.pages)} 页', reason='扫描件/无文本层——需 OCR 或走视觉模型识别')
114
+ except Exception as ex:
115
+ return _result('pdf', False, reason=f'{type(ex).__name__}: {str(ex)[:80]}')
116
+
117
+
118
+ def _read_docx(path):
119
+ import docx
120
+ d = docx.Document(path)
121
+ paras = [p.text for p in d.paragraphs if p.text.strip()]
122
+ for tb in d.tables:
123
+ for row in tb.rows:
124
+ paras.append(' | '.join(c.text for c in row.cells))
125
+ body = '\n'.join(paras)
126
+ return _result('word', bool(body.strip()), body, meta=f'{len(d.paragraphs)} 段 {len(d.tables)} 表',
127
+ reason='' if body.strip() else '文档为空或仅含图片')
128
+
129
+
130
+ def _read_doc(path):
131
+ """真·旧版 .doc(OLE2 二进制)。本地无 antiword/olefile 难可靠解析(勉强解出的多是乱码,
132
+ 宁可不出也不给假文本)——诚实给转档建议,需要正文走平台 read_document(已支持 .doc)。"""
133
+ return _result('word', False, reason='旧版 .doc 二进制(OLE2),本地难可靠解析;请转存 .docx,或用平台 read_document 解析')
134
+
135
+
136
+ def _read_pptx(path):
137
+ import pptx
138
+ pr = pptx.Presentation(path)
139
+ out = []
140
+ for i, sl in enumerate(pr.slides, 1):
141
+ buf = [f'--- 幻灯片 {i} ---']
142
+ for sh in sl.shapes:
143
+ if sh.has_text_frame:
144
+ for para in sh.text_frame.paragraphs:
145
+ t = ''.join(run.text for run in para.runs)
146
+ if t.strip():
147
+ buf.append(t)
148
+ out.append('\n'.join(buf))
149
+ body = '\n'.join(out)
150
+ return _result('ppt', len(pr.slides) > 0, body, meta=f'{len(pr.slides)} 页幻灯片')
151
+
152
+
153
+ def _read_ofd(path):
154
+ """OFD(国产电子发票/公文)本质是 zip+XML 容器:抽各 Content/Document XML 里的 TextCode 文本。"""
155
+ try:
156
+ z = zipfile.ZipFile(path)
157
+ except Exception as ex:
158
+ return _result('ofd', False, reason=f'OFD 容器损坏:{type(ex).__name__}')
159
+ xmls = [n for n in z.namelist() if n.lower().endswith('.xml')]
160
+ chunks = []
161
+ for n in xmls:
162
+ try:
163
+ data = z.read(n).decode('utf-8', 'ignore')
164
+ except Exception:
165
+ continue
166
+ # OFD 文本承载在 <ofd:TextCode>…</ofd:TextCode>(命名空间前缀可能不同)
167
+ codes = re.findall(r'<(?:\w+:)?TextCode[^>]*>([^<]+)</(?:\w+:)?TextCode>', data)
168
+ chunks += [c for c in codes if c.strip()]
169
+ body = '\n'.join(chunks)
170
+ if body.strip():
171
+ return _result('ofd', True, body, meta=f'{len(xmls)} 个 XML,{len(chunks)} 段文本')
172
+ return _result('ofd', False, meta=f'{len(xmls)} 个 XML', reason='OFD 未提取到文本(可能纯图形/签章),可走视觉模型')
173
+
174
+
175
+ def _read_zip(path):
176
+ try:
177
+ z = zipfile.ZipFile(path)
178
+ except Exception as ex:
179
+ return _result('zip', False, reason=f'不是有效的 zip 或已损坏:{type(ex).__name__}')
180
+ names = z.namelist()
181
+ listing = '\n'.join(names[:200]) + ('' if len(names) <= 200 else f'\n…(共 {len(names)} 项)')
182
+ return _result('zip', True, listing, meta=f'{len(names)} 个条目')
183
+
184
+
185
+ # RTF 里不是正文的「目的组」(字体/颜色/样式/信息/图片等),整组跳过
186
+ _RTF_SKIP = frozenset(('fonttbl', 'colortbl', 'stylesheet', 'info', 'pict', 'object', 'themedata',
187
+ 'colorschememapping', 'latentstyles', 'datastore', 'generator', 'operator',
188
+ 'listtable', 'listoverridetable', 'rsidtbl', 'mmath', 'wgrffmtfilter'))
189
+ _RTF_BREAK = {'par': '\n', 'sect': '\n', 'page': '\n', 'line': '\n', 'tab': '\t',
190
+ 'emdash': '—', 'endash': '–', 'lquote': '‘', 'rquote': '’',
191
+ 'ldblquote': '“', 'rdblquote': '”', 'bullet': '•'}
192
+ _RTF_TOK = re.compile(r"\\([a-z]{1,32})(-?\d{1,10})?[ ]?|\\'([0-9a-fA-F]{2})|\\([^a-z])|([{}])|[\r\n]+|([^\\{}\r\n]+)", re.I)
193
+
194
+
195
+ def _rtf_to_text(rtf, cp='gbk'):
196
+ """轻量 RTF→文本:跳过字体/样式等目的组,\\'XX 十六进制按代码页缓冲解码(中文 RTF 多为 GBK),
197
+ \\uN Unicode、\\par 换行;其余控制字丢弃。对付「.doc 其实是 RTF」这类中文公文/回执足够。"""
198
+ out, stack, buf = [], [], bytearray()
199
+ ignorable, ucskip, curskip = False, 1, 0
200
+
201
+ def flush():
202
+ if buf:
203
+ out.append(bytes(buf).decode(cp, 'ignore'))
204
+ buf.clear()
205
+
206
+ for m in _RTF_TOK.finditer(rtf):
207
+ word, arg, hexv, ctrl, brace, text = m.groups()
208
+ if brace == '{':
209
+ flush(); stack.append((ignorable, ucskip)); curskip = 0
210
+ elif brace == '}':
211
+ flush()
212
+ if stack:
213
+ ignorable, ucskip = stack.pop()
214
+ curskip = 0
215
+ elif ctrl:
216
+ flush(); curskip = 0
217
+ if ctrl == '*':
218
+ ignorable = True
219
+ elif not ignorable and ctrl in '{}\\':
220
+ out.append(ctrl)
221
+ elif not ignorable and ctrl == '~':
222
+ out.append(' ')
223
+ elif hexv is not None:
224
+ if curskip > 0:
225
+ curskip -= 1
226
+ elif not ignorable:
227
+ buf.append(int(hexv, 16)) # 缓冲字节,遇非 hex token 再整体按 cp 解码
228
+ elif word is not None:
229
+ flush()
230
+ if word in _RTF_SKIP:
231
+ ignorable = True
232
+ elif word == 'ansicpg' and arg:
233
+ cp = 'cp' + arg # \ansicpg936 → GBK
234
+ elif word == 'uc' and arg:
235
+ ucskip = int(arg)
236
+ elif word == 'u' and arg:
237
+ if not ignorable:
238
+ out.append(chr(int(arg) % 0x10000))
239
+ curskip = ucskip
240
+ elif not ignorable and word in _RTF_BREAK:
241
+ out.append(_RTF_BREAK[word])
242
+ else:
243
+ curskip = 0
244
+ elif text is not None:
245
+ flush()
246
+ if curskip > 0:
247
+ curskip -= len(text)
248
+ elif not ignorable:
249
+ out.append(text)
250
+ flush()
251
+ return re.sub(r'\n{3,}', '\n\n', ''.join(out)).strip()
252
+
253
+
254
+ def _strip_markup(path, kind):
255
+ raw = open(path, 'rb').read()
256
+ if kind == 'rtf':
257
+ s = _rtf_to_text(raw.decode('latin-1', 'ignore'))
258
+ else: # html / mhtml
259
+ for enc in ('utf-8', 'gbk', 'latin-1'):
260
+ try:
261
+ s = raw.decode(enc); break
262
+ except UnicodeDecodeError:
263
+ s = raw.decode('utf-8', 'ignore')
264
+ s = re.sub(r'(?is)<(script|style).*?</\1>', ' ', s)
265
+ s = re.sub(r'(?s)<[^>]+>', ' ', s)
266
+ s = re.sub(r'[ \t\r\f]{2,}', ' ', re.sub(r'\n{3,}', '\n\n', s)).strip()
267
+ return _result(kind, bool(s), s, meta=f'{len(s)} 字')
268
+
269
+
270
+ def _read_html_or_table(path):
271
+ """HTML/MHTML:很多"Excel"其实是 HTML 表格(税务/银行导出常见)。先用 read_html 抽结构化表,抽不到再剥标签取文本。"""
272
+ import pandas as pd
273
+ for enc in ('utf-8', 'gbk', 'utf-8-sig'):
274
+ try:
275
+ tables = pd.read_html(path, encoding=enc)
276
+ if tables:
277
+ body = '\n\n'.join(f'# 表 {i+1}({t.shape[0]}×{t.shape[1]})\n{t.head(20).to_string(index=False)}'
278
+ for i, t in enumerate(tables[:6]))
279
+ return _result('html-table', True, body, meta=f'{len(tables)} 张表')
280
+ except (ValueError, UnicodeDecodeError):
281
+ continue
282
+ except Exception:
283
+ break
284
+ return _strip_markup(path, 'html')
285
+
286
+
287
+ def _read_image(path):
288
+ raw = open(path, 'rb').read(64)
289
+ dim = ''
290
+ if raw[:8] == b'\x89PNG\r\n\x1a\n':
291
+ w = int.from_bytes(raw[16:20], 'big'); h = int.from_bytes(raw[20:24], 'big'); dim = f'{w}x{h}'
292
+ return _result('image', False, meta=dim or '图片', reason='图片文件——本地无 OCR,请走视觉模型识别其内容')
293
+
294
+
295
+ def _read_text(path):
296
+ raw = open(path, 'rb').read()
297
+ for enc in ('utf-8', 'gbk', 'utf-8-sig'):
298
+ try:
299
+ s = raw.decode(enc)
300
+ printable = sum(c.isprintable() or c in '\n\t ' for c in s[:2000])
301
+ if printable / max(1, len(s[:2000])) > 0.85:
302
+ return _result('text', True, s, meta=f'{len(s)} 字({enc})')
303
+ except UnicodeDecodeError:
304
+ continue
305
+ return _result('unknown', False, reason='二进制或未知格式,无法作为文本读取')
306
+
307
+
308
+ # ── 主入口:按内容嗅探 + 扩展名分发 ──────────────────────────────────────────
309
+ def read_any(path):
310
+ """任意文件 → {kind, ok, text, meta, reason}。绝不抛异常,不支持/加密/损坏均以 ok=False + reason 返回。"""
311
+ if not os.path.exists(path):
312
+ return _result('missing', False, reason='文件不存在')
313
+ sniff = _sniff(path)
314
+ if sniff == 'empty':
315
+ return _result('empty', False, reason='空文件(0 字节)')
316
+ e = _ext(path)
317
+ try:
318
+ # ① 内容签名优先(扩展名错配时以真实内容为准)
319
+ if sniff == 'pdf':
320
+ return _read_pdf(path)
321
+ if sniff in ('png', 'jpeg'):
322
+ return _read_image(path)
323
+ if sniff == 'rtf': # .doc 常是伪装的 rtf
324
+ return _strip_markup(path, 'rtf')
325
+ if sniff == 'html': # .xls 常是伪装的 html 表格
326
+ return _read_html_or_table(path)
327
+ if sniff == 'zip': # 真 zip 容器 → 按扩展名细分
328
+ if e in ('.xlsx', '.xlsm'):
329
+ return _read_excel(path)
330
+ if e == '.docx':
331
+ return _read_docx(path)
332
+ if e == '.pptx':
333
+ return _read_pptx(path)
334
+ if e == '.ofd':
335
+ return _read_ofd(path)
336
+ return _read_zip(path)
337
+ if sniff == 'ole2': # 真 OLE2 二进制 → 按扩展名细分
338
+ if e in ('.xls', '.xlsm'):
339
+ return _read_excel(path)
340
+ if e == '.doc':
341
+ return _read_doc(path)
342
+ r = _read_excel(path) # 未知 OLE2:先当表格,失败再当 doc
343
+ return r if r['ok'] else _read_doc(path)
344
+ # ② 内容无明确签名 → 回退扩展名
345
+ if e in ('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp'):
346
+ return _read_image(path)
347
+ if e in ('.xlsx', '.xlsm', '.xls'):
348
+ return _read_excel(path)
349
+ if e in ('.csv', '.tsv'):
350
+ return _read_csv(path)
351
+ if e == '.docx':
352
+ return _read_docx(path)
353
+ if e == '.doc':
354
+ return _read_doc(path)
355
+ if e == '.pptx':
356
+ return _read_pptx(path)
357
+ if e == '.ofd':
358
+ return _read_ofd(path)
359
+ if e in ('.html', '.htm', '.mhtml'):
360
+ return _read_html_or_table(path)
361
+ if e == '.rtf':
362
+ return _strip_markup(path, 'rtf')
363
+ return _read_text(path)
364
+ except Exception as ex: # 兜底:任何未预料异常都转成可读原因,绝不抛给调用方
365
+ return _result(e or sniff, False, reason=f'读取失败 {type(ex).__name__}: {str(ex)[:100]}')
366
+
367
+
368
+ def _selftest():
369
+ import sys, tempfile, zipfile as zf
370
+ d = tempfile.mkdtemp()
371
+ j = lambda n: os.path.join(d, n)
372
+ # csv
373
+ open(j('a.csv'), 'w', encoding='utf-8').write('月份,金额\n2026-01,1000\n2026-02,2000\n')
374
+ assert read_any(j('a.csv'))['ok']
375
+ # rtf(GBK 十六进制转义)——宋 = \'cb\'ce 在此用 ASCII 验证控制字/组跳过与正文提取
376
+ open(j('b.rtf'), 'w', encoding='latin-1').write(r'{\rtf1\ansi{\fonttbl{\f0 Arial;}}\f0 Hello \b World\b0 \par End}')
377
+ rt = read_any(j('b.rtf'))
378
+ assert rt['ok'] and 'Hello' in rt['text'] and 'World' in rt['text'] and 'fonttbl' not in rt['text'], rt
379
+ # html 表格
380
+ open(j('c.html'), 'w', encoding='utf-8').write('<html><body><table><tr><td>甲</td><td>乙</td></tr><tr><td>1</td><td>2</td></tr></table></body></html>')
381
+ assert read_any(j('c.html'))['ok']
382
+ # zip
383
+ with zf.ZipFile(j('d.zip'), 'w') as z:
384
+ z.writestr('inner.txt', 'x')
385
+ assert read_any(j('d.zip'))['ok']
386
+ # 扩展名错配:.doc 内容其实是 rtf → 按内容仍识别为 rtf 并读出正文(中文经 GBK 十六进制转义)
387
+ open(j('e.doc'), 'wb').write(rb"{\rtf1\ansi\ansicpg936 \'b1\'a8\'cf\'fa\'b5\'a5\par}") # \'b1..=“报销单”GBK
388
+ ed = read_any(j('e.doc'))
389
+ assert ed['kind'] == 'rtf' and '报销单' in ed['text'], ed
390
+ # 空文件 / 缺失 → 优雅 ok=False
391
+ open(j('f.xlsx'), 'wb').close()
392
+ assert not read_any(j('f.xlsx'))['ok']
393
+ assert not read_any(j('zzz.pdf'))['ok']
394
+ print('✅ file_toolkit 自测通过')
395
+
396
+
397
+ if __name__ == '__main__':
398
+ import sys
399
+ if len(sys.argv) < 2:
400
+ _selftest()
401
+ else:
402
+ r = read_any(sys.argv[1])
403
+ print('kind=%s ok=%s meta=%s' % (r['kind'], r['ok'], r['meta']))
404
+ print('reason:', r['reason']) if not r['ok'] else print(r['text'][:800])
@@ -0,0 +1,60 @@
1
+ ---
2
+ name: finance-analysis
3
+ description: 财务分析(三表勾稽、比率体系、杜邦分解、现金流质量、财务预警、财务月报/年报解读)。当用户给出资产负债表/利润表/现金流量表或说"财务分析/财报解读/经营分析/预算执行"时使用。
4
+ version: 1.0.0
5
+ ---
6
+
7
+ # 财务分析(经 run_python;先勾稽后分析,无勾稽不出结论)
8
+
9
+ ## 工作流程(强制)
10
+
11
+ 1. **取数与口径**:确认报表口径(合并/母公司、审计/未审、单位、期间);多期数据整成
12
+ 「科目 × 期间」长表。缺口径先问,不猜。
13
+ 2. **勾稽校验(第一道闸)**:`read_skill_file` 读 `scripts/fin_ratios.py`,跑 `check_articulation()`——
14
+ 资产=负债+所有者权益;三表间:净利润(利润表) ↔ 现金流量表补充资料起点、
15
+ 现金及等价物净增加 ↔ 资产负债表货币资金变动(允差内)。**勾稽不过=数据有问题,
16
+ 停下来报告差异,绝不带病分析**。
17
+ 3. **比率体系**:`compute_ratios()` 一键产出四维比率(見 `references/ratios.md` 指标字典与参考区间):
18
+ 偿债(流动/速动/资产负债率/利息保障)、营运(应收/存货/总资产周转)、
19
+ 盈利(毛利率/净利率/ROE/ROA)、成长(收入/净利/资产增速)。多期自动算趋势。
20
+ 4. **杜邦分解**:`dupont()` 把 ROE 拆为 净利率 × 总资产周转 × 权益乘数,逐期对比定位驱动因子——
21
+ 「ROE 降是赚钱能力降、效率降还是杠杆降」必须给出定量答案。
22
+ 5. **现金流质量**:净利润现金含量(经营现金流/净利润)、收现比(销售收现/营收);
23
+ 连续两期 <0.8 必须专段分析(应收积压/存货积压/激进确认)。
24
+ 6. **预警**:`altman_z()`(制造业口径 Z-score,非制造业如实注明不适用)+ 规则预警
25
+ (资产负债率>70%、速动<1、应收周转天数同比+30% 等,阈值见字典,行业不同要调)。
26
+ 7. **成稿**:Excel 结果簿(原数/勾稽/比率/杜邦/预警 各一 Sheet,xlsx-analysis 样式)+
27
+ Word 分析报告(docx-author:结论先行→分维度论证→风险与建议,每个判断带数字)+ 趋势图(data-analysis)。
28
+
29
+ ## 领域要点(写进结论前自查)
30
+
31
+ - 比率**孤立无意义**:必须给对比锚(上期/同业/预算),无锚就向用户要或明示局限;
32
+ - 一次性损益(资产处置/政府补助)要剔除后看经常性盈利能力;
33
+ - 应收与收入背离(收入+20%、应收+60%)是最经典的质量红旗;
34
+ - 存贷双高(大额货币资金 + 大额有息负债并存)要点名存疑;
35
+ - 负数/零分母(权益为负、EBIT<0)时比率如实标注"不适用"而非硬算。
36
+
37
+ ## 长任务作业法
38
+
39
+ 年报级分析(多年多主体)按阶段推进:取数标准化 → 勾稽 → 单期比率 → 多期趋势 → 专题
40
+ (现金流/减值/关联交易)→ 成稿;每阶段中间表落盘,todo_write 跟踪。
41
+
42
+ ## 边界
43
+
44
+ - 你做的是**分析**不是审计鉴证:结论措辞用"数据显示/存在迹象",不用"证实/查实";
45
+ - 估值类诉求转 securities-research;造假核查类转 audit-assist 的程序思路并声明局限。
46
+
47
+ ## 验收自查
48
+
49
+ - [ ] 勾稽校验通过或差异已如实报告
50
+ - [ ] 每个比率结论都有对比锚与数字
51
+ - [ ] 杜邦分解定位到了驱动因子
52
+ - [ ] 一次性损益已识别剔除;不适用指标已标注
53
+ - [ ] Excel 结果簿 + Word 报告 + 图三件套齐且同源
54
+
55
+ ## 附件
56
+
57
+ - `scripts/fin_ratios.py` — 勾稽校验/四维比率/杜邦/Z-score/预警,输入「科目×期间」表;可直接执行自测
58
+ - `references/ratios.md` — 指标字典:公式、口径陷阱、常用参考区间与红旗信号
59
+
60
+ > 本技能属**财务技能链**——岗位路由与端到端工作流(月结 · 审计 · 资金 · 投资)见 `finance-chain`。
@@ -0,0 +1,54 @@
1
+ # 财务指标字典(公式 · 口径陷阱 · 通用参考区间 · 红旗信号)
2
+
3
+ > 参考区间为跨行业通用经验值,**结论前必须按行业校准**(重资产/贸易/软件差异巨大);
4
+ > 有同业或上期数据时,永远优先用相对比较而非绝对区间。
5
+
6
+ ## 偿债能力
7
+
8
+ | 指标 | 公式 | 通用参考 | 口径陷阱 |
9
+ |---|---|---|---|
10
+ | 流动比率 | 流动资产/流动负债 | 1.5–2.5 | 含大额存货/一年内到期非流动负债时失真 |
11
+ | 速动比率 | (流动资产-存货)/流动负债 | ≥1 | 预付款项大的企业建议一并剔除 |
12
+ | 资产负债率 | 负债/资产 | 40%–70%(行业差异极大) | 含大额预收(合同负债)的要拆看:经营性负债≠债务压力 |
13
+ | 利息保障倍数 | EBIT/利息费用 | ≥3 | 利息费用取"费用化利息",财务费用含汇兑/手续费要拆 |
14
+
15
+ ## 营运能力
16
+
17
+ | 指标 | 公式 | 红旗 |
18
+ |---|---|---|
19
+ | 应收周转天数 | 应收账款×365/营业收入 | 同比 +30% 以上;显著慢于同业 |
20
+ | 存货周转天数 | 存货×365/营业成本 | 持续走高 + 毛利率反常稳定(跌价准备计提不足嫌疑) |
21
+ | 总资产周转率 | 营业收入/总资产 | 骤降=资产虚增或收入失速,两头都要查 |
22
+
23
+ 严谨口径用**平均余额**(期初+期末)/2;快速分析用期末口径务必注明。
24
+
25
+ ## 盈利能力
26
+
27
+ | 指标 | 公式 | 陷阱 |
28
+ |---|---|---|
29
+ | 毛利率 | (收入-成本)/收入 | 骤升要查成本资本化/存货跌价转回;显著高于同业要有解释 |
30
+ | 净利率 | 净利润/收入 | 剔除一次性损益看经常性净利率 |
31
+ | ROE | 净利润/所有者权益 | 权益为负不适用;高 ROE 先看杜邦是不是纯靠杠杆 |
32
+ | 净利润现金含量 | 经营现金净流/净利润 | <0.8 连续两期=盈利质量红旗;>1.5 也要看是否压占上游 |
33
+ | 收现比 | 销售商品收到的现金/营业收入 | 长期 <1(含税口径应 >1)= 收入确认激进或回款差 |
34
+
35
+ ## 成长与结构
36
+
37
+ - 收入增速 vs 应收增速:应收增速 > 收入增速 × 1.5 → 红旗;
38
+ - 收入增速 vs 经营现金流增速长期背离 → 红旗;
39
+ - 存贷双高:货币资金与有息负债同时高企 → 资金真实性/受限情况必查;
40
+ - 商誉/总资产 > 30% → 减值风险专段;
41
+ - 其他应收款异常大 → 资金占用嫌疑。
42
+
43
+ ## 预警模型
44
+
45
+ - **Altman Z**(制造业):1.2×营运资本/总资产 + 1.4×留存收益/总资产 + 3.3×EBIT/总资产
46
+ + 0.6×权益市值/总负债 + 1.0×收入/总资产;<1.8 危险、1.8–2.99 灰色、>2.99 安全。
47
+ 非上市用账面权益近似并注明;非制造业用修正版或声明不适用。
48
+ - **Beneish M-score 思路**(造假倾向,做提示不做定论):重点看 DSRI(应收/收入指数)、
49
+ GMI(毛利指数)、AQI(资产质量)、SGI(收入增长)——多项同时异常时提示深查,措辞用"存在特征"。
50
+
51
+ ## 报告措辞规范
52
+
53
+ - 数据显示/呈现/存在…迹象(✓);证实/查实/确定造假(✗——那是审计与监管的事)
54
+ - 每个判断句式:结论 + 数字 + 对比锚(上期/同业/预算)
@@ -0,0 +1,132 @@
1
+ # 财务分析计算件(经 run_python 执行):勾稽校验 / 四维比率 / 杜邦分解 / Altman Z / 规则预警。
2
+ # 输入约定:DataFrame,行=科目(中文标准名),列=期间(如 2024/2025),单位自定但需一致。
3
+ # 直接执行本文件 = 自测:内置两期样例三表全流程跑通并断言。
4
+ import pandas as pd
5
+ import numpy as np
6
+
7
+ TOL = 0.01 # 勾稽允差(同单位)
8
+
9
+
10
+ def g(df, item, period):
11
+ """取数(缺科目返回 NaN 而非报错,由调用侧决定如何处理)。"""
12
+ try:
13
+ return float(df.loc[item, period])
14
+ except KeyError:
15
+ return np.nan
16
+
17
+
18
+ def check_articulation(bs, is_, cf, period):
19
+ """三表勾稽:资产=负债+权益;现金净增加=期末-期初货币资金。返回 [(项目, 差额, 是否通过)]。"""
20
+ out = []
21
+ a = g(bs, '资产总计', period)
22
+ le = g(bs, '负债合计', period) + g(bs, '所有者权益合计', period)
23
+ out.append(('资产=负债+权益', a - le, abs(a - le) <= max(TOL, abs(a) * 1e-4)))
24
+ dcash = g(cf, '现金及现金等价物净增加额', period)
25
+ bs_d = g(bs, '货币资金', period) - g(bs, '货币资金_期初', period)
26
+ if not np.isnan(dcash) and not np.isnan(bs_d):
27
+ out.append(('现金净增加↔货币资金变动', dcash - bs_d, abs(dcash - bs_d) <= max(TOL, abs(dcash) * 0.01)))
28
+ for name, gap, ok in out:
29
+ print(f"[勾稽] {name}: 差额 {gap:,.2f} {'✓' if ok else '✗ 不通过'}")
30
+ return out
31
+
32
+
33
+ def compute_ratios(bs, is_, cf, periods):
34
+ """四维比率表(行=指标,列=期间)。分母≤0 一律 NaN(如实不适用)。"""
35
+ rows = {}
36
+ for p in periods:
37
+ rev = g(is_, '营业收入', p)
38
+ ni = g(is_, '净利润', p)
39
+ assets = g(bs, '资产总计', p)
40
+ equity = g(bs, '所有者权益合计', p)
41
+ cur_a, cur_l = g(bs, '流动资产合计', p), g(bs, '流动负债合计', p)
42
+ inv, ar = g(bs, '存货', p), g(bs, '应收账款', p)
43
+ ocf = g(cf, '经营活动产生的现金流量净额', p)
44
+ d = lambda x, y: (x / y) if (y and not np.isnan(y) and y > 0) else np.nan
45
+ rows.setdefault('流动比率', {})[p] = d(cur_a, cur_l)
46
+ rows.setdefault('速动比率', {})[p] = d(cur_a - (inv if not np.isnan(inv) else 0), cur_l)
47
+ rows.setdefault('资产负债率%', {})[p] = d(g(bs, '负债合计', p), assets) * 100 if assets else np.nan
48
+ rows.setdefault('应收周转天数', {})[p] = d(ar * 365, rev)
49
+ rows.setdefault('存货周转天数', {})[p] = d(inv * 365, g(is_, '营业成本', p))
50
+ rows.setdefault('总资产周转率', {})[p] = d(rev, assets)
51
+ rows.setdefault('毛利率%', {})[p] = d(rev - g(is_, '营业成本', p), rev) * 100 if rev else np.nan
52
+ rows.setdefault('净利率%', {})[p] = d(ni, rev) * 100 if rev else np.nan
53
+ rows.setdefault('ROE%', {})[p] = d(ni, equity) * 100 if equity else np.nan
54
+ rows.setdefault('ROA%', {})[p] = d(ni, assets) * 100 if assets else np.nan
55
+ rows.setdefault('净利润现金含量', {})[p] = d(ocf, ni) if (ni and ni > 0) else np.nan
56
+ out = pd.DataFrame(rows).T[periods].round(2)
57
+ print('[比率]\n' + out.to_markdown())
58
+ return out
59
+
60
+
61
+ def dupont(bs, is_, periods):
62
+ """杜邦:ROE = 净利率 × 总资产周转 × 权益乘数(期末口径,简化版——严谨场合用平均余额并注明)。"""
63
+ rows = []
64
+ for p in periods:
65
+ rev, ni = g(is_, '营业收入', p), g(is_, '净利润', p)
66
+ assets, eq = g(bs, '资产总计', p), g(bs, '所有者权益合计', p)
67
+ npm = ni / rev if rev else np.nan
68
+ tat = rev / assets if assets else np.nan
69
+ em = assets / eq if eq and eq > 0 else np.nan
70
+ rows.append({'期间': p, '净利率': round(npm, 4), '总资产周转': round(tat, 4),
71
+ '权益乘数': round(em, 4), 'ROE(重构)': round(npm * tat * em, 4)})
72
+ out = pd.DataFrame(rows)
73
+ print('[杜邦]\n' + out.to_markdown(index=False))
74
+ return out
75
+
76
+
77
+ def altman_z(bs, is_, period, market_cap=None):
78
+ """Altman Z(制造业原始口径)。market_cap 缺省用账面权益近似并注明。Z<1.8 危险 / >2.99 安全。"""
79
+ ta = g(bs, '资产总计', period)
80
+ wc = g(bs, '流动资产合计', period) - g(bs, '流动负债合计', period)
81
+ re = g(bs, '未分配利润', period)
82
+ ebit = g(is_, '利润总额', period) + g(is_, '财务费用', period)
83
+ tl = g(bs, '负债合计', period)
84
+ mv = market_cap if market_cap else g(bs, '所有者权益合计', period)
85
+ z = 1.2 * wc / ta + 1.4 * re / ta + 3.3 * ebit / ta + 0.6 * mv / tl + 1.0 * g(is_, '营业收入', period) / ta
86
+ tag = '危险区' if z < 1.8 else ('灰色区' if z < 2.99 else '安全区')
87
+ print(f"[Z-score] {period}: {z:.2f}({tag}{',市值用账面权益近似' if not market_cap else ''})")
88
+ return z
89
+
90
+
91
+ def rule_alerts(ratios, prev=None, cur=None):
92
+ """规则预警(阈值为通用参考,行业不同需调整——见 references/ratios.md)。"""
93
+ alerts = []
94
+ p = cur or ratios.columns[-1]
95
+ v = lambda k: ratios.loc[k, p] if k in ratios.index else np.nan
96
+ if v('资产负债率%') > 70: alerts.append(f"资产负债率 {v('资产负债率%')}% > 70%")
97
+ if v('速动比率') < 1: alerts.append(f"速动比率 {v('速动比率')} < 1")
98
+ if v('净利润现金含量') < 0.8: alerts.append(f"净利润现金含量 {v('净利润现金含量')} < 0.8(盈利质量存疑)")
99
+ if prev and '应收周转天数' in ratios.index:
100
+ a, b = ratios.loc['应收周转天数', prev], ratios.loc['应收周转天数', p]
101
+ if b > a * 1.3: alerts.append(f'应收周转天数 {a}→{b}(+{(b / a - 1) * 100:.0f}%,回款恶化)')
102
+ for x in alerts:
103
+ print('[预警] ' + x)
104
+ if not alerts:
105
+ print('[预警] 无触发(阈值为通用参考)')
106
+ return alerts
107
+
108
+
109
+ if __name__ == '__main__':
110
+ P = ['2024', '2025']
111
+ bs = pd.DataFrame({
112
+ '2024': [1000, 520, 480, 600, 300, 150, 180, 120, 200],
113
+ '2025': [1300, 700, 600, 780, 430, 260, 240, 160, 150],
114
+ }, index=['资产总计', '负债合计', '所有者权益合计', '流动资产合计', '流动负债合计',
115
+ '存货', '应收账款', '未分配利润', '货币资金'])
116
+ bs.loc['货币资金_期初'] = [180, 200]
117
+ is_ = pd.DataFrame({
118
+ '2024': [900, 600, 90, 110, 12],
119
+ '2025': [1100, 760, 96, 120, 18],
120
+ }, index=['营业收入', '营业成本', '净利润', '利润总额', '财务费用'])
121
+ cf = pd.DataFrame({'2024': [70, 20], '2025': [60, -50]},
122
+ index=['经营活动产生的现金流量净额', '现金及现金等价物净增加额'])
123
+ art = check_articulation(bs, is_, cf, '2025')
124
+ assert all(ok for _, _, ok in art), '样例勾稽应全过'
125
+ r = compute_ratios(bs, is_, cf, P)
126
+ assert abs(r.loc['ROE%', '2025'] - 16.0) < 0.1
127
+ dp = dupont(bs, is_, P)
128
+ assert abs(dp.iloc[-1]['ROE(重构)'] - 0.16) < 0.001
129
+ altman_z(bs, is_, '2025')
130
+ al = rule_alerts(r, prev='2024', cur='2025')
131
+ assert any('净利润现金含量' in x for x in al) # 60/96=0.63 应触发
132
+ print('OK fin_ratios.py')