feat: 配置文件机制修改,优化WebUI日志排列

This commit is contained in:
foxhui
2025-12-23 03:14:02 +08:00
Unverified
parent ce72c5f9ec
commit d75c84f4c5
27 changed files with 275 additions and 426 deletions
+10 -1
View File
@@ -5,7 +5,16 @@ 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).
## [3.3.0] - 2025-12-21
## [3.3.2] - 2025-12-22
### 🔄 Changed
- **配置文件**
- 自动复制初始化配置文件,并放进`data/config.yaml`Docker友好化
- 优化 Dockerfile
- 初始化脚本不再依赖配置文件,支持交互式和参数传入式配置代理
- 优化 WebUI 文案和日志排列
## [3.3.1] - 2025-12-21
### ✨ Added
- **新增适配器**
+2 -13
View File
@@ -32,18 +32,7 @@ RUN npm install -g pnpm && pnpm install --frozen-lockfile
COPY . .
RUN npm run init
# 4. 生成默认配置文件
RUN cp config.example.yaml config.yaml
EXPOSE 3000 5900
# 5. 启动脚本:同步 data 目录中的配置文件
CMD ["/bin/sh", "-c", "\
if [ -f /app/data/config.yaml ]; then \
cp /app/data/config.yaml /app/config.yaml; \
else \
mkdir -p /app/data; \
cp /app/config.yaml /app/data/config.yaml; \
fi; \
npm start -- -xvfb -vnc \
"]
# 4. 启动服务(配置文件会自动从 config.example.yaml 复制到 data/config.yaml
CMD ["npm", "start", "--", "-xvfb", "-vnc"]
+66 -64
View File
@@ -2,7 +2,11 @@
<p align="center">
<img src="https://github.com/user-attachments/assets/296a518e-c42b-4e39-8ff6-9b4381ed4f6e" width="49%" />
<img src="https://github.com/user-attachments/assets/06f31024-ecd4-48d2-9789-eedc98c9c5b9" width="49%" />
<img src="https://github.com/user-attachments/assets/bfa30ece-6947-4f18-b2c9-ccc8087b7e89" width="49%" />
</p>
<p align="center">
<img src="https://github.com/user-attachments/assets/5b15ebd2-7593-4f0e-8561-83d6ba5d88ab" width="49%" />
<img src="https://github.com/user-attachments/assets/53deea29-4071-4a07-8a61-211761c5f2f7" width="49%" />
</p>
## 📑 目录
@@ -17,15 +21,15 @@
## 📝 项目简介
**WebAI2API** 是一个基于 **Camoufox (Playwright)** 的网页版 AI 服务转通用 API 的工具。通过模拟人类操作与 LMArena、Gemini 等网站交互,提供兼容 **OpenAI 格式** 的接口服务,同时支持 **多窗口并发****多账号管理**(浏览器实例数据隔离)。
**WebAI2API** 是一个基于 **Camoufox (Playwright)** 的网页版 AI 服务转通用 API 的工具。通过模拟人类操作与 LMArena、Gemini 等网站交互, 提供兼容 **OpenAI 格式** 的接口服务, 同时支持 **多窗口并发****多账号管理**(浏览器实例数据隔离)。
### ✨ 主要特性
- 🤖 **拟人交互**: 模拟人类打字与鼠标轨迹,通过特征伪装规避自动化检测
- 🔄 **接口兼容**: 提供标准 OpenAI 格式接口,支持流式响应与心跳保活
- 🚀 **并发隔离**: 支持多窗口并发执行,可配置独立代理,实现多账号浏览器实例级数据隔离
- 🤖 **拟人交互**: 模拟人类打字与鼠标轨迹, 通过特征伪装规避自动化检测
- 🔄 **接口兼容**: 提供标准 OpenAI 格式接口, 支持流式响应与心跳保活
- 🚀 **并发隔离**: 支持多窗口并发执行, 可配置独立代理,实现多账号浏览器实例级数据隔离
- 🛡️ **稳定防护**: 内置任务队列、负载均衡、故障转移、错误重试等基础功能
- 🎨 **Web 管理界面**: 提供可视化管理界面支持实时日志查看、VNC 连接、适配器管理等
- 🎨 **网页管理**: 提供可视化管理界面, 支持实时日志查看、VNC 连接、适配器管理等
### 📋 支持列表
@@ -43,7 +47,7 @@
> [!NOTE]
> **获取完整模型列表**: 通过 `GET /v1/models` 接口查看当前配置下所有可用模型及其详细信息。
>
> ✅目前支持;❌目前不支持,但未来可能会支持;🚫网站不支持是否在支持看网站具体情况;
> ✅目前支持;❌目前不支持,但未来可能会支持;🚫网站不支持, 未来是否在支持看网站具体情况;
---
@@ -61,12 +65,14 @@
1. **安装与配置**
```bash
# 1. 复制配置文件
cp config.example.yaml config.yaml
# 2. 安装依赖与初始化环境
# 1. 安装 NPM 依赖
pnpm install
npm run init # ⚠️ 需确保网络能连接 GitHub
# 2. 安装浏览器等预编译依赖
# ⚠️ 该脚本需连接 GitHub 下载资源。若网络受限,请使用代理
npm run init
# 使用代理
# 直接使用 -proxy 可交互式输入代理配置
npm run init -- -proxy=http://username:passwd@host:port
```
2. **启动服务**
@@ -76,15 +82,18 @@
# Linux 系统 - 虚拟显示启动
npm start -- -xvfb -vnc
# 登录模式 (会临时强行禁用无头模式和自动化)
npm start -- -login (-xvfb -vnc)
```
### 🐳 方式二:Docker 部署
> [!WARNING]
> **安全提醒**:
> - Docker 镜像默认开启虚拟显示器(Xvfb)和 VNC 服务
> - 可通过 WebUI 的虚拟显示器板块或 RealVNC 等工具连接(默认端口 5900)
> - **VNC 和 WebUI 传输过程未加密,公网环境请务必使用 SSH 隧道或 HTTPS**
> - Docker 镜像默认开启虚拟显示器 (Xvfb) 和 VNC 服务
> - 可通过 WebUI 的虚拟显示器板块连接
> - **WebUI 传输过程未加密, 公网环境请使用 SSH 隧道或 HTTPS**
**Docker CLI 启动**
```bash
@@ -100,71 +109,64 @@ docker run -d --name webai-2api \
docker-compose up -d
```
**映射 VNC 端口(如需使用第三方 VNC 客户端)**
```bash
docker run -d --name webai-2api \
-p 3000:3000 \
-p 5900:5900 \
-v "$(pwd)/data:/app/data" \
--shm-size=2gb \
foxhui/webai-2api:latest
```
---
## ⚡ 快速开始
### 1. 访问 Web 管理界面
### 1. 调整配置文件
服务启动后,打开浏览器访问:
程序初次运行会从`config.example.yaml`复制配置文件到`data/config.yaml`
**配置文件的生效需要重启程序!**
```yaml
server:
# 监听端口
port: 3000
# 鉴权 API Token (可使用 npm run genkey 生成)
# 该配置会对 API 接口和 WebUI 生效
auth: sk-change-me-to-your-secure-key
```
> [!TIP]
> **完整配置说明**: 请参考 [config.example.yaml](config.example.yaml) 文件中的详细注释,或访问 [WebAI2API 文档中心](https://foxhui.github.io/WebAI2API/) 查看完整配置指南。
### 2. 访问 Web 管理界面
服务启动后, 打开浏览器访问:
```
http://localhost:3000
```
> [!TIP]
> **远程访问**: 将 `localhost` 替换为服务器 IP 地址即可远程访问。
>
> **API Token**: 配置文件中的`auth`所配置的鉴权密钥。
> **安全建议**: 公网环境建议使用 Nginx/Caddy 配置 HTTPS 或通过 SSH 隧道访问。
### 2. 初始化账号登录
### 3. 初始化账号登录
> [!IMPORTANT]
> **首次使用必须完成以下初始化步骤**:
1. **连接虚拟显示器**:
- Linux/Docker: 在 WebUI 的"虚拟显示器"板块连接,或使用第三方 VNC 工具连接端口 5900
- Linux/Docker: 在 WebUI 的"虚拟显示器"板块连接
- Windows: 直接在弹出的浏览器窗口中操作
2. **完成账号登录**:
- 手动登录所需的 AI 网站账号
- 在输入框发送任意消息,触发并完成 CloudFlare/reCAPTCHA 验证
- 同意服务条款(如需要)
- 在输入框发送任意消息, 触发并完成人机验证 (如需要)
- 同意服务条款或者新手指引 (如需要)
- 确保不再有初次使用相关内容的阻拦
3. **SSH 隧道连接示例**(公网服务器推荐):
```bash
# 在本地终端运行,将服务器的 VNC 和 WebUI 映射到本地
ssh -L 3000:127.0.0.1:3000 -L 5900:127.0.0.1:5900 root@服务器IP
# 在本地终端运行,将服务器的 WebUI 映射到本地
ssh -L 3000:127.0.0.1:3000 root@服务器IP
# 然后在本地访问
# WebUI: http://localhost:3000
# VNC: localhost:5900
```
### 3. 调整配置文件
首次使用必须从 config.example.yaml 复制为 config.yaml (Docker 用户可忽略)
### 基础配置
```yaml
server:
port: 3000 # API 服务端口
apiKey: "your-key" # API 访问密钥
```
> [!TIP]
> **完整配置说明**: 请参考 [config.example.yaml](config.example.yaml) 文件中的详细注释,或访问 [WebAI2API 文档中心](https://foxhui.github.io/WebAI2API/) 查看完整配置指南。
---
## 📖 使用方法
@@ -173,10 +175,10 @@ server:
> [!NOTE]
> **关于有头/无头模式**:
> - **有头模式**(默认): 显示浏览器窗口,便于调试和人工干预
> - **无头模式**: 后台运行,节省资源但无法查看浏览器界面
> - **有头模式**(默认): 显示浏览器窗口, 便于调试和人工干预
> - **无头模式**: 后台运行, 节省资源但无法查看浏览器界面, 且可能会被网站检测
>
> **建议**: 为降低风控,**强烈建议长期保持非无头模式运行**(至少保持虚拟显示器 Xvfb)。
> **建议**: 为降低风控, **强烈建议长期保持非无头模式运行**(或使用虚拟显示器 Xvfb)。
---
@@ -190,9 +192,9 @@ server:
> [!WARNING]
> **并发限制与流式保活建议**
>
> 本项目通过模拟真实浏览器操作实现,处理过程根据实际情况时间可能有所变化,当积压的任务超过设置的数量时会直接拒绝非流式模式的请求。
> 本项目通过模拟真实浏览器操作实现, 处理过程根据实际情况时间可能有所变化, 当积压的任务超过设置的数量时会直接拒绝非流式模式的请求。
>
> **💡 强烈建议开启流式模式**: 服务器将发送保活心跳包,可无限排队避免超时。
> **💡 强烈建议开启流式模式**: 服务器将发送保活心跳包, 可无限排队避免超时。
#### 文本对话
@@ -224,15 +226,15 @@ curl http://localhost:3000/v1/chat/completions \
| 参数 | 类型 | 必填 | 说明 |
| :--- | :--- | :---: | :--- |
| `model` | string | ✅ | 模型名称,可通过 `/v1/models` 获取可用列表 |
| `stream` | boolean | 推荐 | 是否开启流式响应,包含心跳保活机制 |
| `model` | string | ✅ | 模型名称, 可通过 `/v1/models` 获取可用列表 |
| `stream` | boolean | 推荐 | 是否开启流式响应, 包含心跳保活机制 |
> [!NOTE]
> **关于流式保活 (Heartbeat)**
>
> 为防止长连接超时,系统提供两种保活模式 (可在配置中切换):
> 1. **Comment 模式 (默认/推荐)**: 发送 `:keepalive` 注释,符合 SSE 标准,兼容性最好
> 2. **Content 模式**: 发送空内容的 data 包,仅用于必须收到 JSON 数据才重置超时的特殊客户端
> 为防止长连接超时, 系统提供两种保活模式 (可在配置中切换):
> 1. **Comment 模式 (默认/推荐)**: 发送 `:keepalive` 注释, 符合 SSE 标准,兼容性最好
> 2. **Content 模式**: 发送空内容的 data 包, 仅用于必须收到 JSON 数据才重置超时的特殊客户端
### 2. 获取模型列表
@@ -272,8 +274,8 @@ curl "http://localhost:3000/v1/cookies?name=browser_default&domain=lmarena.ai" \
| **磁盘** | 2 GB 可用空间 | 5 GB 及以上 | 7 GB 及以上 |
**实测环境表现** (均为单浏览器实例):
- **Oracle 免费机** (1C1G, Debian 12): 资源紧张,比较卡顿,仅供尝鲜或轻度使用
- **阿里云轻量云** (2C2G, Debian 11): 运行流畅,项目开发测试所用机型
- **Oracle 免费机** (1C1G, Debian 12): 资源紧张, 比较卡顿, 仅供尝鲜或轻度使用
- **阿里云轻量云** (2C2G, Debian 11): 运行流畅但实例也会卡顿, 项目开发测试所用机型
---
@@ -290,11 +292,11 @@ curl "http://localhost:3000/v1/cookies?name=browser_default&domain=lmarena.ai" \
## 📋 更新日志
查看完整的版本历史和更新内容,请访问 [CHANGELOG.md](CHANGELOG.md)。
查看完整的版本历史和更新内容, 请访问 [CHANGELOG.md](CHANGELOG.md)。
### 🕰️ 历史版本说明
本项目已从 Puppeteer 迁移至 Camoufox,以应对日益复杂的反机器人检测机制。基于 Puppeteer 的旧版本代码已归档至 `puppeteer-edition` 分支,仅作留存,**不再提供更新与维护**。
本项目已从 Puppeteer 迁移至 Camoufox, 以应对日益复杂的反机器人检测机制。基于 Puppeteer 的旧版本代码已归档至 `puppeteer-edition` 分支, 仅作留存, **不再提供更新与维护**
---
+14 -11
View File
@@ -4,7 +4,8 @@ logLevel: info
server:
# 监听端口
port: 3000
# 鉴权 Token (Bearer Token) (可使用 npm run genkey 生成)
# 鉴权 API Token (可使用 npm run genkey 生成)
# 该配置会对 API 接口和 WebUI 生效
auth: sk-change-me-to-your-secure-key
# 流式请求心跳设置 (自动对 stream: true 的请求发送心跳防止超时)
keepalive:
@@ -56,15 +57,16 @@ backend:
workers:
- name: "default" # 唯一标识 (用于登录模式和日志显示)
# 适配器类型列表:
# lmarena
# lmarena_text
# gemini_biz
# gemini_biz_text
# gemini
# gemini_text
# zai_is
# nanobananafree_ai
# zenmux_ai_text
# lmarena (LMArena 图片生成)
# lmarena_text (LMArena 文本生成)
# gemini_biz (Gemini Business 图片、视频生成)
# gemini_biz_text (Gemini Business 文本生成)
# gemini (Google Gemini 图片、视频生成)
# gemini_text (Google Gemini 文本生成)
# zai_is (zAI 图片生成)
# nanobananafree_ai (NanoBananaFree 图片生成)
# zenmux_ai_text (ZenMux 文本生成)
# chatgpt (ChatGPT 图片生成)
type: lmarena # 适配器类型
# ------------------------------------------------
@@ -132,7 +134,8 @@ queue:
imageLimit: 5
browser:
# 浏览器可执行文件路径 (留空则使用 Camoufox 默认下载路径)
# 浏览器可执行文件路径 (留空则使用默认的)
# 非必要不建议修改,否则你要处理很多额外依赖
# Windows系统示例 "C:\\camoufox\\camoufox.exe"
# Linux系统示例 "/opt/camoufox/camoufox"
path: ""
+86 -66
View File
@@ -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
View File
@@ -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);
}
})();
+63 -9
View File
@@ -13,11 +13,65 @@ import yaml from 'yaml';
import { logger } from '../utils/logger.js';
const CONFIG_PATH = path.join(process.cwd(), 'config.yaml');
// --- 配置文件路径常量 ---
const DATA_DIR = path.join(process.cwd(), 'data');
const DATA_CONFIG_PATH = path.join(DATA_DIR, 'config.yaml');
const ROOT_CONFIG_PATH = path.join(process.cwd(), 'config.yaml');
const EXAMPLE_CONFIG_PATH = path.join(process.cwd(), 'config.example.yaml');
// 模块级缓存:确保配置只从磁盘读取一次
let cachedConfig = null;
// 实际使用的配置文件路径
let activeConfigPath = null;
/**
* 解析配置文件路径(优先级:data/config.yaml > config.yaml > 从 config.example.yaml 复制)
* 自动迁移:如果只有根目录 config.yaml,会自动移动到 data/config.yaml
* @returns {string} 配置文件路径
*/
function resolveConfigPath() {
// 1. 优先使用 data/config.yaml
if (fs.existsSync(DATA_CONFIG_PATH)) {
return DATA_CONFIG_PATH;
}
// 2. 根目录有 config.yaml,自动迁移到 data/config.yaml
if (fs.existsSync(ROOT_CONFIG_PATH)) {
// 确保 data 目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
// 移动文件到 data 目录
fs.renameSync(ROOT_CONFIG_PATH, DATA_CONFIG_PATH);
logger.info('配置器', `已将 ${ROOT_CONFIG_PATH} 迁移到 ${DATA_CONFIG_PATH}`);
return DATA_CONFIG_PATH;
}
// 3. 两个都没有,从 config.example.yaml 复制到 data/config.yaml
if (fs.existsSync(EXAMPLE_CONFIG_PATH)) {
// 确保 data 目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
}
fs.copyFileSync(EXAMPLE_CONFIG_PATH, DATA_CONFIG_PATH);
logger.info('配置器', `已从 ${EXAMPLE_CONFIG_PATH} 复制配置到 ${DATA_CONFIG_PATH}`);
return DATA_CONFIG_PATH;
}
// 4. 都没有,返回 data/config.yaml 路径(后续会抛出错误)
return DATA_CONFIG_PATH;
}
/**
* 获取当前使用的配置文件路径
* @returns {string} 配置文件路径
*/
export function getConfigPath() {
if (!activeConfigPath) {
activeConfigPath = resolveConfigPath();
}
return activeConfigPath;
}
/**
* 解析用户数据目录路径
@@ -147,17 +201,17 @@ 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 configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
throw new Error(`未找到配置文件: ${configPath}请确保 data/config.yaml、config.yaml 或 config.example.yaml 存在。`);
}
const configFile = fs.readFileSync(CONFIG_PATH, 'utf8');
const configFile = fs.readFileSync(configPath, 'utf8');
let config = yaml.parse(configFile);
if (!config || typeof config !== 'object') {
throw new Error(`配置文件解析失败: ${CONFIG_PATH}`);
throw new Error(`配置文件解析失败: ${configPath}`);
}
// Docker 路径兼容处理
@@ -265,7 +319,7 @@ export function loadConfig() {
}
// 日志输出
logger.debug('配置器', '已加载 config.yaml');
logger.debug('配置器', `已加载配置文件: ${configPath}`);
logger.debug('配置器', `Instances: ${config.backend.pool.instances.length}, Workers: ${config.backend.pool.workers.length}`);
logger.debug('配置器', `调度策略: ${config.backend.pool.strategy}`);
logger.debug('配置器', `流式心跳模式: ${config.server.keepalive.mode}`);
+7 -6
View File
@@ -7,18 +7,18 @@ import fs from 'fs';
import path from 'path';
import yaml from 'yaml';
import { logger } from '../utils/logger.js';
const CONFIG_PATH = path.join(process.cwd(), 'config.yaml');
import { getConfigPath } from './index.js';
/**
* 读取原始配置(不带缓存,直接从磁盘读取)
* @returns {object} 原始配置对象
*/
function readRawConfig() {
if (!fs.existsSync(CONFIG_PATH)) {
const configPath = getConfigPath();
if (!fs.existsSync(configPath)) {
throw new Error('配置文件不存在');
}
const content = fs.readFileSync(CONFIG_PATH, 'utf8');
const content = fs.readFileSync(configPath, 'utf8');
return yaml.parse(content);
}
@@ -27,13 +27,14 @@ function readRawConfig() {
* @param {object} config - 完整配置对象
*/
function writeConfig(config) {
const configPath = getConfigPath();
// 使用 yaml 库的默认序列化(会丢失注释,但结构正确)
const content = yaml.stringify(config, {
indent: 2,
lineWidth: 0 // 不自动换行
});
fs.writeFileSync(CONFIG_PATH, content, 'utf8');
logger.info('管理器', '配置已保存到 config.yaml');
fs.writeFileSync(configPath, content, 'utf8');
logger.info('管理器', `配置已保存到 ${configPath}`);
}
/**
@@ -7,7 +7,7 @@ import fs from 'fs';
import path from 'path';
import os from 'os';
import crypto from 'crypto';
import { logger } from './logger.js';
import { logger } from '../utils/logger.js';
const PROJECT_ROOT = process.cwd();
+1 -1
View File
@@ -18,7 +18,7 @@
import http from 'http';
// ==================== 启动前自检 ====================
import { runPreflight } from '../utils/preflight.js';
import { runPreflight } from './preflight.js';
runPreflight();
// ==================== 加载其他依赖 ====================
const { getBackend } = await import('../backend/index.js');
+4 -1
View File
@@ -118,7 +118,10 @@ export function log(level, mod, msg, meta = {}) {
const ts = formatTime();
const levelMap = { debug: 'DBUG', info: 'INFO', warn: 'WARN', error: 'ERRO' };
const levelTag = levelMap[level.toLowerCase()] || level.toUpperCase().slice(0, 4);
const base = `${ts} [${levelTag}] [${mod}] ${msg}`;
// 将消息中的换行符替换为 ↵ 符号,保持日志为单行
const sanitizedMsg = msg.replace(/\r?\n/g, ' ↵ ');
const base = `${ts} [${levelTag}] [${mod}] ${sanitizedMsg}`;
const metaStr = Object.keys(meta).length
? ' | ' + Object.entries(meta).map(([k, v]) => {
@@ -1 +1 @@
import{c as i,I as u}from"./index-SQeV-w6F.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
import{c as i,I as u}from"./index-6uqbLXPN.js";var l={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M928 140H96c-17.7 0-32 14.3-32 32v496c0 17.7 14.3 32 32 32h380v112H304c-8.8 0-16 7.2-16 16v48c0 4.4 3.6 8 8 8h432c4.4 0 8-3.6 8-8v-48c0-8.8-7.2-16-16-16H548V700h380c17.7 0 32-14.3 32-32V172c0-17.7-14.3-32-32-32zm-40 488H136V212h752v416z"}}]},name:"desktop",theme:"outlined"};function c(r){for(var t=1;t<arguments.length;t++){var e=arguments[t]!=null?Object(arguments[t]):{},n=Object.keys(e);typeof Object.getOwnPropertySymbols=="function"&&(n=n.concat(Object.getOwnPropertySymbols(e).filter(function(a){return Object.getOwnPropertyDescriptor(e,a).enumerable}))),n.forEach(function(a){s(r,a,e[a])})}return r}function s(r,t,e){return t in r?Object.defineProperty(r,t,{value:e,enumerable:!0,configurable:!0,writable:!0}):r[t]=e,r}var o=function(t,e){var n=c({},t,e.attrs);return i(u,c({},n,{icon:l}),null)};o.displayName="DesktopOutlined";o.inheritAttrs=!1;export{o as D};
@@ -1 +1 @@
import{c as l,I as q,j as I,r as b,k as D,o as F,l as T,b as _,d as u,w as s,e as v,f as c,g as y,u as h,t as w,S as L,h as g,i as f,m as G,F as J}from"./index-SQeV-w6F.js";var Q={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"};function S(p){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?Object(arguments[n]):{},o=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(o=o.concat(Object.getOwnPropertySymbols(a).filter(function(t){return Object.getOwnPropertyDescriptor(a,t).enumerable}))),o.forEach(function(t){R(p,t,a[t])})}return p}function R(p,n,a){return n in p?Object.defineProperty(p,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):p[n]=a,p}var k=function(n,a){var o=S({},n,a.attrs);return l(q,S({},o,{icon:Q}),null)};k.displayName="AppstoreOutlined";k.inheritAttrs=!1;const W={style:{display:"flex","align-items":"center","justify-content":"space-between",gap:"8px"}},X={style:{display:"flex","align-items":"center","min-width":"0",flex:"1"}},Y={style:{"font-weight":"600","font-size":"14px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"}},Z={key:0},K={key:2},ee={key:4,style:{"font-size":"12px",color:"#8c8c8c","margin-top":"4px"}},te={style:{"text-align":"right"}},ae={__name:"adapters",setup(p){const n=I(),a=b(!1),o=b(null),t=D({});F(async()=>{await Promise.all([n.fetchAdaptersMeta(),n.fetchAdapterConfig()])});const O=T(()=>n.adaptersMeta),C=m=>{o.value=m;const r=n.adapterConfig[m.id]||{};Object.keys(t).forEach(i=>delete t[i]),m.configSchema&&m.configSchema.forEach(i=>{r[i.key]!==void 0?t[i.key]=r[i.key]:t[i.key]=i.default}),a.value=!0},V=async()=>{if(!o.value)return;const m={[o.value.id]:{...t}};await n.saveAdapterConfig(m)&&(a.value=!1)};return(m,r)=>{const i=c("a-button"),x=c("a-card"),z=c("a-list-item"),A=c("a-list"),U=c("a-empty"),H=c("a-input"),j=c("a-input-number"),M=c("a-switch"),P=c("a-select"),B=c("a-form-item"),E=c("a-form"),N=c("a-drawer"),$=c("a-layout");return u(),_($,{style:{background:"transparent"}},{default:s(()=>[l(x,{title:"适配器管理",bordered:!1},{extra:s(()=>[l(i,{type:"link",onClick:h(n).fetchAdaptersMeta},{default:s(()=>[...r[2]||(r[2]=[g("刷新列表",-1)])]),_:1},8,["onClick"])]),default:s(()=>[l(A,{grid:{gutter:16,xs:1,sm:2,md:3,lg:3,xl:4,xxl:4},"data-source":O.value},{renderItem:s(({item:e})=>[l(z,null,{default:s(()=>[l(x,{hoverable:"",onClick:d=>C(e),bodyStyle:{padding:"12px 16px"}},{default:s(()=>[y("div",W,[y("div",X,[l(h(k),{style:{"font-size":"18px",color:"#1890ff","margin-right":"8px","flex-shrink":"0"}}),y("span",Y,w(e.id),1)]),l(h(L),{style:{"font-size":"16px",color:"#8c8c8c","flex-shrink":"0"}})])]),_:2},1032,["onClick"])]),_:2},1024)]),_:1},8,["data-source"])]),_:1}),o.value?(u(),_(N,{key:0,open:a.value,"onUpdate:open":r[1]||(r[1]=e=>a.value=e),title:`配置适配器 - ${o.value.id}`,width:"500",placement:"right"},{footer:s(()=>[y("div",te,[l(i,{style:{"margin-right":"8px"},onClick:r[0]||(r[0]=e=>a.value=!1)},{default:s(()=>[...r[3]||(r[3]=[g("取消",-1)])]),_:1}),l(i,{type:"primary",onClick:V},{default:s(()=>[...r[4]||(r[4]=[g("保存配置",-1)])]),_:1})])]),default:s(()=>[!o.value.configSchema||o.value.configSchema.length===0?(u(),f("div",Z,[l(U,{description:"该适配器没有可配置项"})])):(u(),_(E,{key:1,layout:"vertical"},{default:s(()=>[(u(!0),f(J,null,G(o.value.configSchema,e=>(u(),_(B,{key:e.key,label:e.label,required:e.required},{default:s(()=>[e.type==="string"?(u(),_(H,{key:0,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,placeholder:e.placeholder},null,8,["value","onUpdate:value","placeholder"])):v("",!0),e.type==="number"?(u(),_(j,{key:1,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,min:e.min,max:e.max,style:{width:"100%"}},null,8,["value","onUpdate:value","min","max"])):v("",!0),e.type==="boolean"?(u(),f("div",K,[l(M,{checked:t[e.key],"onUpdate:checked":d=>t[e.key]=d},null,8,["checked","onUpdate:checked"])])):v("",!0),e.type==="select"?(u(),_(P,{key:3,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,options:e.options},null,8,["value","onUpdate:value","options"])):v("",!0),e.note?(u(),f("div",ee,w(e.note),1)):v("",!0)]),_:2},1032,["label","required"]))),128))]),_:1}))]),_:1},8,["open","title"])):v("",!0)]),_:1})}}};export{ae as default};
import{c as l,I as q,j as I,r as b,k as D,o as F,l as T,b as _,d as u,w as s,e as v,f as c,g as y,u as h,t as w,S as L,h as g,i as f,m as G,F as J}from"./index-6uqbLXPN.js";var Q={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 144H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H212V212h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V160c0-8.8-7.2-16-16-16zm-52 268H612V212h200v200zM464 544H160c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H212V612h200v200zm452-268H560c-8.8 0-16 7.2-16 16v304c0 8.8 7.2 16 16 16h304c8.8 0 16-7.2 16-16V560c0-8.8-7.2-16-16-16zm-52 268H612V612h200v200z"}}]},name:"appstore",theme:"outlined"};function S(p){for(var n=1;n<arguments.length;n++){var a=arguments[n]!=null?Object(arguments[n]):{},o=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(o=o.concat(Object.getOwnPropertySymbols(a).filter(function(t){return Object.getOwnPropertyDescriptor(a,t).enumerable}))),o.forEach(function(t){R(p,t,a[t])})}return p}function R(p,n,a){return n in p?Object.defineProperty(p,n,{value:a,enumerable:!0,configurable:!0,writable:!0}):p[n]=a,p}var k=function(n,a){var o=S({},n,a.attrs);return l(q,S({},o,{icon:Q}),null)};k.displayName="AppstoreOutlined";k.inheritAttrs=!1;const W={style:{display:"flex","align-items":"center","justify-content":"space-between",gap:"8px"}},X={style:{display:"flex","align-items":"center","min-width":"0",flex:"1"}},Y={style:{"font-weight":"600","font-size":"14px",overflow:"hidden","text-overflow":"ellipsis","white-space":"nowrap"}},Z={key:0},K={key:2},ee={key:4,style:{"font-size":"12px",color:"#8c8c8c","margin-top":"4px"}},te={style:{"text-align":"right"}},ae={__name:"adapters",setup(p){const n=I(),a=b(!1),o=b(null),t=D({});F(async()=>{await Promise.all([n.fetchAdaptersMeta(),n.fetchAdapterConfig()])});const O=T(()=>n.adaptersMeta),C=m=>{o.value=m;const r=n.adapterConfig[m.id]||{};Object.keys(t).forEach(i=>delete t[i]),m.configSchema&&m.configSchema.forEach(i=>{r[i.key]!==void 0?t[i.key]=r[i.key]:t[i.key]=i.default}),a.value=!0},V=async()=>{if(!o.value)return;const m={[o.value.id]:{...t}};await n.saveAdapterConfig(m)&&(a.value=!1)};return(m,r)=>{const i=c("a-button"),x=c("a-card"),z=c("a-list-item"),A=c("a-list"),U=c("a-empty"),H=c("a-input"),j=c("a-input-number"),M=c("a-switch"),P=c("a-select"),B=c("a-form-item"),E=c("a-form"),N=c("a-drawer"),$=c("a-layout");return u(),_($,{style:{background:"transparent"}},{default:s(()=>[l(x,{title:"适配器管理",bordered:!1},{extra:s(()=>[l(i,{type:"link",onClick:h(n).fetchAdaptersMeta},{default:s(()=>[...r[2]||(r[2]=[g("刷新列表",-1)])]),_:1},8,["onClick"])]),default:s(()=>[l(A,{grid:{gutter:16,xs:1,sm:2,md:3,lg:3,xl:4,xxl:4},"data-source":O.value},{renderItem:s(({item:e})=>[l(z,null,{default:s(()=>[l(x,{hoverable:"",onClick:d=>C(e),bodyStyle:{padding:"12px 16px"}},{default:s(()=>[y("div",W,[y("div",X,[l(h(k),{style:{"font-size":"18px",color:"#1890ff","margin-right":"8px","flex-shrink":"0"}}),y("span",Y,w(e.id),1)]),l(h(L),{style:{"font-size":"16px",color:"#8c8c8c","flex-shrink":"0"}})])]),_:2},1032,["onClick"])]),_:2},1024)]),_:1},8,["data-source"])]),_:1}),o.value?(u(),_(N,{key:0,open:a.value,"onUpdate:open":r[1]||(r[1]=e=>a.value=e),title:`配置适配器 - ${o.value.id}`,width:"500",placement:"right"},{footer:s(()=>[y("div",te,[l(i,{style:{"margin-right":"8px"},onClick:r[0]||(r[0]=e=>a.value=!1)},{default:s(()=>[...r[3]||(r[3]=[g("取消",-1)])]),_:1}),l(i,{type:"primary",onClick:V},{default:s(()=>[...r[4]||(r[4]=[g("保存配置",-1)])]),_:1})])]),default:s(()=>[!o.value.configSchema||o.value.configSchema.length===0?(u(),f("div",Z,[l(U,{description:"该适配器没有可配置项"})])):(u(),_(E,{key:1,layout:"vertical"},{default:s(()=>[(u(!0),f(J,null,G(o.value.configSchema,e=>(u(),_(B,{key:e.key,label:e.label,required:e.required},{default:s(()=>[e.type==="string"?(u(),_(H,{key:0,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,placeholder:e.placeholder},null,8,["value","onUpdate:value","placeholder"])):v("",!0),e.type==="number"?(u(),_(j,{key:1,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,min:e.min,max:e.max,style:{width:"100%"}},null,8,["value","onUpdate:value","min","max"])):v("",!0),e.type==="boolean"?(u(),f("div",K,[l(M,{checked:t[e.key],"onUpdate:checked":d=>t[e.key]=d},null,8,["checked","onUpdate:checked"])])):v("",!0),e.type==="select"?(u(),_(P,{key:3,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,options:e.options},null,8,["value","onUpdate:value","options"])):v("",!0),e.note?(u(),f("div",ee,w(e.note),1)):v("",!0)]),_:2},1032,["label","required"]))),128))]),_:1}))]),_:1},8,["open","title"])):v("",!0)]),_:1})}}};export{ae as default};
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,2 +1,2 @@
import{c as l,I as P,_ as q,j as J,r as p,l as Q,o as X,a as Y,b as N,d as b,w as s,g as v,f,h as m,u as C,R as k,y as Z,D as K,i as L,e as S,t as _,m as ee,F as te,s as R,M as ae,z as le,A as ne,B as oe}from"./index-SQeV-w6F.js";var se={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z"}},{tag:"path",attrs:{d:"M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2a281.38 281.38 0 00123.2-149.5A120 120 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3C584 874.3 548.8 884 512 884s-72-9.7-102.9-27.8c-30.3-17.7-55.6-43-73.3-73.3A202.75 202.75 0 01308 680V412h408v268z"}}]},name:"bug",theme:"outlined"};function D(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},r=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),r.forEach(function(c){re(n,c,a[c])})}return n}function re(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var j=function(t,a){var r=D({},t,a.attrs);return l(P,D({},r,{icon:se}),null)};j.displayName="BugOutlined";j.inheritAttrs=!1;var ce={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"};function E(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},r=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(r=r.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),r.forEach(function(c){ie(n,c,a[c])})}return n}function ie(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var I=function(t,a){var r=E({},t,a.attrs);return l(P,E({},r,{icon:ce}),null)};I.displayName="WarningOutlined";I.inheritAttrs=!1;const ue={class:"toolbar"},de={class:"toolbar-row"},fe={class:"toolbar-row"},ve={style:{"margin-bottom":"12px",color:"#8c8c8c","font-size":"12px"}},me={key:0,style:{color:"#1890ff","margin-left":"8px"}},pe={class:"log-container"},ge={class:"log-time"},_e={class:"log-module"},he={class:"log-message"},Oe={__name:"logs",setup(n){const t=J(),a=p([]),r=p(!1),c=p(0),h=p(!1),g=p(null),y=p(""),w=p("all"),U={INFO:{color:"#1890ff",icon:oe},WARN:{color:"#faad14",icon:I},ERRO:{color:"#ff4d4f",icon:ne},DBUG:{color:"#722ed1",icon:j}},z=async()=>{r.value=!0;try{const o=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(o.ok){const e=await o.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{r.value=!1}},A=o=>o.map((e,u)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:u,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:u,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(o=>{if(w.value!=="all"&&o.level!==w.value)return!1;if(y.value){const e=y.value.toLowerCase();return o.raw.toLowerCase().includes(e)}return!0})),F=()=>{ae.confirm({title:"确认清除日志",content:"此操作将删除所有系统日志文件,是否继续?",okText:"确认清除",okType:"danger",cancelText:"取消",async onOk(){try{(await fetch("/admin/logs",{method:"DELETE",headers:t.getHeaders()})).ok?(R.success("日志已清除"),a.value=[],c.value=0):R.error("清除失败")}catch{R.error("请求失败")}}})},M=()=>{const o=a.value.map(O=>O.raw).join(`
`),e=new Blob([o],{type:"text/plain"}),u=URL.createObjectURL(e),d=document.createElement("a");d.href=u,d.download=`system-${new Date().toISOString().split("T")[0]}.log`,d.click(),URL.revokeObjectURL(u)},T=o=>{h.value=o,o?(z(),g.value=setInterval(z,5e3)):g.value&&(clearInterval(g.value),g.value=null)};return X(()=>{z()}),Y(()=>{g.value&&clearInterval(g.value)}),(o,e)=>{const u=f("a-select-option"),d=f("a-select"),O=f("a-button"),B=f("a-tooltip"),V=f("a-space"),W=f("a-input-search"),$=f("a-tag"),H=f("a-empty"),G=f("a-card");return b(),N(G,{title:"系统日志",bordered:!1},{default:s(()=>[v("div",ue,[v("div",de,[l(d,{value:w.value,"onUpdate:value":e[0]||(e[0]=i=>w.value=i),style:{width:"90px"},size:"small"},{default:s(()=>[l(u,{value:"all"},{default:s(()=>[...e[3]||(e[3]=[m("全部",-1)])]),_:1}),l(u,{value:"INFO"},{default:s(()=>[...e[4]||(e[4]=[m("INFO",-1)])]),_:1}),l(u,{value:"WARN"},{default:s(()=>[...e[5]||(e[5]=[m("WARN",-1)])]),_:1}),l(u,{value:"ERRO"},{default:s(()=>[...e[6]||(e[6]=[m("ERROR",-1)])]),_:1}),l(u,{value:"DBUG"},{default:s(()=>[...e[7]||(e[7]=[m("DEBUG",-1)])]),_:1})]),_:1},8,["value"]),l(V,{size:4},{default:s(()=>[l(B,{title:h.value?"关闭自动刷新":"开启自动刷新"},{default:s(()=>[l(O,{size:"small",type:h.value?"primary":"default",onClick:e[1]||(e[1]=i=>T(!h.value))},{icon:s(()=>[l(C(k))]),_:1},8,["type"])]),_:1},8,["title"]),l(B,{title:"导出日志"},{default:s(()=>[l(O,{size:"small",onClick:M},{icon:s(()=>[l(C(Z))]),_:1})]),_:1}),l(B,{title:"清除日志"},{default:s(()=>[l(O,{size:"small",danger:"",onClick:F},{icon:s(()=>[l(C(K))]),_:1})]),_:1})]),_:1})]),v("div",fe,[l(W,{value:y.value,"onUpdate:value":e[2]||(e[2]=i=>y.value=i),placeholder:"搜索日志",size:"small","enter-button":"","allow-clear":"",style:{width:"100%"}},null,8,["value"])])]),v("div",ve,[m(" 共 "+_(c.value)+" 条日志,当前显示 "+_(x.value.length)+" 条 ",1),h.value?(b(),L("span",me,[l(C(k),{spin:!0}),e[8]||(e[8]=m(" 自动刷新中 ",-1))])):S("",!0)]),v("div",pe,[(b(!0),L(te,null,ee(x.value,i=>(b(),L("div",{key:i.id,class:le(["log-line","level-"+i.level.toLowerCase()])},[v("span",ge,_(i.time),1),l($,{color:U[i.level]?.color||"#8c8c8c",size:"small",style:{margin:"0 8px"}},{default:s(()=>[m(_(i.level),1)]),_:2},1032,["color"]),v("span",_e,"["+_(i.module)+"]",1),v("span",he,_(i.message),1)],2))),128)),x.value.length===0?(b(),N(H,{key:0,description:"暂无日志"})):S("",!0)])]),_:1})}}},ye=q(Oe,[["__scopeId","data-v-a8b1f18a"]]);export{ye as default};
import{c as l,I as P,_ as q,j as J,r as p,l as Q,o as X,a as Y,b as N,d as b,w as o,g as v,f,h as m,u as C,R as k,y as Z,D as K,i as L,e as S,t as _,m as ee,F as te,s as R,M as ae,z as le,A as ne,B as oe}from"./index-6uqbLXPN.js";var se={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M304 280h56c4.4 0 8-3.6 8-8 0-28.3 5.9-53.2 17.1-73.5 10.6-19.4 26-34.8 45.4-45.4C450.9 142 475.7 136 504 136h16c28.3 0 53.2 5.9 73.5 17.1 19.4 10.6 34.8 26 45.4 45.4C650 218.9 656 243.7 656 272c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-40-8.8-76.7-25.9-108.1a184.31 184.31 0 00-74-74C596.7 72.8 560 64 520 64h-16c-40 0-76.7 8.8-108.1 25.9a184.31 184.31 0 00-74 74C304.8 195.3 296 232 296 272c0 4.4 3.6 8 8 8z"}},{tag:"path",attrs:{d:"M940 512H792V412c76.8 0 139-62.2 139-139 0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8a63 63 0 01-63 63H232a63 63 0 01-63-63c0-4.4-3.6-8-8-8h-60c-4.4 0-8 3.6-8 8 0 76.8 62.2 139 139 139v100H84c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h148v96c0 6.5.2 13 .7 19.3C164.1 728.6 116 796.7 116 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-44.2 23.9-82.9 59.6-103.7a273 273 0 0022.7 49c24.3 41.5 59 76.2 100.5 100.5S460.5 960 512 960s99.8-13.9 141.3-38.2a281.38 281.38 0 00123.2-149.5A120 120 0 01836 876c0 4.4 3.6 8 8 8h56c4.4 0 8-3.6 8-8 0-79.3-48.1-147.4-116.7-176.7.4-6.4.7-12.8.7-19.3v-96h148c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM716 680c0 36.8-9.7 72-27.8 102.9-17.7 30.3-43 55.6-73.3 73.3C584 874.3 548.8 884 512 884s-72-9.7-102.9-27.8c-30.3-17.7-55.6-43-73.3-73.3A202.75 202.75 0 01308 680V412h408v268z"}}]},name:"bug",theme:"outlined"};function D(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){re(n,c,a[c])})}return n}function re(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var j=function(t,a){var s=D({},t,a.attrs);return l(P,D({},s,{icon:se}),null)};j.displayName="BugOutlined";j.inheritAttrs=!1;var ce={icon:{tag:"svg",attrs:{viewBox:"64 64 896 896",focusable:"false"},children:[{tag:"path",attrs:{d:"M464 720a48 48 0 1096 0 48 48 0 10-96 0zm16-304v184c0 4.4 3.6 8 8 8h48c4.4 0 8-3.6 8-8V416c0-4.4-3.6-8-8-8h-48c-4.4 0-8 3.6-8 8zm475.7 440l-416-720c-6.2-10.7-16.9-16-27.7-16s-21.6 5.3-27.7 16l-416 720C56 877.4 71.4 904 96 904h832c24.6 0 40-26.6 27.7-48zm-783.5-27.9L512 239.9l339.8 588.2H172.2z"}}]},name:"warning",theme:"outlined"};function E(n){for(var t=1;t<arguments.length;t++){var a=arguments[t]!=null?Object(arguments[t]):{},s=Object.keys(a);typeof Object.getOwnPropertySymbols=="function"&&(s=s.concat(Object.getOwnPropertySymbols(a).filter(function(c){return Object.getOwnPropertyDescriptor(a,c).enumerable}))),s.forEach(function(c){ie(n,c,a[c])})}return n}function ie(n,t,a){return t in n?Object.defineProperty(n,t,{value:a,enumerable:!0,configurable:!0,writable:!0}):n[t]=a,n}var I=function(t,a){var s=E({},t,a.attrs);return l(P,E({},s,{icon:ce}),null)};I.displayName="WarningOutlined";I.inheritAttrs=!1;const ue={class:"toolbar"},de={class:"toolbar-row"},fe={class:"toolbar-row"},ve={style:{"margin-bottom":"12px",color:"#8c8c8c","font-size":"12px"}},me={key:0,style:{color:"#1890ff","margin-left":"8px"}},pe={class:"log-container"},ge={class:"log-time"},_e={class:"log-module"},he={class:"log-message"},Oe={__name:"logs",setup(n){const t=J(),a=p([]),s=p(!1),c=p(0),h=p(!1),g=p(null),y=p(""),w=p("all"),U={INFO:{color:"#1890ff",icon:oe},WARN:{color:"#faad14",icon:I},ERRO:{color:"#ff4d4f",icon:ne},DBUG:{color:"#722ed1",icon:j}},z=async()=>{s.value=!0;try{const r=await fetch("/admin/logs?lines=500",{headers:t.getHeaders()});if(r.ok){const e=await r.json();a.value=A(e.logs||[]),c.value=e.total||0}}catch{R.error("获取日志失败")}finally{s.value=!1}},A=r=>r.map((e,i)=>{const d=e.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);return d?{id:i,time:d[1],level:d[2],module:d[3],message:d[4],raw:e}:{id:i,raw:e,level:"INFO",time:"",module:"",message:e}}),x=Q(()=>a.value.filter(e=>{if(w.value!=="all"&&e.level!==w.value)return!1;if(y.value){const i=y.value.toLowerCase();return e.raw.toLowerCase().includes(i)}return!0}).reverse()),F=()=>{ae.confirm({title:"确认清除日志",content:"此操作将删除所有系统日志文件,是否继续?",okText:"确认清除",okType:"danger",cancelText:"取消",async onOk(){try{(await fetch("/admin/logs",{method:"DELETE",headers:t.getHeaders()})).ok?(R.success("日志已清除"),a.value=[],c.value=0):R.error("清除失败")}catch{R.error("请求失败")}}})},M=()=>{const r=a.value.map(O=>O.raw).join(`
`),e=new Blob([r],{type:"text/plain"}),i=URL.createObjectURL(e),d=document.createElement("a");d.href=i,d.download=`system-${new Date().toISOString().split("T")[0]}.log`,d.click(),URL.revokeObjectURL(i)},T=r=>{h.value=r,r?(z(),g.value=setInterval(z,5e3)):g.value&&(clearInterval(g.value),g.value=null)};return X(()=>{z()}),Y(()=>{g.value&&clearInterval(g.value)}),(r,e)=>{const i=f("a-select-option"),d=f("a-select"),O=f("a-button"),B=f("a-tooltip"),V=f("a-space"),W=f("a-input-search"),$=f("a-tag"),H=f("a-empty"),G=f("a-card");return b(),N(G,{title:"系统日志",bordered:!1},{default:o(()=>[v("div",ue,[v("div",de,[l(d,{value:w.value,"onUpdate:value":e[0]||(e[0]=u=>w.value=u),style:{width:"90px"},size:"small"},{default:o(()=>[l(i,{value:"all"},{default:o(()=>[...e[3]||(e[3]=[m("全部",-1)])]),_:1}),l(i,{value:"INFO"},{default:o(()=>[...e[4]||(e[4]=[m("INFO",-1)])]),_:1}),l(i,{value:"WARN"},{default:o(()=>[...e[5]||(e[5]=[m("WARN",-1)])]),_:1}),l(i,{value:"ERRO"},{default:o(()=>[...e[6]||(e[6]=[m("ERROR",-1)])]),_:1}),l(i,{value:"DBUG"},{default:o(()=>[...e[7]||(e[7]=[m("DEBUG",-1)])]),_:1})]),_:1},8,["value"]),l(V,{size:4},{default:o(()=>[l(B,{title:h.value?"关闭自动刷新":"开启自动刷新"},{default:o(()=>[l(O,{size:"small",type:h.value?"primary":"default",onClick:e[1]||(e[1]=u=>T(!h.value))},{icon:o(()=>[l(C(k))]),_:1},8,["type"])]),_:1},8,["title"]),l(B,{title:"导出日志"},{default:o(()=>[l(O,{size:"small",onClick:M},{icon:o(()=>[l(C(Z))]),_:1})]),_:1}),l(B,{title:"清除日志"},{default:o(()=>[l(O,{size:"small",danger:"",onClick:F},{icon:o(()=>[l(C(K))]),_:1})]),_:1})]),_:1})]),v("div",fe,[l(W,{value:y.value,"onUpdate:value":e[2]||(e[2]=u=>y.value=u),placeholder:"搜索日志",size:"small","enter-button":"","allow-clear":"",style:{width:"100%"}},null,8,["value"])])]),v("div",ve,[m(" 共 "+_(c.value)+" 条日志,当前显示 "+_(x.value.length)+" 条 ",1),h.value?(b(),L("span",me,[l(C(k),{spin:!0}),e[8]||(e[8]=m(" 自动刷新中 ",-1))])):S("",!0)]),v("div",pe,[(b(!0),L(te,null,ee(x.value,u=>(b(),L("div",{key:u.id,class:le(["log-line","level-"+u.level.toLowerCase()])},[v("span",ge,_(u.time),1),l($,{color:U[u.level]?.color||"#8c8c8c",size:"small",style:{margin:"0 8px"}},{default:o(()=>[m(_(u.level),1)]),_:2},1032,["color"]),v("span",_e,"["+_(u.module)+"]",1),v("span",he,_(u.message),1)],2))),128)),x.value.length===0?(b(),N(H,{key:0,description:"暂无日志"})):S("",!0)])]),_:1})}}},ye=q(Oe,[["__scopeId","data-v-6c3b8e99"]]);export{ye as default};
-1
View File
@@ -1 +0,0 @@
.log-container[data-v-a8b1f18a]{max-height:600px;overflow-y:auto;font-family:Consolas,Monaco,monospace;font-size:12px;background:#fafafa;border-radius:4px;padding:12px}.log-line[data-v-a8b1f18a]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-a8b1f18a]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-a8b1f18a]{color:#8c8c8c}.log-module[data-v-a8b1f18a]{color:#1890ff;margin-right:8px}.log-message[data-v-a8b1f18a]{color:#333}.level-erro .log-message[data-v-a8b1f18a]{color:#ff4d4f}.level-warn .log-message[data-v-a8b1f18a]{color:#faad14}.level-dbug .log-message[data-v-a8b1f18a]{color:#722ed1}.toolbar[data-v-a8b1f18a]{margin-bottom:16px}.toolbar-row[data-v-a8b1f18a]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-a8b1f18a]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-a8b1f18a]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-a8b1f18a]{margin-bottom:0}.toolbar-row[data-v-a8b1f18a]:last-child{flex:1;max-width:300px}}
+1
View File
@@ -0,0 +1 @@
.log-container[data-v-6c3b8e99]{max-height:600px;overflow-y:auto;font-family:Consolas,Monaco,monospace;font-size:12px;background:#fafafa;border-radius:4px;padding:12px}.log-line[data-v-6c3b8e99]{padding:4px 0;border-bottom:1px solid #f0f0f0;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.log-line[data-v-6c3b8e99]:hover{background:#e6f7ff;white-space:normal;word-break:break-all}.log-time[data-v-6c3b8e99]{color:#8c8c8c}.log-module[data-v-6c3b8e99]{color:#1890ff;margin-right:8px}.log-message[data-v-6c3b8e99]{color:#333}.level-erro .log-message[data-v-6c3b8e99]{color:#ff4d4f}.level-warn .log-message[data-v-6c3b8e99]{color:#faad14}.level-dbug .log-message[data-v-6c3b8e99]{color:#722ed1}.toolbar[data-v-6c3b8e99]{margin-bottom:16px}.toolbar-row[data-v-6c3b8e99]{display:flex;justify-content:space-between;align-items:center;gap:8px;margin-bottom:8px}.toolbar-row[data-v-6c3b8e99]:last-child{margin-bottom:0}@media(min-width:768px){.toolbar[data-v-6c3b8e99]{display:flex;justify-content:space-between;align-items:center;gap:12px}.toolbar-row[data-v-6c3b8e99]{margin-bottom:0}.toolbar-row[data-v-6c3b8e99]:last-child{flex:1;max-width:300px}}
@@ -1 +1 @@
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-SQeV-w6F.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},j={style:{"margin-bottom":"8px"}},M={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,g as e,f as a,h as i}from"./index-6uqbLXPN.js";const B={style:{"margin-bottom":"8px"}},T={style:{"margin-bottom":"8px"}},z={style:{"margin-bottom":"8px"}},U={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},j={style:{"margin-bottom":"8px"}},M={style:{"margin-bottom":"8px"}},q={style:{display:"flex","justify-content":"flex-end","margin-top":"24px"}},L={__name:"server",setup(N){const d=w(),s=k({port:5173,authToken:"",keepaliveMode:"comment",queueBuffer:2,imageLimit:5});c(async()=>{await d.fetchServerConfig(),Object.assign(s,d.serverConfig)});const m=async()=>{await d.saveServerConfig(s)};return(V,t)=>{const p=a("a-input-number"),r=a("a-col"),y=a("a-input-password"),u=a("a-select-option"),g=a("a-select"),f=a("a-row"),v=a("a-button"),x=a("a-card"),_=a("a-layout");return S(),C(_,{style:{background:"transparent"}},{default:n(()=>[o(x,{title:"服务器设置",bordered:!1,style:{width:"100%"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",B,[t[5]||(t[5]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"监听端口",-1)),t[6]||(t[6]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 设置服务器监听的端口号,默认为 5173 ",-1)),o(p,{value:s.port,"onUpdate:value":t[0]||(t[0]=l=>s.port=l),min:1,max:65535,placeholder:"请输入端口号",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",T,[t[7]||(t[7]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"鉴权 Token",-1)),t[8]||(t[8]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 用于 API 请求鉴权的密钥,留空则不启用鉴权 ",-1)),o(y,{value:s.authToken,"onUpdate:value":t[1]||(t[1]=l=>s.authToken=l),placeholder:"请输入 Token",type:"password"},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",z,[t[11]||(t[11]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"心跳包类型",-1)),t[12]||(t[12]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}}," 选择 SSE 流式响应的心跳包格式 ",-1)),o(g,{value:s.keepaliveMode,"onUpdate:value":t[2]||(t[2]=l=>s.keepaliveMode=l),style:{width:"100%"},placeholder:"请选择心跳包类型"},{default:n(()=>[o(u,{value:"comment"},{default:n(()=>[...t[9]||(t[9]=[i("Comment - 注释格式",-1)])]),_:1}),o(u,{value:"content"},{default:n(()=>[...t[10]||(t[10]=[i("Content - 内容格式",-1)])]),_:1})]),_:1},8,["value"])])]),_:1})]),_:1}),e("div",U,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[13]||(t[13]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1}),o(x,{title:"队列设置",bordered:!1,style:{width:"100%","margin-top":"10px"}},{default:n(()=>[o(f,{gutter:[16,16]},{default:n(()=>[o(r,{xs:24,md:12},{default:n(()=>[e("div",j,[t[14]||(t[14]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"队列缓冲区大小",-1)),t[15]||(t[15]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 非流式请求的额外排队数(设为 0 则不限制非流式请求数量)"),e("br"),i(" 实际队列上限 = Workers数量 + 缓冲区大小 ")],-1)),o(p,{value:s.queueBuffer,"onUpdate:value":t[3]||(t[3]=l=>s.queueBuffer=l),min:0,max:100,placeholder:"默认为 2",style:{width:"100%"}},null,8,["value"])])]),_:1}),o(r,{xs:24,md:12},{default:n(()=>[e("div",M,[t[16]||(t[16]=e("div",{style:{"font-weight":"600","margin-bottom":"4px"}},"图片数量上限",-1)),t[17]||(t[17]=e("div",{style:{"font-size":"12px",color:"#8c8c8c","margin-bottom":"8px"}},[i(" 单次请求最多支持的图片附件数量"),e("br"),i(" 网页最多支持10个附件,超出会被丢弃 ")],-1)),o(p,{value:s.imageLimit,"onUpdate:value":t[4]||(t[4]=l=>s.imageLimit=l),min:1,max:10,placeholder:"默认为 5",style:{width:"100%"}},null,8,["value"])])]),_:1})]),_:1}),e("div",q,[o(v,{type:"primary",onClick:m},{default:n(()=>[...t[18]||(t[18]=[i(" 保存设置 ",-1)])]),_:1})])]),_:1})]),_:1})}}},A=b(L,[["__scopeId","data-v-bd32923f"]]);export{A as default};
@@ -1 +1 @@
import{x as i,j as a,s as r}from"./index-SQeV-w6F.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
import{x as i,j as a,s as r}from"./index-6uqbLXPN.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},safeMode:{enabled:!1,reason:null},stats:{totalRequests:0,successRate:0,activeWorkers:0,totalWorkers:0,avgResponseTime:0}}),actions:{async fetchStatus(){const t=a();try{const e=await fetch("/admin/status",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.$patch(s)}}catch(e){console.error("Failed to fetch system status:",e)}},async fetchStats(){const t=a();try{const e=await fetch("/admin/stats",{headers:t.getHeaders()});if(e.ok){const s=await e.json();this.stats=s}}catch(e){console.error("Failed to fetch stats:",e)}},async restartService(t={}){const e=a(),{loginMode:s,workerName:n}=t;try{const o=await(await fetch("/admin/restart",{method:"POST",headers:{...e.getHeaders(),"Content-Type":"application/json"},body:JSON.stringify({loginMode:s,workerName:n})})).json();return o.success?(r.success(o.message||"服务重启中..."),!0):(r.error("重启失败"),!1)}catch{return r.error("重启请求失败"),!1}},async stopService(){const t=a();try{const s=await(await fetch("/admin/stop",{method:"POST",headers:t.getHeaders()})).json();return s.success?(r.success(s.message||"服务停止中..."),!0):(r.error("停止失败"),!1)}catch{return r.error("停止请求失败"),!1}}}});export{u};
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -6,7 +6,7 @@
<link rel="icon" type="image/png" href="/favicon.png">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>WebAI2API</title>
<script type="module" crossorigin src="/assets/index-SQeV-w6F.js"></script>
<script type="module" crossorigin src="/assets/index-6uqbLXPN.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-BVr8U7Bl.css">
</head>
+1 -1
View File
@@ -64,7 +64,7 @@ const handleLogin = async () => {
</div>
<a-form layout="vertical">
<a-form-item label="Access Token">
<a-form-item label="API Token">
<a-input-password v-model:value="token" placeholder="请输入 API Token" size="large"
@pressEnter="handleLogin">
<template #prefix>
+4 -2
View File
@@ -69,9 +69,9 @@ const parseLogs = (lines) => {
});
};
// 过滤后的日志
// 过滤后的日志(最新的在最上面)
const filteredLogs = computed(() => {
return logs.value.filter(log => {
const filtered = logs.value.filter(log => {
// 级别过滤
if (levelFilter.value !== 'all' && log.level !== levelFilter.value) {
return false;
@@ -83,6 +83,8 @@ const filteredLogs = computed(() => {
}
return true;
});
// 反转数组,最新的日志显示在最上面
return filtered.reverse();
});
// 清除日志