Skip to content

中间件

AI Adapter 提供 Koa 风格的中间件系统,允许你在处理管道中插入自定义逻辑。

基本概念

中间件是一个 async (ctx, next) => { ... } 函数,在 adapter.process() 执行时被依次调用。

2.0.3 开始,AI Adapter 会先解析自然语言,再进入中间件管道。因此中间件中的 ctx.command 已经是结构化命令,可以在执行任何 query/action/flow 之前做权限判断、审计或拦截。

2.0.3 开始,推荐使用 adapter.capability.register() 集中声明资源、权限、参数规则、返回字段和 handler,再用 adapter.capability.middleware() 统一拦截。

js
adapter.use(async (ctx, next) => {
  // 处理前
  console.log('输入:', ctx.input);

  await next(); // 继续执行下一个中间件或核心处理

  // 处理后
  console.log('结果:', ctx.result);
});

ctx 对象

每个中间件接收的 ctx 包含以下字段:

字段类型说明
inputstring用户原始输入
contextobject额外上下文(来自 process 第二个参数)
commandobject解析后的结构化命令,中间件执行前已填充
resultobject | null执行结果;中间件可设置它并跳过 next()
adapterAIAdapter当前 adapter 实例引用

使用场景

日志记录

js
adapter.use(async (ctx, next) => {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;
  console.log(`[${ctx.input}] → ${ctx.result?.engine || 'error'} (${ms}ms)`);
});

权限控制

js
adapter.use(async (ctx, next) => {
  if (ctx.command.engine === 'action' && ctx.command.type === 'delete' && !ctx.context.isAdmin) {
    ctx.result = {
      type: 'error',
      engine: 'auth',
      success: false,
      message: '❌ 需要管理员权限才能执行删除操作',
    };
    return; // 不调用 next(),阻止后续处理
  }
  await next();
});

输入预处理

js
adapter.use(async (ctx, next) => {
  // 去除敏感词
  ctx.input = ctx.input.replace(/密码/g, '***');
  await next();
});

结果转换

js
adapter.use(async (ctx, next) => {
  await next();

  // 为查询结果添加额外格式化
  if (ctx.result?.type === 'query' && ctx.result.result?.table) {
    ctx.result.result.formattedAt = new Date().toISOString();
  }
});

缓存层

js
const responseCache = new Map();

adapter.use(async (ctx, next) => {
  const key = ctx.input;
  if (responseCache.has(key)) {
    ctx.result = responseCache.get(key);
    return; // 命中缓存,跳过处理
  }

  await next();

  if (ctx.result?.type === 'query') {
    responseCache.set(key, ctx.result);
  }
});

中间件执行顺序

中间件按 use() 调用顺序依次执行,形成洋葱模型:

js
adapter.use(mw1); // 第一层
adapter.use(mw2); // 第二层
adapter.use(mw3); // 第三层

// 执行顺序:
// mw1 before → mw2 before → mw3 before → 核心处理
// → mw3 after → mw2 after → mw1 after

链式调用

use() 返回 this,支持链式调用:

js
adapter
  .use(loggingMiddleware)
  .use(authMiddleware)
  .use(cacheMiddleware);

提前终止

不调用 next() 即可提前终止处理管道,需手动设置 ctx.result

js
adapter.use(async (ctx, next) => {
  if (ctx.input.length < 2) {
    ctx.result = {
      type: 'error',
      engine: 'unknown',
      message: '输入太短,请提供更详细的指令。',
    };
    return; // 不调用 next()
  }
  await next();
});

内置中间件工厂

AI Adapter 提供三个开箱即用的中间件工厂,从 @kupola/ai-adapter 直接导入:

createRateLimiter

滑动窗口速率限制,防止 API 被滥用:

js
import { createRateLimiter } from '@kupola/ai-adapter';

// 每分钟最多 30 次请求
adapter.use(createRateLimiter({ maxRequests: 30, windowMs: 60000 }));

// 超限时返回:
// { type: 'error', message: 'Rate limit exceeded', retryAfter: 42000 }
选项默认值说明
maxRequests30窗口内最大请求数
windowMs60000窗口时间(毫秒)

createDevToolsLogger

自动记录每次 process 的耗时和结果到 window.__KUPOLA_AI_DEVTOOLS__

