@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,133 +1,133 @@
1
- /**
2
- * server/lib/board-relations.js — 关系线的 agent 读侧(2026-08-14,切片③)
3
- *
4
- * 在此之前 bindings 是**只写不读**的:agent 有 relate_on_board 能画线、用户
5
- * 在画布上看得见,但没有任何一层把「用户画了什么关系」喂回 agent —— 线是
6
- * 装饰。这份文件是读侧的唯一实现,两个消费口都从这儿取:
7
- *
8
- * 1. UserPromptSubmit 摘要(relationsDigest):每轮把全图压成 ≤N 行,
9
- * 用户画的排前面 —— 他专门画的线就是他想让你知道的事。
10
- * 2. PreToolUse 邻域(fileNeighborhood):agent 摸某个文件时,只注连着
11
- * 这个文件的边(摘要被截断时的精确补充)。
12
- *
13
- * 端点是画布 id(= kind 前缀 + 工作区相对路径,或画布原生 id `text:`/
14
- * `scribble:`)。手写字端点渲染**内容本身**——这是「标注作用全局」的关键一步:
15
- * 用户在画布上写的字经由一条线变成 agent 眼里的上下文。涂鸦端点诚实占位
16
- * (笔画内容看不见,要看得截图),不假装理解。
17
- */
18
-
19
- import { readBoard } from '../projects/board-store.js';
20
- import { BINDING_TYPES } from './binding-types.js';
21
- import { KINDS } from './kinds/index.js';
22
-
23
- // ⚠️ **从注册表派生,别手写**(2026-08-18 修)。这行原来写死 `['deck:','site:']`,
24
- // 于是 docx 上线之后 `docx:` 前缀的卡不在表里 —— **它们身上的关系线 agent 一条也
25
- // 看不见**(endpointMatchesRel 永远不匹配),而且错得很安静。
26
- // 这是「加形态时最容易漏的写死处」家族的第三处(前两处是前端 cardIdOf 和
27
- // BoardCanvas 的 type:'deck',08-17 已修)。
28
- const KIND_PREFIXES = Object.keys(KINDS).map(id => `${id}:`);
29
-
30
- // 目录型形态的前缀(kind 条目声明了 directory 的那些)。2026-08-18 补修:
31
- // 下面 endpointMatchesRel 的收敛循环原来写死 `['site:']` —— 上面 KIND_PREFIXES
32
- // 修掉写死表的同一天,这半个同款病漏掉了:word 文件夹卡(docx:报告)身上的边,
33
- // agent 摸成员文件(报告/终稿v2.docx)时注不进 fileNeighborhood。
34
- const DIR_KIND_PREFIXES = Object.entries(KINDS)
35
- .filter(([, def]) => def.directory).map(([id]) => `${id}:`);
36
-
37
- /** 端点 id → 给 agent 看的一小段描述 */
38
- export function describeEndpoint(end, board) {
39
- const obj = board?.objects?.[end];
40
- if (obj?.kind === 'text') {
41
- const t = String(obj.data?.t || '').replace(/\s+/g, ' ').trim().slice(0, 80);
42
- return `手写字「${t}」`;
43
- }
44
- if (obj?.kind === 'scribble') return '一笔涂鸦(笔画内容要看得截图画布)';
45
- for (const p of KIND_PREFIXES) {
46
- if (end.startsWith(p)) {
47
- const rel = end.slice(p.length);
48
- const kind = p.slice(0, -1);
49
- return rel ? `${rel}(${kind})` : `工作区根上的${kind}`;
50
- }
51
- }
52
- return end; // 裸文件路径或文件夹路径
53
- }
54
-
55
- /** 一条边 → 一行中文。方向用箭头表达,无向用平线。 */
56
- export function bindingLine(b, board) {
57
- const t = BINDING_TYPES[b.type];
58
- const word = b.label || t?.label || b.type;
59
- const joint = t?.directed ? `─${word}→` : `─${word}─`;
60
- const who = b.by === 'user' ? '〔用户画的〕' : b.by === 'agent' ? '〔你画的〕' : '';
61
- return `${describeEndpoint(b.from, board)} ${joint} ${describeEndpoint(b.to, board)}${who}`;
62
- }
63
-
64
- /**
65
- * 全图摘要。用户画的排前面(他专门画的 = 他想让你知道的),截断报余量。
66
- * 没有任何线 → null(沉默,不占 prompt)。
67
- */
68
- export async function relationsDigest(pid, { limit = 12 } = {}) {
69
- if (!pid) return null;
70
- const board = await readBoard(pid);
71
- const all = Object.values(board.bindings || {});
72
- if (!all.length) return null;
73
- // 手画的逐条列;自动 ref(内容引用对账出来的底仓)**收拢成每源一行**——
74
- // 一个站引三十张图,铺开就把摘要刷没了。优先级 user > agent > auto。
75
- const manual = [
76
- ...all.filter(b => b.by === 'user'),
77
- ...all.filter(b => b.by === 'agent'),
78
- ...all.filter(b => b.by !== 'user' && b.by !== 'agent' && !(b.by === 'auto' && b.type === 'ref')),
79
- ];
80
- const autoRefs = all.filter(b => b.by === 'auto' && b.type === 'ref');
81
- const lines = manual.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
82
- const more = manual.length - lines.length;
83
- if (more > 0) lines.push(` …还有 ${more} 条(摸到相关文件时会单独提示)`);
84
- if (autoRefs.length) {
85
- const bySrc = new Map();
86
- for (const b of autoRefs) {
87
- if (!bySrc.has(b.from)) bySrc.set(b.from, []);
88
- bySrc.get(b.from).push(b.to);
89
- }
90
- for (const [from, tos] of [...bySrc].slice(0, 4)) {
91
- const sample = tos.slice(0, 3).join('、');
92
- lines.push(` ${describeEndpoint(from, board)} ─取材─ ${tos.length} 件素材(自动对账:${sample}${tos.length > 3 ? ' 等' : ''})`);
93
- }
94
- if (bySrc.size > 4) lines.push(` …另有 ${bySrc.size - 4} 件产物的取材清单省略`);
95
- }
96
- return lines.join('\n');
97
- }
98
-
99
- /** 某个端点 id 是否指向这个工作区相对路径(裸路径或任一 kind 前缀形态)。
100
- * 导出仅供测试钉规则(fileNeighborhood 是唯一业务调用方)。 */
101
- export function endpointMatchesRel(end, rel) {
102
- if (end === rel) return true;
103
- for (const p of KIND_PREFIXES) if (end === p + rel) return true;
104
- // 目录型产物的收敛(2026-08-14 根站病族普查补上的):站点/word 文件夹的边
105
- // 挂在**根卡**上,agent 摸的是里面的文件 —— 原来只做精确匹配,等于站点页的
106
- // 一跳邻域从来注入不出来。按"住在这个根里"匹配;根站(root=空串)收根层
107
- // 散文件(.md 除外)—— 与前端 resolveObjectId 同一条规则,两边别岔开。
108
- for (const p of DIR_KIND_PREFIXES) {
109
- if (!end.startsWith(p)) continue;
110
- const root = end.slice(p.length);
111
- if (root === '') {
112
- if (!rel.includes('/') && !/\.md$/i.test(rel)) return true;
113
- continue;
114
- }
115
- if (rel === root || rel.startsWith(`${root}/`)) return true;
116
- }
117
- return false;
118
- }
119
-
120
- /**
121
- * 一跳邻域:连着某个文件/文件夹的全部边。没有 → null。
122
- * PreToolUse 注入用 —— agent 正要摸的东西,它身上的线最相关。
123
- */
124
- export async function fileNeighborhood(pid, rel, { limit = 6 } = {}) {
125
- if (!pid || !rel) return null;
126
- const board = await readBoard(pid);
127
- const hits = Object.values(board.bindings || {})
128
- .filter(b => endpointMatchesRel(b.from, rel) || endpointMatchesRel(b.to, rel));
129
- if (!hits.length) return null;
130
- const lines = hits.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
131
- if (hits.length > limit) lines.push(` …还有 ${hits.length - limit} 条`);
132
- return lines.join('\n');
133
- }
1
+ /**
2
+ * server/lib/board-relations.js — 关系线的 agent 读侧(2026-08-14,切片③)
3
+ *
4
+ * 在此之前 bindings 是**只写不读**的:agent 有 relate_on_board 能画线、用户
5
+ * 在画布上看得见,但没有任何一层把「用户画了什么关系」喂回 agent —— 线是
6
+ * 装饰。这份文件是读侧的唯一实现,两个消费口都从这儿取:
7
+ *
8
+ * 1. UserPromptSubmit 摘要(relationsDigest):每轮把全图压成 ≤N 行,
9
+ * 用户画的排前面 —— 他专门画的线就是他想让你知道的事。
10
+ * 2. PreToolUse 邻域(fileNeighborhood):agent 摸某个文件时,只注连着
11
+ * 这个文件的边(摘要被截断时的精确补充)。
12
+ *
13
+ * 端点是画布 id(= kind 前缀 + 工作区相对路径,或画布原生 id `text:`/
14
+ * `scribble:`)。手写字端点渲染**内容本身**——这是「标注作用全局」的关键一步:
15
+ * 用户在画布上写的字经由一条线变成 agent 眼里的上下文。涂鸦端点诚实占位
16
+ * (笔画内容看不见,要看得截图),不假装理解。
17
+ */
18
+
19
+ import { readBoard } from '../projects/board-store.js';
20
+ import { BINDING_TYPES } from './binding-types.js';
21
+ import { KINDS } from './kinds/index.js';
22
+
23
+ // ⚠️ **从注册表派生,别手写**(2026-08-18 修)。这行原来写死 `['deck:','site:']`,
24
+ // 于是 docx 上线之后 `docx:` 前缀的卡不在表里 —— **它们身上的关系线 agent 一条也
25
+ // 看不见**(endpointMatchesRel 永远不匹配),而且错得很安静。
26
+ // 这是「加形态时最容易漏的写死处」家族的第三处(前两处是前端 cardIdOf 和
27
+ // BoardCanvas 的 type:'deck',08-17 已修)。
28
+ const KIND_PREFIXES = Object.keys(KINDS).map(id => `${id}:`);
29
+
30
+ // 目录型形态的前缀(kind 条目声明了 directory 的那些)。2026-08-18 补修:
31
+ // 下面 endpointMatchesRel 的收敛循环原来写死 `['site:']` —— 上面 KIND_PREFIXES
32
+ // 修掉写死表的同一天,这半个同款病漏掉了:word 文件夹卡(docx:报告)身上的边,
33
+ // agent 摸成员文件(报告/终稿v2.docx)时注不进 fileNeighborhood。
34
+ const DIR_KIND_PREFIXES = Object.entries(KINDS)
35
+ .filter(([, def]) => def.directory).map(([id]) => `${id}:`);
36
+
37
+ /** 端点 id → 给 agent 看的一小段描述 */
38
+ export function describeEndpoint(end, board) {
39
+ const obj = board?.objects?.[end];
40
+ if (obj?.kind === 'text') {
41
+ const t = String(obj.data?.t || '').replace(/\s+/g, ' ').trim().slice(0, 48);
42
+ return `手写字「${t}」`;
43
+ }
44
+ if (obj?.kind === 'scribble') return '一笔涂鸦(笔画内容要看得截图画布)';
45
+ for (const p of KIND_PREFIXES) {
46
+ if (end.startsWith(p)) {
47
+ const rel = end.slice(p.length);
48
+ const kind = p.slice(0, -1);
49
+ return rel ? `${rel}(${kind})` : `工作区根上的${kind}`;
50
+ }
51
+ }
52
+ return end; // 裸文件路径或文件夹路径
53
+ }
54
+
55
+ /** 一条边 → 一行中文。方向用箭头表达,无向用平线。 */
56
+ export function bindingLine(b, board) {
57
+ const t = BINDING_TYPES[b.type];
58
+ const word = b.label || t?.label || b.type;
59
+ const joint = t?.directed ? `─${word}→` : `─${word}─`;
60
+ const who = b.by === 'user' ? '〔用户画的〕' : b.by === 'agent' ? '〔你画的〕' : '';
61
+ return `${describeEndpoint(b.from, board)} ${joint} ${describeEndpoint(b.to, board)}${who}`;
62
+ }
63
+
64
+ /**
65
+ * 全图摘要。用户画的排前面(他专门画的 = 他想让你知道的),截断报余量。
66
+ * 没有任何线 → null(沉默,不占 prompt)。
67
+ */
68
+ export async function relationsDigest(pid, { limit = 12 } = {}) {
69
+ if (!pid) return null;
70
+ const board = await readBoard(pid);
71
+ const all = Object.values(board.bindings || {});
72
+ if (!all.length) return null;
73
+ // 手画的逐条列;自动 ref(内容引用对账出来的底仓)**收拢成每源一行**——
74
+ // 一个站引三十张图,铺开就把摘要刷没了。优先级 user > agent > auto。
75
+ const manual = [
76
+ ...all.filter(b => b.by === 'user'),
77
+ ...all.filter(b => b.by === 'agent'),
78
+ ...all.filter(b => b.by !== 'user' && b.by !== 'agent' && !(b.by === 'auto' && b.type === 'ref')),
79
+ ];
80
+ const autoRefs = all.filter(b => b.by === 'auto' && b.type === 'ref');
81
+ const lines = manual.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
82
+ const more = manual.length - lines.length;
83
+ if (more > 0) lines.push(` …还有 ${more} 条(摸到相关文件时会单独提示)`);
84
+ if (autoRefs.length) {
85
+ const bySrc = new Map();
86
+ for (const b of autoRefs) {
87
+ if (!bySrc.has(b.from)) bySrc.set(b.from, []);
88
+ bySrc.get(b.from).push(b.to);
89
+ }
90
+ for (const [from, tos] of [...bySrc].slice(0, 4)) {
91
+ const sample = tos.slice(0, 3).join('、');
92
+ lines.push(` ${describeEndpoint(from, board)} ─取材─ ${tos.length} 件素材(自动对账:${sample}${tos.length > 3 ? ' 等' : ''})`);
93
+ }
94
+ if (bySrc.size > 4) lines.push(` …另有 ${bySrc.size - 4} 件产物的取材清单省略`);
95
+ }
96
+ return lines.join('\n');
97
+ }
98
+
99
+ /** 某个端点 id 是否指向这个工作区相对路径(裸路径或任一 kind 前缀形态)。
100
+ * 导出仅供测试钉规则(fileNeighborhood 是唯一业务调用方)。 */
101
+ export function endpointMatchesRel(end, rel) {
102
+ if (end === rel) return true;
103
+ for (const p of KIND_PREFIXES) if (end === p + rel) return true;
104
+ // 目录型产物的收敛(2026-08-14 根站病族普查补上的):站点/word 文件夹的边
105
+ // 挂在**根卡**上,agent 摸的是里面的文件 —— 原来只做精确匹配,等于站点页的
106
+ // 一跳邻域从来注入不出来。按"住在这个根里"匹配;根站(root=空串)收根层
107
+ // 散文件(.md 除外)—— 与前端 resolveObjectId 同一条规则,两边别岔开。
108
+ for (const p of DIR_KIND_PREFIXES) {
109
+ if (!end.startsWith(p)) continue;
110
+ const root = end.slice(p.length);
111
+ if (root === '') {
112
+ if (!rel.includes('/') && !/\.md$/i.test(rel)) return true;
113
+ continue;
114
+ }
115
+ if (rel === root || rel.startsWith(`${root}/`)) return true;
116
+ }
117
+ return false;
118
+ }
119
+
120
+ /**
121
+ * 一跳邻域:连着某个文件/文件夹的全部边。没有 → null。
122
+ * PreToolUse 注入用 —— agent 正要摸的东西,它身上的线最相关。
123
+ */
124
+ export async function fileNeighborhood(pid, rel, { limit = 6 } = {}) {
125
+ if (!pid || !rel) return null;
126
+ const board = await readBoard(pid);
127
+ const hits = Object.values(board.bindings || {})
128
+ .filter(b => endpointMatchesRel(b.from, rel) || endpointMatchesRel(b.to, rel));
129
+ if (!hits.length) return null;
130
+ const lines = hits.slice(0, limit).map(b => ` ${bindingLine(b, board)}`);
131
+ if (hits.length > limit) lines.push(` …还有 ${hits.length - limit} 条`);
132
+ return lines.join('\n');
133
+ }
@@ -1,180 +1,180 @@
1
- /**
2
- * server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
3
- *
4
- * ## 为什么闸从 CDP 挪到代理
5
- *
6
- * 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
7
- * 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
8
- *
9
- * - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
10
- * - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
11
- * - **`navigator.sendBeacon`**
12
- * - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
13
- * 能发 GET 和 POST,还能循环开)
14
- *
15
- * 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
16
- * 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
17
- * HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
18
- * 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
19
- *
20
- * ## 附带白拿的两件
21
- *
22
- * 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
23
- * (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
24
- * 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
25
- * 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
26
- * CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
27
- *
28
- * CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
29
- *
30
- * ## 感知通道不挂代理
31
- *
32
- * 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
33
- * 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
34
- */
35
-
36
- import http from 'node:http';
37
- import net from 'node:net';
38
- import dns from 'node:dns/promises';
39
- import { blockReason } from './ssrf-guard.js';
40
-
41
- /** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
42
- const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
43
- const RESOLVE_TIMEOUT_MS = 4000;
44
- const CONNECT_TIMEOUT_MS = 15_000;
45
-
46
- let server = null;
47
- let port = 0;
48
- const blocked = [];
49
-
50
- function note(target, reason) {
51
- blocked.push({ target, reason, at: Date.now() });
52
- if (blocked.length > 200) blocked.shift();
53
- }
54
-
55
- /**
56
- * 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
57
- * @returns {Promise<{ip: string, family: number} | {deny: string}>}
58
- */
59
- async function resolveAllowed(host, portNum) {
60
- if (!ALLOWED_PORTS.has(portNum)) {
61
- return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
62
- }
63
- const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
64
- if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
65
- return { deny: `${bare} resolves inside the local network` };
66
- }
67
- // 字面量 IP 不劳烦 DNS
68
- if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
69
- const why = blockReason(bare);
70
- return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
71
- }
72
- let addrs;
73
- try {
74
- addrs = await Promise.race([
75
- dns.lookup(bare, { all: true, verbatim: true }),
76
- new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
77
- ]);
78
- } catch (err) {
79
- // ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
80
- // 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
81
- // 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
82
- return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
83
- }
84
- if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
85
- // **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
86
- // "挑一个能用的"等于自己开门
87
- for (const a of addrs) {
88
- const why = blockReason(a.address);
89
- if (why) return { deny: `${bare} → ${why}` };
90
- }
91
- return { ip: addrs[0].address, family: addrs[0].family };
92
- }
93
-
94
- // 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
95
- // 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
96
- const denyTail = (kind) => (kind === 'dns'
97
- ? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
98
- : '内网与本机地址是硬边界,不是可配置项。');
99
-
100
- const denyBody = (reason, kind) => {
101
- const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
102
- return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
103
- + `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
104
- };
105
-
106
- /** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
107
- export async function startBrowseProxy() {
108
- if (server) return { port, blocked };
109
-
110
- server = http.createServer();
111
-
112
- // 明文 http:绝对形式的请求行(`GET http://host/path`)
113
- server.on('request', async (req, res) => {
114
- let u;
115
- try { u = new URL(req.url); } catch {
116
- res.writeHead(400); return res.end('proxy: bad absolute-form request');
117
- }
118
- const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
119
- const verdict = await resolveAllowed(u.hostname, p);
120
- if (verdict.deny) {
121
- note(`${u.hostname}:${p}`, verdict.deny);
122
- res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
123
- return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
124
- }
125
- // 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
126
- const upstream = http.request({
127
- host: verdict.ip, port: p, method: req.method,
128
- path: u.pathname + u.search,
129
- headers: { ...req.headers, host: u.host },
130
- timeout: CONNECT_TIMEOUT_MS,
131
- setHost: false,
132
- }, (up) => {
133
- res.writeHead(up.statusCode || 502, up.headers);
134
- up.pipe(res);
135
- });
136
- upstream.on('error', (err) => {
137
- if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
138
- res.end(`proxy upstream error: ${err.message}`);
139
- });
140
- req.pipe(upstream);
141
- });
142
-
143
- // CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
144
- server.on('connect', async (req, socket, head) => {
145
- const [rawHost, rawPort] = String(req.url || '').split(':');
146
- const p = Number(rawPort || 443);
147
- const verdict = await resolveAllowed(rawHost || '', p);
148
- if (verdict.deny) {
149
- note(`${rawHost}:${p}`, verdict.deny);
150
- socket.write(denyBody(verdict.deny, verdict.kind));
151
- return socket.destroy();
152
- }
153
- const up = net.connect({ host: verdict.ip, port: p }, () => {
154
- socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
155
- if (head?.length) up.write(head);
156
- socket.pipe(up); up.pipe(socket);
157
- });
158
- up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
159
- up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
160
- socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
161
- });
162
-
163
- await new Promise((resolve, reject) => {
164
- server.once('error', reject);
165
- server.listen(0, '127.0.0.1', resolve);
166
- });
167
- port = server.address().port;
168
- server.unref?.();
169
- console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
170
- return { port, blocked };
171
- }
172
-
173
- /** 给工具的返回值用:这次调用期间拦了什么 */
174
- export function blockedSince(n) {
175
- return blocked.slice(n);
176
- }
177
- export function blockedCount() {
178
- return blocked.length;
179
- }
180
- export const _proxy = { ALLOWED_PORTS };
1
+ /**
2
+ * server/lib/browse-proxy.js — 浏览通道的出网闸,装在**代理层**(2026-08-18)
3
+ *
4
+ * ## 为什么闸从 CDP 挪到代理
5
+ *
6
+ * 第一版把闸装在 CDP 的 `Fetch.requestPaused` 上。它拦得住导航、子资源、跳转每一跳、
7
+ * 跨源 iframe —— 但审查真攻出来四条它**根本看不见**的路:
8
+ *
9
+ * - **WebSocket 握手**(`ws://127.0.0.1:4001` 双向读到了数据)
10
+ * - **`<link rel=prefetch>` / speculation rules**(打到了生产 :4001)
11
+ * - **`navigator.sendBeacon`**
12
+ * - **还没装上闸的弹窗**(`window.open` / `target=_blank`,在那 250ms 窗口里
13
+ * 能发 GET 和 POST,还能循环开)
14
+ *
15
+ * 共同点是它们都不走 `Fetch` 那个阶段。**逐条补是补不完的** —— 下一个 web 平台特性
16
+ * 又会开一条。所以闸挪到**所有出网都必经的那一层**:给这个浏览器挂一个我们自己的
17
+ * HTTP 代理,`--proxy-server` 之后 chromium 的每一次连接都从这儿走。
18
+ * 实测代理确实看得到上面四条全部(包括 WS 的 `CONNECT 127.0.0.1:4001`)。
19
+ *
20
+ * ## 附带白拿的两件
21
+ *
22
+ * 1. ⭐ **DNS 重绑定被根除,不再是"缓解"。** 我们自己解析、自己**连那个验过的 IP**
23
+ * (pin),浏览器拿不到第二次解析的机会。CDP 那版只能是"解析一次→浏览器再解析
24
+ * 一次",中间有 TOCTOU 窗口,而且主机名缓存把窗口变成了保证 30 秒的洞。
25
+ * 2. **没有初始化竞态。** 代理在浏览器启动参数里,第一个字节之前就生效;
26
+ * CDP 那版要等 `Fetch.enable` 握手完,弹窗根本等不到。
27
+ *
28
+ * CDP 那道闸**保留**当纵深(它能给出漂亮的按请求记账),但不再是唯一一道。
29
+ *
30
+ * ## 感知通道不挂代理
31
+ *
32
+ * 感知通道(看自己产物)**故意**走 loopback 的 artifact-file,它必须能到 127.0.0.1。
33
+ * 所以只有浏览通道的浏览器带 `--proxy-server`,两条通道在这一层物理分开。
34
+ */
35
+
36
+ import http from 'node:http';
37
+ import net from 'node:net';
38
+ import dns from 'node:dns/promises';
39
+ import { blockReason } from './ssrf-guard.js';
40
+
41
+ /** 允许连的端口。80/443 之外的公网端口极少是设计参考站,而放开等于多一片攻击面。 */
42
+ const ALLOWED_PORTS = new Set([80, 443, 8080, 8443]);
43
+ const RESOLVE_TIMEOUT_MS = 4000;
44
+ const CONNECT_TIMEOUT_MS = 15_000;
45
+
46
+ let server = null;
47
+ let port = 0;
48
+ const blocked = [];
49
+
50
+ function note(target, reason) {
51
+ blocked.push({ target, reason, at: Date.now() });
52
+ if (blocked.length > 200) blocked.shift();
53
+ }
54
+
55
+ /**
56
+ * 解析 + 判定,返回可以连的那个 IP(**pin 住它**,不把主机名交给下游再解析一次)。
57
+ * @returns {Promise<{ip: string, family: number} | {deny: string}>}
58
+ */
59
+ async function resolveAllowed(host, portNum) {
60
+ if (!ALLOWED_PORTS.has(portNum)) {
61
+ return { deny: `port ${portNum} is not allowed (only ${[...ALLOWED_PORTS].join('/')})` };
62
+ }
63
+ const bare = host.toLowerCase().replace(/^\[|\]$/g, '');
64
+ if (bare === 'localhost' || bare.endsWith('.localhost') || bare.endsWith('.local')) {
65
+ return { deny: `${bare} resolves inside the local network` };
66
+ }
67
+ // 字面量 IP 不劳烦 DNS
68
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(bare) || bare.includes(':')) {
69
+ const why = blockReason(bare);
70
+ return why ? { deny: why } : { ip: bare, family: bare.includes(':') ? 6 : 4 };
71
+ }
72
+ let addrs;
73
+ try {
74
+ addrs = await Promise.race([
75
+ dns.lookup(bare, { all: true, verbatim: true }),
76
+ new Promise((_, rej) => setTimeout(() => rej(new Error('dns timeout')), RESOLVE_TIMEOUT_MS)),
77
+ ]);
78
+ } catch (err) {
79
+ // ⚠️ kind:'dns' 是给 denyBody 分文案用的:解析失败**不是策略拦截**。
80
+ // 第一版对 NXDOMAIN 也甩「内网与本机地址是硬边界」,agent 上报时据此
81
+ // 推理出一整套"代理有两个解析器"的错误理论 —— 错误提示比没提示更贵。
82
+ return { deny: `cannot resolve ${bare}: ${err.message}`, kind: 'dns' };
83
+ }
84
+ if (!addrs.length) return { deny: `${bare} resolved to nothing`, kind: 'dns' };
85
+ // **任一**地址落在禁止段就整个拒 —— 多 A 记录里混一条内网是标准起手式,
86
+ // "挑一个能用的"等于自己开门
87
+ for (const a of addrs) {
88
+ const why = blockReason(a.address);
89
+ if (why) return { deny: `${bare} → ${why}` };
90
+ }
91
+ return { ip: addrs[0].address, family: addrs[0].family };
92
+ }
93
+
94
+ // 按拒因分文案:DNS 解析失败要说清"这个域名现在指不到任何地方",
95
+ // 策略拦截才配那句硬边界 —— 两种拒混着说,agent 会朝错误的方向排障。
96
+ const denyTail = (kind) => (kind === 'dns'
97
+ ? '这是域名解析失败(域名可能不存在、拼错或已下线),不是出网策略拦截。'
98
+ : '内网与本机地址是硬边界,不是可配置项。');
99
+
100
+ const denyBody = (reason, kind) => {
101
+ const body = `拒绝出网:${reason}\n\n${denyTail(kind)}`;
102
+ return `HTTP/1.1 403 Forbidden\r\nContent-Type: text/plain; charset=utf-8\r\n`
103
+ + `Content-Length: ${Buffer.byteLength(body)}\r\nConnection: close\r\n\r\n${body}`;
104
+ };
105
+
106
+ /** 启动(幂等)。@returns {Promise<{port:number, blocked:Array}>} */
107
+ export async function startBrowseProxy() {
108
+ if (server) return { port, blocked };
109
+
110
+ server = http.createServer();
111
+
112
+ // 明文 http:绝对形式的请求行(`GET http://host/path`)
113
+ server.on('request', async (req, res) => {
114
+ let u;
115
+ try { u = new URL(req.url); } catch {
116
+ res.writeHead(400); return res.end('proxy: bad absolute-form request');
117
+ }
118
+ const p = Number(u.port || (u.protocol === 'https:' ? 443 : 80));
119
+ const verdict = await resolveAllowed(u.hostname, p);
120
+ if (verdict.deny) {
121
+ note(`${u.hostname}:${p}`, verdict.deny);
122
+ res.writeHead(403, { 'Content-Type': 'text/plain; charset=utf-8' });
123
+ return res.end(`拒绝出网:${verdict.deny}\n\n${denyTail(verdict.kind)}`);
124
+ }
125
+ // 连**验过的那个 IP**,Host 头保留原主机名(虚拟主机才认得出)
126
+ const upstream = http.request({
127
+ host: verdict.ip, port: p, method: req.method,
128
+ path: u.pathname + u.search,
129
+ headers: { ...req.headers, host: u.host },
130
+ timeout: CONNECT_TIMEOUT_MS,
131
+ setHost: false,
132
+ }, (up) => {
133
+ res.writeHead(up.statusCode || 502, up.headers);
134
+ up.pipe(res);
135
+ });
136
+ upstream.on('error', (err) => {
137
+ if (!res.headersSent) res.writeHead(502, { 'Content-Type': 'text/plain' });
138
+ res.end(`proxy upstream error: ${err.message}`);
139
+ });
140
+ req.pipe(upstream);
141
+ });
142
+
143
+ // CONNECT:https / wss 都走这条(**WS 就是靠它被盖住的**)
144
+ server.on('connect', async (req, socket, head) => {
145
+ const [rawHost, rawPort] = String(req.url || '').split(':');
146
+ const p = Number(rawPort || 443);
147
+ const verdict = await resolveAllowed(rawHost || '', p);
148
+ if (verdict.deny) {
149
+ note(`${rawHost}:${p}`, verdict.deny);
150
+ socket.write(denyBody(verdict.deny, verdict.kind));
151
+ return socket.destroy();
152
+ }
153
+ const up = net.connect({ host: verdict.ip, port: p }, () => {
154
+ socket.write('HTTP/1.1 200 Connection Established\r\nProxy-Agent: nodesign-browse\r\n\r\n');
155
+ if (head?.length) up.write(head);
156
+ socket.pipe(up); up.pipe(socket);
157
+ });
158
+ up.setTimeout(CONNECT_TIMEOUT_MS, () => up.destroy());
159
+ up.on('error', () => { try { socket.destroy(); } catch { /* */ } });
160
+ socket.on('error', () => { try { up.destroy(); } catch { /* */ } });
161
+ });
162
+
163
+ await new Promise((resolve, reject) => {
164
+ server.once('error', reject);
165
+ server.listen(0, '127.0.0.1', resolve);
166
+ });
167
+ port = server.address().port;
168
+ server.unref?.();
169
+ console.log(`[browse-proxy] listening on 127.0.0.1:${port}`);
170
+ return { port, blocked };
171
+ }
172
+
173
+ /** 给工具的返回值用:这次调用期间拦了什么 */
174
+ export function blockedSince(n) {
175
+ return blocked.slice(n);
176
+ }
177
+ export function blockedCount() {
178
+ return blocked.length;
179
+ }
180
+ export const _proxy = { ALLOWED_PORTS };