204 lines
7.7 KiB
Markdown
204 lines
7.7 KiB
Markdown
# 基础设施设计 — IDE 桥接
|
||
|
||
## 文档元数据
|
||
- 项目名称: free-code
|
||
- 文档类型: 基础设施设计
|
||
- 原始代码来源: `../../src/bridge/`(32个文件)
|
||
- 原始设计意图: 将 claude.ai 远程控制、会话启动、工作轮询和 IDE 侧事件流统一到一个可恢复的桥接服务中
|
||
- 交叉引用: [基础设施设计总览](基础设施设计.md) | [核心模块设计-查询引擎](../核心模块设计/核心模块设计-查询引擎-QueryEngine.md)
|
||
|
||
## 设计目标
|
||
IDE 桥接层负责把编辑器、运行时与后台工作流连接起来,提供会话启动、消息转发、任务轮询和工作区隔离能力。它既要支撑单会话模型,也要允许 worktree 隔离与环境注册/注销。
|
||
|
||
## 12.1 IBridgeService 接口
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// IDE 桥接服务 — 实现 claude.ai 远程控制功能
|
||
/// 对应原始 bridgeMain.ts / replBridge.ts
|
||
/// </summary>
|
||
public interface IBridgeService
|
||
{
|
||
/// <summary>注册环境到 claude.ai</summary>
|
||
Task<BridgeEnvironment> RegisterEnvironmentAsync();
|
||
|
||
/// <summary>轮询获取工作项</summary>
|
||
Task<WorkItem?> PollForWorkAsync(CancellationToken ct);
|
||
|
||
/// <summary>启动会话</summary>
|
||
Task<SessionHandle> SpawnSessionAsync(SessionSpawnOptions options);
|
||
|
||
/// <summary>确认工作项</summary>
|
||
Task AcknowledgeWorkAsync(string workId, string sessionToken);
|
||
|
||
/// <summary>发送权限响应</summary>
|
||
Task SendPermissionResponseAsync(string sessionId, PermissionResponse response);
|
||
|
||
/// <summary>心跳(延长工作租约)</summary>
|
||
Task HeartbeatAsync(string workId, string sessionToken);
|
||
|
||
/// <summary>停止工作</summary>
|
||
Task StopWorkAsync(string workId);
|
||
|
||
/// <summary>注销环境</summary>
|
||
Task DeregisterEnvironmentAsync();
|
||
|
||
/// <summary>桥接状态</summary>
|
||
BridgeStatus Status { get; }
|
||
}
|
||
```
|
||
|
||
## 12.2 BridgeConfig 与 SpawnMode
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// 桥接配置 — 对应原始 BridgeConfig
|
||
/// </summary>
|
||
public record BridgeConfig
|
||
{
|
||
public required string Directory { get; init; }
|
||
public required string MachineName { get; init; }
|
||
public required string Branch { get; init; }
|
||
public string? GitRepoUrl { get; init; }
|
||
public int MaxSessions { get; init; } = 1;
|
||
public SpawnMode SpawnMode { get; init; } = SpawnMode.Worktree;
|
||
public bool Verbose { get; init; }
|
||
public bool Sandbox { get; init; }
|
||
public required Guid BridgeId { get; init; }
|
||
public required string WorkerType { get; init; } // "claude_code" | "claude_code_assistant"
|
||
public required Guid EnvironmentId { get; init; }
|
||
public string? ReuseEnvironmentId { get; init; }
|
||
public required string ApiBaseUrl { get; init; }
|
||
public required string SessionIngressUrl { get; init; }
|
||
public int SessionTimeoutMs { get; init; } = 24 * 60 * 60 * 1000; // 24h
|
||
}
|
||
|
||
/// <summary>会话生成模式</summary>
|
||
public enum SpawnMode
|
||
{
|
||
SingleSession, // 单次会话, 结束即销毁
|
||
Worktree, // 持久, 每会话隔离 git worktree
|
||
SameDir // 持久, 共享工作目录
|
||
}
|
||
```
|
||
|
||
## 12.3 BridgeService 实现
|
||
|
||
```csharp
|
||
public sealed class BridgeService : IBridgeService, IAsyncDisposable
|
||
{
|
||
private readonly IBridgeApiClient _apiClient;
|
||
private readonly ISessionSpawner _sessionSpawner;
|
||
private readonly IBridgeLogger _logger;
|
||
private readonly BridgeConfig _config;
|
||
private readonly ConcurrentDictionary<string, SessionHandle> _activeSessions = new();
|
||
private string? _environmentId;
|
||
private string? _environmentSecret;
|
||
private BridgeStatus _status = BridgeStatus.Idle;
|
||
|
||
public async Task<BridgeEnvironment> RegisterEnvironmentAsync()
|
||
{
|
||
var result = await _apiClient.RegisterBridgeEnvironment(_config);
|
||
_environmentId = result.EnvironmentId;
|
||
_environmentSecret = result.EnvironmentSecret;
|
||
_status = BridgeStatus.Registered;
|
||
return new BridgeEnvironment(result.EnvironmentId, result.EnvironmentSecret);
|
||
}
|
||
|
||
/// <summary>
|
||
/// 主轮询循环 — 对应原始 bridgeMain.ts 的 pollForWork loop
|
||
/// </summary>
|
||
public async Task RunAsync(CancellationToken ct)
|
||
{
|
||
await RegisterEnvironmentAsync();
|
||
_logger.PrintBanner(_config, _environmentId!);
|
||
|
||
while (!ct.IsCancellationRequested)
|
||
{
|
||
try
|
||
{
|
||
var work = await PollForWorkAsync(ct);
|
||
if (work == null) continue;
|
||
|
||
_status = BridgeStatus.Attached;
|
||
_logger.LogSessionStart(work.Id, work.Data.ToString());
|
||
|
||
// 解码 work secret → 获取 session token
|
||
var secret = DecodeWorkSecret(work.Secret);
|
||
await AcknowledgeWorkAsync(work.Id, secret.SessionIngressToken);
|
||
|
||
// 生成会话 (可能使用 worktree 隔离)
|
||
var sessionDir = await PrepareSessionDirectoryAsync(work);
|
||
var handle = await _sessionSpawner.SpawnAsync(new SessionSpawnOptions
|
||
{
|
||
SessionId = work.Id,
|
||
SdkUrl = secret.ApiBaseUrl,
|
||
AccessToken = secret.SessionIngressToken,
|
||
WorkingDirectory = sessionDir,
|
||
}, ct);
|
||
|
||
_activeSessions[work.Id] = handle;
|
||
|
||
// 等待会话完成
|
||
_ = MonitorSessionAsync(work.Id, handle, ct);
|
||
}
|
||
catch (Exception ex) when (ex is not OperationCanceledException)
|
||
{
|
||
_logger.LogError($"Poll error: {ex.Message}");
|
||
await Task.Delay(CalculateBackoff(), ct);
|
||
}
|
||
}
|
||
}
|
||
|
||
private async Task MonitorSessionAsync(
|
||
string sessionId, SessionHandle handle, CancellationToken ct)
|
||
{
|
||
var doneStatus = await handle.Done;
|
||
_activeSessions.TryRemove(sessionId, out _);
|
||
_logger.LogSessionComplete(sessionId, 0);
|
||
_status = _activeSessions.IsEmpty ? BridgeStatus.Registered : BridgeStatus.Attached;
|
||
}
|
||
|
||
public async ValueTask DisposeAsync()
|
||
{
|
||
foreach (var handle in _activeSessions.Values)
|
||
handle.Kill();
|
||
|
||
if (_environmentId != null)
|
||
await DeregisterEnvironmentAsync();
|
||
}
|
||
}
|
||
```
|
||
|
||
## 12.4 IBridgeApiClient
|
||
|
||
```csharp
|
||
/// <summary>
|
||
/// 桥接 API 客户端 — 对应原始 bridgeApi.ts
|
||
/// </summary>
|
||
public interface IBridgeApiClient
|
||
{
|
||
Task<(string EnvironmentId, string EnvironmentSecret)> RegisterBridgeEnvironment(BridgeConfig config);
|
||
Task<WorkResponse?> PollForWork(string envId, string envSecret, CancellationToken ct);
|
||
Task AcknowledgeWork(string envId, string workId, string sessionToken);
|
||
Task StopWork(string envId, string workId);
|
||
Task DeregisterEnvironment(string envId);
|
||
Task SendPermissionResponseEvent(string sessionId, PermissionResponseEvent evt, string token);
|
||
Task ArchiveSession(string sessionId);
|
||
Task HeartbeatWork(string envId, string workId, string sessionToken);
|
||
}
|
||
```
|
||
|
||
## 12.5 生命周期与传输策略
|
||
|
||
- 环境注册先于轮询;只有拿到 `EnvironmentId` / `EnvironmentSecret` 后才开始接收工作项。
|
||
- `RunAsync` 负责持续轮询,`SpawnSessionAsync` 负责把单个 work item 落到独立会话中。
|
||
- `Worktree` 模式用于最强隔离,`SameDir` 用于低开销复用,`SingleSession` 用于短生命周期任务。
|
||
- 传输层上保持 API 侧长轮询/轮询式控制,避免把 IDE 控制流绑死在单一 socket 连接上。
|
||
- `HeartbeatAsync` 用于延长工作租约,防止长任务被服务端提前回收。
|
||
|
||
## 12.6 补充说明
|
||
|
||
- 该层的重点不是本地执行,而是把远端工作项和本地会话生命周期做稳定映射。
|
||
- 所有状态变化最终都应回写到上层状态存储,避免桥接层形成隐式状态孤岛。
|