Files
users/scripts/launch_progress.py

288 lines
8.4 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
# -*- 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()