feat: 新增火山引擎 chat 接口demo
This commit is contained in:
@@ -6,3 +6,7 @@ QWEN_BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
|
||||
DEEPSEEK_API_KEY=your_deepseek_api_key
|
||||
DEEPSEEK_MODEL=deepseek-chat
|
||||
DEEPSEEK_BASE_URL=https://api.deepseek.com/v1
|
||||
|
||||
VOLCENGINE_API_KEY=your_volcengine_api_key
|
||||
VOLCENGINE_MODEL=your_endpoint_id
|
||||
VOLCENGINE_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||
|
||||
41
README.md
41
README.md
@@ -16,6 +16,7 @@ cp .env.example .env
|
||||
|
||||
在 `.env` 中填入你的 `QWEN_API_KEY`(DashScope API Key)。
|
||||
如需调用 DeepSeek,再填入 `DEEPSEEK_API_KEY`。
|
||||
如需调用火山引擎,再填入 `VOLCENGINE_API_KEY` 与 `VOLCENGINE_MODEL`(方舟 Endpoint ID)。
|
||||
|
||||
## 3. 启动服务
|
||||
|
||||
@@ -134,6 +135,44 @@ curl -N -X POST http://localhost:3000/api/deepseek/chat/stream \
|
||||
└── platforms
|
||||
├── deepseek
|
||||
│ └── router.js
|
||||
└── qwen
|
||||
├── qwen
|
||||
│ └── router.js
|
||||
└── volcengine
|
||||
└── router.js
|
||||
```
|
||||
|
||||
## 8. 调用火山引擎 Chat 接口 demo
|
||||
|
||||
### 火山普通接口地址
|
||||
|
||||
`POST /api/volcengine/chat`
|
||||
|
||||
### 火山普通请求示例
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:3000/api/volcengine/chat \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role":"system","content":"你是一个简洁的助手"},
|
||||
{"role":"user","content":"你好,请介绍一下你自己"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 火山流式接口地址
|
||||
|
||||
`POST /api/volcengine/chat/stream`
|
||||
|
||||
### 火山流式请求示例
|
||||
|
||||
```bash
|
||||
curl -N -X POST http://localhost:3000/api/volcengine/chat/stream \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"messages": [
|
||||
{"role":"system","content":"你是一个简洁的助手"},
|
||||
{"role":"user","content":"请用三句话介绍流式输出"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
2
index.js
2
index.js
@@ -2,6 +2,7 @@ require("dotenv").config();
|
||||
const express = require("express");
|
||||
const qwenRouter = require("./platforms/qwen/router");
|
||||
const deepseekRouter = require("./platforms/deepseek/router");
|
||||
const volcengineRouter = require("./platforms/volcengine/router");
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
@@ -14,6 +15,7 @@ app.get("/health", (_req, res) => {
|
||||
|
||||
app.use("/api/qwen", qwenRouter);
|
||||
app.use("/api/deepseek", deepseekRouter);
|
||||
app.use("/api/volcengine", volcengineRouter);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
console.log(`Server running at http://localhost:${PORT}`);
|
||||
|
||||
166
platforms/volcengine/router.js
Normal file
166
platforms/volcengine/router.js
Normal file
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user