@xiaobuyu/nodesign 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (372) hide show
  1. package/LICENSE +661 -0
  2. package/README.md +140 -0
  3. package/bin/nodesign.js +176 -0
  4. package/package.json +104 -0
  5. package/server/api/README.md +45 -0
  6. package/server/api/_guard.js +75 -0
  7. package/server/api/admin.js +187 -0
  8. package/server/api/assets/docx-page.js +103 -0
  9. package/server/api/assets/helpers.js +22 -0
  10. package/server/api/assets/notes.js +144 -0
  11. package/server/api/assets.js +896 -0
  12. package/server/api/board.js +57 -0
  13. package/server/api/browse.js +138 -0
  14. package/server/api/canvas.js +444 -0
  15. package/server/api/chatai.js +324 -0
  16. package/server/api/exports/build-standalone.js +980 -0
  17. package/server/api/exports/cards.js +118 -0
  18. package/server/api/exports/docx-pdf.js +46 -0
  19. package/server/api/exports/export-page.js +142 -0
  20. package/server/api/exports/handoff.js +147 -0
  21. package/server/api/exports.js +690 -0
  22. package/server/api/instruction.js +75 -0
  23. package/server/api/local.js +115 -0
  24. package/server/api/me.js +116 -0
  25. package/server/api/memory.js +185 -0
  26. package/server/api/pending-changes.js +366 -0
  27. package/server/api/plugins.js +164 -0
  28. package/server/api/projects.js +220 -0
  29. package/server/api/publish.js +64 -0
  30. package/server/api/recent.js +73 -0
  31. package/server/api/sessions.js +589 -0
  32. package/server/api/skills.js +77 -0
  33. package/server/api/standalone-fit.js +168 -0
  34. package/server/api/turn-compose.js +180 -0
  35. package/server/api/turn-inflight.js +44 -0
  36. package/server/api/turn.js +669 -0
  37. package/server/auth/README.md +17 -0
  38. package/server/auth/middleware.js +138 -0
  39. package/server/auth/origin-guard.js +71 -0
  40. package/server/auth/session.js +108 -0
  41. package/server/auth/tier.js +113 -0
  42. package/server/auth/users-store.js +348 -0
  43. package/server/edit/README.md +29 -0
  44. package/server/engine/README.md +113 -0
  45. package/server/engine/agent/agent-shared.js +550 -0
  46. package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -0
  47. package/server/engine/agent/auto-mode-rules.js +78 -0
  48. package/server/engine/agent/context.js +418 -0
  49. package/server/engine/agent/events.js +464 -0
  50. package/server/engine/agent/hooks/canvas-validate.js +199 -0
  51. package/server/engine/agent/hooks/failure.js +120 -0
  52. package/server/engine/agent/hooks/file-events.js +75 -0
  53. package/server/engine/agent/hooks/lifecycle.js +233 -0
  54. package/server/engine/agent/hooks/post-canvas-focus.js +80 -0
  55. package/server/engine/agent/hooks/post-guidance.js +206 -0
  56. package/server/engine/agent/hooks/post-subagent-report.js +104 -0
  57. package/server/engine/agent/hooks/post-trim.js +58 -0
  58. package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -0
  59. package/server/engine/agent/hooks/pre-defaults.js +84 -0
  60. package/server/engine/agent/hooks/pre-injectors.js +277 -0
  61. package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -0
  62. package/server/engine/agent/hooks/pre-starter-files.js +61 -0
  63. package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -0
  64. package/server/engine/agent/hooks/site-validate.js +94 -0
  65. package/server/engine/agent/hooks/tool-prompts.js +31 -0
  66. package/server/engine/agent/hooks/turn-state-memory.js +56 -0
  67. package/server/engine/agent/hooks/user-prompt-submit.js +215 -0
  68. package/server/engine/agent/hooks.js +354 -0
  69. package/server/engine/agent/init-contract.js +69 -0
  70. package/server/engine/agent/isolation.js +159 -0
  71. package/server/engine/agent/model-context.js +359 -0
  72. package/server/engine/agent/model-table.js +292 -0
  73. package/server/engine/agent/plugin-loader.js +266 -0
  74. package/server/engine/agent/prompts/nodesign-prelude.md +336 -0
  75. package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -0
  76. package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -0
  77. package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -0
  78. package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -0
  79. package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -0
  80. package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -0
  81. package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -0
  82. package/server/engine/agent/prompts/tools/site-reference.md +106 -0
  83. package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -0
  84. package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -0
  85. package/server/engine/agent/session-loop.js +1024 -0
  86. package/server/engine/agent/session-model.js +164 -0
  87. package/server/engine/agent/skill.js +204 -0
  88. package/server/engine/agent/system-prompts.js +86 -0
  89. package/server/engine/agent/task-events.js +78 -0
  90. package/server/engine/agents/ds-extractor.md +132 -0
  91. package/server/engine/agents/explorer.md +190 -0
  92. package/server/engine/agents/index.js +230 -0
  93. package/server/engine/agents/schemas/design-system.json +156 -0
  94. package/server/engine/agents/schemas/tweak-schema.json +113 -0
  95. package/server/engine/agents/tweak-proposer.md +127 -0
  96. package/server/engine/agents/vision-checker.md +254 -0
  97. package/server/engine/browse/capture.js +422 -0
  98. package/server/engine/browse/card.js +137 -0
  99. package/server/engine/browse/handover.js +59 -0
  100. package/server/engine/browse/page-digest.js +119 -0
  101. package/server/engine/browse/refs.js +196 -0
  102. package/server/engine/browse/registry.js +307 -0
  103. package/server/engine/browse/screencast.js +170 -0
  104. package/server/engine/browse/state.js +121 -0
  105. package/server/engine/chatai/chat-log.js +79 -0
  106. package/server/engine/chatai/index.js +193 -0
  107. package/server/engine/chatai/openai-compat.js +152 -0
  108. package/server/engine/chatai/orchestrate.js +309 -0
  109. package/server/engine/chatai/perform.js +40 -0
  110. package/server/engine/chatai/summarize.js +79 -0
  111. package/server/engine/mcp/capability-gate.js +52 -0
  112. package/server/engine/mcp/index.js +339 -0
  113. package/server/engine/mcp/param-sanitizer.js +142 -0
  114. package/server/engine/mcp/tools/arrange-on-board.js +124 -0
  115. package/server/engine/mcp/tools/artifact-session.js +278 -0
  116. package/server/engine/mcp/tools/browse-computer.js +421 -0
  117. package/server/engine/mcp/tools/browse-find-batch.js +183 -0
  118. package/server/engine/mcp/tools/browse-screenshot.js +157 -0
  119. package/server/engine/mcp/tools/browse.js +480 -0
  120. package/server/engine/mcp/tools/build-docx.js +101 -0
  121. package/server/engine/mcp/tools/clear-pending-changes.js +99 -0
  122. package/server/engine/mcp/tools/create-on-board.js +155 -0
  123. package/server/engine/mcp/tools/crystallize-skill.js +168 -0
  124. package/server/engine/mcp/tools/deliver-files.js +0 -0
  125. package/server/engine/mcp/tools/explain-style.js +237 -0
  126. package/server/engine/mcp/tools/export-handoff.js +133 -0
  127. package/server/engine/mcp/tools/expose-tweaks.js +149 -0
  128. package/server/engine/mcp/tools/generate-image.js +842 -0
  129. package/server/engine/mcp/tools/get-computed-styles.js +164 -0
  130. package/server/engine/mcp/tools/get-pending-changes.js +205 -0
  131. package/server/engine/mcp/tools/h3box-ssh.js +84 -0
  132. package/server/engine/mcp/tools/helpers/acquire-page.js +82 -0
  133. package/server/engine/mcp/tools/helpers/motion-lab.js +554 -0
  134. package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -0
  135. package/server/engine/mcp/tools/helpers/perception-page.js +215 -0
  136. package/server/engine/mcp/tools/helpers/reference-download.js +103 -0
  137. package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -0
  138. package/server/engine/mcp/tools/helpers/rembg.js +273 -0
  139. package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -0
  140. package/server/engine/mcp/tools/highlight.js +68 -0
  141. package/server/engine/mcp/tools/list-pages.js +222 -0
  142. package/server/engine/mcp/tools/lookup-tags.js +59 -0
  143. package/server/engine/mcp/tools/navigate-to-page.js +60 -0
  144. package/server/engine/mcp/tools/organize-board.js +64 -0
  145. package/server/engine/mcp/tools/paint-still.js +434 -0
  146. package/server/engine/mcp/tools/pin-to-board.js +130 -0
  147. package/server/engine/mcp/tools/preview-deck.js +130 -0
  148. package/server/engine/mcp/tools/profile-scroll.js +305 -0
  149. package/server/engine/mcp/tools/publish-site.js +102 -0
  150. package/server/engine/mcp/tools/query-elements.js +195 -0
  151. package/server/engine/mcp/tools/read-board.js +103 -0
  152. package/server/engine/mcp/tools/read-document.js +93 -0
  153. package/server/engine/mcp/tools/read-page.js +264 -0
  154. package/server/engine/mcp/tools/read-tavern-json.js +143 -0
  155. package/server/engine/mcp/tools/record-decision.js +140 -0
  156. package/server/engine/mcp/tools/relate-on-board.js +110 -0
  157. package/server/engine/mcp/tools/remove-background.js +391 -0
  158. package/server/engine/mcp/tools/report-issue.js +115 -0
  159. package/server/engine/mcp/tools/roll-film.js +266 -0
  160. package/server/engine/mcp/tools/screenshot-docx.js +138 -0
  161. package/server/engine/mcp/tools/screenshot-url.js +195 -0
  162. package/server/engine/mcp/tools/screenshot.js +583 -0
  163. package/server/engine/mcp/tools/tier-gate.js +96 -0
  164. package/server/engine/mcp/tools/trace-motion.js +215 -0
  165. package/server/engine/mcp/tools/web-search.js +548 -0
  166. package/server/engine/motion/inventory.js +349 -0
  167. package/server/engine/perception/session.js +235 -0
  168. package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -0
  169. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -0
  170. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -0
  171. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -0
  172. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -0
  173. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -0
  174. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -0
  175. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -0
  176. package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -0
  177. package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -0
  178. package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -0
  179. package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -0
  180. package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -0
  181. package/server/engine/plugins/nodesign/skills/rp-craft/patterns//346/274/224/345/207/272/351/241/265-/346/234/200/345/260/217/345/256/236/347/216/260.html +78 -0
  182. package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -0
  183. package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -0
  184. package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -0
  185. package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -0
  186. package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -0
  187. package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -0
  188. package/server/engine/runs/active-runs.js +698 -0
  189. package/server/engine/runs/live-turn.js +252 -0
  190. package/server/engine/runs/store.js +364 -0
  191. package/server/engine/runs/turn-relay.js +217 -0
  192. package/server/engine/runtime/workspace.js +164 -0
  193. package/server/index.js +235 -0
  194. package/server/lib/artifact-file-path.js +80 -0
  195. package/server/lib/artifact-target.js +284 -0
  196. package/server/lib/asset-refs.js +239 -0
  197. package/server/lib/async-queue.js +104 -0
  198. package/server/lib/auto-relations.js +130 -0
  199. package/server/lib/binding-types.js +60 -0
  200. package/server/lib/board-kind-sizes.js +62 -0
  201. package/server/lib/board-relations.js +133 -0
  202. package/server/lib/browse-proxy.js +180 -0
  203. package/server/lib/canvas-id.js +36 -0
  204. package/server/lib/cover.js +227 -0
  205. package/server/lib/danbooru-tags.js +291 -0
  206. package/server/lib/doc-extract.js +156 -0
  207. package/server/lib/docx/build-from-source.js +260 -0
  208. package/server/lib/docx/build.js +490 -0
  209. package/server/lib/docx/dump-styles.js +287 -0
  210. package/server/lib/docx/fonts/nodesign-cjk.conf +164 -0
  211. package/server/lib/docx/merge-runs.js +129 -0
  212. package/server/lib/docx/numbering.js +218 -0
  213. package/server/lib/docx/order.js +152 -0
  214. package/server/lib/docx/rawzip.js +172 -0
  215. package/server/lib/docx/render.js +92 -0
  216. package/server/lib/docx/text-lint.js +225 -0
  217. package/server/lib/docx/tokens.js +335 -0
  218. package/server/lib/docx/units.js +29 -0
  219. package/server/lib/docx/xml.js +291 -0
  220. package/server/lib/docx-pages.js +187 -0
  221. package/server/lib/export-collect.js +289 -0
  222. package/server/lib/export-package.js +210 -0
  223. package/server/lib/html-srcset.js +145 -0
  224. package/server/lib/image-variant.js +460 -0
  225. package/server/lib/ingress/forward-openai-chat.js +305 -0
  226. package/server/lib/ingress/openai-chat.js +491 -0
  227. package/server/lib/ingress/session-notice.js +66 -0
  228. package/server/lib/ingress/session-routes.js +68 -0
  229. package/server/lib/ingress/slot-probe.js +130 -0
  230. package/server/lib/ingress/upstream-billing.js +52 -0
  231. package/server/lib/ingress/upstream-fail-streak.js +72 -0
  232. package/server/lib/ingress/upstream-truncation.js +48 -0
  233. package/server/lib/issues-store.js +182 -0
  234. package/server/lib/kinds/deck.js +72 -0
  235. package/server/lib/kinds/docx.js +283 -0
  236. package/server/lib/kinds/file-kinds.js +113 -0
  237. package/server/lib/kinds/index.js +202 -0
  238. package/server/lib/kinds/site.js +161 -0
  239. package/server/lib/model-ingress.js +543 -0
  240. package/server/lib/moderation.js +255 -0
  241. package/server/lib/notice-store.js +103 -0
  242. package/server/lib/plugin-install.js +104 -0
  243. package/server/lib/plugin-validator.js +721 -0
  244. package/server/lib/publish-store.js +115 -0
  245. package/server/lib/quick-summary.js +52 -0
  246. package/server/lib/quota.js +270 -0
  247. package/server/lib/rate-window.js +29 -0
  248. package/server/lib/reference-assets.js +92 -0
  249. package/server/lib/region-shot.js +135 -0
  250. package/server/lib/safe-path.js +73 -0
  251. package/server/lib/sdk-session.js +33 -0
  252. package/server/lib/showcase-store.js +92 -0
  253. package/server/lib/site-publish.js +465 -0
  254. package/server/lib/ssrf-guard.js +432 -0
  255. package/server/lib/task-scan.js +158 -0
  256. package/server/lib/tavern-json.js +154 -0
  257. package/server/lib/video-variant.js +237 -0
  258. package/server/lib/workspace-path.js +33 -0
  259. package/server/ops/fix-sdk-musl.mjs +40 -0
  260. package/server/ops/install-macos-fonts.sh +114 -0
  261. package/server/ops/macos-fonts.conf +336 -0
  262. package/server/ops/sandbox-shim/bwrap +57 -0
  263. package/server/projects/assets-summary.js +119 -0
  264. package/server/projects/auto-name.js +53 -0
  265. package/server/projects/board-store.js +640 -0
  266. package/server/projects/move-entry.js +103 -0
  267. package/server/projects/store.js +270 -0
  268. package/server/projects/ui-config.js +54 -0
  269. package/server/projects/workspace-templates.js +53 -0
  270. package/server/projects/workspace.js +1025 -0
  271. package/server/runtime/capabilities.js +153 -0
  272. package/server/runtime/local-config.js +177 -0
  273. package/server/runtime/local-env.js +88 -0
  274. package/server/runtime/platform.js +342 -0
  275. package/server/runtime/profile.js +72 -0
  276. package/server/services/rembg-launcher.js +313 -0
  277. package/server/services/rembg-service.py +334 -0
  278. package/server/shared/README.md +25 -0
  279. package/server/shared/deck.js +54 -0
  280. package/server/shared/time.js +49 -0
  281. package/server/style-pipeline/README.md +53 -0
  282. package/server/ws/broker.js +36 -0
  283. package/server/ws/browse-channel.js +177 -0
  284. package/server/ws/index.js +386 -0
  285. package/web/README.md +89 -0
  286. package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +11 -0
  287. package/web/dist/assets/DeckWindow-4G0xX7P3.js +16 -0
  288. package/web/dist/assets/DocxWindow-BXQsoPkh.js +16 -0
  289. package/web/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  290. package/web/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  291. package/web/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  292. package/web/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  293. package/web/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  294. package/web/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  295. package/web/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  296. package/web/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  297. package/web/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  298. package/web/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  299. package/web/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  300. package/web/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  301. package/web/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  302. package/web/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  303. package/web/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  304. package/web/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  305. package/web/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  306. package/web/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  307. package/web/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  308. package/web/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  309. package/web/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  310. package/web/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  311. package/web/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  312. package/web/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  313. package/web/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  314. package/web/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  315. package/web/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  316. package/web/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  317. package/web/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  318. package/web/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  319. package/web/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  320. package/web/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  321. package/web/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  322. package/web/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  323. package/web/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  324. package/web/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  325. package/web/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  326. package/web/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  327. package/web/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  328. package/web/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  329. package/web/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  330. package/web/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  331. package/web/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  332. package/web/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  333. package/web/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  334. package/web/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  335. package/web/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  336. package/web/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  337. package/web/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  338. package/web/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  339. package/web/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  340. package/web/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  341. package/web/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  342. package/web/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  343. package/web/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  344. package/web/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  345. package/web/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  346. package/web/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  347. package/web/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  348. package/web/dist/assets/SiteWindow-D6eS-2gM.js +18 -0
  349. package/web/dist/assets/again-D8tfgKKO.webp +0 -0
  350. package/web/dist/assets/caveat-nd-CP6HlsNg.woff2 +0 -0
  351. package/web/dist/assets/clock-xJhvfYpS.webp +0 -0
  352. package/web/dist/assets/film-cut-BBKMwH7q.webp +0 -0
  353. package/web/dist/assets/film-still-BZ_DL9Tt.webp +0 -0
  354. package/web/dist/assets/index-C0KUf6Wd.css +1 -0
  355. package/web/dist/assets/index-Cbl3ATvp.js +2125 -0
  356. package/web/dist/assets/ink-desk-BRbxeDsR.webp +0 -0
  357. package/web/dist/assets/ink-night-Bz4BT3XH.webp +0 -0
  358. package/web/dist/assets/ink-portrait-C0DM4V8Q.webp +0 -0
  359. package/web/dist/assets/longcang-regular-C0uvTuKa.woff2 +0 -0
  360. package/web/dist/assets/lxgw-nd-bold-DbRfiTgo.woff2 +0 -0
  361. package/web/dist/assets/lxgw-nd-regular-CpMw9Ehg.woff2 +0 -0
  362. package/web/dist/assets/pending-edit-apply-DTjo2geW.js +31 -0
  363. package/web/dist/assets/question-8H4HSqxh.webp +0 -0
  364. package/web/dist/assets/reject-BaPJqElj.webp +0 -0
  365. package/web/dist/assets/rotate-cw-LMT3x1Zj.js +11 -0
  366. package/web/dist/assets/rp-portrait-CbrGpew2.webp +0 -0
  367. package/web/dist/assets/rp-street-C4jVKjxx.webp +0 -0
  368. package/web/dist/assets/square-dashed-mouse-pointer-CBoAkfhi.js +11 -0
  369. package/web/dist/assets/tangle-DIxHVNuT.webp +0 -0
  370. package/web/dist/assets/thisone-BylZKglV.webp +0 -0
  371. package/web/dist/assets/thumb-DO1wfK1I.webp +0 -0
  372. package/web/dist/index.html +18 -0
