@llblab/pi-telegram 0.3.0 → 0.4.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/README.md +27 -5
- package/docs/README.md +2 -3
- package/docs/architecture.md +15 -7
- package/docs/locks.md +136 -0
- package/index.ts +76 -140
- package/lib/config.ts +17 -5
- package/lib/handlers.ts +474 -0
- package/lib/locks.ts +306 -0
- package/lib/media.ts +50 -6
- package/lib/pi.ts +11 -1
- package/lib/queue.ts +3 -0
- package/lib/registration.ts +89 -5
- package/lib/routing.ts +217 -0
- package/lib/setup.ts +17 -3
- package/lib/status.ts +4 -0
- package/lib/turns.ts +98 -21
- package/package.json +1 -1
package/lib/routing.ts
ADDED
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram inbound routing composition
|
|
3
|
+
* Wires authorized updates into menus, commands, media grouping, and prompt queueing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as Commands from "./commands.ts";
|
|
7
|
+
import type { TelegramConfigStore } from "./config.ts";
|
|
8
|
+
import type { TelegramAttachmentHandlerRuntime } from "./handlers.ts";
|
|
9
|
+
import * as Media from "./media.ts";
|
|
10
|
+
import * as Menu from "./menu.ts";
|
|
11
|
+
import * as Model from "./model.ts";
|
|
12
|
+
import * as Queue from "./queue.ts";
|
|
13
|
+
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
14
|
+
import * as Turns from "./turns.ts";
|
|
15
|
+
import * as Updates from "./updates.ts";
|
|
16
|
+
|
|
17
|
+
export type TelegramRoutedMessage = Updates.TelegramUpdateMessage &
|
|
18
|
+
Media.TelegramMediaMessage &
|
|
19
|
+
Media.TelegramMediaGroupMessage &
|
|
20
|
+
Commands.TelegramCommandRuntimeMessage &
|
|
21
|
+
Turns.TelegramTurnMessage;
|
|
22
|
+
|
|
23
|
+
export type TelegramRoutedCallbackQuery = Updates.TelegramCallbackQuery &
|
|
24
|
+
Menu.MenuCallbackQuery;
|
|
25
|
+
|
|
26
|
+
export interface TelegramInboundRouteRuntimeDeps<
|
|
27
|
+
TUpdate extends Updates.TelegramUpdateFlow & {
|
|
28
|
+
message?: TMessage;
|
|
29
|
+
edited_message?: TMessage;
|
|
30
|
+
callback_query?: TCallbackQuery;
|
|
31
|
+
},
|
|
32
|
+
TMessage extends TelegramRoutedMessage,
|
|
33
|
+
TCallbackQuery extends TelegramRoutedCallbackQuery,
|
|
34
|
+
TContext,
|
|
35
|
+
TModel extends Model.MenuModel,
|
|
36
|
+
> {
|
|
37
|
+
configStore: Pick<
|
|
38
|
+
TelegramConfigStore,
|
|
39
|
+
"getAllowedUserId" | "setAllowedUserId" | "persist"
|
|
40
|
+
>;
|
|
41
|
+
bridgeRuntime: TelegramBridgeRuntime;
|
|
42
|
+
activeTurnRuntime: Queue.TelegramActiveTurnStore;
|
|
43
|
+
mediaGroupRuntime: Media.TelegramMediaGroupController<TMessage>;
|
|
44
|
+
telegramQueueStore: Queue.TelegramQueueStateStore<TContext>;
|
|
45
|
+
queueMutationRuntime: Queue.TelegramQueueMutationController<TContext>;
|
|
46
|
+
modelMenuRuntime: Menu.TelegramModelMenuRuntime<TModel>;
|
|
47
|
+
currentModelRuntime: Model.CurrentModelRuntime<TContext, TModel>;
|
|
48
|
+
modelSwitchController: Model.TelegramModelSwitchController<
|
|
49
|
+
TContext,
|
|
50
|
+
Model.ScopedTelegramModel<TModel>
|
|
51
|
+
>;
|
|
52
|
+
menuActions: Menu.TelegramMenuActionRuntime<TContext, TModel>;
|
|
53
|
+
attachmentHandlerRuntime: TelegramAttachmentHandlerRuntime<TContext>;
|
|
54
|
+
updateStatus: (ctx: TContext, error?: string) => void;
|
|
55
|
+
dispatchNextQueuedTelegramTurn: (ctx: TContext) => void;
|
|
56
|
+
answerCallbackQuery: (
|
|
57
|
+
callbackQueryId: string,
|
|
58
|
+
text?: string,
|
|
59
|
+
) => Promise<void>;
|
|
60
|
+
sendTextReply: (
|
|
61
|
+
chatId: number,
|
|
62
|
+
replyToMessageId: number,
|
|
63
|
+
text: string,
|
|
64
|
+
) => Promise<number | undefined>;
|
|
65
|
+
setMyCommands: Commands.TelegramBotCommandRegistrationDeps["setMyCommands"];
|
|
66
|
+
downloadFile: Media.DownloadTelegramMessageFilesDeps["downloadFile"];
|
|
67
|
+
getThinkingLevel: () => Model.ThinkingLevel;
|
|
68
|
+
setThinkingLevel: (level: Model.ThinkingLevel) => void;
|
|
69
|
+
setModel: (model: TModel) => Promise<boolean>;
|
|
70
|
+
isIdle: (ctx: TContext) => boolean;
|
|
71
|
+
hasPendingMessages: (ctx: TContext) => boolean;
|
|
72
|
+
compact: (
|
|
73
|
+
ctx: TContext,
|
|
74
|
+
callbacks: { onComplete: () => void; onError: (error: unknown) => void },
|
|
75
|
+
) => void;
|
|
76
|
+
recordRuntimeEvent?: (
|
|
77
|
+
category: string,
|
|
78
|
+
error: unknown,
|
|
79
|
+
details?: Record<string, unknown>,
|
|
80
|
+
) => void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createTelegramInboundRouteRuntime<
|
|
84
|
+
TUpdate extends Updates.TelegramUpdateFlow & {
|
|
85
|
+
message?: TMessage;
|
|
86
|
+
edited_message?: TMessage;
|
|
87
|
+
callback_query?: TCallbackQuery;
|
|
88
|
+
},
|
|
89
|
+
TMessage extends TelegramRoutedMessage,
|
|
90
|
+
TCallbackQuery extends TelegramRoutedCallbackQuery,
|
|
91
|
+
TContext,
|
|
92
|
+
TModel extends Model.MenuModel,
|
|
93
|
+
>(
|
|
94
|
+
deps: TelegramInboundRouteRuntimeDeps<
|
|
95
|
+
TUpdate,
|
|
96
|
+
TMessage,
|
|
97
|
+
TCallbackQuery,
|
|
98
|
+
TContext,
|
|
99
|
+
TModel
|
|
100
|
+
>,
|
|
101
|
+
): Updates.TelegramUpdateRuntimeController<TContext, TUpdate> {
|
|
102
|
+
const callbackHandler = Menu.createTelegramMenuCallbackHandlerForContext<
|
|
103
|
+
TCallbackQuery,
|
|
104
|
+
TContext,
|
|
105
|
+
TModel
|
|
106
|
+
>({
|
|
107
|
+
getStoredModelMenuState: deps.modelMenuRuntime.getState,
|
|
108
|
+
getActiveModel: deps.currentModelRuntime.get,
|
|
109
|
+
getThinkingLevel: deps.getThinkingLevel,
|
|
110
|
+
setThinkingLevel: deps.setThinkingLevel,
|
|
111
|
+
updateStatus: deps.updateStatus,
|
|
112
|
+
updateModelMenuMessage: deps.menuActions.updateModelMenuMessage,
|
|
113
|
+
updateThinkingMenuMessage: deps.menuActions.updateThinkingMenuMessage,
|
|
114
|
+
updateStatusMessage: deps.menuActions.updateStatusMessage,
|
|
115
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
116
|
+
isIdle: deps.isIdle,
|
|
117
|
+
hasActiveTelegramTurn: deps.activeTurnRuntime.has,
|
|
118
|
+
hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
|
|
119
|
+
getActiveToolExecutions: deps.bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
120
|
+
setModel: deps.setModel,
|
|
121
|
+
setCurrentModel: deps.currentModelRuntime.setCurrentModel,
|
|
122
|
+
stagePendingModelSwitch: deps.modelSwitchController.stagePendingSwitch,
|
|
123
|
+
restartInterruptedTelegramTurn:
|
|
124
|
+
deps.modelSwitchController.restartInterruptedTurn,
|
|
125
|
+
});
|
|
126
|
+
const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
|
|
127
|
+
TMessage,
|
|
128
|
+
TContext
|
|
129
|
+
>({
|
|
130
|
+
hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
|
|
131
|
+
clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
|
|
132
|
+
hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
|
|
133
|
+
setPreserveQueuedTurnsAsHistory:
|
|
134
|
+
deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
135
|
+
abortCurrentTurn: deps.bridgeRuntime.abort.abortTurn,
|
|
136
|
+
isIdle: deps.isIdle,
|
|
137
|
+
hasPendingMessages: deps.hasPendingMessages,
|
|
138
|
+
hasActiveTelegramTurn: deps.activeTurnRuntime.has,
|
|
139
|
+
hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
|
|
140
|
+
isCompactionInProgress: deps.bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
141
|
+
setCompactionInProgress: deps.bridgeRuntime.lifecycle.setCompactionInProgress,
|
|
142
|
+
updateStatus: deps.updateStatus,
|
|
143
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
144
|
+
compact: deps.compact,
|
|
145
|
+
allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
146
|
+
allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
|
|
147
|
+
appendControlItem: deps.queueMutationRuntime.append,
|
|
148
|
+
showStatus: deps.menuActions.sendStatusMessage,
|
|
149
|
+
openModelMenu: deps.menuActions.openModelMenu,
|
|
150
|
+
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
151
|
+
setAllowedUserId: deps.configStore.setAllowedUserId,
|
|
152
|
+
setMyCommands: deps.setMyCommands,
|
|
153
|
+
persistConfig: deps.configStore.persist,
|
|
154
|
+
sendTextReply: deps.sendTextReply,
|
|
155
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
156
|
+
});
|
|
157
|
+
const promptEnqueue = Queue.createTelegramPromptEnqueueController<
|
|
158
|
+
TMessage,
|
|
159
|
+
TContext
|
|
160
|
+
>({
|
|
161
|
+
...deps.telegramQueueStore,
|
|
162
|
+
getPreserveQueuedTurnsAsHistory:
|
|
163
|
+
deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
164
|
+
setPreserveQueuedTurnsAsHistory:
|
|
165
|
+
deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
166
|
+
createTurn: Turns.createTelegramPromptTurnRuntimeBuilder<
|
|
167
|
+
TMessage,
|
|
168
|
+
TContext
|
|
169
|
+
>({
|
|
170
|
+
allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
171
|
+
downloadFile: deps.downloadFile,
|
|
172
|
+
processAttachments: deps.attachmentHandlerRuntime.process,
|
|
173
|
+
}),
|
|
174
|
+
updateStatus: deps.updateStatus,
|
|
175
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
176
|
+
}).enqueue;
|
|
177
|
+
const commandOrPrompt = Commands.createTelegramCommandOrPromptRuntime<
|
|
178
|
+
TMessage,
|
|
179
|
+
TContext
|
|
180
|
+
>({
|
|
181
|
+
extractRawText: Media.extractFirstTelegramMessageText,
|
|
182
|
+
handleCommand: commandHandler,
|
|
183
|
+
enqueueTurn: promptEnqueue,
|
|
184
|
+
});
|
|
185
|
+
const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
|
|
186
|
+
TMessage,
|
|
187
|
+
TContext
|
|
188
|
+
>({
|
|
189
|
+
mediaGroups: deps.mediaGroupRuntime,
|
|
190
|
+
dispatchMessages: commandOrPrompt.dispatchMessages,
|
|
191
|
+
});
|
|
192
|
+
const editRuntime = Turns.createTelegramQueuedPromptEditRuntime<
|
|
193
|
+
TMessage,
|
|
194
|
+
TContext
|
|
195
|
+
>({
|
|
196
|
+
...deps.telegramQueueStore,
|
|
197
|
+
updateStatus: deps.updateStatus,
|
|
198
|
+
});
|
|
199
|
+
return Updates.createTelegramPairedUpdateRuntime<TContext, TUpdate>({
|
|
200
|
+
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
201
|
+
setAllowedUserId: deps.configStore.setAllowedUserId,
|
|
202
|
+
persistConfig: deps.configStore.persist,
|
|
203
|
+
updateStatus: deps.updateStatus,
|
|
204
|
+
removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
|
|
205
|
+
removeQueuedTelegramTurnsByMessageIds:
|
|
206
|
+
deps.queueMutationRuntime.removeByMessageIds,
|
|
207
|
+
clearQueuedTelegramTurnPriorityByMessageId:
|
|
208
|
+
deps.queueMutationRuntime.clearPriorityByMessageId,
|
|
209
|
+
prioritizeQueuedTelegramTurnByMessageId:
|
|
210
|
+
deps.queueMutationRuntime.prioritizeByMessageId,
|
|
211
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
212
|
+
handleAuthorizedTelegramCallbackQuery: callbackHandler,
|
|
213
|
+
sendTextReply: deps.sendTextReply,
|
|
214
|
+
handleAuthorizedTelegramMessage: mediaDispatch.handleMessage,
|
|
215
|
+
handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
|
|
216
|
+
});
|
|
217
|
+
}
|
package/lib/setup.ts
CHANGED
|
@@ -21,6 +21,11 @@ export interface TelegramSetupUser {
|
|
|
21
21
|
username?: string;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
+
export interface TelegramPollingStartResult {
|
|
25
|
+
ok: boolean;
|
|
26
|
+
message?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
24
29
|
export interface TelegramSetupDeps {
|
|
25
30
|
hasUI: boolean;
|
|
26
31
|
env: NodeJS.ProcessEnv;
|
|
@@ -34,7 +39,7 @@ export interface TelegramSetupDeps {
|
|
|
34
39
|
}>;
|
|
35
40
|
persistConfig: (config: TelegramSetupConfig) => Promise<void>;
|
|
36
41
|
notify: (message: string, level: "info" | "error") => void;
|
|
37
|
-
startPolling: () =>
|
|
42
|
+
startPolling: () => unknown | Promise<unknown>;
|
|
38
43
|
updateStatus: () => void;
|
|
39
44
|
}
|
|
40
45
|
|
|
@@ -61,7 +66,7 @@ export interface TelegramSetupPromptRuntimeDeps<
|
|
|
61
66
|
setupGuard: TelegramSetupGuard;
|
|
62
67
|
getMe: TelegramSetupDeps["getMe"];
|
|
63
68
|
persistConfig: (config: TelegramSetupConfig) => Promise<void>;
|
|
64
|
-
startPolling: (ctx: TContext) =>
|
|
69
|
+
startPolling: (ctx: TContext) => unknown | Promise<unknown>;
|
|
65
70
|
updateStatus: (ctx: TContext) => void;
|
|
66
71
|
recordRuntimeEvent?: (
|
|
67
72
|
category: string,
|
|
@@ -78,6 +83,12 @@ const TELEGRAM_BOT_TOKEN_ENV_VARS = [
|
|
|
78
83
|
"TELEGRAM_KEY",
|
|
79
84
|
] as const;
|
|
80
85
|
|
|
86
|
+
function isTelegramPollingStartResult(
|
|
87
|
+
value: unknown,
|
|
88
|
+
): value is TelegramPollingStartResult {
|
|
89
|
+
return !!value && typeof value === "object" && typeof (value as { ok?: unknown }).ok === "boolean";
|
|
90
|
+
}
|
|
91
|
+
|
|
81
92
|
export function getTelegramBotTokenInputDefault(
|
|
82
93
|
env: NodeJS.ProcessEnv = process.env,
|
|
83
94
|
configToken?: string,
|
|
@@ -135,7 +146,10 @@ export async function runTelegramSetup(
|
|
|
135
146
|
"Send /start to your bot in Telegram to pair this extension with your account.",
|
|
136
147
|
"info",
|
|
137
148
|
);
|
|
138
|
-
await deps.startPolling();
|
|
149
|
+
const startResult = await deps.startPolling();
|
|
150
|
+
if (isTelegramPollingStartResult(startResult) && startResult.message) {
|
|
151
|
+
deps.notify(startResult.message, startResult.ok ? "info" : "error");
|
|
152
|
+
}
|
|
139
153
|
deps.updateStatus();
|
|
140
154
|
return nextConfig;
|
|
141
155
|
}
|
package/lib/status.ts
CHANGED
|
@@ -83,6 +83,7 @@ export interface TelegramRuntimeEventRecorderOptions {
|
|
|
83
83
|
export interface TelegramBridgeStatusLineState {
|
|
84
84
|
botUsername?: string;
|
|
85
85
|
allowedUserId?: number;
|
|
86
|
+
lockState?: string;
|
|
86
87
|
pollingActive: boolean;
|
|
87
88
|
lastUpdateId?: number;
|
|
88
89
|
activeSourceMessageIds?: number[];
|
|
@@ -148,6 +149,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
148
149
|
getQueuedItems: () => TQueueItem[];
|
|
149
150
|
formatQueuedStatus: (items: TQueueItem[]) => string;
|
|
150
151
|
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
|
|
152
|
+
getRuntimeLockState?: () => string;
|
|
151
153
|
}
|
|
152
154
|
|
|
153
155
|
export interface TelegramStatusRuntime<
|
|
@@ -350,6 +352,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
350
352
|
return {
|
|
351
353
|
botUsername: config.botUsername,
|
|
352
354
|
allowedUserId: config.allowedUserId,
|
|
355
|
+
lockState: deps.getRuntimeLockState?.(),
|
|
353
356
|
pollingActive: deps.isPollingActive(),
|
|
354
357
|
lastUpdateId: config.lastUpdateId,
|
|
355
358
|
activeSourceMessageIds: deps.getActiveSourceMessageIds(),
|
|
@@ -404,6 +407,7 @@ export function buildTelegramBridgeStatusLines(
|
|
|
404
407
|
"connection:",
|
|
405
408
|
`- bot: ${state.botUsername ? `@${state.botUsername}` : "not configured"}`,
|
|
406
409
|
`- allowed user: ${state.allowedUserId ?? "not paired"}`,
|
|
410
|
+
...(state.lockState ? [`- owner: ${state.lockState}`] : []),
|
|
407
411
|
"",
|
|
408
412
|
"polling:",
|
|
409
413
|
`- state: ${state.pollingActive ? "running" : "stopped"}`,
|
package/lib/turns.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readFile } from "node:fs/promises";
|
|
7
|
-
import { basename } from "node:path";
|
|
7
|
+
import { basename, dirname, join } from "node:path";
|
|
8
8
|
|
|
9
9
|
import {
|
|
10
10
|
collectTelegramMessageIds,
|
|
@@ -53,9 +53,14 @@ export function truncateTelegramQueueSummary(
|
|
|
53
53
|
export function formatTelegramTurnStatusSummary(
|
|
54
54
|
rawText: string,
|
|
55
55
|
files: DownloadedTelegramTurnFile[],
|
|
56
|
+
handlerOutputs: string[] = [],
|
|
56
57
|
): string {
|
|
57
58
|
const textSummary = truncateTelegramQueueSummary(rawText);
|
|
58
59
|
if (textSummary) return textSummary;
|
|
60
|
+
const handlerSummary = truncateTelegramQueueSummary(
|
|
61
|
+
handlerOutputs.join(" "),
|
|
62
|
+
);
|
|
63
|
+
if (handlerSummary) return handlerSummary;
|
|
59
64
|
if (files.length === 1) {
|
|
60
65
|
const fileName = basename(
|
|
61
66
|
files[0]?.fileName || files[0]?.path || "attachment",
|
|
@@ -66,10 +71,37 @@ export function formatTelegramTurnStatusSummary(
|
|
|
66
71
|
return "(empty message)";
|
|
67
72
|
}
|
|
68
73
|
|
|
74
|
+
function appendTelegramListSection(
|
|
75
|
+
text: string,
|
|
76
|
+
title: string,
|
|
77
|
+
items: string[],
|
|
78
|
+
): string {
|
|
79
|
+
if (items.length === 0) return text;
|
|
80
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
81
|
+
return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function appendTelegramAttachmentSection(
|
|
85
|
+
text: string,
|
|
86
|
+
files: Pick<DownloadedTelegramTurnFile, "path">[],
|
|
87
|
+
): string {
|
|
88
|
+
if (files.length === 0) return text;
|
|
89
|
+
const dirs = [...new Set(files.map((file) => dirname(file.path)))];
|
|
90
|
+
const sameDir = dirs.length === 1;
|
|
91
|
+
const header = sameDir ? `[attachments] ${dirs[0]}` : "[attachments]";
|
|
92
|
+
const items = sameDir
|
|
93
|
+
? files.map((file) => `/${basename(file.path)}`)
|
|
94
|
+
: files.map((file) => file.path);
|
|
95
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
96
|
+
return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
69
99
|
export function buildTelegramTurnPrompt(options: {
|
|
70
100
|
telegramPrefix: string;
|
|
71
101
|
rawText: string;
|
|
72
102
|
files: DownloadedTelegramTurnFile[];
|
|
103
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
104
|
+
handlerOutputs?: string[];
|
|
73
105
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
74
106
|
}): string {
|
|
75
107
|
let prompt = options.telegramPrefix;
|
|
@@ -87,12 +119,9 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
87
119
|
? `\n${options.rawText}`
|
|
88
120
|
: ` ${options.rawText}`;
|
|
89
121
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
prompt += `\n- ${file.path}`;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
122
|
+
const promptFiles = options.promptFiles ?? options.files;
|
|
123
|
+
prompt = appendTelegramAttachmentSection(prompt, promptFiles);
|
|
124
|
+
prompt = appendTelegramListSection(prompt, "outputs", options.handlerOutputs ?? []);
|
|
96
125
|
return prompt;
|
|
97
126
|
}
|
|
98
127
|
|
|
@@ -101,7 +130,7 @@ function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
|
101
130
|
attachmentSuffix: string;
|
|
102
131
|
attachmentFiles: DownloadedTelegramTurnFile[];
|
|
103
132
|
} {
|
|
104
|
-
const marker = "\n\
|
|
133
|
+
const marker = "\n\n[attachments]";
|
|
105
134
|
const markerIndex = prompt.indexOf(marker);
|
|
106
135
|
if (markerIndex === -1) {
|
|
107
136
|
return {
|
|
@@ -112,11 +141,30 @@ function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
|
112
141
|
}
|
|
113
142
|
const promptWithoutAttachments = prompt.slice(0, markerIndex);
|
|
114
143
|
const attachmentSuffix = prompt.slice(markerIndex);
|
|
115
|
-
const
|
|
116
|
-
|
|
144
|
+
const attachmentLines: string[] = [];
|
|
145
|
+
let readingAttachments = false;
|
|
146
|
+
let attachmentDir: string | undefined;
|
|
147
|
+
for (const line of attachmentSuffix.split("\n")) {
|
|
148
|
+
const trimmed = line.trim();
|
|
149
|
+
const attachmentMatch = trimmed.match(/^\[attachments\](?:\s+(.+))?$/);
|
|
150
|
+
if (attachmentMatch) {
|
|
151
|
+
readingAttachments = true;
|
|
152
|
+
attachmentDir = attachmentMatch[1]?.trim();
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
if (readingAttachments && /^\[[^\]]+\](?:\s+.*)?$/.test(trimmed)) break;
|
|
156
|
+
if (readingAttachments) attachmentLines.push(line);
|
|
157
|
+
}
|
|
158
|
+
const attachmentFiles = attachmentLines
|
|
117
159
|
.map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
|
|
118
160
|
.filter((path): path is string => !!path)
|
|
119
|
-
.map((path) => (
|
|
161
|
+
.map((path) => (attachmentDir ? join(attachmentDir, path.replace(/^\/+/, "")) : path))
|
|
162
|
+
.map((path) => ({
|
|
163
|
+
path,
|
|
164
|
+
fileName: basename(path),
|
|
165
|
+
isImage: false,
|
|
166
|
+
kind: "document" as const,
|
|
167
|
+
}));
|
|
120
168
|
return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
|
|
121
169
|
}
|
|
122
170
|
|
|
@@ -242,6 +290,8 @@ export interface BuildTelegramPromptTurnOptions {
|
|
|
242
290
|
queueOrder: number;
|
|
243
291
|
rawText: string;
|
|
244
292
|
files: DownloadedTelegramTurnFile[];
|
|
293
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
294
|
+
handlerOutputs?: string[];
|
|
245
295
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
246
296
|
inferImageMimeType: (path: string) => string | undefined;
|
|
247
297
|
}
|
|
@@ -251,30 +301,50 @@ export type BuildTelegramPromptTurnRuntimeOptions = Omit<
|
|
|
251
301
|
"readBinaryFile"
|
|
252
302
|
>;
|
|
253
303
|
|
|
254
|
-
export interface TelegramPromptTurnRuntimeBuilderDeps
|
|
304
|
+
export interface TelegramPromptTurnRuntimeBuilderDeps<TContext = unknown>
|
|
305
|
+
extends DownloadTelegramMessageFilesDeps {
|
|
255
306
|
allocateQueueOrder: () => number;
|
|
307
|
+
processAttachments?: (
|
|
308
|
+
files: DownloadedTelegramTurnFile[],
|
|
309
|
+
rawText: string,
|
|
310
|
+
ctx: TContext,
|
|
311
|
+
) => Promise<{
|
|
312
|
+
rawText: string;
|
|
313
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
314
|
+
handlerOutputs?: string[];
|
|
315
|
+
}>;
|
|
256
316
|
}
|
|
257
317
|
|
|
258
318
|
export function createTelegramPromptTurnRuntimeBuilder<
|
|
259
319
|
TMessage extends TelegramTurnMessage & TelegramMediaMessage,
|
|
320
|
+
TContext = unknown,
|
|
260
321
|
>(
|
|
261
|
-
deps: TelegramPromptTurnRuntimeBuilderDeps
|
|
322
|
+
deps: TelegramPromptTurnRuntimeBuilderDeps<TContext>,
|
|
262
323
|
): (
|
|
263
324
|
messages: TMessage[],
|
|
264
325
|
historyTurns?: PendingTelegramTurn[],
|
|
326
|
+
ctx?: TContext,
|
|
265
327
|
) => Promise<PendingTelegramTurn> {
|
|
266
|
-
return async (messages, historyTurns = []) =>
|
|
267
|
-
|
|
328
|
+
return async (messages, historyTurns = [], ctx) => {
|
|
329
|
+
const rawText = extractTelegramMessagesText(messages);
|
|
330
|
+
const files = await downloadTelegramMessageFiles(messages, {
|
|
331
|
+
downloadFile: deps.downloadFile,
|
|
332
|
+
});
|
|
333
|
+
const processed = deps.processAttachments
|
|
334
|
+
? await deps.processAttachments(files, rawText, ctx as TContext)
|
|
335
|
+
: { rawText, promptFiles: files };
|
|
336
|
+
return buildTelegramPromptTurnRuntime({
|
|
268
337
|
telegramPrefix: TELEGRAM_PREFIX,
|
|
269
338
|
messages,
|
|
270
339
|
historyTurns,
|
|
271
340
|
queueOrder: deps.allocateQueueOrder(),
|
|
272
|
-
rawText:
|
|
273
|
-
files
|
|
274
|
-
|
|
275
|
-
|
|
341
|
+
rawText: processed.rawText,
|
|
342
|
+
files,
|
|
343
|
+
promptFiles: processed.promptFiles,
|
|
344
|
+
handlerOutputs: processed.handlerOutputs,
|
|
276
345
|
inferImageMimeType: guessMediaType,
|
|
277
346
|
});
|
|
347
|
+
};
|
|
278
348
|
}
|
|
279
349
|
|
|
280
350
|
export async function buildTelegramPromptTurn(
|
|
@@ -291,6 +361,8 @@ export async function buildTelegramPromptTurn(
|
|
|
291
361
|
telegramPrefix: options.telegramPrefix,
|
|
292
362
|
rawText: options.rawText,
|
|
293
363
|
files: options.files,
|
|
364
|
+
promptFiles: options.promptFiles,
|
|
365
|
+
handlerOutputs: options.handlerOutputs,
|
|
294
366
|
historyTurns: options.historyTurns,
|
|
295
367
|
}),
|
|
296
368
|
},
|
|
@@ -316,10 +388,15 @@ export async function buildTelegramPromptTurn(
|
|
|
316
388
|
laneOrder: options.queueOrder,
|
|
317
389
|
queuedAttachments: [],
|
|
318
390
|
content,
|
|
319
|
-
historyText: formatTelegramHistoryText(
|
|
391
|
+
historyText: formatTelegramHistoryText(
|
|
392
|
+
options.rawText,
|
|
393
|
+
options.promptFiles ?? options.files,
|
|
394
|
+
options.handlerOutputs,
|
|
395
|
+
),
|
|
320
396
|
statusSummary: formatTelegramTurnStatusSummary(
|
|
321
397
|
options.rawText,
|
|
322
|
-
options.files,
|
|
398
|
+
options.promptFiles ?? options.files,
|
|
399
|
+
options.handlerOutputs,
|
|
323
400
|
),
|
|
324
401
|
};
|
|
325
402
|
}
|