167 lines
4.5 KiB
JavaScript
167 lines
4.5 KiB
JavaScript
const express = require("express");
|
||
|
||
const router = express.Router();
|
||
|
||
const VOLCENGINE_API_KEY = process.env.VOLCENGINE_API_KEY;
|
||
const VOLCENGINE_MODEL = process.env.VOLCENGINE_MODEL || "your_endpoint_id";
|
||
const VOLCENGINE_BASE_URL =
|
||
process.env.VOLCENGINE_BASE_URL || "https://ark.cn-beijing.volces.com/api/v3";
|
||
|
||
router.post("/chat", async (req, res) => {
|
||
try {
|
||
if (!VOLCENGINE_API_KEY) {
|
||
return res.status(500).json({
|
||
error: "缺少 VOLCENGINE_API_KEY,请先在 .env 中配置",
|
||
});
|
||
}
|
||
|
||
const { messages, model, temperature } = req.body || {};
|
||
if (!Array.isArray(messages) || messages.length === 0) {
|
||
return res.status(400).json({
|
||
error: "请求体 messages 必须是非空数组",
|
||
});
|
||
}
|
||
|
||
const response = await fetch(`${VOLCENGINE_BASE_URL}/chat/completions`, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${VOLCENGINE_API_KEY}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model: model || VOLCENGINE_MODEL,
|
||
messages,
|
||
temperature: typeof temperature === "number" ? temperature : 0.7,
|
||
}),
|
||
});
|
||
|
||
const data = await response.json();
|
||
if (!response.ok) {
|
||
return res.status(response.status).json({
|
||
error: "调用火山引擎 Chat 接口失败",
|
||
detail: data,
|
||
});
|
||
}
|
||
|
||
return res.json({
|
||
id: data.id,
|
||
model: data.model,
|
||
content: data?.choices?.[0]?.message?.content || "",
|
||
raw: data,
|
||
});
|
||
} catch (error) {
|
||
return res.status(500).json({
|
||
error: "服务内部错误",
|
||
detail: error.message,
|
||
});
|
||
}
|
||
});
|
||
|
||
router.post("/chat/stream", async (req, res) => {
|
||
try {
|
||
if (!VOLCENGINE_API_KEY) {
|
||
return res.status(500).json({
|
||
error: "缺少 VOLCENGINE_API_KEY,请先在 .env 中配置",
|
||
});
|
||
}
|
||
|
||
const { messages, model, temperature } = req.body || {};
|
||
if (!Array.isArray(messages) || messages.length === 0) {
|
||
return res.status(400).json({
|
||
error: "请求体 messages 必须是非空数组",
|
||
});
|
||
}
|
||
|
||
res.setHeader("Content-Type", "text/event-stream; charset=utf-8");
|
||
res.setHeader("Cache-Control", "no-cache, no-transform");
|
||
res.setHeader("Connection", "keep-alive");
|
||
res.flushHeaders();
|
||
|
||
const controller = new AbortController();
|
||
req.on("close", () => {
|
||
controller.abort();
|
||
});
|
||
|
||
const response = await fetch(`${VOLCENGINE_BASE_URL}/chat/completions`, {
|
||
method: "POST",
|
||
headers: {
|
||
"Content-Type": "application/json",
|
||
Authorization: `Bearer ${VOLCENGINE_API_KEY}`,
|
||
},
|
||
body: JSON.stringify({
|
||
model: model || VOLCENGINE_MODEL,
|
||
messages,
|
||
temperature: typeof temperature === "number" ? temperature : 0.7,
|
||
stream: true,
|
||
}),
|
||
signal: controller.signal,
|
||
});
|
||
|
||
if (!response.ok) {
|
||
const errorData = await response.json().catch(() => ({}));
|
||
res.write(
|
||
`data: ${JSON.stringify({
|
||
error: "调用火山引擎流式接口失败",
|
||
detail: errorData,
|
||
})}\n\n`,
|
||
);
|
||
return res.end();
|
||
}
|
||
|
||
if (!response.body) {
|
||
res.write(`data: ${JSON.stringify({ error: "未获取到流式响应体" })}\n\n`);
|
||
return res.end();
|
||
}
|
||
|
||
const reader = response.body.getReader();
|
||
const decoder = new TextDecoder("utf-8");
|
||
let buffer = "";
|
||
|
||
while (true) {
|
||
const { done, value } = await reader.read();
|
||
if (done) break;
|
||
|
||
buffer += decoder.decode(value, { stream: true });
|
||
const lines = buffer.split("\n");
|
||
buffer = lines.pop() || "";
|
||
|
||
for (const line of lines) {
|
||
const trimmed = line.trim();
|
||
if (!trimmed.startsWith("data:")) continue;
|
||
|
||
const payload = trimmed.slice(5).trim();
|
||
if (payload === "[DONE]") {
|
||
res.write("data: [DONE]\n\n");
|
||
res.end();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
const parsed = JSON.parse(payload);
|
||
const delta = parsed?.choices?.[0]?.delta?.content;
|
||
if (delta) {
|
||
res.write(`data: ${JSON.stringify({ delta })}\n\n`);
|
||
}
|
||
} catch (_error) {
|
||
// 忽略非 JSON 片段,继续解析后续流内容
|
||
}
|
||
}
|
||
}
|
||
|
||
res.write("data: [DONE]\n\n");
|
||
return res.end();
|
||
} catch (error) {
|
||
if (!res.headersSent) {
|
||
return res.status(500).json({
|
||
error: "服务内部错误",
|
||
detail: error.message,
|
||
});
|
||
}
|
||
|
||
res.write(`data: ${JSON.stringify({ error: error.message })}\n\n`);
|
||
return res.end();
|
||
}
|
||
});
|
||
|
||
module.exports = router;
|