@llblab/pi-telegram 0.2.9 → 0.3.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 +40 -26
- package/docs/architecture.md +62 -35
- package/index.ts +388 -1936
- package/lib/api.ts +647 -76
- package/lib/attachments.ts +128 -16
- package/lib/commands.ts +721 -0
- package/lib/config.ts +157 -0
- package/lib/media.ts +211 -36
- package/lib/menu.ts +920 -338
- package/lib/model.ts +647 -0
- package/lib/pi.ts +80 -0
- package/lib/polling.ts +264 -18
- package/lib/preview.ts +451 -29
- package/lib/queue.ts +1134 -110
- package/lib/registration.ts +127 -28
- package/lib/rendering.ts +575 -281
- package/lib/replies.ts +198 -8
- package/lib/runtime.ts +475 -0
- package/lib/setup.ts +129 -1
- package/lib/status.ts +428 -13
- package/lib/turns.ts +207 -17
- package/lib/updates.ts +392 -99
- package/package.json +18 -3
- package/AGENTS.md +0 -91
- package/BACKLOG.md +0 -5
- package/CHANGELOG.md +0 -23
- package/lib/model-switch.ts +0 -62
- package/tests/api.test.ts +0 -89
- package/tests/attachments.test.ts +0 -132
- package/tests/config.test.ts +0 -80
- package/tests/media.test.ts +0 -77
- package/tests/menu.test.ts +0 -676
- package/tests/polling.test.ts +0 -129
- package/tests/preview.test.ts +0 -441
- package/tests/queue.test.ts +0 -3245
- package/tests/registration.test.ts +0 -268
- package/tests/rendering.test.ts +0 -475
- package/tests/replies.test.ts +0 -142
- package/tests/turns.test.ts +0 -132
- package/tests/updates.test.ts +0 -357
package/lib/runtime.ts
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram bridge runtime-state helpers
|
|
3
|
+
* Owns small session-local runtime primitives that are shared by orchestration but are not specific to queueing, rendering, polling, or Telegram transport
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const TELEGRAM_TYPING_ACTION_INTERVAL_MS = 4000;
|
|
7
|
+
|
|
8
|
+
export interface TelegramRuntimeQueueCounters {
|
|
9
|
+
nextQueuedTelegramItemOrder: number;
|
|
10
|
+
nextQueuedTelegramControlOrder: number;
|
|
11
|
+
nextPriorityReactionOrder: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface TelegramRuntimeLifecycleFlags {
|
|
15
|
+
activeTelegramToolExecutions: number;
|
|
16
|
+
telegramTurnDispatchPending: boolean;
|
|
17
|
+
compactionInProgress: boolean;
|
|
18
|
+
preserveQueuedTurnsAsHistory: boolean;
|
|
19
|
+
setupInProgress: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface TelegramBridgeRuntimeState
|
|
23
|
+
extends TelegramRuntimeQueueCounters, TelegramRuntimeLifecycleFlags {
|
|
24
|
+
abortHandler?: () => void;
|
|
25
|
+
typingInterval?: ReturnType<typeof setInterval>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TelegramRuntimeQueuePort {
|
|
29
|
+
syncCounters: (counters: Partial<TelegramRuntimeQueueCounters>) => void;
|
|
30
|
+
allocateItemOrder: () => number;
|
|
31
|
+
allocateControlOrder: () => number;
|
|
32
|
+
getNextPriorityReactionOrder: () => number;
|
|
33
|
+
incrementNextPriorityReactionOrder: () => void;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface TelegramRuntimeLifecyclePort {
|
|
37
|
+
syncFlags: (flags: Partial<TelegramRuntimeLifecycleFlags>) => void;
|
|
38
|
+
getActiveToolExecutions: () => number;
|
|
39
|
+
setActiveToolExecutions: (count: number) => void;
|
|
40
|
+
resetActiveToolExecutions: () => void;
|
|
41
|
+
hasDispatchPending: () => boolean;
|
|
42
|
+
setDispatchPending: (pending: boolean) => void;
|
|
43
|
+
clearDispatchPending: () => void;
|
|
44
|
+
isCompactionInProgress: () => boolean;
|
|
45
|
+
setCompactionInProgress: (inProgress: boolean) => void;
|
|
46
|
+
shouldPreserveQueuedTurnsAsHistory: () => boolean;
|
|
47
|
+
setPreserveQueuedTurnsAsHistory: (preserve: boolean) => void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TelegramRuntimeSetupPort {
|
|
51
|
+
isInProgress: () => boolean;
|
|
52
|
+
start: () => boolean;
|
|
53
|
+
finish: () => void;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface TelegramRuntimeAbortPort {
|
|
57
|
+
hasHandler: () => boolean;
|
|
58
|
+
setHandler: (abortHandler: () => void) => void;
|
|
59
|
+
clearHandler: () => void;
|
|
60
|
+
getHandler: () => (() => void) | undefined;
|
|
61
|
+
abortTurn: () => boolean;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TelegramRuntimeTypingPort {
|
|
65
|
+
start: (deps: TelegramTypingLoopDeps) => boolean;
|
|
66
|
+
stop: () => boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface TelegramBridgeRuntime {
|
|
70
|
+
state: TelegramBridgeRuntimeState;
|
|
71
|
+
queue: TelegramRuntimeQueuePort;
|
|
72
|
+
lifecycle: TelegramRuntimeLifecyclePort;
|
|
73
|
+
setup: TelegramRuntimeSetupPort;
|
|
74
|
+
abort: TelegramRuntimeAbortPort;
|
|
75
|
+
typing: TelegramRuntimeTypingPort;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function createTelegramBridgeRuntimeState(): TelegramBridgeRuntimeState {
|
|
79
|
+
return {
|
|
80
|
+
nextQueuedTelegramItemOrder: 0,
|
|
81
|
+
nextQueuedTelegramControlOrder: 0,
|
|
82
|
+
nextPriorityReactionOrder: 0,
|
|
83
|
+
activeTelegramToolExecutions: 0,
|
|
84
|
+
telegramTurnDispatchPending: false,
|
|
85
|
+
compactionInProgress: false,
|
|
86
|
+
preserveQueuedTurnsAsHistory: false,
|
|
87
|
+
setupInProgress: false,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function createTelegramBridgeRuntime(
|
|
92
|
+
state = createTelegramBridgeRuntimeState(),
|
|
93
|
+
): TelegramBridgeRuntime {
|
|
94
|
+
return {
|
|
95
|
+
state,
|
|
96
|
+
queue: {
|
|
97
|
+
syncCounters: (counters) =>
|
|
98
|
+
syncTelegramQueueRuntimeCounters(state, counters),
|
|
99
|
+
allocateItemOrder: () => allocateTelegramQueueItemOrder(state),
|
|
100
|
+
allocateControlOrder: () => allocateTelegramQueueControlOrder(state),
|
|
101
|
+
getNextPriorityReactionOrder: () =>
|
|
102
|
+
getNextTelegramPriorityReactionOrder(state),
|
|
103
|
+
incrementNextPriorityReactionOrder: () =>
|
|
104
|
+
incrementNextTelegramPriorityReactionOrder(state),
|
|
105
|
+
},
|
|
106
|
+
lifecycle: {
|
|
107
|
+
syncFlags: (flags) => syncTelegramLifecycleRuntimeFlags(state, flags),
|
|
108
|
+
getActiveToolExecutions: () => getActiveTelegramToolExecutions(state),
|
|
109
|
+
setActiveToolExecutions: (count) =>
|
|
110
|
+
setActiveTelegramToolExecutions(state, count),
|
|
111
|
+
resetActiveToolExecutions: () => resetActiveTelegramToolExecutions(state),
|
|
112
|
+
hasDispatchPending: () => hasTelegramDispatchPending(state),
|
|
113
|
+
setDispatchPending: (pending) =>
|
|
114
|
+
setTelegramDispatchPending(state, pending),
|
|
115
|
+
clearDispatchPending: () => clearTelegramDispatchPending(state),
|
|
116
|
+
isCompactionInProgress: () => isTelegramCompactionInProgress(state),
|
|
117
|
+
setCompactionInProgress: (inProgress) =>
|
|
118
|
+
setTelegramCompactionInProgress(state, inProgress),
|
|
119
|
+
shouldPreserveQueuedTurnsAsHistory: () =>
|
|
120
|
+
shouldPreserveQueuedTurnsAsHistory(state),
|
|
121
|
+
setPreserveQueuedTurnsAsHistory: (preserve) =>
|
|
122
|
+
setPreserveQueuedTurnsAsHistory(state, preserve),
|
|
123
|
+
},
|
|
124
|
+
setup: {
|
|
125
|
+
isInProgress: () => isTelegramSetupInProgress(state),
|
|
126
|
+
start: () => startTelegramSetup(state),
|
|
127
|
+
finish: () => finishTelegramSetup(state),
|
|
128
|
+
},
|
|
129
|
+
abort: {
|
|
130
|
+
hasHandler: () => hasTelegramAbortHandler(state),
|
|
131
|
+
setHandler: (abortHandler) =>
|
|
132
|
+
setTelegramAbortHandler(state, abortHandler),
|
|
133
|
+
clearHandler: () => clearTelegramAbortHandler(state),
|
|
134
|
+
getHandler: () => getTelegramAbortHandler(state),
|
|
135
|
+
abortTurn: () => abortTelegramTurn(state),
|
|
136
|
+
},
|
|
137
|
+
typing: {
|
|
138
|
+
start: (deps) => startTelegramTypingLoop(state, deps),
|
|
139
|
+
stop: () => stopTelegramTypingLoop(state),
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function syncTelegramQueueRuntimeCounters(
|
|
145
|
+
state: TelegramBridgeRuntimeState,
|
|
146
|
+
counters: Partial<TelegramRuntimeQueueCounters>,
|
|
147
|
+
): void {
|
|
148
|
+
if (counters.nextQueuedTelegramItemOrder !== undefined) {
|
|
149
|
+
state.nextQueuedTelegramItemOrder = counters.nextQueuedTelegramItemOrder;
|
|
150
|
+
}
|
|
151
|
+
if (counters.nextQueuedTelegramControlOrder !== undefined) {
|
|
152
|
+
state.nextQueuedTelegramControlOrder =
|
|
153
|
+
counters.nextQueuedTelegramControlOrder;
|
|
154
|
+
}
|
|
155
|
+
if (counters.nextPriorityReactionOrder !== undefined) {
|
|
156
|
+
state.nextPriorityReactionOrder = counters.nextPriorityReactionOrder;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function allocateTelegramQueueItemOrder(
|
|
161
|
+
state: TelegramBridgeRuntimeState,
|
|
162
|
+
): number {
|
|
163
|
+
return state.nextQueuedTelegramItemOrder++;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function allocateTelegramQueueControlOrder(
|
|
167
|
+
state: TelegramBridgeRuntimeState,
|
|
168
|
+
): number {
|
|
169
|
+
return state.nextQueuedTelegramControlOrder++;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function getNextTelegramPriorityReactionOrder(
|
|
173
|
+
state: TelegramBridgeRuntimeState,
|
|
174
|
+
): number {
|
|
175
|
+
return state.nextPriorityReactionOrder;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function incrementNextTelegramPriorityReactionOrder(
|
|
179
|
+
state: TelegramBridgeRuntimeState,
|
|
180
|
+
): void {
|
|
181
|
+
state.nextPriorityReactionOrder += 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export function syncTelegramLifecycleRuntimeFlags(
|
|
185
|
+
state: TelegramBridgeRuntimeState,
|
|
186
|
+
flags: Partial<TelegramRuntimeLifecycleFlags>,
|
|
187
|
+
): void {
|
|
188
|
+
if (flags.activeTelegramToolExecutions !== undefined) {
|
|
189
|
+
state.activeTelegramToolExecutions = flags.activeTelegramToolExecutions;
|
|
190
|
+
}
|
|
191
|
+
if (flags.telegramTurnDispatchPending !== undefined) {
|
|
192
|
+
state.telegramTurnDispatchPending = flags.telegramTurnDispatchPending;
|
|
193
|
+
}
|
|
194
|
+
if (flags.compactionInProgress !== undefined) {
|
|
195
|
+
state.compactionInProgress = flags.compactionInProgress;
|
|
196
|
+
}
|
|
197
|
+
if (flags.preserveQueuedTurnsAsHistory !== undefined) {
|
|
198
|
+
state.preserveQueuedTurnsAsHistory = flags.preserveQueuedTurnsAsHistory;
|
|
199
|
+
}
|
|
200
|
+
if (flags.setupInProgress !== undefined) {
|
|
201
|
+
state.setupInProgress = flags.setupInProgress;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function getActiveTelegramToolExecutions(
|
|
206
|
+
state: TelegramBridgeRuntimeState,
|
|
207
|
+
): number {
|
|
208
|
+
return state.activeTelegramToolExecutions;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function setActiveTelegramToolExecutions(
|
|
212
|
+
state: TelegramBridgeRuntimeState,
|
|
213
|
+
count: number,
|
|
214
|
+
): void {
|
|
215
|
+
state.activeTelegramToolExecutions = count;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
export function resetActiveTelegramToolExecutions(
|
|
219
|
+
state: TelegramBridgeRuntimeState,
|
|
220
|
+
): void {
|
|
221
|
+
state.activeTelegramToolExecutions = 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function hasTelegramDispatchPending(
|
|
225
|
+
state: TelegramBridgeRuntimeState,
|
|
226
|
+
): boolean {
|
|
227
|
+
return state.telegramTurnDispatchPending;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function setTelegramDispatchPending(
|
|
231
|
+
state: TelegramBridgeRuntimeState,
|
|
232
|
+
pending: boolean,
|
|
233
|
+
): void {
|
|
234
|
+
state.telegramTurnDispatchPending = pending;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export function clearTelegramDispatchPending(
|
|
238
|
+
state: TelegramBridgeRuntimeState,
|
|
239
|
+
): void {
|
|
240
|
+
state.telegramTurnDispatchPending = false;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function isTelegramCompactionInProgress(
|
|
244
|
+
state: TelegramBridgeRuntimeState,
|
|
245
|
+
): boolean {
|
|
246
|
+
return state.compactionInProgress;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function setTelegramCompactionInProgress(
|
|
250
|
+
state: TelegramBridgeRuntimeState,
|
|
251
|
+
inProgress: boolean,
|
|
252
|
+
): void {
|
|
253
|
+
state.compactionInProgress = inProgress;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
export function shouldPreserveQueuedTurnsAsHistory(
|
|
257
|
+
state: TelegramBridgeRuntimeState,
|
|
258
|
+
): boolean {
|
|
259
|
+
return state.preserveQueuedTurnsAsHistory;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export function setPreserveQueuedTurnsAsHistory(
|
|
263
|
+
state: TelegramBridgeRuntimeState,
|
|
264
|
+
preserve: boolean,
|
|
265
|
+
): void {
|
|
266
|
+
state.preserveQueuedTurnsAsHistory = preserve;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function isTelegramSetupInProgress(
|
|
270
|
+
state: TelegramBridgeRuntimeState,
|
|
271
|
+
): boolean {
|
|
272
|
+
return state.setupInProgress;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export function startTelegramSetup(state: TelegramBridgeRuntimeState): boolean {
|
|
276
|
+
if (state.setupInProgress) return false;
|
|
277
|
+
state.setupInProgress = true;
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function finishTelegramSetup(state: TelegramBridgeRuntimeState): void {
|
|
282
|
+
state.setupInProgress = false;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export function hasTelegramAbortHandler(
|
|
286
|
+
state: TelegramBridgeRuntimeState,
|
|
287
|
+
): boolean {
|
|
288
|
+
return !!state.abortHandler;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function setTelegramAbortHandler(
|
|
292
|
+
state: TelegramBridgeRuntimeState,
|
|
293
|
+
abortHandler: () => void,
|
|
294
|
+
): void {
|
|
295
|
+
state.abortHandler = abortHandler;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
export function clearTelegramAbortHandler(
|
|
299
|
+
state: TelegramBridgeRuntimeState,
|
|
300
|
+
): void {
|
|
301
|
+
state.abortHandler = undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
export function getTelegramAbortHandler(
|
|
305
|
+
state: TelegramBridgeRuntimeState,
|
|
306
|
+
): (() => void) | undefined {
|
|
307
|
+
return state.abortHandler;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
export function abortTelegramTurn(state: TelegramBridgeRuntimeState): boolean {
|
|
311
|
+
if (!state.abortHandler) return false;
|
|
312
|
+
state.abortHandler();
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
export interface TelegramTypingLoopDeps {
|
|
317
|
+
chatId: number | undefined;
|
|
318
|
+
intervalMs: number;
|
|
319
|
+
sendTypingAction: (chatId: number) => Promise<unknown>;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
export interface TelegramRuntimeEventRecorderPort {
|
|
323
|
+
recordRuntimeEvent?: (
|
|
324
|
+
category: string,
|
|
325
|
+
error: unknown,
|
|
326
|
+
details?: Record<string, unknown>,
|
|
327
|
+
) => void;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
export interface TelegramTypingLoopStarterDeps<TContext>
|
|
331
|
+
extends TelegramRuntimeEventRecorderPort {
|
|
332
|
+
typing: TelegramRuntimeTypingPort;
|
|
333
|
+
getDefaultChatId: () => number | undefined;
|
|
334
|
+
sendTypingAction: (chatId: number) => Promise<unknown>;
|
|
335
|
+
updateStatus: (ctx: TContext, error?: string) => void;
|
|
336
|
+
intervalMs?: number;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
export function createTelegramTypingLoopStarter<TContext>(
|
|
340
|
+
deps: TelegramTypingLoopStarterDeps<TContext>,
|
|
341
|
+
): (ctx: TContext, chatId?: number) => void {
|
|
342
|
+
return (ctx, chatId) => {
|
|
343
|
+
deps.typing.start({
|
|
344
|
+
chatId: chatId ?? deps.getDefaultChatId(),
|
|
345
|
+
intervalMs: deps.intervalMs ?? TELEGRAM_TYPING_ACTION_INTERVAL_MS,
|
|
346
|
+
sendTypingAction: async (targetChatId) => {
|
|
347
|
+
try {
|
|
348
|
+
await deps.sendTypingAction(targetChatId);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
const message =
|
|
351
|
+
error instanceof Error ? error.message : String(error);
|
|
352
|
+
deps.recordRuntimeEvent?.("typing", error, {
|
|
353
|
+
chatId: targetChatId,
|
|
354
|
+
});
|
|
355
|
+
deps.updateStatus(ctx, `typing failed: ${message}`);
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
});
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
export function startTelegramTypingLoop(
|
|
363
|
+
state: TelegramBridgeRuntimeState,
|
|
364
|
+
deps: TelegramTypingLoopDeps,
|
|
365
|
+
): boolean {
|
|
366
|
+
if (state.typingInterval || deps.chatId === undefined) return false;
|
|
367
|
+
const sendTyping = (): void => {
|
|
368
|
+
void deps.sendTypingAction(deps.chatId as number);
|
|
369
|
+
};
|
|
370
|
+
sendTyping();
|
|
371
|
+
state.typingInterval = setInterval(sendTyping, deps.intervalMs);
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
export function stopTelegramTypingLoop(
|
|
376
|
+
state: TelegramBridgeRuntimeState,
|
|
377
|
+
): boolean {
|
|
378
|
+
if (!state.typingInterval) return false;
|
|
379
|
+
clearInterval(state.typingInterval);
|
|
380
|
+
state.typingInterval = undefined;
|
|
381
|
+
return true;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
export function createTelegramContextAbortHandlerSetter<
|
|
385
|
+
TContext extends { abort: () => void },
|
|
386
|
+
>(
|
|
387
|
+
abort: Pick<TelegramRuntimeAbortPort, "setHandler">,
|
|
388
|
+
): (ctx: TContext) => void {
|
|
389
|
+
return (ctx) => {
|
|
390
|
+
abort.setHandler(() => ctx.abort());
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
export interface TelegramAgentEndResetDeps {
|
|
395
|
+
abort: Pick<TelegramRuntimeAbortPort, "clearHandler">;
|
|
396
|
+
typing: Pick<TelegramRuntimeTypingPort, "stop">;
|
|
397
|
+
clearActiveTurn: () => void;
|
|
398
|
+
resetToolExecutions: () => void;
|
|
399
|
+
clearPendingModelSwitch: () => void;
|
|
400
|
+
clearDispatchPending: () => void;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function createTelegramAgentEndResetter(
|
|
404
|
+
deps: TelegramAgentEndResetDeps,
|
|
405
|
+
): () => void {
|
|
406
|
+
return () => {
|
|
407
|
+
deps.abort.clearHandler();
|
|
408
|
+
deps.typing.stop();
|
|
409
|
+
deps.clearActiveTurn();
|
|
410
|
+
deps.resetToolExecutions();
|
|
411
|
+
deps.clearPendingModelSwitch();
|
|
412
|
+
deps.clearDispatchPending();
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export interface TelegramPromptDispatchLifecycleDeps<TContext>
|
|
417
|
+
extends TelegramRuntimeEventRecorderPort {
|
|
418
|
+
lifecycle: Pick<
|
|
419
|
+
TelegramRuntimeLifecyclePort,
|
|
420
|
+
"setDispatchPending" | "clearDispatchPending"
|
|
421
|
+
>;
|
|
422
|
+
typing: Pick<TelegramRuntimeTypingPort, "stop">;
|
|
423
|
+
startTypingLoop: (ctx: TContext, chatId?: number) => void;
|
|
424
|
+
updateStatus: (ctx: TContext, error?: string) => void;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
export interface TelegramPromptDispatchRuntimeDeps<TContext>
|
|
428
|
+
extends TelegramRuntimeEventRecorderPort {
|
|
429
|
+
lifecycle: TelegramPromptDispatchLifecycleDeps<TContext>["lifecycle"];
|
|
430
|
+
typing: TelegramRuntimeTypingPort;
|
|
431
|
+
getDefaultChatId: () => number | undefined;
|
|
432
|
+
sendTypingAction: (chatId: number) => Promise<unknown>;
|
|
433
|
+
updateStatus: (ctx: TContext, error?: string) => void;
|
|
434
|
+
intervalMs?: number;
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
export interface TelegramPromptDispatchRuntime<TContext> {
|
|
438
|
+
startTypingLoop: (ctx: TContext, chatId?: number) => void;
|
|
439
|
+
onPromptDispatchStart: (ctx: TContext, chatId?: number) => void;
|
|
440
|
+
onPromptDispatchFailure: (ctx: TContext, message: string) => void;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
export function createTelegramPromptDispatchRuntime<TContext>(
|
|
444
|
+
deps: TelegramPromptDispatchRuntimeDeps<TContext>,
|
|
445
|
+
): TelegramPromptDispatchRuntime<TContext> {
|
|
446
|
+
const startTypingLoop = createTelegramTypingLoopStarter(deps);
|
|
447
|
+
return {
|
|
448
|
+
startTypingLoop,
|
|
449
|
+
...createTelegramPromptDispatchLifecycle({
|
|
450
|
+
lifecycle: deps.lifecycle,
|
|
451
|
+
typing: deps.typing,
|
|
452
|
+
startTypingLoop,
|
|
453
|
+
updateStatus: deps.updateStatus,
|
|
454
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
455
|
+
}),
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
export function createTelegramPromptDispatchLifecycle<TContext>(
|
|
460
|
+
deps: TelegramPromptDispatchLifecycleDeps<TContext>,
|
|
461
|
+
) {
|
|
462
|
+
return {
|
|
463
|
+
onPromptDispatchStart: (ctx: TContext, chatId?: number): void => {
|
|
464
|
+
deps.lifecycle.setDispatchPending(true);
|
|
465
|
+
deps.startTypingLoop(ctx, chatId);
|
|
466
|
+
deps.updateStatus(ctx);
|
|
467
|
+
},
|
|
468
|
+
onPromptDispatchFailure: (ctx: TContext, message: string): void => {
|
|
469
|
+
deps.lifecycle.clearDispatchPending();
|
|
470
|
+
deps.typing.stop();
|
|
471
|
+
deps.recordRuntimeEvent?.("dispatch", new Error(message));
|
|
472
|
+
deps.updateStatus(ctx, `dispatch failed: ${message}`);
|
|
473
|
+
},
|
|
474
|
+
};
|
|
475
|
+
}
|
package/lib/setup.ts
CHANGED
|
@@ -3,13 +3,75 @@
|
|
|
3
3
|
* Computes token-prefill defaults and prompt mode selection for /telegram-setup
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export interface TelegramSetupConfig {
|
|
7
|
+
botToken?: string;
|
|
8
|
+
botId?: number;
|
|
9
|
+
botUsername?: string;
|
|
10
|
+
allowedUserId?: number;
|
|
11
|
+
lastUpdateId?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
6
14
|
export interface TelegramBotTokenPromptSpec {
|
|
7
15
|
method: "input" | "editor";
|
|
8
16
|
value: string;
|
|
9
17
|
}
|
|
10
18
|
|
|
19
|
+
export interface TelegramSetupUser {
|
|
20
|
+
id: number;
|
|
21
|
+
username?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface TelegramSetupDeps {
|
|
25
|
+
hasUI: boolean;
|
|
26
|
+
env: NodeJS.ProcessEnv;
|
|
27
|
+
config: TelegramSetupConfig;
|
|
28
|
+
promptInput: (label: string, value: string) => Promise<string | undefined>;
|
|
29
|
+
promptEditor: (label: string, value: string) => Promise<string | undefined>;
|
|
30
|
+
getMe: (botToken: string) => Promise<{
|
|
31
|
+
ok: boolean;
|
|
32
|
+
result?: TelegramSetupUser;
|
|
33
|
+
description?: string;
|
|
34
|
+
}>;
|
|
35
|
+
persistConfig: (config: TelegramSetupConfig) => Promise<void>;
|
|
36
|
+
notify: (message: string, level: "info" | "error") => void;
|
|
37
|
+
startPolling: () => void | Promise<void>;
|
|
38
|
+
updateStatus: () => void;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface TelegramSetupPromptContext {
|
|
42
|
+
hasUI: boolean;
|
|
43
|
+
ui: {
|
|
44
|
+
input: (label: string, value: string) => Promise<string | undefined>;
|
|
45
|
+
editor: (label: string, value: string) => Promise<string | undefined>;
|
|
46
|
+
notify: (message: string, level: "info" | "error") => void;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface TelegramSetupGuard {
|
|
51
|
+
start: () => boolean;
|
|
52
|
+
finish: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface TelegramSetupPromptRuntimeDeps<
|
|
56
|
+
TContext extends TelegramSetupPromptContext,
|
|
57
|
+
> {
|
|
58
|
+
env?: NodeJS.ProcessEnv;
|
|
59
|
+
getConfig: () => TelegramSetupConfig;
|
|
60
|
+
setConfig: (config: TelegramSetupConfig) => void;
|
|
61
|
+
setupGuard: TelegramSetupGuard;
|
|
62
|
+
getMe: TelegramSetupDeps["getMe"];
|
|
63
|
+
persistConfig: (config: TelegramSetupConfig) => Promise<void>;
|
|
64
|
+
startPolling: (ctx: TContext) => void | Promise<void>;
|
|
65
|
+
updateStatus: (ctx: TContext) => void;
|
|
66
|
+
recordRuntimeEvent?: (
|
|
67
|
+
category: string,
|
|
68
|
+
error: unknown,
|
|
69
|
+
details?: Record<string, unknown>,
|
|
70
|
+
) => void;
|
|
71
|
+
}
|
|
72
|
+
|
|
11
73
|
export const TELEGRAM_BOT_TOKEN_INPUT_PLACEHOLDER = "123456:ABCDEF...";
|
|
12
|
-
|
|
74
|
+
const TELEGRAM_BOT_TOKEN_ENV_VARS = [
|
|
13
75
|
"TELEGRAM_BOT_TOKEN",
|
|
14
76
|
"TELEGRAM_BOT_KEY",
|
|
15
77
|
"TELEGRAM_TOKEN",
|
|
@@ -39,3 +101,69 @@ export function getTelegramBotTokenPromptSpec(
|
|
|
39
101
|
value,
|
|
40
102
|
};
|
|
41
103
|
}
|
|
104
|
+
|
|
105
|
+
export async function runTelegramSetup(
|
|
106
|
+
deps: TelegramSetupDeps,
|
|
107
|
+
): Promise<TelegramSetupConfig | undefined> {
|
|
108
|
+
if (!deps.hasUI) return undefined;
|
|
109
|
+
const tokenPrompt = getTelegramBotTokenPromptSpec(
|
|
110
|
+
deps.env,
|
|
111
|
+
deps.config.botToken,
|
|
112
|
+
);
|
|
113
|
+
const token =
|
|
114
|
+
tokenPrompt.method === "editor"
|
|
115
|
+
? await deps.promptEditor("Telegram bot token", tokenPrompt.value)
|
|
116
|
+
: await deps.promptInput("Telegram bot token", tokenPrompt.value);
|
|
117
|
+
if (!token) return undefined;
|
|
118
|
+
const nextConfig: TelegramSetupConfig = {
|
|
119
|
+
...deps.config,
|
|
120
|
+
botToken: token.trim(),
|
|
121
|
+
};
|
|
122
|
+
const data = await deps.getMe(nextConfig.botToken ?? "");
|
|
123
|
+
if (!data.ok || !data.result) {
|
|
124
|
+
deps.notify(data.description || "Invalid Telegram bot token", "error");
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
nextConfig.botId = data.result.id;
|
|
128
|
+
nextConfig.botUsername = data.result.username;
|
|
129
|
+
await deps.persistConfig(nextConfig);
|
|
130
|
+
deps.notify(
|
|
131
|
+
`Telegram bot connected: @${nextConfig.botUsername ?? "unknown"}`,
|
|
132
|
+
"info",
|
|
133
|
+
);
|
|
134
|
+
deps.notify(
|
|
135
|
+
"Send /start to your bot in Telegram to pair this extension with your account.",
|
|
136
|
+
"info",
|
|
137
|
+
);
|
|
138
|
+
await deps.startPolling();
|
|
139
|
+
deps.updateStatus();
|
|
140
|
+
return nextConfig;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function createTelegramSetupPromptRuntime<
|
|
144
|
+
TContext extends TelegramSetupPromptContext,
|
|
145
|
+
>(deps: TelegramSetupPromptRuntimeDeps<TContext>) {
|
|
146
|
+
return async function promptForConfig(ctx: TContext): Promise<void> {
|
|
147
|
+
if (!ctx.hasUI || !deps.setupGuard.start()) return;
|
|
148
|
+
try {
|
|
149
|
+
const nextConfig = await runTelegramSetup({
|
|
150
|
+
hasUI: ctx.hasUI,
|
|
151
|
+
env: deps.env ?? process.env,
|
|
152
|
+
config: deps.getConfig(),
|
|
153
|
+
promptInput: (label, value) => ctx.ui.input(label, value),
|
|
154
|
+
promptEditor: (label, value) => ctx.ui.editor(label, value),
|
|
155
|
+
getMe: deps.getMe,
|
|
156
|
+
persistConfig: deps.persistConfig,
|
|
157
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
158
|
+
startPolling: () => deps.startPolling(ctx),
|
|
159
|
+
updateStatus: () => deps.updateStatus(ctx),
|
|
160
|
+
});
|
|
161
|
+
if (nextConfig) deps.setConfig(nextConfig);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
deps.recordRuntimeEvent?.("setup", error);
|
|
164
|
+
throw error;
|
|
165
|
+
} finally {
|
|
166
|
+
deps.setupGuard.finish();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
}
|