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/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-DA71_jD3.js";
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 { basename, dirname, extname, isAbsolute, join } from "node:path";
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$1(error) ?? "Failed to download the Telegram voice file.");
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 voice file.");
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$1(error) {
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, voiceTranscriptionService) {
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, voiceTranscriptionService);
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 = ["en", "zh-CN"];
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, image, or voice message directly."
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
- voiceNotConfigured: "Voice transcription is not configured.",
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
- voiceNotConfigured: "未配置语音转写服务。",
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
- return normalizeBotLanguage(language) === "zh-CN" ? ZH_CN_BOT_COPY : EN_BOT_COPY;
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)), presentPlainStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, 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)), presentMarkdownStatusBullet(layout.voiceRecognitionLabel, formatVoiceRecognitionBadge(input.voiceRecognition, 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.systemStatus.title === BOT_COPY.systemStatus.title) return {
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$1 = 20 * 1024 * 1024;
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$1) throw new ImageMessageUnsupportedError(`Image file size ${image.fileSize} exceeds the Telegram download limit of ${TELEGRAM_MAX_DOWNLOAD_BYTES$1} 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 executePromptRequest(ctx, dependencies, async () => {
4622
- const voice = ctx.message.voice;
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) {