OpenCode 是一个开源的 AI 编程助手,类似于 Claude Code,使用 TypeScript 开发。本文将深入分析 OpenCode 的架构设计,帮助你理解其内部工作原理。

1. 整体架构概览

OpenCode 采用模块化的分层架构设计,主要由以下几个核心组件构成:

┌─────────────────────────────────────────────────────────────┐
│                      CLI Interface                          │
├─────────────────────────────────────────────────────────────┤
│                      Session Manager                        │
├─────────────────────────────────────────────────────────────┤
│    ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│    │   Provider   │  │    Agent     │  │    Tool      │     │
│    └──────────────┘  └──────────────┘  └──────────────┘     │
├─────────────────────────────────────────────────────────────┤
│    ┌──────────────┐  ┌──────────────┐  ┌──────────────┐     │
│    │   Context    │  │   Message    │  │   Config     │     │
│    └──────────────┘  └──────────────┘  └──────────────┘     │
└─────────────────────────────────────────────────────────────┘

2. 项目结构

OpenCode 的典型项目结构如下:

opencode/
├── src/
│   ├── index.ts              # 入口文件
│   ├── cli.ts                # CLI 命令定义
│   ├── commands/             # 各种命令实现
│   │   ├── chat.ts           # 交互式聊天
│   │   ├── completion.ts     # 单次补全
│   │   └── config.ts         # 配置管理
│   ├── providers/            # LLM 提供者
│   │   ├── base.ts           # 基础接口
│   │   ├── anthropic.ts      # Anthropic Claude
│   │   ├── openai.ts         # OpenAI
│   │   ├── ollama.ts         # Ollama 本地模型
│   │   └── index.ts          # 提供者注册
│   ├── tools/                # 工具系统
│   │   ├── base.ts           # 工具基类
│   │   ├── file.ts           # 文件操作
│   │   ├── shell.ts          # Shell 命令
│   │   ├── search.ts         # 代码搜索
│   │   └── index.ts          # 工具注册
│   ├── agent/                # 智能体核心
│   │   ├── agent.ts          # Agent 实现
│   │   ├── planner.ts        # 任务规划
│   │   └── executor.ts       # 执行器
│   ├── context/              # 上下文管理
│   │   ├── manager.ts        # 上下文管理器
│   │   ├── file-tracker.ts   # 文件追踪
│   │   └── token-counter.ts  # Token 计数
│   ├── session/              # 会话管理
│   │   ├── session.ts        # 会话实现
│   │   ├── history.ts        # 历史记录
│   │   └── storage.ts        # 持久化存储
│   ├── config/               # 配置系统
│   │   ├── schema.ts         # 配置 Schema
│   │   ├── loader.ts         # 配置加载
│   │   └── defaults.ts       # 默认配置
│   └── utils/               # 工具函数
│       ├── logger.ts         # 日志
│       ├── prompt.ts         # Prompt 模板
│       └── format.ts         # 格式化
├── package.json
├── tsconfig.json
└── README.md

3. 核心组件详解

3.1 Provider(模型提供者)

Provider 是一个抽象层,统一不同 LLM 提供商的接口:

// src/providers/base.ts
export interface ProviderConfig {
  apiKey?: string;
  baseUrl?: string;
  model: string;
  maxTokens?: number;
  temperature?: number;
}

export interface ChatMessage {
  role: 'system' | 'user' | 'assistant' | 'tool';
  content: string;
  toolCalls?: ToolCall[];
  toolCallId?: string;
}

export interface ProviderResponse {
  content: string;
  toolCalls?: ToolCall[];
  usage: {
    promptTokens: number;
    completionTokens: number;
  };
  stopReason: 'end_turn' | 'tool_use' | 'max_tokens';
}

export interface Provider {
  readonly name: string;
  
  // 同步完成
  complete(messages: ChatMessage[], tools?: ToolDefinition[]): Promise<ProviderResponse>;
  
