From 8631a4c9a5d147bbb0fb02b9b9d19748fa07aef1 Mon Sep 17 00:00:00 2001 From: alboped Date: Wed, 15 Apr 2026 17:38:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E7=81=AB=E5=B1=B1?= =?UTF-8?q?=E5=BC=95=E6=93=8E=20chat=20=E6=8E=A5=E5=8F=A3demo?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 4 + README.md | 41 +++++++- index.js | 2 + platforms/volcengine/router.js | 166 +++++++++++++++++++++++++++++++++ 4 files changed, 212 insertions(+), 1 deletion(-) create mode 100644 platforms/volcengine/router.js diff --git a/.env.example b/.env.example index 6305e14..5abb69d 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/README.md b/README.md index dc4c2c3..126380b 100644 --- a/README.md +++ b/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":"请用三句话介绍流式输出"} + ] + }' +``` diff --git a/index.js b/index.js index cb9daec..f6b974e 100644 --- a/index.js +++ b/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}`); diff --git a/platforms/volcengine/router.js b/platforms/volcengine/router.js new file mode 100644 index 0000000..973c84c --- /dev/null +++ b/platforms/volcengine/router.js @@ -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;