@xiaobuyu/nodesign 0.0.5 → 0.0.7

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 (137) hide show
  1. package/README.md +1 -1
  2. package/README.zh-CN.md +1 -1
  3. package/package.json +1 -1
  4. package/server/api/assets/helpers.js +18 -0
  5. package/server/api/assets/notes.js +50 -0
  6. package/server/api/assets.js +11 -6
  7. package/server/api/instruction.js +3 -3
  8. package/server/engine/agent/events.js +11 -1
  9. package/server/engine/agent/hooks/failure.js +11 -1
  10. package/server/engine/agent/hooks/lifecycle.js +10 -39
  11. package/server/engine/agent/hooks/post-guidance.js +4 -38
  12. package/server/engine/agent/hooks/user-prompt-submit.js +11 -40
  13. package/server/engine/agent/hooks.js +4 -8
  14. package/server/engine/agent/memory-config.js +61 -0
  15. package/server/engine/agent/prompts/nodesign-prelude.md +14 -12
  16. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +24 -7
  17. package/server/engine/agent/prompts/tools/site-reference.md +13 -3
  18. package/server/engine/agent/session-loop.js +29 -18
  19. package/server/engine/agents/explorer.md +1 -1
  20. package/server/engine/agents/index.js +1 -1
  21. package/server/engine/mcp/index.js +4 -11
  22. package/server/engine/mcp/tools/arrange-on-board.js +3 -1
  23. package/server/engine/mcp/tools/browse-computer.js +2 -1
  24. package/server/engine/mcp/tools/browse-find-batch.js +20 -3
  25. package/server/engine/mcp/tools/browse.js +24 -2
  26. package/server/engine/mcp/tools/crystallize-skill.js +1 -1
  27. package/server/engine/mcp/tools/generate-image.js +50 -70
  28. package/server/engine/mcp/tools/helpers/codex-imagegen.js +139 -0
  29. package/server/engine/mcp/tools/helpers/rembg.js +7 -2
  30. package/server/engine/mcp/tools/helpers/shot-pipeline.js +109 -0
  31. package/server/engine/mcp/tools/list-pages.js +11 -5
  32. package/server/engine/mcp/tools/organize-board.js +26 -1
  33. package/server/engine/mcp/tools/pin-to-board.js +26 -17
  34. package/server/engine/mcp/tools/read-board.js +19 -5
  35. package/server/engine/mcp/tools/relate-on-board.js +42 -4
  36. package/server/engine/mcp/tools/report-issue.js +14 -7
  37. package/server/engine/mcp/tools/screenshot.js +17 -7
  38. package/server/engine/mcp/tools/sketch-on-board.js +13 -5
  39. package/server/engine/mcp/tools/web-search.js +11 -15
  40. package/server/engine/mcp/tools/write-on-board.js +15 -6
  41. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +3 -4
  42. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +10 -1
  43. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +16 -0
  44. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +4 -4
  45. package/server/index.js +0 -2
  46. package/server/lib/artifact-target.js +31 -14
  47. package/server/lib/board-kind-sizes.js +2 -3
  48. package/server/lib/canvas-id.js +29 -4
  49. package/server/lib/export-collect.js +3 -1
  50. package/server/lib/issues-store.js +3 -1
  51. package/server/lib/kinds/docx.js +11 -4
  52. package/server/lib/kinds/index.js +6 -16
  53. package/server/lib/kinds/site.js +40 -7
  54. package/server/lib/rewrite-refs.js +110 -0
  55. package/server/lib/ssrf-guard.js +9 -0
  56. package/server/lib/task-scan.js +18 -0
  57. package/server/projects/board-store.js +42 -1
  58. package/server/projects/memory-migration.js +85 -0
  59. package/server/projects/move-entry.js +11 -1
  60. package/server/projects/ui-config.js +2 -1
  61. package/server/projects/workspace-templates.js +13 -10
  62. package/server/projects/workspace.js +15 -15
  63. package/server/services/rembg-launcher.js +5 -2
  64. package/web/dist/assets/{BrowserWindow-P_vIdh-G.js → BrowserWindow-BoOBaIyz.js} +1 -1
  65. package/web/dist/assets/DeckWindow-Bg3rgkvi.js +11 -0
  66. package/web/dist/assets/{DocxWindow-DSAU1-tD.js → DocxWindow-B_u6aX9o.js} +1 -1
  67. package/web/dist/assets/{MermaidBlock-DR1GikCv.js → MermaidBlock-BO-46Du5.js} +6 -6
  68. package/web/dist/assets/{SiteWindow-92uBuRRR.js → SiteWindow-ConPmzdM.js} +1 -1
  69. package/web/dist/assets/{abnfDiagram-VCTEODGH-uA8Esix4.js → abnfDiagram-VCTEODGH-FCO9yf-n.js} +1 -1
  70. package/web/dist/assets/{arc-D_khH_xN.js → arc-BLcEOSRX.js} +1 -1
  71. package/web/dist/assets/{architectureDiagram-5GKGNRK7-DydJHqOt.js → architectureDiagram-5GKGNRK7-kxiX4TeN.js} +1 -1
  72. package/web/dist/assets/{blockDiagram-NRAW4CY4-B7wBbKiW.js → blockDiagram-NRAW4CY4-CptFksix.js} +1 -1
  73. package/web/dist/assets/{c4Diagram-UCG6FXSJ-6mrH0x7Y.js → c4Diagram-UCG6FXSJ-Dxmxj34F.js} +1 -1
  74. package/web/dist/assets/channel-DFyiuGL0.js +1 -0
  75. package/web/dist/assets/{chunk-2Q5K7J3B-Cuikho5Q.js → chunk-2Q5K7J3B-D9inx5oo.js} +1 -1
  76. package/web/dist/assets/{chunk-5VM5RSS4-3-KZ5AUz.js → chunk-5VM5RSS4-DydfHlEc.js} +1 -1
  77. package/web/dist/assets/{chunk-F27PBJKO-Bkwq2gGU.js → chunk-F27PBJKO-BO7GxhSv.js} +1 -1
  78. package/web/dist/assets/{chunk-G27WJ6UU-BLyQzdGE.js → chunk-G27WJ6UU-BzP6YiQ_.js} +1 -1
  79. package/web/dist/assets/{chunk-JWPE2WC7-B7HxxQds.js → chunk-JWPE2WC7-CHcgcd0s.js} +1 -1
  80. package/web/dist/assets/{chunk-LCL6LL3I-CtXrnUwF.js → chunk-LCL6LL3I-D2Wg5JtX.js} +1 -1
  81. package/web/dist/assets/{chunk-POPQ4Y6H-dxddZlXt.js → chunk-POPQ4Y6H-B2fCRfpM.js} +1 -1
  82. package/web/dist/assets/{chunk-SVP7TREG-DpkmrgHn.js → chunk-SVP7TREG-JSLcsRlZ.js} +1 -1
  83. package/web/dist/assets/{chunk-XXDRQBXY-BwZ7RbpQ.js → chunk-XXDRQBXY-BCjvaeHm.js} +1 -1
  84. package/web/dist/assets/classDiagram-DTDB5LWJ-Aw1f8z-b.js +1 -0
  85. package/web/dist/assets/classDiagram-v2-JRS7N3AN-Aw1f8z-b.js +1 -0
  86. package/web/dist/assets/{cose-bilkent-JH36ORCC-DGFOsMUt.js → cose-bilkent-JH36ORCC-0uKv6oJ7.js} +1 -1
  87. package/web/dist/assets/{cynefin-OW5HDTMX-DUwn3ehS.js → cynefin-OW5HDTMX-Bvt-gEmA.js} +1 -1
  88. package/web/dist/assets/{cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js → cynefinDiagram-5FMLGOSQ-C9Ht02qC.js} +1 -1
  89. package/web/dist/assets/{dagre-3AP2YEHR-CyourLfz.js → dagre-3AP2YEHR-BfxZny4s.js} +1 -1
  90. package/web/dist/assets/{diagram-S7CK7UJ4-BTMbx2np.js → diagram-S7CK7UJ4-CrE0ZETE.js} +1 -1
  91. package/web/dist/assets/{diagram-UQ7AKVKN-UK94_1-P.js → diagram-UQ7AKVKN-BO8VQ4Fx.js} +1 -1
  92. package/web/dist/assets/{diagram-VSXAHHWV-C7zS-v9v.js → diagram-VSXAHHWV-CHUZnR9F.js} +1 -1
  93. package/web/dist/assets/{diagram-VX7I27RA-DKJRa6CK.js → diagram-VX7I27RA-DYQAz1Vu.js} +1 -1
  94. package/web/dist/assets/{diagram-Z3DM3KII-kUbAfGAp.js → diagram-Z3DM3KII-BDjivgLr.js} +1 -1
  95. package/web/dist/assets/{ebnfDiagram-PWID7BFC-CVW16_vb.js → ebnfDiagram-PWID7BFC-Bpe_QVbH.js} +1 -1
  96. package/web/dist/assets/{erDiagram-SSCWMZ5O-BRQMFQ5y.js → erDiagram-SSCWMZ5O-BxhwuAu1.js} +1 -1
  97. package/web/dist/assets/{flowDiagram-A5DVABFB-S34LpLtK.js → flowDiagram-A5DVABFB-CCw2H08-.js} +1 -1
  98. package/web/dist/assets/{ganttDiagram-EL5Y4UJY-BFiqk1-G.js → ganttDiagram-EL5Y4UJY-DtTc7MyI.js} +1 -1
  99. package/web/dist/assets/{gitGraphDiagram-WWUBYQGX-LZsrdyyv.js → gitGraphDiagram-WWUBYQGX-BvORxMd2.js} +1 -1
  100. package/web/dist/assets/index-DGG5wIZs.js +2118 -0
  101. package/web/dist/assets/{infoDiagram-RXCK75RN-0DvN6KIp.js → infoDiagram-RXCK75RN-DMcrkypD.js} +1 -1
  102. package/web/dist/assets/{ishikawaDiagram-5VMMS53U-CaY3A-Bx.js → ishikawaDiagram-5VMMS53U-CZkTIbe-.js} +1 -1
  103. package/web/dist/assets/{journeyDiagram-EYS64GPL-BBeiaCrj.js → journeyDiagram-EYS64GPL-DR0XazdE.js} +1 -1
  104. package/web/dist/assets/{kanban-definition-3QL26DDD-ux42DRXQ.js → kanban-definition-3QL26DDD-DN8VbwVu.js} +1 -1
  105. package/web/dist/assets/{layout-CnXiF_NB.js → layout-9_paCbyt.js} +1 -1
  106. package/web/dist/assets/{linear-BMW0u6G6.js → linear-g6Ct012e.js} +1 -1
  107. package/web/dist/assets/{mindmap-definition-FBJOCRG2-hUxSpdKn.js → mindmap-definition-FBJOCRG2-BGt4omYC.js} +1 -1
  108. package/web/dist/assets/{pegDiagram-XKGWAZYB-aCaQPH0w.js → pegDiagram-XKGWAZYB-uSfS-PBE.js} +1 -1
  109. package/web/dist/assets/{pending-edit-apply-AcdOQ9Ij.js → pending-edit-apply-uP4uvrOD.js} +1 -1
  110. package/web/dist/assets/{pieDiagram-E7YTZNPT-CmOEZ8zg.js → pieDiagram-E7YTZNPT-DpeI935H.js} +1 -1
  111. package/web/dist/assets/{quadrantDiagram-AXDQQJYC-VW6fAhsV.js → quadrantDiagram-AXDQQJYC-B7IuNvWa.js} +1 -1
  112. package/web/dist/assets/{railroadDiagram-O6MQD6OU-CKJ7Ba5K.js → railroadDiagram-O6MQD6OU-ARzYpPQI.js} +1 -1
  113. package/web/dist/assets/{requirementDiagram-EFPCY7ZU-pNGkq9Vw.js → requirementDiagram-EFPCY7ZU-Cj4upkAo.js} +1 -1
  114. package/web/dist/assets/{rotate-cw-KAfChbGV.js → rotate-cw-CXJcyHWk.js} +1 -1
  115. package/web/dist/assets/{sankeyDiagram-P5KCCOFB-BIMmDTfZ.js → sankeyDiagram-P5KCCOFB-OaVwSnLP.js} +1 -1
  116. package/web/dist/assets/{sequenceDiagram-WJ2MYXX4-BBW7wzKY.js → sequenceDiagram-WJ2MYXX4-1yrUbq4t.js} +1 -1
  117. package/web/dist/assets/{sizeCapture-X5ZJPWSS-DdkXmLYR.js → sizeCapture-X5ZJPWSS-nYH5p9D6.js} +1 -1
  118. package/web/dist/assets/{square-dashed-mouse-pointer-CDUlhUP7.js → square-dashed-mouse-pointer-CTtTVKoH.js} +1 -1
  119. package/web/dist/assets/{stateDiagram-HBIQ2CUA-W313Uz16.js → stateDiagram-HBIQ2CUA-Dj5HvyNq.js} +1 -1
  120. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-kJ1m_zJT.js +1 -0
  121. package/web/dist/assets/{swimlanes-XN3QIQJK-C29TL26K.js → swimlanes-XN3QIQJK-umeT0HpB.js} +1 -1
  122. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-DPSkMbbl.js +8 -0
  123. package/web/dist/assets/{timeline-definition-24CTP7MA-CF0JMObX.js → timeline-definition-24CTP7MA-MqnQfHKB.js} +1 -1
  124. package/web/dist/assets/{vennDiagram-4TSXK5OY-DrDFsaH3.js → vennDiagram-4TSXK5OY-b2liyZra.js} +1 -1
  125. package/web/dist/assets/{wardleyDiagram-VM6X3IG4-CVrnRIS_.js → wardleyDiagram-VM6X3IG4-QMK-zNh1.js} +1 -1
  126. package/web/dist/assets/{xychartDiagram-S5SC5T6Z-BvkN1zsT.js → xychartDiagram-S5SC5T6Z-D5Mc6tfA.js} +1 -1
  127. package/web/dist/index.html +1 -1
  128. package/server/api/memory.js +0 -185
  129. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +0 -583
  130. package/server/engine/mcp/tools/record-decision.js +0 -140
  131. package/web/dist/assets/DeckWindow-BpLv5082.js +0 -16
  132. package/web/dist/assets/channel-DGkccMO1.js +0 -1
  133. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +0 -1
  134. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +0 -1
  135. package/web/dist/assets/index-GAamh1p4.js +0 -2152
  136. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +0 -1
  137. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +0 -8
