@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.
- package/README.md +25 -8
- package/README.zh.md +24 -8
- package/bin/qqbot-cli.js +5 -2
- package/dist/index.cjs +15549 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +890 -0
- package/index.ts +56 -19
- package/openclaw.plugin.json +19 -3
- package/package.json +14 -23
- package/preload.cjs +17 -14
- package/scripts/link-sdk-core.cjs +133 -50
- package/scripts/upgrade-via-npm.sh +42 -11
- package/scripts/upgrade-via-source.sh +4 -4
- package/skills/qqbot-channel/SKILL.md +34 -12
- package/src/adapter/contract.ts +69 -0
- package/src/adapter/gateway.ts +61 -0
- package/src/adapter/index.ts +15 -0
- package/src/adapter/lint.ts +144 -0
- package/src/adapter/media.ts +112 -0
- package/src/adapter/pairing.ts +99 -0
- package/src/adapter/resolve.ts +333 -0
- package/src/adapter/setup.ts +55 -0
- package/src/adapter/webhook.ts +248 -0
- package/src/adapter/workspace.ts +29 -0
- package/src/bot-instance.ts +60 -0
- package/src/channel.ts +202 -413
- package/src/commands/bot-approve.ts +143 -0
- package/src/commands/bot-clear-storage.ts +114 -0
- package/src/commands/bot-group-always.ts +62 -0
- package/src/commands/bot-help.ts +40 -0
- package/src/commands/bot-logs.ts +248 -0
- package/src/commands/bot-me.ts +18 -0
- package/src/commands/bot-pairing.ts +57 -0
- package/src/commands/bot-ping.ts +33 -0
- package/src/commands/bot-streaming.ts +55 -0
- package/src/commands/bot-upgrade.ts +56 -0
- package/src/commands/bot-version.ts +41 -0
- package/src/commands/config-util.ts +96 -0
- package/src/commands/index.ts +49 -0
- package/src/config.ts +46 -19
- package/src/dispatch/body-assembler.ts +356 -0
- package/src/dispatch/ctx-builder.ts +110 -0
- package/src/dispatch/dispatch.ts +352 -0
- package/src/dispatch/envelope-builder.ts +112 -0
- package/src/dispatch/index.ts +2 -0
- package/src/{approval-handler.ts → features/approval-handler.ts} +40 -94
- package/src/features/approval-utils.ts +41 -0
- package/src/features/credential-backup.ts +82 -0
- package/src/features/history-store.ts +19 -0
- package/src/features/msgid-cache.ts +55 -0
- package/src/features/onboarding.ts +38 -0
- package/src/{proactive.ts → features/proactive.ts} +68 -49
- package/src/features/ref-index-store.ts +282 -0
- package/src/{update-checker.ts → features/update-checker.ts} +15 -35
- package/src/gateway/event-handlers.ts +241 -0
- package/src/gateway/index.ts +3 -0
- package/src/gateway/lifecycle.ts +221 -0
- package/src/gateway/middleware-setup.ts +139 -0
- package/src/gateway/qqbot-gateway.ts +342 -0
- package/src/middleware/access-control.ts +130 -0
- package/src/middleware/attachment.ts +314 -0
- package/src/middleware/policy-injector.ts +66 -0
- package/src/openclaw-plugin-sdk.d.ts +81 -10
- package/src/outbound/cron-scheduler.ts +130 -0
- package/src/outbound/debounce.ts +102 -0
- package/src/outbound/deliver-pipeline.ts +235 -0
- package/src/outbound/image-size.ts +123 -0
- package/src/outbound/index.ts +3 -0
- package/src/outbound/local-file-router.ts +145 -0
- package/src/outbound/media-send.ts +377 -0
- package/src/outbound/outbound-service.ts +222 -0
- package/src/outbound/reply-limiter.ts +128 -0
- package/src/outbound/sanitize.ts +32 -0
- package/src/outbound/streaming-controller.ts +228 -0
- package/src/outbound/target.ts +63 -0
- package/src/outbound/tts-provider.ts +84 -0
- package/src/request-context.ts +9 -8
- package/src/runtime.ts +31 -6
- package/src/setup/account-key.ts +41 -0
- package/src/setup/finalize.ts +110 -0
- package/src/setup/login.ts +197 -0
- package/src/setup/surface.ts +40 -0
- package/src/tools/platform.ts +149 -0
- package/src/tools/remind.ts +2 -0
- package/src/types-augment.d.ts +54 -0
- package/src/types.ts +51 -9
- package/src/utils/mention.ts +52 -0
- package/src/utils/pkg-version.ts +84 -47
- package/src/utils/platform.ts +31 -7
- package/src/utils/plugin-logger.ts +104 -0
- package/src/utils/ssrf-guard.ts +32 -2
- package/src/utils/stt.ts +137 -0
- package/src/utils/voice-text.ts +61 -0
- package/tsconfig.json +2 -1
- package/tsup.config.ts +57 -0
- package/dist/index.d.ts +0 -18
- package/dist/index.js +0 -27
- package/dist/src/admin-resolver.d.ts +0 -33
- package/dist/src/admin-resolver.js +0 -157
- package/dist/src/api.d.ts +0 -309
- package/dist/src/api.js +0 -923
- package/dist/src/approval-handler.d.ts +0 -47
- package/dist/src/approval-handler.js +0 -372
- package/dist/src/channel.d.ts +0 -29
- package/dist/src/channel.js +0 -528
- package/dist/src/config.d.ts +0 -60
- package/dist/src/config.js +0 -288
- package/dist/src/credential-backup.d.ts +0 -31
- package/dist/src/credential-backup.js +0 -66
- package/dist/src/deliver-debounce.d.ts +0 -74
- package/dist/src/deliver-debounce.js +0 -174
- package/dist/src/gateway.d.ts +0 -18
- package/dist/src/gateway.js +0 -2025
- package/dist/src/group-history.d.ts +0 -136
- package/dist/src/group-history.js +0 -226
- package/dist/src/image-server.d.ts +0 -87
- package/dist/src/image-server.js +0 -570
- package/dist/src/inbound-attachments.d.ts +0 -60
- package/dist/src/inbound-attachments.js +0 -248
- package/dist/src/known-users.d.ts +0 -100
- package/dist/src/known-users.js +0 -263
- package/dist/src/message-gating.d.ts +0 -53
- package/dist/src/message-gating.js +0 -107
- package/dist/src/message-queue.d.ts +0 -91
- package/dist/src/message-queue.js +0 -257
- package/dist/src/onboarding.d.ts +0 -10
- package/dist/src/onboarding.js +0 -203
- package/dist/src/outbound-deliver.d.ts +0 -48
- package/dist/src/outbound-deliver.js +0 -392
- package/dist/src/outbound.d.ts +0 -220
- package/dist/src/outbound.js +0 -975
- package/dist/src/proactive.d.ts +0 -170
- package/dist/src/proactive.js +0 -399
- package/dist/src/ref-index-store.d.ts +0 -101
- package/dist/src/ref-index-store.js +0 -298
- package/dist/src/reply-dispatcher.d.ts +0 -35
- package/dist/src/reply-dispatcher.js +0 -311
- package/dist/src/request-context.d.ts +0 -25
- package/dist/src/request-context.js +0 -37
- package/dist/src/runtime.d.ts +0 -3
- package/dist/src/runtime.js +0 -13
- package/dist/src/session-store.d.ts +0 -52
- package/dist/src/session-store.js +0 -254
- package/dist/src/slash-commands.d.ts +0 -82
- package/dist/src/slash-commands.js +0 -2233
- package/dist/src/startup-greeting.d.ts +0 -30
- package/dist/src/startup-greeting.js +0 -97
- package/dist/src/streaming.d.ts +0 -247
- package/dist/src/streaming.js +0 -899
- package/dist/src/stt.d.ts +0 -21
- package/dist/src/stt.js +0 -70
- package/dist/src/tools/channel.d.ts +0 -16
- package/dist/src/tools/channel.js +0 -231
- package/dist/src/tools/remind.d.ts +0 -2
- package/dist/src/tools/remind.js +0 -255
- package/dist/src/transport/index.d.ts +0 -10
- package/dist/src/transport/index.js +0 -9
- package/dist/src/transport/webhook-transport.d.ts +0 -67
- package/dist/src/transport/webhook-transport.js +0 -245
- package/dist/src/transport/webhook-verify.d.ts +0 -48
- package/dist/src/transport/webhook-verify.js +0 -98
- package/dist/src/types.d.ts +0 -462
- package/dist/src/types.js +0 -22
- package/dist/src/typing-keepalive.d.ts +0 -27
- package/dist/src/typing-keepalive.js +0 -64
- package/dist/src/update-checker.d.ts +0 -36
- package/dist/src/update-checker.js +0 -171
- package/dist/src/utils/audio-convert.d.ts +0 -98
- package/dist/src/utils/audio-convert.js +0 -783
- package/dist/src/utils/chunked-upload.d.ts +0 -68
- package/dist/src/utils/chunked-upload.js +0 -341
- package/dist/src/utils/file-utils.d.ts +0 -61
- package/dist/src/utils/file-utils.js +0 -172
- package/dist/src/utils/image-size.d.ts +0 -51
- package/dist/src/utils/image-size.js +0 -234
- package/dist/src/utils/media-send.d.ts +0 -158
- package/dist/src/utils/media-send.js +0 -502
- package/dist/src/utils/media-tags.d.ts +0 -14
- package/dist/src/utils/media-tags.js +0 -165
- package/dist/src/utils/payload.d.ts +0 -112
- package/dist/src/utils/payload.js +0 -186
- package/dist/src/utils/pkg-version.d.ts +0 -5
- package/dist/src/utils/pkg-version.js +0 -61
- package/dist/src/utils/platform.d.ts +0 -137
- package/dist/src/utils/platform.js +0 -390
- package/dist/src/utils/ssrf-guard.d.ts +0 -25
- package/dist/src/utils/ssrf-guard.js +0 -91
- package/dist/src/utils/text-parsing.d.ts +0 -31
- package/dist/src/utils/text-parsing.js +0 -74
- package/dist/src/utils/upload-cache.d.ts +0 -34
- package/dist/src/utils/upload-cache.js +0 -93
- package/node_modules/@eshaz/web-worker/LICENSE +0 -201
- package/node_modules/@eshaz/web-worker/README.md +0 -134
- package/node_modules/@eshaz/web-worker/browser.js +0 -17
- package/node_modules/@eshaz/web-worker/cjs/browser.js +0 -16
- package/node_modules/@eshaz/web-worker/cjs/node.js +0 -219
- package/node_modules/@eshaz/web-worker/index.d.ts +0 -4
- package/node_modules/@eshaz/web-worker/node.js +0 -223
- package/node_modules/@eshaz/web-worker/package.json +0 -54
- package/node_modules/@wasm-audio-decoders/common/index.js +0 -5
- package/node_modules/@wasm-audio-decoders/common/package.json +0 -36
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderCommon.js +0 -231
- package/node_modules/@wasm-audio-decoders/common/src/WASMAudioDecoderWorker.js +0 -129
- package/node_modules/@wasm-audio-decoders/common/src/puff/README +0 -67
- package/node_modules/@wasm-audio-decoders/common/src/puff/build_puff.js +0 -31
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.c +0 -863
- package/node_modules/@wasm-audio-decoders/common/src/puff/puff.h +0 -35
- package/node_modules/@wasm-audio-decoders/common/src/utilities.js +0 -3
- package/node_modules/@wasm-audio-decoders/common/types.d.ts +0 -7
- package/node_modules/mpg123-decoder/README.md +0 -265
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js +0 -185
- package/node_modules/mpg123-decoder/dist/mpg123-decoder.min.js.map +0 -1
- package/node_modules/mpg123-decoder/index.js +0 -8
- package/node_modules/mpg123-decoder/package.json +0 -58
- package/node_modules/mpg123-decoder/src/EmscriptenWasm.js +0 -464
- package/node_modules/mpg123-decoder/src/MPEGDecoder.js +0 -200
- package/node_modules/mpg123-decoder/src/MPEGDecoderWebWorker.js +0 -21
- package/node_modules/mpg123-decoder/types.d.ts +0 -30
- package/node_modules/silk-wasm/LICENSE +0 -21
- package/node_modules/silk-wasm/README.md +0 -85
- package/node_modules/silk-wasm/lib/index.cjs +0 -16
- package/node_modules/silk-wasm/lib/index.d.ts +0 -70
- package/node_modules/silk-wasm/lib/index.mjs +0 -16
- package/node_modules/silk-wasm/lib/silk.wasm +0 -0
- package/node_modules/silk-wasm/lib/utils.d.ts +0 -4
- package/node_modules/silk-wasm/package.json +0 -39
- package/node_modules/simple-yenc/.github/FUNDING.yml +0 -1
- package/node_modules/simple-yenc/.prettierignore +0 -1
- package/node_modules/simple-yenc/LICENSE +0 -7
- package/node_modules/simple-yenc/README.md +0 -163
- package/node_modules/simple-yenc/dist/esm.js +0 -1
- package/node_modules/simple-yenc/dist/index.js +0 -1
- package/node_modules/simple-yenc/package.json +0 -50
- package/node_modules/simple-yenc/rollup.config.js +0 -27
- package/node_modules/simple-yenc/src/simple-yenc.js +0 -302
- package/node_modules/ws/LICENSE +0 -20
- package/node_modules/ws/README.md +0 -548
- package/node_modules/ws/browser.js +0 -8
- package/node_modules/ws/index.js +0 -13
- package/node_modules/ws/lib/buffer-util.js +0 -131
- package/node_modules/ws/lib/constants.js +0 -19
- package/node_modules/ws/lib/event-target.js +0 -292
- package/node_modules/ws/lib/extension.js +0 -203
- package/node_modules/ws/lib/limiter.js +0 -55
- package/node_modules/ws/lib/permessage-deflate.js +0 -528
- package/node_modules/ws/lib/receiver.js +0 -706
- package/node_modules/ws/lib/sender.js +0 -602
- package/node_modules/ws/lib/stream.js +0 -161
- package/node_modules/ws/lib/subprotocol.js +0 -62
- package/node_modules/ws/lib/validation.js +0 -152
- package/node_modules/ws/lib/websocket-server.js +0 -554
- package/node_modules/ws/lib/websocket.js +0 -1393
- package/node_modules/ws/package.json +0 -69
- package/node_modules/ws/wrapper.mjs +0 -8
- package/scripts/postinstall-link-sdk.js +0 -170
- package/skills/qqbot-media/SKILL.md +0 -60
- package/src/admin-resolver.ts +0 -181
- package/src/api.ts +0 -1333
- package/src/credential-backup.ts +0 -72
- package/src/deliver-debounce.ts +0 -229
- package/src/gateway.ts +0 -2272
- package/src/group-history.ts +0 -328
- package/src/image-server.ts +0 -675
- package/src/inbound-attachments.ts +0 -321
- package/src/known-users.ts +0 -353
- package/src/message-gating.ts +0 -190
- package/src/message-queue.ts +0 -354
- package/src/onboarding.ts +0 -282
- package/src/outbound-deliver.ts +0 -486
- package/src/outbound.ts +0 -1179
- package/src/ref-index-store.ts +0 -412
- package/src/reply-dispatcher.ts +0 -334
- package/src/session-store.ts +0 -303
- package/src/slash-commands.ts +0 -2431
- package/src/startup-greeting.ts +0 -120
- package/src/streaming.ts +0 -1077
- package/src/stt.ts +0 -86
- package/src/tools/channel.ts +0 -275
- package/src/transport/index.ts +0 -11
- package/src/transport/webhook-transport.ts +0 -332
- package/src/transport/webhook-verify.ts +0 -119
- package/src/typing-keepalive.ts +0 -59
- package/src/utils/audio-convert.ts +0 -887
- package/src/utils/chunked-upload.ts +0 -483
- package/src/utils/file-utils.ts +0 -193
- package/src/utils/image-size.ts +0 -266
- package/src/utils/media-send.ts +0 -636
- package/src/utils/media-tags.ts +0 -183
- package/src/utils/payload.ts +0 -265
- package/src/utils/text-parsing.ts +0 -85
- 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
|
+
}
|