@llblab/pi-kit 0.2.0 → 0.3.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 +1 -0
- package/CHANGELOG.md +5 -0
- package/README.md +1 -1
- package/node_modules/@llblab/pi-telegram/CHANGELOG.md +14 -0
- package/node_modules/@llblab/pi-telegram/README.md +3 -3
- package/node_modules/@llblab/pi-telegram/docs/architecture.md +6 -6
- package/node_modules/@llblab/pi-telegram/docs/compact-matrix-literal.md +16 -14
- package/node_modules/@llblab/pi-telegram/docs/inbound.md +1 -1
- package/node_modules/@llblab/pi-telegram/docs/outbound.md +11 -20
- package/node_modules/@llblab/pi-telegram/docs/public-api.md +4 -4
- package/node_modules/@llblab/pi-telegram/lib/activity.ts +7 -0
- package/node_modules/@llblab/pi-telegram/lib/bindings.ts +3 -0
- package/node_modules/@llblab/pi-telegram/lib/commands.ts +16 -6
- package/node_modules/@llblab/pi-telegram/lib/media.ts +12 -2
- package/node_modules/@llblab/pi-telegram/lib/outbound-buttons.ts +9 -8
- package/node_modules/@llblab/pi-telegram/lib/outbound-markup.ts +212 -82
- package/node_modules/@llblab/pi-telegram/lib/routing.ts +78 -36
- package/node_modules/@llblab/pi-telegram/lib/status.ts +13 -4
- package/node_modules/@llblab/pi-telegram/lib/turns.ts +10 -1
- package/node_modules/@llblab/pi-telegram/lib/updates.ts +1 -1
- package/node_modules/@llblab/pi-telegram/package.json +1 -1
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/SKILL.md +61 -207
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/node_modules/@llblab/pi-telegram/skills/generated-control-surface/references/layout-and-state.md +35 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/SKILL.md +70 -110
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/configuration.md +15 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/node_modules/@llblab/pi-telegram/skills/telegram-bridge/references/diagnosis.md +14 -0
- package/package.json +2 -2
|
@@ -153,63 +153,153 @@ export function parseTopLevelTelegramComment(
|
|
|
153
153
|
};
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
function
|
|
156
|
+
function parseTolerantTelegramAttributes(
|
|
157
157
|
source: string,
|
|
158
|
+
names: readonly string[],
|
|
158
159
|
): Record<string, string> | undefined {
|
|
159
160
|
const attributes: Record<string, string> = {};
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const value = match[2].trim();
|
|
167
|
-
if (value) attributes[match[1]] = value;
|
|
168
|
-
offset = pattern.lastIndex;
|
|
161
|
+
const namePattern = names.join("|");
|
|
162
|
+
const pattern = new RegExp(
|
|
163
|
+
`\\b(${namePattern})\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s]+))`,
|
|
164
|
+
"gu",
|
|
165
|
+
);
|
|
166
|
+
for (const match of source.matchAll(pattern)) {
|
|
167
|
+
const value = (match[2] ?? match[3] ?? match[4] ?? "").trim();
|
|
168
|
+
if (value) attributes[match[1]!] = value;
|
|
169
169
|
}
|
|
170
170
|
return Object.keys(attributes).length > 0 ? attributes : undefined;
|
|
171
171
|
}
|
|
172
172
|
|
|
173
|
-
function getTelegramActionPayloadSource(
|
|
174
|
-
comment: TelegramTopLevelHtmlComment,
|
|
175
|
-
command: string,
|
|
176
|
-
): { source: string; hasBody: boolean } | undefined {
|
|
177
|
-
const parsed = parseTopLevelTelegramComment(comment, command);
|
|
178
|
-
if (!parsed || parsed.head.trimStart().startsWith(":")) return undefined;
|
|
179
|
-
const source = [parsed.head, parsed.body]
|
|
180
|
-
.filter((part): part is string => part !== undefined)
|
|
181
|
-
.join("\n")
|
|
182
|
-
.trim();
|
|
183
|
-
return source ? { source, hasBody: parsed.body !== undefined } : undefined;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
173
|
function isTelegramActionPayload(value: unknown): value is Record<string, unknown> {
|
|
187
174
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
188
175
|
}
|
|
189
176
|
|
|
177
|
+
function removeTelegramJsonTrailingCommas(source: string): string {
|
|
178
|
+
let normalized = "";
|
|
179
|
+
let inString = false;
|
|
180
|
+
let escaped = false;
|
|
181
|
+
for (let offset = 0; offset < source.length; offset += 1) {
|
|
182
|
+
const character = source[offset]!;
|
|
183
|
+
if (inString) {
|
|
184
|
+
normalized += character;
|
|
185
|
+
if (escaped) escaped = false;
|
|
186
|
+
else if (character === "\\") escaped = true;
|
|
187
|
+
else if (character === '"') inString = false;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (character === '"') {
|
|
191
|
+
inString = true;
|
|
192
|
+
normalized += character;
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (character === ",") {
|
|
196
|
+
let next = offset + 1;
|
|
197
|
+
while (/\s/u.test(source[next] ?? "")) next += 1;
|
|
198
|
+
if (source[next] === "}" || source[next] === "]") continue;
|
|
199
|
+
}
|
|
200
|
+
normalized += character;
|
|
201
|
+
}
|
|
202
|
+
return normalized;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function parseTelegramJsonObjectCandidate(
|
|
206
|
+
source: string,
|
|
207
|
+
): Record<string, unknown> | undefined {
|
|
208
|
+
const normalized = removeTelegramJsonTrailingCommas(source);
|
|
209
|
+
for (const candidate of normalized === source ? [source] : [source, normalized]) {
|
|
210
|
+
try {
|
|
211
|
+
const value: unknown = JSON.parse(candidate);
|
|
212
|
+
if (isTelegramActionPayload(value)) return value;
|
|
213
|
+
} catch {
|
|
214
|
+
// Try the bounded trailing-comma normalization before rejecting JSON.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return undefined;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function looksLikeTelegramNamedJsonObject(
|
|
221
|
+
source: string,
|
|
222
|
+
offset: number,
|
|
223
|
+
): boolean {
|
|
224
|
+
return /^\{\s*"(?:[^"\\]|\\.)*"\s*:/u.test(source.slice(offset));
|
|
225
|
+
}
|
|
226
|
+
|
|
190
227
|
export function parseTelegramActionPayload(
|
|
191
228
|
comment: TelegramTopLevelHtmlComment,
|
|
192
229
|
command: string,
|
|
193
230
|
): Record<string, unknown> | undefined {
|
|
194
|
-
|
|
195
|
-
if (!
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
231
|
+
let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
|
|
232
|
+
if (!content.startsWith(command)) return undefined;
|
|
233
|
+
content = content.slice(command.length);
|
|
234
|
+
let attributeEnvelope = content;
|
|
235
|
+
for (let offset = 0; offset < content.length; offset += 1) {
|
|
236
|
+
if (content[offset] !== "{" && content[offset] !== "[") continue;
|
|
237
|
+
if (
|
|
238
|
+
content[offset] === "[" &&
|
|
239
|
+
!isPlausibleTelegramMatrixStart(content, offset)
|
|
240
|
+
) {
|
|
241
|
+
const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
|
|
242
|
+
if (noiseEnd !== undefined) {
|
|
243
|
+
attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
|
|
244
|
+
offset = noiseEnd - 1;
|
|
245
|
+
}
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
if (content[offset] === "{") {
|
|
249
|
+
const parsed = parseTelegramAdaptiveActionPayloadRows(
|
|
250
|
+
content.slice(offset),
|
|
251
|
+
parseTelegramVoiceCompactActionPayload,
|
|
252
|
+
{ allowTrailing: true },
|
|
253
|
+
);
|
|
254
|
+
if (parsed) return parsed.rows[0]![0];
|
|
202
255
|
}
|
|
256
|
+
const end = findTelegramStructuredPayloadEnd(content, offset);
|
|
257
|
+
if (end === undefined) continue;
|
|
258
|
+
attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
|
|
259
|
+
offset = end - 1;
|
|
203
260
|
}
|
|
204
|
-
|
|
205
|
-
|
|
261
|
+
return parseTolerantTelegramAttributes(attributeEnvelope, [
|
|
262
|
+
"text",
|
|
263
|
+
"value",
|
|
264
|
+
"lang",
|
|
265
|
+
"rate",
|
|
266
|
+
]);
|
|
206
267
|
}
|
|
207
268
|
|
|
208
269
|
const TELEGRAM_COMPACT_ACTION_CONTROL_PATTERN = /[\u0000-\u001f\u007f-\u009f]/u;
|
|
209
270
|
|
|
271
|
+
type TelegramCompactActionPayloadParser = (
|
|
272
|
+
atoms: readonly string[],
|
|
273
|
+
) => Record<string, unknown> | undefined;
|
|
274
|
+
|
|
275
|
+
function parseTelegramButtonCompactActionPayload(
|
|
276
|
+
atoms: readonly string[],
|
|
277
|
+
): Record<string, unknown> | undefined {
|
|
278
|
+
const [label, prompt, selectedStyle] = atoms;
|
|
279
|
+
if (atoms.length === 1) return { value: label };
|
|
280
|
+
if (atoms.length === 2) return { label, prompt };
|
|
281
|
+
if (
|
|
282
|
+
selectedStyle !== "primary" &&
|
|
283
|
+
selectedStyle !== "success" &&
|
|
284
|
+
selectedStyle !== "danger"
|
|
285
|
+
) return undefined;
|
|
286
|
+
return { label, prompt, selected_style: selectedStyle };
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseTelegramVoiceCompactActionPayload(
|
|
290
|
+
atoms: readonly string[],
|
|
291
|
+
): Record<string, unknown> | undefined {
|
|
292
|
+
const [text, lang, rate] = atoms;
|
|
293
|
+
if (atoms.length === 1) return { text };
|
|
294
|
+
if (atoms.length === 2) return { text, lang };
|
|
295
|
+
return { text, lang, rate };
|
|
296
|
+
}
|
|
297
|
+
|
|
210
298
|
function parseTelegramAdaptiveActionPayloadRows(
|
|
211
299
|
source: string,
|
|
212
|
-
|
|
300
|
+
parseCompactPayload: TelegramCompactActionPayloadParser,
|
|
301
|
+
options: { allowTrailing?: boolean } = {},
|
|
302
|
+
): { rows: Record<string, unknown>[][]; end: number } | undefined {
|
|
213
303
|
let offset = 0;
|
|
214
304
|
const isStructuralWhitespace = (character: string | undefined): boolean =>
|
|
215
305
|
character === " " ||
|
|
@@ -224,7 +314,7 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
224
314
|
if (source[offset] !== ",") return true;
|
|
225
315
|
offset += 1;
|
|
226
316
|
skipWhitespace();
|
|
227
|
-
return source[offset] !== ","
|
|
317
|
+
return source[offset] !== ",";
|
|
228
318
|
};
|
|
229
319
|
const normalizeAtom = (value: string): string | undefined => {
|
|
230
320
|
const normalized = value.trim();
|
|
@@ -259,15 +349,7 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
259
349
|
normalizeAtom(atom.join("")),
|
|
260
350
|
);
|
|
261
351
|
if (atoms.some((atom) => atom === undefined)) return undefined;
|
|
262
|
-
|
|
263
|
-
if (atoms.length === 1) return { value: label };
|
|
264
|
-
if (atoms.length === 2) return { label, prompt };
|
|
265
|
-
if (
|
|
266
|
-
selectedStyle !== "primary" &&
|
|
267
|
-
selectedStyle !== "success" &&
|
|
268
|
-
selectedStyle !== "danger"
|
|
269
|
-
) return undefined;
|
|
270
|
-
return { label, prompt, selected_style: selectedStyle };
|
|
352
|
+
return parseCompactPayload(atoms as string[]);
|
|
271
353
|
}
|
|
272
354
|
atomSources.at(-1)!.push(character);
|
|
273
355
|
offset += 1;
|
|
@@ -304,14 +386,10 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
304
386
|
) return undefined;
|
|
305
387
|
if (stack.length > 0) continue;
|
|
306
388
|
const candidate = source.slice(start, index + 1);
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
return value;
|
|
312
|
-
} catch {
|
|
313
|
-
return undefined;
|
|
314
|
-
}
|
|
389
|
+
const value = parseTelegramJsonObjectCandidate(candidate);
|
|
390
|
+
if (!value) return undefined;
|
|
391
|
+
offset = index + 1;
|
|
392
|
+
return value;
|
|
315
393
|
}
|
|
316
394
|
return undefined;
|
|
317
395
|
};
|
|
@@ -320,6 +398,7 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
320
398
|
const jsonCell = parseJsonObjectCell();
|
|
321
399
|
if (jsonCell) return jsonCell;
|
|
322
400
|
offset = start;
|
|
401
|
+
if (looksLikeTelegramNamedJsonObject(source, start)) return undefined;
|
|
323
402
|
return parseCompactCell();
|
|
324
403
|
};
|
|
325
404
|
const parseRow = (): Record<string, unknown>[] | undefined => {
|
|
@@ -377,42 +456,92 @@ function parseTelegramAdaptiveActionPayloadRows(
|
|
|
377
456
|
}
|
|
378
457
|
if (!rows) return undefined;
|
|
379
458
|
skipWhitespace();
|
|
380
|
-
|
|
459
|
+
if (!options.allowTrailing && offset !== source.length) return undefined;
|
|
460
|
+
return { rows, end: offset };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function findTelegramStructuredPayloadEnd(
|
|
464
|
+
source: string,
|
|
465
|
+
start: number,
|
|
466
|
+
): number | undefined {
|
|
467
|
+
const stack: string[] = [source[start]!];
|
|
468
|
+
let inString = false;
|
|
469
|
+
let escaped = false;
|
|
470
|
+
for (let offset = start + 1; offset < source.length; offset += 1) {
|
|
471
|
+
const character = source[offset]!;
|
|
472
|
+
if (inString) {
|
|
473
|
+
if (escaped) escaped = false;
|
|
474
|
+
else if (character === "\\") escaped = true;
|
|
475
|
+
else if (character === '"') inString = false;
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
if (character === '"') {
|
|
479
|
+
inString = true;
|
|
480
|
+
continue;
|
|
481
|
+
}
|
|
482
|
+
if (character === "[" || character === "{") {
|
|
483
|
+
stack.push(character);
|
|
484
|
+
continue;
|
|
485
|
+
}
|
|
486
|
+
if (character !== "]" && character !== "}") continue;
|
|
487
|
+
const expected = character === "]" ? "[" : "{";
|
|
488
|
+
if (stack.at(-1) === expected) stack.pop();
|
|
489
|
+
if (stack.length === 0) return offset + 1;
|
|
490
|
+
}
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function isPlausibleTelegramMatrixStart(
|
|
495
|
+
source: string,
|
|
496
|
+
start: number,
|
|
497
|
+
): boolean {
|
|
498
|
+
let offset = start + 1;
|
|
499
|
+
while (/\s/u.test(source[offset] ?? "")) offset += 1;
|
|
500
|
+
return (
|
|
501
|
+
source[offset] === "{" ||
|
|
502
|
+
source[offset] === "[" ||
|
|
503
|
+
source[offset] === "]"
|
|
504
|
+
);
|
|
381
505
|
}
|
|
382
506
|
|
|
383
507
|
export function parseTelegramActionPayloadRows(
|
|
384
508
|
comment: TelegramTopLevelHtmlComment,
|
|
385
509
|
command: string,
|
|
386
510
|
): Record<string, unknown>[][] | undefined {
|
|
387
|
-
|
|
388
|
-
if (!
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
}
|
|
400
|
-
|
|
401
|
-
!Array.isArray(entry) ||
|
|
402
|
-
entry.length === 0 ||
|
|
403
|
-
!entry.every(isTelegramActionPayload)
|
|
404
|
-
) {
|
|
405
|
-
return undefined;
|
|
406
|
-
}
|
|
407
|
-
rows.push(entry);
|
|
511
|
+
let content = comment.content.replace(/^\s+/, "").replace(/^!/, "");
|
|
512
|
+
if (!content.startsWith(command)) return undefined;
|
|
513
|
+
content = content.slice(command.length);
|
|
514
|
+
let attributeEnvelope = content;
|
|
515
|
+
for (let offset = 0; offset < content.length; offset += 1) {
|
|
516
|
+
if (content[offset] !== "[" && content[offset] !== "{") continue;
|
|
517
|
+
if (
|
|
518
|
+
content[offset] === "[" &&
|
|
519
|
+
!isPlausibleTelegramMatrixStart(content, offset)
|
|
520
|
+
) {
|
|
521
|
+
const noiseEnd = findTelegramStructuredPayloadEnd(content, offset);
|
|
522
|
+
if (noiseEnd !== undefined) {
|
|
523
|
+
attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(noiseEnd - offset)}${attributeEnvelope.slice(noiseEnd)}`;
|
|
524
|
+
offset = noiseEnd - 1;
|
|
408
525
|
}
|
|
409
|
-
|
|
410
|
-
} catch {
|
|
411
|
-
return parseTelegramAdaptiveActionPayloadRows(payload.source);
|
|
526
|
+
continue;
|
|
412
527
|
}
|
|
528
|
+
const parsed = parseTelegramAdaptiveActionPayloadRows(
|
|
529
|
+
content.slice(offset),
|
|
530
|
+
parseTelegramButtonCompactActionPayload,
|
|
531
|
+
{ allowTrailing: true },
|
|
532
|
+
);
|
|
533
|
+
if (parsed) return parsed.rows;
|
|
534
|
+
const end = findTelegramStructuredPayloadEnd(content, offset);
|
|
535
|
+
if (end === undefined) continue;
|
|
536
|
+
attributeEnvelope = `${attributeEnvelope.slice(0, offset)}${" ".repeat(end - offset)}${attributeEnvelope.slice(end)}`;
|
|
537
|
+
offset = end - 1;
|
|
413
538
|
}
|
|
414
|
-
|
|
415
|
-
|
|
539
|
+
const attributes = parseTolerantTelegramAttributes(attributeEnvelope, [
|
|
540
|
+
"label",
|
|
541
|
+
"prompt",
|
|
542
|
+
"value",
|
|
543
|
+
"selected_style",
|
|
544
|
+
]);
|
|
416
545
|
return attributes ? [[attributes]] : undefined;
|
|
417
546
|
}
|
|
418
547
|
|
|
@@ -481,9 +610,10 @@ export function planTelegramVoiceReply(
|
|
|
481
610
|
let lang: string | undefined;
|
|
482
611
|
let rate: string | undefined;
|
|
483
612
|
const stripped = replaceTopLevelHtmlComments(markdown, (comment) => {
|
|
484
|
-
const command =
|
|
485
|
-
|
|
486
|
-
|
|
613
|
+
const command = "telegram_voice";
|
|
614
|
+
const normalizedContent = comment.content.replace(/^\s+/, "").replace(/^!/, "");
|
|
615
|
+
if (!normalizedContent.startsWith(command)) return comment.raw;
|
|
616
|
+
const payload = parseTelegramActionPayload(comment, command);
|
|
487
617
|
if (!payload) return "";
|
|
488
618
|
const text =
|
|
489
619
|
getTelegramActionString(payload, "text") ??
|
|
@@ -107,8 +107,9 @@ function appendTelegramSourceAttachmentSection(
|
|
|
107
107
|
text: string,
|
|
108
108
|
from: string | undefined,
|
|
109
109
|
files: Pick<Media.DownloadedTelegramFile, "path">[],
|
|
110
|
+
outputs: readonly string[] = [],
|
|
110
111
|
): string {
|
|
111
|
-
if (files.length === 0) return text;
|
|
112
|
+
if (files.length === 0 && outputs.length === 0) return text;
|
|
112
113
|
const dirs = [...new Set(files.map((file) => dirname(file.path)))];
|
|
113
114
|
const sameDir = dirs.length === 1;
|
|
114
115
|
const source = from ? `|from:${from}` : "";
|
|
@@ -118,8 +119,15 @@ function appendTelegramSourceAttachmentSection(
|
|
|
118
119
|
const items = sameDir
|
|
119
120
|
? files.map((file) => `/${basename(file.path)}`)
|
|
120
121
|
: files.map((file) => file.path);
|
|
121
|
-
const
|
|
122
|
-
|
|
122
|
+
const sections = text ? [text] : [];
|
|
123
|
+
if (items.length > 0) {
|
|
124
|
+
sections.push(`${header}\n${items.map((item) => `- ${item}`).join("\n")}`);
|
|
125
|
+
}
|
|
126
|
+
if (outputs.length > 0) {
|
|
127
|
+
const outputHeader = `[outputs${source}]`;
|
|
128
|
+
sections.push(`${outputHeader}\n${outputs.map((output) => `- ${output}`).join("\n")}`);
|
|
129
|
+
}
|
|
130
|
+
return sections.join("\n\n");
|
|
123
131
|
}
|
|
124
132
|
|
|
125
133
|
function getContextCwd(ctx: unknown): string | undefined {
|
|
@@ -269,7 +277,7 @@ function getTelegramRoutableThreadRecords(
|
|
|
269
277
|
|
|
270
278
|
function formatTelegramAllTabMenuChooserText(command: string): string {
|
|
271
279
|
return [
|
|
272
|
-
"<b>🧵 Choose target thread
|
|
280
|
+
"<b>🧵 Choose target thread:</b>",
|
|
273
281
|
"",
|
|
274
282
|
`You used <code>/${escapeHtml(command)}</code> from the <b>All</b> tab.`,
|
|
275
283
|
"Select the Pi thread that should handle it:",
|
|
@@ -332,7 +340,7 @@ function buildTelegramUnboundRerouteRestoreChooserMarkup(
|
|
|
332
340
|
|
|
333
341
|
function formatTelegramUnboundRerouteRestoreChooserText(): string {
|
|
334
342
|
return [
|
|
335
|
-
"<b>🧵 Replace/restore Telegram thread
|
|
343
|
+
"<b>🧵 Replace/restore Telegram thread:</b>",
|
|
336
344
|
"",
|
|
337
345
|
"Choose the Pi instance to move to this new Telegram thread:",
|
|
338
346
|
].join("\n");
|
|
@@ -340,7 +348,7 @@ function formatTelegramUnboundRerouteRestoreChooserText(): string {
|
|
|
340
348
|
|
|
341
349
|
function formatTelegramUnboundTopicGuidance(): string {
|
|
342
350
|
return [
|
|
343
|
-
"
|
|
351
|
+
"<b>⚠️ New thread is not a Pi instance.</b>",
|
|
344
352
|
"",
|
|
345
353
|
"To create a bound Telegram tab:",
|
|
346
354
|
"<code>1.</code> Start another Pi instance in your terminal.",
|
|
@@ -358,7 +366,7 @@ function formatTelegramUnboundRerouteChooserText(
|
|
|
358
366
|
options: { includeGuidance?: boolean } = {},
|
|
359
367
|
): string {
|
|
360
368
|
const rerouteText = [
|
|
361
|
-
"
|
|
369
|
+
"<b>🧵 Choose target thread:</b>",
|
|
362
370
|
"",
|
|
363
371
|
"Your message is still in this Telegram thread.",
|
|
364
372
|
"Select the Pi thread that should handle it:",
|
|
@@ -751,6 +759,42 @@ export function createTelegramInboundRouteRuntime<
|
|
|
751
759
|
);
|
|
752
760
|
}
|
|
753
761
|
};
|
|
762
|
+
const resolveTelegramThreadLabel = (message: {
|
|
763
|
+
chat: { id: number };
|
|
764
|
+
message_thread_id?: number;
|
|
765
|
+
}): string | undefined => {
|
|
766
|
+
const chatId = message.chat.id;
|
|
767
|
+
const threadId = message.message_thread_id;
|
|
768
|
+
if (!threadId) return undefined;
|
|
769
|
+
const localLabel = deps.getLocalThreadLabelForTarget?.({ chatId, threadId });
|
|
770
|
+
if (localLabel) return localLabel;
|
|
771
|
+
if (!deps.threadStore) return undefined;
|
|
772
|
+
const records = deps.threadStore.list();
|
|
773
|
+
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
774
|
+
for (const record of records) {
|
|
775
|
+
if (
|
|
776
|
+
record.target.chatId !== chatId ||
|
|
777
|
+
record.target.threadId !== threadId
|
|
778
|
+
) {
|
|
779
|
+
continue;
|
|
780
|
+
}
|
|
781
|
+
if (
|
|
782
|
+
currentInstanceId &&
|
|
783
|
+
record.instanceId &&
|
|
784
|
+
record.instanceId !== currentInstanceId
|
|
785
|
+
) {
|
|
786
|
+
continue;
|
|
787
|
+
}
|
|
788
|
+
return record.threadName &&
|
|
789
|
+
Threads.isTelegramTopicThreadNameValidForSlot(
|
|
790
|
+
record.threadName,
|
|
791
|
+
record.slot,
|
|
792
|
+
)
|
|
793
|
+
? record.threadName
|
|
794
|
+
: getRestoredThreadName(record, record.slot ?? "");
|
|
795
|
+
}
|
|
796
|
+
return undefined;
|
|
797
|
+
};
|
|
754
798
|
const createAdmissionReceipts = (
|
|
755
799
|
queueKind: Queue.TelegramQueueItemKind,
|
|
756
800
|
sources: readonly unknown[],
|
|
@@ -1620,6 +1664,13 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1620
1664
|
replyToMessageId: messageId,
|
|
1621
1665
|
queueOrder,
|
|
1622
1666
|
action,
|
|
1667
|
+
telegramPrefix: Turns.createTelegramTurnPrefix({
|
|
1668
|
+
thread: resolveTelegramThreadLabel({
|
|
1669
|
+
chat: { id: chatId },
|
|
1670
|
+
message_thread_id:
|
|
1671
|
+
buttonQuery.message?.message_thread_id,
|
|
1672
|
+
}),
|
|
1673
|
+
}),
|
|
1623
1674
|
}),
|
|
1624
1675
|
...(admissionReceipts.length > 0 ? { admissionReceipts } : {}),
|
|
1625
1676
|
};
|
|
@@ -1762,34 +1813,7 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1762
1813
|
|
|
1763
1814
|
// Voice policy resolves missing, invalid, and legacy manual config to hidden.
|
|
1764
1815
|
getVoiceReplyMode: () => getTelegramVoiceReplyMode(deps.configStore.get()),
|
|
1765
|
-
getTelegramThreadLabel
|
|
1766
|
-
if (!deps.threadStore) return undefined;
|
|
1767
|
-
const chatId = message.chat.id;
|
|
1768
|
-
const threadId = message.message_thread_id;
|
|
1769
|
-
if (!threadId) return undefined;
|
|
1770
|
-
const localLabel = deps.getLocalThreadLabelForTarget?.({
|
|
1771
|
-
chatId,
|
|
1772
|
-
threadId,
|
|
1773
|
-
});
|
|
1774
|
-
if (localLabel) return localLabel;
|
|
1775
|
-
const records = deps.threadStore.list();
|
|
1776
|
-
const currentInstanceId = deps.getCurrentInstanceId?.();
|
|
1777
|
-
for (const r of records) {
|
|
1778
|
-
if (r.target.chatId !== chatId || r.target.threadId !== threadId)
|
|
1779
|
-
continue;
|
|
1780
|
-
if (
|
|
1781
|
-
currentInstanceId &&
|
|
1782
|
-
r.instanceId &&
|
|
1783
|
-
r.instanceId !== currentInstanceId
|
|
1784
|
-
)
|
|
1785
|
-
continue;
|
|
1786
|
-
return r.threadName &&
|
|
1787
|
-
Threads.isTelegramTopicThreadNameValidForSlot(r.threadName, r.slot)
|
|
1788
|
-
? r.threadName
|
|
1789
|
-
: getRestoredThreadName(r, r.slot ?? "");
|
|
1790
|
-
}
|
|
1791
|
-
return undefined;
|
|
1792
|
-
},
|
|
1816
|
+
getTelegramThreadLabel: resolveTelegramThreadLabel,
|
|
1793
1817
|
});
|
|
1794
1818
|
const enqueueContinueTurn = async (
|
|
1795
1819
|
message: TMessage,
|
|
@@ -1877,6 +1901,18 @@ export function createTelegramInboundRouteRuntime<
|
|
|
1877
1901
|
getPromptTemplateCommands,
|
|
1878
1902
|
persistConfig: deps.configStore.persist,
|
|
1879
1903
|
sendTextReply: deps.sendTextReply,
|
|
1904
|
+
getActiveTurnReply: () => {
|
|
1905
|
+
const activeTurn = deps.activeTurnRuntime.get();
|
|
1906
|
+
if (!activeTurn) return undefined;
|
|
1907
|
+
return async (text, options) => {
|
|
1908
|
+
await deps.sendTextReply(
|
|
1909
|
+
activeTurn.chatId,
|
|
1910
|
+
activeTurn.replyToMessageId,
|
|
1911
|
+
text,
|
|
1912
|
+
{ target: activeTurn.target, parseMode: options?.parseMode },
|
|
1913
|
+
);
|
|
1914
|
+
};
|
|
1915
|
+
},
|
|
1880
1916
|
sendInteractiveMessage: deps.sendInteractiveMessage,
|
|
1881
1917
|
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
1882
1918
|
});
|
|
@@ -2265,6 +2301,11 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2265
2301
|
)
|
|
2266
2302
|
: [];
|
|
2267
2303
|
assertExecutionCurrent();
|
|
2304
|
+
const processedReply =
|
|
2305
|
+
replyFiles.length > 0
|
|
2306
|
+
? await deps.inboundHandlerRuntime.process(replyFiles, "", ctx)
|
|
2307
|
+
: undefined;
|
|
2308
|
+
assertExecutionCurrent();
|
|
2268
2309
|
const files = await Media.downloadTelegramMessageFiles([guestMsg], {
|
|
2269
2310
|
downloadFile: deps.downloadFile,
|
|
2270
2311
|
});
|
|
@@ -2285,7 +2326,8 @@ export function createTelegramInboundRouteRuntime<
|
|
|
2285
2326
|
sourceContext = appendTelegramSourceAttachmentSection(
|
|
2286
2327
|
replyBlock,
|
|
2287
2328
|
replyPeer,
|
|
2288
|
-
replyFiles,
|
|
2329
|
+
processedReply?.promptFiles ?? replyFiles,
|
|
2330
|
+
processedReply?.handlerOutputs,
|
|
2289
2331
|
);
|
|
2290
2332
|
}
|
|
2291
2333
|
const promptText = Turns.buildTelegramTurnPrompt({
|
|
@@ -1494,17 +1494,22 @@ function buildUsageSummary(stats: TelegramUsageStats): string | undefined {
|
|
|
1494
1494
|
const tokenParts: string[] = [];
|
|
1495
1495
|
if (stats.totalInput) tokenParts.push(`↑${formatTokens(stats.totalInput)}`);
|
|
1496
1496
|
if (stats.totalOutput) tokenParts.push(`↓${formatTokens(stats.totalOutput)}`);
|
|
1497
|
+
return tokenParts.length > 0 ? tokenParts.join(" ") : undefined;
|
|
1498
|
+
}
|
|
1499
|
+
|
|
1500
|
+
function buildCacheSummary(stats: TelegramUsageStats): string | undefined {
|
|
1501
|
+
const cacheParts: string[] = [];
|
|
1497
1502
|
if (stats.totalCacheRead)
|
|
1498
|
-
|
|
1503
|
+
cacheParts.push(`R${formatTokens(stats.totalCacheRead)}`);
|
|
1499
1504
|
if (stats.totalCacheWrite)
|
|
1500
|
-
|
|
1505
|
+
cacheParts.push(`W${formatTokens(stats.totalCacheWrite)}`);
|
|
1501
1506
|
if (
|
|
1502
1507
|
(stats.totalCacheRead > 0 || stats.totalCacheWrite > 0) &&
|
|
1503
1508
|
stats.latestCacheHitRate !== undefined
|
|
1504
1509
|
) {
|
|
1505
|
-
|
|
1510
|
+
cacheParts.push(`CH${stats.latestCacheHitRate.toFixed(1)}%`);
|
|
1506
1511
|
}
|
|
1507
|
-
return
|
|
1512
|
+
return cacheParts.length > 0 ? cacheParts.join(" ") : undefined;
|
|
1508
1513
|
}
|
|
1509
1514
|
|
|
1510
1515
|
function buildCostSummary(
|
|
@@ -1557,10 +1562,14 @@ export function buildStatusHtml(
|
|
|
1557
1562
|
),
|
|
1558
1563
|
];
|
|
1559
1564
|
const usageSummary = buildUsageSummary(stats);
|
|
1565
|
+
const cacheSummary = buildCacheSummary(stats);
|
|
1560
1566
|
const costSummary = buildCostSummary(stats, usesSubscription);
|
|
1561
1567
|
if (usageSummary) {
|
|
1562
1568
|
lines.push(buildStatusRow("Tokens", usageSummary));
|
|
1563
1569
|
}
|
|
1570
|
+
if (cacheSummary) {
|
|
1571
|
+
lines.push(buildStatusRow("Cache", cacheSummary));
|
|
1572
|
+
}
|
|
1564
1573
|
if (costSummary) {
|
|
1565
1574
|
lines.push(buildStatusRow("Cost", costSummary));
|
|
1566
1575
|
}
|
|
@@ -468,8 +468,17 @@ export function createTelegramPromptTurnRuntimeBuilder<
|
|
|
468
468
|
)
|
|
469
469
|
: [];
|
|
470
470
|
if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
|
|
471
|
+
const processedReply =
|
|
472
|
+
deps.processAttachments && replyFiles.length > 0
|
|
473
|
+
? await deps.processAttachments(replyFiles, "", ctx as TContext)
|
|
474
|
+
: undefined;
|
|
475
|
+
if (firstMessage) deps.assertExecutionCurrent?.(firstMessage);
|
|
471
476
|
const replyContext = firstMessage
|
|
472
|
-
? buildTelegramReplyContextBlock(
|
|
477
|
+
? buildTelegramReplyContextBlock(
|
|
478
|
+
firstMessage,
|
|
479
|
+
processedReply?.promptFiles ?? replyFiles,
|
|
480
|
+
processedReply?.handlerOutputs,
|
|
481
|
+
)
|
|
473
482
|
: "";
|
|
474
483
|
const forwardEntries = messages.flatMap((message) => {
|
|
475
484
|
const context = extractTelegramForwardContextText(
|
|
@@ -895,7 +895,7 @@ const TELEGRAM_UNAUTHORIZED_DENIAL_COPY = "Access denied.";
|
|
|
895
895
|
|
|
896
896
|
function formatTelegramUnauthorizedDenial(format: "plain" | "html"): string {
|
|
897
897
|
return format === "html"
|
|
898
|
-
?
|
|
898
|
+
? `<b>🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}</b>`
|
|
899
899
|
: `🚫 ${TELEGRAM_UNAUTHORIZED_DENIAL_COPY}`;
|
|
900
900
|
}
|
|
901
901
|
|