跳到主要内容

ClaudeSdkEngine 集成

ClaudeSdkEngine 是对 @anthropic-ai/claude-agent-sdk 的 IEngine 封装。它处理提供商代理(Proxy)设置、API Key 解析、多 Key 负载均衡,以及 Windows 环境下的 Git/Bash 自动配置。

架构图

graph TB
Router["AgentRouter / MagiService"] --> Engine["ClaudeSdkEngine"]

Engine --> EnvBuild["buildProviderEnvWithProxy()"]
Engine --> AgentSvc["AgentService"]
Engine --> GitRt["GitRuntimeService<br/>(Windows)"]

EnvBuild --> Decision{"路径?"}
Decision -->|"cliBackend='claude-code'<br/>(订阅 OAuth)"| PassThrough["AgentProxyServer<br/>passThrough 模式"]
Decision -->|"id='anthropic'<br/>且无流回调"| Direct["直接传递 API Key<br/>env: ANTHROPIC_API_KEY"]
Decision -->|"其他提供商"| ProxySvc["AgentProxyServer<br/>转换链 / 直传"]

PassThrough --> Anthropic["api.anthropic.com<br/>透传 SDK 自带 Bearer<br/>采集 5h/7d 配额头"]
ProxySvc --> URLNorm["URL 规范化<br/>去除重复 /v1"]
ProxySvc --> AuthH["Auth 头转换<br/>x-api-key → Bearer"]
ProxySvc --> FmtConv["格式转换<br/>OpenAI ↔ Anthropic"]
ProxySvc --> Retry["重试回调<br/>429/529 → IPC 通知"]
ProxySvc --> BgModel["后台模型路由<br/>背景任务模型替换"]

Direct --> AgentSvc
ProxySvc -->|"env: ANTHROPIC_BASE_URL=localhost:PORT"| AgentSvc
PassThrough -->|"env: ANTHROPIC_BASE_URL=localhost:PORT"| AgentSvc

AgentSvc --> SDK["Claude Agent SDK<br/>子进程"]

核心逻辑:buildProviderEnvWithProxy

这个函数负责为 Claude Agent SDK 准备环境变量。根据 chat_sessions 行的扁平字段(providerIdcliBackenduseExtendedContext、裸 model)采取不同策略。

