Skip to content

Commit 3afc760

Browse files
lishuceoclaude
andcommitted
fix: 守卫 websearch 数值配置 + 上游错误体改写服务端日志
review 发现的两处低危问题: - 非数字的 WEBSEARCH_TIMEOUT_MS/WEBSEARCH_MAX_RESULTS 会得到 NaN, setTimeout(fn, NaN) 被强转为 0 立即触发,导致每次搜索秒超时。 新增 parsePositiveInt 守卫,非正整数回退默认值。 - Tavily 非 2xx 错误的完整 body 改为只写服务端日志(截断 500 字), 返回聊天的提示仍由 describeHttpError 截断到 200 字。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent aef2077 commit 3afc760

3 files changed

Lines changed: 71 additions & 3 deletions

File tree

src/__tests__/config.test.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,62 @@ describe('config', () => {
5959
expect(config.claude.defaultWorkDir).toBe(dirname(process.cwd()));
6060
});
6161
});
62+
63+
describe('parsePositiveInt', () => {
64+
it('parses valid positive integers', async () => {
65+
const { parsePositiveInt } = await loadConfig();
66+
expect(parsePositiveInt('15000', 999)).toBe(15000);
67+
expect(parsePositiveInt('1', 999)).toBe(1);
68+
});
69+
70+
it('falls back when undefined or empty', async () => {
71+
const { parsePositiveInt } = await loadConfig();
72+
expect(parsePositiveInt(undefined, 15000)).toBe(15000);
73+
expect(parsePositiveInt('', 15000)).toBe(15000);
74+
});
75+
76+
it('falls back on non-numeric values (guards setTimeout(fn, NaN) firing instantly)', async () => {
77+
const { parsePositiveInt } = await loadConfig();
78+
expect(parsePositiveInt('abc', 15000)).toBe(15000);
79+
expect(parsePositiveInt('none', 15000)).toBe(15000);
80+
});
81+
82+
it('falls back on zero and negative values', async () => {
83+
const { parsePositiveInt } = await loadConfig();
84+
expect(parsePositiveInt('0', 15000)).toBe(15000);
85+
expect(parsePositiveInt('-5', 15000)).toBe(15000);
86+
});
87+
});
88+
89+
describe('websearch config', () => {
90+
it('auto-enables when TAVILY_API_KEY is present', async () => {
91+
vi.stubEnv('TAVILY_API_KEY', 'tvly-abc');
92+
delete process.env.WEBSEARCH_ENABLED;
93+
const { config } = await loadConfig();
94+
expect(config.websearch.enabled).toBe(true);
95+
expect(config.websearch.apiKey).toBe('tvly-abc');
96+
});
97+
98+
it('stays disabled when no key and no explicit enable', async () => {
99+
vi.stubEnv('TAVILY_API_KEY', '');
100+
delete process.env.WEBSEARCH_ENABLED;
101+
const { config } = await loadConfig();
102+
expect(config.websearch.enabled).toBe(false);
103+
});
104+
105+
it('WEBSEARCH_ENABLED=false overrides key presence', async () => {
106+
vi.stubEnv('TAVILY_API_KEY', 'tvly-abc');
107+
vi.stubEnv('WEBSEARCH_ENABLED', 'false');
108+
const { config } = await loadConfig();
109+
expect(config.websearch.enabled).toBe(false);
110+
});
111+
112+
it('falls back to safe timeout/maxResults on non-numeric env', async () => {
113+
vi.stubEnv('WEBSEARCH_TIMEOUT_MS', 'abc');
114+
vi.stubEnv('WEBSEARCH_MAX_RESULTS', 'xyz');
115+
const { config } = await loadConfig();
116+
expect(config.websearch.timeoutMs).toBe(15000);
117+
expect(config.websearch.maxResults).toBe(5);
118+
});
119+
});
62120
});

src/config.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,15 @@ dotenv.config();
44
import { dirname } from 'node:path';
55
import type { GroupConfig } from './agent/types.js';
66

7+
/**
8+
* parseInt 的正整数守卫:非数字或 <=0 的环境变量值回退到默认值。
9+
* 防止形如 setTimeout(fn, NaN) 被强转为 0 立即触发的隐患。
10+
*/
11+
export function parsePositiveInt(raw: string | undefined, fallback: number): number {
12+
const n = parseInt(raw ?? '', 10);
13+
return Number.isFinite(n) && n > 0 ? n : fallback;
14+
}
15+
716
function parseGroupConfigs(raw?: string): Record<string, GroupConfig> {
817
if (!raw?.trim()) return {};
918
try {
@@ -185,11 +194,11 @@ export const config = {
185194
/** Tavily API base URL */
186195
baseUrl: process.env.TAVILY_BASE_URL || 'https://api.tavily.com',
187196
/** 默认返回结果数 (1-20) */
188-
maxResults: parseInt(process.env.WEBSEARCH_MAX_RESULTS || '5', 10),
197+
maxResults: parsePositiveInt(process.env.WEBSEARCH_MAX_RESULTS, 5),
189198
/** 默认搜索深度: basic (1 credit) | advanced (2 credits) */
190199
searchDepth: (process.env.WEBSEARCH_DEPTH || 'basic') as 'basic' | 'advanced',
191200
/** 单次请求超时毫秒数 */
192-
timeoutMs: parseInt(process.env.WEBSEARCH_TIMEOUT_MS || '15000', 10),
201+
timeoutMs: parsePositiveInt(process.env.WEBSEARCH_TIMEOUT_MS, 15000),
193202
},
194203

195204
// 定时任务配置

src/websearch/tool.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,7 +135,8 @@ export function webSearchTool() {
135135
if (!resp.ok) {
136136
const body = await resp.text().catch(() => '');
137137
const detail = describeHttpError(resp.status, body);
138-
logger.warn({ status: resp.status, query: args.query }, 'web_search Tavily API error');
138+
// 完整 body 仅写服务端日志(便于排障),返回给聊天的 detail 已被 describeHttpError 截断
139+
logger.warn({ status: resp.status, query: args.query, body: body.slice(0, 500) }, 'web_search Tavily API error');
139140
return {
140141
content: [{ type: 'text' as const, text: detail }],
141142
isError: true,

0 commit comments

Comments
 (0)