@@ -0,0 +1,422 @@
1
+ /**
2
+ * server/engine/browse/capture.js — 从浏览的站上把可复用的东西带回来(2026-08-18)
3
+ *
4
+ * 用户的追加需求:「**此后从浏览器中获得的可复用内容都存到这里**」。关键词是
5
+ * "此后" —— 采集要落盘,不能只进这一轮的上下文然后随会话消失。
6
+ *
7
+ * ## 落哪儿、为什么不新建目录
8
+ *
9
+ * `assets/references/web/`。**不新建"浏览器根"**:`assets/references/` 已经是既有
10
+ * 目录、既有写入方(web-search 下载的参考图落那儿)、既有 skill 锚点。再立一个根就是
11
+ * 「同一件事两个地方」—— 这个仓库最贵的一课。放它下面的 `web/` 子目录,与搜索图
12
+ * 分栏但同根。
13
+ *
14
+ * 出处 sidecar 照抄 `generate-image.js` 已有的约定:`<产物层>/.meta/<name>.json`。
15
+ * **不另发明一套** —— 而且那个约定的读者(assets.js 的清单合并)已经在,扩一处就能
16
+ * 让参考素材也带出处。
17
+ *
18
+ * ## ⭐「可复用内容」比截图宽得多
19
+ *
20
+ * 对做设计这件事,从一个站上带回来最值钱的往往不是位图:**调色板**、**用了哪几个
21
+ * 字体**、某一块的 **CSS 片段**、页面的**结构骨架**。这些能直接落成代码,位图只能
22
+ * 靠眼睛转译。所以 capture 是五种,不是一种。
23
+ *
24
+ * ⚠️ 五种里四种是从 `getComputedStyle` / CDP 直接读出来的(可靠),
25
+ * **`skeleton` 那三个数是启发式**(按布局签名聚类数"节的形状种类"),返回值里明确
26
+ * 标着"粗略,当对照不当真相"—— 它的用处是让 site-craft 的「无聊诊断」从"靠眼睛数"
27
+ * 变成"对参照站有对照数字",不是当成事实。
28
+ */
29
+
30
+ import path from 'node:path';
31
+ import fs from 'node:fs/promises';
32
+
33
+ /**
34
+ * 采集根(工作区相对)。web-search 的搜索图在同一个 references 根下、分栏放。
35
+ *
36
+ * ⭐ **一个参照站一个文件夹**(2026-08-18 下午,用户拍板"按 site 那种范式"):
37
+ * `assets/references/web/<站名>/`。理由是一次采集的五档本来就是**一个单位**
38
+ * (同一个站、同一次、同一个 lookingFor),平铺到十几个文件之后靠文件名前缀
39
+ * 认亲太弱;而目录 = 单位正是站点产物那条既有范式,桌面上一张卡装一个站。
40
+ * 目录里再按**文件名表明类别**(`.screenshot.webp` / `.palette.json` / …)。
41
+ */
42
+ export const CAPTURE_DIR = path.posix.join('assets', 'references', 'web');
43
+
44
+ /** 站名 → 目录名。去掉 www.、端口里的冒号,别让它在 Windows 上炸 */
45
+ export const siteDirOf = (url) => {
46
+ try {
47
+ const u = new URL(url);
48
+ return slug(`${u.hostname.replace(/^www\./, '')}${u.port ? `-${u.port}` : ''}`);
49
+ } catch { return 'page'; }
50
+ };
51
+
52
+ /**
53
+ * 命中规则 → **真的能抄的 CSS 文本**。
54
+ *
55
+ * 这一档的全部价值是"照抄就能复现那个技术",而包在 json 里的它得先被解开一层
56
+ * 才能用。写成 .css 之后类别由扩展名表明,人和 agent 都能直接读。
57
+ * 顺序 = CDP 给的级联顺序(后面的覆盖前面的),头注释里写清出处。
58
+ */
59
+ function cssText(rules, info, selector) {
60
+ const head = [
61
+ '/* 从参照站采下来的命中规则(按级联顺序,后面的覆盖前面的)',
62
+ ` * 元素:${selector}`,
63
+ ` * 来源:${info.url}`,
64
+ ` * 采于:${new Date().toISOString()}`,
65
+ ' */',
66
+ '',
67
+ ];
68
+ const body = rules.map((r) => {
69
+ const decls = r.declarations.map(d => ` ${d};`).join('\n');
70
+ const rule = `${r.selector} {\n${decls}\n}`;
71
+ return r.media?.length
72
+ ? `@media ${r.media.join(' and ')} {\n${rule.replace(/^/gm, ' ')}\n}`
73
+ : rule;
74
+ });
75
+ return `${head.concat(body).join('\n')}\n`;
76
+ }
77
+
78
+ const slug = (s) => String(s || '')
79
+ .replace(/[^\w一-龥-]+/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '').slice(0, 48) || 'capture';
80
+
81
+ /** 页面里跑的采集器。⚠️ 整段在浏览器上下文里执行,不能引用外面的东西。 */
82
+ /* eslint-disable no-undef */
83
+ function collectorSource() {
84
+ return () => {
85
+ const cs = (el) => (el ? getComputedStyle(el) : null);
86
+ const norm = (c) => {
87
+ if (!c || c === 'rgba(0, 0, 0, 0)' || c === 'transparent') return null;
88
+ return c;
89
+ };
90
+
91
+ // ── 调色板:按"用途"取,不是把图量化 ──
92
+ // 量化位图会给一堆没名字的相近色;从 computed style 取到的是**作者的选择**,
93
+ // 而且自带用途,拿回来能直接用。
94
+ // ⚠️ 每一行显式声明取哪个属性 —— 第一版对每个元素同时报 color 和 background,
95
+ // 于是出现了 `page-bg/color=black` 这种自相矛盾的行(body 的 color 是正文色)。
96
+ const uses = [
97
+ ['page-bg', 'body', 'backgroundColor'],
98
+ ['body-text', 'body', 'color'],
99
+ ['h1', 'h1', 'color'],
100
+ ['h2', 'h2', 'color'],
101
+ ['link', 'a', 'color'],
102
+ ['button-bg', 'button, .btn, [class*=button]', 'backgroundColor'],
103
+ ['button-text', 'button, .btn, [class*=button]', 'color'],
104
+ ['card-bg', 'article, .card, [class*=card]', 'backgroundColor'],
105
+ ['footer-bg', 'footer', 'backgroundColor'],
106
+ ['footer-text', 'footer', 'color'],
107
+ ];
108
+ const palette = [];
109
+ for (const [role, sel, prop] of uses) {
110
+ const el = document.querySelector(sel);
111
+ const s = cs(el);
112
+ const v = s ? norm(s[prop]) : null;
113
+ if (v) palette.push({ role, value: v });
114
+ }
115
+
116
+ // ── 字体:字族 + 实际用到的字重/字号(光有字族没法照做)──
117
+ const fontOfEl = (el, label) => {
118
+ const s = cs(el);
119
+ if (!s) return null;
120
+ return {
121
+ role: label,
122
+ family: s.fontFamily,
123
+ size: s.fontSize,
124
+ weight: s.fontWeight,
125
+ lineHeight: s.lineHeight,
126
+ letterSpacing: s.letterSpacing,
127
+ };
128
+ };
129
+ const fontOf = (sel, label) => fontOfEl(document.querySelector(sel), label);
130
+
131
+ // ⚠️ 「正文」不能拿**第一个 `<p>`**(agent 上报逮到:真站上首个 `<p>` 常是
132
+ // 顶部公告条/导航里的一句话,字号字族都不是正文的,量出来整份报告跟着错)。
133
+ // 改成找**最大的可见叶子文字块**:字多、够宽、且没有块级孩子替它装内容。
134
+ // 附 selector + 文字样本 —— 量具的读数要能核对出处,不然错了没人看得见。
135
+ const pickParagraph = () => {
136
+ let best = null; let bestScore = 0;
137
+ for (const el of document.querySelectorAll('p, li, dd, blockquote')) {
138
+ if (el.querySelector('p, div, ul, ol, table, blockquote, pre, h1, h2, h3')) continue;
139
+ const r = el.getBoundingClientRect();
140
+ const s = cs(el);
141
+ if (!s || s.display === 'none' || s.visibility === 'hidden' || r.width < 120) continue;
142
+ const text = (el.textContent || '').trim();
143
+ if (text.length < 40) continue;
144
+ // 长度封顶:一段 5000 字的法律条款不该只因为长就赢过真正的正文版式
145
+ const score = Math.min(text.length, 600) * Math.min(r.width, window.innerWidth);
146
+ if (score > bestScore) { bestScore = score; best = el; }
147
+ }
148
+ return best || document.querySelector('p');
149
+ };
150
+ const selectorOf = (el) => {
151
+ const tag = el.tagName.toLowerCase();
152
+ if (el.id) return `${tag}#${el.id}`;
153
+ const cls = [...el.classList].slice(0, 2).join('.');
154
+ return cls ? `${tag}.${cls}` : tag;
155
+ };
156
+ const pEl = pickParagraph();
157
+ const pFont = fontOfEl(pEl, 'paragraph');
158
+ if (pFont) {
159
+ pFont.selector = selectorOf(pEl);
160
+ pFont.sample = (pEl.textContent || '').trim().replace(/\s+/g, ' ').slice(0, 60);
161
+ }
162
+ const fonts = [
163
+ fontOf('body', 'body'), fontOf('h1', 'h1'), fontOf('h2', 'h2'),
164
+ pFont, fontOf('code, pre', 'mono'),
165
+ ].filter(Boolean);
166
+
167
+ // ── 结构骨架(⚠️ 启发式)──
168
+ //
169
+ // ⚠️ 第一版按标签选(`main > *, body > section` …),在第一个真站(MDN)上就
170
+ // **数出 0 节**。一个在真站上读 0 的指标比没有更坏 —— 它会告诉 agent
171
+ // 「这个参照站有 0 种形状」。所以改成按**视觉横带**找:宽度接近视口、够高、
172
+ // 而且内部不再包含更小的带(取最内层)。人看一个页面看到的"一节"就是这个。
173
+ const vw = window.innerWidth;
174
+ const cand = [...document.querySelectorAll('body *')].filter((el) => {
175
+ const r = el.getBoundingClientRect();
176
+ // 高度门槛跟视口挂钩而不是写死 120 —— 真站上一条 104px 的 stats strip
177
+ // 就这么整条消失了(它确实是"一节",人一眼就看见)
178
+ if (r.width < vw * 0.7 || r.height < Math.max(72, window.innerHeight * 0.08)) return false;
179
+ const s = cs(el);
180
+ return s.display !== 'none' && s.visibility !== 'hidden';
181
+ });
182
+ // 取最内层的带:一个候选如果还含着别的候选,它是容器不是"一节"
183
+ const candSet = new Set(cand);
184
+ const sections = cand.filter(el => ![...el.querySelectorAll('*')].some(d => candSet.has(d)));
185
+
186
+ // 「节的形状种类」按布局签名聚类:同签名的节读起来是"同一种节奏"。
187
+ // 这是 site-craft「用户说无聊」那节要的第二个数。
188
+ // 签名要**粗**。第一版把图/标题/文字块数都算进去(各封顶 3),结果在三个真站上
189
+ // `形状种类 === 节数` —— 每节都独一无二,那第二个数就不携带任何信息。
190
+ // 现在只留三个粗轴:排布方式 / 有没有图 / 文字密度档。同一种节奏才会撞在一起。
191
+ const sig = (el) => {
192
+ const s = cs(el);
193
+ const layout = s.gridTemplateColumns !== 'none' ? 'grid'
194
+ : (s.display.includes('flex') ? (s.flexDirection.startsWith('row') ? 'row' : 'col') : 'block');
195
+ // ⚠️ `querySelector` **只找后代**。最内层的那条带很可能**自己就是**那张
196
+ // 全幅 hero 图/视频 —— 于是它被标成 noimg,图 hero 和纯文字 hero 聚成一类。
197
+ const IMGISH = 'img, picture, video, svg';
198
+ const hasImg = (el.matches(IMGISH) || el.querySelector(IMGISH)) ? 'img' : 'noimg';
199
+ const chars = (el.textContent || '').trim().length;
200
+ const density = chars < 120 ? 'bare' : (chars < 800 ? 'some' : 'lots');
201
+ return `${layout}|${hasImg}|${density}`;
202
+ };
203
+ const shapes = new Map();
204
+ for (const el of sections) {
205
+ const k = sig(el);
206
+ shapes.set(k, (shapes.get(k) || 0) + 1);
207
+ }
208
+ // 交互重心:值得动手的东西。
209
+ // ⚠️ 三个坑,都是审查在真站上撞出来的:
210
+ // ① `[class*=tab]` 是子串匹配 → `.tabular-figures`、`.tabbed-wrapper` 全中;
211
+ // `:not([class*=table])` 也补不完。改成按 class **token** 比对。
212
+ // ② 一个元素同时命中两条选择器(既 `[role=tab]` 又 `.tab-item`)会被数两遍。
213
+ // 改用 Set 存元素再取 size。
214
+ // ③ 裸 `[aria-expanded]` 把汉堡菜单、下拉导航全算成"手风琴"。它们确实是
215
+ // disclosure,但不是"这页的交互重心"。单列一档,别混进 accordions。
216
+ const clsTokens = (el) => (typeof el.className === 'string'
217
+ ? el.className.toLowerCase().split(/[^a-z0-9]+/).filter(Boolean) : []);
218
+ const hasTok = (el, names) => clsTokens(el).some(t => names.has(t));
219
+ const setOf = (sel, pred) => {
220
+ const out = new Set();
221
+ for (const el of document.querySelectorAll(sel)) { if (!pred || pred(el)) out.add(el); }
222
+ return out;
223
+ };
224
+ const TAB_TOK = new Set(['tab', 'tabs']);
225
+ const ACC_TOK = new Set(['accordion', 'accordions', 'collapse', 'collapsible', 'disclosure']);
226
+ const interactive = {
227
+ forms: setOf('form').size,
228
+ tabs: setOf('[role=tab], [role=tablist], [class]', el => el.getAttribute('role') === 'tab'
229
+ || el.getAttribute('role') === 'tablist' || hasTok(el, TAB_TOK)).size,
230
+ accordions: setOf('details, [class]', el => el.tagName === 'DETAILS' || hasTok(el, ACC_TOK)).size,
231
+ disclosures: setOf('[aria-expanded]').size, // 多半是导航开关,单独列
232
+ draggables: setOf('[draggable=true], input[type=range]').size,
233
+ videos: setOf('video').size,
234
+ };
235
+
236
+ return {
237
+ url: location.href,
238
+ title: document.title,
239
+ viewportWidth: window.innerWidth,
240
+ palette,
241
+ fonts,
242
+ skeleton: {
243
+ sectionCount: sections.length,
244
+ shapeKinds: shapes.size,
245
+ shapes: [...shapes.entries()].map(([k, n]) => ({ signature: k, count: n })),
246
+ interactive,
247
+ // disclosures 不计进"交互点":它多半是汉堡菜单/下拉导航,
248
+ // 每个站都有,加进去只会把这个数字变成噪音
249
+ interactivePoints: ['forms', 'tabs', 'accordions', 'draggables', 'videos']
250
+ .reduce((a, k) => a + interactive[k], 0),
251
+ _note: '启发式:节的形状种类是按布局签名聚类数出来的,当对照数字用,别当真相。'
252
+ + '同一页做 A/B 最可靠;跨页比较受各家标记方式影响,看数量级别抠个位数。',
253
+ },
254
+ };
255
+ };
256
+ }
257
+ /* eslint-enable no-undef */
258
+
259
+ /**
260
+ * 采一次。
261
+ * @param {object} p
262
+ * @param {import('playwright').Page} p.page
263
+ * @param {string} p.workspaceRoot
264
+ * @param {string[]} p.kinds screenshot / palette / fonts / css / skeleton
265
+ * @param {string} [p.name] 文件名主干(不给就按站名+时间)
266
+ * @param {string} [p.lookingFor] agent 当时在找什么 —— 三天后没有这句就是一堆不知道哪来的图
267
+ * @param {string} [p.selector] css 那一种要指一块
268
+ * @param {object} [p.ids] {sessionId, runId}
269
+ * @param {(buf: Buffer) => Promise<{data: string, mimeType: string, note?: string}>} [p.normalize]
270
+ * @returns {Promise<{files: Array<{rel: string, kind: string, bytes: number}>, data: object}>}
271
+ */
272
+ export async function capture({
273
+ page, workspaceRoot, kinds, name, lookingFor, selector, ids = {}, normalize,
274
+ }) {
275
+ const info = await page.evaluate(`(${collectorSource().toString()})()`);
276
+ // 一站一文件夹(见 CAPTURE_DIR 的注释)。子目录相对工作区根的前缀,
277
+ // 落盘和返回给 agent 的 rel 都从它拼 —— 只有一处拼法。
278
+ const siteDir = path.posix.join(CAPTURE_DIR, siteDirOf(info.url));
279
+ const dir = path.join(workspaceRoot, siteDir);
280
+ const metaDir = path.join(dir, '.meta');
281
+ await fs.mkdir(metaDir, { recursive: true });
282
+
283
+ // 文件名主干不再重复站名(目录已经说了是哪个站):给的名字优先,
284
+ // 否则用页面在站内的位置 + 时间 —— 同一个站逛三层会采三份,得分得开。
285
+ const pageSlug = (() => {
286
+ try {
287
+ const segs = new URL(info.url).pathname.split('/').filter(Boolean);
288
+ return segs.length ? slug(segs.slice(-2).join('-')) : '首页';
289
+ } catch { return '页面'; }
290
+ })();
291
+ // `0818-2253`(月日-时分,+8 时区)。原来是 `.slice(5,16)` 直接把 ISO 的
292
+ // `08-18T22:53` 去掉分隔符,读出来是 `08-182253` —— 文件名是给人看的
293
+ const iso = new Date(Date.now() + 8 * 3600e3).toISOString();
294
+ const stamp = `${iso.slice(5, 7)}${iso.slice(8, 10)}-${iso.slice(11, 13)}${iso.slice(14, 16)}`;
295
+ const stem = slug(name || `${pageSlug}-${stamp}`);
296
+
297
+ const files = [];
298
+ const failed = [];
299
+
300
+ /** 出处 sidecar 的键 = 文件名去掉最后一节扩展名(`listReferences` 用的是同一条规则) */
301
+ const metaStemOf = (fileName) => fileName.replace(/\.[^.]+$/, '');
302
+
303
+ /**
304
+ * 写一份素材 + 它自己的出处。
305
+ *
306
+ * ⭐ **类别写在文件名里**(2026-08-18 用户拍板):`.screenshot.webp` /
307
+ * `.palette.json` / `.fonts.json` / `.skeleton.json` / `.css`。这跟站点产物是
308
+ * 同一条规矩 —— 形态由文件名表明,不用打开文件才知道里面是什么。
309
+ * 在这之前调色板/字体/结构/CSS **全挤在一个 `<名>.json` 里**,抽屉里看到的
310
+ * 是一个什么都可能的 json,agent 下个会话想复用得先读一遍才知道有没有它要的。
311
+ *
312
+ * 出处**一份文件一份**,不是一次采集一份:每份素材要能单独被认出来
313
+ * (三天后 `assets/references/web/` 里的东西是靠自己说明自己的)。
314
+ */
315
+ const write = async (suffix, body, kind) => {
316
+ const fileName = `${stem}${suffix}`;
317
+ const rel = path.posix.join(siteDir, fileName);
318
+ await fs.writeFile(path.join(workspaceRoot, rel), body);
319
+ files.push({ rel, kind, bytes: Buffer.byteLength(body) });
320
+ await fs.writeFile(path.join(metaDir, `${metaStemOf(fileName)}.json`), `${JSON.stringify({
321
+ kind,
322
+ sourceUrl: info.url,
323
+ pageTitle: info.title,
324
+ capturedAt: new Date().toISOString(),
325
+ viewportWidth: info.viewportWidth,
326
+ lookingFor: lookingFor || null,
327
+ selector: selector || null,
328
+ sessionId: ids.sessionId ?? null,
329
+ runId: ids.runId ?? null,
330
+ }, null, 2)}\n`, 'utf8');
331
+ return rel;
332
+ };
333
+
334
+ /** 一档一个 json,各自带齐来历 —— 拿单独一份出来看也知道它是什么 */
335
+ const writeJson = (suffix, payload, kind) => write(suffix, `${JSON.stringify({
336
+ source: info.url, title: info.title, kind, ...payload,
337
+ }, null, 2)}\n`, kind);
338
+
339
+ // ⚠️ **每一种单独 try**。第一版是一路顺着写下来的,真跑时 `selector: 'main'` 在
340
+ // MDN 上等了 30 秒超时 → 整个函数抛错 → 调色板/字体/结构/CSS 明明都已经采到了
341
+ // (evaluate 跑在最前面),连出处 sidecar 都没写下去,**一次白采**。
342
+ // 部分失败不该丢掉已经成功的部分。
343
+ if (kinds.includes('screenshot')) {
344
+ try {
345
+ let raw;
346
+ if (selector) {
347
+ const loc = page.locator(selector).first();
348
+ // 短超时快速失败:一个选不中的选择器不该让整次采集卡 30 秒
349
+ await loc.waitFor({ state: 'visible', timeout: 6000 });
350
+ raw = await loc.screenshot({ type: 'png', timeout: 8000 });
351
+ } else {
352
+ raw = await page.screenshot({ type: 'png', fullPage: false });
353
+ }
354
+ // 走跟感知层同一条归一化(webp、缩到 API 反正会缩的规格)—— 参考图不需要
355
+ // 无损,而 76MB 的 references 目录已经是这个项目的既有账
356
+ const shot = normalize ? await normalize(raw) : null;
357
+ const buf = shot ? Buffer.from(shot.data, 'base64') : raw;
358
+ await write(shot ? '.screenshot.webp' : '.screenshot.png', buf, 'screenshot');
359
+ } catch (err) {
360
+ failed.push(`screenshot: ${err.message.split('\n')[0]}`
361
+ + (selector ? `(选择器 ${selector} 可能选不中或不可见 —— 先用 browser_read 看页面上有什么)` : ''));
362
+ }
363
+ }
364
+
365
+ const bundle = {};
366
+ if (kinds.includes('palette')) bundle.palette = info.palette;
367
+ if (kinds.includes('fonts')) bundle.fonts = info.fonts;
368
+ if (kinds.includes('skeleton')) bundle.skeleton = info.skeleton;
369
+ if (kinds.includes('css') && selector) {
370
+ // 复用 explain_style 那条 CDP 通路:拿这个元素**命中的全部规则**,
371
+ // 而不是 computed 值 —— 前者能照抄,后者只是结果
372
+ try {
373
+ const cdp = await page.context().newCDPSession(page);
374
+ await cdp.send('DOM.enable'); await cdp.send('CSS.enable');
375
+ const { root } = await cdp.send('DOM.getDocument', { depth: -1 });
376
+ const { nodeId } = await cdp.send('DOM.querySelector', { nodeId: root.nodeId, selector });
377
+ if (!nodeId) {
378
+ // 原来这里没有 else:选择器选不中就静默少一份产物,agent 看不到
379
+ failed.push(`css: 选择器 ${selector} 在页面上选不中(先 browser_read 看页面上有什么)`);
380
+ } else {
381
+ const m = await cdp.send('CSS.getMatchedStylesForNode', { nodeId });
382
+ bundle.css = (m.matchedCSSRules || []).map(r => ({
383
+ selector: r.rule?.selectorList?.text,
384
+ media: (r.rule?.media || []).map(x => x.text).filter(Boolean),
385
+ declarations: (r.rule?.style?.cssProperties || [])
386
+ .filter(d => !d.disabled && d.value != null && d.range) // 有 range = 作者真写了这条
387
+ .map(d => `${d.name}: ${d.value}${d.important ? ' !important' : ''}`),
388
+ })).filter(r => r.declarations.length);
389
+ }
390
+ } catch (err) {
391
+ // ⚠️ 原来只写进磁盘 JSON 的 cssError,返回给 agent 的文本里**一个字都没有**
392
+ // (打印分支只在 `r.data.css` 存在时才跑)→ css 这一档完全静默失败
393
+ bundle.cssError = err.message;
394
+ failed.push(`css: ${err.message.split('\n')[0]}`);
395
+ }
396
+ }
397
+ // motion 档(08-21):引擎在 engine/motion/inventory.js(对页面无知,产物会话也能用)。
398
+ // 排在截图之后 —— 它会真滚一遍页面(做完滚回顶),别把滚动痕迹截进 screenshot 档。
399
+ if (kinds.includes('motion')) {
400
+ try {
401
+ const { collectMotionInventory } = await import('../motion/inventory.js');
402
+ bundle.motion = await collectMotionInventory(page);
403
+ } catch (err) {
404
+ failed.push(`motion: ${err.message.split('\n')[0]}`);
405
+ }
406
+ }
407
+ // 一档一文件。写盘也各自 try —— 一档写不下去不该把别的档带走
408
+ // (跟上面「每一种单独 try」同一条理由,只是这次是磁盘不是页面)。
409
+ for (const [key, suffix] of [['palette', '.palette.json'], ['fonts', '.fonts.json'], ['skeleton', '.skeleton.json'], ['motion', '.motion.json']]) {
410
+ if (bundle[key] == null) continue;
411
+ try { await writeJson(suffix, { [key]: bundle[key] }, key); }
412
+ catch (err) { failed.push(`${key}: 写盘失败 ${err.message}`); }
413
+ }
414
+ if (bundle.css) {
415
+ // ⭐ CSS 落成**真的 CSS**,不是包在 json 里的字符串数组:这一档的全部价值
416
+ // 就是"能照抄",而 json 里的它要先被解开一层才能用。扩展名本身就是类别。
417
+ try { await write('.css', cssText(bundle.css, info, selector), 'css'); }
418
+ catch (err) { failed.push(`css: 写盘失败 ${err.message}`); }
419
+ }
420
+
421
+ return { files, failed, data: { ...bundle, url: info.url, title: info.title } };
422
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }