From 0aa4a71fa7a7178776f58a657918e904e2c1dcd3 Mon Sep 17 00:00:00 2001 From: Wang Xinming <1353119036@qq.com> Date: Sun, 8 Nov 2026 06:11:31 +0800 Subject: [PATCH] fix(service): find Homebrew tools in macOS LaunchAgents --- CHANGELOG.md | 1 + src/octop/infra/setup/service.py | 9 +++++++++ tests/unit/infra/setup/test_service.py | 19 +++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b18f4d5dc1..c5501834b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - 专家市场默认打开「精选」,用书签标出接入市场前随安装包提供的 22 个专家模板 ### 修复 +- macOS 用户服务可以发现通过 Homebrew 安装的 adb,无需手动修改 LaunchAgent 配置 - 关机时会等正在进行的数据库查询结束再关闭连接,避免 Windows 上测试进程被访问冲突打断 - 模型池里的优先模型可以取消,并且不再显示为聊天框里的选中模型。聊天框只显示专家默认模型或这场对话的手选模型 - 添加云端协同时,权限不足、连接被占用、连到本机或达到条数上限会立刻显示可读错误,不再重试原始关闭帧 diff --git a/src/octop/infra/setup/service.py b/src/octop/infra/setup/service.py index 0128cbb150..6cea52b6e8 100644 --- a/src/octop/infra/setup/service.py +++ b/src/octop/infra/setup/service.py @@ -433,6 +433,14 @@ def render_launchd_plist(runtime: ServiceRuntime) -> str: if runtime.scope == "system" else "" ) + # A LaunchAgent does not inherit the interactive shell's Homebrew PATH. + # Keep system binaries first and don't extend the PATH of LaunchDaemons. + path_block = ( + " PATH\n" + " /usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/usr/local/bin\n" + if runtime.scope == "user" + else "" + ) return ( '\n' ' str: f" {runtime.home}\n" " OCTOP_SERVICE_MODE\n" " launchd\n" + f"{path_block}" " \n" f" WorkingDirectory\n" f" {runtime.home}\n" diff --git a/tests/unit/infra/setup/test_service.py b/tests/unit/infra/setup/test_service.py index 192a88a815..d0aa33a330 100644 --- a/tests/unit/infra/setup/test_service.py +++ b/tests/unit/infra/setup/test_service.py @@ -5,6 +5,7 @@ import getpass import json import os +import plistlib import shutil import sys from dataclasses import replace @@ -662,11 +663,29 @@ def test_render_launchd_plist_user_scope_omits_username(tmp_path: Path) -> None: assert f"{getpass.getuser()}" not in plist # run_as_user should not leak +def test_render_launchd_plist_user_scope_adds_homebrew_to_path( + monkeypatch: pytest.MonkeyPatch, tmp_path: Path +) -> None: + monkeypatch.setenv("PATH", str(tmp_path / "installer-only-bin")) + runtime = replace(_runtime(tmp_path), mode="launchd", scope="user") + env = plistlib.loads(render_launchd_plist(runtime).encode())["EnvironmentVariables"] + + assert env["PATH"].split(os.pathsep) == [ + "/usr/bin", + "/bin", + "/usr/sbin", + "/sbin", + "/opt/homebrew/bin", + "/usr/local/bin", + ] + + def test_render_launchd_plist_system_scope_includes_username(tmp_path: Path) -> None: runtime = replace(_runtime(tmp_path), mode="launchd", scope="system") plist = render_launchd_plist(runtime) assert "UserName" in plist assert f"{getpass.getuser()}" in plist + assert "PATH" not in plistlib.loads(plist.encode())["EnvironmentVariables"] def test_install_service_launchd_user_no_sudo(