Files
dsh-telegram-channel/lib/bridge.d.ts
T
RRRosmontis cde6979f2d feat: resilient delivery, live progress visibility, TG-side answering, /stop /mission /new
Delivery & visibility:
- per-chunk retry with capped backoff on rich/HTML sends; rich failure now
  falls back to HTML instead of dropping the reply
- typing heartbeat: refresh sendChatAction every 4s for the whole turn
  (a single action expires in ~5s, long turns looked dead)
- turn/end error / abort / crash-recovery notices reach Telegram
  (API failures were previously invisible)
- tool/call progress summaries, tool/result failure notices, todo progress
- thinking indicator (one notice per reasoning phase, status only)
- notices prefixed '> ' render as Telegram blockquotes; plain-text fallback
- non-text messages get a hint instead of being silently dropped
- busy feedback: a followup queued mid-turn gets an explicit ack

TG-side answering (dual-path race; UI path untouched, first answer wins):
- ask_user_question questions render with letter options on TG; reply A /
  [A] / A,B (multi) / free text / '1: A' per-line for multi-question
- approval requests (approval/request waterfall) answerable from TG:
  [A] allow once / [B] reject; /cancel closes only the TG side

Commands: /stop (cancel current turn), /mission (todo snapshot + busy state),
/new (create a session in the current workspace and attach)

Persistence: chat→session bindings stored at $DSH_HOME/
telegram-channel-bindings.json (override: DSH_TELEGRAM_BINDINGS_FILE) and
restored on start — hot reloads and restarts no longer drop attachments.

Host seams without public surface yet are reached with documented structural
casts (userQuestions provider wrap, approval/request waterfall).

Tests: 41/41 pass. Two pre-existing environment failures in bridge.test.ts
(/last history mirror, assistant mirror) now pass: the former was fixed by
the rich→HTML fallback, the latter needed an event-loop flush after the
(void) listener call since resilient delivery lengthens the chain.
2026-09-01 16:06:00 +08:00

108 lines
3.8 KiB
TypeScript

import type { Context } from '@deepseek-ai/cordis';
import { type TelegramClientLike, type TelegramUpdate } from './client.js';
export interface TelegramBridgeOptions {
token: string;
allowedUserIds: number[];
allowAllUsers: boolean;
client?: TelegramClientLike;
sleep?: (ms: number) => Promise<void>;
maxMessageLength?: number;
pollingTimeoutSec?: number;
rendering?: 'rich' | 'html';
}
export declare class TelegramBridge {
private readonly ctx;
private readonly token;
private readonly allowedUserIds;
private readonly allowAllUsers;
private readonly client;
private readonly sleep;
private readonly maxMessageLength;
private renderingMode;
private readonly bindings;
private readonly pickers;
/** chatId → model awaiting reasoning-effort pick (kept outside picker so list refreshes won't drop it). */
private readonly pendingModels;
private polling;
private offset;
private pollPromise;
private pollAbort;
private disposeSessionListener;
/** sessionIds mid-turn (busy feedback, /stop state) */
private readonly busySessions;
/** chatId → typing heartbeat interval handle */
private readonly heartbeats;
/** chatId → serialized notice chain (prevents 429 storms) */
private readonly noticeQueue;
private readonly pendingAsks;
private hookTimer;
private readonly pendingApprovalsTG;
private disposeApprovalHook;
/** sessionId → thinking indicator state (one notice per reasoning phase) */
private readonly thinkingSessions;
/** callId → tool name (tool/result failure notices) */
private readonly callNames;
/** sessionId → latest todo snapshot (/mission) */
private readonly lastTodos;
constructor(ctx: Context, options: TelegramBridgeOptions);
start(): void;
stop(): Promise<void>;
processUpdate(update: TelegramUpdate): Promise<void>;
private handleCallback;
private resolveCatalog;
private sendWorkspacePicker;
private sendSessionPicker;
private bindSession;
private sendLastTurn;
private sendModelPicker;
private sendEffortPicker;
private applyModel;
private liveAgents;
private findLiveAgent;
/** Resume cold sessions when needed; never dispose the returned handle. */
private ensureLiveAgent;
private sendStatus;
private followupBound;
private pollLoop;
private interruptibleDelay;
private interruptibleSleep;
private onSessionEvent;
private deliver;
private deliverHtml;
/** Retry an outbound call with capped linear backoff (500ms, 1s, 2s… max 4s). */
private withRetry;
private escHtml;
/**
* Send a notice; a leading `> ` renders as a Telegram blockquote (HTML).
* Falls back to plain text when the HTML send fails — a notice is never lost.
*/
private deliverNotice;
/** Serialized per-chat notice chain — bursts can't race into Telegram 429s. */
private enqueueNotice;
private startTypingHeartbeat;
private stopTypingHeartbeat;
private stopAllHeartbeats;
private userQuestions;
/**
* Wrap the UI provider's ask() so Telegram gets a parallel answer path.
* `Promise.race` decides; the UI path is untouched. The TG promise NEVER
* settles when there is no bound chat — race would kill the UI's window
* with that early rejection.
*/
private hookUserQuestions;
private registerTgAsk;
private settleGuiSide;
private formatAskPending;
private handleTgAnswer;
private onApprovalRequest;
private handleTgApproval;
private stopBound;
private sendMission;
/** /new: create a session in the bound session's workspace and attach to it. */
private newSessionHere;
private bindingsPath;
private saveBindings;
private loadBindings;
private redact;
}