# Infoflow OpenClaw Plugin

[中文](#中文) | [English](#english)

---
<a id="中文"></a>

百度如流 (Infoflow) 企业消息平台 — OpenClaw 频道插件。支持 Webhook 和 WebSocket 两种连接方式，具备图片消息收发、引用回复等能力。

## 目录

- [快速开始](#快速开始)
- [配置](#配置)
- [插件架构](#插件架构)
- [开发指南](#开发指南)
- [内置命令](#内置命令)
- [扩展：Agent Tools](#扩展-agent-tools)
- [扩展：Agent Hooks](#扩展-agent-hooks)
- [扩展：Agent Skills](#扩展-agent-skills)
- [FAQ](#faq)

---

<a id="快速开始"></a>

## 快速开始

> 5 步完成部署，从零到机器人上线回消息。

### 准备：在如流企业后台创建应用

1. 如流企业后台 → 应用管理 → 创建机器人应用
2. 记录以下 4 个参数：

   | 参数 | 在哪找 |
   |------|--------|
   | `appKey` | 应用基本信息 |
   | `appSecret` | 应用基本信息 |
   | `checkToken` | 消息推送 → 配置 |
   | `encodingAESKey` | 消息推送 → 配置 |

3. 连接方式二选一：
   - **WebSocket**（推荐，无需公网）：不用额外配置
   - **Webhook**：在"消息推送"填入 `https://你的域名/webhook/infoflow`

### 第一步：安装 OpenClaw

```bash
# macOS / Linux
curl -fsSL https://openclaw.dev/install.sh | bash

# 安装完成后登录（按提示操作）
openclaw configure
```

### 第二步：克隆插件

```bash
git clone ssh://icode.baidu.com:8235/baidu/hi/openclaw_infoflow
cd openclaw_infoflow
npm install
```

### 第三步：写配置

打开 `~/.openclaw/openclaw.json`，在 `channels` 下加入如流配置：

```json5
{
  "channels": {
    "infoflow": {
      "connectionMode": "webhook",
      "appSecret": "填你的 appSecret",
      "checkToken": "填你的 checkToken",
      "encodingAESKey": "填你的 encodingAESKey"
    }
  }
}
```

更多配置项（访问控制、群聊策略、消息格式等）见[配置参考](#配置)。

### 第四步：部署插件

```bash
./scripts/deploy.sh
```

### 第五步：验证

- **私聊**：在如流给机器人发一条消息，应有回复
- **群聊**：在白名单群 @机器人，应有回复
- **查日志**：

```bash
openclaw logs | grep infoflow
```

### 常见问题

**机器人不回复？**
```bash
openclaw doctor            # 检查配置
openclaw status            # 查看各渠道状态
openclaw logs | grep -i "infoflow\|error"
```

**改了配置没生效？**
```bash
./scripts/deploy.sh        # 重新同步并重启
```

**群 ID 怎么查？**
```bash
openclaw directory groups infoflow
```

---

<a id="配置"></a>

## 配置

### 最小配置

在 OpenClaw 配置文件中添加：

```json5
{
  channels: {
    infoflow: {
      apiHost: "https://api.im.baidu.com",
      checkToken: "your-check-token",
      encodingAESKey: "your-encoding-aes-key",
      appKey: "your-app-key",
      appSecret: "your-app-secret",
    },
  },
}
```

### 全量配置

```json5
{
  channels: {
    infoflow: {
      // ── 基础连接 ──────────────────────────────────────────
      enabled: true,
      apiHost: "https://api.im.baidu.com",          // 如流 API 地址（必填）
      connectionMode: "webhook",                       // "webhook"（默认）或 "websocket"
      wsGateway: "infoflow-open-gateway.baidu.com", // WebSocket 网关（websocket 模式专用）
      checkToken: "your-check-token",                   // 消息验签 Token（必填）
      encodingAESKey: "your-encoding-aes-key",          // 消息 AES 加密密钥（必填）
      appKey: "your-app-key",                           // 应用 Key（必填）
      appSecret: "your-app-secret",                     // 应用 Secret（必填）
      appAgentId: 12345,                                // 企业后台应用ID，私聊撤回需要
      robotName: "MyBot",                               // 机器人显示名，用于群内 @ 检测

      // ── 访问控制 ──────────────────────────────────────────
      dmPolicy: "allowlist",        // "open" / "pairing" / "allowlist"
      allowFrom: ["alice", "bob"],  // 私聊白名单（uuapName），dmPolicy="allowlist" 时生效
      groupPolicy: "allowlist",     // "open" / "allowlist" / "disabled"
      groupAllowFrom: ["12345678"], // 群聊白名单（群ID字符串），groupPolicy="allowlist" 时生效

      // ── 群聊回复行为 ───────────────────────────────────────
      replyMode: "mention-only",   // "ignore" / "record" / "mention-only"
      requireMention: false,          // 是否强制要求 @ 机器人才触发
      followUp: true,                 // 机器人回复后时间窗口内免 @ 追问
      followUpWindow: 300,            // 跟进时间窗口（秒）

      // ── 消息格式 ──────────────────────────────────────────
      dmMessageFormat: "markdown",    // 私聊格式："text"（默认）或 "markdown"
      groupMessageFormat: "text",     // 群聊格式："text"（默认）或 "markdown"（不支持引用回复）
      processingHint: true,           // 响应慢时发送"⏳ 处理中..."提示
      processingHintDelay: 5,         // 发送提示前等待秒数（0 = 立即发送）

      // ── 多账号 ────────────────────────────────────────────
      defaultAccount: "main",
      accounts: {
        main: { appKey: "...", appSecret: "..." },    // 字段与顶层相同，会合并覆盖
      },

      // ── 群级别覆盖 ────────────────────────────────────────
      groups: {
        "12345678": {
          replyMode: "mention-only",
          groupSessionMode: "user", // 此群按群+人拆分会话
          systemPrompt: "你是该群的专属助手，回答要简洁。",
        },
        "87654321": {
          replyMode: "record", // 此群仅记录不回复
          groupSessionMode: "group", // 此群按群拆分会话
        },
      },
    },
  },
}
```

### 连接方式

插件支持两种连接方式，通过 `connectionMode` 配置切换：

| 模式 | 说明 | 适用场景 |
|------|------|---------|
| `webhook`（默认） | 如流服务器主动推送 HTTP 请求到你的服务 | 有公网域名或内网穿透 |
| `websocket` | 插件主动连接如流 WebSocket 长连接 | 无公网域名，本地开发调试 |

**Webhook 模式**：在如流企业后台配置 Webhook URL：

```
https://your-domain/webhook/infoflow
```

**WebSocket 模式**：无需公网域名，适合本地开发：

```json5
{
  channels: {
    infoflow: {
      connectionMode: "websocket",
      // wsGateway: "infoflow-open-gateway.baidu.com",  // 可选，默认值    },
  },
}
```

> WebSocket 模式下 `checkToken` 和 `encodingAESKey` 不用于消息解密（连接本身已认证），但仍建议配置以便随时切回 Webhook。

### 配置参考

#### 基础连接

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:----:|--------|------|
| `apiHost` | `string` | ✅ | — | 如流 API 地址，例如 `https://api.im.baidu.com` |
| `checkToken` | `string` | — | — | 消息验签 Token，从如流企业后台获取（**webhook 模式必填**）|
| `encodingAESKey` | `string` | — | — | 消息 AES 加密密钥，从如流企业后台获取（**webhook 模式必填**）|
| `appKey` | `string` | ✅ | — | 应用 Key，从如流企业后台获取 |
| `appSecret` | `string` | ✅ | — | 应用 Secret，从如流企业后台获取 |
| `appAgentId` | `number` | — | — | 企业后台"应用ID"（数字），私聊消息撤回需要此字段 |
| `robotName` | `string` | — | — | 机器人在群里的显示名称，用于检测消息中的 @ 提及。**未配置时无法识别 @机器人，`mention-only` 模式将退化为对所有群消息触发** |
| `connectionMode` | `string` | — | `"webhook"` | 消息接收方式：`"webhook"` 如流主动推送；`"websocket"` 插件主动长连接 |
| `wsGateway` | `string` | — | `"infoflow-open-gateway.baidu.com"` | WebSocket 网关域名，仅 `websocket` 模式使用 |
| `enabled` | `boolean` | — | `true` | 是否启用该插件 |

#### 访问控制

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:----:|--------|------|
| `dmPolicy` | `string` | — | `"open"` | 私聊策略：`"open"` 任何人可触发；`"pairing"` 需配对授权；`"allowlist"` 仅白名单用户 |
| `allowFrom` | `string[]` | — | `[]` | 私聊白名单（uuapName / 邮箱前缀），`dmPolicy="allowlist"` 时生效 |
| `groupPolicy` | `string` | — | `"open"` | 群聊策略：`"open"` 任何群可触发；`"allowlist"` 仅白名单群；`"disabled"` 完全禁用群聊 |
| `groupAllowFrom` | `string[]` | — | `[]` | 群聊白名单（群 ID 数字字符串），`groupPolicy="allowlist"` 时生效 |
| `requireMention` | `boolean` | — | `false` | 群聊是否强制要求 @ 机器人才触发（与 `replyMode` 配合使用） |

#### 回复行为

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:----:|--------|------|
| `replyMode` | `string` | — | `"mention-only"` | 群聊回复策略：`ignore`（丢弃）/ `record`（仅记录）/ `mention-only`（仅被@时） |
| `groupSessionMode` | `string` | — | `"group"` | 群聊会话拆分模式：`"group"` = 按群拆分；`"user"` = 按群+人拆分 |
| `followUp` | `boolean` | — | `true` | 机器人回复后，时间窗口内自动识别追问，无需再次 @ |
| `followUpWindow` | `number` | — | `300` | 跟进时间窗口（秒） |

#### 消息格式

| 字段 | 类型 | 必填 | 默认值 | 说明 |
|------|------|:----:|--------|------|
| `dmMessageFormat` | `string` | — | `"text"` | 私聊发出消息的格式：`"text"` 纯文本；`"markdown"` 富文本，支持标题/加粗/列表 |
| `groupMessageFormat` | `string` | — | `"text"` | 群聊发出消息的格式：`"text"` 纯文本；`"markdown"` 富文本。**注意：`markdown` 格式不支持引用回复** |
| `textChunkLimit` | `number` | — | `1800` | 长消息自动分片的每片最大字符数，超出部分拆成多条消息发送 |
| `processingHint` | `boolean` | — | `true` | LLM 响应较慢时，提前发送"⏳ 处理中..."提示；出错时发"处理出错，请稍后重试" |
| `processingHintDelay` | `number` | — | `5` | 发送"⏳ 处理中..."前等待的秒数；LLM 在此时间内响应则不发；设为 `0` 立即发送 |

#### 多账号与分组

| 字段 | 类型 | 必填 | 说明 |
|------|------|:----:|------|
| `accounts` | `object` | — | 多账号配置，key 为自定义账号 ID，value 字段与顶层配置相同 |
| `defaultAccount` | `string` | — | 多账号模式下的默认账号 ID |
| `groups` | `object` | — | 按群覆盖配置，key 为群 ID（数字字符串），字段见下方 |

#### 群级别配置 (`groups.<groupId>`)

每个群可独立覆盖全局配置，未设置的字段继承顶层配置。

| 字段 | 类型 | 说明 |
|------|------|------|
| `replyMode` | `string` | 覆盖该群的回复策略 |
| `groupSessionMode` | `string` | 覆盖该群的会话拆分模式：`"group"` = 按群拆分；`"user"` = 按群+人拆分 |
| `followUp` | `boolean` | 覆盖该群的跟进开关 |
| `followUpWindow` | `number` | 覆盖该群的跟进时间窗口（秒） |
| `systemPrompt` | `string` | 该群专属系统提示词，覆盖全局 Agent 的 system prompt |

**配置优先级**：群级别 > 账号级别 > 顶层默认值

---

<a id="插件架构"></a>

## 插件架构

### 核心模块

| 模块 | 文件 | 功能 |
|------|------|------|
| **插件入口** | `index.ts` | 注册 Channel、Webhook 路由、Tools、Hooks、Commands |
| **Channel 定义** | `src/channel/channel.ts` | 插件结构、生命周期、Actions |
| **消息接收(Webhook)** | `src/adapter/inbound/webhook-parser.ts` | Webhook 解析、解密、去重 |
| **消息接收(WebSocket)** | `src/adapter/inbound/ws-receiver.ts` | WebSocket 长连接 |
| **接入管理** | `src/channel/monitor.ts` | Webhook/WebSocket 启动管理 |
| **消息处理** | `src/handler/message-handler.ts` | replyMode 决策、历史注入、LLM 调用 |
| **消息发送** | `src/channel/outbound.ts` | 私聊/群聊发送 API |
| **回复分发** | `src/adapter/outbound/reply-dispatcher.ts` | @mentions 解析、分块、引用回复构造 |
| **图片处理** | `src/channel/media.ts` | 图片下载、压缩、Base64（含 SSRF 防护） |
| **Actions** | `src/tools/actions/index.ts` | Channel Actions（send/delete，供 LLM 通过 Channel 调用） |
| **Agent Tools** | `src/tools/index.ts` | `infoflow_send` / `infoflow_recall`（LLM Function Calling） |
| **Agent Hooks** | `src/tools/hooks/index.ts` | `before_agent_start` 钩子（如流平台背景知识注入） |
| **多账号** | `src/channel/accounts.ts` | 账号解析、配置合并 |
| **SDK 适配** | `src/utils/token-adapter.ts` | Token 管理，封装 SDK TokenManager |
| **消息存储** | `src/utils/store/message-store.ts` | 已发送消息记录（支持撤回） |
| **安全策略** | `src/security/dm-policy.ts` | 私聊权限策略 |
| **群聊策略** | `src/security/group-policy.ts` | 群组权限、replyMode、followUp |
| **内置命令** | `src/commands/doctor.ts` | `/infoflow-doctor` 自检命令 |
| **内置命令** | `src/commands/logs.ts` | `/infoflow-logs` 日志查看命令 |

### 项目结构

```
openclaw_infoflow/
├── index.ts                              # 插件入口
├── openclaw.plugin.json                  # 插件元数据
├── skills/x
│   └── infoflow-dev/                     # 如流开发者指南 skill
├── src/
│   ├── types.ts                          # 类型定义
│   ├── runtime.ts                        # 插件运行时单例
│   ├── logging.ts                        # 日志模块
│   ├── channel/
│   │   ├── channel.ts                    # Channel 插件定义
│   │   ├── accounts.ts                   # 多账号解析
│   │   ├── monitor.ts                    # Webhook/WS 启动管理
│   │   ├── outbound.ts                   # 消息发送 API
│   │   └── media.ts                      # 图片处理
│   ├── adapter/
│   │   ├── inbound/
│   │   │   ├── webhook-parser.ts         # Webhook 解析解密
│   │   │   └── ws-receiver.ts            # WebSocket 长连接
│   │   └── outbound/
│   │       ├── reply-dispatcher.ts       # 回复分发（分片/@mention/引用）
│   │       └── target-resolver.ts        # 目标地址解析
│   ├── handler/
│   │   └── message-handler.ts            # 消息处理核心（replyMode 决策）
│   ├── security/
│   │   ├── dm-policy.ts                  # 私聊权限策略
│   │   └── group-policy.ts               # 群聊策略 + followUp
│   ├── tools/
│   │   ├── index.ts                      # infoflow_send / infoflow_recall
│   │   ├── actions/index.ts              # Channel Actions
│   │   └── hooks/index.ts                # Agent Hooks
│   ├── commands/
│   │   ├── doctor.ts                     # /infoflow-doctor
│   │   └── logs.ts                       # /infoflow-logs
│   └── utils/
│       ├── token-adapter.ts              # SDK Token 适配
│       └── store/message-store.ts        # 已发消息存储
└── README.md
```

---

<a id="开发指南"></a>

## 开发指南

### 前置要求

- Node.js >= 18
- OpenClaw >= 2026.3.2

### 本地开发

```bash
# 修改代码后同步并重启
rsync -av --delete ./ ~/.openclaw/extensions/infoflow/ \
  --exclude node_modules --exclude dist --exclude .git
openclaw gateway restart

# 查看日志
tail -f ~/.openclaw/logs/gateway.log | grep -i infoflow
```

### 常用命令

```bash
# 查看最近日志（含 DEBUG）
tail -200 ~/.openclaw/logs/gateway.log | grep DEBUG

# 检查 Webhook 是否收到消息
tail -200 ~/.openclaw/logs/gateway.log | grep "webhook"

# 检查消息是否发送成功
tail -200 ~/.openclaw/logs/gateway.log | grep "sendInfoflowMessage"
```

---

<a id="内置命令"></a>

## 内置命令

插件注册了两个可在如流中直接发送的内置命令。

### /infoflow-doctor — 自检

一键检查插件配置与 API 连通性，帮助排查收不到消息的问题。

**在如流私聊或群聊中发送：**
```
/infoflow-doctor
```

**自检项：**
1. 账号配置完整性（appKey、appSecret、checkToken 等）
2. Access Token 获取（实际调用 Infoflow API）
3. 发私聊测试消息
4. 发群聊测试消息（仅在群中触发时执行）
5. 发图片测试消息

### /infoflow-logs — 日志查看

查看 Infoflow 最近的收发消息日志，方便排查消息丢失、发送失败等问题。

**用法：**

```
/infoflow-logs          # 最近 50 条
/infoflow-logs 100      # 最近 100 条（最大 200）
/infoflow-logs 50-100   # 往前翻页：跳过最近 50 条，再往前看 50 条
```

**日志来源：** `/tmp/openclaw/openclaw-YYYY-MM-DD.log`（优先今日，降级昨日）

**输出上限：** 20,000 字符，超出时自动丢弃最老的条目。长输出由框架自动拆成多条消息发送。

---

<a id="扩展-agent-tools"></a>

## 扩展：Agent Tools

**Tools** 是 LLM Function Calling 的实现，Agent 可主动调用来执行操作。

### 内置 Tools

#### `infoflow_send` — 主动发消息

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `to` | string | ✅ | `"username"` 私聊，`"group:GROUP_ID"` 群聊 |
| `message` | string | ✅ | 消息正文，支持 Markdown |
| `atAll` | boolean | — | 群消息中 @全体成员 |
| `mentionUserIds` | string | — | 逗号分隔的 uuapName（群消息 @指定成员） |
| `accountId` | string | — | 多账号时指定账号 ID |

#### `infoflow_recall` — 撤回消息

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `to` | string | ✅ | 格式同 `infoflow_send` |
| `count` | number | — | 撤回最近 N 条（默认 1） |
| `messageId` | string | — | 撤回指定消息 ID（优先于 count） |
| `accountId` | string | — | 多账号时指定账号 ID |

#### `infoflow_emoji_reply` — 表情回复

为如流消息添加或移除表情回复，支持 146 种表情（d01-d146）。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `messageId` | string | ✅ | 要添加表情回复的消息 ID |
| `msgId2` | string | ✅ | 消息的 msgId2（辅助 ID） |
| `chatType` | 2 \| 7 | ✅ | 聊天类型：2 = 群聊，7 = 私聊 |
| `chatId` | number | — | 群聊 ID（仅群聊时需要） |
| `fromUid` | string | — | 消息发送者 ID（私聊时自动从会话上下文提取） |
| `emojiId` | string | ✅ | 表情 ID，如 d135（敲键盘）、d101（收到）、d48（赞） |
| `action` | "add" \| "remove" | ✅ | 操作类型：add = 添加表情，remove = 移除表情 |
| `accountId` | string | — | 多账号时指定账号 ID |

**常用表情**：
- 确认：d101（收到）、d105（搞定）、d48（赞）
- 情绪：d01（微笑）、d02（大笑）、d71（爱心）
- 工作：d135（敲键盘）、d118（优秀）、d124（编码中）

**主动使用场景**（LLM 会在以下情况主动添加表情）：
1. **长任务开始时**：执行耗时任务前，先用 d101（收到）或 d124（编码中）快速响应
2. **出错/失败时**：处理失败或遇到错误，用 d19（衰）或 d32（委屈）表达歉意
3. **理解/确认时**：收到用户指令后，用 d101（收到）或 d47（OK）确认理解
4. **用户表扬时**：用户说"厉害"、"棒"、"谢谢"、"牛"等夸赞词，用 d13（害羞）、d01（微笑）或 d71（爱心）回应
5. **幽默/轻松时**：对话轻松愉快，用 d03（哈哈）、d07（偷笑）或 d15（调皮）增加趣味

#### `infoflow_list_emojis` — 查询表情列表

查询如流支持的所有表情或指定类别的表情。

| 参数 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `category` | string | — | 表情类别，默认为 "all"（全部表情） |

**可用类别**：
- `all`：全部 146 种表情
- `confirm`：确认类（收到、搞定、OK 等）
- `emotion`：情绪类（微笑、大笑、爱心等）
- `work`：工作类（敲键盘、优秀、编码中等）
- `status`：状态类（工作中、会议中、休息中等）
- `gesture`：手势类（赞、握手、胜利等）
- `symbol`：符号类（爱心、月亮、太阳等）

### 添加自定义 Tool

在 `src/tools/` 下新建文件，在 `src/tools/index.ts` 的 `registerInfoflowTools` 中注册：

```typescript
import { Type, type Static } from "@sinclair/typebox";
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";

const MyToolSchema = Type.Object({
  query: Type.String({ description: "查询关键词" }),
});

export function registerMyTool(api: OpenClawPluginApi) {
  api.registerTool({
    name: "infoflow_my_tool",
    description: "工具说明...",
    parameters: MyToolSchema,
    async execute(_toolCallId, params) {
      const p = params as Static<typeof MyToolSchema>;
      const result = { data: "..." };
      return {
        content: [{ type: "text" as const, text: JSON.stringify(result) }],
        details: result,
      };
    },
  });
}
```

---

<a id="扩展-agent-hooks"></a>

## 扩展：Agent Hooks

**Hooks** 通过 `api.on()` 注册生命周期钩子，在 Agent 启动时向 system prompt 注入背景知识。

### 内置 Hook

#### `infoflow-intro` — 如流平台背景知识

每次 Agent 启动时自动注入，告知 LLM 如流平台的会话类型、消息格式、Bot 能力和访问控制。注入内容被缓存到 system prompt 尾部（prompt caching），不产生额外每轮 token 消耗。

### 添加自定义 Hook

在 `src/hooks/index.ts` 的 `registerInfoflowHooks` 中添加：

```typescript
api.on("before_agent_start", (_event, ctx) => {
  return {
    appendSystemContext: `
## 业务规则
- 工单 SLA：P0 30 分钟，P1 2 小时
    `.trim(),
  };
});
```

**Hook 返回值字段：**

| 字段 | 说明 |
|------|------|
| `appendSystemContext` | 追加到 system prompt 尾部（推荐，利用 prompt caching） |
| `prependSystemContext` | 插入到 system prompt 头部 |
| `systemPrompt` | 完全覆盖 system prompt（慎用） |
| `modelOverride` | 覆盖模型 |

---

<a id="扩展-agent-skills"></a>

## 扩展：Agent Skills

**Skills** 是随插件分发的知识包，安装插件时自动安装到用户的 OpenClaw 实例，为 Agent 提供领域专属知识。

### 内置 Skill

#### `infoflow-dev` — 如流开发者指南

包含如流消息 API 参考、鉴权机制、插件配置说明等开发文档。当用户在 Agent 会话中询问如流相关开发问题时自动触发。

Skill 文件位于 `skills/infoflow-dev/`，随 `openclaw plugins install` 一并安装。

### 添加自定义 Skill

在 `skills/` 下新建目录，包含 `SKILL.md`（必须）和可选的 `scripts/`、`references/`、`assets/` 目录：

```
skills/
└── my-skill/
    ├── SKILL.md          # Skill 元数据（frontmatter）+ 使用说明
    └── references/
        └── guide.md      # 按需加载的参考文档
```

`SKILL.md` 格式：

```markdown
---
name: my-skill
description: Skill 描述，说明何时触发（这是触发机制，写清楚触发场景）
---

# 使用说明
...
```

在 `openclaw.plugin.json` 的 `skills` 字段声明 Skill 目录路径：

```json
{
  "skills": ["./skills/my-skill"]
}
```

---

<a id="faq"></a>

## FAQ

### 1. 如何获取如流配置参数？

在如流企业后台：
1. 进入"应用管理" → 选择你的机器人应用
2. 获取 `appKey`、`appSecret`、`appAgentId`
3. 配置 Webhook，获取 `checkToken` 和 `encodingAESKey`

### 2. 机器人不回复消息？

1. 检查 `replyMode` 是否允许该群触发
2. 检查日志是否收到 Webhook 请求：
   ```bash
   tail -200 ~/.openclaw/logs/gateway.log | grep "webhook"
   ```
3. 检查 `robotName` 是否与如流后台机器人显示名一致（影响 @mention 检测）。**注意：不配置 `robotName` 时，`mention-only` 模式将退化为对所有群消息触发，而非完全不触发**
4. 检查 `groupPolicy` 是否允许该群
5. 在如流中发送 `/infoflow-doctor` 进行自检

### 3. 消息撤回失败？

- **时间限制**：如流 API 通常限制 2 分钟内可撤回
- **缺少参数**：群聊需要 `messageid` + `msgseqid`；私聊需要 `msgkey` + `appAgentId`
- **未记录**：消息未被记录到 `sent-message-store`（查看 `~/.openclaw/plugins/infoflow/sent-messages/default.json`）

### 4. WebSocket 模式和 Webhook 模式有什么区别？

| | Webhook | WebSocket |
|--|---------|-----------|
| 需要公网域名 | ✅ | ❌ |
| 消息解密（checkToken/AES） | ✅ 需要 | ❌ 不需要 |
| 适合场景 | 生产环境 | 本地开发调试 |

---

## License

MIT

---

<a id="english"></a>

# Infoflow OpenClaw Plugin (English)

Baidu Infoflow enterprise messaging platform — OpenClaw channel plugin. Supports both Webhook and WebSocket connection modes, with image message handling and quote reply support.

## Table of Contents

- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Architecture](#architecture)
- [Development Guide](#development-guide)
- [Built-in Commands](#built-in-commands)
- [Extending: Agent Tools](#extending-agent-tools)
- [Extending: Agent Hooks](#extending-agent-hooks)
- [Extending: Agent Skills](#extending-agent-skills)
- [FAQ (EN)](#faq-en)

---

<a id="quick-start"></a>

## Quick Start

> 5 steps from zero to a running bot.

### Prerequisites: Create an app in Infoflow enterprise console

1. Infoflow enterprise console → App Management → Create bot app
2. Note the following 4 parameters:

   | Parameter | Where to find |
   |-----------|---------------|
   | `appKey` | App basic info |
   | `appSecret` | App basic info |
   | `checkToken` | Message push → Config |
   | `encodingAESKey` | Message push → Config |

3. Choose a connection mode:
   - **WebSocket** (recommended, no public domain needed): no extra setup
   - **Webhook**: set `https://your-domain/webhook/infoflow` in "Message push"

### Step 1: Install OpenClaw

```bash
# macOS / Linux
curl -fsSL https://openclaw.dev/install.sh | bash

openclaw configure
```

### Step 2: Clone the plugin

```bash
git clone ssh://icode.baidu.com:8235/baidu/hi/openclaw_infoflow
cd openclaw_infoflow
npm install
```

### Step 3: Write config

Open `~/.openclaw/openclaw.json` and add the infoflow channel config:

```json5
{
  "channels": {
    "infoflow": {
      "connectionMode": "webhook",
      "wsGateway": "infoflow-open-gateway.baidu.com",
      "apiHost": "https://api.im.baidu.com",
      "appKey": "your-app-key",
      "appSecret": "your-app-secret",
      "checkToken": "your-check-token",
      "encodingAESKey": "your-encoding-aes-key"
  }
}
```

For more options (access control, group policies, message format, etc.) see [Configuration Reference](#configuration).

### Step 4: Deploy

```bash
./scripts/deploy.sh
```

### Step 5: Verify

- **DM**: Send the bot a direct message — it should reply
- **Group**: @mention the bot in a whitelisted group — it should reply
- **Logs**: `openclaw logs | grep infoflow`

---

<a id="configuration"></a>

## Configuration

### Minimal Configuration

```json5
{
  channels: {
    infoflow: {
      apiHost: "https://api.im.baidu.com",
      checkToken: "your-check-token",
      encodingAESKey: "your-encoding-aes-key",
      appKey: "your-app-key",
      appSecret: "your-app-secret",
    },
  },
}
```

### Full Configuration

```json5
{
  channels: {
    infoflow: {
      // ── Connection ────────────────────────────────────────
      enabled: true,
      apiHost: "https://api.im.baidu.com",           // required
      connectionMode: "webhook",                        // "webhook" (default) or "websocket"
      wsGateway: "infoflow-open-gateway.baidu.com", // websocket mode only
      checkToken: "your-check-token",                    // required
      encodingAESKey: "your-encoding-aes-key",           // required
      appKey: "your-app-key",                            // required
      appSecret: "your-app-secret",                      // required
      appAgentId: 12345,                                 // required for DM recall
      robotName: "MyBot",                                // for @mention detection in groups

      // ── Access Control ────────────────────────────────────
      dmPolicy: "allowlist",        // "open" / "pairing" / "allowlist"
      allowFrom: ["alice", "bob"],  // DM whitelist (uuapName), used when dmPolicy="allowlist"
      groupPolicy: "allowlist",     // "open" / "allowlist" / "disabled"
      groupAllowFrom: ["12345678"], // Group whitelist (group ID string), used when groupPolicy="allowlist"

      // ── Reply Behavior ────────────────────────────────────
      replyMode: "mention-only",   // "ignore" / "record" / "mention-only"
      requireMention: false,
      followUp: true,                 // allow follow-up questions without re-mentioning
      followUpWindow: 300,            // follow-up window in seconds

      // ── Message Format ────────────────────────────────────
      dmMessageFormat: "markdown",    // "text" (default) or "markdown" for DMs
      groupMessageFormat: "text",     // "text" (default) or "markdown" for groups (no quote reply)
      processingHint: true,
      processingHintDelay: 5,

      // ── Multi-account ─────────────────────────────────────
      defaultAccount: "main",
      accounts: {
        main: { appKey: "...", appSecret: "..." },
      },

      // ── Per-group Overrides ───────────────────────────────
      groups: {
        "12345678": {
          replyMode: "mention-only",
          systemPrompt: "You are the dedicated assistant for this group.",
        },
      },
    },
  },
}
```

### Connection Modes

| Mode | Description | Use Case |
|------|-------------|----------|
| `webhook` (default) | Infoflow server pushes HTTP requests to your service | Public domain or tunnel |
| `websocket` | Plugin connects to Infoflow WebSocket gateway | No public domain, local dev |

**Webhook mode** — configure Webhook URL in Infoflow enterprise console:
```
https://your-domain/webhook/infoflow
```

**WebSocket mode** — no public domain needed:
```json5
{
  channels: {
    infoflow: {
      connectionMode: "websocket",
      // wsGateway: "infoflow-open-gateway.baidu.com",  // optional
    },
  },
}
```

### Configuration Reference

#### Connection

| Field | Type | Required | Default | Description |
|-------|------|:--------:|---------|-------------|
| `apiHost` | `string` | ✅ | — | Infoflow API host, e.g. `https://api.im.baidu.com` |
| `checkToken` | `string` | — | — | Message verification token from Infoflow enterprise console (**required for webhook mode**) |
| `encodingAESKey` | `string` | — | — | AES encryption key from Infoflow enterprise console (**required for webhook mode**) |
| `appKey` | `string` | ✅ | — | App Key from Infoflow enterprise console |
| `appSecret` | `string` | ✅ | — | App Secret from Infoflow enterprise console |
| `appAgentId` | `number` | — | — | App Agent ID (numeric); required for private message recall |
| `robotName` | `string` | — | — | Bot display name in groups, used for @mention detection. **Without this, `mention-only` mode falls back to triggering on every message** |
| `connectionMode` | `string` | — | `"webhook"` | `"webhook"` (Infoflow pushes to your server) or `"websocket"` (plugin connects to Infoflow) |
| `wsGateway` | `string` | — | `"infoflow-open-gateway.baidu.com"` | WebSocket gateway hostname, only used in `websocket` mode |
| `enabled` | `boolean` | — | `true` | Whether to enable the plugin |

#### Access Control

| Field | Type | Required | Default | Description |
|-------|------|:--------:|---------|-------------|
| `dmPolicy` | `string` | — | `"open"` | DM policy: `"open"` (anyone); `"pairing"` (requires pairing); `"allowlist"` (whitelist only) |
| `allowFrom` | `string[]` | — | `[]` | DM whitelist (uuapName / email prefix), effective when `dmPolicy="allowlist"` |
| `groupPolicy` | `string` | — | `"open"` | Group policy: `"open"` (any group); `"allowlist"` (whitelist only); `"disabled"` (no group messages) |
| `groupAllowFrom` | `string[]` | — | `[]` | Group whitelist (numeric group ID strings), effective when `groupPolicy="allowlist"` |
| `requireMention` | `boolean` | — | `false` | Require explicit @ in group messages to trigger |

#### Reply Behavior

| Field | Type | Required | Default | Description |
|-------|------|:--------:|---------|-------------|
| `replyMode` | `string` | — | `"mention-only"` | `ignore` (discard) / `record` (save only) / `mention-only` (when @mentioned) |
| `followUp` | `boolean` | — | `true` | Auto-detect follow-up questions after bot replies (no @ needed) |
| `followUpWindow` | `number` | — | `300` | Follow-up window in seconds |

#### Message Format

| Field | Type | Required | Default | Description |
|-------|------|:--------:|---------|-------------|
| `dmMessageFormat` | `string` | — | `"text"` | Format for private (DM) messages: `"text"` (plain) or `"markdown"` (rich text) |
| `groupMessageFormat` | `string` | — | `"text"` | Format for group messages: `"text"` (plain) or `"markdown"` (rich text). **Note: `markdown` does not support quote replies** |
| `textChunkLimit` | `number` | — | `1800` | Max characters per outbound message chunk; long messages are split into multiple messages |
| `processingHint` | `boolean` | — | `true` | Send "⏳ processing..." hint when LLM is slow; send error message on failure |
| `processingHintDelay` | `number` | — | `5` | Seconds to wait before sending the hint; set to `0` to send immediately |

#### Multi-account & Groups

| Field | Type | Required | Description |
|-------|------|:--------:|-------------|
| `accounts` | `object` | — | Multi-account config; key = account ID, value has same fields as top-level |
| `defaultAccount` | `string` | — | Default account ID |
| `groups` | `object` | — | Per-group overrides; key = group ID (numeric string) |

Per-group fields: `replyMode`, `followUp`, `followUpWindow`, `systemPrompt`

**Priority**: per-group > account-level > top-level defaults

---

<a id="architecture"></a>

## Architecture

| Module | File | Description |
|--------|------|-------------|
| Entry | `index.ts` | Register channel, webhook route, tools, hooks |
| Channel | `src/channel.ts` | Plugin structure, lifecycle, actions |
| Webhook | `src/inbound/webhook-parser.ts` | Parse, decrypt, deduplicate |
| WebSocket | `src/inbound/ws-receiver.ts` | Long-lived connection |
| Monitor | `src/inbound/monitor.ts` | Manage Webhook/WebSocket startup |
| Handler | `src/inbound/message-handler.ts` | replyMode logic, history injection, LLM call |
| Send | `src/outbound/send.ts` | DM / group message sending |
| Dispatcher | `src/outbound/reply-dispatcher.ts` | @mentions, chunking, quote reply |
| Media | `src/outbound/media.ts` | Image download, compress, Base64 (SSRF protection) |
| Actions | `src/outbound/actions.ts` | Channel actions (send/delete for LLM) |
| Tools | `src/tools/index.ts` | `infoflow_send` / `infoflow_recall` |
| Hooks | `src/hooks/index.ts` | `before_agent_start` hook |
| Accounts | `src/config/accounts.ts` | Multi-account resolution |
| Token | `src/sdk/token-adapter.ts` | Token management |
| Store | `src/outbound/message-store.ts` | Sent message store (for recall) |

---

<a id="development-guide"></a>

## Development Guide

```bash
# Sync and restart after code changes
rsync -av --delete ./ ~/.openclaw/extensions/infoflow/ \
  --exclude node_modules --exclude dist --exclude .git
openclaw gateway restart

# View logs
tail -f ~/.openclaw/logs/gateway.log | grep -i infoflow
```

---

<a id="built-in-commands"></a>

## Built-in Commands

### /infoflow-doctor — Self-check

Diagnose configuration and API connectivity issues.

```
/infoflow-doctor
```

Checks: account config completeness, access token fetch, DM send test, group send test, image send test.

### /infoflow-logs — Log viewer

View recent Infoflow send/receive logs.

```
/infoflow-logs          # last 50 entries
/infoflow-logs 100      # last 100 entries (max 200)
/infoflow-logs 50-100   # page back: skip last 50, show previous 50
```

Log source: `/tmp/openclaw/openclaw-YYYY-MM-DD.log`. Total output capped at 20,000 characters.

---

<a id="extending-agent-tools"></a>

## Extending: Agent Tools

**Tools** implement LLM Function Calling — the agent calls them to take actions.

#### `infoflow_send` — Send a message

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `to` | string | ✅ | `"username"` for DM, `"group:GROUP_ID"` for group |
| `message` | string | ✅ | Message body (Markdown supported) |
| `atAll` | boolean | — | @all in group |
| `mentionUserIds` | string | — | Comma-separated uuapNames to @mention |
| `accountId` | string | — | Account ID for multi-account setups |

#### `infoflow_recall` — Recall messages

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| `to` | string | ✅ | Same format as `infoflow_send` |
| `count` | number | — | Recall last N messages (default: 1) |
| `messageId` | string | — | Recall by message ID (overrides count) |
| `accountId` | string | — | Account ID for multi-account setups |

To add a custom tool, register it inside `registerInfoflowTools` in `src/tools/index.ts`.

---

<a id="extending-agent-hooks"></a>

## Extending: Agent Hooks

**Hooks** inject domain knowledge into the agent system prompt via `api.on("before_agent_start", ...)`.

The built-in `infoflow-intro` hook injects Infoflow platform context (session types, message formats, bot capabilities, access control) into every agent run. The content is appended to the system prompt and benefits from prompt caching.

To add a custom hook, call `api.on(...)` inside `registerInfoflowHooks` in `src/hooks/index.ts`.

**Hook return fields:** `appendSystemContext`, `prependSystemContext`, `systemPrompt`, `modelOverride`

---

<a id="extending-agent-skills"></a>

## Extending: Agent Skills

**Skills** are knowledge packages bundled with the plugin and auto-installed alongside it. They provide domain-specific context to the Agent.

### Built-in Skill

#### `infoflow-dev` — Infoflow Developer Guide

Contains Infoflow message API reference, authentication, and plugin configuration docs. Automatically triggers when users ask Infoflow-related development questions.

Located in `skills/infoflow-dev/`, installed via `openclaw plugins install`.

### Adding a Custom Skill

Create a directory under `skills/` with a `SKILL.md` and optional resource folders:

```
skills/
└── my-skill/
    ├── SKILL.md
    └── references/
        └── guide.md
```

Declare the skill path in `openclaw.plugin.json`:

```json
{
  "skills": ["./skills/my-skill"]
}
```

---

<a id="faq-en"></a>

## FAQ (EN)

### Bot not replying?

1. Check `replyMode` — confirm the group is allowed to trigger
2. Check logs for incoming webhook requests
3. Verify `robotName` matches the bot's display name in Infoflow (affects @mention detection). **Note: without `robotName`, `mention-only` mode falls back to triggering on every message**
4. Check `groupPolicy` allows the group
5. Send `/infoflow-doctor` in Infoflow to run self-checks

### Message recall failing?

- **Time limit**: Infoflow API typically allows recall within 2 minutes
- **Missing params**: Group recall needs `messageid` + `msgseqid`; DM recall needs `msgkey` + `appAgentId`
- **Not recorded**: Message may not have been saved to the sent-message store

### Webhook vs WebSocket?

| | Webhook | WebSocket |
|--|---------|-----------|
| Requires public domain | ✅ | ❌ |
| Message decryption | ✅ needed | ❌ not needed |
| Best for | Production | Local development |

---

## License

MIT
