AggRoot
文档目录

插件开发

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() 被卸载时调用,清理资源

生命周期顺序:initializeactivatedeactivatedispose


创建工具插件

完整代码示例

以下示例创建一个简单的天气查询工具插件:

// 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 中导出 添加导出语句