@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,548 +1,548 @@
1
- /**
2
- * mcp/tools/web-search.js — web_search MCP tool
3
- *
4
- * 多 provider 联网搜索(移植自 ~/.deskclaw/skills/deskclaw-search-pro/scripts/search.py)。
5
- * Provider 优先级按 query 语言自动路由:CJK → baidu 优先;非 CJK → tavily 优先;
6
- * zhipu 配额稀缺永远最后。zero 外部依赖(仅 Node 内置 fetch + URL)。
7
- *
8
- * key 配置(.env,至少配一个):
9
- * NODESIGN_BAIDU_QIANFAN_KEY=bce-v3/...
10
- * NODESIGN_TAVILY_KEY=tvly-...
11
- * NODESIGN_EXA_KEY=...
12
- * NODESIGN_ZHIPU_KEY=...
13
- *
14
- * agent 视角的工具名:mcp__nodesign__web_search
15
- *
16
- * 返回 markdown(top N 条 title + source + snippet + url),让 agent 直接消费,
17
- * 不暴露 raw JSON 避免 context 灌满。
18
- *
19
- * # 调用上限(agent 应自律,SKILL.md 强约束)
20
- * - baidu 中文:≤2 次 / turn(snippet 即正文,单次通常够)
21
- * - tavily:≤3-4 次(摘要浅 100-200 字,需多 query 三角验证)
22
- * - exa:≤1-2 次(content 字段 2000+ 字完整正文,3 次以上爆 context)
23
- * - zhipu:≤1 次(包配额稀缺)
24
- * - baidu 英文:禁用(实测严重跑题)
25
- */
26
-
27
- import path from 'node:path';
28
- import fs from 'node:fs/promises';
29
- import crypto from 'node:crypto';
30
- import { downloadReferenceImages } from './helpers/reference-download.js';
31
- import { tool } from '@anthropic-ai/claude-agent-sdk';
32
- import { z } from 'zod';
33
-
34
- const PROVIDERS = {
35
- tavily: { keyEnv: 'NODESIGN_TAVILY_KEY' },
36
- exa: { keyEnv: 'NODESIGN_EXA_KEY' },
37
- baidu: { keyEnv: 'NODESIGN_BAIDU_QIANFAN_KEY' },
38
- zhipu: { keyEnv: 'NODESIGN_ZHIPU_KEY' },
39
- };
40
-
41
- const PRIORITY_CJK = ['baidu', 'tavily', 'exa', 'zhipu'];
42
- const PRIORITY_NON_CJK = ['tavily', 'exa', 'baidu', 'zhipu'];
43
-
44
- function looksChinese(text) {
45
- // U+4E00-U+9FFF Unified CJK
46
- return /[一-鿿]/.test(text || '');
47
- }
48
-
49
- function getKey(providerId) {
50
- const env = PROVIDERS[providerId]?.keyEnv;
51
- return env ? (process.env[env] || '') : '';
52
- }
53
-
54
- function autoSelectProvider(query) {
55
- const order = looksChinese(query) ? PRIORITY_CJK : PRIORITY_NON_CJK;
56
- for (const id of order) if (getKey(id)) return id;
57
- return null;
58
- }
59
-
60
- function domainOf(url) {
61
- try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; }
62
- }
63
-
64
- // ── adapters ──
65
-
66
- async function searchTavily(query, key, n, { includeImages = false } = {}) {
67
- const body = {
68
- query,
69
- search_depth: includeImages ? 'advanced' : 'basic',
70
- topic: 'general',
71
- max_results: n,
72
- include_answer: false,
73
- include_raw_content: false,
74
- };
75
- if (includeImages) {
76
- body.include_images = true;
77
- body.include_image_descriptions = true;
78
- body.include_favicon = true;
79
- }
80
- const res = await fetch('https://api.tavily.com/search', {
81
- method: 'POST',
82
- headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
83
- body: JSON.stringify(body),
84
- });
85
- if (!res.ok) throw new ProviderError('tavily', res.status, await res.text());
86
- const raw = await res.json();
87
- const hits = (raw.results || []).map(r => ({
88
- title: r.title || '',
89
- url: r.url || '',
90
- snippet: r.content || '',
91
- source: domainOf(r.url || ''),
92
- publishedAt: r.published_date || '',
93
- }));
94
- const images = includeImages
95
- ? (raw.images || [])
96
- .filter(i => i?.url)
97
- .map(i => ({
98
- url: i.url,
99
- description: i.description || '',
100
- title: i.title || '',
101
- }))
102
- : [];
103
- return { hits, images };
104
- }
105
-
106
- async function searchExa(query, key, n, { includeImages = false } = {}) {
107
- // Exa 顶层是 camelCase(numResults / maxCharacters / imageLinks),
108
- // 旧字段 num_results 仍兼容,新功能用官方约定的 camelCase。
109
- const contents = {
110
- highlights: { maxCharacters: 4000 },
111
- };
112
- if (includeImages) {
113
- contents.extras = { imageLinks: Math.max(3, n) };
114
- }
115
- const res = await fetch('https://api.exa.ai/search', {
116
- method: 'POST',
117
- headers: { 'x-api-key': key, 'Content-Type': 'application/json' },
118
- body: JSON.stringify({
119
- query,
120
- type: 'auto',
121
- numResults: n,
122
- contents,
123
- }),
124
- });
125
- if (!res.ok) throw new ProviderError('exa', res.status, await res.text());
126
- const raw = await res.json();
127
- const hits = (raw.results || []).map(r => {
128
- const highlights = r.highlights || [];
129
- return {
130
- title: r.title || '',
131
- url: r.url || '',
132
- snippet: highlights[0] || r.text || '',
133
- source: domainOf(r.url || ''),
134
- publishedAt: r.publishedDate || '',
135
- };
136
- });
137
- // images: results[].image(页面代表图)+ results[].extras.imageLinks(页面内抽图),dedupe by url
138
- const images = [];
139
- if (includeImages) {
140
- const seen = new Set();
141
- for (const r of (raw.results || [])) {
142
- const parentTitle = r.title || '';
143
- // 1) 页面代表图(每条最多 1 张)
144
- if (r.image && !seen.has(r.image)) {
145
- seen.add(r.image);
146
- images.push({ url: r.image, description: parentTitle, title: parentTitle });
147
- }
148
- // 2) 页面内抽到的图片链接(无描述,用 parent title 兜底)
149
- const links = r.extras?.imageLinks || [];
150
- for (const link of links) {
151
- if (!link || seen.has(link)) continue;
152
- seen.add(link);
153
- images.push({ url: link, description: parentTitle, title: parentTitle });
154
- }
155
- }
156
- }
157
- return { hits, images };
158
- }
159
-
160
- async function searchBaidu(query, key, n, { includeImages = false } = {}) {
161
- // Baidu Qianfan content 字段硬上限 72 字符
162
- const truncated = query.length > 72 ? query.slice(0, 72) : query;
163
- const body = {
164
- messages: [{ role: 'user', content: truncated }],
165
- };
166
- if (includeImages) {
167
- // 默认 image.top_k=0;必须显式声明才返图。同时保留 web 模态拿 hits。
168
- body.search_source = 'baidu_search_v2';
169
- body.resource_type_filter = [
170
- { type: 'web', top_k: n },
171
- { type: 'image', top_k: Math.min(30, Math.max(n, 10)) },
172
- ];
173
- }
174
- const res = await fetch('https://qianfan.baidubce.com/v2/ai_search/web_search', {
175
- method: 'POST',
176
- headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
177
- body: JSON.stringify(body),
178
- });
179
- if (!res.ok) throw new ProviderError('baidu', res.status, await res.text());
180
- const raw = await res.json();
181
- let results = raw.search_results || raw.results || [];
182
- if (!Array.isArray(results) || results.length === 0) {
183
- results = raw.references || [];
184
- }
185
- // 把 web 与 image 两类拆开:image 模态是 type==='image'
186
- const webRefs = results.filter(r => (r?.type || 'web') !== 'image');
187
- const imageRefs = results.filter(r => r?.type === 'image');
188
-
189
- const hits = webRefs.slice(0, n).map(r => ({
190
- title: r.title || '',
191
- url: r.url || r.link || '',
192
- snippet: r.content || r.abstract || r.segment_text || '',
193
- source: r.source || domainOf(r.url || r.link || ''),
194
- publishedAt: r.publish_time || '',
195
- }));
196
-
197
- const images = [];
198
- if (includeImages) {
199
- const seen = new Set();
200
- // 1) 图片模态条目
201
- for (const r of imageRefs) {
202
- const u = r.image?.url || r.url;
203
- if (!u || seen.has(u)) continue;
204
- seen.add(u);
205
- images.push({
206
- url: u,
207
- description: r.title || '',
208
- title: r.title || '',
209
- });
210
- }
211
- // 2) 网页条目里夹带的相关图(web_extensions.images)
212
- for (const r of webRefs) {
213
- const arr = r.web_extensions?.images || [];
214
- for (const img of arr) {
215
- const u = img?.url;
216
- if (!u || seen.has(u)) continue;
217
- seen.add(u);
218
- images.push({
219
- url: u,
220
- description: r.title || '',
221
- title: r.title || '',
222
- });
223
- }
224
- }
225
- }
226
- return { hits, images };
227
- }
228
-
229
- async function searchZhipu(query, key, n) {
230
- // 用 dedicated tools/web_search endpoint(比 chat completions 路径更直接)
231
- const res = await fetch('https://open.bigmodel.cn/api/paas/v4/tools/web_search', {
232
- method: 'POST',
233
- headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
234
- body: JSON.stringify({
235
- search_engine: 'search_pro',
236
- search_query: query,
237
- count: n,
238
- }),
239
- });
240
- if (!res.ok) throw new ProviderError('zhipu', res.status, await res.text());
241
- const raw = await res.json();
242
- const hits = (raw.search_result || []).slice(0, n).map(r => ({
243
- title: r.title || '',
244
- url: r.link || r.url || '',
245
- snippet: r.content || '',
246
- source: r.media || domainOf(r.link || r.url || ''),
247
- publishedAt: '',
248
- }));
249
- return { hits, images: [] };
250
- }
251
-
252
- const ADAPTERS = {
253
- tavily: searchTavily,
254
- exa: searchExa,
255
- baidu: searchBaidu,
256
- zhipu: searchZhipu,
257
- };
258
-
259
- class ProviderError extends Error {
260
- constructor(provider, code, body) {
261
- super(`[${provider}] HTTP ${code}: ${String(body || '').slice(0, 300)}`);
262
- this.provider = provider;
263
- this.code = code;
264
- }
265
- }
266
-
267
- function formatMarkdown(query, provider, hits, { images = [] } = {}) {
268
- const lines = [`## Search results (${provider}, ${hits.length} hits)`, '', `> Query: ${query}`, ''];
269
- if (hits.length === 0) {
270
- lines.push('No text results found.');
271
- } else {
272
- hits.forEach((h, i) => {
273
- const src = h.source ? ` — ${h.source}` : '';
274
- const pub = h.publishedAt ? ` (${h.publishedAt})` : '';
275
- lines.push(`${i + 1}. **${h.title || 'Untitled'}**${src}${pub}`);
276
- if (h.snippet) {
277
- // 缩进显示,限单条 snippet 800 字符防爆 context
278
- const snippet = h.snippet.length > 800 ? h.snippet.slice(0, 800) + '…' : h.snippet;
279
- for (const line of snippet.split('\n')) lines.push(` ${line}`);
280
- }
281
- if (h.url) lines.push(` ${h.url}`);
282
- lines.push('');
283
- });
284
- }
285
-
286
- if (images.length > 0) {
287
- lines.push(`## Reference images (downloaded, ${images.length})`, '');
288
- lines.push(
289
- '> The actual pixel content of each image follows this text as inline '
290
- + 'image content blocks (in the same numbered order). Vision-check them '
291
- + 'directly to pick the best 1-2 candidates.',
292
- '> Then pass the chosen `local_path` value(s) as `referenceImages[]` to '
293
- + '`mcp__nodesign__generate_image` (paths are already inside the workspace).',
294
- '',
295
- );
296
- images.forEach((img, i) => {
297
- const desc = img.description || img.title || '(no description)';
298
- const sizeKB = (img.sizeBytes / 1024).toFixed(0);
299
- lines.push(`${i + 1}. ${desc}`);
300
- lines.push(` local_path: ${img.relPath}`);
301
- lines.push(` size: ${sizeKB} KB, mime: ${img.mimeType}`);
302
- lines.push(` source: ${domainOf(img.url)}`);
303
- lines.push(` url: ${img.url}`);
304
- lines.push('');
305
- });
306
- }
307
- return lines.join('\n');
308
- }
309
-
310
- /**
311
- * @param {object} deps
312
- * @param {string} [deps.workspaceRoot] agent cwd(include_images 下载落档需要)
313
- * @param {string} [deps.sharedRoot] project shared/,存在时优先落档
314
- * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
315
- */
316
- export function makeWebSearchTool({ workspaceRoot, sharedRoot, ctx } = {}) {
317
- return tool(
318
- 'web_search',
319
- `Search the web via tavily / exa / baidu (Qianfan) / zhipu providers. Auto-routes by query language: CJK
320
- queries prefer baidu, English queries prefer tavily.
321
-
322
- Use this tool when:
323
- - You need current information (latest design trends, library docs, recent events)
324
- - You need to verify a fact or find a citation
325
- - You need to find a URL to then GO LOOK AT: for anything visual, follow up with browser_navigate and browse the site's inner pages — snippets carry no layout, type or colour. (Or web_fetch for text.)
326
- - You need REFERENCE IMAGES for generate_image (set include_images=true; see below)
327
-
328
- DO NOT:
329
- - Run more than 2-3 queries per turn (context bloat)
330
- - Use baidu for English queries (returns severely off-topic results)
331
- - Re-issue the same query to retry — change the wording instead
332
-
333
- For Chinese queries baidu's snippet field already contains 500-3000 chars of body text;
334
- you usually don't need to web_fetch the URL afterwards. Tavily snippets are short (100-200
335
- chars) and may need a follow-up fetch.
336
-
337
- Add a year hint (e.g., "2025 2026") to the query — search engines especially Chinese ones
338
- often return stale results without it.
339
-
340
- # include_images mode (reference imagery for generate_image)
341
-
342
- When you need real-world subject anchor before generate_image (product shots, scenes,
343
- iconic landmarks, brand visuals, etc.), pass include_images=true. Behavior:
344
- - Auto-routes by language (excluding zhipu, which has no image support):
345
- CJK query → baidu (native CJK image search; no translation)
346
- EN query → tavily (richest image descriptions, ~100% covered)
347
- fallback → exa (page-representative image + page-internal imageLinks)
348
- You can override with provider='tavily'|'exa'|'baidu'.
349
- - When provider lands on tavily/exa AND query is CJK, the tool auto-translates
350
- the query to English first (Tavily/Exa image descriptions are dramatically
351
- richer in English; baidu doesn't need translation).
352
- - Top-N image hits are downloaded into <workspace>/assets/references/ref-<hash>.<ext>
353
- and listed in the markdown output as "Reference images (downloaded, N)".
354
- - The CallToolResult also returns each downloaded image as an inline image
355
- content block (in the same numbered order as the markdown), so you can
356
- vision-check the candidates immediately without calling Read.
357
- - Each entry has a 'local_path' field — pass that path directly into
358
- mcp__nodesign__generate_image referenceImages[] (NOT the http url; the gen tool
359
- only accepts workspace-relative paths).
360
- - Filtered: 5KB ≤ size ≤ 8MB, only png/jpg/webp/gif content-types accepted.
361
- - Per-provider quirks:
362
- tavily: top-level images[] — clean, on-topic, every image has a description
363
- exa: page-rep image + extras.imageLinks — many small/decorative URLs
364
- survive the size filter, expect more variance
365
- baidu: native CJK + image size/ratio/format filter via search_filter (we
366
- don't expose those yet); image entries lack descriptions, we use
367
- the parent reference title as fallback caption
368
-
369
- Suggested flow for image-led pages:
370
- 1. user picks theme → 2. web_search(query, include_images=true) → 3. pick the
371
- best-matching local_path → 4. generate_image(prompt, referenceImages=[that path])`,
372
- {
373
- query: z.string().min(2).max(500).describe('Search query. Add year hints (2025/2026) for time-sensitive results. Baidu truncates content to 72 chars — keep CJK queries short.'),
374
- provider: z
375
- .enum(['tavily', 'exa', 'baidu', 'zhipu', 'auto'])
376
- .optional()
377
- .describe('Force a provider; default "auto" routes by query language. include_images=true auto-routes among tavily/exa/baidu (zhipu rejected, no image support).'),
378
- count: z
379
- .number()
380
- .int()
381
- .min(1)
382
- .max(10)
383
- .optional()
384
- .describe('Max text results to return (default 5). Image count follows the same value.'),
385
- include_images: z
386
- .boolean()
387
- .optional()
388
- .describe('Returns top-N reference images downloaded into assets/references/ for use as generate_image referenceImages. Auto-routes CJK→baidu / EN→tavily (exa fallback). CJK queries on tavily/exa are auto-translated to English for richer descriptions. zhipu rejected. Default false.'),
389
- },
390
- async ({ query, provider = 'auto', count = 5, include_images = false }) => {
391
- try {
392
- // 1) Provider 选择
393
- // - 普通模式:tavily/exa/baidu/zhipu auto-route by language(CJK→baidu, EN→tavily)
394
- // - include_images:zhipu 不支持图,从候选中剔除;其余按语言 auto-route
395
- let providerId;
396
- if (include_images) {
397
- if (provider === 'zhipu') {
398
- return {
399
- content: [{
400
- type: 'text',
401
- text: 'web_search failed: provider "zhipu" does not support image search. Use tavily / exa / baidu, or omit provider for auto-routing.',
402
- }],
403
- isError: true,
404
- };
405
- }
406
- if (provider === 'auto') {
407
- const order = (looksChinese(query) ? PRIORITY_CJK : PRIORITY_NON_CJK)
408
- .filter(id => id !== 'zhipu');
409
- providerId = order.find(id => getKey(id)) || null;
410
- if (!providerId) {
411
- return {
412
- content: [{
413
- type: 'text',
414
- text: 'web_search failed (include_images=true): no image-capable provider configured. Set at least one of '
415
- + ['tavily', 'exa', 'baidu'].map(id => PROVIDERS[id].keyEnv).join(' / '),
416
- }],
417
- isError: true,
418
- };
419
- }
420
- } else {
421
- providerId = provider;
422
- if (!getKey(providerId)) {
423
- return {
424
- content: [{
425
- type: 'text',
426
- text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set in .env.`,
427
- }],
428
- isError: true,
429
- };
430
- }
431
- }
432
- } else {
433
- providerId = provider === 'auto' ? autoSelectProvider(query) : provider;
434
- if (!providerId) {
435
- return {
436
- content: [{
437
- type: 'text',
438
- text: 'web_search failed: no provider configured. Set at least one of '
439
- + Object.values(PROVIDERS).map(p => p.keyEnv).join(' / ')
440
- + ' in NoDesign .env.',
441
- }],
442
- isError: true,
443
- };
444
- }
445
- if (!getKey(providerId)) {
446
- return {
447
- content: [{
448
- type: 'text',
449
- text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set in .env.`,
450
- }],
451
- isError: true,
452
- };
453
- }
454
- }
455
-
456
- const key = getKey(providerId);
457
-
458
- // 2) CJK query 翻英文再搜图这一步已移除(2026-07-31)。
459
- // 它唯一的实现走 NoDesk 网关,网关 07-27 永久退役后函数直接 return null,
460
- // 于是这段代码一直在跑但从来没生效过 —— 中文 query 搜图拿到的
461
- // images[].description 约一半是 null,而日志里什么都看不出来。
462
- // 静默失效比不存在更坏:读代码的人会以为这个能力还在。
463
- // 要恢复的话得先给它一个活着的 provider(见 lib/openai 那套 key),
464
- // 不是把这几行搬回来就完事。
465
- const effectiveQuery = query;
466
-
467
- // 3) Provider call —— tavily/exa/baidu 都接 includeImages opts;zhipu 不传
468
- const adapter = ADAPTERS[providerId];
469
- const adapterOpts = providerId === 'zhipu' ? undefined : { includeImages: include_images };
470
- const result = await adapter(effectiveQuery, key, count, adapterOpts);
471
- const hits = result.hits || [];
472
- const rawImages = result.images || [];
473
-
474
- // 4) Image 下载(include_images=true 才走)
475
- // Provider 之间数量差异大(tavily ≤5、exa 可达 count*5、baidu image
476
- // 模态 + web_extensions 合起来可达 30+)。取 count*2 当下载头空间,
477
- // 再把最终结果截到 count 张,让 markdown 输出可预期。
478
- let downloadedImages = [];
479
- if (include_images && rawImages.length > 0) {
480
- if (!workspaceRoot) {
481
- return {
482
- content: [{
483
- type: 'text',
484
- text: 'web_search internal error: workspaceRoot not configured for include_images mode.',
485
- }],
486
- isError: true,
487
- };
488
- }
489
- // ⚠️ **下多少就报多少**(2026-08-18 修)。以前按 `count*2` 下载却只
490
- // `slice(0, count)` 上报 —— 磁盘上留着一半 agent 不知道存在的文件,
491
- // 而它们照样占空间、照样进导出包。多下的那批本来是为了容错(有些 URL
492
- // 会 404),现在改成:**下够 count 张就停**,成功几张报几张。
493
- const dlCandidates = rawImages.slice(0, Math.max(count * 2, 5));
494
- const downloaded = await downloadReferenceImages(dlCandidates, {
495
- workspaceRoot, sharedRoot, stopAfter: count,
496
- });
497
- downloadedImages = downloaded;
498
- // 落盘了就发 file_changed —— generate_image 一直在发,这里漏了,
499
- // 于是参考图落进工作区之后前端毫无感知(素材抽屉要刷新才看得见)
500
- for (const img of downloadedImages) {
501
- try { ctx?.emit?.({ type: 'run.file_changed', path: img.relPath, change: 'add' }); } catch { /* */ }
502
- }
503
- }
504
-
505
- try {
506
- ctx?.emit?.({
507
- type: 'run.web_search',
508
- provider: providerId,
509
- query: effectiveQuery,
510
- hits: hits.length,
511
- referenceImages: downloadedImages.length,
512
- });
513
- } catch { /* fail-safe */ }
514
-
515
- // 文本 + 每张图一块 image content block(顺序与 markdown 编号一致)
516
- // 让 agent 当 turn 直接 vision-check 候选 reference,省掉逐张 Read。
517
- // 内联图片成本:~1568 token/张(Anthropic image block tokenization)×
518
- // count 张 + base64 体积。已在上一步通过 count 截断控量。
519
- const out = [{
520
- type: 'text',
521
- text: formatMarkdown(query, providerId, hits, { images: downloadedImages }),
522
- }];
523
- for (const img of downloadedImages) {
524
- if (img.base64) {
525
- out.push({
526
- type: 'image',
527
- data: img.base64,
528
- mimeType: img.mimeType,
529
- });
530
- }
531
- }
532
- return { content: out };
533
- } catch (err) {
534
- const msg = err instanceof ProviderError
535
- ? err.message + (err.code === 401 || err.code === 403
536
- ? ` (auth failed — check ${PROVIDERS[err.provider].keyEnv})`
537
- : err.code === 429
538
- ? ' (rate limit / quota exhausted — try another provider)'
539
- : '')
540
- : `web_search error: ${err?.message || String(err)}`;
541
- return {
542
- content: [{ type: 'text', text: msg }],
543
- isError: true,
544
- };
545
- }
546
- },
547
- );
548
- }
1
+ /**
2
+ * mcp/tools/web-search.js — web_search MCP tool
3
+ *
4
+ * 多 provider 联网搜索(移植自 ~/.deskclaw/skills/deskclaw-search-pro/scripts/search.py)。
5
+ * Provider 优先级按 query 语言自动路由:CJK → baidu 优先;非 CJK → tavily 优先;
6
+ * zhipu 配额稀缺永远最后。zero 外部依赖(仅 Node 内置 fetch + URL)。
7
+ *
8
+ * key 配置(.env,至少配一个):
9
+ * NODESIGN_BAIDU_QIANFAN_KEY=bce-v3/...
10
+ * NODESIGN_TAVILY_KEY=tvly-...
11
+ * NODESIGN_EXA_KEY=...
12
+ * NODESIGN_ZHIPU_KEY=...
13
+ *
14
+ * agent 视角的工具名:mcp__nodesign__web_search
15
+ *
16
+ * 返回 markdown(top N 条 title + source + snippet + url),让 agent 直接消费,
17
+ * 不暴露 raw JSON 避免 context 灌满。
18
+ *
19
+ * # 调用上限(agent 应自律,SKILL.md 强约束)
20
+ * - baidu 中文:≤2 次 / turn(snippet 即正文,单次通常够)
21
+ * - tavily:≤3-4 次(摘要浅 100-200 字,需多 query 三角验证)
22
+ * - exa:≤1-2 次(content 字段 2000+ 字完整正文,3 次以上爆 context)
23
+ * - zhipu:≤1 次(包配额稀缺)
24
+ * - baidu 英文:禁用(实测严重跑题)
25
+ */
26
+
27
+ import path from 'node:path';
28
+ import fs from 'node:fs/promises';
29
+ import crypto from 'node:crypto';
30
+ import { downloadReferenceImages } from './helpers/reference-download.js';
31
+ import { tool } from '@anthropic-ai/claude-agent-sdk';
32
+ import { z } from 'zod';
33
+
34
+ const PROVIDERS = {
35
+ tavily: { keyEnv: 'NODESIGN_TAVILY_KEY' },
36
+ exa: { keyEnv: 'NODESIGN_EXA_KEY' },
37
+ baidu: { keyEnv: 'NODESIGN_BAIDU_QIANFAN_KEY' },
38
+ zhipu: { keyEnv: 'NODESIGN_ZHIPU_KEY' },
39
+ };
40
+
41
+ const PRIORITY_CJK = ['baidu', 'tavily', 'exa', 'zhipu'];
42
+ const PRIORITY_NON_CJK = ['tavily', 'exa', 'baidu', 'zhipu'];
43
+
44
+ function looksChinese(text) {
45
+ // U+4E00-U+9FFF Unified CJK
46
+ return /[一-鿿]/.test(text || '');
47
+ }
48
+
49
+ function getKey(providerId) {
50
+ const env = PROVIDERS[providerId]?.keyEnv;
51
+ return env ? (process.env[env] || '') : '';
52
+ }
53
+
54
+ function autoSelectProvider(query) {
55
+ const order = looksChinese(query) ? PRIORITY_CJK : PRIORITY_NON_CJK;
56
+ for (const id of order) if (getKey(id)) return id;
57
+ return null;
58
+ }
59
+
60
+ function domainOf(url) {
61
+ try { return new URL(url).hostname.replace(/^www\./, ''); } catch { return ''; }
62
+ }
63
+
64
+ // ── adapters ──
65
+
66
+ async function searchTavily(query, key, n, { includeImages = false } = {}) {
67
+ const body = {
68
+ query,
69
+ search_depth: includeImages ? 'advanced' : 'basic',
70
+ topic: 'general',
71
+ max_results: n,
72
+ include_answer: false,
73
+ include_raw_content: false,
74
+ };
75
+ if (includeImages) {
76
+ body.include_images = true;
77
+ body.include_image_descriptions = true;
78
+ body.include_favicon = true;
79
+ }
80
+ const res = await fetch('https://api.tavily.com/search', {
81
+ method: 'POST',
82
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
83
+ body: JSON.stringify(body),
84
+ });
85
+ if (!res.ok) throw new ProviderError('tavily', res.status, await res.text());
86
+ const raw = await res.json();
87
+ const hits = (raw.results || []).map(r => ({
88
+ title: r.title || '',
89
+ url: r.url || '',
90
+ snippet: r.content || '',
91
+ source: domainOf(r.url || ''),
92
+ publishedAt: r.published_date || '',
93
+ }));
94
+ const images = includeImages
95
+ ? (raw.images || [])
96
+ .filter(i => i?.url)
97
+ .map(i => ({
98
+ url: i.url,
99
+ description: i.description || '',
100
+ title: i.title || '',
101
+ }))
102
+ : [];
103
+ return { hits, images };
104
+ }
105
+
106
+ async function searchExa(query, key, n, { includeImages = false } = {}) {
107
+ // Exa 顶层是 camelCase(numResults / maxCharacters / imageLinks),
108
+ // 旧字段 num_results 仍兼容,新功能用官方约定的 camelCase。
109
+ const contents = {
110
+ highlights: { maxCharacters: 4000 },
111
+ };
112
+ if (includeImages) {
113
+ contents.extras = { imageLinks: Math.max(3, n) };
114
+ }
115
+ const res = await fetch('https://api.exa.ai/search', {
116
+ method: 'POST',
117
+ headers: { 'x-api-key': key, 'Content-Type': 'application/json' },
118
+ body: JSON.stringify({
119
+ query,
120
+ type: 'auto',
121
+ numResults: n,
122
+ contents,
123
+ }),
124
+ });
125
+ if (!res.ok) throw new ProviderError('exa', res.status, await res.text());
126
+ const raw = await res.json();
127
+ const hits = (raw.results || []).map(r => {
128
+ const highlights = r.highlights || [];
129
+ return {
130
+ title: r.title || '',
131
+ url: r.url || '',
132
+ snippet: highlights[0] || r.text || '',
133
+ source: domainOf(r.url || ''),
134
+ publishedAt: r.publishedDate || '',
135
+ };
136
+ });
137
+ // images: results[].image(页面代表图)+ results[].extras.imageLinks(页面内抽图),dedupe by url
138
+ const images = [];
139
+ if (includeImages) {
140
+ const seen = new Set();
141
+ for (const r of (raw.results || [])) {
142
+ const parentTitle = r.title || '';
143
+ // 1) 页面代表图(每条最多 1 张)
144
+ if (r.image && !seen.has(r.image)) {
145
+ seen.add(r.image);
146
+ images.push({ url: r.image, description: parentTitle, title: parentTitle });
147
+ }
148
+ // 2) 页面内抽到的图片链接(无描述,用 parent title 兜底)
149
+ const links = r.extras?.imageLinks || [];
150
+ for (const link of links) {
151
+ if (!link || seen.has(link)) continue;
152
+ seen.add(link);
153
+ images.push({ url: link, description: parentTitle, title: parentTitle });
154
+ }
155
+ }
156
+ }
157
+ return { hits, images };
158
+ }
159
+
160
+ async function searchBaidu(query, key, n, { includeImages = false } = {}) {
161
+ // Baidu Qianfan content 字段硬上限 72 字符
162
+ const truncated = query.length > 72 ? query.slice(0, 72) : query;
163
+ const body = {
164
+ messages: [{ role: 'user', content: truncated }],
165
+ };
166
+ if (includeImages) {
167
+ // 默认 image.top_k=0;必须显式声明才返图。同时保留 web 模态拿 hits。
168
+ body.search_source = 'baidu_search_v2';
169
+ body.resource_type_filter = [
170
+ { type: 'web', top_k: n },
171
+ { type: 'image', top_k: Math.min(30, Math.max(n, 10)) },
172
+ ];
173
+ }
174
+ const res = await fetch('https://qianfan.baidubce.com/v2/ai_search/web_search', {
175
+ method: 'POST',
176
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
177
+ body: JSON.stringify(body),
178
+ });
179
+ if (!res.ok) throw new ProviderError('baidu', res.status, await res.text());
180
+ const raw = await res.json();
181
+ let results = raw.search_results || raw.results || [];
182
+ if (!Array.isArray(results) || results.length === 0) {
183
+ results = raw.references || [];
184
+ }
185
+ // 把 web 与 image 两类拆开:image 模态是 type==='image'
186
+ const webRefs = results.filter(r => (r?.type || 'web') !== 'image');
187
+ const imageRefs = results.filter(r => r?.type === 'image');
188
+
189
+ const hits = webRefs.slice(0, n).map(r => ({
190
+ title: r.title || '',
191
+ url: r.url || r.link || '',
192
+ snippet: r.content || r.abstract || r.segment_text || '',
193
+ source: r.source || domainOf(r.url || r.link || ''),
194
+ publishedAt: r.publish_time || '',
195
+ }));
196
+
197
+ const images = [];
198
+ if (includeImages) {
199
+ const seen = new Set();
200
+ // 1) 图片模态条目
201
+ for (const r of imageRefs) {
202
+ const u = r.image?.url || r.url;
203
+ if (!u || seen.has(u)) continue;
204
+ seen.add(u);
205
+ images.push({
206
+ url: u,
207
+ description: r.title || '',
208
+ title: r.title || '',
209
+ });
210
+ }
211
+ // 2) 网页条目里夹带的相关图(web_extensions.images)
212
+ for (const r of webRefs) {
213
+ const arr = r.web_extensions?.images || [];
214
+ for (const img of arr) {
215
+ const u = img?.url;
216
+ if (!u || seen.has(u)) continue;
217
+ seen.add(u);
218
+ images.push({
219
+ url: u,
220
+ description: r.title || '',
221
+ title: r.title || '',
222
+ });
223
+ }
224
+ }
225
+ }
226
+ return { hits, images };
227
+ }
228
+
229
+ async function searchZhipu(query, key, n) {
230
+ // 用 dedicated tools/web_search endpoint(比 chat completions 路径更直接)
231
+ const res = await fetch('https://open.bigmodel.cn/api/paas/v4/tools/web_search', {
232
+ method: 'POST',
233
+ headers: { 'Authorization': `Bearer ${key}`, 'Content-Type': 'application/json' },
234
+ body: JSON.stringify({
235
+ search_engine: 'search_pro',
236
+ search_query: query,
237
+ count: n,
238
+ }),
239
+ });
240
+ if (!res.ok) throw new ProviderError('zhipu', res.status, await res.text());
241
+ const raw = await res.json();
242
+ const hits = (raw.search_result || []).slice(0, n).map(r => ({
243
+ title: r.title || '',
244
+ url: r.link || r.url || '',
245
+ snippet: r.content || '',
246
+ source: r.media || domainOf(r.link || r.url || ''),
247
+ publishedAt: '',
248
+ }));
249
+ return { hits, images: [] };
250
+ }
251
+
252
+ const ADAPTERS = {
253
+ tavily: searchTavily,
254
+ exa: searchExa,
255
+ baidu: searchBaidu,
256
+ zhipu: searchZhipu,
257
+ };
258
+
259
+ class ProviderError extends Error {
260
+ constructor(provider, code, body) {
261
+ super(`[${provider}] HTTP ${code}: ${String(body || '').slice(0, 300)}`);
262
+ this.provider = provider;
263
+ this.code = code;
264
+ }
265
+ }
266
+
267
+ function formatMarkdown(query, provider, hits, { images = [] } = {}) {
268
+ const lines = [`## Search results (${provider}, ${hits.length} hits)`, '', `> Query: ${query}`, ''];
269
+ if (hits.length === 0) {
270
+ lines.push('No text results found.');
271
+ } else {
272
+ hits.forEach((h, i) => {
273
+ const src = h.source ? ` — ${h.source}` : '';
274
+ const pub = h.publishedAt ? ` (${h.publishedAt})` : '';
275
+ lines.push(`${i + 1}. **${h.title || 'Untitled'}**${src}${pub}`);
276
+ if (h.snippet) {
277
+ // 缩进显示,限单条 snippet 800 字符防爆 context
278
+ const snippet = h.snippet.length > 800 ? h.snippet.slice(0, 800) + '…' : h.snippet;
279
+ for (const line of snippet.split('\n')) lines.push(` ${line}`);
280
+ }
281
+ if (h.url) lines.push(` ${h.url}`);
282
+ lines.push('');
283
+ });
284
+ }
285
+
286
+ if (images.length > 0) {
287
+ lines.push(`## Reference images (downloaded, ${images.length})`, '');
288
+ lines.push(
289
+ '> The actual pixel content of each image follows this text as inline '
290
+ + 'image content blocks (in the same numbered order). Vision-check them '
291
+ + 'directly to pick the best 1-2 candidates.',
292
+ '> Then pass the chosen `local_path` value(s) as `referenceImages[]` to '
293
+ + '`mcp__nodesign__generate_image` (paths are already inside the workspace).',
294
+ '',
295
+ );
296
+ images.forEach((img, i) => {
297
+ const desc = img.description || img.title || '(no description)';
298
+ const sizeKB = (img.sizeBytes / 1024).toFixed(0);
299
+ lines.push(`${i + 1}. ${desc}`);
300
+ lines.push(` local_path: ${img.relPath}`);
301
+ lines.push(` size: ${sizeKB} KB, mime: ${img.mimeType}`);
302
+ lines.push(` source: ${domainOf(img.url)}`);
303
+ lines.push(` url: ${img.url}`);
304
+ lines.push('');
305
+ });
306
+ }
307
+ return lines.join('\n');
308
+ }
309
+
310
+ /**
311
+ * @param {object} deps
312
+ * @param {string} [deps.workspaceRoot] agent cwd(include_images 下载落档需要)
313
+ * @param {string} [deps.sharedRoot] project shared/,存在时优先落档
314
+ * @param {import('../../agent/context.js').AgentContext} [deps.ctx]
315
+ */
316
+ export function makeWebSearchTool({ workspaceRoot, sharedRoot, ctx } = {}) {
317
+ return tool(
318
+ 'web_search',
319
+ `Search the web via tavily / exa / baidu (Qianfan) / zhipu providers. Auto-routes by query language: CJK
320
+ queries prefer baidu, English queries prefer tavily.
321
+
322
+ Use this tool when:
323
+ - You need current information (latest design trends, library docs, recent events)
324
+ - You need to verify a fact or find a citation
325
+ - You need to find a URL to then GO LOOK AT: for anything visual, follow up with browser_navigate and browse the site's inner pages — snippets carry no layout, type or colour. (Or web_fetch for text.)
326
+ - You need REFERENCE IMAGES for generate_image (set include_images=true; see below)
327
+
328
+ DO NOT:
329
+ - Run more than 2-3 queries per turn (context bloat)
330
+ - Use baidu for English queries (returns severely off-topic results)
331
+ - Re-issue the same query to retry — change the wording instead
332
+
333
+ For Chinese queries baidu's snippet field already contains 500-3000 chars of body text;
334
+ you usually don't need to web_fetch the URL afterwards. Tavily snippets are short (100-200
335
+ chars) and may need a follow-up fetch.
336
+
337
+ Add a year hint (e.g., "2025 2026") to the query — search engines especially Chinese ones
338
+ often return stale results without it.
339
+
340
+ # include_images mode (reference imagery for generate_image)
341
+
342
+ When you need real-world subject anchor before generate_image (product shots, scenes,
343
+ iconic landmarks, brand visuals, etc.), pass include_images=true. Behavior:
344
+ - Auto-routes by language (excluding zhipu, which has no image support):
345
+ CJK query → baidu (native CJK image search; no translation)
346
+ EN query → tavily (richest image descriptions, ~100% covered)
347
+ fallback → exa (page-representative image + page-internal imageLinks)
348
+ You can override with provider='tavily'|'exa'|'baidu'.
349
+ - When provider lands on tavily/exa AND query is CJK, the tool auto-translates
350
+ the query to English first (Tavily/Exa image descriptions are dramatically
351
+ richer in English; baidu doesn't need translation).
352
+ - Top-N image hits are downloaded into <workspace>/assets/references/ref-<hash>.<ext>
353
+ and listed in the markdown output as "Reference images (downloaded, N)".
354
+ - The CallToolResult also returns each downloaded image as an inline image
355
+ content block (in the same numbered order as the markdown), so you can
356
+ vision-check the candidates immediately without calling Read.
357
+ - Each entry has a 'local_path' field — pass that path directly into
358
+ mcp__nodesign__generate_image referenceImages[] (NOT the http url; the gen tool
359
+ only accepts workspace-relative paths).
360
+ - Filtered: 5KB ≤ size ≤ 8MB, only png/jpg/webp/gif content-types accepted.
361
+ - Per-provider quirks:
362
+ tavily: top-level images[] — clean, on-topic, every image has a description
363
+ exa: page-rep image + extras.imageLinks — many small/decorative URLs
364
+ survive the size filter, expect more variance
365
+ baidu: native CJK + image size/ratio/format filter via search_filter (we
366
+ don't expose those yet); image entries lack descriptions, we use
367
+ the parent reference title as fallback caption
368
+
369
+ Suggested flow for image-led pages:
370
+ 1. user picks theme → 2. web_search(query, include_images=true) → 3. pick the
371
+ best-matching local_path → 4. generate_image(prompt, referenceImages=[that path])`,
372
+ {
373
+ query: z.string().min(2).max(500).describe('Search query. Add year hints (2025/2026) for time-sensitive results. Baidu truncates content to 72 chars — keep CJK queries short.'),
374
+ provider: z
375
+ .enum(['tavily', 'exa', 'baidu', 'zhipu', 'auto'])
376
+ .optional()
377
+ .describe('Force a provider; default "auto" routes by query language. include_images=true auto-routes among tavily/exa/baidu (zhipu rejected, no image support).'),
378
+ count: z
379
+ .number()
380
+ .int()
381
+ .min(1)
382
+ .max(10)
383
+ .optional()
384
+ .describe('Max text results to return (default 5). Image count follows the same value.'),
385
+ include_images: z
386
+ .boolean()
387
+ .optional()
388
+ .describe('Returns top-N reference images downloaded into assets/references/ for use as generate_image referenceImages. Auto-routes CJK→baidu / EN→tavily (exa fallback). CJK queries on tavily/exa are auto-translated to English for richer descriptions. zhipu rejected. Default false.'),
389
+ },
390
+ async ({ query, provider = 'auto', count = 5, include_images = false }) => {
391
+ try {
392
+ // 1) Provider 选择
393
+ // - 普通模式:tavily/exa/baidu/zhipu auto-route by language(CJK→baidu, EN→tavily)
394
+ // - include_images:zhipu 不支持图,从候选中剔除;其余按语言 auto-route
395
+ let providerId;
396
+ if (include_images) {
397
+ if (provider === 'zhipu') {
398
+ return {
399
+ content: [{
400
+ type: 'text',
401
+ text: 'web_search failed: provider "zhipu" does not support image search. Use tavily / exa / baidu, or omit provider for auto-routing.',
402
+ }],
403
+ isError: true,
404
+ };
405
+ }
406
+ if (provider === 'auto') {
407
+ const order = (looksChinese(query) ? PRIORITY_CJK : PRIORITY_NON_CJK)
408
+ .filter(id => id !== 'zhipu');
409
+ providerId = order.find(id => getKey(id)) || null;
410
+ if (!providerId) {
411
+ return {
412
+ content: [{
413
+ type: 'text',
414
+ text: 'web_search failed (include_images=true): no image-capable provider configured. Set at least one of '
415
+ + ['tavily', 'exa', 'baidu'].map(id => PROVIDERS[id].keyEnv).join(' / '),
416
+ }],
417
+ isError: true,
418
+ };
419
+ }
420
+ } else {
421
+ providerId = provider;
422
+ if (!getKey(providerId)) {
423
+ return {
424
+ content: [{
425
+ type: 'text',
426
+ text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set in .env.`,
427
+ }],
428
+ isError: true,
429
+ };
430
+ }
431
+ }
432
+ } else {
433
+ providerId = provider === 'auto' ? autoSelectProvider(query) : provider;
434
+ if (!providerId) {
435
+ return {
436
+ content: [{
437
+ type: 'text',
438
+ text: 'web_search failed: no provider configured. Set at least one of '
439
+ + Object.values(PROVIDERS).map(p => p.keyEnv).join(' / ')
440
+ + ' in NoDesign .env.',
441
+ }],
442
+ isError: true,
443
+ };
444
+ }
445
+ if (!getKey(providerId)) {
446
+ return {
447
+ content: [{
448
+ type: 'text',
449
+ text: `web_search failed: ${PROVIDERS[providerId].keyEnv} not set in .env.`,
450
+ }],
451
+ isError: true,
452
+ };
453
+ }
454
+ }
455
+
456
+ const key = getKey(providerId);
457
+
458
+ // 2) CJK query 翻英文再搜图这一步已移除(2026-07-31)。
459
+ // 它唯一的实现走 NoDesk 网关,网关 07-27 永久退役后函数直接 return null,
460
+ // 于是这段代码一直在跑但从来没生效过 —— 中文 query 搜图拿到的
461
+ // images[].description 约一半是 null,而日志里什么都看不出来。
462
+ // 静默失效比不存在更坏:读代码的人会以为这个能力还在。
463
+ // 要恢复的话得先给它一个活着的 provider(见 lib/openai 那套 key),
464
+ // 不是把这几行搬回来就完事。
465
+ const effectiveQuery = query;
466
+
467
+ // 3) Provider call —— tavily/exa/baidu 都接 includeImages opts;zhipu 不传
468
+ const adapter = ADAPTERS[providerId];
469
+ const adapterOpts = providerId === 'zhipu' ? undefined : { includeImages: include_images };
470
+ const result = await adapter(effectiveQuery, key, count, adapterOpts);
471
+ const hits = result.hits || [];
472
+ const rawImages = result.images || [];
473
+
474
+ // 4) Image 下载(include_images=true 才走)
475
+ // Provider 之间数量差异大(tavily ≤5、exa 可达 count*5、baidu image
476
+ // 模态 + web_extensions 合起来可达 30+)。取 count*2 当下载头空间,
477
+ // 再把最终结果截到 count 张,让 markdown 输出可预期。
478
+ let downloadedImages = [];
479
+ if (include_images && rawImages.length > 0) {
480
+ if (!workspaceRoot) {
481
+ return {
482
+ content: [{
483
+ type: 'text',
484
+ text: 'web_search internal error: workspaceRoot not configured for include_images mode.',
485
+ }],
486
+ isError: true,
487
+ };
488
+ }
489
+ // ⚠️ **下多少就报多少**(2026-08-18 修)。以前按 `count*2` 下载却只
490
+ // `slice(0, count)` 上报 —— 磁盘上留着一半 agent 不知道存在的文件,
491
+ // 而它们照样占空间、照样进导出包。多下的那批本来是为了容错(有些 URL
492
+ // 会 404),现在改成:**下够 count 张就停**,成功几张报几张。
493
+ const dlCandidates = rawImages.slice(0, Math.max(count * 2, 5));
494
+ const downloaded = await downloadReferenceImages(dlCandidates, {
495
+ workspaceRoot, sharedRoot, stopAfter: count,
496
+ });
497
+ downloadedImages = downloaded;
498
+ // 落盘了就发 file_changed —— generate_image 一直在发,这里漏了,
499
+ // 于是参考图落进工作区之后前端毫无感知(素材抽屉要刷新才看得见)
500
+ for (const img of downloadedImages) {
501
+ try { ctx?.emit?.({ type: 'run.file_changed', path: img.relPath, change: 'add' }); } catch { /* */ }
502
+ }
503
+ }
504
+
505
+ try {
506
+ ctx?.emit?.({
507
+ type: 'run.web_search',
508
+ provider: providerId,
509
+ query: effectiveQuery,
510
+ hits: hits.length,
511
+ referenceImages: downloadedImages.length,
512
+ });
513
+ } catch { /* fail-safe */ }
514
+
515
+ // 文本 + 每张图一块 image content block(顺序与 markdown 编号一致)
516
+ // 让 agent 当 turn 直接 vision-check 候选 reference,省掉逐张 Read。
517
+ // 内联图片成本:~1568 token/张(Anthropic image block tokenization)×
518
+ // count 张 + base64 体积。已在上一步通过 count 截断控量。
519
+ const out = [{
520
+ type: 'text',
521
+ text: formatMarkdown(query, providerId, hits, { images: downloadedImages }),
522
+ }];
523
+ for (const img of downloadedImages) {
524
+ if (img.base64) {
525
+ out.push({
526
+ type: 'image',
527
+ data: img.base64,
528
+ mimeType: img.mimeType,
529
+ });
530
+ }
531
+ }
532
+ return { content: out };
533
+ } catch (err) {
534
+ const msg = err instanceof ProviderError
535
+ ? err.message + (err.code === 401 || err.code === 403
536
+ ? ` (auth failed — check ${PROVIDERS[err.provider].keyEnv})`
537
+ : err.code === 429
538
+ ? ' (rate limit / quota exhausted — try another provider)'
539
+ : '')
540
+ : `web_search error: ${err?.message || String(err)}`;
541
+ return {
542
+ content: [{ type: 'text', text: msg }],
543
+ isError: true,
544
+ };
545
+ }
546
+ },
547
+ );
548
+ }