Files
chat-api-demo/platforms/qwen/router.js

168 lines
4.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const express = require("express");
const router = express.Router();
const QWEN_API_KEY = process.env.QWEN_API_KEY;
const QWEN_MODEL = process.env.QWEN_MODEL || "qwen-plus";
const QWEN_BASE_URL =
process.env.QWEN_BASE_URL ||
"https://dashscope.aliyuncs.com/compatible-mode/v1";
router.post("/chat", async (req, res) => {
try {
if (!QWEN_API_KEY) {
return res.status(500).json({
error: "缺少 QWEN_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(`${QWEN_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${QWEN_API_KEY}`,
},
body: JSON.stringify({
model: model || QWEN_MODEL,
messages,
temperature: typeof temperature === "number" ? temperature : 0.7,
}),
});
const data = await response.json();
if (!response.ok) {
return res.status(response.status).json({
error: "调用千问失败",
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 (!QWEN_API_KEY) {
return res.status(500).json({
error: "缺少 QWEN_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(`${QWEN_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${QWEN_API_KEY}`,
},
body: JSON.stringify({
model: model || QWEN_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;