#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ 神射手 桌面启动器 - 带进度条 - 本地版:Ollama → Docker → MongoDB → pnpm dev → 打开浏览器 - NAS版(--nas):检查 NAS 可达性 → 打开 http://192.168.1.201:3117 """ import os import sys import time import subprocess import signal import threading USE_GUI = True try: import tkinter as tk from tkinter import ttk except ImportError: USE_GUI = False PROJECT = "/Users/karuo/Documents/开发/2、私域银行/神射手" import glob PATH_DIRS = [ "/opt/homebrew/bin", "/usr/local/bin", os.path.expanduser("~/.volta/bin"), ] for d in PATH_DIRS: if os.path.isdir(d): os.environ["PATH"] = d + os.pathsep + os.environ.get("PATH", "") for p in glob.glob(os.path.expanduser("~/.fnm/*/installation/bin")): if os.path.isdir(p): os.environ["PATH"] = p + os.pathsep + os.environ.get("PATH", "") for p in glob.glob(os.path.expanduser("~/.nvm/versions/node/*/bin")): if os.path.isdir(p): os.environ["PATH"] = p + os.pathsep + os.environ.get("PATH", "") server_pid = None _widgets = {} # 存放控件引用 NAS_MODE = "--nas" in sys.argv or "-n" in sys.argv NAS_URL = "http://192.168.1.201:3117" def notify(msg): """发送 macOS 通知(无 GUI 时使用)""" subprocess.run( ["osascript", "-e", f'display notification "{msg}" with title "神射手"'], capture_output=True, timeout=2 ) def set_progress(pct, step, msg): """线程安全更新进度""" if not USE_GUI: return w = _widgets if "pct" in w: w["pct"]["text"] = f"{pct}%" if "step" in w: w["step"]["text"] = step if "msg" in w: w["msg"]["text"] = msg if "progress" in w: w["progress"]["value"] = pct if "root" in w: try: w["root"].update_idletasks() except Exception: pass def port_listen(port): """检测端口是否在监听""" try: r = subprocess.run( ["lsof", "-i", f":{port}", "-sTCP:LISTEN"], capture_output=True, timeout=2 ) return r.returncode == 0 except Exception: return False def http_ok(url, timeout=2): """检测 HTTP 是否可访问""" try: import urllib.request req = urllib.request.Request(url) urllib.request.urlopen(req, timeout=timeout) return True except Exception: return False def run_steps_nas(root, start_time): """NAS 版:检查 NAS 可达性 → 打开浏览器""" def elapsed(): return int(time.time() - start_time) def upd(pct, step, msg): full = f"{msg} (已用时 {elapsed()} 秒)" set_progress(pct, step, full) if not USE_GUI: notify(f"{pct}% | {step}: {msg}") upd(0, "神射手 (NAS)", "检查 NAS 连通性...") if http_ok(NAS_URL, timeout=5): upd(90, "神射手 (NAS)", "NAS 服务可用") else: upd(50, "神射手 (NAS)", "NAS 可能未启动,仍尝试打开...") time.sleep(1) upd(100, "完成", "正在打开浏览器...") subprocess.run(["open", NAS_URL], timeout=3) time.sleep(2) # NAS 版无后台进程,2 秒后自动关闭窗口 if root is not None and "root" in _widgets: try: r = _widgets["root"] r.after(0, r.destroy) except Exception: pass def run_steps(root, start_time): """root 为 None 时表示无 GUI 模式""" global server_pid if NAS_MODE: run_steps_nas(root, start_time) return def elapsed(): return int(time.time() - start_time) def upd(pct, step, msg): full = f"{msg} (已用时 {elapsed()} 秒)" set_progress(pct, step, full) if not USE_GUI: notify(f"{pct}% | {step}: {msg}") try: os.chdir(PROJECT) except Exception as e: upd(0, "错误", str(e)) return # 1. Ollama 本地大模型 (0-15%) upd(0, "本地大模型", "检查 Ollama (localhost:11434)...") if not http_ok("http://localhost:11434/api/tags"): upd(2, "本地大模型", "正在启动 Ollama...") subprocess.run(["open", "-a", "Ollama"], capture_output=True, timeout=5) for i in range(45): if http_ok("http://localhost:11434/api/tags"): break upd(2 + i // 3, "本地大模型", f"等待 Ollama 就绪... ({i+1}s)") time.sleep(1) upd(15, "本地大模型", "Ollama 已就绪") # 2. Docker (15-35%) upd(15, "Docker", "检查 Docker Engine...") subprocess.run(["open", "-a", "Docker"], capture_output=True, timeout=5) for i in range(60): r = subprocess.run(["docker", "info"], capture_output=True, timeout=5) if r.returncode == 0: break upd(15 + i // 3, "Docker", f"等待 Docker Engine... ({i*2}s)") time.sleep(2) upd(35, "Docker", "Docker 已就绪") # 3. MongoDB (35-45%) upd(35, "MongoDB", "启动 MongoDB 容器...") subprocess.run(["docker", "start", "datacenter_mongodb"], capture_output=True, timeout=10) time.sleep(3) upd(45, "MongoDB", "MongoDB 已就绪") # 4. 若 3117 已监听,直接完成 if port_listen(3117): upd(95, "神射手", "服务已在运行") time.sleep(1) subprocess.run(["open", "http://localhost:3117"], timeout=3) upd(100, "完成", "已打开浏览器") return # 5. 启动 pnpm dev (45-90%) upd(45, "神射手", "启动开发服务 (pnpm dev)...") proc = subprocess.Popen( ["pnpm", "dev"], cwd=PROJECT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, start_new_session=True, ) server_pid = proc.pid # 6. 等待 3117 (90-100%) for i in range(90): if port_listen(3117): break pct = 45 + min(44, i * 2) upd(pct, "神射手", f"等待端口 3117 就绪... ({i+1}s)") time.sleep(1) upd(95, "神射手", "正在打开浏览器...") subprocess.run(["open", "http://localhost:3117"], timeout=3) upd(100, "完成", "神射手已启动") # 有 GUI 时保持进程直到窗口关闭;无 GUI 时直接退出(pnpm 在后台继续运行) if root is not None: try: proc.wait() except Exception: pass def main(): global server_pid if not USE_GUI: run_steps(None, time.time()) return try: root = tk.Tk() root.title("神射手 (NAS) - 启动中" if NAS_MODE else "神射手 - 启动中") root.geometry("420x180") root.resizable(False, False) f = ttk.Frame(root, padding=16) f.pack(fill=tk.BOTH, expand=True) title = "神射手 (NAS) 数据中台" if NAS_MODE else "神射手 数据中台" subtitle = "NAS 192.168.1.201:3117" if NAS_MODE else "Ollama → Docker → MongoDB → 神射手" ttk.Label(f, text=title, font=("", 14, "bold")).pack(anchor=tk.W) ttk.Label(f, text=subtitle, font=("", 9)).pack(anchor=tk.W) f2 = ttk.Frame(f) f2.pack(fill=tk.X, pady=(12, 0)) lb_pct = ttk.Label(f2, text="0%", font=("", 11, "bold")) lb_pct.pack(side=tk.RIGHT) lb_step = ttk.Label(f2, text="初始化", font=("", 10)) lb_step.pack(anchor=tk.W) lb_msg = ttk.Label(f2, text="准备启动...", font=("", 9)) lb_msg.pack(anchor=tk.W) p = ttk.Progressbar(f2, length=380, mode="determinate") p.pack(fill=tk.X, pady=(4, 0)) p["value"] = 0 _widgets["root"] = root _widgets["pct"] = lb_pct _widgets["step"] = lb_step _widgets["msg"] = lb_msg _widgets["progress"] = p def on_close(): global server_pid if server_pid and not NAS_MODE: try: os.kill(server_pid, signal.SIGTERM) except Exception: pass root.destroy() root.protocol("WM_DELETE_WINDOW", on_close) root.update_idletasks() except Exception: run_steps(None, time.time()) return t = threading.Thread(target=lambda: run_steps(root, time.time())) t.daemon = True t.start() try: root.mainloop() except Exception: pass if server_pid and t.is_alive(): try: os.kill(server_pid, signal.SIGTERM) except Exception: pass if __name__ == "__main__": main()