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.
Files changed (484) hide show
  1. package/README.md +14 -12
  2. package/package.json +34 -1
  3. package/plugins/cron-runner/.claude-plugin/plugin.json +5 -0
  4. package/plugins/cron-runner/commands/cron.md +26 -0
  5. package/plugins/cron-runner/commands/crons.md +7 -0
  6. package/plugins/cron-runner/plugin.js +34 -0
  7. package/plugins/cron-runner/src/commands.js +348 -0
  8. package/plugins/cron-runner/src/cronTime.js +148 -0
  9. package/plugins/cron-runner/src/lock.js +68 -0
  10. package/plugins/cron-runner/src/runs.js +89 -0
  11. package/plugins/cron-runner/src/scheduleEngine.js +72 -0
  12. package/plugins/cron-runner/src/scheduler.js +355 -0
  13. package/plugins/cron-runner/src/schema.js +140 -0
  14. package/plugins/cron-runner/src/service.js +167 -0
  15. package/plugins/cron-runner/src/store.js +298 -0
  16. package/plugins/cron-runner/src/types.js +1 -0
  17. package/plugins/workflow-runner/commands/workflow.md +4 -4
  18. package/plugins/workflow-runner/commands/workflows.md +1 -1
  19. package/plugins/workflow-runner/plugin.js +3 -1
  20. package/plugins/workflow-runner/src/agentCommon.js +249 -51
  21. package/plugins/workflow-runner/src/dslManifest.js +72 -0
  22. package/plugins/workflow-runner/src/expressions.js +198 -15
  23. package/plugins/workflow-runner/src/index.js +40 -8
  24. package/plugins/workflow-runner/src/jsonPayload.js +1 -1
  25. package/plugins/workflow-runner/src/loader.js +286 -72
  26. package/plugins/workflow-runner/src/multimodal.js +37 -0
  27. package/plugins/workflow-runner/src/recovery.js +49 -39
  28. package/plugins/workflow-runner/src/runner.js +802 -287
  29. package/plugins/workflow-runner/src/stepExecutors/agent.js +157 -39
  30. package/plugins/workflow-runner/src/stepExecutors/assert.js +16 -3
  31. package/plugins/workflow-runner/src/stepExecutors/llm.js +60 -111
  32. package/plugins/workflow-runner/src/stepExecutors/skill.js +47 -47
  33. package/plugins/workflow-runner/src/stepExecutors/tool.js +76 -10
  34. package/plugins/workflow-runner/src/stepExecutors/verify.js +27 -10
  35. package/plugins/workflow-runner/src/taskIdentity.js +296 -0
  36. package/plugins/workflow-runner/src/types.js +19 -9
  37. package/plugins/workflow-runner/src/workflowState.js +516 -33
  38. package/skills/adversarial-review/SKILL.md +0 -1
  39. package/skills/ai-image/SKILL.md +89 -0
  40. package/skills/ai-image/scripts/generate.mjs +497 -0
  41. package/skills/ai-image/scripts/generate_from_plan.mjs +469 -0
  42. package/skills/ai-image/scripts/generate_from_plan_test.mjs +223 -0
  43. package/skills/ai-image/scripts/generate_test.mjs +74 -0
  44. package/skills/browser-automation/SKILL.md +47 -0
  45. package/skills/docx/SKILL.md +25 -3
  46. package/skills/docx/scripts/office/helpers/__pycache__/__init__.cpython-314.pyc +0 -0
  47. package/skills/docx/scripts/office/helpers/__pycache__/merge_runs.cpython-314.pyc +0 -0
  48. package/skills/docx/scripts/office/helpers/__pycache__/simplify_redlines.cpython-314.pyc +0 -0
  49. package/skills/docx/scripts/office/validators/__pycache__/__init__.cpython-314.pyc +0 -0
  50. package/skills/docx/scripts/office/validators/__pycache__/base.cpython-314.pyc +0 -0
  51. package/skills/docx/scripts/office/validators/__pycache__/docx.cpython-314.pyc +0 -0
  52. package/skills/docx/scripts/office/validators/__pycache__/pptx.cpython-314.pyc +0 -0
  53. package/skills/docx/scripts/office/validators/__pycache__/redlining.cpython-314.pyc +0 -0
  54. package/skills/docx/scripts/opc_check.py +253 -0
  55. package/skills/image-gen-openrouter/SKILL.md +10 -2
  56. package/skills/image-stats/SKILL.md +65 -0
  57. package/skills/image-stats/scripts/__pycache__/image_stats.cpython-314.pyc +0 -0
  58. package/skills/image-stats/scripts/__pycache__/image_stats_test.cpython-314.pyc +0 -0
  59. package/skills/image-stats/scripts/image_stats.py +368 -0
  60. package/skills/image-stats/scripts/image_stats_test.py +181 -0
  61. package/skills/pptx/SKILL.md +123 -95
  62. package/skills/pptx/design.md +82 -0
  63. package/skills/pptx/examples/README.md +29 -0
  64. package/skills/pptx/examples/hybrid_example.mjs +107 -0
  65. package/skills/pptx/examples/hybrid_example.pptx +0 -0
  66. package/skills/pptx/pptxgenjs.md +41 -0
  67. package/skills/pptx/scripts/deck_helpers.mjs +224 -0
  68. package/skills/pptx/scripts/layout_lint.py +293 -0
  69. package/skills/pptx/scripts/layout_lint_test.py +160 -0
  70. package/skills/pptx/scripts/opc_check.py +339 -0
  71. package/skills/pptx/scripts/svg_fallback.mjs +145 -0
  72. package/skills/pptx/svg.md +67 -0
  73. package/skills/tts-zh/SKILL.md +19 -2
  74. package/skills/ui-design/SKILL.md +158 -0
  75. package/skills/ui-design/claude_design.md +309 -0
  76. package/skills/ui-design/evals/evals.json +254 -0
  77. package/skills/ui-design/references/aesthetic-direction.md +83 -0
  78. package/skills/ui-design/references/claude-design-protocol.md +36 -0
  79. package/skills/ui-design/references/design-context.md +53 -0
  80. package/skills/ui-design/references/design-execution-playbook.md +82 -0
  81. package/skills/ui-design/references/export-pptx.md +124 -0
  82. package/skills/ui-design/references/layout-composition.md +654 -0
  83. package/skills/ui-design/references/preview-feedback.md +37 -0
  84. package/skills/ui-design/references/source-image-layout.md +189 -0
  85. package/skills/ui-design/references/starter-components.md +78 -0
  86. package/skills/ui-design/references/vi-aegis-vector.md +144 -0
  87. package/skills/ui-design/references/vi-amber-prism.md +133 -0
  88. package/skills/ui-design/references/vi-aqua-glass.md +200 -0
  89. package/skills/ui-design/references/vi-comfort-vision.md +147 -0
  90. package/skills/ui-design/references/vi-ember-prism.md +140 -0
  91. package/skills/ui-design/references/vi-nebula-console.md +151 -0
  92. package/skills/ui-design/references/vi-obsidian-optics.md +139 -0
  93. package/skills/ui-design/references/vi-obsidian-xui.md +143 -0
  94. package/skills/ui-design/references/vi-precision-horizon-dark.md +117 -0
  95. package/skills/ui-design/references/vi-precision-horizon.md +215 -0
  96. package/skills/ui-design/references/vi-solar-relay.md +130 -0
  97. package/skills/ui-design/references/vi-template-workflow.md +48 -0
  98. package/skills/ui-design/references/vi-verdant-grid.md +141 -0
  99. package/skills/ui-design/references/vi-violet-meridian.md +172 -0
  100. package/skills/ui-design/scripts/__pycache__/assemble_material.cpython-314.pyc +0 -0
  101. package/skills/ui-design/scripts/__pycache__/assemble_material_test.cpython-314.pyc +0 -0
  102. package/skills/ui-design/scripts/__pycache__/chart_sanity_check.cpython-314.pyc +0 -0
  103. package/skills/ui-design/scripts/__pycache__/check_language_policy.cpython-314.pyc +0 -0
  104. package/skills/ui-design/scripts/__pycache__/check_ui_artifact.cpython-314.pyc +0 -0
  105. package/skills/ui-design/scripts/__pycache__/check_ui_artifact_pptx_test.cpython-314.pyc +0 -0
  106. package/skills/ui-design/scripts/__pycache__/deck_pages_check.cpython-314.pyc +0 -0
  107. package/skills/ui-design/scripts/__pycache__/deck_pages_check_test.cpython-314.pyc +0 -0
  108. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check.cpython-314.pyc +0 -0
  109. package/skills/ui-design/scripts/__pycache__/deck_visual_style_check_test.cpython-314.pyc +0 -0
  110. package/skills/ui-design/scripts/__pycache__/design_budget_check.cpython-314.pyc +0 -0
  111. package/skills/ui-design/scripts/__pycache__/design_budget_check_test.cpython-314.pyc +0 -0
  112. package/skills/ui-design/scripts/__pycache__/design_repair_hints.cpython-314.pyc +0 -0
  113. package/skills/ui-design/scripts/__pycache__/design_repair_hints_test.cpython-314.pyc +0 -0
  114. package/skills/ui-design/scripts/__pycache__/design_style_check.cpython-314.pyc +0 -0
  115. package/skills/ui-design/scripts/__pycache__/design_style_check_test.cpython-314.pyc +0 -0
  116. package/skills/ui-design/scripts/__pycache__/draft_coverage_check.cpython-314.pyc +0 -0
  117. package/skills/ui-design/scripts/__pycache__/extract_source_assets.cpython-314.pyc +0 -0
  118. package/skills/ui-design/scripts/__pycache__/extract_source_assets_test.cpython-314.pyc +0 -0
  119. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect.cpython-314.pyc +0 -0
  120. package/skills/ui-design/scripts/__pycache__/generated_image_review_collect_test.cpython-314.pyc +0 -0
  121. package/skills/ui-design/scripts/__pycache__/image_plan_lint.cpython-314.pyc +0 -0
  122. package/skills/ui-design/scripts/__pycache__/image_plan_lint_test.cpython-314.pyc +0 -0
  123. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite.cpython-314.pyc +0 -0
  124. package/skills/ui-design/scripts/__pycache__/image_prompt_rewrite_test.cpython-314.pyc +0 -0
  125. package/skills/ui-design/scripts/__pycache__/layout_balance.cpython-314.pyc +0 -0
  126. package/skills/ui-design/scripts/__pycache__/layout_balance_test.cpython-314.pyc +0 -0
  127. package/skills/ui-design/scripts/__pycache__/normalize_design_json.cpython-314.pyc +0 -0
  128. package/skills/ui-design/scripts/__pycache__/normalize_design_json_test.cpython-314.pyc +0 -0
  129. package/skills/ui-design/scripts/__pycache__/plan_source_layout.cpython-314.pyc +0 -0
  130. package/skills/ui-design/scripts/__pycache__/plan_source_layout_test.cpython-314.pyc +0 -0
  131. package/skills/ui-design/scripts/__pycache__/resolve_source_hint.cpython-314.pyc +0 -0
  132. package/skills/ui-design/scripts/__pycache__/resolve_source_hint_test.cpython-314.pyc +0 -0
  133. package/skills/ui-design/scripts/__pycache__/resolve_source_input.cpython-314.pyc +0 -0
  134. package/skills/ui-design/scripts/__pycache__/resolve_source_input_test.cpython-314.pyc +0 -0
  135. package/skills/ui-design/scripts/__pycache__/scaffold_ui_artifact.cpython-314.pyc +0 -0
  136. package/skills/ui-design/scripts/__pycache__/source_usage_check.cpython-314.pyc +0 -0
  137. package/skills/ui-design/scripts/__pycache__/source_usage_check_test.cpython-314.pyc +0 -0
  138. package/skills/ui-design/scripts/__pycache__/text_readability_pixel.cpython-314.pyc +0 -0
  139. package/skills/ui-design/scripts/__pycache__/visual_quality_gate.cpython-314.pyc +0 -0
  140. package/skills/ui-design/scripts/__pycache__/visual_quality_gate_test.cpython-314.pyc +0 -0
  141. package/skills/ui-design/scripts/assemble_material.py +217 -0
  142. package/skills/ui-design/scripts/assemble_material_test.py +149 -0
  143. package/skills/ui-design/scripts/browser_check_ui_artifact.mjs +221 -0
  144. package/skills/ui-design/scripts/chart_sanity_check.py +261 -0
  145. package/skills/ui-design/scripts/chart_sanity_check_test.py +165 -0
  146. package/skills/ui-design/scripts/check_language_policy.py +351 -0
  147. package/skills/ui-design/scripts/check_ui_artifact.py +435 -0
  148. package/skills/ui-design/scripts/check_ui_artifact_pptx_test.py +251 -0
  149. package/skills/ui-design/scripts/deck_pages_check.py +158 -0
  150. package/skills/ui-design/scripts/deck_pages_check_test.py +134 -0
  151. package/skills/ui-design/scripts/deck_visual_style_check.py +767 -0
  152. package/skills/ui-design/scripts/deck_visual_style_check_test.py +713 -0
  153. package/skills/ui-design/scripts/design_budget_check.py +271 -0
  154. package/skills/ui-design/scripts/design_budget_check_test.py +183 -0
  155. package/skills/ui-design/scripts/design_repair_hints.py +162 -0
  156. package/skills/ui-design/scripts/design_repair_hints_test.py +54 -0
  157. package/skills/ui-design/scripts/design_style_check.py +397 -0
  158. package/skills/ui-design/scripts/design_style_check_test.py +233 -0
  159. package/skills/ui-design/scripts/draft_coverage_check.py +184 -0
  160. package/skills/ui-design/scripts/draft_coverage_check_test.py +131 -0
  161. package/skills/ui-design/scripts/export_pptx_from_html.mjs +2367 -0
  162. package/skills/ui-design/scripts/extract_source_assets.py +607 -0
  163. package/skills/ui-design/scripts/extract_source_assets_test.py +256 -0
  164. package/skills/ui-design/scripts/generated_image_review_collect.py +206 -0
  165. package/skills/ui-design/scripts/generated_image_review_collect_test.py +91 -0
  166. package/skills/ui-design/scripts/image_plan_lint.py +571 -0
  167. package/skills/ui-design/scripts/image_plan_lint_test.py +515 -0
  168. package/skills/ui-design/scripts/image_prompt_rewrite.py +713 -0
  169. package/skills/ui-design/scripts/image_prompt_rewrite_test.py +966 -0
  170. package/skills/ui-design/scripts/layout_balance.py +881 -0
  171. package/skills/ui-design/scripts/layout_balance_test.py +927 -0
  172. package/skills/ui-design/scripts/normalize_design_json.py +272 -0
  173. package/skills/ui-design/scripts/normalize_design_json_test.py +119 -0
  174. package/skills/ui-design/scripts/plan_source_layout.py +1450 -0
  175. package/skills/ui-design/scripts/plan_source_layout_test.py +1136 -0
  176. package/skills/ui-design/scripts/resolve_source_input.py +270 -0
  177. package/skills/ui-design/scripts/resolve_source_input_test.py +98 -0
  178. package/skills/ui-design/scripts/scaffold_ui_artifact.py +377 -0
  179. package/skills/ui-design/scripts/source_usage_check.py +581 -0
  180. package/skills/ui-design/scripts/source_usage_check_test.py +427 -0
  181. package/skills/ui-design/scripts/text_readability_pixel.py +340 -0
  182. package/skills/ui-design/scripts/text_readability_pixel_test.py +219 -0
  183. package/skills/ui-design/scripts/visual_quality_gate.py +301 -0
  184. package/skills/ui-design/scripts/visual_quality_gate_test.py +233 -0
  185. package/skills/ui-design/tests/e2e/prompts/deck.md +19 -0
  186. package/skills/ui-design/tests/e2e/prompts/webpage.md +17 -0
  187. package/skills/ui-design/tests/e2e/render_check.mjs +101 -0
  188. package/skills/ui-design/tests/e2e/rubric.json +54 -0
  189. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/browser-check.json +49 -0
  190. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/language-check.txt +12 -0
  191. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/checks/static-check.txt +15 -0
  192. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/outputs/index.html +837 -0
  193. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/prompt.md +12 -0
  194. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/review.md +21 -0
  195. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/desktop.png +0 -0
  196. package/skills/ui-design/tests/e2e/runs/aegis-vector-preview-1/screenshots/mobile.png +0 -0
  197. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/homepage.html +373 -0
  198. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/outputs/platform.html +378 -0
  199. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/prompt.md +13 -0
  200. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/review.md +26 -0
  201. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  202. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  203. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  204. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  205. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  206. package/skills/ui-design/tests/e2e/runs/amber-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  207. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/checks/static-check.txt +2 -0
  208. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/outputs/index.html +656 -0
  209. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/prompt.md +12 -0
  210. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-1/render_check.mjs +67 -0
  211. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/checks/static-check.txt +2 -0
  212. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/outputs/index.html +587 -0
  213. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/prompt.md +12 -0
  214. package/skills/ui-design/tests/e2e/runs/aqua-glass-iteration-2/review.md +36 -0
  215. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/browser-check.json +49 -0
  216. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/language-check.txt +8 -0
  217. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/checks/static-check.txt +16 -0
  218. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/outputs/index.html +739 -0
  219. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/prompt.md +11 -0
  220. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/review.md +22 -0
  221. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/desktop.png +0 -0
  222. package/skills/ui-design/tests/e2e/runs/aqua-glass-volcano-refresh-1/screenshots/mobile.png +0 -0
  223. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/checks/browser-check.json +52 -0
  224. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/outputs/index.html +641 -0
  225. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/desktop.png +0 -0
  226. package/skills/ui-design/tests/e2e/runs/comfort-vision-preview-1/screenshots/mobile.png +0 -0
  227. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/outputs/index.html +467 -0
  228. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/browser-check.json +49 -0
  229. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/desktop.png +0 -0
  230. package/skills/ui-design/tests/e2e/runs/ember-prism-red-preview-1/screenshots/mobile.png +0 -0
  231. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/homepage.html +373 -0
  232. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/outputs/platform.html +378 -0
  233. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/prompt.md +13 -0
  234. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/review.md +26 -0
  235. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/browser-check.json +49 -0
  236. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/desktop.png +0 -0
  237. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/homepage/mobile.png +0 -0
  238. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/browser-check.json +49 -0
  239. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/desktop.png +0 -0
  240. package/skills/ui-design/tests/e2e/runs/ember-prism-screenshot-check-1/screenshots/platform/mobile.png +0 -0
  241. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/browser-check.json +49 -0
  242. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/language-check.txt +4 -0
  243. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/checks/static-check.txt +2 -0
  244. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/execution_notes.md +10 -0
  245. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/outputs/index.html +587 -0
  246. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/prompt.md +14 -0
  247. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/review.md +13 -0
  248. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/desktop.png +0 -0
  249. package/skills/ui-design/tests/e2e/runs/empty-agent-protocol-check/screenshots/mobile.png +0 -0
  250. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/browser-check.json +24 -0
  251. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/checks/static-check.txt +2 -0
  252. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/execution_notes.md +21 -0
  253. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/outputs/deck.html +451 -0
  254. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-01.png +0 -0
  255. package/skills/ui-design/tests/e2e/runs/iteration-1/deck/screenshots/slide-02.png +0 -0
  256. package/skills/ui-design/tests/e2e/runs/iteration-1/review.md +79 -0
  257. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/browser-check.json +26 -0
  258. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/checks/static-check.txt +2 -0
  259. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/execution_notes.md +20 -0
  260. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/outputs/index.html +534 -0
  261. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/desktop.png +0 -0
  262. package/skills/ui-design/tests/e2e/runs/iteration-1/webpage/screenshots/mobile.png +0 -0
  263. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/browser-check.json +64 -0
  264. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/language-check.txt +7 -0
  265. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/quick-validate.txt +1 -0
  266. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-console.txt +2 -0
  267. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/checks/static-homepage.txt +2 -0
  268. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/console.html +600 -0
  269. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/outputs/homepage.html +556 -0
  270. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/prompt.md +29 -0
  271. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/review.md +57 -0
  272. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-desktop.png +0 -0
  273. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/console-mobile.png +0 -0
  274. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-desktop.png +0 -0
  275. package/skills/ui-design/tests/e2e/runs/nebula-console-iteration-1/screenshots/homepage-mobile.png +0 -0
  276. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/browser-check.json +33 -0
  277. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/language-check.txt +6 -0
  278. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/quick-validate.txt +1 -0
  279. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/checks/static-check.txt +2 -0
  280. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/outputs/index.html +682 -0
  281. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/prompt.md +13 -0
  282. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/review.md +59 -0
  283. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/desktop.png +0 -0
  284. package/skills/ui-design/tests/e2e/runs/obsidian-optics-iteration-1/screenshots/mobile.png +0 -0
  285. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-01.html +466 -0
  286. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/outputs/page-02.html +433 -0
  287. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/prompt.md +13 -0
  288. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/browser-check.json +49 -0
  289. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/desktop.png +0 -0
  290. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-01/mobile.png +0 -0
  291. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/browser-check.json +49 -0
  292. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/desktop.png +0 -0
  293. package/skills/ui-design/tests/e2e/runs/obsidian-xui-screenshot-check-1/screenshots/page-02/mobile.png +0 -0
  294. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/browser-check.json +49 -0
  295. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/language-check.txt +3 -0
  296. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/checks/static-check.txt +2 -0
  297. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/execution_notes.md +24 -0
  298. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/outputs/index.html +723 -0
  299. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/prompt.md +15 -0
  300. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/review.md +26 -0
  301. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/desktop.png +0 -0
  302. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-1/screenshots/mobile.png +0 -0
  303. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-dark.json +49 -0
  304. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/browser-light.json +49 -0
  305. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/language-check.txt +11 -0
  306. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-dark.txt +2 -0
  307. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/checks/static-light.txt +3 -0
  308. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/execution_notes.md +23 -0
  309. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/dark.html +392 -0
  310. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/outputs/light.html +476 -0
  311. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/prompt.md +23 -0
  312. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/review.md +41 -0
  313. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/desktop.png +0 -0
  314. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/dark/mobile.png +0 -0
  315. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/desktop.png +0 -0
  316. package/skills/ui-design/tests/e2e/runs/precision-horizon-iteration-2/screenshots/light/mobile.png +0 -0
  317. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/browser-check.json +54 -0
  318. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/language-check.txt +5 -0
  319. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/quick-validate.txt +1 -0
  320. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/checks/static-check.txt +2 -0
  321. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/outputs/index.html +765 -0
  322. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/prompt.md +15 -0
  323. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/review.md +59 -0
  324. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/desktop.png +0 -0
  325. package/skills/ui-design/tests/e2e/runs/solar-relay-iteration-1/screenshots/mobile.png +0 -0
  326. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/outputs/index.html +508 -0
  327. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/browser-check.json +49 -0
  328. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/desktop.png +0 -0
  329. package/skills/ui-design/tests/e2e/runs/verdant-grid-preview-1/screenshots/mobile.png +0 -0
  330. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/browser-check.json +54 -0
  331. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/language-check.txt +6 -0
  332. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/quick-validate.txt +1 -0
  333. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/checks/static-check.txt +2 -0
  334. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/outputs/index.html +788 -0
  335. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/prompt.md +14 -0
  336. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/review.md +60 -0
  337. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/desktop.png +0 -0
  338. package/skills/ui-design/tests/e2e/runs/violet-meridian-iteration-1/screenshots/mobile.png +0 -0
  339. package/skills/video-compose/SKILL.md +8 -1
  340. package/skills/web-crawler/SKILL.md +70 -0
  341. package/skills/web-crawler/scripts/crawl.mjs +538 -0
  342. package/skills/web-crawler/scripts/crawl.test.mjs +204 -0
  343. package/skills/xlsx/SKILL.md +12 -0
  344. package/skills/xlsx/scripts/opc_check.py +253 -0
  345. package/src/advisorState.js +22 -0
  346. package/src/askBridge.js +97 -3
  347. package/src/bootstrap/workingDir.js +12 -1
  348. package/src/cli/args.js +17 -1
  349. package/src/cli/configMenu.js +236 -0
  350. package/src/cli/configWizard.js +7 -5
  351. package/src/cli/print.js +37 -4
  352. package/src/cli/streamJson.js +77 -24
  353. package/src/cli/wizards/advisorWizard.js +147 -0
  354. package/src/cli/wizards/fieldForm.js +149 -0
  355. package/src/cli/wizards/imageWizard.js +60 -0
  356. package/src/cli/wizards/openRouterWizard.js +23 -0
  357. package/src/cli/wizards/providerPresets.js +95 -0
  358. package/src/cli/wizards/providerWizard.js +219 -0
  359. package/src/cli/wizards/searchWizard.js +23 -0
  360. package/src/cli/wizards/ttsWizard.js +34 -0
  361. package/src/config/configFile.js +441 -83
  362. package/src/config/configService.js +446 -0
  363. package/src/config.js +24 -64
  364. package/src/context/agentHome.js +9 -0
  365. package/src/context/archive.js +7 -2
  366. package/src/context/compact.js +8 -5
  367. package/src/context/contextLock.js +81 -0
  368. package/src/context/persistContext.js +30 -11
  369. package/src/context/reactiveCompact.js +4 -2
  370. package/src/context/recentDirs.js +21 -61
  371. package/src/context/sessionPath.js +97 -23
  372. package/src/context/sessionRegistry.js +303 -24
  373. package/src/context/tokenCounter.js +18 -0
  374. package/src/context/workspaceIndex.js +731 -0
  375. package/src/infra/verification.js +28 -15
  376. package/src/llm/client.js +394 -159
  377. package/src/llm/providers/anthropicFormat.js +241 -0
  378. package/src/llm/providers/openaiFormat.js +190 -0
  379. package/src/loop.js +219 -32
  380. package/src/main.js +61 -11
  381. package/src/messageQueue.js +97 -0
  382. package/src/multimodal.js +203 -0
  383. package/src/plugin-sdk.js +15 -0
  384. package/src/plugins/commandRouter.js +3 -0
  385. package/src/plugins/hookEngine.js +5 -1
  386. package/src/plugins/pluginLifecycle.js +28 -0
  387. package/src/plugins/pluginLoader.js +2 -1
  388. package/src/plugins/pluginRunner.js +49 -21
  389. package/src/plugins/transcript.js +4 -3
  390. package/src/prompts/pluginList.js +5 -5
  391. package/src/prompts/projectInstructions.js +3 -3
  392. package/src/prompts/promptAssembly.js +133 -144
  393. package/src/prompts/skillList.js +12 -22
  394. package/src/prompts/system.js +0 -1
  395. package/src/session/runAgentTurn.js +70 -0
  396. package/src/session/runTurn.js +41 -6
  397. package/src/toolGate.js +17 -0
  398. package/src/tools/advisor/advisor.js +379 -0
  399. package/src/tools/agent/agent.js +555 -0
  400. package/src/tools/agent/agentTypes.js +59 -0
  401. package/src/tools/agent/coordinator.js +93 -0
  402. package/src/tools/agent/worktree.js +270 -0
  403. package/src/tools/ask/ask.js +7 -2
  404. package/src/tools/bash/bash.js +112 -18
  405. package/src/tools/download/download.js +144 -0
  406. package/src/tools/edit/edit.js +1 -0
  407. package/src/tools/edit/multi-edit.js +1 -0
  408. package/src/tools/genimage/genimage.js +242 -0
  409. package/src/tools/genspeech/genspeech.js +67 -0
  410. package/src/tools/glob/glob.js +1 -0
  411. package/src/tools/grep/grep.js +3 -0
  412. package/src/tools/grep/rgPath.js +2 -0
  413. package/src/tools/index.js +25 -2
  414. package/src/tools/read/read.js +1 -0
  415. package/src/tools/shared/killTree.js +52 -0
  416. package/src/tools/toolProgressBridge.js +48 -0
  417. package/src/tools/webbrowser/agentBrowserPath.js +258 -0
  418. package/src/tools/webbrowser/browser.js +59 -42
  419. package/src/tools/webbrowser/browserControl.js +118 -0
  420. package/src/tools/webbrowser/browserDaemon.js +97 -0
  421. package/src/tools/webbrowser/browserService.js +27 -0
  422. package/src/tools/webbrowser/browserStream.js +119 -0
  423. package/src/tools/webbrowser/webbrowser.js +699 -130
  424. package/src/tools/webfetch/webfetch.js +1 -0
  425. package/src/tools/websearch/websearch.js +134 -51
  426. package/src/tools/write/write.js +1 -0
  427. package/src/ui/App.js +249 -22
  428. package/src/ui/AskQuestion.js +29 -10
  429. package/src/ui/InputBox.js +104 -12
  430. package/src/ui/LiveArea.js +112 -19
  431. package/src/ui/MessageList.js +100 -14
  432. package/src/ui/ModelPicker.js +56 -0
  433. package/src/ui/QueuedMessagesBanner.js +14 -0
  434. package/src/ui/Root.js +18 -10
  435. package/src/ui/SessionPicker.js +102 -0
  436. package/src/ui/StatusLine.js +13 -4
  437. package/src/ui/WorkspacePicker.js +55 -0
  438. package/src/ui/figures.js +44 -0
  439. package/src/ui/hooks/askSlot.js +82 -0
  440. package/src/ui/hooks/useChat.js +358 -64
  441. package/src/ui/hooks/usePasteHandler.js +11 -1
  442. package/src/ui/hooks/useTextBuffer.js +1 -3
  443. package/src/ui/liveViewport.js +5 -1
  444. package/src/ui/markdown.js +206 -0
  445. package/src/ui/textBuffer.js +0 -7
  446. package/src/ui/theme.js +32 -0
  447. package/src/utils/debugLog.js +0 -4
  448. package/src/utils/resourcePaths.js +2 -2
  449. package/src/utils/stripThink.js +50 -0
  450. package/src/web/browserBridge.js +447 -0
  451. package/src/web/protocol.js +19 -0
  452. package/workflows/adaptive-research.yaml +13 -5
  453. package/workflows/book-review-short.yaml +36 -10
  454. package/workflows/e2e-write-greet.yaml +6 -2
  455. package/workflows/illustrate.yaml +227 -0
  456. package/workflows/plan.yaml +460 -179
  457. package/workflows/ppt.yaml +1659 -0
  458. package/workflows/ralph-loop.yaml +199 -0
  459. package/workflows/schema.json +81 -23
  460. package/workflows/webppt.yaml +496 -0
  461. package/workflows/word.yaml +710 -0
  462. package/workflows/youtube-shorts.yaml +46 -15
  463. package/MINIMAL-TRACE.md +0 -77
  464. package/plugins/HOW-TO-WRITE-A-PLUGIN.md +0 -186
  465. package/plugins/ralph-wiggum/.claude-plugin/plugin.json +0 -9
  466. package/plugins/ralph-wiggum/README.md +0 -197
  467. package/plugins/ralph-wiggum/commands/cancel-ralph.md +0 -18
  468. package/plugins/ralph-wiggum/commands/help.md +0 -137
  469. package/plugins/ralph-wiggum/commands/ralph-loop.md +0 -85
  470. package/plugins/ralph-wiggum/hooks/hooks.json +0 -15
  471. package/plugins/ralph-wiggum/hooks/stop-hook.sh +0 -191
  472. package/plugins/ralph-wiggum/plugin.js +0 -280
  473. package/plugins/ralph-wiggum/src/goalState.js +0 -258
  474. package/plugins/ralph-wiggum/src/sentinels.js +0 -76
  475. package/plugins/ralph-wiggum/src/stopHookRunner.js +0 -104
  476. package/plugins/ralph-wiggum/src/verificationGate.js +0 -202
  477. package/plugins/workflow-runner/src/miniReAct.js +0 -223
  478. package/plugins/workflow-runner/src/voteHelpers.js +0 -110
  479. package/skills/config/SKILL.md +0 -123
  480. package/skills/frontend-design/LICENSE.txt +0 -177
  481. package/skills/frontend-design/SKILL.md +0 -42
  482. package/src/ui/ProgressPanel.js +0 -54
  483. package/src/ui/StreamingBlock.js +0 -11
  484. package/src/ui/ToolStatus.js +0 -11
