Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 52 additions & 47 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,60 +35,65 @@ export async function POST(req: Request) {
// ====== 尝试优雅降级代理到 Java 后端 ======
// Java 后端 /openai/responses/stream 带 @SaCheckLogin,匿名请求必 401;
// 直接跳过代理省掉 5s 超时,也避免 401 文案被上游误显示为"unauthorized"。
const hasAuthToken = Boolean(req.headers.get("x-satoken"));
try {
if (!hasAuthToken) {
throw new Error("Anonymous request, skip backend proxy.");
}
const backendUrl = process.env.BACKEND_URL;
if (!backendUrl) throw new Error("BACKEND_URL is not configured.");
// 匿名分支走显式 if 短路,不进 try/catch —— 否则每个匿名请求都会被 catch
// 打成 "Java Backend unavailable" 带 stack 的 warn,生产日志会刷爆
// (Copilot CR #1)。
const satoken = req.headers.get("x-satoken");
if (!satoken) {
console.log(
"[Chat Fallback Proxy] ⏭️ Anonymous request, skip backend proxy, use local inference.",
);
} else {
Comment on lines +41 to +46
Copy link

Copilot AI Apr 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

req.clone() is always executed, but in the anonymous (!satoken) path the proxy is skipped so proxyReq is never used. To avoid unnecessary body cloning/memory overhead on high-volume anonymous traffic, consider cloning lazily only inside the authenticated branch right before the proxy fetch (or only when you actually need to call proxyReq.text()).

Copilot uses AI. Check for mistakes.
try {
const backendUrl = process.env.BACKEND_URL;
if (!backendUrl) throw new Error("BACKEND_URL is not configured.");

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 5000); // 5秒超时

// 原封不动把前端的参数丢给 Java
let proxyRes: Response;
try {
proxyRes = await fetch(`${backendUrl}/openai/responses/stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
// 浏览器侧用 x-satoken 传递 token,转发给后端时改回后端期望的 satoken
...(req.headers.get("x-satoken")
? { satoken: req.headers.get("x-satoken")! }
: {}),
},
body: await proxyReq.text(),
signal: controller.signal,
});
} finally {
// 无论成功还是抛出(网络错误/超时中断),都清除定时器
clearTimeout(timeoutId);
}
// 原封不动把前端的参数丢给 Java
let proxyRes: Response;
try {
proxyRes = await fetch(`${backendUrl}/openai/responses/stream`, {
method: "POST",
headers: {
"Content-Type": "application/json",
// 浏览器侧用 x-satoken 传递 token,转发给后端时改回后端期望的 satoken
satoken,
},
body: await proxyReq.text(),
signal: controller.signal,
});
} finally {
// 无论成功还是抛出(网络错误/超时中断),都清除定时器
clearTimeout(timeoutId);
}

// 如果 Java 后端返回成功,则直接把它的流传回浏览器,提前结束
if (proxyRes.ok && proxyRes.body) {
console.log(
"[Chat Fallback Proxy] 🚀 Java Backend responded successfully. Piping stream...",
);
return new Response(proxyRes.body, {
headers: {
"Content-Type":
proxyRes.headers.get("Content-Type") || "text/plain; charset=utf-8",
},
});
} else {
// 如果 Java 后端返回成功,则直接把它的流传回浏览器,提前结束
if (proxyRes.ok && proxyRes.body) {
console.log(
"[Chat Fallback Proxy] 🚀 Java Backend responded successfully. Piping stream...",
);
return new Response(proxyRes.body, {
headers: {
"Content-Type":
proxyRes.headers.get("Content-Type") ||
"text/plain; charset=utf-8",
},
});
} else {
console.warn(
`[Chat Fallback Proxy] ⚠️ Java Backend returned status: ${proxyRes.status}, fallback to local Next.js inference.`,
);
}
} catch (error) {
console.warn(
`[Chat Fallback Proxy] ⚠️ Java Backend returned status: ${proxyRes.status}, fallback to local Next.js inference.`,
`[Chat Fallback Proxy] ❌ Java Backend unavailable or timed out, fallback to local Next.js inference. Error:`,
error,
);
}
} catch (error) {
console.warn(
`[Chat Fallback Proxy] ❌ Java Backend unavailable or timed out, fallback to local Next.js inference. Error:`,
error,
);
}
// ====== 代理失败,继续往下走,启用备选方案(本地直连 AI)======
// ====== 代理失败/匿名短路,继续往下走,启用备选方案(本地直连 AI)======

try {
// 先把 body 消费掉,再并行验证用户身份
Expand Down
14 changes: 13 additions & 1 deletion lib/ai/providers/intern.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,22 @@ export function createInternModel() {
return deepseek("deepseek-chat");
}

// 显式校验 ZHIPU_API_KEY:若漏配,下游 401 又会在 UI 上变成 "unauthorized"
// 透传 —— 正好绕回 issue #285 原本要修的症状。在这里早抛出带指引的错误,
// 运维看日志一眼知道补哪个 env var,避免二次塌房(Copilot CR #2)。
const zhipuApiKey = process.env.ZHIPU_API_KEY;
if (!zhipuApiKey || zhipuApiKey.trim() === "") {
throw new Error(
"Missing required environment variable ZHIPU_API_KEY. " +
"配置位置:Vercel Project Settings → Environment Variables。" +
"免费 key 从 https://open.bigmodel.cn/ 获取。",
);
}

const glm = createOpenAICompatible({
name: "zhipu",
baseURL: "https://open.bigmodel.cn/api/paas/v4/",
apiKey: process.env.ZHIPU_API_KEY,
apiKey: zhipuApiKey,
});

return glm("glm-4.6v-flash");
Expand Down
Loading