@openclaw/qqbot 2026.5.2 → 2026.5.3-beta.1
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/dist/api.js +646 -0
- package/dist/approval-cg0SVahb.js +94 -0
- package/dist/channel-N6Y_Rcjp.js +551 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.setup-ByCRAe5H.js +25 -0
- package/dist/config-D6545NkC.js +94 -0
- package/dist/config-schema-DFcjQw73.js +309 -0
- package/dist/exec-approvals-COUsM6wZ.js +138 -0
- package/dist/gateway-CmSUJKSt.js +6726 -0
- package/dist/handler-runtime-Bqm6N0WG.js +120 -0
- package/dist/index.js +29 -0
- package/dist/narrowing-BoieBTIU.js +25 -0
- package/dist/outbound-BJfhwrPg.js +1357 -0
- package/dist/request-context-DXtpwWui.js +921 -0
- package/dist/resolve-D_06fV6-.js +284 -0
- package/dist/runtime-BJAS3eXW.js +18 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-api.js +56 -0
- package/dist/sender-p-B14eLG.js +1987 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +2 -0
- package/dist/string-normalize-Ci6NM5DE.js +98 -0
- package/dist/target-parser-Y0prnrXD.js +245 -0
- package/package.json +15 -6
- package/api.ts +0 -56
- package/channel-plugin-api.ts +0 -1
- package/index.ts +0 -29
- package/runtime-api.ts +0 -9
- package/setup-entry.ts +0 -9
- package/setup-plugin-api.ts +0 -3
- package/src/bridge/approval/capability.ts +0 -237
- package/src/bridge/approval/handler-runtime.ts +0 -204
- package/src/bridge/bootstrap.ts +0 -135
- package/src/bridge/channel-entry.ts +0 -18
- package/src/bridge/commands/framework-context-adapter.ts +0 -60
- package/src/bridge/commands/framework-registration.ts +0 -47
- package/src/bridge/commands/from-parser.test.ts +0 -86
- package/src/bridge/commands/from-parser.ts +0 -60
- package/src/bridge/commands/result-dispatcher.ts +0 -76
- package/src/bridge/config-shared.ts +0 -132
- package/src/bridge/config.ts +0 -111
- package/src/bridge/gateway.ts +0 -174
- package/src/bridge/logger.ts +0 -31
- package/src/bridge/narrowing.ts +0 -31
- package/src/bridge/plugin-version.test.ts +0 -146
- package/src/bridge/plugin-version.ts +0 -102
- package/src/bridge/runtime.ts +0 -25
- package/src/bridge/sdk-adapter.ts +0 -131
- package/src/bridge/setup/finalize.ts +0 -144
- package/src/bridge/setup/surface.ts +0 -34
- package/src/bridge/tools/channel.ts +0 -58
- package/src/bridge/tools/index.ts +0 -15
- package/src/bridge/tools/remind.test.ts +0 -124
- package/src/bridge/tools/remind.ts +0 -91
- package/src/channel.setup.ts +0 -33
- package/src/channel.ts +0 -288
- package/src/command-auth.test.ts +0 -62
- package/src/config-schema.ts +0 -84
- package/src/config.test.ts +0 -364
- package/src/engine/access/access-control.test.ts +0 -198
- package/src/engine/access/access-control.ts +0 -226
- package/src/engine/access/index.ts +0 -16
- package/src/engine/access/resolve-policy.test.ts +0 -59
- package/src/engine/access/resolve-policy.ts +0 -57
- package/src/engine/access/sender-match.test.ts +0 -60
- package/src/engine/access/sender-match.ts +0 -55
- package/src/engine/access/types.ts +0 -53
- package/src/engine/adapter/audio.port.ts +0 -27
- package/src/engine/adapter/commands.port.ts +0 -22
- package/src/engine/adapter/history.port.ts +0 -52
- package/src/engine/adapter/index.ts +0 -139
- package/src/engine/adapter/mention-gate.port.ts +0 -50
- package/src/engine/adapter/types.ts +0 -38
- package/src/engine/api/api-client.ts +0 -212
- package/src/engine/api/media-chunked.test.ts +0 -336
- package/src/engine/api/media-chunked.ts +0 -622
- package/src/engine/api/media.ts +0 -218
- package/src/engine/api/messages.ts +0 -293
- package/src/engine/api/retry.ts +0 -217
- package/src/engine/api/routes.ts +0 -95
- package/src/engine/api/token.ts +0 -271
- package/src/engine/approval/index.test.ts +0 -22
- package/src/engine/approval/index.ts +0 -224
- package/src/engine/commands/builtin/log-helpers.ts +0 -319
- package/src/engine/commands/builtin/register-all.ts +0 -17
- package/src/engine/commands/builtin/register-approve.ts +0 -201
- package/src/engine/commands/builtin/register-basic.ts +0 -95
- package/src/engine/commands/builtin/register-clear-storage.ts +0 -187
- package/src/engine/commands/builtin/register-logs.ts +0 -20
- package/src/engine/commands/builtin/register-streaming.ts +0 -137
- package/src/engine/commands/builtin/state.ts +0 -31
- package/src/engine/commands/slash-command-auth.ts +0 -48
- package/src/engine/commands/slash-command-handler.ts +0 -146
- package/src/engine/commands/slash-commands-impl.test.ts +0 -8
- package/src/engine/commands/slash-commands-impl.ts +0 -61
- package/src/engine/commands/slash-commands.ts +0 -199
- package/src/engine/config/credential-backup.test.ts +0 -88
- package/src/engine/config/credential-backup.ts +0 -107
- package/src/engine/config/credentials.ts +0 -76
- package/src/engine/config/group.test.ts +0 -234
- package/src/engine/config/group.ts +0 -299
- package/src/engine/config/resolve.test.ts +0 -152
- package/src/engine/config/resolve.ts +0 -283
- package/src/engine/config/setup-logic.ts +0 -84
- package/src/engine/engine-import-boundary.test.ts +0 -73
- package/src/engine/gateway/codec.ts +0 -47
- package/src/engine/gateway/constants.ts +0 -117
- package/src/engine/gateway/event-dispatcher.ts +0 -177
- package/src/engine/gateway/gateway-connection.ts +0 -371
- package/src/engine/gateway/gateway.ts +0 -291
- package/src/engine/gateway/inbound-attachments.test.ts +0 -126
- package/src/engine/gateway/inbound-attachments.ts +0 -360
- package/src/engine/gateway/inbound-context.ts +0 -195
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +0 -218
- package/src/engine/gateway/inbound-pipeline.ts +0 -235
- package/src/engine/gateway/interaction-handler.ts +0 -220
- package/src/engine/gateway/message-queue.test.ts +0 -282
- package/src/engine/gateway/message-queue.ts +0 -499
- package/src/engine/gateway/outbound-dispatch.test.ts +0 -231
- package/src/engine/gateway/outbound-dispatch.ts +0 -575
- package/src/engine/gateway/reconnect.ts +0 -199
- package/src/engine/gateway/stages/access-stage.ts +0 -132
- package/src/engine/gateway/stages/assembly-stage.ts +0 -156
- package/src/engine/gateway/stages/content-stage.test.ts +0 -77
- package/src/engine/gateway/stages/content-stage.ts +0 -77
- package/src/engine/gateway/stages/envelope-stage.test.ts +0 -152
- package/src/engine/gateway/stages/envelope-stage.ts +0 -144
- package/src/engine/gateway/stages/group-gate-stage.ts +0 -292
- package/src/engine/gateway/stages/index.ts +0 -18
- package/src/engine/gateway/stages/quote-stage.ts +0 -113
- package/src/engine/gateway/stages/refidx-stage.ts +0 -62
- package/src/engine/gateway/stages/stub-contexts.ts +0 -116
- package/src/engine/gateway/types.ts +0 -264
- package/src/engine/gateway/typing-keepalive.ts +0 -79
- package/src/engine/group/activation.test.ts +0 -114
- package/src/engine/group/activation.ts +0 -147
- package/src/engine/group/history.test.ts +0 -314
- package/src/engine/group/history.ts +0 -321
- package/src/engine/group/mention.test.ts +0 -141
- package/src/engine/group/mention.ts +0 -197
- package/src/engine/group/message-gating.test.ts +0 -188
- package/src/engine/group/message-gating.ts +0 -216
- package/src/engine/messaging/decode-media-path.ts +0 -82
- package/src/engine/messaging/media-source.ts +0 -215
- package/src/engine/messaging/media-type-detect.ts +0 -37
- package/src/engine/messaging/outbound-audio-port.ts +0 -38
- package/src/engine/messaging/outbound-deliver.ts +0 -810
- package/src/engine/messaging/outbound-media-send.ts +0 -702
- package/src/engine/messaging/outbound-reply.ts +0 -27
- package/src/engine/messaging/outbound-result-helpers.ts +0 -54
- package/src/engine/messaging/outbound-types.ts +0 -45
- package/src/engine/messaging/outbound.ts +0 -485
- package/src/engine/messaging/reply-dispatcher.ts +0 -597
- package/src/engine/messaging/reply-limiter.ts +0 -164
- package/src/engine/messaging/sender.ts +0 -729
- package/src/engine/messaging/streaming-c2c.ts +0 -1192
- package/src/engine/messaging/streaming-media-send.ts +0 -544
- package/src/engine/messaging/target-parser.ts +0 -104
- package/src/engine/ref/format-message-ref.ts +0 -142
- package/src/engine/ref/format-ref-entry.test.ts +0 -60
- package/src/engine/ref/format-ref-entry.ts +0 -27
- package/src/engine/ref/store.ts +0 -208
- package/src/engine/ref/types.ts +0 -27
- package/src/engine/session/known-users.ts +0 -137
- package/src/engine/session/session-store.ts +0 -204
- package/src/engine/tools/channel-api.ts +0 -244
- package/src/engine/tools/remind-logic.test.ts +0 -280
- package/src/engine/tools/remind-logic.ts +0 -377
- package/src/engine/types.ts +0 -313
- package/src/engine/utils/attachment-tags.test.ts +0 -186
- package/src/engine/utils/attachment-tags.ts +0 -174
- package/src/engine/utils/audio.test.ts +0 -250
- package/src/engine/utils/audio.ts +0 -585
- package/src/engine/utils/data-paths.ts +0 -38
- package/src/engine/utils/diagnostics.ts +0 -109
- package/src/engine/utils/file-utils.test.ts +0 -72
- package/src/engine/utils/file-utils.ts +0 -225
- package/src/engine/utils/format.test.ts +0 -68
- package/src/engine/utils/format.ts +0 -70
- package/src/engine/utils/image-size.test.ts +0 -158
- package/src/engine/utils/image-size.ts +0 -249
- package/src/engine/utils/log.test.ts +0 -28
- package/src/engine/utils/log.ts +0 -61
- package/src/engine/utils/media-tags.test.ts +0 -32
- package/src/engine/utils/media-tags.ts +0 -177
- package/src/engine/utils/payload.test.ts +0 -68
- package/src/engine/utils/payload.ts +0 -145
- package/src/engine/utils/platform-storage-laziness.test.ts +0 -63
- package/src/engine/utils/platform.test.ts +0 -148
- package/src/engine/utils/platform.ts +0 -343
- package/src/engine/utils/request-context.ts +0 -60
- package/src/engine/utils/string-normalize.ts +0 -91
- package/src/engine/utils/stt.test.ts +0 -104
- package/src/engine/utils/stt.ts +0 -100
- package/src/engine/utils/text-parsing.test.ts +0 -29
- package/src/engine/utils/text-parsing.ts +0 -155
- package/src/engine/utils/upload-cache.ts +0 -96
- package/src/engine/utils/voice-text.ts +0 -15
- package/src/exec-approvals.ts +0 -218
- package/src/manifest-schema.test.ts +0 -56
- package/src/qqbot-test-support.ts +0 -29
- package/src/types.ts +0 -210
- package/tsconfig.json +0 -16
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Core gateway entry point — thin shell that wires together:
|
|
3
|
-
*
|
|
4
|
-
* - GatewayConnection: WebSocket lifecycle, heartbeat, reconnect
|
|
5
|
-
* - buildInboundContext: content building, attachments, quote resolution
|
|
6
|
-
* - dispatchOutbound: AI dispatch, deliver callbacks, timeouts
|
|
7
|
-
*
|
|
8
|
-
* The only responsibilities of this file are:
|
|
9
|
-
* 1. Initialize adapters from EngineAdapters
|
|
10
|
-
* 2. Initialize API config + refIdx cache hook
|
|
11
|
-
* 3. Create the message handler (inbound → outbound pipeline)
|
|
12
|
-
* 4. Start GatewayConnection
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import path from "node:path";
|
|
16
|
-
import { initCommands } from "../commands/slash-commands-impl.js";
|
|
17
|
-
import { createNodeSessionStoreReader } from "../group/activation.js";
|
|
18
|
-
import type { HistoryEntry } from "../group/history.js";
|
|
19
|
-
import { setOutboundAudioPort } from "../messaging/outbound.js";
|
|
20
|
-
import {
|
|
21
|
-
clearTokenCache,
|
|
22
|
-
getAccessToken,
|
|
23
|
-
initApiConfig,
|
|
24
|
-
onMessageSent,
|
|
25
|
-
sendInputNotify as senderSendInputNotify,
|
|
26
|
-
createRawInputNotifyFn,
|
|
27
|
-
accountToCreds,
|
|
28
|
-
} from "../messaging/sender.js";
|
|
29
|
-
import { setRefIndex } from "../ref/store.js";
|
|
30
|
-
import { runDiagnostics } from "../utils/diagnostics.js";
|
|
31
|
-
import { runWithRequestContext } from "../utils/request-context.js";
|
|
32
|
-
import { GatewayConnection } from "./gateway-connection.js";
|
|
33
|
-
import { buildInboundContext, clearGroupPendingHistory } from "./inbound-pipeline.js";
|
|
34
|
-
import { createInteractionHandler } from "./interaction-handler.js";
|
|
35
|
-
import type { QueuedMessage } from "./message-queue.js";
|
|
36
|
-
import { dispatchOutbound } from "./outbound-dispatch.js";
|
|
37
|
-
import type {
|
|
38
|
-
CoreGatewayContext,
|
|
39
|
-
GatewayAccount,
|
|
40
|
-
EngineLogger,
|
|
41
|
-
RefAttachmentSummary,
|
|
42
|
-
} from "./types.js";
|
|
43
|
-
import { TypingKeepAlive, TYPING_INPUT_SECOND } from "./typing-keepalive.js";
|
|
44
|
-
|
|
45
|
-
// Re-export context type for consumers.
|
|
46
|
-
export type { CoreGatewayContext } from "./types.js";
|
|
47
|
-
|
|
48
|
-
// ============ startGateway ============
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Start the Gateway WebSocket connection with automatic reconnect support.
|
|
52
|
-
*/
|
|
53
|
-
export async function startGateway(ctx: CoreGatewayContext): Promise<void> {
|
|
54
|
-
const { account, log, runtime, adapters } = ctx;
|
|
55
|
-
|
|
56
|
-
// ---- 1. Initialize adapters ----
|
|
57
|
-
setOutboundAudioPort(adapters.outboundAudio);
|
|
58
|
-
initCommands(adapters.commands);
|
|
59
|
-
|
|
60
|
-
// ---- 2. Validate ----
|
|
61
|
-
if (!account.appId || !account.clientSecret) {
|
|
62
|
-
throw new Error("QQBot not configured (missing appId or clientSecret)");
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ---- 3. Diagnostics ----
|
|
66
|
-
const diag = await runDiagnostics();
|
|
67
|
-
if (diag.warnings.length > 0) {
|
|
68
|
-
for (const w of diag.warnings) {
|
|
69
|
-
log?.info(w);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// ---- 4. API config ----
|
|
74
|
-
initApiConfig(account.appId, { markdownSupport: account.markdownSupport });
|
|
75
|
-
log?.debug?.(`API config: markdownSupport=${account.markdownSupport}`);
|
|
76
|
-
|
|
77
|
-
// ---- 5. Outbound refIdx cache hook ----
|
|
78
|
-
onMessageSent(account.appId, (refIdx, meta) => {
|
|
79
|
-
log?.info(
|
|
80
|
-
`onMessageSent called: refIdx=${refIdx}, mediaType=${meta.mediaType}, ttsText=${meta.ttsText?.slice(0, 30)}`,
|
|
81
|
-
);
|
|
82
|
-
const attachments: RefAttachmentSummary[] = [];
|
|
83
|
-
if (meta.mediaType) {
|
|
84
|
-
const localPath = meta.mediaLocalPath;
|
|
85
|
-
const filename = localPath ? path.basename(localPath) : undefined;
|
|
86
|
-
const attachment: RefAttachmentSummary = {
|
|
87
|
-
type: meta.mediaType,
|
|
88
|
-
...(localPath ? { localPath } : {}),
|
|
89
|
-
...(filename ? { filename } : {}),
|
|
90
|
-
...(meta.mediaUrl ? { url: meta.mediaUrl } : {}),
|
|
91
|
-
};
|
|
92
|
-
if (meta.mediaType === "voice" && meta.ttsText) {
|
|
93
|
-
attachment.transcript = meta.ttsText;
|
|
94
|
-
attachment.transcriptSource = "tts";
|
|
95
|
-
}
|
|
96
|
-
attachments.push(attachment);
|
|
97
|
-
}
|
|
98
|
-
setRefIndex(refIdx, {
|
|
99
|
-
content: meta.text ?? "",
|
|
100
|
-
senderId: account.accountId,
|
|
101
|
-
senderName: account.accountId,
|
|
102
|
-
timestamp: Date.now(),
|
|
103
|
-
isBot: true,
|
|
104
|
-
...(attachments.length > 0 ? { attachments } : {}),
|
|
105
|
-
});
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
// ---- 6. Group support (per-connection state) ----
|
|
109
|
-
const groupOpts = {
|
|
110
|
-
enabled: ctx.group?.enabled ?? true,
|
|
111
|
-
allowTextCommands: ctx.group?.allowTextCommands,
|
|
112
|
-
isControlCommand: ctx.group?.isControlCommand,
|
|
113
|
-
resolveIntroHint: ctx.group?.resolveIntroHint,
|
|
114
|
-
sessionStoreReader: ctx.group?.sessionStoreReader,
|
|
115
|
-
};
|
|
116
|
-
const groupChatEnabled = groupOpts.enabled;
|
|
117
|
-
const groupHistories: Map<string, HistoryEntry[]> | undefined = groupChatEnabled
|
|
118
|
-
? new Map()
|
|
119
|
-
: undefined;
|
|
120
|
-
const sessionStoreReader = groupChatEnabled
|
|
121
|
-
? (groupOpts.sessionStoreReader ?? createNodeSessionStoreReader())
|
|
122
|
-
: undefined;
|
|
123
|
-
|
|
124
|
-
// ---- 7. Message handler ----
|
|
125
|
-
const handleMessage = async (event: QueuedMessage): Promise<void> => {
|
|
126
|
-
log?.info(`Processing message from ${event.senderId}: ${event.content}`, {
|
|
127
|
-
accountId: account.accountId,
|
|
128
|
-
messageId: event.messageId,
|
|
129
|
-
senderId: event.senderId,
|
|
130
|
-
type: event.type,
|
|
131
|
-
groupOpenid: event.groupOpenid,
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
runtime.channel.activity.record({
|
|
135
|
-
channel: "qqbot",
|
|
136
|
-
accountId: account.accountId,
|
|
137
|
-
direction: "inbound",
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
const inbound = await buildInboundContext(event, {
|
|
141
|
-
account,
|
|
142
|
-
cfg: ctx.cfg,
|
|
143
|
-
log,
|
|
144
|
-
runtime,
|
|
145
|
-
startTyping: (ev) => startTypingForEvent(ev, account, log),
|
|
146
|
-
groupHistories,
|
|
147
|
-
sessionStoreReader,
|
|
148
|
-
allowTextCommands: groupOpts.allowTextCommands,
|
|
149
|
-
isControlCommand: groupOpts.isControlCommand,
|
|
150
|
-
resolveGroupIntroHint: groupOpts.resolveIntroHint,
|
|
151
|
-
adapters,
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
if (inbound.blocked) {
|
|
155
|
-
log?.info(`Dropped inbound qqbot message: ${inbound.blockReason ?? "blocked by allowFrom"}`, {
|
|
156
|
-
accountId: account.accountId,
|
|
157
|
-
messageId: event.messageId,
|
|
158
|
-
blockReason: inbound.blockReason,
|
|
159
|
-
});
|
|
160
|
-
inbound.typing.keepAlive?.stop();
|
|
161
|
-
return;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// Group gate decided to stop early (drop_other_mention, block, skip
|
|
165
|
-
// no-mention). History has already been recorded inside the
|
|
166
|
-
// pipeline; there is no outbound to dispatch.
|
|
167
|
-
if (inbound.skipped) {
|
|
168
|
-
log?.info(
|
|
169
|
-
`Skipped group inbound: reason=${inbound.skipReason ?? "unknown"} group=${event.groupOpenid ?? ""}`,
|
|
170
|
-
{
|
|
171
|
-
accountId: account.accountId,
|
|
172
|
-
messageId: event.messageId,
|
|
173
|
-
skipReason: inbound.skipReason,
|
|
174
|
-
groupOpenid: event.groupOpenid,
|
|
175
|
-
},
|
|
176
|
-
);
|
|
177
|
-
inbound.typing.keepAlive?.stop();
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
try {
|
|
182
|
-
await runWithRequestContext(
|
|
183
|
-
{
|
|
184
|
-
accountId: account.accountId,
|
|
185
|
-
target: inbound.qualifiedTarget,
|
|
186
|
-
targetId: inbound.peerId,
|
|
187
|
-
chatType: event.type,
|
|
188
|
-
},
|
|
189
|
-
() => dispatchOutbound(inbound, { runtime, cfg: ctx.cfg, account, log }),
|
|
190
|
-
);
|
|
191
|
-
} catch (err) {
|
|
192
|
-
log?.error(`Message processing failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
193
|
-
} finally {
|
|
194
|
-
inbound.typing.keepAlive?.stop();
|
|
195
|
-
// Reset the buffered non-@ chatter after every @-activation turn
|
|
196
|
-
// (success or failure), matching the standalone build. Guards
|
|
197
|
-
// against stale history leaking into the next reply.
|
|
198
|
-
if (event.type === "group" && event.groupOpenid && inbound.group) {
|
|
199
|
-
clearGroupPendingHistory({
|
|
200
|
-
historyMap: groupHistories,
|
|
201
|
-
groupOpenid: event.groupOpenid,
|
|
202
|
-
historyLimit: inbound.group.historyLimit,
|
|
203
|
-
historyPort: adapters.history,
|
|
204
|
-
});
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
// ---- 8. Interaction handler ----
|
|
210
|
-
const handleInteraction = createInteractionHandler(account, ctx.runtime, log);
|
|
211
|
-
|
|
212
|
-
// ---- 9. Start connection ----
|
|
213
|
-
const connection = new GatewayConnection({
|
|
214
|
-
account,
|
|
215
|
-
abortSignal: ctx.abortSignal,
|
|
216
|
-
cfg: ctx.cfg,
|
|
217
|
-
log,
|
|
218
|
-
runtime,
|
|
219
|
-
onReady: ctx.onReady,
|
|
220
|
-
onResumed: ctx.onResumed,
|
|
221
|
-
onError: ctx.onError,
|
|
222
|
-
onInteraction: handleInteraction,
|
|
223
|
-
handleMessage,
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
await connection.start();
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// ============ Typing helper ============
|
|
230
|
-
|
|
231
|
-
/**
|
|
232
|
-
* Start typing indicator for a C2C event.
|
|
233
|
-
* Returns the refIdx from InputNotify and a TypingKeepAlive handle.
|
|
234
|
-
*/
|
|
235
|
-
async function startTypingForEvent(
|
|
236
|
-
event: QueuedMessage,
|
|
237
|
-
account: GatewayAccount,
|
|
238
|
-
log?: EngineLogger,
|
|
239
|
-
): Promise<{ refIdx?: string; keepAlive: TypingKeepAlive | null }> {
|
|
240
|
-
const isC2C = event.type === "c2c" || event.type === "dm";
|
|
241
|
-
if (!isC2C) {
|
|
242
|
-
return { keepAlive: null };
|
|
243
|
-
}
|
|
244
|
-
try {
|
|
245
|
-
const creds = accountToCreds(account);
|
|
246
|
-
const rawNotifyFn = createRawInputNotifyFn(account.appId);
|
|
247
|
-
try {
|
|
248
|
-
const resp = await senderSendInputNotify({
|
|
249
|
-
openid: event.senderId,
|
|
250
|
-
creds,
|
|
251
|
-
msgId: event.messageId,
|
|
252
|
-
inputSecond: TYPING_INPUT_SECOND,
|
|
253
|
-
});
|
|
254
|
-
const keepAlive = new TypingKeepAlive(
|
|
255
|
-
() => getAccessToken(account.appId, account.clientSecret),
|
|
256
|
-
() => clearTokenCache(account.appId),
|
|
257
|
-
rawNotifyFn,
|
|
258
|
-
event.senderId,
|
|
259
|
-
event.messageId,
|
|
260
|
-
log,
|
|
261
|
-
);
|
|
262
|
-
keepAlive.start();
|
|
263
|
-
return { refIdx: resp.refIdx, keepAlive };
|
|
264
|
-
} catch (notifyErr) {
|
|
265
|
-
const errMsg = String(notifyErr);
|
|
266
|
-
if (errMsg.includes("token") || errMsg.includes("401") || errMsg.includes("11244")) {
|
|
267
|
-
clearTokenCache(account.appId);
|
|
268
|
-
const resp = await senderSendInputNotify({
|
|
269
|
-
openid: event.senderId,
|
|
270
|
-
creds,
|
|
271
|
-
msgId: event.messageId,
|
|
272
|
-
inputSecond: TYPING_INPUT_SECOND,
|
|
273
|
-
});
|
|
274
|
-
const keepAlive = new TypingKeepAlive(
|
|
275
|
-
() => getAccessToken(account.appId, account.clientSecret),
|
|
276
|
-
() => clearTokenCache(account.appId),
|
|
277
|
-
rawNotifyFn,
|
|
278
|
-
event.senderId,
|
|
279
|
-
event.messageId,
|
|
280
|
-
log,
|
|
281
|
-
);
|
|
282
|
-
keepAlive.start();
|
|
283
|
-
return { refIdx: resp.refIdx, keepAlive };
|
|
284
|
-
}
|
|
285
|
-
throw notifyErr;
|
|
286
|
-
}
|
|
287
|
-
} catch (err) {
|
|
288
|
-
log?.error(`sendInputNotify error: ${err instanceof Error ? err.message : String(err)}`);
|
|
289
|
-
return { keepAlive: null };
|
|
290
|
-
}
|
|
291
|
-
}
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
-
import { processAttachments, type AudioConvertPort } from "./inbound-attachments.js";
|
|
3
|
-
|
|
4
|
-
const downloadFileMock = vi.hoisted(() => vi.fn());
|
|
5
|
-
const resolveSTTConfigMock = vi.hoisted(() => vi.fn());
|
|
6
|
-
const transcribeAudioMock = vi.hoisted(() => vi.fn());
|
|
7
|
-
|
|
8
|
-
vi.mock("../utils/file-utils.js", () => ({
|
|
9
|
-
downloadFile: downloadFileMock,
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock("../utils/platform.js", () => ({
|
|
13
|
-
getQQBotMediaDir: () => "/tmp/openclaw-qqbot-downloads",
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock("../utils/stt.js", () => ({
|
|
17
|
-
resolveSTTConfig: resolveSTTConfigMock,
|
|
18
|
-
transcribeAudio: transcribeAudioMock,
|
|
19
|
-
}));
|
|
20
|
-
|
|
21
|
-
function createAudioConvert(overrides: Partial<AudioConvertPort> = {}): AudioConvertPort {
|
|
22
|
-
return {
|
|
23
|
-
convertSilkToWav: vi.fn(async () => null),
|
|
24
|
-
formatDuration: (seconds: number) => `${seconds}s`,
|
|
25
|
-
isVoiceAttachment: (att: { content_type: string; filename?: string }) =>
|
|
26
|
-
att.content_type === "voice" || att.content_type.startsWith("audio/"),
|
|
27
|
-
...overrides,
|
|
28
|
-
};
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
describe("engine/gateway/inbound-attachments", () => {
|
|
32
|
-
let audioConvert: AudioConvertPort;
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
vi.clearAllMocks();
|
|
36
|
-
resolveSTTConfigMock.mockReturnValue(null);
|
|
37
|
-
transcribeAudioMock.mockResolvedValue(null);
|
|
38
|
-
audioConvert = createAudioConvert();
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it("returns an empty result when no attachments are present", async () => {
|
|
42
|
-
await expect(
|
|
43
|
-
processAttachments(undefined, { accountId: "qq", cfg: {}, audioConvert }),
|
|
44
|
-
).resolves.toMatchObject({
|
|
45
|
-
attachmentInfo: "",
|
|
46
|
-
imageUrls: [],
|
|
47
|
-
voiceAttachmentPaths: [],
|
|
48
|
-
attachmentLocalPaths: [],
|
|
49
|
-
});
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
it("uses remote image URL when image download fails", async () => {
|
|
53
|
-
downloadFileMock.mockResolvedValue(null);
|
|
54
|
-
|
|
55
|
-
const result = await processAttachments(
|
|
56
|
-
[{ content_type: "image/png", url: "//cdn.example.test/a.png", filename: "a.png" }],
|
|
57
|
-
{ accountId: "qq", cfg: {}, audioConvert },
|
|
58
|
-
);
|
|
59
|
-
|
|
60
|
-
expect(downloadFileMock).toHaveBeenCalledWith(
|
|
61
|
-
"https://cdn.example.test/a.png",
|
|
62
|
-
"/tmp/openclaw-qqbot-downloads",
|
|
63
|
-
"a.png",
|
|
64
|
-
);
|
|
65
|
-
expect(result.imageUrls).toEqual(["https://cdn.example.test/a.png"]);
|
|
66
|
-
expect(result.imageMediaTypes).toEqual(["image/png"]);
|
|
67
|
-
expect(result.attachmentLocalPaths).toEqual([null]);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
it("prefers voice_wav_url for voice downloads and transcribes with configured STT", async () => {
|
|
71
|
-
downloadFileMock.mockResolvedValue("/tmp/openclaw-qqbot-downloads/voice.wav");
|
|
72
|
-
resolveSTTConfigMock.mockReturnValue({
|
|
73
|
-
baseUrl: "https://stt.example.test",
|
|
74
|
-
apiKey: "key",
|
|
75
|
-
model: "whisper-1",
|
|
76
|
-
});
|
|
77
|
-
transcribeAudioMock.mockResolvedValue("transcribed voice");
|
|
78
|
-
|
|
79
|
-
const result = await processAttachments(
|
|
80
|
-
[
|
|
81
|
-
{
|
|
82
|
-
content_type: "voice",
|
|
83
|
-
url: "https://cdn.example.test/voice.silk",
|
|
84
|
-
filename: "voice.silk",
|
|
85
|
-
voice_wav_url: "//cdn.example.test/voice.wav",
|
|
86
|
-
asr_refer_text: "platform text",
|
|
87
|
-
},
|
|
88
|
-
],
|
|
89
|
-
{ accountId: "qq", cfg: { channels: { qqbot: { stt: {} } } }, audioConvert },
|
|
90
|
-
);
|
|
91
|
-
|
|
92
|
-
expect(downloadFileMock).toHaveBeenCalledWith(
|
|
93
|
-
"https://cdn.example.test/voice.wav",
|
|
94
|
-
"/tmp/openclaw-qqbot-downloads",
|
|
95
|
-
);
|
|
96
|
-
expect(transcribeAudioMock).toHaveBeenCalledWith("/tmp/openclaw-qqbot-downloads/voice.wav", {
|
|
97
|
-
channels: { qqbot: { stt: {} } },
|
|
98
|
-
});
|
|
99
|
-
expect(result.voiceAttachmentPaths).toEqual(["/tmp/openclaw-qqbot-downloads/voice.wav"]);
|
|
100
|
-
expect(result.voiceAttachmentUrls).toEqual(["https://cdn.example.test/voice.wav"]);
|
|
101
|
-
expect(result.voiceAsrReferTexts).toEqual(["platform text"]);
|
|
102
|
-
expect(result.voiceTranscripts).toEqual(["transcribed voice"]);
|
|
103
|
-
expect(result.voiceTranscriptSources).toEqual(["stt"]);
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it("falls back to platform ASR text when voice download fails", async () => {
|
|
107
|
-
downloadFileMock.mockResolvedValue(null);
|
|
108
|
-
|
|
109
|
-
const result = await processAttachments(
|
|
110
|
-
[
|
|
111
|
-
{
|
|
112
|
-
content_type: "voice",
|
|
113
|
-
url: "https://cdn.example.test/voice.silk",
|
|
114
|
-
filename: "voice.silk",
|
|
115
|
-
asr_refer_text: "platform text",
|
|
116
|
-
},
|
|
117
|
-
],
|
|
118
|
-
{ accountId: "qq", cfg: {}, audioConvert },
|
|
119
|
-
);
|
|
120
|
-
|
|
121
|
-
expect(result.voiceAttachmentUrls).toEqual(["https://cdn.example.test/voice.silk"]);
|
|
122
|
-
expect(result.voiceTranscripts).toEqual(["platform text"]);
|
|
123
|
-
expect(result.voiceTranscriptSources).toEqual(["asr"]);
|
|
124
|
-
expect(result.attachmentLocalPaths).toEqual([null]);
|
|
125
|
-
});
|
|
126
|
-
});
|