@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,266 +1,266 @@
1
- /**
2
- * mcp/tools/roll-film.js — roll_film MCP tool(2026-08-08;批量制同晚补上)
3
- *
4
- * 自部署 MiniMax-H3 视频产线。一次调用 = 一批镜头(1-16 条),全批共用一个
5
- * seed(成片纪律);盒上串行渲,每出一镜当场落盘上墙。后端两档:
6
- * box(默认) 站主 5090 盒子(h3box.py video over SSH),模型常驻零冷启动。
7
- * modal Modal H100+sage 备用档,NODESIGN_FILM_BACKEND=modal 显式才走。
8
- * 配方恒定:Turbo 8 步 / 1344×768 / 单镜 ≤12.25s。批准制:admin+获批账号。
9
- * 只回文本路径,不回 image block;agent 可以自己抽帧看(2026-08-18 解禁),
10
- * 但"这条好不好"归用户判。
11
- */
12
-
13
- import os from 'node:os';
14
- import path from 'node:path';
15
- import fs from 'node:fs/promises';
16
- import { spawn } from 'node:child_process';
17
- import { tool } from '@anthropic-ai/claude-agent-sdk';
18
- import { z } from 'zod';
19
- import { Events } from '../../agent/events.js';
20
- import { getProject } from '../../../projects/store.js';
21
- import { getUserById } from '../../../auth/users-store.js';
22
- import { can, localGenApproved, DENIAL } from '../../../auth/tier.js';
23
- import { boxConfig, runBox, sshArgs, scpArgs, localBoxEnabled, BOX_OFF_MSG } from './h3box-ssh.js';
24
-
25
- const H3_REPO = process.env.NODESIGN_H3_REPO || '/home/wangang-dev/projects/minimax-h3-modal';
26
- const MODAL_BIN = process.env.NODESIGN_MODAL_BIN || path.join(os.homedir(), '.local/bin/modal');
27
- const PER_SHOT_TIMEOUT_MS = Number(process.env.NODESIGN_FILM_TIMEOUT_MS) || 900_000;
28
-
29
- function frameCount(durationS) {
30
- const f = Math.max(5, Math.round(durationS * 24));
31
- return f + ((5 - (f % 17)) % 17 + 17) % 17;
32
- }
33
-
34
- function estCostUsd(d, backend) {
35
- const pts = backend === 'modal'
36
- ? [[5.17, 0.10], [8, 0.16], [10, 0.21], [12.25, 0.28]]
37
- : [[5.17, 0.015], [8, 0.02], [12.25, 0.03]];
38
- if (d <= pts[0][0]) return pts[0][1];
39
- for (let i = 1; i < pts.length; i++) {
40
- if (d <= pts[i][0]) {
41
- const [x0, y0] = pts[i - 1]; const [x1, y1] = pts[i];
42
- return Math.round((y0 + (d - x0) * (y1 - y0) / (x1 - x0)) * 1000) / 1000;
43
- }
44
- }
45
- return pts[pts.length - 1][1];
46
- }
47
-
48
- function runModal(args, { cwd, signal, timeoutMs }) {
49
- return new Promise((resolve) => {
50
- const child = spawn(MODAL_BIN, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
51
- let out = ''; let err = '';
52
- child.stdout.on('data', (d) => { out = (out + d).slice(-8000); });
53
- child.stderr.on('data', (d) => { err = (err + d).slice(-4000); });
54
- const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* */ } }, timeoutMs);
55
- const onAbort = () => { try { child.kill('SIGKILL'); } catch { /* */ } };
56
- signal?.addEventListener?.('abort', onAbort, { once: true });
57
- child.on('close', (code) => {
58
- clearTimeout(timer);
59
- signal?.removeEventListener?.('abort', onAbort);
60
- resolve({ code, out, err });
61
- });
62
- child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, out, err: String(e.message) }); });
63
- });
64
- }
65
-
66
- /** 单镜落盘 + sidecar + 上墙事件;返回 caption 行 */
67
- async function landShot({ localMp4, shot, seed, backend, wallS, outDir, ctx, sessionId }) {
68
- const fileName = `${shot.name}-${shot.jobId.slice(5)}.mp4`;
69
- const absOut = path.join(outDir, fileName);
70
- await fs.rename(localMp4, absOut).catch(async () => {
71
- await fs.copyFile(localMp4, absOut); await fs.unlink(localMp4).catch(() => { /* */ });
72
- });
73
- const sizeBytes = (await fs.stat(absOut)).size;
74
- const cost = estCostUsd(shot.duration, backend);
75
- try {
76
- const metaDir = path.join(outDir, '.meta');
77
- await fs.mkdir(metaDir, { recursive: true });
78
- await fs.writeFile(path.join(metaDir, `${path.parse(fileName).name}.json`), JSON.stringify({
79
- prompt: shot.prompt, kind: 'film', durationS: shot.duration, frames: frameCount(shot.duration),
80
- seed, model: backend === 'modal' ? 'minimax-h3-turbo8-h100-sage' : 'minimax-h3-turbo8-5090-sage2',
81
- estCostUsd: cost, wallClockS: wallS,
82
- firstFrame: shot.first_frame || null, lastFrame: shot.last_frame || null,
83
- sessionId: ctx?.sessionId || sessionId || null, runId: ctx?.runId || null,
84
- ts: new Date().toISOString(),
85
- }, null, 2));
86
- } catch (e) { console.warn(`[roll-film] meta sidecar failed: ${e.message}`); }
87
- // MCP 工具写盘不走自动 file_changed —— 手动发,出一镜上墙一镜
88
- // 工作区相对路径(fileChanged 正字法,2026-08-14 普查改;绝对路径=前端寻址哑弹)
89
- try { ctx?.emit?.(Events.fileChanged(path.posix.join('assets', 'generated', fileName), 'add')); } catch { /* */ }
90
- return `[${shot.name}] assets/generated/${fileName} — ${shot.duration}s ${(sizeBytes / 1e6).toFixed(1)}MB ${Math.floor(wallS / 60)}m${wallS % 60}s ~$${cost}`;
91
- }
92
-
93
- export async function rollFilm(
94
- { workspaceRoot, sharedRoot, projectId, sessionId, ctx },
95
- { shots, seed },
96
- ) {
97
- const asText = (text, isError = false) =>
98
- ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
99
- try {
100
- const project = getProject(projectId);
101
- if (!project) return asText('错误:项目不存在', true);
102
- const owner = project.ownerId ? getUserById(project.ownerId) : null;
103
- if (!owner) return asText('错误:找不到项目归属用户', true);
104
- // 档位闸 + 逐人批准(auth/tier.js):basic 档不开本地产线;pro 档还要被站主批过
105
- if (!can(owner, 'localGen')) return asText(DENIAL.localGenTier, true);
106
- if (!localGenApproved(owner)) return asText(DENIAL.localGenApproval, true);
107
-
108
- // 全批先验完再花钱:帧域 + 关键帧存在性 + 名字唯一
109
- const names = new Set();
110
- for (const s of shots) {
111
- const frames = frameCount(s.duration);
112
- if (frames > 294) return asText(`[${s.name}] ${frames} 帧超产线安全域 294(12.25s),拆镜`, true);
113
- if (names.has(s.name)) return asText(`镜名重复:${s.name}`, true);
114
- names.add(s.name);
115
- for (const slot of ['first_frame', 'last_frame']) {
116
- if (!s[slot]) continue;
117
- const abs = path.resolve(workspaceRoot || process.cwd(), s[slot]);
118
- try {
119
- if (!(await fs.stat(abs)).isFile()) throw new Error('x');
120
- } catch { return asText(`[${s.name}] ${slot} 找不到:${s[slot]}`, true); }
121
- s[`_abs_${slot}`] = abs;
122
- }
123
- }
124
-
125
- const backend = (process.env.NODESIGN_FILM_BACKEND || 'box').toLowerCase();
126
- // 只拦 box 后端:modal 是另一台机器,站主关本地盒子不影响它
127
- if (backend === 'box' && !localBoxEnabled()) {
128
- return asText(BOX_OFF_MSG, true);
129
- }
130
- const signal = ctx?.abortController?.signal;
131
- const outDir = path.join(sharedRoot || workspaceRoot, 'assets', 'generated');
132
- await fs.mkdir(outDir, { recursive: true });
133
- const batch = Date.now().toString(36);
134
- const lines = []; const failed = [];
135
-
136
- let box = null;
137
- if (backend !== 'modal') {
138
- box = boxConfig();
139
- if (!box) return asText('视频盒子未配置(站主没开机)。转告用户;备用 Modal 档要站主设 NODESIGN_FILM_BACKEND=modal。', true);
140
- const mk = await runBox(box, 'ssh', [...sshArgs(box), 'mkdir -p nd_jobs'], { timeoutMs: 30_000, signal });
141
- if (mk.code !== 0) return asText(`盒子连不上(没开机/地址过期):\n${(mk.err || '').slice(-400)}\n转告用户。`, true);
142
- }
143
-
144
- // 逐镜串行:一镜完整走完(渲→取回→上墙)再下一镜,agent 中途被打断也保住已出的
145
- for (let i = 0; i < shots.length; i++) {
146
- const shot = shots[i];
147
- shot.jobId = `film-${batch}s${i}`;
148
- const job = { name: shot.jobId, prompt: shot.prompt, seed, duration: shot.duration, width: 1344, height: 768 };
149
- if (shot.steps) job.steps = shot.steps;
150
- const t0 = Date.now();
151
- let localMp4 = null; let failMsg = null;
152
-
153
- if (backend === 'modal') {
154
- if (shot._abs_first_frame) job.first_frame = shot._abs_first_frame;
155
- if (shot._abs_last_frame) job.last_frame = shot._abs_last_frame;
156
- const jp = path.join(os.tmpdir(), `nd-${shot.jobId}.json`);
157
- await fs.writeFile(jp, JSON.stringify([job]));
158
- const r = await runModal(['run', 'h3_comfy.py', '--jobs-file', jp], { cwd: H3_REPO, signal, timeoutMs: PER_SHOT_TIMEOUT_MS });
159
- fs.unlink(jp).catch(() => { /* */ });
160
- if (r.code !== 0) failMsg = (r.err || r.out).slice(-600);
161
- else {
162
- const hits = (await fs.readdir(path.join(H3_REPO, 'outputs'))).filter((f) => f.startsWith(`${shot.jobId}_`) && f.endsWith('.mp4'));
163
- if (hits.length) localMp4 = path.join(H3_REPO, 'outputs', hits[0]);
164
- else failMsg = `无成片。输出尾:${r.out.slice(-400)}`;
165
- }
166
- } else {
167
- for (const slot of ['first_frame', 'last_frame']) {
168
- const abs = shot[`_abs_${slot}`];
169
- if (!abs) continue;
170
- const remote = `nd_jobs/${shot.jobId}-${slot}${path.extname(abs) || '.png'}`;
171
- const up = await runBox(box, 'scp', [...scpArgs(box), abs, `${box.target}:${remote}`], { timeoutMs: 60_000, signal });
172
- if (up.code !== 0) { failMsg = `关键帧上传失败:${up.err.slice(-300)}`; break; }
173
- job[slot] = `~/${remote}`;
174
- }
175
- if (!failMsg) {
176
- const jl = path.join(os.tmpdir(), `nd-${shot.jobId}.json`);
177
- await fs.writeFile(jl, JSON.stringify([job]));
178
- const upJ = await runBox(box, 'scp', [...scpArgs(box), jl, `${box.target}:nd_jobs/${shot.jobId}.json`], { timeoutMs: 30_000, signal });
179
- fs.unlink(jl).catch(() => { /* */ });
180
- if (upJ.code !== 0) failMsg = `任务上传失败:${upJ.err.slice(-300)}`;
181
- else {
182
- const gen = await runBox(box, 'ssh', [...sshArgs(box), `python3 ~/h3box.py video ~/nd_jobs/${shot.jobId}.json`], { timeoutMs: PER_SHOT_TIMEOUT_MS, signal });
183
- if (gen.code !== 0 || !gen.out.includes('success')) failMsg = `渲染失败(exit ${gen.code}):${(gen.err || gen.out).slice(-600)}`;
184
- else {
185
- const pd = await fs.mkdtemp(path.join(os.tmpdir(), 'nd-film-'));
186
- const pull = await runBox(box, 'scp', [...scpArgs(box), `${box.target}:outputs/${shot.jobId}_*.mp4`, pd], { timeoutMs: 120_000, signal });
187
- const got = pull.code === 0 ? (await fs.readdir(pd)).filter((f) => f.endsWith('.mp4')) : [];
188
- if (got.length) localMp4 = path.join(pd, got[0]);
189
- else failMsg = `成片取回失败:${pull.err.slice(-300)}`;
190
- }
191
- }
192
- }
193
- }
194
-
195
- const wallS = Math.round((Date.now() - t0) / 1000);
196
- if (localMp4) {
197
- lines.push(await landShot({ localMp4, shot, seed, backend, wallS, outDir, ctx, sessionId }));
198
- } else {
199
- failed.push(`[${shot.name}] ${failMsg}`);
200
- if (signal?.aborted) break;
201
- break; // 串行批中途失败即停:后镜大概率同因失败,别空烧
202
- }
203
- }
204
-
205
- const head = `Batch done ${lines.length}/${shots.length} shots, seed=${seed}, via ${backend === 'modal' ? 'Modal H100+sage' : '5090 盒子 sage2'}`;
206
- // 2026-08-18:解禁。mp4 塞不进视觉通道,要看就 ffmpeg 抽帧再看那几张图。
207
- const tail = 'You may check these: pull two or three frames with ffmpeg and look at them '
208
- + 'for technical breakage (colour cast, duplicated figures, broken limbs, mush, first '
209
- + 'frame not matching the anchor) and re-roll those yourself; delete the temp frames after. '
210
- + 'Do NOT judge whether a shot is good — hand the paths to the user for that.';
211
- if (failed.length) {
212
- return asText([head, ...lines, 'FAILED(批在此中断,未渲镜不再空烧):', ...failed, tail].join('\n'), lines.length === 0);
213
- }
214
- return asText([head, ...lines, tail].join('\n'));
215
- } catch (err) {
216
- return asText(`roll_film 失败:${err.message}`, true);
217
- }
218
- }
219
-
220
- /**
221
- * @param {object} deps
222
- */
223
- export function makeRollFilmTool(deps) {
224
- return tool(
225
- 'roll_film',
226
- ((localBoxEnabled() || (process.env.NODESIGN_FILM_BACKEND || 'box').toLowerCase() === 'modal')
227
- ? ''
228
- : '⛔ CURRENTLY UNAVAILABLE — the owner powers this GPU box on and off by hand, '
229
- + 'and it is off right now. Do not call this tool; tell the user the local box is off.\n\n')
230
- + `Generate video shots (picture + native audio) on the owner's self-hosted
231
- MiniMax-H3 lane (RTX 5090 box, SageAttention, Turbo 8-step, 24fps). One call =
232
- one batch of 1-16 shots rendered back-to-back; each finished shot lands on the
233
- canvas immediately. Max 12.25s per shot — longer stories are multiple shots.
234
-
235
- Call ONLY when the user explicitly asks for video, and align the shot list with
236
- the user before rolling a multi-shot batch. Roughly 3-5 minutes per shot,
237
- serial; tell the user the camera is rolling. Never auto-retry shots you already
238
- got back. Requires the GPU box online — if unreachable, relay that and stop.
239
-
240
- Prompts must follow the H3 three-field English format (cookbook arrives as a
241
- system reminder attached to your FIRST call — treat your first batch as the
242
- learning pass and refine from it). Keep character/style blocks verbatim
243
- identical across shots; the whole batch shares ONE seed automatically. Keyframe
244
- anchors (first_frame/last_frame) accept paths like "assets/generated/kf1.png"
245
- (make them with paint_still or generate_image, ideally 1344x768).
246
-
247
- Clips land at assets/generated/<name>-*.mp4. To check one, pull a couple of
248
- frames with ffmpeg and look at those — catch colour cast, duplicated figures,
249
- broken limbs, mush, or a first frame that does not match its anchor, and re-roll
250
- those yourself (delete the temp frames after). Whether a shot is GOOD is the
251
- user's call, not yours: report paths
252
- and move on.`,
253
- {
254
- shots: z.array(z.object({
255
- prompt: z.string().describe('H3 three-field English prompt'),
256
- duration: z.number().min(5.2).max(12.25).default(8),
257
- name: z.string().regex(/^[\w-]{1,40}$/).describe('shot slug, unique in batch'),
258
- first_frame: z.string().optional(),
259
- last_frame: z.string().optional(),
260
- steps: z.number().int().min(4).max(8).optional().describe('default 8; 4 = fast draft'),
261
- })).min(1).max(16).describe('shots rendered serially in one batch'),
262
- seed: z.number().int().default(1101).describe('ONE seed shared by the whole batch (film discipline)'),
263
- },
264
- (args) => rollFilm(deps, args),
265
- );
266
- }
1
+ /**
2
+ * mcp/tools/roll-film.js — roll_film MCP tool(2026-08-08;批量制同晚补上)
3
+ *
4
+ * 自部署 MiniMax-H3 视频产线。一次调用 = 一批镜头(1-16 条),全批共用一个
5
+ * seed(成片纪律);盒上串行渲,每出一镜当场落盘上墙。后端两档:
6
+ * box(默认) 站主 5090 盒子(h3box.py video over SSH),模型常驻零冷启动。
7
+ * modal Modal H100+sage 备用档,NODESIGN_FILM_BACKEND=modal 显式才走。
8
+ * 配方恒定:Turbo 8 步 / 1344×768 / 单镜 ≤12.25s。批准制:admin+获批账号。
9
+ * 只回文本路径,不回 image block;agent 可以自己抽帧看(2026-08-18 解禁),
10
+ * 但"这条好不好"归用户判。
11
+ */
12
+
13
+ import os from 'node:os';
14
+ import path from 'node:path';
15
+ import fs from 'node:fs/promises';
16
+ import { spawn } from 'node:child_process';
17
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
18
+ import { z } from 'zod';
19
+ import { Events } from '../../agent/events.js';
20
+ import { getProject } from '../../../projects/store.js';
21
+ import { getUserById } from '../../../auth/users-store.js';
22
+ import { can, localGenApproved, DENIAL } from '../../../auth/tier.js';
23
+ import { boxConfig, runBox, sshArgs, scpArgs, localBoxEnabled, BOX_OFF_MSG } from './h3box-ssh.js';
24
+
25
+ const H3_REPO = process.env.NODESIGN_H3_REPO || '/home/wangang-dev/projects/minimax-h3-modal';
26
+ const MODAL_BIN = process.env.NODESIGN_MODAL_BIN || path.join(os.homedir(), '.local/bin/modal');
27
+ const PER_SHOT_TIMEOUT_MS = Number(process.env.NODESIGN_FILM_TIMEOUT_MS) || 900_000;
28
+
29
+ function frameCount(durationS) {
30
+ const f = Math.max(5, Math.round(durationS * 24));
31
+ return f + ((5 - (f % 17)) % 17 + 17) % 17;
32
+ }
33
+
34
+ function estCostUsd(d, backend) {
35
+ const pts = backend === 'modal'
36
+ ? [[5.17, 0.10], [8, 0.16], [10, 0.21], [12.25, 0.28]]
37
+ : [[5.17, 0.015], [8, 0.02], [12.25, 0.03]];
38
+ if (d <= pts[0][0]) return pts[0][1];
39
+ for (let i = 1; i < pts.length; i++) {
40
+ if (d <= pts[i][0]) {
41
+ const [x0, y0] = pts[i - 1]; const [x1, y1] = pts[i];
42
+ return Math.round((y0 + (d - x0) * (y1 - y0) / (x1 - x0)) * 1000) / 1000;
43
+ }
44
+ }
45
+ return pts[pts.length - 1][1];
46
+ }
47
+
48
+ function runModal(args, { cwd, signal, timeoutMs }) {
49
+ return new Promise((resolve) => {
50
+ const child = spawn(MODAL_BIN, args, { cwd, stdio: ['ignore', 'pipe', 'pipe'], env: process.env });
51
+ let out = ''; let err = '';
52
+ child.stdout.on('data', (d) => { out = (out + d).slice(-8000); });
53
+ child.stderr.on('data', (d) => { err = (err + d).slice(-4000); });
54
+ const timer = setTimeout(() => { try { child.kill('SIGKILL'); } catch { /* */ } }, timeoutMs);
55
+ const onAbort = () => { try { child.kill('SIGKILL'); } catch { /* */ } };
56
+ signal?.addEventListener?.('abort', onAbort, { once: true });
57
+ child.on('close', (code) => {
58
+ clearTimeout(timer);
59
+ signal?.removeEventListener?.('abort', onAbort);
60
+ resolve({ code, out, err });
61
+ });
62
+ child.on('error', (e) => { clearTimeout(timer); resolve({ code: -1, out, err: String(e.message) }); });
63
+ });
64
+ }
65
+
66
+ /** 单镜落盘 + sidecar + 上墙事件;返回 caption 行 */
67
+ async function landShot({ localMp4, shot, seed, backend, wallS, outDir, ctx, sessionId }) {
68
+ const fileName = `${shot.name}-${shot.jobId.slice(5)}.mp4`;
69
+ const absOut = path.join(outDir, fileName);
70
+ await fs.rename(localMp4, absOut).catch(async () => {
71
+ await fs.copyFile(localMp4, absOut); await fs.unlink(localMp4).catch(() => { /* */ });
72
+ });
73
+ const sizeBytes = (await fs.stat(absOut)).size;
74
+ const cost = estCostUsd(shot.duration, backend);
75
+ try {
76
+ const metaDir = path.join(outDir, '.meta');
77
+ await fs.mkdir(metaDir, { recursive: true });
78
+ await fs.writeFile(path.join(metaDir, `${path.parse(fileName).name}.json`), JSON.stringify({
79
+ prompt: shot.prompt, kind: 'film', durationS: shot.duration, frames: frameCount(shot.duration),
80
+ seed, model: backend === 'modal' ? 'minimax-h3-turbo8-h100-sage' : 'minimax-h3-turbo8-5090-sage2',
81
+ estCostUsd: cost, wallClockS: wallS,
82
+ firstFrame: shot.first_frame || null, lastFrame: shot.last_frame || null,
83
+ sessionId: ctx?.sessionId || sessionId || null, runId: ctx?.runId || null,
84
+ ts: new Date().toISOString(),
85
+ }, null, 2));
86
+ } catch (e) { console.warn(`[roll-film] meta sidecar failed: ${e.message}`); }
87
+ // MCP 工具写盘不走自动 file_changed —— 手动发,出一镜上墙一镜
88
+ // 工作区相对路径(fileChanged 正字法,2026-08-14 普查改;绝对路径=前端寻址哑弹)
89
+ try { ctx?.emit?.(Events.fileChanged(path.posix.join('assets', 'generated', fileName), 'add')); } catch { /* */ }
90
+ return `[${shot.name}] assets/generated/${fileName} — ${shot.duration}s ${(sizeBytes / 1e6).toFixed(1)}MB ${Math.floor(wallS / 60)}m${wallS % 60}s ~$${cost}`;
91
+ }
92
+
93
+ export async function rollFilm(
94
+ { workspaceRoot, sharedRoot, projectId, sessionId, ctx },
95
+ { shots, seed },
96
+ ) {
97
+ const asText = (text, isError = false) =>
98
+ ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
99
+ try {
100
+ const project = getProject(projectId);
101
+ if (!project) return asText('错误:项目不存在', true);
102
+ const owner = project.ownerId ? getUserById(project.ownerId) : null;
103
+ if (!owner) return asText('错误:找不到项目归属用户', true);
104
+ // 档位闸 + 逐人批准(auth/tier.js):basic 档不开本地产线;pro 档还要被站主批过
105
+ if (!can(owner, 'localGen')) return asText(DENIAL.localGenTier, true);
106
+ if (!localGenApproved(owner)) return asText(DENIAL.localGenApproval, true);
107
+
108
+ // 全批先验完再花钱:帧域 + 关键帧存在性 + 名字唯一
109
+ const names = new Set();
110
+ for (const s of shots) {
111
+ const frames = frameCount(s.duration);
112
+ if (frames > 294) return asText(`[${s.name}] ${frames} 帧超产线安全域 294(12.25s),拆镜`, true);
113
+ if (names.has(s.name)) return asText(`镜名重复:${s.name}`, true);
114
+ names.add(s.name);
115
+ for (const slot of ['first_frame', 'last_frame']) {
116
+ if (!s[slot]) continue;
117
+ const abs = path.resolve(workspaceRoot || process.cwd(), s[slot]);
118
+ try {
119
+ if (!(await fs.stat(abs)).isFile()) throw new Error('x');
120
+ } catch { return asText(`[${s.name}] ${slot} 找不到:${s[slot]}`, true); }
121
+ s[`_abs_${slot}`] = abs;
122
+ }
123
+ }
124
+
125
+ const backend = (process.env.NODESIGN_FILM_BACKEND || 'box').toLowerCase();
126
+ // 只拦 box 后端:modal 是另一台机器,站主关本地盒子不影响它
127
+ if (backend === 'box' && !localBoxEnabled()) {
128
+ return asText(BOX_OFF_MSG, true);
129
+ }
130
+ const signal = ctx?.abortController?.signal;
131
+ const outDir = path.join(sharedRoot || workspaceRoot, 'assets', 'generated');
132
+ await fs.mkdir(outDir, { recursive: true });
133
+ const batch = Date.now().toString(36);
134
+ const lines = []; const failed = [];
135
+
136
+ let box = null;
137
+ if (backend !== 'modal') {
138
+ box = boxConfig();
139
+ if (!box) return asText('视频盒子未配置(站主没开机)。转告用户;备用 Modal 档要站主设 NODESIGN_FILM_BACKEND=modal。', true);
140
+ const mk = await runBox(box, 'ssh', [...sshArgs(box), 'mkdir -p nd_jobs'], { timeoutMs: 30_000, signal });
141
+ if (mk.code !== 0) return asText(`盒子连不上(没开机/地址过期):\n${(mk.err || '').slice(-400)}\n转告用户。`, true);
142
+ }
143
+
144
+ // 逐镜串行:一镜完整走完(渲→取回→上墙)再下一镜,agent 中途被打断也保住已出的
145
+ for (let i = 0; i < shots.length; i++) {
146
+ const shot = shots[i];
147
+ shot.jobId = `film-${batch}s${i}`;
148
+ const job = { name: shot.jobId, prompt: shot.prompt, seed, duration: shot.duration, width: 1344, height: 768 };
149
+ if (shot.steps) job.steps = shot.steps;
150
+ const t0 = Date.now();
151
+ let localMp4 = null; let failMsg = null;
152
+
153
+ if (backend === 'modal') {
154
+ if (shot._abs_first_frame) job.first_frame = shot._abs_first_frame;
155
+ if (shot._abs_last_frame) job.last_frame = shot._abs_last_frame;
156
+ const jp = path.join(os.tmpdir(), `nd-${shot.jobId}.json`);
157
+ await fs.writeFile(jp, JSON.stringify([job]));
158
+ const r = await runModal(['run', 'h3_comfy.py', '--jobs-file', jp], { cwd: H3_REPO, signal, timeoutMs: PER_SHOT_TIMEOUT_MS });
159
+ fs.unlink(jp).catch(() => { /* */ });
160
+ if (r.code !== 0) failMsg = (r.err || r.out).slice(-600);
161
+ else {
162
+ const hits = (await fs.readdir(path.join(H3_REPO, 'outputs'))).filter((f) => f.startsWith(`${shot.jobId}_`) && f.endsWith('.mp4'));
163
+ if (hits.length) localMp4 = path.join(H3_REPO, 'outputs', hits[0]);
164
+ else failMsg = `无成片。输出尾:${r.out.slice(-400)}`;
165
+ }
166
+ } else {
167
+ for (const slot of ['first_frame', 'last_frame']) {
168
+ const abs = shot[`_abs_${slot}`];
169
+ if (!abs) continue;
170
+ const remote = `nd_jobs/${shot.jobId}-${slot}${path.extname(abs) || '.png'}`;
171
+ const up = await runBox(box, 'scp', [...scpArgs(box), abs, `${box.target}:${remote}`], { timeoutMs: 60_000, signal });
172
+ if (up.code !== 0) { failMsg = `关键帧上传失败:${up.err.slice(-300)}`; break; }
173
+ job[slot] = `~/${remote}`;
174
+ }
175
+ if (!failMsg) {
176
+ const jl = path.join(os.tmpdir(), `nd-${shot.jobId}.json`);
177
+ await fs.writeFile(jl, JSON.stringify([job]));
178
+ const upJ = await runBox(box, 'scp', [...scpArgs(box), jl, `${box.target}:nd_jobs/${shot.jobId}.json`], { timeoutMs: 30_000, signal });
179
+ fs.unlink(jl).catch(() => { /* */ });
180
+ if (upJ.code !== 0) failMsg = `任务上传失败:${upJ.err.slice(-300)}`;
181
+ else {
182
+ const gen = await runBox(box, 'ssh', [...sshArgs(box), `python3 ~/h3box.py video ~/nd_jobs/${shot.jobId}.json`], { timeoutMs: PER_SHOT_TIMEOUT_MS, signal });
183
+ if (gen.code !== 0 || !gen.out.includes('success')) failMsg = `渲染失败(exit ${gen.code}):${(gen.err || gen.out).slice(-600)}`;
184
+ else {
185
+ const pd = await fs.mkdtemp(path.join(os.tmpdir(), 'nd-film-'));
186
+ const pull = await runBox(box, 'scp', [...scpArgs(box), `${box.target}:outputs/${shot.jobId}_*.mp4`, pd], { timeoutMs: 120_000, signal });
187
+ const got = pull.code === 0 ? (await fs.readdir(pd)).filter((f) => f.endsWith('.mp4')) : [];
188
+ if (got.length) localMp4 = path.join(pd, got[0]);
189
+ else failMsg = `成片取回失败:${pull.err.slice(-300)}`;
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ const wallS = Math.round((Date.now() - t0) / 1000);
196
+ if (localMp4) {
197
+ lines.push(await landShot({ localMp4, shot, seed, backend, wallS, outDir, ctx, sessionId }));
198
+ } else {
199
+ failed.push(`[${shot.name}] ${failMsg}`);
200
+ if (signal?.aborted) break;
201
+ break; // 串行批中途失败即停:后镜大概率同因失败,别空烧
202
+ }
203
+ }
204
+
205
+ const head = `Batch done ${lines.length}/${shots.length} shots, seed=${seed}, via ${backend === 'modal' ? 'Modal H100+sage' : '5090 盒子 sage2'}`;
206
+ // 2026-08-18:解禁。mp4 塞不进视觉通道,要看就 ffmpeg 抽帧再看那几张图。
207
+ const tail = 'You may check these: pull two or three frames with ffmpeg and look at them '
208
+ + 'for technical breakage (colour cast, duplicated figures, broken limbs, mush, first '
209
+ + 'frame not matching the anchor) and re-roll those yourself; delete the temp frames after. '
210
+ + 'Do NOT judge whether a shot is good — hand the paths to the user for that.';
211
+ if (failed.length) {
212
+ return asText([head, ...lines, 'FAILED(批在此中断,未渲镜不再空烧):', ...failed, tail].join('\n'), lines.length === 0);
213
+ }
214
+ return asText([head, ...lines, tail].join('\n'));
215
+ } catch (err) {
216
+ return asText(`roll_film 失败:${err.message}`, true);
217
+ }
218
+ }
219
+
220
+ /**
221
+ * @param {object} deps
222
+ */
223
+ export function makeRollFilmTool(deps) {
224
+ return tool(
225
+ 'roll_film',
226
+ ((localBoxEnabled() || (process.env.NODESIGN_FILM_BACKEND || 'box').toLowerCase() === 'modal')
227
+ ? ''
228
+ : '⛔ CURRENTLY UNAVAILABLE — the owner powers this GPU box on and off by hand, '
229
+ + 'and it is off right now. Do not call this tool; tell the user the local box is off.\n\n')
230
+ + `Generate video shots (picture + native audio) on the owner's self-hosted
231
+ MiniMax-H3 lane (RTX 5090 box, SageAttention, Turbo 8-step, 24fps). One call =
232
+ one batch of 1-16 shots rendered back-to-back; each finished shot lands on the
233
+ canvas immediately. Max 12.25s per shot — longer stories are multiple shots.
234
+
235
+ Call ONLY when the user explicitly asks for video, and align the shot list with
236
+ the user before rolling a multi-shot batch. Roughly 3-5 minutes per shot,
237
+ serial; tell the user the camera is rolling. Never auto-retry shots you already
238
+ got back. Requires the GPU box online — if unreachable, relay that and stop.
239
+
240
+ Prompts must follow the H3 three-field English format (cookbook arrives as a
241
+ system reminder attached to your FIRST call — treat your first batch as the
242
+ learning pass and refine from it). Keep character/style blocks verbatim
243
+ identical across shots; the whole batch shares ONE seed automatically. Keyframe
244
+ anchors (first_frame/last_frame) accept paths like "assets/generated/kf1.png"
245
+ (make them with paint_still or generate_image, ideally 1344x768).
246
+
247
+ Clips land at assets/generated/<name>-*.mp4. To check one, pull a couple of
248
+ frames with ffmpeg and look at those — catch colour cast, duplicated figures,
249
+ broken limbs, mush, or a first frame that does not match its anchor, and re-roll
250
+ those yourself (delete the temp frames after). Whether a shot is GOOD is the
251
+ user's call, not yours: report paths
252
+ and move on.`,
253
+ {
254
+ shots: z.array(z.object({
255
+ prompt: z.string().describe('H3 three-field English prompt'),
256
+ duration: z.number().min(5.2).max(12.25).default(8),
257
+ name: z.string().regex(/^[\w-]{1,40}$/).describe('shot slug, unique in batch'),
258
+ first_frame: z.string().optional(),
259
+ last_frame: z.string().optional(),
260
+ steps: z.number().int().min(4).max(8).optional().describe('default 8; 4 = fast draft'),
261
+ })).min(1).max(16).describe('shots rendered serially in one batch'),
262
+ seed: z.number().int().default(1101).describe('ONE seed shared by the whole batch (film discipline)'),
263
+ },
264
+ (args) => rollFilm(deps, args),
265
+ );
266
+ }