diff --git a/Windows-client/PlugAILauncher.py b/Windows-client/PlugAILauncher.py index 0171abc..591d7de 100644 --- a/Windows-client/PlugAILauncher.py +++ b/Windows-client/PlugAILauncher.py @@ -1,13 +1,20 @@ -import socket -import json -import threading -import webbrowser -import tkinter as tk -from tkinter import messagebox +import socket, json, threading, webbrowser, sys, os, tkinter as tk +import pystray # pip install pystray pillow +from pystray import MenuItem as item +from PIL import Image, ImageDraw, ImageTk -UDP_PORT = 9876 -popup_shown = False # 确保只弹一次 +UDP_PORT = 9876 +opened_once = False +WIN_W, WIN_H = 400, 200 +ICON_FILE = "logo.ico" # ← 你的图标文件名(ico/png 都行) +# ───── 通用资源路径(兼容 PyInstaller) ─────────────────────────── +def resource_path(rel_path: str): + """Return absolute path to resource, works for dev & PyInstaller""" + base = getattr(sys, "_MEIPASS", os.path.abspath(".")) + return os.path.join(base, rel_path) + +# ───── 收到广播就跳浏览器 ─────────────────────────────────────────── def open_browser(url): try: webbrowser.open(url) @@ -15,52 +22,101 @@ def open_browser(url): print(f"Failed to open browser: {e}") def listen_udp(): - global popup_shown - + global opened_once sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(("", UDP_PORT)) - print(f"Listening for UDP broadcasts on port {UDP_PORT}...") while True: try: - data, addr = sock.recvfrom(1024) - print(f"Received raw data from {addr}: {data}") + data, _ = sock.recvfrom(1024) msg = json.loads(data.decode("utf-8")) + if msg.get("type") != "ai_server_announce": + continue - if msg.get("type") == "ai_server_announce": - ip = msg.get("ip") - port = msg.get("port") - name = msg.get("name", "Unknown") + ip, port, name = msg.get("ip"), msg.get("port"), msg.get("name", "Unknown") + url = f"http://{ip}:80" + print(f"Discovered {name} at {url}") - url = f"http://{ip}:80" - print(f"Discovered {name} at {url}") - - if not popup_shown: - popup_shown = True - - def show_popup(): - if messagebox.askyesno("发现AI服务器", f"名称: {name}\n地址: {url}\n\n是否打开浏览器访问?"): - open_browser(url) - - root.after(0, show_popup) - - except json.JSONDecodeError: - print("Failed to decode JSON") + if not opened_once: + opened_once = True + open_browser(url) except Exception as e: - print(f"Error while receiving UDP: {e}") + print(f"UDP error: {e}") -# 初始化 Tkinter 主窗口(显示) +# ───── 托盘辅助 ─────────────────────────────────────────────────── +def load_icon_img(): + """Load ICO/PNG; fall back to blue square if文件缺失/损坏""" + try: + return Image.open(resource_path(ICON_FILE)) + except Exception as e: + print(f"[warn] load icon failed: {e}, fallback to default") + return create_fallback_icon() + +def create_fallback_icon(size=(64, 64), bg="#004080", fg="#ffffff"): + img = Image.new("RGB", size, bg) + ImageDraw.Draw(img).rectangle([16,16,48,48], fill=fg) + return img + +def show_window(icon=None, _=None): + root.deiconify(); root.focus_force() + if icon: icon.visible = False + +def hide_to_tray(): + root.withdraw(); tray_icon.visible = True + +def on_exit(icon=None, _=None): + if icon: icon.stop() + root.quit(); sys.exit() + +tray_icon = pystray.Icon( + "ai_discovery", + icon = load_icon_img(), # ⭐ 自定义托盘图标 + title = "PlugAI Launcher", + menu = pystray.Menu( + item("显示窗口", show_window), + item("退出程序", on_exit) + ) +) + +# ───── 主窗口 ───────────────────────────────────────────────────── root = tk.Tk() -root.title("AI 服务器发现工具") -root.geometry("400x200") -label = tk.Label(root, text="正在监听 AI 服务器广播...\n端口: 9876", font=("Arial", 12)) -label.pack(pady=60) +root.title("PlugAI Launcher") -# 启动监听线程 -listener_thread = threading.Thread(target=listen_udp, daemon=True) -listener_thread.start() +# ⭐ 窗口标题栏图标(ICO 必须;PNG 需转 PhotoImage) +ico_path = resource_path(ICON_FILE) +if ico_path.lower().endswith(".ico"): + try: + root.iconbitmap(ico_path) + except Exception as e: + print(f"[warn] set window icon failed: {e}") +else: # PNG fallback + try: + img = ImageTk.PhotoImage(file=ico_path) + root.iconphoto(False, img) + except Exception as e: + print(f"[warn] set window icon failed: {e}") + +def center_window(): + sw, sh = root.winfo_screenwidth(), root.winfo_screenheight() + x, y = (sw - WIN_W) // 2, (sh - WIN_H) // 2 + root.geometry(f"{WIN_W}x{WIN_H}+{x}+{y}") + +center_window() +tk.Label(root, text="正在等待 PlugAI 准备好中...\n端口: 9876", + font=("Arial", 12)).pack(pady=60) + +root.protocol("WM_DELETE_WINDOW", on_exit) + +def on_minimize(event): + if root.state() == "iconic": + hide_to_tray() + +root.bind("", on_minimize) + +# ───── 启动后台线程 ─────────────────────────────────────────────── +threading.Thread(target=listen_udp, daemon=True).start() +threading.Thread(target=tray_icon.run, daemon=True).start() -# 启动 UI 主循环 root.mainloop() diff --git a/Windows-client/build.bat b/Windows-client/build.bat index 16d68f5..ae2f566 100644 --- a/Windows-client/build.bat +++ b/Windows-client/build.bat @@ -1,26 +1,37 @@ @echo off -setlocal +setlocal enabledelayedexpansion + +rem ───── 配置 ────────────────────────────────────────────── +set SCRIPT=PlugAILauncher.py +set EXE_NAME=PlugAILauncher +set ICON=logo.ico +rem ─────────────────────────────────────────────────────── echo === Activating virtual environment === -call venv310\Scripts\activate.bat -if errorlevel 1 ( +call venv310\Scripts\activate.bat || ( echo [ERROR] Failed to activate virtual environment! - pause - exit /b 1 + pause & exit /b 1 ) -echo === Installing PyInstaller (if not present) === -pip show pyinstaller >nul 2>&1 -if errorlevel 1 ( +echo === Installing PyInstaller (if needed) === +pip show pyinstaller >nul 2>&1 || ( echo Installing PyInstaller... - pip install pyinstaller -) else ( - echo PyInstaller already installed. + pip install -q pyinstaller ) -echo === Building PlugAILauncher.exe === -pyinstaller --noconsole --onefile PlugAILauncher.py +echo === Building %EXE_NAME%.exe (no-upx) === +pyinstaller ^ + --noconsole ^ + --onefile ^ + --noupx ^ + --name "%EXE_NAME%" ^ + --icon "%ICON%" ^ + --add-data "%ICON%;." ^ + "%SCRIPT%" || ( + echo [ERROR] PyInstaller build failed! + pause & exit /b 1 +) -echo. -echo === Build complete! Check the dist\ directory. === +echo/ +echo === Build complete! Output: dist\%EXE_NAME%.exe === pause diff --git a/Windows-client/logo.ico b/Windows-client/logo.ico new file mode 100644 index 0000000..d998ec9 Binary files /dev/null and b/Windows-client/logo.ico differ diff --git a/Windows-client/logo.png b/Windows-client/logo.png new file mode 100644 index 0000000..1e305f8 Binary files /dev/null and b/Windows-client/logo.png differ diff --git a/Windows-client/png2ico.py b/Windows-client/png2ico.py new file mode 100644 index 0000000..a9587f5 --- /dev/null +++ b/Windows-client/png2ico.py @@ -0,0 +1,6 @@ +from PIL import Image + +src = Image.open("logo.png") # 任意高分辨率 PNG +sizes = [(16,16), (24,24), (32,32), (48,48), (64,64), (128,128), (256,256)] +src.save("logo.ico", sizes=sizes) +print("logo.ico 已生成")