OpenClaw Gateway 插件开发实战:从零构建企业级 AI 插件

一、为什么需要 Gateway 插件?

OpenClaw Gateway 是 Agent 架构中的”交通枢纽”——它负责请求路由、鉴权、限流、日志等横切关注点。但在实际生产中,企业往往需要:

  • 自定义认证:对接内部 OAuth2/LDAP 而非默认的 API Key
  • 数据转换:在 Agent 调用外部 API 前后做格式适配
  • 审计日志:将每一次 Agent 交互写入公司合规系统
  • 动态限流:基于团队配额、API 成本做精细化管控

这些需求无法通过配置「开箱即用」,而是需要插件化扩展。OpenClaw Gateway 的插件系统为此而生。

本文将带你完整走通一个 Elasticsearch 审计日志插件的开发全流程。

二、插件系统架构

2.1 生命周期钩子

OpenClaw Gateway 插件围绕一系列钩子(Hooks)展开,常见的有:

钩子触发时机典型用途
onRequest请求进入 Gateway 时(鉴权前)自定义认证、请求改写
onBeforeAgent请求路由到 Agent 前注入上下文、加载记忆
onAfterAgentAgent 返回响应后响应转换、结果缓存
onResponse响应返回客户端前审计日志、数据脱敏
onError任意环节抛出异常错误记录、自定义错误响应

2.2 插件注册流程

请求 → Gateway → [onRequest] → [onBeforeAgent] → Agent
                                                        ↓
客户端 ← [onResponse] ← [onAfterAgent] ←──────────────响应

三、实战:构建审计日志插件

3.1 项目结构

openclaw-audit-plugin/
├── package.json
├── src/
│   ├── index.ts          # 插件入口
│   ├── es-client.ts      # Elasticsearch 客户端
│   └── types.ts          # 类型定义
└── README.md

3.2 插件入口文件

// src/index.ts
import { GatewayPlugin, PluginContext } from '@openclaw/gateway';

interface AuditPluginConfig {
  esNodes: string[];
  indexPrefix: string;
  flushIntervalMs: number;
}

export default class AuditPlugin implements GatewayPlugin {
  name = 'audit-log';
  private config: AuditPluginConfig;
  private buffer: AuditEntry[] = [];
  private flushTimer: NodeJS.Timeout | null = null;

  constructor(config: AuditPluginConfig) {
    this.config = {
      esNodes: config.esNodes,
      indexPrefix: config.indexPrefix || 'openclaw-audit',
      flushIntervalMs: config.flushIntervalMs || 5_000,
    };
    this.initEsClient();
  }

  private async flush(): Promise<void> {
    if (this.buffer.length === 0) return;
    const batch = this.buffer.splice(0);
    try {
      await this.bulkInsert(batch);
    } catch (err) {
      console.error('[AuditPlugin] flush failed:', err);
      this.buffer.unshift(...batch);
    }
  }

  async onStart(ctx: PluginContext): Promise<void> {
    this.flushTimer = setInterval(() => this.flush(), this.config.flushIntervalMs);
    ctx.logger.info('AuditPlugin initialized');
  }

  async onStop(): Promise<void> {
    if (this.flushTimer) clearInterval(this.flushTimer);
    await this.flush();
  }

  async onRequest(ctx: PluginContext): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      phase: 'request',
      requestId: ctx.request.id,
      method: ctx.request.method,
      path: ctx.request.path,
      headers: this.sanitizeHeaders(ctx.request.headers),
    });
  }

  async onAfterAgent(ctx: PluginContext): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      phase: 'agent_response',
      requestId: ctx.request.id,
      agentName: ctx.agent?.name,
      tokenUsage: ctx.usage,
      durationMs: Date.now() - ctx.request.startTime,
    });
  }

  async onError(ctx: PluginContext, error: Error): Promise<void> {
    this.buffer.push({
      timestamp: new Date().toISOString(),
      phase: 'error',
      requestId: ctx.request.id,
      errorMessage: error.message,
      stack: error.stack,
    });
  }

  private sanitizeHeaders(headers: Record<string, string>): Record<string, string> {
    const sensitive = ['authorization', 'x-api-key', 'cookie'];
    const sanitized = { ...headers };
    for (const key of sensitive) {
      if (sanitized[key]) sanitized[key] = '***';
    }
    return sanitized;
  }
}

3.3 插件注册配置

# openclaw.yaml
plugins:
  - name: audit-log
    package: "./openclaw-audit-plugin"
    enabled: true
    config:
      esNodes:
        - "http://es01:9200"
        - "http://es02:9200"
      indexPrefix: "openclaw-audit"
      flushIntervalMs: 3000

