Paseo 架构分析

用通俗的语言,解释 Paseo 的系统设计和技术亮点

目录

  1. Paseo 是什么
  2. 整体结构:8 个包各管什么
  3. 桌面端 UI 层级:项目、工作区、标签页
  4. 工作区到底是什么
  5. 客户端和服务端怎么通信
  6. 终端数据为什么用二进制传输
  7. 终端在各个平台怎么渲染
  8. 结构化消息在各端怎么显示
  9. Agent 的生命周期
  10. 给运行中的 Agent 发消息会怎样
  11. 怎么支持多种 AI 编码工具
  12. 数据怎么存储
  13. 远程访问怎么保证安全
  14. 协议怎么做到向后兼容

1. Paseo 是什么

解决什么问题

现在越来越多开发者用 AI 编码工具(Claude Code、Codex、Copilot 等)写代码。但这些工具各自独立,你没法统一管理它们,也没法离开电脑后在手机上继续看进度。

Paseo 就是一个统一的管理界面:在你自己的电脑上跑一个后台服务,然后通过桌面 app、手机 app、网页、或者命令行来监控和操作所有的 AI 编码工具。代码始终留在你自己的机器上。

2. 整体结构:8 个包各管什么

这是一个 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
手机/桌面/网页/CLI(客户端) │ │ WebSocket 连接 ▼ daemon(server 包) │ │ 管理和启动 ▼ Claude Code / Codex / Copilot / OpenCode / Pi(各种 AI agent)

3. 桌面端 UI 层级:项目、工作区、标签页

解决什么问题

开发者同时在做好几个项目、每个项目可能有好几条分支在同时开发、每条分支上可能跑了好几个 AI agent。需要一个清晰的方式来组织这些东西。

设计亮点

Paseo 用了三层结构来组织:

左侧边栏 主内容区 ┌─────────────────────┐ ┌──────────────────────────────────┐ │ │ │ [Tab1: CC会话] [Tab2: CC会话] [+]│ ← 标签页 │ 项目 blade-agent │ ├──────────────────────────────────┤ │ ├─ 工作区 A (分支1)│ │ │ │ ├─ 工作区 B (分支2)│ │ 当前选中的标签页的内容 │ │ └─ 工作区 C (main) │ │ (agent 对话 / 终端 / 浏览器) │ │ │ │ │ │ 项目 daily │ │ │ │ └─ 工作区 D │ │ │ │ │ │ │ └─────────────────────┘ └──────────────────────────────────┘

第一层:项目(Project)

左侧边栏最外层,按 git 仓库或目录自动分组。比如 blade-hq/blade-agent 就是一个项目。

第二层:工作区(Workspace)

项目下面的每一行就是一个工作区。工作区通常对应一个 git 分支或目录,名字默认取自分支名。比如 "Single-worker Redis independence" 就是一个分支名。一个项目下可以有多个工作区。

第三层:标签页(Tab)

选中一个工作区后,顶部的横向标签页就是这个工作区里打开的东西。每个标签页可以是:

+ 按钮可以在同一个工作区里新开 agent 会话、终端或浏览器。

总结一句话:左边选"在哪个分支上干活",上面选"看哪个 agent 或工具"。

4. 工作区到底是什么

工作区是一个抽象概念,不一定要有 git。它有三种类型:

类型含义例子
directory普通目录,没有 git 管理一个随便放笔记的文件夹
local_checkout你自己 clone 下来的 git 仓库git clone 出来的项目目录
worktreePaseo 创建的 git worktree(独立分支+独立目录)在项目里新建一个分支开发新功能

新建工作区时的选择

点"新建工作区"按钮时,如果项目是 git 管理的,会让你选隔离模式:

如果项目不是 git 管理的(比如一个普通文件夹),就只能创建 directory 类型的工作区。

细节

