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 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,2 @@
1
+ export declare function normalizeOptionalText(value: unknown): string | undefined;
2
+ //# sourceMappingURL=config-text.d.ts.map
@@ -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;AAiBpB,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;AAqcF,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;AAg0CD,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,CAuHlB"}
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"}
@@ -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) => splitTextByPreferredBreaks(text, finalTextChunkLimit);
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) => splitTextByPreferredBreaks(text, finalTextChunkLimit);
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
- // 根据 SdkAppid To_Account 匹配目标账号
1617
- const target = targets.find((candidate) => {
1618
- if (!candidate.account.configured)
1619
- return false;
1620
- // 如果 URL 带了 SdkAppid,校验是否匹配
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
- if (target.account.token) {
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 expectedSign = createHash("sha256")
1649
- .update(target.account.token + requestTime)
1650
- .digest("hex");
1651
- if (sign.length !== expectedSign.length
1652
- || !timingSafeEqual(Buffer.from(sign), Buffer.from(expectedSign))) {
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: "" });