@llblab/pi-telegram 0.21.1 → 0.22.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 +7 -4
- package/BACKLOG.md +29 -1
- package/CHANGELOG.md +479 -452
- package/docs/architecture.md +12 -10
- package/docs/delivery.md +2 -1
- package/docs/locks.md +13 -5
- package/docs/multi-instance-bus.md +5 -5
- package/index.ts +311 -633
- package/lib/bindings.ts +59 -21
- package/lib/bus-api.ts +12 -2
- package/lib/bus-follower.ts +327 -83
- package/lib/bus-leader.ts +201 -121
- package/lib/bus.ts +345 -37
- package/lib/config.ts +168 -28
- package/lib/delivery.ts +148 -61
- package/lib/lifecycle.ts +199 -8
- package/lib/locks.ts +854 -57
- package/lib/logs.ts +185 -22
- package/lib/media.ts +79 -24
- package/lib/model.ts +5 -10
- package/lib/ownership.ts +150 -6
- package/lib/polling.ts +156 -7
- package/lib/preview.ts +15 -3
- package/lib/queue.ts +167 -20
- package/lib/routing.ts +68 -12
- package/lib/sync.ts +105 -32
- package/lib/telegram-api.ts +86 -10
- package/lib/text-groups.ts +71 -15
- package/lib/thread-reconciler.ts +49 -36
- package/lib/threads.ts +505 -118
- package/package.json +1 -1
package/lib/logs.ts
CHANGED
|
@@ -10,13 +10,15 @@ import {
|
|
|
10
10
|
mkdirSync,
|
|
11
11
|
statSync,
|
|
12
12
|
writeFileSync,
|
|
13
|
-
|
|
13
|
+
appendFileSync,
|
|
14
14
|
} from "node:fs";
|
|
15
15
|
import { dirname } from "node:path";
|
|
16
16
|
import {
|
|
17
17
|
resolveAgentDir,
|
|
18
18
|
resolveTelegramProfileTempFilePath,
|
|
19
19
|
} from "./paths.ts";
|
|
20
|
+
import { withTelegramFileTransaction } from "./locks.ts";
|
|
21
|
+
import * as Status from "./status.ts";
|
|
20
22
|
|
|
21
23
|
export type TelegramLogPathInput = string | (() => string);
|
|
22
24
|
|
|
@@ -32,6 +34,8 @@ export interface TelegramRuntimeJsonlLogOptions {
|
|
|
32
34
|
previousPath?: TelegramLogPathInput;
|
|
33
35
|
maxBytes?: number;
|
|
34
36
|
getNowMs?: () => number;
|
|
37
|
+
canReset?: () => boolean;
|
|
38
|
+
commitReset?: (commit: () => void) => boolean;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export interface TelegramRuntimeJsonlLog {
|
|
@@ -89,7 +93,8 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
89
93
|
? options.path()
|
|
90
94
|
: (options.path ?? getTelegramRuntimeLogPath());
|
|
91
95
|
const resolvePreviousPath = () => {
|
|
92
|
-
if (typeof options.previousPath === "function")
|
|
96
|
+
if (typeof options.previousPath === "function")
|
|
97
|
+
return options.previousPath();
|
|
93
98
|
if (options.previousPath) return options.previousPath;
|
|
94
99
|
return resolvePath().replace(/\.jsonl$/u, "._prev.jsonl");
|
|
95
100
|
};
|
|
@@ -108,9 +113,12 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
108
113
|
copyFileSync(path, previousPath);
|
|
109
114
|
};
|
|
110
115
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
116
|
+
const writeResetLocked = (
|
|
117
|
+
path: string,
|
|
118
|
+
previousPath: string,
|
|
119
|
+
reason: string,
|
|
120
|
+
scope?: Record<string, unknown>,
|
|
121
|
+
) => {
|
|
114
122
|
ensureParent(path);
|
|
115
123
|
preserveCurrentLog(path, previousPath);
|
|
116
124
|
writeFileSync(
|
|
@@ -126,31 +134,61 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
126
134
|
);
|
|
127
135
|
};
|
|
128
136
|
|
|
137
|
+
const writeReset = (
|
|
138
|
+
reason: string,
|
|
139
|
+
scope?: Record<string, unknown>,
|
|
140
|
+
): boolean => {
|
|
141
|
+
if (options.canReset && !options.canReset()) return false;
|
|
142
|
+
const path = resolvePath();
|
|
143
|
+
let didReset = false;
|
|
144
|
+
withTelegramFileTransaction(`${path}.transaction`, () => {
|
|
145
|
+
const commit = () => {
|
|
146
|
+
writeResetLocked(path, resolvePreviousPath(), reason, scope);
|
|
147
|
+
didReset = true;
|
|
148
|
+
};
|
|
149
|
+
if (options.commitReset) {
|
|
150
|
+
options.commitReset(commit);
|
|
151
|
+
} else {
|
|
152
|
+
commit();
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return didReset;
|
|
156
|
+
};
|
|
157
|
+
|
|
129
158
|
const appendLine = (line: string) => {
|
|
159
|
+
const path = resolvePath();
|
|
160
|
+
const previousPath = resolvePreviousPath();
|
|
130
161
|
pending = pending
|
|
131
|
-
.
|
|
132
|
-
.then(async () => {
|
|
133
|
-
const path = resolvePath();
|
|
162
|
+
.then(() => {
|
|
134
163
|
ensureParent(path);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
164
|
+
withTelegramFileTransaction(`${path}.transaction`, () => {
|
|
165
|
+
if (
|
|
166
|
+
existsSync(path) &&
|
|
167
|
+
statSync(path).size > maxBytes &&
|
|
168
|
+
(!options.canReset || options.canReset())
|
|
169
|
+
) {
|
|
170
|
+
const rotate = () =>
|
|
171
|
+
writeResetLocked(path, previousPath, "max-bytes", { maxBytes });
|
|
172
|
+
if (options.commitReset) {
|
|
173
|
+
options.commitReset(rotate);
|
|
174
|
+
} else {
|
|
175
|
+
rotate();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
appendFileSync(path, line, { mode: 0o600 });
|
|
143
179
|
});
|
|
144
|
-
})
|
|
180
|
+
})
|
|
181
|
+
.catch(() => undefined);
|
|
145
182
|
};
|
|
146
183
|
|
|
147
184
|
return {
|
|
148
185
|
getPath: resolvePath,
|
|
149
186
|
reset(reason, scope) {
|
|
150
187
|
const path = resolvePath();
|
|
151
|
-
scopeKeys.set(path, scope ? safeJsonLine(scope) : undefined);
|
|
152
188
|
try {
|
|
153
|
-
writeReset(reason, scope)
|
|
189
|
+
if (writeReset(reason, scope)) {
|
|
190
|
+
scopeKeys.set(path, scope ? safeJsonLine(scope) : undefined);
|
|
191
|
+
}
|
|
154
192
|
} catch {
|
|
155
193
|
// Diagnostics must never break Telegram runtime behavior.
|
|
156
194
|
}
|
|
@@ -158,15 +196,140 @@ export function createTelegramRuntimeJsonlLog(
|
|
|
158
196
|
resetIfScopeChanged(nextScopeKey, reason, scope) {
|
|
159
197
|
const path = resolvePath();
|
|
160
198
|
if (scopeKeys.get(path) === nextScopeKey) return;
|
|
161
|
-
scopeKeys.set(path, nextScopeKey);
|
|
162
199
|
try {
|
|
163
|
-
writeReset(reason, scope);
|
|
200
|
+
if (writeReset(reason, scope)) scopeKeys.set(path, nextScopeKey);
|
|
164
201
|
} catch {
|
|
165
202
|
// Diagnostics must never break Telegram runtime behavior.
|
|
166
203
|
}
|
|
167
204
|
},
|
|
168
205
|
record(event) {
|
|
169
|
-
|
|
206
|
+
try {
|
|
207
|
+
appendLine(safeJsonLine({ kind: "event", ...event }) + "\n");
|
|
208
|
+
} catch {
|
|
209
|
+
// Diagnostics must never break Telegram runtime behavior.
|
|
210
|
+
}
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export interface TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
216
|
+
events: Status.TelegramRuntimeEventRecorder;
|
|
217
|
+
recordRuntimeEvent(
|
|
218
|
+
category: string,
|
|
219
|
+
error: unknown,
|
|
220
|
+
details?: Record<string, unknown>,
|
|
221
|
+
): void;
|
|
222
|
+
bindStorage(ports: {
|
|
223
|
+
getBotToken(): string | undefined;
|
|
224
|
+
getProfileName(): string | undefined;
|
|
225
|
+
canReset(): boolean;
|
|
226
|
+
commitReset(commit: () => void): boolean;
|
|
227
|
+
}): void;
|
|
228
|
+
bindStatus(ports: {
|
|
229
|
+
instanceId: string;
|
|
230
|
+
updateStatus(ctx: TContext, error?: string): void;
|
|
231
|
+
getStatusState(): Status.TelegramBridgeStatusLineState;
|
|
232
|
+
persistSnapshot(
|
|
233
|
+
snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
|
|
234
|
+
): Promise<void>;
|
|
235
|
+
}): void;
|
|
236
|
+
updateStatus(ctx: TContext, error?: string): void;
|
|
237
|
+
getStatusLines(options?: Status.TelegramBridgeStatusLineOptions): string[];
|
|
238
|
+
scheduleSnapshotPersist(): void;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export function createTelegramRuntimeDiagnosticsRuntime<
|
|
242
|
+
TContext,
|
|
243
|
+
>(): TelegramRuntimeDiagnosticsRuntime<TContext> {
|
|
244
|
+
let getBotToken = (): string | undefined => undefined;
|
|
245
|
+
let getProfileName = (): string | undefined => undefined;
|
|
246
|
+
let canReset = (): boolean => false;
|
|
247
|
+
let commitReset = (_commit: () => void): boolean => false;
|
|
248
|
+
let statusPorts:
|
|
249
|
+
| {
|
|
250
|
+
instanceId: string;
|
|
251
|
+
updateStatus(ctx: TContext, error?: string): void;
|
|
252
|
+
getStatusState(): Status.TelegramBridgeStatusLineState;
|
|
253
|
+
persistSnapshot(
|
|
254
|
+
snapshot: ReturnType<typeof Status.createTelegramStatusSnapshot>,
|
|
255
|
+
): Promise<void>;
|
|
256
|
+
}
|
|
257
|
+
| undefined;
|
|
258
|
+
let requestSnapshotPersist = (): void => {};
|
|
259
|
+
const events = Status.createTelegramRuntimeEventRecorder({
|
|
260
|
+
getBotToken: () => getBotToken(),
|
|
261
|
+
});
|
|
262
|
+
const jsonl = createTelegramRuntimeJsonlLog({
|
|
263
|
+
path: () => getTelegramRuntimeLogPath(undefined, getProfileName()),
|
|
264
|
+
previousPath: () =>
|
|
265
|
+
getTelegramPreviousRuntimeLogPath(undefined, getProfileName()),
|
|
266
|
+
canReset: () => canReset(),
|
|
267
|
+
commitReset: (commit) => commitReset(commit),
|
|
268
|
+
});
|
|
269
|
+
const recordRuntimeEvent = function (
|
|
270
|
+
category: string,
|
|
271
|
+
error: unknown,
|
|
272
|
+
details?: Record<string, unknown>,
|
|
273
|
+
): void {
|
|
274
|
+
events.record(category, error, details);
|
|
275
|
+
const latestEvent = events.getEvents().at(-1);
|
|
276
|
+
if (latestEvent) jsonl.record(latestEvent);
|
|
277
|
+
requestSnapshotPersist();
|
|
278
|
+
};
|
|
279
|
+
const persistCurrentSnapshot = async (): Promise<void> => {
|
|
280
|
+
if (!statusPorts) return;
|
|
281
|
+
await statusPorts.persistSnapshot(
|
|
282
|
+
Status.createTelegramStatusSnapshot(statusPorts.getStatusState()),
|
|
283
|
+
);
|
|
284
|
+
};
|
|
285
|
+
const updateRuntimeLogScope = function (reason: string): void {
|
|
286
|
+
if (!statusPorts) return;
|
|
287
|
+
const scope = Status.createTelegramRuntimeLogScope({
|
|
288
|
+
state: statusPorts.getStatusState(),
|
|
289
|
+
instanceId: statusPorts.instanceId,
|
|
290
|
+
});
|
|
291
|
+
jsonl.resetIfScopeChanged(JSON.stringify(scope), reason, scope);
|
|
292
|
+
};
|
|
293
|
+
return {
|
|
294
|
+
events,
|
|
295
|
+
recordRuntimeEvent,
|
|
296
|
+
bindStorage(ports) {
|
|
297
|
+
getBotToken = ports.getBotToken;
|
|
298
|
+
getProfileName = ports.getProfileName;
|
|
299
|
+
canReset = ports.canReset;
|
|
300
|
+
commitReset = ports.commitReset;
|
|
301
|
+
},
|
|
302
|
+
bindStatus(ports) {
|
|
303
|
+
statusPorts = ports;
|
|
304
|
+
requestSnapshotPersist =
|
|
305
|
+
Status.createTelegramRuntimeDiagnosticsSnapshotScheduler({
|
|
306
|
+
persistSnapshot: persistCurrentSnapshot,
|
|
307
|
+
recordError(error) {
|
|
308
|
+
events.record("telegram", error, {
|
|
309
|
+
phase: "runtime-diagnostics-snapshot-persist",
|
|
310
|
+
});
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
},
|
|
314
|
+
updateStatus(ctx, error) {
|
|
315
|
+
if (!statusPorts) return;
|
|
316
|
+
statusPorts.updateStatus(ctx, error);
|
|
317
|
+
updateRuntimeLogScope("status-scope-change");
|
|
318
|
+
},
|
|
319
|
+
getStatusLines(options) {
|
|
320
|
+
if (!statusPorts) return [];
|
|
321
|
+
void persistCurrentSnapshot().catch((error) => {
|
|
322
|
+
recordRuntimeEvent("telegram", error, {
|
|
323
|
+
phase: "status-snapshot-persist",
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
return Status.buildTelegramBridgeStatusLines(
|
|
327
|
+
statusPorts.getStatusState(),
|
|
328
|
+
options,
|
|
329
|
+
);
|
|
330
|
+
},
|
|
331
|
+
scheduleSnapshotPersist() {
|
|
332
|
+
requestSnapshotPersist();
|
|
170
333
|
},
|
|
171
334
|
};
|
|
172
335
|
}
|
package/lib/media.ts
CHANGED
|
@@ -100,6 +100,9 @@ export interface TelegramMediaGroupState<TMessage, TContext = unknown> {
|
|
|
100
100
|
messages: TMessage[];
|
|
101
101
|
context?: TContext;
|
|
102
102
|
flushTimer?: ReturnType<typeof setTimeout>;
|
|
103
|
+
dispatching?: boolean;
|
|
104
|
+
suspended?: boolean;
|
|
105
|
+
reschedule?: () => void;
|
|
103
106
|
}
|
|
104
107
|
|
|
105
108
|
export interface TelegramMediaGroupController<
|
|
@@ -109,9 +112,14 @@ export interface TelegramMediaGroupController<
|
|
|
109
112
|
queueMessage: (options: {
|
|
110
113
|
message: TMessage;
|
|
111
114
|
context?: TContext;
|
|
112
|
-
dispatchMessages: (
|
|
115
|
+
dispatchMessages: (
|
|
116
|
+
messages: TMessage[],
|
|
117
|
+
ctx?: TContext,
|
|
118
|
+
) => unknown | Promise<unknown>;
|
|
113
119
|
}) => boolean;
|
|
114
120
|
removeMessages: (messageIds: number[]) => number;
|
|
121
|
+
suspend: () => void;
|
|
122
|
+
resume: (context: TContext) => void;
|
|
115
123
|
clear: () => void;
|
|
116
124
|
}
|
|
117
125
|
|
|
@@ -140,13 +148,7 @@ export interface TelegramMediaGroupControllerOptions {
|
|
|
140
148
|
}
|
|
141
149
|
|
|
142
150
|
export type TelegramAttachmentKind =
|
|
143
|
-
| "
|
|
144
|
-
| "document"
|
|
145
|
-
| "video"
|
|
146
|
-
| "audio"
|
|
147
|
-
| "voice"
|
|
148
|
-
| "animation"
|
|
149
|
-
| "sticker";
|
|
151
|
+
"photo" | "document" | "video" | "audio" | "voice" | "animation" | "sticker";
|
|
150
152
|
|
|
151
153
|
export interface TelegramFileInfo {
|
|
152
154
|
file_id: string;
|
|
@@ -306,11 +308,16 @@ function truncateTelegramReplyContextText(text: string): string {
|
|
|
306
308
|
return `${text.slice(0, TELEGRAM_REPLY_CONTEXT_MAX_LENGTH).trimEnd()}…`;
|
|
307
309
|
}
|
|
308
310
|
|
|
309
|
-
function formatTelegramUser(
|
|
311
|
+
function formatTelegramUser(
|
|
312
|
+
user: TelegramMessageUser | undefined,
|
|
313
|
+
): string | undefined {
|
|
310
314
|
if (!user) return undefined;
|
|
311
315
|
if (user.username) return user.username;
|
|
312
316
|
if (typeof user.id === "number") return String(user.id);
|
|
313
|
-
const name = [user.first_name, user.last_name]
|
|
317
|
+
const name = [user.first_name, user.last_name]
|
|
318
|
+
.filter(Boolean)
|
|
319
|
+
.join(" ")
|
|
320
|
+
.trim();
|
|
314
321
|
return name || undefined;
|
|
315
322
|
}
|
|
316
323
|
|
|
@@ -331,7 +338,8 @@ export function extractTelegramForwardContextText(
|
|
|
331
338
|
message: TelegramMediaMessage,
|
|
332
339
|
allowedUserId?: number,
|
|
333
340
|
): string {
|
|
334
|
-
const originUser =
|
|
341
|
+
const originUser =
|
|
342
|
+
message.forward_origin?.sender_user ?? message.forward_from;
|
|
335
343
|
const isOwnerOrigin =
|
|
336
344
|
typeof allowedUserId === "number" && originUser?.id === allowedUserId;
|
|
337
345
|
const origin = formatTelegramForwardOriginIdentifier(message);
|
|
@@ -497,21 +505,55 @@ export function queueTelegramMediaGroupMessage<
|
|
|
497
505
|
debounceMs: number;
|
|
498
506
|
setTimer: (callback: () => void, ms: number) => ReturnType<typeof setTimeout>;
|
|
499
507
|
clearTimer: (timer: ReturnType<typeof setTimeout>) => void;
|
|
500
|
-
dispatchMessages: (
|
|
508
|
+
dispatchMessages: (
|
|
509
|
+
messages: TMessage[],
|
|
510
|
+
ctx?: TContext,
|
|
511
|
+
) => unknown | Promise<unknown>;
|
|
501
512
|
}): boolean {
|
|
502
513
|
const key = getTelegramMediaGroupKey(options.message);
|
|
503
514
|
if (!key) return false;
|
|
504
515
|
const existing = options.groups.get(key) ?? { messages: [] };
|
|
505
516
|
existing.messages.push(options.message);
|
|
506
517
|
existing.context = options.context;
|
|
518
|
+
const scheduleDispatch = (): void => {
|
|
519
|
+
if (existing.suspended) return;
|
|
520
|
+
existing.flushTimer = options.setTimer(() => {
|
|
521
|
+
existing.flushTimer = undefined;
|
|
522
|
+
const state = options.groups.get(key);
|
|
523
|
+
if (!state) return;
|
|
524
|
+
if (state.dispatching) {
|
|
525
|
+
scheduleDispatch();
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
const dispatchedMessages = [...state.messages];
|
|
529
|
+
const dispatchedIds = new Set(
|
|
530
|
+
dispatchedMessages.map((message) => message.message_id),
|
|
531
|
+
);
|
|
532
|
+
state.dispatching = true;
|
|
533
|
+
void Promise.resolve(
|
|
534
|
+
options.dispatchMessages(dispatchedMessages, state.context),
|
|
535
|
+
).then(
|
|
536
|
+
() => {
|
|
537
|
+
if (options.groups.get(key) !== state) return;
|
|
538
|
+
state.messages = state.messages.filter(
|
|
539
|
+
(message) => !dispatchedIds.has(message.message_id),
|
|
540
|
+
);
|
|
541
|
+
state.dispatching = false;
|
|
542
|
+
if (state.messages.length === 0) options.groups.delete(key);
|
|
543
|
+
else if (!state.flushTimer) scheduleDispatch();
|
|
544
|
+
},
|
|
545
|
+
() => {
|
|
546
|
+
if (options.groups.get(key) !== state) return;
|
|
547
|
+
state.dispatching = false;
|
|
548
|
+
if (!state.flushTimer) scheduleDispatch();
|
|
549
|
+
},
|
|
550
|
+
);
|
|
551
|
+
}, options.debounceMs);
|
|
552
|
+
existing.flushTimer.unref?.();
|
|
553
|
+
};
|
|
554
|
+
existing.reschedule = scheduleDispatch;
|
|
507
555
|
if (existing.flushTimer) options.clearTimer(existing.flushTimer);
|
|
508
|
-
|
|
509
|
-
const state = options.groups.get(key);
|
|
510
|
-
options.groups.delete(key);
|
|
511
|
-
if (!state) return;
|
|
512
|
-
options.dispatchMessages(state.messages, state.context);
|
|
513
|
-
}, options.debounceMs);
|
|
514
|
-
existing.flushTimer.unref?.();
|
|
556
|
+
scheduleDispatch();
|
|
515
557
|
options.groups.set(key, existing);
|
|
516
558
|
return true;
|
|
517
559
|
}
|
|
@@ -542,6 +584,20 @@ export function createTelegramMediaGroupController<
|
|
|
542
584
|
}),
|
|
543
585
|
removeMessages: (messageIds) =>
|
|
544
586
|
removePendingTelegramMediaGroupMessages(groups, messageIds, clearTimer),
|
|
587
|
+
suspend: () => {
|
|
588
|
+
for (const state of groups.values()) {
|
|
589
|
+
state.suspended = true;
|
|
590
|
+
if (state.flushTimer) clearTimer(state.flushTimer);
|
|
591
|
+
state.flushTimer = undefined;
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
resume: (context) => {
|
|
595
|
+
for (const state of groups.values()) {
|
|
596
|
+
state.context = context;
|
|
597
|
+
state.suspended = false;
|
|
598
|
+
if (!state.dispatching && !state.flushTimer) state.reschedule?.();
|
|
599
|
+
}
|
|
600
|
+
},
|
|
545
601
|
clear: () => {
|
|
546
602
|
for (const state of groups.values()) {
|
|
547
603
|
if (state.flushTimer) clearTimer(state.flushTimer);
|
|
@@ -562,11 +618,10 @@ export function createTelegramMediaGroupDispatchRuntime<
|
|
|
562
618
|
const queuedMediaGroup = deps.mediaGroups.queueMessage({
|
|
563
619
|
message,
|
|
564
620
|
context: ctx,
|
|
565
|
-
dispatchMessages: (messages, queuedCtx) =>
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
},
|
|
621
|
+
dispatchMessages: (messages, queuedCtx) =>
|
|
622
|
+
queuedCtx === undefined
|
|
623
|
+
? Promise.resolve()
|
|
624
|
+
: deps.dispatchMessages(messages, queuedCtx),
|
|
570
625
|
});
|
|
571
626
|
if (queuedMediaGroup) return;
|
|
572
627
|
await deps.dispatchMessages([message], ctx);
|
package/lib/model.ts
CHANGED
|
@@ -15,13 +15,7 @@ export interface MenuModel {
|
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
export type ThinkingLevel =
|
|
18
|
-
| "
|
|
19
|
-
| "minimal"
|
|
20
|
-
| "low"
|
|
21
|
-
| "medium"
|
|
22
|
-
| "high"
|
|
23
|
-
| "xhigh"
|
|
24
|
-
| "max";
|
|
18
|
+
"off" | "minimal" | "low" | "medium" | "high" | "xhigh" | "max";
|
|
25
19
|
|
|
26
20
|
export interface ScopedTelegramModel<TModel extends MenuModel = MenuModel> {
|
|
27
21
|
model: TModel;
|
|
@@ -443,7 +437,7 @@ export function buildTelegramModelSwitchContinuationText<
|
|
|
443
437
|
export function buildTelegramModelSwitchContinuationTurn<
|
|
444
438
|
TModel extends MenuModel,
|
|
445
439
|
>(options: {
|
|
446
|
-
turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId">;
|
|
440
|
+
turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
|
|
447
441
|
selection: ScopedTelegramModel<TModel>;
|
|
448
442
|
telegramPrefix?: string;
|
|
449
443
|
queueOrder: number;
|
|
@@ -456,6 +450,7 @@ export function buildTelegramModelSwitchContinuationTurn<
|
|
|
456
450
|
return {
|
|
457
451
|
kind: "prompt",
|
|
458
452
|
chatId: options.turn.chatId,
|
|
453
|
+
...(options.turn.target ? { target: { ...options.turn.target } } : {}),
|
|
459
454
|
replyToMessageId: options.turn.replyToMessageId,
|
|
460
455
|
sourceMessageIds: [],
|
|
461
456
|
queueOrder: options.queueOrder,
|
|
@@ -484,7 +479,7 @@ export function createTelegramModelSwitchContinuationTurnBuilder<
|
|
|
484
479
|
allocateItemOrder: () => number;
|
|
485
480
|
allocateControlOrder: () => number;
|
|
486
481
|
}): (options: {
|
|
487
|
-
turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId">;
|
|
482
|
+
turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
|
|
488
483
|
selection: ScopedTelegramModel<TModel>;
|
|
489
484
|
}) => PendingTelegramTurn {
|
|
490
485
|
return (options) =>
|
|
@@ -501,7 +496,7 @@ export function createTelegramModelSwitchContinuationQueue<
|
|
|
501
496
|
TSelection extends ScopedTelegramModel,
|
|
502
497
|
>(deps: {
|
|
503
498
|
createContinuationTurn: (options: {
|
|
504
|
-
turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId">;
|
|
499
|
+
turn: Pick<PendingTelegramTurn, "chatId" | "replyToMessageId" | "target">;
|
|
505
500
|
selection: TSelection;
|
|
506
501
|
}) => PendingTelegramTurn;
|
|
507
502
|
appendQueuedItem: (item: PendingTelegramTurn, ctx: TContext) => void;
|
package/lib/ownership.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface TelegramMessageOwnershipRecord {
|
|
|
11
11
|
messageId: number;
|
|
12
12
|
target: TelegramTarget;
|
|
13
13
|
instanceId: string;
|
|
14
|
+
profileKey?: string;
|
|
15
|
+
ownerGeneration?: string;
|
|
14
16
|
createdAt: number;
|
|
15
17
|
updatedAt: number;
|
|
16
18
|
}
|
|
@@ -21,6 +23,8 @@ export interface TelegramMessageOwnershipStore {
|
|
|
21
23
|
messageId: number;
|
|
22
24
|
target?: TelegramTarget;
|
|
23
25
|
instanceId: string;
|
|
26
|
+
profileKey?: string;
|
|
27
|
+
ownerGeneration?: string;
|
|
24
28
|
now?: number;
|
|
25
29
|
}) => TelegramMessageOwnershipRecord;
|
|
26
30
|
get: (
|
|
@@ -38,11 +42,110 @@ export interface TelegramMessageOwnershipStore {
|
|
|
38
42
|
clear: () => void;
|
|
39
43
|
}
|
|
40
44
|
|
|
45
|
+
export interface TelegramFollowerOwnershipView {
|
|
46
|
+
instanceId: string;
|
|
47
|
+
connectedAtMs: number;
|
|
48
|
+
registrationGeneration?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface TelegramBusMessageOwnershipRuntime {
|
|
52
|
+
store: TelegramMessageOwnershipStore;
|
|
53
|
+
recordLocal(input: {
|
|
54
|
+
chatId: number;
|
|
55
|
+
messageId: number;
|
|
56
|
+
target?: TelegramTarget;
|
|
57
|
+
}): TelegramMessageOwnershipRecord;
|
|
58
|
+
recordRouted(input: {
|
|
59
|
+
chatId: number;
|
|
60
|
+
messageId: number;
|
|
61
|
+
target?: TelegramTarget;
|
|
62
|
+
instanceId: string;
|
|
63
|
+
}): TelegramMessageOwnershipRecord;
|
|
64
|
+
recordFollower(input: {
|
|
65
|
+
chatId: number;
|
|
66
|
+
messageId: number;
|
|
67
|
+
target?: TelegramTarget;
|
|
68
|
+
follower: TelegramFollowerOwnershipView;
|
|
69
|
+
}): TelegramMessageOwnershipRecord;
|
|
70
|
+
isOwnedByFollower(input: {
|
|
71
|
+
chatId: number;
|
|
72
|
+
messageId: number;
|
|
73
|
+
follower: TelegramFollowerOwnershipView;
|
|
74
|
+
}): boolean;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function getTelegramFollowerOwnershipGeneration(
|
|
78
|
+
follower: TelegramFollowerOwnershipView,
|
|
79
|
+
): string {
|
|
80
|
+
return (
|
|
81
|
+
follower.registrationGeneration ??
|
|
82
|
+
`${follower.instanceId}:${follower.connectedAtMs}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function createTelegramBusMessageOwnershipRuntime(deps: {
|
|
87
|
+
instanceId: string;
|
|
88
|
+
getProfileKey(): string;
|
|
89
|
+
listFollowers(): readonly TelegramFollowerOwnershipView[];
|
|
90
|
+
}): TelegramBusMessageOwnershipRuntime {
|
|
91
|
+
const store = createTelegramMessageOwnershipStore({
|
|
92
|
+
getProfileKey: deps.getProfileKey,
|
|
93
|
+
isOwnerGenerationLive(record) {
|
|
94
|
+
if (!record.ownerGeneration) return true;
|
|
95
|
+
return deps.listFollowers().some((follower) => {
|
|
96
|
+
return (
|
|
97
|
+
follower.instanceId === record.instanceId &&
|
|
98
|
+
getTelegramFollowerOwnershipGeneration(follower) ===
|
|
99
|
+
record.ownerGeneration
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
const recordFollower = function (input: {
|
|
105
|
+
chatId: number;
|
|
106
|
+
messageId: number;
|
|
107
|
+
target?: TelegramTarget;
|
|
108
|
+
follower: TelegramFollowerOwnershipView;
|
|
109
|
+
}): TelegramMessageOwnershipRecord {
|
|
110
|
+
return store.record({
|
|
111
|
+
chatId: input.chatId,
|
|
112
|
+
messageId: input.messageId,
|
|
113
|
+
target: input.target,
|
|
114
|
+
instanceId: input.follower.instanceId,
|
|
115
|
+
ownerGeneration: getTelegramFollowerOwnershipGeneration(input.follower),
|
|
116
|
+
});
|
|
117
|
+
};
|
|
118
|
+
return {
|
|
119
|
+
store,
|
|
120
|
+
recordLocal(input) {
|
|
121
|
+
return store.record({ ...input, instanceId: deps.instanceId });
|
|
122
|
+
},
|
|
123
|
+
recordRouted(input) {
|
|
124
|
+
const follower = deps
|
|
125
|
+
.listFollowers()
|
|
126
|
+
.find((candidate) => candidate.instanceId === input.instanceId);
|
|
127
|
+
return follower
|
|
128
|
+
? recordFollower({ ...input, follower })
|
|
129
|
+
: store.record(input);
|
|
130
|
+
},
|
|
131
|
+
recordFollower,
|
|
132
|
+
isOwnedByFollower({ chatId, messageId, follower }) {
|
|
133
|
+
const ownership = store.get(chatId, messageId);
|
|
134
|
+
return (
|
|
135
|
+
ownership?.instanceId === follower.instanceId &&
|
|
136
|
+
ownership.ownerGeneration ===
|
|
137
|
+
getTelegramFollowerOwnershipGeneration(follower)
|
|
138
|
+
);
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
41
143
|
function getTelegramMessageOwnershipKey(
|
|
42
144
|
chatId: number,
|
|
43
145
|
messageId: number,
|
|
146
|
+
profileKey?: string,
|
|
44
147
|
): string {
|
|
45
|
-
return `${chatId}:${messageId}`;
|
|
148
|
+
return `${profileKey ?? ""}:${chatId}:${messageId}`;
|
|
46
149
|
}
|
|
47
150
|
|
|
48
151
|
function createTelegramMessageOwnershipRecord(input: {
|
|
@@ -50,6 +153,8 @@ function createTelegramMessageOwnershipRecord(input: {
|
|
|
50
153
|
messageId: number;
|
|
51
154
|
target?: TelegramTarget;
|
|
52
155
|
instanceId: string;
|
|
156
|
+
profileKey?: string;
|
|
157
|
+
ownerGeneration?: string;
|
|
53
158
|
now: number;
|
|
54
159
|
previous?: TelegramMessageOwnershipRecord;
|
|
55
160
|
}): TelegramMessageOwnershipRecord {
|
|
@@ -58,33 +163,72 @@ function createTelegramMessageOwnershipRecord(input: {
|
|
|
58
163
|
messageId: input.messageId,
|
|
59
164
|
target: input.target ?? { chatId: input.chatId },
|
|
60
165
|
instanceId: input.instanceId,
|
|
166
|
+
...(input.profileKey ? { profileKey: input.profileKey } : {}),
|
|
167
|
+
...(input.ownerGeneration
|
|
168
|
+
? { ownerGeneration: input.ownerGeneration }
|
|
169
|
+
: {}),
|
|
61
170
|
createdAt: input.previous?.createdAt ?? input.now,
|
|
62
171
|
updatedAt: input.now,
|
|
63
172
|
};
|
|
64
173
|
}
|
|
65
174
|
|
|
66
|
-
export function createTelegramMessageOwnershipStore(
|
|
175
|
+
export function createTelegramMessageOwnershipStore(
|
|
176
|
+
options: {
|
|
177
|
+
getProfileKey?: () => string | undefined;
|
|
178
|
+
isOwnerGenerationLive?: (record: TelegramMessageOwnershipRecord) => boolean;
|
|
179
|
+
} = {},
|
|
180
|
+
): TelegramMessageOwnershipStore {
|
|
67
181
|
const records = new Map<string, TelegramMessageOwnershipRecord>();
|
|
68
182
|
return {
|
|
69
183
|
record: (input) => {
|
|
70
|
-
const
|
|
184
|
+
const profileKey = input.profileKey ?? options.getProfileKey?.();
|
|
185
|
+
const key = getTelegramMessageOwnershipKey(
|
|
186
|
+
input.chatId,
|
|
187
|
+
input.messageId,
|
|
188
|
+
profileKey,
|
|
189
|
+
);
|
|
71
190
|
const now = input.now ?? Date.now();
|
|
72
191
|
const record = createTelegramMessageOwnershipRecord({
|
|
73
192
|
...input,
|
|
193
|
+
profileKey,
|
|
74
194
|
now,
|
|
75
195
|
previous: records.get(key),
|
|
76
196
|
});
|
|
77
197
|
records.set(key, record);
|
|
78
198
|
return record;
|
|
79
199
|
},
|
|
80
|
-
get: (chatId, messageId) =>
|
|
81
|
-
records.get(
|
|
200
|
+
get: (chatId, messageId) => {
|
|
201
|
+
const record = records.get(
|
|
202
|
+
getTelegramMessageOwnershipKey(
|
|
203
|
+
chatId,
|
|
204
|
+
messageId,
|
|
205
|
+
options.getProfileKey?.(),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
if (!record) return undefined;
|
|
209
|
+
if (
|
|
210
|
+
record.ownerGeneration &&
|
|
211
|
+
options.isOwnerGenerationLive &&
|
|
212
|
+
!options.isOwnerGenerationLive(record)
|
|
213
|
+
) {
|
|
214
|
+
return undefined;
|
|
215
|
+
}
|
|
216
|
+
return record;
|
|
217
|
+
},
|
|
82
218
|
forget: (chatId, messageId) =>
|
|
83
|
-
records.delete(
|
|
219
|
+
records.delete(
|
|
220
|
+
getTelegramMessageOwnershipKey(
|
|
221
|
+
chatId,
|
|
222
|
+
messageId,
|
|
223
|
+
options.getProfileKey?.(),
|
|
224
|
+
),
|
|
225
|
+
),
|
|
84
226
|
forgetTarget: (target) => {
|
|
85
227
|
const targetKey = getTelegramTargetKey(target);
|
|
228
|
+
const profileKey = options.getProfileKey?.();
|
|
86
229
|
let removed = 0;
|
|
87
230
|
for (const [key, record] of records) {
|
|
231
|
+
if ((record.profileKey ?? undefined) !== profileKey) continue;
|
|
88
232
|
if (getTelegramTargetKey(record.target) !== targetKey) continue;
|
|
89
233
|
records.delete(key);
|
|
90
234
|
removed += 1;
|