js
import { createDevToolsLogger } from '@kupola/ai-adapter';

adapter.use(createDevToolsLogger({ maxEntries: 100 }));

// 在浏览器控制台中检查:
// window.__KUPOLA_AI_DEVTOOLS__
// => [{ input, engine, duration, result, timestamp }, ...]

默认会脱敏 passwordtokensecretauthorizationaccessTokenrefreshToken 等字段。可通过 redactFields 覆盖:

js
adapter.use(createDevToolsLogger({
  maxEntries: 100,
  redactFields: ['password', 'token', 'apiKey'],
}));

createAuthGuard

基于角色和权限的命令拦截。它会在解析完成后、handler 执行前检查 ctx.command,可集中控制 query/action/flow:

js
import { createAuthGuard } from '@kupola/ai-adapter';

adapter.use(createAuthGuard({
  roleField: 'role',
  permissionsField: 'permissions',
  rules: [
    { engine: 'query', type: 'roles', roles: ['admin'], permissions: ['role:read'] },
    { engine: 'query', type: 'permissions', roles: ['admin'], permissions: ['permission:read'] },
    { engine: 'action', types: ['delete', 'create_user'], roles: ['admin'] },
    { engine: 'flow', type: 'execute', names: ['发工资条'], roles: ['admin', 'hr'] },
  ],
}));

// 调用时传入用户角色:
await adapter.process('查询所有角色', { role: 'user', permissions: [] });
// => { engine: 'auth-guard', code: 'PERMISSION_DENIED', message: '无权限...' }

也可以继续使用旧的简写配置:

js
adapter.use(createAuthGuard({
  restrictedTypes: ['delete'],
  restrictedQueries: ['roles', 'permissions'],
  restrictedFlows: ['发工资条'],
  allowedRoles: ['admin'],
}));
选项说明
restrictedTypes受限 action type,兼容旧用法
restrictedQueries受限 query type
restrictedFlows受限 flow name
rules集中权限规则,支持 enginetype/typesname/namesrolespermissionsmatch()
permissionsmap 写法,如 { 'query:roles': ['admin'] }
roleFieldctx.context 读取角色的字段,默认 role
permissionsFieldctx.context 读取权限码的字段,默认 permissions
message自定义无权限提示,支持函数

安全边界

createAuthGuard 是前端交互层的拦截和提示机制,不能替代后端鉴权。后端 API 必须对角色、用户、菜单、敏感数据查询做最终校验;前端被绕过时,服务端应返回 401403

capability.middleware

adapter.capability.middleware() 会根据已经注册的能力,在 handler 执行前做权限和参数校验。

js
adapter.capability.register({
  engine: 'query',
  type: 'roles',
  roles: ['admin'],
  permissions: ['role:read'],
  paramsSchema: { keyword: 'string' },
  resultFields: ['id', 'name', 'code'],
  handler: async (params, context) => api.get('/api/roles', params, context),
});

adapter.use(adapter.capability.middleware());
选项说明
stricttrue 时,未注册到 capability 的 command 会被拒绝

TIP

即使不挂载 capability.middleware(),通过 adapter.capability.register() 注册出来的 handler 仍会在执行时做权限、参数和结果过滤。中间件主要用于更早地给出拦截提示。

组合使用

js
import { createRateLimiter, createAuthGuard, createDevToolsLogger } from '@kupola/ai-adapter';

adapter
  .use(createDevToolsLogger())               // 第一层:记录所有请求
  .use(createRateLimiter({ maxRequests: 20 }))  // 第二层:速率限制
  .use(createAuthGuard({ restrictedTypes: ['delete'] })); // 第三层:权限检查

DevTools 快照

除了中间件日志,AIAdapter 还提供 getDevToolsSnapshot() 方法,返回完整运行时状态:

js
const snap = adapter.getDevToolsSnapshot();
// {
//   version: '2.0.3',
//   messages: [...],
//   middlewares: 3,
//   query: { registered: 5, history: 12, cache: 3 },
//   action: { registered: 4, undoStack: 2, audit: 18 },
//   capability: { registered: 9, ai: 7 },
//   flow: { defined: 3, executions: 7 },
//   events: ['input', 'parsed', 'result', 'flow:step', 'action:before', 'action:after'],
// }

下一步