feat: 完善 WebUI 功能

This commit is contained in:
foxhui
2025-12-20 18:35:53 +08:00
Unverified
parent c8c7aec0e1
commit 10c96e420f
32 changed files with 1395 additions and 507 deletions
+18
View File
@@ -5,12 +5,30 @@ 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.2.1] - 2025-12-20
### ✨ Added
- **WebUI**
- 完善 WebUI 功能,添加接口测试和日志查看器,优化部分布局
- **日志记录**
- 会在 data/temp 文件夹下记录日志(最大5MB轮转)
### 🔄 Changed
- **初始化失败逻辑**
- 程序初始化失败后不会直接推出,以便利用 WebUI 修改错误的配置
- **LMArena 图片适配器**
- 支持通过配置直接返回图片URL (但其他不支持的适配器仍然会返回 Base64)
## [3.2.0] - 2025-12-19
### ✨ Added
- **WebUI**
- 为项目添加了网页版管理工具,便于修改配置文件(可能会有问题,可随时反馈)
- **增加看门狗**
- 增加看门狗机制(Supervisor),保证程序失败重载和利于利用 WebUI 完整重启程序
- 同时将 Linux 上的虚拟显示器和 VNC 服务器启动程序也迁移至看门狗机制
## [3.1.0] - 2025-12-17
### ✨ Added
+4
View File
@@ -114,6 +114,10 @@ backend:
# 入口URL
# 示例: "https://business.gemini.google/home/cid/8888a888-b6e0-88be-86e1-888cf3ee8cf4"
entryUrl: ""
# Lmarena 配置
lmarena:
# 开启后直接返回图片 URL (但其他不支持的适配器仍然会返回 Base64)
returnUrl: false
queue:
+18
View File
@@ -136,6 +136,13 @@ async function generate(context, prompt, imgPaths, modelId, meta = {}) {
// 9. 提取图片 URL
const img = extractImage(content);
if (img) {
// 检查是否配置了返回 URL
const returnUrl = config?.backend?.adapter?.lmarena?.returnUrl || false;
if (returnUrl) {
logger.info('适配器', '已获取结果,返回 URL', meta);
return { image: img };
}
logger.info('适配器', '已获取结果,正在下载图片...', meta);
const result = await downloadImage(img, context);
if (result.image) {
@@ -170,6 +177,17 @@ export const manifest = {
id: 'lmarena',
displayName: 'LMArena',
// 配置项模式
configSchema: [
{
key: 'returnUrl',
label: '返回图片 URL',
type: 'boolean',
default: false,
note: '开启后直接返回图片 URL (但其他不支持的适配器仍然会返回 Base64)'
}
],
// 入口 URL
getTargetUrl(config, workerConfig) {
return TARGET_URL;
+24 -2
View File
@@ -59,7 +59,7 @@ async function readBody(req) {
* @returns {Function} Admin 路由处理函数
*/
export function createAdminRouter(context) {
const { config, queueManager, tempDir } = context;
const { config, queueManager, tempDir, getSafeMode } = context;
/**
* Admin 路由处理函数
@@ -76,7 +76,8 @@ export function createAdminRouter(context) {
// GET /admin/status - 系统状态
if (method === 'GET' && pathname === '/status') {
const status = getSystemStatus();
sendJson(res, 200, status);
const safeMode = getSafeMode?.() || { enabled: false, reason: null };
sendJson(res, 200, { ...status, safeMode });
return;
}
@@ -175,6 +176,27 @@ export function createAdminRouter(context) {
return;
}
// GET /admin/logs - 读取系统日志
if (method === 'GET' && pathname === '/logs') {
const url = new URL(req.url, `http://${req.headers.host}`);
const lines = parseInt(url.searchParams.get('lines') || '200', 10);
const result = logger.readLogs(lines);
sendJson(res, 200, result);
return;
}
// DELETE /admin/logs - 清除系统日志
if (method === 'DELETE' && pathname === '/logs') {
const success = logger.clearLogs();
if (success) {
logger.info('管理器', '系统日志已清除');
sendJson(res, 200, { success: true, message: '日志已清除' });
} else {
sendJson(res, 500, { success: false, message: '日志清除失败' });
}
return;
}
// GET /admin/data-folders - 列出数据文件夹
if (method === 'GET' && pathname === '/data-folders') {
const workers = config.backend?.pool?.workers || [];
+14 -2
View File
@@ -36,14 +36,14 @@ const WEBUI_DIR = path.join(process.cwd(), 'webui', 'dist');
* @returns {Function} 请求处理函数
*/
export function createGlobalRouter(context) {
const { authToken, config, queueManager, tempDir, loginMode } = context;
const { authToken, config, queueManager, tempDir, loginMode, getSafeMode } = context;
// 创建鉴权中间件
const checkAuth = createAuthMiddleware(authToken);
// 创建子路由处理器
const handleOpenAIRequest = loginMode ? null : createOpenAIRouter(context);
const handleAdminRequest = createAdminRouter({ config, queueManager, tempDir });
const handleAdminRequest = createAdminRouter({ config, queueManager, tempDir, getSafeMode });
/**
* 主路由处理函数
@@ -100,6 +100,18 @@ export function createGlobalRouter(context) {
// OpenAI API (/v1)
if (pathname.startsWith('/v1')) {
// 安全模式下禁用 OpenAI API
const safeMode = getSafeMode?.();
if (safeMode?.enabled) {
res.writeHead(503, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({
error: {
message: `服务运行在安全模式,OpenAI API 不可用。原因: ${safeMode.reason}`,
type: 'service_unavailable'
}
}));
return;
}
// 登录模式下禁用 OpenAI API
if (!handleOpenAIRequest) {
res.writeHead(503, { 'Content-Type': 'application/json' });
+18 -4
View File
@@ -101,6 +101,16 @@ const queueManager = createQueueManager(
*/
const isLoginMode = process.argv.some(arg => arg.startsWith('-login'));
/**
* 安全模式状态
* 当 Pool 初始化失败时进入安全模式,此时:
* - HTTP 服务器正常启动
* - Admin API 和 WebUI 可用
* - OpenAI API 返回 503
*/
let safeMode = false;
let safeModeReason = null;
const handleRequest = createGlobalRouter({
authToken: AUTH_TOKEN,
backendName,
@@ -112,7 +122,8 @@ const handleRequest = createGlobalRouter({
imageLimit: IMAGE_LIMIT,
queueManager,
config,
loginMode: isLoginMode
loginMode: isLoginMode,
getSafeMode: () => ({ enabled: safeMode, reason: safeModeReason })
});
// ==================== 启动服务器 ====================
@@ -128,12 +139,15 @@ async function startServer() {
logger.info('服务器', '完成后可直接关闭浏览器窗口或按 Ctrl+C 退出');
}
// 预先启动 Pool
// 预先启动工作池(失败时进入安全模式)
try {
await queueManager.initializePool();
} catch (err) {
logger.error('服务器', 'Pool 初始化失败', { error: err.message });
process.exit(1);
logger.error('服务器', '工作池初始化失败', { error: err.message });
logger.warn('服务器', '进入安全模式:WebUI 和 Admin API 可用,OpenAI API 不可用');
logger.warn('服务器', '请通过 配置文件或者 WebUI 修改正确的配置后重启服务');
safeMode = true;
safeModeReason = err.message;
}
// 创建并启动 HTTP 服务器
+113 -1
View File
@@ -4,9 +4,12 @@
*
* - 环境变量:LOG_LEVEL=debug|info|warn|error
* - 输出格式:YYYY-MM-DD HH:mm:ss.SSS [LEVEL] [模块] 消息 | k=v ...
* - 日志文件:data/temp/system.log(超过 5MB 自动轮转)
*/
import process from 'process';
import fs from 'fs';
import path from 'path';
const LEVELS = ['debug', 'info', 'warn', 'error'];
@@ -19,6 +22,49 @@ const COLORS = {
white: '\x1b[37m'
};
// 日志文件配置
const LOG_DIR = path.join(process.cwd(), 'data', 'temp');
const LOG_FILE = path.join(LOG_DIR, 'system.log');
const LOG_FILE_OLD = path.join(LOG_DIR, 'system.log.old');
const MAX_LOG_SIZE = 5 * 1024 * 1024; // 5MB
// 确保日志目录存在
function ensureLogDir() {
if (!fs.existsSync(LOG_DIR)) {
fs.mkdirSync(LOG_DIR, { recursive: true });
}
}
// 日志轮转:超过 5MB 时重命名为 .old
function rotateLogIfNeeded() {
try {
if (fs.existsSync(LOG_FILE)) {
const stats = fs.statSync(LOG_FILE);
if (stats.size >= MAX_LOG_SIZE) {
// 删除旧的 .old 文件
if (fs.existsSync(LOG_FILE_OLD)) {
fs.unlinkSync(LOG_FILE_OLD);
}
// 重命名当前日志
fs.renameSync(LOG_FILE, LOG_FILE_OLD);
}
}
} catch (e) {
// 忽略轮转错误
}
}
// 写入日志文件
function writeToFile(line) {
try {
ensureLogDir();
rotateLogIfNeeded();
fs.appendFileSync(LOG_FILE, line + '\n', 'utf8');
} catch (e) {
// 忽略写入错误
}
}
// 根据日志级别获取颜色
function getColor(level) {
switch (level.toLowerCase()) {
@@ -94,6 +140,7 @@ export function log(level, mod, msg, meta = {}) {
const color = getColor(level);
const coloredLine = `${color}${line}${COLORS.reset}`;
// 输出到控制台
if (level === 'error') {
console.error(coloredLine);
} else if (level === 'warn') {
@@ -101,6 +148,66 @@ export function log(level, mod, msg, meta = {}) {
} else {
console.log(coloredLine);
}
// 写入日志文件(不带颜色)
writeToFile(line);
}
/**
* 获取日志文件路径
*/
export function getLogPath() {
return LOG_FILE;
}
/**
* 获取旧日志文件路径
*/
export function getOldLogPath() {
return LOG_FILE_OLD;
}
/**
* 清除日志文件
*/
export function clearLogs() {
try {
if (fs.existsSync(LOG_FILE)) {
fs.unlinkSync(LOG_FILE);
}
if (fs.existsSync(LOG_FILE_OLD)) {
fs.unlinkSync(LOG_FILE_OLD);
}
return true;
} catch (e) {
return false;
}
}
/**
* 读取日志文件(返回最后 N 行)
* @param {number} lines - 读取行数
* @returns {{logs: string[], total: number, file: string}}
*/
export function readLogs(lines = 200) {
const result = { logs: [], total: 0, file: LOG_FILE };
try {
if (!fs.existsSync(LOG_FILE)) {
return result;
}
const content = fs.readFileSync(LOG_FILE, 'utf8');
const allLines = content.split('\n').filter(line => line.trim());
result.total = allLines.length;
// 返回最后 N 行
result.logs = allLines.slice(-lines);
} catch (e) {
// 忽略读取错误
}
return result;
}
export const logger = {
@@ -108,5 +215,10 @@ export const logger = {
info: (mod, msg, meta) => log('info', mod, msg, meta),
warn: (mod, msg, meta) => log('warn', mod, msg, meta),
error: (mod, msg, meta) => log('error', mod, msg, meta),
setLevel: setLogLevel
setLevel: setLogLevel,
getLogPath,
getOldLogPath,
clearLogs,
readLogs
};
@@ -1 +1 @@
import{c as i,I as u}from"./index-N039yHLa.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-BGGdzQd9.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
View File
@@ -1 +0,0 @@
import{c as l,I as q,j as I,r as x,k as D,o as F,l as T,b as _,d as i,w as s,i as y,e as c,f as v,u as g,t as S,S as L,g as h,h as f,m as G,F as J}from"./index-N039yHLa.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 w(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=w({},n,a.attrs);return l(q,w({},o,{icon:Q}),null)};k.displayName="AppstoreOutlined";k.inheritAttrs=!1;const W={style:{display:"flex","align-items":"center","justify-content":"space-between"}},X={style:{display:"flex","align-items":"center"}},Y={style:{"font-weight":"600","font-size":"15px"}},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=x(!1),o=x(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(u=>delete t[u]),m.configSchema&&m.configSchema.forEach(u=>{r[u.key]!==void 0?t[u.key]=r[u.key]:t[u.key]=u.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 u=c("a-button"),b=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 i(),_($,{style:{background:"transparent"}},{default:s(()=>[l(b,{title:"适配器管理",bordered:!1},{extra:s(()=>[l(u,{type:"link",onClick:g(n).fetchAdaptersMeta},{default:s(()=>[...r[2]||(r[2]=[h("刷新列表",-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(b,{hoverable:"",onClick:d=>C(e),bodyStyle:{padding:"16px"}},{default:s(()=>[v("div",W,[v("div",X,[l(g(k),{style:{"font-size":"20px",color:"#1890ff","margin-right":"12px"}}),v("span",Y,S(e.id),1)]),l(g(L),{style:{"font-size":"16px",color:"#8c8c8c"}})])]),_:2},1032,["onClick"])]),_:2},1024)]),_:1},8,["data-source"])]),_:1}),o.value?(i(),_(N,{key:0,open:a.value,"onUpdate:open":r[1]||(r[1]=e=>a.value=e),title:`配置适配器 - ${o.value.name}`,width:"500",placement:"right"},{footer:s(()=>[v("div",te,[l(u,{style:{"margin-right":"8px"},onClick:r[0]||(r[0]=e=>a.value=!1)},{default:s(()=>[...r[3]||(r[3]=[h("取消",-1)])]),_:1}),l(u,{type:"primary",onClick:V},{default:s(()=>[...r[4]||(r[4]=[h("保存配置",-1)])]),_:1})])]),default:s(()=>[!o.value.configSchema||o.value.configSchema.length===0?(i(),f("div",Z,[l(U,{description:"该适配器没有可配置项"})])):(i(),_(E,{key:1,layout:"vertical"},{default:s(()=>[(i(!0),f(J,null,G(o.value.configSchema,e=>(i(),_(B,{key:e.key,label:e.label,required:e.required},{default:s(()=>[e.type==="string"?(i(),_(H,{key:0,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,placeholder:e.placeholder},null,8,["value","onUpdate:value","placeholder"])):y("",!0),e.type==="number"?(i(),_(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"])):y("",!0),e.type==="boolean"?(i(),f("div",K,[l(M,{checked:t[e.key],"onUpdate:checked":d=>t[e.key]=d},null,8,["checked","onUpdate:checked"])])):y("",!0),e.type==="select"?(i(),_(P,{key:3,value:t[e.key],"onUpdate:value":d=>t[e.key]=d,options:e.options},null,8,["value","onUpdate:value","options"])):y("",!0),e.note?(i(),f("div",ee,S(e.note),1)):y("",!0)]),_:2},1032,["label","required"]))),128))]),_:1}))]),_:1},8,["open","title"])):y("",!0)]),_:1})}}};export{ae as default};
+1
View File
@@ -0,0 +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-BGGdzQd9.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 +1 @@
[data-v-9531bced]::-webkit-scrollbar{width:6px;height:6px}[data-v-9531bced]::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}[data-v-9531bced]::-webkit-scrollbar-track{background:#f1f1f1}html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
[data-v-b38da084]::-webkit-scrollbar{width:6px;height:6px}[data-v-b38da084]::-webkit-scrollbar-thumb{background:#ccc;border-radius:3px}[data-v-b38da084]::-webkit-scrollbar-track{background:#f1f1f1}html,body{width:100%;height:100%}input::-ms-clear,input::-ms-reveal{display:none}*,*:before,*:after{box-sizing:border-box}html{font-family:sans-serif;line-height:1.15;-webkit-text-size-adjust:100%;-ms-text-size-adjust:100%;-ms-overflow-style:scrollbar;-webkit-tap-highlight-color:rgba(0,0,0,0)}@-ms-viewport{width:device-width}body{margin:0}[tabindex="-1"]:focus{outline:none}hr{box-sizing:content-box;height:0;overflow:visible}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:.5em;font-weight:500}p{margin-top:0;margin-bottom:1em}abbr[title],abbr[data-original-title]{-webkit-text-decoration:underline dotted;text-decoration:underline;text-decoration:underline dotted;border-bottom:0;cursor:help}address{margin-bottom:1em;font-style:normal;line-height:inherit}input[type=text],input[type=password],input[type=number],textarea{-webkit-appearance:none}ol,ul,dl{margin-top:0;margin-bottom:1em}ol ol,ul ul,ol ul,ul ol{margin-bottom:0}dt{font-weight:500}dd{margin-bottom:.5em;margin-left:0}blockquote{margin:0 0 1em}dfn{font-style:italic}b,strong{font-weight:bolder}small{font-size:80%}sub,sup{position:relative;font-size:75%;line-height:0;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}pre,code,kbd,samp{font-size:1em;font-family:SFMono-Regular,Consolas,Liberation Mono,Menlo,Courier,monospace}pre{margin-top:0;margin-bottom:1em;overflow:auto}figure{margin:0 0 1em}img{vertical-align:middle;border-style:none}a,area,button,[role=button],input:not([type=range]),label,select,summary,textarea{touch-action:manipulation}table{border-collapse:collapse}caption{padding-top:.75em;padding-bottom:.3em;text-align:left;caption-side:bottom}input,button,select,optgroup,textarea{margin:0;color:inherit;font-size:inherit;font-family:inherit;line-height:inherit}button,input{overflow:visible}button,select{text-transform:none}button,html [type=button],[type=reset],[type=submit]{-webkit-appearance:button}button::-moz-focus-inner,[type=button]::-moz-focus-inner,[type=reset]::-moz-focus-inner,[type=submit]::-moz-focus-inner{padding:0;border-style:none}input[type=radio],input[type=checkbox]{box-sizing:border-box;padding:0}input[type=date],input[type=time],input[type=datetime-local],input[type=month]{-webkit-appearance:listbox}textarea{overflow:auto;resize:vertical}fieldset{min-width:0;margin:0;padding:0;border:0}legend{display:block;width:100%;max-width:100%;margin-bottom:.5em;padding:0;color:inherit;font-size:1.5em;line-height:inherit;white-space:normal}progress{vertical-align:baseline}[type=number]::-webkit-inner-spin-button,[type=number]::-webkit-outer-spin-button{height:auto}[type=search]{outline-offset:-2px;-webkit-appearance:none}[type=search]::-webkit-search-cancel-button,[type=search]::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{font:inherit;-webkit-appearance:button}output{display:inline-block}summary{display:list-item}template{display:none}[hidden]{display:none!important}mark{padding:.2em;background-color:#feffe6}
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
View File
@@ -0,0 +1 @@
.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}}
+2
View File
@@ -0,0 +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-BGGdzQd9.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};
@@ -1 +1 @@
import{_ as b,j as w,k,o as c,b as C,d as S,w as n,c as o,f as e,e as a,g as i}from"./index-N039yHLa.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-BGGdzQd9.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
View File
@@ -1 +0,0 @@
import{x as i,j as a,s as r}from"./index-N039yHLa.js";const u=i("system",{state:()=>({status:"",version:"1.0.0",systemVersion:"",uptime:0,cpuUsage:0,memoryUsage:{total:0,used:0,free:0},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};
+1
View File
@@ -0,0 +1 @@
import{x as i,j as a,s as r}from"./index-BGGdzQd9.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
+2 -2
View File
@@ -6,8 +6,8 @@
<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-N039yHLa.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CAMDRWk5.css">
<script type="module" crossorigin src="/assets/index-BGGdzQd9.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-B8cer5ye.css">
</head>
<body>
+348 -4
View File
@@ -1,13 +1,19 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { useRouter } from 'vue-router';
import { Modal } from 'ant-design-vue';
import { Modal, message } from 'ant-design-vue';
import {
DashboardOutlined,
SettingOutlined,
ToolOutlined,
PoweroffOutlined,
GithubOutlined
GithubOutlined,
ApiOutlined,
CheckCircleOutlined,
CloseCircleOutlined,
LoadingOutlined,
InboxOutlined,
PictureOutlined
} from '@ant-design/icons-vue';
import { useSettingsStore } from '@/stores/settings';
import LoginModal from '@/components/auth/LoginModal.vue';
@@ -29,6 +35,189 @@ const enterIconLoading = () => {
}, 500);
};
// 接口测试抽屉
const apiTestDrawer = ref(false);
const apiTestResults = ref({
models: { status: 'pending', data: null, error: null },
cookies: { status: 'pending', data: null, error: null },
chat: { status: 'pending', data: null, error: null }
});
const chatTestPrompt = ref('Say hello in one word');
const chatTestModel = ref('');
const chatModelList = ref([]);
const chatImageList = ref([]);
const chatStreamMode = ref(false);
const chatStreamContent = ref('');
// 获取模型列表
const fetchModelList = async () => {
try {
const res = await fetch('/v1/models', { headers: settingsStore.getHeaders() });
if (res.ok) {
const data = await res.json();
chatModelList.value = data.data || [];
if (chatModelList.value.length > 0 && !chatTestModel.value) {
chatTestModel.value = chatModelList.value[0].id;
}
}
} catch (e) {
console.error('获取模型列表失败', e);
}
};
// 图片转 base64
const fileToBase64 = (file) => {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.readAsDataURL(file);
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
});
};
// 图片上传前检查
const beforeUpload = (file) => {
const allowedTypes = ['image/png', 'image/jpeg', 'image/gif', 'image/webp'];
if (!allowedTypes.includes(file.type)) {
message.error('仅支持 PNG, JPEG, GIF, WebP 格式');
return false;
}
if (chatImageList.value.length >= 10) {
message.error('最多上传 10 张图片');
return false;
}
return false; // 阻止自动上传,手动处理
};
// 处理图片选择
const handleImageChange = async (info) => {
const file = info.file;
if (file.status === 'removed') {
chatImageList.value = chatImageList.value.filter(f => f.uid !== file.uid);
return;
}
try {
const base64 = await fileToBase64(file.originFileObj || file);
chatImageList.value.push({
uid: file.uid,
name: file.name,
base64
});
} catch (e) {
message.error('图片读取失败');
}
};
const testApi = async (type) => {
apiTestResults.value[type].status = 'loading';
apiTestResults.value[type].error = null;
apiTestResults.value[type].data = null;
chatStreamContent.value = '';
try {
let url, options;
if (type === 'models') {
url = '/v1/models';
options = { headers: settingsStore.getHeaders() };
} else if (type === 'cookies') {
url = '/v1/cookies';
options = { headers: settingsStore.getHeaders() };
} else if (type === 'chat') {
url = '/v1/chat/completions';
// 构建消息内容
let content;
if (chatImageList.value.length > 0) {
// 多模态请求
content = [
{ type: 'text', text: chatTestPrompt.value }
];
for (const img of chatImageList.value) {
content.push({
type: 'image_url',
image_url: { url: img.base64 }
});
}
} else {
content = chatTestPrompt.value;
}
options = {
method: 'POST',
headers: { ...settingsStore.getHeaders(), 'Content-Type': 'application/json' },
body: JSON.stringify({
model: chatTestModel.value,
messages: [{ role: 'user', content }],
stream: chatStreamMode.value
})
};
// 流式请求处理
if (chatStreamMode.value) {
const res = await fetch(url, options);
if (!res.ok) {
const errData = await res.json();
throw new Error(errData.error?.message || `HTTP ${res.status}`);
}
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.startsWith('data: ')) {
const data = line.slice(6).trim();
if (data === '[DONE]') continue;
try {
const json = JSON.parse(data);
const delta = json.choices?.[0]?.delta?.content || '';
chatStreamContent.value += delta;
} catch { /* 忽略解析错误 */ }
}
}
}
apiTestResults.value[type].status = 'success';
apiTestResults.value[type].data = { content: chatStreamContent.value };
return;
}
}
const res = await fetch(url, options);
const data = await res.json();
if (res.ok) {
apiTestResults.value[type].status = 'success';
apiTestResults.value[type].data = data;
} else {
apiTestResults.value[type].status = 'error';
apiTestResults.value[type].error = data.error?.message || `HTTP ${res.status}`;
}
} catch (e) {
apiTestResults.value[type].status = 'error';
apiTestResults.value[type].error = e.message;
}
};
const openApiTestDrawer = () => {
apiTestDrawer.value = true;
// 重置状态
Object.keys(apiTestResults.value).forEach(key => {
apiTestResults.value[key] = { status: 'pending', data: null, error: null };
});
chatImageList.value = [];
// 获取模型列表
fetchModelList();
};
// 菜单 key 到路由路径的映射
const menuRoutes = {
'dash': '/',
@@ -37,7 +226,8 @@ const menuRoutes = {
'settings-browser': '/settings/browser',
'settings-adapters': '/settings/adapters',
'tools-display': '/tools/display',
'tools-cache': '/tools/cache'
'tools-cache': '/tools/cache',
'tools-logs': '/tools/logs'
};
// 处理菜单点击
@@ -134,7 +324,13 @@ onMounted(async () => {
<div class="logo" style="font-size: 1.25rem; font-weight: bold; color: #1890ff; margin-right: 24px;">
WebAI2API
</div>
<a-flex justify="end" align="center" style="flex: 1;">
<a-flex justify="end" align="center" style="flex: 1;" :gap="12">
<a-button @click="openApiTestDrawer">
<template #icon>
<ApiOutlined />
</template>
接口测试
</a-button>
<a-button danger :loading="iconLoading" @click="enterIconLoading">
<template #icon>
<PoweroffOutlined />
@@ -172,6 +368,7 @@ onMounted(async () => {
</template>
<a-menu-item key="tools-display">虚拟显示器</a-menu-item>
<a-menu-item key="tools-cache">缓存与重启</a-menu-item>
<a-menu-item key="tools-logs">日志查看器</a-menu-item>
</a-sub-menu>
</a-menu>
</a-layout-sider>
@@ -193,6 +390,153 @@ onMounted(async () => {
</a-layout>
</a-layout>
</a-layout>
<!-- 接口测试抽屉 -->
<a-drawer v-model:open="apiTestDrawer" title="接口测试" placement="right" :width="500">
<a-space direction="vertical" style="width: 100%" size="large">
<!-- Models 接口 -->
<a-card title="GET /v1/models" size="small">
<template #extra>
<a-button size="small" type="primary" @click="testApi('models')"
:loading="apiTestResults.models.status === 'loading'">
测试
</a-button>
</template>
<div v-if="apiTestResults.models.status === 'success'">
<a-tag color="success">
<CheckCircleOutlined /> 成功
</a-tag>
<div style="margin-top: 8px; font-size: 12px; color: #8c8c8c;">
返回 {{ apiTestResults.models.data?.data?.length || 0 }} 个模型
</div>
</div>
<div v-else-if="apiTestResults.models.status === 'error'">
<a-tag color="error">
<CloseCircleOutlined /> 失败
</a-tag>
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
{{ apiTestResults.models.error }}
</div>
</div>
<div v-else style="color: #8c8c8c; font-size: 12px;">点击测试按钮开始</div>
</a-card>
<!-- Cookies 接口 -->
<a-card title="GET /v1/cookies" size="small">
<template #extra>
<a-button size="small" type="primary" @click="testApi('cookies')"
:loading="apiTestResults.cookies.status === 'loading'">
测试
</a-button>
</template>
<div v-if="apiTestResults.cookies.status === 'success'">
<a-tag color="success">
<CheckCircleOutlined /> 成功
</a-tag>
<div style="margin-top: 8px; font-size: 12px; color: #8c8c8c;">
返回 {{ apiTestResults.cookies.data?.cookies?.length || 0 }} Cookie
</div>
</div>
<div v-else-if="apiTestResults.cookies.status === 'error'">
<a-tag color="error">
<CloseCircleOutlined /> 失败
</a-tag>
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
{{ apiTestResults.cookies.error }}
</div>
</div>
<div v-else style="color: #8c8c8c; font-size: 12px;">点击测试按钮开始</div>
</a-card>
<!-- Chat 接口 -->
<a-card title="POST /v1/chat/completions" size="small">
<template #extra>
<a-button size="small" type="primary" @click="testApi('chat')"
:loading="apiTestResults.chat.status === 'loading'" :disabled="!chatTestModel">
测试
</a-button>
</template>
<!-- 模型选择 -->
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">模型</div>
<a-select v-model:value="chatTestModel" style="width: 100%" size="small" placeholder="选择模型" show-search>
<a-select-option v-for="model in chatModelList" :key="model.id" :value="model.id">
{{ model.id }}
</a-select-option>
</a-select>
</div>
<!-- 提示词 -->
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">提示词</div>
<a-textarea v-model:value="chatTestPrompt" placeholder="输入提示词" :rows="2" size="small" />
</div>
<!-- 图片上传 -->
<div style="margin-bottom: 12px;">
<div style="font-size: 12px; color: #8c8c8c; margin-bottom: 4px;">
附加图片 ({{ chatImageList.length }}/10)
</div>
<a-upload-dragger :file-list="[]" :multiple="true" :before-upload="beforeUpload" @change="handleImageChange"
accept=".png,.jpg,.jpeg,.gif,.webp" :show-upload-list="false" style="padding: 8px;">
<p style="margin: 0;">
<InboxOutlined style="font-size: 24px; color: #1890ff;" />
</p>
<p style="font-size: 12px; margin: 4px 0 0 0; color: #8c8c8c;">
点击或拖拽上传图片 (PNG/JPEG/GIF/WebP)
</p>
</a-upload-dragger>
<div v-if="chatImageList.length > 0" style="margin-top: 8px; display: flex; flex-wrap: wrap; gap: 4px;">
<a-tag v-for="img in chatImageList" :key="img.uid" closable
@close="chatImageList = chatImageList.filter(i => i.uid !== img.uid)">
<PictureOutlined /> {{ img.name.slice(0, 15) }}{{ img.name.length > 15 ? '...' : '' }}
</a-tag>
</div>
</div>
<!-- 流式选项 -->
<div style="margin-bottom: 12px;">
<a-checkbox v-model:checked="chatStreamMode">流式响应</a-checkbox>
</div>
<!-- 测试结果 -->
<!-- 流式模式实时显示内容 -->
<div v-if="chatStreamMode && apiTestResults.chat.status === 'loading'"
style="background: #fafafa; padding: 12px; border-radius: 4px; font-size: 12px;">
<div style="color: #1890ff; margin-bottom: 8px;">
<LoadingOutlined /> 正在接收流式响应...
</div>
<pre style="white-space: pre-wrap; word-break: break-all; margin: 0; min-height: 50px;">{{ chatStreamContent ||
'等待内容...' }}</pre>
</div>
<div v-else-if="apiTestResults.chat.status === 'success'">
<a-tag color="success">
<CheckCircleOutlined /> 成功
</a-tag>
<div
style="margin-top: 8px; font-size: 12px; max-height: 200px; overflow-y: auto; background: #fafafa; padding: 8px; border-radius: 4px;">
<pre v-if="chatStreamMode" style="white-space: pre-wrap; word-break: break-all; margin: 0;">{{
apiTestResults.chat.data?.content || '' }}</pre>
<pre v-else style="white-space: pre-wrap; word-break: break-all; margin: 0;">{{
JSON.stringify(apiTestResults.chat.data, null, 2) }}</pre>
</div>
</div>
<div v-else-if="apiTestResults.chat.status === 'error'">
<a-tag color="error">
<CloseCircleOutlined /> 失败
</a-tag>
<div style="margin-top: 8px; font-size: 12px; color: #ff4d4f;">
{{ apiTestResults.chat.error }}
</div>
</div>
<div v-else-if="apiTestResults.chat.status === 'loading' && !chatStreamMode"
style="color: #1890ff; font-size: 12px;">
<LoadingOutlined /> 请求中可能需要较长时间...
</div>
</a-card>
</a-space>
</a-drawer>
</div>
</template>
+20
View File
@@ -95,6 +95,26 @@ onUnmounted(() => {
<template>
<a-layout style="width: 100%; background: transparent;">
<!-- 安全模式告警横幅 -->
<a-alert v-if="systemStore.safeMode?.enabled" type="error" show-icon style="margin-bottom: 16px;" closable>
<template #message>
<span style="font-weight: 600;"> 安全模式</span>
</template>
<template #description>
<div>
<p style="margin-bottom: 8px;">
服务因初始化失败进入安全模式OpenAI API 不可用
</p>
<p style="margin-bottom: 8px; color: #cf1322;">
<b>原因</b>{{ systemStore.safeMode.reason }}
</p>
<p style="margin: 0;">
请前往系统设置修改正确的配置后重启服务
</p>
</div>
</template>
</a-alert>
<!-- 响应式布局手机竖向电脑横向 -->
<a-row :gutter="[16, 16]" style="margin-bottom: 24px">
<!-- 系统信息卡片 -->
+11 -8
View File
@@ -69,13 +69,16 @@ const handleSave = async () => {
<a-list :grid="{ gutter: 16, xs: 1, sm: 2, md: 3, lg: 3, xl: 4, xxl: 4 }" :data-source="adapters">
<template #renderItem="{ item }">
<a-list-item>
<a-card hoverable @click="handleEdit(item)" :bodyStyle="{ padding: '16px' }">
<div style="display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center;">
<AppstoreOutlined style="font-size: 20px; color: #1890ff; margin-right: 12px;" />
<span style="font-weight: 600; font-size: 15px;">{{ item.id }}</span>
<a-card hoverable @click="handleEdit(item)" :bodyStyle="{ padding: '12px 16px' }">
<div style="display: flex; align-items: center; justify-content: space-between; gap: 8px;">
<div style="display: flex; align-items: center; min-width: 0; flex: 1;">
<AppstoreOutlined
style="font-size: 18px; color: #1890ff; margin-right: 8px; flex-shrink: 0;" />
<span
style="font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">{{
item.id }}</span>
</div>
<SettingOutlined style="font-size: 16px; color: #8c8c8c;" />
<SettingOutlined style="font-size: 16px; color: #8c8c8c; flex-shrink: 0;" />
</div>
</a-card>
</a-list-item>
@@ -84,8 +87,8 @@ const handleSave = async () => {
</a-card>
<!-- 配置抽屉 -->
<a-drawer v-if="currentAdapter" v-model:open="drawerVisible" :title="`配置适配器 - ${currentAdapter.name}`"
width="500" placement="right">
<a-drawer v-if="currentAdapter" v-model:open="drawerVisible" :title="`配置适配器 - ${currentAdapter.id}`" width="500"
placement="right">
<div v-if="!currentAdapter.configSchema || currentAdapter.configSchema.length === 0">
<a-empty description="该适配器没有可配置项" />
</div>
+306
View File
@@ -0,0 +1,306 @@
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useSettingsStore } from '@/stores/settings';
import {
ReloadOutlined,
DeleteOutlined,
SearchOutlined,
DownloadOutlined,
WarningOutlined,
CloseCircleOutlined,
InfoCircleOutlined,
BugOutlined
} from '@ant-design/icons-vue';
import { message, Modal } from 'ant-design-vue';
const settingsStore = useSettingsStore();
const logs = ref([]);
const loading = ref(false);
const total = ref(0);
const autoRefresh = ref(false);
const refreshInterval = ref(null);
const searchText = ref('');
const levelFilter = ref('all');
// 日志级别配置
const levelConfig = {
'INFO': { color: '#1890ff', icon: InfoCircleOutlined },
'WARN': { color: '#faad14', icon: WarningOutlined },
'ERRO': { color: '#ff4d4f', icon: CloseCircleOutlined },
'DBUG': { color: '#722ed1', icon: BugOutlined }
};
// 获取日志
const fetchLogs = async () => {
loading.value = true;
try {
const res = await fetch('/admin/logs?lines=500', {
headers: settingsStore.getHeaders()
});
if (res.ok) {
const data = await res.json();
logs.value = parseLogs(data.logs || []);
total.value = data.total || 0;
}
} catch (e) {
message.error('获取日志失败');
} finally {
loading.value = false;
}
};
// 解析日志行
const parseLogs = (lines) => {
return lines.map((line, index) => {
// 格式: 2025-12-20 17:00:00.000 [INFO] [模块] 消息
const match = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3}) \[(\w+)\] \[([^\]]+)\] (.*)$/);
if (match) {
return {
id: index,
time: match[1],
level: match[2],
module: match[3],
message: match[4],
raw: line
};
}
return { id: index, raw: line, level: 'INFO', time: '', module: '', message: line };
});
};
// 过滤后的日志
const filteredLogs = computed(() => {
return logs.value.filter(log => {
// 级别过滤
if (levelFilter.value !== 'all' && log.level !== levelFilter.value) {
return false;
}
// 搜索过滤
if (searchText.value) {
const search = searchText.value.toLowerCase();
return log.raw.toLowerCase().includes(search);
}
return true;
});
});
// 清除日志
const clearLogs = () => {
Modal.confirm({
title: '确认清除日志',
content: '此操作将删除所有系统日志文件,是否继续?',
okText: '确认清除',
okType: 'danger',
cancelText: '取消',
async onOk() {
try {
const res = await fetch('/admin/logs', {
method: 'DELETE',
headers: settingsStore.getHeaders()
});
if (res.ok) {
message.success('日志已清除');
logs.value = [];
total.value = 0;
} else {
message.error('清除失败');
}
} catch (e) {
message.error('请求失败');
}
}
});
};
// 导出日志
const exportLogs = () => {
const content = logs.value.map(l => l.raw).join('\n');
const blob = new Blob([content], { type: 'text/plain' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `system-${new Date().toISOString().split('T')[0]}.log`;
a.click();
URL.revokeObjectURL(url);
};
// 切换自动刷新
const toggleAutoRefresh = (newState) => {
autoRefresh.value = newState;
if (newState) {
fetchLogs(); // 立即刷新一次
refreshInterval.value = setInterval(fetchLogs, 5000);
} else {
if (refreshInterval.value) {
clearInterval(refreshInterval.value);
refreshInterval.value = null;
}
}
};
onMounted(() => {
fetchLogs();
});
onUnmounted(() => {
if (refreshInterval.value) {
clearInterval(refreshInterval.value);
}
});
</script>
<template>
<a-card title="系统日志" :bordered="false">
<!-- 工具栏 -->
<div class="toolbar">
<!-- 第一行级别筛选和操作按钮 -->
<div class="toolbar-row">
<a-select v-model:value="levelFilter" style="width: 90px" size="small">
<a-select-option value="all">全部</a-select-option>
<a-select-option value="INFO">INFO</a-select-option>
<a-select-option value="WARN">WARN</a-select-option>
<a-select-option value="ERRO">ERROR</a-select-option>
<a-select-option value="DBUG">DEBUG</a-select-option>
</a-select>
<a-space :size="4">
<a-tooltip :title="autoRefresh ? '关闭自动刷新' : '开启自动刷新'">
<a-button size="small" :type="autoRefresh ? 'primary' : 'default'"
@click="toggleAutoRefresh(!autoRefresh)">
<template #icon>
<ReloadOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip title="导出日志">
<a-button size="small" @click="exportLogs">
<template #icon>
<DownloadOutlined />
</template>
</a-button>
</a-tooltip>
<a-tooltip title="清除日志">
<a-button size="small" danger @click="clearLogs">
<template #icon>
<DeleteOutlined />
</template>
</a-button>
</a-tooltip>
</a-space>
</div>
<!-- 第二行搜索框 -->
<div class="toolbar-row">
<a-input-search v-model:value="searchText" placeholder="搜索日志" size="small" enter-button allow-clear
style="width: 100%;" />
</div>
</div>
<!-- 统计信息 -->
<div style="margin-bottom: 12px; color: #8c8c8c; font-size: 12px;">
{{ total }} 条日志当前显示 {{ filteredLogs.length }}
<span v-if="autoRefresh" style="color: #1890ff; margin-left: 8px;">
<ReloadOutlined :spin="true" /> 自动刷新中
</span>
</div>
<!-- 日志列表 -->
<div class="log-container">
<div v-for="log in filteredLogs" :key="log.id" class="log-line" :class="'level-' + log.level.toLowerCase()">
<span class="log-time">{{ log.time }}</span>
<a-tag :color="levelConfig[log.level]?.color || '#8c8c8c'" size="small" style="margin: 0 8px;">
{{ log.level }}
</a-tag>
<span class="log-module">[{{ log.module }}]</span>
<span class="log-message">{{ log.message }}</span>
</div>
<a-empty v-if="filteredLogs.length === 0" description="暂无日志" />
</div>
</a-card>
</template>
<style scoped>
.log-container {
max-height: 600px;
overflow-y: auto;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 12px;
background: #fafafa;
border-radius: 4px;
padding: 12px;
}
.log-line {
padding: 4px 0;
border-bottom: 1px solid #f0f0f0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.log-line:hover {
background: #e6f7ff;
white-space: normal;
word-break: break-all;
}
.log-time {
color: #8c8c8c;
}
.log-module {
color: #1890ff;
margin-right: 8px;
}
.log-message {
color: #333;
}
.level-erro .log-message {
color: #ff4d4f;
}
.level-warn .log-message {
color: #faad14;
}
.level-dbug .log-message {
color: #722ed1;
}
/* 工具栏样式 */
.toolbar {
margin-bottom: 16px;
}
.toolbar-row {
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.toolbar-row:last-child {
margin-bottom: 0;
}
/* 大屏幕:工具栏一行显示 */
@media (min-width: 768px) {
.toolbar {
display: flex;
justify-content: space-between;
align-items: center;
gap: 12px;
}
.toolbar-row {
margin-bottom: 0;
}
.toolbar-row:last-child {
flex: 1;
max-width: 300px;
}
}
</style>
+1
View File
@@ -13,6 +13,7 @@ const routes = [
{ path: '/settings/adapters', component: () => import('@/components/settings/adapters.vue') },
{ path: '/tools/display', component: () => import('@/components/tools/display.vue') },
{ path: '/tools/cache', component: () => import('@/components/tools/cache.vue') },
{ path: '/tools/logs', component: () => import('@/components/tools/logs.vue') },
];
const router = createRouter({
+6
View File
@@ -16,6 +16,12 @@ export const useSystemStore = defineStore('system', {
free: 0
},
// 安全模式状态
safeMode: {
enabled: false,
reason: null
},
// 仪表盘统计信息
stats: {
totalRequests: 0,
+4
View File
@@ -16,6 +16,10 @@ export default defineConfig({
'/admin': {
target: 'http://127.0.0.1:3000',
changeOrigin: true
},
'/v1': {
target: 'http://127.0.0.1:3000',
changeOrigin: true
}
}
}