docs: add detailed answers for 21 learning roadmap questions

This commit is contained in:
2026-05-13 03:40:13 +00:00
parent a8d90d2980
commit c4753da8f5
10 changed files with 1308 additions and 0 deletions

View File

@@ -0,0 +1,170 @@
# Q18-Q19: Provider 与 MCP
---
## Q18: Claude Agent SDK、OpenCode、Ollama 三个 provider 怎么抽象成统一接口?切换 provider 改什么?
### 答案
### 统一接口:`AgentProvider`
定义在 `container/agent-runner/src/providers/types.ts:1-92`
```typescript
interface AgentProvider {
readonly supportsNativeSlashCommands: boolean; // SDK 是否原生处理 /commands
query(input: QueryInput): AgentQuery; // 开始一轮对话
isSessionInvalid(err: unknown): boolean; // 检测过期的延续语境错误
}
```
- `QueryInput`line 40-60携带 `prompt``continuation`(不透明 session token`cwd``systemContext`。Provider 自己决定 "continuation" 的含义
- `AgentQuery`line 68-80流式句柄`push(message)` 推送后续输入,`events: AsyncIterable<ProviderEvent>` 流式输出,`abort()` 强制停止
- `ProviderEvent`line 82-92判别联合类型 `init | result | error | progress | activity`
- `ProviderOptions`line 23-38`assistantName``mcpServers``env``additionalDirectories``model``effort`
### 当前 Providers
**ClaudeProvider**`claude.ts:253-360`
- 包装 `@anthropic-ai/claude-agent-sdk``query()` 函数
- 用自定义 `MessageStream` async generatorline 80-112把后续消息推入 SDK 流式输入
- 把原始 SDK 事件翻译成 `ProviderEvent` 联合类型(`translateEvents()`line 318-346
- HooksPreToolUse记录 in-flight tool + 屏蔽不允许的 SDK builtins、PostToolUse清除 in-flight、PreCompact转录归档
- 在 line 360 注册:`registerProvider('claude', (opts) => new ClaudeProvider(opts))`
**MockProvider**`mock.ts:8-76`
- 测试用——从 `responseFactory` 返回预设回复
**OpenCode**`providers` 分支通过 `/add-opencode` skill 安装,实现 `AgentProvider` 接口
**Ollama** 尚未实现。会实现 `AgentProvider` 直接调用 Ollama API
### factory.ts 的选择逻辑
`container/agent-runner/src/providers/factory.ts`(全部 13 行):
```typescript
export function createProvider(name: string, options: ProviderOptions = {}): AgentProvider {
return getProviderFactory(name)(options);
}
```
这是对自注册 registry`provider-registry.ts`的薄分发。Registry 是 `Map<string, ProviderFactory>`,每个 provider 模块在模块作用域调用 `registerProvider(name, factory)`
选择逻辑:
1. 调用者传入 provider name 字符串(来自 `container_configs.provider` 列)
2. `getProviderFactory(name)` 查 Map——未找到抛错`"Unknown provider: ${name}"`
3. 调用 factory 函数并传入 options
### 注册流程
1. 每个 provider 文件在顶层调用 `registerProvider()`(如 `claude.ts:360`
2. Barrel `container/agent-runner/src/providers/index.ts` 导入所有 provider 模块做副作用:
```typescript
import './claude.js';
import './mock.js';
```
3. 运行时 agent-runner poll loop 从 `container_configs` 解析 provider name → `createProvider(name, options)` → 获取 `AgentProvider` → `provider.query(input)`
### 切换 Provider
修改 agent group 的 `container_configs.provider` 列:
```bash
ncl groups config update --id <group-id> --provider opencode
```
写入中央库 → 下次容器 spawn 物化到 `container.json` → agent-runner 调用 `createProvider('opencode', ...)`
### Host 端 Provider 注册
对于需要 host 端 setup 的 provider额外 mounts、env 传递),有独立的 registry`src/providers/provider-container-registry.ts:43-54`。Provider 注册 `ProviderContainerConfigFn`container-runner 在 spawn 时调用它来合并额外的 mounts/env。目前仅 `claude` 内建——用默认容器,无需额外注册。非默认 provider如 OpenCode会在这里注册。
---
## Q19: 容器里的 MCP server 怎么启动?内置工具和外部 MCP server 有什么不同?
### 答案
### MCP Server Bootstrap
入口是 `container/agent-runner/src/mcp-tools/index.ts:1-22`
```
1. 导入链index.ts → 导入 core.ts, scheduling.ts, interactive.ts,
agents.ts, self-mod.ts 以便它们的副作用 registerTools([...]) 调用生效
2. 所有导入解决后,调用 startMcpServer()
3. 如果 server 崩溃process.exit(1)
```
`startMcpServer()``server.ts:35-54`
1. 创建 MCP `Server` 实例name=`'nanoclaw'`version=`'2.0.0'`
2. 注册两个请求 handler
- `ListToolsRequestSchema` → 返回 `allTools.map(t => t.tool)`line 38-40
- `CallToolRequestSchema` → 查 `toolMap.get(name)`,调 `tool.handler(args)`line 42-48
3. 创建 `StdioServerTransport` 并连接line 51-52——Claude SDK 通过 stdio 发现 MCP server
4. 记录所有注册的 tool 名称
### Tool 自注册模式
每个 tool 模块在模块作用域调用 `registerTools([...])`。`registerTools()``server.ts:24-33`):将每个 `McpToolDefinition` 推入两个结构:`allTools[]`ListTools 用)和 `toolMap`name→definitionCallTool 用)。重复的名称警告但不报错。
`McpToolDefinition` 类型(`types.ts:1-6`
```typescript
interface McpToolDefinition {
tool: Tool; // MCP SDK Tool schema (name, description, inputSchema)
handler: (args) => Promise<CallToolResult>;
}
```
### 内置 Tool vs 外部 MCP Server
**内置in-tree工具** — 定义在 `container/agent-runner/src/mcp-tools/`
| 模块 | 工具 | 机制 |
|------|------|------|
| `core.ts`line 95-263 | `send_message`、`send_file`、`edit_message`、`add_reaction` | 写 `outbound.db` 的 `messages_out` 表;通过 local destinations map 解析目标 |
| `scheduling.ts` | `schedule_task` | 持久调度,`process_after` / `recurrence` 字段 |
| `interactive.ts` | `ask_user_question` | 写中央库 `pending_questions`host poll 并发送卡片 |
| `agents.ts` | `create_agent` | 通过 system actions spawn 子 agent 容器 |
| `self-mod.ts` | `install_packages`、`add_mcp_server` | 写 `pending_approvals` 表host 处理审批和容器重建 |
这些工具以 JavaScript 函数形式**运行在容器进程内部**。与 host 通信通过写 DB 表(`messages_out`、`pending_approvals` 等)——不是 IPC。
**外部 MCP server** — per-agent-group 配置在 `container_configs.mcp_servers`JSON 字符串,默认 `'{}'`
```json
{
"server-name": {
"command": "npx",
"args": ["-y", "@some/mcp-server"],
"env": { "API_KEY": "..." }
}
}
```
Claude provider 把 `this.mcpServers` 直接传给 SDK 的 `mcpServers` 选项(`claude.ts:306`。SDK 以子进程方式 **spawn 它们**,通过 stdio 自动发现它们的工具。Tool allowlist`claude.ts:66-68`)派生 MCP patterns
```typescript
function mcpAllowPattern(serverName: string): string {
return `mcp__${serverName.replace(/[^a-zA-Z0-9_-]/g, '_')}__*`;
}
```
**关键区别:** 内置工具写 session DB外部 MCP server 是由 SDK 管理的 stdio 子进程,通过 MCP 协议通信。内置工具能直接访问所有内部 DB 表和 destination/resolution 系统;外部 MCP 工具只能看到 provider SDK 通过 MCP 协议暴露的内容。
### MCP Tools ↔ Provider 交互
交互是间接的:
1. Provider 启动 → 传 `mcpServers` config 给 SDK → SDK spawn 外部 MCP 进程并发现它们的 tool schema
2. **NanoClaw MCP server**stdio在 provider 之前由 `index.ts` 启动——SDK 连接它并发现内置 tool schema
3. AgentClaude 模型)决定调用 tool → SDK 路由到内置或外部 MCP server → 结果返回模型
4. Provider hooks`PreToolUse`、`PostToolUse`)对**所有** tool 调用运行,不管来源(`claude.ts:160-189`)——用于为 host sweep 的卡住容忍度逻辑跟踪 tool-in-flight 状态
Provider 永远不会直接"调用"MCP tool。模型调用。Provider 只设置环境MCP server configs、tool allowlists、hooks
### 不允许的 SDK Builtins
`claude.ts:25-35` 定义 `SDK_DISALLOWED_TOOLS` —— Claude Code SDK builtins 被屏蔽,因为 NanoClaw 有等价实现或它们不适合无头模型:
- `CronCreate/CronDelete/CronList/ScheduleWakeup` → 被 `mcp__nanoclaw__schedule_task` 替代
- `AskUserQuestion` → 被 `mcp__nanoclaw__ask_user_question` 替代
- `EnterPlanMode/ExitPlanMode/EnterWorktree/ExitWorktree` → Claude Code UI 功能;在无头容器中挂起
如果 disallowed tool 意外通过 allowlist 过滤器,`preToolUseHook`line 160-169在调用时拦截它——深度防御。