@xiaobuyu/nodesign 0.0.1

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 (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. 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 -0
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,690 @@
1
+ /**
2
+ * server/api/exports.js — 用户主动导出
3
+ *
4
+ * 路径(2026-08-13 起**项目级**,会话级留作 alias —— 同 pending-changes.js):
5
+ * GET /api/projects/:pid/exports 列已生成交付包
6
+ * GET /api/projects/:pid/exports/file/:filename 单文件下载
7
+ * GET /api/projects/:pid/exports/items 可单独导出的东西
8
+ * POST /api/projects/:pid/exports/pick 下载勾选的东西
9
+ * GET /api/projects/:pid/exports/html 导出 canvas.html
10
+ * GET /api/projects/:pid/exports/pdf playwright print → PDF
11
+ * GET /api/projects/:pid/exports/pptx 截图 → pptxgenjs
12
+ * GET /api/projects/:pid/exports/site 整站打包
13
+ * GET /api/projects/:pid/exports/handoff JSZip 工程交付包(旧口径,待退役)
14
+ * POST /api/projects/:pid/exports/cards 按产物卡导出(→ exports/cards.js)
15
+ * (/:pid/sessions/:sid/exports/... 同 handler 双挂载。老 sid 路由**永远保留**:
16
+ * jsonl 历史里持久化了绝对 URL —— export-handoff.js:104 拼出来发给用户的
17
+ * 下载链接就是这个形状,砍掉 alias 等于让所有历史消息里的链接变 404。)
18
+ *
19
+ * 文件位置(扁平化后全在项目工作区根):
20
+ * <workspace>/exports/ ← 已生成的导出包(agent 调
21
+ * mcp__nodesign__export_handoff 也写这)
22
+ * <workspace>/canvas.html, spec.json
23
+ * <workspace>/assets/ ← handoff 打包时从这取共享 assets
24
+ */
25
+
26
+ import express from 'express';
27
+ import { launchPerceptionBrowser } from '../engine/mcp/tools/helpers/perception-page.js';
28
+ import { promises as fs } from 'fs';
29
+ import path from 'path';
30
+ import JSZip from 'jszip';
31
+ import { validateProjectId, getProject, listRunsForProject } from '../projects/store.js';
32
+ import { guardProject } from './_guard.js';
33
+ import {
34
+ getSessionWorkspace, getSharedDir, getWorkspaceRoot, validateSessionId,
35
+ } from '../projects/workspace.js';
36
+ import { DECK } from '../shared/deck.js';
37
+ import { buildStandaloneHtml, isHybridHtml, inlineLocalImages } from './exports/build-standalone.js';
38
+ import {
39
+ resolveCanvasTarget, KIND_SITE, ENTRY_FILE, formatAllowed,
40
+ } from '../lib/artifact-target.js';
41
+ import { makeCardsExportHandler } from './exports/cards.js';
42
+ import { can } from '../lib/kinds/index.js';
43
+ import { docxToPdfResponse } from './exports/docx-pdf.js';
44
+ import { prepareExportPage, injectViewportFit } from './exports/export-page.js';
45
+ // 旧交付包打包逻辑已拆到 ./exports/handoff.js(待退役)。这里 re-export 是因为
46
+ // MCP 工具 export_handoff 从本文件 import 它 —— 换那条路由时一起收拾。
47
+ export { buildHandoffZip } from './exports/handoff.js';
48
+ import { buildHandoffZip } from './exports/handoff.js';
49
+ import { walkTaskFiles, loadIgnore } from '../lib/task-scan.js';
50
+
51
+ const router = express.Router();
52
+
53
+ function guard(req, res) {
54
+ // sid 只在走老 alias 时存在 —— 有就校验形状,没有就是项目级路由
55
+ if (req.params.sid !== undefined) {
56
+ try {
57
+ validateSessionId(req.params.sid);
58
+ } catch (err) {
59
+ res.status(400).json({ error: err.message || 'invalid pid/sid' });
60
+ return null;
61
+ }
62
+ }
63
+ // pid 校验 + 存在性 + 归属(2026-07-30 多用户)统一走 guardProject
64
+ return guardProject(req, res);
65
+ }
66
+
67
+ /**
68
+ * 两条挂载共用:alias 带 sid 走原路,项目级直接取工作区根。
69
+ * 导出全是读操作,不 ensure —— 工作区还不存在的话本来也没什么可导。
70
+ * resolveCanvasTarget 的第三参(sid)照传 req.params.sid:undefined 时它只是
71
+ * 跳过"当前会话正在做哪份产物"的记忆,落回全工作区寻址,正是项目级想要的。
72
+ */
73
+ function rootOf(req) {
74
+ return req.params.sid !== undefined
75
+ ? getSessionWorkspace(req.params.pid, req.params.sid)
76
+ : getWorkspaceRoot(req.params.pid);
77
+ }
78
+
79
+ // 按产物卡导出(2026-08-17 重做)实现在 ./exports/cards.js:跟这里的烘焙路由
80
+ // 不是一档东西(原样打包 vs 跑 playwright/esbuild)
81
+ router.post(['/:pid/exports/cards', '/:pid/sessions/:sid/exports/cards'],
82
+ makeCardsExportHandler({ guard, rootOf }));
83
+
84
+ function safeFilename(name) {
85
+ return (name || 'design').replace(/[^A-Za-z0-9._一-龥-]/g, '_').slice(0, 60);
86
+ }
87
+
88
+ // ── 已生成的交付包列表 ──
89
+ router.get(['/:pid/exports', '/:pid/sessions/:sid/exports'], async (req, res, next) => {
90
+ try {
91
+ if (!guard(req, res)) return;
92
+ const sessionRoot = rootOf(req);
93
+ const exportsDir = path.join(sessionRoot, 'exports');
94
+
95
+ let entries;
96
+ try {
97
+ entries = await fs.readdir(exportsDir, { withFileTypes: true });
98
+ } catch (err) {
99
+ if (err.code === 'ENOENT') return res.json({ files: [] });
100
+ throw err;
101
+ }
102
+
103
+ const files = [];
104
+ for (const e of entries) {
105
+ if (!e.isFile()) continue;
106
+ try {
107
+ const stat = await fs.stat(path.join(exportsDir, e.name));
108
+ files.push({ name: e.name, size: stat.size, mtime: stat.mtime.toISOString() });
109
+ } catch { /* skip unreadable */ }
110
+ }
111
+ files.sort((a, b) => (a.mtime < b.mtime ? 1 : -1));
112
+ res.json({ files });
113
+ } catch (err) { next(err); }
114
+ });
115
+
116
+ router.get(['/:pid/exports/file/:filename', '/:pid/sessions/:sid/exports/file/:filename'], async (req, res, next) => {
117
+ try {
118
+ if (!guard(req, res)) return;
119
+ const sessionRoot = rootOf(req);
120
+
121
+ // 文件名只禁路径分隔与上跳;中文名要能下(agent 交付的包常叫「终焉之莉莉-交付.zip」)
122
+ const filename = req.params.filename;
123
+ if (!filename || filename.includes('/') || filename.includes('\\') || filename.includes('..')) {
124
+ return res.status(400).json({ error: 'invalid filename' });
125
+ }
126
+ const filePath = path.join(sessionRoot, 'exports', filename);
127
+ try {
128
+ await fs.access(filePath);
129
+ } catch {
130
+ return res.status(404).json({ error: 'file not found' });
131
+ }
132
+
133
+ const ext = filename.toLowerCase().split('.').pop();
134
+ const mime = ext === 'zip' ? 'application/zip'
135
+ : ext === 'pdf' ? 'application/pdf'
136
+ : ext === 'html' ? 'text/html; charset=utf-8'
137
+ : 'application/octet-stream';
138
+ res.setHeader('Content-Type', mime);
139
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
140
+ const buf = await fs.readFile(filePath);
141
+ res.setHeader('Content-Length', buf.length);
142
+ res.end(buf);
143
+ } catch (err) { next(err); }
144
+ });
145
+
146
+ /**
147
+ * GET /:pid/sessions/:sid/exports/items —— 当前任务里可以单独导出的东西
148
+ *
149
+ * 用户视角的"这次任务做出来的内容":任务目录下的文件 + deck 真正引用到的图。
150
+ * 不做整包,让用户勾选(?path= 指定别的 deck;缺省走统一寻址)。
151
+ */
152
+ router.get(['/:pid/exports/items', '/:pid/sessions/:sid/exports/items'], async (req, res, next) => {
153
+ try {
154
+ if (!guard(req, res)) return;
155
+ const sessionRoot = rootOf(req);
156
+ const target = await resolveCanvasTarget(sessionRoot, req.query.path, req.params.sid);
157
+ const items = [];
158
+ const seen = new Set();
159
+ const add = async (rel, kind) => {
160
+ if (seen.has(rel)) return;
161
+ const abs = path.resolve(sessionRoot, rel);
162
+ if (abs !== sessionRoot && !abs.startsWith(sessionRoot + path.sep)) return;
163
+ try {
164
+ const st = await fs.stat(abs);
165
+ if (!st.isFile()) return;
166
+ seen.add(rel);
167
+ items.push({ path: rel, name: path.basename(rel), size: st.size, kind });
168
+ } catch { /* 读不到就不列 */ }
169
+ };
170
+
171
+ // 产物自己的文件(统一扫描规则 task-scan.js:硬清单 + .ndignore 生效,
172
+ // `_drafts/` 试作照列 —— 用户可能就想下某一版)。
173
+ //
174
+ // ⚠️ 扁平模型改造(2026-08-14):老判据是 `target.task && target.taskDir`,
175
+ // 而 artifact-target 在任务层拆掉后 task 恒 null(兼容字段)—— 那条
176
+ // walkTaskFiles 分支从扁平化起就是死路,站点导出清单只剩入口页 + 引用图,
177
+ // 子页 / 样式表 / 试作**全部静默缺席**。现按产物根收:
178
+ // - 住文件夹的站:整个产物目录(maxDepth 4)
179
+ // - 根站(artifactRel='.'):工作区根还住着 notes/ assets/ 别的产物,
180
+ // 全扫会把别家打包进去 —— 只收根层散文件(.md 除外,同前端
181
+ // resolveObjectId 的认领规则)+ `_drafts/`;引用图由下面的扫描补
182
+ // - deck / 单页站:本体一份(扁平后它住的文件夹是用户的收纳空间,
183
+ // 可能装着不相干的东西,不能整夹打包)
184
+ const isSite = target.ok && target.kind === KIND_SITE;
185
+ const isDirArtifact = target.ok && isSite && !target.artifact?.single;
186
+ if (isDirArtifact) {
187
+ const rootRel = target.artifactRel === '.' ? '' : target.artifactRel;
188
+ const files = await walkTaskFiles(target.artifactDir, {
189
+ maxDepth: rootRel ? 4 : 3,
190
+ includeDrafts: true,
191
+ });
192
+ for (const f of files) {
193
+ if (!rootRel) {
194
+ const rootLevel = !f.rel.includes('/');
195
+ const isDraft = f.rel.startsWith('_drafts/');
196
+ if (!rootLevel && !isDraft) continue;
197
+ if (rootLevel && /\.md$/i.test(f.name)) continue;
198
+ }
199
+ const rel = rootRel ? `${rootRel}/${f.rel}` : f.rel;
200
+ const kind = /\.html?$/i.test(f.name)
201
+ ? (f.rel.startsWith('_drafts/') ? 'draft' : (isSite ? 'site-page' : 'deck'))
202
+ : 'file';
203
+ await add(rel, kind);
204
+ }
205
+ } else if (target.ok) {
206
+ await add(target.relPath, isSite ? 'site-page' : 'deck');
207
+ }
208
+
209
+ // 产物引用到的图(相对它自己的目录解,落成 workspace 相对路径)。
210
+ // 站点扫全部页面 —— 只扫入口页会漏掉子页独有的图,用户下下来是裂的。
211
+ if (target.ok) {
212
+ // 站点:所有 .html + .css 都可能引图;deck:就它自己那一份
213
+ const sources = isSite
214
+ ? items.filter(i => /\.(html?|css)$/i.test(i.path)).map(i => path.resolve(sessionRoot, i.path))
215
+ : [target.absPath];
216
+ for (const src of sources) {
217
+ try {
218
+ const html = await fs.readFile(src, 'utf8');
219
+ const base = path.dirname(src);
220
+ const refs = new Set();
221
+ for (const m of html.matchAll(/<img\b[^>]*?\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)')/gi)) refs.add(m[1] || m[2]);
222
+ for (const m of html.matchAll(/url\(\s*(?:"([^"]+)"|'([^']+)'|([^)]+?))\s*\)/gi)) refs.add((m[1] || m[2] || m[3] || '').trim());
223
+ for (const r of refs) {
224
+ if (!r || /^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i.test(r) || path.isAbsolute(r)) continue;
225
+ const abs = path.resolve(base, r);
226
+ if (!abs.startsWith(sessionRoot + path.sep)) continue;
227
+ await add(path.relative(sessionRoot, abs).split(path.sep).join('/'), 'image');
228
+ }
229
+ } catch { /* 读不到就跳过这一份 */ }
230
+ }
231
+ }
232
+
233
+ res.json({
234
+ deck: target.ok ? target.relPath : null,
235
+ kind: target.ok ? target.kind : null,
236
+ task: target.ok ? target.task : null,
237
+ items,
238
+ });
239
+ } catch (err) { next(err); }
240
+ });
241
+
242
+ /**
243
+ * POST /:pid/sessions/:sid/exports/pick —— 下载勾选的东西
244
+ * body: { paths: string[], filename?: string }
245
+ * 单个文件直接流回;多个打成 zip。
246
+ */
247
+ router.post(['/:pid/exports/pick', '/:pid/sessions/:sid/exports/pick'], async (req, res, next) => {
248
+ try {
249
+ const project = guard(req, res);
250
+ if (!project) return;
251
+ const sessionRoot = rootOf(req);
252
+ const paths = Array.isArray(req.body?.paths) ? req.body.paths.filter(p => typeof p === 'string') : [];
253
+ if (paths.length === 0) return res.status(400).json({ error: 'paths required' });
254
+
255
+ const files = [];
256
+ for (const rel of paths) {
257
+ const abs = path.resolve(sessionRoot, rel);
258
+ if (abs !== sessionRoot && !abs.startsWith(sessionRoot + path.sep)) {
259
+ return res.status(400).json({ error: `path escapes workspace: ${rel}` });
260
+ }
261
+ try {
262
+ const st = await fs.stat(abs);
263
+ if (st.isFile()) files.push({ rel, abs, size: st.size });
264
+ } catch {
265
+ return res.status(404).json({ error: `not found: ${rel}` });
266
+ }
267
+ }
268
+ if (files.length === 0) return res.status(404).json({ error: 'nothing to export' });
269
+
270
+ if (files.length === 1) {
271
+ const only = files[0];
272
+ const buf = await fs.readFile(only.abs);
273
+ const name = path.basename(only.rel);
274
+ res.setHeader('Content-Type', 'application/octet-stream');
275
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(name)}`);
276
+ res.setHeader('Content-Length', buf.length);
277
+ return res.end(buf);
278
+ }
279
+
280
+ // 包内路径:剥掉所有勾选项的公共目录前缀。deck 场景(全在同一个任务目录里)
281
+ // 结果还是平铺的文件名;站点勾了子目录里的页面时保留 `css/style.css` 这层结构 ——
282
+ // 一律 basename 会让 `pages/a.html` 和 `posts/a.html` 在包里撞成一个。
283
+ const segs = files.map(f => f.rel.split('/').slice(0, -1));
284
+ let common = segs[0] || [];
285
+ for (const s of segs) {
286
+ let i = 0;
287
+ while (i < common.length && i < s.length && common[i] === s[i]) i++;
288
+ common = common.slice(0, i);
289
+ }
290
+ const strip = common.length ? common.join('/') + '/' : '';
291
+ const zip = new JSZip();
292
+ for (const f of files) {
293
+ zip.file(f.rel.startsWith(strip) ? f.rel.slice(strip.length) : f.rel, await fs.readFile(f.abs));
294
+ }
295
+ const buf = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
296
+ const name = `${safeFilename(req.body?.filename || project.name)}.zip`;
297
+ res.setHeader('Content-Type', 'application/zip');
298
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(name)}`);
299
+ res.setHeader('Content-Length', buf.length);
300
+ res.end(buf);
301
+ } catch (err) { next(err); }
302
+ });
303
+
304
+ router.get(['/:pid/exports/html', '/:pid/sessions/:sid/exports/html'], async (req, res, next) => {
305
+ try {
306
+ const project = guard(req, res);
307
+ if (!project) return;
308
+ const sessionRoot = rootOf(req);
309
+ // 导出哪一份走统一寻址(?path= 显式 → 本会话当前 deck → 本会话名下的任务 deck
310
+ // → cwd/canvas.html)。任务模型下 deck 不在 cwd,写死 cwd 会永远导出空(2026-07-28)
311
+ const target = await resolveCanvasTarget(sessionRoot, req.query.path, req.params.sid);
312
+ if (!target.ok) return res.status(404).json({ error: target.message });
313
+ const file = target.absPath;
314
+ let html;
315
+ try {
316
+ html = await fs.readFile(file, 'utf8');
317
+ } catch (err) {
318
+ if (err.code === 'ENOENT') return res.status(404).json({ error: 'canvas.html not yet generated' });
319
+ throw err;
320
+ }
321
+
322
+ // **fit script 只给 deck**(2026-07-28):那段脚本把每个 section 包成
323
+ // 100vw×100vh 的 frame + scroll-snap,是幻灯片范式。站点是自然滚动的长页,
324
+ // 注进去等于把用户的网站改造成翻页器 —— 而且是导出物,坏了还带不回来。
325
+ // 顺带 stripFitScripts 也要跳过:它按启发式删"像 fit 的脚本",站点里正当的
326
+ // `transform: scale(` 动画会被误删。
327
+ const injectFit = target.kind !== KIND_SITE;
328
+
329
+ // Hybrid 文件 → 走自包含构建管道(CDN 全 inline,离线可双击打开)
330
+ // 老 deck(无 babel script)→ 降级到 injectViewportFit 文本替换
331
+ // 任一步骤失败也降级——保证用户至少拿到能用的 HTML
332
+ if (isHybridHtml(html)) {
333
+ try {
334
+ // baseDir = deck 自己的目录:任务 deck 写的是 ../../assets/generated/x.png
335
+ html = await buildStandaloneHtml(html, { sessionRoot, baseDir: path.dirname(file), injectFit });
336
+ } catch (err) {
337
+ // standalone 任何一步炸——管道里图片 inline 也不会跑,用户拿到的 HTML
338
+ // 离开 session 目录后 <img src="assets/..."> 全 404。降级路径必须仍把
339
+ // 图片 inline 一遍兜底,否则一次 esbuild 失败 = 整个 deck 图片全丢。
340
+ console.warn('[exports/html] standalone build failed, falling back to viewport-fit:', err.message);
341
+ if (injectFit) html = injectViewportFit(html);
342
+ try {
343
+ html = await inlineLocalImages(html, path.dirname(file), sessionRoot);
344
+ } catch (e2) {
345
+ console.warn('[exports/html] image inline fallback also failed:', e2.message);
346
+ }
347
+ }
348
+ } else {
349
+ if (injectFit) html = injectViewportFit(html);
350
+ try {
351
+ html = await inlineLocalImages(html, path.dirname(file), sessionRoot);
352
+ } catch (e) {
353
+ console.warn('[exports/html] image inline (legacy path) failed:', e.message);
354
+ }
355
+ }
356
+
357
+ const filename = `${safeFilename(project.name)}.html`;
358
+ // application/octet-stream 绕过 Cloudflare HTML 处理(Auto Minify / Rocket Loader)
359
+ // 否则 19MB base64 HTML 会被 CF 尝试 parse/minify → 吞吐降到 7KB/s
360
+ res.setHeader('Content-Type', 'application/octet-stream');
361
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
362
+ res.setHeader('Cache-Control', 'no-store, no-transform');
363
+ res.send(html);
364
+ } catch (err) { next(err); }
365
+ });
366
+
367
+ // PDF:跟 PPTX 同款位图方案——截每页 PNG → 拼成 HTML → page.pdf 输出。
368
+ //
369
+ // 为什么不直接 page.pdf 渲染原 deck?Chromium PDF 后端对 Google Fonts 拆 80+
370
+ // 个 unicode-range subset + data URL inline 的 web font 无法做正常字体子集化
371
+ // 嵌入,全部退回 Type 3(路径填充)渲染——CJK 字体严重失真,跟 HTML/preview
372
+ // 对不上。截图方案把字体烤成像素,绕开 PDF 字体嵌入管道,视觉跟 HTML 1:1。
373
+ //
374
+ // 代价:PDF 文字不可选/不可搜索(位图);文件略大(每页一张 PNG)。
375
+ // 跟 PPTX 已有方案对齐——用户工作流"看完发邮件/打印"为主,可接受。
376
+ router.get(['/:pid/exports/pdf', '/:pid/sessions/:sid/exports/pdf'], async (req, res, next) => {
377
+ try {
378
+ const project = guard(req, res);
379
+ if (!project) return;
380
+ const sessionRoot = rootOf(req);
381
+ const target = await resolveCanvasTarget(sessionRoot, req.query.path, req.params.sid);
382
+ if (!target.ok) return res.status(404).json({ error: target.message });
383
+ if (rejectFormat(res, target, 'pdf', 'PDF')) return;
384
+ // 可渲染形态(docx)走 LibreOffice,不碰 playwright —— 下面整段是 deck 的
385
+ // `<section data-page>` 逻辑,对 docx 一行都不适用。按能力位分流,跟感知层同一条规矩。
386
+ if (can(target.kind, 'renderable')) return await docxToPdfResponse(res, target);
387
+ const file = target.absPath;
388
+
389
+ let browser;
390
+ try {
391
+ browser = await launchPerceptionBrowser();
392
+ } catch (err) {
393
+ return res.status(500).json({
394
+ error: 'playwright chromium not installed — run `npx playwright install chromium`',
395
+ details: err.message,
396
+ });
397
+ }
398
+
399
+ let prepCleanup = async () => {};
400
+ try {
401
+ const { page, ctx, pageSize, cleanup } = await prepareExportPage(browser, file, { sessionRoot });
402
+ prepCleanup = cleanup;
403
+
404
+ const sectionHandles = await page.$$('section[data-page]');
405
+ if (sectionHandles.length === 0) {
406
+ await ctx.close();
407
+ return res.status(400).json({
408
+ error: 'canvas.html 没有 <section data-page="N"> 分页结构 — 无法转 PDF',
409
+ });
410
+ }
411
+
412
+ // 拿排好序的 section + bbox(跟 PPTX 路径完全一致)
413
+ const pageInfos = [];
414
+ for (const handle of sectionHandles) {
415
+ const pageNum = await handle.getAttribute('data-page');
416
+ const bbox = await handle.boundingBox();
417
+ pageInfos.push({ handle, pageNum: parseInt(pageNum, 10) || 0, bbox });
418
+ }
419
+ pageInfos.sort((a, b) => a.pageNum - b.pageNum);
420
+
421
+ // 每页截图(pageSize 已被 prepareExportPage 中和掉 fit transform,原坐标)。
422
+ // 用 JPEG 不用 PNG:图片自包含之后每页都是整幅照片级画面,PNG 无损一页
423
+ // 就 5-8MB,7 页的 PDF 能到 60MB —— 发不出去的东西等于没导出。
424
+ // q=88 的 JPEG 在同画面下小一个数量级,肉眼看不出差别(deck 本来就是看的)。
425
+ const pngs = [];
426
+ for (const { handle, bbox } of pageInfos) {
427
+ const clipOpts = bbox
428
+ ? { clip: { x: bbox.x, y: bbox.y, width: pageSize.w, height: pageSize.h } }
429
+ : {};
430
+ const buf = await handle.screenshot({ type: 'jpeg', quality: 88, ...clipOpts });
431
+ pngs.push(buf);
432
+ }
433
+
434
+ // 拼成多页 HTML:每页一个 .slide 容器 + img 满铺,page-break 控制分页
435
+ const slidesHtml = pngs.map((buf) =>
436
+ `<div class="slide"><img src="data:image/jpeg;base64,${buf.toString('base64')}"/></div>`,
437
+ ).join('\n');
438
+
439
+ const composeHtml = `<!DOCTYPE html>
440
+ <html><head><meta charset="utf-8"><style>
441
+ @page { size: ${pageSize.w}px ${pageSize.h}px; margin: 0; }
442
+ * { box-sizing: border-box; }
443
+ html, body { margin: 0; padding: 0; background: #fff; }
444
+ .slide {
445
+ width: ${pageSize.w}px;
446
+ height: ${pageSize.h}px;
447
+ page-break-after: always;
448
+ page-break-inside: avoid;
449
+ break-after: page;
450
+ break-inside: avoid;
451
+ overflow: hidden;
452
+ }
453
+ .slide:last-child { page-break-after: auto; break-after: auto; }
454
+ img { display: block; width: 100%; height: 100%; }
455
+ </style></head><body>
456
+ ${slidesHtml}
457
+ </body></html>`;
458
+
459
+ // 复用同 page setContent —— 不需要新开 page,省一次 launch 开销
460
+ await page.setContent(composeHtml, { waitUntil: 'load' });
461
+
462
+ const pdfBuffer = await page.pdf({
463
+ width: `${pageSize.w}px`,
464
+ height: `${pageSize.h}px`,
465
+ printBackground: true,
466
+ preferCSSPageSize: true,
467
+ margin: { top: 0, right: 0, bottom: 0, left: 0 },
468
+ });
469
+ await ctx.close();
470
+
471
+ const filename = `${safeFilename(project.name)}.pdf`;
472
+ res.setHeader('Content-Type', 'application/pdf');
473
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
474
+ res.setHeader('Content-Length', pdfBuffer.length);
475
+ res.end(pdfBuffer);
476
+ } finally {
477
+ await browser.close().catch(() => { /* ignore */ });
478
+ await prepCleanup();
479
+ }
480
+ } catch (err) { next(err); }
481
+ });
482
+
483
+ // PPTX:playwright 截每个 section[data-page] PNG → pptxgenjs 嵌图
484
+ // MVP 位图方案:用户拿到的 PPTX 文字不可编辑(每页是图),但视觉 1:1 还原
485
+ // 16:9 默认 layout(10" × 5.625")匹配 deck 1920×1080 比例(pageSize.h/pageSize.w * 10 公式自动算)
486
+ router.get(['/:pid/exports/pptx', '/:pid/sessions/:sid/exports/pptx'], async (req, res, next) => {
487
+ try {
488
+ const project = guard(req, res);
489
+ if (!project) return;
490
+ const sessionRoot = rootOf(req);
491
+ const target = await resolveCanvasTarget(sessionRoot, req.query.path, req.params.sid);
492
+ if (!target.ok) return res.status(404).json({ error: target.message });
493
+ if (rejectFormat(res, target, 'pptx', 'PPTX')) return;
494
+ const file = target.absPath;
495
+
496
+ const PptxGenJS = (await import('pptxgenjs')).default;
497
+
498
+ let browser;
499
+ try {
500
+ browser = await launchPerceptionBrowser();
501
+ } catch (err) {
502
+ return res.status(500).json({
503
+ error: 'playwright chromium not installed — run `npx playwright install chromium`',
504
+ details: err.message,
505
+ });
506
+ }
507
+
508
+ let prepCleanup = async () => {};
509
+ try {
510
+ const { page, ctx, pageSize, cleanup } = await prepareExportPage(browser, file, { sessionRoot });
511
+ prepCleanup = cleanup;
512
+
513
+ const sectionHandles = await page.$$('section[data-page]');
514
+ if (sectionHandles.length === 0) {
515
+ await ctx.close();
516
+ return res.status(400).json({
517
+ error: 'canvas.html 没有 <section data-page="N"> 分页结构 — 无法转 PPTX',
518
+ });
519
+ }
520
+
521
+ const pageInfos = [];
522
+ for (const handle of sectionHandles) {
523
+ const pageNum = await handle.getAttribute('data-page');
524
+ const bbox = await handle.boundingBox();
525
+ pageInfos.push({ handle, pageNum: parseInt(pageNum, 10) || 0, bbox });
526
+ }
527
+ pageInfos.sort((a, b) => a.pageNum - b.pageNum);
528
+
529
+ const slideW = 10;
530
+ const slideH = pageSize.h / pageSize.w * slideW;
531
+ const pres = new PptxGenJS();
532
+ pres.defineLayout({ name: 'DECK', width: slideW, height: slideH });
533
+ pres.layout = 'DECK';
534
+ pres.title = safeFilename(project.name);
535
+
536
+ for (const { handle, pageNum, bbox } of pageInfos) {
537
+ const clipOpts = bbox
538
+ ? { clip: { x: bbox.x, y: bbox.y, width: pageSize.w, height: pageSize.h } }
539
+ : {};
540
+ // 同 PDF:JPEG 不用 PNG,否则一份 7 页 deck 的 pptx 能到几十 MB
541
+ const buf = await handle.screenshot({ type: 'jpeg', quality: 88, ...clipOpts });
542
+ const slide = pres.addSlide();
543
+ slide.addImage({
544
+ data: `data:image/jpeg;base64,${buf.toString('base64')}`,
545
+ x: 0, y: 0, w: slideW, h: slideH,
546
+ });
547
+ slide.addNotes(`Page ${pageNum} — exported from NoDesign canvas.html`);
548
+ }
549
+ await ctx.close();
550
+
551
+ // pptxgenjs write 到 nodebuffer
552
+ const pptxBuffer = await pres.write({ outputType: 'nodebuffer' });
553
+ const filename = `${safeFilename(project.name)}.pptx`;
554
+ res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.presentationml.presentation');
555
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
556
+ res.setHeader('Content-Length', pptxBuffer.length);
557
+ res.end(pptxBuffer);
558
+ } finally {
559
+ await browser.close().catch(() => { /* ignore */ });
560
+ await prepCleanup();
561
+ }
562
+ } catch (err) { next(err); }
563
+ });
564
+
565
+ /**
566
+ * 形态 × 格式守卫(注册表驱动):不适用的格式提前 400,不白烧 playwright /
567
+ * esbuild。以前是 if kind === site 的散装判断,第三种形态进来就得再改一轮 ——
568
+ * 现在各形态可用的格式表在 kinds/ 注册条目里,这里只查表。
569
+ */
570
+ function rejectFormat(res, target, formatId, label) {
571
+ if (formatAllowed(target.kind, formatId)) return false;
572
+ res.status(400).json({
573
+ error: `${target.relPath} 是 ${target.kind} —— ${label} 导出不适用于这种形态。`
574
+ + (target.kind === KIND_SITE ? '站点请用「整站打包」(/exports/site)或导出菜单里的站点 zip。' : ''),
575
+ });
576
+ return true;
577
+ }
578
+
579
+ /**
580
+ * GET /:pid/sessions/:sid/exports/site —— 整站打包
581
+ *
582
+ * 打的是**产物根**(手写站点 = 任务根,构建型站点 = dist/ 之类),不是源目录 ——
583
+ * 用户要的是能直接发布的站,不是构建脚本和 md。扫描走统一规则(task-scan.js):
584
+ * 硬清单 + .ndignore + 跳 `_drafts/`,node_modules 打进 zip 这种事故从根上没了。
585
+ *
586
+ * 原样保留目录结构和文件名(相对链接才不会断),不注入 fit、不改写结构、不重命名。
587
+ * 唯一的改写是**素材路径归一**:站点引用项目共享素材写的是 `../../assets/x.png`
588
+ * (相对它在 workspace 里的位置),包里没有 workspace 这层,所以把素材拷进
589
+ * `site/assets/` 并按每个文件自己的深度重写前缀 —— 子目录里的页面要 `../assets/`,
590
+ * 写死成 `assets/` 就又裂一次。任务本地 assets/(推荐写法)本来就在包里,零改写。
591
+ */
592
+ router.get(['/:pid/exports/site', '/:pid/sessions/:sid/exports/site'], async (req, res, next) => {
593
+ try {
594
+ const project = guard(req, res);
595
+ if (!project) return;
596
+ const sessionRoot = rootOf(req);
597
+ const target = await resolveCanvasTarget(sessionRoot, req.query.path, req.params.sid);
598
+ if (!target.ok) return res.status(404).json({ error: target.message });
599
+ if (!formatAllowed(target.kind, 'site')) {
600
+ return res.status(400).json({ error: `${target.relPath} 是 ${target.kind},不是站点 —— 用 /exports/html 或 /exports/handoff。` });
601
+ }
602
+ if (target.artifact?.single) {
603
+ return res.status(400).json({ error: `${target.relPath} 是单页产物,没有"整站"可打包 —— 用 /exports/html 导出这一页。` });
604
+ }
605
+ if (!target.taskDir) return res.status(400).json({ error: 'site must live in a task folder' });
606
+
607
+ const zip = new JSZip();
608
+ const referenced = new Map(); // workspace 相对路径 → 包内相对 site/ 的落点
609
+ const baseDir = target.artifactDir || target.taskDir;
610
+ const files = await walkTaskFiles(baseDir, {
611
+ maxDepth: 6,
612
+ ignore: await loadIgnore(target.taskDir),
613
+ ignoreBase: target.taskDir,
614
+ });
615
+
616
+ for (const f of files) {
617
+ if (!/\.(html?|css)$/i.test(f.name)) {
618
+ try { zip.file(`site/${f.rel}`, await fs.readFile(f.abs)); } catch { /* 中途被删就跳过 */ }
619
+ continue;
620
+ }
621
+ let text;
622
+ try { text = await fs.readFile(f.abs, 'utf8'); } catch { continue; }
623
+ // 先按原文收集引用(改写之后就找不回原路径了)
624
+ for (const r of localRefsOf(text)) {
625
+ const refAbs = path.resolve(path.dirname(f.abs), r);
626
+ if (!refAbs.startsWith(sessionRoot + path.sep)) continue;
627
+ if (refAbs.startsWith(target.taskDir + path.sep)) continue; // 站内文件整目录已经打包了
628
+ const wsRel = path.relative(sessionRoot, refAbs).split(path.sep).join('/');
629
+ if (wsRel.startsWith('assets/')) referenced.set(wsRel, wsRel);
630
+ }
631
+ // `../../assets/…` → 按本文件深度重算前缀(根层是 `assets/`,一层子目录是 `../assets/`)
632
+ const depth = f.rel.split('/').length;
633
+ const up = '../'.repeat(depth - 1);
634
+ zip.file(`site/${f.rel}`, text.replace(/(["'(])(?:\.\.\/)+assets\//g, `$1${up}assets/`));
635
+ }
636
+
637
+ for (const [wsRel, dst] of referenced) {
638
+ try {
639
+ zip.file(`site/${dst}`, await fs.readFile(path.resolve(sessionRoot, wsRel)));
640
+ } catch { /* 引用了不存在的文件:页面里本来就是裂的,不因此让打包失败 */ }
641
+ }
642
+
643
+ const buf = await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' });
644
+ const filename = `${safeFilename(target.task || project.name)}-site.zip`;
645
+ res.setHeader('Content-Type', 'application/zip');
646
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
647
+ res.setHeader('Content-Length', buf.length);
648
+ res.end(buf);
649
+ } catch (err) { next(err); }
650
+ });
651
+
652
+ /** html/css 里引用的本地相对路径(排除 http(s):// data: 和绝对路径) */
653
+ function localRefsOf(text) {
654
+ const refs = new Set();
655
+ for (const m of text.matchAll(/<img\b[^>]*?\bsrc\s*=\s*(?:"([^"]+)"|'([^']+)')/gi)) refs.add(m[1] || m[2]);
656
+ for (const m of text.matchAll(/url\(\s*(?:"([^"]+)"|'([^']+)'|([^)]+?))\s*\)/gi)) refs.add((m[1] || m[2] || m[3] || '').trim());
657
+ return [...refs].filter(r => r && !/^(?:[a-z][a-z0-9+\-.]*:|\/\/)/i.test(r) && !path.isAbsolute(r));
658
+ }
659
+
660
+ router.get(['/:pid/exports/handoff', '/:pid/sessions/:sid/exports/handoff'], async (req, res, next) => {
661
+ try {
662
+ const project = guard(req, res);
663
+ if (!project) return;
664
+ const sessionRoot = rootOf(req);
665
+ const sharedRoot = getSharedDir(req.params.pid);
666
+ const runs = listRunsForProject(req.params.pid);
667
+ const target = await resolveCanvasTarget(sessionRoot, req.query.path, req.params.sid);
668
+
669
+ const zipBuffer = await buildHandoffZip(sessionRoot, sharedRoot, {
670
+ projectId: project.id,
671
+ projectName: project.name,
672
+ skillId: project.skillId,
673
+ // 项目级挂载没有 sid —— README 里那行落 null,不硬造一个
674
+ sessionId: req.params.sid ?? null,
675
+ runs,
676
+ deckPath: target.ok ? target.relPath : ENTRY_FILE.deck,
677
+ kind: target.ok ? target.kind : null,
678
+ });
679
+
680
+ const filename = `${safeFilename(project.name)}-handoff.zip`;
681
+ res.setHeader('Content-Type', 'application/zip');
682
+ res.setHeader('Content-Disposition', `attachment; filename*=UTF-8''${encodeURIComponent(filename)}`);
683
+ res.setHeader('Content-Length', zipBuffer.length);
684
+ res.end(zipBuffer);
685
+ } catch (err) { next(err); }
686
+ });
687
+
688
+
689
+ export default router;
690
+