@@ -2,7 +2,7 @@
2
2
 
3
3
  本文 append 在 SDK preset `claude_code` 之后,是平台事实与硬约束。设计怎么做由 skill 讲
4
4
  (deck 看 `deskskill-engine-mini`,站点看 `site-craft`,你自己判断何时 `Skill` 加载)。
5
- 工具用法不在这里教:胖工具(生图 / tweaks / DirectEdit 细则 / 技术参考)在你第一次用到时由系统注入。
5
+ 工具用法不在这里教:胖工具(生图 / DirectEdit 细则 / 技术参考)在你第一次用到时由系统注入。
6
6
 
7
7
  ## 不是每次对话都是设计任务
8
8
 
@@ -84,10 +84,10 @@ cwd = 这个项目的工作区,所有路径默认相对 cwd。仓库路径你
84
84
  | `canvas.template.html` | deck 起手模板,Read 后改写(加载 skill 时自动拷进来)。站点**没有**模板,从骨架和风格名自己写 |
85
85
  | `notes/` | 便利贴(见下一节) |
86
86
  | `assets/` | 上传素材 + 生成图;`assets/references/` 是逛站采回来的(palette / fonts / css / skeleton / motion json + 截图,出处在同目录 `.meta/`)。**开工前先看有没有现成的,别重复去搜**;参考图挑关键的一张 Read 看一眼再下笔。每轮开头的状态块首轮列全、之后只报新增 |
