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