Hagicode 多 AI 提供者切换与互操作实现方案
在现代开发工具生态中,开发者经常需要使用不同的 AI 编码助手来辅助开发工作。Anthropic 的 Claude Code CLI 和 OpenAI 的 Codex CLI 各有其优势:Claude 以出色的代码理解和长上下文处理能力著称,而 Codex 在代码生成和工具使用方面表现优异。
本文将深入分析 hagicode 项目如何实现多个 AI 提供者的无缝切换与互操作,包括核心架构设计、关键实现细节以及实践中的注意事项。
背景
问题域
hagicode 项目面临的核心挑战是在同一平台中支持多种 AI CLI,让用户能够:
- 根据需求灵活切换不同的 AI 提供者
- 在切换过程中保持会话状态的连续性
- 统一抽象不同 CLI 的 API 差异
- 为未来添加新的 AI 提供者预留扩展空间
技术挑战
- 接口差异统一:Claude Code CLI 通过命令行调用,Codex CLI 使用 JSON 事件流
- 流式响应处理:两种提供者都支持流式响应,但数据格式不同
- 工具调用语义:Claude 和 Codex 对工具调用的表示和生命周期管理不同
- 会话生命周期:需要正确管理每个提供者的会话创建、恢复和终止
分析
架构设计思路
hagicode 采用了提供者模式(Provider Pattern)结合工厂模式来抽象 AI 服务的调用。这种设计的核心思想是:
- 统一接口抽象:定义 IAIProvider 接口作为所有 AI 提供者的统一抽象
- 工厂创建实例:通过 AIProviderFactory 根据类型动态创建对应的提供者实例
- 智能选择逻辑:使用 AIProviderSelector 根据场景和配置自动选择最合适的提供者
- 会话状态管理:通过数据库持久化会话与 CLI 线程的绑定关系
关键组件
组件职责语言IAIProvider统一提供者接口C#AIProviderFactory创建和管理提供者实例C#AIProviderSelector智能选择提供者C#ClaudeCodeCliProviderClaude Code CLI 实现C#CodexCliProviderCodex CLI 实现C#AgentCliManager桌面端 CLI 管理TypeScript解决
1. 核心接口设计
IAIProvider 接口 定义了统一的提供者抽象:- public interface IAIProvider
- {
- /// <summary>
- /// 提供者显示名称
- /// </summary>
- string Name { get; }
- /// <summary>
- /// 是否支持流式响应
- /// </summary>
- bool SupportsStreaming { get; }
- /// <summary>
- /// 提供者能力描述
- /// </summary>
- ProviderCapabilities Capabilities { get; }
- /// <summary>
- /// 执行单个 AI 请求
- /// </summary>
- Task ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default);
- /// <summary>
- /// 执行流式 AI 请求
- /// </summary>
- IAsyncEnumerable StreamAsync(AIRequest request, CancellationToken cancellationToken = default);
- /// <summary>
- /// 检查提供者连接性和响应速度
- /// </summary>
- Task<ProviderTestResult> PingAsync(CancellationToken cancellationToken = default);
- /// <summary>
- /// 发送带嵌入式命令的消息
- /// </summary>
- IAsyncEnumerable SendMessageAsync(
- AIRequest request,
- string? embeddedCommandPrompt = null,
- CancellationToken cancellationToken = default);
- }
复制代码 接口设计的关键特性:
- 统一的请求/响应模型:所有提供者使用相同的 AIRequest 和 AIResponse 类型
- 流式支持:通过 IAsyncEnumerable 统一流式输出
- 能力描述:ProviderCapabilities 描述提供者支持的功能(流式、工具、最大 token 等)
- 嵌入式命令:SendMessageAsync 支持将 OpenSpec 命令嵌入到提示中
2. 提供者类型枚举
- public enum AIProviderType
- {
- ClaudeCodeCli, // Anthropic Claude Code
- OpenCodeCli, // 其他 CLI(可扩展)
- GitHubCopilot, // GitHub Copilot
- CodebuddyCli, // Codebuddy
- CodexCli // OpenAI Codex
- }
复制代码 这个枚举为系统支持的所有提供者提供了类型安全的表示。
3. 工厂模式实现
AIProviderFactory 负责创建和管理提供者实例:- public class AIProviderFactory : IAIProviderFactory
- {
- private readonly ConcurrentDictionary _cache;
- private readonly IOptions _options;
- private readonly IServiceProvider _serviceProvider;
- public Task<IAIProvider?> GetProviderAsync(AIProviderType providerType)
- {
- // 使用缓存避免重复创建
- if (_cache.TryGetValue(providerType, out var cached))
- return Task.FromResult<IAIProvider?>(cached);
- // 从配置中获取提供者配置
- var aiOptions = _options.Value;
- if (!aiOptions.Providers.TryGetValue(providerType, out var config))
- {
- _logger.LogWarning("Provider '{ProviderType}' not found in configuration", providerType);
- return Task.FromResult<IAIProvider?>(null);
- }
- // 根据类型创建提供者
- var provider = providerType switch
- {
- AIProviderType.ClaudeCodeCli =>
- _serviceProvider.GetService(typeof(ClaudeCodeCliProvider)) as IAIProvider,
- AIProviderType.CodexCli =>
- _serviceProvider.GetService(typeof(CodexCliProvider)) as IAIProvider,
- AIProviderType.GitHubCopilot =>
- _serviceProvider.GetService(typeof(CopilotAIProvider)) as IAIProvider,
- _ => null
- };
- if (provider != null)
- {
- _cache[providerType] = provider;
- }
- return Task.FromResult<IAIProvider?>(provider);
- }
- }
复制代码 工厂模式的优势:
- 实例缓存:避免重复创建相同类型的提供者
- 依赖注入:通过 IServiceProvider 创建实例,支持依赖注入
- 配置驱动:从配置文件读取提供者配置
- 异常处理:创建失败时返回 null,便于上层处理
4. 智能选择器
AIProviderSelector 实现提供者选择策略:- public class AIProviderSelector : IAIProviderSelector
- {
- private readonly BusinessLayerConfiguration _configuration;
- private readonly IAIProviderFactory _providerFactory;
- private readonly IMemoryCache _cache;
- public async Task SelectProviderAsync(
- BusinessScenario scenario,
- CancellationToken cancellationToken = default)
- {
- // 1. 尝试从场景映射获取提供者
- if (_configuration.ScenarioProviderMapping.TryGetValue(scenario, out var providerType))
- {
- if (await IsProviderAvailableAsync(providerType, cancellationToken))
- {
- _logger.LogDebug("Selected provider '{Provider}' for scenario '{Scenario}'",
- providerType, scenario);
- return providerType;
- }
- _logger.LogWarning("Configured provider '{Provider}' for scenario '{Scenario}' is not available",
- providerType, scenario);
- }
- // 2. 尝试使用默认提供者
- if (await IsProviderAvailableAsync(_configuration.DefaultProvider, cancellationToken))
- {
- _logger.LogDebug("Using default provider '{Provider}' for scenario '{Scenario}'",
- _configuration.DefaultProvider, scenario);
- return _configuration.DefaultProvider;
- }
- // 3. 尝试回退链
- foreach (var fallbackProvider in _configuration.FallbackChain)
- {
- if (await IsProviderAvailableAsync(fallbackProvider, cancellationToken))
- {
- _logger.LogInformation("Using fallback provider '{Provider}' for scenario '{Scenario}'",
- fallbackProvider, scenario);
- return fallbackProvider;
- }
- }
- // 4. 无法找到可用提供者
- throw new InvalidOperationException(
- $"No available AI provider found for scenario '{scenario}'");
- }
- public async Task<bool> IsProviderAvailableAsync(
- AIProviderType providerType,
- CancellationToken cancellationToken = default)
- {
- var cacheKey = $"provider_available_{providerType}";
- // 使用缓存减少 Ping 调用
- if (_configuration.EnableCache &&
- _cache.TryGetValue<bool>(cacheKey, out var cached))
- {
- return cached;
- }
- var provider = await _providerFactory.GetProviderAsync(providerType);
- var isAvailable = provider != null;
- if (_configuration.EnableCache && isAvailable)
- {
- _cache.Set(cacheKey, isAvailable,
- TimeSpan.FromSeconds(_configuration.CacheExpirationSeconds));
- }
- return isAvailable;
- }
- }
复制代码 选择器策略:
- 场景映射优先:首先检查业务场景是否有特定的提供者映射
- 默认提供者回退:场景映射失败时使用默认提供者
- 回退链兜底:逐个尝试回退链中的提供者
- 可用性缓存:缓存提供者可用性检查结果,减少 Ping 调用
5. Claude Code CLI 提供者实现
- public class ClaudeCodeCliProvider : IAIProvider
- {
- private readonly ILogger<ClaudeCodeCliProvider> _logger;
- private readonly IClaudeStreamManager _streamManager;
- private readonly ProviderConfiguration _config;
- public string Name => "ClaudeCodeCli";
- public bool SupportsStreaming => true;
- public ProviderCapabilities Capabilities { get; }
- public async Task ExecuteAsync(AIRequest request, CancellationToken cancellationToken = default)
- {
- _logger.LogInformation("Executing AI request with provider: {Provider}", Name);
- var sessionOptions = ClaudeRequestMapper.MapToSessionOptions(request, _config);
- var messages = _streamManager.SendMessageAsync(request.Prompt, sessionOptions, cancellationToken);
- var responseBuilder = new StringBuilder();
- ResultMessage? finalResult = null;
- await foreach (var streamMessage in messages)
- {
- switch (streamMessage.Message)
- {
- case ResultMessage result:
- finalResult = result;
- responseBuilder.Append(result.Result);
- break;
- }
- }
- if (finalResult != null)
- {
- return ClaudeResponseMapper.MapToAIResponse(finalResult, Name);
- }
- return new AIResponse
- {
- Content = responseBuilder.ToString(),
- FinishReason = FinishReason.Unknown,
- Provider = Name
- };
- }
- }
复制代码 Claude Code CLI 提供者的特点:
- 流式管理器集成:使用 IClaudeStreamManager 与 Claude CLI 通信
- CessionId 会话隔离:使用 CessionId 作为会话唯一标识,与系统 sessionId 区分
- 工作目录配置:支持配置工作目录、权限模式等
- 工具支持:支持 AllowedTools、DisallowedTools 等工具权限配置
6. Codex CLI 提供者实现
- public class CodexCliProvider : IAIProvider
- {
- private readonly ILogger _logger;
- private readonly CodexSettings _settings;
- private readonly ConcurrentDictionary<string, string> _sessionThreadBindings;
- public string Name => "CodexCli";
- public bool SupportsStreaming => true;
- public ProviderCapabilities Capabilities { get; }
- public async IAsyncEnumerable StreamAsync(
- AIRequest request,
- [EnumeratorCancellation] CancellationToken cancellationToken = default)
- {
- _logger.LogInformation("Executing streaming AI request with provider: {Provider}", Name);
- var codex = CreateCodexClient();
- var thread = ResolveThread(codex, request);
- var currentTurn = 0;
- var activeToolCalls = new Dictionary<string, AIToolCallDelta>();
- await foreach (var threadEvent in thread.RunStreamedAsync(BuildPrompt(request), cancellationToken))
- {
- if (threadEvent is TurnStartedEvent)
- {
- currentTurn++;
- }
- switch (threadEvent)
- {
- case ItemCompletedEvent { Item: AgentMessageItem message }:
- var messageText = message.Text ?? string.Empty;
- yield return new AIStreamingChunk
- {
- Content = messageText,
- Type = StreamingChunkType.ContentDelta,
- IsComplete = false
- };
- break;
- case ItemStartedEvent or ItemUpdatedEvent or ItemCompletedEvent:
- var toolChunk = BuildToolChunk(threadEvent, currentTurn);
- if (toolChunk?.ToolCallDelta != null)
- {
- yield return toolChunk;
- }
- break;
- case TurnCompletedEvent turnCompleted:
- activeToolCalls.Clear();
- yield return new AIStreamingChunk
- {
- Content = string.Empty,
- Type = StreamingChunkType.Metadata,
- IsComplete = true,
- Usage = MapUsage(turnCompleted.Usage)
- };
- break;
- }
- }
- BindSessionThread(request.SessionId, thread.Id);
- }
- private CodexThread ResolveThread(Codex codex, AIRequest request)
- {
- var sessionId = request.SessionId;
- // 检查是否已有绑定的线程
- if (!string.IsNullOrWhiteSpace(sessionId) &&
- _sessionThreadBindings.TryGetValue(sessionId, out var threadId) &&
- !string.IsNullOrWhiteSpace(threadId))
- {
- _logger.LogInformation("Resuming Codex thread {ThreadId} for session {SessionId}", threadId, sessionId);
- return codex.ResumeThread(threadId, threadOptions);
- }
- _logger.LogInformation("Starting new Codex thread for session {SessionId}", sessionId ?? "(none)");
- return codex.StartThread(threadOptions);
- }
- }
复制代码 Codex CLI 提供者的特点:
- JSON 事件流处理:解析 Codex 的 JSON 事件流(TurnStarted、ItemStarted、TurnCompleted 等)
- 会话线程绑定:使用 SQLite 数据库持久化会话与线程的绑定关系
- 线程复用:支持恢复已有线程,保持会话连续性
- 工具调用追踪:追踪活动工具调用状态,正确处理工具生命周期
7. 会话线程绑定机制
Codex CLI 使用 SQLite 数据库持久化会话与线程的绑定:- public class CodexCliProvider : IAIProvider
- {
- private const int SessionThreadBindingRetentionDays = 30;
- private readonly ConcurrentDictionary<string, string> _sessionThreadBindings;
- private readonly string _sessionThreadBindingDatabaseConnectionString;
- private readonly string _sessionThreadBindingDatabasePath;
- private void BindSessionThread(string? sessionId, string? threadId)
- {
- if (string.IsNullOrWhiteSpace(sessionId) || string.IsNullOrWhiteSpace(threadId))
- {
- return;
- }
- // 内存缓存
- _sessionThreadBindings.AddOrUpdate(sessionId, threadId, (_, _) => threadId);
- // 持久化到 SQLite
- PersistSessionThreadBinding(sessionId, threadId);
- }
- private void PersistSessionThreadBinding(string sessionId, string threadId)
- {
- try
- {
- using var connection = new SqliteConnection(_sessionThreadBindingDatabaseConnectionString);
- connection.Open();
- using var upsertCommand = connection.CreateCommand();
- upsertCommand.CommandText =
- """
- INSERT INTO SessionThreadBindings (SessionId, ThreadId, CreatedAtUtc, UpdatedAtUtc)
- VALUES ($sessionId, $threadId, $createdAtUtc, $updatedAtUtc)
- ON CONFLICT(SessionId) DO UPDATE SET
- ThreadId = excluded.ThreadId,
- UpdatedAtUtc = excluded.UpdatedAtUtc;
- """;
- var nowUtc = DateTimeOffset.UtcNow.ToString("O");
- upsertCommand.Parameters.AddWithValue("$sessionId", sessionId);
- upsertCommand.Parameters.AddWithValue("$threadId", threadId);
- upsertCommand.Parameters.AddWithValue("$createdAtUtc", nowUtc);
- upsertCommand.Parameters.AddWithValue("$updatedAtUtc", nowUtc);
- upsertCommand.ExecuteNonQuery();
- }
- catch (Exception ex)
- {
- _logger.LogWarning(
- ex,
- "Failed to persist Codex session-thread binding for session {SessionId} to {DatabasePath}",
- sessionId,
- _sessionThreadBindingDatabasePath);
- }
- }
- private void LoadPersistedSessionThreadBindings()
- {
- using var connection = new SqliteConnection(_sessionThreadBindingDatabaseConnectionString);
- connection.Open();
- using var loadCommand = connection.CreateCommand();
- loadCommand.CommandText = "SELECT SessionId, ThreadId FROM SessionThreadBindings;";
- using var reader = loadCommand.ExecuteReader();
- while (reader.Read())
- {
- var sessionId = reader.GetString(0);
- var threadId = reader.GetString(1);
- _sessionThreadBindings[sessionId] = threadId;
- }
- }
- }
复制代码 会话线程绑定的优势:
- 会话恢复:系统重启后可以恢复之前的会话
- 线程复用:同一会话可以复用已有的 Codex 线程
- 自动清理:超过 30 天的绑定会被自动清理
8. 桌面端 CLI 管理
hagicode-desktop 通过 AgentCliManager 管理 CLI 选择:- export enum AgentCliType {
- ClaudeCode = 'claude-code',
- Codex = 'codex',
- // 未来可扩展: Aider, Cursor 等其他 CLI
- }
- export class AgentCliManager {
- private static readonly STORE_KEY = 'agentCliSelection';
- private static readonly EXECUTOR_TYPE_MAP: Record = {
- [AgentCliType.ClaudeCode]: 'ClaudeCodeCli',
- [AgentCliType.Codex]: 'CodexCli',
- };
- constructor(private store: any) {}
- async saveSelection(cliType: AgentCliType): Promise<void> {
- const selection: StoredAgentCliSelection = {
- cliType,
- isSkipped: false,
- selectedAt: new Date().toISOString(),
- };
- this.store.set(AgentCliManager.STORE_KEY, selection);
- }
- loadSelection(): StoredAgentCliSelection {
- return this.store.get(AgentCliManager.STORE_KEY, {
- cliType: null,
- isSkipped: false,
- selectedAt: null,
- });
- }
- getCommandName(cliType: AgentCliType): string {
- switch (cliType) {
- case AgentCliType.ClaudeCode:
- return 'claude';
- case AgentCliType.Codex:
- return 'codex';
- default:
- return 'claude';
- }
- }
- getExecutorType(cliType: AgentCliType | null): string {
- if (!cliType) return 'ClaudeCodeCli';
- return this.EXECUTOR_TYPE_MAP[cliType] || 'ClaudeCodeCli';
- }
- }
复制代码 桌面端 IPC 处理器示例:- ipcMain.handle('llm:call-api', async (event, manifestPath, region) => {
- if (!state.llmInstallationManager) {
- return { success: false, error: 'LLM Installation Manager not initialized' };
- }
- try {
- const prompt = await state.llmInstallationManager.loadPrompt(manifestPath, region);
- // 根据用户选择确定 CLI 命令
- let commandName = 'claude';
- if (state.agentCliManager) {
- const selectedCliType = state.agentCliManager.getSelectedCliType();
- if (selectedCliType) {
- commandName = state.agentCliManager.getCommandName(selectedCliType);
- }
- }
- // 使用对应的 CLI 执行
- const result = await state.llmInstallationManager.callApi(
- prompt.filePath,
- event.sender,
- commandName
- );
- return result;
- } catch (error) {
- return {
- success: false,
- error: error instanceof Error ? error.message : 'Unknown error'
- };
- }
- });
复制代码 9. Codex 内部的模型提供者系统
Codex 本身也支持多种模型提供者,通过 ModelProviderInfo 配置:- pub const OPENAI_PROVIDER_NAME: &str = "OpenAI";
- pub const OLLAMA_OSS_PROVIDER_ID: &str = "ollama";
- pub const LMSTUDIO_OSS_PROVIDER_ID: &str = "lmstudio";
- pub fn built_in_model_providers() -> HashMap<String, ModelProviderInfo> {
- use ModelProviderInfo as P;
- [
- ("openai", P::create_openai_provider()),
- (OLLAMA_OSS_PROVIDER_ID, create_oss_provider(DEFAULT_OLLAMA_PORT, WireApi::Responses)),
- (LMSTUDIO_OSS_PROVIDER_ID, create_oss_provider(DEFAULT_LMSTUDIO_PORT, WireApi::Responses)),
- ]
- .into_iter()
- .map(|(k, v)| (k.to_string(), v))
- .collect()
- }
- pub struct ModelProviderInfo {
- pub name: String,
- pub base_url: Option<String>,
- pub env_key: Option<String>,
- pub query_params: Option<HashMap<String, String>>,
- pub http_headers: Option<HashMap<String, String>>,
- pub request_max_retries: Option<u64>,
- pub stream_max_retries: Option<u64>,
- pub stream_idle_timeout_ms: Option<u64>,
- pub requires_openai_auth: bool,
- pub supports_websockets: bool,
- }
复制代码 Codex 的模型提供者支持:
- 内置提供者:OpenAI、Ollama、LM Studio
- 自定义提供者:用户可在 config.toml 中添加自定义提供者
- 重试策略:可配置请求和流的重试次数
- WebSocket 支持:部分提供者支持 WebSocket 传输
实践
配置示例
appsettings.json 配置多个提供者:- {
- "AI": {
- "Providers": {
- "DefaultProvider": "ClaudeCodeCli",
- "Providers": {
- "ClaudeCodeCli": {
- "Type": "ClaudeCodeCli",
- "Model": "claude-sonnet-4-20250514",
- "WorkingDirectory": "/path/to/workspace",
- "PermissionMode": "acceptEdits",
- "AllowedTools": ["file-edit", "command-run", "bash"]
- },
- "CodexCli": {
- "Type": "CodexCli",
- "Model": "gpt-4.1",
- "ExecutablePath": "codex",
- "SandboxMode": "enabled",
- "WebSearchMode": "auto",
- "NetworkAccessEnabled": false
- }
- },
- "ScenarioProviderMapping": {
- "CodeAnalysis": "ClaudeCodeCli",
- "CodeGeneration": "CodexCli",
- "Refactoring": "ClaudeCodeCli",
- "Debugging": "CodexCli"
- },
- "FallbackChain": ["CodexCli", "ClaudeCodeCli"]
- },
- "Selector": {
- "EnableCache": true,
- "CacheExpirationSeconds": 300
- }
- }
- }
复制代码 使用示例 - 后端服务
- public class AIOrchestrator
- {
- private readonly IAIProviderFactory _providerFactory;
- private readonly IAIProviderSelector _providerSelector;
- private readonly ILogger _logger;
- public AIOrchestrator(
- IAIProviderFactory providerFactory,
- IAIProviderSelector providerSelector,
- ILogger logger)
- {
- _providerFactory = providerFactory;
- _providerSelector = providerSelector;
- _logger = logger;
- }
- public async Task ProcessRequestAsync(
- AIRequest request,
- BusinessScenario scenario)
- {
- _logger.LogInformation("Processing request for scenario: {Scenario}", scenario);
- try
- {
- // 智能选择提供者
- var providerType = await _providerSelector.SelectProviderAsync(scenario, request.CancellationToken);
- // 获取提供者实例
- var provider = await _providerFactory.GetProviderAsync(providerType);
- if (provider == null)
- {
- throw new InvalidOperationException($"Provider {providerType} not available");
- }
- _logger.LogInformation("Using provider: {Provider} for request", provider.Name);
- // 执行请求
- var response = await provider.ExecuteAsync(request, request.CancellationToken);
- _logger.LogInformation("Request completed with provider: {Provider}, tokens used: {Tokens}",
- provider.Name,
- response.Usage?.TotalTokens ?? 0);
- return response;
- }
- catch (Exception ex)
- {
- _logger.LogError(ex, "Failed to process request for scenario: {Scenario}", scenario);
- throw;
- }
- }
- }
复制代码 使用示例 - 流式响应
- public async IAsyncEnumerable StreamResponseAsync(
- AIRequest request,
- BusinessScenario scenario)
- {
- var providerType = await _providerSelector.SelectProviderAsync(scenario);
- var provider = await _providerFactory.GetProviderAsync(providerType);
- if (provider == null)
- {
- throw new InvalidOperationException($"Provider {providerType} not available");
- }
- await foreach (var chunk in provider.StreamAsync(request))
- {
- // 处理流式块
- switch (chunk.Type)
- {
- case StreamingChunkType.ContentDelta:
- // 实时显示文本内容
- await SendToClientAsync(chunk.Content);
- break;
- case StreamingChunkType.ToolCallDelta:
- // 处理工具调用
- await HandleToolCallAsync(chunk.ToolCallDelta);
- break;
- case StreamingChunkType.Metadata:
- // 处理完成事件和统计
- if (chunk.IsComplete)
- {
- _logger.LogInformation("Stream completed, usage: {@Usage}", chunk.Usage);
- }
- break;
- case StreamingChunkType.Error:
- // 处理错误
- _logger.LogError("Stream error: {Error}", chunk.ErrorMessage);
- throw new InvalidOperationException(chunk.ErrorMessage);
- }
- }
- }
复制代码 使用示例 - OpenSpec 命令
- public async Task<string> ExecuteOpenSpecCommandAsync(
- string command,
- string arguments,
- BusinessScenario scenario)
- {
- var providerType = await _providerSelector.SelectProviderAsync(scenario);
- var provider = await _providerFactory.GetProviderAsync(providerType);
- // 构建嵌入式命令提示
- var commandPrompt = $"""
- Execute the following OpenSpec command:
- Command: {command}
- Arguments: {arguments}
- Please execute this command and return the results.
- """;
- var request = new AIRequest
- {
- Prompt = "Process this command request",
- EmbeddedCommandPrompt = commandPrompt,
- WorkingDirectory = Directory.GetCurrentDirectory()
- };
- var response = await provider.SendMessageAsync(request, commandPrompt);
- return response.Content;
- }
复制代码 注意事项
1. 提供者健康检查
在切换提供者前,建议先调用 PingAsync 确保目标提供者可用:- public async Task<bool> IsProviderHealthyAsync(AIProviderType providerType)
- {
- var provider = await _providerFactory.GetProviderAsync(providerType);
- if (provider == null) return false;
- var testResult = await provider.PingAsync();
- return testResult.Success &&
- testResult.ResponseTimeMs < 5000; // 5 秒内响应视为健康
- }
复制代码 2. 会话隔离
使用 CessionId(Claude)或 ThreadId(Codex)确保会话隔离:
- Claude Code CLI:使用 CessionId 作为会话唯一标识
- Codex CLI:使用 ThreadId 作为会话标识
- // Claude Code CLI 会话选项
- var claudeSessionOptions = new ClaudeSessionOptions
- {
- CessionId = CessionId.New(), // 生成唯一 ID
- WorkingDirectory = workspacePath,
- AllowedTools = allowedTools,
- PermissionMode = PermissionMode.acceptEdits
- };
- // Codex 线程选项
- var codexThreadOptions = new ThreadOptions
- {
- Model = "gpt-4.1",
- SandboxMode = "enabled",
- WorkingDirectory = workspacePath
- };
复制代码 3. 错误处理
提供者不可用时的回退机制要健壮,确保至少有一个可用提供者:- public async Task ExecuteWithFallbackAsync(
- AIRequest request,
- List preferredProviders)
- {
- Exception? lastException = null;
- foreach (var providerType in preferredProviders)
- {
- try
- {
- var provider = await _providerFactory.GetProviderAsync(providerType);
- if (provider == null) continue;
- // 尝试执行
- return await provider.ExecuteAsync(request);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "Provider {ProviderType} failed, trying next", providerType);
- lastException = ex;
- }
- }
- // 所有提供者都失败
- throw new InvalidOperationException(
- "All preferred providers failed. Last error: " + lastException?.Message,
- lastException);
- }
复制代码 4. 配置验证
启动时验证所有配置的提供者设置,避免运行时错误:- public void ValidateConfiguration(AIProviderOptions options)
- {
- foreach (var (providerType, config) in options.Providers)
- {
- // 验证可执行文件路径(CLI 类型提供者)
- if (IsCliBasedProvider(providerType))
- {
- if (string.IsNullOrWhiteSpace(config.ExecutablePath))
- {
- throw new ConfigurationException(
- $"Provider {providerType} requires ExecutablePath");
- }
- if (!File.Exists(config.ExecutablePath))
- {
- throw new ConfigurationException(
- $"Executable not found for {providerType}: {config.ExecutablePath}");
- }
- }
- // 验证 API 密钥(API 类型提供者)
- if (IsApiBasedProvider(providerType))
- {
- if (string.IsNullOrWhiteSpace(config.ApiKey))
- {
- throw new ConfigurationException(
- $"Provider {providerType} requires ApiKey");
- }
- }
- // 验证模型名称
- if (string.IsNullOrWhiteSpace(config.Model))
- {
- _logger.LogWarning("No model configured for {ProviderType}, using default", providerType);
- }
- }
- }
复制代码 5. 缓存管理
提供者实例会被缓存,注意生命周期管理和内存使用:- // 定期清理缓存
- public void ClearInactiveProviders(TimeSpan inactiveThreshold)
- {
- var now = DateTimeOffset.UtcNow;
- var keysToRemove = new List();
- foreach (var (type, instance) in _cache)
- {
- // 假设提供者有 LastUsedTime 属性
- if (instance.LastUsedTime.HasValue &&
- now - instance.LastUsedTime.Value > inactiveThreshold)
- {
- keysToRemove.Add(type);
- }
- }
- foreach (var key in keysToRemove)
- {
- _cache.TryRemove(key, out _);
- _logger.LogInformation("Cleared inactive provider: {Provider}", key);
- }
- }
复制代码 6. 日志记录
详细记录提供者选择、切换和执行过程,便于调试:- public class AIProviderLogging
- {
- private readonly ILogger _logger;
- public void LogProviderSelection(
- BusinessScenario scenario,
- AIProviderType selectedProvider,
- SelectionReason reason)
- {
- _logger.LogInformation(
- "[ProviderSelection] Scenario={Scenario}, Provider={Provider}, Reason={Reason}",
- scenario,
- selectedProvider,
- reason);
- }
- public void LogProviderSwitch(
- AIProviderType fromProvider,
- AIProviderType toProvider,
- string reason)
- {
- _logger.LogWarning(
- "[ProviderSwitch] From={FromProvider} To={ToProvider}, Reason={Reason}",
- fromProvider,
- toProvider,
- reason);
- }
- public void LogProviderError(
- AIProviderType provider,
- Exception error,
- AIRequest request)
- {
- _logger.LogError(error,
- "[ProviderError] Provider={Provider}, RequestLength={Length}, Error={Message}",
- provider,
- request.Prompt.Length,
- error.Message);
- }
- }
复制代码 7. 线程安全
ConcurrentDictionary 等并发集合的使用确保线程安全:- public class ThreadSafeProviderCache
- {
- private readonly ConcurrentDictionary _cache;
- private readonly ReaderWriterLockSlim _lock = new();
- public IAIProvider? GetProvider(AIProviderType type)
- {
- // 读取操作无需锁
- if (_cache.TryGetValue(type, out var provider))
- return provider;
- // 创建需要写锁
- _lock.EnterWriteLock();
- try
- {
- // 双重检查
- if (_cache.TryGetValue(type, out provider))
- return provider;
- var newProvider = CreateProvider(type);
- if (newProvider != null)
- {
- _cache[type] = newProvider;
- }
- return newProvider;
- }
- finally
- {
- _lock.ExitWriteLock();
- }
- }
- }
复制代码 8. 数据库迁移
会话线程绑定数据库结构变更时需要考虑数据迁移:- public class SessionThreadMigration
- {
- public async Task MigrateAsync(string dbPath)
- {
- var version = await GetSchemaVersionAsync(dbPath);
- if (version >= 2) return; // 已是最新版本
- using var connection = new SqliteConnection(dbPath);
- connection.Open();
- // 迁移到 v2:添加 CreatedAtUtc 列
- if (version < 2)
- {
- _logger.LogInformation("Migrating SessionThreadBindings to v2...");
- using var addColumnCommand = connection.CreateCommand();
- addColumnCommand.CommandText = "ALTER TABLE SessionThreadBindings ADD COLUMN CreatedAtUtc TEXT;";
- addColumnCommand.ExecuteNonQuery();
- using var backfillCommand = connection.CreateCommand();
- backfillCommand.CommandText =
- """
- UPDATE SessionThreadBindings
- SET CreatedAtUtc = COALESCE(NULLIF(UpdatedAtUtc, ''), $nowUtc)
- WHERE CreatedAtUtc IS NULL OR CreatedAtUtc = '';
- """;
- backfillCommand.Parameters.AddWithValue("$nowUtc", DateTimeOffset.UtcNow.ToString("O"));
- backfillCommand.ExecuteNonQuery();
- }
- await UpdateSchemaVersionAsync(dbPath, 2);
- _logger.LogInformation("Migration to v2 completed");
- }
- }
复制代码 总结
hagicode 通过提供者模式、工厂模式和选择器模式的组合,实现了一个灵活、可扩展的多 AI 提供者架构:
- 统一接口抽象:IAIProvider 接口屏蔽了不同 CLI 的差异
- 动态实例创建:AIProviderFactory 支持运行时创建提供者实例
- 智能选择策略:AIProviderSelector 实现场景驱动的提供者选择
- 会话状态持久化:通过数据库绑定确保会话连续性
- 桌面端集成:AgentCliManager 支持用户选择和配置
这种架构设计的优势在于:
- 可扩展性:添加新的 AI 提供者只需实现 IAIProvider 接口
- 可测试性:提供者可以独立测试和模拟
- 可维护性:每个提供者的实现独立,职责单一
- 用户友好:支持场景自动选择和手动切换
通过这种设计,hagicode 成功实现了 Claude Code CLI 和 Codex CLI 的无缝切换与互操作,为开发者提供了灵活、强大的 AI 编码助手体验。
参考资料
- HagiCode 项目地址:github.com/HagiCode-org/site
- HagiCode 官网:hagicode.com
- Claude Code 官方文档:docs.anthropic.com
- OpenAI Codex 文档:platform.openai.com
- Codex SDK 官方仓库:github.com/openai/codex
- HagiCode 多平台 CLI 支持:https://docs.hagicode.com/blog/hagicode-ai-cli-multi-platform-support/
感谢您的阅读,如果您觉得本文有用,快点击下方点赞按钮
来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作! |