目录
  1. 1. 一、Bridge 解决的问题
  2. 2. 二、核心类型定义 (types.ts, 263行)
    1. 2.1. 2.1 BridgeConfig — 桥接配置
    2. 2.2. 2.2 SpawnMode — 会话隔离策略
    3. 2.3. 2.3 WorkSecret — 工作密钥
    4. 2.4. 2.4 SessionHandle — 子进程控制句柄
  3. 3. 三、Bridge 主循环 (bridgeMain.ts, 3000行)
    1. 3.1. 3.1 入口函数 runBridgeLoop()
    2. 3.2. 3.2 退避配置
    3. 3.3. 3.3 核心数据结构
    4. 3.4. 3.4 主轮询循环逻辑
    5. 3.5. 3.5 心跳机制 (heartbeatActiveWorkItems)
    6. 3.6. 3.6 会话完成回调 (onSessionDone)
  4. 4. 四、容量唤醒原语 (capacityWake.ts)
  5. 5. 五、会话生成器 (sessionRunner.ts, 551行)
    1. 5.1. 5.1 子进程生成
    2. 5.2. 5.2 活动追踪
  6. 6. 六、Bridge API 客户端 (bridgeApi.ts, 540行)
    1. 6.1. 6.1 API端点
    2. 6.2. 6.2 安全:ID验证
    3. 6.3. 6.3 OAuth 401重试
  7. 7. 七、Work Secret 解码 (workSecret.ts, 128行)
  8. 8. 八、JWT 令牌管理 (jwtUtils.ts, 257行)
    1. 8.1. 8.1 JWT解码(不验签)
    2. 8.2. 8.2 主动刷新调度器
  9. 9. 九、轮询配置 (pollConfig.ts + pollConfigDefaults.ts)
  10. 10. 十、Bridge 启用检查 (bridgeEnabled.ts, 203行)
  11. 11. 十一、REPL Bridge (replBridge.ts, 2407行)
  12. 12. 十二、完整的会话生命周期图
【Claude Code源码剖析】12-远程桥接 Bridge 系统深度解析

源码路径: src/bridge/ — 31个文件, 核心 bridgeMain.ts (3000行), replBridge.ts (2407行)
功能: 让 claude.ai 网页端远程控制你本地终端里的 Claude Code
实质: 一个工业级的长轮询工作调度器 + 子进程会话管理器


一、Bridge 解决的问题

正常使用 Claude Code:你在终端里输入,Claude 在终端里回答,一切都发生在本地。

Bridge 模式改变了这个拓扑:

┌────────────────────────┐
│ 你的浏览器 (claude.ai) │
│ 或移动端 │
└──────────┬─────────────┘
│ HTTPS / WebSocket

┌────────────────────────┐
│ Anthropic CCR 服务器 │ CCR = Claude Code Remote
│ (云端, Redis Work Queue)│
└──────────┬─────────────┘
│ 长轮询 (HTTP Long Poll)
│ 或 SSE (Server-Sent Events, v2)

┌────────────────────────┐
│ Bridge 进程 (你的机器) │ ← 由 `claude remote-control` 启动
│ ├─ pollForWork() │
│ ├─ spawn child Claude │
│ └─ 转发消息来回 │
└──────────┬─────────────┘
│ 子进程 stdin/stdout

┌────────────────────────┐
│ Claude Code 子进程 │ ← 用 --sdk-url 连接到 CCR 会话
│ (独立的 Claude Code) │ 实际执行工具调用
└────────────────────────┘

用途:你人在外面用手机,但想让家里的机器执行代码修改。或者你在 claude.ai 网页上工作,但需要访问本地文件系统。


二、核心类型定义 (types.ts, 263行)

2.1 BridgeConfig — 桥接配置

// types.ts 原文:
export type BridgeConfig = {
dir: string // 工作目录
machineName: string // 机器名 (hostname)
branch: string // git 分支
gitRepoUrl: string | null // git 仓库 URL
maxSessions: number // 最大并发会话数
spawnMode: SpawnMode // 会话隔离模式
verbose: boolean // 详细日志
sandbox: boolean // 沙箱模式
bridgeId: string // 客户端生成的 UUID
workerType: string // 'claude_code' | 'claude_code_assistant'
environmentId: string // 用于幂等注册的 UUID
reuseEnvironmentId?: string // 断线重连时复用的后端ID
apiBaseUrl: string // API 基础 URL
sessionIngressUrl: string // WebSocket 入口 URL
debugFile?: string // 调试日志路径
sessionTimeoutMs?: number // 会话超时 (默认24小时)
}

