插件开发
AggRoot 采用 DDD + 插件架构,所有工具和模型都通过插件系统实现。本章介绍如何开发自定义工具插件和模型插件。
插件架构概览
DDD 分层
AggRoot 的代码遵循领域驱动设计分层:
┌─────────────────────────────────┐
│ Interface Layer │ CLI / API 入口
├─────────────────────────────────┤
│ Application Layer │ 服务编排、用例
├──────────────────┬──────────────┤
│ Domain Layer │Infrastructure│ 核心业务逻辑 │ 外部依赖
│ (Aggregates / │ (Plugins / │ │ 持久化 / LLM
│ Values / │ Persistence │ │
│ Repos) │ / External) │ │
└──────────────────┴──────────────┘
插件属于基础设施层,通过扩展点(Extension Points)与领域层交互。
热插拔
AggRoot 的插件支持热插拔——可以在运行时添加或移除插件,无需重启应用。
插件生命周期
每个插件必须实现以下生命周期方法:
| 阶段 | 方法 | 说明 |
|---|---|---|
| 初始化 | initialize(context) |
加载后调用,初始化内部状态,注册工具/模型 |
| 激活 | activate() |
被启用时调用,注册扩展点 |
| 停用 | deactivate() |
被禁用时调用,注销扩展点 |
| 销毁 | dispose() |
被卸载时调用,清理资源 |
生命周期顺序:initialize → activate → deactivate → dispose
创建工具插件
完整代码示例
以下示例创建一个简单的天气查询工具插件:
// src/plugins/weather-tools/index.ts
import type { Plugin, PluginContext, ToolExtension } from '../../infrastructure/plugins/types.js';
import type { ToolResult, JSONSchema } from '../../shared/types/index.js';
import { createLogger } from '../../utils/logger.js';
const logger = createLogger('weather-tools-plugin');
const PLUGIN_ID = '@aggroot/weather-tools';
/**
* 天气查询工具实现
*/
async function executeWeatherQuery(
args: Record<string, unknown>
): Promise<ToolResult> {
const city = args.city as string;
const unit = (args.unit as string) || 'celsius';
try {
// 调用天气 API
const response = await fetch(
`https://api.weather.com/v1?city=${encodeURIComponent(city)}&unit=${unit}`
);
const data = await response.json();
return {
output: `天气查询结果:${city}\n` +
`温度:${data.temperature}°${unit === 'celsius' ? 'C' : 'F'}\n` +
`天气:${data.condition}\n` +
`湿度:${data.humidity}%`,
success: true,
};
} catch (error) {
return {
output: `天气查询失败:${String(error)}`,
success: false,
};
}
}
/**
* 天气工具插件
*/
export class WeatherToolsPlugin implements Plugin {
id = PLUGIN_ID;
name = 'Weather Tools';
version = '1.0.0';
description = '天气查询工具插件';
manifest = {
id: PLUGIN_ID,
name: 'Weather Tools',
version: '1.0.0',
description: 'Weather query tools',
main: 'index.js',
};
async initialize(context: PluginContext): Promise<void> {
logger.info('Initializing weather tools plugin...');
// 定义工具参数 Schema
const parameters: JSONSchema = {
type: 'object',
properties: {
city: {
type: 'string',
description: '城市名称',
},
unit: {
type: 'string',
enum: ['celsius', 'fahrenheit'],
description: '温度单位(默认 celsius)',
},
},
required: ['city'],
};
// 注册工具
context.tools.register({
id: 'weather_query',
type: 'tool',
pluginId: PLUGIN_ID,
name: 'WeatherQuery',
description: '查询指定城市的天气信息',
parameters,
riskLevel: 'low', // 风险等级
requiresConfirmation: false, // 是否需要用户确认
execute: executeWeatherQuery,
isConcurrencySafe: () => true, // 是否并发安全
});
logger.info('Weather tools plugin initialized');
}
async activate(): Promise<void> {
logger.info('Weather tools plugin activated');
}
async deactivate(): Promise<void> {
logger.info('Weather tools plugin deactivated');
}
async dispose(): Promise<void> {
logger.info('Weather tools plugin disposed');
}
}
export default WeatherToolsPlugin;
工具参数详解
JSONSchema 参数定义
工具的参数使用 JSON Schema 描述,支持以下类型:
const parameters: JSONSchema = {
type: 'object',
properties: {
// 字符串参数
filePath: {
type: 'string',
description: '文件路径',
},
// 枚举参数
mode: {
type: 'string',
enum: ['quick', 'standard', 'deep'],
description: '分析模式',
},
// 数值参数
timeout: {
type: 'number',
description: '超时时间(毫秒)',
},
// 布尔参数
verbose: {
type: 'boolean',
description: '是否输出详细日志',
},
// 数组参数
targets: {
type: 'array',
items: { type: 'string' },
description: '目标文件列表',
},
},
required: ['filePath'],
};
riskLevel — 风险等级
每个工具必须声明风险等级,决定是否需要用户确认:
| 等级 | 说明 | 示例 |
|---|---|---|
low |
低风险,无需确认 | 读取文件、搜索代码 |
medium |
中等风险,建议确认 | 写入文件、编辑代码 |
high |
高风险,必须确认 | 删除文件、执行 Shell 命令 |
requiresConfirmation — 是否需要确认
true— 执行前必须获得用户确认false— 可自动执行(YOLO 模式下自动跳过)
isConcurrencySafe — 并发安全性
() => true— 该工具可以与其他工具并行执行() => false— 该工具必须独占执行
文件读取工具通常是并发安全的,而文件写入工具通常不是。
创建模型插件
模型插件为 AggRoot 接入新的 AI 模型提供商。需要实现 ModelClient 接口。
完整代码示例
// src/plugins/custom-model/index.ts
import type { Plugin, PluginContext, ModelExtension, ModelClient } from '../../infrastructure/plugins/types.js';
import type { ModelInfo, Message } from '../../shared/types/index.js';
import { createLogger } from '../../utils/logger.js';
const logger = createLogger('custom-model-plugin');
const PLUGIN_ID = '@aggroot/custom-model';
/**
* 自定义模型客户端实现
*/
class CustomModelClient implements ModelClient {
readonly modelId = 'custom-model-v1';
readonly provider = 'custom';
async chat(
messages: Message[],
options?: { temperature?: number; maxTokens?: number }
): Promise<string> {
logger.debug({ messageCount: messages.length }, 'Sending chat request');
// 调用自定义模型 API
const response = await fetch('https://api.custom-model.com/v1/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CUSTOM_MODEL_API_KEY}`,
},
body: JSON.stringify({
messages: messages.map(m => ({
role: m.role,
content: m.content,
})),
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 4096,
}),
});
const data = await response.json();
return data.choices[0].message.content;
}
async *chatStream(
messages: Message[],
options?: { temperature?: number; maxTokens?: number }
): AsyncGenerator<string> {
// 流式响应实现
const response = await fetch('https://api.custom-model.com/v1/chat/stream', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.CUSTOM_MODEL_API_KEY}`,
},
body: JSON.stringify({
messages: messages.map(m => ({
role: m.role,
content: m.content,
})),
temperature: options?.temperature ?? 0.7,
max_tokens: options?.maxTokens ?? 4096,
stream: true,
}),
});
const reader = response.body!.getReader();
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(l => l.startsWith('data: '));
for (const line of lines) {
const data = line.slice(6);
if (data === '[DONE]') return;
try {
const parsed = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) yield content;
} catch {
// 忽略解析错误
}
}
}
}
getModelInfo(): ModelInfo {
return {
id: this.modelId,
name: 'Custom Model v1',
provider: this.provider,
maxTokens: 8192,
supportsTools: true,
supportsVision: false,
};
}
}
/**
* 自定义模型插件
*/
export class CustomModelPlugin implements Plugin {
id = PLUGIN_ID;
name = 'Custom Model';
version = '1.0.0';
description = '自定义 AI 模型插件';
manifest = {
id: PLUGIN_ID,
name: 'Custom Model',
version: '1.0.0',
description: 'Custom AI model integration',
main: 'index.js',
};
private client = new CustomModelClient();
async initialize(context: PluginContext): Promise<void> {
logger.info('Initializing custom model plugin...');
// 注册模型扩展
context.models.register({
id: 'custom-model-v1',
type: 'model',
pluginId: PLUGIN_ID,
name: 'Custom Model v1',
description: '自定义 AI 模型',
provider: 'custom',
modelId: 'custom-model-v1',
client: this.client,
});
logger.info('Custom model plugin initialized');
}
async activate(): Promise<void> {
logger.info('Custom model plugin activated');
}
async deactivate(): Promise<void> {
logger.info('Custom model plugin deactivated');
}
async dispose(): Promise<void> {
logger.info('Custom model plugin disposed');
}
}
export default CustomModelPlugin;
注册插件
在插件注册表中注册
将插件添加到 src/plugins/index.ts:
export { WeatherToolsPlugin } from './weather-tools/index.js';
export { CustomModelPlugin } from './custom-model/index.js';
在代理配置中启用工具
创建或修改代理 JSON 配置文件,将工具添加到 tools 列表:
{
"name": "my-agent",
"tools": [
"Read", "Write", "Edit", "Glob", "Grep",
"WeatherQuery"
]
}
插件文件结构
每个插件应遵循以下目录结构:
plugin-name/
├── index.ts # 插件入口,导出插件类
├── tool-definitions.ts # 工具定义(工具插件)
├── model-definitions.ts # 模型定义(模型插件)
├── implementations/ # 具体实现
│ ├── tool1.ts # 单个工具实现
│ ├── tool2.ts # 另一个工具实现
│ └── index.ts # 导出所有实现
└── schemas/ # 验证 Schema
调试插件
日志
使用 createLogger 创建插件专用日志:
const logger = createLogger('my-plugin');
logger.info('插件已初始化');
logger.debug({ toolId: 'my-tool' }, '工具已注册');
logger.warn('需要注意的情况');
logger.error({ error: String(err) }, '操作失败');
日志级别通过 LOG_LEVEL 环境变量控制。
常见问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 工具未注册 | initialize 中未调用 context.tools.register |
确认注册代码在 initialize 方法中 |
| 工具调用失败 | execute 函数抛出异常 |
在 execute 中使用 try-catch,返回 ToolResult |
| 模型不可选 | 未注册到 context.models |
确认 ModelClient 实现完整 |
| 插件未加载 | 未在 src/plugins/index.ts 中导出 |
添加导出语句 |