算法步骤

  1. 无 providerId → 返回空对象,SDK 使用 process.env
  2. code-cli + cliBackend='claude-code'(订阅 OAuth)buildClaudeCodePassThroughResult
    • 启动 AgentProxyServer,开启 passThrough: true
    • 透传 SDK 自带的 Bearer header 到 api.anthropic.com
    • 响应头采集 anthropic-ratelimit-unified-{5h,7d}-*,触发 SubscriptionUsageService 更新前端 5h/7d 角标
    • 可选注入 Elftia-managed OAuth Token(TokensService.getValidClaudeAccessToken())覆盖系统凭据
    • 标记 isOfficialProvider: false
  3. 其余路径llmConfig.getProvider(providerId) + resolveApiFormat(provider)
  4. Anthropic 官方提供商provider.id === 'anthropic'):
    • 从 ApiKeyPool 或 provider.api_key 获取 Key
    • 设置 ANTHROPIC_API_KEY
    • 如有自定义 base URL,设置 ANTHROPIC_BASE_URL(去除尾部 /v1
    • 标记 isOfficialProvider: true
  5. 其他提供商
    • 从 ApiKeyPool 或 provider.api_key 获取 Key
    • 启动 AgentProxyServer(本地 HTTP 代理)
    • 设置 ANTHROPIC_BASE_URL = proxy.getBaseUrl()
    • Anthropic 格式提供商传真实 Key(启用服务端工具)
    • 非 Anthropic 格式传 'proxy-mode'(Key 由 proxy 处理)
    • 返回 onSessionEnd 清理回调(停止代理)

函数签名补充了 schemaFields?: { cliBackend, useExtendedContext } 参数(v89+),由调用方从 chat_sessions 行透传。cliBackend === 'claude-code' 触发 passThrough 分支;useExtendedContext === true 触发 1M-context beta 注入(见下文)。

1M-context beta 注入(v89+)

chat_sessions.useExtendedContext = 1model 在 1M-capable 白名单(claude-opus-4-7 / claude-opus-4-6 / claude-sonnet-4-6)内时,injectExtendedContextBeta(headers, model, useExtendedContext)'context-1m-2025-08-07' 合并到出站请求的 anthropic-beta HTTP 头(逗号分隔列表)。

不是 body 字段。早期实现把 flag 写到 body.anthropic_beta 数组,被 /v1/messages 端点以 400 "anthropic_beta: Extra inputs are not permitted" 拒绝。canonical 路径是 HTTP header。

注入点(三处共用同一个幂等 + 大小写无关 helper):

路径位置注入到哪个 headers 对象
passThrough 模式(claude-code OAuth)AgentProxyServer.handlePassThroughRequestfetch 之前upstreamHeaders(从 req.headers 复制 + 规范化)
isOfficialProvider 直传分支AgentProxyServerfetchWithRetry 之前getProviderHeaders(provider, apiKey) 返回的对象
Transformer 链路TransformerChainExecutor.executeRequestChain 出口config.headers(最终合到 fetch 的 headers)

helper 保留 SDK 已写入的其他 beta(如 prompt-caching-2024-07-31),并兼容大小写变体(Anthropic-Beta 也会被识别 + 重写为规范小写 anthropic-beta)。

Claude Agent SDK 自身不会自动加 1M-context beta(源码 0 处出现 context-1m)。[1m] UI 标记如果不通过这个 helper 注入到 HTTP 头,1M context 不会生效,请求仍走 200K 模式。

代理服务器的作用

所有非内置 Anthropic 提供商都通过代理,原因如下:

  1. URL 规范化api_base_url 可能包含 /v1,SDK 会重复拼接为 /v1/v1/messages
  2. Auth 头处理 — 不同提供商使用不同的认证头格式(x-api-key vs Bearer)
  3. 格式转换 — 非 Anthropic 提供商需要请求/响应格式转换
  4. 重试回调 — 429/529 错误通过回调通知前端
  5. 后台模型路由 — SDK 的 background task 请求可路由到不同模型

API Key 解析

function resolveApiKey(apiKey: string): string {
if (apiKey.startsWith('$')) {
return process.env[apiKey.slice(1)] || '';
}
return apiKey;
}

支持通过 $ 前缀引用环境变量,例如 $ANTHROPIC_API_KEY

多 Key 负载均衡

通过 ApiKeyPoolService 实现:

setApiKeyPool(pool: ApiKeyPoolService): void;

设置后,buildProviderEnvWithProxy 优先从 Pool 获取 Key:

let apiKey = '';
if (apiKeyPool && sessionId) {
apiKey = await apiKeyPool.getKeyForSession(provider.id, sessionId);
}
if (!apiKey) {
apiKey = resolveApiKey(provider.api_key);
}

Pool 使用加权轮询 + 会话亲和性策略分配 Key,429/529 错误触发自动冷却。

Windows Git 配置

setGitRuntime(runtime: GitRuntimeService): void;

Claude Agent SDK 的子进程需要 Git 和 Bash。在 Windows 上,GitRuntimeService 负责:

  • 检测 Git for Windows 安装路径
  • 将 git-bash 路径加入 PATH
  • 确保 SDK 子进程可以正常运行

每次 startSessionresumeSession 前自动调用 ensureGitForSdk()

两种调用路径

Self-service 路径(AgentRouter)

AgentRouter 直接调用 ClaudeSdkEngine,不传 providerEnv

// ctx.providerEnv 为空
engine.startSession(ctx);
// → 内部调用 buildProviderEnvWithProxy() 自行构建

Magi 预构建路径

MagiService 预先构建好 providerEnv 并传入:

// ctx.providerEnv 已由 MagiService 构建
engine.startSession(ctx);
// → 直接使用 ctx.providerEnv,跳过自行构建
// → 使用 startWithExistingSession(DB 会话已存在)

判断逻辑

if (ctx.providerEnv && dbSessionId) {
await this.agent.startWithExistingSession(sender, dbSessionId, sessionOpts);
} else {
await this.agent.createSession(sender, sessionOpts);
}

API Key 验证

启动会话前进行 Key 验证,避免 CLI 子进程启动后因 Key 缺失而崩溃:

if (!env?.ANTHROPIC_API_KEY && !process.env.ANTHROPIC_API_KEY) {
throw new Error(`API key not configured for provider "${providerId}"`);
}

IPC 事件

除标准的 agent:event 事件外,ClaudeSdkEngine 还发送重试通知:

事件载荷说明
agent:event type=retry{ attempt, maxAttempts, delayMs, error }API 请求重试通知

SessionStore(SDK 0.3.x,2026-05-19+)

SDK 0.3.x 提供官方 SessionStore 接口,把会话 transcripts 镜像到任意外部存储。AgentService.runSession 通过 sdkOptions.sessionStore = new SqliteSessionStore(db) 注入:

SDK 子进程写磁盘 JSONL → SDK 也调 sessionStore.append(key, entries)
→ SqliteSessionStore → sdkSessionStore:append IPC → DB worker
→ INSERT OR IGNORE 到 sdk_session_store 表(按 entryUuid 幂等)

Resume 时:SDK 调 sessionStore.load(key) → 返回 entries[] | null
→ SDK 把 entries materialize 到临时 JSONL → 子进程 --resume
→ 磁盘 JSONL 丢失也能恢复(只要 sdk_session_store 有数据)

Resume 安全检查AgentService.resumeSession):dispatch 前同时检查 hasResumableSdkJsonl(projectPath, sdkSessionId)(磁盘文件存在 + 含 type:'user' 记录)和 sdkSessionStore:countBySessionId > 0;两者都空 → 清空 sdkSessionId,让 SDK 起新 conversation(DB 聊天历史保留)。

