@tencent-connect/openclaw-qqbot 1.7.2 → 2.0.0

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 (291) hide show
  1. package/README.md +25 -8
  2. package/README.zh.md +24 -8
  3. package/bin/qqbot-cli.js +5 -2
  4. package/dist/index.cjs +15549 -0
  5. package/dist/index.cjs.map +1 -0
  6. package/dist/index.d.cts +890 -0
  7. package/index.ts +56 -19
  8. package/openclaw.plugin.json +19 -3
  9. package/package.json +14 -23
  10. package/preload.cjs +17 -14
  11. package/scripts/link-sdk-core.cjs +133 -50
  12. package/scripts/upgrade-via-npm.sh +42 -11
  13. package/scripts/upgrade-via-source.sh +4 -4
  14. package/skills/qqbot-channel/SKILL.md +34 -12
  15. package/src/adapter/contract.ts +69 -0
  16. package/src/adapter/gateway.ts +61 -0
  17. package/src/adapter/index.ts +15 -0
  18. package/src/adapter/lint.ts +144 -0
  19. package/src/adapter/media.ts +112 -0
  20. package/src/adapter/pairing.ts +99 -0
  21. package/src/adapter/resolve.ts +333 -0
  22. package/src/adapter/setup.ts +55 -0
  23. package/src/adapter/webhook.ts +248 -0
  24. package/src/adapter/workspace.ts +29 -0
  25. package/src/bot-instance.ts +60 -0
  26. package/src/channel.ts +202 -413
  27. package/src/commands/bot-approve.ts +143 -0
  28. package/src/commands/bot-clear-storage.ts +114 -0
  29. package/src/commands/bot-group-always.ts +62 -0
  30. package/src/commands/bot-help.ts +40 -0
  31. package/src/commands/bot-logs.ts +248 -0
  32. package/src/commands/bot-me.ts +18 -0
  33. package/src/commands/bot-pairing.ts +57 -0
  34. package/src/commands/bot-ping.ts +33 -0
  35. package/src/commands/bot-streaming.ts +55 -0
  36. package/src/commands/bot-upgrade.ts +56 -0
  37. package/src/commands/bot-version.ts +41 -0
  38. package/src/commands/config-util.ts +96 -0
  39. package/src/commands/index.ts +49 -0
  40. package/src/config.ts +46 -19
  41. package/src/dispatch/body-assembler.ts +356 -0
  42. package/src/dispatch/ctx-builder.ts +110 -0
  43. package/src/dispatch/dispatch.ts +352 -0
  44. package/src/dispatch/envelope-builder.ts +112 -0
  45. package/src/dispatch/index.ts +2 -0
  46. package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
  47. package/src/features/approval-utils.ts +41 -0
  48. package/src/features/credential-backup.ts +82 -0
  49. package/src/features/history-store.ts +19 -0
  50. package/src/features/msgid-cache.ts +55 -0
  51. package/src/features/onboarding.ts +38 -0
  52. package/src/{proactive.ts → features/proactive.ts} +68 -49
  53. package/src/features/ref-index-store.ts +282 -0
  54. package/src/{update-checker.ts → features/update-checker.ts} +15 -35
  55. package/src/gateway/event-handlers.ts +241 -0
  56. package/src/gateway/index.ts +3 -0
  57. package/src/gateway/lifecycle.ts +221 -0
  58. package/src/gateway/middleware-setup.ts +139 -0
  59. package/src/gateway/qqbot-gateway.ts +342 -0
  60. package/src/middleware/access-control.ts +130 -0
  61. package/src/middleware/attachment.ts +314 -0
  62. package/src/middleware/policy-injector.ts +66 -0
  63. package/src/openclaw-plugin-sdk.d.ts +81 -10
  64. package/src/outbound/cron-scheduler.ts +130 -0
  65. package/src/outbound/debounce.ts +102 -0
  66. package/src/outbound/deliver-pipeline.ts +235 -0
  67. package/src/outbound/image-size.ts +123 -0
  68. package/src/outbound/index.ts +3 -0
  69. package/src/outbound/local-file-router.ts +145 -0
  70. package/src/outbound/media-send.ts +377 -0
  71. package/src/outbound/outbound-service.ts +222 -0
  72. package/src/outbound/reply-limiter.ts +128 -0
  73. package/src/outbound/sanitize.ts +32 -0
  74. package/src/outbound/streaming-controller.ts +228 -0
  75. package/src/outbound/target.ts +63 -0
  76. package/src/outbound/tts-provider.ts +84 -0
  77. package/src/request-context.ts +9 -8
  78. package/src/runtime.ts +31 -6
  79. package/src/setup/account-key.ts +41 -0
  80. package/src/setup/finalize.ts +110 -0
  81. package/src/setup/login.ts +197 -0
  82. package/src/setup/surface.ts +40 -0
  83. package/src/tools/platform.ts +149 -0
  84. package/src/tools/remind.ts +2 -0
  85. package/src/types-augment.d.ts +54 -0
  86. package/src/types.ts +51 -9
  87. package/src/utils/mention.ts +52 -0
  88. package/src/utils/pkg-version.ts +84 -47
  89. package/src/utils/platform.ts +31 -7
  90. package/src/utils/plugin-logger.ts +104 -0
  91. package/src/utils/ssrf-guard.ts +32 -2
  92. package/src/utils/stt.ts +137 -0
  93. package/src/utils/voice-text.ts +61 -0
  94. package/tsconfig.json +2 -1
  95. package/tsup.config.ts +57 -0
  96. package/dist/index.d.ts +0 -18
  97. package/dist/index.js +0 -27
  98. package/dist/src/admin-resolver.d.ts +0 -33
  99. package/dist/src/admin-resolver.js +0 -157
  100. package/dist/src/api.d.ts +0 -309
  101. package/dist/src/api.js +0 -923
  102. package/dist/src/approval-handler.d.ts +0 -47
  103. package/dist/src/approval-handler.js +0 -372
  104. package/dist/src/channel.d.ts +0 -29
  105. package/dist/src/channel.js +0 -528
  106. package/dist/src/config.d.ts +0 -60
  107. package/dist/src/config.js +0 -288
  108. package/dist/src/credential-backup.d.ts +0 -31
  109. package/dist/src/credential-backup.js +0 -66
  110. package/dist/src/deliver-debounce.d.ts +0 -74
  111. package/dist/src/deliver-debounce.js +0 -174
  112. package/dist/src/gateway.d.ts +0 -18
  113. package/dist/src/gateway.js +0 -2025
  114. package/dist/src/group-history.d.ts +0 -136
  115. package/dist/src/group-history.js +0 -226
  116. package/dist/src/image-server.d.ts +0 -87
  117. package/dist/src/image-server.js +0 -570
  118. package/dist/src/inbound-attachments.d.ts +0 -60
  119. package/dist/src/inbound-attachments.js +0 -248
  120. package/dist/src/known-users.d.ts +0 -100
  121. package/dist/src/known-users.js +0 -263
  122. package/dist/src/message-gating.d.ts +0 -53
  123. package/dist/src/message-gating.js +0 -107
  124. package/dist/src/message-queue.d.ts +0 -91
  125. package/dist/src/message-queue.js +0 -257
  126. package/dist/src/onboarding.d.ts +0 -10
  127. package/dist/src/onboarding.js +0 -203
  128. package/dist/src/outbound-deliver.d.ts +0 -48
  129. package/dist/src/outbound-deliver.js +0 -392
  130. package/dist/src/outbound.d.ts +0 -220
  131. package/dist/src/outbound.js +0 -975
  132. package/dist/src/proactive.d.ts +0 -170
  133. package/dist/src/proactive.js +0 -399
  134. package/dist/src/ref-index-store.d.ts +0 -101
  135. package/dist/src/ref-index-store.js +0 -298
  136. package/dist/src/reply-dispatcher.d.ts +0 -35
  137. package/dist/src/reply-dispatcher.js +0 -311
  138. package/dist/src/request-context.d.ts +0 -25
  139. package/dist/src/request-context.js +0 -37
  140. package/dist/src/runtime.d.ts +0 -3
  141. package/dist/src/runtime.js +0 -13
  142. package/dist/src/session-store.d.ts +0 -52
  143. package/dist/src/session-store.js +0 -254
  144. package/dist/src/slash-commands.d.ts +0 -82
  145. package/dist/src/slash-commands.js +0 -2233
  146. package/dist/src/startup-greeting.d.ts +0 -30
  147. package/dist/src/startup-greeting.js +0 -97
  148. package/dist/src/streaming.d.ts +0 -247
  149. package/dist/src/streaming.js +0 -899
  150. package/dist/src/stt.d.ts +0 -21
  151. package/dist/src/stt.js +0 -70
  152. package/dist/src/tools/channel.d.ts +0 -16
  153. package/dist/src/tools/channel.js +0 -231
  154. package/dist/src/tools/remind.d.ts +0 -2
  155. package/dist/src/tools/remind.js +0 -255
  156. package/dist/src/transport/index.d.ts +0 -10
  157. package/dist/src/transport/index.js +0 -9
  158. package/dist/src/transport/webhook-transport.d.ts +0 -67
  159. package/dist/src/transport/webhook-transport.js +0 -245
  160. package/dist/src/transport/webhook-verify.d.ts +0 -48
  161. package/dist/src/transport/webhook-verify.js +0 -98
  162. package/dist/src/types.d.ts +0 -462
  163. package/dist/src/types.js +0 -22
  164. package/dist/src/typing-keepalive.d.ts +0 -27
  165. package/dist/src/typing-keepalive.js +0 -64
  166. package/dist/src/update-checker.d.ts +0 -36
  167. package/dist/src/update-checker.js +0 -171
  168. package/dist/src/utils/audio-convert.d.ts +0 -98
  169. package/dist/src/utils/audio-convert.js +0 -783
  170. package/dist/src/utils/chunked-upload.d.ts +0 -68
  171. package/dist/src/utils/chunked-upload.js +0 -341
  172. package/dist/src/utils/file-utils.d.ts +0 -61
  173. package/dist/src/utils/file-utils.js +0 -172
  174. package/dist/src/utils/image-size.d.ts +0 -51
  175. package/dist/src/utils/image-size.js +0 -234
  176. package/dist/src/utils/media-send.d.ts +0 -158
  177. package/dist/src/utils/media-send.js +0 -502
  178. package/dist/src/utils/media-tags.d.ts +0 -14
  179. package/dist/src/utils/media-tags.js +0 -165
  180. package/dist/src/utils/payload.d.ts +0 -112
  181. package/dist/src/utils/payload.js +0 -186
  182. package/dist/src/utils/pkg-version.d.ts +0 -5
  183. package/dist/src/utils/pkg-version.js +0 -61
  184. package/dist/src/utils/platform.d.ts +0 -137
  185. package/dist/src/utils/platform.js +0 -390
  186. package/dist/src/utils/ssrf-guard.d.ts +0 -25
  187. package/dist/src/utils/ssrf-guard.js +0 -91
  188. package/dist/src/utils/text-parsing.d.ts +0 -31
  189. package/dist/src/utils/text-parsing.js +0 -74
  190. package/dist/src/utils/upload-cache.d.ts +0 -34
  191. package/dist/src/utils/upload-cache.js +0 -93
  192. package/node_modules/@eshaz/web-worker/LICENSE +0 -201
  193. package/node_modules/@eshaz/web-worker/README.md +0 -134
  194. package/node_modules/@eshaz/web-worker/browser.js +0 -17
  195. package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
  196. package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
  197. package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
  198. package/node_modules/@eshaz/web-worker/node.js +0 -223
  199. package/node_modules/@eshaz/web-worker/package.json +0 -54
  200. package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
  201. package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
  202. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
  203. package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
  204. package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
  205. package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
  206. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
  207. package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
  208. package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
  209. package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
  210. package/node_modules/mpg123-decoder/README.md +0 -265
  211. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
  212. package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
  213. package/node_modules/mpg123-decoder/index.js +0 -8
  214. package/node_modules/mpg123-decoder/package.json +0 -58
  215. package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
  216. package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
  217. package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
  218. package/node_modules/mpg123-decoder/types.d.ts +0 -30
  219. package/node_modules/silk-wasm/LICENSE +0 -21
  220. package/node_modules/silk-wasm/README.md +0 -85
  221. package/node_modules/silk-wasm/lib/index.cjs +0 -16
  222. package/node_modules/silk-wasm/lib/index.d.ts +0 -70
  223. package/node_modules/silk-wasm/lib/index.mjs +0 -16
  224. package/node_modules/silk-wasm/lib/silk.wasm +0 -0
  225. package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
  226. package/node_modules/silk-wasm/package.json +0 -39
  227. package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
  228. package/node_modules/simple-yenc/.prettierignore +0 -1
  229. package/node_modules/simple-yenc/LICENSE +0 -7
  230. package/node_modules/simple-yenc/README.md +0 -163
  231. package/node_modules/simple-yenc/dist/esm.js +0 -1
  232. package/node_modules/simple-yenc/dist/index.js +0 -1
  233. package/node_modules/simple-yenc/package.json +0 -50
  234. package/node_modules/simple-yenc/rollup.config.js +0 -27
  235. package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
  236. package/node_modules/ws/LICENSE +0 -20
  237. package/node_modules/ws/README.md +0 -548
  238. package/node_modules/ws/browser.js +0 -8
  239. package/node_modules/ws/index.js +0 -13
  240. package/node_modules/ws/lib/buffer-util.js +0 -131
  241. package/node_modules/ws/lib/constants.js +0 -19
  242. package/node_modules/ws/lib/event-target.js +0 -292
  243. package/node_modules/ws/lib/extension.js +0 -203
  244. package/node_modules/ws/lib/limiter.js +0 -55
  245. package/node_modules/ws/lib/permessage-deflate.js +0 -528
  246. package/node_modules/ws/lib/receiver.js +0 -706
  247. package/node_modules/ws/lib/sender.js +0 -602
  248. package/node_modules/ws/lib/stream.js +0 -161
  249. package/node_modules/ws/lib/subprotocol.js +0 -62
  250. package/node_modules/ws/lib/validation.js +0 -152
  251. package/node_modules/ws/lib/websocket-server.js +0 -554
  252. package/node_modules/ws/lib/websocket.js +0 -1393
  253. package/node_modules/ws/package.json +0 -69
  254. package/node_modules/ws/wrapper.mjs +0 -8
  255. package/scripts/postinstall-link-sdk.js +0 -170
  256. package/skills/qqbot-media/SKILL.md +0 -60
  257. package/src/admin-resolver.ts +0 -181
  258. package/src/api.ts +0 -1333
  259. package/src/credential-backup.ts +0 -72
  260. package/src/deliver-debounce.ts +0 -229
  261. package/src/gateway.ts +0 -2272
  262. package/src/group-history.ts +0 -328
  263. package/src/image-server.ts +0 -675
  264. package/src/inbound-attachments.ts +0 -321
  265. package/src/known-users.ts +0 -353
  266. package/src/message-gating.ts +0 -190
  267. package/src/message-queue.ts +0 -354
  268. package/src/onboarding.ts +0 -282
  269. package/src/outbound-deliver.ts +0 -486
  270. package/src/outbound.ts +0 -1179
  271. package/src/ref-index-store.ts +0 -412
  272. package/src/reply-dispatcher.ts +0 -334
  273. package/src/session-store.ts +0 -303
  274. package/src/slash-commands.ts +0 -2431
  275. package/src/startup-greeting.ts +0 -120
  276. package/src/streaming.ts +0 -1077
  277. package/src/stt.ts +0 -86
  278. package/src/tools/channel.ts +0 -275
  279. package/src/transport/index.ts +0 -11
  280. package/src/transport/webhook-transport.ts +0 -332
  281. package/src/transport/webhook-verify.ts +0 -119
  282. package/src/typing-keepalive.ts +0 -59
  283. package/src/utils/audio-convert.ts +0 -887
  284. package/src/utils/chunked-upload.ts +0 -483
  285. package/src/utils/file-utils.ts +0 -193
  286. package/src/utils/image-size.ts +0 -266
  287. package/src/utils/media-send.ts +0 -636
  288. package/src/utils/media-tags.ts +0 -183
  289. package/src/utils/payload.ts +0 -265
  290. package/src/utils/text-parsing.ts +0 -85
  291. package/src/utils/upload-cache.ts +0 -128
