dsh-crwu-workbench 0.0.10

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 (479) hide show
  1. package/CHANGELOG.md +1516 -0
  2. package/LICENSE +26 -0
  3. package/README.en.md +204 -0
  4. package/README.md +521 -0
  5. package/SECURITY.md +39 -0
  6. package/bin/darwin-arm64/crwu +0 -0
  7. package/bin/darwin-arm64/dws +0 -0
  8. package/bin/darwin-arm64/ossutil +0 -0
  9. package/bin/manifest.json +82 -0
  10. package/bin/win32-x64/crwu.exe +0 -0
  11. package/bin/win32-x64/dws.exe +4 -0
  12. package/bin/win32-x64/ossutil.exe +0 -0
  13. package/common/skills/.synced.json +8 -0
  14. package/common/skills/crwu-dws/SKILL.md +208 -0
  15. package/common/skills/crwu-dws/references/00-/347/233/256/345/275/225/345/277/253/347/205/247schema.md +113 -0
  16. package/common/skills/crwu-dws/references/01-/345/256/241/346/240/270/344/270/213/350/275/275/344/270/216manifest/350/247/204/350/214/203.md +115 -0
  17. package/common/skills/crwu-dws/references/02-/347/274/223/345/255/230/344/270/216/345/205/234/345/272/225/346/237/245/346/211/276/350/247/204/350/214/203.md +119 -0
  18. package/common/skills/crwu-dws/scripts/test_dws_source_contract.py +493 -0
  19. package/common/skills/crwu-h3yun-login/SKILL.md +130 -0
  20. package/common/skills/crwu-h3yun-query/SKILL.md +192 -0
  21. package/config/crwu-workbench.yml +26 -0
  22. package/cordis.patch.yml +46 -0
  23. package/lib/client.js +10346 -0
  24. package/lib/index.js +11584 -0
  25. package/package.json +119 -0
  26. package/scripts/prepare.mjs +59 -0
  27. package/skills/README.md +26 -0
  28. package/skills/crwu/crwu-audit/SKILL.md +152 -0
  29. package/skills/crwu/crwu-audit/references/00-input-and-route-profile.md +232 -0
  30. package/skills/crwu/crwu-audit/references/01-report-id-resolution.md +13 -0
  31. package/skills/crwu/crwu-audit/references/02-scope-classification.md +25 -0
  32. package/skills/crwu/crwu-audit/references/03-asset-classification.md +27 -0
  33. package/skills/crwu/crwu-audit/references/04-business-classification.md +63 -0
  34. package/skills/crwu/crwu-audit/references/05-method-classification.md +66 -0
  35. package/skills/crwu/crwu-audit/references/06-overlay-classification.md +42 -0
  36. package/skills/crwu/crwu-audit/references/07-skill-registry.md +57 -0
  37. package/skills/crwu/crwu-audit/references/08-union-dispatch-rules.md +142 -0
  38. package/skills/crwu/crwu-audit/references/09-review-risk-classification.md +85 -0
  39. package/skills/crwu/crwu-audit/references/10-capability-gap-proposal.md +68 -0
  40. package/skills/crwu/crwu-audit/references/11-html-delivery-spec.md +881 -0
  41. package/skills/crwu/crwu-audit/references/12-leaf-common-contract.md +202 -0
  42. package/skills/crwu/crwu-audit/references/13-dingtalk-result-publish.md +88 -0
  43. package/skills/crwu/crwu-audit/references/14-orchestration-workflow.md +52 -0
  44. package/skills/crwu/crwu-audit/references/99-maintenance.md +62 -0
  45. package/skills/crwu/crwu-audit/scripts/README.md +157 -0
  46. package/skills/crwu/crwu-audit/scripts/audit_delivery.py +2736 -0
  47. package/skills/crwu/crwu-audit/scripts/audit_result.schema.json +1659 -0
  48. package/skills/crwu/crwu-audit/scripts/examples/audit-result.sample.json +739 -0
  49. package/skills/crwu/crwu-audit/scripts/fetch_review_records.py +85 -0
  50. package/skills/crwu/crwu-audit/scripts/media_extract.py +513 -0
  51. package/skills/crwu/crwu-audit/scripts/prepare_materials.py +1252 -0
  52. package/skills/crwu/crwu-audit/scripts/test_audit_delivery.py +1678 -0
  53. package/skills/crwu/crwu-audit/scripts/test_audit_multiaxis_router.py +1339 -0
  54. package/skills/crwu/crwu-audit/scripts/test_fetch_review_records.py +77 -0
  55. package/skills/crwu/crwu-audit/scripts/test_media_extract.py +332 -0
  56. package/skills/crwu/crwu-audit/scripts/test_prepare_materials.py +1081 -0
  57. package/skills/crwu/crwu-audit/template/audit-report.html +435 -0
  58. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/SKILL.md +38 -0
  59. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/00-applicability.md +28 -0
  60. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/01-kb-assembly.md +31 -0
  61. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/02-review-focus.md +21 -0
  62. package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/03-common-contract.md +202 -0
  63. package/skills/crwu/crwu-audit-asset-debt/SKILL.md +38 -0
  64. package/skills/crwu/crwu-audit-asset-debt/references/00-applicability.md +40 -0
  65. package/skills/crwu/crwu-audit-asset-debt/references/01-kb-assembly.md +41 -0
  66. package/skills/crwu/crwu-audit-asset-debt/references/02-review-focus.md +54 -0
  67. package/skills/crwu/crwu-audit-asset-debt/references/03-common-contract.md +202 -0
  68. package/skills/crwu/crwu-audit-asset-enterprise-value/SKILL.md +38 -0
  69. package/skills/crwu/crwu-audit-asset-enterprise-value/references/00-applicability.md +31 -0
  70. package/skills/crwu/crwu-audit-asset-enterprise-value/references/01-kb-assembly.md +35 -0
  71. package/skills/crwu/crwu-audit-asset-enterprise-value/references/02-review-focus.md +33 -0
  72. package/skills/crwu/crwu-audit-asset-enterprise-value/references/03-common-contract.md +202 -0
  73. package/skills/crwu/crwu-audit-asset-equipment/SKILL.md +38 -0
  74. package/skills/crwu/crwu-audit-asset-equipment/references/00-applicability.md +40 -0
  75. package/skills/crwu/crwu-audit-asset-equipment/references/01-kb-assembly.md +39 -0
  76. package/skills/crwu/crwu-audit-asset-equipment/references/02-review-focus.md +52 -0
  77. package/skills/crwu/crwu-audit-asset-equipment/references/03-common-contract.md +202 -0
  78. package/skills/crwu/crwu-audit-asset-intangible/SKILL.md +38 -0
  79. package/skills/crwu/crwu-audit-asset-intangible/references/00-applicability.md +36 -0
  80. package/skills/crwu/crwu-audit-asset-intangible/references/01-kb-assembly.md +40 -0
  81. package/skills/crwu/crwu-audit-asset-intangible/references/02-review-focus.md +68 -0
  82. package/skills/crwu/crwu-audit-asset-intangible/references/03-common-contract.md +202 -0
  83. package/skills/crwu/crwu-audit-asset-inventory/SKILL.md +38 -0
  84. package/skills/crwu/crwu-audit-asset-inventory/references/00-applicability.md +28 -0
  85. package/skills/crwu/crwu-audit-asset-inventory/references/01-kb-assembly.md +31 -0
  86. package/skills/crwu/crwu-audit-asset-inventory/references/02-review-focus.md +21 -0
  87. package/skills/crwu/crwu-audit-asset-inventory/references/03-common-contract.md +202 -0
  88. package/skills/crwu/crwu-audit-asset-mining-right/SKILL.md +38 -0
  89. package/skills/crwu/crwu-audit-asset-mining-right/references/00-applicability.md +31 -0
  90. package/skills/crwu/crwu-audit-asset-mining-right/references/01-kb-assembly.md +35 -0
  91. package/skills/crwu/crwu-audit-asset-mining-right/references/02-review-focus.md +33 -0
  92. package/skills/crwu/crwu-audit-asset-mining-right/references/03-common-contract.md +202 -0
  93. package/skills/crwu/crwu-audit-asset-other/SKILL.md +38 -0
  94. package/skills/crwu/crwu-audit-asset-other/references/00-applicability.md +28 -0
  95. package/skills/crwu/crwu-audit-asset-other/references/01-kb-assembly.md +31 -0
  96. package/skills/crwu/crwu-audit-asset-other/references/02-review-focus.md +21 -0
  97. package/skills/crwu/crwu-audit-asset-other/references/03-common-contract.md +202 -0
  98. package/skills/crwu/crwu-audit-asset-portfolio/SKILL.md +38 -0
  99. package/skills/crwu/crwu-audit-asset-portfolio/references/00-applicability.md +28 -0
  100. package/skills/crwu/crwu-audit-asset-portfolio/references/01-kb-assembly.md +31 -0
  101. package/skills/crwu/crwu-audit-asset-portfolio/references/02-review-focus.md +21 -0
  102. package/skills/crwu/crwu-audit-asset-portfolio/references/03-common-contract.md +202 -0
  103. package/skills/crwu/crwu-audit-asset-realestate/SKILL.md +38 -0
  104. package/skills/crwu/crwu-audit-asset-realestate/references/00-applicability.md +43 -0
  105. package/skills/crwu/crwu-audit-asset-realestate/references/01-kb-assembly.md +75 -0
  106. package/skills/crwu/crwu-audit-asset-realestate/references/02-review-focus.md +93 -0
  107. package/skills/crwu/crwu-audit-asset-realestate/references/03-common-contract.md +202 -0
  108. package/skills/crwu/crwu-audit-asset-scrap-materials/SKILL.md +38 -0
  109. package/skills/crwu/crwu-audit-asset-scrap-materials/references/00-applicability.md +28 -0
  110. package/skills/crwu/crwu-audit-asset-scrap-materials/references/01-kb-assembly.md +31 -0
  111. package/skills/crwu/crwu-audit-asset-scrap-materials/references/02-review-focus.md +21 -0
  112. package/skills/crwu/crwu-audit-asset-scrap-materials/references/03-common-contract.md +202 -0
  113. package/skills/crwu/crwu-audit-asset-transport-equipment/SKILL.md +38 -0
  114. package/skills/crwu/crwu-audit-asset-transport-equipment/references/00-applicability.md +31 -0
  115. package/skills/crwu/crwu-audit-asset-transport-equipment/references/01-kb-assembly.md +35 -0
  116. package/skills/crwu/crwu-audit-asset-transport-equipment/references/02-review-focus.md +33 -0
  117. package/skills/crwu/crwu-audit-asset-transport-equipment/references/03-common-contract.md +202 -0
  118. package/skills/crwu/crwu-audit-biz-asset-operation/SKILL.md +38 -0
  119. package/skills/crwu/crwu-audit-biz-asset-operation/references/00-applicability.md +30 -0
  120. package/skills/crwu/crwu-audit-biz-asset-operation/references/01-kb-assembly.md +32 -0
  121. package/skills/crwu/crwu-audit-biz-asset-operation/references/02-review-focus.md +42 -0
  122. package/skills/crwu/crwu-audit-biz-asset-operation/references/03-common-contract.md +202 -0
  123. package/skills/crwu/crwu-audit-biz-consulting-review/SKILL.md +38 -0
  124. package/skills/crwu/crwu-audit-biz-consulting-review/references/00-applicability.md +30 -0
  125. package/skills/crwu/crwu-audit-biz-consulting-review/references/01-kb-assembly.md +32 -0
  126. package/skills/crwu/crwu-audit-biz-consulting-review/references/02-review-focus.md +40 -0
  127. package/skills/crwu/crwu-audit-biz-consulting-review/references/03-common-contract.md +202 -0
  128. package/skills/crwu/crwu-audit-biz-financial-reporting/SKILL.md +38 -0
  129. package/skills/crwu/crwu-audit-biz-financial-reporting/references/00-applicability.md +41 -0
  130. package/skills/crwu/crwu-audit-biz-financial-reporting/references/01-kb-assembly.md +40 -0
  131. package/skills/crwu/crwu-audit-biz-financial-reporting/references/02-review-focus.md +64 -0
  132. package/skills/crwu/crwu-audit-biz-financial-reporting/references/03-common-contract.md +202 -0
  133. package/skills/crwu/crwu-audit-biz-financing-debt/SKILL.md +38 -0
  134. package/skills/crwu/crwu-audit-biz-financing-debt/references/00-applicability.md +31 -0
  135. package/skills/crwu/crwu-audit-biz-financing-debt/references/01-kb-assembly.md +34 -0
  136. package/skills/crwu/crwu-audit-biz-financing-debt/references/02-review-focus.md +47 -0
  137. package/skills/crwu/crwu-audit-biz-financing-debt/references/03-common-contract.md +202 -0
  138. package/skills/crwu/crwu-audit-biz-investment-capital/SKILL.md +38 -0
  139. package/skills/crwu/crwu-audit-biz-investment-capital/references/00-applicability.md +31 -0
  140. package/skills/crwu/crwu-audit-biz-investment-capital/references/01-kb-assembly.md +34 -0
  141. package/skills/crwu/crwu-audit-biz-investment-capital/references/02-review-focus.md +47 -0
  142. package/skills/crwu/crwu-audit-biz-investment-capital/references/03-common-contract.md +202 -0
  143. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/SKILL.md +38 -0
  144. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/00-applicability.md +30 -0
  145. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/01-kb-assembly.md +32 -0
  146. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/02-review-focus.md +40 -0
  147. package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/03-common-contract.md +202 -0
  148. package/skills/crwu/crwu-audit-biz-tax-history/SKILL.md +38 -0
  149. package/skills/crwu/crwu-audit-biz-tax-history/references/00-applicability.md +29 -0
  150. package/skills/crwu/crwu-audit-biz-tax-history/references/01-kb-assembly.md +30 -0
  151. package/skills/crwu/crwu-audit-biz-tax-history/references/02-review-focus.md +33 -0
  152. package/skills/crwu/crwu-audit-biz-tax-history/references/03-common-contract.md +202 -0
  153. package/skills/crwu/crwu-audit-biz-transaction-disposal/SKILL.md +38 -0
  154. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/00-applicability.md +31 -0
  155. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/01-kb-assembly.md +34 -0
  156. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/02-review-focus.md +47 -0
  157. package/skills/crwu/crwu-audit-biz-transaction-disposal/references/03-common-contract.md +202 -0
  158. package/skills/crwu/crwu-audit-datacheck/SKILL.md +59 -0
  159. package/skills/crwu/crwu-audit-datacheck/references/00-KB/350/243/205/351/205/215/350/241/250.md +25 -0
  160. package/skills/crwu/crwu-audit-datacheck/references/01-runtime-and-check-catalog.md +139 -0
  161. package/skills/crwu/crwu-audit-datacheck/scripts/recalc_check.py +521 -0
  162. package/skills/crwu/crwu-audit-datacheck/scripts/test_recalc_check.py +419 -0
  163. package/skills/crwu/crwu-audit-datacheck/scripts/test_skill_self_containment.py +44 -0
  164. package/skills/crwu/crwu-audit-external-data/SKILL.md +73 -0
  165. package/skills/crwu/crwu-audit-external-data/references/00-KB/350/243/205/351/205/215/350/241/250.md +38 -0
  166. package/skills/crwu/crwu-audit-external-data/references/01-connector-access.md +116 -0
  167. package/skills/crwu/crwu-audit-external-data/references/02-verification-workflow.md +78 -0
  168. package/skills/crwu/crwu-audit-external-data/scripts/connector_probe.py +374 -0
  169. package/skills/crwu/crwu-audit-external-data/scripts/test_connector_probe.py +264 -0
  170. package/skills/crwu/crwu-audit-output-filter/SKILL.md +34 -0
  171. package/skills/crwu/crwu-audit-output-filter/references/00-applicability.md +7 -0
  172. package/skills/crwu/crwu-audit-output-filter/references/01-kb-assembly.md +11 -0
  173. package/skills/crwu/crwu-audit-output-filter/references/02-review-focus.md +25 -0
  174. package/skills/crwu/crwu-audit-public-general-standards/SKILL.md +46 -0
  175. package/skills/crwu/crwu-audit-public-general-standards/references/00-applicability.md +41 -0
  176. package/skills/crwu/crwu-audit-public-general-standards/references/01-kb-assembly.md +66 -0
  177. package/skills/crwu/crwu-audit-public-general-standards/references/02-review-focus.md +111 -0
  178. package/skills/crwu/crwu-audit-public-general-standards/references/03-execution-contract.md +89 -0
  179. package/skills/crwu/crwu-dev-audit-optimize/SKILL.md +98 -0
  180. package/skills/crwu/crwu-dev-audit-optimize/references/00-/344/274/230/345/214/226/350/247/204/350/214/203/344/270/216/346/226/207/344/273/266/350/220/275/347/202/271.md +132 -0
  181. package/skills/crwu/crwu-dev-audit-optimize/references/01-/345/217/215/351/246/210/345/256/232/344/275/215/344/270/216/347/224/273/345/203/217/346/265/201/347/250/213.md +108 -0
  182. package/skills/crwu/crwu-dev-audit-optimize/references/02-/346/226/271/346/241/210/346/250/241/346/235/277/344/270/216/347/241/256/350/256/244/351/227/250/347/246/201.md +92 -0
  183. package/skills/crwu/crwu-dev-audit-optimize/references/03-AI/344/272/272/345/267/245/345/267/256/350/267/235/345/210/206/346/236/220/346/265/201/347/250/213.md +95 -0
  184. package/skills/crwu/crwu-dev-audit-optimize/scripts/examples/gap-analysis.sample.json +116 -0
  185. package/skills/crwu/crwu-dev-audit-optimize/scripts/gap_analysis.schema.json +33 -0
  186. package/skills/crwu/crwu-dev-audit-optimize/scripts/gap_analysis_delivery.py +475 -0
  187. package/skills/crwu/crwu-dev-audit-optimize/scripts/test_gap_analysis_delivery.py +855 -0
  188. package/skills/crwu/crwu-dev-audit-optimize/template/NOTICE.echarts.txt +7 -0
  189. package/skills/crwu/crwu-dev-audit-optimize/template/echarts.min.js +45 -0
  190. package/skills/crwu/crwu-dev-audit-optimize/template/gap-analysis-report.html +71 -0
  191. package/skills/crwu/crwu-dev-audit-skill-maintainer/SKILL.md +74 -0
  192. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/00-responsibility-and-modes.md +62 -0
  193. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/01-kb-source-discovery.md +89 -0
  194. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/02-child-skill-contract.md +99 -0
  195. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/03-review-item-execution-contract.md +51 -0
  196. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/04-registry-and-mapping-update.md +73 -0
  197. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/05-validation-and-delivery.md +127 -0
  198. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/06-live-routing-reconciliation.md +136 -0
  199. package/skills/crwu/crwu-dev-audit-skill-maintainer/references/07-kb-skill-map.md +411 -0
  200. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/README.md +200 -0
  201. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/check_audit_skill_mappings.py +2477 -0
  202. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/kb_tool.py +577 -0
  203. package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/test_audit_skill_maintainer.py +5097 -0
  204. package/skills/dws/LICENSE +178 -0
  205. package/skills/dws/NOTICE +17 -0
  206. package/skills/dws/dingtalk-aisearch/SKILL.md +83 -0
  207. package/skills/dws/dingtalk-aisearch/references/aisearch.md +249 -0
  208. package/skills/dws/dingtalk-aisearch/references/intent-guide.md +15 -0
  209. package/skills/dws/dingtalk-aisearch/references/lite-recipes.md +31 -0
  210. package/skills/dws/dingtalk-aitable/SKILL.md +139 -0
  211. package/skills/dws/dingtalk-aitable/references/06-data-analytics.md +11 -0
  212. package/skills/dws/dingtalk-aitable/references/aitable/aitable-advperm.md +251 -0
  213. package/skills/dws/dingtalk-aitable/references/aitable/aitable-attachment.md +49 -0
  214. package/skills/dws/dingtalk-aitable/references/aitable/aitable-best-practices.md +48 -0
  215. package/skills/dws/dingtalk-aitable/references/aitable/aitable-cell-value.md +346 -0
  216. package/skills/dws/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md +46 -0
  217. package/skills/dws/dingtalk-aitable/references/aitable/aitable-data-analysis-sop.md +131 -0
  218. package/skills/dws/dingtalk-aitable/references/aitable/aitable-datasource.md +272 -0
  219. package/skills/dws/dingtalk-aitable/references/aitable/aitable-error-recovery.md +128 -0
  220. package/skills/dws/dingtalk-aitable/references/aitable/aitable-export-import.md +113 -0
  221. package/skills/dws/dingtalk-aitable/references/aitable/aitable-field-properties.md +238 -0
  222. package/skills/dws/dingtalk-aitable/references/aitable/aitable-field.md +186 -0
  223. package/skills/dws/dingtalk-aitable/references/aitable/aitable-filter-sort.md +135 -0
  224. package/skills/dws/dingtalk-aitable/references/aitable/aitable-form.md +130 -0
  225. package/skills/dws/dingtalk-aitable/references/aitable/aitable-formula-guide.md +224 -0
  226. package/skills/dws/dingtalk-aitable/references/aitable/aitable-primary-doc.md +58 -0
  227. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-create.md +52 -0
  228. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-delete.md +20 -0
  229. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-history.md +97 -0
  230. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-name-key.md +47 -0
  231. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-query.md +120 -0
  232. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-share.md +54 -0
  233. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-stats.md +59 -0
  234. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-update.md +66 -0
  235. package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-upsert.md +89 -0
  236. package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-config.md +140 -0
  237. package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-extras.md +195 -0
  238. package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-types.md +95 -0
  239. package/skills/dws/dingtalk-aitable/references/aitable/aitable-workflow.md +384 -0
  240. package/skills/dws/dingtalk-aitable/references/aitable-record-ops.md +80 -0
  241. package/skills/dws/dingtalk-aitable/references/aitable-section.md +30 -0
  242. package/skills/dws/dingtalk-aitable/references/aitable.md +59 -0
  243. package/skills/dws/dingtalk-aitable/references/field-rules.md +17 -0
  244. package/skills/dws/dingtalk-aitable/references/intent-guide.md +17 -0
  245. package/skills/dws/dingtalk-aitable/references/recipes/conventions.md +44 -0
  246. package/skills/dws/dingtalk-aitable/references/url-patterns.md +144 -0
  247. package/skills/dws/dingtalk-aitable/scripts/aitable_export_via_task.py +210 -0
  248. package/skills/dws/dingtalk-aitable/scripts/aitable_import_via_task.py +173 -0
  249. package/skills/dws/dingtalk-aitable/scripts/bulk_add_fields.py +273 -0
  250. package/skills/dws/dingtalk-aitable/scripts/import_records.py +333 -0
  251. package/skills/dws/dingtalk-aitable/scripts/upload_attachment.py +190 -0
  252. package/skills/dws/dingtalk-calendar/SKILL.md +137 -0
  253. package/skills/dws/dingtalk-calendar/references/03-meeting.md +56 -0
  254. package/skills/dws/dingtalk-calendar/references/calendar.md +760 -0
  255. package/skills/dws/dingtalk-calendar/references/intent-guide.md +9 -0
  256. package/skills/dws/dingtalk-calendar/references/lite-recipes.md +25 -0
  257. package/skills/dws/dingtalk-calendar/scripts/calendar_free_slot_finder.py +199 -0
  258. package/skills/dws/dingtalk-calendar/scripts/calendar_schedule_meeting.py +233 -0
  259. package/skills/dws/dingtalk-calendar/scripts/calendar_today_agenda.py +139 -0
  260. package/skills/dws/dingtalk-chat/SKILL.md +127 -0
  261. package/skills/dws/dingtalk-chat/references/01-messaging.md +103 -0
  262. package/skills/dws/dingtalk-chat/references/card/callback.md +10 -0
  263. package/skills/dws/dingtalk-chat/references/card/create.md +27 -0
  264. package/skills/dws/dingtalk-chat/references/card/schema.md +14 -0
  265. package/skills/dws/dingtalk-chat/references/card/update.md +13 -0
  266. package/skills/dws/dingtalk-chat/references/chat/chat-bot.md +187 -0
  267. package/skills/dws/dingtalk-chat/references/chat/chat-conversation.md +165 -0
  268. package/skills/dws/dingtalk-chat/references/chat/group-admin.md +144 -0
  269. package/skills/dws/dingtalk-chat/references/chat/group-discovery.md +121 -0
  270. package/skills/dws/dingtalk-chat/references/chat/message-actions.md +132 -0
  271. package/skills/dws/dingtalk-chat/references/chat/message-media.md +76 -0
  272. package/skills/dws/dingtalk-chat/references/chat/message-query.md +135 -0
  273. package/skills/dws/dingtalk-chat/references/chat/thread.md +46 -0
  274. package/skills/dws/dingtalk-chat/references/chat-emoji-list.md +216 -0
  275. package/skills/dws/dingtalk-chat/references/chat.md +140 -0
  276. package/skills/dws/dingtalk-chat/references/contracts.md +70 -0
  277. package/skills/dws/dingtalk-chat/references/intent-guide.md +56 -0
  278. package/skills/dws/dingtalk-contact/SKILL.md +122 -0
  279. package/skills/dws/dingtalk-contact/references/08-directory.md +51 -0
  280. package/skills/dws/dingtalk-contact/references/contact.md +595 -0
  281. package/skills/dws/dingtalk-contact/references/intent-guide.md +14 -0
  282. package/skills/dws/dingtalk-contact/references/lite-recipes.md +30 -0
  283. package/skills/dws/dingtalk-contact/references/recipes/conventions.md +44 -0
  284. package/skills/dws/dingtalk-contact/scripts/contact_dept_members.py +130 -0
  285. package/skills/dws/dingtalk-doc/SKILL.md +112 -0
  286. package/skills/dws/dingtalk-doc/references/04-document.md +57 -0
  287. package/skills/dws/dingtalk-doc/references/contracts.md +40 -0
  288. package/skills/dws/dingtalk-doc/references/doc/doc-block.md +39 -0
  289. package/skills/dws/dingtalk-doc/references/doc/doc-comment.md +29 -0
  290. package/skills/dws/dingtalk-doc/references/doc/doc-create.md +54 -0
  291. package/skills/dws/dingtalk-doc/references/doc/doc-export.md +30 -0
  292. package/skills/dws/dingtalk-doc/references/doc/doc-import.md +28 -0
  293. package/skills/dws/dingtalk-doc/references/doc/doc-info.md +12 -0
  294. package/skills/dws/dingtalk-doc/references/doc/doc-media.md +46 -0
  295. package/skills/dws/dingtalk-doc/references/doc/doc-read.md +56 -0
  296. package/skills/dws/dingtalk-doc/references/doc/doc-update.md +87 -0
  297. package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md +469 -0
  298. package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-schema.json +526 -0
  299. package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-schema.md +711 -0
  300. package/skills/dws/dingtalk-doc/references/doc/style/doc-create-workflow.md +414 -0
  301. package/skills/dws/dingtalk-doc/references/doc/style/doc-style-guideline.md +404 -0
  302. package/skills/dws/dingtalk-doc/references/doc/style/doc-update-workflow.md +316 -0
  303. package/skills/dws/dingtalk-doc/references/doc.md +82 -0
  304. package/skills/dws/dingtalk-doc/references/intent-guide.md +20 -0
  305. package/skills/dws/dingtalk-drive/SKILL.md +128 -0
  306. package/skills/dws/dingtalk-drive/references/drive.md +126 -0
  307. package/skills/dws/dingtalk-drive/references/folder-sync.md +40 -0
  308. package/skills/dws/dingtalk-drive/references/intent-guide.md +22 -0
  309. package/skills/dws/dingtalk-drive/references/lite-recipes.md +39 -0
  310. package/skills/dws/dingtalk-event/SKILL.md +106 -0
  311. package/skills/dws/dingtalk-event/references/event-im-keys.md +54 -0
  312. package/skills/dws/dingtalk-event/references/event-im-lifecycle.md +35 -0
  313. package/skills/dws/dingtalk-event/references/event-im-operations.md +52 -0
  314. package/skills/dws/dingtalk-event/references/event-im-output.md +39 -0
  315. package/skills/dws/dingtalk-event/references/event-im.md +58 -0
  316. package/skills/dws/dingtalk-event/references/event-oa.md +155 -0
  317. package/skills/dws/dingtalk-event/references/event-todo.md +50 -0
  318. package/skills/dws/dingtalk-event/references/event-voip.md +69 -0
  319. package/skills/dws/dingtalk-mail/SKILL.md +113 -0
  320. package/skills/dws/dingtalk-mail/references/09-mail.md +45 -0
  321. package/skills/dws/dingtalk-mail/references/intent-guide.md +8 -0
  322. package/skills/dws/dingtalk-mail/references/lite-recipes.md +45 -0
  323. package/skills/dws/dingtalk-mail/references/mail.md +1889 -0
  324. package/skills/dws/dingtalk-mail/references/recipes/conventions.md +44 -0
  325. package/skills/dws/dingtalk-mail/scripts/mail_send_with_cc.py +122 -0
  326. package/skills/dws/dingtalk-mail/scripts/mail_unread_summary.py +114 -0
  327. package/skills/dws/dingtalk-minutes/SKILL.md +116 -0
  328. package/skills/dws/dingtalk-minutes/references/07-minutes.md +149 -0
  329. package/skills/dws/dingtalk-minutes/references/10-minutes-speaker-match.md +32 -0
  330. package/skills/dws/dingtalk-minutes/references/11-minutes-speaker-correct.md +40 -0
  331. package/skills/dws/dingtalk-minutes/references/intent-guide.md +79 -0
  332. package/skills/dws/dingtalk-minutes/references/lite-recipes.md +25 -0
  333. package/skills/dws/dingtalk-minutes/references/minutes.md +210 -0
  334. package/skills/dws/dingtalk-minutes/references/recipes/conventions.md +33 -0
  335. package/skills/dws/dingtalk-minutes/scripts/minutes_recent_summary.py +196 -0
  336. package/skills/dws/dingtalk-minutes/scripts/test_minutes_scripts.py +222 -0
  337. package/skills/dws/dingtalk-misc/SKILL.md +55 -0
  338. package/skills/dws/dingtalk-misc/references/05-reporting.md +29 -0
  339. package/skills/dws/dingtalk-misc/references/agoal.md +302 -0
  340. package/skills/dws/dingtalk-misc/references/attendance-intent-guide.md +7 -0
  341. package/skills/dws/dingtalk-misc/references/attendance-report.md +109 -0
  342. package/skills/dws/dingtalk-misc/references/attendance-schedule.md +115 -0
  343. package/skills/dws/dingtalk-misc/references/attendance-vacation.md +129 -0
  344. package/skills/dws/dingtalk-misc/references/attendance.md +149 -0
  345. package/skills/dws/dingtalk-misc/references/dev/app.md +42 -0
  346. package/skills/dws/dingtalk-misc/references/dev/connect.md +108 -0
  347. package/skills/dws/dingtalk-misc/references/dev/credentials.md +26 -0
  348. package/skills/dws/dingtalk-misc/references/dev/dev-index.md +13 -0
  349. package/skills/dws/dingtalk-misc/references/dev/event.md +47 -0
  350. package/skills/dws/dingtalk-misc/references/dev/mcp.md +106 -0
  351. package/skills/dws/dingtalk-misc/references/dev/member.md +19 -0
  352. package/skills/dws/dingtalk-misc/references/dev/permission.md +56 -0
  353. package/skills/dws/dingtalk-misc/references/dev/recipes.md +58 -0
  354. package/skills/dws/dingtalk-misc/references/dev/robot.md +74 -0
  355. package/skills/dws/dingtalk-misc/references/dev/security.md +21 -0
  356. package/skills/dws/dingtalk-misc/references/dev/version.md +99 -0
  357. package/skills/dws/dingtalk-misc/references/dev/webapp.md +22 -0
  358. package/skills/dws/dingtalk-misc/references/devapp.md +148 -0
  359. package/skills/dws/dingtalk-misc/references/devdoc-intent-guide.md +7 -0
  360. package/skills/dws/dingtalk-misc/references/devdoc.md +42 -0
  361. package/skills/dws/dingtalk-misc/references/ding.md +184 -0
  362. package/skills/dws/dingtalk-misc/references/hrbrain.md +67 -0
  363. package/skills/dws/dingtalk-misc/references/live.md +23 -0
  364. package/skills/dws/dingtalk-misc/references/markdown.md +167 -0
  365. package/skills/dws/dingtalk-misc/references/oa/oa-form-components.md +345 -0
  366. package/skills/dws/dingtalk-misc/references/oa/oa-process-nodes.md +374 -0
  367. package/skills/dws/dingtalk-misc/references/oa.md +1014 -0
  368. package/skills/dws/dingtalk-misc/references/openapi-explorer.md +47 -0
  369. package/skills/dws/dingtalk-misc/references/pat.md +52 -0
  370. package/skills/dws/dingtalk-misc/references/profile.md +41 -0
  371. package/skills/dws/dingtalk-misc/references/recruit.md +89 -0
  372. package/skills/dws/dingtalk-misc/references/report-conventions.md +44 -0
  373. package/skills/dws/dingtalk-misc/references/report-intent-guide.md +11 -0
  374. package/skills/dws/dingtalk-misc/references/report-lite-recipes.md +25 -0
  375. package/skills/dws/dingtalk-misc/references/report.md +109 -0
  376. package/skills/dws/dingtalk-misc/references/sheet/sheet-batch-operations.md +104 -0
  377. package/skills/dws/dingtalk-misc/references/sheet/sheet-chart.md +69 -0
  378. package/skills/dws/dingtalk-misc/references/sheet/sheet-comment.md +166 -0
  379. package/skills/dws/dingtalk-misc/references/sheet/sheet-conditional-format.md +59 -0
  380. package/skills/dws/dingtalk-misc/references/sheet/sheet-dimension-operations.md +60 -0
  381. package/skills/dws/dingtalk-misc/references/sheet/sheet-dropdown.md +103 -0
  382. package/skills/dws/dingtalk-misc/references/sheet/sheet-export.md +135 -0
  383. package/skills/dws/dingtalk-misc/references/sheet/sheet-filter-view.md +51 -0
  384. package/skills/dws/dingtalk-misc/references/sheet/sheet-filter.md +181 -0
  385. package/skills/dws/dingtalk-misc/references/sheet/sheet-formula.md +192 -0
  386. package/skills/dws/dingtalk-misc/references/sheet/sheet-import.md +76 -0
  387. package/skills/dws/dingtalk-misc/references/sheet/sheet-media-image.md +263 -0
  388. package/skills/dws/dingtalk-misc/references/sheet/sheet-pivot-table.md +292 -0
  389. package/skills/dws/dingtalk-misc/references/sheet/sheet-range-operations.md +161 -0
  390. package/skills/dws/dingtalk-misc/references/sheet/sheet-read-data.md +60 -0
  391. package/skills/dws/dingtalk-misc/references/sheet/sheet-revision-changeset.md +46 -0
  392. package/skills/dws/dingtalk-misc/references/sheet/sheet-search-replace.md +120 -0
  393. package/skills/dws/dingtalk-misc/references/sheet/sheet-style-format.md +71 -0
  394. package/skills/dws/dingtalk-misc/references/sheet/sheet-version.md +84 -0
  395. package/skills/dws/dingtalk-misc/references/sheet/sheet-workbook.md +59 -0
  396. package/skills/dws/dingtalk-misc/references/sheet/sheet-write-data.md +93 -0
  397. package/skills/dws/dingtalk-misc/references/sheet-conventions.md +44 -0
  398. package/skills/dws/dingtalk-misc/references/sheet-intent-guide.md +12 -0
  399. package/skills/dws/dingtalk-misc/references/sheet.md +166 -0
  400. package/skills/dws/dingtalk-misc/references/skill.md +98 -0
  401. package/skills/dws/dingtalk-misc/references/unsupported-scripts.md +49 -0
  402. package/skills/dws/dingtalk-misc/references/url-patterns.md +144 -0
  403. package/skills/dws/dingtalk-misc/references/whiteboard/compose.md +200 -0
  404. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/01-overview.md +57 -0
  405. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/02-query.md +288 -0
  406. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/03-update.md +249 -0
  407. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/04-text-style.md +395 -0
  408. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/05-shape-frame-group-connector.md +186 -0
  409. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/06-vector-icon-path.md +313 -0
  410. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/07-examples-errors-write-support.md +270 -0
  411. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/08-catalogs.md +61 -0
  412. package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1.md +54 -0
  413. package/skills/dws/dingtalk-misc/references/whiteboard/replace.md +41 -0
  414. package/skills/dws/dingtalk-misc/references/whiteboard/vector.md +51 -0
  415. package/skills/dws/dingtalk-misc/references/whiteboard.md +115 -0
  416. package/skills/dws/dingtalk-misc/scripts/aiapp_create_and_poll.py +138 -0
  417. package/skills/dws/dingtalk-misc/scripts/attendance_my_record.py +91 -0
  418. package/skills/dws/dingtalk-misc/scripts/attendance_report_checkin.py +440 -0
  419. package/skills/dws/dingtalk-misc/scripts/attendance_report_common.py +1698 -0
  420. package/skills/dws/dingtalk-misc/scripts/attendance_report_daily.py +558 -0
  421. package/skills/dws/dingtalk-misc/scripts/attendance_report_detail.py +809 -0
  422. package/skills/dws/dingtalk-misc/scripts/attendance_report_monthly.py +758 -0
  423. package/skills/dws/dingtalk-misc/scripts/attendance_report_record.py +947 -0
  424. package/skills/dws/dingtalk-misc/scripts/attendance_schedule_export.py +344 -0
  425. package/skills/dws/dingtalk-misc/scripts/attendance_schedule_import.py +498 -0
  426. package/skills/dws/dingtalk-misc/scripts/attendance_team_shift.py +89 -0
  427. package/skills/dws/dingtalk-misc/scripts/attendance_vacation_balance.py +617 -0
  428. package/skills/dws/dingtalk-misc/scripts/finance_daily_cashflow.py +70 -0
  429. package/skills/dws/dingtalk-misc/scripts/finance_expense_flow.py +138 -0
  430. package/skills/dws/dingtalk-misc/scripts/oa_batch_approve.py +158 -0
  431. package/skills/dws/dingtalk-misc/scripts/oa_pending_review.py +132 -0
  432. package/skills/dws/dingtalk-misc/scripts/report_received_today.py +315 -0
  433. package/skills/dws/dingtalk-misc/scripts/yida_custom_page_update.py +401 -0
  434. package/skills/dws/dingtalk-misc/scripts/yida_form_builder.py +526 -0
  435. package/skills/dws/dingtalk-misc/scripts/yida_form_fields.py +315 -0
  436. package/skills/dws/dingtalk-misc/scripts/yida_form_inspector.py +311 -0
  437. package/skills/dws/dingtalk-misc/scripts/yida_form_update.py +185 -0
  438. package/skills/dws/dingtalk-misc/scripts/yida_jsx_pipeline.py +1588 -0
  439. package/skills/dws/dingtalk-misc/scripts/yida_page_compiler.py +1441 -0
  440. package/skills/dws/dingtalk-misc/scripts/yida_page_generate.py +514 -0
  441. package/skills/dws/dingtalk-misc/scripts/yida_page_schema.py +519 -0
  442. package/skills/dws/dingtalk-misc/scripts/yida_page_self_check.py +398 -0
  443. package/skills/dws/dingtalk-misc/scripts/yida_process_flow.py +1478 -0
  444. package/skills/dws/dingtalk-misc/scripts/yida_process_update.py +589 -0
  445. package/skills/dws/dingtalk-misc/scripts/yida_report_builder.py +494 -0
  446. package/skills/dws/dingtalk-misc/scripts/yida_report_charts.py +1434 -0
  447. package/skills/dws/dingtalk-misc/scripts/yida_report_update.py +244 -0
  448. package/skills/dws/dingtalk-misc/scripts/yida_schema_common.py +340 -0
  449. package/skills/dws/dingtalk-shared/SKILL.md +83 -0
  450. package/skills/dws/dingtalk-shared/references/capability-limits.md +31 -0
  451. package/skills/dws/dingtalk-shared/references/channel-login.md +26 -0
  452. package/skills/dws/dingtalk-shared/references/confirmation.md +59 -0
  453. package/skills/dws/dingtalk-shared/references/error-codes.md +155 -0
  454. package/skills/dws/dingtalk-shared/references/global-reference.md +122 -0
  455. package/skills/dws/dingtalk-shared/references/intent-guide.md +537 -0
  456. package/skills/dws/dingtalk-shared/references/recipes/conventions.md +45 -0
  457. package/skills/dws/dingtalk-shared/references/recipes/lite-catalog.md +275 -0
  458. package/skills/dws/dingtalk-shared/references/recipes/meta.md +67 -0
  459. package/skills/dws/dingtalk-shared/references/routing.md +52 -0
  460. package/skills/dws/dingtalk-shared/references/runtime-contract.md +11 -0
  461. package/skills/dws/dingtalk-shared/references/schema-usage.md +88 -0
  462. package/skills/dws/dingtalk-shared/references/url-patterns.md +144 -0
  463. package/skills/dws/dingtalk-shared/references/workflow-routing.md +45 -0
  464. package/skills/dws/dingtalk-todo/SKILL.md +119 -0
  465. package/skills/dws/dingtalk-todo/references/02-task.md +70 -0
  466. package/skills/dws/dingtalk-todo/references/intent-guide.md +15 -0
  467. package/skills/dws/dingtalk-todo/references/lite-recipes.md +68 -0
  468. package/skills/dws/dingtalk-todo/references/recipes/conventions.md +44 -0
  469. package/skills/dws/dingtalk-todo/references/todo.md +517 -0
  470. package/skills/dws/dingtalk-todo/scripts/todo_batch_create.py +354 -0
  471. package/skills/dws/dingtalk-todo/scripts/todo_daily_summary.py +153 -0
  472. package/skills/dws/dingtalk-todo/scripts/todo_overdue_check.py +101 -0
  473. package/skills/dws/dingtalk-wiki/SKILL.md +117 -0
  474. package/skills/dws/dingtalk-wiki/references/intent-guide.md +14 -0
  475. package/skills/dws/dingtalk-wiki/references/lite-recipes.md +44 -0
  476. package/skills/dws/dingtalk-wiki/references/wiki-members.md +32 -0
  477. package/skills/dws/dingtalk-wiki/references/wiki-node-ops.md +46 -0
  478. package/skills/dws/dingtalk-wiki/references/wiki.md +65 -0
  479. package/skills/dws/provenance.json +27 -0
