@xiaobuyu/nodesign 0.0.1 → 0.0.2

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 (282) 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 +22 -22
  9. package/server/api/assets/notes.js +144 -144
  10. package/server/api/assets.js +896 -896
  11. package/server/api/board.js +57 -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 +464 -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 +215 -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 +336 -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 +1024 -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 +339 -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 +155 -155
  118. package/server/engine/mcp/tools/crystallize-skill.js +168 -168
  119. package/server/engine/mcp/tools/explain-style.js +237 -237
  120. package/server/engine/mcp/tools/export-handoff.js +133 -133
  121. package/server/engine/mcp/tools/expose-tweaks.js +149 -149
  122. package/server/engine/mcp/tools/generate-image.js +842 -842
  123. package/server/engine/mcp/tools/get-computed-styles.js +164 -164
  124. package/server/engine/mcp/tools/get-pending-changes.js +205 -205
  125. package/server/engine/mcp/tools/h3box-ssh.js +84 -84
  126. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
  127. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
  128. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
  129. package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
  130. package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
  131. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
  132. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
  133. package/server/engine/mcp/tools/highlight.js +68 -68
  134. package/server/engine/mcp/tools/list-pages.js +222 -222
  135. package/server/engine/mcp/tools/lookup-tags.js +59 -59
  136. package/server/engine/mcp/tools/navigate-to-page.js +60 -60
  137. package/server/engine/mcp/tools/organize-board.js +64 -64
  138. package/server/engine/mcp/tools/paint-still.js +434 -434
  139. package/server/engine/mcp/tools/pin-to-board.js +130 -130
  140. package/server/engine/mcp/tools/preview-deck.js +130 -130
  141. package/server/engine/mcp/tools/profile-scroll.js +305 -305
  142. package/server/engine/mcp/tools/publish-site.js +102 -102
  143. package/server/engine/mcp/tools/query-elements.js +195 -195
  144. package/server/engine/mcp/tools/read-board.js +103 -103
  145. package/server/engine/mcp/tools/read-document.js +93 -93
  146. package/server/engine/mcp/tools/read-page.js +264 -264
  147. package/server/engine/mcp/tools/read-tavern-json.js +143 -143
  148. package/server/engine/mcp/tools/record-decision.js +140 -140
  149. package/server/engine/mcp/tools/relate-on-board.js +110 -110
  150. package/server/engine/mcp/tools/report-issue.js +115 -115
  151. package/server/engine/mcp/tools/roll-film.js +266 -266
  152. package/server/engine/mcp/tools/screenshot-docx.js +138 -138
  153. package/server/engine/mcp/tools/screenshot-url.js +195 -195
  154. package/server/engine/mcp/tools/screenshot.js +583 -583
  155. package/server/engine/mcp/tools/tier-gate.js +96 -96
  156. package/server/engine/mcp/tools/trace-motion.js +215 -215
  157. package/server/engine/mcp/tools/web-search.js +548 -548
  158. package/server/engine/motion/inventory.js +349 -349
  159. package/server/engine/perception/session.js +235 -235
  160. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
  161. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
  162. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
  163. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
  164. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
  165. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
  166. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
  167. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
  168. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
  169. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
  170. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
  171. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
  172. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
  173. 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
  174. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
  175. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
  176. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
  177. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
  178. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
  179. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
  180. package/server/engine/runs/active-runs.js +698 -698
  181. package/server/engine/runs/live-turn.js +252 -252
  182. package/server/engine/runs/store.js +364 -364
  183. package/server/engine/runs/turn-relay.js +217 -217
  184. package/server/engine/runtime/workspace.js +164 -164
  185. package/server/lib/artifact-file-path.js +80 -80
  186. package/server/lib/artifact-target.js +284 -284
  187. package/server/lib/asset-refs.js +239 -239
  188. package/server/lib/async-queue.js +104 -104
  189. package/server/lib/auto-relations.js +130 -130
  190. package/server/lib/binding-types.js +60 -60
  191. package/server/lib/board-kind-sizes.js +62 -62
  192. package/server/lib/board-relations.js +133 -133
  193. package/server/lib/browse-proxy.js +180 -180
  194. package/server/lib/canvas-id.js +36 -36
  195. package/server/lib/cover.js +227 -227
  196. package/server/lib/danbooru-tags.js +291 -291
  197. package/server/lib/doc-extract.js +156 -156
  198. package/server/lib/docx/build-from-source.js +260 -260
  199. package/server/lib/docx/build.js +490 -490
  200. package/server/lib/docx/dump-styles.js +287 -287
  201. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
  202. package/server/lib/docx/merge-runs.js +129 -129
  203. package/server/lib/docx/numbering.js +218 -218
  204. package/server/lib/docx/order.js +152 -152
  205. package/server/lib/docx/rawzip.js +172 -172
  206. package/server/lib/docx/render.js +92 -92
  207. package/server/lib/docx/text-lint.js +225 -225
  208. package/server/lib/docx/tokens.js +335 -335
  209. package/server/lib/docx/units.js +29 -29
  210. package/server/lib/docx/xml.js +291 -291
  211. package/server/lib/docx-pages.js +187 -187
  212. package/server/lib/export-collect.js +289 -289
  213. package/server/lib/export-package.js +210 -210
  214. package/server/lib/html-srcset.js +145 -145
  215. package/server/lib/image-variant.js +460 -460
  216. package/server/lib/ingress/forward-openai-chat.js +305 -305
  217. package/server/lib/ingress/openai-chat.js +491 -491
  218. package/server/lib/ingress/session-notice.js +66 -66
  219. package/server/lib/ingress/session-routes.js +68 -68
  220. package/server/lib/ingress/slot-probe.js +130 -130
  221. package/server/lib/ingress/upstream-billing.js +52 -52
  222. package/server/lib/ingress/upstream-fail-streak.js +72 -72
  223. package/server/lib/ingress/upstream-truncation.js +48 -48
  224. package/server/lib/issues-store.js +182 -182
  225. package/server/lib/kinds/deck.js +72 -72
  226. package/server/lib/kinds/docx.js +283 -283
  227. package/server/lib/kinds/file-kinds.js +113 -113
  228. package/server/lib/kinds/index.js +202 -202
  229. package/server/lib/kinds/site.js +161 -161
  230. package/server/lib/model-ingress.js +543 -543
  231. package/server/lib/moderation.js +255 -255
  232. package/server/lib/notice-store.js +103 -103
  233. package/server/lib/plugin-install.js +104 -104
  234. package/server/lib/plugin-validator.js +721 -721
  235. package/server/lib/publish-store.js +115 -115
  236. package/server/lib/quick-summary.js +52 -52
  237. package/server/lib/quota.js +270 -270
  238. package/server/lib/rate-window.js +29 -29
  239. package/server/lib/reference-assets.js +92 -92
  240. package/server/lib/region-shot.js +135 -135
  241. package/server/lib/safe-path.js +73 -73
  242. package/server/lib/sdk-session.js +33 -33
  243. package/server/lib/showcase-store.js +92 -92
  244. package/server/lib/site-publish.js +465 -465
  245. package/server/lib/ssrf-guard.js +432 -432
  246. package/server/lib/task-scan.js +158 -158
  247. package/server/lib/tavern-json.js +154 -154
  248. package/server/lib/video-variant.js +237 -237
  249. package/server/lib/workspace-path.js +33 -33
  250. package/server/ops/fix-sdk-musl.mjs +40 -40
  251. package/server/ops/install-macos-fonts.sh +114 -114
  252. package/server/ops/macos-fonts.conf +336 -336
  253. package/server/ops/sandbox-shim/bwrap +57 -57
  254. package/server/projects/assets-summary.js +119 -119
  255. package/server/projects/auto-name.js +53 -53
  256. package/server/projects/board-store.js +640 -640
  257. package/server/projects/move-entry.js +103 -103
  258. package/server/projects/store.js +270 -270
  259. package/server/projects/ui-config.js +54 -54
  260. package/server/projects/workspace-templates.js +53 -53
  261. package/server/projects/workspace.js +1025 -1025
  262. package/server/runtime/local-config.js +177 -177
  263. package/server/runtime/profile.js +72 -72
  264. package/server/services/rembg-service.py +334 -334
  265. package/server/shared/README.md +25 -25
  266. package/server/shared/deck.js +54 -54
  267. package/server/shared/time.js +49 -49
  268. package/server/style-pipeline/README.md +53 -53
  269. package/server/ws/broker.js +36 -36
  270. package/server/ws/browse-channel.js +177 -177
  271. package/server/ws/index.js +386 -386
  272. package/web/README.md +89 -89
  273. package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
  274. package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
  275. package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
  276. package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
  277. package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
  278. package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
  279. package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
  280. package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
  281. package/web/dist/index.html +17 -17
  282. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