package/src/channel.ts CHANGED
@@ -1,155 +1,127 @@
1
+ /**
2
+ * QQ Bot ChannelPlugin 定义
3
+ *
4
+ * 薄壳编排层 — 实现 OpenClaw ChannelPlugin 接口,
5
+ * 将各子模块(gateway/outbound/config/features)连接为完整通道插件。
6
+ */
1
7
  import {
2
8
  type ChannelPlugin,
3
9
  type OpenClawConfig,
4
10
  applyAccountNameToChannelSection,
5
11
  deleteAccountFromConfigSection,
6
12
  setAccountEnabledInConfigSection,
7
- } from "openclaw/plugin-sdk/core";
8
-
9
- import type { ResolvedQQBotAccount } from "./types.js";
10
- import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount, applyQQBotAccountConfig, resolveDefaultQQBotAccountId, resolveRequireMention, resolveToolPolicy, resolveGroupConfig } from "./config.js";
11
- import { sendText, sendMedia, resolveUserFacingMediaError } from "./outbound.js";
12
- import { startGateway } from "./gateway.js";
13
- import { qqbotOnboardingAdapter } from "./onboarding.js";
14
- import { getQQBotRuntime } from "./runtime.js";
15
- import { saveCredentialBackup, loadCredentialBackup } from "./credential-backup.js";
16
- import { initApiConfig } from "./api.js";
17
- import { getApprovalHandler } from "./approval-handler.js";
13
+ } from 'openclaw/plugin-sdk/core';
18
14
 
