@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
@@ -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
+ }