2.2 SpawnMode — 会话隔离策略

// types.ts 原文:
/**
* How `claude remote-control` chooses session working directories.
* - `single-session`: one session in cwd, bridge tears down when it ends
* - `worktree`: persistent server, every session gets an isolated git worktree
* - `same-dir`: persistent server, every session shares cwd (can stomp each other)
*/
export type SpawnMode = 'single-session' | 'worktree' | 'same-dir'

三种模式的区别:

模式 并发 隔离 场景
single-session 1个会话 用CWD 默认模式,会话结束Bridge退出
worktree 多个并发 每个会话独立worktree 团队协作,互不干扰
same-dir 多个并发 共享CWD 简单多会话(可能冲突)

2.3 WorkSecret — 工作密钥

// types.ts 原文:
export type WorkSecret = {
version: number // 协议版本 (目前=1)
session_ingress_token: string // JWT, 用于会话API认证
api_base_url: string // API基础URL
sources: Array<{ // 代码来源
type: string
git_info?: { type: string; repo: string; ref?: string; token?: string }
}>
auth: Array<{ type: string; token: string }> // 认证信息
claude_code_args?: Record<string, string> | null // CLI参数
mcp_config?: unknown | null // MCP配置
environment_variables?: Record<string, string> | null // 环境变量
use_code_sessions?: boolean // CCR v2标记
}

2.4 SessionHandle — 子进程控制句柄

// types.ts 原文:
export type SessionHandle = {
sessionId: string
done: Promise<SessionDoneStatus> // 会话完成Promise
kill(): void // 优雅终止
forceKill(): void // 强制终止
activities: SessionActivity[] // 最近10个活动的环形缓冲区
currentActivity: SessionActivity | null // 当前活动
accessToken: string // JWT令牌
lastStderr: string[] // 最近N条stderr
writeStdin(data: string): void // 写入子进程stdin
updateAccessToken(token: string): void // 更新令牌
}

三、Bridge 主循环 (bridgeMain.ts, 3000行)

3.1 入口函数 runBridgeLoop()

这个函数是整个Bridge系统的核心——一个无限轮询循环,管理多个并发会话。

// bridgeMain.ts 原文签名:
export async function runBridgeLoop(
config: BridgeConfig,
environmentId: string,
environmentSecret: string,
api: BridgeApiClient,
spawner: SessionSpawner,
logger: BridgeLogger,
signal: AbortSignal,
backoffConfig: BackoffConfig = DEFAULT_BACKOFF,
initialSessionId?: string,
getAccessToken?: () => string | undefined | Promise<string | undefined>,
): Promise<void>

3.2 退避配置

// bridgeMain.ts 原文:
const DEFAULT_BACKOFF: BackoffConfig = {
connInitialMs: 2_000, // 连接错误初始等待 2s
connCapMs: 120_000, // 最大退避 2分钟
connGiveUpMs: 600_000, // 10分钟放弃
generalInitialMs: 500, // 一般错误初始等待 500ms
generalCapMs: 30_000, // 最大退避 30s
generalGiveUpMs: 600_000, // 10分钟放弃
}

3.3 核心数据结构

循环内部维护多个 Map 来管理并发会话状态:

// bridgeMain.ts 原文 (runBridgeLoop函数内):
const activeSessions = new Map<string, SessionHandle>()
const sessionStartTimes = new Map<string, number>()
const sessionWorkIds = new Map<string, string>()
const sessionCompatIds = new Map<string, string>() // cse_* ↔ session_* 兼容
const sessionIngressTokens = new Map<string, string>() // JWT 用于心跳认证
const sessionTimers = new Map<string, ReturnType<typeof setTimeout>>()
const completedWorkIds = new Set<string>() // 防止重复投递
const sessionWorktrees = new Map<string, { // worktree清理信息
worktreePath: string; worktreeBranch?: string;
gitRoot?: string; hookBased?: boolean
}>()
const timedOutSessions = new Set<string>() // 超时被kill的会话
const titledSessions = new Set<string>() // 已有标题的会话

3.4 主轮询循环逻辑

while (!loopSignal.aborted):
┌─────────────────────────────────────┐
│ pollForWork(environmentId, secret) │ ← HTTP长轮询, 等待云端分配工作
└──────────────┬──────────────────────┘

┌────────────┴────────────┐
│ work == null │ work != null
│ (无工作) │ (有新工作)
▼ ▼
┌────────────────┐ ┌─────────────────────┐
│ 在容量上限? │ │ decodeWorkSecret() │
│ yes → 心跳循环 │ │ 解码 base64url JWT │
│ no → sleep后 │ └──────────┬──────────┘
│ 继续轮询 │ │
└────────────────┘ ┌──────────┴──────────┐
│ acknowledgeWork() │ ← 告知服务器"我接了这个任务"
└──────────┬──────────┘

