opencode-tbot 0.1.16 → 0.1.19
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.ja.md +159 -0
- package/README.md +5 -17
- package/README.zh-CN.md +12 -24
- package/dist/assets/{plugin-config-DA71_jD3.js → plugin-config-B8ginwol.js} +7 -51
- package/dist/assets/plugin-config-B8ginwol.js.map +1 -0
- package/dist/cli.js +5 -30
- package/dist/cli.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.js +17 -545
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -3
- package/tbot.config.example.json +0 -5
- package/dist/assets/plugin-config-DA71_jD3.js.map +0 -1
package/dist/plugin.js
CHANGED
|
@@ -1,13 +1,10 @@
|
|
|
1
|
-
import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-
|
|
2
|
-
import { createRequire } from "node:module";
|
|
1
|
+
import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-B8ginwol.js";
|
|
3
2
|
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
|
-
import {
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
5
4
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
6
5
|
import { z } from "zod";
|
|
7
|
-
import { OpenRouter } from "@openrouter/sdk";
|
|
8
6
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
9
7
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
8
|
import { run } from "@grammyjs/runner";
|
|
12
9
|
import { Bot, InlineKeyboard } from "grammy";
|
|
13
10
|
//#region src/infra/utils/redact.ts
|
|
@@ -1089,14 +1086,6 @@ var TelegramFileDownloadError = class extends Error {
|
|
|
1089
1086
|
this.data = { message };
|
|
1090
1087
|
}
|
|
1091
1088
|
};
|
|
1092
|
-
var VoiceMessageUnsupportedError = class extends Error {
|
|
1093
|
-
data;
|
|
1094
|
-
constructor(message) {
|
|
1095
|
-
super(message);
|
|
1096
|
-
this.name = "VoiceMessageUnsupportedError";
|
|
1097
|
-
this.data = { message };
|
|
1098
|
-
}
|
|
1099
|
-
};
|
|
1100
1089
|
var TelegramFileClient = class {
|
|
1101
1090
|
baseUrl;
|
|
1102
1091
|
fetchFn;
|
|
@@ -1111,11 +1100,11 @@ var TelegramFileClient = class {
|
|
|
1111
1100
|
try {
|
|
1112
1101
|
response = await this.fetchFn(new URL(filePath, this.baseUrl));
|
|
1113
1102
|
} catch (error) {
|
|
1114
|
-
throw new TelegramFileDownloadError(extractErrorMessage
|
|
1103
|
+
throw new TelegramFileDownloadError(extractErrorMessage(error) ?? "Failed to download the Telegram file.");
|
|
1115
1104
|
}
|
|
1116
1105
|
if (!response.ok) throw new TelegramFileDownloadError(await buildDownloadFailureMessage(response));
|
|
1117
1106
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
1118
|
-
if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty
|
|
1107
|
+
if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty file.");
|
|
1119
1108
|
return {
|
|
1120
1109
|
data,
|
|
1121
1110
|
mimeType: response.headers.get("content-type")
|
|
@@ -1137,7 +1126,7 @@ async function safeReadResponseText(response) {
|
|
|
1137
1126
|
return null;
|
|
1138
1127
|
}
|
|
1139
1128
|
}
|
|
1140
|
-
function extractErrorMessage
|
|
1129
|
+
function extractErrorMessage(error) {
|
|
1141
1130
|
return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : null;
|
|
1142
1131
|
}
|
|
1143
1132
|
//#endregion
|
|
@@ -1180,435 +1169,6 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
|
|
|
1180
1169
|
}
|
|
1181
1170
|
};
|
|
1182
1171
|
//#endregion
|
|
1183
|
-
//#region src/services/voice-transcription/audio-transcoder.ts
|
|
1184
|
-
var OPENROUTER_SUPPORTED_AUDIO_FORMATS = ["mp3", "wav"];
|
|
1185
|
-
var VoiceTranscodingFailedError = class extends Error {
|
|
1186
|
-
data;
|
|
1187
|
-
constructor(message) {
|
|
1188
|
-
super(message);
|
|
1189
|
-
this.name = "VoiceTranscodingFailedError";
|
|
1190
|
-
this.data = { message };
|
|
1191
|
-
}
|
|
1192
|
-
};
|
|
1193
|
-
var DEFAULT_TRANSCODE_TIMEOUT_MS = 15e3;
|
|
1194
|
-
var FfmpegAudioTranscoder = class {
|
|
1195
|
-
ffmpegPath;
|
|
1196
|
-
spawnProcess;
|
|
1197
|
-
timeoutMs;
|
|
1198
|
-
constructor(options) {
|
|
1199
|
-
this.ffmpegPath = options.ffmpegPath?.trim() || null;
|
|
1200
|
-
this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1201
|
-
this.timeoutMs = options.timeoutMs ?? DEFAULT_TRANSCODE_TIMEOUT_MS;
|
|
1202
|
-
}
|
|
1203
|
-
async transcode(input) {
|
|
1204
|
-
if (!this.ffmpegPath) throw new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, "Bundled ffmpeg is unavailable."));
|
|
1205
|
-
if (input.targetFormat !== "wav") throw new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, `Unsupported transcode target: ${input.targetFormat}.`));
|
|
1206
|
-
return {
|
|
1207
|
-
data: await runFfmpegTranscode({
|
|
1208
|
-
data: toUint8Array$1(input.data),
|
|
1209
|
-
ffmpegPath: this.ffmpegPath,
|
|
1210
|
-
filename: input.filename,
|
|
1211
|
-
sourceFormat: input.sourceFormat,
|
|
1212
|
-
spawnProcess: this.spawnProcess,
|
|
1213
|
-
timeoutMs: this.timeoutMs,
|
|
1214
|
-
targetFormat: input.targetFormat
|
|
1215
|
-
}),
|
|
1216
|
-
filename: replaceExtension(input.filename, ".wav"),
|
|
1217
|
-
format: "wav",
|
|
1218
|
-
mimeType: "audio/wav"
|
|
1219
|
-
};
|
|
1220
|
-
}
|
|
1221
|
-
};
|
|
1222
|
-
async function runFfmpegTranscode(input) {
|
|
1223
|
-
return await new Promise((resolve, reject) => {
|
|
1224
|
-
const child = input.spawnProcess(input.ffmpegPath, buildFfmpegArgs(input.targetFormat), {
|
|
1225
|
-
stdio: [
|
|
1226
|
-
"pipe",
|
|
1227
|
-
"pipe",
|
|
1228
|
-
"pipe"
|
|
1229
|
-
],
|
|
1230
|
-
windowsHide: true
|
|
1231
|
-
});
|
|
1232
|
-
const stdoutChunks = [];
|
|
1233
|
-
const stderrChunks = [];
|
|
1234
|
-
let settled = false;
|
|
1235
|
-
let timedOut = false;
|
|
1236
|
-
const timer = setTimeout(() => {
|
|
1237
|
-
timedOut = true;
|
|
1238
|
-
child.kill();
|
|
1239
|
-
}, input.timeoutMs);
|
|
1240
|
-
const cleanup = () => {
|
|
1241
|
-
clearTimeout(timer);
|
|
1242
|
-
};
|
|
1243
|
-
const rejectOnce = (message) => {
|
|
1244
|
-
if (settled) return;
|
|
1245
|
-
settled = true;
|
|
1246
|
-
cleanup();
|
|
1247
|
-
reject(new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, message)));
|
|
1248
|
-
};
|
|
1249
|
-
const resolveOnce = (value) => {
|
|
1250
|
-
if (settled) return;
|
|
1251
|
-
settled = true;
|
|
1252
|
-
cleanup();
|
|
1253
|
-
resolve(value);
|
|
1254
|
-
};
|
|
1255
|
-
child.stdout.on("data", (chunk) => {
|
|
1256
|
-
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1257
|
-
});
|
|
1258
|
-
child.stderr.on("data", (chunk) => {
|
|
1259
|
-
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1260
|
-
});
|
|
1261
|
-
child.once("error", (error) => {
|
|
1262
|
-
rejectOnce(`Failed to start bundled ffmpeg: ${error.message}`);
|
|
1263
|
-
});
|
|
1264
|
-
child.once("close", (code, signal) => {
|
|
1265
|
-
if (timedOut) {
|
|
1266
|
-
rejectOnce(`Bundled ffmpeg timed out after ${input.timeoutMs} ms.`);
|
|
1267
|
-
return;
|
|
1268
|
-
}
|
|
1269
|
-
if (code !== 0) {
|
|
1270
|
-
rejectOnce(Buffer.concat(stderrChunks).toString("utf8").trim() || `Bundled ffmpeg exited with code ${code}${signal ? ` (${signal})` : ""}.`);
|
|
1271
|
-
return;
|
|
1272
|
-
}
|
|
1273
|
-
const output = Buffer.concat(stdoutChunks);
|
|
1274
|
-
if (output.length === 0) {
|
|
1275
|
-
rejectOnce("Bundled ffmpeg returned empty audio output.");
|
|
1276
|
-
return;
|
|
1277
|
-
}
|
|
1278
|
-
resolveOnce(new Uint8Array(output));
|
|
1279
|
-
});
|
|
1280
|
-
child.stdin.on("error", (error) => {
|
|
1281
|
-
rejectOnce(`Failed to write audio data to bundled ffmpeg: ${error.message}`);
|
|
1282
|
-
});
|
|
1283
|
-
child.stdin.write(Buffer.from(input.data));
|
|
1284
|
-
child.stdin.end();
|
|
1285
|
-
});
|
|
1286
|
-
}
|
|
1287
|
-
function buildFfmpegArgs(targetFormat) {
|
|
1288
|
-
if (targetFormat !== "wav") throw new Error(`Unsupported target format: ${targetFormat}`);
|
|
1289
|
-
return [
|
|
1290
|
-
"-hide_banner",
|
|
1291
|
-
"-loglevel",
|
|
1292
|
-
"error",
|
|
1293
|
-
"-i",
|
|
1294
|
-
"pipe:0",
|
|
1295
|
-
"-f",
|
|
1296
|
-
"wav",
|
|
1297
|
-
"-acodec",
|
|
1298
|
-
"pcm_s16le",
|
|
1299
|
-
"-ac",
|
|
1300
|
-
"1",
|
|
1301
|
-
"-ar",
|
|
1302
|
-
"16000",
|
|
1303
|
-
"pipe:1"
|
|
1304
|
-
];
|
|
1305
|
-
}
|
|
1306
|
-
function buildTranscodingMessage(sourceFormat, targetFormat, reason) {
|
|
1307
|
-
return `Failed to transcode audio from ${sourceFormat} to ${targetFormat}. ${reason}`;
|
|
1308
|
-
}
|
|
1309
|
-
function replaceExtension(filename, nextExtension) {
|
|
1310
|
-
const trimmedFilename = basename(filename).trim();
|
|
1311
|
-
if (!trimmedFilename) return `telegram-voice${nextExtension}`;
|
|
1312
|
-
const currentExtension = extname(trimmedFilename);
|
|
1313
|
-
return currentExtension ? `${trimmedFilename.slice(0, -currentExtension.length)}${nextExtension}` : `${trimmedFilename}${nextExtension}`;
|
|
1314
|
-
}
|
|
1315
|
-
function toUint8Array$1(data) {
|
|
1316
|
-
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1317
|
-
}
|
|
1318
|
-
function defaultSpawnProcess(command, args, options) {
|
|
1319
|
-
return spawn(command, args, options);
|
|
1320
|
-
}
|
|
1321
|
-
//#endregion
|
|
1322
|
-
//#region src/services/voice-transcription/openrouter-voice.client.ts
|
|
1323
|
-
var VoiceTranscriptionNotConfiguredError = class extends Error {
|
|
1324
|
-
data;
|
|
1325
|
-
constructor(message) {
|
|
1326
|
-
super(message);
|
|
1327
|
-
this.name = "VoiceTranscriptionNotConfiguredError";
|
|
1328
|
-
this.data = { message };
|
|
1329
|
-
}
|
|
1330
|
-
};
|
|
1331
|
-
var VoiceTranscriptionFailedError = class extends Error {
|
|
1332
|
-
data;
|
|
1333
|
-
constructor(message) {
|
|
1334
|
-
super(message);
|
|
1335
|
-
this.name = "VoiceTranscriptionFailedError";
|
|
1336
|
-
this.data = { message };
|
|
1337
|
-
}
|
|
1338
|
-
};
|
|
1339
|
-
var VoiceTranscriptEmptyError = class extends Error {
|
|
1340
|
-
data;
|
|
1341
|
-
constructor(message) {
|
|
1342
|
-
super(message);
|
|
1343
|
-
this.name = "VoiceTranscriptEmptyError";
|
|
1344
|
-
this.data = { message };
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
var DisabledVoiceTranscriptionClient = class {
|
|
1348
|
-
getStatus() {
|
|
1349
|
-
return {
|
|
1350
|
-
status: "not_configured",
|
|
1351
|
-
model: null
|
|
1352
|
-
};
|
|
1353
|
-
}
|
|
1354
|
-
async transcribe() {
|
|
1355
|
-
throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
|
|
1356
|
-
}
|
|
1357
|
-
};
|
|
1358
|
-
var OpenRouterVoiceTranscriptionClient = class {
|
|
1359
|
-
audioTranscoder;
|
|
1360
|
-
model;
|
|
1361
|
-
sdk;
|
|
1362
|
-
timeoutMs;
|
|
1363
|
-
transcriptionPrompt;
|
|
1364
|
-
constructor(options, sdk, audioTranscoder = new FfmpegAudioTranscoder({
|
|
1365
|
-
ffmpegPath: null,
|
|
1366
|
-
timeoutMs: options.timeoutMs
|
|
1367
|
-
})) {
|
|
1368
|
-
this.audioTranscoder = audioTranscoder;
|
|
1369
|
-
this.model = options.model;
|
|
1370
|
-
this.sdk = sdk;
|
|
1371
|
-
this.timeoutMs = options.timeoutMs;
|
|
1372
|
-
this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
|
|
1373
|
-
}
|
|
1374
|
-
getStatus() {
|
|
1375
|
-
return {
|
|
1376
|
-
status: "configured",
|
|
1377
|
-
model: this.model
|
|
1378
|
-
};
|
|
1379
|
-
}
|
|
1380
|
-
async transcribe(input) {
|
|
1381
|
-
const preparedAudio = await prepareAudioForOpenRouter(input, resolveAudioFormat(input.filename, input.mimeType), this.audioTranscoder);
|
|
1382
|
-
const audioData = toBase64(preparedAudio.data);
|
|
1383
|
-
const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
|
|
1384
|
-
let response;
|
|
1385
|
-
try {
|
|
1386
|
-
response = await this.sdk.chat.send({ chatGenerationParams: {
|
|
1387
|
-
messages: [{
|
|
1388
|
-
role: "user",
|
|
1389
|
-
content: [{
|
|
1390
|
-
type: "text",
|
|
1391
|
-
text: prompt
|
|
1392
|
-
}, {
|
|
1393
|
-
type: "input_audio",
|
|
1394
|
-
inputAudio: {
|
|
1395
|
-
data: audioData,
|
|
1396
|
-
format: preparedAudio.format
|
|
1397
|
-
}
|
|
1398
|
-
}]
|
|
1399
|
-
}],
|
|
1400
|
-
model: this.model,
|
|
1401
|
-
stream: false,
|
|
1402
|
-
temperature: 0
|
|
1403
|
-
} }, { timeoutMs: this.timeoutMs });
|
|
1404
|
-
} catch (error) {
|
|
1405
|
-
throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
|
|
1406
|
-
format: preparedAudio.format,
|
|
1407
|
-
model: this.model
|
|
1408
|
-
}));
|
|
1409
|
-
}
|
|
1410
|
-
return { text: extractTranscript(response) };
|
|
1411
|
-
}
|
|
1412
|
-
};
|
|
1413
|
-
async function prepareAudioForOpenRouter(input, sourceFormat, audioTranscoder) {
|
|
1414
|
-
if (isOpenRouterSupportedAudioFormat(sourceFormat)) return {
|
|
1415
|
-
data: toUint8Array(input.data),
|
|
1416
|
-
format: sourceFormat
|
|
1417
|
-
};
|
|
1418
|
-
const transcoded = await audioTranscoder.transcode({
|
|
1419
|
-
data: input.data,
|
|
1420
|
-
filename: input.filename,
|
|
1421
|
-
sourceFormat,
|
|
1422
|
-
targetFormat: "wav"
|
|
1423
|
-
});
|
|
1424
|
-
return {
|
|
1425
|
-
data: transcoded.data,
|
|
1426
|
-
format: transcoded.format
|
|
1427
|
-
};
|
|
1428
|
-
}
|
|
1429
|
-
var MIME_TYPE_FORMAT_MAP = {
|
|
1430
|
-
"audio/aac": "aac",
|
|
1431
|
-
"audio/aiff": "aiff",
|
|
1432
|
-
"audio/flac": "flac",
|
|
1433
|
-
"audio/m4a": "m4a",
|
|
1434
|
-
"audio/mp3": "mp3",
|
|
1435
|
-
"audio/mp4": "m4a",
|
|
1436
|
-
"audio/mpeg": "mp3",
|
|
1437
|
-
"audio/ogg": "ogg",
|
|
1438
|
-
"audio/wav": "wav",
|
|
1439
|
-
"audio/wave": "wav",
|
|
1440
|
-
"audio/x-aac": "aac",
|
|
1441
|
-
"audio/x-aiff": "aiff",
|
|
1442
|
-
"audio/x-flac": "flac",
|
|
1443
|
-
"audio/x-m4a": "m4a",
|
|
1444
|
-
"audio/x-wav": "wav",
|
|
1445
|
-
"audio/vnd.wave": "wav"
|
|
1446
|
-
};
|
|
1447
|
-
var FILE_EXTENSION_FORMAT_MAP = {
|
|
1448
|
-
".aac": "aac",
|
|
1449
|
-
".aif": "aiff",
|
|
1450
|
-
".aiff": "aiff",
|
|
1451
|
-
".flac": "flac",
|
|
1452
|
-
".m4a": "m4a",
|
|
1453
|
-
".mp3": "mp3",
|
|
1454
|
-
".oga": "ogg",
|
|
1455
|
-
".ogg": "ogg",
|
|
1456
|
-
".wav": "wav"
|
|
1457
|
-
};
|
|
1458
|
-
function resolveAudioFormat(filename, mimeType) {
|
|
1459
|
-
const normalizedMimeType = mimeType?.trim().toLowerCase() || null;
|
|
1460
|
-
if (normalizedMimeType && MIME_TYPE_FORMAT_MAP[normalizedMimeType]) return MIME_TYPE_FORMAT_MAP[normalizedMimeType];
|
|
1461
|
-
const extension = extname(basename(filename).trim()).toLowerCase();
|
|
1462
|
-
if (extension && FILE_EXTENSION_FORMAT_MAP[extension]) return FILE_EXTENSION_FORMAT_MAP[extension];
|
|
1463
|
-
return "ogg";
|
|
1464
|
-
}
|
|
1465
|
-
function toBase64(data) {
|
|
1466
|
-
const bytes = toUint8Array(data);
|
|
1467
|
-
return Buffer.from(bytes).toString("base64");
|
|
1468
|
-
}
|
|
1469
|
-
function toUint8Array(data) {
|
|
1470
|
-
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1471
|
-
}
|
|
1472
|
-
function isOpenRouterSupportedAudioFormat(format) {
|
|
1473
|
-
return OPENROUTER_SUPPORTED_AUDIO_FORMATS.includes(format);
|
|
1474
|
-
}
|
|
1475
|
-
function buildTranscriptionPrompt(transcriptionPrompt) {
|
|
1476
|
-
const basePrompt = [
|
|
1477
|
-
"Transcribe the provided audio verbatim.",
|
|
1478
|
-
"Return only the transcript text.",
|
|
1479
|
-
"Do not translate, summarize, explain, or add speaker labels.",
|
|
1480
|
-
"If the audio is empty or unintelligible, return an empty string."
|
|
1481
|
-
].join(" ");
|
|
1482
|
-
return transcriptionPrompt ? `${basePrompt}\n\nAdditional instructions: ${transcriptionPrompt}` : basePrompt;
|
|
1483
|
-
}
|
|
1484
|
-
function extractTranscript(response) {
|
|
1485
|
-
const content = response.choices?.[0]?.message?.content;
|
|
1486
|
-
if (typeof content === "string") return content.trim();
|
|
1487
|
-
if (!Array.isArray(content)) return "";
|
|
1488
|
-
return content.flatMap((item) => isTextContentItem(item) ? [item.text.trim()] : []).filter((text) => text.length > 0).join("\n").trim();
|
|
1489
|
-
}
|
|
1490
|
-
function isTextContentItem(value) {
|
|
1491
|
-
return !!value && typeof value === "object" && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
|
|
1492
|
-
}
|
|
1493
|
-
function buildTranscriptionErrorMessage(error, context) {
|
|
1494
|
-
const parsedBody = parseJsonBody(extractStringField(error, "body"));
|
|
1495
|
-
const rawMessages = dedupeNonEmptyStrings([
|
|
1496
|
-
extractErrorMessage(error),
|
|
1497
|
-
extractErrorMessage(readField(error, "error")),
|
|
1498
|
-
extractErrorMessage(readField(error, "data")),
|
|
1499
|
-
extractErrorMessage(readField(readField(error, "data"), "error")),
|
|
1500
|
-
extractErrorMessage(parsedBody),
|
|
1501
|
-
extractErrorMessage(readField(parsedBody, "error")),
|
|
1502
|
-
extractMetadataRawMessage(error),
|
|
1503
|
-
extractMetadataRawMessage(parsedBody)
|
|
1504
|
-
]);
|
|
1505
|
-
const messages = rawMessages.some((message) => !isGenericProviderMessage(message)) ? rawMessages.filter((message) => !isGenericProviderMessage(message)) : rawMessages;
|
|
1506
|
-
const providerName = extractProviderName(error) ?? extractProviderName(parsedBody);
|
|
1507
|
-
const statusCode = extractNumericField(error, "statusCode") ?? extractNumericField(parsedBody, "statusCode");
|
|
1508
|
-
const errorCode = extractErrorCode(error, parsedBody);
|
|
1509
|
-
return joinNonEmptyParts$1([
|
|
1510
|
-
...messages,
|
|
1511
|
-
`model: ${context.model}`,
|
|
1512
|
-
`format: ${context.format}`,
|
|
1513
|
-
providerName ? `provider: ${providerName}` : null,
|
|
1514
|
-
statusCode !== null ? `status: ${statusCode}` : null,
|
|
1515
|
-
errorCode !== null ? `code: ${errorCode}` : null
|
|
1516
|
-
]) ?? "Failed to reach OpenRouter voice transcription.";
|
|
1517
|
-
}
|
|
1518
|
-
function extractErrorMessage(error) {
|
|
1519
|
-
if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
|
|
1520
|
-
return extractStringField(error, "message");
|
|
1521
|
-
}
|
|
1522
|
-
function extractMetadataRawMessage(value) {
|
|
1523
|
-
const raw = extractStringField(readField(value, "metadata") ?? readField(readField(value, "error"), "metadata") ?? readField(readField(readField(value, "data"), "error"), "metadata"), "raw");
|
|
1524
|
-
if (!raw) return null;
|
|
1525
|
-
return raw.length <= 280 ? raw : `${raw.slice(0, 277)}...`;
|
|
1526
|
-
}
|
|
1527
|
-
function extractProviderName(value) {
|
|
1528
|
-
const candidates = [
|
|
1529
|
-
readField(value, "metadata"),
|
|
1530
|
-
readField(readField(value, "error"), "metadata"),
|
|
1531
|
-
readField(readField(readField(value, "data"), "error"), "metadata")
|
|
1532
|
-
];
|
|
1533
|
-
for (const candidate of candidates) {
|
|
1534
|
-
const providerName = extractStringField(candidate, "provider_name") ?? extractStringField(candidate, "providerName") ?? extractStringField(candidate, "provider");
|
|
1535
|
-
if (providerName) return providerName;
|
|
1536
|
-
}
|
|
1537
|
-
return null;
|
|
1538
|
-
}
|
|
1539
|
-
function extractErrorCode(...values) {
|
|
1540
|
-
for (const value of values) {
|
|
1541
|
-
const candidates = [
|
|
1542
|
-
value,
|
|
1543
|
-
readField(value, "error"),
|
|
1544
|
-
readField(value, "data"),
|
|
1545
|
-
readField(readField(value, "data"), "error")
|
|
1546
|
-
];
|
|
1547
|
-
for (const candidate of candidates) {
|
|
1548
|
-
const code = extractNumericField(candidate, "code");
|
|
1549
|
-
if (code !== null) return code;
|
|
1550
|
-
}
|
|
1551
|
-
}
|
|
1552
|
-
return null;
|
|
1553
|
-
}
|
|
1554
|
-
function extractNumericField(value, fieldName) {
|
|
1555
|
-
if (!value || typeof value !== "object" || !(fieldName in value)) return null;
|
|
1556
|
-
const fieldValue = value[fieldName];
|
|
1557
|
-
return typeof fieldValue === "number" && Number.isFinite(fieldValue) ? fieldValue : null;
|
|
1558
|
-
}
|
|
1559
|
-
function extractStringField(value, fieldName) {
|
|
1560
|
-
if (!value || typeof value !== "object" || !(fieldName in value)) return null;
|
|
1561
|
-
const fieldValue = value[fieldName];
|
|
1562
|
-
return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue.trim() : null;
|
|
1563
|
-
}
|
|
1564
|
-
function readField(value, fieldName) {
|
|
1565
|
-
return value && typeof value === "object" && fieldName in value ? value[fieldName] : null;
|
|
1566
|
-
}
|
|
1567
|
-
function parseJsonBody(body) {
|
|
1568
|
-
if (!body) return null;
|
|
1569
|
-
try {
|
|
1570
|
-
return JSON.parse(body);
|
|
1571
|
-
} catch {
|
|
1572
|
-
return null;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
function dedupeNonEmptyStrings(values) {
|
|
1576
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1577
|
-
const result = [];
|
|
1578
|
-
for (const value of values) {
|
|
1579
|
-
const normalized = value?.trim();
|
|
1580
|
-
if (!normalized) continue;
|
|
1581
|
-
const key = normalized.toLowerCase();
|
|
1582
|
-
if (seen.has(key)) continue;
|
|
1583
|
-
seen.add(key);
|
|
1584
|
-
result.push(normalized);
|
|
1585
|
-
}
|
|
1586
|
-
return result;
|
|
1587
|
-
}
|
|
1588
|
-
function joinNonEmptyParts$1(parts) {
|
|
1589
|
-
const filtered = parts.map((part) => part?.trim()).filter((part) => !!part);
|
|
1590
|
-
return filtered.length > 0 ? filtered.join(" | ") : null;
|
|
1591
|
-
}
|
|
1592
|
-
function isGenericProviderMessage(message) {
|
|
1593
|
-
const normalized = message.trim().toLowerCase();
|
|
1594
|
-
return normalized === "provider returned error" || normalized === "failed to reach openrouter voice transcription.";
|
|
1595
|
-
}
|
|
1596
|
-
//#endregion
|
|
1597
|
-
//#region src/services/voice-transcription/voice-transcription.service.ts
|
|
1598
|
-
var VoiceTranscriptionService = class {
|
|
1599
|
-
constructor(client) {
|
|
1600
|
-
this.client = client;
|
|
1601
|
-
}
|
|
1602
|
-
getStatus() {
|
|
1603
|
-
return this.client.getStatus();
|
|
1604
|
-
}
|
|
1605
|
-
async transcribeVoice(input) {
|
|
1606
|
-
const text = (await this.client.transcribe(input)).text.trim();
|
|
1607
|
-
if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
|
|
1608
|
-
return { text };
|
|
1609
|
-
}
|
|
1610
|
-
};
|
|
1611
|
-
//#endregion
|
|
1612
1172
|
//#region src/use-cases/abort-prompt.usecase.ts
|
|
1613
1173
|
var AbortPromptUseCase = class {
|
|
1614
1174
|
constructor(sessionRepo, opencodeClient) {
|
|
@@ -1714,14 +1274,13 @@ var GetPathUseCase = class {
|
|
|
1714
1274
|
//#endregion
|
|
1715
1275
|
//#region src/use-cases/get-status.usecase.ts
|
|
1716
1276
|
var GetStatusUseCase = class {
|
|
1717
|
-
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo
|
|
1277
|
+
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo) {
|
|
1718
1278
|
this.getHealthUseCase = getHealthUseCase;
|
|
1719
1279
|
this.getPathUseCase = getPathUseCase;
|
|
1720
1280
|
this.listLspUseCase = listLspUseCase;
|
|
1721
1281
|
this.listMcpUseCase = listMcpUseCase;
|
|
1722
1282
|
this.listSessionsUseCase = listSessionsUseCase;
|
|
1723
1283
|
this.sessionRepo = sessionRepo;
|
|
1724
|
-
this.voiceTranscriptionService = voiceTranscriptionService;
|
|
1725
1284
|
}
|
|
1726
1285
|
async execute(input) {
|
|
1727
1286
|
const [health, path, lsp, mcp] = await Promise.allSettled([
|
|
@@ -1736,7 +1295,6 @@ var GetStatusUseCase = class {
|
|
|
1736
1295
|
health: mapSettledResult(health),
|
|
1737
1296
|
path: pathResult,
|
|
1738
1297
|
plugins,
|
|
1739
|
-
voiceRecognition: this.voiceTranscriptionService.getStatus(),
|
|
1740
1298
|
workspace,
|
|
1741
1299
|
lsp: mapSettledResult(lsp),
|
|
1742
1300
|
mcp: mapSettledResult(mcp)
|
|
@@ -2342,7 +1900,6 @@ function resolveExtension(mimeType) {
|
|
|
2342
1900
|
}
|
|
2343
1901
|
//#endregion
|
|
2344
1902
|
//#region src/app/container.ts
|
|
2345
|
-
var require = createRequire(import.meta.url);
|
|
2346
1903
|
function createAppContainer(config, client) {
|
|
2347
1904
|
const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
|
|
2348
1905
|
return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
|
|
@@ -2361,7 +1918,6 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2361
1918
|
apiRoot: config.telegramApiRoot
|
|
2362
1919
|
});
|
|
2363
1920
|
const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
|
|
2364
|
-
const voiceTranscriptionService = new VoiceTranscriptionService(createVoiceTranscriptionClient(config.openrouter));
|
|
2365
1921
|
const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
|
|
2366
1922
|
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
2367
1923
|
const getHealthUseCase = new GetHealthUseCase(opencodeClient);
|
|
@@ -2370,7 +1926,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2370
1926
|
const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
|
|
2371
1927
|
const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
|
|
2372
1928
|
const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
|
|
2373
|
-
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo
|
|
1929
|
+
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
|
|
2374
1930
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
2375
1931
|
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
2376
1932
|
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
|
|
@@ -2394,7 +1950,6 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2394
1950
|
renameSessionUseCase,
|
|
2395
1951
|
sessionRepo,
|
|
2396
1952
|
sendPromptUseCase,
|
|
2397
|
-
voiceTranscriptionService,
|
|
2398
1953
|
switchAgentUseCase,
|
|
2399
1954
|
switchModelUseCase,
|
|
2400
1955
|
switchSessionUseCase,
|
|
@@ -2411,27 +1966,6 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2411
1966
|
}
|
|
2412
1967
|
};
|
|
2413
1968
|
}
|
|
2414
|
-
function createVoiceTranscriptionClient(config) {
|
|
2415
|
-
return config.configured && config.apiKey ? new OpenRouterVoiceTranscriptionClient({
|
|
2416
|
-
model: config.model,
|
|
2417
|
-
timeoutMs: config.timeoutMs,
|
|
2418
|
-
transcriptionPrompt: config.transcriptionPrompt
|
|
2419
|
-
}, new OpenRouter({
|
|
2420
|
-
apiKey: config.apiKey,
|
|
2421
|
-
timeoutMs: config.timeoutMs
|
|
2422
|
-
}), new FfmpegAudioTranscoder({
|
|
2423
|
-
ffmpegPath: loadBundledFfmpegPath(),
|
|
2424
|
-
timeoutMs: config.timeoutMs
|
|
2425
|
-
})) : new DisabledVoiceTranscriptionClient();
|
|
2426
|
-
}
|
|
2427
|
-
function loadBundledFfmpegPath() {
|
|
2428
|
-
try {
|
|
2429
|
-
const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg");
|
|
2430
|
-
return typeof ffmpegInstaller.path === "string" && ffmpegInstaller.path.trim().length > 0 ? ffmpegInstaller.path : null;
|
|
2431
|
-
} catch {
|
|
2432
|
-
return null;
|
|
2433
|
-
}
|
|
2434
|
-
}
|
|
2435
1969
|
//#endregion
|
|
2436
1970
|
//#region src/app/bootstrap.ts
|
|
2437
1971
|
function bootstrapPluginApp(client, configSource = {}, options = {}) {
|
|
@@ -2623,7 +2157,7 @@ var EN_BOT_COPY = {
|
|
|
2623
2157
|
"1. Run `/status` to confirm the server is ready.",
|
|
2624
2158
|
"2. Run `/new [title]` to create a fresh session.",
|
|
2625
2159
|
"",
|
|
2626
|
-
"Send a text
|
|
2160
|
+
"Send a text or image message directly."
|
|
2627
2161
|
] },
|
|
2628
2162
|
systemStatus: { title: "System Status" },
|
|
2629
2163
|
common: {
|
|
@@ -2662,12 +2196,7 @@ var EN_BOT_COPY = {
|
|
|
2662
2196
|
requestAborted: "Request was aborted.",
|
|
2663
2197
|
promptTimeout: "OpenCode request timed out.",
|
|
2664
2198
|
structuredOutput: "Structured output validation failed.",
|
|
2665
|
-
|
|
2666
|
-
voiceDownload: "Failed to download the Telegram voice file.",
|
|
2667
|
-
voiceTranscoding: "Voice audio preprocessing failed.",
|
|
2668
|
-
voiceTranscription: "Voice transcription failed.",
|
|
2669
|
-
voiceEmpty: "Voice transcription returned empty text.",
|
|
2670
|
-
voiceUnsupported: "Voice message file is too large or unsupported.",
|
|
2199
|
+
voiceUnsupported: "Voice messages are not supported. Send text or an image instead.",
|
|
2671
2200
|
imageDownload: "Failed to download the Telegram image file.",
|
|
2672
2201
|
imageUnsupported: "Image file is too large or unsupported.",
|
|
2673
2202
|
outputLength: "Reply hit the model output limit.",
|
|
@@ -2833,7 +2362,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2833
2362
|
"1. 先运行 `/status` 确认服务状态正常。",
|
|
2834
2363
|
"2. 运行 `/new [title]` 创建一个新会话。",
|
|
2835
2364
|
"",
|
|
2836
|
-
"
|
|
2365
|
+
"直接发送文本或图片消息即可。"
|
|
2837
2366
|
] },
|
|
2838
2367
|
systemStatus: { title: "系统状态" },
|
|
2839
2368
|
common: {
|
|
@@ -2872,12 +2401,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2872
2401
|
requestAborted: "请求已中止。",
|
|
2873
2402
|
promptTimeout: "OpenCode 响应超时。",
|
|
2874
2403
|
structuredOutput: "结构化输出校验失败。",
|
|
2875
|
-
|
|
2876
|
-
voiceDownload: "下载 Telegram 语音文件失败。",
|
|
2877
|
-
voiceTranscoding: "语音转码失败。",
|
|
2878
|
-
voiceTranscription: "语音转写失败。",
|
|
2879
|
-
voiceEmpty: "语音转写结果为空。",
|
|
2880
|
-
voiceUnsupported: "语音文件过大或不受支持。",
|
|
2404
|
+
voiceUnsupported: "暂不支持语音消息,请改发文本或图片。",
|
|
2881
2405
|
imageDownload: "下载 Telegram 图片文件失败。",
|
|
2882
2406
|
imageUnsupported: "图片文件过大或不受支持。",
|
|
2883
2407
|
outputLength: "回复触发了模型输出长度上限。",
|
|
@@ -3223,30 +2747,6 @@ function normalizeError(error, copy) {
|
|
|
3223
2747
|
message: copy.errors.structuredOutput,
|
|
3224
2748
|
cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
|
|
3225
2749
|
};
|
|
3226
|
-
if (isNamedError(error, "VoiceTranscriptionNotConfiguredError")) return {
|
|
3227
|
-
message: copy.errors.voiceNotConfigured,
|
|
3228
|
-
cause: extractMessage(error.data) ?? null
|
|
3229
|
-
};
|
|
3230
|
-
if (isNamedError(error, "TelegramFileDownloadError")) return {
|
|
3231
|
-
message: copy.errors.voiceDownload,
|
|
3232
|
-
cause: extractMessage(error.data) ?? null
|
|
3233
|
-
};
|
|
3234
|
-
if (isNamedError(error, "VoiceTranscodingFailedError")) return {
|
|
3235
|
-
message: copy.errors.voiceTranscoding,
|
|
3236
|
-
cause: extractMessage(error.data) ?? null
|
|
3237
|
-
};
|
|
3238
|
-
if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
|
|
3239
|
-
message: copy.errors.voiceTranscription,
|
|
3240
|
-
cause: extractMessage(error.data) ?? null
|
|
3241
|
-
};
|
|
3242
|
-
if (isNamedError(error, "VoiceTranscriptEmptyError")) return {
|
|
3243
|
-
message: copy.errors.voiceEmpty,
|
|
3244
|
-
cause: extractMessage(error.data) ?? null
|
|
3245
|
-
};
|
|
3246
|
-
if (isNamedError(error, "VoiceMessageUnsupportedError")) return {
|
|
3247
|
-
message: copy.errors.voiceUnsupported,
|
|
3248
|
-
cause: extractMessage(error.data) ?? null
|
|
3249
|
-
};
|
|
3250
2750
|
if (isNamedError(error, "ImageFileDownloadError")) return {
|
|
3251
2751
|
message: copy.errors.imageDownload,
|
|
3252
2752
|
cause: extractMessage(error.data) ?? null
|
|
@@ -3379,7 +2879,7 @@ function presentStatusMarkdownSection(title, lines) {
|
|
|
3379
2879
|
return [`## ${title}`, ...lines].join("\n");
|
|
3380
2880
|
}
|
|
3381
2881
|
function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
3382
|
-
const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))
|
|
2882
|
+
const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
|
|
3383
2883
|
if (input.health.status === "error") return [
|
|
3384
2884
|
...lines,
|
|
3385
2885
|
...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -3392,7 +2892,7 @@ function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
|
3392
2892
|
];
|
|
3393
2893
|
}
|
|
3394
2894
|
function presentStatusMarkdownOverviewLines(input, copy, layout) {
|
|
3395
|
-
const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))
|
|
2895
|
+
const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
|
|
3396
2896
|
if (input.health.status === "error") return [
|
|
3397
2897
|
...lines,
|
|
3398
2898
|
...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -3492,10 +2992,6 @@ function splitStatusLines(text) {
|
|
|
3492
2992
|
function formatHealthBadge(healthy, layout) {
|
|
3493
2993
|
return healthy ? "🟢" : layout.errorStatus;
|
|
3494
2994
|
}
|
|
3495
|
-
function formatVoiceRecognitionBadge(status, _layout) {
|
|
3496
|
-
if (status.status === "configured") return status.model ? `\uD83D\uDFE2 (${status.model})` : "🟡";
|
|
3497
|
-
return "⚪";
|
|
3498
|
-
}
|
|
3499
2995
|
function formatLspStatusBadge(status) {
|
|
3500
2996
|
switch (status.status) {
|
|
3501
2997
|
case "connected": return "🟢";
|
|
@@ -3564,7 +3060,6 @@ function getStatusLayoutCopy(copy) {
|
|
|
3564
3060
|
rootLabel: "Root",
|
|
3565
3061
|
statusLabel: "Status",
|
|
3566
3062
|
tbotVersionLabel: "opencode-tbot Version",
|
|
3567
|
-
voiceRecognitionLabel: "Voice Recognition",
|
|
3568
3063
|
workspaceTitle: "📁 Workspace"
|
|
3569
3064
|
};
|
|
3570
3065
|
return {
|
|
@@ -3587,7 +3082,6 @@ function getStatusLayoutCopy(copy) {
|
|
|
3587
3082
|
rootLabel: "根目录",
|
|
3588
3083
|
statusLabel: "状态",
|
|
3589
3084
|
tbotVersionLabel: "opencode-tbot版本",
|
|
3590
|
-
voiceRecognitionLabel: "语音识别",
|
|
3591
3085
|
workspaceTitle: "📁 工作区"
|
|
3592
3086
|
};
|
|
3593
3087
|
}
|
|
@@ -4737,13 +4231,13 @@ function isRecoverableStructuredOutputError(promptReply) {
|
|
|
4737
4231
|
}
|
|
4738
4232
|
//#endregion
|
|
4739
4233
|
//#region src/bot/handlers/file.handler.ts
|
|
4740
|
-
var TELEGRAM_MAX_DOWNLOAD_BYTES
|
|
4234
|
+
var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
4741
4235
|
async function handleImageMessage(ctx, dependencies) {
|
|
4742
4236
|
const image = resolveTelegramImage(ctx.message);
|
|
4743
4237
|
if (!image) return;
|
|
4744
4238
|
if (await replyIfSessionRenamePending(ctx, dependencies)) return;
|
|
4745
4239
|
await executePromptRequest(ctx, dependencies, async () => {
|
|
4746
|
-
if (typeof image.fileSize === "number" && image.fileSize > TELEGRAM_MAX_DOWNLOAD_BYTES
|
|
4240
|
+
if (typeof image.fileSize === "number" && image.fileSize > TELEGRAM_MAX_DOWNLOAD_BYTES) throw new ImageMessageUnsupportedError(`Image file size ${image.fileSize} exceeds the Telegram download limit of ${TELEGRAM_MAX_DOWNLOAD_BYTES} bytes.`);
|
|
4747
4241
|
const filePath = (await ctx.getFile()).file_path?.trim();
|
|
4748
4242
|
if (!filePath) throw new ImageMessageUnsupportedError("Telegram did not provide a downloadable image file path.");
|
|
4749
4243
|
return {
|
|
@@ -4809,39 +4303,17 @@ function registerMessageHandler(bot, dependencies) {
|
|
|
4809
4303
|
}
|
|
4810
4304
|
//#endregion
|
|
4811
4305
|
//#region src/bot/handlers/voice.handler.ts
|
|
4812
|
-
var DEFAULT_VOICE_FILE_NAME = "telegram-voice.ogg";
|
|
4813
|
-
var DEFAULT_VOICE_MIME_TYPE = "audio/ogg";
|
|
4814
|
-
var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
4815
4306
|
async function handleVoiceMessage(ctx, dependencies) {
|
|
4816
4307
|
if (!ctx.message.voice) return;
|
|
4817
4308
|
if (await replyIfSessionRenamePending(ctx, dependencies)) return;
|
|
4818
|
-
await
|
|
4819
|
-
|
|
4820
|
-
if (!voice) throw new VoiceMessageUnsupportedError("Telegram voice payload is missing.");
|
|
4821
|
-
if (typeof voice.file_size === "number" && voice.file_size > TELEGRAM_MAX_DOWNLOAD_BYTES) throw new VoiceMessageUnsupportedError(`Voice file size ${voice.file_size} exceeds the Telegram download limit of ${TELEGRAM_MAX_DOWNLOAD_BYTES} bytes.`);
|
|
4822
|
-
const filePath = (await ctx.getFile()).file_path?.trim();
|
|
4823
|
-
if (!filePath) throw new VoiceMessageUnsupportedError("Telegram did not provide a downloadable voice file path.");
|
|
4824
|
-
const download = await dependencies.telegramFileClient.downloadFile({ filePath });
|
|
4825
|
-
const mimeType = voice.mime_type?.trim() || download.mimeType || DEFAULT_VOICE_MIME_TYPE;
|
|
4826
|
-
const filename = resolveVoiceFilename(filePath);
|
|
4827
|
-
return { text: (await dependencies.voiceTranscriptionService.transcribeVoice({
|
|
4828
|
-
data: download.data,
|
|
4829
|
-
filename,
|
|
4830
|
-
mimeType
|
|
4831
|
-
})).text };
|
|
4832
|
-
});
|
|
4309
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4310
|
+
await ctx.reply(copy.errors.voiceUnsupported);
|
|
4833
4311
|
}
|
|
4834
4312
|
function registerVoiceHandler(bot, dependencies) {
|
|
4835
4313
|
bot.on("message:voice", async (ctx) => {
|
|
4836
4314
|
await handleVoiceMessage(ctx, dependencies);
|
|
4837
4315
|
});
|
|
4838
4316
|
}
|
|
4839
|
-
function resolveVoiceFilename(filePath) {
|
|
4840
|
-
const normalizedPath = filePath.trim();
|
|
4841
|
-
if (!normalizedPath) return DEFAULT_VOICE_FILE_NAME;
|
|
4842
|
-
const filename = normalizedPath.split("/").at(-1)?.trim();
|
|
4843
|
-
return filename && filename.length > 0 ? filename : DEFAULT_VOICE_FILE_NAME;
|
|
4844
|
-
}
|
|
4845
4317
|
//#endregion
|
|
4846
4318
|
//#region src/bot/middlewares/auth.ts
|
|
4847
4319
|
function createAuthMiddleware(allowedChatIds) {
|