# 本地服务与 LaunchAgent 健康检查 - id: `local-service-launchagent-doctor` - kind: `diagnostic-tool` - risk: `medium` - value: `high` ## 1.1 摘要 这个主题有效:多个片段都指向同一类本机自动化故障,表象包括端口拒绝、LaunchControl 状态误读、旧 label 残留、端口冲突和 macOS TCC 权限拦截。应沉淀成一个先查真实监听、再查 launchd/plist/disabled database、最后判断权限边界的本地诊断工具,减少把 UI 状态或单点报错误判为根因。 ## 1.2 证据 | session_id | cwd | snippet | | --- | --- | --- | | 019dfcaa-4bf0-7683-ba97-1895c5927bb6 | ~/Documents/product-bu | Obsidian 图片上传失败,Failed request: net::ERR_CONNECTION_REFUSED。上传目标是 http://127.0.0.1:36677/upload,但本机没有进程监听 36677;根因是 PicGo 没运行。 | | 019dfcaa-4bf0-7683-ba97-1895c5927bb6 | ~/Documents/product-bu | 已创建并加载 com.user.codex.picgo-login.plist;验证 plutil 通过、launchctl 已加载、PicGo 当前仍监听 127.0.0.1:36677。 | | 019dfc5c-897b-7560-bc71-8ab7a6632afa | ~/Documents/info-bu | com.user.pomodoro-watcher 是旧 label 残留,旧 plist 不存在、live launchd 里没有 service;真正运行的是中文 label 番茄强制休息。 | | 019dfc7b-f02b-7083-950d-237f0321ea58 | ~/Documents/product-bu | 8765 端口是 AnkiConnect,不能占用;复核写入服务改到 127.0.0.1:18765。LaunchAgent 方案被 macOS TCC 权限挡住,改用当前 Codex 进程托管写入服务。 | | 019dfc5c-897b-7560-bc71-8ab7a6632afa | ~/Documents/info-bu | LaunchControl 里的绿勾表示没有被禁用,不等于已经加载;旧 disabled database 残留需要和真实运行 job 分开判断,不能只看 UI 状态。 | ## 1.3 拟议改动 1. 工具入口:新增 `local-service-launchagent-doctor` skill 或放入现有 `定时任务` skill 的 `scripts/local_service_launchagent_doctor.py`;CLI 形态为 `python scripts/local_service_launchagent_doctor.py --url http://127.0.0.1:36677/upload --label com.user.codex.picgo-login --plist ~/Library/LaunchAgents/com.user.codex.picgo-login.plist`。 2. 输入来源:支持显式 URL/host/port、LaunchAgent label、plist path、expected process name、fallback command;同时支持从 Obsidian/PicGo/AnkiConnect 等报错文本中解析 `127.0.0.1:<port>`、endpoint、connection refused、unloaded 等关键词。 3. 核心检查步骤:先用 HTTP 探测和 `lsof -nP -iTCP:<port> -sTCP:LISTEN` 判断真实监听,再用 `launchctl print gui/$UID/<label>` 判断 live job,用 `plutil -lint` 与 `~/Library/LaunchAgents` 判断 plist 是否存在且有效,用 `launchctl print-disabled gui/$UID` 区分 disabled database 残留,最后检查端口冲突、TCC 拒绝、ProgramArguments、KeepAlive、StandardOut/ErrorPath。 4. 成功/失败信号与落地路径:成功信号是端口返回预期 HTTP 状态、launchctl live job 存在、PID/监听进程与预期一致、plist lint 通过;失败信号包括 connection refused、live job missing、old label only、disabled residue、port occupied、TCC denied。落地时输出 `diagnosis.json` 和 markdown 修复建议,并把诊断流程样例接入 Night Gym 的 diagnostic-tool examples。 ## 1.4 示例 ## 诊断流程样例:Obsidian 图片上传失败 ### 输入 | 字段 | 示例 | | --- | --- | | 报错文本 | `Failed request: net::ERR_CONNECTION_REFUSED` | | URL | `http://127.0.0.1:36677/upload` | | 期望服务 | PicGo | | LaunchAgent label | `com.user.codex.picgo-login` | | plist | `~/Library/LaunchAgents/com.user.codex.picgo-login.plist` | ### 执行 ```bash python scripts/local_service_launchagent_doctor.py \ --url http://127.0.0.1:36677/upload \ --label com.user.codex.picgo-login \ --plist ~/Library/LaunchAgents/com.user.codex.picgo-login.plist \ --expect-process PicGo ``` ### 核心判断 | 检查项 | 成功信号 | 失败信号 | 下一步 | | --- | --- | --- | --- | | HTTP/端口 | `/upload` 有响应或至少端口 listening | `connection refused` | 查进程是否启动 | | 监听进程 | `lsof` 显示 PicGo 监听 `36677` | 无监听或被其他进程占用 | 启动 PicGo 或换端口 | | live job | `launchctl print gui/$UID/com.user.codex.picgo-login` 有 job/PID | `Could not find service` | 检查 plist 是否存在 | | plist | `plutil -lint` 通过 | 文件缺失或语法错误 | 修复 plist 后 bootstrap | | disabled database | label 未被禁用 | 旧 label 残留或 disabled 状态误导 | 与真实 live job 分开报告 | | TCC/GUI 权限 | job 能启动 GUI app 或本地服务 | TCC denied / GUI session 不可用 | 改成用户手动启动或当前 Codex 进程托管 | ### 输出示例 ```json { "status": "fail", "root_cause": "local_port_not_listening", "evidence": [ "curl http://127.0.0.1:36677/upload -> connection refused", "lsof :36677 -> no listener", "launchctl live job missing for com.user.codex.picgo-login" ], "recommended_fix": "启动 PicGo,并安装/加载 com.user.codex.picgo-login LaunchAgent;修复后再次确认 127.0.0.1:36677 正在监听。" } ``` ### 诊断原则 先相信系统事实,不先相信 LaunchControl 的单个 UI 状态。绿勾只表示未禁用,不等于 live job 已加载;旧 disabled database、旧 label、缺失 plist 和真实运行服务必须分开报告。