┌──────────┴──────────┐
│ 是否已有该session? │
│ (existingHandle) │
└───┬─────────┬───────┘
│ 是 │ 否
▼ ▼
更新token spawn新会话:
(reconnect) 1. worktree模式? → createAgentWorktree()
2. buildSdkUrl() → ws(s)://...
3. spawner.spawn(opts, dir)
4. 注册超时定时器
5. 注册token刷新调度器
6. onSessionDone回调

3.5 心跳机制 (heartbeatActiveWorkItems)

当Bridge已满载(activeSessions >= maxSessions)时,不再接受新工作,但需要通过心跳维持已有会话的租约:

// bridgeMain.ts 原文:
async function heartbeatActiveWorkItems(): Promise<
'ok' | 'auth_failed' | 'fatal' | 'failed'
> {
let anySuccess = false
let anyFatal = false
const authFailedSessions: string[] = []
for (const [sessionId] of activeSessions) {
const workId = sessionWorkIds.get(sessionId)
const ingressToken = sessionIngressTokens.get(sessionId)
if (!workId || !ingressToken) continue
try {
await api.heartbeatWork(environmentId, workId, ingressToken)
anySuccess = true
} catch (err) {
// 401/403 → 令牌过期, 触发reconnectSession
// 404/410 → 环境过期, fatal
if (err instanceof BridgeFatalError) {
if (err.status === 401 || err.status === 403) {
authFailedSessions.push(sessionId)
} else {
anyFatal = true
}
}
}
}
// JWT过期 → 触发服务端重新分发
for (const sessionId of authFailedSessions) {
await api.reconnectSession(environmentId, sessionId)
}
// ...
}

3.6 会话完成回调 (onSessionDone)

每个会话结束时的清理流程:

onSessionDone(sessionId, startTime, handle) → (status):
1. 从所有Map中删除该session
2. 清除超时定时器
3. 取消token刷新
4. capacityWake.wake() ← 唤醒容量等待,立即接受新工作
5. status == 'completed' → stopWork + archiveSession
6. status == 'failed' → 记录错误日志
7. worktree? → removeAgentWorktree() 清理
8. single-session模式 → abort整个循环,Bridge退出
9. multi-session模式 → 继续轮询新工作

四、容量唤醒原语 (capacityWake.ts)

这是一个精巧的并发原语,解决”在满载时sleep但会话结束时立即醒来”的问题:

// capacityWake.ts 完整源码:
export function createCapacityWake(outerSignal: AbortSignal): CapacityWake {
let wakeController = new AbortController()

function wake(): void {
wakeController.abort() // 终止当前sleep
wakeController = new AbortController() // 重置,等待下次
}

function signal(): CapacitySignal {
const merged = new AbortController()
const abort = (): void => merged.abort()
// 合并两个信号: 外部abort OR 容量唤醒
outerSignal.addEventListener('abort', abort, { once: true })
const capSig = wakeController.signal
capSig.addEventListener('abort', abort, { once: true })
return {
signal: merged.signal,
cleanup: () => { // sleep正常结束时清理listener
outerSignal.removeEventListener('abort', abort)
capSig.removeEventListener('abort', abort)
},
}
}

return { signal, wake }
}

使用模式:

// 在at-capacity sleep中:
const cap = capacityWake.signal()
await sleep(pollInterval, cap.signal) // 被wake()或shutdown中断
cap.cleanup()

// 在onSessionDone中:
capacityWake.wake() // 立即唤醒上面的sleep

五、会话生成器 (sessionRunner.ts, 551行)

5.1 子进程生成

SessionRunner 负责 spawn 一个 Claude Code 子进程:

// sessionRunner.ts 的核心:
// spawn 实际调用 child_process.spawn:
const child = spawn(deps.execPath, [
...deps.scriptArgs, // npm安装时需要script路径
'--sdk-url', sdkUrl, // WebSocket连接URL
'--sdk-access-token', accessToken, // JWT令牌
// ... 其他flags
], {
cwd: dir, // 工作目录 (可能是worktree路径)
env: deps.env,
stdio: ['pipe', 'pipe', 'pipe'], // 全管道控制
})

5.2 活动追踪

子进程的stdout是NDJSON格式,SessionRunner解析每行提取活动信息:

