mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
235 lines
7.7 KiB
JavaScript
235 lines
7.7 KiB
JavaScript
/**
|
||
* @fileoverview 本地 HTTP 调用测试(CLI)
|
||
* @description 用交互式方式构造请求并调用本地服务的 `/v1/chat/completions`,用于快速验证服务可用性与流式输出。
|
||
*
|
||
* 用法:`npm run test`
|
||
*/
|
||
|
||
import { select, input } from '@inquirer/prompts';
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import http from 'http';
|
||
import yaml from 'yaml';
|
||
|
||
// 简易日志:脚本内部使用,避免引入服务端 logger 造成格式混淆
|
||
const logger = {
|
||
info: (tag, msg) => console.log(`[${new Date().toLocaleTimeString()}] [INFO] [${tag}] ${msg}`),
|
||
warn: (tag, msg) => console.log(`[${new Date().toLocaleTimeString()}] [WARN] [${tag}] ${msg}`),
|
||
error: (tag, msg, meta) => console.error(`[${new Date().toLocaleTimeString()}] [ERROR] [${tag}] ${msg}`, meta || '')
|
||
};
|
||
|
||
// 读取本地配置:用于获取端口与鉴权 Token(读取失败时使用默认值)
|
||
let config = { server: { port: 3000, auth: '' } };
|
||
try {
|
||
if (fs.existsSync('config.yaml')) {
|
||
const file = fs.readFileSync('config.yaml', 'utf8');
|
||
const parsed = yaml.parse(file);
|
||
if (parsed && parsed.server) {
|
||
config.server.port = parsed.server.port || 3000;
|
||
config.server.auth = parsed.server.auth || '';
|
||
}
|
||
}
|
||
} catch (e) {
|
||
logger.warn('Test', '无法读取 config.yaml,将使用默认设置');
|
||
}
|
||
|
||
/**
|
||
* 输入提示词
|
||
*/
|
||
async function promptForInput() {
|
||
const prompt = await input({
|
||
message: '输入提示词 (必填)',
|
||
validate: (val) => val.trim().length > 0 || '提示词不能为空'
|
||
});
|
||
return prompt.trim();
|
||
}
|
||
|
||
/**
|
||
* 输入图片路径
|
||
*/
|
||
async function promptForImages() {
|
||
const imagePaths = [];
|
||
while (true) {
|
||
const imgPath = await input({
|
||
message: `输入参考图片路径 (留空跳过,已添加 ${imagePaths.length} 张)`,
|
||
});
|
||
|
||
if (!imgPath.trim()) break;
|
||
|
||
const cleanPath = imgPath.trim().replace(/^["']|["']$/g, '');
|
||
if (fs.existsSync(cleanPath)) {
|
||
imagePaths.push(cleanPath);
|
||
} else {
|
||
logger.warn('Test', `图片不存在: ${cleanPath}`);
|
||
}
|
||
}
|
||
return imagePaths;
|
||
}
|
||
|
||
/**
|
||
* HTTP 测试模式 - OpenAI 格式
|
||
*/
|
||
async function testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming) {
|
||
const PORT = config.server.port;
|
||
const AUTH_TOKEN = config.server.auth;
|
||
|
||
if (!AUTH_TOKEN) {
|
||
logger.warn('Test', '警告: 未配置 API Key (server.auth)');
|
||
}
|
||
|
||
logger.info('Test', `HTTP 测试 - ${isStreaming ? '流式' : '非流式'} - 端口: ${PORT}`);
|
||
|
||
return new Promise((resolve, reject) => {
|
||
// 构造请求体
|
||
const messages = [];
|
||
const lastMessage = { role: 'user', content: [] };
|
||
|
||
if (prompt) {
|
||
lastMessage.content.push({ type: 'text', text: prompt });
|
||
}
|
||
|
||
for (const imgPath of imagePaths) {
|
||
if (fs.existsSync(imgPath)) {
|
||
const buffer = fs.readFileSync(imgPath);
|
||
const base64 = buffer.toString('base64');
|
||
const ext = path.extname(imgPath).slice(1).toLowerCase();
|
||
const mimeType = ext === 'jpg' ? 'jpeg' : ext;
|
||
lastMessage.content.push({
|
||
type: 'image_url',
|
||
image_url: { url: `data:image/${mimeType};base64,${base64}` }
|
||
});
|
||
} else {
|
||
logger.warn('Test', `图片不存在,已跳过: ${imgPath}`);
|
||
}
|
||
}
|
||
|
||
messages.push(lastMessage);
|
||
|
||
const body = {
|
||
messages,
|
||
stream: isStreaming,
|
||
model: modelId || 'default'
|
||
};
|
||
|
||
const bodyStr = JSON.stringify(body);
|
||
|
||
const options = {
|
||
hostname: '127.0.0.1',
|
||
port: PORT,
|
||
path: '/v1/chat/completions',
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'Content-Length': Buffer.byteLength(bodyStr),
|
||
'Authorization': `Bearer ${AUTH_TOKEN}`
|
||
}
|
||
};
|
||
|
||
const req = http.request(options, (res) => {
|
||
if (isStreaming) {
|
||
// 流式响应
|
||
let buffer = '';
|
||
let contentReceived = '';
|
||
|
||
res.on('data', chunk => {
|
||
buffer += chunk.toString();
|
||
const lines = buffer.split('\n');
|
||
buffer = lines.pop(); // 保留未完成的行
|
||
|
||
for (const line of lines) {
|
||
if (!line.trim()) continue;
|
||
|
||
// 心跳注释
|
||
if (line.startsWith(':')) {
|
||
process.stdout.write('💓'); // 显示心跳
|
||
continue;
|
||
}
|
||
|
||
if (line.startsWith('data:')) {
|
||
const data = line.slice(5).trim();
|
||
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) {
|
||
const content = chunk.choices[0].delta.content;
|
||
contentReceived += content;
|
||
process.stdout.write(content); // 实时输出内容
|
||
}
|
||
if (chunk.error) {
|
||
console.log(`\n❌ 错误: ${chunk.error}`);
|
||
}
|
||
} catch (e) {
|
||
// 忽略解析错误
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
res.on('end', () => {
|
||
console.log(''); // 换行
|
||
if (res.statusCode === 200) {
|
||
resolve({ choices: [{ message: { content: contentReceived } }] });
|
||
} else {
|
||
reject(new Error(`HTTP ${res.statusCode}`));
|
||
}
|
||
});
|
||
} else {
|
||
// 非流式响应
|
||
let data = '';
|
||
res.on('data', chunk => data += chunk);
|
||
res.on('end', () => {
|
||
if (res.statusCode === 200) {
|
||
const response = JSON.parse(data);
|
||
resolve(response);
|
||
} else {
|
||
reject(new Error(`HTTP ${res.statusCode}: ${data}`));
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
req.on('error', reject);
|
||
req.write(bodyStr);
|
||
req.end();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 主流程
|
||
*/
|
||
(async () => {
|
||
try {
|
||
logger.info('Test', '=== API 独立测试脚本 ===');
|
||
|
||
// 1. 输入提示词
|
||
const prompt = await promptForInput();
|
||
|
||
// 2. 输入图片路径
|
||
const imagePaths = await promptForImages();
|
||
|
||
// 3. 选择流式模式
|
||
const isStreaming = await select({
|
||
message: '选择请求模式',
|
||
choices: [
|
||
{ name: '流式 (stream: true)', value: true },
|
||
{ name: '非流式 (stream: false)', value: false }
|
||
]
|
||
});
|
||
|
||
// 4. 执行测试
|
||
logger.info('Test', '正在发送请求...');
|
||
await testViaHttpOpenAI(prompt, null, imagePaths, isStreaming);
|
||
|
||
logger.info('Test', '测试完成');
|
||
process.exit(0);
|
||
|
||
} catch (err) {
|
||
logger.error('Test', '测试失败', { error: err.message });
|
||
process.exit(1);
|
||
}
|
||
})();
|