diff --git a/CHANGELOG.md b/CHANGELOG.md index 05de3d4..829615e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/config.example.yaml b/config.example.yaml index de76b38..399d668 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -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: diff --git a/lib/utils/config.js b/lib/utils/config.js index 3511a46..83dadd3 100644 --- a/lib/utils/config.js +++ b/lib/utils/config.js @@ -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}`); } diff --git a/lib/utils/test.js b/lib/utils/test.js index d42587c..a34a71f 100644 --- a/lib/utils/test.js +++ b/lib/utils/test.js @@ -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) { diff --git a/server.js b/server.js index 3f5e90c..90f3f28 100644 --- a/server.js +++ b/server.js @@ -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}`); }); }