87
- | `.claude/agent-memory/` | `memory.md` 用户偏好档(他说"记住…"就 Read Edit 追加,别覆盖)· `brand/memory.md` 风格档案(锚定风格后写:色号 / 字体链 / 版式语言 / 动效预算)· `auto/` 系统自动记的,**不要碰** |
87
+ | `记忆/` | **你的长期记忆**(系统提示里那套 memory 机制的家)。画布可见:用户看得到每一条,也可能改过 —— 他改的算数。风格定案(色号/字体/材质/艺术方向)随手记一条 `type: project` 记忆,别等收尾 |
88
+ | `CLAUDE.md`(根上)| **项目档案**:指引 / 风格档案 / 用户习惯三节,每次会话全文进你的上下文。放定了就不常变的东西;硬约束改动要用户点头。生长中的事实写 `记忆/` 不写这里 |
88
89
  | `.claude/skills/` `.claude/agents/` | 项目级自定义 skill / 子代理 |
89
90
  | `exports/` | export_handoff 的落点 |
90
- | `.claude/CLAUDE.md` | 项目指引。用户点头才写,别自作主张 |
91
91
  | `.nd/<会话id>/` | 这次对话自己的暗档案(压缩历史)。系统写,你一般不用碰 |
92
92
 
93
93
  `curl -L -o` 只能写到 cwd 底下,写别处静默失败。
@@ -127,7 +127,6 @@ index.html 直接在工作区根上的「根站」仍被识别,但**新站一
127
127
  他看得到、翻得动、也能改 —— 这不是你的私人日志,是你们俩共用的白板边角。
128
128
 
129
129
  - 一个文件 = 一张贴;`\n---\n` 分面(翻页看);每面首行 `# 标题`。
130
- - **设计决策**调 `record_decision`,它追加到 `notes/决策.md` 的新一面。
131
130
  - **讨论结论 / 参考素材消化 / 接力棒**("下次从这继续")直接 Write `notes/<slug>.md`。
132
131
  搜索/调研的发现里值得留的也落一张 —— 你的上下文迟早压缩,贴纸不会。
133
132
  - 别当垃圾桶:一张贴讲一件事,过时的删掉(文件删了贴就下墙)。每轮注入的清单
@@ -290,7 +289,7 @@ Edit/Write canvas 后系统会自动跑一致性校验(anchor 唯一 / layout-
290
289
  出一张按时刻拼好的胶片条,缓动/过冲/硬切一张图看完,数值级判断再上 `trace_motion`,`saveVideo: true` 出 webm 给用户过目)· `screenshot_url`
291
290
  (外部 URL 截图,找视觉参考用眼睛看)· `list_pages` · `read_page` ·
292
291
  `query_elements` · `get_computed_styles` · `navigate_to_page` · `highlight` ·
293
- `preview_deck` · `record_decision` · `get_pending_changes` / `clear_pending_changes`
292
+ `preview_deck` · `get_pending_changes` / `clear_pending_changes`
294
293
 
295
294
  浏览器九件(常驻):`browser_navigate` · `browser_read` · `browser_click` ·
296
295
  `browser_screenshot` · `browser_capture` · `browser_request_help` ·
@@ -340,7 +339,7 @@ batch**,一页一个回合太慢;逛到不确定的地方(要不要进、
340
339
  所以你打开的页面是"给人看的",别把它停在一张错误页上就走。
341
340
 
342
341
  按需先 `ToolSearch("select:mcp__nodesign__<tool>")` 拉 schema:`generate_image` ·
343
- `remove_background` · `web_search` · `expose_tweaks` · `export_handoff` ·
342
+ `remove_background` · `web_search` · `export_handoff` ·
344
343
  `pin_to_board` · `relate_on_board` ·
345
344
  `read_board` · `arrange_on_board` · `create_on_board` · `organize_board` · `deliver_files` ·
346
345
  `write_on_board` · `sketch_on_board` · `edit_sketch` · `finish_sketch` · `look_at_board` · `read_user_view` ·
@@ -358,12 +357,15 @@ noobai / noobai-eps / pony=danbooru 标签,anima=自然语言,krea2=12B 审
358
357
  限制,那是谁说了算的问题:别替他判断"好不好看",别因为自己不喜欢就重滚。
359
358
  看的成本要克制:默认清晰度扫一眼够判崩坏,别每张都 `detail:'high'`。
360
359
 
361
- harness 里的事用 `report_issue` 给维护者记一笔(进问题列表,不是给用户看的),
362
- 分三类:bug(行为错了)、friction(能用但绕路,同一个坑绕了不止一次)、
363
- idea(没坏,但你干活时看出了更好的做法 —— 想法会被认真读,具体的会被做出来,
364
- 别让它蒸发)。**这不是交差的替代品**:照常把活儿干完,顺手记一笔而已,别因为
365
- 报了就少交东西。自己犯的错(路径写错、Edit 没匹配上)不算,重试一次就好的偶发
366
- 抖动也不算,空泛的夸奖或"感觉能更好"也不算 —— 没有可实施内容的上报是噪音。
360
+ **这套 harness 正处 beta 测试期,你就是它的一线测试员** —— harness 里的事用
361
+ `report_issue` 给维护者记一笔(进问题列表,不是给用户看的),分三类:
362
+ bug(行为错了)、friction(能用但绕路 —— 绕过一次就值得记,别等第二次)、
363
+ idea(没坏,但你看出了更好的做法:该存在的工具、能合三步为一步的参数、
364
+ 以及**你自己摸出来的方法论** —— 某个套路/判断依据值得 harness 教给每个会话
365
+ 或做成自动化的,写下来)。每一条都会被认真读,具体的会被做出来;犹豫要不要
366
+ 报的时候,报。**这不是交差的替代品**:照常把活儿干完,顺手记一笔而已,别因为
367
+ 报了就少交东西。自己犯的错(路径写错、Edit 没匹配上)不算,空泛的夸奖或
368
+ "感觉能更好"也不算 —— 没有可实施内容的上报是噪音。
367
369
 
368
370
  用户说"把这套风格留下来""以后还想这么做"时用 `crystallize_skill`:把这次探索的**判断
369
371
  依据**(为什么这个字号阶梯、他否掉了什么、这套气质在什么场合会失效)写成他自己的
@@ -4,10 +4,9 @@
4
4
  > 注入,每 session 只注一次。SKILL.md 已含精简版核心要点,本文是深度参考。
5
5
  >
6
6
  > ⚠️ Gemini gateway 时代的参数段(imageSize / thinkingLevel / model 路由 flash-pro /
7
- > Image Search Grounding / PDF 文档输入 / NB2 model card 失败模式)**已整体移出本文**,
8
- > 存在同目录 `generate-image-cookbook.gemini-gateway.md`,不再注入。那些旋钮在当前
9
- > 后端上传了不报错也没效果,留在这里只会让你花心思拨一个不存在的开关。
10
- > `NODESIGN_IMAGE_PROVIDER=gateway` 重开时再把那份取回来。
7
+ > Image Search Grounding / PDF 文档输入 / NB2 model card 失败模式)**已整体移出本文**
8
+ > (那份存档 08-24 已删:全仓无引用的孤儿文件;要复活 gateway 从 git 历史里捞)。
9
+ > 那些旋钮在当前后端上传了不报错也没效果,留在这里只会让你花心思拨一个不存在的开关。
11
10
 
