mirror of
https://github.com/foxhui/WebAI2API.git
synced 2026-06-16 21:03:59 +08:00
feat: 配置文件机制修改,优化WebUI日志排列
This commit is contained in:
+86
-66
@@ -1,8 +1,13 @@
|
||||
/**
|
||||
* @fileoverview 运行环境初始化脚本(CLI)
|
||||
* @description 用于下载/准备运行所需依赖(如 Camoufox、better-sqlite3 等),并按需更新 `config.yaml`。
|
||||
* @description 用于下载/准备运行所需依赖(如 Camoufox、better-sqlite3 等)。
|
||||
*
|
||||
* 用法:`npm run init`
|
||||
* 用法:
|
||||
* npm run init # 自动初始化(无代理)
|
||||
* npm run init -- -proxy # 自动初始化(交互式输入代理)
|
||||
* npm run init -- -proxy=http://127.0.0.1:7890
|
||||
* npm run init -- -proxy=socks5://user:pass@127.0.0.1:1080
|
||||
* npm run init -- -custom # 自定义模式
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
@@ -11,15 +16,80 @@ import os from 'os';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { gotScraping } from 'got-scraping';
|
||||
import compressing from 'compressing';
|
||||
import yaml from 'yaml';
|
||||
import { logger } from '../src/utils/logger.js';
|
||||
import { getHttpProxy, getProxyConfig } from '../src/utils/proxy.js';
|
||||
import { select } from '@inquirer/prompts';
|
||||
import { select, input } from '@inquirer/prompts';
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const PROJECT_ROOT = path.join(__dirname, '..');
|
||||
const TEMP_DIR = path.join(PROJECT_ROOT, 'data', 'temp');
|
||||
const CONFIG_PATH = path.join(PROJECT_ROOT, 'config.yaml');
|
||||
|
||||
/**
|
||||
* 解析命令行代理参数
|
||||
* @returns {Promise<string|null>} 代理 URL
|
||||
*/
|
||||
async function parseProxyArg() {
|
||||
// 查找 -proxy 或 -proxy=xxx 参数
|
||||
const proxyArg = process.argv.find(arg => arg.startsWith('-proxy'));
|
||||
|
||||
if (!proxyArg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// -proxy=http://... 格式
|
||||
if (proxyArg.includes('=')) {
|
||||
const proxyUrl = proxyArg.split('=')[1];
|
||||
if (proxyUrl) {
|
||||
logger.info('初始化', `使用代理: ${proxyUrl}`);
|
||||
return proxyUrl;
|
||||
}
|
||||
}
|
||||
|
||||
// -proxy 不带参数,交互式输入
|
||||
logger.info('初始化', '请输入代理配置...');
|
||||
|
||||
const proxyType = await select({
|
||||
message: '代理类型',
|
||||
choices: [
|
||||
{ name: 'HTTP', value: 'http' },
|
||||
{ name: 'SOCKS5', value: 'socks5' }
|
||||
]
|
||||
});
|
||||
|
||||
const host = await input({
|
||||
message: '代理服务器地址',
|
||||
default: '127.0.0.1',
|
||||
validate: (val) => val.trim().length > 0 || '地址不能为空'
|
||||
});
|
||||
|
||||
const port = await input({
|
||||
message: '代理端口',
|
||||
default: '7890',
|
||||
validate: (val) => {
|
||||
const num = parseInt(val, 10);
|
||||
return (num > 0 && num <= 65535) || '端口必须是 1-65535 的数字';
|
||||
}
|
||||
});
|
||||
|
||||
const username = await input({
|
||||
message: '用户名 (可选,回车跳过)',
|
||||
});
|
||||
|
||||
const password = await input({
|
||||
message: '密码 (可选,回车跳过)',
|
||||
});
|
||||
|
||||
// 构建代理 URL
|
||||
let proxyUrl = `${proxyType}://`;
|
||||
if (username && password) {
|
||||
proxyUrl += `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
|
||||
} else if (username) {
|
||||
proxyUrl += `${encodeURIComponent(username)}@`;
|
||||
}
|
||||
proxyUrl += `${host}:${port}`;
|
||||
|
||||
logger.info('初始化', `使用代理: ${proxyUrl}`);
|
||||
return proxyUrl;
|
||||
}
|
||||
|
||||
// 确保临时目录存在
|
||||
if (!fs.existsSync(TEMP_DIR)) {
|
||||
@@ -339,9 +409,6 @@ async function installCamoufox(platform, arch, proxyUrl) {
|
||||
|
||||
logger.info('初始化', `Camoufox 安装成功: ${camoufoxDir}`);
|
||||
|
||||
// 更新 config.yaml
|
||||
updateConfigPath(platform, camoufoxDir);
|
||||
|
||||
// 创建 version.json
|
||||
const versionJsonPath = path.join(camoufoxDir, 'version.json');
|
||||
const versionData = {
|
||||
@@ -355,51 +422,6 @@ async function installCamoufox(platform, arch, proxyUrl) {
|
||||
fs.unlinkSync(downloadPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新 config.yaml 中的 browser.path
|
||||
*/
|
||||
function updateConfigPath(platform, camoufoxDir) {
|
||||
try {
|
||||
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
|
||||
// 解析为文档对象 (CST)
|
||||
const doc = yaml.parseDocument(configContent);
|
||||
|
||||
// 构造绝对路径
|
||||
let browserPath;
|
||||
if (platform === 'win32') {
|
||||
browserPath = path.join(camoufoxDir, 'camoufox.exe');
|
||||
} else if (platform === 'darwin') {
|
||||
browserPath = path.join(camoufoxDir, 'Camoufox.app', 'Contents', 'MacOS', 'camoufox');
|
||||
} else {
|
||||
browserPath = path.join(camoufoxDir, 'camoufox');
|
||||
}
|
||||
|
||||
// 规范化路径分隔符
|
||||
browserPath = browserPath.replace(/\\/g, '/');
|
||||
|
||||
// 安全地更新路径,如果节点不存在则创建
|
||||
if (!doc.has('browser')) {
|
||||
doc.set('browser', { path: browserPath });
|
||||
} else {
|
||||
const browserNode = doc.get('browser');
|
||||
if (browserNode && typeof browserNode.set === 'function') {
|
||||
browserNode.set('path', browserPath);
|
||||
} else {
|
||||
// 如果 browser 不是对象(理论上不应该发生),强制覆盖
|
||||
doc.set('browser', { path: browserPath });
|
||||
}
|
||||
}
|
||||
|
||||
// 转回字符串,保留注释
|
||||
const updatedYaml = doc.toString();
|
||||
fs.writeFileSync(CONFIG_PATH, updatedYaml, 'utf8');
|
||||
|
||||
logger.info('初始化', `已更新配置文件 browser.path: ${browserPath}`);
|
||||
} catch (e) {
|
||||
logger.error('初始化', '更新配置文件失败', { error: e.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 主流程
|
||||
@@ -410,6 +432,14 @@ function updateConfigPath(platform, camoufoxDir) {
|
||||
logger.info('初始化', '依赖初始化脚本启动');
|
||||
logger.info('初始化', '========================================');
|
||||
|
||||
// 代理使用提示
|
||||
if (!process.argv.some(arg => arg.startsWith('-proxy'))) {
|
||||
logger.warn('初始化', '该脚本需连接 GitHub 下载资源。若网络受限,请使用代理:');
|
||||
logger.warn('初始化', ' - 用法: npm run init -- -proxy 可交互式填写代理信息');
|
||||
logger.warn('初始化', ' - 同时支持直接传入参数或者使用带鉴权的代理 (支持HTTP和SOCKS5)');
|
||||
logger.warn('初始化', ' - 示例: npm run init -- -proxy=http://username:passwd@127.0.0.1:7890');
|
||||
}
|
||||
|
||||
// 显示系统信息
|
||||
const { platform, arch, nodeVersion, abi } = getPlatformInfo();
|
||||
logger.info('初始化', `操作系统: ${platform}`);
|
||||
@@ -438,18 +468,8 @@ function updateConfigPath(platform, camoufoxDir) {
|
||||
|
||||
logger.info('初始化', 'ABI 版本检查通过');
|
||||
|
||||
// 读取并转换代理配置
|
||||
let proxyUrl = null;
|
||||
try {
|
||||
const configContent = fs.readFileSync(CONFIG_PATH, 'utf8');
|
||||
const config = yaml.parse(configContent);
|
||||
const proxyConfig = getProxyConfig(config);
|
||||
if (proxyConfig) {
|
||||
proxyUrl = await getHttpProxy(proxyConfig);
|
||||
}
|
||||
} catch (e) {
|
||||
logger.warn('初始化', '无法读取配置文件或转换代理,不使用代理');
|
||||
}
|
||||
// 解析代理参数
|
||||
const proxyUrl = await parseProxyArg();
|
||||
|
||||
// 检查是否为自定义模式
|
||||
const isCustomMode = process.argv.includes('-custom');
|
||||
|
||||
-234
@@ -1,234 +0,0 @@
|
||||
/**
|
||||
* @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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user