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:14001tidy-fox
web--feature-pmnt--my-app.localhost:14003bold-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 冲突,手动改自动透传
可读 URLlocalhost: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 就能用。但问题在于:

Paseo 的整套机制,本质上是把 N 个 Agent × M 个服务的 O(N×M) 手工配置复杂度,降维成一个 paseo.json 声明 + 零配置运行时

10.4 对开发体验的降维打击

场景传统做法Paseo 做法
Agent 跑完要预览记下端口号 → 手动开浏览器 → 输入 localhost:XXXXPaseo 内嵌浏览器,自动打开正确 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 内部的四个核心能力,每一个都可以拆成独立可复用的模块。以下逐一分析接口、可行性、以及如果独立发布会是什么形态。

📦 模块一:Worktree Manager
职责

管理 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)。

🔢 模块二:Port Allocator
职责

为多个并发进程分配互不冲突的端口,支持 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 端口感知)。

🌐 模块三:Dev Reverse Proxy
职责

基于 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 功能强但重在静态配置 → 本模块重在动态路由注册(运行时可增删路由)。

🔍 模块四:Service Discovery Injector
职责

向子进程注入服务和 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 服务,甚至可以跨机器调度。

拆分的代价与边界

⚠️ 不是银弹:拆成独立模块也有一些真实的代价——

一个可能的开放标准:「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,都是一样的体验。