Files

153 lines
6.6 KiB
Python

"""One-click desktop launcher: validate stored data, back it up, then open Huobao."""
from __future__ import annotations
import argparse
from datetime import datetime
import json
import os
from pathlib import Path
import re
import sqlite3
import subprocess
import sys
import time
import urllib.error
import urllib.request
ROOT = Path(__file__).resolve().parent.parent
DATA = ROOT / "huobao-data"
EXE = ROOT / "HuobaoDrama/HuobaoDrama.exe"
PROFILE = Path(os.environ["APPDATA"]) / "HuobaoDrama"
HTTP = urllib.request.build_opener(urllib.request.ProxyHandler({}))
def get_json(url, timeout=2):
with HTTP.open(url, timeout=timeout) as response:
return json.load(response)
def running_app():
try:
log = (PROFILE / "logs/startup.log").read_text("utf-8")
ports = re.findall(r"port=(\d+) userData=", log)
if not ports:
return None
base = "http://127.0.0.1:" + ports[-1] + "/api/v1"
info = get_json(base + "/storage").get("data", {})
if not info.get("dataDir") or info.get("mode") != "desktop":
return None
return base, Path(info["dataDir"]).resolve()
except (OSError, ValueError, urllib.error.URLError):
return None
def validate_data():
if not EXE.is_file():
raise RuntimeError(f"找不到火宝程序:{EXE}")
database = DATA / "huobao.sqlite3"
if not database.is_file():
raise RuntimeError(f"找不到原数据库:{database}\n为避免打开空项目,本次未启动。请先恢复数据目录。")
with sqlite3.connect(database.as_uri() + "?mode=ro", uri=True, timeout=10) as conn:
if conn.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise RuntimeError("数据库检查未通过,本次未启动。请使用备份恢复。")
configs = conn.execute("SELECT count(*) FROM ai_service_configs").fetchone()[0]
projects = conn.execute("SELECT count(*) FROM dramas").fetchone()[0]
if configs == 0:
raise RuntimeError("数据库中没有模型配置,本次未启动,以免误用空数据库。")
grok_enabled = bool(conn.execute("SELECT 1 FROM ai_service_configs WHERE is_active=1 AND model LIKE '%grok-local%' LIMIT 1").fetchone())
return database, configs, projects, grok_enabled
def save_storage_pointer():
PROFILE.mkdir(parents=True, exist_ok=True)
pointer = PROFILE / "storage-config.json"
if pointer.exists():
try:
previous = json.loads(pointer.read_text("utf-8"))
selected = previous.get("dataDir")
except (ValueError, AttributeError):
selected = None
if selected and Path(selected).resolve() != DATA.resolve():
raise RuntimeError(f"火宝目前选择了另一个数据目录:{selected}\n一键启动使用:{DATA}\n请先确认存储位置;原设置未改动。")
if selected:
return
backup = DATA / "backups" / ("storage-pointer-" + datetime.now().strftime("%Y%m%d-%H%M%S") + ".json")
backup.parent.mkdir(parents=True, exist_ok=True)
backup.write_bytes(pointer.read_bytes())
temporary = pointer.with_name(f"storage-config.{os.getpid()}.tmp")
temporary.write_text(json.dumps({"version": 1, "dataDir": str(DATA)}, ensure_ascii=False, indent=2), encoding="utf-8")
temporary.replace(pointer)
def backup_database(database):
folder = DATA / "backups"
folder.mkdir(parents=True, exist_ok=True)
target = folder / ("startup-" + datetime.now().strftime("%Y%m%d-%H%M%S") + f"-{os.getpid()}.sqlite3")
# SQLite's online backup also includes committed WAL changes while Huobao is open.
with sqlite3.connect(database.as_uri() + "?mode=ro", uri=True, timeout=10) as source:
with sqlite3.connect(target) as destination:
source.backup(destination)
if destination.execute("PRAGMA quick_check").fetchone()[0] != "ok":
raise RuntimeError("启动前数据库备份校验失败,本次未启动。")
return target
def ensure_grok():
try:
if get_json("http://127.0.0.1:18793/health").get("service") == "huobao-model-bridge":
return
except (OSError, ValueError, urllib.error.URLError):
pass
pythonw = Path(sys.executable).with_name("pythonw.exe")
runner = pythonw if pythonw.is_file() else Path(sys.executable)
subprocess.Popen([str(runner), str(ROOT / "model-bridge/launch_local.py")],
cwd=ROOT / "model-bridge", creationflags=subprocess.CREATE_NO_WINDOW,
stdin=subprocess.DEVNULL, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--check", action="store_true", help="只检查,不启动或修改配置")
args = parser.parse_args()
database, configs, projects, grok_enabled = validate_data()
active = running_app()
if active and active[1] != DATA.resolve():
raise RuntimeError(f"当前火宝正在使用另一个数据目录:{active[1]}\n请先关闭火宝,再运行一键启动。")
print(f"数据检查通过:{configs} 条模型配置,{projects} 个项目。", flush=True)
if args.check:
print(f"数据目录:{DATA}")
return
save_storage_pointer()
backup = backup_database(database)
env = os.environ.copy()
env.pop("ELECTRON_RUN_AS_NODE", None)
# Huobao's single-instance lock brings its existing window forward.
subprocess.Popen([str(EXE)], cwd=EXE.parent, env=env)
deadline = time.monotonic() + 45
while time.monotonic() < deadline:
current = running_app()
if current:
if current[1] != DATA.resolve():
raise RuntimeError("火宝启动后的数据目录不匹配,请先停止编辑并检查存储设置。")
break
time.sleep(0.5)
else:
raise RuntimeError(f"火宝未能正常启动,请查看:{PROFILE / 'logs/startup.log'}")
if grok_enabled:
ensure_grok()
status = {"started_at": datetime.now().astimezone().isoformat(), "already_running": bool(active),
"data_directory": str(DATA), "database_backup": str(backup), "configs": configs, "projects": projects,
"api": current[0]}
(DATA / "last-launch.json").write_text(json.dumps(status, ensure_ascii=False, indent=2), encoding="utf-8")
print("火宝已打开,启动前数据库备份已保存。", flush=True)
if __name__ == "__main__":
if sys.stdout is not None:
sys.stdout.reconfigure(encoding="utf-8")
try:
main()
except Exception as error:
print(f"\n启动失败:{error}", flush=True)
sys.exit(1)