@llblab/pi-telegram 0.3.0 → 0.5.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 +41 -10
- package/docs/README.md +4 -3
- package/docs/architecture.md +43 -38
- package/docs/attachment-handlers.md +60 -0
- package/docs/command-templates.md +75 -0
- package/docs/locks.md +136 -0
- package/index.ts +80 -142
- package/lib/attachments.ts +70 -2
- package/lib/commands.ts +116 -48
- package/lib/config.ts +17 -5
- package/lib/handlers.ts +400 -0
- package/lib/lifecycle.ts +140 -0
- package/lib/locks.ts +336 -0
- package/lib/media.ts +50 -6
- package/lib/menu.ts +0 -4
- package/lib/pi.ts +11 -1
- package/lib/prompts.ts +44 -0
- package/lib/queue.ts +12 -6
- package/lib/routing.ts +219 -0
- package/lib/runtime.ts +9 -6
- package/lib/setup.ts +21 -3
- package/lib/status.ts +33 -4
- package/lib/turns.ts +103 -21
- package/package.json +1 -1
- package/lib/registration.ts +0 -262
package/lib/routing.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
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:
|
|
120
|
+
deps.bridgeRuntime.lifecycle.getActiveToolExecutions,
|
|
121
|
+
setModel: deps.setModel,
|
|
122
|
+
setCurrentModel: deps.currentModelRuntime.setCurrentModel,
|
|
123
|
+
stagePendingModelSwitch: deps.modelSwitchController.stagePendingSwitch,
|
|
124
|
+
restartInterruptedTelegramTurn:
|
|
125
|
+
deps.modelSwitchController.restartInterruptedTurn,
|
|
126
|
+
});
|
|
127
|
+
const commandHandler = Commands.createTelegramCommandHandlerTargetRuntime<
|
|
128
|
+
TMessage,
|
|
129
|
+
TContext
|
|
130
|
+
>({
|
|
131
|
+
hasAbortHandler: deps.bridgeRuntime.abort.hasHandler,
|
|
132
|
+
clearPendingModelSwitch: deps.modelSwitchController.clearPendingSwitch,
|
|
133
|
+
hasQueuedTelegramItems: deps.telegramQueueStore.hasQueuedItems,
|
|
134
|
+
setPreserveQueuedTurnsAsHistory:
|
|
135
|
+
deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
136
|
+
abortCurrentTurn: deps.bridgeRuntime.abort.abortTurn,
|
|
137
|
+
isIdle: deps.isIdle,
|
|
138
|
+
hasPendingMessages: deps.hasPendingMessages,
|
|
139
|
+
hasActiveTelegramTurn: deps.activeTurnRuntime.has,
|
|
140
|
+
hasDispatchPending: deps.bridgeRuntime.lifecycle.hasDispatchPending,
|
|
141
|
+
isCompactionInProgress: deps.bridgeRuntime.lifecycle.isCompactionInProgress,
|
|
142
|
+
setCompactionInProgress:
|
|
143
|
+
deps.bridgeRuntime.lifecycle.setCompactionInProgress,
|
|
144
|
+
updateStatus: deps.updateStatus,
|
|
145
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
146
|
+
compact: deps.compact,
|
|
147
|
+
allocateItemOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
148
|
+
allocateControlOrder: deps.bridgeRuntime.queue.allocateControlOrder,
|
|
149
|
+
appendControlItem: deps.queueMutationRuntime.append,
|
|
150
|
+
showStatus: deps.menuActions.sendStatusMessage,
|
|
151
|
+
openModelMenu: deps.menuActions.openModelMenu,
|
|
152
|
+
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
153
|
+
setAllowedUserId: deps.configStore.setAllowedUserId,
|
|
154
|
+
setMyCommands: deps.setMyCommands,
|
|
155
|
+
persistConfig: deps.configStore.persist,
|
|
156
|
+
sendTextReply: deps.sendTextReply,
|
|
157
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
158
|
+
});
|
|
159
|
+
const promptEnqueue = Queue.createTelegramPromptEnqueueController<
|
|
160
|
+
TMessage,
|
|
161
|
+
TContext
|
|
162
|
+
>({
|
|
163
|
+
...deps.telegramQueueStore,
|
|
164
|
+
getPreserveQueuedTurnsAsHistory:
|
|
165
|
+
deps.bridgeRuntime.lifecycle.shouldPreserveQueuedTurnsAsHistory,
|
|
166
|
+
setPreserveQueuedTurnsAsHistory:
|
|
167
|
+
deps.bridgeRuntime.lifecycle.setPreserveQueuedTurnsAsHistory,
|
|
168
|
+
createTurn: Turns.createTelegramPromptTurnRuntimeBuilder<
|
|
169
|
+
TMessage,
|
|
170
|
+
TContext
|
|
171
|
+
>({
|
|
172
|
+
allocateQueueOrder: deps.bridgeRuntime.queue.allocateItemOrder,
|
|
173
|
+
downloadFile: deps.downloadFile,
|
|
174
|
+
processAttachments: deps.attachmentHandlerRuntime.process,
|
|
175
|
+
}),
|
|
176
|
+
updateStatus: deps.updateStatus,
|
|
177
|
+
dispatchNextQueuedTelegramTurn: deps.dispatchNextQueuedTelegramTurn,
|
|
178
|
+
}).enqueue;
|
|
179
|
+
const commandOrPrompt = Commands.createTelegramCommandOrPromptRuntime<
|
|
180
|
+
TMessage,
|
|
181
|
+
TContext
|
|
182
|
+
>({
|
|
183
|
+
extractRawText: Media.extractFirstTelegramMessageText,
|
|
184
|
+
handleCommand: commandHandler,
|
|
185
|
+
enqueueTurn: promptEnqueue,
|
|
186
|
+
});
|
|
187
|
+
const mediaDispatch = Media.createTelegramMediaGroupDispatchRuntime<
|
|
188
|
+
TMessage,
|
|
189
|
+
TContext
|
|
190
|
+
>({
|
|
191
|
+
mediaGroups: deps.mediaGroupRuntime,
|
|
192
|
+
dispatchMessages: commandOrPrompt.dispatchMessages,
|
|
193
|
+
});
|
|
194
|
+
const editRuntime = Turns.createTelegramQueuedPromptEditRuntime<
|
|
195
|
+
TMessage,
|
|
196
|
+
TContext
|
|
197
|
+
>({
|
|
198
|
+
...deps.telegramQueueStore,
|
|
199
|
+
updateStatus: deps.updateStatus,
|
|
200
|
+
});
|
|
201
|
+
return Updates.createTelegramPairedUpdateRuntime<TContext, TUpdate>({
|
|
202
|
+
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
203
|
+
setAllowedUserId: deps.configStore.setAllowedUserId,
|
|
204
|
+
persistConfig: deps.configStore.persist,
|
|
205
|
+
updateStatus: deps.updateStatus,
|
|
206
|
+
removePendingMediaGroupMessages: deps.mediaGroupRuntime.removeMessages,
|
|
207
|
+
removeQueuedTelegramTurnsByMessageIds:
|
|
208
|
+
deps.queueMutationRuntime.removeByMessageIds,
|
|
209
|
+
clearQueuedTelegramTurnPriorityByMessageId:
|
|
210
|
+
deps.queueMutationRuntime.clearPriorityByMessageId,
|
|
211
|
+
prioritizeQueuedTelegramTurnByMessageId:
|
|
212
|
+
deps.queueMutationRuntime.prioritizeByMessageId,
|
|
213
|
+
answerCallbackQuery: deps.answerCallbackQuery,
|
|
214
|
+
handleAuthorizedTelegramCallbackQuery: callbackHandler,
|
|
215
|
+
sendTextReply: deps.sendTextReply,
|
|
216
|
+
handleAuthorizedTelegramMessage: mediaDispatch.handleMessage,
|
|
217
|
+
handleAuthorizedTelegramEditedMessage: editRuntime.updateFromEditedMessage,
|
|
218
|
+
});
|
|
219
|
+
}
|
package/lib/runtime.ts
CHANGED
|
@@ -327,8 +327,9 @@ export interface TelegramRuntimeEventRecorderPort {
|
|
|
327
327
|
) => void;
|
|
328
328
|
}
|
|
329
329
|
|
|
330
|
-
export interface TelegramTypingLoopStarterDeps<
|
|
331
|
-
|
|
330
|
+
export interface TelegramTypingLoopStarterDeps<
|
|
331
|
+
TContext,
|
|
332
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
332
333
|
typing: TelegramRuntimeTypingPort;
|
|
333
334
|
getDefaultChatId: () => number | undefined;
|
|
334
335
|
sendTypingAction: (chatId: number) => Promise<unknown>;
|
|
@@ -413,8 +414,9 @@ export function createTelegramAgentEndResetter(
|
|
|
413
414
|
};
|
|
414
415
|
}
|
|
415
416
|
|
|
416
|
-
export interface TelegramPromptDispatchLifecycleDeps<
|
|
417
|
-
|
|
417
|
+
export interface TelegramPromptDispatchLifecycleDeps<
|
|
418
|
+
TContext,
|
|
419
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
418
420
|
lifecycle: Pick<
|
|
419
421
|
TelegramRuntimeLifecyclePort,
|
|
420
422
|
"setDispatchPending" | "clearDispatchPending"
|
|
@@ -424,8 +426,9 @@ export interface TelegramPromptDispatchLifecycleDeps<TContext>
|
|
|
424
426
|
updateStatus: (ctx: TContext, error?: string) => void;
|
|
425
427
|
}
|
|
426
428
|
|
|
427
|
-
export interface TelegramPromptDispatchRuntimeDeps<
|
|
428
|
-
|
|
429
|
+
export interface TelegramPromptDispatchRuntimeDeps<
|
|
430
|
+
TContext,
|
|
431
|
+
> extends TelegramRuntimeEventRecorderPort {
|
|
429
432
|
lifecycle: TelegramPromptDispatchLifecycleDeps<TContext>["lifecycle"];
|
|
430
433
|
typing: TelegramRuntimeTypingPort;
|
|
431
434
|
getDefaultChatId: () => number | undefined;
|
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,16 @@ 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 (
|
|
90
|
+
!!value &&
|
|
91
|
+
typeof value === "object" &&
|
|
92
|
+
typeof (value as { ok?: unknown }).ok === "boolean"
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
export function getTelegramBotTokenInputDefault(
|
|
82
97
|
env: NodeJS.ProcessEnv = process.env,
|
|
83
98
|
configToken?: string,
|
|
@@ -135,7 +150,10 @@ export async function runTelegramSetup(
|
|
|
135
150
|
"Send /start to your bot in Telegram to pair this extension with your account.",
|
|
136
151
|
"info",
|
|
137
152
|
);
|
|
138
|
-
await deps.startPolling();
|
|
153
|
+
const startResult = await deps.startPolling();
|
|
154
|
+
if (isTelegramPollingStartResult(startResult) && startResult.message) {
|
|
155
|
+
deps.notify(startResult.message, startResult.ok ? "info" : "error");
|
|
156
|
+
}
|
|
139
157
|
deps.updateStatus();
|
|
140
158
|
return nextConfig;
|
|
141
159
|
}
|
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[];
|
|
@@ -107,6 +108,7 @@ export interface TelegramStatusBarState {
|
|
|
107
108
|
paired: boolean;
|
|
108
109
|
compactionInProgress: boolean;
|
|
109
110
|
processing: boolean;
|
|
111
|
+
processingStatus?: string;
|
|
110
112
|
queuedStatus: string;
|
|
111
113
|
error?: string;
|
|
112
114
|
}
|
|
@@ -148,6 +150,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
148
150
|
getQueuedItems: () => TQueueItem[];
|
|
149
151
|
formatQueuedStatus: (items: TQueueItem[]) => string;
|
|
150
152
|
getRecentRuntimeEvents: () => TelegramRuntimeEvent[];
|
|
153
|
+
getRuntimeLockState?: () => string;
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
export interface TelegramStatusRuntime<
|
|
@@ -331,6 +334,10 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
331
334
|
getStatusBarState: (_ctx, error) => {
|
|
332
335
|
const config = deps.getConfig();
|
|
333
336
|
const queuedItems = deps.getQueuedItems();
|
|
337
|
+
const hasActiveTurn = deps.hasActiveTurn();
|
|
338
|
+
const hasPendingDispatch = deps.hasDispatchPending();
|
|
339
|
+
const hasPendingModelSwitch = deps.hasPendingModelSwitch();
|
|
340
|
+
const activeToolExecutions = deps.getActiveToolExecutions();
|
|
334
341
|
const compactionInProgress = deps.isCompactionInProgress();
|
|
335
342
|
return {
|
|
336
343
|
hasBotToken: !!config.botToken,
|
|
@@ -338,9 +345,14 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
338
345
|
paired: !!config.allowedUserId,
|
|
339
346
|
compactionInProgress,
|
|
340
347
|
processing:
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
348
|
+
hasActiveTurn || hasPendingDispatch || queuedItems.length > 0,
|
|
349
|
+
processingStatus: getTelegramStatusBarProcessingStatus({
|
|
350
|
+
hasActiveTurn,
|
|
351
|
+
hasPendingDispatch,
|
|
352
|
+
hasPendingModelSwitch,
|
|
353
|
+
activeToolExecutions,
|
|
354
|
+
queuedItems: queuedItems.length,
|
|
355
|
+
}),
|
|
344
356
|
queuedStatus: deps.formatQueuedStatus(queuedItems),
|
|
345
357
|
error,
|
|
346
358
|
};
|
|
@@ -350,6 +362,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
350
362
|
return {
|
|
351
363
|
botUsername: config.botUsername,
|
|
352
364
|
allowedUserId: config.allowedUserId,
|
|
365
|
+
lockState: deps.getRuntimeLockState?.(),
|
|
353
366
|
pollingActive: deps.isPollingActive(),
|
|
354
367
|
lastUpdateId: config.lastUpdateId,
|
|
355
368
|
activeSourceMessageIds: deps.getActiveSourceMessageIds(),
|
|
@@ -364,6 +377,21 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
364
377
|
});
|
|
365
378
|
}
|
|
366
379
|
|
|
380
|
+
export function getTelegramStatusBarProcessingStatus(state: {
|
|
381
|
+
hasActiveTurn: boolean;
|
|
382
|
+
hasPendingDispatch: boolean;
|
|
383
|
+
hasPendingModelSwitch: boolean;
|
|
384
|
+
activeToolExecutions: number;
|
|
385
|
+
queuedItems: number;
|
|
386
|
+
}): string | undefined {
|
|
387
|
+
if (state.hasPendingModelSwitch) return "model";
|
|
388
|
+
if (state.activeToolExecutions > 0) return "tool running";
|
|
389
|
+
if (state.hasActiveTurn) return "active";
|
|
390
|
+
if (state.hasPendingDispatch) return "dispatching";
|
|
391
|
+
if (state.queuedItems > 0) return "queued";
|
|
392
|
+
return undefined;
|
|
393
|
+
}
|
|
394
|
+
|
|
367
395
|
export function buildTelegramStatusBarText(
|
|
368
396
|
theme: TelegramStatusBarTheme,
|
|
369
397
|
state: TelegramStatusBarState,
|
|
@@ -383,7 +411,7 @@ export function buildTelegramStatusBarText(
|
|
|
383
411
|
return `${label} ${theme.fg("accent", "compacting")}${queued}`;
|
|
384
412
|
}
|
|
385
413
|
if (state.processing) {
|
|
386
|
-
return `${label} ${theme.fg("accent", "processing")}${queued}`;
|
|
414
|
+
return `${label} ${theme.fg("accent", state.processingStatus ?? "processing")}${queued}`;
|
|
387
415
|
}
|
|
388
416
|
return `${label} ${theme.fg("success", "connected")}`;
|
|
389
417
|
}
|
|
@@ -404,6 +432,7 @@ export function buildTelegramBridgeStatusLines(
|
|
|
404
432
|
"connection:",
|
|
405
433
|
`- bot: ${state.botUsername ? `@${state.botUsername}` : "not configured"}`,
|
|
406
434
|
`- allowed user: ${state.allowedUserId ?? "not paired"}`,
|
|
435
|
+
...(state.lockState ? [`- owner: ${state.lockState}`] : []),
|
|
407
436
|
"",
|
|
408
437
|
"polling:",
|
|
409
438
|
`- 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,12 @@ 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(handlerOutputs.join(" "));
|
|
61
|
+
if (handlerSummary) return handlerSummary;
|
|
59
62
|
if (files.length === 1) {
|
|
60
63
|
const fileName = basename(
|
|
61
64
|
files[0]?.fileName || files[0]?.path || "attachment",
|
|
@@ -66,10 +69,37 @@ export function formatTelegramTurnStatusSummary(
|
|
|
66
69
|
return "(empty message)";
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
function appendTelegramListSection(
|
|
73
|
+
text: string,
|
|
74
|
+
title: string,
|
|
75
|
+
items: string[],
|
|
76
|
+
): string {
|
|
77
|
+
if (items.length === 0) return text;
|
|
78
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
79
|
+
return `${prefix}[${title}]\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function appendTelegramAttachmentSection(
|
|
83
|
+
text: string,
|
|
84
|
+
files: Pick<DownloadedTelegramTurnFile, "path">[],
|
|
85
|
+
): string {
|
|
86
|
+
if (files.length === 0) return text;
|
|
87
|
+
const dirs = [...new Set(files.map((file) => dirname(file.path)))];
|
|
88
|
+
const sameDir = dirs.length === 1;
|
|
89
|
+
const header = sameDir ? `[attachments] ${dirs[0]}` : "[attachments]";
|
|
90
|
+
const items = sameDir
|
|
91
|
+
? files.map((file) => `/${basename(file.path)}`)
|
|
92
|
+
: files.map((file) => file.path);
|
|
93
|
+
const prefix = text.length > 0 ? `${text}\n\n` : "";
|
|
94
|
+
return `${prefix}${header}\n${items.map((item) => `- ${item}`).join("\n")}`;
|
|
95
|
+
}
|
|
96
|
+
|
|
69
97
|
export function buildTelegramTurnPrompt(options: {
|
|
70
98
|
telegramPrefix: string;
|
|
71
99
|
rawText: string;
|
|
72
100
|
files: DownloadedTelegramTurnFile[];
|
|
101
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
102
|
+
handlerOutputs?: string[];
|
|
73
103
|
historyTurns?: Pick<PendingTelegramTurn, "historyText">[];
|
|
74
104
|
}): string {
|
|
75
105
|
let prompt = options.telegramPrefix;
|
|
@@ -87,12 +117,13 @@ export function buildTelegramTurnPrompt(options: {
|
|
|
87
117
|
? `\n${options.rawText}`
|
|
88
118
|
: ` ${options.rawText}`;
|
|
89
119
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
120
|
+
const promptFiles = options.promptFiles ?? options.files;
|
|
121
|
+
prompt = appendTelegramAttachmentSection(prompt, promptFiles);
|
|
122
|
+
prompt = appendTelegramListSection(
|
|
123
|
+
prompt,
|
|
124
|
+
"outputs",
|
|
125
|
+
options.handlerOutputs ?? [],
|
|
126
|
+
);
|
|
96
127
|
return prompt;
|
|
97
128
|
}
|
|
98
129
|
|
|
@@ -101,7 +132,7 @@ function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
|
101
132
|
attachmentSuffix: string;
|
|
102
133
|
attachmentFiles: DownloadedTelegramTurnFile[];
|
|
103
134
|
} {
|
|
104
|
-
const marker = "\n\
|
|
135
|
+
const marker = "\n\n[attachments]";
|
|
105
136
|
const markerIndex = prompt.indexOf(marker);
|
|
106
137
|
if (markerIndex === -1) {
|
|
107
138
|
return {
|
|
@@ -112,11 +143,32 @@ function splitTelegramPromptAttachmentSuffix(prompt: string): {
|
|
|
112
143
|
}
|
|
113
144
|
const promptWithoutAttachments = prompt.slice(0, markerIndex);
|
|
114
145
|
const attachmentSuffix = prompt.slice(markerIndex);
|
|
115
|
-
const
|
|
116
|
-
|
|
146
|
+
const attachmentLines: string[] = [];
|
|
147
|
+
let readingAttachments = false;
|
|
148
|
+
let attachmentDir: string | undefined;
|
|
149
|
+
for (const line of attachmentSuffix.split("\n")) {
|
|
150
|
+
const trimmed = line.trim();
|
|
151
|
+
const attachmentMatch = trimmed.match(/^\[attachments\](?:\s+(.+))?$/);
|
|
152
|
+
if (attachmentMatch) {
|
|
153
|
+
readingAttachments = true;
|
|
154
|
+
attachmentDir = attachmentMatch[1]?.trim();
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
if (readingAttachments && /^\[[^\]]+\](?:\s+.*)?$/.test(trimmed)) break;
|
|
158
|
+
if (readingAttachments) attachmentLines.push(line);
|
|
159
|
+
}
|
|
160
|
+
const attachmentFiles = attachmentLines
|
|
117
161
|
.map((line) => line.match(/^- (.+)$/)?.[1]?.trim())
|
|
118
162
|
.filter((path): path is string => !!path)
|
|
119
|
-
.map((path) =>
|
|
163
|
+
.map((path) =>
|
|
164
|
+
attachmentDir ? join(attachmentDir, path.replace(/^\/+/, "")) : path,
|
|
165
|
+
)
|
|
166
|
+
.map((path) => ({
|
|
167
|
+
path,
|
|
168
|
+
fileName: basename(path),
|
|
169
|
+
isImage: false,
|
|
170
|
+
kind: "document" as const,
|
|
171
|
+
}));
|
|
120
172
|
return { promptWithoutAttachments, attachmentSuffix, attachmentFiles };
|
|
121
173
|
}
|
|
122
174
|
|
|
@@ -242,6 +294,8 @@ export interface BuildTelegramPromptTurnOptions {
|
|
|
242
294
|
queueOrder: number;
|
|
243
295
|
rawText: string;
|
|
244
296
|
files: DownloadedTelegramTurnFile[];
|
|
297
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
298
|
+
handlerOutputs?: string[];
|
|
245
299
|
readBinaryFile: (path: string) => Promise<Uint8Array>;
|
|
246
300
|
inferImageMimeType: (path: string) => string | undefined;
|
|
247
301
|
}
|
|
@@ -251,30 +305,51 @@ export type BuildTelegramPromptTurnRuntimeOptions = Omit<
|
|
|
251
305
|
"readBinaryFile"
|
|
252
306
|
>;
|
|
253
307
|
|
|
254
|
-
export interface TelegramPromptTurnRuntimeBuilderDeps
|
|
308
|
+
export interface TelegramPromptTurnRuntimeBuilderDeps<
|
|
309
|
+
TContext = unknown,
|
|
310
|
+
> extends DownloadTelegramMessageFilesDeps {
|
|
255
311
|
allocateQueueOrder: () => number;
|
|
312
|
+
processAttachments?: (
|
|
313
|
+
files: DownloadedTelegramTurnFile[],
|
|
314
|
+
rawText: string,
|
|
315
|
+
ctx: TContext,
|
|
316
|
+
) => Promise<{
|
|
317
|
+
rawText: string;
|
|
318
|
+
promptFiles?: DownloadedTelegramTurnFile[];
|
|
319
|
+
handlerOutputs?: string[];
|
|
320
|
+
}>;
|
|
256
321
|
}
|
|
257
322
|
|
|
258
323
|
export function createTelegramPromptTurnRuntimeBuilder<
|
|
259
324
|
TMessage extends TelegramTurnMessage & TelegramMediaMessage,
|
|
325
|
+
TContext = unknown,
|
|
260
326
|
>(
|
|
261
|
-
deps: TelegramPromptTurnRuntimeBuilderDeps
|
|
327
|
+
deps: TelegramPromptTurnRuntimeBuilderDeps<TContext>,
|
|
262
328
|
): (
|
|
263
329
|
messages: TMessage[],
|
|
264
330
|
historyTurns?: PendingTelegramTurn[],
|
|
331
|
+
ctx?: TContext,
|
|
265
332
|
) => Promise<PendingTelegramTurn> {
|
|
266
|
-
return async (messages, historyTurns = []) =>
|
|
267
|
-
|
|
333
|
+
return async (messages, historyTurns = [], ctx) => {
|
|
334
|
+
const rawText = extractTelegramMessagesText(messages);
|
|
335
|
+
const files = await downloadTelegramMessageFiles(messages, {
|
|
336
|
+
downloadFile: deps.downloadFile,
|
|
337
|
+
});
|
|
338
|
+
const processed = deps.processAttachments
|
|
339
|
+
? await deps.processAttachments(files, rawText, ctx as TContext)
|
|
340
|
+
: { rawText, promptFiles: files };
|
|
341
|
+
return buildTelegramPromptTurnRuntime({
|
|
268
342
|
telegramPrefix: TELEGRAM_PREFIX,
|
|
269
343
|
messages,
|
|
270
344
|
historyTurns,
|
|
271
345
|
queueOrder: deps.allocateQueueOrder(),
|
|
272
|
-
rawText:
|
|
273
|
-
files
|
|
274
|
-
|
|
275
|
-
|
|
346
|
+
rawText: processed.rawText,
|
|
347
|
+
files,
|
|
348
|
+
promptFiles: processed.promptFiles,
|
|
349
|
+
handlerOutputs: processed.handlerOutputs,
|
|
276
350
|
inferImageMimeType: guessMediaType,
|
|
277
351
|
});
|
|
352
|
+
};
|
|
278
353
|
}
|
|
279
354
|
|
|
280
355
|
export async function buildTelegramPromptTurn(
|
|
@@ -291,6 +366,8 @@ export async function buildTelegramPromptTurn(
|
|
|
291
366
|
telegramPrefix: options.telegramPrefix,
|
|
292
367
|
rawText: options.rawText,
|
|
293
368
|
files: options.files,
|
|
369
|
+
promptFiles: options.promptFiles,
|
|
370
|
+
handlerOutputs: options.handlerOutputs,
|
|
294
371
|
historyTurns: options.historyTurns,
|
|
295
372
|
}),
|
|
296
373
|
},
|
|
@@ -316,10 +393,15 @@ export async function buildTelegramPromptTurn(
|
|
|
316
393
|
laneOrder: options.queueOrder,
|
|
317
394
|
queuedAttachments: [],
|
|
318
395
|
content,
|
|
319
|
-
historyText: formatTelegramHistoryText(
|
|
396
|
+
historyText: formatTelegramHistoryText(
|
|
397
|
+
options.rawText,
|
|
398
|
+
options.promptFiles ?? options.files,
|
|
399
|
+
options.handlerOutputs,
|
|
400
|
+
),
|
|
320
401
|
statusSummary: formatTelegramTurnStatusSummary(
|
|
321
402
|
options.rawText,
|
|
322
|
-
options.files,
|
|
403
|
+
options.promptFiles ?? options.files,
|
|
404
|
+
options.handlerOutputs,
|
|
323
405
|
),
|
|
324
406
|
};
|
|
325
407
|
}
|