@llblab/pi-telegram 0.8.0 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -0
- package/docs/command-templates.md +21 -5
- package/docs/locks.md +13 -11
- package/docs/outbound-handlers.md +1 -1
- package/lib/menu-queue.ts +161 -20
- package/lib/menu-status.ts +1 -1
- package/lib/outbound-handlers.ts +2 -2
- package/lib/queue.ts +18 -4
- package/lib/updates.ts +34 -21
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.8.1: Outbound Voice Translation Hotfix
|
|
4
|
+
|
|
5
|
+
- `[Outbound Voice]` Composed voice handlers now pass the original `telegram_voice` text to the first pipeline step through stdin, then continue piping each step's stdout into the next step. Impact: translate-from-stdin voice pipelines can translate hidden voice text before TTS instead of failing with an empty first-step input.
|
|
6
|
+
- `[Queue Menu]` Queue item detail previews now render prompt text inside a bounded raw `<pre>` block, and generic queue navigation/headings use the `⏳` waiting icon. Impact: absolute file paths and attachment references remain readable without Telegram interpreting slash-prefixed paths as commands, long previews are truncated below Telegram's message limit, and the queue surface has a clearer generic icon distinct from ordered-list or priority markers.
|
|
7
|
+
- `[Queue Delete]` Queue item removal now uses explicit `🗑 Delete` wording and opens a two-button confirmation (`🗑 Yes, delete` / `❌ No`) before mutating the queue. Impact: accidental queue-item deletion is harder while the item detail flow remains compact.
|
|
8
|
+
- `[Queue Priority]` Priority reactions now preserve the exact normalized promotion emoji and render it in both queue-menu rows and the π status-bar queued preview. Reaction metadata is grouped into semantic id ranges (`10..13` for priority, `20..23` for removal). Impact: `👍`, `⚡`, `❤️`, and `🕊️` keep the same priority semantics while making the user's chosen reaction visible across Telegram and TUI surfaces.
|
|
9
|
+
- `[Configuration Docs]` Documented the configuration philosophy that rich visual/TUI setup stays minimal for now while agents can read README/docs and update `telegram.json` for advanced workflows. Impact: configuration guidance matches the extension's agent-assisted operator model without adding premature TUI surfaces.
|
|
10
|
+
- `[Outbound Docs]` Tightened voice-handler critical-step wording around transform → TTS → conversion pipelines and handler-level fallbacks. Impact: docs now match translated voice pipelines without implying provider-specific TTS fallbacks.
|
|
11
|
+
- `[Package]` Bumped package metadata to `0.8.1` and kept the lockfile in sync.
|
|
12
|
+
- `[Command Template Docs]` Synchronized `docs/command-templates.md` bit-for-bit with the current portable standard shared by `pi-auto-tools`. Impact: the documented standard now includes retry, fail-open composition, critical-step abort semantics, and the 30s default timeout in the same wording across both extensions.
|
|
13
|
+
- `[Lock Docs]` Synchronized `docs/locks.md` bit-for-bit with the extension-neutral Locks Standard shared by `pi-wakeup`. Impact: singleton ownership documentation no longer carries project-specific examples that prevent exact reuse across extensions.
|
|
14
|
+
|
|
3
15
|
## 0.8.0: Handler Bus
|
|
4
16
|
|
|
5
17
|
- `[Inbound Handlers]` Added `inboundHandlers` as the provider-neutral Telegram → π transformation bus. Raw Telegram text can match `type: "text"`, `mime: "text/plain"`, or `mime: "text/*"`, receives text on stdin and `{text}`, and non-empty stdout replaces the prompt text before queueing; media/file handlers keep the existing `{file}`/`{mime}`/`{type}` behavior with optional independent selectors. Impact: translation, normalization, STT, OCR, and file extraction can share one command-template integration model.
|
package/README.md
CHANGED
|
@@ -37,6 +37,12 @@ pi install git:github.com/llblab/pi-telegram
|
|
|
37
37
|
|
|
38
38
|
## Configure
|
|
39
39
|
|
|
40
|
+
### Configuration Philosophy
|
|
41
|
+
|
|
42
|
+
The extension intentionally keeps rich visual/TUI configuration minimal for now. Rich setup screens may arrive later, but they are not the main configuration surface yet.
|
|
43
|
+
|
|
44
|
+
For advanced setup, ask an agent to read this `README.md` and the docs, then update `~/.pi/agent/telegram.json` for your workflow. Agents are good at small configuration changes, and this keeps the bridge simple while handler pipelines and operator preferences continue to evolve.
|
|
45
|
+
|
|
40
46
|
### 1. Telegram Bot
|
|
41
47
|
|
|
42
48
|
1. Open [@BotFather](https://t.me/BotFather)
|
|
@@ -4,7 +4,7 @@ Command templates are the portable integration format for deterministic local au
|
|
|
4
4
|
|
|
5
5
|
**Meta-contract:** transportable (bit-for-bit identical across projects), high-density (zero fluff), constant (evolve by crystallizing, not speculating), optimal minimum (add only when it hurts).
|
|
6
6
|
|
|
7
|
-
**Scope:** portable command execution format — shell-free exec, composition/pipes, default
|
|
7
|
+
**Scope:** portable command execution format — shell-free exec, composition/pipes, timeout (30s default), retry, critical-step branching, output artifact selection, handler-level fallback. Single JSON standard; no platform lock-in.
|
|
8
8
|
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -35,8 +35,9 @@ Common object fields:
|
|
|
35
35
|
| `template` | Required command string or ordered composition array |
|
|
36
36
|
| `args` | Optional placeholder-name declarations only; never stores defaults |
|
|
37
37
|
| `defaults` | Placeholder default values by name |
|
|
38
|
-
| `timeout` | Optional execution timeout
|
|
38
|
+
| `timeout` | Optional execution timeout in milliseconds; default `30000` (30s) |
|
|
39
39
|
| `output` | Optional result selector; default `"stdout"`, or a "runtime value", e.g. `"ogg"` |
|
|
40
|
+
| `retry` | Optional max attempts (including first); default `1`. Retries immediately on non-zero exit |
|
|
40
41
|
| `critical` | Optional boolean; default `false`. When `true`, failure aborts the entire root composition |
|
|
41
42
|
|
|
42
43
|
Storage paths, labels, selectors, descriptions, and registry-specific metadata belong to each extension's local schema.
|
|
@@ -118,7 +119,7 @@ template="echo 'literal words' {text}"
|
|
|
118
119
|
|
|
119
120
|
Composition rules:
|
|
120
121
|
|
|
121
|
-
- Execute leaves in order and
|
|
122
|
+
- Execute leaves in order; non-critical failures are recorded and execution continues, while `critical: true` failures abort the root composition
|
|
122
123
|
- Treat the whole composition as one handler for selector matching and fallback
|
|
123
124
|
- Top-level `args` and `defaults` apply to every leaf unless the leaf defines private values
|
|
124
125
|
- Leaf `args` replace inherited `args`; leaf `defaults` merge over inherited defaults; `timeout` and `output` are not inherited into leaves
|
|
@@ -167,6 +168,21 @@ Set `critical: true` on any leaf to abort the entire root composition on failure
|
|
|
167
168
|
|
|
168
169
|
A `critical` leaf in a nested composition still aborts the outermost root `template: [...]`. There is no per-branch scoping in the current standard.
|
|
169
170
|
|
|
171
|
+
## Retry
|
|
172
|
+
|
|
173
|
+
Set `retry: N` on a leaf to attempt execution up to `N` times (including the first). Retries happen immediately on non-zero exit. The first successful attempt stops the retry loop.
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"template": [
|
|
178
|
+
{ "template": "npm install", "retry": 3 },
|
|
179
|
+
{ "template": "npm test", "critical": true, "retry": 2 }
|
|
180
|
+
]
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
`npm install` is retried up to 3 times. `npm test` is retried up to 2 times; if all attempts fail, the critical step aborts the pipeline.
|
|
185
|
+
|
|
170
186
|
## Progressive Disclosure
|
|
171
187
|
|
|
172
188
|
The standard uses a single `template` field that grows with the user's needs:
|
|
@@ -175,10 +191,10 @@ The standard uses a single `template` field that grows with the user's needs:
|
|
|
175
191
|
string → leaf command
|
|
176
192
|
string[] → sequential composition
|
|
177
193
|
{ template } → leaf with defaults
|
|
178
|
-
{ template, critical, output } → full leaf
|
|
194
|
+
{ template, retry, critical, output } → full leaf
|
|
179
195
|
```
|
|
180
196
|
|
|
181
|
-
Start with a string. Add composition when needed. Add critical when safety matters. Same contract, growing capability, no dead weight.
|
|
197
|
+
Start with a string. Add composition when needed. Add retry when flaky. Add critical when safety matters. Same contract, growing capability, no dead weight.
|
|
182
198
|
|
|
183
199
|
## Tool Boundary
|
|
184
200
|
|
package/docs/locks.md
CHANGED
|
@@ -16,7 +16,7 @@ Path:
|
|
|
16
16
|
|
|
17
17
|
```json
|
|
18
18
|
{
|
|
19
|
-
"@
|
|
19
|
+
"@scope/pi-singleton": {
|
|
20
20
|
"pid": 2590864,
|
|
21
21
|
"cwd": "/home/user/project"
|
|
22
22
|
}
|
|
@@ -38,9 +38,9 @@ For npm-style package extensions, the canonical value is the `package.json` `nam
|
|
|
38
38
|
Examples:
|
|
39
39
|
|
|
40
40
|
```text
|
|
41
|
-
extensions/pi-
|
|
42
|
-
extensions/pi-
|
|
43
|
-
extensions/pi-
|
|
41
|
+
extensions/pi-singleton/package.json name=@scope/pi-singleton -> @scope/pi-singleton
|
|
42
|
+
extensions/pi-singleton/index.ts without package.json -> pi-singleton
|
|
43
|
+
extensions/pi-singleton.ts -> pi-singleton
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
## Required fields
|
|
@@ -62,7 +62,7 @@ During a user-initiated start/connect event, an extension should:
|
|
|
62
62
|
|
|
63
63
|
## Acquisition timing
|
|
64
64
|
|
|
65
|
-
Lock writes must be caused by an explicit user-initiated runtime event, such as
|
|
65
|
+
Lock writes must be caused by an explicit user-initiated runtime event, such as a start/connect command or a confirmed takeover prompt.
|
|
66
66
|
|
|
67
67
|
Extension initialization and session-start hooks may read `locks.json`, update local status, install ownership watchers, and resume local work when the existing lock already points at the current `pid`/`cwd`. After a full process restart, a session-start hook may replace a stale lock from the same `cwd` to restore explicitly requested ownership. They must not create ownership from an inactive lock, take over a live external owner, or replace a stale lock from another directory by themselves. Such locks should stay visible as state until the user runs the start/connect command. Session replacement should suspend local runtime work and ownership watchers without releasing the lock, so the next session in the same `pid`/`cwd` can resume from explicit ownership.
|
|
68
68
|
|
|
@@ -92,11 +92,13 @@ Do not print optional fields in normal UI unless they help the user act.
|
|
|
92
92
|
|
|
93
93
|
## Runtime status
|
|
94
94
|
|
|
95
|
-
Singleton extensions with footer/status presence should expose quiet but explicit local state
|
|
95
|
+
Singleton extensions with footer/status presence should expose quiet but explicit local state:
|
|
96
96
|
|
|
97
|
-
- `
|
|
98
|
-
- `
|
|
99
|
-
- `
|
|
97
|
+
- `off` when this pi instance does not own the singleton runtime
|
|
98
|
+
- `on` when this pi instance owns the runtime but has no pending runtime detail to show
|
|
99
|
+
- `[16:32:39]` when the runtime owns scheduled work and can show the next countdown
|
|
100
|
+
|
|
101
|
+
Extensions may prefix those states with their own compact name, such as `wakeup off` or `telegram on`.
|
|
100
102
|
|
|
101
103
|
## Interactive takeover
|
|
102
104
|
|
|
@@ -110,7 +112,7 @@ Start/connect commands should make singleton moves easy:
|
|
|
110
112
|
Takeover prompts should use the extension name as the dialog title, then the question, a blank line, and source/target lines:
|
|
111
113
|
|
|
112
114
|
```text
|
|
113
|
-
pi-
|
|
115
|
+
pi-singleton
|
|
114
116
|
move singleton lock here?
|
|
115
117
|
|
|
116
118
|
from: pid 2590864, cwd /old
|
|
@@ -123,7 +125,7 @@ The previous owner may use `fs.watch`, mtime polling, or an existing status/time
|
|
|
123
125
|
|
|
124
126
|
## Reset
|
|
125
127
|
|
|
126
|
-
Delete `~/.pi/agent/locks.json` to reset singleton runtime ownership for all participating extensions without deleting their configuration files
|
|
128
|
+
Delete `~/.pi/agent/locks.json` to reset singleton runtime ownership for all participating extensions without deleting their configuration files.
|
|
127
129
|
|
|
128
130
|
## Atomicity
|
|
129
131
|
|
|
@@ -112,7 +112,7 @@ For composed handlers, `output` selects the primary artifact after the compositi
|
|
|
112
112
|
|
|
113
113
|
For one-step `template` handlers, stdout remains the default result channel: the command should print the generated OGG/Opus path.
|
|
114
114
|
|
|
115
|
-
**Critical steps:** voice synthesis is a multi-step
|
|
115
|
+
**Critical steps:** voice synthesis is often a multi-step transform → TTS → conversion pipeline. The final audio conversion step is inherently critical — if it fails, the voice output is invalid. Mark conversion steps as `"critical": true` when a composed handler must abort after conversion failure instead of continuing to later non-critical steps. Use multiple matching `type: "voice"` handlers when you need provider or command fallbacks. See [Command Template Standard](./command-templates.md) for semantics.
|
|
116
116
|
|
|
117
117
|
## Buttons Markup
|
|
118
118
|
|
package/lib/menu-queue.ts
CHANGED
|
@@ -11,11 +11,14 @@ import * as Queue from "./queue.ts";
|
|
|
11
11
|
|
|
12
12
|
// --- Queue Menu ---
|
|
13
13
|
|
|
14
|
+
const QUEUE_ITEM_PROMPT_HTML_LIMIT = 3600;
|
|
15
|
+
const QUEUE_ITEM_PROMPT_TRUNCATION_SUFFIX = "\n… [truncated]";
|
|
14
16
|
type TelegramQueueMenuReplyMarkup = TelegramInlineKeyboardMarkup;
|
|
15
17
|
interface TelegramQueueMenuItem {
|
|
16
18
|
chatId: number;
|
|
17
19
|
replyToMessageId: number;
|
|
18
20
|
isPriority: boolean;
|
|
21
|
+
priorityEmoji?: string;
|
|
19
22
|
hasAttachments: boolean;
|
|
20
23
|
statusSummary: string;
|
|
21
24
|
promptText: string;
|
|
@@ -40,6 +43,7 @@ function toTelegramQueueMenuItems<Context>(
|
|
|
40
43
|
chatId: item.chatId,
|
|
41
44
|
replyToMessageId: item.replyToMessageId,
|
|
42
45
|
isPriority: item.queueLane === "priority",
|
|
46
|
+
priorityEmoji: item.kind === "prompt" ? item.priorityEmoji : undefined,
|
|
43
47
|
hasAttachments:
|
|
44
48
|
item.kind === "prompt" && item.queuedAttachments.length > 0,
|
|
45
49
|
statusSummary: item.statusSummary,
|
|
@@ -53,7 +57,11 @@ function buildTelegramQueueMenuReplyMarkup(
|
|
|
53
57
|
const backRow = [{ text: "⬆️ Main menu", callback_data: "menu:back" }];
|
|
54
58
|
if (items.length === 0) return { inline_keyboard: [backRow] };
|
|
55
59
|
const rows = items.map(function buildTelegramQueueMenuRow(item, index) {
|
|
56
|
-
const prefix = item.isPriority
|
|
60
|
+
const prefix = item.isPriority
|
|
61
|
+
? `${item.priorityEmoji ?? "⚡"} `
|
|
62
|
+
: item.hasAttachments
|
|
63
|
+
? "📎 "
|
|
64
|
+
: "";
|
|
57
65
|
const label = `${index + 1}. ${prefix}${item.statusSummary}`;
|
|
58
66
|
return [
|
|
59
67
|
{
|
|
@@ -82,21 +90,44 @@ function findTelegramQueueMenuItem(
|
|
|
82
90
|
return item.chatId === chatId && item.replyToMessageId === replyToMessageId;
|
|
83
91
|
});
|
|
84
92
|
}
|
|
93
|
+
function escapeTelegramQueueMenuHtmlChar(char: string): string {
|
|
94
|
+
if (char === "&") return "&";
|
|
95
|
+
if (char === "<") return "<";
|
|
96
|
+
if (char === ">") return ">";
|
|
97
|
+
return char;
|
|
98
|
+
}
|
|
85
99
|
function escapeTelegramQueueMenuHtml(text: string): string {
|
|
86
|
-
return text
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
100
|
+
return Array.from(text).map(escapeTelegramQueueMenuHtmlChar).join("");
|
|
101
|
+
}
|
|
102
|
+
function escapeTelegramQueueMenuHtmlPreview(text: string): string {
|
|
103
|
+
const suffix = escapeTelegramQueueMenuHtml(QUEUE_ITEM_PROMPT_TRUNCATION_SUFFIX);
|
|
104
|
+
let escaped = "";
|
|
105
|
+
let truncated = false;
|
|
106
|
+
for (const char of text) {
|
|
107
|
+
const next = escapeTelegramQueueMenuHtmlChar(char);
|
|
108
|
+
if (
|
|
109
|
+
escaped.length + next.length + suffix.length >
|
|
110
|
+
QUEUE_ITEM_PROMPT_HTML_LIMIT
|
|
111
|
+
) {
|
|
112
|
+
truncated = true;
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
escaped += next;
|
|
116
|
+
}
|
|
117
|
+
return truncated ? escaped + suffix : escaped;
|
|
90
118
|
}
|
|
91
119
|
function getTelegramQueueMenuItemText(item: TelegramQueueMenuItem): string {
|
|
92
|
-
return
|
|
120
|
+
return `<pre>${escapeTelegramQueueMenuHtmlPreview(item.promptText)}</pre>`;
|
|
93
121
|
}
|
|
94
122
|
function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
95
123
|
chatId: number,
|
|
96
124
|
replyToMessageId: number,
|
|
97
125
|
isPriority: boolean,
|
|
126
|
+
priorityEmoji?: string,
|
|
98
127
|
): TelegramQueueMenuReplyMarkup {
|
|
99
|
-
const priorityLabel = isPriority
|
|
128
|
+
const priorityLabel = isPriority
|
|
129
|
+
? `🐢 Deprioritize ${priorityEmoji ?? "⚡"}`
|
|
130
|
+
: "⚡ Prioritize";
|
|
100
131
|
return {
|
|
101
132
|
inline_keyboard: [
|
|
102
133
|
[{ text: "⬆️ Back", callback_data: "queue:list" }],
|
|
@@ -108,8 +139,29 @@ function buildTelegramQueueItemSubmenuReplyMarkup(
|
|
|
108
139
|
],
|
|
109
140
|
[
|
|
110
141
|
{
|
|
111
|
-
text: "
|
|
112
|
-
callback_data: `queue:
|
|
142
|
+
text: "🗑 Delete",
|
|
143
|
+
callback_data: `queue:delete:${chatId}:${replyToMessageId}`,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
],
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
function buildTelegramQueueDeleteConfirmationReplyMarkup(
|
|
150
|
+
chatId: number,
|
|
151
|
+
replyToMessageId: number,
|
|
152
|
+
): TelegramQueueMenuReplyMarkup {
|
|
153
|
+
return {
|
|
154
|
+
inline_keyboard: [
|
|
155
|
+
[
|
|
156
|
+
{
|
|
157
|
+
text: "🗑 Yes, delete",
|
|
158
|
+
callback_data: `queue:confirm-delete:${chatId}:${replyToMessageId}`,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
[
|
|
162
|
+
{
|
|
163
|
+
text: "❌ No",
|
|
164
|
+
callback_data: `queue:keep:${chatId}:${replyToMessageId}`,
|
|
113
165
|
},
|
|
114
166
|
],
|
|
115
167
|
],
|
|
@@ -186,14 +238,38 @@ async function handleTelegramQueueMenuCallback<Context>(
|
|
|
186
238
|
);
|
|
187
239
|
return true;
|
|
188
240
|
}
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
191
|
-
await
|
|
241
|
+
const deleteMatch = data.match(/^queue:(?:delete|cancel):(\d+):(\d+)$/);
|
|
242
|
+
if (deleteMatch) {
|
|
243
|
+
await handleTelegramQueueMenuDeleteRequest(
|
|
192
244
|
callbackQueryId,
|
|
193
245
|
replyChatId,
|
|
194
246
|
replyMessageId,
|
|
195
|
-
Number(
|
|
196
|
-
Number(
|
|
247
|
+
Number(deleteMatch[1]),
|
|
248
|
+
Number(deleteMatch[2]),
|
|
249
|
+
deps,
|
|
250
|
+
);
|
|
251
|
+
return true;
|
|
252
|
+
}
|
|
253
|
+
const keepMatch = data.match(/^queue:keep:(\d+):(\d+)$/);
|
|
254
|
+
if (keepMatch) {
|
|
255
|
+
await handleTelegramQueueMenuKeep(
|
|
256
|
+
callbackQueryId,
|
|
257
|
+
replyChatId,
|
|
258
|
+
replyMessageId,
|
|
259
|
+
Number(keepMatch[1]),
|
|
260
|
+
Number(keepMatch[2]),
|
|
261
|
+
deps,
|
|
262
|
+
);
|
|
263
|
+
return true;
|
|
264
|
+
}
|
|
265
|
+
const confirmDeleteMatch = data.match(/^queue:confirm-delete:(\d+):(\d+)$/);
|
|
266
|
+
if (confirmDeleteMatch) {
|
|
267
|
+
await handleTelegramQueueMenuConfirmDelete(
|
|
268
|
+
callbackQueryId,
|
|
269
|
+
replyChatId,
|
|
270
|
+
replyMessageId,
|
|
271
|
+
Number(confirmDeleteMatch[1]),
|
|
272
|
+
Number(confirmDeleteMatch[2]),
|
|
197
273
|
ctx,
|
|
198
274
|
deps,
|
|
199
275
|
);
|
|
@@ -204,8 +280,8 @@ async function handleTelegramQueueMenuCallback<Context>(
|
|
|
204
280
|
function getTelegramQueueMenuListText(
|
|
205
281
|
items: readonly TelegramQueueMenuItem[],
|
|
206
282
|
): string {
|
|
207
|
-
if (items.length === 0) return "<b
|
|
208
|
-
return "<b
|
|
283
|
+
if (items.length === 0) return "<b>⏳ Queue is empty.</b>";
|
|
284
|
+
return "<b>⏳ Queue:</b>";
|
|
209
285
|
}
|
|
210
286
|
async function updateTelegramQueueMenuList<Context>(
|
|
211
287
|
callbackQueryId: string,
|
|
@@ -258,7 +334,12 @@ async function handleTelegramQueueMenuPick<Context>(
|
|
|
258
334
|
replyChatId,
|
|
259
335
|
replyMessageId,
|
|
260
336
|
getTelegramQueueMenuItemText(item),
|
|
261
|
-
buildTelegramQueueItemSubmenuReplyMarkup(
|
|
337
|
+
buildTelegramQueueItemSubmenuReplyMarkup(
|
|
338
|
+
chatId,
|
|
339
|
+
msgId,
|
|
340
|
+
item.isPriority,
|
|
341
|
+
item.priorityEmoji,
|
|
342
|
+
),
|
|
262
343
|
);
|
|
263
344
|
await deps.answerCallbackQuery(callbackQueryId);
|
|
264
345
|
}
|
|
@@ -288,14 +369,74 @@ async function handleTelegramQueueMenuPriority<Context>(
|
|
|
288
369
|
replyChatId,
|
|
289
370
|
replyMessageId,
|
|
290
371
|
getTelegramQueueMenuItemText(item),
|
|
291
|
-
buildTelegramQueueItemSubmenuReplyMarkup(
|
|
372
|
+
buildTelegramQueueItemSubmenuReplyMarkup(
|
|
373
|
+
chatId,
|
|
374
|
+
msgId,
|
|
375
|
+
newPriority,
|
|
376
|
+
updated?.priorityEmoji ?? item.priorityEmoji,
|
|
377
|
+
),
|
|
292
378
|
);
|
|
293
379
|
await deps.answerCallbackQuery(
|
|
294
380
|
callbackQueryId,
|
|
295
381
|
newPriority ? "Prioritized." : "Deprioritized.",
|
|
296
382
|
);
|
|
297
383
|
}
|
|
298
|
-
async function
|
|
384
|
+
async function handleTelegramQueueMenuDeleteRequest<Context>(
|
|
385
|
+
callbackQueryId: string,
|
|
386
|
+
replyChatId: number,
|
|
387
|
+
replyMessageId: number,
|
|
388
|
+
chatId: number,
|
|
389
|
+
msgId: number,
|
|
390
|
+
deps: TelegramQueueMenuCallbackDeps<Context>,
|
|
391
|
+
): Promise<void> {
|
|
392
|
+
const item = deps.findItem(chatId, msgId);
|
|
393
|
+
if (!item) {
|
|
394
|
+
return refreshStaleTelegramQueueMenuItem(
|
|
395
|
+
callbackQueryId,
|
|
396
|
+
replyChatId,
|
|
397
|
+
replyMessageId,
|
|
398
|
+
deps,
|
|
399
|
+
);
|
|
400
|
+
}
|
|
401
|
+
await deps.updateQueueMessage(
|
|
402
|
+
replyChatId,
|
|
403
|
+
replyMessageId,
|
|
404
|
+
"<b>Delete this queued prompt?</b>",
|
|
405
|
+
buildTelegramQueueDeleteConfirmationReplyMarkup(chatId, msgId),
|
|
406
|
+
);
|
|
407
|
+
await deps.answerCallbackQuery(callbackQueryId);
|
|
408
|
+
}
|
|
409
|
+
async function handleTelegramQueueMenuKeep<Context>(
|
|
410
|
+
callbackQueryId: string,
|
|
411
|
+
replyChatId: number,
|
|
412
|
+
replyMessageId: number,
|
|
413
|
+
chatId: number,
|
|
414
|
+
msgId: number,
|
|
415
|
+
deps: TelegramQueueMenuCallbackDeps<Context>,
|
|
416
|
+
): Promise<void> {
|
|
417
|
+
const item = deps.findItem(chatId, msgId);
|
|
418
|
+
if (!item) {
|
|
419
|
+
return refreshStaleTelegramQueueMenuItem(
|
|
420
|
+
callbackQueryId,
|
|
421
|
+
replyChatId,
|
|
422
|
+
replyMessageId,
|
|
423
|
+
deps,
|
|
424
|
+
);
|
|
425
|
+
}
|
|
426
|
+
await deps.updateQueueMessage(
|
|
427
|
+
replyChatId,
|
|
428
|
+
replyMessageId,
|
|
429
|
+
getTelegramQueueMenuItemText(item),
|
|
430
|
+
buildTelegramQueueItemSubmenuReplyMarkup(
|
|
431
|
+
chatId,
|
|
432
|
+
msgId,
|
|
433
|
+
item.isPriority,
|
|
434
|
+
item.priorityEmoji,
|
|
435
|
+
),
|
|
436
|
+
);
|
|
437
|
+
await deps.answerCallbackQuery(callbackQueryId, "Kept in queue.");
|
|
438
|
+
}
|
|
439
|
+
async function handleTelegramQueueMenuConfirmDelete<Context>(
|
|
299
440
|
callbackQueryId: string,
|
|
300
441
|
replyChatId: number,
|
|
301
442
|
replyMessageId: number,
|
|
@@ -311,7 +452,7 @@ async function handleTelegramQueueMenuCancel<Context>(
|
|
|
311
452
|
replyChatId,
|
|
312
453
|
replyMessageId,
|
|
313
454
|
deps,
|
|
314
|
-
removed ? "
|
|
455
|
+
removed ? "Deleted from queue." : "Item not found.",
|
|
315
456
|
);
|
|
316
457
|
}
|
|
317
458
|
|
package/lib/menu-status.ts
CHANGED
package/lib/outbound-handlers.ts
CHANGED
|
@@ -650,7 +650,7 @@ async function generateTelegramVoiceReplyFileWithHandler(
|
|
|
650
650
|
const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
|
|
651
651
|
if (steps.length > 0) {
|
|
652
652
|
const startedAt = Date.now();
|
|
653
|
-
let stdout =
|
|
653
|
+
let stdout = text;
|
|
654
654
|
for (const [index, step] of steps.entries()) {
|
|
655
655
|
try {
|
|
656
656
|
const result = await runVoiceReplyCommand(
|
|
@@ -665,7 +665,7 @@ async function generateTelegramVoiceReplyFileWithHandler(
|
|
|
665
665
|
startedAt,
|
|
666
666
|
),
|
|
667
667
|
execCommand: options.execCommand,
|
|
668
|
-
|
|
668
|
+
stdin: stdout,
|
|
669
669
|
},
|
|
670
670
|
);
|
|
671
671
|
stdout = result.stdout;
|
package/lib/queue.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface PendingTelegramTurn extends TelegramQueueItemBase {
|
|
|
78
78
|
queuedAttachments: QueuedAttachment[];
|
|
79
79
|
content: TelegramPromptContent[];
|
|
80
80
|
historyText: string;
|
|
81
|
+
priorityEmoji?: string;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
export interface PendingTelegramControlItem<
|
|
@@ -304,6 +305,7 @@ export function clearTelegramQueuePromptPriority<TContext = unknown>(
|
|
|
304
305
|
...item,
|
|
305
306
|
queueLane: "default" as const,
|
|
306
307
|
laneOrder: item.queueOrder,
|
|
308
|
+
priorityEmoji: undefined,
|
|
307
309
|
};
|
|
308
310
|
});
|
|
309
311
|
return { items: nextItems, changed };
|
|
@@ -313,6 +315,7 @@ export function prioritizeTelegramQueuePrompt<TContext = unknown>(
|
|
|
313
315
|
items: TelegramQueueItem<TContext>[],
|
|
314
316
|
messageId: number,
|
|
315
317
|
laneOrder: number,
|
|
318
|
+
priorityEmoji = "⚡",
|
|
316
319
|
): { items: TelegramQueueItem<TContext>[]; changed: boolean } {
|
|
317
320
|
let changed = false;
|
|
318
321
|
const nextItems = items.map((item) => {
|
|
@@ -327,6 +330,7 @@ export function prioritizeTelegramQueuePrompt<TContext = unknown>(
|
|
|
327
330
|
...item,
|
|
328
331
|
queueLane: "priority" as const,
|
|
329
332
|
laneOrder,
|
|
333
|
+
priorityEmoji,
|
|
330
334
|
};
|
|
331
335
|
});
|
|
332
336
|
return { items: nextItems, changed };
|
|
@@ -353,7 +357,7 @@ function formatTelegramQueueItemStatusSummary<TContext = unknown>(
|
|
|
353
357
|
item: TelegramQueueItem<TContext>,
|
|
354
358
|
): string {
|
|
355
359
|
if (item.queueLane === "priority") {
|
|
356
|
-
return
|
|
360
|
+
return `${item.kind === "prompt" ? item.priorityEmoji ?? "⚡" : "⚡"} ${item.statusSummary}`;
|
|
357
361
|
}
|
|
358
362
|
return item.statusSummary;
|
|
359
363
|
}
|
|
@@ -1161,7 +1165,11 @@ export interface TelegramQueueMutationController<TContext> {
|
|
|
1161
1165
|
clear: (ctx: TContext) => number;
|
|
1162
1166
|
removeByMessageIds: (messageIds: number[], ctx: TContext) => number;
|
|
1163
1167
|
clearPriorityByMessageId: (messageId: number, ctx: TContext) => boolean;
|
|
1164
|
-
prioritizeByMessageId: (
|
|
1168
|
+
prioritizeByMessageId: (
|
|
1169
|
+
messageId: number,
|
|
1170
|
+
ctx: TContext,
|
|
1171
|
+
priorityEmoji?: string,
|
|
1172
|
+
) => boolean;
|
|
1165
1173
|
}
|
|
1166
1174
|
|
|
1167
1175
|
export interface TelegramControlQueueControllerDeps<TContext> {
|
|
@@ -1375,8 +1383,12 @@ export function createTelegramQueueMutationController<TContext>(
|
|
|
1375
1383
|
),
|
|
1376
1384
|
clearPriorityByMessageId: (messageId, ctx) =>
|
|
1377
1385
|
clearTelegramQueuePromptPriorityRuntime(messageId, buildRuntimeDeps(ctx)),
|
|
1378
|
-
prioritizeByMessageId: (messageId, ctx) =>
|
|
1379
|
-
prioritizeTelegramQueuePromptRuntime(
|
|
1386
|
+
prioritizeByMessageId: (messageId, ctx, priorityEmoji) =>
|
|
1387
|
+
prioritizeTelegramQueuePromptRuntime(
|
|
1388
|
+
messageId,
|
|
1389
|
+
buildRuntimeDeps(ctx),
|
|
1390
|
+
priorityEmoji,
|
|
1391
|
+
),
|
|
1380
1392
|
};
|
|
1381
1393
|
}
|
|
1382
1394
|
|
|
@@ -1438,6 +1450,7 @@ export function clearTelegramQueuePromptPriorityRuntime<TContext>(
|
|
|
1438
1450
|
export function prioritizeTelegramQueuePromptRuntime<TContext>(
|
|
1439
1451
|
messageId: number,
|
|
1440
1452
|
deps: TelegramQueueMutationRuntimeDeps<TContext>,
|
|
1453
|
+
priorityEmoji?: string,
|
|
1441
1454
|
): boolean {
|
|
1442
1455
|
const nextPriorityReactionOrder = deps.getNextPriorityReactionOrder?.();
|
|
1443
1456
|
if (nextPriorityReactionOrder === undefined) return false;
|
|
@@ -1445,6 +1458,7 @@ export function prioritizeTelegramQueuePromptRuntime<TContext>(
|
|
|
1445
1458
|
deps.getQueuedItems(),
|
|
1446
1459
|
messageId,
|
|
1447
1460
|
nextPriorityReactionOrder,
|
|
1461
|
+
priorityEmoji,
|
|
1448
1462
|
);
|
|
1449
1463
|
if (!changed) return false;
|
|
1450
1464
|
deps.setQueuedItems(items);
|
package/lib/updates.ts
CHANGED
|
@@ -26,18 +26,23 @@ export type TelegramReactionType =
|
|
|
26
26
|
| TelegramReactionTypeEmoji
|
|
27
27
|
| TelegramReactionTypeNonEmoji;
|
|
28
28
|
|
|
29
|
-
export const
|
|
30
|
-
"👍",
|
|
31
|
-
"⚡",
|
|
32
|
-
"❤",
|
|
33
|
-
"🕊",
|
|
29
|
+
export const TELEGRAM_PRIORITY_REACTIONS = [
|
|
30
|
+
{ id: 10, name: "like", emoji: "👍" },
|
|
31
|
+
{ id: 11, name: "lightning", emoji: "⚡" },
|
|
32
|
+
{ id: 12, name: "heart", emoji: "❤" },
|
|
33
|
+
{ id: 13, name: "dove", emoji: "🕊" },
|
|
34
34
|
] as const;
|
|
35
|
-
export const
|
|
36
|
-
"👎",
|
|
37
|
-
"👻",
|
|
38
|
-
"💔",
|
|
39
|
-
"💩",
|
|
35
|
+
export const TELEGRAM_REMOVAL_REACTIONS = [
|
|
36
|
+
{ id: 20, name: "dislike", emoji: "👎" },
|
|
37
|
+
{ id: 21, name: "ghost", emoji: "👻" },
|
|
38
|
+
{ id: 22, name: "broken-heart", emoji: "💔" },
|
|
39
|
+
{ id: 23, name: "poop", emoji: "💩" },
|
|
40
40
|
] as const;
|
|
41
|
+
export const TELEGRAM_PRIORITY_REACTION_EMOJIS =
|
|
42
|
+
TELEGRAM_PRIORITY_REACTIONS.map((reaction) => reaction.emoji);
|
|
43
|
+
export const TELEGRAM_REMOVAL_REACTION_EMOJIS = TELEGRAM_REMOVAL_REACTIONS.map(
|
|
44
|
+
(reaction) => reaction.emoji,
|
|
45
|
+
);
|
|
41
46
|
|
|
42
47
|
export interface TelegramUpdateDeletion {
|
|
43
48
|
deleted_business_messages?: { message_ids?: unknown };
|
|
@@ -71,15 +76,22 @@ function hasAnyTelegramReactionEmoji(
|
|
|
71
76
|
return candidates.some((emoji) => emojis.has(emoji));
|
|
72
77
|
}
|
|
73
78
|
|
|
74
|
-
function
|
|
79
|
+
function getAddedTelegramReactionEmoji(
|
|
75
80
|
oldEmojis: Set<string>,
|
|
76
81
|
newEmojis: Set<string>,
|
|
77
82
|
candidates: readonly string[],
|
|
78
|
-
):
|
|
79
|
-
return candidates.
|
|
83
|
+
): string | undefined {
|
|
84
|
+
return candidates.find(
|
|
80
85
|
(emoji) => !oldEmojis.has(emoji) && newEmojis.has(emoji),
|
|
81
86
|
);
|
|
82
87
|
}
|
|
88
|
+
function hasAddedTelegramReactionEmoji(
|
|
89
|
+
oldEmojis: Set<string>,
|
|
90
|
+
newEmojis: Set<string>,
|
|
91
|
+
candidates: readonly string[],
|
|
92
|
+
): boolean {
|
|
93
|
+
return !!getAddedTelegramReactionEmoji(oldEmojis, newEmojis, candidates);
|
|
94
|
+
}
|
|
83
95
|
|
|
84
96
|
export function extractDeletedTelegramMessageIds(
|
|
85
97
|
update: TelegramUpdateDeletion,
|
|
@@ -410,6 +422,7 @@ export interface TelegramUpdateRuntimeControllerDeps<
|
|
|
410
422
|
prioritizeQueuedTelegramTurnByMessageId: (
|
|
411
423
|
messageId: number,
|
|
412
424
|
ctx: TContext,
|
|
425
|
+
priorityEmoji?: string,
|
|
413
426
|
) => boolean;
|
|
414
427
|
pairTelegramUserIfNeeded: (userId: number, ctx: TContext) => Promise<boolean>;
|
|
415
428
|
answerCallbackQuery: (
|
|
@@ -592,6 +605,7 @@ export interface AuthorizedTelegramReactionUpdateDeps<TContext> {
|
|
|
592
605
|
prioritizeQueuedTelegramTurnByMessageId: (
|
|
593
606
|
messageId: number,
|
|
594
607
|
ctx: TContext,
|
|
608
|
+
priorityEmoji?: string,
|
|
595
609
|
) => boolean;
|
|
596
610
|
}
|
|
597
611
|
|
|
@@ -638,17 +652,16 @@ export async function handleAuthorizedTelegramReactionUpdate<TContext>(
|
|
|
638
652
|
deps.ctx,
|
|
639
653
|
);
|
|
640
654
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
)
|
|
648
|
-
return;
|
|
655
|
+
const addedPriorityEmoji = getAddedTelegramReactionEmoji(
|
|
656
|
+
oldEmojis,
|
|
657
|
+
newEmojis,
|
|
658
|
+
TELEGRAM_PRIORITY_REACTION_EMOJIS,
|
|
659
|
+
);
|
|
660
|
+
if (!addedPriorityEmoji) return;
|
|
649
661
|
deps.prioritizeQueuedTelegramTurnByMessageId(
|
|
650
662
|
reactionUpdate.message_id,
|
|
651
663
|
deps.ctx,
|
|
664
|
+
addedPriorityEmoji,
|
|
652
665
|
);
|
|
653
666
|
}
|
|
654
667
|
|