Files
WebAI2API/scripts/test.js
T

235 lines
7.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* @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);
}
})();