12
11
  ## 前置 · 后端事实 —— 先读这节,它决定下面所有写法
13
12
 
@@ -344,6 +343,24 @@ Step 3: 给角色起名锚定("Maya, character from Reference 1")
344
343
 
345
344
  每次只改一处,改完再看,别一轮里改五件事。
346
345
 
346
+ ### G1. variationOf 模式 —— 系列图一致性用参数,别手写咒语
347
+
348
+ 上面那套 "Change only… Keep… unchanged" 的骨架,工具已经内置成结构化参数:
349
+
350
+ ```json
351
+ { "variationOf": "assets/generated/A1.png",
352
+ "change": "裙子换成薄荷绿短裙,长度到膝盖以上",
353
+ "preserve": ["pose","framing","face","hair","top","shoes"] }
354
+ ```
355
+
356
+ - `variationOf` 就是编辑基底(自动成为第 1 张 reference),`change` 是唯一要动的那处,
357
+ `preserve` 是必须一致的方面(**省略 = 全保,最安全**)。工具会展开成完整骨架并
358
+ 自动追加通用禁止项(不凭空加背带/发饰、缺席也要保持 —— 都是真实翻过车的)。
359
+ - 何时用它而不是手写 § G 模板:**做系列**(换装矩阵、多页同角色、多帧同场景)。
360
+ 组织成一棵树:先出一张锚图,之后每次调用只沿一个轴变一处。
361
+ - 手写 prompt 仍然适合单次精修(改光线、删元素这类一次性编辑)。
362
+ - 此模式下 `prompt` 可省略;传了就当补充说明附在骨架后。
363
+
347
364
  ## H. 连续帧的能力边界(实测,别重新踩)
348
365
 
349
366
  | 用法 | 结果 |
@@ -390,9 +407,9 @@ Step 3: 给角色起名锚定("Maya, character from Reference 1")
390
407
 
391
408
  ## J. 调完必做
392
409
 
393
- 1. **`record_decision`** —— prompt + role + path + 用户评价记进任务便利贴
394
- (`notes/决策.md`)。重生时能查回,用户在画布上也看得到。
395
- ⚠️ 入参是 `title`(必填)/ `rationale`(必填)/ `scope` / `alternatives`,**没有 `topic`**。
410
+ 1. **记进记忆** —— 定案的 anchor(prompt 骨架 + role + 产物路径 + 用户评价)写一条
411
+ `type: project` 记忆(`记忆/` 主题文件 + MEMORY.md 一行)。重生时能查回,
412
+ 用户在画布上也看得到。
396
413
  2. **关键节点的反馈循环**:cover / 第一个 portrait / logo 嵌入这种会被当跨页种子的 anchor,
397
414
  生完在自然回话里邀请用户看一眼方向(生图的 image block 已自动渲染在 chat,他直接看得到)。
398
415
  种子早定早收益。
@@ -21,12 +21,22 @@
21
21
 
22
22
  (旧项目里工作区根上直接放 index.html 的「根站」仍被识别和支持,但**新站一律入夹**。)
23
23
 
24
- **构建型站点**(Astro / 11ty / 自写 build 脚本):源随便组织,构建产物落
25
- `dist/`(或 `out/` `build/` `_site/`,有 index.html 的那个自动被认作**产物根**;
26
- 也可在 `.nd-project.json` `"root": "<目录>"` 显式指定)。预览、截图、`list_pages`、
24
+ **构建型站点**(Vite / React / Vue / Astro / 11ty / 自写 build 脚本):源随便组织,
25
+ 构建产物落 `dist/`(或 `out/` `build/` `_site/` `public/`,有 index.html 的那个自动
26
+ 被认作**产物根**)。现代工程源码里的 `index.html`(引 `/src/main.tsx` 那种 dev 入口)
27
+ **不会**被误当产物 —— 构建完系统自动优先 dist。`.nd-project.json` 的 `"root": "<目录>"`
28
+ 可显式指定产物根:**放站点文件夹里,路径相对该文件夹**(如 `观察日志/.nd-project.json`
29
+ 写 `{"root":"dist"}`);根站的放工作区根。预览、截图、`list_pages`、
27
30
  导出、发布看的**全是产物根** —— 改完源必须重新构建,否则用户看到的还是旧的。
