opencode-tbot 0.1.16 → 0.1.17
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/plugin.js +5 -202
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -2
package/dist/plugin.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-DA71_jD3.js";
|
|
2
|
-
import { createRequire } from "node:module";
|
|
3
2
|
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
4
3
|
import { basename, dirname, extname, isAbsolute, join } from "node:path";
|
|
5
4
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -7,7 +6,6 @@ import { z } from "zod";
|
|
|
7
6
|
import { OpenRouter } from "@openrouter/sdk";
|
|
8
7
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
9
8
|
import { randomUUID } from "node:crypto";
|
|
10
|
-
import { spawn } from "node:child_process";
|
|
11
9
|
import { run } from "@grammyjs/runner";
|
|
12
10
|
import { Bot, InlineKeyboard } from "grammy";
|
|
13
11
|
//#region src/infra/utils/redact.ts
|
|
@@ -1180,145 +1178,6 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
|
|
|
1180
1178
|
}
|
|
1181
1179
|
};
|
|
1182
1180
|
//#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
1181
|
//#region src/services/voice-transcription/openrouter-voice.client.ts
|
|
1323
1182
|
var VoiceTranscriptionNotConfiguredError = class extends Error {
|
|
1324
1183
|
data;
|
|
@@ -1356,16 +1215,11 @@ var DisabledVoiceTranscriptionClient = class {
|
|
|
1356
1215
|
}
|
|
1357
1216
|
};
|
|
1358
1217
|
var OpenRouterVoiceTranscriptionClient = class {
|
|
1359
|
-
audioTranscoder;
|
|
1360
1218
|
model;
|
|
1361
1219
|
sdk;
|
|
1362
1220
|
timeoutMs;
|
|
1363
1221
|
transcriptionPrompt;
|
|
1364
|
-
constructor(options, sdk
|
|
1365
|
-
ffmpegPath: null,
|
|
1366
|
-
timeoutMs: options.timeoutMs
|
|
1367
|
-
})) {
|
|
1368
|
-
this.audioTranscoder = audioTranscoder;
|
|
1222
|
+
constructor(options, sdk) {
|
|
1369
1223
|
this.model = options.model;
|
|
1370
1224
|
this.sdk = sdk;
|
|
1371
1225
|
this.timeoutMs = options.timeoutMs;
|
|
@@ -1378,8 +1232,8 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
1378
1232
|
};
|
|
1379
1233
|
}
|
|
1380
1234
|
async transcribe(input) {
|
|
1381
|
-
const
|
|
1382
|
-
const audioData = toBase64(
|
|
1235
|
+
const format = resolveAudioFormat(input.filename, input.mimeType);
|
|
1236
|
+
const audioData = toBase64(input.data);
|
|
1383
1237
|
const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
|
|
1384
1238
|
let response;
|
|
1385
1239
|
try {
|
|
@@ -1393,7 +1247,7 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
1393
1247
|
type: "input_audio",
|
|
1394
1248
|
inputAudio: {
|
|
1395
1249
|
data: audioData,
|
|
1396
|
-
format
|
|
1250
|
+
format
|
|
1397
1251
|
}
|
|
1398
1252
|
}]
|
|
1399
1253
|
}],
|
|
@@ -1403,53 +1257,23 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
1403
1257
|
} }, { timeoutMs: this.timeoutMs });
|
|
1404
1258
|
} catch (error) {
|
|
1405
1259
|
throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
|
|
1406
|
-
format
|
|
1260
|
+
format,
|
|
1407
1261
|
model: this.model
|
|
1408
1262
|
}));
|
|
1409
1263
|
}
|
|
1410
1264
|
return { text: extractTranscript(response) };
|
|
1411
1265
|
}
|
|
1412
1266
|
};
|
|
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
1267
|
var MIME_TYPE_FORMAT_MAP = {
|
|
1430
|
-
"audio/aac": "aac",
|
|
1431
|
-
"audio/aiff": "aiff",
|
|
1432
|
-
"audio/flac": "flac",
|
|
1433
|
-
"audio/m4a": "m4a",
|
|
1434
1268
|
"audio/mp3": "mp3",
|
|
1435
|
-
"audio/mp4": "m4a",
|
|
1436
1269
|
"audio/mpeg": "mp3",
|
|
1437
1270
|
"audio/ogg": "ogg",
|
|
1438
1271
|
"audio/wav": "wav",
|
|
1439
1272
|
"audio/wave": "wav",
|
|
1440
|
-
"audio/x-aac": "aac",
|
|
1441
|
-
"audio/x-aiff": "aiff",
|
|
1442
|
-
"audio/x-flac": "flac",
|
|
1443
|
-
"audio/x-m4a": "m4a",
|
|
1444
1273
|
"audio/x-wav": "wav",
|
|
1445
1274
|
"audio/vnd.wave": "wav"
|
|
1446
1275
|
};
|
|
1447
1276
|
var FILE_EXTENSION_FORMAT_MAP = {
|
|
1448
|
-
".aac": "aac",
|
|
1449
|
-
".aif": "aiff",
|
|
1450
|
-
".aiff": "aiff",
|
|
1451
|
-
".flac": "flac",
|
|
1452
|
-
".m4a": "m4a",
|
|
1453
1277
|
".mp3": "mp3",
|
|
1454
1278
|
".oga": "ogg",
|
|
1455
1279
|
".ogg": "ogg",
|
|
@@ -1469,9 +1293,6 @@ function toBase64(data) {
|
|
|
1469
1293
|
function toUint8Array(data) {
|
|
1470
1294
|
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1471
1295
|
}
|
|
1472
|
-
function isOpenRouterSupportedAudioFormat(format) {
|
|
1473
|
-
return OPENROUTER_SUPPORTED_AUDIO_FORMATS.includes(format);
|
|
1474
|
-
}
|
|
1475
1296
|
function buildTranscriptionPrompt(transcriptionPrompt) {
|
|
1476
1297
|
const basePrompt = [
|
|
1477
1298
|
"Transcribe the provided audio verbatim.",
|
|
@@ -2342,7 +2163,6 @@ function resolveExtension(mimeType) {
|
|
|
2342
2163
|
}
|
|
2343
2164
|
//#endregion
|
|
2344
2165
|
//#region src/app/container.ts
|
|
2345
|
-
var require = createRequire(import.meta.url);
|
|
2346
2166
|
function createAppContainer(config, client) {
|
|
2347
2167
|
const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
|
|
2348
2168
|
return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
|
|
@@ -2419,19 +2239,8 @@ function createVoiceTranscriptionClient(config) {
|
|
|
2419
2239
|
}, new OpenRouter({
|
|
2420
2240
|
apiKey: config.apiKey,
|
|
2421
2241
|
timeoutMs: config.timeoutMs
|
|
2422
|
-
}), new FfmpegAudioTranscoder({
|
|
2423
|
-
ffmpegPath: loadBundledFfmpegPath(),
|
|
2424
|
-
timeoutMs: config.timeoutMs
|
|
2425
2242
|
})) : new DisabledVoiceTranscriptionClient();
|
|
2426
2243
|
}
|
|
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
2244
|
//#endregion
|
|
2436
2245
|
//#region src/app/bootstrap.ts
|
|
2437
2246
|
function bootstrapPluginApp(client, configSource = {}, options = {}) {
|
|
@@ -2664,7 +2473,6 @@ var EN_BOT_COPY = {
|
|
|
2664
2473
|
structuredOutput: "Structured output validation failed.",
|
|
2665
2474
|
voiceNotConfigured: "Voice transcription is not configured.",
|
|
2666
2475
|
voiceDownload: "Failed to download the Telegram voice file.",
|
|
2667
|
-
voiceTranscoding: "Voice audio preprocessing failed.",
|
|
2668
2476
|
voiceTranscription: "Voice transcription failed.",
|
|
2669
2477
|
voiceEmpty: "Voice transcription returned empty text.",
|
|
2670
2478
|
voiceUnsupported: "Voice message file is too large or unsupported.",
|
|
@@ -2874,7 +2682,6 @@ var ZH_CN_BOT_COPY = {
|
|
|
2874
2682
|
structuredOutput: "结构化输出校验失败。",
|
|
2875
2683
|
voiceNotConfigured: "未配置语音转写服务。",
|
|
2876
2684
|
voiceDownload: "下载 Telegram 语音文件失败。",
|
|
2877
|
-
voiceTranscoding: "语音转码失败。",
|
|
2878
2685
|
voiceTranscription: "语音转写失败。",
|
|
2879
2686
|
voiceEmpty: "语音转写结果为空。",
|
|
2880
2687
|
voiceUnsupported: "语音文件过大或不受支持。",
|
|
@@ -3231,10 +3038,6 @@ function normalizeError(error, copy) {
|
|
|
3231
3038
|
message: copy.errors.voiceDownload,
|
|
3232
3039
|
cause: extractMessage(error.data) ?? null
|
|
3233
3040
|
};
|
|
3234
|
-
if (isNamedError(error, "VoiceTranscodingFailedError")) return {
|
|
3235
|
-
message: copy.errors.voiceTranscoding,
|
|
3236
|
-
cause: extractMessage(error.data) ?? null
|
|
3237
|
-
};
|
|
3238
3041
|
if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
|
|
3239
3042
|
message: copy.errors.voiceTranscription,
|
|
3240
3043
|
cause: extractMessage(error.data) ?? null
|