mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 流式和非流式客户端可共存
This commit is contained in:
@@ -11,6 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **支持新网站**
|
||||
- 初步支持对 Gemini 网页版的支持
|
||||
|
||||
### Changed
|
||||
- **接口逻辑优化**
|
||||
- 移除了流式与非流式接口的统一开关限制,现在两种客户端可同时存在。
|
||||
- **行为说明**:
|
||||
- **流式请求 (stream: true)**:支持无限排队,并通过心跳机制保持连接以等待结果。
|
||||
- **非流式请求**:在队列未满时正常处理;当队列满时,将立即拒绝并返回明确的拒绝原因,但仍受 `maxQueueSize` 限制。
|
||||
|
||||
## [2.2.3] - 2025-12-12
|
||||
|
||||
### Added
|
||||
|
||||
+3
-8
@@ -6,16 +6,11 @@ server:
|
||||
port: 3000
|
||||
# 鉴权 Token (Bearer Token) (可使用 npm run genkey 生成)
|
||||
auth: sk-change-me-to-your-secure-key
|
||||
# 保活
|
||||
# 流式请求心跳设置 (自动对 stream: true 的请求发送心跳防止超时)
|
||||
keepalive:
|
||||
# 是否启用流式保活
|
||||
# 使用OpenAI接口的标准流式接口格式,客户端请求需强制使用 stream: true
|
||||
enable: false
|
||||
|
||||
# 心跳模式
|
||||
# "comment": (推荐) 发送 :keepalive 注释。不污染数据,绝大多数 SDK 支持,不会影响接口标准
|
||||
# "content": (备用) 在 choices[0].delta.content = "" 中发送空字符串
|
||||
# 仅当你使用的客户端非常特殊,必须收到 data JSON 包才重置超时时使用
|
||||
# "comment": (推荐) 发送 :keepalive 注释,不污染数据
|
||||
# "content": (备用) 发送空 delta,仅当客户端必须收到 JSON 包才重置超时时使用
|
||||
mode: "comment"
|
||||
|
||||
backend:
|
||||
|
||||
+4
-11
@@ -32,16 +32,11 @@ server:
|
||||
port: 3000
|
||||
# 鉴权 Token (Bearer Token) (可使用 npm run genkey 生成)
|
||||
auth: ${generateApiKey()}
|
||||
# 保活
|
||||
# 流式请求心跳设置 (自动对 stream: true 的请求发送心跳防止超时)
|
||||
keepalive:
|
||||
# 是否启用流式保活
|
||||
# 使用OpenAI接口的标准流式接口格式,客户端请求需强制使用 stream: true
|
||||
enable: false
|
||||
|
||||
# 心跳模式
|
||||
# "comment": (推荐) 发送 :keepalive 注释。不污染数据,绝大多数 SDK 支持,不会影响接口标准
|
||||
# "content": (备用) 在 choices[0].delta.content = "" 中发送空字符串
|
||||
# 仅当你使用的客户端非常特殊,必须收到 data JSON 包才重置超时时使用
|
||||
# "comment": (推荐) 发送 :keepalive 注释,不污染数据
|
||||
# "content": (备用) 发送空 delta,仅当客户端必须收到 JSON 包才重置超时时使用
|
||||
mode: "comment"
|
||||
|
||||
backend:
|
||||
@@ -156,11 +151,9 @@ export function loadConfig() {
|
||||
// 设置 keepalive 配置默认值
|
||||
if (!config.server.keepalive) {
|
||||
config.server.keepalive = {
|
||||
enable: true,
|
||||
mode: 'comment'
|
||||
};
|
||||
} else {
|
||||
if (config.server.keepalive.enable === undefined) config.server.keepalive.enable = true;
|
||||
if (config.server.keepalive.mode === undefined) config.server.keepalive.mode = 'comment';
|
||||
// 验证 mode 值
|
||||
if (!['comment', 'content'].includes(config.server.keepalive.mode)) {
|
||||
@@ -194,7 +187,7 @@ export function loadConfig() {
|
||||
|
||||
logger.debug('配置器', '已加载 config.yaml');
|
||||
logger.debug('配置器', '后端类型:', config.backend.type);
|
||||
logger.debug('配置器', '流式保活:', config.server.keepalive.enable ? '已启用' : '已禁用');
|
||||
logger.debug('配置器', '流式心跳模式:', config.server.keepalive.mode);
|
||||
if (config.backend.type === 'gemini_biz') {
|
||||
logger.debug('配置器', `GeminiBiz 入口: ${config.backend.geminiBiz.entryUrl}`);
|
||||
}
|
||||
|
||||
+37
-17
@@ -1,5 +1,4 @@
|
||||
import { getBackend } from '../backend/index.js';
|
||||
import { getModelsForBackend, resolveModelId } from '../backend/models.js';
|
||||
import { select, input } from '@inquirer/prompts';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
@@ -7,7 +6,7 @@ import http from 'http';
|
||||
import { logger } from './logger.js';
|
||||
|
||||
// 使用统一后端获取配置和函数
|
||||
const { config, name, TEMP_DIR } = getBackend();
|
||||
const { config, name, TEMP_DIR, getModels } = getBackend();
|
||||
|
||||
logger.info('CLI/Test', `测试工具启动 (后端适配器: ${name})`);
|
||||
|
||||
@@ -15,7 +14,7 @@ logger.info('CLI/Test', `测试工具启动 (后端适配器: ${name})`);
|
||||
* 选择模型
|
||||
*/
|
||||
async function selectModel() {
|
||||
const models = getModelsForBackend(name);
|
||||
const models = getModels(); // 使用后端统一接口,支持聚合模式
|
||||
const choices = [
|
||||
{ name: 'Skip(使用默认模型)', value: null },
|
||||
...models.data.map(m => ({ name: m.id, value: m.id }))
|
||||
@@ -65,16 +64,16 @@ async function promptForImages() {
|
||||
|
||||
/**
|
||||
* HTTP 测试模式 - OpenAI 格式
|
||||
* @param {string} prompt - 提示词
|
||||
* @param {string|null} modelId - 模型 ID
|
||||
* @param {string[]} imagePaths - 图片路径
|
||||
* @param {boolean} isStreaming - 是否使用流式模式
|
||||
*/
|
||||
async function testViaHttpOpenAI(prompt, modelId, imagePaths) {
|
||||
async function testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming) {
|
||||
const PORT = config.server.port || 3000;
|
||||
const AUTH_TOKEN = config.server.auth;
|
||||
const KEEPALIVE_ENABLED = config.server.keepalive?.enable ?? true;
|
||||
|
||||
logger.info('CLI/Test', 'HTTP 测试 - OpenAI 模式');
|
||||
if (KEEPALIVE_ENABLED) {
|
||||
logger.info('CLI/Test', '流式保活已启用,将使用 stream=true');
|
||||
}
|
||||
logger.info('CLI/Test', `HTTP 测试 - ${isStreaming ? '流式模式' : '非流式模式'}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// 构造请求体
|
||||
@@ -104,7 +103,7 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths) {
|
||||
|
||||
const body = {
|
||||
messages,
|
||||
stream: KEEPALIVE_ENABLED, // 如果启用 keepalive,必须使用 stream
|
||||
stream: isStreaming,
|
||||
...(modelId && { model: modelId })
|
||||
};
|
||||
|
||||
@@ -123,7 +122,7 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths) {
|
||||
};
|
||||
|
||||
const req = http.request(options, (res) => {
|
||||
if (KEEPALIVE_ENABLED) {
|
||||
if (isStreaming) {
|
||||
// 流式响应
|
||||
let buffer = '';
|
||||
let contentReceived = '';
|
||||
@@ -136,17 +135,28 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths) {
|
||||
for (const line of lines) {
|
||||
if (!line.trim()) continue;
|
||||
|
||||
// 跳过心跳注释
|
||||
if (line.startsWith(':')) continue;
|
||||
// 心跳注释
|
||||
if (line.startsWith(':')) {
|
||||
process.stdout.write('💓'); // 显示心跳
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('data:')) {
|
||||
const data = line.slice(5).trim();
|
||||
if (data === '[DONE]') continue;
|
||||
if (data === '[DONE]') {
|
||||
console.log('\n📦 [DONE]');
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
const chunk = JSON.parse(data);
|
||||
if (chunk.choices && chunk.choices[0].delta && chunk.choices[0].delta.content) {
|
||||
contentReceived += chunk.choices[0].delta.content;
|
||||
const content = chunk.choices[0].delta.content;
|
||||
contentReceived += content;
|
||||
process.stdout.write(content); // 实时输出内容
|
||||
}
|
||||
if (chunk.error) {
|
||||
console.log(`\n❌ 错误: ${chunk.error}`);
|
||||
}
|
||||
} catch (e) {
|
||||
// 忽略解析错误
|
||||
@@ -156,6 +166,7 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths) {
|
||||
});
|
||||
|
||||
res.on('end', () => {
|
||||
console.log(''); // 换行
|
||||
if (res.statusCode === 200) {
|
||||
resolve({ choices: [{ message: { content: contentReceived } }] });
|
||||
} else {
|
||||
@@ -229,9 +240,18 @@ function saveImage(base64Data) {
|
||||
logger.info('CLI/Test', `参考图片: ${imagePaths.join(', ')}`);
|
||||
}
|
||||
|
||||
// 4. 执行测试
|
||||
// 4. 选择流式模式
|
||||
const isStreaming = await select({
|
||||
message: '选择请求模式',
|
||||
choices: [
|
||||
{ name: '流式 (stream: true) - 实时输出,支持心跳保活', value: true },
|
||||
{ name: '非流式 (stream: false) - 等待完整响应', value: false }
|
||||
]
|
||||
});
|
||||
|
||||
// 5. 执行测试
|
||||
logger.info('CLI/Test', '正在发送请求...');
|
||||
const result = await testViaHttpOpenAI(prompt, modelId, imagePaths);
|
||||
const result = await testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming);
|
||||
|
||||
// 5. 处理响应
|
||||
if (result.choices) {
|
||||
|
||||
@@ -229,7 +229,6 @@ const {
|
||||
|
||||
const PORT = config.server.port || 3000;
|
||||
const AUTH_TOKEN = config.server.auth;
|
||||
const KEEPALIVE_ENABLED = config.server.keepalive?.enable ?? true;
|
||||
const KEEPALIVE_MODE = config.server.keepalive?.mode || 'comment';
|
||||
|
||||
// --- 全局状态 ---
|
||||
@@ -454,18 +453,14 @@ async function startServer() {
|
||||
const messages = data.messages;
|
||||
const isStreaming = data.stream === true;
|
||||
|
||||
// Stream 参数验证
|
||||
if (KEEPALIVE_ENABLED && !isStreaming) {
|
||||
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Stream mode is required when keepalive is enabled. Please set "stream": true in your request.' }));
|
||||
return;
|
||||
}
|
||||
|
||||
// 限流检查(仅在未开启 keepalive 时限制)
|
||||
if (!KEEPALIVE_ENABLED && processingCount + queue.length >= MAX_CONCURRENT + MAX_QUEUE_SIZE) {
|
||||
logger.warn('服务器', '请求过多,已拒绝 (最大队列限制)', { id });
|
||||
// 限流检查:非流式请求在队列满时拒绝
|
||||
const totalPending = processingCount + queue.length;
|
||||
if (!isStreaming && totalPending >= MAX_QUEUE_SIZE) {
|
||||
logger.warn('服务器', '非流式请求被拒绝 (队列已满)', { id, queueSize: totalPending });
|
||||
res.writeHead(429, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ error: 'Too Many Requests. Server is busy.' }));
|
||||
res.end(JSON.stringify({
|
||||
error: `Server is busy (queue: ${totalPending}/${MAX_QUEUE_SIZE}). Please use streaming mode (stream: true) to wait in queue, or try again later.`
|
||||
}));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -646,7 +641,7 @@ async function startServer() {
|
||||
|
||||
server.listen(PORT, () => {
|
||||
logger.info('服务器', `HTTP 服务器启动成功,监听端口 ${PORT}`);
|
||||
logger.info('服务器', `流式保活: ${KEEPALIVE_ENABLED ? '已启用 (' + KEEPALIVE_MODE + ' 模式)' : '已禁用'}`);
|
||||
logger.info('服务器', `流式心跳模式: ${KEEPALIVE_MODE}`);
|
||||
logger.info('服务器', `最大队列: ${MAX_QUEUE_SIZE},最大图片数量: ${IMAGE_LIMIT}`);
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user