31
+ 构建必须用绝对路径或 `(cd <站点文件夹> && npm run build)` 子 shell 跑,**build 完
32
+ `ls` 确认产物真的落在了站点文件夹里的 dist/**(cwd 漂了会把 dist 建到别处)。
28
33
  `node_modules` / 构建缓存系统永远不扫,不用进 .ndignore。
29
34
 
35
+ WebGL/Three.js(含 R3F)的一个静默坑:渲染器属性(如剖切要的
36
+ `localClippingEnabled`)传给 `<Canvas gl={{…}}>` 构造参数会被**静默忽略**(无报错
37
+ 无效果),要在 `onCreated={({gl}) => { gl.localClippingEnabled = true }}` 里设;
38
+ `clippingPlanes` 裁掉的是法线**负**侧,"看起来没剖"先怀疑法线方向。
39
+
30
40
  手写起手:`mkdir 观察日志`,然后 Write `观察日志/index.html`(文件夹名换成真站名;
31
41
  没有起手模板,骨架从需求推、样式从风格名推)。**唯一硬要求是产物根上有 `index.html`**;
32
42
  样式文件怎么拆怎么叫你定,按站的体量来。
@@ -54,6 +54,7 @@ import {
54
54
  import { loadInstalledPlugins } from './plugin-loader.js';
55
55
  import { createHooks } from './hooks.js';
56
56
  import { buildIsolationOptions, prepareAgentDirs, sandboxShimEnv } from './isolation.js';
57
+ import { MEMORY_EXTRA_GUIDELINES, mergeAgentSettings } from './memory-config.js';
57
58
  import { createNodesignMcpServer } from '../mcp/index.js';
58
59
  import { assertInitContract } from './init-contract.js';
59
60
  import { createAgents, resolveDefaultFastModel } from '../agents/index.js';
@@ -329,8 +330,21 @@ export async function runSession({
329
330
  // npm 缓存 + 沙盒可写 tmp($TMPDIR / pip 缓存):细节与教训见 isolation.js
330
331
  const agentDirs = await prepareAgentDirs({ dataRoot: PROJECTS_DATA_ROOT, projectId, sessionId });
331
332
 
333
+ // ⛔ 服务器自己的运行姿态不许漏进 agent 环境(08-24 案):
334
+ // - NODE_ENV=production(pm2 注的)会让 agent 沙盒里的 npm install 静默跳过
335
+ // devDependencies —— 返回 0 还报 "up to date",vite/typescript 根本没装上,
336
+ // 构建道当场断。npm_config_production / npm_config_omit 是同一开关的旁路。
337
+ // - PWD 是 pm2 进程的 cwd(仓库根);spawn({cwd}) 不更新它,bash 会自校正但
338
+ // python/node/构建工具读 $PWD 拿到的就是错目录 ——"cwd 被重置到父目录"的
339
+ // 另一半病根。下面显式钉成 cwdRoot。
340
+ const {
341
+ NODE_ENV: _dropNodeEnv, npm_config_production: _dropNpmProd,
342
+ npm_config_omit: _dropNpmOmit, OLDPWD: _dropOldpwd,
343
+ ...inheritedEnv
344
+ } = process.env;
332
345
  const sdkEnv = {
333
- ...process.env,
346
+ ...inheritedEnv,
347
+ PWD: cwdRoot,
334
348
  ANTHROPIC_BASE_URL: baseUrlForBinary,
335
349
  // 订阅模型:apiKeyForBinary = process.env 原值(通常 undefined)——binary 见到
336
350
  // ANTHROPIC_API_KEY 会弃用 ~/.claude 订阅 OAuth,所以订阅路绝不能注入。
@@ -341,6 +355,8 @@ export async function runSession({
341
355
  // auto-memory 强制开启分支(binary gate:DISABLE 置 falsy 值 = force on,
342
356
  // 绕过 CLAUDE_CODE_SIMPLE 等后置门;前置门 U$/zl 若拦住则此招无效 → 走自建 B 计划)
343
357
  CLAUDE_CODE_DISABLE_AUTO_MEMORY: '0',
358
+ // 记忆指导追加段(保留 SDK 原合同只追加产品口径,理由见 memory-config.js)
359
+ CLAUDE_COWORK_MEMORY_EXTRA_GUIDELINES: MEMORY_EXTRA_GUIDELINES,
344
360
  // 工具搜索:非 alwaysLoad 的 MCP 工具延迟加载(省 ~25-30k 常驻 schema tokens),
345
361
  // agent 用 ToolSearch 按需取。白名单见 mcp/index.js ALWAYS_LOAD_TOOLS
346
362
  ENABLE_TOOL_SEARCH: 'true',
@@ -530,22 +546,6 @@ export async function runSession({
530
546
  persistSession: true,
531
547
  settingSources: ['project'],
532
548
 
533
- // skipWebFetchPreflight 来自 runtime/platform.js(gateway key 模式永远关)
534
- // 详细因果链见 platform.js 的 skipWebFetchPreflight 注释
535
- settings: {
536
- skipWebFetchPreflight: platform.skipWebFetchPreflight,
537
- // 项目 memory(2026-07-28 正式启用):骑 SDK 原生 auto-memory ——
538
- // 二级索引(MEMORY.md 常驻 + 主题文件按需召回)、召回监督器
539
- // (run.memory_recall 前端已渲染)、auto-dream 后台固化全由 SDK 包办。
540
- // ⚠️ 目录必须显式指到项目共享区:默认按 cwd 派生,而 cwd 是会话级的,
541
- // 不指定则每个会话一座记忆孤岛;指定后全项目所有会话共享一套。
542
- ...(sharedRoot ? {
543
- autoMemoryEnabled: true,
544
- // 落在 .claude/agent-memory/auto —— 跟前端记忆卡读的是同一棵树
545
- // (2026-07-28 修:之前指到 shared/agent-memory/auto,SDK 记了前端也看不到)
546
- autoMemoryDirectory: path.join(sharedRoot, '.claude', 'agent-memory', 'auto'),
547
- } : {}),
548
- },
549
549
 
550
550
  includePartialMessages: STREAMING_ENABLED,
551
551
  // 子代理时间轴(2026-07-28):转发子代理完整对话(text/thinking 也带
@@ -582,7 +582,18 @@ export async function runSession({
582
582
 
583
583
  // 隔离两道闸(sandbox 管 Bash,permissions.deny 管 Read/Write 这类进程内工具)
584
584
  // 全在 agent/isolation.js 里,改之前读那份文件头上的四条实测教训。
585
- ...buildIsolationOptions({ cwdRoot, sharedRoot, ...agentDirs, dataRoot: PROJECTS_DATA_ROOT, env: sdkEnv }),
585
+ // settings 必须与 isolationOptions.settings 深合并 —— 08-15 起两处 settings
586
+ // 静默互吞八天(autoMemory*/skipWebFetchPreflight 全丢)。合并+出口断言在
587
+ // memory-config.js 的 mergeAgentSettings。
588
+ ...(() => {
589
+ const isolation = buildIsolationOptions({ cwdRoot, sharedRoot, ...agentDirs, dataRoot: PROJECTS_DATA_ROOT, env: sdkEnv });
590
+ return {
591
+ ...isolation,
592
+ settings: mergeAgentSettings(isolation.settings, {
593
+ skipWebFetchPreflight: platform.skipWebFetchPreflight, sharedRoot,
594
+ }),
595
+ };
596
+ })(),
586
597
 
