# @lark-apaas/nestjs-capability

用于加载和执行能力（插件实例）的 NestJS 模块。

## 安装

```bash
npm install @lark-apaas/nestjs-capability
```

## 概述

本模块提供以下功能：

- 从 `server/capabilities/` 目录加载能力配置
- 加载并实例化关联的插件
- 多种调用方式（Node 调用、Debug 调用、前端调用）
- 流式调用支持（SSE）
- 模板参数解析

## 快速开始

### 模块注册

```typescript
// app.module.ts
import { CapabilityModule } from '@lark-apaas/nestjs-capability';
import * as path from 'path';

@Module({
  imports: [
    CapabilityModule.forRoot({
      capabilitiesDir: path.join(__dirname, '../capabilities'),
    }),
  ],
})
export class AppModule {}
```

### Node 调用

```typescript
import { Injectable } from '@nestjs/common';
import { CapabilityService } from '@lark-apaas/nestjs-capability';

@Injectable()
export class TaskService {
  constructor(private readonly capabilityService: CapabilityService) {}

  async onTaskCreated(task: Task) {
    const result = await this.capabilityService
      .load('create_feishu_group_for_task')
      .call('run', {
        group_name: `任务群 - ${task.name}`,
        creator_id: task.creatorId,
      });

    return result;
  }
}
```

### 流式调用

用于 LLM 对话等流式输出场景：

```typescript
@Injectable()
export class ChatService {
  constructor(private readonly capabilityService: CapabilityService) {}

  async *chat(message: string): AsyncIterable<{ content: string }> {
    const stream = this.capabilityService
      .load('ai_chat')
      .callStream('chat', { message });

    for await (const chunk of stream) {
      yield chunk as { content: string };
    }
  }
}
```

普通调用会自动聚合流式结果：

```typescript
// 即使是流式 action，call() 也会返回聚合后的完整结果
const result = await this.capabilityService
  .load('ai_chat')
  .call('chat', { message: 'hello' });
```

### 上下文覆盖

```typescript
// 覆盖 userContext
const result = await this.capabilityService
  .load('notify_task_created')
  .call('run', inputParams, {
    userContext: {
      userId: 'custom-user-id',
      tenantId: 'custom-tenant-id',
      appId: 'custom-app-id',
      isSystemAccount: false,
    },
  });

// 设置调试模式（插件可据此返回更详细的错误信息）
const debugResult = await this.capabilityService
  .load('ai_chat')
  .call('chat', inputParams, {
    isDebug: true,
  });
```

## 能力配置

能力配置是位于 `server/capabilities/` 目录下的 JSON 文件：

```json
{
  "id": "create_feishu_group_for_task",
  "pluginID": "@official-components/feishu-group-create",
  "pluginVersion": "1.0.5",
  "name": "任务创建时自动创建飞书群组",
  "description": "在任务创建时，根据任务名称自动生成飞书群组",
  "paramsSchema": {
    "type": "object",
    "properties": {
      "group_name": { "type": "string" }
    }
  },
  "formValue": {
    "group_name": "{{input.group_name}}",
    "members": ["123"]
  },
  "createdAt": 1764234360374,
  "updatedAt": 1764234360374
}
```

### 模板语法

`formValue` 字段支持模板占位符：

- `{{input.xxx}}` - 引用用户输入参数
- `{{input.user.name}}` - 支持嵌套路径

## HTTP API

### 前端调用

```
POST /api/capability/:capability_id

请求体：
{
  "action": "run",
  "params": {
    "group_name": "项目A群"
  }
}

响应：
{
  "code": 0,
  "message": "success",
  "data": { ... }
}
```

### 前端流式调用

```
POST /api/capability/:capability_id/stream

请求体：
{
  "action": "chat",
  "params": {
    "message": "hello"
  }
}

响应（SSE 格式）：
data: {"content":"Hello"}
data: {"content":" World"}
data: [DONE]
```

### Debug 调用（仅开发环境）

```
POST /__innerapi__/capability/debug/:capability_id

请求体（所有字段可选）：
{
  "action": "run",              // 可选，默认使用插件第一个 action
  "params": {                   // 可选，用户输入参数
    "group_name": "测试群"
  },
  "capability": {               // 可选，完整的 capability 配置（优先使用）
    "id": "test",
    "pluginID": "@test/plugin",
    "pluginVersion": "1.0.0",
    "name": "测试",
    "formValue": {}
  }
}

响应：
{
  "code": 0,
  "message": "success",
  "data": { ... },
  "debug": {
    "capabilityConfig": { ... },
    "resolvedParams": { ... },
    "duration": 123,
    "pluginID": "@xxx/plugin",
    "action": "run"
  }
}
```

### Debug 流式调用（仅开发环境）

```
POST /__innerapi__/capability/debug/:capability_id/stream

请求体（所有字段可选）：
{
  "action": "chat",
  "params": { "message": "hello" },
  "capability": { ... }
}

响应（SSE 格式）：
data: {"content":"Hello"}
data: {"content":" World"}
data: [DONE]
```

### 列出所有能力（仅开发环境）

```
GET /__innerapi__/capability/list

响应：
{
  "code": 0,
  "message": "success",
  "data": [
    {
      "id": "create_feishu_group_for_task",
      "name": "任务创建时自动创建飞书群组",
      "pluginID": "@official-components/feishu-group-create",
      "pluginVersion": "1.0.5"
    }
  ]
}
```

## 服务

### CapabilityService

用于加载和执行能力的核心服务。

