@grinev/opencode-telegram-bot 0.11.1 → 0.11.3
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 -1
- package/dist/app/start-bot-app.js +2 -0
- package/dist/bot/commands/{stop.js → abort.js} +44 -16
- package/dist/bot/commands/definitions.js +1 -1
- package/dist/bot/commands/models.js +4 -4
- package/dist/bot/commands/opencode-start.js +24 -8
- package/dist/bot/commands/start.js +8 -0
- package/dist/bot/commands/status.js +2 -3
- package/dist/bot/handlers/permission.js +3 -4
- package/dist/bot/handlers/question.js +1 -3
- package/dist/bot/index.js +6 -5
- package/dist/bot/utils/send-with-markdown-fallback.js +20 -0
- package/dist/bot/utils/telegram-text.js +26 -0
- package/dist/i18n/de.js +11 -10
- package/dist/i18n/en.js +11 -10
- package/dist/i18n/es.js +11 -10
- package/dist/i18n/ru.js +11 -10
- package/dist/i18n/zh.js +11 -10
- package/dist/interaction/guard.js +3 -0
- package/dist/interaction/manager.js +1 -1
- package/dist/model/manager.js +110 -4
- package/dist/pinned/manager.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -102,7 +102,7 @@ opencode-telegram config
|
|
|
102
102
|
| ----------------- | ------------------------------------------------------- |
|
|
103
103
|
| `/status` | Server health, current project, session, and model info |
|
|
104
104
|
| `/new` | Create a new session |
|
|
105
|
-
| `/
|
|
105
|
+
| `/abort` | Abort the current task |
|
|
106
106
|
| `/sessions` | Browse and switch between recent sessions |
|
|
107
107
|
| `/projects` | Switch between OpenCode projects |
|
|
108
108
|
| `/rename` | Rename the current session |
|
|
@@ -4,6 +4,7 @@ import { config } from "../config.js";
|
|
|
4
4
|
import { loadSettings } from "../settings/manager.js";
|
|
5
5
|
import { processManager } from "../process/manager.js";
|
|
6
6
|
import { warmupSessionDirectoryCache } from "../session/cache-manager.js";
|
|
7
|
+
import { reconcileStoredModelSelection } from "../model/manager.js";
|
|
7
8
|
import { getRuntimeMode } from "../runtime/mode.js";
|
|
8
9
|
import { getRuntimePaths } from "../runtime/paths.js";
|
|
9
10
|
import { logger } from "../utils/logger.js";
|
|
@@ -29,6 +30,7 @@ export async function startBotApp() {
|
|
|
29
30
|
logger.debug(`[Runtime] Application start mode: ${mode}`);
|
|
30
31
|
await loadSettings();
|
|
31
32
|
await processManager.initialize();
|
|
33
|
+
await reconcileStoredModelSelection();
|
|
32
34
|
await warmupSessionDirectoryCache();
|
|
33
35
|
const bot = createBot();
|
|
34
36
|
const webhookInfo = await bot.api.getWebhookInfo();
|
|
@@ -6,10 +6,10 @@ import { summaryAggregator } from "../../summary/aggregator.js";
|
|
|
6
6
|
import { logger } from "../../utils/logger.js";
|
|
7
7
|
import { t } from "../../i18n/index.js";
|
|
8
8
|
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
9
|
-
function
|
|
9
|
+
function abortLocalStreaming() {
|
|
10
10
|
stopEventListening();
|
|
11
11
|
summaryAggregator.clear();
|
|
12
|
-
clearAllInteractionState("
|
|
12
|
+
clearAllInteractionState("abort_command");
|
|
13
13
|
}
|
|
14
14
|
async function pollSessionStatus(sessionId, directory, maxWaitMs = 5000) {
|
|
15
15
|
const startedAt = Date.now();
|
|
@@ -33,21 +33,34 @@ async function pollSessionStatus(sessionId, directory, maxWaitMs = 5000) {
|
|
|
33
33
|
await sleep(pollIntervalMs);
|
|
34
34
|
}
|
|
35
35
|
catch (error) {
|
|
36
|
-
logger.warn("[
|
|
36
|
+
logger.warn("[Abort] Failed to poll session status:", error);
|
|
37
37
|
break;
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
40
|
return "busy";
|
|
41
41
|
}
|
|
42
|
-
export async function
|
|
42
|
+
export async function abortCurrentOperation(ctx, options = {}) {
|
|
43
|
+
const notifyUser = options.notifyUser ?? true;
|
|
43
44
|
try {
|
|
44
|
-
|
|
45
|
+
abortLocalStreaming();
|
|
45
46
|
const currentSession = getCurrentSession();
|
|
46
47
|
if (!currentSession) {
|
|
47
|
-
|
|
48
|
+
if (notifyUser) {
|
|
49
|
+
await ctx.reply(t("stop.no_active_session"));
|
|
50
|
+
}
|
|
48
51
|
return;
|
|
49
52
|
}
|
|
50
|
-
|
|
53
|
+
let waitingMessageId = null;
|
|
54
|
+
let chatId = null;
|
|
55
|
+
if (notifyUser) {
|
|
56
|
+
const waitingMessage = await ctx.reply(t("stop.in_progress"));
|
|
57
|
+
waitingMessageId = waitingMessage.message_id;
|
|
58
|
+
chatId = ctx.chat?.id ?? null;
|
|
59
|
+
if (!chatId) {
|
|
60
|
+
logger.warn("[Abort] Chat context is missing while aborting active session");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
51
64
|
const controller = new AbortController();
|
|
52
65
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
53
66
|
try {
|
|
@@ -57,35 +70,50 @@ export async function stopCommand(ctx) {
|
|
|
57
70
|
}, { signal: controller.signal });
|
|
58
71
|
clearTimeout(timeoutId);
|
|
59
72
|
if (abortError) {
|
|
60
|
-
logger.warn("[
|
|
61
|
-
|
|
73
|
+
logger.warn("[Abort] Abort request failed:", abortError);
|
|
74
|
+
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
75
|
+
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_unconfirmed"));
|
|
76
|
+
}
|
|
62
77
|
return;
|
|
63
78
|
}
|
|
64
79
|
if (abortResult !== true) {
|
|
65
|
-
|
|
80
|
+
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
81
|
+
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_maybe_finished"));
|
|
82
|
+
}
|
|
66
83
|
return;
|
|
67
84
|
}
|
|
68
85
|
const finalStatus = await pollSessionStatus(currentSession.id, currentSession.directory, 5000);
|
|
69
86
|
if (finalStatus === "idle" || finalStatus === "not-found") {
|
|
70
|
-
|
|
87
|
+
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
88
|
+
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.success"));
|
|
89
|
+
}
|
|
71
90
|
}
|
|
72
91
|
else {
|
|
73
|
-
|
|
92
|
+
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
93
|
+
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_still_busy"));
|
|
94
|
+
}
|
|
74
95
|
}
|
|
75
96
|
}
|
|
76
97
|
catch (error) {
|
|
77
98
|
clearTimeout(timeoutId);
|
|
78
99
|
if (error instanceof Error && error.name === "AbortError") {
|
|
79
|
-
|
|
100
|
+
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
101
|
+
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_timeout"));
|
|
102
|
+
}
|
|
80
103
|
}
|
|
81
104
|
else {
|
|
82
|
-
logger.error("[
|
|
83
|
-
|
|
105
|
+
logger.error("[Abort] Error while aborting session:", error);
|
|
106
|
+
if (notifyUser && chatId !== null && waitingMessageId !== null) {
|
|
107
|
+
await ctx.api.editMessageText(chatId, waitingMessageId, t("stop.warn_local_only"));
|
|
108
|
+
}
|
|
84
109
|
}
|
|
85
110
|
}
|
|
86
111
|
}
|
|
87
112
|
catch (error) {
|
|
88
|
-
logger.error("[
|
|
113
|
+
logger.error("[Abort] Unexpected error:", error);
|
|
89
114
|
await ctx.reply(t("stop.error"));
|
|
90
115
|
}
|
|
91
116
|
}
|
|
117
|
+
export async function abortCommand(ctx) {
|
|
118
|
+
await abortCurrentOperation(ctx);
|
|
119
|
+
}
|
|
@@ -6,7 +6,7 @@ import { t } from "../../i18n/index.js";
|
|
|
6
6
|
const COMMAND_DEFINITIONS = [
|
|
7
7
|
{ command: "status", descriptionKey: "cmd.description.status" },
|
|
8
8
|
{ command: "new", descriptionKey: "cmd.description.new" },
|
|
9
|
-
{ command: "
|
|
9
|
+
{ command: "abort", descriptionKey: "cmd.description.stop" },
|
|
10
10
|
{ command: "sessions", descriptionKey: "cmd.description.sessions" },
|
|
11
11
|
{ command: "projects", descriptionKey: "cmd.description.projects" },
|
|
12
12
|
{ command: "rename", descriptionKey: "cmd.description.rename" },
|
|
@@ -15,21 +15,21 @@ export async function modelsCommand(ctx) {
|
|
|
15
15
|
}
|
|
16
16
|
let message = t("legacy.models.header");
|
|
17
17
|
for (const provider of providers) {
|
|
18
|
-
message += `🔹
|
|
18
|
+
message += `🔹 ${provider.id}\n`;
|
|
19
19
|
const models = Object.values(provider.models);
|
|
20
20
|
if (models.length === 0) {
|
|
21
21
|
message += t("legacy.models.no_provider_models");
|
|
22
22
|
}
|
|
23
23
|
else {
|
|
24
24
|
for (const model of models) {
|
|
25
|
-
message += `
|
|
25
|
+
message += ` - ${model.id}\n`;
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
message += "\n";
|
|
29
29
|
}
|
|
30
30
|
message += t("legacy.models.env_hint");
|
|
31
|
-
message += "
|
|
32
|
-
await ctx.reply(message
|
|
31
|
+
message += "OPENCODE_MODEL_PROVIDER=<provider.id>\nOPENCODE_MODEL_ID=<model.id>";
|
|
32
|
+
await ctx.reply(message);
|
|
33
33
|
}
|
|
34
34
|
catch (error) {
|
|
35
35
|
logger.error("[ModelsCommand] Error listing models:", error);
|
|
@@ -2,6 +2,7 @@ import { opencodeClient } from "../../opencode/client.js";
|
|
|
2
2
|
import { processManager } from "../../process/manager.js";
|
|
3
3
|
import { logger } from "../../utils/logger.js";
|
|
4
4
|
import { t } from "../../i18n/index.js";
|
|
5
|
+
import { editBotText } from "../utils/telegram-text.js";
|
|
5
6
|
/**
|
|
6
7
|
* Wait for OpenCode server to become ready by polling health endpoint
|
|
7
8
|
* @param maxWaitMs Maximum time to wait in milliseconds
|
|
@@ -58,24 +59,39 @@ export async function opencodeStartCommand(ctx) {
|
|
|
58
59
|
// 4. Start the process
|
|
59
60
|
const { success, error } = await processManager.start();
|
|
60
61
|
if (!success) {
|
|
61
|
-
await
|
|
62
|
+
await editBotText({
|
|
63
|
+
api: ctx.api,
|
|
64
|
+
chatId: ctx.chat.id,
|
|
65
|
+
messageId: statusMessage.message_id,
|
|
66
|
+
text: t("opencode_start.start_error", { error: error || t("common.unknown_error") }),
|
|
67
|
+
});
|
|
62
68
|
return;
|
|
63
69
|
}
|
|
64
70
|
// 5. Wait for server to become ready
|
|
65
71
|
logger.info("[Bot] Waiting for OpenCode server to become ready...");
|
|
66
72
|
const ready = await waitForServerReady(10000);
|
|
67
73
|
if (!ready) {
|
|
68
|
-
await
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
await editBotText({
|
|
75
|
+
api: ctx.api,
|
|
76
|
+
chatId: ctx.chat.id,
|
|
77
|
+
messageId: statusMessage.message_id,
|
|
78
|
+
text: t("opencode_start.started_not_ready", {
|
|
79
|
+
pid: processManager.getPID() ?? "-",
|
|
80
|
+
}),
|
|
81
|
+
});
|
|
71
82
|
return;
|
|
72
83
|
}
|
|
73
84
|
// 6. Get server version and send success message
|
|
74
85
|
const { data: health } = await opencodeClient.global.health();
|
|
75
|
-
await
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
86
|
+
await editBotText({
|
|
87
|
+
api: ctx.api,
|
|
88
|
+
chatId: ctx.chat.id,
|
|
89
|
+
messageId: statusMessage.message_id,
|
|
90
|
+
text: t("opencode_start.success", {
|
|
91
|
+
pid: processManager.getPID() ?? "-",
|
|
92
|
+
version: health?.version || t("common.unknown"),
|
|
93
|
+
}),
|
|
94
|
+
});
|
|
79
95
|
logger.info(`[Bot] OpenCode server started successfully, PID=${processManager.getPID()}`);
|
|
80
96
|
}
|
|
81
97
|
catch (err) {
|
|
@@ -4,6 +4,9 @@ import { getStoredModel } from "../../model/manager.js";
|
|
|
4
4
|
import { formatVariantForButton } from "../../variant/manager.js";
|
|
5
5
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
6
6
|
import { keyboardManager } from "../../keyboard/manager.js";
|
|
7
|
+
import { clearSession } from "../../session/manager.js";
|
|
8
|
+
import { clearProject } from "../../settings/manager.js";
|
|
9
|
+
import { abortCurrentOperation } from "./abort.js";
|
|
7
10
|
import { t } from "../../i18n/index.js";
|
|
8
11
|
export async function startCommand(ctx) {
|
|
9
12
|
if (ctx.chat) {
|
|
@@ -12,6 +15,11 @@ export async function startCommand(ctx) {
|
|
|
12
15
|
}
|
|
13
16
|
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
14
17
|
}
|
|
18
|
+
await abortCurrentOperation(ctx, { notifyUser: false });
|
|
19
|
+
clearSession();
|
|
20
|
+
clearProject();
|
|
21
|
+
keyboardManager.clearContext();
|
|
22
|
+
await pinnedMessageManager.clear();
|
|
15
23
|
if (pinnedMessageManager.getContextLimit() === 0) {
|
|
16
24
|
await pinnedMessageManager.refreshContextLimit();
|
|
17
25
|
}
|
|
@@ -9,7 +9,7 @@ import { keyboardManager } from "../../keyboard/manager.js";
|
|
|
9
9
|
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
10
10
|
import { logger } from "../../utils/logger.js";
|
|
11
11
|
import { t } from "../../i18n/index.js";
|
|
12
|
-
import {
|
|
12
|
+
import { sendBotText } from "../utils/telegram-text.js";
|
|
13
13
|
export async function statusCommand(ctx) {
|
|
14
14
|
try {
|
|
15
15
|
const { data, error } = await opencodeClient.global.health();
|
|
@@ -77,12 +77,11 @@ export async function statusCommand(ctx) {
|
|
|
77
77
|
}
|
|
78
78
|
const keyboard = keyboardManager.getKeyboard();
|
|
79
79
|
if (ctx.chat) {
|
|
80
|
-
await
|
|
80
|
+
await sendBotText({
|
|
81
81
|
api: ctx.api,
|
|
82
82
|
chatId: ctx.chat.id,
|
|
83
83
|
text: message,
|
|
84
84
|
options: { reply_markup: keyboard },
|
|
85
|
-
parseMode: "Markdown",
|
|
86
85
|
});
|
|
87
86
|
}
|
|
88
87
|
else {
|
|
@@ -21,6 +21,7 @@ const PERMISSION_NAME_KEYS = {
|
|
|
21
21
|
list: "permission.name.list",
|
|
22
22
|
task: "permission.name.task",
|
|
23
23
|
lsp: "permission.name.lsp",
|
|
24
|
+
external_directory: "permission.name.external_directory",
|
|
24
25
|
};
|
|
25
26
|
// Permission type emojis
|
|
26
27
|
const PERMISSION_EMOJIS = {
|
|
@@ -35,6 +36,7 @@ const PERMISSION_EMOJIS = {
|
|
|
35
36
|
list: "📂",
|
|
36
37
|
task: "⚙️",
|
|
37
38
|
lsp: "🔧",
|
|
39
|
+
external_directory: "📁",
|
|
38
40
|
};
|
|
39
41
|
function getCallbackMessageId(ctx) {
|
|
40
42
|
const message = ctx.callbackQuery?.message;
|
|
@@ -191,7 +193,6 @@ export async function showPermissionRequest(bot, chatId, request) {
|
|
|
191
193
|
try {
|
|
192
194
|
const message = await bot.sendMessage(chatId, text, {
|
|
193
195
|
reply_markup: keyboard,
|
|
194
|
-
parse_mode: "Markdown",
|
|
195
196
|
});
|
|
196
197
|
logger.debug(`[PermissionHandler] Message sent, messageId=${message.message_id}`);
|
|
197
198
|
permissionManager.startPermission(request, message.message_id);
|
|
@@ -217,9 +218,7 @@ function formatPermissionText(request) {
|
|
|
217
218
|
// Show patterns (commands/files)
|
|
218
219
|
if (request.patterns.length > 0) {
|
|
219
220
|
request.patterns.forEach((pattern) => {
|
|
220
|
-
|
|
221
|
-
const escapedPattern = pattern.replace(/`/g, "\\`");
|
|
222
|
-
text += `\`${escapedPattern}\`\n`;
|
|
221
|
+
text += `• ${pattern}\n`;
|
|
223
222
|
});
|
|
224
223
|
}
|
|
225
224
|
return text;
|
|
@@ -192,7 +192,6 @@ async function updateQuestionMessage(ctx) {
|
|
|
192
192
|
try {
|
|
193
193
|
await ctx.editMessageText(text, {
|
|
194
194
|
reply_markup: keyboard,
|
|
195
|
-
parse_mode: "Markdown",
|
|
196
195
|
});
|
|
197
196
|
}
|
|
198
197
|
catch (err) {
|
|
@@ -212,7 +211,6 @@ export async function showCurrentQuestion(bot, chatId) {
|
|
|
212
211
|
try {
|
|
213
212
|
const message = await bot.sendMessage(chatId, text, {
|
|
214
213
|
reply_markup: keyboard,
|
|
215
|
-
parse_mode: "Markdown",
|
|
216
214
|
});
|
|
217
215
|
logger.debug(`[QuestionHandler] Message sent, messageId=${message.message_id}`);
|
|
218
216
|
questionManager.addMessageId(message.message_id);
|
|
@@ -342,7 +340,7 @@ function formatQuestionText(question) {
|
|
|
342
340
|
const totalQuestions = questionManager.getTotalQuestions();
|
|
343
341
|
const progressText = totalQuestions > 0 ? `${currentIndex + 1}/${totalQuestions}` : "";
|
|
344
342
|
const headerTitle = [progressText, question.header].filter(Boolean).join(" ");
|
|
345
|
-
const header = headerTitle ?
|
|
343
|
+
const header = headerTitle ? `${headerTitle}\n\n` : "";
|
|
346
344
|
const multiple = question.multiple ? t("question.multi_hint") : "";
|
|
347
345
|
return `${header}${question.question}${multiple}`;
|
|
348
346
|
}
|
package/dist/bot/index.js
CHANGED
|
@@ -16,7 +16,7 @@ import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTT
|
|
|
16
16
|
import { sessionsCommand, handleSessionSelect } from "./commands/sessions.js";
|
|
17
17
|
import { newCommand } from "./commands/new.js";
|
|
18
18
|
import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
|
|
19
|
-
import {
|
|
19
|
+
import { abortCommand } from "./commands/abort.js";
|
|
20
20
|
import { opencodeStartCommand } from "./commands/opencode-start.js";
|
|
21
21
|
import { opencodeStopCommand } from "./commands/opencode-stop.js";
|
|
22
22
|
import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
|
|
@@ -46,7 +46,7 @@ import { processUserPrompt } from "./handlers/prompt.js";
|
|
|
46
46
|
import { handleVoiceMessage } from "./handlers/voice.js";
|
|
47
47
|
import { handleDocumentMessage } from "./handlers/document.js";
|
|
48
48
|
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
49
|
-
import {
|
|
49
|
+
import { sendBotText } from "./utils/telegram-text.js";
|
|
50
50
|
import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
|
|
51
51
|
import { getStoredModel } from "../model/manager.js";
|
|
52
52
|
let botInstance = null;
|
|
@@ -151,17 +151,18 @@ async function ensureEventSubscription(directory) {
|
|
|
151
151
|
try {
|
|
152
152
|
const parts = formatSummary(messageText);
|
|
153
153
|
const assistantParseMode = getAssistantParseMode();
|
|
154
|
+
const assistantMessageFormat = assistantParseMode === "MarkdownV2" ? "markdown_v2" : "raw";
|
|
154
155
|
logger.debug(`[Bot] Sending completed message to Telegram (chatId=${chatIdInstance}, parts=${parts.length})`);
|
|
155
156
|
for (let i = 0; i < parts.length; i++) {
|
|
156
157
|
const isLastPart = i === parts.length - 1;
|
|
157
158
|
const keyboard = isLastPart && keyboardManager.isInitialized() ? keyboardManager.getKeyboard() : undefined;
|
|
158
159
|
const options = keyboard ? { reply_markup: keyboard } : undefined;
|
|
159
|
-
await
|
|
160
|
+
await sendBotText({
|
|
160
161
|
api: botInstance.api,
|
|
161
162
|
chatId: chatIdInstance,
|
|
162
163
|
text: parts[i],
|
|
163
164
|
options,
|
|
164
|
-
|
|
165
|
+
format: assistantMessageFormat,
|
|
165
166
|
});
|
|
166
167
|
}
|
|
167
168
|
}
|
|
@@ -456,7 +457,7 @@ export function createBot() {
|
|
|
456
457
|
bot.command("projects", projectsCommand);
|
|
457
458
|
bot.command("sessions", sessionsCommand);
|
|
458
459
|
bot.command("new", newCommand);
|
|
459
|
-
bot.command("
|
|
460
|
+
bot.command("abort", abortCommand);
|
|
460
461
|
bot.command("rename", renameCommand);
|
|
461
462
|
bot.command("commands", commandsCommand);
|
|
462
463
|
bot.on("message:text", unknownCommandMiddleware);
|
|
@@ -56,3 +56,23 @@ export async function sendMessageWithMarkdownFallback({ api, chatId, text, optio
|
|
|
56
56
|
await api.sendMessage(chatId, text, options);
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
|
+
export async function editMessageWithMarkdownFallback({ api, chatId, messageId, text, options, parseMode, }) {
|
|
60
|
+
if (!parseMode) {
|
|
61
|
+
await api.editMessageText(chatId, messageId, text, options);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const markdownOptions = {
|
|
65
|
+
...(options || {}),
|
|
66
|
+
parse_mode: parseMode,
|
|
67
|
+
};
|
|
68
|
+
try {
|
|
69
|
+
await api.editMessageText(chatId, messageId, text, markdownOptions);
|
|
70
|
+
}
|
|
71
|
+
catch (error) {
|
|
72
|
+
if (!isTelegramMarkdownParseError(error)) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
logger.warn("[Bot] Markdown parse failed, retrying edited message in raw mode", error);
|
|
76
|
+
await api.editMessageText(chatId, messageId, text, options);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { editMessageWithMarkdownFallback, sendMessageWithMarkdownFallback, } from "./send-with-markdown-fallback.js";
|
|
2
|
+
function resolveParseMode(format) {
|
|
3
|
+
if (format === "markdown_v2") {
|
|
4
|
+
return "MarkdownV2";
|
|
5
|
+
}
|
|
6
|
+
return undefined;
|
|
7
|
+
}
|
|
8
|
+
export async function sendBotText({ api, chatId, text, options, format = "raw", }) {
|
|
9
|
+
await sendMessageWithMarkdownFallback({
|
|
10
|
+
api,
|
|
11
|
+
chatId,
|
|
12
|
+
text,
|
|
13
|
+
options,
|
|
14
|
+
parseMode: resolveParseMode(format),
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
export async function editBotText({ api, chatId, messageId, text, options, format = "raw", }) {
|
|
18
|
+
await editMessageWithMarkdownFallback({
|
|
19
|
+
api,
|
|
20
|
+
chatId,
|
|
21
|
+
messageId,
|
|
22
|
+
text,
|
|
23
|
+
options,
|
|
24
|
+
parseMode: resolveParseMode(format),
|
|
25
|
+
});
|
|
26
|
+
}
|
package/dist/i18n/de.js
CHANGED
|
@@ -38,11 +38,11 @@ export const de = {
|
|
|
38
38
|
"bot.creating_session": "🔄 Erstelle eine neue Sitzung...",
|
|
39
39
|
"bot.create_session_error": "🔴 Sitzung konnte nicht erstellt werden. Versuche /new oder prüfe den Serverstatus mit /status.",
|
|
40
40
|
"bot.session_created": "✅ Sitzung erstellt: {title}",
|
|
41
|
-
"bot.session_busy": "⏳ Agent führt bereits eine Aufgabe aus. Warte auf Abschluss oder nutze /
|
|
41
|
+
"bot.session_busy": "⏳ Agent führt bereits eine Aufgabe aus. Warte auf Abschluss oder nutze /abort, um den aktuellen Lauf zu unterbrechen.",
|
|
42
42
|
"bot.session_reset_project_mismatch": "⚠️ Die aktive Sitzung passt nicht zum ausgewählten Projekt und wurde daher zurückgesetzt. Nutze /sessions zur Auswahl oder /new, um eine neue Sitzung zu erstellen.",
|
|
43
43
|
"bot.prompt_send_error": "Anfrage konnte nicht an OpenCode gesendet werden.",
|
|
44
44
|
"bot.session_error": "🔴 OpenCode meldete einen Fehler: {message}",
|
|
45
|
-
"bot.session_retry": "🔁 {message}\n\nDer Provider liefert bei wiederholten Versuchen immer wieder denselben Fehler. Mit /
|
|
45
|
+
"bot.session_retry": "🔁 {message}\n\nDer Provider liefert bei wiederholten Versuchen immer wieder denselben Fehler. Mit /abort abbrechen.",
|
|
46
46
|
"bot.unknown_command": "⚠️ Unbekannter Befehl: {command}. Nutze /help, um verfügbare Befehle zu sehen.",
|
|
47
47
|
"bot.photo_downloading": "⏳ Lade Foto herunter...",
|
|
48
48
|
"bot.photo_too_large": "⚠️ Foto ist zu groß (max. {maxSizeMb}MB)",
|
|
@@ -54,7 +54,7 @@ export const de = {
|
|
|
54
54
|
"bot.file_download_error": "🔴 Datei konnte nicht heruntergeladen werden",
|
|
55
55
|
"bot.model_no_pdf": "⚠️ Das aktuelle Modell unterstützt keine PDF-Eingabe. Sende nur Text.",
|
|
56
56
|
"bot.text_file_too_large": "⚠️ Textdatei ist zu groß (max. {maxSizeKb}KB)",
|
|
57
|
-
"status.header_running": "🟢
|
|
57
|
+
"status.header_running": "🟢 OpenCode-Server läuft",
|
|
58
58
|
"status.health.healthy": "OK",
|
|
59
59
|
"status.health.unhealthy": "Nicht OK",
|
|
60
60
|
"status.line.health": "Status: {health}",
|
|
@@ -105,17 +105,17 @@ export const de = {
|
|
|
105
105
|
"new.create_error": "🔴 OpenCode-Server ist nicht verfügbar oder beim Erstellen der Sitzung ist ein Fehler aufgetreten.",
|
|
106
106
|
"stop.no_active_session": "🛑 Agent wurde nicht gestartet\n\nErstelle eine Sitzung mit /new oder wähle eine über /sessions aus.",
|
|
107
107
|
"stop.in_progress": "🛑 Event-Stream gestoppt, sende Abbruchsignal...\n\nWarte darauf, dass der Agent stoppt.",
|
|
108
|
-
"stop.warn_unconfirmed": "⚠️ Event-Stream gestoppt, aber der Server hat den Abbruch nicht bestätigt.\n\nPrüfe /status und versuche /
|
|
108
|
+
"stop.warn_unconfirmed": "⚠️ Event-Stream gestoppt, aber der Server hat den Abbruch nicht bestätigt.\n\nPrüfe /status und versuche /abort in ein paar Sekunden erneut.",
|
|
109
109
|
"stop.warn_maybe_finished": "⚠️ Event-Stream gestoppt, aber der Agent konnte bereits fertig sein.",
|
|
110
110
|
"stop.success": "✅ Agent-Aktion unterbrochen. Von diesem Lauf werden keine weiteren Nachrichten gesendet.",
|
|
111
111
|
"stop.warn_still_busy": "⚠️ Signal gesendet, aber der Agent ist noch beschäftigt.\n\nDer Event-Stream ist bereits deaktiviert, daher werden keine Zwischenmeldungen gesendet.",
|
|
112
|
-
"stop.warn_timeout": "⚠️ Timeout beim Abbruch.\n\nDer Event-Stream ist bereits deaktiviert, versuche /
|
|
112
|
+
"stop.warn_timeout": "⚠️ Timeout beim Abbruch.\n\nDer Event-Stream ist bereits deaktiviert, versuche /abort in ein paar Sekunden erneut.",
|
|
113
113
|
"stop.warn_local_only": "⚠️ Event-Stream lokal gestoppt, aber serverseitiger Abbruch ist fehlgeschlagen.",
|
|
114
|
-
"stop.error": "🔴 Aktion konnte nicht gestoppt werden.\n\nEvent-Stream ist gestoppt, versuche /
|
|
114
|
+
"stop.error": "🔴 Aktion konnte nicht gestoppt werden.\n\nEvent-Stream ist gestoppt, versuche /abort erneut.",
|
|
115
115
|
"opencode_start.already_running_managed": "⚠️ OpenCode-Server läuft bereits\n\nPID: {pid}\nBetriebszeit: {seconds} Sekunden",
|
|
116
116
|
"opencode_start.already_running_external": "✅ OpenCode-Server läuft bereits als externer Prozess\n\nVersion: {version}\n\nDieser Server wurde nicht vom Bot gestartet, daher kann /opencode-stop ihn nicht stoppen.",
|
|
117
117
|
"opencode_start.starting": "🔄 Starte OpenCode-Server...",
|
|
118
|
-
"opencode_start.start_error": "🔴 OpenCode-Server konnte nicht gestartet werden\n\nFehler: {error}\n\nPrüfe, ob OpenCode CLI installiert und im PATH verfügbar ist:\
|
|
118
|
+
"opencode_start.start_error": "🔴 OpenCode-Server konnte nicht gestartet werden\n\nFehler: {error}\n\nPrüfe, ob OpenCode CLI installiert und im PATH verfügbar ist:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
119
119
|
"opencode_start.started_not_ready": "⚠️ OpenCode-Server gestartet, aber reagiert nicht\n\nPID: {pid}\n\nDer Server startet möglicherweise noch. Versuche /status in ein paar Sekunden.",
|
|
120
120
|
"opencode_start.success": "✅ OpenCode-Server erfolgreich gestartet\n\nPID: {pid}\nVersion: {version}",
|
|
121
121
|
"opencode_start.error": "🔴 Beim Starten des Servers ist ein Fehler aufgetreten.\n\nSiehe Anwendungslogs für Details.",
|
|
@@ -169,7 +169,7 @@ export const de = {
|
|
|
169
169
|
"permission.send_reply_error": "❌ Antwort auf Berechtigungsanfrage konnte nicht gesendet werden",
|
|
170
170
|
"permission.blocked.expected_reply": "⚠️ Bitte beantworte zuerst die Berechtigungsanfrage mit den Buttons oben.",
|
|
171
171
|
"permission.blocked.command_not_allowed": "⚠️ Dieser Befehl ist erst verfügbar, wenn du die Berechtigungsanfrage beantwortet hast.",
|
|
172
|
-
"permission.header": "{emoji}
|
|
172
|
+
"permission.header": "{emoji} Berechtigungsanfrage: {name}\n\n",
|
|
173
173
|
"permission.button.allow": "✅ Einmal erlauben",
|
|
174
174
|
"permission.button.always": "🔓 Immer erlauben",
|
|
175
175
|
"permission.button.reject": "❌ Ablehnen",
|
|
@@ -184,6 +184,7 @@ export const de = {
|
|
|
184
184
|
"permission.name.list": "Verzeichnis auflisten",
|
|
185
185
|
"permission.name.task": "Task",
|
|
186
186
|
"permission.name.lsp": "LSP",
|
|
187
|
+
"permission.name.external_directory": "Externes Verzeichnis",
|
|
187
188
|
"question.inactive_callback": "Umfrage ist inaktiv",
|
|
188
189
|
"question.processing_error_callback": "Verarbeitungsfehler",
|
|
189
190
|
"question.select_one_required_callback": "Wähle mindestens eine Option",
|
|
@@ -194,7 +195,7 @@ export const de = {
|
|
|
194
195
|
"question.no_active_project": "❌ Kein aktives Projekt",
|
|
195
196
|
"question.no_active_request": "❌ Keine aktive Anfrage",
|
|
196
197
|
"question.send_answers_error": "❌ Antworten konnten nicht an den Agenten gesendet werden",
|
|
197
|
-
"question.multi_hint": "\n
|
|
198
|
+
"question.multi_hint": "\n(Du kannst mehrere Optionen auswählen)",
|
|
198
199
|
"question.button.submit": "✅ Fertig",
|
|
199
200
|
"question.button.custom": "🔤 Eigene Antwort",
|
|
200
201
|
"question.button.cancel": "❌ Abbrechen",
|
|
@@ -272,7 +273,7 @@ export const de = {
|
|
|
272
273
|
"cli.args.mode_only_start": "Option --mode wird nur für den start-Befehl unterstützt",
|
|
273
274
|
"legacy.models.fetch_error": "🔴 Modellliste konnte nicht geladen werden. Prüfe den Serverstatus mit /status.",
|
|
274
275
|
"legacy.models.empty": "📋 Keine verfügbaren Modelle. Konfiguriere Provider in OpenCode.",
|
|
275
|
-
"legacy.models.header": "📋
|
|
276
|
+
"legacy.models.header": "📋 Verfügbare Modelle:\n\n",
|
|
276
277
|
"legacy.models.no_provider_models": " ⚠️ Keine verfügbaren Modelle\n",
|
|
277
278
|
"legacy.models.env_hint": "💡 Um ein Modell in .env zu nutzen:\n",
|
|
278
279
|
"legacy.models.error": "🔴 Beim Laden der Modellliste ist ein Fehler aufgetreten.",
|
package/dist/i18n/en.js
CHANGED
|
@@ -38,11 +38,11 @@ export const en = {
|
|
|
38
38
|
"bot.creating_session": "🔄 Creating a new session...",
|
|
39
39
|
"bot.create_session_error": "🔴 Failed to create session. Try /new or check server status with /status.",
|
|
40
40
|
"bot.session_created": "✅ Session created: {title}",
|
|
41
|
-
"bot.session_busy": "⏳ Agent is already running a task. Wait for completion or use /
|
|
41
|
+
"bot.session_busy": "⏳ Agent is already running a task. Wait for completion or use /abort to interrupt current run.",
|
|
42
42
|
"bot.session_reset_project_mismatch": "⚠️ Active session does not match the selected project, so it was reset. Use /sessions to pick one or /new to create a new session.",
|
|
43
43
|
"bot.prompt_send_error": "Failed to send request to OpenCode.",
|
|
44
44
|
"bot.session_error": "🔴 OpenCode returned an error: {message}",
|
|
45
|
-
"bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /
|
|
45
|
+
"bot.session_retry": "🔁 {message}\n\nProvider keeps returning the same error on repeated retries. Use /abort to abort.",
|
|
46
46
|
"bot.unknown_command": "⚠️ Unknown command: {command}. Use /help to see available commands.",
|
|
47
47
|
"bot.photo_downloading": "⏳ Downloading photo...",
|
|
48
48
|
"bot.photo_too_large": "⚠️ Photo is too large (max {maxSizeMb}MB)",
|
|
@@ -54,7 +54,7 @@ export const en = {
|
|
|
54
54
|
"bot.file_download_error": "🔴 Failed to download file",
|
|
55
55
|
"bot.model_no_pdf": "⚠️ Current model doesn't support PDF input. Sending text only.",
|
|
56
56
|
"bot.text_file_too_large": "⚠️ Text file is too large (max {maxSizeKb}KB)",
|
|
57
|
-
"status.header_running": "🟢
|
|
57
|
+
"status.header_running": "🟢 OpenCode Server is running",
|
|
58
58
|
"status.health.healthy": "Healthy",
|
|
59
59
|
"status.health.unhealthy": "Unhealthy",
|
|
60
60
|
"status.line.health": "Status: {health}",
|
|
@@ -105,17 +105,17 @@ export const en = {
|
|
|
105
105
|
"new.create_error": "🔴 OpenCode Server is unavailable or an error occurred while creating session.",
|
|
106
106
|
"stop.no_active_session": "🛑 Agent was not started\n\nCreate a session with /new or select one via /sessions.",
|
|
107
107
|
"stop.in_progress": "🛑 Event stream stopped, sending abort signal...\n\nWaiting for agent to stop.",
|
|
108
|
-
"stop.warn_unconfirmed": "⚠️ Event stream stopped, but server did not confirm abort.\n\nCheck /status and retry /
|
|
108
|
+
"stop.warn_unconfirmed": "⚠️ Event stream stopped, but server did not confirm abort.\n\nCheck /status and retry /abort in a few seconds.",
|
|
109
109
|
"stop.warn_maybe_finished": "⚠️ Event stream stopped, but the agent may have already finished.",
|
|
110
110
|
"stop.success": "✅ Agent action interrupted. No more messages from this run will be sent.",
|
|
111
111
|
"stop.warn_still_busy": "⚠️ Signal sent, but agent is still busy.\n\nEvent stream is already disabled, so no intermediate messages will be sent.",
|
|
112
|
-
"stop.warn_timeout": "⚠️ Abort request timeout.\n\nEvent stream is already disabled, retry /
|
|
112
|
+
"stop.warn_timeout": "⚠️ Abort request timeout.\n\nEvent stream is already disabled, retry /abort in a few seconds.",
|
|
113
113
|
"stop.warn_local_only": "⚠️ Event stream stopped locally, but server-side abort failed.",
|
|
114
|
-
"stop.error": "🔴 Failed to stop action.\n\nEvent stream is stopped, try /
|
|
114
|
+
"stop.error": "🔴 Failed to stop action.\n\nEvent stream is stopped, try /abort again.",
|
|
115
115
|
"opencode_start.already_running_managed": "⚠️ OpenCode Server is already running\n\nPID: {pid}\nUptime: {seconds} seconds",
|
|
116
116
|
"opencode_start.already_running_external": "✅ OpenCode Server is already running as an external process\n\nVersion: {version}\n\nThis server was not started by bot, so /opencode-stop cannot stop it.",
|
|
117
117
|
"opencode_start.starting": "🔄 Starting OpenCode Server...",
|
|
118
|
-
"opencode_start.start_error": "🔴 Failed to start OpenCode Server\n\nError: {error}\n\nCheck that OpenCode CLI is installed and available in PATH:\
|
|
118
|
+
"opencode_start.start_error": "🔴 Failed to start OpenCode Server\n\nError: {error}\n\nCheck that OpenCode CLI is installed and available in PATH:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
119
119
|
"opencode_start.started_not_ready": "⚠️ OpenCode Server started, but is not responding\n\nPID: {pid}\n\nServer may still be starting. Try /status in a few seconds.",
|
|
120
120
|
"opencode_start.success": "✅ OpenCode Server started successfully\n\nPID: {pid}\nVersion: {version}",
|
|
121
121
|
"opencode_start.error": "🔴 An error occurred while starting server.\n\nCheck application logs for details.",
|
|
@@ -169,7 +169,7 @@ export const en = {
|
|
|
169
169
|
"permission.send_reply_error": "❌ Failed to send permission reply",
|
|
170
170
|
"permission.blocked.expected_reply": "⚠️ Please answer the permission request first using the buttons above.",
|
|
171
171
|
"permission.blocked.command_not_allowed": "⚠️ This command is not available until you answer the permission request.",
|
|
172
|
-
"permission.header": "{emoji}
|
|
172
|
+
"permission.header": "{emoji} Permission request: {name}\n\n",
|
|
173
173
|
"permission.button.allow": "✅ Allow once",
|
|
174
174
|
"permission.button.always": "🔓 Allow always",
|
|
175
175
|
"permission.button.reject": "❌ Reject",
|
|
@@ -184,6 +184,7 @@ export const en = {
|
|
|
184
184
|
"permission.name.list": "List Directory",
|
|
185
185
|
"permission.name.task": "Task",
|
|
186
186
|
"permission.name.lsp": "LSP",
|
|
187
|
+
"permission.name.external_directory": "External Directory",
|
|
187
188
|
"question.inactive_callback": "Poll is inactive",
|
|
188
189
|
"question.processing_error_callback": "Processing error",
|
|
189
190
|
"question.select_one_required_callback": "Select at least one option",
|
|
@@ -194,7 +195,7 @@ export const en = {
|
|
|
194
195
|
"question.no_active_project": "❌ No active project",
|
|
195
196
|
"question.no_active_request": "❌ No active request",
|
|
196
197
|
"question.send_answers_error": "❌ Failed to send answers to agent",
|
|
197
|
-
"question.multi_hint": "\n
|
|
198
|
+
"question.multi_hint": "\n(You can select multiple options)",
|
|
198
199
|
"question.button.submit": "✅ Done",
|
|
199
200
|
"question.button.custom": "🔤 Custom answer",
|
|
200
201
|
"question.button.cancel": "❌ Cancel",
|
|
@@ -272,7 +273,7 @@ export const en = {
|
|
|
272
273
|
"cli.args.mode_only_start": "Option --mode is supported only for the start command",
|
|
273
274
|
"legacy.models.fetch_error": "🔴 Failed to get models list. Check server status with /status.",
|
|
274
275
|
"legacy.models.empty": "📋 No available models. Configure providers in OpenCode.",
|
|
275
|
-
"legacy.models.header": "📋
|
|
276
|
+
"legacy.models.header": "📋 Available models:\n\n",
|
|
276
277
|
"legacy.models.no_provider_models": " ⚠️ No available models\n",
|
|
277
278
|
"legacy.models.env_hint": "💡 To use model in .env:\n",
|
|
278
279
|
"legacy.models.error": "🔴 An error occurred while loading models list.",
|
package/dist/i18n/es.js
CHANGED
|
@@ -38,11 +38,11 @@ export const es = {
|
|
|
38
38
|
"bot.creating_session": "🔄 Creando una sesión nueva...",
|
|
39
39
|
"bot.create_session_error": "🔴 No se pudo crear la sesión. Prueba /new o revisa el estado del servidor con /status.",
|
|
40
40
|
"bot.session_created": "✅ Sesión creada: {title}",
|
|
41
|
-
"bot.session_busy": "⏳ El agente ya está ejecutando una tarea. Espera a que termine o usa /
|
|
41
|
+
"bot.session_busy": "⏳ El agente ya está ejecutando una tarea. Espera a que termine o usa /abort para interrumpir la ejecución actual.",
|
|
42
42
|
"bot.session_reset_project_mismatch": "⚠️ La sesión activa no coincide con el proyecto seleccionado, así que se reinició. Usa /sessions para elegir una o /new para crear una nueva.",
|
|
43
43
|
"bot.prompt_send_error": "No se pudo enviar la solicitud a OpenCode.",
|
|
44
44
|
"bot.session_error": "🔴 OpenCode devolvió un error: {message}",
|
|
45
|
-
"bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /
|
|
45
|
+
"bot.session_retry": "🔁 {message}\n\nEl proveedor devuelve el mismo error en intentos repetidos. Usa /abort para detenerlo.",
|
|
46
46
|
"bot.unknown_command": "⚠️ Comando desconocido: {command}. Usa /help para ver los comandos disponibles.",
|
|
47
47
|
"bot.photo_downloading": "⏳ Descargando foto...",
|
|
48
48
|
"bot.photo_too_large": "⚠️ La foto es demasiado grande (max {maxSizeMb}MB)",
|
|
@@ -54,7 +54,7 @@ export const es = {
|
|
|
54
54
|
"bot.file_download_error": "🔴 No se pudo descargar el archivo",
|
|
55
55
|
"bot.model_no_pdf": "⚠️ El modelo actual no admite entrada PDF. Enviaré solo texto.",
|
|
56
56
|
"bot.text_file_too_large": "⚠️ El archivo de texto es demasiado grande (max {maxSizeKb}KB)",
|
|
57
|
-
"status.header_running": "🟢
|
|
57
|
+
"status.header_running": "🟢 OpenCode Server está en ejecución",
|
|
58
58
|
"status.health.healthy": "Saludable",
|
|
59
59
|
"status.health.unhealthy": "No saludable",
|
|
60
60
|
"status.line.health": "Estado: {health}",
|
|
@@ -105,17 +105,17 @@ export const es = {
|
|
|
105
105
|
"new.create_error": "🔴 OpenCode Server no está disponible u ocurrió un error al crear la sesión.",
|
|
106
106
|
"stop.no_active_session": "🛑 El agente no se inició\n\nCrea una sesión con /new o selecciona una con /sessions.",
|
|
107
107
|
"stop.in_progress": "🛑 Flujo de eventos detenido; enviando señal de aborto...\n\nEsperando a que el agente se detenga.",
|
|
108
|
-
"stop.warn_unconfirmed": "⚠️ Flujo de eventos detenido, pero el servidor no confirmó el aborto.\n\nRevisa /status y vuelve a intentar /
|
|
108
|
+
"stop.warn_unconfirmed": "⚠️ Flujo de eventos detenido, pero el servidor no confirmó el aborto.\n\nRevisa /status y vuelve a intentar /abort en unos segundos.",
|
|
109
109
|
"stop.warn_maybe_finished": "⚠️ Flujo de eventos detenido, pero el agente podría haber terminado ya.",
|
|
110
110
|
"stop.success": "✅ Acción del agente interrumpida. No se enviarán más mensajes de esta ejecución.",
|
|
111
111
|
"stop.warn_still_busy": "⚠️ Señal enviada, pero el agente sigue ocupado.\n\nEl flujo de eventos ya está deshabilitado, así que no se enviarán mensajes intermedios.",
|
|
112
|
-
"stop.warn_timeout": "⚠️ Tiempo de espera agotado al solicitar el aborto.\n\nEl flujo de eventos ya está deshabilitado; vuelve a intentar /
|
|
112
|
+
"stop.warn_timeout": "⚠️ Tiempo de espera agotado al solicitar el aborto.\n\nEl flujo de eventos ya está deshabilitado; vuelve a intentar /abort en unos segundos.",
|
|
113
113
|
"stop.warn_local_only": "⚠️ Flujo de eventos detenido localmente, pero el aborto en el servidor falló.",
|
|
114
|
-
"stop.error": "🔴 No se pudo detener la acción.\n\nEl flujo de eventos está detenido; prueba /
|
|
114
|
+
"stop.error": "🔴 No se pudo detener la acción.\n\nEl flujo de eventos está detenido; prueba /abort otra vez.",
|
|
115
115
|
"opencode_start.already_running_managed": "⚠️ OpenCode Server ya está en ejecución\n\nPID: {pid}\nTiempo activo: {seconds} segundos",
|
|
116
116
|
"opencode_start.already_running_external": "✅ OpenCode Server ya está en ejecución como un proceso externo\n\nVersión: {version}\n\nEste servidor no fue iniciado por el bot, por lo que /opencode-stop no puede detenerlo.",
|
|
117
117
|
"opencode_start.starting": "🔄 Iniciando OpenCode Server...",
|
|
118
|
-
"opencode_start.start_error": "🔴 No se pudo iniciar OpenCode Server\n\nError: {error}\n\nRevisa que OpenCode CLI esté instalado y disponible en PATH:\
|
|
118
|
+
"opencode_start.start_error": "🔴 No se pudo iniciar OpenCode Server\n\nError: {error}\n\nRevisa que OpenCode CLI esté instalado y disponible en PATH:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
119
119
|
"opencode_start.started_not_ready": "⚠️ OpenCode Server se inició, pero no responde\n\nPID: {pid}\n\nEl servidor puede estar iniciando. Prueba /status en unos segundos.",
|
|
120
120
|
"opencode_start.success": "✅ OpenCode Server iniciado correctamente\n\nPID: {pid}\nVersión: {version}",
|
|
121
121
|
"opencode_start.error": "🔴 Ocurrió un error al iniciar el servidor.\n\nRevisa los logs de la aplicación para más detalles.",
|
|
@@ -169,7 +169,7 @@ export const es = {
|
|
|
169
169
|
"permission.send_reply_error": "❌ No se pudo enviar la respuesta de permisos",
|
|
170
170
|
"permission.blocked.expected_reply": "⚠️ Primero responde a la solicitud de permisos usando los botones de arriba.",
|
|
171
171
|
"permission.blocked.command_not_allowed": "⚠️ Este comando no está disponible hasta que respondas a la solicitud de permisos.",
|
|
172
|
-
"permission.header": "{emoji}
|
|
172
|
+
"permission.header": "{emoji} Solicitud de permisos: {name}\n\n",
|
|
173
173
|
"permission.button.allow": "✅ Permitir una vez",
|
|
174
174
|
"permission.button.always": "🔓 Permitir siempre",
|
|
175
175
|
"permission.button.reject": "❌ Rechazar",
|
|
@@ -184,6 +184,7 @@ export const es = {
|
|
|
184
184
|
"permission.name.list": "Listar directorio",
|
|
185
185
|
"permission.name.task": "Tarea",
|
|
186
186
|
"permission.name.lsp": "LSP",
|
|
187
|
+
"permission.name.external_directory": "Directorio externo",
|
|
187
188
|
"question.inactive_callback": "La encuesta está inactiva",
|
|
188
189
|
"question.processing_error_callback": "Error de procesamiento",
|
|
189
190
|
"question.select_one_required_callback": "Selecciona al menos una opción",
|
|
@@ -194,7 +195,7 @@ export const es = {
|
|
|
194
195
|
"question.no_active_project": "❌ No hay un proyecto activo",
|
|
195
196
|
"question.no_active_request": "❌ No hay una solicitud activa",
|
|
196
197
|
"question.send_answers_error": "❌ No se pudieron enviar las respuestas al agente",
|
|
197
|
-
"question.multi_hint": "\n
|
|
198
|
+
"question.multi_hint": "\n(Puedes seleccionar varias opciones)",
|
|
198
199
|
"question.button.submit": "✅ Listo",
|
|
199
200
|
"question.button.custom": "🔤 Respuesta personalizada",
|
|
200
201
|
"question.button.cancel": "❌ Cancelar",
|
|
@@ -272,7 +273,7 @@ export const es = {
|
|
|
272
273
|
"cli.args.mode_only_start": "La opción --mode solo se admite para el comando start",
|
|
273
274
|
"legacy.models.fetch_error": "🔴 No se pudo obtener la lista de modelos. Revisa el estado del servidor con /status.",
|
|
274
275
|
"legacy.models.empty": "📋 No hay modelos disponibles. Configura los proveedores en OpenCode.",
|
|
275
|
-
"legacy.models.header": "📋
|
|
276
|
+
"legacy.models.header": "📋 Modelos disponibles:\n\n",
|
|
276
277
|
"legacy.models.no_provider_models": " ⚠️ No hay modelos disponibles\n",
|
|
277
278
|
"legacy.models.env_hint": "💡 Para usar el modelo en .env:\n",
|
|
278
279
|
"legacy.models.error": "🔴 Ocurrió un error al cargar la lista de modelos.",
|
package/dist/i18n/ru.js
CHANGED
|
@@ -38,11 +38,11 @@ export const ru = {
|
|
|
38
38
|
"bot.creating_session": "🔄 Создаю новую сессию...",
|
|
39
39
|
"bot.create_session_error": "🔴 Не удалось создать сессию. Попробуйте команду /new или проверьте статус сервера /status.",
|
|
40
40
|
"bot.session_created": "✅ Сессия создана: {title}",
|
|
41
|
-
"bot.session_busy": "⏳ Агент уже выполняет задачу. Дождитесь завершения или используйте /
|
|
41
|
+
"bot.session_busy": "⏳ Агент уже выполняет задачу. Дождитесь завершения или используйте /abort, чтобы прервать текущий запуск.",
|
|
42
42
|
"bot.session_reset_project_mismatch": "⚠️ Активная сессия не соответствует выбранному проекту, поэтому была сброшена. Используйте /sessions для выбора или /new для создания новой сессии.",
|
|
43
43
|
"bot.prompt_send_error": "Не удалось отправить запрос в OpenCode.",
|
|
44
44
|
"bot.session_error": "🔴 OpenCode вернул ошибку: {message}",
|
|
45
|
-
"bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /
|
|
45
|
+
"bot.session_retry": "🔁 {message}\n\nПровайдер возвращает одну и ту же ошибку при повторных запросах. Используйте /abort для остановки.",
|
|
46
46
|
"bot.unknown_command": "⚠️ Неизвестная команда: {command}. Используйте /help для списка команд.",
|
|
47
47
|
"bot.photo_downloading": "⏳ Скачиваю фото...",
|
|
48
48
|
"bot.photo_too_large": "⚠️ Фото слишком большое (макс. {maxSizeMb}МБ)",
|
|
@@ -54,7 +54,7 @@ export const ru = {
|
|
|
54
54
|
"bot.file_download_error": "🔴 Не удалось скачать файл",
|
|
55
55
|
"bot.model_no_pdf": "⚠️ Текущая модель не поддерживает PDF. Отправляю только текст.",
|
|
56
56
|
"bot.text_file_too_large": "⚠️ Текстовый файл слишком большой (макс. {maxSizeKb}КБ)",
|
|
57
|
-
"status.header_running": "🟢
|
|
57
|
+
"status.header_running": "🟢 OpenCode Server запущен",
|
|
58
58
|
"status.health.healthy": "Healthy",
|
|
59
59
|
"status.health.unhealthy": "Unhealthy",
|
|
60
60
|
"status.line.health": "Статус: {health}",
|
|
@@ -105,17 +105,17 @@ export const ru = {
|
|
|
105
105
|
"new.create_error": "🔴 OpenCode Server недоступен или произошла ошибка при создании сессии.",
|
|
106
106
|
"stop.no_active_session": "🛑 Агент не был запущен\n\nСначала создайте сессию командой /new или выберите существующую через /sessions.",
|
|
107
107
|
"stop.in_progress": "🛑 Отключил поток событий и отправляю сигнал прерывания...\n\nОжидание остановки агента.",
|
|
108
|
-
"stop.warn_unconfirmed": "⚠️ Поток событий остановлен, но сервер не подтвердил прерывание.\n\nПроверьте /status и повторите /
|
|
108
|
+
"stop.warn_unconfirmed": "⚠️ Поток событий остановлен, но сервер не подтвердил прерывание.\n\nПроверьте /status и повторите /abort через пару секунд.",
|
|
109
109
|
"stop.warn_maybe_finished": "⚠️ Поток событий остановлен, но агент мог уже завершиться к моменту запроса.",
|
|
110
110
|
"stop.success": "✅ Действие агента прервано. Новые сообщения от текущего запуска больше не придут.",
|
|
111
111
|
"stop.warn_still_busy": "⚠️ Сигнал отправлен, но агент еще busy.\n\nПоток событий уже отключен, поэтому бот не будет присылать промежуточные сообщения.",
|
|
112
|
-
"stop.warn_timeout": "⚠️ Таймаут запроса на прерывание.\n\nПоток событий уже отключен, повторите /
|
|
112
|
+
"stop.warn_timeout": "⚠️ Таймаут запроса на прерывание.\n\nПоток событий уже отключен, повторите /abort через пару секунд.",
|
|
113
113
|
"stop.warn_local_only": "⚠️ Поток событий остановлен локально, но при прерывании на сервере произошла ошибка.",
|
|
114
|
-
"stop.error": "🔴 Ошибка при прерывании действия.\n\nПоток событий остановлен, попробуйте /
|
|
114
|
+
"stop.error": "🔴 Ошибка при прерывании действия.\n\nПоток событий остановлен, попробуйте /abort еще раз.",
|
|
115
115
|
"opencode_start.already_running_managed": "⚠️ OpenCode Server уже запущен\n\nPID: {pid}\nUptime: {seconds} секунд",
|
|
116
116
|
"opencode_start.already_running_external": "✅ OpenCode Server уже запущен внешним процессом\n\nВерсия: {version}\n\nЭтот сервер не был запущен через бота, поэтому команда /opencode-stop не сможет его остановить.",
|
|
117
117
|
"opencode_start.starting": "🔄 Запускаю OpenCode Server...",
|
|
118
|
-
"opencode_start.start_error": "🔴 Не удалось запустить OpenCode Server\n\nОшибка: {error}\n\nПроверьте, что OpenCode CLI установлен и доступен в PATH:\
|
|
118
|
+
"opencode_start.start_error": "🔴 Не удалось запустить OpenCode Server\n\nОшибка: {error}\n\nПроверьте, что OpenCode CLI установлен и доступен в PATH:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
119
119
|
"opencode_start.started_not_ready": "⚠️ OpenCode Server запущен, но не отвечает\n\nPID: {pid}\n\nСервер может запускаться. Попробуйте /status через несколько секунд.",
|
|
120
120
|
"opencode_start.success": "✅ OpenCode Server успешно запущен\n\nPID: {pid}\nВерсия: {version}",
|
|
121
121
|
"opencode_start.error": "🔴 Произошла ошибка при запуске сервера.\n\nПроверьте логи приложения для подробностей.",
|
|
@@ -169,7 +169,7 @@ export const ru = {
|
|
|
169
169
|
"permission.send_reply_error": "❌ Не удалось отправить ответ на запрос разрешения",
|
|
170
170
|
"permission.blocked.expected_reply": "⚠️ Сначала ответьте на запрос разрешения кнопками выше.",
|
|
171
171
|
"permission.blocked.command_not_allowed": "⚠️ Эта команда недоступна, пока вы не ответите на запрос разрешения.",
|
|
172
|
-
"permission.header": "{emoji}
|
|
172
|
+
"permission.header": "{emoji} Запрос разрешения: {name}\n\n",
|
|
173
173
|
"permission.button.allow": "✅ Разрешить один раз",
|
|
174
174
|
"permission.button.always": "🔓 Разрешить всегда",
|
|
175
175
|
"permission.button.reject": "❌ Отклонить",
|
|
@@ -184,6 +184,7 @@ export const ru = {
|
|
|
184
184
|
"permission.name.list": "List Directory",
|
|
185
185
|
"permission.name.task": "Task",
|
|
186
186
|
"permission.name.lsp": "LSP",
|
|
187
|
+
"permission.name.external_directory": "Внешняя директория",
|
|
187
188
|
"question.inactive_callback": "Опрос неактивен",
|
|
188
189
|
"question.processing_error_callback": "Ошибка при обработке",
|
|
189
190
|
"question.select_one_required_callback": "Выберите хотя бы один вариант",
|
|
@@ -194,7 +195,7 @@ export const ru = {
|
|
|
194
195
|
"question.no_active_project": "❌ Нет активного проекта",
|
|
195
196
|
"question.no_active_request": "❌ Нет активного запроса",
|
|
196
197
|
"question.send_answers_error": "❌ Не удалось отправить ответы агенту",
|
|
197
|
-
"question.multi_hint": "\n
|
|
198
|
+
"question.multi_hint": "\n(Можно выбрать несколько вариантов)",
|
|
198
199
|
"question.button.submit": "✅ Готово",
|
|
199
200
|
"question.button.custom": "🔤 Свой ответ",
|
|
200
201
|
"question.button.cancel": "❌ Отмена",
|
|
@@ -272,7 +273,7 @@ export const ru = {
|
|
|
272
273
|
"cli.args.mode_only_start": "Опция --mode поддерживается только для команды start",
|
|
273
274
|
"legacy.models.fetch_error": "🔴 Не удалось получить список моделей. Проверьте статус сервера /status.",
|
|
274
275
|
"legacy.models.empty": "📋 Нет доступных моделей. Настройте провайдеры через OpenCode.",
|
|
275
|
-
"legacy.models.header": "📋
|
|
276
|
+
"legacy.models.header": "📋 Доступные модели:\n\n",
|
|
276
277
|
"legacy.models.no_provider_models": " ⚠️ Нет доступных моделей\n",
|
|
277
278
|
"legacy.models.env_hint": "💡 Для использования модели в .env:\n",
|
|
278
279
|
"legacy.models.error": "🔴 Произошла ошибка при получении списка моделей.",
|
package/dist/i18n/zh.js
CHANGED
|
@@ -38,11 +38,11 @@ export const zh = {
|
|
|
38
38
|
"bot.creating_session": "🔄 正在创建新会话...",
|
|
39
39
|
"bot.create_session_error": "🔴 创建会话失败。请重试 /new,或使用 /status 检查服务器状态。",
|
|
40
40
|
"bot.session_created": "✅ 会话已创建:{title}",
|
|
41
|
-
"bot.session_busy": "⏳ 代理正在执行任务。请等待完成,或使用 /
|
|
41
|
+
"bot.session_busy": "⏳ 代理正在执行任务。请等待完成,或使用 /abort 中断当前运行。",
|
|
42
42
|
"bot.session_reset_project_mismatch": "⚠️ 活动会话与所选项目不匹配,因此已重置。使用 /sessions 选择一个会话,或 /new 创建新会话。",
|
|
43
43
|
"bot.prompt_send_error": "向 OpenCode 发送请求失败。",
|
|
44
44
|
"bot.session_error": "🔴 OpenCode 返回错误:{message}",
|
|
45
|
-
"bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /
|
|
45
|
+
"bot.session_retry": "🔁 {message}\n\n提供方在重复重试时持续返回同一错误。使用 /abort 可停止。",
|
|
46
46
|
"bot.unknown_command": "⚠️ 未知命令:{command}。使用 /help 查看可用命令。",
|
|
47
47
|
"bot.photo_downloading": "⏳ 正在下载照片...",
|
|
48
48
|
"bot.photo_too_large": "⚠️ 照片过大(最大 {maxSizeMb}MB)",
|
|
@@ -54,7 +54,7 @@ export const zh = {
|
|
|
54
54
|
"bot.file_download_error": "🔴 下载文件失败",
|
|
55
55
|
"bot.model_no_pdf": "⚠️ 当前模型不支持PDF输入。将仅发送文本。",
|
|
56
56
|
"bot.text_file_too_large": "⚠️ 文本文件过大(最大 {maxSizeKb}KB)",
|
|
57
|
-
"status.header_running": "🟢
|
|
57
|
+
"status.header_running": "🟢 OpenCode 服务器正在运行",
|
|
58
58
|
"status.health.healthy": "健康",
|
|
59
59
|
"status.health.unhealthy": "不健康",
|
|
60
60
|
"status.line.health": "状态:{health}",
|
|
@@ -105,17 +105,17 @@ export const zh = {
|
|
|
105
105
|
"new.create_error": "🔴 OpenCode 服务器不可用,或创建会话时发生错误。",
|
|
106
106
|
"stop.no_active_session": "🛑 代理尚未启动\n\n使用 /new 创建会话,或通过 /sessions 选择一个。",
|
|
107
107
|
"stop.in_progress": "🛑 已停止事件流,正在发送中止信号...\n\n等待代理停止。",
|
|
108
|
-
"stop.warn_unconfirmed": "⚠️ 事件流已停止,但服务器未确认中止。\n\n检查 /status,并在几秒后重试 /
|
|
108
|
+
"stop.warn_unconfirmed": "⚠️ 事件流已停止,但服务器未确认中止。\n\n检查 /status,并在几秒后重试 /abort。",
|
|
109
109
|
"stop.warn_maybe_finished": "⚠️ 事件流已停止,但代理可能已完成。",
|
|
110
110
|
"stop.success": "✅ 代理操作已中断。本次运行的后续消息将不再发送。",
|
|
111
111
|
"stop.warn_still_busy": "⚠️ 信号已发送,但代理仍在忙。\n\n事件流已禁用,因此不会发送中间消息。",
|
|
112
|
-
"stop.warn_timeout": "⚠️ 中止请求超时。\n\n事件流已禁用,请在几秒后重试 /
|
|
112
|
+
"stop.warn_timeout": "⚠️ 中止请求超时。\n\n事件流已禁用,请在几秒后重试 /abort。",
|
|
113
113
|
"stop.warn_local_only": "⚠️ 已在本地停止事件流,但服务器端中止失败。",
|
|
114
|
-
"stop.error": "🔴 停止操作失败。\n\n事件流已停止,请再次尝试 /
|
|
114
|
+
"stop.error": "🔴 停止操作失败。\n\n事件流已停止,请再次尝试 /abort。",
|
|
115
115
|
"opencode_start.already_running_managed": "⚠️ OpenCode 服务器已在运行\n\nPID:{pid}\n运行时间:{seconds} 秒",
|
|
116
116
|
"opencode_start.already_running_external": "✅ OpenCode 服务器正作为外部进程运行\n\n版本:{version}\n\n该服务器不是由机器人启动,因此 /opencode-stop 无法停止它。",
|
|
117
117
|
"opencode_start.starting": "🔄 正在启动 OpenCode 服务器...",
|
|
118
|
-
"opencode_start.start_error": "🔴 启动 OpenCode 服务器失败\n\n错误:{error}\n\n请检查 OpenCode CLI 已安装且在 PATH 中可用:\
|
|
118
|
+
"opencode_start.start_error": "🔴 启动 OpenCode 服务器失败\n\n错误:{error}\n\n请检查 OpenCode CLI 已安装且在 PATH 中可用:\nopencode --version\nnpm install -g @opencode-ai/cli",
|
|
119
119
|
"opencode_start.started_not_ready": "⚠️ OpenCode 服务器已启动,但未响应\n\nPID:{pid}\n\n服务器可能仍在启动中。几秒后试试 /status。",
|
|
120
120
|
"opencode_start.success": "✅ OpenCode 服务器启动成功\n\nPID:{pid}\n版本:{version}",
|
|
121
121
|
"opencode_start.error": "🔴 启动服务器时发生错误。\n\n请查看应用日志了解详情。",
|
|
@@ -169,7 +169,7 @@ export const zh = {
|
|
|
169
169
|
"permission.send_reply_error": "❌ 发送权限回复失败",
|
|
170
170
|
"permission.blocked.expected_reply": "⚠️ 请先使用上方按钮回答权限请求。",
|
|
171
171
|
"permission.blocked.command_not_allowed": "⚠️ 在你回答权限请求之前不可用此命令。",
|
|
172
|
-
"permission.header": "{emoji}
|
|
172
|
+
"permission.header": "{emoji} 权限请求:{name}\n\n",
|
|
173
173
|
"permission.button.allow": "✅ 允许一次",
|
|
174
174
|
"permission.button.always": "🔓 始终允许",
|
|
175
175
|
"permission.button.reject": "❌ 拒绝",
|
|
@@ -184,6 +184,7 @@ export const zh = {
|
|
|
184
184
|
"permission.name.list": "列出目录",
|
|
185
185
|
"permission.name.task": "任务",
|
|
186
186
|
"permission.name.lsp": "LSP",
|
|
187
|
+
"permission.name.external_directory": "外部目录",
|
|
187
188
|
"question.inactive_callback": "投票已失效",
|
|
188
189
|
"question.processing_error_callback": "处理错误",
|
|
189
190
|
"question.select_one_required_callback": "请至少选择一个选项",
|
|
@@ -194,7 +195,7 @@ export const zh = {
|
|
|
194
195
|
"question.no_active_project": "❌ 没有活动项目",
|
|
195
196
|
"question.no_active_request": "❌ 没有活动请求",
|
|
196
197
|
"question.send_answers_error": "❌ 向代理发送答案失败",
|
|
197
|
-
"question.multi_hint": "\n
|
|
198
|
+
"question.multi_hint": "\n(你可以选择多个选项)",
|
|
198
199
|
"question.button.submit": "✅ 完成",
|
|
199
200
|
"question.button.custom": "🔤 自定义回答",
|
|
200
201
|
"question.button.cancel": "❌ 取消",
|
|
@@ -272,7 +273,7 @@ export const zh = {
|
|
|
272
273
|
"cli.args.mode_only_start": "选项 --mode 仅支持 start 命令",
|
|
273
274
|
"legacy.models.fetch_error": "🔴 获取模型列表失败。请使用 /status 检查服务器状态。",
|
|
274
275
|
"legacy.models.empty": "📋 没有可用模型。请在 OpenCode 中配置 providers。",
|
|
275
|
-
"legacy.models.header": "📋
|
|
276
|
+
"legacy.models.header": "📋 可用模型:\n\n",
|
|
276
277
|
"legacy.models.no_provider_models": " ⚠️ 没有可用模型\n",
|
|
277
278
|
"legacy.models.env_hint": "💡 在 .env 中使用该模型:\n",
|
|
278
279
|
"legacy.models.error": "🔴 加载模型列表时发生错误。",
|
|
@@ -73,6 +73,9 @@ export function resolveInteractionGuardDecision(ctx) {
|
|
|
73
73
|
return createBlockDecision(inputType, state, "expired", command);
|
|
74
74
|
}
|
|
75
75
|
if (inputType === "command") {
|
|
76
|
+
if (command === "/start") {
|
|
77
|
+
return createAllowDecision(inputType, state, command);
|
|
78
|
+
}
|
|
76
79
|
if (command && state.allowedCommands.includes(command)) {
|
|
77
80
|
return createAllowDecision(inputType, state, command);
|
|
78
81
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { logger } from "../utils/logger.js";
|
|
2
|
-
export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/
|
|
2
|
+
export const DEFAULT_ALLOWED_INTERACTION_COMMANDS = ["/help", "/status", "/abort"];
|
|
3
3
|
function normalizeCommand(command) {
|
|
4
4
|
const trimmed = command.trim().toLowerCase();
|
|
5
5
|
if (!trimmed) {
|
package/dist/model/manager.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { getCurrentModel, setCurrentModel } from "../settings/manager.js";
|
|
2
2
|
import { config } from "../config.js";
|
|
3
|
+
import { opencodeClient } from "../opencode/client.js";
|
|
3
4
|
import { logger } from "../utils/logger.js";
|
|
4
5
|
import path from "node:path";
|
|
6
|
+
const MODEL_CATALOG_CACHE_TTL_MS = 10 * 60 * 1000;
|
|
7
|
+
let cachedValidModelKeys = null;
|
|
8
|
+
let modelCatalogCacheExpiresAt = 0;
|
|
9
|
+
let modelCatalogFetchInFlight = null;
|
|
10
|
+
function getModelKey(providerID, modelID) {
|
|
11
|
+
return `${providerID}/${modelID}`;
|
|
12
|
+
}
|
|
5
13
|
function getEnvDefaultModel() {
|
|
6
14
|
const providerID = config.opencode.model.provider;
|
|
7
15
|
const modelID = config.opencode.model.modelId;
|
|
@@ -20,6 +28,58 @@ function dedupeModels(models) {
|
|
|
20
28
|
}
|
|
21
29
|
return Array.from(unique.values());
|
|
22
30
|
}
|
|
31
|
+
function filterModelsByCatalog(models, validModelKeys) {
|
|
32
|
+
if (!validModelKeys) {
|
|
33
|
+
return models;
|
|
34
|
+
}
|
|
35
|
+
return models.filter((model) => validModelKeys.has(getModelKey(model.providerID, model.modelID)));
|
|
36
|
+
}
|
|
37
|
+
async function getValidModelKeys() {
|
|
38
|
+
if (cachedValidModelKeys && Date.now() < modelCatalogCacheExpiresAt) {
|
|
39
|
+
logger.debug(`[ModelManager] Model catalog cache hit: models=${cachedValidModelKeys.size}, ttlMs=${modelCatalogCacheExpiresAt - Date.now()}`);
|
|
40
|
+
return cachedValidModelKeys;
|
|
41
|
+
}
|
|
42
|
+
if (modelCatalogFetchInFlight) {
|
|
43
|
+
logger.debug("[ModelManager] Awaiting in-flight model catalog refresh");
|
|
44
|
+
return modelCatalogFetchInFlight;
|
|
45
|
+
}
|
|
46
|
+
modelCatalogFetchInFlight = (async () => {
|
|
47
|
+
try {
|
|
48
|
+
logger.debug("[ModelManager] Refreshing model catalog from OpenCode API");
|
|
49
|
+
const response = await opencodeClient.config.providers();
|
|
50
|
+
if (response.error || !response.data) {
|
|
51
|
+
logger.warn("[ModelManager] Failed to refresh model catalog:", response.error);
|
|
52
|
+
if (cachedValidModelKeys) {
|
|
53
|
+
logger.warn("[ModelManager] Using stale model catalog cache after refresh failure");
|
|
54
|
+
return cachedValidModelKeys;
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
const validModelKeys = new Set();
|
|
59
|
+
for (const provider of response.data.providers) {
|
|
60
|
+
for (const modelID of Object.keys(provider.models)) {
|
|
61
|
+
validModelKeys.add(getModelKey(provider.id, modelID));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
cachedValidModelKeys = validModelKeys;
|
|
65
|
+
modelCatalogCacheExpiresAt = Date.now() + MODEL_CATALOG_CACHE_TTL_MS;
|
|
66
|
+
logger.debug(`[ModelManager] Model catalog refreshed: providers=${response.data.providers.length}, models=${validModelKeys.size}`);
|
|
67
|
+
return cachedValidModelKeys;
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
logger.warn("[ModelManager] Error refreshing model catalog:", err);
|
|
71
|
+
if (cachedValidModelKeys) {
|
|
72
|
+
logger.warn("[ModelManager] Using stale model catalog cache after refresh exception");
|
|
73
|
+
return cachedValidModelKeys;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
modelCatalogFetchInFlight = null;
|
|
79
|
+
}
|
|
80
|
+
})();
|
|
81
|
+
return modelCatalogFetchInFlight;
|
|
82
|
+
}
|
|
23
83
|
function normalizeFavoriteModels(state) {
|
|
24
84
|
if (!Array.isArray(state.favorite)) {
|
|
25
85
|
return [];
|
|
@@ -68,17 +128,27 @@ export async function getModelSelectionLists() {
|
|
|
68
128
|
const content = await fs.readFile(stateFilePath, "utf-8");
|
|
69
129
|
const state = JSON.parse(content);
|
|
70
130
|
const rawFavorites = normalizeFavoriteModels(state);
|
|
131
|
+
const rawRecent = normalizeRecentModels(state);
|
|
132
|
+
const shouldValidateWithCatalog = rawFavorites.length > 0 || rawRecent.length > 0;
|
|
133
|
+
const validModelKeys = shouldValidateWithCatalog ? await getValidModelKeys() : null;
|
|
134
|
+
const validatedFavorites = filterModelsByCatalog(rawFavorites, validModelKeys);
|
|
135
|
+
const validatedRecent = filterModelsByCatalog(rawRecent, validModelKeys);
|
|
71
136
|
const favorites = envDefaultModel
|
|
72
|
-
? dedupeModels([...
|
|
73
|
-
:
|
|
137
|
+
? dedupeModels([...validatedFavorites, envDefaultModel])
|
|
138
|
+
: validatedFavorites;
|
|
74
139
|
if (rawFavorites.length === 0 && envDefaultModel) {
|
|
75
140
|
logger.info(`[ModelManager] No favorites in ${stateFilePath}, using config model as favorite`);
|
|
76
141
|
}
|
|
77
142
|
if (favorites.length === 0) {
|
|
78
143
|
logger.warn(`[ModelManager] No favorites in ${stateFilePath}`);
|
|
79
144
|
}
|
|
80
|
-
const
|
|
81
|
-
const
|
|
145
|
+
const filteredOutFavorites = rawFavorites.length - validatedFavorites.length;
|
|
146
|
+
const filteredOutRecent = rawRecent.length - validatedRecent.length;
|
|
147
|
+
if (filteredOutFavorites > 0 || filteredOutRecent > 0) {
|
|
148
|
+
logger.info(`[ModelManager] Filtered unavailable models from OpenCode state: favoritesRemoved=${filteredOutFavorites}, recentRemoved=${filteredOutRecent}`);
|
|
149
|
+
}
|
|
150
|
+
const favoriteKeys = new Set(favorites.map((model) => getModelKey(model.providerID, model.modelID)));
|
|
151
|
+
const recent = dedupeModels(validatedRecent).filter((model) => !favoriteKeys.has(getModelKey(model.providerID, model.modelID)));
|
|
82
152
|
logger.debug(`[ModelManager] Loaded model selection lists from ${stateFilePath}: favorites=${favorites.length}, recent=${recent.length}`);
|
|
83
153
|
return { favorites, recent };
|
|
84
154
|
}
|
|
@@ -97,6 +167,42 @@ export async function getModelSelectionLists() {
|
|
|
97
167
|
};
|
|
98
168
|
}
|
|
99
169
|
}
|
|
170
|
+
/**
|
|
171
|
+
* Validate stored selected model against OpenCode providers catalog.
|
|
172
|
+
* If selected model is unavailable, fallback to env default model.
|
|
173
|
+
*/
|
|
174
|
+
export async function reconcileStoredModelSelection() {
|
|
175
|
+
const currentModel = getCurrentModel();
|
|
176
|
+
if (!currentModel?.providerID || !currentModel.modelID) {
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
const validModelKeys = await getValidModelKeys();
|
|
180
|
+
if (!validModelKeys) {
|
|
181
|
+
logger.warn("[ModelManager] Skipping stored model validation: model catalog unavailable");
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const currentModelKey = getModelKey(currentModel.providerID, currentModel.modelID);
|
|
185
|
+
if (validModelKeys.has(currentModelKey)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
const envDefaultModel = getEnvDefaultModel();
|
|
189
|
+
if (!envDefaultModel) {
|
|
190
|
+
logger.warn(`[ModelManager] Stored model ${currentModelKey} is unavailable and env default model is missing`);
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
const fallbackKey = getModelKey(envDefaultModel.providerID, envDefaultModel.modelID);
|
|
194
|
+
logger.warn(`[ModelManager] Stored model ${currentModelKey} is unavailable, falling back to ${fallbackKey}`);
|
|
195
|
+
setCurrentModel({
|
|
196
|
+
providerID: envDefaultModel.providerID,
|
|
197
|
+
modelID: envDefaultModel.modelID,
|
|
198
|
+
variant: "default",
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
export function __resetModelCatalogCacheForTests() {
|
|
202
|
+
cachedValidModelKeys = null;
|
|
203
|
+
modelCatalogCacheExpiresAt = 0;
|
|
204
|
+
modelCatalogFetchInFlight = null;
|
|
205
|
+
}
|
|
100
206
|
/**
|
|
101
207
|
* Get list of favorite models from OpenCode local state file
|
|
102
208
|
* Falls back to env default model if file is unavailable or empty
|
package/dist/pinned/manager.js
CHANGED
|
@@ -128,7 +128,7 @@ class PinnedMessageManager {
|
|
|
128
128
|
*/
|
|
129
129
|
async onMessageComplete(tokens) {
|
|
130
130
|
// Ensure context limit is available even if session was restored
|
|
131
|
-
// without a fresh onSessionChange call (for example after /
|
|
131
|
+
// without a fresh onSessionChange call (for example after /abort + continue).
|
|
132
132
|
if (this.getContextLimit() === 0) {
|
|
133
133
|
await this.fetchContextLimit();
|
|
134
134
|
}
|