@tencent-connect/openclaw-qqbot 1.7.1 → 1.7.2

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.
Files changed (38) hide show
  1. package/README.md +188 -3
  2. package/README.zh.md +190 -3
  3. package/dist/index.d.ts +1 -0
  4. package/dist/index.js +1 -0
  5. package/dist/src/api.d.ts +2 -0
  6. package/dist/src/api.js +16 -3
  7. package/dist/src/config.d.ts +5 -1
  8. package/dist/src/config.js +12 -2
  9. package/dist/src/gateway.js +131 -169
  10. package/dist/src/slash-commands.js +119 -3
  11. package/dist/src/tools/channel.js +1 -4
  12. package/dist/src/tools/remind.js +0 -1
  13. package/dist/src/transport/index.d.ts +10 -0
  14. package/dist/src/transport/index.js +9 -0
  15. package/dist/src/transport/webhook-transport.d.ts +67 -0
  16. package/dist/src/transport/webhook-transport.js +245 -0
  17. package/dist/src/transport/webhook-verify.d.ts +48 -0
  18. package/dist/src/transport/webhook-verify.js +98 -0
  19. package/dist/src/types.d.ts +19 -0
  20. package/dist/src/utils/audio-convert.js +37 -9
  21. package/index.ts +1 -0
  22. package/package.json +1 -1
  23. package/scripts/postinstall-link-sdk.js +44 -0
  24. package/scripts/upgrade-via-npm.sh +358 -62
  25. package/scripts/upgrade-via-source.sh +122 -85
  26. package/src/api.ts +18 -4
  27. package/src/config.ts +15 -2
  28. package/src/gateway.ts +135 -167
  29. package/src/onboarding.ts +8 -0
  30. package/src/slash-commands.ts +137 -3
  31. package/src/tools/channel.ts +1 -7
  32. package/src/tools/remind.ts +0 -2
  33. package/src/transport/index.ts +11 -0
  34. package/src/transport/webhook-transport.ts +332 -0
  35. package/src/transport/webhook-verify.ts +119 -0
  36. package/src/types.ts +22 -1
  37. package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
  38. package/src/utils/audio-convert.ts +37 -9
package/README.md CHANGED
@@ -36,7 +36,9 @@ Scan to join the QQ group chat
36
36
 
37
37
  | Feature | Description |
38
38
  |---------|-------------|
39
- | 🔒 **Multi-Scene** | C2C private chat, group @messages |
39
+ | 🔒 **Multi-Scene** | C2C private chat, group chat (@mention / autonomous dual mode) |
40
+ | 👥 **Group Fine-Tuning** | Per-group @trigger rules, tool policies, custom prompts, message filtering |
41
+ | 🌐 **Dual Transport** | WebSocket (default) or Webhook (HTTP callback) — switch via config |
40
42
  | 🖼️ **Rich Media** | Send & receive images, voice, video, and files |
41
43
  | 🎙️ **Voice (STT/TTS)** | Speech-to-text transcription & text-to-speech replies |
42
44
  | 🔥 **One-Click Hot Upgrade** | Send `/bot-upgrade` in private chat to upgrade — no server login needed |
@@ -242,6 +244,22 @@ Manage the AI command execution approval policy. Supported subcommands:
242
244
 
243
245
  `/bot-clear-storage` lists files generated by the conversation and files in the downloaded resources directory. Use `/bot-clear-storage --force` to confirm deletion.
244
246
 
247
+ #### `/bot-group-allways` — Group Response Mode Toggle
248
+
249
+ > **You**: `/bot-group-allways`
250
+ >
251
+ > **QQBot**: 🤖 Group autonomous mode: ❌ @mention required
252
+
253
+ Toggle group @trigger behavior at runtime — changes persist instantly, no restart needed:
254
+
255
+ | Subcommand | Description |
256
+ |------------|-------------|
257
+ | `/bot-group-allways on` | AI decides when to speak autonomously (no @ needed) |
258
+ | `/bot-group-allways off` | Only respond when @mentioned |
259
+ | `/bot-group-allways` (no arg) | View current setting |
260
+
261
+ > ⚠️ This command modifies the account-level `defaultRequireMention`. It has lower priority than per-group `groups.{groupId}.requireMention` settings.
262
+
245
263
  ---
246
264
 
247
265
  ## 🚀 Getting Started
