@tencent-connect/openclaw-qqbot 1.7.1 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -4
- package/README.zh.md +207 -4
- 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 -18
- 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 +400 -73
- package/scripts/upgrade-via-source.sh +126 -89
- 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 +59 -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 +1 -1
- package/src/types-augment.d.ts +54 -0
- package/src/types.ts +71 -8
- package/src/typings/openclaw-webhook-ingress.d.ts +66 -0
- 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 -17
- package/dist/index.js +0 -26
- package/dist/src/admin-resolver.d.ts +0 -33
- package/dist/src/admin-resolver.js +0 -157
- package/dist/src/api.d.ts +0 -307
- package/dist/src/api.js +0 -910
- 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 -56
- package/dist/src/config.js +0 -278
- 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 -2063
- 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 -2117
- 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 -234
- package/dist/src/tools/remind.d.ts +0 -2
- package/dist/src/tools/remind.js +0 -256
- package/dist/src/types.d.ts +0 -443
- 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 -755
- 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 -126
- package/skills/qqbot-media/SKILL.md +0 -60
- package/src/admin-resolver.ts +0 -181
- package/src/api.ts +0 -1319
- package/src/credential-backup.ts +0 -72
- package/src/deliver-debounce.ts +0 -229
- package/src/gateway.ts +0 -2304
- 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 -274
- 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 -2297
- 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 -281
- package/src/typing-keepalive.ts +0 -59
- package/src/utils/audio-convert.ts +0 -859
- 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,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 审批相关工具函数
|
|
3
|
+
*
|
|
4
|
+
* 审批 payload 判断逻辑,供 channel.ts 和 features/approval-handler 使用。
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** 检查 payload 是否为审批消息(execApproval / plugin approval) */
|
|
8
|
+
export function isApprovalPayload(payload: unknown): boolean {
|
|
9
|
+
if (!payload || typeof payload !== 'object') return false;
|
|
10
|
+
const p = payload as Record<string, unknown>;
|
|
11
|
+
const cd = p.channelData;
|
|
12
|
+
if (cd && typeof cd === 'object' && !Array.isArray(cd)) {
|
|
13
|
+
const execApproval = (cd as Record<string, unknown>).execApproval;
|
|
14
|
+
if (execApproval && typeof execApproval === 'object' && !Array.isArray(execApproval)) {
|
|
15
|
+
return true;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
const text = typeof p.text === 'string' ? p.text : '';
|
|
19
|
+
return /(?:Plugin|Exec) approval (?:required|allowed|denied|expired)/i.test(text);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 审批 ChannelPlugin stub — 空壳实现(实际审批由 features/approval-handler 处理) */
|
|
23
|
+
export const approvalStubs = {
|
|
24
|
+
execApprovals: {
|
|
25
|
+
getInitiatingSurfaceState: () => ({ kind: 'enabled' as const }),
|
|
26
|
+
shouldSuppressForwardingFallback: () => true,
|
|
27
|
+
shouldSuppressLocalPrompt: ({ payload }: { payload: unknown }) => isApprovalPayload(payload),
|
|
28
|
+
buildPendingPayload: () => null,
|
|
29
|
+
buildResolvedPayload: () => null,
|
|
30
|
+
},
|
|
31
|
+
approvals: {
|
|
32
|
+
delivery: {
|
|
33
|
+
hasConfiguredDmRoute: () => true,
|
|
34
|
+
shouldSuppressForwardingFallback: () => true,
|
|
35
|
+
},
|
|
36
|
+
render: {
|
|
37
|
+
exec: { buildPendingPayload: () => null, buildResolvedPayload: () => null },
|
|
38
|
+
plugin: { buildPendingPayload: () => null, buildResolvedPayload: () => null },
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
} as const;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 凭证暂存与恢复
|
|
3
|
+
*
|
|
4
|
+
* 解决热更新被打断时 openclaw.json 中 appId/secret 丢失的问题。
|
|
5
|
+
*
|
|
6
|
+
* 存储路径:~/.openclaw/qqbot/data/credential-backup/current.json
|
|
7
|
+
* 使用子目录避免被框架 SQLite 迁移扫描(迁移只匹配 data/credential-backup.json)。
|
|
8
|
+
*/
|
|
9
|
+
import fs from 'node:fs';
|
|
10
|
+
import path from 'node:path';
|
|
11
|
+
import { getQQBotDataDir } from '../utils/platform.js';
|
|
12
|
+
|
|
13
|
+
const BACKUP_DIR = 'credential-backup';
|
|
14
|
+
const BACKUP_FILENAME = 'current.json';
|
|
15
|
+
const LEGACY_FILENAME = 'credential-backup.json';
|
|
16
|
+
|
|
17
|
+
interface CredentialBackup {
|
|
18
|
+
accountId: string;
|
|
19
|
+
appId: string;
|
|
20
|
+
clientSecret: string;
|
|
21
|
+
savedAt: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getBackupPath(): string {
|
|
25
|
+
return path.join(getQQBotDataDir('data'), BACKUP_DIR, BACKUP_FILENAME);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 保存凭证快照到暂存文件(gateway 成功启动后调用)
|
|
30
|
+
*/
|
|
31
|
+
export function saveCredentialBackup(accountId: string, appId: string, clientSecret: string): void {
|
|
32
|
+
if (!appId || !clientSecret) return;
|
|
33
|
+
try {
|
|
34
|
+
const backupPath = getBackupPath();
|
|
35
|
+
const dir = path.dirname(backupPath);
|
|
36
|
+
if (!fs.existsSync(dir)) {
|
|
37
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
const data: CredentialBackup = {
|
|
40
|
+
accountId,
|
|
41
|
+
appId,
|
|
42
|
+
clientSecret,
|
|
43
|
+
savedAt: new Date().toISOString(),
|
|
44
|
+
};
|
|
45
|
+
const tmpPath = backupPath + '.tmp';
|
|
46
|
+
fs.writeFileSync(tmpPath, JSON.stringify(data, null, 2) + '\n', 'utf8');
|
|
47
|
+
fs.renameSync(tmpPath, backupPath);
|
|
48
|
+
} catch {
|
|
49
|
+
// 非关键操作,静默忽略
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 从暂存文件读取凭证(仅在配置为空时调用)
|
|
55
|
+
* 返回 null 表示无可用备份
|
|
56
|
+
*/
|
|
57
|
+
export function loadCredentialBackup(accountId?: string): CredentialBackup | null {
|
|
58
|
+
try {
|
|
59
|
+
// 优先读新路径
|
|
60
|
+
const backupPath = getBackupPath();
|
|
61
|
+
if (fs.existsSync(backupPath)) {
|
|
62
|
+
const data = readBackupFile(backupPath, accountId);
|
|
63
|
+
if (data) return data;
|
|
64
|
+
}
|
|
65
|
+
// 兼容旧路径(升级过渡)
|
|
66
|
+
const legacyPath = path.join(getQQBotDataDir('data'), LEGACY_FILENAME);
|
|
67
|
+
if (fs.existsSync(legacyPath)) {
|
|
68
|
+
return readBackupFile(legacyPath, accountId);
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
} catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readBackupFile(filePath: string, accountId?: string): CredentialBackup | null {
|
|
77
|
+
const raw = fs.readFileSync(filePath, 'utf8');
|
|
78
|
+
const data: CredentialBackup = JSON.parse(raw);
|
|
79
|
+
if (!data.appId || !data.clientSecret) return null;
|
|
80
|
+
if (accountId && data.accountId !== accountId) return null;
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { MemoryHistoryStore } from '@tencent-connect/qqbot-nodejs';
|
|
2
|
+
import type { HistoryStore } from '@tencent-connect/qqbot-nodejs';
|
|
3
|
+
|
|
4
|
+
let _store: HistoryStore | null = null;
|
|
5
|
+
|
|
6
|
+
export function getHistoryStore(): HistoryStore {
|
|
7
|
+
if (!_store) _store = new MemoryHistoryStore();
|
|
8
|
+
return _store;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/** 用 accountId 前缀隔离多账号,避免同群历史串用 */
|
|
12
|
+
export function historyGroupKey(accountId: string, groupId: string): string {
|
|
13
|
+
return `${accountId}:${groupId}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** 清空群历史(dispatch 完成后调用) */
|
|
17
|
+
export function clearGroupHistory(accountId: string, groupId: string): void {
|
|
18
|
+
_store?.clear?.(historyGroupKey(accountId, groupId));
|
|
19
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息 ID 缓存 — 记录最近处理的 msgId,供被动回复使用。
|
|
3
|
+
*
|
|
4
|
+
* 场景:外部调用发送消息时没有上下文,
|
|
5
|
+
* 从缓存获取最近一条未过期的 msgId 作为被动回复目标。
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
interface CachedMsgId {
|
|
9
|
+
msgId: string;
|
|
10
|
+
timestamp: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const MAX_PER_TARGET = 10;
|
|
14
|
+
const TTL_GROUP = 5 * 60 * 1000; // 5 分钟
|
|
15
|
+
const TTL_C2C = 30 * 60 * 1000; // 30 分钟
|
|
16
|
+
const MAX_TARGETS = 200;
|
|
17
|
+
|
|
18
|
+
const cache = new Map<string, CachedMsgId[]>();
|
|
19
|
+
|
|
20
|
+
export function cacheMsgId(scope: string, targetId: string, msgId: string): void {
|
|
21
|
+
if (!scope || !targetId || !msgId) return;
|
|
22
|
+
const key = `${scope}:${targetId}`;
|
|
23
|
+
const existing = cache.get(key);
|
|
24
|
+
if (existing) {
|
|
25
|
+
// LRU: 删除再重新插入
|
|
26
|
+
cache.delete(key);
|
|
27
|
+
existing.push({ msgId, timestamp: Date.now() });
|
|
28
|
+
if (existing.length > MAX_PER_TARGET) existing.shift();
|
|
29
|
+
cache.set(key, existing);
|
|
30
|
+
} else {
|
|
31
|
+
cache.set(key, [{ msgId, timestamp: Date.now() }]);
|
|
32
|
+
if (cache.size > MAX_TARGETS) {
|
|
33
|
+
const oldest = cache.keys().next().value;
|
|
34
|
+
if (oldest) cache.delete(oldest);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function getCachedMsgId(scope: string, targetId: string): string | undefined {
|
|
40
|
+
const key = `${scope}:${targetId}`;
|
|
41
|
+
const list = cache.get(key);
|
|
42
|
+
if (!list || list.length === 0) return undefined;
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const ttl = scope === 'group' ? TTL_GROUP : TTL_C2C;
|
|
45
|
+
for (let i = list.length - 1; i >= 0; i--) {
|
|
46
|
+
if (now - list[i].timestamp < ttl) {
|
|
47
|
+
return list[i].msgId;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function clearMsgIdCache(scope: string, targetId: string): void {
|
|
54
|
+
cache.delete(`${scope}:${targetId}`);
|
|
55
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QQBot CLI Onboarding Adapter
|
|
3
|
+
*
|
|
4
|
+
* 提供 openclaw onboard 命令的交互式配置支持。
|
|
5
|
+
* 从原 src/onboarding.ts 迁移,保持接口兼容。
|
|
6
|
+
*/
|
|
7
|
+
import type {
|
|
8
|
+
ChannelOnboardingAdapter,
|
|
9
|
+
OpenClawConfig,
|
|
10
|
+
} from 'openclaw/plugin-sdk';
|
|
11
|
+
import { DEFAULT_ACCOUNT_ID, listQQBotAccountIds, resolveQQBotAccount } from '../config.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Onboarding adapter — 导出给 ChannelPlugin 使用
|
|
15
|
+
*
|
|
16
|
+
* 注:完整 onboarding 交互逻辑较复杂(约 300 行),
|
|
17
|
+
* 此处仅保留 adapter 骨架,完整实现从原文件迁移。
|
|
18
|
+
*/
|
|
19
|
+
export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = {
|
|
20
|
+
getStatus: (ctx) => {
|
|
21
|
+
const cfg = ctx.config as OpenClawConfig;
|
|
22
|
+
const accountIds = listQQBotAccountIds(cfg);
|
|
23
|
+
if (accountIds.length === 0) {
|
|
24
|
+
return { configured: false, accountCount: 0 };
|
|
25
|
+
}
|
|
26
|
+
const firstAccount = resolveQQBotAccount(cfg, accountIds[0]);
|
|
27
|
+
return {
|
|
28
|
+
configured: Boolean(firstAccount.appId && firstAccount.clientSecret),
|
|
29
|
+
accountCount: accountIds.length,
|
|
30
|
+
defaultAccountId: accountIds[0],
|
|
31
|
+
};
|
|
32
|
+
},
|
|
33
|
+
configure: async (ctx) => {
|
|
34
|
+
// 完整实现需要从原 onboarding.ts 迁移交互逻辑
|
|
35
|
+
// 此处返回默认结果
|
|
36
|
+
return { success: false, message: 'Onboarding not yet migrated to new architecture' };
|
|
37
|
+
},
|
|
38
|
+
};
|
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import { createPluginLogger } from "../utils/plugin-logger.js";
|
|
2
|
+
|
|
3
|
+
const log = createPluginLogger({ prefix: '[proactive]' });
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
6
|
* QQ Bot 主动发送消息模块
|
|
3
7
|
*
|
|
@@ -9,7 +13,7 @@
|
|
|
9
13
|
|
|
10
14
|
import * as fs from "node:fs";
|
|
11
15
|
import * as path from "node:path";
|
|
12
|
-
import type { ResolvedQQBotAccount } from "
|
|
16
|
+
import type { ResolvedQQBotAccount } from "../types.js";
|
|
13
17
|
|
|
14
18
|
// ============ 类型定义(本地) ============
|
|
15
19
|
|
|
@@ -56,14 +60,9 @@ export interface ListKnownUsersOptions {
|
|
|
56
60
|
limit?: number;
|
|
57
61
|
}
|
|
58
62
|
import {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
sendChannelMessage,
|
|
63
|
-
sendC2CImageMessage,
|
|
64
|
-
sendGroupImageMessage,
|
|
65
|
-
} from "./api.js";
|
|
66
|
-
import { resolveQQBotAccount } from "./config.js";
|
|
63
|
+
getBotForAccount,
|
|
64
|
+
} from "../bot-instance.js";
|
|
65
|
+
import { resolveQQBotAccount } from "../config.js";
|
|
67
66
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
68
67
|
|
|
69
68
|
// ============ 用户存储管理 ============
|
|
@@ -72,7 +71,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
|
72
71
|
* 已知用户存储
|
|
73
72
|
* 使用简单的 JSON 文件存储,保存在 .openclaw/qqbot 目录下
|
|
74
73
|
*/
|
|
75
|
-
import { getQQBotDataDir } from "
|
|
74
|
+
import { getQQBotDataDir } from "../utils/platform.js";
|
|
76
75
|
|
|
77
76
|
const STORAGE_DIR = getQQBotDataDir("data");
|
|
78
77
|
const KNOWN_USERS_FILE = path.join(STORAGE_DIR, "known-users.json");
|
|
@@ -127,7 +126,7 @@ function loadKnownUsers(): Map<string, KnownUser> {
|
|
|
127
126
|
cacheLastModified = fs.statSync(KNOWN_USERS_FILE).mtimeMs;
|
|
128
127
|
}
|
|
129
128
|
} catch (err) {
|
|
130
|
-
|
|
129
|
+
log.error(`Failed to load known users: ${err}`);
|
|
131
130
|
}
|
|
132
131
|
|
|
133
132
|
knownUsersCache = users;
|
|
@@ -145,7 +144,7 @@ function saveKnownUsers(users: Map<string, KnownUser>): void {
|
|
|
145
144
|
cacheLastModified = Date.now();
|
|
146
145
|
knownUsersCache = users;
|
|
147
146
|
} catch (err) {
|
|
148
|
-
|
|
147
|
+
log.error(`Failed to save known users: ${err}`);
|
|
149
148
|
}
|
|
150
149
|
}
|
|
151
150
|
|
|
@@ -170,7 +169,7 @@ export function recordKnownUser(user: Omit<KnownUser, "firstInteractionAt">): vo
|
|
|
170
169
|
});
|
|
171
170
|
|
|
172
171
|
saveKnownUsers(users);
|
|
173
|
-
|
|
172
|
+
log.info(`Recorded user: ${key}`);
|
|
174
173
|
}
|
|
175
174
|
|
|
176
175
|
/**
|
|
@@ -314,44 +313,29 @@ export async function sendProactive(
|
|
|
314
313
|
}
|
|
315
314
|
|
|
316
315
|
try {
|
|
317
|
-
const
|
|
318
|
-
|
|
316
|
+
const bot = getBotForAccount(account.accountId);
|
|
317
|
+
const target = { scope: type as "c2c" | "group", targetId: to };
|
|
318
|
+
|
|
319
319
|
// 如果有图片,先发送图片
|
|
320
320
|
if (imageUrl) {
|
|
321
321
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
} else if (type === "group") {
|
|
325
|
-
await sendGroupImageMessage(accessToken, to, imageUrl, undefined, undefined);
|
|
326
|
-
}
|
|
327
|
-
console.log(`[qqbot:proactive] Sent image to ${type}:${to}`);
|
|
322
|
+
await bot.sendImage(target, { url: imageUrl });
|
|
323
|
+
log.info(`Sent image to ${type}:${to}`);
|
|
328
324
|
} catch (err) {
|
|
329
|
-
|
|
330
|
-
// 图片发送失败不影响文本发送
|
|
325
|
+
log.error(`Failed to send image: ${err}`);
|
|
331
326
|
}
|
|
332
327
|
}
|
|
333
328
|
|
|
334
|
-
//
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if (type === "c2c") {
|
|
338
|
-
result = await sendProactiveC2CMessage(accessToken, to, text);
|
|
339
|
-
} else if (type === "group") {
|
|
340
|
-
result = await sendProactiveGroupMessage(accessToken, to, text);
|
|
341
|
-
} else if (type === "channel") {
|
|
342
|
-
// 频道消息需要 channel_id,这里暂时不支持主动发送
|
|
329
|
+
// 发送文本消息(主动消息:无 msgId)
|
|
330
|
+
if (type === "channel") {
|
|
343
331
|
return {
|
|
344
332
|
success: false,
|
|
345
333
|
error: "Channel proactive messages are not supported. Please use group or c2c.",
|
|
346
334
|
};
|
|
347
|
-
} else {
|
|
348
|
-
return {
|
|
349
|
-
success: false,
|
|
350
|
-
error: `Unknown message type: ${type}`,
|
|
351
|
-
};
|
|
352
335
|
}
|
|
353
|
-
|
|
354
|
-
|
|
336
|
+
|
|
337
|
+
const result = await bot.sendText(target, text);
|
|
338
|
+
log.info(`Sent message to ${type}:${to}, id: ${result.id}`);
|
|
355
339
|
|
|
356
340
|
return {
|
|
357
341
|
success: true,
|
|
@@ -360,7 +344,7 @@ export async function sendProactive(
|
|
|
360
344
|
};
|
|
361
345
|
} catch (err) {
|
|
362
346
|
const message = err instanceof Error ? err.message : String(err);
|
|
363
|
-
|
|
347
|
+
log.error(`Failed to send message: ${message}`);
|
|
364
348
|
|
|
365
349
|
return {
|
|
366
350
|
success: false,
|
|
@@ -487,15 +471,9 @@ export async function sendProactiveMessageDirect(
|
|
|
487
471
|
}
|
|
488
472
|
|
|
489
473
|
try {
|
|
490
|
-
const
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
if (type === "c2c") {
|
|
495
|
-
result = await sendProactiveC2CMessage(accessToken, to, text);
|
|
496
|
-
} else {
|
|
497
|
-
result = await sendProactiveGroupMessage(accessToken, to, text);
|
|
498
|
-
}
|
|
474
|
+
const bot = getBotForAccount(account.accountId);
|
|
475
|
+
const target = { scope: type, targetId: to };
|
|
476
|
+
const result = await bot.sendText(target, text);
|
|
499
477
|
|
|
500
478
|
return {
|
|
501
479
|
success: true,
|
|
@@ -528,3 +506,44 @@ export function getKnownUsersStats(accountId?: string): {
|
|
|
528
506
|
channel: users.filter(u => u.type === "channel").length,
|
|
529
507
|
};
|
|
530
508
|
}
|
|
509
|
+
|
|
510
|
+
/**
|
|
511
|
+
* 获取用户的所有群组(某用户在哪些群里交互过)
|
|
512
|
+
*
|
|
513
|
+
* @param accountId 机器人账户 ID
|
|
514
|
+
* @param openid 用户 openid
|
|
515
|
+
* @returns 群组 openid 列表
|
|
516
|
+
*/
|
|
517
|
+
export function getUserGroups(accountId: string, openid: string): string[] {
|
|
518
|
+
const users = loadKnownUsers();
|
|
519
|
+
const groups: string[] = [];
|
|
520
|
+
for (const [, user] of users) {
|
|
521
|
+
if (user.accountId !== accountId) continue;
|
|
522
|
+
if (user.type !== 'group') continue;
|
|
523
|
+
// group 类型的 openid 是 groupOpenid,检查该群中是否有此用户交互记录
|
|
524
|
+
// 注:当前数据模型 group 记录的 openid 是 groupOpenid 本身
|
|
525
|
+
// 要实现精确的"用户在哪些群",需要在 recordKnownUser 时同时记录 senderOpenid
|
|
526
|
+
// 这里降级为:列出该账户的所有群 openid
|
|
527
|
+
groups.push(user.openid);
|
|
528
|
+
}
|
|
529
|
+
return [...new Set(groups)];
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* 获取群组的所有已知成员
|
|
534
|
+
*
|
|
535
|
+
* @param accountId 机器人账户 ID
|
|
536
|
+
* @param groupOpenid 群组 openid
|
|
537
|
+
* @returns 在该群中交互过的用户列表
|
|
538
|
+
*/
|
|
539
|
+
export function getGroupMembers(accountId: string, groupOpenid: string): KnownUser[] {
|
|
540
|
+
const users = loadKnownUsers();
|
|
541
|
+
const members: KnownUser[] = [];
|
|
542
|
+
for (const [, user] of users) {
|
|
543
|
+
if (user.accountId !== accountId) continue;
|
|
544
|
+
if (user.type === 'group' && user.openid === groupOpenid) {
|
|
545
|
+
members.push(user);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
return members;
|
|
549
|
+
}
|