@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
@@ -0,0 +1,143 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
3
+ import { getAdapters } from '../adapter/resolve.js';
4
+ import { updateGlobalConfig, checkCommandAuth } from './config-util.js';
5
+
6
+ /** 审批预设配置 */
7
+ const PRESETS: Record<string, { security: string; ask: string; desc: string }> = {
8
+ on: { security: 'allowlist', ask: 'on-miss', desc: '开启审批(白名单模式)' },
9
+ off: { security: 'full', ask: 'off', desc: '关闭审批' },
10
+ always: { security: 'allowlist', ask: 'always', desc: '严格模式(每次都审批)' },
11
+ };
12
+
13
+ /** 格式化当前审批状态 */
14
+ function formatStatus(security: string, ask: string): string {
15
+ const secIcon = security === 'full' ? '🟢' : security === 'allowlist' ? '🟡' : '🔴';
16
+ const askIcon = ask === 'off' ? '🟢' : ask === 'always' ? '🔴' : '🟡';
17
+ const desc =
18
+ security === 'deny' ? '⚠️ 当前为 deny 模式,所有命令执行被拒绝' :
19
+ security === 'full' && ask === 'off' ? '✅ 所有命令无需审批直接执行' :
20
+ security === 'allowlist' && ask === 'on-miss' ? '🛡️ 白名单命令直接执行,其余需审批' :
21
+ ask === 'always' ? '🔒 每次命令执行都需要人工审批' :
22
+ `ℹ️ security=${security}, ask=${ask}`;
23
+
24
+ return [
25
+ '🔐 当前审批配置',
26
+ '',
27
+ `${secIcon} 安全模式 (security): **${security}**`,
28
+ `${askIcon} 审批模式 (ask): **${ask}**`,
29
+ '',
30
+ desc,
31
+ ].join('\n');
32
+ }
33
+
34
+ /** 操作指引菜单 */
35
+ function menuText(): string {
36
+ return [
37
+ '🔐 命令执行审批配置',
38
+ '',
39
+ '<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批(白名单模式)',
40
+ '<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批',
41
+ '<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式',
42
+ '<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认',
43
+ '<qqbot-cmd-input text="/bot-approve status" show="/bot-approve status"/> 查看当前配置',
44
+ ].join('\n');
45
+ }
46
+
47
+ /** /bot-approve — 管理命令执行审批配置 */
48
+ export function botApprove(getRuntime: () => PluginRuntime): SlashCommand {
49
+ return {
50
+ name: 'bot-approve',
51
+ description: '管理命令执行审批配置',
52
+ scope: 'c2c',
53
+ authorized: checkCommandAuth,
54
+ usage: [
55
+ '/bot-approve 查看操作指引',
56
+ '/bot-approve on 开启审批(白名单模式,推荐)',
57
+ '/bot-approve off 关闭审批,命令直接执行',
58
+ '/bot-approve always 始终审批,每次执行都需审批',
59
+ '/bot-approve reset 恢复框架默认值',
60
+ '/bot-approve status 查看当前审批配置',
61
+ ].join('\n'),
62
+ handler: async (ctx) => {
63
+ const arg = (Array.isArray(ctx.command.args) ? ctx.command.args.join(' ') : String(ctx.command.args ?? '')).trim().toLowerCase();
64
+ const runtime = getRuntime();
65
+
66
+ if (!runtime) {
67
+ return '⚠️ runtime 不可用,无法管理审批配置。';
68
+ }
69
+
70
+ const adapters = getAdapters(runtime);
71
+
72
+ const loadExecConfig = () => {
73
+ const cfg = adapters.getConfig?.() ?? {};
74
+ const tools = (cfg as any).tools ?? {};
75
+ const exec = tools.exec ?? {};
76
+ return {
77
+ security: String(exec.security ?? 'deny'),
78
+ ask: String(exec.ask ?? 'on-miss'),
79
+ };
80
+ };
81
+
82
+ // 无参数 → 操作指引
83
+ if (!arg) {
84
+ return menuText();
85
+ }
86
+
87
+ // status → 查看当前配置
88
+ if (arg === 'status') {
89
+ const { security, ask } = loadExecConfig();
90
+ return [
91
+ formatStatus(security, ask),
92
+ '',
93
+ '<qqbot-cmd-input text="/bot-approve on" show="/bot-approve on"/> 开启审批',
94
+ '<qqbot-cmd-input text="/bot-approve off" show="/bot-approve off"/> 关闭审批',
95
+ '<qqbot-cmd-input text="/bot-approve always" show="/bot-approve always"/> 严格模式',
96
+ '<qqbot-cmd-input text="/bot-approve reset" show="/bot-approve reset"/> 恢复默认',
97
+ ].join('\n');
98
+ }
99
+
100
+ // on / off / always → 写入预设
101
+ const preset = PRESETS[arg];
102
+ if (preset) {
103
+ const error = await updateGlobalConfig(getRuntime, (cfg: any) => {
104
+ cfg.tools ??= {};
105
+ cfg.tools.exec ??= {};
106
+ cfg.tools.exec.security = preset.security;
107
+ cfg.tools.exec.ask = preset.ask;
108
+ });
109
+ if (error) return error;
110
+
111
+ if (arg === 'on') {
112
+ return ['✅ 审批已开启', '', '• security = allowlist', '• ask = on-miss', '', '已批准的命令自动加入白名单,下次直接执行。'].join('\n');
113
+ }
114
+ if (arg === 'off') {
115
+ return ['✅ 审批已关闭', '', '• security = full', '• ask = off', '', '⚠️ 所有命令将直接执行,不会弹出审批确认。'].join('\n');
116
+ }
117
+ return ['✅ 已切换为严格审批模式', '', '• security = allowlist', '• ask = always', '', '每个命令都会弹出审批按钮,需手动确认。'].join('\n');
118
+ }
119
+
120
+ // reset → 删除 tools.exec.security 和 tools.exec.ask
121
+ if (arg === 'reset') {
122
+ const error = await updateGlobalConfig(getRuntime, (cfg: any) => {
123
+ const exec = cfg.tools?.exec;
124
+ if (exec) {
125
+ delete exec.security;
126
+ delete exec.ask;
127
+ if (Object.keys(exec).length === 0) delete cfg.tools.exec;
128
+ if (cfg.tools && Object.keys(cfg.tools).length === 0) delete cfg.tools;
129
+ }
130
+ });
131
+ if (error) return error;
132
+
133
+ return ['✅ 审批配置已重置', '', '已移除 tools.exec.security 和 tools.exec.ask', '框架将使用默认值(security=deny, ask=on-miss)', '', '如需开启命令执行,请使用 /bot-approve on'].join('\n');
134
+ }
135
+
136
+ return [
137
+ `❌ 未知参数: ${arg}`,
138
+ '',
139
+ '可用选项: on | off | always | reset | status',
140
+ ].join('\n');
141
+ },
142
+ };
143
+ }
@@ -0,0 +1,114 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+ import type { ResolvedQQBotAccount } from '../types.js';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
5
+ import os from 'node:os';
6
+ import { checkCommandAuth } from './config-util.js';
7
+ import { getQQBotMediaDir } from '../utils/platform.js';
8
+
9
+ const MAX_DISPLAY = 10;
10
+
11
+ interface FileEntry {
12
+ filePath: string;
13
+ size: number;
14
+ }
15
+
16
+ /** 递归扫描目录下所有文件,按大小降序 */
17
+ function scanFiles(dirPath: string): FileEntry[] {
18
+ const files: FileEntry[] = [];
19
+ if (!fs.existsSync(dirPath)) return files;
20
+
21
+ const walk = (dir: string) => {
22
+ let entries: fs.Dirent[];
23
+ try { entries = fs.readdirSync(dir, { withFileTypes: true }); } catch { return; }
24
+ for (const entry of entries) {
25
+ const full = path.join(dir, entry.name);
26
+ if (entry.isDirectory()) { walk(full); }
27
+ else if (entry.isFile()) {
28
+ try { files.push({ filePath: full, size: fs.statSync(full).size }); } catch { /* skip */ }
29
+ }
30
+ }
31
+ };
32
+ walk(dirPath);
33
+ return files.sort((a, b) => b.size - a.size);
34
+ }
35
+
36
+ function formatSize(bytes: number): string {
37
+ if (bytes < 1024) return `${bytes} B`;
38
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
39
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
40
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
41
+ }
42
+
43
+ /** /bot-clear-storage — 清理下载文件(两步:扫描 → --force 删除) */
44
+ export function botClearStorage(_account: ResolvedQQBotAccount): SlashCommand {
45
+ const targetDir = getQQBotMediaDir('downloads');
46
+ const displayDir = targetDir.replace(os.homedir(), '~');
47
+
48
+ return {
49
+ name: 'bot-clear-storage',
50
+ description: '清理通过QQBot对话产生的文件以及下载的资源',
51
+ scope: 'c2c',
52
+ authorized: checkCommandAuth,
53
+ usage: [
54
+ '/bot-clear-storage',
55
+ '',
56
+ '扫描当前机器人产生的下载文件并列出明细。',
57
+ '确认后执行删除,释放主机磁盘空间。',
58
+ '',
59
+ '/bot-clear-storage --force 确认执行清理',
60
+ '',
61
+ '⚠️ 仅在私聊中可用。',
62
+ ].join('\n'),
63
+ handler: (ctx) => {
64
+ const isForce = ctx.command.raw?.trim() === '--force';
65
+
66
+ if (!isForce) {
67
+ // ── 第一步:扫描并展示 ──
68
+ const files = scanFiles(targetDir);
69
+ if (files.length === 0) {
70
+ return `✅ 当前没有需要清理的文件\n\n目录 \`${displayDir}\` 为空或不存在。`;
71
+ }
72
+
73
+ const totalSize = files.reduce((s, f) => s + f.size, 0);
74
+ const lines = [
75
+ `即将清理 \`${displayDir}\` 下所有文件,共 ${files.length} 个,占用 ${formatSize(totalSize)}。`,
76
+ '',
77
+ `文件概况:`,
78
+ ];
79
+
80
+ for (const f of files.slice(0, MAX_DISPLAY)) {
81
+ const rel = path.relative(targetDir, f.filePath).replace(/\\/g, '/');
82
+ lines.push(` ${rel} (${formatSize(f.size)})`);
83
+ }
84
+ if (files.length > MAX_DISPLAY) {
85
+ lines.push(` ...共 ${files.length} 个文件 (${formatSize(totalSize)})`);
86
+ }
87
+
88
+ lines.push('', '---', '', '确认清理后文件将永久删除,后续 AI 无法找回。', '‼️ <qqbot-cmd-enter text="/bot-clear-storage --force" />');
89
+ return lines.join('\n');
90
+ }
91
+
92
+ // ── 第二步:--force 执行删除 ──
93
+ const files = scanFiles(targetDir);
94
+ if (files.length === 0) {
95
+ return '✅ 没有需要清理的文件。';
96
+ }
97
+
98
+ let deleted = 0;
99
+ let failed = 0;
100
+ for (const f of files) {
101
+ try { fs.unlinkSync(f.filePath); deleted++; } catch { failed++; }
102
+ }
103
+
104
+ const parts = [`🗑️ 已删除 ${deleted} 个文件`];
105
+ if (failed > 0) parts.push(`,${failed} 个失败`);
106
+ parts.push(`\n📁 \`${displayDir}\``);
107
+
108
+ // 清理空目录
109
+ try { fs.rmdirSync(targetDir); } catch { /* not empty */ }
110
+
111
+ return parts.join('');
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,62 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
3
+ import type { ResolvedQQBotAccount } from '../types.js';
4
+ import { updateAccountConfig, checkCommandAuth } from './config-util.js';
5
+
6
+ /** /bot-group-always — 修改群消息默认响应模式 */
7
+ export function botGroupAlways(account: ResolvedQQBotAccount, getRuntime: () => PluginRuntime): SlashCommand {
8
+ return {
9
+ name: ['bot-group-always', 'bot-group-allways'],
10
+ description: '修改群消息默认响应模式',
11
+ scope: 'c2c',
12
+ authorized: checkCommandAuth,
13
+ usage: [
14
+ '/bot-group-always on AI 自主判断何时发言(无需 @)',
15
+ '/bot-group-always off 仅在被 @ 时回复',
16
+ '/bot-group-always 查看当前设置',
17
+ '',
18
+ '设为 on 后,AI 会自主判断每条消息是否需要回复(无需 @)。',
19
+ '仍可通过 groups.{groupId}.requireMention 对单个群覆盖。',
20
+ ].join('\n'),
21
+ handler: async (ctx) => {
22
+ const arg = (Array.isArray(ctx.command.args) ? ctx.command.args.join(' ') : String(ctx.command.args ?? '')).trim().toLowerCase();
23
+ const currentRequireMention = account.config.defaultRequireMention ?? true;
24
+
25
+ // 无参数 → 查看状态
26
+ if (!arg) {
27
+ return [
28
+ `🤖 群自主发言状态:${currentRequireMention ? '❌ 仅被 @ 时回复' : '✅ 自主判断何时发言'}`,
29
+ `使用 <qqbot-cmd-input text="/bot-group-always on" show="/bot-group-always on"/> 设为自主发言`,
30
+ `使用 <qqbot-cmd-input text="/bot-group-always off" show="/bot-group-always off"/> 设为仅被 @ 时回复`,
31
+ ].join('\n');
32
+ }
33
+
34
+ if (arg !== 'on' && arg !== 'off') {
35
+ return '❌ 参数错误,请使用 on 或 off\n\n示例:/bot-group-always on';
36
+ }
37
+
38
+ // on = 自主发言 (requireMention=false), off = 仅被 @ (requireMention=true)
39
+ const newRequireMention = arg === 'off';
40
+
41
+ if (newRequireMention === currentRequireMention) {
42
+ return `🤖 群自主发言已经是"${arg}"状态,无需操作`;
43
+ }
44
+
45
+ const error = await updateAccountConfig(account, getRuntime, (acfg) => {
46
+ (acfg as any).defaultRequireMention = newRequireMention;
47
+ });
48
+ if (error) return error;
49
+
50
+ // 更新内存中的配置
51
+ account.config.defaultRequireMention = newRequireMention;
52
+
53
+ return [
54
+ `✅ 群自主发言已设置为 ${newRequireMention ? '**off**(仅被 @ 时回复)' : '**on**(AI 自主判断何时发言)'}`,
55
+ '',
56
+ newRequireMention
57
+ ? '仅在被 @ 机器人才会回复。'
58
+ : 'AI 将自主判断群消息是否需要回复,无需被 @ 即可发言。',
59
+ ].join('\n');
60
+ },
61
+ };
62
+ }
@@ -0,0 +1,40 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+ import type { ResolvedQQBotAccount } from '../types.js';
3
+ import { getPackageVersion } from '../utils/pkg-version.js';
4
+
5
+ const PLUGIN_VERSION = getPackageVersion();
6
+
7
+ /** 群聊时隐藏的仅限私聊指令 */
8
+ const GROUP_EXCLUDED = new Set([
9
+ 'bot-upgrade', 'bot-clear-storage', 'bot-logs',
10
+ 'bot-approve', 'bot-group-always', 'bot-group-allways', 'bot-streaming', 'bot-me',
11
+ ]);
12
+
13
+ /** /bot-help — 查看所有指令以及用途 */
14
+ export function botHelp(_account: ResolvedQQBotAccount, allCommands: () => SlashCommand[]): SlashCommand {
15
+ return {
16
+ name: 'bot-help',
17
+ description: '查看所有指令以及用途',
18
+ usage: [
19
+ '/bot-help',
20
+ '',
21
+ '列出所有可用的 QQBot 插件内置指令及其简要说明。',
22
+ '使用 /指令名 ? 可查看某条指令的详细用法。',
23
+ ].join('\n'),
24
+ handler: (ctx) => {
25
+ const isGroup = ctx.message.kind === 'group';
26
+ const lines = ['### QQBot插件内置调试指令', ''];
27
+
28
+ for (const cmd of allCommands()) {
29
+ const name = Array.isArray(cmd.name) ? cmd.name[0] : cmd.name;
30
+ if (cmd.hidden) continue;
31
+ if (isGroup && GROUP_EXCLUDED.has(name)) continue;
32
+ if (cmd.authorized && cmd.authorized(ctx as any) !== true) continue;
33
+ lines.push(`<qqbot-cmd-input text="/${name}" show="/${name}"/> ${cmd.description ?? ''}`);
34
+ }
35
+
36
+ lines.push('', `> 插件版本 v${PLUGIN_VERSION}`);
37
+ return lines.join('\n');
38
+ },
39
+ };
40
+ }
@@ -0,0 +1,248 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import os from 'node:os';
5
+ import crypto from 'node:crypto';
6
+ import { getQQBotMediaDir } from '../utils/platform.js';
7
+ import { checkCommandAuth } from './config-util.js';
8
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
9
+ import { getAdapters } from '../adapter/resolve.js';
10
+
11
+
12
+ const MAX_LINES_PER_FILE = 1000;
13
+ const MAX_FILES = 4;
14
+ const LOG_KEYWORDS = ['gateway', 'openclaw', 'clawdbot', 'moltbot'];
15
+ const LOG_PATTERN = new RegExp(LOG_KEYWORDS.join('|'), 'i');
16
+
17
+ interface LogFileEntry {
18
+ filePath: string;
19
+ sourceDir: string;
20
+ mtime: number;
21
+ }
22
+
23
+ // ── 配置日志路径 ──
24
+
25
+ /** 从 openclaw.json logging.file 提取配置的日志文件路径 */
26
+ function getConfiguredLogFiles(runtime: PluginRuntime): string[] {
27
+ const files: string[] = [];
28
+ try {
29
+ const cfg = getAdapters(runtime).getConfig?.() ?? {};
30
+ const logFile = (cfg as any)?.logging?.file;
31
+ if (typeof logFile === 'string') {
32
+ files.push(path.resolve(logFile));
33
+ }
34
+ } catch { /* config unavailable */ }
35
+ return files;
36
+ }
37
+
38
+ // ── 候选目录收集 ──
39
+
40
+ function collectCandidateLogDirs(runtime: PluginRuntime): string[] {
41
+ const homeDir = os.homedir();
42
+ const dirs = new Set<string>();
43
+ const pushDir = (p: string | undefined | null) => {
44
+ if (!p) return;
45
+ try { dirs.add(path.resolve(p)); } catch { /* skip */ }
46
+ };
47
+ const pushStateDir = (stateDir: string) => {
48
+ if (!stateDir) return;
49
+ pushDir(stateDir);
50
+ pushDir(path.join(stateDir, 'logs'));
51
+ };
52
+
53
+ // 0. 从配置 logging.file 提取目录(最精确)
54
+ for (const logFile of getConfiguredLogFiles(runtime)) {
55
+ pushDir(path.dirname(logFile));
56
+ }
57
+
58
+ // 1. *_STATE_DIR 环境变量
59
+ for (const [key, value] of Object.entries(process.env)) {
60
+ if (!value) continue;
61
+ if (/STATE_DIR$/i.test(key) && /(OPENCLAW|CLAWDBOT|MOLTBOT)/i.test(key)) {
62
+ pushStateDir(value);
63
+ }
64
+ }
65
+
66
+ // 2. 常见状态目录
67
+ for (const name of LOG_KEYWORDS) {
68
+ pushDir(path.join(homeDir, `.${name}`));
69
+ pushDir(path.join(homeDir, `.${name}`, 'logs'));
70
+ pushDir(path.join(homeDir, name));
71
+ pushDir(path.join(homeDir, name, 'logs'));
72
+ }
73
+
74
+ // 3. home/cwd/AppData 下包含产品名的子目录
75
+ const searchRoots = new Set([homeDir, process.cwd(), path.dirname(process.cwd())]);
76
+ if (process.env.APPDATA) searchRoots.add(process.env.APPDATA);
77
+ if (process.env.LOCALAPPDATA) searchRoots.add(process.env.LOCALAPPDATA);
78
+ for (const root of searchRoots) {
79
+ try {
80
+ const entries = fs.readdirSync(root, { withFileTypes: true });
81
+ for (const entry of entries) {
82
+ if (!entry.isDirectory()) continue;
83
+ const re = LOG_PATTERN;
84
+ if (!re.test(entry.name)) continue;
85
+ const base = path.join(root, entry.name);
86
+ pushDir(base);
87
+ pushDir(path.join(base, 'logs'));
88
+ }
89
+ } catch { /* skip */ }
90
+ }
91
+
92
+ // 4. /var/log (Linux)
93
+ if (process.platform !== 'win32') {
94
+ for (const name of LOG_KEYWORDS) {
95
+ pushDir(path.join('/var/log', name));
96
+ }
97
+ }
98
+
99
+ // 5. /tmp
100
+ if (process.platform === 'win32') {
101
+ pushDir('C:\\tmp');
102
+ if (process.env.TEMP) pushDir(process.env.TEMP);
103
+ if (process.env.TMP) pushDir(process.env.TMP);
104
+ if (process.env.LOCALAPPDATA) pushDir(path.join(process.env.LOCALAPPDATA, 'Temp'));
105
+ } else {
106
+ pushDir('/tmp');
107
+ }
108
+ for (const name of LOG_KEYWORDS) {
109
+ pushDir(path.join('/tmp', name));
110
+ if (process.platform === 'win32' && process.env.TEMP) {
111
+ pushDir(path.join(process.env.TEMP, name));
112
+ }
113
+ }
114
+
115
+ // PM2
116
+ const pm2Home = process.env.PM2_HOME ?? path.join(homeDir, '.pm2');
117
+ pushDir(path.join(pm2Home, 'logs'));
118
+
119
+ return Array.from(dirs);
120
+ }
121
+
122
+ // ── 日志文件收集 ──
123
+
124
+ function collectRecentLogFiles(logDirs: string[], runtime: PluginRuntime): LogFileEntry[] {
125
+ const candidates: LogFileEntry[] = [];
126
+ const dedupe = new Set<string>();
127
+
128
+ const pushFile = (filePath: string, sourceDir: string) => {
129
+ const normalized = path.resolve(filePath);
130
+ if (dedupe.has(normalized)) return;
131
+ try {
132
+ const stat = fs.statSync(normalized);
133
+ if (!stat.isFile() || stat.size === 0) return;
134
+ dedupe.add(normalized);
135
+ candidates.push({ filePath: normalized, sourceDir, mtime: stat.mtimeMs });
136
+ } catch { /* skip */ }
137
+ };
138
+
139
+ // 配置指定的日志文件(最高优先级)
140
+ for (const logFile of getConfiguredLogFiles(runtime)) {
141
+ pushFile(logFile, path.dirname(logFile));
142
+ }
143
+
144
+ for (const dir of logDirs) {
145
+ // 知名文件名
146
+ for (const name of ['gateway.log', 'gateway.err.log', 'openclaw.log', 'clawdbot.log', 'moltbot.log']) {
147
+ pushFile(path.join(dir, name), dir);
148
+ }
149
+ // 扫描所有 .log/.txt,按关键词过滤
150
+ try {
151
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
152
+ for (const entry of entries) {
153
+ if (!entry.isFile()) continue;
154
+ if (!/\.(log|txt)$/i.test(entry.name)) continue;
155
+ const re = LOG_PATTERN;
156
+ if (!re.test(entry.name)) continue;
157
+ pushFile(path.join(dir, entry.name), dir);
158
+ }
159
+ } catch { /* skip */ }
160
+ }
161
+
162
+ return candidates.sort((a, b) => b.mtime - a.mtime);
163
+ }
164
+
165
+ // ── 命令 ──
166
+
167
+ /** /bot-logs — 导出本地日志文件 */
168
+ export function botLogs(runtime: PluginRuntime): SlashCommand {
169
+ return {
170
+ name: 'bot-logs',
171
+ description: '导出本地日志文件',
172
+ scope: 'c2c',
173
+ authorized: checkCommandAuth,
174
+ usage: [
175
+ '/bot-logs',
176
+ '',
177
+ `导出最近的 OpenClaw 日志文件(最多 ${MAX_FILES} 个)。`,
178
+ `每个文件最多保留最后 ${MAX_LINES_PER_FILE} 行,以文件形式返回。`,
179
+ ].join('\n'),
180
+ handler: async (ctx) => {
181
+ const logDirs = collectCandidateLogDirs(runtime);
182
+ const recentFiles = collectRecentLogFiles(logDirs, runtime).slice(0, MAX_FILES);
183
+
184
+ if (recentFiles.length === 0) {
185
+ const existingDirs = logDirs.filter((d) => { try { return fs.existsSync(d); } catch { return false; } });
186
+ const searched = existingDirs.length > 0
187
+ ? existingDirs.map((d) => ` • ${d}`).join('\n')
188
+ : logDirs.map((d) => ` • ${d}`).join('\n');
189
+ return [
190
+ '⚠️ 未找到日志文件',
191
+ '',
192
+ '已搜索以下路径:',
193
+ searched,
194
+ ].join('\n');
195
+ }
196
+
197
+ const lines: string[] = [];
198
+ let totalIncluded = 0;
199
+ let totalOriginal = 0;
200
+ let truncatedCount = 0;
201
+
202
+ for (const logFile of recentFiles) {
203
+ try {
204
+ const content = fs.readFileSync(logFile.filePath, 'utf8');
205
+ const allLines = content.split('\n');
206
+ const tail = allLines.slice(-MAX_LINES_PER_FILE);
207
+ if (tail.length > 0) {
208
+ const fileName = path.basename(logFile.filePath);
209
+ lines.push(`\n== ${fileName} (last ${tail.length}/${allLines.length}) ==`);
210
+ lines.push(...tail);
211
+ totalIncluded += tail.length;
212
+ totalOriginal += allLines.length;
213
+ if (allLines.length > MAX_LINES_PER_FILE) truncatedCount++;
214
+ }
215
+ } catch { /* skip */ }
216
+ }
217
+
218
+ if (lines.length === 0) {
219
+ return '⚠️ 找到日志文件但读取失败';
220
+ }
221
+
222
+ const tmpDir = getQQBotMediaDir('exports');
223
+ if (!fs.existsSync(tmpDir)) fs.mkdirSync(tmpDir, { recursive: true });
224
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19);
225
+ const suffix = crypto.randomBytes(4).toString('hex');
226
+ const tmpFile = path.join(tmpDir, `bot-logs-${timestamp}-${suffix}.txt`);
227
+ fs.writeFileSync(tmpFile, lines.join('\n'), 'utf8');
228
+
229
+ let summary = `${recentFiles.length} 个日志文件,共 ${totalIncluded} 行`;
230
+ if (truncatedCount > 0) summary += `(${truncatedCount} 个截断,原始 ${totalOriginal} 行)`;
231
+
232
+ try {
233
+ const senderId = ctx.message.senderId;
234
+ if (senderId) {
235
+ await ctx.bot.sendFile(
236
+ { scope: 'c2c', targetId: senderId, msgId: ctx.message.messageId },
237
+ { localPath: tmpFile },
238
+ { fileName: `bot-logs-${timestamp}-${suffix}.txt` },
239
+ );
240
+ }
241
+ } catch (err) {
242
+ return `📋 ${summary}\n⚠️ 文件发送失败:${err instanceof Error ? err.message : err}\n📎 ${tmpFile}`;
243
+ }
244
+
245
+ return `📋 ${summary}`;
246
+ },
247
+ };
248
+ }
@@ -0,0 +1,18 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+
3
+ /** /bot-me — 查看发送者 OpenID(仅私聊) */
4
+ export function botMe(): SlashCommand {
5
+ return {
6
+ name: 'bot-me',
7
+ description: '查看你的 OpenID(仅私聊)',
8
+ usage: `/bot-me
9
+
10
+ 查看你在当前 QQBot 应用下的唯一 OpenID。
11
+ 此 ID 用于管理员识别、访问控制等场景。`,
12
+ scope: 'c2c',
13
+ handler: (ctx) => {
14
+ const senderId = ctx.message.senderId ?? 'unknown';
15
+ return `🆔 你的 OpenID: \`${senderId}\``;
16
+ },
17
+ };
18
+ }
@@ -0,0 +1,57 @@
1
+ import type { SlashCommand } from '@tencent-connect/qqbot-nodejs';
2
+ import type { PluginRuntime } from 'openclaw/plugin-sdk';
3
+ import { getPairingApi } from '../adapter/pairing.js';
4
+ import { checkCommandAuth } from './config-util.js';
5
+
6
+ /** /bot-pairing — DM 配对审批管理 */
7
+ export function botPairing(_getRuntime: () => PluginRuntime): SlashCommand {
8
+ return {
9
+ name: 'bot-pairing',
10
+ description: '管理 DM 配对审批',
11
+ scope: 'c2c',
12
+ hidden: true,
13
+ usage: `/bot-pairing approve <code>
14
+
15
+ 批准指定配对码,允许对应用户私聊机器人。
16
+ 配对码由用户首次私聊时自动生成。`,
17
+ authorized: checkCommandAuth,
18
+ handler: async (ctx) => {
19
+ const args = (Array.isArray(ctx.command.args) ? ctx.command.args.join(' ') : String(ctx.command.args ?? '')).trim();
20
+ const parts = args.split(/\s+/);
21
+ const subCmd = parts[0]?.toLowerCase();
22
+ const code = parts[1]?.trim();
23
+
24
+ if (subCmd !== 'approve' || !code) {
25
+ return '⚠️ 用法: /bot-pairing approve <配对码>';
26
+ }
27
+
28
+ const api = getPairingApi();
29
+ if (!api) {
30
+ return '⚠️ 当前 OpenClaw 版本不支持配对审批功能。';
31
+ }
32
+
33
+ try {
34
+ const result = await api.approveCode({
35
+ channel: 'qqbot',
36
+ code,
37
+ });
38
+
39
+ if (!result?.id) {
40
+ return [
41
+ `⚠️ 配对码 \`${code}\` 无效或已过期。`,
42
+ '',
43
+ '每个配对码有效期为 1 小时。',
44
+ ].join('\n');
45
+ }
46
+
47
+ return [
48
+ '✅ 已批准用户访问。',
49
+ '',
50
+ `用户 ID: \`${result.id}\``,
51
+ ].join('\n');
52
+ } catch (err) {
53
+ return `❌ 审批失败: ${(err as Error).message}`;
54
+ }
55
+ },
56
+ };
57
+ }