T3 Code 源码拆解
系统全景

02 · Monorepo 与边界

从 workspace 结构看协议、运行时、平台壳和基础设施如何解耦。

顶层地图

T3 Code 使用 pnpm workspace 与 Vite+ 管理一个跨 Web、Electron、React Native、Node/Bun、Rust 和云基础设施的仓库。 目录多并不等于边界清楚;真正的架构信号是哪些包可以被谁依赖。

t3code/
├── apps/
│   ├── server/       # 权威执行边界、RPC、编排、Provider、Git、PTY
│   ├── web/          # React 19 + TanStack Router 控制面
│   ├── desktop/      # Electron 主进程、内嵌 Server、IPC、更新、SSH/WSL
│   ├── mobile/       # Expo / React Native 客户端与原生性能模块
│   └── marketing/    # 营销站点,不进入核心运行链路
├── packages/
│   ├── contracts/    # Schema、RPC、事件、跨进程共享类型
│   ├── client-runtime/ # 环境目录、连接监督、快照和缓存所有权
│   ├── shared/       # 跨端纯工具与通用模型
│   ├── effect-codex-app-server/ # Codex JSON-RPC 客户端
│   ├── effect-acp/   # Agent Client Protocol 的 Effect 封装
│   ├── ssh/          # SSH transport / process 辅助
│   └── tailscale/    # Tailscale 集成
├── infra/relay/      # T3 Connect 云控制面与边缘入口
├── native/resource-monitor/ # Rust 资源采集器
├── docs/             # 用户与维护者文档
└── scripts/          # 构建、发布、截图、仓库同步

依赖方向比目录名重要

核心包的允许依赖方向
contracts shared client-runtime web mobile desktop server effect-codex-app-server effect-acp ssh / tailscale infra/relay

contracts 是协议,不是共享杂物箱

它承载跨进程必须一致的 Schema:RPC 输入/输出、错误、领域命令与事件、Provider 配置、执行环境和权限范围。 Server 和客户端都依赖它,但它不应反向依赖 React、SQLite 或某个 Provider SDK。

rpc.ts 集中定义约 95 个 RPC, WsRpcGroup 把它们组合成 Effect RPC group。统一定义让 handler、客户端代理、序列化和错误 Schema 使用同一来源, 代价是这个文件成为协议变更的高冲突区域。

client-runtime 共享的是连接语义

Web 与 Mobile 的 UI 栈不同,但它们面对相同的执行环境:目录发现、租约、WebSocket 建立、快照装载、断线重试、 缓存清理。把这些逻辑放进 client-runtime,可以避免两个产品在“何时算在线”上产生分叉。

重要的是,它没有把 UI 状态也全部抽走。布局、导航、编辑器和原生页面仍留在应用层,共享包只拥有跨端必须一致的运行语义。

协议适配被拆成独立包

Codex 与 ACP 都是足够复杂的双向协议。将它们放进独立 Effect 包有三个好处:

  1. 可以单独测试 JSON-RPC framing、request/response 和 notification;
  2. Server 的 Provider adapter 只做产品语义映射,不重复实现传输;
  3. 上游协议升级时,改动边界清楚。

应用层的不同职责

应用状态权威平台特权失败后能否重建
Server是:事件、投影、session 绑定文件、Git、进程、PTY、网络监听从 SQLite + Provider 状态恢复
Web否:本地缓存与视图状态浏览器 API、Web Worker/WASM从 Server 快照恢复
Desktop后端池与窗口状态部分权威Electron IPC、更新、协议、SSH/WSL可重启内嵌 Server 与 renderer
Mobile否:环境目录和导航状态原生模块、后台/前台生命周期从环境目录与快照恢复
Relay配对/发现/路由元数据边缘网络与隧道配置不应重建业务会话

Workspace 布局与运行数据

源码仓库与用户运行数据是两回事。Server 将数据库、认证状态、日志和 Provider 运行元数据放在自己的 userdata 目录, 工作区只承载被 Agent 修改的项目。设计上必须避免:

  • 把 Server 数据库写进每个项目,污染 Git;
  • 把多个执行环境的缓存混在一起;
  • 让测试误用真实用户数据目录;
  • 用项目路径直接做网络身份,导致移动或重命名后失联。

内部文档 workspace-layout.md 用于说明构建和运行资产的归属。代码中进一步用 environment ID、project ID 和规范化 path 区分网络身份与文件位置。

构建工具为何混合

根脚本通过 Vite+ (vp) 编排 TypeScript 构建、测试、lint 和格式化;不同目标再使用最合适的工具:

  • Web:Vite 系列构建;
  • Desktop:Electron 打包与平台安装器;
  • Mobile:Expo / 原生 iOS、Android 工具链;
  • Server:可在 Node 或 Bun HTTP/PTY 适配器上运行;
  • Resource monitor:Cargo 构建 Rust 二进制。

这不是“一个 bundler 统一天下”的仓库。统一的是顶层命令、版本目录和契约,平台产物仍有独立 pipeline。

变更影响分析

假设要给 Thread 增加一个可同步字段,通常会跨过以下层:

  1. contracts 增加 command/event/schema;
  2. decider 校验并产生事件;
  3. projector 更新内存 read model;
  4. persistence migration / repository 更新持久化投影;
  5. RPC 快照或事件订阅携带新字段;
  6. client-runtime snapshot loader 接收;
  7. Web/Mobile 展示和交互;
  8. 兼容旧事件与旧数据库的测试。

这解释了为何 domain 变更看似“改一个 boolean”却会触及多个包:系统在主动支付跨进程、断线恢复和历史兼容的成本。

源码锚点

可迁移经验

  • 共享稳定语义,不共享所有代码。 跨端包适合连接、协议、Schema,不适合强行统一导航和 UI。
  • 协议实现与产品适配分层。 一个包处理 wire protocol,另一个模块把它映射成领域事件。
  • Monorepo 的价值是原子演化。 客户端、服务端和契约可在同一变更中升级,但仍要用依赖方向维持边界。
  • 平台特权应该显式列出。 一旦知道哪个进程能碰文件、进程和密钥,安全审查与故障归属都会简单很多。

On this page