fix: 修复所有超时设置0=禁用不生效的问题

1. 流式超时: baseTimeout=0时被忽略,仍走动态计算120s-600s 2. HTTP超时: ipc.ts中0>0=false导致setHTTPTimeout未调用 + Math.max(5000,ms)阻止0值 3. MCP超时: 同HTTP模式 + Math.max(10000,ms)阻止0值 4. fetchWithTimeout和web_fetch中直接setTimeout在HTTP_TIMEOUT=0时仍触发abort
This commit is contained in:
thzxx
2026-07-11 15:38:28 +08:00
parent 028cf33dee
commit e2350a784d
4 changed files with 45 additions and 24 deletions
+2 -2
View File
@@ -262,10 +262,10 @@ export async function setupIPC(): Promise<void> {
}); });
ipcMain.handle('tool:setTimeouts', (_, timeouts: { http?: number; mcp?: number }) => { ipcMain.handle('tool:setTimeouts', (_, timeouts: { http?: number; mcp?: number }) => {
if (typeof timeouts.http === 'number' && timeouts.http > 0) { if (typeof timeouts.http === 'number' && timeouts.http >= 0) {
setHTTPTimeout(timeouts.http); setHTTPTimeout(timeouts.http);
} }
if (typeof timeouts.mcp === 'number' && timeouts.mcp > 0) { if (typeof timeouts.mcp === 'number' && timeouts.mcp >= 0) {
setMCPTimeout(timeouts.mcp); setMCPTimeout(timeouts.mcp);
} }
return { success: true }; return { success: true };
+8 -8
View File
@@ -42,7 +42,7 @@ interface MCPServerInstance {
config: { name: string; command: string; args: string[]; enabled: boolean; description?: string }; config: { name: string; command: string; args: string[]; enabled: boolean; description?: string };
process: ChildProcess; process: ChildProcess;
requestId: number; requestId: number;
pendingRequests: Map<number, { resolve: (value: unknown) => void; reject: (reason: Error) => void; timer: ReturnType<typeof setTimeout> }>; pendingRequests: Map<number, { resolve: (value: unknown) => void; reject: (reason: Error) => void; timer: ReturnType<typeof setTimeout> | null }>;
buffer: string; buffer: string;
tools: MCPTool[]; tools: MCPTool[];
initialized: boolean; initialized: boolean;
@@ -55,9 +55,9 @@ const servers = new Map<string, MCPServerInstance>();
/** MCP 请求超时(毫秒),默认 60s,可通过 IPC 设置 */ /** MCP 请求超时(毫秒),默认 60s,可通过 IPC 设置 */
let MCP_TIMEOUT = 60_000; let MCP_TIMEOUT = 60_000;
/** 设置 MCP 请求超时(毫秒) */ /** 设置 MCP 请求超时(毫秒)0=禁用超时 */
export function setMCPTimeout(ms: number): void { export function setMCPTimeout(ms: number): void {
MCP_TIMEOUT = Math.max(10_000, ms); MCP_TIMEOUT = ms === 0 ? 0 : Math.max(10_000, ms);
} }
// ─── JSON-RPC 通信 ─── // ─── JSON-RPC 通信 ───
@@ -67,10 +67,10 @@ function sendRequest(server: MCPServerInstance, method: string, params?: Record<
const id = ++server.requestId; const id = ++server.requestId;
const request: JSONRPCRequest = { jsonrpc: '2.0', id, method, params }; const request: JSONRPCRequest = { jsonrpc: '2.0', id, method, params };
const timer = setTimeout(() => { const timer = MCP_TIMEOUT > 0 ? setTimeout(() => {
server.pendingRequests.delete(id); server.pendingRequests.delete(id);
reject(new Error(`MCP 请求超时: ${server.config.name} / ${method} (${MCP_TIMEOUT}ms)`)); reject(new Error(`MCP 请求超时: ${server.config.name} / ${method} (${MCP_TIMEOUT}ms)`));
}, MCP_TIMEOUT); }, MCP_TIMEOUT) : null;
server.pendingRequests.set(id, { resolve, reject, timer }); server.pendingRequests.set(id, { resolve, reject, timer });
@@ -79,7 +79,7 @@ function sendRequest(server: MCPServerInstance, method: string, params?: Record<
server.process.stdin!.write(line); server.process.stdin!.write(line);
} catch (err) { } catch (err) {
server.pendingRequests.delete(id); server.pendingRequests.delete(id);
clearTimeout(timer); if (timer) clearTimeout(timer);
reject(new Error(`MCP 写入失败: ${(err as Error).message}`)); reject(new Error(`MCP 写入失败: ${(err as Error).message}`));
} }
}); });
@@ -89,7 +89,7 @@ function handleResponse(server: MCPServerInstance, response: JSONRPCResponse): v
const pending = server.pendingRequests.get(response.id); const pending = server.pendingRequests.get(response.id);
if (!pending) return; if (!pending) return;
clearTimeout(pending.timer); if (pending.timer) clearTimeout(pending.timer);
server.pendingRequests.delete(response.id); server.pendingRequests.delete(response.id);
if (response.error) { if (response.error) {
@@ -201,7 +201,7 @@ export async function startServer(config: { name: string; command: string; args:
function cleanupServer(server: MCPServerInstance): void { function cleanupServer(server: MCPServerInstance): void {
for (const [, pending] of server.pendingRequests) { for (const [, pending] of server.pendingRequests) {
clearTimeout(pending.timer); if (pending.timer) clearTimeout(pending.timer);
pending.reject(new Error('MCP 服务器已关闭')); pending.reject(new Error('MCP 服务器已关闭'));
} }
server.pendingRequests.clear(); server.pendingRequests.clear();
+17 -5
View File
@@ -127,9 +127,9 @@ export function killToolProcess(): boolean {
/** HTTP 请求超时(毫秒),默认 30s,可通过 IPC 设置 */ /** HTTP 请求超时(毫秒),默认 30s,可通过 IPC 设置 */
let HTTP_TIMEOUT = 30_000; let HTTP_TIMEOUT = 30_000;
/** 设置 HTTP 请求超时(毫秒) */ /** 设置 HTTP 请求超时(毫秒)0=禁用超时 */
export function setHTTPTimeout(ms: number): void { export function setHTTPTimeout(ms: number): void {
HTTP_TIMEOUT = Math.max(5000, ms); HTTP_TIMEOUT = ms === 0 ? 0 : Math.max(5000, ms);
} }
// ── LRU 搜索缓存 ────────────────────────────────── // ── LRU 搜索缓存 ──────────────────────────────────
@@ -233,10 +233,17 @@ function buildFetchHeaders(url: string, attemptIndex: number, useMobileUA: boole
}; };
} }
/** 带超时的 fetch 封装 */ /** 带超时的 fetch 封装timeout=0 表示不超时 */
async function fetchWithTimeout(url: string, timeout = HTTP_TIMEOUT, headers?: Record<string,string>, attemptIndex = 0): Promise<Response | null> { async function fetchWithTimeout(url: string, timeout = HTTP_TIMEOUT, headers?: Record<string,string>, attemptIndex = 0): Promise<Response | null> {
const _headers = headers || buildFetchHeaders(url, attemptIndex); const _headers = headers || buildFetchHeaders(url, attemptIndex);
if (timeout <= 0) {
// 0 = 禁用超时
try {
return await fetch(url, { headers: _headers, redirect: 'follow' });
} catch { return null; }
}
const controller = new AbortController(); const controller = new AbortController();
const tid = setTimeout(() => controller.abort(), timeout); const tid = setTimeout(() => controller.abort(), timeout);
try { try {
@@ -444,14 +451,19 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
const headers = buildFetchHeaders(url, attempt, useMobileUA); const headers = buildFetchHeaders(url, attempt, useMobileUA);
try { try {
let resp: Response;
if (HTTP_TIMEOUT > 0) {
const controller = new AbortController(); const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT); const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
let resp: Response;
try { try {
resp = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' }); resp = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' });
} finally { } finally {
clearTimeout(timeoutId); clearTimeout(timeoutId);
} }
} else {
// 0 = 禁用超时
resp = await fetch(url, { headers, redirect: 'follow' });
}
if (!resp.ok) { if (!resp.ok) {
// 反爬信号(403/503/429)→ 跳过后续 fetch,直接走浏览器回退 // 反爬信号(403/503/429)→ 跳过后续 fetch,直接走浏览器回退
@@ -566,7 +578,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
} catch (err) { } catch (err) {
const errMsg = (err as Error).message; const errMsg = (err as Error).message;
if (errMsg.includes('abort') && attempt < maxAttempts - 1) { if (errMsg.includes('abort') && attempt < maxAttempts - 1) {
lastError = `请求超时 (${HTTP_TIMEOUT / 1000}s)(将重试)`; lastError = `请求超时 (${HTTP_TIMEOUT > 0 ? HTTP_TIMEOUT / 1000 : '∞'}s)(将重试)`;
continue; continue;
} }
if (errMsg.includes('abort')) { if (errMsg.includes('abort')) {
+13 -4
View File
@@ -1336,12 +1336,21 @@ async function handleThinking(
const abortController = registerAbortController(); const abortController = registerAbortController();
// ── 流式超时保护 — 动态计算:基础 120s + 上下文 token 数 * 0.5s/1K tokens ── // ── 流式超时保护 — 0=禁用, 正数=自定义, -1/未设置=动态计算 ──
const baseTimeout = state.get<number>('streamTimeout', 0); const baseTimeout = state.get<number>('streamTimeout', 0);
const numCtxForTimeout = getEffectiveNumCtx(); const numCtxForTimeout = getEffectiveNumCtx();
const dynamicTimeout = baseTimeout > 0 ? baseTimeout : Math.max(120_000, 120_000 + Math.floor(numCtxForTimeout / 1000) * 500); let STREAM_TIMEOUT_MS: number;
// 上限 10 分钟,避免极端值 if (baseTimeout === 0) {
const STREAM_TIMEOUT_MS = Math.min(dynamicTimeout, 600_000); // 0 = 禁用流式超时
STREAM_TIMEOUT_MS = 0;
} else if (baseTimeout > 0) {
// 用户自定义超时,上限 10 分钟
STREAM_TIMEOUT_MS = Math.min(baseTimeout, 600_000);
} else {
// -1 或未设置 = 动态计算:基础 120s + 上下文 token 数 * 0.5s/1K tokens
const dynamicTimeout = Math.max(120_000, 120_000 + Math.floor(numCtxForTimeout / 1000) * 500);
STREAM_TIMEOUT_MS = Math.min(dynamicTimeout, 600_000);
}
let totalTimer: ReturnType<typeof setTimeout> | null = null; let totalTimer: ReturnType<typeof setTimeout> | null = null;