@llblab/pi-telegram 0.23.0 → 0.23.2
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 +19 -12
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +42 -59
- package/README.md +3 -3
- package/docs/architecture.md +7 -8
- package/docs/multi-instance-bus.md +1 -1
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/docs/ui-style.md +1 -1
- package/docs/voice.md +8 -9
- package/index.ts +8 -7
- package/lib/bindings.ts +15 -16
- package/lib/bus-leader.ts +11 -128
- package/lib/config.ts +32 -14
- package/lib/menu-settings.ts +37 -97
- package/lib/prompts.ts +4 -2
- package/lib/queue.ts +52 -8
- package/lib/routing.ts +10 -16
- package/lib/status.ts +1 -1
- package/lib/text-groups.ts +29 -15
- package/lib/thread-reconciler.ts +3 -71
- package/lib/threads.ts +0 -43
- package/lib/turns.ts +25 -21
- package/lib/voice.ts +6 -7
- package/package.json +3 -2
package/lib/menu-settings.ts
CHANGED
|
@@ -4,10 +4,6 @@
|
|
|
4
4
|
* Owns hidden settings-menu rendering, settings callbacks, and persisted toggle wiring
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import {
|
|
8
|
-
getTelegramExtensionSettingsRows,
|
|
9
|
-
type TelegramSectionRegistry,
|
|
10
|
-
} from "./sections.ts";
|
|
11
7
|
import type {
|
|
12
8
|
TelegramAssistantRenderingMode,
|
|
13
9
|
TelegramTimeMode,
|
|
@@ -15,6 +11,10 @@ import type {
|
|
|
15
11
|
import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
|
|
16
12
|
import type { TelegramModelMenuState } from "./menu-model.ts";
|
|
17
13
|
import type { MenuModel } from "./model.ts";
|
|
14
|
+
import {
|
|
15
|
+
getTelegramExtensionSettingsRows,
|
|
16
|
+
type TelegramSectionRegistry,
|
|
17
|
+
} from "./sections.ts";
|
|
18
18
|
import type { TelegramVoiceReplyMode } from "./voice.ts";
|
|
19
19
|
|
|
20
20
|
export type TelegramSettingsMenuReplyMarkup = TelegramInlineKeyboardMarkup;
|
|
@@ -201,10 +201,9 @@ export function buildVoiceReplyModeSettingsText(
|
|
|
201
201
|
"",
|
|
202
202
|
"Controls when pi-telegram converts assistant text replies into Telegram voice messages.",
|
|
203
203
|
"",
|
|
204
|
-
"<code>-</code> <code>hidden</code> (default):
|
|
205
|
-
"<code>-</code> <code>
|
|
206
|
-
"<code>-</code> <code>
|
|
207
|
-
"<code>-</code> <code>always</code>: every reply is converted to voice when delivery succeeds.",
|
|
204
|
+
"<code>-</code> <code>hidden</code> (default): add no automatic voice context; explicit 'telegram_voice' actions still work.",
|
|
205
|
+
"<code>-</code> <code>mirror</code>: voice input activates automatic voice delivery; text input follows 'hidden' behavior.",
|
|
206
|
+
"<code>-</code> <code>always</code>: activate automatic voice delivery for every reply.",
|
|
208
207
|
].join("\n");
|
|
209
208
|
}
|
|
210
209
|
|
|
@@ -225,18 +224,19 @@ export function buildTimeInjectionModeSettingsText(
|
|
|
225
224
|
export function buildTelegramSettingsMenuReplyMarkup(
|
|
226
225
|
proactivePushEnabled: boolean,
|
|
227
226
|
draftPreviewsEnabled: boolean,
|
|
228
|
-
assistantRenderingModeOrVoiceReplyMode:
|
|
227
|
+
assistantRenderingModeOrVoiceReplyMode:
|
|
228
|
+
TelegramAssistantRenderingMode | TelegramVoiceReplyMode,
|
|
229
229
|
voiceReplyModeOrTimeInjectionMode: TelegramVoiceReplyMode | TelegramTimeMode,
|
|
230
|
-
timeInjectionModeOrSectionRegistry?:
|
|
230
|
+
timeInjectionModeOrSectionRegistry?:
|
|
231
|
+
TelegramTimeMode | TelegramSectionRegistry,
|
|
231
232
|
sectionRegistryOrVoiceReplyModeConfigured?: TelegramSectionRegistry | boolean,
|
|
232
233
|
voiceReplyModeConfigured = true,
|
|
233
234
|
): TelegramSettingsMenuReplyMarkup {
|
|
234
235
|
const hasRenderingMode =
|
|
235
236
|
assistantRenderingModeOrVoiceReplyMode === "rich" ||
|
|
236
237
|
assistantRenderingModeOrVoiceReplyMode === "html";
|
|
237
|
-
const assistantRenderingMode: TelegramAssistantRenderingMode =
|
|
238
|
-
? assistantRenderingModeOrVoiceReplyMode
|
|
239
|
-
: "rich";
|
|
238
|
+
const assistantRenderingMode: TelegramAssistantRenderingMode =
|
|
239
|
+
hasRenderingMode ? assistantRenderingModeOrVoiceReplyMode : "rich";
|
|
240
240
|
const voiceReplyMode = hasRenderingMode
|
|
241
241
|
? (voiceReplyModeOrTimeInjectionMode as TelegramVoiceReplyMode)
|
|
242
242
|
: (assistantRenderingModeOrVoiceReplyMode as TelegramVoiceReplyMode);
|
|
@@ -245,9 +245,9 @@ export function buildTelegramSettingsMenuReplyMarkup(
|
|
|
245
245
|
: (voiceReplyModeOrTimeInjectionMode as TelegramTimeMode);
|
|
246
246
|
const sectionRegistry = hasRenderingMode
|
|
247
247
|
? (sectionRegistryOrVoiceReplyModeConfigured as
|
|
248
|
-
|
|
|
249
|
-
|
|
250
|
-
|
|
248
|
+
TelegramSectionRegistry | undefined)
|
|
249
|
+
: (timeInjectionModeOrSectionRegistry as
|
|
250
|
+
TelegramSectionRegistry | undefined);
|
|
251
251
|
const effectiveVoiceReplyModeConfigured = hasRenderingMode
|
|
252
252
|
? voiceReplyModeConfigured
|
|
253
253
|
: typeof sectionRegistryOrVoiceReplyModeConfigured === "boolean"
|
|
@@ -268,7 +268,10 @@ export function buildTelegramSettingsMenuReplyMarkup(
|
|
|
268
268
|
{
|
|
269
269
|
text: `👄 Voice reply: ${getTelegramSettingsStateValueLabel(
|
|
270
270
|
getVoiceReplyModeLabel(
|
|
271
|
-
getVoiceReplyModeSetting(
|
|
271
|
+
getVoiceReplyModeSetting(
|
|
272
|
+
voiceReplyMode,
|
|
273
|
+
effectiveVoiceReplyModeConfigured,
|
|
274
|
+
),
|
|
272
275
|
),
|
|
273
276
|
)}`,
|
|
274
277
|
callback_data: "settings:open:voice-reply",
|
|
@@ -361,7 +364,7 @@ export function buildDraftPreviewsSettingsReplyMarkup(
|
|
|
361
364
|
},
|
|
362
365
|
{
|
|
363
366
|
text: enabled ? "⚫️ Off" : "🟡 Off",
|
|
364
|
-
callback_data: "settings:set:draft-previews:off"
|
|
367
|
+
callback_data: "settings:set:draft-previews:off",
|
|
365
368
|
},
|
|
366
369
|
],
|
|
367
370
|
],
|
|
@@ -407,12 +410,7 @@ export function buildVoiceReplyModeSettingsReplyMarkup(
|
|
|
407
410
|
configured = true,
|
|
408
411
|
): TelegramSettingsMenuReplyMarkup {
|
|
409
412
|
const activeMode = getVoiceReplyModeSetting(mode, configured);
|
|
410
|
-
const modes: TelegramVoiceReplyModeSetting[] = [
|
|
411
|
-
"hidden",
|
|
412
|
-
"manual",
|
|
413
|
-
"mirror",
|
|
414
|
-
"always",
|
|
415
|
-
];
|
|
413
|
+
const modes: TelegramVoiceReplyModeSetting[] = ["hidden", "mirror", "always"];
|
|
416
414
|
return {
|
|
417
415
|
inline_keyboard: [
|
|
418
416
|
[{ text: "⬆️ Back", callback_data: "settings:list" }],
|
|
@@ -511,7 +509,10 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
511
509
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
512
510
|
return true;
|
|
513
511
|
}
|
|
514
|
-
if (
|
|
512
|
+
if (
|
|
513
|
+
data === "settings:open:draft-previews" ||
|
|
514
|
+
data === "settings:open:rich-drafts"
|
|
515
|
+
) {
|
|
515
516
|
await updateDraftPreviewsSettingsMessage(deps);
|
|
516
517
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
517
518
|
return true;
|
|
@@ -536,12 +537,7 @@ export async function handleTelegramSettingsMenuCallbackAction(
|
|
|
536
537
|
}
|
|
537
538
|
if (data.startsWith("settings:set:voice-reply:")) {
|
|
538
539
|
const mode = data.slice("settings:set:voice-reply:".length);
|
|
539
|
-
if (
|
|
540
|
-
mode === "hidden" ||
|
|
541
|
-
mode === "manual" ||
|
|
542
|
-
mode === "mirror" ||
|
|
543
|
-
mode === "always"
|
|
544
|
-
) {
|
|
540
|
+
if (mode === "hidden" || mode === "mirror" || mode === "always") {
|
|
545
541
|
await deps.setVoiceReplyMode(mode === "hidden" ? undefined : mode);
|
|
546
542
|
await updateVoiceReplyModeSettingsMessage(deps);
|
|
547
543
|
await deps.answerCallbackQuery(
|
|
@@ -663,79 +659,23 @@ export function createTelegramSettingsMenuRuntime<
|
|
|
663
659
|
},
|
|
664
660
|
sectionRegistry,
|
|
665
661
|
),
|
|
666
|
-
handleCallbackQuery: async (query) => {
|
|
662
|
+
handleCallbackQuery: async (query, ctx) => {
|
|
667
663
|
if (!query.data?.startsWith("settings:")) return false;
|
|
668
|
-
const
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
);
|
|
664
|
+
const messageId = query.message?.message_id;
|
|
665
|
+
const chatId = query.message?.chat?.id;
|
|
666
|
+
let state = deps.getStoredModelMenuState(messageId, chatId);
|
|
672
667
|
if (!state) {
|
|
673
|
-
|
|
674
|
-
if (
|
|
675
|
-
query.data.startsWith("settings:set:voice-reply:") &&
|
|
676
|
-
(voiceMode === "hidden" ||
|
|
677
|
-
voiceMode === "manual" ||
|
|
678
|
-
voiceMode === "mirror" ||
|
|
679
|
-
voiceMode === "always")
|
|
680
|
-
) {
|
|
681
|
-
await deps.setVoiceReplyMode(
|
|
682
|
-
voiceMode === "hidden" ? undefined : voiceMode,
|
|
683
|
-
);
|
|
684
|
-
await deps.answerCallbackQuery(
|
|
685
|
-
query.id,
|
|
686
|
-
`Voice reply mode: ${voiceMode}`,
|
|
687
|
-
);
|
|
688
|
-
return true;
|
|
689
|
-
}
|
|
690
|
-
if (
|
|
691
|
-
query.data === "settings:set:draft-previews:on" ||
|
|
692
|
-
query.data === "settings:set:draft-previews:off" ||
|
|
693
|
-
query.data === "settings:set:rich-drafts:on" ||
|
|
694
|
-
query.data === "settings:set:rich-drafts:off"
|
|
695
|
-
) {
|
|
696
|
-
const enabled = query.data.endsWith(":on");
|
|
697
|
-
await deps.setDraftPreviewsEnabled(enabled);
|
|
698
|
-
await deps.answerCallbackQuery(
|
|
699
|
-
query.id,
|
|
700
|
-
`Draft previews ${enabled ? "enabled" : "disabled"}`,
|
|
701
|
-
);
|
|
702
|
-
return true;
|
|
703
|
-
}
|
|
704
|
-
if (query.data.startsWith("settings:set:assistant-rendering:")) {
|
|
705
|
-
const mode = query.data.slice("settings:set:assistant-rendering:".length);
|
|
706
|
-
if (mode === "rich" || mode === "html") {
|
|
707
|
-
await deps.setAssistantRenderingMode(mode);
|
|
708
|
-
await deps.answerCallbackQuery(query.id, `Rendering: ${mode}`);
|
|
709
|
-
return true;
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
const hasTimeInjectionPrefix = query.data.startsWith(
|
|
713
|
-
"settings:set:time-injection:",
|
|
714
|
-
);
|
|
715
|
-
const timeMode = hasTimeInjectionPrefix
|
|
716
|
-
? query.data.slice("settings:set:time-injection:".length)
|
|
717
|
-
: query.data.slice("settings:set:time:".length);
|
|
718
|
-
if (
|
|
719
|
-
(hasTimeInjectionPrefix ||
|
|
720
|
-
query.data.startsWith("settings:set:time:")) &&
|
|
721
|
-
(timeMode === "off" ||
|
|
722
|
-
timeMode === "hidden" ||
|
|
723
|
-
timeMode === "always" ||
|
|
724
|
-
timeMode === "interval")
|
|
725
|
-
) {
|
|
726
|
-
const normalizedMode = timeMode === "off" ? "hidden" : timeMode;
|
|
727
|
-
await deps.setTimeInjectionMode(normalizedMode);
|
|
668
|
+
if (typeof messageId !== "number" || typeof chatId !== "number") {
|
|
728
669
|
await deps.answerCallbackQuery(
|
|
729
670
|
query.id,
|
|
730
|
-
|
|
671
|
+
"Interactive message expired.",
|
|
731
672
|
);
|
|
732
673
|
return true;
|
|
733
674
|
}
|
|
734
|
-
await deps.
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
);
|
|
738
|
-
return true;
|
|
675
|
+
state = await deps.getModelMenuState(chatId, ctx);
|
|
676
|
+
state.messageId = messageId;
|
|
677
|
+
state.mode = "settings";
|
|
678
|
+
deps.storeModelMenuState(state);
|
|
739
679
|
}
|
|
740
680
|
return handleTelegramSettingsMenuCallbackAction(query.id, query.data, {
|
|
741
681
|
isProactivePushEnabled: deps.isProactivePushEnabled,
|
package/lib/prompts.ts
CHANGED
|
@@ -16,7 +16,7 @@ Telegram bridge available. Do not use it from local/TUI prompts unless explicitl
|
|
|
16
16
|
|
|
17
17
|
const TELEGRAM_TURN_SYSTEM_PROMPT_SUFFIX = `
|
|
18
18
|
|
|
19
|
-
Telegram turn note:
|
|
19
|
+
Telegram turn note: Call \`telegram_help\` if you need the pi-telegram bridge action contract.`;
|
|
20
20
|
|
|
21
21
|
function buildTelegramHelpText(profileName?: string): string {
|
|
22
22
|
const diagnosticsPaths = getTelegramDiagnosticsDisplayPaths(profileName);
|
|
@@ -26,7 +26,8 @@ How to understand Telegram turns:
|
|
|
26
26
|
- \`[telegram|thread:name|from:user|guest:group]\` marks Telegram origin and attributes.
|
|
27
27
|
- \`thread\` is the visible Thread identity in Threaded Mode; it is not a bus role.
|
|
28
28
|
- \`[reply]\` is quoted context only; act on the user's current instruction.
|
|
29
|
-
- \`[attachments]\` are local files; \`[outputs]\` are handler results/transcripts; \`[time]\` is wall-clock context
|
|
29
|
+
- \`[attachments]\` are local files; \`[outputs]\` are handler results/transcripts; \`[time]\` is wall-clock context.
|
|
30
|
+
- \`[voice] delivery: automatic voice\` means pi-telegram will synthesize ordinary assistant text for this turn; no \`[voice]\` line means no automatic voice policy.
|
|
30
31
|
|
|
31
32
|
How to answer Telegram turns:
|
|
32
33
|
- Reply in concise, scannable mobile Telegram Rich Markdown.
|
|
@@ -41,6 +42,7 @@ Assistant-authored Telegram actions:
|
|
|
41
42
|
- Keep the complete action at top level and include a non-empty voice payload.
|
|
42
43
|
- Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
|
|
43
44
|
- Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
|
|
45
|
+
- Voice reply modes are compact: \`hidden\` emits no automatic context, \`mirror\` emits it for voice/audio input, and \`always\` emits it for every Telegram turn. Explicit \`telegram_voice\` remains available for an intentionally distinct spoken payload.
|
|
44
46
|
- Button forms: \`<!-- telegram_button: OK -->\`, \`<!-- telegram_button label=Continue prompt="Continue with the current plan." -->\`, or multiline \`<!-- telegram_button label="Show risks"\nList the main risks first.\n-->\`.
|
|
45
47
|
- If hidden comments would be the whole reply, add visible text such as \`Choose one:\`.
|
|
46
48
|
|
package/lib/queue.ts
CHANGED
|
@@ -850,14 +850,55 @@ export function createTelegramAgentLifecycleHooks<
|
|
|
850
850
|
TReplyMarkup
|
|
851
851
|
>,
|
|
852
852
|
) {
|
|
853
|
+
const onAgentStart = createTelegramAgentStartHook<TTurn, TContext>(deps);
|
|
854
|
+
const deliverAgentEnd = createTelegramAgentEndHook<
|
|
855
|
+
TTurn,
|
|
856
|
+
TContext,
|
|
857
|
+
TMessage,
|
|
858
|
+
TReplyMarkup
|
|
859
|
+
>(deps);
|
|
860
|
+
let retainedErrorEvent: TelegramAgentEndHookEvent<TMessage> | undefined;
|
|
853
861
|
return {
|
|
854
|
-
onAgentStart
|
|
855
|
-
onAgentEnd
|
|
856
|
-
|
|
857
|
-
TContext,
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
862
|
+
onAgentStart,
|
|
863
|
+
async onAgentEnd(
|
|
864
|
+
event: TelegramAgentEndHookEvent<TMessage>,
|
|
865
|
+
ctx: TContext,
|
|
866
|
+
): Promise<void> {
|
|
867
|
+
const turn = deps.getActiveTurn();
|
|
868
|
+
const assistant = turn ? deps.extractAssistant(event.messages) : {};
|
|
869
|
+
if (turn && assistant.stopReason === "error") {
|
|
870
|
+
retainedErrorEvent = event;
|
|
871
|
+
deps.recordRuntimeEvent?.(
|
|
872
|
+
"provider-retry",
|
|
873
|
+
new Error("Retained Telegram turn after retryable agent error"),
|
|
874
|
+
{ phase: "retained", hasFinalText: !!assistant.text?.trim() },
|
|
875
|
+
);
|
|
876
|
+
return;
|
|
877
|
+
}
|
|
878
|
+
if (retainedErrorEvent) {
|
|
879
|
+
retainedErrorEvent = undefined;
|
|
880
|
+
deps.recordRuntimeEvent?.(
|
|
881
|
+
"provider-retry",
|
|
882
|
+
new Error("Recovered retained Telegram turn after agent retry"),
|
|
883
|
+
{ phase: "recovered" },
|
|
884
|
+
);
|
|
885
|
+
}
|
|
886
|
+
await deliverAgentEnd(event, ctx, assistant);
|
|
887
|
+
},
|
|
888
|
+
async onAgentSettled(_event: unknown, ctx: TContext): Promise<void> {
|
|
889
|
+
const event = retainedErrorEvent;
|
|
890
|
+
if (!event) return;
|
|
891
|
+
retainedErrorEvent = undefined;
|
|
892
|
+
deps.recordRuntimeEvent?.(
|
|
893
|
+
"provider-retry",
|
|
894
|
+
new Error("Finalized retained Telegram turn after agent settled"),
|
|
895
|
+
{ phase: "settled-failure" },
|
|
896
|
+
);
|
|
897
|
+
await deliverAgentEnd(event, ctx);
|
|
898
|
+
},
|
|
899
|
+
clearRetainedAgentEnd(): void {
|
|
900
|
+
retainedErrorEvent = undefined;
|
|
901
|
+
},
|
|
861
902
|
...createTelegramToolExecutionHooks<TContext>(deps),
|
|
862
903
|
};
|
|
863
904
|
}
|
|
@@ -1132,13 +1173,16 @@ export function createTelegramAgentEndHook<
|
|
|
1132
1173
|
return async (
|
|
1133
1174
|
event: TelegramAgentEndHookEvent<TMessage>,
|
|
1134
1175
|
ctx: TContext,
|
|
1176
|
+
assistantOverride?: TelegramAgentEndAssistantResult,
|
|
1135
1177
|
): Promise<void> => {
|
|
1136
1178
|
await deps.loadConfig?.();
|
|
1137
1179
|
if (deps.isSessionActive && !deps.isSessionActive(ctx)) return;
|
|
1138
1180
|
const turn = deps.getActiveTurn();
|
|
1139
1181
|
await handleTelegramAgentEndRuntime({
|
|
1140
1182
|
turn,
|
|
1141
|
-
assistant:
|
|
1183
|
+
assistant:
|
|
1184
|
+
assistantOverride ??
|
|
1185
|
+
(turn ? deps.extractAssistant(event.messages) : {}),
|
|
1142
1186
|
foldQueuedPromptsIntoHistory: deps.getFoldQueuedPromptsIntoHistory(),
|
|
1143
1187
|
resetRuntimeState: deps.resetRuntimeState,
|
|
1144
1188
|
isSessionActive: () => deps.isSessionActive?.(ctx) ?? true,
|
package/lib/routing.ts
CHANGED
|
@@ -9,7 +9,6 @@ import { basename, dirname } from "node:path";
|
|
|
9
9
|
import * as Bus from "./bus.ts";
|
|
10
10
|
import * as Commands from "./commands.ts";
|
|
11
11
|
import type { TelegramConfigStore } from "./config.ts";
|
|
12
|
-
import type { TelegramSectionRegistry } from "./sections.ts";
|
|
13
12
|
import type { TelegramInboundHandlerRuntime } from "./inbound.ts";
|
|
14
13
|
import * as Media from "./media.ts";
|
|
15
14
|
import * as Menu from "./menu.ts";
|
|
@@ -18,12 +17,13 @@ import * as OutboundHandlers from "./outbound.ts";
|
|
|
18
17
|
import * as PromptTemplates from "./prompt-templates.ts";
|
|
19
18
|
import * as Queue from "./queue.ts";
|
|
20
19
|
import type { TelegramBridgeRuntime } from "./runtime.ts";
|
|
20
|
+
import type { TelegramSectionRegistry } from "./sections.ts";
|
|
21
21
|
import * as TextGroups from "./text-groups.ts";
|
|
22
|
+
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
22
23
|
import type {
|
|
23
24
|
TelegramInstanceThreadIdentityCandidate,
|
|
24
25
|
TelegramTopicTargetRecord,
|
|
25
26
|
} from "./threads.ts";
|
|
26
|
-
import * as ThreadReconciler from "./thread-reconciler.ts";
|
|
27
27
|
import * as Turns from "./turns.ts";
|
|
28
28
|
|
|
29
29
|
interface TelegramPromptPeerView {
|
|
@@ -408,10 +408,10 @@ function formatTelegramUnboundRerouteChooserText(
|
|
|
408
408
|
: [formatTelegramUnboundTopicGuidance(), "", rerouteText].join("\n");
|
|
409
409
|
}
|
|
410
410
|
|
|
411
|
-
import { getTelegramVoiceReplyMode } from "./voice.ts";
|
|
412
|
-
import type { TelegramUser } from "./updates.ts";
|
|
413
411
|
import * as Threads from "./threads.ts";
|
|
412
|
+
import type { TelegramUser } from "./updates.ts";
|
|
414
413
|
import * as Updates from "./updates.ts";
|
|
414
|
+
import { getTelegramVoiceReplyMode } from "./voice.ts";
|
|
415
415
|
|
|
416
416
|
async function deleteReservedTelegramTopicThroughReconciler(
|
|
417
417
|
deps: {
|
|
@@ -1524,13 +1524,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1524
1524
|
resolveTimeLine: deps.resolveTimeLine,
|
|
1525
1525
|
getAllowedUserId: deps.configStore.getAllowedUserId,
|
|
1526
1526
|
|
|
1527
|
-
// Voice policy
|
|
1528
|
-
// but only explicit telegram.json voice.replyMode is shown in prompt context.
|
|
1527
|
+
// Voice policy resolves missing, invalid, and legacy manual config to hidden.
|
|
1529
1528
|
getVoiceReplyMode: () => getTelegramVoiceReplyMode(deps.configStore.get()),
|
|
1530
|
-
isVoiceReplyModeConfigured: () => {
|
|
1531
|
-
const mode = deps.configStore.get().voice?.replyMode;
|
|
1532
|
-
return mode === "manual" || mode === "mirror" || mode === "always";
|
|
1533
|
-
},
|
|
1534
1529
|
getTelegramThreadLabel(message) {
|
|
1535
1530
|
if (!deps.threadStore) return undefined;
|
|
1536
1531
|
const chatId = message.chat.id;
|
|
@@ -2490,7 +2485,9 @@ export interface TelegramAssistantOutputAuthorityRuntime<TTransportStamp> {
|
|
|
2490
2485
|
canDeliver: () => boolean;
|
|
2491
2486
|
}
|
|
2492
2487
|
|
|
2493
|
-
export function createTelegramAssistantOutputAuthorityRuntime<
|
|
2488
|
+
export function createTelegramAssistantOutputAuthorityRuntime<
|
|
2489
|
+
TTransportStamp,
|
|
2490
|
+
>(deps: {
|
|
2494
2491
|
getPreferredTarget: () => Queue.TelegramQueueTarget | undefined;
|
|
2495
2492
|
getFallbackChatId: () => number | undefined;
|
|
2496
2493
|
getTransportStamp: () => TTransportStamp;
|
|
@@ -2509,9 +2506,7 @@ export function createTelegramAssistantOutputAuthorityRuntime<TTransportStamp>(d
|
|
|
2509
2506
|
return {
|
|
2510
2507
|
captureAuthority() {
|
|
2511
2508
|
const target = getCurrentTarget();
|
|
2512
|
-
const directEpoch = deps.ownsDirect()
|
|
2513
|
-
? deps.getDirectEpoch()
|
|
2514
|
-
: undefined;
|
|
2509
|
+
const directEpoch = deps.ownsDirect() ? deps.getDirectEpoch() : undefined;
|
|
2515
2510
|
const followerGeneration = deps.isFollowerRegistered()
|
|
2516
2511
|
? deps.getFollowerGeneration()
|
|
2517
2512
|
: undefined;
|
|
@@ -2540,8 +2535,7 @@ export function createTelegramAssistantOutputAuthorityRuntime<TTransportStamp>(d
|
|
|
2540
2535
|
}
|
|
2541
2536
|
if (authority.route === "direct") {
|
|
2542
2537
|
return (
|
|
2543
|
-
deps.ownsDirect() &&
|
|
2544
|
-
deps.getDirectEpoch() === authority.directEpoch
|
|
2538
|
+
deps.ownsDirect() && deps.getDirectEpoch() === authority.directEpoch
|
|
2545
2539
|
);
|
|
2546
2540
|
}
|
|
2547
2541
|
if (authority.route === "follower") {
|
package/lib/status.ts
CHANGED
|
@@ -767,7 +767,7 @@ export function buildTelegramStatusBarText(
|
|
|
767
767
|
): string {
|
|
768
768
|
const label = theme.fg("accent", getTelegramStatusBarLabel(state));
|
|
769
769
|
if (state.error) {
|
|
770
|
-
return `${label} ${theme.fg("error", "error")}
|
|
770
|
+
return `${label} ${theme.fg("error", "error")}`;
|
|
771
771
|
}
|
|
772
772
|
const queued = state.queuedStatus
|
|
773
773
|
? theme.fg("success", state.queuedStatus)
|
package/lib/text-groups.ts
CHANGED
|
@@ -39,7 +39,7 @@ export interface TelegramTextGroupState<TMessage, TContext = unknown> {
|
|
|
39
39
|
suspended?: boolean;
|
|
40
40
|
reschedule?: (delayMs?: number) => void;
|
|
41
41
|
dispatchLimit?: number;
|
|
42
|
-
|
|
42
|
+
forwardPairCandidate?: TelegramForwardCommentBatchPosition;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
export type TelegramForwardCommentBatchPosition = "comment" | "forward";
|
|
@@ -131,7 +131,12 @@ function getTelegramTextGroupKey(
|
|
|
131
131
|
): string | undefined {
|
|
132
132
|
if (message.media_group_id) return undefined;
|
|
133
133
|
if (!message.from || message.from.is_bot) return undefined;
|
|
134
|
-
if (
|
|
134
|
+
if (
|
|
135
|
+
!extractTelegramTextGroupText(message) &&
|
|
136
|
+
!isTelegramForwardedMessage(message)
|
|
137
|
+
) {
|
|
138
|
+
return undefined;
|
|
139
|
+
}
|
|
135
140
|
const threadKey =
|
|
136
141
|
typeof message.message_thread_id === "number"
|
|
137
142
|
? `thread:${message.message_thread_id}`
|
|
@@ -160,7 +165,7 @@ function canAppendTelegramTextGroupMessage<
|
|
|
160
165
|
message.message_id > previous.message_id &&
|
|
161
166
|
message.message_id <=
|
|
162
167
|
previous.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP &&
|
|
163
|
-
text.length > 0 &&
|
|
168
|
+
(text.length > 0 || isTelegramForwardedMessage(message)) &&
|
|
164
169
|
!isTelegramTextGroupCommand(text)
|
|
165
170
|
);
|
|
166
171
|
}
|
|
@@ -182,7 +187,7 @@ export function queueTelegramTextGroupMessage<
|
|
|
182
187
|
) => unknown | Promise<unknown>;
|
|
183
188
|
forceStart?: boolean;
|
|
184
189
|
dispatchImmediately?: boolean;
|
|
185
|
-
|
|
190
|
+
forwardPairCandidate?: TelegramForwardCommentBatchPosition;
|
|
186
191
|
delayMs?: number;
|
|
187
192
|
}): boolean {
|
|
188
193
|
const key = getTelegramTextGroupKey(options.message);
|
|
@@ -202,7 +207,7 @@ export function queueTelegramTextGroupMessage<
|
|
|
202
207
|
const state = existing ?? { messages: [] };
|
|
203
208
|
state.messages.push(options.message);
|
|
204
209
|
state.context = options.context;
|
|
205
|
-
state.
|
|
210
|
+
state.forwardPairCandidate = options.forwardPairCandidate;
|
|
206
211
|
const dispatchQueued = (): void => {
|
|
207
212
|
state.flushTimer = undefined;
|
|
208
213
|
const queued = options.groups.get(key);
|
|
@@ -335,21 +340,30 @@ export function createTelegramTextGroupController<
|
|
|
335
340
|
if (existing.flushTimer) clearTimer(existing.flushTimer);
|
|
336
341
|
groups.delete(key!);
|
|
337
342
|
}
|
|
343
|
+
const candidatePosition: TelegramForwardCommentBatchPosition = forwarded
|
|
344
|
+
? "forward"
|
|
345
|
+
: "comment";
|
|
346
|
+
const existingCandidate = existing?.forwardPairCandidate;
|
|
347
|
+
const pairCompleted =
|
|
348
|
+
existingCandidate !== undefined &&
|
|
349
|
+
existingCandidate !== candidatePosition;
|
|
338
350
|
const separateFromCandidate =
|
|
339
|
-
|
|
340
|
-
!forwarded &&
|
|
351
|
+
existingCandidate === candidatePosition &&
|
|
341
352
|
!isTelegramTextGroupCommand(text);
|
|
342
|
-
if (separateFromCandidate) {
|
|
353
|
+
if (separateFromCandidate && existing) {
|
|
343
354
|
existing.dispatchLimit = existing.messages.length;
|
|
344
355
|
}
|
|
345
356
|
const forceStart =
|
|
346
357
|
plannedStart ||
|
|
347
358
|
(forwardCommentWaitMs !== false &&
|
|
348
|
-
!forwarded &&
|
|
349
359
|
!!key &&
|
|
350
|
-
|
|
351
|
-
|
|
360
|
+
(forwarded ||
|
|
361
|
+
(typeof message.text === "string" &&
|
|
362
|
+
!isTelegramTextGroupCommand(
|
|
363
|
+
extractTelegramTextGroupText(message),
|
|
364
|
+
))));
|
|
352
365
|
const dispatchImmediately =
|
|
366
|
+
pairCompleted ||
|
|
353
367
|
separateFromCandidate ||
|
|
354
368
|
plannedForwardCommentEnds.delete(identity) ||
|
|
355
369
|
(forwarded && !!key && groups.has(key));
|
|
@@ -364,10 +378,10 @@ export function createTelegramTextGroupController<
|
|
|
364
378
|
dispatchMessages,
|
|
365
379
|
forceStart,
|
|
366
380
|
dispatchImmediately,
|
|
367
|
-
|
|
368
|
-
forceStart &&
|
|
369
|
-
|
|
370
|
-
|
|
381
|
+
forwardPairCandidate:
|
|
382
|
+
forceStart && !canStartTelegramTextGroup(message, minSplitLength)
|
|
383
|
+
? candidatePosition
|
|
384
|
+
: undefined,
|
|
371
385
|
delayMs:
|
|
372
386
|
forceStart && !canStartTelegramTextGroup(message, minSplitLength)
|
|
373
387
|
? forwardCommentWaitMs === false
|