Files
CloudSearch/cloudsearch_admin/server.py
admin 83cbfaf03f v0.2.7: 修复Redis连接 + 启动管理后台
- 修复Redis认证 (配置密码)
- 启动Python管理后台 (端口9531, 15个功能开关)
- 统一版本号 0.2.7
- 更新docker-compose.yml (镜像版本/Redis URL/Admin服务)
2026-05-17 02:22:18 +08:00

207 lines
8.3 KiB
Python
Raw Permalink 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.
#!/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)