minimal-agent 0.8.2 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -12
- package/package.json +34 -1
- package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
- package/plugins/cron-runner/commands/cron.md +26 -0
- package/plugins/cron-runner/commands/crons.md +7 -0
- package/plugins/cron-runner/plugin.js +34 -0
- package/plugins/cron-runner/src/commands.js +348 -0
- package/plugins/cron-runner/src/cronTime.js +148 -0
- package/plugins/cron-runner/src/lock.js +68 -0
- package/plugins/cron-runner/src/runs.js +89 -0
- package/plugins/cron-runner/src/scheduleEngine.js +72 -0
- package/plugins/cron-runner/src/scheduler.js +355 -0
- package/plugins/cron-runner/src/schema.js +140 -0
- package/plugins/cron-runner/src/service.js +167 -0
- package/plugins/cron-runner/src/store.js +298 -0
- package/plugins/cron-runner/src/types.js +1 -0
- package/plugins/workflow-runner/commands/workflow.md +4 -4
- package/plugins/workflow-runner/commands/workflows.md +1 -1
- package/plugins/workflow-runner/plugin.js +3 -1
- package/plugins/workflow-runner/src/agentCommon.js +249 -51
- package/plugins/workflow-runner/src/dslManifest.js +72 -0
- package/plugins/workflow-runner/src/expressions.js +198 -15
- package/plugins/workflow-runner/src/index.js +40 -8
- package/plugins/workflow-runner/src/jsonPayload.js +1 -1
- package/plugins/workflow-runner/src/loader.js +286 -72
- package/plugins/workflow-runner/src/multimodal.js +37 -0
- package/plugins/workflow-runner/src/recovery.js +49 -39
- package/plugins/workflow-runner/src/runner.js +802 -287
- package/plugins/workflow-runner/src/stepExecutors/agent.js +157 -39
- package/plugins/workflow-runner/src/stepExecutors/assert.js +16 -3
- package/plugins/workflow-runner/src/stepExecutors/llm.js +60 -111
- package/plugins/workflow-runner/src/stepExecutors/skill.js +47 -47
- package/plugins/workflow-runner/src/stepExecutors/tool.js +76 -10
- package/plugins/workflow-runner/src/stepExecutors/verify.js +27 -10
- package/plugins/workflow-runner/src/taskIdentity.js +296 -0
- package/plugins/workflow-runner/src/types.js +19 -9
- package/plugins/workflow-runner/src/workflowState.js +516 -33
- package/skills/adversarial-review/SKILL.md +0 -1
- package/skills/ai-image/SKILL.md +89 -0
- package/skills/ai-image/scripts/generate.mjs +497 -0
- package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
- package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
- package/skills/ai-image/scripts/generate_test.mjs +74 -0
- package/skills/browser-automation/SKILL.md +47 -0
- package/skills/docx/SKILL.md +25 -3
- package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
- package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
- package/skills/docx/scripts/opc_check.py +253 -0
- package/skills/image-gen-openrouter/SKILL.md +10 -2
- package/skills/image-stats/SKILL.md +65 -0
- package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
- package/skills/image-stats/scripts/image_stats.py +368 -0
- package/skills/image-stats/scripts/image_stats_test.py +181 -0
- package/skills/pptx/SKILL.md +123 -95
- package/skills/pptx/design.md +82 -0
- package/skills/pptx/examples/README.md +29 -0
- package/skills/pptx/examples/hybrid_example.mjs +107 -0
- package/skills/pptx/examples/hybrid_example.pptx +0 -0
- package/skills/pptx/pptxgenjs.md +41 -0
- package/skills/pptx/scripts/deck_helpers.mjs +224 -0
- package/skills/pptx/scripts/layout_lint.py +293 -0
- package/skills/pptx/scripts/layout_lint_test.py +160 -0
- package/skills/pptx/scripts/opc_check.py +339 -0
- package/skills/pptx/scripts/svg_fallback.mjs +145 -0
- package/skills/pptx/svg.md +67 -0
- package/skills/tts-zh/SKILL.md +19 -2
- package/skills/ui-design/SKILL.md +158 -0
- package/skills/ui-design/claude_design.md +309 -0
- package/skills/ui-design/evals/evals.json +254 -0
- package/skills/ui-design/references/aesthetic-direction.md +83 -0
- package/skills/ui-design/references/claude-design-protocol.md +36 -0
- package/skills/ui-design/references/design-context.md +53 -0
- package/skills/ui-design/references/design-execution-playbook.md +82 -0
- package/skills/ui-design/references/export-pptx.md +124 -0
- package/skills/ui-design/references/layout-composition.md +654 -0
- package/skills/ui-design/references/preview-feedback.md +37 -0
- package/skills/ui-design/references/source-image-layout.md +189 -0
- package/skills/ui-design/references/starter-components.md +78 -0
- package/skills/ui-design/references/vi-aegis-vector.md +144 -0
- package/skills/ui-design/references/vi-amber-prism.md +133 -0
- package/skills/ui-design/references/vi-aqua-glass.md +200 -0
- package/skills/ui-design/references/vi-comfort-vision.md +147 -0
- package/skills/ui-design/references/vi-ember-prism.md +140 -0
- package/skills/ui-design/references/vi-nebula-console.md +151 -0
- package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
- package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
- package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
- package/skills/ui-design/references/vi-precision-horizon.md +215 -0
- package/skills/ui-design/references/vi-solar-relay.md +130 -0
- package/skills/ui-design/references/vi-template-workflow.md +48 -0
- package/skills/ui-design/references/vi-verdant-grid.md +141 -0
- package/skills/ui-design/references/vi-violet-meridian.md +172 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
- package/skills/ui-design/scripts/assemble_material.py +217 -0
- package/skills/ui-design/scripts/assemble_material_test.py +149 -0
- package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
- package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
- package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
- package/skills/ui-design/scripts/check_language_policy.py +351 -0
- package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
- package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
- package/skills/ui-design/scripts/deck_pages_check.py +158 -0
- package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
- package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
- package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
- package/skills/ui-design/scripts/design_budget_check.py +271 -0
- package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
- package/skills/ui-design/scripts/design_repair_hints.py +162 -0
- package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
- package/skills/ui-design/scripts/design_style_check.py +397 -0
- package/skills/ui-design/scripts/design_style_check_test.py +233 -0
- package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
- package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
- package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
- package/skills/ui-design/scripts/extract_source_assets.py +607 -0
- package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
- package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
- package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
- package/skills/ui-design/scripts/image_plan_lint.py +571 -0
- package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
- package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
- package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
- package/skills/ui-design/scripts/layout_balance.py +881 -0
- package/skills/ui-design/scripts/layout_balance_test.py +927 -0
- package/skills/ui-design/scripts/normalize_design_json.py +272 -0
- package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
- package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
- package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
- package/skills/ui-design/scripts/resolve_source_input.py +270 -0
- package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
- package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
- package/skills/ui-design/scripts/source_usage_check.py +581 -0
- package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
- package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
- package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
- package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
- package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
- package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
- package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
- package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
- package/skills/ui-design/tests/e2e/rubric.json +54 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
- package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
- package/skills/video-compose/SKILL.md +8 -1
- package/skills/web-crawler/SKILL.md +70 -0
- package/skills/web-crawler/scripts/crawl.mjs +538 -0
- package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
- package/skills/xlsx/SKILL.md +12 -0
- package/skills/xlsx/scripts/opc_check.py +253 -0
- package/src/advisorState.js +22 -0
- package/src/askBridge.js +97 -3
- package/src/bootstrap/workingDir.js +12 -1
- package/src/cli/args.js +17 -1
- package/src/cli/configMenu.js +236 -0
- package/src/cli/configWizard.js +7 -5
- package/src/cli/print.js +37 -4
- package/src/cli/streamJson.js +77 -24
- package/src/cli/wizards/advisorWizard.js +147 -0
- package/src/cli/wizards/fieldForm.js +149 -0
- package/src/cli/wizards/imageWizard.js +60 -0
- package/src/cli/wizards/openRouterWizard.js +23 -0
- package/src/cli/wizards/providerPresets.js +95 -0
- package/src/cli/wizards/providerWizard.js +219 -0
- package/src/cli/wizards/searchWizard.js +23 -0
- package/src/cli/wizards/ttsWizard.js +34 -0
- package/src/config/configFile.js +441 -83
- package/src/config/configService.js +446 -0
- package/src/config.js +24 -64
- package/src/context/agentHome.js +9 -0
- package/src/context/archive.js +7 -2
- package/src/context/compact.js +8 -5
- package/src/context/contextLock.js +81 -0
- package/src/context/persistContext.js +30 -11
- package/src/context/reactiveCompact.js +4 -2
- package/src/context/recentDirs.js +21 -61
- package/src/context/sessionPath.js +97 -23
- package/src/context/sessionRegistry.js +303 -24
- package/src/context/tokenCounter.js +18 -0
- package/src/context/workspaceIndex.js +731 -0
- package/src/infra/verification.js +28 -15
- package/src/llm/client.js +394 -159
- package/src/llm/providers/anthropicFormat.js +241 -0
- package/src/llm/providers/openaiFormat.js +190 -0
- package/src/loop.js +219 -32
- package/src/main.js +61 -11
- package/src/messageQueue.js +97 -0
- package/src/multimodal.js +203 -0
- package/src/plugin-sdk.js +15 -0
- package/src/plugins/commandRouter.js +3 -0
- package/src/plugins/hookEngine.js +5 -1
- package/src/plugins/pluginLifecycle.js +28 -0
- package/src/plugins/pluginLoader.js +2 -1
- package/src/plugins/pluginRunner.js +49 -21
- package/src/plugins/transcript.js +4 -3
- package/src/prompts/pluginList.js +5 -5
- package/src/prompts/projectInstructions.js +3 -3
- package/src/prompts/promptAssembly.js +133 -144
- package/src/prompts/skillList.js +12 -22
- package/src/prompts/system.js +0 -1
- package/src/session/runAgentTurn.js +70 -0
- package/src/session/runTurn.js +41 -6
- package/src/toolGate.js +17 -0
- package/src/tools/advisor/advisor.js +379 -0
- package/src/tools/agent/agent.js +555 -0
- package/src/tools/agent/agentTypes.js +59 -0
- package/src/tools/agent/coordinator.js +93 -0
- package/src/tools/agent/worktree.js +270 -0
- package/src/tools/ask/ask.js +7 -2
- package/src/tools/bash/bash.js +112 -18
- package/src/tools/download/download.js +144 -0
- package/src/tools/edit/edit.js +1 -0
- package/src/tools/edit/multi-edit.js +1 -0
- package/src/tools/genimage/genimage.js +242 -0
- package/src/tools/genspeech/genspeech.js +67 -0
- package/src/tools/glob/glob.js +1 -0
- package/src/tools/grep/grep.js +3 -0
- package/src/tools/grep/rgPath.js +2 -0
- package/src/tools/index.js +25 -2
- package/src/tools/read/read.js +1 -0
- package/src/tools/shared/killTree.js +52 -0
- package/src/tools/toolProgressBridge.js +48 -0
- package/src/tools/webbrowser/agentBrowserPath.js +258 -0
- package/src/tools/webbrowser/browser.js +59 -42
- package/src/tools/webbrowser/browserControl.js +118 -0
- package/src/tools/webbrowser/browserDaemon.js +97 -0
- package/src/tools/webbrowser/browserService.js +27 -0
- package/src/tools/webbrowser/browserStream.js +119 -0
- package/src/tools/webbrowser/webbrowser.js +699 -130
- package/src/tools/webfetch/webfetch.js +1 -0
- package/src/tools/websearch/websearch.js +134 -51
- package/src/tools/write/write.js +1 -0
- package/src/ui/App.js +249 -22
- package/src/ui/AskQuestion.js +29 -10
- package/src/ui/InputBox.js +104 -12
- package/src/ui/LiveArea.js +112 -19
- package/src/ui/MessageList.js +100 -14
- package/src/ui/ModelPicker.js +56 -0
- package/src/ui/QueuedMessagesBanner.js +14 -0
- package/src/ui/Root.js +18 -10
- package/src/ui/SessionPicker.js +102 -0
- package/src/ui/StatusLine.js +13 -4
- package/src/ui/WorkspacePicker.js +55 -0
- package/src/ui/figures.js +44 -0
- package/src/ui/hooks/askSlot.js +82 -0
- package/src/ui/hooks/useChat.js +358 -64
- package/src/ui/hooks/usePasteHandler.js +11 -1
- package/src/ui/hooks/useTextBuffer.js +1 -3
- package/src/ui/liveViewport.js +5 -1
- package/src/ui/markdown.js +206 -0
- package/src/ui/textBuffer.js +0 -7
- package/src/ui/theme.js +32 -0
- package/src/utils/debugLog.js +0 -4
- package/src/utils/resourcePaths.js +2 -2
- package/src/utils/stripThink.js +50 -0
- package/src/web/browserBridge.js +447 -0
- package/src/web/protocol.js +19 -0
- package/workflows/adaptive-research.yaml +13 -5
- package/workflows/book-review-short.yaml +36 -10
- package/workflows/e2e-write-greet.yaml +6 -2
- package/workflows/illustrate.yaml +227 -0
- package/workflows/plan.yaml +460 -179
- package/workflows/ppt.yaml +1659 -0
- package/workflows/ralph-loop.yaml +199 -0
- package/workflows/schema.json +81 -23
- package/workflows/webppt.yaml +496 -0
- package/workflows/word.yaml +710 -0
- package/workflows/youtube-shorts.yaml +46 -15
- package/MINIMAL-TRACE.md +0 -77
- package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
- package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
- package/plugins/ralph-wiggum/README.md +0 -197
- package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
- package/plugins/ralph-wiggum/commands/help.md +0 -137
- package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
- package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
- package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
- package/plugins/ralph-wiggum/plugin.js +0 -280
- package/plugins/ralph-wiggum/src/goalState.js +0 -258
- package/plugins/ralph-wiggum/src/sentinels.js +0 -76
- package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
- package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
- package/plugins/workflow-runner/src/miniReAct.js +0 -223
- package/plugins/workflow-runner/src/voteHelpers.js +0 -110
- package/skills/config/SKILL.md +0 -123
- package/skills/frontend-design/LICENSE.txt +0 -177
- package/skills/frontend-design/SKILL.md +0 -42
- package/src/ui/ProgressPanel.js +0 -54
- package/src/ui/StreamingBlock.js +0 -11
- package/src/ui/ToolStatus.js +0 -11
|
@@ -0,0 +1,767 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""检查生成后的 deck.html 是否退回卡片墙/表格墙/文字墙。
|
|
4
|
+
|
|
5
|
+
它不替代 layout_balance.py。layout_balance 管越界、重叠、截图夹带文本等硬几何问题;
|
|
6
|
+
本脚本只管产品详情页式 PPT 的结构性视觉退化。
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
from collections import Counter
|
|
13
|
+
import json
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from html.parser import HTMLParser
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
SECTION_RE = re.compile(r"<section\b[\s\S]*?</section>", re.I)
|
|
21
|
+
LABEL_RE = re.compile(r"data-screen-label=[\"']([^\"']+)", re.I)
|
|
22
|
+
# 讲稿 aside(<aside class="speaker-notes" hidden>…</aside>,display:none 不渲染、导出进 PPT 备注栏):
|
|
23
|
+
# 其正文是演讲论证,提到「表格/矩阵/card」是在讲内容不是页面布局——所有源码级扫描前先剥掉。
|
|
24
|
+
SPEAKER_NOTES_RE = re.compile(
|
|
25
|
+
r"<aside\b[^>]*class=[\"'][^\"']*speaker-notes[^\"']*[\"'][\s\S]*?</aside>", re.I
|
|
26
|
+
)
|
|
27
|
+
CARD_RE = re.compile(r"card|panel|tile|module|stat-card|dark-card|content-card", re.I)
|
|
28
|
+
TABLE_RE = re.compile(r"<table\b|compare-grid|matrix|table-", re.I)
|
|
29
|
+
BG_RE = re.compile(r"edit-bg|bg-img|background-image\s*:|url\(", re.I)
|
|
30
|
+
OBJECT_FILL_RE = re.compile(r"object-fit\s*:\s*fill", re.I)
|
|
31
|
+
IMAGE_FRAME_RE = re.compile(r"(?:src-figure|fig-fill|image-tile|mosaic|gallery|product-visual|hero-visual|closeup)", re.I)
|
|
32
|
+
DATA_VISUAL_RE = re.compile(r"<svg\b|chart|axis|bar-|bars|line-chart|sparkline|plot|meter|gauge|timeline|data-viz", re.I)
|
|
33
|
+
STRONG_DATA_VISUAL_RE = re.compile(r"<svg\b|bar-chart|line-chart|axis|parameter-axis|data-viz|chart|bars|plot|meter|gauge", re.I)
|
|
34
|
+
CHART_MARKER_RE = re.compile(r"bar-chart|line-chart|axis|parameter-axis|data-viz|chart|bars|plot|meter|gauge", re.I)
|
|
35
|
+
BAD_FRAME_CLASS_RE = re.compile(r"(?:src-figure|fig-fill|image-tile|mosaic|gallery|product-visual|hero-visual|closeup)", re.I)
|
|
36
|
+
BAD_FRAME_STYLE_RE = re.compile(
|
|
37
|
+
r"background\s*:\s*(?:#fff|white|var\(--surface\)|rgba\(255,\s*255,\s*255)"
|
|
38
|
+
r"|padding\s*:|border\s*:",
|
|
39
|
+
re.I,
|
|
40
|
+
)
|
|
41
|
+
IMG_SRC_RE = re.compile(r"<img\b[^>]*?(?<!\.)\bsrc=[\"']([^\"']+)[\"']", re.I)
|
|
42
|
+
BG_IMG_RE = re.compile(r"<img\b[^>]*class=[\"'][^\"']*bg-img[^\"']*[\"']", re.I)
|
|
43
|
+
CLASS_RE = re.compile(r"class=[\"']([^\"']+)[\"']", re.I)
|
|
44
|
+
SOURCE_FILE_RE = re.compile(r"data-source-file=[\"']([^\"']+)[\"']", re.I)
|
|
45
|
+
AI_BACKGROUND_ONLY_RE = re.compile(r"^ai_p\d+\.(?:png|jpg|jpeg|webp)$", re.I)
|
|
46
|
+
FONT_SIZE_RE = re.compile(r"font-size\s*:\s*(\d+(?:\.\d+)?)px", re.I)
|
|
47
|
+
SYSTEMIC_FONT_SIZE_MIN_COUNT = 3 # 同一错误字号的 ERROR 数达此值 → 判定系统性重复值,追加归组提示
|
|
48
|
+
SVG_TEXT_SIZE_RE = re.compile(r"<text\b[^>]*font-size=[\"'](\d+(?:\.\d+)?)[\"']", re.I)
|
|
49
|
+
CONNECTOR_RE = re.compile(r"<(?:line|path|polyline)\b", re.I)
|
|
50
|
+
DIAGRAM_NODE_RE = re.compile(
|
|
51
|
+
r"(?:diagram-node|data-node|flow-node|flow-step|question-node|answer-node|class=[\"'][^\"']*(?:node|step|chip|pill))",
|
|
52
|
+
re.I,
|
|
53
|
+
)
|
|
54
|
+
# 纯装饰性 SVG(layout-composition.md §0.8「装饰几何(连线/节点/分隔光带)」明确要求每页≥1视觉
|
|
55
|
+
# 元素时可用它填充空页;ppt.yaml 要求统一命名 `class="vi-deco"`):其内部的 line/path/polyline
|
|
56
|
+
# 只是几何点缀,不是图表/流程图连线,不该被孤立连线检查盯上——否则跟"没配图的页也要有装饰几何"
|
|
57
|
+
# 这条指导自相矛盾。2026-07-06 真机复盘:模型在提示词未强制命名前自造了 `crosshair-svg` 这个
|
|
58
|
+
# 不同名字表达同一种纯装饰意图,因此这里额外接受几个同义命名兜底(提示词已同步要求统一用 vi-deco,
|
|
59
|
+
# 这里的宽松匹配是双保险,不能替代提示词约定)。
|
|
60
|
+
DECOR_SVG_RE = re.compile(
|
|
61
|
+
r"<svg\b[^>]*\bclass=[\"'][^\"']*\b(?:vi-deco|crosshair-svg|deco-svg|ambient-svg)\b[^\"']*[\"'][^>]*>.*?</svg>",
|
|
62
|
+
re.I | re.S,
|
|
63
|
+
)
|
|
64
|
+
# 2026-07-07 真机三度复盘(同一"命名约定不可靠"主题的第三种表现):这次是一份**真实、完整**的
|
|
65
|
+
# 对比图表——4 组 `<rect>+<text>` 标签节点("记录主体→机器"/"空间覆盖→360°"等)用带箭头的
|
|
66
|
+
# `<line>` 连到右侧的另一组文字标签,节点内容齐全,不是缺节点的孤立连线;但这些 `<rect>` 没有
|
|
67
|
+
# 任何 `class="...node/step/chip/pill..."` 之类的语义类名,DIAGRAM_NODE_RE 纯靠类名匹配抓不到,
|
|
68
|
+
# 照样误报"孤立连线缺节点"。结构特征识别:连线所在的 SVG 里如果同时有形状元素(`<rect>`/
|
|
69
|
+
# `<circle>`)和足够数量的 `<text>` 标签(≥2,一条孤立装饰线不会无缘无故配文字标签),就是
|
|
70
|
+
# 真实数据节点存在的可靠信号,不依赖任何命名约定。
|
|
71
|
+
SVG_SHAPE_RE = re.compile(r"<(?:rect|circle)\b", re.I)
|
|
72
|
+
SVG_TEXT_LABEL_RE = re.compile(r"<text\b", re.I)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _has_labeled_shape_nodes(scope: str) -> bool:
|
|
76
|
+
return bool(SVG_SHAPE_RE.search(scope)) and len(SVG_TEXT_LABEL_RE.findall(scope)) >= 2
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
# 2026-07-06 真机三度复盘:模型这次连 class 都没写(`<svg viewBox="0 0 600 600" style="...">`),
|
|
80
|
+
# 命名约定型豁免(DECOR_SVG_RE)天生管不到。改用结构特征识别:径向准线/雷达式装饰的标志性几何是
|
|
81
|
+
# "多个圆共享同一圆心"(同心圆),这是纯装饰的可靠指纹——真正的图表/流程图几乎不会用同心圆做
|
|
82
|
+
# 核心结构。不依赖任何命名约定,模型不管怎么起名/起不起名都能识别。
|
|
83
|
+
SVG_BLOCK_RE = re.compile(r"<svg\b[^>]*>.*?</svg>", re.I | re.S)
|
|
84
|
+
CIRCLE_CENTER_RE = re.compile(r"<circle\b[^>]*\bcx=[\"']([\d.]+)[\"'][^>]*\bcy=[\"']([\d.]+)[\"']", re.I)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _strip_concentric_decorative_svgs(text: str) -> str:
|
|
88
|
+
def _replace(m: re.Match) -> str:
|
|
89
|
+
block = m.group(0)
|
|
90
|
+
centers = Counter(CIRCLE_CENTER_RE.findall(block))
|
|
91
|
+
if centers and centers.most_common(1)[0][1] >= 2:
|
|
92
|
+
return "" # ≥2 个圆共享同一圆心 = 同心圆装饰,整块视为纯装饰剔除
|
|
93
|
+
return block
|
|
94
|
+
|
|
95
|
+
return SVG_BLOCK_RE.sub(_replace, text)
|
|
96
|
+
BACKPLATE_CLASS_RE = re.compile(r"(?:frost|glass|caption|callout|pill|panel|card|surface|copy-box|text-box)", re.I)
|
|
97
|
+
STYLE_ATTR_RE = re.compile(r"style=[\"']([^\"']+)[\"']", re.I)
|
|
98
|
+
WIDTH_RE = re.compile(r"(?:^|;)\s*(?:width|max-width|min-width)\s*:\s*(\d+(?:\.\d+)?)px", re.I)
|
|
99
|
+
HEIGHT_RE = re.compile(r"(?:^|;)\s*(?:height|max-height|min-height)\s*:\s*(\d+(?:\.\d+)?)px", re.I)
|
|
100
|
+
WRITING_MODE_RE = re.compile(r"writing-mode\s*:\s*(?:vertical|sideways)", re.I)
|
|
101
|
+
GRID_NARROW_RE = re.compile(r"grid-template-columns\s*:[^;]*(?:\b\d{1,2}px\b|\b0\.\d+fr\b)", re.I)
|
|
102
|
+
TEXT_COLOR_RE = re.compile(r"(?:^|;)\s*color\s*:\s*(#[0-9a-f]{3,6}|rgb\([^)]+\))", re.I)
|
|
103
|
+
BG_COLOR_RE = re.compile(r"(?:^|;)\s*background(?:-color)?\s*:\s*(#[0-9a-f]{3,6}|rgb\([^)]+\)|rgba\([^)]+\)|black|white)", re.I)
|
|
104
|
+
IMAGE_LED_STYLES = {
|
|
105
|
+
"product-hero",
|
|
106
|
+
"feature-theater",
|
|
107
|
+
"detail-closeup",
|
|
108
|
+
"scenario-fullbleed",
|
|
109
|
+
"chapter-cinematic",
|
|
110
|
+
"quote-focus",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class TextExtractor(HTMLParser):
|
|
115
|
+
def __init__(self):
|
|
116
|
+
super().__init__(convert_charrefs=True)
|
|
117
|
+
self.parts: list[str] = []
|
|
118
|
+
self._skip = 0
|
|
119
|
+
|
|
120
|
+
def handle_starttag(self, tag, attrs):
|
|
121
|
+
if tag.lower() in {"script", "style"}:
|
|
122
|
+
self._skip += 1
|
|
123
|
+
|
|
124
|
+
def handle_endtag(self, tag):
|
|
125
|
+
if tag.lower() in {"script", "style"} and self._skip:
|
|
126
|
+
self._skip -= 1
|
|
127
|
+
|
|
128
|
+
def handle_data(self, data):
|
|
129
|
+
if not self._skip:
|
|
130
|
+
self.parts.append(data)
|
|
131
|
+
|
|
132
|
+
def text(self) -> str:
|
|
133
|
+
return re.sub(r"\s+", " ", " ".join(self.parts)).strip()
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def load_design(path: str | None):
|
|
137
|
+
if not path:
|
|
138
|
+
return {}
|
|
139
|
+
try:
|
|
140
|
+
data = json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
|
141
|
+
except Exception:
|
|
142
|
+
return {}
|
|
143
|
+
pages = data.get("pages") if isinstance(data, dict) else []
|
|
144
|
+
result = {}
|
|
145
|
+
for page in pages if isinstance(pages, list) else []:
|
|
146
|
+
if isinstance(page, dict):
|
|
147
|
+
result[int(page.get("page") or len(result) + 1)] = page
|
|
148
|
+
return result
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def load_image_report(path: str | None):
|
|
152
|
+
if not path:
|
|
153
|
+
return {"degraded": {}, "fallbacks": {}}
|
|
154
|
+
try:
|
|
155
|
+
data = json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
|
156
|
+
except Exception:
|
|
157
|
+
return {"degraded": {}, "fallbacks": {}}
|
|
158
|
+
degraded = {}
|
|
159
|
+
fallbacks = {}
|
|
160
|
+
for item in data.get("degraded", []) if isinstance(data, dict) else []:
|
|
161
|
+
if not isinstance(item, dict):
|
|
162
|
+
continue
|
|
163
|
+
file = item.get("file")
|
|
164
|
+
if not file:
|
|
165
|
+
continue
|
|
166
|
+
degraded[str(file)] = item
|
|
167
|
+
fallbacks[str(file)] = item.get("fallback")
|
|
168
|
+
return {"degraded": degraded, "fallbacks": fallbacks}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def load_source_plan(path: str | None):
|
|
172
|
+
if not path:
|
|
173
|
+
return {"enhanced": {}, "sources": {}, "plan_images": {}}
|
|
174
|
+
try:
|
|
175
|
+
data = json.loads(Path(path).read_text(encoding="utf-8-sig"))
|
|
176
|
+
except Exception:
|
|
177
|
+
return {"enhanced": {}, "sources": {}, "plan_images": {}}
|
|
178
|
+
enhanced = {}
|
|
179
|
+
sources = {}
|
|
180
|
+
plan_images = {}
|
|
181
|
+
entries = data.get("enhance_entries", []) if isinstance(data, dict) else []
|
|
182
|
+
for item in entries if isinstance(entries, list) else []:
|
|
183
|
+
if not isinstance(item, dict):
|
|
184
|
+
continue
|
|
185
|
+
file = item.get("file")
|
|
186
|
+
source = item.get("source_file")
|
|
187
|
+
if file:
|
|
188
|
+
enhanced[str(file)] = item
|
|
189
|
+
if source:
|
|
190
|
+
sources[str(source)] = item
|
|
191
|
+
# 逐图规划条目(usage_intent / enhance_required / has_embedded_text / target_layer):
|
|
192
|
+
# 供「原图带字禁用」「证据图禁做背景」两项确定性检查(2026-07-02 逐页体检裁决)。
|
|
193
|
+
images = data.get("images", []) if isinstance(data, dict) else []
|
|
194
|
+
for item in images if isinstance(images, list) else []:
|
|
195
|
+
if isinstance(item, dict) and item.get("file"):
|
|
196
|
+
plan_images[str(item["file"])] = item
|
|
197
|
+
return {"enhanced": enhanced, "sources": sources, "plan_images": plan_images}
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def section_text(section: str) -> str:
|
|
201
|
+
parser = TextExtractor()
|
|
202
|
+
parser.feed(section)
|
|
203
|
+
parser.close()
|
|
204
|
+
return parser.text()
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _short_text(html: str) -> str:
|
|
208
|
+
parser = TextExtractor()
|
|
209
|
+
parser.feed(html)
|
|
210
|
+
parser.close()
|
|
211
|
+
return parser.text()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def page_number(label: str | None, index: int) -> int:
|
|
215
|
+
if label:
|
|
216
|
+
m = re.match(r"\s*(\d+)", label)
|
|
217
|
+
if m:
|
|
218
|
+
return int(m.group(1))
|
|
219
|
+
return index
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def _is_remote_or_data(src: str) -> bool:
|
|
223
|
+
return src.startswith(("http://", "https://", "data:", "file:"))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _missing_assets(section: str, deck_dir: Path) -> list[str]:
|
|
227
|
+
if not (deck_dir / "assets").exists():
|
|
228
|
+
return []
|
|
229
|
+
missing = []
|
|
230
|
+
for src in IMG_SRC_RE.findall(section):
|
|
231
|
+
if _is_remote_or_data(src):
|
|
232
|
+
continue
|
|
233
|
+
path = (deck_dir / src).resolve()
|
|
234
|
+
try:
|
|
235
|
+
path.relative_to(deck_dir.resolve())
|
|
236
|
+
except ValueError:
|
|
237
|
+
continue
|
|
238
|
+
if not path.exists():
|
|
239
|
+
missing.append(src)
|
|
240
|
+
return missing
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _basename(src: str) -> str:
|
|
244
|
+
src = src.split("#", 1)[0].split("?", 1)[0]
|
|
245
|
+
return Path(src).name
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _asset_name(value: str) -> str:
|
|
249
|
+
if not value:
|
|
250
|
+
return ""
|
|
251
|
+
return _basename(str(value).strip())
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _section_srcs(section: str) -> list[str]:
|
|
255
|
+
return [_basename(src) for src in IMG_SRC_RE.findall(section)]
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
def _bg_srcs(section: str) -> list[str]:
|
|
259
|
+
out = []
|
|
260
|
+
for tag in re.findall(r"<img\b[^>]*>", section, re.I):
|
|
261
|
+
cls = CLASS_RE.search(tag)
|
|
262
|
+
if not cls or "bg-img" not in cls.group(1):
|
|
263
|
+
continue
|
|
264
|
+
src = re.search(r"\bsrc=[\"']([^\"']+)[\"']", tag, re.I)
|
|
265
|
+
if src:
|
|
266
|
+
out.append(_basename(src.group(1)))
|
|
267
|
+
return out
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _foreground_srcs(section: str) -> list[str]:
|
|
271
|
+
bg = set(_bg_srcs(section))
|
|
272
|
+
return [src for src in _section_srcs(section) if src not in bg]
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _font_size_errors(section: str) -> list[str]:
|
|
276
|
+
errors = []
|
|
277
|
+
for raw in FONT_SIZE_RE.findall(section):
|
|
278
|
+
size = float(raw)
|
|
279
|
+
if size < 24:
|
|
280
|
+
errors.append(f"发现过小字体 font-size:{raw}px;PPT 正文/图表标签必须使用统一字体 token 且不低于 24px")
|
|
281
|
+
for raw in SVG_TEXT_SIZE_RE.findall(section):
|
|
282
|
+
size = float(raw)
|
|
283
|
+
if size < 24:
|
|
284
|
+
errors.append(f"SVG 图表文字过小 font-size={raw}px;轴标/图例/节点标签必须使用可读字体 token")
|
|
285
|
+
return errors
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _hex_to_rgb(color: str) -> tuple[int, int, int] | None:
|
|
289
|
+
color = color.strip().lower()
|
|
290
|
+
if color in {"black", "#000", "#000000"}:
|
|
291
|
+
return (0, 0, 0)
|
|
292
|
+
if color in {"white", "#fff", "#ffffff"}:
|
|
293
|
+
return (255, 255, 255)
|
|
294
|
+
if color.startswith("#"):
|
|
295
|
+
raw = color[1:]
|
|
296
|
+
if len(raw) == 3:
|
|
297
|
+
raw = "".join(ch * 2 for ch in raw)
|
|
298
|
+
if len(raw) == 6:
|
|
299
|
+
try:
|
|
300
|
+
return tuple(int(raw[i : i + 2], 16) for i in (0, 2, 4)) # type: ignore[return-value]
|
|
301
|
+
except Exception:
|
|
302
|
+
return None
|
|
303
|
+
m = re.match(r"rgba?\(([^)]+)\)", color)
|
|
304
|
+
if m:
|
|
305
|
+
parts = [p.strip() for p in m.group(1).split(",")]
|
|
306
|
+
if len(parts) >= 3:
|
|
307
|
+
try:
|
|
308
|
+
return (int(float(parts[0])), int(float(parts[1])), int(float(parts[2])))
|
|
309
|
+
except Exception:
|
|
310
|
+
return None
|
|
311
|
+
return None
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def _rel_lum(rgb: tuple[int, int, int]) -> float:
|
|
315
|
+
def chan(v: int) -> float:
|
|
316
|
+
x = v / 255.0
|
|
317
|
+
return x / 12.92 if x <= 0.03928 else ((x + 0.055) / 1.055) ** 2.4
|
|
318
|
+
|
|
319
|
+
r, g, b = (chan(v) for v in rgb)
|
|
320
|
+
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _contrast_ratio(a: tuple[int, int, int], b: tuple[int, int, int]) -> float:
|
|
324
|
+
l1 = _rel_lum(a)
|
|
325
|
+
l2 = _rel_lum(b)
|
|
326
|
+
hi, lo = max(l1, l2), min(l1, l2)
|
|
327
|
+
return (hi + 0.05) / (lo + 0.05)
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def _text_layout_errors(section: str) -> list[str]:
|
|
331
|
+
errors: list[str] = []
|
|
332
|
+
if WRITING_MODE_RE.search(section):
|
|
333
|
+
errors.append("发现 vertical/sideways writing-mode;PPT 中文正文不得一字一行或竖排破碎")
|
|
334
|
+
if GRID_NARROW_RE.search(section):
|
|
335
|
+
errors.append("发现极窄 grid 列,容易导致中文一字一行;请改宽列或拆页")
|
|
336
|
+
|
|
337
|
+
for tag in re.findall(r"<[^>]+style=[\"'][^\"']+[\"'][^>]*>[\s\S]{0,220}?</[^>]+>", section, re.I):
|
|
338
|
+
style_m = STYLE_ATTR_RE.search(tag)
|
|
339
|
+
if not style_m:
|
|
340
|
+
continue
|
|
341
|
+
style = style_m.group(1)
|
|
342
|
+
text = _short_text(tag)
|
|
343
|
+
if len(text) >= 8:
|
|
344
|
+
widths = [float(v) for v in WIDTH_RE.findall(style)]
|
|
345
|
+
if widths and min(widths) < 180:
|
|
346
|
+
errors.append(f"文本框过窄({min(widths):.0f}px)导致中文易异常换行:{text[:24]}")
|
|
347
|
+
|
|
348
|
+
color_m = TEXT_COLOR_RE.search(style)
|
|
349
|
+
bg_m = BG_COLOR_RE.search(style)
|
|
350
|
+
if color_m and bg_m and len(text) >= 2:
|
|
351
|
+
fg = _hex_to_rgb(color_m.group(1))
|
|
352
|
+
bg = _hex_to_rgb(bg_m.group(1))
|
|
353
|
+
if fg and bg and _contrast_ratio(fg, bg) < 3.0:
|
|
354
|
+
errors.append(f"文本与自身背景对比度不足:{text[:24]}")
|
|
355
|
+
|
|
356
|
+
return errors
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
def _backplate_errors(section: str) -> list[str]:
|
|
360
|
+
errors: list[str] = []
|
|
361
|
+
for tag in re.findall(r"<[^>]+class=[\"'][^\"']+[\"'][^>]*>[\s\S]{0,500}?</[^>]+>", section, re.I):
|
|
362
|
+
cls_m = CLASS_RE.search(tag)
|
|
363
|
+
if not cls_m or not BACKPLATE_CLASS_RE.search(cls_m.group(1)):
|
|
364
|
+
continue
|
|
365
|
+
style_m = STYLE_ATTR_RE.search(tag)
|
|
366
|
+
if not style_m:
|
|
367
|
+
continue
|
|
368
|
+
style = style_m.group(1)
|
|
369
|
+
text = _short_text(tag)
|
|
370
|
+
widths = [float(v) for v in WIDTH_RE.findall(style)]
|
|
371
|
+
heights = [float(v) for v in HEIGHT_RE.findall(style)]
|
|
372
|
+
too_wide = widths and max(widths) >= 760 and len(text) >= 18
|
|
373
|
+
too_tall = heights and max(heights) >= 360 and len(text) >= 25
|
|
374
|
+
if too_wide or too_tall:
|
|
375
|
+
errors.append(
|
|
376
|
+
f"文字背板/浮动层过大({cls_m.group(1)}),会遮挡图片并退回卡片化:{text[:28]}"
|
|
377
|
+
)
|
|
378
|
+
return errors
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def _is_cutout_plan(item: dict | None) -> bool:
|
|
382
|
+
if not isinstance(item, dict):
|
|
383
|
+
return False
|
|
384
|
+
return item.get("enhance_style") == "cutout" or item.get("image_role") == "foreground_cutout"
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _cutout_framed(section: str, src: str) -> bool:
|
|
388
|
+
if not src:
|
|
389
|
+
return False
|
|
390
|
+
escaped = re.escape(src)
|
|
391
|
+
# 只在图片附近检查,避免同页其它说明文字背板误伤透明前景特写。
|
|
392
|
+
near = re.findall(rf"(<[^>]*?(?:{escaped})[^>]*>[\s\S]{{0,260}})", section, re.I)
|
|
393
|
+
near += re.findall(rf"([\s\S]{{0,260}}<[^>]*?(?:{escaped})[^>]*>)", section, re.I)
|
|
394
|
+
hay = "\n".join(near) if near else section if src in section else ""
|
|
395
|
+
return bool(hay and BAD_FRAME_STYLE_RE.search(hay))
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _source_enhanced_framed(section: str, src: str) -> bool:
|
|
399
|
+
"""Detect white-card framing around the source image, not elsewhere on the page."""
|
|
400
|
+
if not src:
|
|
401
|
+
return False
|
|
402
|
+
escaped = re.escape(src)
|
|
403
|
+
img_tags = re.findall(rf"<img\b[^>]*{escaped}[^>]*>", section, re.I)
|
|
404
|
+
if any(BAD_FRAME_STYLE_RE.search(tag) for tag in img_tags):
|
|
405
|
+
return True
|
|
406
|
+
|
|
407
|
+
# Check only likely immediate wrappers. Page-level text padding/borders are
|
|
408
|
+
# normal and should not be treated as image framing.
|
|
409
|
+
wrapper_pattern = (
|
|
410
|
+
rf"<(?P<tag>figure|div)\b(?P<attrs>[^>]*)>"
|
|
411
|
+
rf"(?:(?!</(?P=tag)>)[\s\S]){{0,520}}"
|
|
412
|
+
rf"<img\b[^>]*{escaped}[^>]*>"
|
|
413
|
+
rf"(?:(?!</(?P=tag)>)[\s\S]){{0,220}}</(?P=tag)>"
|
|
414
|
+
)
|
|
415
|
+
for match in re.finditer(wrapper_pattern, section, re.I):
|
|
416
|
+
attrs = match.group("attrs") or ""
|
|
417
|
+
cls_m = CLASS_RE.search(attrs)
|
|
418
|
+
style_m = STYLE_ATTR_RE.search(attrs)
|
|
419
|
+
classes = cls_m.group(1) if cls_m else ""
|
|
420
|
+
style = style_m.group(1) if style_m else ""
|
|
421
|
+
if BAD_FRAME_CLASS_RE.search(classes) and BAD_FRAME_STYLE_RE.search(style):
|
|
422
|
+
return True
|
|
423
|
+
return False
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def _any_image_framed(section: str) -> bool:
|
|
427
|
+
"""Detect a likely image slot wrapped as a white card."""
|
|
428
|
+
for src in _section_srcs(section):
|
|
429
|
+
if _source_enhanced_framed(section, src):
|
|
430
|
+
return True
|
|
431
|
+
return False
|
|
432
|
+
|
|
433
|
+
|
|
434
|
+
def check_section(section: str, index: int, design: dict, deck_dir: Path, image_report: dict, source_plan: dict, strict: bool):
|
|
435
|
+
section = SPEAKER_NOTES_RE.sub("", section) # 讲稿不参与任何布局倾向/字数/资产扫描
|
|
436
|
+
label_m = LABEL_RE.search(section)
|
|
437
|
+
label = label_m.group(1) if label_m else f"{index:02d}"
|
|
438
|
+
page_no = page_number(label, index)
|
|
439
|
+
page_design = design.get(page_no, {})
|
|
440
|
+
style = page_design.get("composition_style")
|
|
441
|
+
grammar = page_design.get("page_grammar")
|
|
442
|
+
errors: list[str] = []
|
|
443
|
+
warnings: list[str] = []
|
|
444
|
+
|
|
445
|
+
text = section_text(section)
|
|
446
|
+
card_count = len(CARD_RE.findall(section))
|
|
447
|
+
table_count = len(TABLE_RE.findall(section))
|
|
448
|
+
has_bg = bool(BG_RE.search(section))
|
|
449
|
+
img_count = len(re.findall(r"<img\b", section, re.I))
|
|
450
|
+
bg_img_count = len(BG_IMG_RE.findall(section))
|
|
451
|
+
foreground_img_count = max(0, img_count - bg_img_count)
|
|
452
|
+
bg_srcs = _bg_srcs(section)
|
|
453
|
+
fg_srcs = _foreground_srcs(section)
|
|
454
|
+
all_srcs = _section_srcs(section)
|
|
455
|
+
image_frame_count = len(IMAGE_FRAME_RE.findall(section))
|
|
456
|
+
has_data_visual = bool(DATA_VISUAL_RE.search(section))
|
|
457
|
+
has_strong_data_visual = bool(STRONG_DATA_VISUAL_RE.search(section))
|
|
458
|
+
max_words = page_design.get("max_visible_words")
|
|
459
|
+
supporting_visual = str(page_design.get("supporting_visual_layer") or "")
|
|
460
|
+
contract = page_design.get("visual_asset_contract") if isinstance(page_design, dict) else None
|
|
461
|
+
# 2026-07-06 真机误报:composition_style/page_grammar/supporting_visual_layer 都是自由文本,
|
|
462
|
+
# 只能靠关键词/枚举值猜"这页该有图";但 design.json 自己的 visual_asset_contract 才是权威——
|
|
463
|
+
# foreground_asset/background_asset 显式声明为空串,就是设计阶段明确表态"这页前景/整页本就
|
|
464
|
+
# 不打算配真实图片,用 HTML/SVG/文字代替"(如目录页 foreground_role="html_svg_marks"、章节页
|
|
465
|
+
# 副标题浮前景本身就是纯文字层)。契约里显式声明的"无图"意图应优先于这些粗糙的文本猜测。
|
|
466
|
+
contract_no_fg = isinstance(contract, dict) and not str(contract.get("foreground_asset") or "").strip()
|
|
467
|
+
contract_no_image_at_all = contract_no_fg and not str(contract.get("background_asset") or "").strip()
|
|
468
|
+
|
|
469
|
+
for src in _missing_assets(section, deck_dir):
|
|
470
|
+
errors.append(f"引用的图片资产不存在: {src}")
|
|
471
|
+
|
|
472
|
+
degraded = image_report.get("degraded", {})
|
|
473
|
+
for src in all_srcs:
|
|
474
|
+
if src in degraded:
|
|
475
|
+
item = degraded[src]
|
|
476
|
+
errors.append(
|
|
477
|
+
f"引用的语义图片 {src} 来自生图降级 fallback={item.get('fallback') or 'unknown'},"
|
|
478
|
+
"不能作为最终产品详情页主视觉"
|
|
479
|
+
)
|
|
480
|
+
|
|
481
|
+
src_counts = Counter(all_srcs)
|
|
482
|
+
bg_counts = Counter(bg_srcs)
|
|
483
|
+
for src in sorted(bg_counts):
|
|
484
|
+
if src_counts[src] > bg_counts[src]:
|
|
485
|
+
hint = ";AI 前景必须独立规划为 ai_pN_fg.png" if AI_BACKGROUND_ONLY_RE.match(src) else ""
|
|
486
|
+
errors.append(f"同一图片 {src} 在同页同时作为背景和前景,容易造成重复/切割/卡片化{hint}")
|
|
487
|
+
|
|
488
|
+
for src in all_srcs:
|
|
489
|
+
if src.startswith("enhanced_src_") and src not in source_plan.get("enhanced", {}):
|
|
490
|
+
warnings.append(f"增强客户图 {src} 未在 source_layout_plan.json 中找到映射,建议检查来源关系")
|
|
491
|
+
|
|
492
|
+
# ── 素材使用契约三连(2026-07-02 逐页体检裁决;plan 缺字段时不判,fail-open)──
|
|
493
|
+
plan_images = source_plan.get("plan_images", {})
|
|
494
|
+
for src in sorted(set(all_srcs)):
|
|
495
|
+
item = plan_images.get(src)
|
|
496
|
+
if not isinstance(item, dict):
|
|
497
|
+
continue
|
|
498
|
+
# ① 原图带字/需重绘禁令:规划标了 enhance_required(去字重绘版已生成/将生成),
|
|
499
|
+
# 原图 src_N 绝不允许直接进 deck——坠机字幕图原样上墙(slide03 事故)就是这条失守。
|
|
500
|
+
if item.get("enhance_required") or item.get("has_embedded_text"):
|
|
501
|
+
enhanced_name = item.get("enhanced_file") or f"enhanced_{src}"
|
|
502
|
+
errors.append(
|
|
503
|
+
f"客户原图 {src} 规划为需重绘(enhance_required/图上带字),不得直接引用;"
|
|
504
|
+
f"改引去字重绘版 {enhanced_name} 或删除该引用"
|
|
505
|
+
)
|
|
506
|
+
# ② 证据/嵌入图禁做整页背景:direct_contain/gallery/evidence 类(旧 PPT 截图、密信息图)
|
|
507
|
+
# 只能 contain 框图展示,铺成背景再叠字 = slide22 事故。
|
|
508
|
+
usage = str(item.get("usage_intent") or "")
|
|
509
|
+
target_layer = str(item.get("target_layer") or "")
|
|
510
|
+
if src in bg_srcs and (usage in {"direct_contain", "gallery", "discard"} or target_layer == "evidence"):
|
|
511
|
+
errors.append(
|
|
512
|
+
f"客户图 {src} 规划为 {usage or target_layer}(证据/嵌入类),不得用作整页背景;"
|
|
513
|
+
"改为 contain 框图 + 短说明,且不得在其上覆盖正文"
|
|
514
|
+
)
|
|
515
|
+
# ③ 弃用图禁入 deck:规划判定与主题不相关/负面(如坠机资讯图)。
|
|
516
|
+
if usage == "discard":
|
|
517
|
+
errors.append(
|
|
518
|
+
f"客户图 {src} 已被规划判定为弃用(与主题不相关/负面),不得出现在 deck 中;删除该引用"
|
|
519
|
+
)
|
|
520
|
+
|
|
521
|
+
# ④ 同族背景+前景重复:src_N 与 enhanced_src_N 视为同一主体(既有检查只抓同名同图)。
|
|
522
|
+
# 背景已是该主体、前景再放同主体特写卡 = 重复抢戏(slide10/14 事故)。
|
|
523
|
+
def _family(name: str) -> str:
|
|
524
|
+
return re.sub(r"^enhanced_", "", name)
|
|
525
|
+
|
|
526
|
+
bg_families = {_family(s) for s in bg_srcs}
|
|
527
|
+
for src in sorted(set(fg_srcs)):
|
|
528
|
+
fam = _family(src)
|
|
529
|
+
if fam in bg_families and src not in bg_srcs:
|
|
530
|
+
errors.append(
|
|
531
|
+
f"客户图同族重复:背景已使用 {fam} 家族(src/enhanced),前景又放 {src};"
|
|
532
|
+
"保留一处主视觉,另一处删除或换独立互补素材"
|
|
533
|
+
)
|
|
534
|
+
|
|
535
|
+
if isinstance(contract, dict):
|
|
536
|
+
foreground_role = str(contract.get("foreground_role") or "").strip().lower()
|
|
537
|
+
foreground_asset = _asset_name(str(contract.get("foreground_asset") or ""))
|
|
538
|
+
background_asset = _asset_name(str(contract.get("background_asset") or ""))
|
|
539
|
+
if foreground_role in {"none", "无", "no", "null"} and fg_srcs:
|
|
540
|
+
errors.append(
|
|
541
|
+
f"design.visual_asset_contract 声明 foreground_role=none,但 HTML 仍插入前景图片 {fg_srcs[:3]}"
|
|
542
|
+
)
|
|
543
|
+
if background_asset and background_asset not in bg_srcs and background_asset in all_srcs:
|
|
544
|
+
errors.append(f"契约要求 {background_asset} 是背景图,但 HTML 没有放入 bg-img/edit-bg 背景层")
|
|
545
|
+
if foreground_asset and foreground_asset in bg_srcs:
|
|
546
|
+
errors.append(f"契约要求 {foreground_asset} 是前景图,但 HTML 把它放入背景层")
|
|
547
|
+
if background_asset and foreground_asset and background_asset == foreground_asset:
|
|
548
|
+
errors.append("design.visual_asset_contract 背景和前景使用同一张图片")
|
|
549
|
+
|
|
550
|
+
for src in fg_srcs:
|
|
551
|
+
if AI_BACKGROUND_ONLY_RE.match(src):
|
|
552
|
+
errors.append(f"{src} 是背景约定名,不能作为前景插图;前景 AI 必须独立规划为 ai_pN_fg.png")
|
|
553
|
+
|
|
554
|
+
for src in fg_srcs:
|
|
555
|
+
item = source_plan.get("enhanced", {}).get(src)
|
|
556
|
+
if _is_cutout_plan(item):
|
|
557
|
+
if src in bg_srcs:
|
|
558
|
+
errors.append(f"cutout 客户图 {src} 被当背景使用;透明前景特写只能浮在独立背景上")
|
|
559
|
+
if _cutout_framed(section, src):
|
|
560
|
+
errors.append(f"cutout 客户图 {src} 被白底/边框/padding 包裹;应透明前景直接融入背景")
|
|
561
|
+
|
|
562
|
+
errors.extend(_font_size_errors(section))
|
|
563
|
+
errors.extend(_text_layout_errors(section))
|
|
564
|
+
errors.extend(_backplate_errors(section))
|
|
565
|
+
connector_scope = _strip_concentric_decorative_svgs(DECOR_SVG_RE.sub("", section))
|
|
566
|
+
if (
|
|
567
|
+
CONNECTOR_RE.search(connector_scope)
|
|
568
|
+
and not DIAGRAM_NODE_RE.search(connector_scope)
|
|
569
|
+
and not CHART_MARKER_RE.search(connector_scope)
|
|
570
|
+
and not _has_labeled_shape_nodes(connector_scope)
|
|
571
|
+
):
|
|
572
|
+
errors.append("发现孤立 line/path/polyline 连线,但没有 diagram-node/data-node/flow-step 等节点归属")
|
|
573
|
+
|
|
574
|
+
if not has_bg:
|
|
575
|
+
errors.append("缺少背景图层或背景引用")
|
|
576
|
+
elif strict and not bg_srcs and style in IMAGE_LED_STYLES and not isinstance(contract, dict):
|
|
577
|
+
# 2026-07-06 真机误报:design 常把真实图片契约声明在**前景**槽(background_role=
|
|
578
|
+
# "vi_fallback" 故意留白背景、真实主视觉走 foreground_asset,如产品实物图/客户图放前景
|
|
579
|
+
# 插图而非铺满整页背景),这条检查却只认 bg_srcs(背景层的图),把"图在前景不在背景"的
|
|
580
|
+
# 合法设计判成"没有真实图片"。contract 存在时(不论声明有资产还是两槽都显式留空)都是
|
|
581
|
+
# 设计阶段的明确表态,一律采信、不再用 bg_srcs 猜;只有 contract 整个缺失(旧版
|
|
582
|
+
# design.json 没这个字段)才退回这条基于 bg_srcs 的粗糙检查。
|
|
583
|
+
errors.append("影像叙事页没有真实背景图片,只靠 CSS 背景/渐变不足以成为产品详情页主视觉")
|
|
584
|
+
if OBJECT_FILL_RE.search(section):
|
|
585
|
+
errors.append("发现 object-fit:fill,图片可能被非等比拉伸")
|
|
586
|
+
if _any_image_framed(section):
|
|
587
|
+
errors.append("图片疑似被白底/边框/padding 裱成小卡,产品详情页式图槽应直接满铺")
|
|
588
|
+
if strict:
|
|
589
|
+
for src in [s for s in all_srcs if s.startswith("enhanced_src_")]:
|
|
590
|
+
if _source_enhanced_framed(section, src):
|
|
591
|
+
errors.append("source-enhanced 客户图被白底/边框/padding 包裹,不能作为产品详情页主视觉")
|
|
592
|
+
break
|
|
593
|
+
|
|
594
|
+
if grammar == "image-mosaic":
|
|
595
|
+
if img_count < 3:
|
|
596
|
+
errors.append("image-mosaic 至少需要 3 张图片作为主体")
|
|
597
|
+
# 2026-07-06/07 两次真机复盘同一结论:这条"网格必须以图片为主体"检查此前跟 img_count
|
|
598
|
+
# 门槛无关,只要 card_count>=3 且 card_count>img_count 就报——但两次生成的真实截图都是
|
|
599
|
+
# "3 张真实驾驶舱照片图集 + 4-5 张解释每个痛点的文字卡片",图集本身已充分满足"图片为
|
|
600
|
+
# 主体"的证据要求,卡片是**独立的补充说明区**,不是把图片"换成"了文字卡片墙。只有真的
|
|
601
|
+
# 没凑够 3 张图(第一条 img_count<3 已报)时,大量卡片才说明"图片被文字卡片顶替",
|
|
602
|
+
# 嵌套进这个分支后不再误伤"图集+说明"这种合理混合版式。
|
|
603
|
+
if card_count >= 3 and img_count < card_count:
|
|
604
|
+
errors.append("image-mosaic 的网格必须以图片为主体,不能是文字卡片网格")
|
|
605
|
+
# 文字预算从 180 放宽到 280:两次真机复盘的"图集+说明"版式实测 201/242 字符,视觉效果良好,
|
|
606
|
+
# 180 对这类合理混合版式过严;仍保留上限防止真正退化成纯文字墙。
|
|
607
|
+
if len(text) > 280:
|
|
608
|
+
errors.append(f"image-mosaic 文字过多: {len(text)} 字符,应只保留短标签")
|
|
609
|
+
elif card_count >= 4 and style not in {"gallery-strip", "data-visual"}:
|
|
610
|
+
errors.append(f"疑似卡片墙: card/panel 类出现 {card_count} 次")
|
|
611
|
+
elif card_count >= 2 and style in IMAGE_LED_STYLES:
|
|
612
|
+
errors.append(f"影像叙事页退回卡片/白卡分区: card/panel 类出现 {card_count} 次")
|
|
613
|
+
if style in {"quote-focus", "chapter-cinematic"} and card_count:
|
|
614
|
+
errors.append("quote-focus / chapter-cinematic 只能是大图大字和少量参数,不能出现卡片总结")
|
|
615
|
+
|
|
616
|
+
if table_count and style != "data-visual" and grammar != "data-visual":
|
|
617
|
+
errors.append("疑似表格/矩阵墙,但 composition_style 不是 data-visual")
|
|
618
|
+
if (style == "data-visual" or grammar == "data-visual") and not has_strong_data_visual:
|
|
619
|
+
errors.append("data-visual 页必须包含 SVG/chart/axis/bar/line 等可编辑图形化表达,不能只放表格或文字")
|
|
620
|
+
if (style == "data-visual" or grammar == "data-visual") and card_count >= 3 and not has_strong_data_visual:
|
|
621
|
+
errors.append("data-visual 页不能用多张文字卡片替代真正的图表/参数轴")
|
|
622
|
+
if len(text) > 260 and style not in {"data-visual", "gallery-strip"}:
|
|
623
|
+
warnings.append(f"文字量偏高: {len(text)} 字符,建议图形化或拆页")
|
|
624
|
+
# 知情反转(2026-07-02 逐页体检):字数预算从 ERROR(×2) 降为 WARNING(×3)——预算是设计期 LLM 拍的
|
|
625
|
+
# 紧值(50-80),把时间轴标签/图内短标全算进去,视觉良好页(06 四失/20 风险)反被判「文字墙」。
|
|
626
|
+
# 文字过多的**后果**(越界/重叠/挤压)由几何门按坐标精确抓,字数只作精炼提示。
|
|
627
|
+
if isinstance(max_words, int) and len(text) > max_words * 3 and style not in {"gallery-strip"}:
|
|
628
|
+
warnings.append(f"实际文字量 {len(text)} 明显超过设计预算 max_visible_words={max_words},建议精炼/图形化")
|
|
629
|
+
if img_count == 0 and style in {"product-hero", "feature-theater", "detail-closeup", "scenario-fullbleed"} and not contract_no_image_at_all:
|
|
630
|
+
errors.append(f"{style} 需要图片/场景主视觉,但页面没有 img")
|
|
631
|
+
if strict and style in IMAGE_LED_STYLES and not (bg_srcs or fg_srcs) and not contract_no_image_at_all:
|
|
632
|
+
errors.append(f"{style} 必须有真实 AI/客户图主视觉,不能只用渐变、色块或卡片补救")
|
|
633
|
+
if grammar in {"hero-scene", "cinematic-feature", "product-closeup", "before-after-scene", "spec-overlay"} and img_count == 0 and not contract_no_image_at_all:
|
|
634
|
+
errors.append(f"{grammar} 需要产品/场景/特写主视觉,但页面没有 img")
|
|
635
|
+
if grammar in {"hero-scene", "cinematic-feature", "product-closeup", "spec-overlay"} and image_frame_count == 0:
|
|
636
|
+
warnings.append(f"{grammar} 建议使用 fig-fill/product-visual/closeup 等明确图槽类,便于检查图像主导")
|
|
637
|
+
if grammar == "before-after-scene" and card_count >= 2:
|
|
638
|
+
errors.append("before-after-scene 应使用左右大图/大场景对比,不能用 before/after 卡片组")
|
|
639
|
+
if (re.search(r"底部|前景|特写|插图|场景|锚点|产品", supporting_visual) and foreground_img_count == 0
|
|
640
|
+
and not has_strong_data_visual and not contract_no_fg):
|
|
641
|
+
errors.append("design 要求辅助视觉层,但 HTML 只有背景/文字,没有前景插图、特写或数据视觉")
|
|
642
|
+
if not style:
|
|
643
|
+
errors.append("design.json 缺少 composition_style,无法验证构图落地")
|
|
644
|
+
if not grammar:
|
|
645
|
+
errors.append("design.json 缺少 page_grammar,无法验证 Apple/DJI 式页面语法")
|
|
646
|
+
|
|
647
|
+
return label, errors, warnings
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
# 2026-07-06 真机复盘:`.deck-title on-dark`/`.deck-subtitle on-dark` 这类组合在深色背景页(scrim-top-dark
|
|
651
|
+
# 等)里标题/副标题实际渲染成深墨字直接压在照片上,肉眼截图确认几乎不可读。根因是样式表里只给
|
|
652
|
+
# `.t-eyebrow`/`.t-takeaway`/`.t-subtitle`/`.t-body`/`.chip`/`.metric-num`/`.metric-label` 定义了
|
|
653
|
+
# `.on-dark`/`.on-light` 修饰符组合,但 HTML 用了 `class="deck-title on-dark"`,样式表压根没有
|
|
654
|
+
# `.deck-title.on-dark` 这条规则——修饰符对这个元素零效果,不是"被更高特异性覆盖"(那是另一种已知
|
|
655
|
+
# 模式),是"这个组合从未被定义过"。跟改哪个具体组件名字无关,是"on-dark/on-light 工具类只对部分
|
|
656
|
+
# 组件类实现、忘了其余实际用到的组件类"这一类通用缺口,用确定性扫描全量捕获比逐条记文案更可靠。
|
|
657
|
+
ON_DARK_LIGHT_MODIFIER_RE = re.compile(r"^(?:on-dark|on-light)$")
|
|
658
|
+
COMPOUND_MODIFIER_SELECTOR_RE = re.compile(r"\.([A-Za-z][\w-]*)\.(on-dark|on-light)\b")
|
|
659
|
+
CLASS_ATTR_RE = re.compile(r"class=[\"']([^\"']+)[\"']", re.I)
|
|
660
|
+
STYLE_BLOCK_RE = re.compile(r"<style\b[^>]*>[\s\S]*?</style>", re.I)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
def _check_on_dark_light_gaps(html: str) -> list[str]:
|
|
664
|
+
style_m = re.search(r"<style\b[^>]*>([\s\S]*?)</style>", html, re.I)
|
|
665
|
+
if not style_m:
|
|
666
|
+
return []
|
|
667
|
+
defined = {(m.group(1), m.group(2)) for m in COMPOUND_MODIFIER_SELECTOR_RE.finditer(style_m.group(1))}
|
|
668
|
+
if not defined:
|
|
669
|
+
return [] # 这份 deck 没用 on-dark/on-light 工具类约定,fail-open 不判
|
|
670
|
+
body = SPEAKER_NOTES_RE.sub("", STYLE_BLOCK_RE.sub("", html))
|
|
671
|
+
errors: list[str] = []
|
|
672
|
+
seen: set[tuple] = set()
|
|
673
|
+
for m in CLASS_ATTR_RE.finditer(body):
|
|
674
|
+
classes = m.group(1).split()
|
|
675
|
+
modifiers = [c for c in classes if ON_DARK_LIGHT_MODIFIER_RE.match(c)]
|
|
676
|
+
if not modifiers:
|
|
677
|
+
continue
|
|
678
|
+
bases = [c for c in classes if not ON_DARK_LIGHT_MODIFIER_RE.match(c)]
|
|
679
|
+
if not bases:
|
|
680
|
+
continue
|
|
681
|
+
for modifier in modifiers:
|
|
682
|
+
if any((b, modifier) in defined for b in bases):
|
|
683
|
+
continue
|
|
684
|
+
key = (tuple(sorted(bases)), modifier)
|
|
685
|
+
if key in seen:
|
|
686
|
+
continue
|
|
687
|
+
seen.add(key)
|
|
688
|
+
errors.append(
|
|
689
|
+
f"class=\"{' '.join(classes)}\" 用了 {modifier} 修饰符,但样式表没有为 "
|
|
690
|
+
f"{'/'.join(bases)} 定义对应的 .{bases[0]}.{modifier} 规则——这个修饰符对它零效果,"
|
|
691
|
+
f"实际颜色仍是该类的默认色(深底深字/浅底浅字风险);补一条 .{bases[0]}.{modifier} 规则,"
|
|
692
|
+
"或改用内联 style 显式定色"
|
|
693
|
+
)
|
|
694
|
+
return errors
|
|
695
|
+
|
|
696
|
+
|
|
697
|
+
def main(argv=None):
|
|
698
|
+
parser = argparse.ArgumentParser()
|
|
699
|
+
parser.add_argument("deck", help="build/deck.html")
|
|
700
|
+
parser.add_argument("--design", help="build/design.json")
|
|
701
|
+
parser.add_argument("--image-report", help="build/image_generation_report.json")
|
|
702
|
+
parser.add_argument("--source-plan", help="build/source_layout_plan.json")
|
|
703
|
+
parser.add_argument("--strict-product-detail", action="store_true", help="启用 Apple/DJI 产品详情页式硬门")
|
|
704
|
+
parser.add_argument("-v", "--verbose", action="store_true")
|
|
705
|
+
args = parser.parse_args(argv)
|
|
706
|
+
|
|
707
|
+
try:
|
|
708
|
+
html = Path(args.deck).read_text(encoding="utf-8", errors="ignore")
|
|
709
|
+
except Exception as exc: # noqa: BLE001
|
|
710
|
+
print(f"DECK_VISUAL_STYLE_FAILED: {exc}")
|
|
711
|
+
return 2
|
|
712
|
+
|
|
713
|
+
sections = SECTION_RE.findall(html)
|
|
714
|
+
if not sections:
|
|
715
|
+
print("DECK_VISUAL_STYLE_FAILED: 未找到 section.slide")
|
|
716
|
+
return 2
|
|
717
|
+
|
|
718
|
+
design = load_design(args.design)
|
|
719
|
+
image_report = load_image_report(args.image_report)
|
|
720
|
+
source_plan = load_source_plan(args.source_plan)
|
|
721
|
+
all_errors: list[str] = []
|
|
722
|
+
all_warnings: list[str] = []
|
|
723
|
+
for idx, section in enumerate(sections, 1):
|
|
724
|
+
label, errors, warnings = check_section(
|
|
725
|
+
section,
|
|
726
|
+
idx,
|
|
727
|
+
design,
|
|
728
|
+
Path(args.deck).parent,
|
|
729
|
+
image_report,
|
|
730
|
+
source_plan,
|
|
731
|
+
args.strict_product_detail,
|
|
732
|
+
)
|
|
733
|
+
all_errors.extend(f"{label}: {e}" for e in errors)
|
|
734
|
+
all_warnings.extend(f"{label}: {w}" for w in warnings)
|
|
735
|
+
|
|
736
|
+
all_errors.extend(f"全局样式表: {e}" for e in _check_on_dark_light_gaps(html))
|
|
737
|
+
|
|
738
|
+
# 系统性同字号归组(2026-07-05 真机复盘:单份 deck 里 10 处 ERROR 共享同一个错误字号 font-size:22px,
|
|
739
|
+
# 分散在 3 个不同页面/不同组件——跟 text_readability_pixel.py 已验证过的「系统性同色归组」同一根因:
|
|
740
|
+
# 逐条改在有限修复轮数内修不完,真正的修法是一次性把这个错误数值批量替换成 ≥24px。纯汇总已收集的
|
|
741
|
+
# errors、不新增判断,只是把「这些其实是一件事」的信号显式标出来)。
|
|
742
|
+
font_size_counts = Counter(
|
|
743
|
+
m.group(1) for e in all_errors for m in [re.search(r"font-size[:=]\"?'?(\d+(?:\.\d+)?)", e)] if m
|
|
744
|
+
)
|
|
745
|
+
for size, count in font_size_counts.most_common():
|
|
746
|
+
if count >= SYSTEMIC_FONT_SIZE_MIN_COUNT:
|
|
747
|
+
all_errors.append(
|
|
748
|
+
f"⚠️ {count} 处过小字体共享同一数值 font-size:{size}px:疑似同一错误字号被多处复用/独立重复选中,"
|
|
749
|
+
f"建议一次性全文搜索替换该数值到 ≥24px(或改用共享字体 token),而非逐条改——"
|
|
750
|
+
f"逐条修在有限轮数内修不完,改一次能同时清零全部同数值实例。"
|
|
751
|
+
)
|
|
752
|
+
|
|
753
|
+
for item in all_errors:
|
|
754
|
+
print(f"ERROR: {item}")
|
|
755
|
+
if args.verbose:
|
|
756
|
+
for item in all_warnings:
|
|
757
|
+
print(f"WARNING: {item}")
|
|
758
|
+
|
|
759
|
+
if all_errors:
|
|
760
|
+
print(f"DECK_VISUAL_STYLE_FAILED: {len(all_errors)} 个阻断问题")
|
|
761
|
+
return 1
|
|
762
|
+
print(f"DECK_VISUAL_STYLE_OK: {len(sections)} 页")
|
|
763
|
+
return 0
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
if __name__ == "__main__":
|
|
767
|
+
raise SystemExit(main())
|