Skip to content

fix(local-cli): AppleScript 被拒时降级到 Launch Services#486

Merged
deepcoldy merged 3 commits into
deepcoldy:masterfrom
47seek:fix/local-cli-launch-services-fallback
Jul 19, 2026
Merged

fix(local-cli): AppleScript 被拒时降级到 Launch Services#486
deepcoldy merged 3 commits into
deepcoldy:masterfrom
47seek:fix/local-cli-launch-services-fallback

Conversation

@47seek

@47seek 47seek commented Jul 16, 2026

Copy link
Copy Markdown
Collaborator

改了什么

macOS 本机 CLI 打开流程在全部 AppleScript 尝试失败后,新增 Launch Services 降级路径:

  • 将已校验的 attach/resume 命令写入权限为 0700 的临时 .command 文件
  • 异步调用 /usr/bin/open,依次尝试 iTerm 与 Terminal.app 的 bundle id,不阻塞 daemon 事件循环
  • 脚本真正开始执行后自删私有临时目录;另有 24 小时定时清理和下次启动时的 stale sweep 兜底
  • stale sweep 只匹配 mkdtemp 生成的精确六位后缀目录,避免误删同前缀的用户目录
  • AppleScript 与 Launch Services 都失败时保留实际错误信息

为什么

当 daemon 由 PM2/launchd 后台运行时,osascript 发送 Apple Event 的负责进程是 node/PM2,而不是启动 daemon 的 iTerm。macOS TCC 因此不会向 iTerm 弹出 Automation 授权,AppleScript 会持续返回 -1743

open 走 Launch Services,不需要 Automation 权限,可以覆盖这个后台 daemon 场景。

影响面

  • 仅影响 macOS 的 openLocalCliInIterm,并且只在现有 5 次 AppleScript 尝试全部失败后执行
  • AppleScript 成功路径保持不变
  • iTerm 不可用时会继续尝试 Terminal.app
  • attach/resume 共用既有命令构建与校验逻辑;其它 CLI 适配器、后端和会话类型不变
  • 非 Darwin 平台在进入终端探测前即返回,Linux 路径不受影响

测试验证

vitest run --project unit test/local-cli-opener.test.ts test/card-handler-open-local-cli.test.ts
→ 2 files passed, 46 tests passed

pnpm build
→ 通过

git diff --check
→ 通过

回归测试额外验证:

  • Launch Services 成功/失败与 Terminal.app fallback
  • .command 文件权限、自清理与 24 小时 stale sweep
  • 旧版临时目录清理
  • 同前缀但非 botmux 生成的目录不会被误删

注:未切换当前 live daemon;真实 macOS PM2/launchd 下的 TCC 与终端确认框交互仍需手动 click-through 验证。

daemon 通过 PM2/launchd 后台运行时,osascript 发送 Apple事件的负责进程是
node/PM2 而非 iTerm,macOS TCC 永不弹 Automation 授权弹窗,导致 -1743
错误且自动化设置里找不到 iTerm/Terminal。

新增 openViaCommandFile 降级路径:AppleScript 全部失败后,将命令写入临时
.command 文件并通过 `open -a iTerm` 打开。Launch Services 无需 Automation
权限,覆盖后台 daemon 场景。

影响面:
- 仅影响 macOS 本机 CLI 打开流程(openLocalCliInIterm)
- AppleScript 成功时行为不变,仅在全部失败后触发降级
- Linux 路径不受影响(darwin 平台检查在前)
- 新增 runOpenCommand 依赖注入点,默认走 spawnSync('open', ['-a', 'iTerm', path])

测试:新增 2 个单测覆盖降级成功与降级失败场景,原有 5 个 osascript
调用断言保持不变。vitest run --project unit test/local-cli-opener.test.ts
(25 passed) + test/card-handler-open-local-cli.test.ts (19 passed)。
@47seek
47seek requested a review from deepcoldy as a code owner July 16, 2026 05:18
@deepcoldy

Copy link
Copy Markdown
Owner

Review 总结

感谢 @47seek 贡献这个修复!方案方向正确——daemon 在 PM2/launchd 后台运行时 TCC 永不弹 Automation 授权弹窗,用 Launch Services 降级是合理思路。代码结构清晰,依赖注入设计好,测试覆盖到位。

