feat: 调整数据存储模式为SQLite,优化代码结构

This commit is contained in:
dqzboy
2025-07-11 15:41:29 +08:00
parent 8bdbf6a48c
commit daadff788a
45 changed files with 4471 additions and 2230 deletions

View File

@@ -8,4 +8,4 @@ RUN npm install
# 暴露应用程序的端口 # 暴露应用程序的端口
EXPOSE 3000 EXPOSE 3000
# 运行应用程序 # 运行应用程序
CMD ["node", "server.js"] CMD ["npm", "start"]

View File

@@ -150,7 +150,7 @@ proxy:
``` ```
#### 13、解决国内服务器上hubcmdui无法使用http代理请求 #### 13、[项目已实现]解决国内服务器上hubcmdui无法使用http代理请求
简单的讲,需要解决两个问题: 简单的讲,需要解决两个问题:
1. dns污染,请自行搭建smartdns服务 1. dns污染,请自行搭建smartdns服务
2. 修改axios.get相关代码 2. 修改axios.get相关代码

21
hubcmdui/.env.example Normal file
View File

@@ -0,0 +1,21 @@
# HubCmdUI 环境变量配置示例
# 复制此文件为 .env 并修改相应的值
# HTTP 代理配置 (用于所有出站请求)
# HTTP_PROXY=http://proxy.example.com:8080
# HTTPS_PROXY=https://proxy.example.com:8080
# NO_PROXY=localhost,127.0.0.1,.local
# 如果代理需要用户名和密码认证
# HTTP_PROXY=http://username:password@proxy.example.com:8080
# HTTPS_PROXY=https://username:password@proxy.example.com:8080
# 服务器配置
PORT=3000
# 会话密钥
SESSION_SECRET=OhTq3faqSKoxbV%NJV
# 服务器端口
PORT=3000

View File

@@ -26,22 +26,27 @@
--- ---
## 📝 源码构建运行 ## 📝 源码运行
#### 1. 克隆项目
```bash
git clone git@github.com:dqzboy/Docker-Proxy.git
```
#### 2. 安装依赖
```bash ```bash
# 克隆项目并启动
git clone git@github.com:dqzboy/Docker-Proxy.git
cd Docker-Proxy/hubcmdui cd Docker-Proxy/hubcmdui
npm install npm install
npm start
``` ```
#### 3. 启动服务 系统会自动检测并完成:
```bash - ✅ 依赖包安装(如果需要)
node server.js - ✅ SQLite数据库初始化如果需要
``` - ✅ 启动服务
### 访问系统
- **主页**: http://localhost:3000
- **管理面板**: http://localhost:3000/admin
- **默认账户**: root / admin@123
## 📦 Docker 方式运行 ## 📦 Docker 方式运行
@@ -52,7 +57,7 @@ docker pull dqzboy/hubcmd-ui:latest
#### 2. 运行 hubcmd-ui 容器 #### 2. 运行 hubcmd-ui 容器
```bash ```bash
docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v ./data/config:/app/data -v ./data/docs:/app/documentation -v ./data/user/users.json:/app/users.json -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui docker run -d -v /var/run/docker.sock:/var/run/docker.sock -v ./data:/app/data -p 30080:3000 --name hubcmdui-server dqzboy/hubcmd-ui
``` ```
- `-v` 参数解释:左边是宿主机上的 Docker socket 文件路径,右边是容器内的映射路径 - `-v` 参数解释:左边是宿主机上的 Docker socket 文件路径,右边是容器内的映射路径
@@ -71,6 +76,56 @@ docker logs -f [容器ID或名称]
--- ---
## 🌐 代理配置
支持通过环境变量配置 HTTP 代理,用于所有出站网络请求。
### 环境变量配置
```bash
# HTTP 代理配置
export HTTP_PROXY=http://proxy.example.com:8080
export HTTPS_PROXY=https://proxy.example.com:8080
export NO_PROXY=localhost,127.0.0.1,.local
# 启动应用
npm start
```
### Docker 部署代理配置
```bash
docker run -d \
-e HTTP_PROXY=http://proxy.example.com:8080 \
-e HTTPS_PROXY=https://proxy.example.com:8080 \
-e NO_PROXY=localhost,127.0.0.1,.local \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ./data:/app/data \
-p 30080:3000 \
dqzboy/hubcmd-ui
```
### Docker Compose 代理配置
```yaml
version: '3.8'
services:
hubcmdui:
image: dqzboy/hubcmd-ui
environment:
- HTTP_PROXY=http://proxy.example.com:8080
- HTTPS_PROXY=https://proxy.example.com:8080
- NO_PROXY=localhost,127.0.0.1,.local
volumes:
- /var/run/docker.sock:/var/run/docker.sock
# SQLite数据库文件
- ./data:/app/data
ports:
- "30080:3000"
```
---
## UI界面 ## UI界面
- 默认容器监听`3000`端口,映射宿主机端口`30080` - 默认容器监听`3000`端口,映射宿主机端口`30080`
@@ -141,6 +196,41 @@ docker logs -f [容器ID或名称]
--- ---
## 🚀 系统特性
### 数据存储优化
- **SQLite数据库**: 所有数据统一存储在SQLite数据库中
- **Session管理**: 使用数据库存储用户会话,自动清理过期会话
- **配置管理**: 系统配置、用户数据、文档内容统一存储
- **零文件依赖**: 不再依赖JSON文件存储简化部署和维护
### 功能特性
- 🔐 **用户认证**: 基于数据库的用户管理系统
- ⚙️ **配置管理**: 灵活的系统配置和菜单管理
- 📚 **文档系统**: 内置Markdown文档管理
- 🔍 **镜像搜索**: Docker Hub镜像搜索和代理
- 📊 **系统监控**: 实时系统状态监控
- 🎨 **响应式界面**: 现代化的Web管理界面
## 📁 项目结构
```
hubcmdui/
├── database/ # SQLite数据库相关
│ └── database.js # 数据库管理模块
├── services/ # 业务服务层
│ ├── configServiceDB.js # 配置服务
│ ├── userServiceDB.js # 用户服务
│ └── documentationServiceDB.js # 文档服务
├── routes/ # API路由
├── web/ # 前端静态文件
├── middleware/ # 中间件
└── data/ # 数据目录SQLite文件
└── app.db # SQLite数据库文件
```
---
## 🫶 赞助 ## 🫶 赞助
如果你觉得这个项目对你有帮助请给我点个Star。并且情况允许的话可以给我一点点支持总之非常感谢支持😊 如果你觉得这个项目对你有帮助请给我点个Star。并且情况允许的话可以给我一点点支持总之非常感谢支持😊

View File

