@llblab/pi-telegram 0.19.3 → 0.20.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/AGENTS.md +6 -3
- package/BACKLOG.md +7 -6
- package/CHANGELOG.md +14 -2
- package/README.md +8 -5
- package/docs/README.md +2 -2
- package/docs/architecture.md +6 -1
- package/docs/command-templates.md +7 -7
- package/docs/inbound.md +11 -11
- package/docs/multi-instance-bus.md +2 -0
- package/docs/outbound.md +5 -5
- package/docs/public-api.md +1 -1
- package/index.ts +95 -26
- package/lib/bindings.ts +61 -11
- package/lib/bus-follower.ts +37 -13
- package/lib/bus-leader.ts +28 -43
- package/lib/bus-transport.ts +28 -4
- package/lib/bus.ts +66 -43
- 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/bus-transport.ts
CHANGED
|
@@ -99,31 +99,55 @@ export function getTelegramBusTransportRetryPolicy(input: {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
function normalizeTelegramBusEndpointScope(value: string): string {
|
|
103
|
+
return value.replace(/[^a-zA-Z0-9_.-]/g, "_").slice(0, 80);
|
|
104
|
+
}
|
|
105
|
+
|
|
102
106
|
export function getTelegramBusLeaderEndpoint(input: {
|
|
103
107
|
agentDir: string;
|
|
104
108
|
platform: NodeJS.Platform | string;
|
|
109
|
+
profileName?: string;
|
|
105
110
|
}): string {
|
|
111
|
+
const profileScope = input.profileName
|
|
112
|
+
? normalizeTelegramBusEndpointScope(input.profileName)
|
|
113
|
+
: undefined;
|
|
106
114
|
return input.platform === "win32"
|
|
107
|
-
? getTelegramBusPipePath({
|
|
108
|
-
|
|
115
|
+
? getTelegramBusPipePath({
|
|
116
|
+
agentDir: input.agentDir,
|
|
117
|
+
scope: profileScope ? `bus-${profileScope}` : "bus",
|
|
118
|
+
})
|
|
119
|
+
: join(
|
|
120
|
+
input.agentDir,
|
|
121
|
+
"tmp",
|
|
122
|
+
"telegram",
|
|
123
|
+
profileScope ? `bus.${profileScope}.sock` : "bus.sock",
|
|
124
|
+
);
|
|
109
125
|
}
|
|
110
126
|
|
|
111
127
|
export function getTelegramBusFollowerEndpoint(input: {
|
|
112
128
|
agentDir: string;
|
|
113
129
|
platform: NodeJS.Platform | string;
|
|
114
130
|
instanceId: string;
|
|
131
|
+
profileName?: string;
|
|
115
132
|
}): string {
|
|
133
|
+
const instanceScope = normalizeTelegramBusEndpointScope(input.instanceId);
|
|
134
|
+
const profileScope = input.profileName
|
|
135
|
+
? normalizeTelegramBusEndpointScope(input.profileName)
|
|
136
|
+
: undefined;
|
|
116
137
|
return input.platform === "win32"
|
|
117
138
|
? getTelegramBusPipePath({
|
|
118
139
|
agentDir: input.agentDir,
|
|
119
|
-
scope:
|
|
140
|
+
scope: profileScope
|
|
141
|
+
? `follower-${profileScope}-${instanceScope}`
|
|
142
|
+
: `follower-${instanceScope}`,
|
|
120
143
|
})
|
|
121
144
|
: join(
|
|
122
145
|
input.agentDir,
|
|
123
146
|
"tmp",
|
|
124
147
|
"telegram",
|
|
125
148
|
"followers",
|
|
126
|
-
|
|
149
|
+
...(profileScope ? [profileScope] : []),
|
|
150
|
+
`${instanceScope}.sock`,
|
|
127
151
|
);
|
|
128
152
|
}
|
|
129
153
|
|
package/lib/bus.ts
CHANGED
|
@@ -13,8 +13,8 @@ import {
|
|
|
13
13
|
type Server,
|
|
14
14
|
type Socket,
|
|
15
15
|
} from "node:net";
|
|
16
|
-
import {
|
|
17
|
-
import { dirname
|
|
16
|
+
import { platform as getPlatform } from "node:os";
|
|
17
|
+
import { dirname } from "node:path";
|
|
18
18
|
|
|
19
19
|
import {
|
|
20
20
|
classifyTelegramBusTransportError,
|
|
@@ -30,32 +30,34 @@ import {
|
|
|
30
30
|
type TelegramBusTransportRetryPolicy,
|
|
31
31
|
} from "./bus-transport.ts";
|
|
32
32
|
import type { TelegramTarget } from "./target.ts";
|
|
33
|
+
import { resolveAgentDir } from "./paths.ts";
|
|
33
34
|
|
|
34
35
|
export type TelegramBusRole = "leader" | "follower";
|
|
35
36
|
|
|
36
|
-
function getAgentDir(): string {
|
|
37
|
-
return process.env.PI_CODING_AGENT_DIR
|
|
38
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
39
|
-
: join(homedir(), ".pi", "agent");
|
|
40
|
-
}
|
|
41
|
-
|
|
42
37
|
export function createTelegramBusAuthSecret(): string {
|
|
43
38
|
return randomBytes(32).toString("base64url");
|
|
44
39
|
}
|
|
45
40
|
|
|
46
41
|
export function getTelegramBusSocketPath(
|
|
47
|
-
agentDir =
|
|
42
|
+
agentDir = resolveAgentDir(),
|
|
48
43
|
platform = getPlatform(),
|
|
44
|
+
profileName?: string,
|
|
49
45
|
): string {
|
|
50
|
-
return getTelegramBusLeaderEndpoint({ agentDir, platform });
|
|
46
|
+
return getTelegramBusLeaderEndpoint({ agentDir, platform, profileName });
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
export function getTelegramBusFollowerSocketPath(
|
|
54
50
|
instanceId: string,
|
|
55
|
-
agentDir =
|
|
51
|
+
agentDir = resolveAgentDir(),
|
|
56
52
|
platform = getPlatform(),
|
|
53
|
+
profileName?: string,
|
|
57
54
|
): string {
|
|
58
|
-
return getTelegramBusFollowerEndpoint({
|
|
55
|
+
return getTelegramBusFollowerEndpoint({
|
|
56
|
+
agentDir,
|
|
57
|
+
platform,
|
|
58
|
+
instanceId,
|
|
59
|
+
profileName,
|
|
60
|
+
});
|
|
59
61
|
}
|
|
60
62
|
|
|
61
63
|
export interface TelegramBusInstanceRegistration {
|
|
@@ -155,7 +157,9 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
155
157
|
const messageId = (body as Record<string, unknown>).message_id;
|
|
156
158
|
const parsedMessageId =
|
|
157
159
|
typeof messageId === "number" ? messageId : Number(messageId);
|
|
158
|
-
return
|
|
160
|
+
return (
|
|
161
|
+
Number.isInteger(parsedMessageId) && matchesId(messageId, parsedMessageId)
|
|
162
|
+
);
|
|
159
163
|
};
|
|
160
164
|
const isBotCommandRegistration = (body: unknown): boolean => {
|
|
161
165
|
if (!body || typeof body !== "object" || Array.isArray(body)) return false;
|
|
@@ -185,7 +189,8 @@ export function isTelegramFollowerApiCallAllowed(input: {
|
|
|
185
189
|
if (apiMethod === "getMe") return true;
|
|
186
190
|
if (apiMethod === "setMyCommands")
|
|
187
191
|
return isBotCommandRegistration(input.args[1]);
|
|
188
|
-
if (apiMethod === "sendChatAction")
|
|
192
|
+
if (apiMethod === "sendChatAction")
|
|
193
|
+
return isTargetChatScoped(input.args[1]);
|
|
189
194
|
if (apiMethod === "deleteMessage" || apiMethod === "editMessageText") {
|
|
190
195
|
return isTargetMessageScoped(input.args[1]);
|
|
191
196
|
}
|
|
@@ -346,14 +351,20 @@ export interface TelegramBusLocalServer {
|
|
|
346
351
|
stop: () => Promise<void>;
|
|
347
352
|
}
|
|
348
353
|
|
|
354
|
+
export type TelegramBusSocketPathSource = string | (() => string);
|
|
355
|
+
|
|
356
|
+
export function resolveTelegramBusSocketPath(
|
|
357
|
+
source: TelegramBusSocketPathSource,
|
|
358
|
+
): string {
|
|
359
|
+
return typeof source === "function" ? source() : source;
|
|
360
|
+
}
|
|
361
|
+
|
|
349
362
|
export interface TelegramBusLocalServerDeps {
|
|
350
|
-
socketPath:
|
|
363
|
+
socketPath: TelegramBusSocketPathSource;
|
|
351
364
|
handleEnvelope: (
|
|
352
365
|
envelope: TelegramBusEnvelope,
|
|
353
366
|
) =>
|
|
354
|
-
|
|
355
|
-
| TelegramBusEnvelope
|
|
356
|
-
| undefined;
|
|
367
|
+
Promise<TelegramBusEnvelope | undefined> | TelegramBusEnvelope | undefined;
|
|
357
368
|
recordTransportEvent?: TelegramBusTransportEventRecorder;
|
|
358
369
|
}
|
|
359
370
|
|
|
@@ -366,7 +377,7 @@ export interface TelegramBusLocalClientOptions {
|
|
|
366
377
|
}
|
|
367
378
|
|
|
368
379
|
export interface TelegramBusForeignOwnedForwarderDeps {
|
|
369
|
-
socketPath:
|
|
380
|
+
socketPath: TelegramBusSocketPathSource;
|
|
370
381
|
createRequestId: () => string;
|
|
371
382
|
getNowMs?: () => number;
|
|
372
383
|
timeoutMs?: number;
|
|
@@ -405,12 +416,13 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
405
416
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
406
417
|
const send = async (envelope: TelegramBusEnvelope): Promise<boolean> => {
|
|
407
418
|
if (deps.getAuthSecret) envelope.auth = deps.getAuthSecret();
|
|
419
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
408
420
|
const response = await sendTelegramBusLocalEnvelope({
|
|
409
|
-
socketPath
|
|
421
|
+
socketPath,
|
|
410
422
|
envelope,
|
|
411
423
|
timeoutMs: deps.timeoutMs,
|
|
412
424
|
retry: getTelegramBusTransportRetryPolicy({
|
|
413
|
-
endpoint:
|
|
425
|
+
endpoint: socketPath,
|
|
414
426
|
operation: "operation",
|
|
415
427
|
}),
|
|
416
428
|
});
|
|
@@ -454,7 +466,9 @@ export function createTelegramBusForeignOwnedUpdateForwarder<
|
|
|
454
466
|
|
|
455
467
|
export interface TelegramBusFollowerThreadRestoreHandlerDeps {
|
|
456
468
|
followerRegistry: Pick<TelegramBusFollowerRegistry, "get" | "register">;
|
|
457
|
-
followerTargetController: ReturnType<
|
|
469
|
+
followerTargetController: ReturnType<
|
|
470
|
+
typeof createTelegramBusFollowerTargetController
|
|
471
|
+
>;
|
|
458
472
|
onRestored?: () => void;
|
|
459
473
|
}
|
|
460
474
|
|
|
@@ -517,11 +531,7 @@ export function createTelegramBusFollowerThreadRestoreHandler(
|
|
|
517
531
|
target: TelegramTarget & { threadId: number };
|
|
518
532
|
oldTarget?: TelegramTarget & { threadId: number };
|
|
519
533
|
}) => Promise<boolean> {
|
|
520
|
-
return async ({
|
|
521
|
-
record,
|
|
522
|
-
target,
|
|
523
|
-
oldTarget,
|
|
524
|
-
}) => {
|
|
534
|
+
return async ({ record, target, oldTarget }) => {
|
|
525
535
|
if (!record.instanceId) return false;
|
|
526
536
|
const follower = deps.followerRegistry.get(record.instanceId);
|
|
527
537
|
if (!follower) return false;
|
|
@@ -564,6 +574,7 @@ export function createTelegramBusLocalServer(
|
|
|
564
574
|
deps: TelegramBusLocalServerDeps,
|
|
565
575
|
): TelegramBusLocalServer {
|
|
566
576
|
let server: Server | undefined;
|
|
577
|
+
let activeSocketPath: string | undefined;
|
|
567
578
|
const sockets = new Set<Socket>();
|
|
568
579
|
const closeSocket = (socket: Socket) => {
|
|
569
580
|
sockets.delete(socket);
|
|
@@ -572,16 +583,18 @@ export function createTelegramBusLocalServer(
|
|
|
572
583
|
return {
|
|
573
584
|
start: async () => {
|
|
574
585
|
if (server) return;
|
|
575
|
-
const
|
|
586
|
+
const socketPath = resolveTelegramBusSocketPath(deps.socketPath);
|
|
587
|
+
activeSocketPath = socketPath;
|
|
588
|
+
const usesWindowsPipe = isTelegramBusPipePath(socketPath);
|
|
576
589
|
deps.recordTransportEvent?.(
|
|
577
590
|
"server-start",
|
|
578
|
-
getTelegramBusEndpointDiagnostics(
|
|
591
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
579
592
|
);
|
|
580
593
|
if (!usesWindowsPipe) {
|
|
581
|
-
const socketDir = dirname(
|
|
594
|
+
const socketDir = dirname(socketPath);
|
|
582
595
|
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
583
596
|
chmodSync(socketDir, 0o700);
|
|
584
|
-
if (existsSync(
|
|
597
|
+
if (existsSync(socketPath)) unlinkSync(socketPath);
|
|
585
598
|
}
|
|
586
599
|
server = createServer((socket) => {
|
|
587
600
|
sockets.add(socket);
|
|
@@ -597,14 +610,14 @@ export function createTelegramBusLocalServer(
|
|
|
597
610
|
socket,
|
|
598
611
|
deps.handleEnvelope,
|
|
599
612
|
deps.recordTransportEvent,
|
|
600
|
-
|
|
613
|
+
socketPath,
|
|
601
614
|
);
|
|
602
615
|
}
|
|
603
616
|
});
|
|
604
617
|
socket.on("close", () => sockets.delete(socket));
|
|
605
618
|
socket.on("error", (error) => {
|
|
606
619
|
deps.recordTransportEvent?.("server-socket-error", {
|
|
607
|
-
...getTelegramBusEndpointDiagnostics(
|
|
620
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
608
621
|
...classifyTelegramBusTransportError(error),
|
|
609
622
|
});
|
|
610
623
|
closeSocket(socket);
|
|
@@ -613,37 +626,47 @@ export function createTelegramBusLocalServer(
|
|
|
613
626
|
try {
|
|
614
627
|
await new Promise<void>((resolve, reject) => {
|
|
615
628
|
server?.once("error", reject);
|
|
616
|
-
server?.listen(
|
|
629
|
+
server?.listen(socketPath, resolve);
|
|
617
630
|
});
|
|
618
631
|
deps.recordTransportEvent?.(
|
|
619
632
|
"server-started",
|
|
620
|
-
getTelegramBusEndpointDiagnostics(
|
|
633
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
621
634
|
);
|
|
622
635
|
} catch (error) {
|
|
636
|
+
server = undefined;
|
|
637
|
+
activeSocketPath = undefined;
|
|
623
638
|
deps.recordTransportEvent?.("server-start-failed", {
|
|
624
|
-
...getTelegramBusEndpointDiagnostics(
|
|
639
|
+
...getTelegramBusEndpointDiagnostics(socketPath),
|
|
625
640
|
...classifyTelegramBusTransportError(error),
|
|
626
641
|
});
|
|
627
642
|
throw error;
|
|
628
643
|
}
|
|
629
|
-
if (!usesWindowsPipe) chmodSync(
|
|
644
|
+
if (!usesWindowsPipe) chmodSync(socketPath, 0o600);
|
|
630
645
|
},
|
|
631
646
|
stop: async () => {
|
|
632
647
|
const activeServer = server;
|
|
648
|
+
const socketPath = activeSocketPath;
|
|
633
649
|
server = undefined;
|
|
650
|
+
activeSocketPath = undefined;
|
|
634
651
|
for (const socket of sockets) closeSocket(socket);
|
|
635
652
|
if (activeServer) {
|
|
636
653
|
await new Promise<void>((resolve) =>
|
|
637
654
|
activeServer.close(() => resolve()),
|
|
638
655
|
);
|
|
639
656
|
}
|
|
640
|
-
if (
|
|
641
|
-
|
|
657
|
+
if (
|
|
658
|
+
socketPath &&
|
|
659
|
+
!isTelegramBusPipePath(socketPath) &&
|
|
660
|
+
existsSync(socketPath)
|
|
661
|
+
) {
|
|
662
|
+
unlinkSync(socketPath);
|
|
663
|
+
}
|
|
664
|
+
if (socketPath) {
|
|
665
|
+
deps.recordTransportEvent?.(
|
|
666
|
+
"server-stopped",
|
|
667
|
+
getTelegramBusEndpointDiagnostics(socketPath),
|
|
668
|
+
);
|
|
642
669
|
}
|
|
643
|
-
deps.recordTransportEvent?.(
|
|
644
|
-
"server-stopped",
|
|
645
|
-
getTelegramBusEndpointDiagnostics(deps.socketPath),
|
|
646
|
-
);
|
|
647
670
|
},
|
|
648
671
|
};
|
|
649
672
|
}
|
package/lib/commands.ts
CHANGED
|
@@ -295,7 +295,7 @@ export interface TelegramBridgeCommandStartPollingResult {
|
|
|
295
295
|
}
|
|
296
296
|
|
|
297
297
|
export interface TelegramBridgeCommandRegistrationDeps {
|
|
298
|
-
promptForConfig: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
298
|
+
promptForConfig: (ctx: ExtensionCommandContext, profileName?: string) => Promise<void>;
|
|
299
299
|
getStatusLines: (options?: TelegramBridgeStatusLineOptions) => string[];
|
|
300
300
|
reloadConfig: () => Promise<void>;
|
|
301
301
|
hasBotToken: () => boolean;
|
|
@@ -308,6 +308,19 @@ export interface TelegramBridgeCommandRegistrationDeps {
|
|
|
308
308
|
| TelegramBridgeCommandStartPollingResult;
|
|
309
309
|
stopPolling: () => Promise<void | string>;
|
|
310
310
|
updateStatus: (ctx: ExtensionCommandContext) => void;
|
|
311
|
+
getProfileNames?: () => string[];
|
|
312
|
+
activateDefaultProfileConfig?: (ctx: ExtensionCommandContext) => Promise<void>;
|
|
313
|
+
activateProfileConfig?: (
|
|
314
|
+
ctx: ExtensionCommandContext,
|
|
315
|
+
profileName: string,
|
|
316
|
+
) => Promise<boolean>;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function parseTelegramProfileArg(args: string): string | undefined {
|
|
320
|
+
const word = args.trim().split(/\s+/)[0];
|
|
321
|
+
if (!word || word.length === 0) return undefined;
|
|
322
|
+
if (word.startsWith("-")) return undefined;
|
|
323
|
+
return word;
|
|
311
324
|
}
|
|
312
325
|
|
|
313
326
|
function formatTelegramTakeoverTitle(ctx: ExtensionCommandContext): string {
|
|
@@ -331,9 +344,9 @@ export function registerTelegramBridgeCommands(
|
|
|
331
344
|
deps: TelegramBridgeCommandRegistrationDeps,
|
|
332
345
|
): void {
|
|
333
346
|
pi.registerCommand("telegram-setup", {
|
|
334
|
-
description: "Configure Telegram bot token",
|
|
335
|
-
handler: async (
|
|
336
|
-
await deps.promptForConfig(ctx);
|
|
347
|
+
description: "Configure Telegram bot token. Use /telegram-setup <name> for named profiles.",
|
|
348
|
+
handler: async (args, ctx) => {
|
|
349
|
+
await deps.promptForConfig(ctx, parseTelegramProfileArg(args));
|
|
337
350
|
},
|
|
338
351
|
});
|
|
339
352
|
pi.registerCommand("telegram-status", {
|
|
@@ -346,11 +359,31 @@ export function registerTelegramBridgeCommands(
|
|
|
346
359
|
},
|
|
347
360
|
});
|
|
348
361
|
pi.registerCommand("telegram-connect", {
|
|
349
|
-
description: "Start the Telegram bridge
|
|
350
|
-
handler: async (
|
|
351
|
-
|
|
362
|
+
description: "Start the Telegram bridge. Use /telegram-connect <name> for named profiles.",
|
|
363
|
+
handler: async (args, ctx) => {
|
|
364
|
+
const profileName = parseTelegramProfileArg(args);
|
|
365
|
+
if (profileName && deps.activateProfileConfig) {
|
|
366
|
+
const ok = await deps.activateProfileConfig(ctx, profileName);
|
|
367
|
+
if (!ok) {
|
|
368
|
+
ctx.ui.notify(`Profile "${profileName}" not found.`, "error");
|
|
369
|
+
deps.updateStatus(ctx);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
ctx.ui.notify(`Activated profile "${profileName}".`, "info");
|
|
373
|
+
} else {
|
|
374
|
+
await (deps.activateDefaultProfileConfig?.(ctx) ?? deps.reloadConfig());
|
|
375
|
+
}
|
|
352
376
|
if (!deps.hasBotToken()) {
|
|
353
|
-
|
|
377
|
+
const profileNames = deps.getProfileNames?.() ?? [];
|
|
378
|
+
if (!profileName && profileNames.length > 0) {
|
|
379
|
+
ctx.ui.notify(
|
|
380
|
+
`No default Telegram profile configured. Available profiles: ${profileNames.join(", ")}. Use /telegram-connect <profileName> or /telegram-setup to create a default profile.`,
|
|
381
|
+
"info",
|
|
382
|
+
);
|
|
383
|
+
deps.updateStatus(ctx);
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
await deps.promptForConfig(ctx, profileName);
|
|
354
387
|
return;
|
|
355
388
|
}
|
|
356
389
|
let result = await deps.startPolling(ctx, {
|
package/lib/config.ts
CHANGED
|
@@ -6,27 +6,19 @@
|
|
|
6
6
|
|
|
7
7
|
import { existsSync } from "node:fs";
|
|
8
8
|
import { chmod, mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
9
|
-
import {
|
|
10
|
-
import { join, resolve } from "node:path";
|
|
9
|
+
import { resolveAgentDir, resolveTelegramConfigPath } from "./paths.ts";
|
|
11
10
|
|
|
12
11
|
import type { TelegramInboundHandlerConfig } from "./inbound.ts";
|
|
13
12
|
import type { CommandTemplateObjectConfig } from "./command-templates.ts";
|
|
14
13
|
|
|
15
14
|
const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
|
|
16
15
|
|
|
17
|
-
function getAgentDir(): string {
|
|
18
|
-
return process.env.PI_CODING_AGENT_DIR
|
|
19
|
-
? resolve(process.env.PI_CODING_AGENT_DIR)
|
|
20
|
-
: join(homedir(), ".pi", "agent");
|
|
21
|
-
}
|
|
22
|
-
|
|
23
16
|
function getConfigPath(): string {
|
|
24
|
-
return
|
|
17
|
+
return resolveTelegramConfigPath();
|
|
25
18
|
}
|
|
26
19
|
|
|
27
20
|
export type TelegramOutboundCommandTemplateConfig =
|
|
28
|
-
|
|
|
29
|
-
| CommandTemplateObjectConfig;
|
|
21
|
+
string | CommandTemplateObjectConfig;
|
|
30
22
|
export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConfig {
|
|
31
23
|
type?: string;
|
|
32
24
|
match?: string | string[];
|
|
@@ -75,12 +67,88 @@ export interface TelegramConfig {
|
|
|
75
67
|
sendTranscript?: boolean;
|
|
76
68
|
};
|
|
77
69
|
time?: TelegramTimeConfig;
|
|
70
|
+
/** Named bot/session profiles (e.g. "work", "omp"). */
|
|
71
|
+
profiles?: Record<string, TelegramBotProfile>;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Per-profile bot/session identity fields.
|
|
76
|
+
* Stored under `profiles.<name>` in telegram.json.
|
|
77
|
+
* Shared bridge settings (inboundHandlers, outboundHandlers, voice, time,
|
|
78
|
+
* assistant, proactivePush) stay at the top level.
|
|
79
|
+
*/
|
|
80
|
+
export interface TelegramBotProfile {
|
|
81
|
+
botToken: string;
|
|
82
|
+
botUsername?: string;
|
|
83
|
+
botId?: number;
|
|
84
|
+
allowedUserId?: number;
|
|
85
|
+
lastUpdateId?: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Profile names must be lowercase letters, digits, hyphens, underscores; max 32 chars. */
|
|
89
|
+
const TELEGRAM_PROFILE_NAME_PATTERN = /^[a-z0-9][a-z0-9_-]{0,31}$/;
|
|
90
|
+
const TELEGRAM_RESERVED_PROFILE_NAMES: ReadonlySet<string> = new Set([
|
|
91
|
+
"default",
|
|
92
|
+
"main",
|
|
93
|
+
"active",
|
|
94
|
+
]);
|
|
95
|
+
|
|
96
|
+
export function isValidTelegramProfileName(name: string): boolean {
|
|
97
|
+
return (
|
|
98
|
+
TELEGRAM_PROFILE_NAME_PATTERN.test(name) &&
|
|
99
|
+
!TELEGRAM_RESERVED_PROFILE_NAMES.has(name)
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve the effective config for a named (or default) profile.
|
|
105
|
+
* Returns bot/session fields from the named profile, falling back to
|
|
106
|
+
* top-level fields for the default profile. Shared bridge settings
|
|
107
|
+
* always come from the top level.
|
|
108
|
+
*/
|
|
109
|
+
export function resolveTelegramActiveProfile(
|
|
110
|
+
config: TelegramConfig,
|
|
111
|
+
profileName?: string,
|
|
112
|
+
): {
|
|
113
|
+
botToken?: string;
|
|
114
|
+
botUsername?: string;
|
|
115
|
+
botId?: number;
|
|
116
|
+
allowedUserId?: number;
|
|
117
|
+
lastUpdateId?: number;
|
|
118
|
+
} {
|
|
119
|
+
if (!profileName || !config.profiles?.[profileName]) {
|
|
120
|
+
return {
|
|
121
|
+
botToken: config.botToken,
|
|
122
|
+
botUsername: config.botUsername,
|
|
123
|
+
botId: config.botId,
|
|
124
|
+
allowedUserId: config.allowedUserId,
|
|
125
|
+
lastUpdateId: config.lastUpdateId,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
const profile = config.profiles[profileName];
|
|
129
|
+
return {
|
|
130
|
+
botToken: profile.botToken,
|
|
131
|
+
botUsername: profile.botUsername,
|
|
132
|
+
botId: profile.botId,
|
|
133
|
+
allowedUserId: profile.allowedUserId,
|
|
134
|
+
lastUpdateId: profile.lastUpdateId,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** List defined profile names. */
|
|
139
|
+
export function getTelegramProfileNames(
|
|
140
|
+
config: TelegramConfig,
|
|
141
|
+
): string[] {
|
|
142
|
+
return Object.keys(config.profiles ?? {}).sort();
|
|
78
143
|
}
|
|
79
144
|
|
|
80
145
|
export interface TelegramConfigStore {
|
|
81
146
|
get: () => TelegramConfig;
|
|
147
|
+
getStoredConfig: () => TelegramConfig;
|
|
82
148
|
set: (config: TelegramConfig) => void;
|
|
83
149
|
update: (mutate: (config: TelegramConfig) => void) => void;
|
|
150
|
+
activateProfile: (profileName: string | undefined) => boolean;
|
|
151
|
+
getActiveProfileName: () => string | undefined;
|
|
84
152
|
getBotToken: () => string | undefined;
|
|
85
153
|
hasBotToken: () => boolean;
|
|
86
154
|
getAllowedUserId: () => number | undefined;
|
|
@@ -211,23 +279,85 @@ export async function writeTelegramConfig(
|
|
|
211
279
|
await chmod(configPath, 0o600);
|
|
212
280
|
}
|
|
213
281
|
|
|
282
|
+
function getTelegramProfileFields(config: TelegramConfig): TelegramBotProfile | undefined {
|
|
283
|
+
const token = config.botToken?.trim();
|
|
284
|
+
if (!token) return undefined;
|
|
285
|
+
return {
|
|
286
|
+
botToken: token,
|
|
287
|
+
botUsername: config.botUsername,
|
|
288
|
+
botId: config.botId,
|
|
289
|
+
allowedUserId: config.allowedUserId,
|
|
290
|
+
lastUpdateId: config.lastUpdateId,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function applyTelegramProfile(
|
|
295
|
+
config: TelegramConfig,
|
|
296
|
+
profileName: string | undefined,
|
|
297
|
+
): TelegramConfig {
|
|
298
|
+
if (!profileName) return config;
|
|
299
|
+
const profile = config.profiles?.[profileName];
|
|
300
|
+
if (!profile) return config;
|
|
301
|
+
return {
|
|
302
|
+
...config,
|
|
303
|
+
botToken: profile.botToken,
|
|
304
|
+
botUsername: profile.botUsername,
|
|
305
|
+
botId: profile.botId,
|
|
306
|
+
allowedUserId: profile.allowedUserId,
|
|
307
|
+
lastUpdateId: profile.lastUpdateId,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function storeTelegramEffectiveConfig(
|
|
312
|
+
baseConfig: TelegramConfig,
|
|
313
|
+
nextConfig: TelegramConfig,
|
|
314
|
+
profileName: string | undefined,
|
|
315
|
+
): TelegramConfig {
|
|
316
|
+
if (!profileName) return nextConfig;
|
|
317
|
+
const profile = getTelegramProfileFields(nextConfig);
|
|
318
|
+
const profiles = { ...(baseConfig.profiles ?? {}) };
|
|
319
|
+
if (profile) profiles[profileName] = profile;
|
|
320
|
+
else delete profiles[profileName];
|
|
321
|
+
return {
|
|
322
|
+
...nextConfig,
|
|
323
|
+
botToken: baseConfig.botToken,
|
|
324
|
+
botUsername: baseConfig.botUsername,
|
|
325
|
+
botId: baseConfig.botId,
|
|
326
|
+
allowedUserId: baseConfig.allowedUserId,
|
|
327
|
+
lastUpdateId: baseConfig.lastUpdateId,
|
|
328
|
+
profiles: Object.keys(profiles).length > 0 ? profiles : undefined,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
214
332
|
export function createTelegramConfigStore(
|
|
215
333
|
options: TelegramConfigStoreOptions = {},
|
|
216
334
|
): TelegramConfigStore {
|
|
217
335
|
let config: TelegramConfig = options.initialConfig ?? {};
|
|
218
|
-
|
|
336
|
+
let activeProfileName: string | undefined;
|
|
337
|
+
const agentDir = options.agentDir ?? resolveAgentDir();
|
|
219
338
|
const configPath = options.configPath ?? getConfigPath();
|
|
339
|
+
const getEffectiveConfig = () => applyTelegramProfile(config, activeProfileName);
|
|
340
|
+
const setEffectiveConfig = (nextConfig: TelegramConfig) => {
|
|
341
|
+
config = storeTelegramEffectiveConfig(config, nextConfig, activeProfileName);
|
|
342
|
+
};
|
|
220
343
|
return {
|
|
221
|
-
get:
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
},
|
|
344
|
+
get: getEffectiveConfig,
|
|
345
|
+
getStoredConfig: () => config,
|
|
346
|
+
set: setEffectiveConfig,
|
|
225
347
|
update: (mutate) => {
|
|
226
|
-
|
|
348
|
+
const nextConfig = getEffectiveConfig();
|
|
349
|
+
mutate(nextConfig);
|
|
350
|
+
setEffectiveConfig(nextConfig);
|
|
351
|
+
},
|
|
352
|
+
activateProfile: (profileName) => {
|
|
353
|
+
if (profileName && !config.profiles?.[profileName]) return false;
|
|
354
|
+
activeProfileName = profileName;
|
|
355
|
+
return true;
|
|
227
356
|
},
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
357
|
+
getActiveProfileName: () => activeProfileName,
|
|
358
|
+
getBotToken: () => getEffectiveConfig().botToken,
|
|
359
|
+
hasBotToken: () => !!getEffectiveConfig().botToken,
|
|
360
|
+
getAllowedUserId: () => getEffectiveConfig().allowedUserId,
|
|
231
361
|
getInboundHandlers: () => [
|
|
232
362
|
...(config.inboundHandlers ?? []),
|
|
233
363
|
...(config.attachmentHandlers ?? []),
|
|
@@ -235,7 +365,9 @@ export function createTelegramConfigStore(
|
|
|
235
365
|
getAttachmentHandlers: () => config.attachmentHandlers,
|
|
236
366
|
getOutboundHandlers: () => config.outboundHandlers,
|
|
237
367
|
setAllowedUserId: (userId) => {
|
|
238
|
-
|
|
368
|
+
const nextConfig = getEffectiveConfig();
|
|
369
|
+
nextConfig.allowedUserId = userId;
|
|
370
|
+
setEffectiveConfig(nextConfig);
|
|
239
371
|
},
|
|
240
372
|
load: async () => {
|
|
241
373
|
config = await readTelegramConfig(configPath, {
|
|
@@ -247,9 +379,18 @@ export function createTelegramConfigStore(
|
|
|
247
379
|
});
|
|
248
380
|
},
|
|
249
381
|
});
|
|
382
|
+
if (activeProfileName && !config.profiles?.[activeProfileName]) {
|
|
383
|
+
activeProfileName = undefined;
|
|
384
|
+
}
|
|
250
385
|
},
|
|
251
|
-
persist: async (nextConfig =
|
|
252
|
-
|
|
386
|
+
persist: async (nextConfig = getEffectiveConfig()) => {
|
|
387
|
+
const storedConfig = storeTelegramEffectiveConfig(
|
|
388
|
+
config,
|
|
389
|
+
nextConfig,
|
|
390
|
+
activeProfileName,
|
|
391
|
+
);
|
|
392
|
+
config = storedConfig;
|
|
393
|
+
await writeTelegramConfig(agentDir, configPath, storedConfig);
|
|
253
394
|
},
|
|
254
395
|
};
|
|
255
396
|
}
|
|
@@ -481,9 +622,7 @@ export function createTelegramConfigControls(
|
|
|
481
622
|
}
|
|
482
623
|
|
|
483
624
|
export type TelegramAuthorizationState =
|
|
484
|
-
|
|
485
|
-
| { kind: "allow" }
|
|
486
|
-
| { kind: "deny" };
|
|
625
|
+
{ kind: "pair"; userId: number } | { kind: "allow" } | { kind: "deny" };
|
|
487
626
|
|
|
488
627
|
export interface TelegramUserPairingDeps<TContext> {
|
|
489
628
|
allowedUserId?: number;
|