// sessionRunner.ts 原文:
const TOOL_VERBS: Record<string, string> = {
Read: 'Reading', Write: 'Writing', Edit: 'Editing',
Bash: 'Running', Glob: 'Searching', Grep: 'Searching',
WebFetch: 'Fetching', WebSearch: 'Searching',
Task: 'Running task', LSP: 'LSP',
// ...
}

function toolSummary(name: string, input: Record<string, unknown>): string {
const verb = TOOL_VERBS[name] ?? name
const target = input.file_path ?? input.command?.slice(0, 60) ?? input.url ?? ''
return target ? `${verb} ${target}` : verb
}

六、Bridge API 客户端 (bridgeApi.ts, 540行)

6.1 API端点

POST   /v1/environments/bridge         → 注册Bridge环境
GET /v1/environments/{id}/work → 长轮询获取工作
POST /v1/environments/{id}/work/{workId}/ack → 确认接收工作
POST /v1/environments/{id}/work/{workId}/stop → 停止工作
DELETE /v1/environments/{id} → 注销环境
POST /v1/environments/{id}/work/{workId}/heartbeat → 心跳
POST /v1/environments/{id}/sessions/{sid}/reconnect → 重连
POST /v1/sessions/{id}/archive → 归档会话
POST /v1/sessions/{id}/events → 发送权限响应

6.2 安全:ID验证

// bridgeApi.ts 原文:
const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/

/**
* Validate that a server-provided ID is safe to interpolate into a URL path.
* Prevents path traversal (e.g. `../../admin`).
*/
export function validateBridgeId(id: string, label: string): string {
if (!id || !SAFE_ID_PATTERN.test(id)) {
throw new Error(`Invalid ${label}: contains unsafe characters`)
}
return id
}

6.3 OAuth 401重试

// bridgeApi.ts 原文:
async function withOAuthRetry<T>(
fn: (accessToken: string) => Promise<{ status: number; data: T }>,
context: string,
): Promise<{ status: number; data: T }> {
const accessToken = resolveAuth()
const response = await fn(accessToken)
if (response.status !== 401) return response
// 401 → 尝试刷新token → 重试一次
const refreshed = await deps.onAuth401?.(accessToken)
if (refreshed) {
const newToken = resolveAuth()
return fn(newToken)
}
return response // 刷新失败,返回401
}

七、Work Secret 解码 (workSecret.ts, 128行)

// workSecret.ts 原文:
export function decodeWorkSecret(secret: string): WorkSecret {
const json = Buffer.from(secret, 'base64url').toString('utf-8')
const parsed = jsonParse(json)
// 验证 version === 1
// 验证 session_ingress_token 非空
// 验证 api_base_url 存在
return parsed as WorkSecret
}