同一个目录可以对应多个工作区。工作区用独立的 wks_xxxx ID 标识,不是靠目录路径区分。左侧边栏上工作区的状态指示灯(运行中、等待输入等)是从里面所有 agent 的状态汇总出来的。

5. 客户端和服务端怎么通信

解决什么问题

手机、桌面、网页都需要实时看到 agent 的输出。普通的 HTTP 请求做不到"服务端主动推消息给客户端"。

方案:用 WebSocket 长连接。客户端和 daemon 之间只建一条连接,所有数据都走这一条线。

这条连接上混合传输两种数据:

数据类型格式内容
结构化消息JSON 文本Agent 状态变化、对话内容、权限请求、Git 操作等
终端数据二进制帧终端的字符输入输出(见下一节)

客户端收到消息时,先尝试按二进制解码,如果不是二进制就按 JSON 处理。

6. 终端数据为什么用二进制传输

解决什么问题

终端的字符流量非常大(想想 cat 一个大文件的输出),频率非常高。如果用 JSON 包装每一段终端输出,光是 JSON 的格式字符(引号、转义、键名)就会占很多空间,解析也慢。

方案:终端数据用紧凑的二进制格式传输,只有 2 个字节的头信息 + 原始数据。

二进制帧的结构

字节 0 字节 1 字节 2 ~ N ┌──────────┬──────────┬─────────────────┐ │ 操作码 │ 槽位号 │ 原始数据 │ │ (1 byte) │ (1 byte) │ (变长) │ └──────────┴──────────┴─────────────────┘

设计亮点

对比 JSON 方案:如果要用 JSON 传同样的终端数据,需要把二进制内容 base64 编码(体积膨胀 33%),再加上 {"terminalId":"xxx","type":"output","data":"..."} 这样的包装。二进制方案的开销只有固定的 2 字节,数据原封不动传过去。

多路复用

同一条 WebSocket 连接可以同时传多个终端的数据,靠槽位号区分。客户端有一个路由器(TerminalStreamRouter),维护"槽位号 → 终端 ID"的映射表,收到二进制帧后根据槽位号分发到对应终端。

7. 终端在各个平台怎么渲染

解决什么问题

终端模拟器(处理颜色、光标移动、滚动等 ANSI 转义码)是个复杂的东西。Paseo 用的是 xterm.js,一个成熟的浏览器端终端库。但手机端不是浏览器,怎么用它?

平台方案说明
Web / 桌面(Electron)直接用 xterm.js + WebGL 渲染因为本身就是浏览器环境,直接渲染,还用了 WebGL 加速
iOS / Android在 WebView 里跑 xterm.js把 xterm.js 打包成一个 HTML 文件,嵌入到一个 WebView 里运行。通过 postMessage 在 React Native 和 WebView 之间传数据

在手机端,数据流是这样的:

daemon → WebSocket 二进制帧 → 客户端 SDK 解码 → React Native → postMessage → WebView 里的 xterm.js → 渲染到屏幕

8. 结构化消息在各端怎么显示

解决什么问题

终端只是 agent 工作的一部分。Agent 的对话消息、状态变化、权限请求、工具调用结果这些结构化内容,需要用正常的 UI 组件来展示,不是终端字符。

方案:这些内容全部用 JSON 格式传输,通过 React Native 渲染成原生组件。

平台渲染方式
iOSReact Native → UIKit 原生控件(不是 WebView)
AndroidReact Native → Android 原生控件(不是 WebView)
Web / 桌面React → DOM 元素

只有终端部分用了 WebView,其他所有 UI(消息气泡、按钮、列表等)都是原生组件。所以在 iOS 上看到的对话界面的滑动手感和原生 app 一样。

9. Agent 的生命周期

每个 agent 会经历这些状态:

initializing → idle ⇄ running → error → closed ↑ │ └────────┘ (完成一轮对话,等待下一条消息)

Agent 之间的关系

一个 agent 可以启动其他 agent,形成父子关系:

