mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-21 15:55:50 +00:00
feat: 添加 builtin-tools 增强与测试覆盖
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,19 +9,11 @@ const inputSchema = lazySchema(() =>
|
||||
z.strictObject({
|
||||
url: z
|
||||
.string()
|
||||
.describe('URL to navigate to in the browser.'),
|
||||
.describe('URL to fetch and extract content from.'),
|
||||
action: z
|
||||
.enum(['navigate', 'screenshot', 'click', 'type', 'scroll'])
|
||||
.enum(['navigate', 'screenshot'])
|
||||
.optional()
|
||||
.describe('Browser action to perform. Defaults to "navigate".'),
|
||||
selector: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('CSS selector for click/type actions.'),
|
||||
text: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Text to type when action is "type".'),
|
||||
.describe('Action to perform. "navigate" fetches page content (default). "screenshot" returns a text snapshot of the page.'),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
@@ -45,16 +37,24 @@ export const WebBrowserTool = buildTool({
|
||||
},
|
||||
|
||||
async description() {
|
||||
return 'Browse the web using an embedded browser'
|
||||
return 'Fetch and read web page content via HTTP'
|
||||
},
|
||||
async prompt() {
|
||||
return `Open and interact with web pages in an embedded browser. Supports navigation, screenshots, clicking, typing, and scrolling.
|
||||
return `Fetch web pages via HTTP and extract their text content. This is a lightweight browser tool (HTTP fetch, not a full browser engine).
|
||||
|
||||
Supported actions:
|
||||
- navigate: Fetch a URL and extract page title + text content
|
||||
- screenshot: Same as navigate (returns text snapshot, not a visual screenshot)
|
||||
|
||||
Limitations:
|
||||
- No JavaScript execution — only sees server-rendered HTML
|
||||
- click/type/scroll require a full browser runtime (not available)
|
||||
- For full browser interaction, use the Claude-in-Chrome MCP tools instead
|
||||
|
||||
Use this for:
|
||||
- Viewing web pages and their content
|
||||
- Taking screenshots of UI
|
||||
- Interacting with web applications
|
||||
- Testing web endpoints with full browser rendering`
|
||||
- Reading web page content and documentation
|
||||
- Checking API endpoints that return HTML
|
||||
- Quick page title/content extraction`
|
||||
},
|
||||
|
||||
isConcurrencySafe() {
|
||||
@@ -85,12 +85,84 @@ Use this for:
|
||||
},
|
||||
|
||||
async call(input: BrowserInput) {
|
||||
// Browser integration requires the WEB_BROWSER_TOOL runtime (Bun WebView).
|
||||
const action = input.action ?? 'navigate'
|
||||
|
||||
if (action === 'navigate' || action === 'screenshot') {
|
||||
// Fetch the page content via HTTP
|
||||
try {
|
||||
const response = await fetch(input.url, {
|
||||
headers: {
|
||||
'User-Agent':
|
||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
Accept:
|
||||
'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
|
||||
},
|
||||
redirect: 'follow',
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
return {
|
||||
data: {
|
||||
title: `HTTP ${response.status}`,
|
||||
url: input.url,
|
||||
content: `Error: ${response.status} ${response.statusText}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const html = await response.text()
|
||||
|
||||
// Extract title
|
||||
const titleMatch = html.match(/<title[^>]*>([^<]*)<\/title>/i)
|
||||
const title = titleMatch?.[1]?.trim() ?? ''
|
||||
|
||||
// Extract text content (strip HTML tags, scripts, styles)
|
||||
let textContent = html
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, '')
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, '')
|
||||
.replace(/<[^>]+>/g, ' ')
|
||||
.replace(/\s+/g, ' ')
|
||||
.trim()
|
||||
|
||||
// Truncate to reasonable size
|
||||
if (textContent.length > 50_000) {
|
||||
textContent = textContent.slice(0, 50_000) + '\n[truncated]'
|
||||
}
|
||||
|
||||
if (action === 'screenshot') {
|
||||
return {
|
||||
data: {
|
||||
title,
|
||||
url: response.url,
|
||||
content: `[Text snapshot — visual screenshots require Chrome browser tools]\n\n${textContent}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
data: {
|
||||
title,
|
||||
url: response.url,
|
||||
content: textContent,
|
||||
},
|
||||
}
|
||||
} catch (err) {
|
||||
return {
|
||||
data: {
|
||||
title: 'Error',
|
||||
url: input.url,
|
||||
content: `Failed to fetch: ${err instanceof Error ? err.message : String(err)}`,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Unreachable — schema only allows navigate/screenshot
|
||||
return {
|
||||
data: {
|
||||
title: '',
|
||||
url: input.url,
|
||||
content: 'Web browser requires the WEB_BROWSER_TOOL runtime.',
|
||||
content: `Unknown action "${action}".`,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { describe, test, expect, beforeAll, afterAll } from 'bun:test'
|
||||
|
||||
// Mock fetch directly — avoids flaky dependency on external hosts AND
|
||||
// pollution by other tests that call setGlobalDispatcher (proxy agents make
|
||||
// localhost fetches return 500 in the full-suite run).
|
||||
const realFetch = globalThis.fetch
|
||||
|
||||
beforeAll(() => {
|
||||
globalThis.fetch = (async (
|
||||
input: string | URL | Request,
|
||||
_init?: RequestInit,
|
||||
) => {
|
||||
const url = typeof input === 'string' ? input : input.toString()
|
||||
if (url === 'not-a-url' || !url.startsWith('http')) {
|
||||
throw new TypeError('Failed to fetch')
|
||||
}
|
||||
const body =
|
||||
'<!doctype html><html><head><title>Example Domain</title></head>' +
|
||||
'<body><h1>Example Domain</h1><p>Sample content.</p></body></html>'
|
||||
const res = new Response(body, {
|
||||
status: 200,
|
||||
headers: { 'content-type': 'text/html' },
|
||||
})
|
||||
// Make response.url match the request URL so tests can assert on it.
|
||||
Object.defineProperty(res, 'url', { value: url, configurable: true })
|
||||
return res
|
||||
}) as typeof fetch
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
globalThis.fetch = realFetch
|
||||
})
|
||||
|
||||
describe('WebBrowserTool', () => {
|
||||
test('tool exports and metadata', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
expect(WebBrowserTool).toBeDefined()
|
||||
expect(WebBrowserTool.name).toBe('WebBrowser')
|
||||
expect(typeof WebBrowserTool.call).toBe('function')
|
||||
expect(WebBrowserTool.userFacingName()).toBe('Browser')
|
||||
expect(WebBrowserTool.isReadOnly()).toBe(true)
|
||||
})
|
||||
|
||||
test('description reflects browser-lite', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
const desc = await WebBrowserTool.description()
|
||||
expect(desc).toContain('HTTP')
|
||||
expect(desc).not.toContain('embedded browser')
|
||||
})
|
||||
|
||||
test('prompt mentions limitations', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
const prompt = await WebBrowserTool.prompt()
|
||||
expect(prompt).toContain('Limitations')
|
||||
expect(prompt).toContain('No JavaScript')
|
||||
expect(prompt).toContain('Claude-in-Chrome')
|
||||
})
|
||||
|
||||
test('navigate fetches URL', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
const result = await WebBrowserTool.call({
|
||||
url: 'https://example.com',
|
||||
} as any)
|
||||
expect(result.data.title).toBe('Example Domain')
|
||||
expect(result.data.url).toContain('example.com')
|
||||
expect(result.data.content).toContain('Example Domain')
|
||||
}, 15000)
|
||||
|
||||
test('screenshot returns text snapshot', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
const result = await WebBrowserTool.call({
|
||||
url: 'https://example.com',
|
||||
action: 'screenshot',
|
||||
} as any)
|
||||
expect(result.data.content).toContain('Text snapshot')
|
||||
expect(result.data.content).toContain('Example Domain')
|
||||
}, 15000)
|
||||
|
||||
test('schema only allows navigate and screenshot', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
const schema = WebBrowserTool.inputSchema
|
||||
const parseResult = schema.safeParse({
|
||||
url: 'https://example.com',
|
||||
action: 'click',
|
||||
})
|
||||
expect(parseResult.success).toBe(false)
|
||||
})
|
||||
|
||||
test('invalid URL returns error', async () => {
|
||||
const { WebBrowserTool } = await import('../WebBrowserTool.js')
|
||||
const result = await WebBrowserTool.call({ url: 'not-a-url' } as any)
|
||||
expect(result.data.content).toContain('Failed to fetch')
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user