/** SDK URL 构建 (v1: WebSocket) */
export function buildSdkUrl(apiBaseUrl: string, sessionId: string): string {
const isLocalhost = apiBaseUrl.includes('localhost')
const protocol = isLocalhost ? 'ws' : 'wss'
const version = isLocalhost ? 'v2' : 'v1' // 本地直连v2, 生产Envoy重写v1→v2
const host = apiBaseUrl.replace(/^https?:\/\//, '').replace(/\/+$/, '')
return `${protocol}://${host}/${version}/session_ingress/ws/${sessionId}`
}

/** Session ID 比较 (忽略tag前缀) */
export function sameSessionId(a: string, b: string): boolean {
if (a === b) return true
// cse_xxx 和 session_xxx 的body部分相同即视为同一session
const aBody = a.slice(a.lastIndexOf('_') + 1)
const bBody = b.slice(b.lastIndexOf('_') + 1)
return aBody.length >= 4 && aBody === bBody
}

八、JWT 令牌管理 (jwtUtils.ts, 257行)

8.1 JWT解码(不验签)

// jwtUtils.ts 原文:
export function decodeJwtPayload(token: string): unknown | null {
// 剥离 sk-ant-si- 前缀
const jwt = token.startsWith('sk-ant-si-')
? token.slice('sk-ant-si-'.length) : token
const parts = jwt.split('.')
if (parts.length !== 3 || !parts[1]) return null
return jsonParse(Buffer.from(parts[1], 'base64url').toString('utf8'))
}

export function decodeJwtExpiry(token: string): number | null {
const payload = decodeJwtPayload(token)
return (payload as any)?.exp ?? null // Unix秒
}

8.2 主动刷新调度器

// jwtUtils.ts 原文常量:
const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000 // 过期前5分钟刷新
const FALLBACK_REFRESH_INTERVAL_MS = 30 * 60 * 1000 // 备用30分钟间隔
const MAX_REFRESH_FAILURES = 3 // 最大连续失败次数
const REFRESH_RETRY_DELAY_MS = 60_000 // 刷新重试延迟

调度逻辑:

JWT有效期 5小时 → 在4小时55分时触发刷新
刷新成功 → 解析新JWT的exp → 安排下次刷新
刷新失败 → 重试(最多3次) → 放弃

九、轮询配置 (pollConfig.ts + pollConfigDefaults.ts)

轮询间隔通过 GrowthBook远程配置 实时调整,不需要发版:

// pollConfigDefaults.ts 默认值:
export const DEFAULT_POLL_CONFIG: PollIntervalConfig = {
poll_interval_ms_not_at_capacity: 100, // 空闲时100ms轮询
poll_interval_ms_at_capacity: 0, // 满载时禁用轮询(仅心跳)
non_exclusive_heartbeat_interval_ms: 60_000, // 60s心跳间隔
multisession_poll_interval_ms_not_at_capacity: 1000, // 多会话空闲1s
multisession_poll_interval_ms_partial_capacity: 1000, // 部分容量1s
multisession_poll_interval_ms_at_capacity: 0, // 满载禁用
reclaim_older_than_ms: 5000, // 5s后回收未确认工作
session_keepalive_interval_v2_ms: 120_000, // v2 keepalive 2分钟
}

Zod验证schema确保远程配置不会出错(如误设为10ms导致服务器过载):

// pollConfig.ts: .min(100) 防止fat-finger
// 0-or->=100 refinement: 0=禁用, >=100=有效
// 对象级refine: 必须至少有一种at-capacity存活机制(心跳或轮询)

十、Bridge 启用检查 (bridgeEnabled.ts, 203行)

// bridgeEnabled.ts 原文:
export function isBridgeEnabled(): boolean {
return feature('BRIDGE_MODE')
? isClaudeAISubscriber() && // 必须是claude.ai订阅者
getFeatureValue_CACHED_MAY_BE_STALE('tengu_ccr_bridge', false) // GrowthBook开关
: false // 外部构建中禁用
}

诊断函数 getBridgeDisabledReason() 返回具体原因:

  • 不是claude.ai订阅 → “requires a claude.ai subscription”
  • 缺少profile scope → “requires a full-scope login token”
  • 没有organizationUuid → “Run claude auth login to refresh”
  • GrowthBook未启用 → “not yet enabled for your account”

十一、REPL Bridge (replBridge.ts, 2407行)

REPL Bridge 是另一种运行模式——不启动子进程,而是在当前REPL进程内连接到云端:

标准Bridge:  Bridge进程 → spawn → 子Claude进程 ← WebSocket → CCR
REPL Bridge: 当前Claude进程 ← 直接WebSocket → CCR

REPL Bridge 在你使用 claude.ai 的 “Remote Control” 功能时激活。它将当前REPL会话的消息同步到云端,让网页用户可以看到你的工作。

关键区别:

  • 标准Bridge:独立守护进程,管理多个子会话
  • REPL Bridge:嵌入在现有REPL中,双向同步消息

十二、完整的会话生命周期图

[用户在claude.ai点击 "New Session"]


CCR服务器创建WorkItem → 放入Redis Stream


Bridge pollForWork() 收到WorkResponse

├─ decodeWorkSecret() → 解码JWT + API URL + git info
├─ acknowledgeWork() → 告知服务器"我接了"

├─ SpawnMode判断:
│ ├─ worktree → createAgentWorktree() 创建隔离分支
│ ├─ same-dir → 直接用CWD
│ └─ single-session → 用CWD, 结束后退出

├─ spawner.spawn() → fork子进程
│ ├─ execPath = claude binary
│ ├─ args = [--sdk-url, wss://..., --sdk-access-token, jwt]
│ └─ stdio = pipe (全管道)

├─ 注册超时定时器 (默认24小时)
├─ 注册JWT刷新调度器 (过期前5分钟)


[消息转发循环]

├─ 子进程stdout → 解析NDJSON → 提取活动 → 更新状态显示
├─ 子进程发出permission_request → Bridge转发到CCR → 用户在网页确认
├─ CCR发来用户消息 → 通过子进程stdin注入


[会话结束]

├─ completed → stopWork() + archiveSession()
├─ failed → 记录stderr错误
├─ interrupted → 服务端已知, 不需要通知

├─ worktree? → removeAgentWorktree() 清理
├─ capacityWake.wake() → 立即接受新工作
└─ single-session? → 整个Bridge退出
打赏
  • 微信
  • 支付宝

评论