@@ -0,0 +1,713 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Rewrite PPT image prompts so generated images stay page-bound.
4
+
5
+ This script is intentionally deterministic. It does not invent design content;
6
+ it binds each image plan item to the current user request, page takeaway,
7
+ composition role, VI tone, and source-image plan. The goal is to prevent generic
8
+ "sleek device / product photography" prompts from producing random monitors,
9
+ controllers, bottles, or other unrelated objects.
10
+
11
+ 生产者-闸门契约(单一词表真相源,治「机器自产自拦」halt)
12
+ ========================================================
13
+ 本脚本是 t2i prompt 的**唯一生产/净化点**,下游 `image_plan_lint.py` 只做兜底验证。
14
+ 因此本脚本必须保证产出**必过** lint:直接 import lint 的黑名单
15
+ (STRUCTURAL/BAD_AESTHETIC/BACKGROUND_TEXT_MARKER/GENERIC_OBJECT_RISK)作为
16
+ `RISKY_TERMS`——lint 加词,这里自动跟上,永不再出现「rewrite 拼进 design 字段的
17
+ 中文词(如页 visual_focus 里的『行业缩略图』、页标题里的『架构/流程』)被 lint
18
+ 拦下 → 整条工作流确定性 halt」的死端(2026-07-01 真机事故)。
19
+ 落点三层:① anchor/标题/takeaway/visual_focus 等**机器拼接段**先剥 RISKY 词;
20
+ ② LLM 原 prompt 命中 RISKY 词 → 强制重组成安全的页面绑定 prompt(= yaml 声明的
21
+ 「结构图回退」语义:结构交 HTML 层,AI 图退化为安全底图,确定性完成不必等人修);
22
+ ③ 最终 prompt 再整体剥一遍作闭环兜底。i2i(客户图增强)提示词不经此净化——
23
+ lint 对 image-to-image 本就豁免,保真意图不同。
24
+ """
25
+
26
+ from __future__ import annotations
27
+
28
+ import argparse
29
+ import json
30
+ import re
31
+ import sys
32
+ from pathlib import Path
33
+ from typing import Any
34
+
35
+ # 同目录直跑(python3 .../image_prompt_rewrite.py)时 sys.path[0] 即脚本目录;
36
+ # 被 importlib/测试等其它方式加载时补一道 __file__ 目录守卫,保证 lint 可 import。
37
+ _SCRIPTS_DIR = str(Path(__file__).resolve().parent)
38
+ if _SCRIPTS_DIR not in sys.path:
39
+ sys.path.insert(0, _SCRIPTS_DIR)
40
+ import image_plan_lint as _lint # noqa: E402
41
+
42
+ # lint 黑名单的并集 = 本脚本禁止在 t2i prompt 里出现的词(单一真相源在 lint)。
43
+ RISKY_TERMS = (
44
+ list(_lint.STRUCTURAL_TERMS)
45
+ + list(_lint.BAD_AESTHETIC_TERMS)
46
+ + list(_lint.BACKGROUND_TEXT_MARKER_TERMS)
47
+ + list(_lint.GENERIC_OBJECT_RISK_TERMS)
48
+ )
49
+
50
+
51
+ def _find_risky(text: str) -> list[str]:
52
+ """text 里非否定式命中的 lint 黑名单词(复用 lint 的否定式判定)。"""
53
+ return _lint._find_non_negated_terms(text, RISKY_TERMS)
54
+
55
+
56
+ def _strip_risky_terms(text: str) -> str:
57
+ """从 text 里剥掉所有非否定式命中的 RISKY 词(用于机器拼接段与最终闭环兜底)。"""
58
+ text = str(text or "")
59
+ guard = 0
60
+ hits = _find_risky(text)
61
+ while hits and guard < 6:
62
+ for term in hits:
63
+ text = re.sub(re.escape(term), " ", text, flags=re.IGNORECASE)
64
+ text = re.sub(r"[ \t]{2,}", " ", text)
65
+ hits = _find_risky(text)
66
+ guard += 1
67
+ return text.strip()
68
+
69
+
70
+ PAGE_FILE_RE = re.compile(r"ai_p(\d+)(?:_fg)?\.(?:png|jpe?g|webp)$", re.IGNORECASE)
71
+
72
+ GENERIC_RISK_TERMS = [
73
+ "professional product photography",
74
+ "sleek device",
75
+ "futuristic screen",
76
+ "generic monitor",
77
+ "consumer electronics",
78
+ "game controller",
79
+ "cosmetic bottle",
80
+ "medicine bottle",
81
+ "shampoo bottle",
82
+ ]
83
+
84
+ BACKGROUND_TEXT_MARKER_RE = re.compile(
85
+ r"(?:章节|序号|编号|号码|数字|标号|标签|题注|文字|文案|1\s*/\s*2\s*/\s*3\s*/\s*4|"
86
+ r"\b(?:number|numbers|numbered|chapter|chapters|label|labels|caption|captions|text|words|letters)\b)",
87
+ re.I,
88
+ )
89
+
90
+ FOREGROUND_STRUCTURE_RE = re.compile(
91
+ r"(?:\d+\s*张|\d+\s*个|\d+\s*行|\d+\s*列|\d+\s*组|横排|竖排|并排|中部|顶部|底部|左侧|右侧|"
92
+ r"大字|主标|文字|文案|takeaway|标签|编号|序号|图表|柱图|柱状|饼图|折线|表格|数据表|架构|流程|对比图|"
93
+ r"时间线|时间轴|箭头|胶囊|节点|连线|\bSVG\b|"
94
+ # 2026-07-02 真机事故(ai_p23):design visual_focus 里的「三组'一'短标」漏过词表 →
95
+ # 生图模型照画三条带字药丸横在背景正中、与 HTML 标题叠字。带字元件类词全部拒收。
96
+ r"短标|角标|徽标|字样|清单|列表|药丸|"
97
+ r"comparison chart|chart|diagram|timeline|arrow|arrows|capsules?|nodes?|labels?|text|headline|"
98
+ r"badges?|chips?|pills?|list items?)",
99
+ re.I,
100
+ )
101
+
102
+ NEGATIVE_OBJECTS = (
103
+ "no game controller, no cosmetic bottle, no medicine bottle, no shampoo bottle, "
104
+ "no generic consumer electronics, no random monitor, no keyboard, no UI mockup, "
105
+ "no text, no logo, no letters, no watermark"
106
+ )
107
+
108
+ CHINESE_ANCHOR_RE = re.compile(r"[\u4e00-\u9fff][\u4e00-\u9fffA-Za-z0-9·+\-/]{1,11}")
109
+ ENGLISH_ANCHOR_RE = re.compile(r"\b[A-Za-z][A-Za-z0-9+\-/]{3,}\b")
110
+ ANCHOR_STOPWORDS = {
111
+ "主题", "指令", "用户", "要求", "生成", "做成", "页面", "背景", "图片", "文字", "内容", "设计",
112
+ "视觉", "风格", "中文", "文案", "标题", "素材", "客户", "产品", "场景", "重点", "需要",
113
+ "必须", "用于", "高清", "比例", "信息", "专业", "方案", "系统", "能力", "价值", "结构",
114
+ "用户原始输入与目标", "主题/指令", "文件作为基础", "生成一套专业中文", "专业中文",
115
+ "presentation", "visual", "image", "slide", "title", "core", "message", "premium",
116
+ "product", "detail", "page", "background", "foreground", "style", "clean", "high",
117
+ "resolution", "text", "safe", "zone",
118
+ }
119
+
120
+
121
+ def _load_json(path: Path, default: Any) -> Any:
122
+ if not path.exists():
123
+ return default
124
+ try:
125
+ return json.loads(path.read_text(encoding="utf-8"))
126
+ except Exception:
127
+ return default
128
+
129
+
130
+ def _load_text(path: Path) -> str:
131
+ if not path.exists():
132
+ return ""
133
+ return path.read_text(encoding="utf-8", errors="replace")
134
+
135
+
136
+ def _text_of(value: Any) -> str:
137
+ if value is None:
138
+ return ""
139
+ if isinstance(value, str):
140
+ return value
141
+ if isinstance(value, (int, float, bool)):
142
+ return str(value)
143
+ if isinstance(value, list):
144
+ return " ".join(_text_of(v) for v in value)
145
+ if isinstance(value, dict):
146
+ return " ".join(_text_of(v) for v in value.values())
147
+ return str(value)
148
+
149
+
150
+ def _add_anchor(out: list[str], value: str) -> None:
151
+ value = value.strip(" ,,。.;;::()()[]【】<>《》\"'")
152
+ if len(value) < 2:
153
+ return
154
+ low = value.lower()
155
+ if low in ANCHOR_STOPWORDS:
156
+ return
157
+ if FOREGROUND_STRUCTURE_RE.search(value):
158
+ return
159
+ # 拒收含 lint 黑名单词的 anchor(如「行业缩略图」):拼进 prompt 会被下游闸门拦。
160
+ if _find_risky(value):
161
+ return
162
+ if value not in out:
163
+ out.append(value)
164
+
165
+
166
+ def _extract_anchors(*values: Any, limit: int = 12) -> list[str]:
167
+ text = " ".join(_text_of(v) for v in values)
168
+ anchors: list[str] = []
169
+ for match in CHINESE_ANCHOR_RE.findall(text):
170
+ _add_anchor(anchors, match)
171
+ if len(anchors) >= limit:
172
+ return anchors
173
+ for match in ENGLISH_ANCHOR_RE.findall(text):
174
+ _add_anchor(anchors, match)
175
+ if len(anchors) >= limit:
176
+ return anchors
177
+ return anchors
178
+
179
+
180
+ def _has_anchor(text: str, anchors: list[str]) -> bool:
181
+ hay = text.lower()
182
+ return any(anchor.lower() in hay for anchor in anchors)
183
+
184
+
185
+ def _has_generic_risk(text: str) -> bool:
186
+ hay = text.lower()
187
+ return any(term in hay for term in GENERIC_RISK_TERMS)
188
+
189
+
190
+ def _page_map(design: dict[str, Any]) -> dict[int, dict[str, Any]]:
191
+ pages: dict[int, dict[str, Any]] = {}
192
+ for page in design.get("pages", []) if isinstance(design, dict) else []:
193
+ if not isinstance(page, dict):
194
+ continue
195
+ try:
196
+ pages[int(page.get("page"))] = page
197
+ except Exception:
198
+ continue
199
+ return pages
200
+
201
+
202
+ def _page_no_from_file(file_name: str) -> int | None:
203
+ match = PAGE_FILE_RE.search(file_name)
204
+ if not match:
205
+ return None
206
+ try:
207
+ return int(match.group(1))
208
+ except ValueError:
209
+ return None
210
+
211
+
212
+ def _page_for_item(item: dict[str, Any], pages: dict[int, dict[str, Any]]) -> dict[str, Any] | None:
213
+ try:
214
+ page_no = int(item.get("page"))
215
+ if page_no in pages:
216
+ return pages[page_no]
217
+ except Exception:
218
+ pass
219
+ file_name = str(item.get("file") or "")
220
+ page_no = _page_no_from_file(file_name)
221
+ if page_no in pages:
222
+ return pages[page_no]
223
+ source_file = str(item.get("source_file") or "")
224
+ needles = [v for v in (file_name, source_file) if v]
225
+ if needles:
226
+ for page in pages.values():
227
+ blob = json.dumps(page, ensure_ascii=False)
228
+ if any(n in blob for n in needles):
229
+ return page
230
+ return None
231
+
232
+
233
+ def _deck_meta_anchor(deck_meta: dict[str, Any] | None) -> dict[str, str]:
234
+ """从 design.json 顶层 deck_meta 提取产品外观/客户身份锚,剥净 RISKY 词后返回。
235
+
236
+ 2026-07-07 真机 p9 复盘:页面文案省略主语("拆除 WiFi 模块"不重复"全景相机")时,
237
+ 纯靠该页 title/takeaway 提取的 anchors 完全抓不到"这是客户自己的产品"这层信息,
238
+ t2i 生图凭空画出无关设备(通用电路板/工控盒),而非客户真实产品外观。deck_meta
239
+ 是设计阶段读全部 vision.json 后**只写一次**的深度信息,这里读出来供每页 t2i 复用,
240
+ 不依赖任何单页文案是否显式提及产品名。"""
241
+ deck_meta = deck_meta if isinstance(deck_meta, dict) else {}
242
+ product = _strip_risky_terms(str(deck_meta.get("product_identity") or "")).strip()
243
+ customer = _strip_risky_terms(str(deck_meta.get("customer_context") or "")).strip()
244
+ return {"product_identity": product, "customer_context": customer}
245
+
246
+
247
+ def _page_context(
248
+ page: dict[str, Any] | None,
249
+ user_request: str,
250
+ deck_meta: dict[str, Any] | None = None,
251
+ ) -> dict[str, str]:
252
+ page = page or {}
253
+ title = str(page.get("title") or "")
254
+ takeaway = str(page.get("one_sentence_takeaway") or page.get("key_point") or title)
255
+ visual_focus = str(page.get("visual_focus") or page.get("visual_story") or "")
256
+ grammar = str(page.get("page_grammar") or page.get("composition_style") or "")
257
+ text_zone = str(page.get("safe_text_zone") or page.get("text_overlay_zone") or page.get("text_zone") or "left")
258
+ visual_role = str(page.get("visual_role") or page.get("primary_visual_type") or "")
259
+ vi_style = str(
260
+ page.get("vi")
261
+ or page.get("style")
262
+ or page.get("visual_style")
263
+ or page.get("theme")
264
+ or ""
265
+ )
266
+ anchors = _extract_anchors(
267
+ title,
268
+ takeaway,
269
+ visual_role,
270
+ page.get("visual_story"),
271
+ page.get("semantic_image_prompt"),
272
+ page.get("visual"),
273
+ page.get("blocks"),
274
+ user_request,
275
+ )
276
+ anchor_text = ", ".join(anchors[:8]) or "the user-provided topic and page-specific subject"
277
+ ctx = {
278
+ "title": title,
279
+ "takeaway": takeaway,
280
+ "visual_focus": visual_focus,
281
+ "grammar": grammar,
282
+ "text_zone": text_zone,
283
+ "visual_role": visual_role,
284
+ "vi_style": vi_style,
285
+ "user_request": user_request[:800],
286
+ "anchors": anchor_text,
287
+ }
288
+ ctx.update(_deck_meta_anchor(deck_meta))
289
+ return ctx
290
+
291
+
292
+ # 含数字/金额/百分比的锚点碎片(如「核心总投入约」「3 万元」):文字渲染型生图模型(Seedream 类)
293
+ # 会把它们照画成假图表标注——背景/增强层锚点一律剔除。
294
+ DIGITY_ANCHOR_RE = re.compile(r"[0-90-9%%]|万元|亿|千万")
295
+ # 句子碎片判定:CHINESE_ANCHOR_RE 从 takeaway 整句里切出来的片段常含功能词(的/把/被/约/相当于…),
296
+ # 或顶到 12 字上限(截断即非名词);场景名词锚不该有这些形态。
297
+ SENTENCE_FRAGMENT_RE = re.compile(r"[的了是把被让将约]|相当于|等于|成为|沦为")
298
+ ANCHOR_MAX_NOUN_LEN = 11
299
+
300
+
301
+ def _scene_anchor_text(anchor_text: str) -> str:
302
+ """背景/i2i 层锚点净化:只留场景名词类短语(去数字/金额与句子碎片、限 6 个),空了给通用兜底。
303
+ 页面绑定(anti-generic)靠这些名词,不靠标题/正文原句——原句会被模型当文字画进图里。"""
304
+ kept = []
305
+ for a in (anchor_text or "").split(","):
306
+ a = a.strip()
307
+ if not a or DIGITY_ANCHOR_RE.search(a) or SENTENCE_FRAGMENT_RE.search(a):
308
+ continue
309
+ # 12 字上限截断只发生在 CJK 整句切片上(英文名词天然带词界,不设限)。
310
+ if re.search(r"[一-鿿]", a) and len(a) > ANCHOR_MAX_NOUN_LEN:
311
+ continue
312
+ kept.append(a)
313
+ if len(kept) >= 6:
314
+ break
315
+ return ", ".join(kept) or "the user-provided topic and page-specific scene"
316
+
317
+
318
+ def _style_tone(ctx: dict[str, str]) -> str:
319
+ vi = ctx.get("vi_style") or "the selected VI palette and current user style request"
320
+ return (
321
+ f"premium product-detail page visual, cinematic but restrained, clean lighting, "
322
+ f"high resolution, coherent with {vi}, not decorative clutter"
323
+ )
324
+
325
+
326
+ def _safe_background_focus(text: str) -> str:
327
+ """Background prompts must not ask the image model to draw labels/numbers.
328
+
329
+ Editable PPT text, chapter marks, labels, and numbers belong to HTML/SVG
330
+ foreground layers. If these words leak into a background image prompt, image
331
+ models reliably bake non-editable symbols into the bitmap.
332
+ """
333
+ text = str(text or "").strip()
334
+ if not text:
335
+ return "clean page-relevant atmosphere with clear visual direction"
336
+ if (
337
+ BACKGROUND_TEXT_MARKER_RE.search(text)
338
+ or FOREGROUND_STRUCTURE_RE.search(text)
339
+ or _find_risky(text)
340
+ ):
341
+ return "low-interference complementary background atmosphere with clean negative space, not foreground labels or charts"
342
+ return text
343
+
344
+
345
+ def _is_data_or_html_page(page: dict[str, Any] | None) -> bool:
346
+ if not isinstance(page, dict):
347
+ return False
348
+ text = " ".join(
349
+ str(page.get(k) or "")
350
+ for k in (
351
+ "page_grammar",
352
+ "composition_style",
353
+ "visual_role",
354
+ "primary_visual_type",
355
+ "semantic_image_prompt",
356
+ )
357
+ ).lower()
358
+ return any(term in text for term in ("data-visual", "html-svg", "chart", "axis", "diagram", "架构", "流程", "图表"))
359
+
360
+
361
+ def _product_identity_clause(ctx: dict[str, str]) -> str:
362
+ """deck 级产品外观/客户身份锚——独立于本页文案是否显式提及产品名(见 _deck_meta_anchor)。
363
+ 空值时返回空字符串,不影响没有真实产品照的示意型 deck。"""
364
+ product = ctx.get("product_identity") or ""
365
+ customer = ctx.get("customer_context") or ""
366
+ if not product and not customer:
367
+ return ""
368
+ parts = []
369
+ if product:
370
+ parts.append(
371
+ f"Product visual consistency (this deck's real product — match its actual appearance, "
372
+ f"never invent a different-looking device): {product}."
373
+ )
374
+ if customer:
375
+ parts.append(f"Customer/industry context: {customer}.")
376
+ return " " + " ".join(parts)
377
+
378
+
379
+ def _background_prompt(ctx: dict[str, str], data_page: bool) -> str:
380
+ # 🚫 背景层提示词**绝不携带页标题/正文原句/数字/图表描述**(2026-07-03 ai_p9 真机事故:
381
+ # "Slide title: 投入有限,对冲无限. Core message: 核心总投入约 3 万元… Visual focus: 成本拆解 HTML 表 +
382
+ # 投入/对冲对比柱状图" 被 Seedream 类文字渲染型模型照着画出一整版烘字假幻灯片,与 HTML 真卡互相遮挡)。
383
+ # 页面绑定(anti-generic)只靠去数字化的场景名词锚;表格/图表/标题由 HTML 层承载,背景只管氛围与留白。
384
+ if data_page:
385
+ subject = (
386
+ "low-interference data-space background, subtle material depth, "
387
+ "large clean negative space reserved for the editable HTML/SVG data graphics drawn on top"
388
+ )
389
+ visual_focus = "pure atmosphere only; all data visuals are drawn by the HTML layer, not in this image"
390
+ else:
391
+ subject = (
392
+ "realistic page-specific scene or product environment derived from the slide topic, "
393
+ "visual focus supports the slide message without adding unrelated objects"
394
+ )
395
+ visual_focus = _safe_background_focus(ctx["visual_focus"])
396
+ return (
397
+ f"{subject}. Scene inspiration keywords (mood reference only — never render these words or any writing): "
398
+ f"{_scene_anchor_text(ctx['anchors'])}. Visual focus: {visual_focus}."
399
+ f"{_product_identity_clause(ctx)} "
400
+ f"Text safe zone: {ctx['text_zone']} — keep it as large clean negative space. "
401
+ f"{_style_tone(ctx)}. absolutely no text of any kind, no chinese characters, no digits, "
402
+ f"no charts, no tables, no diagrams, no slide-like layout, {NEGATIVE_OBJECTS}."
403
+ )
404
+
405
+
406
+ def _foreground_prompt(ctx: dict[str, str]) -> str:
407
+ return (
408
+ "single complete page-specific product, object, or scene detail as a foreground cutout, "
409
+ f"matching slide message: {_strip_risky_terms(ctx['takeaway'])}. Topic anchors: {ctx['anchors']}."
410
+ f"{_product_identity_clause(ctx)} "
411
+ "transparent or clean dark background, complete subject, no cropping through the main body, "
412
+ f"{_style_tone(ctx)}. {NEGATIVE_OBJECTS}."
413
+ )
414
+
415
+
416
+ def _rewrite_t2i(
417
+ item: dict[str, Any],
418
+ page: dict[str, Any] | None,
419
+ user_request: str,
420
+ deck_meta: dict[str, Any] | None = None,
421
+ ) -> tuple[str, bool, str]:
422
+ original = str(item.get("prompt") or "")
423
+ ctx = _page_context(page, user_request, deck_meta)
424
+ data_page = _is_data_or_html_page(page)
425
+ role = str(item.get("role") or item.get("target_layer") or ctx["visual_role"]).lower()
426
+ file_name = str(item.get("file") or "").lower()
427
+ is_foreground_file = "_fg." in file_name
428
+ is_background = not is_foreground_file and role not in ("foreground", "fg", "cutout")
429
+
430
+ force_rewrite = False
431
+ reason = []
432
+ if "use the attached image" in original.lower() or "preserve its main subject" in original.lower():
433
+ force_rewrite = True
434
+ reason.append("i2i_prompt_in_t2i")
435
+ anchors = _extract_anchors(
436
+ page,
437
+ item.get("note"),
438
+ item.get("role"),
439
+ item.get("target_layer"),
440
+ user_request,
441
+ )
442
+ if _has_generic_risk(original) and not _has_anchor(original, anchors):
443
+ force_rewrite = True
444
+ reason.append("generic_prompt_without_domain_anchor")
445
+ if not original.strip() or not _has_anchor(original, anchors):
446
+ force_rewrite = True
447
+ reason.append("missing_domain_anchor")
448
+ if is_background and (
449
+ BACKGROUND_TEXT_MARKER_RE.search(original)
450
+ or BACKGROUND_TEXT_MARKER_RE.search(ctx["visual_focus"])
451
+ or FOREGROUND_STRUCTURE_RE.search(original)
452
+ or FOREGROUND_STRUCTURE_RE.search(ctx["visual_focus"])
453
+ ):
454
+ force_rewrite = True
455
+ reason.append("background_text_marker_conflict")
456
+ # 背景 prompt 携带页面文案/图表描述("Slide title:" 等旧模板遗留、或 LLM 自拼)→ 必须重组:
457
+ # 下游 lint 的 slide_copy 检查会拦(生产者产出必过闸门契约);旧 img_plan 复跑也借此收敛到新模板。
458
+ if is_background and _lint._find_slide_copy_terms(original):
459
+ force_rewrite = True
460
+ reason.append("slide_copy_in_background_prompt")
461
+ # 原 prompt 命中 lint 黑名单(结构图/坏审美/背景文字标记/泛消费品)→ 机器重组成
462
+ # 安全的页面绑定 prompt——「结构图回退」的确定性实现:结构交 HTML 层,AI 图退化为安全底图。
463
+ risky = _find_risky(original)
464
+ if risky:
465
+ force_rewrite = True
466
+ reason.append("risky_terms:" + "|".join(str(t) for t in risky[:4]))
467
+
468
+ if not is_background:
469
+ prompt = _foreground_prompt(ctx)
470
+ else:
471
+ prompt = _background_prompt(ctx, data_page)
472
+
473
+ if force_rewrite:
474
+ # 闭环兜底:模板为静态安全文案、动态段已剥 RISKY 词;万一跨段拼接仍构成命中,整体再剥一遍,
475
+ # 保证「rewrite 产出必过 lint」的不变量成立(did_change 按真实差异报,二跑幂等 changed=0)。
476
+ residual = _find_risky(prompt)
477
+ if residual:
478
+ prompt = _strip_risky_terms(prompt)
479
+ return prompt, prompt != original, ",".join(reason)
480
+
481
+ # force_rewrite=False:原 prompt 本身已过审(有锚点/无风险词/无背景文字冲突),但"有锚点"只是指
482
+ # 页面局部词命中,不代表提过客户真实产品外观——该页文案完全可能没提产品名(见本函数头注真机 p9
483
+ # 复盘),产品身份锚必须补齐,不能因为原 prompt "看起来还行" 就放过。幂等:锚文本已存在则不重复拼。
484
+ appended = []
485
+ identity_clause = _product_identity_clause(ctx)
486
+ if identity_clause and identity_clause.strip() not in original:
487
+ original = original.rstrip(" .") + "." + identity_clause
488
+ appended.append("product_identity_anchor")
489
+ if NEGATIVE_OBJECTS.lower() not in original.lower():
490
+ original = original.rstrip(" .") + ". " + NEGATIVE_OBJECTS + "."
491
+ appended.append("append_negative_constraints")
492
+ if appended:
493
+ return original, True, ",".join(appended)
494
+ return original, False, ""
495
+
496
+
497
+ def _source_plan_entry(source_plan: dict[str, Any], source_file: str) -> dict[str, Any]:
498
+ """按 source_file 到 source_layout_plan.json 的 images[] 里找对应条目;找不到返回 {}。"""
499
+ if not source_file:
500
+ return {}
501
+ for im in (source_plan.get("images") or []) if isinstance(source_plan, dict) else []:
502
+ if isinstance(im, dict) and str(im.get("file") or "") == source_file:
503
+ return im
504
+ return {}
505
+
506
+
507
+ def _quote_embedded_text(embedded_text: Any, limit: int = 6) -> str:
508
+ """把 plan_source_layout.py 算好的 embedded_text 列表格式化成可引用的 "'文字'(位置), ..." 短语。
509
+ 与 plan_source_layout.py::_quote_embedded_text 逐字同构(脚本间不耦合,各自拷贝,同一份格式约定)。"""
510
+ parts = []
511
+ for it in (embedded_text or [])[:limit]:
512
+ text = it.get("text") if isinstance(it, dict) else None
513
+ if not text:
514
+ continue
515
+ region = it.get("region") if isinstance(it, dict) else ""
516
+ parts.append(f"'{text}'" + (f" ({region})" if region else ""))
517
+ return ", ".join(parts)
518
+
519
+
520
+ # "看起来像真正的 i2i 指令"特征词——同时出现在 plan_source_layout.py::_enhance_prompt 的
521
+ # _PRESERVE 模板和本文件 Tier 3 自己的 preserve 模板里,保证幂等:上一轮重建的输出、或设计 LLM
522
+ # 微调过的版本,下一轮都能被正确识别为"genuine",不会被误判成需要重建的占位符文本。
523
+ _I2I_SIGNATURE_MARKERS = (
524
+ "attached image",
525
+ "attached photo",
526
+ "reference image",
527
+ "strict visual reference",
528
+ "preserve its main subject",
529
+ "preserve the real main subject",
530
+ )
531
+ _I2I_MIN_LEN = 30 # degenerate-match 防护:真正的 i2i 指令(哪怕被精修过)远长于这个下限
532
+
533
+
534
+ def _looks_like_i2i_prompt(text: str) -> bool:
535
+ """原 prompt 是否已经读起来像真正的 image-to-image 指令(非空、非占位符、非误放的 t2i 文本)。"""
536
+ text = str(text or "")
537
+ if len(text.strip()) < _I2I_MIN_LEN:
538
+ return False
539
+ low = text.lower()
540
+ return any(m in low for m in _I2I_SIGNATURE_MARKERS)
541
+
542
+
543
+ def _has_embedded_text_removal_clause(text: str, embedded_text: Any, limit: int = 6) -> bool:
544
+ """text 是否已经完整包含"删除这些嵌入文字"的指令——要求含"remov"词干(真的是删除指令,
545
+ 不是提到了这段文字)且**每一条** embedded_text 的原文都以精确子串形式出现(LLM 微调的是
546
+ 措辞,不会改写要求它删除的引用文字本身,故不做模糊匹配)。"""
547
+ items = [
548
+ it for it in (embedded_text or [])[:limit]
549
+ if isinstance(it, dict) and str(it.get("text") or "").strip()
550
+ ]
551
+ if not items:
552
+ return False
553
+ if "remov" not in text.lower():
554
+ return False
555
+ return all(str(it["text"]).strip() in text for it in items)
556
+
557
+
558
+ def _append_deltext_clause(original: str, quoted: str) -> str:
559
+ """Tier 2:就地打补丁而非重建——在已有 prompt(可能带设计 LLM 的页面定制文字)末尾追加缺失
560
+ 的去字指令,复用 _quote_embedded_text 的引用格式,不触碰 original 其余部分。"""
561
+ clause = (
562
+ f" Additionally, the original image has this exact baked-in caption/title text that must be "
563
+ f"completely REMOVED, leaving clean empty space in its place: {quoted}. Never reproduce or invent any text."
564
+ )
565
+ return original.rstrip(" .") + "." + clause
566
+
567
+
568
+ def _rewrite_i2i(item: dict[str, Any], source_plan: dict[str, Any], page: dict[str, Any] | None, user_request: str) -> tuple[str, bool, str]:
569
+ original = str(item.get("prompt") or item.get("enhance_prompt") or "")
570
+ intent = str(item.get("enhance_intent") or item.get("enhance_style") or "")
571
+ source_file = str(item.get("source_file") or "")
572
+ plan_entry = _source_plan_entry(source_plan, source_file)
573
+ text_policy = str(plan_entry.get("text_policy") or "")
574
+ embedded_text = plan_entry.get("embedded_text")
575
+ quoted = _quote_embedded_text(embedded_text)
576
+
577
+ # 2026-07-07 架构修复(治"规划算对了、执行没服从"这一大类问题里最隐蔽的一种):这个函数此前
578
+ # 对**每一条** i2i 都无条件从写死的三分支模板重新拼一遍 prompt,从不把 original(设计 LLM
579
+ # 抄自 source_plan.enhance_prompt、且 ppt.yaml 明确授权"按 key_point 微调"过的版本)当作
580
+ # 返回值的基础——只在最后拿 original 跟新 prompt 做字符串比较判断 changed。这意味着设计 LLM
581
+ # 做的任何页面定制、以及 plan_source_layout.py 未来对 enhance_prompt 措辞的任何改进,全部
582
+ # 每轮被无条件推翻重建。i2i 天然对 image_plan_lint 豁免(lint 对 mode=="image-to-image" 处处
583
+ # 门控跳过),信任 original 的自由文本内容没有 lint 失败风险——这解除了 t2i 那种"必须重建成
584
+ # 安全模板"的唯一理由。改为三层信任分级,统一到 _rewrite_t2i 同款"已过审就不动,只在命中
585
+ # 具体检查失败时才动手"的哲学:
586
+ # Tier 1(原样保留):original 已经像真 i2i 指令,且该有的去字引用(若 text_policy 要求)
587
+ # 已完整存在 → 一个字都不改。
588
+ # Tier 2(就地打补丁):original 可用但缺去字指令(= src_6 事故的确切场景:设计 LLM 抄来的
589
+ # 版本本身没问题,只是漏了这一条)→ 追加缺失子句,保留其余全部定制文字。
590
+ # Tier 3(兜底重建,下面的逻辑原样保留):original 缺失/不像 i2i 指令 → 走原有模板重建。
591
+ requires_deltext = bool(quoted) and (text_policy == "remove_all_text" if ("dense" in intent or "faithful" in intent) else True)
592
+ if original.strip() and _looks_like_i2i_prompt(original):
593
+ if not requires_deltext or _has_embedded_text_removal_clause(original, embedded_text):
594
+ return original, False, "kept_as_is"
595
+ return _append_deltext_clause(original, quoted), True, "patched_missing_deltext"
596
+
597
+ # ---- Tier 3:兜底重建(original 缺失/不可信时的安全网,逻辑与此前一致)----
598
+ ctx = _page_context(page, user_request)
599
+ preserve = (
600
+ "Use the attached image as the strict visual reference. Preserve the real main subject and key factual content. "
601
+ )
602
+ if "dense" in intent or "faithful" in intent:
603
+ if text_policy == "remove_all_text" and quoted:
604
+ # 装饰性 chrome 文字(整版截图/海报自带标题/标签,非图表数据):保结构去字,引用实抄文字。
605
+ policy = (
606
+ "Faithfully keep the exact same layout, sub-images, comparison structure and every non-text visual "
607
+ "detail unchanged, and keep the original framing and aspect ratio (do not crop, stretch or outpaint). "
608
+ f"However, the original image also has this exact baked-in caption/title text that must be completely "
609
+ f"REMOVED, leaving clean empty space in its place: {quoted}. Never reproduce or invent any text. "
610
+ "Use contain-friendly composition."
611
+ )
612
+ else:
613
+ policy = (
614
+ "Faithfully restore clarity only; do not rewrite charts, screenshots, numbers, labels, UI structure or data. "
615
+ "Use contain-friendly composition, no invented text."
616
+ )
617
+ elif "cutout" in intent or "remove_background" in intent:
618
+ policy = (
619
+ "Create a clean foreground cutout with transparent or plain background; remove original colored/gradient background, "
620
+ "keep the complete subject, no cropping through devices, no new text or logo."
621
+ )
622
+ if quoted:
623
+ policy += f" The original image has this exact baked-in text that must be completely removed: {quoted}."
624
+ else:
625
+ policy = (
626
+ "Recompose into the target slide ratio with clean negative space for editable PPT text; remove distracting original text, "
627
+ "do not create a collage or thumbnail grid."
628
+ )
629
+ if quoted:
630
+ policy += f" Specifically, this exact baked-in text must be completely removed: {quoted}."
631
+ # i2i 同样不拼标题/正文原句(增强图常被用作 hero/背景层,模型会把原句画成新文字;
632
+ # 内容锚定靠 attached image 本身 + 去数字化场景名词锚即可)。
633
+ prompt = (
634
+ f"{preserve}{policy} Scene inspiration keywords (mood reference only — never paint any new words): "
635
+ f"{_scene_anchor_text(ctx['anchors'])}. Source file: {source_file}. Target visual role: {intent or ctx['visual_role']}. "
636
+ f"Text safe zone: {ctx['text_zone']}. {_style_tone(ctx)}. no new text, no logo, no letters, no watermark."
637
+ )
638
+ if prompt != original:
639
+ return prompt, True, "rebuilt_fallback"
640
+ return original, False, ""
641
+
642
+
643
+ def rewrite_plan(build: Path) -> dict[str, Any]:
644
+ plan_path = build / "img_plan.json"
645
+ plan = _load_json(plan_path, [])
646
+ if not isinstance(plan, list):
647
+ raise ValueError("img_plan.json must be an array")
648
+ design = _load_json(build / "design.json", {})
649
+ source_plan = _load_json(build / "source_layout_plan.json", {})
650
+ user_request = _load_text(build / "user_request.md")
651
+ pages = _page_map(design if isinstance(design, dict) else {})
652
+ deck_meta = design.get("deck_meta") if isinstance(design, dict) else {}
653
+
654
+ changed = []
655
+ i2i_tier_counts = {"kept_as_is": 0, "patched_missing_deltext": 0, "rebuilt_fallback": 0}
656
+ for item in plan:
657
+ if not isinstance(item, dict):
658
+ continue
659
+ file_name = str(item.get("file") or "")
660
+ page = _page_for_item(item, pages)
661
+ mode = str(item.get("mode") or "text-to-image").lower()
662
+ before = str(item.get("prompt") or "")
663
+ if mode == "image-to-image":
664
+ prompt, did_change, reason = _rewrite_i2i(item, source_plan if isinstance(source_plan, dict) else {}, page, user_request)
665
+ if reason in i2i_tier_counts:
666
+ i2i_tier_counts[reason] += 1
667
+ else:
668
+ prompt, did_change, reason = _rewrite_t2i(item, page, user_request, deck_meta)
669
+ item["mode"] = "text-to-image"
670
+ item["prompt"] = prompt
671
+ if did_change:
672
+ changed.append(
673
+ {
674
+ "file": file_name,
675
+ "mode": item.get("mode"),
676
+ "reason": reason,
677
+ "before": before[:240],
678
+ "after": prompt[:360],
679
+ }
680
+ )
681
+
682
+ plan_path.write_text(json.dumps(plan, ensure_ascii=False, indent=2), encoding="utf-8")
683
+ report = {
684
+ "changed_count": len(changed),
685
+ "changed": changed,
686
+ "i2i_tier_counts": i2i_tier_counts,
687
+ }
688
+ (build / "image_prompt_rewrite_report.json").write_text(
689
+ json.dumps(report, ensure_ascii=False, indent=2),
690
+ encoding="utf-8",
691
+ )
692
+ lines = ["# 生图提示词重写报告", "", f"- changed_count: {len(changed)}", ""]
693
+ for entry in changed:
694
+ lines.append(f"## {entry['file']}")
695
+ lines.append(f"- mode: {entry['mode']}")
696
+ lines.append(f"- reason: {entry['reason']}")
697
+ lines.append(f"- after: {entry['after']}")
698
+ lines.append("")
699
+ (build / "image_prompt_rewrite_report.md").write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
700
+ return report
701
+
702
+
703
+ def main() -> int:
704
+ parser = argparse.ArgumentParser(description="Rewrite PPT image prompts with page/domain/style anchors.")
705
+ parser.add_argument("--build", default="./build", help="Build directory")
706
+ args = parser.parse_args()
707
+ report = rewrite_plan(Path(args.build))
708
+ print(f"IMAGE_PROMPT_REWRITE_OK changed={report['changed_count']}")
709
+ return 0
710
+
711
+
712
+ if __name__ == "__main__":
713
+ raise SystemExit(main())