@@ -20,8 +20,8 @@ const { requireLogin, sessionActivity, sanitizeRequestBody, securityHeaders } =
console.log(`服务器启动,时间戳: ${global.serverStartTime}`); console.log(`服务器启动,时间戳: ${global.serverStartTime}`);
logger.warn(`服务器启动,时间戳: ${global.serverStartTime}`); logger.warn(`服务器启动,时间戳: ${global.serverStartTime}`);
// 添加 session 文件存储模块 - 先导入session-file-store并创建对象 // 使用 SQLite 存储 session - 替代文件存储
const FileStore = require('session-file-store')(session); const SQLiteStore = require('connect-sqlite3')(session);
// 确保目录结构存在 // 确保目录结构存在
ensureDirectoriesExist().catch(err => { ensureDirectoriesExist().catch(err => {
@@ -43,7 +43,7 @@ app.use(sessionActivity);
app.use(sanitizeRequestBody); app.use(sanitizeRequestBody);
app.use(securityHeaders); app.use(securityHeaders);
// 会话配置 // 会话配置 - 使用SQLite存储
app.use(session({ app.use(session({
secret: process.env.SESSION_SECRET || 'hubcmdui-secret-key', secret: process.env.SESSION_SECRET || 'hubcmdui-secret-key',
resave: false, resave: false,
@@ -52,9 +52,10 @@ app.use(session({
secure: process.env.NODE_ENV === 'production', secure: process.env.NODE_ENV === 'production',
maxAge: 24 * 60 * 60 * 1000 // 24小时 maxAge: 24 * 60 * 60 * 1000 // 24小时
}, },
store: new FileStore({ store: new SQLiteStore({
path: path.join(__dirname, 'data', 'sessions'), db: 'app.db',
ttl: 86400 dir: path.join(__dirname, 'data'),
table: 'sessions'
}) })
})); }));
@@ -118,23 +119,67 @@ server.listen(PORT, async () => {
try { try {
// 确保目录存在 // 确保目录存在
await ensureDirectoriesExist(); await ensureDirectoriesExist();
// 启动Session清理任务
await startSessionCleanupTask();
logger.success('系统初始化完成'); logger.success('系统初始化完成');
} catch (error) { } catch (error) {
logger.error('系统初始化失败:', error); logger.error('系统初始化失败:', error);
} }
}); });
// 注册进程事件处理 // 启动定期清理过期会话的任务
process.on('SIGINT', () => { async function startSessionCleanupTask() {
const database = require('./database/database');
// 立即清理一次
try {
await database.cleanExpiredSessions();
} catch (error) {
logger.error('清理过期会话失败:', error);
}
// 每小时清理一次过期会话
setInterval(async () => {
try {
await database.cleanExpiredSessions();
} catch (error) {
logger.error('定期清理过期会话失败:', error);
}
}, 60 * 60 * 1000); // 1小时
}
// 监听进程退出事件,确保数据库连接正确关闭
process.on('SIGINT', async () => {
logger.info('收到SIGINT信号正在关闭服务器...');
const database = require('./database/database');
await database.close();
process.exit(0);
});
process.on('SIGTERM', async () => {
logger.info('收到SIGTERM信号正在关闭服务器...');
const database = require('./database/database');
await database.close();
process.exit(0);
});
// 注册进程事件处理 - 优雅关闭
process.on('SIGINT', async () => {
logger.info('接收到中断信号,正在关闭服务...'); logger.info('接收到中断信号,正在关闭服务...');
const database = require('./database/database');
await database.close();
server.close(() => { server.close(() => {
logger.info('服务器已关闭'); logger.info('服务器已关闭');
process.exit(0); process.exit(0);
}); });
}); });
process.on('SIGTERM', () => { process.on('SIGTERM', async () => {
logger.info('接收到终止信号,正在关闭服务...'); logger.info('接收到终止信号,正在关闭服务...');
const database = require('./database/database');
await database.close();
server.close(() => { server.close(() => {
logger.info('服务器已关闭'); logger.info('服务器已关闭');
process.exit(0); process.exit(0);
@@ -169,14 +214,14 @@ function registerRoutes(app) {
app.use('/api', authRouter); app.use('/api', authRouter);
logger.info('认证路由已注册'); logger.info('认证路由已注册');
// 配置路由 - 函数式注册 // 配置路由 - 使用 Express Router
const configRouter = require('./routes/config'); const configRouter = require('./routes/config');
if (typeof configRouter === 'function') { if (configRouter && typeof configRouter === 'object') {
logger.info('配置路由是一个函数,正在注册...'); logger.info('配置路由是一个 Router 对象,正在注册...');
configRouter(app); app.use('/api/config', configRouter);
logger.info('配置路由已注册'); logger.info('配置路由已注册');
} else { } else {
logger.error('配置路由不是一个函数,无法注册', typeof configRouter); logger.error('配置路由不是一个有效的 Router 对象,无法注册', typeof configRouter);
} }
logger.success('✓ 所有路由已注册'); logger.success('✓ 所有路由已注册');

View File

@@ -25,11 +25,33 @@ process.on('SIGINT', gracefulShutdown);
process.on('SIGTERM', gracefulShutdown); process.on('SIGTERM', gracefulShutdown);
// 优雅退出函数 // 优雅退出函数
function gracefulShutdown() { async function gracefulShutdown() {
logger.info('接收到退出信号,正在关闭...'); logger.info('接收到退出信号,正在关闭...');
// 这里可以添加清理代码,如关闭数据库连接等 // 这里可以添加清理代码,如关闭数据库连接等
try { try {
// 关闭HTTP代理服务
try {
const httpProxyService = require('./services/httpProxyService');
if (httpProxyService && httpProxyService.isRunning) {
logger.info('正在关闭HTTP代理服务...');
await httpProxyService.stop();
}
} catch (err) {
logger.debug('HTTP代理服务未运行跳过清理');
}
// 关闭数据库连接
try {
const database = require('./database/database');
if (database) {
logger.info('正在关闭数据库连接...');
await database.close();
}
} catch (err) {
logger.debug('数据库未连接,跳过清理');
}
// 关闭任何可能的资源 // 关闭任何可能的资源
try { try {
const docker = require('./services/dockerService').getDockerConnection(); const docker = require('./services/dockerService').getDockerConnection();

View File

@@ -202,7 +202,7 @@ module.exports = function(app) {
// 文档接口 // 文档接口
app.get('/api/documentation', async (req, res) => { app.get('/api/documentation', async (req, res) => {
try { try {
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
const documents = await docService.getPublishedDocuments(); const documents = await docService.getPublishedDocuments();
res.json(documents); res.json(documents);
} catch (error) { } catch (error) {
@@ -324,7 +324,7 @@ module.exports = function(app) {
// 获取单个文档接口 // 获取单个文档接口
app.get('/api/documentation/:id', async (req, res) => { app.get('/api/documentation/:id', async (req, res) => {
try { try {
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
const document = await docService.getDocument(req.params.id); const document = await docService.getDocument(req.params.id);
// 如果文档不是发布状态,只有已登录用户才能访问 // 如果文档不是发布状态,只有已登录用户才能访问
@@ -345,7 +345,7 @@ module.exports = function(app) {
// 文档列表接口 // 文档列表接口
app.get('/api/documentation-list', requireLogin, async (req, res) => { app.get('/api/documentation-list', requireLogin, async (req, res) => {
try { try {
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
const documents = await docService.getDocumentationList(); const documents = await docService.getDocumentationList();
res.json(documents); res.json(documents);
} catch (error) { } catch (error) {
@@ -586,28 +586,21 @@ module.exports = function(app) {
return res.status(401).json({ error: '验证码错误' }); return res.status(401).json({ error: '验证码错误' });
} }
const userService = require('./services/userService'); const userServiceDB = require('./services/userServiceDB');
const users = await userService.getUsers(); const user = await userServiceDB.validateUser(username, password);
const user = users.users.find(u => u.username === username);
if (!user) { if (!user) {
logger.warn(`User ${username} not found`); logger.warn(`User ${username} not found`);
return res.status(401).json({ error: '用户名或密码错误' }); return res.status(401).json({ error: '用户名或密码错误' });
} }
const bcrypt = require('bcrypt');
if (bcrypt.compareSync(password, user.password)) {
req.session.user = { username: user.username }; req.session.user = { username: user.username };
// 更新用户登录信息 // 更新用户登录信息
await userService.updateUserLoginInfo(username); await userServiceDB.updateUserLoginInfo(username);
logger.info(`User ${username} logged in successfully`); logger.info(`User ${username} logged in successfully`);
res.json({ success: true }); res.json({ success: true });
} else {
logger.warn(`Login failed for user: ${username}`);
res.status(401).json({ error: '用户名或密码错误' });
}
} catch (error) { } catch (error) {
logger.error('登录失败:', error); logger.error('登录失败:', error);
res.status(500).json({ error: '登录处理失败', details: error.message }); res.status(500).json({ error: '登录处理失败', details: error.message });
@@ -781,7 +774,7 @@ module.exports = function(app) {
app.get('/api/documents', requireLogin, async (req, res) => { app.get('/api/documents', requireLogin, async (req, res) => {
try { try {
logger.info('兼容层处理获取文档列表请求'); logger.info('兼容层处理获取文档列表请求');
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
const documents = await docService.getDocumentationList(); const documents = await docService.getDocumentationList();
res.json(documents); res.json(documents);
} catch (err) { } catch (err) {
@@ -794,7 +787,7 @@ module.exports = function(app) {
app.get('/api/documents/:id', async (req, res) => { app.get('/api/documents/:id', async (req, res) => {
try { try {
logger.info(`兼容层处理获取文档请求: ${req.params.id}`); logger.info(`兼容层处理获取文档请求: ${req.params.id}`);
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
const document = await docService.getDocument(req.params.id); const document = await docService.getDocument(req.params.id);
// 如果文档不是发布状态,只有已登录用户才能访问 // 如果文档不是发布状态,只有已登录用户才能访问
@@ -817,7 +810,7 @@ module.exports = function(app) {
try { try {
logger.info(`兼容层处理更新文档请求: ${req.params.id}`); logger.info(`兼容层处理更新文档请求: ${req.params.id}`);
const { title, content, published } = req.body; const { title, content, published } = req.body;
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
// 检查必需参数 // 检查必需参数
if (!title) { if (!title) {
@@ -839,7 +832,7 @@ module.exports = function(app) {
try { try {
logger.info('兼容层处理创建文档请求'); logger.info('兼容层处理创建文档请求');
const { title, content, published } = req.body; const { title, content, published } = req.body;
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
// 检查必需参数 // 检查必需参数
if (!title) { if (!title) {
@@ -861,7 +854,7 @@ module.exports = function(app) {
app.delete('/api/documents/:id', requireLogin, async (req, res) => { app.delete('/api/documents/:id', requireLogin, async (req, res) => {
try { try {
logger.info(`兼容层处理删除文档请求: ${req.params.id}`); logger.info(`兼容层处理删除文档请求: ${req.params.id}`);
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
await docService.deleteDocument(req.params.id); await docService.deleteDocument(req.params.id);
res.json({ success: true, message: '文档已删除' }); res.json({ success: true, message: '文档已删除' });
@@ -875,7 +868,7 @@ module.exports = function(app) {
app.put('/api/documentation/toggle-publish/:id', requireLogin, async (req, res) => { app.put('/api/documentation/toggle-publish/:id', requireLogin, async (req, res) => {
try { try {
logger.info(`兼容层处理切换文档发布状态请求: ${req.params.id}`); logger.info(`兼容层处理切换文档发布状态请求: ${req.params.id}`);
const docService = require('./services/documentationService'); const docService = require('./services/documentationServiceDB');
const result = await docService.toggleDocumentPublish(req.params.id); const result = await docService.toggleDocumentPublish(req.params.id);
res.json({ res.json({
@@ -924,8 +917,8 @@ module.exports = function(app) {
// 用户信息接口 // 用户信息接口
app.get('/api/user-info', requireLogin, async (req, res) => { app.get('/api/user-info', requireLogin, async (req, res) => {
try { try {
const userService = require('./services/userService'); const userServiceDB = require('./services/userServiceDB');
const userStats = await userService.getUserStats(req.session.user.username); const userStats = await userServiceDB.getUserStats(req.session.user.username);
res.json(userStats); res.json(userStats);
} catch (error) { } catch (error) {
@@ -944,8 +937,8 @@ module.exports = function(app) {
} }
try { try {
const userService = require('./services/userService'); const userServiceDB = require('./services/userServiceDB');
await userService.changePassword(username, currentPassword, newPassword); await userServiceDB.changePassword(username, currentPassword, newPassword);
res.json({ success: true, message: '密码修改成功' }); res.json({ success: true, message: '密码修改成功' });
} catch (error) { } catch (error) {
logger.error(`用户 ${username} 修改密码失败:`, error); logger.error(`用户 ${username} 修改密码失败:`, error);

View File

@@ -1 +0,0 @@
[]

View File

@@ -1,29 +0,0 @@
{
"logo": "",
"menuItems": [
{
"text": "首页",
"link": "",
"newTab": false
},
{
"text": "GitHub",
"link": "https://github.com/dqzboy/Docker-Proxy",
"newTab": true
},
{
"text": "VPS推荐",
"link": "https://dqzboy.github.io/proxyui/racknerd",
"newTab": true
}
],
"monitoringConfig": {
"notificationType": "telegram",
"webhookUrl": "",
"telegramToken": "",
"telegramChatId": "",
"monitorInterval": 60,
"isEnabled": false
},
"proxyDomain": "github.dqzboy.Docker-Proxy"
}

View File

@@ -0,0 +1,390 @@
/**
* SQLite 数据库管理模块
*/
const sqlite3 = require('sqlite3').verbose();
const path = require('path');
const fs = require('fs').promises;
const logger = require('../logger');
const bcrypt = require('bcrypt');
// 数据库文件路径
const DB_PATH = path.join(__dirname, '../data/app.db');
class Database {
constructor() {
this.db = null;
}
/**
* 初始化数据库连接
*/
async connect() {
try {
// 确保数据目录存在
const dbDir = path.dirname(DB_PATH);
await fs.mkdir(dbDir, { recursive: true });
return new Promise((resolve, reject) => {
this.db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
logger.error('数据库连接失败:', err);
reject(err);
} else {
logger.info('SQLite 数据库连接成功');
resolve();
}
});
});
} catch (error) {
logger.error('初始化数据库失败:', error);
throw error;
}
}
/**
* 创建数据表
*/
async createTables() {
const tables = [
// 用户表
`CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
username TEXT UNIQUE NOT NULL,
password TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
login_count INTEGER DEFAULT 0,
last_login DATETIME
)`,
// 配置表
`CREATE TABLE IF NOT EXISTS configs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key TEXT UNIQUE NOT NULL,
value TEXT NOT NULL,
type TEXT DEFAULT 'string',
description TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// 文档表
`CREATE TABLE IF NOT EXISTS documents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
doc_id TEXT UNIQUE NOT NULL,
title TEXT NOT NULL,
content TEXT NOT NULL,
published BOOLEAN DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// 系统日志表
`CREATE TABLE IF NOT EXISTS system_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
level TEXT NOT NULL,
message TEXT NOT NULL,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`,
// Session表 - 用于存储用户会话
`CREATE TABLE IF NOT EXISTS sessions (
sid TEXT PRIMARY KEY,
sess TEXT NOT NULL,
expire DATETIME NOT NULL
)`,
// 菜单项表 - 用于存储导航菜单配置
`CREATE TABLE IF NOT EXISTS menu_items (
id INTEGER PRIMARY KEY AUTOINCREMENT,
text TEXT NOT NULL,
link TEXT NOT NULL,
new_tab BOOLEAN DEFAULT 0,
sort_order INTEGER DEFAULT 0,
enabled BOOLEAN DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)`
];
for (const sql of tables) {
await this.run(sql);
}
logger.info('数据表创建完成');
}
/**
* 执行SQL语句
*/
async run(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.run(sql, params, function(err) {
if (err) {
logger.error('SQL执行失败:', err);
reject(err);
} else {
resolve({ id: this.lastID, changes: this.changes });
}
});
});
}
/**
* 查询单条记录
*/
async get(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.get(sql, params, (err, row) => {
if (err) {
logger.error('SQL查询失败:', err);
reject(err);
} else {
resolve(row);
}
});
});
}
/**
* 查询多条记录
*/
async all(sql, params = []) {
return new Promise((resolve, reject) => {
this.db.all(sql, params, (err, rows) => {
if (err) {
logger.error('SQL查询失败:', err);
reject(err);
} else {
resolve(rows);
}
});
});
}
/**
* 初始化默认管理员用户
*/
async createDefaultAdmin() {
try {
const adminUser = await this.get('SELECT id FROM users WHERE username = ?', ['root']);
if (!adminUser) {
const hashedPassword = await bcrypt.hash('admin@123', 10);
await this.run(
'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
['root', hashedPassword, new Date().toISOString(), 0, null]
);
logger.info('默认管理员用户创建成功: root/admin@123');
}
} catch (error) {
logger.error('创建默认管理员用户失败:', error);
}
}
/**
* 创建默认文档
*/
async createDefaultDocuments() {
try {
const docCount = await this.get('SELECT COUNT(*) as count FROM documents');
if (docCount.count === 0) {
const defaultDocs = [
{
doc_id: 'welcome',
title: '欢迎使用 Docker 镜像代理加速系统',
content: `## 系统介绍
这是一个基于 Nginx 的 Docker 镜像代理加速系统,可以帮助您加速 Docker 镜像的下载和部署。
## 主要功能
- 🚀 **镜像加速**: 提供多个 Docker 镜像仓库的代理加速
- 🔧 **配置管理**: 简单易用的 Web 管理界面
- 📊 **监控统计**: 实时监控代理服务状态
- 📖 **文档管理**: 内置文档系统,方便管理和分享
## 快速开始
1. 访问管理面板进行基础配置
2. 配置 Docker 客户端使用代理地址
3. 开始享受加速的镜像下载体验
## 更多信息
如需更多帮助,请查看项目文档或访问 GitHub 仓库。`,
published: 1
},
{
doc_id: 'docker-config',
title: 'Docker 客户端配置指南',
content: `## 配置说明
使用本代理服务需要配置 Docker 客户端的镜像仓库地址。
## Linux/macOS 配置
编辑或创建 \`/etc/docker/daemon.json\` 文件:
\`\`\`json
{
"registry-mirrors": [
"http://your-proxy-domain.com"
]
}
\`\`\`
重启 Docker 服务:
\`\`\`bash
sudo systemctl restart docker
\`\`\`
## Windows 配置
在 Docker Desktop 设置中:
1. 打开 Settings -> Docker Engine
2. 添加配置到 JSON 文件中
3. 点击 "Apply & Restart"
## 验证配置
运行以下命令验证配置是否生效:
\`\`\`bash
docker info
\`\`\`
在输出中查看 "Registry Mirrors" 部分。`,
published: 1
}
];
for (const doc of defaultDocs) {
await this.run(
'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[doc.doc_id, doc.title, doc.content, doc.published, new Date().toISOString(), new Date().toISOString()]
);
}
}
} catch (error) {
logger.error('创建默认文档失败:', error);
}
}
/**
* 检查数据库是否已经初始化
*/
async isInitialized() {
try {
// 先检查是否有用户表
const tableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='users'");
if (!tableExists) {
return false;
}
// 检查是否有初始化标记
const configTableExists = await this.get("SELECT name FROM sqlite_master WHERE type='table' AND name='configs'");
if (configTableExists) {
const initFlag = await this.get('SELECT value FROM configs WHERE key = ?', ['db_initialized']);
if (initFlag) {
return true;
}
}
// 检查是否有用户数据
const userCount = await this.get('SELECT COUNT(*) as count FROM users');
return userCount && userCount.count > 0;
} catch (error) {
// 如果查询失败,认为数据库未初始化
return false;
}
}
/**
* 标记数据库已初始化
*/
async markAsInitialized() {
try {
await this.run(
'INSERT OR REPLACE INTO configs (key, value, type, description) VALUES (?, ?, ?, ?)',
['db_initialized', 'true', 'boolean', '数据库初始化标记']
);
logger.info('数据库已标记为已初始化');
} catch (error) {
logger.error('标记数据库初始化状态失败:', error);
}
}
/**
* 关闭数据库连接
*/
async close() {
return new Promise((resolve, reject) => {
if (this.db) {
this.db.close((err) => {
if (err) {
logger.error('关闭数据库连接失败:', err);
reject(err);
} else {
logger.info('数据库连接已关闭');
resolve();
}
});
} else {
resolve();
}
});
}
/**
* 清理过期的会话
*/
async cleanExpiredSessions() {
try {
const result = await this.run(
'DELETE FROM sessions WHERE expire < ?',
[new Date().toISOString()]
);
if (result.changes > 0) {
logger.info(`清理了 ${result.changes} 个过期会话`);
}
} catch (error) {
logger.error('清理过期会话失败:', error);
}
}
/**
* 创建默认菜单项
*/
async createDefaultMenuItems() {
try {
const menuCount = await this.get('SELECT COUNT(*) as count FROM menu_items');
if (menuCount.count === 0) {
const defaultMenuItems = [
{ text: '控制台', link: '/admin', new_tab: 0, sort_order: 1 },
{ text: '镜像搜索', link: '/', new_tab: 0, sort_order: 2 },
{ text: '文档', link: '/docs', new_tab: 0, sort_order: 3 },
{ text: 'GitHub', link: 'https://github.com/dqzboy/hubcmdui', new_tab: 1, sort_order: 4 }
];
for (const item of defaultMenuItems) {
await this.run(
'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[item.text, item.link, item.new_tab, item.sort_order, 1, new Date().toISOString(), new Date().toISOString()]
);
}
}
} catch (error) {
logger.error('创建默认菜单项失败:', error);
}
}
}
// 创建数据库实例
const database = new Database();
module.exports = database;

View File

@@ -4,13 +4,15 @@ services:
container_name: hubcmd-ui container_name: hubcmd-ui
image: dqzboy/hubcmd-ui:latest image: dqzboy/hubcmd-ui:latest
restart: always restart: always
environment:
# HTTP代理配置可选
#- HTTP_PROXY=http://proxy.example.com:8080
#- HTTPS_PROXY=https://proxy.example.com:8080
#- NO_PROXY=localhost,127.0.0.1,.local
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
# 配置目录 # SQLite数据库文件
- ./data/config:/app/data - ./data:/app/data
# 文档目录
- ./data/docs:/app/documentation
# 用户数据,需提前在宿主机上创建并把项目users.json内容放入
- ./data/user/users.json:/app/users.json
ports: ports:
- 30080:3000 - 30080:3000

Binary file not shown.

View File

@@ -1,7 +0,0 @@
{
"title": "Docker 配置镜像加速",
"content": "# Docker 配置镜像加速\n\n- 修改文件 `/etc/docker/daemon.json`(如果不存在则创建)\n\n```\nsudo mkdir -p /etc/docker\nsudo vi /etc/docker/daemon.json\n{\n \"registry-mirrors\": [\"https://<代理加速地址>\"]\n}\n\nsudo systemctl daemon-reload\nsudo systemctl restart docker\n```",
"published": true,
"createdAt": "2025-04-01T21:27:21.591Z",
"updatedAt": "2025-05-10T06:21:33.539Z"
}

View File

@@ -1,7 +0,0 @@
{
"title": "Containerd 配置镜像加速",
"content": "# Containerd 配置镜像加速\n\n\n* `/etc/containerd/config.toml`,添加如下的配置:\n\n```bash\n [plugins.\"io.containerd.grpc.v1.cri\".registry]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"docker.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"k8s.gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"gcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"ghcr.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n [plugins.\"io.containerd.grpc.v1.cri\".registry.mirrors.\"quay.io\"]\n endpoint = [\"https://<代理加速地址>\"]\n```",
"published": true,
"createdAt": "2025-04-01T21:36:16.092Z",
"updatedAt": "2025-05-10T06:21:38.920Z"
}

View File

@@ -1,7 +0,0 @@
{
"title": "Podman 配置镜像加速",
"content": "# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```# Podman 配置镜像加速\n\n* 修改配置文件 `/etc/containers/registries.conf`,添加配置:\n\n```bash\nunqualified-search-registries = ['docker.io', 'k8s.gcr.io', 'gcr.io', 'ghcr.io', 'quay.io']\n\n[[registry]]\nprefix = \"docker.io\"\ninsecure = true\nlocation = \"registry-1.docker.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"k8s.gcr.io\"\ninsecure = true\nlocation = \"k8s.gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"gcr.io\"\ninsecure = true\nlocation = \"gcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"ghcr.io\"\ninsecure = true\nlocation = \"ghcr.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n\n[[registry]]\nprefix = \"quay.io\"\ninsecure = true\nlocation = \"quay.io\"\n\n[[registry.mirror]]\nlocation = \"https://<代理加速地址>\"\n```",
"published": true,
"createdAt": "2025-04-01T21:36:40.369Z",
"updatedAt": "2025-05-08T15:16:47.900Z"
}

View File

@@ -19,14 +19,12 @@ async function ensureDirectoriesExist() {
path.join(__dirname, 'logs'), path.join(__dirname, 'logs'),
// 图片目录 // 图片目录
path.join(__dirname, 'web', 'images'), path.join(__dirname, 'web', 'images'),
// 数据目录 // 数据目录SQLite数据库文件
path.join(__dirname, 'data'), path.join(__dirname, 'data'),
// 配置目录 // 配置目录
path.join(__dirname, 'config'), path.join(__dirname, 'config'),
// 临时文件目录 // 临时文件目录
path.join(__dirname, 'temp'), path.join(__dirname, 'temp'),
// session 目录
path.join(__dirname, 'data', 'sessions'),
// 文档数据目录 // 文档数据目录
path.join(__dirname, 'web', 'data', 'documentation') path.join(__dirname, 'web', 'data', 'documentation')
]; ];

View File

@@ -4,11 +4,12 @@
"description": "Docker镜像代理加速系统", "description": "Docker镜像代理加速系统",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"start": "node server.js", "start": "node scripts/auto-setup.js",
"start-only": "node server.js",
"dev": "nodemon server.js", "dev": "nodemon server.js",
"test": "jest", "test": "jest",
"init": "node scripts/init-system.js", "init": "node scripts/init-complete.js",
"setup": "npm install && node scripts/init-system.js && echo '系统安装完成,请使用 npm start 启动服务'" "setup": "npm install && node scripts/init-complete.js && echo 'SQLite系统安装完成,请使用 npm start 启动服务'"
}, },
"keywords": [ "keywords": [
"docker", "docker",
@@ -23,13 +24,15 @@
"bcrypt": "^5.0.1", "bcrypt": "^5.0.1",
"body-parser": "^1.20.0", "body-parser": "^1.20.0",
"chalk": "^4.1.2", "chalk": "^4.1.2",
"connect-sqlite3": "^0.9.16",
"cors": "^2.8.5", "cors": "^2.8.5",
"dockerode": "^3.3.4", "dockerode": "^3.3.4",
"editor.md": "^1.5.0",
"express": "^4.21.2", "express": "^4.21.2",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"node-cache": "^5.1.2", "node-cache": "^5.1.2",
"p-limit": "^4.0.0", "p-limit": "^4.0.0",
"session-file-store": "^1.5.0", "sqlite3": "^5.1.7",
"systeminformation": "^5.25.11", "systeminformation": "^5.25.11",
"validator": "^13.7.0", "validator": "^13.7.0",
"ws": "^8.8.1" "ws": "^8.8.1"

View File

@@ -1,35 +1,37 @@
/** /**
* 认证相关路由 * 认证相关路由 - 使用SQLite数据库
*/ */
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const bcrypt = require('bcrypt'); const userServiceDB = require('../services/userServiceDB');
const userService = require('../services/userService');
const logger = require('../logger'); const logger = require('../logger');
const { requireLogin } = require('../middleware/auth'); const { requireLogin } = require('../middleware/auth');
// 登录验证 // 登录验证
router.post('/login', async (req, res) => { router.post('/login', async (req, res) => {
const { username, password, captcha } = req.body; const { username, password, captcha } = req.body;
// 验证码检查
if (req.session.captcha !== parseInt(captcha)) { if (req.session.captcha !== parseInt(captcha)) {
logger.warn(`Captcha verification failed for user: ${username}`); logger.warn(`Captcha verification failed for user: ${username}`);
return res.status(401).json({ error: '验证码错误' }); return res.status(401).json({ error: '验证码错误' });
} }
try { try {
const users = await userService.getUsers(); // 使用数据库认证
const user = users.users.find(u => u.username === username); const user = await userServiceDB.validateUser(username, password);
if (!user) { if (!user) {
logger.warn(`User ${username} not found`); logger.warn(`Login failed for user: ${username}`);
return res.status(401).json({ error: '用户名或密码错误' }); return res.status(401).json({ error: '用户名或密码错误' });
} }
if (bcrypt.compareSync(req.body.password, user.password)) { // 更新登录信息
req.session.user = { username: user.username }; await userServiceDB.updateUserLoginInfo(username);
logger.info(`用户 ${username} 登录成功`);
// 更新用户登录信息 // 设置会话
await userService.updateUserLoginInfo(username); req.session.user = { username: user.username };
// 确保服务器启动时间已设置 // 确保服务器启动时间已设置
if (!global.serverStartTime) { if (!global.serverStartTime) {
@@ -37,15 +39,10 @@ router.post('/login', async (req, res) => {
logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`); logger.warn(`登录时设置服务器启动时间: ${global.serverStartTime}`);
} }
logger.info(`User ${username} logged in successfully`);
res.json({ res.json({
success: true, success: true,
serverStartTime: global.serverStartTime serverStartTime: global.serverStartTime
}); });
} else {
logger.warn(`Login failed for user: ${username}`);
res.status(401).json({ error: '用户名或密码错误' });
}
} catch (error) { } catch (error) {
logger.error('登录失败:', error); logger.error('登录失败:', error);
res.status(500).json({ error: '登录处理失败', details: error.message }); res.status(500).json({ error: '登录处理失败', details: error.message });
@@ -76,16 +73,9 @@ router.post('/change-password', requireLogin, async (req, res) => {
} }
try { try {
const { users } = await userService.getUsers(); // 使用SQLite数据库服务修改密码
const user = users.find(u => u.username === req.session.user.username); await userServiceDB.changePassword(req.session.user.username, currentPassword, newPassword);
if (user && bcrypt.compareSync(currentPassword, user.password)) {
user.password = bcrypt.hashSync(newPassword, 10);
await userService.saveUsers(users);
res.json({ success: true }); res.json({ success: true });
} else {
res.status(401).json({ error: 'Invalid current password' });
}
} catch (error) { } catch (error) {
logger.error('修改密码失败:', error); logger.error('修改密码失败:', error);
res.status(500).json({ error: '修改密码失败', details: error.message }); res.status(500).json({ error: '修改密码失败', details: error.message });
@@ -95,8 +85,7 @@ router.post('/change-password', requireLogin, async (req, res) => {
// 获取用户信息 // 获取用户信息
router.get('/user-info', requireLogin, async (req, res) => { router.get('/user-info', requireLogin, async (req, res) => {
try { try {
const userService = require('../services/userService'); const userStats = await userServiceDB.getUserStats(req.session.user.username);
const userStats = await userService.getUserStats(req.session.user.username);
res.json(userStats); res.json(userStats);
} catch (error) { } catch (error) {

View File

@@ -1,63 +1,32 @@
/** /**
* 配置路由模块 * 配置路由模块 - 使用SQLite数据库
*/ */
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger'); const logger = require('../logger');
const { requireLogin } = require('../middleware/auth'); const { requireLogin } = require('../middleware/auth');
const configService = require('../services/configService'); const configServiceDB = require('../services/configServiceDB');
// 修改配置文件路径,使用独立的配置文件
const configFilePath = path.join(__dirname, '../data/config.json');
// 默认配置
const DEFAULT_CONFIG = {
proxyDomain: 'registry-1.docker.io',
logo: '',
theme: 'light'
};
// 确保配置文件存在
async function ensureConfigFile() {
try {
// 确保目录存在
const dir = path.dirname(configFilePath);
try {
await fs.access(dir);
} catch (error) {
await fs.mkdir(dir, { recursive: true });
logger.info(`创建目录: ${dir}`);
}
// 检查文件是否存在
try {
await fs.access(configFilePath);
const data = await fs.readFile(configFilePath, 'utf8');
return JSON.parse(data);
} catch (error) {
// 文件不存在或JSON解析错误创建默认配置
await fs.writeFile(configFilePath, JSON.stringify(DEFAULT_CONFIG, null, 2));
logger.info(`创建默认配置文件: ${configFilePath}`);
return DEFAULT_CONFIG;
}
} catch (error) {
logger.error(`配置文件操作失败: ${error.message}`);
// 出错时返回默认配置以确保API不会失败
return DEFAULT_CONFIG;
}
}
// 获取配置 // 获取配置
router.get('/config', async (req, res) => { router.get('/config', async (req, res) => {
try { try {
const config = await ensureConfigFile(); const config = await configServiceDB.getConfig();
res.json(config);
// 如果配置为空,使用默认配置
if (!config || Object.keys(config).length === 0) {
const defaultConfig = configServiceDB.getDefaultConfig();
res.json(defaultConfig);
} else {
// 合并默认配置和数据库配置,数据库配置优先
const defaultConfig = configServiceDB.getDefaultConfig();
const mergedConfig = { ...defaultConfig, ...config };
res.json(mergedConfig);
}
} catch (error) { } catch (error) {
logger.error('获取配置失败:', error); logger.error('获取配置失败:', error);
// 即使失败也返回默认配置 // 只在真正出错时返回默认配置
res.json(DEFAULT_CONFIG); const defaultConfig = configServiceDB.getDefaultConfig();
res.json(defaultConfig);
} }
}); });
@@ -74,19 +43,8 @@ router.post('/config', async (req, res) => {
}); });
} }
// 读取现有配置 // 保存配置到数据库
let existingConfig; await configServiceDB.saveConfigs(newConfig);
try {
existingConfig = await ensureConfigFile();
} catch (error) {
existingConfig = DEFAULT_CONFIG;
}
// 合并配置
const mergedConfig = { ...existingConfig, ...newConfig };
// 保存到文件
await fs.writeFile(configFilePath, JSON.stringify(mergedConfig, null, 2));
res.json({ success: true, message: '配置已保存' }); res.json({ success: true, message: '配置已保存' });
} catch (error) { } catch (error) {
@@ -98,127 +56,70 @@ router.post('/config', async (req, res) => {
} }
}); });
// 获取监控配置 // 获取配置 - 兼容旧路由
router.get('/monitoring-config', async (req, res) => { router.get('/', async (req, res) => {
logger.info('收到监控配置请求');
try { try {
logger.info('读取监控配置...'); const config = await configServiceDB.getConfig();
const config = await configService.getConfig(); res.json(config);
} catch (error) {
if (!config.monitoringConfig) { logger.error('读取配置失败:', error);
logger.info('监控配置不存在,创建默认配置'); const defaultConfig = configServiceDB.getDefaultConfig();
config.monitoringConfig = { res.json(defaultConfig);
notificationType: 'wechat',
webhookUrl: '',
telegramToken: '',
telegramChatId: '',
monitorInterval: 60,
isEnabled: false
};
await configService.saveConfig(config);
} }
});
logger.info('返回监控配置'); // 保存配置 - 兼容旧路由
res.json({ router.post('/', async (req, res) => {
notificationType: config.monitoringConfig.notificationType || 'wechat', try {
webhookUrl: config.monitoringConfig.webhookUrl || '', const newConfig = req.body;
telegramToken: config.monitoringConfig.telegramToken || '',
telegramChatId: config.monitoringConfig.telegramChatId || '', if (!newConfig || typeof newConfig !== 'object') {
monitorInterval: config.monitoringConfig.monitorInterval || 60, return res.status(400).json({
isEnabled: config.monitoringConfig.isEnabled || false error: '无效的配置数据',
details: '配置必须是一个对象'
}); });
}
await configServiceDB.saveConfigs(newConfig);
res.json({ success: true, message: '配置已保存' });
} catch (error) { } catch (error) {
logger.error('获取监控配置失败:', error); logger.error('保存配置失败:', error);
res.status(500).json({ error: '获取监控配置失败', details: error.message }); res.status(500).json({
error: '保存配置失败',
details: error.message
});
} }
}); });
// 保存监控配置 // 获取菜单项配置
router.post('/monitoring-config', requireLogin, async (req, res) => { router.get('/menu-items', async (req, res) => {
try { try {
const { const menuItems = await configServiceDB.getMenuItems();
notificationType, res.json(menuItems);
webhookUrl,
telegramToken,
telegramChatId,
monitorInterval,
isEnabled
} = req.body;
// 验证必填字段
if (!notificationType) {
return res.status(400).json({ error: '通知类型不能为空' });
}
// 根据通知类型验证对应的字段
if (notificationType === 'wechat' && !webhookUrl) {
return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' });
}
if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' });
}
// 保存配置
const config = await configService.getConfig();
config.monitoringConfig = {
notificationType,
webhookUrl: webhookUrl || '',
telegramToken: telegramToken || '',
telegramChatId: telegramChatId || '',
monitorInterval: parseInt(monitorInterval) || 60,
isEnabled: !!isEnabled
};
await configService.saveConfig(config);
logger.info('监控配置已更新');
res.json({ success: true, message: '监控配置已保存' });
} catch (error) { } catch (error) {
logger.error('保存监控配置失败:', error); logger.error('获取菜单项失败:', error);
res.status(500).json({ error: '保存监控配置失败', details: error.message }); res.status(500).json({ error: '获取菜单项失败', details: error.message });
} }
}); });
// 测试通知 // 保存菜单项配置
router.post('/test-notification', requireLogin, async (req, res) => { router.post('/menu-items', requireLogin, async (req, res) => {
try { try {
const { notificationType, webhookUrl, telegramToken, telegramChatId } = req.body; const { menuItems } = req.body;
// 验证参数 if (!Array.isArray(menuItems)) {
if (!notificationType) { return res.status(400).json({
return res.status(400).json({ error: '通知类型不能为空' }); error: '无效的菜单项数据',
details: '菜单项必须是一个数组'
});
} }
if (notificationType === 'wechat' && !webhookUrl) { await configServiceDB.saveMenuItems(menuItems);
return res.status(400).json({ error: '企业微信 Webhook URL 不能为空' }); res.json({ success: true, message: '菜单项配置已保存' });
}
if (notificationType === 'telegram' && (!telegramToken || !telegramChatId)) {
return res.status(400).json({ error: 'Telegram Token 和 Chat ID 不能为空' });
}
// 构造测试消息
const testMessage = {
title: '测试通知',
content: `这是一条测试通知消息,发送时间: ${new Date().toLocaleString('zh-CN')}`,
type: 'info'
};
// 模拟发送通知
logger.info('发送测试通知:', testMessage);
// TODO: 实际发送通知的逻辑
// 这里仅做模拟,实际应用中需要实现真正的通知发送逻辑
// 返回成功
res.json({ success: true, message: '测试通知已发送' });
} catch (error) { } catch (error) {
logger.error('发送测试通知失败:', error); logger.error('保存菜单项失败:', error);
res.status(500).json({ error: '发送测试通知失败', details: error.message }); res.status(500).json({ error: '保存菜单项失败', details: error.message });
} }
}); });
// 导出路由
module.exports = router; module.exports = router;

View File

@@ -1,108 +1,16 @@
/** /**
* 文档管理路由 * 文档管理路由 - 使用SQLite数据库
*/ */
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger'); const logger = require('../logger');
const { requireLogin } = require('../middleware/auth'); const { requireLogin } = require('../middleware/auth');
const documentationServiceDB = require('../services/documentationServiceDB');
// 确保文档目录存在 // 获取所有文档列表(管理员)
const docsDir = path.join(__dirname, '../documentation'); router.get('/documents', requireLogin, async (req, res) => {
const metaDir = path.join(docsDir, 'meta');
// 文档文件扩展名
const FILE_EXTENSION = '.md';
const META_EXTENSION = '.json';
// 确保目录存在
async function ensureDirectories() {
try { try {
await fs.mkdir(docsDir, { recursive: true }); const documents = await documentationServiceDB.getDocumentationList();
await fs.mkdir(metaDir, { recursive: true });
} catch (err) {
logger.error('创建文档目录失败:', err);
}
}
// 读取文档元数据
async function getDocumentMeta(id) {
const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
try {
const metaContent = await fs.readFile(metaPath, 'utf8');
return JSON.parse(metaContent);
} catch (err) {
// 如果元数据文件不存在,返回默认值
return {
id,
published: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
}
}
// 保存文档元数据
async function saveDocumentMeta(id, metadata) {
await ensureDirectories();
const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
const metaContent = JSON.stringify(metadata, null, 2);
await fs.writeFile(metaPath, metaContent);
}
// 初始化确保目录存在,但不再创建默认文档
(async function() {
try {
await ensureDirectories();
logger.info('文档目录已初始化');
} catch (error) {
logger.error('初始化文档目录失败:', error);
}
})();
// 获取所有文档列表
router.get('/documents', async (req, res) => {
try {
let files;
try {
files = await fs.readdir(docsDir);
} catch (err) {
// 如果目录不存在,尝试创建它并返回空列表
if (err.code === 'ENOENT') {
await fs.mkdir(docsDir, { recursive: true });
files = [];
} else {
throw err;
}
}
const documents = [];
for (const file of files) {
if (file.endsWith(FILE_EXTENSION)) {
const filePath = path.join(docsDir, file);
const stats = await fs.stat(filePath);
const content = await fs.readFile(filePath, 'utf8');
const id = file.replace(FILE_EXTENSION, '');
// 读取元数据
const metadata = await getDocumentMeta(id);
// 解析文档元数据(简单实现)
const titleMatch = content.match(/^#\s+(.*)$/m);
const title = titleMatch ? titleMatch[1] : id;
documents.push({
id,
title,
content,
createdAt: metadata.createdAt || stats.birthtime,
updatedAt: metadata.updatedAt || stats.mtime,
published: metadata.published || false
});
}
}
res.json(documents); res.json(documents);
} catch (err) { } catch (err) {
logger.error('获取文档列表失败:', err); logger.error('获取文档列表失败:', err);
@@ -110,6 +18,39 @@ router.get('/documents', async (req, res) => {
} }
}); });
// 获取已发布文档列表(公开)
router.get('/published', async (req, res) => {
try {
const documents = await documentationServiceDB.getPublishedDocuments();
res.json(documents);
} catch (err) {
logger.error('获取已发布文档列表失败:', err);
res.status(500).json({ error: '获取已发布文档列表失败' });
}
});
// 获取单个文档
router.get('/documents/:id', async (req, res) => {
try {
const { id } = req.params;
const document = await documentationServiceDB.getDocument(id);
if (!document) {
return res.status(404).json({ error: '文档不存在' });
}
// 如果文档未发布,需要登录权限
if (!document.published && !req.session.user) {
return res.status(403).json({ error: '没有权限访问该文档' });
}
res.json(document);
} catch (err) {
logger.error('获取文档失败:', err);
res.status(500).json({ error: '获取文档失败' });
}
});
// 保存文档 // 保存文档
router.put('/documents/:id', requireLogin, async (req, res) => { router.put('/documents/:id', requireLogin, async (req, res) => {
try { try {
@@ -120,38 +61,11 @@ router.put('/documents/:id', requireLogin, async (req, res) => {
return res.status(400).json({ error: '标题和内容为必填项' }); return res.status(400).json({ error: '标题和内容为必填项' });
} }
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); const document = await documentationServiceDB.saveDocument(id, title, content, published);
// 确保文档目录存在
await ensureDirectories();
// 获取或创建元数据
const metadata = await getDocumentMeta(id);
metadata.title = title;
metadata.published = typeof published === 'boolean' ? published : metadata.published;
metadata.updatedAt = new Date().toISOString();
// 保存文档内容
await fs.writeFile(filePath, content);
// 保存元数据
await saveDocumentMeta(id, metadata);
// 获取文件状态
const stats = await fs.stat(filePath);
const document = {
id,
title,
content,
createdAt: metadata.createdAt,
updatedAt: new Date().toISOString(),
published: metadata.published
};
res.json(document); res.json(document);
} catch (err) { } catch (err) {
logger.error('保存文档失败:', err); logger.error('保存文档失败:', err);
res.status(500).json({ error: '保存文档失败', details: err.message }); res.status(500).json({ error: '保存文档失败' });
} }
}); });
@@ -164,42 +78,12 @@ router.post('/documents', requireLogin, async (req, res) => {
return res.status(400).json({ error: '标题和内容为必填项' }); return res.status(400).json({ error: '标题和内容为必填项' });
} }
// 生成唯一ID
const id = Date.now().toString(); const id = Date.now().toString();
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`); const document = await documentationServiceDB.saveDocument(id, title, content, published);
// 确保文档目录存在
await ensureDirectories();
// 创建元数据
const now = new Date().toISOString();
const metadata = {
id,
title,
published: typeof published === 'boolean' ? published : false,
createdAt: now,
updatedAt: now
};
// 保存文档内容
await fs.writeFile(filePath, content);
// 保存元数据
await saveDocumentMeta(id, metadata);
const document = {
id,
title,
content,
createdAt: now,
updatedAt: now,
published: metadata.published
};
res.status(201).json(document); res.status(201).json(document);
} catch (err) { } catch (err) {
logger.error('创建文档失败:', err); logger.error('创建文档失败:', err);
res.status(500).json({ error: '创建文档失败', details: err.message }); res.status(500).json({ error: '创建文档失败' });
} }
}); });
@@ -207,331 +91,30 @@ router.post('/documents', requireLogin, async (req, res) => {
router.delete('/documents/:id', requireLogin, async (req, res) => { router.delete('/documents/:id', requireLogin, async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
const metaPath = path.join(metaDir, `${id}${META_EXTENSION}`);
let success = false; const success = await documentationServiceDB.deleteDocument(id);
if (!success) {
// 尝试删除主文档文件 return res.status(404).json({ error: '文档不存在' });
try {
await fs.access(filePath);
await fs.unlink(filePath);
success = true;
logger.info(`文档 ${id} 已成功删除`);
} catch (err) {
logger.warn(`删除文档文件 ${id} 失败:`, err);
} }
// 尝试删除元数据文件 res.json({ success: true, message: '文档已删除' });
try {
await fs.access(metaPath);
await fs.unlink(metaPath);
success = true;
logger.info(`文档元数据 ${id} 已成功删除`);
} catch (err) { } catch (err) {
logger.warn(`删除文档元数据 ${id} 失败:`, err); logger.error('删除文档失败:', err);
} res.status(500).json({ error: '删除文档失败' });
if (success) {
res.json({ success: true });
} else {
throw new Error('文档和元数据均不存在或无法删除');
}
} catch (err) {
logger.error(`删除文档 ${req.params.id} 失败:`, err);
res.status(500).json({ error: '删除文档失败', details: err.message });
} }
}); });
// 获取单个文档 // 切换文档发布状态
router.get('/documents/:id', async (req, res) => { router.put('/toggle-publish/:id', requireLogin, async (req, res) => {
try { try {
const { id } = req.params; const { id } = req.params;
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
// 检查文件是否存在
try {
await fs.access(filePath);
} catch (err) {
return res.status(404).json({ error: '文档不存在' });
}
// 读取文件内容和元数据
const content = await fs.readFile(filePath, 'utf8');
const metadata = await getDocumentMeta(id);
// 解析文档标题
const titleMatch = content.match(/^#\s+(.*)$/m);
const title = titleMatch ? titleMatch[1] : metadata.title || id;
const document = {
id,
title,
content,
createdAt: metadata.createdAt,
updatedAt: metadata.updatedAt,
published: metadata.published
};
const document = await documentationServiceDB.toggleDocumentPublish(id);
res.json(document); res.json(document);
} catch (err) { } catch (err) {
logger.error(`获取文档 ${req.params.id} 失败:`, err); logger.error('切换文档发布状态失败:', err);
res.status(500).json({ error: '获取文档失败', details: err.message }); res.status(500).json({ error: '切换文档发布状态失败' });
} }
}); });
// 更新文档发布状态
router.put('/documentation/toggle-publish/:id', requireLogin, async (req, res) => {
try {
const { id } = req.params;
const { published } = req.body;
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
// 检查文件是否存在
try {
await fs.access(filePath);
} catch (err) {
return res.status(404).json({ error: '文档不存在' });
}
// 读取文件内容和元数据
const content = await fs.readFile(filePath, 'utf8');
const metadata = await getDocumentMeta(id);
// 更新元数据
metadata.published = typeof published === 'boolean' ? published : !metadata.published;
metadata.updatedAt = new Date().toISOString();
// 保存元数据
await saveDocumentMeta(id, metadata);
// 解析文档标题
const titleMatch = content.match(/^#\s+(.*)$/m);
const title = titleMatch ? titleMatch[1] : metadata.title || id;
const document = {
id,
title,
content,
createdAt: metadata.createdAt,
updatedAt: metadata.updatedAt,
published: metadata.published
};
res.json(document);
} catch (err) {
logger.error(`更新文档状态 ${req.params.id} 失败:`, err);
res.status(500).json({ error: '更新文档状态失败', details: err.message });
}
});
// 为前端添加获取已发布文档列表的路由
router.get('/documentation', async (req, res) => {
try {
let files;
try {
files = await fs.readdir(docsDir);
} catch (err) {
if (err.code === 'ENOENT') {
await fs.mkdir(docsDir, { recursive: true });
files = [];
} else {
throw err;
}
}
const documents = [];
for (const file of files) {
if (file.endsWith(FILE_EXTENSION)) {
const filePath = path.join(docsDir, file);
const stats = await fs.stat(filePath);
const id = file.replace(FILE_EXTENSION, '');
// 读取元数据
const metadata = await getDocumentMeta(id);
// 如果发布状态为true则包含在返回结果中
if (metadata.published === true) {
const content = await fs.readFile(filePath, 'utf8');
// 解析文档标题
const titleMatch = content.match(/^#\s+(.*)$/m);
const title = titleMatch ? titleMatch[1] : metadata.title || id;
documents.push({
id,
title,
createdAt: metadata.createdAt || stats.birthtime,
updatedAt: metadata.updatedAt || stats.mtime,
published: true
});
}
}
}
logger.info(`前端请求文档列表,返回 ${documents.length} 个已发布文档`);
res.json(documents);
} catch (err) {
logger.error('获取前端文档列表失败:', err);
res.status(500).json({ error: '获取文档列表失败' });
}
});
// 前端获取单个文档内容
router.get('/documentation/:id', async (req, res) => {
try {
const { id } = req.params;
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
// 检查文件是否存在
try {
await fs.access(filePath);
} catch (err) {
return res.status(404).json({ error: '文档不存在' });
}
// 读取文件内容和元数据
const content = await fs.readFile(filePath, 'utf8');
const metadata = await getDocumentMeta(id);
// 如果文档未发布,则不允许前端访问
if (metadata.published !== true) {
return res.status(403).json({ error: '该文档未发布,无法访问' });
}
// 解析文档标题
const titleMatch = content.match(/^#\s+(.*)$/m);
const title = titleMatch ? titleMatch[1] : metadata.title || id;
const document = {
id,
title,
content,
createdAt: metadata.createdAt,
updatedAt: metadata.updatedAt,
published: true
};
logger.info(`前端请求文档: ${id} - ${title}`);
res.json(document);
} catch (err) {
logger.error(`获取前端文档内容 ${req.params.id} 失败:`, err);
res.status(500).json({ error: '获取文档内容失败', details: err.message });
}
});
// 修改发布状态
router.patch('/documents/:id/publish', requireLogin, async (req, res) => {
try {
const { id } = req.params;
const { published } = req.body;
if (typeof published !== 'boolean') {
return res.status(400).json({ error: '发布状态必须为布尔值' });
}
// 获取或创建元数据
const metadata = await getDocumentMeta(id);
metadata.published = published;
metadata.updatedAt = new Date().toISOString();
// 保存元数据
await saveDocumentMeta(id, metadata);
// 获取文档内容
const filePath = path.join(docsDir, `${id}${FILE_EXTENSION}`);
let content;
try {
content = await fs.readFile(filePath, 'utf8');
} catch (err) {
if (err.code === 'ENOENT') {
return res.status(404).json({ error: '文档不存在' });
}
throw err;
}
// 解析标题
const titleMatch = content.match(/^#\s+(.*)$/m);
const title = titleMatch ? titleMatch[1] : id;
// 返回更新后的文档信息
const document = {
id,
title,
content,
createdAt: metadata.createdAt,
updatedAt: metadata.updatedAt,
published: metadata.published
};
res.json(document);
} catch (err) {
logger.error('修改发布状态失败:', err);
res.status(500).json({ error: '修改发布状态失败', details: err.message });
}
});
// 获取单个文档文件内容
router.get('/file', async (req, res) => {
try {
const filePath = req.query.path;
if (!filePath) {
return res.status(400).json({ error: '文件路径不能为空' });
}
logger.info(`请求获取文档文件: ${filePath}`);
// 尝试直接从文件系统读取文件
try {
// 安全检查确保只能访问documentation目录下的文件
const fileName = path.basename(filePath);
const fileDir = path.dirname(filePath);
// 构建完整路径只允许访问documentation目录
const fullPath = path.join(__dirname, '..', 'documentation', fileName);
logger.info(`尝试读取文件: ${fullPath}`);
// 检查文件是否存在
const fileExists = await fs.access(fullPath)
.then(() => true)
.catch(() => false);
// 如果文件不存在,则返回错误
if (!fileExists) {
logger.warn(`文件不存在: ${fullPath}`);
return res.status(404).json({ error: '文档不存在' });
}
logger.info(`文件存在,开始读取: ${fullPath}`);
// 读取文件内容
const fileContent = await fs.readFile(fullPath, 'utf8');
// 设置适当的Content-Type
if (fileName.endsWith('.md')) {
res.setHeader('Content-Type', 'text/markdown; charset=utf-8');
} else if (fileName.endsWith('.json')) {
res.setHeader('Content-Type', 'application/json; charset=utf-8');
} else {
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
}
logger.info(`成功读取文件,内容长度: ${fileContent.length}`);
return res.send(fileContent);
} catch (error) {
logger.error(`读取文件失败: ${error.message}`, error);
return res.status(500).json({ error: `读取文件失败: ${error.message}` });
}
} catch (error) {
logger.error('获取文档文件失败:', error);
res.status(500).json({ error: '获取文档文件失败', details: error.message });
}
});
// 导出路由
logger.success('✓ 文档管理路由已加载');
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,167 @@
/**
* HTTP代理管理路由
*/
const express = require('express');
const router = express.Router();
const logger = require('../logger');
const { requireLogin } = require('../middleware/auth');
const httpProxyService = require('../services/httpProxyService');
// 获取代理状态
router.get('/proxy/status', requireLogin, async (req, res) => {
try {
const status = httpProxyService.getStatus();
res.json(status);
} catch (error) {
logger.error('获取代理状态失败:', error);
res.status(500).json({
error: '获取代理状态失败',
details: error.message
});
}
});
// 启动代理服务
router.post('/proxy/start', requireLogin, async (req, res) => {
try {
const config = req.body;
await httpProxyService.start(config);
res.json({
success: true,
message: '代理服务已启动',
status: httpProxyService.getStatus()
});
} catch (error) {
logger.error('启动代理服务失败:', error);
res.status(500).json({
error: '启动代理服务失败',
details: error.message
});
}
});
// 停止代理服务
router.post('/proxy/stop', requireLogin, async (req, res) => {
try {
await httpProxyService.stop();
res.json({
success: true,
message: '代理服务已停止',
status: httpProxyService.getStatus()
});
} catch (error) {
logger.error('停止代理服务失败:', error);
res.status(500).json({
error: '停止代理服务失败',
details: error.message
});
}
});
// 重启代理服务
router.post('/proxy/restart', requireLogin, async (req, res) => {
try {
await httpProxyService.stop();
await httpProxyService.start(req.body);
res.json({
success: true,
message: '代理服务已重启',
status: httpProxyService.getStatus()
});
} catch (error) {
logger.error('重启代理服务失败:', error);
res.status(500).json({
error: '重启代理服务失败',
details: error.message
});
}
});
// 更新代理配置
router.put('/proxy/config', requireLogin, async (req, res) => {
try {
const config = req.body;
// 验证配置
if (config.port && (config.port < 1 || config.port > 65535)) {
return res.status(400).json({ error: '端口号必须在1-65535之间' });
}
if (config.enableAuth && (!config.username || !config.password)) {
return res.status(400).json({ error: '启用认证时必须提供用户名和密码' });
}
await httpProxyService.updateConfig(config);
res.json({
success: true,
message: '代理配置已更新',
status: httpProxyService.getStatus()
});
} catch (error) {
logger.error('更新代理配置失败:', error);
res.status(500).json({
error: '更新代理配置失败',
details: error.message
});
}
});
// 获取代理配置
router.get('/proxy/config', requireLogin, async (req, res) => {
try {
const status = httpProxyService.getStatus();
res.json({
success: true,
config: status.config
});
} catch (error) {
logger.error('获取代理配置失败:', error);
res.status(500).json({
error: '获取代理配置失败',
details: error.message
});
}
});
// 测试代理连接
router.post('/proxy/test', requireLogin, async (req, res) => {
try {
const { testUrl = 'http://httpbin.org/ip' } = req.body;
const axios = require('axios');
const status = httpProxyService.getStatus();
if (!status.isRunning) {
return res.status(400).json({ error: '代理服务未运行' });
}
// 通过代理测试连接
const proxyConfig = {
host: status.config.host === '0.0.0.0' ? 'localhost' : status.config.host,
port: status.config.port
};
const startTime = Date.now();
const response = await axios.get(testUrl, {
proxy: proxyConfig,
timeout: 10000
});
const responseTime = Date.now() - startTime;
res.json({
success: true,
message: '代理连接测试成功',
testUrl,
responseTime: `${responseTime}ms`,
statusCode: response.status,
proxyConfig
});
} catch (error) {
logger.error('代理连接测试失败:', error);
res.status(500).json({
error: '代理连接测试失败',
details: error.message
});
}
});
module.exports = router;

View File

@@ -1,5 +1,5 @@
/** /**
* 系统相关路由 * 系统相关路由 - 使用SQLite数据库
*/ */
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
@@ -9,7 +9,7 @@ const { exec } = require('child_process');
const execPromise = util.promisify(exec); // 只在这里定义一次 const execPromise = util.promisify(exec); // 只在这里定义一次
const logger = require('../logger'); const logger = require('../logger');
const { requireLogin } = require('../middleware/auth'); const { requireLogin } = require('../middleware/auth');
const configService = require('../services/configService'); const configServiceDB = require('../services/configServiceDB');
const { execCommand, getSystemInfo } = require('../server-utils'); const { execCommand, getSystemInfo } = require('../server-utils');
const dockerService = require('../services/dockerService'); const dockerService = require('../services/dockerService');
const path = require('path'); const path = require('path');
@@ -115,7 +115,7 @@ async function getSystemStats(req, res) {
// 获取系统配置 - 修改版本,避免与其他路由冲突 // 获取系统配置 - 修改版本,避免与其他路由冲突
router.get('/system-config', async (req, res) => { router.get('/system-config', async (req, res) => {
try { try {
const config = await configService.getConfig(); const config = await configServiceDB.getConfig();
res.json(config); res.json(config);
} catch (error) { } catch (error) {
logger.error('读取配置失败:', error); logger.error('读取配置失败:', error);
@@ -129,9 +129,9 @@ router.get('/system-config', async (req, res) => {
// 保存系统配置 - 修改版本,避免与其他路由冲突 // 保存系统配置 - 修改版本,避免与其他路由冲突
router.post('/system-config', requireLogin, async (req, res) => { router.post('/system-config', requireLogin, async (req, res) => {
try { try {
const currentConfig = await configService.getConfig(); const currentConfig = await configServiceDB.getConfig();
const newConfig = { ...currentConfig, ...req.body }; const newConfig = { ...currentConfig, ...req.body };
await configService.saveConfig(newConfig); await configServiceDB.saveConfigs(newConfig);
logger.info('系统配置已更新'); logger.info('系统配置已更新');
res.json({ success: true }); res.json({ success: true });
} catch (error) { } catch (error) {

117
hubcmdui/scripts/auto-setup.js Executable file
View File

@@ -0,0 +1,117 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const { isDatabaseReady, getDatabaseStats } = require('../utils/database-checker');
// 检查是否需要安装依赖
function needsInstall() {
const nodeModulesPath = path.join(process.cwd(), 'node_modules');
const packageLockPath = path.join(process.cwd(), 'package-lock.json');
if (!fs.existsSync(nodeModulesPath)) {
return true;
}
// 检查package.json是否比package-lock.json新
const packageJsonPath = path.join(process.cwd(), 'package.json');
if (fs.existsSync(packageJsonPath) && fs.existsSync(packageLockPath)) {
const packageStat = fs.statSync(packageJsonPath);
const lockStat = fs.statSync(packageLockPath);
if (packageStat.mtime > lockStat.mtime) {
return true;
}
}
return false;
}
// 检查是否需要初始化数据库
async function needsInit() {
const dataDir = path.join(process.cwd(), 'data');
// 如果data目录不存在需要初始化
if (!fs.existsSync(dataDir)) {
return true;
}
// 使用专门的数据库检查器
const isReady = await isDatabaseReady();
return !isReady;
}
// 执行命令并显示输出
function runCommand(command, description) {
console.log(`\n🔄 ${description}...`);
try {
execSync(command, { stdio: 'inherit', cwd: process.cwd() });
console.log(`${description}完成`);
return true;
} catch (error) {
console.error(`${description}失败:`, error.message);
return false;
}
}
async function autoSetup() {
console.log('🚀 HubCmdUI 自动设置检查...\n');
let needsSetup = false;
// 检查是否需要安装依赖
if (needsInstall()) {
console.log('📦 检测到需要安装依赖包');
needsSetup = true;
if (!runCommand('npm install', '安装依赖包')) {
process.exit(1);
}
} else {
console.log('✅ 依赖包已安装');
}
// 检查是否需要初始化
const needsInitialization = await needsInit();
if (needsInitialization) {
console.log('🗄️ 检测到需要初始化数据库');
needsSetup = true;
if (!runCommand('node scripts/init-complete.js', '初始化SQLite数据库')) {
process.exit(1);
}
} else {
console.log('✅ 数据库已初始化');
}
if (needsSetup) {
console.log('\n🎉 系统设置完成!正在启动服务...\n');
} else {
console.log('\n🎯 系统已就绪,正在启动服务...\n');
}
// 启动服务器
console.log('🌐 启动 HubCmdUI 服务器...');
console.log('📍 访问地址: http://localhost:3000');
console.log('🔧 管理面板: http://localhost:3000/admin');
console.log('👤 默认账户: root / admin@123\n');
// 启动主服务器
try {
require('../server.js');
} catch (error) {
console.error('❌ 服务器启动失败:', error.message);
console.error('💡 尝试运行: npm run init 重新初始化');
process.exit(1);
}
}
// 如果直接运行此脚本
if (require.main === module) {
autoSetup().catch(error => {
console.error('❌ 自动设置失败:', error.message);
process.exit(1);
});
}
module.exports = { autoSetup, needsInstall, needsInit };

View File

@@ -0,0 +1,199 @@
#!/usr/bin/env node
/**
* 系统初始化和配置脚本
*/
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger');
// 颜色输出
const chalk = require('chalk');
async function initializeSystem() {
console.log(chalk.blue('🚀 正在初始化 HubCmdUI 系统...\n'));
try {
// 1. 检查并创建必要目录
console.log(chalk.yellow('📁 创建必要目录...'));
await createDirectories();
// 2. 检查数据库是否已初始化
const database = require('../database/database');
try {
await database.connect();
const isInitialized = await database.isInitialized();
if (isInitialized) {
console.log(chalk.green(' ✓ 数据库已初始化,跳过初始化步骤'));
console.log(chalk.green('\n✅ 系统检查完成!'));
console.log(chalk.cyan('💡 使用 npm start 启动服务'));
console.log(chalk.cyan('🌐 默认访问地址: http://localhost:3000'));
return;
}
} catch (error) {
// 数据库连接失败,继续初始化流程
}
// 3. 检查配置文件
console.log(chalk.yellow('⚙️ 检查配置文件...'));
await checkConfigFiles();
// 4. 询问用户是否要启用SQLite
const useDatabase = await askUserChoice();
if (useDatabase) {
// 5. 迁移数据到SQLite
console.log(chalk.yellow('📊 初始化SQLite数据库...'));
await initializeSQLite();
// 6. 设置环境变量
console.log(chalk.yellow('🔧 配置数据库模式...'));
await setDatabaseMode(true);
} else {
console.log(chalk.yellow('📁 使用文件存储模式...'));
await setDatabaseMode(false);
}
// 7. 创建默认用户
console.log(chalk.yellow('👤 创建默认用户...'));
await createDefaultUser();
// 8. 配置HTTP代理
console.log(chalk.yellow('🌐 配置HTTP代理服务...'));
await configureHttpProxy();
console.log(chalk.green('\n✅ 系统初始化完成!'));
console.log(chalk.cyan('💡 使用 npm start 启动服务'));
console.log(chalk.cyan('🌐 默认访问地址: http://localhost:3000'));
console.log(chalk.cyan('👤 默认用户: root / admin@123'));
} catch (error) {
console.error(chalk.red('❌ 初始化失败:'), error.message);
process.exit(1);
}
}
/**
* 创建必要目录
*/
async function createDirectories() {
const dirs = [
'data', // 数据库文件目录
'documentation', // 文档目录(静态文件)
'logs', // 日志目录
'temp' // 临时文件目录
];
for (const dir of dirs) {
const dirPath = path.join(__dirname, '..', dir);
try {
await fs.mkdir(dirPath, { recursive: true });
} catch (error) {
if (error.code !== 'EEXIST') {
throw error;
}
}
}
console.log(chalk.green(' ✓ 目录创建完成'));
}
/**
* 检查配置文件 - 简化版不再创建config.json
*/
async function checkConfigFiles() {
console.log(chalk.green(' ✓ 使用SQLite数据库存储配置'));
}
/**
* 询问用户选择
*/
async function askUserChoice() {
// 简化处理默认使用SQLite
const useDatabase = process.env.USE_SQLITE !== 'false';
if (useDatabase) {
console.log(chalk.green(' ✓ 将使用SQLite数据库存储'));
} else {
console.log(chalk.yellow(' ⚠ 将使用文件存储模式'));
}
return useDatabase;
}
/**
* 初始化SQLite数据库
*/
async function initializeSQLite() {
try {
const database = require('../database/database');
await database.connect();
await database.createTables();
// 初始化数据库(创建默认数据)
await database.createDefaultAdmin();
await database.createDefaultDocuments();
await database.createDefaultMenuItems();
// 初始化默认配置
const configServiceDB = require('../services/configServiceDB');
await configServiceDB.initializeDefaultConfig();
// 标记数据库已初始化
await database.markAsInitialized();
console.log(chalk.green(' ✓ SQLite数据库初始化完成'));
} catch (error) {
console.log(chalk.red(' ❌ SQLite初始化失败:'), error.message);
throw error;
}
}
/**
* 设置数据库模式
*/
async function setDatabaseMode(useDatabase) {
const envPath = path.join(__dirname, '../.env');
const envContent = `# 数据库配置
USE_DATABASE=${useDatabase}
AUTO_MIGRATE=true
# HTTP代理配置
PROXY_PORT=8080
PROXY_HOST=0.0.0.0
`;
await fs.writeFile(envPath, envContent);
console.log(chalk.green(` ✓ 数据库模式已设置为: ${useDatabase ? 'SQLite' : '文件存储'}`));
}
/**
* 创建默认用户 - 简化版,数据库已处理
*/
async function createDefaultUser() {
console.log(chalk.green(' ✓ 默认管理员用户由数据库处理 (root/admin@123)'));
}
/**
* 配置HTTP代理服务信息
*/
async function configureHttpProxy() {
try {
console.log(chalk.green(' ✓ HTTP代理服务需要通过环境变量配置'));
console.log(chalk.cyan(' 配置方式: 设置 PROXY_PORT 和 PROXY_HOST 环境变量'));
console.log(chalk.cyan(' 示例: PROXY_PORT=8080 PROXY_HOST=0.0.0.0 npm start'));
} catch (error) {
console.log(chalk.yellow(' ⚠ HTTP代理服务配置提示显示失败'));
}
}
// 如果直接运行此脚本
if (require.main === module) {
initializeSystem().then(() => {
process.exit(0);
}).catch((error) => {
console.error(chalk.red('初始化失败:'), error);
process.exit(1);
});
}
module.exports = { initializeSystem };

View File

@@ -0,0 +1,92 @@
/**
* 数据库初始化脚本
*/
const database = require('../database/database');
const userServiceDB = require('../services/userServiceDB');
const configServiceDB = require('../services/configServiceDB');
const logger = require('../logger');
async function initializeDatabase() {
try {
logger.info('开始初始化数据库...');
// 连接数据库
await database.connect();
// 检查数据库是否已经初始化
const isInitialized = await database.isInitialized();
if (isInitialized) {
logger.info('数据库已经初始化,跳过重复初始化');
return;
}
// 创建数据表
await database.createTables();
// 创建默认管理员用户(如果不存在)
await database.createDefaultAdmin();
// 创建默认文档
await database.createDefaultDocuments();
// 初始化默认配置
await configServiceDB.initializeDefaultConfig();
// 标记数据库已初始化
await database.markAsInitialized();
logger.info('数据库初始化完成!');
// 显示数据库信息
const userCount = await database.get('SELECT COUNT(*) as count FROM users');
const configCount = await database.get('SELECT COUNT(*) as count FROM configs');
const docCount = await database.get('SELECT COUNT(*) as count FROM documents');
logger.info(`数据库统计:`);
logger.info(` 用户数量: ${userCount.count}`);
logger.info(` 配置项数量: ${configCount.count}`);
logger.info(` 文档数量: ${docCount.count}`);
} catch (error) {
logger.error('数据库初始化失败:', error);
process.exit(1);
}
}
/**
* 检查数据库是否已经初始化
*/
async function checkDatabaseInitialized() {
try {
// 检查用户表是否有数据
const userCount = await database.get('SELECT COUNT(*) as count FROM users');
if (userCount && userCount.count > 0) {
return true;
}
// 检查配置表是否有数据
const configCount = await database.get('SELECT COUNT(*) as count FROM configs');
if (configCount && configCount.count > 0) {
return true;
}
return false;
} catch (error) {
// 如果查询失败,认为数据库未初始化
return false;
}
}
// 如果直接运行此脚本,则执行初始化
if (require.main === module) {
initializeDatabase().then(() => {
process.exit(0);
}).catch((error) => {
logger.error('初始化过程出错:', error);
process.exit(1);
});
}
module.exports = {
initializeDatabase
};

View File

@@ -1,315 +0,0 @@
/**
* 系统初始化脚本 - 首次运行时执行
*/
const fs = require('fs').promises;
const path = require('path');
const bcrypt = require('bcrypt');
const { execSync } = require('child_process');
const logger = require('../logger');
const { ensureDirectoriesExist } = require('../init-dirs');
const { downloadImages } = require('../download-images');
const configService = require('../services/configService');
// 用户文件路径
const USERS_FILE = path.join(__dirname, '..', 'users.json');
/**
* 创建管理员用户
* @param {string} username 用户名
* @param {string} password 密码
*/
async function createAdminUser(username = 'root', password = 'admin') {
try {
// 检查用户文件是否已存在
try {
await fs.access(USERS_FILE);
logger.info('用户文件已存在,跳过创建管理员用户');
return;
} catch (err) {
if (err.code !== 'ENOENT') throw err;
}
// 创建默认管理员用户
const defaultUser = {
username,
password: bcrypt.hashSync(password, 10),
createdAt: new Date().toISOString(),
loginCount: 0,
lastLogin: null
};
await fs.writeFile(USERS_FILE, JSON.stringify({ users: [defaultUser] }, null, 2));
logger.success(`创建默认管理员用户: ${username}/${password}`);
logger.warn('请在首次登录后立即修改默认密码');
} catch (error) {
logger.error('创建管理员用户失败:', error);
throw error;
}
}
/**
* 创建默认配置
*/
async function createDefaultConfig() {
try {
// 检查配置是否已存在
const config = await configService.getConfig();
// 如果菜单项为空,添加默认菜单项
if (!config.menuItems || config.menuItems.length === 0) {
config.menuItems = [
{
text: "控制台",
link: "/admin",
newTab: false
},
{
text: "镜像搜索",
link: "/",
newTab: false
},
{
text: "文档",
link: "/docs",
newTab: false
},
{
text: "GitHub",
link: "https://github.com/dqzboy/hubcmdui",
newTab: true
}
];
await configService.saveConfig(config);
logger.success('创建默认菜单配置');
}
return config;
} catch (error) {
logger.error('初始化配置失败:', error);
throw error;
}
}
/**
* 创建示例文档 - 现已禁用
*/
async function createSampleDocumentation() {
logger.info('示例文档创建功能已禁用');
return; // 不再创建默认文档
/* 旧代码保留注释,已禁用
const docService = require('../services/documentationService');
try {
await docService.ensureDocumentationDir();
// 检查是否有现有文档
const docs = await docService.getDocumentationList();
if (docs && docs.length > 0) {
logger.info('文档已存在,跳过创建示例文档');
return;
}
// 创建示例文档
const welcomeDoc = {
title: "欢迎使用 Docker 镜像代理加速系统",
content: `# 欢迎使用 Docker 镜像代理加速系统
## 系统简介
Docker 镜像代理加速系统是一个帮助用户快速搜索、拉取 Docker 镜像的工具。本系统提供了以下功能:
- 快速搜索 Docker Hub 上的镜像
- 查看镜像的详细信息和标签
- 管理本地 Docker 容器
- 监控容器状态并发送通知
## 快速开始
1. 在首页搜索框中输入要查找的镜像名称
2. 点击搜索结果查看详细信息
3. 使用提供的命令拉取镜像
## 管理功能
管理员可以通过控制面板管理系统:
- 查看所有容器状态
- 启动/停止/重启容器
- 更新容器镜像
- 配置监控告警
祝您使用愉快!
`,
published: true
};
const aboutDoc = {
title: "关于系统",
content: `# 关于 Docker 镜像代理加速系统
## 系统版本
当前版本: v1.0.0
## 技术栈
- 前端: HTML, CSS, JavaScript
- 后端: Node.js, Express
- 容器: Docker, Dockerode
- 数据存储: 文件系统
## 联系方式
如有问题,请通过以下方式联系我们:
- GitHub Issues
- 电子邮件: example@example.com
## 许可证
本项目采用 MIT 许可证
`,
published: true
};
await docService.saveDocument(Date.now().toString(), welcomeDoc.title, welcomeDoc.content);
await docService.saveDocument((Date.now() + 1000).toString(), aboutDoc.title, aboutDoc.content);
logger.success('创建示例文档成功');
} catch (error) {
logger.error('创建示例文档失败:', error);
}
*/
}
/**
* 检查必要依赖
*/
async function checkDependencies() {
try {
logger.info('正在检查系统依赖...');
// 检查 Node.js 版本
const nodeVersion = process.version;
const minNodeVersion = 'v14.0.0';
if (compareVersions(nodeVersion, minNodeVersion) < 0) {
logger.warn(`当前 Node.js 版本 ${nodeVersion} 低于推荐的最低版本 ${minNodeVersion}`);
} else {
logger.success(`Node.js 版本 ${nodeVersion} 满足要求`);
}
// 检查必要的 npm 包
try {
const packageJson = require('../package.json');
const requiredDeps = Object.keys(packageJson.dependencies);
logger.info(`系统依赖共 ${requiredDeps.length} 个包`);
// 检查是否有 node_modules 目录
try {
await fs.access(path.join(__dirname, '..', 'node_modules'));
} catch (err) {
if (err.code === 'ENOENT') {
logger.warn('未找到 node_modules 目录,请运行 npm install 安装依赖');
return false;
}
}
} catch (err) {
logger.warn('无法读取 package.json:', err.message);
}
// 检查 Docker
try {
execSync('docker --version', { stdio: ['ignore', 'ignore', 'ignore'] });
logger.success('Docker 已安装');
} catch (err) {
logger.warn('未检测到 Docker部分功能可能无法正常使用');
}
return true;
} catch (error) {
logger.error('依赖检查失败:', error);
return false;
}
}
/**
* 比较版本号
*/
function compareVersions(v1, v2) {
const v1parts = v1.replace('v', '').split('.');
const v2parts = v2.replace('v', '').split('.');
for (let i = 0; i < Math.max(v1parts.length, v2parts.length); i++) {
const v1part = parseInt(v1parts[i] || 0);
const v2part = parseInt(v2parts[i] || 0);
if (v1part > v2part) return 1;
if (v1part < v2part) return -1;
}
return 0;
}
/**
* 主初始化函数
*/
async function initialize() {
logger.info('开始系统初始化...');
try {
// 1. 检查系统依赖
await checkDependencies();
// 2. 确保目录结构存在
await ensureDirectoriesExist();
logger.success('目录结构初始化完成');
// 3. 下载必要图片
await downloadImages();
// 4. 创建默认用户
await createAdminUser();
// 5. 创建默认配置
await createDefaultConfig();
// 6. 创建示例文档
await createSampleDocumentation();
logger.success('系统初始化完成!');
// 移除敏感的账户信息日志
logger.warn('首次登录后请立即修改默认密码!');
return { success: true };
} catch (error) {
logger.error('系统初始化失败:', error);
return { success: false, error: error.message };
}
}
// 如果直接运行此脚本
if (require.main === module) {
initialize()
.then((result) => {
if (result.success) {
process.exit(0);
} else {
process.exit(1);
}
})
.catch((error) => {
logger.fatal('初始化过程中发生错误:', error);
process.exit(1);
});
}
module.exports = {
initialize,
createAdminUser,
createDefaultConfig,
createSampleDocumentation,
checkDependencies
};

View File

@@ -15,7 +15,9 @@ const { gracefulShutdown } = require('./cleanup');
const os = require('os'); const os = require('os');
const { requireLogin } = require('./middleware/auth'); const { requireLogin } = require('./middleware/auth');
const compatibilityLayer = require('./compatibility-layer'); const compatibilityLayer = require('./compatibility-layer');
const initSystem = require('./scripts/init-system'); const { initializeDatabase } = require('./scripts/init-database');
const database = require('./database/database');
const httpProxyService = require('./services/httpProxyService');
// 设置日志级别 (默认INFO, 可通过环境变量设置) // 设置日志级别 (默认INFO, 可通过环境变量设置)
const logLevel = process.env.LOG_LEVEL || 'WARN'; const logLevel = process.env.LOG_LEVEL || 'WARN';
@@ -146,6 +148,15 @@ async function startServer() {
logger.info(`服务器已启动并监听端口 ${PORT}`); logger.info(`服务器已启动并监听端口 ${PORT}`);
try { try {
// 初始化数据库
try {
await initializeDatabase();
logger.success('数据库初始化完成');
} catch (dbError) {
logger.error('数据库初始化失败:', dbError);
logger.warn('将使用文件存储作为备用方案');
}
// 确保目录存在 // 确保目录存在
await ensureDirectoriesExist(); await ensureDirectoriesExist();
logger.success('系统目录初始化完成'); logger.success('系统目录初始化完成');
@@ -154,14 +165,41 @@ async function startServer() {
await downloadImages(); await downloadImages();
logger.success('资源下载完成'); logger.success('资源下载完成');
// 初始化系统 // 默认使用SQLite数据库模式
try { try {
const { initialize } = require('./scripts/init-system'); logger.info('正在检查SQLite数据库...');
await initialize(); const { isDatabaseReady } = require('./utils/database-checker');
logger.success('系统初始化完成'); const dbReady = await isDatabaseReady();
if (!dbReady) {
logger.warn('数据库未完全初始化,正在初始化...');
await initializeDatabase();
} else {
logger.info('SQLite数据库已就绪');
}
logger.success('SQLite数据库初始化完成');
} catch (dbError) {
logger.error('SQLite数据库初始化失败:', dbError.message);
throw dbError; // 数据库初始化失败时直接退出
}
// 初始化系统配置
try {
// 系统配置已在数据库初始化时完成
logger.info('系统配置初始化完成');
} catch (initError) { } catch (initError) {
logger.warn('系统初始化遇到问题:', initError.message); logger.warn('系统配置初始化遇到问题:', initError.message);
logger.warn('某些功能可能无法正常工作'); }
// 初始化HTTP代理服务
try {
await httpProxyService.loadConfig();
// 检查环境变量并自动启动代理
await httpProxyService.checkEnvironmentAndAutoStart();
logger.success('HTTP代理服务配置已加载');
} catch (proxyError) {
logger.warn('HTTP代理服务初始化失败:', proxyError.message);
} }
// 尝试启动监控 // 尝试启动监控

View File

@@ -1,47 +0,0 @@
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger');
const CONFIG_FILE = path.join(__dirname, '../config.json');
const DEFAULT_CONFIG = {
theme: 'light',
language: 'zh_CN',
notifications: true,
autoRefresh: true,
refreshInterval: 30000,
dockerHost: 'localhost',
dockerPort: 2375,
useHttps: false
};
async function ensureConfigFile() {
try {
await fs.access(CONFIG_FILE);
} catch (error) {
if (error.code === 'ENOENT') {
await fs.writeFile(CONFIG_FILE, JSON.stringify(DEFAULT_CONFIG, null, 2));
} else {
throw error;
}
}
}
async function getConfig() {
try {
await ensureConfigFile();
const data = await fs.readFile(CONFIG_FILE, 'utf8');
return JSON.parse(data);
} catch (error) {
logger.error('读取配置文件失败:', error);
return { ...DEFAULT_CONFIG, error: true };
}
}
module.exports = {
getConfig,
saveConfig: async (config) => {
await ensureConfigFile();
await fs.writeFile(CONFIG_FILE, JSON.stringify(config, null, 2));
},
DEFAULT_CONFIG
};

View File

@@ -0,0 +1,233 @@
/**
* 基于SQLite的配置服务模块
*/
const logger = require('../logger');
const database = require('../database/database');
class ConfigServiceDB {
/**
* 获取配置项
*/
async getConfig(key = null) {
try {
if (key) {
const config = await database.get('SELECT * FROM configs WHERE key = ?', [key]);
if (config) {
return JSON.parse(config.value);
}
return null;
} else {
// 获取所有配置
const configs = await database.all('SELECT * FROM configs');
const result = {};
for (const config of configs) {
result[config.key] = JSON.parse(config.value);
}
return result;
}
} catch (error) {
logger.error('获取配置失败:', error);
throw error;
}
}
/**
* 保存配置项
*/
async saveConfig(key, value, description = null) {
try {
const valueString = JSON.stringify(value);
const valueType = typeof value;
const existingConfig = await database.get('SELECT id FROM configs WHERE key = ?', [key]);
if (existingConfig) {
// 更新现有配置
await database.run(
'UPDATE configs SET value = ?, type = ?, description = ?, updated_at = ? WHERE key = ?',
[valueString, valueType, description, new Date().toISOString(), key]
);
} else {
// 创建新配置
await database.run(
'INSERT INTO configs (key, value, type, description, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[key, valueString, valueType, description, new Date().toISOString(), new Date().toISOString()]
);
}
// 移除详细的配置保存日志,减少日志噪音
} catch (error) {
logger.error('保存配置失败:', error);
throw error;
}
}
/**
* 批量保存配置
*/
async saveConfigs(configs) {
try {
const configCount = Object.keys(configs).length;
for (const [key, value] of Object.entries(configs)) {
await this.saveConfig(key, value);
}
logger.info(`批量保存配置完成,共 ${configCount} 项配置`);
} catch (error) {
logger.error('批量保存配置失败:', error);
throw error;
}
}
/**
* 删除配置项
*/
async deleteConfig(key) {
try {
await database.run('DELETE FROM configs WHERE key = ?', [key]);
// 删除配置时仍保留日志,因为这是重要操作
logger.info(`配置 ${key} 已删除`);
} catch (error) {
logger.error('删除配置失败:', error);
throw error;
}
}
/**
* 获取系统默认配置
*/
getDefaultConfig() {
return {
theme: 'light',
language: 'zh_CN',
notifications: true,
autoRefresh: true,
refreshInterval: 30000,
dockerHost: 'localhost',
dockerPort: 2375,
useHttps: false,
proxyDomain: 'registry-1.docker.io',
logo: '',
menuItems: [
{
text: "首页",
link: "/",
newTab: false
},
{
text: "文档",
link: "https://dqzboy.github.io/docs/",
newTab: true
},
{
text: "推广",
link: "https://dqzboy.github.io/proxyui/zanzhu",
newTab: true
},
{
text: "GitHub",
link: "https://github.com/dqzboy/Docker-Proxy",
newTab: true
}
],
monitoringConfig: {
notificationType: 'wechat',
webhookUrl: '',
telegramToken: '',
telegramChatId: '',
monitorInterval: 60,
isEnabled: false
}
};
}
/**
* 初始化默认配置
*/
async initializeDefaultConfig() {
try {
const defaultConfig = this.getDefaultConfig();
let newConfigCount = 0;
for (const [key, value] of Object.entries(defaultConfig)) {
const existingConfig = await database.get('SELECT id FROM configs WHERE key = ?', [key]);
if (!existingConfig) {
await this.saveConfig(key, value, `默认${key}配置`);
newConfigCount++;
}
}
} catch (error) {
logger.error('初始化默认配置失败:', error);
throw error;
}
}
/**
* 获取监控配置
*/
async getMonitoringConfig() {
try {
return await this.getConfig('monitoringConfig') || this.getDefaultConfig().monitoringConfig;
} catch (error) {
logger.error('获取监控配置失败:', error);
return this.getDefaultConfig().monitoringConfig;
}
}
/**
* 保存监控配置
*/
async saveMonitoringConfig(config) {
try {
await this.saveConfig('monitoringConfig', config, '监控系统配置');
} catch (error) {
logger.error('保存监控配置失败:', error);
throw error;
}
}
/**
* 获取菜单项配置
*/
async getMenuItems() {
try {
const menuItems = await database.all(
'SELECT text, link, new_tab, sort_order, enabled FROM menu_items WHERE enabled = 1 ORDER BY sort_order'
);
return menuItems.map(item => ({
text: item.text,
link: item.link,
newTab: Boolean(item.new_tab)
}));
} catch (error) {
logger.error('获取菜单项失败:', error);
return [];
}
}
/**
* 保存菜单项配置
*/
async saveMenuItems(menuItems) {
try {
// 先清空现有菜单项
await database.run('DELETE FROM menu_items');
// 插入新的菜单项
for (let i = 0; i < menuItems.length; i++) {
const item = menuItems[i];
await database.run(
'INSERT INTO menu_items (text, link, new_tab, sort_order, enabled, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?)',
[item.text, item.link, item.newTab ? 1 : 0, i + 1, 1, new Date().toISOString(), new Date().toISOString()]
);
}
logger.info('菜单项配置保存成功');
} catch (error) {
logger.error('保存菜单项失败:', error);
throw error;
}
}
}
module.exports = new ConfigServiceDB();

View File

@@ -1,324 +0,0 @@
/**
* 文档服务模块 - 处理文档管理功能
*/
const fs = require('fs').promises;
const path = require('path');
const logger = require('../logger');
const DOCUMENTATION_DIR = path.join(__dirname, '..', 'documentation');
const META_DIR = path.join(DOCUMENTATION_DIR, 'meta');
// 确保文档目录存在
async function ensureDocumentationDir() {
try {
await fs.access(DOCUMENTATION_DIR);
logger.debug('文档目录已存在');
// 确保meta目录存在
try {
await fs.access(META_DIR);
logger.debug('文档meta目录已存在');
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(META_DIR, { recursive: true });
logger.success('文档meta目录已创建');
} else {
throw error;
}
}
} catch (error) {
if (error.code === 'ENOENT') {
await fs.mkdir(DOCUMENTATION_DIR, { recursive: true });
logger.success('文档目录已创建');
// 创建meta目录
await fs.mkdir(META_DIR, { recursive: true });
logger.success('文档meta目录已创建');
} else {
throw error;
}
}
}
// 获取文档列表
async function getDocumentationList() {
try {
await ensureDocumentationDir();
const files = await fs.readdir(DOCUMENTATION_DIR);
const documents = await Promise.all(files.map(async file => {
// 跳过目录和非文档文件
if (file === 'meta' || file.startsWith('.')) return null;
// 处理JSON文件
if (file.endsWith('.json')) {
try {
const filePath = path.join(DOCUMENTATION_DIR, file);
const content = await fs.readFile(filePath, 'utf8');
const doc = JSON.parse(content);
return {
id: path.parse(file).name,
title: doc.title,
published: doc.published,
createdAt: doc.createdAt || new Date().toISOString(),
updatedAt: doc.updatedAt || new Date().toISOString()
};
} catch (fileError) {
logger.error(`读取JSON文档文件 ${file} 失败:`, fileError);
return null;
}
}
// 处理MD文件
if (file.endsWith('.md')) {
try {
const id = path.parse(file).name;
let metaData = { published: true, title: path.parse(file).name };
// 尝试读取meta数据
try {
const metaPath = path.join(META_DIR, `${id}.json`);
const metaContent = await fs.readFile(metaPath, 'utf8');
metaData = { ...metaData, ...JSON.parse(metaContent) };
} catch (metaError) {
// meta文件不存在或无法解析使用默认值
logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message);
}
// 确保有发布状态
if (typeof metaData.published !== 'boolean') {
metaData.published = true;
}
return {
id,
title: metaData.title || id,
path: file, // 不直接加载内容,而是提供路径
published: metaData.published,
createdAt: metaData.createdAt || new Date().toISOString(),
updatedAt: metaData.updatedAt || new Date().toISOString()
};
} catch (mdError) {
logger.error(`处理MD文档 ${file} 失败:`, mdError);
return null;
}
}
return null;
}));
return documents.filter(doc => doc !== null);
} catch (error) {
logger.error('获取文档列表失败:', error);
throw error;
}
}
// 获取已发布文档
async function getPublishedDocuments() {
const documents = await getDocumentationList();
return documents.filter(doc => doc.published);
}
// 获取单个文档
async function getDocument(id) {
try {
await ensureDocumentationDir();
// 首先尝试读取JSON文件
try {
const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
const jsonContent = await fs.readFile(jsonPath, 'utf8');
return JSON.parse(jsonContent);
} catch (jsonError) {
// JSON文件不存在尝试读取MD文件
if (jsonError.code === 'ENOENT') {
const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
const mdContent = await fs.readFile(mdPath, 'utf8');
// 读取meta数据
let metaData = { published: true, title: id };
try {
const metaPath = path.join(META_DIR, `${id}.json`);
const metaContent = await fs.readFile(metaPath, 'utf8');
metaData = { ...metaData, ...JSON.parse(metaContent) };
} catch (metaError) {
// meta文件不存在或无法解析使用默认值
logger.warn(`无法读取文档 ${id} 的meta数据:`, metaError.message);
}
return {
id,
title: metaData.title || id,
content: mdContent,
published: metaData.published,
createdAt: metaData.createdAt || new Date().toISOString(),
updatedAt: metaData.updatedAt || new Date().toISOString()
};
}
// 其他错误,直接抛出
throw jsonError;
}
} catch (error) {
logger.error(`获取文档 ${id} 失败:`, error);
throw error;
}
}
// 保存文档
async function saveDocument(id, title, content) {
try {
await ensureDocumentationDir();
const docId = id || Date.now().toString();
const docPath = path.join(DOCUMENTATION_DIR, `${docId}.json`);
// 检查是否已存在,保留发布状态
let published = false;
try {
const existingDoc = await fs.readFile(docPath, 'utf8');
published = JSON.parse(existingDoc).published || false;
} catch (error) {
// 文件不存在,使用默认值
}
const now = new Date().toISOString();
const docData = {
title,
content,
published,
createdAt: now,
updatedAt: now
};
await fs.writeFile(
docPath,
JSON.stringify(docData, null, 2),
'utf8'
);
return { id: docId, ...docData };
} catch (error) {
logger.error('保存文档失败:', error);
throw error;
}
}
// 删除文档
async function deleteDocument(id) {
try {
await ensureDocumentationDir();
// 删除JSON文件(如果存在)
try {
const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
await fs.unlink(jsonPath);
} catch (error) {
if (error.code !== 'ENOENT') {
logger.warn(`删除JSON文档 ${id} 失败:`, error);
}
}
// 删除MD文件(如果存在)
try {
const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
await fs.unlink(mdPath);
} catch (error) {
if (error.code !== 'ENOENT') {
logger.warn(`删除MD文档 ${id} 失败:`, error);
}
}
// 删除meta文件(如果存在)
try {
const metaPath = path.join(META_DIR, `${id}.json`);
await fs.unlink(metaPath);
} catch (error) {
if (error.code !== 'ENOENT') {
logger.warn(`删除文档 ${id} 的meta数据失败:`, error);
}
}
return { success: true };
} catch (error) {
logger.error(`删除文档 ${id} 失败:`, error);
throw error;
}
}
// 切换文档发布状态
async function toggleDocumentPublish(id) {
try {
await ensureDocumentationDir();
// 尝试读取JSON文件
try {
const jsonPath = path.join(DOCUMENTATION_DIR, `${id}.json`);
const content = await fs.readFile(jsonPath, 'utf8');
const doc = JSON.parse(content);
doc.published = !doc.published;
doc.updatedAt = new Date().toISOString();
await fs.writeFile(jsonPath, JSON.stringify(doc, null, 2), 'utf8');
return doc;
} catch (jsonError) {
// 如果JSON文件不存在尝试处理MD文件的meta数据
if (jsonError.code === 'ENOENT') {
const mdPath = path.join(DOCUMENTATION_DIR, `${id}.md`);
// 确认MD文件存在
try {
await fs.access(mdPath);
} catch (mdError) {
throw new Error(`文档 ${id} 不存在`);
}
// 获取或创建meta数据
const metaPath = path.join(META_DIR, `${id}.json`);
let metaData = { published: true, title: id };
try {
const metaContent = await fs.readFile(metaPath, 'utf8');
metaData = { ...metaData, ...JSON.parse(metaContent) };
} catch (metaError) {
// meta文件不存在使用默认值
}
// 切换发布状态
metaData.published = !metaData.published;
metaData.updatedAt = new Date().toISOString();
// 保存meta数据
await fs.writeFile(metaPath, JSON.stringify(metaData, null, 2), 'utf8');
// 获取MD文件内容
const mdContent = await fs.readFile(mdPath, 'utf8');
return {
id,
title: metaData.title,
content: mdContent,
published: metaData.published,
createdAt: metaData.createdAt,
updatedAt: metaData.updatedAt
};
}
// 其他错误,直接抛出
throw jsonError;
}
} catch (error) {
logger.error(`切换文档 ${id} 发布状态失败:`, error);
throw error;
}
}
module.exports = {
ensureDocumentationDir,
getDocumentationList,
getPublishedDocuments,
getDocument,
saveDocument,
deleteDocument,
toggleDocumentPublish
};

View File

@@ -0,0 +1,242 @@
/**
* 基于SQLite的文档服务模块
*/
const logger = require('../logger');
const database = require('../database/database');
class DocumentationServiceDB {
/**
* 获取文档列表
*/
async getDocumentationList() {
try {
const documents = await database.all(
'SELECT doc_id, title, published, created_at, updated_at FROM documents ORDER BY updated_at DESC'
);
return documents.map(doc => ({
id: doc.doc_id,
title: doc.title,
published: Boolean(doc.published),
createdAt: doc.created_at,
updatedAt: doc.updated_at
}));
} catch (error) {
logger.error('获取文档列表失败:', error);
throw error;
}
}
/**
* 获取已发布文档列表
*/
async getPublishedDocuments() {
try {
const documents = await database.all(
'SELECT doc_id, title, published, created_at, updated_at FROM documents WHERE published = 1 ORDER BY updated_at DESC'
);
return documents.map(doc => ({
id: doc.doc_id,
title: doc.title,
published: Boolean(doc.published),
createdAt: doc.created_at,
updatedAt: doc.updated_at
}));
} catch (error) {
logger.error('获取已发布文档列表失败:', error);
throw error;
}
}
/**
* 获取单个文档
*/
async getDocument(docId) {
try {
const document = await database.get(
'SELECT * FROM documents WHERE doc_id = ?',
[docId]
);
if (!document) {
throw new Error(`文档 ${docId} 不存在`);
}
return {
id: document.doc_id,
title: document.title,
content: document.content,
published: Boolean(document.published),
createdAt: document.created_at,
updatedAt: document.updated_at
};
} catch (error) {
logger.error(`获取文档 ${docId} 失败:`, error);
throw error;
}
}
/**
* 保存文档
*/
async saveDocument(docId, title, content, published = false) {
try {
const id = docId || Date.now().toString();
const now = new Date().toISOString();
const existingDoc = await database.get(
'SELECT id FROM documents WHERE doc_id = ?',
[id]
);
if (existingDoc) {
// 更新现有文档
await database.run(
'UPDATE documents SET title = ?, content = ?, published = ?, updated_at = ? WHERE doc_id = ?',
[title, content, published ? 1 : 0, now, id]
);
logger.info(`文档 ${id} 已更新`);
} else {
// 创建新文档
await database.run(
'INSERT INTO documents (doc_id, title, content, published, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?)',
[id, title, content, published ? 1 : 0, now, now]
);
logger.info(`文档 ${id} 已创建`);
}
return {
id,
title,
content,
published: Boolean(published),
createdAt: existingDoc ? existingDoc.created_at : now,
updatedAt: now
};
} catch (error) {
logger.error('保存文档失败:', error);
throw error;
}
}
/**
* 删除文档
*/
async deleteDocument(docId) {
try {
const result = await database.run(
'DELETE FROM documents WHERE doc_id = ?',
[docId]
);
if (result.changes === 0) {
throw new Error(`文档 ${docId} 不存在`);
}
logger.info(`文档 ${docId} 已删除`);
return true;
} catch (error) {
logger.error(`删除文档 ${docId} 失败:`, error);
throw error;
}
}
/**
* 切换文档发布状态
*/
async toggleDocumentPublish(docId) {
try {
const document = await this.getDocument(docId);
const newPublishedStatus = !document.published;
await database.run(
'UPDATE documents SET published = ?, updated_at = ? WHERE doc_id = ?',
[newPublishedStatus ? 1 : 0, new Date().toISOString(), docId]
);
logger.info(`文档 ${docId} 发布状态已切换为: ${newPublishedStatus}`);
return {
...document,
published: newPublishedStatus,
updatedAt: new Date().toISOString()
};
} catch (error) {
logger.error(`切换文档 ${docId} 发布状态失败:`, error);
throw error;
}
}
/**
* 批量更新文档发布状态
*/
async batchUpdatePublishStatus(docIds, published) {
try {
const placeholders = docIds.map(() => '?').join(',');
const params = [...docIds, published ? 1 : 0, new Date().toISOString()];
const result = await database.run(
`UPDATE documents SET published = ?, updated_at = ? WHERE doc_id IN (${placeholders})`,
params
);
logger.info(`批量更新 ${result.changes} 个文档的发布状态`);
return result.changes;
} catch (error) {
logger.error('批量更新文档发布状态失败:', error);
throw error;
}
}
/**
* 搜索文档
*/
async searchDocuments(keyword, publishedOnly = false) {
try {
let sql = 'SELECT doc_id, title, published, created_at, updated_at FROM documents WHERE (title LIKE ? OR content LIKE ?)';
const params = [`%${keyword}%`, `%${keyword}%`];
if (publishedOnly) {
sql += ' AND published = 1';
}
sql += ' ORDER BY updated_at DESC';
const documents = await database.all(sql, params);
return documents.map(doc => ({
id: doc.doc_id,
title: doc.title,
published: Boolean(doc.published),
createdAt: doc.created_at,
updatedAt: doc.updated_at
}));
} catch (error) {
logger.error('搜索文档失败:', error);
throw error;
}
}
/**
* 获取文档统计信息
*/
async getDocumentStats() {
try {
const totalCount = await database.get('SELECT COUNT(*) as count FROM documents');
const publishedCount = await database.get('SELECT COUNT(*) as count FROM documents WHERE published = 1');
const unpublishedCount = await database.get('SELECT COUNT(*) as count FROM documents WHERE published = 0');
return {
total: totalCount.count,
published: publishedCount.count,
unpublished: unpublishedCount.count
};
} catch (error) {
logger.error('获取文档统计信息失败:', error);
throw error;
}
}
}
module.exports = new DocumentationServiceDB();

View File

@@ -0,0 +1,418 @@
/**
* HTTP代理服务模块
*/
const http = require('http');
const https = require('https');
const url = require('url');
const net = require('net');
const logger = require('../logger');
const configServiceDB = require('./configServiceDB');
class HttpProxyService {
constructor() {
this.proxyServer = null;
this.isRunning = false;
this.config = {
port: 8080,
host: '0.0.0.0',
enableHttps: true,
enableAuth: false,
username: '',
password: '',
allowedHosts: [],
blockedHosts: [],
logRequests: true
};
}
/**
* 启动代理服务器
*/
async start(config = {}) {
try {
this.config = { ...this.config, ...config };
if (this.isRunning) {
logger.warn('HTTP代理服务器已在运行');
return;
}
this.proxyServer = http.createServer();
// 处理HTTP请求
this.proxyServer.on('request', this.handleHttpRequest.bind(this));
// 处理HTTPS CONNECT请求
this.proxyServer.on('connect', this.handleHttpsConnect.bind(this));
// 错误处理
this.proxyServer.on('error', this.handleServerError.bind(this));
return new Promise((resolve, reject) => {
this.proxyServer.listen(this.config.port, this.config.host, (err) => {
if (err) {
reject(err);
} else {
this.isRunning = true;
logger.info(`HTTP代理服务器已启动监听 ${this.config.host}:${this.config.port}`);
resolve();
}
});
});
} catch (error) {
logger.error('启动HTTP代理服务器失败:', error);
throw error;
}
}
/**
* 停止代理服务器
*/
async stop() {
return new Promise((resolve) => {
if (this.proxyServer && this.isRunning) {
this.proxyServer.close(() => {
this.isRunning = false;
logger.info('HTTP代理服务器已停止');
resolve();
});
} else {
resolve();
}
});
}
/**
* 处理HTTP请求
*/
handleHttpRequest(clientReq, clientRes) {
try {
const targetUrl = clientReq.url;
const parsedUrl = url.parse(targetUrl);
// 记录请求日志
if (this.config.logRequests) {
logger.info(`HTTP代理请求: ${clientReq.method} ${targetUrl}`);
}
// 认证检查
if (this.config.enableAuth && !this.checkAuth(clientReq)) {
this.sendAuthRequired(clientRes);
return;
}
// 主机检查
if (!this.isHostAllowed(parsedUrl.hostname)) {
this.sendForbidden(clientRes, '主机不在允许列表中');
return;
}
if (this.isHostBlocked(parsedUrl.hostname)) {
this.sendForbidden(clientRes, '主机已被阻止');
return;
}
// 创建目标请求选项
const options = {
hostname: parsedUrl.hostname,
port: parsedUrl.port || (parsedUrl.protocol === 'https:' ? 443 : 80),
path: parsedUrl.path,
method: clientReq.method,
headers: { ...clientReq.headers }
};
// 移除代理相关的头部
delete options.headers['proxy-connection'];
delete options.headers['proxy-authorization'];
// 选择HTTP或HTTPS
const httpModule = parsedUrl.protocol === 'https:' ? https : http;
// 发送请求到目标服务器
const proxyReq = httpModule.request(options, (proxyRes) => {
// 复制响应头
clientRes.writeHead(proxyRes.statusCode, proxyRes.headers);
// 管道传输响应数据
proxyRes.pipe(clientRes);
});
// 错误处理
proxyReq.on('error', (err) => {
logger.error('代理请求错误:', err);
if (!clientRes.headersSent) {
clientRes.writeHead(500, { 'Content-Type': 'text/plain' });
clientRes.end('代理服务器错误');
}
});
// 管道传输请求数据
clientReq.pipe(proxyReq);
} catch (error) {
logger.error('处理HTTP请求失败:', error);
if (!clientRes.headersSent) {
clientRes.writeHead(500, { 'Content-Type': 'text/plain' });
clientRes.end('内部服务器错误');
}
}
}
/**
* 处理HTTPS CONNECT请求
*/
handleHttpsConnect(clientReq, clientSocket, head) {
try {
const { hostname, port } = this.parseConnectUrl(clientReq.url);
// 记录请求日志
if (this.config.logRequests) {
logger.info(`HTTPS代理请求: CONNECT ${hostname}:${port}`);
}
// 认证检查
if (this.config.enableAuth && !this.checkAuth(clientReq)) {
clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n\r\n');
clientSocket.end();
return;
}
// 主机检查
if (!this.isHostAllowed(hostname)) {
clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
clientSocket.end();
return;
}
if (this.isHostBlocked(hostname)) {
clientSocket.write('HTTP/1.1 403 Forbidden\r\n\r\n');
clientSocket.end();
return;
}
// 连接到目标服务器
const serverSocket = net.connect(port, hostname, () => {
// 发送连接成功响应
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
// 建立隧道
serverSocket.write(head);
serverSocket.pipe(clientSocket);
clientSocket.pipe(serverSocket);
});
// 错误处理
serverSocket.on('error', (err) => {
logger.error('服务器连接错误:', err);
clientSocket.write('HTTP/1.1 502 Bad Gateway\r\n\r\n');
clientSocket.end();
});
clientSocket.on('error', (err) => {
logger.error('客户端连接错误:', err);
serverSocket.end();
});
} catch (error) {
logger.error('处理HTTPS CONNECT请求失败:', error);
clientSocket.write('HTTP/1.1 500 Internal Server Error\r\n\r\n');
clientSocket.end();
}
}
/**
* 解析CONNECT请求URL
*/
parseConnectUrl(connectUrl) {
const [hostname, port] = connectUrl.split(':');
return {
hostname,
port: parseInt(port) || 443
};
}
/**
* 检查认证
*/
checkAuth(req) {
if (!this.config.enableAuth) {
return true;
}
const auth = req.headers['proxy-authorization'];
if (!auth) {
return false;
}
const [type, credentials] = auth.split(' ');
if (type !== 'Basic') {
return false;
}
const decoded = Buffer.from(credentials, 'base64').toString();
const [username, password] = decoded.split(':');
return username === this.config.username && password === this.config.password;
}
/**
* 检查主机是否允许
*/
isHostAllowed(hostname) {
if (this.config.allowedHosts.length === 0) {
return true; // 如果没有设置允许列表,则允许所有
}
return this.config.allowedHosts.some(allowed => {
if (allowed.startsWith('*.')) {
const domain = allowed.substring(2);
return hostname.endsWith(domain);
}
return hostname === allowed;
});
}
/**
* 检查主机是否被阻止
*/
isHostBlocked(hostname) {
return this.config.blockedHosts.some(blocked => {
if (blocked.startsWith('*.')) {
const domain = blocked.substring(2);
return hostname.endsWith(domain);
}
return hostname === blocked;
});
}
/**
* 发送认证要求响应
*/
sendAuthRequired(res) {
res.writeHead(407, {
'Proxy-Authenticate': 'Basic realm="Proxy"',
'Content-Type': 'text/plain'
});
res.end('需要代理认证');
}
/**
* 发送禁止访问响应
*/
sendForbidden(res, message) {
res.writeHead(403, { 'Content-Type': 'text/plain' });
res.end(message || '禁止访问');
}
/**
* 处理服务器错误
*/
handleServerError(error) {
logger.error('HTTP代理服务器错误:', error);
}
/**
* 获取代理状态
*/
getStatus() {
return {
isRunning: this.isRunning,
config: this.config,
port: this.config.port,
host: this.config.host
};
}
/**
* 更新配置
*/
async updateConfig(newConfig) {
try {
const needRestart = this.isRunning && (
newConfig.port !== this.config.port ||
newConfig.host !== this.config.host
);
this.config = { ...this.config, ...newConfig };
if (needRestart) {
await this.stop();
await this.start();
logger.info('HTTP代理服务器配置已更新并重启');
} else {
logger.info('HTTP代理服务器配置已更新');
}
// 保存配置到数据库
await configServiceDB.saveConfig('httpProxyConfig', this.config);
} catch (error) {
logger.error('更新HTTP代理配置失败:', error);
throw error;
}
}
/**
* 从数据库加载配置
*/
async loadConfig() {
try {
const savedConfig = await configServiceDB.getConfig('httpProxyConfig');
if (savedConfig) {
this.config = { ...this.config, ...savedConfig };
logger.info('HTTP代理配置已从数据库加载');
} else {
// 从环境变量加载默认配置
this.config = {
...this.config,
port: parseInt(process.env.PROXY_PORT) || this.config.port,
host: process.env.PROXY_HOST || this.config.host
};
logger.info('使用默认HTTP代理配置');
}
} catch (error) {
logger.error('加载HTTP代理配置失败:', error);
// 使用默认配置
logger.info('使用默认HTTP代理配置');
}
}
/**
* 检查环境变量并自动启动代理
*/
async checkEnvironmentAndAutoStart() {
const autoStart = process.env.PROXY_AUTO_START;
const proxyPort = process.env.PROXY_PORT;
const proxyHost = process.env.PROXY_HOST;
const enableAuth = process.env.PROXY_ENABLE_AUTH;
const username = process.env.PROXY_USERNAME;
const password = process.env.PROXY_PASSWORD;
// 检查是否应该自动启动代理
if (autoStart === 'true' || proxyPort || proxyHost) {
logger.info('检测到代理环境变量尝试自动启动HTTP代理服务...');
const envConfig = {};
if (proxyPort) envConfig.port = parseInt(proxyPort);
if (proxyHost) envConfig.host = proxyHost;
if (enableAuth === 'true') {
envConfig.enableAuth = true;
if (username) envConfig.username = username;
if (password) envConfig.password = password;
}
try {
await this.start(envConfig);
logger.info(`HTTP代理服务已自动启动 - ${envConfig.host || '0.0.0.0'}:${envConfig.port || 8080}`);
} catch (error) {
logger.warn('自动启动HTTP代理服务失败:', error.message);
}
} else {
logger.info('未检测到代理自动启动环境变量');
}
}
}
// 创建单例实例
const httpProxyService = new HttpProxyService();
module.exports = httpProxyService;

View File

@@ -3,7 +3,7 @@
*/ */
const axios = require('axios'); const axios = require('axios');
const logger = require('../logger'); const logger = require('../logger');
const configService = require('./configService'); const configServiceDB = require('./configServiceDB');
const dockerService = require('./dockerService'); const dockerService = require('./dockerService');
// 监控相关状态映射 // 监控相关状态映射
@@ -15,13 +15,13 @@ let monitoringInterval = null;
// 更新监控配置 // 更新监控配置
async function updateMonitoringConfig(config) { async function updateMonitoringConfig(config) {
try { try {
const currentConfig = await configService.getConfig(); const currentConfig = await configServiceDB.getConfig();
currentConfig.monitoringConfig = { currentConfig.monitoringConfig = {
...currentConfig.monitoringConfig, ...currentConfig.monitoringConfig,
...config ...config
}; };
await configService.saveConfig(currentConfig); await configServiceDB.saveConfig(currentConfig);
// 重新启动监控 // 重新启动监控
await startMonitoring(); await startMonitoring();
@@ -36,7 +36,7 @@ async function updateMonitoringConfig(config) {
// 启动监控 // 启动监控
async function startMonitoring() { async function startMonitoring() {
try { try {
const config = await configService.getConfig(); const config = await configServiceDB.getConfig();
const { isEnabled, monitorInterval } = config.monitoringConfig || {}; const { isEnabled, monitorInterval } = config.monitoringConfig || {};
// 如果监控已启用 // 如果监控已启用
@@ -308,9 +308,9 @@ async function testNotification(config) {
// 切换监控状态 // 切换监控状态
async function toggleMonitoring(isEnabled) { async function toggleMonitoring(isEnabled) {
const config = await configService.getConfig(); const config = await configServiceDB.getConfig();
config.monitoringConfig.isEnabled = isEnabled; config.monitoringConfig.isEnabled = isEnabled;
await configService.saveConfig(config); await configServiceDB.saveConfig(config);
return startMonitoring(); return startMonitoring();
} }

View File

@@ -1,175 +0,0 @@
/**
* 用户服务模块
*/
const fs = require('fs').promises;
const path = require('path');
const bcrypt = require('bcrypt');
const logger = require('../logger');
const USERS_FILE = path.join(__dirname, '..', 'users.json');
// 获取所有用户
async function getUsers() {
try {
const data = await fs.readFile(USERS_FILE, 'utf8');
return JSON.parse(data);
} catch (error) {
if (error.code === 'ENOENT') {
logger.warn('Users file does not exist, creating default user');
const defaultUser = {
username: 'root',
password: bcrypt.hashSync('admin', 10),
createdAt: new Date().toISOString(),
loginCount: 0,
lastLogin: null
};
await saveUsers([defaultUser]);
return { users: [defaultUser] };
}
throw error;
}
}
// 保存用户
async function saveUsers(users) {
await fs.writeFile(USERS_FILE, JSON.stringify({ users }, null, 2), 'utf8');
}
// 更新用户登录信息
async function updateUserLoginInfo(username) {
try {
const { users } = await getUsers();
const user = users.find(u => u.username === username);
if (user) {
user.loginCount = (user.loginCount || 0) + 1;
user.lastLogin = new Date().toISOString();
await saveUsers(users);
}
} catch (error) {
logger.error('更新用户登录信息失败:', error);
}
}
// 获取用户统计信息
async function getUserStats(username) {
try {
const { users } = await getUsers();
const user = users.find(u => u.username === username);
if (!user) {
return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
}
// 计算账户年龄(如果有创建日期)
let accountAge = '0';
if (user.createdAt) {
const createdDate = new Date(user.createdAt);
const currentDate = new Date();
const diffTime = Math.abs(currentDate - createdDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
accountAge = diffDays.toString();
}
// 格式化最后登录时间
let lastLogin = '未知';
if (user.lastLogin) {
const lastLoginDate = new Date(user.lastLogin);
const now = new Date();
const isToday = lastLoginDate.toDateString() === now.toDateString();
if (isToday) {
lastLogin = '今天 ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
lastLogin = lastLoginDate.toLocaleDateString() + ' ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
}
return {
username: user.username,
loginCount: (user.loginCount || 0).toString(),
lastLogin,
accountAge
};
} catch (error) {
logger.error('获取用户统计信息失败:', error);
return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
}
}
// 创建新用户
async function createUser(username, password) {
try {
const { users } = await getUsers();
// 检查用户是否已存在
if (users.some(u => u.username === username)) {
throw new Error('用户名已存在');
}
const hashedPassword = bcrypt.hashSync(password, 10);
const newUser = {
username,
password: hashedPassword,
createdAt: new Date().toISOString(),
loginCount: 0,
lastLogin: null
};
users.push(newUser);
await saveUsers(users);
return { success: true, username };
} catch (error) {
logger.error('创建用户失败:', error);
throw error;
}
}
// 修改用户密码
async function changePassword(username, currentPassword, newPassword) {
try {
const { users } = await getUsers();
const user = users.find(u => u.username === username);
if (!user) {
throw new Error('用户不存在');
}
// 验证当前密码
const isMatch = await bcrypt.compare(currentPassword, user.password);
if (!isMatch) {
throw new Error('当前密码不正确');
}
// 验证新密码复杂度(虽然前端做了,后端再做一层保险)
if (!isPasswordComplex(newPassword)) {
throw new Error('新密码不符合复杂度要求');
}
// 更新密码
user.password = await bcrypt.hash(newPassword, 10);
await saveUsers(users);
logger.info(`用户 ${username} 密码已成功修改`);
} catch (error) {
logger.error('修改密码失败:', error);
throw error;
}
}
// 验证密码复杂度 (从 userCenter.js 复制过来并调整)
function isPasswordComplex(password) {
// 至少包含1个字母、1个数字和1个特殊字符长度在8-16位之间
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
return passwordRegex.test(password);
}
module.exports = {
getUsers,
saveUsers,
updateUserLoginInfo,
getUserStats,
createUser,
changePassword
};

View File

@@ -0,0 +1,190 @@
/**
* 基于SQLite的用户服务模块
*/
const bcrypt = require('bcrypt');
const logger = require('../logger');
const database = require('../database/database');
class UserServiceDB {
/**
* 获取所有用户
*/
async getUsers() {
try {
const users = await database.all('SELECT * FROM users ORDER BY created_at DESC');
return { users };
} catch (error) {
logger.error('获取用户列表失败:', error);
throw error;
}
}
/**
* 通过用户名获取用户
*/
async getUserByUsername(username) {
try {
return await database.get('SELECT * FROM users WHERE username = ?', [username]);
} catch (error) {
logger.error('获取用户失败:', error);
throw error;
}
}
/**
* 创建新用户
*/
async createUser(username, password) {
try {
// 检查用户是否已存在
const existingUser = await this.getUserByUsername(username);
if (existingUser) {
throw new Error('用户名已存在');
}
const hashedPassword = await bcrypt.hash(password, 10);
const result = await database.run(
'INSERT INTO users (username, password, created_at, login_count, last_login) VALUES (?, ?, ?, ?, ?)',
[username, hashedPassword, new Date().toISOString(), 0, null]
);
return { success: true, username, id: result.id };
} catch (error) {
logger.error('创建用户失败:', error);
throw error;
}
}
/**
* 更新用户登录信息
*/
async updateUserLoginInfo(username) {
try {
const user = await this.getUserByUsername(username);
if (user) {
await database.run(
'UPDATE users SET login_count = login_count + 1, last_login = ? WHERE username = ?',
[new Date().toISOString(), username]
);
}
} catch (error) {
logger.error('更新用户登录信息失败:', error);
}
}
/**
* 获取用户统计信息
*/
async getUserStats(username) {
try {
const user = await this.getUserByUsername(username);
if (!user) {
return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
}
// 计算账户年龄
let accountAge = '0';
if (user.created_at) {
const createdDate = new Date(user.created_at);
const currentDate = new Date();
const diffTime = Math.abs(currentDate - createdDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
accountAge = diffDays.toString();
}
// 格式化最后登录时间
let lastLogin = '未知';
if (user.last_login) {
const lastLoginDate = new Date(user.last_login);
const now = new Date();
const isToday = lastLoginDate.toDateString() === now.toDateString();
if (isToday) {
lastLogin = '今天 ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
} else {
lastLogin = lastLoginDate.toLocaleDateString() + ' ' + lastLoginDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
}
}
return {
username: user.username,
loginCount: (user.login_count || 0).toString(),
lastLogin,
accountAge
};
} catch (error) {
logger.error('获取用户统计信息失败:', error);
return { loginCount: '0', lastLogin: '未知', accountAge: '0' };
}
}
/**
* 修改用户密码
*/
async changePassword(username, currentPassword, newPassword) {
try {
const user = await this.getUserByUsername(username);
if (!user) {
throw new Error('用户不存在');
}
// 验证当前密码
const isMatch = await bcrypt.compare(currentPassword, user.password);
if (!isMatch) {
throw new Error('当前密码不正确');
}
// 验证新密码复杂度
if (!this.isPasswordComplex(newPassword)) {
throw new Error('新密码不符合复杂度要求');
}
// 更新密码
const hashedNewPassword = await bcrypt.hash(newPassword, 10);
await database.run(
'UPDATE users SET password = ?, updated_at = ? WHERE username = ?',
[hashedNewPassword, new Date().toISOString(), username]
);
logger.info(`用户 ${username} 密码已成功修改`);
} catch (error) {
logger.error('修改密码失败:', error);
throw error;
}
}
/**
* 验证密码复杂度
*/
isPasswordComplex(password) {
// 至少包含1个字母、1个数字和1个特殊字符长度在8-16位之间
const passwordRegex = /^(?=.*[A-Za-z])(?=.*\d)(?=.*[.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&])[A-Za-z\d.,\-_+=()[\]{}|\\;:'"<>?/@$!%*#?&]{8,16}$/;
return passwordRegex.test(password);
}
/**
* 验证用户登录
*/
async validateUser(username, password) {
try {
const user = await this.getUserByUsername(username);
if (!user) {
return null;
}
const isMatch = await bcrypt.compare(password, user.password);
if (isMatch) {
return user;
}
return null;
} catch (error) {
logger.error('验证用户失败:', error);
throw error;
}
}
}
module.exports = new UserServiceDB();

View File

@@ -1,10 +0,0 @@
{
"users": [
{
"username": "root",
"password": "$2b$10$HBYJPwEB1gdRxcc6Bm1mKukxCC8eyJOZC7sGJN5meghvsBfoQjKtW",
"loginCount": 2,
"lastLogin": "2025-07-11T02:17:50.457Z"
}
]
}

View File

@@ -0,0 +1,131 @@
/**
* 数据库状态检查工具
*/
const sqlite3 = require('sqlite3');
const path = require('path');
const fs = require('fs');
const DB_PATH = path.join(__dirname, '../data/app.db');
/**
* 检查数据库是否已完全初始化
*/
async function isDatabaseReady() {
return new Promise((resolve) => {
// 检查数据库文件是否存在
if (!fs.existsSync(DB_PATH)) {
resolve(false);
return;
}
// 检查文件大小
try {
const stats = fs.statSync(DB_PATH);
if (stats.size < 1024) {
resolve(false);
return;
}
} catch (error) {
resolve(false);
return;
}
// 检查数据库结构和数据
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
resolve(false);
return;
}
// 检查必要的表是否存在
const requiredTables = ['users', 'configs', 'documents'];
let checkedTables = 0;
let allTablesReady = true;
let tablesWithData = 0;
requiredTables.forEach(tableName => {
db.get(
`SELECT name FROM sqlite_master WHERE type='table' AND name=?`,
[tableName],
(err, row) => {
if (err || !row) {
allTablesReady = false;
checkedTables++;
checkComplete();
return;
}
// 检查表是否有数据(至少用户表和配置表应该有数据)
if (tableName === 'users' || tableName === 'configs') {
db.get(`SELECT COUNT(*) as count FROM ${tableName}`, (err, countRow) => {
if (err || !countRow || countRow.count === 0) {
allTablesReady = false;
} else {
tablesWithData++;
}
checkedTables++;
checkComplete();
});
} else {
checkedTables++;
checkComplete();
}
}
);
});
function checkComplete() {
if (checkedTables === requiredTables.length) {
db.close((err) => {
// 需要至少用户表和配置表有数据
resolve(allTablesReady && tablesWithData >= 2);
});
}
}
});
});
}
/**
* 获取数据库统计信息
*/
async function getDatabaseStats() {
return new Promise((resolve) => {
if (!fs.existsSync(DB_PATH)) {
resolve(null);
return;
}
const db = new sqlite3.Database(DB_PATH, (err) => {
if (err) {
resolve(null);
return;
}
const stats = {};
let completedQueries = 0;
const tables = ['users', 'configs', 'documents'];
tables.forEach(table => {
db.get(`SELECT COUNT(*) as count FROM ${table}`, (err, row) => {
completedQueries++;
if (!err && row) {
stats[table] = row.count;
} else {
stats[table] = 0;
}
if (completedQueries === tables.length) {
db.close();
resolve(stats);
}
});
});
});
});
}
module.exports = {
isDatabaseReady,
getDatabaseStats
};

View File

@@ -12,12 +12,24 @@
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/@popperjs/core@2.11.6/dist/umd/popper.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/js/bootstrap.bundle.min.js"></script>
<!-- 引入 jQuery (Editor.md 需要) -->
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<!-- 引入 Markdown 编辑器 --> <!-- 引入 Markdown 编辑器 -->
<link rel="stylesheet" href="https://uicdn.toast.com/editor/3.0.0/toastui-editor.min.css" />
<script src="https://uicdn.toast.com/editor/3.0.0/toastui-editor.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/dompurify/3.0.6/purify.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<!-- 引入 Editor.md -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/codemirror.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/editor.md@1.5.0/css/editormd.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/codemirror.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/addon/mode/overlay.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/mode/markdown/markdown.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.62.0/mode/gfm/gfm.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/4.0.2/marked.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/editor.md@1.5.0/editormd.min.js"></script>
<!-- 自定义样式 --> <!-- 自定义样式 -->
<link rel="stylesheet" href="css/admin.css"> <link rel="stylesheet" href="css/admin.css">
<style> <style>
@@ -37,6 +49,11 @@
transition: all 0.3s ease; transition: all 0.3s ease;
} }
.sidebar-header {
text-align: center;
margin-bottom: 1rem;
}
/* 文档管理新建文档徽章 */ /* 文档管理新建文档徽章 */
.new-badge { .new-badge {
display: inline-block; display: inline-block;
@@ -1481,7 +1498,7 @@
} }
/* 文档编辑器部分 */ /* 文档编辑器部分 */
#editorContainer { #editor #editorContainer {
margin-top: 2rem; margin-top: 2rem;
border-radius: var(--radius-md); border-radius: var(--radius-md);
background-color: var(--container-bg); background-color: var(--container-bg);
@@ -1493,12 +1510,261 @@
#documentTitle { #documentTitle {
width: 100%; width: 100%;
padding: 1rem; padding: 1rem;
margin-bottom: 1rem; margin-bottom: 0;
border: 1px solid var(--border-color); border: 1px solid var(--border-color);
border-radius: var (--radius-md);; border-radius: var(--radius-md) var(--radius-md) 0 0;
font-size: 1.1rem; font-size: 1.1rem;
font-weight: 500; font-weight: 500;
box-shadow: var(--shadow-sm); box-shadow: none;
background-color: var(--container-bg);
color: var(--text-primary);
border-bottom: 1px solid var(--border-light);
}
#documentTitle:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 2px rgba(61, 124, 244, 0.2);
outline: none;
}
/* Editor.md 样式覆盖 */
#editor #editorContainer .editormd {
border: 1px solid var(--border-light) !important;
border-top: none !important;
border-radius: 0 0 var(--radius-md) var(--radius-md) !important;
box-shadow: none !important;
margin-top: 0 !important;
width: 100% !important;
height: auto !important;
}
#editor #editorContainer .editormd-toolbar {
background-color: var(--container-bg) !important;
border-bottom: 1px solid var(--border-light) !important;
padding: 0.5rem !important;
width: 100% !important;
box-sizing: border-box !important;
}
#editor #editorContainer .editormd-toolbar button {
background-color: transparent !important;
border: 1px solid var(--border-light) !important;
color: var(--text-primary) !important;
border-radius: var(--radius-sm) !important;
margin: 0 2px !important;
padding: 0.4rem 0.6rem !important;
transition: all 0.2s ease !important;
}
#editor #editorContainer .editormd-toolbar button:hover {
background-color: var(--primary-color) !important;
color: white !important;
border-color: var(--primary-color) !important;
}
#editor #editorContainer .editormd-toolbar button.active {
background-color: var(--primary-color) !important;
color: white !important;
border-color: var(--primary-color) !important;
}
/* 编辑区域和预览区域等宽等高 */
#editor #editorContainer .editormd > .editormd-editor {
width: 50% !important;
float: left !important;
box-sizing: border-box !important;
}
#editor #editorContainer .editormd > .editormd-preview {
width: 50% !important;
float: right !important;
box-sizing: border-box !important;
}
#editor #editorContainer .CodeMirror {
background-color: var(--container-bg) !important;
color: var(--text-primary) !important;
border: none !important;
font-family: 'Monaco', 'Consolas', 'Courier New', monospace !important;
font-size: 14px !important;
line-height: 1.6 !important;
height: 500px !important;
width: 100% !important;
box-sizing: border-box !important;
}
#editor #editorContainer .CodeMirror .CodeMirror-cursor {
border-left: 1px solid var(--text-primary) !important;
}
#editor #editorContainer .CodeMirror .CodeMirror-selected {
background-color: rgba(61, 124, 244, 0.2) !important;
}
#editor #editorContainer .editormd-preview {
background-color: var(--container-bg) !important;
color: var(--text-primary) !important;
border-left: 1px solid var(--border-light) !important;
padding: 1rem !important;
height: 500px !important;
overflow-y: auto !important;
box-sizing: border-box !important;
}
/* 确保容器清除浮动 */
#editor #editorContainer .editormd::after {
content: "" !important;
display: table !important;
clear: both !important;
}
#editor #editorContainer .editormd-preview h1,
#editor #editorContainer .editormd-preview h2,
#editor #editorContainer .editormd-preview h3,
#editor #editorContainer .editormd-preview h4,
#editor #editorContainer .editormd-preview h5,
#editor #editorContainer .editormd-preview h6 {
color: var(--text-primary) !important;
border-bottom: 1px solid var(--border-light) !important;
padding-bottom: 0.5rem !important;
margin-bottom: 1rem !important;
}
#editor #editorContainer .editormd-preview pre {
background-color: rgba(0, 0, 0, 0.05) !important;
border: 1px solid var(--border-light) !important;
border-radius: var(--radius-sm) !important;
padding: 1rem !important;
overflow-x: auto !important;
}
#editor #editorContainer .editormd-preview blockquote {
border-left: 4px solid var(--primary-color) !important;
background-color: rgba(61, 124, 244, 0.05) !important;
margin: 1rem 0 !important;
padding: 0.5rem 1rem !important;
}
#editor #editorContainer .editormd-preview table {
border-collapse: collapse !important;
width: 100% !important;
margin: 1rem 0 !important;
}
#editor #editorContainer .editormd-preview table th,
#editor #editorContainer .editormd-preview table td {
border: 1px solid var(--border-light) !important;
padding: 0.5rem 1rem !important;
text-align: left !important;
}
#editor #editorContainer .editormd-preview table th {
background-color: rgba(61, 124, 244, 0.1) !important;
font-weight: 600 !important;
}
/* 编辑器操作按钮样式 */
.editor-actions {
padding: 1rem !important;
background-color: var(--container-bg) !important;
border-top: 1px solid var(--border-light) !important;
display: flex !important;
gap: 0.75rem !important;
justify-content: flex-end !important;
}
.editor-actions .btn {
padding: 0.75rem 1.5rem !important;
border-radius: var(--radius-md) !important;
font-weight: 500 !important;
transition: all 0.2s ease !important;
}
.editor-actions .btn-primary {
background-color: var(--primary-color) !important;
color: white !important;
border: 1px solid var(--primary-color) !important;
}
.editor-actions .btn-primary:hover {
background-color: var(--primary-dark) !important;
border-color: var(--primary-dark) !important;
transform: translateY(-2px) !important;
box-shadow: var(--shadow-md) !important;
}
.editor-actions .btn-secondary {
background-color: transparent !important;
color: var(--text-secondary) !important;
border: 1px solid var(--border-color) !important;
}
.editor-actions .btn-secondary:hover {
background-color: var(--text-secondary) !important;
color: white !important;
transform: translateY(-2px) !important;
box-shadow: var(--shadow-sm) !important;
}
/* 响应式 Editor.md 样式 */
@media (max-width: 768px) {
#editor #editorContainer .editormd {
height: auto !important;
}
/* 移动端编辑区域和预览区域上下布局 */
#editor #editorContainer .editormd > .editormd-editor {
width: 100% !important;
float: none !important;
margin-bottom: 1px !important;
}
#editor #editorContainer .editormd > .editormd-preview {
width: 100% !important;
float: none !important;
border-left: none !important;
border-top: 1px solid var(--border-light) !important;
}
#editor #editorContainer .CodeMirror {
height: 300px !important;
}
#editor #editorContainer .editormd-preview {
height: 300px !important;
}
#editor #editorContainer .editormd-toolbar {
padding: 0.3rem !important;
flex-wrap: wrap !important;
}
#editor #editorContainer .editormd-toolbar button {
margin: 1px !important;
padding: 0.3rem 0.4rem !important;
font-size: 0.85rem !important;
}
.editor-actions {
flex-direction: column !important;
gap: 0.5rem !important;
}
.editor-actions .btn {
width: 100% !important;
text-align: center !important;
}
}
/* 中等屏幕适配 */
@media (max-width: 1024px) and (min-width: 769px) {
#editor #editorContainer .CodeMirror {
height: 450px !important;
}
#editor #editorContainer .editormd-preview {
height: 450px !important;
}
} }
.password-hint { .password-hint {
@@ -1874,11 +2140,6 @@
vertical-align: middle; vertical-align: middle;
} }
/* 网络测试页面美化 */
#network-test {
/* 可以考虑将整个 #network-test 作为一个卡片,如果它还没有被 .content-section 样式化为卡片的话 */
}
/* 直接覆盖#testResults.loading的样式防止旋转 */ /* 直接覆盖#testResults.loading的样式防止旋转 */
#network-test #testResults.loading { #network-test #testResults.loading {
animation: none !important; animation: none !important;
@@ -2401,6 +2662,9 @@
</div> </div>
<div class="admin-container" id="adminContainer" style="display: none;"> <div class="admin-container" id="adminContainer" style="display: none;">
<div class="sidebar"> <div class="sidebar">
<div class="sidebar-header">
<h2><i class="fas fa-cogs"></i>管理面板</h2>
</div>
<div class="user-profile"> <div class="user-profile">
<div class="user-avatar"> <div class="user-avatar">
<i class="fas fa-user"></i> <i class="fas fa-user"></i>
@@ -2414,7 +2678,6 @@
<div class="user-action-btn logout" id="logoutBtn">退出登录</div> <div class="user-action-btn logout" id="logoutBtn">退出登录</div>
</div> </div>
</div> </div>
<h2><i class="fas fa-cogs"></i>管理面板</h2>
<ul class="sidebar-nav"> <ul class="sidebar-nav">
<li data-section="dashboard" class="active"> <li data-section="dashboard" class="active">
<i class="fas fa-tachometer-alt"></i>控制面板 <i class="fas fa-tachometer-alt"></i>控制面板
@@ -2518,17 +2781,6 @@
<i class="fas fa-plus"></i> 新建文档 <i class="fas fa-plus"></i> 新建文档
</button> </button>
</div> </div>
<div id="editorContainer" style="display: none;">
<input type="text" id="documentTitle" placeholder="请输入文档标题" autocomplete="off">
<div id="editor">
<!-- 编辑器将在这里初始化 -->
</div>
<div class="editor-actions">
<button type="button" class="btn btn-secondary" onclick="documentManager.cancelEdit()">取消</button>
<button type="button" class="btn btn-primary" onclick="documentManager.saveDocument()">保存文档</button>
</div>
</div>
</div> </div>
<!-- 修改密码部分 --> <!-- 修改密码部分 -->

View File

@@ -394,3 +394,87 @@
.docker-offline-btn.secondary:hover { .docker-offline-btn.secondary:hover {
background-color: #e5e7eb; background-color: #e5e7eb;
} }
/* 简单 Markdown 编辑器样式 */
.editor-toolbar {
background-color: var(--container-bg);
border: 1px solid var(--border-color);
border-bottom: none;
padding: 0.5rem;
border-radius: 0.25rem 0.25rem 0 0;
}
.editor-toolbar .btn {
margin-right: 0.25rem;
margin-bottom: 0.25rem;
}
.editor-content {
border: 1px solid var(--border-color);
border-top: none;
padding: 1rem;
border-radius: 0 0 0.25rem 0.25rem;
background-color: var(--container-bg);
}
#markdown-editor {
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
border: 1px solid var(--border-color);
background-color: var(--background-color);
color: var(--text-color);
resize: vertical;
}
.markdown-preview {
border: 1px solid var(--border-color);
padding: 1rem;
background-color: var(--background-color);
color: var(--text-color);
min-height: 400px;
border-radius: 0.25rem;
overflow-y: auto;
}
.markdown-preview h1, .markdown-preview h2, .markdown-preview h3,
.markdown-preview h4, .markdown-preview h5, .markdown-preview h6 {
color: var(--text-color);
}
.markdown-preview code {
background-color: rgba(0, 0, 0, 0.1);
padding: 0.125rem 0.25rem;
border-radius: 0.125rem;
font-family: 'Consolas', 'Monaco', 'Courier New', monospace;
}
.markdown-preview pre {
background-color: rgba(0, 0, 0, 0.1);
padding: 1rem;
border-radius: 0.25rem;
overflow-x: auto;
}
.markdown-preview blockquote {
border-left: 4px solid var(--border-color);
padding-left: 1rem;
margin-left: 0;
color: var(--text-secondary);
}
.markdown-preview table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
.markdown-preview table th,
.markdown-preview table td {
border: 1px solid var(--border-color);
padding: 0.5rem;
text-align: left;
}
.markdown-preview table th {
background-color: rgba(0, 0, 0, 0.05);
font-weight: bold;
}

View File

@@ -0,0 +1,500 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>文档编辑器 - HubCmdUI</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" type="image/png">
<!-- 引入 Bootstrap CSS -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.2.3/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<!-- 引入 jQuery -->
<script src="https://cdn.bootcss.com/jquery/1.11.3/jquery.min.js"></script>
<!-- 引入 Editor.md -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/editor.md@1.5.0/css/editormd.min.css">
<script src="https://cdn.jsdelivr.net/npm/editor.md@1.5.0/editormd.min.js"></script>
<!-- 引入 SweetAlert2 -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<!-- 自定义样式 -->
<link rel="stylesheet" href="style.css">
<link rel="stylesheet" href="css/admin.css">
<style>
body {
background-color: var(--background-color, #f8f9fa);
font-family: var(--font-family, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif);
margin: 0;
padding: 0;
height: 100vh;
overflow: hidden;
}
.editor-container {
background-color: var(--container-bg, #ffffff);
height: 100vh;
display: flex;
flex-direction: column;
padding: 0;
margin: 0;
}
.editor-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
border-bottom: 2px solid var(--border-light, #e9ecef);
background-color: var(--container-bg, #ffffff);
flex-shrink: 0;
z-index: 10;
}
.editor-title {
color: var(--text-primary, #2c3e50);
font-size: 1.5rem;
font-weight: 600;
margin: 0;
display: flex;
align-items: center;
gap: 0.75rem;
}
.editor-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.document-title-input {
width: 100%;
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
font-weight: 500;
border: none;
border-bottom: 2px solid var(--border-light, #e9ecef);
background-color: var(--container-bg, #ffffff);
color: var(--text-primary, #2c3e50);
transition: border-color 0.3s ease;
flex-shrink: 0;
}
.document-title-input:focus {
outline: none;
border-bottom-color: var(--primary-color, #3d7cfa);
}
.document-title-input::placeholder {
color: var(--text-secondary, #6c757d);
}
/* Editor.md 样式定制 - 铺满全屏 */
.editormd {
border: none !important;
border-radius: 0 !important;
flex: 1;
height: auto !important;
}
/* 编辑器容器铺满剩余空间 */
#editor-md {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* 确保 CodeMirror 和预览区域铺满高度 */
.editormd .editormd-editor,
.editormd .editormd-preview {
height: 100% !important;
}
.CodeMirror {
height: 100% !important;
}
.btn-custom {
padding: 0.5rem 1rem;
font-weight: 500;
border-radius: var(--radius-md, 8px);
transition: all 0.3s ease;
text-decoration: none;
display: inline-flex;
align-items: center;
gap: 0.5rem;
font-size: 0.9rem;
}
.btn-primary-custom {
background-color: var(--primary-color, #3d7cfa);
color: white;
border: 2px solid var(--primary-color, #3d7cfa);
}
.btn-primary-custom:hover {
background-color: var(--primary-dark, #2c5aa0);
border-color: var(--primary-dark, #2c5aa0);
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(61, 124, 244, 0.3);
}
.btn-secondary-custom {
background-color: transparent;
color: var(--text-secondary, #6c757d);
border: 2px solid var(--border-light, #e9ecef);
}
.btn-secondary-custom:hover {
background-color: var(--text-secondary, #6c757d);
color: white;
border-color: var(--text-secondary, #6c757d);
}
.btn-success-custom {
background-color: var(--success-color, #28a745);
color: white;
border: 2px solid var(--success-color, #28a745);
}
.btn-success-custom:hover {
background-color: #218838;
border-color: #218838;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(40, 167, 69, 0.3);
}
/* 响应式设计 */
@media (max-width: 768px) {
.editor-header {
flex-direction: column;
gap: 0.5rem;
align-items: stretch;
padding: 1rem;
}
.editor-title {
font-size: 1.3rem;
text-align: center;
}
.editor-actions {
flex-direction: row;
gap: 0.5rem;
justify-content: center;
}
.btn-custom {
flex: 1;
justify-content: center;
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
.document-title-input {
padding: 0.6rem 1rem;
font-size: 1rem;
}
}
.loading-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
background-color: white;
padding: 2rem;
border-radius: var(--radius-lg, 12px);
text-align: center;
box-shadow: var(--shadow-lg, 0 8px 16px rgba(0, 0, 0, 0.15));
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid var(--primary-color, #3d7cfa);
border-radius: 50%;
width: 40px;
height: 40px;
animation: spin 1s linear infinite;
margin: 0 auto 1rem;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
</style>
</head>
<body>
<div class="editor-container">
<div class="editor-header">
<h1 class="editor-title">
<i class="fas fa-edit"></i>
<span id="pageTitle">新建文档</span>
</h1>
<div class="editor-actions">
<a href="/admin" class="btn-custom btn-secondary-custom">
<i class="fas fa-arrow-left"></i> 返回管理面板
</a>
<button type="button" class="btn-custom btn-success-custom" id="saveBtn">
<i class="fas fa-save"></i> 保存文档
</button>
</div>
</div>
<input
type="text"
id="documentTitle"
class="document-title-input"
placeholder="请输入文档标题..."
autocomplete="off"
>
<div id="editor-md">
<textarea style="display:none;" id="editorContent"></textarea>
</div>
</div>
<!-- 加载覆盖层 -->
<div class="loading-overlay" id="loadingOverlay" style="display: none;">
<div class="loading-spinner">
<div class="spinner"></div>
<p>正在保存文档...</p>
</div>
</div>
<script>
let editor;
let currentDocId = null;
// 从 URL 参数获取文档 ID如果是编辑模式
const urlParams = new URLSearchParams(window.location.search);
const docId = urlParams.get('id');
$(document).ready(function() {
// 初始化 Editor.md
initEditor();
// 如果有文档 ID则加载文档
if (docId) {
loadDocument(docId);
document.getElementById('pageTitle').textContent = '编辑文档';
}
// 绑定保存按钮事件
document.getElementById('saveBtn').addEventListener('click', saveDocument);
// 绑定键盘快捷键 Ctrl+S
document.addEventListener('keydown', function(e) {
if (e.ctrlKey && e.key === 's') {
e.preventDefault();
saveDocument();
}
});
});
function initEditor() {
editor = editormd("editor-md", {
width: "100%",
height: "100%", // 使用 CSS 的 flex 布局控制高度
syncScrolling: "single",
placeholder: "在这里编写您的 Markdown 内容...",
path: "https://cdn.jsdelivr.net/npm/editor.md@1.5.0/lib/",
// 使用官方默认主题
theme: "default",
previewTheme: "default",
editorTheme: "default",
markdown: "",
codeFold: true,
saveHTMLToTextarea: true,
searchReplace: true,
htmlDecode: "style,script,iframe",
emoji: true,
taskList: true,
tocm: true,
tex: false,
flowChart: false,
sequenceDiagram: false,
dialogLockScreen: false,
dialogShowMask: false,
previewCodeHighlight: true,
toolbar: true,
watch: true,
lineNumbers: true,
lineWrapping: false,
autoCloseTags: true,
autoFocus: true,
indentUnit: 4,
// 使用官方默认工具栏配置
onload: function() {
console.log('Editor.md 初始化完成');
},
onchange: function() {
// 标记内容已修改
markAsModified();
}
});
}
function markAsModified() {
const saveBtn = document.getElementById('saveBtn');
if (!saveBtn.classList.contains('modified')) {
saveBtn.classList.add('modified');
saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档 *';
}
}
function markAsSaved() {
const saveBtn = document.getElementById('saveBtn');
saveBtn.classList.remove('modified');
saveBtn.innerHTML = '<i class="fas fa-save"></i> 保存文档';
}
async function loadDocument(id) {
try {
const response = await fetch(`/api/documents/${id}`, {
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error(`加载文档失败: ${response.status}`);
}
const doc = await response.json();
currentDocId = id;
// 设置文档标题
document.getElementById('documentTitle').value = doc.title || '';
// 设置文档内容
if (editor && editor.setMarkdown) {
editor.setMarkdown(doc.content || '');
}
// 更新页面标题
document.title = `编辑文档: ${doc.title} - HubCmdUI`;
} catch (error) {
console.error('加载文档失败:', error);
Swal.fire({
icon: 'error',
title: '加载失败',
text: '无法加载文档内容,请检查网络连接或文档是否存在。',
confirmButtonColor: '#3d7cfa'
});
}
}
async function saveDocument() {
const title = document.getElementById('documentTitle').value.trim();
const content = editor ? editor.getMarkdown() : '';
if (!title) {
Swal.fire({
icon: 'warning',
title: '请输入标题',
text: '文档标题不能为空',
confirmButtonColor: '#3d7cfa'
});
return;
}
if (!content.trim()) {
Swal.fire({
icon: 'warning',
title: '请输入内容',
text: '文档内容不能为空',
confirmButtonColor: '#3d7cfa'
});
return;
}
// 显示加载动画
document.getElementById('loadingOverlay').style.display = 'flex';
try {
const url = currentDocId ? `/api/documents/${currentDocId}` : '/api/documents';
const method = currentDocId ? 'PUT' : 'POST';
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
},
credentials: 'same-origin',
body: JSON.stringify({
title: title,
content: content
})
});
if (!response.ok) {
throw new Error(`保存失败: ${response.status}`);
}
const result = await response.json();
// 如果是新建文档,更新当前文档 ID
if (!currentDocId && result.id) {
currentDocId = result.id;
// 更新 URL
window.history.replaceState({}, '', `?id=${result.id}`);
document.getElementById('pageTitle').textContent = '编辑文档';
}
// 标记为已保存
markAsSaved();
// 更新页面标题
document.title = `编辑文档: ${title} - HubCmdUI`;
// 显示成功消息
Swal.fire({
icon: 'success',
title: '保存成功',
text: currentDocId ? '文档已更新' : '文档已创建',
timer: 2000,
showConfirmButton: false,
toast: true,
position: 'top-end'
});
} catch (error) {
console.error('保存文档失败:', error);
Swal.fire({
icon: 'error',
title: '保存失败',
text: '无法保存文档,请检查网络连接或稍后重试。',
confirmButtonColor: '#3d7cfa'
});
} finally {
// 隐藏加载动画
document.getElementById('loadingOverlay').style.display = 'none';
}
}
// 页面卸载前提醒保存
window.addEventListener('beforeunload', function(e) {
const saveBtn = document.getElementById('saveBtn');
if (saveBtn && saveBtn.classList.contains('modified')) {
e.preventDefault();
e.returnValue = '您有未保存的更改,确定要离开吗?';
return e.returnValue;
}
});
</script>
</body>
</html>

View File

@@ -13,7 +13,7 @@
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-content">
<a href="/" class="logo-link"> <a href="/" class="logo-link">
<img src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" alt="Logo" class="logo"> <img id="mainLogo" src="https://cdn.jsdelivr.net/gh/dqzboy/Blog-Image/BlogCourse/docker-proxy.png" alt="Logo" class="logo" style="opacity: 0; transition: opacity 0.3s ease;">
</a> </a>
<nav class="nav-menu" id="navMenu"> <nav class="nav-menu" id="navMenu">
<!-- 菜单项通过 JavaScript 动态加载 --> <!-- 菜单项通过 JavaScript 动态加载 -->
@@ -1530,7 +1530,11 @@
// 显示指定的文档 // 显示指定的文档
function showDocument(index) { function showDocument(index) {
// console.log('显示文档索引:', index); // 清理之前的返回顶部按钮
const existingBackToTopBtn = document.querySelector('.back-to-top-btn');
if (existingBackToTopBtn) {
existingBackToTopBtn.remove();
}
if (!window.documentationData || !Array.isArray(window.documentationData)) { if (!window.documentationData || !Array.isArray(window.documentationData)) {
console.error('文档数据不可用'); console.error('文档数据不可用');
@@ -1673,74 +1677,192 @@
function renderMarkdownContent(container, doc) { function renderMarkdownContent(container, doc) {
if (!container) return; if (!container) return;
// console.log('渲染Markdown内容:', doc.title, '内容长度:', doc.content ? doc.content.length : 0);
if (doc.content) { if (doc.content) {
// 使用marked渲染Markdown内容 // 使用marked渲染Markdown内容
if (window.marked) { if (window.marked) {
try { try {
// 配置marked选项以获得更好的渲染效果
marked.setOptions({
highlight: function(code, lang) {
// 如果有语法高亮库,可以在这里使用
return code;
},
langPrefix: 'language-',
breaks: true,
gfm: true
});
const rawHtml = marked.parse(doc.content); const rawHtml = marked.parse(doc.content);
// 创建一个临时的根元素来容纳和处理已解析的Markdown内容 // 创建一个临时的根元素来容纳和处理已解析的Markdown内容
const docFragmentRoot = document.createElement('div'); const docFragmentRoot = document.createElement('div');
docFragmentRoot.innerHTML = rawHtml; docFragmentRoot.innerHTML = rawHtml;
// 在这个临时根元素中查找所有的 <pre> 元素 // 为代码块添加语言标识和复制按钮
const preElements = docFragmentRoot.querySelectorAll('pre'); const preElements = docFragmentRoot.querySelectorAll('pre');
preElements.forEach(preElement => { preElements.forEach((preElement, index) => {
const codeElement = preElement.querySelector('code'); const codeElement = preElement.querySelector('code');
let codeToCopy = ''; let codeToCopy = '';
let language = 'Code';
if (codeElement) { if (codeElement) {
codeToCopy = codeElement.textContent; codeToCopy = codeElement.textContent;
// 尝试从className获取语言信息
const className = codeElement.className;
const langMatch = className.match(/language-(\w+)/);
if (langMatch) {
language = langMatch[1].toUpperCase();
}
} else { } else {
codeToCopy = preElement.textContent; codeToCopy = preElement.textContent;
} }
// 设置语言属性用于CSS显示
preElement.setAttribute('data-language', language);
if (codeToCopy.trim() !== '') { if (codeToCopy.trim() !== '') {
const copyButton = document.createElement('button'); const copyButton = document.createElement('button');
copyButton.className = 'copy-btn'; // 应用现有样式 copyButton.className = 'copy-btn';
copyButton.textContent = '复制'; copyButton.innerHTML = '<i class="fas fa-copy"></i> 复制';
copyButton.onclick = function() { // 事件监听器在此处附加到按钮对象 copyButton.onclick = function() {
// console.log('[Tutorial Copy Button] Attempting to copy:', codeToCopy); // 保留此调试日志
copyToClipboard(codeToCopy, this); copyToClipboard(codeToCopy, this);
}; };
preElement.style.position = 'relative'; preElement.appendChild(copyButton);
preElement.appendChild(copyButton); // 按钮被追加到 docFragmentRoot 内的 preElement
} }
}); });
// 为链接添加外部链接图标
const links = docFragmentRoot.querySelectorAll('a');
links.forEach(link => {
const href = link.getAttribute('href');
if (href && (href.startsWith('http') || href.startsWith('https'))) {
link.innerHTML += ' <i class="fas fa-external-link-alt" style="font-size: 0.8em; margin-left: 0.25rem;"></i>';
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer');
}
});
// 为表格添加响应式包装
const tables = docFragmentRoot.querySelectorAll('table');
tables.forEach(table => {
const wrapper = document.createElement('div');
wrapper.className = 'table-wrapper';
wrapper.style.overflowX = 'auto';
wrapper.style.marginBottom = '1.5rem';
table.parentNode.insertBefore(wrapper, table);
wrapper.appendChild(table);
});
// 清空页面上的主容器 // 清空页面上的主容器
container.innerHTML = ''; container.innerHTML = '';
// 创建文档头部
const docHeader = document.createElement('div');
docHeader.className = 'doc-header';
docHeader.innerHTML = `
<h1>${doc.title || '文档标题'}</h1>
${doc.description ? `<p class="doc-description">${doc.description}</p>` : ''}
`;
container.appendChild(docHeader);
// 创建 .doc-content div 并将处理过的文档片段追加进去 // 创建 .doc-content div 并将处理过的文档片段追加进去
const docContentDiv = document.createElement('div'); const docContentDiv = document.createElement('div');
docContentDiv.className = 'doc-content'; docContentDiv.className = 'doc-content';
// 将 docFragmentRoot 的所有子节点移动到 docContentDiv,以避免多余的包裹 div // 将 docFragmentRoot 的所有子节点移动到 docContentDiv
while (docFragmentRoot.firstChild) { while (docFragmentRoot.firstChild) {
docContentDiv.appendChild(docFragmentRoot.firstChild); docContentDiv.appendChild(docFragmentRoot.firstChild);
} }
container.appendChild(docContentDiv); // docContentDiv 现在包含带有活动按钮的 PRE 元素 container.appendChild(docContentDiv);
// 创建并追加 .doc-meta div // 创建并追加 .doc-meta div
const docMetaDiv = document.createElement('div'); const docMetaDiv = document.createElement('div');
docMetaDiv.className = 'doc-meta'; docMetaDiv.className = 'doc-meta';
docMetaDiv.innerHTML = `${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}`; const updateTime = doc.lastUpdated || doc.updatedAt || doc.updated_at;
if (updateTime) {
const formattedDate = new Date(updateTime).toLocaleDateString('zh-CN', {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
docMetaDiv.innerHTML = `
<i class="fas fa-clock"></i>
<span>最后更新: ${formattedDate}</span>
`;
}
container.appendChild(docMetaDiv); container.appendChild(docMetaDiv);
// 添加返回顶部按钮(如果内容很长)
if (docContentDiv.scrollHeight > 1000) {
const backToTopBtn = document.createElement('button');
backToTopBtn.className = 'back-to-top-btn';
backToTopBtn.innerHTML = '<i class="fas fa-arrow-up"></i>';
backToTopBtn.style.cssText = `
position: fixed;
bottom: 2rem;
right: 2rem;
width: 3rem;
height: 3rem;
border-radius: 50%;
background: var(--primary-color);
color: white;
border: none;
cursor: pointer;
box-shadow: 0 4px 12px rgba(61, 124, 244, 0.3);
z-index: 1000;
opacity: 0.8;
transition: all 0.3s ease;
`;
backToTopBtn.onclick = () => {
container.scrollIntoView({ behavior: 'smooth' });
};
backToTopBtn.onmouseenter = () => {
backToTopBtn.style.opacity = '1';
backToTopBtn.style.transform = 'scale(1.1)';
};
backToTopBtn.onmouseleave = () => {
backToTopBtn.style.opacity = '0.8';
backToTopBtn.style.transform = 'scale(1)';
};
document.body.appendChild(backToTopBtn);
// 当切换文档时清理按钮
container.setAttribute('data-back-to-top', 'true');
}
} catch (error) { } catch (error) {
console.error('Markdown解析失败:', error); console.error('Markdown解析失败:', error);
// 发生错误时仍然显示原始Markdown内容 + Meta // 发生错误时的降级处理
container.innerHTML = ` container.innerHTML = `
<div class="doc-content">${doc.content}</div> <div class="doc-header">
<h1>${doc.title || '文档标题'}</h1>
</div>
<div class="doc-content">
<div class="error-container">
<i class="fas fa-exclamation-triangle"></i>
<h3>内容解析失败</h3>
<p>无法正确解析文档内容,显示原始内容:</p>
<pre><code>${doc.content}</code></pre>
</div>
</div>
<div class="doc-meta"> <div class="doc-meta">
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''} ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
</div> </div>
`; `;
} }
} else { } else {
// marked 不可用时,直接显示内容 + Meta // marked 不可用时的降级处理
container.innerHTML = ` container.innerHTML = `
<div class="doc-content">${doc.content}</div> <div class="doc-header">
<h1>${doc.title || '文档标题'}</h1>
</div>
<div class="doc-content">
<div class="markdown-fallback">
<p><em>Markdown 解析器未加载,显示原始内容:</em></p>
<pre><code>${doc.content}</code></pre>
</div>
</div>
<div class="doc-meta"> <div class="doc-meta">
${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''} ${doc.lastUpdated || doc.updatedAt ? `<span>最后更新: ${new Date(doc.lastUpdated || doc.updatedAt).toLocaleDateString('zh-CN')}</span>` : ''}
</div> </div>
@@ -1767,6 +1889,9 @@
// DOMContentLoaded 事件监听器 // DOMContentLoaded 事件监听器
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// 加载系统配置(包括 logo
loadSystemConfig();
// 初始化代理域名 // 初始化代理域名
initProxyDomain(); initProxyDomain();
@@ -1786,6 +1911,38 @@
// 统一调用文档加载函数 // 统一调用文档加载函数
loadAndDisplayDocumentation(); loadAndDisplayDocumentation();
}); });
// 加载系统配置
function loadSystemConfig() {
fetch('/api/config')
.then(response => {
if (response.ok) {
return response.json();
}
// 如果配置加载失败,使用默认配置
return {};
})
.then(config => {
const logoElement = document.getElementById('mainLogo');
if (logoElement) {
// 如果有自定义logo配置且不为空则使用自定义logo
if (config.logo && config.logo.trim() !== '') {
logoElement.src = config.logo;
}
// 如果没有配置或为空保持默认logo不变
// 显示logo无论是默认还是自定义
logoElement.style.opacity = '1';
}
})
.catch(error => {
// 如果出错也要显示默认logo
console.warn('加载配置失败:', error);
const logoElement = document.getElementById('mainLogo');
if (logoElement) {
logoElement.style.opacity = '1';
}
});
}
</script> </script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/marked/2.0.3/marked.min.js"></script>
</body> </body>

View File

@@ -150,6 +150,11 @@ function applySystemConfig(config) {
document.getElementById('proxyDomain').value = config.proxyDomain; document.getElementById('proxyDomain').value = config.proxyDomain;
} }
// 更新logo配置输入框管理页面不显示logo图片只显示配置
if (document.getElementById('logoUrl')) {
document.getElementById('logoUrl').value = config.logo || '';
}
// 应用其他配置... // 应用其他配置...
} }
@@ -477,7 +482,6 @@ function toggleLoadingState(isLoading, elementId, originalText = null) {
// 如果按钮文本没有被修改为 "加载中...",则不需要恢复 // 如果按钮文本没有被修改为 "加载中...",则不需要恢复
} }
} }
// 页面加载时初始化 // 页面加载时初始化
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {
// console.log('DOM已加载正在初始化应用...'); // console.log('DOM已加载正在初始化应用...');

View File

@@ -4,10 +4,6 @@
// 文档列表 // 文档列表
let documents = []; let documents = [];
// 当前正在编辑的文档
let currentDocument = null;
// Markdown编辑器实例
let editorMd = null;
// 创建documentManager对象 // 创建documentManager对象
const documentManager = { const documentManager = {
@@ -179,240 +175,10 @@ const documentManager = {
} }
}, },
// 初始化编辑器
initEditor: function() {
try {
const editorContainer = document.getElementById('editor');
if (!editorContainer) {
// console.error('找不到编辑器容器元素');
return;
}
// 检查 toastui 是否已加载
// console.log('检查编辑器依赖项:', typeof toastui);
// 确保 toastui 对象存在
if (typeof toastui === 'undefined') {
// console.error('Toast UI Editor 未加载');
return;
}
// 创建编辑器实例
editorMd = new toastui.Editor({
el: editorContainer,
height: '600px',
initialValue: '',
previewStyle: 'vertical',
initialEditType: 'markdown',
toolbarItems: [
['heading', 'bold', 'italic', 'strike'],
['hr', 'quote'],
['ul', 'ol', 'task', 'indent', 'outdent'],
['table', 'image', 'link'],
['code', 'codeblock']
]
});
// console.log('编辑器初始化完成', editorMd);
} catch (error) {
// console.error('初始化编辑器出错:', error);
core.showAlert('初始化编辑器失败: ' + error.message, 'error');
}
},
// 检查编辑器是否已初始化
isEditorInitialized: function() {
return editorMd !== null;
},
// 创建新文档 // 创建新文档
newDocument: function() { newDocument: function() {
// 首先确保编辑器已初始化 // 跳转到专门的文档编辑页面
if (!editorMd) { window.open('/document-editor.html', '_blank');
this.initEditor();
// 等待编辑器初始化完成后再继续
setTimeout(() => {
currentDocument = null;
document.getElementById('documentTitle').value = '';
editorMd.setMarkdown('');
this.showEditor();
}, 500);
} else {
currentDocument = null;
document.getElementById('documentTitle').value = '';
editorMd.setMarkdown('');
this.showEditor();
}
},
// 显示编辑器
showEditor: function() {
document.getElementById('documentTable').style.display = 'none';
document.getElementById('editorContainer').style.display = 'block';
if (editorMd) {
// 确保每次显示编辑器时都切换到编辑模式
editorMd.focus();
}
},
// 隐藏编辑器
hideEditor: function() {
document.getElementById('documentTable').style.display = 'table';
document.getElementById('editorContainer').style.display = 'none';
},
// 取消编辑
cancelEdit: function() {
this.hideEditor();
},
// 保存文档
saveDocument: async function() {
const title = document.getElementById('documentTitle').value.trim();
const content = editorMd.getMarkdown();
if (!title) {
core.showAlert('请输入文档标题', 'error');
return;
}
// 显示保存中状态
core.showLoading();
try {
// 简化会话检查逻辑,只验证会话是否有效
let sessionValid = true;
try {
const sessionResponse = await fetch('/api/check-session', {
headers: {
'Cache-Control': 'no-cache',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (sessionResponse.status === 401) {
// console.warn('会话已过期,无法保存文档');
sessionValid = false;
}
} catch (sessionError) {
// console.warn('检查会话状态发生网络错误:', sessionError);
// 发生网络错误时继续尝试保存操作
}
// 只有在会话明确无效时才退出
if (!sessionValid) {
core.showAlert('您的会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
window.location.reload();
}, 1500);
return;
}
// 确保Markdown内容以标题开始
let processedContent = content;
if (!content.startsWith('# ')) {
// 如果内容不是以一级标题开始,则在开头添加标题
processedContent = `# ${title}\n\n${content}`;
} else {
// 如果已经有一级标题,替换为当前标题
processedContent = content.replace(/^# .*$/m, `# ${title}`);
}
const apiUrl = currentDocument && currentDocument.id
? `/api/documents/${currentDocument.id}`
: '/api/documents';
const method = currentDocument && currentDocument.id ? 'PUT' : 'POST';
// console.log(`尝试${method === 'PUT' ? '更新' : '创建'}文档,标题: ${title}`);
const response = await fetch(apiUrl, {
method: method,
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin',
body: JSON.stringify({
title,
content: processedContent,
published: currentDocument && currentDocument.published ? currentDocument.published : false
})
});
// 处理响应
if (response.status === 401) {
// 明确的未授权响应
// console.warn('保存文档返回401未授权');
core.showAlert('未登录或会话已过期,请重新登录', 'warning');
setTimeout(() => {
localStorage.removeItem('isLoggedIn');
window.location.reload();
}, 1500);
return;
}
if (!response.ok) {
const errorText = await response.text();
let errorData;
try {
errorData = JSON.parse(errorText);
} catch (e) {
// 如果不是有效的JSON直接使用文本
throw new Error(errorText || '保存失败,请重试');
}
throw new Error(errorData.error || errorData.message || '保存失败,请重试');
}
const savedDoc = await response.json();
// console.log('保存的文档:', savedDoc);
// 确保savedDoc包含必要的时间字段
if (savedDoc) {
// 如果返回的保存文档中没有时间字段从API获取完整文档信息
if (!savedDoc.createdAt || !savedDoc.updatedAt) {
try {
const docId = savedDoc.id || (currentDocument ? currentDocument.id : null);
if (docId) {
const docResponse = await fetch(`/api/documents/${docId}`, {
headers: { 'Cache-Control': 'no-cache' },
credentials: 'same-origin'
});
if (docResponse.ok) {
const fullDoc = await docResponse.json();
Object.assign(savedDoc, {
createdAt: fullDoc.createdAt,
updatedAt: fullDoc.updatedAt
});
// console.log('获取到完整的文档时间信息:', fullDoc);
}
}
} catch (timeError) {
// console.warn('获取文档完整时间信息失败:', timeError);
}
}
// 更新文档列表中的文档
const existingIndex = documents.findIndex(d => d.id === savedDoc.id);
if (existingIndex >= 0) {
documents[existingIndex] = { ...documents[existingIndex], ...savedDoc };
} else {
documents.push(savedDoc);
}
}
core.showAlert('文档保存成功', 'success');
this.hideEditor();
await this.loadDocuments(); // 重新加载文档列表
} catch (error) {
// console.error('保存文档失败:', error);
core.showAlert('保存文档失败: ' + error.message, 'error');
} finally {
core.hideLoading();
}
}, },
// 渲染文档列表 // 渲染文档列表
@@ -504,84 +270,8 @@ const documentManager = {
// 编辑文档 // 编辑文档
editDocument: async function(id) { editDocument: async function(id) {
try { // 跳转到专门的文档编辑页面并传递文档ID
// console.log(`准备编辑文档ID: ${id}`); window.open(`/document-editor.html?id=${id}`, '_blank');
core.showLoading('加载文档中...'); // 更明确的加载提示
// 会话检查逻辑保持不变
let sessionValid = true;
try {
const sessionResponse = await fetch('/api/check-session', {
headers: {
'Cache-Control': 'no-cache',
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (sessionResponse.status === 401) sessionValid = false;
} catch (sessionError) {
// console.warn('检查会话状态发生网络错误:', sessionError);
// 继续尝试API调用时会再次处理401
}
if (!sessionValid) {
core.showAlert('您的会话已过期,请重新登录', 'warning');
auth.showLoginModal(); // 使用 auth 模块显示登录
core.hideLoading();
return;
}
// 不再依赖本地缓存的列表项获取content始终从API获取完整文档
// console.log('始终从API获取完整文档详情进行编辑ID:', id);
const response = await fetch(`/api/documents/${id}`, {
headers: {
'Cache-Control': 'no-cache', // 确保获取最新数据
'X-Requested-With': 'XMLHttpRequest'
},
credentials: 'same-origin'
});
if (response.status === 401) {
core.showAlert('您的会话已过期或无权限访问此文档,请重新登录', 'warning');
auth.showLoginModal();
core.hideLoading();
return;
}
if (!response.ok) {
const errorText = await response.text();
// console.error(`获取文档失败 (${response.status}):`, errorText);
throw new Error(`获取文档内容失败: ${errorText || response.status}`);
}
const docToEdit = await response.json();
currentDocument = docToEdit; // 更新当前编辑的文档对象
// 确保编辑器已初始化
if (!editorMd) {
this.initEditor();
// 等待编辑器初始化完成后再继续
// 使用短延时确保编辑器DOM完全准备好
await new Promise(resolve => setTimeout(resolve, 100));
}
if (!editorMd) {
core.showAlert('编辑器初始化失败,无法编辑文档。', 'error');
core.hideLoading();
return;
}
document.getElementById('documentTitle').value = docToEdit.title || '';
editorMd.setMarkdown(docToEdit.content || '');
this.showEditor();
} catch (error) {
// console.error('编辑文档时出错:', error);
core.showAlert(`加载文档进行编辑失败: ${error.message}`, 'error');
} finally {
core.hideLoading();
}
}, },
// 查看文档 // 查看文档
@@ -754,89 +444,3 @@ const documentManager = {
} }
} }
}; };
// 全局公开文档管理模块
window.documentManager = documentManager;
/**
* 显示指定文档的内容
* @param {string} docId 文档ID
*/
async function showDocument(docId) {
try {
// console.log('正在获取文档内容ID:', docId);
// 显示加载状态
const documentContent = document.getElementById('documentContent');
if (documentContent) {
documentContent.innerHTML = '<div class="loading-container"><i class="fas fa-spinner fa-spin"></i> 正在加载文档内容...</div>';
}
// 获取文档内容
const response = await fetch(`/api/documentation/${docId}`);
if (!response.ok) {
throw new Error(`获取文档内容失败,状态码: ${response.status}`);
}
const doc = await response.json();
// console.log('获取到文档:', doc);
// 更新文档内容区域
if (documentContent) {
if (doc.content) {
// 使用marked渲染markdown内容
documentContent.innerHTML = `
<h1>${doc.title || '无标题'}</h1>
${doc.lastUpdated ? `<div class="doc-meta">最后更新: ${new Date(doc.lastUpdated).toLocaleDateString('zh-CN')}</div>` : ''}
<div class="doc-content">${window.marked ? marked.parse(doc.content) : doc.content}</div>
`;
} else {
documentContent.innerHTML = `
<h1>${doc.title || '无标题'}</h1>
<div class="empty-content">
<i class="fas fa-file-alt fa-3x"></i>
<p>该文档暂无内容</p>
</div>
`;
}
} else {
// console.error('找不到文档内容容器ID: documentContent');
}
// 高亮当前选中的文档
highlightSelectedDocument(docId);
} catch (error) {
// console.error('获取文档内容失败:', error);
// 显示错误信息
const documentContent = document.getElementById('documentContent');
if (documentContent) {
documentContent.innerHTML = `
<div class="error-container">
<i class="fas fa-exclamation-triangle fa-3x"></i>
<h2>加载失败</h2>
<p>无法获取文档内容: ${error.message}</p>
<button class="btn btn-retry" onclick="showDocument('${docId}')">重试</button>
</div>
`;
}
}
}
/**
* 高亮选中的文档
* @param {string} docId 文档ID
*/
function highlightSelectedDocument(docId) {
// 移除所有高亮
const docLinks = document.querySelectorAll('.doc-list .doc-item');
docLinks.forEach(link => link.classList.remove('active'));
// 添加当前高亮
const selectedLink = document.querySelector(`.doc-list .doc-item[data-id="${docId}"]`);
if (selectedLink) {
selectedLink.classList.add('active');
// 确保选中项可见
selectedLink.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
}

View File

@@ -0,0 +1,321 @@
/**
* HTTP代理管理模块
*/
const httpProxyManager = {
currentConfig: {},
// 初始化代理管理
init: async function() {
try {
console.log('初始化HTTP代理管理...');
await this.loadProxyStatus();
this.bindEvents();
return Promise.resolve();
} catch (error) {
console.error('初始化HTTP代理管理失败:', error);
}
},
// 加载代理状态
loadProxyStatus: async function() {
try {
const response = await fetch('/api/httpProxy/proxy/status');
if (!response.ok) {
throw new Error('获取代理状态失败');
}
const status = await response.json();
this.updateStatusDisplay(status);
// 加载配置
const configResponse = await fetch('/api/httpProxy/proxy/config');
if (configResponse.ok) {
const configData = await configResponse.json();
this.currentConfig = configData.config || {};
this.updateConfigForm();
}
} catch (error) {
console.error('加载代理状态失败:', error);
this.updateStatusDisplay({
isRunning: false,
error: error.message
});
}
},
// 更新状态显示
updateStatusDisplay: function(status) {
const statusElement = document.getElementById('proxyStatus');
const statusBadge = document.getElementById('proxyStatusBadge');
const portInfo = document.getElementById('proxyPortInfo');
if (statusElement) {
if (status.isRunning) {
statusElement.textContent = '运行中';
statusElement.className = 'status-running';
if (statusBadge) {
statusBadge.textContent = '运行中';
statusBadge.className = 'badge badge-success';
}
} else {
statusElement.textContent = '已停止';
statusElement.className = 'status-stopped';
if (statusBadge) {
statusBadge.textContent = '已停止';
statusBadge.className = 'badge badge-secondary';
}
}
}
if (portInfo && status.config) {
portInfo.textContent = `${status.config.host}:${status.config.port}`;
}
},
// 更新配置表单
updateConfigForm: function() {
if (!this.currentConfig) return;
const elements = {
'proxy-port': this.currentConfig.port || 8080,
'proxy-host': this.currentConfig.host || '0.0.0.0',
'proxy-enable-https': this.currentConfig.enableHttps || false,
'proxy-enable-auth': this.currentConfig.enableAuth || false,
'proxy-username': this.currentConfig.username || '',
'proxy-password': this.currentConfig.password || '',
'proxy-log-requests': this.currentConfig.logRequests !== false
};
for (const [id, value] of Object.entries(elements)) {
const element = document.getElementById(id);
if (element) {
if (element.type === 'checkbox') {
element.checked = Boolean(value);
} else {
element.value = value;
}
}
}
// 更新允许和阻止的主机列表
this.updateHostLists();
},
// 更新主机列表显示
updateHostLists: function() {
const allowedList = document.getElementById('allowedHostsList');
const blockedList = document.getElementById('blockedHostsList');
if (allowedList && this.currentConfig.allowedHosts) {
allowedList.innerHTML = this.currentConfig.allowedHosts
.map(host => `<span class="host-tag">${host} <button onclick="httpProxyManager.removeAllowedHost('${host}')">&times;</button></span>`)
.join('');
}
if (blockedList && this.currentConfig.blockedHosts) {
blockedList.innerHTML = this.currentConfig.blockedHosts
.map(host => `<span class="host-tag blocked">${host} <button onclick="httpProxyManager.removeBlockedHost('${host}')">&times;</button></span>`)
.join('');
}
},
// 绑定事件
bindEvents: function() {
// 启动代理按钮
const startBtn = document.getElementById('startProxyBtn');
if (startBtn) {
startBtn.addEventListener('click', () => this.startProxy());
}
// 停止代理按钮
const stopBtn = document.getElementById('stopProxyBtn');
if (stopBtn) {
stopBtn.addEventListener('click', () => this.stopProxy());
}
// 保存配置按钮
const saveBtn = document.getElementById('saveProxyConfigBtn');
if (saveBtn) {
saveBtn.addEventListener('click', () => this.saveConfig());
}
// 测试代理按钮
const testBtn = document.getElementById('testProxyBtn');
if (testBtn) {
testBtn.addEventListener('click', () => this.testProxy());
}
// 添加允许主机
const addAllowedBtn = document.getElementById('addAllowedHostBtn');
if (addAllowedBtn) {
addAllowedBtn.addEventListener('click', () => this.addAllowedHost());
}
// 添加阻止主机
const addBlockedBtn = document.getElementById('addBlockedHostBtn');
if (addBlockedBtn) {
addBlockedBtn.addEventListener('click', () => this.addBlockedHost());
}
},
// 启动代理
startProxy: async function() {
try {
const config = this.getConfigFromForm();
const response = await fetch('/api/httpProxy/proxy/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '启动失败');
}
const result = await response.json();
core.showAlert('代理服务启动成功', 'success');
this.updateStatusDisplay(result.status);
} catch (error) {
console.error('启动代理失败:', error);
core.showAlert('启动代理失败: ' + error.message, 'error');
}
},
// 停止代理
stopProxy: async function() {
try {
const response = await fetch('/api/httpProxy/proxy/stop', {
method: 'POST'
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '停止失败');
}
const result = await response.json();
core.showAlert('代理服务已停止', 'success');
this.updateStatusDisplay(result.status);
} catch (error) {
console.error('停止代理失败:', error);
core.showAlert('停止代理失败: ' + error.message, 'error');
}
},
// 保存配置
saveConfig: async function() {
try {
const config = this.getConfigFromForm();
const response = await fetch('/api/httpProxy/proxy/config', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(config)
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '保存配置失败');
}
const result = await response.json();
this.currentConfig = config;
core.showAlert('代理配置已保存', 'success');
this.updateStatusDisplay(result.status);
} catch (error) {
console.error('保存配置失败:', error);
core.showAlert('保存配置失败: ' + error.message, 'error');
}
},
// 测试代理
testProxy: async function() {
try {
const testUrl = document.getElementById('proxyTestUrl')?.value || 'http://httpbin.org/ip';
const response = await fetch('/api/httpProxy/proxy/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ testUrl })
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || '测试失败');
}
const result = await response.json();
core.showAlert(`代理测试成功 (${result.responseTime})`, 'success');
} catch (error) {
console.error('代理测试失败:', error);
core.showAlert('代理测试失败: ' + error.message, 'error');
}
},
// 从表单获取配置
getConfigFromForm: function() {
return {
port: parseInt(document.getElementById('proxy-port')?.value) || 8080,
host: document.getElementById('proxy-host')?.value || '0.0.0.0',
enableHttps: document.getElementById('proxy-enable-https')?.checked || false,
enableAuth: document.getElementById('proxy-enable-auth')?.checked || false,
username: document.getElementById('proxy-username')?.value || '',
password: document.getElementById('proxy-password')?.value || '',
logRequests: document.getElementById('proxy-log-requests')?.checked !== false,
allowedHosts: this.currentConfig.allowedHosts || [],
blockedHosts: this.currentConfig.blockedHosts || []
};
},
// 添加允许的主机
addAllowedHost: function() {
const input = document.getElementById('newAllowedHost');
const host = input?.value?.trim();
if (host && !this.currentConfig.allowedHosts?.includes(host)) {
if (!this.currentConfig.allowedHosts) {
this.currentConfig.allowedHosts = [];
}
this.currentConfig.allowedHosts.push(host);
this.updateHostLists();
input.value = '';
}
},
// 移除允许的主机
removeAllowedHost: function(host) {
if (this.currentConfig.allowedHosts) {
this.currentConfig.allowedHosts = this.currentConfig.allowedHosts.filter(h => h !== host);
this.updateHostLists();
}
},
// 添加阻止的主机
addBlockedHost: function() {
const input = document.getElementById('newBlockedHost');
const host = input?.value?.trim();
if (host && !this.currentConfig.blockedHosts?.includes(host)) {
if (!this.currentConfig.blockedHosts) {
this.currentConfig.blockedHosts = [];
}
this.currentConfig.blockedHosts.push(host);
this.updateHostLists();
input.value = '';
}
},
// 移除阻止的主机
removeBlockedHost: function(host) {
if (this.currentConfig.blockedHosts) {
this.currentConfig.blockedHosts = this.currentConfig.blockedHosts.filter(h => h !== host);
this.updateHostLists();
}
}
};
// 全局公开模块
window.httpProxyManager = httpProxyManager;

View File

@@ -762,170 +762,547 @@
color: white; color: white;
} }
/* ===== 文档内容区域优化样式 ===== */
#documentationText { #documentationText {
padding: 0.5rem 1.5rem; /* 增加左右内边距 */ padding: 2rem;
max-width: 900px; /* 限制最大宽度以提高可读性 */ max-width: 900px;
/* margin-left: auto; 移除左边距自动 */ line-height: 1.7;
/* margin-right: auto; 移除右边距自动 */ font-size: 16px;
color: #2d3748;
background: #ffffff;
border-radius: var(--radius-lg);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
margin: 1rem 0;
}
/* 标题样式层次化 */
#documentationText h1 {
font-size: 2.5em;
font-weight: 700;
color: #1a202c;
margin: 0 0 1.5rem 0;
padding-bottom: 0.8rem;
border-bottom: 3px solid var(--primary-color);
background: linear-gradient(135deg, var(--primary-color), var(--primary-dark));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
} }
#documentationText h2 { #documentationText h2 {
font-size: 1.8em; font-size: 2em;
margin-top: 2.5em;
margin-bottom: 1em;
border-bottom: 1px solid #eaecef;
padding-bottom: 0.4em;
font-weight: 600; font-weight: 600;
color: #2d3748;
margin: 2.5rem 0 1.2rem 0;
padding: 0.5rem 0 0.5rem 1rem;
border-left: 4px solid var(--primary-color);
border-bottom: 1px solid #e2e8f0;
background: linear-gradient(90deg, rgba(61, 124, 244, 0.05) 0%, transparent 100%);
border-radius: 0 8px 8px 0;
} }
#documentationText h3 {
font-size: 1.5em;
font-weight: 600;
color: #4a5568;
margin: 2rem 0 1rem 0;
padding-left: 0.5rem;
border-left: 3px solid var(--primary-light);
}
#documentationText h4 {
font-size: 1.25em;
font-weight: 600;
color: #4a5568;
margin: 1.5rem 0 0.8rem 0;
}
#documentationText h5 {
font-size: 1.1em;
font-weight: 600;
color: #4a5568;
margin: 1.2rem 0 0.6rem 0;
}
#documentationText h6 {
font-size: 1em;
font-weight: 600;
color: #718096;
margin: 1rem 0 0.5rem 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}
/* 段落和文本样式 */
#documentationText p { #documentationText p {
margin-bottom: 1.5rem; /* 增大段落间距 */
font-size: 1.05rem; /* 稍微增大正文字号 */
}
#documentationText ul, #documentationText ol {
padding-left: 1.8em; /* 调整缩进 */
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
font-size: 1.05rem;
line-height: 1.8;
color: #4a5568;
text-align: justify;
} }
#documentationText li { #documentationText p:first-of-type {
margin-bottom: 0.6rem; font-size: 1.1rem;
color: #2d3748;
font-weight: 400;
} }
/* 列表样式优化 */
#documentationText ul, #documentationText ol {
padding-left: 0;
margin: 1.5rem 0;
list-style: none;
}
#documentationText ul li {
position: relative;
margin-bottom: 0.8rem;
padding-left: 2rem;
line-height: 1.7;
}
#documentationText ul li::before {
content: "▸";
position: absolute;
left: 0.5rem;
color: var(--primary-color);
font-weight: bold;
font-size: 1.1em;
}
#documentationText ol {
counter-reset: ordered-list;
}
#documentationText ol li {
position: relative;
margin-bottom: 0.8rem;
padding-left: 2.5rem;
counter-increment: ordered-list;
line-height: 1.7;
}
#documentationText ol li::before {
content: counter(ordered-list);
position: absolute;
left: 0;
top: 0;
width: 1.8rem;
height: 1.8rem;
background: var(--primary-color);
color: white;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: 600;
font-size: 0.9rem;
}
/* 嵌套列表样式 */
#documentationText ul ul, #documentationText ol ul {
margin: 0.5rem 0;
padding-left: 1.5rem;
}
#documentationText ul ul li::before {
content: "◦";
color: var(--primary-light);
}
/* 代码块样式大幅优化 */
#documentationText pre { #documentationText pre {
background-color: #1F2937; /* 深色背景 */ background: linear-gradient(135deg, #1e293b 0%, #0f172a 100%);
color: #F3F4F6; /* 浅色文字 */ color: #f8fafc;
padding: 1.2rem 1.5rem; /* 调整内边距 */ padding: 0;
border-radius: var(--radius-md); border-radius: 12px;
overflow-x: auto; overflow: hidden;
margin: 1.8rem 0; /* 增加垂直外边距 */ margin: 2rem 0;
line-height: 1.6; /* 调整行高 */ box-shadow:
border: 1px solid #374151; /* 深色边框 */ 0 10px 25px rgba(0, 0, 0, 0.2),
font-family: 'JetBrains Mono', 'Fira Code', Consolas, monospace; /* 使用适合编程的字体 */ 0 4px 6px rgba(0, 0, 0, 0.1),
font-size: 0.95rem; /* 标准化字体大小 */ inset 0 1px 0 rgba(255, 255, 255, 0.1);
position: relative; /* 为复制按钮定位 */ border: 1px solid rgba(255, 255, 255, 0.1);
position: relative;
font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, 'Courier New', monospace;
} }
/* 代码块头部 */
.doc-content pre::before { .doc-content pre::before {
content: ''; /* 模拟终端窗口的顶部栏 */ content: attr(data-language, 'Code');
display: block; display: block;
height: 28px; /* 顶部栏高度 */ padding: 0.8rem 1.5rem;
background-color: #111827; /* 顶部栏颜色 */ background: linear-gradient(90deg, #374151 0%, #4b5563 100%);
border-top-left-radius: var(--radius-md); color: #d1d5db;
border-top-right-radius: var(--radius-md); font-size: 0.875rem;
margin: -1.2rem -1.5rem 1rem -1.5rem; /* 定位和添加下方间距 */ font-weight: 600;
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
position: relative; position: relative;
} }
/* 模拟窗口按钮 */ /* 模拟 macOS 窗口按钮 */
.doc-content pre::after { .doc-content pre::after {
content: ''; content: '';
position: absolute; position: absolute;
top: 8px; top: 0.8rem;
left: 15px; right: 1.5rem;
width: 12px; width: 12px;
height: 12px; height: 12px;
background-color: #FF5F57; background: #ff5f57;
border-radius: 50%; border-radius: 50%;
box-shadow: 20px 0 #FEBC2E, 40px 0 #28C840; box-shadow:
-20px 0 #febc2e,
-40px 0 #28ca42;
} }
.doc-content pre code { .doc-content pre code {
display: block; /* 确保代码块充满 pre */ display: block;
background-color: transparent; padding: 1.5rem;
padding: 0; background: transparent;
margin: 0;
color: inherit; color: inherit;
border-radius: 0;
border: none; border: none;
line-height: inherit; border-radius: 0;
font-family: inherit; font-family: inherit;
white-space: pre; /* 保留空格和换行 */ font-size: 0.95rem;
font-size: inherit; /* 继承 pre 的字号 */ line-height: 1.6;
white-space: pre;
overflow-x: auto;
scrollbar-width: thin;
scrollbar-color: rgba(255, 255, 255, 0.3) transparent;
} }
/* 行内代码样式 */ .doc-content pre code::-webkit-scrollbar {
height: 6px;
}
.doc-content pre code::-webkit-scrollbar-track {
background: transparent;
}
.doc-content pre code::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.3);
border-radius: 3px;
}
.doc-content pre code::-webkit-scrollbar-thumb:hover {
background: rgba(255, 255, 255, 0.5);
}
/* 行内代码样式优化 */
.doc-content code { .doc-content code {
font-family: 'Fira Code', Consolas, 'Liberation Mono', Menlo, Courier, monospace; font-family: 'SF Mono', 'Monaco', 'Cascadia Code', 'Roboto Mono', Consolas, monospace;
font-size: 0.9em; /* 调整大小 */ font-size: 0.9em;
background-color: rgba(61, 124, 244, 0.1); /* 更柔和的背景 */ background: linear-gradient(135deg, rgba(61, 124, 244, 0.08) 0%, rgba(61, 124, 244, 0.12) 100%);
padding: 0.25em 0.5em; color: #3d7cf4;
margin: 0 0.1em; padding: 0.3rem 0.6rem;
border-radius: 4px; margin: 0 0.2rem;
color: #2c5282; /* 主色调的深色 */ border-radius: 6px;
border: 1px solid rgba(61, 124, 244, 0.2); border: 1px solid rgba(61, 124, 244, 0.15);
vertical-align: middle; /* 垂直对齐 */ font-weight: 500;
box-shadow: 0 1px 3px rgba(61, 124, 244, 0.1);
} }
/* 链接样式 */ /* 链接样式优化 */
.doc-content a { .doc-content a {
color: var(--primary-dark); /* 使用更深的蓝色 */ color: var(--primary-color);
text-decoration: underline; text-decoration: none;
text-decoration-color: rgba(61, 124, 244, 0.4); font-weight: 500;
transition: all 0.2s ease; border-bottom: 2px solid transparent;
transition: all 0.3s ease;
position: relative;
}
.doc-content a::after {
content: '';
position: absolute;
bottom: -2px;
left: 0;
width: 0;
height: 2px;
background: linear-gradient(90deg, var(--primary-color), var(--primary-light));
transition: width 0.3s ease;
} }
.doc-content a:hover { .doc-content a:hover {
color: var(--primary-color); color: var(--primary-dark);
text-decoration-color: var(--primary-color); background: rgba(61, 124, 244, 0.05);
background-color: rgba(61, 124, 244, 0.05); padding: 0.2rem 0.4rem;
margin: -0.2rem -0.4rem;
border-radius: 4px;
} }
/* 引用块样式 */ .doc-content a:hover::after {
width: 100%;
}
/* 引用块样式大幅优化 */
.doc-content blockquote { .doc-content blockquote {
margin: 2em 0; margin: 2rem 0;
padding: 1em 1.5em; padding: 1.5rem 2rem;
color: #555; background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
border-left: 4px solid var(--primary-light); border-left: 5px solid var(--primary-color);
background-color: #f8faff; /* 淡蓝色背景 */ border-radius: 0 12px 12px 0;
border-radius: var(--radius-sm); box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
position: relative;
font-style: italic;
}
.doc-content blockquote::before {
content: '"';
position: absolute;
top: -10px;
left: 15px;
font-size: 3rem;
color: var(--primary-light);
font-family: serif;
line-height: 1;
}
.doc-content blockquote p {
margin: 0;
color: #4a5568;
font-size: 1.05rem;
line-height: 1.7;
} }
.doc-content blockquote p:last-child { .doc-content blockquote p:last-child {
margin-bottom: 0; margin-bottom: 0;
} }
/* 表格样式 */ /* 表格样式大幅优化 */
.doc-content table { .doc-content table {
border-collapse: separate; /* 使用 separate 以应用圆角 */ border-collapse: separate;
border-spacing: 0; border-spacing: 0;
margin: 1.8rem 0; margin: 2rem 0;
width: 100%; width: 100%;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.07);
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: var(--radius-md);
overflow: hidden; /* 应用圆角 */
} }
.doc-content th, .doc-content th {
.doc-content td { background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
border-bottom: 1px solid #e2e8f0; color: white;
padding: 0.8em 1.2em; font-weight: 600;
padding: 1rem 1.5rem;
text-align: left; text-align: left;
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.doc-content td {
padding: 1rem 1.5rem;
border-bottom: 1px solid #e2e8f0;
background: white;
color: #4a5568;
}
.doc-content tr:nth-child(even) td {
background: #f8fafc;
} }
.doc-content tr:last-child td { .doc-content tr:last-child td {
border-bottom: none; border-bottom: none;
} }
.doc-content th { .doc-content tr:hover td {
font-weight: 600; background: #eef2ff;
background-color: #f7f9fc; /* 更浅的表头背景 */ color: #2d3748;
color: #4a5568;
} }
.doc-content tr:nth-child(even) td { /* 分隔线样式 */
background-color: #fafcff; /* 斑马纹 */ .doc-content hr {
border: none;
height: 3px;
background: linear-gradient(90deg, transparent, var(--primary-light), transparent);
margin: 3rem 0;
border-radius: 2px;
} }
/* 元数据 (更新时间) 样式 */ /* 图片样式优化 */
.doc-content img {
max-width: 100%;
height: auto;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
margin: 1.5rem 0;
transition: transform 0.3s ease;
}
.doc-content img:hover {
transform: scale(1.02);
}
/* 强调文本样式 */
.doc-content strong {
font-weight: 700;
color: #2d3748;
background: linear-gradient(135deg, rgba(61, 124, 244, 0.1) 0%, rgba(61, 124, 244, 0.05) 100%);
padding: 0.1rem 0.3rem;
border-radius: 3px;
}
.doc-content em {
font-style: italic;
color: var(--primary-dark);
font-weight: 500;
}
/* 复制按钮样式优化 */
.copy-btn {
position: absolute;
top: 0.8rem;
right: 4rem;
background: rgba(255, 255, 255, 0.1);
color: #d1d5db;
border: 1px solid rgba(255, 255, 255, 0.2);
padding: 0.4rem 0.8rem;
border-radius: 6px;
font-size: 0.8rem;
cursor: pointer;
transition: all 0.3s ease;
backdrop-filter: blur(10px);
}
.copy-btn:hover {
background: rgba(255, 255, 255, 0.2);
color: white;
transform: translateY(-1px);
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
}
.copy-btn:active {
transform: translateY(0);
}
/* 元数据样式优化 */
.doc-meta { .doc-meta {
font-size: 0.9em; margin-top: 3rem;
color: #888; padding: 1rem 1.5rem;
text-align: right; /* 右对齐 */ background: linear-gradient(135deg, #f7fafc 0%, #edf2f7 100%);
margin-top: 0rem; /* 调整与内容的距离 */ border-radius: 8px;
padding-top: 0.5rem; /* 增加顶部内边距 */ border-left: 4px solid var(--primary-light);
border-top: 1px dashed #eee; /* 放到顶部 */ font-size: 0.9rem;
clear: both; /* 确保在内容下方 */ color: #718096;
text-align: right;
}
/* 空内容和错误状态样式优化 */
.empty-content, .error-container {
text-align: center;
padding: 4rem 2rem;
color: #a0aec0;
}
.empty-content i, .error-container i {
font-size: 4rem;
margin-bottom: 1rem;
color: #cbd5e0;
}
.empty-content h2, .error-container h2 {
color: #4a5568;
margin-bottom: 0.5rem;
}
.empty-content p, .error-container p {
color: #718096;
}
/* 加载状态样式优化 */
.loading-container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 4rem 2rem;
color: var(--primary-color);
}
.loading-container i {
font-size: 2rem;
margin-bottom: 1rem;
animation: spin 1s linear infinite;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 文档列表样式优化 */
.doc-list {
list-style: none;
padding: 0;
margin: 0;
}
.doc-item {
margin-bottom: 0.5rem;
}
.doc-item a {
display: flex;
align-items: center;
padding: 0.75rem 1rem;
color: #4a5568;
text-decoration: none;
border-radius: 8px;
transition: all 0.3s ease;
border: 1px solid transparent;
}
.doc-item a:hover {
background: rgba(61, 124, 244, 0.05);
color: var(--primary-color);
border-color: rgba(61, 124, 244, 0.2);
transform: translateX(4px);
}
.doc-item a.active {
background: linear-gradient(135deg, var(--primary-color) 0%, var(--primary-dark) 100%);
color: white;
box-shadow: 0 4px 12px rgba(61, 124, 244, 0.3);
}
.doc-item a i {
margin-right: 0.75rem;
width: 1.2rem;
text-align: center;
}
/* 响应式优化 */
@media (max-width: 768px) {
#documentationText {
padding: 1.5rem;
margin: 0.5rem 0;
}
#documentationText h1 {
font-size: 2rem;
}
#documentationText h2 {
font-size: 1.75rem;
margin: 2rem 0 1rem 0;
}
.doc-content pre {
margin: 1.5rem -1.5rem;
border-radius: 0;
}
.doc-content table {
font-size: 0.9rem;
}
.doc-content th,
.doc-content td {
padding: 0.75rem 1rem;
}
} }
/* 加载中和消息提示 */ /* 加载中和消息提示 */