@llblab/pi-kit 0.3.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -3
- package/BACKLOG.md +1 -1
- package/CHANGELOG.md +10 -0
- package/README.md +19 -10
- package/node_modules/@llblab/pi-actors/AGENTS.md +1 -0
- package/node_modules/@llblab/pi-actors/CHANGELOG.md +6 -0
- package/node_modules/@llblab/pi-actors/dist/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/dist/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-actors/docs/recipe-library.md +1 -1
- package/node_modules/@llblab/pi-actors/package.json +1 -1
- package/node_modules/@llblab/pi-actors/skills/actors/SKILL.md +16 -6
- package/node_modules/@llblab/pi-actors/skills/music-player/SKILL.md +3 -3
- package/node_modules/@llblab/pi-actors/skills/music-player/genapps/music-player.mjs +6 -4
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback.mjs +85 -18
- package/node_modules/@llblab/pi-actors/skills/swarm/SKILL.md +2 -6
- package/node_modules/@llblab/pi-actors/skills/swarm/references/development-swarm.md +2 -31
- package/node_modules/@llblab/pi-clean-room/AGENTS.md +5 -0
- package/node_modules/@llblab/pi-clean-room/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-clean-room/CHANGELOG.md +15 -0
- package/node_modules/@llblab/pi-clean-room/README.md +61 -0
- package/node_modules/@llblab/pi-clean-room/banner.jpg +0 -0
- package/node_modules/@llblab/pi-clean-room/index.ts +178 -0
- package/node_modules/@llblab/pi-clean-room/package.json +53 -0
- package/node_modules/@llblab/pi-grow-loop/AGENTS.md +1 -1
- package/node_modules/@llblab/pi-grow-loop/CHANGELOG.md +4 -0
- package/node_modules/@llblab/pi-grow-loop/package.json +1 -1
- package/node_modules/@llblab/pi-grow-loop/skills/grow-loop/SKILL.md +3 -17
- package/node_modules/@llblab/pi-state-flow/AGENTS.md +28 -0
- package/node_modules/@llblab/pi-state-flow/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-state-flow/CHANGELOG.md +79 -0
- package/node_modules/@llblab/pi-state-flow/README.md +197 -0
- package/node_modules/@llblab/pi-state-flow/index.ts +4 -0
- package/node_modules/@llblab/pi-state-flow/lib/context.ts +88 -0
- package/node_modules/@llblab/pi-state-flow/lib/episode.ts +29 -0
- package/node_modules/@llblab/pi-state-flow/lib/extension.ts +238 -0
- package/node_modules/@llblab/pi-state-flow/lib/json.ts +78 -0
- package/node_modules/@llblab/pi-state-flow/lib/recovery.ts +38 -0
- package/node_modules/@llblab/pi-state-flow/lib/session.ts +49 -0
- package/node_modules/@llblab/pi-state-flow/lib/skills.ts +58 -0
- package/node_modules/@llblab/pi-state-flow/lib/snapshot.ts +103 -0
- package/node_modules/@llblab/pi-state-flow/lib/state.ts +20 -0
- package/node_modules/@llblab/pi-state-flow/lib/status.ts +16 -0
- package/node_modules/@llblab/pi-state-flow/lib/terminal.ts +148 -0
- package/node_modules/@llblab/pi-state-flow/lib/transition.ts +47 -0
- package/node_modules/@llblab/pi-state-flow/lib/validation.ts +27 -0
- package/node_modules/@llblab/pi-state-flow/package.json +55 -0
- package/node_modules/@llblab/pi-telegram/BACKLOG.md +3 -0
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +18 -0
- package/node_modules/@llblab/pi-telegram/README.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +5 -3
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +39 -11
- package/node_modules/@llblab/pi-telegram/docs/generative-apps.md +2 -2
- package/node_modules/@llblab/pi-telegram/docs/multi-instance-bus.md +17 -5
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +6 -4
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +1 -1
- package/node_modules/@llblab/pi-telegram/index.ts +13 -1
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +12 -3
- package/node_modules/@llblab/pi-telegram/lib/bus-follower.ts +29 -18
- package/node_modules/@llblab/pi-telegram/lib/bus-leader.ts +15 -6
- package/node_modules/@llblab/pi-telegram/lib/bus.ts +11 -4
- package/node_modules/@llblab/pi-telegram/lib/keyboard.ts +5 -3
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +72 -15
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +81 -9
- package/node_modules/@llblab/pi-telegram/lib/outbound.ts +2 -0
- package/node_modules/@llblab/pi-telegram/lib/replies.ts +1 -1
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +89 -32
- package/node_modules/@llblab/pi-telegram/lib/sync.ts +74 -15
- package/node_modules/@llblab/pi-telegram/lib/telegram-api.ts +32 -1
- package/node_modules/@llblab/pi-telegram/lib/thread-reconciler.ts +17 -0
- package/node_modules/@llblab/pi-telegram/lib/threads.ts +123 -15
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +4 -2
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +4 -2
- package/node_modules/@llblab/pi-telegram/skills/generative-apps/SKILL.md +4 -3
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +19 -8
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +4 -2
- package/node_modules/@llblab/skills/abcd-context/AGENTS.md +40 -0
- package/node_modules/@llblab/skills/abcd-context/BACKLOG.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/CHANGELOG.md +19 -0
- package/node_modules/@llblab/skills/abcd-context/README.md +30 -0
- package/node_modules/@llblab/skills/abcd-context/SKILL.md +122 -0
- package/node_modules/@llblab/skills/abcd-context/docs/README.md +9 -0
- package/node_modules/@llblab/skills/abcd-context/docs/protocols.md +199 -0
- package/node_modules/@llblab/skills/abcd-context/docs/templates.md +194 -0
- package/node_modules/@llblab/skills/abcd-context/docs/validation-design.md +117 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/AGENTS.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/BACKLOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/CHANGELOG.md +5 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/README.md +10 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/README.md +7 -0
- package/node_modules/@llblab/skills/abcd-context/fixtures/abcd-project/docs/overview.md +3 -0
- package/node_modules/@llblab/skills/abcd-context/recipes/validate-context.json +7 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/_self-test.mjs +244 -0
- package/node_modules/@llblab/skills/abcd-context/scripts/validate-context.mjs +497 -0
- package/node_modules/@llblab/skills/brain-storm/AGENTS.md +5 -0
- package/node_modules/@llblab/skills/brain-storm/SKILL.md +321 -0
- package/node_modules/@llblab/skills/coding-contract/SKILL.md +228 -0
- package/node_modules/@llblab/skills/domain-dag/AGENTS.md +6 -0
- package/node_modules/@llblab/skills/domain-dag/SKILL.md +257 -0
- package/node_modules/@llblab/skills/domain-dag/domain-dag.json +9 -0
- package/node_modules/@llblab/skills/domain-dag/recipes/validate-domain-dag.json +7 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.mjs +771 -0
- package/node_modules/@llblab/skills/domain-dag/scripts/validate-domain-dag.sh +4 -0
- package/node_modules/@llblab/skills/edge-tts/AGENTS.md +19 -0
- package/node_modules/@llblab/skills/edge-tts/SKILL.md +56 -0
- package/node_modules/@llblab/skills/edge-tts/recipes/say-edge.json +9 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.mjs +1548 -0
- package/node_modules/@llblab/skills/edge-tts/scripts/say.sh +4 -0
- package/node_modules/@llblab/skills/extra-self/SKILL.md +229 -0
- package/node_modules/@llblab/skills/frontend-design/SKILL.md +78 -0
- package/node_modules/@llblab/skills/groq-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/groq-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/groq-stt/recipes/transcribe-groq.json +10 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.mjs +190 -0
- package/node_modules/@llblab/skills/groq-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/mistral-stt/AGENTS.md +15 -0
- package/node_modules/@llblab/skills/mistral-stt/SKILL.md +35 -0
- package/node_modules/@llblab/skills/mistral-stt/recipes/transcribe-mistral.json +10 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.mjs +191 -0
- package/node_modules/@llblab/skills/mistral-stt/scripts/transcribe.sh +4 -0
- package/node_modules/@llblab/skills/package.json +46 -0
- package/node_modules/@llblab/skills/re-review/SKILL.md +318 -0
- package/node_modules/@llblab/skills/release-flow/SKILL.md +456 -0
- package/node_modules/@llblab/skills/show-me/SKILL.md +144 -0
- package/package.json +20 -8
- package/node_modules/@llblab/pi-actors/dist/skills/music-player/scripts/playback-client.mjs +0 -143
- package/node_modules/@llblab/pi-actors/skills/music-player/scripts/playback-client.mjs +0 -143
|
@@ -155,12 +155,14 @@ I can continue.
|
|
|
155
155
|
|
|
156
156
|
Rules:
|
|
157
157
|
|
|
158
|
-
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
158
|
+
- The payload may be a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row. Commas are optional between completed elements, and one trailing comma before a closing delimiter is tolerated; JSON object validation likewise tolerates trailing commas but does not invent missing values, property names, or internal separators. CML uses `{value}`, `{label|prompt}`, `{|prompt}`, or the corresponding three-atom form with `selected_style`; the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling, permitting an empty style atom in this form: `{|Next||1}`. An omitted fourth position stays enabled. JSON uses boolean `disabled`; legacy attributes accept `disabled="true"` or `disabled="false"`. Invalid disabled values reject the candidate matrix atomically. Disabled cells require no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank, equivalent to JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`. They retain their row and label, serialize as `{ text, disabled: {} }` (non-breaking-space text for a blank cell), and register no callback, prompt, or bound app action. It trims every atom, preserves non-structural printable text, and decodes only `\|`, `\}`, and `\\`.
|
|
159
159
|
- Use `label` plus `prompt`, or the compact `value` key when both strings are identical. If only `label`, only `prompt`, one-field `{value}`, or prompt-only `{|prompt}` is present, that string supplies both visible label and queued prompt. An explicit counterpart takes precedence over `value`. Use JSON with `\n` escapes for multiline prompts.
|
|
160
160
|
- The opening marker must start at column zero on a top-level line outside fenced code, quotes, lists, and indented examples; otherwise it does not activate a button action and is still removed from the Telegram surface.
|
|
161
|
-
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a
|
|
161
|
+
- Prefer one matrix comment for multiple buttons. Each top-level JSON object or CML cell becomes one full-width inline-keyboard row in source order; a nested row groups one or more buttons horizontally. The parser imposes no artificial per-row width cap; empty rows, malformed cells, unknown/trailing CML escapes, a fourth unescaped CML separator, empty enabled-cell prompts or three-atom styles, invalid disabled flags, empty one-atom cells, unknown selected styles, and deeper nesting are rejected atomically. Multi-atom cells may omit the label; four-atom cells may additionally omit the selected style, and disabled cells may omit the prompt. Generated surfaces default to five columns and expand to six through eight only for short position-bearing labels. Repeated singular comments remain valid.
|
|
162
162
|
- Button actions are stored in memory with short `callback_data`; Telegram never sees the full prompt in the button payload.
|
|
163
|
-
-
|
|
163
|
+
- Standalone column-zero fences opened by exactly three backticks and `telegram_button` use the same singleton/matrix JSON/CML grammar. Complete valid blocks become native `<tg-button-row>` elements in `InputRichMessage.markdown`, at their original positions. Each row supports at most eight buttons and must fit a Rich Message chunk; validation precedes callback registration for the entire block. Labels are escaped, prompts never enter the native markup, and disabled cells have no callback. Missing closures, trailing payload garbage, and invalid blocks produce no controls. Other language names and enclosing code examples stay literal. Draft previews withhold complete and unfinished action fences without registering actions. HTML compatibility projects fenced rows into the bottom keyboard before comment-authored footer rows; source ordering within each group is retained.
|
|
164
|
+
- In-body controls share prompt/app dispatch, TTL, generation/revision checks, and sent-message target ownership with footer controls. Their acknowledgement is the selection feedback; `selected_style` does not recolor the Rich body. No callback-triggered body rewrite is introduced, preventing a stale click from overwriting newer app output. The operator confirmed the post-reload current-client smoke: singleton CML and mixed JSON/CML in-body rows, their ordinary prompt callbacks, a labeled disabled control, and the separate footer callback work as intended. Client type was not recorded. The operator also confirmed that HTML mode projects the in-body groups into the bottom keyboard as intended. Second-client coverage, follower routing, blank cells, and app-method dispatch/revision rejection remain live-gated. Bot API evidence: `InputRichBlockButtons` / Rich Markdown `tg-button-row` and `RichMessageButton` use ordinary `callback_data` and support disabled controls.
|
|
165
|
+
- After Telegram accepts a generated footer 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.
|
|
164
166
|
- 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.
|
|
165
167
|
|
|
166
168
|
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)`.
|
|
@@ -176,7 +178,7 @@ The extension injects prompt guidance by context:
|
|
|
176
178
|
- For Telegram-originated turns, the compact note routes the agent to `telegram-bridge`, which owns voice/button/direct-delivery/Threaded Mode/formatting/debug guidance.
|
|
177
179
|
- For Telegram-originated turns, write the full technical answer as normal Markdown.
|
|
178
180
|
- Add `telegram_voice` with positional CML by default or JSON when multiline content, named fields, or escaping requires it. A companion summary is optional, no specific summary format is required.
|
|
179
|
-
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style`
|
|
181
|
+
- Add `telegram_button` with a JSON object, JSON matrix, or positional CML. Use a hidden comment for footer placement or a standalone triple-backtick `telegram_button` block for placement between paragraphs. Prefer one matrix for multiple controls. Use `label` plus `prompt`, or `value` when they are identical; `selected_style` and boolean `disabled` are optional. Keep at least one useful enabled action on an interactive surface. A button-only reply may omit parent text because the bridge supplies `☑️ **Choose an option:**` automatically.
|
|
180
182
|
- 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.
|
|
181
183
|
- 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.
|
|
182
184
|
|
|
@@ -68,7 +68,7 @@ Every assistant-authored HTML comment is transport-private on Telegram: previews
|
|
|
68
68
|
- `telegram_message(text, chat_id?, thread_id?)` sends a direct Telegram Markdown message when this Pi instance owns `/telegram-connect` or is registered with the multi-instance bus. During an active Telegram turn, omitted targeting and an explicit target equal to that turn are rejected so the ordinary final-reply path remains the sole current-target response; an explicit different chat/thread target remains allowed for requested cross-target delivery. Outside active turns, paired/default local/TUI delivery remains unchanged. 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.
|
|
69
69
|
- The bundled `telegram-bridge` Skill owns action syntax, target routing, Threaded Mode, formatting, Generative App operation, and profile-specific debugging guidance. The regular prompt routes applicable turns to that Skill. `telegram_attach`, `telegram_bind`, and `telegram_message` 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.
|
|
70
70
|
- `telegram_voice` hidden comments request Telegram-native voice delivery through `{text}`, `{text|lang}`, `{text|lang|rate}`, or a JSON object. JSON is the fallback for multiline content, named fields, or escaping; equivalent `text` or `value` supplies the spoken payload, with explicit `text` taking precedence.
|
|
71
|
-
- `telegram_button` hidden comments create
|
|
71
|
+
- `telegram_button` hidden comments create footer buttons; standalone column-zero triple-backtick `telegram_button` blocks create button rows between paragraphs in Native Rich Markdown. Both accept the same singleton or mixed JSON/CML matrix and share prompt/app routing. Native rows allow at most eight buttons and must fit one Rich Message chunk; invalid or incomplete blocks register nothing. Drafts hide action fences. HTML compatibility projects fenced controls into the footer. In-body clicks acknowledge without recoloring the Rich body; selected-style highlighting remains footer-only. One marker accepts a JSON object, adaptive JSON/CML matrix, or positional [Compact Matrix Literal](./compact-matrix-literal.md). Named JSON objects and positional cells may coexist in one matrix or row; separators are optional and one trailing comma is tolerated at matrix, row, and JSON-object boundaries. Top-level cells become full-width rows, while nested rows group one or more buttons horizontally without an artificial parser-width cap. CML uses `{value}`, `{label|prompt}`, prompt-only `{|prompt}`, or the corresponding three-atom form with `selected_style`; an omitted label uses the existing prompt-as-label fallback, and the optional third atom requires a non-empty prompt and accepts only `primary`, `success`, or `danger`. A fourth atom accepts `1` or `true` (disabled), and `0` or `false` (enabled), with exact lowercase spelling; an omitted fourth position stays enabled, and the third atom may be empty in this form (`{|Next||1}`). JSON uses boolean `disabled`. Disabled cells need no prompt or selected style: `{Next|||1}` is label-only and `{|||1}` is blank (JSON `{"label":"Next","disabled":true}` and `{"disabled":true}`). The Telegram renderer supplies a non-breaking space only when the label is empty. Disabled cells stay visible but carry `disabled: {}` instead of callback data and register no prompt or bound action; invalid disabled values reject the candidate matrix. It trims atom boundaries and supports only the minimal escapes `\|`, `\}`, and `\\`. Prefer one matrix comment for multiple buttons. Use JSON `label` plus `prompt`, or `value` when both strings are identical. Action markers are colon-free; colon-prefixed payloads are rejected. Use top-level column-zero action wrappers, outside quotes, lists, or enclosing code examples. Ordinary code fences and larger outer fences preserve literal examples; bare JSON/CML in prose never activates.
|
|
72
72
|
|
|
73
73
|
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.
|
|
74
74
|
|
|
@@ -245,7 +245,10 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
245
245
|
Config.createTelegramProactivePushChatIdGetter(proactivePushTargetGetter);
|
|
246
246
|
const buttonActionStore = Outbound.createTelegramButtonActionStore();
|
|
247
247
|
const planGenerativeAppOutput =
|
|
248
|
-
Outbound.createTelegramOutboundReplyPlanner(
|
|
248
|
+
Outbound.createTelegramOutboundReplyPlanner(
|
|
249
|
+
buttonActionStore,
|
|
250
|
+
configControls.getAssistantRenderingMode,
|
|
251
|
+
);
|
|
249
252
|
const pendingModelSwitchStore =
|
|
250
253
|
Model.createPendingModelSwitchStore<
|
|
251
254
|
Model.ScopedTelegramModel<ActivePiModel>
|
|
@@ -400,6 +403,15 @@ export default function (pi: Pi.ExtensionAPI) {
|
|
|
400
403
|
TelegramApi.createDefaultTelegramBridgeApiRuntime({
|
|
401
404
|
getBotToken: configStore.getBotToken,
|
|
402
405
|
recordRuntimeEvent,
|
|
406
|
+
captureRequestErrorHandler(body) {
|
|
407
|
+
return Sync.captureTelegramStaleTargetRequestRecovery(body, {
|
|
408
|
+
...staleTopicApiErrorRecoveryDeps,
|
|
409
|
+
getCurrentLeaderEpoch,
|
|
410
|
+
getSessionGeneration: telegramSessionContextStore.getGeneration,
|
|
411
|
+
getProfileName: configStore.getActiveProfileName,
|
|
412
|
+
onRecovered: runtimeDiagnostics.scheduleSnapshotPersist,
|
|
413
|
+
});
|
|
414
|
+
},
|
|
403
415
|
});
|
|
404
416
|
const telegramBusFollowerClients =
|
|
405
417
|
BusFollower.createTelegramBusFollowerClientRuntime<
|
|
@@ -518,7 +518,10 @@ export function registerTelegramCommandsAndTools({
|
|
|
518
518
|
GenerativeApps.registerTelegramBindTool(pi, {
|
|
519
519
|
agentDir,
|
|
520
520
|
getActiveTurn: activeTurnRuntime.get,
|
|
521
|
-
planOutput: OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
521
|
+
planOutput: OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
522
|
+
buttonActionStore,
|
|
523
|
+
Config.createTelegramConfigControls(configStore).getAssistantRenderingMode,
|
|
524
|
+
),
|
|
522
525
|
sendMarkdownReply,
|
|
523
526
|
recordRuntimeEvent,
|
|
524
527
|
});
|
|
@@ -538,7 +541,10 @@ export function registerTelegramCommandsAndTools({
|
|
|
538
541
|
routeAgentMessage,
|
|
539
542
|
canSendDirect,
|
|
540
543
|
planMessage:
|
|
541
|
-
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
544
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
545
|
+
buttonActionStore,
|
|
546
|
+
Config.createTelegramConfigControls(configStore).getAssistantRenderingMode,
|
|
547
|
+
),
|
|
542
548
|
sendMarkdownMessage: (chatId, markdown, options) =>
|
|
543
549
|
sendMarkdownReply(chatId, undefined, markdown, options),
|
|
544
550
|
recordRuntimeEvent,
|
|
@@ -854,7 +860,10 @@ export function registerTelegramLifecycleRuntimeHooks({
|
|
|
854
860
|
});
|
|
855
861
|
};
|
|
856
862
|
const outboundReplyPlanner =
|
|
857
|
-
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
863
|
+
OutboundHandlers.createTelegramOutboundReplyPlanner(
|
|
864
|
+
buttonActionStore,
|
|
865
|
+
getAssistantRenderingMode,
|
|
866
|
+
);
|
|
858
867
|
const voiceReplySenderDeps = {
|
|
859
868
|
execCommand: CommandTemplates.execCommandTemplate,
|
|
860
869
|
sendMultipart: callMultipart,
|
|
@@ -390,9 +390,8 @@ export interface TelegramBusFollowerTargetReplacementHandlerDeps<TContext> {
|
|
|
390
390
|
>;
|
|
391
391
|
registrationState: Pick<
|
|
392
392
|
TelegramBusFollowerRegistrationState,
|
|
393
|
-
"getTarget" | "setRegistered"
|
|
394
|
-
|
|
395
|
-
Partial<Pick<TelegramBusFollowerRegistrationState, "getGeneration">>;
|
|
393
|
+
"getTarget" | "setRegistered" | "getGeneration"
|
|
394
|
+
>;
|
|
396
395
|
instanceId: string;
|
|
397
396
|
getManualFollowerProfileKey: () => string;
|
|
398
397
|
manualFollowerOwnerId: string;
|
|
@@ -471,6 +470,7 @@ export interface TelegramBusForwardedUpdateReceiverRuntimeDeps<TContext> {
|
|
|
471
470
|
target: TelegramTarget & { threadId: number };
|
|
472
471
|
oldTarget?: TelegramTarget & { threadId: number };
|
|
473
472
|
reason: "thread-restore";
|
|
473
|
+
registrationGeneration: string;
|
|
474
474
|
},
|
|
475
475
|
ctx: TContext,
|
|
476
476
|
) => Promise<void> | void;
|
|
@@ -569,26 +569,35 @@ export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
|
569
569
|
> {
|
|
570
570
|
const getNowMs = deps.getNowMs ?? Date.now;
|
|
571
571
|
return async (input, ctx) => {
|
|
572
|
+
const assertCurrent = (expectedTarget = input.oldTarget): void => {
|
|
573
|
+
const target = deps.registrationState.getTarget();
|
|
574
|
+
if (!input.registrationGeneration ||
|
|
575
|
+
deps.registrationState.getGeneration() !== input.registrationGeneration ||
|
|
576
|
+
!input.oldTarget || !expectedTarget || target?.chatId !== expectedTarget.chatId ||
|
|
577
|
+
target.threadId !== expectedTarget.threadId ||
|
|
578
|
+
input.target.chatId !== input.oldTarget.chatId ||
|
|
579
|
+
input.target.threadId === input.oldTarget.threadId) {
|
|
580
|
+
throw new Error("Stale Telegram follower target replacement authority.");
|
|
581
|
+
}
|
|
582
|
+
};
|
|
583
|
+
assertCurrent();
|
|
572
584
|
await deps.topicTargetStore.load();
|
|
585
|
+
assertCurrent();
|
|
573
586
|
const nowMs = getNowMs();
|
|
574
587
|
const currentRecord = Threads.findCurrentTelegramInstanceThreadRecord({
|
|
575
588
|
records: deps.topicTargetStore.list(),
|
|
576
589
|
instanceId: deps.instanceId,
|
|
577
|
-
preferredTarget: input.oldTarget
|
|
590
|
+
preferredTarget: input.oldTarget,
|
|
578
591
|
});
|
|
579
|
-
if (input.oldTarget
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
"deleted",
|
|
583
|
-
"Follower thread was replaced by thread restore.",
|
|
584
|
-
);
|
|
585
|
-
} else if (currentRecord) {
|
|
586
|
-
deps.topicTargetStore.markStaleByTarget(
|
|
587
|
-
currentRecord.target,
|
|
588
|
-
"deleted",
|
|
589
|
-
"Follower thread was replaced by thread restore.",
|
|
590
|
-
);
|
|
592
|
+
if (currentRecord && (currentRecord.target.chatId !== input.oldTarget!.chatId ||
|
|
593
|
+
currentRecord.target.threadId !== input.oldTarget!.threadId)) {
|
|
594
|
+
throw new Error("Stale Telegram follower binding replacement target.");
|
|
591
595
|
}
|
|
596
|
+
deps.topicTargetStore.markStaleByTarget(
|
|
597
|
+
input.oldTarget!,
|
|
598
|
+
"deleted",
|
|
599
|
+
"Follower thread was replaced by thread restore.",
|
|
600
|
+
);
|
|
592
601
|
const profileKey =
|
|
593
602
|
currentRecord?.profileKey ?? deps.getManualFollowerProfileKey();
|
|
594
603
|
deps.topicTargetStore.upsert({
|
|
@@ -612,15 +621,16 @@ export function createTelegramBusFollowerTargetReplacementHandler<TContext>(
|
|
|
612
621
|
deps.registrationState.setRegistered(true, input.target, {
|
|
613
622
|
slot: currentRecord?.slot,
|
|
614
623
|
threadName: currentRecord?.threadName,
|
|
615
|
-
generation:
|
|
624
|
+
generation: input.registrationGeneration,
|
|
616
625
|
});
|
|
626
|
+
await deps.topicTargetStore.persist();
|
|
627
|
+
assertCurrent(input.target);
|
|
617
628
|
deps.setSyncState(
|
|
618
629
|
Sync.markTelegramSyncSliceFresh(deps.getSyncState(), "target-bindings", {
|
|
619
630
|
nowMs,
|
|
620
631
|
action: "follower-thread-restore",
|
|
621
632
|
}),
|
|
622
633
|
);
|
|
623
|
-
await deps.topicTargetStore.persist();
|
|
624
634
|
deps.updateStatus(ctx);
|
|
625
635
|
deps.recordRuntimeEvent?.(
|
|
626
636
|
"bus",
|
|
@@ -1992,6 +2002,7 @@ export function createTelegramBusForwardedUpdateReceiverRuntime<TContext>(
|
|
|
1992
2002
|
target: envelope.target,
|
|
1993
2003
|
...(envelope.oldTarget ? { oldTarget: envelope.oldTarget } : {}),
|
|
1994
2004
|
reason: envelope.reason,
|
|
2005
|
+
registrationGeneration,
|
|
1995
2006
|
},
|
|
1996
2007
|
ctx,
|
|
1997
2008
|
);
|
|
@@ -135,6 +135,7 @@ export interface TelegramBusFollowerTargetProvisionerDeps {
|
|
|
135
135
|
export interface TelegramBusFollowerDisconnectHandlerDeps {
|
|
136
136
|
topicTargetStore: Pick<
|
|
137
137
|
Threads.TelegramTopicTargetStore,
|
|
138
|
+
| "list"
|
|
138
139
|
| "markStaleByTarget"
|
|
139
140
|
| "persist"
|
|
140
141
|
| "upsertPendingCleanup"
|
|
@@ -726,6 +727,7 @@ export function createTelegramBusFollowerTargetProvisioner(
|
|
|
726
727
|
],
|
|
727
728
|
}),
|
|
728
729
|
{
|
|
730
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
729
731
|
callApi: deps.callApi,
|
|
730
732
|
markStaleByTarget(target, syncStatus, lastSyncError) {
|
|
731
733
|
return deps.topicTargetStore.markStaleByTarget(
|
|
@@ -805,16 +807,21 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
805
807
|
target: { chatId: target.chatId, threadId: target.threadId },
|
|
806
808
|
requestedAtMs: (deps.getNowMs ?? Date.now)(),
|
|
807
809
|
};
|
|
810
|
+
const departingRecord = deps.topicTargetStore.list().find((record) => record.instanceId === follower.instanceId &&
|
|
811
|
+
record.target.chatId === target.chatId && record.target.threadId === target.threadId);
|
|
812
|
+
const isCleanupTargetProtected = Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore, departingRecord);
|
|
808
813
|
deps.topicTargetStore.upsertPendingCleanup(intent);
|
|
809
814
|
await deps.topicTargetStore.persist();
|
|
815
|
+
const cleanupPlan = ThreadReconciler.planThreadReconciliation({
|
|
816
|
+
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
817
|
+
currentLeaderEpoch: leaderEpoch,
|
|
818
|
+
records: [],
|
|
819
|
+
pendingCleanups: [intent],
|
|
820
|
+
});
|
|
810
821
|
const cleanup = await ThreadReconciler.applyThreadReconciliationPlan(
|
|
811
|
-
|
|
812
|
-
nowMs: (deps.getNowMs ?? Date.now)(),
|
|
813
|
-
currentLeaderEpoch: leaderEpoch,
|
|
814
|
-
records: [],
|
|
815
|
-
pendingCleanups: [intent],
|
|
816
|
-
}),
|
|
822
|
+
cleanupPlan,
|
|
817
823
|
{
|
|
824
|
+
isCleanupTargetProtected,
|
|
818
825
|
callApi: deps.callApi,
|
|
819
826
|
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
820
827
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
@@ -823,6 +830,7 @@ function createTelegramBusFollowerCleanupHandler(
|
|
|
823
830
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
824
831
|
},
|
|
825
832
|
);
|
|
833
|
+
if (cleanupPlan.actions.some((action) => isCleanupTargetProtected(action.target, action))) return;
|
|
826
834
|
if (cleanup.incompleteActions?.length) {
|
|
827
835
|
throw new Error(
|
|
828
836
|
"Telegram follower thread deletion was not confirmed; reconnect the leader to retry cleanup.",
|
|
@@ -916,6 +924,7 @@ export function createTelegramBusLeaderTargetProvisioner<TContext>(
|
|
|
916
924
|
});
|
|
917
925
|
deps.recordThreadReconciliationPlan?.(pendingCleanupPlan);
|
|
918
926
|
const cleanupPorts = {
|
|
927
|
+
isCleanupTargetProtected: Threads.createTelegramCleanupTargetProtection(deps.topicTargetStore),
|
|
919
928
|
callApi: deps.callApi,
|
|
920
929
|
markStaleByTarget: deps.topicTargetStore.markStaleByTarget,
|
|
921
930
|
removeCleanupIntentById: deps.topicTargetStore.removePendingCleanup,
|
|
@@ -1450,18 +1450,25 @@ export function createTelegramBusFollowerThreadRestoreHandler(
|
|
|
1450
1450
|
return async ({ record, target, oldTarget }) => {
|
|
1451
1451
|
if (!record.instanceId) return false;
|
|
1452
1452
|
const follower = deps.followerRegistry.get(record.instanceId);
|
|
1453
|
-
if (!follower
|
|
1453
|
+
if (!follower?.registrationGeneration || !oldTarget ||
|
|
1454
|
+
follower.target?.chatId !== oldTarget.chatId ||
|
|
1455
|
+
follower.target.threadId !== oldTarget.threadId ||
|
|
1456
|
+
target.chatId !== oldTarget.chatId || target.threadId === oldTarget.threadId) return false;
|
|
1454
1457
|
const replaced = await deps.followerTargetController.replaceTarget({
|
|
1455
1458
|
follower,
|
|
1456
1459
|
target,
|
|
1457
1460
|
oldTarget,
|
|
1458
1461
|
reason: "thread-restore",
|
|
1459
1462
|
});
|
|
1460
|
-
|
|
1463
|
+
const current = deps.followerRegistry.get(record.instanceId);
|
|
1464
|
+
if (!replaced || !current ||
|
|
1465
|
+
current.registrationGeneration !== follower.registrationGeneration ||
|
|
1466
|
+
current.target?.chatId !== oldTarget.chatId ||
|
|
1467
|
+
current.target.threadId !== oldTarget.threadId) return false;
|
|
1461
1468
|
deps.followerRegistry.register({
|
|
1462
|
-
...
|
|
1469
|
+
...current,
|
|
1463
1470
|
target,
|
|
1464
|
-
connectedAtMs:
|
|
1471
|
+
connectedAtMs: current.connectedAtMs,
|
|
1465
1472
|
});
|
|
1466
1473
|
deps.onRestored?.();
|
|
1467
1474
|
return true;
|
|
@@ -9,11 +9,13 @@ export type TelegramInlineKeyboardButtonStyle =
|
|
|
9
9
|
| "success"
|
|
10
10
|
| "primary";
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export type TelegramInlineKeyboardButton = {
|
|
13
13
|
text: string;
|
|
14
|
-
callback_data: string;
|
|
15
14
|
style?: TelegramInlineKeyboardButtonStyle;
|
|
16
|
-
}
|
|
15
|
+
} & (
|
|
16
|
+
| { callback_data: string; disabled?: never }
|
|
17
|
+
| { disabled: Record<string, never>; callback_data?: never }
|
|
18
|
+
);
|
|
17
19
|
|
|
18
20
|
export interface TelegramInlineKeyboardMarkup {
|
|
19
21
|
inline_keyboard: TelegramInlineKeyboardButton[][];
|
|
@@ -12,6 +12,8 @@ import type {
|
|
|
12
12
|
} from "./keyboard.ts";
|
|
13
13
|
import {
|
|
14
14
|
parseTelegramActionPayloadRows,
|
|
15
|
+
parseTelegramButtonPayloadRows,
|
|
16
|
+
replaceTelegramButtonFences,
|
|
15
17
|
replaceTopLevelHtmlComments,
|
|
16
18
|
} from "./outbound-markup.ts";
|
|
17
19
|
import {
|
|
@@ -34,6 +36,7 @@ export interface TelegramOutboundButtonAction {
|
|
|
34
36
|
prompt: string;
|
|
35
37
|
binding?: TelegramOutboundButtonBinding;
|
|
36
38
|
selectedStyle?: TelegramInlineKeyboardButtonStyle;
|
|
39
|
+
disabled?: true;
|
|
37
40
|
}
|
|
38
41
|
|
|
39
42
|
export interface TelegramOutboundButtonStoredAction extends TelegramOutboundButtonAction {
|
|
@@ -115,6 +118,12 @@ function parseTelegramButtonAction(
|
|
|
115
118
|
const explicitLabel = getTelegramButtonString(payload, "label");
|
|
116
119
|
const explicitPrompt = getTelegramButtonString(payload, "prompt");
|
|
117
120
|
const label = explicitLabel ?? value ?? explicitPrompt;
|
|
121
|
+
if (payload.disabled !== undefined && typeof payload.disabled !== "boolean") {
|
|
122
|
+
return undefined;
|
|
123
|
+
}
|
|
124
|
+
if (payload.disabled === true) {
|
|
125
|
+
return { text: label ?? "", prompt: "", disabled: true };
|
|
126
|
+
}
|
|
118
127
|
const prompt = explicitPrompt ?? value ?? explicitLabel;
|
|
119
128
|
if (!label || !prompt) return undefined;
|
|
120
129
|
const selectedStyle = payload.selected_style;
|
|
@@ -159,6 +168,7 @@ export function createTelegramButtonActionStore(
|
|
|
159
168
|
return {
|
|
160
169
|
text: action.text,
|
|
161
170
|
prompt: action.prompt,
|
|
171
|
+
...(action.disabled ? { disabled: true as const } : {}),
|
|
162
172
|
...(action.binding ? { binding: action.binding } : {}),
|
|
163
173
|
...(action.selectedStyle
|
|
164
174
|
? { selectedStyle: action.selectedStyle }
|
|
@@ -171,33 +181,75 @@ export function createTelegramButtonActionStore(
|
|
|
171
181
|
const DEFAULT_TELEGRAM_BUTTON_REPLY_MARKDOWN =
|
|
172
182
|
"☑️ **Choose an option:**";
|
|
173
183
|
|
|
184
|
+
function escapeTelegramRichButtonText(text: string): string {
|
|
185
|
+
return text.replace(/[&<>"'`\\*_\[\]{}$~\r\n]/g, (character) =>
|
|
186
|
+
`&#${character.charCodeAt(0)};`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function renderTelegramRichButtonRow(
|
|
191
|
+
row: TelegramOutboundButtonMarkup["inline_keyboard"][number],
|
|
192
|
+
): string {
|
|
193
|
+
return `<tg-button-row>${row.map((button) => {
|
|
194
|
+
const attributes = button.disabled
|
|
195
|
+
? 'type="disabled"'
|
|
196
|
+
: `type="callback_data" data="${escapeTelegramRichButtonText(button.callback_data)}"`;
|
|
197
|
+
return `<tg-button ${attributes}>${escapeTelegramRichButtonText(button.text)}</tg-button>`;
|
|
198
|
+
}).join("")}</tg-button-row>`;
|
|
199
|
+
}
|
|
200
|
+
|
|
174
201
|
export function planTelegramButtonReply(
|
|
175
202
|
markdown: string,
|
|
176
203
|
deps: {
|
|
177
204
|
registerAction: (action: TelegramOutboundButtonAction) => string;
|
|
178
205
|
binding?: TelegramOutboundButtonBinding;
|
|
206
|
+
rendering?: "rich" | "html";
|
|
179
207
|
},
|
|
180
208
|
): TelegramButtonReplyPlan {
|
|
181
209
|
const keyboard: TelegramOutboundButtonMarkup["inline_keyboard"] = [];
|
|
182
|
-
const
|
|
210
|
+
const buildRows = (
|
|
211
|
+
payloadRows: Record<string, unknown>[][],
|
|
212
|
+
rich: boolean,
|
|
213
|
+
): TelegramOutboundButtonMarkup["inline_keyboard"] | undefined => {
|
|
214
|
+
const actions = payloadRows.map((row) => row.map(parseTelegramButtonAction));
|
|
215
|
+
if (actions.some((row) => row.some((action) => !action))) return undefined;
|
|
216
|
+
if (rich && actions.some((row) => {
|
|
217
|
+
const projected = row.map((action) => action!.disabled
|
|
218
|
+
? { text: action!.text || "\u00a0", disabled: {} }
|
|
219
|
+
: { text: action!.text, callback_data: "x".repeat(64) });
|
|
220
|
+
return row.length > 8 || renderTelegramRichButtonRow(projected).length > 32768;
|
|
221
|
+
})) return undefined;
|
|
222
|
+
return actions.map((row) => row.map((action) => action!.disabled
|
|
223
|
+
? { text: action!.text || "\u00a0", disabled: {} }
|
|
224
|
+
: {
|
|
225
|
+
text: action!.text,
|
|
226
|
+
callback_data: deps.registerAction({
|
|
227
|
+
...action!,
|
|
228
|
+
...(deps.binding ? { binding: deps.binding } : {}),
|
|
229
|
+
}),
|
|
230
|
+
}));
|
|
231
|
+
};
|
|
232
|
+
const withRichButtons = replaceTelegramButtonFences(markdown, (payload, closed) => {
|
|
233
|
+
if (!closed) return "";
|
|
234
|
+
const payloadRows = parseTelegramButtonPayloadRows(payload);
|
|
235
|
+
if (!payloadRows) return "";
|
|
236
|
+
const rich = deps.rendering !== "html";
|
|
237
|
+
const rows = buildRows(payloadRows, rich);
|
|
238
|
+
if (!rows) return "";
|
|
239
|
+
if (!rich) {
|
|
240
|
+
keyboard.push(...rows);
|
|
241
|
+
return "";
|
|
242
|
+
}
|
|
243
|
+
return `\n${rows.map(renderTelegramRichButtonRow).join("\n\n")}\n`;
|
|
244
|
+
});
|
|
245
|
+
const stripped = replaceTopLevelHtmlComments(withRichButtons, (comment) => {
|
|
183
246
|
const command = "telegram_button";
|
|
184
247
|
const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
|
|
185
248
|
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
186
249
|
const payloadRows = parseTelegramActionPayloadRows(comment, command);
|
|
187
250
|
if (!payloadRows) return "";
|
|
188
|
-
const
|
|
189
|
-
|
|
190
|
-
);
|
|
191
|
-
if (actionRows.some((row) => row.some((action) => !action))) return "";
|
|
192
|
-
for (const actionRow of actionRows) {
|
|
193
|
-
keyboard.push(actionRow.map((action) => ({
|
|
194
|
-
text: action!.text,
|
|
195
|
-
callback_data: deps.registerAction({
|
|
196
|
-
...action!,
|
|
197
|
-
...(deps.binding ? { binding: deps.binding } : {}),
|
|
198
|
-
}),
|
|
199
|
-
})));
|
|
200
|
-
}
|
|
251
|
+
const rows = buildRows(payloadRows, false);
|
|
252
|
+
if (rows) keyboard.push(...rows);
|
|
201
253
|
return "";
|
|
202
254
|
});
|
|
203
255
|
const visibleMarkdown = normalizeMarkdownAfterButtonExtraction(stripped);
|
|
@@ -254,7 +306,7 @@ export function markTelegramButtonSelected(
|
|
|
254
306
|
let matched = false;
|
|
255
307
|
const inlineKeyboard = replyMarkup.inline_keyboard.map((row) =>
|
|
256
308
|
row.map((button) => {
|
|
257
|
-
if (button.callback_data !== callbackData) return { ...button };
|
|
309
|
+
if (button.disabled || button.callback_data !== callbackData) return { ...button };
|
|
258
310
|
matched = true;
|
|
259
311
|
return { ...button, style: selectedStyle };
|
|
260
312
|
}),
|
|
@@ -277,6 +329,11 @@ export async function handleTelegramButtonCallbackQuery<TContext = unknown>(
|
|
|
277
329
|
return false;
|
|
278
330
|
}
|
|
279
331
|
|
|
332
|
+
if (action.disabled) {
|
|
333
|
+
await deps.answerCallbackQuery(query.id, "Button action unavailable.");
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
280
337
|
const chatId = query.message?.chat?.id;
|
|
281
338
|
const messageId = query.message?.message_id;
|
|
282
339
|
if (typeof chatId !== "number" || typeof messageId !== "number") {
|
|
@@ -90,6 +90,50 @@ export function collectTopLevelHtmlComments(markdown: string): {
|
|
|
90
90
|
return { comments };
|
|
91
91
|
}
|
|
92
92
|
|
|
93
|
+
export function replaceTelegramButtonFences(
|
|
94
|
+
markdown: string,
|
|
95
|
+
replace: (payload: string, closed: boolean) => string,
|
|
96
|
+
): string {
|
|
97
|
+
let result = "";
|
|
98
|
+
let copied = 0;
|
|
99
|
+
let offset = 0;
|
|
100
|
+
let fence: TelegramTopLevelFenceState | undefined;
|
|
101
|
+
let actionStart: number | undefined;
|
|
102
|
+
let contentStart = 0;
|
|
103
|
+
while (offset < markdown.length) {
|
|
104
|
+
const end = getMarkdownLineEnd(markdown, offset);
|
|
105
|
+
const line = getMarkdownLineText(markdown, offset, end);
|
|
106
|
+
if (fence) {
|
|
107
|
+
if (isTopLevelClosingFence(line, fence)) {
|
|
108
|
+
if (actionStart !== undefined) {
|
|
109
|
+
result += markdown.slice(copied, actionStart);
|
|
110
|
+
result += replace(markdown.slice(contentStart, offset), true) + "\n";
|
|
111
|
+
copied = end;
|
|
112
|
+
actionStart = undefined;
|
|
113
|
+
}
|
|
114
|
+
fence = undefined;
|
|
115
|
+
}
|
|
116
|
+
} else if (line.includes("<!--")) {
|
|
117
|
+
const close = markdown.indexOf("-->", offset + line.indexOf("<!--") + 4);
|
|
118
|
+
if (close < 0) break;
|
|
119
|
+
offset = getMarkdownLineEnd(markdown, close + 3);
|
|
120
|
+
continue;
|
|
121
|
+
} else {
|
|
122
|
+
fence = getTopLevelOpeningFence(line);
|
|
123
|
+
if (fence && /^```telegram_button[ \t]*$/.test(line)) {
|
|
124
|
+
actionStart = offset;
|
|
125
|
+
contentStart = end;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
offset = end;
|
|
129
|
+
}
|
|
130
|
+
if (actionStart !== undefined) {
|
|
131
|
+
result += markdown.slice(copied, actionStart);
|
|
132
|
+
return result + replace(markdown.slice(contentStart), false);
|
|
133
|
+
}
|
|
134
|
+
return result + markdown.slice(copied);
|
|
135
|
+
}
|
|
136
|
+
|
|
93
137
|
export function replaceTopLevelHtmlComments(
|
|
94
138
|
markdown: string,
|
|
95
139
|
replacer: (comment: TelegramTopLevelHtmlComment) => string,
|
|
@@ -275,17 +319,26 @@ type TelegramCompactActionPayloadParser = (
|
|
|
275
319
|
function parseTelegramButtonCompactActionPayload(
|
|
276
320
|
atoms: readonly string[],
|
|
277
321
|
): Record<string, unknown> | undefined {
|
|
278
|
-
const [label, prompt, selectedStyle] = atoms;
|
|
322
|
+
const [label, prompt, selectedStyle, disabled] = atoms;
|
|
279
323
|
if (atoms.length === 1) return label ? { value: label } : undefined;
|
|
280
|
-
|
|
324
|
+
const isDisabled = disabled === "1" || disabled === "true";
|
|
325
|
+
if (!prompt && !(atoms.length === 4 && isDisabled)) return undefined;
|
|
281
326
|
const action = label ? { label, prompt } : { prompt };
|
|
282
327
|
if (atoms.length === 2) return action;
|
|
283
328
|
if (
|
|
284
329
|
selectedStyle !== "primary" &&
|
|
285
330
|
selectedStyle !== "success" &&
|
|
286
|
-
selectedStyle !== "danger"
|
|
331
|
+
selectedStyle !== "danger" &&
|
|
332
|
+
!(atoms.length === 4 && selectedStyle === "")
|
|
287
333
|
) return undefined;
|
|
288
|
-
|
|
334
|
+
if (atoms.length === 4 && !isDisabled && disabled !== "0" && disabled !== "false") {
|
|
335
|
+
return undefined;
|
|
336
|
+
}
|
|
337
|
+
return {
|
|
338
|
+
...action,
|
|
339
|
+
...(selectedStyle ? { selected_style: selectedStyle } : {}),
|
|
340
|
+
...(atoms.length === 4 ? { disabled: isDisabled } : {}),
|
|
341
|
+
};
|
|
289
342
|
}
|
|
290
343
|
|
|
291
344
|
function parseTelegramVoiceCompactActionPayload(
|
|
@@ -303,7 +356,7 @@ function parseTelegramVoiceCompactActionPayload(
|
|
|
303
356
|
function parseTelegramAdaptiveActionPayloadRows(
|
|
304
357
|
source: string,
|
|
305
358
|
parseCompactPayload: TelegramCompactActionPayloadParser,
|
|
306
|
-
options: { allowTrailing?: boolean } = {},
|
|
359
|
+
options: { allowTrailing?: boolean; maxCompactAtoms?: number } = {},
|
|
307
360
|
): { rows: Record<string, unknown>[][]; end: number } | undefined {
|
|
308
361
|
let offset = 0;
|
|
309
362
|
const isStructuralWhitespace = (character: string | undefined): boolean =>
|
|
@@ -343,7 +396,7 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
343
396
|
continue;
|
|
344
397
|
}
|
|
345
398
|
if (character === "|") {
|
|
346
|
-
if (atomSources.length >= 3) return undefined;
|
|
399
|
+
if (atomSources.length >= (options.maxCompactAtoms ?? 3)) return undefined;
|
|
347
400
|
atomSources.push([]);
|
|
348
401
|
offset += 1;
|
|
349
402
|
continue;
|
|
@@ -509,6 +562,16 @@ function isPlausibleTelegramMatrixStart(
|
|
|
509
562
|
);
|
|
510
563
|
}
|
|
511
564
|
|
|
565
|
+
export function parseTelegramButtonPayloadRows(
|
|
566
|
+
source: string,
|
|
567
|
+
): Record<string, unknown>[][] | undefined {
|
|
568
|
+
return parseTelegramAdaptiveActionPayloadRows(
|
|
569
|
+
source.trim(),
|
|
570
|
+
parseTelegramButtonCompactActionPayload,
|
|
571
|
+
{ maxCompactAtoms: 4 },
|
|
572
|
+
)?.rows;
|
|
573
|
+
}
|
|
574
|
+
|
|
512
575
|
export function parseTelegramActionPayloadRows(
|
|
513
576
|
comment: TelegramTopLevelHtmlComment,
|
|
514
577
|
command: string,
|
|
@@ -533,7 +596,7 @@ export function parseTelegramActionPayloadRows(
|
|
|
533
596
|
const parsed = parseTelegramAdaptiveActionPayloadRows(
|
|
534
597
|
content.slice(offset),
|
|
535
598
|
parseTelegramButtonCompactActionPayload,
|
|
536
|
-
{ allowTrailing: true },
|
|
599
|
+
{ allowTrailing: true, maxCompactAtoms: 4 },
|
|
537
600
|
);
|
|
538
601
|
if (parsed) return parsed.rows;
|
|
539
602
|
const end = findTelegramStructuredPayloadEnd(content, offset);
|
|
@@ -546,8 +609,15 @@ export function parseTelegramActionPayloadRows(
|
|
|
546
609
|
"prompt",
|
|
547
610
|
"value",
|
|
548
611
|
"selected_style",
|
|
612
|
+
"disabled",
|
|
549
613
|
]);
|
|
550
|
-
|
|
614
|
+
if (!attributes) return undefined;
|
|
615
|
+
return [[{
|
|
616
|
+
...attributes,
|
|
617
|
+
...(attributes.disabled === "true" || attributes.disabled === "false"
|
|
618
|
+
? { disabled: attributes.disabled === "true" }
|
|
619
|
+
: {}),
|
|
620
|
+
}]];
|
|
551
621
|
}
|
|
552
622
|
|
|
553
623
|
export function normalizeMarkdownAfterVoiceExtraction(
|
|
@@ -589,7 +659,9 @@ function stripTelegramHtmlCommentBlocks(markdown: string): string {
|
|
|
589
659
|
}
|
|
590
660
|
|
|
591
661
|
export function stripTelegramCommentMarkupForPreview(markdown: string): string {
|
|
592
|
-
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(
|
|
662
|
+
const withoutClosedBlocks = stripTelegramHtmlCommentBlocks(
|
|
663
|
+
replaceTelegramButtonFences(markdown, () => ""),
|
|
664
|
+
);
|
|
593
665
|
const openBlockIndex =
|
|
594
666
|
findTopLevelOpenOrPartialHtmlCommentIndex(withoutClosedBlocks);
|
|
595
667
|
const previewMarkdown =
|
|
@@ -835,6 +835,7 @@ export {
|
|
|
835
835
|
|
|
836
836
|
export function createTelegramOutboundReplyPlanner(
|
|
837
837
|
store: Pick<TelegramButtonActionStore, "register">,
|
|
838
|
+
getRenderingMode: () => "rich" | "html" = () => "rich",
|
|
838
839
|
): (
|
|
839
840
|
markdown: string,
|
|
840
841
|
options?: { binding?: TelegramOutboundButtonBinding },
|
|
@@ -842,6 +843,7 @@ export function createTelegramOutboundReplyPlanner(
|
|
|
842
843
|
return (markdown, options) => {
|
|
843
844
|
const buttonReply = planTelegramButtonReply(markdown, {
|
|
844
845
|
registerAction: store.register,
|
|
846
|
+
rendering: getRenderingMode(),
|
|
845
847
|
...(options?.binding ? { binding: options.binding } : {}),
|
|
846
848
|
});
|
|
847
849
|
|
|
@@ -532,7 +532,7 @@ function splitTelegramNativeMarkdownCountedBlocks(block: string): string[] {
|
|
|
532
532
|
function countTelegramNativeMarkdownBlocks(block: string): number {
|
|
533
533
|
if (/^ {0,3}(`{3,}|~{3,})/.test(block)) return 1;
|
|
534
534
|
const lines = block.split("\n").filter((line) => line.trim().length > 0);
|
|
535
|
-
if (lines.some((line) => /^\s*([-*+] |\d+\.
|
|
535
|
+
if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\||<tg-button-row>)/.test(line))) {
|
|
536
536
|
return Math.max(1, lines.length);
|
|
537
537
|
}
|
|
538
538
|
return 1;
|