opencode-tbot 0.1.17 → 0.1.20
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 +164 -0
- package/README.md +51 -46
- package/README.zh-CN.md +51 -46
- 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 +263 -353
- 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 = {}) {
|
|
@@ -2411,8 +2136,13 @@ function toResolvedApproval(approval, reply) {
|
|
|
2411
2136
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2412
2137
|
};
|
|
2413
2138
|
}
|
|
2414
|
-
var SUPPORTED_BOT_LANGUAGES = [
|
|
2139
|
+
var SUPPORTED_BOT_LANGUAGES = [
|
|
2140
|
+
"en",
|
|
2141
|
+
"zh-CN",
|
|
2142
|
+
"ja"
|
|
2143
|
+
];
|
|
2415
2144
|
var EN_BOT_COPY = {
|
|
2145
|
+
locale: "en",
|
|
2416
2146
|
commands: {
|
|
2417
2147
|
start: "Welcome and quick start",
|
|
2418
2148
|
status: "Show system status",
|
|
@@ -2432,7 +2162,7 @@ var EN_BOT_COPY = {
|
|
|
2432
2162
|
"1. Run `/status` to confirm the server is ready.",
|
|
2433
2163
|
"2. Run `/new [title]` to create a fresh session.",
|
|
2434
2164
|
"",
|
|
2435
|
-
"Send a text
|
|
2165
|
+
"Send a text or image message directly."
|
|
2436
2166
|
] },
|
|
2437
2167
|
systemStatus: { title: "System Status" },
|
|
2438
2168
|
common: {
|
|
@@ -2471,11 +2201,7 @@ var EN_BOT_COPY = {
|
|
|
2471
2201
|
requestAborted: "Request was aborted.",
|
|
2472
2202
|
promptTimeout: "OpenCode request timed out.",
|
|
2473
2203
|
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.",
|
|
2204
|
+
voiceUnsupported: "Voice messages are not supported. Send text or an image instead.",
|
|
2479
2205
|
imageDownload: "Failed to download the Telegram image file.",
|
|
2480
2206
|
imageUnsupported: "Image file is too large or unsupported.",
|
|
2481
2207
|
outputLength: "Reply hit the model output limit.",
|
|
@@ -2617,11 +2343,13 @@ var EN_BOT_COPY = {
|
|
|
2617
2343
|
expired: "The language option is no longer available. Run /language again.",
|
|
2618
2344
|
labels: {
|
|
2619
2345
|
en: "English",
|
|
2620
|
-
"zh-CN": "Simplified Chinese"
|
|
2346
|
+
"zh-CN": "Simplified Chinese",
|
|
2347
|
+
ja: "Japanese"
|
|
2621
2348
|
}
|
|
2622
2349
|
}
|
|
2623
2350
|
};
|
|
2624
2351
|
var ZH_CN_BOT_COPY = {
|
|
2352
|
+
locale: "zh-CN",
|
|
2625
2353
|
commands: {
|
|
2626
2354
|
start: "查看欢迎与快速开始",
|
|
2627
2355
|
status: "查看系统状态",
|
|
@@ -2641,7 +2369,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2641
2369
|
"1. 先运行 `/status` 确认服务状态正常。",
|
|
2642
2370
|
"2. 运行 `/new [title]` 创建一个新会话。",
|
|
2643
2371
|
"",
|
|
2644
|
-
"
|
|
2372
|
+
"直接发送文本或图片消息即可。"
|
|
2645
2373
|
] },
|
|
2646
2374
|
systemStatus: { title: "系统状态" },
|
|
2647
2375
|
common: {
|
|
@@ -2680,11 +2408,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2680
2408
|
requestAborted: "请求已中止。",
|
|
2681
2409
|
promptTimeout: "OpenCode 响应超时。",
|
|
2682
2410
|
structuredOutput: "结构化输出校验失败。",
|
|
2683
|
-
|
|
2684
|
-
voiceDownload: "下载 Telegram 语音文件失败。",
|
|
2685
|
-
voiceTranscription: "语音转写失败。",
|
|
2686
|
-
voiceEmpty: "语音转写结果为空。",
|
|
2687
|
-
voiceUnsupported: "语音文件过大或不受支持。",
|
|
2411
|
+
voiceUnsupported: "暂不支持语音消息,请改发文本或图片。",
|
|
2688
2412
|
imageDownload: "下载 Telegram 图片文件失败。",
|
|
2689
2413
|
imageUnsupported: "图片文件过大或不受支持。",
|
|
2690
2414
|
outputLength: "回复触发了模型输出长度上限。",
|
|
@@ -2826,7 +2550,215 @@ var ZH_CN_BOT_COPY = {
|
|
|
2826
2550
|
expired: "该语言选项已不可用。请重新运行 /language。",
|
|
2827
2551
|
labels: {
|
|
2828
2552
|
en: "English",
|
|
2829
|
-
"zh-CN": "简体中文"
|
|
2553
|
+
"zh-CN": "简体中文",
|
|
2554
|
+
ja: "日语"
|
|
2555
|
+
}
|
|
2556
|
+
}
|
|
2557
|
+
};
|
|
2558
|
+
var JA_BOT_COPY = {
|
|
2559
|
+
locale: "ja",
|
|
2560
|
+
commands: {
|
|
2561
|
+
start: "ようこそ / クイックスタート",
|
|
2562
|
+
status: "システム状態を表示",
|
|
2563
|
+
new: "新しいセッションを作成",
|
|
2564
|
+
agents: "agent を表示して切り替え",
|
|
2565
|
+
sessions: "セッションを表示して切り替え",
|
|
2566
|
+
cancel: "名前変更を取り消すか実行中のリクエストを中止",
|
|
2567
|
+
model: "モデルを表示して切り替え",
|
|
2568
|
+
language: "言語を表示して切り替え"
|
|
2569
|
+
},
|
|
2570
|
+
start: { lines: [
|
|
2571
|
+
"# opencode-tbot へようこそ",
|
|
2572
|
+
"",
|
|
2573
|
+
"Telegram から OpenCode サーバーとやり取りできます。",
|
|
2574
|
+
"",
|
|
2575
|
+
"## クイックスタート",
|
|
2576
|
+
"1. `/status` を実行してサーバーの準備完了を確認します。",
|
|
2577
|
+
"2. `/new [title]` を実行して新しいセッションを作成します。",
|
|
2578
|
+
"",
|
|
2579
|
+
"そのままテキストまたは画像メッセージを送信できます。"
|
|
2580
|
+
] },
|
|
2581
|
+
systemStatus: { title: "システム状態" },
|
|
2582
|
+
common: {
|
|
2583
|
+
notSelected: "未選択",
|
|
2584
|
+
openCodeDefault: "未選択(OpenCode のデフォルトを使用)",
|
|
2585
|
+
previousPage: "前へ",
|
|
2586
|
+
nextPage: "次へ",
|
|
2587
|
+
page(currentPage, totalPages) {
|
|
2588
|
+
return `ページ ${currentPage}/${totalPages}`;
|
|
2589
|
+
}
|
|
2590
|
+
},
|
|
2591
|
+
status: {
|
|
2592
|
+
processing: "処理中...",
|
|
2593
|
+
alreadyProcessing: "別のリクエストがまだ実行中です。完了するまで新しいプロンプトを送信しないでください。"
|
|
2594
|
+
},
|
|
2595
|
+
prompt: { emptyResponse: "OpenCode から空の応答が返されました。" },
|
|
2596
|
+
replyMetrics: {
|
|
2597
|
+
durationLabel: "所要時間",
|
|
2598
|
+
tokensLabel: "トークン",
|
|
2599
|
+
totalLabel: "合計",
|
|
2600
|
+
inputLabel: "入力",
|
|
2601
|
+
outputLabel: "出力",
|
|
2602
|
+
reasoningLabel: "推論",
|
|
2603
|
+
cacheReadLabel: "キャッシュ読込",
|
|
2604
|
+
cacheWriteLabel: "キャッシュ書込",
|
|
2605
|
+
notAvailable: "該当なし"
|
|
2606
|
+
},
|
|
2607
|
+
abort: {
|
|
2608
|
+
noSession: "このチャットにはまだアクティブなセッションが紐付いていません。",
|
|
2609
|
+
notRunning: "現在のセッションで実行中のリクエストはありません。",
|
|
2610
|
+
aborted: "現在のセッションに中止シグナルを送信しました。"
|
|
2611
|
+
},
|
|
2612
|
+
errors: {
|
|
2613
|
+
unexpected: "予期しないエラーが発生しました。",
|
|
2614
|
+
providerAuth: "Provider の認証に失敗しました。",
|
|
2615
|
+
requestAborted: "リクエストは中止されました。",
|
|
2616
|
+
promptTimeout: "OpenCode リクエストがタイムアウトしました。",
|
|
2617
|
+
structuredOutput: "構造化出力の検証に失敗しました。",
|
|
2618
|
+
voiceUnsupported: "音声メッセージには対応していません。代わりにテキストまたは画像を送信してください。",
|
|
2619
|
+
imageDownload: "Telegram の画像ファイルをダウンロードできませんでした。",
|
|
2620
|
+
imageUnsupported: "画像ファイルが大きすぎるか、サポートされていません。",
|
|
2621
|
+
outputLength: "返信がモデルの出力上限に達しました。",
|
|
2622
|
+
contextOverflow: "会話がモデルのコンテキスト上限を超えました。",
|
|
2623
|
+
providerRequest: "Provider へのリクエストに失敗しました。",
|
|
2624
|
+
notFound: "要求されたリソースが見つかりませんでした。",
|
|
2625
|
+
badRequest: "リクエストは OpenCode に拒否されました。",
|
|
2626
|
+
causeLabel: "原因",
|
|
2627
|
+
retryableLabel: "再試行可能",
|
|
2628
|
+
statusCodeLabel: "ステータス"
|
|
2629
|
+
},
|
|
2630
|
+
health: {
|
|
2631
|
+
title: "サーバー状態",
|
|
2632
|
+
status(healthy) {
|
|
2633
|
+
return `状態: ${healthy ? "正常" : "異常"}`;
|
|
2634
|
+
},
|
|
2635
|
+
version(version) {
|
|
2636
|
+
return `バージョン: ${version}`;
|
|
2637
|
+
}
|
|
2638
|
+
},
|
|
2639
|
+
path: {
|
|
2640
|
+
title: "現在のパス",
|
|
2641
|
+
home(path) {
|
|
2642
|
+
return `ホーム: ${path}`;
|
|
2643
|
+
},
|
|
2644
|
+
state(path) {
|
|
2645
|
+
return `状態: ${path}`;
|
|
2646
|
+
},
|
|
2647
|
+
config(path) {
|
|
2648
|
+
return `設定: ${path}`;
|
|
2649
|
+
},
|
|
2650
|
+
worktree(path) {
|
|
2651
|
+
return `ワークツリー: ${path}`;
|
|
2652
|
+
},
|
|
2653
|
+
directory(path) {
|
|
2654
|
+
return `現在の作業ディレクトリ: ${path}`;
|
|
2655
|
+
}
|
|
2656
|
+
},
|
|
2657
|
+
sessions: {
|
|
2658
|
+
none: "現在のプロジェクトで利用できるセッションはありません。",
|
|
2659
|
+
title: "セッション一覧",
|
|
2660
|
+
actionTitle: "セッション操作",
|
|
2661
|
+
chooseAction: "このセッションに対する操作を選択してください。",
|
|
2662
|
+
currentProject(worktree) {
|
|
2663
|
+
return `現在のプロジェクト: ${worktree}`;
|
|
2664
|
+
},
|
|
2665
|
+
currentSession(session) {
|
|
2666
|
+
return `現在のセッション: ${session}`;
|
|
2667
|
+
},
|
|
2668
|
+
selectedSession(session) {
|
|
2669
|
+
return `選択中のセッション: ${session}`;
|
|
2670
|
+
},
|
|
2671
|
+
switched: "セッションを切り替えました。",
|
|
2672
|
+
created: "セッションを作成しました。",
|
|
2673
|
+
renamed: "セッション名を変更しました。",
|
|
2674
|
+
renameCancelled: "セッション名の変更を取り消しました。",
|
|
2675
|
+
renameEmpty: "セッション名は空にできません。新しい名前を送信するか /cancel を実行してください。",
|
|
2676
|
+
renameExpired: "このセッションはもう利用できません。/sessions を再実行してください。",
|
|
2677
|
+
renamePendingInput: "新しいセッション名の入力待ちです。プレーンテキストを送るか /cancel を実行してください。",
|
|
2678
|
+
renamePrompt(session) {
|
|
2679
|
+
return [
|
|
2680
|
+
`セッション名を変更: ${session}`,
|
|
2681
|
+
"次のテキストメッセージで新しいセッション名を送信してください。",
|
|
2682
|
+
"/cancel で取り消します。"
|
|
2683
|
+
].join("\n");
|
|
2684
|
+
},
|
|
2685
|
+
switchAction: "切り替え",
|
|
2686
|
+
renameAction: "名前変更",
|
|
2687
|
+
backToList: "戻る",
|
|
2688
|
+
expired: "このセッションはもう利用できません。/sessions を再実行してください。"
|
|
2689
|
+
},
|
|
2690
|
+
lsp: {
|
|
2691
|
+
none: "現在のプロジェクトで LSP サーバーは検出されませんでした。",
|
|
2692
|
+
title: "LSP サーバー",
|
|
2693
|
+
currentProject(worktree) {
|
|
2694
|
+
return `現在のプロジェクト: ${worktree}`;
|
|
2695
|
+
},
|
|
2696
|
+
connected: "接続済み",
|
|
2697
|
+
error: "エラー"
|
|
2698
|
+
},
|
|
2699
|
+
mcp: {
|
|
2700
|
+
none: "現在のプロジェクトで MCP サーバーは設定されていません。",
|
|
2701
|
+
title: "MCP サーバー",
|
|
2702
|
+
currentProject(worktree) {
|
|
2703
|
+
return `現在のプロジェクト: ${worktree}`;
|
|
2704
|
+
},
|
|
2705
|
+
connected: "接続済み",
|
|
2706
|
+
disabled: "無効",
|
|
2707
|
+
needsAuth: "認証が必要",
|
|
2708
|
+
failed(error) {
|
|
2709
|
+
return `失敗: ${error}`;
|
|
2710
|
+
},
|
|
2711
|
+
needsClientRegistration(error) {
|
|
2712
|
+
return `クライアント登録が必要: ${error}`;
|
|
2713
|
+
}
|
|
2714
|
+
},
|
|
2715
|
+
agents: {
|
|
2716
|
+
none: "利用可能な agent はありません。",
|
|
2717
|
+
title: "agent 一覧",
|
|
2718
|
+
current(agent) {
|
|
2719
|
+
return `現在の agent: ${agent}`;
|
|
2720
|
+
},
|
|
2721
|
+
switched: "agent を切り替えました。",
|
|
2722
|
+
expired: "この agent はもう利用できません。/agents を再実行してください。"
|
|
2723
|
+
},
|
|
2724
|
+
models: {
|
|
2725
|
+
none: "利用可能なモデルはありません。",
|
|
2726
|
+
title: "モデル一覧",
|
|
2727
|
+
configuredOnly: "OpenCode と接続済み provider で現在利用可能なモデルのみ表示します。",
|
|
2728
|
+
current(model) {
|
|
2729
|
+
return `現在のモデル: ${model}`;
|
|
2730
|
+
},
|
|
2731
|
+
switched: "モデルを切り替えました。",
|
|
2732
|
+
currentReasoningLevel(variant) {
|
|
2733
|
+
return `現在の推論レベル: ${variant}`;
|
|
2734
|
+
},
|
|
2735
|
+
reasoningLevel(variant) {
|
|
2736
|
+
return `推論レベル: ${variant}`;
|
|
2737
|
+
},
|
|
2738
|
+
noReasoningLevels: "このモデルには選択可能な推論レベルがありません。",
|
|
2739
|
+
reasoningLevelsTitle: "推論レベル",
|
|
2740
|
+
model(model) {
|
|
2741
|
+
return `モデル: ${model}`;
|
|
2742
|
+
},
|
|
2743
|
+
modelNumber(modelIndex) {
|
|
2744
|
+
return `モデル番号: ${modelIndex}`;
|
|
2745
|
+
},
|
|
2746
|
+
expired: "このモデルはもう利用できません。/model を再実行してください。",
|
|
2747
|
+
reasoningLevelExpired: "この推論レベルはもう利用できません。/model を再実行してください。",
|
|
2748
|
+
defaultReasoningLevel: "デフォルト"
|
|
2749
|
+
},
|
|
2750
|
+
language: {
|
|
2751
|
+
title: "言語",
|
|
2752
|
+
choose: "bot のメニューと返信に使う表示言語を選択してください。",
|
|
2753
|
+
current(label) {
|
|
2754
|
+
return `現在の言語: ${label}`;
|
|
2755
|
+
},
|
|
2756
|
+
switched: "言語を切り替えました。",
|
|
2757
|
+
expired: "この言語オプションはもう利用できません。/language を再実行してください。",
|
|
2758
|
+
labels: {
|
|
2759
|
+
en: "English",
|
|
2760
|
+
"zh-CN": "简体中文",
|
|
2761
|
+
ja: "日本語"
|
|
2830
2762
|
}
|
|
2831
2763
|
}
|
|
2832
2764
|
};
|
|
@@ -2838,10 +2770,14 @@ function normalizeBotLanguage(value) {
|
|
|
2838
2770
|
if (!value) return "en";
|
|
2839
2771
|
const normalized = value.trim().toLowerCase();
|
|
2840
2772
|
if (normalized === "zh-cn" || normalized === "zh-hans" || normalized === "zh") return "zh-CN";
|
|
2773
|
+
if (normalized === "ja" || normalized === "ja-jp" || normalized === "ja_jp") return "ja";
|
|
2841
2774
|
return "en";
|
|
2842
2775
|
}
|
|
2843
2776
|
function getBotCopy(language = "en") {
|
|
2844
|
-
|
|
2777
|
+
const normalized = normalizeBotLanguage(language);
|
|
2778
|
+
if (normalized === "zh-CN") return ZH_CN_BOT_COPY;
|
|
2779
|
+
if (normalized === "ja") return JA_BOT_COPY;
|
|
2780
|
+
return EN_BOT_COPY;
|
|
2845
2781
|
}
|
|
2846
2782
|
function getLanguageLabel(language, copy = BOT_COPY) {
|
|
2847
2783
|
return copy.language.labels[language];
|
|
@@ -3030,26 +2966,6 @@ function normalizeError(error, copy) {
|
|
|
3030
2966
|
message: copy.errors.structuredOutput,
|
|
3031
2967
|
cause: joinNonEmptyParts([extractMessage(error.data), extractRetries(error.data)])
|
|
3032
2968
|
};
|
|
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
2969
|
if (isNamedError(error, "ImageFileDownloadError")) return {
|
|
3054
2970
|
message: copy.errors.imageDownload,
|
|
3055
2971
|
cause: extractMessage(error.data) ?? null
|
|
@@ -3182,7 +3098,7 @@ function presentStatusMarkdownSection(title, lines) {
|
|
|
3182
3098
|
return [`## ${title}`, ...lines].join("\n");
|
|
3183
3099
|
}
|
|
3184
3100
|
function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
3185
|
-
const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))
|
|
3101
|
+
const lines = [presentPlainStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
|
|
3186
3102
|
if (input.health.status === "error") return [
|
|
3187
3103
|
...lines,
|
|
3188
3104
|
...presentStatusPlainErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -3195,7 +3111,7 @@ function presentStatusPlainOverviewLines(input, copy, layout) {
|
|
|
3195
3111
|
];
|
|
3196
3112
|
}
|
|
3197
3113
|
function presentStatusMarkdownOverviewLines(input, copy, layout) {
|
|
3198
|
-
const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))
|
|
3114
|
+
const lines = [presentMarkdownStatusBullet(layout.connectivityLabel, input.health.status === "error" ? layout.errorStatus : formatHealthBadge(input.health.data.healthy, layout))];
|
|
3199
3115
|
if (input.health.status === "error") return [
|
|
3200
3116
|
...lines,
|
|
3201
3117
|
...presentStatusMarkdownErrorDetailLines(input.health.error, copy, layout),
|
|
@@ -3295,10 +3211,6 @@ function splitStatusLines(text) {
|
|
|
3295
3211
|
function formatHealthBadge(healthy, layout) {
|
|
3296
3212
|
return healthy ? "🟢" : layout.errorStatus;
|
|
3297
3213
|
}
|
|
3298
|
-
function formatVoiceRecognitionBadge(status, _layout) {
|
|
3299
|
-
if (status.status === "configured") return status.model ? `\uD83D\uDFE2 (${status.model})` : "🟡";
|
|
3300
|
-
return "⚪";
|
|
3301
|
-
}
|
|
3302
3214
|
function formatLspStatusBadge(status) {
|
|
3303
3215
|
switch (status.status) {
|
|
3304
3216
|
case "connected": return "🟢";
|
|
@@ -3347,7 +3259,7 @@ function normalizeStatusInlineValue(value) {
|
|
|
3347
3259
|
return formatStatusValue(value);
|
|
3348
3260
|
}
|
|
3349
3261
|
function getStatusLayoutCopy(copy) {
|
|
3350
|
-
if (copy.
|
|
3262
|
+
if (copy.locale === "en") return {
|
|
3351
3263
|
connectivityLabel: "Connectivity",
|
|
3352
3264
|
currentProjectLabel: "Current Project",
|
|
3353
3265
|
currentSessionLabel: "Current Session",
|
|
@@ -3367,9 +3279,30 @@ function getStatusLayoutCopy(copy) {
|
|
|
3367
3279
|
rootLabel: "Root",
|
|
3368
3280
|
statusLabel: "Status",
|
|
3369
3281
|
tbotVersionLabel: "opencode-tbot Version",
|
|
3370
|
-
voiceRecognitionLabel: "Voice Recognition",
|
|
3371
3282
|
workspaceTitle: "📁 Workspace"
|
|
3372
3283
|
};
|
|
3284
|
+
if (copy.locale === "ja") return {
|
|
3285
|
+
connectivityLabel: "接続性",
|
|
3286
|
+
currentProjectLabel: "現在のプロジェクト",
|
|
3287
|
+
currentSessionLabel: "現在のセッション",
|
|
3288
|
+
defaultSessionValue: "OpenCode のデフォルト",
|
|
3289
|
+
detailsLabel: "詳細",
|
|
3290
|
+
errorStatus: "🔴",
|
|
3291
|
+
lspTitle: "🧠 LSP",
|
|
3292
|
+
mcpFailedStatus: "🔴",
|
|
3293
|
+
mcpNotesLabel: "補足",
|
|
3294
|
+
mcpRegistrationRequiredStatus: "🟡",
|
|
3295
|
+
mcpTitle: "🔌 MCP",
|
|
3296
|
+
noPluginsMessage: "現在の OpenCode 設定にはプラグインが設定されていません。",
|
|
3297
|
+
noneStatus: "⚪",
|
|
3298
|
+
openCodeVersionLabel: "OpenCode バージョン",
|
|
3299
|
+
overviewTitle: "🖥️ 概要",
|
|
3300
|
+
pluginsTitle: "🧩 プラグイン",
|
|
3301
|
+
rootLabel: "ルート",
|
|
3302
|
+
statusLabel: "状態",
|
|
3303
|
+
tbotVersionLabel: "opencode-tbot バージョン",
|
|
3304
|
+
workspaceTitle: "📁 ワークスペース"
|
|
3305
|
+
};
|
|
3373
3306
|
return {
|
|
3374
3307
|
connectivityLabel: "连通性",
|
|
3375
3308
|
currentProjectLabel: "当前项目",
|
|
@@ -3390,7 +3323,6 @@ function getStatusLayoutCopy(copy) {
|
|
|
3390
3323
|
rootLabel: "根目录",
|
|
3391
3324
|
statusLabel: "状态",
|
|
3392
3325
|
tbotVersionLabel: "opencode-tbot版本",
|
|
3393
|
-
voiceRecognitionLabel: "语音识别",
|
|
3394
3326
|
workspaceTitle: "📁 工作区"
|
|
3395
3327
|
};
|
|
3396
3328
|
}
|
|
@@ -4540,13 +4472,13 @@ function isRecoverableStructuredOutputError(promptReply) {
|
|
|
4540
4472
|
}
|
|
4541
4473
|
//#endregion
|
|
4542
4474
|
//#region src/bot/handlers/file.handler.ts
|
|
4543
|
-
var TELEGRAM_MAX_DOWNLOAD_BYTES
|
|
4475
|
+
var TELEGRAM_MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
|
|
4544
4476
|
async function handleImageMessage(ctx, dependencies) {
|
|
4545
4477
|
const image = resolveTelegramImage(ctx.message);
|
|
4546
4478
|
if (!image) return;
|
|
4547
4479
|
if (await replyIfSessionRenamePending(ctx, dependencies)) return;
|
|
4548
4480
|
await executePromptRequest(ctx, dependencies, async () => {
|
|
4549
|
-
if (typeof image.fileSize === "number" && image.fileSize > TELEGRAM_MAX_DOWNLOAD_BYTES
|
|
4481
|
+
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
4482
|
const filePath = (await ctx.getFile()).file_path?.trim();
|
|
4551
4483
|
if (!filePath) throw new ImageMessageUnsupportedError("Telegram did not provide a downloadable image file path.");
|
|
4552
4484
|
return {
|
|
@@ -4612,39 +4544,17 @@ function registerMessageHandler(bot, dependencies) {
|
|
|
4612
4544
|
}
|
|
4613
4545
|
//#endregion
|
|
4614
4546
|
//#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
4547
|
async function handleVoiceMessage(ctx, dependencies) {
|
|
4619
4548
|
if (!ctx.message.voice) return;
|
|
4620
4549
|
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
|
-
});
|
|
4550
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4551
|
+
await ctx.reply(copy.errors.voiceUnsupported);
|
|
4636
4552
|
}
|
|
4637
4553
|
function registerVoiceHandler(bot, dependencies) {
|
|
4638
4554
|
bot.on("message:voice", async (ctx) => {
|
|
4639
4555
|
await handleVoiceMessage(ctx, dependencies);
|
|
4640
4556
|
});
|
|
4641
4557
|
}
|
|
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
4558
|
//#endregion
|
|
4649
4559
|
//#region src/bot/middlewares/auth.ts
|
|
4650
4560
|
function createAuthMiddleware(allowedChatIds) {
|