tg-agent 0.1.0 → 1.1.1
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 +7 -0
- package/dist/auth.js +1 -71
- package/dist/cli.js +1 -1
- package/dist/codexAuth.js +1 -93
- package/dist/config.js +1 -59
- package/dist/customTools.js +9 -386
- package/dist/index.js +17 -954
- package/dist/mcp.js +5 -427
- package/dist/piAgentRunner.js +7 -407
- package/dist/piAiRunner.js +5 -99
- package/dist/proxy.js +1 -19
- package/dist/sessionStore.js +1 -138
- package/dist/types.js +0 -1
- package/dist/utils.js +2 -91
- package/package.json +4 -2
package/dist/index.js
CHANGED
|
@@ -1,954 +1,17 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
activeRuns.set(userId, run);
|
|
19
|
-
}
|
|
20
|
-
function clearActiveRun(userId) {
|
|
21
|
-
activeRuns.delete(userId);
|
|
22
|
-
}
|
|
23
|
-
function resolveTelegramParseMode(value) {
|
|
24
|
-
const trimmed = value.trim();
|
|
25
|
-
if (!trimmed) {
|
|
26
|
-
return undefined;
|
|
27
|
-
}
|
|
28
|
-
if (trimmed === "Markdown" || trimmed === "MarkdownV2" || trimmed === "HTML") {
|
|
29
|
-
return trimmed;
|
|
30
|
-
}
|
|
31
|
-
console.warn(`[tg-agent] invalid TELEGRAM_PARSE_MODE=${trimmed}, ignoring`);
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
const MARKDOWN_V2_RESERVED = new Set([
|
|
35
|
-
"_",
|
|
36
|
-
"*",
|
|
37
|
-
"[",
|
|
38
|
-
"]",
|
|
39
|
-
"(",
|
|
40
|
-
")",
|
|
41
|
-
"~",
|
|
42
|
-
"`",
|
|
43
|
-
">",
|
|
44
|
-
"#",
|
|
45
|
-
"+",
|
|
46
|
-
"-",
|
|
47
|
-
"=",
|
|
48
|
-
"|",
|
|
49
|
-
"{",
|
|
50
|
-
"}",
|
|
51
|
-
".",
|
|
52
|
-
"!",
|
|
53
|
-
]);
|
|
54
|
-
function countUnescaped(text, target) {
|
|
55
|
-
let count = 0;
|
|
56
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
57
|
-
const char = text[i];
|
|
58
|
-
if (char === "\\") {
|
|
59
|
-
i += 1;
|
|
60
|
-
continue;
|
|
61
|
-
}
|
|
62
|
-
if (char === target) {
|
|
63
|
-
count += 1;
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
return count;
|
|
67
|
-
}
|
|
68
|
-
function isMarkdownSafe(text) {
|
|
69
|
-
const stars = countUnescaped(text, "*");
|
|
70
|
-
if (stars % 2 !== 0)
|
|
71
|
-
return false;
|
|
72
|
-
const underscores = countUnescaped(text, "_");
|
|
73
|
-
if (underscores % 2 !== 0)
|
|
74
|
-
return false;
|
|
75
|
-
const backticks = countUnescaped(text, "`");
|
|
76
|
-
if (backticks % 2 !== 0)
|
|
77
|
-
return false;
|
|
78
|
-
const left = countUnescaped(text, "[");
|
|
79
|
-
const right = countUnescaped(text, "]");
|
|
80
|
-
if (left !== right)
|
|
81
|
-
return false;
|
|
82
|
-
return true;
|
|
83
|
-
}
|
|
84
|
-
function isMarkdownV2Safe(text) {
|
|
85
|
-
for (let i = 0; i < text.length; i += 1) {
|
|
86
|
-
const char = text[i];
|
|
87
|
-
if (char === "\\") {
|
|
88
|
-
i += 1;
|
|
89
|
-
continue;
|
|
90
|
-
}
|
|
91
|
-
if (MARKDOWN_V2_RESERVED.has(char)) {
|
|
92
|
-
return false;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
return true;
|
|
96
|
-
}
|
|
97
|
-
function resolveBestParseModes(text) {
|
|
98
|
-
const configured = resolveTelegramParseMode(config.telegramParseMode);
|
|
99
|
-
if (configured) {
|
|
100
|
-
return [configured];
|
|
101
|
-
}
|
|
102
|
-
if (isMarkdownV2Safe(text)) {
|
|
103
|
-
return ["MarkdownV2", "Markdown"];
|
|
104
|
-
}
|
|
105
|
-
if (isMarkdownSafe(text)) {
|
|
106
|
-
return ["Markdown"];
|
|
107
|
-
}
|
|
108
|
-
return [];
|
|
109
|
-
}
|
|
110
|
-
const AUTH_PROMPT_TIMEOUT_MS = 10 * 60 * 1000;
|
|
111
|
-
const authPromptState = new Map();
|
|
112
|
-
const authInFlight = new Set();
|
|
113
|
-
const PROVIDER_ALIASES = new Map([
|
|
114
|
-
["codex", "openai-codex"],
|
|
115
|
-
["antigravity", "google-antigravity"],
|
|
116
|
-
["gemini", "google-gemini-cli"],
|
|
117
|
-
["gemini-cli", "google-gemini-cli"],
|
|
118
|
-
]);
|
|
119
|
-
const CODEX_MODEL_ALIASES = {
|
|
120
|
-
"codex-mini-latest": "gpt-5.1-codex-mini",
|
|
121
|
-
"codex-max-latest": "gpt-5.1-codex-max",
|
|
122
|
-
"codex-latest": "gpt-5.2-codex",
|
|
123
|
-
};
|
|
124
|
-
function normalizeProviderId(input) {
|
|
125
|
-
const trimmed = input.trim().toLowerCase();
|
|
126
|
-
return PROVIDER_ALIASES.get(trimmed) ?? trimmed;
|
|
127
|
-
}
|
|
128
|
-
function normalizeModelId(provider, modelId) {
|
|
129
|
-
if (provider !== "openai-codex") {
|
|
130
|
-
return modelId;
|
|
131
|
-
}
|
|
132
|
-
return CODEX_MODEL_ALIASES[modelId] ?? modelId;
|
|
133
|
-
}
|
|
134
|
-
async function loadModelRegistry() {
|
|
135
|
-
await ensureDir(config.agentDir);
|
|
136
|
-
const authStorage = discoverAuthStorage(config.agentDir);
|
|
137
|
-
const codex = readCodexOAuth();
|
|
138
|
-
if (codex) {
|
|
139
|
-
authStorage.setRuntimeApiKey("openai-codex", codex.accessToken);
|
|
140
|
-
}
|
|
141
|
-
const modelRegistry = discoverModels(authStorage, config.agentDir);
|
|
142
|
-
return { authStorage, modelRegistry };
|
|
143
|
-
}
|
|
144
|
-
function clearAuthPrompt(userId) {
|
|
145
|
-
const pending = authPromptState.get(userId);
|
|
146
|
-
if (!pending)
|
|
147
|
-
return;
|
|
148
|
-
clearTimeout(pending.timeoutId);
|
|
149
|
-
authPromptState.delete(userId);
|
|
150
|
-
}
|
|
151
|
-
function cancelAuthPrompt(userId, reason) {
|
|
152
|
-
const pending = authPromptState.get(userId);
|
|
153
|
-
if (!pending)
|
|
154
|
-
return;
|
|
155
|
-
clearTimeout(pending.timeoutId);
|
|
156
|
-
authPromptState.delete(userId);
|
|
157
|
-
pending.reject(new Error(reason));
|
|
158
|
-
}
|
|
159
|
-
async function promptForAuthInput(userId, chatId, prompt) {
|
|
160
|
-
if (authPromptState.has(userId)) {
|
|
161
|
-
throw new Error("Login is already awaiting input.");
|
|
162
|
-
}
|
|
163
|
-
const text = prompt.placeholder ? `${prompt.message} (${prompt.placeholder})` : prompt.message;
|
|
164
|
-
await sendLongMessage(chatId, text);
|
|
165
|
-
return await new Promise((resolve, reject) => {
|
|
166
|
-
const timeoutId = setTimeout(() => {
|
|
167
|
-
authPromptState.delete(userId);
|
|
168
|
-
reject(new Error("Login prompt timed out."));
|
|
169
|
-
}, AUTH_PROMPT_TIMEOUT_MS);
|
|
170
|
-
authPromptState.set(userId, { resolve, reject, timeoutId, chatId });
|
|
171
|
-
});
|
|
172
|
-
}
|
|
173
|
-
async function handleStopCommand(chatId, userId) {
|
|
174
|
-
const active = activeRuns.get(userId);
|
|
175
|
-
if (!active) {
|
|
176
|
-
await sendLongMessage(chatId, "No active request to stop.");
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
active.cancelRequested = true;
|
|
180
|
-
active.abortRequested = true;
|
|
181
|
-
if (active.status) {
|
|
182
|
-
active.status.update("Cancelled by user.", true);
|
|
183
|
-
}
|
|
184
|
-
if (active.abort) {
|
|
185
|
-
active.abort();
|
|
186
|
-
}
|
|
187
|
-
if (!active.status || active.chatId !== chatId) {
|
|
188
|
-
await sendLongMessage(chatId, "Stopping current request...");
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
function logStartupInfo() {
|
|
192
|
-
const proxyVars = {
|
|
193
|
-
https_proxy: process.env.https_proxy ?? process.env.HTTPS_PROXY ?? "",
|
|
194
|
-
http_proxy: process.env.http_proxy ?? process.env.HTTP_PROXY ?? "",
|
|
195
|
-
all_proxy: process.env.all_proxy ?? process.env.ALL_PROXY ?? "",
|
|
196
|
-
no_proxy: process.env.no_proxy ?? process.env.NO_PROXY ?? "",
|
|
197
|
-
};
|
|
198
|
-
const tlsVars = {
|
|
199
|
-
NODE_EXTRA_CA_CERTS: process.env.NODE_EXTRA_CA_CERTS ?? "",
|
|
200
|
-
NODE_TLS_REJECT_UNAUTHORIZED: process.env.NODE_TLS_REJECT_UNAUTHORIZED ?? "",
|
|
201
|
-
};
|
|
202
|
-
console.log(`[tg-agent] modelProvider=${config.modelProvider} modelRef=${config.modelRef} sessionDir=${config.sessionDir} maxConcurrent=${config.maxConcurrent}`);
|
|
203
|
-
console.log(`[tg-agent] agentDir=${config.agentDir} workspaceDir=${config.workspaceDir}`);
|
|
204
|
-
const mcpServers = loadMcpServersSync(config.agentDir);
|
|
205
|
-
const mcpStatus = mcpServers.length > 0 ? `on (${mcpServers.length})` : "off";
|
|
206
|
-
console.log(`[tg-agent] tools fetchMaxBytes=${config.fetchMaxBytes} fetchTimeoutMs=${config.fetchTimeoutMs} mcp=${mcpStatus}`);
|
|
207
|
-
console.log(`[tg-agent] proxy https=${proxyVars.https_proxy || "(empty)"} http=${proxyVars.http_proxy || "(empty)"} all=${proxyVars.all_proxy || "(empty)"} no=${proxyVars.no_proxy || "(empty)"}`);
|
|
208
|
-
console.log(`[tg-agent] tls extra_ca=${tlsVars.NODE_EXTRA_CA_CERTS || "(empty)"} reject_unauthorized=${tlsVars.NODE_TLS_REJECT_UNAUTHORIZED || "(empty)"}`);
|
|
209
|
-
try {
|
|
210
|
-
const { source } = resolveApiKeyForProvider(config.modelProvider);
|
|
211
|
-
console.log(`[tg-agent] authSource=${source}`);
|
|
212
|
-
}
|
|
213
|
-
catch (error) {
|
|
214
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
215
|
-
console.warn(`[tg-agent] authSource=missing (${message})`);
|
|
216
|
-
}
|
|
217
|
-
const proxyInfo = resolveProxyInfo();
|
|
218
|
-
if (proxyInfo) {
|
|
219
|
-
console.log(`[tg-agent] proxyUrl=${proxyInfo.url} kind=${proxyInfo.kind} source=${proxyInfo.source}`);
|
|
220
|
-
}
|
|
221
|
-
else {
|
|
222
|
-
console.log("[tg-agent] proxyUrl=(none)");
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
logStartupInfo();
|
|
226
|
-
void ensureDir(config.agentDir).catch((error) => {
|
|
227
|
-
console.warn(`[tg-agent] ensure agentDir failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
-
});
|
|
229
|
-
const appliedProxy = applyFetchProxy();
|
|
230
|
-
if (appliedProxy) {
|
|
231
|
-
console.log(`[tg-agent] fetchProxy=${appliedProxy.url} kind=${appliedProxy.kind} source=${appliedProxy.source}`);
|
|
232
|
-
}
|
|
233
|
-
else {
|
|
234
|
-
console.log("[tg-agent] fetchProxy=(none)");
|
|
235
|
-
}
|
|
236
|
-
function parseCommand(text) {
|
|
237
|
-
if (!text.startsWith("/")) {
|
|
238
|
-
return null;
|
|
239
|
-
}
|
|
240
|
-
const trimmed = text.trim();
|
|
241
|
-
const [rawCommand, ...rest] = trimmed.split(" ");
|
|
242
|
-
const command = rawCommand.split("@")[0].slice(1).toLowerCase();
|
|
243
|
-
return { command, args: rest.join(" ").trim() };
|
|
244
|
-
}
|
|
245
|
-
async function ensureActiveSessionForCommand(state, chatId) {
|
|
246
|
-
let session = getActiveSession(state);
|
|
247
|
-
if (!session) {
|
|
248
|
-
session = createSession(state, "");
|
|
249
|
-
await saveUserState(state);
|
|
250
|
-
await sendLongMessage(chatId, `Created session ${session.id}.`);
|
|
251
|
-
}
|
|
252
|
-
return session;
|
|
253
|
-
}
|
|
254
|
-
function formatProviders(modelRegistry, authStorage) {
|
|
255
|
-
const models = modelRegistry.getAll();
|
|
256
|
-
const providerStats = new Map();
|
|
257
|
-
for (const model of models) {
|
|
258
|
-
const entry = providerStats.get(model.provider) ?? { count: 0, auth: authStorage.hasAuth(model.provider) };
|
|
259
|
-
entry.count += 1;
|
|
260
|
-
providerStats.set(model.provider, entry);
|
|
261
|
-
}
|
|
262
|
-
if (providerStats.size === 0) {
|
|
263
|
-
return "No providers found.";
|
|
264
|
-
}
|
|
265
|
-
const lines = Array.from(providerStats.entries())
|
|
266
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
267
|
-
.map(([provider, info]) => `- ${provider} (models: ${info.count}, auth: ${info.auth ? "ok" : "missing"})`);
|
|
268
|
-
return ["Providers:", ...lines, "", "Use /models <provider> to list models."].join("\n");
|
|
269
|
-
}
|
|
270
|
-
function formatModelsForProvider(modelRegistry, provider, authOk) {
|
|
271
|
-
const models = modelRegistry.getAll().filter((model) => model.provider === provider);
|
|
272
|
-
if (models.length === 0) {
|
|
273
|
-
return `No models found for provider ${provider}.`;
|
|
274
|
-
}
|
|
275
|
-
const lines = models.map((model) => `- ${model.id} | ${model.name}`);
|
|
276
|
-
return [`Models for ${provider} (auth: ${authOk ? "ok" : "missing"}):`, ...lines].join("\n");
|
|
277
|
-
}
|
|
278
|
-
function parseModelSelector(raw) {
|
|
279
|
-
const trimmed = raw.trim();
|
|
280
|
-
if (!trimmed)
|
|
281
|
-
return null;
|
|
282
|
-
if (trimmed.includes("/")) {
|
|
283
|
-
const [provider, modelId] = trimmed.split("/", 2);
|
|
284
|
-
if (!provider || !modelId)
|
|
285
|
-
return null;
|
|
286
|
-
return { provider: normalizeProviderId(provider), modelId: modelId.trim() };
|
|
287
|
-
}
|
|
288
|
-
return { modelId: trimmed };
|
|
289
|
-
}
|
|
290
|
-
const unauthorizedLogWindowMs = 60_000;
|
|
291
|
-
const unauthorizedLogState = new Map();
|
|
292
|
-
function logUnauthorizedUser(userId, chatId) {
|
|
293
|
-
const now = Date.now();
|
|
294
|
-
const lastLoggedAt = unauthorizedLogState.get(userId) ?? 0;
|
|
295
|
-
if (now - lastLoggedAt < unauthorizedLogWindowMs) {
|
|
296
|
-
return;
|
|
297
|
-
}
|
|
298
|
-
unauthorizedLogState.set(userId, now);
|
|
299
|
-
console.warn(`[tg-agent] unauthorized user=${userId} chat=${chatId}`);
|
|
300
|
-
}
|
|
301
|
-
function isUserAllowed(userId) {
|
|
302
|
-
const allowed = config.telegramAllowedUsers;
|
|
303
|
-
if (!allowed || allowed.size === 0) {
|
|
304
|
-
return true;
|
|
305
|
-
}
|
|
306
|
-
return allowed.has(userId);
|
|
307
|
-
}
|
|
308
|
-
function formatSessions(stateSessions) {
|
|
309
|
-
if (stateSessions.length === 0) {
|
|
310
|
-
return "No sessions found.";
|
|
311
|
-
}
|
|
312
|
-
const lines = stateSessions.map((session) => {
|
|
313
|
-
const updated = new Date(session.updatedAt).toISOString();
|
|
314
|
-
return `- ${session.id} | ${session.title} | updated ${updated}`;
|
|
315
|
-
});
|
|
316
|
-
return ["Sessions:", ...lines].join("\n");
|
|
317
|
-
}
|
|
318
|
-
async function sendTelegramMessage(chatId, text, parseMode) {
|
|
319
|
-
if (parseMode) {
|
|
320
|
-
return await bot.sendMessage(chatId, text, { parse_mode: parseMode });
|
|
321
|
-
}
|
|
322
|
-
return await bot.sendMessage(chatId, text);
|
|
323
|
-
}
|
|
324
|
-
async function editTelegramMessage(chatId, messageId, text, parseMode) {
|
|
325
|
-
if (parseMode) {
|
|
326
|
-
await bot.editMessageText(text, { chat_id: chatId, message_id: messageId, parse_mode: parseMode });
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
await bot.editMessageText(text, { chat_id: chatId, message_id: messageId });
|
|
330
|
-
}
|
|
331
|
-
async function sendWithParseModes(attempt, parseModes) {
|
|
332
|
-
let lastError;
|
|
333
|
-
for (const mode of parseModes) {
|
|
334
|
-
try {
|
|
335
|
-
return await attempt(mode);
|
|
336
|
-
}
|
|
337
|
-
catch (error) {
|
|
338
|
-
lastError = error;
|
|
339
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
340
|
-
console.warn(`[tg-agent] parse_mode ${mode} failed, trying fallback: ${message}`);
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
|
-
return await attempt(undefined);
|
|
345
|
-
}
|
|
346
|
-
catch (error) {
|
|
347
|
-
if (lastError) {
|
|
348
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
349
|
-
console.warn(`[tg-agent] parse_mode fallback to plain failed: ${message}`);
|
|
350
|
-
}
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
async function sendTelegramMessageBest(chatId, text) {
|
|
355
|
-
return await sendWithParseModes((parseMode) => sendTelegramMessage(chatId, text, parseMode), resolveBestParseModes(text));
|
|
356
|
-
}
|
|
357
|
-
async function editTelegramMessageBest(chatId, messageId, text) {
|
|
358
|
-
await sendWithParseModes((parseMode) => editTelegramMessage(chatId, messageId, text, parseMode), resolveBestParseModes(text));
|
|
359
|
-
}
|
|
360
|
-
async function sendLongMessage(chatId, text) {
|
|
361
|
-
const chunks = chunkText(text, 3900);
|
|
362
|
-
for (const chunk of chunks) {
|
|
363
|
-
await sendTelegramMessageBest(chatId, chunk);
|
|
364
|
-
}
|
|
365
|
-
}
|
|
366
|
-
const TELEGRAM_MAX_LEN = 3900;
|
|
367
|
-
const STATUS_UPDATE_MIN_MS = 1200;
|
|
368
|
-
function truncateForTelegram(text, maxLen = TELEGRAM_MAX_LEN) {
|
|
369
|
-
if (text.length <= maxLen) {
|
|
370
|
-
return { text, truncated: false };
|
|
371
|
-
}
|
|
372
|
-
const slice = text.slice(0, Math.max(0, maxLen - 15));
|
|
373
|
-
return { text: `${slice}...\n\n[truncated]`, truncated: true };
|
|
374
|
-
}
|
|
375
|
-
async function createStatusReporter(chatId) {
|
|
376
|
-
const initial = "Working...";
|
|
377
|
-
const sent = await sendTelegramMessage(chatId, initial);
|
|
378
|
-
const messageId = sent.message_id;
|
|
379
|
-
let lastText = initial;
|
|
380
|
-
let lastSentAt = 0;
|
|
381
|
-
let chain = Promise.resolve();
|
|
382
|
-
const enqueueEdit = (text, force = false) => {
|
|
383
|
-
const now = Date.now();
|
|
384
|
-
if (!force && now - lastSentAt < STATUS_UPDATE_MIN_MS)
|
|
385
|
-
return;
|
|
386
|
-
if (text === lastText)
|
|
387
|
-
return;
|
|
388
|
-
lastText = text;
|
|
389
|
-
lastSentAt = now;
|
|
390
|
-
chain = chain
|
|
391
|
-
.then(async () => {
|
|
392
|
-
await editTelegramMessage(chatId, messageId, text);
|
|
393
|
-
})
|
|
394
|
-
.catch((error) => {
|
|
395
|
-
console.warn("[tg-agent] status edit failed", error);
|
|
396
|
-
});
|
|
397
|
-
};
|
|
398
|
-
const enqueueEditBest = (text) => {
|
|
399
|
-
lastText = text;
|
|
400
|
-
lastSentAt = Date.now();
|
|
401
|
-
chain = chain
|
|
402
|
-
.then(async () => {
|
|
403
|
-
await editTelegramMessageBest(chatId, messageId, text);
|
|
404
|
-
})
|
|
405
|
-
.catch((error) => {
|
|
406
|
-
console.warn("[tg-agent] status edit failed", error);
|
|
407
|
-
});
|
|
408
|
-
};
|
|
409
|
-
return {
|
|
410
|
-
update: (text, force = false) => {
|
|
411
|
-
const trimmed = truncateForTelegram(text).text;
|
|
412
|
-
enqueueEdit(trimmed, force);
|
|
413
|
-
},
|
|
414
|
-
finalize: async (text) => {
|
|
415
|
-
const trimmed = truncateForTelegram(text).text;
|
|
416
|
-
enqueueEditBest(trimmed);
|
|
417
|
-
await chain;
|
|
418
|
-
},
|
|
419
|
-
fail: async (text) => {
|
|
420
|
-
const trimmed = truncateForTelegram(text).text;
|
|
421
|
-
enqueueEditBest(trimmed);
|
|
422
|
-
await chain;
|
|
423
|
-
},
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
function formatStatusEvent(event) {
|
|
427
|
-
switch (event.type) {
|
|
428
|
-
case "agent_start":
|
|
429
|
-
return "Working...";
|
|
430
|
-
case "tool_start":
|
|
431
|
-
return `Running tool: ${event.name}\nargs: ${summarizeArgs(event.args)}`;
|
|
432
|
-
case "tool_end":
|
|
433
|
-
return event.ok
|
|
434
|
-
? `Tool finished: ${event.name} (${event.durationMs}ms)`
|
|
435
|
-
: `Tool failed: ${event.name} (${event.durationMs}ms)`;
|
|
436
|
-
case "message_start":
|
|
437
|
-
return event.role === "assistant" ? "Generating reply..." : null;
|
|
438
|
-
case "message_end":
|
|
439
|
-
return event.role === "assistant" ? "Finalizing reply..." : null;
|
|
440
|
-
case "heartbeat": {
|
|
441
|
-
const seconds = Math.max(1, Math.round(event.elapsedMs / 1000));
|
|
442
|
-
return `Working... ${seconds}s`;
|
|
443
|
-
}
|
|
444
|
-
default:
|
|
445
|
-
return null;
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
function summarizeArgs(args) {
|
|
449
|
-
try {
|
|
450
|
-
const seen = new WeakSet();
|
|
451
|
-
const maxString = 200;
|
|
452
|
-
const maxKeys = 12;
|
|
453
|
-
const maxArray = 12;
|
|
454
|
-
const maxDepth = 3;
|
|
455
|
-
const normalize = (value, depth) => {
|
|
456
|
-
if (value === null || value === undefined)
|
|
457
|
-
return value;
|
|
458
|
-
if (typeof value === "string") {
|
|
459
|
-
return value.length > maxString ? `${value.slice(0, maxString)}...` : value;
|
|
460
|
-
}
|
|
461
|
-
if (typeof value === "number" || typeof value === "boolean")
|
|
462
|
-
return value;
|
|
463
|
-
if (typeof value === "bigint")
|
|
464
|
-
return value.toString();
|
|
465
|
-
if (typeof value === "function")
|
|
466
|
-
return "[function]";
|
|
467
|
-
if (typeof value !== "object")
|
|
468
|
-
return String(value);
|
|
469
|
-
if (seen.has(value))
|
|
470
|
-
return "[circular]";
|
|
471
|
-
if (depth >= maxDepth)
|
|
472
|
-
return "[truncated]";
|
|
473
|
-
seen.add(value);
|
|
474
|
-
if (Array.isArray(value)) {
|
|
475
|
-
const items = value.slice(0, maxArray).map((item) => normalize(item, depth + 1));
|
|
476
|
-
if (value.length > maxArray)
|
|
477
|
-
items.push("[truncated]");
|
|
478
|
-
return items;
|
|
479
|
-
}
|
|
480
|
-
const entries = Object.entries(value);
|
|
481
|
-
const result = {};
|
|
482
|
-
for (const [key, val] of entries.slice(0, maxKeys)) {
|
|
483
|
-
result[key] = normalize(val, depth + 1);
|
|
484
|
-
}
|
|
485
|
-
if (entries.length > maxKeys) {
|
|
486
|
-
result._truncated = true;
|
|
487
|
-
}
|
|
488
|
-
return result;
|
|
489
|
-
};
|
|
490
|
-
const normalized = normalize(args, 0);
|
|
491
|
-
const json = JSON.stringify(normalized);
|
|
492
|
-
if (!json)
|
|
493
|
-
return "null";
|
|
494
|
-
if (json.length > 700) {
|
|
495
|
-
return `${json.slice(0, 700)}...`;
|
|
496
|
-
}
|
|
497
|
-
return json;
|
|
498
|
-
}
|
|
499
|
-
catch (error) {
|
|
500
|
-
return "[unavailable]";
|
|
501
|
-
}
|
|
502
|
-
}
|
|
503
|
-
function formatErrorMessage(error) {
|
|
504
|
-
const base = error instanceof Error ? error.message : String(error);
|
|
505
|
-
const cause = error?.cause;
|
|
506
|
-
if (cause instanceof Error) {
|
|
507
|
-
return `${base} (${cause.message})`;
|
|
508
|
-
}
|
|
509
|
-
if (cause) {
|
|
510
|
-
return `${base} (${String(cause)})`;
|
|
511
|
-
}
|
|
512
|
-
return base;
|
|
513
|
-
}
|
|
514
|
-
function truncateLine(text, maxLen = 140) {
|
|
515
|
-
if (text.length <= maxLen) {
|
|
516
|
-
return text;
|
|
517
|
-
}
|
|
518
|
-
return `${text.slice(0, maxLen - 3)}...`;
|
|
519
|
-
}
|
|
520
|
-
async function formatMcpStatus() {
|
|
521
|
-
const servers = await loadMcpServers(config.agentDir);
|
|
522
|
-
if (servers.length === 0) {
|
|
523
|
-
return "No MCP servers configured. Add [mcp_servers.*] to ~/.tg-agent/config.toml.";
|
|
524
|
-
}
|
|
525
|
-
const probes = await Promise.all(servers.map((server) => probeMcpServer(server)));
|
|
526
|
-
const lines = ["MCP servers:"];
|
|
527
|
-
for (let i = 0; i < servers.length; i += 1) {
|
|
528
|
-
const server = servers[i];
|
|
529
|
-
const probe = probes[i];
|
|
530
|
-
const status = probe.ok ? "ok" : `error: ${truncateLine(probe.error ?? "unknown")}`;
|
|
531
|
-
lines.push(`- ${server.name} (${server.type}) ${formatMcpTarget(server)} status=${status} (${probe.durationMs}ms)`);
|
|
532
|
-
}
|
|
533
|
-
return lines.join("\n");
|
|
534
|
-
}
|
|
535
|
-
async function handleCommand(chatId, userId, command, args) {
|
|
536
|
-
const state = await loadUserState(userId);
|
|
537
|
-
const removed = pruneExpiredSessions(state);
|
|
538
|
-
if (removed.length > 0) {
|
|
539
|
-
await saveUserState(state);
|
|
540
|
-
await Promise.all(removed.map((sessionId) => deleteSessionFile(userId, sessionId).catch((error) => {
|
|
541
|
-
console.warn(`[tg-agent] cleanup session file failed id=${sessionId}`, error);
|
|
542
|
-
})));
|
|
543
|
-
}
|
|
544
|
-
switch (command) {
|
|
545
|
-
case "start":
|
|
546
|
-
case "help": {
|
|
547
|
-
const helpText = [
|
|
548
|
-
"Commands:",
|
|
549
|
-
"/new [title] - create a new session",
|
|
550
|
-
"/list - list sessions",
|
|
551
|
-
"/use <id> - switch active session",
|
|
552
|
-
"/close [id] - close a session (default: active)",
|
|
553
|
-
"/reset - clear active session history",
|
|
554
|
-
"/providers - list available providers",
|
|
555
|
-
"/models [provider] - list models for provider",
|
|
556
|
-
"/provider <name> - set provider for current session",
|
|
557
|
-
"/model <provider>/<model> - set model for current session",
|
|
558
|
-
"/mcp - list configured MCP servers",
|
|
559
|
-
"/status - show session model settings",
|
|
560
|
-
"/login <provider> - login to OAuth provider",
|
|
561
|
-
"/logout <provider> - logout from provider",
|
|
562
|
-
"/stop - stop the current running request",
|
|
563
|
-
].join("\n");
|
|
564
|
-
await sendLongMessage(chatId, helpText);
|
|
565
|
-
return;
|
|
566
|
-
}
|
|
567
|
-
case "new": {
|
|
568
|
-
try {
|
|
569
|
-
const title = args || "";
|
|
570
|
-
const session = createSession(state, title);
|
|
571
|
-
await saveUserState(state);
|
|
572
|
-
await sendLongMessage(chatId, `Created session ${session.id} (${session.title}).`);
|
|
573
|
-
}
|
|
574
|
-
catch (error) {
|
|
575
|
-
await sendLongMessage(chatId, error.message);
|
|
576
|
-
}
|
|
577
|
-
return;
|
|
578
|
-
}
|
|
579
|
-
case "list": {
|
|
580
|
-
await saveUserState(state);
|
|
581
|
-
await sendLongMessage(chatId, formatSessions(listSessions(state)));
|
|
582
|
-
return;
|
|
583
|
-
}
|
|
584
|
-
case "use": {
|
|
585
|
-
if (!args) {
|
|
586
|
-
await sendLongMessage(chatId, "Usage: /use <id>");
|
|
587
|
-
return;
|
|
588
|
-
}
|
|
589
|
-
const ok = setActiveSession(state, args);
|
|
590
|
-
if (!ok) {
|
|
591
|
-
await sendLongMessage(chatId, `Session not found: ${args}`);
|
|
592
|
-
return;
|
|
593
|
-
}
|
|
594
|
-
await saveUserState(state);
|
|
595
|
-
await sendLongMessage(chatId, `Active session set to ${args}.`);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
case "close": {
|
|
599
|
-
const target = args || state.activeSessionId;
|
|
600
|
-
if (!target) {
|
|
601
|
-
await sendLongMessage(chatId, "No active session to close.");
|
|
602
|
-
return;
|
|
603
|
-
}
|
|
604
|
-
const ok = closeSession(state, target);
|
|
605
|
-
if (!ok) {
|
|
606
|
-
await sendLongMessage(chatId, `Session not found: ${target}`);
|
|
607
|
-
return;
|
|
608
|
-
}
|
|
609
|
-
await saveUserState(state);
|
|
610
|
-
await deleteSessionFile(userId, target).catch((error) => {
|
|
611
|
-
console.warn(`[tg-agent] delete session file failed id=${target}`, error);
|
|
612
|
-
});
|
|
613
|
-
await sendLongMessage(chatId, `Closed session ${target}.`);
|
|
614
|
-
return;
|
|
615
|
-
}
|
|
616
|
-
case "reset": {
|
|
617
|
-
const active = getActiveSession(state);
|
|
618
|
-
if (!active) {
|
|
619
|
-
await sendLongMessage(chatId, "No active session.");
|
|
620
|
-
return;
|
|
621
|
-
}
|
|
622
|
-
resetSession(active);
|
|
623
|
-
await saveUserState(state);
|
|
624
|
-
await deleteSessionFile(userId, active.id).catch((error) => {
|
|
625
|
-
console.warn(`[tg-agent] reset session file failed id=${active.id}`, error);
|
|
626
|
-
});
|
|
627
|
-
await sendLongMessage(chatId, `Reset session ${active.id}.`);
|
|
628
|
-
return;
|
|
629
|
-
}
|
|
630
|
-
case "providers": {
|
|
631
|
-
const { authStorage, modelRegistry } = await loadModelRegistry();
|
|
632
|
-
const error = modelRegistry.getError();
|
|
633
|
-
const text = formatProviders(modelRegistry, authStorage);
|
|
634
|
-
const message = error ? `Warning: ${error}\n\n${text}` : text;
|
|
635
|
-
await sendLongMessage(chatId, message);
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
case "models": {
|
|
639
|
-
const { authStorage, modelRegistry } = await loadModelRegistry();
|
|
640
|
-
let provider = args ? normalizeProviderId(args) : "";
|
|
641
|
-
if (!provider) {
|
|
642
|
-
const active = getActiveSession(state);
|
|
643
|
-
if (!active?.modelProvider) {
|
|
644
|
-
await sendLongMessage(chatId, "Usage: /models <provider>");
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
provider = active.modelProvider;
|
|
648
|
-
}
|
|
649
|
-
const authOk = authStorage.hasAuth(provider);
|
|
650
|
-
await sendLongMessage(chatId, formatModelsForProvider(modelRegistry, provider, authOk));
|
|
651
|
-
return;
|
|
652
|
-
}
|
|
653
|
-
case "provider": {
|
|
654
|
-
if (!args) {
|
|
655
|
-
await sendLongMessage(chatId, "Usage: /provider <name>");
|
|
656
|
-
return;
|
|
657
|
-
}
|
|
658
|
-
const provider = normalizeProviderId(args);
|
|
659
|
-
const { modelRegistry } = await loadModelRegistry();
|
|
660
|
-
const exists = modelRegistry.getAll().some((model) => model.provider === provider);
|
|
661
|
-
if (!exists) {
|
|
662
|
-
await sendLongMessage(chatId, `Unknown provider: ${provider}`);
|
|
663
|
-
return;
|
|
664
|
-
}
|
|
665
|
-
const session = await ensureActiveSessionForCommand(state, chatId);
|
|
666
|
-
session.modelProvider = provider;
|
|
667
|
-
session.modelId = undefined;
|
|
668
|
-
session.updatedAt = nowMs();
|
|
669
|
-
await saveUserState(state);
|
|
670
|
-
await sendLongMessage(chatId, `Session ${session.id} provider set to ${provider}.`);
|
|
671
|
-
return;
|
|
672
|
-
}
|
|
673
|
-
case "model": {
|
|
674
|
-
const selector = parseModelSelector(args);
|
|
675
|
-
if (!selector) {
|
|
676
|
-
await sendLongMessage(chatId, "Usage: /model <provider>/<model>");
|
|
677
|
-
return;
|
|
678
|
-
}
|
|
679
|
-
const session = await ensureActiveSessionForCommand(state, chatId);
|
|
680
|
-
const provider = normalizeProviderId(selector.provider ?? session.modelProvider ?? "");
|
|
681
|
-
if (!provider) {
|
|
682
|
-
await sendLongMessage(chatId, "Usage: /model <provider>/<model>");
|
|
683
|
-
return;
|
|
684
|
-
}
|
|
685
|
-
const { modelRegistry } = await loadModelRegistry();
|
|
686
|
-
const normalizedModelId = normalizeModelId(provider, selector.modelId);
|
|
687
|
-
const model = modelRegistry.find(provider, normalizedModelId);
|
|
688
|
-
if (!model) {
|
|
689
|
-
await sendLongMessage(chatId, `Model not found: ${provider}/${normalizedModelId}`);
|
|
690
|
-
return;
|
|
691
|
-
}
|
|
692
|
-
session.modelProvider = provider;
|
|
693
|
-
session.modelId = normalizedModelId;
|
|
694
|
-
session.updatedAt = nowMs();
|
|
695
|
-
await saveUserState(state);
|
|
696
|
-
await sendLongMessage(chatId, `Session ${session.id} model set to ${provider}/${normalizedModelId}.`);
|
|
697
|
-
return;
|
|
698
|
-
}
|
|
699
|
-
case "status": {
|
|
700
|
-
const active = getActiveSession(state);
|
|
701
|
-
if (!active) {
|
|
702
|
-
await sendLongMessage(chatId, "No active session.");
|
|
703
|
-
return;
|
|
704
|
-
}
|
|
705
|
-
const defaultModel = config.modelRef.includes("/")
|
|
706
|
-
? config.modelRef
|
|
707
|
-
: `${config.modelProvider}/${config.modelRef}`;
|
|
708
|
-
const overrides = active.modelProvider || active.modelId
|
|
709
|
-
? `Session model: ${active.modelProvider ?? "?"}/${active.modelId ?? "?"}`
|
|
710
|
-
: `Default model: ${defaultModel}`;
|
|
711
|
-
const message = [
|
|
712
|
-
`Session: ${active.id} (${active.title})`,
|
|
713
|
-
overrides,
|
|
714
|
-
].join("\n");
|
|
715
|
-
await sendLongMessage(chatId, message);
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
case "mcp": {
|
|
719
|
-
const message = await formatMcpStatus();
|
|
720
|
-
await sendLongMessage(chatId, message);
|
|
721
|
-
return;
|
|
722
|
-
}
|
|
723
|
-
case "login": {
|
|
724
|
-
const providers = getOAuthProviders();
|
|
725
|
-
if (!args) {
|
|
726
|
-
const lines = providers.map((p) => `- ${p.id} (${p.name})`);
|
|
727
|
-
await sendLongMessage(chatId, ["OAuth providers:", ...lines].join("\n"));
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
730
|
-
const provider = normalizeProviderId(args);
|
|
731
|
-
if (authInFlight.has(userId)) {
|
|
732
|
-
await sendLongMessage(chatId, "Login already in progress.");
|
|
733
|
-
return;
|
|
734
|
-
}
|
|
735
|
-
const providerInfo = providers.find((p) => p.id === provider);
|
|
736
|
-
if (!providerInfo) {
|
|
737
|
-
await sendLongMessage(chatId, `Unknown OAuth provider: ${provider}`);
|
|
738
|
-
return;
|
|
739
|
-
}
|
|
740
|
-
authInFlight.add(userId);
|
|
741
|
-
try {
|
|
742
|
-
const { authStorage } = await loadModelRegistry();
|
|
743
|
-
await sendLongMessage(chatId, `Starting login for ${providerInfo.id}...`);
|
|
744
|
-
await authStorage.login(providerInfo.id, {
|
|
745
|
-
onAuth: ({ url, instructions }) => {
|
|
746
|
-
const lines = [
|
|
747
|
-
"Open this URL in your browser:",
|
|
748
|
-
url,
|
|
749
|
-
];
|
|
750
|
-
if (instructions) {
|
|
751
|
-
lines.push("", instructions);
|
|
752
|
-
}
|
|
753
|
-
void sendLongMessage(chatId, lines.join("\n"));
|
|
754
|
-
},
|
|
755
|
-
onPrompt: (prompt) => promptForAuthInput(userId, chatId, prompt),
|
|
756
|
-
onProgress: (message) => {
|
|
757
|
-
void sendLongMessage(chatId, message);
|
|
758
|
-
},
|
|
759
|
-
onManualCodeInput: () => promptForAuthInput(userId, chatId, { message: "Paste the authorization code:" }),
|
|
760
|
-
});
|
|
761
|
-
await sendLongMessage(chatId, `Login completed for ${providerInfo.id}.`);
|
|
762
|
-
}
|
|
763
|
-
catch (error) {
|
|
764
|
-
const message = formatErrorMessage(error);
|
|
765
|
-
await sendLongMessage(chatId, `Login failed: ${message}`);
|
|
766
|
-
}
|
|
767
|
-
finally {
|
|
768
|
-
authInFlight.delete(userId);
|
|
769
|
-
clearAuthPrompt(userId);
|
|
770
|
-
}
|
|
771
|
-
return;
|
|
772
|
-
}
|
|
773
|
-
case "logout": {
|
|
774
|
-
if (!args) {
|
|
775
|
-
await sendLongMessage(chatId, "Usage: /logout <provider>");
|
|
776
|
-
return;
|
|
777
|
-
}
|
|
778
|
-
const provider = normalizeProviderId(args);
|
|
779
|
-
const { authStorage } = await loadModelRegistry();
|
|
780
|
-
authStorage.logout(provider);
|
|
781
|
-
await sendLongMessage(chatId, `Logged out from ${provider}.`);
|
|
782
|
-
return;
|
|
783
|
-
}
|
|
784
|
-
default: {
|
|
785
|
-
await sendLongMessage(chatId, `Unknown command: ${command}`);
|
|
786
|
-
}
|
|
787
|
-
}
|
|
788
|
-
}
|
|
789
|
-
async function handleMessage(chatId, userId, text) {
|
|
790
|
-
const command = parseCommand(text);
|
|
791
|
-
if (command) {
|
|
792
|
-
await handleCommand(chatId, userId, command.command, command.args);
|
|
793
|
-
return;
|
|
794
|
-
}
|
|
795
|
-
const state = await loadUserState(userId);
|
|
796
|
-
const expired = pruneExpiredSessions(state);
|
|
797
|
-
if (expired.length > 0) {
|
|
798
|
-
await saveUserState(state);
|
|
799
|
-
await Promise.all(expired.map((sessionId) => deleteSessionFile(userId, sessionId).catch((error) => {
|
|
800
|
-
console.warn(`[tg-agent] cleanup session file failed id=${sessionId}`, error);
|
|
801
|
-
})));
|
|
802
|
-
}
|
|
803
|
-
let session = getActiveSession(state);
|
|
804
|
-
if (!session) {
|
|
805
|
-
try {
|
|
806
|
-
session = createSession(state, "");
|
|
807
|
-
await saveUserState(state);
|
|
808
|
-
await sendLongMessage(chatId, `Created session ${session.id}.`);
|
|
809
|
-
}
|
|
810
|
-
catch (error) {
|
|
811
|
-
await sendLongMessage(chatId, error.message);
|
|
812
|
-
return;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
const userMessage = {
|
|
816
|
-
role: "user",
|
|
817
|
-
content: text,
|
|
818
|
-
ts: nowMs(),
|
|
819
|
-
};
|
|
820
|
-
appendMessage(session, userMessage, config.maxHistoryMessages);
|
|
821
|
-
await saveUserState(state);
|
|
822
|
-
console.log(`[tg-agent] request user=${userId} session=${session.id} messages=${session.messages.length} textLen=${text.length} provider=${session.modelProvider ?? "-"} model=${session.modelId ?? "-"}`);
|
|
823
|
-
let statusReporter = null;
|
|
824
|
-
try {
|
|
825
|
-
statusReporter = await createStatusReporter(chatId);
|
|
826
|
-
}
|
|
827
|
-
catch (error) {
|
|
828
|
-
console.warn("[tg-agent] status message failed", error);
|
|
829
|
-
}
|
|
830
|
-
const activeRun = {
|
|
831
|
-
sessionId: session.id,
|
|
832
|
-
chatId,
|
|
833
|
-
status: statusReporter,
|
|
834
|
-
abortRequested: false,
|
|
835
|
-
cancelRequested: false,
|
|
836
|
-
};
|
|
837
|
-
registerActiveRun(userId, activeRun);
|
|
838
|
-
try {
|
|
839
|
-
const assistantText = await withSemaphore(async () => {
|
|
840
|
-
const result = await runPiAgentPrompt({
|
|
841
|
-
userId,
|
|
842
|
-
sessionId: session.id,
|
|
843
|
-
prompt: text,
|
|
844
|
-
systemPrompt: config.systemPrompt,
|
|
845
|
-
modelProvider: session.modelProvider,
|
|
846
|
-
modelId: session.modelId,
|
|
847
|
-
onAbortReady: (abort) => {
|
|
848
|
-
activeRun.abort = abort;
|
|
849
|
-
if (activeRun.abortRequested) {
|
|
850
|
-
abort();
|
|
851
|
-
}
|
|
852
|
-
},
|
|
853
|
-
onStatus: statusReporter
|
|
854
|
-
? (event) => {
|
|
855
|
-
const statusText = formatStatusEvent(event);
|
|
856
|
-
if (statusText) {
|
|
857
|
-
statusReporter?.update(statusText);
|
|
858
|
-
}
|
|
859
|
-
}
|
|
860
|
-
: undefined,
|
|
861
|
-
});
|
|
862
|
-
console.log(`[tg-agent] response session=${session.id} model=${result.modelProvider}/${result.modelId} file=${result.sessionFile}`);
|
|
863
|
-
return result.text;
|
|
864
|
-
});
|
|
865
|
-
if (activeRun.cancelRequested) {
|
|
866
|
-
if (statusReporter) {
|
|
867
|
-
await statusReporter.finalize("Cancelled by user.");
|
|
868
|
-
}
|
|
869
|
-
else {
|
|
870
|
-
await sendLongMessage(chatId, "Cancelled by user.");
|
|
871
|
-
}
|
|
872
|
-
return;
|
|
873
|
-
}
|
|
874
|
-
const assistantMessage = {
|
|
875
|
-
role: "assistant",
|
|
876
|
-
content: assistantText,
|
|
877
|
-
ts: nowMs(),
|
|
878
|
-
};
|
|
879
|
-
appendMessage(session, assistantMessage, config.maxHistoryMessages);
|
|
880
|
-
await saveUserState(state);
|
|
881
|
-
if (statusReporter) {
|
|
882
|
-
await statusReporter.finalize(assistantText);
|
|
883
|
-
}
|
|
884
|
-
else {
|
|
885
|
-
await sendLongMessage(chatId, assistantText);
|
|
886
|
-
}
|
|
887
|
-
}
|
|
888
|
-
catch (error) {
|
|
889
|
-
console.error("[tg-agent] runModel error", error);
|
|
890
|
-
if (activeRun.cancelRequested) {
|
|
891
|
-
if (statusReporter) {
|
|
892
|
-
await statusReporter.fail("Cancelled by user.");
|
|
893
|
-
}
|
|
894
|
-
else {
|
|
895
|
-
await sendLongMessage(chatId, "Cancelled by user.");
|
|
896
|
-
}
|
|
897
|
-
return;
|
|
898
|
-
}
|
|
899
|
-
const message = formatErrorMessage(error);
|
|
900
|
-
if (statusReporter) {
|
|
901
|
-
await statusReporter.fail(`Error: ${message}`);
|
|
902
|
-
}
|
|
903
|
-
else {
|
|
904
|
-
await sendLongMessage(chatId, `Error: ${message}`);
|
|
905
|
-
}
|
|
906
|
-
}
|
|
907
|
-
finally {
|
|
908
|
-
clearActiveRun(userId);
|
|
909
|
-
}
|
|
910
|
-
}
|
|
911
|
-
bot.on("message", (msg) => {
|
|
912
|
-
if (!msg.text) {
|
|
913
|
-
return;
|
|
914
|
-
}
|
|
915
|
-
if (msg.chat.type !== "private") {
|
|
916
|
-
void bot.sendMessage(msg.chat.id, "This bot only supports direct messages.");
|
|
917
|
-
return;
|
|
918
|
-
}
|
|
919
|
-
if (!msg.from?.id) {
|
|
920
|
-
return;
|
|
921
|
-
}
|
|
922
|
-
const userId = String(msg.from.id);
|
|
923
|
-
const chatId = msg.chat.id;
|
|
924
|
-
if (!isUserAllowed(userId)) {
|
|
925
|
-
logUnauthorizedUser(userId, chatId);
|
|
926
|
-
return;
|
|
927
|
-
}
|
|
928
|
-
const text = msg.text.trim();
|
|
929
|
-
const pendingAuth = authPromptState.get(userId);
|
|
930
|
-
if (pendingAuth) {
|
|
931
|
-
if (text === "/stop" || text === "/cancel") {
|
|
932
|
-
cancelAuthPrompt(userId, "Login cancelled by user.");
|
|
933
|
-
void sendLongMessage(chatId, "Login cancelled.");
|
|
934
|
-
return;
|
|
935
|
-
}
|
|
936
|
-
clearAuthPrompt(userId);
|
|
937
|
-
pendingAuth.resolve(text);
|
|
938
|
-
return;
|
|
939
|
-
}
|
|
940
|
-
const command = parseCommand(text);
|
|
941
|
-
if (command?.command === "stop") {
|
|
942
|
-
void handleStopCommand(chatId, userId);
|
|
943
|
-
return;
|
|
944
|
-
}
|
|
945
|
-
enqueueUser(userId, () => handleMessage(chatId, userId, text)).catch(async (error) => {
|
|
946
|
-
console.error("[tg-agent] runModel error", error);
|
|
947
|
-
const message = formatErrorMessage(error);
|
|
948
|
-
await sendLongMessage(chatId, `Error: ${message}`);
|
|
949
|
-
});
|
|
950
|
-
});
|
|
951
|
-
bot.on("polling_error", (error) => {
|
|
952
|
-
console.error("Polling error", error);
|
|
953
|
-
});
|
|
954
|
-
console.log("tg-agent started");
|
|
1
|
+
import b from"node:path";import B from"node:fs/promises";import te from"node-telegram-bot-api";import{discoverAuthStorage as ne,discoverModels as oe}from"@mariozechner/pi-coding-agent";import{getOAuthProviders as re}from"@mariozechner/pi-ai";import{config as m,assertConfig as se}from"./config.js";import{resolveApiKeyForProvider as ie,resolveProxyInfo as ae}from"./auth.js";import{applyFetchProxy as ce}from"./proxy.js";import{runPiAgentPrompt as le}from"./piAgentRunner.js";import{readCodexOAuth as ue}from"./codexAuth.js";import{formatMcpTarget as de,freezeMcpCatalog as F,getMcpCatalog as q,loadMcpServers as me,loadMcpServersSync as fe,probeMcpServer as ge,refreshMcpCatalog as pe}from"./mcp.js";import{appendMessage as W,closeSession as we,createSession as L,deleteSessionFile as k,getActiveSession as x,listSessions as he,loadUserState as H,pruneExpiredSessions as X,resetSession as ye,saveUserState as w,setActiveSession as $e}from"./sessionStore.js";import{chunkText as ve,createQueueMap as Se,createSemaphore as Me,ensureDir as U,nowMs as C}from"./utils.js";se();const y=new te(m.telegramToken,{polling:!0}),_e=Se(),Ae=Me(m.maxConcurrent),O=new Map;function Pe(e,t){O.set(e,t)}function Te(e){O.delete(e)}function xe(e){const t=e.trim();if(t){if(t==="Markdown"||t==="MarkdownV2"||t==="HTML")return t;console.warn(`[tg-agent] invalid TELEGRAM_PARSE_MODE=${t}, ignoring`)}}const Ee="image/jpeg";function Re(){return b.join(m.agentDir,"uploads")}function ke(e){return e.length===0?null:e.reduce((t,n)=>{const o=t.file_size??t.width*t.height;return(n.file_size??n.width*n.height)>o?n:t})}function Ce(e){return!!e?.startsWith("image/")}function De(e){switch(b.extname(e).toLowerCase()){case".png":return"image/png";case".jpg":case".jpeg":return"image/jpeg";case".webp":return"image/webp";case".gif":return"image/gif";default:return}}function be(e){if(!Number.isFinite(e)||e<=0)return"0 B";const t=["B","KB","MB","GB"];let n=e,o=0;for(;n>=1024&&o<t.length-1;)n/=1024,o+=1;return`${n.toFixed(n>=10||o===0?0:1)} ${t[o]}`}function Le(e){return e.length===0?"":e.map(t=>{const n=[];t.mimeType&&n.push(t.mimeType),t.bytes>0&&n.push(be(t.bytes));const o=n.length>0?` (${n.join(", ")})`:"";return`Attachment saved: ${t.path}${o}`}).join(`
|
|
2
|
+
`)}function Ue(e){const t=[];if(e.photo&&e.photo.length>0){const n=ke(e.photo);n&&t.push({kind:"photo",fileId:n.file_id,fileSize:n.file_size})}return e.document&&t.push({kind:"document",fileId:e.document.file_id,fileName:e.document.file_name,mimeType:e.document.mime_type,fileSize:e.document.file_size}),t}async function Oe(e){if(e.length===0)return{resolved:[],images:[]};const t=Re();await U(t);const n=[],o=[];for(const s of e)try{const a=await y.downloadFile(s.fileId,t),r=await B.stat(a),c=s.fileName??b.basename(a),u=s.mimeType??De(a),i=s.kind==="photo"||Ce(u),d={path:a,name:c,mimeType:u,bytes:r.size,isImage:i};if(n.push(d),i){const f=await B.readFile(a);o.push({type:"image",data:f.toString("base64"),mimeType:u??Ee})}}catch(a){console.warn("[tg-agent] attachment download failed",a)}return{resolved:n,images:o}}const ze=new Set(["_","*","[","]","(",")","~","`",">","#","+","-","=","|","{","}",".","!"]);function E(e,t){let n=0;for(let o=0;o<e.length;o+=1){const s=e[o];if(s==="\\"){o+=1;continue}s===t&&(n+=1)}return n}function Ne(e){if(E(e,"*")%2!==0||E(e,"_")%2!==0||E(e,"`")%2!==0)return!1;const s=E(e,"["),a=E(e,"]");return s===a}function je(e){for(let t=0;t<e.length;t+=1){const n=e[t];if(n==="\\"){t+=1;continue}if(ze.has(n))return!1}return!0}function V(e){const t=xe(m.telegramParseMode);return t?[t]:je(e)?["MarkdownV2","Markdown"]:Ne(e)?["Markdown"]:[]}const Be=600*1e3,S=new Map,z=new Set,Fe=new Map([["codex","openai-codex"],["antigravity","google-antigravity"],["gemini","google-gemini-cli"],["gemini-cli","google-gemini-cli"]]),qe={"codex-mini-latest":"gpt-5.1-codex-mini","codex-max-latest":"gpt-5.1-codex-max","codex-latest":"gpt-5.2-codex"};function A(e){const t=e.trim().toLowerCase();return Fe.get(t)??t}function We(e,t){return e!=="openai-codex"?t:qe[t]??t}async function P(){await U(m.agentDir);const e=ne(m.agentDir),t=ue();t&&e.setRuntimeApiKey("openai-codex",t.accessToken);const n=oe(e,m.agentDir);return{authStorage:e,modelRegistry:n}}function K(e){const t=S.get(e);t&&(clearTimeout(t.timeoutId),S.delete(e))}function He(e,t){const n=S.get(e);n&&(clearTimeout(n.timeoutId),S.delete(e),n.reject(new Error(t)))}async function G(e,t,n){if(S.has(e))throw new Error("Login is already awaiting input.");const o=n.placeholder?`${n.message} (${n.placeholder})`:n.message;return await l(t,o),await new Promise((s,a)=>{const r=setTimeout(()=>{S.delete(e),a(new Error("Login prompt timed out."))},Be);S.set(e,{resolve:s,reject:a,timeoutId:r,chatId:t})})}async function Xe(e,t){const n=O.get(t);if(!n){await l(e,"No active request to stop.");return}n.cancelRequested=!0,n.abortRequested=!0,n.status&&n.status.update("Cancelled by user.",!0),n.abort&&n.abort(),(!n.status||n.chatId!==e)&&await l(e,"Stopping current request...")}function Ve(){const e={https_proxy:process.env.https_proxy??process.env.HTTPS_PROXY??"",http_proxy:process.env.http_proxy??process.env.HTTP_PROXY??"",all_proxy:process.env.all_proxy??process.env.ALL_PROXY??"",no_proxy:process.env.no_proxy??process.env.NO_PROXY??""},t={NODE_EXTRA_CA_CERTS:process.env.NODE_EXTRA_CA_CERTS??"",NODE_TLS_REJECT_UNAUTHORIZED:process.env.NODE_TLS_REJECT_UNAUTHORIZED??""};console.log(`[tg-agent] modelProvider=${m.modelProvider} modelRef=${m.modelRef} sessionDir=${m.sessionDir} maxConcurrent=${m.maxConcurrent}`),console.log(`[tg-agent] agentDir=${m.agentDir} workspaceDir=${m.workspaceDir}`);const n=fe(m.agentDir),o=n.length>0?`on (${n.length})`:"off";console.log(`[tg-agent] tools fetchMaxBytes=${m.fetchMaxBytes} fetchTimeoutMs=${m.fetchTimeoutMs} mcp=${o}`),console.log(`[tg-agent] proxy https=${e.https_proxy||"(empty)"} http=${e.http_proxy||"(empty)"} all=${e.all_proxy||"(empty)"} no=${e.no_proxy||"(empty)"}`),console.log(`[tg-agent] tls extra_ca=${t.NODE_EXTRA_CA_CERTS||"(empty)"} reject_unauthorized=${t.NODE_TLS_REJECT_UNAUTHORIZED||"(empty)"}`);try{const{source:a}=ie(m.modelProvider);console.log(`[tg-agent] authSource=${a}`)}catch(a){const r=a instanceof Error?a.message:String(a);console.warn(`[tg-agent] authSource=missing (${r})`)}const s=ae();console.log(s?`[tg-agent] proxyUrl=${s.url} kind=${s.kind} source=${s.source}`:"[tg-agent] proxyUrl=(none)")}Ve(),U(m.agentDir).catch(e=>{console.warn(`[tg-agent] ensure agentDir failed: ${e instanceof Error?e.message:String(e)}`)}),(async()=>{try{await q(m.agentDir,{timeoutMs:3e3,maxBytes:m.fetchMaxBytes,maxChars:2e3}),F()}catch(e){const t=e instanceof Error?e.message:String(e);console.warn(`[tg-agent] mcp catalog warmup failed: ${t}`)}})();const D=ce();console.log(D?`[tg-agent] fetchProxy=${D.url} kind=${D.kind} source=${D.source}`:"[tg-agent] fetchProxy=(none)");function J(e){if(!e.startsWith("/"))return null;const t=e.trim(),[n,...o]=t.split(" ");return{command:n.split("@")[0].slice(1).toLowerCase(),args:o.join(" ").trim()}}async function Y(e,t){let n=x(e);return n||(n=L(e,""),await w(e),await l(t,`Created session ${n.id}.`)),n}function Ke(e,t){const n=e.getAll(),o=new Map;for(const a of n){const r=o.get(a.provider)??{count:0,auth:t.hasAuth(a.provider)};r.count+=1,o.set(a.provider,r)}return o.size===0?"No providers found.":["Providers:",...Array.from(o.entries()).sort((a,r)=>a[0].localeCompare(r[0])).map(([a,r])=>`- ${a} (models: ${r.count}, auth: ${r.auth?"ok":"missing"})`),"","Use /models <provider> to list models."].join(`
|
|
3
|
+
`)}function Ge(e,t,n){const o=e.getAll().filter(a=>a.provider===t);if(o.length===0)return`No models found for provider ${t}.`;const s=o.map(a=>`- ${a.id} | ${a.name}`);return[`Models for ${t} (auth: ${n?"ok":"missing"}):`,...s].join(`
|
|
4
|
+
`)}function Je(e){const t=e.trim();if(!t)return null;if(t.includes("/")){const[n,o]=t.split("/",2);return!n||!o?null:{provider:A(n),modelId:o.trim()}}return{modelId:t}}const Ye=6e4,Z=new Map;function Ze(e,t){const n=Date.now(),o=Z.get(e)??0;n-o<Ye||(Z.set(e,n),console.warn(`[tg-agent] unauthorized user=${e} chat=${t}`))}function Qe(e){const t=m.telegramAllowedUsers;return!t||t.size===0?!0:t.has(e)}function Ie(e){return e.length===0?"No sessions found.":["Sessions:",...e.map(n=>{const o=new Date(n.updatedAt).toISOString();return`- ${n.id} | ${n.title} | updated ${o}`})].join(`
|
|
5
|
+
`)}async function Q(e,t,n){return n?await y.sendMessage(e,t,{parse_mode:n}):await y.sendMessage(e,t)}async function I(e,t,n,o){if(o){await y.editMessageText(n,{chat_id:e,message_id:t,parse_mode:o});return}await y.editMessageText(n,{chat_id:e,message_id:t})}async function ee(e,t){let n;for(const o of t)try{return await e(o)}catch(s){n=s;const a=s instanceof Error?s.message:String(s);console.warn(`[tg-agent] parse_mode ${o} failed, trying fallback: ${a}`)}try{return await e(void 0)}catch(o){if(n){const s=o instanceof Error?o.message:String(o);console.warn(`[tg-agent] parse_mode fallback to plain failed: ${s}`)}throw o}}async function et(e,t){return await ee(n=>Q(e,t,n),V(t))}async function tt(e,t,n){await ee(o=>I(e,t,n,o),V(n))}async function l(e,t){const n=ve(t,3900);for(const o of n)await et(e,o)}const nt=3900,ot=1200;function N(e,t=nt){return e.length<=t?{text:e,truncated:!1}:{text:`${e.slice(0,Math.max(0,t-15))}...
|
|
6
|
+
|
|
7
|
+
[truncated]`,truncated:!0}}async function rt(e){const t="Working...",o=(await Q(e,t)).message_id;let s=t,a=0,r=Promise.resolve();const c=(i,d=!1)=>{const f=Date.now();!d&&f-a<ot||i!==s&&(s=i,a=f,r=r.then(async()=>{await I(e,o,i)}).catch(h=>{console.warn("[tg-agent] status edit failed",h)}))},u=i=>{s=i,a=Date.now(),r=r.then(async()=>{await tt(e,o,i)}).catch(d=>{console.warn("[tg-agent] status edit failed",d)})};return{update:(i,d=!1)=>{const f=N(i).text;c(f,d)},finalize:async i=>{const d=N(i).text;u(d),await r},fail:async i=>{const d=N(i).text;u(d),await r}}}function st(e){switch(e.type){case"agent_start":return"Working...";case"tool_start":return`Running tool: ${e.name}
|
|
8
|
+
args: ${it(e.args)}`;case"tool_end":return e.ok?`Tool finished: ${e.name} (${e.durationMs}ms)`:`Tool failed: ${e.name} (${e.durationMs}ms)`;case"message_start":return e.role==="assistant"?"Generating reply...":null;case"message_end":return e.role==="assistant"?"Finalizing reply...":null;case"heartbeat":return`Working... ${Math.max(1,Math.round(e.elapsedMs/1e3))}s`;default:return null}}function it(e){try{const t=new WeakSet,n=200,o=12,s=12,a=3,r=(i,d)=>{if(i==null)return i;if(typeof i=="string")return i.length>n?`${i.slice(0,n)}...`:i;if(typeof i=="number"||typeof i=="boolean")return i;if(typeof i=="bigint")return i.toString();if(typeof i=="function")return"[function]";if(typeof i!="object")return String(i);if(t.has(i))return"[circular]";if(d>=a)return"[truncated]";if(t.add(i),Array.isArray(i)){const M=i.slice(0,s).map(g=>r(g,d+1));return i.length>s&&M.push("[truncated]"),M}const f=Object.entries(i),h={};for(const[M,g]of f.slice(0,o))h[M]=r(g,d+1);return f.length>o&&(h._truncated=!0),h},c=r(e,0),u=JSON.stringify(c);return u?u.length>700?`${u.slice(0,700)}...`:u:"null"}catch{return"[unavailable]"}}function j(e){const t=e instanceof Error?e.message:String(e),n=e?.cause;return n instanceof Error?`${t} (${n.message})`:n?`${t} (${String(n)})`:t}function at(e,t=140){return e.length<=t?e:`${e.slice(0,t-3)}...`}async function ct(){const e=await me(m.agentDir);if(e.length===0)return"No MCP servers configured. Add [mcp_servers.*] to ~/.tg-agent/config.toml.";const t=await Promise.all(e.map(o=>ge(o))),n=["MCP servers:"];for(let o=0;o<e.length;o+=1){const s=e[o],a=t[o],r=a.ok?"ok":`error: ${at(a.error??"unknown")}`;n.push(`- ${s.name} (${s.type}) ${de(s)} status=${r} (${a.durationMs}ms)`)}return n.join(`
|
|
9
|
+
`)}async function lt(e,t,n,o){const s=await H(t),a=X(s);switch(a.length>0&&(await w(s),await Promise.all(a.map(r=>k(t,r).catch(c=>{console.warn(`[tg-agent] cleanup session file failed id=${r}`,c)})))),n){case"start":case"help":{const r=["Commands:","/new [title] - create a new session","/list - list sessions","/use <id> - switch active session","/close [id] - close a session (default: active)","/reset - clear active session history","/providers - list available providers","/models [provider] - list models for provider","/provider <name> - set provider for current session","/model <provider>/<model> - set model for current session","/mcp - list configured MCP servers","/mcp refresh - reload MCP catalog","/status - show session model settings","/login <provider> - login to OAuth provider","/logout <provider> - logout from provider","/stop - stop the current running request","","Tips:","Send images or files to attach them to the prompt."].join(`
|
|
10
|
+
`);await l(e,r);return}case"new":{try{const c=L(s,o||"");await w(s),await l(e,`Created session ${c.id} (${c.title}).`)}catch(r){await l(e,r.message)}return}case"list":{await w(s),await l(e,Ie(he(s)));return}case"use":{if(!o){await l(e,"Usage: /use <id>");return}if(!$e(s,o)){await l(e,`Session not found: ${o}`);return}await w(s),await l(e,`Active session set to ${o}.`);return}case"close":{const r=o||s.activeSessionId;if(!r){await l(e,"No active session to close.");return}if(!we(s,r)){await l(e,`Session not found: ${r}`);return}await w(s),await k(t,r).catch(u=>{console.warn(`[tg-agent] delete session file failed id=${r}`,u)}),await l(e,`Closed session ${r}.`);return}case"reset":{const r=x(s);if(!r){await l(e,"No active session.");return}ye(r),await w(s),await k(t,r.id).catch(c=>{console.warn(`[tg-agent] reset session file failed id=${r.id}`,c)}),await l(e,`Reset session ${r.id}.`);return}case"providers":{const{authStorage:r,modelRegistry:c}=await P(),u=c.getError(),i=Ke(c,r),d=u?`Warning: ${u}
|
|
11
|
+
|
|
12
|
+
${i}`:i;await l(e,d);return}case"models":{const{authStorage:r,modelRegistry:c}=await P();let u=o?A(o):"";if(!u){const d=x(s);if(!d?.modelProvider){await l(e,"Usage: /models <provider>");return}u=d.modelProvider}const i=r.hasAuth(u);await l(e,Ge(c,u,i));return}case"provider":{if(!o){await l(e,"Usage: /provider <name>");return}const r=A(o),{modelRegistry:c}=await P();if(!c.getAll().some(d=>d.provider===r)){await l(e,`Unknown provider: ${r}`);return}const i=await Y(s,e);i.modelProvider=r,i.modelId=void 0,i.updatedAt=C(),await w(s),await l(e,`Session ${i.id} provider set to ${r}.`);return}case"model":{const r=Je(o);if(!r){await l(e,"Usage: /model <provider>/<model>");return}const c=await Y(s,e),u=A(r.provider??c.modelProvider??"");if(!u){await l(e,"Usage: /model <provider>/<model>");return}const{modelRegistry:i}=await P(),d=We(u,r.modelId);if(!i.find(u,d)){await l(e,`Model not found: ${u}/${d}`);return}c.modelProvider=u,c.modelId=d,c.updatedAt=C(),await w(s),await l(e,`Session ${c.id} model set to ${u}/${d}.`);return}case"status":{const r=x(s);if(!r){await l(e,"No active session.");return}const c=m.modelRef.includes("/")?m.modelRef:`${m.modelProvider}/${m.modelRef}`,u=r.modelProvider||r.modelId?`Session model: ${r.modelProvider??"?"}/${r.modelId??"?"}`:`Default model: ${c}`,i=[`Session: ${r.id} (${r.title})`,u].join(`
|
|
13
|
+
`);await l(e,i);return}case"mcp":{if(o.trim().toLowerCase()==="refresh"){pe(),await q(m.agentDir,{timeoutMs:5e3,maxBytes:m.fetchMaxBytes,maxChars:3e3}),F(),await l(e,"MCP catalog refreshed.");return}const r=await ct();await l(e,r);return}case"login":{const r=re();if(!o){const i=r.map(d=>`- ${d.id} (${d.name})`);await l(e,["OAuth providers:",...i].join(`
|
|
14
|
+
`));return}const c=A(o);if(z.has(t)){await l(e,"Login already in progress.");return}const u=r.find(i=>i.id===c);if(!u){await l(e,`Unknown OAuth provider: ${c}`);return}z.add(t);try{const{authStorage:i}=await P();await l(e,`Starting login for ${u.id}...`),await i.login(u.id,{onAuth:({url:d,instructions:f})=>{const h=["Open this URL in your browser:",d];f&&h.push("",f),l(e,h.join(`
|
|
15
|
+
`))},onPrompt:d=>G(t,e,d),onProgress:d=>{l(e,d)},onManualCodeInput:()=>G(t,e,{message:"Paste the authorization code:"})}),await l(e,`Login completed for ${u.id}.`)}catch(i){const d=j(i);await l(e,`Login failed: ${d}`)}finally{z.delete(t),K(t)}return}case"logout":{if(!o){await l(e,"Usage: /logout <provider>");return}const r=A(o),{authStorage:c}=await P();c.logout(r),await l(e,`Logged out from ${r}.`);return}default:await l(e,`Unknown command: ${n}`)}}async function ut(e,t,n,o=[]){const s=J(n);if(s){await lt(e,t,s.command,s.args);return}const a=await H(t),r=X(a);r.length>0&&(await w(a),await Promise.all(r.map(p=>k(t,p).catch(_=>{console.warn(`[tg-agent] cleanup session file failed id=${p}`,_)}))));let c=x(a);if(!c)try{c=L(a,""),await w(a),await l(e,`Created session ${c.id}.`)}catch(p){await l(e,p.message);return}const{resolved:u,images:i}=await Oe(o);if(o.length>0&&u.length===0&&!n.trim()){await l(e,"Failed to download attachments.");return}const d=Le(u),f=[];n.trim()&&f.push(n.trim()),d&&f.push(d);const h=f.join(`
|
|
16
|
+
|
|
17
|
+
`)||"Attachment received.",M={role:"user",content:h,ts:C()};W(c,M,m.maxHistoryMessages),await w(a),console.log(`[tg-agent] request user=${t} session=${c.id} messages=${c.messages.length} textLen=${h.length} attachments=${u.length} provider=${c.modelProvider??"-"} model=${c.modelId??"-"}`);let g=null;try{g=await rt(e)}catch(p){console.warn("[tg-agent] status message failed",p)}const T={sessionId:c.id,chatId:e,status:g,abortRequested:!1,cancelRequested:!1};Pe(t,T);try{const p=await Ae(async()=>{const R=await le({userId:t,sessionId:c.id,prompt:h,images:i,systemPrompt:m.systemPrompt,modelProvider:c.modelProvider,modelId:c.modelId,telegram:{chatId:e,sendPhoto:async($,v)=>{await y.sendPhoto(e,$,v?{caption:v}:void 0)},sendDocument:async($,v)=>{await y.sendDocument(e,$,v?{caption:v}:void 0)}},onAbortReady:$=>{T.abort=$,T.abortRequested&&$()},onStatus:g?$=>{const v=st($);v&&g?.update(v)}:void 0});return console.log(`[tg-agent] response session=${c.id} model=${R.modelProvider}/${R.modelId} file=${R.sessionFile}`),R.text});if(T.cancelRequested){g?await g.finalize("Cancelled by user."):await l(e,"Cancelled by user.");return}const _={role:"assistant",content:p,ts:C()};W(c,_,m.maxHistoryMessages),await w(a),g?await g.finalize(p):await l(e,p)}catch(p){if(console.error("[tg-agent] runModel error",p),T.cancelRequested){g?await g.fail("Cancelled by user."):await l(e,"Cancelled by user.");return}const _=j(p);g?await g.fail(`Error: ${_}`):await l(e,`Error: ${_}`)}finally{Te(t)}}y.on("message",e=>{if(e.chat.type!=="private"){y.sendMessage(e.chat.id,"This bot only supports direct messages.");return}if(!e.from?.id)return;const t=String(e.from.id),n=e.chat.id;if(!Qe(t)){Ze(t,n);return}const o=(e.text??e.caption??"").trim(),s=Ue(e);if(!o&&s.length===0)return;const a=S.get(t);if(a){if(o==="/stop"||o==="/cancel"){He(t,"Login cancelled by user."),l(n,"Login cancelled.");return}if(!o){l(n,"Login expects a text response.");return}K(t),a.resolve(o);return}if(J(o)?.command==="stop"){Xe(n,t);return}_e(t,()=>ut(n,t,o,s)).catch(async c=>{console.error("[tg-agent] runModel error",c);const u=j(c);await l(n,`Error: ${u}`)})}),y.on("polling_error",e=>{console.error("Polling error",e)}),console.log("tg-agent started");
|