diff --git a/src/api/client.ts b/src/api/client.ts index 2de6f0b..ab987ee 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -60,7 +60,7 @@ export class ApiError extends Error { code?: string, details?: InsufficientBalanceDetails | Record, ) { - super(message); + super(sanitizeUserFacingErrorMessage(message)); this.name = 'ApiError'; this.status = status; this.code = code; @@ -68,6 +68,21 @@ export class ApiError extends Error { } } +function sanitizeUserFacingErrorMessage(message: string) { + const normalized = String(message ?? '').trim(); + if (!normalized) return normalized; + if (!/goose|goosed/i.test(normalized)) return normalized; + if (/超时|timeout/i.test(normalized)) { + return '后端连接超时,请确认后端服务正常后重试'; + } + if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) { + return '后端连接失败,请稍后重试'; + } + return normalized + .replace(/\bgoosed\b/gi, '后端服务') + .replace(/\bgoose\b/gi, '后端'); +} + async function parseErrorResponse(res: Response): Promise<{ message: string; code?: string; @@ -95,7 +110,7 @@ async function parseErrorResponse(res: Response): Promise<{ const details = nested.details ?? body.details; if (code === 'INSUFFICIENT_BALANCE') { return { - message, + message: sanitizeUserFacingErrorMessage(message), code, details: { code: 'INSUFFICIENT_BALANCE' as const, @@ -112,14 +127,14 @@ async function parseErrorResponse(res: Response): Promise<{ }; } return { - message, + message: sanitizeUserFacingErrorMessage(message), code, details: details && typeof details === 'object' ? (details as InsufficientBalanceDetails | Record) : undefined, }; } catch { - return { message: text || res.statusText }; + return { message: sanitizeUserFacingErrorMessage(text || res.statusText) }; } } @@ -200,12 +215,17 @@ async function portalFetch(path: string, init?: RequestInit): Promise { function formatNetworkError(err: unknown) { const message = err instanceof Error ? err.message : '网络请求失败'; if (err instanceof DOMException && err.name === 'AbortError') { - return '连接 goose 服务超时,请确认 goosed/H5 后端正在运行后重试'; + return '后端连接超时,请确认后端服务正常后重试'; } if (message.includes('Failed to fetch') || message.includes('NetworkError')) { return '无法连接后端服务,请先运行: pnpm dev 或 node server.mjs'; } - return message; + return sanitizeUserFacingErrorMessage(message); +} + +function sanitizeSessionEvent(event: SessionEvent): SessionEvent { + if (event.type !== 'Error') return event; + return { ...event, error: sanitizeUserFacingErrorMessage(event.error) }; } async function apiFetch(path: string, init?: RequestInit): Promise { @@ -1921,7 +1941,7 @@ export function subscribeSessionEvents( } continue; } - onEvent(JSON.parse(data) as SessionEvent); + onEvent(sanitizeSessionEvent(JSON.parse(data) as SessionEvent)); } catch { // ignore malformed frames } diff --git a/tkmind-proxy.mjs b/tkmind-proxy.mjs index 531890e..9d33e88 100644 --- a/tkmind-proxy.mjs +++ b/tkmind-proxy.mjs @@ -14,6 +14,21 @@ function isHttpsTarget(target) { return target.startsWith('https://'); } +function sanitizeUserFacingProxyMessage(message, fallback = '后端连接失败,请稍后重试') { + const normalized = String(message ?? '').trim(); + if (!normalized) return fallback; + if (!/goose|goosed/i.test(normalized)) return normalized; + if (/超时|timeout/i.test(normalized)) { + return '后端连接超时,请确认后端服务正常后重试'; + } + if (/不可用|连接失败|failed to fetch|networkerror|fetch failed|upstream|econn|enotfound/i.test(normalized)) { + return fallback; + } + return normalized + .replace(/\bgoosed\b/gi, '后端服务') + .replace(/\bgoose\b/gi, '后端'); +} + async function apiFetch(target, apiSecret, pathname, init = {}) { const url = new URL(pathname, target); const headers = { @@ -308,7 +323,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, res.status(upstream.status).json(payload); } catch (err) { - res.status(500).json({ message: err instanceof Error ? err.message : '恢复会话失败' }); + res.status(500).json({ + message: sanitizeUserFacingProxyMessage( + err instanceof Error ? err.message : '恢复会话失败', + '恢复会话失败', + ), + }); } }, ], @@ -339,12 +359,15 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, if (owned.has(item.id)) sessionsById.set(item.id, item); } } catch (err) { - lastFailure = err instanceof Error ? err.message : '读取会话失败'; + lastFailure = sanitizeUserFacingProxyMessage( + err instanceof Error ? err.message : '读取会话失败', + '读取会话失败', + ); } } if (healthyTargets === 0) { - res.status(502).json({ message: lastFailure ?? '所有 goose 服务不可用' }); + res.status(502).json({ message: lastFailure ?? '后端连接失败,请稍后重试' }); return; } if (healthyTargets < targets.length) { @@ -352,7 +375,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, } res.json({ sessions: [...sessionsById.values()] }); } catch (err) { - res.status(500).json({ message: err instanceof Error ? err.message : '读取会话失败' }); + res.status(500).json({ + message: sanitizeUserFacingProxyMessage( + err instanceof Error ? err.message : '读取会话失败', + '读取会话失败', + ), + }); } }, ], @@ -377,7 +405,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, req.agentSessionId = sessionId; await build(req, res, next); } catch (err) { - res.status(500).json({ message: err instanceof Error ? err.message : '请求失败' }); + res.status(500).json({ + message: sanitizeUserFacingProxyMessage( + err instanceof Error ? err.message : '请求失败', + '请求失败', + ), + }); } }, ]; @@ -441,7 +474,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, source.on('error', () => res.end()); source.pipe(billingTransform); } catch (err) { - res.status(502).json({ message: err instanceof Error ? err.message : 'SSE 代理失败' }); + res.status(502).json({ + message: sanitizeUserFacingProxyMessage( + err instanceof Error ? err.message : 'SSE 代理失败', + 'SSE 代理失败', + ), + }); } }; @@ -494,7 +532,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth, sendProxyResponse(res, upstream); } catch (err) { - res.status(502).json({ message: err instanceof Error ? err.message : '代理失败' }); + res.status(502).json({ + message: sanitizeUserFacingProxyMessage( + err instanceof Error ? err.message : '代理失败', + '代理失败', + ), + }); } };