@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,282 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 持久化 RefIndex 存储
|
|
3
|
+
*
|
|
4
|
+
* SDK 提供的 MemoryRefIndexStore 在进程重启后即丢失,
|
|
5
|
+
* 而 QQ 引用消息(REFIDX_xxx)入站事件只携带 key,必须本地缓存才能回查。
|
|
6
|
+
*
|
|
7
|
+
* 设计:
|
|
8
|
+
* - 内存 LRU(与 SDK 同款)保证 O(1) 读取
|
|
9
|
+
* - JSONL 追加写持久化,进程重启时按时间顺序回放重建 LRU
|
|
10
|
+
* - 写入触发 compact 阈值时重写文件(去重 + 截断到 maxEntries)
|
|
11
|
+
* - 文件路径: ~/.openclaw/qqbot/data/ref-index.jsonl
|
|
12
|
+
*
|
|
13
|
+
* 实现 SDK 的 RefIndexStore 接口,可直接通过 `quoteRef({ store })` 注入。
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import fs from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import type { RefEntry, RefIndexStore } from '@tencent-connect/qqbot-nodejs';
|
|
19
|
+
import { getQQBotDataDir } from '../utils/platform.js';
|
|
20
|
+
import { createPluginLogger } from '../utils/plugin-logger.js';
|
|
21
|
+
const log = createPluginLogger({ prefix: '[ref-index]' });
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
// ── 常量 ──
|
|
25
|
+
|
|
26
|
+
const DEFAULT_MAX_ENTRIES = 50000;
|
|
27
|
+
const DEFAULT_FILENAME = 'ref-index.jsonl';
|
|
28
|
+
/** 当磁盘 line 数超过 maxEntries * COMPACT_RATIO 时触发 compact */
|
|
29
|
+
const COMPACT_RATIO = 2;
|
|
30
|
+
/** 内存 LRU 的最小容量保护值 */
|
|
31
|
+
const MIN_MAX_ENTRIES = 100;
|
|
32
|
+
|
|
33
|
+
// ── 磁盘行结构 ──
|
|
34
|
+
|
|
35
|
+
interface DiskLine {
|
|
36
|
+
/** RefIndex 键,例如 REFIDX_xxx 或 messageId */
|
|
37
|
+
k: string;
|
|
38
|
+
/** RefEntry 内容 */
|
|
39
|
+
v: RefEntry;
|
|
40
|
+
/** 写入时间戳(毫秒) */
|
|
41
|
+
t: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// ── 持久化 Store ──
|
|
45
|
+
|
|
46
|
+
export interface PersistedRefIndexStoreOptions {
|
|
47
|
+
/** 内存与磁盘的最大条目数。默认 2000。 */
|
|
48
|
+
maxEntries?: number;
|
|
49
|
+
/** 自定义存储文件路径。默认 ~/.openclaw/qqbot/data/ref-index.jsonl */
|
|
50
|
+
filePath?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 持久化版本的 RefIndexStore
|
|
55
|
+
*
|
|
56
|
+
* - get:仅查内存(启动时从磁盘回放重建)
|
|
57
|
+
* - set:内存 + JSONL 追加写;磁盘行数过多时触发 compact
|
|
58
|
+
*/
|
|
59
|
+
export class PersistedRefIndexStore implements RefIndexStore {
|
|
60
|
+
private readonly memory = new Map<string, RefEntry>();
|
|
61
|
+
private readonly maxEntries: number;
|
|
62
|
+
private readonly filePath: string;
|
|
63
|
+
/** 当前磁盘累计写入的行数(用于 compact 阈值判断) */
|
|
64
|
+
private diskLineCount = 0;
|
|
65
|
+
/** 是否已成功初始化(磁盘回放完成) */
|
|
66
|
+
private initialized = false;
|
|
67
|
+
/** 串行化写入,防止并发 append 撕裂行 */
|
|
68
|
+
private writeChain: Promise<void> = Promise.resolve();
|
|
69
|
+
|
|
70
|
+
constructor(options: PersistedRefIndexStoreOptions = {}) {
|
|
71
|
+
this.maxEntries = Math.max(options.maxEntries ?? DEFAULT_MAX_ENTRIES, MIN_MAX_ENTRIES);
|
|
72
|
+
this.filePath = options.filePath ?? path.join(getQQBotDataDir('data'), DEFAULT_FILENAME);
|
|
73
|
+
this.init();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 初始化:按时间顺序回放 JSONL 重建内存 LRU
|
|
78
|
+
*/
|
|
79
|
+
private init(): void {
|
|
80
|
+
try {
|
|
81
|
+
if (!fs.existsSync(this.filePath)) {
|
|
82
|
+
this.initialized = true;
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
const raw = fs.readFileSync(this.filePath, 'utf8');
|
|
86
|
+
const lines = raw.split('\n').filter((l) => l.length > 0);
|
|
87
|
+
this.diskLineCount = lines.length;
|
|
88
|
+
|
|
89
|
+
// 按时间排序回放(防止文件被外部修改后乱序)
|
|
90
|
+
const parsed: DiskLine[] = [];
|
|
91
|
+
for (const line of lines) {
|
|
92
|
+
try {
|
|
93
|
+
const obj = JSON.parse(line) as DiskLine;
|
|
94
|
+
if (obj?.k && obj?.v) {
|
|
95
|
+
parsed.push(obj);
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// 跳过损坏行
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
parsed.sort((a, b) => a.t - b.t);
|
|
102
|
+
|
|
103
|
+
for (const { k, v } of parsed) {
|
|
104
|
+
this.touchMemory(k, v);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 启动时若磁盘行数已超阈值,立刻 compact 一次
|
|
108
|
+
if (this.diskLineCount > this.maxEntries * COMPACT_RATIO) {
|
|
109
|
+
this.compactSync();
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
log.error(
|
|
113
|
+
`init failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
114
|
+
);
|
|
115
|
+
} finally {
|
|
116
|
+
this.initialized = true;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ── RefIndexStore 接口 ──
|
|
121
|
+
|
|
122
|
+
get(key: string): RefEntry | undefined {
|
|
123
|
+
return this.memory.get(key);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
set(key: string, entry: RefEntry): void {
|
|
127
|
+
this.touchMemory(key, entry);
|
|
128
|
+
// 追加写持久化(串行化)
|
|
129
|
+
this.writeChain = this.writeChain.then(() => this.appendToDisk(key, entry));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ── 内部:内存 LRU 维护 ──
|
|
133
|
+
|
|
134
|
+
private touchMemory(key: string, entry: RefEntry): void {
|
|
135
|
+
// 已存在则先删除再插入,保证插入顺序 = LRU 顺序
|
|
136
|
+
if (this.memory.has(key)) {
|
|
137
|
+
this.memory.delete(key);
|
|
138
|
+
} else if (this.memory.size >= this.maxEntries) {
|
|
139
|
+
// 容量已满 → 淘汰最旧的(Map 的迭代顺序即插入顺序)
|
|
140
|
+
const oldest = this.memory.keys().next().value;
|
|
141
|
+
if (oldest !== undefined) {
|
|
142
|
+
this.memory.delete(oldest);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
this.memory.set(key, entry);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ── 内部:磁盘追加 + compact ──
|
|
149
|
+
|
|
150
|
+
private async appendToDisk(key: string, entry: RefEntry): Promise<void> {
|
|
151
|
+
const line: DiskLine = { k: key, v: entry, t: Date.now() };
|
|
152
|
+
const text = JSON.stringify(line) + '\n';
|
|
153
|
+
try {
|
|
154
|
+
await fs.promises.appendFile(this.filePath, text, 'utf8');
|
|
155
|
+
this.diskLineCount += 1;
|
|
156
|
+
if (this.diskLineCount > this.maxEntries * COMPACT_RATIO) {
|
|
157
|
+
await this.compact();
|
|
158
|
+
}
|
|
159
|
+
} catch (err) {
|
|
160
|
+
log.error(
|
|
161
|
+
`append failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* 异步 compact:将内存 LRU 状态完整重写到磁盘,丢弃历史冗余。
|
|
168
|
+
*/
|
|
169
|
+
private async compact(): Promise<void> {
|
|
170
|
+
const tmpPath = `${this.filePath}.tmp`;
|
|
171
|
+
try {
|
|
172
|
+
const now = Date.now();
|
|
173
|
+
// Map 迭代顺序 = 插入顺序(旧 → 新),保持 LRU 顺序
|
|
174
|
+
const lines: string[] = [];
|
|
175
|
+
for (const [k, v] of this.memory.entries()) {
|
|
176
|
+
lines.push(JSON.stringify({ k, v, t: now } satisfies DiskLine));
|
|
177
|
+
}
|
|
178
|
+
const content = lines.length > 0 ? lines.join('\n') + '\n' : '';
|
|
179
|
+
await fs.promises.writeFile(tmpPath, content, 'utf8');
|
|
180
|
+
await fs.promises.rename(tmpPath, this.filePath);
|
|
181
|
+
this.diskLineCount = lines.length;
|
|
182
|
+
log.info(
|
|
183
|
+
`compacted to ${lines.length} entries`,
|
|
184
|
+
);
|
|
185
|
+
} catch (err) {
|
|
186
|
+
log.error(
|
|
187
|
+
`compact failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
188
|
+
);
|
|
189
|
+
// 清理可能残留的 tmp 文件
|
|
190
|
+
try {
|
|
191
|
+
await fs.promises.unlink(tmpPath);
|
|
192
|
+
} catch {
|
|
193
|
+
// ignore
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* 同步 compact(仅 init 阶段使用,避免回放后立刻保留巨大磁盘文件)
|
|
200
|
+
*/
|
|
201
|
+
private compactSync(): void {
|
|
202
|
+
const tmpPath = `${this.filePath}.tmp`;
|
|
203
|
+
try {
|
|
204
|
+
const now = Date.now();
|
|
205
|
+
const lines: string[] = [];
|
|
206
|
+
for (const [k, v] of this.memory.entries()) {
|
|
207
|
+
lines.push(JSON.stringify({ k, v, t: now } satisfies DiskLine));
|
|
208
|
+
}
|
|
209
|
+
const content = lines.length > 0 ? lines.join('\n') + '\n' : '';
|
|
210
|
+
fs.writeFileSync(tmpPath, content, 'utf8');
|
|
211
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
212
|
+
this.diskLineCount = lines.length;
|
|
213
|
+
} catch (err) {
|
|
214
|
+
log.error(
|
|
215
|
+
`compactSync failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ── 诊断 ──
|
|
221
|
+
|
|
222
|
+
/** 当前内存中的条目数 */
|
|
223
|
+
get size(): number {
|
|
224
|
+
return this.memory.size;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** 是否已完成初始化(磁盘回放) */
|
|
228
|
+
get isInitialized(): boolean {
|
|
229
|
+
return this.initialized;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** 诊断快照 */
|
|
233
|
+
stats(): {
|
|
234
|
+
memoryEntries: number;
|
|
235
|
+
diskLines: number;
|
|
236
|
+
maxEntries: number;
|
|
237
|
+
filePath: string;
|
|
238
|
+
} {
|
|
239
|
+
return {
|
|
240
|
+
memoryEntries: this.memory.size,
|
|
241
|
+
diskLines: this.diskLineCount,
|
|
242
|
+
maxEntries: this.maxEntries,
|
|
243
|
+
filePath: this.filePath,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* 强制将当前内存状态持久化到磁盘(进程退出前调用)
|
|
249
|
+
*/
|
|
250
|
+
flush(): void {
|
|
251
|
+
this.compactSync();
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ── 默认单例(按 accountId 隔离) ──
|
|
256
|
+
|
|
257
|
+
const stores = new Map<string, PersistedRefIndexStore>();
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* 获取按 accountId 隔离的持久化 RefIndexStore 单例。
|
|
261
|
+
*
|
|
262
|
+
* 每个账户独立存储文件,避免多账户混用同一个 refIdx 命名空间。
|
|
263
|
+
*/
|
|
264
|
+
|
|
265
|
+
export function getPersistedRefIndexStore(accountId: string): PersistedRefIndexStore {
|
|
266
|
+
let store = stores.get(accountId);
|
|
267
|
+
if (!store) {
|
|
268
|
+
const filePath = path.join(getQQBotDataDir('data', accountId), DEFAULT_FILENAME);
|
|
269
|
+
store = new PersistedRefIndexStore({ filePath });
|
|
270
|
+
stores.set(accountId, store);
|
|
271
|
+
}
|
|
272
|
+
return store;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 进程退出前 flush 所有 store
|
|
277
|
+
*/
|
|
278
|
+
export function flushAllRefIndexStores(): void {
|
|
279
|
+
for (const store of stores.values()) {
|
|
280
|
+
store.flush();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
@@ -8,11 +8,9 @@
|
|
|
8
8
|
* 支持多 registry fallback:npmjs.org → npmmirror.com,解决国内网络问题。
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { createRequire } from "node:module";
|
|
12
11
|
import https from "node:https";
|
|
13
|
-
import { getPackageVersion } from "
|
|
14
|
-
|
|
15
|
-
const require = createRequire(import.meta.url);
|
|
12
|
+
import { getPackageVersion } from "../utils/pkg-version.js";
|
|
13
|
+
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
16
14
|
|
|
17
15
|
const PKG_NAME = "@tencent-connect/openclaw-qqbot";
|
|
18
16
|
const ENCODED_PKG = encodeURIComponent(PKG_NAME);
|
|
@@ -22,23 +20,18 @@ const REGISTRIES = [
|
|
|
22
20
|
`https://registry.npmmirror.com/${ENCODED_PKG}`,
|
|
23
21
|
];
|
|
24
22
|
|
|
25
|
-
let CURRENT_VERSION = getPackageVersion(
|
|
23
|
+
let CURRENT_VERSION = getPackageVersion();
|
|
26
24
|
|
|
27
25
|
export interface UpdateInfo {
|
|
28
26
|
current: string;
|
|
29
|
-
/** 最佳升级目标(prerelease 用户优先 alpha,稳定版用户取 latest) */
|
|
30
27
|
latest: string | null;
|
|
31
|
-
/** 稳定版 dist-tag */
|
|
32
28
|
stable: string | null;
|
|
33
|
-
/** alpha dist-tag */
|
|
34
29
|
alpha: string | null;
|
|
35
30
|
hasUpdate: boolean;
|
|
36
31
|
checkedAt: number;
|
|
37
32
|
error?: string;
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
let _log: { info: (msg: string) => void; error: (msg: string) => void; debug?: (msg: string) => void } | undefined;
|
|
41
|
-
|
|
42
35
|
function fetchJson(url: string, timeoutMs: number): Promise<any> {
|
|
43
36
|
return new Promise((resolve, reject) => {
|
|
44
37
|
const req = https.get(url, { timeout: timeoutMs, headers: { Accept: "application/json" } }, (res) => {
|
|
@@ -58,14 +51,14 @@ function fetchJson(url: string, timeoutMs: number): Promise<any> {
|
|
|
58
51
|
});
|
|
59
52
|
}
|
|
60
53
|
|
|
61
|
-
async function fetchDistTags(): Promise<Record<string, string>> {
|
|
54
|
+
async function fetchDistTags(log?: PluginLogger): Promise<Record<string, string>> {
|
|
62
55
|
for (const url of REGISTRIES) {
|
|
63
56
|
try {
|
|
64
57
|
const json = await fetchJson(url, 10_000);
|
|
65
58
|
const tags = json["dist-tags"];
|
|
66
59
|
if (tags && typeof tags === "object") return tags;
|
|
67
60
|
} catch (e: any) {
|
|
68
|
-
|
|
61
|
+
log?.debug(`[update-checker] ${url} failed: ${e.message}`);
|
|
69
62
|
}
|
|
70
63
|
}
|
|
71
64
|
throw new Error("all registries failed");
|
|
@@ -76,7 +69,6 @@ function buildUpdateInfo(tags: Record<string, string>): UpdateInfo {
|
|
|
76
69
|
const stableTag = tags.latest || null;
|
|
77
70
|
const alphaTag = tags.alpha || null;
|
|
78
71
|
|
|
79
|
-
// 严格隔离:alpha 只跟 alpha 比,正式版只跟正式版比,不交叉
|
|
80
72
|
const compareTarget = currentIsPrerelease ? alphaTag : stableTag;
|
|
81
73
|
|
|
82
74
|
const hasUpdate = typeof compareTarget === "string"
|
|
@@ -93,37 +85,31 @@ function buildUpdateInfo(tags: Record<string, string>): UpdateInfo {
|
|
|
93
85
|
};
|
|
94
86
|
}
|
|
95
87
|
|
|
96
|
-
/**
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
if (log) _log = log;
|
|
103
|
-
// 预热:fire-and-forget
|
|
104
|
-
getUpdateInfo().then((info) => {
|
|
88
|
+
/**
|
|
89
|
+
* Gateway 启动时调用,后台预热版本缓存。
|
|
90
|
+
* log 通过闭包传递,避免模块级可变状态。
|
|
91
|
+
*/
|
|
92
|
+
export function triggerUpdateCheck(log: PluginLogger): void {
|
|
93
|
+
getUpdateInfo(log).then((info) => {
|
|
105
94
|
if (info.hasUpdate) {
|
|
106
|
-
|
|
95
|
+
log.info(`[update-checker] new version available: ${info.latest} (current: ${CURRENT_VERSION})`);
|
|
107
96
|
}
|
|
108
97
|
}).catch(() => {});
|
|
109
98
|
}
|
|
110
99
|
|
|
111
100
|
/** 每次实时查询 npm registry */
|
|
112
|
-
export async function getUpdateInfo(): Promise<UpdateInfo> {
|
|
101
|
+
export async function getUpdateInfo(log?: PluginLogger): Promise<UpdateInfo> {
|
|
113
102
|
try {
|
|
114
|
-
const tags = await fetchDistTags();
|
|
103
|
+
const tags = await fetchDistTags(log);
|
|
115
104
|
return buildUpdateInfo(tags);
|
|
116
105
|
} catch (err: any) {
|
|
117
|
-
|
|
106
|
+
log?.debug(`[update-checker] check failed: ${err.message}`);
|
|
118
107
|
return { current: CURRENT_VERSION, latest: null, stable: null, alpha: null, hasUpdate: false, checkedAt: Date.now(), error: err.message };
|
|
119
108
|
}
|
|
120
109
|
}
|
|
121
110
|
|
|
122
111
|
/**
|
|
123
112
|
* 检查指定版本是否存在于 npm registry
|
|
124
|
-
* 用于 /bot-upgrade --version 的前置校验
|
|
125
|
-
* @param version 要检查的版本号
|
|
126
|
-
* @param pkgName 可选的包名(如 "@ryantest/openclaw-qqbot"),默认使用内置包名
|
|
127
113
|
*/
|
|
128
114
|
export async function checkVersionExists(version: string, pkgName?: string): Promise<boolean> {
|
|
129
115
|
const registries = pkgName ? buildRegistries(pkgName) : REGISTRIES;
|
|
@@ -139,7 +125,6 @@ export async function checkVersionExists(version: string, pkgName?: string): Pro
|
|
|
139
125
|
return false;
|
|
140
126
|
}
|
|
141
127
|
|
|
142
|
-
/** 根据自定义包名构建 registry URL 列表 */
|
|
143
128
|
function buildRegistries(pkgName: string): string[] {
|
|
144
129
|
const encoded = encodeURIComponent(pkgName);
|
|
145
130
|
return [
|
|
@@ -156,16 +141,13 @@ function compareVersions(a: string, b: string): number {
|
|
|
156
141
|
};
|
|
157
142
|
const pa = parse(a);
|
|
158
143
|
const pb = parse(b);
|
|
159
|
-
// 先比主版本号
|
|
160
144
|
for (let i = 0; i < 3; i++) {
|
|
161
145
|
const diff = (pa.parts[i] || 0) - (pb.parts[i] || 0);
|
|
162
146
|
if (diff !== 0) return diff;
|
|
163
147
|
}
|
|
164
|
-
// 主版本号相同:正式版 > prerelease
|
|
165
148
|
if (!pa.pre && pb.pre) return 1;
|
|
166
149
|
if (pa.pre && !pb.pre) return -1;
|
|
167
150
|
if (!pa.pre && !pb.pre) return 0;
|
|
168
|
-
// 都是 prerelease:按段逐一比较(alpha.1 vs alpha.2)
|
|
169
151
|
const aParts = pa.pre!.split(".");
|
|
170
152
|
const bParts = pb.pre!.split(".");
|
|
171
153
|
for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
|
|
@@ -173,11 +155,9 @@ function compareVersions(a: string, b: string): number {
|
|
|
173
155
|
const bP = bParts[i] ?? "";
|
|
174
156
|
const aNum = Number(aP);
|
|
175
157
|
const bNum = Number(bP);
|
|
176
|
-
// 都是数字则按数字比较
|
|
177
158
|
if (!isNaN(aNum) && !isNaN(bNum)) {
|
|
178
159
|
if (aNum !== bNum) return aNum - bNum;
|
|
179
160
|
} else {
|
|
180
|
-
// 字符串比较
|
|
181
161
|
if (aP < bP) return -1;
|
|
182
162
|
if (aP > bP) return 1;
|
|
183
163
|
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBotGateway 事件处理
|
|
3
|
+
*
|
|
4
|
+
* 处理 SDK 的 message / interaction 事件:
|
|
5
|
+
* - message: 中间件处理完毕后,将消息转发到 OpenClaw AI
|
|
6
|
+
* - interaction: 配置更新 / 审批按钮
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { MiddlewareContext, QQBotInboundMessage, InteractionEvent } from '@tencent-connect/qqbot-nodejs';
|
|
10
|
+
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
11
|
+
import type { ResolvedQQBotAccount } from '../types.js';
|
|
12
|
+
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
13
|
+
import { dispatchToOpenClaw } from '../dispatch/index.js';
|
|
14
|
+
import { runWithRequestContext } from '../request-context.js';
|
|
15
|
+
import { getApprovalHandler } from '../features/approval-handler.js';
|
|
16
|
+
import { recordKnownUser } from '../features/proactive.js';
|
|
17
|
+
import { cacheMsgId } from '../features/msgid-cache.js';
|
|
18
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
19
|
+
import { resolveGroupConfigFromAccount, resolveGroupPolicy, resolveMentionPatterns } from '../config.js';
|
|
20
|
+
import { getPackageVersion } from '../utils/pkg-version.js';
|
|
21
|
+
import { getOpenClawVersion } from '../bot-instance.js';
|
|
22
|
+
|
|
23
|
+
export async function handleMessage(
|
|
24
|
+
ctx: MiddlewareContext,
|
|
25
|
+
msg: QQBotInboundMessage,
|
|
26
|
+
account: ResolvedQQBotAccount,
|
|
27
|
+
runtime: PluginRuntime,
|
|
28
|
+
log: PluginLogger,
|
|
29
|
+
): Promise<void> {
|
|
30
|
+
const hlog = log.child('handle');
|
|
31
|
+
const scope = msg.replyTarget.scope;
|
|
32
|
+
const targetId = scope === 'group'
|
|
33
|
+
? `qqbot:group:${msg.replyTarget.targetId}`
|
|
34
|
+
: `qqbot:c2c:${msg.replyTarget.targetId}`;
|
|
35
|
+
|
|
36
|
+
const mergedCount = (ctx.state.mergedMessages as unknown[] | undefined)?.length;
|
|
37
|
+
if (mergedCount) {
|
|
38
|
+
hlog.info(`merged batch count=${mergedCount} msgId=${msg.messageId}`);
|
|
39
|
+
} else {
|
|
40
|
+
hlog.debug(`enter msgId=${msg.messageId} scope=${scope} contentLen=${(msg.content ?? '').length}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
cacheMsgId(scope, msg.replyTarget.targetId, msg.messageId);
|
|
45
|
+
|
|
46
|
+
recordKnownUser({
|
|
47
|
+
type: scope === 'group' ? 'group' : 'c2c',
|
|
48
|
+
openid: scope === 'group' ? msg.replyTarget.targetId : msg.senderId,
|
|
49
|
+
accountId: account.accountId,
|
|
50
|
+
nickname: msg.senderName,
|
|
51
|
+
lastInteractionAt: Date.now(),
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await runWithRequestContext(
|
|
55
|
+
{
|
|
56
|
+
accountId: account.accountId,
|
|
57
|
+
messageId: msg.messageId,
|
|
58
|
+
openId: msg.senderId,
|
|
59
|
+
target: targetId,
|
|
60
|
+
},
|
|
61
|
+
() => dispatchToOpenClaw(ctx, msg, account, runtime, log),
|
|
62
|
+
);
|
|
63
|
+
} catch (err) {
|
|
64
|
+
hlog.error(`dispatch error: ${err}`);
|
|
65
|
+
}
|
|
66
|
+
hlog.debug(`done msgId=${msg.messageId}`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const INTERACTION_QUERY = 2001;
|
|
70
|
+
const INTERACTION_UPDATE = 2002;
|
|
71
|
+
|
|
72
|
+
export async function handleInteraction(
|
|
73
|
+
event: InteractionEvent,
|
|
74
|
+
account: ResolvedQQBotAccount,
|
|
75
|
+
runtime: PluginRuntime,
|
|
76
|
+
log: PluginLogger,
|
|
77
|
+
acknowledgeInteraction: (id: string, code?: number, data?: Record<string, unknown>) => Promise<void>,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
if (event.data?.type === INTERACTION_QUERY) {
|
|
80
|
+
await handleConfigQuery(event, account, runtime, log, acknowledgeInteraction);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (event.data?.type === INTERACTION_UPDATE) {
|
|
84
|
+
await handleConfigUpdate(event, account, runtime, log);
|
|
85
|
+
try {
|
|
86
|
+
const adapters = getAdapters(runtime);
|
|
87
|
+
const cfg = adapters.getConfig?.() ?? {};
|
|
88
|
+
const groupOpenid = (event as any).group_openid ?? '';
|
|
89
|
+
const updatedCfg = groupOpenid ? resolveGroupConfigFromAccount(account, groupOpenid) : null;
|
|
90
|
+
const requireMention = updatedCfg?.requireMention ?? true;
|
|
91
|
+
const clawCfg = buildClawCfg(requireMention, [], resolveGroupPolicy(cfg, account.accountId));
|
|
92
|
+
await acknowledgeInteraction(event.id, 0, { claw_cfg: clawCfg });
|
|
93
|
+
} catch {
|
|
94
|
+
try { await acknowledgeInteraction(event.id); } catch { /* ignore */ }
|
|
95
|
+
}
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
await handleApproval(event, account, log, acknowledgeInteraction);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Interaction 子处理 ──
|
|
103
|
+
|
|
104
|
+
async function handleConfigQuery(
|
|
105
|
+
event: InteractionEvent,
|
|
106
|
+
account: ResolvedQQBotAccount,
|
|
107
|
+
runtime: PluginRuntime,
|
|
108
|
+
log: PluginLogger,
|
|
109
|
+
ack: (id: string, code?: number, data?: Record<string, unknown>) => Promise<void>,
|
|
110
|
+
): Promise<void> {
|
|
111
|
+
const groupOpenid = (event as any).group_openid ?? '';
|
|
112
|
+
try {
|
|
113
|
+
const adapters = getAdapters(runtime);
|
|
114
|
+
const cfg = adapters.getConfig?.() ?? {};
|
|
115
|
+
const groupCfg = groupOpenid ? resolveGroupConfigFromAccount(account, groupOpenid) : null;
|
|
116
|
+
const requireMention = groupCfg?.requireMention ?? true;
|
|
117
|
+
const agentId = groupOpenid
|
|
118
|
+
? adapters.resolveAgentRoute?.({ cfg, channel: 'qqbot', accountId: account.accountId, peer: { kind: 'group', id: groupOpenid } })?.agentId
|
|
119
|
+
: undefined;
|
|
120
|
+
const mentionPatterns = resolveMentionPatterns(cfg, agentId);
|
|
121
|
+
const clawCfg = buildClawCfg(requireMention, mentionPatterns, resolveGroupPolicy(cfg, account.accountId));
|
|
122
|
+
log.info(`interaction query: group=${groupOpenid} requireMention=${requireMention}`);
|
|
123
|
+
await ack(event.id, 0, { claw_cfg: clawCfg });
|
|
124
|
+
} catch (err) {
|
|
125
|
+
log.warn(`interaction query failed: ${(err as Error)?.message ?? err}, ack without data`);
|
|
126
|
+
try { await ack(event.id); } catch { /* ignore */ }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function handleConfigUpdate(
|
|
131
|
+
event: InteractionEvent,
|
|
132
|
+
account: ResolvedQQBotAccount,
|
|
133
|
+
runtime: PluginRuntime,
|
|
134
|
+
log: PluginLogger,
|
|
135
|
+
): Promise<void> {
|
|
136
|
+
const resolved = (event.data as any)?.resolved;
|
|
137
|
+
const update = resolved?.claw_cfg;
|
|
138
|
+
const groupOpenid = (event as any).group_openid ?? '';
|
|
139
|
+
|
|
140
|
+
if (update?.require_mention !== undefined && groupOpenid) {
|
|
141
|
+
try {
|
|
142
|
+
await setGroupRequireMention(runtime, account.accountId, groupOpenid, update.require_mention === 'mention');
|
|
143
|
+
log.info(`interaction: group=${groupOpenid} requireMention=${update.require_mention}`);
|
|
144
|
+
} catch (err) {
|
|
145
|
+
log.error(`interaction update failed: ${err}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function handleApproval(
|
|
151
|
+
event: InteractionEvent,
|
|
152
|
+
account: ResolvedQQBotAccount,
|
|
153
|
+
log: PluginLogger,
|
|
154
|
+
ack: (id: string) => Promise<void>,
|
|
155
|
+
): Promise<void> {
|
|
156
|
+
try { await ack(event.id); } catch { /* ignore */ }
|
|
157
|
+
|
|
158
|
+
const buttonData = event.data?.resolved?.button_data;
|
|
159
|
+
if (!buttonData?.startsWith('approve:')) return;
|
|
160
|
+
|
|
161
|
+
// 身份授权校验:操作者需在 allowFrom 白名单中
|
|
162
|
+
const operatorId = resolveOperatorId(event);
|
|
163
|
+
if (!isApprovalAuthorized(account, operatorId)) {
|
|
164
|
+
log.warn(`[approval] unauthorized operator=${operatorId ?? 'unknown'} account=${account.accountId}`);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const parts = buttonData.split(':');
|
|
169
|
+
if (parts.length < 3) return;
|
|
170
|
+
|
|
171
|
+
const handler = getApprovalHandler(account.accountId);
|
|
172
|
+
if (!handler) return;
|
|
173
|
+
|
|
174
|
+
const approvalId = parts[1];
|
|
175
|
+
const decision = parts[2] as 'allow-once' | 'allow-always' | 'deny';
|
|
176
|
+
try { await handler.resolveApproval(approvalId, decision); } catch (err) {
|
|
177
|
+
log.error(`interaction approve error: ${err instanceof Error ? err.message : String(err)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const CHANNEL_VER = getPackageVersion();
|
|
182
|
+
|
|
183
|
+
// ── 审批授权校验 ──
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* 从交互事件中提取操作者身份标识。
|
|
187
|
+
* QQ Bot 按钮回调事件中,操作者 openid 通常在 `user_openid` 或 `data.resolved.user_id` 字段。
|
|
188
|
+
*/
|
|
189
|
+
function resolveOperatorId(event: InteractionEvent): string | undefined {
|
|
190
|
+
const evt = event as any;
|
|
191
|
+
return evt.user_openid
|
|
192
|
+
?? evt.data?.resolved?.user_id
|
|
193
|
+
?? evt.data?.resolved?.user_openid
|
|
194
|
+
?? evt.openid;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* 校验操作者是否有权限执行审批操作。
|
|
199
|
+
* 规则:allowFrom 为空或含 "*" → 开放;否则操作者必须在 allowFrom 中。
|
|
200
|
+
*/
|
|
201
|
+
function isApprovalAuthorized(account: ResolvedQQBotAccount, operatorId?: string): boolean {
|
|
202
|
+
if (!operatorId) return false;
|
|
203
|
+
const allowFrom = account.config?.allowFrom ?? [];
|
|
204
|
+
if (!allowFrom.length || allowFrom.includes('*')) return true;
|
|
205
|
+
return allowFrom.includes(operatorId);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function buildClawCfg(
|
|
209
|
+
requireMention: boolean,
|
|
210
|
+
mentionPatterns: string[],
|
|
211
|
+
groupPolicy: string,
|
|
212
|
+
): Record<string, unknown> {
|
|
213
|
+
return {
|
|
214
|
+
channel_type: 'qqbot',
|
|
215
|
+
channel_ver: CHANNEL_VER,
|
|
216
|
+
claw_type: 'openclaw',
|
|
217
|
+
claw_ver: getOpenClawVersion(),
|
|
218
|
+
require_mention: requireMention ? 'mention' : 'always',
|
|
219
|
+
group_policy: groupPolicy,
|
|
220
|
+
mention_patterns: mentionPatterns.join(','),
|
|
221
|
+
online_state: 'online',
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// ── Config 写入 ──
|
|
226
|
+
|
|
227
|
+
function setGroupRequireMention(
|
|
228
|
+
runtime: PluginRuntime,
|
|
229
|
+
accountId: string,
|
|
230
|
+
groupOpenid: string,
|
|
231
|
+
requireMention: boolean,
|
|
232
|
+
): Promise<void> {
|
|
233
|
+
const adapters = getAdapters(runtime);
|
|
234
|
+
return adapters.persistConfig?.((cfg: any) => {
|
|
235
|
+
const qqbot = (cfg.channels ?? {})?.qqbot ?? {};
|
|
236
|
+
const groups = accountId !== 'default' && qqbot.accounts?.[accountId]
|
|
237
|
+
? (qqbot.accounts[accountId].groups = { ...qqbot.accounts[accountId].groups })
|
|
238
|
+
: (qqbot.groups = { ...qqbot.groups });
|
|
239
|
+
groups[groupOpenid] = { ...groups[groupOpenid], requireMention };
|
|
240
|
+
}) ?? Promise.resolve();
|
|
241
|
+
}
|