@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,289 @@
1
+ /**
2
+ * lib/export-collect.js — 按「产物卡」收集要导出的东西(2026-08-17)
3
+ *
4
+ * 导出的寻址单位从「会话当前产物」换成**画布上的那张卡**:卡的类型决定收什么。
5
+ * site 卡收站点那棵树、图片卡收那张图,以此类推。
6
+ *
7
+ * 为什么按卡 id 就够、不用再造一份枚举:
8
+ * 卡 id 本身**就是地址**(`site:<产物根>` / `deck:<html路径>` / 文件卡是裸路径),
9
+ * 前端 `BoardCanvas` 拼它、服务端 `board-store.mapId` 认它,用的是同一套前缀判据。
10
+ * 这里只解析地址、落到磁盘上收,权威清单仍然只有 `/artifacts` 那一份。
11
+ *
12
+ * 素材只收**这份产物真正引用到的**(`asset-refs.js` 扫出来的),不再像旧交付包那样
13
+ * 把整个项目级 `shared/assets` 一锅端 —— 那会把别的任务的图一起交出去,生产上最大的
14
+ * 项目那个目录有 280MB。
15
+ *
16
+ * ── 2026-08-17 评审后修的四个洞(都有单测钉着)──
17
+ * 1. **根级站点卡**(`site:`,rel 为空 —— 扁平化后「根 index.html = 一个站」是常态)
18
+ * 从工作区根盲走,把 `assets/`、`exports/`、`board.json`、别的任务全打进包,
19
+ * 280MB 病原样复活。
20
+ * 2. **单页站点卡**(`site:任务/_drafts/试作.html`)整类 400 —— 它是画布上真实存在
21
+ * 的一种卡,收集器却要求 site 必须是目录。
22
+ * 3. **`.ndignore` 基准**从卡 id 字符串上切第一段,只有「深度 1 的任务文件夹」这一
23
+ * 种布局碰巧对;根级构建站、嵌套站全取错,忽略规则静默失效。
24
+ * 4. **软链逃逸**:`safeResolve` 只做词法检查,工作区里一个指向外面的软链能把
25
+ * 工作区外的文件收进包(实测读出过 /tmp 下的文件内容)。
26
+ */
27
+
28
+ import path from 'node:path';
29
+ import fs from 'node:fs/promises';
30
+ import { walkTaskFiles, loadIgnore, RESERVED_DIRS, isReservedFile } from './task-scan.js';
31
+ import { collectAssetRefs } from './asset-refs.js';
32
+ import { kindDef, taskManifest, can } from './kinds/index.js';
33
+ import { fileKindOfPath, fileKindDef } from './kinds/file-kinds.js';
34
+ // 越界判据收在一处(原来这里有第二份实现,docx 页图路由抄它的形状时抄漏了 realpath)
35
+ import { safeResolveRead as safeResolve } from './safe-path.js';
36
+
37
+ /**
38
+ * 卡 id → { kind, rel }。
39
+ *
40
+ * ⚠️ 前缀只认纯字母(`deck:` `site:`)—— 路径里本来就可能有冒号,判据必须跟
41
+ * 前端 `stage.js` 的 `zoneOfObjectId` 和服务端 `board-store.mapId` 保持一致。
42
+ * 没有合法前缀 = 文件卡,类型按路径推。
43
+ * 认哪些前缀问注册表(kindDef)—— 原来是两个手写 Set,加形态时这里必漏,
44
+ * 新形态的卡会被当成裸文件路径去解析(「写死表家族」第 5 处,2026-08-18 收)。
45
+ *
46
+ * ⚠️ 已知歧义:根层一个叫 `final:v2.png` 的文件,卡 id 就是裸路径,这里会把
47
+ * `final` 当前缀剥掉。三处判据是**一致地**这样,改判据要三处一起改;collectCard
48
+ * 里对这种情况有一次「整串当路径」的回退。
49
+ */
50
+ export function parseCardId(cardId) {
51
+ if (!cardId || typeof cardId !== 'string') return null;
52
+ const c = cardId.indexOf(':');
53
+ if (c > 0 && /^[a-z]+$/.test(cardId.slice(0, c))) {
54
+ const kind = cardId.slice(0, c);
55
+ const rel = cardId.slice(c + 1).replace(/\\/g, '/');
56
+ if (kindDef(kind)) return { kind, rel };
57
+ return { kind: fileKindOfPath(rel), rel };
58
+ }
59
+ const rel = cardId.replace(/\\/g, '/');
60
+ return { kind: fileKindOfPath(rel), rel };
61
+ }
62
+
63
+
64
+ /** 这种形态能导出成什么。single = 单页产物,没有「整站 zip」可言(跟 taskManifest 的 decorate 同一条剥除) */
65
+ export function exportFormatsFor(kind, { single = false } = {}) {
66
+ const list = kindDef(kind)?.exportFormats || fileKindDef(kind)?.exportFormats || [];
67
+ return single ? list.filter(f => f !== 'site') : list;
68
+ }
69
+
70
+ /**
71
+ * 忽略规则的基准目录 = **`.ndignore` 自己所在的那一层**,从产物根往上找第一个。
72
+ *
73
+ * 这跟 `.gitignore` 是同一条语义:规则文件的作用域就是它自己的目录。
74
+ *
75
+ * 曾经从卡 id 字符串上切第一段(`rel.split('/')[0]`)当基准,只有「深度 1 的任务
76
+ * 文件夹」这一种布局碰巧对:根级构建站(`site:dist`)会切出 `dist`、嵌套站
77
+ * (`site:客户/官网`)会切出 `客户`,两种情况下忽略规则都**静默失效**。
78
+ * 也试过问 `taskManifest` 谁认领了这个产物根,但构建站的 `dist/` 自己看起来也像
79
+ * 个站,从深往浅问会停在 `dist` 上 —— 而 `.ndignore` 明明在更外面。
80
+ * 按规则文件自己的位置找,三种布局一次都不会错。
81
+ */
82
+ async function ignoreBaseFor(workspaceRoot, rel) {
83
+ const segs = rel ? rel.split('/') : [];
84
+ for (let i = segs.length; i >= 0; i--) {
85
+ const cand = segs.slice(0, i).join('/');
86
+ const dir = cand ? path.join(workspaceRoot, cand) : workspaceRoot;
87
+ try {
88
+ await fs.access(path.join(dir, '.ndignore'));
89
+ return cand;
90
+ } catch { /* 这层没有,往上找 */ }
91
+ }
92
+ return ''; // 一个都没有:基准取工作区根(规则为空,等价于不过滤)
93
+ }
94
+
95
+ /**
96
+ * 收一张卡。
97
+ *
98
+ * @returns {Promise<{cardId, kind, rel, title, single, files, assets, missing, unresolved, exportFormats}>}
99
+ * files 产物自身的文件(rel 相对工作区根)
100
+ * assets 它真正引用到、且磁盘上确实存在的素材
101
+ * missing 引用了但磁盘上找不到的(裂图预警)
102
+ * unresolved 算不出来的引用(动态拼接 / 脚本字面量 / 树外引用 / `<base href>`)
103
+ */
104
+ export async function collectCard({ workspaceRoot, cardId }) {
105
+ const parsed = parseCardId(cardId);
106
+ if (!parsed) throw Object.assign(new Error(`认不出这张卡:${cardId}`), { status: 400 });
107
+
108
+ let { kind, rel } = parsed;
109
+ let abs = await safeResolve(workspaceRoot, rel);
110
+ let stat = null;
111
+ if (abs) { try { stat = await fs.stat(abs); } catch { /* 下面回退 */ } }
112
+
113
+ // 前缀剥错了的回退:`final:v2.png` 这种文件名会被当成 `final:` 前缀
114
+ if (!stat && cardId !== rel) {
115
+ const whole = cardId.replace(/\\/g, '/');
116
+ const absWhole = await safeResolve(workspaceRoot, whole);
117
+ if (absWhole) {
118
+ try {
119
+ stat = await fs.stat(absWhole);
120
+ abs = absWhole; rel = whole; kind = fileKindOfPath(whole);
121
+ } catch { /* 仍然没有 */ }
122
+ }
123
+ }
124
+ if (!abs) throw Object.assign(new Error(`路径越界或指向工作区外:${rel}`), { status: 400 });
125
+ if (!stat) throw Object.assign(new Error(`产物不存在:${rel}`), { status: 404 });
126
+
127
+ const files = [];
128
+ // 「这种形态有没有目录型」问注册表(kind 条目声明 directory)。整树打包只对
129
+ // browsable 的目录型成立(站点 = 一棵互相引用的树);word 文件夹也是目录型,
130
+ // 但 .docx 成员各自自包含,卡级导出走下面挑主成员那条,不走树
131
+ const dirKind = !!kindDef(kind)?.directory;
132
+ // 单页站点(`_drafts/试作.html` 这类)也是 site 卡,只是它是一个文件不是一棵树
133
+ const single = dirKind && can(kind, 'browsable') && stat.isFile();
134
+ const isTree = dirKind && can(kind, 'browsable') && stat.isDirectory();
135
+
136
+ const deepWarn = [];
137
+ if (isTree && !rel) {
138
+ // ⭐**根站不盲走。** 扁平化后「根 index.html = 一个站」是常态,而根站的「树」
139
+ // 就是整个工作区 —— 盲走会把项目基础设施和**别的任务**一起卷进来。
140
+ // 正解是问权威解析器要页面清单:`taskManifest` 的根站条目(site / 非 single /
141
+ // 无 srcRoot,跟 assets.js 判 rootSite 同一口径)里的 `pages` 就是它认领了哪些页。
142
+ // 别的任务的 index.html 不在这份 pages 里,天然被排除;页面引用到的 css/js/图
143
+ // 由下面的 refs 收。这比我自己拿一张保留目录表去猜要靠谱 —— 谁属于这个站,
144
+ // 只有解析器说了算。
145
+ const m = await taskManifest(workspaceRoot).catch(() => null);
146
+ const rootSite = (m?.artifacts || []).find(a => a.kind === 'site' && !a.single && !a.srcRoot);
147
+ for (const pg of (rootSite?.pages || [])) {
148
+ const pRel = String(pg).replace(/\\/g, '/');
149
+ const pAbs = await safeResolve(workspaceRoot, pRel);
150
+ if (!pAbs) continue;
151
+ try { if ((await fs.stat(pAbs)).isFile()) files.push({ abs: pAbs, rel: pRel }); } catch { /* 页面刚被删 */ }
152
+ }
153
+ if (!files.length) {
154
+ // 解析器认不出根站(还没写出 index.html 之类)→ 退回入口文件本身,
155
+ // 绝不退回「盲走整个工作区」
156
+ const entryAbs = await safeResolve(workspaceRoot, 'index.html');
157
+ if (entryAbs) {
158
+ try { if ((await fs.stat(entryAbs)).isFile()) files.push({ abs: entryAbs, rel: 'index.html' }); } catch { /* */ }
159
+ }
160
+ }
161
+ } else if (isTree) {
162
+ const ignoreBaseRel = await ignoreBaseFor(workspaceRoot, rel);
163
+ const ignoreBaseAbs = (await safeResolve(workspaceRoot, ignoreBaseRel)) || workspaceRoot;
164
+ const ignore = await loadIgnore(ignoreBaseAbs);
165
+ const walked = await walkTaskFiles(abs, { maxDepth: 6, ignore, ignoreBase: ignoreBaseAbs });
166
+ for (const f of walked) {
167
+ const r = path.relative(workspaceRoot, f.abs).replace(/\\/g, '/');
168
+ files.push({ abs: f.abs, rel: r });
169
+ // maxDepth 撞顶是静默截断的(walkTaskFiles 不给信号)。贴着上限的文件说明
170
+ // 这棵树很深,更深的东西可能没进包 —— 报出来,别让人以为齐了。
171
+ // ⚠️ 深度要按 **walk 相对卡根** 的路径算(`f.rel`),不是工作区相对的 `r`:
172
+ // 嵌套站 `客户/官网/a/b/c/x.png` 在工作区坐标下有 6 段,但 walk 只走了 4 层,
173
+ // 离截断远着 —— 用错坐标系会往交付物里印假警报。
174
+ if (f.rel.split('/').length >= 6) deepWarn.push(r);
175
+ }
176
+ } else if (kind === 'docx' && stat.isDirectory()) {
177
+ // word 文件夹卡(`docx:报告`):地址是文件夹,导出对象是主成员 —— 窗里的
178
+ // 导出按钮会点名具体成员(`docx:报告/文档v2.docx`),走上面的单文件分支;
179
+ // 这条是卡级导出的兜底。挑主成员的判据在 kinds/docx.js,别在这儿抄第二份。
180
+ let names = [];
181
+ try { names = kindDef('docx').sortDocxNames(await fs.readdir(abs)); } catch { /* 下面报 404 */ }
182
+ if (!names.length) throw Object.assign(new Error(`${rel} 里没有 .docx`), { status: 404 });
183
+ rel = `${rel}/${names[0]}`;
184
+ files.push({ abs: path.join(abs, names[0]), rel });
185
+ } else {
186
+ if (!stat.isFile()) throw Object.assign(new Error(`${rel} 不是文件`), { status: 400 });
187
+ files.push({ abs, rel });
188
+ }
189
+
190
+ if (!files.length) {
191
+ throw Object.assign(new Error(`${rel} 里没有可导出的文件`), { status: 400 });
192
+ }
193
+
194
+ // 树外引用只认 `assets/` 和产物自己的目录(旧 /exports/site 有这道闸,别丢)。
195
+ // 根层产物的自有前缀是空串 = 不设闸:它的树本来就是整个工作区,这条认账。
196
+ const ownPrefix = isTree ? (rel ? `${rel}/` : '') : (path.posix.dirname(rel) === '.' ? '' : `${path.posix.dirname(rel)}/`);
197
+ // 只有**标记语言**产物需要跟着引用扫下去。`browsable` 正好是这条线:能用浏览器
198
+ // 打开 = 由 html/css 组成 = 有外部引用要带。docx 是自包含的 zip(图片字体都在
199
+ // 包里),拿二进制去跑引用扫描器是白费力气,还可能在压缩字节里撞出假路径。
200
+ // 问注册表和能力,不问形态名 —— 加第四种形态时这行不用动。
201
+ const scannable = !!kindDef(kind) && can(kind, 'browsable');
202
+ let refs = []; let unresolved = []; let candidates = [];
203
+ if (scannable) {
204
+ // ⭐**传递引用要跟着扫下去。** html 引 css、css 再引图 —— 只扫产物自身的话,
205
+ // 那张图既不进包也不进清单(用户拍的规矩里最坏的状态),解压即裂图且零预警。
206
+ // deck 卡 / 单页站 / 根站三类全中,因为它们的 files 里只有页面本身。
207
+ // 定点到不动为止,最多四轮(防环、也防超深链把单次导出拖垮)。
208
+ const allow = ownPrefix === '' ? [] : [ownPrefix, 'assets/'];
209
+ const scanned = new Set(files.map(f => f.rel));
210
+ let batch = files;
211
+ for (let round = 0; round < 4 && batch.length; round++) {
212
+ const r = await collectAssetRefs({ files: batch, baseRoot: workspaceRoot, allowPrefixes: allow });
213
+ refs.push(...r.refs); unresolved.push(...r.unresolved); candidates.push(...r.candidates);
214
+ batch = [];
215
+ for (const rr of r.refs) {
216
+ if (scanned.has(rr) || !/\.(html?|css|m?js|cjs)$/i.test(rr)) continue;
217
+ scanned.add(rr);
218
+ const a = await safeResolve(workspaceRoot, rr);
219
+ if (a) batch.push({ abs: a, rel: rr });
220
+ }
221
+ }
222
+ refs = [...new Set(refs)];
223
+ }
224
+ if (deepWarn.length) {
225
+ unresolved.push({
226
+ from: rel || '(工作区根)',
227
+ why: `目录深度贴到上限 6 层,更深的文件没有进包(命中 ${deepWarn.length} 个)`,
228
+ snippet: deepWarn.slice(0, 3).join(' / '),
229
+ });
230
+ }
231
+
232
+ const assets = []; const missing = [];
233
+ const ownSet = new Set(files.map(f => f.rel));
234
+ // 脚本里的完整字面量路径(画廊 / lightbox 那种):**磁盘上真有这个文件就不是猜**,
235
+ // 提升成素材;解析不到的仍然只进清单。这不违反「扫不到的不多塞」——
236
+ // 它扫得到,只是来源是脚本而不是标签。
237
+ for (const c of candidates) {
238
+ const a = await safeResolve(workspaceRoot, c.rel);
239
+ if (!a || ownSet.has(c.rel)) continue;
240
+ try {
241
+ if ((await fs.stat(a)).isFile()) { refs.push(c.rel); continue; }
242
+ } catch { /* 不存在 */ }
243
+ unresolved.push({ from: c.from, why: '脚本里的字符串字面量(磁盘上没找到对应文件)', snippet: c.snippet });
244
+ }
245
+ refs = [...new Set(refs)].sort();
246
+ for (const r of refs) {
247
+ const a = await safeResolve(workspaceRoot, r);
248
+ if (!a) continue; // 软链逃逸的引用直接不收
249
+ let st = null;
250
+ try { st = await fs.stat(a); } catch { missing.push(r); continue; }
251
+ if (st.isFile()) { assets.push({ abs: a, rel: r }); continue; }
252
+ if (st.isDirectory()) {
253
+ // pretty-URL(`href="about/"`)指的是 `about/index.html`,不是裂图。
254
+ // 不处理的话每个多页站导出都会带一堆假的缺失警报。
255
+ const idxRel = `${r.replace(/\/$/, '')}/index.html`;
256
+ if (ownSet.has(idxRel)) continue;
257
+ const idxAbs = await safeResolve(workspaceRoot, idxRel);
258
+ if (idxAbs) {
259
+ try {
260
+ if ((await fs.stat(idxAbs)).isFile()) { assets.push({ abs: idxAbs, rel: idxRel }); continue; }
261
+ } catch { /* 落 missing */ }
262
+ }
263
+ }
264
+ missing.push(r);
265
+ }
266
+
267
+ return {
268
+ cardId, kind, rel, single,
269
+ title: path.basename(rel) || rel || '产物',
270
+ files, assets, missing, unresolved,
271
+ exportFormats: exportFormatsFor(kind, { single }),
272
+ };
273
+ }
274
+
275
+ /**
276
+ * 按类型收一批卡(「导出全部图片」这种)。
277
+ * 单张失败不拖累整批 —— 收不到的记进 skipped,让人看见少了什么。
278
+ */
279
+ export async function collectCards({ workspaceRoot, cardIds }) {
280
+ const bundles = []; const skipped = [];
281
+ for (const id of cardIds) {
282
+ try {
283
+ bundles.push(await collectCard({ workspaceRoot, cardId: id }));
284
+ } catch (err) {
285
+ skipped.push({ cardId: id, reason: err.message, status: err.status || 500 });
286
+ }
287
+ }
288
+ return { bundles, skipped };
289
+ }
@@ -0,0 +1,210 @@
1
+ /**
2
+ * lib/export-package.js — 把收集到的产物打成用户拿得走的东西(2026-08-17)
3
+ *
4
+ * 分两个出口,各自说清给谁:
5
+ * **看的出口** raw / zip —— 给不动手的人。原件或一个压缩包,双击就能看。
6
+ * **接手的出口** handoff(工程包)—— 给要继续开发的人。除了源码,还带一份
7
+ * 「这是什么、怎么跑、mock 数据在哪、你的后端要实现哪些接口」。
8
+ *
9
+ * ⭐**zip 布局保持工作区相对路径,不做任何改写。**
10
+ * 旧交付包把东西塞进 `design/` 再回头重写 `../../assets/` 前缀,于是有了一整类
11
+ * 裂图 bug(深度算错一层,图全裂)。保持原布局的话,产物里的相对引用**天然成立**,
12
+ * 零改写、零深度计算 —— 那类 bug 从根上不存在。代价只是解压出来多一层目录,
13
+ * 换来的是"打开就对"。
14
+ */
15
+
16
+ import path from 'node:path';
17
+ import fs from 'node:fs/promises';
18
+ import JSZip from 'jszip';
19
+ import { renderUnresolvedReport } from './asset-refs.js';
20
+
21
+ /** 文件名里不能出现的东西(路径分隔与父目录) */
22
+ export function safeName(name, fallback = '导出') {
23
+ const s = String(name || '').replace(/[/\\]/g, '_').replace(/\.\.+/g, '.').trim();
24
+ return s || fallback;
25
+ }
26
+
27
+ /**
28
+ * 从 `api.js` 那层抽出后端接口清单。
29
+ *
30
+ * 应用道的约定是:所有数据访问走一层 `api/` 函数,每个函数右边注上建议路由
31
+ * (`// GET /api/posts`)。**那列注释就是交接文档的草稿** —— 这里把它抽成表,
32
+ * 用户拿到的就不只是前端,还有一份现成的后端需求规格。这是「用了模拟数据」
33
+ * 从缺陷变成资产的那一步。
34
+ *
35
+ * 抓的是 `export const 名字 = ... // METHOD /path` 和 `export async function 名字`。
36
+ */
37
+ export function extractApiContract(source) {
38
+ const rows = [];
39
+ // ⚠️ 标识符类必须认 Unicode。JS 的标识符本来就允许非 ASCII,而这个产品里
40
+ // agent 写的代码大量用中文命名(`export const 列出书目 = …`)—— 用
41
+ // `[A-Za-z_$]` 会把它们整片漏掉,接口表少一半还不报错。
42
+ const lineRe = /export\s+(?:const|let|async\s+function|function)\s+([\p{L}_$][\p{L}\p{N}_$]*)[^\n]*?(?:\/\/\s*(GET|POST|PUT|PATCH|DELETE)\s+(\S+))?\s*$/gmu;
43
+ let m;
44
+ while ((m = lineRe.exec(source)) !== null) {
45
+ const [, name, method, route] = m;
46
+ if (!name) continue;
47
+ rows.push({ name, method: method || null, route: route || null });
48
+ }
49
+ return rows;
50
+ }
51
+
52
+ /** 在收到的文件里找 api 层(`app/api.js` / `src/api/index.js` / 任何 `api.js`) */
53
+ function findApiFile(files) {
54
+ const score = (rel) => {
55
+ if (/(^|\/)app\/api\.js$/i.test(rel)) return 3;
56
+ if (/(^|\/)api\/index\.js$/i.test(rel)) return 2;
57
+ if (/(^|\/)api\.js$/i.test(rel)) return 1;
58
+ return 0;
59
+ };
60
+ return files.map(f => ({ f, s: score(f.rel) })).filter(x => x.s > 0)
61
+ .sort((a, b) => b.s - a.s)[0]?.f || null;
62
+ }
63
+
64
+ /** 有没有 package.json,决定 README 里写「双击打开」还是「npm i && npm run build」 */
65
+ function findPackageJson(files) {
66
+ return files.find(f => /(^|\/)package\.json$/.test(f.rel)) || null;
67
+ }
68
+
69
+ /**
70
+ * 工程包的 README —— 交付物的一半。
71
+ * 没有它,用户拿到的是一堆他不敢动的文件。
72
+ */
73
+ async function renderHandoffReadme({ projectName, bundles, apiRows, pkg, missing, hasMock }) {
74
+ const titles = bundles.map(b => b.title).join('、');
75
+ const L = [];
76
+ L.push(`# ${projectName || titles || '设计交付'}`, '');
77
+ L.push(`本包由 NoDesign 导出,包含 ${bundles.length} 份产物:${titles}。`, '');
78
+
79
+ L.push('## 怎么打开', '');
80
+ if (pkg) {
81
+ L.push('这是一个需要构建的工程:', '', '```bash', 'npm install', 'npm run build', '```', '');
82
+ L.push('构建产物在 `dist/`(或 package.json 里 build 脚本指定的目录)。**`node_modules` 不在包里**,靠上面这两行装回来。', '');
83
+ } else {
84
+ L.push('直接用浏览器打开入口 html 就行,不需要装任何东西。页面之间是相对链接,不依赖服务器。', '');
85
+ }
86
+
87
+ if (hasMock) {
88
+ L.push('## 现在的数据是假的', '');
89
+ L.push('所有数据来自 `mock` / `seed` 那一层,写操作存在浏览器 localStorage 里。清掉浏览器数据就回到初始状态。', '');
90
+ L.push('**这是一个可运行的原型,不是上线的系统** —— 托管和真实数据是你自己的事。', '');
91
+ }
92
+
93
+ if (apiRows.length) {
94
+ L.push('## 换成真后端要改哪里', '');
95
+ L.push('只改 api 那一层文件:把每个函数从调用 mock 改成 `fetch`。**页面代码一行都不用动。**', '');
96
+ L.push('你的后端需要提供这些接口:', '');
97
+ L.push('| 函数 | 建议路由 |', '|---|---|');
98
+ for (const r of apiRows) {
99
+ L.push(`| \`${r.name}\` | ${r.method && r.route ? `\`${r.method} ${r.route}\`` : '(源码里没注路由,自己定)'} |`);
100
+ }
101
+ L.push('');
102
+ }
103
+
104
+ if (missing.length) {
105
+ L.push('## ⚠️ 有几个引用没找到对应文件', '');
106
+ L.push('下面这些是页面里引用了、但打包时磁盘上没有的(原工程里可能已经删了或改名了):', '');
107
+ for (const m of missing.slice(0, 30)) L.push(`- \`${m}\``);
108
+ if (missing.length > 30) L.push(`- …还有 ${missing.length - 30} 条`);
109
+ L.push('');
110
+ }
111
+
112
+ L.push('## 包里有什么', '');
113
+ L.push('目录结构跟原工作区一致,**没有做任何路径改写** —— 所以页面里的相对引用原样成立,解压出来直接能用。', '');
114
+ return L.join('\n');
115
+ }
116
+
117
+ /**
118
+ * 打包。
119
+ *
120
+ * @param {Array} bundles collectCard 的产出
121
+ * @param {object} opts
122
+ * @param {'raw'|'zip'|'md'|'handoff'|'site'} opts.format
123
+ * @param {string} [opts.projectName]
124
+ * @returns {Promise<{filename:string, buffer:Buffer, mime:string}>}
125
+ */
126
+ export async function packageBundles(bundles, { format, projectName } = {}) {
127
+ if (!bundles?.length) {
128
+ throw Object.assign(new Error('没有可导出的产物'), { status: 400 });
129
+ }
130
+
131
+ // 原件直下:只有单张卡、且只有一个文件时成立
132
+ if (format === 'raw') {
133
+ if (bundles.length !== 1 || bundles[0].files.length !== 1) {
134
+ throw Object.assign(new Error('原件直下只能对单个文件用,多个请用 zip'), { status: 400 });
135
+ }
136
+ const f = bundles[0].files[0];
137
+ return {
138
+ filename: path.basename(f.rel),
139
+ buffer: await fs.readFile(f.abs),
140
+ mime: 'application/octet-stream',
141
+ };
142
+ }
143
+
144
+ // 便签合并成一份 markdown
145
+ if (format === 'md') {
146
+ const parts = [];
147
+ for (const b of bundles) {
148
+ for (const f of b.files) {
149
+ if (!/\.md$/i.test(f.rel)) continue;
150
+ parts.push(`<!-- ${f.rel} -->\n\n${await fs.readFile(f.abs, 'utf8')}`);
151
+ }
152
+ }
153
+ if (!parts.length) throw Object.assign(new Error('这些卡里没有 markdown 可合并'), { status: 400 });
154
+ return {
155
+ filename: `${safeName(projectName, '便签')}.md`,
156
+ buffer: Buffer.from(parts.join('\n\n---\n\n'), 'utf8'),
157
+ mime: 'text/markdown; charset=utf-8',
158
+ };
159
+ }
160
+
161
+ const zip = new JSZip();
162
+ const seen = new Set();
163
+
164
+ // 裹不裹一层:**只在真会「解压即散落」时裹**。
165
+ // 根级站点的文件就住在工作区根(`index.html`、`lib/…`),不裹的话 unzip 会把
166
+ // 一堆东西直接倒进当前目录 —— 旧的 /exports/site 靠 `site/` 前缀避开了这点,
167
+ // 换新管线不能把它丢了。判据就是"有没有文件贴在包根上"。
168
+ // ⚠️ 裹是**整体平移**,所有文件和素材同加一个前缀,相对引用原样成立 ——
169
+ // 仍然零改写,不碰旧那套按深度重写 `../assets/` 的老路(那是裂图事故的根)。
170
+ const allRels = bundles.flatMap(b => [...b.files, ...b.assets].map(f => f.rel));
171
+ const needsWrap = allRels.some(r => !r.includes('/'));
172
+ const wrapBase = needsWrap ? `${safeName(projectName || bundles[0].title, '导出')}/` : '';
173
+ const allMissing = []; const allUnresolved = [];
174
+ const allFiles = []; const allAssets = [];
175
+
176
+ for (const b of bundles) {
177
+ allMissing.push(...b.missing);
178
+ allUnresolved.push(...b.unresolved);
179
+ allFiles.push(...b.files);
180
+ allAssets.push(...b.assets);
181
+ for (const f of [...b.files, ...b.assets]) {
182
+ if (seen.has(f.rel)) continue; // 多张卡引用同一张图时只进一次
183
+ seen.add(f.rel);
184
+ try { zip.file(wrapBase + f.rel, await fs.readFile(f.abs)); } catch { /* 中途被删就跳过 */ }
185
+ }
186
+ }
187
+
188
+ // 没解析出来的引用写成清单。**没有就不放** —— 旧包里那个常年空着的
189
+ // prompt.txt 就是"占位符放着放着就成了垃圾"的样板。
190
+ const report = renderUnresolvedReport(allUnresolved);
191
+ if (report) zip.file('未解析的引用.md', report);
192
+
193
+ if (format === 'handoff') {
194
+ const apiFile = findApiFile(allFiles);
195
+ const apiRows = apiFile ? extractApiContract(await fs.readFile(apiFile.abs, 'utf8').catch(() => '')) : [];
196
+ const pkg = findPackageJson(allFiles);
197
+ const hasMock = allFiles.some(f => /(^|\/)(mock|seed)[./]/i.test(f.rel));
198
+ zip.file('README.md', await renderHandoffReadme({
199
+ projectName, bundles, apiRows, pkg, missing: allMissing, hasMock,
200
+ }));
201
+ }
202
+
203
+ const base = safeName(projectName || bundles[0].title, '导出');
204
+ const suffix = format === 'handoff' ? '-工程包' : '';
205
+ return {
206
+ filename: `${base}${suffix}.zip`,
207
+ buffer: await zip.generateAsync({ type: 'nodebuffer', compression: 'DEFLATE' }),
208
+ mime: 'application/zip',
209
+ };
210
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * lib/html-srcset.js — 给站点 HTML 的 <img> 注入 srcset
3
+ *
4
+ * 问题:agent 写的站点是 `<img src="assets/x.png">`,手机上也照发 1536px 宽的原图。
5
+ * 站点按真实设备宽取景,服务端不能自作主张缩尺寸(缩了桌面就糊),所以只能把
6
+ * "有哪些尺寸可选"告诉浏览器,让它按视口和 DPR 自己挑。
7
+ *
8
+ * 做法:只加属性,**不改 DOM 结构**。
9
+ *
10
+ * <img src="assets/x.png">
11
+ * →
12
+ * <img src="assets/x.png?v=ab12ef" sizes="100vw"
13
+ * srcset="assets/x.png?w=480&v=ab12ef 480w, assets/x.png?w=960&v=ab12ef 960w, assets/x.png 1536w">
14
+ *
15
+ * **为什么不用 <picture>**:<picture> 能按 type 分发 avif/webp 且不需要 Vary,
16
+ * 技术上更优。但它要在 <img> 外面套一层元素,而站点 HTML 是 agent 写的,
17
+ * `.grid > img` 这类直接子选择器会失配,img 作为 grid/flex item 时盒子也会变成
18
+ * picture(默认 inline)导致布局位移。为了压几十 KB 去动别人的布局不划算。
19
+ * 所以格式仍走 Accept 协商(响应带 Vary: Accept),这里只管尺寸。
20
+ *
21
+ * sizes 的取值:默认 100vw。这是"高估"的一侧 —— 浏览器会挑偏大的那档,省得少
22
+ * 但绝不会糊。反过来低估会让图变糊,那是肉眼可见的质量事故。真正的收益在移动端:
23
+ * 480 CSS px 的手机按 DPR 2 只需要 960w,不再拉 1536。img 自己写了 width 属性或
24
+ * 内联 px 宽度时按它算,那是 agent 明确表达过的意图,比默认值准。
25
+ *
26
+ * 跳过的情况:已有 srcset(agent 自己写过)、非本地相对路径(http/data/协议相对)、
27
+ * 拿不到原图尺寸、原图本来就比最小那档还窄。
28
+ */
29
+
30
+ import path from 'node:path';
31
+ import fs from 'node:fs/promises';
32
+ import sharp from 'sharp';
33
+ import { RESPONSIVE_WIDTHS, shouldTranscode, versionTag } from './image-variant.js';
34
+
35
+ /** 图片元数据缓存:绝对路径 → { key, width, height, v }。key 带 mtime+size,源变了自然失效 */
36
+ const metaCache = new Map();
37
+ const META_CACHE_MAX = 2000;
38
+
39
+ async function imageMeta(absPath) {
40
+ let stat;
41
+ try {
42
+ stat = await fs.stat(absPath);
43
+ if (!stat.isFile()) return null;
44
+ } catch { return null; }
45
+
46
+ const key = `${stat.mtimeMs}|${stat.size}`;
47
+ const hit = metaCache.get(absPath);
48
+ if (hit && hit.key === key) return hit;
49
+
50
+ let meta;
51
+ try {
52
+ meta = await sharp(await fs.readFile(absPath)).metadata();
53
+ } catch { return null; }
54
+ if (!meta?.width) return null;
55
+
56
+ const entry = { key, width: meta.width, height: meta.height || 0, v: versionTag(stat) };
57
+ if (metaCache.size >= META_CACHE_MAX) metaCache.delete(metaCache.keys().next().value);
58
+ metaCache.set(absPath, entry);
59
+ return entry;
60
+ }
61
+
62
+ /** <img ...> 整标签。属性值里不会出现 '>' 的常规写法,够用且不引入 HTML parser 依赖 */
63
+ const IMG_TAG_RE = /<img\b([^>]*)>/gi;
64
+
65
+ function attrOf(attrs, name) {
66
+ const m = attrs.match(new RegExp(`\\b${name}\\s*=\\s*("([^"]*)"|'([^']*)')`, 'i'));
67
+ return m ? (m[2] ?? m[3] ?? '') : null;
68
+ }
69
+
70
+ /**
71
+ * 这个 img 打算显示多宽(CSS px)。拿不准返 null,调用方退回 100vw。
72
+ * 只认 width 属性和内联 style 里的 px 宽度,百分比 / vw / rem 一律不猜。
73
+ */
74
+ function declaredWidth(attrs) {
75
+ const w = attrs.match(/\bwidth\s*=\s*["']?(\d+)/i);
76
+ if (w) return Number(w[1]);
77
+ const style = attrOf(attrs, 'style');
78
+ const m = style && style.match(/(?:^|;)\s*width\s*:\s*(\d+(?:\.\d+)?)px/i);
79
+ return m ? Math.round(Number(m[1])) : null;
80
+ }
81
+
82
+ function appendQuery(url, params) {
83
+ const [base, hash = ''] = url.split('#');
84
+ const sep = base.includes('?') ? '&' : '?';
85
+ return `${base}${sep}${params}${hash ? '#' + hash : ''}`;
86
+ }
87
+
88
+ /**
89
+ * 给一份 HTML 里的 <img> 加 srcset/sizes。
90
+ *
91
+ * @param {string} html
92
+ * @param {string} baseDir 这份 HTML 所在目录的绝对路径(相对 src 按它解析)
93
+ * @param {string} rootGuard 允许触及的根(防路径逃逸;解析结果必须在它之下)
94
+ * @returns {Promise<string>}
95
+ */
96
+ export async function injectSrcset(html, baseDir, rootGuard) {
97
+ if (!html || !/<img\b/i.test(html)) return html;
98
+
99
+ // 先把要查的 src 收齐,去重后并发查元数据,避免同一张图查 N 遍
100
+ const tags = [...html.matchAll(IMG_TAG_RE)];
101
+ const wanted = new Map(); // src → absPath
102
+ for (const [, attrs] of tags) {
103
+ const src = attrOf(attrs, 'src');
104
+ if (!src || wanted.has(src)) continue;
105
+ if (attrOf(attrs, 'srcset') !== null) continue; // agent 自己写过
106
+ if (/^(?:[a-z][a-z0-9+\-.]*:|\/\/|#)/i.test(src)) continue; // 绝对 / data: / 协议相对
107
+ if (!shouldTranscode(path.extname(src.split('?')[0]))) continue;
108
+ const abs = path.resolve(baseDir, decodeURIComponent(src.split('?')[0]));
109
+ if (abs !== rootGuard && !abs.startsWith(rootGuard + path.sep)) continue;
110
+ wanted.set(src, abs);
111
+ }
112
+ if (!wanted.size) return html;
113
+
114
+ const metas = new Map();
115
+ await Promise.all([...wanted].map(async ([src, abs]) => {
116
+ const m = await imageMeta(abs);
117
+ if (m) metas.set(src, m);
118
+ }));
119
+ if (!metas.size) return html;
120
+
121
+ return html.replace(IMG_TAG_RE, (whole, attrs) => {
122
+ const src = attrOf(attrs, 'src');
123
+ const meta = src && metas.get(src);
124
+ if (!meta) return whole;
125
+
126
+ // 比原图还宽的档没有意义(withoutEnlargement 会让它跟原图一样大,白占缓存)
127
+ const widths = RESPONSIVE_WIDTHS.filter(w => w < meta.width);
128
+ if (!widths.length) return whole;
129
+
130
+ const candidates = widths.map(w => `${appendQuery(src, `w=${w}&v=${meta.v}`)} ${w}w`);
131
+ candidates.push(`${appendQuery(src, `v=${meta.v}`)} ${meta.width}w`);
132
+
133
+ const dw = declaredWidth(attrs);
134
+ const sizes = dw ? `${dw}px` : '100vw';
135
+
136
+ let next = attrs;
137
+ if (attrOf(attrs, 'sizes') === null) next += ` sizes="${sizes}"`;
138
+ next += ` srcset="${candidates.join(', ')}"`;
139
+ // loading/decoding 是白送的:站点常常一屏之外还有十几张图,浏览器原生懒加载
140
+ // 能把它们整个推迟掉,比任何编码优化都省。已经写了的不覆盖。
141
+ if (attrOf(attrs, 'loading') === null) next += ' loading="lazy"';
142
+ if (attrOf(attrs, 'decoding') === null) next += ' decoding="async"';
143
+ return `<img${next}>`;
144
+ });
145
+ }