From d2d5d98d785c9415d0f6c9be97d6afc5817b77eb Mon Sep 17 00:00:00 2001 From: ooaaooaa123 Date: Sun, 10 May 2026 17:20:45 +0800 Subject: [PATCH 1/2] feat(web): auto-switch port on conflict and open browser on startup --- channel/web/web_channel.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index 6823c02d..f8d59724 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -535,8 +535,24 @@ class WebChannel(ChatChannel): with open(file_path, 'r', encoding='utf-8') as f: return f.read() + def _find_available_port(self, start_port, max_tries=10): + """从 start_port 开始向上找第一个可用端口,最多尝试 max_tries 次。""" + import socket + for port in range(start_port, start_port + max_tries): + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + try: + s.bind(("0.0.0.0", port)) + return port + except OSError: + continue + raise OSError(f"[WebChannel] 无法找到可用端口(尝试范围 {start_port}-{start_port + max_tries - 1})") + def startup(self): - port = conf().get("web_port", 9899) + base_port = conf().get("web_port", 9899) + port = self._find_available_port(base_port) + if port != base_port: + logger.info(f"[WebChannel] 端口 {base_port} 已被占用,自动切换到端口 {port}") # 打印可用渠道类型提示 logger.info( @@ -554,6 +570,9 @@ class WebChannel(ChatChannel): logger.info(f"[WebChannel] 🌐 本地访问: http://localhost:{port}") logger.info(f"[WebChannel] 🌍 服务器访问: http://YOUR_IP:{port} (请将YOUR_IP替换为服务器IP)") + import webbrowser + webbrowser.open(f"http://localhost:{port}") + # 确保静态文件目录存在 static_dir = os.path.join(os.path.dirname(__file__), 'static') if not os.path.exists(static_dir): From ad51aabfd7b1ec5155260ebf4a31b096bc073d4a Mon Sep 17 00:00:00 2001 From: ooaaooaa123 Date: Sun, 10 May 2026 19:30:07 +0800 Subject: [PATCH 2/2] feat(web): open browser on startup with safe fallback; friendly error on port conflict --- channel/web/web_channel.py | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/channel/web/web_channel.py b/channel/web/web_channel.py index f8d59724..98087deb 100644 --- a/channel/web/web_channel.py +++ b/channel/web/web_channel.py @@ -535,24 +535,8 @@ class WebChannel(ChatChannel): with open(file_path, 'r', encoding='utf-8') as f: return f.read() - def _find_available_port(self, start_port, max_tries=10): - """从 start_port 开始向上找第一个可用端口,最多尝试 max_tries 次。""" - import socket - for port in range(start_port, start_port + max_tries): - with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: - s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - try: - s.bind(("0.0.0.0", port)) - return port - except OSError: - continue - raise OSError(f"[WebChannel] 无法找到可用端口(尝试范围 {start_port}-{start_port + max_tries - 1})") - def startup(self): - base_port = conf().get("web_port", 9899) - port = self._find_available_port(base_port) - if port != base_port: - logger.info(f"[WebChannel] 端口 {base_port} 已被占用,自动切换到端口 {port}") + port = conf().get("web_port", 9899) # 打印可用渠道类型提示 logger.info( @@ -570,8 +554,12 @@ class WebChannel(ChatChannel): logger.info(f"[WebChannel] 🌐 本地访问: http://localhost:{port}") logger.info(f"[WebChannel] 🌍 服务器访问: http://YOUR_IP:{port} (请将YOUR_IP替换为服务器IP)") - import webbrowser - webbrowser.open(f"http://localhost:{port}") + try: + import webbrowser + webbrowser.open(f"http://localhost:{port}") + logger.debug(f"[WebChannel] Opened browser at http://localhost:{port}") + except Exception as e: + logger.debug(f"[WebChannel] Could not open browser: {e}") # 确保静态文件目录存在 static_dir = os.path.join(os.path.dirname(__file__), 'static') @@ -638,6 +626,13 @@ class WebChannel(ChatChannel): server.start() except (KeyboardInterrupt, SystemExit): server.stop() + except OSError as e: + if e.errno in (48, 98): # macOS/Linux EADDRINUSE + logger.error( + f"[WebChannel] 端口 {port} 已被占用,可执行 `cow restart` 清理残留进程," + f"或在 config.json 中修改 web_port" + ) + raise def stop(self): if self._http_server: