@sliverp/qqbot 1.3.6 → 1.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 CHANGED
@@ -1,106 +1,96 @@
1
+ <div align="center">
2
+
1
3
  # QQ Bot Channel Plugin for Moltbot
2
4
 
3
- QQ 开放平台Bot API 的 Moltbot 渠道插件,支持 C2C 私聊、群聊 @消息、频道消息。
5
+ QQ 开放平台 Bot API 的 Moltbot 渠道插件,支持 C2C 私聊、群聊 @消息、频道消息。
6
+
7
+ [![npm version](https://img.shields.io/badge/npm-v1.3.7-blue)](https://www.npmjs.com/package/@sliverp/qqbot)
8
+ [![License](https://img.shields.io/badge/license-MIT-green)](./LICENSE)
9
+ [![QQ Bot](https://img.shields.io/badge/QQ_Bot-API_v2-red)](https://bot.q.qq.com/wiki/)
10
+ [![Platform](https://img.shields.io/badge/platform-Moltbot-orange)](https://github.com/sliverp/moltbot)
11
+ [![Node.js](https://img.shields.io/badge/Node.js->=18-339933)](https://nodejs.org/)
12
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-3178C6)](https://www.typescriptlang.org/)
13
+
14
+ </div>
4
15
 
5
- ## 功能特性
16
+ ---
6
17
 
7
- - **多场景支持**:C2C 单聊、QQ 群 @消息、频道公开消息、频道私信
8
- - **自动重连**:WebSocket 断连后自动重连,支持 Session Resume
9
- - **消息去重**:自动管理 `msg_seq`,支持对同一消息多次回复
10
- - **系统提示词**:可配置自定义系统提示词注入到 AI 请求
11
- - **错误提示**:AI 无响应时自动提示用户检查配置
18
+ ## Star 趋势
12
19
 
13
- ## 使用示例:
14
- <img width="952" height="582" alt="image" src="https://github.com/user-attachments/assets/a16d582b-708c-473e-b3a2-e0c4c503a0c8" />
20
+ <!-- 在这里放置 Star 数量变化图 -->
15
21
 
16
- ## 版本更新
17
- **使用`openclaw plugins list`来产看插件版本,建议使用最新版的版本**
18
- <img width="902" height="248" alt="Clipboard_Screenshot_1769739939" src="https://github.com/user-attachments/assets/d6f37458-900c-4de9-8fdc-f8e6bf5c7ee5" />
22
+ ---
19
23
 
20
- ### 1.4.0(即将更新)
21
- - 支持Markdonw格式
24
+ ## 📸 使用示例
22
25
 
23
-
24
- ### 1.3.0-2026.02.03
25
- - 支持图片收发等功能
26
- <img width="924" height="428" alt="Clipboard_Screenshot_1770112572" src="https://github.com/user-attachments/assets/80f38ae9-dc40-4545-ad17-e7e254064cf4" />
26
+ <img width="600" alt="使用示例" src="https://github.com/user-attachments/assets/6f1704ab-584b-497e-8937-96f84ce2958f" />
27
27
 
28
- - 支持定时任务到时后主动推送
29
- <img width="930" height="288" alt="Clipboard_Screenshot_1770112539" src="https://github.com/user-attachments/assets/9674cda0-91e9-4860-8dcc-bc50007862a2" />
30
- - 优化一些已知问题
28
+ ---
31
29
 
30
+ ## ✨ 功能特性
32
31
 
33
- ### 1.2.5-2026.02.02
34
- - 解除URL发送限制,现在可以直接在私聊发送URL
35
- <img width="886" height="276" alt="Clipboard_Screenshot_1770092858" src="https://github.com/user-attachments/assets/c660949e-28a5-4e5f-abc2-77f0a2c67bad" />
36
- - 更新Bot正在输入中状态
37
- <img width="740" height="212" alt="Clipboard_Screenshot_1770091969" src="https://github.com/user-attachments/assets/47835c4b-ccd2-4782-aaa6-b873cb58f7d7" />
38
- - 提供主动推送能力(目前AI还不知道怎么调用主动推送,相关完整Skill能力将在后续版本更新)
39
- - 优化一些已知问题
40
- - 优化未收到未收到大模型响应时的提示信息
32
+ - 🔒 **多场景支持** - C2C 私聊、群聊 @消息、频道消息、频道私信
33
+ - 🖼️ **富媒体消息** - 支持图片收发、文件发送
34
+ - **定时推送** - 支持定时任务到时后主动推送
35
+ - 🔗 **URL 无限制** - 私聊可直接发送 URL
36
+ - ⌨️ **输入状态** - Bot 正在输入中状态提示
37
+ - 🔄 **热更新** - 支持 npm 方式安装和热更新
38
+ - 📝 **Markdown** - 支持 Markdown 格式(即将更新)
41
39
 
40
+ ---
42
41
 
43
- ### 1.2.2-2026.01.31
44
- - 支持发送文件
45
- - 支持openclaw、moltbot命令行
46
- - 修复[health]检查提示: [health] refresh failed: Cannot read properties of undefined (reading 'appId')的问题(不影响使用)
47
- - 修复文件发送后clawdbot无法读取的问题
42
+ ## 📦 安装
48
43
 
49
- ### 1.2.1
50
- - 解决了长时间使用会断联的问题
51
- - 解决了频繁重连的问题
52
- - 增加了大模型调用失败后的提示消息
44
+ ### 方式一:腾讯云 Lighthouse 镜像(最简单)
53
45
 
46
+ [![Lighthouse](https://img.shields.io/badge/腾讯云-Lighthouse_镜像-00A4FF)](https://cloud.tencent.com/product/lighthouse)
54
47
 
55
- ### 1.1.0
56
- - 解决了一些url会被拦截的问题
57
- - 解决了多轮消息会发送失败的问题
58
- - 修复了部分图片无法接受的问题
59
- - 增加支持onboard的方式配置AppId 和 AppSecret
48
+ 直接使用预装好的腾讯云 Lighthouse 镜像,开箱即用,无需手动安装配置。
60
49
 
50
+ ### 方式二:npm 安装(推荐)
61
51
 
62
- ## 安装
52
+ ```bash
53
+ openclaw plugins install @sliverp/qqbot@1.3.7
54
+ ```
63
55
 
64
- 在插件目录下执行:
56
+ ### 方式三:源码安装
65
57
 
66
58
  ```bash
67
59
  git clone https://github.com/sliverp/qqbot.git && cd qqbot
68
- clawdbot plugins install . # 这一步会有点久,需要安装一些依赖。稍微耐心等待一下,尤其是小内存机器
60
+ clawdbot plugins install .
69
61
  ```
70
62
 
71
- ## 配置
63
+ > 💡 安装过程需要一些时间,尤其是小内存机器,请耐心等待
64
+
65
+ ---
66
+
67
+ ## ⚙️ 配置
72
68
 
73
69
  ### 1. 获取 QQ 机器人凭证
74
70
 
75
71
  1. 访问 [QQ 开放平台](https://q.qq.com/)
76
72
  2. 创建机器人应用
77
73
  3. 获取 `AppID` 和 `AppSecret`(ClientSecret)
78
- 4. Token 格式为 `AppID:AppSecret`,例如 `102146862:Xjv7JVhu7KXkxANbp3HVjxCRgvAPeuAQ`
74
+ 4. Token 格式:`AppID:AppSecret`
79
75
 
80
76
  ### 2. 添加配置
81
77
 
82
- #### 方式一:交互式配置
78
+ **交互式配置:**
83
79
 
84
80
  ```bash
85
81
  clawdbot channels add
86
82
  # 选择 qqbot,按提示输入 Token
87
83
  ```
88
84
 
89
- #### 方式二:命令行配置
85
+ **命令行配置:**
90
86
 
91
87
  ```bash
92
88
  clawdbot channels add --channel qqbot --token "AppID:AppSecret"
93
89
  ```
94
90
 
95
- 示例:
96
-
97
- ```bash
98
- clawdbot channels add --channel qqbot --token "102146862:xxxxxxxx"
99
- ```
100
-
101
91
  ### 3. 手动编辑配置(可选)
102
92
 
103
- 也可以直接编辑 `~/.clawdbot/clawdbot.json`:
93
+ 编辑 `~/.clawdbot/clawdbot.json`:
104
94
 
105
95
  ```json
106
96
  {
@@ -108,27 +98,28 @@ clawdbot channels add --channel qqbot --token "102146862:xxxxxxxx"
108
98
  "qqbot": {
109
99
  "enabled": true,
110
100
  "appId": "你的AppID",
111
- "clientSecret": "你的AppSecret",
112
- "systemPrompt": "你是一个友好的助手"
101
+ "clientSecret": "你的AppSecret"
113
102
  }
114
103
  }
115
104
  }
116
105
  ```
117
106
 
107
+ ---
118
108
 
119
-
120
- ## 配置项说明
109
+ ## 📋 配置项说明
121
110
 
122
111
  | 配置项 | 类型 | 必填 | 说明 |
123
112
  |--------|------|------|------|
124
- | `appId` | string | | QQ 机器人 AppID |
125
- | `clientSecret` | string | 是* | AppSecret,与 `clientSecretFile` 二选一 |
126
- | `clientSecretFile` | string | 是* | AppSecret 文件路径 |
127
- | `enabled` | boolean | | 是否启用,默认 `true` |
128
- | `name` | string | | 账户显示名称 |
129
- | `systemPrompt` | string | | 自定义系统提示词 |
113
+ | `appId` | string | | QQ 机器人 AppID |
114
+ | `clientSecret` | string | ✅* | AppSecret,与 `clientSecretFile` 二选一 |
115
+ | `clientSecretFile` | string | ✅* | AppSecret 文件路径 |
116
+ | `enabled` | boolean | | 是否启用,默认 `true` |
117
+ | `name` | string | | 账户显示名称 |
118
+ | `systemPrompt` | string | | 自定义系统提示词 |
130
119
 
131
- ## 支持的消息类型
120
+ ---
121
+
122
+ ## 📨 支持的消息类型
132
123
 
133
124
  | 事件类型 | 说明 | Intent |
134
125
  |----------|------|--------|
@@ -137,17 +128,17 @@ clawdbot channels add --channel qqbot --token "102146862:xxxxxxxx"
137
128
  | `AT_MESSAGE_CREATE` | 频道 @机器人消息 | `1 << 30` |
138
129
  | `DIRECT_MESSAGE_CREATE` | 频道私信 | `1 << 12` |
139
130
 
140
- ## 使用
131
+ ---
132
+
133
+ ## 🚀 使用
141
134
 
142
- ### 启动
135
+ ### 启动服务
143
136
 
144
- 后台启动
145
137
  ```bash
138
+ # 后台启动
146
139
  clawdbot gateway restart
147
- ```
148
140
 
149
- 前台启动, 方便试试查看日志
150
- ```bash
141
+ # 前台启动(查看日志)
151
142
  clawdbot gateway --port 18789 --verbose
152
143
  ```
153
144
 
@@ -158,23 +149,35 @@ clawdbot onboard
158
149
  # 选择 QQ Bot 进行交互式配置
159
150
  ```
160
151
 
161
- ## 注意事项
152
+ ---
153
+
154
+ ## ⚠️ 注意事项
155
+
156
+ - **群消息**:需要在群内 @机器人 才能触发回复
157
+ - **沙箱模式**:新创建的机器人默认在沙箱模式,需要添加测试用户
162
158
 
163
- 1. **群消息**:需要在群内 @机器人 才能触发回复
164
- 2. **沙箱模式**:新创建的机器人默认在沙箱模式,需要添加测试用户
159
+ ---
160
+
161
+ ## 🔄 升级
162
+
163
+ ### npm 热更新
164
+
165
+ ```bash
166
+ npx -y @sliverp/qqbot@1.3.7 upgrade
167
+ ```
165
168
 
166
- ## 升级
169
+ > 热更新后无需重新配置 AppId 和 AppSecret
167
170
 
168
- 如果需要升级插件,先运行升级脚本清理旧版本:
171
+ ### 源码热更新
169
172
 
170
173
  ```bash
171
174
  git clone https://github.com/sliverp/qqbot.git && cd qqbot
172
175
 
173
- # 运行升级脚本(清理旧版本和配置)
176
+ # 运行升级脚本
174
177
  bash ./scripts/upgrade.sh
175
178
 
176
- # 重新安装插件
177
- clawdbot plugins install . # 这一步会有点久,需要安装一些依赖。稍微耐心等待一下,尤其是小内存机器
179
+ # 重新安装
180
+ clawdbot plugins install .
178
181
 
179
182
  # 重新配置
180
183
  clawdbot channels add --channel qqbot --token "AppID:AppSecret"
@@ -183,49 +186,82 @@ clawdbot channels add --channel qqbot --token "AppID:AppSecret"
183
186
  clawdbot gateway restart
184
187
  ```
185
188
 
186
- 升级脚本会自动:
187
- - 删除 `~/.clawdbot/extensions/qqbot` 目录
188
- - 清理 `clawdbot.json` 中的 qqbot 相关配置
189
+ 升级脚本会自动清理旧版本和配置。
189
190
 
190
- ## 开发
191
+ ---
191
192
 
192
- ```bash
193
- # 安装依赖
194
- npm install
195
193
 
196
- # 编译
197
- npm run build
198
194
 
199
- # 监听模式
200
- npm run dev
201
- ```
195
+ ---
202
196
 
203
- ## 文件结构
197
+ ## 📚 版本历史
204
198
 
205
- ```
206
- qqbot/
207
- ├── index.ts # 入口文件
208
- ├── src/
209
- │ ├── api.ts # QQ Bot API 封装
210
- │ ├── channel.ts # Channel Plugin 定义
211
- │ ├── config.ts # 配置解析
212
- │ ├── gateway.ts # WebSocket 网关
213
- │ ├── onboarding.ts # CLI 配置向导
214
- │ ├── outbound.ts # 出站消息处理
215
- │ ├── runtime.ts # 运行时状态
216
- │ └── types.ts # 类型定义
217
- ├── scripts/
218
- │ └── upgrade.sh # 升级脚本
219
- ├── package.json
220
- └── tsconfig.json
221
- ```
199
+ <details>
200
+ <summary><b>v1.4.0(即将更新)</b></summary>
201
+
202
+ - 支持 Markdown 格式
203
+
204
+ </details>
205
+
206
+ <details>
207
+ <summary><b>v1.3.0 - 2026.02.03</b></summary>
208
+
209
+ - 支持图片收发等功能
210
+ - 支持定时任务到时后主动推送
211
+ - ✨ 支持使用 npm 等方式安装和升级
212
+ - 🐛 优化一些已知问题
213
+
214
+ </details>
215
+
216
+ <details>
217
+ <summary><b>v1.2.5 - 2026.02.02</b></summary>
218
+
219
+ - ✨ 解除 URL 发送限制
220
+ - ✨ 更新 Bot 正在输入中状态
221
+ - ✨ 提供主动推送能力
222
+ - 🐛 优化一些已知问题
223
+
224
+ </details>
222
225
 
223
- ## 相关链接
226
+ <details>
227
+ <summary><b>v1.2.2 - 2026.01.31</b></summary>
228
+
229
+ - ✨ 支持发送文件
230
+ - ✨ 支持 openclaw、moltbot 命令行
231
+ - 🐛 修复 health 检查提示问题
232
+ - 🐛 修复文件发送后 clawdbot 无法读取的问题
233
+
234
+ </details>
235
+
236
+ <details>
237
+ <summary><b>v1.2.1</b></summary>
238
+
239
+ - 🐛 解决长时间使用会断联的问题
240
+ - 🐛 解决频繁重连的问题
241
+ - ✨ 增加大模型调用失败后的提示消息
242
+
243
+ </details>
244
+
245
+ <details>
246
+ <summary><b>v1.1.0</b></summary>
247
+
248
+ - 🐛 解决 URL 被拦截的问题
249
+ - 🐛 解决多轮消息发送失败的问题
250
+ - 🐛 修复部分图片无法接收的问题
251
+ - ✨ 增加支持 onboard 配置方式
252
+
253
+ </details>
254
+
255
+ ---
256
+
257
+ ## 🔗 相关链接
224
258
 
225
259
  - [QQ 机器人官方文档](https://bot.q.qq.com/wiki/)
226
260
  - [QQ 开放平台](https://q.qq.com/)
227
261
  - [API v2 文档](https://bot.q.qq.com/wiki/develop/api-v2/)
228
262
 
229
- ## License
263
+ ---
264
+
265
+ ## 📄 License
230
266
 
231
267
  MIT
package/bin/qqbot-cli.js CHANGED
@@ -164,8 +164,8 @@ function upgrade() {
164
164
  }
165
165
 
166
166
  console.log('\n=== 升级完成 ===');
167
- console.log(`\n可以运行以下命令启动机器人:`);
168
- console.log(` ${foundInstallation} gateway --verbose`);
167
+ console.log(`\n可以运行以下命令前台运行启动机器人:`);
168
+ console.log(` ${foundInstallation} gateway stop && ${foundInstallation} gateway --port 18789 --verbose`);
169
169
  }
170
170
 
171
171
  // 安装命令
package/dist/index.js CHANGED
@@ -3,8 +3,8 @@ import { qqbotPlugin } from "./src/channel.js";
3
3
  import { setQQBotRuntime } from "./src/runtime.js";
4
4
  const plugin = {
5
5
  id: "qqbot",
6
- name: "QQ Bot (Stream)",
7
- description: "QQ Bot channel plugin with streaming message support",
6
+ name: "QQ Bot",
7
+ description: "QQ Bot channel plugin",
8
8
  configSchema: emptyPluginConfigSchema(),
9
9
  register(api) {
10
10
  setQQBotRuntime(api.runtime);
@@ -1,3 +1,3 @@
1
- import type { ChannelPlugin } from "clawdbot/plugin-sdk";
1
+ import { type ChannelPlugin } from "openclaw/plugin-sdk";
2
2
  import type { ResolvedQQBotAccount } from "./types.js";
3
3
  export declare const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount>;
@@ -1,8 +1,38 @@
1
- import { listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig } from "./config.js";
2
- import { sendText } from "./outbound.js";
1
+ import { applyAccountNameToChannelSection, deleteAccountFromConfigSection, setAccountEnabledInConfigSection, } from "openclaw/plugin-sdk";
2
+ import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId } from "./config.js";
3
+ import { sendText, sendMedia } from "./outbound.js";
3
4
  import { startGateway } from "./gateway.js";
4
5
  import { qqbotOnboardingAdapter } from "./onboarding.js";
5
- const DEFAULT_ACCOUNT_ID = "default";
6
+ import { getQQBotRuntime } from "./runtime.js";
7
+ /**
8
+ * 简单的文本分块函数
9
+ * 用于预先分块长文本
10
+ */
11
+ function chunkText(text, limit) {
12
+ if (text.length <= limit)
13
+ return [text];
14
+ const chunks = [];
15
+ let remaining = text;
16
+ while (remaining.length > 0) {
17
+ if (remaining.length <= limit) {
18
+ chunks.push(remaining);
19
+ break;
20
+ }
21
+ // 尝试在换行处分割
22
+ let splitAt = remaining.lastIndexOf("\n", limit);
23
+ if (splitAt <= 0 || splitAt < limit * 0.5) {
24
+ // 没找到合适的换行,尝试在空格处分割
25
+ splitAt = remaining.lastIndexOf(" ", limit);
26
+ }
27
+ if (splitAt <= 0 || splitAt < limit * 0.5) {
28
+ // 还是没找到,强制在 limit 处分割
29
+ splitAt = limit;
30
+ }
31
+ chunks.push(remaining.slice(0, splitAt));
32
+ remaining = remaining.slice(splitAt).trimStart();
33
+ }
34
+ return chunks;
35
+ }
6
36
  export const qqbotPlugin = {
7
37
  id: "qqbot",
8
38
  meta: {
@@ -15,9 +45,14 @@ export const qqbotPlugin = {
15
45
  },
16
46
  capabilities: {
17
47
  chatTypes: ["direct", "group"],
18
- media: false,
48
+ media: true,
19
49
  reactions: false,
20
50
  threads: false,
51
+ /**
52
+ * blockStreaming: true 表示该 Channel 支持块流式
53
+ * 框架会收集流式响应,然后通过 deliver 回调发送
54
+ */
55
+ blockStreaming: false,
21
56
  },
22
57
  reload: { configPrefixes: ["channels.qqbot"] },
23
58
  // CLI onboarding wizard
@@ -47,7 +82,22 @@ export const qqbotPlugin = {
47
82
  config: {
48
83
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
49
84
  resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId),
50
- defaultAccountId: () => DEFAULT_ACCOUNT_ID,
85
+ defaultAccountId: (cfg) => resolveDefaultQQBotAccountId(cfg),
86
+ // 新增:设置账户启用状态
87
+ setAccountEnabled: ({ cfg, accountId, enabled }) => setAccountEnabledInConfigSection({
88
+ cfg,
89
+ sectionKey: "qqbot",
90
+ accountId,
91
+ enabled,
92
+ allowTopLevel: true,
93
+ }),
94
+ // 新增:删除账户
95
+ deleteAccount: ({ cfg, accountId }) => deleteAccountFromConfigSection({
96
+ cfg,
97
+ sectionKey: "qqbot",
98
+ accountId,
99
+ clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
100
+ }),
51
101
  isConfigured: (account) => Boolean(account?.appId && account?.clientSecret),
52
102
  describeAccount: (account) => ({
53
103
  accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
@@ -56,8 +106,30 @@ export const qqbotPlugin = {
56
106
  configured: Boolean(account?.appId && account?.clientSecret),
57
107
  tokenSource: account?.secretSource,
58
108
  }),
109
+ // 关键:解析 allowFrom 配置,用于命令授权
110
+ resolveAllowFrom: ({ cfg, accountId }) => {
111
+ const account = resolveQQBotAccount(cfg, accountId);
112
+ const allowFrom = account.config?.allowFrom ?? [];
113
+ console.log(`[qqbot] resolveAllowFrom: accountId=${accountId}, allowFrom=${JSON.stringify(allowFrom)}`);
114
+ return allowFrom.map((entry) => String(entry));
115
+ },
116
+ // 格式化 allowFrom 条目(移除 qqbot: 前缀,统一大写)
117
+ formatAllowFrom: ({ allowFrom }) => allowFrom
118
+ .map((entry) => String(entry).trim())
119
+ .filter(Boolean)
120
+ .map((entry) => entry.replace(/^qqbot:/i, ""))
121
+ .map((entry) => entry.toUpperCase()), // QQ openid 是大写的
59
122
  },
60
123
  setup: {
124
+ // 新增:规范化账户 ID
125
+ resolveAccountId: ({ accountId }) => accountId?.trim().toLowerCase() || DEFAULT_ACCOUNT_ID,
126
+ // 新增:应用账户名称
127
+ applyAccountName: ({ cfg, accountId, name }) => applyAccountNameToChannelSection({
128
+ cfg,
129
+ channelKey: "qqbot",
130
+ accountId,
131
+ name,
132
+ }),
61
133
  validateInput: ({ input }) => {
62
134
  if (!input.token && !input.tokenFile && !input.useEnv) {
63
135
  return "QQBot requires --token (format: appId:clientSecret) or --use-env";
@@ -79,11 +151,14 @@ export const qqbotPlugin = {
79
151
  clientSecret,
80
152
  clientSecretFile: input.tokenFile,
81
153
  name: input.name,
154
+ imageServerBaseUrl: input.imageServerBaseUrl,
82
155
  });
83
156
  },
84
157
  },
85
158
  outbound: {
86
159
  deliveryMode: "direct",
160
+ chunker: chunkText,
161
+ chunkerMode: "markdown",
87
162
  textChunkLimit: 2000,
88
163
  sendText: async ({ to, text, accountId, replyToId, cfg }) => {
89
164
  const account = resolveQQBotAccount(cfg, accountId);
@@ -94,6 +169,15 @@ export const qqbotPlugin = {
94
169
  error: result.error ? new Error(result.error) : undefined,
95
170
  };
96
171
  },
172
+ sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
173
+ const account = resolveQQBotAccount(cfg, accountId);
174
+ const result = await sendMedia({ to, text: text ?? "", mediaUrl: mediaUrl ?? "", accountId, replyToId, account });
175
+ return {
176
+ channel: "qqbot",
177
+ messageId: result.messageId,
178
+ error: result.error ? new Error(result.error) : undefined,
179
+ };
180
+ },
97
181
  },
98
182
  gateway: {
99
183
  startAccount: async (ctx) => {
@@ -122,6 +206,44 @@ export const qqbotPlugin = {
122
206
  },
123
207
  });
124
208
  },
209
+ // 新增:登出账户(清除配置中的凭证)
210
+ logoutAccount: async ({ accountId, cfg }) => {
211
+ const nextCfg = { ...cfg };
212
+ const nextQQBot = cfg.channels?.qqbot ? { ...cfg.channels.qqbot } : undefined;
213
+ let cleared = false;
214
+ let changed = false;
215
+ if (nextQQBot) {
216
+ const qqbot = nextQQBot;
217
+ if (accountId === DEFAULT_ACCOUNT_ID && qqbot.clientSecret) {
218
+ delete qqbot.clientSecret;
219
+ cleared = true;
220
+ changed = true;
221
+ }
222
+ const accounts = qqbot.accounts;
223
+ if (accounts && accountId in accounts) {
224
+ const entry = accounts[accountId];
225
+ if (entry && "clientSecret" in entry) {
226
+ delete entry.clientSecret;
227
+ cleared = true;
228
+ changed = true;
229
+ }
230
+ if (entry && Object.keys(entry).length === 0) {
231
+ delete accounts[accountId];
232
+ changed = true;
233
+ }
234
+ }
235
+ }
236
+ if (changed && nextQQBot) {
237
+ nextCfg.channels = { ...nextCfg.channels, qqbot: nextQQBot };
238
+ const runtime = getQQBotRuntime();
239
+ const configApi = runtime.config;
240
+ await configApi.writeConfigFile(nextCfg);
241
+ }
242
+ const resolved = resolveQQBotAccount(changed ? nextCfg : cfg, accountId);
243
+ const loggedOut = resolved.secretSource === "none";
244
+ const envToken = Boolean(process.env.QQBOT_CLIENT_SECRET);
245
+ return { ok: true, cleared, envToken, loggedOut };
246
+ },
125
247
  },
126
248
  status: {
127
249
  defaultRuntime: {
@@ -130,7 +252,18 @@ export const qqbotPlugin = {
130
252
  connected: false,
131
253
  lastConnectedAt: null,
132
254
  lastError: null,
255
+ lastInboundAt: null,
256
+ lastOutboundAt: null,
133
257
  },
258
+ // 新增:构建通道摘要
259
+ buildChannelSummary: ({ snapshot }) => ({
260
+ configured: snapshot.configured ?? false,
261
+ tokenSource: snapshot.tokenSource ?? "none",
262
+ running: snapshot.running ?? false,
263
+ connected: snapshot.connected ?? false,
264
+ lastConnectedAt: snapshot.lastConnectedAt ?? null,
265
+ lastError: snapshot.lastError ?? null,
266
+ }),
134
267
  buildAccountSnapshot: ({ account, runtime }) => ({
135
268
  accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
136
269
  name: account?.name,
@@ -141,6 +274,8 @@ export const qqbotPlugin = {
141
274
  connected: runtime?.connected ?? false,
142
275
  lastConnectedAt: runtime?.lastConnectedAt ?? null,
143
276
  lastError: runtime?.lastError ?? null,
277
+ lastInboundAt: runtime?.lastInboundAt ?? null,
278
+ lastOutboundAt: runtime?.lastOutboundAt ?? null,
144
279
  }),
145
280
  },
146
281
  };
@@ -104,11 +104,15 @@ export function resolveQQBotAccount(cfg, accountId) {
104
104
  export function applyQQBotAccountConfig(cfg, accountId, input) {
105
105
  const next = { ...cfg };
106
106
  if (accountId === DEFAULT_ACCOUNT_ID) {
107
+ // 如果没有设置过 allowFrom,默认设置为 ["*"]
108
+ const existingConfig = next.channels?.qqbot || {};
109
+ const allowFrom = existingConfig.allowFrom ?? ["*"];
107
110
  next.channels = {
108
111
  ...next.channels,
109
112
  qqbot: {
110
113
  ...(next.channels?.qqbot || {}),
111
114
  enabled: true,
115
+ allowFrom,
112
116
  ...(input.appId ? { appId: input.appId } : {}),
113
117
  ...(input.clientSecret
114
118
  ? { clientSecret: input.clientSecret }
@@ -121,6 +125,9 @@ export function applyQQBotAccountConfig(cfg, accountId, input) {
121
125
  };
122
126
  }
123
127
  else {
128
+ // 如果没有设置过 allowFrom,默认设置为 ["*"]
129
+ const existingAccountConfig = next.channels?.qqbot?.accounts?.[accountId] || {};
130
+ const allowFrom = existingAccountConfig.allowFrom ?? ["*"];
124
131
  next.channels = {
125
132
  ...next.channels,
126
133
  qqbot: {
@@ -131,6 +138,7 @@ export function applyQQBotAccountConfig(cfg, accountId, input) {
131
138
  [accountId]: {
132
139
  ...(next.channels?.qqbot?.accounts?.[accountId] || {}),
133
140
  enabled: true,
141
+ allowFrom,
134
142
  ...(input.appId ? { appId: input.appId } : {}),
135
143
  ...(input.clientSecret
136
144
  ? { clientSecret: input.clientSecret }
@@ -13,5 +13,6 @@ export interface GatewayContext {
13
13
  }
14
14
  /**
15
15
  * 启动 Gateway WebSocket 连接(带自动重连)
16
+ * 支持流式消息发送
16
17
  */
17
18
  export declare function startGateway(ctx: GatewayContext): Promise<void>;