@@ -0,0 +1,1252 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """阶段一材料准备:可读性核查 + 隐藏数据隔离 + 「工作版」重建(crwu-audit 编排层专用)。
4
+
5
+ 归属:`crwu-audit` 技能自带脚本(随技能安装)。执行契约见本技能
6
+ `references/00-input-and-route-profile.md` § Excel 隐藏数据隔离。
7
+
8
+ 纪律(每条都对应一次真实失效,不得回退):
9
+
10
+ H0 人工隐藏区**整体排除审核范围**:sheet / 行 / 列 / 折叠分组是**员工手动隐藏**的,
11
+ 表示其已明确不纳入审核。隐藏区**禁读禁报**——不读取、不引用、不输出任何内容值,
12
+ 不产生差异或意见;只允许读「哪些行/列被隐藏了」这类结构元数据,用于剔除。
13
+
14
+ 1. **缓存值优先**:重建时单元格有缓存值(`data_only=True`)就写缓存值,**仅当无缓存值**
15
+ 才写公式串。重建簿不会被 Excel 重算,写公式串会让后续 `data_only=True` 读回 `None`,
16
+ 把"公式未重算"误读成"数据缺失"(曾据此误报"评估结果列全空")。
17
+ 2. **「值不可得」≠「数据缺失」**:无缓存值且为公式/外部引用(或两者皆无但有样式)的格,
18
+ 登记为「值不可得」元数据(按表计数,不含内容值),供后续**如实声明**该格取不到值;
19
+ **禁止**据此判"空白/缺失/未填"。
20
+ 3. **输出名唯一 + 来源可追溯**:同名不同版本(如定稿/送审稿同名)必须消歧(追加来源目录
21
+ 后缀),并在盘点中记录来源子目录(`stage`)与工作版路径一一对应,**禁止静默覆盖**
22
+ (曾因送审稿覆盖定稿,误判"使用状况=自用")。
23
+ 4. **不得改变格式语义**:逐格复制 `number_format`,重建后数值/日期语义与原件一致;
24
+ 对"疑似被日期化的数值列",结论前须回 raw 原件核对格式。
25
+ 5. 只读源材料:源目录**不写入任何派生文件**(`textutil` 等中间产物落系统临时目录)。
26
+ 6. **媒体(图片)证据必须导出并放行**:重建法必然丢掉 `xl/media`、`word/media` 等部件,只登记
27
+ 一个计数等于没有通道。本脚本把可见锚点的媒体导出到 `<案例>/媒体证据/` 并写
28
+ `媒体索引.json`,供宿主多模态读图;读不到只能出「未核验」——**未核 ≠ 缺失**。
29
+ H0 同样适用于媒体锚点:锚点落在隐藏区 / 隐藏结构不可得 / 锚点不可判定者**一律不导出**,
30
+ 只记数量与未核原因。实现见本技能 `scripts/media_extract.py`。
31
+
32
+ 产出:`材料盘点.json`、`媒体索引.json`、`提取/<文件>.txt`、`工作版/<文件>.xlsx`、
33
+ `媒体证据/<来源子目录>/<文件>/…`。
34
+
35
+ 用法:
36
+ python3 scripts/prepare_materials.py --case <案例目录> [--src 材料-源] [--work 工作版]
37
+ # 阶段二复核件(复核意见附件里的图同样要抽取;产物名加后缀,绝不覆盖阶段一冻结产物)
38
+ python3 scripts/prepare_materials.py --case <案例目录> --src 复核-人工 --label 复核
39
+ """
40
+ from __future__ import annotations
41
+
42
+ import argparse
43
+ import json
44
+ import os
45
+ import re
46
+ import shutil
47
+ import subprocess
48
+ import tempfile
49
+
50
+ def _import_media_extract():
51
+ """按文件位置加载同目录的 `media_extract.py`。
52
+
53
+ 本脚本既能作为 `python3 scripts/prepare_materials.py` 运行(脚本目录已在 `sys.path`),
54
+ 也会被契约测试用 `importlib` 以任意模块名加载(此时脚本目录不在 `sys.path`)——
55
+ 两种入口都要能拿到媒体抽取通道,故按 `__file__` 定位而不是靠 `sys.path`。
56
+ """
57
+ import importlib.util
58
+ here = os.path.dirname(os.path.abspath(__file__))
59
+ spec = importlib.util.spec_from_file_location(
60
+ "crwu_media_extract", os.path.join(here, "media_extract.py"))
61
+ module = importlib.util.module_from_spec(spec)
62
+ spec.loader.exec_module(module)
63
+ return module
64
+
65
+
66
+ media_extract = _import_media_extract()
67
+
68
+ HIDDEN_SHEET = "[隐藏]"
69
+
70
+
71
+ OPTIONAL_DEPS = {"openpyxl": ".xlsx", "docx": ".docx", "pypdf": ".pdf", "xlrd": ".xls"}
72
+
73
+
74
+ ARCHIVE_EXTS = (".zip", ".tar", ".tgz", ".tbz2", ".txz", ".tar.gz", ".tar.bz2", ".tar.xz",
75
+ ".rar", ".7z")
76
+
77
+ REVIEW_NAME_HINTS = ("复核", "质控", "外审", "答复", "审核意见", "复核意见", "底稿意见")
78
+
79
+ MAX_ENTRIES = 2000
80
+
81
+ MAX_TOTAL_BYTES = 500 * 1024 * 1024
82
+
83
+ MAX_NESTED_DEPTH = 2
84
+
85
+ ORPHAN_SPAN_ROWS = 1000
86
+
87
+ ORPHAN_SPAN_COLS = 1000
88
+
89
+ MAX_SHEET_CELLS = 2_000_000
90
+
91
+
92
+ # **格式级依赖边界**(2026-09-26 · R2):顶层依赖固定到格式,绝不由 `e.name` 推导 ——
93
+ # 传导依赖(如 pypdf 内部缺的模块)只进 `causeModule`/`detail`,不得顶替顶层依赖字段。
94
+ # `.docx` 不入表:它保留 `docx` → `textutil` 的既有回退,不产生依赖缺口。
95
+ FORMAT_DEPENDENCIES = {".xlsx": "openpyxl", ".pdf": "pypdf", ".xls": "xlrd"}
96
+
97
+ DEPENDENCY_STATE_MISSING = "missing"
98
+ DEPENDENCY_STATE_IMPORT_FAILED = "import_failed"
99
+ DEPENDENCY_STATE_PROBE_FAILED = "probe_failed"
100
+
101
+ # 只有 `missing` 允许说"缺少";另外两态必须如实说清是哪一种,不得伪装成缺包。
102
+ DEPENDENCY_STATE_TEXT = {
103
+ DEPENDENCY_STATE_MISSING: "缺少第三方运行时依赖",
104
+ DEPENDENCY_STATE_IMPORT_FAILED: "第三方依赖存在但导入失败/不可用",
105
+ DEPENDENCY_STATE_PROBE_FAILED: "第三方依赖状态探测失败",
106
+ }
107
+
108
+
109
+ class DependencyUnavailable(Exception):
110
+ """本件因**顶层依赖不可用**而未核:`_record_dependency_gap` 已记账,跳过本件继续下一件。
111
+
112
+ 单独成类是为了让分支内的捕获能干净地终止该分支,而不必依赖文件级 `except ImportError`
113
+ 去猜"是谁缺了"(那正是要消灭的跨格式推导)。
114
+ """
115
+
116
+
117
+ def dependency_state(module: str) -> tuple:
118
+ """探测某个**顶层依赖**的三态,返回 `(state, detail)`。
119
+
120
+ - `missing`:`find_spec` 返回 None(确实没装)
121
+ - `probe_failed`:`find_spec` 自身抛异常(无法确认安装状态)
122
+ - `import_failed`:spec 存在,但调用方已经观测到 import 失败
123
+
124
+ 只在"该依赖的 import 已经失败"之后调用,所以 spec 存在即可判定为 `import_failed`。
125
+ """
126
+ import importlib.util as iu
127
+ try:
128
+ spec = iu.find_spec(module)
129
+ except Exception as exc: # noqa: BLE001 —— 探测本身失败也是一种状态
130
+ return DEPENDENCY_STATE_PROBE_FAILED, f"{type(exc).__name__}: {exc}"
131
+ if spec is None:
132
+ return DEPENDENCY_STATE_MISSING, f"find_spec({module}) is None"
133
+ origin = getattr(spec, "origin", None) or "(namespace)"
134
+ return DEPENDENCY_STATE_IMPORT_FAILED, f"find_spec({module}) → {origin}"
135
+
136
+
137
+ def _dependency_absent(module: str) -> bool:
138
+ """该第三方依赖是否**确实不可用**(供 `missing_deps()` 汇总用)。"""
139
+ return dependency_state(module)[0] == DEPENDENCY_STATE_MISSING
140
+
141
+
142
+ def missing_deps() -> list:
143
+ """缺哪些可选依赖(缺了会让对应格式读不到,必须显式告知,不得静默)。"""
144
+ return [f"{mod}({ext})" for mod, ext in OPTIONAL_DEPS.items() if _dependency_absent(mod)]
145
+
146
+
147
+ def _record_dependency_gap(rec: dict, rel: str, dependency: str, ext: str,
148
+ exc: ImportError) -> None:
149
+ """把"顶层依赖不可用"结构化记进盘点记录(三态共用;fail closed,本次未核、未生成工作版)。
150
+
151
+ `dependency` **始终**是该格式的顶层依赖(由 `FORMAT_DEPENDENCIES` 固定);
152
+ 传递依赖/缺失子模块只写进 `causeModule` 与 `detail`。
153
+ """
154
+ state, probe_detail = dependency_state(dependency)
155
+ text = DEPENDENCY_STATE_TEXT[state]
156
+ # `causeModule` 只记**与顶层依赖不同的**那个模块(即传递依赖 / 导入期真正缺的子模块);
157
+ # 顶层依赖自己缺失或失败时它就是 `dependency` 本身,不重复记,保持字段含义单一。
158
+ cause = (getattr(exc, "name", None) or "").strip().split(".")[0] or None
159
+ if cause == dependency:
160
+ cause = None
161
+ rec["readable"] = False
162
+ rec["dependencyGaps"] = [{
163
+ "path": rel,
164
+ "dependency": dependency, # 顶层依赖,固定
165
+ "format": ext,
166
+ "state": state, # missing / import_failed / probe_failed
167
+ "reason": f"{text} {dependency}",
168
+ "causeModule": cause, # 传递依赖只进这里
169
+ "detail": f"{type(exc).__name__}: {exc}|{probe_detail}",
170
+ "notChecked": True,
171
+ "workVersionCreated": False,
172
+ }]
173
+ rec["capabilityGaps"] = list(rec.get("capabilityGaps") or []) + [
174
+ f"{text} {dependency}({ext}):本件未核(notChecked)、未生成工作版"
175
+ ]
176
+ rec["note"] = (
177
+ f"{text} {dependency}:本件未读值、未读公式、未重算、未生成工作版;"
178
+ "按 capability gap 登记(不是材料缺失;不得改用标准库解析 OOXML 读值代替)"
179
+ )
180
+
181
+
182
+ def _call_with_dependency(rec: dict, rel: str, dependency: str, ext: str, func, *args):
183
+ """在**格式分支内**调用需要第三方依赖的函数。
184
+
185
+ - 依赖可用 → `(True, 返回值)`
186
+ - 导入失败 → 按该格式的**顶层依赖**精确记账,返回 `(False, None)`;调用方应终止该分支
187
+
188
+ 顶层依赖与格式必须一一对应(`FORMAT_DEPENDENCIES`);不一致属编程错误,直接抛出让盘点
189
+ 如实记录,而不是猜一个依赖名继续。
190
+ """
191
+ expected = FORMAT_DEPENDENCIES.get(ext)
192
+ if expected != dependency:
193
+ raise ValueError(f"格式 {ext} 的顶层依赖应为 {expected!r},收到 {dependency!r}")
194
+ try:
195
+ return True, func(*args)
196
+ except ImportError as exc:
197
+ _record_dependency_gap(rec, rel, dependency, ext, exc)
198
+ return False, None
199
+
200
+
201
+ def is_archive(name: str) -> bool:
202
+ low = name.lower()
203
+ return any(low.endswith(ext) for ext in ARCHIVE_EXTS)
204
+
205
+
206
+ def _safe_join(dest: str, member: str):
207
+ """拒绝绝对路径与 .. 越界(zip-slip);返回安全绝对路径或 None。"""
208
+ pure = member.replace("\\", "/")
209
+ if pure.startswith("/") or re.match(r"^[A-Za-z]:", pure):
210
+ return None
211
+ parts = [seg for seg in pure.split("/") if seg not in ("", ".")]
212
+ if any(seg == ".." for seg in parts):
213
+ return None
214
+ if not parts:
215
+ return None
216
+ target = os.path.abspath(os.path.join(dest, *parts))
217
+ if not target.startswith(os.path.abspath(dest) + os.sep):
218
+ return None
219
+ return target
220
+
221
+
222
+ def _decode_zip_name(info) -> str:
223
+ """zip 内中文名常为 GBK 且无 UTF-8 标志位 → 默认 cp437 会成乱码,需重解码。"""
224
+ name = info.filename
225
+ if info.flag_bits & 0x800: # 已声明 UTF-8
226
+ return name
227
+ try:
228
+ raw = name.encode("cp437")
229
+ except UnicodeEncodeError:
230
+ return name
231
+ for enc in ("gbk", "utf-8"):
232
+ try:
233
+ return raw.decode(enc)
234
+ except UnicodeDecodeError:
235
+ continue
236
+ return name
237
+
238
+
239
+ def extract_archive(path: str, dest_root: str, depth: int = 0):
240
+ """解压单个归档;返回 (解压目录, 记录 dict)。不合规条目跳过并如实记账,不静默。"""
241
+ name = os.path.basename(path)
242
+ stem = name
243
+ for ext in sorted(ARCHIVE_EXTS, key=len, reverse=True):
244
+ if stem.lower().endswith(ext):
245
+ stem = stem[: -len(ext)]
246
+ break
247
+ dest = os.path.join(dest_root, stem)
248
+ rec = {"archive": path, "destDir": dest, "entries": 0, "extracted": [],
249
+ "skippedIsolation": [], "failures": [], "capabilityGaps": []}
250
+ os.makedirs(dest, exist_ok=True)
251
+ ext = name.lower()
252
+ try:
253
+ if ext.endswith(".zip"):
254
+ import zipfile
255
+ with zipfile.ZipFile(path) as z:
256
+ infos = z.infolist()
257
+ rec["entries"] = len(infos)
258
+ if len(infos) > MAX_ENTRIES:
259
+ rec["capabilityGaps"].append(f"条目数 {len(infos)} 超上限 {MAX_ENTRIES},未解压")
260
+ return dest, rec
261
+ total = sum(i.file_size for i in infos)
262
+ if total > MAX_TOTAL_BYTES:
263
+ rec["capabilityGaps"].append(f"解压后 {total/1e6:.0f}MB 超上限,未解压")
264
+ return dest, rec
265
+ for info in infos:
266
+ member = _decode_zip_name(info)
267
+ if info.is_dir():
268
+ continue
269
+ if any(h in member for h in REVIEW_NAME_HINTS):
270
+ rec["skippedIsolation"].append(member)
271
+ continue
272
+ if (info.external_attr >> 16) & 0o170000 == 0o120000: # 符号链接
273
+ rec["failures"].append(f"{member}: 符号链接,已跳过")
274
+ continue
275
+ target = _safe_join(dest, member)
276
+ if target is None:
277
+ rec["failures"].append(f"{member}: 绝对路径或越界(zip-slip),已跳过")
278
+ continue
279
+ os.makedirs(os.path.dirname(target), exist_ok=True)
280
+ with z.open(info) as src, open(target, "wb") as out:
281
+ shutil.copyfileobj(src, out)
282
+ rec["extracted"].append(member)
283
+ elif ext.endswith((".tar", ".tgz", ".tbz2", ".txz", ".tar.gz", ".tar.bz2", ".tar.xz")):
284
+ import tarfile
285
+ mode = "r:gz" if ext.endswith((".tgz", ".tar.gz")) else (
286
+ "r:bz2" if ext.endswith((".tbz2", ".tar.bz2")) else (
287
+ "r:xz" if ext.endswith((".txz", ".tar.xz")) else "r:"))
288
+ with tarfile.open(path, mode) as tar:
289
+ members = [m for m in tar.getmembers() if m.isfile() or m.issym() or m.islnk()]
290
+ rec["entries"] = len(members)
291
+ if len(members) > MAX_ENTRIES:
292
+ rec["capabilityGaps"].append(f"条目数超上限,未解压")
293
+ return dest, rec
294
+ for m in members:
295
+ member = m.name
296
+ if any(h in member for h in REVIEW_NAME_HINTS):
297
+ rec["skippedIsolation"].append(member)
298
+ continue
299
+ if m.issym() or m.islnk():
300
+ rec["failures"].append(f"{member}: 链接条目,已跳过")
301
+ continue
302
+ target = _safe_join(dest, member)
303
+ if target is None:
304
+ rec["failures"].append(f"{member}: 绝对路径或越界,已跳过")
305
+ continue
306
+ if m.size > MAX_TOTAL_BYTES:
307
+ rec["failures"].append(f"{member}: 单文件过大,已跳过")
308
+ continue
309
+ os.makedirs(os.path.dirname(target), exist_ok=True)
310
+ src = tar.extractfile(m)
311
+ if src is None:
312
+ rec["failures"].append(f"{member}: 无法读取")
313
+ continue
314
+ with src, open(target, "wb") as out:
315
+ shutil.copyfileobj(src, out)
316
+ rec["extracted"].append(member)
317
+ else:
318
+ tool = shutil.which("7z") or shutil.which("7zz") or shutil.which("unar")
319
+ if not tool:
320
+ rec["capabilityGaps"].append(
321
+ f"{os.path.splitext(name)[1]} 需外部解压工具(7z/unar)且本机未安装:该件本次未核")
322
+ return dest, rec
323
+ r = subprocess.run([tool, "x", "-y", f"-o{dest}", path],
324
+ capture_output=True, text=True)
325
+ if r.returncode != 0:
326
+ rec["capabilityGaps"].append(f"{tool} 解压失败:{r.stderr.strip()[:100]}")
327
+ return dest, rec
328
+ for root, _dirs, fs in os.walk(dest):
329
+ for f in fs:
330
+ rec["extracted"].append(os.path.relpath(os.path.join(root, f), dest))
331
+ except Exception as e:
332
+ rec["capabilityGaps"].append(f"{type(e).__name__}: {e}")
333
+ return dest, rec
334
+
335
+ # 嵌套归档:限深继续解压
336
+ if depth < MAX_NESTED_DEPTH:
337
+ for root, _dirs, fs in list(os.walk(dest)):
338
+ for f in sorted(fs):
339
+ if is_archive(f):
340
+ _, sub = extract_archive(os.path.join(root, f), root, depth + 1)
341
+ rec.setdefault("nested", []).append(sub)
342
+ rec["dirUnreadable"] = [p for p in rec["skippedIsolation"]]
343
+ return dest, rec
344
+
345
+
346
+ def magic(p: str) -> str:
347
+ with open(p, "rb") as f:
348
+ h = f.read(8)
349
+ if h[:4] == b"PK\x03\x04":
350
+ return "zip-ooxml"
351
+ if h[:8] == b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1":
352
+ return "ole"
353
+ if h[:5] == b"%PDF-":
354
+ return "pdf"
355
+ if h[:4] == b"\x89PNG":
356
+ return "png"
357
+ return "other"
358
+
359
+
360
+ def doc_to_text(p: str):
361
+ """OLE .doc:textutil 转换。
362
+
363
+ 中间产物必须落系统临时目录:否则会在源材料目录里落下 `.doc.txt` 派生文件,
364
+ 污染源目录并被后续盘点重复计入(曾真实发生)。
365
+ """
366
+ with tempfile.TemporaryDirectory() as td:
367
+ out = os.path.join(td, "out.txt")
368
+ r = subprocess.run(["textutil", "-convert", "txt", "-output", out, p],
369
+ capture_output=True, text=True)
370
+ if r.returncode != 0 or not os.path.exists(out):
371
+ return None, f"textutil 失败:{r.stderr.strip()[:120]}"
372
+ with open(out, encoding="utf-8", errors="replace") as fh:
373
+ return fh.read(), None
374
+
375
+
376
+ def docx_to_text(p: str):
377
+ """`.docx` 文本抽取:python-docx 优先(保留表格结构),缺失则回退 textutil。
378
+
379
+ 不得因为缺依赖就静默读不到:评估说明/报告正文多为 .docx,读不到会直接改变审核结论。
380
+ """
381
+ try:
382
+ import docx
383
+ d = docx.Document(p)
384
+ buf = [para.text for para in d.paragraphs]
385
+ for ti, t in enumerate(d.tables):
386
+ buf.append(f"\n[表 {ti + 1}]")
387
+ for row in t.rows:
388
+ buf.append(" | ".join(c.text.replace("\n", " ") for c in row.cells))
389
+ return "\n".join(buf), None
390
+ except ImportError:
391
+ txt, err = doc_to_text(p) # textutil 同样能吃 .docx
392
+ if txt is not None:
393
+ return txt, "python-docx 缺失,已用 textutil 回退(表格结构可能丢失)"
394
+ return None, f"python-docx 缺失且 textutil 回退失败:{err}"
395
+
396
+
397
+ def pdf_to_text(p: str):
398
+ from pypdf import PdfReader
399
+ r = PdfReader(p)
400
+ out = []
401
+ for i, page in enumerate(r.pages, 1):
402
+ out.append(f"\n===== 第 {i} 页 =====")
403
+ out.append(page.extract_text() or "")
404
+ txt = "\n".join(out)
405
+ if len(txt.strip()) < 50:
406
+ return txt, "PDF 无文本层或内容极少(疑扫描件)"
407
+ return txt, None
408
+
409
+
410
+ def xls_dump(p: str):
411
+ import xlrd
412
+ bk = xlrd.open_workbook(p, formatting_info=False)
413
+ buf = []
414
+ for sh in bk.sheets():
415
+ tag = "[隐藏]" if getattr(sh, "visibility", 0) != 0 else ""
416
+ buf.append(f"\n===== sheet: {sh.name}{tag} ({sh.nrows}行 × {sh.ncols}列) =====")
417
+ if tag:
418
+ continue # H0:隐藏 sheet 只记名字,不取其单元格
419
+ for r in range(sh.nrows):
420
+ vals = [str(sh.cell_value(r, c)) for c in range(sh.ncols)]
421
+ if any(v.strip() and v != "0.0" for v in vals):
422
+ buf.append(f"r{r + 1}: " + " | ".join(vals))
423
+ return "\n".join(buf), "xlrd 只读值:公式串不可得且无 soffice 可重算"
424
+
425
+
426
+ def _collapsed_outline_rows(ws) -> set:
427
+ """折叠分组内的行视同隐藏(Excel 折叠后这些行对人不展示)。"""
428
+ levels = {r: (getattr(d, "outlineLevel", 0) or 0) for r, d in ws.row_dimensions.items()}
429
+ if not any(lv > 0 for lv in levels.values()):
430
+ return set()
431
+ summary_below = True
432
+ try:
433
+ pr = ws.sheet_properties.outlinePr
434
+ if pr is not None and pr.summaryBelow is not None:
435
+ summary_below = bool(pr.summaryBelow)
436
+ except Exception:
437
+ pass
438
+ collapsed = {r for r, d in ws.row_dimensions.items() if getattr(d, "collapsed", False)}
439
+ if not collapsed:
440
+ return set()
441
+ out, max_r = set(), ws.max_row
442
+ for r in range(1, max_r + 1):
443
+ lv = levels.get(r, 0)
444
+ if lv <= 0:
445
+ continue
446
+ s = r
447
+ if summary_below:
448
+ while s <= max_r and levels.get(s, 0) >= lv:
449
+ s += 1
450
+ else:
451
+ while s >= 1 and levels.get(s, 0) >= lv:
452
+ s -= 1
453
+ if s in collapsed:
454
+ out.add(r)
455
+ return out
456
+
457
+
458
+ def _collapsed_outline_cols(ws) -> set:
459
+ """折叠分组内的列视同隐藏(列方向同理)。"""
460
+ from openpyxl.utils import column_index_from_string, get_column_letter
461
+ levels = {c: (getattr(d, "outlineLevel", 0) or 0) for c, d in ws.column_dimensions.items()}
462
+ if not any(lv > 0 for lv in levels.values()):
463
+ return set()
464
+ summary_right = True
465
+ try:
466
+ pr = ws.sheet_properties.outlinePr
467
+ if pr is not None and pr.summaryRight is not None:
468
+ summary_right = bool(pr.summaryRight)
469
+ except Exception:
470
+ pass
471
+ collapsed = {c for c, d in ws.column_dimensions.items() if getattr(d, "collapsed", False)}
472
+ if not collapsed:
473
+ return set()
474
+ out = set()
475
+ for c in range(1, ws.max_column + 1):
476
+ letter = get_column_letter(c)
477
+ lv = levels.get(letter, 0)
478
+ if lv <= 0:
479
+ continue
480
+ i = c
481
+ if summary_right:
482
+ while i <= ws.max_column and levels.get(get_column_letter(i), 0) >= lv:
483
+ i += 1
484
+ s = get_column_letter(i)
485
+ else:
486
+ while i >= 1 and levels.get(get_column_letter(i), 0) >= lv:
487
+ i -= 1
488
+ s = get_column_letter(i)
489
+ if s in collapsed:
490
+ out.add(letter)
491
+ return out
492
+
493
+
494
+ def _norm_sheet(name: str) -> str:
495
+ """工作表名归一:去首尾空白 + 全角空格 + 统一大小写。
496
+
497
+ 用于隐藏结构 key 与 openpyxl `ws.title` 的比较——**两侧都必须归一**。
498
+ 历史失效:只对一侧 strip(),凡表名含首尾空格(如 `2-1市场法询价记录 `)即恒返回 0 处引用(漏报)。
499
+ 不做 casefold:公式里的表名是**大小写敏感字面量**,折大小写会把 `'Sheet1'!A1` 与 `Sheet1` 混同,
500
+ 从而漏判“引用隐藏工作表”。
501
+ """
502
+ if not isinstance(name, str):
503
+ return ""
504
+ return name.replace("\u3000", " ").strip()
505
+
506
+
507
+ def _xml_attrs(tag_body: str) -> dict:
508
+ """把标签体解析为属性字典。
509
+
510
+ **不能假设属性顺序**:openpyxl 写 `Target=/xl/… Id=rId1`,而 Excel 常见 `Id=… Target=…`。
511
+ 早期实现用单条 `Id="…" Target="…"` 正则,遇到倒序即静默取空 → 隐藏行/列恒为空集
512
+ (真实失效:workbook.xml.rels 中 Target 在 Id 之前,导致修复前 `_raw_xml_hidden` 永远返回空集合)。
513
+ """
514
+ import re
515
+ return {m.group(1): m.group(2)
516
+ for m in re.finditer(r'([A-Za-z_:][\w:.-]*)\s*=\s*"([^"]*)"', tag_body)}
517
+
518
+
519
+ def _raw_xml_hidden(p: str):
520
+ """从 xlsx 包内 XML 直读隐藏结构(**区段展开**),返回 {sheetName: {"state":…, "cols":set,"rows":set}}。
521
+
522
+ 为什么必须读原始 XML:openpyxl 把 `<col min="10" max="11" hidden="1"/>` 这样的**区段**只挂在首列
523
+ (J)上,`ws.column_dimensions` 因此看不到 K —— 只按它取隐藏集合会**漏剔区段内的其余列**,
524
+ 把人工隐藏的内容当成可见内容读进工作版,进而产出假阳性意见(2026-302150-LX9757-BG8677 实测:
525
+ 03/04 两簿共 10 个真实隐藏列被误当可见)。行同理(`<row r="…" hidden="1"/>`)。
526
+ """
527
+ import re
528
+ import zipfile
529
+
530
+ out = {}
531
+ try:
532
+ z = zipfile.ZipFile(p)
533
+ except Exception:
534
+ return out
535
+ with z:
536
+ try:
537
+ wb = z.read("xl/workbook.xml").decode("utf-8", "replace")
538
+ except KeyError:
539
+ return out
540
+ rels = ""
541
+ try:
542
+ rels = z.read("xl/_rels/workbook.xml.rels").decode("utf-8", "replace")
543
+ except KeyError:
544
+ pass
545
+ targets = {}
546
+ for rel in re.finditer(r"<Relationship\b([^>]*)/?>", rels):
547
+ a = _xml_attrs(rel.group(1))
548
+ rid, tgt = a.get("Id"), a.get("Target")
549
+ if rid and tgt:
550
+ targets[rid] = tgt
551
+ for m in re.finditer(r"<sheet\b([^>]*)/?>", wb):
552
+ a = _xml_attrs(m.group(1))
553
+ name = a.get("name")
554
+ if not name:
555
+ continue
556
+ state = a.get("state") or "visible"
557
+ target = targets.get(a.get("r:id") or "", "")
558
+ cols, rows = set(), set()
559
+ if target:
560
+ # Target 三种写法都要认:`/xl/worksheets/sheet1.xml`(绝对,openpyxl 即此)
561
+ # / `worksheets/sheet1.xml`(相对 rels 所在的 xl/) / `xl/…`(已带前缀)。
562
+ # 早期实现直接 `"xl/" + target.lstrip("/")` 把绝对路径拼成 `xl/xl/…`,
563
+ # KeyError 被 except 吞掉 → 隐藏行/列**恒为空集**(本次修复的真正根因)。
564
+ t = target.replace("\\", "/").lstrip("/")
565
+ for cand in (t, "xl/" + t):
566
+ try:
567
+ xml = z.read(cand).decode("utf-8", "replace")
568
+ break
569
+ except KeyError:
570
+ continue
571
+ else:
572
+ xml = ""
573
+ for c in re.finditer(r"<col\b([^>]*)/?>", xml):
574
+ ca = _xml_attrs(c.group(1))
575
+ if str(ca.get("hidden", "")).lower() not in ("1", "true"):
576
+ continue
577
+ if "min" not in ca:
578
+ continue
579
+ lo = int(ca["min"])
580
+ hi = int(ca["max"]) if ca.get("max") else lo
581
+ for i in range(lo, hi + 1): # 区段展开:这是本函数存在的理由
582
+ cols.add(i)
583
+ for r_ in re.finditer(r"<row\b([^>]*)/?>", xml):
584
+ ra = _xml_attrs(r_.group(1))
585
+ if str(ra.get("hidden", "")).lower() not in ("1", "true"):
586
+ continue
587
+ if ra.get("r"):
588
+ rows.add(int(ra["r"]))
589
+ out[name] = {"state": state, "cols": cols, "rows": rows}
590
+ return out
591
+
592
+
593
+ def _raw_media_count(p: str) -> int:
594
+ """raw 原件 `xl/media/` 内的媒体对象数(图片等)。
595
+
596
+ 重建工作版只复制单元格值与格式,**必然**丢掉全部媒体;而审核对象是工作版,
597
+ 于是"证据不在单元格里"的检查项会被判成"缺失"。
598
+ 真实失效(2026-302150-LX9757-BG8677):`MKT-004` 判"可比实例位置图为空、
599
+ 询价截图缺失",而原件 `04评估计算表.xlsx` 实有 27 张内嵌图(含带 4.6/5.4/3.8 km
600
+ 标注的位置图)——工作版 media 为 0,审核对象已非送审件原貌。
601
+ 故这里登记原件媒体数,让"未核 ≠ 缺失"有据可依。
602
+ """
603
+ import zipfile
604
+ try:
605
+ with zipfile.ZipFile(p) as z:
606
+ return sum(1 for n in z.namelist()
607
+ if n.startswith("xl/media/") and not n.endswith("/"))
608
+ except Exception:
609
+ return 0
610
+
611
+
612
+ def _hidden_metadata(wb_value, wb_formula, path: str | None = None) -> dict:
613
+ """只读结构元数据以识别隐藏区;隐藏 sheet 只记名字,不枚举其行列。
614
+
615
+ `cols/rows` 优先取 raw XML 的**展开后**真实集合(`path` 给出时);raw XML 不可得才退回
616
+ openpyxl(此时区段内其余列会漏,属降级,会在 note 中体现为 hiddenMetaSource)。
617
+ """
618
+ from openpyxl.utils import get_column_letter
619
+
620
+ raw = _raw_xml_hidden(path) if path else {}
621
+ hidden = {"hiddenSheets": [], "hiddenRows": {}, "hiddenCols": {},
622
+ "hiddenMetaSource": "raw-xml" if raw else "openpyxl"}
623
+ for ws in wb_value.worksheets:
624
+ rinfo = raw.get(ws.title)
625
+ if rinfo and rinfo["state"] != "visible":
626
+ hidden["hiddenSheets"].append(ws.title)
627
+ continue
628
+ if ws.sheet_state != "visible":
629
+ hidden["hiddenSheets"].append(ws.title)
630
+ continue
631
+ if rinfo:
632
+ rows = set(rinfo["rows"]) | _collapsed_outline_rows(ws)
633
+ cols = {get_column_letter(i) for i in rinfo["cols"]} | _collapsed_outline_cols(ws)
634
+ else:
635
+ rows = {r for r, d in ws.row_dimensions.items() if d.hidden} | _collapsed_outline_rows(ws)
636
+ cols = {c for c, d in ws.column_dimensions.items() if d.hidden} | _collapsed_outline_cols(ws)
637
+ if rows:
638
+ hidden["hiddenRows"][ws.title] = sorted(rows)
639
+ if cols:
640
+ hidden["hiddenCols"][ws.title] = sorted(cols)
641
+ return hidden
642
+
643
+
644
+ # ---- 隐藏区引用审计(H0 内合法动作:只读「可见区公式的坐标」,绝不读隐藏区内容) ----
645
+ _CELL_RE = r"\$?[A-Z]{1,3}\$?\d{1,7}"
646
+ _REF_RE = re.compile(
647
+ r"(?:(?:'(?P<q>[^']+)'|(?P<s>[A-Za-z\u4e00-\u9fff_][^!:'\[\]]*))!)?"
648
+ r"(?P<a>" + _CELL_RE + r")(?::(?P<b>" + _CELL_RE + r"))?(?!\s*\()"
649
+ )
650
+
651
+
652
+ def _coord(ref: str):
653
+ from openpyxl.utils import column_index_from_string
654
+ ref = ref.replace("$", "")
655
+ i = 0
656
+ while i < len(ref) and not ref[i].isdigit():
657
+ i += 1
658
+ return int(ref[i:]), column_index_from_string(ref[:i])
659
+
660
+
661
+ def _span(a: str, b: str):
662
+ """范围涉及的 (行集合, 列集合);无 b 时即单格。"""
663
+ r1, c1 = _coord(a)
664
+ r2, c2 = _coord(b) if b else (r1, c1)
665
+ return set(range(min(r1, r2), max(r1, r2) + 1)), set(range(min(c1, c2), max(c1, c2) + 1))
666
+
667
+
668
+ def audit_hidden_references(wbf, hidden: dict):
669
+ """逐「可见格公式」解析其引用坐标,判定是否指向隐藏行/列/隐藏工作表。
670
+
671
+ 只读可见格的公式串;隐藏行/列/隐藏 sheet 的单元格**一律不读**(H0)。
672
+ 用途:判定"可见结果是否依赖不可见的计算输入" → **人工建议检查项**(`manualConfirmationItems[]`)。
673
+
674
+ 去向(2026-09-16 用户口径):审核只要**可见区公式计算正确**即可,因此"引用了隐藏区"**不得**出成 AI 问题、
675
+ 不得据此升级严重度;本函数只产出**审计元数据**(供生成建议项)。可见区**自身**算错仍按普通可见区缺陷定级。
676
+
677
+ 表名归一:工作表名常带首尾空格(如 `2-1市场法询价记录 `),此前用 strip() 后的名字与未 strip 的
678
+ `ws.title` 比较,**凡表名含首尾空格即恒返回 0 处引用**(漏报)。此处双方统一 `_norm_sheet()` 归一,
679
+ 并在"隐藏结构里出现了但没有任何可见表与之匹配"时抛错,**不得静默返回 0**。
680
+ """
681
+ from openpyxl.utils import column_index_from_string
682
+ hidden_sheets = {_norm_sheet(s) for s in hidden.get("hiddenSheets", [])}
683
+ hid_rows = {_norm_sheet(s): set(v) for s, v in hidden.get("hiddenRows", {}).items()}
684
+ # 忽略清单里列用字母(给人看),比对时必须换算成列序号
685
+ hid_cols = {_norm_sheet(s): {column_index_from_string(c) for c in v}
686
+ for s, v in hidden.get("hiddenCols", {}).items()}
687
+ visible = [ws for ws in wbf.worksheets if ws.sheet_state == "visible"]
688
+ visible_norm = {_norm_sheet(ws.title) for ws in visible}
689
+ declared = set(hid_rows) | set(hid_cols)
690
+ unmatched = sorted(declared - visible_norm)
691
+ if unmatched:
692
+ raise RuntimeError(
693
+ "隐藏区引用审计无法完成:隐藏结构中的工作表 {0} 与任何可见工作表名不匹配"
694
+ "(表名归一后仍不一致)→ 不得视为'0 处引用'".format(unmatched))
695
+ hits, seen = [], set()
696
+ for ws in visible:
697
+ key_title = _norm_sheet(ws.title)
698
+ rows_h = hid_rows.get(key_title, set())
699
+ cols_h = hid_cols.get(key_title, set()) # 列序号集合
700
+ # B1 扫描边界:只遍历**实际存在**的格(精确集合),不用 `ws.iter_rows()`——
701
+ # 后者按 dimension 扫满矩形,遇游离格式格会把 1M 行全走一遍
702
+ # (2026-302135-LX9619-BG8634 实测该表单此一处 69.9 s;改后 0.12 s)。
703
+ for (r_, c_), cell in _cells_of(ws).items():
704
+ if r_ in rows_h or c_ in cols_h:
705
+ continue # H0:隐藏格不读
706
+ f = cell.value
707
+ if not isinstance(f, str) or not f.startswith("="):
708
+ continue
709
+ for m in _REF_RE.finditer(f):
710
+ sheet = _norm_sheet(m.group("q") or m.group("s") or ws.title)
711
+ kind = None
712
+ if sheet in hidden_sheets:
713
+ kind = "引用隐藏工作表"
714
+ elif sheet == key_title:
715
+ rows, cols = _span(m.group("a"), m.group("b"))
716
+ if rows & rows_h:
717
+ kind = "引用隐藏行"
718
+ elif cols & cols_h:
719
+ kind = "引用隐藏列"
720
+ if kind:
721
+ key = (ws.title, cell.coordinate, m.group(0))
722
+ if key in seen:
723
+ continue
724
+ seen.add(key)
725
+ hits.append({"sheet": ws.title, "cell": cell.coordinate,
726
+ "kind": kind, "ref": m.group(0)})
727
+ return hits
728
+
729
+
730
+ def _cells_of(ws) -> dict:
731
+ """该表**实际存在**的格:`{(row, col): Cell}`。
732
+
733
+ 注意"存在"≠"有值":原件常见游离的 `has_style=True`、`value=None` 空格
734
+ (整列/整行刷格式的残留)。二者必须分开用——见 `_sheet_bounds` 与 `_content_coords`。
735
+ """
736
+ cells = getattr(ws, "_cells", None)
737
+ if not isinstance(cells, dict):
738
+ return {}
739
+ return {k: c for k, c in cells.items() if isinstance(k, tuple) and len(k) == 2}
740
+
741
+
742
+ def _content_coords(ws, wsf=None) -> set:
743
+ """**有值**格的坐标集合:缓存值非 `None`(`ws`)∪ 公式串非空(`wsf`)。样式不算。
744
+
745
+ 与重建循环的取舍口径一致(`if v is None and f is None: continue`):
746
+ 纯格式格本来就不进工作版,因此它们既不该参与边界判定、也不该被遍历。
747
+ """
748
+ out = set()
749
+ for w in (ws, wsf):
750
+ if w is None:
751
+ continue
752
+ for key, cell in _cells_of(w).items():
753
+ if getattr(cell, "value", None) is not None:
754
+ out.add(key)
755
+ return out
756
+
757
+
758
+ def _declared_span(ws) -> tuple:
759
+ """原件**声明**的用区上界 `(maxRow, maxCol)`,取其 dimension 字面,不重建边界。"""
760
+ try:
761
+ dim = ws.calculate_dimension()
762
+ except Exception:
763
+ return 0, 0
764
+ m = re.match(r"^\$?([A-Z]{1,3})\$?(\d+)(?::\$?([A-Z]{1,3})\$?(\d+))?$", (dim or "").strip())
765
+ if not m:
766
+ return 0, 0
767
+ from openpyxl.utils import column_index_from_string
768
+ return int(m.group(4) or m.group(2)), column_index_from_string(m.group(3) or m.group(1))
769
+
770
+
771
+ def _sheet_anomaly(ws, content_rows: int, content_cols: int, style_only: int):
772
+ """B3:声明用区与实际有值区相差上千行/列 → 该表不规范,登记(**不改变遍历**)。
773
+
774
+ 这类表会让**任何**按 dimension 遍历的下游工具一起变慢,属可见区表格规范缺陷,
775
+ 与 `hiddenStructureDrift` 同为一类提示。
776
+ """
777
+ declared_rows, declared_cols = _declared_span(ws)
778
+ orphan_rows = max(0, declared_rows - content_rows)
779
+ orphan_cols = max(0, declared_cols - content_cols)
780
+ if orphan_rows <= ORPHAN_SPAN_ROWS and orphan_cols <= ORPHAN_SPAN_COLS:
781
+ return None
782
+ return {"sheet": ws.title,
783
+ "declaredDim": ws.calculate_dimension(),
784
+ "contentRows": content_rows, "contentCols": content_cols,
785
+ "orphanRows": orphan_rows, "orphanCols": orphan_cols,
786
+ "styleOnlyCells": style_only,
787
+ "note": "声明用区远大于实际有值区(疑整行/整列刷格式残留);"
788
+ "本次按有值区处理,不影响审核范围"}
789
+
790
+
791
+ def _sheet_bounds(ws, wsf=None) -> tuple:
792
+ """真实**用区**上界 (maxRow, maxCol):只按**有值**的格算,用于遍历边界。
793
+
794
+ 三段历史,缺一不可:
795
+
796
+ 1. 不能直接用 `ws.max_row/max_column`:原件常带被虚增的 dimension(如 `A1:Y1048575`),
797
+ 按它遍历会退化成百万行循环。
798
+ 2. 也不能只按 `_cells` 的**存在格**算:第 1048575 行常有一个 `has_style=True`、
799
+ `value=None` 的游离格式格,它同样把边界顶到 104 万行
800
+ (2026-302135-LX9619-BG8634 实测:944 个存在格中 697 个纯格式 + 1 个游离在末行
801
+ → 2614 万次循环 → 单表 56 s)。
802
+ 3. 故按**有值格**定界;声明用区与实际用区的差距交由 `_sheet_anomaly()` 单独登记,
803
+ 不丢信息。
804
+ """
805
+ coords = _content_coords(ws, wsf)
806
+ if coords:
807
+ return max(r for r, _ in coords), max(c for _, c in coords)
808
+ dim = ws.calculate_dimension(force=True)
809
+ m = re.match(r"^[A-Z]+(\d+)(?::[A-Z]+(\d+))?$", dim or "")
810
+ if m:
811
+ from openpyxl.utils import column_index_from_string
812
+ parts = (dim.split(":") + [dim])[:2]
813
+ cols = [column_index_from_string(re.match(r"^([A-Z]+)", x).group(1)) for x in parts]
814
+ rows_ = [int(re.search(r"(\d+)$", x).group(1)) for x in parts]
815
+ return max(rows_), max(cols)
816
+ return ws.max_row, ws.max_column
817
+
818
+
819
+ def xlsx_visible(p: str, outdir: str, out_name: str):
820
+ """重建法:新建簿,只复制「可见 sheet × 可见行 × 可见列」的值(缓存值优先)与格式。
821
+
822
+ 禁止用 delete_rows/delete_cols 逐行列删除(会残留维度元数据)。
823
+ 隐藏集合取 **raw XML 展开后的真实区段**(`_hidden_metadata(..., path=p)`),
824
+ 遍历边界取真实用区(`_sheet_bounds`),避免"隐藏区漏剔"与"虚增 dimension 长循环"两类失效。
825
+ 返回 `(out, hidden, stats, resid, unavailable, refs, anomalies, gaps)`。
826
+ """
827
+ import openpyxl
828
+ from openpyxl.utils import get_column_letter
829
+
830
+ src = openpyxl.load_workbook(p, data_only=True) # 缓存值
831
+ srcf = openpyxl.load_workbook(p, data_only=False) # 公式串
832
+ hidden = _hidden_metadata(src, srcf, path=p)
833
+
834
+ dst = openpyxl.Workbook()
835
+ dst.remove(dst.active)
836
+ stats, unavailable, anomalies, gaps = [], [], [], []
837
+ for ws in src.worksheets:
838
+ if ws.sheet_state != "visible":
839
+ continue # H0:隐藏 sheet 整体跳过,不读其任何单元格
840
+ wsf = srcf[ws.title]
841
+ hidden_rows = set(hidden["hiddenRows"].get(ws.title, []))
842
+ hidden_cols = set(hidden["hiddenCols"].get(ws.title, []))
843
+ # B4 硬护栏:按**规模**(有值格数)裁,不按空洞裁。超限记 gap 并跳过该表,其余表继续。
844
+ coords = _content_coords(ws, wsf)
845
+ if len(coords) > MAX_SHEET_CELLS:
846
+ gaps.append({"sheet": ws.title,
847
+ "reason": f"有值格 {len(coords)} 超上限 {MAX_SHEET_CELLS},该表本次未完整处理"})
848
+ continue
849
+ o = dst.create_sheet(ws.title[:31])
850
+ max_row, max_col = _sheet_bounds(ws, wsf)
851
+ # B1 扫描边界:只遍历**有值**的格(精确集合),不按矩形扫。
852
+ # 按 dimension(或按"存在的格")扫矩形,会在游离格式格上退化成百万行循环
853
+ # (2026-302135-LX9619-BG8634 实测 26,214,375 次 → 单表 56 s;改后 0.00 s)。
854
+ # 行号语义不变:`vis_rows` 仍是「1..有值末行 减去隐藏行」。
855
+ vis_rows = [r for r in range(1, max_row + 1) if r not in hidden_rows]
856
+ row_out = {r: i for i, r in enumerate(vis_rows, 1)}
857
+ used = no_cached = 0
858
+ for r, c in sorted(coords):
859
+ ri = row_out.get(r)
860
+ if ri is None or get_column_letter(c) in hidden_cols:
861
+ continue # H0:隐藏行 / 隐藏列不读
862
+ v = ws.cell(r, c).value
863
+ f = wsf.cell(r, c).value
864
+ if v is None and f is None:
865
+ continue # 双簿口径兜底(有值格集合已保证至少一簿非 None)
866
+ cell = o.cell(ri, c)
867
+ if v is not None:
868
+ cell.value = v # 缓存值优先
869
+ if f is not None:
870
+ cell.number_format = wsf.cell(r, c).number_format
871
+ elif isinstance(f, str) and f.startswith("="):
872
+ cell.value = f # 仅当无缓存值时保留公式串
873
+ cell.number_format = wsf.cell(r, c).number_format
874
+ no_cached += 1
875
+ used += 1
876
+ if no_cached:
877
+ unavailable.append({"sheet": ws.title, "cells": no_cached})
878
+ entry = {"sheet": ws.title, "visibleRows": len(vis_rows), "cells": used,
879
+ "maxRow": max_row, "maxCol": max_col,
880
+ "valueUnavailable": no_cached}
881
+ # D3 不许丢信息:有值区与声明用区不一致时,把声明用区一并记账。
882
+ # 未超 B3 阈值(<1000 行/列)的表不会进 sheetAnomalies,此处是其唯一留痕处。
883
+ declared = _declared_span(ws)
884
+ if declared != (max_row, max_col):
885
+ entry["declaredSpan"] = {"maxRow": declared[0], "maxCol": declared[1]}
886
+ stats.append(entry)
887
+ # B3 异常登记(仅提示,不改变遍历):声明用区远大于实际有值区 → 表格规范缺陷
888
+ anomaly = _sheet_anomaly(ws, max_row, max_col, len(_cells_of(ws)) - len(coords))
889
+ if anomaly:
890
+ anomalies.append(anomaly)
891
+ os.makedirs(outdir, exist_ok=True)
892
+ out = os.path.join(outdir, out_name)
893
+ dst.save(out)
894
+
895
+ # 契约要求(refs/00 §Excel):保存后重新打开工作版,验证隐藏区数量为 0。
896
+ chk = openpyxl.load_workbook(out)
897
+ resid = sum(1 for ws in chk.worksheets if ws.sheet_state != "visible")
898
+ refs = audit_hidden_references(srcf, hidden)
899
+ return out, hidden, stats, resid, unavailable, refs, anomalies, gaps
900
+
901
+
902
+ def prepare(case: str, src_dir: str, txt_dir: str, work_dir: str,
903
+ extract_dir: str = None, media_dir: str = None,
904
+ inventory_name: str = "材料盘点.json",
905
+ media_index_name: str = "媒体索引.json") -> dict:
906
+ """盘点 + 隔离 + 解压 + 工作版重建 + 媒体证据导出。
907
+
908
+ 队列式处理:源材料先入队;**归档解压后的文件以同一套逻辑继续处理**(again 提取文本、
909
+ 重建工作版、导出媒体),并在盘点中记录来源归档(originArchive),可追溯。
910
+
911
+ 阶段二复跑复核件时必须换 `inventory_name` / `media_index_name`(见 `--label`):
912
+ `材料盘点.json` 属阶段一冻结产物,不得被第二次运行覆盖。
913
+ """
914
+ os.makedirs(txt_dir, exist_ok=True)
915
+ os.makedirs(work_dir, exist_ok=True)
916
+ extract_dir = os.path.abspath(extract_dir or os.path.join(case, "解压"))
917
+ media_dir = os.path.abspath(media_dir or os.path.join(case, "媒体证据"))
918
+ media_entries, budget = [], media_extract._Budget()
919
+ inv, seen_xlsx, archives = [], set(), []
920
+ hidden_by_stem = {} # 同名(跨版本)文件的隐藏结构,用于元数据级跨版本比对
921
+ sheet_anomalies = [] # B3:声明用区远大于实际有值区的表(表格规范提示,非审核范围变更)
922
+ queue = []
923
+ for root, _, fs in os.walk(src_dir):
924
+ for f in sorted(fs):
925
+ if f.startswith(".") or f.endswith(".part"):
926
+ continue
927
+ queue.append((os.path.join(root, f), None))
928
+
929
+ def process(p: str, origin):
930
+ rel = os.path.relpath(p, case)
931
+ ext = os.path.splitext(p)[1].lower()
932
+ kind = magic(p)
933
+ rec = {"path": rel, "name": os.path.basename(p),
934
+ "stage": os.path.basename(os.path.dirname(p)),
935
+ "ext": ext, "magic": kind, "size": os.path.getsize(p)}
936
+ if origin:
937
+ rec["originArchive"] = origin
938
+ extracted = []
939
+ try:
940
+ if is_archive(os.path.basename(p)):
941
+ # 下载后必须先解压再审核:解压产物落派生目录,源材料目录零写入
942
+ dest, arec = extract_archive(p, extract_dir)
943
+ arec["archive"] = rel
944
+ archives.append(arec)
945
+ rec["readable"] = True
946
+ rec["archive"] = {"destDir": os.path.relpath(dest, case),
947
+ "entries": arec["entries"],
948
+ "extracted": len(arec["extracted"]),
949
+ "skippedIsolation": arec["skippedIsolation"],
950
+ "failures": arec["failures"],
951
+ "capabilityGaps": arec["capabilityGaps"]}
952
+ for name in arec["extracted"]:
953
+ extracted.append(os.path.join(dest, name))
954
+ notes = []
955
+ if arec["skippedIsolation"]:
956
+ notes.append(f"按阶段一隔离门禁跳过 {len(arec['skippedIsolation'])} 项(复核记录类)")
957
+ if arec["failures"]:
958
+ notes.append(f"跳过 {len(arec['failures'])} 项(越界/链接等)")
959
+ if arec["capabilityGaps"]:
960
+ notes.append(";".join(arec["capabilityGaps"]))
961
+ rec["note"] = ";".join(notes) if notes else "已解压,随解压产物继续盘点"
962
+ elif ext == ".doc" and kind == "ole":
963
+ txt, err = doc_to_text(p)
964
+ rec["readable"] = txt is not None
965
+ if txt:
966
+ with open(os.path.join(txt_dir, os.path.basename(p) + ".txt"), "w",
967
+ encoding="utf-8") as fh:
968
+ fh.write(txt)
969
+ rec["chars"] = len(txt)
970
+ if err:
971
+ rec["note"] = err
972
+ elif ext == ".docx":
973
+ txt, err = docx_to_text(p)
974
+ rec["readable"] = txt is not None
975
+ if txt is not None:
976
+ with open(os.path.join(txt_dir, os.path.basename(p) + ".txt"), "w",
977
+ encoding="utf-8") as fh:
978
+ fh.write(txt)
979
+ rec["chars"] = len(txt)
980
+ if err:
981
+ rec["note"] = err
982
+ elif ext == ".pdf":
983
+ _ok, _parsed = _call_with_dependency(rec, rel, "pypdf", ext, pdf_to_text, p)
984
+ if not _ok:
985
+ raise DependencyUnavailable()
986
+ txt, err = _parsed
987
+ rec["readable"] = not err
988
+ with open(os.path.join(txt_dir, os.path.basename(p) + ".txt"), "w",
989
+ encoding="utf-8") as fh:
990
+ fh.write(txt)
991
+ rec["chars"] = len(txt)
992
+ if err:
993
+ rec["note"] = err
994
+ elif ext == ".xlsx":
995
+ out_name = os.path.basename(p)
996
+ if out_name in seen_xlsx:
997
+ stem, se = os.path.splitext(out_name)
998
+ out_name = f"{stem}__{os.path.basename(os.path.dirname(p))}{se}"
999
+ rec["nameCollision"] = True
1000
+ seen_xlsx.add(out_name)
1001
+ _ok, _built = _call_with_dependency(
1002
+ rec, rel, "openpyxl", ext, xlsx_visible, p, work_dir, out_name)
1003
+ if not _ok:
1004
+ raise DependencyUnavailable()
1005
+ out, hidden, stats, resid, unavailable, refs, anomalies, wb_gaps = _built
1006
+ rec["readable"] = True
1007
+ media_n = _raw_media_count(p)
1008
+ rec["workbook"] = {"workVersion": os.path.relpath(out, case),
1009
+ "sheets": stats, "hiddenResidual": resid,
1010
+ "hiddenMeta": hidden,
1011
+ "valueUnavailable": unavailable,
1012
+ "hiddenRefs": refs[:50],
1013
+ "hiddenRefsCount": len(refs),
1014
+ "sheetAnomalies": anomalies,
1015
+ "capabilityGaps": wb_gaps,
1016
+ # 重建法**必然**丢弃全部媒体(图片/形状/图表)与页眉页脚/批注:
1017
+ # openpyxl 新建簿只复制单元格值与 number_format。
1018
+ # 这里登记原件媒体数,供"禁止依据工作版判缺失"这条铁律有据可依。
1019
+ "rawMediaCount": media_n,
1020
+ "mediaCarriedOver": 0,
1021
+ "nonCellEvidenceNote": (
1022
+ "工作版不含媒体/页眉页脚/批注等非单元格证据;"
1023
+ "凡'不存在/缺失/为空/未列示'类结论禁止依据工作版下判断,"
1024
+ "必须回 raw 原件直读" if media_n else None),
1025
+ "calcChainNotReproducible": sorted(
1026
+ {f"{h['sheet']}!{h['cell']}" for h in refs}),
1027
+ # 去向随数据一起走:引用隐藏区只出人工建议检查项,不作 AI 问题
1028
+ # (2026-09-16 用户口径;定级见 references/12-leaf-common-contract.md §7.1)
1029
+ "hiddenRefDisposition": (
1030
+ "manualConfirmationItems(人工建议检查项,不作为 AI 问题;"
1031
+ "只为被可见公式引用的隐藏区出;可见区自身算错照常出问题)"
1032
+ if refs else None)}
1033
+ for a in anomalies: # B3:表格规范提示,逐表登记
1034
+ sheet_anomalies.append({"path": rel, "stage": rec["stage"], **a})
1035
+ for g in wb_gaps: # B4:超限未处理,如实记账
1036
+ rec.setdefault("capabilityGaps", []).append(
1037
+ f"{g['sheet']}:{g['reason']}")
1038
+ # 跨版本隐藏结构比对(只比对元数据,不读隐藏内容)
1039
+ stem = os.path.basename(p).split(".")[0]
1040
+ hidden_by_stem.setdefault(stem, []).append(
1041
+ {"path": rel, "stage": rec["stage"], "hiddenMeta": hidden})
1042
+ if resid:
1043
+ rec["note"] = f"隐藏区残留 {resid}:需按重建法重做,仍残留则挂起"
1044
+ with open(os.path.join(txt_dir, os.path.basename(p) + ".sheets.json"), "w",
1045
+ encoding="utf-8") as fh:
1046
+ fh.write(json.dumps(stats, ensure_ascii=False, indent=1))
1047
+ elif ext == ".xls" and kind == "ole":
1048
+ _ok, _dumped = _call_with_dependency(rec, rel, "xlrd", ext, xls_dump, p)
1049
+ if not _ok:
1050
+ raise DependencyUnavailable()
1051
+ txt, note = _dumped
1052
+ rec["readable"] = True
1053
+ rec["note"] = note
1054
+ with open(os.path.join(txt_dir, os.path.basename(p) + ".txt"), "w",
1055
+ encoding="utf-8") as fh:
1056
+ fh.write(txt)
1057
+ elif kind == "png" or ext in (".jpg", ".jpeg", ".gif", ".bmp", ".webp", ".tif", ".tiff"):
1058
+ rec["readable"] = False # 文本抽取口径:图片本机无 OCR,仍不是"文本可读"
1059
+ rec["note"] = "图片:本机无 OCR;媒体证据由媒体通道导出,判读交宿主多模态(见 mediaEvidence)"
1060
+ elif kind == "zip-ooxml" or ext == ".zip":
1061
+ rec["readable"] = False
1062
+ rec["note"] = "压缩包:未解压审核"
1063
+ else:
1064
+ rec["readable"] = False
1065
+ rec["note"] = f"未支持格式 magic={kind}"
1066
+ except DependencyUnavailable:
1067
+ # 本件因**该格式的顶层依赖**不可用而未核:分支内已通过 `_call_with_dependency`
1068
+ # 精确记账(readable / dependencyGaps / capabilityGaps / notChecked /
1069
+ # workVersionCreated 都已写好)——这里只跳过本件,继续处理下一个文件。
1070
+ #
1071
+ # 依赖归因**只**发生在各格式分支内,这里不再按 `e.name` 猜依赖(跨格式推导正是
1072
+ # R1/R2 要消灭的形态)。
1073
+ pass
1074
+ except Exception as e:
1075
+ rec["readable"] = False
1076
+ rec["note"] = f"{type(e).__name__}: {e}"
1077
+
1078
+ # 媒体证据通道(所有格式统一走这里):把可见锚点的图片/独立图片导出到
1079
+ # `媒体证据/`,并登记计数与未核原因。H0:隐藏锚点不导出、只记数量。
1080
+ try:
1081
+ hidden_meta = (rec.get("workbook") or {}).get("hiddenMeta")
1082
+ mres = media_extract.plan_media(
1083
+ p, ext, kind, hidden_meta, media_dir, case, rel, rec["stage"],
1084
+ origin=origin, budget=budget)
1085
+ if mres["entries"]:
1086
+ media_entries.extend(mres["entries"])
1087
+ if mres["summary"]:
1088
+ rec["mediaEvidence"] = mres["summary"]
1089
+ except Exception as e: # noqa: BLE001
1090
+ rec["capabilityGaps"] = list(rec.get("capabilityGaps") or []) + [
1091
+ f"媒体证据导出失败({type(e).__name__}: {e}):该件图片本次未核,未核 ≠ 缺失"]
1092
+ return rec, extracted
1093
+
1094
+ i = 0
1095
+ while i < len(queue):
1096
+ p, origin = queue[i]
1097
+ i += 1
1098
+ rec, extracted = process(p, origin)
1099
+ inv.append(rec)
1100
+ for ex in extracted: # 解压产物继续按同一套逻辑处理
1101
+ queue.append((ex, rec["path"]))
1102
+ print(f"[{'OK ' if rec.get('readable') else 'NG '}] {rec['path']} {rec.get('note', '')}",
1103
+ flush=True)
1104
+
1105
+ drift = []
1106
+ for stem, entries in hidden_by_stem.items():
1107
+ metas = {}
1108
+ for e in entries:
1109
+ key = json.dumps(e["hiddenMeta"], ensure_ascii=False, sort_keys=True)
1110
+ metas.setdefault(key, []).append({"path": e["path"], "stage": e["stage"]})
1111
+ if len(metas) > 1:
1112
+ drift.append({
1113
+ "stem": stem,
1114
+ "note": "同一材料多版本隐藏结构不一致(仅元数据比对,未读隐藏内容)——可见区表格规范提示",
1115
+ "variants": sorted(
1116
+ ({"stages": sorted({e["stage"] for e in v}),
1117
+ "hiddenSheets": list(next(iter(v)).get("hiddenMeta", {}).get("hiddenSheets", [])),
1118
+ "hiddenRowsCount": sum(len(x) for x in next(iter(v)).get("hiddenMeta", {}).get("hiddenRows", {}).values()),
1119
+ "hiddenColsCount": sum(len(x) for x in next(iter(v)).get("hiddenMeta", {}).get("hiddenCols", {}).values())}
1120
+ for v in metas.values()),
1121
+ key=lambda d: sorted(d["stages"]))
1122
+ })
1123
+ # 非单元格证据:工作版必然丢媒体,汇总登记(供"未核 ≠ 缺失"与"原件直读"口径使用)。
1124
+ # 覆盖**全部格式**(此前只登记 xlsx 的 xl/media 计数;docx/doc/pdf/独立图片全无痕迹)。
1125
+ non_cell = []
1126
+ for r in inv:
1127
+ m = r.get("mediaEvidence") or {}
1128
+ raw_n = (r.get("workbook") or {}).get("rawMediaCount") or 0
1129
+ if not (m or raw_n):
1130
+ continue
1131
+ non_cell.append({
1132
+ "path": r["path"], "stage": r.get("stage"),
1133
+ "rawMediaCount": raw_n,
1134
+ "exportedCount": m.get("exportedCount", 0),
1135
+ "hiddenSkippedCount": m.get("hiddenSkippedCount", 0),
1136
+ "unresolvedCount": m.get("unresolvedCount", 0),
1137
+ "unresolvedReasons": m.get("unresolvedReasons", []),
1138
+ "localDir": m.get("localDir"),
1139
+ })
1140
+ non_cell_summary = {
1141
+ "note": "工作版不含媒体/页眉页脚/批注等非单元格证据;图片证据已由编排层导出到"
1142
+ "`媒体证据/`(清单见 `" + media_index_name + "`),判读须经宿主多模态读图;"
1143
+ "凡'不存在/缺失/为空/未列示'类结论禁止依据工作版下判断,读不到只能出「未核验」"
1144
+ "——未核 ≠ 缺失",
1145
+ "evidenceChannel": "host-vision",
1146
+ "mediaIndexPath": media_index_name,
1147
+ "filesWithMedia": len(non_cell),
1148
+ "rawMediaCount": sum(x["rawMediaCount"] for x in non_cell),
1149
+ "exportedMediaCount": sum(x["exportedCount"] for x in non_cell),
1150
+ "hiddenSkippedCount": sum(x["hiddenSkippedCount"] for x in non_cell),
1151
+ "unresolvedCount": sum(x["unresolvedCount"] for x in non_cell),
1152
+ "files": non_cell,
1153
+ }
1154
+ payload = {"items": inv, "archives": archives, "hiddenStructureDrift": drift,
1155
+ "sheetAnomalies": sheet_anomalies,
1156
+ "nonCellEvidence": non_cell_summary}
1157
+ inv_path = os.path.join(case, inventory_name)
1158
+ with open(inv_path, "w", encoding="utf-8") as fh:
1159
+ json.dump(payload, fh, ensure_ascii=False, indent=1)
1160
+ media_index_path = os.path.join(case, media_index_name)
1161
+ with open(media_index_path, "w", encoding="utf-8") as fh:
1162
+ json.dump({
1163
+ "note": "本次(本阶段)可从原件判读的媒体证据放行清单;"
1164
+ "`locator` 为受控写法,与交付物一致;锚点落在隐藏区的媒体按 H0 不在清单内",
1165
+ "evidenceChannel": "host-vision",
1166
+ "count": len(media_entries),
1167
+ "budget": {"maxItems": budget.items, "maxBytes": budget.nbytes,
1168
+ "stopped": budget.stopped},
1169
+ "entries": media_entries,
1170
+ }, fh, ensure_ascii=False, indent=1)
1171
+ return {"inventory": inv_path, "items": len(inv),
1172
+ "readable": sum(1 for r in inv if r.get("readable")),
1173
+ "archives": archives, "sheetAnomalies": sheet_anomalies,
1174
+ "nonCellEvidence": non_cell_summary,
1175
+ "mediaIndex": media_index_path,
1176
+ "mediaExported": len(media_entries),
1177
+ "mediaHiddenSkipped": non_cell_summary["hiddenSkippedCount"]}
1178
+
1179
+
1180
+ def main() -> int:
1181
+ ap = argparse.ArgumentParser(description="阶段一材料准备:可读性核查 + 隐藏数据隔离 + 工作版重建 + 媒体证据导出")
1182
+ ap.add_argument("--case", default=os.getcwd(), help="案例目录(默认当前目录)")
1183
+ ap.add_argument("--src", default=None, help="源材料目录(默认 <案例>/材料-源)")
1184
+ ap.add_argument("--txt", default=None, help="文本提取目录(默认 <案例>/提取)")
1185
+ ap.add_argument("--work", default=None, help="工作版目录(默认 <案例>/工作版)")
1186
+ ap.add_argument("--media", default=None, help="媒体证据目录(默认 <案例>/媒体证据)")
1187
+ ap.add_argument("--extract", default=None, help="归档解压目录(默认 <案例>/解压)")
1188
+ ap.add_argument("--inventory", default=None, help="盘点文件名(默认 材料盘点.json)")
1189
+ ap.add_argument("--label", default=None,
1190
+ help="阶段标签(如 复核):各产物名加后缀(提取-复核/工作版-复核/…),"
1191
+ "避免阶段二复跑覆盖阶段一冻结产物;须与 --src 一起指向复核件目录")
1192
+ args = ap.parse_args()
1193
+
1194
+ case = os.path.abspath(args.case)
1195
+
1196
+ def _under_case(value, default_name):
1197
+ """相对路径一律相对 `--case` 解析(否则 `--src 复核-人工` 会落到当前工作目录)。"""
1198
+ if not value:
1199
+ return os.path.join(case, default_name)
1200
+ return os.path.abspath(value) if os.path.isabs(value) else os.path.join(case, value)
1201
+
1202
+ src_dir = _under_case(args.src, "材料-源")
1203
+ if not os.path.isdir(src_dir):
1204
+ print(f"error: 源材料目录不存在:{src_dir}")
1205
+ return 2
1206
+ label = (args.label or "").strip()
1207
+ suffix = f"-{label}" if label else ""
1208
+ result = prepare(
1209
+ case, src_dir,
1210
+ _under_case(args.txt, "提取" + suffix),
1211
+ _under_case(args.work, "工作版" + suffix),
1212
+ extract_dir=_under_case(args.extract, "解压" + suffix),
1213
+ media_dir=_under_case(args.media, "媒体证据" + suffix),
1214
+ inventory_name=args.inventory or (f"{label}盘点.json" if label else "材料盘点.json"),
1215
+ media_index_name=f"{label}媒体索引.json" if label else "媒体索引.json")
1216
+ print(f"\n盘点 {result['items']} 件(含解压产物);可读 {result['readable']} / "
1217
+ f"不可读 {result['items'] - result['readable']};盘点表 {result['inventory']}")
1218
+ for a in result.get("archives", []):
1219
+ print(f" 解压 {os.path.basename(a['archive'])}:{len(a['extracted'])}/{a['entries']} 项 → "
1220
+ f"{os.path.relpath(a['destDir'], os.path.abspath(args.case))}"
1221
+ + (f"(隔离跳过 {len(a['skippedIsolation'])})" if a["skippedIsolation"] else "")
1222
+ + (f"({'; '.join(a['capabilityGaps'])})" if a["capabilityGaps"] else ""))
1223
+ anomalies = result.get("sheetAnomalies", [])
1224
+ if anomalies:
1225
+ print(f"\n表格规范提示 {len(anomalies)} 处(声明用区远大于实际有值区,本次按有值区处理,"
1226
+ f"不影响审核范围;明细见 {os.path.basename(result['inventory'])} 的 sheetAnomalies):")
1227
+ for a in anomalies[:10]:
1228
+ print(f" {a['path']} [{a['sheet']}] 声明 {a['declaredDim']},"
1229
+ f"实际有值 {a['contentRows']}行×{a['contentCols']}列,"
1230
+ f"多出 {a['orphanRows']}行/{a['orphanCols']}列,纯格式格 {a['styleOnlyCells']}")
1231
+ if len(anomalies) > 10:
1232
+ print(f" ……另有 {len(anomalies) - 10} 处")
1233
+ nce = result.get("nonCellEvidence") or {}
1234
+ if nce.get("filesWithMedia"):
1235
+ print(f"\n媒体证据提示:{nce['filesWithMedia']} 个原件含媒体对象(原件媒体部件 "
1236
+ f"{nce['rawMediaCount']} 个),本次**导出可判读媒体 {nce.get('exportedMediaCount', 0)} 个** → "
1237
+ f"{os.path.basename(result['mediaIndex'])};隐藏区锚点按 H0 跳过 "
1238
+ f"{nce.get('hiddenSkippedCount', 0)} 个(不导出、不定位);未核原因 "
1239
+ f"{nce.get('unresolvedCount', 0)} 条。**工作版不含媒体**:图片证据须经宿主多模态读图,"
1240
+ f"读不到只能出「未核验」——未核 ≠ 缺失;凡'不存在/缺失/为空'类结论禁止依据工作版下判断。")
1241
+ for f in nce.get("files", [])[:10]:
1242
+ if f.get("unresolvedCount"):
1243
+ print(f" [{f['path']}] 未核原因:" + ";".join(f.get("unresolvedReasons", [])[:3]))
1244
+ missing = missing_deps()
1245
+ if missing:
1246
+ print(f"提示:缺少可选依赖 {', '.join(missing)}——对应格式可能读不到,"
1247
+ f"不读到的部分必须在审核结论中如实声明(不得当作\"材料缺失\")")
1248
+ return 0
1249
+
1250
+
1251
+ if __name__ == "__main__":
1252
+ raise SystemExit(main())