19
- /** 检查 payload 是否为审批消息(与 getExecApprovalReplyMetadata 等效,内联避免版本兼容问题) */
20
- function isApprovalPayload(payload: unknown): boolean {
21
- if (!payload || typeof payload !== "object") return false;
22
- const p = payload as Record<string, unknown>;
23
- // channelData.execApproval 存在 → exec/plugin approval pending/resolved
24
- const cd = p.channelData;
25
- if (cd && typeof cd === "object" && !Array.isArray(cd)) {
26
- const execApproval = (cd as Record<string, unknown>).execApproval;
27
- if (execApproval && typeof execApproval === "object" && !Array.isArray(execApproval)) {
28
- return true;
29
- }
30
- }
31
- // text 匹配兜底:框架渲染的审批纯文本通知
32
- const text = typeof p.text === "string" ? p.text : "";
33
- return /(?:Plugin|Exec) approval (?:required|allowed|denied|expired)/i.test(text);
34
- }
15
+ import type { ResolvedQQBotAccount } from './types.js';
16
+ import {
17
+ DEFAULT_ACCOUNT_ID,
18
+ listQQBotAccountIds,
19
+ resolveQQBotAccount,
20
+ applyQQBotAccountConfig,
21
+ resolveDefaultQQBotAccountId,
22
+ resolveRequireMention,
23
+ resolveToolPolicy,
24
+ resolveGroupConfig,
25
+ } from './config.js';
26
+ import { getQQBotRuntime, tryGetQQBotRuntime } from './runtime.js';
27
+ import { getAdapters } from './adapter/resolve.js';
28
+ import { sendText, getGateway } from './outbound/outbound-service.js';
29
+ import { sendMedia } from './outbound/media-send.js';
30
+ import type { PluginLogger } from './utils/plugin-logger.js';
31
+ import { qqbotSetupWizard } from './setup/surface.js';
32
+ import { qqbotLogin, startQrLogin, waitQrLogin } from './setup/login.js';
33
+ import { normalizeTarget, isQQBotTarget } from './outbound/target.js';
34
+ import { sanitizeQQBotText } from './outbound/sanitize.js';
35
+ import { startAccountWithCredentialRecovery, logoutAndClearCredentials, stopAccountGracefully } from './gateway/lifecycle.js';
36
+ import { loadCredentialBackup } from './features/credential-backup.js';
37
+ import { isApprovalPayload, approvalStubs } from './features/approval-utils.js';
38
+ import { qqbotOnboardingAdapter } from './features/onboarding.js';
39
+ import { stripMentionText } from './utils/mention.js';
35
40
 
