@llblab/pi-kit 0.1.7 → 0.1.9
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/CHANGELOG.md +9 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +1 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +17 -0
- package/node_modules/@llblab/pi-telegram/README.md +3 -3
- package/node_modules/@llblab/pi-telegram/api/voice.ts +0 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -5
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +2 -3
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +5 -15
- package/node_modules/@llblab/pi-telegram/docs/voice.md +9 -37
- package/node_modules/@llblab/pi-telegram/index.ts +30 -7
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +112 -25
- package/node_modules/@llblab/pi-telegram/lib/config.ts +38 -46
- package/node_modules/@llblab/pi-telegram/lib/journal.ts +107 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-model.ts +4 -2
- package/node_modules/@llblab/pi-telegram/lib/menu-settings.ts +15 -11
- package/node_modules/@llblab/pi-telegram/lib/outbound-voice.ts +3 -18
- package/node_modules/@llblab/pi-telegram/lib/polling.ts +66 -24
- package/node_modules/@llblab/pi-telegram/lib/setup.ts +0 -1
- package/node_modules/@llblab/pi-telegram/lib/status.ts +2 -2
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/voice.ts +7 -31
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +1 -1
- package/package.json +2 -2
|
@@ -157,6 +157,7 @@ export interface TelegramUpdateJournalEntry {
|
|
|
157
157
|
export interface TelegramUpdateJournalFile {
|
|
158
158
|
version: typeof TELEGRAM_UPDATE_JOURNAL_VERSION;
|
|
159
159
|
revision?: number;
|
|
160
|
+
acceptedThroughUpdateId?: number;
|
|
160
161
|
profile: string;
|
|
161
162
|
botIdentity: TelegramUpdateJournalBotIdentity;
|
|
162
163
|
entries: TelegramUpdateJournalEntry[];
|
|
@@ -312,6 +313,7 @@ export interface TelegramUpdateJournalStore {
|
|
|
312
313
|
read(): TelegramUpdateJournalSnapshot;
|
|
313
314
|
appendBatch<TUpdate extends TelegramUpdateJournalInput>(
|
|
314
315
|
updates: readonly TUpdate[],
|
|
316
|
+
acceptedThroughUpdateId?: number,
|
|
315
317
|
): TelegramUpdateJournalAppendResult;
|
|
316
318
|
markQueued(
|
|
317
319
|
receipt: TelegramUpdateJournalQueueReceipt,
|
|
@@ -385,6 +387,7 @@ export interface TelegramUpdateJournalSegment {
|
|
|
385
387
|
version: typeof TELEGRAM_UPDATE_JOURNAL_VERSION;
|
|
386
388
|
revision: number;
|
|
387
389
|
previousRevision: number;
|
|
390
|
+
acceptedThroughUpdateId?: number;
|
|
388
391
|
profile: string;
|
|
389
392
|
botIdentity: TelegramUpdateJournalBotIdentity;
|
|
390
393
|
upsertedEntries: TelegramUpdateJournalEntry[];
|
|
@@ -975,6 +978,7 @@ function parseJournalFile(
|
|
|
975
978
|
!hasOnlyKeys(value, [
|
|
976
979
|
"version",
|
|
977
980
|
"revision",
|
|
981
|
+
"acceptedThroughUpdateId",
|
|
978
982
|
"profile",
|
|
979
983
|
"botIdentity",
|
|
980
984
|
"entries",
|
|
@@ -983,6 +987,8 @@ function parseJournalFile(
|
|
|
983
987
|
!Number.isSafeInteger(value.version) ||
|
|
984
988
|
(value.revision !== undefined &&
|
|
985
989
|
(!isSafeNonNegativeInteger(value.revision) || value.revision === 0)) ||
|
|
990
|
+
(value.acceptedThroughUpdateId !== undefined &&
|
|
991
|
+
!isSafeNonNegativeInteger(value.acceptedThroughUpdateId)) ||
|
|
986
992
|
!isNonEmptyString(value.profile) ||
|
|
987
993
|
!Array.isArray(value.entries) ||
|
|
988
994
|
(value.operatorDispositions !== undefined &&
|
|
@@ -1009,6 +1015,17 @@ function parseJournalFile(
|
|
|
1009
1015
|
);
|
|
1010
1016
|
}
|
|
1011
1017
|
}
|
|
1018
|
+
if (
|
|
1019
|
+
entries.length > 0 &&
|
|
1020
|
+
value.acceptedThroughUpdateId !== undefined &&
|
|
1021
|
+
value.acceptedThroughUpdateId < entries.at(-1)!.updateId
|
|
1022
|
+
) {
|
|
1023
|
+
throw createJournalError(
|
|
1024
|
+
"invalid",
|
|
1025
|
+
path,
|
|
1026
|
+
"has an admission cursor behind its active entries",
|
|
1027
|
+
);
|
|
1028
|
+
}
|
|
1012
1029
|
const queuedReceipts = new Map<
|
|
1013
1030
|
string,
|
|
1014
1031
|
{
|
|
@@ -1096,6 +1113,9 @@ function parseJournalFile(
|
|
|
1096
1113
|
...(value.revision !== undefined
|
|
1097
1114
|
? { revision: value.revision as number }
|
|
1098
1115
|
: {}),
|
|
1116
|
+
...(value.acceptedThroughUpdateId !== undefined
|
|
1117
|
+
? { acceptedThroughUpdateId: value.acceptedThroughUpdateId as number }
|
|
1118
|
+
: {}),
|
|
1099
1119
|
profile: value.profile,
|
|
1100
1120
|
botIdentity: validateBotIdentity(value.botIdentity, path),
|
|
1101
1121
|
entries,
|
|
@@ -1113,6 +1133,7 @@ function parseJournalSegment(
|
|
|
1113
1133
|
"version",
|
|
1114
1134
|
"revision",
|
|
1115
1135
|
"previousRevision",
|
|
1136
|
+
"acceptedThroughUpdateId",
|
|
1116
1137
|
"profile",
|
|
1117
1138
|
"botIdentity",
|
|
1118
1139
|
"upsertedEntries",
|
|
@@ -1122,6 +1143,8 @@ function parseJournalSegment(
|
|
|
1122
1143
|
value.version !== TELEGRAM_UPDATE_JOURNAL_VERSION ||
|
|
1123
1144
|
!isSafePositiveInteger(value.revision) ||
|
|
1124
1145
|
!isSafeNonNegativeInteger(value.previousRevision) ||
|
|
1146
|
+
(value.acceptedThroughUpdateId !== undefined &&
|
|
1147
|
+
!isSafeNonNegativeInteger(value.acceptedThroughUpdateId)) ||
|
|
1125
1148
|
!isNonEmptyString(value.profile) ||
|
|
1126
1149
|
!Array.isArray(value.upsertedEntries) ||
|
|
1127
1150
|
!Array.isArray(value.removedUpdateIds) ||
|
|
@@ -1140,6 +1163,18 @@ function parseJournalSegment(
|
|
|
1140
1163
|
}
|
|
1141
1164
|
upsertedIds.add(entry.updateId);
|
|
1142
1165
|
}
|
|
1166
|
+
if (
|
|
1167
|
+
upsertedEntries.length > 0 &&
|
|
1168
|
+
value.acceptedThroughUpdateId !== undefined &&
|
|
1169
|
+
value.acceptedThroughUpdateId <
|
|
1170
|
+
Math.max(...upsertedEntries.map((entry) => entry.updateId))
|
|
1171
|
+
) {
|
|
1172
|
+
throw createJournalError(
|
|
1173
|
+
"invalid",
|
|
1174
|
+
path,
|
|
1175
|
+
"has an admission cursor behind its segment upserts",
|
|
1176
|
+
);
|
|
1177
|
+
}
|
|
1143
1178
|
const removedUpdateIds: number[] = [];
|
|
1144
1179
|
const removedIds = new Set<number>();
|
|
1145
1180
|
for (const updateId of value.removedUpdateIds) {
|
|
@@ -1162,6 +1197,9 @@ function parseJournalSegment(
|
|
|
1162
1197
|
version: TELEGRAM_UPDATE_JOURNAL_VERSION,
|
|
1163
1198
|
revision: value.revision,
|
|
1164
1199
|
previousRevision: value.previousRevision,
|
|
1200
|
+
...(value.acceptedThroughUpdateId !== undefined
|
|
1201
|
+
? { acceptedThroughUpdateId: value.acceptedThroughUpdateId as number }
|
|
1202
|
+
: {}),
|
|
1165
1203
|
profile: value.profile,
|
|
1166
1204
|
botIdentity: validateBotIdentity(value.botIdentity, path),
|
|
1167
1205
|
upsertedEntries,
|
|
@@ -1190,6 +1228,9 @@ function cloneFile(file: TelegramUpdateJournalFile): TelegramUpdateJournalFile {
|
|
|
1190
1228
|
return {
|
|
1191
1229
|
version: TELEGRAM_UPDATE_JOURNAL_VERSION,
|
|
1192
1230
|
...(file.revision !== undefined ? { revision: file.revision } : {}),
|
|
1231
|
+
...(file.acceptedThroughUpdateId !== undefined
|
|
1232
|
+
? { acceptedThroughUpdateId: file.acceptedThroughUpdateId }
|
|
1233
|
+
: {}),
|
|
1193
1234
|
profile: file.profile,
|
|
1194
1235
|
botIdentity: { ...file.botIdentity },
|
|
1195
1236
|
entries: file.entries.map(cloneEntry),
|
|
@@ -1879,6 +1920,17 @@ export function createTelegramUpdateJournalStore(
|
|
|
1879
1920
|
`has a revision gap after ${revision}`,
|
|
1880
1921
|
);
|
|
1881
1922
|
}
|
|
1923
|
+
if (
|
|
1924
|
+
segment.acceptedThroughUpdateId !== undefined &&
|
|
1925
|
+
file.acceptedThroughUpdateId !== undefined &&
|
|
1926
|
+
segment.acceptedThroughUpdateId < file.acceptedThroughUpdateId
|
|
1927
|
+
) {
|
|
1928
|
+
throw createJournalError(
|
|
1929
|
+
"invalid",
|
|
1930
|
+
segmentPath,
|
|
1931
|
+
"regresses the admission cursor",
|
|
1932
|
+
);
|
|
1933
|
+
}
|
|
1882
1934
|
if (
|
|
1883
1935
|
segment.profile !== storedProfile ||
|
|
1884
1936
|
!identitiesMatch(segment.botIdentity, storedIdentity)
|
|
@@ -1907,6 +1959,11 @@ export function createTelegramUpdateJournalStore(
|
|
|
1907
1959
|
{
|
|
1908
1960
|
version: TELEGRAM_UPDATE_JOURNAL_VERSION,
|
|
1909
1961
|
revision: segment.revision,
|
|
1962
|
+
...(segment.acceptedThroughUpdateId !== undefined
|
|
1963
|
+
? { acceptedThroughUpdateId: segment.acceptedThroughUpdateId }
|
|
1964
|
+
: file.acceptedThroughUpdateId !== undefined
|
|
1965
|
+
? { acceptedThroughUpdateId: file.acceptedThroughUpdateId }
|
|
1966
|
+
: {}),
|
|
1910
1967
|
profile: storedProfile,
|
|
1911
1968
|
botIdentity: mergeBotIdentity(file.botIdentity, segment.botIdentity),
|
|
1912
1969
|
entries: [...entriesById.values()].sort(
|
|
@@ -2136,6 +2193,7 @@ export function createTelegramUpdateJournalStore(
|
|
|
2136
2193
|
entries: TelegramUpdateJournalEntry[],
|
|
2137
2194
|
contentChanged: boolean,
|
|
2138
2195
|
operatorDispositions = current.file.operatorDispositions,
|
|
2196
|
+
acceptedThroughUpdateId = current.file.acceptedThroughUpdateId,
|
|
2139
2197
|
): { file: TelegramUpdateJournalFile; serializedBytes: number } => {
|
|
2140
2198
|
const botIdentity = mergeBotIdentity(
|
|
2141
2199
|
current.file.botIdentity,
|
|
@@ -2147,7 +2205,8 @@ export function createTelegramUpdateJournalStore(
|
|
|
2147
2205
|
isDeepStrictEqual(
|
|
2148
2206
|
current.file.operatorDispositions ?? [],
|
|
2149
2207
|
operatorDispositions ?? [],
|
|
2150
|
-
)
|
|
2208
|
+
) &&
|
|
2209
|
+
current.file.acceptedThroughUpdateId === acceptedThroughUpdateId
|
|
2151
2210
|
) {
|
|
2152
2211
|
return { file: current.file, serializedBytes: current.serializedBytes };
|
|
2153
2212
|
}
|
|
@@ -2156,6 +2215,9 @@ export function createTelegramUpdateJournalStore(
|
|
|
2156
2215
|
profile,
|
|
2157
2216
|
botIdentity,
|
|
2158
2217
|
entries,
|
|
2218
|
+
...(acceptedThroughUpdateId !== undefined
|
|
2219
|
+
? { acceptedThroughUpdateId }
|
|
2220
|
+
: {}),
|
|
2159
2221
|
...(operatorDispositions?.length
|
|
2160
2222
|
? { operatorDispositions }
|
|
2161
2223
|
: {}),
|
|
@@ -2164,6 +2226,7 @@ export function createTelegramUpdateJournalStore(
|
|
|
2164
2226
|
const serializedBytes = assertCapacity(file, serialized);
|
|
2165
2227
|
const changed =
|
|
2166
2228
|
contentChanged ||
|
|
2229
|
+
current.file.acceptedThroughUpdateId !== acceptedThroughUpdateId ||
|
|
2167
2230
|
!isDeepStrictEqual(current.file.botIdentity, file.botIdentity);
|
|
2168
2231
|
if (!changed) {
|
|
2169
2232
|
return { file, serializedBytes: current.serializedBytes };
|
|
@@ -2193,6 +2256,9 @@ export function createTelegramUpdateJournalStore(
|
|
|
2193
2256
|
botIdentity: file.botIdentity,
|
|
2194
2257
|
upsertedEntries,
|
|
2195
2258
|
removedUpdateIds,
|
|
2259
|
+
...(acceptedThroughUpdateId !== undefined
|
|
2260
|
+
? { acceptedThroughUpdateId }
|
|
2261
|
+
: {}),
|
|
2196
2262
|
...(!isDeepStrictEqual(
|
|
2197
2263
|
current.file.operatorDispositions ?? [],
|
|
2198
2264
|
operatorDispositions ?? [],
|
|
@@ -2260,8 +2326,18 @@ export function createTelegramUpdateJournalStore(
|
|
|
2260
2326
|
};
|
|
2261
2327
|
});
|
|
2262
2328
|
},
|
|
2263
|
-
appendBatch(updates) {
|
|
2329
|
+
appendBatch(updates, requestedAcceptedThroughUpdateId) {
|
|
2264
2330
|
return runMutation(() => {
|
|
2331
|
+
if (
|
|
2332
|
+
requestedAcceptedThroughUpdateId !== undefined &&
|
|
2333
|
+
!isSafeNonNegativeInteger(requestedAcceptedThroughUpdateId)
|
|
2334
|
+
) {
|
|
2335
|
+
throw createJournalError(
|
|
2336
|
+
"invalid",
|
|
2337
|
+
path,
|
|
2338
|
+
"received an invalid admission cursor",
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2265
2341
|
const current = readCurrent();
|
|
2266
2342
|
const normalizedUpdates: TelegramJournaledUpdate[] = [];
|
|
2267
2343
|
for (const update of updates) {
|
|
@@ -2290,6 +2366,8 @@ export function createTelegramUpdateJournalStore(
|
|
|
2290
2366
|
const entriesById = new Map(
|
|
2291
2367
|
current.file.entries.map((entry) => [entry.updateId, entry]),
|
|
2292
2368
|
);
|
|
2369
|
+
const previousAcceptedThroughUpdateId =
|
|
2370
|
+
current.file.acceptedThroughUpdateId;
|
|
2293
2371
|
const discardedUpdateIds = new Set(
|
|
2294
2372
|
(current.file.operatorDispositions ?? [])
|
|
2295
2373
|
.filter((disposition) => disposition.action === "discard")
|
|
@@ -2335,6 +2413,31 @@ export function createTelegramUpdateJournalStore(
|
|
|
2335
2413
|
addedUpdateIds.push(entry.updateId);
|
|
2336
2414
|
}
|
|
2337
2415
|
|
|
2416
|
+
const batchLastUpdateId = normalizedUpdates.at(-1)?.update_id;
|
|
2417
|
+
if (
|
|
2418
|
+
requestedAcceptedThroughUpdateId !== undefined &&
|
|
2419
|
+
batchLastUpdateId !== undefined &&
|
|
2420
|
+
requestedAcceptedThroughUpdateId < batchLastUpdateId
|
|
2421
|
+
) {
|
|
2422
|
+
throw createJournalError(
|
|
2423
|
+
"invalid",
|
|
2424
|
+
path,
|
|
2425
|
+
"received an admission cursor behind its batch",
|
|
2426
|
+
);
|
|
2427
|
+
}
|
|
2428
|
+
if (
|
|
2429
|
+
requestedAcceptedThroughUpdateId !== undefined &&
|
|
2430
|
+
previousAcceptedThroughUpdateId !== undefined &&
|
|
2431
|
+
requestedAcceptedThroughUpdateId < previousAcceptedThroughUpdateId
|
|
2432
|
+
) {
|
|
2433
|
+
throw createJournalError(
|
|
2434
|
+
"conflict",
|
|
2435
|
+
path,
|
|
2436
|
+
"received a regressing admission cursor",
|
|
2437
|
+
);
|
|
2438
|
+
}
|
|
2439
|
+
const acceptedThroughUpdateId =
|
|
2440
|
+
requestedAcceptedThroughUpdateId ?? previousAcceptedThroughUpdateId;
|
|
2338
2441
|
const contentChanged = addedUpdateIds.length > 0;
|
|
2339
2442
|
const published = publishMutation(
|
|
2340
2443
|
current,
|
|
@@ -2344,6 +2447,8 @@ export function createTelegramUpdateJournalStore(
|
|
|
2344
2447
|
)
|
|
2345
2448
|
: current.file.entries,
|
|
2346
2449
|
contentChanged,
|
|
2450
|
+
current.file.operatorDispositions,
|
|
2451
|
+
acceptedThroughUpdateId,
|
|
2347
2452
|
);
|
|
2348
2453
|
return {
|
|
2349
2454
|
addedUpdateIds,
|
|
@@ -103,7 +103,7 @@ export interface TelegramModelMenuRuntimeOptions<
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
export interface MenuSettingsManager {
|
|
106
|
-
reload
|
|
106
|
+
reload?: () => Promise<void>;
|
|
107
107
|
flush?: () => Promise<void>;
|
|
108
108
|
getEnabledModels: () => string[] | undefined;
|
|
109
109
|
setEnabledModels?: (patterns: string[] | undefined) => void;
|
|
@@ -498,7 +498,9 @@ export function createTelegramModelMenuStateBuilder<
|
|
|
498
498
|
threadId,
|
|
499
499
|
activeModel: deps.getActiveModel(ctx),
|
|
500
500
|
ctx,
|
|
501
|
-
reloadSettings: () =>
|
|
501
|
+
reloadSettings: async () => {
|
|
502
|
+
await settingsManager.reload?.();
|
|
503
|
+
},
|
|
502
504
|
getConfiguredScopedModelPatterns: () =>
|
|
503
505
|
settingsManager.getEnabledModels(),
|
|
504
506
|
});
|
|
@@ -149,9 +149,7 @@ export const TIME_INJECTION_MODE_SETTINGS_TITLE =
|
|
|
149
149
|
"<b>🕒 Time injection mode:</b>";
|
|
150
150
|
export const VOICE_REPLY_MODE_SETTINGS_TITLE = "<b>👄 Voice reply mode:</b>";
|
|
151
151
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
function getVoiceReplyModeLabel(mode: TelegramVoiceReplyModeSetting): string {
|
|
152
|
+
function getVoiceReplyModeLabel(mode: TelegramVoiceReplyMode): string {
|
|
155
153
|
return mode;
|
|
156
154
|
}
|
|
157
155
|
|
|
@@ -162,8 +160,8 @@ function getTelegramSettingsStateValueLabel(value: string): string {
|
|
|
162
160
|
function getVoiceReplyModeSetting(
|
|
163
161
|
mode: TelegramVoiceReplyMode,
|
|
164
162
|
configured: boolean,
|
|
165
|
-
):
|
|
166
|
-
return configured ? mode : "
|
|
163
|
+
): TelegramVoiceReplyMode {
|
|
164
|
+
return configured ? mode : "manual";
|
|
167
165
|
}
|
|
168
166
|
|
|
169
167
|
export function buildTelegramSettingsMenuText(): string {
|
|
@@ -246,8 +244,8 @@ export function buildVoiceReplyModeSettingsText(
|
|
|
246
244
|
"",
|
|
247
245
|
"Controls when pi-telegram converts assistant text replies into Telegram voice messages.",
|
|
248
246
|
"",
|
|
249
|
-
"<code>-</code> <code>
|
|
250
|
-
"<code>-</code> <code>mirror</code>: voice input activates automatic voice delivery; text input follows '
|
|
247
|
+
"<code>-</code> <code>manual</code> (default): add no automatic voice context; explicit 'telegram_voice' actions still work.",
|
|
248
|
+
"<code>-</code> <code>mirror</code>: voice input activates automatic voice delivery; text input follows 'manual' behavior.",
|
|
251
249
|
"<code>-</code> <code>always</code>: activate automatic voice delivery for every reply.",
|
|
252
250
|
].join("\n");
|
|
253
251
|
}
|
|
@@ -502,7 +500,7 @@ export function buildVoiceReplyModeSettingsReplyMarkup(
|
|
|
502
500
|
configured = true,
|
|
503
501
|
): TelegramSettingsMenuReplyMarkup {
|
|
504
502
|
const activeMode = getVoiceReplyModeSetting(mode, configured);
|
|
505
|
-
const modes:
|
|
503
|
+
const modes: TelegramVoiceReplyMode[] = ["manual", "mirror", "always"];
|
|
506
504
|
return {
|
|
507
505
|
inline_keyboard: [
|
|
508
506
|
[{ text: "⬆️ Back", callback_data: "settings:list" }],
|
|
@@ -661,12 +659,18 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
661
659
|
}
|
|
662
660
|
if (data.startsWith("settings:set:voice-reply:")) {
|
|
663
661
|
const mode = data.slice("settings:set:voice-reply:".length);
|
|
664
|
-
if (
|
|
665
|
-
|
|
662
|
+
if (
|
|
663
|
+
mode === "manual" ||
|
|
664
|
+
mode === "hidden" ||
|
|
665
|
+
mode === "mirror" ||
|
|
666
|
+
mode === "always"
|
|
667
|
+
) {
|
|
668
|
+
const normalizedMode = mode === "hidden" ? "manual" : mode;
|
|
669
|
+
await deps.setVoiceReplyMode(normalizedMode);
|
|
666
670
|
await updateVoiceReplyModeSettingsMessage(deps);
|
|
667
671
|
await deps.answerCallbackQuery(
|
|
668
672
|
callbackQueryId,
|
|
669
|
-
`Voice reply mode: ${
|
|
673
|
+
`Voice reply mode: ${normalizedMode}`,
|
|
670
674
|
);
|
|
671
675
|
return true;
|
|
672
676
|
}
|
|
@@ -100,17 +100,6 @@ async function ensureTelegramVoiceFileFormat(
|
|
|
100
100
|
);
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function extractVoiceResult(result: any): {
|
|
104
|
-
filePath: string;
|
|
105
|
-
transcriptText?: string;
|
|
106
|
-
} {
|
|
107
|
-
if (typeof result === "string") return { filePath: result };
|
|
108
|
-
return {
|
|
109
|
-
filePath: result.audioPath,
|
|
110
|
-
transcriptText: result.transcriptText,
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
|
|
114
103
|
async function sendVoiceChatAction(
|
|
115
104
|
deps: TelegramVoiceReplySenderDeps,
|
|
116
105
|
chatId: number,
|
|
@@ -132,7 +121,6 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
132
121
|
options?: {
|
|
133
122
|
replyToPrompt?: boolean;
|
|
134
123
|
replyMarkup?: unknown;
|
|
135
|
-
transcriptText?: string;
|
|
136
124
|
},
|
|
137
125
|
): Promise<void> => {
|
|
138
126
|
const voiceFilePath = await ensureTelegramVoiceFileFormat(filePath);
|
|
@@ -148,7 +136,6 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
148
136
|
"sendVoice",
|
|
149
137
|
{
|
|
150
138
|
chat_id: String(turn.chatId),
|
|
151
|
-
...(options?.transcriptText ? { caption: options.transcriptText } : {}),
|
|
152
139
|
...(replyParameters ? { reply_parameters: replyParameters } : {}),
|
|
153
140
|
...(turn.target
|
|
154
141
|
? Object.fromEntries(
|
|
@@ -257,13 +244,11 @@ export function createTelegramVoiceReplySender<THandler = unknown>(
|
|
|
257
244
|
continue;
|
|
258
245
|
}
|
|
259
246
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
await uploadVoiceFile(turn, filePath, {
|
|
247
|
+
voiceFilePath = providerResult;
|
|
248
|
+
originalFilePath = providerResult;
|
|
249
|
+
await uploadVoiceFile(turn, providerResult, {
|
|
264
250
|
replyToPrompt: options?.replyToPrompt,
|
|
265
251
|
replyMarkup: options?.replyMarkup,
|
|
266
|
-
transcriptText,
|
|
267
252
|
});
|
|
268
253
|
return;
|
|
269
254
|
} catch (error) {
|
|
@@ -8,7 +8,6 @@ type MaybePromise<T> = T | Promise<T>;
|
|
|
8
8
|
|
|
9
9
|
export interface TelegramPollingConfig {
|
|
10
10
|
botToken?: string;
|
|
11
|
-
lastUpdateId?: number;
|
|
12
11
|
}
|
|
13
12
|
|
|
14
13
|
export interface TelegramUpdate {
|
|
@@ -227,6 +226,7 @@ export interface TelegramPollingAdmissionRuntime<TContext> {
|
|
|
227
226
|
|
|
228
227
|
export function createTelegramPollingAdmissionRuntime<TContext>(deps: {
|
|
229
228
|
polling: TelegramPollingController<TContext>;
|
|
229
|
+
prepareStart?: () => MaybePromise<void>;
|
|
230
230
|
validateStart?: () => void;
|
|
231
231
|
worker: {
|
|
232
232
|
onSessionStart: (ctx: TContext) => Promise<void>;
|
|
@@ -235,6 +235,7 @@ export function createTelegramPollingAdmissionRuntime<TContext>(deps: {
|
|
|
235
235
|
return {
|
|
236
236
|
isActive: deps.polling.isActive,
|
|
237
237
|
async start(ctx) {
|
|
238
|
+
await deps.prepareStart?.();
|
|
238
239
|
deps.validateStart?.();
|
|
239
240
|
await deps.worker.onSessionStart(ctx);
|
|
240
241
|
await deps.polling.start(ctx);
|
|
@@ -256,7 +257,12 @@ export type TelegramDurablePollingRuntimeAssemblyDeps<
|
|
|
256
257
|
"appendUpdateBatch" | "getJournalEntryCount" | "signalUpdateWorker"
|
|
257
258
|
> & {
|
|
258
259
|
journal: {
|
|
259
|
-
appendBatch: (
|
|
260
|
+
appendBatch: (
|
|
261
|
+
updates: readonly TUpdate[],
|
|
262
|
+
acceptedThroughUpdateId?: number,
|
|
263
|
+
) => MaybePromise<unknown>;
|
|
264
|
+
getAcceptedThroughUpdateId: () => number | undefined;
|
|
265
|
+
prepareCursorCutover?: () => MaybePromise<void>;
|
|
260
266
|
getEntryCount: () => number;
|
|
261
267
|
signalWorker: () => void;
|
|
262
268
|
getBootstrapEntryCount: () => number;
|
|
@@ -274,13 +280,15 @@ export function createTelegramDurablePollingRuntimeAssembly<
|
|
|
274
280
|
const controller = createTelegramPollingControllerRuntime({
|
|
275
281
|
...deps,
|
|
276
282
|
appendUpdateBatch: deps.journal.appendBatch,
|
|
283
|
+
getAcceptedThroughUpdateId: deps.journal.getAcceptedThroughUpdateId,
|
|
277
284
|
getJournalEntryCount: deps.journal.getEntryCount,
|
|
278
285
|
signalUpdateWorker: deps.journal.signalWorker,
|
|
279
286
|
});
|
|
280
287
|
const admission = createTelegramPollingAdmissionRuntime({
|
|
281
288
|
polling: controller,
|
|
289
|
+
prepareStart: deps.journal.prepareCursorCutover,
|
|
282
290
|
validateStart() {
|
|
283
|
-
if (deps.
|
|
291
|
+
if (deps.journal.getAcceptedThroughUpdateId() !== undefined) return;
|
|
284
292
|
if (deps.journal.getBootstrapEntryCount() === 0) return;
|
|
285
293
|
throw new TelegramPollingCursorBootstrapError(
|
|
286
294
|
"Telegram polling cursor is missing while the durable update journal is non-empty.",
|
|
@@ -356,6 +364,7 @@ export function createTelegramPollingControllerRuntime<
|
|
|
356
364
|
getUpdatesRequestBudgetMs: deps.getUpdatesRequestBudgetMs,
|
|
357
365
|
persistConfig: deps.persistConfig,
|
|
358
366
|
appendUpdateBatch: deps.appendUpdateBatch,
|
|
367
|
+
getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
|
|
359
368
|
getJournalEntryCount: deps.getJournalEntryCount,
|
|
360
369
|
signalUpdateWorker: deps.signalUpdateWorker,
|
|
361
370
|
prepareUpdateBatch: deps.prepareUpdateBatch,
|
|
@@ -1185,6 +1194,32 @@ export class TelegramPollingCursorBootstrapError extends Error {
|
|
|
1185
1194
|
}
|
|
1186
1195
|
}
|
|
1187
1196
|
|
|
1197
|
+
export interface TelegramPollingCursorCutoverDeps {
|
|
1198
|
+
getLegacyCursor: () => number | undefined;
|
|
1199
|
+
readJournal: () => {
|
|
1200
|
+
acceptedThroughUpdateId?: number;
|
|
1201
|
+
entries: readonly { updateId: number }[];
|
|
1202
|
+
};
|
|
1203
|
+
publishJournalCursor: (acceptedThroughUpdateId: number) => MaybePromise<void>;
|
|
1204
|
+
removeLegacyCursor: () => MaybePromise<void>;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
/** Transfer one legacy config cursor into journal authority before deleting it. */
|
|
1208
|
+
export async function cutOverTelegramPollingCursor(
|
|
1209
|
+
deps: TelegramPollingCursorCutoverDeps,
|
|
1210
|
+
): Promise<void> {
|
|
1211
|
+
const legacyCursor = deps.getLegacyCursor();
|
|
1212
|
+
if (legacyCursor === undefined) return;
|
|
1213
|
+
const snapshot = deps.readJournal();
|
|
1214
|
+
if (snapshot.acceptedThroughUpdateId === undefined) {
|
|
1215
|
+
const provenEntryCursor = snapshot.entries.at(-1)?.updateId;
|
|
1216
|
+
await deps.publishJournalCursor(
|
|
1217
|
+
Math.max(legacyCursor, provenEntryCursor ?? legacyCursor),
|
|
1218
|
+
);
|
|
1219
|
+
}
|
|
1220
|
+
await deps.removeLegacyCursor();
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1188
1223
|
export interface TelegramPollingBatchAdmissionResult {
|
|
1189
1224
|
updateCount: number;
|
|
1190
1225
|
latestUpdateId?: number;
|
|
@@ -1195,7 +1230,11 @@ export interface TelegramPollingBatchAdmissionDeps<
|
|
|
1195
1230
|
> extends TelegramRuntimeEventRecorderPort {
|
|
1196
1231
|
updates: readonly TUpdate[];
|
|
1197
1232
|
config: TelegramPollingConfig;
|
|
1198
|
-
appendBatch: (
|
|
1233
|
+
appendBatch: (
|
|
1234
|
+
updates: readonly TUpdate[],
|
|
1235
|
+
acceptedThroughUpdateId?: number,
|
|
1236
|
+
) => MaybePromise<unknown>;
|
|
1237
|
+
getAcceptedThroughUpdateId?: () => number | undefined;
|
|
1199
1238
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
1200
1239
|
signalWorker: () => void;
|
|
1201
1240
|
onPhaseChange?: (
|
|
@@ -1229,7 +1268,8 @@ export async function admitTelegramPollingUpdateBatch<
|
|
|
1229
1268
|
deps: TelegramPollingBatchAdmissionDeps<TUpdate>,
|
|
1230
1269
|
): Promise<TelegramPollingBatchAdmissionResult> {
|
|
1231
1270
|
if (deps.updates.length === 0) return { updateCount: 0 };
|
|
1232
|
-
|
|
1271
|
+
const acceptedThroughUpdateId = deps.getAcceptedThroughUpdateId?.();
|
|
1272
|
+
validateTelegramPollingBatch(deps.updates, acceptedThroughUpdateId);
|
|
1233
1273
|
const latestUpdateId = getLatestTelegramUpdateId(deps.updates);
|
|
1234
1274
|
if (latestUpdateId === undefined) return { updateCount: 0 };
|
|
1235
1275
|
reportTelegramPollingPhase(
|
|
@@ -1237,18 +1277,7 @@ export async function admitTelegramPollingUpdateBatch<
|
|
|
1237
1277
|
"persisting-journal",
|
|
1238
1278
|
deps.updates[0]?.update_id,
|
|
1239
1279
|
);
|
|
1240
|
-
await deps.appendBatch(deps.updates);
|
|
1241
|
-
reportTelegramPollingPhase(deps, "persisting-offset", latestUpdateId);
|
|
1242
|
-
const previousUpdateId = deps.config.lastUpdateId;
|
|
1243
|
-
deps.config.lastUpdateId = latestUpdateId;
|
|
1244
|
-
try {
|
|
1245
|
-
await deps.persistConfig(deps.config);
|
|
1246
|
-
} catch (error) {
|
|
1247
|
-
if (deps.config.lastUpdateId === latestUpdateId) {
|
|
1248
|
-
deps.config.lastUpdateId = previousUpdateId;
|
|
1249
|
-
}
|
|
1250
|
-
throw error;
|
|
1251
|
-
}
|
|
1280
|
+
await deps.appendBatch(deps.updates, latestUpdateId);
|
|
1252
1281
|
try {
|
|
1253
1282
|
deps.signalWorker();
|
|
1254
1283
|
} catch (error) {
|
|
@@ -1275,7 +1304,11 @@ export interface TelegramPollLoopDeps<
|
|
|
1275
1304
|
) => Promise<TUpdate[]>;
|
|
1276
1305
|
getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
|
|
1277
1306
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
1278
|
-
appendUpdateBatch: (
|
|
1307
|
+
appendUpdateBatch: (
|
|
1308
|
+
updates: readonly TUpdate[],
|
|
1309
|
+
acceptedThroughUpdateId?: number,
|
|
1310
|
+
) => MaybePromise<unknown>;
|
|
1311
|
+
getAcceptedThroughUpdateId?: () => number | undefined;
|
|
1279
1312
|
getJournalEntryCount: () => number;
|
|
1280
1313
|
signalUpdateWorker: () => void;
|
|
1281
1314
|
prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
|
|
@@ -1301,7 +1334,11 @@ export interface TelegramPollLoopRunnerDeps<
|
|
|
1301
1334
|
) => Promise<TUpdate[]>;
|
|
1302
1335
|
getUpdatesRequestBudgetMs?: (body: Record<string, unknown>) => number;
|
|
1303
1336
|
persistConfig: (config: TelegramPollingConfig) => Promise<void>;
|
|
1304
|
-
appendUpdateBatch: (
|
|
1337
|
+
appendUpdateBatch: (
|
|
1338
|
+
updates: readonly TUpdate[],
|
|
1339
|
+
acceptedThroughUpdateId?: number,
|
|
1340
|
+
) => MaybePromise<unknown>;
|
|
1341
|
+
getAcceptedThroughUpdateId?: () => number | undefined;
|
|
1305
1342
|
getJournalEntryCount: () => number;
|
|
1306
1343
|
signalUpdateWorker: () => void;
|
|
1307
1344
|
prepareUpdateBatch?: (updates: readonly TUpdate[]) => void;
|
|
@@ -1356,6 +1393,7 @@ export function createTelegramPollLoopRunner<
|
|
|
1356
1393
|
getUpdatesRequestBudgetMs: deps.getUpdatesRequestBudgetMs,
|
|
1357
1394
|
persistConfig: deps.persistConfig,
|
|
1358
1395
|
appendUpdateBatch: deps.appendUpdateBatch,
|
|
1396
|
+
getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
|
|
1359
1397
|
getJournalEntryCount: deps.getJournalEntryCount,
|
|
1360
1398
|
signalUpdateWorker: deps.signalUpdateWorker,
|
|
1361
1399
|
prepareUpdateBatch: deps.prepareUpdateBatch,
|
|
@@ -1494,14 +1532,16 @@ export async function runTelegramPollLoop<
|
|
|
1494
1532
|
// ignore
|
|
1495
1533
|
}
|
|
1496
1534
|
if (
|
|
1497
|
-
deps.
|
|
1535
|
+
deps.getAcceptedThroughUpdateId?.() === undefined &&
|
|
1498
1536
|
deps.getJournalEntryCount() > 0
|
|
1499
1537
|
) {
|
|
1500
1538
|
throw new TelegramPollingCursorBootstrapError(
|
|
1501
1539
|
"Telegram polling cursor is missing while the durable update journal is non-empty.",
|
|
1502
1540
|
);
|
|
1503
1541
|
}
|
|
1504
|
-
if (
|
|
1542
|
+
if (
|
|
1543
|
+
deps.getAcceptedThroughUpdateId?.() === undefined
|
|
1544
|
+
) {
|
|
1505
1545
|
try {
|
|
1506
1546
|
const request = buildTelegramInitialSyncRequest();
|
|
1507
1547
|
reportTelegramPollingPhase(deps, "long-poll");
|
|
@@ -1514,8 +1554,7 @@ export async function runTelegramPollLoop<
|
|
|
1514
1554
|
"persisting-offset",
|
|
1515
1555
|
lastUpdateId,
|
|
1516
1556
|
);
|
|
1517
|
-
deps.
|
|
1518
|
-
await deps.persistConfig(deps.config);
|
|
1557
|
+
await deps.appendUpdateBatch([], lastUpdateId);
|
|
1519
1558
|
deps.recordRuntimeEvent?.(
|
|
1520
1559
|
"polling",
|
|
1521
1560
|
new Error("Initialized Telegram cursor without executing history."),
|
|
@@ -1538,7 +1577,9 @@ export async function runTelegramPollLoop<
|
|
|
1538
1577
|
while (!deps.signal.aborted) {
|
|
1539
1578
|
try {
|
|
1540
1579
|
currentUpdateId = undefined;
|
|
1541
|
-
const request = buildTelegramLongPollRequest(
|
|
1580
|
+
const request = buildTelegramLongPollRequest(
|
|
1581
|
+
deps.getAcceptedThroughUpdateId?.(),
|
|
1582
|
+
);
|
|
1542
1583
|
reportTelegramPollingPhase(deps, "long-poll");
|
|
1543
1584
|
const updates = await requestTelegramUpdatesWithinBudget(deps, request);
|
|
1544
1585
|
reportTelegramPollingResponse(deps, updates.length);
|
|
@@ -1549,6 +1590,7 @@ export async function runTelegramPollLoop<
|
|
|
1549
1590
|
updates,
|
|
1550
1591
|
config: deps.config,
|
|
1551
1592
|
appendBatch: deps.appendUpdateBatch,
|
|
1593
|
+
getAcceptedThroughUpdateId: deps.getAcceptedThroughUpdateId,
|
|
1552
1594
|
persistConfig: deps.persistConfig,
|
|
1553
1595
|
signalWorker: deps.signalUpdateWorker,
|
|
1554
1596
|
onPhaseChange: deps.onPhaseChange,
|
|
@@ -321,7 +321,6 @@ export interface TelegramBridgeStatusConfig {
|
|
|
321
321
|
botToken?: string;
|
|
322
322
|
botUsername?: string;
|
|
323
323
|
allowedUserId?: number;
|
|
324
|
-
lastUpdateId?: number;
|
|
325
324
|
}
|
|
326
325
|
|
|
327
326
|
export interface TelegramBridgeStatusRuntimeDeps<
|
|
@@ -337,6 +336,7 @@ export interface TelegramBridgeStatusRuntimeDeps<
|
|
|
337
336
|
isPollingActive: () => boolean;
|
|
338
337
|
getPollingState?: () => TelegramBridgePollingState;
|
|
339
338
|
getInboundWorkerState?: () => TelegramBridgeInboundWorkerState | undefined;
|
|
339
|
+
getAcceptedThroughUpdateId?: () => number | undefined;
|
|
340
340
|
getActiveSourceMessageIds: () => number[] | undefined;
|
|
341
341
|
hasActiveTurn: () => boolean;
|
|
342
342
|
hasDispatchPending: () => boolean;
|
|
@@ -717,7 +717,7 @@ export function createTelegramBridgeStatusRuntime<
|
|
|
717
717
|
...(deps.getInboundWorkerState
|
|
718
718
|
? { inboundWorker: deps.getInboundWorkerState() }
|
|
719
719
|
: {}),
|
|
720
|
-
lastUpdateId:
|
|
720
|
+
lastUpdateId: deps.getAcceptedThroughUpdateId?.(),
|
|
721
721
|
activeSourceMessageIds: deps.getActiveSourceMessageIds(),
|
|
722
722
|
pendingDispatch: deps.hasDispatchPending(),
|
|
723
723
|
compactionInProgress: deps.isCompactionInProgress(),
|