opencode-tbot 0.1.17 → 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 -348
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -2
- package/tbot.config.example.json +0 -5
- package/dist/assets/plugin-config-DA71_jD3.js.map +0 -1
package/dist/plugin.js
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-
|
|
1
|
+
import { c as loadAppConfig, i as preparePluginConfiguration, o as OPENCODE_TBOT_VERSION } from "./assets/plugin-config-B8ginwol.js";
|
|
2
2
|
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
|
-
import {
|
|
3
|
+
import { dirname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
5
5
|
import { z } from "zod";
|
|
6
|
-
import { OpenRouter } from "@openrouter/sdk";
|
|
7
6
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
8
7
|
import { randomUUID } from "node:crypto";
|
|
9
8
|
import { run } from "@grammyjs/runner";
|
|
@@ -1087,14 +1086,6 @@ var TelegramFileDownloadError = class extends Error {
|
|
|
1087
1086
|
this.data = { message };
|
|
1088
1087
|
}
|
|
1089
1088
|
};
|
|
1090
|
-
var VoiceMessageUnsupportedError = class extends Error {
|
|
1091
|
-
data;
|
|
1092
|
-
constructor(message) {
|
|
1093
|
-
super(message);
|
|
1094
|
-
this.name = "VoiceMessageUnsupportedError";
|
|
1095
|
-
this.data = { message };
|
|
1096
|
-
}
|
|
1097
|
-
};
|
|
1098
1089
|
var TelegramFileClient = class {
|
|
1099
1090
|
baseUrl;
|
|
1100
1091
|
fetchFn;
|
|
@@ -1109,11 +1100,11 @@ var TelegramFileClient = class {
|
|
|
1109
1100
|
try {
|
|
1110
1101
|
response = await this.fetchFn(new URL(filePath, this.baseUrl));
|
|
1111
1102
|
} catch (error) {
|
|
1112
|
-
throw new TelegramFileDownloadError(extractErrorMessage
|
|
1103
|
+
throw new TelegramFileDownloadError(extractErrorMessage(error) ?? "Failed to download the Telegram file.");
|
|
1113
1104
|
}
|
|
1114
1105
|
if (!response.ok) throw new TelegramFileDownloadError(await buildDownloadFailureMessage(response));
|
|
1115
1106
|
const data = new Uint8Array(await response.arrayBuffer());
|
|
1116
|
-
if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty
|
|
1107
|
+
if (data.byteLength === 0) throw new TelegramFileDownloadError("Telegram returned an empty file.");
|
|
1117
1108
|
return {
|
|
1118
1109
|
data,
|
|
1119
1110
|
mimeType: response.headers.get("content-type")
|
|
@@ -1135,7 +1126,7 @@ async function safeReadResponseText(response) {
|
|
|
1135
1126
|
return null;
|
|
1136
1127
|
}
|
|
1137
1128
|
}
|
|
1138
|
-
function extractErrorMessage
|
|
1129
|
+
function extractErrorMessage(error) {
|
|
1139
1130
|
return error instanceof Error && error.message.trim().length > 0 ? error.message.trim() : null;
|
|
1140
1131
|
}
|
|
1141
1132
|
//#endregion
|
|
@@ -1178,258 +1169,6 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
|
|
|
1178
1169
|
}
|
|
1179
1170
|
};
|
|
1180
1171
|
//#endregion
|
|
1181
|
-
//#region src/services/voice-transcription/openrouter-voice.client.ts
|
|
1182
|
-
var VoiceTranscriptionNotConfiguredError = class extends Error {
|
|
1183
|
-
data;
|
|
1184
|
-
constructor(message) {
|
|
1185
|
-
super(message);
|
|
1186
|
-
this.name = "VoiceTranscriptionNotConfiguredError";
|
|
1187
|
-
this.data = { message };
|
|
1188
|
-
}
|
|
1189
|
-
};
|
|
1190
|
-
var VoiceTranscriptionFailedError = class extends Error {
|
|
1191
|
-
data;
|
|
1192
|
-
constructor(message) {
|
|
1193
|
-
super(message);
|
|
1194
|
-
this.name = "VoiceTranscriptionFailedError";
|
|
1195
|
-
this.data = { message };
|
|
1196
|
-
}
|
|
1197
|
-
};
|
|
1198
|
-
var VoiceTranscriptEmptyError = class extends Error {
|
|
1199
|
-
data;
|
|
1200
|
-
constructor(message) {
|
|
1201
|
-
super(message);
|
|
1202
|
-
this.name = "VoiceTranscriptEmptyError";
|
|
1203
|
-
this.data = { message };
|
|
1204
|
-
}
|
|
1205
|
-
};
|
|
1206
|
-
var DisabledVoiceTranscriptionClient = class {
|
|
1207
|
-
getStatus() {
|
|
1208
|
-
return {
|
|
1209
|
-
status: "not_configured",
|
|
1210
|
-
model: null
|
|
1211
|
-
};
|
|
1212
|
-
}
|
|
1213
|
-
async transcribe() {
|
|
1214
|
-
throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
|
|
1215
|
-
}
|
|
1216
|
-
};
|
|
1217
|
-
var OpenRouterVoiceTranscriptionClient = class {
|
|
1218
|
-
model;
|
|
1219
|
-
sdk;
|
|
1220
|
-
timeoutMs;
|
|
1221
|
-
transcriptionPrompt;
|
|
1222
|
-
constructor(options, sdk) {
|
|
1223
|
-
this.model = options.model;
|
|
1224
|
-
this.sdk = sdk;
|
|
1225
|
-
this.timeoutMs = options.timeoutMs;
|
|
1226
|
-
this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
|
|
1227
|
-
}
|
|
1228
|
-
getStatus() {
|
|
1229
|
-
return {
|
|
1230
|
-
status: "configured",
|
|
1231
|
-
model: this.model
|
|
1232
|
-
};
|
|
1233
|
-
}
|
|
1234
|
-
async transcribe(input) {
|
|
1235
|
-
const format = resolveAudioFormat(input.filename, input.mimeType);
|
|
1236
|
-
const audioData = toBase64(input.data);
|
|
1237
|
-
const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
|
|
1238
|
-
let response;
|
|
1239
|
-
try {
|
|
1240
|
-
response = await this.sdk.chat.send({ chatGenerationParams: {
|
|
1241
|
-
messages: [{
|
|
1242
|
-
role: "user",
|
|
1243
|
-
content: [{
|
|
1244
|
-
type: "text",
|
|
1245
|
-
text: prompt
|
|
1246
|
-
}, {
|
|
1247
|
-
type: "input_audio",
|
|
1248
|
-
inputAudio: {
|
|
1249
|
-
data: audioData,
|
|
1250
|
-
format
|
|
1251
|
-
}
|
|
1252
|
-
}]
|
|
1253
|
-
}],
|
|
1254
|
-
model: this.model,
|
|
1255
|
-
stream: false,
|
|
1256
|
-
temperature: 0
|
|
1257
|
-
} }, { timeoutMs: this.timeoutMs });
|
|
1258
|
-
} catch (error) {
|
|
1259
|
-
throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
|
|
1260
|
-
format,
|
|
1261
|
-
model: this.model
|
|
1262
|
-
}));
|
|
1263
|
-
}
|
|
1264
|
-
return { text: extractTranscript(response) };
|
|
1265
|
-
}
|
|
1266
|
-
};
|
|
1267
|
-
var MIME_TYPE_FORMAT_MAP = {
|
|
1268
|
-
"audio/mp3": "mp3",
|
|
1269
|
-
"audio/mpeg": "mp3",
|
|
1270
|
-
"audio/ogg": "ogg",
|
|
1271
|
-
"audio/wav": "wav",
|
|
1272
|
-
"audio/wave": "wav",
|
|
1273
|
-
"audio/x-wav": "wav",
|
|
1274
|
-
"audio/vnd.wave": "wav"
|
|
1275
|
-
};
|
|
1276
|
-
var FILE_EXTENSION_FORMAT_MAP = {
|
|
1277
|
-
".mp3": "mp3",
|
|
1278
|
-
".oga": "ogg",
|
|
1279
|
-
".ogg": "ogg",
|
|
1280
|
-
".wav": "wav"
|
|
1281
|
-
};
|
|
1282
|
-
function resolveAudioFormat(filename, mimeType) {
|
|
1283
|
-
const normalizedMimeType = mimeType?.trim().toLowerCase() || null;
|
|
1284
|
-
if (normalizedMimeType && MIME_TYPE_FORMAT_MAP[normalizedMimeType]) return MIME_TYPE_FORMAT_MAP[normalizedMimeType];
|
|
1285
|
-
const extension = extname(basename(filename).trim()).toLowerCase();
|
|
1286
|
-
if (extension && FILE_EXTENSION_FORMAT_MAP[extension]) return FILE_EXTENSION_FORMAT_MAP[extension];
|
|
1287
|
-
return "ogg";
|
|
1288
|
-
}
|
|
1289
|
-
function toBase64(data) {
|
|
1290
|
-
const bytes = toUint8Array(data);
|
|
1291
|
-
return Buffer.from(bytes).toString("base64");
|
|
1292
|
-
}
|
|
1293
|
-
function toUint8Array(data) {
|
|
1294
|
-
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1295
|
-
}
|
|
1296
|
-
function buildTranscriptionPrompt(transcriptionPrompt) {
|
|
1297
|
-
const basePrompt = [
|
|
1298
|
-
"Transcribe the provided audio verbatim.",
|
|
1299
|
-
"Return only the transcript text.",
|
|
1300
|
-
"Do not translate, summarize, explain, or add speaker labels.",
|
|
1301
|
-
"If the audio is empty or unintelligible, return an empty string."
|
|
1302
|
-
].join(" ");
|
|
1303
|
-
return transcriptionPrompt ? `${basePrompt}\n\nAdditional instructions: ${transcriptionPrompt}` : basePrompt;
|
|
1304
|
-
}
|
|
1305
|
-
function extractTranscript(response) {
|
|
1306
|
-
const content = response.choices?.[0]?.message?.content;
|
|
1307
|
-
if (typeof content === "string") return content.trim();
|
|
1308
|
-
if (!Array.isArray(content)) return "";
|
|
1309
|
-
return content.flatMap((item) => isTextContentItem(item) ? [item.text.trim()] : []).filter((text) => text.length > 0).join("\n").trim();
|
|
1310
|
-
}
|
|
1311
|
-
function isTextContentItem(value) {
|
|
1312
|
-
return !!value && typeof value === "object" && "type" in value && value.type === "text" && "text" in value && typeof value.text === "string";
|
|
1313
|
-
}
|
|
1314
|
-
function buildTranscriptionErrorMessage(error, context) {
|
|
1315
|
-
const parsedBody = parseJsonBody(extractStringField(error, "body"));
|
|
1316
|
-
const rawMessages = dedupeNonEmptyStrings([
|
|
1317
|
-
extractErrorMessage(error),
|
|
1318
|
-
extractErrorMessage(readField(error, "error")),
|
|
1319
|
-
extractErrorMessage(readField(error, "data")),
|
|
1320
|
-
extractErrorMessage(readField(readField(error, "data"), "error")),
|
|
1321
|
-
extractErrorMessage(parsedBody),
|
|
1322
|
-
extractErrorMessage(readField(parsedBody, "error")),
|
|
1323
|
-
extractMetadataRawMessage(error),
|
|
1324
|
-
extractMetadataRawMessage(parsedBody)
|
|
1325
|
-
]);
|
|
1326
|
-
const messages = rawMessages.some((message) => !isGenericProviderMessage(message)) ? rawMessages.filter((message) => !isGenericProviderMessage(message)) : rawMessages;
|
|
1327
|
-
const providerName = extractProviderName(error) ?? extractProviderName(parsedBody);
|
|
1328
|
-
const statusCode = extractNumericField(error, "statusCode") ?? extractNumericField(parsedBody, "statusCode");
|
|
1329
|
-
const errorCode = extractErrorCode(error, parsedBody);
|
|
1330
|
-
return joinNonEmptyParts$1([
|
|
1331
|
-
...messages,
|
|
1332
|
-
`model: ${context.model}`,
|
|
1333
|
-
`format: ${context.format}`,
|
|
1334
|
-
providerName ? `provider: ${providerName}` : null,
|
|
1335
|
-
statusCode !== null ? `status: ${statusCode}` : null,
|
|
1336
|
-
errorCode !== null ? `code: ${errorCode}` : null
|
|
1337
|
-
]) ?? "Failed to reach OpenRouter voice transcription.";
|
|
1338
|
-
}
|
|
1339
|
-
function extractErrorMessage(error) {
|
|
1340
|
-
if (error instanceof Error && error.message.trim().length > 0) return error.message.trim();
|
|
1341
|
-
return extractStringField(error, "message");
|
|
1342
|
-
}
|
|
1343
|
-
function extractMetadataRawMessage(value) {
|
|
1344
|
-
const raw = extractStringField(readField(value, "metadata") ?? readField(readField(value, "error"), "metadata") ?? readField(readField(readField(value, "data"), "error"), "metadata"), "raw");
|
|
1345
|
-
if (!raw) return null;
|
|
1346
|
-
return raw.length <= 280 ? raw : `${raw.slice(0, 277)}...`;
|
|
1347
|
-
}
|
|
1348
|
-
function extractProviderName(value) {
|
|
1349
|
-
const candidates = [
|
|
1350
|
-
readField(value, "metadata"),
|
|
1351
|
-
readField(readField(value, "error"), "metadata"),
|
|
1352
|
-
readField(readField(readField(value, "data"), "error"), "metadata")
|
|
1353
|
-
];
|
|
1354
|
-
for (const candidate of candidates) {
|
|
1355
|
-
const providerName = extractStringField(candidate, "provider_name") ?? extractStringField(candidate, "providerName") ?? extractStringField(candidate, "provider");
|
|
1356
|
-
if (providerName) return providerName;
|
|
1357
|
-
}
|
|
1358
|
-
return null;
|
|
1359
|
-
}
|
|
1360
|
-
function extractErrorCode(...values) {
|
|
1361
|
-
for (const value of values) {
|
|
1362
|
-
const candidates = [
|
|
1363
|
-
value,
|
|
1364
|
-
readField(value, "error"),
|
|
1365
|
-
readField(value, "data"),
|
|
1366
|
-
readField(readField(value, "data"), "error")
|
|
1367
|
-
];
|
|
1368
|
-
for (const candidate of candidates) {
|
|
1369
|
-
const code = extractNumericField(candidate, "code");
|
|
1370
|
-
if (code !== null) return code;
|
|
1371
|
-
}
|
|
1372
|
-
}
|
|
1373
|
-
return null;
|
|
1374
|
-
}
|
|
1375
|
-
function extractNumericField(value, fieldName) {
|
|
1376
|
-
if (!value || typeof value !== "object" || !(fieldName in value)) return null;
|
|
1377
|
-
const fieldValue = value[fieldName];
|
|
1378
|
-
return typeof fieldValue === "number" && Number.isFinite(fieldValue) ? fieldValue : null;
|
|
1379
|
-
}
|
|
1380
|
-
function extractStringField(value, fieldName) {
|
|
1381
|
-
if (!value || typeof value !== "object" || !(fieldName in value)) return null;
|
|
1382
|
-
const fieldValue = value[fieldName];
|
|
1383
|
-
return typeof fieldValue === "string" && fieldValue.trim().length > 0 ? fieldValue.trim() : null;
|
|
1384
|
-
}
|
|
1385
|
-
function readField(value, fieldName) {
|
|
1386
|
-
return value && typeof value === "object" && fieldName in value ? value[fieldName] : null;
|
|
1387
|
-
}
|
|
1388
|
-
function parseJsonBody(body) {
|
|
1389
|
-
if (!body) return null;
|
|
1390
|
-
try {
|
|
1391
|
-
return JSON.parse(body);
|
|
1392
|
-
} catch {
|
|
1393
|
-
return null;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
function dedupeNonEmptyStrings(values) {
|
|
1397
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1398
|
-
const result = [];
|
|
1399
|
-
for (const value of values) {
|
|
1400
|
-
const normalized = value?.trim();
|
|
1401
|
-
if (!normalized) continue;
|
|
1402
|
-
const key = normalized.toLowerCase();
|
|
1403
|
-
if (seen.has(key)) continue;
|
|
1404
|
-
seen.add(key);
|
|
1405
|
-
result.push(normalized);
|
|
1406
|
-
}
|
|
1407
|
-
return result;
|
|
1408
|
-
}
|
|
1409
|
-
function joinNonEmptyParts$1(parts) {
|
|
1410
|
-
const filtered = parts.map((part) => part?.trim()).filter((part) => !!part);
|
|
1411
|
-
return filtered.length > 0 ? filtered.join(" | ") : null;
|
|
1412
|
-
}
|
|
1413
|
-
function isGenericProviderMessage(message) {
|
|
1414
|
-
const normalized = message.trim().toLowerCase();
|
|
1415
|
-
return normalized === "provider returned error" || normalized === "failed to reach openrouter voice transcription.";
|
|
1416
|
-
}
|
|
1417
|
-
//#endregion
|
|
1418
|
-
//#region src/services/voice-transcription/voice-transcription.service.ts
|
|
1419
|
-
var VoiceTranscriptionService = class {
|
|
1420
|
-
constructor(client) {
|
|
1421
|
-
this.client = client;
|
|
1422
|
-
}
|
|
1423
|
-
getStatus() {
|
|
1424
|
-
return this.client.getStatus();
|
|
1425
|
-
}
|
|
1426
|
-
async transcribeVoice(input) {
|
|
1427
|
-
const text = (await this.client.transcribe(input)).text.trim();
|
|
1428
|
-
if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
|
|
1429
|
-
return { text };
|
|
1430
|
-
}
|
|
1431
|
-
};
|
|
1432
|
-
//#endregion
|
|
1433
1172
|
//#region src/use-cases/abort-prompt.usecase.ts
|
|
1434
1173
|
var AbortPromptUseCase = class {
|
|
1435
1174
|
constructor(sessionRepo, opencodeClient) {
|
|
@@ -1535,14 +1274,13 @@ var GetPathUseCase = class {
|
|
|
1535
1274
|
//#endregion
|
|
1536
1275
|
//#region src/use-cases/get-status.usecase.ts
|
|
1537
1276
|
var GetStatusUseCase = class {
|
|
1538
|
-
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo
|
|
1277
|
+
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo) {
|
|
1539
1278
|
this.getHealthUseCase = getHealthUseCase;
|
|
1540
1279
|
this.getPathUseCase = getPathUseCase;
|
|
1541
1280
|
this.listLspUseCase = listLspUseCase;
|
|
1542
1281
|
this.listMcpUseCase = listMcpUseCase;
|
|
1543
1282
|
this.listSessionsUseCase = listSessionsUseCase;
|
|
1544
1283
|
this.sessionRepo = sessionRepo;
|
|
1545
|
-
this.voiceTranscriptionService = voiceTranscriptionService;
|
|
1546
1284
|
}
|
|
1547
1285
|
async execute(input) {
|
|
1548
1286
|
const [health, path, lsp, mcp] = await Promise.allSettled([
|
|
@@ -1557,7 +1295,6 @@ var GetStatusUseCase = class {
|
|
|
1557
1295
|
health: mapSettledResult(health),
|
|
1558
1296
|
path: pathResult,
|
|
1559
1297
|
plugins,
|
|
1560
|
-
voiceRecognition: this.voiceTranscriptionService.getStatus(),
|
|
1561
1298
|
workspace,
|
|
1562
1299
|
lsp: mapSettledResult(lsp),
|
|
1563
1300
|
mcp: mapSettledResult(mcp)
|
|
@@ -2181,7 +1918,6 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2181
1918
|
apiRoot: config.telegramApiRoot
|
|
2182
1919
|
});
|
|
2183
1920
|
const uploadFileUseCase = new UploadFileUseCase(telegramFileClient);
|
|
2184
|
-
const voiceTranscriptionService = new VoiceTranscriptionService(createVoiceTranscriptionClient(config.openrouter));
|
|
2185
1921
|
const abortPromptUseCase = new AbortPromptUseCase(sessionRepo, opencodeClient);
|
|
2186
1922
|
const createSessionUseCase = new CreateSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
2187
1923
|
const getHealthUseCase = new GetHealthUseCase(opencodeClient);
|
|
@@ -2190,7 +1926,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2190
1926
|
const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
|
|
2191
1927
|
const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
|
|
2192
1928
|
const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
|
|
2193
|
-
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo
|
|
1929
|
+
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo);
|
|
2194
1930
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
2195
1931
|
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
2196
1932
|
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
|
|
@@ -2214,7 +1950,6 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2214
1950
|
renameSessionUseCase,
|
|
2215
1951
|
sessionRepo,
|
|
2216
1952
|
sendPromptUseCase,
|
|
2217
|
-
voiceTranscriptionService,
|
|
2218
1953
|
switchAgentUseCase,
|
|
2219
1954
|
switchModelUseCase,
|
|
2220
1955
|
switchSessionUseCase,
|
|
@@ -2231,16 +1966,6 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2231
1966
|
}
|
|
2232
1967
|
};
|
|
2233
1968
|
}
|
|
2234
|
-
function createVoiceTranscriptionClient(config) {
|
|
2235
|
-
return config.configured && config.apiKey ? new OpenRouterVoiceTranscriptionClient({
|
|
2236
|
-
model: config.model,
|
|
2237
|
-
timeoutMs: config.timeoutMs,
|
|
2238
|
-
transcriptionPrompt: config.transcriptionPrompt
|
|
2239
|
-
}, new OpenRouter({
|
|
2240
|
-
apiKey: config.apiKey,
|
|
2241
|
-
timeoutMs: config.timeoutMs
|
|
2242
|
-
})) : new DisabledVoiceTranscriptionClient();
|
|
2243
|
-
}
|
|
2244
1969
|
//#endregion
|
|
2245
1970
|
//#region src/app/bootstrap.ts
|
|
2246
1971
|
function bootstrapPluginApp(client, configSource = {}, options = {}) {
|
|
@@ -2432,7 +2157,7 @@ var EN_BOT_COPY = {
|
|
|
2432
2157
|
"1. Run `/status` to confirm the server is ready.",
|
|
2433
2158
|
"2. Run `/new [title]` to create a fresh session.",
|
|
2434
2159
|
"",
|
|
2435
|
-
"Send a text
|
|
2160
|
+
"Send a text or image message directly."
|
|
2436
2161
|
] },
|
|
2437
2162
|
systemStatus: { title: "System Status" },
|
|
2438
2163
|
common: {
|
|
@@ -2471,11 +2196,7 @@ var EN_BOT_COPY = {
|
|
|
2471
2196
|
requestAborted: "Request was aborted.",
|
|
2472
2197
|
promptTimeout: "OpenCode request timed out.",
|
|
2473
2198
|
structuredOutput: "Structured output validation failed.",
|
|
2474
|
-
|
|
2475
|
-
voiceDownload: "Failed to download the Telegram voice file.",
|
|
2476
|
-
voiceTranscription: "Voice transcription failed.",
|
|
2477
|
-
voiceEmpty: "Voice transcription returned empty text.",
|
|
2478
|
-
voiceUnsupported: "Voice message file is too large or unsupported.",
|
|
2199
|
+
voiceUnsupported: "Voice messages are not supported. Send text or an image instead.",
|
|
2479
2200
|
imageDownload: "Failed to download the Telegram image file.",
|
|
2480
2201
|
imageUnsupported: "Image file is too large or unsupported.",
|
|
2481
2202
|
outputLength: "Reply hit the model output limit.",
|
|
@@ -2641,7 +2362,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2641
2362
|
"1. 先运行 `/status` 确认服务状态正常。",
|
|
2642
2363
|
"2. 运行 `/new [title]` 创建一个新会话。",
|
|
2643
2364
|
"",
|
|
2644
|
-
"
|
|
2365
|
+
"直接发送文本或图片消息即可。"
|
|
2645
2366
|
] },
|
|
2646
2367
|
systemStatus: { title: "系统状态" },
|
|
2647
2368
|
common: {
|
|
@@ -2680,11 +2401,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2680
2401
|
requestAborted: "请求已中止。",
|
|
2681
2402
|
promptTimeout: "OpenCode 响应超时。",
|
|
2682
2403
|
structuredOutput: "结构化输出校验失败。",
|
|
2683
|
-
|
|
2684
|
-
voiceDownload: "下载 Telegram 语音文件失败。",
|
|
2685
|
-
voiceTranscription: "语音转写失败。",
|
|
2686
|
-
voiceEmpty: "语音转写结果为空。",
|
|
2687
|
-
voiceUnsupported: "语音文件过大或不受支持。",
|
|
2404
|
+
voiceUnsupported: "暂不支持语音消息,请改发文本或图片。",
|
|
2688
2405
|
imageDownload: "下载 Telegram 图片文件失败。",
|
|
2689
2406
|
imageUnsupported: "图片文件过大或不受支持。",
|
|
2690
2407
|
outputLength: "回复触发了模型输出长度上限。",
|
|
@@ -3030,26 +2747,6 @@ function normalizeError(error, copy) {
|
|
|
3030
2747
|
message: copy.errors.structuredOutput,
|
|
3031
2748
|
cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
|
|
3032
2749
|
};
|
|
3033
|
-
if (isNamedError(error, "VoiceTranscriptionNotConfiguredError")) return {
|
|
3034
|
-
message: copy.errors.voiceNotConfigured,
|
|
3035
|
-
cause: extractMessage(error.data) ?? null
|
|
3036
|
-
};
|
|
3037
|
-
if (isNamedError(error, "TelegramFileDownloadError")) return {
|
|
3038
|
-
message: copy.errors.voiceDownload,
|
|
3039
|
-
cause: extractMessage(error.data) ?? null
|
|
3040
|
-
};
|
|
3041
|
-
if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
|
|
3042
|
-
message: copy.errors.voiceTranscription,
|
|
3043
|
-
cause: extractMessage(error.data) ?? null
|
|
3044
|
-
};
|
|
3045
|
-
if (isNamedError(error, "VoiceTranscriptEmptyError")) return {
|
|
3046
|
-
message: copy.errors.voiceEmpty,
|
|
3047
|
-
cause: extractMessage(error.data) ?? null
|
|
3048
|
-
};
|
|
3049
|
-
if (isNamedError(error, "VoiceMessageUnsupportedError")) return {
|
|
3050
|
-
message: copy.errors.voiceUnsupported,
|
|
3051
|
-
cause: extractMessage(error.data) ?? null
|
|
3052
|
-
};
|
|
3053
2750
|
if (isNamedError(error, "ImageFileDownloadError")) return {
|
|
3054
2751
|
message: copy.errors.imageDownload,
|
|
3055
2752
|
cause: extractMessage(error.data) ?? null
|
|
@@ -3182,7 +2879,7 @@ function presentStatusMarkdownSection(title, lines) {
|
|
|
3182
2879
|
return [`## ${title}`, ...lines].join("\n");
|
|
3183
2880
|
}
|
|
3184
2881
|
function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
3185
|
-
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))];
|
|
3186
2883
|
if (input.health.status === "error") return [
|
|
3187
2884
|
...lines,
|
|
3188
2885
|
...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -3195,7 +2892,7 @@ function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
|
3195
2892
|
];
|
|
3196
2893
|
}
|
|
3197
2894
|
function presentStatusMarkdownOverviewLines(input, copy, layout) {
|
|
3198
|
-
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))];
|
|
3199
2896
|
if (input.health.status === "error") return [
|
|
3200
2897
|
...lines,
|
|
3201
2898
|
...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -3295,10 +2992,6 @@ function splitStatusLines(text) {
|
|
|
3295
2992
|
function formatHealthBadge(healthy, layout) {
|
|
3296
2993
|
return healthy ? "🟢" : layout.errorStatus;
|
|
3297
2994
|
}
|
|
3298
|
-
function formatVoiceRecognitionBadge(status, _layout) {
|
|
3299
|
-
if (status.status === "configured") return status.model ? `\uD83D\uDFE2 (${status.model})` : "🟡";
|
|
3300
|
-
return "⚪";
|
|
3301
|
-
}
|
|
3302
2995
|
function formatLspStatusBadge(status) {
|
|
3303
2996
|
switch (status.status) {
|
|
3304
2997
|
case "connected": return "🟢";
|
|
@@ -3367,7 +3060,6 @@ function getStatusLayoutCopy(copy) {
|
|
|
3367
3060
|
rootLabel: "Root",
|
|
3368
3061
|
statusLabel: "Status",
|
|
3369
3062
|
tbotVersionLabel: "opencode-tbot Version",
|
|
3370
|
-
voiceRecognitionLabel: "Voice Recognition",
|
|
3371
3063
|
workspaceTitle: "📁 Workspace"
|
|
3372
3064
|
};
|
|
3373
3065
|
return {
|
|
@@ -3390,7 +3082,6 @@ function getStatusLayoutCopy(copy) {
|
|
|
3390
3082
|
rootLabel: "根目录",
|
|
3391
3083
|
statusLabel: "状态",
|
|
3392
3084
|
tbotVersionLabel: "opencode-tbot版本",
|
|
3393
|
-
voiceRecognitionLabel: "语音识别",
|
|
3394
3085
|
workspaceTitle: "📁 工作区"
|
|
3395
3086
|
};
|
|
3396
3087
|
}
|
|
@@ -4540,13 +4231,13 @@ function isRecoverableStructuredOutputError(promptReply) {
|
|
|
4540
4231
|
}
|
|
4541
4232
|
//#endregion
|
|
4542
4233
|
//#region src/bot/handlers/file.handler.ts
|
|
4543
|
-
var TELEGRAM_MAX_DOWNLOAD_BYTES
|
|
4234
|
+
var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
4544
4235
|
async function handleImageMessage(ctx, dependencies) {
|
|
4545
4236
|
const image = resolveTelegramImage(ctx.message);
|
|
4546
4237
|
if (!image) return;
|
|
4547
4238
|
if (await replyIfSessionRenamePending(ctx, dependencies)) return;
|
|
4548
4239
|
await executePromptRequest(ctx, dependencies, async () => {
|
|
4549
|
-
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.`);
|
|
4550
4241
|
const filePath = (await ctx.getFile()).file_path?.trim();
|
|
4551
4242
|
if (!filePath) throw new ImageMessageUnsupportedError("Telegram did not provide a downloadable image file path.");
|
|
4552
4243
|
return {
|
|
@@ -4612,39 +4303,17 @@ function registerMessageHandler(bot, dependencies) {
|
|
|
4612
4303
|
}
|
|
4613
4304
|
//#endregion
|
|
4614
4305
|
//#region src/bot/handlers/voice.handler.ts
|
|
4615
|
-
var DEFAULT_VOICE_FILE_NAME = "telegram-voice.ogg";
|
|
4616
|
-
var DEFAULT_VOICE_MIME_TYPE = "audio/ogg";
|
|
4617
|
-
var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
4618
4306
|
async function handleVoiceMessage(ctx, dependencies) {
|
|
4619
4307
|
if (!ctx.message.voice) return;
|
|
4620
4308
|
if (await replyIfSessionRenamePending(ctx, dependencies)) return;
|
|
4621
|
-
await
|
|
4622
|
-
|
|
4623
|
-
if (!voice) throw new VoiceMessageUnsupportedError("Telegram voice payload is missing.");
|
|
4624
|
-
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.`);
|
|
4625
|
-
const filePath = (await ctx.getFile()).file_path?.trim();
|
|
4626
|
-
if (!filePath) throw new VoiceMessageUnsupportedError("Telegram did not provide a downloadable voice file path.");
|
|
4627
|
-
const download = await dependencies.telegramFileClient.downloadFile({ filePath });
|
|
4628
|
-
const mimeType = voice.mime_type?.trim() || download.mimeType || DEFAULT_VOICE_MIME_TYPE;
|
|
4629
|
-
const filename = resolveVoiceFilename(filePath);
|
|
4630
|
-
return { text: (await dependencies.voiceTranscriptionService.transcribeVoice({
|
|
4631
|
-
data: download.data,
|
|
4632
|
-
filename,
|
|
4633
|
-
mimeType
|
|
4634
|
-
})).text };
|
|
4635
|
-
});
|
|
4309
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4310
|
+
await ctx.reply(copy.errors.voiceUnsupported);
|
|
4636
4311
|
}
|
|
4637
4312
|
function registerVoiceHandler(bot, dependencies) {
|
|
4638
4313
|
bot.on("message:voice", async (ctx) => {
|
|
4639
4314
|
await handleVoiceMessage(ctx, dependencies);
|
|
4640
4315
|
});
|
|
4641
4316
|
}
|
|
4642
|
-
function resolveVoiceFilename(filePath) {
|
|
4643
|
-
const normalizedPath = filePath.trim();
|
|
4644
|
-
if (!normalizedPath) return DEFAULT_VOICE_FILE_NAME;
|
|
4645
|
-
const filename = normalizedPath.split("/").at(-1)?.trim();
|
|
4646
|
-
return filename && filename.length > 0 ? filename : DEFAULT_VOICE_FILE_NAME;
|
|
4647
|
-
}
|
|
4648
4317
|
//#endregion
|
|
4649
4318
|
//#region src/bot/middlewares/auth.ts
|
|
4650
4319
|
function createAuthMiddleware(allowedChatIds) {
|