@llblab/pi-telegram 0.46.0 → 0.47.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/BACKLOG.md +0 -1
- package/CHANGELOG.md +8 -0
- package/README.md +2 -2
- package/dist/api/activity.d.ts +6 -0
- package/dist/api/activity.js +6 -0
- package/dist/api/commands.d.ts +6 -0
- package/dist/api/commands.js +6 -0
- package/dist/api/delivery.d.ts +6 -0
- package/dist/api/delivery.js +6 -0
- package/dist/api/inbound.d.ts +6 -0
- package/dist/api/inbound.js +6 -0
- package/dist/api/keyboard.d.ts +6 -0
- package/dist/api/keyboard.js +6 -0
- package/dist/api/outbound.d.ts +6 -0
- package/dist/api/outbound.js +6 -0
- package/dist/api/sections.d.ts +7 -0
- package/dist/api/sections.js +6 -0
- package/dist/api/status.d.ts +6 -0
- package/dist/api/status.js +6 -0
- package/dist/api/updates.d.ts +6 -0
- package/dist/api/updates.js +6 -0
- package/dist/api/voice.d.ts +6 -0
- package/dist/api/voice.js +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/lib/activity-verbosity.d.ts +52 -0
- package/dist/lib/activity-verbosity.js +596 -0
- package/dist/lib/activity.d.ts +220 -0
- package/dist/lib/activity.js +574 -0
- package/dist/lib/agent-messages.d.ts +28 -0
- package/dist/lib/agent-messages.js +86 -0
- package/dist/lib/bindings.d.ts +250 -0
- package/dist/lib/bindings.js +936 -0
- package/dist/lib/bus-api.d.ts +18 -0
- package/dist/lib/bus-api.js +254 -0
- package/dist/lib/bus-follower.d.ts +464 -0
- package/dist/lib/bus-follower.js +1686 -0
- package/dist/lib/bus-leader.d.ts +292 -0
- package/dist/lib/bus-leader.js +2242 -0
- package/dist/lib/bus-transport.d.ts +64 -0
- package/dist/lib/bus-transport.js +140 -0
- package/dist/lib/bus.d.ts +518 -0
- package/dist/lib/bus.js +2055 -0
- package/dist/lib/channel-posts.d.ts +170 -0
- package/dist/lib/channel-posts.js +611 -0
- package/dist/lib/command-templates.d.ts +70 -0
- package/dist/lib/command-templates.js +744 -0
- package/dist/lib/commands.d.ts +541 -0
- package/dist/lib/commands.js +1155 -0
- package/dist/lib/config.d.ts +252 -0
- package/dist/lib/config.js +889 -0
- package/dist/lib/delivery.d.ts +163 -0
- package/dist/lib/delivery.js +542 -0
- package/dist/lib/extension.d.ts +7 -0
- package/dist/lib/extension.js +1608 -0
- package/dist/lib/generative-app-worker.mjs +104 -0
- package/dist/lib/generative-apps.d.ts +212 -0
- package/dist/lib/generative-apps.js +1006 -0
- package/dist/lib/inbound.d.ts +91 -0
- package/dist/lib/inbound.js +502 -0
- package/dist/lib/journal.d.ts +665 -0
- package/dist/lib/journal.js +3701 -0
- package/dist/lib/keyboard.d.ts +23 -0
- package/dist/lib/keyboard.js +37 -0
- package/dist/lib/lifecycle.d.ts +157 -0
- package/dist/lib/lifecycle.js +395 -0
- package/dist/lib/locks.d.ts +164 -0
- package/dist/lib/locks.js +1208 -0
- package/dist/lib/logging.d.ts +50 -0
- package/dist/lib/logging.js +274 -0
- package/dist/lib/media.d.ts +181 -0
- package/dist/lib/media.js +602 -0
- package/dist/lib/menu-model.d.ts +217 -0
- package/dist/lib/menu-model.js +663 -0
- package/dist/lib/menu-queue.d.ts +37 -0
- package/dist/lib/menu-queue.js +408 -0
- package/dist/lib/menu-settings.d.ts +116 -0
- package/dist/lib/menu-settings.js +595 -0
- package/dist/lib/menu-status.d.ts +32 -0
- package/dist/lib/menu-status.js +112 -0
- package/dist/lib/menu-thinking.d.ts +29 -0
- package/dist/lib/menu-thinking.js +82 -0
- package/dist/lib/menu.d.ts +171 -0
- package/dist/lib/menu.js +323 -0
- package/dist/lib/model.d.ts +127 -0
- package/dist/lib/model.js +409 -0
- package/dist/lib/outbound-attachments.d.ts +241 -0
- package/dist/lib/outbound-attachments.js +639 -0
- package/dist/lib/outbound-buttons.d.ts +69 -0
- package/dist/lib/outbound-buttons.js +248 -0
- package/dist/lib/outbound-markup.d.ts +42 -0
- package/dist/lib/outbound-markup.js +678 -0
- package/dist/lib/outbound-voice.d.ts +55 -0
- package/dist/lib/outbound-voice.js +152 -0
- package/dist/lib/outbound.d.ts +185 -0
- package/dist/lib/outbound.js +516 -0
- package/dist/lib/ownership.d.ts +77 -0
- package/dist/lib/ownership.js +174 -0
- package/dist/lib/paths.d.ts +40 -0
- package/dist/lib/paths.js +94 -0
- package/dist/lib/pi.d.ts +72 -0
- package/dist/lib/pi.js +121 -0
- package/dist/lib/polling.d.ts +351 -0
- package/dist/lib/polling.js +1196 -0
- package/dist/lib/preview.d.ts +192 -0
- package/dist/lib/preview.js +614 -0
- package/dist/lib/prompt-templates.d.ts +24 -0
- package/dist/lib/prompt-templates.js +118 -0
- package/dist/lib/prompts.d.ts +57 -0
- package/dist/lib/prompts.js +167 -0
- package/dist/lib/queue.d.ts +766 -0
- package/dist/lib/queue.js +1965 -0
- package/dist/lib/recovery.d.ts +86 -0
- package/dist/lib/recovery.js +285 -0
- package/dist/lib/rendering.d.ts +20 -0
- package/dist/lib/rendering.js +983 -0
- package/dist/lib/replies.d.ts +214 -0
- package/dist/lib/replies.js +737 -0
- package/dist/lib/routing.d.ts +207 -0
- package/dist/lib/routing.js +2282 -0
- package/dist/lib/runtime.d.ts +172 -0
- package/dist/lib/runtime.js +402 -0
- package/dist/lib/sections.d.ts +165 -0
- package/dist/lib/sections.js +327 -0
- package/dist/lib/setup.d.ts +82 -0
- package/dist/lib/setup.js +150 -0
- package/dist/lib/skills.d.ts +8 -0
- package/dist/lib/skills.js +12 -0
- package/dist/lib/status.d.ts +442 -0
- package/dist/lib/status.js +1006 -0
- package/dist/lib/sync.d.ts +179 -0
- package/dist/lib/sync.js +782 -0
- package/dist/lib/target.d.ts +20 -0
- package/dist/lib/target.js +27 -0
- package/dist/lib/telegram-api.d.ts +541 -0
- package/dist/lib/telegram-api.js +1159 -0
- package/dist/lib/text-groups.d.ts +89 -0
- package/dist/lib/text-groups.js +317 -0
- package/dist/lib/thread-cleanup-manager.d.ts +288 -0
- package/dist/lib/thread-cleanup-manager.js +560 -0
- package/dist/lib/thread-display.d.ts +46 -0
- package/dist/lib/thread-display.js +257 -0
- package/dist/lib/thread-naming.d.ts +46 -0
- package/dist/lib/thread-naming.js +78 -0
- package/dist/lib/thread-reconciler.d.ts +239 -0
- package/dist/lib/thread-reconciler.js +644 -0
- package/dist/lib/threads.d.ts +621 -0
- package/dist/lib/threads.js +3679 -0
- package/dist/lib/time-injection.d.ts +15 -0
- package/dist/lib/time-injection.js +56 -0
- package/dist/lib/turns.d.ts +109 -0
- package/dist/lib/turns.js +500 -0
- package/dist/lib/updates.d.ts +1290 -0
- package/dist/lib/updates.js +3634 -0
- package/dist/lib/voice.d.ts +117 -0
- package/dist/lib/voice.js +174 -0
- package/dist/lib/workspace-admission.d.ts +264 -0
- package/dist/lib/workspace-admission.js +1136 -0
- package/dist/lib/workspace-retirement.d.ts +219 -0
- package/dist/lib/workspace-retirement.js +587 -0
- package/dist/lib/workspace-slots.d.ts +30 -0
- package/dist/lib/workspace-slots.js +54 -0
- package/dist/package.json +126 -0
- package/dist/pi-telegram/index.js +1 -0
- package/dist/skills/generated-control-surface/SKILL.md +107 -0
- package/dist/skills/generated-control-surface/references/capability-adapters.md +27 -0
- package/dist/skills/generated-control-surface/references/layout-and-state.md +37 -0
- package/dist/skills/generative-apps/SKILL.md +115 -0
- package/dist/skills/show-me/SKILL.md +166 -0
- package/dist/skills/show-me/references/telegram-surfaces.md +43 -0
- package/dist/skills/telegram-bridge/SKILL.md +129 -0
- package/dist/skills/telegram-bridge/references/configuration.md +15 -0
- package/dist/skills/telegram-bridge/references/delivery-and-threads.md +27 -0
- package/dist/skills/telegram-bridge/references/diagnosis.md +18 -0
- package/docs/architecture.md +1 -1
- package/docs/compact-matrix-literal.md +10 -3
- package/docs/generative-apps.md +15 -13
- package/docs/multi-instance-bus.md +4 -2
- package/docs/outbound.md +1 -1
- package/docs/public-api.md +2 -2
- package/lib/bindings.ts +89 -6
- package/lib/bus-follower.ts +7 -3
- package/lib/bus-leader.ts +44 -19
- package/lib/bus.ts +4 -1
- package/lib/commands.ts +2 -2
- package/lib/config.ts +14 -7
- package/lib/delivery.ts +38 -6
- package/lib/extension.ts +19 -0
- package/lib/generative-apps.ts +379 -13
- package/lib/menu-settings.ts +21 -5
- package/lib/outbound-markup.ts +16 -7
- package/lib/telegram-api.ts +12 -1
- package/lib/thread-display.ts +96 -18
- package/package.json +56 -13
- package/scripts/build-dist.mjs +44 -0
- package/scripts/measure-bus.mjs +8 -1
- package/scripts/measure-workspace.mjs +8 -1
- package/skills/generative-apps/SKILL.md +1 -1
- package/skills/telegram-bridge/SKILL.md +1 -1
|
@@ -0,0 +1,516 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram outbound surface helpers
|
|
3
|
+
* Zones: telegram outbound, command templates, voice delivery
|
|
4
|
+
* Owns configured outbound handler execution, text transforms, public assistant-output reply composition and mutation fencing, voice-file generation/delivery, runtime-event bridge, and compatibility re-exports; assistant markup parsing lives in outbound-markup and button callback actions live in outbound-buttons
|
|
5
|
+
*/
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { mkdir } from "node:fs/promises";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { resolveTelegramTempDir } from "./paths.js";
|
|
10
|
+
import * as Replies from "./replies.js";
|
|
11
|
+
import { isTelegramApiCommitUnknownError } from "./telegram-api.js";
|
|
12
|
+
import { planTelegramButtonReply, } from "./outbound-buttons.js";
|
|
13
|
+
import { planTelegramVoiceReply, stripTelegramCommentMarkupForDelivery, } from "./outbound-markup.js";
|
|
14
|
+
import { createTelegramVoiceReplySender as createTelegramVoiceReplySenderWithPorts } from "./outbound-voice.js";
|
|
15
|
+
const OUTBOUND_HANDLER_REGISTRY_KEY = "__piTelegramOutboundHandlers__";
|
|
16
|
+
const VOICE_EVENT_RECORDER_KEY = "__piTelegramVoiceEventRecorder__";
|
|
17
|
+
import { buildCommandTemplateInvocation, expandCommandTemplateConfigs, substituteCommandTemplateToken, } from "./command-templates.js";
|
|
18
|
+
const DEFAULT_VOICE_TIMEOUT_MS = 120_000;
|
|
19
|
+
export function bindTelegramRuntimeEventRecorder(recorder) {
|
|
20
|
+
globalThis[VOICE_EVENT_RECORDER_KEY] = recorder;
|
|
21
|
+
}
|
|
22
|
+
export function recordTelegramRuntimeEvent(category, error, details) {
|
|
23
|
+
const recorder = globalThis[VOICE_EVENT_RECORDER_KEY];
|
|
24
|
+
if (typeof recorder === "function") {
|
|
25
|
+
recorder(category, error, details);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export { normalizeMarkdownAfterVoiceExtraction, planTelegramVoiceReply, stripTelegramCommentMarkupForDelivery, stripTelegramCommentMarkupForPreview, stripTelegramVoiceMarkupForPreview, } from "./outbound-markup.js";
|
|
29
|
+
// --- Programmatic Outbound Handler Registry Runtime ---
|
|
30
|
+
function getOrCreateOutboundHandlerRegistry() {
|
|
31
|
+
const existing = globalThis[OUTBOUND_HANDLER_REGISTRY_KEY];
|
|
32
|
+
if (existing &&
|
|
33
|
+
typeof existing === "object" &&
|
|
34
|
+
existing !== null &&
|
|
35
|
+
"handlers" in existing &&
|
|
36
|
+
existing.handlers instanceof Map) {
|
|
37
|
+
return existing;
|
|
38
|
+
}
|
|
39
|
+
const registry = {
|
|
40
|
+
handlers: new Map(),
|
|
41
|
+
};
|
|
42
|
+
globalThis[OUTBOUND_HANDLER_REGISTRY_KEY] =
|
|
43
|
+
registry;
|
|
44
|
+
return registry;
|
|
45
|
+
}
|
|
46
|
+
export function registerTelegramOutboundHandler(kind, handler) {
|
|
47
|
+
const registry = getOrCreateOutboundHandlerRegistry();
|
|
48
|
+
const list = registry.handlers.get(kind) ?? [];
|
|
49
|
+
list.push(handler);
|
|
50
|
+
registry.handlers.set(kind, list);
|
|
51
|
+
return () => {
|
|
52
|
+
const updated = registry.handlers.get(kind) ?? [];
|
|
53
|
+
const index = updated.indexOf(handler);
|
|
54
|
+
if (index !== -1) {
|
|
55
|
+
updated.splice(index, 1);
|
|
56
|
+
registry.handlers.set(kind, updated);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export function getTelegramOutboundProgrammaticHandlers(kind) {
|
|
61
|
+
const registry = getOrCreateOutboundHandlerRegistry();
|
|
62
|
+
return [...(registry.handlers.get(kind) ?? [])];
|
|
63
|
+
}
|
|
64
|
+
// --- Voice Reply Timeout Helpers ---
|
|
65
|
+
function resolveOutboundNumericControlField(value, values, label) {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return undefined;
|
|
68
|
+
const resolved = typeof value === "string"
|
|
69
|
+
? substituteCommandTemplateToken(value, values, label)
|
|
70
|
+
: value;
|
|
71
|
+
if (resolved === "")
|
|
72
|
+
return undefined;
|
|
73
|
+
const numeric = Number(resolved);
|
|
74
|
+
if (!Number.isFinite(numeric) || numeric < 0)
|
|
75
|
+
throw new Error(`Command template ${label} must be a non-negative number.`);
|
|
76
|
+
return numeric;
|
|
77
|
+
}
|
|
78
|
+
function getVoiceReplyConfiguredTimeout(config) {
|
|
79
|
+
const timeout = typeof config === "string" ? undefined : config?.timeout;
|
|
80
|
+
return resolveOutboundNumericControlField(timeout, {}, "timeout");
|
|
81
|
+
}
|
|
82
|
+
function getVoiceReplyTimeout(config) {
|
|
83
|
+
return getVoiceReplyConfiguredTimeout(config) ?? DEFAULT_VOICE_TIMEOUT_MS;
|
|
84
|
+
}
|
|
85
|
+
function getRemainingVoiceReplyTimeout(timeout, startedAt) {
|
|
86
|
+
return Math.max(1, timeout - (Date.now() - startedAt));
|
|
87
|
+
}
|
|
88
|
+
function getVoiceReplyCompositionStepTimeout(handlerTimeout, step, startedAt) {
|
|
89
|
+
const remaining = getRemainingVoiceReplyTimeout(handlerTimeout, startedAt);
|
|
90
|
+
const stepTimeout = getVoiceReplyConfiguredTimeout(step);
|
|
91
|
+
return stepTimeout === undefined
|
|
92
|
+
? remaining
|
|
93
|
+
: Math.min(stepTimeout, remaining);
|
|
94
|
+
}
|
|
95
|
+
function formatVoiceReplyExecutionFailure(label, result) {
|
|
96
|
+
const parts = [
|
|
97
|
+
`${label} exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
|
|
98
|
+
];
|
|
99
|
+
if (result.stderr.trim())
|
|
100
|
+
parts.push(`stderr:\n${result.stderr.trimEnd()}`);
|
|
101
|
+
if (result.stdout.trim())
|
|
102
|
+
parts.push(`stdout:\n${result.stdout.trimEnd()}`);
|
|
103
|
+
return parts.join("\n\n");
|
|
104
|
+
}
|
|
105
|
+
async function runVoiceReplyCommand(label, config, values, options) {
|
|
106
|
+
if (!options.execCommand) {
|
|
107
|
+
throw new Error("execCommand is required for command template execution");
|
|
108
|
+
}
|
|
109
|
+
const invocation = buildCommandTemplateInvocation(config, values, options.cwd, {
|
|
110
|
+
emptyMessage: "Outbound voice template is empty",
|
|
111
|
+
missingLabel: "outbound voice template",
|
|
112
|
+
});
|
|
113
|
+
const result = await options.execCommand(invocation.command, invocation.args, {
|
|
114
|
+
cwd: options.cwd,
|
|
115
|
+
timeout: options.timeout,
|
|
116
|
+
...(typeof config === "object" && config.retry !== undefined
|
|
117
|
+
? {
|
|
118
|
+
retry: resolveOutboundNumericControlField(config.retry, {}, "retry"),
|
|
119
|
+
}
|
|
120
|
+
: {}),
|
|
121
|
+
...(options.stdin !== undefined ? { stdin: options.stdin } : {}),
|
|
122
|
+
});
|
|
123
|
+
if (result.code !== 0)
|
|
124
|
+
throw new Error(formatVoiceReplyExecutionFailure(label, result));
|
|
125
|
+
return result;
|
|
126
|
+
}
|
|
127
|
+
function normalizeOutboundHandlerStringList(value) {
|
|
128
|
+
if (Array.isArray(value))
|
|
129
|
+
return value
|
|
130
|
+
.map(String)
|
|
131
|
+
.map((item) => item.trim())
|
|
132
|
+
.filter(Boolean);
|
|
133
|
+
if (typeof value === "string" && value.trim())
|
|
134
|
+
return [value.trim()];
|
|
135
|
+
return [];
|
|
136
|
+
}
|
|
137
|
+
function outboundHandlerMatchesType(handler, type) {
|
|
138
|
+
const selectors = [
|
|
139
|
+
...normalizeOutboundHandlerStringList(handler.type),
|
|
140
|
+
...normalizeOutboundHandlerStringList(handler.match),
|
|
141
|
+
];
|
|
142
|
+
if (selectors.length === 0)
|
|
143
|
+
return false;
|
|
144
|
+
return selectors.includes(type);
|
|
145
|
+
}
|
|
146
|
+
export function findTelegramOutboundHandlers(handlers, type) {
|
|
147
|
+
if (!Array.isArray(handlers))
|
|
148
|
+
return [];
|
|
149
|
+
return handlers.filter((handler) => !!handler &&
|
|
150
|
+
typeof handler === "object" &&
|
|
151
|
+
outboundHandlerMatchesType(handler, type));
|
|
152
|
+
}
|
|
153
|
+
function getTelegramVoiceHandlerCompositionSteps(handler) {
|
|
154
|
+
if (Array.isArray(handler.template)) {
|
|
155
|
+
return expandCommandTemplateConfigs(handler);
|
|
156
|
+
}
|
|
157
|
+
return [];
|
|
158
|
+
}
|
|
159
|
+
function extractVoiceReplyPath(stdout) {
|
|
160
|
+
const path = stdout.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
161
|
+
if (!path)
|
|
162
|
+
throw new Error("Voice generator did not print an output path");
|
|
163
|
+
return path;
|
|
164
|
+
}
|
|
165
|
+
function getVoiceReplyOutputPath(config, values, stdout) {
|
|
166
|
+
const output = config.output ?? "stdout";
|
|
167
|
+
if (output === "stdout")
|
|
168
|
+
return extractVoiceReplyPath(stdout);
|
|
169
|
+
const keyMatch = output.match(/^\{?([A-Za-z_][A-Za-z0-9_-]*)\}?$/);
|
|
170
|
+
if (keyMatch && Object.hasOwn(values, keyMatch[1])) {
|
|
171
|
+
return values[keyMatch[1]] ?? "";
|
|
172
|
+
}
|
|
173
|
+
return output.replace(/\{([A-Za-z_][A-Za-z0-9_-]*)\}/g, (_match, key) => values[key] ?? "");
|
|
174
|
+
}
|
|
175
|
+
function getVoiceReplyTemplateValues(text, options) {
|
|
176
|
+
return {
|
|
177
|
+
text,
|
|
178
|
+
type: "voice",
|
|
179
|
+
mp3: options.mp3Path,
|
|
180
|
+
ogg: options.oggPath,
|
|
181
|
+
...(options.lang ? { lang: options.lang } : {}),
|
|
182
|
+
...(options.rate ? { rate: options.rate } : {}),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
function getDefaultTelegramVoiceTempDir() {
|
|
186
|
+
return resolveTelegramTempDir();
|
|
187
|
+
}
|
|
188
|
+
async function generateTelegramVoiceReplyFileWithHandler(text, options) {
|
|
189
|
+
await mkdir(options.tempDir, { recursive: true });
|
|
190
|
+
const artifactId = randomUUID();
|
|
191
|
+
const values = getVoiceReplyTemplateValues(text, {
|
|
192
|
+
lang: options.lang,
|
|
193
|
+
rate: options.rate,
|
|
194
|
+
mp3Path: join(options.tempDir, `${artifactId}-voice.mp3`),
|
|
195
|
+
oggPath: join(options.tempDir, `${artifactId}-voice.ogg`),
|
|
196
|
+
});
|
|
197
|
+
const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
|
|
198
|
+
if (steps.length > 0) {
|
|
199
|
+
const startedAt = Date.now();
|
|
200
|
+
let stdout = text;
|
|
201
|
+
for (const [index, step] of steps.entries()) {
|
|
202
|
+
try {
|
|
203
|
+
const result = await runVoiceReplyCommand(`Outbound voice template step ${index + 1}`, step, values, {
|
|
204
|
+
cwd: options.cwd,
|
|
205
|
+
timeout: getVoiceReplyCompositionStepTimeout(options.timeout, step, startedAt),
|
|
206
|
+
execCommand: options.execCommand,
|
|
207
|
+
stdin: stdout,
|
|
208
|
+
});
|
|
209
|
+
stdout = result.stdout;
|
|
210
|
+
}
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (typeof step === "object" && step.failure === "root")
|
|
213
|
+
throw error;
|
|
214
|
+
stdout = "";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return getVoiceReplyOutputPath(options.handler, values, stdout);
|
|
218
|
+
}
|
|
219
|
+
const result = await runVoiceReplyCommand("Outbound voice template", options.handler, values, {
|
|
220
|
+
cwd: options.cwd,
|
|
221
|
+
timeout: options.timeout,
|
|
222
|
+
execCommand: options.execCommand,
|
|
223
|
+
stdin: text,
|
|
224
|
+
});
|
|
225
|
+
return getVoiceReplyOutputPath(options.handler, values, result.stdout);
|
|
226
|
+
}
|
|
227
|
+
export async function generateTelegramVoiceReplyFile(text, options) {
|
|
228
|
+
const handler = options.handler;
|
|
229
|
+
if (!handler?.template)
|
|
230
|
+
return undefined;
|
|
231
|
+
return generateTelegramVoiceReplyFileWithHandler(text, {
|
|
232
|
+
lang: options.lang,
|
|
233
|
+
rate: options.rate,
|
|
234
|
+
handler,
|
|
235
|
+
tempDir: options.tempDir ?? getDefaultTelegramVoiceTempDir(),
|
|
236
|
+
cwd: options.cwd ?? process.cwd(),
|
|
237
|
+
timeout: getVoiceReplyTimeout(handler),
|
|
238
|
+
execCommand: options.execCommand,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
function getOutboundTextTemplateValues(text) {
|
|
242
|
+
return { text, type: "text" };
|
|
243
|
+
}
|
|
244
|
+
async function transformTelegramOutboundTextWithHandler(text, options) {
|
|
245
|
+
const values = getOutboundTextTemplateValues(text);
|
|
246
|
+
const steps = getTelegramVoiceHandlerCompositionSteps(options.handler);
|
|
247
|
+
if (steps.length > 0) {
|
|
248
|
+
const startedAt = Date.now();
|
|
249
|
+
let stdout = text;
|
|
250
|
+
for (const [index, step] of steps.entries()) {
|
|
251
|
+
try {
|
|
252
|
+
const result = await runVoiceReplyCommand(`Outbound text template step ${index + 1}`, step, values, {
|
|
253
|
+
cwd: options.cwd,
|
|
254
|
+
timeout: getVoiceReplyCompositionStepTimeout(getVoiceReplyTimeout(options.handler), step, startedAt),
|
|
255
|
+
execCommand: options.execCommand,
|
|
256
|
+
stdin: stdout,
|
|
257
|
+
});
|
|
258
|
+
stdout = result.stdout;
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
if (typeof step === "object" && step.failure === "root")
|
|
262
|
+
throw error;
|
|
263
|
+
stdout = "";
|
|
264
|
+
}
|
|
265
|
+
if (!stdout)
|
|
266
|
+
stdout = text;
|
|
267
|
+
}
|
|
268
|
+
return stdout.trim() || text;
|
|
269
|
+
}
|
|
270
|
+
const result = await runVoiceReplyCommand("Outbound text template", options.handler, values, {
|
|
271
|
+
cwd: options.cwd,
|
|
272
|
+
timeout: getVoiceReplyTimeout(options.handler),
|
|
273
|
+
execCommand: options.execCommand,
|
|
274
|
+
stdin: text,
|
|
275
|
+
});
|
|
276
|
+
return result.stdout.trim() || text;
|
|
277
|
+
}
|
|
278
|
+
export async function transformTelegramOutboundText(text, options) {
|
|
279
|
+
let transformed = text;
|
|
280
|
+
for (const handler of findTelegramOutboundHandlers(options.handlers, "text")) {
|
|
281
|
+
try {
|
|
282
|
+
transformed = await transformTelegramOutboundTextWithHandler(transformed, {
|
|
283
|
+
handler,
|
|
284
|
+
cwd: options.cwd ?? process.cwd(),
|
|
285
|
+
execCommand: options.execCommand,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
catch (error) {
|
|
289
|
+
options.recordRuntimeEvent?.("outbound-text-handler", error, {
|
|
290
|
+
handler: outboundHandlerMatchesType(handler, "text")
|
|
291
|
+
? "text"
|
|
292
|
+
: "unknown",
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return transformed;
|
|
297
|
+
}
|
|
298
|
+
function isTelegramInlineKeyboardLike(replyMarkup) {
|
|
299
|
+
if (!replyMarkup || typeof replyMarkup !== "object")
|
|
300
|
+
return false;
|
|
301
|
+
const keyboard = replyMarkup
|
|
302
|
+
.inline_keyboard;
|
|
303
|
+
return Array.isArray(keyboard);
|
|
304
|
+
}
|
|
305
|
+
async function transformTelegramOutboundReplyMarkup(replyMarkup, options) {
|
|
306
|
+
if (!isTelegramInlineKeyboardLike(replyMarkup))
|
|
307
|
+
return replyMarkup;
|
|
308
|
+
const translatedRows = [];
|
|
309
|
+
for (const row of replyMarkup.inline_keyboard) {
|
|
310
|
+
const translatedRow = [];
|
|
311
|
+
for (const button of row) {
|
|
312
|
+
const text = await transformTelegramOutboundText(button.text, options);
|
|
313
|
+
translatedRow.push({ ...button, text });
|
|
314
|
+
}
|
|
315
|
+
translatedRows.push(translatedRow);
|
|
316
|
+
}
|
|
317
|
+
return { ...replyMarkup, inline_keyboard: translatedRows };
|
|
318
|
+
}
|
|
319
|
+
export async function transformTelegramOutboundTextReply(text, options) {
|
|
320
|
+
const transformOptions = {
|
|
321
|
+
handlers: options.handlers,
|
|
322
|
+
cwd: options.cwd,
|
|
323
|
+
execCommand: options.execCommand,
|
|
324
|
+
recordRuntimeEvent: options.recordRuntimeEvent,
|
|
325
|
+
};
|
|
326
|
+
const transformedText = await transformTelegramOutboundText(text, transformOptions);
|
|
327
|
+
const replyMarkup = await transformTelegramOutboundReplyMarkup(options.replyMarkup, transformOptions);
|
|
328
|
+
return { text: transformedText, ...(replyMarkup ? { replyMarkup } : {}) };
|
|
329
|
+
}
|
|
330
|
+
export function createTelegramOutboundTextReplyRuntime(deps) {
|
|
331
|
+
return {
|
|
332
|
+
sendTextReply: async (chatId, replyToMessageId, text, options) => {
|
|
333
|
+
const transformed = await transformTelegramOutboundText(text, {
|
|
334
|
+
handlers: deps.getHandlers?.(),
|
|
335
|
+
cwd: deps.cwd,
|
|
336
|
+
execCommand: deps.execCommand,
|
|
337
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
338
|
+
});
|
|
339
|
+
return deps.sendTextReply(chatId, replyToMessageId, transformed, options);
|
|
340
|
+
},
|
|
341
|
+
sendMarkdownReply: async (chatId, replyToMessageId, markdown, options) => {
|
|
342
|
+
const deliveryMarkdown = stripTelegramCommentMarkupForDelivery(markdown);
|
|
343
|
+
if (!deliveryMarkdown)
|
|
344
|
+
return undefined;
|
|
345
|
+
const transformed = await transformTelegramOutboundTextReply(deliveryMarkdown, {
|
|
346
|
+
handlers: deps.getHandlers?.(),
|
|
347
|
+
cwd: deps.cwd,
|
|
348
|
+
execCommand: deps.execCommand,
|
|
349
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
350
|
+
replyMarkup: options?.replyMarkup,
|
|
351
|
+
});
|
|
352
|
+
return deps.sendMarkdownReply(chatId, replyToMessageId, transformed.text, {
|
|
353
|
+
...options,
|
|
354
|
+
...(transformed.replyMarkup
|
|
355
|
+
? { replyMarkup: transformed.replyMarkup }
|
|
356
|
+
: {}),
|
|
357
|
+
});
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
export function createTelegramOutboundTextPreviewRuntime(deps) {
|
|
362
|
+
const wrap = (finalize) => async (chatId, markdown, replyToMessageId, options) => {
|
|
363
|
+
const transformed = await transformTelegramOutboundTextReply(markdown, {
|
|
364
|
+
handlers: deps.getHandlers?.(),
|
|
365
|
+
cwd: deps.cwd,
|
|
366
|
+
execCommand: deps.execCommand,
|
|
367
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
368
|
+
replyMarkup: options?.replyMarkup,
|
|
369
|
+
});
|
|
370
|
+
return finalize(chatId, transformed.text, replyToMessageId, {
|
|
371
|
+
...options,
|
|
372
|
+
...(transformed.replyMarkup
|
|
373
|
+
? { replyMarkup: transformed.replyMarkup }
|
|
374
|
+
: {}),
|
|
375
|
+
});
|
|
376
|
+
};
|
|
377
|
+
return {
|
|
378
|
+
finalizeMarkdownPreview: wrap(deps.finalizeMarkdownPreview),
|
|
379
|
+
preparePreviewDelivery(isDeliveryActive) {
|
|
380
|
+
const prepared = deps.preparePreviewDelivery?.(isDeliveryActive);
|
|
381
|
+
return prepared ? { ...prepared, finalizeMarkdownPreview: wrap(prepared.finalizeMarkdownPreview) } : undefined;
|
|
382
|
+
},
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
// --- Voice Policy Re-Exports ---
|
|
386
|
+
export { clearTelegramVoiceSynthesisProviders, clearTelegramVoiceTranscriptionProviders, computeVoicePromptContribution, computeVoiceTurnFlags, getTelegramVoiceReplyMode, getTelegramVoiceSynthesisProviders, getTelegramVoiceTranscriptionProviders, hasTelegramVoiceSynthesisProvider, hasTelegramVoiceTranscriptionProvider, isVoiceTurn, registerTelegramVoiceSynthesisProvider, registerTelegramVoiceTranscriptionProvider, shouldSuppressPreviewForVoice, } from "./voice.js";
|
|
387
|
+
export function createTelegramVoiceReplySender(deps) {
|
|
388
|
+
return createTelegramVoiceReplySenderWithPorts(deps, {
|
|
389
|
+
findVoiceHandlers: (handlers) => findTelegramOutboundHandlers(handlers, "voice"),
|
|
390
|
+
generateVoiceFile: (text, options) => generateTelegramVoiceReplyFile(text, {
|
|
391
|
+
lang: options.lang,
|
|
392
|
+
rate: options.rate,
|
|
393
|
+
handler: options.handler,
|
|
394
|
+
tempDir: options.tempDir,
|
|
395
|
+
cwd: options.cwd,
|
|
396
|
+
execCommand: options.execCommand,
|
|
397
|
+
}),
|
|
398
|
+
getProgrammaticVoiceHandlers: () => getTelegramOutboundProgrammaticHandlers("voice"),
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
export { createTelegramButtonActionStore, createTelegramButtonPromptTurn, createTelegramButtonReplyPlanner, handleTelegramButtonCallbackQuery, markTelegramButtonSelected, planTelegramButtonReply, } from "./outbound-buttons.js";
|
|
402
|
+
export function createTelegramOutboundReplyPlanner(store, getRenderingMode = () => "rich") {
|
|
403
|
+
return (markdown, options) => {
|
|
404
|
+
const buttonReply = planTelegramButtonReply(markdown, {
|
|
405
|
+
registerAction: store.register,
|
|
406
|
+
rendering: getRenderingMode(),
|
|
407
|
+
...(options?.binding ? { binding: options.binding } : {}),
|
|
408
|
+
});
|
|
409
|
+
// Button replies can also contain <!-- telegram_voice --> markup
|
|
410
|
+
const voiceReply = planTelegramVoiceReply(buttonReply.markdown);
|
|
411
|
+
return {
|
|
412
|
+
markdown: voiceReply.markdown,
|
|
413
|
+
...(buttonReply.replyMarkup
|
|
414
|
+
? { replyMarkup: buttonReply.replyMarkup }
|
|
415
|
+
: {}),
|
|
416
|
+
...(voiceReply.voiceText ? { voiceText: voiceReply.voiceText } : {}),
|
|
417
|
+
...(voiceReply.voiceReplies
|
|
418
|
+
? { voiceReplies: voiceReply.voiceReplies }
|
|
419
|
+
: {}),
|
|
420
|
+
...(voiceReply.lang ? { lang: voiceReply.lang } : {}),
|
|
421
|
+
...(voiceReply.rate ? { rate: voiceReply.rate } : {}),
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
/**
|
|
426
|
+
* Create an artifact sender that delivers planned voice replies for a turn.
|
|
427
|
+
* Iterates over `voiceReplies` (or a single `voiceText`) and sends each as
|
|
428
|
+
* a Telegram voice message via the voice reply sender. Throws if no voice
|
|
429
|
+
* reply could be delivered.
|
|
430
|
+
*/
|
|
431
|
+
// --- Outbound Reply Artifacts ---
|
|
432
|
+
export function createTelegramOutboundReplyArtifactSender(deps) {
|
|
433
|
+
return async (turn, plan, options) => {
|
|
434
|
+
const isDeliveryActive = () => deps.isDeliveryActive?.() !== false && options?.isDeliveryActive?.() !== false;
|
|
435
|
+
const sendVoiceReply = createTelegramVoiceReplySender({ ...deps, isDeliveryActive });
|
|
436
|
+
// Normalize voice replies: either use explicit voiceReplies array or fall back to voiceText
|
|
437
|
+
const voiceReplies = plan.voiceReplies?.length
|
|
438
|
+
? plan.voiceReplies
|
|
439
|
+
: plan.voiceText
|
|
440
|
+
? [{ text: plan.voiceText, lang: plan.lang, rate: plan.rate }]
|
|
441
|
+
: [];
|
|
442
|
+
let anyDelivered = false;
|
|
443
|
+
for (const reply of voiceReplies) {
|
|
444
|
+
if (!isDeliveryActive())
|
|
445
|
+
return;
|
|
446
|
+
try {
|
|
447
|
+
await sendVoiceReply(turn, reply.text, {
|
|
448
|
+
lang: reply.lang ?? plan.lang,
|
|
449
|
+
rate: reply.rate ?? plan.rate,
|
|
450
|
+
// Only attach reply parameters to the first voice message
|
|
451
|
+
replyToPrompt: options?.replyToPrompt === true && !anyDelivered,
|
|
452
|
+
replyMarkup: !anyDelivered ? plan.replyMarkup : undefined,
|
|
453
|
+
});
|
|
454
|
+
anyDelivered = true;
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
if (isTelegramApiCommitUnknownError(error))
|
|
458
|
+
throw error;
|
|
459
|
+
// sendVoiceReply already recorded the error; continue to next reply
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!isDeliveryActive())
|
|
463
|
+
return;
|
|
464
|
+
if (!anyDelivered) {
|
|
465
|
+
throw new Error("Failed to send voice reply: every voice synthesis provider failed.");
|
|
466
|
+
}
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
export function createTelegramAssistantOutputMutationFence(isAuthorityActive) {
|
|
470
|
+
return {
|
|
471
|
+
run(mutation, ...args) {
|
|
472
|
+
if (!isAuthorityActive()) {
|
|
473
|
+
return Promise.reject(new Error("Assistant output lost admission authority before transport mutation."));
|
|
474
|
+
}
|
|
475
|
+
return mutation(...args);
|
|
476
|
+
},
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
export function createTelegramAssistantOutputSender(deps) {
|
|
480
|
+
return async function sendAssistantOutput(event, authority, isAuthorityActive) {
|
|
481
|
+
const target = authority.target;
|
|
482
|
+
if (!target) {
|
|
483
|
+
throw new Error("Assistant output has no authorized Telegram target.");
|
|
484
|
+
}
|
|
485
|
+
const mutationFence = createTelegramAssistantOutputMutationFence(isAuthorityActive);
|
|
486
|
+
const replyRuntime = Replies.createTelegramRenderedMessageDeliveryRuntime({
|
|
487
|
+
recordOwnership: deps.recordOwnership,
|
|
488
|
+
sendMessage(body) {
|
|
489
|
+
return mutationFence.run(deps.sendMessage, body);
|
|
490
|
+
},
|
|
491
|
+
sendRichMessage(body) {
|
|
492
|
+
return mutationFence.run(deps.sendRichMessage, body);
|
|
493
|
+
},
|
|
494
|
+
getAssistantRenderingMode: deps.getAssistantRenderingMode,
|
|
495
|
+
editMessage(body) {
|
|
496
|
+
return mutationFence.run(deps.editMessage, body);
|
|
497
|
+
},
|
|
498
|
+
});
|
|
499
|
+
const outboundRuntime = createTelegramOutboundTextReplyRuntime({
|
|
500
|
+
sendTextReply: replyRuntime.sendTextReply,
|
|
501
|
+
sendMarkdownReply: replyRuntime.sendMarkdownReply,
|
|
502
|
+
execCommand: deps.execCommand,
|
|
503
|
+
getHandlers: deps.getHandlers,
|
|
504
|
+
recordRuntimeEvent: deps.recordRuntimeEvent,
|
|
505
|
+
});
|
|
506
|
+
const buttonReply = deps.planButtonReply?.(event.text) ?? {
|
|
507
|
+
markdown: event.text,
|
|
508
|
+
};
|
|
509
|
+
await outboundRuntime.sendMarkdownReply(target.chatId, event.source === "telegram" ? event.replyToMessageId : undefined, buttonReply.markdown, {
|
|
510
|
+
target,
|
|
511
|
+
...(buttonReply.replyMarkup
|
|
512
|
+
? { replyMarkup: buttonReply.replyMarkup }
|
|
513
|
+
: {}),
|
|
514
|
+
});
|
|
515
|
+
};
|
|
516
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Telegram message ownership helpers
|
|
3
|
+
* Zones: telegram routing, multi-instance bus, in-memory coordination
|
|
4
|
+
* Owns message-id ownership and live forwarding projections so inbound updates resolve back to the instance/target that owns them
|
|
5
|
+
*/
|
|
6
|
+
import { type TelegramBusFollowerView, type TelegramBusForwardOwnership } from "./bus.ts";
|
|
7
|
+
import { type TelegramTarget } from "./target.ts";
|
|
8
|
+
export interface TelegramMessageOwnershipRecord {
|
|
9
|
+
chatId: number;
|
|
10
|
+
messageId: number;
|
|
11
|
+
target: TelegramTarget;
|
|
12
|
+
instanceId: string;
|
|
13
|
+
profileKey?: string;
|
|
14
|
+
ownerGeneration?: string;
|
|
15
|
+
recipientBindingKey?: string;
|
|
16
|
+
createdAt: number;
|
|
17
|
+
updatedAt: number;
|
|
18
|
+
}
|
|
19
|
+
export interface TelegramMessageOwnershipStore {
|
|
20
|
+
record: (input: {
|
|
21
|
+
chatId: number;
|
|
22
|
+
messageId: number;
|
|
23
|
+
target?: TelegramTarget;
|
|
24
|
+
instanceId: string;
|
|
25
|
+
profileKey?: string;
|
|
26
|
+
ownerGeneration?: string;
|
|
27
|
+
recipientBindingKey?: string;
|
|
28
|
+
now?: number;
|
|
29
|
+
}) => TelegramMessageOwnershipRecord;
|
|
30
|
+
get: (chatId: number, messageId: number) => TelegramMessageOwnershipRecord | undefined;
|
|
31
|
+
forget: (chatId: number, messageId: number) => boolean;
|
|
32
|
+
forgetTarget: (target: TelegramTarget) => number;
|
|
33
|
+
prune: (options: {
|
|
34
|
+
now: number;
|
|
35
|
+
maxAgeMs?: number;
|
|
36
|
+
maxRecords?: number;
|
|
37
|
+
}) => number;
|
|
38
|
+
entries: () => TelegramMessageOwnershipRecord[];
|
|
39
|
+
clear: () => void;
|
|
40
|
+
}
|
|
41
|
+
export type TelegramFollowerOwnershipView = Pick<TelegramBusFollowerView, "instanceId" | "connectedAtMs" | "profileKey" | "registrationGeneration" | "protocol">;
|
|
42
|
+
export interface TelegramBusMessageOwnershipRuntime {
|
|
43
|
+
store: TelegramMessageOwnershipStore;
|
|
44
|
+
getForwardOwnership(chatId: number, messageId: number): TelegramBusForwardOwnership | undefined;
|
|
45
|
+
recordLocal(input: {
|
|
46
|
+
chatId: number;
|
|
47
|
+
messageId: number;
|
|
48
|
+
target?: TelegramTarget;
|
|
49
|
+
}): TelegramMessageOwnershipRecord;
|
|
50
|
+
recordRouted(input: {
|
|
51
|
+
chatId: number;
|
|
52
|
+
messageId: number;
|
|
53
|
+
target?: TelegramTarget;
|
|
54
|
+
instanceId: string;
|
|
55
|
+
}): TelegramMessageOwnershipRecord;
|
|
56
|
+
recordFollower(input: {
|
|
57
|
+
chatId: number;
|
|
58
|
+
messageId: number;
|
|
59
|
+
target?: TelegramTarget;
|
|
60
|
+
follower: TelegramFollowerOwnershipView;
|
|
61
|
+
}): TelegramMessageOwnershipRecord;
|
|
62
|
+
isOwnedByFollower(input: {
|
|
63
|
+
chatId: number;
|
|
64
|
+
messageId: number;
|
|
65
|
+
follower: TelegramFollowerOwnershipView;
|
|
66
|
+
}): boolean;
|
|
67
|
+
}
|
|
68
|
+
export declare function createTelegramBusMessageOwnershipRuntime(deps: {
|
|
69
|
+
instanceId: string;
|
|
70
|
+
getProfileKey(): string;
|
|
71
|
+
listFollowers(): readonly TelegramFollowerOwnershipView[];
|
|
72
|
+
}): TelegramBusMessageOwnershipRuntime;
|
|
73
|
+
export declare function createTelegramMessageOwnershipStore(options?: {
|
|
74
|
+
getProfileKey?: () => string | undefined;
|
|
75
|
+
isOwnerGenerationLive?: (record: TelegramMessageOwnershipRecord) => boolean;
|
|
76
|
+
resolveOwnerReplacement?: (record: TelegramMessageOwnershipRecord) => Pick<TelegramMessageOwnershipRecord, "instanceId" | "ownerGeneration" | "recipientBindingKey"> | undefined;
|
|
77
|
+
}): TelegramMessageOwnershipStore;
|