@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,429 @@
|
|
|
1
|
+
# Infoflow OpenClaw 插件架构说明
|
|
2
|
+
|
|
3
|
+
## 一、整体分层
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
┌─────────────────────────────────────────────────────────────────┐
|
|
7
|
+
│ 如流服务端 / WS Gateway │
|
|
8
|
+
└──────────────────────┬────────────────────┬──────────────────────┘
|
|
9
|
+
│ HTTP POST │ WebSocket 长连接
|
|
10
|
+
─────────▼───────── ─────────▼─────────
|
|
11
|
+
│ Webhook 接入层 │ │ WebSocket 接入层 │ ← 【区分层】
|
|
12
|
+
│ webhook-parser │ │ ws-receiver │
|
|
13
|
+
─────────┬─────── ──────────┬────────────
|
|
14
|
+
│ │
|
|
15
|
+
▼ 汇合点(复用) ▼
|
|
16
|
+
┌──────────────────────────────────────┐
|
|
17
|
+
│ 统一消息处理层 │ ← 【复用层】
|
|
18
|
+
│ message-handler.ts │
|
|
19
|
+
│ (私聊/群聊路由、权限、replyMode 门控) │
|
|
20
|
+
└──────────────────┬───────────────────┘
|
|
21
|
+
│
|
|
22
|
+
┌──────────────────▼───────────────────┐
|
|
23
|
+
│ LLM 调度层(OpenClaw core) │ ← 【复用层】
|
|
24
|
+
│ dispatchReplyWithBufferedBlock... │
|
|
25
|
+
└──────────────────┬───────────────────┘
|
|
26
|
+
│
|
|
27
|
+
┌──────────────────▼───────────────────┐
|
|
28
|
+
│ 统一发送层 │ ← 【复用层】
|
|
29
|
+
│ reply-dispatcher.ts → send.ts │
|
|
30
|
+
│ (文本分片、@处理、引用回复、HTTP POST) │
|
|
31
|
+
└──────────────────────────────────────┘
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## 二、Webhook vs WebSocket:区别在哪里
|
|
37
|
+
|
|
38
|
+
### 区别点(仅在接入层)
|
|
39
|
+
|
|
40
|
+
| 对比项 | Webhook (`webhook-parser.ts`) | WebSocket (`ws-receiver.ts`) |
|
|
41
|
+
|--------|-------------------------------|-------------------------------|
|
|
42
|
+
| 连接方向 | 如流服务器主动 POST 到本机 | 插件主动连接 WS Gateway |
|
|
43
|
+
| 私聊消息格式 | form-urlencoded + `Encrypt` 密文 | SDK 归一化后直接拿到明文 |
|
|
44
|
+
| 群聊消息格式 | text/plain,rawBody 是 AES 密文 | SDK 归一化后的 JSON body |
|
|
45
|
+
| 需要解密 | ✅ AES-ECB 解密(encodingAESKey) | ❌ 不需要,WS 握手时已鉴权 |
|
|
46
|
+
| echostr 验签 | ✅ 首次验签(MD5) | ❌ 无 |
|
|
47
|
+
| 图片字段来源 | 解密后 JSON 里的 `PicUrl` | SDK bug 丢失字段,需 monkey-patch `originalMessage.PicUrl` |
|
|
48
|
+
| 响应方式 | fire-and-forget,立即返回 200 | async/await,事件回调中处理 |
|
|
49
|
+
|
|
50
|
+
**两者汇合在同一个函数**:
|
|
51
|
+
- `handlePrivateChatMessage({ cfg, msgData, accountId })`
|
|
52
|
+
- `handleGroupChatMessage({ cfg, msgData, accountId })`
|
|
53
|
+
|
|
54
|
+
两条路径都将各自的消息格式整形为统一的 `msgData` 结构后,调用同一入口。
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## 三、三种消息类型的完整收发流程
|
|
59
|
+
|
|
60
|
+
### 3.1 普通文本消息
|
|
61
|
+
|
|
62
|
+
#### 收(以群聊为例)
|
|
63
|
+
|
|
64
|
+
**Webhook 路径**:
|
|
65
|
+
```
|
|
66
|
+
POST /webhook/infoflow
|
|
67
|
+
Content-Type: text/plain
|
|
68
|
+
Body: <AES密文>
|
|
69
|
+
│
|
|
70
|
+
▼ webhook-parser.ts: tryDecryptAndDispatch
|
|
71
|
+
AES-ECB 解密 → JSON.parse
|
|
72
|
+
{
|
|
73
|
+
eventtype: "MESSAGE_RECEIVE",
|
|
74
|
+
groupid: "12600364",
|
|
75
|
+
message: {
|
|
76
|
+
header: { fromuserid: "zhangsan", msgtype: "mixed", at: { atrobotids: [1234] } },
|
|
77
|
+
body: [
|
|
78
|
+
{ type: "AT", name: "龙虾", robotid: 1234 },
|
|
79
|
+
{ type: "TEXT", content: "今天天气怎么样" }
|
|
80
|
+
]
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
│
|
|
84
|
+
▼ handleGroupChatMessage
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**WebSocket 路径**:
|
|
88
|
+
```
|
|
89
|
+
WS event: "group.text"
|
|
90
|
+
event.data = {
|
|
91
|
+
chatType: "group",
|
|
92
|
+
msgType: "mixed",
|
|
93
|
+
fromUserId: "zhangsan",
|
|
94
|
+
groupId: "12600364",
|
|
95
|
+
body: [
|
|
96
|
+
{ type: "AT", name: "龙虾", robotid: 1234 },
|
|
97
|
+
{ type: "TEXT", content: "今天天气怎么样" }
|
|
98
|
+
],
|
|
99
|
+
originalMessage: { header: { ... } }
|
|
100
|
+
}
|
|
101
|
+
│
|
|
102
|
+
▼ ws-receiver.ts: handleGroupEvent 重组为同样的 msgData 结构
|
|
103
|
+
▼ handleGroupChatMessage ← 与 webhook 走同一函数
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**之后(两条路径完全一致)**:
|
|
107
|
+
```
|
|
108
|
+
message-handler.ts:
|
|
109
|
+
1. 提取 bodyItems → mes = "今天天气怎么样"
|
|
110
|
+
2. checkBotMentioned(bodyItems, "龙虾") → true
|
|
111
|
+
3. replyMode="mention-only",@了机器人,触发 LLM
|
|
112
|
+
4. buildAgentMediaPayload(无图片) → ctxPayload
|
|
113
|
+
5. dispatchReplyWithBufferedBlock → LLM 推理
|
|
114
|
+
|
|
115
|
+
reply-dispatcher.ts:
|
|
116
|
+
6. LLM 输出 → chunkText (4000字切片)
|
|
117
|
+
7. sendInfoflowMessage({ to: "group:12600364", contents: [{ type: "markdown", content: "..." }] })
|
|
118
|
+
|
|
119
|
+
send.ts:
|
|
120
|
+
8. POST ${apiHost}/api/v1/robot/msg/groupmsgsend
|
|
121
|
+
body: { groupid, robotid, contents: [{ type: "markdown", content: "..." }] }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
### 3.2 Markdown 格式消息
|
|
127
|
+
|
|
128
|
+
Markdown 与纯文本的**收**流程完全相同,区别只在**发送侧**配置:
|
|
129
|
+
|
|
130
|
+
**配置决定格式**(`openclaw.json`):
|
|
131
|
+
```json
|
|
132
|
+
"channels": {
|
|
133
|
+
"infoflow": {
|
|
134
|
+
"dmMessageFormat": "markdown",
|
|
135
|
+
"groupMessageFormat": "text"
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
**发送时(message-handler.ts → reply-dispatcher.ts)**:
|
|
141
|
+
```typescript
|
|
142
|
+
// message-handler.ts 中:
|
|
143
|
+
messageFormat: isGroup
|
|
144
|
+
? (account.config.groupMessageFormat ?? "text") // "text"
|
|
145
|
+
: (account.config.dmMessageFormat ?? "text"), // "markdown"
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**群聊发送(text 格式)**:
|
|
149
|
+
```
|
|
150
|
+
send.ts: sendInfoflowGroupMessage
|
|
151
|
+
POST /api/v1/robot/msg/groupmsgsend
|
|
152
|
+
{
|
|
153
|
+
"groupid": "12600364",
|
|
154
|
+
"robotid": 1234,
|
|
155
|
+
"contents": [
|
|
156
|
+
{ "type": "text", "content": "今天天气晴,适合出门。" }
|
|
157
|
+
]
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**私聊发送(markdown 格式)**:
|
|
162
|
+
```
|
|
163
|
+
send.ts: sendInfoflowPrivateMessage
|
|
164
|
+
POST /api/v1/app/message/send
|
|
165
|
+
{
|
|
166
|
+
"tousers": ["zhangsan"],
|
|
167
|
+
"appid": "appKey",
|
|
168
|
+
"msgtype": "markdown",
|
|
169
|
+
"markdown": { "content": "## 天气预报\n今天**晴**,气温 25°C" }
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
### 3.3 图片消息
|
|
176
|
+
|
|
177
|
+
图片消息在 webhook 和 websocket 两条路径上有明显差异:
|
|
178
|
+
|
|
179
|
+
#### 收图片(私聊)
|
|
180
|
+
|
|
181
|
+
**Webhook 路径**:
|
|
182
|
+
```
|
|
183
|
+
POST /webhook/infoflow(私聊,form-urlencoded)
|
|
184
|
+
AES 解密后 JSON:
|
|
185
|
+
{
|
|
186
|
+
"MsgType": "image",
|
|
187
|
+
"FromUserId": "zhangsan",
|
|
188
|
+
"PicUrl": "https://infoflow.baidu.com/xxxxx/image.jpg",
|
|
189
|
+
"MsgId": "12345678",
|
|
190
|
+
"CreateTime": "1234567890"
|
|
191
|
+
}
|
|
192
|
+
│
|
|
193
|
+
▼ handlePrivateChatMessage
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
**WebSocket 路径(存在 SDK Bug)**:
|
|
197
|
+
```
|
|
198
|
+
WS event: "private.image"
|
|
199
|
+
SDK 调用 normalizePrivateMessage(payload)
|
|
200
|
+
← SDK Bug: 丢失了 PicUrl / MsgId!
|
|
201
|
+
|
|
202
|
+
→ 插件 Monkey-patch 解决:
|
|
203
|
+
dispatcher.normalizePrivateMessage = function(payload) {
|
|
204
|
+
const normalized = original(payload);
|
|
205
|
+
normalized.originalMessage = payload; // 保留原始 payload
|
|
206
|
+
return normalized;
|
|
207
|
+
}
|
|
208
|
+
│
|
|
209
|
+
▼ ws-receiver.ts: handlePrivateEvent
|
|
210
|
+
msgData = {
|
|
211
|
+
FromUserId: data.fromUserId,
|
|
212
|
+
MsgType: "image",
|
|
213
|
+
PicUrl: data.picUrl ?? originalMessage.PicUrl, ← 从 originalMessage 兜底
|
|
214
|
+
MsgId: data.msgId ?? originalMessage.MsgId,
|
|
215
|
+
...
|
|
216
|
+
}
|
|
217
|
+
│
|
|
218
|
+
▼ handlePrivateChatMessage ← 与 webhook 走同一函数
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**之后(两条路径完全一致,图片下载 + 注入 LLM)**:
|
|
222
|
+
```
|
|
223
|
+
message-handler.ts:
|
|
224
|
+
1. MsgType === "image",有 PicUrl
|
|
225
|
+
2. fetchRemoteMedia(PicUrl, allowedHostnames: ["infoflow.baidu.com"])
|
|
226
|
+
← SSRF 防护:只允许如流域名
|
|
227
|
+
3. saveMediaBuffer → 写入临时文件
|
|
228
|
+
4. buildAgentMediaPayload([{ path: tmpFile, contentType: "image/jpeg" }])
|
|
229
|
+
→ { MediaPath, MediaPaths, MediaTypes }
|
|
230
|
+
5. ctxPayload 携带 mediaPayload → LLM 可看到图片内容(多模态)
|
|
231
|
+
6. LLM 识别图片 → 回复文字结果
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
#### 收图片(群聊)
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
群消息 body item 中有 IMAGE 类型:
|
|
238
|
+
body: [
|
|
239
|
+
{ type: "AT", name: "龙虾", robotid: 1234 },
|
|
240
|
+
{ type: "IMAGE", downloadurl: "https://infoflow.baidu.com/xxx.jpg" }
|
|
241
|
+
]
|
|
242
|
+
│
|
|
243
|
+
▼ message-handler.ts: 遍历 bodyItems
|
|
244
|
+
imageItems = body.filter(item => item.type === "IMAGE")
|
|
245
|
+
imageItems.forEach(item => downloadUrl = item.downloadurl)
|
|
246
|
+
│ 下载 + 注入(与私聊图片处理相同)
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### 发图片(LLM 调用工具 `infoflow_send`)
|
|
250
|
+
|
|
251
|
+
```
|
|
252
|
+
tools/index.ts: infoflow_send 工具(LLM function calling)
|
|
253
|
+
{
|
|
254
|
+
"to": "group:12600364",
|
|
255
|
+
"message": ""
|
|
256
|
+
}
|
|
257
|
+
│ OR 通过 outbound/media.ts 发送原生图片
|
|
258
|
+
▼
|
|
259
|
+
send.ts: sendInfoflowGroupMessage
|
|
260
|
+
- LINK/IMAGE 类型 contents 必须单独发送(API 限制)
|
|
261
|
+
- contents: [{ type: "image", url: "https://..." }]
|
|
262
|
+
|
|
263
|
+
POST /api/v1/robot/msg/groupmsgsend
|
|
264
|
+
{
|
|
265
|
+
"groupid": "12600364",
|
|
266
|
+
"robotid": 1234,
|
|
267
|
+
"contents": [{ "type": "image", "url": "https://..." }]
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
## 四、如流 → OpenClaw(入方向)完整路线
|
|
274
|
+
|
|
275
|
+
```
|
|
276
|
+
┌─────────────────────────────────────────────────────────────────────┐
|
|
277
|
+
│ 如流服务端 │
|
|
278
|
+
└──────────────────┬────────────────────────────┬──────────────────────┘
|
|
279
|
+
│ HTTP POST /webhook/infoflow │ WebSocket 长连接推送
|
|
280
|
+
│ │
|
|
281
|
+
─────────────▼────────── ───────▼────────────
|
|
282
|
+
│ 【适配层 ①】Webhook │ │ 【适配层 ①】WebSocket │
|
|
283
|
+
│ monitor.ts │ │ ws-receiver.ts │
|
|
284
|
+
│ - 路由注册与分发 │ │ - SDK WSClient 封装 │
|
|
285
|
+
│ - loadRawBody (≤20MB) │ │ - 监听 group.* / │
|
|
286
|
+
│ - 405/413 错误处理 │ │ private.* 事件 │
|
|
287
|
+
─────────────┬────────── ───────┬────────────
|
|
288
|
+
│ │
|
|
289
|
+
─────────────▼────────── │
|
|
290
|
+
│ 【适配层 ②】解密解析 │ │
|
|
291
|
+
│ webhook-parser.ts │ │
|
|
292
|
+
│ 私聊: │ │
|
|
293
|
+
│ form-urlencoded │ │
|
|
294
|
+
│ → JSON.parse(Encrypt) │ │
|
|
295
|
+
│ → AES-ECB 解密 │ │
|
|
296
|
+
│ 群聊: │ ───────▼────────────
|
|
297
|
+
│ text/plain (密文) │ │ 【适配层 ②】格式整形 │
|
|
298
|
+
│ → AES-ECB 解密 │ │ ws-receiver.ts │
|
|
299
|
+
│ echostr → MD5 验签 │ │ 重组成统一 msgData: │
|
|
300
|
+
─────────────┬────────── │ │ { FromUserId, │
|
|
301
|
+
│ │ │ groupid, │
|
|
302
|
+
│ │ │ message.body, │
|
|
303
|
+
│ │ │ PicUrl(兜底fix), .. }│
|
|
304
|
+
│ ────────┘ │
|
|
305
|
+
│ │
|
|
306
|
+
└──────────────┬───────────────────────┘
|
|
307
|
+
│ 统一 msgData 结构(两路完全对齐)
|
|
308
|
+
│
|
|
309
|
+
══════════════▼══════════════════════════
|
|
310
|
+
║ 【复用层 A】去重过滤 ║
|
|
311
|
+
║ isDuplicateMessage() ║
|
|
312
|
+
║ (webhook-parser 定义,ws-receiver 复用)║
|
|
313
|
+
║ key: messageid / clientmsgid / ║
|
|
314
|
+
║ fromuserid_groupid_ctime ║
|
|
315
|
+
╚══════════════╤══════════════════════════╝
|
|
316
|
+
│
|
|
317
|
+
══════════════▼══════════════════════════
|
|
318
|
+
║ 【复用层 B】统一消息处理 ║
|
|
319
|
+
║ message-handler.ts ║
|
|
320
|
+
║ ║
|
|
321
|
+
║ handlePrivateChatMessage ║
|
|
322
|
+
║ ↓ 字段提取(兼容大小写变体) ║
|
|
323
|
+
║ ↓ dmPolicy 权限校验 ║
|
|
324
|
+
║ ↓ MsgType=image → 下载图片 ║
|
|
325
|
+
║ ║
|
|
326
|
+
║ handleGroupChatMessage ║
|
|
327
|
+
║ ↓ 解析 bodyItems(TEXT/AT/IMAGE/LINK)║
|
|
328
|
+
║ ↓ groupPolicy 权限校验 ║
|
|
329
|
+
║ ↓ checkBotMentioned(@机器人检测) ║
|
|
330
|
+
║ ↓ replyMode 门控 ║
|
|
331
|
+
║ (ignore/record/mention-only/ ║
|
|
332
|
+
║ mention-and-watch/proactive) ║
|
|
333
|
+
║ ↓ 下载 IMAGE body item 图片 ║
|
|
334
|
+
║ ║
|
|
335
|
+
║ 共用: buildAgentMediaPayload → ctxPayload ║
|
|
336
|
+
╚══════════════╤══════════════════════════╝
|
|
337
|
+
│
|
|
338
|
+
══════════════▼══════════════════════════
|
|
339
|
+
║ 【复用层 C】OpenClaw Core LLM ║
|
|
340
|
+
║ dispatchReplyWithBufferedBlock... ║
|
|
341
|
+
║ (OpenClaw 内置,插件不感知细节) ║
|
|
342
|
+
╚═════════════════════════════════════════╝
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## 五、OpenClaw → 如流(出方向)完整路线
|
|
348
|
+
|
|
349
|
+
```
|
|
350
|
+
╔═════════════════════════════════════════╗
|
|
351
|
+
║ 【复用层 C】OpenClaw Core LLM ║
|
|
352
|
+
║ LLM 流式输出 → ReplyPayload ║
|
|
353
|
+
║ { text, mediaUrl?, mediaUrls? } ║
|
|
354
|
+
╚══════════════╤══════════════════════════╝
|
|
355
|
+
│
|
|
356
|
+
══════════════▼══════════════════════════
|
|
357
|
+
║ 【复用层 D】回复调度器 ║
|
|
358
|
+
║ reply-dispatcher.ts ║
|
|
359
|
+
║ ║
|
|
360
|
+
║ 1. LLM 输出中解析 @id 模式 ║
|
|
361
|
+
║ → resolvedUserIds / agentIds ║
|
|
362
|
+
║ 2. 合并 atOptions(回 @发送者) ║
|
|
363
|
+
║ 3. chunkText 4000字分片 ║
|
|
364
|
+
║ 4. 仅首片附 replyTo(引用回复) ║
|
|
365
|
+
║ 注: markdown 格式禁用 replyTo ║
|
|
366
|
+
║ 5. mediaUrl → prepareInfoflowImageBase64║
|
|
367
|
+
║ 失败则降级为 link 类型 ║
|
|
368
|
+
╚══════════════╤══════════════════════════╝
|
|
369
|
+
│ contents[] 构建完成
|
|
370
|
+
│
|
|
371
|
+
══════════════▼══════════════════════════
|
|
372
|
+
║ 【复用层 E】统一发送入口 ║
|
|
373
|
+
║ send.ts: sendInfoflowMessage ║
|
|
374
|
+
║ ║
|
|
375
|
+
║ 解析 to: ║
|
|
376
|
+
║ "group:12600364" → sendGroup ║
|
|
377
|
+
║ "zhangsan" → sendPrivate ║
|
|
378
|
+
║ ║
|
|
379
|
+
║ 共用: getOrCreateAdapter() ║
|
|
380
|
+
║ → accessToken 缓存+自动刷新 ║
|
|
381
|
+
║ → recordSentMessageId (防自回环) ║
|
|
382
|
+
║ → recordSentMessage (备撤回) ║
|
|
383
|
+
╚══════════════╤══════════════════════════╝
|
|
384
|
+
┌─────────────┴────────────────┐
|
|
385
|
+
│ │
|
|
386
|
+
──────────────▼───────── ────────────▼──────────
|
|
387
|
+
│ 【适配层 ③】群聊发送 │ │ 【适配层 ③】私聊发送 │
|
|
388
|
+
│ sendInfoflowGroupMessage│ │ sendInfoflowPrivateMessage│
|
|
389
|
+
│ │ │ │
|
|
390
|
+
│ - contents[] 逐项处理 │ │ 按 messageFormat: │
|
|
391
|
+
│ - LINK/IMAGE 必须单独 │ │ "text" → │
|
|
392
|
+
│ 发送(API 限制) │ │ msgtype: "text" │
|
|
393
|
+
│ - AT 节点置首位 │ │ "markdown" → │
|
|
394
|
+
│ - replyTo 引用结构 │ │ msgtype: "markdown"│
|
|
395
|
+
│ │ │ - appid 字段 │
|
|
396
|
+
│ POST /api/v1/robot/msg/ │ │ │
|
|
397
|
+
│ groupmsgsend │ │ POST /api/v1/app/ │
|
|
398
|
+
──────────────┬────────── │ message/send │
|
|
399
|
+
│ ───────────┬───────────
|
|
400
|
+
└──────────────┬───────────────────┘
|
|
401
|
+
│
|
|
402
|
+
┌──────────────────────────────────▼──────────────────────────────────┐
|
|
403
|
+
│ 如流服务端 │
|
|
404
|
+
│ 消息投递到用户/群 │
|
|
405
|
+
└─────────────────────────────────────────────────────────────────────┘
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
---
|
|
409
|
+
|
|
410
|
+
## 六、复用 vs 适配 一览表
|
|
411
|
+
|
|
412
|
+
| 层级 | 文件 | 性质 | 说明 |
|
|
413
|
+
|------|------|------|------|
|
|
414
|
+
| 适配层 ① | `monitor.ts` | **Infoflow 专属** | Webhook HTTP 路由注册;WS Monitor 启动 |
|
|
415
|
+
| 适配层 ① | `ws-receiver.ts` | **Infoflow 专属** | SDK WSClient 封装,监听 group.*/private.* |
|
|
416
|
+
| 适配层 ② | `webhook-parser.ts` | **Infoflow 专属** | AES-ECB 解密、echostr 验签、Content-Type 分支 |
|
|
417
|
+
| 适配层 ② | `ws-receiver.ts` | **Infoflow 专属** | SDK 格式整形为统一 msgData;monkey-patch 修 SDK bug |
|
|
418
|
+
| **复用层 A** | `webhook-parser.ts` | **webhook+WS 共用** | `isDuplicateMessage()` 去重缓存,两条路径共享 |
|
|
419
|
+
| **复用层 B** | `message-handler.ts` | **webhook+WS 共用** | 权限校验、bodyItems 解析、replyMode、图片下载、LLM 调度 |
|
|
420
|
+
| **复用层 C** | OpenClaw Core | **所有通道共用** | LLM 推理、流式输出缓冲(插件不感知) |
|
|
421
|
+
| **复用层 D** | `reply-dispatcher.ts` | **所有消息类型共用** | @解析、4000字分片、引用回复、图片降级 |
|
|
422
|
+
| **复用层 E** | `send.ts` | **私聊+群聊共用** | Token 管理、防自回环、消息记录、统一入口 |
|
|
423
|
+
| 适配层 ③ | `send.ts` | **私聊/群聊各自适配** | 群聊:LINK/IMAGE 单独发、AT 节点结构;私聊:msgtype 字段、appid |
|
|
424
|
+
|
|
425
|
+
---
|
|
426
|
+
|
|
427
|
+
## 七、一句话总结
|
|
428
|
+
|
|
429
|
+
> **两条路(Webhook / WebSocket)只在「接入 + 解密」阶段有区别,整形成统一 `msgData` 结构后,完全走同一套逻辑**;发送侧在「群聊 / 私聊 API 适配」前也完全共用,差异只在最后一步的 HTTP Body 格式。
|