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.
- package/CHANGELOG.md +1516 -0
- package/LICENSE +26 -0
- package/README.en.md +204 -0
- package/README.md +521 -0
- package/SECURITY.md +39 -0
- package/bin/darwin-arm64/crwu +0 -0
- package/bin/darwin-arm64/dws +0 -0
- package/bin/darwin-arm64/ossutil +0 -0
- package/bin/manifest.json +82 -0
- package/bin/win32-x64/crwu.exe +0 -0
- package/bin/win32-x64/dws.exe +4 -0
- package/bin/win32-x64/ossutil.exe +0 -0
- package/common/skills/.synced.json +8 -0
- package/common/skills/crwu-dws/SKILL.md +208 -0
- package/common/skills/crwu-dws/references/00-/347/233/256/345/275/225/345/277/253/347/205/247schema.md +113 -0
- 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
- 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
- package/common/skills/crwu-dws/scripts/test_dws_source_contract.py +493 -0
- package/common/skills/crwu-h3yun-login/SKILL.md +130 -0
- package/common/skills/crwu-h3yun-query/SKILL.md +192 -0
- package/config/crwu-workbench.yml +26 -0
- package/cordis.patch.yml +46 -0
- package/lib/client.js +10346 -0
- package/lib/index.js +11584 -0
- package/package.json +119 -0
- package/scripts/prepare.mjs +59 -0
- package/skills/README.md +26 -0
- package/skills/crwu/crwu-audit/SKILL.md +152 -0
- package/skills/crwu/crwu-audit/references/00-input-and-route-profile.md +232 -0
- package/skills/crwu/crwu-audit/references/01-report-id-resolution.md +13 -0
- package/skills/crwu/crwu-audit/references/02-scope-classification.md +25 -0
- package/skills/crwu/crwu-audit/references/03-asset-classification.md +27 -0
- package/skills/crwu/crwu-audit/references/04-business-classification.md +63 -0
- package/skills/crwu/crwu-audit/references/05-method-classification.md +66 -0
- package/skills/crwu/crwu-audit/references/06-overlay-classification.md +42 -0
- package/skills/crwu/crwu-audit/references/07-skill-registry.md +57 -0
- package/skills/crwu/crwu-audit/references/08-union-dispatch-rules.md +142 -0
- package/skills/crwu/crwu-audit/references/09-review-risk-classification.md +85 -0
- package/skills/crwu/crwu-audit/references/10-capability-gap-proposal.md +68 -0
- package/skills/crwu/crwu-audit/references/11-html-delivery-spec.md +881 -0
- package/skills/crwu/crwu-audit/references/12-leaf-common-contract.md +202 -0
- package/skills/crwu/crwu-audit/references/13-dingtalk-result-publish.md +88 -0
- package/skills/crwu/crwu-audit/references/14-orchestration-workflow.md +52 -0
- package/skills/crwu/crwu-audit/references/99-maintenance.md +62 -0
- package/skills/crwu/crwu-audit/scripts/README.md +157 -0
- package/skills/crwu/crwu-audit/scripts/audit_delivery.py +2736 -0
- package/skills/crwu/crwu-audit/scripts/audit_result.schema.json +1659 -0
- package/skills/crwu/crwu-audit/scripts/examples/audit-result.sample.json +739 -0
- package/skills/crwu/crwu-audit/scripts/fetch_review_records.py +85 -0
- package/skills/crwu/crwu-audit/scripts/media_extract.py +513 -0
- package/skills/crwu/crwu-audit/scripts/prepare_materials.py +1252 -0
- package/skills/crwu/crwu-audit/scripts/test_audit_delivery.py +1678 -0
- package/skills/crwu/crwu-audit/scripts/test_audit_multiaxis_router.py +1339 -0
- package/skills/crwu/crwu-audit/scripts/test_fetch_review_records.py +77 -0
- package/skills/crwu/crwu-audit/scripts/test_media_extract.py +332 -0
- package/skills/crwu/crwu-audit/scripts/test_prepare_materials.py +1081 -0
- package/skills/crwu/crwu-audit/template/audit-report.html +435 -0
- package/skills/crwu/crwu-audit-asset-asset-group-goodwill/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/00-applicability.md +28 -0
- package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/01-kb-assembly.md +31 -0
- package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/02-review-focus.md +21 -0
- package/skills/crwu/crwu-audit-asset-asset-group-goodwill/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-debt/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-debt/references/00-applicability.md +40 -0
- package/skills/crwu/crwu-audit-asset-debt/references/01-kb-assembly.md +41 -0
- package/skills/crwu/crwu-audit-asset-debt/references/02-review-focus.md +54 -0
- package/skills/crwu/crwu-audit-asset-debt/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-enterprise-value/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-enterprise-value/references/00-applicability.md +31 -0
- package/skills/crwu/crwu-audit-asset-enterprise-value/references/01-kb-assembly.md +35 -0
- package/skills/crwu/crwu-audit-asset-enterprise-value/references/02-review-focus.md +33 -0
- package/skills/crwu/crwu-audit-asset-enterprise-value/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-equipment/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-equipment/references/00-applicability.md +40 -0
- package/skills/crwu/crwu-audit-asset-equipment/references/01-kb-assembly.md +39 -0
- package/skills/crwu/crwu-audit-asset-equipment/references/02-review-focus.md +52 -0
- package/skills/crwu/crwu-audit-asset-equipment/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-intangible/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-intangible/references/00-applicability.md +36 -0
- package/skills/crwu/crwu-audit-asset-intangible/references/01-kb-assembly.md +40 -0
- package/skills/crwu/crwu-audit-asset-intangible/references/02-review-focus.md +68 -0
- package/skills/crwu/crwu-audit-asset-intangible/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-inventory/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-inventory/references/00-applicability.md +28 -0
- package/skills/crwu/crwu-audit-asset-inventory/references/01-kb-assembly.md +31 -0
- package/skills/crwu/crwu-audit-asset-inventory/references/02-review-focus.md +21 -0
- package/skills/crwu/crwu-audit-asset-inventory/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-mining-right/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-mining-right/references/00-applicability.md +31 -0
- package/skills/crwu/crwu-audit-asset-mining-right/references/01-kb-assembly.md +35 -0
- package/skills/crwu/crwu-audit-asset-mining-right/references/02-review-focus.md +33 -0
- package/skills/crwu/crwu-audit-asset-mining-right/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-other/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-other/references/00-applicability.md +28 -0
- package/skills/crwu/crwu-audit-asset-other/references/01-kb-assembly.md +31 -0
- package/skills/crwu/crwu-audit-asset-other/references/02-review-focus.md +21 -0
- package/skills/crwu/crwu-audit-asset-other/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-portfolio/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-portfolio/references/00-applicability.md +28 -0
- package/skills/crwu/crwu-audit-asset-portfolio/references/01-kb-assembly.md +31 -0
- package/skills/crwu/crwu-audit-asset-portfolio/references/02-review-focus.md +21 -0
- package/skills/crwu/crwu-audit-asset-portfolio/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-realestate/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-realestate/references/00-applicability.md +43 -0
- package/skills/crwu/crwu-audit-asset-realestate/references/01-kb-assembly.md +75 -0
- package/skills/crwu/crwu-audit-asset-realestate/references/02-review-focus.md +93 -0
- package/skills/crwu/crwu-audit-asset-realestate/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-scrap-materials/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-scrap-materials/references/00-applicability.md +28 -0
- package/skills/crwu/crwu-audit-asset-scrap-materials/references/01-kb-assembly.md +31 -0
- package/skills/crwu/crwu-audit-asset-scrap-materials/references/02-review-focus.md +21 -0
- package/skills/crwu/crwu-audit-asset-scrap-materials/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-asset-transport-equipment/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-asset-transport-equipment/references/00-applicability.md +31 -0
- package/skills/crwu/crwu-audit-asset-transport-equipment/references/01-kb-assembly.md +35 -0
- package/skills/crwu/crwu-audit-asset-transport-equipment/references/02-review-focus.md +33 -0
- package/skills/crwu/crwu-audit-asset-transport-equipment/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-asset-operation/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-asset-operation/references/00-applicability.md +30 -0
- package/skills/crwu/crwu-audit-biz-asset-operation/references/01-kb-assembly.md +32 -0
- package/skills/crwu/crwu-audit-biz-asset-operation/references/02-review-focus.md +42 -0
- package/skills/crwu/crwu-audit-biz-asset-operation/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-consulting-review/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-consulting-review/references/00-applicability.md +30 -0
- package/skills/crwu/crwu-audit-biz-consulting-review/references/01-kb-assembly.md +32 -0
- package/skills/crwu/crwu-audit-biz-consulting-review/references/02-review-focus.md +40 -0
- package/skills/crwu/crwu-audit-biz-consulting-review/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-financial-reporting/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-financial-reporting/references/00-applicability.md +41 -0
- package/skills/crwu/crwu-audit-biz-financial-reporting/references/01-kb-assembly.md +40 -0
- package/skills/crwu/crwu-audit-biz-financial-reporting/references/02-review-focus.md +64 -0
- package/skills/crwu/crwu-audit-biz-financial-reporting/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-financing-debt/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-financing-debt/references/00-applicability.md +31 -0
- package/skills/crwu/crwu-audit-biz-financing-debt/references/01-kb-assembly.md +34 -0
- package/skills/crwu/crwu-audit-biz-financing-debt/references/02-review-focus.md +47 -0
- package/skills/crwu/crwu-audit-biz-financing-debt/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-investment-capital/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-investment-capital/references/00-applicability.md +31 -0
- package/skills/crwu/crwu-audit-biz-investment-capital/references/01-kb-assembly.md +34 -0
- package/skills/crwu/crwu-audit-biz-investment-capital/references/02-review-focus.md +47 -0
- package/skills/crwu/crwu-audit-biz-investment-capital/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/00-applicability.md +30 -0
- package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/01-kb-assembly.md +32 -0
- package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/02-review-focus.md +40 -0
- package/skills/crwu/crwu-audit-biz-judicial-liquidation-compensation/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-tax-history/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-tax-history/references/00-applicability.md +29 -0
- package/skills/crwu/crwu-audit-biz-tax-history/references/01-kb-assembly.md +30 -0
- package/skills/crwu/crwu-audit-biz-tax-history/references/02-review-focus.md +33 -0
- package/skills/crwu/crwu-audit-biz-tax-history/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-biz-transaction-disposal/SKILL.md +38 -0
- package/skills/crwu/crwu-audit-biz-transaction-disposal/references/00-applicability.md +31 -0
- package/skills/crwu/crwu-audit-biz-transaction-disposal/references/01-kb-assembly.md +34 -0
- package/skills/crwu/crwu-audit-biz-transaction-disposal/references/02-review-focus.md +47 -0
- package/skills/crwu/crwu-audit-biz-transaction-disposal/references/03-common-contract.md +202 -0
- package/skills/crwu/crwu-audit-datacheck/SKILL.md +59 -0
- package/skills/crwu/crwu-audit-datacheck/references/00-KB/350/243/205/351/205/215/350/241/250.md +25 -0
- package/skills/crwu/crwu-audit-datacheck/references/01-runtime-and-check-catalog.md +139 -0
- package/skills/crwu/crwu-audit-datacheck/scripts/recalc_check.py +521 -0
- package/skills/crwu/crwu-audit-datacheck/scripts/test_recalc_check.py +419 -0
- package/skills/crwu/crwu-audit-datacheck/scripts/test_skill_self_containment.py +44 -0
- package/skills/crwu/crwu-audit-external-data/SKILL.md +73 -0
- package/skills/crwu/crwu-audit-external-data/references/00-KB/350/243/205/351/205/215/350/241/250.md +38 -0
- package/skills/crwu/crwu-audit-external-data/references/01-connector-access.md +116 -0
- package/skills/crwu/crwu-audit-external-data/references/02-verification-workflow.md +78 -0
- package/skills/crwu/crwu-audit-external-data/scripts/connector_probe.py +374 -0
- package/skills/crwu/crwu-audit-external-data/scripts/test_connector_probe.py +264 -0
- package/skills/crwu/crwu-audit-output-filter/SKILL.md +34 -0
- package/skills/crwu/crwu-audit-output-filter/references/00-applicability.md +7 -0
- package/skills/crwu/crwu-audit-output-filter/references/01-kb-assembly.md +11 -0
- package/skills/crwu/crwu-audit-output-filter/references/02-review-focus.md +25 -0
- package/skills/crwu/crwu-audit-public-general-standards/SKILL.md +46 -0
- package/skills/crwu/crwu-audit-public-general-standards/references/00-applicability.md +41 -0
- package/skills/crwu/crwu-audit-public-general-standards/references/01-kb-assembly.md +66 -0
- package/skills/crwu/crwu-audit-public-general-standards/references/02-review-focus.md +111 -0
- package/skills/crwu/crwu-audit-public-general-standards/references/03-execution-contract.md +89 -0
- package/skills/crwu/crwu-dev-audit-optimize/SKILL.md +98 -0
- 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
- 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
- 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
- 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
- package/skills/crwu/crwu-dev-audit-optimize/scripts/examples/gap-analysis.sample.json +116 -0
- package/skills/crwu/crwu-dev-audit-optimize/scripts/gap_analysis.schema.json +33 -0
- package/skills/crwu/crwu-dev-audit-optimize/scripts/gap_analysis_delivery.py +475 -0
- package/skills/crwu/crwu-dev-audit-optimize/scripts/test_gap_analysis_delivery.py +855 -0
- package/skills/crwu/crwu-dev-audit-optimize/template/NOTICE.echarts.txt +7 -0
- package/skills/crwu/crwu-dev-audit-optimize/template/echarts.min.js +45 -0
- package/skills/crwu/crwu-dev-audit-optimize/template/gap-analysis-report.html +71 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/SKILL.md +74 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/00-responsibility-and-modes.md +62 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/01-kb-source-discovery.md +89 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/02-child-skill-contract.md +99 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/03-review-item-execution-contract.md +51 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/04-registry-and-mapping-update.md +73 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/05-validation-and-delivery.md +127 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/06-live-routing-reconciliation.md +136 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/references/07-kb-skill-map.md +411 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/README.md +200 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/check_audit_skill_mappings.py +2477 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/kb_tool.py +577 -0
- package/skills/crwu/crwu-dev-audit-skill-maintainer/scripts/test_audit_skill_maintainer.py +5097 -0
- package/skills/dws/LICENSE +178 -0
- package/skills/dws/NOTICE +17 -0
- package/skills/dws/dingtalk-aisearch/SKILL.md +83 -0
- package/skills/dws/dingtalk-aisearch/references/aisearch.md +249 -0
- package/skills/dws/dingtalk-aisearch/references/intent-guide.md +15 -0
- package/skills/dws/dingtalk-aisearch/references/lite-recipes.md +31 -0
- package/skills/dws/dingtalk-aitable/SKILL.md +139 -0
- package/skills/dws/dingtalk-aitable/references/06-data-analytics.md +11 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-advperm.md +251 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-attachment.md +49 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-best-practices.md +48 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-cell-value.md +346 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-dashboard-chart.md +46 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-data-analysis-sop.md +131 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-datasource.md +272 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-error-recovery.md +128 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-export-import.md +113 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-field-properties.md +238 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-field.md +186 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-filter-sort.md +135 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-form.md +130 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-formula-guide.md +224 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-primary-doc.md +58 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-create.md +52 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-delete.md +20 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-history.md +97 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-name-key.md +47 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-query.md +120 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-share.md +54 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-stats.md +59 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-update.md +66 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-record-upsert.md +89 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-config.md +140 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-extras.md +195 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-view-types.md +95 -0
- package/skills/dws/dingtalk-aitable/references/aitable/aitable-workflow.md +384 -0
- package/skills/dws/dingtalk-aitable/references/aitable-record-ops.md +80 -0
- package/skills/dws/dingtalk-aitable/references/aitable-section.md +30 -0
- package/skills/dws/dingtalk-aitable/references/aitable.md +59 -0
- package/skills/dws/dingtalk-aitable/references/field-rules.md +17 -0
- package/skills/dws/dingtalk-aitable/references/intent-guide.md +17 -0
- package/skills/dws/dingtalk-aitable/references/recipes/conventions.md +44 -0
- package/skills/dws/dingtalk-aitable/references/url-patterns.md +144 -0
- package/skills/dws/dingtalk-aitable/scripts/aitable_export_via_task.py +210 -0
- package/skills/dws/dingtalk-aitable/scripts/aitable_import_via_task.py +173 -0
- package/skills/dws/dingtalk-aitable/scripts/bulk_add_fields.py +273 -0
- package/skills/dws/dingtalk-aitable/scripts/import_records.py +333 -0
- package/skills/dws/dingtalk-aitable/scripts/upload_attachment.py +190 -0
- package/skills/dws/dingtalk-calendar/SKILL.md +137 -0
- package/skills/dws/dingtalk-calendar/references/03-meeting.md +56 -0
- package/skills/dws/dingtalk-calendar/references/calendar.md +760 -0
- package/skills/dws/dingtalk-calendar/references/intent-guide.md +9 -0
- package/skills/dws/dingtalk-calendar/references/lite-recipes.md +25 -0
- package/skills/dws/dingtalk-calendar/scripts/calendar_free_slot_finder.py +199 -0
- package/skills/dws/dingtalk-calendar/scripts/calendar_schedule_meeting.py +233 -0
- package/skills/dws/dingtalk-calendar/scripts/calendar_today_agenda.py +139 -0
- package/skills/dws/dingtalk-chat/SKILL.md +127 -0
- package/skills/dws/dingtalk-chat/references/01-messaging.md +103 -0
- package/skills/dws/dingtalk-chat/references/card/callback.md +10 -0
- package/skills/dws/dingtalk-chat/references/card/create.md +27 -0
- package/skills/dws/dingtalk-chat/references/card/schema.md +14 -0
- package/skills/dws/dingtalk-chat/references/card/update.md +13 -0
- package/skills/dws/dingtalk-chat/references/chat/chat-bot.md +187 -0
- package/skills/dws/dingtalk-chat/references/chat/chat-conversation.md +165 -0
- package/skills/dws/dingtalk-chat/references/chat/group-admin.md +144 -0
- package/skills/dws/dingtalk-chat/references/chat/group-discovery.md +121 -0
- package/skills/dws/dingtalk-chat/references/chat/message-actions.md +132 -0
- package/skills/dws/dingtalk-chat/references/chat/message-media.md +76 -0
- package/skills/dws/dingtalk-chat/references/chat/message-query.md +135 -0
- package/skills/dws/dingtalk-chat/references/chat/thread.md +46 -0
- package/skills/dws/dingtalk-chat/references/chat-emoji-list.md +216 -0
- package/skills/dws/dingtalk-chat/references/chat.md +140 -0
- package/skills/dws/dingtalk-chat/references/contracts.md +70 -0
- package/skills/dws/dingtalk-chat/references/intent-guide.md +56 -0
- package/skills/dws/dingtalk-contact/SKILL.md +122 -0
- package/skills/dws/dingtalk-contact/references/08-directory.md +51 -0
- package/skills/dws/dingtalk-contact/references/contact.md +595 -0
- package/skills/dws/dingtalk-contact/references/intent-guide.md +14 -0
- package/skills/dws/dingtalk-contact/references/lite-recipes.md +30 -0
- package/skills/dws/dingtalk-contact/references/recipes/conventions.md +44 -0
- package/skills/dws/dingtalk-contact/scripts/contact_dept_members.py +130 -0
- package/skills/dws/dingtalk-doc/SKILL.md +112 -0
- package/skills/dws/dingtalk-doc/references/04-document.md +57 -0
- package/skills/dws/dingtalk-doc/references/contracts.md +40 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-block.md +39 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-comment.md +29 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-create.md +54 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-export.md +30 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-import.md +28 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-info.md +12 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-media.md +46 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-read.md +56 -0
- package/skills/dws/dingtalk-doc/references/doc/doc-update.md +87 -0
- package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-cookbook.md +469 -0
- package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-schema.json +526 -0
- package/skills/dws/dingtalk-doc/references/doc/format/doc-jsonml-schema.md +711 -0
- package/skills/dws/dingtalk-doc/references/doc/style/doc-create-workflow.md +414 -0
- package/skills/dws/dingtalk-doc/references/doc/style/doc-style-guideline.md +404 -0
- package/skills/dws/dingtalk-doc/references/doc/style/doc-update-workflow.md +316 -0
- package/skills/dws/dingtalk-doc/references/doc.md +82 -0
- package/skills/dws/dingtalk-doc/references/intent-guide.md +20 -0
- package/skills/dws/dingtalk-drive/SKILL.md +128 -0
- package/skills/dws/dingtalk-drive/references/drive.md +126 -0
- package/skills/dws/dingtalk-drive/references/folder-sync.md +40 -0
- package/skills/dws/dingtalk-drive/references/intent-guide.md +22 -0
- package/skills/dws/dingtalk-drive/references/lite-recipes.md +39 -0
- package/skills/dws/dingtalk-event/SKILL.md +106 -0
- package/skills/dws/dingtalk-event/references/event-im-keys.md +54 -0
- package/skills/dws/dingtalk-event/references/event-im-lifecycle.md +35 -0
- package/skills/dws/dingtalk-event/references/event-im-operations.md +52 -0
- package/skills/dws/dingtalk-event/references/event-im-output.md +39 -0
- package/skills/dws/dingtalk-event/references/event-im.md +58 -0
- package/skills/dws/dingtalk-event/references/event-oa.md +155 -0
- package/skills/dws/dingtalk-event/references/event-todo.md +50 -0
- package/skills/dws/dingtalk-event/references/event-voip.md +69 -0
- package/skills/dws/dingtalk-mail/SKILL.md +113 -0
- package/skills/dws/dingtalk-mail/references/09-mail.md +45 -0
- package/skills/dws/dingtalk-mail/references/intent-guide.md +8 -0
- package/skills/dws/dingtalk-mail/references/lite-recipes.md +45 -0
- package/skills/dws/dingtalk-mail/references/mail.md +1889 -0
- package/skills/dws/dingtalk-mail/references/recipes/conventions.md +44 -0
- package/skills/dws/dingtalk-mail/scripts/mail_send_with_cc.py +122 -0
- package/skills/dws/dingtalk-mail/scripts/mail_unread_summary.py +114 -0
- package/skills/dws/dingtalk-minutes/SKILL.md +116 -0
- package/skills/dws/dingtalk-minutes/references/07-minutes.md +149 -0
- package/skills/dws/dingtalk-minutes/references/10-minutes-speaker-match.md +32 -0
- package/skills/dws/dingtalk-minutes/references/11-minutes-speaker-correct.md +40 -0
- package/skills/dws/dingtalk-minutes/references/intent-guide.md +79 -0
- package/skills/dws/dingtalk-minutes/references/lite-recipes.md +25 -0
- package/skills/dws/dingtalk-minutes/references/minutes.md +210 -0
- package/skills/dws/dingtalk-minutes/references/recipes/conventions.md +33 -0
- package/skills/dws/dingtalk-minutes/scripts/minutes_recent_summary.py +196 -0
- package/skills/dws/dingtalk-minutes/scripts/test_minutes_scripts.py +222 -0
- package/skills/dws/dingtalk-misc/SKILL.md +55 -0
- package/skills/dws/dingtalk-misc/references/05-reporting.md +29 -0
- package/skills/dws/dingtalk-misc/references/agoal.md +302 -0
- package/skills/dws/dingtalk-misc/references/attendance-intent-guide.md +7 -0
- package/skills/dws/dingtalk-misc/references/attendance-report.md +109 -0
- package/skills/dws/dingtalk-misc/references/attendance-schedule.md +115 -0
- package/skills/dws/dingtalk-misc/references/attendance-vacation.md +129 -0
- package/skills/dws/dingtalk-misc/references/attendance.md +149 -0
- package/skills/dws/dingtalk-misc/references/dev/app.md +42 -0
- package/skills/dws/dingtalk-misc/references/dev/connect.md +108 -0
- package/skills/dws/dingtalk-misc/references/dev/credentials.md +26 -0
- package/skills/dws/dingtalk-misc/references/dev/dev-index.md +13 -0
- package/skills/dws/dingtalk-misc/references/dev/event.md +47 -0
- package/skills/dws/dingtalk-misc/references/dev/mcp.md +106 -0
- package/skills/dws/dingtalk-misc/references/dev/member.md +19 -0
- package/skills/dws/dingtalk-misc/references/dev/permission.md +56 -0
- package/skills/dws/dingtalk-misc/references/dev/recipes.md +58 -0
- package/skills/dws/dingtalk-misc/references/dev/robot.md +74 -0
- package/skills/dws/dingtalk-misc/references/dev/security.md +21 -0
- package/skills/dws/dingtalk-misc/references/dev/version.md +99 -0
- package/skills/dws/dingtalk-misc/references/dev/webapp.md +22 -0
- package/skills/dws/dingtalk-misc/references/devapp.md +148 -0
- package/skills/dws/dingtalk-misc/references/devdoc-intent-guide.md +7 -0
- package/skills/dws/dingtalk-misc/references/devdoc.md +42 -0
- package/skills/dws/dingtalk-misc/references/ding.md +184 -0
- package/skills/dws/dingtalk-misc/references/hrbrain.md +67 -0
- package/skills/dws/dingtalk-misc/references/live.md +23 -0
- package/skills/dws/dingtalk-misc/references/markdown.md +167 -0
- package/skills/dws/dingtalk-misc/references/oa/oa-form-components.md +345 -0
- package/skills/dws/dingtalk-misc/references/oa/oa-process-nodes.md +374 -0
- package/skills/dws/dingtalk-misc/references/oa.md +1014 -0
- package/skills/dws/dingtalk-misc/references/openapi-explorer.md +47 -0
- package/skills/dws/dingtalk-misc/references/pat.md +52 -0
- package/skills/dws/dingtalk-misc/references/profile.md +41 -0
- package/skills/dws/dingtalk-misc/references/recruit.md +89 -0
- package/skills/dws/dingtalk-misc/references/report-conventions.md +44 -0
- package/skills/dws/dingtalk-misc/references/report-intent-guide.md +11 -0
- package/skills/dws/dingtalk-misc/references/report-lite-recipes.md +25 -0
- package/skills/dws/dingtalk-misc/references/report.md +109 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-batch-operations.md +104 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-chart.md +69 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-comment.md +166 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-conditional-format.md +59 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-dimension-operations.md +60 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-dropdown.md +103 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-export.md +135 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-filter-view.md +51 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-filter.md +181 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-formula.md +192 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-import.md +76 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-media-image.md +263 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-pivot-table.md +292 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-range-operations.md +161 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-read-data.md +60 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-revision-changeset.md +46 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-search-replace.md +120 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-style-format.md +71 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-version.md +84 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-workbook.md +59 -0
- package/skills/dws/dingtalk-misc/references/sheet/sheet-write-data.md +93 -0
- package/skills/dws/dingtalk-misc/references/sheet-conventions.md +44 -0
- package/skills/dws/dingtalk-misc/references/sheet-intent-guide.md +12 -0
- package/skills/dws/dingtalk-misc/references/sheet.md +166 -0
- package/skills/dws/dingtalk-misc/references/skill.md +98 -0
- package/skills/dws/dingtalk-misc/references/unsupported-scripts.md +49 -0
- package/skills/dws/dingtalk-misc/references/url-patterns.md +144 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/compose.md +200 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/01-overview.md +57 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/02-query.md +288 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/03-update.md +249 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/04-text-style.md +395 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/05-shape-frame-group-connector.md +186 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/06-vector-icon-path.md +313 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/07-examples-errors-write-support.md +270 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1/08-catalogs.md +61 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/open-nodes-v1.md +54 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/replace.md +41 -0
- package/skills/dws/dingtalk-misc/references/whiteboard/vector.md +51 -0
- package/skills/dws/dingtalk-misc/references/whiteboard.md +115 -0
- package/skills/dws/dingtalk-misc/scripts/aiapp_create_and_poll.py +138 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_my_record.py +91 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_report_checkin.py +440 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_report_common.py +1698 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_report_daily.py +558 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_report_detail.py +809 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_report_monthly.py +758 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_report_record.py +947 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_schedule_export.py +344 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_schedule_import.py +498 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_team_shift.py +89 -0
- package/skills/dws/dingtalk-misc/scripts/attendance_vacation_balance.py +617 -0
- package/skills/dws/dingtalk-misc/scripts/finance_daily_cashflow.py +70 -0
- package/skills/dws/dingtalk-misc/scripts/finance_expense_flow.py +138 -0
- package/skills/dws/dingtalk-misc/scripts/oa_batch_approve.py +158 -0
- package/skills/dws/dingtalk-misc/scripts/oa_pending_review.py +132 -0
- package/skills/dws/dingtalk-misc/scripts/report_received_today.py +315 -0
- package/skills/dws/dingtalk-misc/scripts/yida_custom_page_update.py +401 -0
- package/skills/dws/dingtalk-misc/scripts/yida_form_builder.py +526 -0
- package/skills/dws/dingtalk-misc/scripts/yida_form_fields.py +315 -0
- package/skills/dws/dingtalk-misc/scripts/yida_form_inspector.py +311 -0
- package/skills/dws/dingtalk-misc/scripts/yida_form_update.py +185 -0
- package/skills/dws/dingtalk-misc/scripts/yida_jsx_pipeline.py +1588 -0
- package/skills/dws/dingtalk-misc/scripts/yida_page_compiler.py +1441 -0
- package/skills/dws/dingtalk-misc/scripts/yida_page_generate.py +514 -0
- package/skills/dws/dingtalk-misc/scripts/yida_page_schema.py +519 -0
- package/skills/dws/dingtalk-misc/scripts/yida_page_self_check.py +398 -0
- package/skills/dws/dingtalk-misc/scripts/yida_process_flow.py +1478 -0
- package/skills/dws/dingtalk-misc/scripts/yida_process_update.py +589 -0
- package/skills/dws/dingtalk-misc/scripts/yida_report_builder.py +494 -0
- package/skills/dws/dingtalk-misc/scripts/yida_report_charts.py +1434 -0
- package/skills/dws/dingtalk-misc/scripts/yida_report_update.py +244 -0
- package/skills/dws/dingtalk-misc/scripts/yida_schema_common.py +340 -0
- package/skills/dws/dingtalk-shared/SKILL.md +83 -0
- package/skills/dws/dingtalk-shared/references/capability-limits.md +31 -0
- package/skills/dws/dingtalk-shared/references/channel-login.md +26 -0
- package/skills/dws/dingtalk-shared/references/confirmation.md +59 -0
- package/skills/dws/dingtalk-shared/references/error-codes.md +155 -0
- package/skills/dws/dingtalk-shared/references/global-reference.md +122 -0
- package/skills/dws/dingtalk-shared/references/intent-guide.md +537 -0
- package/skills/dws/dingtalk-shared/references/recipes/conventions.md +45 -0
- package/skills/dws/dingtalk-shared/references/recipes/lite-catalog.md +275 -0
- package/skills/dws/dingtalk-shared/references/recipes/meta.md +67 -0
- package/skills/dws/dingtalk-shared/references/routing.md +52 -0
- package/skills/dws/dingtalk-shared/references/runtime-contract.md +11 -0
- package/skills/dws/dingtalk-shared/references/schema-usage.md +88 -0
- package/skills/dws/dingtalk-shared/references/url-patterns.md +144 -0
- package/skills/dws/dingtalk-shared/references/workflow-routing.md +45 -0
- package/skills/dws/dingtalk-todo/SKILL.md +119 -0
- package/skills/dws/dingtalk-todo/references/02-task.md +70 -0
- package/skills/dws/dingtalk-todo/references/intent-guide.md +15 -0
- package/skills/dws/dingtalk-todo/references/lite-recipes.md +68 -0
- package/skills/dws/dingtalk-todo/references/recipes/conventions.md +44 -0
- package/skills/dws/dingtalk-todo/references/todo.md +517 -0
- package/skills/dws/dingtalk-todo/scripts/todo_batch_create.py +354 -0
- package/skills/dws/dingtalk-todo/scripts/todo_daily_summary.py +153 -0
- package/skills/dws/dingtalk-todo/scripts/todo_overdue_check.py +101 -0
- package/skills/dws/dingtalk-wiki/SKILL.md +117 -0
- package/skills/dws/dingtalk-wiki/references/intent-guide.md +14 -0
- package/skills/dws/dingtalk-wiki/references/lite-recipes.md +44 -0
- package/skills/dws/dingtalk-wiki/references/wiki-members.md +32 -0
- package/skills/dws/dingtalk-wiki/references/wiki-node-ops.md +46 -0
- package/skills/dws/dingtalk-wiki/references/wiki.md +65 -0
- package/skills/dws/provenance.json +27 -0
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""`prepare_materials.py` 契约测试(H0 数据隔离 + 重建法三条纪律)。
|
|
4
|
+
|
|
5
|
+
自洽:只用本技能 `scripts/` 内的脚本造临时 fixture,可随技能一起安装运行。
|
|
6
|
+
每条用例都对应一次**真实失效**,防止回退:
|
|
7
|
+
|
|
8
|
+
1. 缓存值优先 —— 源格"公式 + 缓存值"时,重建后必须落**缓存值**(否则后续 data_only
|
|
9
|
+
读回 None,把"未重算"读成"数据缺失",曾误报"评估结果列全空")。
|
|
10
|
+
2. H0 —— 人工隐藏的 sheet / 行 / 列 / 折叠分组整体排除:其内容值**不得出现在任何产物**里。
|
|
11
|
+
3. 同名消歧 —— 定稿与送审稿同名时,工作版必须两份都在、来源可区分、不得静默覆盖。
|
|
12
|
+
4. 「值不可得」登记 —— 无缓存值的公式格单独计数,不混进"数据缺失"。
|
|
13
|
+
5. 源材料目录只读 —— 运行后源目录文件集合不变。
|
|
14
|
+
6. 表格遍历四层边界 —— 扫描边界按**有值格**(不按 dimension、也不按"存在的格");
|
|
15
|
+
声明用区远大于有值区时登记表格规范提示;单表超规模上限时记 capability gap 并跳过。
|
|
16
|
+
7. 媒体证据通道 —— 内嵌图/独立图片必须**导出到 `媒体证据/` 并写入 `媒体索引.json`**
|
|
17
|
+
(此前只登记计数、无读取通道 → 证据在图片里被判"为空/缺失");阶段二复核件复跑
|
|
18
|
+
必须换产物名,**不得覆盖阶段一冻结的 `材料盘点.json`**(复核意见附件里的图同样要被抽取)。
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import base64
|
|
23
|
+
import hashlib
|
|
24
|
+
import importlib.util
|
|
25
|
+
import json
|
|
26
|
+
import os
|
|
27
|
+
import shutil
|
|
28
|
+
import subprocess
|
|
29
|
+
import sys
|
|
30
|
+
import tempfile
|
|
31
|
+
import unittest
|
|
32
|
+
import zipfile
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
36
|
+
|
|
37
|
+
_PNG = base64.b64decode(
|
|
38
|
+
b"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8DwHwAFAAH/q842iQAAAABJRU5ErkJggg==")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _minimal_docx_with_image(path: Path) -> Path:
|
|
42
|
+
"""造一个正文第 2 段内联一张图的 `.docx`(复核意见附件常见形态)。"""
|
|
43
|
+
ct = ('<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/'
|
|
44
|
+
'content-types"><Default Extension="rels" ContentType="application/vnd.'
|
|
45
|
+
'openxmlformats-package.relationships+xml"/><Default Extension="xml" '
|
|
46
|
+
'ContentType="application/xml"/><Default Extension="png" ContentType="image/png"/>'
|
|
47
|
+
'<Override PartName="/word/document.xml" ContentType="application/vnd.'
|
|
48
|
+
'openxmlformats-officedocument.wordprocessingml.document.main+xml"/></Types>')
|
|
49
|
+
rels = ('<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/'
|
|
50
|
+
'2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/'
|
|
51
|
+
'officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/>'
|
|
52
|
+
'</Relationships>')
|
|
53
|
+
doc = ('<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/'
|
|
54
|
+
'wordprocessingml/2006/main" xmlns:r="http://schemas.openxmlformats.org/officeDocument/'
|
|
55
|
+
'2006/relationships" xmlns:wp="http://schemas.openxmlformats.org/drawingml/2006/'
|
|
56
|
+
'wordprocessingDrawing" xmlns:a="http://schemas.openxmlformats.org/drawingml/2006/main">'
|
|
57
|
+
'<w:body><w:p><w:r><w:t>问题 1:可比案例位置图缺失。</w:t></w:r></w:p>'
|
|
58
|
+
'<w:p><w:r><w:t>证据截图:</w:t></w:r><w:r><w:drawing><wp:inline><wp:extent cx="1" cy="1"/>'
|
|
59
|
+
'<wp:docPr id="1" name="P1"/><a:graphic><a:graphicData uri="http://schemas.'
|
|
60
|
+
'openxmlformats.org/drawingml/2006/picture"><a:blip r:embed="rId10"/></a:graphicData>'
|
|
61
|
+
'</a:graphic></wp:inline></w:drawing></w:r></w:p></w:body></w:document>')
|
|
62
|
+
doc_rels = ('<?xml version="1.0"?><Relationships xmlns="http://schemas.openxmlformats.org/package/'
|
|
63
|
+
'2006/relationships"><Relationship Id="rId10" Type="http://schemas.openxmlformats.org/'
|
|
64
|
+
'officeDocument/2006/relationships/image" Target="media/image1.png"/></Relationships>')
|
|
65
|
+
with zipfile.ZipFile(path, "w", zipfile.ZIP_DEFLATED) as z:
|
|
66
|
+
z.writestr("[Content_Types].xml", ct)
|
|
67
|
+
z.writestr("_rels/.rels", rels)
|
|
68
|
+
z.writestr("word/document.xml", doc)
|
|
69
|
+
z.writestr("word/_rels/document.xml.rels", doc_rels)
|
|
70
|
+
z.writestr("word/media/image1.png", _PNG)
|
|
71
|
+
return path
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _sha256(path: Path) -> str:
|
|
75
|
+
return hashlib.sha256(path.read_bytes()).hexdigest()
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _load_module():
|
|
79
|
+
spec = importlib.util.spec_from_file_location(
|
|
80
|
+
"prepare_materials_for_test", SCRIPTS_DIR / "prepare_materials.py"
|
|
81
|
+
)
|
|
82
|
+
module = importlib.util.module_from_spec(spec)
|
|
83
|
+
spec.loader.exec_module(module)
|
|
84
|
+
return module
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _with_cached_value(path: Path, cell_ref: str, formula: str, cached: str) -> None:
|
|
88
|
+
"""把 openpyxl 写出的公式格补上 Excel 才会写的 `<v>` 缓存值。
|
|
89
|
+
|
|
90
|
+
直接造"公式 + 缓存值"的源格,才能复现线上那种"源有意义的值、重建后却读回 None"的场景。
|
|
91
|
+
"""
|
|
92
|
+
tmp = path.with_suffix(".tmp.xlsx")
|
|
93
|
+
with zipfile.ZipFile(path) as zin, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
94
|
+
for item in zin.infolist():
|
|
95
|
+
data = zin.read(item.filename)
|
|
96
|
+
if item.filename.startswith("xl/worksheets/sheet"):
|
|
97
|
+
text = data.decode("utf-8")
|
|
98
|
+
needle = f'<f>{formula}</f>'
|
|
99
|
+
assert needle in text, f"{item.filename}: 未找到 {needle}"
|
|
100
|
+
text = text.replace(needle, f"{needle}<v>{cached}</v>", 1)
|
|
101
|
+
data = text.encode("utf-8")
|
|
102
|
+
zout.writestr(item, data)
|
|
103
|
+
shutil.move(str(tmp), str(path))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
class PrepareMaterialsContractTest(unittest.TestCase):
|
|
107
|
+
def setUp(self):
|
|
108
|
+
import openpyxl # noqa: F401 (缺失则整体 skip)
|
|
109
|
+
self.module = _load_module()
|
|
110
|
+
self.tmp = tempfile.TemporaryDirectory()
|
|
111
|
+
self.case = Path(self.tmp.name)
|
|
112
|
+
self.src = self.case / "材料-源"
|
|
113
|
+
self.src.mkdir()
|
|
114
|
+
|
|
115
|
+
def tearDown(self):
|
|
116
|
+
self.tmp.cleanup()
|
|
117
|
+
|
|
118
|
+
def _run(self):
|
|
119
|
+
return self.module.prepare(
|
|
120
|
+
str(self.case), str(self.src),
|
|
121
|
+
str(self.case / "提取"), str(self.case / "工作版"),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _payload(self):
|
|
125
|
+
return json.loads((self.case / "材料盘点.json").read_text(encoding="utf-8"))
|
|
126
|
+
|
|
127
|
+
def _inventory(self):
|
|
128
|
+
return self._payload()["items"]
|
|
129
|
+
|
|
130
|
+
def _run_label(self, label: str, src: Path, inventory: str = None,
|
|
131
|
+
media_index: str = None):
|
|
132
|
+
"""按阶段标签复跑(阶段二复核件用):产物名加后缀,不影响阶段一冻结产物。"""
|
|
133
|
+
return self.module.prepare(
|
|
134
|
+
str(self.case), str(src),
|
|
135
|
+
str(self.case / f"提取-{label}"), str(self.case / f"工作版-{label}"),
|
|
136
|
+
extract_dir=str(self.case / f"解压-{label}"),
|
|
137
|
+
media_dir=str(self.case / f"媒体证据-{label}"),
|
|
138
|
+
inventory_name=inventory or f"{label}盘点.json",
|
|
139
|
+
media_index_name=media_index or f"{label}媒体索引.json")
|
|
140
|
+
|
|
141
|
+
# ---- 1 缓存值优先 ----------------------------------------------------
|
|
142
|
+
def test_cached_value_wins_over_formula_string(self):
|
|
143
|
+
"""源格 = 公式 + 缓存值 → 重建后必须能 data_only 读回该值。"""
|
|
144
|
+
import openpyxl
|
|
145
|
+
d = self.src / "定稿"
|
|
146
|
+
d.mkdir()
|
|
147
|
+
f = d / "明细表.xlsx"
|
|
148
|
+
wb = openpyxl.Workbook()
|
|
149
|
+
wb.active["A1"] = "评估结果"
|
|
150
|
+
wb.active["A2"] = "=1+1"
|
|
151
|
+
wb.save(f)
|
|
152
|
+
_with_cached_value(f, "A2", "1+1", "42")
|
|
153
|
+
|
|
154
|
+
self._run()
|
|
155
|
+
|
|
156
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/明细表.xlsx", data_only=True)
|
|
157
|
+
self.assertEqual(42, rebuilt.active["A2"].value)
|
|
158
|
+
self.assertEqual(
|
|
159
|
+
0,
|
|
160
|
+
self._inventory()[0]["workbook"]["sheets"][0]["valueUnavailable"],
|
|
161
|
+
"有缓存值的格不应被记成「值不可得」",
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def test_formula_without_cached_value_is_registered_as_value_unavailable(self):
|
|
165
|
+
"""无缓存值的公式格:保留公式串(不丢结构),并单独登记「值不可得」。"""
|
|
166
|
+
import openpyxl
|
|
167
|
+
d = self.src / "定稿"
|
|
168
|
+
d.mkdir()
|
|
169
|
+
wb = openpyxl.Workbook()
|
|
170
|
+
wb.active["A1"] = "=SUM(B1:B2)"
|
|
171
|
+
wb.save(d / "外部引用.xlsx")
|
|
172
|
+
|
|
173
|
+
self._run()
|
|
174
|
+
|
|
175
|
+
rec = self._inventory()[0]
|
|
176
|
+
self.assertEqual(1, rec["workbook"]["sheets"][0]["valueUnavailable"])
|
|
177
|
+
self.assertTrue(rec["workbook"]["valueUnavailable"])
|
|
178
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/外部引用.xlsx", data_only=False)
|
|
179
|
+
self.assertEqual("=SUM(B1:B2)", rebuilt.active["A1"].value)
|
|
180
|
+
|
|
181
|
+
# ---- 2 H0 隐藏区 -----------------------------------------------------
|
|
182
|
+
def test_hidden_content_never_reaches_any_artifact(self):
|
|
183
|
+
"""隐藏 sheet / 行 / 列的内容值不得出现在工作版或盘点中(H0)。"""
|
|
184
|
+
import openpyxl
|
|
185
|
+
d = self.src / "定稿"
|
|
186
|
+
d.mkdir()
|
|
187
|
+
f = d / "含隐藏.xlsx"
|
|
188
|
+
wb = openpyxl.Workbook()
|
|
189
|
+
ws = wb.active
|
|
190
|
+
ws.title = "可见表"
|
|
191
|
+
# A1/C1 可见(对照组),A2 隐藏行、B1 隐藏列、隐藏 sheet 各放一个哨兵值
|
|
192
|
+
ws["A1"], ws["A2"], ws["B1"], ws["C1"] = "保留", 999999, 888888, 777777
|
|
193
|
+
ws.row_dimensions[2].hidden = True # 隐藏行
|
|
194
|
+
ws.column_dimensions["B"].hidden = True # 隐藏列
|
|
195
|
+
hid = wb.create_sheet("隐藏表")
|
|
196
|
+
hid["A1"] = 666666 # 隐藏 sheet 的内容值
|
|
197
|
+
hid.sheet_state = "hidden" # 必须显式设为隐藏(create_sheet 默认可见)
|
|
198
|
+
wb.save(f)
|
|
199
|
+
|
|
200
|
+
self._run()
|
|
201
|
+
|
|
202
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/含隐藏.xlsx", data_only=True)
|
|
203
|
+
values = [c.value for row in rebuilt["可见表"].iter_rows() for c in row if c.value is not None]
|
|
204
|
+
self.assertIn("保留", values)
|
|
205
|
+
self.assertIn(777777, values, "可见列的值必须保留(对照组)")
|
|
206
|
+
for sentinel in (999999, 888888, 666666):
|
|
207
|
+
self.assertNotIn(sentinel, values, f"隐藏值 {sentinel} 泄漏到工作版")
|
|
208
|
+
meta = self._inventory()[0]["workbook"]["hiddenMeta"]
|
|
209
|
+
self.assertEqual(["隐藏表"], meta["hiddenSheets"], "隐藏 sheet 的**名字**必须登记(忽略清单)")
|
|
210
|
+
self.assertEqual([2], meta["hiddenRows"]["可见表"], "隐藏行段位必须登记")
|
|
211
|
+
self.assertEqual(["B"], meta["hiddenCols"]["可见表"], "隐藏列段位必须登记")
|
|
212
|
+
blob = (self.case / "材料盘点.json").read_text(encoding="utf-8")
|
|
213
|
+
for sentinel in ("999999", "888888", "666666"):
|
|
214
|
+
self.assertNotIn(sentinel, blob, f"隐藏区内容值 {sentinel} 不得进入盘点")
|
|
215
|
+
|
|
216
|
+
def test_collapsed_outline_group_counts_as_hidden(self):
|
|
217
|
+
"""折叠分组内的行视同隐藏(Excel 折叠后不可见)。"""
|
|
218
|
+
import openpyxl
|
|
219
|
+
d = self.src / "定稿"
|
|
220
|
+
d.mkdir()
|
|
221
|
+
f = d / "分组.xlsx"
|
|
222
|
+
wb = openpyxl.Workbook()
|
|
223
|
+
ws = wb.active
|
|
224
|
+
ws["A1"], ws["A2"], ws["A3"], ws["A4"] = "头", 12345, 23456, "尾"
|
|
225
|
+
ws.row_dimensions[2].outlineLevel = 1 # 组内行
|
|
226
|
+
ws.row_dimensions[3].outlineLevel = 1 # 组内行
|
|
227
|
+
ws.row_dimensions[4].collapsed = True # 汇总行(默认在下方)= 组已折叠
|
|
228
|
+
wb.save(f)
|
|
229
|
+
|
|
230
|
+
self._run()
|
|
231
|
+
|
|
232
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/分组.xlsx", data_only=True)
|
|
233
|
+
values = [c.value for row in rebuilt.active.iter_rows() for c in row if c.value is not None]
|
|
234
|
+
self.assertEqual(["头", "尾"], values, "折叠组内的行必须排除")
|
|
235
|
+
|
|
236
|
+
# ---- 3 同名消歧 ------------------------------------------------------
|
|
237
|
+
def test_same_named_versions_do_not_overwrite_each_other(self):
|
|
238
|
+
"""定稿与送审稿同名 → 工作版两份都在,来源可区分。"""
|
|
239
|
+
import openpyxl
|
|
240
|
+
for stage, val in (("定稿", "出租"), ("送审稿", "自用")):
|
|
241
|
+
d = self.src / stage
|
|
242
|
+
d.mkdir()
|
|
243
|
+
wb = openpyxl.Workbook()
|
|
244
|
+
wb.active["A1"] = val
|
|
245
|
+
wb.save(d / "13-评估明细表-20231231.xlsx")
|
|
246
|
+
|
|
247
|
+
self._run()
|
|
248
|
+
|
|
249
|
+
work = self.case / "工作版"
|
|
250
|
+
names = sorted(p.name for p in work.glob("*.xlsx"))
|
|
251
|
+
self.assertEqual(2, len(names), names)
|
|
252
|
+
got = {
|
|
253
|
+
openpyxl.load_workbook(work / n, data_only=True).active["A1"].value
|
|
254
|
+
for n in names
|
|
255
|
+
}
|
|
256
|
+
self.assertEqual({"出租", "自用"}, got, "两份同名材料不得互相覆盖")
|
|
257
|
+
records = self._inventory()
|
|
258
|
+
self.assertEqual({"定稿", "送审稿"}, {r["stage"] for r in records})
|
|
259
|
+
self.assertTrue(any(r.get("nameCollision") for r in records), "同名冲突须在盘点中标出")
|
|
260
|
+
|
|
261
|
+
# ---- 4 源目录只读 ----------------------------------------------------
|
|
262
|
+
def test_source_materials_directory_is_not_polluted(self):
|
|
263
|
+
"""运行后源材料目录的文件集合不得变化(不得落派生文件)。"""
|
|
264
|
+
import openpyxl
|
|
265
|
+
d = self.src / "定稿"
|
|
266
|
+
d.mkdir()
|
|
267
|
+
wb = openpyxl.Workbook()
|
|
268
|
+
wb.active["A1"] = "x"
|
|
269
|
+
wb.save(d / "a.xlsx")
|
|
270
|
+
before = sorted(str(p.relative_to(self.src)) for p in self.src.rglob("*"))
|
|
271
|
+
|
|
272
|
+
self._run()
|
|
273
|
+
|
|
274
|
+
after = sorted(str(p.relative_to(self.src)) for p in self.src.rglob("*"))
|
|
275
|
+
self.assertEqual(before, after)
|
|
276
|
+
|
|
277
|
+
# ---- 4b 缺依赖不得静默空返 -------------------------------------------
|
|
278
|
+
def test_docx_without_python_docx_never_silently_returns_empty(self):
|
|
279
|
+
"""缺 python-docx 时必须回退(或明确报因),不得静默产出空文本。"""
|
|
280
|
+
import sys
|
|
281
|
+
import zipfile as zf
|
|
282
|
+
d = self.src / "定稿"
|
|
283
|
+
d.mkdir()
|
|
284
|
+
# 手工造最小 .docx(zip + 两个必需部件)
|
|
285
|
+
f = d / "说明.docx"
|
|
286
|
+
with zf.ZipFile(f, "w") as z:
|
|
287
|
+
z.writestr("[Content_Types].xml",
|
|
288
|
+
'<?xml version="1.0"?><Types xmlns="http://schemas.openxmlformats.org/'
|
|
289
|
+
'package/2006/content-types"><Default Extension="xml" ContentType='
|
|
290
|
+
'"application/xml"/></Types>')
|
|
291
|
+
z.writestr("word/document.xml",
|
|
292
|
+
'<?xml version="1.0"?><w:document xmlns:w="http://schemas.openxmlformats.org/'
|
|
293
|
+
'wordprocessingml/2006/main"><w:body><w:p><w:r><w:t>评估说明正文</w:t>'
|
|
294
|
+
'</w:r></w:p></w:body></w:document>')
|
|
295
|
+
|
|
296
|
+
saved = sys.modules.get("docx")
|
|
297
|
+
sys.modules["docx"] = None # 模拟未安装 python-docx
|
|
298
|
+
try:
|
|
299
|
+
self._run()
|
|
300
|
+
finally:
|
|
301
|
+
if saved is None:
|
|
302
|
+
sys.modules.pop("docx", None)
|
|
303
|
+
else:
|
|
304
|
+
sys.modules["docx"] = saved
|
|
305
|
+
|
|
306
|
+
rec = self._inventory()[0]
|
|
307
|
+
if rec["readable"]:
|
|
308
|
+
text = (self.case / "提取/说明.docx.txt").read_text(encoding="utf-8")
|
|
309
|
+
self.assertTrue(text.strip(), "回退成功时不得产出空文本")
|
|
310
|
+
self.assertIn("回退", rec.get("note", ""))
|
|
311
|
+
else:
|
|
312
|
+
self.assertTrue(rec.get("note"), "读不到必须写明原因,不得静默")
|
|
313
|
+
|
|
314
|
+
# ---- 4c 隐藏区引用审计(只坐标,不读内容)-------------------------------
|
|
315
|
+
def test_hidden_reference_audit_flags_visible_results_depending_on_hidden_inputs(self):
|
|
316
|
+
"""可见公式引用隐藏列/行/隐藏表 → 记**人工建议检查项**(不是 AI 问题);且不得读隐藏内容。
|
|
317
|
+
|
|
318
|
+
对应真实案件:土地表隐藏列 N(账面价值)被 32 处可见公式引用、底稿隐藏评分列
|
|
319
|
+
被 24 处引用 —— 这类隐藏列**是计算输入**,一刀切剔除会让结果不可复核。
|
|
320
|
+
去向(2026-09-16 用户口径):审核只要可见区公式计算正确即可 → 只出建议项、不出 `issues[]`。
|
|
321
|
+
"""
|
|
322
|
+
import openpyxl
|
|
323
|
+
d = self.src / "定稿"
|
|
324
|
+
d.mkdir()
|
|
325
|
+
f = d / "引用审计.xlsx"
|
|
326
|
+
wb = openpyxl.Workbook()
|
|
327
|
+
ws = wb.active
|
|
328
|
+
ws.title = "土地表"
|
|
329
|
+
ws["P1"] = "=U25*2" # 相对引用隐藏列 U
|
|
330
|
+
ws["P2"] = "=SUM($U$25:$U$30)" # 绝对引用 + 范围(曾漏匹配 $U$25)
|
|
331
|
+
ws["P3"] = "=A1*2" # 仅引用可见格 → 不得报
|
|
332
|
+
ws["P4"] = "=LOG10(A1)" # 函数名不得被误判为单元格引用
|
|
333
|
+
ws["P5"] = "='隐藏表'!B2" # 引用隐藏工作表
|
|
334
|
+
ws["U25"] = 999999 # 隐藏列的内容(哨兵)
|
|
335
|
+
ws.column_dimensions["U"].hidden = True
|
|
336
|
+
hid = wb.create_sheet("隐藏表")
|
|
337
|
+
hid["B2"] = 888888
|
|
338
|
+
hid.sheet_state = "hidden"
|
|
339
|
+
wb.save(f)
|
|
340
|
+
|
|
341
|
+
self._run()
|
|
342
|
+
|
|
343
|
+
rec = self._inventory()[0]["workbook"]
|
|
344
|
+
kinds = sorted({h["kind"] for h in rec["hiddenRefs"]})
|
|
345
|
+
self.assertEqual(["引用隐藏列", "引用隐藏工作表"], kinds, rec["hiddenRefs"])
|
|
346
|
+
cells = {h["cell"] for h in rec["hiddenRefs"]}
|
|
347
|
+
self.assertEqual({"P1", "P2", "P5"}, cells, "只报真正引用隐藏区的可见格")
|
|
348
|
+
self.assertEqual(["土地表!P1", "土地表!P2", "土地表!P5"], rec["calcChainNotReproducible"])
|
|
349
|
+
self.assertIn("manualConfirmationItems", rec["hiddenRefDisposition"],
|
|
350
|
+
"去向必须随数据走:人工建议检查项,不作为 AI 问题")
|
|
351
|
+
self.assertIn("不作为 AI 问题", rec["hiddenRefDisposition"])
|
|
352
|
+
blob = (self.case / "材料盘点.json").read_text(encoding="utf-8")
|
|
353
|
+
for sentinel in ("999999", "888888"):
|
|
354
|
+
self.assertNotIn(sentinel, blob, "引用审计只出坐标,绝不能带出隐藏区内容")
|
|
355
|
+
|
|
356
|
+
def test_hidden_reference_audit_is_quiet_when_hidden_columns_are_unreferenced(self):
|
|
357
|
+
"""隐藏列仅被隐藏区内部自引用(可见区无引用)→ 不产生建议项元数据(无声即无噪声)。"""
|
|
358
|
+
import openpyxl
|
|
359
|
+
d = self.src / "定稿"
|
|
360
|
+
d.mkdir()
|
|
361
|
+
wb = openpyxl.Workbook()
|
|
362
|
+
ws = wb.active
|
|
363
|
+
ws.title = "汇总表"
|
|
364
|
+
ws["A1"] = 1
|
|
365
|
+
ws["C1"] = "=C2*2" # 隐藏列内部自引用(隐藏格,不得读取)
|
|
366
|
+
ws["C2"] = 5
|
|
367
|
+
ws.column_dimensions["C"].hidden = True
|
|
368
|
+
wb.save(d / "自引用.xlsx")
|
|
369
|
+
|
|
370
|
+
self._run()
|
|
371
|
+
|
|
372
|
+
rec = self._inventory()[0]["workbook"]
|
|
373
|
+
self.assertEqual([], rec["hiddenRefs"])
|
|
374
|
+
self.assertEqual([], rec["calcChainNotReproducible"])
|
|
375
|
+
self.assertIsNone(rec["hiddenRefDisposition"], "无引用时不出任何建议项元数据")
|
|
376
|
+
|
|
377
|
+
# ---- 4d2 多版本隐藏结构比对(H0 元数据级)----------------------------
|
|
378
|
+
def test_same_named_versions_with_different_hidden_structure_flag_drift(self):
|
|
379
|
+
"""同名定稿/送审稿隐藏结构不一致 → 输出元数据级提示,且不读隐藏内容。"""
|
|
380
|
+
import openpyxl
|
|
381
|
+
for stage, hide_col in (("定稿", "O"), ("送审稿", None)):
|
|
382
|
+
d = self.src / stage
|
|
383
|
+
d.mkdir()
|
|
384
|
+
wb = openpyxl.Workbook()
|
|
385
|
+
wb.active["A1"] = "值"
|
|
386
|
+
wb.active["O1"] = 999999
|
|
387
|
+
if hide_col:
|
|
388
|
+
wb.active.column_dimensions[hide_col].hidden = True
|
|
389
|
+
wb.save(d / "13-评估明细表-20231231.xlsx")
|
|
390
|
+
|
|
391
|
+
self._run()
|
|
392
|
+
|
|
393
|
+
drift = self._payload().get("hiddenStructureDrift", [])
|
|
394
|
+
self.assertEqual(1, len(drift), drift)
|
|
395
|
+
variants = drift[0]["variants"]
|
|
396
|
+
self.assertEqual(2, len(variants), variants)
|
|
397
|
+
blob = (self.case / "材料盘点.json").read_text(encoding="utf-8")
|
|
398
|
+
self.assertNotIn("999999", blob, "比对只出隐藏元数据,不得带出隐藏内容值")
|
|
399
|
+
|
|
400
|
+
def test_same_named_versions_with_identical_hidden_structure_do_not_flag(self):
|
|
401
|
+
import openpyxl
|
|
402
|
+
for stage in ("定稿", "送审稿"):
|
|
403
|
+
d = self.src / stage
|
|
404
|
+
d.mkdir()
|
|
405
|
+
wb = openpyxl.Workbook()
|
|
406
|
+
wb.active["A1"] = "值"
|
|
407
|
+
wb.active.column_dimensions["O"].hidden = True
|
|
408
|
+
wb.save(d / "13-评估明细表-20231231.xlsx")
|
|
409
|
+
|
|
410
|
+
self._run()
|
|
411
|
+
|
|
412
|
+
self.assertEqual([], self._payload().get("hiddenStructureDrift", []))
|
|
413
|
+
|
|
414
|
+
# ---- 4d 压缩包:下载后必须先解压再审核 --------------------------------
|
|
415
|
+
def test_archive_is_extracted_and_contents_are_processed(self):
|
|
416
|
+
"""归档必须先解压,且解压产物随同一套逻辑继续处理(xlsx 也要出工作版)。"""
|
|
417
|
+
import openpyxl
|
|
418
|
+
import zipfile
|
|
419
|
+
d = self.src / "参考材料"
|
|
420
|
+
d.mkdir()
|
|
421
|
+
inner = self.case / "tmp_inner.xlsx"
|
|
422
|
+
wb = openpyxl.Workbook()
|
|
423
|
+
wb.active["A1"] = "底稿"
|
|
424
|
+
wb.save(inner)
|
|
425
|
+
with zipfile.ZipFile(d / "测算表.zip", "w") as z:
|
|
426
|
+
z.write(inner, "13-工业变性住宅-测算底稿.xlsx")
|
|
427
|
+
inner.unlink()
|
|
428
|
+
|
|
429
|
+
self._run()
|
|
430
|
+
|
|
431
|
+
payload = self._payload()
|
|
432
|
+
self.assertEqual(1, len(payload["archives"]))
|
|
433
|
+
self.assertEqual(["13-工业变性住宅-测算底稿.xlsx"], payload["archives"][0]["extracted"])
|
|
434
|
+
extracted = [r for r in self._inventory() if r.get("originArchive")]
|
|
435
|
+
self.assertEqual(1, len(extracted), self._inventory())
|
|
436
|
+
self.assertEqual("材料-源/参考材料/测算表.zip", extracted[0]["originArchive"])
|
|
437
|
+
self.assertTrue(extracted[0]["workbook"]["workVersion"].startswith("工作版/"),
|
|
438
|
+
"解压出的 xlsx 必须同样重建工作版")
|
|
439
|
+
|
|
440
|
+
def test_archive_with_gbk_filenames_is_decoded(self):
|
|
441
|
+
"""zip 内中文名常为 GBK(无 UTF-8 标志位)→ 必须正确解码,文件名可读。
|
|
442
|
+
|
|
443
|
+
注意:`zipfile.writestr` 遇到非 ASCII 名会自动置 UTF-8 标志位,造不出真实 GBK 包;
|
|
444
|
+
故用同长度 ASCII 占位写入后,在字节层替换为 GBK 字节(标志位保持 0)。
|
|
445
|
+
"""
|
|
446
|
+
import zipfile
|
|
447
|
+
d = self.src / "参考材料"
|
|
448
|
+
d.mkdir()
|
|
449
|
+
raw = "13-轻型汽车-土地使用权-工业变性住宅-测算底稿.xlsx".encode("gbk")
|
|
450
|
+
placeholder = "A" * len(raw)
|
|
451
|
+
path = d / "gbk.zip"
|
|
452
|
+
with zipfile.ZipFile(path, "w") as z:
|
|
453
|
+
z.writestr(placeholder, b"x")
|
|
454
|
+
blob = path.read_bytes()
|
|
455
|
+
assert placeholder.encode("ascii") in blob
|
|
456
|
+
path.write_bytes(blob.replace(placeholder.encode("ascii"), raw))
|
|
457
|
+
with zipfile.ZipFile(path) as z: # 自检:确实是"无 UTF-8 标志位"的 GBK 名
|
|
458
|
+
info = z.infolist()[0]
|
|
459
|
+
self.assertEqual(0, info.flag_bits & 0x800)
|
|
460
|
+
self.assertEqual(raw.decode("cp437"), info.filename) # cp437 读出的乱码名(真实情形)
|
|
461
|
+
|
|
462
|
+
self._run()
|
|
463
|
+
|
|
464
|
+
names = self._payload()["archives"][0]["extracted"]
|
|
465
|
+
self.assertEqual(["13-轻型汽车-土地使用权-工业变性住宅-测算底稿.xlsx"], names)
|
|
466
|
+
|
|
467
|
+
def test_archive_entries_escaping_or_symlinked_are_refused(self):
|
|
468
|
+
"""zip-slip / 绝对路径 / 符号链接一律拒绝,并如实记账。"""
|
|
469
|
+
import zipfile
|
|
470
|
+
d = self.src / "参考材料"
|
|
471
|
+
d.mkdir()
|
|
472
|
+
with zipfile.ZipFile(d / "evil.zip", "w") as z:
|
|
473
|
+
z.writestr("ok.txt", "fine")
|
|
474
|
+
z.writestr("../escape.txt", "bad")
|
|
475
|
+
z.writestr("/abs/escape.txt", "bad")
|
|
476
|
+
link = zipfile.ZipInfo("link")
|
|
477
|
+
link.external_attr = (0o120777 << 16)
|
|
478
|
+
z.writestr(link, "/etc/passwd")
|
|
479
|
+
|
|
480
|
+
self._run()
|
|
481
|
+
|
|
482
|
+
arec = self._payload()["archives"][0]
|
|
483
|
+
self.assertEqual(["ok.txt"], arec["extracted"])
|
|
484
|
+
self.assertEqual(3, len(arec["failures"]), arec["failures"])
|
|
485
|
+
self.assertFalse((self.case / "escape.txt").exists())
|
|
486
|
+
self.assertFalse((self.src / ".." / "escape.txt").resolve().exists())
|
|
487
|
+
|
|
488
|
+
def test_archive_entries_that_are_review_records_are_not_extracted(self):
|
|
489
|
+
"""阶段一隔离门禁优先于解压:归档内复核记录类文件不解压。"""
|
|
490
|
+
import zipfile
|
|
491
|
+
d = self.src / "参考材料"
|
|
492
|
+
d.mkdir()
|
|
493
|
+
with zipfile.ZipFile(d / "mixed.zip", "w") as z:
|
|
494
|
+
z.writestr("测算底稿.xlsx", "x")
|
|
495
|
+
z.writestr("三级复核意见.docx", "secret")
|
|
496
|
+
|
|
497
|
+
self._run()
|
|
498
|
+
|
|
499
|
+
arec = self._payload()["archives"][0]
|
|
500
|
+
self.assertEqual(["测算底稿.xlsx"], arec["extracted"])
|
|
501
|
+
self.assertEqual(["三级复核意见.docx"], arec["skippedIsolation"])
|
|
502
|
+
blob = (self.case / "材料盘点.json").read_text(encoding="utf-8")
|
|
503
|
+
self.assertIn("三级复核意见.docx", blob) # 只记名字
|
|
504
|
+
self.assertNotIn("secret", blob) # 不得带出内容
|
|
505
|
+
|
|
506
|
+
def test_unsupported_archive_records_capability_gap_not_silence(self):
|
|
507
|
+
"""不支持的归档格式(.rar/.7z 且无外部工具)必须记 capability gap,不得静默跳过。"""
|
|
508
|
+
import shutil as sh
|
|
509
|
+
d = self.src / "参考材料"
|
|
510
|
+
d.mkdir()
|
|
511
|
+
(d / "扫描件.rar").write_bytes(b"Rar!\x1a\x07\x00dummy")
|
|
512
|
+
saved = sh.which
|
|
513
|
+
sh.which = lambda name: None
|
|
514
|
+
try:
|
|
515
|
+
self._run()
|
|
516
|
+
finally:
|
|
517
|
+
sh.which = saved
|
|
518
|
+
|
|
519
|
+
arec = self._payload()["archives"][0]
|
|
520
|
+
self.assertTrue(arec["capabilityGaps"], arec)
|
|
521
|
+
self.assertIn("未安装", arec["capabilityGaps"][0])
|
|
522
|
+
|
|
523
|
+
# ---- 5 格式语义 ------------------------------------------------------
|
|
524
|
+
def test_number_format_is_preserved(self):
|
|
525
|
+
"""重建须保留 number_format,避免数值/日期语义被改变。"""
|
|
526
|
+
import openpyxl
|
|
527
|
+
d = self.src / "定稿"
|
|
528
|
+
d.mkdir()
|
|
529
|
+
wb = openpyxl.Workbook()
|
|
530
|
+
ws = wb.active
|
|
531
|
+
ws["A1"] = 0.075
|
|
532
|
+
ws["A1"].number_format = "0.00%"
|
|
533
|
+
wb.save(d / "格式.xlsx")
|
|
534
|
+
|
|
535
|
+
self._run()
|
|
536
|
+
|
|
537
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/格式.xlsx")
|
|
538
|
+
self.assertEqual("0.00%", rebuilt.active["A1"].number_format)
|
|
539
|
+
|
|
540
|
+
|
|
541
|
+
# ---- 6 隐藏区区段展开(防"隐藏列漏剔"假阳性)------------------------
|
|
542
|
+
@staticmethod
|
|
543
|
+
def _hide_columns(path: Path, sheet_name: str, lo: int, hi: int):
|
|
544
|
+
"""按 raw XML 写 `<col min lo max hi hidden="1"/>`(含区段,覆盖假阳性根因场景)。
|
|
545
|
+
|
|
546
|
+
注意:openpyxl 的 `ws.column_dimensions` 只把该区段挂在**首列**上,这正是此前
|
|
547
|
+
`_hidden_metadata()` 漏剔区段内其余列、把人工隐藏内容读进工作版的根因。
|
|
548
|
+
"""
|
|
549
|
+
import re
|
|
550
|
+
tmp = path.with_suffix(".hide.xlsx")
|
|
551
|
+
with zipfile.ZipFile(path) as zin, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
552
|
+
names = dict(re.findall(
|
|
553
|
+
r'<sheet[^>]*name="([^"]+)"[^>]*r:id="([^"]+)"',
|
|
554
|
+
zin.read("xl/workbook.xml").decode("utf-8")))
|
|
555
|
+
rels = {}
|
|
556
|
+
for rel in re.finditer(r"<Relationship\b([^>]*)/?>",
|
|
557
|
+
zin.read("xl/_rels/workbook.xml.rels").decode("utf-8")):
|
|
558
|
+
a = dict(re.findall(r'([A-Za-z_:][\w:.-]*)\s*=\s*"([^"]*)"', rel.group(1)))
|
|
559
|
+
if a.get("Id") and a.get("Target"):
|
|
560
|
+
rels[a["Id"]] = a["Target"]
|
|
561
|
+
target = rels[names[sheet_name]].lstrip("/")
|
|
562
|
+
target = target if target.startswith("xl/") else "xl/" + target
|
|
563
|
+
for item in zin.infolist():
|
|
564
|
+
data = zin.read(item.filename)
|
|
565
|
+
if item.filename == target:
|
|
566
|
+
text = data.decode("utf-8")
|
|
567
|
+
cols = '<cols><col min="{0}" max="{1}" hidden="1"/></cols>'.format(lo, hi)
|
|
568
|
+
text = re.sub(r"<sheetData", cols + "<sheetData", text, count=1)
|
|
569
|
+
data = text.encode("utf-8")
|
|
570
|
+
zout.writestr(item, data)
|
|
571
|
+
shutil.move(str(tmp), str(path))
|
|
572
|
+
|
|
573
|
+
def test_hidden_column_range_is_fully_excluded(self):
|
|
574
|
+
"""隐藏列**区段**(min<max)内每一列都不得进入工作版(此前只剔首列 → 假阳性)。"""
|
|
575
|
+
import openpyxl
|
|
576
|
+
d = self.src / "定稿"
|
|
577
|
+
d.mkdir()
|
|
578
|
+
wb = openpyxl.Workbook()
|
|
579
|
+
ws = wb.active
|
|
580
|
+
ws.title = "汇总表"
|
|
581
|
+
ws["A1"] = "序号"
|
|
582
|
+
ws["J1"] = "隐藏段首列"
|
|
583
|
+
ws["K1"] = "隐藏段第二列(曾漏剔)"
|
|
584
|
+
wb.save(d / "区段隐藏.xlsx")
|
|
585
|
+
self._hide_columns(d / "区段隐藏.xlsx", "汇总表", lo=10, hi=11) # J:K
|
|
586
|
+
|
|
587
|
+
self._run()
|
|
588
|
+
|
|
589
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/区段隐藏.xlsx")
|
|
590
|
+
rws = rebuilt["汇总表"]
|
|
591
|
+
self.assertEqual("序号", rws["A1"].value)
|
|
592
|
+
self.assertIsNone(rws["J1"].value, "区段内首列 J 应被剔除")
|
|
593
|
+
self.assertIsNone(rws["K1"].value, "区段内其余列 K 也必须被剔除(本条即历史失效点)")
|
|
594
|
+
meta = [i for i in self._inventory() if i["name"] == "区段隐藏.xlsx"][0]["workbook"]
|
|
595
|
+
self.assertIn("K", meta["hiddenMeta"]["hiddenCols"]["汇总表"])
|
|
596
|
+
|
|
597
|
+
# ---- 7 表名含首尾空格时隐藏区引用审计仍须命中 ------------------------
|
|
598
|
+
def test_hidden_reference_audit_matches_sheet_name_with_spaces(self):
|
|
599
|
+
"""表名含首尾空格时,隐藏**列**引用仍须被审计命中(此前恒返回 0 处 → 漏报)。"""
|
|
600
|
+
import openpyxl
|
|
601
|
+
d = self.src / "定稿"
|
|
602
|
+
d.mkdir()
|
|
603
|
+
wb = openpyxl.Workbook()
|
|
604
|
+
ws = wb.active
|
|
605
|
+
ws.title = "报价表 " # 尾空格,历史失效场景
|
|
606
|
+
ws["A1"] = 1
|
|
607
|
+
ws["B1"] = "=A1*2"
|
|
608
|
+
wb.save(d / "空格表名.xlsx")
|
|
609
|
+
self._hide_columns(d / "空格表名.xlsx", "报价表 ", lo=1, hi=1) # 隐藏 A 列
|
|
610
|
+
|
|
611
|
+
self._run()
|
|
612
|
+
|
|
613
|
+
meta = [i for i in self._inventory() if i["name"] == "空格表名.xlsx"][0]["workbook"]
|
|
614
|
+
self.assertGreaterEqual(meta["hiddenRefsCount"], 1,
|
|
615
|
+
"表名含空格时不得漏报隐藏列引用(归一化缺失的回归)")
|
|
616
|
+
self.assertTrue(any(r["kind"] == "引用隐藏列" for r in meta["hiddenRefs"]), meta["hiddenRefs"])
|
|
617
|
+
|
|
618
|
+
# ---- 8 虚增 dimension 不得拖垮遍历 ----------------------------------
|
|
619
|
+
def test_inflated_dimension_does_not_force_full_sheet_scan(self):
|
|
620
|
+
"""`dimension` 被虚增(如 A1:Y1048575)时,遍历边界须取**真实用区**。"""
|
|
621
|
+
import openpyxl
|
|
622
|
+
d = self.src / "定稿"
|
|
623
|
+
d.mkdir()
|
|
624
|
+
wb = openpyxl.Workbook()
|
|
625
|
+
ws = wb.active
|
|
626
|
+
ws["A1"] = "合计"
|
|
627
|
+
wb.save(d / "虚增维度.xlsx")
|
|
628
|
+
p = d / "虚增维度.xlsx"
|
|
629
|
+
tmp = p.with_suffix(".dim.xlsx")
|
|
630
|
+
with zipfile.ZipFile(p) as zin, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
631
|
+
for item in zin.infolist():
|
|
632
|
+
data = zin.read(item.filename)
|
|
633
|
+
if item.filename.startswith("xl/worksheets/sheet"):
|
|
634
|
+
text = data.decode("utf-8")
|
|
635
|
+
text = text.replace('ref="A1"', 'ref="A1:Y1048575"', 1)
|
|
636
|
+
data = text.encode("utf-8")
|
|
637
|
+
zout.writestr(item, data)
|
|
638
|
+
shutil.move(str(tmp), str(p))
|
|
639
|
+
|
|
640
|
+
src_ws = openpyxl.load_workbook(p).active
|
|
641
|
+
max_row, max_col = self.module._sheet_bounds(src_ws)
|
|
642
|
+
self.assertEqual((1, 1), (max_row, max_col), "虚增 dimension 不得被当作遍历边界")
|
|
643
|
+
|
|
644
|
+
self._run() # 仍须在正常时间内完成
|
|
645
|
+
meta = [i for i in self._inventory() if i["name"] == "虚增维度.xlsx"][0]["workbook"]
|
|
646
|
+
self.assertEqual(1, meta["sheets"][0]["maxRow"])
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
# ---- 9 表格遍历的四层边界(B1 扫描 / B2 内容 / B3 异常 / B4 护栏) ------
|
|
650
|
+
def test_orphan_styled_cell_does_not_force_full_sheet_scan(self):
|
|
651
|
+
"""游离格式格(`value=None` 但有样式)落在末行:边界须按**有值格**定,并登记规范提示。
|
|
652
|
+
|
|
653
|
+
真实失效(2026-302135-LX9619-BG8634 的 `4-15-3无形-其他`):
|
|
654
|
+
944 个存在格中 697 个纯格式 + 1 个游离在第 1048575 行
|
|
655
|
+
→ 重建循环 26,214,375 次、单表 56 s;隐藏引用审计再扫满 → 70 s。
|
|
656
|
+
只按"存在的格"(`_cells`)定界**仍会踩到**——那个游离格就在 `_cells` 里。
|
|
657
|
+
"""
|
|
658
|
+
import openpyxl
|
|
659
|
+
from openpyxl.styles import Font
|
|
660
|
+
d = self.src / "定稿"
|
|
661
|
+
d.mkdir()
|
|
662
|
+
wb = openpyxl.Workbook()
|
|
663
|
+
ws = wb.active
|
|
664
|
+
ws["A1"] = "合计"
|
|
665
|
+
ws["A2"] = 42
|
|
666
|
+
ws.cell(row=1048575, column=6).font = Font(bold=True) # 只有样式、没有值
|
|
667
|
+
wb.save(d / "游离格式格.xlsx")
|
|
668
|
+
|
|
669
|
+
src_ws = openpyxl.load_workbook(d / "游离格式格.xlsx").active
|
|
670
|
+
self.assertEqual((2, 1), self.module._sheet_bounds(src_ws),
|
|
671
|
+
"边界须按有值格定,不得被游离格式格顶到 1048575 行")
|
|
672
|
+
|
|
673
|
+
self._run() # 且须在正常时间内完成
|
|
674
|
+
|
|
675
|
+
meta = [i for i in self._inventory() if i["name"] == "游离格式格.xlsx"][0]["workbook"]
|
|
676
|
+
self.assertEqual(2, meta["sheets"][0]["maxRow"])
|
|
677
|
+
anomalies = [a for a in self._payload().get("sheetAnomalies", [])
|
|
678
|
+
if a["path"].endswith("游离格式格.xlsx")]
|
|
679
|
+
self.assertEqual(1, len(anomalies), "声明用区远大于实际有值区时须登记表格规范提示")
|
|
680
|
+
self.assertGreater(anomalies[0]["orphanRows"], 1000)
|
|
681
|
+
self.assertIn("1048575", anomalies[0]["declaredDim"])
|
|
682
|
+
self.assertEqual(1, anomalies[0]["styleOnlyCells"])
|
|
683
|
+
self.assertEqual({"maxRow": 1048575, "maxCol": 6}, meta["sheets"][0]["declaredSpan"],
|
|
684
|
+
"声明用区与有值区不一致时须一并留痕(低于 B3 阈值时更要有)")
|
|
685
|
+
rebuilt = openpyxl.load_workbook(self.case / "工作版/游离格式格.xlsx", data_only=True)
|
|
686
|
+
self.assertEqual("合计", rebuilt.active["A1"].value)
|
|
687
|
+
self.assertEqual(42, rebuilt.active["A2"].value)
|
|
688
|
+
|
|
689
|
+
def test_oversized_sheet_records_capability_gap_not_silence(self):
|
|
690
|
+
"""单表有值格超上限:记 capability gap 并跳过该表,其余表继续,不得静默。"""
|
|
691
|
+
import openpyxl
|
|
692
|
+
orig = self.module.MAX_SHEET_CELLS
|
|
693
|
+
self.module.MAX_SHEET_CELLS = 5
|
|
694
|
+
try:
|
|
695
|
+
d = self.src / "定稿"
|
|
696
|
+
d.mkdir()
|
|
697
|
+
wb = openpyxl.Workbook()
|
|
698
|
+
big = wb.active
|
|
699
|
+
big.title = "超限表"
|
|
700
|
+
for i in range(1, 11):
|
|
701
|
+
big.cell(row=i, column=1, value=i)
|
|
702
|
+
wb.create_sheet("正常表")["A1"] = "ok"
|
|
703
|
+
wb.save(d / "超限.xlsx")
|
|
704
|
+
self._run()
|
|
705
|
+
finally:
|
|
706
|
+
self.module.MAX_SHEET_CELLS = orig
|
|
707
|
+
|
|
708
|
+
rec = [i for i in self._inventory() if i["name"] == "超限.xlsx"][0]
|
|
709
|
+
self.assertTrue(rec.get("capabilityGaps"), "超限必须记 capability gap,不得静默跳过")
|
|
710
|
+
self.assertIn("未完整处理", " ".join(rec["capabilityGaps"]))
|
|
711
|
+
self.assertEqual(["正常表"], [s["sheet"] for s in rec["workbook"]["sheets"]],
|
|
712
|
+
"超限表不进工作版,其余表照常处理")
|
|
713
|
+
|
|
714
|
+
# ---- 10 非单元格证据(媒体)不得因工作版丢失而被判"缺失" ----------------
|
|
715
|
+
def test_raw_media_is_counted_and_flagged_as_non_cell_evidence(self):
|
|
716
|
+
"""原件含内嵌媒体时:登记 `rawMediaCount`,汇总进 `nonCellEvidence` 并提示不得据工作版判缺失。
|
|
717
|
+
|
|
718
|
+
真实失效(2026-302150-LX9757-BG8677):重建工作版丢失全部 `xl/media`(审核对象已非送审件原貌),
|
|
719
|
+
导致 `MKT-004` 判"可比实例位置图为空、询价截图缺失",而原件实有 26 个媒体对象。
|
|
720
|
+
"""
|
|
721
|
+
import openpyxl
|
|
722
|
+
import shutil
|
|
723
|
+
import zipfile
|
|
724
|
+
d = self.src / "定稿"
|
|
725
|
+
d.mkdir()
|
|
726
|
+
wb = openpyxl.Workbook()
|
|
727
|
+
wb.active["A1"] = "x"
|
|
728
|
+
p = d / "带图.xlsx"
|
|
729
|
+
wb.save(p)
|
|
730
|
+
tmp = p.with_suffix(".media.xlsx") # openpyxl 不写图,直接补 zip 部件模拟内嵌图
|
|
731
|
+
with zipfile.ZipFile(p) as zin, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
732
|
+
for item in zin.infolist():
|
|
733
|
+
zout.writestr(item, zin.read(item.filename))
|
|
734
|
+
zout.writestr("xl/media/image1.png", b"\x89PNG\r\n\x1a\n" + b"0" * 64)
|
|
735
|
+
shutil.move(str(tmp), str(p))
|
|
736
|
+
|
|
737
|
+
self._run()
|
|
738
|
+
|
|
739
|
+
rec = [i for i in self._inventory() if i["name"] == "带图.xlsx"][0]["workbook"]
|
|
740
|
+
self.assertEqual(1, rec["rawMediaCount"], "须登记原件媒体数")
|
|
741
|
+
self.assertEqual(0, rec["mediaCarriedOver"], "重建法不复制媒体,须如实记账")
|
|
742
|
+
self.assertIn("禁止依据工作版", rec["nonCellEvidenceNote"])
|
|
743
|
+
nce = self._payload()["nonCellEvidence"]
|
|
744
|
+
self.assertEqual((1, 1), (nce["filesWithMedia"], nce["rawMediaCount"]))
|
|
745
|
+
rebuilt = self.case / "工作版/带图.xlsx"
|
|
746
|
+
self.assertEqual(
|
|
747
|
+
[], [n for n in zipfile.ZipFile(rebuilt).namelist() if n.startswith("xl/media/")],
|
|
748
|
+
"工作版不得残留媒体——正因如此,媒体类结论必须回 raw 原件")
|
|
749
|
+
|
|
750
|
+
# ---- 11 媒体证据通道:docx 与独立图片也必须导出(此前只有 xlsx 计数)----
|
|
751
|
+
def test_docx_and_standalone_images_are_exported_into_media_index(self):
|
|
752
|
+
"""docx 内嵌图与独立图片必须落 `媒体证据/` 并进 `媒体索引.json`。
|
|
753
|
+
|
|
754
|
+
真实失效:`评估说明.docx` 正文写着"以下为询价截图:",图片被文本抽取静默丢弃,
|
|
755
|
+
盘点里连媒体字段都没有;独立图片的 note 只有"图片:无 OCR",没有任何可读路径。
|
|
756
|
+
"""
|
|
757
|
+
d = self.src / "定稿"
|
|
758
|
+
d.mkdir()
|
|
759
|
+
_minimal_docx_with_image(d / "评估说明.docx")
|
|
760
|
+
(d / "现场照片.png").write_bytes(_PNG)
|
|
761
|
+
|
|
762
|
+
self._run()
|
|
763
|
+
|
|
764
|
+
items = {r["name"]: r for r in self._inventory()}
|
|
765
|
+
docx_ev = items["评估说明.docx"]["mediaEvidence"]
|
|
766
|
+
self.assertEqual(1, docx_ev["exportedCount"], docx_ev)
|
|
767
|
+
self.assertEqual(1, items["现场照片.png"]["mediaEvidence"]["exportedCount"])
|
|
768
|
+
self.assertEqual("host-vision", items["现场照片.png"]["mediaEvidence"]["evidenceChannel"])
|
|
769
|
+
|
|
770
|
+
idx = json.loads((self.case / "媒体索引.json").read_text(encoding="utf-8"))
|
|
771
|
+
self.assertEqual(2, idx["count"], idx)
|
|
772
|
+
for e in idx["entries"]:
|
|
773
|
+
self.assertTrue((self.case / e["localPath"]).exists(), e)
|
|
774
|
+
self.assertTrue(e["sha256"] and e["sizeBytes"] > 0, e)
|
|
775
|
+
locators = sorted(e["locator"] for e in idx["entries"])
|
|
776
|
+
self.assertTrue(any("图片#" in x for x in locators), locators)
|
|
777
|
+
self.assertTrue(any("(整图)" in x for x in locators), locators)
|
|
778
|
+
|
|
779
|
+
nce = self._payload()["nonCellEvidence"]
|
|
780
|
+
self.assertEqual(2, nce["filesWithMedia"])
|
|
781
|
+
self.assertEqual(2, nce["exportedMediaCount"])
|
|
782
|
+
self.assertEqual("媒体索引.json", nce["mediaIndexPath"])
|
|
783
|
+
|
|
784
|
+
def test_unreferenced_media_is_not_silently_dropped(self):
|
|
785
|
+
"""未被 drawing 引用的媒体部件:不导出,但必须留未核原因(未核 ≠ 缺失)。"""
|
|
786
|
+
import openpyxl
|
|
787
|
+
d = self.src / "定稿"
|
|
788
|
+
d.mkdir()
|
|
789
|
+
wb = openpyxl.Workbook()
|
|
790
|
+
wb.active["A1"] = "x"
|
|
791
|
+
p = d / "无锚点图.xlsx"
|
|
792
|
+
wb.save(p)
|
|
793
|
+
tmp = p.with_suffix(".media.xlsx")
|
|
794
|
+
with zipfile.ZipFile(p) as zin, zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout:
|
|
795
|
+
for item in zin.infolist():
|
|
796
|
+
zout.writestr(item, zin.read(item.filename))
|
|
797
|
+
zout.writestr("xl/media/image1.png", _PNG)
|
|
798
|
+
shutil.move(str(tmp), str(p))
|
|
799
|
+
|
|
800
|
+
self._run()
|
|
801
|
+
|
|
802
|
+
rec = [r for r in self._inventory() if r["name"] == "无锚点图.xlsx"][0]
|
|
803
|
+
ev = rec["mediaEvidence"]
|
|
804
|
+
self.assertEqual(0, ev["exportedCount"], "锚点不可得时不得导出")
|
|
805
|
+
self.assertTrue(ev["unresolvedCount"] >= 1, ev)
|
|
806
|
+
self.assertIn("未导出", " ".join(ev["unresolvedReasons"]))
|
|
807
|
+
self.assertFalse((self.case / "媒体证据").exists() and
|
|
808
|
+
any((self.case / "媒体证据").rglob("*.png")),
|
|
809
|
+
"锚点不可得的媒体不得落盘")
|
|
810
|
+
|
|
811
|
+
# ---- 12 阶段二复跑不得覆盖阶段一冻结产物 ------------------------------
|
|
812
|
+
def test_review_pass_does_not_clobber_phase1_artifacts(self):
|
|
813
|
+
"""阶段二对 `复核-人工/` 复跑:抽复核件媒体,但阶段一冻结产物逐字节不变。"""
|
|
814
|
+
d = self.src / "定稿"
|
|
815
|
+
d.mkdir()
|
|
816
|
+
_minimal_docx_with_image(d / "评估说明.docx")
|
|
817
|
+
self._run()
|
|
818
|
+
inv_before = _sha256(self.case / "材料盘点.json")
|
|
819
|
+
idx_before = _sha256(self.case / "媒体索引.json")
|
|
820
|
+
|
|
821
|
+
review = self.case / "复核-人工"
|
|
822
|
+
review.mkdir()
|
|
823
|
+
_minimal_docx_with_image(review / "三级复核意见.docx")
|
|
824
|
+
|
|
825
|
+
self._run_label("复核", review)
|
|
826
|
+
|
|
827
|
+
self.assertEqual(inv_before, _sha256(self.case / "材料盘点.json"),
|
|
828
|
+
"阶段一盘点表是冻结产物,阶段二复跑不得改写")
|
|
829
|
+
self.assertEqual(idx_before, _sha256(self.case / "媒体索引.json"),
|
|
830
|
+
"阶段一媒体索引同样不得被阶段二覆盖")
|
|
831
|
+
idx = json.loads((self.case / "复核媒体索引.json").read_text(encoding="utf-8"))
|
|
832
|
+
self.assertEqual(1, idx["count"], idx)
|
|
833
|
+
self.assertIn("复核意见", json.dumps(idx, ensure_ascii=False))
|
|
834
|
+
self.assertTrue((self.case / "复核盘点.json").exists())
|
|
835
|
+
# 复核件的媒体落在独立的媒体证据目录,不混入阶段一目录
|
|
836
|
+
self.assertTrue(next((self.case / "媒体证据-复核").rglob("*.png"), None))
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
# ---- 13 CLI:相对 --src 必须相对 --case 解析 --------------------------
|
|
840
|
+
def test_cli_relative_src_resolves_against_case(self):
|
|
841
|
+
"""`--src 复核-人工` 必须解析到 `<案例>/复核-人工`,而不是当前工作目录。
|
|
842
|
+
|
|
843
|
+
真实踩坑:SKILL.md 里的阶段二命令是 `--case <案例目录> --src 复核-人工 --label 复核`,
|
|
844
|
+
若相对路径按 cwd 解析,命令会直接报"源材料目录不存在"。
|
|
845
|
+
"""
|
|
846
|
+
import sys
|
|
847
|
+
review = self.case / "复核-人工"
|
|
848
|
+
review.mkdir()
|
|
849
|
+
_minimal_docx_with_image(review / "三级复核意见.docx")
|
|
850
|
+
|
|
851
|
+
saved = sys.argv
|
|
852
|
+
sys.argv = ["prepare_materials.py", "--case", str(self.case),
|
|
853
|
+
"--src", "复核-人工", "--label", "复核"]
|
|
854
|
+
try:
|
|
855
|
+
rc = self.module.main()
|
|
856
|
+
finally:
|
|
857
|
+
sys.argv = saved
|
|
858
|
+
|
|
859
|
+
self.assertEqual(0, rc)
|
|
860
|
+
idx = json.loads((self.case / "复核媒体索引.json").read_text(encoding="utf-8"))
|
|
861
|
+
self.assertEqual(1, idx["count"], idx)
|
|
862
|
+
self.assertFalse((self.case / "材料盘点.json").exists(),
|
|
863
|
+
"只跑阶段二时不应产出阶段一盘点表")
|
|
864
|
+
|
|
865
|
+
|
|
866
|
+
_RUNNER_TEMPLATE = """\
|
|
867
|
+
import importlib.abc
|
|
868
|
+
import importlib.util
|
|
869
|
+
import pathlib
|
|
870
|
+
import runpy
|
|
871
|
+
import sys
|
|
872
|
+
|
|
873
|
+
PRUNE = set({prune!r})
|
|
874
|
+
PROBE = set({probe!r})
|
|
875
|
+
|
|
876
|
+
|
|
877
|
+
def _prune(names):
|
|
878
|
+
dropped = []
|
|
879
|
+
for entry in list(sys.path):
|
|
880
|
+
if not entry:
|
|
881
|
+
continue
|
|
882
|
+
base = pathlib.Path(entry)
|
|
883
|
+
if any((base / n).is_dir() or (base / (n + ".py")).is_file() for n in names):
|
|
884
|
+
dropped.append(entry)
|
|
885
|
+
sys.path[:] = [e for e in sys.path if e not in dropped]
|
|
886
|
+
for name in names:
|
|
887
|
+
sys.modules.pop(name, None)
|
|
888
|
+
importlib.invalidate_caches()
|
|
889
|
+
|
|
890
|
+
|
|
891
|
+
class _ProbeRaiser(importlib.abc.MetaPathFinder):
|
|
892
|
+
def find_spec(self, fullname, path=None, target=None):
|
|
893
|
+
if fullname.split(".")[0] in PROBE:
|
|
894
|
+
raise ModuleNotFoundError("No module named " + repr(fullname), name=fullname)
|
|
895
|
+
return None
|
|
896
|
+
|
|
897
|
+
|
|
898
|
+
_prune(PRUNE)
|
|
899
|
+
sys.meta_path.insert(0, _ProbeRaiser())
|
|
900
|
+
sys.argv = {argv!r}
|
|
901
|
+
runpy.run_path({script!r}, run_name="__main__")
|
|
902
|
+
"""
|
|
903
|
+
|
|
904
|
+
|
|
905
|
+
class DependencyStateBoundaryTest(unittest.TestCase):
|
|
906
|
+
"""OPT-005-R1/-R2:**格式级依赖边界** + 三态结构化登记。
|
|
907
|
+
|
|
908
|
+
顶层依赖固定:`.xlsx`→`openpyxl`、`.pdf`→`pypdf`、`.xls`→`xlrd`;`.docx` 保留
|
|
909
|
+
`docx`→`textutil` 的既有回退(不产生依赖缺口)。传递依赖只能进 `causeModule`/`detail`。
|
|
910
|
+
|
|
911
|
+
三种模拟都在**子进程**里做,不卸载、不改动本机包:
|
|
912
|
+
|
|
913
|
+
- `prune`:删掉 sys.path 里装着该依赖的条目 → `find_spec` 返回 None(**真缺包** = `missing`)
|
|
914
|
+
- `shadow`:影子模块(`find_spec` 找得到、import 抛错)→ **`import_failed`**
|
|
915
|
+
- `probe`:finder 的 `find_spec` 自身抛异常 → **`probe_failed`**
|
|
916
|
+
"""
|
|
917
|
+
|
|
918
|
+
def _run(self, case: Path, src: Path, *, prune=(), probe=(), shadow=None,
|
|
919
|
+
name: str = "材料盘点.json"):
|
|
920
|
+
runner = case / "_runner.py"
|
|
921
|
+
runner.write_text(
|
|
922
|
+
_RUNNER_TEMPLATE.format(
|
|
923
|
+
prune=tuple(prune),
|
|
924
|
+
probe=tuple(probe),
|
|
925
|
+
argv=[str(SCRIPTS_DIR / "prepare_materials.py"), "--case", str(case),
|
|
926
|
+
"--src", str(src), "--inventory", name],
|
|
927
|
+
script=str(SCRIPTS_DIR / "prepare_materials.py"),
|
|
928
|
+
),
|
|
929
|
+
encoding="utf-8",
|
|
930
|
+
)
|
|
931
|
+
env = dict(os.environ)
|
|
932
|
+
paths = []
|
|
933
|
+
if shadow:
|
|
934
|
+
shadow_dir = case / "_shadow"
|
|
935
|
+
shadow_dir.mkdir(exist_ok=True)
|
|
936
|
+
for module, body in shadow.items():
|
|
937
|
+
(shadow_dir / f"{module}.py").write_text(body, encoding="utf-8")
|
|
938
|
+
paths.append(str(shadow_dir))
|
|
939
|
+
if env.get("PYTHONPATH"):
|
|
940
|
+
paths.append(env["PYTHONPATH"])
|
|
941
|
+
if paths:
|
|
942
|
+
env["PYTHONPATH"] = os.pathsep.join(paths)
|
|
943
|
+
env["PYTHONDONTWRITEBYTECODE"] = "1"
|
|
944
|
+
return subprocess.run(
|
|
945
|
+
[sys.executable, str(runner)], capture_output=True, text=True, env=env, cwd=str(case)
|
|
946
|
+
)
|
|
947
|
+
|
|
948
|
+
def _case(self, files, *, prune=(), probe=(), shadow=None):
|
|
949
|
+
"""建临时案例跑一次,返回 (案例目录, 进程结果, {文件名: 记录})。"""
|
|
950
|
+
temp = tempfile.TemporaryDirectory()
|
|
951
|
+
self.addCleanup(temp.cleanup)
|
|
952
|
+
case = Path(temp.name)
|
|
953
|
+
src = case / "材料-源"
|
|
954
|
+
src.mkdir()
|
|
955
|
+
for name, data in files.items():
|
|
956
|
+
(src / name).write_bytes(data)
|
|
957
|
+
proc = self._run(case, src, prune=prune, probe=probe, shadow=shadow)
|
|
958
|
+
payload = json.loads((case / "材料盘点.json").read_text(encoding="utf-8"))
|
|
959
|
+
return case, proc, {item["name"]: item for item in payload["items"]}
|
|
960
|
+
|
|
961
|
+
def _assert_gap(self, rec, *, dependency, ext, state):
|
|
962
|
+
"""三态共同的结构要求 + 该状态专属的措辞。"""
|
|
963
|
+
self.assertFalse(rec.get("readable"), f"readable 必须为 false:{rec}")
|
|
964
|
+
gaps = rec.get("dependencyGaps") or []
|
|
965
|
+
self.assertEqual(1, len(gaps), f"必须有一条结构化依赖记录:{rec}")
|
|
966
|
+
gap = gaps[0]
|
|
967
|
+
self.assertEqual(dependency, gap["dependency"], f"依赖字段必须是该格式的顶层依赖:{gap}")
|
|
968
|
+
self.assertEqual(ext, gap["format"])
|
|
969
|
+
self.assertEqual(state, gap["state"], f"状态判定不符:{gap}")
|
|
970
|
+
self.assertIs(True, gap["notChecked"])
|
|
971
|
+
self.assertIs(False, gap["workVersionCreated"])
|
|
972
|
+
self.assertEqual(rec["path"], gap["path"])
|
|
973
|
+
# 三态都必须进 capabilityGaps(上游据此知道"该件未核")
|
|
974
|
+
self.assertTrue(
|
|
975
|
+
any(dependency in entry for entry in (rec.get("capabilityGaps") or [])),
|
|
976
|
+
f"必须进结构化 capabilityGaps:{rec}",
|
|
977
|
+
)
|
|
978
|
+
self.assertIsNot(True, rec.get("missing"), "依赖问题不得登记为材料缺失")
|
|
979
|
+
self.assertNotIn("materialGap", rec)
|
|
980
|
+
note = rec.get("note") or ""
|
|
981
|
+
if state == "missing":
|
|
982
|
+
self.assertIn("缺少第三方运行时依赖", note)
|
|
983
|
+
self.assertIn("缺少第三方运行时依赖", gap["reason"])
|
|
984
|
+
else:
|
|
985
|
+
self.assertNotIn("缺少", note, f"只有 missing 才能说「缺少」:{note}")
|
|
986
|
+
self.assertNotIn("缺少", gap["reason"], f"只有 missing 才能说「缺少」:{gap}")
|
|
987
|
+
return gap
|
|
988
|
+
|
|
989
|
+
# ---- 1 真缺包:.xlsx ----
|
|
990
|
+
def test_xlsx_missing_openpyxl(self):
|
|
991
|
+
case, proc, items = self._case(
|
|
992
|
+
{"明细表.xlsx": b"PK\x03\x04", "说明.txt": b"non excel"},
|
|
993
|
+
prune=("openpyxl",),
|
|
994
|
+
)
|
|
995
|
+
self.assertEqual(0, proc.returncode, proc.stderr[-600:])
|
|
996
|
+
self.assertEqual({"明细表.xlsx", "说明.txt"}, set(items), "其他文件不得被短路")
|
|
997
|
+
gap = self._assert_gap(items["明细表.xlsx"], dependency="openpyxl", ext=".xlsx",
|
|
998
|
+
state="missing")
|
|
999
|
+
self.assertFalse((case / "工作版" / "明细表.xlsx").exists(), "缺依赖时不得生成工作版")
|
|
1000
|
+
|
|
1001
|
+
# ---- 2 真缺包:.pdf ----
|
|
1002
|
+
def test_pdf_missing_pypdf_is_not_blamed_on_openpyxl(self):
|
|
1003
|
+
_case, _proc, items = self._case({"说明.pdf": b"%PDF-1.4\n% fake\n"}, prune=("pypdf",))
|
|
1004
|
+
gap = self._assert_gap(items["说明.pdf"], dependency="pypdf", ext=".pdf", state="missing")
|
|
1005
|
+
self.assertIsNone(gap.get("causeModule"))
|
|
1006
|
+
self.assertNotIn("openpyxl", json.dumps(items["说明.pdf"], ensure_ascii=False))
|
|
1007
|
+
|
|
1008
|
+
# ---- 3 真缺包:.xls(OLE magic 最小夹具)----
|
|
1009
|
+
def test_xls_missing_xlrd_with_ole_fixture(self):
|
|
1010
|
+
ole = b"\xd0\xcf\x11\xe0\xa1\xb1\x1a\xe1" + b"\x00" * 512
|
|
1011
|
+
_case, _proc, items = self._case({"明细表.xls": ole}, prune=("xlrd",))
|
|
1012
|
+
self._assert_gap(items["明细表.xls"], dependency="xlrd", ext=".xls", state="missing")
|
|
1013
|
+
|
|
1014
|
+
# ---- 4 传递依赖缺失:dependency 仍是顶层依赖 ----
|
|
1015
|
+
def test_transitive_missing_module_keeps_the_top_level_dependency(self):
|
|
1016
|
+
_case, _proc, items = self._case(
|
|
1017
|
+
{"说明.pdf": b"%PDF-1.4\n% fake\n"},
|
|
1018
|
+
shadow={"pypdf": "import _missing_inner_helper_xyz\n"},
|
|
1019
|
+
)
|
|
1020
|
+
gap = self._assert_gap(items["说明.pdf"], dependency="pypdf", ext=".pdf",
|
|
1021
|
+
state="import_failed")
|
|
1022
|
+
self.assertEqual("_missing_inner_helper_xyz", gap.get("causeModule"),
|
|
1023
|
+
f"传递依赖只能进 causeModule:{gap}")
|
|
1024
|
+
self.assertIn("_missing_inner_helper_xyz", gap.get("detail") or "")
|
|
1025
|
+
|
|
1026
|
+
# ---- 5 顶层依赖存在但导入失败 ----
|
|
1027
|
+
def test_import_failed_openpyxl_is_a_structured_gap(self):
|
|
1028
|
+
_case, _proc, items = self._case(
|
|
1029
|
+
{"明细表.xlsx": b"PK\x03\x04"},
|
|
1030
|
+
shadow={"openpyxl": 'raise ImportError("模拟:openpyxl 已损坏")\n'},
|
|
1031
|
+
)
|
|
1032
|
+
rec = items["明细表.xlsx"]
|
|
1033
|
+
self._assert_gap(rec, dependency="openpyxl", ext=".xlsx", state="import_failed")
|
|
1034
|
+
self.assertIn("导入失败", rec["note"], f"import_failed 必须说清是导入失败:{rec['note']}")
|
|
1035
|
+
|
|
1036
|
+
# ---- 6 探测失败 ----
|
|
1037
|
+
def test_probe_failed_is_not_reported_as_missing(self):
|
|
1038
|
+
_case, _proc, items = self._case({"明细表.xlsx": b"PK\x03\x04"}, probe=("openpyxl",))
|
|
1039
|
+
gap = self._assert_gap(items["明细表.xlsx"], dependency="openpyxl", ext=".xlsx",
|
|
1040
|
+
state="probe_failed")
|
|
1041
|
+
self.assertIn("探测失败", gap["reason"])
|
|
1042
|
+
self.assertNotIn("缺少", json.dumps(gap, ensure_ascii=False))
|
|
1043
|
+
|
|
1044
|
+
# ---- 7 结构护栏:不得再有重复相邻的宽泛 except ----
|
|
1045
|
+
def test_no_duplicate_adjacent_broad_except(self):
|
|
1046
|
+
import ast
|
|
1047
|
+
|
|
1048
|
+
source = (SCRIPTS_DIR / "prepare_materials.py").read_text(encoding="utf-8")
|
|
1049
|
+
tree = ast.parse(source)
|
|
1050
|
+
offenders = []
|
|
1051
|
+
for node in ast.walk(tree):
|
|
1052
|
+
if not isinstance(node, ast.Try):
|
|
1053
|
+
continue
|
|
1054
|
+
for first, second in zip(node.handlers, node.handlers[1:]):
|
|
1055
|
+
left, right = first.type, second.type
|
|
1056
|
+
if left is None or right is None:
|
|
1057
|
+
offenders.append(second.lineno)
|
|
1058
|
+
continue
|
|
1059
|
+
if (isinstance(left, ast.Name) and isinstance(right, ast.Name)
|
|
1060
|
+
and left.id == right.id and left.id in ("Exception", "BaseException")):
|
|
1061
|
+
offenders.append(second.lineno)
|
|
1062
|
+
self.assertEqual(
|
|
1063
|
+
[], offenders,
|
|
1064
|
+
"出现相邻且捕获同一宽泛类型的 except(后一个恒不可达)—— 行号:" + repr(offenders),
|
|
1065
|
+
)
|
|
1066
|
+
|
|
1067
|
+
def test_no_cross_format_openpyxl_default(self):
|
|
1068
|
+
"""不得存在"跨格式默认回退到 openpyxl"的逻辑;顶层依赖必须来自固定表。"""
|
|
1069
|
+
source = (SCRIPTS_DIR / "prepare_materials.py").read_text(encoding="utf-8")
|
|
1070
|
+
for forbidden in ('or "openpyxl"', "or 'openpyxl'", 'or"openpyxl"'):
|
|
1071
|
+
self.assertNotIn(forbidden, source, f"不得硬编码回退:{forbidden}")
|
|
1072
|
+
module = _load_module()
|
|
1073
|
+
self.assertEqual(
|
|
1074
|
+
{".xlsx": "openpyxl", ".pdf": "pypdf", ".xls": "xlrd"},
|
|
1075
|
+
dict(module.FORMAT_DEPENDENCIES),
|
|
1076
|
+
"格式 → 顶层依赖必须是固定表(.docx 保留 textutil 回退,不入表)",
|
|
1077
|
+
)
|
|
1078
|
+
|
|
1079
|
+
|
|
1080
|
+
if __name__ == "__main__":
|
|
1081
|
+
unittest.main(verbosity=2)
|