opencode-tbot 0.1.13 → 0.1.16
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/assets/{plugin-config-CGIe9zdA.js → plugin-config-DA71_jD3.js} +24 -5
- package/dist/assets/plugin-config-DA71_jD3.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/index.js +1 -1
- package/dist/plugin.js +216 -22
- package/dist/plugin.js.map +1 -1
- package/package.json +2 -1
- package/tbot.config.example.json +0 -1
- package/dist/assets/plugin-config-CGIe9zdA.js.map +0 -1
|
@@ -31,9 +31,9 @@ function loadAppConfig(configSource = {}, options = {}) {
|
|
|
31
31
|
return buildAppConfig(parseConfig(AppConfigSchema, configSource), options);
|
|
32
32
|
}
|
|
33
33
|
function buildAppConfig(data, options) {
|
|
34
|
-
const openRouterApiKey = normalizeOptionalString(data.openrouter.apiKey);
|
|
35
|
-
const openRouterModel = normalizeOptionalString(data.openrouter.model) ?? "openai/gpt-audio-mini";
|
|
36
|
-
const transcriptionPrompt = normalizeOptionalString(data.openrouter.transcriptionPrompt);
|
|
34
|
+
const openRouterApiKey = normalizeOptionalString$1(data.openrouter.apiKey);
|
|
35
|
+
const openRouterModel = normalizeOptionalString$1(data.openrouter.model) ?? "openai/gpt-audio-mini";
|
|
36
|
+
const transcriptionPrompt = normalizeOptionalString$1(data.openrouter.transcriptionPrompt);
|
|
37
37
|
return {
|
|
38
38
|
telegramBotToken: data.telegram.botToken,
|
|
39
39
|
telegramAllowedChatIds: data.telegram.allowedChatIds,
|
|
@@ -49,7 +49,7 @@ function buildAppConfig(data, options) {
|
|
|
49
49
|
}
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
-
function normalizeOptionalString(value) {
|
|
52
|
+
function normalizeOptionalString$1(value) {
|
|
53
53
|
const normalized = value.trim();
|
|
54
54
|
return normalized.length > 0 ? normalized : null;
|
|
55
55
|
}
|
|
@@ -93,6 +93,7 @@ async function preparePluginConfiguration(options) {
|
|
|
93
93
|
const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);
|
|
94
94
|
const [globalConfig, projectConfig] = await Promise.all([loadPluginConfigFile(globalConfigFilePath), loadPluginConfigFile(projectConfigFilePath)]);
|
|
95
95
|
const config = mergePluginConfigSources(globalConfig, projectConfig, options.config);
|
|
96
|
+
applyGlobalOpenRouterApiKey(config, globalConfig);
|
|
96
97
|
const configFilePath = await pathExists(projectConfigFilePath) ? projectConfigFilePath : globalConfigFilePath;
|
|
97
98
|
return {
|
|
98
99
|
cwd: options.cwd,
|
|
@@ -139,6 +140,20 @@ function mergePluginConfigSources(...sources) {
|
|
|
139
140
|
}
|
|
140
141
|
return merged;
|
|
141
142
|
}
|
|
143
|
+
function applyGlobalOpenRouterApiKey(config, globalConfig) {
|
|
144
|
+
const globalApiKey = normalizeOptionalString(globalConfig.openrouter?.apiKey);
|
|
145
|
+
if (!config.openrouter) {
|
|
146
|
+
if (!globalApiKey) return;
|
|
147
|
+
config.openrouter = { apiKey: globalApiKey };
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (globalApiKey) {
|
|
151
|
+
config.openrouter.apiKey = globalApiKey;
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
delete config.openrouter.apiKey;
|
|
155
|
+
if (Object.keys(config.openrouter).length === 0) delete config.openrouter;
|
|
156
|
+
}
|
|
142
157
|
function serializePluginConfig(config) {
|
|
143
158
|
return `${JSON.stringify(orderPluginConfig(config), null, 2)}\n`;
|
|
144
159
|
}
|
|
@@ -177,6 +192,10 @@ function orderPluginConfig(config) {
|
|
|
177
192
|
function isPlainObject(value) {
|
|
178
193
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
179
194
|
}
|
|
195
|
+
function normalizeOptionalString(value) {
|
|
196
|
+
const normalized = value?.trim();
|
|
197
|
+
return normalized ? normalized : null;
|
|
198
|
+
}
|
|
180
199
|
function isMissingFileError(error) {
|
|
181
200
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
182
201
|
}
|
|
@@ -195,4 +214,4 @@ async function pathExists(filePath) {
|
|
|
195
214
|
//#endregion
|
|
196
215
|
export { writePluginConfigFile as a, loadAppConfig as c, preparePluginConfiguration as i, getOpenCodeConfigFilePath as n, OPENCODE_TBOT_VERSION as o, mergePluginConfigSources as r, DEFAULT_TELEGRAM_API_ROOT as s, getGlobalPluginConfigFilePath as t };
|
|
197
216
|
|
|
198
|
-
//# sourceMappingURL=plugin-config-
|
|
217
|
+
//# sourceMappingURL=plugin-config-DA71_jD3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-config-DA71_jD3.js","names":[],"sources":["../../src/app/config.ts","../../src/app/package-info.ts","../../src/app/plugin-config.ts"],"sourcesContent":["import { resolve } from \"node:path\";\nimport { z } from \"zod\";\n\nexport const DEFAULT_OPENROUTER_MODEL = \"openai/gpt-audio-mini\";\nexport const DEFAULT_STATE_FILE_PATH = \"./data/opencode-tbot.state.json\";\nexport const DEFAULT_TELEGRAM_API_ROOT = \"https://api.telegram.org\";\n\nconst AllowedChatIdSchema = z.union([\n z.number().int(),\n z.string().regex(/^-?\\d+$/u).transform((value) => Number(value)),\n]);\n\nconst TelegramConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n botToken: z.string().trim().min(1),\n allowedChatIds: z.array(AllowedChatIdSchema).default([]),\n apiRoot: z.string().trim().url().default(DEFAULT_TELEGRAM_API_ROOT),\n }),\n);\n\nconst StateConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n path: z.string().trim().min(1).default(DEFAULT_STATE_FILE_PATH),\n }),\n);\n\nconst OpenRouterConfigSchema = z.preprocess(\n (value) => value ?? {},\n z.object({\n apiKey: z.string().default(\"\"),\n model: z.string().default(DEFAULT_OPENROUTER_MODEL),\n timeoutMs: z.coerce.number().int().positive().default(30_000),\n transcriptionPrompt: z.string().default(\"\"),\n }),\n);\n\nconst AppConfigSchema = z.object({\n telegram: TelegramConfigSchema,\n state: StateConfigSchema,\n openrouter: OpenRouterConfigSchema,\n logLevel: z.string().default(\"info\"),\n});\n\nexport interface PluginConfigSource {\n telegram?: {\n botToken?: string;\n allowedChatIds?: Array<number | string>;\n apiRoot?: string;\n [key: string]: unknown;\n };\n state?: {\n path?: string;\n [key: string]: unknown;\n };\n openrouter?: {\n apiKey?: string;\n model?: string;\n timeoutMs?: number;\n transcriptionPrompt?: string;\n [key: string]: unknown;\n };\n logLevel?: string;\n [key: string]: unknown;\n}\n\nexport interface AppOpenRouterConfig {\n configured: boolean;\n apiKey: string | null;\n model: string;\n timeoutMs: number;\n transcriptionPrompt: string | null;\n}\n\nexport interface AppConfig {\n telegramBotToken: string;\n telegramAllowedChatIds: number[];\n telegramApiRoot: string;\n logLevel: string;\n stateFilePath: string;\n openrouter: AppOpenRouterConfig;\n}\n\nexport interface LoadAppConfigOptions {\n cwd?: string;\n}\n\nexport function loadAppConfig(\n configSource: PluginConfigSource | undefined = {},\n options: LoadAppConfigOptions = {},\n): AppConfig {\n const parsed = parseConfig(AppConfigSchema, configSource);\n\n return buildAppConfig(parsed, options);\n}\n\nexport const loadPluginConfig = loadAppConfig;\n\nfunction buildAppConfig(\n data: z.infer<typeof AppConfigSchema>,\n options: LoadAppConfigOptions,\n): AppConfig {\n const openRouterApiKey = normalizeOptionalString(data.openrouter.apiKey);\n const openRouterModel = normalizeOptionalString(data.openrouter.model) ?? DEFAULT_OPENROUTER_MODEL;\n const transcriptionPrompt = normalizeOptionalString(data.openrouter.transcriptionPrompt);\n\n return {\n telegramBotToken: data.telegram.botToken,\n telegramAllowedChatIds: data.telegram.allowedChatIds,\n telegramApiRoot: normalizeApiRoot(data.telegram.apiRoot),\n logLevel: data.logLevel,\n stateFilePath: resolveStatePath(data, options.cwd ?? process.cwd()),\n openrouter: {\n configured: !!openRouterApiKey,\n apiKey: openRouterApiKey,\n model: openRouterModel,\n timeoutMs: data.openrouter.timeoutMs,\n transcriptionPrompt,\n },\n };\n}\n\nfunction normalizeOptionalString(value: string): string | null {\n const normalized = value.trim();\n\n return normalized.length > 0 ? normalized : null;\n}\n\nfunction resolveStatePath(\n data: z.infer<typeof AppConfigSchema>,\n cwd: string,\n): string {\n return resolve(cwd, data.state.path || DEFAULT_STATE_FILE_PATH);\n}\n\nfunction normalizeApiRoot(value: string): string {\n const normalized = value.trim();\n\n return normalized.endsWith(\"/\")\n ? normalized.slice(0, -1)\n : normalized;\n}\n\nfunction parseConfig<TSchema extends z.ZodTypeAny>(\n schema: TSchema,\n configSource: PluginConfigSource | undefined,\n): z.infer<TSchema> {\n const parsed = schema.safeParse(configSource ?? {});\n\n if (parsed.success) {\n return parsed.data;\n }\n\n throw new Error(\n `Invalid plugin configuration: ${JSON.stringify(parsed.error.flatten())}`,\n );\n}\n","import { existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nexport const OPENCODE_TBOT_VERSION = resolvePackageVersion();\n\nfunction resolvePackageVersion(): string {\n let directory = dirname(fileURLToPath(import.meta.url));\n\n while (true) {\n const packageFilePath = join(directory, \"package.json\");\n\n if (existsSync(packageFilePath)) {\n try {\n const parsed = JSON.parse(readFileSync(packageFilePath, \"utf8\")) as {\n version?: unknown;\n };\n\n if (typeof parsed.version === \"string\" && parsed.version.trim().length > 0) {\n return parsed.version;\n }\n } catch {\n // Fall through and continue searching parent directories.\n }\n }\n\n const parentDirectory = dirname(directory);\n\n if (parentDirectory === directory) {\n break;\n }\n\n directory = parentDirectory;\n }\n\n return \"unknown\";\n}\n","import { access, mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\nimport type { PluginConfigSource } from \"./config.js\";\n\nexport const PLUGIN_CONFIG_FILE_NAME = \"tbot.config.json\";\nexport const GLOBAL_PLUGIN_DIRECTORY_NAME = \"opencode-tbot\";\nexport const GLOBAL_PLUGIN_CONFIG_FILE_NAME = \"config.json\";\nexport const OPENCODE_CONFIG_FILE_NAME = \"opencode.json\";\n\nexport interface PreparedPluginConfiguration {\n cwd: string;\n config: PluginConfigSource;\n globalConfigFilePath: string;\n projectConfigFilePath: string;\n configFilePath: string;\n}\n\nexport interface PreparePluginConfigurationOptions {\n cwd: string;\n config?: PluginConfigSource;\n homeDir?: string;\n}\n\nexport async function preparePluginConfiguration(\n options: PreparePluginConfigurationOptions,\n): Promise<PreparedPluginConfiguration> {\n const homeDir = options.homeDir ?? homedir();\n const globalConfigFilePath = getGlobalPluginConfigFilePath(homeDir);\n const projectConfigFilePath = await resolveProjectPluginConfigFilePath(options.cwd);\n const [globalConfig, projectConfig] = await Promise.all([\n loadPluginConfigFile(globalConfigFilePath),\n loadPluginConfigFile(projectConfigFilePath),\n ]);\n const config = mergePluginConfigSources(globalConfig, projectConfig, options.config);\n applyGlobalOpenRouterApiKey(config, globalConfig);\n const configFilePath = await pathExists(projectConfigFilePath)\n ? projectConfigFilePath\n : globalConfigFilePath;\n\n return {\n cwd: options.cwd,\n config,\n globalConfigFilePath,\n projectConfigFilePath,\n configFilePath,\n };\n}\n\nexport function getOpenCodeConfigDirectory(homeDir: string = homedir()): string {\n return join(homeDir, \".config\", \"opencode\");\n}\n\nexport function getOpenCodeConfigFilePath(homeDir: string = homedir()): string {\n return join(getOpenCodeConfigDirectory(homeDir), OPENCODE_CONFIG_FILE_NAME);\n}\n\nexport function getGlobalPluginConfigFilePath(homeDir: string = homedir()): string {\n return join(\n getOpenCodeConfigDirectory(homeDir),\n GLOBAL_PLUGIN_DIRECTORY_NAME,\n GLOBAL_PLUGIN_CONFIG_FILE_NAME,\n );\n}\n\nexport async function writePluginConfigFile(\n configFilePath: string,\n config: PluginConfigSource,\n): Promise<void> {\n await mkdir(dirname(configFilePath), { recursive: true });\n await writeFile(configFilePath, serializePluginConfig(config), \"utf8\");\n}\n\nexport function mergePluginConfigSources(\n ...sources: Array<PluginConfigSource | undefined>\n): PluginConfigSource {\n const merged: PluginConfigSource = {};\n\n for (const source of sources) {\n if (!source) {\n continue;\n }\n\n const normalized = source;\n const previousTelegram = merged.telegram;\n const previousState = merged.state;\n const previousOpenRouter = merged.openrouter;\n\n Object.assign(merged, normalized);\n\n if (normalized.telegram) {\n merged.telegram = {\n ...(previousTelegram ?? {}),\n ...normalized.telegram,\n };\n }\n\n if (normalized.state) {\n merged.state = {\n ...(previousState ?? {}),\n ...normalized.state,\n };\n }\n\n if (normalized.openrouter) {\n merged.openrouter = {\n ...(previousOpenRouter ?? {}),\n ...normalized.openrouter,\n };\n }\n }\n\n return merged;\n}\n\nfunction applyGlobalOpenRouterApiKey(\n config: PluginConfigSource,\n globalConfig: PluginConfigSource,\n) {\n const globalApiKey = normalizeOptionalString(globalConfig.openrouter?.apiKey);\n\n if (!config.openrouter) {\n if (!globalApiKey) {\n return;\n }\n\n config.openrouter = {\n apiKey: globalApiKey,\n };\n\n return;\n }\n\n if (globalApiKey) {\n config.openrouter.apiKey = globalApiKey;\n return;\n }\n\n delete config.openrouter.apiKey;\n\n if (Object.keys(config.openrouter).length === 0) {\n delete config.openrouter;\n }\n}\n\nexport function serializePluginConfig(config: PluginConfigSource): string {\n return `${JSON.stringify(orderPluginConfig(config), null, 2)}\\n`;\n}\n\nasync function loadPluginConfigFile(configFilePath: string): Promise<PluginConfigSource> {\n try {\n const content = await readFile(configFilePath, \"utf8\");\n\n return parsePluginConfigText(content, configFilePath);\n } catch (error) {\n if (isMissingFileError(error)) {\n return {};\n }\n\n throw error;\n }\n}\n\nfunction parsePluginConfigText(\n content: string,\n configFilePath: string,\n): PluginConfigSource {\n try {\n const parsed = JSON.parse(content) as unknown;\n\n if (!isPlainObject(parsed)) {\n throw new Error(\"Config root must be a JSON object.\");\n }\n\n return parsed as PluginConfigSource;\n } catch (error) {\n throw new Error(\n [\n `Failed to parse ${configFilePath} as JSON.`,\n error instanceof Error ? error.message : String(error),\n ].join(\" \"),\n );\n }\n}\n\nfunction orderPluginConfig(config: PluginConfigSource): PluginConfigSource {\n const prioritizedKeys = new Set([\n \"telegram\",\n \"state\",\n \"openrouter\",\n \"logLevel\",\n ]);\n const ordered: PluginConfigSource = {};\n\n if (config.telegram) {\n ordered.telegram = config.telegram;\n }\n\n if (config.state) {\n ordered.state = config.state;\n }\n\n if (config.openrouter) {\n ordered.openrouter = config.openrouter;\n }\n\n if (config.logLevel !== undefined) {\n ordered.logLevel = config.logLevel;\n }\n\n for (const [key, value] of Object.entries(config)) {\n if (!prioritizedKeys.has(key)) {\n ordered[key] = value;\n }\n }\n\n return ordered;\n}\n\nfunction isPlainObject(value: unknown): value is Record<string, unknown> {\n return value !== null && typeof value === \"object\" && !Array.isArray(value);\n}\n\nfunction normalizeOptionalString(value: string | undefined): string | null {\n const normalized = value?.trim();\n\n return normalized\n ? normalized\n : null;\n}\n\nfunction isMissingFileError(error: unknown): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === \"ENOENT\";\n}\n\nasync function resolveProjectPluginConfigFilePath(cwd: string): Promise<string> {\n const preferredPath = join(cwd, PLUGIN_CONFIG_FILE_NAME);\n\n return preferredPath;\n}\n\nasync function pathExists(filePath: string): Promise<boolean> {\n try {\n await access(filePath);\n return true;\n } catch (error) {\n if (isMissingFileError(error)) {\n return false;\n }\n\n throw error;\n }\n}\n"],"mappings":"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"}
|
package/dist/cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-
|
|
1
|
+
import { a as writePluginConfigFile, n as getOpenCodeConfigFilePath, o as OPENCODE_TBOT_VERSION, r as mergePluginConfigSources, t as getGlobalPluginConfigFilePath } from "./assets/plugin-config-DA71_jD3.js";
|
|
2
2
|
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { dirname, join, resolve } from "node:path";
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import "./assets/plugin-config-
|
|
1
|
+
import "./assets/plugin-config-DA71_jD3.js";
|
|
2
2
|
import { TelegramBotPlugin, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests } from "./plugin.js";
|
|
3
3
|
export { TelegramBotPlugin, TelegramBotPlugin as default, ensureTelegramBotPluginRuntime, resetTelegramBotPluginRuntimeForTests };
|
package/dist/plugin.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
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-DA71_jD3.js";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
2
3
|
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
4
|
import { basename, dirname, extname, isAbsolute, join } from "node:path";
|
|
4
5
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -6,6 +7,7 @@ import { z } from "zod";
|
|
|
6
7
|
import { OpenRouter } from "@openrouter/sdk";
|
|
7
8
|
import { createOpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
8
9
|
import { randomUUID } from "node:crypto";
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
9
11
|
import { run } from "@grammyjs/runner";
|
|
10
12
|
import { Bot, InlineKeyboard } from "grammy";
|
|
11
13
|
//#region src/infra/utils/redact.ts
|
|
@@ -1178,6 +1180,145 @@ var NOOP_FOREGROUND_SESSION_TRACKER = {
|
|
|
1178
1180
|
}
|
|
1179
1181
|
};
|
|
1180
1182
|
//#endregion
|
|
1183
|
+
//#region src/services/voice-transcription/audio-transcoder.ts
|
|
1184
|
+
var OPENROUTER_SUPPORTED_AUDIO_FORMATS = ["mp3", "wav"];
|
|
1185
|
+
var VoiceTranscodingFailedError = class extends Error {
|
|
1186
|
+
data;
|
|
1187
|
+
constructor(message) {
|
|
1188
|
+
super(message);
|
|
1189
|
+
this.name = "VoiceTranscodingFailedError";
|
|
1190
|
+
this.data = { message };
|
|
1191
|
+
}
|
|
1192
|
+
};
|
|
1193
|
+
var DEFAULT_TRANSCODE_TIMEOUT_MS = 15e3;
|
|
1194
|
+
var FfmpegAudioTranscoder = class {
|
|
1195
|
+
ffmpegPath;
|
|
1196
|
+
spawnProcess;
|
|
1197
|
+
timeoutMs;
|
|
1198
|
+
constructor(options) {
|
|
1199
|
+
this.ffmpegPath = options.ffmpegPath?.trim() || null;
|
|
1200
|
+
this.spawnProcess = options.spawnProcess ?? defaultSpawnProcess;
|
|
1201
|
+
this.timeoutMs = options.timeoutMs ?? DEFAULT_TRANSCODE_TIMEOUT_MS;
|
|
1202
|
+
}
|
|
1203
|
+
async transcode(input) {
|
|
1204
|
+
if (!this.ffmpegPath) throw new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, "Bundled ffmpeg is unavailable."));
|
|
1205
|
+
if (input.targetFormat !== "wav") throw new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, `Unsupported transcode target: ${input.targetFormat}.`));
|
|
1206
|
+
return {
|
|
1207
|
+
data: await runFfmpegTranscode({
|
|
1208
|
+
data: toUint8Array$1(input.data),
|
|
1209
|
+
ffmpegPath: this.ffmpegPath,
|
|
1210
|
+
filename: input.filename,
|
|
1211
|
+
sourceFormat: input.sourceFormat,
|
|
1212
|
+
spawnProcess: this.spawnProcess,
|
|
1213
|
+
timeoutMs: this.timeoutMs,
|
|
1214
|
+
targetFormat: input.targetFormat
|
|
1215
|
+
}),
|
|
1216
|
+
filename: replaceExtension(input.filename, ".wav"),
|
|
1217
|
+
format: "wav",
|
|
1218
|
+
mimeType: "audio/wav"
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
};
|
|
1222
|
+
async function runFfmpegTranscode(input) {
|
|
1223
|
+
return await new Promise((resolve, reject) => {
|
|
1224
|
+
const child = input.spawnProcess(input.ffmpegPath, buildFfmpegArgs(input.targetFormat), {
|
|
1225
|
+
stdio: [
|
|
1226
|
+
"pipe",
|
|
1227
|
+
"pipe",
|
|
1228
|
+
"pipe"
|
|
1229
|
+
],
|
|
1230
|
+
windowsHide: true
|
|
1231
|
+
});
|
|
1232
|
+
const stdoutChunks = [];
|
|
1233
|
+
const stderrChunks = [];
|
|
1234
|
+
let settled = false;
|
|
1235
|
+
let timedOut = false;
|
|
1236
|
+
const timer = setTimeout(() => {
|
|
1237
|
+
timedOut = true;
|
|
1238
|
+
child.kill();
|
|
1239
|
+
}, input.timeoutMs);
|
|
1240
|
+
const cleanup = () => {
|
|
1241
|
+
clearTimeout(timer);
|
|
1242
|
+
};
|
|
1243
|
+
const rejectOnce = (message) => {
|
|
1244
|
+
if (settled) return;
|
|
1245
|
+
settled = true;
|
|
1246
|
+
cleanup();
|
|
1247
|
+
reject(new VoiceTranscodingFailedError(buildTranscodingMessage(input.sourceFormat, input.targetFormat, message)));
|
|
1248
|
+
};
|
|
1249
|
+
const resolveOnce = (value) => {
|
|
1250
|
+
if (settled) return;
|
|
1251
|
+
settled = true;
|
|
1252
|
+
cleanup();
|
|
1253
|
+
resolve(value);
|
|
1254
|
+
};
|
|
1255
|
+
child.stdout.on("data", (chunk) => {
|
|
1256
|
+
stdoutChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1257
|
+
});
|
|
1258
|
+
child.stderr.on("data", (chunk) => {
|
|
1259
|
+
stderrChunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
1260
|
+
});
|
|
1261
|
+
child.once("error", (error) => {
|
|
1262
|
+
rejectOnce(`Failed to start bundled ffmpeg: ${error.message}`);
|
|
1263
|
+
});
|
|
1264
|
+
child.once("close", (code, signal) => {
|
|
1265
|
+
if (timedOut) {
|
|
1266
|
+
rejectOnce(`Bundled ffmpeg timed out after ${input.timeoutMs} ms.`);
|
|
1267
|
+
return;
|
|
1268
|
+
}
|
|
1269
|
+
if (code !== 0) {
|
|
1270
|
+
rejectOnce(Buffer.concat(stderrChunks).toString("utf8").trim() || `Bundled ffmpeg exited with code ${code}${signal ? ` (${signal})` : ""}.`);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
const output = Buffer.concat(stdoutChunks);
|
|
1274
|
+
if (output.length === 0) {
|
|
1275
|
+
rejectOnce("Bundled ffmpeg returned empty audio output.");
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
resolveOnce(new Uint8Array(output));
|
|
1279
|
+
});
|
|
1280
|
+
child.stdin.on("error", (error) => {
|
|
1281
|
+
rejectOnce(`Failed to write audio data to bundled ffmpeg: ${error.message}`);
|
|
1282
|
+
});
|
|
1283
|
+
child.stdin.write(Buffer.from(input.data));
|
|
1284
|
+
child.stdin.end();
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1287
|
+
function buildFfmpegArgs(targetFormat) {
|
|
1288
|
+
if (targetFormat !== "wav") throw new Error(`Unsupported target format: ${targetFormat}`);
|
|
1289
|
+
return [
|
|
1290
|
+
"-hide_banner",
|
|
1291
|
+
"-loglevel",
|
|
1292
|
+
"error",
|
|
1293
|
+
"-i",
|
|
1294
|
+
"pipe:0",
|
|
1295
|
+
"-f",
|
|
1296
|
+
"wav",
|
|
1297
|
+
"-acodec",
|
|
1298
|
+
"pcm_s16le",
|
|
1299
|
+
"-ac",
|
|
1300
|
+
"1",
|
|
1301
|
+
"-ar",
|
|
1302
|
+
"16000",
|
|
1303
|
+
"pipe:1"
|
|
1304
|
+
];
|
|
1305
|
+
}
|
|
1306
|
+
function buildTranscodingMessage(sourceFormat, targetFormat, reason) {
|
|
1307
|
+
return `Failed to transcode audio from ${sourceFormat} to ${targetFormat}. ${reason}`;
|
|
1308
|
+
}
|
|
1309
|
+
function replaceExtension(filename, nextExtension) {
|
|
1310
|
+
const trimmedFilename = basename(filename).trim();
|
|
1311
|
+
if (!trimmedFilename) return `telegram-voice${nextExtension}`;
|
|
1312
|
+
const currentExtension = extname(trimmedFilename);
|
|
1313
|
+
return currentExtension ? `${trimmedFilename.slice(0, -currentExtension.length)}${nextExtension}` : `${trimmedFilename}${nextExtension}`;
|
|
1314
|
+
}
|
|
1315
|
+
function toUint8Array$1(data) {
|
|
1316
|
+
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1317
|
+
}
|
|
1318
|
+
function defaultSpawnProcess(command, args, options) {
|
|
1319
|
+
return spawn(command, args, options);
|
|
1320
|
+
}
|
|
1321
|
+
//#endregion
|
|
1181
1322
|
//#region src/services/voice-transcription/openrouter-voice.client.ts
|
|
1182
1323
|
var VoiceTranscriptionNotConfiguredError = class extends Error {
|
|
1183
1324
|
data;
|
|
@@ -1204,24 +1345,41 @@ var VoiceTranscriptEmptyError = class extends Error {
|
|
|
1204
1345
|
}
|
|
1205
1346
|
};
|
|
1206
1347
|
var DisabledVoiceTranscriptionClient = class {
|
|
1348
|
+
getStatus() {
|
|
1349
|
+
return {
|
|
1350
|
+
status: "not_configured",
|
|
1351
|
+
model: null
|
|
1352
|
+
};
|
|
1353
|
+
}
|
|
1207
1354
|
async transcribe() {
|
|
1208
|
-
throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in tbot
|
|
1355
|
+
throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
|
|
1209
1356
|
}
|
|
1210
1357
|
};
|
|
1211
1358
|
var OpenRouterVoiceTranscriptionClient = class {
|
|
1359
|
+
audioTranscoder;
|
|
1212
1360
|
model;
|
|
1213
1361
|
sdk;
|
|
1214
1362
|
timeoutMs;
|
|
1215
1363
|
transcriptionPrompt;
|
|
1216
|
-
constructor(options, sdk
|
|
1364
|
+
constructor(options, sdk, audioTranscoder = new FfmpegAudioTranscoder({
|
|
1365
|
+
ffmpegPath: null,
|
|
1366
|
+
timeoutMs: options.timeoutMs
|
|
1367
|
+
})) {
|
|
1368
|
+
this.audioTranscoder = audioTranscoder;
|
|
1217
1369
|
this.model = options.model;
|
|
1218
1370
|
this.sdk = sdk;
|
|
1219
1371
|
this.timeoutMs = options.timeoutMs;
|
|
1220
1372
|
this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
|
|
1221
1373
|
}
|
|
1374
|
+
getStatus() {
|
|
1375
|
+
return {
|
|
1376
|
+
status: "configured",
|
|
1377
|
+
model: this.model
|
|
1378
|
+
};
|
|
1379
|
+
}
|
|
1222
1380
|
async transcribe(input) {
|
|
1223
|
-
const
|
|
1224
|
-
const audioData = toBase64(
|
|
1381
|
+
const preparedAudio = await prepareAudioForOpenRouter(input, resolveAudioFormat(input.filename, input.mimeType), this.audioTranscoder);
|
|
1382
|
+
const audioData = toBase64(preparedAudio.data);
|
|
1225
1383
|
const prompt = buildTranscriptionPrompt(this.transcriptionPrompt);
|
|
1226
1384
|
let response;
|
|
1227
1385
|
try {
|
|
@@ -1235,7 +1393,7 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
1235
1393
|
type: "input_audio",
|
|
1236
1394
|
inputAudio: {
|
|
1237
1395
|
data: audioData,
|
|
1238
|
-
format
|
|
1396
|
+
format: preparedAudio.format
|
|
1239
1397
|
}
|
|
1240
1398
|
}]
|
|
1241
1399
|
}],
|
|
@@ -1245,13 +1403,29 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
1245
1403
|
} }, { timeoutMs: this.timeoutMs });
|
|
1246
1404
|
} catch (error) {
|
|
1247
1405
|
throw new VoiceTranscriptionFailedError(buildTranscriptionErrorMessage(error, {
|
|
1248
|
-
format,
|
|
1406
|
+
format: preparedAudio.format,
|
|
1249
1407
|
model: this.model
|
|
1250
1408
|
}));
|
|
1251
1409
|
}
|
|
1252
1410
|
return { text: extractTranscript(response) };
|
|
1253
1411
|
}
|
|
1254
1412
|
};
|
|
1413
|
+
async function prepareAudioForOpenRouter(input, sourceFormat, audioTranscoder) {
|
|
1414
|
+
if (isOpenRouterSupportedAudioFormat(sourceFormat)) return {
|
|
1415
|
+
data: toUint8Array(input.data),
|
|
1416
|
+
format: sourceFormat
|
|
1417
|
+
};
|
|
1418
|
+
const transcoded = await audioTranscoder.transcode({
|
|
1419
|
+
data: input.data,
|
|
1420
|
+
filename: input.filename,
|
|
1421
|
+
sourceFormat,
|
|
1422
|
+
targetFormat: "wav"
|
|
1423
|
+
});
|
|
1424
|
+
return {
|
|
1425
|
+
data: transcoded.data,
|
|
1426
|
+
format: transcoded.format
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1255
1429
|
var MIME_TYPE_FORMAT_MAP = {
|
|
1256
1430
|
"audio/aac": "aac",
|
|
1257
1431
|
"audio/aiff": "aiff",
|
|
@@ -1289,9 +1463,15 @@ function resolveAudioFormat(filename, mimeType) {
|
|
|
1289
1463
|
return "ogg";
|
|
1290
1464
|
}
|
|
1291
1465
|
function toBase64(data) {
|
|
1292
|
-
const bytes =
|
|
1466
|
+
const bytes = toUint8Array(data);
|
|
1293
1467
|
return Buffer.from(bytes).toString("base64");
|
|
1294
1468
|
}
|
|
1469
|
+
function toUint8Array(data) {
|
|
1470
|
+
return data instanceof Uint8Array ? data : new Uint8Array(data);
|
|
1471
|
+
}
|
|
1472
|
+
function isOpenRouterSupportedAudioFormat(format) {
|
|
1473
|
+
return OPENROUTER_SUPPORTED_AUDIO_FORMATS.includes(format);
|
|
1474
|
+
}
|
|
1295
1475
|
function buildTranscriptionPrompt(transcriptionPrompt) {
|
|
1296
1476
|
const basePrompt = [
|
|
1297
1477
|
"Transcribe the provided audio verbatim.",
|
|
@@ -1419,6 +1599,9 @@ var VoiceTranscriptionService = class {
|
|
|
1419
1599
|
constructor(client) {
|
|
1420
1600
|
this.client = client;
|
|
1421
1601
|
}
|
|
1602
|
+
getStatus() {
|
|
1603
|
+
return this.client.getStatus();
|
|
1604
|
+
}
|
|
1422
1605
|
async transcribeVoice(input) {
|
|
1423
1606
|
const text = (await this.client.transcribe(input)).text.trim();
|
|
1424
1607
|
if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
|
|
@@ -1531,14 +1714,14 @@ var GetPathUseCase = class {
|
|
|
1531
1714
|
//#endregion
|
|
1532
1715
|
//#region src/use-cases/get-status.usecase.ts
|
|
1533
1716
|
var GetStatusUseCase = class {
|
|
1534
|
-
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo,
|
|
1717
|
+
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService) {
|
|
1535
1718
|
this.getHealthUseCase = getHealthUseCase;
|
|
1536
1719
|
this.getPathUseCase = getPathUseCase;
|
|
1537
1720
|
this.listLspUseCase = listLspUseCase;
|
|
1538
1721
|
this.listMcpUseCase = listMcpUseCase;
|
|
1539
1722
|
this.listSessionsUseCase = listSessionsUseCase;
|
|
1540
1723
|
this.sessionRepo = sessionRepo;
|
|
1541
|
-
this.
|
|
1724
|
+
this.voiceTranscriptionService = voiceTranscriptionService;
|
|
1542
1725
|
}
|
|
1543
1726
|
async execute(input) {
|
|
1544
1727
|
const [health, path, lsp, mcp] = await Promise.allSettled([
|
|
@@ -1553,7 +1736,7 @@ var GetStatusUseCase = class {
|
|
|
1553
1736
|
health: mapSettledResult(health),
|
|
1554
1737
|
path: pathResult,
|
|
1555
1738
|
plugins,
|
|
1556
|
-
voiceRecognition: this.
|
|
1739
|
+
voiceRecognition: this.voiceTranscriptionService.getStatus(),
|
|
1557
1740
|
workspace,
|
|
1558
1741
|
lsp: mapSettledResult(lsp),
|
|
1559
1742
|
mcp: mapSettledResult(mcp)
|
|
@@ -2159,6 +2342,7 @@ function resolveExtension(mimeType) {
|
|
|
2159
2342
|
}
|
|
2160
2343
|
//#endregion
|
|
2161
2344
|
//#region src/app/container.ts
|
|
2345
|
+
var require = createRequire(import.meta.url);
|
|
2162
2346
|
function createAppContainer(config, client) {
|
|
2163
2347
|
const logger = createOpenCodeAppLogger(client, { level: config.logLevel });
|
|
2164
2348
|
return createContainer(config, createOpenCodeClientFromSdkClient(client), logger);
|
|
@@ -2186,10 +2370,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2186
2370
|
const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
|
|
2187
2371
|
const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
|
|
2188
2372
|
const listSessionsUseCase = new ListSessionsUseCase(sessionRepo, opencodeClient);
|
|
2189
|
-
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo,
|
|
2190
|
-
status: config.openrouter.configured ? "configured" : "not_configured",
|
|
2191
|
-
model: config.openrouter.configured ? config.openrouter.model : null
|
|
2192
|
-
});
|
|
2373
|
+
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService);
|
|
2193
2374
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
2194
2375
|
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
2195
2376
|
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
|
|
@@ -2238,8 +2419,19 @@ function createVoiceTranscriptionClient(config) {
|
|
|
2238
2419
|
}, new OpenRouter({
|
|
2239
2420
|
apiKey: config.apiKey,
|
|
2240
2421
|
timeoutMs: config.timeoutMs
|
|
2422
|
+
}), new FfmpegAudioTranscoder({
|
|
2423
|
+
ffmpegPath: loadBundledFfmpegPath(),
|
|
2424
|
+
timeoutMs: config.timeoutMs
|
|
2241
2425
|
})) : new DisabledVoiceTranscriptionClient();
|
|
2242
2426
|
}
|
|
2427
|
+
function loadBundledFfmpegPath() {
|
|
2428
|
+
try {
|
|
2429
|
+
const ffmpegInstaller = require("@ffmpeg-installer/ffmpeg");
|
|
2430
|
+
return typeof ffmpegInstaller.path === "string" && ffmpegInstaller.path.trim().length > 0 ? ffmpegInstaller.path : null;
|
|
2431
|
+
} catch {
|
|
2432
|
+
return null;
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2243
2435
|
//#endregion
|
|
2244
2436
|
//#region src/app/bootstrap.ts
|
|
2245
2437
|
function bootstrapPluginApp(client, configSource = {}, options = {}) {
|
|
@@ -2472,6 +2664,7 @@ var EN_BOT_COPY = {
|
|
|
2472
2664
|
structuredOutput: "Structured output validation failed.",
|
|
2473
2665
|
voiceNotConfigured: "Voice transcription is not configured.",
|
|
2474
2666
|
voiceDownload: "Failed to download the Telegram voice file.",
|
|
2667
|
+
voiceTranscoding: "Voice audio preprocessing failed.",
|
|
2475
2668
|
voiceTranscription: "Voice transcription failed.",
|
|
2476
2669
|
voiceEmpty: "Voice transcription returned empty text.",
|
|
2477
2670
|
voiceUnsupported: "Voice message file is too large or unsupported.",
|
|
@@ -2681,6 +2874,7 @@ var ZH_CN_BOT_COPY = {
|
|
|
2681
2874
|
structuredOutput: "结构化输出校验失败。",
|
|
2682
2875
|
voiceNotConfigured: "未配置语音转写服务。",
|
|
2683
2876
|
voiceDownload: "下载 Telegram 语音文件失败。",
|
|
2877
|
+
voiceTranscoding: "语音转码失败。",
|
|
2684
2878
|
voiceTranscription: "语音转写失败。",
|
|
2685
2879
|
voiceEmpty: "语音转写结果为空。",
|
|
2686
2880
|
voiceUnsupported: "语音文件过大或不受支持。",
|
|
@@ -3037,6 +3231,10 @@ function normalizeError(error, copy) {
|
|
|
3037
3231
|
message: copy.errors.voiceDownload,
|
|
3038
3232
|
cause: extractMessage(error.data) ?? null
|
|
3039
3233
|
};
|
|
3234
|
+
if (isNamedError(error, "VoiceTranscodingFailedError")) return {
|
|
3235
|
+
message: copy.errors.voiceTranscoding,
|
|
3236
|
+
cause: extractMessage(error.data) ?? null
|
|
3237
|
+
};
|
|
3040
3238
|
if (isNamedError(error, "VoiceTranscriptionFailedError")) return {
|
|
3041
3239
|
message: copy.errors.voiceTranscription,
|
|
3042
3240
|
cause: extractMessage(error.data) ?? null
|
|
@@ -3294,9 +3492,9 @@ function splitStatusLines(text) {
|
|
|
3294
3492
|
function formatHealthBadge(healthy, layout) {
|
|
3295
3493
|
return healthy ? "🟢" : layout.errorStatus;
|
|
3296
3494
|
}
|
|
3297
|
-
function formatVoiceRecognitionBadge(status,
|
|
3298
|
-
if (status.status === "configured") return status.model ? `\uD83D\
|
|
3299
|
-
return
|
|
3495
|
+
function formatVoiceRecognitionBadge(status, _layout) {
|
|
3496
|
+
if (status.status === "configured") return status.model ? `\uD83D\uDFE2 (${status.model})` : "🟡";
|
|
3497
|
+
return "⚪";
|
|
3300
3498
|
}
|
|
3301
3499
|
function formatLspStatusBadge(status) {
|
|
3302
3500
|
switch (status.status) {
|
|
@@ -3366,9 +3564,7 @@ function getStatusLayoutCopy(copy) {
|
|
|
3366
3564
|
rootLabel: "Root",
|
|
3367
3565
|
statusLabel: "Status",
|
|
3368
3566
|
tbotVersionLabel: "opencode-tbot Version",
|
|
3369
|
-
voiceRecognitionConfiguredLabel: "configured",
|
|
3370
3567
|
voiceRecognitionLabel: "Voice Recognition",
|
|
3371
|
-
voiceRecognitionNotConfiguredLabel: "not configured",
|
|
3372
3568
|
workspaceTitle: "📁 Workspace"
|
|
3373
3569
|
};
|
|
3374
3570
|
return {
|
|
@@ -3391,9 +3587,7 @@ function getStatusLayoutCopy(copy) {
|
|
|
3391
3587
|
rootLabel: "根目录",
|
|
3392
3588
|
statusLabel: "状态",
|
|
3393
3589
|
tbotVersionLabel: "opencode-tbot版本",
|
|
3394
|
-
voiceRecognitionConfiguredLabel: "已配置",
|
|
3395
3590
|
voiceRecognitionLabel: "语音识别",
|
|
3396
|
-
voiceRecognitionNotConfiguredLabel: "未配置",
|
|
3397
3591
|
workspaceTitle: "📁 工作区"
|
|
3398
3592
|
};
|
|
3399
3593
|
}
|