Files
ssh_panel/appstore-server/app.py
T
AKAxedx 90bc4bd398 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 国内镜像
2026-09-08 01:25:54 +08:00

235 lines
7.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
应用商店 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)