zardbot-telegram 1.0.0
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/.env.example +116 -0
- package/LICENSE +21 -0
- package/README.md +250 -0
- package/dist/agent/manager.js +88 -0
- package/dist/agent/types.js +26 -0
- package/dist/app/start-bot-app.js +49 -0
- package/dist/bot/commands/abort.js +121 -0
- package/dist/bot/commands/commands.js +480 -0
- package/dist/bot/commands/definitions.js +27 -0
- package/dist/bot/commands/help.js +10 -0
- package/dist/bot/commands/models.js +38 -0
- package/dist/bot/commands/new.js +70 -0
- package/dist/bot/commands/opencode-start.js +101 -0
- package/dist/bot/commands/opencode-stop.js +44 -0
- package/dist/bot/commands/projects.js +223 -0
- package/dist/bot/commands/rename.js +139 -0
- package/dist/bot/commands/sessions.js +351 -0
- package/dist/bot/commands/start.js +43 -0
- package/dist/bot/commands/status.js +95 -0
- package/dist/bot/commands/task.js +399 -0
- package/dist/bot/commands/tasklist.js +220 -0
- package/dist/bot/commands/voice.js +145 -0
- package/dist/bot/handlers/agent.js +118 -0
- package/dist/bot/handlers/context.js +100 -0
- package/dist/bot/handlers/document.js +65 -0
- package/dist/bot/handlers/inline-menu.js +119 -0
- package/dist/bot/handlers/model.js +143 -0
- package/dist/bot/handlers/permission.js +235 -0
- package/dist/bot/handlers/prompt.js +240 -0
- package/dist/bot/handlers/question.js +390 -0
- package/dist/bot/handlers/tts.js +89 -0
- package/dist/bot/handlers/variant.js +138 -0
- package/dist/bot/handlers/voice.js +173 -0
- package/dist/bot/index.js +977 -0
- package/dist/bot/message-patterns.js +4 -0
- package/dist/bot/middleware/auth.js +30 -0
- package/dist/bot/middleware/interaction-guard.js +95 -0
- package/dist/bot/middleware/unknown-command.js +22 -0
- package/dist/bot/streaming/response-streamer.js +286 -0
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/busy-guard.js +15 -0
- package/dist/bot/utils/commands.js +21 -0
- package/dist/bot/utils/file-download.js +91 -0
- package/dist/bot/utils/finalize-assistant-response.js +52 -0
- package/dist/bot/utils/keyboard.js +69 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +165 -0
- package/dist/bot/utils/telegram-text.js +28 -0
- package/dist/bot/utils/thinking-message.js +8 -0
- package/dist/cli/args.js +98 -0
- package/dist/cli.js +80 -0
- package/dist/config.js +97 -0
- package/dist/i18n/de.js +357 -0
- package/dist/i18n/en.js +357 -0
- package/dist/i18n/es.js +357 -0
- package/dist/i18n/fr.js +357 -0
- package/dist/i18n/index.js +109 -0
- package/dist/i18n/ru.js +357 -0
- package/dist/i18n/zh.js +357 -0
- package/dist/index.js +26 -0
- package/dist/interaction/busy.js +8 -0
- package/dist/interaction/cleanup.js +32 -0
- package/dist/interaction/guard.js +140 -0
- package/dist/interaction/manager.js +106 -0
- package/dist/interaction/types.js +1 -0
- package/dist/keyboard/manager.js +172 -0
- package/dist/keyboard/types.js +1 -0
- package/dist/model/capabilities.js +62 -0
- package/dist/model/context-limit.js +57 -0
- package/dist/model/manager.js +259 -0
- package/dist/model/types.js +24 -0
- package/dist/opencode/client.js +13 -0
- package/dist/opencode/events.js +140 -0
- package/dist/permission/manager.js +100 -0
- package/dist/permission/types.js +1 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +682 -0
- package/dist/pinned/types.js +1 -0
- package/dist/process/manager.js +273 -0
- package/dist/process/types.js +1 -0
- package/dist/project/manager.js +88 -0
- package/dist/question/manager.js +176 -0
- package/dist/question/types.js +1 -0
- package/dist/rename/manager.js +53 -0
- package/dist/runtime/bootstrap.js +350 -0
- package/dist/runtime/mode.js +74 -0
- package/dist/runtime/paths.js +37 -0
- package/dist/scheduled-task/creation-manager.js +113 -0
- package/dist/scheduled-task/display.js +239 -0
- package/dist/scheduled-task/executor.js +87 -0
- package/dist/scheduled-task/foreground-state.js +32 -0
- package/dist/scheduled-task/next-run.js +207 -0
- package/dist/scheduled-task/runtime.js +368 -0
- package/dist/scheduled-task/schedule-parser.js +169 -0
- package/dist/scheduled-task/store.js +65 -0
- package/dist/scheduled-task/types.js +19 -0
- package/dist/session/cache-manager.js +455 -0
- package/dist/session/manager.js +10 -0
- package/dist/settings/manager.js +158 -0
- package/dist/stt/client.js +97 -0
- package/dist/summary/aggregator.js +1136 -0
- package/dist/summary/formatter.js +491 -0
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +90 -0
- package/dist/tts/client.js +130 -0
- package/dist/utils/error-format.js +29 -0
- package/dist/utils/logger.js +127 -0
- package/dist/utils/safe-background-task.js +33 -0
- package/dist/utils/telegram-rate-limit-retry.js +93 -0
- package/dist/variant/manager.js +103 -0
- package/dist/variant/types.js +1 -0
- package/package.json +79 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
import { logger } from "../utils/logger.js";
|
|
3
|
+
const TTS_REQUEST_TIMEOUT_MS = 2_400_000; // 40 minutes for long responses
|
|
4
|
+
/**
|
|
5
|
+
* Returns true if TTS is configured (API URL is set).
|
|
6
|
+
*/
|
|
7
|
+
export function isTtsConfigured() {
|
|
8
|
+
return Boolean(config.tts?.apiUrl);
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Fetches available voices from the TTS server.
|
|
12
|
+
*
|
|
13
|
+
* GETs from `{TTS_API_URL}/v1/audio/voices` (Pocket TTS API).
|
|
14
|
+
*
|
|
15
|
+
* @returns Array of available voices
|
|
16
|
+
*/
|
|
17
|
+
export async function getAvailableVoices() {
|
|
18
|
+
if (!isTtsConfigured()) {
|
|
19
|
+
logger.warn("[TTS] Cannot fetch voices: TTS API URL not configured");
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const voicesUrl = `${config.tts.apiUrl}/v1/audio/voices`;
|
|
24
|
+
const response = await fetch(voicesUrl, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
signal: AbortSignal.timeout(30_000),
|
|
27
|
+
});
|
|
28
|
+
if (response.ok) {
|
|
29
|
+
const rawData = await response.text();
|
|
30
|
+
logger.debug("[TTS] Raw voices response:", rawData.substring(0, 200));
|
|
31
|
+
const parsed = JSON.parse(rawData);
|
|
32
|
+
// Pocket TTS returns {voices: [{voice_id, name}, ...]} or an array
|
|
33
|
+
const voices = Array.isArray(parsed)
|
|
34
|
+
? parsed
|
|
35
|
+
: parsed.voices ||
|
|
36
|
+
parsed.data ||
|
|
37
|
+
[];
|
|
38
|
+
logger.info(`[TTS] Parsed voices count: ${voices.length}`);
|
|
39
|
+
if (Array.isArray(voices) && voices.length > 0) {
|
|
40
|
+
return voices.map((v) => {
|
|
41
|
+
const voice = v;
|
|
42
|
+
const id = voice.voice_id || voice.id || voice.name || String(v);
|
|
43
|
+
const name = voice.name ||
|
|
44
|
+
id
|
|
45
|
+
.split("-")
|
|
46
|
+
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
|
|
47
|
+
.join(" ");
|
|
48
|
+
return {
|
|
49
|
+
id,
|
|
50
|
+
name,
|
|
51
|
+
description: voice.name ? undefined : "Pocket TTS voice",
|
|
52
|
+
};
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
logger.warn(`[TTS] Voices endpoint returned HTTP ${response.status}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch (err) {
|
|
61
|
+
logger.warn("[TTS] Failed to fetch voices from server", err);
|
|
62
|
+
}
|
|
63
|
+
// Fallback: return just the configured default voice
|
|
64
|
+
const defaultVoices = [
|
|
65
|
+
{
|
|
66
|
+
id: config.tts.voice || "david-attenborough-original",
|
|
67
|
+
name: config.tts.voice || "David Attenborough",
|
|
68
|
+
description: "Default voice",
|
|
69
|
+
},
|
|
70
|
+
];
|
|
71
|
+
logger.debug(`[TTS] Using ${defaultVoices.length} default voice(s)`);
|
|
72
|
+
return defaultVoices;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Synthesizes speech from text using the TTS API.
|
|
76
|
+
*
|
|
77
|
+
* Sends a JSON POST to `{TTS_API_URL}/v1/audio/speech`.
|
|
78
|
+
*
|
|
79
|
+
* @param text - Text to synthesize
|
|
80
|
+
* @param voice - Voice ID to use (defaults to configured default voice)
|
|
81
|
+
* @returns Audio buffer and content type
|
|
82
|
+
*/
|
|
83
|
+
export async function synthesizeSpeech(text, voice) {
|
|
84
|
+
if (!isTtsConfigured()) {
|
|
85
|
+
throw new Error("TTS is not configured: TTS_API_URL is required");
|
|
86
|
+
}
|
|
87
|
+
const url = `${config.tts.apiUrl}/v1/audio/speech`;
|
|
88
|
+
let selectedVoice = voice || config.tts.voice || "david-attenborough-original";
|
|
89
|
+
if (!selectedVoice.match(/\.\w+$/)) {
|
|
90
|
+
selectedVoice += ".wav";
|
|
91
|
+
}
|
|
92
|
+
const body = {
|
|
93
|
+
model: config.tts.model || "tts-1",
|
|
94
|
+
input: text,
|
|
95
|
+
voice: selectedVoice,
|
|
96
|
+
response_format: "wav",
|
|
97
|
+
speed: config.tts.speed || 1.0,
|
|
98
|
+
};
|
|
99
|
+
logger.debug(`[TTS] Sending speech synthesis request: url=${url}, voice=${selectedVoice}, text=${text.length} chars`);
|
|
100
|
+
const controller = new AbortController();
|
|
101
|
+
const timeout = setTimeout(() => controller.abort(), TTS_REQUEST_TIMEOUT_MS);
|
|
102
|
+
try {
|
|
103
|
+
const response = await fetch(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
},
|
|
108
|
+
body: JSON.stringify(body),
|
|
109
|
+
signal: controller.signal,
|
|
110
|
+
});
|
|
111
|
+
if (!response.ok) {
|
|
112
|
+
const errorBody = await response.text().catch(() => "");
|
|
113
|
+
throw new Error(`TTS API returned HTTP ${response.status}: ${errorBody || response.statusText}`);
|
|
114
|
+
}
|
|
115
|
+
const contentType = response.headers.get("content-type") || "audio/wav";
|
|
116
|
+
const arrayBuffer = await response.arrayBuffer();
|
|
117
|
+
const audioBuffer = Buffer.from(arrayBuffer);
|
|
118
|
+
logger.debug(`[TTS] Synthesized audio: ${audioBuffer.length} bytes, type=${contentType}`);
|
|
119
|
+
return { audio: audioBuffer, contentType };
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
if (err instanceof DOMException && err.name === "AbortError") {
|
|
123
|
+
throw new Error(`TTS request timed out after ${TTS_REQUEST_TIMEOUT_MS}ms`);
|
|
124
|
+
}
|
|
125
|
+
throw err;
|
|
126
|
+
}
|
|
127
|
+
finally {
|
|
128
|
+
clearTimeout(timeout);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DEFAULT_MAX_ERROR_DETAILS_LENGTH = 1500;
|
|
2
|
+
function clipText(value, maxLength) {
|
|
3
|
+
if (value.length <= maxLength) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return `${value.slice(0, Math.max(0, maxLength - 3))}...`;
|
|
7
|
+
}
|
|
8
|
+
export function formatErrorDetails(error, maxLength = DEFAULT_MAX_ERROR_DETAILS_LENGTH) {
|
|
9
|
+
let details = "";
|
|
10
|
+
if (error instanceof Error) {
|
|
11
|
+
details = error.stack ?? `${error.name}: ${error.message}`;
|
|
12
|
+
}
|
|
13
|
+
else if (typeof error === "string") {
|
|
14
|
+
details = error;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
try {
|
|
18
|
+
details = JSON.stringify(error, null, 2);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
details = String(error);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const normalized = details.trim();
|
|
25
|
+
if (!normalized || normalized === "{}" || normalized === "[object Object]") {
|
|
26
|
+
return "unknown error";
|
|
27
|
+
}
|
|
28
|
+
return clipText(normalized, maxLength);
|
|
29
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { config } from "../config.js";
|
|
2
|
+
/**
|
|
3
|
+
* Mapping of log levels to numeric values for comparison
|
|
4
|
+
* Used to determine if a message should be logged based on configured level
|
|
5
|
+
*/
|
|
6
|
+
const LOG_LEVELS = {
|
|
7
|
+
debug: 0,
|
|
8
|
+
info: 1,
|
|
9
|
+
warn: 2,
|
|
10
|
+
error: 3,
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Normalizes a string value to a valid LogLevel
|
|
14
|
+
* Falls back to 'info' if the value is invalid
|
|
15
|
+
*
|
|
16
|
+
* @param value - The log level string to normalize
|
|
17
|
+
* @returns A valid LogLevel
|
|
18
|
+
*/
|
|
19
|
+
function normalizeLogLevel(value) {
|
|
20
|
+
if (value in LOG_LEVELS) {
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
return "info";
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Formats the log message prefix with timestamp and level
|
|
27
|
+
*
|
|
28
|
+
* @param level - The log level for the message
|
|
29
|
+
* @returns Formatted prefix string
|
|
30
|
+
*/
|
|
31
|
+
function formatPrefix(level) {
|
|
32
|
+
return `[${new Date().toISOString()}] [${level.toUpperCase()}]`;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Formats individual arguments for logging
|
|
36
|
+
* Special handling for Error objects to extract stack trace
|
|
37
|
+
*
|
|
38
|
+
* @param arg - The argument to format
|
|
39
|
+
* @returns Formatted argument
|
|
40
|
+
*/
|
|
41
|
+
function formatArg(arg) {
|
|
42
|
+
if (arg instanceof Error) {
|
|
43
|
+
return arg.stack ?? `${arg.name}: ${arg.message}`;
|
|
44
|
+
}
|
|
45
|
+
return arg;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Prepends formatted prefix to log arguments
|
|
49
|
+
* Handles different argument formats (string vs non-string first argument)
|
|
50
|
+
*
|
|
51
|
+
* @param level - The log level for prefix formatting
|
|
52
|
+
* @param args - The arguments to log
|
|
53
|
+
* @returns Array with prefix prepended
|
|
54
|
+
*/
|
|
55
|
+
function withPrefix(level, args) {
|
|
56
|
+
const formattedArgs = args.map((arg) => formatArg(arg));
|
|
57
|
+
const prefix = formatPrefix(level);
|
|
58
|
+
if (formattedArgs.length === 0) {
|
|
59
|
+
return [prefix];
|
|
60
|
+
}
|
|
61
|
+
if (typeof formattedArgs[0] === "string") {
|
|
62
|
+
return [`${prefix} ${formattedArgs[0]}`, ...formattedArgs.slice(1)];
|
|
63
|
+
}
|
|
64
|
+
return [prefix, ...formattedArgs];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Determines if a message should be logged based on configured log level
|
|
68
|
+
* Messages with level >= configured level will be logged
|
|
69
|
+
*
|
|
70
|
+
* @param level - The level of the message to check
|
|
71
|
+
* @returns True if the message should be logged
|
|
72
|
+
*/
|
|
73
|
+
function shouldLog(level) {
|
|
74
|
+
const configLevel = normalizeLogLevel(config.server.logLevel);
|
|
75
|
+
return LOG_LEVELS[level] >= LOG_LEVELS[configLevel];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Logger interface with methods for different log levels
|
|
79
|
+
* Each method checks if the message should be logged based on configured level
|
|
80
|
+
* and formats the output with timestamp and level prefix
|
|
81
|
+
*/
|
|
82
|
+
export const logger = {
|
|
83
|
+
/**
|
|
84
|
+
* Logs debug-level messages (most verbose)
|
|
85
|
+
* Used for detailed diagnostics and internal operations
|
|
86
|
+
*
|
|
87
|
+
* @param args - Arguments to log
|
|
88
|
+
*/
|
|
89
|
+
debug: (...args) => {
|
|
90
|
+
if (shouldLog("debug")) {
|
|
91
|
+
console.log(...withPrefix("debug", args));
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Logs info-level messages
|
|
96
|
+
* Used for important events and general information
|
|
97
|
+
*
|
|
98
|
+
* @param args - Arguments to log
|
|
99
|
+
*/
|
|
100
|
+
info: (...args) => {
|
|
101
|
+
if (shouldLog("info")) {
|
|
102
|
+
console.log(...withPrefix("info", args));
|
|
103
|
+
}
|
|
104
|
+
},
|
|
105
|
+
/**
|
|
106
|
+
* Logs warning-level messages
|
|
107
|
+
* Used for recoverable errors and potential issues
|
|
108
|
+
*
|
|
109
|
+
* @param args - Arguments to log
|
|
110
|
+
*/
|
|
111
|
+
warn: (...args) => {
|
|
112
|
+
if (shouldLog("warn")) {
|
|
113
|
+
console.warn(...withPrefix("warn", args));
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
/**
|
|
117
|
+
* Logs error-level messages
|
|
118
|
+
* Used for critical failures and exceptions
|
|
119
|
+
*
|
|
120
|
+
* @param args - Arguments to log
|
|
121
|
+
*/
|
|
122
|
+
error: (...args) => {
|
|
123
|
+
if (shouldLog("error")) {
|
|
124
|
+
console.error(...withPrefix("error", args));
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { logger } from "./logger.js";
|
|
2
|
+
function runHookSafely(taskName, hookName, hook, value) {
|
|
3
|
+
if (!hook) {
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
try {
|
|
7
|
+
void Promise.resolve(hook(value)).catch((hookError) => {
|
|
8
|
+
logger.error(`[safeBackgroundTask] ${taskName}: ${hookName} failed:`, hookError);
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
catch (hookError) {
|
|
12
|
+
logger.error(`[safeBackgroundTask] ${taskName}: ${hookName} failed:`, hookError);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
export function safeBackgroundTask({ taskName, task, onSuccess, onError, }) {
|
|
16
|
+
const handleError = (error) => {
|
|
17
|
+
logger.error(`[safeBackgroundTask] ${taskName} failed:`, error);
|
|
18
|
+
runHookSafely(taskName, "onError", onError, error);
|
|
19
|
+
};
|
|
20
|
+
try {
|
|
21
|
+
const taskPromise = task();
|
|
22
|
+
void taskPromise
|
|
23
|
+
.then((result) => {
|
|
24
|
+
runHookSafely(taskName, "onSuccess", onSuccess, result);
|
|
25
|
+
})
|
|
26
|
+
.catch((error) => {
|
|
27
|
+
handleError(error);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
handleError(error);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
function getErrorMessage(error) {
|
|
2
|
+
const parts = [];
|
|
3
|
+
if (error instanceof Error) {
|
|
4
|
+
parts.push(error.message);
|
|
5
|
+
}
|
|
6
|
+
if (typeof error === "object" && error !== null) {
|
|
7
|
+
const description = Reflect.get(error, "description");
|
|
8
|
+
if (typeof description === "string") {
|
|
9
|
+
parts.push(description);
|
|
10
|
+
}
|
|
11
|
+
const message = Reflect.get(error, "message");
|
|
12
|
+
if (typeof message === "string") {
|
|
13
|
+
parts.push(message);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
if (typeof error === "string") {
|
|
17
|
+
parts.push(error);
|
|
18
|
+
}
|
|
19
|
+
return parts.join("\n");
|
|
20
|
+
}
|
|
21
|
+
function getRetryAfterSecondsFromError(error) {
|
|
22
|
+
if (typeof error === "object" && error !== null) {
|
|
23
|
+
const parameters = Reflect.get(error, "parameters");
|
|
24
|
+
if (typeof parameters === "object" && parameters !== null) {
|
|
25
|
+
const retryAfter = Reflect.get(parameters, "retry_after");
|
|
26
|
+
if (typeof retryAfter === "number" && Number.isFinite(retryAfter) && retryAfter > 0) {
|
|
27
|
+
return retryAfter;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const message = getErrorMessage(error);
|
|
32
|
+
const retryMatch = message.match(/retry after\s+(\d+)/i);
|
|
33
|
+
if (!retryMatch) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
const parsedSeconds = Number.parseInt(retryMatch[1], 10);
|
|
37
|
+
if (!Number.isFinite(parsedSeconds) || parsedSeconds <= 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return parsedSeconds;
|
|
41
|
+
}
|
|
42
|
+
function isTelegramRateLimitError(error) {
|
|
43
|
+
if (typeof error === "object" && error !== null) {
|
|
44
|
+
const status = Reflect.get(error, "status");
|
|
45
|
+
if (typeof status === "number" && status === 429) {
|
|
46
|
+
return true;
|
|
47
|
+
}
|
|
48
|
+
const errorCode = Reflect.get(error, "error_code");
|
|
49
|
+
if (typeof errorCode === "number" && errorCode === 429) {
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const message = getErrorMessage(error).toLowerCase();
|
|
54
|
+
return /\b429\b/.test(message) || message.includes("too many requests");
|
|
55
|
+
}
|
|
56
|
+
function wait(ms) {
|
|
57
|
+
return new Promise((resolve) => {
|
|
58
|
+
setTimeout(resolve, ms);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export function getTelegramRetryAfterMs(error, fallbackDelayMs = 1000) {
|
|
62
|
+
if (!isTelegramRateLimitError(error)) {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
const retryAfterSeconds = getRetryAfterSecondsFromError(error);
|
|
66
|
+
if (retryAfterSeconds !== null) {
|
|
67
|
+
return retryAfterSeconds * 1000;
|
|
68
|
+
}
|
|
69
|
+
return Math.max(1, Math.floor(fallbackDelayMs));
|
|
70
|
+
}
|
|
71
|
+
export async function withTelegramRateLimitRetry(operation, options) {
|
|
72
|
+
const maxRetries = Math.max(0, Math.floor(options?.maxRetries ?? 3));
|
|
73
|
+
const fallbackDelayMs = options?.fallbackDelayMs ?? 1000;
|
|
74
|
+
let attempt = 0;
|
|
75
|
+
while (true) {
|
|
76
|
+
try {
|
|
77
|
+
return await operation();
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const retryAfterMs = getTelegramRetryAfterMs(error, fallbackDelayMs);
|
|
81
|
+
if (retryAfterMs === null || attempt >= maxRetries) {
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
attempt += 1;
|
|
85
|
+
options?.onRetry?.({
|
|
86
|
+
attempt,
|
|
87
|
+
retryAfterMs,
|
|
88
|
+
error,
|
|
89
|
+
});
|
|
90
|
+
await wait(retryAfterMs);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Variant Manager - manages model variants (reasoning modes)
|
|
3
|
+
*/
|
|
4
|
+
import { opencodeClient } from "../opencode/client.js";
|
|
5
|
+
import { getCurrentModel, setCurrentModel } from "../settings/manager.js";
|
|
6
|
+
import { logger } from "../utils/logger.js";
|
|
7
|
+
/**
|
|
8
|
+
* Get available variants for a model from OpenCode API
|
|
9
|
+
* @param providerID Provider ID
|
|
10
|
+
* @param modelID Model ID
|
|
11
|
+
* @returns Array of available variants
|
|
12
|
+
*/
|
|
13
|
+
export async function getAvailableVariants(providerID, modelID) {
|
|
14
|
+
try {
|
|
15
|
+
const { data, error } = await opencodeClient.config.providers();
|
|
16
|
+
if (error || !data) {
|
|
17
|
+
logger.warn("[VariantManager] Failed to fetch providers:", error);
|
|
18
|
+
return [{ id: "default" }];
|
|
19
|
+
}
|
|
20
|
+
const provider = data.providers.find((p) => p.id === providerID);
|
|
21
|
+
if (!provider) {
|
|
22
|
+
logger.warn(`[VariantManager] Provider ${providerID} not found`);
|
|
23
|
+
return [{ id: "default" }];
|
|
24
|
+
}
|
|
25
|
+
const model = provider.models[modelID];
|
|
26
|
+
if (!model) {
|
|
27
|
+
logger.warn(`[VariantManager] Model ${modelID} not found in provider ${providerID}`);
|
|
28
|
+
return [{ id: "default" }];
|
|
29
|
+
}
|
|
30
|
+
// Start with default variant (always present)
|
|
31
|
+
const variants = [{ id: "default" }];
|
|
32
|
+
if (model.variants) {
|
|
33
|
+
// Add other variants from API (excluding default if it's already there)
|
|
34
|
+
const apiVariants = Object.entries(model.variants)
|
|
35
|
+
.filter(([id]) => id !== "default")
|
|
36
|
+
.map(([id, info]) => ({
|
|
37
|
+
id,
|
|
38
|
+
disabled: info.disabled,
|
|
39
|
+
}));
|
|
40
|
+
variants.push(...apiVariants);
|
|
41
|
+
logger.debug(`[VariantManager] Found ${variants.length} variants for ${providerID}/${modelID} (including default)`);
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
logger.debug(`[VariantManager] No variants found for ${providerID}/${modelID}, using default only`);
|
|
45
|
+
}
|
|
46
|
+
return variants;
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
logger.error("[VariantManager] Error fetching variants:", err);
|
|
50
|
+
return [{ id: "default" }];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Get current variant from settings
|
|
55
|
+
* @returns Current variant ID (defaults to "default")
|
|
56
|
+
*/
|
|
57
|
+
export function getCurrentVariant() {
|
|
58
|
+
const currentModel = getCurrentModel();
|
|
59
|
+
return currentModel?.variant || "default";
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Set current variant in settings
|
|
63
|
+
* @param variantId Variant ID to set
|
|
64
|
+
*/
|
|
65
|
+
export function setCurrentVariant(variantId) {
|
|
66
|
+
const currentModel = getCurrentModel();
|
|
67
|
+
if (!currentModel) {
|
|
68
|
+
logger.warn("[VariantManager] Cannot set variant: no current model");
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
currentModel.variant = variantId;
|
|
72
|
+
setCurrentModel(currentModel);
|
|
73
|
+
logger.info(`[VariantManager] Variant set to: ${variantId}`);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Format variant for button display
|
|
77
|
+
* @param variantId Variant ID (e.g., "default", "low", "high")
|
|
78
|
+
* @returns Formatted string "💠Default", "💠Low", etc.
|
|
79
|
+
*/
|
|
80
|
+
export function formatVariantForButton(variantId) {
|
|
81
|
+
const capitalized = variantId.charAt(0).toUpperCase() + variantId.slice(1);
|
|
82
|
+
return `💡 ${capitalized}`;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Format variant for display in messages
|
|
86
|
+
* @param variantId Variant ID
|
|
87
|
+
* @returns Formatted string with capitalized first letter
|
|
88
|
+
*/
|
|
89
|
+
export function formatVariantForDisplay(variantId) {
|
|
90
|
+
return variantId.charAt(0).toUpperCase() + variantId.slice(1);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Validate if a model supports a specific variant
|
|
94
|
+
* @param providerID Provider ID
|
|
95
|
+
* @param modelID Model ID
|
|
96
|
+
* @param variantId Variant ID to validate
|
|
97
|
+
* @returns true if variant is supported, false otherwise
|
|
98
|
+
*/
|
|
99
|
+
export async function validateVariantForModel(providerID, modelID, variantId) {
|
|
100
|
+
const variants = await getAvailableVariants(providerID, modelID);
|
|
101
|
+
const found = variants.find((v) => v.id === variantId && !v.disabled);
|
|
102
|
+
return found !== undefined;
|
|
103
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "zardbot-telegram",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ZardBot Telegram Bot - A Telegram client for OpenCode with TTS and STT support.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"zardbot-telegram": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": "./dist/index.js",
|
|
12
|
+
"./cli": "./dist/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist",
|
|
16
|
+
"README.md",
|
|
17
|
+
"LICENSE",
|
|
18
|
+
".env.example"
|
|
19
|
+
],
|
|
20
|
+
"engines": {
|
|
21
|
+
"node": ">=20"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"build": "tsc",
|
|
25
|
+
"start": "node dist/index.js",
|
|
26
|
+
"dev": "npm run build && npm start",
|
|
27
|
+
"release:prepare": "node scripts/release-prepare.mjs",
|
|
28
|
+
"release:rc": "node scripts/release-prepare.mjs rc",
|
|
29
|
+
"release:notes:preview": "node scripts/release-notes-preview.mjs",
|
|
30
|
+
"lint": "eslint src --ext .ts --max-warnings=0",
|
|
31
|
+
"format": "prettier --write \"src/**/*.ts\"",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:coverage": "vitest run --coverage"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "git+https://github.com/ai-joe-git/zardbot-telegram.git"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"zardbot",
|
|
41
|
+
"opencode",
|
|
42
|
+
"telegram",
|
|
43
|
+
"telegram-bot",
|
|
44
|
+
"grammy",
|
|
45
|
+
"tts",
|
|
46
|
+
"stt",
|
|
47
|
+
"developer-tools",
|
|
48
|
+
"cli"
|
|
49
|
+
],
|
|
50
|
+
"author": "ZardBot Team",
|
|
51
|
+
"license": "MIT",
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/ai-joe-git/zardbot-telegram/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/ai-joe-git/zardbot-telegram#readme",
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"@grammyjs/menu": "^1.3.1",
|
|
58
|
+
"@opencode-ai/sdk": "^1.1.21",
|
|
59
|
+
"better-sqlite3": "^12.6.2",
|
|
60
|
+
"dotenv": "^17.2.3",
|
|
61
|
+
"grammy": "^1.39.2",
|
|
62
|
+
"https-proxy-agent": "^7.0.6",
|
|
63
|
+
"socks-proxy-agent": "^8.0.5",
|
|
64
|
+
"telegram-markdown-v2": "^0.0.4"
|
|
65
|
+
},
|
|
66
|
+
"devDependencies": {
|
|
67
|
+
"@types/better-sqlite3": "^7.6.13",
|
|
68
|
+
"@types/node": "^25.0.8",
|
|
69
|
+
"@typescript-eslint/eslint-plugin": "^8.53.0",
|
|
70
|
+
"@typescript-eslint/parser": "^8.53.0",
|
|
71
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
72
|
+
"eslint": "^8.57.1",
|
|
73
|
+
"eslint-config-prettier": "^10.1.8",
|
|
74
|
+
"prettier": "^3.8.0",
|
|
75
|
+
"tsx": "^4.21.0",
|
|
76
|
+
"typescript": "^5.9.3",
|
|
77
|
+
"vitest": "^3.2.4"
|
|
78
|
+
}
|
|
79
|
+
}
|