@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,352 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 消息转发 — 入站消息 → OpenClaw AI
|
|
3
|
+
*
|
|
4
|
+
* 核心职责:
|
|
5
|
+
* 1. 从 SDK MiddlewareContext 构建 OpenClaw 标准信封
|
|
6
|
+
* 2. 通过 runtime-adapter 将消息交给 AI 处理
|
|
7
|
+
*
|
|
8
|
+
* 架构说明:
|
|
9
|
+
* - 所有 runtime.channel.* 访问均通过 runtime-adapter 隔离
|
|
10
|
+
* - log: 前缀由 PluginLogger + 框架自动注入,消息体不重复 accountId
|
|
11
|
+
*/
|
|
12
|
+
import type { MiddlewareContext, QQBotInboundMessage } from '@tencent-connect/qqbot-nodejs';
|
|
13
|
+
import type { PluginRuntime } from 'openclaw/plugin-sdk';
|
|
14
|
+
import type { ResolvedQQBotAccount } from '../types.js';
|
|
15
|
+
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
16
|
+
import { buildEnvelope } from './envelope-builder.js';
|
|
17
|
+
import { assembleBody, type AssembledBody } from './body-assembler.js';
|
|
18
|
+
import { sendText, getGateway } from '../outbound/outbound-service.js';
|
|
19
|
+
import { sendMedia } from '../outbound/media-send.js';
|
|
20
|
+
import { deliverReply, type DeliverPayload, type DeliverInfo, type DeliverContext } from '../outbound/deliver-pipeline.js';
|
|
21
|
+
import { buildCtxPayload } from './ctx-builder.js';
|
|
22
|
+
|
|
23
|
+
import { DeliverDebouncer } from '../outbound/debounce.js';
|
|
24
|
+
import { StreamingController, shouldUseStreaming } from '../outbound/streaming-controller.js';
|
|
25
|
+
import { getAdapters } from '../adapter/resolve.js';
|
|
26
|
+
import { clearGroupHistory } from '../features/history-store.js';
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 将经过中间件处理的入站消息转发给 OpenClaw AI
|
|
31
|
+
*/
|
|
32
|
+
export async function dispatchToOpenClaw(
|
|
33
|
+
ctx: MiddlewareContext,
|
|
34
|
+
msg: QQBotInboundMessage,
|
|
35
|
+
account: ResolvedQQBotAccount,
|
|
36
|
+
runtime: PluginRuntime,
|
|
37
|
+
log?: PluginLogger,
|
|
38
|
+
): Promise<void> {
|
|
39
|
+
const dlog = log?.child('dispatch');
|
|
40
|
+
const adapters = getAdapters(runtime, dlog);
|
|
41
|
+
const envelope = buildEnvelope(ctx, msg, account);
|
|
42
|
+
|
|
43
|
+
dlog?.debug(`received sender=${envelope.senderId} scope=${envelope.chatScope} msgId=${envelope.messageId}`);
|
|
44
|
+
|
|
45
|
+
if (!adapters.dispatchReply) {
|
|
46
|
+
dlog?.error(`runtime adapter dispatchReply not available (openclaw=${adapters.version})`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const assembled: AssembledBody =
|
|
51
|
+
((ctx.state as Record<string, unknown>).assembledBody as AssembledBody | undefined) ??
|
|
52
|
+
assembleBody(ctx, msg, account);
|
|
53
|
+
|
|
54
|
+
const cfg = adapters.getConfig?.() ?? {};
|
|
55
|
+
|
|
56
|
+
const route = adapters.resolveAgentRoute?.({
|
|
57
|
+
cfg,
|
|
58
|
+
channel: 'qqbot',
|
|
59
|
+
accountId: account.accountId,
|
|
60
|
+
peer: {
|
|
61
|
+
kind: envelope.chatScope === 'group' ? 'group' : 'direct',
|
|
62
|
+
id: envelope.chatScope === 'group' ? (envelope.groupId ?? envelope.senderId) : envelope.senderId,
|
|
63
|
+
},
|
|
64
|
+
}) ?? { sessionKey: `qqbot:${account.accountId}:${envelope.senderId}`, accountId: account.accountId };
|
|
65
|
+
|
|
66
|
+
const qualifiedTarget = envelope.targetId;
|
|
67
|
+
const agentId = route.agentId ?? 'default';
|
|
68
|
+
const storePath = adapters.resolveStorePath?.((cfg as any)?.session?.store, { agentId }) ?? '';
|
|
69
|
+
|
|
70
|
+
const ctxPayload = buildCtxPayload({ assembled, envelope, route, msg, ctx, adapters });
|
|
71
|
+
|
|
72
|
+
const ttsRuntime = (runtime as any)?.tts ?? (runtime as any)?.channel?.runtimeContexts?.get?.('tts');
|
|
73
|
+
|
|
74
|
+
const debounceConfig = account.config?.deliverDebounce;
|
|
75
|
+
const debouncer = debounceConfig?.enabled !== false
|
|
76
|
+
? new DeliverDebouncer(debounceConfig, (targetId, mergedText) =>
|
|
77
|
+
sendText({ to: targetId, text: mergedText, accountId: account.accountId, replyToId: envelope.messageId, account }).then(() => {}),
|
|
78
|
+
)
|
|
79
|
+
: undefined;
|
|
80
|
+
|
|
81
|
+
const deliverCtx: DeliverContext = {
|
|
82
|
+
qualifiedTarget,
|
|
83
|
+
accountId: account.accountId,
|
|
84
|
+
replyToId: envelope.messageId,
|
|
85
|
+
chatScope: envelope.chatScope === 'group' ? 'group' : 'direct',
|
|
86
|
+
cfg,
|
|
87
|
+
debouncer: debouncer?.enabled ? debouncer : undefined,
|
|
88
|
+
sendText: (to, text) => sendText({ to, text, accountId: account.accountId, replyToId: envelope.messageId, account }),
|
|
89
|
+
sendMedia: (to, source, opts) => sendMedia({
|
|
90
|
+
to,
|
|
91
|
+
source,
|
|
92
|
+
text: opts?.text ?? '',
|
|
93
|
+
replyToId: envelope.messageId,
|
|
94
|
+
accountId: account.accountId,
|
|
95
|
+
agentId: route.agentId,
|
|
96
|
+
log: deliverCtx.log,
|
|
97
|
+
}),
|
|
98
|
+
textToSpeech: ttsRuntime?.textToSpeech
|
|
99
|
+
? (params) => ttsRuntime.textToSpeech(params)
|
|
100
|
+
: undefined,
|
|
101
|
+
audioFileToSilkBase64: ttsRuntime?.audioFileToSilkBase64
|
|
102
|
+
? (audioPath: string) => ttsRuntime.audioFileToSilkBase64(audioPath)
|
|
103
|
+
: undefined,
|
|
104
|
+
log: log?.child('deliver'),
|
|
105
|
+
agentId: route.agentId ?? 'default',
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const streamingEnabled = shouldUseStreaming(
|
|
109
|
+
account,
|
|
110
|
+
envelope.chatScope === 'group' ? 'group' : 'c2c',
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
const streamingController = streamingEnabled
|
|
114
|
+
? createStreamingController(envelope, account, log?.child('streaming'))
|
|
115
|
+
: null;
|
|
116
|
+
|
|
117
|
+
if (streamingController) {
|
|
118
|
+
dlog?.debug(`streaming enabled for ${envelope.senderId}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const deliveredMediaUrls = new Set<string>();
|
|
122
|
+
const deliveredTexts = new Set<string>();
|
|
123
|
+
|
|
124
|
+
if (!adapters.inboundRun) {
|
|
125
|
+
// 低版本:手动 session + dispatchReply 直调
|
|
126
|
+
if (adapters.recordInboundSession) {
|
|
127
|
+
try {
|
|
128
|
+
await adapters.recordInboundSession({
|
|
129
|
+
storePath,
|
|
130
|
+
sessionKey: route.sessionKey,
|
|
131
|
+
ctx: ctxPayload,
|
|
132
|
+
});
|
|
133
|
+
} catch { /* best-effort */ }
|
|
134
|
+
}
|
|
135
|
+
await adapters.dispatchReply!({
|
|
136
|
+
ctx: ctxPayload,
|
|
137
|
+
cfg,
|
|
138
|
+
dispatcherOptions: {
|
|
139
|
+
deliver: async (payload: DeliverPayload, info?: DeliverInfo) => {
|
|
140
|
+
const text = payload.text?.trim() ?? '';
|
|
141
|
+
// 低版本无 block/final 协议,流式启动后 final 仍需跳过(降级除外)
|
|
142
|
+
if (!payload.mediaUrl && !payload.mediaUrls?.length
|
|
143
|
+
&& text
|
|
144
|
+
&& (deliveredTexts.has(text)
|
|
145
|
+
|| (streamingController?.hasStarted && !streamingController?.shouldFallbackToStatic))
|
|
146
|
+
) {
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const filteredPayload = deliveredMediaUrls.size > 0
|
|
150
|
+
? {
|
|
151
|
+
...payload,
|
|
152
|
+
mediaUrl: payload.mediaUrl && !deliveredMediaUrls.has(payload.mediaUrl)
|
|
153
|
+
? payload.mediaUrl : undefined,
|
|
154
|
+
mediaUrls: payload.mediaUrls?.filter((u) => !deliveredMediaUrls.has(u)),
|
|
155
|
+
}
|
|
156
|
+
: payload;
|
|
157
|
+
await deliverReply(filteredPayload, info, deliverCtx);
|
|
158
|
+
if (text) deliveredTexts.add(text);
|
|
159
|
+
// 记录已投递媒体(流式路径可能已先发送)
|
|
160
|
+
for (const u of filteredPayload.mediaUrls ?? []) deliveredMediaUrls.add(u);
|
|
161
|
+
if (filteredPayload.mediaUrl) deliveredMediaUrls.add(filteredPayload.mediaUrl);
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
replyOptions: {
|
|
165
|
+
abortSignal: ctx.signal,
|
|
166
|
+
runId: envelope.messageId,
|
|
167
|
+
...(streamingController
|
|
168
|
+
? {
|
|
169
|
+
onPartialReply: async (p: { text?: string }) => {
|
|
170
|
+
if (p.text) await streamingController.onPartialReply(p.text);
|
|
171
|
+
},
|
|
172
|
+
}
|
|
173
|
+
: {}),
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
if (streamingController && !streamingController.isTerminal) {
|
|
177
|
+
await streamingController.finalize();
|
|
178
|
+
}
|
|
179
|
+
if (debouncer) await debouncer.flushAll();
|
|
180
|
+
} else {
|
|
181
|
+
await adapters.inboundRun!({
|
|
182
|
+
channel: 'qqbot',
|
|
183
|
+
accountId: route.accountId,
|
|
184
|
+
raw: envelope,
|
|
185
|
+
adapter: {
|
|
186
|
+
ingest: (raw: any) => ({
|
|
187
|
+
id: envelope.messageId,
|
|
188
|
+
rawText: assembled.rawBody,
|
|
189
|
+
textForAgent: assembled.agentBody,
|
|
190
|
+
textForCommands: assembled.rawBody,
|
|
191
|
+
raw,
|
|
192
|
+
}),
|
|
193
|
+
resolveTurn: (_input: unknown, _eventClass: unknown, _preflight: unknown) => ({
|
|
194
|
+
channel: 'qqbot',
|
|
195
|
+
accountId: route.accountId,
|
|
196
|
+
routeSessionKey: route.sessionKey,
|
|
197
|
+
storePath,
|
|
198
|
+
ctxPayload,
|
|
199
|
+
recordInboundSession: adapters.recordInboundSession,
|
|
200
|
+
record: {
|
|
201
|
+
onRecordError: (err: unknown) => {
|
|
202
|
+
dlog?.error(`Session record error: ${err}`);
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
runDispatch: () => {
|
|
206
|
+
return adapters.dispatchReply!({
|
|
207
|
+
ctx: ctxPayload,
|
|
208
|
+
cfg,
|
|
209
|
+
dispatcherOptions: {
|
|
210
|
+
deliver: async (payload: DeliverPayload, info?: DeliverInfo) => {
|
|
211
|
+
try {
|
|
212
|
+
const kind = (info as any)?.kind as string | undefined;
|
|
213
|
+
const text = payload.text?.trim() ?? '';
|
|
214
|
+
const hasMedia = !!(payload.mediaUrl || payload.mediaUrls?.length);
|
|
215
|
+
dlog?.debug(`deliver kind=${kind ?? 'none'} textLen=${text.length} voice=${!!payload.audioAsVoice} media=${hasMedia}`);
|
|
216
|
+
|
|
217
|
+
// ── 1. block: 媒体/语音立即发送,文本留给流式 ──
|
|
218
|
+
if (kind === 'block') {
|
|
219
|
+
if (payload.audioAsVoice) {
|
|
220
|
+
await deliverReply(payload, info, deliverCtx);
|
|
221
|
+
} else {
|
|
222
|
+
await forwardMediaUrls(payload, deliverCtx, deliveredMediaUrls, dlog);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// ── 2. 流式路径:流式已启动且未降级 → 跳过静态发送 ──
|
|
227
|
+
if (streamingController?.hasStarted && !streamingController.shouldFallbackToStatic) {
|
|
228
|
+
if (kind !== 'block') await streamingController.finalize();
|
|
229
|
+
if (!streamingController.shouldFallbackToStatic) return;
|
|
230
|
+
dlog?.warn(`streaming fallback to static`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// ── 3. 文本去重:同文本已发过 → 跳过 ──
|
|
234
|
+
if (kind === 'final' && !hasMedia && text && deliveredTexts.has(text)) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ── 4. tool 媒体:立即转发 ──
|
|
239
|
+
if (kind === 'tool') {
|
|
240
|
+
await forwardMediaUrls(payload, deliverCtx, deliveredMediaUrls, dlog);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
// ── 5. 默认路径:过滤已发媒体 + 发送 ──
|
|
245
|
+
const filteredPayload = filterDeliveredMedia(payload, deliveredMediaUrls);
|
|
246
|
+
await deliverReply(filteredPayload, info, deliverCtx);
|
|
247
|
+
if (text) deliveredTexts.add(text);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
dlog?.error(`deliver error: ${err instanceof Error ? err.message : String(err)}`);
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
replyOptions: {
|
|
254
|
+
abortSignal: ctx.signal,
|
|
255
|
+
runId: envelope.messageId,
|
|
256
|
+
...(streamingController
|
|
257
|
+
? {
|
|
258
|
+
onPartialReply: async (p: { text?: string }) => {
|
|
259
|
+
if (p.text) await streamingController.onPartialReply(p.text);
|
|
260
|
+
},
|
|
261
|
+
}
|
|
262
|
+
: {}),
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
},
|
|
266
|
+
}),
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
dlog?.debug(`inboundRun completed sessionKey=${route.sessionKey}`);
|
|
272
|
+
|
|
273
|
+
// 群消息回复后清空历史缓存(避免下次 @ 时重复组包)
|
|
274
|
+
if (envelope.chatScope === 'group') {
|
|
275
|
+
clearGroupHistory(account.accountId, envelope.groupId ?? envelope.senderId);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (streamingController && !streamingController.isTerminal) {
|
|
279
|
+
await streamingController.finalize();
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (debouncer) {
|
|
283
|
+
await debouncer.flushAll();
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function createStreamingController(
|
|
288
|
+
envelope: ReturnType<typeof buildEnvelope>,
|
|
289
|
+
account: ResolvedQQBotAccount,
|
|
290
|
+
log?: PluginLogger,
|
|
291
|
+
): StreamingController | null {
|
|
292
|
+
const gw = getGateway(account.accountId);
|
|
293
|
+
if (!gw) {
|
|
294
|
+
log?.error(`cannot enable streaming — gateway not running`);
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
return new StreamingController({
|
|
298
|
+
gateway: gw,
|
|
299
|
+
target: {
|
|
300
|
+
scope: 'c2c',
|
|
301
|
+
targetId: envelope.senderId,
|
|
302
|
+
msgId: envelope.messageId,
|
|
303
|
+
},
|
|
304
|
+
accountId: account.accountId,
|
|
305
|
+
replyToId: envelope.messageId,
|
|
306
|
+
log,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ── 辅助函数 ──
|
|
311
|
+
|
|
312
|
+
/** 提取 payload 中的媒体 URL 并逐个发送(去重) */
|
|
313
|
+
async function forwardMediaUrls(
|
|
314
|
+
payload: DeliverPayload,
|
|
315
|
+
ctx: DeliverContext,
|
|
316
|
+
delivered: Set<string>,
|
|
317
|
+
log?: PluginLogger,
|
|
318
|
+
): Promise<void> {
|
|
319
|
+
const urls: string[] = [];
|
|
320
|
+
if (payload.mediaUrls?.length) urls.push(...payload.mediaUrls);
|
|
321
|
+
if (payload.mediaUrl && !urls.includes(payload.mediaUrl)) urls.push(payload.mediaUrl);
|
|
322
|
+
const newUrls = urls.filter((u) => !delivered.has(u));
|
|
323
|
+
for (const url of newUrls) {
|
|
324
|
+
try {
|
|
325
|
+
await sendMedia({
|
|
326
|
+
to: ctx.qualifiedTarget,
|
|
327
|
+
source: url,
|
|
328
|
+
text: '',
|
|
329
|
+
replyToId: ctx.replyToId,
|
|
330
|
+
accountId: ctx.accountId,
|
|
331
|
+
log: ctx.log,
|
|
332
|
+
agentId: ctx.agentId,
|
|
333
|
+
});
|
|
334
|
+
delivered.add(url);
|
|
335
|
+
} catch (err) {
|
|
336
|
+
log?.error(`media forward failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/** 过滤已发送的媒体 URL */
|
|
342
|
+
function filterDeliveredMedia(
|
|
343
|
+
payload: DeliverPayload,
|
|
344
|
+
delivered: Set<string>,
|
|
345
|
+
): DeliverPayload {
|
|
346
|
+
if (delivered.size === 0) return payload;
|
|
347
|
+
return {
|
|
348
|
+
...payload,
|
|
349
|
+
mediaUrl: payload.mediaUrl && !delivered.has(payload.mediaUrl) ? payload.mediaUrl : undefined,
|
|
350
|
+
mediaUrls: payload.mediaUrls?.filter((u) => !delivered.has(u)),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 信封构建器
|
|
3
|
+
*
|
|
4
|
+
* 将 SDK MiddlewareContext 中由各中间件填充的 state 数据
|
|
5
|
+
* 转换为 OpenClaw 框架标准的 InboundMessage 结构。
|
|
6
|
+
*/
|
|
7
|
+
import type { MiddlewareContext, QQBotInboundMessage, HistoryEntry, ResolvedQuote } from '@tencent-connect/qqbot-nodejs';
|
|
8
|
+
import type { ResolvedQQBotAccount } from '../types.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* OpenClaw 入站消息结构(传递给 runtime.channel.dispatch)
|
|
12
|
+
*/
|
|
13
|
+
export interface OpenClawInboundMessage {
|
|
14
|
+
channelId: string;
|
|
15
|
+
accountId: string;
|
|
16
|
+
targetId: string;
|
|
17
|
+
chatScope: 'direct' | 'group';
|
|
18
|
+
senderId: string;
|
|
19
|
+
senderName?: string;
|
|
20
|
+
messageId: string;
|
|
21
|
+
content: string;
|
|
22
|
+
history?: Array<{ role: string; content: string; senderId?: string; senderName?: string }>;
|
|
23
|
+
quote?: { content: string; senderId: string; attachments?: unknown[] };
|
|
24
|
+
attachments?: unknown[];
|
|
25
|
+
/** 入站图片 URL 列表(从附件中提取) */
|
|
26
|
+
imageUrls?: string[];
|
|
27
|
+
groupId?: string;
|
|
28
|
+
systemPrompt?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* 将 HistoryEntry 转换为 OpenClaw 历史格式
|
|
33
|
+
*/
|
|
34
|
+
function mapHistory(
|
|
35
|
+
entries: HistoryEntry[] | undefined,
|
|
36
|
+
): OpenClawInboundMessage['history'] {
|
|
37
|
+
if (!entries || entries.length === 0) return undefined;
|
|
38
|
+
return entries.map((e) => ({
|
|
39
|
+
role: 'user',
|
|
40
|
+
content: e.content,
|
|
41
|
+
senderId: e.senderId,
|
|
42
|
+
senderName: e.senderName,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 将 ResolvedQuote 转换为 OpenClaw quote 格式
|
|
48
|
+
*/
|
|
49
|
+
function mapQuote(
|
|
50
|
+
quote: ResolvedQuote | undefined,
|
|
51
|
+
): OpenClawInboundMessage['quote'] {
|
|
52
|
+
if (!quote) return undefined;
|
|
53
|
+
return {
|
|
54
|
+
content: quote.text,
|
|
55
|
+
senderId: quote.entry?.senderId ?? '',
|
|
56
|
+
attachments: quote.attachments as unknown[],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* 从 SDK 中间件上下文构建 OpenClaw InboundMessage
|
|
62
|
+
*/
|
|
63
|
+
export function buildEnvelope(
|
|
64
|
+
ctx: MiddlewareContext,
|
|
65
|
+
msg: QQBotInboundMessage,
|
|
66
|
+
account: ResolvedQQBotAccount,
|
|
67
|
+
): OpenClawInboundMessage {
|
|
68
|
+
const scope = msg.replyTarget.scope;
|
|
69
|
+
const targetId = scope === 'group'
|
|
70
|
+
? `qqbot:group:${msg.replyTarget.targetId}`
|
|
71
|
+
: `qqbot:c2c:${msg.replyTarget.targetId}`;
|
|
72
|
+
|
|
73
|
+
// envelope 是 string 类型,直接作为 content 使用;回退到原始消息内容
|
|
74
|
+
const envelope = ctx.state.envelope as string | undefined;
|
|
75
|
+
let content = envelope ?? msg.content;
|
|
76
|
+
|
|
77
|
+
// 将语音转录文本和附件信息注入到 content 中
|
|
78
|
+
const processed = ctx.state.processedAttachments as
|
|
79
|
+
| { voiceText?: string; imageUrls?: string[]; otherInfo?: string }
|
|
80
|
+
| undefined;
|
|
81
|
+
|
|
82
|
+
if (processed) {
|
|
83
|
+
const parts: string[] = [];
|
|
84
|
+
if (processed.voiceText) {
|
|
85
|
+
parts.push(processed.voiceText);
|
|
86
|
+
}
|
|
87
|
+
if (content) {
|
|
88
|
+
parts.push(content);
|
|
89
|
+
}
|
|
90
|
+
if (processed.otherInfo) {
|
|
91
|
+
parts.push(processed.otherInfo);
|
|
92
|
+
}
|
|
93
|
+
content = parts.join('\n');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
channelId: 'qqbot',
|
|
98
|
+
accountId: account.accountId,
|
|
99
|
+
targetId,
|
|
100
|
+
chatScope: scope === 'group' ? 'group' : 'direct',
|
|
101
|
+
senderId: msg.senderId,
|
|
102
|
+
senderName: msg.senderName,
|
|
103
|
+
messageId: msg.messageId,
|
|
104
|
+
content,
|
|
105
|
+
history: mapHistory(ctx.state.history as HistoryEntry[] | undefined),
|
|
106
|
+
quote: mapQuote(ctx.state.quote as ResolvedQuote | undefined),
|
|
107
|
+
attachments: msg.attachments,
|
|
108
|
+
imageUrls: processed?.imageUrls,
|
|
109
|
+
groupId: scope === 'group' ? msg.replyTarget.targetId : undefined,
|
|
110
|
+
systemPrompt: account.systemPrompt,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
@@ -10,75 +10,11 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { OpenClawConfig } from "openclaw/plugin-sdk";
|
|
13
|
-
import {
|
|
14
|
-
import
|
|
15
|
-
import {
|
|
16
|
-
import {
|
|
17
|
-
getAccessToken,
|
|
18
|
-
sendC2CMessageWithInlineKeyboard,
|
|
19
|
-
sendGroupMessageWithInlineKeyboard,
|
|
20
|
-
} from "./api.js";
|
|
21
|
-
import type { InlineKeyboard, KeyboardButton } from "./types.js";
|
|
22
|
-
|
|
23
|
-
// ─── 动态加载 gateway-runtime(兼容不同安装环境) ────────
|
|
24
|
-
|
|
25
|
-
function loadGatewayRuntime(): { createOperatorApprovalsGatewayClient: (...args: any[]) => Promise<GatewayClient> } {
|
|
26
|
-
const req = createRequire(import.meta.url);
|
|
27
|
-
const currentFile = fileURLToPath(import.meta.url);
|
|
28
|
-
const pluginRoot = path.resolve(path.dirname(currentFile), "..", "..");
|
|
29
|
-
const fs = req("node:fs") as typeof import("node:fs");
|
|
30
|
-
|
|
31
|
-
// 尝试从找到的 openclaw 根目录加载 gateway-runtime.js
|
|
32
|
-
const tryLoadFromRoot = (root: string) => {
|
|
33
|
-
for (const rel of ["dist/plugin-sdk/gateway-runtime.js", "plugin-sdk/gateway-runtime.js"]) {
|
|
34
|
-
const p = path.join(root, rel);
|
|
35
|
-
try {
|
|
36
|
-
if (fs.existsSync(p)) return req(p);
|
|
37
|
-
} catch { /* try next */ }
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
// 策略 1: link-sdk-core.cjs findOpenclawRoot
|
|
43
|
-
try {
|
|
44
|
-
const { findOpenclawRoot } = req(path.join(pluginRoot, "scripts", "link-sdk-core.cjs")) as {
|
|
45
|
-
findOpenclawRoot: (root: string) => string | null;
|
|
46
|
-
};
|
|
47
|
-
const root = findOpenclawRoot(pluginRoot);
|
|
48
|
-
if (root) {
|
|
49
|
-
const mod = tryLoadFromRoot(root);
|
|
50
|
-
if (mod) return mod;
|
|
51
|
-
}
|
|
52
|
-
} catch { /* fallback */ }
|
|
53
|
-
|
|
54
|
-
// 策略 2: process.argv[1] 反推(当前进程就是 openclaw)
|
|
55
|
-
try {
|
|
56
|
-
const entry = process.argv[1];
|
|
57
|
-
if (entry) {
|
|
58
|
-
const realEntry = fs.realpathSync(entry);
|
|
59
|
-
let dir = path.dirname(realEntry);
|
|
60
|
-
for (let i = 0; i < 6; i++) {
|
|
61
|
-
const mod = tryLoadFromRoot(dir);
|
|
62
|
-
if (mod) return mod;
|
|
63
|
-
const parent = path.dirname(dir);
|
|
64
|
-
if (parent === dir) break;
|
|
65
|
-
dir = parent;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
} catch { /* fallback */ }
|
|
69
|
-
|
|
70
|
-
throw new Error("Cannot find openclaw/plugin-sdk/gateway-runtime (all strategies failed)");
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ─── 动态加载的模块类型(兼容旧版框架) ───────────────────────
|
|
74
|
-
|
|
75
|
-
/** gateway-runtime 模块的接口(动态 import) */
|
|
76
|
-
type GatewayClient = {
|
|
77
|
-
start: () => void | Promise<void>;
|
|
78
|
-
stop: () => void | Promise<void>;
|
|
79
|
-
request: (method: string, params: unknown) => Promise<unknown>;
|
|
80
|
-
};
|
|
13
|
+
import { getBotForAccount } from "../bot-instance.js";
|
|
14
|
+
import type { PluginLogger } from '../utils/plugin-logger.js';
|
|
15
|
+
import type { InlineKeyboard, KeyboardButton } from "../types.js";
|
|
81
16
|
|
|
17
|
+
import { loadApprovalGatewayRuntime, type ApprovalGatewayClient } from '../adapter/gateway.js';
|
|
82
18
|
type EventFrame = {
|
|
83
19
|
event: string;
|
|
84
20
|
payload: unknown;
|
|
@@ -139,11 +75,7 @@ export interface QQBotApprovalHandlerOpts {
|
|
|
139
75
|
clientSecret: string;
|
|
140
76
|
cfg: OpenClawConfig;
|
|
141
77
|
gatewayUrl?: string;
|
|
142
|
-
log?:
|
|
143
|
-
info: (msg: string) => void;
|
|
144
|
-
error: (msg: string) => void;
|
|
145
|
-
debug?: (msg: string) => void;
|
|
146
|
-
};
|
|
78
|
+
log?: PluginLogger;
|
|
147
79
|
}
|
|
148
80
|
|
|
149
81
|
type ApprovalKind = "exec" | "plugin";
|
|
@@ -255,11 +187,12 @@ function resolveTarget(
|
|
|
255
187
|
// ─── Handler 类 ──────────────────────────────────────────────
|
|
256
188
|
|
|
257
189
|
export class QQBotApprovalHandler {
|
|
258
|
-
private gatewayClient:
|
|
190
|
+
private gatewayClient: ApprovalGatewayClient | null = null;
|
|
259
191
|
private pending = new Map<string, PendingEntry>();
|
|
260
192
|
private requestCache = new Map<string, CachedApprovalRequest>();
|
|
261
193
|
private opts: QQBotApprovalHandlerOpts;
|
|
262
194
|
private started = false;
|
|
195
|
+
private connected = false;
|
|
263
196
|
|
|
264
197
|
constructor(opts: QQBotApprovalHandlerOpts) {
|
|
265
198
|
this.opts = opts;
|
|
@@ -269,14 +202,12 @@ export class QQBotApprovalHandler {
|
|
|
269
202
|
if (this.started) return;
|
|
270
203
|
this.started = true;
|
|
271
204
|
const { log } = this.opts;
|
|
272
|
-
log?.
|
|
205
|
+
log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: starting`);
|
|
273
206
|
|
|
274
207
|
// 动态加载 gateway-runtime(兼容旧版框架 / pnpm 环境)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
} catch (err) {
|
|
279
|
-
log?.error(`[qqbot:${this.opts.accountId}] approval-handler: gateway-runtime module not available, approval feature disabled. Error: ${err}`);
|
|
208
|
+
const gatewayRuntime = loadApprovalGatewayRuntime();
|
|
209
|
+
if (!gatewayRuntime) {
|
|
210
|
+
log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: gateway-runtime not available, approval disabled`);
|
|
280
211
|
this.started = false;
|
|
281
212
|
return;
|
|
282
213
|
}
|
|
@@ -287,9 +218,15 @@ export class QQBotApprovalHandler {
|
|
|
287
218
|
gatewayUrl: this.opts.gatewayUrl,
|
|
288
219
|
clientDisplayName: "QQBot Approval Handler",
|
|
289
220
|
onEvent: (evt: EventFrame) => this.handleGatewayEvent(evt),
|
|
290
|
-
onHelloOk: () =>
|
|
221
|
+
onHelloOk: () => {
|
|
222
|
+
this.connected = true;
|
|
223
|
+
log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: connected to gateway`);
|
|
224
|
+
},
|
|
291
225
|
onConnectError: (err: { message: string }) => log?.error(`[qqbot:${this.opts.accountId}] approval-handler: connect error: ${err.message}`),
|
|
292
|
-
onClose: (code: number, reason: string) =>
|
|
226
|
+
onClose: (code: number, reason: string) => {
|
|
227
|
+
this.connected = false;
|
|
228
|
+
log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: gateway closed: ${code} ${reason}`);
|
|
229
|
+
},
|
|
293
230
|
});
|
|
294
231
|
this.gatewayClient.start();
|
|
295
232
|
setApprovalFeatureAvailable(true);
|
|
@@ -305,11 +242,14 @@ export class QQBotApprovalHandler {
|
|
|
305
242
|
for (const entry of this.pending.values()) clearTimeout(entry.timeoutId);
|
|
306
243
|
this.pending.clear();
|
|
307
244
|
this.requestCache.clear();
|
|
308
|
-
this.gatewayClient?.stop();
|
|
245
|
+
await this.gatewayClient?.stop();
|
|
309
246
|
this.gatewayClient = null;
|
|
310
|
-
this.opts.log?.
|
|
247
|
+
this.opts.log?.debug(`[qqbot:${this.opts.accountId}] approval-handler: stopped`);
|
|
311
248
|
}
|
|
312
249
|
|
|
250
|
+
/** gateway 是否已建立连接 */
|
|
251
|
+
get isConnected(): boolean { return this.connected; }
|
|
252
|
+
|
|
313
253
|
/** 检查是否有指定 shortId 对应的 pending 审批 */
|
|
314
254
|
hasShortId(shortId: string): boolean {
|
|
315
255
|
for (const id of this.pending.keys()) {
|
|
@@ -323,7 +263,10 @@ export class QQBotApprovalHandler {
|
|
|
323
263
|
approvalId: string,
|
|
324
264
|
decision: "allow-once" | "allow-always" | "deny"
|
|
325
265
|
): Promise<boolean> {
|
|
326
|
-
if (!this.gatewayClient)
|
|
266
|
+
if (!this.gatewayClient) {
|
|
267
|
+
this.opts.log?.warn(`[qqbot:${this.opts.accountId}] approval-handler: resolve ignored ${approvalId} → gatewayClient not ready`);
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
327
270
|
|
|
328
271
|
// 查找完整 ID:支持完整 ID(exec:uuid / plugin:uuid)、纯 UUID、或 shortId(8位)
|
|
329
272
|
let fullId = approvalId;
|
|
@@ -351,11 +294,11 @@ export class QQBotApprovalHandler {
|
|
|
351
294
|
const isPending = this.pending.has(fullId);
|
|
352
295
|
const isCached = this.requestCache.has(fullId);
|
|
353
296
|
|
|
354
|
-
this.opts.log?.
|
|
297
|
+
this.opts.log?.debug?.(`[qqbot:${this.opts.accountId}] approval-handler: resolving ${fullId} (input=${approvalId}) kind=${kind} → ${decision}, pending=${isPending}, cached=${isCached}`);
|
|
355
298
|
|
|
356
299
|
try {
|
|
357
300
|
await this.gatewayClient.request(method, { id: fullId, decision });
|
|
358
|
-
this.opts.log?.
|
|
301
|
+
this.opts.log?.debug(`[qqbot:${this.opts.accountId}] approval-handler: RPC success ${toShortId(fullId)} → ${decision} (method=${method})`);
|
|
359
302
|
return true;
|
|
360
303
|
} catch (err) {
|
|
361
304
|
this.opts.log?.error(`[qqbot:${this.opts.accountId}] approval-handler: resolve failed: ${err}`);
|
|
@@ -384,7 +327,10 @@ export class QQBotApprovalHandler {
|
|
|
384
327
|
|
|
385
328
|
// 只处理本账号的请求
|
|
386
329
|
const reqAccountId = (request.request as any).turnSourceAccountId?.trim();
|
|
387
|
-
if (reqAccountId && reqAccountId !== accountId)
|
|
330
|
+
if (reqAccountId && reqAccountId !== accountId) {
|
|
331
|
+
log?.debug?.(`[qqbot:${accountId}] approval-handler: ${kind} ${shortId} ignored → account mismatch (req=${reqAccountId})`);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
388
334
|
|
|
389
335
|
// 解析投递目标
|
|
390
336
|
const sessionKey = (request.request as any).sessionKey;
|
|
@@ -419,12 +365,12 @@ export class QQBotApprovalHandler {
|
|
|
419
365
|
await new Promise((r) => setTimeout(r, 2000));
|
|
420
366
|
|
|
421
367
|
try {
|
|
422
|
-
const
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
368
|
+
const bot = getBotForAccount(accountId);
|
|
369
|
+
const target_reply = {
|
|
370
|
+
scope: target.type as "c2c" | "group",
|
|
371
|
+
targetId: target.id,
|
|
372
|
+
};
|
|
373
|
+
await bot.sendTextWithKeyboard(target_reply, text, keyboard as any);
|
|
428
374
|
log?.info(`[qqbot:${accountId}] approval-handler: sent ${kind} approval ${shortId}`);
|
|
429
375
|
|
|
430
376
|
const timeoutId = setTimeout(() => {
|