Hide goose from user-facing connection errors
This commit is contained in:
+27
-7
@@ -60,7 +60,7 @@ export class ApiError extends Error {
|
|||||||
code?: string,
|
code?: string,
|
||||||
details?: InsufficientBalanceDetails | Record<string, unknown>,
|
details?: InsufficientBalanceDetails | Record<string, unknown>,
|
||||||
) {
|
) {
|
||||||
super(message);
|
super(sanitizeUserFacingErrorMessage(message));
|
||||||
this.name = 'ApiError';
|
this.name = 'ApiError';
|
||||||
this.status = status;
|
this.status = status;
|
||||||
this.code = code;
|
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<{
|
async function parseErrorResponse(res: Response): Promise<{
|
||||||
message: string;
|
message: string;
|
||||||
code?: string;
|
code?: string;
|
||||||
@@ -95,7 +110,7 @@ async function parseErrorResponse(res: Response): Promise<{
|
|||||||
const details = nested.details ?? body.details;
|
const details = nested.details ?? body.details;
|
||||||
if (code === 'INSUFFICIENT_BALANCE') {
|
if (code === 'INSUFFICIENT_BALANCE') {
|
||||||
return {
|
return {
|
||||||
message,
|
message: sanitizeUserFacingErrorMessage(message),
|
||||||
code,
|
code,
|
||||||
details: {
|
details: {
|
||||||
code: 'INSUFFICIENT_BALANCE' as const,
|
code: 'INSUFFICIENT_BALANCE' as const,
|
||||||
@@ -112,14 +127,14 @@ async function parseErrorResponse(res: Response): Promise<{
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
message,
|
message: sanitizeUserFacingErrorMessage(message),
|
||||||
code,
|
code,
|
||||||
details: details && typeof details === 'object'
|
details: details && typeof details === 'object'
|
||||||
? (details as InsufficientBalanceDetails | Record<string, unknown>)
|
? (details as InsufficientBalanceDetails | Record<string, unknown>)
|
||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
return { message: text || res.statusText };
|
return { message: sanitizeUserFacingErrorMessage(text || res.statusText) };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,12 +215,17 @@ async function portalFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
|||||||
function formatNetworkError(err: unknown) {
|
function formatNetworkError(err: unknown) {
|
||||||
const message = err instanceof Error ? err.message : '网络请求失败';
|
const message = err instanceof Error ? err.message : '网络请求失败';
|
||||||
if (err instanceof DOMException && err.name === 'AbortError') {
|
if (err instanceof DOMException && err.name === 'AbortError') {
|
||||||
return '连接 goose 服务超时,请确认 goosed/H5 后端正在运行后重试';
|
return '后端连接超时,请确认后端服务正常后重试';
|
||||||
}
|
}
|
||||||
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
|
if (message.includes('Failed to fetch') || message.includes('NetworkError')) {
|
||||||
return '无法连接后端服务,请先运行: pnpm dev 或 node server.mjs';
|
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<T>(path: string, init?: RequestInit): Promise<T> {
|
async function apiFetch<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
@@ -1921,7 +1941,7 @@ export function subscribeSessionEvents(
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
onEvent(JSON.parse(data) as SessionEvent);
|
onEvent(sanitizeSessionEvent(JSON.parse(data) as SessionEvent));
|
||||||
} catch {
|
} catch {
|
||||||
// ignore malformed frames
|
// ignore malformed frames
|
||||||
}
|
}
|
||||||
|
|||||||
+50
-7
@@ -14,6 +14,21 @@ function isHttpsTarget(target) {
|
|||||||
return target.startsWith('https://');
|
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 = {}) {
|
async function apiFetch(target, apiSecret, pathname, init = {}) {
|
||||||
const url = new URL(pathname, target);
|
const url = new URL(pathname, target);
|
||||||
const headers = {
|
const headers = {
|
||||||
@@ -308,7 +323,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth,
|
|||||||
|
|
||||||
res.status(upstream.status).json(payload);
|
res.status(upstream.status).json(payload);
|
||||||
} catch (err) {
|
} 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);
|
if (owned.has(item.id)) sessionsById.set(item.id, item);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
lastFailure = err instanceof Error ? err.message : '读取会话失败';
|
lastFailure = sanitizeUserFacingProxyMessage(
|
||||||
|
err instanceof Error ? err.message : '读取会话失败',
|
||||||
|
'读取会话失败',
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (healthyTargets === 0) {
|
if (healthyTargets === 0) {
|
||||||
res.status(502).json({ message: lastFailure ?? '所有 goose 服务不可用' });
|
res.status(502).json({ message: lastFailure ?? '后端连接失败,请稍后重试' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (healthyTargets < targets.length) {
|
if (healthyTargets < targets.length) {
|
||||||
@@ -352,7 +375,12 @@ export function createTkmindProxy({ apiTarget, apiTargets, apiSecret, userAuth,
|
|||||||
}
|
}
|
||||||
res.json({ sessions: [...sessionsById.values()] });
|
res.json({ sessions: [...sessionsById.values()] });
|
||||||
} catch (err) {
|
} 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;
|
req.agentSessionId = sessionId;
|
||||||
await build(req, res, next);
|
await build(req, res, next);
|
||||||
} catch (err) {
|
} 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.on('error', () => res.end());
|
||||||
source.pipe(billingTransform);
|
source.pipe(billingTransform);
|
||||||
} catch (err) {
|
} 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);
|
sendProxyResponse(res, upstream);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(502).json({ message: err instanceof Error ? err.message : '代理失败' });
|
res.status(502).json({
|
||||||
|
message: sanitizeUserFacingProxyMessage(
|
||||||
|
err instanceof Error ? err.message : '代理失败',
|
||||||
|
'代理失败',
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user