@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,137 +1,137 @@
1
- /**
2
- * server/engine/browse/card.js — 桌面上那张浏览器卡(2026-08-18)
3
- *
4
- * `/artifacts` 每次都会调它,所以要便宜:一次读小 json + 一次 stat,读不到就 null。
5
- *
6
- * ## 为什么它是一张卡,而不只是一扇窗
7
- *
8
- * 建这条通道时我在 `BrowserWindow.jsx` 的文件头里写过:「这扇窗不是产物…
9
- * **你不会想在桌面上永久摆着一张"某次浏览"的卡片**」。用户 2026-08-18 拍反了,
10
- * 而且理由比我的判断硬:agent 逛站这件事**用户要能随时进去看、随时接手**。
11
- * 只由 `run.browser_opened` 事件开窗意味着"错过就没了"——刷新一下、切个项目回来,
12
- * 那扇窗和它背后正在等人的 agent 就都找不见了。
13
- *
14
- * 所以卡片的性质要说清楚,它跟另外三种产物卡**不一样**:
15
- * - deck / 站点 / word 的真相是**磁盘上的文件**,卡是文件的投影;
16
- * - 这张卡的真相是**一段浏览痕迹**(`.browser/state.json`),卡是"上次逛到哪"的
17
- * 投影,背后那只 chromium 是**运行时**,可以不在(空闲 5 分钟就回收)。
18
- *
19
- * ## 它是「工具卡」这一类的第一张(2026-08-18 下午,用户定的类别)
20
- *
21
- * 用户的原话:「可以是单纯的内容卡,也可以是单纯的**工具卡**(工具存放工具采集到的
22
- * 内容,以及可互动工具的显示)」。所以这张卡同时是两样东西:
23
- *
24
- * 1. **一个目录**(`assets/references/web/`,一站一子文件夹)—— 工具采回来的东西
25
- * 住在里面,按站点产物那条范式:目录 = 单位,文件名表明类别;
26
- * 2. **一个可交互的东西** —— 双击进去是活的浏览器画面,能接手操作。
27
- *
28
- * 类别是**两条叠加的轴**(也是用户定的):内容轴(这是什么)× 来源轴(谁产出的)。
29
- * 这张卡在内容轴上是 `tool`、来源轴上是 `tool`;agent 写的 deck 是 `work`+`agent`;
30
- * 用户传的图是 `material`+`user`。桌面按任意一条轴过滤。
31
- *
32
- * 后果是具体的:它不进 kinds 注册表(那张表登记的是**产物**形态)、不能导出、
33
- * 不进上下文托盘。它是入口 + 容器。
34
- */
35
-
36
- import path from 'node:path';
37
- import fs from 'node:fs/promises';
38
- import { browseState } from './registry.js';
39
- import { readVisit, framePath } from './state.js';
40
- import { CAPTURE_DIR } from './capture.js';
41
- import { getSharedDir } from '../../projects/workspace.js';
42
-
43
- /**
44
- * @param {string} projectId
45
- * @returns {Promise<null | {url: string, host: string, title: string|null,
46
- * at: string, live: boolean, busy: boolean, hasPreview: boolean}>}
47
- * null = 这个项目没逛过任何站 → 桌面上就不该有这张卡
48
- */
49
- export async function browseCard(projectId) {
50
- let visit = null;
51
- let state = { live: false, url: null, busy: false };
52
- try {
53
- visit = await readVisit(projectId);
54
- state = browseState(projectId);
55
- } catch { return null; }
56
- // 活实例的 url 才是当下;没有活实例时用落盘的那个(卡活得比实例长)
57
- const raw = state.live ? (state.url || visit?.url) : visit?.url;
58
- const url = (raw && /^https?:/.test(raw)) ? raw : null;
59
- const sites = await collectedSites(projectId);
60
- /**
61
- * ⭐ 存在判据是**两者之一**:有访问记录,或者采到过东西。
62
- *
63
- * 只看访问记录是不够的(第一版就是,普查时逮到):`state.json` 是 2026-08-18
64
- * 下午才开始写的,而 `assets/references/web/` 里线上已经躺着别的项目采的东西 ——
65
- * 那些项目会一张卡都没有,采集成果彻底看不见。反过来"逛过但没采"也该有卡
66
- * (它是入口)。所以两条任一成立就出卡。
67
- */
68
- if (!url && !sites.length) return null;
69
- const hasPreview = await fs.stat(framePath(projectId)).then(() => true, () => false);
70
- return {
71
- sites,
72
- url,
73
- host: (() => {
74
- if (!url) return sites[0]?.site || '采集'; // 只有采集没有访问记录时,卡上写第一个站
75
- try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; }
76
- })(),
77
- title: visit?.title || null,
78
- at: visit?.at || null,
79
- live: !!state.live,
80
- busy: !!state.busy,
81
- hasPreview,
82
- };
83
- }
84
-
85
- /**
86
- * 这个工具采回来的东西,**按站分组**(目录 = 单位)。
87
- *
88
- * 只 readdir 两层(站目录 + 里面的文件),不进 `.meta/`。封面取第一张 screenshot ——
89
- * 卡面和窗里的分组头都用它。读不到就当没有:采集清单列不出来不该让整张卡消失。
90
- */
91
- async function collectedSites(projectId) {
92
- const root = path.join(getSharedDir(projectId), CAPTURE_DIR);
93
- let ents;
94
- try { ents = await fs.readdir(root, { withFileTypes: true }); } catch { return []; }
95
- const groups = new Map(); // 站名 → { site, count, cover, dir }
96
- const add = (site, dir, file) => {
97
- const g = groups.get(site) || { site, count: 0, cover: null, dir };
98
- g.count += 1;
99
- if (!g.cover && /\.screenshot\.(webp|png)$/i.test(file)) g.cover = `${dir}/${file}`;
100
- groups.set(site, g);
101
- };
102
-
103
- for (const d of ents) {
104
- if (d.name.startsWith('.')) continue;
105
- if (d.isDirectory()) {
106
- // 现行布局:一站一文件夹
107
- let files;
108
- try { files = await fs.readdir(path.join(root, d.name)); } catch { continue; }
109
- for (const f of files.filter(n => !n.startsWith('.'))) {
110
- add(d.name, `${CAPTURE_DIR}/${d.name}`, f);
111
- }
112
- }
113
- }
114
-
115
- /**
116
- * ⚠️ **存量:平铺在 `web/` 根上的老采集**(一站一文件夹是 2026-08-18 下午才改的)。
117
- * 线上真有 —— 普查时另一个项目躺着 8 个。不认它们的话那张卡会写"采过 0 个站",
118
- * 而文件明明在,用户只会觉得东西丢了。
119
- *
120
- * 归站靠**出处 sidecar 里的 sourceUrl**,不是猜文件名前缀(老命名是
121
- * `<host>-<时间>.<档>`,host 里带点、时间带横线,切出来的东西不能当站名)。
122
- * **刻意不搬文件**:references 是用户数据,静默 mv 会打断任何引用了老路径的
123
- * 东西(便签里记的路径、站点里 `<img src>`)。读的时候认就够了。
124
- */
125
- const loose = ents.filter(e => e.isFile() && !e.name.startsWith('.')).slice(0, 60);
126
- for (const f of loose) {
127
- let host = '早期采集';
128
- try {
129
- const stem = f.name.replace(/\.[^.]+$/, '');
130
- const meta = JSON.parse(await fs.readFile(path.join(root, '.meta', `${stem}.json`), 'utf8'));
131
- if (meta?.sourceUrl) host = new URL(meta.sourceUrl).hostname.replace(/^www\./, '');
132
- } catch { /* 没出处就归到"早期采集" */ }
133
- add(host, CAPTURE_DIR, f.name);
134
- }
135
-
136
- return [...groups.values()].sort((a, b) => b.count - a.count || a.site.localeCompare(b.site));
137
- }
1
+ /**
2
+ * server/engine/browse/card.js — 桌面上那张浏览器卡(2026-08-18)
3
+ *
4
+ * `/artifacts` 每次都会调它,所以要便宜:一次读小 json + 一次 stat,读不到就 null。
5
+ *
6
+ * ## 为什么它是一张卡,而不只是一扇窗
7
+ *
8
+ * 建这条通道时我在 `BrowserWindow.jsx` 的文件头里写过:「这扇窗不是产物…
9
+ * **你不会想在桌面上永久摆着一张"某次浏览"的卡片**」。用户 2026-08-18 拍反了,
10
+ * 而且理由比我的判断硬:agent 逛站这件事**用户要能随时进去看、随时接手**。
11
+ * 只由 `run.browser_opened` 事件开窗意味着"错过就没了"——刷新一下、切个项目回来,
12
+ * 那扇窗和它背后正在等人的 agent 就都找不见了。
13
+ *
14
+ * 所以卡片的性质要说清楚,它跟另外三种产物卡**不一样**:
15
+ * - deck / 站点 / word 的真相是**磁盘上的文件**,卡是文件的投影;
16
+ * - 这张卡的真相是**一段浏览痕迹**(`.browser/state.json`),卡是"上次逛到哪"的
17
+ * 投影,背后那只 chromium 是**运行时**,可以不在(空闲 5 分钟就回收)。
18
+ *
19
+ * ## 它是「工具卡」这一类的第一张(2026-08-18 下午,用户定的类别)
20
+ *
21
+ * 用户的原话:「可以是单纯的内容卡,也可以是单纯的**工具卡**(工具存放工具采集到的
22
+ * 内容,以及可互动工具的显示)」。所以这张卡同时是两样东西:
23
+ *
24
+ * 1. **一个目录**(`assets/references/web/`,一站一子文件夹)—— 工具采回来的东西
25
+ * 住在里面,按站点产物那条范式:目录 = 单位,文件名表明类别;
26
+ * 2. **一个可交互的东西** —— 双击进去是活的浏览器画面,能接手操作。
27
+ *
28
+ * 类别是**两条叠加的轴**(也是用户定的):内容轴(这是什么)× 来源轴(谁产出的)。
29
+ * 这张卡在内容轴上是 `tool`、来源轴上是 `tool`;agent 写的 deck 是 `work`+`agent`;
30
+ * 用户传的图是 `material`+`user`。桌面按任意一条轴过滤。
31
+ *
32
+ * 后果是具体的:它不进 kinds 注册表(那张表登记的是**产物**形态)、不能导出、
33
+ * 不进上下文托盘。它是入口 + 容器。
34
+ */
35
+
36
+ import path from 'node:path';
37
+ import fs from 'node:fs/promises';
38
+ import { browseState } from './registry.js';
39
+ import { readVisit, framePath } from './state.js';
40
+ import { CAPTURE_DIR } from './capture.js';
41
+ import { getSharedDir } from '../../projects/workspace.js';
42
+
43
+ /**
44
+ * @param {string} projectId
45
+ * @returns {Promise<null | {url: string, host: string, title: string|null,
46
+ * at: string, live: boolean, busy: boolean, hasPreview: boolean}>}
47
+ * null = 这个项目没逛过任何站 → 桌面上就不该有这张卡
48
+ */
49
+ export async function browseCard(projectId) {
50
+ let visit = null;
51
+ let state = { live: false, url: null, busy: false };
52
+ try {
53
+ visit = await readVisit(projectId);
54
+ state = browseState(projectId);
55
+ } catch { return null; }
56
+ // 活实例的 url 才是当下;没有活实例时用落盘的那个(卡活得比实例长)
57
+ const raw = state.live ? (state.url || visit?.url) : visit?.url;
58
+ const url = (raw && /^https?:/.test(raw)) ? raw : null;
59
+ const sites = await collectedSites(projectId);
60
+ /**
61
+ * ⭐ 存在判据是**两者之一**:有访问记录,或者采到过东西。
62
+ *
63
+ * 只看访问记录是不够的(第一版就是,普查时逮到):`state.json` 是 2026-08-18
64
+ * 下午才开始写的,而 `assets/references/web/` 里线上已经躺着别的项目采的东西 ——
65
+ * 那些项目会一张卡都没有,采集成果彻底看不见。反过来"逛过但没采"也该有卡
66
+ * (它是入口)。所以两条任一成立就出卡。
67
+ */
68
+ if (!url && !sites.length) return null;
69
+ const hasPreview = await fs.stat(framePath(projectId)).then(() => true, () => false);
70
+ return {
71
+ sites,
72
+ url,
73
+ host: (() => {
74
+ if (!url) return sites[0]?.site || '采集'; // 只有采集没有访问记录时,卡上写第一个站
75
+ try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return url; }
76
+ })(),
77
+ title: visit?.title || null,
78
+ at: visit?.at || null,
79
+ live: !!state.live,
80
+ busy: !!state.busy,
81
+ hasPreview,
82
+ };
83
+ }
84
+
85
+ /**
86
+ * 这个工具采回来的东西,**按站分组**(目录 = 单位)。
87
+ *
88
+ * 只 readdir 两层(站目录 + 里面的文件),不进 `.meta/`。封面取第一张 screenshot ——
89
+ * 卡面和窗里的分组头都用它。读不到就当没有:采集清单列不出来不该让整张卡消失。
90
+ */
91
+ async function collectedSites(projectId) {
92
+ const root = path.join(getSharedDir(projectId), CAPTURE_DIR);
93
+ let ents;
94
+ try { ents = await fs.readdir(root, { withFileTypes: true }); } catch { return []; }
95
+ const groups = new Map(); // 站名 → { site, count, cover, dir }
96
+ const add = (site, dir, file) => {
97
+ const g = groups.get(site) || { site, count: 0, cover: null, dir };
98
+ g.count += 1;
99
+ if (!g.cover && /\.screenshot\.(webp|png)$/i.test(file)) g.cover = `${dir}/${file}`;
100
+ groups.set(site, g);
101
+ };
102
+
103
+ for (const d of ents) {
104
+ if (d.name.startsWith('.')) continue;
105
+ if (d.isDirectory()) {
106
+ // 现行布局:一站一文件夹
107
+ let files;
108
+ try { files = await fs.readdir(path.join(root, d.name)); } catch { continue; }
109
+ for (const f of files.filter(n => !n.startsWith('.'))) {
110
+ add(d.name, `${CAPTURE_DIR}/${d.name}`, f);
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * ⚠️ **存量:平铺在 `web/` 根上的老采集**(一站一文件夹是 2026-08-18 下午才改的)。
117
+ * 线上真有 —— 普查时另一个项目躺着 8 个。不认它们的话那张卡会写"采过 0 个站",
118
+ * 而文件明明在,用户只会觉得东西丢了。
119
+ *
120
+ * 归站靠**出处 sidecar 里的 sourceUrl**,不是猜文件名前缀(老命名是
121
+ * `<host>-<时间>.<档>`,host 里带点、时间带横线,切出来的东西不能当站名)。
122
+ * **刻意不搬文件**:references 是用户数据,静默 mv 会打断任何引用了老路径的
123
+ * 东西(便签里记的路径、站点里 `<img src>`)。读的时候认就够了。
124
+ */
125
+ const loose = ents.filter(e => e.isFile() && !e.name.startsWith('.')).slice(0, 60);
126
+ for (const f of loose) {
127
+ let host = '早期采集';
128
+ try {
129
+ const stem = f.name.replace(/\.[^.]+$/, '');
130
+ const meta = JSON.parse(await fs.readFile(path.join(root, '.meta', `${stem}.json`), 'utf8'));
131
+ if (meta?.sourceUrl) host = new URL(meta.sourceUrl).hostname.replace(/^www\./, '');
132
+ } catch { /* 没出处就归到"早期采集" */ }
133
+ add(host, CAPTURE_DIR, f.name);
134
+ }
135
+
136
+ return [...groups.values()].sort((a, b) => b.count - a.count || a.site.localeCompare(b.site));
137
+ }
@@ -1,59 +1,59 @@
1
- /**
2
- * server/engine/browse/handover.js — agent 求助 / 人接手(2026-08-18)
3
- *
4
- * 用户拍的方向是「**agent 需要帮助的时候人类才介入**」,不做人与 agent 同时操作的
5
- * 共享控制。所以这一层很小:agent 举手 → 用户在窗里点几下 → 用户说"好了" → agent 继续。
6
- *
7
- * ## 为什么 agent 要**阻塞等**
8
- *
9
- * 接手期间页面状态被人改(过验证墙、登录、关弹窗),agent 并行去干别的会基于陈旧
10
- * 状态出错。所以 `browser_request_help` 就停在这儿等。
11
- *
12
- * ## 为什么必须有超时
13
- *
14
- * 人可能就走了。没有超时的话 agent 会挂到会话结束 —— 那是最糟的失败形态
15
- * (用户看到的是"它卡住了",而不是"它说了它需要什么")。
16
- */
17
-
18
- /** projectId → { resolve, timer, reason, since } */
19
- const waiting = new Map();
20
-
21
- export const HELP_TIMEOUT_MS = Number(process.env.ND_BROWSE_HELP_TIMEOUT_MS || 0) || 120_000;
22
-
23
- /**
24
- * agent 举手,等人。
25
- * @returns {Promise<{released: boolean, url: string|null, waitedMs: number, by: string}>}
26
- * released=false 表示等超时了(人没来)
27
- */
28
- export function requestHelp(projectId, reason, { timeoutMs = HELP_TIMEOUT_MS } = {}) {
29
- // 同一项目重复举手:把前一次直接放走(agent 不该被自己的两次求助卡死)
30
- const prev = waiting.get(projectId);
31
- if (prev) { clearTimeout(prev.timer); prev.resolve({ released: false, url: null, waitedMs: 0, by: 'superseded' }); }
32
-
33
- const since = Date.now();
34
- return new Promise((resolve) => {
35
- const done = (v) => {
36
- if (!waiting.has(projectId)) return;
37
- clearTimeout(waiting.get(projectId).timer);
38
- waiting.delete(projectId);
39
- resolve({ ...v, waitedMs: Date.now() - since });
40
- };
41
- const timer = setTimeout(() => done({ released: false, url: null, by: 'timeout' }), timeoutMs);
42
- timer.unref?.();
43
- waiting.set(projectId, { resolve: done, timer, reason, since });
44
- });
45
- }
46
-
47
- /** 人点了「好了继续」 */
48
- export function releaseHelp(projectId, { url = null, by = 'human' } = {}) {
49
- const w = waiting.get(projectId);
50
- if (!w) return false;
51
- w.resolve({ released: true, url, by });
52
- return true;
53
- }
54
-
55
- /** 有人在等吗(前端开窗时用来决定要不要亮 banner) */
56
- export function pendingHelp(projectId) {
57
- const w = waiting.get(projectId);
58
- return w ? { reason: w.reason, waitingMs: Date.now() - w.since } : null;
59
- }
1
+ /**
2
+ * server/engine/browse/handover.js — agent 求助 / 人接手(2026-08-18)
3
+ *
4
+ * 用户拍的方向是「**agent 需要帮助的时候人类才介入**」,不做人与 agent 同时操作的
5
+ * 共享控制。所以这一层很小:agent 举手 → 用户在窗里点几下 → 用户说"好了" → agent 继续。
6
+ *
7
+ * ## 为什么 agent 要**阻塞等**
8
+ *
9
+ * 接手期间页面状态被人改(过验证墙、登录、关弹窗),agent 并行去干别的会基于陈旧
10
+ * 状态出错。所以 `browser_request_help` 就停在这儿等。
11
+ *
12
+ * ## 为什么必须有超时
13
+ *
14
+ * 人可能就走了。没有超时的话 agent 会挂到会话结束 —— 那是最糟的失败形态
15
+ * (用户看到的是"它卡住了",而不是"它说了它需要什么")。
16
+ */
17
+
18
+ /** projectId → { resolve, timer, reason, since } */
19
+ const waiting = new Map();
20
+
21
+ export const HELP_TIMEOUT_MS = Number(process.env.ND_BROWSE_HELP_TIMEOUT_MS || 0) || 120_000;
22
+
23
+ /**
24
+ * agent 举手,等人。
25
+ * @returns {Promise<{released: boolean, url: string|null, waitedMs: number, by: string}>}
26
+ * released=false 表示等超时了(人没来)
27
+ */
28
+ export function requestHelp(projectId, reason, { timeoutMs = HELP_TIMEOUT_MS } = {}) {
29
+ // 同一项目重复举手:把前一次直接放走(agent 不该被自己的两次求助卡死)
30
+ const prev = waiting.get(projectId);
31
+ if (prev) { clearTimeout(prev.timer); prev.resolve({ released: false, url: null, waitedMs: 0, by: 'superseded' }); }
32
+
33
+ const since = Date.now();
34
+ return new Promise((resolve) => {
35
+ const done = (v) => {
36
+ if (!waiting.has(projectId)) return;
37
+ clearTimeout(waiting.get(projectId).timer);
38
+ waiting.delete(projectId);
39
+ resolve({ ...v, waitedMs: Date.now() - since });
40
+ };
41
+ const timer = setTimeout(() => done({ released: false, url: null, by: 'timeout' }), timeoutMs);
42
+ timer.unref?.();
43
+ waiting.set(projectId, { resolve: done, timer, reason, since });
44
+ });
45
+ }
46
+
47
+ /** 人点了「好了继续」 */
48
+ export function releaseHelp(projectId, { url = null, by = 'human' } = {}) {
49
+ const w = waiting.get(projectId);
50
+ if (!w) return false;
51
+ w.resolve({ released: true, url, by });
52
+ return true;
53
+ }
54
+
55
+ /** 有人在等吗(前端开窗时用来决定要不要亮 banner) */
56
+ export function pendingHelp(projectId) {
57
+ const w = waiting.get(projectId);
58
+ return w ? { reason: w.reason, waitingMs: Date.now() - w.since } : null;
59
+ }
@@ -1,119 +1,119 @@
1
- /**
2
- * server/engine/browse/page-digest.js — 「这一页上有什么」的唯一一份采集器(2026-08-18)
3
- *
4
- * 两个消费者:`browser_read`(全量:正文 + 标题层级 + 链接清单)和
5
- * `browser_navigate`(**打开就顺手带回一份摘要**)。
6
- *
7
- * ## 为什么 navigate 要带摘要
8
- *
9
- * 量过一轮(2026-08-18,这台机器):冷启动 738ms、导航 1.0~4.5s、读页
10
- * 29~183ms、视口截图 0.2~4.2s。**浏览器本体不慢**。慢的是回合数 ——
11
- * `navigate` → `read` → `screenshot` 是三次模型往返,每一次的模型延迟都比
12
- * 上面任何一个数字大。而 navigate 之后紧跟一次 read 几乎是必然的(不看看
13
- * 页面上有什么,下一步无从决定)。
14
- *
15
- * 所以把 read 的采集器抽出来给 navigate 复用:一次导航直接回答"打开了,
16
- * 上面有这些东西,站内可以往这几个地方去"。**省掉的是一整个回合**,
17
- * 这是这条通道唯一真能砍掉的时间。
18
- *
19
- * 抽出来还有第二个收益:`browser_read` 报的东西和 `browser_navigate` 报的
20
- * 东西**只有一个来源**。抄两份的下场是"同一页,两个工具说的结构数不一样"。
21
- */
22
-
23
- /**
24
- * 页面里跑的采集器。⚠️ 整段在浏览器上下文执行,不能引用外面的东西。
25
- *
26
- * @param {import('playwright').Page} page
27
- * @param {{selector?: string|null, links?: boolean}} opts
28
- */
29
- export function collectPage(page, { selector = null, links = true } = {}) {
30
- return page.evaluate(({ sel, want }) => {
31
- const root = sel ? document.querySelector(sel) : (document.querySelector('main') || document.body);
32
- if (!root) return { missing: true };
33
- const headings = [...root.querySelectorAll('h1,h2,h3')]
34
- .map(h => `${'#'.repeat(Number(h.tagName[1]))} ${(h.textContent || '').trim().slice(0, 90)}`)
35
- .filter(t => t.length > 2).slice(0, 40);
36
- // ⛔ 链接**不能**跟正文共用 root。没传 selector 时正文 root 是 `main`
37
- // (对:导航文字是噪音),但链接跟着 main 一收,站名、整条导航、页脚法务
38
- // 链接就全没了 —— 实测 6 个链接只报出 1 个。而链接清单正是"下一步去哪"
39
- // 的唯一依据,browser_click 的全部意义也是跟这些链接。
40
- // 所以:正文按 root 收,链接按**整页**收,再标出它在哪个区。
41
- const linkRoot = sel ? root : document;
42
- const regionOf = (el) => {
43
- for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
44
- const tag = n.tagName ? n.tagName.toLowerCase() : '';
45
- if (tag === 'nav') return 'nav';
46
- if (tag === 'header') return 'header';
47
- if (tag === 'footer') return 'footer';
48
- if (tag === 'aside') return 'aside';
49
- const role = n.getAttribute && n.getAttribute('role');
50
- if (role === 'navigation') return 'nav';
51
- if (role === 'contentinfo') return 'footer';
52
- }
53
- return 'content';
54
- };
55
- const seen = new Set();
56
- const RANK = { content: 0, header: 1, nav: 1, aside: 2, footer: 3 };
57
- // ⚠️ **排序必须在截断之前**,而且得在页面里做:按文档顺序取前 80 条,
58
- // 侧边导航就把配额吃干了(MDN 实测 79 条站内链接里前 30 条有 29 条是 nav,
59
- // 正文那几条一条都没进来)。要跟的多半是正文里那几条。
60
- const all = want ? [...linkRoot.querySelectorAll('a[href]')].map((a) => {
61
- const href = a.href;
62
- if (!href || !/^https?:/.test(href) || seen.has(href)) return null;
63
- seen.add(href);
64
- const label = (a.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
65
- return { href, label, internal: a.hostname === location.hostname, region: regionOf(a) };
66
- }).filter(Boolean) : [];
67
- const linkList = all
68
- .map((l, i) => ({ l, i }))
69
- .sort((x, y) => (RANK[x.l.region] ?? 0) - (RANK[y.l.region] ?? 0) || x.i - y.i)
70
- .map(x => x.l)
71
- .slice(0, 80);
72
- return {
73
- text: (root.innerText || '').replace(/\n{3,}/g, '\n\n').trim(),
74
- headings, linkList,
75
- textScope: sel || (document.querySelector('main') ? 'main' : 'body'),
76
- counts: { // ⚠️ 这几个数是**整页**的,别跟着正文 root 缩(原来标着"页面结构"其实是 main 里的)
77
- sections: document.querySelectorAll('section, article').length,
78
- images: document.querySelectorAll('img').length,
79
- forms: document.querySelectorAll('form').length,
80
- },
81
- };
82
- }, { sel: selector, want: links });
83
- }
84
-
85
- /** 区标只在非 content 时打 —— 正文链接是默认情况,标了全是噪音 */
86
- const fmtLink = (l) => ` ${l.label || '(无文字)'}${l.region && l.region !== 'content' ? ` [${l.region}]` : ''} → ${l.href}`;
87
-
88
- /**
89
- * 采集结果 → 给 agent 的文本行。
90
- *
91
- * @param {object} data collectPage 的返回
92
- * @param {{compact?: boolean, cap?: number}} opts
93
- * compact = navigate 用的摘要档:短正文 + 少量标题 + 站内链接,够决定下一步就行
94
- * @returns {string[]} 行(调用方自己在前面拼"现在在哪")
95
- */
96
- export function formatPage(data, { compact = false, cap } = {}) {
97
- const textCap = cap ?? (compact ? 700 : 4000);
98
- const maxHeadings = compact ? 12 : 40;
99
- const maxInner = compact ? 14 : 30;
100
- const body = data.text.length > textCap
101
- ? `${data.text.slice(0, textCap)}\n… (还有 ${data.text.length - textCap} 个字符${compact ? ',要全文用 browser_read' : ',要全文就加大 maxChars 或用 selector 缩范围'})`
102
- : data.text;
103
- // linkList 已经在页面里按区排过序(正文优先),这里只分站内/站外
104
- const inner = data.linkList.filter(l => l.internal);
105
- const outer = data.linkList.filter(l => !l.internal);
106
- const headings = data.headings.slice(0, maxHeadings);
107
-
108
- return [
109
- `结构(整页):${data.counts.sections} 个 section/article · ${data.counts.images} 张图 · ${data.counts.forms} 个表单`,
110
- compact ? null
111
- : `正文读的是 <${data.textScope}>${data.textScope === 'main' ? '(导航/页脚的文字不在里面,但它们的链接在下面)' : ''}`,
112
- headings.length ? `\n标题层级:\n${headings.join('\n')}` : null,
113
- `\n正文:\n${body || '(读不到文字 —— 可能整页是图或靠 JS 渲染,试 browser_navigate 带 waitUntil:"networkidle",或直接截图看)'}`,
114
- inner.length
115
- ? `\n站内链接(${inner.length}${inner.length > maxInner ? `,列前 ${maxInner}` : ''})—— 往里翻就跟这些:\n${inner.slice(0, maxInner).map(fmtLink).join('\n')}`
116
- : null,
117
- outer.length && !compact ? `\n站外链接(${outer.length},只列前 8):\n${outer.slice(0, 8).map(fmtLink).join('\n')}` : null,
118
- ].filter(Boolean);
119
- }
1
+ /**
2
+ * server/engine/browse/page-digest.js — 「这一页上有什么」的唯一一份采集器(2026-08-18)
3
+ *
4
+ * 两个消费者:`browser_read`(全量:正文 + 标题层级 + 链接清单)和
5
+ * `browser_navigate`(**打开就顺手带回一份摘要**)。
6
+ *
7
+ * ## 为什么 navigate 要带摘要
8
+ *
9
+ * 量过一轮(2026-08-18,这台机器):冷启动 738ms、导航 1.0~4.5s、读页
10
+ * 29~183ms、视口截图 0.2~4.2s。**浏览器本体不慢**。慢的是回合数 ——
11
+ * `navigate` → `read` → `screenshot` 是三次模型往返,每一次的模型延迟都比
12
+ * 上面任何一个数字大。而 navigate 之后紧跟一次 read 几乎是必然的(不看看
13
+ * 页面上有什么,下一步无从决定)。
14
+ *
15
+ * 所以把 read 的采集器抽出来给 navigate 复用:一次导航直接回答"打开了,
16
+ * 上面有这些东西,站内可以往这几个地方去"。**省掉的是一整个回合**,
17
+ * 这是这条通道唯一真能砍掉的时间。
18
+ *
19
+ * 抽出来还有第二个收益:`browser_read` 报的东西和 `browser_navigate` 报的
20
+ * 东西**只有一个来源**。抄两份的下场是"同一页,两个工具说的结构数不一样"。
21
+ */
22
+
23
+ /**
24
+ * 页面里跑的采集器。⚠️ 整段在浏览器上下文执行,不能引用外面的东西。
25
+ *
26
+ * @param {import('playwright').Page} page
27
+ * @param {{selector?: string|null, links?: boolean}} opts
28
+ */
29
+ export function collectPage(page, { selector = null, links = true } = {}) {
30
+ return page.evaluate(({ sel, want }) => {
31
+ const root = sel ? document.querySelector(sel) : (document.querySelector('main') || document.body);
32
+ if (!root) return { missing: true };
33
+ const headings = [...root.querySelectorAll('h1,h2,h3')]
34
+ .map(h => `${'#'.repeat(Number(h.tagName[1]))} ${(h.textContent || '').trim().slice(0, 90)}`)
35
+ .filter(t => t.length > 2).slice(0, 40);
36
+ // ⛔ 链接**不能**跟正文共用 root。没传 selector 时正文 root 是 `main`
37
+ // (对:导航文字是噪音),但链接跟着 main 一收,站名、整条导航、页脚法务
38
+ // 链接就全没了 —— 实测 6 个链接只报出 1 个。而链接清单正是"下一步去哪"
39
+ // 的唯一依据,browser_click 的全部意义也是跟这些链接。
40
+ // 所以:正文按 root 收,链接按**整页**收,再标出它在哪个区。
41
+ const linkRoot = sel ? root : document;
42
+ const regionOf = (el) => {
43
+ for (let n = el; n && n !== document.documentElement; n = n.parentElement) {
44
+ const tag = n.tagName ? n.tagName.toLowerCase() : '';
45
+ if (tag === 'nav') return 'nav';
46
+ if (tag === 'header') return 'header';
47
+ if (tag === 'footer') return 'footer';
48
+ if (tag === 'aside') return 'aside';
49
+ const role = n.getAttribute && n.getAttribute('role');
50
+ if (role === 'navigation') return 'nav';
51
+ if (role === 'contentinfo') return 'footer';
52
+ }
53
+ return 'content';
54
+ };
55
+ const seen = new Set();
56
+ const RANK = { content: 0, header: 1, nav: 1, aside: 2, footer: 3 };
57
+ // ⚠️ **排序必须在截断之前**,而且得在页面里做:按文档顺序取前 80 条,
58
+ // 侧边导航就把配额吃干了(MDN 实测 79 条站内链接里前 30 条有 29 条是 nav,
59
+ // 正文那几条一条都没进来)。要跟的多半是正文里那几条。
60
+ const all = want ? [...linkRoot.querySelectorAll('a[href]')].map((a) => {
61
+ const href = a.href;
62
+ if (!href || !/^https?:/.test(href) || seen.has(href)) return null;
63
+ seen.add(href);
64
+ const label = (a.textContent || '').replace(/\s+/g, ' ').trim().slice(0, 60);
65
+ return { href, label, internal: a.hostname === location.hostname, region: regionOf(a) };
66
+ }).filter(Boolean) : [];
67
+ const linkList = all
68
+ .map((l, i) => ({ l, i }))
69
+ .sort((x, y) => (RANK[x.l.region] ?? 0) - (RANK[y.l.region] ?? 0) || x.i - y.i)
70
+ .map(x => x.l)
71
+ .slice(0, 80);
72
+ return {
73
+ text: (root.innerText || '').replace(/\n{3,}/g, '\n\n').trim(),
74
+ headings, linkList,
75
+ textScope: sel || (document.querySelector('main') ? 'main' : 'body'),
76
+ counts: { // ⚠️ 这几个数是**整页**的,别跟着正文 root 缩(原来标着"页面结构"其实是 main 里的)
77
+ sections: document.querySelectorAll('section, article').length,
78
+ images: document.querySelectorAll('img').length,
79
+ forms: document.querySelectorAll('form').length,
80
+ },
81
+ };
82
+ }, { sel: selector, want: links });
83
+ }
84
+
85
+ /** 区标只在非 content 时打 —— 正文链接是默认情况,标了全是噪音 */
86
+ const fmtLink = (l) => ` ${l.label || '(无文字)'}${l.region && l.region !== 'content' ? ` [${l.region}]` : ''} → ${l.href}`;
87
+
88
+ /**
89
+ * 采集结果 → 给 agent 的文本行。
90
+ *
91
+ * @param {object} data collectPage 的返回
92
+ * @param {{compact?: boolean, cap?: number}} opts
93
+ * compact = navigate 用的摘要档:短正文 + 少量标题 + 站内链接,够决定下一步就行
94
+ * @returns {string[]} 行(调用方自己在前面拼"现在在哪")
95
+ */
96
+ export function formatPage(data, { compact = false, cap } = {}) {
97
+ const textCap = cap ?? (compact ? 700 : 4000);
98
+ const maxHeadings = compact ? 12 : 40;
99
+ const maxInner = compact ? 14 : 30;
100
+ const body = data.text.length > textCap
101
+ ? `${data.text.slice(0, textCap)}\n… (还有 ${data.text.length - textCap} 个字符${compact ? ',要全文用 browser_read' : ',要全文就加大 maxChars 或用 selector 缩范围'})`
102
+ : data.text;
103
+ // linkList 已经在页面里按区排过序(正文优先),这里只分站内/站外
104
+ const inner = data.linkList.filter(l => l.internal);
105
+ const outer = data.linkList.filter(l => !l.internal);
106
+ const headings = data.headings.slice(0, maxHeadings);
107
+
108
+ return [
109
+ `结构(整页):${data.counts.sections} 个 section/article · ${data.counts.images} 张图 · ${data.counts.forms} 个表单`,
110
+ compact ? null
111
+ : `正文读的是 <${data.textScope}>${data.textScope === 'main' ? '(导航/页脚的文字不在里面,但它们的链接在下面)' : ''}`,
112
+ headings.length ? `\n标题层级:\n${headings.join('\n')}` : null,
113
+ `\n正文:\n${body || '(读不到文字 —— 可能整页是图或靠 JS 渲染,试 browser_navigate 带 waitUntil:"networkidle",或直接截图看)'}`,
114
+ inner.length
115
+ ? `\n站内链接(${inner.length}${inner.length > maxInner ? `,列前 ${maxInner}` : ''})—— 往里翻就跟这些:\n${inner.slice(0, maxInner).map(fmtLink).join('\n')}`
116
+ : null,
117
+ outer.length && !compact ? `\n站外链接(${outer.length},只列前 8):\n${outer.slice(0, 8).map(fmtLink).join('\n')}` : null,
118
+ ].filter(Boolean);
119
+ }