36
41
  /** QQ Bot 单条消息文本长度上限 */
37
42
  export const TEXT_CHUNK_LIMIT = 5000;
38
43
 
44
+ // ── GFM 表格检测 ──
45
+
46
+ /** GFM 表格数据行: | col1 | col2 | */
47
+ const GFM_TABLE_DATA_RE = /^\|.+\|.*\|/;
48
+ /** GFM 表格分隔行: |---|:---:|---| (1 个或多于 1 个破折号,支持对齐冒号) */
49
+ const GFM_TABLE_SEP_RE = /^\|[\s:-]+\|/;
50
+
39
51
  /**
40
- * Markdown 感知的文本分块函数
41
- * 委托给 SDK 内置的 channel.text.chunkMarkdownText
42
- * 支持代码块自动关闭/重开、括号感知等
52
+ * 判断一行是否为 GFM 表格行(数据行或分隔行)。
53
+ * 保障 table-aware chunker 不会在表格内部切分。
43
54
  */
44
- export function chunkText(text: string, limit: number): string[] {
45
- const runtime = getQQBotRuntime();
46
- return runtime.channel.text.chunkMarkdownText(text, limit);
47
- }
48
-
49
- function buildChannelMediaError(result: Parameters<typeof resolveUserFacingMediaError>[0]): Error {
50
- const err = new Error(resolveUserFacingMediaError(result));
51
- if (result.errorCode) {
52
- (err as Error & { code?: string }).code = result.errorCode;
53
- }
54
- if (result.qqBizCode !== undefined) {
55
- (err as Error & { qqBizCode?: number }).qqBizCode = result.qqBizCode;
56
- }
57
- return err;
55
+ function isGfmTableLine(line: string): boolean {
56
+ return GFM_TABLE_DATA_RE.test(line) || GFM_TABLE_SEP_RE.test(line);
58
57
  }
59
58
 
60
59
  export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
61
- id: "qqbot",
60
+ id: 'qqbot',
62
61
  meta: {
63
- id: "qqbot",
64
- label: "QQ Bot",
65
- selectionLabel: "QQ Bot",
66
- docsPath: "/docs/channels/qqbot",
67
- blurb: "Connect to QQ via official QQ Bot API",
62
+ id: 'qqbot',
63
+ label: 'QQ Bot',
64
+ selectionLabel: 'QQ Bot',
65
+ docsPath: '/docs/channels/qqbot',
66
+ blurb: 'Connect to QQ via official QQ Bot API',
68
67
  order: 50,
69
68
  },
70
69
  capabilities: {
71
- chatTypes: ["direct", "group"],
70
+ chatTypes: ['direct', 'group'],
72
71
  media: true,
73
72
  reactions: false,
74
73
  threads: false,
75
- /**
76
- * blockStreaming: true 表示该 Channel 支持块流式
77
- * 框架会收集流式响应,然后通过 deliver 回调发送
78
- */
79
- blockStreaming: true,
74
+ blockStreaming: false,
80
75
  },
81
- reload: { configPrefixes: ["channels.qqbot"] },
76
+ gatewayMethods: ['web.login.start', 'web.login.wait'],
77
+ reload: { configPrefixes: ['channels.qqbot'] },
82
78
 
83
- // ============ 群消息策略适配器 ============
79
+ // ── 群消息策略 ──
84
80
  groups: {
85
- /** 是否需要 @机器人才响应 */
86
81
  resolveRequireMention: ({ cfg, accountId, groupId }) => {
87
82
  if (!groupId) return undefined;
88
83
  return resolveRequireMention(cfg, groupId, accountId ?? undefined);
89
84
  },
90
-
91
- /** 群聊工具范围 */
92
85
  resolveToolPolicy: ({ cfg, accountId, groupId }) => {
93
86
  if (!groupId) return undefined;
94
87
  const policy = resolveToolPolicy(cfg, groupId, accountId ?? undefined);
95
- // 将简单字符串策略映射为 GroupToolPolicyConfig 对象
96
- if (policy === "full") return undefined; // full = 默认不限制
97
- if (policy === "none") return { allow: [], deny: ["*"] };
98
- // restricted: 默认空 allow(框架会使用内置 restricted 列表)
88
+ if (policy === 'full') return undefined;
89
+ if (policy === 'none') return { allow: [], deny: ['*'] };
99
90
  return { allow: [] };
100
91
  },
101
-
102
- /** QQ Bot 平台特有的群聊行为提示 */
103
92
  resolveGroupIntroHint: ({ cfg, accountId, groupId }) => {
104
93
  if (!groupId) return undefined;
105
94
  const groupCfg = resolveGroupConfig(cfg, groupId, accountId ?? undefined);
106
- const hints: string[] = [];
107
- if (groupCfg.name) {
108
- hints.push(`当前群: ${groupCfg.name}`);
109
- }
110
- // bot 互聊防护、@状态行为指引在 gateway.ts 动态注入
111
- return hints.join(" ") || undefined;
95
+ return groupCfg.name ? `当前群: ${groupCfg.name}` : undefined;
112
96
  },
113
97
  },
114
98
 
115
- // ============ @mention 检测与清理 ============
99
+ // ── @mention 检测与清理 ──
116
100
  mentions: {
117
- /** 清理 @mention 文本(SDK ChannelMentionAdapter 接口) */
118
101
  stripMentions: ({ text, ctx }) => {
119
- const mentions = (ctx as any)?.mentions as Array<{ member_openid?: string; id?: string; user_openid?: string; is_you?: boolean; nickname?: string; username?: string }> | undefined;
102
+ const mentions = (ctx as any)?.mentions;
120
103
  return stripMentionText(text, mentions);
121
104
  },
122
105
  },
123
- // CLI onboarding wizard
124
- // @ts-ignore onboarding removed from ChannelPlugin type in 2026.3.23 but still supported at runtime
106
+
107
+ // @ts-ignore onboarding 兼容
125
108
  onboarding: qqbotOnboardingAdapter,
126
109
 
110
+ // ── 配置管理 ──
127
111
  config: {
128
112
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
129
113
  resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId),
130
114
  defaultAccountId: (cfg) => resolveDefaultQQBotAccountId(cfg),
131
- // 新增:设置账户启用状态
132
115
  setAccountEnabled: ({ cfg, accountId, enabled }) =>
133
- setAccountEnabledInConfigSection({
134
- cfg,
135
- sectionKey: "qqbot",
136
- accountId,
137
- enabled,
138
- allowTopLevel: true,
139
- }),
140
- // 新增:删除账户
116
+ setAccountEnabledInConfigSection({ cfg, sectionKey: 'qqbot', accountId, enabled, allowTopLevel: true }),
141
117
  deleteAccount: ({ cfg, accountId }) =>
142
118
  deleteAccountFromConfigSection({
143
- cfg,
144
- sectionKey: "qqbot",
145
- accountId,
146
- clearBaseFields: ["appId", "clientSecret", "clientSecretFile", "name"],
119
+ cfg, sectionKey: 'qqbot', accountId,
120
+ clearBaseFields: ['appId', 'clientSecret', 'clientSecretFile', 'name'],
147
121
  }),
148
122
  isConfigured: (account) => {
149
123
  if (account?.appId && account?.clientSecret) return true;
150
- // 配置为空但有凭证备份时仍返回 true,让 startAccount 有机会恢复凭证
151
- const backup = loadCredentialBackup(account?.accountId);
152
- return backup !== null;
124
+ return loadCredentialBackup(account?.accountId) !== null;
153
125
  },
154
126
  describeAccount: (account) => ({
155
127
  accountId: account?.accountId ?? DEFAULT_ACCOUNT_ID,
@@ -158,275 +130,160 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
158
130
  configured: Boolean(account?.appId && account?.clientSecret),
159
131
  tokenSource: account?.secretSource,
160
132
  }),
161
- // 关键:解析 allowFrom 配置,用于命令授权
162
133
  resolveAllowFrom: ({ cfg, accountId }: { cfg: OpenClawConfig; accountId?: string | null }) => {
163
134
  const account = resolveQQBotAccount(cfg, accountId ?? undefined);
164
- const allowFrom = account.config?.allowFrom ?? [];
165
- console.log(`[qqbot] resolveAllowFrom: accountId=${accountId}, allowFrom=${JSON.stringify(allowFrom)}`);
166
- return allowFrom.map((entry: string | number) => String(entry)) as (string | number)[];
135
+ return (account.config?.allowFrom ?? []).map((e: string | number) => String(e)) as (string | number)[];
167
136
  },
168
- // 格式化 allowFrom 条目(移除 qqbot: 前缀,统一大写)
169
- formatAllowFrom: ({ allowFrom }: { allowFrom: Array<string | number> }) =>
137
+ formatAllowFrom: ({ allowFrom }: { allowFrom: (string | number)[] }) =>
170
138
  allowFrom
171
- .map((entry: string | number) => String(entry).trim())
139
+ .map((e: string | number) => String(e).trim())
172
140
  .filter(Boolean)
173
- .map((entry: string) => entry.replace(/^qqbot:/i, ""))
174
- .map((entry: string) => entry.toUpperCase()), // QQ openid 是大写的
141
+ .map((e: string) => e.replace(/^qqbot:/i, '').toUpperCase()),
175
142
  },
