npm package: @qiov/tsmai-api-openclaw
Description: HTTP API channel plugin for OpenClaw — exposes agent capabilities via REST API (sync + SSE streaming)
Latest version: 0.0.3

# tsmai-api-openclaw

OpenClaw 的 HTTP API 频道插件，通过 REST API 暴露 OpenClaw 上的 Agent 能力。

## 功能

- ✅通过标准 HTTP 接口调用 OpenClaw 上任意已注册的 Agent
- ✅支持同步调用（`/api/chat`）和 SSE 流式调用（`/api/chat/stream`）
- ✅支持多轮对话（通过 `conversationId` 维持上下文）
- ✅支持指定目标 Agent 或使用默认 main Agent
- ✅可选 API Key 认证
- ✅跨域（CORS）支持
- ✅接口返回额外信息（`meta` 字段）
    - 请求耗时（`durationMs`）
    - 当前使用的模型（`model`）
    - 输入输出的 token 消耗（`usage.inputTokens` / `outputTokens` / `totalTokens`）

## 目录结构

```
tsmai-api-openclaw/
├── index.ts               # 插件入口，注册 channel
├── package.json           # 包配置 & OpenClaw 插件声明
├── openclaw.plugin.json   # OpenClaw 插件元数据
├── README.md
└── src/
    ├── channel.ts         # ChannelPlugin 定义（配置、状态、网关启动）
    ├── config-schema.ts   # Zod 配置 schema
    ├── http-server.ts     # HTTP 服务核心（路由、同步/流式处理）
    ├── onboarding.ts      # 配置向导（openclaw onboard 时交互式配置）
    └── runtime.ts         # PluginRuntime 管理
```

## 安装

```bash
openclaw plugins install @qiov/tsmai-api-openclaw@latest
```

安装后插件会自动注册到 `openclaw.json`，只需添加 channel 配置即可：

```bash
# 编辑 ~/.openclaw/openclaw.json，在 channels 中添加：
"channels": {
    "api": { 
        "enabled": true, 
        "port": 3100, 
        "host": "0.0.0.0" 
    }
}

# 重启 gateway
openclaw gateway restart
```

## 配置

在 `~/.openclaw/openclaw.json` 中配置：

```json
{
  "channels": {
    "api": {
      "enabled": true,
      "port": 3100,
      "host": "0.0.0.0",
      "apiKey": ""
    }
  },
  "plugins": {
    "entries": {
      "tsmai-api-openclaw": {
        "enabled": true
      }
    }
  }
}
```

| 配置项 | 类型 | 默认值 | 说明 |
|--------|------|--------|------|
| `enabled` | boolean | `true` | 是否启用 |
| `port` | number | `3100` | HTTP 服务监听端口 |
| `host` | string | `0.0.0.0` | HTTP 服务监听地址 |
| `apiKey` | string | `""` | API Key（为空则不启用认证） |

也支持通过环境变量配置：`API_OPENCLAW_PORT`、`API_OPENCLAW_HOST`、`API_OPENCLAW_API_KEY`。

## API 接口

### 健康检查

```
GET /health
```

**响应**：

```json
{ "ok": true, "status": "running" }
```

---

### 同步调用

```
POST /api/chat
Content-Type: application/json
```

等待 Agent 处理完成后返回完整回复，适用于简单快速的查询场景。

**请求体**：

```json
{
  "prompt": "分析一下最近的服务器告警",
  "agent": "ops-lead",
  "conversationId": "session-123"
}
```

| 字段 | 必填 | 类型 | 说明 |
|------|------|------|------|
| `prompt` | 是 | string | 用户提示词 |
| `agent` | 否 | string | 目标 Agent ID，不传则使用 main agent |
| `conversationId` | 否 | string | 会话 ID，用于多轮对话，不传自动生成 |

**成功响应**：

```json
{
  "ok": true,
  "text": "根据分析，最近的告警主要集中在...",
  "conversationId": "session-123",
  "meta": {
    "durationMs": 3456,
    "model": "claude-sonnet-4-20250514",
    "usage": {
      "inputTokens": 1250,
      "outputTokens": 680,
      "totalTokens": 1930
    }
  }
}
```

| 字段 | 类型 | 说明 |
|------|------|------|
| `meta.durationMs` | number | 请求总耗时（毫秒） |
| `meta.model` | string? | 本次使用的模型名称（取决于 Agent 配置） |
| `meta.usage.inputTokens` | number? | 输入 token 消耗 |
| `meta.usage.outputTokens` | number? | 输出 token 消耗 |
| `meta.usage.totalTokens` | number? | 总 token 消耗 |