已废弃路径:旧版 sdk_records 表 + JsonlBuilder.reconstruct() 路径(IPC schema ≠ 磁盘 schema,SDK 拒绝)已在 migration v97 整体下线。详见 docs/dev/66_sdk_records/01_schema_divergence_investigation.md

关键文件

文件路径说明
ClaudeSdkEngineagent-core/engine/ClaudeSdkEngine.tsIEngine 实现 + buildProviderEnvWithProxy(含 passThrough 分支 + extendedContext 透传)
AgentServiceagent-core/agent/AgentService.tsSDK 会话生命周期;AgentSessionOptions 持有 cliBackend + useExtendedContext,DB model 列存裸 SDK id;runSession 注入 sdkOptions.sessionStoreresumeSession 走 fallback 检查
AgentProxyServeragent-core/agent/AgentProxyServer.tsHTTP 代理服务器,三种模式:passThrough / isOfficialProvider 直传 / 默认 transformer 链路
1M-context beta 注入 helperagent-core/agent/anthropicBetaInject.tsinjectExtendedContextBeta(body, model, useExtendedContext),三处出口共用
SqliteSessionStoreagent-core/agent/SqliteSessionStore.tsSDK 0.3.x SessionStore 接口的 SQLite 实现(append/load/delete/listSubkeys)
sdkPathEncoding helperagent-core/agent/sdkPathEncoding.tsencodeProjectPath(=replace(/[^A-Za-z0-9]/g, '-'))+ hasResumableSdkJsonl(文件存在 + 含 user 记录)
sdkSessionStore worker DAOworkers/db/sdkSessionStore.tsCRUD on sdk_session_store 表,6 个 IPC(append/load/delete/listSubkeys/listSessions/countBySessionId)
Code CLI 类型 + 拆分协议shared/contracts/code-cli-types.tssplitModelReference() 是 IPC 入口处的唯一字符串协议解析器
ApiKeyPoolServicecapabilities/llm/completion/ApiKeyPoolService.ts多 Key 负载均衡
GitRuntimeServiceplatform/runtime/GitRuntimeService.tsWindows Git 配置

所有路径相对于 packages/desktop/app/main/services/shared/contracts/code-cli-types.tspackages/desktop/app/workers/db/sdkSessionStore.tspackages/desktop/app/main/

用户交互通道(permission + ask_user_question)

Claude SDK 引擎从后端到渲染端有两条对称的弹窗通道,都由 AgentService 维护 pendingXxx: Map<requestId, resolver> + 5 分钟超时 + 通过 active.sender.send 的 IPC 推送。

通道触发Main → Renderer IPCRenderer → Main IPC
PermissionSDK canUseTool 回调(工具使用授权)agent:permissionRequestagent:respondPermission
AskUserQuestionAgent 主动调 mcp__elftia-ask__ask 工具agent:askUserQuestionagent:respondAskUserQuestion

Permission:bypassPermissions 模式下的兜底回调

AgentService.runSession 始终注册 onPermissionRequestlib/claude-sdk.tsmapCliOptionsToSDK 始终把它桥接成 SDK 的 canUseTool。bypass 模式下回调内部直接 behavior: 'allow' 并打印 agentID / blockedPath / decisionReason 诊断字段。

之前的代码两层都用 if (skipPermissions) skip 把 callback 短路了。这造成了一个静默 deny 的 bug:当主 agent 是 bypassPermissions 但 subagent 用 tools: [Read, Glob] 缩窄工具白名单时,SDK 仍会对 subagent 越界调用走 canUseTool gate,没注册 → silent deny → 用户看到模型说"权限被拒绝"但 elftia 没弹窗。

修复关键代码位置:

  • packages/desktop/app/main/lib/claude-sdk.ts:262-339(双层 callback 始终注册)
  • packages/desktop/app/main/services/agent-core/agent/AgentService.ts:1064-1086(删 isSkipPermissions 短路)

