apps/daemon/src/agent/providers/claude/ · 逐行深读 + Codex 复核 + 竞品 ref 交叉验证claude 实测runner.ts 1809 行 大半不是过度建造。Claude headless -p --input-format stream-json + piped stdin 是长活、多轮、双向进程,runner 正确建模了它。真正可净删的是 hooks 表 + 重复 schema(图像路径是补 probe 接通优先,删为退路,见 §05);discovery.ts/sessions.ts 是要接回契约的能力实现,不是垃圾(见下文)。
/context/cost 解析 ~135 + hooks 表 ~55 + 私有方法面 ~50;测试端约 −540 行(claude-sessions 190 + claude-discovery ~250 + 死面用例若干)。9334 → 约 6900 行;留下的重镇 runner.ts 1809 + claude-runner.test.ts 1746 属必要适配与核心覆盖,不算膨胀。
分层其实很干净。问题不在「乱」,在「范围」和「地基」。从下往上:
index.ts · errors.tsrunner.tscommands.ts · config.tsprocess.tstranslator.tsclaude 子进程 (NDJSON stdout / stdin)discovery.ts · sessions.ts · index.ts 8 个非契约方法 —— 能力要留,接线要重做(见 §05/§06)// daemon 侧:只认契约引用 —— 这是它认识 provider 的全部方式 const provider: AgentProvider = await registry.getProvider(id) provider.getCapabilities() // ✓ 契约上有,调得到 provider.createRunner(...) // ✓ // PR #42 的 ClaudeProvider(index.ts)—— 契约 3 项之外又挂了 8 个公开方法: class ClaudeProvider implements AgentProvider { getCapabilities() {...} // ✓ 契约 getControlCommands() {...} // ✓ 契约 createRunner() {...} // ✓ 契约 discoverSlashCommands() {...} // ✗ 契约外 → daemon 持有的引用上根本没有这个方法 discoverNativeSessions() {...} // ✗ 背后是 sessions.ts 726 行,只从这里可达 importNativeSession() {...} // ✗ getMetadata() / getAvailability() / discoverModels() / discoverAgents() / readMcpDiagnostic() } // git grep 实证:这 8 个方法在 daemon 生产代码里零调用方 —— 能力是对的,接线是死的
-p headless = 跑一次就退,runner 在伪装 server」idle 复用、单 reader 跨生命周期startTurn 复用同进程,无需 --resume (测试 claude-runner.test.ts:207 证实)--resume 重开--resume <sid> 只在重开时用(崩溃自愈 / 续旧会话),不是每轮都用closed 只在 dispose() resolve、崩溃永不 resolve(进程死 → 自愈复用)。这是刻意设计,但顶到 RunnerManager 契约边界,是最 load-bearing 的取舍,需地基统一定。审批、输入、中断都挤在同一条 stdout/stdin 里(带内 JSON 请求/响应)。核心桥接有据可行(对齐团队实测 + SDK 类型),但 PR42 自己加塞的 hooks 表是多余的。
--permission-prompt-tool=stdio)hooks 表(PreToolUse + 凭空造的 callback id)→ 偏离实测基线、无接收端 → 该整块删,发 hooks:null// PR 现在的 initialize(runner.ts:969,表来自 buildClaudeHooks :1731) { "subtype": "initialize", "hooks": { "PreToolUse": [{ "matcher": "^(?!(Glob|Grep|Read|...)$).*", // 手写工具名正则 "hookCallbackIds": ["auto-approve-callback"] }] } } // ✗ 凭空造的回调 id // ✗ 接收端 parseClaudeControlRequest 只认 can_use_tool,没有任何 hook 回调处理 —— // CLI 若真按表回调,请求会挂起;实际审批门控由 set_permission_mode 原生触发,这张表全程多余 // 团队 2026-05-21 实测基线(hands-on-notes:359) { "subtype": "initialize", "hooks": null }
can_use_tool 5 字段、PermissionResult = allow{updatedInput,updatedPermissions} | deny{message,interrupt} 逐条对齐团队 2026-05-21 实测 + sdk.d.ts:1861-1873,且 vibe-kanban 同套路。这块不是瞎栓。ClaudeProvider implements AgentProvider —— provider 工厂入口。getCapabilities / getControlCommands / createRunner)+ id/kind/transport —— KEEP createRunner 干净(normalize → 拒 load replay → 取 providerSessionId → 委托)。AgentProvider 引用,这些方法够不着 = 当前死代码。但能力是要的:5 项契约已有口子(metadata/availability/models/config → AgentAvailability;native resume → AgentPersistenceHandle)直接接;2 项契约缺方法(slash 发现、原生 session 发现)→ 契约方法已立项(spec 0008,owner 侧推进;见 §06,别在本 PR 自行扩契约)。stdin.end → SIGTERM → SIGKILL 打进程组;Windows taskkill;ESRCH 吞;unref);早退窗口探测;写背压经 'error' 监听;CVE-aware 的 Windows .cmd 处理 —— 都是真功夫。--permission-mode bypassPermissions(386 行)。起步过门有据,但到 set_permission_mode 落地前那段窗口无沙箱。translateClaudeLine —— stream-json 行 → AgentProviderEvent[]。结构漂亮、可独立测。message.delta + 末尾又发带全文的 message.completed,都不带 itemId 配对 → 消费者整条重复。input/output_tokens,丢 cache/context/model。tool_result.content,丢消息级富结构 tool_use_result(stdout/stderr/interrupted)。is_plan / occurredAt 是逆向字段。// translator.ts 现状:同一段助手文本发两遍,且都没有 itemId 可配对 emit({ type: 'message.delta', text: '正在分析…' }) // 流式增量,逐段到达 emit({ type: 'message.delta', text: '…完成' }) emit({ type: 'message.completed', text: '正在分析……完成' }) // ✗ 末尾再带一遍全文 // 消费者把 delta 拼完,又收到 completed 全文 → 整条消息渲染两遍; // 修法:completed 不重复携带全文,或两边都带 itemId 让消费者可配对去重
ClaudeRunner —— 5 态机 + 全部运行时(session-id 缓冲、控制协议桥、interrupt、EOF/失败处理、turn/slash 双模式、竞态)。buildClaudeHooks hooks 表(多余 + 半接线 + 偏离实测,发 hooks:null)。capabilities.imageInput 恒 'unknown'、probe 没建 → 任何图像 turn 必抛,~80 行处理代码永跑不到。image 是契约 AgentInput.parts 一等成员(#50)→ 优先补 probe 接通;确认 CLI 不支持再删。ExitPlanMode→永久 bypass 改掉 · parseLine 非 JSON 别杀进程。// runner.ts:65 —— 能力探针初始化之后,全仓没有任何代码再更新它 imageInput: 'unknown', // runner.ts:1436 —— 于是图像轮永远走进 throw,后面 ~80 行图像处理永跑不到 if (capabilities.imageInput !== 'supported') { throw unsupportedOperation('Claude stream-json image input is not supported.') } // image 是契约 AgentInput.parts 一等成员(#50)→ 修法是把 probe 建起来接通,不是留死路
/context /cost 输出解析。/context /cost 正则刮取人类可读输出 —— 投机。/compact 当 user 消息发,CLI 会当命令解释」,实测无确认 —— 若被当聊天文本则整面非功能。// commands.ts:193-328 —— 把 /context、/cost 的「人类可读输出」用正则刮成数字(~135 行,示意) readLabeledNumber(rawText, /Context size:\s*([\d,]+)/i) // ✗ CLI 文案改一个词就全失效 // 命令定义与 semantic 映射保留;这套解析删 —— 这类数据该走结构化口子(usage 事件),不是刮终端文案
ADAPTER_OWNED_FLAGS / DISALLOWED_FLAG_PATTERNS)—— 防 extraArgs 破协议,有用。ClaudeProviderConfigFieldMetadata + 14 条 configFields 与契约 AgentConfigField 干同一件事(描述字段给 UI 渲染)→ 改填 AgentAvailability.sessionConfig,删平行类型(契约本来就有这个口子)。DEFAULT_PERMISSION_MODE='bypassPermissions' 但 capability 声明 approval → 矛盾,默认应审批开启。getClaudeProviderAvailability() 直接 available:true —— 假阳性,必须真探针(paseo isAvailable() 是真去 resolve binary)。extraArgs / advancedRawArgs 两字段做同一件事,冗余。// config.ts:129 —— 默认权限模式 const DEFAULT_PERMISSION_MODE: ClaudePermissionMode = 'bypassPermissions' // ✗ 默认全放行 // 同时 getCapabilities() 声明了 'approval' —— 名义上有审批能力,默认配置却整个绕过它 // config.ts:326 —— 可用性上报 getClaudeProviderAvailability() { return { available: true, ... } // ✗ 从不探测 claude 是否安装 → 没装的用户到 createRunner 才炸 }
~/.claude + 项目 .claude/ 的 slash 命令 / agent markdown / model / MCP 配置。能力 Eyrie 要,接线接错了。BASELINE_MODELS + getClaudeAvailabilityModelOptions()(~90 行)是活的 → 接进 AgentAvailability.models / 注册机制 ProviderModule.models。getControlCommands() 只管静态内置)→ 需接口层补统一 discovery 方法,实现挪到其后。~/.claude.json 索引 + projects/**/*.jsonl transcript),把已有原生 session 列出来供导入 Eyrie。「导入我的 CLI session」是 Eyrie 想要的产品功能。AgentNativeSession。AgentPersistenceHandle 喂 createRunner({resume}) —— 这段契约本来就有。// sessions.ts 逆向的磁盘布局(Claude 私有格式,无兼容承诺): ~/.claude.json // 全局索引:项目 → 会话清单 ~/.claude/projects/<path-slug>/<sessionId>.jsonl // transcript;文件名即 session id(已实测核对) // 能力(把已有 CLI 会话导入 Eyrie 续聊)是真产品功能;但私有格式必须版本兜底 + 失败降级, // 且产出要走统一契约出口(AgentPersistenceHandle),不能自成第二套持久化权威
-p stream-json 进程set_permission_mode--resume/context/cost 正则刮取 —— 投机index.ts 那 8 个私有方法面本身(能力另接契约,见右侧)ExitPlanMode 永久 bypass 改掉is_plan / occurredAt 真伪tool_use_result 确切字段这些不是删功能。问题是走了 Claude 私有方法面,绕过契约。7 项里 5 项契约已有口子(直接接),2 项契约缺方法(需扩契约 + spec):
| 想要的能力 | 接口层契约口子 | 状态 / 处置 |
|---|---|---|
| provider metadata / availability | AgentAvailability(registry.detectAll()) | 已有 改走它,弃 getMetadata/getAvailability |
| models 列表 | AgentModelOption → ProviderModule.models | 已有(#50/#55) ~90 行基线填进你的 ProviderModule 声明 |
| session 配置 schema | AgentConfigField → ProviderModule.sessionConfig | 已有(#50/#55) 弃自造 ClaudeProviderConfigFieldMetadata |
| 原生 session 续(resume) | AgentPersistenceHandle + createRunner({resume}) | 已有 import 产物直接喂 |
| 原生 session 发现 | —— | 缺 契约已立项:spec 0008 discoverNativeSessions + 统一 AgentNativeSession(owner 侧推进,落地后对齐) |
| 内置 slash 命令 | getControlCommands() | 已有 |
| 用户/项目自定义 slash 发现 | —— | 缺 契约已立项:spec 0008 discoverCommands,复用既有 AgentControlCommandDef——不造 AgentSlashCommand 新类型(落地后对齐) |
closed 语义 · F3 注册(已解决:PR #55 ProviderModule 自注册已合入)· model 归属(落点 = ProviderModule.models)—— 归注册/接口层,见 00-foundation-arch-review.md。/context/cost 解析)+ 私有面能力接回契约(5 项);sessions.ts + discovery.ts 扫描 + 对应测试本 PR 先撤出(待发现契约落地另 PR 回归)→ 约 −2470 行claude 验 4 处 wire shapeproviders/claude/* + claudeProviderModule 一行声明