@openclaw/qqbot 2026.5.1-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/api.ts +56 -0
- package/channel-plugin-api.ts +1 -0
- package/dist/.boundary-tsc.stamp +1 -0
- package/dist/.boundary-tsc.tsbuildinfo +1 -0
- package/index.ts +29 -0
- package/openclaw.plugin.json +814 -0
- package/package.json +66 -0
- package/runtime-api.ts +9 -0
- package/setup-entry.ts +9 -0
- package/setup-plugin-api.ts +3 -0
- package/skills/qqbot-channel/SKILL.md +262 -0
- package/skills/qqbot-channel/references/api_references.md +521 -0
- package/skills/qqbot-media/SKILL.md +37 -0
- package/skills/qqbot-remind/SKILL.md +153 -0
- package/src/bridge/approval/capability.ts +237 -0
- package/src/bridge/approval/handler-runtime.ts +204 -0
- package/src/bridge/bootstrap.ts +135 -0
- package/src/bridge/channel-entry.ts +18 -0
- package/src/bridge/commands/framework-context-adapter.ts +60 -0
- package/src/bridge/commands/framework-registration.ts +47 -0
- package/src/bridge/commands/from-parser.test.ts +86 -0
- package/src/bridge/commands/from-parser.ts +60 -0
- package/src/bridge/commands/result-dispatcher.ts +76 -0
- package/src/bridge/config-shared.ts +132 -0
- package/src/bridge/config.ts +111 -0
- package/src/bridge/gateway.ts +174 -0
- package/src/bridge/logger.ts +31 -0
- package/src/bridge/narrowing.ts +31 -0
- package/src/bridge/plugin-version.test.ts +146 -0
- package/src/bridge/plugin-version.ts +102 -0
- package/src/bridge/runtime.ts +25 -0
- package/src/bridge/sdk-adapter.ts +131 -0
- package/src/bridge/setup/finalize.ts +144 -0
- package/src/bridge/setup/surface.ts +34 -0
- package/src/bridge/tools/channel.ts +58 -0
- package/src/bridge/tools/index.ts +15 -0
- package/src/bridge/tools/remind.test.ts +124 -0
- package/src/bridge/tools/remind.ts +91 -0
- package/src/channel.setup.ts +33 -0
- package/src/channel.ts +288 -0
- package/src/command-auth.test.ts +62 -0
- package/src/config-schema.ts +84 -0
- package/src/config.test.ts +364 -0
- package/src/engine/access/access-control.test.ts +198 -0
- package/src/engine/access/access-control.ts +226 -0
- package/src/engine/access/index.ts +16 -0
- package/src/engine/access/resolve-policy.test.ts +59 -0
- package/src/engine/access/resolve-policy.ts +57 -0
- package/src/engine/access/sender-match.test.ts +60 -0
- package/src/engine/access/sender-match.ts +55 -0
- package/src/engine/access/types.ts +53 -0
- package/src/engine/adapter/audio.port.ts +27 -0
- package/src/engine/adapter/commands.port.ts +22 -0
- package/src/engine/adapter/history.port.ts +52 -0
- package/src/engine/adapter/index.ts +139 -0
- package/src/engine/adapter/mention-gate.port.ts +50 -0
- package/src/engine/adapter/types.ts +38 -0
- package/src/engine/api/api-client.ts +212 -0
- package/src/engine/api/media-chunked.test.ts +336 -0
- package/src/engine/api/media-chunked.ts +622 -0
- package/src/engine/api/media.ts +218 -0
- package/src/engine/api/messages.ts +293 -0
- package/src/engine/api/retry.ts +217 -0
- package/src/engine/api/routes.ts +95 -0
- package/src/engine/api/token.ts +271 -0
- package/src/engine/approval/index.test.ts +22 -0
- package/src/engine/approval/index.ts +224 -0
- package/src/engine/commands/builtin/log-helpers.ts +319 -0
- package/src/engine/commands/builtin/register-all.ts +17 -0
- package/src/engine/commands/builtin/register-approve.ts +201 -0
- package/src/engine/commands/builtin/register-basic.ts +95 -0
- package/src/engine/commands/builtin/register-clear-storage.ts +187 -0
- package/src/engine/commands/builtin/register-logs.ts +20 -0
- package/src/engine/commands/builtin/register-streaming.ts +137 -0
- package/src/engine/commands/builtin/state.ts +31 -0
- package/src/engine/commands/slash-command-auth.ts +48 -0
- package/src/engine/commands/slash-command-handler.ts +146 -0
- package/src/engine/commands/slash-commands-impl.test.ts +8 -0
- package/src/engine/commands/slash-commands-impl.ts +61 -0
- package/src/engine/commands/slash-commands.ts +199 -0
- package/src/engine/config/credential-backup.test.ts +88 -0
- package/src/engine/config/credential-backup.ts +107 -0
- package/src/engine/config/credentials.ts +76 -0
- package/src/engine/config/group.test.ts +234 -0
- package/src/engine/config/group.ts +299 -0
- package/src/engine/config/resolve.test.ts +152 -0
- package/src/engine/config/resolve.ts +283 -0
- package/src/engine/config/setup-logic.ts +84 -0
- package/src/engine/engine-import-boundary.test.ts +73 -0
- package/src/engine/gateway/codec.ts +47 -0
- package/src/engine/gateway/constants.ts +117 -0
- package/src/engine/gateway/event-dispatcher.ts +177 -0
- package/src/engine/gateway/gateway-connection.ts +371 -0
- package/src/engine/gateway/gateway.ts +291 -0
- package/src/engine/gateway/inbound-attachments.test.ts +126 -0
- package/src/engine/gateway/inbound-attachments.ts +360 -0
- package/src/engine/gateway/inbound-context.ts +195 -0
- package/src/engine/gateway/inbound-pipeline.self-echo.test.ts +218 -0
- package/src/engine/gateway/inbound-pipeline.ts +235 -0
- package/src/engine/gateway/interaction-handler.ts +220 -0
- package/src/engine/gateway/message-queue.test.ts +282 -0
- package/src/engine/gateway/message-queue.ts +499 -0
- package/src/engine/gateway/outbound-dispatch.test.ts +231 -0
- package/src/engine/gateway/outbound-dispatch.ts +575 -0
- package/src/engine/gateway/reconnect.ts +199 -0
- package/src/engine/gateway/stages/access-stage.ts +132 -0
- package/src/engine/gateway/stages/assembly-stage.ts +156 -0
- package/src/engine/gateway/stages/content-stage.test.ts +77 -0
- package/src/engine/gateway/stages/content-stage.ts +77 -0
- package/src/engine/gateway/stages/envelope-stage.test.ts +152 -0
- package/src/engine/gateway/stages/envelope-stage.ts +144 -0
- package/src/engine/gateway/stages/group-gate-stage.ts +292 -0
- package/src/engine/gateway/stages/index.ts +18 -0
- package/src/engine/gateway/stages/quote-stage.ts +113 -0
- package/src/engine/gateway/stages/refidx-stage.ts +62 -0
- package/src/engine/gateway/stages/stub-contexts.ts +116 -0
- package/src/engine/gateway/types.ts +264 -0
- package/src/engine/gateway/typing-keepalive.ts +79 -0
- package/src/engine/group/activation.test.ts +114 -0
- package/src/engine/group/activation.ts +147 -0
- package/src/engine/group/history.test.ts +314 -0
- package/src/engine/group/history.ts +321 -0
- package/src/engine/group/mention.test.ts +141 -0
- package/src/engine/group/mention.ts +197 -0
- package/src/engine/group/message-gating.test.ts +188 -0
- package/src/engine/group/message-gating.ts +216 -0
- package/src/engine/messaging/decode-media-path.ts +82 -0
- package/src/engine/messaging/media-source.ts +215 -0
- package/src/engine/messaging/media-type-detect.ts +37 -0
- package/src/engine/messaging/outbound-audio-port.ts +38 -0
- package/src/engine/messaging/outbound-deliver.ts +810 -0
- package/src/engine/messaging/outbound-media-send.ts +702 -0
- package/src/engine/messaging/outbound-reply.ts +27 -0
- package/src/engine/messaging/outbound-result-helpers.ts +54 -0
- package/src/engine/messaging/outbound-types.ts +45 -0
- package/src/engine/messaging/outbound.ts +485 -0
- package/src/engine/messaging/reply-dispatcher.ts +597 -0
- package/src/engine/messaging/reply-limiter.ts +164 -0
- package/src/engine/messaging/sender.ts +729 -0
- package/src/engine/messaging/streaming-c2c.ts +1192 -0
- package/src/engine/messaging/streaming-media-send.ts +544 -0
- package/src/engine/messaging/target-parser.ts +104 -0
- package/src/engine/ref/format-message-ref.ts +142 -0
- package/src/engine/ref/format-ref-entry.test.ts +60 -0
- package/src/engine/ref/format-ref-entry.ts +27 -0
- package/src/engine/ref/store.ts +224 -0
- package/src/engine/ref/types.ts +27 -0
- package/src/engine/session/known-users.ts +254 -0
- package/src/engine/session/session-store.ts +284 -0
- package/src/engine/tools/channel-api.ts +244 -0
- package/src/engine/tools/remind-logic.test.ts +280 -0
- package/src/engine/tools/remind-logic.ts +377 -0
- package/src/engine/types.ts +313 -0
- package/src/engine/utils/attachment-tags.test.ts +186 -0
- package/src/engine/utils/attachment-tags.ts +174 -0
- package/src/engine/utils/audio.test.ts +250 -0
- package/src/engine/utils/audio.ts +585 -0
- package/src/engine/utils/data-paths.ts +38 -0
- package/src/engine/utils/diagnostics.ts +109 -0
- package/src/engine/utils/file-utils.test.ts +72 -0
- package/src/engine/utils/file-utils.ts +225 -0
- package/src/engine/utils/format.test.ts +68 -0
- package/src/engine/utils/format.ts +70 -0
- package/src/engine/utils/image-size.test.ts +158 -0
- package/src/engine/utils/image-size.ts +249 -0
- package/src/engine/utils/log.test.ts +28 -0
- package/src/engine/utils/log.ts +61 -0
- package/src/engine/utils/media-tags.test.ts +32 -0
- package/src/engine/utils/media-tags.ts +177 -0
- package/src/engine/utils/payload.test.ts +68 -0
- package/src/engine/utils/payload.ts +145 -0
- package/src/engine/utils/platform-storage-laziness.test.ts +65 -0
- package/src/engine/utils/platform.test.ts +148 -0
- package/src/engine/utils/platform.ts +343 -0
- package/src/engine/utils/request-context.ts +60 -0
- package/src/engine/utils/string-normalize.ts +91 -0
- package/src/engine/utils/stt.test.ts +104 -0
- package/src/engine/utils/stt.ts +100 -0
- package/src/engine/utils/text-parsing.test.ts +29 -0
- package/src/engine/utils/text-parsing.ts +155 -0
- package/src/engine/utils/upload-cache.ts +96 -0
- package/src/engine/utils/voice-text.ts +15 -0
- package/src/exec-approvals.ts +218 -0
- package/src/manifest-schema.test.ts +56 -0
- package/src/qqbot-test-support.ts +29 -0
- package/src/types.ts +210 -0
- package/tsconfig.json +16 -0
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import type { AudioConvertPort } from "../adapter/audio.port.js";
|
|
2
|
+
import { downloadFile } from "../utils/file-utils.js";
|
|
3
|
+
import { getQQBotMediaDir } from "../utils/platform.js";
|
|
4
|
+
import { normalizeOptionalString } from "../utils/string-normalize.js";
|
|
5
|
+
import { transcribeAudio, resolveSTTConfig } from "../utils/stt.js";
|
|
6
|
+
|
|
7
|
+
// Re-export the port type for convenience.
|
|
8
|
+
export type { AudioConvertPort } from "../adapter/audio.port.js";
|
|
9
|
+
|
|
10
|
+
interface RawAttachment {
|
|
11
|
+
content_type: string;
|
|
12
|
+
url: string;
|
|
13
|
+
filename?: string;
|
|
14
|
+
voice_wav_url?: string;
|
|
15
|
+
asr_refer_text?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type TranscriptSource = "stt" | "asr" | "fallback";
|
|
19
|
+
|
|
20
|
+
/** Normalized attachment output consumed by the gateway. */
|
|
21
|
+
export interface ProcessedAttachments {
|
|
22
|
+
attachmentInfo: string;
|
|
23
|
+
imageUrls: string[];
|
|
24
|
+
imageMediaTypes: string[];
|
|
25
|
+
voiceAttachmentPaths: string[];
|
|
26
|
+
voiceAttachmentUrls: string[];
|
|
27
|
+
voiceAsrReferTexts: string[];
|
|
28
|
+
voiceTranscripts: string[];
|
|
29
|
+
voiceTranscriptSources: TranscriptSource[];
|
|
30
|
+
attachmentLocalPaths: Array<string | null>;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface ProcessContext {
|
|
34
|
+
accountId: string;
|
|
35
|
+
cfg: unknown;
|
|
36
|
+
audioConvert: AudioConvertPort;
|
|
37
|
+
log?: {
|
|
38
|
+
info: (msg: string) => void;
|
|
39
|
+
error: (msg: string) => void;
|
|
40
|
+
debug?: (msg: string) => void;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const EMPTY_RESULT: ProcessedAttachments = {
|
|
45
|
+
attachmentInfo: "",
|
|
46
|
+
imageUrls: [],
|
|
47
|
+
imageMediaTypes: [],
|
|
48
|
+
voiceAttachmentPaths: [],
|
|
49
|
+
voiceAttachmentUrls: [],
|
|
50
|
+
voiceAsrReferTexts: [],
|
|
51
|
+
voiceTranscripts: [],
|
|
52
|
+
voiceTranscriptSources: [],
|
|
53
|
+
attachmentLocalPaths: [],
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** Download, convert, transcribe, and classify inbound attachments. */
|
|
57
|
+
export async function processAttachments(
|
|
58
|
+
attachments: RawAttachment[] | undefined,
|
|
59
|
+
ctx: ProcessContext,
|
|
60
|
+
): Promise<ProcessedAttachments> {
|
|
61
|
+
if (!attachments?.length) {
|
|
62
|
+
return EMPTY_RESULT;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const { accountId: _accountId, cfg, log, audioConvert } = ctx;
|
|
66
|
+
const downloadDir = getQQBotMediaDir("downloads");
|
|
67
|
+
|
|
68
|
+
const imageUrls: string[] = [];
|
|
69
|
+
const imageMediaTypes: string[] = [];
|
|
70
|
+
const voiceAttachmentPaths: string[] = [];
|
|
71
|
+
const voiceAttachmentUrls: string[] = [];
|
|
72
|
+
const voiceAsrReferTexts: string[] = [];
|
|
73
|
+
const voiceTranscripts: string[] = [];
|
|
74
|
+
const voiceTranscriptSources: TranscriptSource[] = [];
|
|
75
|
+
const attachmentLocalPaths: Array<string | null> = [];
|
|
76
|
+
const otherAttachments: string[] = [];
|
|
77
|
+
|
|
78
|
+
// Phase 1: download all attachments in parallel.
|
|
79
|
+
const downloadTasks = attachments.map(async (att) => {
|
|
80
|
+
const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
|
|
81
|
+
const isVoice = audioConvert.isVoiceAttachment(att);
|
|
82
|
+
const wavUrl =
|
|
83
|
+
isVoice && att.voice_wav_url
|
|
84
|
+
? att.voice_wav_url.startsWith("//")
|
|
85
|
+
? `https:${att.voice_wav_url}`
|
|
86
|
+
: att.voice_wav_url
|
|
87
|
+
: "";
|
|
88
|
+
|
|
89
|
+
let localPath: string | null = null;
|
|
90
|
+
let audioPath: string | null = null;
|
|
91
|
+
|
|
92
|
+
if (isVoice && wavUrl) {
|
|
93
|
+
const wavLocalPath = await downloadFile(wavUrl, downloadDir);
|
|
94
|
+
if (wavLocalPath) {
|
|
95
|
+
localPath = wavLocalPath;
|
|
96
|
+
audioPath = wavLocalPath;
|
|
97
|
+
log?.debug?.(`Voice attachment: ${att.filename}, downloaded WAV directly (skip SILK→WAV)`);
|
|
98
|
+
} else {
|
|
99
|
+
log?.error(`Failed to download voice_wav_url, falling back to original URL`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!localPath) {
|
|
104
|
+
localPath = await downloadFile(attUrl, downloadDir, att.filename);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return { att, attUrl, isVoice, localPath, audioPath };
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const downloadResults = await Promise.all(downloadTasks);
|
|
111
|
+
|
|
112
|
+
// Phase 2: convert/transcribe voice attachments and classify everything else.
|
|
113
|
+
const processTasks = downloadResults.map(
|
|
114
|
+
async ({ att, attUrl, isVoice, localPath, audioPath }) => {
|
|
115
|
+
const asrReferText = normalizeOptionalString(att.asr_refer_text) ?? "";
|
|
116
|
+
const wavUrl =
|
|
117
|
+
isVoice && att.voice_wav_url
|
|
118
|
+
? att.voice_wav_url.startsWith("//")
|
|
119
|
+
? `https:${att.voice_wav_url}`
|
|
120
|
+
: att.voice_wav_url
|
|
121
|
+
: "";
|
|
122
|
+
const voiceSourceUrl = wavUrl || attUrl;
|
|
123
|
+
|
|
124
|
+
const meta = {
|
|
125
|
+
voiceUrl: isVoice && voiceSourceUrl ? voiceSourceUrl : undefined,
|
|
126
|
+
asrReferText: isVoice && asrReferText ? asrReferText : undefined,
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
if (localPath) {
|
|
130
|
+
if (att.content_type?.startsWith("image/")) {
|
|
131
|
+
log?.debug?.(`Downloaded attachment to: ${localPath}`);
|
|
132
|
+
return { localPath, type: "image" as const, contentType: att.content_type, meta };
|
|
133
|
+
}
|
|
134
|
+
if (isVoice) {
|
|
135
|
+
log?.debug?.(`Downloaded attachment to: ${localPath}`);
|
|
136
|
+
return processVoiceAttachment(
|
|
137
|
+
localPath,
|
|
138
|
+
audioPath,
|
|
139
|
+
att,
|
|
140
|
+
asrReferText,
|
|
141
|
+
cfg,
|
|
142
|
+
downloadDir,
|
|
143
|
+
audioConvert,
|
|
144
|
+
log,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
log?.debug?.(`Downloaded attachment to: ${localPath}`);
|
|
148
|
+
return { localPath, type: "other" as const, filename: att.filename, meta };
|
|
149
|
+
}
|
|
150
|
+
log?.error(`Failed to download: ${attUrl}`);
|
|
151
|
+
if (att.content_type?.startsWith("image/")) {
|
|
152
|
+
return {
|
|
153
|
+
localPath: null,
|
|
154
|
+
type: "image-fallback" as const,
|
|
155
|
+
attUrl,
|
|
156
|
+
contentType: att.content_type,
|
|
157
|
+
meta,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (isVoice && asrReferText) {
|
|
161
|
+
log?.info(`Voice attachment download failed, using asr_refer_text fallback`);
|
|
162
|
+
return {
|
|
163
|
+
localPath: null,
|
|
164
|
+
type: "voice-fallback" as const,
|
|
165
|
+
transcript: asrReferText,
|
|
166
|
+
meta,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
localPath: null,
|
|
171
|
+
type: "other-fallback" as const,
|
|
172
|
+
filename: att.filename ?? att.content_type,
|
|
173
|
+
meta,
|
|
174
|
+
};
|
|
175
|
+
},
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
const processResults = await Promise.all(processTasks);
|
|
179
|
+
|
|
180
|
+
// Phase 3: collect results in the original attachment order.
|
|
181
|
+
for (const result of processResults) {
|
|
182
|
+
if (result.meta.voiceUrl) {
|
|
183
|
+
voiceAttachmentUrls.push(result.meta.voiceUrl);
|
|
184
|
+
}
|
|
185
|
+
if (result.meta.asrReferText) {
|
|
186
|
+
voiceAsrReferTexts.push(result.meta.asrReferText);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (result.type === "image" && result.localPath) {
|
|
190
|
+
imageUrls.push(result.localPath);
|
|
191
|
+
imageMediaTypes.push(result.contentType);
|
|
192
|
+
attachmentLocalPaths.push(result.localPath);
|
|
193
|
+
} else if (result.type === "voice" && result.localPath) {
|
|
194
|
+
voiceAttachmentPaths.push(result.localPath);
|
|
195
|
+
voiceTranscripts.push(result.transcript);
|
|
196
|
+
voiceTranscriptSources.push(result.transcriptSource);
|
|
197
|
+
attachmentLocalPaths.push(result.localPath);
|
|
198
|
+
} else if (result.type === "other" && result.localPath) {
|
|
199
|
+
otherAttachments.push(`[Attachment: ${result.localPath}]`);
|
|
200
|
+
attachmentLocalPaths.push(result.localPath);
|
|
201
|
+
} else if (result.type === "image-fallback") {
|
|
202
|
+
imageUrls.push(result.attUrl);
|
|
203
|
+
imageMediaTypes.push(result.contentType);
|
|
204
|
+
attachmentLocalPaths.push(null);
|
|
205
|
+
} else if (result.type === "voice-fallback") {
|
|
206
|
+
voiceTranscripts.push(result.transcript);
|
|
207
|
+
voiceTranscriptSources.push("asr");
|
|
208
|
+
attachmentLocalPaths.push(null);
|
|
209
|
+
} else if (result.type === "other-fallback") {
|
|
210
|
+
otherAttachments.push(`[Attachment: ${result.filename}] (download failed)`);
|
|
211
|
+
attachmentLocalPaths.push(null);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const attachmentInfo = otherAttachments.length > 0 ? "\n" + otherAttachments.join("\n") : "";
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
attachmentInfo,
|
|
219
|
+
imageUrls,
|
|
220
|
+
imageMediaTypes,
|
|
221
|
+
voiceAttachmentPaths,
|
|
222
|
+
voiceAttachmentUrls,
|
|
223
|
+
voiceAsrReferTexts,
|
|
224
|
+
voiceTranscripts,
|
|
225
|
+
voiceTranscriptSources,
|
|
226
|
+
attachmentLocalPaths,
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// formatVoiceText is now in core/utils/voice-text.ts (re-exported above).
|
|
231
|
+
|
|
232
|
+
// Internal helpers.
|
|
233
|
+
|
|
234
|
+
type VoiceResult =
|
|
235
|
+
| {
|
|
236
|
+
localPath: string;
|
|
237
|
+
type: "voice";
|
|
238
|
+
transcript: string;
|
|
239
|
+
transcriptSource: TranscriptSource;
|
|
240
|
+
meta: { voiceUrl?: string; asrReferText?: string };
|
|
241
|
+
}
|
|
242
|
+
| {
|
|
243
|
+
localPath: string;
|
|
244
|
+
type: "voice";
|
|
245
|
+
transcript: string;
|
|
246
|
+
transcriptSource: TranscriptSource;
|
|
247
|
+
meta: { voiceUrl?: string; asrReferText?: string };
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
async function processVoiceAttachment(
|
|
251
|
+
localPath: string,
|
|
252
|
+
audioPath: string | null,
|
|
253
|
+
att: RawAttachment,
|
|
254
|
+
asrReferText: string,
|
|
255
|
+
cfg: unknown,
|
|
256
|
+
downloadDir: string,
|
|
257
|
+
audioConvert: AudioConvertPort,
|
|
258
|
+
log: ProcessContext["log"],
|
|
259
|
+
): Promise<VoiceResult> {
|
|
260
|
+
const wavUrl = att.voice_wav_url
|
|
261
|
+
? att.voice_wav_url.startsWith("//")
|
|
262
|
+
? `https:${att.voice_wav_url}`
|
|
263
|
+
: att.voice_wav_url
|
|
264
|
+
: "";
|
|
265
|
+
const attUrl = att.url?.startsWith("//") ? `https:${att.url}` : att.url;
|
|
266
|
+
const voiceSourceUrl = wavUrl || attUrl;
|
|
267
|
+
const meta = {
|
|
268
|
+
voiceUrl: voiceSourceUrl || undefined,
|
|
269
|
+
asrReferText: asrReferText || undefined,
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const sttCfg = resolveSTTConfig(cfg as Record<string, unknown>);
|
|
273
|
+
if (!sttCfg) {
|
|
274
|
+
if (asrReferText) {
|
|
275
|
+
log?.debug?.(
|
|
276
|
+
`Voice attachment: ${att.filename} (STT not configured, using asr_refer_text fallback)`,
|
|
277
|
+
);
|
|
278
|
+
return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
|
|
279
|
+
}
|
|
280
|
+
log?.debug?.(`Voice attachment: ${att.filename} (STT not configured, skipping transcription)`);
|
|
281
|
+
return {
|
|
282
|
+
localPath,
|
|
283
|
+
type: "voice",
|
|
284
|
+
transcript: "[Voice message - transcription unavailable because STT is not configured]",
|
|
285
|
+
transcriptSource: "fallback",
|
|
286
|
+
meta,
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Convert SILK input to WAV before STT when necessary.
|
|
291
|
+
if (!audioPath) {
|
|
292
|
+
log?.debug?.(`Voice attachment: ${att.filename}, converting SILK→WAV...`);
|
|
293
|
+
try {
|
|
294
|
+
const wavResult = await audioConvert.convertSilkToWav(localPath, downloadDir);
|
|
295
|
+
if (wavResult) {
|
|
296
|
+
audioPath = wavResult.wavPath;
|
|
297
|
+
log?.debug?.(
|
|
298
|
+
`Voice converted: ${wavResult.wavPath} (${audioConvert.formatDuration(wavResult.duration)})`,
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
audioPath = localPath;
|
|
302
|
+
}
|
|
303
|
+
} catch (convertErr) {
|
|
304
|
+
log?.error(
|
|
305
|
+
`Voice conversion failed: ${
|
|
306
|
+
convertErr instanceof Error ? convertErr.message : JSON.stringify(convertErr)
|
|
307
|
+
}`,
|
|
308
|
+
);
|
|
309
|
+
if (asrReferText) {
|
|
310
|
+
return {
|
|
311
|
+
localPath,
|
|
312
|
+
type: "voice",
|
|
313
|
+
transcript: asrReferText,
|
|
314
|
+
transcriptSource: "asr",
|
|
315
|
+
meta,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
localPath,
|
|
320
|
+
type: "voice",
|
|
321
|
+
transcript: "[Voice message - format conversion failed]",
|
|
322
|
+
transcriptSource: "fallback",
|
|
323
|
+
meta,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// Run speech-to-text on the prepared audio file.
|
|
329
|
+
try {
|
|
330
|
+
const transcript = await transcribeAudio(audioPath, cfg as Record<string, unknown>);
|
|
331
|
+
if (transcript) {
|
|
332
|
+
log?.debug?.(`STT transcript: ${transcript.slice(0, 100)}...`);
|
|
333
|
+
return { localPath, type: "voice", transcript, transcriptSource: "stt", meta };
|
|
334
|
+
}
|
|
335
|
+
if (asrReferText) {
|
|
336
|
+
log?.debug?.(`STT returned empty result, using asr_refer_text fallback`);
|
|
337
|
+
return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
|
|
338
|
+
}
|
|
339
|
+
log?.debug?.(`STT returned empty result`);
|
|
340
|
+
return {
|
|
341
|
+
localPath,
|
|
342
|
+
type: "voice",
|
|
343
|
+
transcript: "[Voice message - transcription returned an empty result]",
|
|
344
|
+
transcriptSource: "fallback",
|
|
345
|
+
meta,
|
|
346
|
+
};
|
|
347
|
+
} catch (sttErr) {
|
|
348
|
+
log?.error(`STT failed: ${sttErr instanceof Error ? sttErr.message : JSON.stringify(sttErr)}`);
|
|
349
|
+
if (asrReferText) {
|
|
350
|
+
return { localPath, type: "voice", transcript: asrReferText, transcriptSource: "asr", meta };
|
|
351
|
+
}
|
|
352
|
+
return {
|
|
353
|
+
localPath,
|
|
354
|
+
type: "voice",
|
|
355
|
+
transcript: "[Voice message - transcription failed]",
|
|
356
|
+
transcriptSource: "fallback",
|
|
357
|
+
meta,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
}
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InboundContext — the structured result of the inbound pipeline.
|
|
3
|
+
*
|
|
4
|
+
* Connects the inbound stage (content building, attachment processing,
|
|
5
|
+
* quote resolution) with the outbound stage (AI dispatch, deliver callbacks).
|
|
6
|
+
*
|
|
7
|
+
* All fields are readonly after construction. The outbound dispatcher
|
|
8
|
+
* reads from this object but never mutates it.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import type { QQBotAccessDecision, QQBotAccessReasonCode } from "../access/index.js";
|
|
12
|
+
import type { EngineAdapters } from "../adapter/index.js";
|
|
13
|
+
import type { GroupActivationMode, SessionStoreReader } from "../group/activation.js";
|
|
14
|
+
import type { HistoryEntry } from "../group/history.js";
|
|
15
|
+
import type { GroupMessageGateResult } from "../group/message-gating.js";
|
|
16
|
+
import type { QueuedMessage } from "./message-queue.js";
|
|
17
|
+
import type {
|
|
18
|
+
GatewayAccount,
|
|
19
|
+
EngineLogger,
|
|
20
|
+
GatewayPluginRuntime,
|
|
21
|
+
ProcessedAttachments,
|
|
22
|
+
} from "./types.js";
|
|
23
|
+
import type { TypingKeepAlive } from "./typing-keepalive.js";
|
|
24
|
+
|
|
25
|
+
// ============ InboundContext ============
|
|
26
|
+
|
|
27
|
+
/** Quote (reply-to) metadata resolved during inbound processing. */
|
|
28
|
+
export interface ReplyToInfo {
|
|
29
|
+
id: string;
|
|
30
|
+
body?: string;
|
|
31
|
+
sender?: string;
|
|
32
|
+
isQuote: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Group-specific inbound metadata.
|
|
37
|
+
*
|
|
38
|
+
* Populated for group / guild events; left `undefined` for DMs. Keeping
|
|
39
|
+
* the group fields under a nested bag makes it obvious which fields are
|
|
40
|
+
* safe to read only when `isGroupChat === true`.
|
|
41
|
+
*
|
|
42
|
+
* The shape is kept small on purpose: everything derivable from `gate`
|
|
43
|
+
* (raw wasMentioned / explicit / implicit / hasAnyMention / bypass) is
|
|
44
|
+
* stored once on `gate`, not duplicated on the outer object.
|
|
45
|
+
*/
|
|
46
|
+
export interface InboundGroupInfo {
|
|
47
|
+
// ---- Gating decision ----
|
|
48
|
+
/** Full gate evaluation result (source of truth for mention state). */
|
|
49
|
+
gate: GroupMessageGateResult;
|
|
50
|
+
/** Effective activation mode after session-store / cfg merge. */
|
|
51
|
+
activation: GroupActivationMode;
|
|
52
|
+
|
|
53
|
+
// ---- Persistence-relevant ----
|
|
54
|
+
/** Per-group history buffer cap. Zero → disabled. */
|
|
55
|
+
historyLimit: number;
|
|
56
|
+
/** `true` if this message was built by merging several queued entries. */
|
|
57
|
+
isMerged: boolean;
|
|
58
|
+
/** The unfiltered list of queued messages when `isMerged`, else undefined. */
|
|
59
|
+
mergedMessages?: readonly QueuedMessage[];
|
|
60
|
+
|
|
61
|
+
// ---- Presentation / prompt inputs ----
|
|
62
|
+
/** Bundle of display-only strings; assembled by the envelope stage. */
|
|
63
|
+
display: {
|
|
64
|
+
/** Human-readable group name ("My Group" / first 8 chars of openid). */
|
|
65
|
+
groupName: string;
|
|
66
|
+
/** Sender label ("Nick (OPENID)" / "OPENID") for the UI. */
|
|
67
|
+
senderLabel: string;
|
|
68
|
+
/** Channel-level intro hint contributed by the platform adapter. */
|
|
69
|
+
introHint?: string;
|
|
70
|
+
/** Per-group behaviour prompt appended to the system prompt. */
|
|
71
|
+
behaviorPrompt?: string;
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Fully resolved inbound context passed to the outbound dispatcher. */
|
|
76
|
+
export interface InboundContext {
|
|
77
|
+
// ---- Original event ----
|
|
78
|
+
event: QueuedMessage;
|
|
79
|
+
|
|
80
|
+
// ---- Routing ----
|
|
81
|
+
route: { sessionKey: string; accountId: string; agentId?: string };
|
|
82
|
+
isGroupChat: boolean;
|
|
83
|
+
peerId: string;
|
|
84
|
+
/** Fully qualified target address: "qqbot:c2c:xxx" / "qqbot:group:xxx" etc. */
|
|
85
|
+
qualifiedTarget: string;
|
|
86
|
+
fromAddress: string;
|
|
87
|
+
|
|
88
|
+
// ---- Content ----
|
|
89
|
+
/** event.content after parseFaceTags. */
|
|
90
|
+
parsedContent: string;
|
|
91
|
+
/** parsedContent + voiceText + attachmentInfo — the user-visible text. */
|
|
92
|
+
userContent: string;
|
|
93
|
+
/** "[Quoted message begins]…[ends]" or empty. */
|
|
94
|
+
quotePart: string;
|
|
95
|
+
/** Per-message dynamic metadata lines (images, voice, ASR). */
|
|
96
|
+
dynamicCtx: string;
|
|
97
|
+
/** quotePart + userContent. */
|
|
98
|
+
userMessage: string;
|
|
99
|
+
/** dynamicCtx + userMessage (or raw content for slash commands). */
|
|
100
|
+
agentBody: string;
|
|
101
|
+
/** Formatted inbound envelope (Web UI body). */
|
|
102
|
+
body: string;
|
|
103
|
+
|
|
104
|
+
// ---- System prompts ----
|
|
105
|
+
systemPrompts: string[];
|
|
106
|
+
groupSystemPrompt?: string;
|
|
107
|
+
|
|
108
|
+
// ---- Attachments ----
|
|
109
|
+
attachments: ProcessedAttachments;
|
|
110
|
+
localMediaPaths: string[];
|
|
111
|
+
localMediaTypes: string[];
|
|
112
|
+
remoteMediaUrls: string[];
|
|
113
|
+
remoteMediaTypes: string[];
|
|
114
|
+
|
|
115
|
+
// ---- Voice ----
|
|
116
|
+
uniqueVoicePaths: string[];
|
|
117
|
+
uniqueVoiceUrls: string[];
|
|
118
|
+
uniqueVoiceAsrReferTexts: string[];
|
|
119
|
+
voiceMediaTypes: string[];
|
|
120
|
+
hasAsrReferFallback: boolean;
|
|
121
|
+
voiceTranscriptSources: string[];
|
|
122
|
+
|
|
123
|
+
// ---- Reply-to / Quote ----
|
|
124
|
+
replyTo?: ReplyToInfo;
|
|
125
|
+
|
|
126
|
+
// ---- Auth ----
|
|
127
|
+
commandAuthorized: boolean;
|
|
128
|
+
|
|
129
|
+
// ---- Group ----
|
|
130
|
+
/** Populated only for group / guild messages. */
|
|
131
|
+
group?: InboundGroupInfo;
|
|
132
|
+
|
|
133
|
+
// ---- Blocking / skipping ----
|
|
134
|
+
/**
|
|
135
|
+
* Whether the inbound message should be blocked outright (access policy
|
|
136
|
+
* refused the sender). Mutually exclusive with `skipped`.
|
|
137
|
+
*/
|
|
138
|
+
blocked: boolean;
|
|
139
|
+
/** Human-readable reason for `blocked`, for logging only. */
|
|
140
|
+
blockReason?: string;
|
|
141
|
+
/** Structured reason code for `blocked`. */
|
|
142
|
+
blockReasonCode?: QQBotAccessReasonCode;
|
|
143
|
+
/** The raw access decision produced by the policy engine. */
|
|
144
|
+
accessDecision?: QQBotAccessDecision;
|
|
145
|
+
/**
|
|
146
|
+
* Whether the inbound was accepted by access control but stopped before
|
|
147
|
+
* AI dispatch by the group gate (e.g. "skip_no_mention"). The caller
|
|
148
|
+
* should NOT forward `skipped` messages to the outbound dispatcher, but
|
|
149
|
+
* history / activity side-effects may already have been applied.
|
|
150
|
+
*/
|
|
151
|
+
skipped: boolean;
|
|
152
|
+
/** Structured reason code for `skipped`. */
|
|
153
|
+
skipReason?: "drop_other_mention" | "block_unauthorized_command" | "skip_no_mention";
|
|
154
|
+
|
|
155
|
+
// ---- Typing ----
|
|
156
|
+
typing: { keepAlive: TypingKeepAlive | null };
|
|
157
|
+
/** refIdx returned by the initial InputNotify call. */
|
|
158
|
+
inputNotifyRefIdx?: string;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ============ Pipeline dependencies ============
|
|
162
|
+
|
|
163
|
+
/** Dependencies injected into the inbound pipeline. */
|
|
164
|
+
export interface InboundPipelineDeps {
|
|
165
|
+
account: GatewayAccount;
|
|
166
|
+
cfg: unknown;
|
|
167
|
+
log?: EngineLogger;
|
|
168
|
+
runtime: GatewayPluginRuntime;
|
|
169
|
+
/** Start typing indicator and return the refIdx from InputNotify. */
|
|
170
|
+
startTyping: (event: QueuedMessage) => Promise<{
|
|
171
|
+
refIdx?: string;
|
|
172
|
+
keepAlive: TypingKeepAlive | null;
|
|
173
|
+
}>;
|
|
174
|
+
// ---- Group dependencies (optional — omit when the caller doesn't need
|
|
175
|
+
// group support, e.g. a DM-only test harness). ----
|
|
176
|
+
/** Shared per-connection history buffer, created by the gateway. */
|
|
177
|
+
groupHistories?: Map<string, HistoryEntry[]>;
|
|
178
|
+
/** Session-store reader for activation-mode overrides. */
|
|
179
|
+
sessionStoreReader?: SessionStoreReader;
|
|
180
|
+
/** Whether text-based control commands are enabled globally. */
|
|
181
|
+
allowTextCommands?: boolean;
|
|
182
|
+
/**
|
|
183
|
+
* Framework probe that returns true when `content` is a known control
|
|
184
|
+
* command. Injected to avoid hard-coding a list of commands in engine.
|
|
185
|
+
*/
|
|
186
|
+
isControlCommand?: (content: string) => boolean;
|
|
187
|
+
/** Optional platform hook that contributes a channel-level intro hint. */
|
|
188
|
+
resolveGroupIntroHint?: (params: {
|
|
189
|
+
cfg: unknown;
|
|
190
|
+
accountId: string;
|
|
191
|
+
groupId: string;
|
|
192
|
+
}) => string | undefined;
|
|
193
|
+
/** SDK adapter ports for delegating to shared implementations. */
|
|
194
|
+
adapters: EngineAdapters;
|
|
195
|
+
}
|