@xiaobuyu/nodesign 0.0.1 → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +156 -106
- package/bin/nodesign.js +0 -0
- package/package.json +1 -1
- package/server/api/README.md +45 -45
- package/server/api/_guard.js +75 -75
- package/server/api/admin.js +187 -187
- package/server/api/assets/docx-page.js +103 -103
- package/server/api/assets/helpers.js +22 -22
- package/server/api/assets/notes.js +144 -144
- package/server/api/assets.js +896 -896
- package/server/api/board.js +57 -57
- package/server/api/browse.js +138 -138
- package/server/api/canvas.js +444 -444
- package/server/api/chatai.js +324 -324
- package/server/api/exports/build-standalone.js +980 -980
- package/server/api/exports/cards.js +118 -118
- package/server/api/exports/docx-pdf.js +46 -46
- package/server/api/exports/export-page.js +142 -142
- package/server/api/exports/handoff.js +147 -147
- package/server/api/exports.js +690 -690
- package/server/api/instruction.js +75 -75
- package/server/api/me.js +116 -116
- package/server/api/memory.js +185 -185
- package/server/api/pending-changes.js +366 -366
- package/server/api/plugins.js +164 -164
- package/server/api/projects.js +220 -220
- package/server/api/publish.js +64 -64
- package/server/api/recent.js +73 -73
- package/server/api/sessions.js +589 -589
- package/server/api/skills.js +77 -77
- package/server/api/standalone-fit.js +168 -168
- package/server/api/turn-compose.js +180 -180
- package/server/api/turn-inflight.js +44 -44
- package/server/auth/README.md +17 -17
- package/server/auth/middleware.js +138 -138
- package/server/auth/origin-guard.js +71 -71
- package/server/auth/session.js +108 -108
- package/server/auth/tier.js +113 -113
- package/server/auth/users-store.js +348 -348
- package/server/edit/README.md +29 -29
- package/server/engine/README.md +113 -113
- package/server/engine/agent/agent-shared.js +550 -550
- package/server/engine/agent/auto-mode-default-hard-deny.txt +4 -4
- package/server/engine/agent/auto-mode-rules.js +78 -78
- package/server/engine/agent/context.js +418 -418
- package/server/engine/agent/events.js +464 -464
- package/server/engine/agent/hooks/canvas-validate.js +199 -199
- package/server/engine/agent/hooks/failure.js +120 -120
- package/server/engine/agent/hooks/file-events.js +75 -75
- package/server/engine/agent/hooks/lifecycle.js +233 -233
- package/server/engine/agent/hooks/post-canvas-focus.js +80 -80
- package/server/engine/agent/hooks/post-guidance.js +206 -206
- package/server/engine/agent/hooks/post-subagent-report.js +104 -104
- package/server/engine/agent/hooks/post-trim.js +58 -58
- package/server/engine/agent/hooks/pre-board-neighborhood.js +34 -34
- package/server/engine/agent/hooks/pre-defaults.js +84 -84
- package/server/engine/agent/hooks/pre-injectors.js +277 -277
- package/server/engine/agent/hooks/pre-performance-log-guard.js +78 -78
- package/server/engine/agent/hooks/pre-starter-files.js +61 -61
- package/server/engine/agent/hooks/pre-workspace-scope-guard.js +78 -78
- package/server/engine/agent/hooks/site-validate.js +94 -94
- package/server/engine/agent/hooks/tool-prompts.js +31 -31
- package/server/engine/agent/hooks/turn-state-memory.js +56 -56
- package/server/engine/agent/hooks/user-prompt-submit.js +215 -215
- package/server/engine/agent/hooks.js +354 -354
- package/server/engine/agent/init-contract.js +69 -69
- package/server/engine/agent/isolation.js +159 -159
- package/server/engine/agent/model-table.js +292 -292
- package/server/engine/agent/plugin-loader.js +266 -266
- package/server/engine/agent/prompts/nodesign-prelude.md +336 -336
- package/server/engine/agent/prompts/tools/ask-user-question-protocol.md +71 -71
- package/server/engine/agent/prompts/tools/direct-edit-protocol.md +98 -98
- package/server/engine/agent/prompts/tools/generate-image-cookbook.gemini-gateway.md +583 -583
- package/server/engine/agent/prompts/tools/generate-image-cookbook.md +483 -483
- package/server/engine/agent/prompts/tools/hybrid-reference.md +100 -100
- package/server/engine/agent/prompts/tools/paint-still-cookbook.md +270 -270
- package/server/engine/agent/prompts/tools/roll-film-cookbook.md +70 -70
- package/server/engine/agent/prompts/tools/site-reference.md +106 -106
- package/server/engine/agent/prompts/tools/tweaks-syntax.md +125 -125
- package/server/engine/agent/prompts/tools/vision-checker-dispatch.md +48 -48
- package/server/engine/agent/session-loop.js +1024 -1024
- package/server/engine/agent/session-model.js +164 -164
- package/server/engine/agent/skill.js +204 -204
- package/server/engine/agent/task-events.js +78 -78
- package/server/engine/agents/ds-extractor.md +132 -132
- package/server/engine/agents/explorer.md +190 -190
- package/server/engine/agents/index.js +230 -230
- package/server/engine/agents/schemas/design-system.json +156 -156
- package/server/engine/agents/schemas/tweak-schema.json +113 -113
- package/server/engine/agents/tweak-proposer.md +127 -127
- package/server/engine/agents/vision-checker.md +254 -254
- package/server/engine/browse/capture.js +422 -422
- package/server/engine/browse/card.js +137 -137
- package/server/engine/browse/handover.js +59 -59
- package/server/engine/browse/page-digest.js +119 -119
- package/server/engine/browse/refs.js +196 -196
- package/server/engine/browse/registry.js +307 -307
- package/server/engine/browse/screencast.js +170 -170
- package/server/engine/browse/state.js +121 -121
- package/server/engine/chatai/chat-log.js +79 -79
- package/server/engine/chatai/index.js +193 -193
- package/server/engine/chatai/openai-compat.js +152 -152
- package/server/engine/chatai/orchestrate.js +309 -309
- package/server/engine/chatai/perform.js +40 -40
- package/server/engine/chatai/summarize.js +79 -79
- package/server/engine/mcp/capability-gate.js +52 -52
- package/server/engine/mcp/index.js +339 -339
- package/server/engine/mcp/param-sanitizer.js +142 -142
- package/server/engine/mcp/tools/arrange-on-board.js +124 -124
- package/server/engine/mcp/tools/artifact-session.js +278 -278
- package/server/engine/mcp/tools/browse-computer.js +421 -421
- package/server/engine/mcp/tools/browse-find-batch.js +183 -183
- package/server/engine/mcp/tools/browse-screenshot.js +157 -157
- package/server/engine/mcp/tools/browse.js +480 -480
- package/server/engine/mcp/tools/build-docx.js +101 -101
- package/server/engine/mcp/tools/clear-pending-changes.js +99 -99
- package/server/engine/mcp/tools/create-on-board.js +155 -155
- package/server/engine/mcp/tools/crystallize-skill.js +168 -168
- package/server/engine/mcp/tools/explain-style.js +237 -237
- package/server/engine/mcp/tools/export-handoff.js +133 -133
- package/server/engine/mcp/tools/expose-tweaks.js +149 -149
- package/server/engine/mcp/tools/generate-image.js +842 -842
- package/server/engine/mcp/tools/get-computed-styles.js +164 -164
- package/server/engine/mcp/tools/get-pending-changes.js +205 -205
- package/server/engine/mcp/tools/h3box-ssh.js +84 -84
- package/server/engine/mcp/tools/helpers/acquire-page.js +82 -82
- package/server/engine/mcp/tools/helpers/motion-lab.js +554 -554
- package/server/engine/mcp/tools/helpers/motion-scroll.js +112 -112
- package/server/engine/mcp/tools/helpers/perception-page.js +215 -215
- package/server/engine/mcp/tools/helpers/reference-download.js +103 -103
- package/server/engine/mcp/tools/helpers/rembg-bridge.py +80 -80
- package/server/engine/mcp/tools/helpers/shot-pipeline.js +280 -280
- package/server/engine/mcp/tools/highlight.js +68 -68
- package/server/engine/mcp/tools/list-pages.js +222 -222
- package/server/engine/mcp/tools/lookup-tags.js +59 -59
- package/server/engine/mcp/tools/navigate-to-page.js +60 -60
- package/server/engine/mcp/tools/organize-board.js +64 -64
- package/server/engine/mcp/tools/paint-still.js +434 -434
- package/server/engine/mcp/tools/pin-to-board.js +130 -130
- package/server/engine/mcp/tools/preview-deck.js +130 -130
- package/server/engine/mcp/tools/profile-scroll.js +305 -305
- package/server/engine/mcp/tools/publish-site.js +102 -102
- package/server/engine/mcp/tools/query-elements.js +195 -195
- package/server/engine/mcp/tools/read-board.js +103 -103
- package/server/engine/mcp/tools/read-document.js +93 -93
- package/server/engine/mcp/tools/read-page.js +264 -264
- package/server/engine/mcp/tools/read-tavern-json.js +143 -143
- package/server/engine/mcp/tools/record-decision.js +140 -140
- package/server/engine/mcp/tools/relate-on-board.js +110 -110
- package/server/engine/mcp/tools/report-issue.js +115 -115
- package/server/engine/mcp/tools/roll-film.js +266 -266
- package/server/engine/mcp/tools/screenshot-docx.js +138 -138
- package/server/engine/mcp/tools/screenshot-url.js +195 -195
- package/server/engine/mcp/tools/screenshot.js +583 -583
- package/server/engine/mcp/tools/tier-gate.js +96 -96
- package/server/engine/mcp/tools/trace-motion.js +215 -215
- package/server/engine/mcp/tools/web-search.js +548 -548
- package/server/engine/motion/inventory.js +349 -349
- package/server/engine/perception/session.js +235 -235
- package/server/engine/plugins/nodesign/.claude-plugin/plugin.json +5 -5
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/SKILL.md +192 -192
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/canvas.template.html +667 -667
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/hybrid-grid.md +22 -22
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/image-led-cover.md +25 -25
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/portrait.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/quote-backdrop.md +21 -21
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/section-divider.md +23 -23
- package/server/engine/plugins/nodesign/skills/deskskill-engine-mini/patterns/text-led.md +21 -21
- package/server/engine/plugins/nodesign/skills/docx-craft/SKILL.md +162 -162
- package/server/engine/plugins/nodesign/skills/docx-craft/references/token-schema.md +407 -407
- package/server/engine/plugins/nodesign/skills/docx-craft//346/226/207/346/241/243.template.json +80 -80
- package/server/engine/plugins/nodesign/skills/rp-craft/SKILL.md +228 -228
- 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
- package/server/engine/plugins/nodesign/skills/rp-craft//346/274/224/345/207/272.template.js +268 -268
- package/server/engine/plugins/nodesign/skills/site-craft/SKILL.md +323 -323
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/build-lane.md +134 -134
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/cutout-collage.md +61 -61
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/mock-app.md +163 -163
- package/server/engine/plugins/nodesign/skills/site-craft/patterns/page-transitions.md +86 -86
- package/server/engine/runs/active-runs.js +698 -698
- package/server/engine/runs/live-turn.js +252 -252
- package/server/engine/runs/store.js +364 -364
- package/server/engine/runs/turn-relay.js +217 -217
- package/server/engine/runtime/workspace.js +164 -164
- package/server/lib/artifact-file-path.js +80 -80
- package/server/lib/artifact-target.js +284 -284
- package/server/lib/asset-refs.js +239 -239
- package/server/lib/async-queue.js +104 -104
- package/server/lib/auto-relations.js +130 -130
- package/server/lib/binding-types.js +60 -60
- package/server/lib/board-kind-sizes.js +62 -62
- package/server/lib/board-relations.js +133 -133
- package/server/lib/browse-proxy.js +180 -180
- package/server/lib/canvas-id.js +36 -36
- package/server/lib/cover.js +227 -227
- package/server/lib/danbooru-tags.js +291 -291
- package/server/lib/doc-extract.js +156 -156
- package/server/lib/docx/build-from-source.js +260 -260
- package/server/lib/docx/build.js +490 -490
- package/server/lib/docx/dump-styles.js +287 -287
- package/server/lib/docx/fonts/nodesign-cjk.conf +164 -164
- package/server/lib/docx/merge-runs.js +129 -129
- package/server/lib/docx/numbering.js +218 -218
- package/server/lib/docx/order.js +152 -152
- package/server/lib/docx/rawzip.js +172 -172
- package/server/lib/docx/render.js +92 -92
- package/server/lib/docx/text-lint.js +225 -225
- package/server/lib/docx/tokens.js +335 -335
- package/server/lib/docx/units.js +29 -29
- package/server/lib/docx/xml.js +291 -291
- package/server/lib/docx-pages.js +187 -187
- package/server/lib/export-collect.js +289 -289
- package/server/lib/export-package.js +210 -210
- package/server/lib/html-srcset.js +145 -145
- package/server/lib/image-variant.js +460 -460
- package/server/lib/ingress/forward-openai-chat.js +305 -305
- package/server/lib/ingress/openai-chat.js +491 -491
- package/server/lib/ingress/session-notice.js +66 -66
- package/server/lib/ingress/session-routes.js +68 -68
- package/server/lib/ingress/slot-probe.js +130 -130
- package/server/lib/ingress/upstream-billing.js +52 -52
- package/server/lib/ingress/upstream-fail-streak.js +72 -72
- package/server/lib/ingress/upstream-truncation.js +48 -48
- package/server/lib/issues-store.js +182 -182
- package/server/lib/kinds/deck.js +72 -72
- package/server/lib/kinds/docx.js +283 -283
- package/server/lib/kinds/file-kinds.js +113 -113
- package/server/lib/kinds/index.js +202 -202
- package/server/lib/kinds/site.js +161 -161
- package/server/lib/model-ingress.js +543 -543
- package/server/lib/moderation.js +255 -255
- package/server/lib/notice-store.js +103 -103
- package/server/lib/plugin-install.js +104 -104
- package/server/lib/plugin-validator.js +721 -721
- package/server/lib/publish-store.js +115 -115
- package/server/lib/quick-summary.js +52 -52
- package/server/lib/quota.js +270 -270
- package/server/lib/rate-window.js +29 -29
- package/server/lib/reference-assets.js +92 -92
- package/server/lib/region-shot.js +135 -135
- package/server/lib/safe-path.js +73 -73
- package/server/lib/sdk-session.js +33 -33
- package/server/lib/showcase-store.js +92 -92
- package/server/lib/site-publish.js +465 -465
- package/server/lib/ssrf-guard.js +432 -432
- package/server/lib/task-scan.js +158 -158
- package/server/lib/tavern-json.js +154 -154
- package/server/lib/video-variant.js +237 -237
- package/server/lib/workspace-path.js +33 -33
- package/server/ops/fix-sdk-musl.mjs +40 -40
- package/server/ops/install-macos-fonts.sh +114 -114
- package/server/ops/macos-fonts.conf +336 -336
- package/server/ops/sandbox-shim/bwrap +57 -57
- package/server/projects/assets-summary.js +119 -119
- package/server/projects/auto-name.js +53 -53
- package/server/projects/board-store.js +640 -640
- package/server/projects/move-entry.js +103 -103
- package/server/projects/store.js +270 -270
- package/server/projects/ui-config.js +54 -54
- package/server/projects/workspace-templates.js +53 -53
- package/server/projects/workspace.js +1025 -1025
- package/server/runtime/local-config.js +177 -177
- package/server/runtime/profile.js +72 -72
- package/server/services/rembg-service.py +334 -334
- package/server/shared/README.md +25 -25
- package/server/shared/deck.js +54 -54
- package/server/shared/time.js +49 -49
- package/server/style-pipeline/README.md +53 -53
- package/server/ws/broker.js +36 -36
- package/server/ws/browse-channel.js +177 -177
- package/server/ws/index.js +386 -386
- package/web/README.md +89 -89
- package/web/dist/assets/BrowserWindow-BYCgizHw.js +11 -0
- package/web/dist/assets/{DeckWindow-4G0xX7P3.js → DeckWindow-pNydEM4L.js} +1 -1
- package/web/dist/assets/{DocxWindow-BXQsoPkh.js → DocxWindow-PPgzh3Gq.js} +1 -1
- package/web/dist/assets/{SiteWindow-D6eS-2gM.js → SiteWindow-DM0LQPCO.js} +1 -1
- package/web/dist/assets/{index-Cbl3ATvp.js → index-BHTBJlpY.js} +32 -32
- package/web/dist/assets/{pending-edit-apply-DTjo2geW.js → pending-edit-apply-C0I13CbD.js} +1 -1
- package/web/dist/assets/{rotate-cw-LMT3x1Zj.js → rotate-cw-B1UQ8Hld.js} +1 -1
- package/web/dist/assets/{square-dashed-mouse-pointer-CBoAkfhi.js → square-dashed-mouse-pointer-8lrgqmfv.js} +1 -1
- package/web/dist/index.html +17 -17
- package/web/dist/assets/BrowserWindow-Dc7J7CYc.js +0 -11
|
@@ -1,183 +1,183 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp/tools/browse-find-batch.js — browser_find + 通用 batch 工厂 + browser_batch(2026-08-21)
|
|
3
|
-
*
|
|
4
|
-
* 两个都照 Anthropic browser use toolset / Claude in Chrome 的形状:
|
|
5
|
-
*
|
|
6
|
-
* - `browser_find`:自然语言描述 → 元素清单,每个带 `[ref_N]` 和视口中心坐标。
|
|
7
|
-
* ref 能直接喂给 browser_computer(ref 比像素稳:版面抖一下像素就偏,ref 还在)。
|
|
8
|
-
* 匹配是**词法**的(engine/browse/refs.js 头注释说了为什么),描述里明说。
|
|
9
|
-
*
|
|
10
|
-
* - `makeBatchTool`:一次调用串行跑多条动作,**遇错即停**,后面的条目一律回规格原话
|
|
11
|
-
* "Not executed: an earlier action in this turn failed.",结尾自动补一张截图(规格:
|
|
12
|
-
* "your application can attach its own observation on the last result to save a round
|
|
13
|
-
* trip")。省掉的是模型往返 —— 浏览和产物检查两条通道唯一真能砍掉的时间。
|
|
14
|
-
* browser_batch 和 artifact_batch 都是它的实例(同一份合同,别抄两遍)。
|
|
15
|
-
*
|
|
16
|
-
* ⚠️ 不在 withBrowser / withSession 里套锁:registry 的 mutex 同键串行、不可重入。
|
|
17
|
-
* batch 逐条调用各工具自己的 handler,每条各自拿一次锁。
|
|
18
|
-
*
|
|
19
|
-
* 典型一趟(采参考站的设计 token):navigate 内页 A → capture palette+fonts →
|
|
20
|
-
* navigate 内页 B → capture → navigate 内页 C → capture → 结尾自动截一张。
|
|
21
|
-
* 以前这是 6 个回合,现在是 1 个。
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
|
-
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
-
import { z } from 'zod';
|
|
26
|
-
import { withBrowser } from '../../browse/registry.js';
|
|
27
|
-
import { findInPage, formatMatches } from '../../browse/refs.js';
|
|
28
|
-
|
|
29
|
-
const asText = (text, isError = false) => ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
|
|
30
|
-
|
|
31
|
-
/** 规格原话(browser 版 halt 文案) */
|
|
32
|
-
export const HALT_TEXT = 'Not executed: an earlier action in this turn failed.';
|
|
33
|
-
/**
|
|
34
|
-
* browser_batch 能跑的工具。只有 request_help 不进来(它阻塞等人,一等两分钟)。
|
|
35
|
-
* capture **在里面**(用户 08-21 点的):采参考站的 token 本来就是"进内页 → 采 →
|
|
36
|
-
* 进下一页 → 再采"的多步活,一页一个回合太贵;一趟 batch 把三四页的调色板/字体
|
|
37
|
-
* 一起带回来才对味。
|
|
38
|
-
*/
|
|
39
|
-
export const BATCHABLE = ['browser_computer', 'browser_find', 'browser_navigate', 'browser_read', 'browser_click', 'browser_screenshot', 'browser_capture'];
|
|
40
|
-
const MAX_ITEMS = 20;
|
|
41
|
-
|
|
42
|
-
export function makeBrowserFindTool({ projectId }) {
|
|
43
|
-
return tool(
|
|
44
|
-
'browser_find',
|
|
45
|
-
`Find elements on the current page by describing them: "search box",
|
|
46
|
-
"接受全部 按钮", "pricing link", "logo image". Returns up to 20 matches, each
|
|
47
|
-
with a ref (ref_N) you can hand to browser_computer (left_click / hover /
|
|
48
|
-
scroll_to with ref) and its viewport-center coordinate. Using refs is cheaper
|
|
49
|
-
and steadier than reading pixels off a screenshot.
|
|
50
|
-
|
|
51
|
-
Matching is LEXICAL, not semantic: your words are matched against the element's
|
|
52
|
-
visible text, aria-label, placeholder, alt, role and href. So describe the
|
|
53
|
-
element by the words that actually appear on it (button label, link text,
|
|
54
|
-
placeholder) plus a role word (button / link / input / image / 按钮 / 链接 /
|
|
55
|
-
输入框), and keep it short. No match → read the page (browser_read) to see the
|
|
56
|
-
real wording, or screenshot and click by coordinate.
|
|
57
|
-
|
|
58
|
-
Refs stay valid until the page navigates or re-renders; a stale ref comes back
|
|
59
|
-
as an error telling you to find again.`,
|
|
60
|
-
{
|
|
61
|
-
query: z.string().min(1).max(120)
|
|
62
|
-
.describe('What to find, in the words shown on the element plus an optional role word. E.g. "search", "Sign in button", "下一页 链接".'),
|
|
63
|
-
limit: z.number().int().min(1).max(20).optional().describe('Max matches to return (default 20).'),
|
|
64
|
-
},
|
|
65
|
-
async ({ query, limit }) => {
|
|
66
|
-
try {
|
|
67
|
-
return await withBrowser(projectId, async ({ page }) => {
|
|
68
|
-
const r = await findInPage(page, { query, limit: limit ?? 20 });
|
|
69
|
-
return asText(formatMatches(r, query).join('\n'));
|
|
70
|
-
});
|
|
71
|
-
} catch (err) {
|
|
72
|
-
return asText(`browser_find 失败:${err.message}`, true);
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
/**
|
|
79
|
-
* 通用 batch 工厂。
|
|
80
|
-
* @param {object} o
|
|
81
|
-
* @param {string} o.name 工具名
|
|
82
|
-
* @param {string} o.description 描述(调用方写;工厂只负责合同)
|
|
83
|
-
* @param {Array<{name:string, inputSchema:object, handler:Function}>} o.tools tool() 的返回值;
|
|
84
|
-
* 传进来而不是自己 import 构造:同一批 handler 只该有一份实例(projectId/ctx 都绑在里面)
|
|
85
|
-
* @param {string[]} o.batchable 允许的名字
|
|
86
|
-
* @param {{name:string, input:object}} [o.finalShot] 结尾补的那张"当前状态";不传就不补
|
|
87
|
-
*/
|
|
88
|
-
export function makeBatchTool({ name, description, tools, batchable, finalShot }) {
|
|
89
|
-
const byName = new Map(tools.filter(t => batchable.includes(t.name)).map(t => [t.name, t]));
|
|
90
|
-
const names = [...byName.keys()];
|
|
91
|
-
return tool(
|
|
92
|
-
name,
|
|
93
|
-
description,
|
|
94
|
-
{
|
|
95
|
-
actions: z.array(z.object({
|
|
96
|
-
name: z.string().describe(`Tool name: one of ${names.join(' | ')}.`),
|
|
97
|
-
input: z.record(z.string(), z.unknown()).optional().describe("That tool's input, same shape as calling it directly."),
|
|
98
|
-
})).min(1).max(MAX_ITEMS)
|
|
99
|
-
.describe(`1-${MAX_ITEMS} items, run in order. Each item: {"name": <tool>, "input": {...}}.`),
|
|
100
|
-
screenshotAfter: z.boolean().optional()
|
|
101
|
-
.describe('Append a screenshot of the resulting state at the end (default true). Set false for text-only sequences to save tokens.'),
|
|
102
|
-
},
|
|
103
|
-
async ({ actions, screenshotAfter }, extra) => {
|
|
104
|
-
const out = [];
|
|
105
|
-
const n = actions.length;
|
|
106
|
-
let failedAt = -1;
|
|
107
|
-
let lastHadImage = false;
|
|
108
|
-
for (let i = 0; i < n; i += 1) {
|
|
109
|
-
const { name: toolName, input } = actions[i];
|
|
110
|
-
const label = `[${i + 1}/${n}] ${toolName}${input?.action ? ` ${input.action}` : ''}`;
|
|
111
|
-
if (failedAt >= 0) { out.push({ type: 'text', text: `${label}: ${HALT_TEXT}` }); continue; }
|
|
112
|
-
const def = byName.get(toolName);
|
|
113
|
-
if (!def) {
|
|
114
|
-
failedAt = i;
|
|
115
|
-
out.push({ type: 'text', text: `${label}: Error: "${toolName}" is not batchable here (use one of ${names.join(', ')}).` });
|
|
116
|
-
continue;
|
|
117
|
-
}
|
|
118
|
-
const parsed = z.object(def.inputSchema).safeParse(input || {});
|
|
119
|
-
if (!parsed.success) {
|
|
120
|
-
failedAt = i;
|
|
121
|
-
const why = parsed.error.issues.map(is => `${is.path.join('.') || '(root)'}: ${is.message}`).join('; ');
|
|
122
|
-
out.push({ type: 'text', text: `${label}: Error: invalid input — ${why}` });
|
|
123
|
-
continue;
|
|
124
|
-
}
|
|
125
|
-
let r;
|
|
126
|
-
try { r = await def.handler(parsed.data, extra); } catch (err) {
|
|
127
|
-
r = asText(`Error: ${err.message.split('\n')[0]}`, true);
|
|
128
|
-
}
|
|
129
|
-
const blocks = Array.isArray(r?.content) ? r.content : [{ type: 'text', text: String(r) }];
|
|
130
|
-
lastHadImage = blocks.some(b => b.type === 'image');
|
|
131
|
-
// 第一块文本前面挂上 [i/n] 标签;没有文本块(纯图)就补一行标签再放图
|
|
132
|
-
if (!blocks.some(b => b.type === 'text')) out.push({ type: 'text', text: `${label}: done` });
|
|
133
|
-
let prefixed = false;
|
|
134
|
-
for (const b of blocks) {
|
|
135
|
-
if (b.type === 'text' && !prefixed) { out.push({ type: 'text', text: `${label}: ${b.text}` }); prefixed = true; }
|
|
136
|
-
else out.push(b);
|
|
137
|
-
}
|
|
138
|
-
if (r?.isError) failedAt = i;
|
|
139
|
-
}
|
|
140
|
-
if (finalShot && screenshotAfter !== false && !lastHadImage) {
|
|
141
|
-
const shotDef = byName.get(finalShot.name);
|
|
142
|
-
if (shotDef) {
|
|
143
|
-
try {
|
|
144
|
-
const s = await shotDef.handler(finalShot.input || {}, extra);
|
|
145
|
-
out.push({ type: 'text', text: `[after] current state${failedAt >= 0 ? ' (batch stopped early — look and replan)' : ''}:` });
|
|
146
|
-
for (const b of (s.content || [])) out.push(b);
|
|
147
|
-
} catch (err) {
|
|
148
|
-
out.push({ type: 'text', text: `[after] screenshot failed: ${err.message.split('\n')[0]}` });
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return { content: out, ...(failedAt >= 0 ? { isError: true } : {}) };
|
|
153
|
-
},
|
|
154
|
-
);
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
export function makeBrowserBatchTool({ tools }) {
|
|
158
|
-
const names = BATCHABLE.filter(n => tools.some(t => t.name === n));
|
|
159
|
-
return makeBatchTool({
|
|
160
|
-
name: 'browser_batch',
|
|
161
|
-
tools,
|
|
162
|
-
batchable: BATCHABLE,
|
|
163
|
-
finalShot: { name: 'browser_screenshot', input: {} },
|
|
164
|
-
description: `Run a sequence of browser tool calls in ONE round trip. Each item is
|
|
165
|
-
{name, input} where input is exactly what you would pass to that tool on its
|
|
166
|
-
own. Items run SEQUENTIALLY and the batch STOPS at the first error: the failed
|
|
167
|
-
item reports its error, every later item is answered "${HALT_TEXT}", and you
|
|
168
|
-
replan from there. A viewport screenshot is appended at the end (unless the
|
|
169
|
-
last item already produced an image, or screenshotAfter:false), so you always
|
|
170
|
-
see the resulting state.
|
|
171
|
-
|
|
172
|
-
Use this whenever you can predict two or more steps ahead — navigate, find the
|
|
173
|
-
field, click it, type, press Enter, look — instead of paying a model round trip
|
|
174
|
-
per action. The classic design-reference run is one batch: navigate to inner
|
|
175
|
-
page A → browser_capture palette+fonts → navigate to inner page B → capture →
|
|
176
|
-
navigate to C → capture. Six round trips become one, and every page's tokens
|
|
177
|
-
land in assets/references/web/ with provenance. Coordinates you write in a
|
|
178
|
-
batch refer to the screenshot you saw BEFORE the call; refs from an earlier
|
|
179
|
-
browser_find are fine as long as the page has not navigated. Batchable tools:
|
|
180
|
-
${names.join(', ')}. browser_batch cannot be nested.
|
|
181
|
-
Example: [{"name":"browser_find","input":{"query":"search"}},{"name":"browser_computer","input":{"action":"left_click","ref":"ref_1"}},{"name":"browser_computer","input":{"action":"type","text":"hello"}},{"name":"browser_computer","input":{"action":"key","text":"Enter"}}]`,
|
|
182
|
-
});
|
|
183
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/browse-find-batch.js — browser_find + 通用 batch 工厂 + browser_batch(2026-08-21)
|
|
3
|
+
*
|
|
4
|
+
* 两个都照 Anthropic browser use toolset / Claude in Chrome 的形状:
|
|
5
|
+
*
|
|
6
|
+
* - `browser_find`:自然语言描述 → 元素清单,每个带 `[ref_N]` 和视口中心坐标。
|
|
7
|
+
* ref 能直接喂给 browser_computer(ref 比像素稳:版面抖一下像素就偏,ref 还在)。
|
|
8
|
+
* 匹配是**词法**的(engine/browse/refs.js 头注释说了为什么),描述里明说。
|
|
9
|
+
*
|
|
10
|
+
* - `makeBatchTool`:一次调用串行跑多条动作,**遇错即停**,后面的条目一律回规格原话
|
|
11
|
+
* "Not executed: an earlier action in this turn failed.",结尾自动补一张截图(规格:
|
|
12
|
+
* "your application can attach its own observation on the last result to save a round
|
|
13
|
+
* trip")。省掉的是模型往返 —— 浏览和产物检查两条通道唯一真能砍掉的时间。
|
|
14
|
+
* browser_batch 和 artifact_batch 都是它的实例(同一份合同,别抄两遍)。
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ 不在 withBrowser / withSession 里套锁:registry 的 mutex 同键串行、不可重入。
|
|
17
|
+
* batch 逐条调用各工具自己的 handler,每条各自拿一次锁。
|
|
18
|
+
*
|
|
19
|
+
* 典型一趟(采参考站的设计 token):navigate 内页 A → capture palette+fonts →
|
|
20
|
+
* navigate 内页 B → capture → navigate 内页 C → capture → 结尾自动截一张。
|
|
21
|
+
* 以前这是 6 个回合,现在是 1 个。
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
25
|
+
import { z } from 'zod';
|
|
26
|
+
import { withBrowser } from '../../browse/registry.js';
|
|
27
|
+
import { findInPage, formatMatches } from '../../browse/refs.js';
|
|
28
|
+
|
|
29
|
+
const asText = (text, isError = false) => ({ content: [{ type: 'text', text }], ...(isError ? { isError: true } : {}) });
|
|
30
|
+
|
|
31
|
+
/** 规格原话(browser 版 halt 文案) */
|
|
32
|
+
export const HALT_TEXT = 'Not executed: an earlier action in this turn failed.';
|
|
33
|
+
/**
|
|
34
|
+
* browser_batch 能跑的工具。只有 request_help 不进来(它阻塞等人,一等两分钟)。
|
|
35
|
+
* capture **在里面**(用户 08-21 点的):采参考站的 token 本来就是"进内页 → 采 →
|
|
36
|
+
* 进下一页 → 再采"的多步活,一页一个回合太贵;一趟 batch 把三四页的调色板/字体
|
|
37
|
+
* 一起带回来才对味。
|
|
38
|
+
*/
|
|
39
|
+
export const BATCHABLE = ['browser_computer', 'browser_find', 'browser_navigate', 'browser_read', 'browser_click', 'browser_screenshot', 'browser_capture'];
|
|
40
|
+
const MAX_ITEMS = 20;
|
|
41
|
+
|
|
42
|
+
export function makeBrowserFindTool({ projectId }) {
|
|
43
|
+
return tool(
|
|
44
|
+
'browser_find',
|
|
45
|
+
`Find elements on the current page by describing them: "search box",
|
|
46
|
+
"接受全部 按钮", "pricing link", "logo image". Returns up to 20 matches, each
|
|
47
|
+
with a ref (ref_N) you can hand to browser_computer (left_click / hover /
|
|
48
|
+
scroll_to with ref) and its viewport-center coordinate. Using refs is cheaper
|
|
49
|
+
and steadier than reading pixels off a screenshot.
|
|
50
|
+
|
|
51
|
+
Matching is LEXICAL, not semantic: your words are matched against the element's
|
|
52
|
+
visible text, aria-label, placeholder, alt, role and href. So describe the
|
|
53
|
+
element by the words that actually appear on it (button label, link text,
|
|
54
|
+
placeholder) plus a role word (button / link / input / image / 按钮 / 链接 /
|
|
55
|
+
输入框), and keep it short. No match → read the page (browser_read) to see the
|
|
56
|
+
real wording, or screenshot and click by coordinate.
|
|
57
|
+
|
|
58
|
+
Refs stay valid until the page navigates or re-renders; a stale ref comes back
|
|
59
|
+
as an error telling you to find again.`,
|
|
60
|
+
{
|
|
61
|
+
query: z.string().min(1).max(120)
|
|
62
|
+
.describe('What to find, in the words shown on the element plus an optional role word. E.g. "search", "Sign in button", "下一页 链接".'),
|
|
63
|
+
limit: z.number().int().min(1).max(20).optional().describe('Max matches to return (default 20).'),
|
|
64
|
+
},
|
|
65
|
+
async ({ query, limit }) => {
|
|
66
|
+
try {
|
|
67
|
+
return await withBrowser(projectId, async ({ page }) => {
|
|
68
|
+
const r = await findInPage(page, { query, limit: limit ?? 20 });
|
|
69
|
+
return asText(formatMatches(r, query).join('\n'));
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
return asText(`browser_find 失败:${err.message}`, true);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* 通用 batch 工厂。
|
|
80
|
+
* @param {object} o
|
|
81
|
+
* @param {string} o.name 工具名
|
|
82
|
+
* @param {string} o.description 描述(调用方写;工厂只负责合同)
|
|
83
|
+
* @param {Array<{name:string, inputSchema:object, handler:Function}>} o.tools tool() 的返回值;
|
|
84
|
+
* 传进来而不是自己 import 构造:同一批 handler 只该有一份实例(projectId/ctx 都绑在里面)
|
|
85
|
+
* @param {string[]} o.batchable 允许的名字
|
|
86
|
+
* @param {{name:string, input:object}} [o.finalShot] 结尾补的那张"当前状态";不传就不补
|
|
87
|
+
*/
|
|
88
|
+
export function makeBatchTool({ name, description, tools, batchable, finalShot }) {
|
|
89
|
+
const byName = new Map(tools.filter(t => batchable.includes(t.name)).map(t => [t.name, t]));
|
|
90
|
+
const names = [...byName.keys()];
|
|
91
|
+
return tool(
|
|
92
|
+
name,
|
|
93
|
+
description,
|
|
94
|
+
{
|
|
95
|
+
actions: z.array(z.object({
|
|
96
|
+
name: z.string().describe(`Tool name: one of ${names.join(' | ')}.`),
|
|
97
|
+
input: z.record(z.string(), z.unknown()).optional().describe("That tool's input, same shape as calling it directly."),
|
|
98
|
+
})).min(1).max(MAX_ITEMS)
|
|
99
|
+
.describe(`1-${MAX_ITEMS} items, run in order. Each item: {"name": <tool>, "input": {...}}.`),
|
|
100
|
+
screenshotAfter: z.boolean().optional()
|
|
101
|
+
.describe('Append a screenshot of the resulting state at the end (default true). Set false for text-only sequences to save tokens.'),
|
|
102
|
+
},
|
|
103
|
+
async ({ actions, screenshotAfter }, extra) => {
|
|
104
|
+
const out = [];
|
|
105
|
+
const n = actions.length;
|
|
106
|
+
let failedAt = -1;
|
|
107
|
+
let lastHadImage = false;
|
|
108
|
+
for (let i = 0; i < n; i += 1) {
|
|
109
|
+
const { name: toolName, input } = actions[i];
|
|
110
|
+
const label = `[${i + 1}/${n}] ${toolName}${input?.action ? ` ${input.action}` : ''}`;
|
|
111
|
+
if (failedAt >= 0) { out.push({ type: 'text', text: `${label}: ${HALT_TEXT}` }); continue; }
|
|
112
|
+
const def = byName.get(toolName);
|
|
113
|
+
if (!def) {
|
|
114
|
+
failedAt = i;
|
|
115
|
+
out.push({ type: 'text', text: `${label}: Error: "${toolName}" is not batchable here (use one of ${names.join(', ')}).` });
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
const parsed = z.object(def.inputSchema).safeParse(input || {});
|
|
119
|
+
if (!parsed.success) {
|
|
120
|
+
failedAt = i;
|
|
121
|
+
const why = parsed.error.issues.map(is => `${is.path.join('.') || '(root)'}: ${is.message}`).join('; ');
|
|
122
|
+
out.push({ type: 'text', text: `${label}: Error: invalid input — ${why}` });
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
let r;
|
|
126
|
+
try { r = await def.handler(parsed.data, extra); } catch (err) {
|
|
127
|
+
r = asText(`Error: ${err.message.split('\n')[0]}`, true);
|
|
128
|
+
}
|
|
129
|
+
const blocks = Array.isArray(r?.content) ? r.content : [{ type: 'text', text: String(r) }];
|
|
130
|
+
lastHadImage = blocks.some(b => b.type === 'image');
|
|
131
|
+
// 第一块文本前面挂上 [i/n] 标签;没有文本块(纯图)就补一行标签再放图
|
|
132
|
+
if (!blocks.some(b => b.type === 'text')) out.push({ type: 'text', text: `${label}: done` });
|
|
133
|
+
let prefixed = false;
|
|
134
|
+
for (const b of blocks) {
|
|
135
|
+
if (b.type === 'text' && !prefixed) { out.push({ type: 'text', text: `${label}: ${b.text}` }); prefixed = true; }
|
|
136
|
+
else out.push(b);
|
|
137
|
+
}
|
|
138
|
+
if (r?.isError) failedAt = i;
|
|
139
|
+
}
|
|
140
|
+
if (finalShot && screenshotAfter !== false && !lastHadImage) {
|
|
141
|
+
const shotDef = byName.get(finalShot.name);
|
|
142
|
+
if (shotDef) {
|
|
143
|
+
try {
|
|
144
|
+
const s = await shotDef.handler(finalShot.input || {}, extra);
|
|
145
|
+
out.push({ type: 'text', text: `[after] current state${failedAt >= 0 ? ' (batch stopped early — look and replan)' : ''}:` });
|
|
146
|
+
for (const b of (s.content || [])) out.push(b);
|
|
147
|
+
} catch (err) {
|
|
148
|
+
out.push({ type: 'text', text: `[after] screenshot failed: ${err.message.split('\n')[0]}` });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return { content: out, ...(failedAt >= 0 ? { isError: true } : {}) };
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function makeBrowserBatchTool({ tools }) {
|
|
158
|
+
const names = BATCHABLE.filter(n => tools.some(t => t.name === n));
|
|
159
|
+
return makeBatchTool({
|
|
160
|
+
name: 'browser_batch',
|
|
161
|
+
tools,
|
|
162
|
+
batchable: BATCHABLE,
|
|
163
|
+
finalShot: { name: 'browser_screenshot', input: {} },
|
|
164
|
+
description: `Run a sequence of browser tool calls in ONE round trip. Each item is
|
|
165
|
+
{name, input} where input is exactly what you would pass to that tool on its
|
|
166
|
+
own. Items run SEQUENTIALLY and the batch STOPS at the first error: the failed
|
|
167
|
+
item reports its error, every later item is answered "${HALT_TEXT}", and you
|
|
168
|
+
replan from there. A viewport screenshot is appended at the end (unless the
|
|
169
|
+
last item already produced an image, or screenshotAfter:false), so you always
|
|
170
|
+
see the resulting state.
|
|
171
|
+
|
|
172
|
+
Use this whenever you can predict two or more steps ahead — navigate, find the
|
|
173
|
+
field, click it, type, press Enter, look — instead of paying a model round trip
|
|
174
|
+
per action. The classic design-reference run is one batch: navigate to inner
|
|
175
|
+
page A → browser_capture palette+fonts → navigate to inner page B → capture →
|
|
176
|
+
navigate to C → capture. Six round trips become one, and every page's tokens
|
|
177
|
+
land in assets/references/web/ with provenance. Coordinates you write in a
|
|
178
|
+
batch refer to the screenshot you saw BEFORE the call; refs from an earlier
|
|
179
|
+
browser_find are fine as long as the page has not navigated. Batchable tools:
|
|
180
|
+
${names.join(', ')}. browser_batch cannot be nested.
|
|
181
|
+
Example: [{"name":"browser_find","input":{"query":"search"}},{"name":"browser_computer","input":{"action":"left_click","ref":"ref_1"}},{"name":"browser_computer","input":{"action":"type","text":"hello"}},{"name":"browser_computer","input":{"action":"key","text":"Enter"}}]`,
|
|
182
|
+
});
|
|
183
|
+
}
|