143
+
144
+ // ── Setup ──
176
145
  setup: {
177
- // 新增:规范化账户 ID
178
146
  resolveAccountId: ({ accountId }) => accountId?.trim().toLowerCase() || DEFAULT_ACCOUNT_ID,
179
- // 新增:应用账户名称
180
147
  applyAccountName: ({ cfg, accountId, name }) =>
181
- applyAccountNameToChannelSection({
182
- cfg,
183
- channelKey: "qqbot",
184
- accountId,
185
- name,
186
- }),
148
+ applyAccountNameToChannelSection({ cfg, channelKey: 'qqbot', accountId, name }),
187
149
  validateInput: ({ input }) => {
188
150
  if (!input.token && !input.tokenFile && !input.useEnv) {
189
- return "QQBot requires --token (format: appId:clientSecret) or --use-env";
151
+ return 'QQBot requires --token (format: appId:clientSecret) or --use-env';
190
152
  }
191
153
  return null;
192
154
  },
193
155
  applyAccountConfig: ({ cfg, accountId, input }) => {
194
- let appId = "";
195
- let clientSecret = "";
196
-
156
+ let appId = '';
157
+ let clientSecret = '';
197
158
  if (input.token) {
198
- const parts = input.token.split(":");
199
- if (parts.length === 2) {
200
- appId = parts[0];
201
- clientSecret = parts[1];
202
- }
159
+ const parts = input.token.split(':');
160
+ if (parts.length === 2) { appId = parts[0]; clientSecret = parts[1]; }
203
161
  }
204
-
205
162
  return applyQQBotAccountConfig(cfg, accountId, {
206
- appId,
207
- clientSecret,
163
+ appId, clientSecret,
208
164
  clientSecretFile: input.tokenFile,
209
165
  name: input.name,
210
- imageServerBaseUrl: (input as Record<string, unknown>).imageServerBaseUrl as string | undefined,
211
166
  }) as OpenClawConfig;
212
167
  },
213
168
  },
