chore: fork @llblab/[email protected].0 as @evandrodevbr/oh-my-pi-telegram
Fork of upstream llblab/[email protected].0 (SHA afe09c5) repackaged as a drop-in extension for can1357/oh-my-pi (omp >= 16.5.2). Changes vs upstream: - Rename package: @llblab/pi-telegram -> @evandrodevbr/oh-my-pi-telegram - Bump version: 0.22.0 -> 0.22.0-evandro.1 - Update repository/homepage/bugs URLs to evandrodevbr - Add omp field in package.json (displayName + compatibleWith) - README: prepend 'Oh My Pi (omp) Compatibility' section with install commands, verified-components table, upstream lineage, and rationale - CHANGELOG: add '0.22.0-evandro.1' entry under Unreleased - docs/: rename @llblab/pi-telegram imports to @evandrodevbr/oh-my-pi-telegram - tests/public-api.test.ts: rename package import paths Gates re-run on the fork: - npm run typecheck -> tsc --noEmit (success) - npm test -> 1229 pass / 0 fail / 1 platform-only skip - npm run audit -> found 0 vulnerabilities - npm run pack:check -> evandrodevbr-oh-my-pi-telegram-0.22.0-evandro.1.tgz Verified: - omp install --link ./pi-telegram -> Linked OK - omp plugin list -> @evandrodevbr/[email protected].0-evandro.1 Upstream lineage: badlogic/pi-telegram -> llblab/pi-telegram (0.22.0) -> evandrodevbr/oh-my-pi-telegram (this fork)
This commit is contained in:
commit
d1d04001d0
155 files changed
+107848
No files matched your search
+828
@@ -0,0 +1,828 @@
|
||||
/**
|
||||
* Telegram inbound handler pipeline
|
||||
* Zones: telegram inbound, command templates, prompt preparation
|
||||
* Owns MIME/type matching, command-template execution, fallback handling, and prompt injection before prompt enqueueing
|
||||
*/
|
||||
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { basename } from "node:path";
|
||||
|
||||
import {
|
||||
buildCommandTemplateInvocation,
|
||||
expandCommandTemplateConfigs,
|
||||
normalizeCommandTemplateConfig,
|
||||
substituteCommandTemplateToken,
|
||||
type CommandTemplateConfig,
|
||||
type CommandTemplateObjectConfig,
|
||||
} from "./command-templates.ts";
|
||||
import { getTelegramVoiceTranscriptionProviders } from "./voice.ts";
|
||||
|
||||
const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
|
||||
const INBOUND_HANDLER_REGISTRY_KEY = "__piTelegramInboundHandlers__";
|
||||
const MAX_INBOUND_HANDLER_OUTPUT_LENGTH = 12_000;
|
||||
const MAX_INBOUND_HANDLER_FAILURE_STREAM_LENGTH = 4_000;
|
||||
|
||||
type TelegramInboundCommandTemplateConfig =
|
||||
| string
|
||||
| CommandTemplateObjectConfig;
|
||||
|
||||
export interface TelegramInboundHandlerConfig {
|
||||
match?: string | string[];
|
||||
mime?: string | string[];
|
||||
type?: string | string[];
|
||||
template?: string | TelegramInboundCommandTemplateConfig[];
|
||||
args?: string[];
|
||||
defaults?: Record<string, unknown>;
|
||||
timeout?: number | string;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerFile {
|
||||
path: string;
|
||||
fileName?: string;
|
||||
mimeType?: string;
|
||||
kind?: string;
|
||||
isImage?: boolean;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerOutput {
|
||||
file: TelegramInboundHandlerFile;
|
||||
output: string;
|
||||
handler: TelegramInboundHandlerConfig;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerProcessResult<
|
||||
TFile extends TelegramInboundHandlerFile = TelegramInboundHandlerFile,
|
||||
> {
|
||||
rawText: string;
|
||||
promptFiles: TFile[];
|
||||
handlerOutputs: string[];
|
||||
handledFiles: TelegramInboundHandlerOutput[];
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerExecOptions {
|
||||
cwd?: string;
|
||||
timeout?: number;
|
||||
signal?: AbortSignal;
|
||||
stdin?: string;
|
||||
retry?: number;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerExecResult {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
code: number;
|
||||
killed: boolean;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerRuntimeContext {
|
||||
cwd: string;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerRuntimeDeps<TContext> {
|
||||
getHandlers: () => TelegramInboundHandlerConfig[] | undefined;
|
||||
execCommand: (
|
||||
command: string,
|
||||
args: string[],
|
||||
options?: TelegramInboundHandlerExecOptions,
|
||||
) => Promise<TelegramInboundHandlerExecResult>;
|
||||
getCwd: (ctx: TContext) => string;
|
||||
recordRuntimeEvent?: (
|
||||
category: string,
|
||||
error: unknown,
|
||||
details?: Record<string, unknown>,
|
||||
) => void;
|
||||
}
|
||||
|
||||
export interface TelegramInboundHandlerRuntime<TContext> {
|
||||
process: <TFile extends TelegramInboundHandlerFile>(
|
||||
files: TFile[],
|
||||
rawText: string,
|
||||
ctx: TContext,
|
||||
) => Promise<TelegramInboundHandlerProcessResult<TFile>>;
|
||||
}
|
||||
|
||||
export type TelegramInboundProgrammaticHandlerResult =
|
||||
| string
|
||||
| { text: string }
|
||||
| undefined;
|
||||
|
||||
export interface TelegramInboundProgrammaticHandlerInput {
|
||||
kind: string;
|
||||
text?: string;
|
||||
file?: TelegramInboundHandlerFile;
|
||||
mimeType?: string;
|
||||
}
|
||||
|
||||
export type TelegramInboundProgrammaticHandler = (
|
||||
input: TelegramInboundProgrammaticHandlerInput,
|
||||
options?: { cwd?: string },
|
||||
) => Promise<TelegramInboundProgrammaticHandlerResult>;
|
||||
|
||||
export interface TelegramInboundHandlerRegistry {
|
||||
handlers: Map<string, TelegramInboundProgrammaticHandler[]>;
|
||||
}
|
||||
|
||||
interface InboundHandlerInvocation {
|
||||
command: string;
|
||||
args: string[];
|
||||
}
|
||||
|
||||
const BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES = 1_000_000;
|
||||
|
||||
function getOrCreateInboundHandlerRegistry(): TelegramInboundHandlerRegistry {
|
||||
const existing = (globalThis as Record<string, unknown>)[
|
||||
INBOUND_HANDLER_REGISTRY_KEY
|
||||
];
|
||||
if (
|
||||
existing &&
|
||||
typeof existing === "object" &&
|
||||
existing !== null &&
|
||||
"handlers" in existing &&
|
||||
existing.handlers instanceof Map
|
||||
) {
|
||||
return existing as TelegramInboundHandlerRegistry;
|
||||
}
|
||||
const registry: TelegramInboundHandlerRegistry = {
|
||||
handlers: new Map(),
|
||||
};
|
||||
(globalThis as Record<string, unknown>)[INBOUND_HANDLER_REGISTRY_KEY] =
|
||||
registry;
|
||||
return registry;
|
||||
}
|
||||
|
||||
export function registerTelegramInboundHandler(
|
||||
kind: string,
|
||||
handler: TelegramInboundProgrammaticHandler,
|
||||
): () => void {
|
||||
const normalizedKind = kind.trim() || "*";
|
||||
const registry = getOrCreateInboundHandlerRegistry();
|
||||
const list = registry.handlers.get(normalizedKind) ?? [];
|
||||
list.push(handler);
|
||||
registry.handlers.set(normalizedKind, list);
|
||||
return () => {
|
||||
const updated = registry.handlers.get(normalizedKind) ?? [];
|
||||
const index = updated.indexOf(handler);
|
||||
if (index !== -1) {
|
||||
updated.splice(index, 1);
|
||||
registry.handlers.set(normalizedKind, updated);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function getTelegramInboundProgrammaticHandlers(
|
||||
kind: string,
|
||||
): TelegramInboundProgrammaticHandler[] {
|
||||
const registry = getOrCreateInboundHandlerRegistry();
|
||||
return [
|
||||
...(registry.handlers.get(kind) ?? []),
|
||||
...(kind === "*" ? [] : (registry.handlers.get("*") ?? [])),
|
||||
];
|
||||
}
|
||||
|
||||
export function hasTelegramInboundHandler(kind?: string): boolean {
|
||||
const registry = getOrCreateInboundHandlerRegistry();
|
||||
if (kind !== undefined) return (registry.handlers.get(kind)?.length ?? 0) > 0;
|
||||
return Array.from(registry.handlers.values()).some((list) => list.length > 0);
|
||||
}
|
||||
|
||||
export function clearTelegramInboundHandlers(): void {
|
||||
getOrCreateInboundHandlerRegistry().handlers.clear();
|
||||
}
|
||||
|
||||
function truncateTelegramInboundText(text: string, maxLength: number): string {
|
||||
if (text.length <= maxLength) return text;
|
||||
return `${text.slice(0, maxLength).trimEnd()}… [truncated ${text.length - maxLength} chars]`;
|
||||
}
|
||||
|
||||
function truncateTelegramInboundOutput(text: string): string {
|
||||
return truncateTelegramInboundText(text, MAX_INBOUND_HANDLER_OUTPUT_LENGTH);
|
||||
}
|
||||
|
||||
function truncateTelegramInboundFailureStream(text: string): string {
|
||||
return truncateTelegramInboundText(
|
||||
text.trimEnd(),
|
||||
MAX_INBOUND_HANDLER_FAILURE_STREAM_LENGTH,
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeInboundProgrammaticHandlerText(
|
||||
result: TelegramInboundProgrammaticHandlerResult,
|
||||
): string | undefined {
|
||||
const text = typeof result === "string" ? result : result?.text;
|
||||
const normalized = text?.trim();
|
||||
return normalized ? truncateTelegramInboundOutput(normalized) : undefined;
|
||||
}
|
||||
|
||||
function normalizeStringList(value: string | string[] | undefined): string[] {
|
||||
if (Array.isArray(value)) {
|
||||
return value
|
||||
.map(String)
|
||||
.map((item) => item.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
if (typeof value === "string" && value.trim()) return [value.trim()];
|
||||
return [];
|
||||
}
|
||||
|
||||
function matchesWildcard(pattern: string, value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
const normalizedPattern = pattern.toLowerCase();
|
||||
const normalizedValue = value.toLowerCase();
|
||||
if (normalizedPattern === "*") return true;
|
||||
const escaped = normalizedPattern
|
||||
.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\\\*/g, ".*");
|
||||
return new RegExp(`^${escaped}$`).test(normalizedValue);
|
||||
}
|
||||
|
||||
function handlerHasSelectors(handler: TelegramInboundHandlerConfig): boolean {
|
||||
return (
|
||||
normalizeStringList(handler.match).length > 0 ||
|
||||
normalizeStringList(handler.mime).length > 0 ||
|
||||
normalizeStringList(handler.type).length > 0
|
||||
);
|
||||
}
|
||||
|
||||
function matchesAnyPattern(
|
||||
patterns: string[],
|
||||
value: string | undefined,
|
||||
): boolean {
|
||||
return patterns.some((pattern) => matchesWildcard(pattern, value));
|
||||
}
|
||||
|
||||
function isTelegramTextMimeType(mimeType: string | undefined): boolean {
|
||||
return matchesWildcard("text/*", mimeType);
|
||||
}
|
||||
|
||||
export function telegramInboundHandlerMatchesFile(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
file: TelegramInboundHandlerFile,
|
||||
): boolean {
|
||||
if (!handlerHasSelectors(handler)) return true;
|
||||
const matchPatterns = normalizeStringList(handler.match);
|
||||
const mimePatterns = normalizeStringList(handler.mime);
|
||||
const typePatterns = normalizeStringList(handler.type);
|
||||
if (matchesAnyPattern(mimePatterns, file.mimeType)) return true;
|
||||
if (matchesAnyPattern(typePatterns, file.kind)) return true;
|
||||
if (matchesAnyPattern(matchPatterns, file.mimeType)) return true;
|
||||
return matchesAnyPattern(matchPatterns, file.kind);
|
||||
}
|
||||
|
||||
export function findTelegramInboundHandlers(
|
||||
handlers: TelegramInboundHandlerConfig[] | undefined,
|
||||
file: TelegramInboundHandlerFile,
|
||||
): TelegramInboundHandlerConfig[] {
|
||||
if (!Array.isArray(handlers)) return [];
|
||||
return handlers.filter(
|
||||
(handler) =>
|
||||
!!handler &&
|
||||
typeof handler === "object" &&
|
||||
telegramInboundHandlerMatchesFile(handler, file),
|
||||
);
|
||||
}
|
||||
|
||||
export function findTelegramInboundHandler(
|
||||
handlers: TelegramInboundHandlerConfig[] | undefined,
|
||||
file: TelegramInboundHandlerFile,
|
||||
): TelegramInboundHandlerConfig | undefined {
|
||||
return findTelegramInboundHandlers(handlers, file)[0];
|
||||
}
|
||||
|
||||
function hasInboundFilePlaceholder(value: string): boolean {
|
||||
return /\{file\}/.test(value);
|
||||
}
|
||||
|
||||
function getTelegramInboundHandlerTemplateValues(
|
||||
file: TelegramInboundHandlerFile,
|
||||
text = "",
|
||||
): Record<string, string> {
|
||||
return {
|
||||
file: file.path,
|
||||
mime: file.mimeType ?? "",
|
||||
text,
|
||||
type: file.kind ?? "",
|
||||
};
|
||||
}
|
||||
|
||||
function buildTelegramInboundTemplateInvocation(
|
||||
handler: CommandTemplateConfig,
|
||||
file: TelegramInboundHandlerFile,
|
||||
cwd: string,
|
||||
appendFileIfMissing = true,
|
||||
): InboundHandlerInvocation {
|
||||
const values = getTelegramInboundHandlerTemplateValues(file);
|
||||
const templateConfig = normalizeCommandTemplateConfig(handler);
|
||||
const hadFilePlaceholder =
|
||||
typeof templateConfig.template === "string"
|
||||
? hasInboundFilePlaceholder(templateConfig.template)
|
||||
: false;
|
||||
const invocation = buildCommandTemplateInvocation(handler, values, cwd, {
|
||||
emptyMessage: "Inbound handler template is empty",
|
||||
missingLabel: "inbound handler template",
|
||||
});
|
||||
if (appendFileIfMissing && !hadFilePlaceholder)
|
||||
invocation.args.push(file.path);
|
||||
return invocation;
|
||||
}
|
||||
|
||||
export function buildTelegramInboundHandlerInvocation(
|
||||
handler: CommandTemplateConfig,
|
||||
file: TelegramInboundHandlerFile,
|
||||
cwd: string,
|
||||
appendFileIfMissing = true,
|
||||
): InboundHandlerInvocation {
|
||||
const { template } = normalizeCommandTemplateConfig(handler);
|
||||
if (!template) throw new Error("Inbound handler template is required");
|
||||
return buildTelegramInboundTemplateInvocation(
|
||||
handler,
|
||||
file,
|
||||
cwd,
|
||||
appendFileIfMissing,
|
||||
);
|
||||
}
|
||||
|
||||
function resolveTelegramInboundNumericControlField(
|
||||
value: number | string | undefined,
|
||||
values: Record<string, unknown>,
|
||||
label: string,
|
||||
): number | undefined {
|
||||
if (value === undefined) return undefined;
|
||||
const resolved =
|
||||
typeof value === "string"
|
||||
? substituteCommandTemplateToken(value, values, label)
|
||||
: value;
|
||||
if (resolved === "") return undefined;
|
||||
const numeric = Number(resolved);
|
||||
if (!Number.isFinite(numeric) || numeric < 0)
|
||||
throw new Error(`Command template ${label} must be a non-negative number.`);
|
||||
return numeric;
|
||||
}
|
||||
|
||||
function getTelegramInboundHandlerConfiguredTimeout(
|
||||
handler: TelegramInboundCommandTemplateConfig,
|
||||
): number | undefined {
|
||||
const timeout = typeof handler === "string" ? undefined : handler.timeout;
|
||||
return resolveTelegramInboundNumericControlField(timeout, {}, "timeout");
|
||||
}
|
||||
|
||||
function getTelegramInboundHandlerTimeout(
|
||||
handler: TelegramInboundCommandTemplateConfig,
|
||||
): number {
|
||||
return (
|
||||
getTelegramInboundHandlerConfiguredTimeout(handler) ??
|
||||
DEFAULT_INBOUND_HANDLER_TIMEOUT_MS
|
||||
);
|
||||
}
|
||||
|
||||
function getRemainingTelegramInboundTimeout(
|
||||
timeout: number,
|
||||
startedAt: number,
|
||||
): number {
|
||||
return Math.max(1, timeout - (Date.now() - startedAt));
|
||||
}
|
||||
|
||||
function getTelegramInboundInitialCompositionStepTimeout(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
step: TelegramInboundCommandTemplateConfig,
|
||||
): number {
|
||||
const timeout = getTelegramInboundHandlerTimeout(handler);
|
||||
const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
|
||||
return stepTimeout === undefined ? timeout : Math.min(stepTimeout, timeout);
|
||||
}
|
||||
|
||||
function getTelegramInboundCompositionStepTimeout(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
step: TelegramInboundCommandTemplateConfig,
|
||||
startedAt: number,
|
||||
): number {
|
||||
const remaining = getRemainingTelegramInboundTimeout(
|
||||
getTelegramInboundHandlerTimeout(handler),
|
||||
startedAt,
|
||||
);
|
||||
const stepTimeout = getTelegramInboundHandlerConfiguredTimeout(step);
|
||||
return stepTimeout === undefined
|
||||
? remaining
|
||||
: Math.min(stepTimeout, remaining);
|
||||
}
|
||||
|
||||
function getTelegramInboundHandlerKind(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
): string {
|
||||
if (Array.isArray(handler.template)) return "composition";
|
||||
if (handler.template) return "template";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function formatTelegramInboundHandlerFailure(
|
||||
result: TelegramInboundHandlerExecResult,
|
||||
): string {
|
||||
const parts = [
|
||||
`Inbound handler exited with code ${result.code}${result.killed ? " (killed)" : ""}`,
|
||||
];
|
||||
if (result.stderr.trim())
|
||||
parts.push(`stderr:\n${truncateTelegramInboundFailureStream(result.stderr)}`);
|
||||
if (result.stdout.trim())
|
||||
parts.push(`stdout:\n${truncateTelegramInboundFailureStream(result.stdout)}`);
|
||||
return parts.join("\n\n");
|
||||
}
|
||||
|
||||
async function executeTelegramInboundHandlerInvocation(
|
||||
handler: TelegramInboundCommandTemplateConfig,
|
||||
file: TelegramInboundHandlerFile,
|
||||
cwd: string,
|
||||
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
||||
appendFileIfMissing = true,
|
||||
timeout = getTelegramInboundHandlerTimeout(handler),
|
||||
stdin?: string,
|
||||
): Promise<string> {
|
||||
const invocation = buildTelegramInboundHandlerInvocation(
|
||||
handler,
|
||||
file,
|
||||
cwd,
|
||||
appendFileIfMissing,
|
||||
);
|
||||
const result = await deps.execCommand(invocation.command, invocation.args, {
|
||||
cwd,
|
||||
timeout,
|
||||
...(typeof handler === "object" && handler.retry !== undefined
|
||||
? { retry: resolveTelegramInboundNumericControlField(handler.retry, {}, "retry") }
|
||||
: {}),
|
||||
...(stdin !== undefined ? { stdin } : {}),
|
||||
});
|
||||
if (result.code !== 0)
|
||||
throw new Error(formatTelegramInboundHandlerFailure(result));
|
||||
return truncateTelegramInboundOutput(result.stdout);
|
||||
}
|
||||
|
||||
function getTelegramInboundHandlerCompositionSteps(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
): TelegramInboundCommandTemplateConfig[] {
|
||||
if (Array.isArray(handler.template)) {
|
||||
return expandCommandTemplateConfigs(
|
||||
handler,
|
||||
) as TelegramInboundCommandTemplateConfig[];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function getTelegramTextHandlerFile(): TelegramInboundHandlerFile {
|
||||
return {
|
||||
path: "",
|
||||
fileName: "message.txt",
|
||||
mimeType: "text/plain",
|
||||
kind: "text",
|
||||
isImage: false,
|
||||
};
|
||||
}
|
||||
|
||||
function findTelegramTextHandlers(
|
||||
handlers: TelegramInboundHandlerConfig[] | undefined,
|
||||
): TelegramInboundHandlerConfig[] {
|
||||
if (!Array.isArray(handlers)) return [];
|
||||
const textFile = getTelegramTextHandlerFile();
|
||||
return handlers.filter(
|
||||
(handler) =>
|
||||
!!handler &&
|
||||
typeof handler === "object" &&
|
||||
handlerHasSelectors(handler) &&
|
||||
telegramInboundHandlerMatchesFile(handler, textFile),
|
||||
);
|
||||
}
|
||||
|
||||
function buildTelegramTextHandlerInvocation(
|
||||
handler: CommandTemplateConfig,
|
||||
text: string,
|
||||
cwd: string,
|
||||
): InboundHandlerInvocation {
|
||||
const values = getTelegramInboundHandlerTemplateValues(
|
||||
getTelegramTextHandlerFile(),
|
||||
text,
|
||||
);
|
||||
const { template } = normalizeCommandTemplateConfig(handler);
|
||||
if (!template) throw new Error("Text handler template is required");
|
||||
return buildCommandTemplateInvocation(handler, values, cwd, {
|
||||
emptyMessage: "Text handler template is empty",
|
||||
missingLabel: "text handler template",
|
||||
});
|
||||
}
|
||||
|
||||
async function executeTelegramTextHandlerInvocation(
|
||||
handler: TelegramInboundCommandTemplateConfig,
|
||||
text: string,
|
||||
cwd: string,
|
||||
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
||||
timeout = getTelegramInboundHandlerTimeout(handler),
|
||||
): Promise<string> {
|
||||
const invocation = buildTelegramTextHandlerInvocation(handler, text, cwd);
|
||||
const result = await deps.execCommand(invocation.command, invocation.args, {
|
||||
cwd,
|
||||
timeout,
|
||||
stdin: text,
|
||||
...(typeof handler === "object" && handler.retry !== undefined
|
||||
? { retry: resolveTelegramInboundNumericControlField(handler.retry, {}, "retry") }
|
||||
: {}),
|
||||
});
|
||||
if (result.code !== 0)
|
||||
throw new Error(formatTelegramInboundHandlerFailure(result));
|
||||
return truncateTelegramInboundOutput(result.stdout);
|
||||
}
|
||||
|
||||
async function executeTelegramTextHandler(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
text: string,
|
||||
cwd: string,
|
||||
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
||||
): Promise<string> {
|
||||
const steps = getTelegramInboundHandlerCompositionSteps(handler);
|
||||
if (steps.length === 0) {
|
||||
return (
|
||||
await executeTelegramTextHandlerInvocation(handler, text, cwd, deps)
|
||||
).trim();
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
let output = text;
|
||||
for (const [index, step] of steps.entries()) {
|
||||
try {
|
||||
output = await executeTelegramTextHandlerInvocation(
|
||||
step,
|
||||
output,
|
||||
cwd,
|
||||
deps,
|
||||
index === 0
|
||||
? getTelegramInboundInitialCompositionStepTimeout(handler, step)
|
||||
: getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
||||
);
|
||||
} catch (error) {
|
||||
if (typeof step === "object" && step.failure === "root") throw error;
|
||||
output = "";
|
||||
}
|
||||
if (index > 0 && !output) output = text;
|
||||
}
|
||||
return output.trim();
|
||||
}
|
||||
|
||||
async function processTelegramTextHandlers(options: {
|
||||
rawText: string;
|
||||
handlers?: TelegramInboundHandlerConfig[];
|
||||
cwd: string;
|
||||
execCommand: TelegramInboundHandlerRuntimeDeps<unknown>["execCommand"];
|
||||
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
||||
}): Promise<string> {
|
||||
if (!options.rawText) return options.rawText;
|
||||
let text = options.rawText;
|
||||
for (const handler of findTelegramTextHandlers(options.handlers)) {
|
||||
try {
|
||||
const output = await executeTelegramTextHandler(
|
||||
handler,
|
||||
text,
|
||||
options.cwd,
|
||||
options,
|
||||
);
|
||||
if (output) text = output;
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("inbound-text-handler", error, {
|
||||
handler: getTelegramInboundHandlerKind(handler),
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const handler of getTelegramInboundProgrammaticHandlers("text")) {
|
||||
try {
|
||||
const output = normalizeInboundProgrammaticHandlerText(
|
||||
await handler(
|
||||
{ kind: "text", text, mimeType: "text/plain" },
|
||||
{ cwd: options.cwd },
|
||||
),
|
||||
);
|
||||
if (output) text = output;
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
|
||||
kind: "text",
|
||||
});
|
||||
}
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
function isTelegramVoiceLikeFile(file: TelegramInboundHandlerFile): boolean {
|
||||
return (
|
||||
file.kind === "voice" ||
|
||||
file.kind === "audio" ||
|
||||
matchesWildcard("audio/*", file.mimeType)
|
||||
);
|
||||
}
|
||||
|
||||
async function processTelegramFileWithProgrammaticHandlers(
|
||||
file: TelegramInboundHandlerFile,
|
||||
options: {
|
||||
cwd: string;
|
||||
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
||||
},
|
||||
): Promise<string | undefined> {
|
||||
const kind = file.kind || "*";
|
||||
for (const handler of getTelegramInboundProgrammaticHandlers(kind)) {
|
||||
try {
|
||||
const output = normalizeInboundProgrammaticHandlerText(
|
||||
await handler(
|
||||
{
|
||||
kind,
|
||||
file,
|
||||
mimeType: file.mimeType,
|
||||
},
|
||||
{ cwd: options.cwd },
|
||||
),
|
||||
);
|
||||
if (output) return output;
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
|
||||
fileName: file.fileName || basename(file.path),
|
||||
kind,
|
||||
});
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function transcribeTelegramVoiceFileWithProviders(
|
||||
file: TelegramInboundHandlerFile,
|
||||
options: {
|
||||
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
||||
},
|
||||
): Promise<string | undefined> {
|
||||
if (!isTelegramVoiceLikeFile(file)) return undefined;
|
||||
for (const provider of getTelegramVoiceTranscriptionProviders()) {
|
||||
try {
|
||||
const result = await provider(file, {});
|
||||
const text = typeof result === "string" ? result : result?.text;
|
||||
if (text?.trim()) return truncateTelegramInboundOutput(text.trim());
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("voice-transcription-provider", error, {
|
||||
fileName: file.fileName || basename(file.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function readBuiltInTelegramTextAttachment(
|
||||
file: TelegramInboundHandlerFile,
|
||||
): Promise<string | undefined> {
|
||||
if (!isTelegramTextMimeType(file.mimeType)) return undefined;
|
||||
const content = await readFile(file.path, "utf8");
|
||||
const normalized = content.trim();
|
||||
if (
|
||||
!normalized ||
|
||||
Buffer.byteLength(normalized, "utf8") > BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
const name = file.fileName || basename(file.path);
|
||||
return truncateTelegramInboundOutput(`[${name}]\n${normalized}`);
|
||||
}
|
||||
|
||||
async function executeTelegramInboundHandler(
|
||||
handler: TelegramInboundHandlerConfig,
|
||||
file: TelegramInboundHandlerFile,
|
||||
cwd: string,
|
||||
deps: Pick<TelegramInboundHandlerRuntimeDeps<unknown>, "execCommand">,
|
||||
): Promise<string> {
|
||||
const steps = getTelegramInboundHandlerCompositionSteps(handler);
|
||||
if (steps.length === 0) {
|
||||
const output = await executeTelegramInboundHandlerInvocation(
|
||||
handler,
|
||||
file,
|
||||
cwd,
|
||||
deps,
|
||||
);
|
||||
return output.trim();
|
||||
}
|
||||
const startedAt = Date.now();
|
||||
let output = "";
|
||||
for (const [index, step] of steps.entries()) {
|
||||
try {
|
||||
output = await executeTelegramInboundHandlerInvocation(
|
||||
step,
|
||||
file,
|
||||
cwd,
|
||||
deps,
|
||||
false,
|
||||
index === 0
|
||||
? getTelegramInboundInitialCompositionStepTimeout(handler, step)
|
||||
: getTelegramInboundCompositionStepTimeout(handler, step, startedAt),
|
||||
index === 0 ? undefined : output,
|
||||
);
|
||||
} catch (error) {
|
||||
if (typeof step === "object" && step.failure === "root") throw error;
|
||||
output = "";
|
||||
}
|
||||
}
|
||||
return output.trim();
|
||||
}
|
||||
|
||||
export async function processTelegramInboundHandlers<
|
||||
TFile extends TelegramInboundHandlerFile,
|
||||
>(options: {
|
||||
files: TFile[];
|
||||
rawText: string;
|
||||
handlers?: TelegramInboundHandlerConfig[];
|
||||
cwd: string;
|
||||
execCommand: TelegramInboundHandlerRuntimeDeps<unknown>["execCommand"];
|
||||
recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
|
||||
}): Promise<TelegramInboundHandlerProcessResult<TFile>> {
|
||||
const rawText = await processTelegramTextHandlers({
|
||||
rawText: options.rawText,
|
||||
handlers: options.handlers,
|
||||
cwd: options.cwd,
|
||||
execCommand: options.execCommand,
|
||||
recordRuntimeEvent: options.recordRuntimeEvent,
|
||||
});
|
||||
const promptFiles: TFile[] = [...options.files];
|
||||
const outputs: TelegramInboundHandlerOutput[] = [];
|
||||
for (const file of options.files) {
|
||||
let hasOutput = false;
|
||||
const handlers = findTelegramInboundHandlers(options.handlers, file);
|
||||
for (const handler of handlers) {
|
||||
try {
|
||||
const output = await executeTelegramInboundHandler(
|
||||
handler,
|
||||
file,
|
||||
options.cwd,
|
||||
options,
|
||||
);
|
||||
if (output) {
|
||||
outputs.push({ file, output, handler });
|
||||
hasOutput = true;
|
||||
}
|
||||
break;
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("inbound-handler", error, {
|
||||
fileName: file.fileName || basename(file.path),
|
||||
handler: getTelegramInboundHandlerKind(handler),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!hasOutput) {
|
||||
try {
|
||||
const output = await processTelegramFileWithProgrammaticHandlers(file, {
|
||||
cwd: options.cwd,
|
||||
recordRuntimeEvent: options.recordRuntimeEvent,
|
||||
});
|
||||
if (output) {
|
||||
outputs.push({ file, output, handler: { type: "programmatic" } });
|
||||
hasOutput = true;
|
||||
}
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
|
||||
fileName: file.fileName || basename(file.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!hasOutput) {
|
||||
try {
|
||||
const output = await transcribeTelegramVoiceFileWithProviders(file, {
|
||||
recordRuntimeEvent: options.recordRuntimeEvent,
|
||||
});
|
||||
if (output) {
|
||||
outputs.push({ file, output, handler: { type: "voice-provider" } });
|
||||
hasOutput = true;
|
||||
}
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("voice-transcription-provider", error, {
|
||||
fileName: file.fileName || basename(file.path),
|
||||
});
|
||||
}
|
||||
}
|
||||
if (!hasOutput) {
|
||||
try {
|
||||
const output = await readBuiltInTelegramTextAttachment(file);
|
||||
if (output) outputs.push({ file, output, handler: { type: "text" } });
|
||||
} catch (error) {
|
||||
options.recordRuntimeEvent?.("inbound-handler", error, {
|
||||
fileName: file.fileName || basename(file.path),
|
||||
handler: "built-in-text",
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
rawText,
|
||||
promptFiles,
|
||||
handlerOutputs: outputs.map((output) => output.output),
|
||||
handledFiles: outputs,
|
||||
};
|
||||
}
|
||||
|
||||
export function createTelegramInboundHandlerRuntime<TContext>(
|
||||
deps: TelegramInboundHandlerRuntimeDeps<TContext>,
|
||||
): TelegramInboundHandlerRuntime<TContext> {
|
||||
return {
|
||||
process: (files, rawText, ctx) =>
|
||||
processTelegramInboundHandlers({
|
||||
files,
|
||||
rawText,
|
||||
handlers: deps.getHandlers(),
|
||||
cwd: deps.getCwd(ctx),
|
||||
execCommand: deps.execCommand,
|
||||
recordRuntimeEvent: deps.recordRuntimeEvent,
|
||||
}),
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user