refactor: 重构服务器部分并整理目录和注释

This commit is contained in:
foxhui
2025-12-13 03:37:51 +08:00
Unverified
parent 063a05a499
commit 5d8dc7c0a5
27 changed files with 367 additions and 378 deletions
+7
View File
@@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [2.4.0] - 2025-12-13
### Changed
- **重构部分**
- 重构服务器部分,并整理项目目录
- 加强注释,便于后期维护
## [2.3.0] - 2025-12-12
### Added
-218
View File
@@ -1,218 +0,0 @@
import fs from 'fs';
import path from 'path';
import yaml from 'yaml';
import crypto from 'crypto';
import { logger } from './logger.js';
const CONFIG_PATH = path.join(process.cwd(), 'config.yaml');
// 模块级缓存,确保配置只从磁盘读取一次
let cachedConfig = null;
/**
* 生成随机 API Key
* 格式: sk-{48位十六进制字符}
* @returns {string} API Key
*/
function generateApiKey() {
return 'sk-' + crypto.randomBytes(24).toString('hex');
}
/**
* 默认配置模板
*/
function getDefaultConfig() {
return `# 自动生成于 ${new Date().toLocaleString()}
# 日志等级: debug | info | warn | error
logLevel: info
server:
# 监听端口
port: 3000
# 鉴权 Token (Bearer Token) (可使用 npm run genkey 生成)
auth: ${generateApiKey()}
# 流式请求心跳设置 (自动对 stream: true 的请求发送心跳防止超时)
keepalive:
# 心跳模式
# "comment": (推荐) 发送 :keepalive 注释,不污染数据
# "content": (备用) 发送空 delta,仅当客户端必须收到 JSON 包才重置超时时使用
mode: "comment"
backend:
# 适配器设置
# - lmarena (LMArena)
# - gemini (Gemini 网页版)
# - gemini_biz (Gemini Enterprise Business)
# - nanobananafree_ai (Nano Banana Free)
# - zai_is (zAI)
type: lmarena
# 聚合模式设置 (优先级高于 type)
# 启用后,系统将复用单个浏览器标签页处理所有后端任务
merge:
enable: false
# 聚合列表 (按优先级排序,模型匹配时优先使用靠前的适配器)
type:
- zai_is
- lmarena
# Gemini Business 设置
geminiBiz:
# 入口链接
# 示例: "https://business.gemini.google/home/cid/8888a888-b6e0-88be-86e1-888cf3ee8cf4"
entryUrl: ""
queue:
# 最大排队数
# 仅对未开启流式保活模式时做出限制,非必要不建议更改
# 因客户端可能有超时保护,队列大于2是一定会触发超时保护的
maxQueueSize: 2
# 图片数量上限
# 网页最多支持10个附件,如果设置大于10则直接丢弃超出10的图片
imageLimit: 5
browser:
# 浏览器可执行文件路径 (留空则使用 Camoufox 默认下载路径)
# Windows系统示例 "C:\\camoufox\\camoufox.exe"
# Linux系统示例 "/opt/camoufox/camoufox"
path: ""
# 是否启用无头模式
headless: false
# 代理设置
proxy:
# 是否启用代理
enable: false
# 代理类型: http | socks5
type: http
# 代理主机
host: 127.0.0.1
# 代理端口
port: 7890
# 代理认证 (可选)
# user: username
# passwd: password
`;
}
/**
* 加载配置,如果不存在则自动创建
* @returns {object} 配置对象
*/
export function loadConfig() {
// 如果已有缓存,直接返回
if (cachedConfig) return cachedConfig;
try {
if (!fs.existsSync(CONFIG_PATH)) {
logger.warn('配置器', '配置文件不存在,正在生成默认配置...');
const defaultConfig = getDefaultConfig();
fs.writeFileSync(CONFIG_PATH, defaultConfig, 'utf8');
logger.info('配置器', `已生成默认配置文件: ${CONFIG_PATH}`);
logger.warn('配置器', '请注意查看生成的随机 API Key');
}
const configFile = fs.readFileSync(CONFIG_PATH, 'utf8');
const config = yaml.parse(configFile);
// Docker 路径兼容处理
if ((!config.browser?.path || !fs.existsSync(config.browser.path)) &&
fs.existsSync('/app/camoufox/camoufox')) {
logger.info('配置器', '检测到容器环境,自动修正浏览器路径为 /app/camoufox/camoufox');
if (!config.browser) config.browser = {};
config.browser.path = '/app/camoufox/camoufox';
}
// 基础配置校验
if (!config.server || !config.server.port) {
throw new Error('配置文件缺少必需字段: server.port');
}
if (!config.server.auth) {
throw new Error('配置文件缺少必需字段: server.auth');
}
// 设置队列配置默认值
if (!config.queue) {
config.queue = {
maxConcurrent: 1,
maxQueueSize: 2,
imageLimit: 5
};
} else {
// 强制 maxConcurrent 为 1
config.queue.maxConcurrent = 1;
if (config.queue.maxQueueSize === undefined) config.queue.maxQueueSize = 2;
if (config.queue.imageLimit === undefined) config.queue.imageLimit = 5;
}
// 设置 keepalive 配置默认值
if (!config.server.keepalive) {
config.server.keepalive = {
mode: 'comment'
};
} else {
if (config.server.keepalive.mode === undefined) config.server.keepalive.mode = 'comment';
// 验证 mode 值
if (!['comment', 'content'].includes(config.server.keepalive.mode)) {
logger.warn('配置器', `无效的 keepalive.mode: ${config.server.keepalive.mode},使用默认值 comment`);
config.server.keepalive.mode = 'comment';
}
}
// 设置 backend 配置默认值
if (!config.backend) {
config.backend = {
type: 'lmarena',
geminiBiz: { entryUrl: '' }
};
}
// 新增:Merge 配置初始化
if (!config.backend.merge) {
config.backend.merge = {
enable: false,
type: ['zai_is', 'lmarena']
};
}
// 校验 GeminiBiz 配置
if (config.backend.type === 'gemini_biz') {
if (!config.backend.geminiBiz || !config.backend.geminiBiz.entryUrl) {
throw new Error('backend.type = gemini_biz requires backend.geminiBiz.entryUrl');
}
}
logger.debug('配置器', '已加载 config.yaml');
logger.debug('配置器', '后端类型:', config.backend.type);
logger.debug('配置器', '流式心跳模式:', config.server.keepalive.mode);
if (config.backend.type === 'gemini_biz') {
logger.debug('配置器', `GeminiBiz 入口: ${config.backend.geminiBiz.entryUrl}`);
}
// 设置日志级别
if (config.logLevel) {
logger.setLevel(config.logLevel);
}
// 缓存配置
cachedConfig = config;
return config;
} catch (e) {
logger.error('配置器', '无法加载或生成配置文件', { error: e.message });
process.exit(1);
}
}
// 默认导出为函数
export default loadConfig;
// 生成 API Key
if (process.argv.includes('-genkey')) {
console.log('>>> [GenAPIKey] 生成新的 API Key:');
console.log(generateApiKey());
console.log('\n>>> 请将此 Key 复制到 config.yaml 文件的 server.auth 字段中。');
process.exit(0);
}
+3 -3
View File
@@ -7,9 +7,9 @@
"type": "module",
"scripts": {
"start": "node server.js",
"test": "node lib/utils/test.js",
"genkey": "node lib/utils/config.js -genkey",
"init": "node lib/utils/init.js"
"test": "node scripts/test.js",
"genkey": "node scripts/genkey.js",
"init": "node scripts/init.js"
},
"dependencies": {
"@inquirer/prompts": "^8.0.1",
+21
View File
@@ -0,0 +1,21 @@
/**
* @fileoverview 生成 API KeyCLI
* @description 输出一个新的 `server.auth` Key,供写入 `config.yaml` 使用。
*
* 用法:`npm run genkey`
*/
import crypto from 'crypto';
/**
* 生成随机 API Key(用于 `config.yaml` 的 `server.auth`
* 格式:sk-{48位十六进制字符}
* @returns {string} API Key
*/
function generateApiKey() {
return 'sk-' + crypto.randomBytes(24).toString('hex');
}
console.log('>>> [GenAPIKey] 生成新的 API Key:');
console.log(generateApiKey());
console.log('\n>>> 请将此 Key 复制到 config.yaml 文件的 server.auth 字段中。');
+10 -3
View File
@@ -1,3 +1,10 @@
/**
* @fileoverview 运行环境初始化脚本CLI
* @description 用于下载/准备运行所需依赖 Camoufoxbetter-sqlite3 并按需更新 `config.yaml`
*
* 用法`npm run init`
*/
import fs from 'fs';
import { execSync } from 'child_process';
import path from 'path';
@@ -6,11 +13,11 @@ import { fileURLToPath } from 'url';
import { gotScraping } from 'got-scraping';
import compressing from 'compressing';
import yaml from 'yaml';
import { logger } from './logger.js';
import { getHttpProxy, getProxyConfig } from './proxy.js';
import { logger } from '../src/utils/logger.js';
import { getHttpProxy, getProxyConfig } from '../src/utils/proxy.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const PROJECT_ROOT = path.join(__dirname, '..', '..');
const PROJECT_ROOT = path.join(__dirname, '..');
const TEMP_DIR = path.join(PROJECT_ROOT, 'data', 'temp');
const CONFIG_PATH = path.join(PROJECT_ROOT, 'config.yaml');
+48 -91
View File
@@ -1,32 +1,36 @@
import { getBackend } from '../backend/index.js';
/**
* @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 { logger } from './logger.js';
import yaml from 'yaml';
// 使用统一后端获取配置和函数
const { config, name, TEMP_DIR, getModels } = getBackend();
// 简易日志:脚本内部使用,避免引入服务端 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 || '')
};
logger.info('CLI/Test', `测试工具启动 (后端适配器: ${name})`);
/**
* 选择模型
*/
async function selectModel() {
const models = getModels(); // 使用后端统一接口,支持聚合模式
const choices = [
{ name: 'Skip(使用默认模型)', value: null },
...models.data.map(m => ({ name: m.id, value: m.id }))
];
const modelId = await select({
message: '选择模型',
choices,
pageSize: 15
});
return modelId;
// 读取本地配置:用于获取端口与鉴权 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,将使用默认设置');
}
/**
@@ -56,7 +60,7 @@ async function promptForImages() {
if (fs.existsSync(cleanPath)) {
imagePaths.push(cleanPath);
} else {
logger.warn('CLI/Test', `图片不存在: ${cleanPath}`);
logger.warn('Test', `图片不存在: ${cleanPath}`);
}
}
return imagePaths;
@@ -64,16 +68,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, isStreaming) {
const PORT = config.server.port || 3000;
const PORT = config.server.port;
const AUTH_TOKEN = config.server.auth;
logger.info('CLI/Test', `HTTP 测试 - ${isStreaming ? '流式模式' : '非流式模式'}`);
if (!AUTH_TOKEN) {
logger.warn('Test', '警告: 未配置 API Key (server.auth)');
}
logger.info('Test', `HTTP 测试 - ${isStreaming ? '流式' : '非流式'} - 端口: ${PORT}`);
return new Promise((resolve, reject) => {
// 构造请求体
@@ -95,7 +99,7 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming) {
image_url: { url: `data:image/${mimeType};base64,${base64}` }
});
} else {
logger.warn('CLI/Test', `图片不存在,已跳过: ${imgPath}`);
logger.warn('Test', `图片不存在,已跳过: ${imgPath}`);
}
}
@@ -104,7 +108,7 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming) {
const body = {
messages,
stream: isStreaming,
...(modelId && { model: modelId })
model: modelId || 'default'
};
const bodyStr = JSON.stringify(body);
@@ -194,84 +198,37 @@ async function testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming) {
});
}
/**
* 保存图片
*/
function saveImage(base64Data) {
const testSaveDir = path.join(TEMP_DIR, 'testSave');
if (!fs.existsSync(testSaveDir)) {
fs.mkdirSync(testSaveDir, { recursive: true });
}
const timestamp = Date.now();
const savePath = path.join(testSaveDir, `test_${timestamp}.png`);
// 移除 Data URI 前缀(如果有)
const cleanBase64 = base64Data.replace(/^data:image\/\w+;base64,/, '');
fs.writeFileSync(savePath, Buffer.from(cleanBase64, 'base64'));
logger.info('CLI/Test', `图片已保存: ${savePath}`);
return savePath;
}
/**
* 主流程
*/
(async () => {
try {
logger.info('CLI/Test', '=== HTTP 服务器测试 ===');
logger.info('CLI/Test', '请确保服务器已启动 (npm start)');
logger.info('Test', '=== API 独立测试脚本 ===');
// 1. 选择模型
const modelId = await selectModel();
if (modelId) {
logger.info('CLI/Test', `选择模型: ${modelId}`);
} else {
logger.info('CLI/Test', '跳过模型选择,使用默认');
}
// 2. 输入提示词
// 1. 输入提示词
const prompt = await promptForInput();
logger.info('CLI/Test', `提示词: ${prompt}`);
// 3. 输入图片路径
// 2. 输入图片路径
const imagePaths = await promptForImages();
if (imagePaths.length > 0) {
logger.info('CLI/Test', `参考图片: ${imagePaths.join(', ')}`);
}
// 4. 选择流式模式
// 3. 选择流式模式
const isStreaming = await select({
message: '选择请求模式',
choices: [
{ name: '流式 (stream: true) - 实时输出,支持心跳保活', value: true },
{ name: '非流式 (stream: false) - 等待完整响应', value: false }
{ name: '流式 (stream: true)', value: true },
{ name: '非流式 (stream: false)', value: false }
]
});
// 5. 执行测试
logger.info('CLI/Test', '正在发送请求...');
const result = await testViaHttpOpenAI(prompt, modelId, imagePaths, isStreaming);
// 4. 执行测试
logger.info('Test', '正在发送请求...');
await testViaHttpOpenAI(prompt, null, imagePaths, isStreaming);
// 5. 处理响应
if (result.choices) {
const content = result.choices[0].message.content;
logger.info('CLI/Test', `响应内容: ${content.slice(0, 100)}...`);
// 提取图片(如果有)
const match = content.match(/!\[.*?\]\((data:image\/[^)]+)\)/);
if (match) {
saveImage(match[1]);
} else {
logger.info('CLI/Test', `文本回复: ${content}`);
}
}
logger.info('CLI/Test', '测试完成');
logger.info('Test', '测试完成');
process.exit(0);
} catch (err) {
logger.error('CLI/Test', '测试失败', { error: err.message });
logger.error('Test', '测试失败', { error: err.message });
process.exit(1);
}
})();
})();
+13 -4
View File
@@ -14,9 +14,9 @@
*/
import http from 'http';
import { getBackend } from './lib/backend/index.js';
import { logger } from './lib/utils/logger.js';
import { handleDisplayParams, createQueueManager, createRouter } from './lib/server/index.js';
import { getBackend } from './src/backend/index.js';
import { logger } from './src/utils/logger.js';
import { handleDisplayParams, createQueueManager, createRouter } from './src/server/index.js';
// ==================== 命令行参数处理 ====================
@@ -33,6 +33,15 @@ if (displayResult === 'XVFB_REDIRECT') {
/**
* 从统一后端获取配置和函数
*/
let backend;
try {
backend = getBackend();
} catch (err) {
logger.error('服务器', '配置加载失败', { error: err.message });
logger.error('服务器', '请先初始化配置:复制 config.example.yaml 为 config.yaml');
process.exit(1);
}
const {
config,
name: backendName,
@@ -42,7 +51,7 @@ const {
resolveModelId,
getModels,
getImagePolicy
} = getBackend();
} = backend;
/** @type {number} 服务器端口 */
const PORT = config.server?.port || 3000;
@@ -1,3 +1,8 @@
/**
* @fileoverview Gemini消费者版适配器
* @description 通过自动化方式驱动 Gemini 网页端生成图片并将结果转换为统一的后端返回结构
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
@@ -1,3 +1,8 @@
/**
* @fileoverview Gemini Business 适配器
* @description 通过自动化方式驱动 Gemini Business 网页端生成图片并将结果转换为统一的后端返回结构
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
@@ -19,7 +24,7 @@ const INPUT_SELECTOR = 'ucs-prosemirror-editor .ProseMirror';
/**
* 处理账户选择页面跳转
* @param {import('puppeteer').Page} page
* @param {import('playwright-core').Page} page - Playwright 页面对象
* @param {string} targetUrl - 目标 URL用于判断跳转完成
* @returns {Promise<boolean>} 是否处理了跳转
*/
@@ -349,4 +354,3 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
export { initBrowser, generateImage, handleAccountChooser };
@@ -1,3 +1,8 @@
/**
* @fileoverview LMArena 适配器
* @description 通过自动化方式驱动 LMArena 网页端生成图片或解析文本并将结果转换为统一的后端返回结构
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
@@ -1,3 +1,8 @@
/**
* @fileoverview NanoBananaFree AI 适配器
* @description 通过自动化方式驱动 nanobananafree.ai 网页端生成图片并将结果转换为统一的后端返回结构
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
@@ -148,4 +153,3 @@ async function generateImage(context, prompt, imgPaths, modelId, meta = {}) {
}
export { initBrowser, generateImage };
@@ -1,3 +1,8 @@
/**
* @fileoverview zAIzai.is适配器
* @description 通过自动化方式驱动 zai.is 网页端生成图片并将结果转换为统一的后端返回结构
*/
import { initBrowserBase } from '../../browser/launcher.js';
import {
sleep,
+40 -31
View File
@@ -1,3 +1,13 @@
/**
* @fileoverview 后端适配器入口
* @description 负责加载配置准备运行目录用户数据/临时目录并根据配置返回单后端聚合后端的统一接口
*
* 对外统一能力
* - `initBrowser(cfg)`
* - `generateImage(ctx, prompt, imagePaths, modelId, meta)`
* - `resolveModelId(modelKey)` / `getModels()` / `getImagePolicy(modelKey)`
*/
import fs from 'fs';
import path from 'path';
import { loadConfig } from '../utils/config.js';
@@ -24,14 +34,6 @@ if (!fs.existsSync(TEMP_DIR)) {
fs.mkdirSync(TEMP_DIR, { recursive: true });
}
const config = loadConfig();
// 将路径常量注入 config 对象
config.paths = {
userDataDir: USER_DATA_DIR,
tempDir: TEMP_DIR
};
// 适配器映射表
const ADAPTER_MAP = {
'gemini_biz': geminiBackend,
@@ -41,26 +43,7 @@ const ADAPTER_MAP = {
'lmarena': lmarenaBackend
};
// 1. 单一后端模式实现
const SingleBackend = {
name: config.backend.type,
initBrowser: async (cfg) => {
const adapter = ADAPTER_MAP[cfg.backend.type] || lmarenaBackend;
return adapter.initBrowser(cfg);
},
generateImage: async (ctx, prompt, paths, modelId, meta) => {
const adapter = ADAPTER_MAP[config.backend.type] || lmarenaBackend;
return adapter.generateImage(ctx, prompt, paths, modelId, meta);
},
resolveModelId: (modelKey) => modelsModule.resolveModelId(config.backend.type, modelKey),
getModels: () => modelsModule.getModelsForBackend(config.backend.type),
getImagePolicy: (modelKey) => modelsModule.getImagePolicy(config.backend.type, modelKey)
};
// 2. 聚合后端模式实现
// 2. 聚合后端模式实现(跨调用复用全局 Page
const MergedBackend = {
name: 'merge',
_globalBrowser: null,
@@ -113,12 +96,11 @@ const MergedBackend = {
logger.info('适配器', `[后端聚合] 路由至: ${adapterType}, Model: ${realId}`, meta);
// 构造子上下文:复用全局 Page,但传入全局 Config
// 适配器会从 config.backend.geminiBiz 等字段读取所需配置
// 构造子上下文:复用全局 Page,但传入当前 config(适配器会读取 config.backend.geminiBiz 等字段)
const subContext = {
...ctx,
page: MergedBackend._globalPage,
config: config
config: cfg
};
return adapter.generateImage(subContext, prompt, paths, realId, meta);
@@ -206,6 +188,33 @@ const MergedBackend = {
};
export function getBackend() {
const config = loadConfig();
// 将路径常量注入 config 对象
config.paths = {
userDataDir: USER_DATA_DIR,
tempDir: TEMP_DIR
};
// 单一后端模式实现(基于当前配置构建)
const SingleBackend = {
name: config.backend.type,
initBrowser: async (cfg) => {
const adapter = ADAPTER_MAP[cfg.backend.type] || lmarenaBackend;
return adapter.initBrowser(cfg);
},
generateImage: async (ctx, prompt, paths, modelId, meta) => {
const adapter = ADAPTER_MAP[config.backend.type] || lmarenaBackend;
return adapter.generateImage(ctx, prompt, paths, modelId, meta);
},
resolveModelId: (modelKey) => modelsModule.resolveModelId(config.backend.type, modelKey),
getModels: () => modelsModule.getModelsForBackend(config.backend.type),
getImagePolicy: (modelKey) => modelsModule.getImagePolicy(config.backend.type, modelKey)
};
const isMerge = config.backend.merge && config.backend.merge.enable;
const activeBackend = isMerge ? MergedBackend : SingleBackend;
@@ -1,11 +1,21 @@
// 图片策略枚举
/**
* @fileoverview 模型与图片策略映射
* @description 维护各后端的模型列表/别名与图片输入策略并提供统一的解析与查询方法供服务器层使用
*/
/**
* 图片输入策略枚举
* - optional可带可不带默认
* - required必须有参考图
* - forbidden禁止带图
*/
export const IMAGE_POLICY = {
OPTIONAL: 'optional', // 可带可不带(默认)
REQUIRED: 'required', // 必须有参考图
FORBIDDEN: 'forbidden' // 禁止带图
};
// LMArena 后端模型配置
/** LMArena 后端模型配置modelKey -> 上游 codeName + 图片策略) */
export const LMARENA_MODELS = {
"gemini-3-pro-image-preview-2k": {
codeName: "019abc10-e78d-7932-b725-7f1563ed8a12",
@@ -141,21 +151,21 @@ export const LMARENA_MODELS = {
}
};
// Gemini Biz 后端模型配置
/** Gemini Business 后端模型配置 */
export const GEMINI_BIZ_MODELS = {
"gemini-3-pro-image-preview": {
imagePolicy: IMAGE_POLICY.OPTIONAL
}
};
// NanoBananaFree AI 后端模型配置
/** NanoBananaFree AI 后端模型配置 */
export const NANOBANANAFREE_AI_MODELS = {
"gemini-2.5-flash-image": {
imagePolicy: IMAGE_POLICY.OPTIONAL
}
};
// zai.is 后端模型配置
/** zai.is 后端模型配置 */
export const ZAI_IS_MODELS = {
"gemini-3-pro-image-preview": {
imagePolicy: IMAGE_POLICY.OPTIONAL
@@ -249,4 +259,3 @@ export function getImagePolicy(backendName, modelKey) {
return model.imagePolicy;
}
@@ -1,11 +1,13 @@
/**
* 适配器公共流程函数
*
* - fillPrompt: 填写提示词
* - submit: 提交表单带回退逻辑
* - waitApiResponse: 等待 API 响应含页面监听
* - normalizePageError: 页面错误处理
* - normalizeHttpError: HTTP 错误处理
* @fileoverview 后端适配器公共流程
* @description 提供各适配器复用的通用页面操作与错误归一化能力
*
* 主要函数
* - `fillPrompt`拟人化输入提示词
* - `submit`提交表单点击按钮失败则回退为回车
* - `waitApiResponse`等待匹配的 API 响应包含页面关闭/崩溃监听
* - `normalizePageError`将页面级异常归一化为可返回给服务器层的错误
* - `normalizeHttpError` HTTP 响应错误含限流/人机验证归一化
*/
import { sleep, humanType, safeClick, isPageValid, createPageCloseWatcher, getRealViewport, clamp, random } from '../browser/utils.js';
@@ -267,4 +269,3 @@ export async function downloadImage(url, config) {
return { error: `已获取结果,但图片下载时遇到错误: ${e.message}` };
}
}
@@ -1,3 +1,12 @@
/**
* @fileoverview 浏览器启动与生命周期管理
* @description 负责启动 CamoufoxPlaywright 内核注入指纹与代理创建/复用页面并在进程退出时做资源清理
*
* 约定
* - 登录模式会尽量保留 Profile用户数据目录
* - 清理采用三级退出Playwright close -> SIGTERM -> SIGKILL
*/
import { Camoufox } from 'camoufox-js';
import { FingerprintGenerator } from 'fingerprint-generator';
import fs from 'fs';
@@ -8,7 +17,7 @@ import { getRealViewport, clamp, random, sleep } from './utils.js';
import { logger } from '../utils/logger.js';
import { getBrowserProxy, cleanupProxy } from '../utils/proxy.js';
// 全局状态跟踪
// 全局状态:用于在登录模式下管理残留进程与复用上下文
let globalBrowserProcess = null;
let globalContext = null; // 替代 globalBrowser
+15 -1
View File
@@ -1,3 +1,18 @@
/**
* @fileoverview 浏览器自动化工具函数
* @description 封装 Playwright 页面常用操作等待查找点击上传拟人化行为等供后端适配器复用
*
* 主要函数
* - `random` / `sleep`随机与延迟工具
* - `getMimeType`根据文件扩展名推断 MIME
* - `getRealViewport` / `clamp`视口与坐标工具防止越界
* - `queryDeep`深层查询包含 Shadow DOM / iframe
* - `safeClick` / `humanType`拟人化点击与输入
* - `pasteImages` / `uploadFilesViaChooser`图片粘贴/上传辅助
* - `isPageValid` / `createPageCloseWatcher`页面有效性与关闭/崩溃监听
* - `getCookies`读取 CookiesJSON 格式
*/
import path from 'path';
import { logger } from '../utils/logger.js';
@@ -520,4 +535,3 @@ export async function getCookies(page) {
const context = page.context();
return await context.cookies();
}
@@ -3,7 +3,7 @@
* @description 封装 JSONSSE 响应和错误响应的统一处理函数
*/
import { getErrorDetails } from './errors.js';
import { getErrorDetails } from '../errors.js';
/**
* 发送 JSON 响应
@@ -4,10 +4,10 @@
*/
import crypto from 'crypto';
import { logger } from '../utils/logger.js';
import { ERROR_CODES } from './errors.js';
import { logger } from '../../utils/logger.js';
import { ERROR_CODES } from '../errors.js';
import { sendJson, sendApiError } from './respond.js';
import { parseRequest } from './parseChat.js';
import { parseRequest } from '../parseChat.js';
/**
* 鉴权检查
+3 -3
View File
@@ -1,5 +1,5 @@
/**
* @fileoverview lib/server 模块入口
* @fileoverview src/server 模块入口
* @description 导出服务器相关模块
*/
@@ -12,8 +12,8 @@ export {
sendApiError,
buildChatCompletion,
buildChatCompletionChunk
} from './respond.js';
} from './http/respond.js';
export { handleDisplayParams } from './display.js';
export { createQueueManager } from './queue.js';
export { parseRequest } from './parseChat.js';
export { createRouter } from './routes.js';
export { createRouter } from './http/routes.js';
+1 -1
View File
@@ -12,7 +12,7 @@ import {
sendApiError,
buildChatCompletion,
buildChatCompletionChunk
} from './respond.js';
} from './http/respond.js';
/**
* @typedef {object} TaskContext
+128
View File
@@ -0,0 +1,128 @@
/**
* @fileoverview 配置加载模块
* @description 负责读取/解析 `config.yaml`,并提供 API Key 生成能力(供脚本使用)。
*
* 约定:
* - 该模块只负责“读取 + 校验 + 默认值补全”,不负责创建/写入配置文件。
* - 初始化/拷贝配置请使用 `config.example.yaml` + `scripts/config-init.js`。
*/
import fs from 'fs';
import path from 'path';
import yaml from 'yaml';
import { logger } from './logger.js';
const CONFIG_PATH = path.join(process.cwd(), 'config.yaml');
const EXAMPLE_CONFIG_PATH = path.join(process.cwd(), 'config.example.yaml');
// 模块级缓存:确保配置只从磁盘读取一次
let cachedConfig = null;
/**
* 加载并校验配置(只读)
* @returns {object} 配置对象
*/
export function loadConfig() {
// 如果已有缓存,直接返回
if (cachedConfig) return cachedConfig;
if (!fs.existsSync(CONFIG_PATH)) {
const hint = fs.existsSync(EXAMPLE_CONFIG_PATH)
? `请复制 ${EXAMPLE_CONFIG_PATH}${CONFIG_PATH}`
: `请创建 ${CONFIG_PATH}(仓库根目录通常提供 config.example.yaml 作为模板)`;
throw new Error(`未找到配置文件: ${CONFIG_PATH}${hint}`);
}
const configFile = fs.readFileSync(CONFIG_PATH, 'utf8');
const config = yaml.parse(configFile);
if (!config || typeof config !== 'object') {
throw new Error(`配置文件解析失败: ${CONFIG_PATH}`);
}
// Docker 路径兼容处理
if ((!config.browser?.path || !fs.existsSync(config.browser.path)) &&
fs.existsSync('/app/camoufox/camoufox')) {
logger.info('配置器', '检测到容器环境,自动修正浏览器路径为 /app/camoufox/camoufox');
if (!config.browser) config.browser = {};
config.browser.path = '/app/camoufox/camoufox';
}
// 基础配置校验
if (!config.server || !config.server.port) {
throw new Error('配置文件缺少必需字段: server.port');
}
if (!config.server.auth) {
throw new Error('配置文件缺少必需字段: server.auth');
}
// 设置队列配置默认值
if (!config.queue) {
config.queue = {
maxConcurrent: 1,
maxQueueSize: 2,
imageLimit: 5
};
} else {
// 强制 maxConcurrent 为 1
config.queue.maxConcurrent = 1;
if (config.queue.maxQueueSize === undefined) config.queue.maxQueueSize = 2;
if (config.queue.imageLimit === undefined) config.queue.imageLimit = 5;
}
// 设置 keepalive 配置默认值(兼容旧字段:keepalive.enable
if (!config.server.keepalive) {
config.server.keepalive = { mode: 'comment' };
} else {
if (config.server.keepalive.mode === undefined) config.server.keepalive.mode = 'comment';
// 验证 mode 值
if (!['comment', 'content'].includes(config.server.keepalive.mode)) {
logger.warn('配置器', `无效的 keepalive.mode: ${config.server.keepalive.mode},使用默认值 comment`);
config.server.keepalive.mode = 'comment';
}
}
// 设置 backend 配置默认值
if (!config.backend) {
config.backend = {
type: 'lmarena',
geminiBiz: { entryUrl: '' }
};
}
// 新增:Merge 配置初始化
if (!config.backend.merge) {
config.backend.merge = {
enable: false,
type: ['zai_is', 'lmarena']
};
}
// 校验 GeminiBiz 配置
if (config.backend.type === 'gemini_biz') {
if (!config.backend.geminiBiz || !config.backend.geminiBiz.entryUrl) {
throw new Error('backend.type = gemini_biz requires backend.geminiBiz.entryUrl');
}
}
logger.debug('配置器', '已加载 config.yaml');
logger.debug('配置器', '后端类型:', config.backend.type);
logger.debug('配置器', '流式心跳模式:', config.server.keepalive.mode);
if (config.backend.type === 'gemini_biz') {
logger.debug('配置器', `GeminiBiz 入口: ${config.backend.geminiBiz.entryUrl}`);
}
// 设置日志级别
if (config.logLevel) {
logger.setLevel(config.logLevel);
}
// 缓存配置
cachedConfig = config;
return config;
}
// 默认导出为函数
export default loadConfig;
@@ -1,3 +1,11 @@
/**
* @fileoverview 控制台日志模块
* @description 提供带时间戳/级别/模块名的彩色日志输出并支持通过环境变量控制日志等级
*
* - 环境变量LOG_LEVEL=debug|info|warn|error
* - 输出格式YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
*/
import process from 'process';
const LEVELS = ['debug', 'info', 'warn', 'error'];
+6 -1
View File
@@ -1,7 +1,12 @@
/**
* @fileoverview 代理适配模块
* @description 将配置中的 HTTP/SOCKS5 代理转换为 Playwright 可用的代理配置并在需要时通过 proxy-chain 搭建本地 HTTP 代理桥接
*/
import { anonymizeProxy, closeAnonymizedProxy } from 'proxy-chain';
import { logger } from './logger.js';
// 全局代理状态追踪
// 全局代理状态:用于清理 proxy-chain 创建的本地代理资源
const proxyState = {
anonymizedProxyUrl: null, // 转换后的 HTTP 代理地址
originalProxyUrl: null // 原始代理地址