@xiaobuyu/nodesign 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (358) hide show
  1. package/README.md +156 -106
  2. package/bin/nodesign.js +0 -0
  3. package/package.json +1 -1
  4. package/server/api/README.md +45 -45
  5. package/server/api/_guard.js +75 -75
  6. package/server/api/admin.js +187 -187
  7. package/server/api/assets/docx-page.js +103 -103
  8. package/server/api/assets/helpers.js +42 -22
  9. package/server/api/assets/notes.js +194 -144
  10. package/server/api/assets.js +891 -896
  11. package/server/api/board.js +134 -57
  12. package/server/api/browse.js +138 -138
  13. package/server/api/canvas.js +444 -444
  14. package/server/api/chatai.js +324 -324
  15. package/server/api/exports/build-standalone.js +980 -980
  16. package/server/api/exports/cards.js +118 -118
  17. package/server/api/exports/docx-pdf.js +46 -46
  18. package/server/api/exports/export-page.js +142 -142
  19. package/server/api/exports/handoff.js +147 -147
  20. package/server/api/exports.js +690 -690
  21. package/server/api/instruction.js +75 -75
  22. package/server/api/me.js +116 -116
  23. package/server/api/memory.js +185 -185
  24. package/server/api/pending-changes.js +366 -366
  25. package/server/api/plugins.js +164 -164
  26. package/server/api/projects.js +220 -220
  27. package/server/api/publish.js +64 -64
  28. package/server/api/recent.js +73 -73
  29. package/server/api/sessions.js +589 -589
  30. package/server/api/skills.js +77 -77
  31. package/server/api/standalone-fit.js +168 -168
  32. package/server/api/turn-compose.js +180 -180
  33. package/server/api/turn-inflight.js +44 -44
  34. package/server/auth/README.md +17 -17
  35. package/server/auth/middleware.js +138 -138
  36. package/server/auth/origin-guard.js +71 -71
  37. package/server/auth/session.js +108 -108
  38. package/server/auth/tier.js +113 -113
  39. package/server/auth/users-store.js +348 -348
  40. package/server/edit/README.md +29 -29
  41. package/server/engine/README.md +113 -113
  42. package/server/engine/agent/agent-shared.js +550 -550
  43. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
  44. package/server/engine/agent/auto-mode-rules.js +78 -78
  45. package/server/engine/agent/context.js +418 -418
  46. package/server/engine/agent/events.js +465 -464
  47. package/server/engine/agent/hooks/canvas-validate.js +199 -199
  48. package/server/engine/agent/hooks/failure.js +120 -120
  49. package/server/engine/agent/hooks/file-events.js +75 -75
  50. package/server/engine/agent/hooks/lifecycle.js +233 -233
  51. package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
  52. package/server/engine/agent/hooks/post-guidance.js +206 -206
  53. package/server/engine/agent/hooks/post-subagent-report.js +104 -104
  54. package/server/engine/agent/hooks/post-trim.js +58 -58
  55. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
  56. package/server/engine/agent/hooks/pre-defaults.js +84 -84
  57. package/server/engine/agent/hooks/pre-injectors.js +277 -277
  58. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
  59. package/server/engine/agent/hooks/pre-starter-files.js +61 -61
  60. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
  61. package/server/engine/agent/hooks/site-validate.js +94 -94
  62. package/server/engine/agent/hooks/tool-prompts.js +31 -31
  63. package/server/engine/agent/hooks/turn-state-memory.js +56 -56
  64. package/server/engine/agent/hooks/user-prompt-submit.js +258 -215
  65. package/server/engine/agent/hooks.js +354 -354
  66. package/server/engine/agent/init-contract.js +69 -69
  67. package/server/engine/agent/isolation.js +159 -159
  68. package/server/engine/agent/model-table.js +292 -292
  69. package/server/engine/agent/plugin-loader.js +266 -266
  70. package/server/engine/agent/prompts/nodesign-prelude.md +388 -336
  71. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
  72. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
  73. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
  74. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
  75. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
  76. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
  77. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
  78. package/server/engine/agent/prompts/tools/site-reference.md +106 -106
  79. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
  80. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
  81. package/server/engine/agent/session-loop.js +1035 -1024
  82. package/server/engine/agent/session-model.js +164 -164
  83. package/server/engine/agent/skill.js +204 -204
  84. package/server/engine/agent/task-events.js +78 -78
  85. package/server/engine/agents/ds-extractor.md +132 -132
  86. package/server/engine/agents/explorer.md +190 -190
  87. package/server/engine/agents/index.js +230 -230
  88. package/server/engine/agents/schemas/design-system.json +156 -156
  89. package/server/engine/agents/schemas/tweak-schema.json +113 -113
  90. package/server/engine/agents/tweak-proposer.md +127 -127
  91. package/server/engine/agents/vision-checker.md +254 -254
  92. package/server/engine/browse/capture.js +422 -422
  93. package/server/engine/browse/card.js +137 -137
  94. package/server/engine/browse/handover.js +59 -59
  95. package/server/engine/browse/page-digest.js +119 -119
  96. package/server/engine/browse/refs.js +196 -196
  97. package/server/engine/browse/registry.js +307 -307
  98. package/server/engine/browse/screencast.js +170 -170
  99. package/server/engine/browse/state.js +121 -121
  100. package/server/engine/chatai/chat-log.js +79 -79
  101. package/server/engine/chatai/index.js +193 -193
  102. package/server/engine/chatai/openai-compat.js +152 -152
  103. package/server/engine/chatai/orchestrate.js +309 -309
  104. package/server/engine/chatai/perform.js +40 -40
  105. package/server/engine/chatai/summarize.js +79 -79
  106. package/server/engine/mcp/capability-gate.js +52 -52
  107. package/server/engine/mcp/index.js +351 -339
  108. package/server/engine/mcp/param-sanitizer.js +142 -142
  109. package/server/engine/mcp/tools/arrange-on-board.js +124 -124
  110. package/server/engine/mcp/tools/artifact-session.js +278 -278
  111. package/server/engine/mcp/tools/browse-computer.js +421 -421
  112. package/server/engine/mcp/tools/browse-find-batch.js +183 -183
  113. package/server/engine/mcp/tools/browse-screenshot.js +157 -157
  114. package/server/engine/mcp/tools/browse.js +480 -480
  115. package/server/engine/mcp/tools/build-docx.js +101 -101
  116. package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
  117. package/server/engine/mcp/tools/create-on-board.js +152 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/edit-sketch.js +167 -0
  120. package/server/engine/mcp/tools/explain-style.js +237 -237
  121. package/server/engine/mcp/tools/export-handoff.js +133 -133
  122. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  123. package/server/engine/mcp/tools/generate-image.js +842 -842
  124. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  125. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  126. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  127. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  128. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  129. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  130. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  131. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  132. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  133. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  134. package/server/engine/mcp/tools/highlight.js +68 -68
  135. package/server/engine/mcp/tools/list-pages.js +222 -222
  136. package/server/engine/mcp/tools/look-at-board.js +124 -0
  137. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  138. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  139. package/server/engine/mcp/tools/organize-board.js +64 -64
  140. package/server/engine/mcp/tools/paint-still.js +434 -434
  141. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  142. package/server/engine/mcp/tools/preview-deck.js +130 -130
  143. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  144. package/server/engine/mcp/tools/publish-site.js +102 -102
  145. package/server/engine/mcp/tools/query-elements.js +195 -195
  146. package/server/engine/mcp/tools/read-board.js +190 -103
  147. package/server/engine/mcp/tools/read-document.js +93 -93
  148. package/server/engine/mcp/tools/read-page.js +264 -264
  149. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  150. package/server/engine/mcp/tools/read-user-view.js +59 -0
  151. package/server/engine/mcp/tools/record-decision.js +140 -140
  152. package/server/engine/mcp/tools/relate-on-board.js +112 -110
  153. package/server/engine/mcp/tools/report-issue.js +115 -115
  154. package/server/engine/mcp/tools/roll-film.js +266 -266
  155. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  156. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  157. package/server/engine/mcp/tools/screenshot.js +583 -583
  158. package/server/engine/mcp/tools/sketch-on-board.js +337 -0
  159. package/server/engine/mcp/tools/tier-gate.js +96 -96
  160. package/server/engine/mcp/tools/trace-motion.js +215 -215
  161. package/server/engine/mcp/tools/web-search.js +548 -548
  162. package/server/engine/mcp/tools/write-on-board.js +130 -0
  163. package/server/engine/motion/inventory.js +349 -349
  164. package/server/engine/perception/session.js +235 -235
  165. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +199 -192
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  174. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +169 -162
  175. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  176. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  177. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +235 -228
  178. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -78
  179. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  180. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +330 -323
  181. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  182. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  183. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  185. package/server/engine/runs/active-runs.js +698 -698
  186. package/server/engine/runs/board-tasklist.js +130 -0
  187. package/server/engine/runs/live-turn.js +252 -252
  188. package/server/engine/runs/store.js +364 -364
  189. package/server/engine/runs/turn-relay.js +217 -217
  190. package/server/engine/runtime/workspace.js +164 -164
  191. package/server/index.js +3 -0
  192. package/server/lib/artifact-file-path.js +80 -80
  193. package/server/lib/artifact-target.js +284 -284
  194. package/server/lib/asset-refs.js +239 -239
  195. package/server/lib/async-queue.js +104 -104
  196. package/server/lib/auto-relations.js +130 -130
  197. package/server/lib/binding-types.js +72 -60
  198. package/server/lib/board-graph-export.js +191 -0
  199. package/server/lib/board-groups.js +167 -0
  200. package/server/lib/board-hero.js +76 -0
  201. package/server/lib/board-kind-sizes.js +85 -62
  202. package/server/lib/board-relations.js +133 -133
  203. package/server/lib/browse-proxy.js +180 -180
  204. package/server/lib/canvas-id.js +36 -36
  205. package/server/lib/chalk.js +121 -0
  206. package/server/lib/cover.js +227 -227
  207. package/server/lib/danbooru-tags.js +291 -291
  208. package/server/lib/doc-extract.js +156 -156
  209. package/server/lib/docx/build-from-source.js +260 -260
  210. package/server/lib/docx/build.js +490 -490
  211. package/server/lib/docx/dump-styles.js +287 -287
  212. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  213. package/server/lib/docx/merge-runs.js +129 -129
  214. package/server/lib/docx/numbering.js +218 -218
  215. package/server/lib/docx/order.js +152 -152
  216. package/server/lib/docx/rawzip.js +172 -172
  217. package/server/lib/docx/render.js +92 -92
  218. package/server/lib/docx/text-lint.js +225 -225
  219. package/server/lib/docx/tokens.js +335 -335
  220. package/server/lib/docx/units.js +29 -29
  221. package/server/lib/docx/xml.js +291 -291
  222. package/server/lib/docx-pages.js +187 -187
  223. package/server/lib/export-collect.js +289 -289
  224. package/server/lib/export-package.js +210 -210
  225. package/server/lib/html-srcset.js +145 -145
  226. package/server/lib/image-variant.js +460 -460
  227. package/server/lib/ingress/forward-openai-chat.js +305 -305
  228. package/server/lib/ingress/openai-chat.js +491 -491
  229. package/server/lib/ingress/session-notice.js +66 -66
  230. package/server/lib/ingress/session-routes.js +68 -68
  231. package/server/lib/ingress/slot-probe.js +130 -130
  232. package/server/lib/ingress/upstream-billing.js +52 -52
  233. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  234. package/server/lib/ingress/upstream-truncation.js +48 -48
  235. package/server/lib/issues-store.js +182 -182
  236. package/server/lib/kinds/deck.js +72 -72
  237. package/server/lib/kinds/docx.js +283 -283
  238. package/server/lib/kinds/file-kinds.js +113 -113
  239. package/server/lib/kinds/index.js +202 -202
  240. package/server/lib/kinds/site.js +161 -161
  241. package/server/lib/model-ingress.js +543 -543
  242. package/server/lib/moderation.js +255 -255
  243. package/server/lib/notice-store.js +103 -103
  244. package/server/lib/plugin-install.js +104 -104
  245. package/server/lib/plugin-validator.js +721 -721
  246. package/server/lib/publish-store.js +115 -115
  247. package/server/lib/quick-summary.js +52 -52
  248. package/server/lib/quota.js +270 -270
  249. package/server/lib/rate-window.js +29 -29
  250. package/server/lib/reference-assets.js +92 -92
  251. package/server/lib/region-shot.js +135 -135
  252. package/server/lib/safe-path.js +73 -73
  253. package/server/lib/sdk-session.js +33 -33
  254. package/server/lib/showcase-store.js +92 -92
  255. package/server/lib/site-publish.js +465 -465
  256. package/server/lib/sketch-layout.js +277 -0
  257. package/server/lib/ssrf-guard.js +432 -432
  258. package/server/lib/task-scan.js +158 -158
  259. package/server/lib/tavern-json.js +154 -154
  260. package/server/lib/video-variant.js +237 -237
  261. package/server/lib/workspace-path.js +33 -33
  262. package/server/ops/fix-sdk-musl.mjs +40 -40
  263. package/server/ops/install-macos-fonts.sh +114 -114
  264. package/server/ops/macos-fonts.conf +336 -336
  265. package/server/ops/sandbox-shim/bwrap +57 -57
  266. package/server/projects/assets-summary.js +119 -119
  267. package/server/projects/auto-name.js +53 -53
  268. package/server/projects/board-limits.js +12 -0
  269. package/server/projects/board-sanitize.js +252 -0
  270. package/server/projects/board-store.js +507 -640
  271. package/server/projects/move-entry.js +103 -103
  272. package/server/projects/store.js +270 -270
  273. package/server/projects/ui-config.js +60 -54
  274. package/server/projects/viewpoint-store.js +78 -0
  275. package/server/projects/workspace-templates.js +53 -53
  276. package/server/projects/workspace.js +1025 -1025
  277. package/server/runtime/local-config.js +177 -177
  278. package/server/runtime/profile.js +72 -72
  279. package/server/services/rembg-service.py +334 -334
  280. package/server/shared/README.md +25 -25
  281. package/server/shared/deck.js +54 -54
  282. package/server/shared/time.js +49 -49
  283. package/server/style-pipeline/README.md +53 -53
  284. package/server/ws/broker.js +39 -36
  285. package/server/ws/browse-channel.js +177 -177
  286. package/server/ws/index.js +386 -386
  287. package/web/README.md +89 -89
  288. package/web/dist/assets/BrowserWindow-P_vIdh-G.js +11 -0
  289. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-BpLv5082.js} +3 -3
  290. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-DSAU1-tD.js} +4 -4
  291. package/web/dist/assets/MermaidBlock-DR1GikCv.js +313 -0
  292. package/web/dist/assets/SiteWindow-92uBuRRR.js +18 -0
  293. package/web/dist/assets/abnfDiagram-VCTEODGH-uA8Esix4.js +1 -0
  294. package/web/dist/assets/arc-D_khH_xN.js +1 -0
  295. package/web/dist/assets/architectureDiagram-5GKGNRK7-DydJHqOt.js +36 -0
  296. package/web/dist/assets/blockDiagram-NRAW4CY4-B7wBbKiW.js +129 -0
  297. package/web/dist/assets/c4Diagram-UCG6FXSJ-6mrH0x7Y.js +38 -0
  298. package/web/dist/assets/channel-DGkccMO1.js +1 -0
  299. package/web/dist/assets/chunk-2Q5K7J3B-Cuikho5Q.js +1 -0
  300. package/web/dist/assets/chunk-5VM5RSS4-3-KZ5AUz.js +15 -0
  301. package/web/dist/assets/chunk-F27PBJKO-Bkwq2gGU.js +1 -0
  302. package/web/dist/assets/chunk-G27WJ6UU-BLyQzdGE.js +231 -0
  303. package/web/dist/assets/chunk-JWPE2WC7-B7HxxQds.js +1 -0
  304. package/web/dist/assets/chunk-LCL6LL3I-CtXrnUwF.js +206 -0
  305. package/web/dist/assets/chunk-POPQ4Y6H-dxddZlXt.js +1 -0
  306. package/web/dist/assets/chunk-SVP7TREG-DpkmrgHn.js +88 -0
  307. package/web/dist/assets/chunk-XXDRQBXY-BwZ7RbpQ.js +1 -0
  308. package/web/dist/assets/classDiagram-DTDB5LWJ-BGscw3OD.js +1 -0
  309. package/web/dist/assets/classDiagram-v2-JRS7N3AN-BGscw3OD.js +1 -0
  310. package/web/dist/assets/cose-bilkent-JH36ORCC-DGFOsMUt.js +1 -0
  311. package/web/dist/assets/cynefin-OW5HDTMX-DUwn3ehS.js +178 -0
  312. package/web/dist/assets/cynefinDiagram-5FMLGOSQ-DlLAOZ9x.js +62 -0
  313. package/web/dist/assets/cytoscape.esm-BjAbmLdb.js +331 -0
  314. package/web/dist/assets/dagre-3AP2YEHR-CyourLfz.js +4 -0
  315. package/web/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
  316. package/web/dist/assets/diagram-S7CK7UJ4-BTMbx2np.js +30 -0
  317. package/web/dist/assets/diagram-UQ7AKVKN-UK94_1-P.js +41 -0
  318. package/web/dist/assets/diagram-VSXAHHWV-C7zS-v9v.js +3 -0
  319. package/web/dist/assets/diagram-VX7I27RA-DKJRa6CK.js +24 -0
  320. package/web/dist/assets/diagram-Z3DM3KII-kUbAfGAp.js +24 -0
  321. package/web/dist/assets/ebnfDiagram-PWID7BFC-CVW16_vb.js +1 -0
  322. package/web/dist/assets/erDiagram-SSCWMZ5O-BRQMFQ5y.js +99 -0
  323. package/web/dist/assets/flowDiagram-A5DVABFB-S34LpLtK.js +168 -0
  324. package/web/dist/assets/ganttDiagram-EL5Y4UJY-BFiqk1-G.js +292 -0
  325. package/web/dist/assets/gitGraphDiagram-WWUBYQGX-LZsrdyyv.js +106 -0
  326. package/web/dist/assets/index-GAamh1p4.js +2152 -0
  327. package/web/dist/assets/infoDiagram-RXCK75RN-0DvN6KIp.js +2 -0
  328. package/web/dist/assets/init-Gi6I4Gst.js +1 -0
  329. package/web/dist/assets/ishikawaDiagram-5VMMS53U-CaY3A-Bx.js +70 -0
  330. package/web/dist/assets/journeyDiagram-EYS64GPL-BBeiaCrj.js +139 -0
  331. package/web/dist/assets/kanban-definition-3QL26DDD-ux42DRXQ.js +89 -0
  332. package/web/dist/assets/layout-CnXiF_NB.js +1 -0
  333. package/web/dist/assets/linear-BMW0u6G6.js +1 -0
  334. package/web/dist/assets/mindmap-definition-FBJOCRG2-hUxSpdKn.js +96 -0
  335. package/web/dist/assets/ordinal-Cboi1Yqb.js +1 -0
  336. package/web/dist/assets/pegDiagram-XKGWAZYB-aCaQPH0w.js +1 -0
  337. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-AcdOQ9Ij.js} +6 -6
  338. package/web/dist/assets/pieDiagram-E7YTZNPT-CmOEZ8zg.js +39 -0
  339. package/web/dist/assets/quadrantDiagram-AXDQQJYC-VW6fAhsV.js +7 -0
  340. package/web/dist/assets/railroadDiagram-O6MQD6OU-CKJ7Ba5K.js +1 -0
  341. package/web/dist/assets/requirementDiagram-EFPCY7ZU-pNGkq9Vw.js +84 -0
  342. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-KAfChbGV.js} +1 -1
  343. package/web/dist/assets/sankeyDiagram-P5KCCOFB-BIMmDTfZ.js +40 -0
  344. package/web/dist/assets/sequenceDiagram-WJ2MYXX4-BBW7wzKY.js +162 -0
  345. package/web/dist/assets/sizeCapture-X5ZJPWSS-DdkXmLYR.js +1 -0
  346. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-CDUlhUP7.js} +1 -1
  347. package/web/dist/assets/stateDiagram-HBIQ2CUA-W313Uz16.js +1 -0
  348. package/web/dist/assets/stateDiagram-v2-4QOOHH4V-DDhJBZzf.js +1 -0
  349. package/web/dist/assets/swimlanes-XN3QIQJK-C29TL26K.js +1 -0
  350. package/web/dist/assets/swimlanesDiagram-VK2B7HYN-BfN-Xdmu.js +8 -0
  351. package/web/dist/assets/timeline-definition-24CTP7MA-CF0JMObX.js +120 -0
  352. package/web/dist/assets/vennDiagram-4TSXK5OY-DrDFsaH3.js +34 -0
  353. package/web/dist/assets/wardleyDiagram-VM6X3IG4-CVrnRIS_.js +78 -0
  354. package/web/dist/assets/xychartDiagram-S5SC5T6Z-BvkN1zsT.js +7 -0
  355. package/web/dist/index.html +17 -17
  356. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
  357. package/web/dist/assets/SiteWindow-D6eS-2gM.js +0 -18
  358. package/web/dist/assets/index-Cbl3ATvp.js +0 -2125
@@ -1,289 +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
- }
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
+ }