  // 流式完成
  stream(messages: ChatMessage[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk>;
  
  // 获取可用模型列表
  models(): ModelInfo[];
  
  // 验证配置
  validate(): Promise<boolean>;
}

Anthropic Provider 实现

// src/providers/anthropic.ts
import Anthropic from '@anthropic-ai/sdk';
import { Provider, ChatMessage, ProviderResponse } from './base';

export class AnthropicProvider implements Provider {
  readonly name = 'anthropic';
  private client: Anthropic;
  private model: string;
  
  constructor(config: ProviderConfig) {
    this.client = new Anthropic({
      apiKey: config.apiKey || process.env.ANTHROPIC_API_KEY,
      baseURL: config.baseUrl,
    });
    this.model = config.model || 'claude-3-5-sonnet-20241022';
  }
  
  async complete(
    messages: ChatMessage[], 
    tools?: ToolDefinition[]
  ): Promise<ProviderResponse> {
    const response = await this.client.messages.create({
      model: this.model,
      max_tokens: 4096,
      messages: this.convertMessages(messages),
      tools: this.convertTools(tools),
    });
    
    return {
      content: this.extractContent(response),
      toolCalls: this.extractToolCalls(response),
      usage: {
        promptTokens: response.usage.input_tokens,
        completionTokens: response.usage.output_tokens,
      },
      stopReason: response.stop_reason,
    };
  }
  
  async *stream(
    messages: ChatMessage[], 
    tools?: ToolDefinition[]
  ): AsyncIterable<StreamChunk> {
    const stream = await this.client.messages.stream({
      model: this.model,
      max_tokens: 4096,
      messages: this.convertMessages(messages),
      tools: this.convertTools(tools),
    });
    
    for await (const event of stream) {
      yield this.convertChunk(event);
    }
  }
  
  private convertMessages(messages: ChatMessage[]): Anthropic.MessageParam[] {
    return messages.map(msg => ({
      role: msg.role,
      content: msg.content,
    }));
  }
}

Ollama Provider 实现

// src/providers/ollama.ts
import ollama from 'ollama';
import { Provider, ChatMessage, ProviderResponse } from './base';

export class OllamaProvider implements Provider {
  readonly name = 'ollama';
  private model: string;
  private baseUrl: string;
  
  constructor(config: ProviderConfig) {
    this.model = config.model || 'llama3.1';
    this.baseUrl = config.baseUrl || 'http://localhost:11434';
  }
  
  async complete(
    messages: ChatMessage[], 
    tools?: ToolDefinition[]
  ): Promise<ProviderResponse> {
    const response = await ollama.chat({
      model: this.model,
      messages: this.convertMessages(messages),
      tools: this.convertTools(tools),
    });
    
    return {
      content: response.message.content,
      toolCalls: this.extractToolCalls(response),
      usage: {
        promptTokens: response.prompt_eval_count || 0,
        completionTokens: response.eval_count || 0,
      },
      stopReason: 'end_turn',
    };
  }
  
  async *stream(
    messages: ChatMessage[], 
    tools?: ToolDefinition[]
  ): AsyncIterable<StreamChunk> {
    const stream = await ollama.chat({
      model: this.model,
      messages: this.convertMessages(messages),
      stream: true,
    });
    
    for await (const chunk of stream) {
      yield {
        type: 'content',
        content: chunk.message.content,
      };
    }
  }
}

3.2 Tool System(工具系统)

工具系统是 OpenCode 与外部环境交互的关键:

// src/tools/base.ts
import { z } from 'zod';

export interface ToolDefinition {
  name: string;
  description: string;
  parameters: z.ZodType<any>;
}

export interface ToolResult {
  success: boolean;
  content: string;
  metadata?: Record<string, any>;
}

export interface ToolContext {
  workingDirectory: string;
  sessionId: string;
  permissions: PermissionManager;
}

export abstract class Tool {
  abstract readonly name: string;
  abstract readonly description: string;
  abstract readonly parameters: z.ZodType<any>;
  
  abstract execute(params: any, context: ToolContext): Promise<ToolResult>;
  
  toDefinition(): ToolDefinition {
    return {
      name: this.name,
      description: this.description,
      parameters: this.parameters,
    };
  }
}

文件读取工具

// src/tools/file.ts
import { Tool, ToolResult, ToolContext, ToolDefinition } from './base';
import { z } from 'zod';
import * as fs from 'fs/promises';
import * as path from 'path';

export class FileReadTool extends Tool {
  readonly name = 'read_file';
  readonly description = '读取指定路径的文件内容';
  readonly parameters = z.object({
    path: z.string().describe('要读取的文件路径'),
    start_line: z.number().optional().describe('起始行号'),
    end_line: z.number().optional().describe('结束行号'),
  });
  
  async execute(params: any, context: ToolContext): Promise<ToolResult> {
    const { path: filePath, start_line, end_line } = params;
    
    // 安全检查:确保路径在工作目录内
    const absolutePath = path.resolve(context.workingDirectory, filePath);
    if (!absolutePath.startsWith(context.workingDirectory)) {
      return {
        success: false,
        content: 'Error: Cannot read files outside working directory',
      };
    }
    
    try {
      let content = await fs.readFile(absolutePath, 'utf-8');
      
      // 处理行号范围
      if (start_line !== undefined || end_line !== undefined) {
        const lines = content.split('\n');
        const start = (start_line || 1) - 1;
        const end = end_line || lines.length;
        content = lines.slice(start, end).join('\n');
      }
      
      return {
        success: true,
        content,
        metadata: { path: filePath },
      };
    } catch (error) {
      return {
        success: false,
        content: `Error reading file: ${error}`,
      };
    }
  }
}

Shell 执行工具

// src/tools/shell.ts
import { Tool, ToolResult, ToolContext } from './base';
import { z } from 'zod';
import { spawn } from 'child_process';

export class ShellTool extends Tool {
  readonly name = 'execute_command';
  readonly description = '执行 shell 命令';
  readonly parameters = z.object({
    command: z.string().describe('要执行的命令'),
    timeout: z.number().optional().default(30000).describe('超时时间(ms)'),
  });
  
  async execute(params: any, context: ToolContext): Promise<ToolResult> {
    const { command, timeout } = params;
    
    // 权限检查
    const permission = await context.permissions.check('shell', command);
    if (permission === 'denied') {
      return {
        success: false,
        content: 'Error: Command execution denied',
      };
    }
    
    if (permission === 'ask') {
      // 需要用户确认
      const approved = await this.requestApproval(command, context);
      if (!approved) {
        return {
          success: false,
          content: 'Error: Command execution rejected by user',
        };
      }
    }
    
    return new Promise((resolve) => {
      const proc = spawn('sh', ['-c', command], {
        cwd: context.workingDirectory,
        timeout,
      });
      
      let stdout = '';
      let stderr = '';
      
      proc.stdout.on('data', (data) => {
        stdout += data.toString();
      });
      
      proc.stderr.on('data', (data) => {
        stderr += data.toString();
      });
      
      proc.on('close', (code) => {
        resolve({
          success: code === 0,
          content: stdout || stderr,
          metadata: { exitCode: code },
        });
      });
      
      proc.on('error', (error) => {
        resolve({
          success: false,
          content: `Error executing command: ${error.message}`,
        });
      });
    });
  }
  
  private async requestApproval(command: string, context: ToolContext): Promise<boolean> {
    // 实现用户确认逻辑
    return true;
  }
}

工具注册表

// src/tools/index.ts
import { Tool, ToolDefinition } from './base';
import { FileReadTool } from './file';
import { FileWriteTool } from './file-write';
import { ShellTool } from './shell';
import { SearchTool } from './search';

export class ToolRegistry {
  private tools: Map<string, Tool> = new Map();
  
  constructor() {
    // 注册内置工具
    this.register(new FileReadTool());
    this.register(new FileWriteTool());
    this.register(new ShellTool());
    this.register(new SearchTool());
  }
  
  register(tool: Tool): void {
    this.tools.set(tool.name, tool);
  }
  
  get(name: string): Tool | undefined {
    return this.tools.get(name);
  }
  
  getAll(): Tool[] {
    return Array.from(this.tools.values());
  }
  
  getDefinitions(): ToolDefinition[] {
    return this.getAll().map(tool => tool.toDefinition());
  }
  
  // 过滤允许的工具
  filter(allowed: string[], denied: string[]): ToolRegistry {
    const filtered = new ToolRegistry();
    for (const [name, tool] of this.tools) {
      if (denied.includes(name)) continue;
      if (allowed.length === 0 || allowed.includes(name)) {
        filtered.register(tool);
      }
    }
    return filtered;
  }
}

3.3 Agent(智能体)

Agent 是 OpenCode 的核心智能组件:

// src/agent/agent.ts
import { Provider, ChatMessage, ProviderResponse } from '../providers/base';
import { ToolRegistry, ToolResult } from '../tools';
import { Session } from '../session/session';

export interface AgentConfig {
  maxSteps: number;
  systemPrompt: string;
}

export class Agent {
  private provider: Provider;
  private tools: ToolRegistry;
  private config: AgentConfig;
  
  constructor(provider: Provider, tools: ToolRegistry, config?: Partial<AgentConfig>) {
    this.provider = provider;
    this.tools = tools;
    this.config = {
      maxSteps: config?.maxSteps || 50,
      systemPrompt: config?.systemPrompt || this.getDefaultSystemPrompt(),
    };
  }
  
  async execute(
    task: string, 
    session: Session,
    onProgress?: (event: AgentEvent) => void
  ): Promise<string> {
    const messages: ChatMessage[] = [
      { role: 'system', content: this.config.systemPrompt },
      { role: 'user', content: task },
    ];
    
    for (let step = 0; step < this.config.maxSteps; step++) {
      onProgress?.({ type: 'step', step });
      
      // 调用 LLM
      const response = await this.provider.complete(
        messages,
        this.tools.getDefinitions()
      );
      
      // 添加助手消息
      messages.push({
        role: 'assistant',
        content: response.content,
        toolCalls: response.toolCalls,
      });
      
      // 检查是否需要调用工具
      if (response.toolCalls && response.toolCalls.length > 0) {
        onProgress?.({ type: 'tool_calls', calls: response.toolCalls });
        
        // 执行所有工具调用
        const results = await this.executeTools(response.toolCalls, session);
        
        // 添加工具结果消息
        for (const result of results) {
          messages.push({
            role: 'tool',
            toolCallId: result.toolCallId,
            content: result.content,
          });
        }
        
        continue;
      }
      
      // 没有工具调用,返回最终结果
      return response.content;
    }
    
    throw new Error('Max steps exceeded');
  }
  
  private async executeTools(
    calls: ToolCall[], 
    session: Session
  ): Promise<ToolResult[]> {
    const results: ToolResult[] = [];
    
    for (const call of calls) {
      const tool = this.tools.get(call.name);
      if (!tool) {
        results.push({
          toolCallId: call.id,
          content: `Error: Tool ${call.name} not found`,
        });
        continue;
      }
      
      try {
        const result = await tool.execute(call.arguments, {
          workingDirectory: session.workingDirectory,
          sessionId: session.id,
          permissions: session.permissions,
        });
        
        results.push({
          toolCallId: call.id,
          content: result.content,
        });
      } catch (error) {
        results.push({
          toolCallId: call.id,
          content: `Error: ${error}`,
        });
      }
    }
    
    return results;
  }
  
  private getDefaultSystemPrompt(): string {
    return `You are an AI programming assistant. You can use tools to help the user.

When using tools:
1. Read files before modifying them
2. Test your changes when possible
3. Explain what you're doing

Available tools: ${this.tools.getAll().map(t => t.name).join(', ')}`;
  }
}

3.4 Session Manager(会话管理)

// src/session/session.ts
import { v4 as uuidv4 } from 'uuid';
import { ChatMessage } from '../providers/base';
import { PermissionManager } from '../permissions';

export interface SessionConfig {
  workingDirectory: string;
  maxHistoryLength?: number;
}

export class Session {
  readonly id: string;
  readonly workingDirectory: string;
  readonly permissions: PermissionManager;
  
  private messages: ChatMessage[] = [];
  private maxHistoryLength: number;
  
  constructor(config: SessionConfig) {
    this.id = uuidv4();
    this.workingDirectory = config.workingDirectory;
    this.maxHistoryLength = config.maxHistoryLength || 100;
    this.permissions = new PermissionManager();
  }
  
  addMessage(message: ChatMessage): void {
    this.messages.push(message);
    
    // 如果历史过长,裁剪
    if (this.messages.length > this.maxHistoryLength) {
      this.pruneHistory();
    }
  }
  
  getMessages(): ChatMessage[] {
    return [...this.messages];
  }
  
  private pruneHistory(): void {
    // 保留系统消息和最近的对话
    const systemMessages = this.messages.filter(m => m.role === 'system');
    const recentMessages = this.messages
      .filter(m => m.role !== 'system')
      .slice(-this.maxHistoryLength + systemMessages.length);
    
    this.messages = [...systemMessages, ...recentMessages];
  }
  
  // 序列化会话
  toJSON(): object {
    return {
      id: this.id,
      workingDirectory: this.workingDirectory,
      messages: this.messages,
      permissions: this.permissions.toJSON(),
    };
  }
  
  // 从 JSON 恢复
  static fromJSON(data: any): Session {
    const session = new Session({
      workingDirectory: data.workingDirectory,
    });
    session.messages = data.messages;
    session.permissions.fromJSON(data.permissions);
    return session;
  }
}

3.5 Context Management(上下文管理)

// src/context/manager.ts
import { ChatMessage } from '../providers/base';
import { TokenCounter } from './token-counter';

export interface FileInfo {
  path: string;
  content: string;
  lastRead: Date;
  importance: number;
}

export class ContextManager {
  private tokenCounter: TokenCounter;
  private maxTokens: number;
  private files: Map<string, FileInfo> = new Map();
  
  constructor(maxTokens: number = 128000) {
    this.tokenCounter = new TokenCounter();
    this.maxTokens = maxTokens;
  }
  
  // 添加文件到上下文
  addFile(path: string, content: string): void {
    this.files.set(path, {
      path,
      content,
      lastRead: new Date(),
      importance: 1.0,
    });
  }
  
  // 构建上下文消息
  buildContextMessages(): ChatMessage[] {
    const messages: ChatMessage[] = [];
    
    // 添加文件上下文
    if (this.files.size > 0) {
      const fileContext = this.formatFileContext();
      messages.push({
        role: 'system',
        content: `Here are the relevant files:\n\n${fileContext}`,
      });
    }
    
    return messages;
  }
  
  private formatFileContext(): string {
    const parts: string[] = [];
    
    for (const [path, info] of this.files) {
      parts.push(`--- ${path} ---\n${info.content}\n`);
    }
    
    return parts.join('\n');
  }
  
  // 智能裁剪
  pruneToFit(messages: ChatMessage[]): ChatMessage[] {
    let totalTokens = this.tokenCounter.countMessages(messages);
    
    if (totalTokens <= this.maxTokens) {
      return messages;
    }
    
    // 策略1: 移除不重要的文件
    for (const [path, info] of this.files) {
      if (info.importance < 0.5) {
        this.files.delete(path);
        totalTokens -= this.tokenCounter.count(info.content);
        
        if (totalTokens <= this.maxTokens) {
          return messages;
        }
      }
    }
    
    // 策略2: 压缩历史消息
    const compressed = this.compressHistory(messages);
    totalTokens = this.tokenCounter.countMessages(compressed);
    
    if (totalTokens <= this.maxTokens) {
      return compressed;
    }
    
    // 策略3: 移除最旧的消息
    return this.removeOldest(compressed, totalTokens);
  }
  
  private compressHistory(messages: ChatMessage[]): ChatMessage[] {
    // 实现消息压缩逻辑
    // 可以使用摘要模型来压缩历史对话
    return messages;
  }
  
  private removeOldest(messages: ChatMessage[], currentTokens: number): ChatMessage[] {
    // 保留系统消息,移除最旧的用户/助手消息
    const systemMessages = messages.filter(m => m.role === 'system');
    const otherMessages = messages.filter(m => m.role !== 'system');
    
    while (currentTokens > this.maxTokens && otherMessages.length > 2) {
      const removed = otherMessages.shift();
      if (removed) {
        currentTokens -= this.tokenCounter.count(removed.content);
      }
    }
    
    return [...systemMessages, ...otherMessages];
  }
}

4. CLI 入口

// src/cli.ts
import { Command } from 'commander';
import { ChatCommand } from './commands/chat';
import { ConfigCommand } from './commands/config';
import { version } from '../package.json';

const program = new Command();

program
  .name('opencode')
  .description('AI-powered coding assistant')
  .version(version);

program
  .command('chat')
  .description('Start an interactive chat session')
  .option('-p, --provider <name>', 'LLM provider to use', 'anthropic')
  .option('-m, --model <name>', 'Model to use')
  .option('-d, --directory <path>', 'Working directory', process.cwd())
  .action(async (options) => {
    const chat = new ChatCommand();
    await chat.run(options);
  });

program
  .command('complete <prompt>')
  .description('Execute a single prompt and exit')
  .option('-p, --provider <name>', 'LLM provider to use', 'anthropic')
  .option('-m, --model <name>', 'Model to use')
  .action(async (prompt, options) => {
    // 单次补全逻辑
  });

program
  .command('config')
  .description('Manage configuration')
  .command('set <key> <value>')
  .description('Set a configuration value')
  .action(async (key, value) => {
    // 配置设置逻辑
  });

program.parse();
// src/commands/chat.ts
import inquirer from 'inquirer';
import chalk from 'chalk';
import { ProviderFactory } from '../providers';
import { ToolRegistry } from '../tools';
import { Agent } from '../agent/agent';
import { Session } from '../session/session';

export class ChatCommand {
  async run(options: { provider: string; model?: string; directory: string }) {
    // 创建 Provider
    const provider = ProviderFactory.create(options.provider, {
      model: options.model,
    });
    
    // 创建工具注册表
    const tools = new ToolRegistry();
    
    // 创建会话
    const session = new Session({
      workingDirectory: options.directory,
    });
    
    // 创建 Agent
    const agent = new Agent(provider, tools);
    
    console.log(chalk.green('OpenCode Chat'));
    console.log(chalk.gray('Type /help for commands, /exit to quit\n'));
    
    // 主循环
    while (true) {
      const { prompt } = await inquirer.prompt([
        {
          type: 'input',
          name: 'prompt',
          message: chalk.blue('You:'),
        },
      ]);
      
      // 处理命令
      if (prompt.startsWith('/')) {
        const handled = await this.handleCommand(prompt, session);
        if (handled === 'exit') break;
        continue;
      }
      
      // 执行任务
      console.log(chalk.yellow('\nAssistant:'));
      const response = await agent.execute(prompt, session, (event) => {
        this.handleProgress(event);
      });
      
      console.log(response);
      console.log();
    }
  }
  
  private async handleCommand(command: string, session: Session): Promise<string | void> {
    switch (command.trim()) {
      case '/help':
        console.log('Commands: /help, /exit, /clear, /save, /load');
        break;
      case '/exit':
        return 'exit';
      case '/clear':
        session.clear();
        console.log('Session cleared');
        break;
      default:
        console.log('Unknown command');
    }
  }
  
  private handleProgress(event: AgentEvent): void {
    switch (event.type) {
      case 'tool_calls':
        console.log(chalk.gray(`Using tools: ${event.calls.map(c => c.name).join(', ')}`));
        break;
      case 'step':
        console.log(chalk.gray(`Step ${event.step}`));
        break;
    }
  }
}

5. 配置系统

// src/config/schema.ts
import { z } from 'zod';

export const ConfigSchema = z.object({
  provider: z.enum(['anthropic', 'openai', 'ollama']).default('anthropic'),
  model: z.string().optional(),
  apiKey: z.string().optional(),
  baseUrl: z.string().optional(),
  
  generation: z.object({
    maxTokens: z.number().default(4096),
    temperature: z.number().default(0.7),
    topP: z.number().default(1.0),
  }).default({}),
  
  tools: z.object({
    requireApproval: z.boolean().default(true),
    allowed: z.array(z.string()).default([]),
    denied: z.array(z.string()).default([]),
  }).default({}),
  
  context: z.object({
    maxTokens: z.number().default(128000),
    includePatterns: z.array(z.string()).default(['**/*']),
    excludePatterns: z.array(z.string()).default(['node_modules/**', '.git/**']),
  }).default({}),
  
  logging: z.object({
    level: z.enum(['debug', 'info', 'warn', 'error']).default('info'),
    file: z.string().optional(),
  }).default({}),
});

export type Config = z.infer<typeof ConfigSchema>;
// src/config/loader.ts
import * as fs from 'fs/promises';
import * as path from 'path';
import { ConfigSchema, Config } from './schema';

export class ConfigLoader {
  private static CONFIG_FILES = [
    '.opencode/config.json',
    '.opencode.json',
    'opencode.json',
  ];
  
  async load(directory: string): Promise<Config> {
    // 1. 加载默认配置
    let config = ConfigSchema.parse({});
    
    // 2. 加载用户配置
    const userConfig = await this.loadUserConfig();
    config = this.merge(config, userConfig);
    
    // 3. 加载项目配置
    const projectConfig = await this.loadProjectConfig(directory);
    config = this.merge(config, projectConfig);
    
    // 4. 加载环境变量
    config = this.loadEnvVars(config);
    
    // 5. 验证
    return ConfigSchema.parse(config);
  }
  
  private async loadProjectConfig(directory: string): Promise<Partial<Config>> {
    for (const file of ConfigLoader.CONFIG_FILES) {
      const filePath = path.join(directory, file);
      try {
        const content = await fs.readFile(filePath, 'utf-8');
        return JSON.parse(content);
      } catch {
        // 文件不存在,继续
      }
    }
    return {};
  }
  
  private async loadUserConfig(): Promise<Partial<Config>> {
    const homeDir = process.env.HOME || process.env.USERPROFILE;
    const configPath = path.join(homeDir, '.config', 'opencode', 'config.json');
    
    try {
      const content = await fs.readFile(configPath, 'utf-8');
      return JSON.parse(content);
    } catch {
      return {};
    }
  }
  
  private loadEnvVars(config: Config): Config {
    return {
      ...config,
      apiKey: process.env.OPENCODE_API_KEY || config.apiKey,
      baseUrl: process.env.OPENCODE_BASE_URL || config.baseUrl,
      model: process.env.OPENCODE_MODEL || config.model,
    };
  }
  
  private merge(base: Config, override: Partial<Config>): Config {
    return {
      ...base,
      ...override,
      generation: { ...base.generation, ...override.generation },
      tools: { ...base.tools, ...override.tools },
      context: { ...base.context, ...override.context },
      logging: { ...base.logging, ...override.logging },
    };
  }
}

6. 数据流分析

6.1 用户请求处理流程

用户输入
    │
    ▼
┌─────────────┐
│ CLI 解析    │
└─────┬───────┘
      │
      ▼
┌─────────────┐
│ 构建消息    │ ◄─── 添加系统提示
└─────┬───────┘ ◄─── 加载历史消息
      │         ◄─── 注入文件上下文
      ▼
┌─────────────┐
│ Provider    │
│ 调用 LLM    │
└─────┬───────┘
      │
      ▼
┌─────────────┐
│ 解析响应    │
└─────┬───────┘
      │
      ├─── 文本响应 ──► 渲染输出
      │
      └─── 工具调用 ──► 执行工具 ──► 构建新消息 ──► 循环

6.2 工具调用流程

// 工具调用处理流程
async function handleToolCalls(
  calls: ToolCall[], 
  tools: ToolRegistry, 
  session: Session
): Promise<ChatMessage[]> {
  const results: ChatMessage[] = [];
  
  for (const call of calls) {
    // 1. 获取工具
    const tool = tools.get(call.name);
    if (!tool) {
      results.push({
        role: 'tool',
        toolCallId: call.id,
        content: `Error: Tool ${call.name} not found`,
      });
      continue;
    }
    
    // 2. 权限检查
    const permission = await session.permissions.check(call.name, call.arguments);
    if (permission === 'denied') {
      results.push({
        role: 'tool',
        toolCallId: call.id,
        content: 'Error: Tool call denied',
      });
      continue;
    }
    
    if (permission === 'ask') {
      const approved = await requestUserApproval(call);
      if (!approved) {
        results.push({
          role: 'tool',
          toolCallId: call.id,
          content: 'Error: Tool call rejected by user',
        });
        continue;
      }
    }
    
    // 3. 执行工具
    try {
      const result = await tool.execute(call.arguments, {
        workingDirectory: session.workingDirectory,
        sessionId: session.id,
        permissions: session.permissions,
      });
      
      results.push({
        role: 'tool',
        toolCallId: call.id,
        content: result.content,
      });
    } catch (error) {
      results.push({
        role: 'tool',
        toolCallId: call.id,
        content: `Error: ${error}`,
      });
    }
  }
  
  return results;
}

7. 扩展机制

7.1 自定义 Provider

// src/providers/custom.ts
import { Provider, ChatMessage, ProviderResponse, StreamChunk } from './base';

export class CustomProvider implements Provider {
  readonly name = 'custom';
  private baseUrl: string;
  private apiKey: string;
  private model: string;
  
  constructor(config: ProviderConfig) {
    this.baseUrl = config.baseUrl || 'https://api.example.com/v1';
    this.apiKey = config.apiKey || '';
    this.model = config.model || 'default';
  }
  
  async complete(
    messages: ChatMessage[], 
    tools?: ToolDefinition[]
  ): Promise<ProviderResponse> {
    const response = await fetch(`${this.baseUrl}/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`,
      },
      body: JSON.stringify({
        model: this.model,
        messages: messages,
        tools: tools,
      }),
    });
    
    const data = await response.json();
    
    return {
      content: data.choices[0].message.content,
      toolCalls: data.choices[0].message.tool_calls,
      usage: {
        promptTokens: data.usage.prompt_tokens,
        completionTokens: data.usage.completion_tokens,
      },
      stopReason: data.choices[0].finish_reason,
    };
  }
  
