v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码) - 启动Python管理后台 (端口9531, 15个功能开关) - 统一版本号 0.2.7 - 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
This commit is contained in:
206
cloudsearch_admin/server.py
Normal file
206
cloudsearch_admin/server.py
Normal file
@@ -0,0 +1,206 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
CloudSearch 管理后台 v2.2.0
|
||||
功能开关一键管理 — 支持本地 SQLite + MySQL 双向同步
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import sqlite3
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
from flask import Flask, render_template, request, jsonify
|
||||
|
||||
# ── 日志 ──────────────────────────────────────────────────
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
|
||||
log = logging.getLogger("admin")
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
# ── 配置 ──────────────────────────────────────────────────
|
||||
ADMIN_PORT = int(os.getenv("ADMIN_PORT", "9531"))
|
||||
ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", "admin123")
|
||||
DB_PATH = os.getenv("ADMIN_DB_PATH", "/data/admin_flags.sqlite")
|
||||
|
||||
# MySQL(主应用 system_configs 表,可选)
|
||||
MYSQL_HOST = os.getenv("MYSQL_HOST", "")
|
||||
MYSQL_PORT = int(os.getenv("MYSQL_PORT", "3306"))
|
||||
MYSQL_USER = os.getenv("MYSQL_USER", "")
|
||||
MYSQL_PASS = os.getenv("MYSQL_PASS", "")
|
||||
MYSQL_DB = os.getenv("MYSQL_DB", "cloudsearch")
|
||||
|
||||
# ── 功能开关定义 ──────────────────────────────────────────
|
||||
FEATURES: Dict[str, dict] = {
|
||||
"feature_quark_pid": {"name": "夸克推广PID", "group": "核心", "default": True},
|
||||
"feature_seo": {"name": "SEO / Sitemap", "group": "核心", "default": True},
|
||||
"feature_link_monitor": {"name": "失效链接监控", "group": "核心", "default": True},
|
||||
"feature_tmdb": {"name": "TMDB影视刮削", "group": "增强", "default": True},
|
||||
"feature_telegram_bot": {"name": "Telegram Bot", "group": "增强", "default": False},
|
||||
"feature_subscription": {"name": "关键词订阅通知", "group": "增强", "default": False},
|
||||
"feature_alist": {"name": "AList打通", "group": "增强", "default": False},
|
||||
"feature_transfer_quark": {"name": "夸克转存", "group": "转存", "default": True},
|
||||
"feature_transfer_baidu": {"name": "百度转存", "group": "转存", "default": False},
|
||||
"feature_transfer_aliyun": {"name": "阿里转存", "group": "转存", "default": False},
|
||||
"feature_transfer_uc": {"name": "UC转存", "group": "转存", "default": False},
|
||||
"feature_transfer_xunlei": {"name": "迅雷转存", "group": "转存", "default": False},
|
||||
"feature_transfer_115": {"name": "115转存", "group": "转存", "default": False},
|
||||
"feature_transfer_123": {"name": "123转存", "group": "转存", "default": False},
|
||||
"feature_transfer_cloud189":{"name": "天翼转存", "group": "转存", "default": False},
|
||||
}
|
||||
|
||||
# ── SQLite ────────────────────────────────────────────────
|
||||
def get_db():
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode=WAL")
|
||||
conn.execute("""
|
||||
CREATE TABLE IF NOT EXISTS flags (
|
||||
key TEXT PRIMARY KEY,
|
||||
value INTEGER NOT NULL DEFAULT 0,
|
||||
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
)
|
||||
""")
|
||||
# 初始化默认值
|
||||
for key in FEATURES:
|
||||
conn.execute(
|
||||
"INSERT OR IGNORE INTO flags(key, value) VALUES(?, ?)",
|
||||
(key, int(FEATURES[key]["default"]))
|
||||
)
|
||||
conn.commit()
|
||||
return conn
|
||||
|
||||
def read_flags_sqlite() -> Dict[str, bool]:
|
||||
conn = get_db()
|
||||
rows = conn.execute("SELECT key, value, updated_at FROM flags ORDER BY key").fetchall()
|
||||
conn.close()
|
||||
return {r["key"]: bool(r["value"]) for r in rows}, {r["key"]: r["updated_at"] for r in rows}
|
||||
|
||||
def write_flag_sqlite(key: str, value: bool):
|
||||
conn = get_db()
|
||||
conn.execute(
|
||||
"INSERT INTO flags(key, value, updated_at) VALUES(?, ?, datetime('now')) "
|
||||
"ON CONFLICT(key) DO UPDATE SET value=excluded.value, updated_at=datetime('now')",
|
||||
(key, int(value))
|
||||
)
|
||||
conn.commit()
|
||||
conn.close()
|
||||
|
||||
# ── MySQL 同步 ────────────────────────────────────────────
|
||||
def sync_to_mysql(key: str, value: bool):
|
||||
"""将开关状态同步到主应用的 system_configs 表"""
|
||||
if not MYSQL_HOST:
|
||||
return # MySQL 未配置,跳过
|
||||
try:
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT,
|
||||
user=MYSQL_USER, password=MYSQL_PASS,
|
||||
database=MYSQL_DB, charset="utf8mb4",
|
||||
connect_timeout=5
|
||||
)
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("""
|
||||
INSERT INTO system_configs (config_key, config_value, updated_at)
|
||||
VALUES (%s, %s, NOW())
|
||||
ON DUPLICATE KEY UPDATE config_value=VALUES(config_value), updated_at=NOW()
|
||||
""", (key, "true" if value else "false"))
|
||||
conn.commit()
|
||||
conn.close()
|
||||
log.info(f"MySQL 同步成功: {key} = {value}")
|
||||
except Exception as e:
|
||||
log.warning(f"MySQL 同步失败 ({key}): {e}")
|
||||
|
||||
def read_flags_mysql() -> Optional[Dict[str, bool]]:
|
||||
if not MYSQL_HOST:
|
||||
return None
|
||||
try:
|
||||
import pymysql
|
||||
conn = pymysql.connect(
|
||||
host=MYSQL_HOST, port=MYSQL_PORT,
|
||||
user=MYSQL_USER, password=MYSQL_PASS,
|
||||
database=MYSQL_DB, charset="utf8mb4",
|
||||
connect_timeout=5
|
||||
)
|
||||
with conn.cursor(pymysql.cursors.DictCursor) as cur:
|
||||
cur.execute("SELECT config_key, config_value FROM system_configs WHERE config_key LIKE 'feature_%'")
|
||||
rows = cur.fetchall()
|
||||
conn.close()
|
||||
return {r["config_key"]: r["config_value"].lower() == "true" for r in rows}
|
||||
except Exception as e:
|
||||
log.warning(f"MySQL 读取失败: {e}")
|
||||
return None
|
||||
|
||||
# ── 路由 ──────────────────────────────────────────────────
|
||||
@app.route("/")
|
||||
def index():
|
||||
"""管理后台首页"""
|
||||
return render_template("admin.html", features=FEATURES)
|
||||
|
||||
@app.route("/health")
|
||||
def health():
|
||||
return jsonify({"status": "ok", "service": "cloudsearch-admin"})
|
||||
|
||||
@app.route("/api/flags", methods=["GET"])
|
||||
def api_list_flags():
|
||||
"""列出所有开关"""
|
||||
flags_sqlite, updated = read_flags_sqlite()
|
||||
flags_mysql = read_flags_mysql()
|
||||
|
||||
result = {}
|
||||
for key, meta in FEATURES.items():
|
||||
result[key] = {
|
||||
"name": meta["name"],
|
||||
"group": meta["group"],
|
||||
"value": flags_sqlite.get(key, meta["default"]),
|
||||
"mysql_value": flags_mysql.get(key) if flags_mysql else None,
|
||||
"synced": (flags_mysql is None) or (flags_sqlite.get(key) == flags_mysql.get(key)),
|
||||
"updated_at": updated.get(key, ""),
|
||||
}
|
||||
return jsonify(result)
|
||||
|
||||
@app.route("/api/flags/<key>", methods=["PUT"])
|
||||
def api_set_flag(key):
|
||||
"""设置单个开关"""
|
||||
if key not in FEATURES:
|
||||
return jsonify({"error": f"未知开关: {key}"}), 404
|
||||
|
||||
data = request.get_json(force=True)
|
||||
value = bool(data.get("value", False))
|
||||
|
||||
# 写本地 SQLite
|
||||
write_flag_sqlite(key, value)
|
||||
|
||||
# 同步到 MySQL
|
||||
sync_to_mysql(key, value)
|
||||
|
||||
log.info(f"开关切换: {key} = {value}")
|
||||
return jsonify({"ok": True, "key": key, "value": value})
|
||||
|
||||
@app.route("/api/flags/batch", methods=["PUT"])
|
||||
def api_batch_set_flags():
|
||||
"""批量设置开关"""
|
||||
data = request.get_json(force=True)
|
||||
if not isinstance(data, dict):
|
||||
return jsonify({"error": "请求体需为 {key: value} 字典"}), 400
|
||||
|
||||
results = {}
|
||||
for key, value in data.items():
|
||||
if key not in FEATURES:
|
||||
results[key] = {"error": "未知"}
|
||||
continue
|
||||
val = bool(value)
|
||||
write_flag_sqlite(key, val)
|
||||
sync_to_mysql(key, val)
|
||||
results[key] = val
|
||||
log.info(f"批量切换: {key} = {val}")
|
||||
|
||||
return jsonify({"ok": True, "results": results})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
from waitress import serve
|
||||
log.info(f"管理后台启动: http://0.0.0.0:{ADMIN_PORT}")
|
||||
log.info(f"MySQL 同步: {'已配置' if MYSQL_HOST else '未配置 (仅使用本地 SQLite)'}")
|
||||
serve(app, host="0.0.0.0", port=ADMIN_PORT, threads=4)
|
||||
Reference in New Issue
Block a user