归档 ≠ 关闭标签页

归档(archive)是全局操作,所有客户端都能看到。关闭标签页只是"我不看了"。

10. 给运行中的 Agent 发消息会怎样

解决什么问题

Agent 正在执行上一个任务,用户想改主意或者补充新指令。这时候发消息应该怎么处理?

Paseo 提供了两种行为,用户可以在设置里切换:

模式一:打断(interrupt)— 默认行为

消息直接发到服务端,服务端的处理流程:

1
取消当前正在执行的任务(调用 agent 的 interrupt(),最多等 2 秒)
2
取消完成后,立即用新消息启动新一轮对话
3
整个过程中 agent 状态保持 "running",不会闪烁

设计细节

服务端用一个 pendingReplacement 标记来防止状态闪烁——在旧任务取消和新任务启动之间,agent 不会短暂变成 "idle" 再变回 "running",用户看到的是无缝切换。

模式二:排队(queue)

消息不会发到服务端,而是存在客户端本地的队列里:

两种模式下发送按钮都不会被禁用,用户随时可以发消息。

特殊情况:带外命令

有些特殊命令(如 /goal pause)可以在不打断当前任务的情况下执行。服务端会先判断新消息是否是带外命令,如果是,就作为副作用执行,当前任务继续不受影响。

11. 怎么支持多种 AI 编码工具

解决什么问题

不同的 AI 编码工具(Claude Code、Codex、Copilot 等)各有各的启动方式、通信协议、输出格式。Paseo 怎么用一套代码管理它们?

方案:定义一个统一的接口(AgentClient),每个工具各自实现一个适配器。

Provider接入方式说明
Claude CodeAgent SDK(直接集成)通过 Claude 的 SDK 直接调用
Codexapp-server 协议通过 Codex 的本地服务协议对接
GitHub CopilotACP 协议标准化的 Agent Communication Protocol
OpenCode直接集成启动 OpenCode 进程
Pi直接集成启动 Pi 进程

设计亮点

适配器的职责很明确:把各家 agent 的差异全部吸收掉,向上只暴露统一的"发消息、收消息、中断、获取状态"接口。上层代码完全不需要知道底下跑的是 Claude Code 还是 Codex。新增一种 agent 只需要写一个新适配器。

12. 数据怎么存储

解决什么问题

daemon 需要记住 agent 的状态、工作区配置、历史记录等。用数据库太重了——这是一个跑在用户自己电脑上的工具,不应该依赖 PostgreSQL 之类的东西。

方案:所有数据都存成 JSON 文件,放在 ~/.paseo 目录下。

~/.paseo/
├── agents/                    # 每个 agent 一个 JSON 文件
│   └── {目录名}/
│       └── {agent-id}.json
├── workspaces.json            # 工作区注册表
├── projects.json              # 项目注册表
├── daemon.log                 # 日志
└── ...

写文件用"先写临时文件、再重命名"的方式,保证写入过程中断电也不会损坏数据。所有数据用 Zod schema 校验,确保格式正确。

不需要数据库迁移——读取时遇到旧格式会自动用默认值填充新字段。

13. 远程访问怎么保证安全

解决什么问题

在外面用手机连回家里电脑上的 daemon,数据经过公网传输,怎么防止被偷看?

方案:端到端加密。数据在你手机上加密,到你电脑上才解密,中间经过的服务器(relay)只看到密文。

relay 服务器只负责转发密文,拿不到任何明文内容。即使 relay 被攻破,攻击者也无法读取你的数据。

14. 协议怎么做到向后兼容

解决什么问题

手机 app 和 daemon 可能版本不同(手机还没更新,电脑已经更新了)。怎么保证它们还能正常通信?

规则很简单:

所有向后兼容的临时代码都标记了 COMPAT(name) 注释和计划删除日期,用 rg "COMPAT(" 一条命令就能找到所有需要清理的地方。