This commit is contained in:
parent
83fb170333
commit
fa37a82b2c
|
|
@ -1,13 +1,20 @@
|
||||||
import socket
|
import socket, json, threading, webbrowser, sys, os, tkinter as tk
|
||||||
import json
|
import pystray # pip install pystray pillow
|
||||||
import threading
|
from pystray import MenuItem as item
|
||||||
import webbrowser
|
from PIL import Image, ImageDraw, ImageTk
|
||||||
import tkinter as tk
|
|
||||||
from tkinter import messagebox
|
|
||||||
|
|
||||||
UDP_PORT = 9876
|
UDP_PORT = 9876
|
||||||
popup_shown = False # 确保只弹一次
|
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):
|
def open_browser(url):
|
||||||
try:
|
try:
|
||||||
webbrowser.open(url)
|
webbrowser.open(url)
|
||||||
|
|
@ -15,52 +22,101 @@ def open_browser(url):
|
||||||
print(f"Failed to open browser: {e}")
|
print(f"Failed to open browser: {e}")
|
||||||
|
|
||||||
def listen_udp():
|
def listen_udp():
|
||||||
global popup_shown
|
global opened_once
|
||||||
|
|
||||||
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||||
sock.bind(("", UDP_PORT))
|
sock.bind(("", UDP_PORT))
|
||||||
|
|
||||||
print(f"Listening for UDP broadcasts on port {UDP_PORT}...")
|
print(f"Listening for UDP broadcasts on port {UDP_PORT}...")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
data, addr = sock.recvfrom(1024)
|
data, _ = sock.recvfrom(1024)
|
||||||
print(f"Received raw data from {addr}: {data}")
|
|
||||||
msg = json.loads(data.decode("utf-8"))
|
msg = json.loads(data.decode("utf-8"))
|
||||||
|
if msg.get("type") != "ai_server_announce":
|
||||||
|
continue
|
||||||
|
|
||||||
if msg.get("type") == "ai_server_announce":
|
ip, port, name = msg.get("ip"), msg.get("port"), msg.get("name", "Unknown")
|
||||||
ip = msg.get("ip")
|
|
||||||
port = msg.get("port")
|
|
||||||
name = msg.get("name", "Unknown")
|
|
||||||
|
|
||||||
url = f"http://{ip}:80"
|
url = f"http://{ip}:80"
|
||||||
print(f"Discovered {name} at {url}")
|
print(f"Discovered {name} at {url}")
|
||||||
|
|
||||||
if not popup_shown:
|
if not opened_once:
|
||||||
popup_shown = True
|
opened_once = True
|
||||||
|
|
||||||
def show_popup():
|
|
||||||
if messagebox.askyesno("发现AI服务器", f"名称: {name}\n地址: {url}\n\n是否打开浏览器访问?"):
|
|
||||||
open_browser(url)
|
open_browser(url)
|
||||||
|
|
||||||
root.after(0, show_popup)
|
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
|
||||||
print("Failed to decode JSON")
|
|
||||||
except Exception as e:
|
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 = tk.Tk()
|
||||||
root.title("AI 服务器发现工具")
|
root.title("PlugAI Launcher")
|
||||||
root.geometry("400x200")
|
|
||||||
label = tk.Label(root, text="正在监听 AI 服务器广播...\n端口: 9876", font=("Arial", 12))
|
|
||||||
label.pack(pady=60)
|
|
||||||
|
|
||||||
# 启动监听线程
|
# ⭐ 窗口标题栏图标(ICO 必须;PNG 需转 PhotoImage)
|
||||||
listener_thread = threading.Thread(target=listen_udp, daemon=True)
|
ico_path = resource_path(ICON_FILE)
|
||||||
listener_thread.start()
|
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("<Unmap>", on_minimize)
|
||||||
|
|
||||||
|
# ───── 启动后台线程 ───────────────────────────────────────────────
|
||||||
|
threading.Thread(target=listen_udp, daemon=True).start()
|
||||||
|
threading.Thread(target=tray_icon.run, daemon=True).start()
|
||||||
|
|
||||||
# 启动 UI 主循环
|
|
||||||
root.mainloop()
|
root.mainloop()
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,37 @@
|
||||||
@echo off
|
@echo off
|
||||||
setlocal
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
rem ───── 配置 ──────────────────────────────────────────────
|
||||||
|
set SCRIPT=PlugAILauncher.py
|
||||||
|
set EXE_NAME=PlugAILauncher
|
||||||
|
set ICON=logo.ico
|
||||||
|
rem ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
echo === Activating virtual environment ===
|
echo === Activating virtual environment ===
|
||||||
call venv310\Scripts\activate.bat
|
call venv310\Scripts\activate.bat || (
|
||||||
if errorlevel 1 (
|
|
||||||
echo [ERROR] Failed to activate virtual environment!
|
echo [ERROR] Failed to activate virtual environment!
|
||||||
pause
|
pause & exit /b 1
|
||||||
exit /b 1
|
|
||||||
)
|
)
|
||||||
|
|
||||||
echo === Installing PyInstaller (if not present) ===
|
echo === Installing PyInstaller (if needed) ===
|
||||||
pip show pyinstaller >nul 2>&1
|
pip show pyinstaller >nul 2>&1 || (
|
||||||
if errorlevel 1 (
|
|
||||||
echo Installing PyInstaller...
|
echo Installing PyInstaller...
|
||||||
pip install pyinstaller
|
pip install -q pyinstaller
|
||||||
) else (
|
|
||||||
echo PyInstaller already installed.
|
|
||||||
)
|
)
|
||||||
|
|
||||||
echo === Building PlugAILauncher.exe ===
|
echo === Building %EXE_NAME%.exe (no-upx) ===
|
||||||
pyinstaller --noconsole --onefile PlugAILauncher.py
|
pyinstaller ^
|
||||||
|
--noconsole ^
|
||||||
|
--onefile ^
|
||||||
|
--noupx ^
|
||||||
|
--name "%EXE_NAME%" ^
|
||||||
|
--icon "%ICON%" ^
|
||||||
|
--add-data "%ICON%;." ^
|
||||||
|
"%SCRIPT%" || (
|
||||||
|
echo [ERROR] PyInstaller build failed!
|
||||||
|
pause & exit /b 1
|
||||||
|
)
|
||||||
|
|
||||||
echo.
|
echo/
|
||||||
echo === Build complete! Check the dist\ directory. ===
|
echo === Build complete! Output: dist\%EXE_NAME%.exe ===
|
||||||
pause
|
pause
|
||||||
|
|
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 28 KiB |
|
|
@ -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 已生成")
|
||||||
Loading…
Reference in New Issue