用通俗的语言,解释 Paseo 的系统设计和技术亮点
解决什么问题
现在越来越多开发者用 AI 编码工具(Claude Code、Codex、Copilot 等)写代码。但这些工具各自独立,你没法统一管理它们,也没法离开电脑后在手机上继续看进度。
Paseo 就是一个统一的管理界面:在你自己的电脑上跑一个后台服务,然后通过桌面 app、手机 app、网页、或者命令行来监控和操作所有的 AI 编码工具。代码始终留在你自己的机器上。
这是一个 npm workspace 的单仓库(monorepo),分成 8 个包:
| 包名 | 干什么的 |
|---|---|
server | 后台服务(daemon),管理所有 AI agent 的启动、停止、通信 |
protocol | 定义客户端和服务端之间通信的消息格式(用 Zod schema 校验) |
client | 客户端 SDK,封装了连接服务端的逻辑 |
app | 手机 + 网页客户端(用 Expo/React Native 写,一套代码跑 iOS/Android/Web) |
desktop | 桌面 app(Electron 壳子,包着 app 里的网页版) |
cli | 命令行工具(类似 Docker CLI 风格,paseo run/ls/logs) |
relay | 中继服务,让你在外面也能连回家里电脑上的 daemon |
website | 官网 paseo.sh |
解决什么问题
开发者同时在做好几个项目、每个项目可能有好几条分支在同时开发、每条分支上可能跑了好几个 AI agent。需要一个清晰的方式来组织这些东西。
设计亮点
Paseo 用了三层结构来组织:
左侧边栏最外层,按 git 仓库或目录自动分组。比如 blade-hq/blade-agent 就是一个项目。
项目下面的每一行就是一个工作区。工作区通常对应一个 git 分支或目录,名字默认取自分支名。比如 "Single-worker Redis independence" 就是一个分支名。一个项目下可以有多个工作区。
选中一个工作区后,顶部的横向标签页就是这个工作区里打开的东西。每个标签页可以是:
点 + 按钮可以在同一个工作区里新开 agent 会话、终端或浏览器。
总结一句话:左边选"在哪个分支上干活",上面选"看哪个 agent 或工具"。
工作区是一个抽象概念,不一定要有 git。它有三种类型:
| 类型 | 含义 | 例子 |
|---|---|---|
directory | 普通目录,没有 git 管理 | 一个随便放笔记的文件夹 |
local_checkout | 你自己 clone 下来的 git 仓库 | git clone 出来的项目目录 |
worktree | Paseo 创建的 git worktree(独立分支+独立目录) | 在项目里新建一个分支开发新功能 |
点"新建工作区"按钮时,如果项目是 git 管理的,会让你选隔离模式:
git worktree add,创建一个独立分支和目录,agent 在里面工作不会影响主分支如果项目不是 git 管理的(比如一个普通文件夹),就只能创建 directory 类型的工作区。
细节
同一个目录可以对应多个工作区。工作区用独立的 wks_xxxx ID 标识,不是靠目录路径区分。左侧边栏上工作区的状态指示灯(运行中、等待输入等)是从里面所有 agent 的状态汇总出来的。
解决什么问题
手机、桌面、网页都需要实时看到 agent 的输出。普通的 HTTP 请求做不到"服务端主动推消息给客户端"。
方案:用 WebSocket 长连接。客户端和 daemon 之间只建一条连接,所有数据都走这一条线。
这条连接上混合传输两种数据:
| 数据类型 | 格式 | 内容 |
|---|---|---|
| 结构化消息 | JSON 文本 | Agent 状态变化、对话内容、权限请求、Git 操作等 |
| 终端数据 | 二进制帧 | 终端的字符输入输出(见下一节) |
客户端收到消息时,先尝试按二进制解码,如果不是二进制就按 JSON 处理。
解决什么问题
终端的字符流量非常大(想想 cat 一个大文件的输出),频率非常高。如果用 JSON 包装每一段终端输出,光是 JSON 的格式字符(引号、转义、键名)就会占很多空间,解析也慢。
方案:终端数据用紧凑的二进制格式传输,只有 2 个字节的头信息 + 原始数据。
设计亮点
对比 JSON 方案:如果要用 JSON 传同样的终端数据,需要把二进制内容 base64 编码(体积膨胀 33%),再加上 {"terminalId":"xxx","type":"output","data":"..."} 这样的包装。二进制方案的开销只有固定的 2 字节,数据原封不动传过去。
同一条 WebSocket 连接可以同时传多个终端的数据,靠槽位号区分。客户端有一个路由器(TerminalStreamRouter),维护"槽位号 → 终端 ID"的映射表,收到二进制帧后根据槽位号分发到对应终端。
解决什么问题
终端模拟器(处理颜色、光标移动、滚动等 ANSI 转义码)是个复杂的东西。Paseo 用的是 xterm.js,一个成熟的浏览器端终端库。但手机端不是浏览器,怎么用它?
| 平台 | 方案 | 说明 |
|---|---|---|
| Web / 桌面(Electron) | 直接用 xterm.js + WebGL 渲染 | 因为本身就是浏览器环境,直接渲染,还用了 WebGL 加速 |
| iOS / Android | 在 WebView 里跑 xterm.js | 把 xterm.js 打包成一个 HTML 文件,嵌入到一个 WebView 里运行。通过 postMessage 在 React Native 和 WebView 之间传数据 |
在手机端,数据流是这样的:
解决什么问题
终端只是 agent 工作的一部分。Agent 的对话消息、状态变化、权限请求、工具调用结果这些结构化内容,需要用正常的 UI 组件来展示,不是终端字符。
方案:这些内容全部用 JSON 格式传输,通过 React Native 渲染成原生组件。
| 平台 | 渲染方式 |
|---|---|
| iOS | React Native → UIKit 原生控件(不是 WebView) |
| Android | React Native → Android 原生控件(不是 WebView) |
| Web / 桌面 | React → DOM 元素 |
只有终端部分用了 WebView,其他所有 UI(消息气泡、按钮、列表等)都是原生组件。所以在 iOS 上看到的对话界面的滑动手感和原生 app 一样。
每个 agent 会经历这些状态:
一个 agent 可以启动其他 agent,形成父子关系:
归档(archive)是全局操作,所有客户端都能看到。关闭标签页只是"我不看了"。
解决什么问题
Agent 正在执行上一个任务,用户想改主意或者补充新指令。这时候发消息应该怎么处理?
Paseo 提供了两种行为,用户可以在设置里切换:
消息直接发到服务端,服务端的处理流程:
interrupt(),最多等 2 秒)设计细节
服务端用一个 pendingReplacement 标记来防止状态闪烁——在旧任务取消和新任务启动之间,agent 不会短暂变成 "idle" 再变回 "running",用户看到的是无缝切换。
消息不会发到服务端,而是存在客户端本地的队列里:
两种模式下发送按钮都不会被禁用,用户随时可以发消息。
有些特殊命令(如 /goal pause)可以在不打断当前任务的情况下执行。服务端会先判断新消息是否是带外命令,如果是,就作为副作用执行,当前任务继续不受影响。
解决什么问题
不同的 AI 编码工具(Claude Code、Codex、Copilot 等)各有各的启动方式、通信协议、输出格式。Paseo 怎么用一套代码管理它们?
方案:定义一个统一的接口(AgentClient),每个工具各自实现一个适配器。
| Provider | 接入方式 | 说明 |
|---|---|---|
| Claude Code | Agent SDK(直接集成) | 通过 Claude 的 SDK 直接调用 |
| Codex | app-server 协议 | 通过 Codex 的本地服务协议对接 |
| GitHub Copilot | ACP 协议 | 标准化的 Agent Communication Protocol |
| OpenCode | 直接集成 | 启动 OpenCode 进程 |
| Pi | 直接集成 | 启动 Pi 进程 |
设计亮点
适配器的职责很明确:把各家 agent 的差异全部吸收掉,向上只暴露统一的"发消息、收消息、中断、获取状态"接口。上层代码完全不需要知道底下跑的是 Claude Code 还是 Codex。新增一种 agent 只需要写一个新适配器。
解决什么问题
daemon 需要记住 agent 的状态、工作区配置、历史记录等。用数据库太重了——这是一个跑在用户自己电脑上的工具,不应该依赖 PostgreSQL 之类的东西。
方案:所有数据都存成 JSON 文件,放在 ~/.paseo 目录下。
~/.paseo/
├── agents/ # 每个 agent 一个 JSON 文件
│ └── {目录名}/
│ └── {agent-id}.json
├── workspaces.json # 工作区注册表
├── projects.json # 项目注册表
├── daemon.log # 日志
└── ...
写文件用"先写临时文件、再重命名"的方式,保证写入过程中断电也不会损坏数据。所有数据用 Zod schema 校验,确保格式正确。
不需要数据库迁移——读取时遇到旧格式会自动用默认值填充新字段。
解决什么问题
在外面用手机连回家里电脑上的 daemon,数据经过公网传输,怎么防止被偷看?
方案:端到端加密。数据在你手机上加密,到你电脑上才解密,中间经过的服务器(relay)只看到密文。
relay 服务器只负责转发密文,拿不到任何明文内容。即使 relay 被攻破,攻击者也无法读取你的数据。
解决什么问题
手机 app 和 daemon 可能版本不同(手机还没更新,电脑已经更新了)。怎么保证它们还能正常通信?
规则很简单:
所有向后兼容的临时代码都标记了 COMPAT(name) 注释和计划删除日期,用 rg "COMPAT(" 一条命令就能找到所有需要清理的地方。