@xiaobuyu/nodesign 0.0.1 → 0.0.3

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 (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,434 +1,434 @@
1
- /**
2
- * mcp/tools/paint-still.js — paint_still MCP tool(2026-08-08;批量制+FLUX.2 同晚补上)
3
- *
4
- * 站主本地 GPU 盒子生图。一次调用 = 一批(1-16 条,串行渲、出一张上墙一张);
5
- * 每条还可以再开 batch(同提示词多变体,一次采样出 N 张,抽卡用,比串行快数倍)。
6
- *
7
- * 模型档(08-11 扩到五档):
8
- * noobai NoobAI-XL V-Pred 1.0,danbooru/e621 标签,解剖与标签理解最强
9
- * noobai-eps NoobAI-XL 1.1 eps,只在 LoRA 仅有 eps 版时用(LoRA 跨预测目标会发灰)
10
- * pony Pony Diffusion V6 XL,score_9 六段串体系,clip skip 2 已在盒端配好
11
- * anima 自然语言英文
12
- * krea2 Krea 2 Turbo 12B 审美向,自然语言
13
- *
14
- * ⚠️ 许可:noobai/noobai-eps **禁止任何形式商业化,含生成物**(Laxhar Lab 在 FAIPL
15
- * 之上自加条款);pony **禁止在任何货币化的站点/应用上跑推理**;anima 非商用。
16
- * 三者都只适合站主自用,不能做成对外服务。要商用走 Illustrious-XL(RAIL++-M
17
- * 明文允许 SaaS)或 krea2。08-11 前这里写着 noobai"商用可",是错的。
18
- *
19
- * 配方 = 08-08/08-11 实测 + 官方模板。盒子不在线就明说,不静默降级。
20
- * 落盘/缩略图/事件照 generate-image.js;只回文本路径(图不进返回值),
21
- * 但 agent 可以自己去看那些文件挑废图(2026-08-18 解禁),审美判断仍归用户。
22
- */
23
-
24
- import os from 'node:os';
25
- import path from 'node:path';
26
- import fs from 'node:fs/promises';
27
- import { tool } from '@anthropic-ai/claude-agent-sdk';
28
- import { z } from 'zod';
29
- import sharp from 'sharp';
30
- import { Events } from '../../agent/events.js';
31
- import { getProject } from '../../../projects/store.js';
32
- import { getUserById } from '../../../auth/users-store.js';
33
- import { can, localGenApproved, DENIAL } from '../../../auth/tier.js';
34
- import {
35
- THUMBNAIL_MAX_DIM, THUMBNAIL_QUALITY, enqueueWarm, warmSpecsFor,
36
- } from '../../../lib/image-variant.js';
37
- import { boxConfig, shq, runBox, sshArgs, scpArgs, localBoxEnabled, BOX_OFF_MSG } from './h3box-ssh.js';
38
-
39
- const SSH_TIMEOUT_MS = Number(process.env.NODESIGN_H3BOX_TIMEOUT_MS) || 240_000;
40
- // krea2 bf16 24G 全驻卡;换模型后的首张要付一次装载(~1 分钟),给足余量
41
- const TIMEOUT_BY_MODEL = {
42
- noobai: SSH_TIMEOUT_MS, 'noobai-eps': SSH_TIMEOUT_MS, pony: SSH_TIMEOUT_MS,
43
- anima: SSH_TIMEOUT_MS, krea2: 400_000,
44
- };
45
- // batch 会线性拉长单次渲染,超时按张数放大(封顶 10 分钟,别让挂死的活撑满)
46
- const timeoutFor = (still) => Math.min(
47
- (TIMEOUT_BY_MODEL[still.model] || SSH_TIMEOUT_MS) * Math.max(1, (still.batch || 1) * 0.6),
48
- 600_000,
49
- );
50
-
51
- async function makeThumbnail(rawBuf) {
52
- try {
53
- const meta = await sharp(rawBuf).metadata();
54
- const w = meta.width || 0; const h = meta.height || 0;
55
- let pipeline = sharp(rawBuf);
56
- if (Math.max(w, h) > THUMBNAIL_MAX_DIM) {
57
- pipeline = pipeline.resize({
58
- width: w >= h ? THUMBNAIL_MAX_DIM : null,
59
- height: h > w ? THUMBNAIL_MAX_DIM : null,
60
- fit: 'inside', withoutEnlargement: true,
61
- });
62
- }
63
- const buf = await pipeline.webp({ quality: THUMBNAIL_QUALITY }).toBuffer();
64
- return { buf, mimeType: 'image/webp' };
65
- } catch (err) {
66
- console.warn(`[paint-still] thumbnail failed (${err.message})`);
67
- return null;
68
- }
69
- }
70
-
71
- /**
72
- * 把工作区里的参考图推到盒子,返回盒子上的绝对路径。
73
- * 失败返回 {err},调用方据此中止这条 still —— 参考图丢了还照跑等于白烧。
74
- */
75
- /**
76
- * 参考图推盒前先缩:上行实测只有 ~0.2MB/s(08-11),2MB 原图光上传就 10 秒。
77
- * IP-Adapter 的 CLIP 端最终只吃 224px,ref 缩到 512 零损失;init/control 参与
78
- * 生成分辨率,封顶 1344(SDXL 原生上限)。统一出 PNG 保透明;缩完反而更大
79
- * (罕见,如高压缩 JPEG 转 PNG)就推原图。缩图失败也推原图,别因优化挂正事。
80
- */
81
- async function shrinkForPush(abs, slot) {
82
- const maxDim = slot.startsWith('ref') ? 512 : 1344;
83
- try {
84
- const meta = await sharp(abs).metadata();
85
- if (!meta.width || !meta.height || Math.max(meta.width, meta.height) <= maxDim) return null;
86
- const buf = await sharp(abs)
87
- .resize({ width: maxDim, height: maxDim, fit: 'inside', withoutEnlargement: true })
88
- .png().toBuffer();
89
- if (buf.length >= (await fs.stat(abs)).size) return null;
90
- return buf;
91
- } catch { return null; }
92
- }
93
-
94
- async function pushRef({ box, root, relPath, jobId, slot, signal }) {
95
- const abs = path.resolve(root, relPath);
96
- // 越狱防护:参考图必须在工作区内
97
- if (!abs.startsWith(path.resolve(root))) return { err: `${slot} 路径越出工作区:${relPath}` };
98
- try { await fs.access(abs); } catch { return { err: `${slot} 找不到文件:${relPath}` }; }
99
- const shrunk = await shrinkForPush(abs, slot);
100
- const remote = `~/refs/${jobId}-${slot}${shrunk ? '.png' : (path.extname(abs) || '.png')}`;
101
- const mk = await runBox(box, 'ssh', [...sshArgs(box), 'mkdir -p ~/refs'], { timeoutMs: 30_000, signal });
102
- if (mk.code !== 0) return { err: `盒子建 refs 目录失败:${(mk.err || '').slice(-200)}` };
103
- let src = abs; let tmp = null;
104
- if (shrunk) {
105
- tmp = path.join(os.tmpdir(), `h3ref-${jobId}-${slot}.png`);
106
- await fs.writeFile(tmp, shrunk);
107
- src = tmp;
108
- }
109
- const put = await runBox(box, 'scp',
110
- [...scpArgs(box), src, `${box.target}:${remote}`], { timeoutMs: 120_000, signal });
111
- if (tmp) fs.unlink(tmp).catch(() => { /* */ });
112
- if (put.code !== 0) return { err: `${slot} 上传失败:${(put.err || '').slice(-200)}` };
113
- return { remote };
114
- }
115
-
116
- /** 单张落盘 + 缩略图 + sidecar + 上墙事件;返回 caption 行 */
117
- async function landStill({ imgBuf, still, seed, outDir, ctx, sessionId, wallS, idx = 0, total = 1 }) {
118
- // batch>1 时同一条 still 出 N 张,文件名加序号;单张时不加,保持老路径形态
119
- const finalName = `still-${still.jobId}-${still.name}${total > 1 ? `-${idx + 1}` : ''}`;
120
- const fileName = `${finalName}.png`;
121
- const absOut = path.join(outDir, fileName);
122
- await fs.writeFile(absOut, imgBuf);
123
-
124
- const thumbDir = path.join(outDir, '.thumbnails');
125
- await fs.mkdir(thumbDir, { recursive: true });
126
- const thumb = await makeThumbnail(imgBuf);
127
- let thumbAgentRelPath = null;
128
- if (thumb) {
129
- await fs.writeFile(path.join(thumbDir, `${finalName}.thumb.webp`), thumb.buf);
130
- thumbAgentRelPath = path.posix.join('assets', 'generated', '.thumbnails', `${finalName}.thumb.webp`);
131
- }
132
- fs.stat(absOut)
133
- .then((st) => enqueueWarm(absOut, st, warmSpecsFor()))
134
- .catch(() => { /* 预热失败下次请求现编 */ });
135
-
136
- try {
137
- const metaDir = path.join(outDir, '.meta');
138
- await fs.mkdir(metaDir, { recursive: true });
139
- await fs.writeFile(path.join(metaDir, `${finalName}.json`), JSON.stringify({
140
- prompt: still.prompt, negative: still.negative || null, provider: 'h3box',
141
- model: still.model, seed, size: still.size,
142
- sessionId: ctx?.sessionId || sessionId || null,
143
- runId: ctx?.runId || null,
144
- ts: new Date().toISOString(),
145
- }, null, 2));
146
- } catch (e) { console.warn(`[paint-still] meta sidecar failed: ${e.message}`); }
147
-
148
- // 发**工作区相对路径**(fileChanged 的正字法,hooks 同款)——发绝对路径的话
149
- // 前端寻址/版本记账全部静默失配(2026-08-14 普查改)
150
- try { ctx?.emit?.(Events.fileChanged(path.posix.join('assets', 'generated', finalName), 'add')); } catch { /* fail-safe */ }
151
- try {
152
- ctx?.emit?.({
153
- type: 'run.image_generated',
154
- path: path.posix.join('assets', 'generated', fileName),
155
- thumbnailPath: thumbAgentRelPath,
156
- absPath: absOut,
157
- sizeBytes: imgBuf.length,
158
- thumbnailSizeBytes: thumb?.buf.length || null,
159
- prompt: still.prompt, assetRole: null, aspectRatio: null, imageSize: still.size,
160
- model: `h3box-${still.model}`,
161
- referenceImageCount: (still.ref_image ? String(still.ref_image).split(',').filter((s) => s.trim()).length : 0)
162
- + (still.init_image ? 1 : 0) + (still.control_image ? 1 : 0),
163
- accompanyText: null,
164
- });
165
- } catch { /* fail-safe */ }
166
-
167
- const tag = total > 1 ? `${still.name} #${idx + 1}/${total}` : still.name;
168
- return `[${tag}] assets/generated/${fileName} — ${still.model} ${still.size} seed=${seed}${total > 1 ? `+${idx}` : ''} ${(imgBuf.length / 1024).toFixed(0)}KB ${wallS}s`;
169
- }
170
-
171
- /** 核心流程(从 handler 拆出,便于不起 SDK 直接实弹测试) */
172
- export async function paintStills(
173
- { workspaceRoot, sharedRoot, projectId, sessionId, ctx },
174
- { stills },
175
- ) {
176
- const asText = (text, isError = false) =>
177
- ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
178
- try {
179
- if (!localBoxEnabled()) {
180
- return asText(BOX_OFF_MSG, true);
181
- }
182
- const box = boxConfig();
183
- if (!box) {
184
- return asText('本地生图盒子未配置(站主没开机或没设 NODESIGN_H3BOX_SSH)。转告用户,改用 generate_image。', true);
185
- }
186
- const project = getProject(projectId);
187
- if (!project) return asText('错误:项目不存在', true);
188
- const owner = project.ownerId ? getUserById(project.ownerId) : null;
189
- // 档位闸 + 逐人批准(auth/tier.js):basic 档不开任何生图;pro 档还要被站主批过本地产线
190
- if (!can(owner, 'localGen')) return asText(DENIAL.localGenTier, true);
191
- if (!localGenApproved(owner)) return asText(`${DENIAL.localGenApproval} 改用 generate_image。`, true);
192
-
193
- const outDir = path.join(sharedRoot || workspaceRoot, 'assets', 'generated');
194
- await fs.mkdir(outDir, { recursive: true });
195
- const signal = ctx?.abortController?.signal;
196
- const batch = Date.now().toString(36);
197
- const lines = []; const failed = [];
198
-
199
- for (let i = 0; i < stills.length; i++) {
200
- const still = stills[i];
201
- still.jobId = `${batch}p${i}`;
202
- const seed = still.seed ?? ((Date.now() + i * 7919) % 1_000_000);
203
- const nBatch = Math.max(1, Math.min(8, still.batch || 1));
204
-
205
- // 参考图先推上盒子(h3box.py 的 LoadImage 只认盒子本地路径)
206
- const refRoot = sharedRoot || workspaceRoot;
207
- const refArgs = []; let refErr = null;
208
- for (const [slot, rel] of [
209
- ['init', still.init_image], ['control', still.control_image],
210
- ]) {
211
- if (!rel) continue;
212
- const r = await pushRef({ box, root: refRoot, relPath: rel, jobId: still.jobId, slot, signal });
213
- if (r.err) { refErr = r.err; break; }
214
- if (slot === 'init') {
215
- refArgs.push(`--init ${shq(r.remote)}`);
216
- if (still.denoise != null) refArgs.push(`--denoise ${still.denoise}`);
217
- } else {
218
- refArgs.push(`--control ${shq(r.remote)}`,
219
- `--control-type ${shq(still.control_type || 'openpose')}`,
220
- `--control-strength ${still.control_strength ?? 0.7}`);
221
- }
222
- }
223
- // ref_image 可以是多张(逗号分隔,最多 5 张)—— 同一角色的不同视角一起喂,
224
- // 一致性明显强于单张。每张各自推上盒子,再按同样顺序拼回去。
225
- if (!refErr && still.ref_image) {
226
- const rels = String(still.ref_image).split(',').map((s) => s.trim()).filter(Boolean);
227
- if (rels.length > 5) {
228
- refErr = `ref_image 最多 5 张,给了 ${rels.length}`;
229
- } else {
230
- const remotes = [];
231
- for (let k = 0; k < rels.length; k++) {
232
- const r = await pushRef({
233
- box, root: refRoot, relPath: rels[k], jobId: still.jobId, slot: `ref${k}`, signal,
234
- });
235
- if (r.err) { refErr = r.err; break; }
236
- remotes.push(r.remote);
237
- }
238
- if (!refErr) {
239
- refArgs.push(`--ref ${shq(remotes.join(','))}`,
240
- `--ref-weight ${shq(String(still.ref_weight ?? '0.8'))}`);
241
- if (still.ref_mode) refArgs.push(`--ref-mode ${shq(still.ref_mode)}`);
242
- if (still.ref_combine) refArgs.push(`--ref-combine ${shq(still.ref_combine)}`);
243
- if (still.ref_preset) refArgs.push(`--ref-preset ${shq(still.ref_preset)}`);
244
- }
245
- }
246
- }
247
- if (refErr) { failed.push(`[${still.name}] ${refErr}`); break; }
248
-
249
- const remoteCmd = [
250
- 'python3 ~/h3box.py image',
251
- `-p ${shq(still.prompt)}`,
252
- `--model ${still.model}`,
253
- `--seed ${seed}`,
254
- `--size ${still.size}`,
255
- `--name ${still.jobId}`,
256
- nBatch > 1 ? `--batch ${nBatch}` : '',
257
- still.negative ? `--neg ${shq(still.negative)}` : '',
258
- still.lora ? `--lora ${shq(still.lora)} --lora-strength ${shq(String(still.lora_strength ?? '0.8'))}` : '',
259
- ...refArgs,
260
- ].filter(Boolean).join(' ');
261
-
262
- const t0 = Date.now();
263
- const gen = await runBox(box, 'ssh', [...sshArgs(box), remoteCmd],
264
- { timeoutMs: timeoutFor({ ...still, batch: nBatch }), signal });
265
- let failMsg = null; const bufs = [];
266
- if (gen.code !== 0) {
267
- failMsg = gen.code === 255 ? `盒子连不上(没开机/地址过期):${(gen.err || '').slice(-300)}`
268
- : `生成失败 exit ${gen.code}:${(gen.err || gen.out).slice(-500)}`;
269
- } else {
270
- const remotePaths = gen.out.split('\n').map((l) => l.trim())
271
- .filter((l) => l.includes('/outputs/') && /\.(png|webp|jpg)$/.test(l));
272
- if (!remotePaths.length) failMsg = `盒子跑完但没报出文件路径:${gen.out.slice(-400)}`;
273
- // batch 的 N 张全取回来,别只拿第一张(08-11 前就是丢了后面全部)。
274
- // 多源拼进一次 scp —— 逐张各开连接的老写法,握手开销能跟生成时间打平(08-11 实测)
275
- if (!failMsg && remotePaths.length) {
276
- const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `h3pull-${still.jobId}-`));
277
- const pull = await runBox(box, 'scp',
278
- [...scpArgs(box), ...remotePaths.map((r) => `${box.target}:${r}`), tmpDir],
279
- { timeoutMs: 60_000 + 20_000 * remotePaths.length });
280
- if (pull.code !== 0) failMsg = `取图失败:${(pull.err || '').slice(-300)}`;
281
- else {
282
- try {
283
- for (const r of remotePaths) bufs.push(await fs.readFile(path.join(tmpDir, path.basename(r))));
284
- } catch (e) { failMsg = `取图落盘缺文件:${e.message}`; }
285
- }
286
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { /* */ });
287
- }
288
- }
289
- const wallS = Math.round((Date.now() - t0) / 1000);
290
- for (let k = 0; k < bufs.length; k++) {
291
- lines.push(await landStill({
292
- imgBuf: bufs[k], still, seed: seed + k, outDir, ctx, sessionId, wallS,
293
- idx: k, total: bufs.length,
294
- }));
295
- }
296
- if (!bufs.length) {
297
- failed.push(`[${still.name}] ${failMsg}`);
298
- break; // 串行批中途失败即停:后张大概率同因,别空烧
299
- }
300
- if (failMsg) failed.push(`[${still.name}] 部分失败:${failMsg}`);
301
- if (signal?.aborted) break;
302
- }
303
-
304
- // ── 标签体检(2026-08-18)──
305
- // danbooru 系模型的标签写错不报错、只静默失效,一个 agent 因此白烧过至少
306
- // 五轮出图。fail-open:查不到就什么都不说。位置有讲究 —— 放在"看图挑废图"
307
- // 那句**之前**,因为它说的是"这批图可能根本不该按它判断方向"。
308
- let tagNote = null;
309
- try {
310
- const danbooruStills = stills.filter(s => ['noobai', 'noobai-eps', 'pony'].includes(s.model ?? 'noobai'));
311
- if (danbooruStills.length) {
312
- const { lintTags, formatTagLint } = await import('../../../lib/danbooru-tags.js');
313
- tagNote = formatTagLint(await lintTags(
314
- danbooruStills.flatMap(s => [s.prompt, s.negative].filter(Boolean)),
315
- ));
316
- }
317
- } catch { /* 体检本身不能变成新的故障源 */ }
318
-
319
- const head = `Batch done ${lines.length}/${stills.length} stills`;
320
- // ("产物可以看、只挑技术性废图"08-21 起只写在工具描述和 prelude 里,不再每批返回都带)
321
- if (failed.length) {
322
- return asText([head, ...lines, 'FAILED(批在此中断):', ...failed, tagNote]
323
- .filter(Boolean).join('\n'), lines.length === 0);
324
- }
325
- return asText([head, ...lines, tagNote].filter(Boolean).join('\n'));
326
- } catch (err) {
327
- return asText(`paint_still 失败:${err.message}`, true);
328
- }
329
- }
330
-
331
- /**
332
- * @param {object} deps
333
- */
334
- export function makePaintStillTool(deps) {
335
- return tool(
336
- 'paint_still',
337
- (localBoxEnabled()
338
- ? ''
339
- : '⛔ CURRENTLY UNAVAILABLE — the owner powers this GPU box on and off by hand, '
340
- + 'and it is off right now. Do not call this tool. Use generate_image instead, '
341
- + 'and tell the user the local box is off if they asked for it specifically.\n\n')
342
- + `Generate anime/illustration images on the owner's local GPU box. One call =
343
- 1-16 stills rendered serially; each finished image lands on the canvas
344
- immediately. Each still can also set batch=N to get N variations of the SAME
345
- prompt from one sampling pass — far cheaper than N separate stills, and it is
346
- the right way to "roll" for a good result.
347
-
348
- Models per still:
349
- - "noobai" (default): NoobAI-XL V-Pred 1.0. Danbooru/e621 tags,
350
- comma-separated. Best anatomy and tag control for anime. ~25-40s.
351
- - "noobai-eps": NoobAI-XL 1.1 (epsilon). Same tag language. Use ONLY when a
352
- LoRA you need exists solely in an eps build — eps LoRAs on the v-pred model
353
- wash out or oversaturate.
354
- - "pony": Pony Diffusion V6 XL. Quality prefix is added box-side; write plain
355
- danbooru-ish tags. Its LoRA ecosystem is separate from NoobAI's and the two
356
- do NOT interchange. ~25-40s.
357
- - "anima": natural-language English. ~20-40s.
358
- - "krea2": Krea 2 Turbo 12B — natural-language English, aesthetic-first, good
359
- photoreal/editorial. 8-step, seconds per image once warm (first call after
360
- another model ~1 min to load 24GB). The negative field is a NO-OP here.
361
-
362
- REFERENCE IMAGES (SDXL models: noobai / noobai-eps / pony). Three independent
363
- channels, stackable, all taking workspace-relative paths:
364
- - ref_image -> IP-Adapter. Carries the CHARACTER'S LOOK across into a new
365
- picture. This is the right tool for "draw this character somewhere else".
366
- - control_image -> ControlNet. Locks pose/structure; prompt drives the rest.
367
- - init_image -> img2img. Repaints on top of the given picture (denoise 0.3 =
368
- touch-up, 0.6 default, 0.8 = loose reinterpretation).
369
- Combine them: ref_image for who it is + control_image for the pose + prompt
370
- for the scene is the strongest setup for keeping a character consistent.
371
-
372
- Quality prefixes and per-model sampler settings are applied box-side — do not
373
- repeat them in the prompt. LoRA trigger words DO have to be written into the
374
- prompt yourself; the cookbook lists them.
375
-
376
- TAG DISCIPLINE (noobai / noobai-eps / pony): pure comma-separated tags, no
377
- sentences, spaces not underscores. Before the FIRST still of a new subject or
378
- scene: understand what the user wants → write candidate tags → run
379
- lookup_tags ONCE on all of them → paint with the verified ones. Re-rolls of
380
- the same scene need no new lookup. The return of this tool carries a tag
381
- check-up (weak/missing/sentence-like fragments) — fix those before judging
382
- the batch.
383
-
384
- Use for anime needs and video keyframes (1344x768 matches the video lane).
385
- Requires the box online — if unreachable, tell the user and fall back to
386
- generate_image.
387
-
388
- Outputs land at assets/generated/still-*.png. You MAY look at them to catch
389
- technical write-offs — duplicated figures, broken limbs, colour cast, all
390
- black/white, stray watermark text — and just re-roll those yourself. Taste and
391
- style direction stay the user's call: do not re-roll because you dislike it, and
392
- do not tell the user which one is better. Default detail is enough to spot
393
- breakage; do not burn high-detail on every frame.`,
394
- {
395
- stills: z.array(z.object({
396
- prompt: z.string().describe('danbooru/e621 tags (noobai/noobai-eps/pony) or natural English (anima/krea2)'),
397
- model: z.enum(['noobai', 'noobai-eps', 'pony', 'anima', 'krea2']).default('noobai'),
398
- negative: z.string().optional().describe('overrides the per-model default; no-op for krea2'),
399
- size: z.string().regex(/^\d{3,4}x\d{3,4}$/).default('1344x768'),
400
- seed: z.number().int().optional().describe('omit for fresh random per still; batch uses seed, seed+1, ...'),
401
- name: z.string().regex(/^[\w-]{1,40}$/).default('still'),
402
- batch: z.number().int().min(1).max(8).optional()
403
- .describe('variations of this same prompt in one pass (default 1). Use 4-8 to roll for a keeper.'),
404
- lora: z.string().optional()
405
- .describe('LoRA filename(s) in the box loras/ dir, comma-separated for stacking. Only names from the cookbook or given by the user.'),
406
- lora_strength: z.string().optional()
407
- .describe('single value applied to all, or comma-separated per LoRA. Default "0.8". Slider-type LoRAs want 2-4 and accept negatives — do not assume 0-1.'),
408
- // ---- 参考图三路,可叠加。路径都是工作区相对路径 ----
409
- init_image: z.string().optional()
410
- .describe('img2img base, workspace-relative path. Redraws ON TOP of this image. SDXL models only.'),
411
- denoise: z.number().min(0.1).max(1).optional()
412
- .describe('with init_image only. Default 0.6. Lower = closer to the original (0.3 = light touch-up, 0.8 = loose reinterpretation).'),
413
- control_image: z.string().optional()
414
- .describe('ControlNet reference, workspace-relative. Locks POSE/STRUCTURE while the prompt decides everything else.'),
415
- control_type: z.enum(['openpose', 'depth', 'canny', 'lineart', 'scribble', 'none'])
416
- .optional().describe('what to extract from control_image. Default openpose. "none" = image is already a processed control map.'),
417
- control_strength: z.number().min(0).max(2).optional().describe('default 0.7'),
418
- ref_image: z.string().optional()
419
- .describe('IP-Adapter reference(s), workspace-relative, comma-separated for UP TO 5. Transfers the CHARACTER LOOK into a new picture — the one for "draw my character somewhere else". Feeding 2-4 shots of the same character from different angles is markedly more consistent than one.'),
420
- ref_weight: z.string().optional()
421
- .describe('single value for all, or comma-separated per reference. Default "0.8". Higher = closer to the reference, less obedient to the prompt.'),
422
- ref_mode: z.enum(['style and composition', 'style transfer', 'composition',
423
- 'strong style transfer', 'style transfer precise', 'composition precise']).optional()
424
- .describe('what to carry over. Default "style and composition". Use "style transfer" to take the look but NOT the layout — usually what you want when moving a character to a new scene.'),
425
- ref_combine: z.enum(['concat', 'add', 'subtract', 'average', 'norm average', 'max', 'min'])
426
- .optional().describe('how multiple references merge. Default concat. "average" is calmer when the refs disagree.'),
427
- ref_preset: z.enum(['PLUS (high strength)', 'PLUS FACE (portraits)',
428
- 'STANDARD (medium strength)', 'VIT-G (medium strength)']).optional()
429
- .describe('IP-Adapter weight set. Default PLUS. Switch to "PLUS FACE (portraits)" when the point is keeping a FACE consistent.'),
430
- })).min(1).max(16).describe('stills rendered serially; each may itself batch'),
431
- },
432
- (args) => paintStills(deps, args),
433
- );
434
- }
1
+ /**
2
+ * mcp/tools/paint-still.js — paint_still MCP tool(2026-08-08;批量制+FLUX.2 同晚补上)
3
+ *
4
+ * 站主本地 GPU 盒子生图。一次调用 = 一批(1-16 条,串行渲、出一张上墙一张);
5
+ * 每条还可以再开 batch(同提示词多变体,一次采样出 N 张,抽卡用,比串行快数倍)。
6
+ *
7
+ * 模型档(08-11 扩到五档):
8
+ * noobai NoobAI-XL V-Pred 1.0,danbooru/e621 标签,解剖与标签理解最强
9
+ * noobai-eps NoobAI-XL 1.1 eps,只在 LoRA 仅有 eps 版时用(LoRA 跨预测目标会发灰)
10
+ * pony Pony Diffusion V6 XL,score_9 六段串体系,clip skip 2 已在盒端配好
11
+ * anima 自然语言英文
12
+ * krea2 Krea 2 Turbo 12B 审美向,自然语言
13
+ *
14
+ * ⚠️ 许可:noobai/noobai-eps **禁止任何形式商业化,含生成物**(Laxhar Lab 在 FAIPL
15
+ * 之上自加条款);pony **禁止在任何货币化的站点/应用上跑推理**;anima 非商用。
16
+ * 三者都只适合站主自用,不能做成对外服务。要商用走 Illustrious-XL(RAIL++-M
17
+ * 明文允许 SaaS)或 krea2。08-11 前这里写着 noobai"商用可",是错的。
18
+ *
19
+ * 配方 = 08-08/08-11 实测 + 官方模板。盒子不在线就明说,不静默降级。
20
+ * 落盘/缩略图/事件照 generate-image.js;只回文本路径(图不进返回值),
21
+ * 但 agent 可以自己去看那些文件挑废图(2026-08-18 解禁),审美判断仍归用户。
22
+ */
23
+
24
+ import os from 'node:os';
25
+ import path from 'node:path';
26
+ import fs from 'node:fs/promises';
27
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
28
+ import { z } from 'zod';
29
+ import sharp from 'sharp';
30
+ import { Events } from '../../agent/events.js';
31
+ import { getProject } from '../../../projects/store.js';
32
+ import { getUserById } from '../../../auth/users-store.js';
33
+ import { can, localGenApproved, DENIAL } from '../../../auth/tier.js';
34
+ import {
35
+ THUMBNAIL_MAX_DIM, THUMBNAIL_QUALITY, enqueueWarm, warmSpecsFor,
36
+ } from '../../../lib/image-variant.js';
37
+ import { boxConfig, shq, runBox, sshArgs, scpArgs, localBoxEnabled, BOX_OFF_MSG } from './h3box-ssh.js';
38
+
39
+ const SSH_TIMEOUT_MS = Number(process.env.NODESIGN_H3BOX_TIMEOUT_MS) || 240_000;
40
+ // krea2 bf16 24G 全驻卡;换模型后的首张要付一次装载(~1 分钟),给足余量
41
+ const TIMEOUT_BY_MODEL = {
42
+ noobai: SSH_TIMEOUT_MS, 'noobai-eps': SSH_TIMEOUT_MS, pony: SSH_TIMEOUT_MS,
43
+ anima: SSH_TIMEOUT_MS, krea2: 400_000,
44
+ };
45
+ // batch 会线性拉长单次渲染,超时按张数放大(封顶 10 分钟,别让挂死的活撑满)
46
+ const timeoutFor = (still) => Math.min(
47
+ (TIMEOUT_BY_MODEL[still.model] || SSH_TIMEOUT_MS) * Math.max(1, (still.batch || 1) * 0.6),
48
+ 600_000,
49
+ );
50
+
51
+ async function makeThumbnail(rawBuf) {
52
+ try {
53
+ const meta = await sharp(rawBuf).metadata();
54
+ const w = meta.width || 0; const h = meta.height || 0;
55
+ let pipeline = sharp(rawBuf);
56
+ if (Math.max(w, h) > THUMBNAIL_MAX_DIM) {
57
+ pipeline = pipeline.resize({
58
+ width: w >= h ? THUMBNAIL_MAX_DIM : null,
59
+ height: h > w ? THUMBNAIL_MAX_DIM : null,
60
+ fit: 'inside', withoutEnlargement: true,
61
+ });
62
+ }
63
+ const buf = await pipeline.webp({ quality: THUMBNAIL_QUALITY }).toBuffer();
64
+ return { buf, mimeType: 'image/webp' };
65
+ } catch (err) {
66
+ console.warn(`[paint-still] thumbnail failed (${err.message})`);
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * 把工作区里的参考图推到盒子,返回盒子上的绝对路径。
73
+ * 失败返回 {err},调用方据此中止这条 still —— 参考图丢了还照跑等于白烧。
74
+ */
75
+ /**
76
+ * 参考图推盒前先缩:上行实测只有 ~0.2MB/s(08-11),2MB 原图光上传就 10 秒。
77
+ * IP-Adapter 的 CLIP 端最终只吃 224px,ref 缩到 512 零损失;init/control 参与
78
+ * 生成分辨率,封顶 1344(SDXL 原生上限)。统一出 PNG 保透明;缩完反而更大
79
+ * (罕见,如高压缩 JPEG 转 PNG)就推原图。缩图失败也推原图,别因优化挂正事。
80
+ */
81
+ async function shrinkForPush(abs, slot) {
82
+ const maxDim = slot.startsWith('ref') ? 512 : 1344;
83
+ try {
84
+ const meta = await sharp(abs).metadata();
85
+ if (!meta.width || !meta.height || Math.max(meta.width, meta.height) <= maxDim) return null;
86
+ const buf = await sharp(abs)
87
+ .resize({ width: maxDim, height: maxDim, fit: 'inside', withoutEnlargement: true })
88
+ .png().toBuffer();
89
+ if (buf.length >= (await fs.stat(abs)).size) return null;
90
+ return buf;
91
+ } catch { return null; }
92
+ }
93
+
94
+ async function pushRef({ box, root, relPath, jobId, slot, signal }) {
95
+ const abs = path.resolve(root, relPath);
96
+ // 越狱防护:参考图必须在工作区内
97
+ if (!abs.startsWith(path.resolve(root))) return { err: `${slot} 路径越出工作区:${relPath}` };
98
+ try { await fs.access(abs); } catch { return { err: `${slot} 找不到文件:${relPath}` }; }
99
+ const shrunk = await shrinkForPush(abs, slot);
100
+ const remote = `~/refs/${jobId}-${slot}${shrunk ? '.png' : (path.extname(abs) || '.png')}`;
101
+ const mk = await runBox(box, 'ssh', [...sshArgs(box), 'mkdir -p ~/refs'], { timeoutMs: 30_000, signal });
102
+ if (mk.code !== 0) return { err: `盒子建 refs 目录失败:${(mk.err || '').slice(-200)}` };
103
+ let src = abs; let tmp = null;
104
+ if (shrunk) {
105
+ tmp = path.join(os.tmpdir(), `h3ref-${jobId}-${slot}.png`);
106
+ await fs.writeFile(tmp, shrunk);
107
+ src = tmp;
108
+ }
109
+ const put = await runBox(box, 'scp',
110
+ [...scpArgs(box), src, `${box.target}:${remote}`], { timeoutMs: 120_000, signal });
111
+ if (tmp) fs.unlink(tmp).catch(() => { /* */ });
112
+ if (put.code !== 0) return { err: `${slot} 上传失败:${(put.err || '').slice(-200)}` };
113
+ return { remote };
114
+ }
115
+
116
+ /** 单张落盘 + 缩略图 + sidecar + 上墙事件;返回 caption 行 */
117
+ async function landStill({ imgBuf, still, seed, outDir, ctx, sessionId, wallS, idx = 0, total = 1 }) {
118
+ // batch>1 时同一条 still 出 N 张,文件名加序号;单张时不加,保持老路径形态
119
+ const finalName = `still-${still.jobId}-${still.name}${total > 1 ? `-${idx + 1}` : ''}`;
120
+ const fileName = `${finalName}.png`;
121
+ const absOut = path.join(outDir, fileName);
122
+ await fs.writeFile(absOut, imgBuf);
123
+
124
+ const thumbDir = path.join(outDir, '.thumbnails');
125
+ await fs.mkdir(thumbDir, { recursive: true });
126
+ const thumb = await makeThumbnail(imgBuf);
127
+ let thumbAgentRelPath = null;
128
+ if (thumb) {
129
+ await fs.writeFile(path.join(thumbDir, `${finalName}.thumb.webp`), thumb.buf);
130
+ thumbAgentRelPath = path.posix.join('assets', 'generated', '.thumbnails', `${finalName}.thumb.webp`);
131
+ }
132
+ fs.stat(absOut)
133
+ .then((st) => enqueueWarm(absOut, st, warmSpecsFor()))
134
+ .catch(() => { /* 预热失败下次请求现编 */ });
135
+
136
+ try {
137
+ const metaDir = path.join(outDir, '.meta');
138
+ await fs.mkdir(metaDir, { recursive: true });
139
+ await fs.writeFile(path.join(metaDir, `${finalName}.json`), JSON.stringify({
140
+ prompt: still.prompt, negative: still.negative || null, provider: 'h3box',
141
+ model: still.model, seed, size: still.size,
142
+ sessionId: ctx?.sessionId || sessionId || null,
143
+ runId: ctx?.runId || null,
144
+ ts: new Date().toISOString(),
145
+ }, null, 2));
146
+ } catch (e) { console.warn(`[paint-still] meta sidecar failed: ${e.message}`); }
147
+
148
+ // 发**工作区相对路径**(fileChanged 的正字法,hooks 同款)——发绝对路径的话
149
+ // 前端寻址/版本记账全部静默失配(2026-08-14 普查改)
150
+ try { ctx?.emit?.(Events.fileChanged(path.posix.join('assets', 'generated', finalName), 'add')); } catch { /* fail-safe */ }
151
+ try {
152
+ ctx?.emit?.({
153
+ type: 'run.image_generated',
154
+ path: path.posix.join('assets', 'generated', fileName),
155
+ thumbnailPath: thumbAgentRelPath,
156
+ absPath: absOut,
157
+ sizeBytes: imgBuf.length,
158
+ thumbnailSizeBytes: thumb?.buf.length || null,
159
+ prompt: still.prompt, assetRole: null, aspectRatio: null, imageSize: still.size,
160
+ model: `h3box-${still.model}`,
161
+ referenceImageCount: (still.ref_image ? String(still.ref_image).split(',').filter((s) => s.trim()).length : 0)
162
+ + (still.init_image ? 1 : 0) + (still.control_image ? 1 : 0),
163
+ accompanyText: null,
164
+ });
165
+ } catch { /* fail-safe */ }
166
+
167
+ const tag = total > 1 ? `${still.name} #${idx + 1}/${total}` : still.name;
168
+ return `[${tag}] assets/generated/${fileName} — ${still.model} ${still.size} seed=${seed}${total > 1 ? `+${idx}` : ''} ${(imgBuf.length / 1024).toFixed(0)}KB ${wallS}s`;
169
+ }
170
+
171
+ /** 核心流程(从 handler 拆出,便于不起 SDK 直接实弹测试) */
172
+ export async function paintStills(
173
+ { workspaceRoot, sharedRoot, projectId, sessionId, ctx },
174
+ { stills },
175
+ ) {
176
+ const asText = (text, isError = false) =>
177
+ ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
178
+ try {
179
+ if (!localBoxEnabled()) {
180
+ return asText(BOX_OFF_MSG, true);
181
+ }
182
+ const box = boxConfig();
183
+ if (!box) {
184
+ return asText('本地生图盒子未配置(站主没开机或没设 NODESIGN_H3BOX_SSH)。转告用户,改用 generate_image。', true);
185
+ }
186
+ const project = getProject(projectId);
187
+ if (!project) return asText('错误:项目不存在', true);
188
+ const owner = project.ownerId ? getUserById(project.ownerId) : null;
189
+ // 档位闸 + 逐人批准(auth/tier.js):basic 档不开任何生图;pro 档还要被站主批过本地产线
190
+ if (!can(owner, 'localGen')) return asText(DENIAL.localGenTier, true);
191
+ if (!localGenApproved(owner)) return asText(`${DENIAL.localGenApproval} 改用 generate_image。`, true);
192
+
193
+ const outDir = path.join(sharedRoot || workspaceRoot, 'assets', 'generated');
194
+ await fs.mkdir(outDir, { recursive: true });
195
+ const signal = ctx?.abortController?.signal;
196
+ const batch = Date.now().toString(36);
197
+ const lines = []; const failed = [];
198
+
199
+ for (let i = 0; i < stills.length; i++) {
200
+ const still = stills[i];
201
+ still.jobId = `${batch}p${i}`;
202
+ const seed = still.seed ?? ((Date.now() + i * 7919) % 1_000_000);
203
+ const nBatch = Math.max(1, Math.min(8, still.batch || 1));
204
+
205
+ // 参考图先推上盒子(h3box.py 的 LoadImage 只认盒子本地路径)
206
+ const refRoot = sharedRoot || workspaceRoot;
207
+ const refArgs = []; let refErr = null;
208
+ for (const [slot, rel] of [
209
+ ['init', still.init_image], ['control', still.control_image],
210
+ ]) {
211
+ if (!rel) continue;
212
+ const r = await pushRef({ box, root: refRoot, relPath: rel, jobId: still.jobId, slot, signal });
213
+ if (r.err) { refErr = r.err; break; }
214
+ if (slot === 'init') {
215
+ refArgs.push(`--init ${shq(r.remote)}`);
216
+ if (still.denoise != null) refArgs.push(`--denoise ${still.denoise}`);
217
+ } else {
218
+ refArgs.push(`--control ${shq(r.remote)}`,
219
+ `--control-type ${shq(still.control_type || 'openpose')}`,
220
+ `--control-strength ${still.control_strength ?? 0.7}`);
221
+ }
222
+ }
223
+ // ref_image 可以是多张(逗号分隔,最多 5 张)—— 同一角色的不同视角一起喂,
224
+ // 一致性明显强于单张。每张各自推上盒子,再按同样顺序拼回去。
225
+ if (!refErr && still.ref_image) {
226
+ const rels = String(still.ref_image).split(',').map((s) => s.trim()).filter(Boolean);
227
+ if (rels.length > 5) {
228
+ refErr = `ref_image 最多 5 张,给了 ${rels.length}`;
229
+ } else {
230
+ const remotes = [];
231
+ for (let k = 0; k < rels.length; k++) {
232
+ const r = await pushRef({
233
+ box, root: refRoot, relPath: rels[k], jobId: still.jobId, slot: `ref${k}`, signal,
234
+ });
235
+ if (r.err) { refErr = r.err; break; }
236
+ remotes.push(r.remote);
237
+ }
238
+ if (!refErr) {
239
+ refArgs.push(`--ref ${shq(remotes.join(','))}`,
240
+ `--ref-weight ${shq(String(still.ref_weight ?? '0.8'))}`);
241
+ if (still.ref_mode) refArgs.push(`--ref-mode ${shq(still.ref_mode)}`);
242
+ if (still.ref_combine) refArgs.push(`--ref-combine ${shq(still.ref_combine)}`);
243
+ if (still.ref_preset) refArgs.push(`--ref-preset ${shq(still.ref_preset)}`);
244
+ }
245
+ }
246
+ }
247
+ if (refErr) { failed.push(`[${still.name}] ${refErr}`); break; }
248
+
249
+ const remoteCmd = [
250
+ 'python3 ~/h3box.py image',
251
+ `-p ${shq(still.prompt)}`,
252
+ `--model ${still.model}`,
253
+ `--seed ${seed}`,
254
+ `--size ${still.size}`,
255
+ `--name ${still.jobId}`,
256
+ nBatch > 1 ? `--batch ${nBatch}` : '',
257
+ still.negative ? `--neg ${shq(still.negative)}` : '',
258
+ still.lora ? `--lora ${shq(still.lora)} --lora-strength ${shq(String(still.lora_strength ?? '0.8'))}` : '',
259
+ ...refArgs,
260
+ ].filter(Boolean).join(' ');
261
+
262
+ const t0 = Date.now();
263
+ const gen = await runBox(box, 'ssh', [...sshArgs(box), remoteCmd],
264
+ { timeoutMs: timeoutFor({ ...still, batch: nBatch }), signal });
265
+ let failMsg = null; const bufs = [];
266
+ if (gen.code !== 0) {
267
+ failMsg = gen.code === 255 ? `盒子连不上(没开机/地址过期):${(gen.err || '').slice(-300)}`
268
+ : `生成失败 exit ${gen.code}:${(gen.err || gen.out).slice(-500)}`;
269
+ } else {
270
+ const remotePaths = gen.out.split('\n').map((l) => l.trim())
271
+ .filter((l) => l.includes('/outputs/') && /\.(png|webp|jpg)$/.test(l));
272
+ if (!remotePaths.length) failMsg = `盒子跑完但没报出文件路径:${gen.out.slice(-400)}`;
273
+ // batch 的 N 张全取回来,别只拿第一张(08-11 前就是丢了后面全部)。
274
+ // 多源拼进一次 scp —— 逐张各开连接的老写法,握手开销能跟生成时间打平(08-11 实测)
275
+ if (!failMsg && remotePaths.length) {
276
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), `h3pull-${still.jobId}-`));
277
+ const pull = await runBox(box, 'scp',
278
+ [...scpArgs(box), ...remotePaths.map((r) => `${box.target}:${r}`), tmpDir],
279
+ { timeoutMs: 60_000 + 20_000 * remotePaths.length });
280
+ if (pull.code !== 0) failMsg = `取图失败:${(pull.err || '').slice(-300)}`;
281
+ else {
282
+ try {
283
+ for (const r of remotePaths) bufs.push(await fs.readFile(path.join(tmpDir, path.basename(r))));
284
+ } catch (e) { failMsg = `取图落盘缺文件:${e.message}`; }
285
+ }
286
+ await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => { /* */ });
287
+ }
288
+ }
289
+ const wallS = Math.round((Date.now() - t0) / 1000);
290
+ for (let k = 0; k < bufs.length; k++) {
291
+ lines.push(await landStill({
292
+ imgBuf: bufs[k], still, seed: seed + k, outDir, ctx, sessionId, wallS,
293
+ idx: k, total: bufs.length,
294
+ }));
295
+ }
296
+ if (!bufs.length) {
297
+ failed.push(`[${still.name}] ${failMsg}`);
298
+ break; // 串行批中途失败即停:后张大概率同因,别空烧
299
+ }
300
+ if (failMsg) failed.push(`[${still.name}] 部分失败:${failMsg}`);
301
+ if (signal?.aborted) break;
302
+ }
303
+
304
+ // ── 标签体检(2026-08-18)──
305
+ // danbooru 系模型的标签写错不报错、只静默失效,一个 agent 因此白烧过至少
306
+ // 五轮出图。fail-open:查不到就什么都不说。位置有讲究 —— 放在"看图挑废图"
307
+ // 那句**之前**,因为它说的是"这批图可能根本不该按它判断方向"。
308
+ let tagNote = null;
309
+ try {
310
+ const danbooruStills = stills.filter(s => ['noobai', 'noobai-eps', 'pony'].includes(s.model ?? 'noobai'));
311
+ if (danbooruStills.length) {
312
+ const { lintTags, formatTagLint } = await import('../../../lib/danbooru-tags.js');
313
+ tagNote = formatTagLint(await lintTags(
314
+ danbooruStills.flatMap(s => [s.prompt, s.negative].filter(Boolean)),
315
+ ));
316
+ }
317
+ } catch { /* 体检本身不能变成新的故障源 */ }
318
+
319
+ const head = `Batch done ${lines.length}/${stills.length} stills`;
320
+ // ("产物可以看、只挑技术性废图"08-21 起只写在工具描述和 prelude 里,不再每批返回都带)
321
+ if (failed.length) {
322
+ return asText([head, ...lines, 'FAILED(批在此中断):', ...failed, tagNote]
323
+ .filter(Boolean).join('\n'), lines.length === 0);
324
+ }
325
+ return asText([head, ...lines, tagNote].filter(Boolean).join('\n'));
326
+ } catch (err) {
327
+ return asText(`paint_still 失败:${err.message}`, true);
328
+ }
329
+ }
330
+
331
+ /**
332
+ * @param {object} deps
333
+ */
334
+ export function makePaintStillTool(deps) {
335
+ return tool(
336
+ 'paint_still',
337
+ (localBoxEnabled()
338
+ ? ''
339
+ : '⛔ CURRENTLY UNAVAILABLE — the owner powers this GPU box on and off by hand, '
340
+ + 'and it is off right now. Do not call this tool. Use generate_image instead, '
341
+ + 'and tell the user the local box is off if they asked for it specifically.\n\n')
342
+ + `Generate anime/illustration images on the owner's local GPU box. One call =
343
+ 1-16 stills rendered serially; each finished image lands on the canvas
344
+ immediately. Each still can also set batch=N to get N variations of the SAME
345
+ prompt from one sampling pass — far cheaper than N separate stills, and it is
346
+ the right way to "roll" for a good result.
347
+
348
+ Models per still:
349
+ - "noobai" (default): NoobAI-XL V-Pred 1.0. Danbooru/e621 tags,
350
+ comma-separated. Best anatomy and tag control for anime. ~25-40s.
351
+ - "noobai-eps": NoobAI-XL 1.1 (epsilon). Same tag language. Use ONLY when a
352
+ LoRA you need exists solely in an eps build — eps LoRAs on the v-pred model
353
+ wash out or oversaturate.
354
+ - "pony": Pony Diffusion V6 XL. Quality prefix is added box-side; write plain
355
+ danbooru-ish tags. Its LoRA ecosystem is separate from NoobAI's and the two
356
+ do NOT interchange. ~25-40s.
357
+ - "anima": natural-language English. ~20-40s.
358
+ - "krea2": Krea 2 Turbo 12B — natural-language English, aesthetic-first, good
359
+ photoreal/editorial. 8-step, seconds per image once warm (first call after
360
+ another model ~1 min to load 24GB). The negative field is a NO-OP here.
361
+
362
+ REFERENCE IMAGES (SDXL models: noobai / noobai-eps / pony). Three independent
363
+ channels, stackable, all taking workspace-relative paths:
364
+ - ref_image -> IP-Adapter. Carries the CHARACTER'S LOOK across into a new
365
+ picture. This is the right tool for "draw this character somewhere else".
366
+ - control_image -> ControlNet. Locks pose/structure; prompt drives the rest.
367
+ - init_image -> img2img. Repaints on top of the given picture (denoise 0.3 =
368
+ touch-up, 0.6 default, 0.8 = loose reinterpretation).
369
+ Combine them: ref_image for who it is + control_image for the pose + prompt
370
+ for the scene is the strongest setup for keeping a character consistent.
371
+
372
+ Quality prefixes and per-model sampler settings are applied box-side — do not
373
+ repeat them in the prompt. LoRA trigger words DO have to be written into the
374
+ prompt yourself; the cookbook lists them.
375
+
376
+ TAG DISCIPLINE (noobai / noobai-eps / pony): pure comma-separated tags, no
377
+ sentences, spaces not underscores. Before the FIRST still of a new subject or
378
+ scene: understand what the user wants → write candidate tags → run
379
+ lookup_tags ONCE on all of them → paint with the verified ones. Re-rolls of
380
+ the same scene need no new lookup. The return of this tool carries a tag
381
+ check-up (weak/missing/sentence-like fragments) — fix those before judging
382
+ the batch.
383
+
384
+ Use for anime needs and video keyframes (1344x768 matches the video lane).
385
+ Requires the box online — if unreachable, tell the user and fall back to
386
+ generate_image.
387
+
388
+ Outputs land at assets/generated/still-*.png. You MAY look at them to catch
389
+ technical write-offs — duplicated figures, broken limbs, colour cast, all
390
+ black/white, stray watermark text — and just re-roll those yourself. Taste and
391
+ style direction stay the user's call: do not re-roll because you dislike it, and
392
+ do not tell the user which one is better. Default detail is enough to spot
393
+ breakage; do not burn high-detail on every frame.`,
394
+ {
395
+ stills: z.array(z.object({
396
+ prompt: z.string().describe('danbooru/e621 tags (noobai/noobai-eps/pony) or natural English (anima/krea2)'),
397
+ model: z.enum(['noobai', 'noobai-eps', 'pony', 'anima', 'krea2']).default('noobai'),
398
+ negative: z.string().optional().describe('overrides the per-model default; no-op for krea2'),
399
+ size: z.string().regex(/^\d{3,4}x\d{3,4}$/).default('1344x768'),
400
+ seed: z.number().int().optional().describe('omit for fresh random per still; batch uses seed, seed+1, ...'),
401
+ name: z.string().regex(/^[\w-]{1,40}$/).default('still'),
402
+ batch: z.number().int().min(1).max(8).optional()
403
+ .describe('variations of this same prompt in one pass (default 1). Use 4-8 to roll for a keeper.'),
404
+ lora: z.string().optional()
405
+ .describe('LoRA filename(s) in the box loras/ dir, comma-separated for stacking. Only names from the cookbook or given by the user.'),
406
+ lora_strength: z.string().optional()
407
+ .describe('single value applied to all, or comma-separated per LoRA. Default "0.8". Slider-type LoRAs want 2-4 and accept negatives — do not assume 0-1.'),
408
+ // ---- 参考图三路,可叠加。路径都是工作区相对路径 ----
409
+ init_image: z.string().optional()
410
+ .describe('img2img base, workspace-relative path. Redraws ON TOP of this image. SDXL models only.'),
411
+ denoise: z.number().min(0.1).max(1).optional()
412
+ .describe('with init_image only. Default 0.6. Lower = closer to the original (0.3 = light touch-up, 0.8 = loose reinterpretation).'),
413
+ control_image: z.string().optional()
414
+ .describe('ControlNet reference, workspace-relative. Locks POSE/STRUCTURE while the prompt decides everything else.'),
415
+ control_type: z.enum(['openpose', 'depth', 'canny', 'lineart', 'scribble', 'none'])
416
+ .optional().describe('what to extract from control_image. Default openpose. "none" = image is already a processed control map.'),
417
+ control_strength: z.number().min(0).max(2).optional().describe('default 0.7'),
418
+ ref_image: z.string().optional()
419
+ .describe('IP-Adapter reference(s), workspace-relative, comma-separated for UP TO 5. Transfers the CHARACTER LOOK into a new picture — the one for "draw my character somewhere else". Feeding 2-4 shots of the same character from different angles is markedly more consistent than one.'),
420
+ ref_weight: z.string().optional()
421
+ .describe('single value for all, or comma-separated per reference. Default "0.8". Higher = closer to the reference, less obedient to the prompt.'),
422
+ ref_mode: z.enum(['style and composition', 'style transfer', 'composition',
423
+ 'strong style transfer', 'style transfer precise', 'composition precise']).optional()
424
+ .describe('what to carry over. Default "style and composition". Use "style transfer" to take the look but NOT the layout — usually what you want when moving a character to a new scene.'),
425
+ ref_combine: z.enum(['concat', 'add', 'subtract', 'average', 'norm average', 'max', 'min'])
426
+ .optional().describe('how multiple references merge. Default concat. "average" is calmer when the refs disagree.'),
427
+ ref_preset: z.enum(['PLUS (high strength)', 'PLUS FACE (portraits)',
428
+ 'STANDARD (medium strength)', 'VIT-G (medium strength)']).optional()
429
+ .describe('IP-Adapter weight set. Default PLUS. Switch to "PLUS FACE (portraits)" when the point is keeping a FACE consistent.'),
430
+ })).min(1).max(16).describe('stills rendered serially; each may itself batch'),
431
+ },
432
+ (args) => paintStills(deps, args),
433
+ );
434
+ }