@@ -1,239 +1,239 @@
1
- /**
2
- * lib/asset-refs.js — 产物真正引用到了哪些素材(2026-08-17)
3
- *
4
- * 为什么要这个:旧的工程交付包把**整个** `shared/assets` 递归塞进 zip。那是
5
- * **项目级**共享目录,不是这份产物引用的东西 —— 生产上最大的一个项目 assets
6
- * 有 280MB,导出一份 deck 会把项目里每一张图(包括别的任务的)一起交出去。
7
- *
8
- * 所以改成扫引用:产物的 html/css/js 里写了哪些素材,就带哪些。
9
- *
10
- * ⚠️ **扫不到的一律不猜,但必须说出来。** 静态引用是确定的;算不出来的(动态拼
11
- * 路径、脚本里的字面量、`<base href>` 改了解析基准)**不往包里多塞**,而是记进
12
- * `unresolved` 由调用方写成清单放进包里。用户拍的板:宁可在纸面上说清缺什么,
13
- * 也不靠多塞蒙混过去。**「既不进包也不进清单」是这条规矩里最坏的状态** —— 那是
14
- * 假装没这回事,比多塞还糟。
15
- *
16
- * ── 2026-08-17 评审后重写,六条实测出来的病(每条都有单测钉着)──
17
- * 1. `<script src="x.js">` 随整块脚本被摘掉 → 真引用静默丢失
18
- * 2. HTML 注释里的旧引用照收 → 别的任务的图混进包
19
- * 3. `src="it's.png"` 用混合字符类解析 → 幻影路径 `it`
20
- * 4. `file:` 之类 scheme 漏过滤(原来是枚举表)→ 幻影 ref
21
- * 5. 脚本里的字面量素材(`['a.png','b.png']`)既不进包也不进清单
22
- * 6. 树外引用全域放行 → `<a href="../别的任务/x.html">` 把别人的产物收进来
23
- */
24
-
25
- import path from 'node:path';
26
- import fs from 'node:fs/promises';
27
-
28
- /** 会被扫的文本类型(二进制不扫) */
29
- const SCANNABLE = /\.(html?|css|jsx?|mjs|cjs)$/i;
30
-
31
- /**
32
- * 不是本地素材的引用。**用泛化判据不用枚举表** —— 枚举表漏一个 scheme(`file:`)
33
- * 就会产出幻影 ref。代价是 `x:30.png` 这种带冒号的文件名会被当 scheme 滤掉,
34
- * 但浏览器本来也按 scheme 解析它,滤掉才是对齐浏览器语义。
35
- */
36
- const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
37
-
38
- /**
39
- * 静态引用的抓取规则。
40
- *
41
- * ⚠️ **引号必须分支写**(`"([^"]*)"|'([^']*)'`),不能用 `["']([^"']+)["']` 那种
42
- * 混合字符类:后者遇到 `src="it's.png"` 会在撇号处截断,抓出幻影 `it`。
43
- * ⚠️ 属性名前的 `(?<![.\w-])` 挡的是 JS 里的 `img.src = '...'` —— 没有它,属性
44
- * 规则会把代码里的赋值当成 HTML 属性,同样抓出幻影。
45
- */
46
- /**
47
- * **只对标记语言成立**的规则。`src=` / `href=` 是 HTML 属性 —— 拿它去扫 .js
48
- * 会把 `const data = 'hello world'` 当成引用,捞出幻影路径 `站/hello world`
49
- * 塞进 missing,交付物里印出假警报(实测踩过)。
50
- */
51
- const MARKUP_PATTERNS = [
52
- /(?<![.\w-])(?:src|href|poster|data-src|data)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
53
- /\bsrcset\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
54
- ];
55
-
56
- /** 在 html / css / js 里语义都成立的规则 */
57
- const UNIVERSAL_PATTERNS = [
58
- /url\(\s*(?:"([^"]*)"|'([^']*)'|([^'")][^)]*?))\s*\)/gi,
59
- /@import\s+(?:"([^"]*)"|'([^']*)')/gi,
60
- /\bfrom\s+["']([^"']+\.(?:css|png|jpe?g|webp|gif|svg))["']/gi,
61
- // `new URL('x.png', import.meta.url)` 的第一个参数是**字面量**,静态可判,
62
- // 不该只当成"算不出来"记进清单。
63
- /new\s+URL\s*\(\s*(?:"([^"]*)"|'([^']*)')\s*,\s*import\.meta\.url/gi,
64
- ];
65
-
66
- /** 从一次匹配里取出那个真正被捕获的分支 */
67
- function capturedOf(m) {
68
- for (let i = 1; i < m.length; i++) if (m[i] != null) return m[i];
69
- return null;
70
- }
71
-
72
- /**
73
- * 摘掉脚本体但**保留开标签** —— `<script src="lib/chart.js"></script>` 里的 src
74
- * 是真引用,整块摘掉会静默丢失它(实测踩过)。
75
- */
76
- const SCRIPT_BLOCK = /(<script\b[^>]*>)[\s\S]*?<\/script>/gi;
77
- /** HTML 注释:里面的引用是历史残留,收进来就是把别的任务的旧图打进包 */
78
- const HTML_COMMENT = /<!--[\s\S]*?-->/g;
79
- /** CSS / JS 的块注释。**不摘 `//` 行注释** —— 会误杀 `https://` */
80
- const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
81
-
82
- /** 动态拼路径:抓到只报告不解析 */
83
- const DYNAMIC_HINTS = [
84
- { re: /["'][^"']*assets\/[^"']*["']\s*\+/gi, why: '字符串拼接' },
85
- { re: /`[^`]*assets\/[^`]*\$\{[^`]*`/gi, why: '模板字符串插值' },
86
- { re: /\+\s*["'][^"']*\.(?:png|jpe?g|webp|gif|svg|mp4)["']/gi, why: '字符串拼接' },
87
- ];
88
-
89
- /** 脚本里的素材字面量(画廊 / lightbox 的常见写法),只进清单不进包 */
90
- const SCRIPT_LITERAL = /["'`]([^"'`\n]{1,200}\.(?:png|jpe?g|webp|gif|svg|mp4|webm|pdf|woff2?))["'`]/gi;
91
-
92
- /** 去掉查询串和锚点(`x.webp?w=480` → `x.webp`)、反斜杠归一、URL 解码 */
93
- function cleanRef(raw) {
94
- const s = String(raw).trim().replace(/\\/g, '/').split('#')[0].split('?')[0];
95
- try { return decodeURIComponent(s); } catch { return s; }
96
- }
97
-
98
- /** srcset 拆成一组候选:`a.png 1x, b.png 2x` → ['a.png','b.png'] */
99
- function splitSrcset(value) {
100
- return value.split(',').map(part => part.trim().split(/\s+/)[0]).filter(Boolean);
101
- }
102
-
103
- /**
104
- * 扫一批产物文件,收出它们引用到的素材。
105
- *
106
- * @param {object} opts
107
- * @param {Array<{abs:string, rel:string}>} opts.files 要扫的文件(rel 相对 baseRoot)
108
- * @param {string} opts.baseRoot 解析基准(工作区根)。返回的路径都相对它
109
- * @param {string[]} [opts.allowPrefixes] 允许收的前缀白名单(树外只认这些,通常是
110
- * 产物自己的根 + `assets/`)。**留空 = 不设闸**(根层产物就是这种情况,它的树
111
- * 本来就是整个工作区)。被挡下的引用不静默丢,进 unresolved。
112
- * @returns {Promise<{refs:string[], unresolved:Array<{from,why,snippet}>}>}
113
- */
114
- export async function collectAssetRefs({ files, baseRoot, allowPrefixes = [] }) {
115
- const own = new Set(files.map(f => f.rel.replace(/\\/g, '/')));
116
- const refs = new Set();
117
- const unresolved = [];
118
- const candidates = []; // 脚本里的字面量,等调用方按"磁盘上真有"再提升
119
- const seenHint = new Set(); // 同一句话被多条规则命中时去重
120
-
121
- const pushHint = (from, why, snippet) => {
122
- const s = String(snippet).replace(/\s+/g, ' ').slice(0, 120);
123
- const key = `${from}|${s}`;
124
- if (seenHint.has(key)) return;
125
- seenHint.add(key);
126
- unresolved.push({ from, why, snippet: s });
127
- };
128
-
129
- const allowed = (rel) => {
130
- if (!allowPrefixes.length) return true;
131
- return allowPrefixes.some(p => (p === '' ? true : rel === p || rel.startsWith(p)));
132
- };
133
-
134
- for (const f of files) {
135
- if (!SCANNABLE.test(f.rel)) continue;
136
- let text;
137
- try { text = await fs.readFile(f.abs, 'utf8'); } catch { continue; }
138
-
139
- const fileDirAbs = path.dirname(f.abs);
140
- const isMarkup = /\.html?$/i.test(f.rel);
141
- const isScript = /\.(m?js|cjs|jsx)$/i.test(f.rel);
142
- // 静态引用只在「非注释、非脚本体」的部分找;动态线索照旧扫全文
143
- const staticText = text
144
- .replace(HTML_COMMENT, ' ')
145
- .replace(BLOCK_COMMENT, ' ')
146
- .replace(SCRIPT_BLOCK, '$1</script>');
147
-
148
- for (const pattern of (isMarkup ? [...MARKUP_PATTERNS, ...UNIVERSAL_PATTERNS] : UNIVERSAL_PATTERNS)) {
149
- pattern.lastIndex = 0;
150
- let m;
151
- while ((m = pattern.exec(staticText)) !== null) {
152
- const captured = capturedOf(m);
153
- if (captured == null) continue;
154
- const isSrcset = /srcset/i.test(pattern.source);
155
- for (const c of (isSrcset ? splitSrcset(captured) : [captured])) {
156
- if (!c || EXTERNAL.test(c.trim())) continue;
157
- const cleaned = cleanRef(c);
158
- if (!cleaned || cleaned.startsWith('/')) continue; // 根路径引用平台本来就禁
159
- const abs = path.resolve(fileDirAbs, cleaned);
160
- const rel = path.relative(baseRoot, abs).replace(/\\/g, '/');
161
- if (!rel || rel.startsWith('..')) continue; // 逃出工作区的不收
162
- if (own.has(rel)) continue; // 产物自己的页面不算素材
163
- if (!allowed(rel)) {
164
- pushHint(f.rel, '引用落在产物树外(只有 assets/ 和产物自己的目录会进包)', cleaned);
165
- continue;
166
- }
167
- refs.add(rel);
168
- }
169
- }
170
- }
171
-
172
- // `<base href>` 会改写整页相对路径的解析基准,扫描器按文件目录解析就会整体偏移
173
- if (/<base\b[^>]*\bhref/i.test(text)) {
174
- pushHint(f.rel, '页面有 <base href>,本页所有相对引用的解析基准可能跟打包时算的不一样', '<base href=…>');
175
- }
176
-
177
- for (const { re, why } of DYNAMIC_HINTS) {
178
- re.lastIndex = 0;
179
- let m;
180
- while ((m = re.exec(text)) !== null) pushHint(f.rel, why, m[0]);
181
- }
182
-
183
- // 脚本里的素材字面量(画廊 / lightbox 的常见写法)。
184
- // **完整字面量是静态可判的**,所以不直接判死:能落进工作区、又过得了树外闸的
185
- // 交给调用方按「磁盘上真有这个文件」提升成素材;剩下的才进清单。
186
- // 这不违反「扫不到的不多塞」—— 它扫得到,只是来源是脚本而不是标签。
187
- const scanLiterals = (body) => {
188
- SCRIPT_LITERAL.lastIndex = 0;
189
- let lit;
190
- while ((lit = SCRIPT_LITERAL.exec(body)) !== null) {
191
- const cleaned = cleanRef(lit[1]);
192
- const snippet = lit[0].slice(0, 120);
193
- if (!cleaned || EXTERNAL.test(cleaned) || cleaned.startsWith('/')) {
194
- pushHint(f.rel, '脚本里的字符串字面量(打包时不解析)', snippet); continue;
195
- }
196
- const rel = path.relative(baseRoot, path.resolve(fileDirAbs, cleaned)).replace(/\\/g, '/');
197
- if (!rel || rel.startsWith('..') || own.has(rel) || !allowed(rel)) {
198
- pushHint(f.rel, '脚本里的字符串字面量(打包时不解析)', snippet); continue;
199
- }
200
- candidates.push({ rel, from: f.rel, snippet });
201
- }
202
- };
203
- // ⚠️ 从**剥过注释**的文本里取脚本体:标签侧早就剥了注释,字面量侧漏剥的话,
204
- // 注释里的旧图会被提升进包 —— 同一条规矩两个半边,只修一半等于没修。
205
- const commentFree = text.replace(HTML_COMMENT, ' ');
206
- for (const sm of commentFree.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) scanLiterals(sm[1]);
207
- // ⚠️ 独立的 .js 文件**整个就是脚本体**,没有 <script> 标签可匹配。
208
- // 不加这条的话,同一份画廊代码写在 HTML 里能收、抽成 js 文件就双失踪。
209
- if (isScript) scanLiterals(text.replace(BLOCK_COMMENT, ' '));
210
- }
211
-
212
- return { refs: [...refs].sort(), candidates, unresolved };
213
- }
214
-
215
- /**
216
- * 把 unresolved 渲染成放进包里的清单。没有动态引用时返回 null(不放空文件 ——
217
- * 旧交付包里那个常年空着的 prompt.txt 就是这么来的)。
218
- */
219
- export function renderUnresolvedReport(unresolved) {
220
- if (!unresolved?.length) return null;
221
- const lines = [
222
- '# 没能解析的素材引用',
223
- '',
224
- '下面这些地方打包时算不出具体引用了哪个文件,**素材可能没进包**。',
225
- '如果打开后有图裂了,照这个清单去原工程里找对应文件补进来。',
226
- '',
227
- ];
228
- const byFile = new Map();
229
- for (const u of unresolved) {
230
- if (!byFile.has(u.from)) byFile.set(u.from, []);
231
- byFile.get(u.from).push(u);
232
- }
233
- for (const [file, items] of [...byFile.entries()].sort()) {
234
- lines.push(`## ${file}`, '');
235
- for (const it of items) lines.push(`- (${it.why})\`${it.snippet}\``);
236
- lines.push('');
237
- }
238
- return lines.join('\n');
239
- }
1
+ /**
2
+ * lib/asset-refs.js — 产物真正引用到了哪些素材(2026-08-17)
3
+ *
4
+ * 为什么要这个:旧的工程交付包把**整个** `shared/assets` 递归塞进 zip。那是
5
+ * **项目级**共享目录,不是这份产物引用的东西 —— 生产上最大的一个项目 assets
6
+ * 有 280MB,导出一份 deck 会把项目里每一张图(包括别的任务的)一起交出去。
7
+ *
8
+ * 所以改成扫引用:产物的 html/css/js 里写了哪些素材,就带哪些。
9
+ *
10
+ * ⚠️ **扫不到的一律不猜,但必须说出来。** 静态引用是确定的;算不出来的(动态拼
11
+ * 路径、脚本里的字面量、`<base href>` 改了解析基准)**不往包里多塞**,而是记进
12
+ * `unresolved` 由调用方写成清单放进包里。用户拍的板:宁可在纸面上说清缺什么,
13
+ * 也不靠多塞蒙混过去。**「既不进包也不进清单」是这条规矩里最坏的状态** —— 那是
14
+ * 假装没这回事,比多塞还糟。
15
+ *
16
+ * ── 2026-08-17 评审后重写,六条实测出来的病(每条都有单测钉着)──
17
+ * 1. `<script src="x.js">` 随整块脚本被摘掉 → 真引用静默丢失
18
+ * 2. HTML 注释里的旧引用照收 → 别的任务的图混进包
19
+ * 3. `src="it's.png"` 用混合字符类解析 → 幻影路径 `it`
20
+ * 4. `file:` 之类 scheme 漏过滤(原来是枚举表)→ 幻影 ref
21
+ * 5. 脚本里的字面量素材(`['a.png','b.png']`)既不进包也不进清单
22
+ * 6. 树外引用全域放行 → `<a href="../别的任务/x.html">` 把别人的产物收进来
23
+ */
24
+
25
+ import path from 'node:path';
26
+ import fs from 'node:fs/promises';
27
+
28
+ /** 会被扫的文本类型(二进制不扫) */
29
+ const SCANNABLE = /\.(html?|css|jsx?|mjs|cjs)$/i;
30
+
31
+ /**
32
+ * 不是本地素材的引用。**用泛化判据不用枚举表** —— 枚举表漏一个 scheme(`file:`)
33
+ * 就会产出幻影 ref。代价是 `x:30.png` 这种带冒号的文件名会被当 scheme 滤掉,
34
+ * 但浏览器本来也按 scheme 解析它,滤掉才是对齐浏览器语义。
35
+ */
36
+ const EXTERNAL = /^(?:[a-z][a-z0-9+.-]*:|\/\/|#)/i;
37
+
38
+ /**
39
+ * 静态引用的抓取规则。
40
+ *
41
+ * ⚠️ **引号必须分支写**(`"([^"]*)"|'([^']*)'`),不能用 `["']([^"']+)["']` 那种
42
+ * 混合字符类:后者遇到 `src="it's.png"` 会在撇号处截断,抓出幻影 `it`。
43
+ * ⚠️ 属性名前的 `(?<![.\w-])` 挡的是 JS 里的 `img.src = '...'` —— 没有它,属性
44
+ * 规则会把代码里的赋值当成 HTML 属性,同样抓出幻影。
45
+ */
46
+ /**
47
+ * **只对标记语言成立**的规则。`src=` / `href=` 是 HTML 属性 —— 拿它去扫 .js
48
+ * 会把 `const data = 'hello world'` 当成引用,捞出幻影路径 `站/hello world`
49
+ * 塞进 missing,交付物里印出假警报(实测踩过)。
50
+ */
51
+ const MARKUP_PATTERNS = [
52
+ /(?<![.\w-])(?:src|href|poster|data-src|data)\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
53
+ /\bsrcset\s*=\s*(?:"([^"]*)"|'([^']*)')/gi,
54
+ ];
55
+
56
+ /** 在 html / css / js 里语义都成立的规则 */
57
+ const UNIVERSAL_PATTERNS = [
58
+ /url\(\s*(?:"([^"]*)"|'([^']*)'|([^'")][^)]*?))\s*\)/gi,
59
+ /@import\s+(?:"([^"]*)"|'([^']*)')/gi,
60
+ /\bfrom\s+["']([^"']+\.(?:css|png|jpe?g|webp|gif|svg))["']/gi,
61
+ // `new URL('x.png', import.meta.url)` 的第一个参数是**字面量**,静态可判,
62
+ // 不该只当成"算不出来"记进清单。
63
+ /new\s+URL\s*\(\s*(?:"([^"]*)"|'([^']*)')\s*,\s*import\.meta\.url/gi,
64
+ ];
65
+
66
+ /** 从一次匹配里取出那个真正被捕获的分支 */
67
+ function capturedOf(m) {
68
+ for (let i = 1; i < m.length; i++) if (m[i] != null) return m[i];
69
+ return null;
70
+ }
71
+
72
+ /**
73
+ * 摘掉脚本体但**保留开标签** —— `<script src="lib/chart.js"></script>` 里的 src
74
+ * 是真引用,整块摘掉会静默丢失它(实测踩过)。
75
+ */
76
+ const SCRIPT_BLOCK = /(<script\b[^>]*>)[\s\S]*?<\/script>/gi;
77
+ /** HTML 注释:里面的引用是历史残留,收进来就是把别的任务的旧图打进包 */
78
+ const HTML_COMMENT = /<!--[\s\S]*?-->/g;
79
+ /** CSS / JS 的块注释。**不摘 `//` 行注释** —— 会误杀 `https://` */
80
+ const BLOCK_COMMENT = /\/\*[\s\S]*?\*\//g;
81
+
82
+ /** 动态拼路径:抓到只报告不解析 */
83
+ const DYNAMIC_HINTS = [
84
+ { re: /["'][^"']*assets\/[^"']*["']\s*\+/gi, why: '字符串拼接' },
85
+ { re: /`[^`]*assets\/[^`]*\$\{[^`]*`/gi, why: '模板字符串插值' },
86
+ { re: /\+\s*["'][^"']*\.(?:png|jpe?g|webp|gif|svg|mp4)["']/gi, why: '字符串拼接' },
87
+ ];
88
+
89
+ /** 脚本里的素材字面量(画廊 / lightbox 的常见写法),只进清单不进包 */
90
+ const SCRIPT_LITERAL = /["'`]([^"'`\n]{1,200}\.(?:png|jpe?g|webp|gif|svg|mp4|webm|pdf|woff2?))["'`]/gi;
91
+
92
+ /** 去掉查询串和锚点(`x.webp?w=480` → `x.webp`)、反斜杠归一、URL 解码 */
93
+ function cleanRef(raw) {
94
+ const s = String(raw).trim().replace(/\\/g, '/').split('#')[0].split('?')[0];
95
+ try { return decodeURIComponent(s); } catch { return s; }
96
+ }
97
+
98
+ /** srcset 拆成一组候选:`a.png 1x, b.png 2x` → ['a.png','b.png'] */
99
+ function splitSrcset(value) {
100
+ return value.split(',').map(part => part.trim().split(/\s+/)[0]).filter(Boolean);
101
+ }
102
+
103
+ /**
104
+ * 扫一批产物文件,收出它们引用到的素材。
105
+ *
106
+ * @param {object} opts
107
+ * @param {Array<{abs:string, rel:string}>} opts.files 要扫的文件(rel 相对 baseRoot)
108
+ * @param {string} opts.baseRoot 解析基准(工作区根)。返回的路径都相对它
109
+ * @param {string[]} [opts.allowPrefixes] 允许收的前缀白名单(树外只认这些,通常是
110
+ * 产物自己的根 + `assets/`)。**留空 = 不设闸**(根层产物就是这种情况,它的树
111
+ * 本来就是整个工作区)。被挡下的引用不静默丢,进 unresolved。
112
+ * @returns {Promise<{refs:string[], unresolved:Array<{from,why,snippet}>}>}
113
+ */
114
+ export async function collectAssetRefs({ files, baseRoot, allowPrefixes = [] }) {
115
+ const own = new Set(files.map(f => f.rel.replace(/\\/g, '/')));
116
+ const refs = new Set();
117
+ const unresolved = [];
118
+ const candidates = []; // 脚本里的字面量,等调用方按"磁盘上真有"再提升
119
+ const seenHint = new Set(); // 同一句话被多条规则命中时去重
120
+
121
+ const pushHint = (from, why, snippet) => {
122
+ const s = String(snippet).replace(/\s+/g, ' ').slice(0, 120);
123
+ const key = `${from}|${s}`;
124
+ if (seenHint.has(key)) return;
125
+ seenHint.add(key);
126
+ unresolved.push({ from, why, snippet: s });
127
+ };
128
+
129
+ const allowed = (rel) => {
130
+ if (!allowPrefixes.length) return true;
131
+ return allowPrefixes.some(p => (p === '' ? true : rel === p || rel.startsWith(p)));
132
+ };
133
+
134
+ for (const f of files) {
135
+ if (!SCANNABLE.test(f.rel)) continue;
136
+ let text;
137
+ try { text = await fs.readFile(f.abs, 'utf8'); } catch { continue; }
138
+
139
+ const fileDirAbs = path.dirname(f.abs);
140
+ const isMarkup = /\.html?$/i.test(f.rel);
141
+ const isScript = /\.(m?js|cjs|jsx)$/i.test(f.rel);
142
+ // 静态引用只在「非注释、非脚本体」的部分找;动态线索照旧扫全文
143
+ const staticText = text
144
+ .replace(HTML_COMMENT, ' ')
145
+ .replace(BLOCK_COMMENT, ' ')
146
+ .replace(SCRIPT_BLOCK, '$1</script>');
147
+
148
+ for (const pattern of (isMarkup ? [...MARKUP_PATTERNS, ...UNIVERSAL_PATTERNS] : UNIVERSAL_PATTERNS)) {
149
+ pattern.lastIndex = 0;
150
+ let m;
151
+ while ((m = pattern.exec(staticText)) !== null) {
152
+ const captured = capturedOf(m);
153
+ if (captured == null) continue;
154
+ const isSrcset = /srcset/i.test(pattern.source);
155
+ for (const c of (isSrcset ? splitSrcset(captured) : [captured])) {
156
+ if (!c || EXTERNAL.test(c.trim())) continue;
157
+ const cleaned = cleanRef(c);
158
+ if (!cleaned || cleaned.startsWith('/')) continue; // 根路径引用平台本来就禁
159
+ const abs = path.resolve(fileDirAbs, cleaned);
160
+ const rel = path.relative(baseRoot, abs).replace(/\\/g, '/');
161
+ if (!rel || rel.startsWith('..')) continue; // 逃出工作区的不收
162
+ if (own.has(rel)) continue; // 产物自己的页面不算素材
163
+ if (!allowed(rel)) {
164
+ pushHint(f.rel, '引用落在产物树外(只有 assets/ 和产物自己的目录会进包)', cleaned);
165
+ continue;
166
+ }
167
+ refs.add(rel);
168
+ }
169
+ }
170
+ }
171
+
172
+ // `<base href>` 会改写整页相对路径的解析基准,扫描器按文件目录解析就会整体偏移
173
+ if (/<base\b[^>]*\bhref/i.test(text)) {
174
+ pushHint(f.rel, '页面有 <base href>,本页所有相对引用的解析基准可能跟打包时算的不一样', '<base href=…>');
175
+ }
176
+
177
+ for (const { re, why } of DYNAMIC_HINTS) {
178
+ re.lastIndex = 0;
179
+ let m;
180
+ while ((m = re.exec(text)) !== null) pushHint(f.rel, why, m[0]);
181
+ }
182
+
183
+ // 脚本里的素材字面量(画廊 / lightbox 的常见写法)。
184
+ // **完整字面量是静态可判的**,所以不直接判死:能落进工作区、又过得了树外闸的
185
+ // 交给调用方按「磁盘上真有这个文件」提升成素材;剩下的才进清单。
186
+ // 这不违反「扫不到的不多塞」—— 它扫得到,只是来源是脚本而不是标签。
187
+ const scanLiterals = (body) => {
188
+ SCRIPT_LITERAL.lastIndex = 0;
189
+ let lit;
190
+ while ((lit = SCRIPT_LITERAL.exec(body)) !== null) {
191
+ const cleaned = cleanRef(lit[1]);
192
+ const snippet = lit[0].slice(0, 120);
193
+ if (!cleaned || EXTERNAL.test(cleaned) || cleaned.startsWith('/')) {
194
+ pushHint(f.rel, '脚本里的字符串字面量(打包时不解析)', snippet); continue;
195
+ }
196
+ const rel = path.relative(baseRoot, path.resolve(fileDirAbs, cleaned)).replace(/\\/g, '/');
197
+ if (!rel || rel.startsWith('..') || own.has(rel) || !allowed(rel)) {
198
+ pushHint(f.rel, '脚本里的字符串字面量(打包时不解析)', snippet); continue;
199
+ }
200
+ candidates.push({ rel, from: f.rel, snippet });
201
+ }
202
+ };
203
+ // ⚠️ 从**剥过注释**的文本里取脚本体:标签侧早就剥了注释,字面量侧漏剥的话,
204
+ // 注释里的旧图会被提升进包 —— 同一条规矩两个半边,只修一半等于没修。
205
+ const commentFree = text.replace(HTML_COMMENT, ' ');
206
+ for (const sm of commentFree.matchAll(/<script\b[^>]*>([\s\S]*?)<\/script>/gi)) scanLiterals(sm[1]);
207
+ // ⚠️ 独立的 .js 文件**整个就是脚本体**,没有 <script> 标签可匹配。
208
+ // 不加这条的话,同一份画廊代码写在 HTML 里能收、抽成 js 文件就双失踪。
209
+ if (isScript) scanLiterals(text.replace(BLOCK_COMMENT, ' '));
210
+ }
211
+
212
+ return { refs: [...refs].sort(), candidates, unresolved };
213
+ }
214
+
215
+ /**
216
+ * 把 unresolved 渲染成放进包里的清单。没有动态引用时返回 null(不放空文件 ——
217
+ * 旧交付包里那个常年空着的 prompt.txt 就是这么来的)。
218
+ */
219
+ export function renderUnresolvedReport(unresolved) {
220
+ if (!unresolved?.length) return null;
221
+ const lines = [
222
+ '# 没能解析的素材引用',
223
+ '',
224
+ '下面这些地方打包时算不出具体引用了哪个文件,**素材可能没进包**。',
225
+ '如果打开后有图裂了,照这个清单去原工程里找对应文件补进来。',
226
+ '',
227
+ ];
228
+ const byFile = new Map();
229
+ for (const u of unresolved) {
230
+ if (!byFile.has(u.from)) byFile.set(u.from, []);
231
+ byFile.get(u.from).push(u);
232
+ }
233
+ for (const [file, items] of [...byFile.entries()].sort()) {
234
+ lines.push(`## ${file}`, '');
235
+ for (const it of items) lines.push(`- (${it.why})\`${it.snippet}\``);
236
+ lines.push('');
237
+ }
238
+ return lines.join('\n');
239
+ }