timbot 2026.3.12-beta.2 → 2026.3.19-beta.1
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 +5 -0
- package/README.zh-CN.md +291 -0
- package/dist/src/config-text.d.ts +2 -0
- package/dist/src/config-text.d.ts.map +1 -0
- package/dist/src/config-text.js +11 -0
- package/dist/src/config-text.js.map +1 -0
- package/dist/src/inbound-routing.d.ts +12 -0
- package/dist/src/inbound-routing.d.ts.map +1 -0
- package/dist/src/inbound-routing.js +112 -0
- package/dist/src/inbound-routing.js.map +1 -0
- package/dist/src/monitor.d.ts.map +1 -1
- package/dist/src/monitor.js +53 -64
- package/dist/src/monitor.js.map +1 -1
- package/dist/src/sender.d.ts +4 -0
- package/dist/src/sender.d.ts.map +1 -0
- package/dist/src/sender.js +27 -0
- package/dist/src/sender.js.map +1 -0
- package/dist/src/stream-text.d.ts +9 -0
- package/dist/src/stream-text.d.ts.map +1 -0
- package/dist/src/stream-text.js +92 -0
- package/dist/src/stream-text.js.map +1 -0
- package/dist/src/text-splitter.d.ts +4 -0
- package/dist/src/text-splitter.d.ts.map +1 -1
- package/dist/src/text-splitter.js +228 -6
- package/dist/src/text-splitter.js.map +1 -1
- package/dist/src/types.d.ts +1 -0
- package/dist/src/types.d.ts.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,11 @@ Local testing, streaming mode selection, and webhook replay examples are documen
|
|
|
21
21
|
|
|
22
22
|
## Changelog
|
|
23
23
|
|
|
24
|
+
### 2026.3.12
|
|
25
|
+
|
|
26
|
+
- docs: 补充流式模式依赖上游 partial 输出的说明与可观测性日志
|
|
27
|
+
- chore: 添加调试配置命令与本地 webhook 测试脚本
|
|
28
|
+
|
|
24
29
|
### 2026.3.10
|
|
25
30
|
|
|
26
31
|
- feat: 流式消息支持,新增 `streamingMode` 配置(`off` / `text_modify` / `custom_modify` / `tim_stream`)
|
package/README.zh-CN.md
CHANGED
|
@@ -103,6 +103,297 @@ openclaw config set channels.timbot.overflowPolicy split
|
|
|
103
103
|
openclaw config set channels.timbot.typingText "思考中,请稍候..."
|
|
104
104
|
```
|
|
105
105
|
|
|
106
|
+
## 多 Agent 配置教程
|
|
107
|
+
|
|
108
|
+
timbot 支持在同一个腾讯 IM 应用下配置多个机器人账号,每个机器人绑定不同的 OpenClaw Agent,实现"不同会话 = 不同 AI 助手"的体验。
|
|
109
|
+
|
|
110
|
+
### 前置条件
|
|
111
|
+
|
|
112
|
+
- timbot >= 2026.3.12
|
|
113
|
+
- 单账号基础配置已完成(`sdkAppId` + `secretKey`)
|
|
114
|
+
- **在腾讯云 IM 控制台创建好多个机器人账号(`@RBT#001`、`@RBT#002` 等)**
|
|
115
|
+
|
|
116
|
+
> 每个 sdkAppId 最多支持 20 个 `@RBT#` 机器人账号。
|
|
117
|
+
|
|
118
|
+
### 原理概览
|
|
119
|
+
|
|
120
|
+
```
|
|
121
|
+
用户发消息给 @RBT#002
|
|
122
|
+
↓
|
|
123
|
+
腾讯 IM Webhook(To_Account = "@RBT#002")
|
|
124
|
+
↓
|
|
125
|
+
timbot 按 To_Account 匹配到 accountId = "translator"
|
|
126
|
+
↓
|
|
127
|
+
OpenClaw bindings 将 accountId 路由到 agentId = "translator"
|
|
128
|
+
↓
|
|
129
|
+
translator agent 的 workspace 处理消息并回复
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### 第一步:创建 Agent workspace
|
|
133
|
+
|
|
134
|
+
为每个 Agent 创建独立的 workspace:
|
|
135
|
+
|
|
136
|
+
```bash
|
|
137
|
+
openclaw agents add translator
|
|
138
|
+
openclaw agents add coder
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
每个 Agent 拥有独立的 `SOUL.md`(人设)、`AGENTS.md`(行为指令)、session 存储和 auth 配置。
|
|
142
|
+
|
|
143
|
+
### 第二步:配置 timbot 多账号
|
|
144
|
+
|
|
145
|
+
#### 方式 A:CLI 命令(推荐)
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# 设置默认账号
|
|
149
|
+
openclaw config set channels.timbot.defaultAccount default
|
|
150
|
+
|
|
151
|
+
# 为每个账号设置 botAccount
|
|
152
|
+
openclaw config set channels.timbot.accounts.default.botAccount "@RBT#001"
|
|
153
|
+
openclaw config set channels.timbot.accounts.translator.botAccount "@RBT#002"
|
|
154
|
+
openclaw config set channels.timbot.accounts.coder.botAccount "@RBT#003"
|
|
155
|
+
|
|
156
|
+
# 可按账号覆盖顶层配置(可选)
|
|
157
|
+
openclaw config set channels.timbot.accounts.coder.streamingMode tim_stream
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
也可以用 `--batch-json` 一次性批量设置:
|
|
161
|
+
|
|
162
|
+
```bash
|
|
163
|
+
openclaw config set --batch-json '[
|
|
164
|
+
{ "path": "channels.timbot.defaultAccount", "value": "default" },
|
|
165
|
+
{ "path": "channels.timbot.accounts.default.botAccount", "value": "@RBT#001" },
|
|
166
|
+
{ "path": "channels.timbot.accounts.translator.botAccount", "value": "@RBT#002" },
|
|
167
|
+
{ "path": "channels.timbot.accounts.coder.botAccount", "value": "@RBT#003" }
|
|
168
|
+
]'
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
#### 方式 B:手动编辑配置文件
|
|
172
|
+
|
|
173
|
+
编辑 `~/.openclaw/openclaw.json`:
|
|
174
|
+
|
|
175
|
+
```json5
|
|
176
|
+
{
|
|
177
|
+
channels: {
|
|
178
|
+
timbot: {
|
|
179
|
+
// 共享凭证
|
|
180
|
+
sdkAppId: "1600012345",
|
|
181
|
+
secretKey: "your-secret-key",
|
|
182
|
+
token: "webhook-token",
|
|
183
|
+
webhookPath: "/timbot",
|
|
184
|
+
|
|
185
|
+
// 顶层作为所有账号的默认值
|
|
186
|
+
streamingMode: "off",
|
|
187
|
+
dm: { policy: "open", allowFrom: ["*"] },
|
|
188
|
+
|
|
189
|
+
// 默认账号
|
|
190
|
+
defaultAccount: "default",
|
|
191
|
+
|
|
192
|
+
// 多账号配置
|
|
193
|
+
accounts: {
|
|
194
|
+
default: {
|
|
195
|
+
botAccount: "@RBT#001", // AI 助手
|
|
196
|
+
},
|
|
197
|
+
translator: {
|
|
198
|
+
botAccount: "@RBT#002", // 翻译官
|
|
199
|
+
},
|
|
200
|
+
coder: {
|
|
201
|
+
botAccount: "@RBT#003", // 代码助手
|
|
202
|
+
streamingMode: "tim_stream", // 可按账号覆盖
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
}
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
账号级字段会覆盖顶层同名字段,未指定的继承顶层默认值。`sdkAppId`、`secretKey` 等共享凭证只需在顶层写一次。
|
|
211
|
+
|
|
212
|
+
### 第三步:添加 bindings
|
|
213
|
+
|
|
214
|
+
bindings 将 timbot 的 `accountId` 映射到 OpenClaw 的 `agentId`。
|
|
215
|
+
|
|
216
|
+
#### 方式 A:CLI 命令(推荐)
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
# 将 timbot 的各账号绑定到对应 agent
|
|
220
|
+
openclaw agents bind --agent main --bind timbot:default
|
|
221
|
+
openclaw agents bind --agent translator --bind timbot:translator
|
|
222
|
+
openclaw agents bind --agent coder --bind timbot:coder
|
|
223
|
+
|
|
224
|
+
# 验证绑定关系
|
|
225
|
+
openclaw agents bindings
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
#### 方式 B:手动编辑配置文件
|
|
229
|
+
|
|
230
|
+
在 `~/.openclaw/openclaw.json` 中添加:
|
|
231
|
+
|
|
232
|
+
```json5
|
|
233
|
+
{
|
|
234
|
+
agents: {
|
|
235
|
+
list: [
|
|
236
|
+
{ id: "main", default: true, workspace: "~/.openclaw/workspace" },
|
|
237
|
+
{ id: "translator", workspace: "~/.openclaw/workspace-translator" },
|
|
238
|
+
{ id: "coder", workspace: "~/.openclaw/workspace-coder" },
|
|
239
|
+
],
|
|
240
|
+
},
|
|
241
|
+
|
|
242
|
+
bindings: [
|
|
243
|
+
{ agentId: "main", match: { channel: "timbot", accountId: "default" } },
|
|
244
|
+
{ agentId: "translator", match: { channel: "timbot", accountId: "translator" } },
|
|
245
|
+
{ agentId: "coder", match: { channel: "timbot", accountId: "coder" } },
|
|
246
|
+
],
|
|
247
|
+
|
|
248
|
+
channels: {
|
|
249
|
+
timbot: {
|
|
250
|
+
// ... 上一步的配置
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### 第四步:设置 Agent 人设
|
|
257
|
+
|
|
258
|
+
每个 Agent 的 workspace 下编辑 `SOUL.md` 定义人格:
|
|
259
|
+
|
|
260
|
+
```bash
|
|
261
|
+
# ~/.openclaw/workspace-translator/SOUL.md
|
|
262
|
+
echo "你是一位专业翻译官,擅长中英互译。请用简洁准确的风格翻译用户提供的内容。" \
|
|
263
|
+
> ~/.openclaw/workspace-translator/SOUL.md
|
|
264
|
+
|
|
265
|
+
# ~/.openclaw/workspace-coder/SOUL.md
|
|
266
|
+
echo "你是一位资深程序员,擅长代码审查、调试和编写。回复时附带代码示例。" \
|
|
267
|
+
> ~/.openclaw/workspace-coder/SOUL.md
|
|
268
|
+
```
|
|
269
|
+
|
|
270
|
+
### 第五步:重启并验证
|
|
271
|
+
|
|
272
|
+
```bash
|
|
273
|
+
# 重启 Gateway
|
|
274
|
+
openclaw gateway restart
|
|
275
|
+
|
|
276
|
+
# 检查 agents 和 bindings
|
|
277
|
+
openclaw agents list --bindings
|
|
278
|
+
|
|
279
|
+
# 检查通道状态
|
|
280
|
+
openclaw channels status --probe
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
### 按用户路由(可选)
|
|
284
|
+
|
|
285
|
+
如果只有一个机器人账号,但想把不同用户的消息路由到不同 Agent,可以用 peer 匹配:
|
|
286
|
+
|
|
287
|
+
```json5
|
|
288
|
+
{
|
|
289
|
+
bindings: [
|
|
290
|
+
// 指定用户 → translator agent
|
|
291
|
+
{
|
|
292
|
+
agentId: "translator",
|
|
293
|
+
match: { channel: "timbot", peer: { kind: "direct", id: "user_alice" } },
|
|
294
|
+
},
|
|
295
|
+
// 指定群 → coder agent
|
|
296
|
+
{
|
|
297
|
+
agentId: "coder",
|
|
298
|
+
match: { channel: "timbot", peer: { kind: "group", id: "@TGS#group001" } },
|
|
299
|
+
},
|
|
300
|
+
// 其余走默认
|
|
301
|
+
{ agentId: "main", match: { channel: "timbot" } },
|
|
302
|
+
],
|
|
303
|
+
}
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
peer 匹配优先级高于 accountId 匹配。更多路由规则见 [OpenClaw Multi-Agent 文档](https://docs.openclaw.ai/concepts/multi-agent)。
|
|
307
|
+
|
|
308
|
+
### 完整配置示例
|
|
309
|
+
|
|
310
|
+
```json5
|
|
311
|
+
{
|
|
312
|
+
agents: {
|
|
313
|
+
list: [
|
|
314
|
+
{
|
|
315
|
+
id: "main",
|
|
316
|
+
default: true,
|
|
317
|
+
name: "AI 助手",
|
|
318
|
+
workspace: "~/.openclaw/workspace",
|
|
319
|
+
},
|
|
320
|
+
{
|
|
321
|
+
id: "translator",
|
|
322
|
+
name: "翻译官",
|
|
323
|
+
workspace: "~/.openclaw/workspace-translator",
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
id: "coder",
|
|
327
|
+
name: "代码助手",
|
|
328
|
+
workspace: "~/.openclaw/workspace-coder",
|
|
329
|
+
model: "anthropic/claude-sonnet-4-5",
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
},
|
|
333
|
+
|
|
334
|
+
bindings: [
|
|
335
|
+
{ agentId: "main", match: { channel: "timbot", accountId: "default" } },
|
|
336
|
+
{ agentId: "translator", match: { channel: "timbot", accountId: "translator" } },
|
|
337
|
+
{ agentId: "coder", match: { channel: "timbot", accountId: "coder" } },
|
|
338
|
+
],
|
|
339
|
+
|
|
340
|
+
channels: {
|
|
341
|
+
timbot: {
|
|
342
|
+
sdkAppId: "1600012345",
|
|
343
|
+
secretKey: "your-secret-key",
|
|
344
|
+
token: "webhook-token",
|
|
345
|
+
webhookPath: "/timbot",
|
|
346
|
+
streamingMode: "tim_stream",
|
|
347
|
+
fallbackPolicy: "final_text",
|
|
348
|
+
dm: { policy: "open", allowFrom: ["*"] },
|
|
349
|
+
defaultAccount: "default",
|
|
350
|
+
accounts: {
|
|
351
|
+
default: {
|
|
352
|
+
botAccount: "@RBT#001",
|
|
353
|
+
},
|
|
354
|
+
translator: {
|
|
355
|
+
botAccount: "@RBT#002",
|
|
356
|
+
},
|
|
357
|
+
coder: {
|
|
358
|
+
botAccount: "@RBT#003",
|
|
359
|
+
},
|
|
360
|
+
},
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### 调试
|
|
367
|
+
|
|
368
|
+
```bash
|
|
369
|
+
# 前台运行,观察路由日志
|
|
370
|
+
openclaw gateway run --verbose --force
|
|
371
|
+
|
|
372
|
+
# 模拟向 @RBT#002 发消息
|
|
373
|
+
TS=$(date +%s) && RAND=$RANDOM
|
|
374
|
+
curl -sS 'http://127.0.0.1:18789/timbot' \
|
|
375
|
+
-H 'content-type: application/json' \
|
|
376
|
+
--data-binary "{
|
|
377
|
+
\"CallbackCommand\":\"Bot.OnC2CMessage\",
|
|
378
|
+
\"From_Account\":\"test_user\",
|
|
379
|
+
\"To_Account\":\"@RBT#002\",
|
|
380
|
+
\"MsgTime\":$TS,
|
|
381
|
+
\"MsgRandom\":$RAND,
|
|
382
|
+
\"MsgKey\":\"test_${TS}_${RAND}\",
|
|
383
|
+
\"MsgBody\":[{\"MsgType\":\"TIMTextElem\",\"MsgContent\":{\"Text\":\"翻译:hello world\"}}]
|
|
384
|
+
}"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
日志中应出现 `agentId=translator`,说明路由生效。
|
|
388
|
+
|
|
389
|
+
### 注意事项
|
|
390
|
+
|
|
391
|
+
- 每个 sdkAppId 最多 20 个 `@RBT#` 机器人账号
|
|
392
|
+
- `@RBT#` 机器人发送 C2C 消息不校验好友关系,但用户端会话列表需要机器人先主动发一条消息或通过 `friend_add` API 强制添加
|
|
393
|
+
- 体验版每日限 1000 条消息(所有机器人共享),生产环境需升级
|
|
394
|
+
- 当前 onboarding 向导不支持多账号交互配置,需手动编辑配置文件
|
|
395
|
+
- Agent 的 auth profile 是隔离的,如需共享 provider 凭证,将 `auth-profiles.json` 复制到对应 Agent 的 `agentDir`
|
|
396
|
+
|
|
106
397
|
## 常用命令速查
|
|
107
398
|
|
|
108
399
|
### Gateway 前台运行 + 日志
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-text.d.ts","sourceRoot":"","sources":["../../src/config-text.ts"],"names":[],"mappings":"AAAA,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAWxE"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function normalizeOptionalText(value) {
|
|
2
|
+
if (typeof value === "string") {
|
|
3
|
+
const trimmed = value.trim();
|
|
4
|
+
return trimmed || undefined;
|
|
5
|
+
}
|
|
6
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
7
|
+
return String(value);
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=config-text.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config-text.js","sourceRoot":"","sources":["../../src/config-text.ts"],"names":[],"mappings":"AAAA,MAAM,UAAU,qBAAqB,CAAC,KAAc;IAClD,IAAI,OAAO,KAAK,KAAK,QAAQ,EAAE,CAAC;QAC9B,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,OAAO,OAAO,IAAI,SAAS,CAAC;IAC9B,CAAC;IAED,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,EAAE,CAAC;QACxD,OAAO,MAAM,CAAC,KAAK,CAAC,CAAC;IACvB,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ResolvedTimbotAccount, TimbotInboundMessage, TimbotMsgBodyElement } from "./types.js";
|
|
2
|
+
export type TimbotWebhookRoutingTarget = {
|
|
3
|
+
account: Pick<ResolvedTimbotAccount, "configured" | "sdkAppId" | "botAccount" | "token">;
|
|
4
|
+
};
|
|
5
|
+
export declare function extractTextFromMsgBody(msgBody?: TimbotMsgBodyElement[]): string;
|
|
6
|
+
export declare function extractMentionedBotAccounts(rawBody: string): string[];
|
|
7
|
+
export declare function matchTimbotWebhookTargetsBySdkAppId<T extends TimbotWebhookRoutingTarget>(targets: T[], sdkAppId: string): T[];
|
|
8
|
+
export declare function selectTimbotWebhookTarget<T extends TimbotWebhookRoutingTarget>(params: {
|
|
9
|
+
targets: T[];
|
|
10
|
+
msg: TimbotInboundMessage;
|
|
11
|
+
}): T | undefined;
|
|
12
|
+
//# sourceMappingURL=inbound-routing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound-routing.d.ts","sourceRoot":"","sources":["../../src/inbound-routing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,qBAAqB,EACrB,oBAAoB,EACpB,oBAAoB,EACrB,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,0BAA0B,GAAG;IACvC,OAAO,EAAE,IAAI,CAAC,qBAAqB,EAAE,YAAY,GAAG,UAAU,GAAG,YAAY,GAAG,OAAO,CAAC,CAAC;CAC1F,CAAC;AA4BF,wBAAgB,sBAAsB,CAAC,OAAO,CAAC,EAAE,oBAAoB,EAAE,GAAG,MAAM,CA2B/E;AAED,wBAAgB,2BAA2B,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAerE;AAED,wBAAgB,mCAAmC,CAAC,CAAC,SAAS,0BAA0B,EACtF,OAAO,EAAE,CAAC,EAAE,EACZ,QAAQ,EAAE,MAAM,GACf,CAAC,EAAE,CAIL;AAED,wBAAgB,yBAAyB,CAAC,CAAC,SAAS,0BAA0B,EAAE,MAAM,EAAE;IACtF,OAAO,EAAE,CAAC,EAAE,CAAC;IACb,GAAG,EAAE,oBAAoB,CAAC;CAC3B,GAAG,CAAC,GAAG,SAAS,CAoChB"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
function normalizeBotAccount(raw) {
|
|
2
|
+
const trimmed = raw?.trim();
|
|
3
|
+
if (!trimmed)
|
|
4
|
+
return undefined;
|
|
5
|
+
return trimmed.replace(/^@/u, "@").toLowerCase();
|
|
6
|
+
}
|
|
7
|
+
function extractMentionedBotAccountsFromField(msg) {
|
|
8
|
+
const mentions = Array.isArray(msg.AtRobots_Account) ? msg.AtRobots_Account : [];
|
|
9
|
+
const normalizedMentions = [];
|
|
10
|
+
const seen = new Set();
|
|
11
|
+
for (const mention of mentions) {
|
|
12
|
+
const normalized = normalizeBotAccount(mention);
|
|
13
|
+
if (!normalized || seen.has(normalized))
|
|
14
|
+
continue;
|
|
15
|
+
seen.add(normalized);
|
|
16
|
+
normalizedMentions.push(mention.replace(/^@/u, "@"));
|
|
17
|
+
}
|
|
18
|
+
return normalizedMentions;
|
|
19
|
+
}
|
|
20
|
+
function isGroupMessage(msg) {
|
|
21
|
+
return msg.CallbackCommand === "Bot.OnGroupMessage" || Boolean(msg.GroupId?.trim());
|
|
22
|
+
}
|
|
23
|
+
// 从 MsgBody 提取文本内容
|
|
24
|
+
export function extractTextFromMsgBody(msgBody) {
|
|
25
|
+
if (!msgBody || !Array.isArray(msgBody))
|
|
26
|
+
return "";
|
|
27
|
+
const texts = [];
|
|
28
|
+
for (const elem of msgBody) {
|
|
29
|
+
if (elem.MsgType === "TIMTextElem" && elem.MsgContent?.Text) {
|
|
30
|
+
texts.push(elem.MsgContent.Text);
|
|
31
|
+
}
|
|
32
|
+
else if (elem.MsgType === "TIMCustomElem") {
|
|
33
|
+
texts.push("[custom]");
|
|
34
|
+
}
|
|
35
|
+
else if (elem.MsgType === "TIMImageElem") {
|
|
36
|
+
texts.push("[image]");
|
|
37
|
+
}
|
|
38
|
+
else if (elem.MsgType === "TIMSoundElem") {
|
|
39
|
+
texts.push("[voice]");
|
|
40
|
+
}
|
|
41
|
+
else if (elem.MsgType === "TIMFileElem") {
|
|
42
|
+
texts.push("[file]");
|
|
43
|
+
}
|
|
44
|
+
else if (elem.MsgType === "TIMVideoFileElem") {
|
|
45
|
+
texts.push("[video]");
|
|
46
|
+
}
|
|
47
|
+
else if (elem.MsgType === "TIMFaceElem") {
|
|
48
|
+
texts.push("[face]");
|
|
49
|
+
}
|
|
50
|
+
else if (elem.MsgType === "TIMLocationElem") {
|
|
51
|
+
texts.push("[location]");
|
|
52
|
+
}
|
|
53
|
+
else if (elem.MsgType === "TIMStreamElem") {
|
|
54
|
+
texts.push("[stream]");
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return texts.join("\n");
|
|
58
|
+
}
|
|
59
|
+
export function extractMentionedBotAccounts(rawBody) {
|
|
60
|
+
if (!rawBody.trim())
|
|
61
|
+
return [];
|
|
62
|
+
const matches = rawBody.match(/[@@]RBT#[A-Za-z0-9._-]+/giu) ?? [];
|
|
63
|
+
const mentions = [];
|
|
64
|
+
const seen = new Set();
|
|
65
|
+
for (const match of matches) {
|
|
66
|
+
const normalized = normalizeBotAccount(match);
|
|
67
|
+
if (!normalized || seen.has(normalized))
|
|
68
|
+
continue;
|
|
69
|
+
seen.add(normalized);
|
|
70
|
+
mentions.push(match.replace(/^@/u, "@"));
|
|
71
|
+
}
|
|
72
|
+
return mentions;
|
|
73
|
+
}
|
|
74
|
+
export function matchTimbotWebhookTargetsBySdkAppId(targets, sdkAppId) {
|
|
75
|
+
const trimmed = sdkAppId.trim();
|
|
76
|
+
if (!trimmed)
|
|
77
|
+
return [...targets];
|
|
78
|
+
return targets.filter((candidate) => candidate.account.sdkAppId === trimmed);
|
|
79
|
+
}
|
|
80
|
+
export function selectTimbotWebhookTarget(params) {
|
|
81
|
+
const configuredTargets = params.targets.filter((candidate) => candidate.account.configured);
|
|
82
|
+
if (configuredTargets.length === 0)
|
|
83
|
+
return undefined;
|
|
84
|
+
const toAccount = normalizeBotAccount(params.msg.To_Account);
|
|
85
|
+
if (toAccount) {
|
|
86
|
+
const directMatch = configuredTargets.find((candidate) => normalizeBotAccount(candidate.account.botAccount) === toAccount);
|
|
87
|
+
if (directMatch)
|
|
88
|
+
return directMatch;
|
|
89
|
+
}
|
|
90
|
+
if (configuredTargets.length === 1) {
|
|
91
|
+
return configuredTargets[0];
|
|
92
|
+
}
|
|
93
|
+
if (!isGroupMessage(params.msg)) {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
const mentionedAccounts = [
|
|
97
|
+
...extractMentionedBotAccountsFromField(params.msg),
|
|
98
|
+
...extractMentionedBotAccounts(extractTextFromMsgBody(params.msg.MsgBody)),
|
|
99
|
+
];
|
|
100
|
+
const seenMentions = new Set();
|
|
101
|
+
for (const mentionedAccount of mentionedAccounts) {
|
|
102
|
+
const normalizedMention = normalizeBotAccount(mentionedAccount);
|
|
103
|
+
if (!normalizedMention || seenMentions.has(normalizedMention))
|
|
104
|
+
continue;
|
|
105
|
+
seenMentions.add(normalizedMention);
|
|
106
|
+
const matched = configuredTargets.find((candidate) => normalizeBotAccount(candidate.account.botAccount) === normalizedMention);
|
|
107
|
+
if (matched)
|
|
108
|
+
return matched;
|
|
109
|
+
}
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=inbound-routing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"inbound-routing.js","sourceRoot":"","sources":["../../src/inbound-routing.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../../src/monitor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzE,OAAO,KAAK,EACV,qBAAqB,EAKtB,MAAM,YAAY,CAAC;
|
|
1
|
+
{"version":3,"file":"monitor.d.ts","sourceRoot":"","sources":["../../src/monitor.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAEjE,OAAO,KAAK,EAAE,cAAc,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AAEzE,OAAO,KAAK,EACV,qBAAqB,EAKtB,MAAM,YAAY,CAAC;AAsBpB,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChC,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACnC,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,OAAO,EAAE,qBAAqB,CAAC;IAC/B,MAAM,EAAE,cAAc,CAAC;IACvB,OAAO,EAAE,gBAAgB,CAAC;IAC1B,IAAI,EAAE,aAAa,CAAC;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAC;QAAC,cAAc,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;CACnF,CAAC;AAodF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE;IAC9C,OAAO,EAAE,qBAAqB,CAAC;IAC/B,SAAS,EAAE,MAAM,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAG/D;AAkFD,wBAAsB,sBAAsB,CAAC,MAAM,EAAE;IACnD,OAAO,EAAE,qBAAqB,CAAC;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B,GAAG,OAAO,CAAC;IAAE,EAAE,EAAE,OAAO,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CAGhF;AAmyCD,wBAAgB,2BAA2B,CAAC,MAAM,EAAE,mBAAmB,GAAG,MAAM,IAAI,CAWnF;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,GAClB,OAAO,CAAC,OAAO,CAAC,CAsIlB"}
|
package/dist/src/monitor.js
CHANGED
|
@@ -2,8 +2,8 @@ import { createHash, timingSafeEqual } from "node:crypto";
|
|
|
2
2
|
import { getTimbotRuntime } from "./runtime.js";
|
|
3
3
|
import { genTestUserSig } from "./debug/GenerateTestUserSig-es.js";
|
|
4
4
|
import { LOG_PREFIX, logSimple } from "./logger.js";
|
|
5
|
+
import { extractMentionedBotAccounts, extractTextFromMsgBody, matchTimbotWebhookTargetsBySdkAppId, selectTimbotWebhookTarget, } from "./inbound-routing.js";
|
|
5
6
|
import { allowsFinalTextRecovery, buildBotErrorPayload, buildBotStreamPayload, buildCustomMsgBody, buildStreamingMsgBody, buildTimStreamChunk, buildTimStreamMsgBody, buildTextMsgBody, } from "./streaming-policy.js";
|
|
6
|
-
import { splitTextByPreferredBreaks } from "./text-splitter.js";
|
|
7
7
|
const webhookTargets = new Map();
|
|
8
8
|
// ============ 日志工具(带 target) ============
|
|
9
9
|
/** 带 target 的日志(有 runtime 回调) */
|
|
@@ -149,6 +149,19 @@ function mergeStreamingText(previousText, nextText) {
|
|
|
149
149
|
function estimateMsgBodyBytes(msgBody) {
|
|
150
150
|
return Buffer.byteLength(JSON.stringify(msgBody), "utf8");
|
|
151
151
|
}
|
|
152
|
+
function splitTextByFixedLength(text, limit) {
|
|
153
|
+
if (!text) {
|
|
154
|
+
return [];
|
|
155
|
+
}
|
|
156
|
+
if (limit <= 0 || text.length <= limit) {
|
|
157
|
+
return [text];
|
|
158
|
+
}
|
|
159
|
+
const chunks = [];
|
|
160
|
+
for (let index = 0; index < text.length; index += limit) {
|
|
161
|
+
chunks.push(text.slice(index, index + limit));
|
|
162
|
+
}
|
|
163
|
+
return chunks;
|
|
164
|
+
}
|
|
152
165
|
function isMsgTooLongError(error) {
|
|
153
166
|
return /msg too long/i.test(error ?? "");
|
|
154
167
|
}
|
|
@@ -1229,42 +1242,6 @@ async function executeStreamingReply(params) {
|
|
|
1229
1242
|
target.statusSink?.({ lastOutboundAt: Date.now() });
|
|
1230
1243
|
}
|
|
1231
1244
|
}
|
|
1232
|
-
// 从 MsgBody 提取文本内容
|
|
1233
|
-
function extractTextFromMsgBody(msgBody) {
|
|
1234
|
-
if (!msgBody || !Array.isArray(msgBody))
|
|
1235
|
-
return "";
|
|
1236
|
-
const texts = [];
|
|
1237
|
-
for (const elem of msgBody) {
|
|
1238
|
-
if (elem.MsgType === "TIMTextElem" && elem.MsgContent?.Text) {
|
|
1239
|
-
texts.push(elem.MsgContent.Text);
|
|
1240
|
-
}
|
|
1241
|
-
else if (elem.MsgType === "TIMCustomElem") {
|
|
1242
|
-
texts.push("[custom]");
|
|
1243
|
-
}
|
|
1244
|
-
else if (elem.MsgType === "TIMImageElem") {
|
|
1245
|
-
texts.push("[image]");
|
|
1246
|
-
}
|
|
1247
|
-
else if (elem.MsgType === "TIMSoundElem") {
|
|
1248
|
-
texts.push("[voice]");
|
|
1249
|
-
}
|
|
1250
|
-
else if (elem.MsgType === "TIMFileElem") {
|
|
1251
|
-
texts.push("[file]");
|
|
1252
|
-
}
|
|
1253
|
-
else if (elem.MsgType === "TIMVideoFileElem") {
|
|
1254
|
-
texts.push("[video]");
|
|
1255
|
-
}
|
|
1256
|
-
else if (elem.MsgType === "TIMFaceElem") {
|
|
1257
|
-
texts.push("[face]");
|
|
1258
|
-
}
|
|
1259
|
-
else if (elem.MsgType === "TIMLocationElem") {
|
|
1260
|
-
texts.push("[location]");
|
|
1261
|
-
}
|
|
1262
|
-
else if (elem.MsgType === "TIMStreamElem") {
|
|
1263
|
-
texts.push("[stream]");
|
|
1264
|
-
}
|
|
1265
|
-
}
|
|
1266
|
-
return texts.join("\n");
|
|
1267
|
-
}
|
|
1268
1245
|
// 处理消息并回复
|
|
1269
1246
|
async function processAndReply(params) {
|
|
1270
1247
|
const { target, msg } = params;
|
|
@@ -1347,7 +1324,7 @@ async function processAndReply(params) {
|
|
|
1347
1324
|
const finalTextChunkLimit = core.channel.text.resolveTextChunkLimit(config, "timbot", account.accountId, {
|
|
1348
1325
|
fallbackLimit: TIMBOT_FINAL_TEXT_CHUNK_LIMIT,
|
|
1349
1326
|
});
|
|
1350
|
-
const splitFinalText = (text) =>
|
|
1327
|
+
const splitFinalText = (text) => splitTextByFixedLength(text, finalTextChunkLimit);
|
|
1351
1328
|
logVerbose(target, `开始生成回复 -> ${fromAccount}`);
|
|
1352
1329
|
logVerbose(target, `转发给 OpenClaw: RawBody=${rawBody.slice(0, 100)}, SessionKey=${ctxPayload.SessionKey}, From=${ctxPayload.From}`);
|
|
1353
1330
|
// C2C 传输适配器
|
|
@@ -1486,6 +1463,7 @@ async function processGroupAndReply(params) {
|
|
|
1486
1463
|
MessageSid: msg.MsgKey ?? String(msg.MsgSeq ?? ""),
|
|
1487
1464
|
OriginatingChannel: "timbot",
|
|
1488
1465
|
OriginatingTo: `timbot:group:${groupId}`,
|
|
1466
|
+
WasMentioned: true,
|
|
1489
1467
|
});
|
|
1490
1468
|
await core.channel.session.recordInboundSession({
|
|
1491
1469
|
storePath,
|
|
@@ -1503,7 +1481,7 @@ async function processGroupAndReply(params) {
|
|
|
1503
1481
|
const finalTextChunkLimit = core.channel.text.resolveTextChunkLimit(config, "timbot", account.accountId, {
|
|
1504
1482
|
fallbackLimit: TIMBOT_FINAL_TEXT_CHUNK_LIMIT,
|
|
1505
1483
|
});
|
|
1506
|
-
const splitFinalText = (text) =>
|
|
1484
|
+
const splitFinalText = (text) => splitTextByFixedLength(text, finalTextChunkLimit);
|
|
1507
1485
|
logVerbose(target, `开始生成群回复 -> group:${groupId}`);
|
|
1508
1486
|
// Group 传输适配器
|
|
1509
1487
|
const transport = {
|
|
@@ -1590,7 +1568,6 @@ export async function handleTimbotWebhookRequest(req, res) {
|
|
|
1590
1568
|
const targets = webhookTargets.get(path);
|
|
1591
1569
|
if (!targets || targets.length === 0)
|
|
1592
1570
|
return false;
|
|
1593
|
-
const firstTarget = targets[0];
|
|
1594
1571
|
// 只处理 POST 请求
|
|
1595
1572
|
if (req.method !== "POST") {
|
|
1596
1573
|
logSimple("warn", `收到非 POST 请求: ${req.method} ${path}`);
|
|
@@ -1613,27 +1590,16 @@ export async function handleTimbotWebhookRequest(req, res) {
|
|
|
1613
1590
|
return true;
|
|
1614
1591
|
}
|
|
1615
1592
|
const msg = bodyResult.value;
|
|
1616
|
-
|
|
1617
|
-
const
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
if (sdkAppId && candidate.account.sdkAppId !== sdkAppId)
|
|
1622
|
-
return false;
|
|
1623
|
-
// 如果配置了 botAccount,校验 To_Account 是否匹配
|
|
1624
|
-
if (candidate.account.botAccount && msg.To_Account) {
|
|
1625
|
-
return candidate.account.botAccount === msg.To_Account;
|
|
1626
|
-
}
|
|
1627
|
-
return true;
|
|
1628
|
-
}) ?? firstTarget;
|
|
1629
|
-
if (!target.account.configured) {
|
|
1630
|
-
logSimple("warn", `账号 ${target.account.accountId} 未配置,跳过处理`);
|
|
1631
|
-
// 即使未配置也返回成功,避免腾讯 IM 重试
|
|
1632
|
-
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|
|
1633
|
-
return true;
|
|
1593
|
+
logSimple("info", `webhook 消息详情: callback=${msg.CallbackCommand || "-"}, To_Account=${msg.To_Account || "-"}, AtRobots_Account=${JSON.stringify(msg.AtRobots_Account ?? [])}, From_Account=${msg.From_Account || "-"}, GroupId=${msg.GroupId || "-"}`);
|
|
1594
|
+
const sdkMatchedTargets = matchTimbotWebhookTargetsBySdkAppId(targets, sdkAppId);
|
|
1595
|
+
let target = selectTimbotWebhookTarget({ targets: sdkMatchedTargets, msg });
|
|
1596
|
+
if (!target && sdkMatchedTargets.length === 1) {
|
|
1597
|
+
target = sdkMatchedTargets[0];
|
|
1634
1598
|
}
|
|
1635
1599
|
// 签名验证
|
|
1636
|
-
|
|
1600
|
+
const signatureTargets = (target ? [target] : sdkMatchedTargets)
|
|
1601
|
+
.filter((candidate) => candidate.account.configured && candidate.account.token);
|
|
1602
|
+
if (signatureTargets.length > 0) {
|
|
1637
1603
|
// 1. 超时校验:RequestTime 与当前时间相差超过 60 秒则拒绝
|
|
1638
1604
|
const requestTimestamp = parseInt(requestTime, 10);
|
|
1639
1605
|
const nowTimestamp = Math.floor(Date.now() / 1000);
|
|
@@ -1645,18 +1611,41 @@ export async function handleTimbotWebhookRequest(req, res) {
|
|
|
1645
1611
|
return true;
|
|
1646
1612
|
}
|
|
1647
1613
|
// 2. 签名校验:sha256(token + requestTime)
|
|
1648
|
-
const
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1614
|
+
const verifiedTargets = signatureTargets.filter((candidate) => {
|
|
1615
|
+
const expectedSign = createHash("sha256")
|
|
1616
|
+
.update(candidate.account.token + requestTime)
|
|
1617
|
+
.digest("hex");
|
|
1618
|
+
return (sign.length === expectedSign.length
|
|
1619
|
+
&& timingSafeEqual(Buffer.from(sign), Buffer.from(expectedSign)));
|
|
1620
|
+
});
|
|
1621
|
+
if (verifiedTargets.length === 0) {
|
|
1622
|
+
const expectedSign = createHash("sha256")
|
|
1623
|
+
.update(signatureTargets[0].account.token + requestTime)
|
|
1624
|
+
.digest("hex");
|
|
1653
1625
|
logSimple("error", `签名验证失败: 收到=${sign.slice(0, 16)}..., 预期=${expectedSign.slice(0, 16)}...`);
|
|
1654
1626
|
res.statusCode = 403;
|
|
1655
1627
|
res.end("Signature verification failed");
|
|
1656
1628
|
return true;
|
|
1657
1629
|
}
|
|
1630
|
+
if (!target && verifiedTargets.length === 1) {
|
|
1631
|
+
target = verifiedTargets[0];
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
if (!target) {
|
|
1635
|
+
const callbackCommand = msg.CallbackCommand ?? "";
|
|
1636
|
+
const mentions = extractMentionedBotAccounts(extractTextFromMsgBody(msg.MsgBody));
|
|
1637
|
+
logSimple("warn", `未能唯一匹配 webhook 账号,跳过处理: callback=${callbackCommand || "-"}, sdkAppId=${sdkAppId || "-"}, to=${msg.To_Account?.trim() || "-"}, group=${msg.GroupId?.trim() || "-"}, mentions=${mentions.join(",") || "-"}`);
|
|
1638
|
+
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|
|
1639
|
+
return true;
|
|
1640
|
+
}
|
|
1641
|
+
if (!target.account.configured) {
|
|
1642
|
+
logSimple("warn", `账号 ${target.account.accountId} 未配置,跳过处理`);
|
|
1643
|
+
// 即使未配置也返回成功,避免腾讯 IM 重试
|
|
1644
|
+
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|
|
1645
|
+
return true;
|
|
1658
1646
|
}
|
|
1659
1647
|
target.statusSink?.({ lastInboundAt: Date.now() });
|
|
1648
|
+
logSimple("info", `匹配到账号: ${target.account.accountId}, botAccount=${target.account.botAccount || "-"}`);
|
|
1660
1649
|
const callbackCommand = msg.CallbackCommand ?? "";
|
|
1661
1650
|
// 立即返回成功响应给腾讯 IM
|
|
1662
1651
|
jsonOk(res, { ActionStatus: "OK", ErrorCode: 0, ErrorInfo: "" });
|