@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,195 +1,195 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* mcp/tools/screenshot-url.js — screenshot_url MCP tool(2026-07-29)
|
|
3
|
-
*
|
|
4
|
-
* 对任意外部 URL 截图。诞生背景:explorer 找视觉参考只能 WebFetch 拿文本,
|
|
5
|
-
* 再用文字向主 agent 转述"这个站是深色的、图占主导"—— 找视觉参考却看不见
|
|
6
|
-
* 视觉。这个工具让 explorer / 主 agent 直接看到参考站长什么样。
|
|
7
|
-
*
|
|
8
|
-
* 安全:
|
|
9
|
-
* - 只放 http/https(file:// 会变成任意本地文件读取,硬拒)
|
|
10
|
-
* - 拒绝内网/环回/link-local 字面量(localhost / 127.* / 10.* / 172.16-31.* /
|
|
11
|
-
* 192.168.* / 169.254.* / *.local / [::1])—— explorer 会读不可信网页内容,
|
|
12
|
-
* prompt injection 不该能借它窥探内网服务。DNS rebinding 不在防御范围
|
|
13
|
-
* (截图只回图片,风险面已经很小)。
|
|
14
|
-
*
|
|
15
|
-
* 加载策略:外站经常永远到不了 networkidle(分析脚本长轮询),goto 超时不算
|
|
16
|
-
* 失败 —— 部分渲染的参考图也比纯文字转述强,超时记进 caption。
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
20
|
-
import { z } from 'zod';
|
|
21
|
-
import { attachPageDiagnostics, runBeforeShot, normalizeShot, FIDELITY_LAUNCH_ARGS, detectPaintTransform } from './helpers/shot-pipeline.js';
|
|
22
|
-
import { checkUrl, attachSsrfGuard } from '../../../lib/ssrf-guard.js';
|
|
23
|
-
import { denyText } from './browse.js';
|
|
24
|
-
import { startBrowseProxy } from '../../../lib/browse-proxy.js';
|
|
25
|
-
|
|
26
|
-
const RASTER_SCALE = 0.6;
|
|
27
|
-
const DEVICE_VIEWPORTS = {
|
|
28
|
-
desktop: { width: 1440, height: 900 },
|
|
29
|
-
tablet: { width: 834, height: 1112 },
|
|
30
|
-
mobile: { width: 390, height: 844 },
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
const PRIVATE_HOST_RE = new RegExp(
|
|
34
|
-
'^(localhost|0\\.0\\.0\\.0|127\\.|10\\.|192\\.168\\.|169\\.254\\.'
|
|
35
|
-
+ '|172\\.(1[6-9]|2[0-9]|3[01])\\.'
|
|
36
|
-
+ '|\\[::1\\]|\\[fc|\\[fd|\\[fe80)'
|
|
37
|
-
, 'i',
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
function validateUrl(raw) {
|
|
41
|
-
let u;
|
|
42
|
-
try {
|
|
43
|
-
u = new URL(raw);
|
|
44
|
-
} catch {
|
|
45
|
-
return { ok: false, message: `not a valid URL: ${raw}` };
|
|
46
|
-
}
|
|
47
|
-
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
48
|
-
return { ok: false, message: `only http/https URLs are allowed (got ${u.protocol})` };
|
|
49
|
-
}
|
|
50
|
-
// ⚠️ 这里**只做词法预筛**。真判据是 checkUrl(解析 DNS 按 IP 判)+ 页面上挂的
|
|
51
|
-
// CDP 闸(拦跳转与子资源)。留着这道是因为它便宜、能在解析之前挡掉最常见的字面量。
|
|
52
|
-
const host = u.hostname.toLowerCase();
|
|
53
|
-
if (PRIVATE_HOST_RE.test(host) || host.endsWith('.local')) {
|
|
54
|
-
return { ok: false, message: `refusing to screenshot private/internal address: ${host}` };
|
|
55
|
-
}
|
|
56
|
-
return { ok: true, url: u };
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @param {object} deps
|
|
61
|
-
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
62
|
-
*/
|
|
63
|
-
export function makeScreenshotUrlTool({ projectId, ctx } = {}) {
|
|
64
|
-
return tool(
|
|
65
|
-
'screenshot_url',
|
|
66
|
-
`Take a screenshot of any external web page (http/https) and return it as an
|
|
67
|
-
image you can see via vision. THE tool for gathering visual design references —
|
|
68
|
-
when researching how other sites handle layout, typography, color, or imagery,
|
|
69
|
-
look at them instead of reading their HTML and imagining.
|
|
70
|
-
|
|
71
|
-
- device: render at a real device width (desktop=1440, tablet=834, mobile=390)
|
|
72
|
-
- fullPage=true captures the whole scrollable page (auto-scrolls first so
|
|
73
|
-
lazy-loaded images and scroll reveals are triggered); default captures the
|
|
74
|
-
first viewport only — usually enough to judge a site's character, and much
|
|
75
|
-
cheaper in context
|
|
76
|
-
- The caption reports console errors / failed resources of the target page —
|
|
77
|
-
ignore those unless they explain a broken-looking render.
|
|
78
|
-
|
|
79
|
-
External pages can be slow; if the network never settles the shot is taken
|
|
80
|
-
anyway after 12s and the caption says so. Only http/https and public hosts.`,
|
|
81
|
-
{
|
|
82
|
-
url: z.string().describe('The http/https URL to screenshot'),
|
|
83
|
-
device: z
|
|
84
|
-
.enum(['desktop', 'tablet', 'mobile'])
|
|
85
|
-
.optional()
|
|
86
|
-
.describe('Viewport width preset: desktop=1440 (default), tablet=834, mobile=390'),
|
|
87
|
-
fullPage: z
|
|
88
|
-
.boolean()
|
|
89
|
-
.optional()
|
|
90
|
-
.describe('Capture the full scrollable page (auto-scrolls through it first to trigger lazyload). Default false = first viewport only, much cheaper.'),
|
|
91
|
-
detail: z
|
|
92
|
-
.enum(['normal', 'high'])
|
|
93
|
-
.optional()
|
|
94
|
-
.describe("Raster detail. 'normal' (default) = 0.6x pixels, enough for layout/palette judgment. 'high' = full resolution, only when you must read small text."),
|
|
95
|
-
},
|
|
96
|
-
async ({ url: rawUrl, device, fullPage, detail }) => {
|
|
97
|
-
const check = validateUrl(rawUrl);
|
|
98
|
-
if (!check.ok) {
|
|
99
|
-
return { content: [{ type: 'text', text: check.message }], isError: true };
|
|
100
|
-
}
|
|
101
|
-
// ⛔ 2026-08-18 补:上面那道 `validateUrl` 是**纯词法**的 —— 挡得住
|
|
102
|
-
// `127.0.0.1` 这种字面量,挡不住一个 DNS 解析到内网的公网域名,也不管 302。
|
|
103
|
-
// 新闸(lib/ssrf-guard.js)本来就是来替换它的,但上线那天**忘了接这个工具**,
|
|
104
|
-
// 于是它自己一直是个活着的 SSRF 洞。现在:按解析出的 IP 判 + 页面上挂 CDP 闸
|
|
105
|
-
// 拦跳转与子资源。
|
|
106
|
-
const pre = await checkUrl(check.url.href);
|
|
107
|
-
if (!pre.ok) {
|
|
108
|
-
// 拒因分种类说(DNS 死域名 ≠ 策略拦截;文案与线上地址提示同 browse 工具)
|
|
109
|
-
const tail = denyText(pre, projectId, check.url.href).join('\n');
|
|
110
|
-
return { content: [{ type: 'text', text: `refusing to screenshot: ${pre.reason}\n${tail}` }], isError: true };
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
const vp = DEVICE_VIEWPORTS[device || 'desktop'];
|
|
114
|
-
let browser;
|
|
115
|
-
try {
|
|
116
|
-
const { chromium } = await import('playwright');
|
|
117
|
-
// ⭐ 走同一个出网代理(2026-08-18 二次修)。CDP 那道闸看不见四类东西:
|
|
118
|
-
// WebSocket 握手、`<link rel=prefetch>`、`sendBeacon`、还没装闸的弹窗
|
|
119
|
-
// —— 四条都是攻出来的,而这个工具吃的正是**任意用户给的 URL**,同一批
|
|
120
|
-
// 绕过一字不改就能用在它身上。代理是所有出网的必经之路,且连的是自己
|
|
121
|
-
// 解析并验过的那个 IP(顺带根除 DNS 重绑定)。
|
|
122
|
-
// `bypass: ''`:默认会放过 loopback,那正是最要拦的。
|
|
123
|
-
const { port: proxyPort } = await startBrowseProxy();
|
|
124
|
-
browser = await chromium.launch({
|
|
125
|
-
headless: true,
|
|
126
|
-
args: FIDELITY_LAUNCH_ARGS,
|
|
127
|
-
proxy: { server: `http://127.0.0.1:${proxyPort}`, bypass: '' },
|
|
128
|
-
});
|
|
129
|
-
const rasterScale = detail === 'high' ? 1 : RASTER_SCALE;
|
|
130
|
-
const ctx = await browser.newContext({ viewport: vp, deviceScaleFactor: rasterScale, colorScheme: 'light' });
|
|
131
|
-
const guard = await attachSsrfGuard(ctx, undefined, { proxied: true });
|
|
132
|
-
const page = await ctx.newPage();
|
|
133
|
-
await guard.armPage(page); // ⭐ 必须 await 完才导航(竞态是攻出来的)
|
|
134
|
-
const diag = attachPageDiagnostics(page);
|
|
135
|
-
|
|
136
|
-
let gotoNote = null;
|
|
137
|
-
try {
|
|
138
|
-
await page.goto(check.url.href, { waitUntil: 'networkidle', timeout: 12000 });
|
|
139
|
-
} catch (err) {
|
|
140
|
-
// 超时(页面已部分渲染)→ 照截;真导航失败(DNS/refused)→ 报错
|
|
141
|
-
if (!/Timeout/i.test(String(err?.message))) {
|
|
142
|
-
return {
|
|
143
|
-
content: [{ type: 'text', text: `Failed to load ${rawUrl}: ${err?.message || err}` }],
|
|
144
|
-
isError: true,
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
gotoNote = 'network never settled (12s) — captured current render state';
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (fullPage) {
|
|
151
|
-
// 滚一遍触发 lazyload / scroll reveal,再回顶整页截
|
|
152
|
-
await runBeforeShot(page, 'scrollToBottom');
|
|
153
|
-
}
|
|
154
|
-
const buf = await page.screenshot({ fullPage: fullPage === true, type: 'png' });
|
|
155
|
-
|
|
156
|
-
try {
|
|
157
|
-
ctx?.emit?.({
|
|
158
|
-
type: 'run.screenshot_taken',
|
|
159
|
-
sizeBytes: buf.length,
|
|
160
|
-
viewport: vp,
|
|
161
|
-
mode: `url=${check.url.hostname}`,
|
|
162
|
-
});
|
|
163
|
-
} catch { /* emit fail-safe */ }
|
|
164
|
-
|
|
165
|
-
const paintNote = await detectPaintTransform(page);
|
|
166
|
-
const shot = await normalizeShot(buf);
|
|
167
|
-
const title = await page.title().catch(() => '');
|
|
168
|
-
const finalUrl = page.url();
|
|
169
|
-
const captionParts = [
|
|
170
|
-
`Screenshot of ${finalUrl}${title ? ` — "${title}"` : ''} (${device || 'desktop'} ${vp.width}x${vp.height} @${rasterScale}x, fullPage=${fullPage === true})`,
|
|
171
|
-
];
|
|
172
|
-
if (shot.note) captionParts.push(shot.note);
|
|
173
|
-
if (gotoNote) captionParts.push(gotoNote);
|
|
174
|
-
if (paintNote) captionParts.push(paintNote);
|
|
175
|
-
captionParts.push(diag.summary());
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
content: [
|
|
179
|
-
{ type: 'text', text: captionParts.join('\n') },
|
|
180
|
-
{ type: 'image', data: shot.data, mimeType: shot.mimeType },
|
|
181
|
-
],
|
|
182
|
-
};
|
|
183
|
-
} catch (err) {
|
|
184
|
-
return {
|
|
185
|
-
content: [{ type: 'text', text: `screenshot_url failed: ${err?.message || String(err)}` }],
|
|
186
|
-
isError: true,
|
|
187
|
-
};
|
|
188
|
-
} finally {
|
|
189
|
-
if (browser) {
|
|
190
|
-
try { await browser.close(); } catch { /* ignore */ }
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
},
|
|
194
|
-
);
|
|
195
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* mcp/tools/screenshot-url.js — screenshot_url MCP tool(2026-07-29)
|
|
3
|
+
*
|
|
4
|
+
* 对任意外部 URL 截图。诞生背景:explorer 找视觉参考只能 WebFetch 拿文本,
|
|
5
|
+
* 再用文字向主 agent 转述"这个站是深色的、图占主导"—— 找视觉参考却看不见
|
|
6
|
+
* 视觉。这个工具让 explorer / 主 agent 直接看到参考站长什么样。
|
|
7
|
+
*
|
|
8
|
+
* 安全:
|
|
9
|
+
* - 只放 http/https(file:// 会变成任意本地文件读取,硬拒)
|
|
10
|
+
* - 拒绝内网/环回/link-local 字面量(localhost / 127.* / 10.* / 172.16-31.* /
|
|
11
|
+
* 192.168.* / 169.254.* / *.local / [::1])—— explorer 会读不可信网页内容,
|
|
12
|
+
* prompt injection 不该能借它窥探内网服务。DNS rebinding 不在防御范围
|
|
13
|
+
* (截图只回图片,风险面已经很小)。
|
|
14
|
+
*
|
|
15
|
+
* 加载策略:外站经常永远到不了 networkidle(分析脚本长轮询),goto 超时不算
|
|
16
|
+
* 失败 —— 部分渲染的参考图也比纯文字转述强,超时记进 caption。
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
20
|
+
import { z } from 'zod';
|
|
21
|
+
import { attachPageDiagnostics, runBeforeShot, normalizeShot, FIDELITY_LAUNCH_ARGS, detectPaintTransform } from './helpers/shot-pipeline.js';
|
|
22
|
+
import { checkUrl, attachSsrfGuard } from '../../../lib/ssrf-guard.js';
|
|
23
|
+
import { denyText } from './browse.js';
|
|
24
|
+
import { startBrowseProxy } from '../../../lib/browse-proxy.js';
|
|
25
|
+
|
|
26
|
+
const RASTER_SCALE = 0.6;
|
|
27
|
+
const DEVICE_VIEWPORTS = {
|
|
28
|
+
desktop: { width: 1440, height: 900 },
|
|
29
|
+
tablet: { width: 834, height: 1112 },
|
|
30
|
+
mobile: { width: 390, height: 844 },
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const PRIVATE_HOST_RE = new RegExp(
|
|
34
|
+
'^(localhost|0\\.0\\.0\\.0|127\\.|10\\.|192\\.168\\.|169\\.254\\.'
|
|
35
|
+
+ '|172\\.(1[6-9]|2[0-9]|3[01])\\.'
|
|
36
|
+
+ '|\\[::1\\]|\\[fc|\\[fd|\\[fe80)'
|
|
37
|
+
, 'i',
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
function validateUrl(raw) {
|
|
41
|
+
let u;
|
|
42
|
+
try {
|
|
43
|
+
u = new URL(raw);
|
|
44
|
+
} catch {
|
|
45
|
+
return { ok: false, message: `not a valid URL: ${raw}` };
|
|
46
|
+
}
|
|
47
|
+
if (u.protocol !== 'http:' && u.protocol !== 'https:') {
|
|
48
|
+
return { ok: false, message: `only http/https URLs are allowed (got ${u.protocol})` };
|
|
49
|
+
}
|
|
50
|
+
// ⚠️ 这里**只做词法预筛**。真判据是 checkUrl(解析 DNS 按 IP 判)+ 页面上挂的
|
|
51
|
+
// CDP 闸(拦跳转与子资源)。留着这道是因为它便宜、能在解析之前挡掉最常见的字面量。
|
|
52
|
+
const host = u.hostname.toLowerCase();
|
|
53
|
+
if (PRIVATE_HOST_RE.test(host) || host.endsWith('.local')) {
|
|
54
|
+
return { ok: false, message: `refusing to screenshot private/internal address: ${host}` };
|
|
55
|
+
}
|
|
56
|
+
return { ok: true, url: u };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {object} deps
|
|
61
|
+
* @param {import('../../agent/context.js').AgentContext} [deps.ctx]
|
|
62
|
+
*/
|
|
63
|
+
export function makeScreenshotUrlTool({ projectId, ctx } = {}) {
|
|
64
|
+
return tool(
|
|
65
|
+
'screenshot_url',
|
|
66
|
+
`Take a screenshot of any external web page (http/https) and return it as an
|
|
67
|
+
image you can see via vision. THE tool for gathering visual design references —
|
|
68
|
+
when researching how other sites handle layout, typography, color, or imagery,
|
|
69
|
+
look at them instead of reading their HTML and imagining.
|
|
70
|
+
|
|
71
|
+
- device: render at a real device width (desktop=1440, tablet=834, mobile=390)
|
|
72
|
+
- fullPage=true captures the whole scrollable page (auto-scrolls first so
|
|
73
|
+
lazy-loaded images and scroll reveals are triggered); default captures the
|
|
74
|
+
first viewport only — usually enough to judge a site's character, and much
|
|
75
|
+
cheaper in context
|
|
76
|
+
- The caption reports console errors / failed resources of the target page —
|
|
77
|
+
ignore those unless they explain a broken-looking render.
|
|
78
|
+
|
|
79
|
+
External pages can be slow; if the network never settles the shot is taken
|
|
80
|
+
anyway after 12s and the caption says so. Only http/https and public hosts.`,
|
|
81
|
+
{
|
|
82
|
+
url: z.string().describe('The http/https URL to screenshot'),
|
|
83
|
+
device: z
|
|
84
|
+
.enum(['desktop', 'tablet', 'mobile'])
|
|
85
|
+
.optional()
|
|
86
|
+
.describe('Viewport width preset: desktop=1440 (default), tablet=834, mobile=390'),
|
|
87
|
+
fullPage: z
|
|
88
|
+
.boolean()
|
|
89
|
+
.optional()
|
|
90
|
+
.describe('Capture the full scrollable page (auto-scrolls through it first to trigger lazyload). Default false = first viewport only, much cheaper.'),
|
|
91
|
+
detail: z
|
|
92
|
+
.enum(['normal', 'high'])
|
|
93
|
+
.optional()
|
|
94
|
+
.describe("Raster detail. 'normal' (default) = 0.6x pixels, enough for layout/palette judgment. 'high' = full resolution, only when you must read small text."),
|
|
95
|
+
},
|
|
96
|
+
async ({ url: rawUrl, device, fullPage, detail }) => {
|
|
97
|
+
const check = validateUrl(rawUrl);
|
|
98
|
+
if (!check.ok) {
|
|
99
|
+
return { content: [{ type: 'text', text: check.message }], isError: true };
|
|
100
|
+
}
|
|
101
|
+
// ⛔ 2026-08-18 补:上面那道 `validateUrl` 是**纯词法**的 —— 挡得住
|
|
102
|
+
// `127.0.0.1` 这种字面量,挡不住一个 DNS 解析到内网的公网域名,也不管 302。
|
|
103
|
+
// 新闸(lib/ssrf-guard.js)本来就是来替换它的,但上线那天**忘了接这个工具**,
|
|
104
|
+
// 于是它自己一直是个活着的 SSRF 洞。现在:按解析出的 IP 判 + 页面上挂 CDP 闸
|
|
105
|
+
// 拦跳转与子资源。
|
|
106
|
+
const pre = await checkUrl(check.url.href);
|
|
107
|
+
if (!pre.ok) {
|
|
108
|
+
// 拒因分种类说(DNS 死域名 ≠ 策略拦截;文案与线上地址提示同 browse 工具)
|
|
109
|
+
const tail = denyText(pre, projectId, check.url.href).join('\n');
|
|
110
|
+
return { content: [{ type: 'text', text: `refusing to screenshot: ${pre.reason}\n${tail}` }], isError: true };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const vp = DEVICE_VIEWPORTS[device || 'desktop'];
|
|
114
|
+
let browser;
|
|
115
|
+
try {
|
|
116
|
+
const { chromium } = await import('playwright');
|
|
117
|
+
// ⭐ 走同一个出网代理(2026-08-18 二次修)。CDP 那道闸看不见四类东西:
|
|
118
|
+
// WebSocket 握手、`<link rel=prefetch>`、`sendBeacon`、还没装闸的弹窗
|
|
119
|
+
// —— 四条都是攻出来的,而这个工具吃的正是**任意用户给的 URL**,同一批
|
|
120
|
+
// 绕过一字不改就能用在它身上。代理是所有出网的必经之路,且连的是自己
|
|
121
|
+
// 解析并验过的那个 IP(顺带根除 DNS 重绑定)。
|
|
122
|
+
// `bypass: ''`:默认会放过 loopback,那正是最要拦的。
|
|
123
|
+
const { port: proxyPort } = await startBrowseProxy();
|
|
124
|
+
browser = await chromium.launch({
|
|
125
|
+
headless: true,
|
|
126
|
+
args: FIDELITY_LAUNCH_ARGS,
|
|
127
|
+
proxy: { server: `http://127.0.0.1:${proxyPort}`, bypass: '' },
|
|
128
|
+
});
|
|
129
|
+
const rasterScale = detail === 'high' ? 1 : RASTER_SCALE;
|
|
130
|
+
const ctx = await browser.newContext({ viewport: vp, deviceScaleFactor: rasterScale, colorScheme: 'light' });
|
|
131
|
+
const guard = await attachSsrfGuard(ctx, undefined, { proxied: true });
|
|
132
|
+
const page = await ctx.newPage();
|
|
133
|
+
await guard.armPage(page); // ⭐ 必须 await 完才导航(竞态是攻出来的)
|
|
134
|
+
const diag = attachPageDiagnostics(page);
|
|
135
|
+
|
|
136
|
+
let gotoNote = null;
|
|
137
|
+
try {
|
|
138
|
+
await page.goto(check.url.href, { waitUntil: 'networkidle', timeout: 12000 });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// 超时(页面已部分渲染)→ 照截;真导航失败(DNS/refused)→ 报错
|
|
141
|
+
if (!/Timeout/i.test(String(err?.message))) {
|
|
142
|
+
return {
|
|
143
|
+
content: [{ type: 'text', text: `Failed to load ${rawUrl}: ${err?.message || err}` }],
|
|
144
|
+
isError: true,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
gotoNote = 'network never settled (12s) — captured current render state';
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (fullPage) {
|
|
151
|
+
// 滚一遍触发 lazyload / scroll reveal,再回顶整页截
|
|
152
|
+
await runBeforeShot(page, 'scrollToBottom');
|
|
153
|
+
}
|
|
154
|
+
const buf = await page.screenshot({ fullPage: fullPage === true, type: 'png' });
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
ctx?.emit?.({
|
|
158
|
+
type: 'run.screenshot_taken',
|
|
159
|
+
sizeBytes: buf.length,
|
|
160
|
+
viewport: vp,
|
|
161
|
+
mode: `url=${check.url.hostname}`,
|
|
162
|
+
});
|
|
163
|
+
} catch { /* emit fail-safe */ }
|
|
164
|
+
|
|
165
|
+
const paintNote = await detectPaintTransform(page);
|
|
166
|
+
const shot = await normalizeShot(buf);
|
|
167
|
+
const title = await page.title().catch(() => '');
|
|
168
|
+
const finalUrl = page.url();
|
|
169
|
+
const captionParts = [
|
|
170
|
+
`Screenshot of ${finalUrl}${title ? ` — "${title}"` : ''} (${device || 'desktop'} ${vp.width}x${vp.height} @${rasterScale}x, fullPage=${fullPage === true})`,
|
|
171
|
+
];
|
|
172
|
+
if (shot.note) captionParts.push(shot.note);
|
|
173
|
+
if (gotoNote) captionParts.push(gotoNote);
|
|
174
|
+
if (paintNote) captionParts.push(paintNote);
|
|
175
|
+
captionParts.push(diag.summary());
|
|
176
|
+
|
|
177
|
+
return {
|
|
178
|
+
content: [
|
|
179
|
+
{ type: 'text', text: captionParts.join('\n') },
|
|
180
|
+
{ type: 'image', data: shot.data, mimeType: shot.mimeType },
|
|
181
|
+
],
|
|
182
|
+
};
|
|
183
|
+
} catch (err) {
|
|
184
|
+
return {
|
|
185
|
+
content: [{ type: 'text', text: `screenshot_url failed: ${err?.message || String(err)}` }],
|
|
186
|
+
isError: true,
|
|
187
|
+
};
|
|
188
|
+
} finally {
|
|
189
|
+
if (browser) {
|
|
190
|
+
try { await browser.close(); } catch { /* ignore */ }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
}
|