@core-workspace/infoflow-openclaw-plugin 2026.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +989 -0
- package/docs/architecture-data-flow.md +429 -0
- package/docs/architecture.md +423 -0
- package/docs/dev-guide.md +611 -0
- package/index.ts +29 -0
- package/openclaw.plugin.json +138 -0
- package/package.json +40 -0
- package/scripts/deploy.sh +34 -0
- package/skills/infoflow-dev/SKILL.md +88 -0
- package/skills/infoflow-dev/references/api.md +413 -0
- package/src/adapter/inbound/webhook-parser.ts +433 -0
- package/src/adapter/inbound/ws-receiver.ts +226 -0
- package/src/adapter/outbound/reply-dispatcher.ts +281 -0
- package/src/adapter/outbound/target-resolver.ts +109 -0
- package/src/channel/accounts.ts +164 -0
- package/src/channel/channel.ts +364 -0
- package/src/channel/media.ts +365 -0
- package/src/channel/monitor.ts +184 -0
- package/src/channel/outbound.ts +934 -0
- package/src/events.ts +62 -0
- package/src/handler/message-handler.ts +801 -0
- package/src/logging.ts +123 -0
- package/src/runtime.ts +14 -0
- package/src/security/dm-policy.ts +80 -0
- package/src/security/group-policy.ts +271 -0
- package/src/tools/actions/index.ts +456 -0
- package/src/tools/hooks/index.ts +82 -0
- package/src/tools/index.ts +277 -0
- package/src/types.ts +277 -0
- package/src/utils/store/message-store.ts +295 -0
- package/src/utils/token-adapter.ts +90 -0
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
# OpenClaw Infoflow 插件开发规范
|
|
2
|
+
|
|
3
|
+
> 面向 `openclaw_infoflow` 插件的开发者,涵盖目录结构、模块职责、核心 API、配置规范、测试要求和提交流程。
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 目录
|
|
8
|
+
|
|
9
|
+
1. [项目概览](#1-项目概览)
|
|
10
|
+
2. [目录结构与模块职责](#2-目录结构与模块职责)
|
|
11
|
+
3. [核心数据流](#3-核心数据流)
|
|
12
|
+
4. [配置参数规范](#4-配置参数规范)
|
|
13
|
+
5. [如流 API 接口](#5-如流-api-接口)
|
|
14
|
+
6. [消息格式规范](#6-消息格式规范)
|
|
15
|
+
7. [安全策略模块](#7-安全策略模块)
|
|
16
|
+
8. [工具与 Hook 开发](#8-工具与-hook-开发)
|
|
17
|
+
9. [测试规范](#9-测试规范)
|
|
18
|
+
10. [开发工作流](#10-开发工作流)
|
|
19
|
+
11. [常见问题与注意事项](#11-常见问题与注意事项)
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 1. 项目概览
|
|
24
|
+
|
|
25
|
+
`openclaw_infoflow` 是 OpenClaw 的如流(Baidu Infoflow)渠道插件,支持:
|
|
26
|
+
|
|
27
|
+
- **私聊(DM)** 与 **群聊(Group)** 双模式
|
|
28
|
+
- **Webhook** 与 **WebSocket** 双接入方式
|
|
29
|
+
- 消息发送、撤回、图片发送
|
|
30
|
+
- 群聊多种触发模式(mention-only / mention-and-watch / proactive / record / ignore)
|
|
31
|
+
- @mention 检测、follow-up 窗口、条件回复(NO_REPLY)
|
|
32
|
+
|
|
33
|
+
**技术栈**:TypeScript (ESM),Node.js 22+,运行时由 OpenClaw gateway 加载(无单独构建步骤)。
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 2. 目录结构与模块职责
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
openclaw_infoflow/
|
|
41
|
+
├── index.ts # 插件入口:注册 channel、HTTP route、tools、hooks
|
|
42
|
+
├── src/
|
|
43
|
+
│ ├── types.ts # 所有类型定义(Policy、Config、Event、Params 等)
|
|
44
|
+
│ ├── runtime.ts # 插件运行时单例(getInfoflowRuntime / setInfoflowRuntime)
|
|
45
|
+
│ ├── logging.ts # 日志工具(getInfoflowBotLog 等)
|
|
46
|
+
│ ├── events.ts # 内部事件总线(coreEvents,解耦 outbound → store)
|
|
47
|
+
│ │
|
|
48
|
+
│ ├── channel/ # Plugin Entry Layer:插件接入层
|
|
49
|
+
│ │ ├── channel.ts # ChannelPlugin 定义,与 OpenClaw SDK 集成
|
|
50
|
+
│ │ ├── accounts.ts # 多账号配置解析(resolveInfoflowAccount)
|
|
51
|
+
│ │ ├── monitor.ts # Webhook HTTP handler + WebSocket 启动
|
|
52
|
+
│ │ ├── outbound.ts # 发送 API(sendInfoflowMessage、recallInfoflowGroupMessage 等)
|
|
53
|
+
│ │ └── media.ts # 图片处理(base64 编解码、发送图片消息)
|
|
54
|
+
│ │
|
|
55
|
+
│ ├── adapter/ # Message Adapter Layer:消息格式转换(纯函数)
|
|
56
|
+
│ │ ├── inbound/
|
|
57
|
+
│ │ │ ├── webhook-parser.ts # AES 解密、消息路由、dedup 缓存
|
|
58
|
+
│ │ │ └── ws-receiver.ts # WebSocket 接收、心跳、重连
|
|
59
|
+
│ │ └── outbound/
|
|
60
|
+
│ │ ├── reply-dispatcher.ts # Reply dispatcher,负责分块、@mention 解析
|
|
61
|
+
│ │ └── target-resolver.ts # 目标地址规范化(group:<id> / username)
|
|
62
|
+
│ │
|
|
63
|
+
│ ├── handler/ # Event Bridge Layer:事件桥接层
|
|
64
|
+
│ │ └── message-handler.ts # 核心消息处理(私聊/群聊路由 → LLM dispatch)
|
|
65
|
+
│ │
|
|
66
|
+
│ ├── security/ # Security Policy Layer:安全策略层
|
|
67
|
+
│ │ ├── group-policy.ts # 群聊策略(@mention 检测、replyMode、follow-up、prompt 构建)
|
|
68
|
+
│ │ └── dm-policy.ts # 接入控制(dmPolicy、groupPolicy 检查)
|
|
69
|
+
│ │
|
|
70
|
+
│ ├── tools/ # Tool Registration Layer:工具注册层
|
|
71
|
+
│ │ ├── index.ts # 注册 infoflow_send、infoflow_recall 工具
|
|
72
|
+
│ │ ├── actions/index.ts # ChannelMessageActionAdapter(send/delete 动作)
|
|
73
|
+
│ │ └── hooks/index.ts # before_agent_start hook(注入如流 intro)
|
|
74
|
+
│ │
|
|
75
|
+
│ └── utils/ # Shared Utilities:共享工具
|
|
76
|
+
│ ├── token-adapter.ts # SDK TokenManager 适配(appKey/appSecret → token)
|
|
77
|
+
│ └── store/
|
|
78
|
+
│ └── message-store.ts # SQLite 持久化已发消息(用于撤回查询)
|
|
79
|
+
│
|
|
80
|
+
└── tests/ # 测试文件(镜像 src/ 结构)
|
|
81
|
+
├── adapter/
|
|
82
|
+
├── channel/
|
|
83
|
+
├── handler/
|
|
84
|
+
├── tools/
|
|
85
|
+
└── utils/
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 模块职责边界
|
|
89
|
+
|
|
90
|
+
| 层 | 职责 | 禁止 |
|
|
91
|
+
|---|---|---|
|
|
92
|
+
| `channel/` | 与 OpenClaw SDK 交互、网络 I/O | 直接处理业务逻辑 |
|
|
93
|
+
| `adapter/` | 消息格式转换,纯函数为主 | 副作用、网络请求 |
|
|
94
|
+
| `handler/` | 事件路由,调用 security/ 决策 | 直接调用如流 API |
|
|
95
|
+
| `security/` | 纯策略判断,无网络 I/O | 发送消息、修改状态 |
|
|
96
|
+
| `tools/` | Tool/Hook 注册 | 包含核心业务逻辑 |
|
|
97
|
+
| `utils/` | 跨层共享工具 | 依赖上层模块 |
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## 3. 核心数据流
|
|
102
|
+
|
|
103
|
+
### 3.1 入站消息(Webhook 模式)
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
如流服务器
|
|
107
|
+
│ POST /webhook/infoflow
|
|
108
|
+
▼
|
|
109
|
+
channel/monitor.ts (handleInfoflowWebhookRequest)
|
|
110
|
+
│ AES 解密 + checkToken 验证
|
|
111
|
+
▼
|
|
112
|
+
adapter/inbound/webhook-parser.ts (parseAndDispatchInfoflowRequest)
|
|
113
|
+
│ dedup 过滤 + MsgType 路由
|
|
114
|
+
▼
|
|
115
|
+
handler/message-handler.ts
|
|
116
|
+
├── handlePrivateChatMessage → security/dm-policy (checkDmPolicy)
|
|
117
|
+
└── handleGroupChatMessage → security/dm-policy (checkGroupPolicy)
|
|
118
|
+
→ security/group-policy (checkBotMentioned, replyMode 判断)
|
|
119
|
+
│
|
|
120
|
+
▼
|
|
121
|
+
handleInfoflowMessage
|
|
122
|
+
│ resolveGroupConfig → 判断 replyMode / follow-up / watch
|
|
123
|
+
│ buildAgentEnvelope → 注入历史上下文
|
|
124
|
+
│ core.channel.reply.dispatchReplyWithBufferedBlockDispatcher (LLM)
|
|
125
|
+
▼
|
|
126
|
+
adapter/outbound/reply-dispatcher.ts
|
|
127
|
+
│ 分块、@mention 解析、reply-to 构建
|
|
128
|
+
▼
|
|
129
|
+
channel/outbound.ts (sendInfoflowMessage)
|
|
130
|
+
│ POST 到如流 API
|
|
131
|
+
▼
|
|
132
|
+
utils/store/message-store.ts (记录 messageId 用于撤回)
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 3.2 出站消息撤回
|
|
136
|
+
|
|
137
|
+
```
|
|
138
|
+
LLM → tools/actions/index.ts (action="delete")
|
|
139
|
+
│ 从 message-store 查询 msgseqid
|
|
140
|
+
▼
|
|
141
|
+
channel/outbound.ts (recallInfoflowGroupMessage / recallInfoflowPrivateMessage)
|
|
142
|
+
│ POST /api/v1/robot/group/msgRecall 或 /api/v1/app/message/revoke
|
|
143
|
+
▼
|
|
144
|
+
message-store 清理已撤回记录
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## 4. 配置参数规范
|
|
150
|
+
|
|
151
|
+
配置路径:`channels.infoflow` 或 `channels.infoflow.accounts.<id>`
|
|
152
|
+
|
|
153
|
+
### 4.1 基础鉴权参数(必填)
|
|
154
|
+
|
|
155
|
+
| 参数 | 类型 | 说明 |
|
|
156
|
+
|------|------|------|
|
|
157
|
+
| `checkToken` | string | Webhook 验证 Token(如流企业后台配置) |
|
|
158
|
+
| `encodingAESKey` | string | 消息加密 AES Key(43 位 Base64) |
|
|
159
|
+
| `appKey` | string | 应用 AppKey |
|
|
160
|
+
| `appSecret` | string | 应用 AppSecret |
|
|
161
|
+
| `apiHost` | string | 如流 API 域名,默认 `https://im.baidu.com` |
|
|
162
|
+
|
|
163
|
+
### 4.2 连接方式
|
|
164
|
+
|
|
165
|
+
| 参数 | 类型 | 默认 | 说明 |
|
|
166
|
+
|------|------|------|------|
|
|
167
|
+
| `connectionMode` | `"webhook"` \| `"websocket"` | `"webhook"` | 接入方式 |
|
|
168
|
+
| `wsGateway` | string | - | WebSocket Gateway 域名(仅 websocket 模式) |
|
|
169
|
+
|
|
170
|
+
### 4.3 私聊策略(dmPolicy)
|
|
171
|
+
|
|
172
|
+
| 值 | 行为 |
|
|
173
|
+
|----|------|
|
|
174
|
+
| `"open"` | 所有用户可发消息(默认) |
|
|
175
|
+
| `"pairing"` | 配对模式,由框架处理(插件仅记录日志) |
|
|
176
|
+
| `"allowlist"` | 仅 `allowFrom` 列表内的 uuapName 可触发 |
|
|
177
|
+
|
|
178
|
+
```yaml
|
|
179
|
+
channels:
|
|
180
|
+
infoflow:
|
|
181
|
+
dmPolicy: allowlist
|
|
182
|
+
allowFrom:
|
|
183
|
+
- zhangsan
|
|
184
|
+
- lisi
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### 4.4 群聊策略(groupPolicy)
|
|
188
|
+
|
|
189
|
+
| 值 | 行为 |
|
|
190
|
+
|----|------|
|
|
191
|
+
| `"open"` | 所有群可触发(默认,**建议修改为 allowlist**) |
|
|
192
|
+
| `"disabled"` | 所有群消息忽略 |
|
|
193
|
+
| `"allowlist"` | 仅 `groupAllowFrom` 内的 groupId 可触发 |
|
|
194
|
+
|
|
195
|
+
```yaml
|
|
196
|
+
channels:
|
|
197
|
+
infoflow:
|
|
198
|
+
groupPolicy: allowlist
|
|
199
|
+
groupAllowFrom:
|
|
200
|
+
- "12345678"
|
|
201
|
+
- "87654321"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 4.5 群聊触发模式(replyMode)
|
|
205
|
+
|
|
206
|
+
| 值 | 触发条件 |
|
|
207
|
+
|----|---------|
|
|
208
|
+
| `"mention-only"` | 仅 @机器人时回复(默认),支持 follow-up 窗口 |
|
|
209
|
+
| `"mention-and-watch"` | @机器人 或 @watchMentions 中的人 或 watchRegex 匹配时回复 |
|
|
210
|
+
| `"proactive"` | 所有消息都进入 LLM 判断,由 LLM 决定是否回复 |
|
|
211
|
+
| `"record"` | 仅记录消息到 session,不回复 |
|
|
212
|
+
| `"ignore"` | 完全忽略,不记录不回复 |
|
|
213
|
+
|
|
214
|
+
可在 account 级别配置,也可在 `groups.<groupId>` 级别覆盖:
|
|
215
|
+
|
|
216
|
+
```yaml
|
|
217
|
+
channels:
|
|
218
|
+
infoflow:
|
|
219
|
+
replyMode: mention-only
|
|
220
|
+
robotName: "小助手"
|
|
221
|
+
followUp: true
|
|
222
|
+
followUpWindow: 300 # 秒,默认 5 分钟
|
|
223
|
+
watchMentions:
|
|
224
|
+
- xuejian
|
|
225
|
+
- zhangsan
|
|
226
|
+
watchRegex: "上线|发布|故障"
|
|
227
|
+
groups:
|
|
228
|
+
"12345678":
|
|
229
|
+
replyMode: proactive # 该群单独配置为 proactive
|
|
230
|
+
systemPrompt: "你是该群的专属助手,聚焦代码 review 相关问题"
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### 4.6 其他配置
|
|
234
|
+
|
|
235
|
+
| 参数 | 类型 | 默认 | 说明 |
|
|
236
|
+
|------|------|------|------|
|
|
237
|
+
| `robotName` | string | - | 机器人名称,用于识别 @机器人 |
|
|
238
|
+
| `processingHint` | boolean | `true` | LLM 超时后发送 "⏳ 处理中..." 提示 |
|
|
239
|
+
| `processingHintDelay` | number | `5` | 提示延迟秒数 |
|
|
240
|
+
| `dmMessageFormat` | `"text"` \| `"markdown"` | `"text"` | 私聊消息格式 |
|
|
241
|
+
| `groupMessageFormat` | `"text"` \| `"markdown"` | `"text"` | 群聊消息格式(markdown 不支持 reply-to) |
|
|
242
|
+
| `appAgentId` | number | - | 应用 AgentId(私聊消息撤回依赖此字段) |
|
|
243
|
+
|
|
244
|
+
---
|
|
245
|
+
|
|
246
|
+
## 5. 如流 API 接口
|
|
247
|
+
|
|
248
|
+
### 5.1 API 路径常量(`src/channel/outbound.ts`)
|
|
249
|
+
|
|
250
|
+
```typescript
|
|
251
|
+
INFOFLOW_PRIVATE_SEND_PATH = "/api/v1/app/message/send"
|
|
252
|
+
INFOFLOW_GROUP_SEND_PATH = "/api/v1/robot/msg/groupmsgsend"
|
|
253
|
+
INFOFLOW_GROUP_RECALL_PATH = "/api/v1/robot/group/msgRecall"
|
|
254
|
+
INFOFLOW_PRIVATE_RECALL_PATH = "/api/v1/app/message/revoke"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
### 5.2 鉴权方式
|
|
258
|
+
|
|
259
|
+
使用 `utils/token-adapter.ts` 中的 `getOrCreateAdapter(account)` 获取 token,自动处理 appKey/appSecret 换取 access_token 及缓存刷新。
|
|
260
|
+
|
|
261
|
+
**不要直接在业务代码中手动构造鉴权 Header**,统一通过 `sendInfoflowMessage` 函数发送。
|
|
262
|
+
|
|
263
|
+
### 5.3 发送消息
|
|
264
|
+
|
|
265
|
+
```typescript
|
|
266
|
+
import { sendInfoflowMessage } from "../channel/outbound.js";
|
|
267
|
+
|
|
268
|
+
await sendInfoflowMessage({
|
|
269
|
+
cfg,
|
|
270
|
+
to: "username", // 私聊:uuapName;群聊:group:<groupId>
|
|
271
|
+
contents: [
|
|
272
|
+
{ type: "text", content: "Hello" },
|
|
273
|
+
{ type: "markdown", content: "**Bold**" },
|
|
274
|
+
{ type: "at", content: "zhangsan" }, // @人
|
|
275
|
+
{ type: "at-agent", content: "123456" }, // @机器人(robotid)
|
|
276
|
+
{ type: "link", content: "[标题]https://..." },
|
|
277
|
+
{ type: "image", content: "<base64>" },
|
|
278
|
+
],
|
|
279
|
+
accountId: account.accountId,
|
|
280
|
+
replyTo: { // 可选,群聊引用回复
|
|
281
|
+
messageid: "...",
|
|
282
|
+
preview: "被引用的消息摘要",
|
|
283
|
+
imid: "...", // 发送者数字 ID
|
|
284
|
+
replytype: "2", // "1"=reply, "2"=quote
|
|
285
|
+
},
|
|
286
|
+
atOptions: { // 可选,群聊 @
|
|
287
|
+
atAll: false,
|
|
288
|
+
atUserIds: ["zhangsan", "lisi"],
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
### 5.4 消息撤回
|
|
294
|
+
|
|
295
|
+
```typescript
|
|
296
|
+
// 群消息撤回(需要 msgseqid)
|
|
297
|
+
await recallInfoflowGroupMessage({
|
|
298
|
+
account,
|
|
299
|
+
groupId: 12345678,
|
|
300
|
+
messageid: "消息ID",
|
|
301
|
+
msgseqid: "消息序号ID",
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// 私聊消息撤回(需要 appAgentId)
|
|
305
|
+
await recallInfoflowPrivateMessage({
|
|
306
|
+
account,
|
|
307
|
+
toUser: "zhangsan",
|
|
308
|
+
msgKey: "消息key",
|
|
309
|
+
});
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
**注意**:撤回群消息必须同时提供 `messageid` 和 `msgseqid`,`msgseqid` 通过 `utils/store/message-store.ts` 的 `findSentMessage` 查询。
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 6. 消息格式规范
|
|
317
|
+
|
|
318
|
+
### 6.1 入站消息字段提取
|
|
319
|
+
|
|
320
|
+
群消息结构嵌套较深,取字段需注意优先级:
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
// 发送者 ID:header 优先
|
|
324
|
+
const fromuser = String(header?.fromuserid ?? msgData.fromuserid ?? "");
|
|
325
|
+
|
|
326
|
+
// 发送者数字 imid(用于 reply-to)
|
|
327
|
+
const senderImid = msgData.fromid ?? header?.imid ?? header?.fromimid;
|
|
328
|
+
|
|
329
|
+
// 消息 ID(防重放 + 撤回)
|
|
330
|
+
const messageId = header?.messageid ?? header?.msgid ?? msgData.MsgId;
|
|
331
|
+
|
|
332
|
+
// GroupId
|
|
333
|
+
const rawGroupId = msgData.groupid ?? header?.groupid;
|
|
334
|
+
const groupid = typeof rawGroupId === "number" ? rawGroupId : Number(rawGroupId);
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### 6.2 Body 类型
|
|
338
|
+
|
|
339
|
+
入站群消息 body 为数组,每个 item 的 `type` 含义:
|
|
340
|
+
|
|
341
|
+
| type | 含义 |
|
|
342
|
+
|------|------|
|
|
343
|
+
| `"TEXT"` | 文本内容,取 `content` |
|
|
344
|
+
| `"AT"` | @mention,机器人有 `robotid`(number),人有 `userid`(string) |
|
|
345
|
+
| `"LINK"` | 链接,取 `label` 作为显示文本 |
|
|
346
|
+
| `"IMAGE"` | 图片,取 `downloadurl` 下载 |
|
|
347
|
+
| `"replyData"` | 引用回复,取 `content` 作为被引用消息文本 |
|
|
348
|
+
|
|
349
|
+
**提取消息文本时,AT 元素不计入 `mes`(CommandBody),但计入 `rawMes`(RawBody)**。
|
|
350
|
+
|
|
351
|
+
### 6.3 大整数处理
|
|
352
|
+
|
|
353
|
+
如流 API 返回的 `messageid` / `msgseqid` 为 64 位整数,超过 JS 安全整数范围。**必须用字符串保存**,不要用 `JSON.parse` 后的数字类型。使用 `extractIdFromRawJson` 从原始 JSON 字符串中提取:
|
|
354
|
+
|
|
355
|
+
```typescript
|
|
356
|
+
import { extractIdFromRawJson } from "../channel/outbound.js";
|
|
357
|
+
const messageId = extractIdFromRawJson(rawJsonText, "messageid");
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
---
|
|
361
|
+
|
|
362
|
+
## 7. 安全策略模块
|
|
363
|
+
|
|
364
|
+
所有安全判断集中在 `src/security/`,**不要在 handler 或 channel 层内联策略代码**。
|
|
365
|
+
|
|
366
|
+
### 7.1 接入控制(`dm-policy.ts`)
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
import { checkDmPolicy, checkGroupPolicy } from "../security/dm-policy.js";
|
|
370
|
+
|
|
371
|
+
// 私聊
|
|
372
|
+
const result = checkDmPolicy(account, fromuser);
|
|
373
|
+
if (!result.allowed) { /* 拒绝,发送无权限提示 */ return; }
|
|
374
|
+
|
|
375
|
+
// 群聊
|
|
376
|
+
const result = checkGroupPolicy(account, groupId, wasMentioned);
|
|
377
|
+
if (!result.allowed) {
|
|
378
|
+
if (result.reason === "allowlist-rejected" && result.wasMentioned) {
|
|
379
|
+
// 仅 @机器人时才发无权限提示,避免在无关群刷屏
|
|
380
|
+
}
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
### 7.2 群聊触发判断(`group-policy.ts`)
|
|
386
|
+
|
|
387
|
+
```typescript
|
|
388
|
+
import {
|
|
389
|
+
checkBotMentioned, // 机器人是否被 @
|
|
390
|
+
checkWatchMentioned, // watchMentions 中的人是否被 @
|
|
391
|
+
checkWatchRegex, // 消息内容是否匹配 watchRegex
|
|
392
|
+
extractMentionIds, // 提取所有非机器人 @mention
|
|
393
|
+
resolveGroupConfig, // 合并 account 级 + group 级配置
|
|
394
|
+
isWithinFollowUpWindow, // 是否在 follow-up 窗口内
|
|
395
|
+
recordGroupReply, // 记录机器人回复时间
|
|
396
|
+
} from "../security/group-policy.js";
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
### 7.3 条件回复 Prompt
|
|
400
|
+
|
|
401
|
+
当触发 watch/follow-up/proactive 时,向 LLM 注入特殊 `GroupSystemPrompt`,要求不确定时输出 `NO_REPLY`:
|
|
402
|
+
|
|
403
|
+
```typescript
|
|
404
|
+
import {
|
|
405
|
+
buildWatchMentionPrompt, // watchMentions 触发
|
|
406
|
+
buildWatchRegexPrompt, // watchRegex 触发
|
|
407
|
+
buildFollowUpPrompt, // follow-up 窗口触发
|
|
408
|
+
buildProactivePrompt, // proactive 模式
|
|
409
|
+
} from "../security/group-policy.js";
|
|
410
|
+
|
|
411
|
+
ctxPayload.GroupSystemPrompt = buildWatchMentionPrompt(matchedId);
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**LLM 输出 `NO_REPLY` 时,reply-dispatcher 不发送任何消息。**
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## 8. 工具与 Hook 开发
|
|
419
|
+
|
|
420
|
+
### 8.1 注册工具
|
|
421
|
+
|
|
422
|
+
在 `src/tools/index.ts` 中通过 `api.registerTool` 注册,工具在 `src/tools/actions/` 中实现:
|
|
423
|
+
|
|
424
|
+
```typescript
|
|
425
|
+
// src/tools/index.ts
|
|
426
|
+
export function registerInfoflowTools(api: OpenClawPluginApi): void {
|
|
427
|
+
api.registerTool({
|
|
428
|
+
name: "infoflow_send",
|
|
429
|
+
description: "...",
|
|
430
|
+
inputSchema: { /* JSON Schema */ },
|
|
431
|
+
handler: async (params, ctx) => { ... },
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
### 8.2 注册 Hook
|
|
437
|
+
|
|
438
|
+
```typescript
|
|
439
|
+
// src/tools/hooks/index.ts
|
|
440
|
+
export function registerInfoflowHooks(api: OpenClawPluginApi): void {
|
|
441
|
+
api.registerHook("before_agent_start", async (ctx) => {
|
|
442
|
+
// 在 LLM 开始前注入上下文
|
|
443
|
+
ctx.systemPrompt += "\n\n...";
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
### 8.3 ChannelMessageActionAdapter
|
|
449
|
+
|
|
450
|
+
`src/tools/actions/index.ts` 实现 `ChannelMessageActionAdapter`,拦截 message tool 的 `send` 和 `delete` 动作,添加如流特有的 @mention、reply-to、撤回逻辑。
|
|
451
|
+
|
|
452
|
+
新增动作时:
|
|
453
|
+
1. 在 `listActions()` 中声明
|
|
454
|
+
2. 在 `handleAction()` 中用 `if (action === "xxx")` 处理
|
|
455
|
+
3. 返回 `jsonResult({ ok, channel, ... })`
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## 9. 测试规范
|
|
460
|
+
|
|
461
|
+
### 9.1 测试框架
|
|
462
|
+
|
|
463
|
+
使用 **vitest**,配置在 `package.json`。
|
|
464
|
+
|
|
465
|
+
```bash
|
|
466
|
+
npm test # 运行全部测试
|
|
467
|
+
npm test -- --watch # 监听模式
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 9.2 目录规则
|
|
471
|
+
|
|
472
|
+
测试文件放在 `tests/` 下,**镜像 `src/` 结构**:
|
|
473
|
+
|
|
474
|
+
```
|
|
475
|
+
src/security/group-policy.ts
|
|
476
|
+
→ tests/security/group-policy.test.ts
|
|
477
|
+
|
|
478
|
+
src/channel/outbound.ts
|
|
479
|
+
→ tests/channel/outbound.test.ts
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### 9.3 Mock 规范
|
|
483
|
+
|
|
484
|
+
`vi.mock` 路径必须使用 `.js` 扩展名(ESM 规范):
|
|
485
|
+
|
|
486
|
+
```typescript
|
|
487
|
+
import { describe, it, expect, vi } from "vitest";
|
|
488
|
+
|
|
489
|
+
vi.mock("../../src/channel/outbound.js", () => ({
|
|
490
|
+
sendInfoflowMessage: vi.fn().mockResolvedValue({ ok: true }),
|
|
491
|
+
}));
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
### 9.4 测试要求
|
|
495
|
+
|
|
496
|
+
- **新增公共函数**必须有对应单测,核心逻辑覆盖率不低于 80%
|
|
497
|
+
- `security/` 中的策略函数(纯函数)要求覆盖所有分支
|
|
498
|
+
- 涉及网络请求的函数(`sendInfoflowMessage` 等)通过 mock 测试,不真实发请求
|
|
499
|
+
|
|
500
|
+
---
|
|
501
|
+
|
|
502
|
+
## 10. 开发工作流
|
|
503
|
+
|
|
504
|
+
### 10.1 本地开发
|
|
505
|
+
|
|
506
|
+
```bash
|
|
507
|
+
# 1. 安装依赖
|
|
508
|
+
npm install
|
|
509
|
+
|
|
510
|
+
# 2. 同步到 openclaw extensions 目录
|
|
511
|
+
rsync -av --delete . ~/.openclaw/extensions/infoflow/ \
|
|
512
|
+
--exclude node_modules --exclude dist --exclude .git
|
|
513
|
+
|
|
514
|
+
# 3. 重启 openclaw gateway(热加载)
|
|
515
|
+
pkill -f "openclaw" && nohup openclaw gateway > /tmp/openclaw.log 2>&1 &
|
|
516
|
+
|
|
517
|
+
# 4. 查看实时日志
|
|
518
|
+
tail -f /tmp/openclaw.log
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### 10.2 静态检查
|
|
522
|
+
|
|
523
|
+
项目无 `tsc` 可执行文件,使用 Python 脚本验证 import 路径:
|
|
524
|
+
|
|
525
|
+
```bash
|
|
526
|
+
python3 << 'EOF'
|
|
527
|
+
import os, re
|
|
528
|
+
src_root = "src"
|
|
529
|
+
errors = []
|
|
530
|
+
for dirpath, _, filenames in os.walk(src_root):
|
|
531
|
+
for fname in filenames:
|
|
532
|
+
if not fname.endswith(".ts"): continue
|
|
533
|
+
fpath = os.path.join(dirpath, fname)
|
|
534
|
+
with open(fpath) as f:
|
|
535
|
+
lines = f.readlines()
|
|
536
|
+
for i, line in enumerate(lines, 1):
|
|
537
|
+
m = re.search(r'from\s+"(\.\.?/[^"]+\.js)"', line)
|
|
538
|
+
if not m: continue
|
|
539
|
+
ts = os.path.normpath(os.path.join(dirpath, m.group(1).replace(".js", ".ts")))
|
|
540
|
+
if not os.path.exists(ts):
|
|
541
|
+
errors.append(f"{fpath}:{i}: {m.group(1)} NOT FOUND")
|
|
542
|
+
print("OK" if not errors else "\n".join(errors))
|
|
543
|
+
EOF
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
### 10.3 提交规范
|
|
547
|
+
|
|
548
|
+
- commit message 必须包含 iCafe 卡片 ID(格式:`so-cli-XXXXX`)
|
|
549
|
+
- 推送命令:`git push origin HEAD:refs/for/master`
|
|
550
|
+
- CR 地址格式:`http://icode.baidu.com/myreview/changes/c/baidu/hi/openclaw_infoflow/+/<id>`
|
|
551
|
+
|
|
552
|
+
```bash
|
|
553
|
+
git commit -m "feat: 添加 xxx 功能
|
|
554
|
+
|
|
555
|
+
so-cli-XXXXX
|
|
556
|
+
|
|
557
|
+
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>"
|
|
558
|
+
|
|
559
|
+
git push origin HEAD:refs/for/master
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### 10.4 import 规范
|
|
563
|
+
|
|
564
|
+
TypeScript 文件中 import 路径**必须使用 `.js` 扩展名**(ESM 运行时要求):
|
|
565
|
+
|
|
566
|
+
```typescript
|
|
567
|
+
// ✅ 正确
|
|
568
|
+
import { sendInfoflowMessage } from "../channel/outbound.js";
|
|
569
|
+
|
|
570
|
+
// ❌ 错误
|
|
571
|
+
import { sendInfoflowMessage } from "../channel/outbound";
|
|
572
|
+
```
|
|
573
|
+
|
|
574
|
+
---
|
|
575
|
+
|
|
576
|
+
## 11. 常见问题与注意事项
|
|
577
|
+
|
|
578
|
+
### 11.1 消息 ID 精度丢失
|
|
579
|
+
|
|
580
|
+
**问题**:如流消息 ID 为 64 位整数,`JSON.parse` 后精度丢失(超过 `Number.MAX_SAFE_INTEGER`)。
|
|
581
|
+
|
|
582
|
+
**解决**:使用 `extractIdFromRawJson(rawText, "messageid")` 从原始字符串中提取,以字符串形式存储和传递。
|
|
583
|
+
|
|
584
|
+
### 11.2 群消息撤回需要 msgseqid
|
|
585
|
+
|
|
586
|
+
**问题**:群消息撤回 API 要求同时提供 `messageid` 和 `msgseqid`,而 `msgseqid` 只在发送响应中返回。
|
|
587
|
+
|
|
588
|
+
**解决**:所有出站消息都通过 `utils/store/message-store.ts` 持久化(SQLite),撤回时用 `findSentMessage(accountId, messageId)` 查询。
|
|
589
|
+
|
|
590
|
+
### 11.3 HTTPS 强制
|
|
591
|
+
|
|
592
|
+
`ensureHttps(apiHost)` 会自动将 HTTP 升级为 HTTPS(localhost 除外)。配置 `apiHost` 时无论写 HTTP 还是 HTTPS,发请求都会用 HTTPS。
|
|
593
|
+
|
|
594
|
+
### 11.4 群消息 markdown 不支持 reply-to
|
|
595
|
+
|
|
596
|
+
`groupMessageFormat: "markdown"` 时,消息体为 markdown 格式,但如流 API 的 markdown 消息**不支持 reply-to(引用回复)**,reply-dispatcher 会自动忽略 `replyTo` 字段。
|
|
597
|
+
|
|
598
|
+
### 11.5 proactive / watchRegex 模式注意频率
|
|
599
|
+
|
|
600
|
+
`proactive` 模式下所有群消息都会进入 LLM 判断,对高频群建议搭配 `watchRegex` 过滤。LLM 返回 `NO_REPLY` 时插件不发送任何内容。
|
|
601
|
+
|
|
602
|
+
### 11.6 WebSocket 模式
|
|
603
|
+
|
|
604
|
+
设置 `connectionMode: "websocket"` 时需额外配置 `wsGateway` 域名。WebSocket 模式由 `adapter/inbound/ws-receiver.ts` 处理,与 Webhook 模式共享 `webhook-parser.ts` 的消息分发和 dedup 逻辑。
|
|
605
|
+
|
|
606
|
+
### 11.7 新增配置字段
|
|
607
|
+
|
|
608
|
+
如需新增配置字段,按以下步骤操作:
|
|
609
|
+
1. 在 `src/types.ts` 的 `InfoflowAccountConfig` 和 `ResolvedInfoflowAccount.config` 中同步添加
|
|
610
|
+
2. 在 `src/channel/accounts.ts` 的 `resolveInfoflowAccount` 中传递该字段
|
|
611
|
+
3. 如果是群粒度配置,同步更新 `InfoflowGroupConfig` 和 `security/group-policy.ts` 的 `resolveGroupConfig`
|
package/index.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { emptyPluginConfigSchema, type OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
|
+
import { infoflowPlugin } from "./src/channel/channel.js";
|
|
3
|
+
import { handleInfoflowWebhookRequest } from "./src/channel/monitor.js";
|
|
4
|
+
import { setInfoflowRuntime } from "./src/runtime.js";
|
|
5
|
+
import { registerInfoflowTools } from "./src/tools/index.js";
|
|
6
|
+
import { registerInfoflowHooks } from "./src/tools/hooks/index.js";
|
|
7
|
+
import { registerStoreListeners } from "./src/utils/store/message-store.js";
|
|
8
|
+
|
|
9
|
+
const plugin = {
|
|
10
|
+
id: "infoflow",
|
|
11
|
+
name: "Infoflow",
|
|
12
|
+
description: "OpenClaw Infoflow channel plugin",
|
|
13
|
+
configSchema: emptyPluginConfigSchema(),
|
|
14
|
+
register(api: OpenClawPluginApi) {
|
|
15
|
+
setInfoflowRuntime(api.runtime);
|
|
16
|
+
api.registerChannel({ plugin: infoflowPlugin });
|
|
17
|
+
api.registerHttpRoute({
|
|
18
|
+
path: "/webhook/infoflow",
|
|
19
|
+
auth: "plugin",
|
|
20
|
+
match: "exact",
|
|
21
|
+
handler: handleInfoflowWebhookRequest,
|
|
22
|
+
});
|
|
23
|
+
registerInfoflowTools(api);
|
|
24
|
+
registerInfoflowHooks(api);
|
|
25
|
+
registerStoreListeners();
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export default plugin;
|