3.4 启用插件

openclaw gateway start --config openclaw.yaml --plugins audit-log

# 或通过环境变量
OPENCLAW_PLUGINS=audit-log openclaw gateway start

四、插件开发最佳实践

4.1 错误隔离

插件抛出的异常不应导致 Gateway 主进程崩溃:

// 建议:用 try-catch 包裹插件回调
safeInvoke(plugin, 'onRequest', ctx);

// 或 Gateway 内部统一 catch
for (const hook of hooks) {
  try {
    await hook(ctx);
  } catch (err) {
    logger.error('Plugin failed:', err);
  }
}

4.2 性能考量

  • DB/网络操作必须异步,避免阻塞事件循环
  • onRequest 每次请求都触发,避免在此处执行重操作
  • 参考 Buffer + Flush 模式,减少 IO 次数
  • 为外部请求设置超时,默认 5s

4.3 配置验证

async onStart(ctx: PluginContext): Promise<void> {
  if (!this.config.esNodes?.length) {
    throw new Error('esNodes is required');
  }
  const health = await this.esClient.ping();
  if (!health) throw new Error('cannot connect to ES');
}

4.4 内存安全

const MAX_BUFFER_SIZE = 10_000;

private pushEntry(entry: AuditEntry): void {
  if (this.buffer.length >= MAX_BUFFER_SIZE) {
    this.flush().catch(() => {
      this.buffer.splice(0, Math.floor(MAX_BUFFER_SIZE * 0.1));
    });
  }
  this.buffer.push(entry);
}

五、测试你的插件

5.1 单元测试

import AuditPlugin from '../src';

describe('AuditPlugin', () => {
  let plugin: AuditPlugin;

  beforeEach(() => {
    plugin = new AuditPlugin({ esNodes: ['http://localhost:9200'], indexPrefix: 'test-audit' });
  });

  it('should buffer audit entries on request', async () => {
    const mockCtx = {
      request: { id: 'req-001', method: 'GET', path: '/api/chat', headers: { authorization: 'Bearer xxx' } },
    } as any;
    await plugin.onRequest(mockCtx);
    expect(plugin['buffer']).toHaveLength(1);
    expect(plugin['buffer'][0].headers.authorization).toBe('***');
  });
});

5.2 集成测试

import { GatewayTestHarness } from '@openclaw/gateway/testing';

it('should log full request lifecycle', async () => {
  const harness = new GatewayTestHarness({
    plugins: [{ name: 'audit-log', package: '../dist/index.js', config: { flushIntervalMs: 100 } }],
  });
  await harness.start();
  await harness.sendRequest({ method: 'POST', path: '/v1/chat', body: 'Hello' });
  await new Promise(r => setTimeout(r, 200));
  const logs = await harness.queryES('test-audit-*');
  expect(logs.length).toBeGreaterThanOrEqual(2);
  await harness.stop();
});

六、高级插件模式

6.1 链式插件

plugins:
  - name: auth          # 1. 先认证
  - name: rate-limit    # 2. 再限流
  - name: audit-log     # 3. 再审计
  - name: response-transform  # 4. 最后转换响应

6.2 动态热加载

openclaw gateway dev --plugin-watch

6.3 插件间数据共享

// auth 插件设置用户信息
async onRequest(ctx) {
  const user = await this.authenticate(ctx.request);
  ctx.pluginData.set('user', user);
}

// audit 插件读取
async onAfterAgent(ctx) {
  const user = ctx.pluginData.get('user');
  this.buffer.push({ user, ... });
}

七、调试与排障

症状原因解决
插件未加载package.json 中 main 字段错误检查入口文件导出
内存持续增长缓冲区未正确刷入检查 onStop 和 flush 逻辑
请求卡死插件中同步阻塞调用确保所有 IO 都是 async
插件间数据丢失插件执行顺序错误检查 yaml 中插件顺序

八、总结

通过本文的审计日志插件实战,你已掌握五大生命周期钩子的触发时机与用途、Buffer + Flush 高性能批量写入模式、错误隔离与配置验证等最佳实践,以及热加载、链式编排、数据共享等高级模式。

下一步可以探索的方向:集成 Prometheus 指标暴露插件、实现基于 Redis 的分布式限流插件、构建自定义词汇过滤或内容审核插件。

技术栈速查:开发语言 TypeScript / JavaScript,SDK @openclaw/gateway,注册方式 openclaw.yaml + –plugins,测试工具 @openclaw/gateway/testing,日志 ctx.logger(结构化),配置 constructor(config) + onStart 验证。