> **注意**：`model` 和 `usage` 字段取决于底层 LLM 提供商是否返回这些信息，部分场景下可能为空。

**错误响应**：

```json
{
  "ok": false,
  "error": "Missing required field: prompt"
}
```

**curl 示例**：

```bash
# 使用默认 main agent
curl -X POST http://localhost:3100/api/chat \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "你好"}'

# 指定 agent
curl -X POST http://localhost:3100/api/chat \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "分析告警", "agent": "ops-lead"}'

# 多轮对话
curl -X POST http://localhost:3100/api/chat \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "继续分析", "agent": "ops-lead", "conversationId": "session-123"}'
```

---

### SSE 流式调用

```
POST /api/chat/stream
Content-Type: application/json
```

以 Server-Sent Events (SSE) 格式实时推送 Agent 处理进度，适用于复杂任务、长时间处理的场景。

**请求体**：与 `/api/chat` 完全一致。

**响应**：`Content-Type: text/event-stream`

#### SSE 事件类型

| 事件 | 触发时机 | data 字段 |
|------|---------|----------|
| `start` | 请求被接受，开始处理 | `{ "conversationId": "...", "agent": "..." }` |
| `chunk` | AI 生成了一段增量文本 | `{ "text": "增量文本", "index": 0 }` |
| `tool` | 工具/技能调用完成 | `{ "name": "tool-name", "status": "done" }` |
| `error` | 处理出错 | `{ "error": "错误信息" }` |
| `done` | 处理完成 | `{ "text": "完整文本", "conversationId": "..." }` |

**响应示例**：

```
event: start
data: {"conversationId":"session-123","agent":"ops-lead"}

event: chunk
data: {"text":"我是","index":0}

event: chunk
data: {"text":" IT 运维","index":1}

event: chunk
data: {"text":"团队的运维主管","index":2}

event: done
data: {"text":"我是 IT 运维团队的运维主管...","conversationId":"session-123","meta":{"durationMs":3456,"model":"claude-sonnet-4-20250514","usage":{"inputTokens":1250,"outputTokens":680,"totalTokens":1930}}}
```

**curl 示例**：

```bash
curl -N -X POST http://localhost:3100/api/chat/stream \
  -H 'Content-Type: application/json' \
  -d '{"prompt": "用3段话介绍你自己", "agent": "ops-lead"}'
```

**JavaScript 客户端示例**：

```javascript
async function chatStream(prompt, agent) {
  const resp = await fetch('http://localhost:3100/api/chat/stream', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ prompt, agent }),
  });

  const reader = resp.body.getReader();
  const decoder = new TextDecoder();
  let buffer = '';

  while (true) {
    const { done, value } = await reader.read();
    if (done) break;

    buffer += decoder.decode(value, { stream: true });
    const lines = buffer.split('\n');
    buffer = lines.pop() || '';

    let eventType = '';
    for (const line of lines) {
      if (line.startsWith('event: ')) {
        eventType = line.slice(7);
      } else if (line.startsWith('data: ')) {
        const data = JSON.parse(line.slice(6));
        switch (eventType) {
          case 'start':
            console.log('Started:', data.agent);
            break;
          case 'chunk':
            process.stdout.write(data.text);
            break;
          case 'done':
            console.log('\n\nDone. Full text:', data.text.length, 'chars');
            break;
          case 'error':
            console.error('Error:', data.error);
            break;
        }
      }
    }
  }
}

chatStream('介绍你自己', 'ops-lead');
```

---

### 认证

如果配置了 `apiKey`，所有 API 请求需要携带认证信息，支持两种方式：

```bash
# 方式一：X-API-Key header
curl -H 'X-API-Key: your-api-key' ...

# 方式二：Bearer token
curl -H 'Authorization: Bearer your-api-key' ...
```

未认证请求返回：

```json
{ "ok": false, "error": "Unauthorized: invalid API key" }
```

## Agent 路由

- **指定 `agent` 参数**：直接路由到对应的 Agent（需要在 `openclaw.json` 的 `agents.list` 中已注册）
- **不指定 `agent`**：使用 `main` Agent（OpenClaw 默认 Agent）

每个 Agent 可以独立配置不同的模型，例如：

```json
{
  "agents": {
    "list": [
      { "id": "ops-lead", "model": "tione/kimi-k2.5" },
      { "id": "ops-l1", "model": "minimax/MiniMax-M2.5" }
    ]
  }
}
```

## 错误码

| HTTP 状态码 | 说明 |
|-------------|------|
| 200 | 成功 |
| 400 | 请求参数错误（JSON 格式错误、缺少 prompt） |
| 401 | 认证失败（API Key 无效） |
| 404 | 路由不存在 |
| 500 | 服务端内部错误 |