```typescript
interface CapabilityService {
  // 列出所有已加载的能力
  listCapabilities(): CapabilityConfig[];

  // 获取指定能力
  getCapability(capabilityId: string): CapabilityConfig | null;

  // 加载能力并返回执行器
  load(capabilityId: string): CapabilityExecutor;

  // 使用传入的配置加载能力执行器（用于 debug 场景）
  loadWithConfig(config: CapabilityConfig): CapabilityExecutor;

  // 设置自定义能力目录
  setCapabilitiesDir(dir: string): void;
}

interface CapabilityExecutor {
  // 调用能力（流式 action 会自动聚合结果）
  call(
    actionName: string,
    input: unknown,
    context?: Partial<PluginActionContext>
  ): Promise<unknown>;

  // 流式调用能力（返回 AsyncIterable）
  callStream(
    actionName: string,
    input: unknown,
    context?: Partial<PluginActionContext>
  ): AsyncIterable<unknown>;

  // 检查 action 是否为流式
  isStream(actionName: string): Promise<boolean>;
}
```

### PluginLoaderService

用于加载和缓存插件实例的服务。

```typescript
interface PluginLoaderService {
  // 加载插件并创建实例
  // - 对于 refType: "config" 的插件，每次调用都创建新实例
  // - 对于 refType: "action" 的插件，使用缓存
  loadPlugin(pluginID: string, config?: unknown): PluginInstance;

  // 检查插件是否已安装
  isPluginInstalled(pluginID: string): boolean;

  // 读取插件的 manifest.json
  getManifest(pluginID: string): PluginManifest | null;

  // 检查插件是否需要 config（form.refType === 'config'）
  isConfigRequired(pluginID: string): boolean;

  // 清除插件缓存（包括 manifest 缓存）
  clearCache(pluginID?: string): void;
}

interface PluginManifest {
  name: string;
  form?: {
    refType?: 'config' | 'action';
    schema?: unknown;
  };
  [key: string]: unknown;
}
```

#### form.refType 说明

插件的 `manifest.json` 中可以通过 `form.refType` 字段指定 `formValue` 的消费方式：

| refType | 行为 | 缓存策略 |
|---------|------|----------|
| `"config"` | `formValue` 作为 config 传给 `create()` 函数 | 不缓存实例，每次重新创建 |
| `"action"` 或不设置 | `formValue` 通过模板引擎解析后作为 action 参数 | 缓存实例 |

**manifest.json 示例：**

```json
{
  "name": "@test/feishu-bitable",
  "form": {
    "refType": "config",
    "schema": { ... }
  }
}
```

### TemplateEngineService

用于解析 formValue 模板的服务。

```typescript
interface TemplateEngineService {
  resolve(
    template: Record<string, unknown>,
    input: Record<string, unknown>
  ): Record<string, unknown>;
}
```

## 接口定义

### CapabilityConfig

```typescript
interface CapabilityConfig {
  id: string;           // 能力唯一标识
  pluginID: string;     // 关联的插件 ID
  pluginVersion: string; // 关联的插件版本
  name: string;         // 能力名称
  description: string;  // 能力描述
  paramsSchema: JSONSchema; // 输入参数 JSON Schema
  formValue: Record<string, unknown>; // 参数映射模板
  createdAt: number;    // 创建时间戳
  updatedAt: number;    // 更新时间戳
}
```

### PluginInstance

```typescript
interface PluginInstance {
  // 执行 action（必需）
  run(actionName: string, context: PluginActionContext, input: unknown): Promise<unknown>;

  // 流式执行 action（可选）
  runStream?(actionName: string, context: PluginActionContext, input: unknown): AsyncIterable<unknown>;

  // 检查是否为流式 action（可选）
  isStreamAction?(actionName: string): boolean;

  // 聚合流式结果（可选，默认返回 chunks 数组）
  aggregate?(actionName: string, chunks: unknown[]): unknown;

  // Action 相关
  hasAction(actionName: string): boolean;
  listActions(): string[];
  getActionSchema(actionName: string): ActionSchema | null;
  getInputSchema(actionName: string, config?: unknown): ZodSchema | undefined;
  getOutputSchema(actionName: string, input: unknown, config?: unknown): ZodSchema | undefined;
}
```

### PluginActionContext

```typescript
interface PluginActionContext {
  pluginKey: string;        // 插件标识（如 @official/ai-chat）
  logger: Logger;           // 日志记录器
  platformHttpClient: PlatformHttpClient; // HTTP 客户端（自动携带 x-plugin-key header）
  userContext: {
    userId: string;         // 用户 ID
    tenantId: string;       // 租户 ID
    appId: string;          // 应用 ID
    isSystemAccount: boolean; // 是否为系统账号
  };
  isDebug: boolean;         // 是否为调试模式
}
```

**pluginKey 说明：**
- 每次插件调用都会传入当前插件的标识
- 插件的 HttpClient 会自动在请求头中携带 `x-plugin-key` header
- 用于计费追踪、日志关联等场景

**isDebug 说明：**
- `DebugController` 调用时 `isDebug = true`
- `WebhookController` 或其他调用时 `isDebug = false`
- 插件可据此返回更详细的错误信息、调试日志等

## 错误类型

| 错误 | 描述 |
|------|------|
| `CapabilityNotFoundError` | 指定 ID 的能力不存在 |
| `PluginNotFoundError` | 插件未安装 |
| `PluginLoadError` | 插件加载或初始化失败 |
| `ActionNotFoundError` | 插件中不存在指定的 Action |

## 模块配置

```typescript
interface CapabilityModuleOptions {
  // 能力配置目录路径（默认：server/capabilities）
  capabilitiesDir?: string;
}
```

## 依赖

- `@lark-apaas/nestjs-common` - 提供 RequestContextService 和 PLATFORM_HTTP_CLIENT
- `@nestjs/common` - NestJS 框架

## 许可证

MIT