@@ -408,13 +426,180 @@ openclaw message send --channel "qqbot" \
408
426
 
409
427
  #### How It Works
410
428
 
411
- - When `openclaw gateway` starts, all accounts with `enabled: true` launch their own WebSocket connections
429
+ - When `openclaw gateway` starts, all accounts with `enabled: true` launch their own connections (WebSocket or Webhook depending on `transport` config)
412
430
  - Each account maintains an independent Token cache (isolated by `appId`), preventing cross-contamination
413
431
  - Incoming message logs are prefixed with `[qqbot:accountId]` for easy debugging
414
432
 
415
433
  ---
416
434
 
417
- ### Voice Configuration (STT / TTS)
435
+ ### Webhook Transport Mode
436
+
437
+ By default, the plugin connects to QQ via **WebSocket** (outbound connection, no public IP required). You can switch to **Webhook** mode where QQ platform POSTs events to your HTTP endpoint.
438
+
439
+ | | WebSocket (default) | Webhook |
440
+ |---|---|---|
441
+ | Connection | Plugin connects to QQ gateway | QQ platform POSTs to your server |
442
+ | Public IP | Not required | Required |
443
+ | Use case | Development, single instance | Production, horizontal scaling, Serverless |
444
+ | Session resume | Supported (RESUME) | Stateless, no resume needed |
445
+ | Signature | Built-in | Ed25519 auto-verified by plugin |
446
+
447
+ #### Configuration
448
+
449
+ ```json
450
+ {
451
+ "channels": {
452
+ "qqbot": {
453
+ "appId": "111111111",
454
+ "clientSecret": "your-secret",
455
+ "transport": "webhook",
456
+ "webhook": {
457
+ "path": "/qqbot/webhook"
458
+ }
459
+ }
460
+ }
461
+ }
462
+ ```
463
+
464
+ | Field | Default | Description |
465
+ |-------|---------|-------------|
466
+ | `transport` | `"websocket"` | `"websocket"` or `"webhook"` |
467
+ | `webhook.path` | `"/qqbot/webhook"` | HTTP path for receiving callbacks |
468
+
469
+ #### Platform Setup
470
+
471
+ 1. Go to [QQ Open Platform](https://q.qq.com/) → Bot Settings → Message Receiving
472
+ 2. Select **HTTP Callback**
473
+ 3. Enter your callback URL: `https://your-domain.com/qqbot/webhook`
474
+ 4. The platform sends an `op:13` validation request — the plugin handles it automatically
475
+ 5. Once validated, all events will be POSTed to your endpoint
476
+
477
+ ---
478
+
479
+ ### Group Chat Configuration
480
+
481
+ The plugin provides flexible group chat controls, allowing you to customize trigger rules, tool permissions, and AI behavior per group.
482
+
483
+ #### @Mention Trigger Mode (`requireMention`)
484
+
485
+ By default, the bot **only responds when @mentioned** in a group. You can configure it to autonomously decide when to speak:
486
+
487
+ | Mode | Config Value | Behavior |
488
+ |------|-------------|----------|
489
+ | **@ only** | `true` (default) | Only messages that @mention the bot trigger a reply |
490
+ | **Autonomous** | `false` | AI decides on its own whether each message needs a reply — no @ required |
491
+
492
+ **Priority chain** (highest to lowest):
493
+
494
+ ```
495
+ groups.{groupOpenid}.requireMention
496
+ > groups."*".requireMention
497
+ > account-level defaultRequireMention
498
+ > default value true
499
+ ```
500
+
501
+ **Example:**
502
+
503
+ ```json
504
+ {
505
+ "channels": {
506
+ "qqbot": {
507
+ // Account-level default for all groups
508
+ "defaultRequireMention": false,
509
+
510
+ "accounts": {
511
+ "default": {
512
+ "groups": {
513
+ "*": {
514
+ // Wildcard fallback for all groups
515
+ "requireMention": false
516
+ },
517
+ "GROUP_OPENID": {
518
+ // Per-group override — this group still requires @
519
+ "requireMention": true
520
+ }
521
+ }
522
+ }
523
+ }
524
+ }
525
+ }
526
+ }
527
+ ```
528
+
529
+ > **Use cases:**
530
+ >
531
+ > - Work groups → `requireMention: true` — avoid AI chiming in on every casual message
532
+ > - Dedicated AI companion groups → `requireMention: false` — participate naturally like a real person
533
+ > - Use [`/bot-group-allways`](#bot-group-allways--group-response-mode-toggle) to toggle account-level defaults at runtime
534
+
535
+ #### Additional Group Config Fields
536
+
537
+ Besides `requireMention`, each group supports these settings:
538
+
539
+ | Field | Type | Default | Description |
540
+ |-------|------|---------|-------------|
541
+ | `ignoreOtherMentions` | `boolean` | `false` | If enabled, messages that @mention others but not the bot are silently dropped (not recorded, no AI trigger) |
542
+ | `toolPolicy` | `"full" \| "restricted" \| "none"` | `"restricted"` | Tool scope available to AI in this group. `full`=all tools; `restricted`=sensitive tools restricted (e.g., command execution, file ops); `none`=no tool calls allowed |
543
+ | `prompt` | `string` | built-in default | Group-specific system prompt, appended after global systemPrompt |
544
+ | `historyLimit` | `number` | `50` | Cached group history message count |
545
+
546
+ **Full example with multiple groups:**
547
+
548
+ ```json
549
+ {
550
+ "channels": {
551
+ "qqbot": {
552
+ "defaultRequireMention": false,
553
+ "accounts": {
554
+ "default": {
555
+ "groups": {
556
+ "*": {
557
+ "requireMention": true,
558
+ "toolPolicy": "restricted",
559
+ "ignoreOtherMentions": true
560
+ },
561
+ "WORK_GROUP_OPENID": {
562
+ "requireMention": true,
563
+ "toolPolicy": "none",
564
+ "prompt": "You are a work assistant. Only answer work-related questions."
565
+ },
566
+ "FRIEND_GROUP_OPENID": {
567
+ "requireMention": false,
568
+ "toolPolicy": "full",
569
+ "prompt": "You are a friend in the group. Chat casually and naturally."
570
+ }
571
+ }
572
+ }
573
+ }
574
+ }
575
+ }
576
+ }
577
+ ```
578
+
579
+ #### Group Access Control (`groupPolicy`)
580
+
581
+ Control which groups are allowed via `groupPolicy`:
582
+
583
+ | Policy | Description |
584
+ |--------|-------------|
585
+ | `"open"` (default) | All groups are allowed |
586
+ | `"allowlist"` | Only groups in `groupAllowFrom` are allowed |
587
+ | `"disabled"` | Group chats are disabled entirely |
588
+
589
+ ```json
590
+ {
591
+ "channels": {
592
+ "qqbot": {
593
+ "groupPolicy": "allowlist",
594
+ "groupAllowFrom": ["ALLOWED_GROUP_OPENID_1", "ALLOWED_GROUP_OPENID_2"]
595
+ }
596
+ }
597
+ }
598
+ ```
599
+
600
+ > You can also use [**`/bot-group-allways`**](#bot-group-allways--group-response-mode-toggle) to toggle account-level defaults at runtime without restarting.
601
+
602
+ ---
418
603
 
419
604
  #### STT (Speech-to-Text) — Transcribe Incoming Voice Messages
420
605
 
package/README.zh.md CHANGED
@@ -31,7 +31,9 @@
31
31
 
32
32
  | 功能 | 说明 |
33
33
  |------|------|
34
- | 🔒 **多场景支持** | C2C 私聊、群聊 @消息 |
34
+ | 🔒 **多场景支持** | C2C 私聊、群聊(@提及 / 自主发言双模式) |
35
+ | 👥 **群聊精细管控** | 按群配置 @触发规则、工具权限、自定义提示词、消息过滤 |
36
+ | 🌐 **双传输模式** | WebSocket(默认)或 Webhook(HTTP 回调)— 配置切换 |
35
37
  | 🖼️ **富媒体消息** | 支持图片、语音、视频、文件的收发 |
36
38
  | 🎙️ **语音能力 (STT/TTS)** | 语音转文字自动转录 & 文字转语音回复 |
37
39
  | 🔥 **一键热更新** | 私聊发送 `/bot-upgrade` 即可完成版本升级,无需登录服务器 |
@@ -237,6 +239,24 @@ AI 可直接发送视频,支持本地文件和公网 URL。
237
239
 
238
240
  `/bot-clear-storage` 列出对话产生的文件以及下载的资源目录里的文件,使用`/bot-clear-storage -- force`确定删除。
239
241
 
242
+ #### `/bot-group-allways` — 群消息响应模式切换
243
+
244
+ > **你**:`/bot-group-allways`
245
+ >
246
+ > **QQBot**:🤖 群自主发言状态:❌ 仅被 @ 时回复
247
+
248
+ 运行时动态切换群聊默认 @触发行为,修改即时持久化,无需重启:
249
+
250
+ | 子命令 | 说明 |
251
+ |--------|------|
252
+ | `/bot-group-allways on` | AI 自主判断何时发言(无需 @) |
253
+ | `/bot-group-allways off` | 仅在被 @ 时回复 |
254
+ | `/bot-group-allways`(无参数) | 查看当前设置 |
255
+
256
+ > ⚠️ 此指令修改账户级 `defaultRequireMention`,优先级低于具体群的 `groups.{groupId}.requireMention` 配置。
257
+
258
+ ---
259
+
240
260
  ---
241
261
 
242
262
  ## 🚀 快速开始
@@ -403,13 +423,180 @@ openclaw message send --channel "qqbot" \
403
423
 
404
424
  #### 工作原理
405
425
 
406
- - 启动 `openclaw gateway` 后,所有 `enabled: true` 的账户会同时启动 WebSocket 连接
426
+ - 启动 `openclaw gateway` 后,所有 `enabled: true` 的账户会同时启动连接(WebSocket 或 Webhook,取决于 `transport` 配置)
407
427
  - 每个账户独立维护 Token 缓存(基于 `appId` 隔离),互不干扰
408
428
  - 接收消息时,日志会带上 `[qqbot:accountId]` 前缀方便排查
409
429
 
410
430
  ---
411
431
 
412
- ### 语音能力配置(STT / TTS)
432
+ ### Webhook 传输模式
433
+
434
+ 默认情况下,插件通过 **WebSocket** 连接 QQ 平台(出站连接,无需公网 IP)。你也可以切换为 **Webhook** 模式,由 QQ 平台主动 POST 事件到你的 HTTP 端点。
435
+
436
+ | | WebSocket(默认) | Webhook |
437
+ |---|---|---|
438
+ | 连接方式 | 插件主动连接 QQ 网关 | QQ 平台 POST 到你的服务器 |
439
+ | 公网 IP | 不需要 | 需要 |
440
+ | 适用场景 | 开发调试、单实例部署 | 生产环境、水平扩展、Serverless |
441
+ | 会话恢复 | 支持 RESUME | 无状态,无需恢复 |
442
+ | 签名验证 | 平台内置 | 插件自动 Ed25519 验签 |
443
+
444
+ #### 配置方式
445
+
446
+ ```json
447
+ {
448
+ "channels": {
449
+ "qqbot": {
450
+ "appId": "111111111",
451
+ "clientSecret": "your-secret",
452
+ "transport": "webhook",
453
+ "webhook": {
454
+ "path": "/qqbot/webhook"
455
+ }
456
+ }
457
+ }
458
+ }
459
+ ```
460
+
461
+ | 字段 | 默认值 | 说明 |
462
+ |------|--------|------|
463
+ | `transport` | `"websocket"` | `"websocket"` 或 `"webhook"` |
464
+ | `webhook.path` | `"/qqbot/webhook"` | 接收回调的 HTTP 路径 |
465
+
466
+ #### 平台配置步骤
467
+
468
+ 1. 登录 [QQ 开放平台](https://q.qq.com/) → 开发设置 → 消息接收方式
469
+ 2. 选择 **HTTP 回调**
470
+ 3. 填写回调 URL:`https://your-domain.com/qqbot/webhook`
471
+ 4. 平台发送 `op:13` 验证请求,插件自动处理签名验证
472
+ 5. 验证通过后,所有事件将以 POST 方式推送到该地址
473
+
474
+ ---
475
+
476
+ ### 群聊配置
477
+
478
+ 插件提供灵活的群聊管控能力,支持按群定制触发规则、工具权限和 AI 行为策略。
479
+
480
+ #### @提及触发模式(requireMention)
481
+
482
+ 默认情况下,群聊中**必须 @机器人**才会触发 AI 回复。你可以通过配置让 AI 自主判断是否需要发言:
483
+
484
+ | 模式 | 配置值 | 行为 |
485
+ |------|--------|------|
486
+ | **仅 @时回复** | `true`(默认) | 群消息中只有 @了机器人才会触发回复 |
487
+ | **自主发言** | `false` | AI 自主判断每条消息是否需要回复,无需 @ |
488
+
489
+ **优先级链**(从高到低):
490
+
491
+ ```
492
+ 具体群 groups.{groupOpenid}.requireMention
493
+ > 通配符 groups."*".requireMention
494
+ > 账户级 defaultRequireMention
495
+ > 默认值 true
496
+ ```
497
+
498
+ **配置示例:**
499
+
500
+ ```json
501
+ {
502
+ "channels": {
503
+ "qqbot": {
504
+ // 账户级:所有群的默认行为
505
+ "defaultRequireMention": false,
506
+
507
+ "accounts": {
508
+ "default": {
509
+ "groups": {
510
+ "*": {
511
+ // 通配符:所有群的兜底规则
512
+ "requireMention": false
513
+ },
514
+ "GROUP_OPENID": {
515
+ // 单群覆盖:这个群仍然需要 @
516
+ "requireMention": true
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+ }
524
+ ```
525
+
526
+ > **使用场景举例:**
527
+ >
528
+ > - 工作群设为 `requireMention: true` — 避免 AI 对每条闲聊都插嘴
529
+ > - 专属 AI 陪伴群设为 `requireMention: false` — 像真人一样自然参与对话
530
+ > - 通过 `/bot-group-allways on|off` 指令可在运行时动态切换账户级默认值
531
+
532
+ #### 其他群配置项
533
+
534
+ 除 `requireMention` 外,每个群还支持以下配置:
535
+
536
+ | 字段 | 类型 | 默认值 | 说明 |
537
+ |------|------|--------|------|
538
+ | `ignoreOtherMentions` | `boolean` | `false` | 是否忽略 @了其他人但没 @机器人的消息。开启后这类消息直接丢弃,不记录历史、不触发 AI |
539
+ | `toolPolicy` | `"full" \| "restricted" \| "none"` | `"restricted"` | 群聊中 AI 可使用的工具范围。`full`=全部可用;`restricted`=限制敏感工具(如命令执行、文件操作);`none`=禁止所有工具调用 |
540
+ | `prompt` | `string` | 内置默认提示词 | 该群专属的系统提示词,会追加到全局 systemPrompt 之后 |
541
+ | `historyLimit` | `number` | `50` | 群历史消息缓存条数 |
542
+
543
+ **完整群配置示例:**
544
+
545
+ ```json
546
+ {
547
+ "channels": {
548
+ "qqbot": {
549
+ "defaultRequireMention": false,
550
+ "accounts": {
551
+ "default": {
552
+ "groups": {
553
+ "*": {
554
+ "requireMention": true,
555
+ "toolPolicy": "restricted",
556
+ "ignoreOtherMentions": true
557
+ },
558
+ "WORK_GROUP_OPENID": {
559
+ "requireMention": true,
560
+ "toolPolicy": "none",
561
+ "prompt": "你是工作助手,只回答与工作相关的问题"
562
+ },
563
+ "FRIEND_GROUP_OPENID": {
564
+ "requireMention": false,
565
+ "toolPolicy": "full",
566
+ "prompt": "你是群里的朋友,轻松随意地聊天"
567
+ }
568
+ }
569
+ }
570
+ }
571
+ }
572
+ }
573
+ }
574
+ ```
575
+
576
+ #### 群访问控制(groupPolicy)
577
+
578
+ 通过 `groupPolicy` 控制哪些群允许机器人加入并接收消息:
579
+
580
+ | 策略 | 说明 |
581
+ |------|------|
582
+ | `"open"`(默认) | 所有群均可使用 |
583
+ | `"allowlist"` | 仅 `groupAllowFrom` 白名单中的群可使用 |
584
+ | `"disabled"` | 禁止所有群聊 |
585
+
586
+ ```json
587
+ {
588
+ "channels": {
589
+ "qqbot": {
590
+ "groupPolicy": "allowlist",
591
+ "groupAllowFrom": ["ALLOWED_GROUP_OPENID_1", "ALLOWED_GROUP_OPENID_2"]
592
+ }
593
+ }
594
+ }
595
+ ```
596
+
597
+ > 也可通过 [**`/bot-group-allways`** 指令](#bot-group-allways--群消息响应模式切换) 在运行时动态切换账户级默认值,无需重启。
598
+
599
+ ---
413
600
 
414
601
  #### STT(语音转文字)— 自动转录用户发来的语音消息
415
602
 
package/dist/index.d.ts CHANGED
@@ -15,3 +15,4 @@ export * from "./src/api.js";
15
15
  export * from "./src/config.js";
16
16
  export * from "./src/gateway.js";
17
17
  export * from "./src/outbound.js";
18
+ export * from "./src/transport/index.js";
package/dist/index.js CHANGED
@@ -24,3 +24,4 @@ export * from "./src/api.js";
24
24
  export * from "./src/config.js";
25
25
  export * from "./src/gateway.js";
26
26
  export * from "./src/outbound.js";
27
+ export * from "./src/transport/index.js";
package/dist/src/api.d.ts CHANGED
@@ -27,6 +27,8 @@ export declare class ApiError extends Error {
27
27
  /** 回包中的原始 message 字段(用于向用户展示兜底文案) */
28
28
  bizMessage?: string | undefined);
29
29
  }
30
+ export declare const API_BASE: string;
31
+ export declare const TOKEN_URL: string;
30
32
  /** 由 setQQBotRuntime 调用,将 api.runtime.version 注入到 User-Agent */
31
33
  export declare function setOpenClawVersion(version: string): void;
32
34
  export declare function getPluginUserAgent(): string;
package/dist/src/api.js CHANGED
@@ -5,6 +5,8 @@
5
5
  import os from "node:os";
6
6
  import { computeFileHash, getCachedFileInfo, setCachedFileInfo } from "./utils/upload-cache.js";
7
7
  import { sanitizeFileName } from "./utils/platform.js";
8
+ import { resolveUserAgentSuffix } from "./config.js";
9
+ import { getQQBotRuntime } from "./runtime.js";
8
10
  /** 默认使用 console,外部可通过 setApiLogger 注入框架 log */
9
11
  let log = {
10
12
  info: (msg) => console.log(msg),
@@ -38,8 +40,9 @@ export class ApiError extends Error {
38
40
  this.name = "ApiError";
39
41
  }
40
42
  }
41
- const API_BASE = "https://api.sgroup.qq.com";
42
- const TOKEN_URL = "https://bots.qq.com/app/getAppAccessToken";
43
+ // 支持环境变量覆盖,用于私有化部署/测试环境
44
+ export const API_BASE = (process.env.QQBOT_BASE_URL?.replace(/\/+$/, "") || "https://api.sgroup.qq.com");
45
+ export const TOKEN_URL = `${process.env.QQBOT_TOKEN_BASE_URL?.replace(/\/+$/, "") || "https://bots.qq.com"}/app/getAppAccessToken`;
43
46
  // ============ Plugin User-Agent ============
44
47
  // 格式: QQBotPlugin/{version} (Node/{nodeVersion}; {os}; OpenClaw/{openclawVersion})
45
48
  // 示例: QQBotPlugin/1.6.0 (Node/22.14.0; darwin; OpenClaw/2026.3.31)
@@ -53,7 +56,17 @@ export function setOpenClawVersion(version) {
53
56
  _openclawVersion = version;
54
57
  }
55
58
  export function getPluginUserAgent() {
56
- return `QQBotPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()}; OpenClaw/${_openclawVersion})`;
59
+ const base = `QQBotPlugin/${_pluginVersion} (Node/${process.versions.node}; ${os.platform()}; OpenClaw/${_openclawVersion})`;
60
+ let suffix = "";
61
+ try {
62
+ const rt = getQQBotRuntime();
63
+ // rt.config 是配置管理器,调用 .current() 获取实际配置数据
64
+ const cfgMgr = rt.config;
65
+ const cfg = typeof cfgMgr.current === "function" ? cfgMgr.current() : cfgMgr;
66
+ suffix = resolveUserAgentSuffix(cfg);
67
+ }
68
+ catch { /* runtime 未初始化时返回无后缀 UA */ }
69
+ return suffix ? `${base} ${suffix}` : base;
57
70
  }
58
71
  // 运行时配置
59
72
  let currentMarkdownSupport = false;
@@ -17,7 +17,7 @@ export declare function resolveGroupAllowFrom(cfg: OpenClawConfig, accountId?: s
17
17
  /** 检查指定群是否被允许(使用标准策略引擎) */
18
18
  export declare function isGroupAllowed(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): boolean;
19
19
  type ResolvedGroupConfig = Omit<Required<GroupConfig>, "prompt"> & Pick<GroupConfig, "prompt">;
20
- /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
20
+ /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 账户级 defaultRequireMention > 硬编码默认值) */
21
21
  export declare function resolveGroupConfig(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ResolvedGroupConfig;
22
22
  /** 解析群历史消息缓存条数 */
23
23
  export declare function resolveHistoryLimit(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): number;
@@ -31,6 +31,10 @@ export declare function resolveIgnoreOtherMentions(cfg: OpenClawConfig, groupOpe
31
31
  export declare function resolveToolPolicy(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): ToolPolicy;
32
32
  /** 解析群名称(优先配置,fallback 为 openid 前 8 位) */
33
33
  export declare function resolveGroupName(cfg: OpenClawConfig, groupOpenid: string, accountId?: string): string;
34
+ /**
35
+ * 解析 User-Agent 追加后缀(仅通道级:channels.qqbot.userAgentSuffix)
36
+ */
37
+ export declare function resolveUserAgentSuffix(cfg: OpenClawConfig): string;
34
38
  /**
35
39
  * 列出所有 QQBot 账户 ID
36
40
  */
@@ -81,14 +81,16 @@ export function isGroupAllowed(cfg, groupOpenid, accountId) {
81
81
  allowlistMatched,
82
82
  }).allowed;
83
83
  }
84
- /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 默认值) */
84
+ /** 解析指定群配置(具体 groupOpenid > 通配符 "*" > 账户级 defaultRequireMention > 硬编码默认值) */
85
85
  export function resolveGroupConfig(cfg, groupOpenid, accountId) {
86
86
  const account = resolveQQBotAccount(cfg, accountId);
87
87
  const groups = account.config?.groups ?? {};
88
88
  const wildcardCfg = groups["*"] ?? {};
89
89
  const specificCfg = groups[groupOpenid] ?? {};
90
+ // 账户级默认值:defaultRequireMention 配置 > 硬编码默认 true
91
+ const accountDefaultRequireMention = account.config?.defaultRequireMention ?? DEFAULT_GROUP_CONFIG.requireMention;
90
92
  return {
91
- requireMention: specificCfg.requireMention ?? wildcardCfg.requireMention ?? DEFAULT_GROUP_CONFIG.requireMention,
93
+ requireMention: specificCfg.requireMention ?? wildcardCfg.requireMention ?? accountDefaultRequireMention,
92
94
  ignoreOtherMentions: specificCfg.ignoreOtherMentions ?? wildcardCfg.ignoreOtherMentions ?? DEFAULT_GROUP_CONFIG.ignoreOtherMentions,
93
95
  toolPolicy: specificCfg.toolPolicy ?? wildcardCfg.toolPolicy ?? DEFAULT_GROUP_CONFIG.toolPolicy,
94
96
  name: specificCfg.name ?? wildcardCfg.name ?? DEFAULT_GROUP_CONFIG.name,
@@ -123,6 +125,13 @@ export function resolveGroupName(cfg, groupOpenid, accountId) {
123
125
  const name = resolveGroupConfig(cfg, groupOpenid, accountId).name;
124
126
  return name || groupOpenid.slice(0, 8);
125
127
  }
128
+ /**
129
+ * 解析 User-Agent 追加后缀(仅通道级:channels.qqbot.userAgentSuffix)
130
+ */
131
+ export function resolveUserAgentSuffix(cfg) {
132
+ const qqbot = cfg.channels?.qqbot;
133
+ return qqbot?.userAgentSuffix ? String(qqbot.userAgentSuffix).trim() : "";
134
+ }
126
135
  function normalizeAppId(raw) {
127
136
  if (raw === null || raw === undefined)
128
137
  return "";
@@ -217,6 +226,7 @@ export function resolveQQBotAccount(cfg, accountId) {
217
226
  systemPrompt: accountConfig.systemPrompt,
218
227
  imageServerBaseUrl: accountConfig.imageServerBaseUrl || process.env.QQBOT_IMAGE_SERVER_BASE_URL,
219
228
  markdownSupport: accountConfig.markdownSupport !== false,
229
+ userAgentSuffix: resolveUserAgentSuffix(cfg),
220
230
  config: accountConfig,
221
231
  };
222
232
  }