@llblab/pi-telegram 0.12.0 → 0.13.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/AGENTS.md +7 -6
- package/BACKLOG.md +26 -1
- package/CHANGELOG.md +31 -1
- package/README.md +3 -33
- package/docs/README.md +1 -1
- package/docs/architecture.md +2 -1
- package/docs/command-templates.md +18 -16
- package/docs/inbound.md +2 -2
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +158 -4
- package/docs/sections.md +5 -5
- package/docs/voice.md +13 -8
- package/index.ts +4 -4
- package/lib/bindings.ts +3 -4
- package/lib/command-templates.ts +249 -60
- package/lib/config.ts +1 -2
- package/lib/inbound.ts +26 -17
- package/lib/lifecycle.ts +25 -8
- package/lib/locks.ts +4 -1
- package/lib/outbound-buttons.ts +226 -0
- package/lib/outbound-markup.ts +357 -0
- package/lib/outbound-voice.ts +263 -0
- package/lib/outbound.ts +87 -852
- package/lib/preview.ts +2 -1
- package/lib/queue.ts +3 -0
- package/lib/rendering.ts +20 -1
- package/lib/replies.ts +4 -1
- package/lib/routing.ts +2 -2
- package/lib/status.ts +13 -0
- package/lib/{api.ts → telegram-api.ts} +4 -4
- package/lib/text-groups.ts +3 -2
- package/lib/voice.ts +35 -8
- package/package.json +12 -12
package/lib/preview.ts
CHANGED
|
@@ -9,7 +9,7 @@ import type {
|
|
|
9
9
|
TelegramReplyParameters,
|
|
10
10
|
TelegramSendMessageBody,
|
|
11
11
|
TelegramSentMessage,
|
|
12
|
-
} from "./api.ts";
|
|
12
|
+
} from "./telegram-api.ts";
|
|
13
13
|
import {
|
|
14
14
|
buildTelegramPreviewSnapshot,
|
|
15
15
|
MAX_MESSAGE_LENGTH,
|
|
@@ -345,6 +345,7 @@ export function createTelegramPreviewControllerRuntime<
|
|
|
345
345
|
maxDraftId: deps.maxDraftId,
|
|
346
346
|
setTimer: deps.setTimer,
|
|
347
347
|
clearTimer: deps.clearTimer,
|
|
348
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
348
349
|
});
|
|
349
350
|
}
|
|
350
351
|
|
package/lib/queue.ts
CHANGED
|
@@ -44,6 +44,9 @@ export interface TelegramQueueLaneContract {
|
|
|
44
44
|
|
|
45
45
|
export const TELEGRAM_QUEUE_LANE_CONTRACTS: readonly TelegramQueueLaneContract[] =
|
|
46
46
|
[
|
|
47
|
+
// Control lane intentionally accepts both direct controls and resume prompts.
|
|
48
|
+
// Model-switch continuations need prompt semantics but must run before queued user work.
|
|
49
|
+
// Do not admit ordinary user prompts here without an explicit control-flow reason.
|
|
47
50
|
{
|
|
48
51
|
lane: "control",
|
|
49
52
|
admissionMode: "control-queue",
|
package/lib/rendering.ts
CHANGED
|
@@ -843,7 +843,7 @@ function renderDelimitedInlineStyle(
|
|
|
843
843
|
): string {
|
|
844
844
|
const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
845
845
|
const pattern = new RegExp(
|
|
846
|
-
`(^|[^\\p{L}\\p{N}\\\\])(${escapedDelimiter})(?=\\S)(
|
|
846
|
+
`(^|[^\\p{L}\\p{N}\\\\])(${escapedDelimiter})(?=\\S)([\\s\\S]+?)(?<=\\S)\\2(?=[^\\p{L}\\p{N}]|$)`,
|
|
847
847
|
"gu",
|
|
848
848
|
);
|
|
849
849
|
return text.replace(
|
|
@@ -1004,9 +1004,28 @@ function renderMarkdownTextPiece(piece: string): string {
|
|
|
1004
1004
|
return renderInlineMarkdown(piece);
|
|
1005
1005
|
}
|
|
1006
1006
|
|
|
1007
|
+
function isPlainInlineMarkdownLine(line: string): boolean {
|
|
1008
|
+
if (line.trim().length === 0) return false;
|
|
1009
|
+
return (
|
|
1010
|
+
matchMarkdownHeadingLine(line) === null &&
|
|
1011
|
+
!/^(\s*)([-*+]|\d+\.)\s+\[([ xX])\]\s+(.+)$/.test(line) &&
|
|
1012
|
+
!/^(\s*)[-*+]\s+(.+)$/.test(line) &&
|
|
1013
|
+
!/^(\s*)(\d+)\.\s+(.+)$/.test(line) &&
|
|
1014
|
+
!/^>\s?(.+)$/.test(line) &&
|
|
1015
|
+
!/^([-*_]\s*){3,}$/.test(line.trim())
|
|
1016
|
+
);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1007
1019
|
function renderMarkdownTextLines(block: string): string[] {
|
|
1008
1020
|
const rendered: string[] = [];
|
|
1009
1021
|
const lines = block.split("\n");
|
|
1022
|
+
const nonBlankLines = lines.filter((line) => line.trim().length > 0);
|
|
1023
|
+
if (
|
|
1024
|
+
nonBlankLines.length > 1 &&
|
|
1025
|
+
nonBlankLines.every(isPlainInlineMarkdownLine)
|
|
1026
|
+
) {
|
|
1027
|
+
return renderInlineMarkdown(nonBlankLines.join("\n")).split("\n");
|
|
1028
|
+
}
|
|
1010
1029
|
for (const line of lines) {
|
|
1011
1030
|
if (line.trim().length === 0) continue;
|
|
1012
1031
|
for (const piece of splitPlainMarkdownLine(line)) {
|
package/lib/replies.ts
CHANGED
|
@@ -4,7 +4,10 @@
|
|
|
4
4
|
* Owns rendered-message delivery, reply transport wiring, and plain or markdown final replies
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type {
|
|
8
|
+
TelegramReplyParameters,
|
|
9
|
+
TelegramSentMessage,
|
|
10
|
+
} from "./telegram-api.ts";
|
|
8
11
|
import {
|
|
9
12
|
renderTelegramMessage,
|
|
10
13
|
type TelegramRenderedChunk,
|
package/lib/routing.ts
CHANGED
|
@@ -351,8 +351,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
351
351
|
const turn = await promptTurnBuilder([continueMessage], [], ctx);
|
|
352
352
|
const continueTurn = {
|
|
353
353
|
...turn,
|
|
354
|
-
queueLane: "
|
|
355
|
-
laneOrder:
|
|
354
|
+
queueLane: "control" as const,
|
|
355
|
+
laneOrder: deps.bridgeRuntime.queue.allocateControlOrder(),
|
|
356
356
|
statusSummary: "continue",
|
|
357
357
|
};
|
|
358
358
|
deps.queueMutationRuntime.append(continueTurn, ctx);
|
package/lib/status.ts
CHANGED
|
@@ -289,12 +289,25 @@ function formatTelegramRuntimeEvent(event: TelegramRuntimeEvent): string {
|
|
|
289
289
|
return `${new Date(event.at).toISOString()} ${formatTelegramRuntimeEventSummary(event)}`;
|
|
290
290
|
}
|
|
291
291
|
|
|
292
|
+
function buildTelegramRuntimeEventSummary(events: TelegramRuntimeEvent[]): string {
|
|
293
|
+
const counts = new Map<string, number>();
|
|
294
|
+
for (const event of events) {
|
|
295
|
+
const category = formatTelegramRuntimeEventCategory(event);
|
|
296
|
+
counts.set(category, (counts.get(category) ?? 0) + 1);
|
|
297
|
+
}
|
|
298
|
+
return Array.from(counts.entries())
|
|
299
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
300
|
+
.map(([category, count]) => `${category}=${count}`)
|
|
301
|
+
.join(", ");
|
|
302
|
+
}
|
|
303
|
+
|
|
292
304
|
export function buildTelegramRuntimeEventLines(
|
|
293
305
|
events: TelegramRuntimeEvent[],
|
|
294
306
|
): string[] {
|
|
295
307
|
if (events.length === 0) return ["recent runtime events: none"];
|
|
296
308
|
return [
|
|
297
309
|
"recent runtime events:",
|
|
310
|
+
`- summary: ${buildTelegramRuntimeEventSummary(events)}`,
|
|
298
311
|
...events
|
|
299
312
|
.slice()
|
|
300
313
|
.reverse()
|
|
@@ -416,13 +416,13 @@ async function writeTelegramDownloadResponse(
|
|
|
416
416
|
if (!response.body) {
|
|
417
417
|
const buffer = Buffer.from(await response.arrayBuffer());
|
|
418
418
|
assertTelegramFileSizeWithinLimit(buffer.byteLength, maxFileSizeBytes);
|
|
419
|
-
await writeFile(targetPath, buffer);
|
|
419
|
+
await writeFile(targetPath, buffer, { mode: 0o600 });
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
422
422
|
await pipeline(
|
|
423
423
|
Readable.from(response.body, { objectMode: false }),
|
|
424
424
|
createTelegramDownloadLimitTransform(maxFileSizeBytes),
|
|
425
|
-
createWriteStream(targetPath),
|
|
425
|
+
createWriteStream(targetPath, { mode: 0o600 }),
|
|
426
426
|
);
|
|
427
427
|
}
|
|
428
428
|
|
|
@@ -536,7 +536,7 @@ export async function prepareTelegramTempDir(
|
|
|
536
536
|
tempDir: string,
|
|
537
537
|
maxAgeMs: number,
|
|
538
538
|
): Promise<number> {
|
|
539
|
-
await mkdir(tempDir, { recursive: true });
|
|
539
|
+
await mkdir(tempDir, { recursive: true, mode: 0o700 });
|
|
540
540
|
return cleanupTelegramTempFiles(tempDir, maxAgeMs);
|
|
541
541
|
}
|
|
542
542
|
|
|
@@ -630,7 +630,7 @@ export async function downloadTelegramFile(
|
|
|
630
630
|
{ signal: options?.signal },
|
|
631
631
|
);
|
|
632
632
|
assertTelegramFileSizeWithinLimit(file.file_size, options?.maxFileSizeBytes);
|
|
633
|
-
await mkdir(tempDir, { recursive: true });
|
|
633
|
+
await mkdir(tempDir, { recursive: true, mode: 0o700 });
|
|
634
634
|
const targetPath = join(
|
|
635
635
|
tempDir,
|
|
636
636
|
`${randomUUID()}-${sanitizeFileName(suggestedName)}`,
|
package/lib/text-groups.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
const TELEGRAM_TEXT_GROUP_DEBOUNCE_MS = 1000;
|
|
8
8
|
const TELEGRAM_TEXT_GROUP_MIN_SPLIT_LENGTH = 3600;
|
|
9
|
-
const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP =
|
|
9
|
+
const TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP = 12;
|
|
10
10
|
|
|
11
11
|
export interface TelegramTextGroupMessage {
|
|
12
12
|
message_id: number;
|
|
@@ -92,7 +92,8 @@ function canAppendTelegramTextGroupMessage<
|
|
|
92
92
|
return (
|
|
93
93
|
!!previous &&
|
|
94
94
|
message.message_id > previous.message_id &&
|
|
95
|
-
message.message_id <=
|
|
95
|
+
message.message_id <=
|
|
96
|
+
previous.message_id + TELEGRAM_TEXT_GROUP_MAX_MESSAGE_ID_GAP &&
|
|
96
97
|
text.length > 0 &&
|
|
97
98
|
!isTelegramTextGroupCommand(text)
|
|
98
99
|
);
|
package/lib/voice.ts
CHANGED
|
@@ -23,6 +23,21 @@ const VOICE_SYNTHESIS_PROVIDER_REGISTRY_KEY =
|
|
|
23
23
|
const VOICE_TRANSCRIPTION_PROVIDER_REGISTRY_KEY =
|
|
24
24
|
"__piTelegramVoiceTranscriptionProviders__";
|
|
25
25
|
|
|
26
|
+
let nextGeneratedVoiceSynthesisProviderId = 0;
|
|
27
|
+
let nextGeneratedVoiceTranscriptionProviderId = 0;
|
|
28
|
+
|
|
29
|
+
function getNextAvailableProviderId<T>(
|
|
30
|
+
registry: Map<string, T>,
|
|
31
|
+
prefix: string,
|
|
32
|
+
nextId: () => number,
|
|
33
|
+
): string {
|
|
34
|
+
let id: string;
|
|
35
|
+
do {
|
|
36
|
+
id = `${prefix}-${nextId()}`;
|
|
37
|
+
} while (registry.has(id));
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
|
|
26
41
|
export type TelegramVoiceReplyMode = "mirror" | "always" | "manual";
|
|
27
42
|
|
|
28
43
|
export type TelegramVoiceSynthesisProviderResult =
|
|
@@ -106,7 +121,13 @@ export function registerTelegramVoiceSynthesisProvider(
|
|
|
106
121
|
options?: { id?: string },
|
|
107
122
|
): () => void {
|
|
108
123
|
const registry = getOrCreateVoiceSynthesisProviderRegistry();
|
|
109
|
-
const id =
|
|
124
|
+
const id =
|
|
125
|
+
options?.id ??
|
|
126
|
+
getNextAvailableProviderId(
|
|
127
|
+
registry,
|
|
128
|
+
"voice-synthesis-provider",
|
|
129
|
+
() => nextGeneratedVoiceSynthesisProviderId++,
|
|
130
|
+
);
|
|
110
131
|
const normalized =
|
|
111
132
|
typeof provider === "function"
|
|
112
133
|
? (Object.assign(
|
|
@@ -123,7 +144,7 @@ export function registerTelegramVoiceSynthesisProvider(
|
|
|
123
144
|
: provider;
|
|
124
145
|
registry.set(id, normalized);
|
|
125
146
|
return () => {
|
|
126
|
-
registry.delete(id);
|
|
147
|
+
if (registry.get(id) === normalized) registry.delete(id);
|
|
127
148
|
};
|
|
128
149
|
}
|
|
129
150
|
|
|
@@ -168,10 +189,16 @@ export function registerTelegramVoiceTranscriptionProvider(
|
|
|
168
189
|
options?: { id?: string },
|
|
169
190
|
): () => void {
|
|
170
191
|
const registry = getOrCreateVoiceTranscriptionProviderRegistry();
|
|
171
|
-
const id =
|
|
192
|
+
const id =
|
|
193
|
+
options?.id ??
|
|
194
|
+
getNextAvailableProviderId(
|
|
195
|
+
registry,
|
|
196
|
+
"voice-transcription-provider",
|
|
197
|
+
() => nextGeneratedVoiceTranscriptionProviderId++,
|
|
198
|
+
);
|
|
172
199
|
registry.set(id, provider);
|
|
173
200
|
return () => {
|
|
174
|
-
registry.delete(id);
|
|
201
|
+
if (registry.get(id) === provider) registry.delete(id);
|
|
175
202
|
};
|
|
176
203
|
}
|
|
177
204
|
|
|
@@ -303,12 +330,12 @@ export function shouldSuppressPreviewForVoice(
|
|
|
303
330
|
return !!(turn?.voiceReplyPreferred || turn?.voiceReplyRequired);
|
|
304
331
|
}
|
|
305
332
|
|
|
306
|
-
// --- Outbound
|
|
333
|
+
// --- Outbound Markup Re-Exports ---
|
|
307
334
|
|
|
308
335
|
export {
|
|
336
|
+
normalizeMarkdownAfterVoiceExtraction,
|
|
309
337
|
planTelegramVoiceReply,
|
|
310
|
-
stripTelegramCommentMarkupForPreview,
|
|
311
338
|
stripTelegramCommentMarkupForDelivery,
|
|
339
|
+
stripTelegramCommentMarkupForPreview,
|
|
312
340
|
stripTelegramVoiceMarkupForPreview,
|
|
313
|
-
|
|
314
|
-
} from "./outbound.ts";
|
|
341
|
+
} from "./outbound-markup.ts";
|
package/package.json
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@llblab/pi-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
8
8
|
"description": "Telegram runtime adapter for π",
|
|
9
|
-
"type": "module",
|
|
10
|
-
"exports": {
|
|
11
|
-
".": "./index.ts",
|
|
12
|
-
"./inbound": "./api/inbound.ts",
|
|
13
|
-
"./outbound": "./api/outbound.ts",
|
|
14
|
-
"./updates": "./api/updates.ts",
|
|
15
|
-
"./sections": "./api/sections.ts",
|
|
16
|
-
"./voice": "./api/voice.ts",
|
|
17
|
-
"./keyboard": "./api/keyboard.ts"
|
|
18
|
-
},
|
|
19
9
|
"keywords": [
|
|
20
10
|
"pi-package",
|
|
21
11
|
"pi",
|
|
@@ -23,6 +13,7 @@
|
|
|
23
13
|
"bot",
|
|
24
14
|
"extension"
|
|
25
15
|
],
|
|
16
|
+
"type": "module",
|
|
26
17
|
"license": "MIT",
|
|
27
18
|
"repository": {
|
|
28
19
|
"type": "git",
|
|
@@ -33,7 +24,7 @@
|
|
|
33
24
|
"url": "https://github.com/llblab/pi-telegram/issues"
|
|
34
25
|
},
|
|
35
26
|
"engines": {
|
|
36
|
-
"node": ">=22.
|
|
27
|
+
"node": ">=22.19.0"
|
|
37
28
|
},
|
|
38
29
|
"scripts": {
|
|
39
30
|
"test": "node --experimental-strip-types --test tests/*.test.ts",
|
|
@@ -53,6 +44,15 @@
|
|
|
53
44
|
"docs/",
|
|
54
45
|
"screenshot.png"
|
|
55
46
|
],
|
|
47
|
+
"exports": {
|
|
48
|
+
".": "./index.ts",
|
|
49
|
+
"./inbound": "./api/inbound.ts",
|
|
50
|
+
"./outbound": "./api/outbound.ts",
|
|
51
|
+
"./updates": "./api/updates.ts",
|
|
52
|
+
"./sections": "./api/sections.ts",
|
|
53
|
+
"./voice": "./api/voice.ts",
|
|
54
|
+
"./keyboard": "./api/keyboard.ts"
|
|
55
|
+
},
|
|
56
56
|
"pi": {
|
|
57
57
|
"extensions": [
|
|
58
58
|
"./index.ts"
|