feat: WFJ Panel v0.1.0 - 完整功能版

- SSH 服务器连接管理 + 登录页自动预填
- 服务器概览监控(CPU/内存/硬盘/网络/进程,进程卡片支持排序和端口显示)
- 远程文件管理(浏览/上传/下载/新建/删除,实时进度条)
- 交互式终端(xterm.js + ssh2 PTY)
- Docker 容器管理(三连接架构,概览/容器列表/镜像/应用商店/任务中心/配置/网络/卷)
- 应用商店一键部署(Redis/MySQL/Halo,在线模板同步,数据驱动 options/showWhen/extraArgs)
- Docker 镜像源配置(daemon.json 编辑 + 自动重启)
- Docker 强制停止修复(三步强制停止 + 轮询确认)
- 系统防火墙管理(firewalld/ufw/iptables 自动检测 + GUI 管理)
- 客户端版本检查(/api/version 接口 + 更新弹窗)
- 品牌名统一为 WFJ Panel
- electron-builder 打包配置(NSIS 安装包,国内镜像)
- .npmrc 配置 Electron 国内镜像
This commit is contained in:
2026-09-08 01:25:54 +08:00
parent e929d4842e
commit 90bc4bd398
46 changed files with 14841 additions and 57 deletions
+234
View File
@@ -0,0 +1,234 @@
"""
应用商店 HTTP API 服务
从 MySQL 读取应用模板数据,返回 JSON 给 Electron 客户端
所有数据库连接参数通过环境变量配置
"""
import os
import json
from flask import Flask, jsonify, request
from flask_cors import CORS
import mysql.connector
app = Flask(__name__)
# 允许跨域(Electron 渲染进程直接 fetch 时需要)
CORS(app)
# ─── 数据库配置(环境变量) ───
DB_HOST = os.environ.get('DB_HOST', '127.0.0.1')
DB_PORT = int(os.environ.get('DB_PORT', '3306'))
DB_USER = os.environ.get('DB_USER', 'root')
DB_PASSWORD = os.environ.get('DB_PASSWORD', '123456')
DB_NAME = os.environ.get('DB_NAME', 'appstore')
# API 监听端口
API_PORT = int(os.environ.get('API_PORT', '3001'))
# API 鉴权 token(可选,客户端通过 Authorization header 传递)
API_TOKEN = os.environ.get('API_TOKEN', '')
def get_db():
"""获取数据库连接"""
return mysql.connector.connect(
host=DB_HOST,
port=DB_PORT,
user=DB_USER,
password=DB_PASSWORD,
database=DB_NAME,
charset='utf8mb4'
)
def check_token():
"""校验 API token,未配置 token 时跳过"""
if not API_TOKEN:
return True
auth = request.headers.get('Authorization', '')
return auth == f'Bearer {API_TOKEN}'
def fetch_template_list():
"""查询所有模板(列表摘要)"""
conn = get_db()
cursor = conn.cursor(dictionary=True)
cursor.execute(
'SELECT id, app_key, name, description, icon, category, '
'image, container_name, sort_order, is_active '
'FROM app_templates WHERE is_active = 1 ORDER BY sort_order, id'
)
rows = cursor.fetchall()
cursor.close()
conn.close()
return rows
def fetch_template_detail(app_key):
"""查询单个模板的完整详情(含子表)"""
conn = get_db()
cursor = conn.cursor(dictionary=True)
# 主表
cursor.execute('SELECT * FROM app_templates WHERE app_key = %s AND is_active = 1', (app_key,))
tpl = cursor.fetchone()
if not tpl:
cursor.close()
conn.close()
return None
# 子表批量查询
sub_tables = ['app_ports', 'app_volumes', 'app_envs', 'app_mkdirs', 'app_write_files', 'app_params']
for table in sub_tables:
cursor.execute(f'SELECT * FROM {table} WHERE template_id = %s ORDER BY sort_order', (tpl['id'],))
rows = cursor.fetchall()
# 序列化:去掉 template_id 和 id,JSON 友好
cleaned = []
for r in rows:
item = {k: v for k, v in r.items() if k not in ('id', 'template_id')}
cleaned.append(item)
# 子表名映射到 deploy 对应字段
field_map = {
'app_ports': 'ports',
'app_volumes': 'volumes',
'app_envs': 'env',
'app_mkdirs': 'mkdirs',
'app_write_files': 'writeFiles',
'app_params': 'params'
}
tpl[field_map[table]] = cleaned
# 删除不需要的主表字段
tpl.pop('id', None)
tpl.pop('is_active', None)
tpl.pop('sort_order', None)
tpl.pop('created_at', None)
tpl.pop('updated_at', None)
cursor.close()
conn.close()
return tpl
def fetch_all_templates_full():
"""一次性查询所有模板的完整详情"""
conn = get_db()
cursor = conn.cursor(dictionary=True)
# 所有主表记录
cursor.execute(
'SELECT * FROM app_templates WHERE is_active = 1 ORDER BY sort_order, id'
)
templates = cursor.fetchall()
sub_tables = ['app_ports', 'app_volumes', 'app_envs', 'app_mkdirs', 'app_write_files', 'app_params']
field_map = {
'app_ports': 'ports',
'app_volumes': 'volumes',
'app_envs': 'env',
'app_mkdirs': 'mkdirs',
'app_write_files': 'writeFiles',
'app_params': 'params'
}
for tpl in templates:
tpl_id = tpl['id']
for table in sub_tables:
cursor.execute(
f'SELECT * FROM {table} WHERE template_id = %s ORDER BY sort_order',
(tpl_id,)
)
rows = cursor.fetchall()
cleaned = []
for r in rows:
item = {k: v for k, v in r.items() if k not in ('id', 'template_id')}
cleaned.append(item)
tpl[field_map[table]] = cleaned
# 清理主表多余字段
tpl.pop('id', None)
tpl.pop('is_active', None)
tpl.pop('sort_order', None)
tpl.pop('created_at', None)
tpl.pop('updated_at', None)
cursor.close()
conn.close()
return templates
# ─── API 路由 ───
@app.route('/api/apps', methods=['GET'])
def list_apps():
"""获取所有应用模板列表(完整详情)"""
if not check_token():
return jsonify({'ok': False, 'error': '未授权'}), 401
try:
templates = fetch_all_templates_full()
return jsonify({'ok': True, 'data': templates, 'count': len(templates)})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@app.route('/api/apps/<app_key>', methods=['GET'])
def get_app(app_key):
"""获取单个应用模板详情"""
if not check_token():
return jsonify({'ok': False, 'error': '未授权'}), 401
try:
tpl = fetch_template_detail(app_key)
if not tpl:
return jsonify({'ok': False, 'error': '模板不存在'}), 404
return jsonify({'ok': True, 'data': tpl})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
@app.route('/api/health', methods=['GET'])
def health():
"""健康检查"""
return jsonify({'ok': True, 'status': 'running'})
@app.route('/api/version', methods=['GET'])
def get_version():
"""获取客户端最新版本信息"""
if not check_token():
return jsonify({'ok': False, 'error': '未授权'}), 401
try:
conn = get_db()
cursor = conn.cursor(dictionary=True)
cursor.execute(
'SELECT version, download_url, release_notes, is_latest, '
'created_at FROM app_version WHERE is_latest = 1 ORDER BY id DESC LIMIT 1'
)
row = cursor.fetchone()
cursor.close()
conn.close()
if not row:
return jsonify({'ok': False, 'error': '暂无版本信息'}), 404
return jsonify({
'ok': True,
'data': {
'version': row['version'],
'downloadUrl': row['download_url'],
'releaseNotes': row['release_notes'] or '',
'isLatest': bool(row['is_latest']),
'createdAt': row['created_at'].isoformat() if row['created_at'] else None,
}
})
except Exception as e:
return jsonify({'ok': False, 'error': str(e)}), 500
if __name__ == '__main__':
print(f'[AppStore API] DB: {DB_HOST}:{DB_PORT}/{DB_NAME}')
print(f'[AppStore API] Listen on :{API_PORT}')
print(f'[AppStore API] Token auth: {"on" if API_TOKEN else "off"}')
app.run(host='0.0.0.0', port=API_PORT, debug=False)