587
598
  toolConfig: {
588
599
  askUserQuestion: { previewFormat: 'html' },
@@ -44,7 +44,7 @@
44
44
  - `Read` / `Glob` / `Grep` —— 看本地 `./assets/` 里的素材 / `./spec.json` 决策档案
45
45
  - `TodoWrite` —— 3 步以上研究列计划
46
46
 
47
- **工具集外的工具**(用了也无效,且打破子代理边界):Write / Edit / Bash / screenshot_canvas / export / record_decision /
47
+ **工具集外的工具**(用了也无效,且打破子代理边界):Write / Edit / Bash / screenshot_canvas / export /
48
48
  AskUserQuestion —— 你的产物是信息不是修改,跟用户对话由主 agent 负责。研究就是研究。
49
49
 
50
50
  ---
@@ -137,7 +137,7 @@ export function createAgents({ mainModel, sdkModel, fastModel } = {}) {
137
137
  // - TodoWrite(多步研究列计划)
138
138
  // 不给:Write/Edit(不写代码)/ Bash(不要 shell shenanigans)/
139
139
  // AskUserQuestion(子代理不直接跟用户说话)/ screenshot/export/
140
- // record_decision(不是它的角色)/ Task(不允许嵌套子代理)
140
+ // Task(不允许嵌套子代理)
141
141
  // ⏸ explorer 研究员(2026-08-14 用户暂时停用:"有点没用")。定义与
142
142
  // prompt(explorer.md)原样保留,恢复 = 取消注释 + prelude 补回子代理
143
143
  // 清单那两行。停用期间搜索/读外链由主 agent 直接用 web_search / WebFetch。
@@ -10,7 +10,7 @@
10
10
  * 反馈层(用户在 canvas 上的直接编辑 + 评论 buffer):
11
11
  * get_pending_changes / clear_pending_changes
12
12
  * 产物层(NoDesign 差异化能力):
13
- * expose_tweaks / export_handoff / record_decision
13
+ * export_handoff …(expose_tweaks 08-24 暂退役待升级;record_decision 08-24 拆除,记忆体系接棒)
14
14
  * 研究层:
15
15
  * web_search(4 provider,CJK auto baidu)
16
16
  *
@@ -33,7 +33,6 @@ import { withCapabilityGate } from './capability-gate.js';
33
33
  import { makeScreenshotCanvasTool } from './tools/screenshot.js';
34
34
  import { makeScreenshotUrlTool } from './tools/screenshot-url.js';
35
35
  import { makeExportHandoffTool } from './tools/export-handoff.js';
36
- import { makeRecordDecisionTool } from './tools/record-decision.js';
37
36
  import { makeWebSearchTool } from './tools/web-search.js';
38
37
  import { withTierGate } from './tools/tier-gate.js'; // 档位闸包装器(auth/tier.js;按项目 owner)
39
38
  import { makeReadPageTool } from './tools/read-page.js';
@@ -55,7 +54,7 @@ import { makeNavigateToPageTool } from './tools/navigate-to-page.js';
55
54
  import { makeHighlightTool } from './tools/highlight.js';
56
55
  import { makePreviewDeckTool } from './tools/preview-deck.js';
57
56
  import { makeBuildDocxTool } from './tools/build-docx.js';
58
- import { makeExposeTweaksTool } from './tools/expose-tweaks.js';
57
+ // expose-tweaks 08-24 暂退役(能力不足待升级再加回):本体与前端 TweaksPanel 留档休眠,不注册
59
58
  import { makeGetPendingChangesTool } from './tools/get-pending-changes.js';
60
59
  import { makeClearPendingChangesTool } from './tools/clear-pending-changes.js';
61
60
  import { makeGenerateImageTool } from './tools/generate-image.js';
@@ -103,7 +102,7 @@ import { makeLookupTagsTool } from './tools/lookup-tags.js';
103
102
  const ALWAYS_LOAD_TOOLS = new Set([
104
103
  'screenshot_canvas', 'read_page', 'list_pages', 'query_elements',
105
104
  'get_computed_styles', 'navigate_to_page', 'highlight', 'preview_deck',
106
- 'record_decision', 'get_pending_changes', 'clear_pending_changes',
105
+ 'get_pending_changes', 'clear_pending_changes',
107
106
  // screenshot_url 常驻:explorer 显式 tools 列表没有 ToolSearch,defer 了它就
108
107
  // 永远拉不到 schema;schema 本身很小(4 字段),常驻成本可忽略
109
108
  'screenshot_url',
@@ -182,9 +181,6 @@ export function createNodesignMcpServer({ workspaceRoot, sharedRoot, projectId,
182
181
  // C10 export_handoff — 复用 exports.js 的 buildHandoffZip,写到 workspace/exports/
183
182
  makeExportHandoffTool({ workspaceRoot, sharedRoot, projectId, sessionId, ctx }),
184
183
 
185
- // C11 record_decision — 写入 spec.json decisions[] 设计意图档案
186
- makeRecordDecisionTool({ workspaceRoot, sessionId, ctx }),
187
-
188
184
  // crystallize_skill — 把探索出来的方法论固化成用户自己的 skill + 作品进橱窗
189
185
  // (2026-07-30)。用户明确要求才调;写的是判断依据不是成品 HTML。
190
186
  makeCrystallizeSkillTool({ projectId, sessionId, ctx }),
@@ -284,9 +280,6 @@ export function createNodesignMcpServer({ workspaceRoot, sharedRoot, projectId,
284
280
  makeGetPendingChangesTool({ workspaceRoot, ctx }),
285
281
  makeClearPendingChangesTool({ workspaceRoot, ctx }),
286
282
 
287
- // Tweaks 协议:agent 暴露 deck 专属可调参数 schema → 前端按 schema 渲染控件
288
- makeExposeTweaksTool({ workspaceRoot, ctx }),
289
-
290
283
  // 图片生成(gemini-3.1-flash-image-preview / Nano Banana 2,via NoDesk passthrough)
291
284
  // 落档优先 sharedRoot/assets/generated/,fallback workspaceRoot/assets/generated/。
292
285
  // 跨 session 共享靠 sessions/<sid>/assets softlink → shared/assets。
@@ -304,7 +297,7 @@ export function createNodesignMcpServer({ workspaceRoot, sharedRoot, projectId,
304
297
  // 关系线(2026-08-07):agent 把「这版改自那版」「这两个是对照」这类
305
298
  // **只有它知道**的关系画到画布上。画布知道每个产物是什么,但不知道它们
306
299
  // 之间是什么关系 —— 那是北极星(排出有版面感的布局)真正缺的那一块。
307
- makeRelateOnBoardTool({ projectId, ctx }),
300
+ makeRelateOnBoardTool({ sharedRoot, projectId, ctx }),
308
301
  // agent 摆位四件(2026-08-14):看版面 / 语义摆位·立主角 / 落手写便签 / 归纳收纳
309
302
  makeReadBoardTool({ projectId }),
310
303
  makeArrangeOnBoardTool({ projectId, ctx }),
@@ -90,7 +90,9 @@ drag things afterwards — their move wins, do not fight it.`,
90
90
  if (subjectEntry) {
91
91
  const subjectLayer = layerOf(subject, subjectEntry, known);
92
92
  if (subjectLayer !== anchorLayer) {
93
- return err(`不同层(${subjectLayer || '根'} vs ${anchorLayer || '根'}):先 mv 文件到同一个文件夹,画布跟着走。`);
93
+ return err(`不同层(${subjectLayer || '根'} vs ${anchorLayer || '根'}):摆位只在同一层内有意义。`
94
+ + '要摆到一起:先把文件放进同一个文件夹(assets/ 里的素材用 cp 复制过去 —— 母版别挪走);'
95
+ + '只是想表达"相关",用 relate_on_board 连一条线更合适。');
94
96
  }
95
97
  }
96
98
 
@@ -179,7 +179,8 @@ async function zoomShot(page, region, frame, lead) {
179
179
  }
180
180
  const [x0, y0, x1, y1] = region.map(Math.round);
181
181
  if (x0 < 0 || y0 < 0 || x1 > frame.w || y1 > frame.h || x1 - x0 < 8 || y1 - y0 < 8) {
182
- return asText(`Error: zoom region [${region.join(', ')}] must lie inside the ${frame.w}×${frame.h} screenshot and be at least 8×8.`, true);
182
+ return asText(`Error: zoom region [${region.join(', ')}] must lie inside the ${frame.w}×${frame.h} LIVE VIEWPORT and be at least 8×8. `
183
+ + 'zoom takes viewport pixels — coordinates from a fullPage screenshot are in page space and do NOT work here; scroll the target into view first, or subtract the scroll offset.', true);
183
184
  }
184
185
  const clip = { x: toPage(x0, frame), y: toPage(y0, frame), width: toPage(x1 - x0, frame), height: toPage(y1 - y0, frame) };
185
186
  const buf = await page.screenshot({ type: 'png', clip });
@@ -104,6 +104,7 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
104
104
  const out = [];
105
105
  const n = actions.length;
106
106
  let failedAt = -1;
107
+ let failText = '';
107
108
  let lastHadImage = false;
108
109
  for (let i = 0; i < n; i += 1) {
109
110
  const { name: toolName, input } = actions[i];
@@ -112,14 +113,16 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
112
113
  const def = byName.get(toolName);
113
114
  if (!def) {
114
115
  failedAt = i;
115
- out.push({ type: 'text', text: `${label}: Error: "${toolName}" is not batchable here (use one of ${names.join(', ')}).` });
116
+ failText = `"${toolName}" is not batchable here (use one of ${names.join(', ')}).`;
117
+ out.push({ type: 'text', text: `${label}: Error: ${failText}` });
116
118
  continue;
117
119
  }
118
120
  const parsed = z.object(def.inputSchema).safeParse(input || {});
119
121
  if (!parsed.success) {
120
122
  failedAt = i;
121
123
  const why = parsed.error.issues.map(is => `${is.path.join('.') || '(root)'}: ${is.message}`).join('; ');
122
- out.push({ type: 'text', text: `${label}: Error: invalid input — ${why}` });
124
+ failText = `invalid input — ${why}`;
125
+ out.push({ type: 'text', text: `${label}: Error: ${failText}` });
123
126
  continue;
124
127
  }
125
128
  let r;
@@ -135,7 +138,21 @@ export function makeBatchTool({ name, description, tools, batchable, finalShot }
135
138
  if (b.type === 'text' && !prefixed) { out.push({ type: 'text', text: `${label}: ${b.text}` }); prefixed = true; }
136
139
  else out.push(b);
137
140
  }
138
- if (r?.isError) failedAt = i;
141
+ if (r?.isError) {
142
+ failedAt = i;
143
+ failText = blocks.find(b => b.type === 'text')?.text || '(no error text)';
144
+ }
145
+ }
146
+ // 失败时把错误行提到最前面:下游(记账层截 120/500 字符、模型扫返回)都先看到
147
+ // 真正的报错,而不是前面成功步骤的输出。同时钉死"别整批重跑"——前面的步骤
148
+ // (click/type 这类非幂等动作)已经执行过了。
149
+ if (failedAt >= 0) {
150
+ const step = actions[failedAt];
151
+ out.unshift({
152
+ type: 'text',
153
+ text: `FAILED at step ${failedAt + 1}/${n} (${step.name}${step.input?.action ? ` ${step.input.action}` : ''}): ${String(failText).split('\n')[0]}\n`
154
+ + `Steps 1-${failedAt} already ran — do NOT re-run the whole batch; continue from the failed step.`,
155
+ });
139
156
  }
140
157
  if (finalShot && screenshotAfter !== false && !lastHadImage) {
141
158
  const shotDef = byName.get(finalShot.name);
@@ -121,7 +121,7 @@ and reopens on the next call — the profile survives, so you stay logged in.
121
121
  The user has a browser card on their desktop and can watch, or take over.`,
122
122
  {
123
123
  url: z.string().min(4).describe('Absolute http(s) URL.'),
124
- waitUntil: z.enum(['domcontentloaded', 'load', 'networkidle']).optional()
124
+ waitUntil: z.enum(['commit', 'domcontentloaded', 'load', 'networkidle']).optional()
125
125
  .describe("How long to wait. Default 'domcontentloaded' — fastest and enough to read; measured on this machine, 'networkidle' costs an extra 1.6-4s per page, so do not reach for it by reflex. Use it only when a JS-built page comes back empty."),
126
126
  digest: z.boolean().optional()
127
127
  .describe('Include the page digest (default true). Set false only when you are navigating purely to screenshot and do not want the text.'),
@@ -175,9 +175,31 @@ The user has a browser card on their desktop and can watch, or take over.`,
175
175
  ].filter(Boolean).join('\n'), true);
176
176
  }
177
177
  if (navErr) {
178
+ // 超时 ≠ 全失败:commit 之后页面往往已有内容。半张页面也交回去 ——
179
+ // agent 拿到骨架就能决定继续还是换站(08-19 上报:原建议「试
180
+ // waitUntil:"load"」是往错方向推 —— load 比 domcontentloaded 严,
181
+ // 连 DOMContentLoaded 都没到换 load 只会更慢地再失败一次)
182
+ const timedOut = /Timeout/i.test(navErr);
183
+ let landedUrl = null;
184
+ try { const u = page.url(); if (timedOut && u !== before && /^https?:/.test(u)) landedUrl = u; } catch { /* */ }
185
+ if (landedUrl) {
186
+ let lines = [];
187
+ try {
188
+ const data = await collectPage(page);
189
+ if (!data.missing) lines = formatPage(data, { compact: true });
190
+ } catch { /* 半张页面读不出摘要也照样交回 */ }
191
+ try { ctx?.emit?.({ type: 'run.browser_opened', url: landedUrl, ts: new Date().toISOString() }); } catch { /* */ }
192
+ return asText([
193
+ `⚠️ 等待超时(${navErr}),但页面已部分加载 —— 当前状态如下,够用就继续(read/screenshot 都可),不够就换站:`,
194
+ ...lines,
195
+ gate,
196
+ ].filter(Boolean).join('\n'));
197
+ }
178
198
  return asText([
179
199
  `导航失败:${navErr}`,
180
- '如果是超时:站点可能慢或者在挡自动化访问;试 waitUntil:"load",或者换一个参考站。',
200
+ timedOut
201
+ ? '超时多半是广告/追踪脚本卡死或站点反自动化,不是"等得不够久"。别换 waitUntil:"load"(更严只会更慢地失败);可试 waitUntil:"commit"(一有响应就接手,配 read/screenshot 看拿到多少),或者直接换一个参考站。'
202
+ : '站点可能在挡自动化访问;换一个参考站。',
181
203
  gate,
182
204
  ].filter(Boolean).join('\n'), true);
183
205
  }
@@ -9,7 +9,7 @@
9
9
  * (~/.nodesign/plugins/<userId>/<name>/),下次开新会话在**这个用户的所有项目**里
10
10
  * 都能用(plugin 发现是 startup-time,当前会话不生效)。
11
11
  *
12
- * record_decision 的分工:决策贴是**这一件作品**的过程档案,随任务走;
12
+ * 跟长期记忆(记忆/ + 根 CLAUDE.md)的分工:记忆记的是**这个项目/这位用户**的持续状态,随项目走;
13
13
  * 这里是跨项目复用的方法论,随用户走。一个是日志,一个是沉淀。
14
14
  *
15
15
  * 归属:写到**项目 owner** 的目录,不是"当前请求者"——同一个项目谁跑都该产出同一
@@ -41,7 +41,6 @@
41
41
 
42
42
  import path from 'node:path';
43
43
  import fs from 'node:fs/promises';
44
- import { spawn } from 'node:child_process';
45
44
  import { tool } from '@anthropic-ai/claude-agent-sdk';
46
45
  import { Events } from '../../agent/events.js';
47
46
  import { z } from 'zod';
@@ -49,6 +48,9 @@ import sharp from 'sharp';
49
48
  import {
50
49
  THUMBNAIL_MAX_DIM, THUMBNAIL_QUALITY, writeWebpSibling, // 预热由 writeWebpSibling 顺带做
51
50
  } from '../../../lib/image-variant.js';
51
+ import {
52
+ buildCodexBridgePrompt, runCodexImageGen, buildVariationPrompt, PRESERVE_KEYS,
53
+ } from './helpers/codex-imagegen.js';
52
54
 
53
55
  // Thumbnail 配置(env 可调)。**原图不动**——保留 Gemini 输出的全分辨率(通常
54
56
  // 1080×1920+ PNG,6-8MB)让用户最终交付不损失质量。仅生成低清 thumbnail 给
@@ -136,71 +138,9 @@ const DEFAULT_DMXAPI_BASE = 'https://www.dmxapi.cn';
136
138
  const DEFAULT_CHANNEL = 'DMX';
137
139
 
138
140
  // ── codex 生图桥(2026-07-27:NoDesk 网关退役,codex 成为默认 provider)──
139
- // codex CLI 订阅(零 API 费):spawn `codex exec` 让它调自带图像生成工具,
140
- // 图直接落到我们指定的绝对路径。实测单张 ~45-60s,参考图走 -i 附件(同样实测
141
- // 风格参照有效)。桥接 prompt 必须写死"逐字传递零改写"——codex agent 默认会
142
- // 按自己的 Augmentation rules 润色 prompt。
141
+ // 桥的 prompt 组装 + 子进程执行 + 变体模式骨架都在 helpers/codex-imagegen.js
142
+ // (2026-08-24 迁出)。这里只留 provider 分流开关。
143
143
  const IMAGE_PROVIDER = () => (process.env.NODESIGN_IMAGE_PROVIDER || 'codex').toLowerCase();
144
- const CODEX_BIN = process.env.NODESIGN_CODEX_BIN || 'codex';
145
- const CODEX_IMAGE_TIMEOUT_MS = Number(process.env.NODESIGN_CODEX_IMAGE_TIMEOUT_MS) || 240_000;
146
-
147
- function buildCodexBridgePrompt({ prompt, aspectRatio, absOut, refCount }) {
148
- return [
149
- '你是图像生成管道的执行端,只做下面几件事,不做任何多余动作:',
150
- '1. 调用你的图像生成工具生成一张图。<image-prompt> 标签内的内容必须逐字作为生成 prompt,禁止改写、增删、翻译或润色。',
151
- `2. 输出比例:${aspectRatio}。优先用工具的比例/尺寸参数;工具没有对应参数时,作为补充说明传给工具,但不修改 <image-prompt> 原文。`,
152
- refCount > 0
153
- ? `3. 本消息附带 ${refCount} 张参考图,把它们作为图像生成的参考输入(风格 / 主体一致性参照)。`
154
- : '3. 本次无参考图。',
155
- `4. 生成后把图片文件复制到精确路径 ${absOut}(目录已存在)。`,
156
- '5. 最后只回复该绝对路径。',
157
- '<image-prompt>',
158
- prompt,
159
- '</image-prompt>',
160
- ].join('\n');
161
- }
162
-
163
- /**
164
- * 跑一次 codex exec 生图,以目标文件落盘为成功标准(codex 的文本回复不可信),
165
- * 失败自动重试一次。abort signal / 超时都 SIGKILL 子进程。
166
- */
167
- async function runCodexImageGen({ bridgePrompt, refPaths, cwd, signal, expectFile, timeoutMs = CODEX_IMAGE_TIMEOUT_MS }) {
168
- const args = ['exec', '--skip-git-repo-check', '-s', 'workspace-write', '-C', cwd, bridgePrompt];
169
- for (const p of refPaths) args.push('-i', p);
170
-
171
- const runOnce = () => new Promise((resolve, reject) => {
172
- const child = spawn(CODEX_BIN, args, { stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
173
- let stderrTail = '';
174
- child.stdout.on('data', () => { /* 排空防背压 */ });
175
- child.stderr.on('data', (d) => { stderrTail = (stderrTail + d.toString()).slice(-2000); });
176
- const killTimer = setTimeout(() => {
177
- try { child.kill('SIGKILL'); } catch { /* */ }
178
- reject(new Error(`codex exec timeout after ${Math.round(timeoutMs / 1000)}s`));
179
- }, timeoutMs);
180
- const onAbort = () => { try { child.kill('SIGKILL'); } catch { /* */ } };
181
- signal?.addEventListener?.('abort', onAbort, { once: true });
182
- child.on('error', (err) => { clearTimeout(killTimer); reject(err); });
183
- child.on('close', (code) => {
184
- clearTimeout(killTimer);
185
- signal?.removeEventListener?.('abort', onAbort);
186
- if (signal?.aborted) return reject(new Error('aborted'));
187
- if (code !== 0) return reject(new Error(`codex exec exited ${code}: ${stderrTail.slice(-300) || 'no stderr'}`));
188
- resolve();
189
- });
190
- });
191
-
192
- for (let attempt = 1; attempt <= 2; attempt++) {
193
- try {
194
- await runOnce();
195
- const st = await fs.stat(expectFile).catch(() => null);
196
- if (st && st.size > 0) return;
197
- throw new Error(`codex finished but target file missing/empty: ${expectFile}`);
198
- } catch (err) {
199
- if (attempt === 2 || signal?.aborted) throw err;
200
- console.warn(`[generate-image] codex attempt ${attempt} failed (${err.message}), retrying once`);
201
- }
202
- }
203
- }
204
144
 
205
145
  const PASSTHROUGH_PATH = '/default/passthrough';
206
146
  // model id 在 callGateway 时动态拼,因为支持 flash / pro 路由
@@ -413,6 +353,15 @@ REFERENCES:
413
353
  conversation. To iterate on a previous image, pass it back in referenceImages
414
354
  and restate the invariants every round.
415
355
 
356
+ VARIATION MODE (series consistency — outfit swaps, one-detail edits):
357
+ Instead of hand-writing "change only X, keep Y" prompts, pass
358
+ variationOf (path of the base image) + change (the ONE thing to alter)
359
+ + optional preserve (which aspects must stay identical; default all).
360
+ The tool expands these into a battle-tested skeleton incl. standard
361
+ prohibitions (no invented straps/ornaments, absences preserved). Build
362
+ series as a tree: one anchor, then vary one axis per call. In this mode
363
+ prompt is optional (used as extra notes).
364
+
416
365
  WHEN TO USE:
417
366
  - You're building a deck / landing / report and want real imagery
418
367
  - You need a backdrop that pure CSS gradient can't achieve
@@ -424,15 +373,29 @@ WHEN NOT TO USE:
424
373
  - Data charts — use Recharts/ECharts/Mermaid via React mount
425
374
  - Simple inline icons (≤5 per page) — use lucide-react inline SVG
426
375
 
427
- ALWAYS pair generation with mcp__nodesign__record_decision so the prompt + role
428
- land on the shared decision sticky (notes/decisions). Its params are title +
429
- rationale (both required), scope, alternatives — there is no "topic" param.`,
376
+ After a generation that settles style direction, record the settled anchor as a
377
+ memory (记忆/, type: project) so later sessions inherit it.`,
430
378
  {
431
379
  prompt: z
432
380
  .string()
433
381
  .min(4)
434
382
  .max(3500)
435
- .describe('Natural-language scene description. Describe, don\'t list keywords.'),
383
+ .optional()
384
+ .describe('Natural-language scene description. Describe, don\'t list keywords. Required unless variationOf is set (then it is appended as extra notes).'),
385
+ variationOf: z
386
+ .string()
387
+ .optional()
388
+ .describe('VARIATION MODE: workspace-relative path of the base image to reproduce with ONE change. Becomes the first reference image (edit target).'),
389
+ change: z
390
+ .string()
391
+ .min(4)
392
+ .max(500)
393
+ .optional()
394
+ .describe('VARIATION MODE: the single thing to alter, e.g. "裙子换成薄荷绿短裙,长度到膝盖以上". Required with variationOf.'),
395
+ preserve: z
396
+ .array(z.enum(PRESERVE_KEYS))
397
+ .optional()
398
+ .describe('VARIATION MODE: aspects that must stay identical. Omit = all of them (safest).'),
436
399
  aspectRatio: z
437
400
  .enum(ASPECT_RATIOS)
438
401
  .optional()
@@ -476,6 +439,9 @@ rationale (both required), scope, alternatives — there is no "topic" param.`,
476
439
  },
477
440
  async ({
478
441
  prompt,
442
+ variationOf,
443
+ change,
444
+ preserve,
479
445
  aspectRatio = '16:9',
480
446
  imageSize = '1K',
481
447
  referenceImages,
@@ -496,6 +462,19 @@ rationale (both required), scope, alternatives — there is no "topic" param.`,
496
462
  isError: true,
497
463
  };
498
464
  }
465
+ // 变体模式归一化:variationOf 只是 referenceImages[0] 的语义化别名,
466
+ // prompt 换成固定骨架 —— 下游(ref 解析 / -i 附件 / gateway inline)零改动。
467
+ const isVariation = Boolean(variationOf);
468
+ if (isVariation && !change) {
469
+ return { content: [{ type: 'text', text: 'generate_image failed: variationOf requires `change` (the ONE thing to alter).' }], isError: true };
470
+ }
471
+ if (!isVariation && !prompt) {
472
+ return { content: [{ type: 'text', text: 'generate_image failed: `prompt` is required (or use variationOf + change).' }], isError: true };
473
+ }
474
+ if (isVariation) {
475
+ prompt = buildVariationPrompt({ change, preserve, extra: prompt });
476
+ referenceImages = [variationOf, ...(referenceImages || [])];
477
+ }
499
478
  // 1. provider 分流(2026-07-27:NoDesk 网关退役,默认 codex 订阅生图;
500
479
  // gateway 分支保留给显式 NODESIGN_IMAGE_PROVIDER=gateway 的场景)
501
480
  const provider = IMAGE_PROVIDER();
@@ -550,7 +529,7 @@ rationale (both required), scope, alternatives — there is no "topic" param.`,
550
529
  fileName = `${finalName}.png`;
551
530
  absOut = path.join(outDir, fileName);
552
531
  const bridgePrompt = buildCodexBridgePrompt({
553
- prompt, aspectRatio, absOut, refCount: resolvedRefs.length,
532
+ prompt, aspectRatio, absOut, refCount: resolvedRefs.length, variation: isVariation,
554
533
  });
555
534
  try {
556
535
  await runCodexImageGen({
@@ -706,6 +685,7 @@ rationale (both required), scope, alternatives — there is no "topic" param.`,
706
685
  provider,
707
686
  model: provider === 'codex' ? 'codex' : model,
708
687
  referenceImageCount: resolvedRefs.length,
688
+ variationOf: variationOf || null, // 变体血缘:产物墙 / 引用层能顺出系列树
709
689
  sessionId: ctx?.sessionId || null,
710
690
  runId: ctx?.runId || null,
711
691
  ts: new Date().toISOString(),