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 }) => {
|
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 };
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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,13 +451,18 @@ export async function handleWebFetch(params: { url: string; max_chars?: number;
|
|||||||
const headers = buildFetchHeaders(url, attempt, useMobileUA);
|
const headers = buildFetchHeaders(url, attempt, useMobileUA);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const controller = new AbortController();
|
|
||||||
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
|
|
||||||
let resp: Response;
|
let resp: Response;
|
||||||
try {
|
if (HTTP_TIMEOUT > 0) {
|
||||||
resp = await fetch(url, { headers, signal: controller.signal, redirect: 'follow' });
|
const controller = new AbortController();
|
||||||
} finally {
|
const timeoutId = setTimeout(() => controller.abort(), HTTP_TIMEOUT);
|
||||||
clearTimeout(timeoutId);
|
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) {
|
if (!resp.ok) {
|
||||||
@@ -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')) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user