mirror of
https://github.com/claude-code-best/claude-code.git
synced 2026-06-22 00:05:51 +00:00
style: 完成所有文件的lint
This commit is contained in:
@@ -1,32 +1,26 @@
|
||||
import React from 'react'
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js'
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js'
|
||||
import { Box, Text } from '@anthropic/ink'
|
||||
import type { ProgressMessage } from 'src/types/message.js'
|
||||
import { truncate } from 'src/utils/format.js'
|
||||
import type {
|
||||
Output,
|
||||
SearchResult,
|
||||
WebSearchProgress,
|
||||
} from './WebSearchTool.js'
|
||||
import React from 'react';
|
||||
import { MessageResponse } from 'src/components/MessageResponse.js';
|
||||
import { TOOL_SUMMARY_MAX_LENGTH } from 'src/constants/toolLimits.js';
|
||||
import { Box, Text } from '@anthropic/ink';
|
||||
import type { ProgressMessage } from 'src/types/message.js';
|
||||
import { truncate } from 'src/utils/format.js';
|
||||
import type { Output, SearchResult, WebSearchProgress } from './WebSearchTool.js';
|
||||
|
||||
function getSearchSummary(
|
||||
results: (SearchResult | string | null | undefined)[],
|
||||
): {
|
||||
searchCount: number
|
||||
totalResultCount: number
|
||||
function getSearchSummary(results: (SearchResult | string | null | undefined)[]): {
|
||||
searchCount: number;
|
||||
totalResultCount: number;
|
||||
} {
|
||||
let searchCount = 0
|
||||
let totalResultCount = 0
|
||||
let searchCount = 0;
|
||||
let totalResultCount = 0;
|
||||
|
||||
for (const result of results) {
|
||||
if (result != null && typeof result !== 'string') {
|
||||
searchCount++
|
||||
totalResultCount += result.content?.length ?? 0
|
||||
searchCount++;
|
||||
totalResultCount += result.content?.length ?? 0;
|
||||
}
|
||||
}
|
||||
|
||||
return { searchCount, totalResultCount }
|
||||
return { searchCount, totalResultCount };
|
||||
}
|
||||
|
||||
export function renderToolUseMessage(
|
||||
@@ -35,48 +29,46 @@ export function renderToolUseMessage(
|
||||
allowed_domains,
|
||||
blocked_domains,
|
||||
}: Partial<{
|
||||
query: string
|
||||
allowed_domains?: string[]
|
||||
blocked_domains?: string[]
|
||||
query: string;
|
||||
allowed_domains?: string[];
|
||||
blocked_domains?: string[];
|
||||
}>,
|
||||
{ verbose }: { verbose: boolean },
|
||||
): React.ReactNode {
|
||||
if (!query) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
let message = ''
|
||||
let message = '';
|
||||
|
||||
if (query) {
|
||||
message += `"${query}"`
|
||||
message += `"${query}"`;
|
||||
}
|
||||
|
||||
if (verbose) {
|
||||
if (allowed_domains && allowed_domains.length > 0) {
|
||||
message += `, only allowing domains: ${allowed_domains.join(', ')}`
|
||||
message += `, only allowing domains: ${allowed_domains.join(', ')}`;
|
||||
}
|
||||
|
||||
if (blocked_domains && blocked_domains.length > 0) {
|
||||
message += `, blocking domains: ${blocked_domains.join(', ')}`
|
||||
message += `, blocking domains: ${blocked_domains.join(', ')}`;
|
||||
}
|
||||
}
|
||||
|
||||
return message
|
||||
return message;
|
||||
}
|
||||
|
||||
export function renderToolUseProgressMessage(
|
||||
progressMessages: ProgressMessage<WebSearchProgress>[],
|
||||
): React.ReactNode {
|
||||
export function renderToolUseProgressMessage(progressMessages: ProgressMessage<WebSearchProgress>[]): React.ReactNode {
|
||||
if (progressMessages.length === 0) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const lastProgress = progressMessages[progressMessages.length - 1]
|
||||
const lastProgress = progressMessages[progressMessages.length - 1];
|
||||
if (!lastProgress?.data) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
|
||||
const data = lastProgress.data
|
||||
const data = lastProgress.data;
|
||||
|
||||
switch (data.type) {
|
||||
case 'query_update':
|
||||
@@ -84,7 +76,7 @@ export function renderToolUseProgressMessage(
|
||||
<MessageResponse>
|
||||
<Text dimColor>Searching: {data.query}</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
case 'search_results_received':
|
||||
return (
|
||||
<MessageResponse>
|
||||
@@ -92,18 +84,18 @@ export function renderToolUseProgressMessage(
|
||||
Found {data.resultCount} results for "{data.query}"
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
)
|
||||
);
|
||||
default:
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function renderToolResultMessage(output: Output): React.ReactNode {
|
||||
const { searchCount } = getSearchSummary(output.results ?? [])
|
||||
const { searchCount } = getSearchSummary(output.results ?? []);
|
||||
const timeDisplay =
|
||||
output.durationSeconds >= 1
|
||||
? `${Math.round(output.durationSeconds)}s`
|
||||
: `${Math.round(output.durationSeconds * 1000)}ms`
|
||||
: `${Math.round(output.durationSeconds * 1000)}ms`;
|
||||
|
||||
return (
|
||||
<Box justifyContent="space-between" width="100%">
|
||||
@@ -114,14 +106,12 @@ export function renderToolResultMessage(output: Output): React.ReactNode {
|
||||
</Text>
|
||||
</MessageResponse>
|
||||
</Box>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export function getToolUseSummary(
|
||||
input: Partial<{ query: string }> | undefined,
|
||||
): string | null {
|
||||
export function getToolUseSummary(input: Partial<{ query: string }> | undefined): string | null {
|
||||
if (!input?.query) {
|
||||
return null
|
||||
return null;
|
||||
}
|
||||
return truncate(input.query, TOOL_SUMMARY_MAX_LENGTH)
|
||||
return truncate(input.query, TOOL_SUMMARY_MAX_LENGTH);
|
||||
}
|
||||
|
||||
@@ -42,7 +42,9 @@ const inputSchema = lazySchema(() =>
|
||||
context_max_characters: z
|
||||
.number()
|
||||
.optional()
|
||||
.describe('Maximum characters for context string optimized for LLMs (default: 10000)'),
|
||||
.describe(
|
||||
'Maximum characters for context string optimized for LLMs (default: 10000)',
|
||||
),
|
||||
}),
|
||||
)
|
||||
type InputSchema = ReturnType<typeof inputSchema>
|
||||
@@ -51,7 +53,10 @@ const searchResultSchema = lazySchema(() => {
|
||||
const searchHitSchema = z.object({
|
||||
title: z.string().describe('The title of the search result'),
|
||||
url: z.string().describe('The URL of the search result'),
|
||||
snippet: z.string().optional().describe('A short description of the search result'),
|
||||
snippet: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('A short description of the search result'),
|
||||
})
|
||||
|
||||
return z.object({
|
||||
@@ -192,7 +197,11 @@ export const WebSearchTool = buildTool({
|
||||
if (adapterResults.length > 0) {
|
||||
results.push({
|
||||
tool_use_id: 'adapter-search-1',
|
||||
content: adapterResults.map(r => ({ title: r.title, url: r.url, snippet: r.snippet })),
|
||||
content: adapterResults.map(r => ({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
snippet: r.snippet,
|
||||
})),
|
||||
})
|
||||
} else {
|
||||
results.push('No search results found.')
|
||||
|
||||
@@ -24,7 +24,7 @@ async function main() {
|
||||
const startTime = Date.now()
|
||||
|
||||
const results = await adapter.search(query, {
|
||||
onProgress: (p) => {
|
||||
onProgress: p => {
|
||||
if (p.type === 'query_update') {
|
||||
console.log(` → Query sent: ${p.query}`)
|
||||
}
|
||||
@@ -51,7 +51,9 @@ async function main() {
|
||||
console.log(` ${r.url}`)
|
||||
if (r.snippet) {
|
||||
const snippet = r.snippet.replace(/\n/g, ' ')
|
||||
console.log(` ${snippet.slice(0, 150)}${snippet.length > 150 ? '…' : ''}`)
|
||||
console.log(
|
||||
` ${snippet.slice(0, 150)}${snippet.length > 150 ? '…' : ''}`,
|
||||
)
|
||||
}
|
||||
console.log()
|
||||
}
|
||||
@@ -76,7 +78,7 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((e) => {
|
||||
main().catch(e => {
|
||||
console.error('❌ Fatal error:', e)
|
||||
process.exit(1)
|
||||
})
|
||||
|
||||
@@ -2,9 +2,13 @@ import { describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
const _abortMock = () => ({
|
||||
AbortError: class AbortError extends Error {
|
||||
constructor(message?: string) { super(message); this.name = 'AbortError' }
|
||||
constructor(message?: string) {
|
||||
super(message)
|
||||
this.name = 'AbortError'
|
||||
}
|
||||
},
|
||||
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
|
||||
isAbortError: (e: unknown) =>
|
||||
e instanceof Error && (e as Error).name === 'AbortError',
|
||||
})
|
||||
mock.module('src/utils/errors.js', _abortMock)
|
||||
mock.module('src/utils/errors', _abortMock)
|
||||
@@ -45,7 +49,9 @@ describe('decodeHtmlEntities', () => {
|
||||
})
|
||||
|
||||
test('handles mixed entities in one string', () => {
|
||||
expect(decodeHtmlEntities('<a href="x">')).toBe('<a\u00A0href="x">')
|
||||
expect(decodeHtmlEntities('<a href="x">')).toBe(
|
||||
'<a\u00A0href="x">',
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -44,7 +44,11 @@ describe('extractBraveResults', () => {
|
||||
})
|
||||
|
||||
expect(results).toEqual([
|
||||
{ title: 'Generic', url: 'https://example.com/generic', snippet: undefined },
|
||||
{
|
||||
title: 'Generic',
|
||||
url: 'https://example.com/generic',
|
||||
snippet: undefined,
|
||||
},
|
||||
{ title: 'POI', url: 'https://maps.example.com/poi', snippet: undefined },
|
||||
{ title: 'Map', url: 'https://maps.example.com/map', snippet: undefined },
|
||||
])
|
||||
|
||||
@@ -5,9 +5,13 @@ import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
|
||||
// import in createAdapter() never needs to resolve the alias at runtime.
|
||||
const _abortMock = () => ({
|
||||
AbortError: class AbortError extends Error {
|
||||
constructor(message?: string) { super(message); this.name = 'AbortError' }
|
||||
constructor(message?: string) {
|
||||
super(message)
|
||||
this.name = 'AbortError'
|
||||
}
|
||||
},
|
||||
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
|
||||
isAbortError: (e: unknown) =>
|
||||
e instanceof Error && (e as Error).name === 'AbortError',
|
||||
})
|
||||
mock.module('src/utils/errors.js', _abortMock)
|
||||
mock.module('src/utils/errors', _abortMock)
|
||||
|
||||
@@ -2,9 +2,13 @@ import { afterEach, describe, expect, mock, test } from 'bun:test'
|
||||
|
||||
const _abortMock = () => ({
|
||||
AbortError: class AbortError extends Error {
|
||||
constructor(message?: string) { super(message); this.name = 'AbortError' }
|
||||
constructor(message?: string) {
|
||||
super(message)
|
||||
this.name = 'AbortError'
|
||||
}
|
||||
},
|
||||
isAbortError: (e: unknown) => e instanceof Error && (e as Error).name === 'AbortError',
|
||||
isAbortError: (e: unknown) =>
|
||||
e instanceof Error && (e as Error).name === 'AbortError',
|
||||
})
|
||||
mock.module('src/utils/errors.js', _abortMock)
|
||||
mock.module('src/utils/errors', _abortMock)
|
||||
@@ -16,7 +20,8 @@ describe('ExaSearchAdapter.search', () => {
|
||||
}
|
||||
|
||||
// Exa MCP returns SSE lines like: data: {"result":{"content":[{"type":"text","text":"..."}]}}
|
||||
const buildSseResponse = (text: string) => `data: ${JSON.stringify({ result: { content: [{ type: 'text', text }] } })}\n`
|
||||
const buildSseResponse = (text: string) =>
|
||||
`data: ${JSON.stringify({ result: { content: [{ type: 'text', text }] } })}\n`
|
||||
|
||||
const STRUCTURED_TEXT = [
|
||||
'Title: Example Result 1',
|
||||
@@ -37,7 +42,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
test('parses structured Title/URL/Content blocks from SSE response', async () => {
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
@@ -59,10 +66,13 @@ describe('ExaSearchAdapter.search', () => {
|
||||
})
|
||||
|
||||
test('parses markdown link fallback when no structured blocks', async () => {
|
||||
const markdownText = '- [React Docs](https://react.dev/docs)\n- [React Hooks](https://react.dev/hooks)'
|
||||
const markdownText =
|
||||
'- [React Docs](https://react.dev/docs)\n- [React Hooks](https://react.dev/hooks)'
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(markdownText) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(markdownText) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
@@ -83,7 +93,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
const plainUrlText = 'https://example.com/page1\nhttps://example.com/page2'
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(plainUrlText) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(plainUrlText) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
@@ -130,7 +142,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
test('calls onProgress with query_update and search_results_received', async () => {
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
@@ -161,13 +175,17 @@ describe('ExaSearchAdapter.search', () => {
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(mixedText) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(mixedText) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
|
||||
const adapter = await createAdapter()
|
||||
const results = await adapter.search('test', { allowedDomains: ['allowed.com'] })
|
||||
const results = await adapter.search('test', {
|
||||
allowedDomains: ['allowed.com'],
|
||||
})
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].url).toBe('https://allowed.com/a')
|
||||
@@ -184,13 +202,17 @@ describe('ExaSearchAdapter.search', () => {
|
||||
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(mixedText) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(mixedText) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
|
||||
const adapter = await createAdapter()
|
||||
const results = await adapter.search('test', { blockedDomains: ['spam.com'] })
|
||||
const results = await adapter.search('test', {
|
||||
blockedDomains: ['spam.com'],
|
||||
})
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].url).toBe('https://good.com/a')
|
||||
@@ -213,7 +235,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
}))
|
||||
|
||||
const adapter = await createAdapter()
|
||||
const results = await adapter.search('test', { allowedDomains: ['example.com'] })
|
||||
const results = await adapter.search('test', {
|
||||
allowedDomains: ['example.com'],
|
||||
})
|
||||
|
||||
expect(results).toHaveLength(1)
|
||||
expect(results[0].url).toBe('https://docs.example.com/page')
|
||||
@@ -222,7 +246,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
test('throws AbortError when signal is already aborted', async () => {
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) })),
|
||||
post: mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
|
||||
),
|
||||
isCancel: () => false,
|
||||
},
|
||||
}))
|
||||
@@ -251,7 +277,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
})
|
||||
|
||||
test('sends correct MCP request payload to Exa endpoint', async () => {
|
||||
const axiosPost = mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }))
|
||||
const axiosPost = mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
|
||||
)
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: axiosPost,
|
||||
@@ -277,7 +305,9 @@ describe('ExaSearchAdapter.search', () => {
|
||||
})
|
||||
|
||||
test('passes custom search options to MCP request', async () => {
|
||||
const axiosPost = mock(() => Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }))
|
||||
const axiosPost = mock(() =>
|
||||
Promise.resolve({ data: buildSseResponse(STRUCTURED_TEXT) }),
|
||||
)
|
||||
mock.module('axios', () => ({
|
||||
default: {
|
||||
post: axiosPost,
|
||||
|
||||
@@ -9,7 +9,11 @@ import type {
|
||||
} from '@anthropic-ai/sdk/resources/beta/messages/messages.mjs'
|
||||
import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js'
|
||||
import { queryModelWithStreaming } from 'src/services/api/claude.js'
|
||||
import { createTrace, endTrace, isLangfuseEnabled } from 'src/services/langfuse/index.js'
|
||||
import {
|
||||
createTrace,
|
||||
endTrace,
|
||||
isLangfuseEnabled,
|
||||
} from 'src/services/langfuse/index.js'
|
||||
import { getSessionId } from 'src/bootstrap/state.js'
|
||||
import { getAPIProvider } from 'src/utils/model/providers.js'
|
||||
import { createUserMessage } from 'src/utils/messages.js'
|
||||
@@ -18,7 +22,10 @@ import { jsonParse } from 'src/utils/slowOperations.js'
|
||||
import { asSystemPrompt } from 'src/utils/systemPromptType.js'
|
||||
import type { SearchResult, SearchOptions, WebSearchAdapter } from './types.js'
|
||||
|
||||
function makeToolSchema(input: { allowedDomains?: string[]; blockedDomains?: string[] }): BetaWebSearchTool20250305 {
|
||||
function makeToolSchema(input: {
|
||||
allowedDomains?: string[]
|
||||
blockedDomains?: string[]
|
||||
}): BetaWebSearchTool20250305 {
|
||||
return {
|
||||
type: 'web_search_20250305',
|
||||
name: 'web_search',
|
||||
@@ -29,10 +36,7 @@ function makeToolSchema(input: { allowedDomains?: string[]; blockedDomains?: str
|
||||
}
|
||||
|
||||
export class ApiSearchAdapter implements WebSearchAdapter {
|
||||
async search(
|
||||
query: string,
|
||||
options: SearchOptions,
|
||||
): Promise<SearchResult[]> {
|
||||
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
|
||||
const { signal, onProgress, allowedDomains, blockedDomains } = options
|
||||
|
||||
const userMessage = createUserMessage({
|
||||
@@ -40,7 +44,10 @@ export class ApiSearchAdapter implements WebSearchAdapter {
|
||||
})
|
||||
const toolSchema = makeToolSchema({ allowedDomains, blockedDomains })
|
||||
|
||||
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE('tengu_plum_vx3', false)
|
||||
const useHaiku = getFeatureValue_CACHED_MAY_BE_STALE(
|
||||
'tengu_plum_vx3',
|
||||
false,
|
||||
)
|
||||
const model = useHaiku ? getSmallFastModel() : getMainLoopModel()
|
||||
const langfuseTrace = isLangfuseEnabled()
|
||||
? createTrace({
|
||||
@@ -71,7 +78,9 @@ export class ApiSearchAdapter implements WebSearchAdapter {
|
||||
isBypassPermissionsModeAvailable: false,
|
||||
}),
|
||||
model,
|
||||
toolChoice: useHaiku ? { type: 'tool' as const, name: 'web_search' } : undefined,
|
||||
toolChoice: useHaiku
|
||||
? { type: 'tool' as const, name: 'web_search' }
|
||||
: undefined,
|
||||
isNonInteractiveSession: false,
|
||||
hasAppendSystemPrompt: false,
|
||||
extraToolSchemas: [toolSchema],
|
||||
@@ -101,8 +110,18 @@ export class ApiSearchAdapter implements WebSearchAdapter {
|
||||
const streamEvt = event as {
|
||||
event?: {
|
||||
type: string
|
||||
content_block?: { type: string; id?: string; tool_use_id?: string; content?: unknown; [key: string]: unknown }
|
||||
delta?: { type: string; partial_json?: string; [key: string]: unknown }
|
||||
content_block?: {
|
||||
type: string
|
||||
id?: string
|
||||
tool_use_id?: string
|
||||
content?: unknown
|
||||
[key: string]: unknown
|
||||
}
|
||||
delta?: {
|
||||
type: string
|
||||
partial_json?: string
|
||||
[key: string]: unknown
|
||||
}
|
||||
[key: string]: unknown
|
||||
}
|
||||
}
|
||||
@@ -116,7 +135,10 @@ export class ApiSearchAdapter implements WebSearchAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
if (currentToolUseId && streamEvt.event?.type === 'content_block_delta') {
|
||||
if (
|
||||
currentToolUseId &&
|
||||
streamEvt.event?.type === 'content_block_delta'
|
||||
) {
|
||||
const delta = streamEvt.event.delta
|
||||
if (delta?.type === 'input_json_delta' && delta.partial_json) {
|
||||
currentToolUseJson += delta.partial_json
|
||||
@@ -168,14 +190,20 @@ export class ApiSearchAdapter implements WebSearchAdapter {
|
||||
}
|
||||
}
|
||||
|
||||
function extractSearchResults(
|
||||
blocks: BetaContentBlock[],
|
||||
): SearchResult[] {
|
||||
function extractSearchResults(blocks: BetaContentBlock[]): SearchResult[] {
|
||||
const results: SearchResult[] = []
|
||||
|
||||
for (const block of blocks) {
|
||||
if (block.type === 'web_search_tool_result' && Array.isArray(block.content)) {
|
||||
for (const r of block.content as Array<{ title: string; url: string; page_age?: string; type?: string }>) {
|
||||
if (
|
||||
block.type === 'web_search_tool_result' &&
|
||||
Array.isArray(block.content)
|
||||
) {
|
||||
for (const r of block.content as Array<{
|
||||
title: string
|
||||
url: string
|
||||
page_age?: string
|
||||
type?: string
|
||||
}>) {
|
||||
results.push({
|
||||
title: r.title,
|
||||
url: r.url,
|
||||
|
||||
@@ -23,7 +23,8 @@ const BROWSER_HEADERS = {
|
||||
'Accept-Encoding': 'gzip, deflate, br',
|
||||
'Cache-Control': 'no-cache',
|
||||
Pragma: 'no-cache',
|
||||
'Sec-Ch-Ua': '"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
||||
'Sec-Ch-Ua':
|
||||
'"Microsoft Edge";v="131", "Chromium";v="131", "Not_A Brand";v="24"',
|
||||
'Sec-Ch-Ua-Mobile': '?0',
|
||||
'Sec-Ch-Ua-Platform': '"macOS"',
|
||||
'Sec-Fetch-Dest': 'document',
|
||||
@@ -34,10 +35,7 @@ const BROWSER_HEADERS = {
|
||||
} as const
|
||||
|
||||
export class BingSearchAdapter implements WebSearchAdapter {
|
||||
async search(
|
||||
query: string,
|
||||
options: SearchOptions,
|
||||
): Promise<SearchResult[]> {
|
||||
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
|
||||
const { signal, onProgress, allowedDomains, blockedDomains } = options
|
||||
|
||||
if (signal?.aborted) {
|
||||
@@ -50,7 +48,9 @@ export class BingSearchAdapter implements WebSearchAdapter {
|
||||
|
||||
const abortController = new AbortController()
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => abortController.abort(), { once: true })
|
||||
signal.addEventListener('abort', () => abortController.abort(), {
|
||||
once: true,
|
||||
})
|
||||
}
|
||||
|
||||
let html: string
|
||||
@@ -76,14 +76,22 @@ export class BingSearchAdapter implements WebSearchAdapter {
|
||||
const rawResults = extractBingResults(html)
|
||||
|
||||
// Client-side domain filtering
|
||||
const results = rawResults.filter((r) => {
|
||||
const results = rawResults.filter(r => {
|
||||
if (!r.url) return false
|
||||
try {
|
||||
const hostname = new URL(r.url).hostname
|
||||
if (allowedDomains?.length && !allowedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
|
||||
if (
|
||||
allowedDomains?.length &&
|
||||
!allowedDomains.some(
|
||||
d => hostname === d || hostname.endsWith('.' + d),
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (blockedDomains?.length && blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
|
||||
if (
|
||||
blockedDomains?.length &&
|
||||
blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
} catch {
|
||||
@@ -116,7 +124,8 @@ export function extractBingResults(html: string): SearchResult[] {
|
||||
const block = blockMatch[1]
|
||||
|
||||
// Extract the primary link from <h2><a href="...">...</a></h2>
|
||||
const h2LinkRegex = /<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i
|
||||
const h2LinkRegex =
|
||||
/<h2[^>]*>\s*<a[^>]+href="([^"]+)"[^>]*>([\s\S]*?)<\/a>/i
|
||||
const linkMatch = h2LinkRegex.exec(block)
|
||||
if (!linkMatch) continue
|
||||
|
||||
@@ -128,9 +137,7 @@ export function extractBingResults(html: string): SearchResult[] {
|
||||
const url = resolveBingUrl(rawUrl)
|
||||
if (!url) continue
|
||||
|
||||
const title = decodeHtmlEntities(
|
||||
titleHtml.replace(/<[^>]+>/g, '').trim(),
|
||||
)
|
||||
const title = decodeHtmlEntities(titleHtml.replace(/<[^>]+>/g, '').trim())
|
||||
|
||||
// Extract snippet: try b_lineclamp → b_caption <p> → b_caption fallback
|
||||
const snippet = extractSnippet(block)
|
||||
@@ -150,14 +157,16 @@ function extractSnippet(block: string): string | undefined {
|
||||
}
|
||||
|
||||
// 2. Try <p> inside b_caption
|
||||
const captionPRegex = /<div[^>]*class="b_caption[^"]*"[^>]*>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/i
|
||||
const captionPRegex =
|
||||
/<div[^>]*class="b_caption[^"]*"[^>]*>[\s\S]*?<p[^>]*>([\s\S]*?)<\/p>/i
|
||||
match = captionPRegex.exec(block)
|
||||
if (match) {
|
||||
return decodeHtmlEntities(match[1].replace(/<[^>]+>/g, '').trim())
|
||||
}
|
||||
|
||||
// 3. Fallback: any text inside b_caption <div>
|
||||
const fallbackRegex = /<div[^>]*class="b_caption[^"]*"[^>]*>([\s\S]*?)<\/div>/i
|
||||
const fallbackRegex =
|
||||
/<div[^>]*class="b_caption[^"]*"[^>]*>([\s\S]*?)<\/div>/i
|
||||
const fallbackMatch = fallbackRegex.exec(block)
|
||||
if (fallbackMatch) {
|
||||
const text = fallbackMatch[1].replace(/<[^>]+>/g, '').trim()
|
||||
|
||||
@@ -9,7 +9,10 @@ import type { SearchResult, SearchOptions, WebSearchAdapter } from './types.js'
|
||||
|
||||
const FETCH_TIMEOUT_MS = 30_000
|
||||
const BRAVE_LLM_CONTEXT_URL = 'https://api.search.brave.com/res/v1/llm/context'
|
||||
const BRAVE_API_KEY_ENV_VARS = ['BRAVE_SEARCH_API_KEY', 'BRAVE_API_KEY'] as const
|
||||
const BRAVE_API_KEY_ENV_VARS = [
|
||||
'BRAVE_SEARCH_API_KEY',
|
||||
'BRAVE_API_KEY',
|
||||
] as const
|
||||
|
||||
interface BraveGroundingResult {
|
||||
title?: string
|
||||
@@ -26,10 +29,7 @@ interface BraveSearchResponse {
|
||||
}
|
||||
|
||||
export class BraveSearchAdapter implements WebSearchAdapter {
|
||||
async search(
|
||||
query: string,
|
||||
options: SearchOptions,
|
||||
): Promise<SearchResult[]> {
|
||||
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
|
||||
const { signal, onProgress, allowedDomains, blockedDomains } = options
|
||||
|
||||
if (signal?.aborted) {
|
||||
|
||||
@@ -16,10 +16,7 @@ const EXA_MCP_URL = 'https://mcp.exa.ai/mcp'
|
||||
const FETCH_TIMEOUT_MS = 25_000
|
||||
|
||||
export class ExaSearchAdapter implements WebSearchAdapter {
|
||||
async search(
|
||||
query: string,
|
||||
options: SearchOptions,
|
||||
): Promise<SearchResult[]> {
|
||||
async search(query: string, options: SearchOptions): Promise<SearchResult[]> {
|
||||
const { signal, onProgress, allowedDomains, blockedDomains } = options
|
||||
|
||||
if (signal?.aborted) {
|
||||
@@ -30,7 +27,9 @@ export class ExaSearchAdapter implements WebSearchAdapter {
|
||||
|
||||
const abortController = new AbortController()
|
||||
if (signal) {
|
||||
signal.addEventListener('abort', () => abortController.abort(), { once: true })
|
||||
signal.addEventListener('abort', () => abortController.abort(), {
|
||||
once: true,
|
||||
})
|
||||
}
|
||||
|
||||
// Use options to derive search params — matches kilocode websearch.ts defaults
|
||||
@@ -90,14 +89,22 @@ export class ExaSearchAdapter implements WebSearchAdapter {
|
||||
const results = this.parseResults(searchText)
|
||||
|
||||
// Client-side domain filtering
|
||||
const filteredResults = results.filter((r) => {
|
||||
const filteredResults = results.filter(r => {
|
||||
if (!r.url) return false
|
||||
try {
|
||||
const hostname = new URL(r.url).hostname
|
||||
if (allowedDomains?.length && !allowedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
|
||||
if (
|
||||
allowedDomains?.length &&
|
||||
!allowedDomains.some(
|
||||
d => hostname === d || hostname.endsWith('.' + d),
|
||||
)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
if (blockedDomains?.length && blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))) {
|
||||
if (
|
||||
blockedDomains?.length &&
|
||||
blockedDomains.some(d => hostname === d || hostname.endsWith('.' + d))
|
||||
) {
|
||||
return false
|
||||
}
|
||||
} catch {
|
||||
@@ -160,7 +167,9 @@ export class ExaSearchAdapter implements WebSearchAdapter {
|
||||
for (const block of blocks) {
|
||||
const titleMatch = block.match(/^Title:\s*(.+)$/m)
|
||||
const urlMatch = block.match(/^URL:\s*(https?:\/\/[^\s]+)$/m)
|
||||
const contentMatch = block.match(/^Content:\s*([\s\S]+?)(?=\n(?:Title:|URL:|---)|$)/m)
|
||||
const contentMatch = block.match(
|
||||
/^Content:\s*([\s\S]+?)(?=\n(?:Title:|URL:|---)|$)/m,
|
||||
)
|
||||
|
||||
if (urlMatch) {
|
||||
results.push({
|
||||
@@ -173,7 +182,7 @@ export class ExaSearchAdapter implements WebSearchAdapter {
|
||||
|
||||
// Fallback: markdown links
|
||||
if (results.length === 0) {
|
||||
const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^\)]+)\)/g
|
||||
const markdownLinkRegex = /\[([^\]]+)\]\((https?:\/\/[^)]+)\)/g
|
||||
let match: RegExpExecArray | null
|
||||
while ((match = markdownLinkRegex.exec(text)) !== null) {
|
||||
results.push({
|
||||
|
||||
@@ -41,7 +41,10 @@ export function createAdapter(): WebSearchAdapter {
|
||||
// 3. First-party Anthropic API → api (server-side web search + connector_text)
|
||||
// 4. Fallback → bing
|
||||
const adapterKey =
|
||||
envAdapter === 'api' || envAdapter === 'bing' || envAdapter === 'brave' || envAdapter === 'exa'
|
||||
envAdapter === 'api' ||
|
||||
envAdapter === 'bing' ||
|
||||
envAdapter === 'brave' ||
|
||||
envAdapter === 'exa'
|
||||
? envAdapter
|
||||
: isThirdPartyProvider()
|
||||
? 'bing'
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getLocalMonthYear = any;
|
||||
export type getLocalMonthYear = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type getAPIProvider = any;
|
||||
export type getAPIProvider = any
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
// Auto-generated type stub — replace with real implementation
|
||||
export type PermissionResult = any;
|
||||
export type PermissionResult = any
|
||||
|
||||
Reference in New Issue
Block a user