  async *stream(
    messages: ChatMessage[], 
    tools?: ToolDefinition[]
  ): AsyncIterable<StreamChunk> {
    const response = await fetch(`${this.baseUrl}/chat/completions`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.apiKey}`,
      },
      body: JSON.stringify({
        model: this.model,
        messages: messages,
        tools: tools,
        stream: true,
      }),
    });
    
    const reader = response.body?.getReader();
    if (!reader) return;
    
    const decoder = new TextDecoder();
    
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;
      
      const chunk = decoder.decode(value);
      const lines = chunk.split('\n').filter(line => line.startsWith('data: '));
      
      for (const line of lines) {
        const data = JSON.parse(line.slice(6));
        yield {
          type: 'content',
          content: data.choices[0]?.delta?.content || '',
        };
      }
    }
  }
}

7.2 自定义 Tool

// src/tools/database.ts
import { Tool, ToolResult, ToolContext } from './base';
import { z } from 'zod';
import { Database } from 'sqlite3';

export class DatabaseQueryTool extends Tool {
  readonly name = 'db_query';
  readonly description = 'Execute SQL queries on the project database';
  readonly parameters = z.object({
    query: z.string().describe('SQL query to execute'),
    params: z.array(z.any()).optional().describe('Query parameters'),
  });
  
  private db: Database;
  
  constructor(db: Database) {
    super();
    this.db = db;
  }
  
  async execute(params: any, context: ToolContext): Promise<ToolResult> {
    const { query, params: queryParams = [] } = params;
    
    // 安全检查:只允许 SELECT 查询
    if (!this.isSafeQuery(query)) {
      return {
        success: false,
        content: 'Error: Only SELECT queries are allowed',
      };
    }
    
    return new Promise((resolve) => {
      this.db.all(query, queryParams, (err, rows) => {
        if (err) {
          resolve({
            success: false,
            content: `Error: ${err.message}`,
          });
        } else {
          resolve({
            success: true,
            content: JSON.stringify(rows, null, 2),
          });
        }
      });
    });
  }
  
  private isSafeQuery(query: string): boolean {
    const normalized = query.trim().toUpperCase();
    return normalized.startsWith('SELECT') && 
           !normalized.includes('INSERT') &&
           !normalized.includes('UPDATE') &&
           !normalized.includes('DELETE') &&
           !normalized.includes('DROP');
  }
}

8. 安全机制

8.1 权限管理

// src/permissions/manager.ts
export type PermissionAction = 'allow' | 'deny' | 'ask';

export interface PermissionRule {
  tool: string;
  action: PermissionAction;
  condition?: (params: any) => boolean;
}

export class PermissionManager {
  private rules: PermissionRule[] = [];
  
  constructor() {
    // 默认规则
    this.rules = [
      { tool: 'read_file', action: 'allow' },
      { tool: 'write_file', action: 'ask' },
      { tool: 'execute_command', action: 'ask' },
    ];
  }
  
  async check(tool: string, params: any): Promise<PermissionAction> {
    for (const rule of this.rules) {
      if (rule.tool === tool || rule.tool === '*') {
        if (!rule.condition || rule.condition(params)) {
          return rule.action;
        }
      }
    }
    return 'ask'; // 默认询问用户
  }
  
  addRule(rule: PermissionRule): void {
    this.rules.unshift(rule); // 新规则优先
  }
  
  toJSON(): object {
    return { rules: this.rules };
  }
  
  fromJSON(data: any): void {
    if (data.rules) {
      this.rules = data.rules;
    }
  }
}

8.2 沙箱执行

// src/tools/sandbox.ts
import { spawn } from 'child_process';
import * as path from 'path';

export class Sandbox {
  private allowedCommands: Set<string>;
  private deniedPatterns: RegExp[];
  private workDir: string;
  private env: NodeJS.ProcessEnv;
  
  constructor(config: {
    allowedCommands?: string[];
    deniedPatterns?: string[];
    workDir: string;
    env?: NodeJS.ProcessEnv;
  }) {
    this.allowedCommands = new Set(config.allowedCommands || ['ls', 'cat', 'grep', 'find']);
    this.deniedPatterns = (config.deniedPatterns || [
      'rm -rf',
      'sudo',
      'chmod 777',
      '> /dev/',
    ]).map(p => new RegExp(p));
    this.workDir = config.workDir;
    this.env = config.env || process.env;
  }
  
  async execute(command: string, args: string[] = []): Promise<{ stdout: string; stderr: string; code: number }> {
    // 检查命令是否允许
    if (!this.allowedCommands.has(command)) {
      throw new Error(`Command not allowed: ${command}`);
    }
    
    // 检查参数是否匹配危险模式
    const fullCommand = `${command} ${args.join(' ')}`;
    for (const pattern of this.deniedPatterns) {
      if (pattern.test(fullCommand)) {
        throw new Error(`Command matches denied pattern: ${pattern}`);
      }
    }
    
    return new Promise((resolve, reject) => {
      const proc = spawn(command, args, {
        cwd: this.workDir,
        env: this.env,
        timeout: 30000,
      });
      
      let stdout = '';
      let stderr = '';
      
      proc.stdout.on('data', (data) => {
        stdout += data.toString();
      });
      
      proc.stderr.on('data', (data) => {
        stderr += data.toString();
      });
      
      proc.on('close', (code) => {
        resolve({ stdout, stderr, code: code || 0 });
      });
      
      proc.on('error', (err) => {
        reject(err);
      });
    });
  }
}

9. 总结

OpenCode 的架构设计体现了几个关键原则:

  1. 模块化:各组件职责清晰,Provider、Tool、Agent 等组件可独立扩展
  2. 抽象层:通过接口抽象,支持多种 LLM 提供商(Anthropic、OpenAI、Ollama 等)
  3. 类型安全:使用 TypeScript 和 Zod 确保类型安全和参数验证
  4. 安全性:内置权限控制和沙箱机制,保护用户系统
  5. 可扩展:支持自定义 Provider、Tool 和权限规则
  6. 会话管理:完整的会话持久化和历史管理

这种架构使得 OpenCode 既可以作为独立的 CLI 工具使用,也可以作为库嵌入到其他应用中。通过理解其架构,你可以更好地定制和扩展 OpenCode,满足特定的开发需求。