用Electron统一调用Claude、Codex与本地模型
教程通过Electron菜单栏应用,将Claude Code、Codex和Ollama接入统一提示入口。实现覆盖提供方抽象、安全IPC、子进程管理、流式输出、取消执行与打包。
AI 深度解读
把 Claude Code、Codex CLI 和 Ollama 收进同一个菜单栏窗口,是作者给多工具切换提出的解法:输入一次提示词,选择后端,再用统一界面接收输出、取消任务。实现靠的是 Electron、React 和子进程管理,没有复杂的模型调度。
这套方案适合拿来理解桌面 AI 工具怎么搭,但原文的「约一小时完成」需要打个问号。它展示了主要骨架,没有给出完整运行验证;统一入口也不等于不同 Agent 已经能够共享上下文。
统一入口容易,统一能力和上下文没那么简单
作者把应用分成三层。Electron 主进程负责 Tray、无边框窗口,以及启动和终止子进程;preload 脚本通过 window.agentAPI 暴露进程间通信(IPC)接口;React 渲染进程负责提示词、后端选择、输出展示和取消按钮。
这套分工的价值,在于让界面不用直接处理不同 CLI 的启动细节。作者用下面的接口描述每个后端,分别负责生成命令和归一化输出:
interface AgentProvider {
id: string; // 'claude' | 'codex' | 'ollama'
label: string;
buildCommand(prompt: string, opts: RunOptions): string[];
parseOutput(chunk: string): string; // normalize streamed text
}
要看的是 buildCommand 和 parseOutput:前者隔离调用差异,后者隔离返回格式差异。新增后端时,主要工作就落在适配文件里。
不过,作者所说的「不修改 UI」并不完全成立。原文的 App.tsx 把选项写死在 PROVIDERS 数组中,FAQ 也要求新增后端后修改这个数组。准确地说,是不用重写交互逻辑,仍要维护界面里的后端清单。
还有一个更大的边界:Claude Code、Codex CLI 是编码 Agent,Ollama 在这里提供的是本地模型调用。把它们包装成同一种接口,并不会让文件访问、工具执行、权限控制和会话能力自动对齐。
原文实现的是统一调用入口,没有实现跨 Agent 的上下文迁移。
示例没有保存对话历史,也没有把前一个后端的工具结果交给下一个后端。切换选择框后能复用输入框里的提示词,但不能据此认为整个任务已经无缝接续。
真正容易出错的是流式输出和取消任务
三个后端的启动方式各不相同。Claude 使用 claude -p,附带 --output-format stream-json 和 --verbose;Codex 使用 codex exec --full-auto;Ollama 使用 ollama run,默认模型为 llama3.2,附带 --format json。准备环境时,还需要安装并认证相关 CLI,以及执行 ollama pull llama3.2。
主进程最终通过下面这段代码启动后端,并设置工作目录、关闭彩色输出:
activeProcess = spawn(args[0], args.slice(1), {
cwd: options?.cwd ?? process.cwd(),
env: { ...process.env, FORCE_COLOR: '0' }
});
要注意 spawn(args[0], args.slice(1), ...):程序名和参数分开传入,示例没有启用 shell,也没有让渲染进程直接拼接任意命令。这是合理的调用边界,但后续仍应校验 providerId、工作目录和参数。
输出处理比启动命令更脆弱。原文把每次 stdout 的 data 事件直接交给 parseOutput,后者按换行切分,再逐行 JSON.parse。问题是,子进程输出的一块数据不保证刚好包含完整 JSON 行。一行被拆成两块时,两次解析都可能失败,而代码会直接丢弃失败内容。
正确的补全方向是维护缓冲区,只解析完整行,把末尾残片留到下一次。原文也没有展示真实输出样本验证:Claude 的 message.content 是否能直接当字符串处理,以及 ollama run --format json 是否会按示例产生带 response、done 的事件,都需要核对。尤其不能把 CLI 的输出格式选项,直接当成 HTTP 流式协议。
取消任务同样有状态问题。示例发送 SIGTERM 后立即把 activeProcess 设为 null,但进程不一定已经退出。用户此时启动新任务,旧进程稍后触发的 close 回调又会清空共享变量,并向界面发送完成事件。
因此,取消请求发出和进程确认退出,应当是两个状态。作者提到部分 Agent 可能需要 SIGINT,这是有用的扩展点;但选择信号不能代替任务标识和生命周期管理。
从 React 窗口到桌面应用,还缺哪些连接
作者要求 Electron 28+,使用 Electron、Vite、React、TypeScript 和 electron-builder。搭建从 mkdir agent-menu-bar && cd agent-menu-bar、npm init -y 开始,随后安装依赖并运行 npx tsc --init。打包配置给出 tsc && vite build,再通过 npm run build && electron-builder 生成分发包。
这些步骤能说明工程组成,却还不能保证照抄即可运行。原文没有完整交代主进程和 preload 的编译入口、Electron 如何加载窗口,以及 Vite 与 Electron 的开发启动如何配合。配置里的 dev 只有 vite,它本身并没有展示启动 Electron 的步骤。
React 示例还有一处连接不当:渲染进程通过 process.cwd() 获取工作目录,但前面的设计又希望通过 preload 暴露受控 API。采用隔离配置时,不能假定页面里可以直接访问 Node 的 process。工作目录更适合由主进程维护,再通过 IPC 提供给界面。
桌面交互则比较直观:Tray 使用 16×16 或 32×32 PNG,macOS 通过 setTemplateImage(true) 适配明暗菜单栏;全局快捷键是 CommandOrControl+Shift+Space。分发目标包括 Windows 的 nsis 和 Linux 的 AppImage。
原文还列出了一组生产化补充:用 which claude、which codex、which ollama 检查命令,借助 electron-store 保存后端与工作目录,设置例如 10 分钟的超时,用 path.join() 处理路径,把 stderr 写入 app.getPath('userData') + '/logs'。这些都是待落实的建议,并非示例已经完成的能力;跨平台检查也不能直接依赖同一组 which 命令。
如果后端要求交互式终端,作者建议将 child_process.spawn 换成 node-pty。这里的伪终端(PTY),可以理解为给程序提供更接近真实终端的运行环境,但终端输出和交互处理仍需要进一步适配。
我的判断
这篇最值得拿走的是三层职责划分和 Provider 抽象。对于想用前端技术做桌面工具的人,它把一个看似复杂的 AI 产品拆成了熟悉的 React 状态、IPC 事件和 Node 子进程。
但它更适合作为实现草图。作者没有给出完整测试记录、跨平台结果,也没有提供与 tmux 或各家原生 CLI 的效率对照。「约一小时完成」和减少切换成本,属于作者的经验判断,不能直接当成一般团队的交付承诺。
菜单栏入口适合频繁发起短任务;如果工作依赖终端里的连续交互和完整会话,tmux 仍有直接价值。两者解决的操作习惯不同,原文没有数据证明哪一种普遍更快。
「本地」也需要限定范围:本地运行的是路由应用和子进程,只有选择 Ollama 时,示例才明确指向本地模型。统一入口降低了接入多个后端的耦合,却没有消除它们各自的账号、能力和执行语义差异。--full-auto 这样的参数,更应被视为后端执行策略的一部分。
能拿来做什么
- 如果准备做个人 AI 菜单栏工具,可以先接入一个已认证的 CLI,跑通启动、输出、报错和退出,再增加其他 Provider,避免同时排查界面和多种协议。
- 如果要复用这套 Node runner,应优先补齐 JSON 分片缓冲、进程启动失败处理、任务标识和退出确认,让流式结果与取消操作有可靠的归属。
- 如果用 React 做桌面界面,应把工作目录和可执行程序检查放在主进程,通过
window.agentAPI返回结果,并处理调用失败后的running状态恢复。 - 如果目标是跨 Agent 继续同一个任务,就要单独设计历史记录和上下文传递;当前这套后端选择器只能作为调用入口。