feat: 流式和非流式客户端可共存

This commit is contained in:
foxhui
2025-12-13 01:40:53 +08:00
Unverified
parent 002c607da0
commit 369757301d
5 changed files with 59 additions and 49 deletions
+37 -17
View File
@@ -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) {