214
- // Messaging 配置:用于解析目标地址
215
- messaging: {
216
- /**
217
- * 规范化目标地址
218
- * 支持以下格式:
219
- * - qqbot:c2c:openid -> 私聊
220
- * - qqbot:group:groupid -> 群聊
221
- * - qqbot:channel:channelid -> 频道
222
- * - c2c:openid -> 私聊
223
- * - group:groupid -> 群聊
224
- * - channel:channelid -> 频道
225
- * - 纯 openid(32位十六进制)-> 私聊
226
- */
227
- normalizeTarget: (target: string): string | undefined => {
228
- // 去掉 qqbot: 前缀(如果有)
229
- const id = target.replace(/^qqbot:/i, "");
230
-
231
- // 检查是否是已知格式
232
- if (id.startsWith("c2c:") || id.startsWith("group:") || id.startsWith("channel:")) {
233
- return `qqbot:${id}`;
234
- }
235
-
236
- // 检查是否是纯 openid(32位十六进制,不带连字符)
237
- // QQ Bot OpenID 格式类似: 207A5B8339D01F6582911C014668B77B
238
- const openIdHexPattern = /^[0-9a-fA-F]{32}$/;
239
- if (openIdHexPattern.test(id)) {
240
- return `qqbot:c2c:${id}`;
241
- }
242
169
 
243
- // 检查是否是 UUID 格式的 openid(带连字符)
244
- const openIdUuidPattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
245
- if (openIdUuidPattern.test(id)) {
246
- return `qqbot:c2c:${id}`;
247
- }
248
-
249
- // 不认识的格式,返回 undefined 让核心使用原始值
250
- return undefined;
251
- },
252
- /**
253
- * 目标解析器配置
254
- * 用于判断一个目标 ID 是否看起来像 QQ Bot 的格式
255
- */
170
+ setupWizard: qqbotSetupWizard,
171
+
172
+ // ── Messaging ──
173
+ messaging: {
174
+ normalizeTarget,
256
175
  targetResolver: {
257
- /**
258
- * 判断目标 ID 是否可能是 QQ Bot 格式
259
- * 支持以下格式:
260
- * - qqbot:c2c:xxx
261
- * - qqbot:group:xxx
262
- * - qqbot:channel:xxx
263
- * - c2c:xxx
264
- * - group:xxx
265
- * - channel:xxx
266
- * - UUID 格式的 openid
267
- */
268
- looksLikeId: (id: string): boolean => {
269
- // qqbot: 前缀的格式
270
- if (/^qqbot:(c2c|group|channel):/i.test(id)) {
271
- return true;
176
+ looksLikeId: isQQBotTarget,
177
+ hint: 'QQ Bot 目标格式: qqbot:c2c:openid (私聊) 或 qqbot:group:groupid (群聊)',
178
+ },
179
+ },
180
+
181
+ // ── 出站 ──
182
+ outbound: {
183
+ deliveryMode: 'direct' as const,
184
+ sanitizeText: ({ text }: { text: string; payload: any }) => sanitizeQQBotText(text),
185
+ chunker: (text, limit) => {
186
+ const adapters = getAdapters(getQQBotRuntime());
187
+ if (adapters.chunkMarkdownText) return adapters.chunkMarkdownText(text, limit);
188
+ // fallback(低版本降级): 按换行边界切分,保留 Markdown 表格完整性
189
+ const lines = text.split('\n');
190
+ const chunks: string[] = [];
191
+ let current = '';
192
+ let tableBuffer: string[] = [];
193
+
194
+ const flushTable = () => {
195
+ if (tableBuffer.length === 0) return;
196
+ const tableBlock = tableBuffer.join('\n');
197
+ const candidate = current ? `${current}\n${tableBlock}` : tableBlock;
198
+ if (candidate.length > limit && current) {
199
+ chunks.push(current);
200
+ current = tableBlock;
201
+ } else {
202
+ current = candidate;
272
203
  }
273
- // 不带前缀但有类型标识
274
- if (/^(c2c|group|channel):/i.test(id)) {
275
- return true;
204
+ tableBuffer = [];
205
+ };
206
+
207
+ for (const line of lines) {
208
+ if (isGfmTableLine(line)) {
209
+ tableBuffer.push(line);
210
+ continue;
276
211
  }
277
- // 32位十六进制 openid(不带连字符)
278
- if (/^[0-9a-fA-F]{32}$/.test(id)) {
279
- return true;
212
+
213
+ // 遇到非表格行,先刷新缓冲的表格
214
+ flushTable();
215
+
216
+ const candidate = current ? `${current}\n${line}` : line;
217
+ if (candidate.length > limit && current) {
218
+ chunks.push(current);
219
+ current = line;
220
+ } else {
221
+ current = candidate;
280
222
  }
281
- // UUID 格式的 openid(带连字符)
282
- const openIdPattern = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
283
- return openIdPattern.test(id);
284
- },
285
- hint: "QQ Bot 目标格式: qqbot:c2c:openid (私聊) 或 qqbot:group:groupid (群聊)",
223
+ }
224
+ // 处理末尾的表格缓冲
225
+ flushTable();
226
+ if (current) chunks.push(current);
227
+ return chunks.length > 0 ? chunks : [text];
286
228
  },
287
- },
288
- outbound: {
289
- deliveryMode: "direct",
290
- chunker: (text, limit) => getQQBotRuntime().channel.text.chunkMarkdownText(text, limit),
291
- chunkerMode: "markdown",
292
- textChunkLimit: 5000,
293
- // 3.31+ outbound 路径:dispatch-from-config → shouldSuppressLocalExecApprovalPrompt → outbound.shouldSuppressLocalPayloadPrompt
294
- shouldSuppressLocalPayloadPrompt: ({ accountId, payload }: any) =>
295
- getApprovalHandler(accountId ?? "") != null &&
296
- isApprovalPayload(payload),
229
+ chunkerMode: 'markdown',
230
+ textChunkLimit: TEXT_CHUNK_LIMIT,
231
+ shouldSuppressLocalPayloadPrompt: ({ payload }: any) => isApprovalPayload(payload),
297
232
  sendText: async ({ to, text, accountId, replyToId, cfg }) => {
298
- console.log(`[qqbot:channel] sendText called — accountId=${accountId}, to=${to}, replyToId=${replyToId}, text.length=${text?.length ?? 0}`);
299
- console.log(`[qqbot:channel] sendText text preview: ${text?.slice(0, 100)}${(text?.length ?? 0) > 100 ? "..." : ""}`);
300
233
  const account = resolveQQBotAccount(cfg, accountId ?? undefined);
301
- initApiConfig({ markdownSupport: account.markdownSupport });
302
- console.log(`[qqbot:channel] sendText resolved account: id=${account.accountId}, appId=${account.appId}, enabled=${account.enabled}`);
234
+ const outLog = createOutLog(account.accountId);
235
+ outLog.debug(`sendText to=${to} len=${text.length} replyTo=${replyToId ?? '-'}`);
303
236
  const result = await sendText({ to, text, accountId, replyToId, account });
304
- console.log(`[qqbot:channel] sendText result: messageId=${result.messageId}, error=${result.error ?? "none"}`);
305
237
  if (result.error) throw new Error(result.error);
306
- return {
307
- channel: "qqbot" as const,
308
- messageId: result.messageId ?? "",
309
- };
238
+ return { channel: 'qqbot' as const, messageId: result.messageId ?? '' };
310
239
  },
311
240
  sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
312
- console.log(`[qqbot:channel] sendMedia called accountId=${accountId}, to=${to}, replyToId=${replyToId}, mediaUrl=${mediaUrl?.slice(0, 80)}, text.length=${text?.length ?? 0}`);
313
- const account = resolveQQBotAccount(cfg, accountId ?? undefined);
314
- initApiConfig({ markdownSupport: account.markdownSupport });
315
- console.log(`[qqbot:channel] sendMedia resolved account: id=${account.accountId}, appId=${account.appId}, enabled=${account.enabled}`);
316
- const result = await sendMedia({ to, text: text ?? "", mediaUrl: mediaUrl ?? "", accountId, replyToId, account });
317
- console.log(`[qqbot:channel] sendMedia result: messageId=${result.messageId}, error=${result.error ?? "none"}`);
318
- // 此 sendMedia 是框架 Channel Plugin 的标准出站接口,
319
- // 由框架 deliver.js (deliverOutboundPayloads) 或 message-actions 调用。
320
- // 当 throw Error 后,框架 pi-tool-definition-adapter 会将错误转化为
321
- // tool 的 { status: "error" } 返回给 AI 模型,模型会自行生成错误回复给用户。
322
- // 因此此处不应主动发送兜底文本,否则会与模型的回复重复。
241
+ const resolvedAccountId = accountId ?? resolveDefaultQQBotAccountId(cfg);
242
+ const outLog = createOutLog(resolvedAccountId);
243
+ outLog.debug(`sendMedia to=${to} url=${mediaUrl?.slice(0, 80)} len=${text?.length ?? 0} replyTo=${replyToId ?? '-'}`);
244
+ const result = await sendMedia({
245
+ to,
246
+ source: mediaUrl ?? '',
247
+ text,
248
+ replyToId,
249
+ accountId: resolvedAccountId,
250
+ log: outLog,
251
+ agentId: resolveMCPAgentId(to, resolvedAccountId, cfg, outLog),
252
+ });
323
253
  if (result.error) {
324
- throw buildChannelMediaError(result);
254
+ outLog.error(`sendMedia failed: ${result.error}`);
255
+ throw new Error(result.error);
325
256
  }
326
- return {
327
- channel: "qqbot" as const,
328
- messageId: result.messageId ?? "",
329
- };
257
+ return { channel: 'qqbot' as const, messageId: result.messageId ?? '' };
330
258
  },
331
259
  },
332
- gateway: {
333
- startAccount: async (ctx) => {
334
- let { account } = ctx;
335
- const { abortSignal, log, cfg } = ctx;
336
-
337
- // 凭证恢复:如果 appId/secret 为空(热更新打断可能导致配置丢失),尝试从暂存文件恢复
338
- if (!account.appId || !account.clientSecret) {
339
- const backup = loadCredentialBackup(account.accountId);
340
- if (backup) {
341
- log?.info(`[qqbot:${account.accountId}] 配置中凭证为空,从暂存文件恢复 (appId=${backup.appId}, savedAt=${backup.savedAt})`);
342
- try {
343
- const runtime = getQQBotRuntime();
344
- const restoredCfg = applyQQBotAccountConfig(cfg, account.accountId, {
345
- appId: backup.appId,
346
- clientSecret: backup.clientSecret,
347
- });
348
- const configApi = runtime.config as { writeConfigFile: (cfg: unknown) => Promise<void> };
349
- await configApi.writeConfigFile(restoredCfg);
350
- // 重新解析 account 以获取恢复后的值
351
- account = resolveQQBotAccount(restoredCfg, account.accountId);
352
- log?.info(`[qqbot:${account.accountId}] 凭证已恢复`);
353
- } catch (e) {
354
- log?.error(`[qqbot:${account.accountId}] 凭证恢复失败: ${e}`);
355
- }
356
- }
357
- }
358
-
359
- log?.info(`[qqbot:${account.accountId}] Starting gateway — appId=${account.appId}, enabled=${account.enabled}, name=${account.name ?? "unnamed"}`);
360
- console.log(`[qqbot:channel] startAccount: accountId=${account.accountId}, appId=${account.appId}, secretSource=${account.secretSource}`);
361
260
 
362
- await startGateway({
363
- account,
364
- abortSignal,
365
- cfg,
366
- log,
367
- onReady: () => {
368
- log?.info(`[qqbot:${account.accountId}] Gateway ready`);
369
- // 启动成功,保存凭证快照供后续恢复使用
370
- saveCredentialBackup(account.accountId, account.appId, account.clientSecret);
371
- ctx.setStatus({
372
- ...ctx.getStatus(),
373
- running: true,
374
- connected: true,
375
- lastConnectedAt: Date.now(),
376
- });
377
- },
378
- onError: (error) => {
379
- log?.error(`[qqbot:${account.accountId}] Gateway error: ${error.message}`);
380
- ctx.setStatus({
381
- ...ctx.getStatus(),
382
- lastError: error.message,
383
- });
384
- },
261
+ // ── 网关 ──
262
+ gateway: {
263
+ startAccount: (ctx) => startAccountWithCredentialRecovery(ctx),
264
+ stopAccount: async (ctx: { accountId: string; log?: any }) => {
265
+ await stopAccountGracefully({
266
+ accountId: ctx.accountId,
267
+ log: ctx.log,
385
268
  });
386
269
  },
387
- // 新增:登出账户(清除配置中的凭证)
388
- logoutAccount: async ({ accountId, cfg }) => {
389
- const nextCfg = { ...cfg } as OpenClawConfig;
390
- const nextQQBot = cfg.channels?.qqbot ? { ...cfg.channels.qqbot } : undefined;
391
- let cleared = false;
392
- let changed = false;
393
-
394
- if (nextQQBot) {
395
- const qqbot = nextQQBot as Record<string, unknown>;
396
- if (accountId === DEFAULT_ACCOUNT_ID && qqbot.clientSecret) {
397
- delete qqbot.clientSecret;
398
- cleared = true;
399
- changed = true;
400
- }
401
- const accounts = qqbot.accounts as Record<string, Record<string, unknown>> | undefined;
402
- if (accounts && accountId in accounts) {
403
- const entry = accounts[accountId] as Record<string, unknown> | undefined;
404
- if (entry && "clientSecret" in entry) {
405
- delete entry.clientSecret;
406
- cleared = true;
407
- changed = true;
408
- }
409
- if (entry && Object.keys(entry).length === 0) {
410
- delete accounts[accountId];
411
- changed = true;
412
- }
413
- }
414
- }
415
-
416
- if (changed && nextQQBot) {
417
- nextCfg.channels = { ...nextCfg.channels, qqbot: nextQQBot };
418
- const runtime = getQQBotRuntime();
419
- const configApi = runtime.config as { writeConfigFile: (cfg: OpenClawConfig) => Promise<void> };
420
- await configApi.writeConfigFile(nextCfg);
421
- }
422
-
423
- const resolved = resolveQQBotAccount(changed ? nextCfg : cfg, accountId);
424
- const loggedOut = resolved.secretSource === "none";
425
- const envToken = Boolean(process.env.QQBOT_CLIENT_SECRET);
426
-
427
- return { ok: true, cleared, envToken, loggedOut };
270
+ logoutAccount: (params) => logoutAndClearCredentials(params),
271
+ loginWithQrStart: async ({ accountId }: { accountId?: string }) => startQrLogin(accountId),
272
+ loginWithQrWait: async ({ accountId }: { accountId?: string }) => {
273
+ const result = await waitQrLogin(accountId);
274
+ return { connected: result.connected, message: result.message };
428
275
  },
429
276
  },
277
+
278
+ // ── 登录认证 ──
279
+ auth: {
280
+ login: qqbotLogin as any,
281
+ // 审批权限(从 approvalStubs 迁移)
282
+ authorizeActorAction: () => ({ authorized: true } as const),
283
+ getActionAvailabilityState: () => ({ kind: 'enabled' as const } as const),
284
+ },
285
+
286
+ // ── 状态 ──
430
287
  status: {
431
288
  defaultRuntime: {
432
289
  accountId: DEFAULT_ACCOUNT_ID,
@@ -437,10 +294,9 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
437
294
  lastInboundAt: null,
438
295
  lastOutboundAt: null,
439
296
  },
440
- // 新增:构建通道摘要
441
297
  buildChannelSummary: ({ snapshot }) => ({
442
298
  configured: snapshot.configured ?? false,
443
- tokenSource: snapshot.tokenSource ?? "none",
299
+ tokenSource: snapshot.tokenSource ?? 'none',
444
300
  running: snapshot.running ?? false,
445
301
  connected: snapshot.connected ?? false,
446
302
  lastConnectedAt: snapshot.lastConnectedAt ?? null,
@@ -460,100 +316,33 @@ export const qqbotPlugin: ChannelPlugin<ResolvedQQBotAccount> = {
460
316
  lastOutboundAt: runtime?.lastOutboundAt ?? null,
461
317
  }),
462
318
  },
463
- // QQBot approval-handler 通过独立 WS 连接自行处理 exec + plugin 审批消息投递(带 Inline Keyboard),
464
- // 完全屏蔽框架 Forwarder 的纯文本通知。
465
- //
466
- // ── 3.28 扁平结构 ──
467
- execApprovals: {
468
- // 3.28 框架通过此方法判断 channel 是否支持审批
469
- getInitiatingSurfaceState: ({ accountId }: { cfg: any; accountId?: string | null }) => {
470
- return getApprovalHandler(accountId ?? "") != null
471
- ? { kind: "enabled" as const }
472
- : { kind: "disabled" as const };
473
- },
474
- shouldSuppressForwardingFallback: (...args: any[]) => {
475
- console.log("[QQBot] shouldSuppressForwardingFallback called", JSON.stringify(args?.[0]?.target ?? null));
476
- return true;
477
- },
478
- shouldSuppressLocalPrompt: ({ accountId, payload }: any) =>
479
- getApprovalHandler(accountId ?? "") != null &&
480
- isApprovalPayload(payload),
481
- buildPendingPayload: () => null,
482
- buildResolvedPayload: () => null,
483
- },
484
- // ── 3.31+ 嵌套结构 ──
485
- // auth 和 approvals 是 ChannelPlugin 顶层平级字段
486
- //
487
- // QQBot 审批模型:
488
- // - QQBotApprovalHandler 通过独立 WS 自行投递带 Inline Keyboard 的审批消息
489
- // - 用户点击按钮 → INTERACTION_CREATE → resolveApproval → gateway RPC
490
- // - /approve 文本命令作为 URGENT_COMMAND 直接入队交给框架处理
491
- auth: {
492
- authorizeActorAction: () => ({ authorized: true }),
493
- getActionAvailabilityState: ({ accountId }: {
494
- cfg: any; accountId?: string | null; action: "approve";
495
- }) => {
496
- return getApprovalHandler(accountId ?? "") != null
497
- ? { kind: "enabled" as const }
498
- : { kind: "disabled" as const };
499
- },
500
- },
501
- approvals: {
502
- delivery: {
503
- hasConfiguredDmRoute: () => true,
504
- shouldSuppressForwardingFallback: () => true,
505
- },
506
- render: {
507
- exec: {
508
- buildPendingPayload: () => null,
509
- buildResolvedPayload: () => null,
510
- },
511
- plugin: {
512
- buildPendingPayload: () => null,
513
- buildResolvedPayload: () => null,
514
- },
515
- },
516
- },
517
- };
518
319
 
519
- // ============ 独立的 mention 工具函数(供 gateway.ts 等直接调用) ============
320
+ // ── 审批(stub 实际由 features/approval-handler 处理)──
321
+ ...approvalStubs,
322
+ };
520
323
 
521
- /** 清理 @mention:替换 <@openid> @用户名,去除 @机器人自身 */
522
- export function stripMentionText(text: string, mentions?: Array<{ member_openid?: string; id?: string; user_openid?: string; is_you?: boolean; nickname?: string; username?: string }>): string {
523
- if (!text || !mentions?.length) return text;
524
- let cleaned = text;
525
- for (const m of mentions) {
526
- const openid = m.member_openid ?? m.id ?? m.user_openid;
527
- if (!openid) continue;
528
- if (m.is_you) {
529
- cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, "g"), "").trim();
530
- } else {
531
- const displayName = m.nickname ?? m.username;
532
- if (displayName) {
533
- cleaned = cleaned.replace(new RegExp(`<@!?${openid}>`, "g"), `@${displayName}`);
534
- }
535
- }
536
- }
537
- return cleaned;
324
+ function resolveMCPAgentId(to: string, accountId: string, cfg: unknown, log?: PluginLogger): string | undefined {
325
+ try {
326
+ const parts = to.split(':');
327
+ const scope = parts[1];
328
+ const peerId = parts[2];
329
+ if (!scope || !peerId) return undefined;
330
+ const rt = tryGetQQBotRuntime();
331
+ if (!rt) return undefined;
332
+ const route = getAdapters(rt).resolveAgentRoute?.({
333
+ cfg, channel: 'qqbot', accountId,
334
+ peer: { kind: scope === 'group' ? 'group' : 'direct', id: peerId },
335
+ });
336
+ log?.debug(`resolveMCPAgentId to=${to} => agentId=${route?.agentId ?? 'none'}`);
337
+ return route?.agentId;
338
+ } catch { return undefined; }
538
339
  }
539
340
 
540
- /** 检测消息是否 @了机器人(mentions > eventType > mentionPatterns) */
541
- export function detectWasMentioned({ eventType, mentions, content, mentionPatterns }: {
542
- eventType?: string;
543
- mentions?: Array<{ is_you?: boolean }>;
544
- content?: string;
545
- mentionPatterns?: string[];
546
- }): boolean {
547
- if (mentions?.some((m) => m.is_you)) return true;
548
- if (eventType === "GROUP_AT_MESSAGE_CREATE") return true;
549
- if (mentionPatterns?.length && content) {
550
- for (const pattern of mentionPatterns) {
551
- try {
552
- if (new RegExp(pattern, "i").test(content)) return true;
553
- } catch {
554
- // 无效正则,跳过
555
- }
556
- }
557
- }
558
- return false;
341
+ function createOutLog(accountId: string): PluginLogger {
342
+ const gwLog = getGateway(accountId)?.log;
343
+ return gwLog?.child('outbound') ?? ({} as PluginLogger);
559
344
  }
345
+
346
+ // Re-export for backward compatibility
347
+ export { stripMentionText } from './utils/mention.js';
348
+ export { detectWasMentioned } from './utils/mention.js';