opencode-tbot 0.1.12 → 0.1.14
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.md +1 -2
- package/README.zh-CN.md +1 -2
- 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 +135 -207
- package/dist/plugin.js.map +1 -1
- package/package.json +1 -1
- package/tbot.config.example.json +0 -1
- package/dist/assets/plugin-config-CGIe9zdA.js.map +0 -1
package/README.md
CHANGED
|
@@ -115,8 +115,7 @@ Notes:
|
|
|
115
115
|
|
|
116
116
|
## Commands
|
|
117
117
|
|
|
118
|
-
- `/start` show
|
|
119
|
-
- `/help` show the full command reference and examples
|
|
118
|
+
- `/start` show a short welcome message and quick-start steps
|
|
120
119
|
- `/status` show aggregated OpenCode health, path, LSP, and MCP status
|
|
121
120
|
- `/new [title]` create a new OpenCode session
|
|
122
121
|
- `/agents` or `/agent` list available agents and switch the active one
|
package/README.zh-CN.md
CHANGED
|
@@ -115,8 +115,7 @@ npm uninstall opencode-tbot
|
|
|
115
115
|
|
|
116
116
|
## 命令
|
|
117
117
|
|
|
118
|
-
- `/start`
|
|
119
|
-
- `/help` 显示完整命令说明和示例
|
|
118
|
+
- `/start` 显示简短欢迎信息和快速开始说明
|
|
120
119
|
- `/status` 显示 OpenCode 健康状态、路径、LSP 和 MCP 信息
|
|
121
120
|
- `/new [title]` 创建新的 OpenCode 会话
|
|
122
121
|
- `/agents` 或 `/agent` 列出可用 agent 并切换当前 agent
|
|
@@ -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,4 @@
|
|
|
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
2
|
import { mkdir, readFile, rename, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, dirname, extname, isAbsolute, join } from "node:path";
|
|
4
4
|
import { parse, printParseErrorCode } from "jsonc-parser";
|
|
@@ -1204,8 +1204,14 @@ var VoiceTranscriptEmptyError = class extends Error {
|
|
|
1204
1204
|
}
|
|
1205
1205
|
};
|
|
1206
1206
|
var DisabledVoiceTranscriptionClient = class {
|
|
1207
|
+
getStatus() {
|
|
1208
|
+
return {
|
|
1209
|
+
status: "not_configured",
|
|
1210
|
+
model: null
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1207
1213
|
async transcribe() {
|
|
1208
|
-
throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in tbot
|
|
1214
|
+
throw new VoiceTranscriptionNotConfiguredError("Set openrouter.apiKey in the global config (~/.config/opencode/opencode-tbot/config.json) to enable Telegram voice transcription.");
|
|
1209
1215
|
}
|
|
1210
1216
|
};
|
|
1211
1217
|
var OpenRouterVoiceTranscriptionClient = class {
|
|
@@ -1219,6 +1225,12 @@ var OpenRouterVoiceTranscriptionClient = class {
|
|
|
1219
1225
|
this.timeoutMs = options.timeoutMs;
|
|
1220
1226
|
this.transcriptionPrompt = options.transcriptionPrompt?.trim() || null;
|
|
1221
1227
|
}
|
|
1228
|
+
getStatus() {
|
|
1229
|
+
return {
|
|
1230
|
+
status: "configured",
|
|
1231
|
+
model: this.model
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1222
1234
|
async transcribe(input) {
|
|
1223
1235
|
const format = resolveAudioFormat(input.filename, input.mimeType);
|
|
1224
1236
|
const audioData = toBase64(input.data);
|
|
@@ -1419,6 +1431,9 @@ var VoiceTranscriptionService = class {
|
|
|
1419
1431
|
constructor(client) {
|
|
1420
1432
|
this.client = client;
|
|
1421
1433
|
}
|
|
1434
|
+
getStatus() {
|
|
1435
|
+
return this.client.getStatus();
|
|
1436
|
+
}
|
|
1422
1437
|
async transcribeVoice(input) {
|
|
1423
1438
|
const text = (await this.client.transcribe(input)).text.trim();
|
|
1424
1439
|
if (!text) throw new VoiceTranscriptEmptyError("Voice transcription returned empty text.");
|
|
@@ -1531,14 +1546,14 @@ var GetPathUseCase = class {
|
|
|
1531
1546
|
//#endregion
|
|
1532
1547
|
//#region src/use-cases/get-status.usecase.ts
|
|
1533
1548
|
var GetStatusUseCase = class {
|
|
1534
|
-
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo,
|
|
1549
|
+
constructor(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService) {
|
|
1535
1550
|
this.getHealthUseCase = getHealthUseCase;
|
|
1536
1551
|
this.getPathUseCase = getPathUseCase;
|
|
1537
1552
|
this.listLspUseCase = listLspUseCase;
|
|
1538
1553
|
this.listMcpUseCase = listMcpUseCase;
|
|
1539
1554
|
this.listSessionsUseCase = listSessionsUseCase;
|
|
1540
1555
|
this.sessionRepo = sessionRepo;
|
|
1541
|
-
this.
|
|
1556
|
+
this.voiceTranscriptionService = voiceTranscriptionService;
|
|
1542
1557
|
}
|
|
1543
1558
|
async execute(input) {
|
|
1544
1559
|
const [health, path, lsp, mcp] = await Promise.allSettled([
|
|
@@ -1553,7 +1568,7 @@ var GetStatusUseCase = class {
|
|
|
1553
1568
|
health: mapSettledResult(health),
|
|
1554
1569
|
path: pathResult,
|
|
1555
1570
|
plugins,
|
|
1556
|
-
voiceRecognition: this.
|
|
1571
|
+
voiceRecognition: this.voiceTranscriptionService.getStatus(),
|
|
1557
1572
|
workspace,
|
|
1558
1573
|
lsp: mapSettledResult(lsp),
|
|
1559
1574
|
mcp: mapSettledResult(mcp)
|
|
@@ -2186,10 +2201,7 @@ function createContainer(config, opencodeClient, logger) {
|
|
|
2186
2201
|
const listLspUseCase = new ListLspUseCase(sessionRepo, opencodeClient);
|
|
2187
2202
|
const listMcpUseCase = new ListMcpUseCase(sessionRepo, opencodeClient);
|
|
2188
2203
|
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
|
-
});
|
|
2204
|
+
const getStatusUseCase = new GetStatusUseCase(getHealthUseCase, getPathUseCase, listLspUseCase, listMcpUseCase, listSessionsUseCase, sessionRepo, voiceTranscriptionService);
|
|
2193
2205
|
const listModelsUseCase = new ListModelsUseCase(sessionRepo, opencodeClient);
|
|
2194
2206
|
const renameSessionUseCase = new RenameSessionUseCase(sessionRepo, opencodeClient, logger);
|
|
2195
2207
|
const sendPromptUseCase = new SendPromptUseCase(sessionRepo, opencodeClient, logger, foregroundSessionTracker);
|
|
@@ -2414,7 +2426,6 @@ var SUPPORTED_BOT_LANGUAGES = ["en", "zh-CN"];
|
|
|
2414
2426
|
var EN_BOT_COPY = {
|
|
2415
2427
|
commands: {
|
|
2416
2428
|
start: "Welcome and quick start",
|
|
2417
|
-
help: "Show commands and examples",
|
|
2418
2429
|
status: "Show system status",
|
|
2419
2430
|
new: "Create a new session",
|
|
2420
2431
|
agents: "View and switch agents",
|
|
@@ -2428,37 +2439,11 @@ var EN_BOT_COPY = {
|
|
|
2428
2439
|
"",
|
|
2429
2440
|
"Talk to your OpenCode server from Telegram.",
|
|
2430
2441
|
"",
|
|
2431
|
-
"## What you can send",
|
|
2432
|
-
"- Text prompts",
|
|
2433
|
-
"- Images with an optional caption",
|
|
2434
|
-
"- Voice messages (requires OpenRouter voice transcription)",
|
|
2435
|
-
"",
|
|
2436
2442
|
"## Quick start",
|
|
2437
2443
|
"1. Run `/status` to confirm the server is ready.",
|
|
2438
2444
|
"2. Run `/new [title]` to create a fresh session.",
|
|
2439
|
-
"3. Send a text, image, or voice message.",
|
|
2440
2445
|
"",
|
|
2441
|
-
"
|
|
2442
|
-
] },
|
|
2443
|
-
help: { lines: [
|
|
2444
|
-
"# Help",
|
|
2445
|
-
"",
|
|
2446
|
-
"Use this bot to chat with OpenCode from Telegram.",
|
|
2447
|
-
"",
|
|
2448
|
-
"## Commands",
|
|
2449
|
-
"- `/status` Check server, workspace, MCP, and LSP status",
|
|
2450
|
-
"- `/new [title]` Create a new session",
|
|
2451
|
-
"- `/sessions` View, switch, or rename sessions",
|
|
2452
|
-
"- `/agents` View and switch agents",
|
|
2453
|
-
"- `/model` View and switch models and reasoning levels",
|
|
2454
|
-
"- `/language` Switch the bot display language",
|
|
2455
|
-
"- `/cancel` Cancel session rename or abort the running request",
|
|
2456
|
-
"",
|
|
2457
|
-
"## Examples",
|
|
2458
|
-
"- `/new bug triage`",
|
|
2459
|
-
"- Send a plain text message directly",
|
|
2460
|
-
"- Send an image with a caption",
|
|
2461
|
-
"- Send a voice message if OpenRouter voice transcription is configured"
|
|
2446
|
+
"Send a text, image, or voice message directly."
|
|
2462
2447
|
] },
|
|
2463
2448
|
systemStatus: { title: "System Status" },
|
|
2464
2449
|
common: {
|
|
@@ -2650,7 +2635,6 @@ var EN_BOT_COPY = {
|
|
|
2650
2635
|
var ZH_CN_BOT_COPY = {
|
|
2651
2636
|
commands: {
|
|
2652
2637
|
start: "查看欢迎与快速开始",
|
|
2653
|
-
help: "查看命令说明与示例",
|
|
2654
2638
|
status: "查看系统状态",
|
|
2655
2639
|
new: "新建会话",
|
|
2656
2640
|
agents: "查看并切换代理",
|
|
@@ -2664,37 +2648,11 @@ var ZH_CN_BOT_COPY = {
|
|
|
2664
2648
|
"",
|
|
2665
2649
|
"通过 Telegram 直接和 OpenCode 服务对话。",
|
|
2666
2650
|
"",
|
|
2667
|
-
"## 支持的输入",
|
|
2668
|
-
"- 文本消息",
|
|
2669
|
-
"- 图片 (可附带 caption)",
|
|
2670
|
-
"- 语音消息 (需先配置 OpenRouter 语音转写)",
|
|
2671
|
-
"",
|
|
2672
2651
|
"## 快速开始",
|
|
2673
2652
|
"1. 先运行 `/status` 确认服务状态正常。",
|
|
2674
2653
|
"2. 运行 `/new [title]` 创建一个新会话。",
|
|
2675
|
-
"3. 直接发送文本、图片或语音消息。",
|
|
2676
2654
|
"",
|
|
2677
|
-
"
|
|
2678
|
-
] },
|
|
2679
|
-
help: { lines: [
|
|
2680
|
-
"# 帮助",
|
|
2681
|
-
"",
|
|
2682
|
-
"使用这个机器人可以通过 Telegram 与 OpenCode 对话。",
|
|
2683
|
-
"",
|
|
2684
|
-
"## 命令",
|
|
2685
|
-
"- `/status` 查看服务、工作区、MCP 和 LSP 状态",
|
|
2686
|
-
"- `/new [title]` 创建一个新会话",
|
|
2687
|
-
"- `/sessions` 查看、切换或重命名会话",
|
|
2688
|
-
"- `/agents` 查看并切换代理",
|
|
2689
|
-
"- `/model` 查看并切换模型与推理级别",
|
|
2690
|
-
"- `/language` 切换机器人的显示语言",
|
|
2691
|
-
"- `/cancel` 取消会话重命名或中止当前请求",
|
|
2692
|
-
"",
|
|
2693
|
-
"## 示例",
|
|
2694
|
-
"- `/new bug triage`",
|
|
2695
|
-
"- 直接发送一条文本消息",
|
|
2696
|
-
"- 发送一张带 caption 的图片",
|
|
2697
|
-
"- 如果已配置 OpenRouter 语音转写,直接发送语音消息"
|
|
2655
|
+
"直接发送文本、图片或语音消息即可。"
|
|
2698
2656
|
] },
|
|
2699
2657
|
systemStatus: { title: "系统状态" },
|
|
2700
2658
|
common: {
|
|
@@ -2908,10 +2866,6 @@ function getTelegramCommands(language = "en") {
|
|
|
2908
2866
|
command: "start",
|
|
2909
2867
|
description: copy.commands.start
|
|
2910
2868
|
},
|
|
2911
|
-
{
|
|
2912
|
-
command: "help",
|
|
2913
|
-
description: copy.commands.help
|
|
2914
|
-
},
|
|
2915
2869
|
{
|
|
2916
2870
|
command: "status",
|
|
2917
2871
|
description: copy.commands.status
|
|
@@ -3353,7 +3307,7 @@ function formatHealthBadge(healthy, layout) {
|
|
|
3353
3307
|
return healthy ? "🟢" : layout.errorStatus;
|
|
3354
3308
|
}
|
|
3355
3309
|
function formatVoiceRecognitionBadge(status, layout) {
|
|
3356
|
-
if (status.status === "configured") return status.model ? `\uD83D\
|
|
3310
|
+
if (status.status === "configured") return status.model ? `\uD83D\uDFE2 ${layout.voiceRecognitionConfiguredLabel} (${status.model})` : `\uD83D\uDFE2 ${layout.voiceRecognitionConfiguredLabel}`;
|
|
3357
3311
|
return `\u26AA ${layout.voiceRecognitionNotConfiguredLabel}`;
|
|
3358
3312
|
}
|
|
3359
3313
|
function formatLspStatusBadge(status) {
|
|
@@ -3744,6 +3698,112 @@ function registerCancelCommand(bot, dependencies) {
|
|
|
3744
3698
|
});
|
|
3745
3699
|
}
|
|
3746
3700
|
//#endregion
|
|
3701
|
+
//#region src/bot/commands/language.ts
|
|
3702
|
+
async function handleLanguageCommand(ctx, dependencies) {
|
|
3703
|
+
const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
|
|
3704
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3705
|
+
try {
|
|
3706
|
+
await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
|
|
3707
|
+
await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
|
|
3708
|
+
} catch (error) {
|
|
3709
|
+
dependencies.logger.error({ error }, "failed to show language options");
|
|
3710
|
+
await ctx.reply(presentError(error, copy));
|
|
3711
|
+
}
|
|
3712
|
+
}
|
|
3713
|
+
async function switchLanguageForChat(api, chatId, language, dependencies) {
|
|
3714
|
+
const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
|
|
3715
|
+
if (!isBotLanguage(language)) return {
|
|
3716
|
+
found: false,
|
|
3717
|
+
copy: currentCopy
|
|
3718
|
+
};
|
|
3719
|
+
await setChatLanguage(dependencies.sessionRepo, chatId, language);
|
|
3720
|
+
await syncTelegramCommandsForChat(api, chatId, language);
|
|
3721
|
+
return {
|
|
3722
|
+
found: true,
|
|
3723
|
+
copy: await getChatCopy(dependencies.sessionRepo, chatId),
|
|
3724
|
+
language
|
|
3725
|
+
};
|
|
3726
|
+
}
|
|
3727
|
+
async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
|
|
3728
|
+
const result = await switchLanguageForChat(api, chatId, language, dependencies);
|
|
3729
|
+
if (!result.found) return {
|
|
3730
|
+
found: false,
|
|
3731
|
+
copy: result.copy,
|
|
3732
|
+
text: result.copy.language.expired,
|
|
3733
|
+
keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
|
|
3734
|
+
};
|
|
3735
|
+
return {
|
|
3736
|
+
found: true,
|
|
3737
|
+
copy: result.copy,
|
|
3738
|
+
text: presentLanguageSwitchMessage(result.language, result.copy),
|
|
3739
|
+
keyboard: buildLanguageKeyboard(result.language, result.copy)
|
|
3740
|
+
};
|
|
3741
|
+
}
|
|
3742
|
+
function registerLanguageCommand(bot, dependencies) {
|
|
3743
|
+
bot.command("language", async (ctx) => {
|
|
3744
|
+
await handleLanguageCommand(ctx, dependencies);
|
|
3745
|
+
});
|
|
3746
|
+
}
|
|
3747
|
+
//#endregion
|
|
3748
|
+
//#region src/bot/commands/models.ts
|
|
3749
|
+
async function handleModelsCommand(ctx, dependencies) {
|
|
3750
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3751
|
+
try {
|
|
3752
|
+
const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
|
|
3753
|
+
if (result.models.length === 0) {
|
|
3754
|
+
await ctx.reply(copy.models.none);
|
|
3755
|
+
return;
|
|
3756
|
+
}
|
|
3757
|
+
const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
|
|
3758
|
+
await ctx.reply(presentModelsMessage({
|
|
3759
|
+
currentModelId: result.currentModelId,
|
|
3760
|
+
currentModelProviderId: result.currentModelProviderId,
|
|
3761
|
+
currentModelVariant: result.currentModelVariant,
|
|
3762
|
+
models: result.models,
|
|
3763
|
+
page: page.page
|
|
3764
|
+
}, copy), { reply_markup: keyboard });
|
|
3765
|
+
} catch (error) {
|
|
3766
|
+
dependencies.logger.error({ error }, "failed to list models");
|
|
3767
|
+
await ctx.reply(presentError(error, copy));
|
|
3768
|
+
}
|
|
3769
|
+
}
|
|
3770
|
+
function registerModelsCommand(bot, dependencies) {
|
|
3771
|
+
bot.command(["model", "models"], async (ctx) => {
|
|
3772
|
+
await handleModelsCommand(ctx, dependencies);
|
|
3773
|
+
});
|
|
3774
|
+
}
|
|
3775
|
+
//#endregion
|
|
3776
|
+
//#region src/bot/commands/new.ts
|
|
3777
|
+
async function handleNewCommand(ctx, dependencies) {
|
|
3778
|
+
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
3779
|
+
try {
|
|
3780
|
+
const title = extractSessionTitle(ctx);
|
|
3781
|
+
const result = await dependencies.createSessionUseCase.execute({
|
|
3782
|
+
chatId: ctx.chat.id,
|
|
3783
|
+
title
|
|
3784
|
+
});
|
|
3785
|
+
await ctx.reply(presentSessionCreatedMessage(result.session, copy));
|
|
3786
|
+
} catch (error) {
|
|
3787
|
+
dependencies.logger.error({ error }, "failed to create new session");
|
|
3788
|
+
await ctx.reply(presentError(error, copy));
|
|
3789
|
+
}
|
|
3790
|
+
}
|
|
3791
|
+
function registerNewCommand(bot, dependencies) {
|
|
3792
|
+
bot.command("new", async (ctx) => {
|
|
3793
|
+
await handleNewCommand(ctx, dependencies);
|
|
3794
|
+
});
|
|
3795
|
+
}
|
|
3796
|
+
function extractSessionTitle(ctx) {
|
|
3797
|
+
if (typeof ctx.match === "string") {
|
|
3798
|
+
const title = ctx.match.trim();
|
|
3799
|
+
return title ? title : null;
|
|
3800
|
+
}
|
|
3801
|
+
const messageText = ctx.message?.text?.trim();
|
|
3802
|
+
if (!messageText) return null;
|
|
3803
|
+
const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
|
|
3804
|
+
return title ? title : null;
|
|
3805
|
+
}
|
|
3806
|
+
//#endregion
|
|
3747
3807
|
//#region src/services/telegram/telegram-format.ts
|
|
3748
3808
|
var MAX_TELEGRAM_MESSAGE_LENGTH = 4096;
|
|
3749
3809
|
var TRUNCATED_SUFFIX = "...";
|
|
@@ -4077,142 +4137,6 @@ function escapeLinkDestination(url) {
|
|
|
4077
4137
|
return url.replace(/\\/g, "\\\\").replace(/\)/g, "\\)").replace(/\(/g, "\\(");
|
|
4078
4138
|
}
|
|
4079
4139
|
//#endregion
|
|
4080
|
-
//#region src/bot/presenters/static.presenter.ts
|
|
4081
|
-
function presentStartMarkdownMessage(copy = BOT_COPY) {
|
|
4082
|
-
return copy.start.lines.join("\n");
|
|
4083
|
-
}
|
|
4084
|
-
function presentHelpMarkdownMessage(copy = BOT_COPY) {
|
|
4085
|
-
return copy.help.lines.join("\n");
|
|
4086
|
-
}
|
|
4087
|
-
//#endregion
|
|
4088
|
-
//#region src/bot/commands/help.ts
|
|
4089
|
-
async function handleHelpCommand(ctx, dependencies) {
|
|
4090
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
|
|
4091
|
-
const reply = buildTelegramStaticReply(presentHelpMarkdownMessage(copy));
|
|
4092
|
-
try {
|
|
4093
|
-
await ctx.reply(reply.preferred.text, reply.preferred.options);
|
|
4094
|
-
} catch (error) {
|
|
4095
|
-
if (reply.preferred.options) {
|
|
4096
|
-
dependencies.logger.error({ error }, "failed to send help markdown reply, falling back to plain text");
|
|
4097
|
-
await ctx.reply(reply.fallback.text);
|
|
4098
|
-
return;
|
|
4099
|
-
}
|
|
4100
|
-
dependencies.logger.error({ error }, "failed to show help message");
|
|
4101
|
-
await ctx.reply(presentError(error, copy));
|
|
4102
|
-
}
|
|
4103
|
-
}
|
|
4104
|
-
function registerHelpCommand(bot, dependencies) {
|
|
4105
|
-
bot.command("help", async (ctx) => {
|
|
4106
|
-
await handleHelpCommand(ctx, dependencies);
|
|
4107
|
-
});
|
|
4108
|
-
}
|
|
4109
|
-
//#endregion
|
|
4110
|
-
//#region src/bot/commands/language.ts
|
|
4111
|
-
async function handleLanguageCommand(ctx, dependencies) {
|
|
4112
|
-
const language = await getChatLanguage(dependencies.sessionRepo, ctx.chat.id);
|
|
4113
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4114
|
-
try {
|
|
4115
|
-
await syncTelegramCommandsForChat(ctx.api, ctx.chat.id, language);
|
|
4116
|
-
await ctx.reply(presentLanguageMessage(language, copy), { reply_markup: buildLanguageKeyboard(language, copy) });
|
|
4117
|
-
} catch (error) {
|
|
4118
|
-
dependencies.logger.error({ error }, "failed to show language options");
|
|
4119
|
-
await ctx.reply(presentError(error, copy));
|
|
4120
|
-
}
|
|
4121
|
-
}
|
|
4122
|
-
async function switchLanguageForChat(api, chatId, language, dependencies) {
|
|
4123
|
-
const currentCopy = await getChatCopy(dependencies.sessionRepo, chatId);
|
|
4124
|
-
if (!isBotLanguage(language)) return {
|
|
4125
|
-
found: false,
|
|
4126
|
-
copy: currentCopy
|
|
4127
|
-
};
|
|
4128
|
-
await setChatLanguage(dependencies.sessionRepo, chatId, language);
|
|
4129
|
-
await syncTelegramCommandsForChat(api, chatId, language);
|
|
4130
|
-
return {
|
|
4131
|
-
found: true,
|
|
4132
|
-
copy: await getChatCopy(dependencies.sessionRepo, chatId),
|
|
4133
|
-
language
|
|
4134
|
-
};
|
|
4135
|
-
}
|
|
4136
|
-
async function presentLanguageSwitchForChat(chatId, api, language, dependencies) {
|
|
4137
|
-
const result = await switchLanguageForChat(api, chatId, language, dependencies);
|
|
4138
|
-
if (!result.found) return {
|
|
4139
|
-
found: false,
|
|
4140
|
-
copy: result.copy,
|
|
4141
|
-
text: result.copy.language.expired,
|
|
4142
|
-
keyboard: buildLanguageKeyboard(await getChatLanguage(dependencies.sessionRepo, chatId), result.copy)
|
|
4143
|
-
};
|
|
4144
|
-
return {
|
|
4145
|
-
found: true,
|
|
4146
|
-
copy: result.copy,
|
|
4147
|
-
text: presentLanguageSwitchMessage(result.language, result.copy),
|
|
4148
|
-
keyboard: buildLanguageKeyboard(result.language, result.copy)
|
|
4149
|
-
};
|
|
4150
|
-
}
|
|
4151
|
-
function registerLanguageCommand(bot, dependencies) {
|
|
4152
|
-
bot.command("language", async (ctx) => {
|
|
4153
|
-
await handleLanguageCommand(ctx, dependencies);
|
|
4154
|
-
});
|
|
4155
|
-
}
|
|
4156
|
-
//#endregion
|
|
4157
|
-
//#region src/bot/commands/models.ts
|
|
4158
|
-
async function handleModelsCommand(ctx, dependencies) {
|
|
4159
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4160
|
-
try {
|
|
4161
|
-
const result = await dependencies.listModelsUseCase.execute({ chatId: ctx.chat.id });
|
|
4162
|
-
if (result.models.length === 0) {
|
|
4163
|
-
await ctx.reply(copy.models.none);
|
|
4164
|
-
return;
|
|
4165
|
-
}
|
|
4166
|
-
const { keyboard, page } = buildModelsKeyboard(result.models, 0, copy);
|
|
4167
|
-
await ctx.reply(presentModelsMessage({
|
|
4168
|
-
currentModelId: result.currentModelId,
|
|
4169
|
-
currentModelProviderId: result.currentModelProviderId,
|
|
4170
|
-
currentModelVariant: result.currentModelVariant,
|
|
4171
|
-
models: result.models,
|
|
4172
|
-
page: page.page
|
|
4173
|
-
}, copy), { reply_markup: keyboard });
|
|
4174
|
-
} catch (error) {
|
|
4175
|
-
dependencies.logger.error({ error }, "failed to list models");
|
|
4176
|
-
await ctx.reply(presentError(error, copy));
|
|
4177
|
-
}
|
|
4178
|
-
}
|
|
4179
|
-
function registerModelsCommand(bot, dependencies) {
|
|
4180
|
-
bot.command(["model", "models"], async (ctx) => {
|
|
4181
|
-
await handleModelsCommand(ctx, dependencies);
|
|
4182
|
-
});
|
|
4183
|
-
}
|
|
4184
|
-
//#endregion
|
|
4185
|
-
//#region src/bot/commands/new.ts
|
|
4186
|
-
async function handleNewCommand(ctx, dependencies) {
|
|
4187
|
-
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat.id);
|
|
4188
|
-
try {
|
|
4189
|
-
const title = extractSessionTitle(ctx);
|
|
4190
|
-
const result = await dependencies.createSessionUseCase.execute({
|
|
4191
|
-
chatId: ctx.chat.id,
|
|
4192
|
-
title
|
|
4193
|
-
});
|
|
4194
|
-
await ctx.reply(presentSessionCreatedMessage(result.session, copy));
|
|
4195
|
-
} catch (error) {
|
|
4196
|
-
dependencies.logger.error({ error }, "failed to create new session");
|
|
4197
|
-
await ctx.reply(presentError(error, copy));
|
|
4198
|
-
}
|
|
4199
|
-
}
|
|
4200
|
-
function registerNewCommand(bot, dependencies) {
|
|
4201
|
-
bot.command("new", async (ctx) => {
|
|
4202
|
-
await handleNewCommand(ctx, dependencies);
|
|
4203
|
-
});
|
|
4204
|
-
}
|
|
4205
|
-
function extractSessionTitle(ctx) {
|
|
4206
|
-
if (typeof ctx.match === "string") {
|
|
4207
|
-
const title = ctx.match.trim();
|
|
4208
|
-
return title ? title : null;
|
|
4209
|
-
}
|
|
4210
|
-
const messageText = ctx.message?.text?.trim();
|
|
4211
|
-
if (!messageText) return null;
|
|
4212
|
-
const title = messageText.match(/^\/new(?:@\S+)?(?:\s+([\s\S]*))?$/i)?.[1]?.trim();
|
|
4213
|
-
return title ? title : null;
|
|
4214
|
-
}
|
|
4215
|
-
//#endregion
|
|
4216
4140
|
//#region src/bot/commands/status.ts
|
|
4217
4141
|
async function handleStatusCommand(ctx, dependencies) {
|
|
4218
4142
|
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
|
|
@@ -4253,6 +4177,11 @@ function registerSessionsCommand(bot, dependencies) {
|
|
|
4253
4177
|
});
|
|
4254
4178
|
}
|
|
4255
4179
|
//#endregion
|
|
4180
|
+
//#region src/bot/presenters/static.presenter.ts
|
|
4181
|
+
function presentStartMarkdownMessage(copy = BOT_COPY) {
|
|
4182
|
+
return copy.start.lines.join("\n");
|
|
4183
|
+
}
|
|
4184
|
+
//#endregion
|
|
4256
4185
|
//#region src/bot/commands/start.ts
|
|
4257
4186
|
async function handleStartCommand(ctx, dependencies) {
|
|
4258
4187
|
const copy = await getChatCopy(dependencies.sessionRepo, ctx.chat?.id);
|
|
@@ -4769,7 +4698,6 @@ function registerBot(bot, container, options) {
|
|
|
4769
4698
|
bot.use(createLoggingMiddleware(container.logger));
|
|
4770
4699
|
bot.use(createAuthMiddleware(options.telegramAllowedChatIds));
|
|
4771
4700
|
registerStartCommand(bot, container);
|
|
4772
|
-
registerHelpCommand(bot, container);
|
|
4773
4701
|
registerStatusCommand(bot, container);
|
|
4774
4702
|
registerNewCommand(bot, container);
|
|
4775
4703
|
registerAgentsCommand(bot, container);
|