@llblab/pi-telegram 0.19.3 → 0.20.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/AGENTS.md +5 -3
- package/BACKLOG.md +15 -2
- package/CHANGELOG.md +9 -3
- package/README.md +57 -57
- package/docs/architecture.md +4 -1
- package/docs/public-api.md +1 -1
- package/index.ts +67 -12
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +27 -8
- package/lib/bus-leader.ts +26 -42
- package/lib/bus.ts +19 -22
- package/lib/commands.ts +41 -8
- package/lib/config.ts +165 -26
- package/lib/locks.ts +78 -25
- package/lib/{runtime-log.ts → logs.ts} +58 -27
- package/lib/media.ts +97 -4
- package/lib/outbound-buttons.ts +1 -1
- package/lib/outbound.ts +5 -8
- package/lib/paths.ts +77 -0
- package/lib/prompt-templates.ts +3 -1
- package/lib/prompts.ts +1 -1
- package/lib/queue.ts +32 -1
- package/lib/routing.ts +106 -41
- package/lib/status.ts +6 -1
- package/lib/sync.ts +22 -17
- package/lib/telegram-api.ts +13 -14
- package/lib/threads.ts +142 -55
- package/lib/turns.ts +47 -13
- package/package.json +3 -3
- /package/{banner.png → screenshot.png} +0 -0
package/lib/media.ts
CHANGED
|
@@ -33,11 +33,36 @@ export interface TelegramRichMessage {
|
|
|
33
33
|
blocks?: unknown[];
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
+
export interface TelegramMessageUser {
|
|
37
|
+
id?: number;
|
|
38
|
+
is_bot?: boolean;
|
|
39
|
+
first_name?: string;
|
|
40
|
+
last_name?: string;
|
|
41
|
+
username?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface TelegramMessageForwardOrigin {
|
|
45
|
+
type?: string;
|
|
46
|
+
sender_user?: TelegramMessageUser;
|
|
47
|
+
sender_user_name?: string;
|
|
48
|
+
sender_chat?: { title?: string; username?: string; id?: number };
|
|
49
|
+
chat?: { title?: string; username?: string; id?: number };
|
|
50
|
+
author_signature?: string;
|
|
51
|
+
}
|
|
52
|
+
|
|
36
53
|
export interface TelegramReplyToMessage {
|
|
37
54
|
message_id?: number;
|
|
55
|
+
from?: TelegramMessageUser;
|
|
38
56
|
text?: string;
|
|
39
57
|
caption?: string;
|
|
40
58
|
rich_message?: TelegramRichMessage;
|
|
59
|
+
photo?: TelegramPhotoSize[];
|
|
60
|
+
document?: TelegramDocument;
|
|
61
|
+
video?: TelegramVideo;
|
|
62
|
+
audio?: TelegramAudio;
|
|
63
|
+
voice?: TelegramVoice;
|
|
64
|
+
animation?: TelegramAnimation;
|
|
65
|
+
sticker?: TelegramSticker;
|
|
41
66
|
}
|
|
42
67
|
|
|
43
68
|
export interface TelegramSticker {
|
|
@@ -46,6 +71,10 @@ export interface TelegramSticker {
|
|
|
46
71
|
|
|
47
72
|
export interface TelegramMediaMessage {
|
|
48
73
|
message_id: number;
|
|
74
|
+
from?: TelegramMessageUser;
|
|
75
|
+
forward_origin?: TelegramMessageForwardOrigin;
|
|
76
|
+
forward_from?: TelegramMessageUser;
|
|
77
|
+
forward_sender_name?: string;
|
|
49
78
|
text?: string;
|
|
50
79
|
caption?: string;
|
|
51
80
|
rich_message?: TelegramRichMessage;
|
|
@@ -277,6 +306,39 @@ function truncateTelegramReplyContextText(text: string): string {
|
|
|
277
306
|
return `${text.slice(0, TELEGRAM_REPLY_CONTEXT_MAX_LENGTH).trimEnd()}…`;
|
|
278
307
|
}
|
|
279
308
|
|
|
309
|
+
function formatTelegramUser(user: TelegramMessageUser | undefined): string | undefined {
|
|
310
|
+
if (!user) return undefined;
|
|
311
|
+
if (user.username) return user.username;
|
|
312
|
+
if (typeof user.id === "number") return String(user.id);
|
|
313
|
+
const name = [user.first_name, user.last_name].filter(Boolean).join(" ").trim();
|
|
314
|
+
return name || undefined;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function formatTelegramForwardOriginIdentifier(
|
|
318
|
+
message: TelegramMediaMessage,
|
|
319
|
+
): string | undefined {
|
|
320
|
+
const origin = message.forward_origin;
|
|
321
|
+
const user = origin?.sender_user ?? message.forward_from;
|
|
322
|
+
if (user?.username) return user.username;
|
|
323
|
+
if (typeof user?.id === "number") return String(user.id);
|
|
324
|
+
const chat = origin?.sender_chat ?? origin?.chat;
|
|
325
|
+
if (chat?.username) return chat.username;
|
|
326
|
+
if (typeof chat?.id === "number") return String(chat.id);
|
|
327
|
+
return origin?.sender_user_name ?? message.forward_sender_name;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export function extractTelegramForwardContextText(
|
|
331
|
+
message: TelegramMediaMessage,
|
|
332
|
+
allowedUserId?: number,
|
|
333
|
+
): string {
|
|
334
|
+
const originUser = message.forward_origin?.sender_user ?? message.forward_from;
|
|
335
|
+
const isOwnerOrigin =
|
|
336
|
+
typeof allowedUserId === "number" && originUser?.id === allowedUserId;
|
|
337
|
+
const origin = formatTelegramForwardOriginIdentifier(message);
|
|
338
|
+
if (!origin || isOwnerOrigin) return "";
|
|
339
|
+
return `from: ${origin}`;
|
|
340
|
+
}
|
|
341
|
+
|
|
280
342
|
export function extractTelegramReplyContextText(
|
|
281
343
|
message: TelegramMediaMessage,
|
|
282
344
|
): string {
|
|
@@ -289,13 +351,44 @@ export function extractTelegramReplyContextText(
|
|
|
289
351
|
return quoted ? truncateTelegramReplyContextText(quoted) : "";
|
|
290
352
|
}
|
|
291
353
|
|
|
354
|
+
export function buildTelegramReplyContextBlock(
|
|
355
|
+
message: TelegramMediaMessage,
|
|
356
|
+
replyFiles: Pick<DownloadedTelegramFile, "path">[] = [],
|
|
357
|
+
): string {
|
|
358
|
+
const from = formatTelegramUser(message.reply_to_message?.from);
|
|
359
|
+
const header = from ? `[reply|from:${from}]` : "[reply]";
|
|
360
|
+
const text = extractTelegramReplyContextText(message);
|
|
361
|
+
const dirs = [...new Set(replyFiles.map((file) => dirname(file.path)))];
|
|
362
|
+
const sameDir = dirs.length === 1;
|
|
363
|
+
const attachmentHeader = sameDir
|
|
364
|
+
? `[attachments${from ? `|from:${from}` : ""}] ${dirs[0]}`
|
|
365
|
+
: `[attachments${from ? `|from:${from}` : ""}]`;
|
|
366
|
+
const fileLines = sameDir
|
|
367
|
+
? replyFiles.map((file) => `- /${basename(file.path)}`)
|
|
368
|
+
: replyFiles.map((file) => `- ${file.path}`);
|
|
369
|
+
const replyBlock = text ? `${header} ${text}` : header;
|
|
370
|
+
if (fileLines.length > 0) {
|
|
371
|
+
return `${replyBlock}\n\n${attachmentHeader}\n${fileLines.join("\n")}`;
|
|
372
|
+
}
|
|
373
|
+
if (text) return replyBlock;
|
|
374
|
+
return "";
|
|
375
|
+
}
|
|
376
|
+
|
|
292
377
|
export function appendTelegramReplyContext(
|
|
293
378
|
text: string,
|
|
294
379
|
replyContext: string,
|
|
295
380
|
): string {
|
|
296
381
|
if (!replyContext) return text;
|
|
297
|
-
|
|
298
|
-
|
|
382
|
+
return text ? `${text}\n\n${replyContext}` : `_\n\n${replyContext}`;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function appendTelegramForwardContext(
|
|
386
|
+
text: string,
|
|
387
|
+
forwardContext: string,
|
|
388
|
+
): string {
|
|
389
|
+
if (!forwardContext) return text;
|
|
390
|
+
const forwardBlock = `[forward|${forwardContext.replace(/:\s+/g, ":")}]`;
|
|
391
|
+
return text ? `\n\n${forwardBlock} ${text}` : `\n\n${forwardBlock}`;
|
|
299
392
|
}
|
|
300
393
|
|
|
301
394
|
export function extractTelegramMessagePromptText(
|
|
@@ -303,7 +396,7 @@ export function extractTelegramMessagePromptText(
|
|
|
303
396
|
): string {
|
|
304
397
|
return appendTelegramReplyContext(
|
|
305
398
|
extractTelegramMessageText(message),
|
|
306
|
-
|
|
399
|
+
buildTelegramReplyContextBlock(message),
|
|
307
400
|
);
|
|
308
401
|
}
|
|
309
402
|
|
|
@@ -321,7 +414,7 @@ export function extractTelegramMessagesPromptText(
|
|
|
321
414
|
if (!firstMessage) return text;
|
|
322
415
|
return appendTelegramReplyContext(
|
|
323
416
|
text,
|
|
324
|
-
|
|
417
|
+
buildTelegramReplyContextBlock(firstMessage),
|
|
325
418
|
);
|
|
326
419
|
}
|
|
327
420
|
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -192,7 +192,7 @@ export function createTelegramButtonPromptTurn(options: {
|
|
|
192
192
|
replyToMessageId: options.replyToMessageId,
|
|
193
193
|
sourceMessageIds: [options.replyToMessageId],
|
|
194
194
|
queueOrder: options.queueOrder,
|
|
195
|
-
queueLane: "
|
|
195
|
+
queueLane: "priority",
|
|
196
196
|
laneOrder: options.queueOrder,
|
|
197
197
|
queuedAttachments: [],
|
|
198
198
|
content: [{ type: "text", text: prompt }],
|
package/lib/outbound.ts
CHANGED
|
@@ -6,8 +6,9 @@
|
|
|
6
6
|
|
|
7
7
|
import { randomUUID } from "node:crypto";
|
|
8
8
|
import { mkdir } from "node:fs/promises";
|
|
9
|
-
import {
|
|
10
|
-
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
|
|
11
|
+
import { resolveTelegramTempDir } from "./paths.ts";
|
|
11
12
|
|
|
12
13
|
import {
|
|
13
14
|
planTelegramButtonReply,
|
|
@@ -66,8 +67,7 @@ export function recordTelegramRuntimeEvent(
|
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
export type TelegramOutboundCommandTemplateConfig =
|
|
69
|
-
|
|
|
70
|
-
| CommandTemplateObjectConfig;
|
|
70
|
+
string | CommandTemplateObjectConfig;
|
|
71
71
|
export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
|
|
72
72
|
type?: string;
|
|
73
73
|
match?: string | string[];
|
|
@@ -448,10 +448,7 @@ function getVoiceReplyTemplateValues(
|
|
|
448
448
|
}
|
|
449
449
|
|
|
450
450
|
function getDefaultTelegramVoiceTempDir(): string {
|
|
451
|
-
|
|
452
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
453
|
-
: join(homedir(), ".pi", "agent");
|
|
454
|
-
return join(agentDir, "tmp", "telegram");
|
|
451
|
+
return resolveTelegramTempDir();
|
|
455
452
|
}
|
|
456
453
|
|
|
457
454
|
async function generateTelegramVoiceReplyFileWithHandler(
|
package/lib/paths.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bridge path resolution for Pi-compatible runtimes
|
|
3
|
+
* Zones: telemetry paths, filesystem, runtime identity
|
|
4
|
+
* Owns agent-dir detection and extension-local path derivation
|
|
5
|
+
*
|
|
6
|
+
* This domain is pure/path-only: it resolves directories and file paths
|
|
7
|
+
* from environment and runtime identity. It does not read config, manage
|
|
8
|
+
* state, or import broader Telegram domains.
|
|
9
|
+
*/
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join, resolve } from "node:path";
|
|
12
|
+
|
|
13
|
+
export interface TelegramAgentDirResolutionInput {
|
|
14
|
+
env?: Partial<Pick<NodeJS.ProcessEnv, "PI_CODING_AGENT_DIR">>;
|
|
15
|
+
execPath?: string;
|
|
16
|
+
argv?: readonly string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Resolve the agent data directory for the current Pi-compatible runtime.
|
|
21
|
+
*
|
|
22
|
+
* Precedence:
|
|
23
|
+
* 1. `PI_CODING_AGENT_DIR` env variable, when explicitly set.
|
|
24
|
+
* 2. Detect Pi-compatible runtime identity from the executable or argv[1]
|
|
25
|
+
* (e.g. OMP vs standard Pi agent).
|
|
26
|
+
* 3. Fallback: `~/.pi/agent`.
|
|
27
|
+
*/
|
|
28
|
+
export function resolveAgentDir(
|
|
29
|
+
input: TelegramAgentDirResolutionInput = {},
|
|
30
|
+
): string {
|
|
31
|
+
const env = input.env ?? process.env;
|
|
32
|
+
if (env.PI_CODING_AGENT_DIR) return resolve(env.PI_CODING_AGENT_DIR);
|
|
33
|
+
const execPath = input.execPath ?? process.execPath;
|
|
34
|
+
const argv = input.argv ?? process.argv;
|
|
35
|
+
const execBasename = execPath.toLowerCase().split(/[\\/]/u).pop() ?? "";
|
|
36
|
+
const argv1Last = (argv[1] ?? "").toLowerCase().split(/[\\/]/u).pop() ?? "";
|
|
37
|
+
if (execBasename.startsWith("omp") || argv1Last.startsWith("omp")) {
|
|
38
|
+
return join(homedir(), ".omp", "agent");
|
|
39
|
+
}
|
|
40
|
+
return join(homedir(), ".pi", "agent");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Telegram bridge configuration file (<agentDir>/telegram.json). */
|
|
44
|
+
export function resolveTelegramConfigPath(): string {
|
|
45
|
+
return join(resolveAgentDir(), "telegram.json");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Telegram singleton lock file (<agentDir>/locks.json). */
|
|
49
|
+
export function resolveTelegramLocksPath(): string {
|
|
50
|
+
return join(resolveAgentDir(), "locks.json");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Telegram bridge temporary directory (<agentDir>/tmp/telegram). */
|
|
54
|
+
export function resolveTelegramTempDir(agentDir = resolveAgentDir()): string {
|
|
55
|
+
return join(agentDir, "tmp", "telegram");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function getTelegramProfilePathSuffix(profileName?: string): string {
|
|
59
|
+
return profileName ? `.${profileName.replace(/[^a-zA-Z0-9._-]+/g, "_")}` : "";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function resolveTelegramProfileTempFilePath(
|
|
63
|
+
baseName: string,
|
|
64
|
+
extension: string,
|
|
65
|
+
agentDir = resolveAgentDir(),
|
|
66
|
+
profileName?: string,
|
|
67
|
+
): string {
|
|
68
|
+
return join(
|
|
69
|
+
resolveTelegramTempDir(agentDir),
|
|
70
|
+
`${baseName}${getTelegramProfilePathSuffix(profileName)}.${extension}`,
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Runtime event log (<agentDir>/tmp/telegram/logs.jsonl). */
|
|
75
|
+
export function resolveTelegramRuntimeLogPath(): string {
|
|
76
|
+
return resolveTelegramProfileTempFilePath("logs", "jsonl");
|
|
77
|
+
}
|
package/lib/prompt-templates.ts
CHANGED
|
@@ -116,11 +116,13 @@ export function getTelegramPromptTemplateCommands(
|
|
|
116
116
|
if (!telegramCommand) continue;
|
|
117
117
|
if (reservedNames.has(telegramCommand)) continue;
|
|
118
118
|
if (seen.has(telegramCommand)) continue;
|
|
119
|
+
const sourcePath = command.sourceInfo?.path;
|
|
120
|
+
if (!sourcePath) continue;
|
|
119
121
|
seen.add(telegramCommand);
|
|
120
122
|
promptCommands.push({
|
|
121
123
|
command: telegramCommand,
|
|
122
124
|
description: command.description,
|
|
123
|
-
path:
|
|
125
|
+
path: sourcePath,
|
|
124
126
|
});
|
|
125
127
|
}
|
|
126
128
|
return promptCommands.sort((a, b) => a.command.localeCompare(b.command));
|
package/lib/prompts.ts
CHANGED
|
@@ -15,7 +15,7 @@ Telegram bridge available. Do not use it from local/TUI prompts unless explicitl
|
|
|
15
15
|
|
|
16
16
|
const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
17
17
|
|
|
18
|
-
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help
|
|
18
|
+
Telegram turn note: If context was compacted or you need the pi-telegram bridge contract, call tool \`telegram_help\`; hidden comments are valid only for explicit \`telegram_voice\` or \`telegram_button\` actions with payload.`;
|
|
19
19
|
|
|
20
20
|
const TELEGRAM_HELP_TEXT = `--- TELEGRAM BRIDGE HELP ---
|
|
21
21
|
|
package/lib/queue.ts
CHANGED
|
@@ -273,6 +273,37 @@ export function appendTelegramQueueItem<
|
|
|
273
273
|
return [...items, item];
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
+
function getTelegramPromptTextSignature(item: PendingTelegramTurn): string {
|
|
277
|
+
return item.content
|
|
278
|
+
.filter((entry): entry is TelegramPromptTextContent => entry.type === "text")
|
|
279
|
+
.map((entry) => entry.text)
|
|
280
|
+
.join("\n");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function isDuplicateTelegramPromptTurn(
|
|
284
|
+
left: PendingTelegramTurn,
|
|
285
|
+
right: PendingTelegramTurn,
|
|
286
|
+
): boolean {
|
|
287
|
+
return (
|
|
288
|
+
left.chatId === right.chatId &&
|
|
289
|
+
left.target?.threadId === right.target?.threadId &&
|
|
290
|
+
left.replyToMessageId === right.replyToMessageId &&
|
|
291
|
+
getTelegramPromptTextSignature(left) === getTelegramPromptTextSignature(right)
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
export function appendTelegramPromptTurnOnce<TContext = unknown>(
|
|
296
|
+
items: TelegramQueueItem<TContext>[],
|
|
297
|
+
turn: PendingTelegramTurn,
|
|
298
|
+
): { items: TelegramQueueItem<TContext>[]; appended: boolean } {
|
|
299
|
+
assertTelegramQueueItemAdmissionValid(turn);
|
|
300
|
+
const duplicate = items.some(
|
|
301
|
+
(item) => isPendingTelegramTurn(item) && isDuplicateTelegramPromptTurn(item, turn),
|
|
302
|
+
);
|
|
303
|
+
if (duplicate) return { items, appended: false };
|
|
304
|
+
return { items: [...items, turn], appended: true };
|
|
305
|
+
}
|
|
306
|
+
|
|
276
307
|
export function compareTelegramQueueItems<TContext = unknown>(
|
|
277
308
|
left: TelegramQueueItem<TContext>,
|
|
278
309
|
right: TelegramQueueItem<TContext>,
|
|
@@ -2051,7 +2082,7 @@ export function executeTelegramQueueDispatchPlan<TContext = unknown>(
|
|
|
2051
2082
|
}
|
|
2052
2083
|
deps.onPromptDispatchStart(plan.item.chatId);
|
|
2053
2084
|
try {
|
|
2054
|
-
deps.sendUserMessage(plan.item.content
|
|
2085
|
+
deps.sendUserMessage(plan.item.content);
|
|
2055
2086
|
} catch (error) {
|
|
2056
2087
|
const message = getTelegramQueueErrorMessage(error);
|
|
2057
2088
|
deps.onPromptDispatchFailure(message);
|
package/lib/routing.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFile } from "node:fs/promises";
|
|
8
|
+
import { basename, dirname } from "node:path";
|
|
8
9
|
import * as Commands from "./commands.ts";
|
|
9
10
|
import type { TelegramConfigStore } from "./config.ts";
|
|
10
11
|
import type { TelegramSectionRegistry } from "./sections.ts";
|
|
@@ -20,6 +21,31 @@ import * as TextGroups from "./text-groups.ts";
|
|
|
20
21
|
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
21
22
|
import * as Turns from "./turns.ts";
|
|
22
23
|
|
|
24
|
+
function formatTelegramPromptPeer(user: { id?: unknown; username?: unknown } | undefined): string | undefined {
|
|
25
|
+
if (!user) return undefined;
|
|
26
|
+
if (typeof user.username === "string" && user.username.length > 0) return user.username;
|
|
27
|
+
return typeof user.id === "number" ? String(user.id) : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function appendTelegramSourceAttachmentSection(
|
|
31
|
+
text: string,
|
|
32
|
+
from: string | undefined,
|
|
33
|
+
files: Pick<Media.DownloadedTelegramFile, "path">[],
|
|
34
|
+
): string {
|
|
35
|
+
if (files.length === 0) return text;
|
|
36
|
+
const dirs = [...new Set(files.map((file) => dirname(file.path)))];
|
|
37
|
+
const sameDir = dirs.length === 1;
|
|
38
|
+
const source = from ? `|from:${from}` : "";
|
|
39
|
+
const header = sameDir
|
|
40
|
+
? `[attachments${source}] ${dirs[0]}`
|
|
41
|
+
: `[attachments${source}]`;
|
|
42
|
+
const items = sameDir
|
|
43
|
+
? files.map((file) => `/${basename(file.path)}`)
|
|
44
|
+
: files.map((file) => file.path);
|
|
45
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
46
|
+
return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
function getContextCwd(ctx: unknown): string | undefined {
|
|
24
50
|
if (!ctx || typeof ctx !== "object") return undefined;
|
|
25
51
|
const cwd = (ctx as { cwd?: unknown }).cwd;
|
|
@@ -1248,22 +1274,25 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1248
1274
|
if (typeof chatId !== "number" || typeof messageId !== "number")
|
|
1249
1275
|
return;
|
|
1250
1276
|
const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1277
|
+
const turn = OutboundHandlers.createTelegramButtonPromptTurn({
|
|
1278
|
+
chatId,
|
|
1279
|
+
target:
|
|
1280
|
+
typeof buttonQuery.message?.message_thread_id === "number"
|
|
1281
|
+
? {
|
|
1282
|
+
chatId,
|
|
1283
|
+
threadId: buttonQuery.message.message_thread_id,
|
|
1284
|
+
}
|
|
1285
|
+
: { chatId },
|
|
1286
|
+
replyToMessageId: messageId,
|
|
1287
|
+
queueOrder,
|
|
1288
|
+
action,
|
|
1289
|
+
});
|
|
1290
|
+
const result = Queue.appendTelegramPromptTurnOnce(
|
|
1291
|
+
deps.telegramQueueStore.getQueuedItems(),
|
|
1292
|
+
turn,
|
|
1266
1293
|
);
|
|
1294
|
+
if (!result.appended) return;
|
|
1295
|
+
deps.telegramQueueStore.setQueuedItems(result.items);
|
|
1267
1296
|
deps.updateStatus(context);
|
|
1268
1297
|
requestDispatchNextQueuedTelegramTurn(context);
|
|
1269
1298
|
},
|
|
@@ -1323,15 +1352,39 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1323
1352
|
);
|
|
1324
1353
|
if (handledBySettings) return;
|
|
1325
1354
|
const callbackData = query.data;
|
|
1326
|
-
if (
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1355
|
+
if (callbackData && !isTelegramOwnedCallbackData(callbackData)) {
|
|
1356
|
+
const chatId = query.message?.chat?.id;
|
|
1357
|
+
const messageId = query.message?.message_id;
|
|
1358
|
+
if (typeof chatId === "number" && typeof messageId === "number") {
|
|
1359
|
+
const queueOrder = deps.bridgeRuntime.queue.allocateItemOrder();
|
|
1360
|
+
const target =
|
|
1361
|
+
typeof query.message?.message_thread_id === "number"
|
|
1362
|
+
? { chatId, threadId: query.message.message_thread_id }
|
|
1363
|
+
: { chatId };
|
|
1364
|
+
const turn: Queue.PendingTelegramTurn = {
|
|
1365
|
+
kind: "prompt",
|
|
1366
|
+
chatId,
|
|
1367
|
+
target,
|
|
1368
|
+
replyToMessageId: messageId,
|
|
1369
|
+
sourceMessageIds: [messageId],
|
|
1370
|
+
queueOrder,
|
|
1371
|
+
queueLane: "priority",
|
|
1372
|
+
laneOrder: queueOrder,
|
|
1373
|
+
queuedAttachments: [],
|
|
1374
|
+
content: [{ type: "text", text: `[callback] ${callbackData}` }],
|
|
1375
|
+
historyText: callbackData,
|
|
1376
|
+
statusSummary: callbackData,
|
|
1377
|
+
};
|
|
1378
|
+
const result = Queue.appendTelegramPromptTurnOnce(
|
|
1379
|
+
deps.telegramQueueStore.getQueuedItems(),
|
|
1380
|
+
turn,
|
|
1381
|
+
);
|
|
1382
|
+
if (result.appended) {
|
|
1383
|
+
deps.telegramQueueStore.setQueuedItems(result.items);
|
|
1384
|
+
deps.updateStatus(ctx);
|
|
1385
|
+
requestDispatchNextQueuedTelegramTurn(ctx);
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1335
1388
|
await deps.answerCallbackQuery(query.id);
|
|
1336
1389
|
return;
|
|
1337
1390
|
}
|
|
@@ -1345,6 +1398,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1345
1398
|
downloadFile: deps.downloadFile,
|
|
1346
1399
|
processAttachments: deps.inboundHandlerRuntime.process,
|
|
1347
1400
|
resolveTimeLine: deps.resolveTimeLine,
|
|
1401
|
+
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
1348
1402
|
|
|
1349
1403
|
// Voice policy for the current turn. Missing config still behaves as manual,
|
|
1350
1404
|
// but only explicit telegram.json voice.replyMode is shown in prompt context.
|
|
@@ -1757,30 +1811,37 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1757
1811
|
const text = guestMessage.text ?? "";
|
|
1758
1812
|
const gm = guestMessage as unknown as Record<string, unknown>;
|
|
1759
1813
|
// Build telegram prefix with guest context
|
|
1760
|
-
const fromRaw = gm.from as Record<string, unknown> | undefined;
|
|
1761
|
-
const fromName =
|
|
1762
|
-
(fromRaw?.username as string) || (fromRaw?.first_name as string) || "";
|
|
1763
1814
|
const chatRaw = gm.chat as Record<string, unknown>;
|
|
1764
1815
|
const chatTitle = chatRaw?.title as string | undefined;
|
|
1765
1816
|
const chatType = chatRaw?.type as string;
|
|
1817
|
+
const fromRaw = gm.from as Record<string, unknown> | undefined;
|
|
1818
|
+
const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
|
|
1819
|
+
const replyFromRaw = replyMsg?.from as Record<string, unknown> | undefined;
|
|
1820
|
+
const fromPeer = formatTelegramPromptPeer(fromRaw);
|
|
1821
|
+
const replyPeer = formatTelegramPromptPeer(replyFromRaw);
|
|
1822
|
+
const fromIsOwner = fromRaw?.id === deps.configStore.getAllowedUserId();
|
|
1823
|
+
const guestPeer = chatType === "private" && fromIsOwner && replyPeer
|
|
1824
|
+
? replyPeer
|
|
1825
|
+
: fromPeer;
|
|
1766
1826
|
const prefixParts = ["telegram"];
|
|
1767
|
-
if (fromName) prefixParts.push(`from:${fromName}`);
|
|
1768
1827
|
if (chatType !== "private" && chatTitle) {
|
|
1769
1828
|
prefixParts.push(`guest:${chatTitle}`);
|
|
1829
|
+
} else if (chatType === "private" && guestPeer) {
|
|
1830
|
+
prefixParts.push(`guest:${guestPeer}`);
|
|
1770
1831
|
}
|
|
1771
1832
|
const telegramPrefix = `[${prefixParts.join("|")}]`;
|
|
1772
1833
|
// Extract reply context
|
|
1773
|
-
const replyMsg = gm.reply_to_message as Record<string, unknown> | undefined;
|
|
1774
1834
|
const replyText = replyMsg
|
|
1775
1835
|
? ((replyMsg.text as string) || (replyMsg.caption as string) || "").trim()
|
|
1776
1836
|
: "";
|
|
1777
|
-
const replyFrom = replyMsg
|
|
1778
|
-
? ((replyMsg.from as Record<string, unknown> | undefined)?.username as
|
|
1779
|
-
| string
|
|
1780
|
-
| undefined)
|
|
1781
|
-
: undefined;
|
|
1782
1837
|
// Download files, run inbound handlers
|
|
1783
1838
|
const guestMsg = guestMessage as unknown as Media.TelegramMediaMessage;
|
|
1839
|
+
const replyFiles = guestMsg.reply_to_message
|
|
1840
|
+
? await Media.downloadTelegramMessageFiles(
|
|
1841
|
+
[guestMsg.reply_to_message as Media.TelegramMediaMessage],
|
|
1842
|
+
{ downloadFile: deps.downloadFile },
|
|
1843
|
+
)
|
|
1844
|
+
: [];
|
|
1784
1845
|
const files = await Media.downloadTelegramMessageFiles([guestMsg], {
|
|
1785
1846
|
downloadFile: deps.downloadFile,
|
|
1786
1847
|
});
|
|
@@ -1789,13 +1850,16 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1789
1850
|
text,
|
|
1790
1851
|
ctx,
|
|
1791
1852
|
);
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
if (
|
|
1795
|
-
const
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1853
|
+
const rawText = processed.rawText || text;
|
|
1854
|
+
let sourceContext = "";
|
|
1855
|
+
if (replyMsg) {
|
|
1856
|
+
const replyHeader = replyPeer ? `[reply|from:${replyPeer}]` : "[reply]";
|
|
1857
|
+
const replyBlock = replyText ? `${replyHeader} ${replyText}` : replyHeader;
|
|
1858
|
+
sourceContext = appendTelegramSourceAttachmentSection(
|
|
1859
|
+
replyBlock,
|
|
1860
|
+
replyPeer,
|
|
1861
|
+
replyFiles,
|
|
1862
|
+
);
|
|
1799
1863
|
}
|
|
1800
1864
|
const promptText = Turns.buildTelegramTurnPrompt({
|
|
1801
1865
|
telegramPrefix,
|
|
@@ -1803,6 +1867,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1803
1867
|
files,
|
|
1804
1868
|
promptFiles: processed.promptFiles,
|
|
1805
1869
|
handlerOutputs: processed.handlerOutputs,
|
|
1870
|
+
sourceContext,
|
|
1806
1871
|
});
|
|
1807
1872
|
const order = deps.bridgeRuntime.queue.allocateItemOrder();
|
|
1808
1873
|
const content: Queue.TelegramPromptContent[] = [
|
package/lib/status.ts
CHANGED
|
@@ -180,6 +180,7 @@ export type TelegramBridgeBusLifecyclePhase = "electing";
|
|
|
180
180
|
export interface TelegramBridgeStatusLineState {
|
|
181
181
|
hasBotToken?: boolean;
|
|
182
182
|
botUsername?: string;
|
|
183
|
+
activeProfileName?: string;
|
|
183
184
|
allowedUserId?: number;
|
|
184
185
|
botThreadMode?: "unknown" | "enabled" | "disabled";
|
|
185
186
|
botThreadModeUpdatedAtMs?: number;
|
|
@@ -257,6 +258,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
257
258
|
> {
|
|
258
259
|
statusKey?: string;
|
|
259
260
|
getConfig: () => TelegramBridgeStatusConfig;
|
|
261
|
+
getActiveProfileName?: () => string | undefined;
|
|
260
262
|
isPollingActive: () => boolean;
|
|
261
263
|
getActiveSourceMessageIds: () => number[] | undefined;
|
|
262
264
|
hasActiveTurn: () => boolean;
|
|
@@ -621,6 +623,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
621
623
|
return {
|
|
622
624
|
hasBotToken: Boolean(config.botToken),
|
|
623
625
|
botUsername: config.botUsername,
|
|
626
|
+
activeProfileName: deps.getActiveProfileName?.(),
|
|
624
627
|
allowedUserId: config.allowedUserId,
|
|
625
628
|
botThreadMode: botThreadMode?.threadMode,
|
|
626
629
|
botThreadModeUpdatedAtMs: botThreadMode?.updatedAtMs,
|
|
@@ -770,7 +773,7 @@ export function buildTelegramStatusBarText(
|
|
|
770
773
|
if (state.busLifecyclePhase === "electing")
|
|
771
774
|
return `${label} ${theme.fg("warning", "electing")}${queued}`;
|
|
772
775
|
if (!state.pollingActive && state.busRole !== "follower")
|
|
773
|
-
return `${
|
|
776
|
+
return `${theme.fg("accent", "telegram")} ${theme.fg("muted", "disconnected")}${queued}`;
|
|
774
777
|
if (state.compactionInProgress) {
|
|
775
778
|
return `${label} ${theme.fg("warning", "compacting")}${queued}`;
|
|
776
779
|
}
|
|
@@ -1065,6 +1068,7 @@ function buildTelegramBridgeCompactStatusLines(
|
|
|
1065
1068
|
return [
|
|
1066
1069
|
"connection:",
|
|
1067
1070
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
1071
|
+
...(state.activeProfileName ? [`- profile: ${state.activeProfileName}`] : []),
|
|
1068
1072
|
`- user: ${state.allowedUserId ?? "not paired"}`,
|
|
1069
1073
|
...(state.botThreadMode ? [`- thread mode: ${state.botThreadMode}`] : []),
|
|
1070
1074
|
...(state.busRole ? [`- role: ${state.busRole}`] : []),
|
|
@@ -1119,6 +1123,7 @@ export function buildTelegramBridgeDiagnosticStatusLines(
|
|
|
1119
1123
|
return [
|
|
1120
1124
|
"connection:",
|
|
1121
1125
|
`- bot: ${formatTelegramBridgeBotStatus(state)}`,
|
|
1126
|
+
...(state.activeProfileName ? [`- profile: ${state.activeProfileName}`] : []),
|
|
1122
1127
|
`- allowed user: ${state.allowedUserId ?? "not paired"}`,
|
|
1123
1128
|
...(state.botThreadMode
|
|
1124
1129
|
? [
|
package/lib/sync.ts
CHANGED
|
@@ -24,6 +24,7 @@ export interface TelegramLeaderThreadSyncDeps {
|
|
|
24
24
|
getAllowedUserId: () => number | undefined;
|
|
25
25
|
instanceId: string;
|
|
26
26
|
cwd?: string;
|
|
27
|
+
telegramProfile?: string;
|
|
27
28
|
forceFreshUnnamed?: boolean;
|
|
28
29
|
getNowMs?: () => number;
|
|
29
30
|
getRandom?: () => number;
|
|
@@ -110,7 +111,7 @@ export interface TelegramLeaderHealthRuntime {
|
|
|
110
111
|
export interface TelegramManualThreadDisconnectDeps<TSyncState> {
|
|
111
112
|
instanceId: string;
|
|
112
113
|
getCurrentThreadRecord: () =>
|
|
113
|
-
| { target: TelegramTarget; instanceId?: string }
|
|
114
|
+
| { target: TelegramTarget; instanceId?: string; owner?: { kind?: string } }
|
|
114
115
|
| undefined;
|
|
115
116
|
topicTargetStore: Pick<
|
|
116
117
|
TelegramTopicTargetStore,
|
|
@@ -160,23 +161,26 @@ export function createTelegramManualThreadDisconnectHandler<
|
|
|
160
161
|
return async () => {
|
|
161
162
|
const currentRecord = deps.getCurrentThreadRecord();
|
|
162
163
|
if (currentRecord?.target.threadId) {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
164
|
+
const isManualFollower = currentRecord.owner?.kind === "manual-follower";
|
|
165
|
+
if (!isManualFollower) {
|
|
166
|
+
await ThreadReconciler.applyThreadReconciliationPlan(
|
|
167
|
+
ThreadReconciler.planDisconnectedInstanceThreadCleanup({
|
|
168
|
+
target: currentRecord.target as TelegramTarget & { threadId: number },
|
|
169
|
+
instanceId: deps.instanceId,
|
|
170
|
+
}),
|
|
171
|
+
{
|
|
172
|
+
callApi(method, body) {
|
|
173
|
+
return deps.callApi(method, body);
|
|
174
|
+
},
|
|
175
|
+
persist() {
|
|
176
|
+
return deps.topicTargetStore.persist();
|
|
177
|
+
},
|
|
178
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
174
179
|
},
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
await deps.topicTargetStore.persist();
|
|
180
|
+
);
|
|
181
|
+
if (deps.topicTargetStore.markOfflineByInstanceId(deps.instanceId) > 0) {
|
|
182
|
+
await deps.topicTargetStore.persist();
|
|
183
|
+
}
|
|
180
184
|
}
|
|
181
185
|
const leaderTarget = deps.getLeaderTarget();
|
|
182
186
|
if (
|
|
@@ -360,6 +364,7 @@ export async function ensureTelegramLeaderThreadBinding(
|
|
|
360
364
|
getAllowedUserId: deps.getAllowedUserId,
|
|
361
365
|
instanceId: deps.instanceId,
|
|
362
366
|
cwd: deps.cwd,
|
|
367
|
+
telegramProfile: deps.telegramProfile,
|
|
363
368
|
getCurrentLeaderEpoch: deps.getCurrentLeaderEpoch,
|
|
364
369
|
getThreadReconciliationMachineState:
|
|
365
370
|
deps.getThreadReconciliationMachineState,
|