# 本地服务与 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 和真实运行服务必须分开报告。