Paseo Internals · 深度版
Paseo 端口分配与 Worktree 机制
深入理解 Paseo 如何让多个 Agent 并行开发同一项目 — 机制、工程意义、以及能否拆成独立模块
一、为什么需要这套机制
当你同时跑多个 AI Coding Agent 操作同一个项目时,会遇到三层冲突:
冲突 1 端口冲突
Agent A 启动 npm run dev 占 :3000,Agent B 再启动直接炸——EADDRINUSE。
冲突 2 WebSocket 冲突
Vite 的 HMR WebSocket 固定用 :24678。HTTP 端口能换,WS 端口不会自动换,多实例必撞。
冲突 3 文件冲突
两个 Agent 同时在 src/ 里写代码、装依赖、跑构建,互相覆盖。
Paseo 从平台层抽象掉这些问题。核心两招:Git Worktree 隔离 + 统一反向代理。
二、第一层隔离:Git Worktree
给每个 Agent 任务创建独立的 Git Worktree——自己的目录、分支、node_modules。
~/.paseo/worktrees/
└── 1vnnm9k3/ ← 源仓库路径的 hash
├── tidy-fox/ ← Agent A:feature-auth 分支
│ ├── src/ node_modules/ paseo.json
└── bold-owl/ ← Agent B:feature-payment 分支,互不干扰
├── src/ node_modules/ paseo.json
1. 创建 worktree→
2. setup→
3. Agent 执行→
4. Review diff→
5. Merge 或 Archive
💡 Worktree 共享 .git 对象库,创建瞬间完成,不额外下载。
三、第二层解决:动态端口分配
paseo.json:项目自描述
{
"worktree": { "setup": "npm ci" },
"scripts": {
"web": {
"type": "service",
"command": "npm run dev -- --port $PASEO_PORT",
"port": 3000
},
"api": {
"type": "service",
"command": "go run . --port $PASEO_PORT"
}
}
}
🔑 核心约定:命令里永远写 $PASEO_PORT 而非 3000。daemon 扫描 paseo.json,给每个 service 分配唯一端口并注入环境变量。
Agent A(feature-auth)
web → $PASEO_PORT=14001
api → $PASEO_PORT=14002
Agent B(feature-pay)
web → $PASEO_PORT=14003
api → $PASEO_PORT=14004
本地(main)
web → $PASEO_PORT=14005
api → $PASEO_PORT=14006
四、第三层统一:反向代理 + 可读 URL
http://<脚本名>--<分支名>--<项目名>.localhost:<daemon端口>
http://web--my-app.localhost:6767 ← 默认分支
http://web--feature-auth--my-app.localhost:6767 ← 特定分支
*.localhost 自动解析到 127.0.0.1,不改 hosts。
| 域名 | → | 端口 | worktree |
web--feature-auth--my-app.localhost | → | :14001 | tidy-fox |
web--feature-pmnt--my-app.localhost | → | :14003 | bold-owl |
web--my-app.localhost | → | :14005 | 主工作区 |
💡 URL 是确定性的——同 worktree 同 service 重启不变,可存书签、写进 CI。
五、WebSocket 自动透传
| 方案 | HTTP 端口 | WS 端口 | WS 代理 |
| 手动 PORT | ✅ | ❌ Vite :24678 | ❌ |
| Nginx | ✅ | ⚠️ Upgrade 头 | ⚠️ 显式配置 |
| Paseo | ✅ | ✅ 自动 | ✅ 零配置 |
浏览器 Paseo Daemon Dev Server
│ GET / HTTP/1.1 │ │
│ Upgrade: websocket ← 关键 │ │
│ ──────────────────────────────→│ │
│ │ 识别 Upgrade → 切换为 TCP 隧道 │
│ │ ─────────────────────────────────→│
│ ←── 101 Switching Protocols ──│ ←── 101 Switching Protocols ──────│
│ ⚡ WebSocket 帧双向流通 ⚡ │ ⚡ 透明转发 ⚡ │
检测到 Upgrade: websocket 后自动升级为 TCP 隧道,Vite HMR / Next.js Fast Refresh 全部正常工作。
六、服务间互相发现
# web 进程里自动注入的环境变量:
PASEO_PORT=14001
PASEO_URL=http://web--feat-x--my-app.localhost:6767
PASEO_SERVICE_API_PORT=14002
PASEO_SERVICE_API_URL=http://api--feat-x--my-app.localhost:6767
🔑 前端代码用 process.env.PASEO_SERVICE_API_URL,永不硬编码端口。
七、完整工作流
$ paseo run --worktree feature-auth --base main "实现登录功能"
┌─────────────────────────────────────────────────────────┐
│ Paseo Daemon │
│ ① 创建 worktree → ② setup(npm ci) → ③ 扫描 paseo.json │
│ ④ 分配端口 web→14001, api→14002 │
│ ⑤ 启动 npm run dev -- --port 14001 / go run . --port 14002 │
│ ⑥ 注册路由 web--feature-auth--my-app.localhost → :14001 │
│ ⑦ Agent 写代码,浏览器预览 + HMR 热更新 ✅ │
└─────────────────────────────────────────────────────────┘
八、有无 Paseo 的对比
| 维度 | 没有 Paseo | 有 Paseo |
| 文件隔离 | 手动 git worktree | 自动 |
| 端口分配 | PORT=3001 手算 | $PASEO_PORT |
| WebSocket | :24678 冲突,手动改 | 自动透传 |
| 可读 URL | localhost:3001 | 分支名.localhost |
| 服务发现 | 硬编码端口号 | $PASEO_SERVICE_* |
| 多端访问 | SSH 端口转发 | 手机/桌面/Web |
| 并行 Agent | 💥 各种冲突 | ✅ 完全隔离 |
九、设计哲学
🎯 声明式
项目声明「有什么服务」,不写「用哪个端口」。端口是运行时概念。
🔀 确定性
同 (worktree, service) 永远同 URL。端口可换,域名不变。
🪞 透明代理
对应用完全透明。dev server 不知道前面有代理。WS 零配置。
🧩 平台层抽象
端口冲突不是业务代码该管的事。提升到基础设施层。
十、工程意义
10.1 端口问题是症状,不是病因
EADDRINUSE 只是表象。真正的工程问题有两个:
🧬 资源命名冲突
端口、工作目录、数据库名、缓存文件——这些是操作系统级的共享资源。多进程并发开发时,没有任何机制保证它们不撞。
🔗 耦合了「开发地址」和「运行环境」
代码里写 localhost:3000 意味着「这段代码只能在 3000 端口跑」。对于一个会被克隆、fork、或同时跑 N 份的项目来说,这是假前提。
10.2 这条思路的工程谱系
Paseo 的做法不是凭空冒出来的。它在工程史上有两条清晰的谱系:
| 谱系 | 代表作 | 解决什么 | Paseo 继承了什么 |
| 十二因子应用 |
12factor.net |
配置与代码分离,端口通过 PORT 环境变量注入 |
$PASEO_PORT 就是这个思想的直接延续 |
| 服务网格 / Sidecar |
Envoy, Linkerd, Istio |
应用不关心网络拓扑,sidecar 透明代理所有流量 |
daemon 作为 sidecar,反向代理 + WS 透传 + 服务发现都是 sidecar 模式 |
| 平台工程 |
Heroku, Fly.io, K8s |
开发者声明意图,平台负责调度资源 |
paseo.json 就是「声明意图」,daemon 就是「调度器」 |
| Nix / Devbox 可复现环境 |
Nix, devenv, Devbox |
开发环境不依赖全局状态 |
Worktree 的 setup hook 保证每个 worktree 的可复现性 |
10.3 为什么这不是「过度工程」
常见反驳:「不就是改个 PORT 环境变量吗?为什么要搞 daemon + 反向代理 + worktree 这么大一套?」
从单 Agent 视角看,确实一把 PORT 就能用。但问题在于:
- N 个 Agent 并行时,需要 N 个不冲突的端口,而且需要知道哪个端口对应哪个 Agent。管理 O(N) 的手工配置很快失控。
- Vite 的 WS 端口(24678)不受 PORT 控制,这是框架层面的硬编码。只改 HTTP 端口不够。
- 多服务项目(前端 + 后端 + 数据库),需要 N×M 个端口的协调编排。
- 跨设备访问,手机/另一台电脑需要知道 VPS 上某 Agent 的端口。纯数字不可读、不可记、不可分享。
Paseo 的整套机制,本质上是把 N 个 Agent × M 个服务的 O(N×M) 手工配置复杂度,降维成一个 paseo.json 声明 + 零配置运行时。
10.4 对开发体验的降维打击
| 场景 | 传统做法 | Paseo 做法 |
| Agent 跑完要预览 | 记下端口号 → 手动开浏览器 → 输入 localhost:XXXX | Paseo 内嵌浏览器,自动打开正确 URL |
| 在手机上检查 Agent 进度 | SSH 到 VPS → docker ps 查端口 → 手机上输入 IP:端口 | Paseo App 扫码即连,自动列出所有 Agent |
| 同事想看 Agent 改了什么 | 你截图/录屏发过去 | 发一个 .localhost URL,他直接打开 |
| CI 里启动预览环境 | 写一堆脚本探测端口、注入 nginx 配置 | paseo run --worktree ci-pr-123,一行 |
10.5 更深远的意义:Agent 时代的「开发环境即服务」
过去十年,CI/CD 把「构建环境」变成可编排的服务。现在 Agent 来了,「开发环境」也需要变成可编排的服务——不只是 Docker 容器这样笨重的方案,而是轻量、瞬时、可按需创建和销毁的 worktree + 动态路由。
Paseo 的这套机制,本质上是 Development Environment as a Service(DEaaS)的最小可行实现:声明一个 paseo.json → 得到一个完整隔离、可预览、可分享、可远程操控的开发环境。
这个方向不再只是「让 Agent 不撞端口」——它是在定义 Agent 时代的开发基础设施标准。
十一、封装为独立模块的可能性
Paseo 内部的四个核心能力,每一个都可以拆成独立可复用的模块。以下逐一分析接口、可行性、以及如果独立发布会是什么形态。
职责
管理 Git worktree 的创建、生命周期钩子、清理。
核心接口
// 创建一个隔离的工作区
func Create(opts CreateOpts) (*Workspace, error)
type CreateOpts struct {
RepoURL string // 源仓库路径
BaseBranch string // 基于哪个分支
Setup []string // 创建后运行的命令
Teardown []string // 销毁前运行的命令
Root string // worktree 存放根目录
}
type Workspace struct {
ID string // 唯一标识(slug)
Path string // 文件系统路径
Branch string // git 分支名
}
func Archive(ws *Workspace) error // 清理并删除
func List(root string) ([]*Workspace, error)
独立可行性
高 —— 可完全独立
不依赖 Paseo 任何其他组件。输入:git 仓库路径 + 配置。输出:独立的工作目录。
独立场景:CI 并行构建、本地多分支开发、教学环境的作业分发。
发布形态:Go library(github.com/xxx/worktree-manager)或独立 CLI(wtm create -b feat-x ./repo)。
职责
为多个并发进程分配互不冲突的端口,支持 HTTP 和 WS 端口分别管理。
核心接口
type Allocator interface {
Allocate(key string, kind PortKind) (int, error)
Release(key string) error
Lookup(key string) (int, bool)
}
type PortKind int
const (
PortHTTP PortKind = iota
PortWS // 针对 Vite 等框架的独立 WS 端口
)
// 两种实现策略:
// 1. HashAllocator —— 基于 key 的哈希确定性地分配,重启不变
// 2. ScanAllocator —— 实时扫描空闲端口,灵活但重启可能变
func NewHashAllocator(rangeStart, rangeEnd int) *HashAllocator
func NewScanAllocator(rangeStart, rangeEnd int) *ScanAllocator
独立可行性
高 —— 可完全独立
零外部依赖。输入:key(如 worktree ID)+ 端口类型。输出:端口号。
独立场景:任何需要管理多个 dev server 的工具、monorepo 开发、E2E 测试并发。
发布形态:Go library / Rust crate(已有类似开源库 comport,但缺少 WS 端口感知)。
职责
基于 Host 头的反向代理,自动路由到对应端口,支持 WebSocket 透传。
核心接口
type Proxy struct { ... }
// 注册一条路由
func (p *Proxy) AddRoute(r Route) error
func (p *Proxy) RemoveRoute(hostname string) error
type Route struct {
Hostname string // "web--feat-x--my-app.localhost"
Target string // "127.0.0.1:14001"
WSPassthrough bool // 是否自动 Upgrade WebSocket
}
// 启动代理服务器
func (p *Proxy) Listen(addr string) error
// 生成标准 hostname
func Hostname(service, branch, project string) string
// → "web--feat-x--my-app.localhost"
独立可行性
中 —— 可独立,但需定义路由管理协议
核心机制简单(Host 头 → TCP 转发 + WS Upgrade 检测),但需要外部系统注册路由。
独立场景:任意多服务本地开发、微服务调试、临时预览环境。
与传统方案差异:Caddy/Traefik 功能强但重在静态配置 → 本模块重在动态路由注册(运行时可增删路由)。
职责
向子进程注入服务和 peer 的地址信息(端口 + URL),实现零配置服务间调用。
核心接口
type EnvInjector struct { ... }
// 注册一个服务
func (inj *EnvInjector) Register(svc Service) error
type Service struct {
Name string // "web"
Port int // 14001
URL string // "http://web--feat-x--my-app.localhost:6767"
Group string // worktree ID,同组服务互相可见
}
// 生成给目标服务的环境变量
func (inj *EnvInjector) EnvFor(svcName string) []string
// → ["PASEO_PORT=14001", "PASEO_SERVICE_API_PORT=14002", ...]
// 启动子进程时注入
func (inj *EnvInjector) StartCommand(svcName string, cmd *exec.Cmd) error
独立可行性
高 —— 可完全独立
纯内存数据结构 + 环境变量注入。输入:服务注册信息。输出:标准化的环境变量列表。
独立场景:docker-compose 替代、monorepo 任务编排、任何需要子进程间通信的工具。
发布形态:Go library,可嵌入任何进程管理器。
模块间的依赖关系
┌──────────────────────────────────────────────────────────┐
│ paseo.json 解析器 │
│ 解析项目声明的 services / setup / teardown │
└──────────┬───────────────────────────────────────────────┘
│ 产出 Service 列表
▼
┌──────────────────┐ ┌──────────────────┐
│ ① Worktree Mgr │ │ ② Port Allocator │
│ 创建隔离工作区 │ │ 为每个 svc 分配端口 │
└────────┬─────────┘ └────────┬─────────┘
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────┐
│ 启动子进程 │◄───│ ④ Env Injector │
│ (exec.Cmd) │ │ 注入 $PASEO_* │
└────────┬─────────┘ └──────────────────┘
│
▼
┌──────────────────┐
│ ③ Reverse Proxy │
│ 注册路由,启动监听 │
└──────────────────┘
四个模块可以独立使用,也可以组合成完整方案。
除了共同依赖「Service 定义」这个数据结构外,彼此之间没有代码耦合。
如果今天就要拆:推荐的独立模块顺序
| 优先级 | 模块 | 理由 | 已有竞品 |
| P0 |
Port Allocator |
需求最普遍、实现最简单、零依赖。任何需要并发 dev server 的场景都用得上。 |
comport (Rust)、find-free-port (npm) |
| P0 |
Worktree Manager |
Agent 并行开发的刚需。git worktree 本身不难,但生命周期管理(setup/teardown/hooks)是空白地带。 |
无直接竞品 |
| P1 |
Env Injector |
配合 Port Allocator 使用,提供「零配置服务发现」体验。 |
Docker Compose 的网络别名(但那是容器层) |
| P1 |
Dev Reverse Proxy |
动态路由注册 + WS 透传在本地开发中是独特需求。可以构建在 Caddy/Traefik 之上,减少造轮子。 |
Caddy(需静态配置或 API 调用) |
独立模块的战略价值
🔌 降低 Agent 工具的开发门槛
如果有人想写一个新的 Agent CLI(不管是给 Claude / Codex / 还是自定义模型),不需要从头实现 worktree + 端口管理。直接引入这 4 个模块,专注做 Agent 逻辑。
🧱 成为 Agent 基础设施标准件
就像 express 之于 Node.js HTTP、docker 之于容器——这些模块可以成为「Agent 开发环境管理」领域的标准组件。生态不需要每个工具都重新发明 worktree 管理。
🧪 可独立测试、独立演进
Port Allocator 可以自己发版、自己积累用户反馈、自己优化哈希算法。不用等 Paseo 整体发布周期。WS 端口感知的 Port Allocator 可以比 Paseo 更快地支持新框架的奇葩端口行为。
🌍 跨语言复用
如果是 Go library,Rust 项目可以通过 FFI 调用。如果发布为独立 CLI + JSON API,任何语言都能用。如果是 gRPC 服务,甚至可以跨机器调度。
拆分的代价与边界
⚠️ 不是银弹:拆成独立模块也有一些真实的代价——
- API 稳定性承诺:独立模块意味着独立 semver。接口一旦发布就有兼容性包袱,不像内部模块可以随意重构。
- 集成测试成本:4 个模块组合使用时,集成问题(如端口分配了但代理没注册上)的责任边界需要额外测试覆盖。
- 跨模块的「Group」概念:当前 Paseo 的 worktree 是隐式的 group——同一 worktree 的 services 自动互相可见。拆开后需要显式传递给每个模块。
- 进程生命周期管理:Reverse Proxy 需要知道目标进程是否还活着(否则路由指向死端口)。这块逻辑目前和 daemon 耦合,拆分需要定义 clear 的 health check 接口。
一个可能的开放标准:「Dev Workspace Spec」
如果这四个模块不只是 Paseo 的内部实现,而是成为社区标准,可以定义一个 dev-workspace.json 规范:
{
"$schema": "https://dev-workspace-spec.dev/schema.json",
"version": "1.0",
"project": "my-app",
"services": {
"web": { "command": "npm run dev", "httpPort": 3000, "wsPort": 24678 },
"api": { "command": "go run .", "httpPort": 8080 }
},
"setup": "npm ci && cp .env.example .env",
"teardown": "rm -rf .cache"
}
这个 spec 可以同时被 Paseo、Multica、CI 系统、甚至 VS Code 插件消费。任何一个实现了 spec 的 runtime(Paseo daemon / 自定义 runner / CI agent),拿到的都是一份标准化的 workspace 描述。
🎯 终局想象:git clone 一个项目 → 它自带 dev-workspace.json → 任何兼容的 runtime 自动创建隔离 worktree + 分配端口 + 启动所有服务 + 生成可预览 URL。不管你用 Paseo、Multica、还是 CI pipeline,都是一样的体验。