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:
+2
-2
@@ -262,10 +262,10 @@ export async function setupIPC(): Promise<void> {
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
if (typeof timeouts.mcp === 'number' && timeouts.mcp > 0) {
|
||||
if (typeof timeouts.mcp === 'number' && timeouts.mcp >= 0) {
|
||||
setMCPTimeout(timeouts.mcp);
|
||||
}
|
||||
return { success: true };
|
||||
|
||||
@@ -42,7 +42,7 @@ interface MCPServerInstance {
|
||||
config: { name: string; command: string; args: string[]; enabled: boolean; description?: string };
|
||||
process: ChildProcess;
|
||||
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;
|
||||
tools: MCPTool[];
|
||||
initialized: boolean;
|
||||
@@ -55,9 +55,9 @@ const servers = new Map<string, MCPServerInstance>();
|
||||
/** MCP 请求超时(毫秒),默认 60s,可通过 IPC 设置 */
|
||||
let MCP_TIMEOUT = 60_000;
|
||||
|
||||
/** 设置 MCP 请求超时(毫秒) */
|
||||
/** 设置 MCP 请求超时(毫秒),0=禁用超时 */
|
||||
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 通信 ───
|
||||
@@ -67,10 +67,10 @@ function sendRequest(server: MCPServerInstance, method: string, params?: Record<
|
||||
const id = ++server.requestId;
|
||||
const request: JSONRPCRequest = { jsonrpc: '2.0', id, method, params };
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
const timer = MCP_TIMEOUT > 0 ? setTimeout(() => {
|
||||
server.pendingRequests.delete(id);
|
||||
reject(new Error(`MCP 请求超时: ${server.config.name} / ${method} (${MCP_TIMEOUT}ms)`));
|
||||
}, MCP_TIMEOUT);
|
||||
}, MCP_TIMEOUT) : null;
|
||||
|
||||
server.pendingRequests.set(id, { resolve, reject, timer });
|
||||
|
||||
@@ -79,7 +79,7 @@ function sendRequest(server: MCPServerInstance, method: string, params?: Record<
|
||||
server.process.stdin!.write(line);
|
||||
} catch (err) {
|
||||
server.pendingRequests.delete(id);
|
||||
clearTimeout(timer);
|
||||
if (timer) clearTimeout(timer);
|
||||
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);
|
||||
if (!pending) return;
|
||||
|
||||
clearTimeout(pending.timer);
|
||||
if (pending.timer) clearTimeout(pending.timer);
|
||||
server.pendingRequests.delete(response.id);
|
||||
|
||||
if (response.error) {
|
||||
@@ -201,7 +201,7 @@ export async function startServer(config: { name: string; command: string; args:
|
||||
|
||||
function cleanupServer(server: MCPServerInstance): void {
|
||||
for (const [, pending] of server.pendingRequests) {
|
||||
clearTimeout(pending.timer);
|
||||
if (pending.timer) clearTimeout(pending.timer);
|
||||
pending.reject(new Error('MCP 服务器已关闭'));
|
||||
}
|
||||
server.pendingRequests.clear();
|
||||
|
||||
@@ -127,9 +127,9 @@ export function killToolProcess(): boolean {
|
||||
/** HTTP 请求超时(毫秒),默认 30s,可通过 IPC 设置 */
|
||||
let HTTP_TIMEOUT = 30_000;
|
||||
|
||||
/** 设置 HTTP 请求超时(毫秒) */
|
||||
/** 设置 HTTP 请求超时(毫秒),0=禁用超时 */
|
||||
export function setHTTPTimeout(ms: number): void {
|
||||
HTTP_TIMEOUT = Math.max(5000, ms);
|
||||
HTTP_TIMEOUT = ms === 0 ? 0 : Math.max(5000, ms);
|
||||
}
|
||||
|
||||
// ── 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> {
|
||||
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 tid = setTimeout(() => controller.abort(), timeout);
|
||||
try {
|
||||
@@ -444,14 +451,19 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
||||
const headers = buildFetchHeaders(url, attempt, useMobileUA);
|
||||
|
||||
try {
|
||||
let resp: Response;
|
||||
if (HTTP_TIMEOUT > 0) {
|
||||
const controller = new AbortController();
|
||||
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
|
||||
let resp: Response;
|
||||
try {
|
||||
resp = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' });
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
} else {
|
||||
// 0 = 禁用超时
|
||||
resp = await fetch(url, { headers, redirect: 'follow' });
|
||||
}
|
||||
|
||||
if (!resp.ok) {
|
||||
// 反爬信号(403/503/429)→ 跳过后续 fetch,直接走浏览器回退
|
||||
@@ -566,7 +578,7 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
||||
} catch (err) {
|
||||
const errMsg = (err as Error).message;
|
||||
if (errMsg.includes('abort') && attempt < maxAttempts - 1) {
|
||||
lastError = `请求超时 (${HTTP_TIMEOUT / 1000}s)(将重试)`;
|
||||
lastError = `请求超时 (${HTTP_TIMEOUT > 0 ? HTTP_TIMEOUT / 1000 : '∞'}s)(将重试)`;
|
||||
continue;
|
||||
}
|
||||
if (errMsg.includes('abort')) {
|
||||
|
||||
@@ -1336,12 +1336,21 @@ async function handleThinking(
|
||||
|
||||
const abortController = registerAbortController();
|
||||
|
||||
// ── 流式超时保护 — 动态计算:基础 120s + 上下文 token 数 * 0.5s/1K tokens ──
|
||||
// ── 流式超时保护 — 0=禁用, 正数=自定义, -1/未设置=动态计算 ──
|
||||
const baseTimeout = state.get<number>('streamTimeout', 0);
|
||||
const numCtxForTimeout = getEffectiveNumCtx();
|
||||
const dynamicTimeout = baseTimeout > 0 ? baseTimeout : Math.max(120_000, 120_000 + Math.floor(numCtxForTimeout / 1000) * 500);
|
||||
// 上限 10 分钟,避免极端值
|
||||
const STREAM_TIMEOUT_MS = Math.min(dynamicTimeout, 600_000);
|
||||
let STREAM_TIMEOUT_MS: number;
|
||||
if (baseTimeout === 0) {
|
||||
// 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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user