@llblab/pi-telegram 0.24.9 β 0.24.11
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 +3 -2
- package/CHANGELOG.md +10 -0
- package/README.md +2 -2
- package/docs/architecture.md +1 -1
- package/docs/outbound.md +17 -24
- package/docs/public-api.md +3 -3
- package/index.ts +4 -19
- package/lib/bus-api.ts +18 -0
- package/lib/bus.ts +14 -0
- package/lib/outbound-buttons.ts +30 -61
- package/lib/outbound-markup.ts +65 -162
- package/lib/polling.ts +7 -17
- package/lib/prompts.ts +4 -4
- package/lib/status.ts +10 -11
- package/lib/telegram-api.ts +12 -0
- package/package.json +1 -1
package/AGENTS.md
CHANGED
|
@@ -124,8 +124,9 @@ The core product loop is mobile continuation: start or supervise work in the ter
|
|
|
124
124
|
- Agent activity status is native-only for the release path: Telegram uses `sendChatAction(typing)` and renders its native `β¦typing` indicator as the automatic work signal. When a connected instance starts any real agent run, send that indicator to the instance's own thread (and aggregate `All` surface), including Telegram turns, local/TUI prompts, and autonomous continuations such as Grow Loop; stop it at agent/session completion. Keep at most one chat action in flight per loop, coalesce identical leader-side actions across follower traffic, and treat Telegram 429 `retry_after` as a shared suppression window rather than a delayed retry or persistent terminal error. This signal is independent from terminal `Active`, which remains scoped to Telegram-owned turns. Confirmed manual `/compact` and automatic compaction tied to an active Telegram turn also use native activity. Do not send activity merely for startup/connect/reload/recovery or add extra in-chat work surfaces without a new explicit design decision.
|
|
125
125
|
- Voice reply mode Settings UI standard: the top-level Settings row is `π Voice reply: hidden|mirror|always`; `hidden` is the true default, persists no `voice.replyMode`, adds no voice context, and still allows explicit agent-authored `telegram_voice`; `mirror` activates automatic voice context only for voice/audio input, and `always` activates it for every Telegram turn. The submenu title is `π Voice reply mode:`; choice buttons use lowercase labels with a model-style active dot (`π’ hidden`, `π’ mirror`) rather than per-mode emoji; the explanatory submenu body uses compact HTML-code bullets such as `<code>-</code> <code>hidden</code> (default): ...`. Preserve this wording/icons unless the operator explicitly asks to redesign it
|
|
126
126
|
- Outbound voice delivery is one fallback pipeline: configured `outboundHandlers` with `type: "voice"` run first in `telegram.json` order, then programmatic voice handlers, then registered voice synthesis providers as zero-config progressive fallbacks; provider extensions must not override operator-configured handlers
|
|
127
|
-
- `telegram_voice`
|
|
128
|
-
- `
|
|
127
|
+
- `telegram_voice` and `telegram_button` action comments have exactly two payload forms: a JSON object or double-quoted HTML-like attributes. An optional colon after the action name never selects or changes the payload format; the parser removes it before detecting `{...}` JSON or attributes. JSON is the universal form for long or escaped content, while attributes are the compact form. Keep each complete action in one top-level comment and encode JSON line breaks as `\n`; shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are invalid.
|
|
128
|
+
- `telegram_voice` accepts equivalent `text` or `value` keys, with explicit `text` taking precedence when both appear; optional `lang` and `rate` use the same payload object or attributes. Voice text is arbitrary TTS-target text; avoid raw Markdown, code, and tables.
|
|
129
|
+
- `telegram_button` accepts `label` plus `prompt`, or the compact `value` key when both strings are identical; explicitly supplied `label` or `prompt` takes precedence over the corresponding `value` fallback. Optional `selected_style="primary|success|danger"` selects blue, green, or red after queue admission, with `primary` as the default. The bridge preserves agent-authored label text and emoji, and color never suppresses the prompt. Keep comments out of code, quotes, lists, and indented examples; write normal Markdown plus top-level hidden comments, and let the bridge supply the standard `βοΈ **Choose an option:**` Rich Markdown heading when buttons are the only output
|
|
129
130
|
|
|
130
131
|
## 6. Engineering Conventions
|
|
131
132
|
|
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 0.24.11: Assistant Action Markup Hotfix
|
|
6
|
+
|
|
7
|
+
- `Assistant Action Markup`: Standardized `telegram_button` and `telegram_voice` on two model-facing payload forms: JSON objects and compact double-quoted attributes. The separator colon after either action name is optional and format-neutral, so mixed model output such as colon-prefixed attributes or colonless JSON parses identically. Buttons may use compact `value` when `label` and `prompt` are identical, while voice treats `text` and `value` as equivalent payload keys; explicit fields take precedence. Removed shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute parsing so one complete comment owns each action. Impact: models use one shared action grammar without duplicating equal button text or navigating hidden legacy variants.
|
|
8
|
+
- `Composition Root Cooling`: Moved current-process bus identity defaults into `bus`, moved generated-button reply-markup edits into the direct/bus-aware Telegram API runtimes, and removed the always-true bus-configuration callback from Threaded Mode orchestration. Impact: `index.ts` loses low-level process/time/API payload work and dormant policy branching while retaining visible high-level port wiring.
|
|
9
|
+
|
|
10
|
+
## 0.24.10: Compact Thread Role Hotfix
|
|
11
|
+
|
|
12
|
+
- `Status`: Threaded Mode status again appends only `@leader` or `@follower` to the execution status instead of repeating the visible Telegram tab name on a separate `Thread` row. Impact: compact `/status` output avoids redundant thread identity while retaining the transport role.
|
|
13
|
+
- `Validation`: Graceful-cleanup integration now accepts both fully committed removal and the safe ownership-fenced fallback of retaining the exact target plus durable cleanup intent after Telegram already confirmed deletion. Impact: Windows validation tests the recovery invariant instead of requiring one filesystem commit timing outcome.
|
|
14
|
+
|
|
5
15
|
## 0.24.9: Button-Only Heading Hotfix
|
|
6
16
|
|
|
7
17
|
- `Prompt Buttons`: The automatic parent text for button-only assistant replies now follows the bridge-owned dialog heading grammar as `βοΈ **Choose an option:**`. Impact: fallback button prompts match reasoning, model, and settings choice surfaces with a semantic icon, bold title, and trailing colon instead of appearing as unformatted assistant text.
|
package/README.md
CHANGED
|
@@ -179,11 +179,11 @@ Inbound files land under `<agent-dir>/tmp/telegram` and default to a 50 MiB limi
|
|
|
179
179
|
|
|
180
180
|
### Voice And Media
|
|
181
181
|
|
|
182
|
-
Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis.
|
|
182
|
+
Voice notes, audio, images, PDFs, and other media can pass through configured inbound handlers, programmatic handlers, or registered STT providers. Outbound voice can use configured `outboundHandlers` or registered TTS providers; `pi-telegram` owns reply policy and Telegram transport, while providers own synthesis. Explicit `telegram_voice` actions accept either a JSON object or compact double-quoted attributes, with equivalent `text` and `value` payload keys and an optional format-neutral colon after the action name.
|
|
183
183
|
|
|
184
184
|
### Buttons And Callbacks
|
|
185
185
|
|
|
186
|
-
Assistant replies can include top-level hidden `telegram_button` comments. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `βοΈ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
|
|
186
|
+
Assistant replies can include top-level hidden `telegram_button` comments using either a JSON object or compact double-quoted attributes. Buttons use `label` plus `prompt`, or the compact `value` key when both are identical. The optional colon after the action name never changes format detection. The bridge strips the comments from visible text, renders inline buttons, and routes callbacks back into Pi as queued prompts or extension-owned callback actions. Button-only replies receive the standard `βοΈ **Choose an option:**` heading as automatic visible fallback text. Once a generated prompt button is accepted, only that exact button switches to its optional `selected_style` (`primary` blue by default, `success` green, or `danger` red) without altering its agent-authored label or emoji; every style still queues the selected prompt.
|
|
187
187
|
|
|
188
188
|
### Threaded Mode And Multi-Instance Bus
|
|
189
189
|
|
package/docs/architecture.md
CHANGED
|
@@ -279,7 +279,7 @@ Final delivery attaches reply metadata only where requested. Reply parameters ap
|
|
|
279
279
|
|
|
280
280
|
Outbound files staged during an active Telegram turn are delivered after that turn completes. They use `telegram_attach`, are checked atomically per tool call, and use configurable size limits before photo/document upload. When no Telegram turn is active, `telegram_attach` sends files immediately to the paired/default chat, an assigned follower thread, or an explicit `chat_id` plus optional `thread_id`; `telegram_message` provides direct local/TUI Markdown text delivery for explicit user requests and runs the same `telegram_button` markup planner so buttons attach to that text message. Direct local/TUI delivery is singleton-controlled: classic mode requires this Pi instance to own `/telegram-connect`, while Threaded Mode followers must be registered and route through the leader-owned transport. Already accepted active-turn reply/attachment delivery remains session-local.
|
|
281
281
|
|
|
282
|
-
Assistant-authored final-message actions use hidden top-level comments:
|
|
282
|
+
Assistant-authored final-message actions use hidden top-level comments. Both actions accept a JSON object or double-quoted HTML-like attributes; an optional colon after the action name is format-neutral and stripped before payload detection:
|
|
283
283
|
|
|
284
284
|
- `telegram_voice` creates voice reply artifacts through configured outbound handlers, programmatic voice handlers, or registered synthesis providers.
|
|
285
285
|
- `telegram_button` creates inline buttons whose callbacks enqueue the configured prompt text as a normal Telegram prompt turn.
|
package/docs/outbound.md
CHANGED
|
@@ -106,51 +106,44 @@ Synthesis providers receive the extracted `telegram_voice` text plus optional `l
|
|
|
106
106
|
|
|
107
107
|
## Voice Markup
|
|
108
108
|
|
|
109
|
-
Assistant replies can include
|
|
109
|
+
Assistant replies can include hidden voice actions in either supported payload form:
|
|
110
110
|
|
|
111
111
|
```md
|
|
112
112
|
Full text answer stays here.
|
|
113
113
|
|
|
114
|
-
<!-- telegram_voice lang
|
|
115
|
-
Text to synthesize as a Telegram voice message.
|
|
116
|
-
-->
|
|
114
|
+
<!-- telegram_voice: {"value":"Short spoken companion summary.","lang":"ru","rate":"+30%"} -->
|
|
117
115
|
|
|
118
|
-
<!-- telegram_voice
|
|
119
|
-
|
|
120
|
-
<!-- telegram_voice: Short spoken companion summary. -->
|
|
116
|
+
<!-- telegram_voice text="Short spoken companion summary." lang="ru" rate="+30%" -->
|
|
121
117
|
```
|
|
122
118
|
|
|
123
|
-
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice`
|
|
119
|
+
The bridge strips the comment from Telegram text. On `agent_end`, it maps each `telegram_voice` action to a provider call, generates one file per action, and sends each file as an independent Telegram-native voice message. Equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence when both appear; `lang` and `rate` are optional. The colon after `telegram_voice` is optional for both JSON and attributes and never changes format detection. Use JSON for long or escaped text and encode line breaks inside JSON strings as `\n`. The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
|
|
124
120
|
|
|
125
121
|
## Buttons Markup
|
|
126
122
|
|
|
127
|
-
Assistant replies can include independent button
|
|
123
|
+
Assistant replies can include independent button actions in the same two payload forms:
|
|
128
124
|
|
|
129
125
|
```md
|
|
130
126
|
I can continue.
|
|
131
127
|
|
|
132
|
-
<!-- telegram_button label
|
|
128
|
+
<!-- telegram_button: {"label":"Continue","prompt":"Continue with the current plan.","selected_style":"primary"} -->
|
|
133
129
|
|
|
134
|
-
<!-- telegram_button label="Show risks"
|
|
135
|
-
List the main risks first.
|
|
136
|
-
-->
|
|
130
|
+
<!-- telegram_button label="Show risks" prompt="List the main risks first." selected_style="danger" -->
|
|
137
131
|
|
|
138
|
-
<!-- telegram_button: Done -->
|
|
132
|
+
<!-- telegram_button: {"value":"Done"} -->
|
|
139
133
|
```
|
|
140
134
|
|
|
141
135
|
Rules:
|
|
142
136
|
|
|
143
|
-
-
|
|
144
|
-
-
|
|
145
|
-
- `
|
|
146
|
-
- The opening
|
|
147
|
-
-
|
|
148
|
-
- Use one block per button; this mirrors HTML's singular element model and avoids a nested button DSL inside comments.
|
|
137
|
+
- JSON objects and double-quoted HTML-like attributes are the only accepted payload forms; shorthand, body, paired-comment, unquoted-attribute, and single-quoted-attribute forms are rejected.
|
|
138
|
+
- The colon after `telegram_button` is optional for both forms and never changes format detection.
|
|
139
|
+
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. Explicit `label` or `prompt` takes precedence over its `value` fallback. Use JSON with `\n` escapes for multiline prompts.
|
|
140
|
+
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it remains literal Markdown.
|
|
141
|
+
- Use one comment per button; this mirrors HTML's singular element model and avoids a nested button DSL.
|
|
149
142
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
150
|
-
- After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style
|
|
143
|
+
- After Telegram accepts a generated button callback as a queued prompt, the bridge changes that exact button to its configured selection style without changing agent-authored text or emoji. Set `selected_style` to `primary` (blue), `success` (green), or `danger` (red); omitted or invalid values fall back to `primary`. The style never suppresses queue admission. Other choices stay visually unchanged and remain available; the callback acknowledgement remains the fallback on clients that do not render button styles.
|
|
151
144
|
- When generated button markup is the entire assistant reply, the bridge supplies the standard `βοΈ **Choose an option:**` heading as visible message text so Telegram has a message to which it can attach the inline keyboard.
|
|
152
145
|
|
|
153
|
-
Do not emit
|
|
146
|
+
Do not emit inline comments after visible text, standalone button actions, or tool calls for ordinary Telegram-turn buttons. The agent writes Markdown plus hidden comments; the bridge strips comments and attaches Telegram `reply_markup` after `agent_end`. For local/TUI-originated direct sends, put the same Markdown and `telegram_button` comments in `telegram_message(text)`.
|
|
154
147
|
|
|
155
148
|
Buttons are built in and do not need a command template because they are pure Telegram reply markup plus callback routing.
|
|
156
149
|
|
|
@@ -162,8 +155,8 @@ The extension injects prompt guidance by context:
|
|
|
162
155
|
- For ordinary local/TUI prompts, the agent only sees compact direct-delivery guidance: use `telegram_attach` or `telegram_message` when the user asks to send something to Telegram, and otherwise answer locally as normal.
|
|
163
156
|
- For Telegram-originated turns, the prompt carries only minimal mobile/reply/file guidance; agents can call `telegram_help()` for full voice/button/direct-delivery/Threaded Mode/formatting/debug details.
|
|
164
157
|
- For Telegram-originated turns, write the full technical answer as normal Markdown.
|
|
165
|
-
- Add `telegram_voice` when a Telegram-native voice message is useful
|
|
166
|
-
- Add `telegram_button
|
|
158
|
+
- Add `telegram_voice` with either a JSON object or double-quoted attributes when a Telegram-native voice message is useful. A companion summary is optional, no specific summary format is required.
|
|
159
|
+
- Add `telegram_button` with either a JSON object or double-quoted attributes. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` is optional. A button-only reply may omit parent text because the bridge supplies `βοΈ **Choose an option:**` automatically.
|
|
167
160
|
- For ordinary Telegram-turn replies, do not call transport tools for voice or buttons; the bridge owns delivery, while registered voice synthesis providers own TTS and OGG/Opus conversion. For explicit local/TUI direct sends, `telegram_message` may include top-level `telegram_button` comments in its Markdown text because those buttons are attached to that text message.
|
|
168
161
|
- Prefer meaningful visible parent text when it adds context; for a button-only answer, rely on the bridge's automatic `βοΈ **Choose an option:**` fallback rather than manufacturing duplicate text.
|
|
169
162
|
|
package/docs/public-api.md
CHANGED
|
@@ -64,8 +64,8 @@ This command surface is a mobile companion subset, not a raw terminal-command br
|
|
|
64
64
|
- `telegram_attach(paths, chat_id?, thread_id?, caption?)` is the stable artifact delivery tool for generated files. During Telegram turns it queues files for the active reply; with `assistant.rendering: "rich"`, exactly one PNG/JPEG, MP4, or MP3 artifact plus non-empty final Markdown can become one reply-anchored Rich Message. HTML mode, multiple/unsupported files, Guest Mode, and voice outputs retain their established paths. Outside Telegram turns the tool sends files directly to the paired/default chat, the registered follower's assigned thread, or an explicit `chat_id` plus optional `thread_id` when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus.
|
|
65
65
|
- `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message from local/TUI-initiated work when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. Top-level `telegram_button` comments inside `text` are parsed with the same planner used for normal replies and attached to that message; buttons are never standalone Telegram messages.
|
|
66
66
|
- `telegram_help()` returns detailed agent-facing guidance for pi-telegram delivery actions, Threaded Mode, formatting, and debugging. The regular prompt only points agents at this tool instead of repeating the full guidance on every turn. `telegram_attach`, `telegram_message`, and `telegram_help` remain registered but are model-active only while this instance owns direct transport or holds a live follower registration; disconnect/loss suppresses their schemas and prompt metadata, and recovery restores only the operator's previously active pi-telegram subset.
|
|
67
|
-
- `telegram_voice` hidden comments request Telegram-native voice delivery.
|
|
68
|
-
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit
|
|
67
|
+
- `telegram_voice` hidden comments request Telegram-native voice delivery through either a JSON object or double-quoted attributes. Equivalent `text` or `value` supplies the spoken payload; explicit `text` takes precedence.
|
|
68
|
+
- `telegram_button` hidden comments create inline buttons whose taps enqueue prompts through the same two payload forms. Use `label` plus `prompt`, or `value` when both strings are identical. The colon after either action name is optional and does not select the format. Use top-level column-zero comments outside code, quotes, lists, and indented examples; do not emit standalone button actions.
|
|
69
69
|
|
|
70
70
|
Prompt guidance is context-aware: local/TUI prompts see only explicit direct-delivery guidance, while Telegram-originated turns receive the full action-comment syntax and phone-width output contract.
|
|
71
71
|
|
|
@@ -336,7 +336,7 @@ Contract:
|
|
|
336
336
|
- Providers are synchronous because `/start` status text is rendered inline with the menu.
|
|
337
337
|
- Return `undefined` when the line is not relevant for the active model.
|
|
338
338
|
- Provider failures are isolated and skipped so optional companion status cannot break the core Telegram menu.
|
|
339
|
-
- The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status,
|
|
339
|
+
- The bridge renders rows as `<Label>: <value>` in the same HTML status block as Status, Usage, Cost, and Context, capitalizing the first label character for Telegram UI consistency.
|
|
340
340
|
|
|
341
341
|
## Updates
|
|
342
342
|
|
package/index.ts
CHANGED
|
@@ -67,11 +67,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
67
67
|
const getActiveTelegramThreadProfile = function (): string | undefined {
|
|
68
68
|
return configStore.getActiveProfileName();
|
|
69
69
|
};
|
|
70
|
-
const busProcessRuntime = Bus.
|
|
70
|
+
const busProcessRuntime = Bus.createCurrentTelegramBusProcessRuntime({
|
|
71
71
|
getActiveProfileName: getActiveTelegramThreadProfile,
|
|
72
|
-
pid: process.pid,
|
|
73
|
-
parentPid: process.ppid,
|
|
74
|
-
createdAtMs: Date.now(),
|
|
75
72
|
});
|
|
76
73
|
const {
|
|
77
74
|
instanceId: telegramInstanceId,
|
|
@@ -119,14 +116,8 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
119
116
|
const runtimeEvents = runtimeDiagnostics.events;
|
|
120
117
|
const recordRuntimeEvent = runtimeDiagnostics.recordRuntimeEvent;
|
|
121
118
|
const configStore = Config.createTelegramConfigStore({ recordRuntimeEvent });
|
|
122
|
-
const isTelegramBusConfigured = function (): boolean {
|
|
123
|
-
return true;
|
|
124
|
-
};
|
|
125
119
|
const isTelegramBusRuntimeEnabled = function (): boolean {
|
|
126
|
-
return (
|
|
127
|
-
isTelegramBusConfigured() &&
|
|
128
|
-
!telegramThreadCapabilityState.isTopicModeUnavailable()
|
|
129
|
-
);
|
|
120
|
+
return !telegramThreadCapabilityState.isTopicModeUnavailable();
|
|
130
121
|
};
|
|
131
122
|
Config.bindGlobalTelegramConfigRuntime(configStore);
|
|
132
123
|
const configControls = Config.createTelegramConfigControls(configStore);
|
|
@@ -397,6 +388,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
397
388
|
sendRichMessageDraft,
|
|
398
389
|
downloadFile: downloadTelegramBridgeFile,
|
|
399
390
|
editMessageText: editTelegramMessageText,
|
|
391
|
+
editMessageReplyMarkup: editTelegramMessageReplyMarkup,
|
|
400
392
|
answerCallbackQuery,
|
|
401
393
|
answerGuestQuery,
|
|
402
394
|
deleteMessage: deleteTelegramMessage,
|
|
@@ -741,13 +733,7 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
741
733
|
stopTypingLoop: typing.stop,
|
|
742
734
|
answerCallbackQuery,
|
|
743
735
|
editInteractiveMessage,
|
|
744
|
-
|
|
745
|
-
await callTelegramApi("editMessageReplyMarkup", {
|
|
746
|
-
chat_id: chatId,
|
|
747
|
-
message_id: messageId,
|
|
748
|
-
reply_markup: replyMarkup,
|
|
749
|
-
});
|
|
750
|
-
},
|
|
736
|
+
editMessageReplyMarkup: editTelegramMessageReplyMarkup,
|
|
751
737
|
sendInteractiveMessage,
|
|
752
738
|
deleteMessage: deleteTelegramMessage,
|
|
753
739
|
answerGuestQuery,
|
|
@@ -932,7 +918,6 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
932
918
|
getAllowedUserId: configStore.getAllowedUserId,
|
|
933
919
|
callApi: callTelegramApi,
|
|
934
920
|
topicTargetStore: threadStore,
|
|
935
|
-
isBusConfigured: isTelegramBusConfigured,
|
|
936
921
|
isBusRuntimeEnabled: isTelegramBusRuntimeEnabled,
|
|
937
922
|
ownsLock: lockRuntime.owns,
|
|
938
923
|
startClassicPolling: pollingRuntime.start,
|
package/lib/bus-api.ts
CHANGED
|
@@ -265,6 +265,24 @@ export function createTelegramBusAwareApiRuntime(
|
|
|
265
265
|
await deps.callFollowerApi("call", ["editMessageText", body]);
|
|
266
266
|
return "edited";
|
|
267
267
|
},
|
|
268
|
+
async editMessageReplyMarkup(
|
|
269
|
+
chatId: number,
|
|
270
|
+
messageId: number,
|
|
271
|
+
replyMarkup: unknown,
|
|
272
|
+
): Promise<void> {
|
|
273
|
+
if (deps.ownsDirect()) {
|
|
274
|
+
await deps.directRuntime.editMessageReplyMarkup(
|
|
275
|
+
chatId,
|
|
276
|
+
messageId,
|
|
277
|
+
replyMarkup,
|
|
278
|
+
);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
await deps.callFollowerApi("call", [
|
|
282
|
+
"editMessageReplyMarkup",
|
|
283
|
+
{ chat_id: chatId, message_id: messageId, reply_markup: replyMarkup },
|
|
284
|
+
]);
|
|
285
|
+
},
|
|
268
286
|
async answerCallbackQuery(
|
|
269
287
|
callbackQueryId: string,
|
|
270
288
|
text?: string,
|
package/lib/bus.ts
CHANGED
|
@@ -72,6 +72,20 @@ export function getTelegramProcessBirthIdentity(
|
|
|
72
72
|
return `${pid}:generation:${fallbackGeneration}`;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
export function createCurrentTelegramBusProcessRuntime(input: {
|
|
76
|
+
getActiveProfileName: () => string | undefined;
|
|
77
|
+
pid?: number;
|
|
78
|
+
parentPid?: number;
|
|
79
|
+
createdAtMs?: number;
|
|
80
|
+
}): TelegramBusProcessRuntime {
|
|
81
|
+
return createTelegramBusProcessRuntime({
|
|
82
|
+
getActiveProfileName: input.getActiveProfileName,
|
|
83
|
+
pid: input.pid ?? process.pid,
|
|
84
|
+
parentPid: input.parentPid ?? process.ppid,
|
|
85
|
+
createdAtMs: input.createdAtMs ?? Date.now(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
75
89
|
export function createTelegramBusProcessRuntime(input: {
|
|
76
90
|
getActiveProfileName: () => string | undefined;
|
|
77
91
|
pid: number;
|
package/lib/outbound-buttons.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type {
|
|
|
11
11
|
TelegramInlineKeyboardMarkup,
|
|
12
12
|
} from "./keyboard.ts";
|
|
13
13
|
import {
|
|
14
|
-
|
|
14
|
+
parseTelegramActionPayload,
|
|
15
15
|
parseTopLevelTelegramComment,
|
|
16
16
|
replaceTopLevelHtmlComments,
|
|
17
17
|
} from "./outbound-markup.ts";
|
|
@@ -87,16 +87,27 @@ function normalizeMarkdownAfterButtonExtraction(markdown: string): string {
|
|
|
87
87
|
return markdown.replace(/\n{3,}/g, "\n\n").trim();
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
function
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
const
|
|
90
|
+
function getTelegramButtonString(
|
|
91
|
+
payload: Record<string, unknown>,
|
|
92
|
+
key: string,
|
|
93
|
+
): string | undefined {
|
|
94
|
+
const value = payload[key];
|
|
95
|
+
if (typeof value !== "string") return undefined;
|
|
96
|
+
const trimmed = value.trim();
|
|
97
|
+
return trimmed || undefined;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function parseTelegramButtonAction(
|
|
101
|
+
payload: Record<string, unknown>,
|
|
102
|
+
): TelegramOutboundButtonAction | undefined {
|
|
103
|
+
const value = getTelegramButtonString(payload, "value");
|
|
104
|
+
const label = getTelegramButtonString(payload, "label") ?? value;
|
|
105
|
+
const prompt = getTelegramButtonString(payload, "prompt") ?? value;
|
|
106
|
+
if (!label || !prompt) return undefined;
|
|
107
|
+
const selectedStyle = payload.selected_style;
|
|
97
108
|
return {
|
|
98
|
-
|
|
99
|
-
|
|
109
|
+
text: label,
|
|
110
|
+
prompt,
|
|
100
111
|
...(selectedStyle === "success" ||
|
|
101
112
|
selectedStyle === "danger" ||
|
|
102
113
|
selectedStyle === "primary"
|
|
@@ -105,49 +116,6 @@ function parseButtonsCommentAttributes(input: string): {
|
|
|
105
116
|
};
|
|
106
117
|
}
|
|
107
118
|
|
|
108
|
-
function parseButtonsCommentRows(
|
|
109
|
-
head: string,
|
|
110
|
-
body: string | undefined,
|
|
111
|
-
): TelegramOutboundButtonAction[][] {
|
|
112
|
-
const trimmedHead = head.trim();
|
|
113
|
-
|
|
114
|
-
if (body === undefined) {
|
|
115
|
-
if (trimmedHead.startsWith(":")) {
|
|
116
|
-
const label = trimmedHead.slice(1).trim();
|
|
117
|
-
return label ? [[{ text: label, prompt: label }]] : [];
|
|
118
|
-
}
|
|
119
|
-
const attributes = parseButtonsCommentAttributes(head);
|
|
120
|
-
return attributes.label && attributes.prompt
|
|
121
|
-
? [
|
|
122
|
-
[
|
|
123
|
-
{
|
|
124
|
-
text: attributes.label,
|
|
125
|
-
prompt: attributes.prompt,
|
|
126
|
-
...(attributes.selectedStyle
|
|
127
|
-
? { selectedStyle: attributes.selectedStyle }
|
|
128
|
-
: {}),
|
|
129
|
-
},
|
|
130
|
-
],
|
|
131
|
-
]
|
|
132
|
-
: [];
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const attributes = parseButtonsCommentAttributes(head);
|
|
136
|
-
const prompt = body.trim();
|
|
137
|
-
if (!attributes.label || !prompt) return [];
|
|
138
|
-
return [
|
|
139
|
-
[
|
|
140
|
-
{
|
|
141
|
-
text: attributes.label,
|
|
142
|
-
prompt,
|
|
143
|
-
...(attributes.selectedStyle
|
|
144
|
-
? { selectedStyle: attributes.selectedStyle }
|
|
145
|
-
: {}),
|
|
146
|
-
},
|
|
147
|
-
],
|
|
148
|
-
];
|
|
149
|
-
}
|
|
150
|
-
|
|
151
119
|
export function createTelegramButtonActionStore(
|
|
152
120
|
options: { ttlMs?: number } = {},
|
|
153
121
|
): TelegramButtonActionStore {
|
|
@@ -197,14 +165,15 @@ export function planTelegramButtonReply(
|
|
|
197
165
|
const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
|
|
198
166
|
const command = parseTopLevelTelegramComment(comment, "telegram_button");
|
|
199
167
|
if (!command) return comment.raw;
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
168
|
+
const payload = parseTelegramActionPayload(comment, "telegram_button");
|
|
169
|
+
const action = payload ? parseTelegramButtonAction(payload) : undefined;
|
|
170
|
+
if (action) {
|
|
171
|
+
keyboard.push([
|
|
172
|
+
{
|
|
173
|
+
text: action.text,
|
|
174
|
+
callback_data: deps.registerAction(action),
|
|
175
|
+
},
|
|
176
|
+
]);
|
|
208
177
|
}
|
|
209
178
|
return "";
|
|
210
179
|
});
|
package/lib/outbound-markup.ts
CHANGED
|
@@ -16,16 +16,6 @@ interface TelegramTopLevelFenceState {
|
|
|
16
16
|
length: number;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
-
function isTelegramActionCommentContent(content: string): boolean {
|
|
20
|
-
const normalizedContent = content.replace(/^\s+/, "");
|
|
21
|
-
const [head = ""] = normalizedContent.split(/\r?\n/, 1);
|
|
22
|
-
return ["telegram_voice", "telegram_button"].some((command) => {
|
|
23
|
-
if (!head.startsWith(command)) return false;
|
|
24
|
-
const nextChar = head[command.length];
|
|
25
|
-
return nextChar === undefined || /\s|:/.test(nextChar);
|
|
26
|
-
});
|
|
27
|
-
}
|
|
28
|
-
|
|
29
19
|
function getMarkdownLineEnd(markdown: string, offset: number): number {
|
|
30
20
|
const newlineIndex = markdown.indexOf("\n", offset);
|
|
31
21
|
return newlineIndex === -1 ? markdown.length : newlineIndex + 1;
|
|
@@ -64,58 +54,6 @@ function isTopLevelClosingFence(
|
|
|
64
54
|
);
|
|
65
55
|
}
|
|
66
56
|
|
|
67
|
-
function collectPairedTelegramVoiceActionBody(
|
|
68
|
-
markdown: string,
|
|
69
|
-
bodyStart: number,
|
|
70
|
-
commentContent: string,
|
|
71
|
-
): { content: string; end: number } | undefined {
|
|
72
|
-
const normalizedContent = commentContent.trim();
|
|
73
|
-
if (
|
|
74
|
-
!normalizedContent.startsWith("telegram_voice") ||
|
|
75
|
-
!isTelegramActionCommentContent(commentContent)
|
|
76
|
-
) {
|
|
77
|
-
return undefined;
|
|
78
|
-
}
|
|
79
|
-
let offset = bodyStart;
|
|
80
|
-
while (offset < markdown.length) {
|
|
81
|
-
const lineEnd = getMarkdownLineEnd(markdown, offset);
|
|
82
|
-
const line = getMarkdownLineText(markdown, offset, lineEnd);
|
|
83
|
-
if (line === "<!-- /telegram_voice -->") {
|
|
84
|
-
const body = markdown.slice(bodyStart, offset).trim();
|
|
85
|
-
if (!body) return undefined;
|
|
86
|
-
return {
|
|
87
|
-
content: `${commentContent.trimEnd()}\n${body}`,
|
|
88
|
-
end: lineEnd,
|
|
89
|
-
};
|
|
90
|
-
}
|
|
91
|
-
if (line.startsWith("<!--")) return undefined;
|
|
92
|
-
offset = lineEnd;
|
|
93
|
-
}
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function collectInlineClosedTelegramActionBody(
|
|
98
|
-
markdown: string,
|
|
99
|
-
bodyStart: number,
|
|
100
|
-
commentContent: string,
|
|
101
|
-
): { content: string; end: number } | undefined {
|
|
102
|
-
const bodyLineEnd = getMarkdownLineEnd(markdown, bodyStart);
|
|
103
|
-
const bodyLine = getMarkdownLineText(markdown, bodyStart, bodyLineEnd);
|
|
104
|
-
const closeLineEnd = getMarkdownLineEnd(markdown, bodyLineEnd);
|
|
105
|
-
const closeLine = getMarkdownLineText(markdown, bodyLineEnd, closeLineEnd);
|
|
106
|
-
const hasRecoverableBody =
|
|
107
|
-
isTelegramActionCommentContent(commentContent) &&
|
|
108
|
-
bodyLine.trim() !== "" &&
|
|
109
|
-
!bodyLine.startsWith("<!--") &&
|
|
110
|
-
!bodyLine.startsWith("-->") &&
|
|
111
|
-
closeLine === "-->";
|
|
112
|
-
if (!hasRecoverableBody) return undefined;
|
|
113
|
-
return {
|
|
114
|
-
content: `${commentContent.trimEnd()}\n${bodyLine}`,
|
|
115
|
-
end: bodyLineEnd + 3,
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
|
|
119
57
|
export function collectTopLevelHtmlComments(markdown: string): {
|
|
120
58
|
comments: TelegramTopLevelHtmlComment[];
|
|
121
59
|
openCommentStart?: number;
|
|
@@ -140,27 +78,9 @@ export function collectTopLevelHtmlComments(markdown: string): {
|
|
|
140
78
|
if (line.startsWith("<!--")) {
|
|
141
79
|
const closeIndex = markdown.indexOf("-->", offset + 4);
|
|
142
80
|
if (closeIndex === -1) return { comments, openCommentStart: offset };
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const closeColumn = closeIndex - offset;
|
|
147
|
-
const closesOnOpeningLine = closeIndex < lineEnd;
|
|
148
|
-
const hasOnlyWhitespaceAfterClose =
|
|
149
|
-
line.slice(closeColumn + 3).trim() === "";
|
|
150
|
-
const pairedVoiceBody =
|
|
151
|
-
closesOnOpeningLine && hasOnlyWhitespaceAfterClose
|
|
152
|
-
? collectPairedTelegramVoiceActionBody(markdown, lineEnd, content)
|
|
153
|
-
: undefined;
|
|
154
|
-
const inlineBody =
|
|
155
|
-
!pairedVoiceBody && closesOnOpeningLine && hasOnlyWhitespaceAfterClose
|
|
156
|
-
? collectInlineClosedTelegramActionBody(markdown, lineEnd, content)
|
|
157
|
-
: undefined;
|
|
158
|
-
const recoveredBody = pairedVoiceBody ?? inlineBody;
|
|
159
|
-
if (recoveredBody) {
|
|
160
|
-
end = recoveredBody.end;
|
|
161
|
-
raw = markdown.slice(offset, end);
|
|
162
|
-
content = recoveredBody.content;
|
|
163
|
-
}
|
|
81
|
+
const end = closeIndex + 3;
|
|
82
|
+
const raw = markdown.slice(offset, end);
|
|
83
|
+
const content = raw.slice(4, -3);
|
|
164
84
|
comments.push({ raw, content, start: offset, end });
|
|
165
85
|
offset = getMarkdownLineEnd(markdown, end);
|
|
166
86
|
continue;
|
|
@@ -233,18 +153,47 @@ export function parseTopLevelTelegramComment(
|
|
|
233
153
|
};
|
|
234
154
|
}
|
|
235
155
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
): Record<string, string> {
|
|
156
|
+
function parseCanonicalTelegramActionAttributes(
|
|
157
|
+
source: string,
|
|
158
|
+
): Record<string, string> | undefined {
|
|
239
159
|
const attributes: Record<string, string> = {};
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
)
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
if (
|
|
160
|
+
const pattern = /\s*([A-Za-z_][A-Za-z0-9_-]*)="([^"]*)"/y;
|
|
161
|
+
let offset = 0;
|
|
162
|
+
while (offset < source.length) {
|
|
163
|
+
pattern.lastIndex = offset;
|
|
164
|
+
const match = pattern.exec(source);
|
|
165
|
+
if (!match) return undefined;
|
|
166
|
+
const value = match[2].trim();
|
|
167
|
+
if (value) attributes[match[1]] = value;
|
|
168
|
+
offset = pattern.lastIndex;
|
|
169
|
+
}
|
|
170
|
+
return Object.keys(attributes).length > 0 ? attributes : undefined;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function parseTelegramActionPayload(
|
|
174
|
+
comment: TelegramTopLevelHtmlComment,
|
|
175
|
+
command: string,
|
|
176
|
+
): Record<string, unknown> | undefined {
|
|
177
|
+
const parsed = parseTopLevelTelegramComment(comment, command);
|
|
178
|
+
if (!parsed) return undefined;
|
|
179
|
+
const source = [parsed.head, parsed.body]
|
|
180
|
+
.filter((part): part is string => part !== undefined)
|
|
181
|
+
.join("\n")
|
|
182
|
+
.trim()
|
|
183
|
+
.replace(/^:\s*/, "");
|
|
184
|
+
if (!source) return undefined;
|
|
185
|
+
if (source.startsWith("{")) {
|
|
186
|
+
try {
|
|
187
|
+
const value: unknown = JSON.parse(source);
|
|
188
|
+
return value !== null && typeof value === "object" && !Array.isArray(value)
|
|
189
|
+
? (value as Record<string, unknown>)
|
|
190
|
+
: undefined;
|
|
191
|
+
} catch {
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
246
194
|
}
|
|
247
|
-
return
|
|
195
|
+
if (parsed.body !== undefined) return undefined;
|
|
196
|
+
return parseCanonicalTelegramActionAttributes(source);
|
|
248
197
|
}
|
|
249
198
|
|
|
250
199
|
export function normalizeMarkdownAfterVoiceExtraction(
|
|
@@ -295,58 +244,14 @@ export interface TelegramVoiceReplyPlan {
|
|
|
295
244
|
rate?: string;
|
|
296
245
|
}
|
|
297
246
|
|
|
298
|
-
function
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
...(attributes.rate ? { rate: attributes.rate } : {}),
|
|
307
|
-
...(attributes.text ? { text: attributes.text } : {}),
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
function parseVoiceCommentBody(
|
|
312
|
-
head: string,
|
|
313
|
-
body: string | undefined,
|
|
314
|
-
): {
|
|
315
|
-
attrs: string;
|
|
316
|
-
text: string;
|
|
317
|
-
} {
|
|
318
|
-
const trimmedHead = head.trim();
|
|
319
|
-
if (body !== undefined) {
|
|
320
|
-
return { attrs: trimmedHead.replace(/^:/, "").trim(), text: body.trim() };
|
|
321
|
-
}
|
|
322
|
-
let colonIndex = -1;
|
|
323
|
-
let inQuote = false;
|
|
324
|
-
let quoteChar = "";
|
|
325
|
-
for (let i = 0; i < trimmedHead.length; i++) {
|
|
326
|
-
const char = trimmedHead[i];
|
|
327
|
-
if (inQuote) {
|
|
328
|
-
if (char === quoteChar) inQuote = false;
|
|
329
|
-
} else {
|
|
330
|
-
if (char === '"' || char === "'") {
|
|
331
|
-
inQuote = true;
|
|
332
|
-
quoteChar = char;
|
|
333
|
-
} else if (char === ":") {
|
|
334
|
-
colonIndex = i;
|
|
335
|
-
break;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
if (colonIndex > 0) {
|
|
340
|
-
const attrsPart = trimmedHead.slice(0, colonIndex).trim();
|
|
341
|
-
const textPart = trimmedHead.slice(colonIndex + 1).trim();
|
|
342
|
-
const attrs = parseVoiceReplyAttributes(attrsPart);
|
|
343
|
-
return { attrs: attrsPart, text: textPart || attrs.text || "", ...attrs };
|
|
344
|
-
}
|
|
345
|
-
if (trimmedHead.startsWith(":")) {
|
|
346
|
-
return { attrs: "", text: trimmedHead.slice(1).trim() };
|
|
347
|
-
}
|
|
348
|
-
const attrs = parseVoiceReplyAttributes(trimmedHead);
|
|
349
|
-
return { attrs: trimmedHead, text: attrs.text ?? "" };
|
|
247
|
+
function getTelegramActionString(
|
|
248
|
+
payload: Record<string, unknown>,
|
|
249
|
+
key: string,
|
|
250
|
+
): string | undefined {
|
|
251
|
+
const value = payload[key];
|
|
252
|
+
if (typeof value !== "string") return undefined;
|
|
253
|
+
const trimmed = value.trim();
|
|
254
|
+
return trimmed || undefined;
|
|
350
255
|
}
|
|
351
256
|
|
|
352
257
|
export function planTelegramVoiceReply(
|
|
@@ -356,26 +261,24 @@ export function planTelegramVoiceReply(
|
|
|
356
261
|
let lang: string | undefined;
|
|
357
262
|
let rate: string | undefined;
|
|
358
263
|
const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
|
|
359
|
-
|
|
360
|
-
if (!command)
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
const attrs = parseVoiceReplyAttributes(parsed.attrs);
|
|
370
|
-
if (parsed.text) {
|
|
264
|
+
const command = parseTopLevelTelegramComment(comment, "telegram_voice");
|
|
265
|
+
if (!command) return comment.raw;
|
|
266
|
+
const payload = parseTelegramActionPayload(comment, "telegram_voice");
|
|
267
|
+
if (!payload) return "";
|
|
268
|
+
const text =
|
|
269
|
+
getTelegramActionString(payload, "text") ??
|
|
270
|
+
getTelegramActionString(payload, "value");
|
|
271
|
+
const itemLang = getTelegramActionString(payload, "lang");
|
|
272
|
+
const itemRate = getTelegramActionString(payload, "rate");
|
|
273
|
+
if (text) {
|
|
371
274
|
voiceReplies.push({
|
|
372
|
-
text
|
|
373
|
-
...(
|
|
374
|
-
...(
|
|
275
|
+
text,
|
|
276
|
+
...(itemLang ? { lang: itemLang } : {}),
|
|
277
|
+
...(itemRate ? { rate: itemRate } : {}),
|
|
375
278
|
});
|
|
376
279
|
}
|
|
377
|
-
if (
|
|
378
|
-
if (
|
|
280
|
+
if (itemLang) lang = itemLang;
|
|
281
|
+
if (itemRate) rate = itemRate;
|
|
379
282
|
return "";
|
|
380
283
|
});
|
|
381
284
|
const voiceText = voiceReplies
|
package/lib/polling.ts
CHANGED
|
@@ -319,7 +319,6 @@ export interface TelegramThreadCapabilityRuntimeDeps<
|
|
|
319
319
|
TContext,
|
|
320
320
|
> extends TelegramThreadCapabilityReaderDeps {
|
|
321
321
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
322
|
-
isBusConfigured: () => boolean;
|
|
323
322
|
ownsLock: (ctx: TContext) => boolean;
|
|
324
323
|
getPollingStartedWithTelegramBus: () => boolean;
|
|
325
324
|
setPollingStartedWithTelegramBus: (started: boolean) => void;
|
|
@@ -394,7 +393,6 @@ export interface TelegramThreadAwarePollingDeps<
|
|
|
394
393
|
TContext,
|
|
395
394
|
TOwner,
|
|
396
395
|
> extends TelegramStartupThreadCapabilityProbeDeps {
|
|
397
|
-
isBusConfigured: () => boolean;
|
|
398
396
|
isBusRuntimeEnabled: () => boolean;
|
|
399
397
|
isTopicModeUnavailableError: (error: unknown) => boolean;
|
|
400
398
|
getPollingStartedWithTelegramBus: () => boolean;
|
|
@@ -419,7 +417,6 @@ export interface TelegramThreadCapabilityOrchestrationDeps<
|
|
|
419
417
|
> extends TelegramThreadCapabilityReaderDeps {
|
|
420
418
|
state: TelegramThreadCapabilityStateRuntime;
|
|
421
419
|
topicTargetStore: TelegramThreadCapabilityStore;
|
|
422
|
-
isBusConfigured: () => boolean;
|
|
423
420
|
isBusRuntimeEnabled: () => boolean;
|
|
424
421
|
ownsLock: (ctx: TContext) => boolean;
|
|
425
422
|
startClassicPolling: (ctx: TContext) => MaybePromise<void>;
|
|
@@ -475,7 +472,6 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
|
475
472
|
getAllowedUserId: deps.getAllowedUserId,
|
|
476
473
|
callApi: deps.callApi,
|
|
477
474
|
topicTargetStore: deps.topicTargetStore,
|
|
478
|
-
isBusConfigured: deps.isBusConfigured,
|
|
479
475
|
ownsLock: deps.ownsLock,
|
|
480
476
|
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
481
477
|
setPollingStartedWithTelegramBus: deps.state.setBusPollingStarted,
|
|
@@ -498,7 +494,6 @@ export function createTelegramThreadCapabilityOrchestration<TContext, TOwner>(
|
|
|
498
494
|
getAllowedUserId: deps.getAllowedUserId,
|
|
499
495
|
callApi: deps.callApi,
|
|
500
496
|
topicTargetStore: deps.topicTargetStore,
|
|
501
|
-
isBusConfigured: deps.isBusConfigured,
|
|
502
497
|
isBusRuntimeEnabled: deps.isBusRuntimeEnabled,
|
|
503
498
|
isTopicModeUnavailableError: deps.isTopicModeUnavailableError,
|
|
504
499
|
getPollingStartedWithTelegramBus: deps.state.isBusPollingStarted,
|
|
@@ -590,7 +585,6 @@ export async function applyTelegramThreadCapability<TContext>(
|
|
|
590
585
|
deps: TelegramThreadCapabilityRuntimeDeps<TContext>,
|
|
591
586
|
): Promise<void> {
|
|
592
587
|
await deps.topicTargetStore.load();
|
|
593
|
-
if (!deps.isBusConfigured()) return;
|
|
594
588
|
const nowMs = (deps.getNowMs ?? Date.now)();
|
|
595
589
|
const previousBotState = deps.topicTargetStore.getBotState();
|
|
596
590
|
if (!threadModeEnabled) {
|
|
@@ -691,17 +685,14 @@ export function createTelegramThreadAwarePollingPorts<TContext, TOwner>(
|
|
|
691
685
|
ctx: TContext,
|
|
692
686
|
options?: { forceFreshLeaderThread?: boolean },
|
|
693
687
|
): Promise<void> => {
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
} catch (error) {
|
|
701
|
-
deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
|
|
702
|
-
}
|
|
703
|
-
deps.setTopicModeUnavailable(startupThreadCapability !== true);
|
|
688
|
+
await deps.topicTargetStore.load();
|
|
689
|
+
let startupThreadCapability: boolean | undefined;
|
|
690
|
+
try {
|
|
691
|
+
startupThreadCapability = await probeTelegramStartupThreadCapability(deps);
|
|
692
|
+
} catch (error) {
|
|
693
|
+
deps.recordEvent("bus", error, { phase: "startup-thread-mode-probe" });
|
|
704
694
|
}
|
|
695
|
+
deps.setTopicModeUnavailable(startupThreadCapability !== true);
|
|
705
696
|
if (deps.isBusRuntimeEnabled()) {
|
|
706
697
|
deps.setTopicModeUnavailable(false);
|
|
707
698
|
try {
|
|
@@ -886,7 +877,6 @@ export function createTelegramThreadCapabilityMonitor<TContext>(
|
|
|
886
877
|
return {
|
|
887
878
|
start(ctx) {
|
|
888
879
|
stop();
|
|
889
|
-
if (!deps.isBusConfigured()) return;
|
|
890
880
|
interval = setInterval(() => {
|
|
891
881
|
check(ctx);
|
|
892
882
|
}, intervalMs);
|
package/lib/prompts.ts
CHANGED
|
@@ -138,13 +138,13 @@ How to answer Telegram turns:
|
|
|
138
138
|
Assistant-authored Telegram actions:
|
|
139
139
|
- \`telegram_voice\` and \`telegram_button\` are hidden top-level HTML comments, not Pi tools.
|
|
140
140
|
- Put action comments at column zero, outside code, quotes, lists, and indented examples.
|
|
141
|
-
-
|
|
142
|
-
- Keep the complete action
|
|
141
|
+
- Action payloads use either JSON or double-quoted attributes; the colon after the action name is optional and does not affect parsing. Voice accepts equivalent \`text\` or \`value\`: \`<!-- telegram_voice: {"value":"Short summary","lang":"en"} -->\` or \`<!-- telegram_voice text="Short summary" lang="en" -->\`.
|
|
142
|
+
- Keep the complete action in one top-level comment and include non-empty \`text\` or \`value\`; encode line breaks inside JSON strings as \`\\n\`.
|
|
143
143
|
- Keep voice text TTS-friendly; avoid raw Markdown, code, and tables in voice text.
|
|
144
144
|
- Voice delivery generates and attaches OGG automatically; do not also call \`telegram_attach\` for the same audio.
|
|
145
145
|
- 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.
|
|
146
|
-
- Button
|
|
147
|
-
- Optional \`selected_style\` controls the button after queue admission: \`primary\` (default, blue), \`success\` (green), or \`danger\` (red). It never suppresses the prompt.
|
|
146
|
+
- Button payloads use \`label\` plus \`prompt\`, or compact \`value\` when both are identical: \`<!-- telegram_button: {"label":"Continue","prompt":"Continue with the current plan."} -->\` or \`<!-- telegram_button value="Continue" -->\`.
|
|
147
|
+
- Optional \`selected_style\` in either payload form controls the button after queue admission: \`primary\` (default, blue), \`success\` (green), or \`danger\` (red). It never suppresses the prompt.
|
|
148
148
|
- If hidden button comments form the whole reply, the bridge supplies the standard \`βοΈ **Choose an option:**\` heading automatically.
|
|
149
149
|
|
|
150
150
|
Local/TUI direct delivery:
|
package/lib/status.ts
CHANGED
|
@@ -1272,13 +1272,11 @@ function buildStatusSummary(ctx: TelegramStatusContext): string {
|
|
|
1272
1272
|
return "unknown";
|
|
1273
1273
|
}
|
|
1274
1274
|
|
|
1275
|
-
function
|
|
1275
|
+
function buildTelegramStatusRoleSuffix(
|
|
1276
1276
|
state: TelegramBridgeStatusLineState | undefined,
|
|
1277
|
-
): string
|
|
1278
|
-
if (state?.botThreadMode !== "enabled" || !state.busRole) return
|
|
1279
|
-
|
|
1280
|
-
const identity = threadName || (state.instanceSlot ? `[${state.instanceSlot}]` : "");
|
|
1281
|
-
return identity ? `${identity} @${state.busRole}` : undefined;
|
|
1277
|
+
): string {
|
|
1278
|
+
if (state?.botThreadMode !== "enabled" || !state.busRole) return "";
|
|
1279
|
+
return ` @${state.busRole}`;
|
|
1282
1280
|
}
|
|
1283
1281
|
|
|
1284
1282
|
export function buildStatusHtml(
|
|
@@ -1290,11 +1288,12 @@ export function buildStatusHtml(
|
|
|
1290
1288
|
const usesSubscription = activeModel
|
|
1291
1289
|
? ctx.modelRegistry.isUsingOAuth(activeModel)
|
|
1292
1290
|
: false;
|
|
1293
|
-
const lines: string[] = [
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1291
|
+
const lines: string[] = [
|
|
1292
|
+
buildStatusRow(
|
|
1293
|
+
"Status",
|
|
1294
|
+
`${buildStatusSummary(ctx)}${buildTelegramStatusRoleSuffix(bridgeStatus)}`,
|
|
1295
|
+
),
|
|
1296
|
+
];
|
|
1298
1297
|
const usageSummary = buildUsageSummary(stats);
|
|
1299
1298
|
const costSummary = buildCostSummary(stats, usesSubscription);
|
|
1300
1299
|
if (usageSummary) {
|
package/lib/telegram-api.ts
CHANGED
|
@@ -497,6 +497,11 @@ export interface TelegramBridgeApiRuntime {
|
|
|
497
497
|
editMessageText: (
|
|
498
498
|
body: TelegramEditMessageTextBody,
|
|
499
499
|
) => Promise<"edited" | "unchanged">;
|
|
500
|
+
editMessageReplyMarkup: (
|
|
501
|
+
chatId: number,
|
|
502
|
+
messageId: number,
|
|
503
|
+
replyMarkup: unknown,
|
|
504
|
+
) => Promise<void>;
|
|
500
505
|
answerCallbackQuery: (
|
|
501
506
|
callbackQueryId: string,
|
|
502
507
|
text?: string,
|
|
@@ -1569,6 +1574,13 @@ export function createTelegramBridgeApiRuntime(
|
|
|
1569
1574
|
throw error;
|
|
1570
1575
|
}
|
|
1571
1576
|
},
|
|
1577
|
+
editMessageReplyMarkup: async (chatId, messageId, replyMarkup) => {
|
|
1578
|
+
await callRecorded("editMessageReplyMarkup", {
|
|
1579
|
+
chat_id: chatId,
|
|
1580
|
+
message_id: messageId,
|
|
1581
|
+
reply_markup: replyMarkup,
|
|
1582
|
+
});
|
|
1583
|
+
},
|
|
1572
1584
|
answerCallbackQuery: async (callbackQueryId, text) => {
|
|
1573
1585
|
try {
|
|
1574
1586
|
await deps.client.answerCallbackQuery(callbackQueryId, text);
|