OpenCode 架构详解
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.md3. 核心组件详解
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 的架构设计体现了几个关键原则:
- 模块化:各组件职责清晰,Provider、Tool、Agent 等组件可独立扩展
- 抽象层:通过接口抽象,支持多种 LLM 提供商(Anthropic、OpenAI、Ollama 等)
- 类型安全:使用 TypeScript 和 Zod 确保类型安全和参数验证
- 安全性:内置权限控制和沙箱机制,保护用户系统
- 可扩展:支持自定义 Provider、Tool 和权限规则
- 会话管理:完整的会话持久化和历史管理
这种架构使得 OpenCode 既可以作为独立的 CLI 工具使用,也可以作为库嵌入到其他应用中。通过理解其架构,你可以更好地定制和扩展 OpenCode,满足特定的开发需求。

