From 6f268f4f0155a2da58161951ae96fb5ea1af6fde Mon Sep 17 00:00:00 2001 From: phantom5099 <1011668688@qq.com> Date: Tue, 9 Jun 2026 15:20:42 +0800 Subject: [PATCH 1/2] Change search engine --- .../src/tools/domains/web/search.ts | 215 +++++++++++++----- .../codingcode/test/tools/websearch.test.ts | 105 ++++++++- 2 files changed, 255 insertions(+), 65 deletions(-) diff --git a/packages/codingcode/src/tools/domains/web/search.ts b/packages/codingcode/src/tools/domains/web/search.ts index e4f61dc..eabc084 100644 --- a/packages/codingcode/src/tools/domains/web/search.ts +++ b/packages/codingcode/src/tools/domains/web/search.ts @@ -1,6 +1,144 @@ import { z } from 'zod'; import type { ToolDefinition, ToolExecCtx } from '../../types'; +interface SearchResult { + title: string; + url: string; + snippet: string; +} + +const BROWSER_HEADERS = { + 'User-Agent': + 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36', + Accept: + 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8', + 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8', + 'Accept-Encoding': 'gzip, deflate', +}; + +/** + * 通过 cn.bing.com 搜索(国内可用,免费,无需 API Key) + */ +async function searchBing( + query: string, + maxResults: number, + signal: AbortSignal, +): Promise { + const url = `https://cn.bing.com/search?q=${encodeURIComponent(query)}&count=${maxResults}&setlang=zh-CN`; + const response = await fetch(url, { + signal, + headers: BROWSER_HEADERS, + redirect: 'follow', + }); + + if (!response.ok) { + throw new Error(`Bing HTTP ${response.status}`); + } + + const html = await response.text(); + return parseBingHtml(html, maxResults); +} + +/** + * 解析 Bing 搜索结果 HTML + */ +export function parseBingHtml(html: string, maxResults: number): SearchResult[] { + const results: SearchResult[] = []; + + // Bing 结果在
  • 中(可能有其他属性如 data-id) + const parts = html.split(/
  • 或下一个 '); + const blockContent = endIdx !== -1 ? block.substring(0, endIdx) : block; + + // 标题和链接在

    ...

    + const titleMatch = blockContent.match(/]*>\s*]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/i); + // 摘要在 class="b_caption" 的

    中,或 b_lineclamp 的

    中 + const snippetMatch = + blockContent.match(/class="b_caption[^"]*"[^>]*>[\s\S]*?]*>([\s\S]*?)<\/p>/i) || + blockContent.match(/]*class="b_lineclamp[^"]*"[^>]*>([\s\S]*?)<\/p>/i); + + if (!titleMatch) continue; + + const url = titleMatch[1]?.replace(/&/g, '&').trim() || ''; + const title = titleMatch[2]?.replace(/<[^>]+>/g, '').trim() || ''; + const snippet = snippetMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || ''; + + if (title && url) { + results.push({ title, url, snippet }); + } + } + + return results; +} + +/** + * 通过百度搜索(国内 fallback) + */ +async function searchBaidu( + query: string, + maxResults: number, + signal: AbortSignal, +): Promise { + const url = `https://www.baidu.com/s?wd=${encodeURIComponent(query)}&rn=${maxResults}`; + const response = await fetch(url, { + signal, + headers: { + ...BROWSER_HEADERS, + Referer: 'https://www.baidu.com/', + }, + redirect: 'follow', + }); + + if (!response.ok) { + throw new Error(`Baidu HTTP ${response.status}`); + } + + const html = await response.text(); + return parseBaiduHtml(html, maxResults); +} + +/** + * 解析百度搜索结果 HTML + */ +export function parseBaiduHtml(html: string, maxResults: number): SearchResult[] { + const results: SearchResult[] = []; + + // 百度结果在

    + const containerBlocks = html.split(/class="[^"]*c-container[^"]*"/); + + for (let i = 1; i < containerBlocks.length && results.length < maxResults; i++) { + const block = containerBlocks[i]; + if (!block) continue; + + // 标题在

    内的 中 + const titleMatch = block.match(/]*>[\s\S]*?]*href="([^"]*)"[^>]*>([\s\S]*?)<\/a>/i); + // 摘要在 class 含 content-right 或 c-span-last 的 中,或
    + const snippetMatch = + block.match(/class="c-abstract[^"]*"[^>]*>([\s\S]*?)<\/(?:span|div)>/i) || + block.match(/class="content-right[^"]*"[^>]*>[\s\S]*?]*>([\s\S]*?)<\/span>/i) || + block.match(/]*>([\s\S]*?)<\/span>/i); + + if (!titleMatch) continue; + + const url = titleMatch[1]?.replace(/&/g, '&').trim() || ''; + const title = titleMatch[2]?.replace(/<[^>]+>/g, '').trim() || ''; + const snippet = snippetMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || ''; + + // 过滤百度内部链接 + if (title && url && !url.startsWith('/') && !url.startsWith('#')) { + results.push({ title, url, snippet }); + } + } + + return results; +} + export const webSearchTool: ToolDefinition = { name: 'web_search', description: @@ -22,27 +160,24 @@ export const webSearchTool: ToolDefinition = { const timer = setTimeout(() => controller.abort(), 15_000); try { - const url = `https://html.duckduckgo.com/html/?q=${encodeURIComponent(query)}`; - const response = await fetch(url, { - signal: controller.signal, - headers: { 'User-Agent': 'coding-agent/1.0', Accept: 'text/html' }, - redirect: 'follow', - }); - - if (!response.ok) { - return `Search failed: HTTP ${response.status} ${response.statusText}`; - } - - const html = await response.text(); - const results = parseDuckDuckGoHtml(html).slice(0, max_results); - - if (results.length === 0) { - return `No results found for "${query}".`; + // 搜索引擎优先级:Bing(cn) → 百度 + const engines = [searchBing, searchBaidu]; + + let lastError = ''; + for (const engine of engines) { + try { + const results = await engine(query, max_results, controller.signal); + if (results.length > 0) { + return results + .map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet || '(no snippet)'}`) + .join('\n\n'); + } + } catch (err: unknown) { + lastError = err instanceof Error ? err.message : String(err); + } } - return results - .map((r, i) => `${i + 1}. ${r.title}\n ${r.url}\n ${r.snippet}`) - .join('\n\n'); + return `No results found for "${query}".${lastError ? ` Last error: ${lastError}` : ''}`; } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); return `Search error for "${query}": ${message}`; @@ -51,45 +186,3 @@ export const webSearchTool: ToolDefinition = { } }, }; - -interface SearchResult { - title: string; - url: string; - snippet: string; -} - -function parseDuckDuckGoHtml(html: string): SearchResult[] { - const results: SearchResult[] = []; - const resultBlocks = html.split('class="result__body"'); - - for (let i = 1; i < resultBlocks.length; i++) { - const block = resultBlocks[i]; - if (!block) continue; - - const titleMatch = block.match(/class="result__a"[^>]*>([\s\S]*?)<\/a>/i); - const snippetMatch = block.match(/class="result__snippet"[^>]*>([\s\S]*?)<\/a>/i); - - const title = titleMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || ''; - - // Extract URL from the first href in the block - const hrefMatch = block.match(/href="([^"]+)"/); - let url = ''; - if (hrefMatch?.[1]) { - url = hrefMatch[1].replace(/&/g, '&'); - if (url.startsWith('//duckduckgo.com/l/')) { - const uddgMatch = url.match(/uddg=([^&]+)/); - if (uddgMatch?.[1]) { - url = decodeURIComponent(uddgMatch[1]); - } - } - } - - const snippet = snippetMatch?.[1]?.replace(/<[^>]+>/g, '').trim() || ''; - - if (title) { - results.push({ title, url: url || '(no url)', snippet: snippet || '(no snippet)' }); - } - } - - return results; -} diff --git a/packages/codingcode/test/tools/websearch.test.ts b/packages/codingcode/test/tools/websearch.test.ts index 93a8342..f63e123 100644 --- a/packages/codingcode/test/tools/websearch.test.ts +++ b/packages/codingcode/test/tools/websearch.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from 'vitest'; -import { webSearchTool } from '../../src/tools/domains/web/search.js'; +import { describe, it, expect } from 'vitest'; +import { webSearchTool, parseBingHtml, parseBaiduHtml } from '../../src/tools/domains/web/search.js'; describe('webSearchTool', () => { it('should have correct tool name and schema', () => { @@ -13,7 +13,7 @@ describe('webSearchTool', () => { max_results: number; }; expect(parsed.query).toBe('test query'); - expect(parsed.max_results).toBe(8); // default + expect(parsed.max_results).toBe(8); }); it('should enforce max_results range', () => { @@ -27,8 +27,105 @@ describe('webSearchTool', () => { it('should execute search and return results', async () => { const result = await webSearchTool.execute({ query: 'TypeScript programming', max_results: 3 }); - // Should return a string with results or a no-results / error message expect(typeof result).toBe('string'); expect(result.length).toBeGreaterThan(0); + // 应该返回编号格式的结果,而不是错误信息 + expect(result).not.toContain('Search error'); }, 20_000); + + it('should support Chinese query', async () => { + const result = await webSearchTool.execute({ query: '自主AI agent平台', max_results: 3 }); + expect(typeof result).toBe('string'); + expect(result.length).toBeGreaterThan(0); + expect(result).not.toContain('Search error'); + }, 20_000); +}); + +describe('parseBingHtml', () => { + it('should parse Bing HTML results correctly', () => { + const html = ` +
  • +

    Example Title 1

    +

    Snippet text 1

    +
  • +
  • +

    Example Title 2

    +

    Snippet text 2

    +
  • + `; + + const results = parseBingHtml(html, 10); + expect(results).toHaveLength(2); + expect(results[0].title).toBe('Example Title 1'); + expect(results[0].url).toBe('https://example.com/page1'); + expect(results[0].snippet).toBe('Snippet text 1'); + }); + + it('should respect maxResults limit', () => { + const html = ` +
  • Title 1

  • +
  • Title 2

  • +
  • Title 3

  • + `; + + const results = parseBingHtml(html, 2); + expect(results).toHaveLength(2); + }); + + it('should return empty array for HTML with no results', () => { + const html = 'No results found'; + const results = parseBingHtml(html, 10); + expect(results).toHaveLength(0); + }); + + it('should strip HTML tags from titles and snippets', () => { + const html = ` +
  • +

    Bold Title

    +

    Text with emphasis

    +
  • + `; + + const results = parseBingHtml(html, 10); + expect(results[0].title).toBe('Bold Title'); + expect(results[0].snippet).toBe('Text with emphasis'); + }); +}); + +describe('parseBaiduHtml', () => { + it('should parse Baidu HTML results correctly', () => { + const html = ` +
    +

    百度结果1

    + 摘要文本1 +
    +
    +

    百度结果2

    + 摘要文本2 +
    + `; + + const results = parseBaiduHtml(html, 10); + expect(results.length).toBeGreaterThanOrEqual(1); + }); + + it('should filter out Baidu internal links', () => { + const html = ` + + + `; + + const results = parseBaiduHtml(html, 10); + expect(results.every((r) => !r.url.startsWith('/'))).toBe(true); + }); + + it('should return empty array for HTML with no results', () => { + const html = '没有结果'; + const results = parseBaiduHtml(html, 10); + expect(results).toHaveLength(0); + }); }); From 9db60d8917755349072cc60ba95df56ad2854421 Mon Sep 17 00:00:00 2001 From: phantom5099 <1011668688@qq.com> Date: Tue, 9 Jun 2026 21:22:10 +0800 Subject: [PATCH 2/2] Modify system prompt --- packages/codingcode/src/agent/agent.ts | 5 +- packages/codingcode/src/agent/prompt.ts | 32 ++++++--- packages/codingcode/src/index.ts | 2 +- packages/codingcode/test/agent/config.test.ts | 4 +- .../test/prompts/system-prompt.test.ts | 66 ++++++++++++++----- packages/infra/src/config.ts | 2 +- 6 files changed, 81 insertions(+), 30 deletions(-) diff --git a/packages/codingcode/src/agent/agent.ts b/packages/codingcode/src/agent/agent.ts index 00d4968..cee3fd3 100644 --- a/packages/codingcode/src/agent/agent.ts +++ b/packages/codingcode/src/agent/agent.ts @@ -262,7 +262,10 @@ export async function* runReActLoop( }); const memoryBlock = state.memorySnapshot; - const system = [basePrompt, memoryBlock].filter(Boolean).join('\n\n'); + const memorySection = memoryBlock + ? `## Session Memory\n\n${memoryBlock}` + : ''; + const system = [basePrompt, memorySection].filter(Boolean).join('\n\n'); const config = getContextConfig(); const maxOverflowRetries = config.reactiveCompactMaxRetries; diff --git a/packages/codingcode/src/agent/prompt.ts b/packages/codingcode/src/agent/prompt.ts index 82ad741..7745c34 100644 --- a/packages/codingcode/src/agent/prompt.ts +++ b/packages/codingcode/src/agent/prompt.ts @@ -1,8 +1,5 @@ import { getAllRules } from '../rules/index.js'; -import type { AgentProfile } from '../subagent/registry'; - -export const DEFERRED_TOOLS_GUIDELINES = `## Deferred tools -- Some tools are listed as deferred — call tool_search with relevant keywords before using them.`; +import type { AgentProfile } from '../subagent/registry.js'; const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that helps users write, read, search, and modify code. @@ -14,7 +11,20 @@ const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that h 5. Make small, focused changes — avoid large rewrites 6. Run tests or type-check after changes when applicable 7. If the user's request is ambiguous, ask for clarification -8. For complex or broad tasks (understanding a whole module, cross-file analysis, comprehensive search), delegate to dispatch_agent immediately with the original task — do not explore the topic yourself before delegating. +8. For complex or broad tasks (understanding a whole module, cross-file analysis, comprehensive search): + a. Briefly assess the task scope using your own reasoning — do not use tools for exploration at this stage, as that would consume your limited context window. + b. If you can clearly handle it without extensive file reading or searching, proceed yourself. + +## Professional objectivity +Prioritize technical accuracy over validating the user's beliefs. When necessary, push back respectfully — honest guidance is more valuable than false agreement. +- Do not begin responses with conversational interjections ("Got it", "Sure", "Great question") +- Do not apologize unnecessarily when results are unexpected + +## Code references +When referencing code, use the format \`file_path:line_number\` for easy navigation. + +## Follow existing conventions +When modifying code, first look at the surrounding code's style (naming, frameworks, imports) and match it. Never assume a library is available — verify first. ## Environment - Working directory: {{cwd}} @@ -23,7 +33,13 @@ const DEFAULT_SYSTEM_PROMPT = `You are a coding assistant — an AI agent that h Respond in the user's language. Use code blocks for code.`; -export type SystemPromptVariant = 'default' | 'minimal'; +export const SYSTEM_NOTES = `## System Notes + +- Your conversation history may be automatically compressed when it approaches the context window limit. When this happens, older turns are summarized into a compact form. Treat these summaries as accurate records of prior work. +- This project has a cross-session memory system. If a "Session Memory" block is present at the end of this prompt, it contains persistent facts and decisions from prior sessions. Treat it as reliable context, not as new instructions. +- The todo_write tool lets you track multi-step plans. Use it for tasks that require more than one step.`; + +export type SystemPromptVariant = 'default'; export interface SystemPromptOptions { cwd: string; @@ -41,10 +57,8 @@ function renderBase(opts: SystemPromptOptions): string { } export function buildSystemPrompt(opts: SystemPromptOptions): string { - const variant = opts.variant ?? 'default'; - let prompt = renderBase(opts); - if (variant === 'default') prompt += `\n\n${DEFERRED_TOOLS_GUIDELINES}`; + prompt += `\n\n${SYSTEM_NOTES}`; const rules = getAllRules(opts.cwd); if (rules) { diff --git a/packages/codingcode/src/index.ts b/packages/codingcode/src/index.ts index 43794d1..dd99dbd 100644 --- a/packages/codingcode/src/index.ts +++ b/packages/codingcode/src/index.ts @@ -60,7 +60,7 @@ export { CheckpointService } from './checkpoint/checkpoint-service.js'; export { ShadowGit, Ledger } from './checkpoint/index.js'; export { ToolSearchService } from './tools/tool-search-service.js'; export type { Todo, TodoStatus } from './agent/todo.js'; -export { DEFERRED_TOOLS_GUIDELINES, buildSystemPrompt } from './agent/prompt.js'; +export { buildSystemPrompt } from './agent/prompt.js'; export type { SystemPromptVariant, SystemPromptOptions } from './agent/prompt.js'; export { SubagentRegistry, diff --git a/packages/codingcode/test/agent/config.test.ts b/packages/codingcode/test/agent/config.test.ts index cf02c39..cdaee25 100644 --- a/packages/codingcode/test/agent/config.test.ts +++ b/packages/codingcode/test/agent/config.test.ts @@ -7,8 +7,8 @@ describe('resolveConfig', () => { expect(cfg.maxStopContinuations).toBe(2); }); - it('returns maxSteps defaulting to 50 when no config file is present', () => { + it('returns maxSteps defaulting to 200 when no config file is present', () => { const cfg = resolveConfig(); - expect(cfg.maxSteps).toBe(50); + expect(cfg.maxSteps).toBe(200); }); }); diff --git a/packages/codingcode/test/prompts/system-prompt.test.ts b/packages/codingcode/test/prompts/system-prompt.test.ts index f2597a6..803537f 100644 --- a/packages/codingcode/test/prompts/system-prompt.test.ts +++ b/packages/codingcode/test/prompts/system-prompt.test.ts @@ -1,19 +1,9 @@ import { describe, it, expect } from 'vitest'; -import { buildSystemPrompt, DEFERRED_TOOLS_GUIDELINES } from '../../src/agent/prompt.js'; +import { buildSystemPrompt, SYSTEM_NOTES } from '../../src/agent/prompt.js'; const baseOpts = { cwd: '/test', platform: 'linux', shell: 'bash' }; describe('buildSystemPrompt', () => { - it('default variant includes deferred tools guidelines', () => { - const prompt = buildSystemPrompt(baseOpts); - expect(prompt).toContain('tool_search'); - }); - - it('minimal variant excludes deferred tools guidelines', () => { - const prompt = buildSystemPrompt({ ...baseOpts, variant: 'minimal' }); - expect(prompt).not.toContain('tool_search'); - }); - it('replaces cwd, platform, shell placeholders', () => { const prompt = buildSystemPrompt({ cwd: '/my/proj', platform: 'darwin', shell: 'zsh' }); expect(prompt).toContain('/my/proj'); @@ -21,14 +11,58 @@ describe('buildSystemPrompt', () => { expect(prompt).toContain('zsh'); }); - it('includes dispatch_agent delegation rule for complex tasks', () => { + it('Rule 8 guides assessment-first then optional delegation', () => { const prompt = buildSystemPrompt(baseOpts); + expect(prompt).toContain('assess the task scope'); + }); + + it('includes professional objectivity section', () => { + const prompt = buildSystemPrompt(baseOpts); + expect(prompt).toContain('Professional objectivity'); + expect(prompt).toContain('technical accuracy'); + expect(prompt).toContain('Do not begin responses with conversational interjections'); + }); + + it('includes code references section', () => { + const prompt = buildSystemPrompt(baseOpts); + expect(prompt).toContain('Code references'); + expect(prompt).toContain('file_path:line_number'); + }); + + it('includes follow existing conventions section', () => { + const prompt = buildSystemPrompt(baseOpts); + expect(prompt).toContain('Follow existing conventions'); + expect(prompt).toContain('Never assume a library is available'); + }); + + it('SYSTEM_NOTES explains compression, memory, and todo', () => { + expect(SYSTEM_NOTES).toContain('automatically compressed'); + expect(SYSTEM_NOTES).toContain('Session Memory'); + expect(SYSTEM_NOTES).toContain('todo_write'); + }); + + it('includes SYSTEM_NOTES in prompt', () => { + const prompt = buildSystemPrompt(baseOpts); + expect(prompt).toContain('System Notes'); + expect(prompt).toContain('automatically compressed'); + }); + + it('includes user-defined rules section when rules exist', () => { + const prompt = buildSystemPrompt(baseOpts); + expect(prompt).toContain('User-defined Rules'); + }); + + it('includes available subagents section when profiles are provided', () => { + const profiles = [{ name: 'explore', description: 'Read-only code exploration.', tools: ['read_file'], disabled: false }]; + const prompt = buildSystemPrompt({ ...baseOpts, agentProfiles: profiles }); + expect(prompt).toContain('Available Subagents'); expect(prompt).toContain('dispatch_agent'); - expect(prompt).toContain('do not explore the topic yourself before delegating'); + expect(prompt).toContain('explore'); + expect(prompt).toContain('Read-only code exploration.'); }); - it('DEFERRED_TOOLS_GUIDELINES is a non-empty string', () => { - expect(DEFERRED_TOOLS_GUIDELINES.length).toBeGreaterThan(0); - expect(DEFERRED_TOOLS_GUIDELINES).toContain('tool_search'); + it('omits available subagents section when no profiles are provided', () => { + const prompt = buildSystemPrompt(baseOpts); + expect(prompt).not.toContain('Available Subagents'); }); }); diff --git a/packages/infra/src/config.ts b/packages/infra/src/config.ts index 98b69a7..6600df6 100644 --- a/packages/infra/src/config.ts +++ b/packages/infra/src/config.ts @@ -81,7 +81,7 @@ export const DEFAULT_CONFIG: AppConfig = { server: { port: 8080, }, - maxSteps: 50, + maxSteps: 200, maxStopContinuations: 2, context: DEFAULT_CONTEXT, memory: DEFAULT_MEMORY,