源码路径 : 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 — 桥接配置 export type BridgeConfig = { dir : string machineName : string branch : string gitRepoUrl : string | null maxSessions : number spawnMode : SpawnMode verbose : boolean sandbox : boolean bridgeId : string workerType : string environmentId : string reuseEnvironmentId ?: string apiBaseUrl : string sessionIngressUrl : string debugFile ?: string sessionTimeoutMs ?: number }
2.2 SpawnMode — 会话隔离策略 export type SpawnMode = 'single-session' | 'worktree' | 'same-dir'
三种模式的区别 :
模式
并发
隔离
场景
single-session
1个会话
用CWD
默认模式,会话结束Bridge退出
worktree
多个并发
每个会话独立worktree
团队协作,互不干扰
same-dir
多个并发
共享CWD
简单多会话(可能冲突)
2.3 WorkSecret — 工作密钥 export type WorkSecret = { version : number session_ingress_token : string api_base_url : string 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 mcp_config ?: unknown | null environment_variables ?: Record <string , string > | null use_code_sessions ?: boolean }
2.4 SessionHandle — 子进程控制句柄 export type SessionHandle = { sessionId : string done : Promise <SessionDoneStatus > kill (): void forceKill (): void activities : SessionActivity [] currentActivity : SessionActivity | null accessToken : string lastStderr : string [] writeStdin (data : string ): void updateAccessToken (token : string ): void }
三、Bridge 主循环 (bridgeMain.ts, 3000行) 3.1 入口函数 runBridgeLoop() 这个函数是整个Bridge系统的核心——一个无限轮询循环 ,管理多个并发会话。
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 退避配置 const DEFAULT_BACKOFF : BackoffConfig = { connInitialMs : 2_000 , connCapMs : 120_000 , connGiveUpMs : 600_000 , generalInitialMs : 500 , generalCapMs : 30_000 , generalGiveUpMs : 600_000 , }
3.3 核心数据结构 循环内部维护多个 Map 来管理并发会话状态:
const activeSessions = new Map <string , SessionHandle >()const sessionStartTimes = new Map <string , number >()const sessionWorkIds = new Map <string , string >()const sessionCompatIds = new Map <string , string >() const sessionIngressTokens = new Map <string , string >() const sessionTimers = new Map <string , ReturnType <typeof setTimeout >>()const completedWorkIds = new Set <string >() const sessionWorktrees = new Map <string , { worktreePath : string ; worktreeBranch ?: string ; gitRoot ?: string ; hookBased ?: boolean }>() const timedOutSessions = new Set <string >() 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)时,不再接受新工作,但需要通过心跳维持已有会话的租约:
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) { if (err instanceof BridgeFatalError ) { if (err.status === 401 || err.status === 403 ) { authFailedSessions.push (sessionId) } else { anyFatal = true } } } } 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但会话结束时立即醒来”的问题:
export function createCapacityWake (outerSignal : AbortSignal ): CapacityWake { let wakeController = new AbortController () function wake ( ): void { wakeController.abort () wakeController = new AbortController () } function signal ( ): CapacitySignal { const merged = new AbortController () const abort = (): void => merged.abort () outerSignal.addEventListener ('abort' , abort, { once : true }) const capSig = wakeController.signal capSig.addEventListener ('abort' , abort, { once : true }) return { signal : merged.signal , cleanup : () => { outerSignal.removeEventListener ('abort' , abort) capSig.removeEventListener ('abort' , abort) }, } } return { signal, wake } }
使用模式 :
const cap = capacityWake.signal ()await sleep (pollInterval, cap.signal ) cap.cleanup () capacityWake.wake ()
五、会话生成器 (sessionRunner.ts, 551行) 5.1 子进程生成 SessionRunner 负责 spawn 一个 Claude Code 子进程:
const child = spawn (deps.execPath , [ ...deps.scriptArgs , '--sdk-url' , sdkUrl, '--sdk-access-token' , accessToken, ], { cwd : dir, env : deps.env , stdio : ['pipe' , 'pipe' , 'pipe' ], })
5.2 活动追踪 子进程的stdout是NDJSON格式,SessionRunner解析每行提取活动信息:
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验证 const SAFE_ID_PATTERN = /^[a-zA-Z0-9_-]+$/ 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重试 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 const refreshed = await deps.onAuth401 ?.(accessToken) if (refreshed) { const newToken = resolveAuth () return fn (newToken) } return response }
七、Work Secret 解码 (workSecret.ts, 128行) export function decodeWorkSecret (secret : string ): WorkSecret { const json = Buffer .from (secret, 'base64url' ).toString ('utf-8' ) const parsed = jsonParse (json) return parsed as WorkSecret } export function buildSdkUrl (apiBaseUrl : string , sessionId : string ): string { const isLocalhost = apiBaseUrl.includes ('localhost' ) const protocol = isLocalhost ? 'ws' : 'wss' const version = isLocalhost ? 'v2' : 'v1' const host = apiBaseUrl.replace (/^https?:\/\// , '' ).replace (/\/+$/ , '' ) return `${protocol} ://${host} /${version} /session_ingress/ws/${sessionId} ` } export function sameSessionId (a : string , b : string ): boolean { if (a === b) return true 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解码(不验签) export function decodeJwtPayload (token : string ): unknown | null { 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 }
8.2 主动刷新调度器 const TOKEN_REFRESH_BUFFER_MS = 5 * 60 * 1000 const FALLBACK_REFRESH_INTERVAL_MS = 30 * 60 * 1000 const MAX_REFRESH_FAILURES = 3 const REFRESH_RETRY_DELAY_MS = 60_000
调度逻辑:
JWT有效期 5小时 → 在4小时55分时触发刷新 刷新成功 → 解析新JWT的exp → 安排下次刷新 刷新失败 → 重试(最多3次) → 放弃
九、轮询配置 (pollConfig.ts + pollConfigDefaults.ts) 轮询间隔通过 GrowthBook远程配置 实时调整,不需要发版:
export const DEFAULT_POLL_CONFIG : PollIntervalConfig = { poll_interval_ms_not_at_capacity : 100 , poll_interval_ms_at_capacity : 0 , non_exclusive_heartbeat_interval_ms : 60_000 , multisession_poll_interval_ms_not_at_capacity : 1000 , multisession_poll_interval_ms_partial_capacity : 1000 , multisession_poll_interval_ms_at_capacity : 0 , reclaim_older_than_ms : 5000 , session_keepalive_interval_v2_ms : 120_000 , }
Zod验证schema确保远程配置不会出错(如误设为10ms导致服务器过载):
十、Bridge 启用检查 (bridgeEnabled.ts, 203行) export function isBridgeEnabled ( ): boolean { return feature ('BRIDGE_MODE' ) ? isClaudeAISubscriber () && getFeatureValue_CACHED_MAY_BE_STALE ('tengu_ccr_bridge' , false ) : 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退出