AskUserQuestion:用 SDK in-process MCP 替换 builtin

Claude Code preset 自带的 AskUserQuestion 工具会拉起终端式提示,elftia 主进程无法 surface。修复方案(方案 A):

  1. 禁用 builtinsdkOptions.toolsSettings.disallowedTools 追加 'AskUserQuestion'
  2. 注册替代 MCPAskUserQuestionMcp.tscreateSdkMcpServer + tool() 暴露 mcp__elftia-ask__ask,schema 与 builtin 完全一致(questions: Array<{question, header(≤12 chars), multiSelect, options[2-4]}>
  3. 引导模型appendSystemPrompt 追加一段指引,说明遇到提问场景调 mcp__elftia-ask__ask 而非 builtin
  4. handler 流:调 AgentService.askUserQuestion(dbSessionId, questions) → 创建 Promise + 存 resolver in pendingQuestions Map → IPC agent:askUserQuestion → 渲染端 AskUserQuestionDialog 显示 stepper 界面 → 用户 Submit → IPC agent:respondAskUserQuestion → resolver 把 answers 序列化为 JSON 作为 tool_result。Cancel 走 isError: true 分支告诉模型用户取消。

MCP server 实例 per session 构建(闭包 dbSessionId),保证多 tab 场景下问题落到正确的渲染端。

相关代码:

  • packages/desktop/app/main/services/agent-core/agent/AskUserQuestionMcp.ts(MCP server 构造器 + ELFTIA_ASK_MCP_NAME
  • packages/desktop/app/main/services/agent-core/agent/AgentService.tspendingQuestions Map + askUserQuestion() + respondToAskUserQuestion(),注入逻辑在 runSession 末尾)
  • packages/desktop/app/main/services/routers/AgentRouter.tsagent:respondAskUserQuestion IPC + zod schema)
  • packages/renderer/src/features/chat/components/agent/AskUserQuestionDialog.tsx(stepper UI:当前题展开 + 已答题塌缩 summary 行可点回退 + 永远存在的 "Other" 文本输入 + 全部答完启用 Submit)

扩展点

  • 新增代理格式转换:在 AgentProxyServer 中添加新的 API 格式适配
  • 自定义重试策略:通过 RetryCallback 参数自定义重试行为
  • 后台模型配置:通过 agentDefaults.background 设置后台任务使用的模型

相关模块

模块路径关系
EngineDispatcheragent-core/engine/EngineDispatcher.ts引擎注册
LLMConfigServicecapabilities/llm/config-service/提供商配置
MagiServiceagent-core/magi/MagiService.ts高层编排(含 assembleMagiMcps 走注册表 + Object.assign 合并到 customAgentOptions.additionalMcpServersbuildTinyElfDirectMcpServers 是 TinyElf 入口)
MagiSdkOptionsBuilderagent-core/magi/MagiSdkOptionsBuilder.tsPrompt 构建 (V1-V4) + 用户 MCP 注入(setUserMcpServers / getMcpServers(allowedUserMcpNames))+ setChannelMcpProbe内置 MCP 装配自 Phase 5.9 起迁出至 services/capabilities/tools/mcp-builtin/;保留原名因为还承担用户 MCP 部分
McpProviderRegistrycapabilities/tools/mcp-builtin/11 个静态注册的内置 MCP Provider + 动态 ScriptPluginProviders 工厂;统一 assembleMcpForSession(ctx) 入口。SDK 路径调用:MagiService.assembleMagiMcps (Clawia)、AgentService.mergeMcpAssembly (其他)。media-tools MCP 已在 Tier C 试点中迁出elftia_toolkit 内的 media toolkit(详见 08_builtin_mcp_and_toolkits.md §3)。详见 capabilities/tools/mcp-builtin/README.md
Skill Toolkit Registrycapabilities/tools/skill-toolkit/elftia_toolkit MCP 内的 in-process 函数调度器。暴露 4 个 meta-tool:list_toolkits / read_toolkit / read_toolkit_reference / skill_invoke。内置两个 toolkit:chrome-use(28 个 CDP 函数)+ media(10 个媒体生成/语音函数)。Progressive disclosure:SKILL.md 是索引,per-provider / per-operation 深入文档放在 toolkit 的 references map 里,按需 read_toolkit_reference 拉取
AgentService MCP 注入agent-core/agent/AgentService.tsmergeMcpAssembly(sdkOptions, options, dbSessionId) 是非 Clawia SDK 会话的注册表入口;通过 setMcpAssemblermain/index.ts 接入。Per-session cleanup 自动串到 onSessionEnd