我们做了两轮独立 review,发现以下问题,建议修复后再合入:

🔴 阻塞:成功路径永久泄漏临时目录

openViaCommandFileopened.ok 后直接 return,未清理 dir(src/services/local-cli-opener.ts:315-321)。每次降级成功留一个含 0755 open-cli.command 的目录(内容含 cwd 和 resume/attach target)。新增成功单测每跑一次也在 /tmp 留目录。

注意:不能简单短延迟 timer 清理——Launch Services 打开是异步的,且 iTerm 对外部脚本默认先弹"OK to run …?"确认框,用户可能很久后才确认或取消。建议让 .command 真正开始执行后自删目录再执行目标命令,同时加较长 TTL/stale sweep 兜底用户取消/未执行的情况。测试需断言清理策略,避免单测自身污染 /tmp

🟡 建议同 PR 补齐

  1. spawnSync 冻结整个 daemon 最长 10 秒(:300-302)——该路径从卡片事件进同一个 Node daemon,open 卡住时所有 bot、WebSocket/事件处理一起停。建议改 Promise + 异步 execFile('/usr/bin/open', ...),用绝对路径,把 result.error(ENOENT/ETIMEDOUT)带进错误信息。
  2. 降级仅覆盖 iTerm,漏 Terminal.app(:301)——TCC 被拒且没装 iTerm 时 fallback 仍必败。建议依次试 iTerm bundle id 和 com.apple.Terminal,或用系统默认 .command handler。
  3. 失败文案过时(:292-297)——base 文案只提"could be opened with AppleScript",建议说明 AppleScript 与 Launch Services 均失败,保留实际 open/文件创建错误。

🟢 额外建议

  • iTerm 默认弹脚本运行确认,open 返回 0 只代表 Launch Services 已受理,不代表 CLI 已执行,这个 UX 建议写进 PR 描述。
  • 当前测试全部 mock 了 open,无法验证核心假设(PM2/launchd 下绕过 TCC、iTerm/Terminal 实际执行 .command)。建议在真实 macOS 后台 daemon 场景 live 测试并记录。

验证结果

  • test/local-cli-opener.test.ts: 25 passed
  • test/card-handler-open-local-cli.test.ts: 19 passed
  • tsc --noEmit: 通过
  • pnpm build: 通过
  • git diff --check: 通过

另外,botmux 项目在飞书有开发者协作群,方便日常沟通、方案讨论和快速 review。如果你愿意加入协作,可以私信我(@deepcoldy)获取入群方式。期待你的更新!🚀

@47seek 47seek changed the title fix(local-cli): AppleScript 被拒时降级到 Launch Services 打开 iTerm fix(local-cli): AppleScript 被拒时降级到 Launch Services Jul 19, 2026
@47seek

47seek commented Jul 19, 2026

Copy link
Copy Markdown
Collaborator Author

已按 review 意见补齐并完成复查,当前 head 为 faf6177c

  • 成功路径改为脚本启动后自清理,另有 24h timer/stale sweep 兜底
  • open 改为异步 /usr/bin/open,不再用 spawnSync 阻塞 daemon
  • Launch Services 依次尝试 iTerm 与 Terminal.app
  • 失败文案包含 AppleScript/Launch Services 的实际错误
  • stale sweep 的目录匹配收紧为 mkdtemp 精确六位后缀,避免误删同前缀用户目录,并新增回归测试
  • PR 标题和描述已同步为当前实现

验证结果:相关 2 个测试文件共 46 tests passed,pnpm build 通过,git diff --check 通过。

当前 CI run 是 action_required(fork workflow 尚待 maintainer 批准,0 jobs),并非测试失败。真实 PM2/launchd + TCC click-through 未切换 live daemon 验证,已在 PR 描述中明确标注。烦请重新 review,并在方便时批准 workflow。

补充:已拉取 GitHub 生成的 merge ref 025b6980(最新 upstream/master@9c3d5f31 + head@faf6177c)复跑,相关 46 tests 与完整 pnpm build 均通过,确认当前 base 合并结果无回归。

@deepcoldy
deepcoldy merged commit 150f1bb into deepcoldy:master Jul 19, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants