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 行的扁平字段(providerId、cliBackend、useExtendedContext、裸 model)采取不同策略。
算法步骤
- 无 providerId → 返回空对象,SDK 使用
process.env 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
- 启动
- 其余路径 →
llmConfig.getProvider(providerId)+resolveApiFormat(provider) - Anthropic 官方提供商(
provider.id === 'anthropic'):- 从 ApiKeyPool 或 provider.api_key 获取 Key
- 设置
ANTHROPIC_API_KEY - 如有自定义 base URL,设置
ANTHROPIC_BASE_URL(去除尾部/v1) - 标记
isOfficialProvider: true
- 其他提供商:
- 从 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 = 1 且 model 在 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.handlePassThroughRequest 在 fetch 之前 | upstreamHeaders(从 req.headers 复制 + 规范化) |
| isOfficialProvider 直传分支 | AgentProxyServer 在 fetchWithRetry 之前 | 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 提供商都通过代理,原因如下:
- URL 规范化 —
api_base_url可能包含/v1,SDK 会重复拼接为/v1/v1/messages - Auth 头处理 — 不同提供商使用不同的认证头格式(x-api-key vs Bearer)
- 格式转换 — 非 Anthropic 提供商需要请求/响应格式转换
- 重试回调 — 429/529 错误通过回调通知前端
- 后台模型路由 — 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 子进程可以正常运行
每次 startSession 和 resumeSession 前自动调用 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。
关键文件
| 文件 | 路径 | 说明 |
|---|---|---|
| ClaudeSdkEngine | agent-core/engine/ClaudeSdkEngine.ts | IEngine 实现 + buildProviderEnvWithProxy(含 passThrough 分支 + extendedContext 透传) |
| AgentService | agent-core/agent/AgentService.ts | SDK 会话生命周期;AgentSessionOptions 持有 cliBackend + useExtendedContext,DB model 列存裸 SDK id;runSession 注入 sdkOptions.sessionStore;resumeSession 走 fallback 检查 |
| AgentProxyServer | agent-core/agent/AgentProxyServer.ts | HTTP 代理服务器,三种模式:passThrough / isOfficialProvider 直传 / 默认 transformer 链路 |
| 1M-context beta 注入 helper | agent-core/agent/anthropicBetaInject.ts | injectExtendedContextBeta(body, model, useExtendedContext),三处出口共用 |
| SqliteSessionStore | agent-core/agent/SqliteSessionStore.ts | SDK 0.3.x SessionStore 接口的 SQLite 实现(append/load/delete/listSubkeys) |
| sdkPathEncoding helper | agent-core/agent/sdkPathEncoding.ts | encodeProjectPath(=replace(/[^A-Za-z0-9]/g, '-'))+ hasResumableSdkJsonl(文件存在 + 含 user 记录) |
| sdkSessionStore worker DAO | workers/db/sdkSessionStore.ts | CRUD on sdk_session_store 表,6 个 IPC(append/load/delete/listSubkeys/listSessions/countBySessionId) |
| Code CLI 类型 + 拆分协议 | shared/contracts/code-cli-types.ts | splitModelReference() 是 IPC 入口处的唯一字符串协议解析器 |
| ApiKeyPoolService | capabilities/llm/completion/ApiKeyPoolService.ts | 多 Key 负载均衡 |
| GitRuntimeService | platform/runtime/GitRuntimeService.ts | Windows Git 配置 |
所有路径相对于 packages/desktop/app/main/services/,shared/contracts/code-cli-types.ts 在 packages/desktop/app/,workers/db/sdkSessionStore.ts 在 packages/desktop/app/main/。
用户交互通道(permission + ask_user_question)
Claude SDK 引擎从后端到渲染端有两条对称的弹窗通道,都由 AgentService 维护 pendingXxx: Map<requestId, resolver> + 5 分钟超时 + 通过 active.sender.send 的 IPC 推送。
| 通道 | 触发 | Main → Renderer IPC | Renderer → Main IPC |
|---|---|---|---|
| Permission | SDK canUseTool 回调(工具使用授权) | agent:permissionRequest | agent:respondPermission |
| AskUserQuestion | Agent 主动调 mcp__elftia-ask__ask 工具 | agent:askUserQuestion | agent:respondAskUserQuestion |
Permission:bypassPermissions 模式下的兜底回调
AgentService.runSession 始终注册 onPermissionRequest,lib/claude-sdk.ts 的 mapCliOptionsToSDK 始终把它桥接成 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):
- 禁用 builtin:
sdkOptions.toolsSettings.disallowedTools追加'AskUserQuestion' - 注册替代 MCP:
AskUserQuestionMcp.ts用createSdkMcpServer+tool()暴露mcp__elftia-ask__ask,schema 与 builtin 完全一致(questions: Array<{question, header(≤12 chars), multiSelect, options[2-4]}>) - 引导模型:
appendSystemPrompt追加一段指引,说明遇到提问场景调mcp__elftia-ask__ask而非 builtin - handler 流:调
AgentService.askUserQuestion(dbSessionId, questions)→ 创建 Promise + 存 resolver inpendingQuestionsMap → IPCagent:askUserQuestion→ 渲染端AskUserQuestionDialog显示 stepper 界面 → 用户 Submit → IPCagent: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.ts(pendingQuestionsMap +askUserQuestion()+respondToAskUserQuestion(),注入逻辑在runSession末尾)packages/desktop/app/main/services/routers/AgentRouter.ts(agent:respondAskUserQuestionIPC + zod schema)packages/renderer/src/features/chat/components/agent/AskUserQuestionDialog.tsx(stepper UI:当前题展开 + 已答题塌缩 summary 行可点回退 + 永远存在的 "Other" 文本输入 + 全部答完启用 Submit)
扩展点
- 新增代理格式转换:在
AgentProxyServer中添加新的 API 格式适配 - 自定义重试策略:通过
RetryCallback参数自定义重试行为 - 后台模型配置:通过
agentDefaults.background设置后台任务使用的模型
相关模块
| 模块 | 路径 | 关系 |
|---|---|---|
| EngineDispatcher | agent-core/engine/EngineDispatcher.ts | 引擎注册 |
| LLMConfigService | capabilities/llm/config-service/ | 提供商配置 |
| MagiService | agent-core/magi/MagiService.ts | 高层编排(含 assembleMagiMcps 走注册表 + Object.assign 合并到 customAgentOptions.additionalMcpServers;buildTinyElfDirectMcpServers 是 TinyElf 入口) |
| MagiSdkOptionsBuilder | agent-core/magi/MagiSdkOptionsBuilder.ts | Prompt 构建 (V1-V4) + 用户 MCP 注入(setUserMcpServers / getMcpServers(allowedUserMcpNames))+ setChannelMcpProbe。内置 MCP 装配自 Phase 5.9 起迁出至 services/capabilities/tools/mcp-builtin/;保留原名因为还承担用户 MCP 部分 |
| McpProviderRegistry | capabilities/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 Registry | capabilities/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.ts | mergeMcpAssembly(sdkOptions, options, dbSessionId) 是非 Clawia SDK 会话的注册表入口;通过 setMcpAssembler 在 main/index.ts 接入。Per-session cleanup 自动串到 onSessionEnd |