From c287601feca1ad226df929a2900ccdf90085c110 Mon Sep 17 00:00:00 2001 From: tqjason <37337136+tqjason@users.noreply.github.com> Date: Thu, 7 May 2026 20:05:06 +0800 Subject: [PATCH] =?UTF-8?q?feat(pet):=20=E6=94=AF=E6=8C=81=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E5=90=AF=E5=8A=A8=E5=A4=9A=E4=B8=AA=E5=AE=A0=E7=89=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontends/desktop_pet_v2.pyw | 45 +++++++++++++------------- frontends/stapp.py | 63 ++++++++++++++++++++++++------------ 2 files changed, 66 insertions(+), 42 deletions(-) diff --git a/frontends/desktop_pet_v2.pyw b/frontends/desktop_pet_v2.pyw index 653aef6b..396010b2 100644 --- a/frontends/desktop_pet_v2.pyw +++ b/frontends/desktop_pet_v2.pyw @@ -4,7 +4,6 @@ from http.server import HTTPServer, BaseHTTPRequestHandler from urllib.parse import urlparse, parse_qs from PIL import Image, ImageDraw, ImageFont, ImageOps -PORT = 41983 SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) PROJECT_DIR = os.path.dirname(SCRIPT_DIR) SKINS_DIR = os.path.join(SCRIPT_DIR, 'skins') @@ -240,7 +239,7 @@ class PetBase: self._schedule_main(lambda m=message: self.show_toast(m)) def _start_server(self): - """Start HTTP control server.""" + """Start HTTP control server with dynamic port allocation.""" pet = self class Handler(BaseHTTPRequestHandler): @@ -280,16 +279,29 @@ class PetBase: def log_message(self, *a): pass - try: - HTTPServer.allow_reuse_address = True - srv = HTTPServer(('127.0.0.1', PORT), Handler) + # Find an available port starting from 41983 + HTTPServer.allow_reuse_address = True + assigned_port = None + for p in range(41983, 42000): + try: + srv = HTTPServer(('127.0.0.1', p), Handler) + assigned_port = p + break + except OSError as e: + if e.errno == 48: + print(f'⚠ Port {p} already in use, try next port') + continue + else: + raise + + if assigned_port: threading.Thread(target=srv.serve_forever, daemon=True).start() - print(f'✓ Server: http://127.0.0.1:{PORT}/?state=walk') - except OSError as e: - if e.errno == 48: - print(f'⚠ Port {PORT} already in use') - else: - raise + print(f'✓ Server: http://127.0.0.1:{assigned_port}/?state=walk') + print(f'__PET_PORT_READY__:{assigned_port}') + sys.stdout.flush() + else: + print('⚠ Failed to bind any port') + raise # ============================================================================ @@ -1068,17 +1080,6 @@ else: self.app.exec() if __name__ == '__main__': - # Singleton: if port already in use, another instance is running - import socket - _s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - try: - _s.connect(('127.0.0.1', PORT)) - _s.close() - print(f'⚠ Pet already running on port {PORT}, exiting.') - sys.exit(0) - except ConnectionRefusedError: - pass - if sys.platform == 'darwin': pet = MacPet('vita') pet.run() diff --git a/frontends/stapp.py b/frontends/stapp.py index 5f59c570..c36bced1 100644 --- a/frontends/stapp.py +++ b/frontends/stapp.py @@ -1,4 +1,4 @@ -import os, sys, subprocess +import os, sys, subprocess, atexit from urllib.request import urlopen from urllib.parse import quote if sys.stdout is None: sys.stdout = open(os.devnull, "w") @@ -71,25 +71,48 @@ def render_sidebar(): st.toast(f"Tools injected") except Exception as e: st.toast(f"Injected tools failed: {e}") if st.button(T('desktop_pet')): - kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {} - pet_script = os.path.join(script_dir, 'desktop_pet_v2.pyw') - if not os.path.exists(pet_script): pet_script = os.path.join(script_dir, 'desktop_pet.pyw') - subprocess.Popen([sys.executable, pet_script], **kwargs) - def _pet_req(q): - def _do(): - try: urlopen(f'http://127.0.0.1:41983/?{q}', timeout=2) - except Exception: pass - threading.Thread(target=_do, daemon=True).start() - agent._pet_req = _pet_req - if not hasattr(agent, '_turn_end_hooks'): agent._turn_end_hooks = {} - def _pet_hook(ctx): - parts = [f"Turn {ctx.get('turn','?')}"] - if ctx.get('summary'): parts.append(ctx['summary']) - if ctx.get('exit_reason'): parts.append('DONE') - _pet_req(f'msg={quote(chr(10).join(parts))}') - if ctx.get('exit_reason'): _pet_req('state=idle') - agent._turn_end_hooks['pet'] = _pet_hook - st.toast("Desktop pet started") + if not getattr(agent, '_pet_running', False): + kwargs = {'creationflags': 0x08} if sys.platform == 'win32' else {} + pet_script = os.path.join(script_dir, 'desktop_pet_v2.pyw') + if not os.path.exists(pet_script): pet_script = os.path.join(script_dir, 'desktop_pet.pyw') + proc = subprocess.Popen([sys.executable, pet_script], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, **kwargs) + atexit.register(proc.kill) + + def _listen_port(): + if proc.stdout: + for line in iter(proc.stdout.readline, b''): + try: + line_str = line.decode('utf-8', errors='replace').strip() + if line_str.startswith('__PET_PORT_READY__:'): + agent._pet_port = int(line_str.split(':')[1]) + break + if '⚠ Failed to bind' in line_str: + break + except: pass + if getattr(agent, '_pet_port', 0) > 0: + proc.wait() # 等待子进程退出 + agent._pet_port = 0 + agent._pet_running = False # 重置标志,允许重新开启 + threading.Thread(target=_listen_port, daemon=True).start() + + def _pet_req(q): + def _do(): + p = getattr(agent, '_pet_port', 41983) + try: urlopen(f'http://127.0.0.1:{p}/?{q}', timeout=2) + except Exception: pass + threading.Thread(target=_do, daemon=True).start() + agent._pet_req = _pet_req + + if not hasattr(agent, '_turn_end_hooks'): agent._turn_end_hooks = {} + def _pet_hook(ctx): + parts = [f"Turn {ctx.get('turn','?')}"] + if ctx.get('summary'): parts.append(ctx['summary']) + if ctx.get('exit_reason'): parts.append('DONE') + _pet_req(f'msg={quote(chr(10).join(parts))}') + if ctx.get('exit_reason'): _pet_req('state=idle') + agent._turn_end_hooks['pet'] = _pet_hook + agent._pet_running = True + st.toast("Desktop pet started") if LANG == 'zh': st.divider()