mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
refactor: simplify Telegram preview streaming to single boolean (#22012)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: a4017d3b94
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Security/Unused Dependencies: remove unused plugin-local `openclaw` devDependencies from `extensions/open-prose`, `extensions/lobster`, and `extensions/llm-task` after removing this dependency from build-time requirements. (#22495) Thanks @vincentkoc.
|
||||
- Agents/Subagents: default subagent spawn depth now uses shared `maxSpawnDepth=2`, enabling depth-1 orchestrator spawning by default while keeping depth policy checks consistent across spawn and prompt paths. (#22223) Thanks @tyler6204.
|
||||
- Channels/CLI: add per-account/channel `defaultTo` outbound routing fallback so `openclaw agent --deliver` can send without explicit `--reply-to` when a default target is configured. (#16985) Thanks @KirillShchetinin.
|
||||
- Telegram/Streaming: simplify preview streaming config to `channels.telegram.streaming` (boolean), auto-map legacy `streamMode` values, and remove block-vs-partial preview branching. (#22012) thanks @obviyus.
|
||||
- iOS/Chat: clean chat UI noise by stripping inbound untrusted metadata/timestamp prefixes, formatting tool outputs into concise summaries/errors, compacting the composer while typing, and supporting tap-to-dismiss keyboard in chat view. (#22122) thanks @mbelinky.
|
||||
- iOS/Watch: bridge mirrored watch prompt notification actions into iOS quick-reply handling, including queued action handoff until app model initialization. (#22123) thanks @mbelinky.
|
||||
- iOS/Tests: cover IPv4-mapped IPv6 loopback in manual TLS policy tests for connect validation paths. (#22045) Thanks @mbelinky.
|
||||
|
||||
@@ -21,7 +21,7 @@ title: grammY
|
||||
- **Webhook support:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown. Gateway enables webhook mode when `channels.telegram.webhookUrl` + `channels.telegram.webhookSecret` are set (otherwise it long-polls).
|
||||
- **Sessions:** direct chats collapse into the agent main session (`agent:<agentId>:<mainKey>`); groups use `agent:<agentId>:telegram:group:<chatId>`; replies route back to the same channel.
|
||||
- **Config knobs:** `channels.telegram.botToken`, `channels.telegram.dmPolicy`, `channels.telegram.groups` (allowlist + mention defaults), `channels.telegram.allowFrom`, `channels.telegram.groupAllowFrom`, `channels.telegram.groupPolicy`, `channels.telegram.mediaMaxMb`, `channels.telegram.linkPreview`, `channels.telegram.proxy`, `channels.telegram.webhookSecret`, `channels.telegram.webhookUrl`, `channels.telegram.webhookHost`.
|
||||
- **Live stream preview:** optional `channels.telegram.streamMode` sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming.
|
||||
- **Live stream preview:** optional `channels.telegram.streaming` sends a temporary message and updates it with `editMessageText`. This is separate from channel block streaming.
|
||||
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
|
||||
|
||||
Open questions
|
||||
|
||||
@@ -226,21 +226,8 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
Requirement:
|
||||
|
||||
- `channels.telegram.streamMode` is not `"off"` (default: `"partial"`)
|
||||
|
||||
Modes:
|
||||
|
||||
- `off`: no live preview
|
||||
- `partial`: frequent preview updates from partial text
|
||||
- `block`: chunked preview updates using `channels.telegram.draftChunk`
|
||||
|
||||
`draftChunk` defaults for `streamMode: "block"`:
|
||||
|
||||
- `minChars: 200`
|
||||
- `maxChars: 800`
|
||||
- `breakPreference: "paragraph"`
|
||||
|
||||
`maxChars` is clamped by `channels.telegram.textChunkLimit`.
|
||||
- `channels.telegram.streaming` is `true` (default)
|
||||
- legacy `channels.telegram.streamMode` values are auto-mapped to `streaming`
|
||||
|
||||
This works in direct chats and groups/topics.
|
||||
|
||||
@@ -248,7 +235,7 @@ curl "https://api.telegram.org/bot<bot_token>/getUpdates"
|
||||
|
||||
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
|
||||
|
||||
`streamMode` is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming.
|
||||
Preview streaming is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming.
|
||||
|
||||
Telegram-only reasoning stream:
|
||||
|
||||
@@ -721,7 +708,7 @@ Primary reference:
|
||||
- `channels.telegram.textChunkLimit`: outbound chunk size (chars).
|
||||
- `channels.telegram.chunkMode`: `length` (default) or `newline` to split on blank lines (paragraph boundaries) before length chunking.
|
||||
- `channels.telegram.linkPreview`: toggle link previews for outbound messages (default: true).
|
||||
- `channels.telegram.streamMode`: `off | partial | block` (live stream preview).
|
||||
- `channels.telegram.streaming`: `true | false` (live stream preview; default: true).
|
||||
- `channels.telegram.mediaMaxMb`: inbound/outbound media cap (MB).
|
||||
- `channels.telegram.retry`: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).
|
||||
- `channels.telegram.network.autoSelectFamily`: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.
|
||||
@@ -745,7 +732,7 @@ Telegram-specific high-signal fields:
|
||||
- access control: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`, `groups.*.topics.*`
|
||||
- command/menu: `commands.native`, `customCommands`
|
||||
- threading/replies: `replyToMode`
|
||||
- streaming: `streamMode` (preview), `draftChunk`, `blockStreaming`
|
||||
- streaming: `streaming` (preview), `blockStreaming`
|
||||
- formatting/delivery: `textChunkLimit`, `chunkMode`, `linkPreview`, `responsePrefix`
|
||||
- media/network: `mediaMaxMb`, `timeoutSeconds`, `retry`, `network.autoSelectFamily`, `proxy`
|
||||
- webhook: `webhookUrl`, `webhookSecret`, `webhookPath`, `webhookHost`
|
||||
|
||||
@@ -100,7 +100,7 @@ This maps to:
|
||||
|
||||
**Channel note:** For non-Telegram channels, block streaming is **off unless**
|
||||
`*.blockStreaming` is explicitly set to `true`. Telegram can stream a live preview
|
||||
(`channels.telegram.streamMode`) without block replies.
|
||||
(`channels.telegram.streaming`) without block replies.
|
||||
|
||||
Config location reminder: the `blockStreaming*` defaults live under
|
||||
`agents.defaults`, not the root config.
|
||||
@@ -110,11 +110,7 @@ Config location reminder: the `blockStreaming*` defaults live under
|
||||
Telegram is the only channel with live preview streaming:
|
||||
|
||||
- Uses Bot API `sendMessage` (first update) + `editMessageText` (subsequent updates).
|
||||
- `channels.telegram.streamMode: "partial" | "block" | "off"`.
|
||||
- `partial`: preview updates with latest stream text.
|
||||
- `block`: preview updates in chunked blocks (same chunker rules).
|
||||
- `off`: no preview streaming.
|
||||
- Preview chunk config (only for `streamMode: "block"`): `channels.telegram.draftChunk` (defaults: `minChars: 200`, `maxChars: 800`).
|
||||
- `channels.telegram.streaming: true | false` (default: `true`).
|
||||
- Preview streaming is separate from block streaming.
|
||||
- When Telegram block streaming is explicitly enabled, preview streaming is skipped to avoid double-streaming.
|
||||
- Text-only finals are applied by editing the preview message in place.
|
||||
@@ -124,8 +120,7 @@ Telegram is the only channel with live preview streaming:
|
||||
```
|
||||
Telegram
|
||||
└─ sendMessage (temporary preview message)
|
||||
├─ streamMode=partial → edit latest text
|
||||
└─ streamMode=block → chunker + edit updates
|
||||
└─ streaming=true → edit latest text
|
||||
└─ final text-only reply → final edit on same message
|
||||
└─ fallback: cleanup preview + normal final delivery (media/complex)
|
||||
```
|
||||
|
||||
@@ -151,12 +151,7 @@ WhatsApp runs through the gateway's web channel (Baileys Web). It starts automat
|
||||
historyLimit: 50,
|
||||
replyToMode: "first", // off | first | all
|
||||
linkPreview: true,
|
||||
streamMode: "partial", // off | partial | block
|
||||
draftChunk: {
|
||||
minChars: 200,
|
||||
maxChars: 800,
|
||||
breakPreference: "paragraph", // paragraph | newline | sentence
|
||||
},
|
||||
streaming: true, // live preview on/off (default true)
|
||||
actions: { reactions: true, sendMessage: true },
|
||||
reactionNotifications: "own", // off | own | all
|
||||
mediaMaxMb: 5,
|
||||
|
||||
@@ -378,11 +378,46 @@ describe("legacy config detection", () => {
|
||||
expect(res.config.channels?.telegram?.groupPolicy).toBe("allowlist");
|
||||
}
|
||||
});
|
||||
it("defaults telegram.streamMode to partial when telegram section exists", async () => {
|
||||
it("defaults telegram.streaming to true when telegram section exists", async () => {
|
||||
const res = validateConfigObject({ channels: { telegram: {} } });
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.telegram?.streamMode).toBe("partial");
|
||||
expect(res.config.channels?.telegram?.streaming).toBe(true);
|
||||
expect(res.config.channels?.telegram?.streamMode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
it("migrates legacy telegram.streamMode=off to streaming=false", async () => {
|
||||
const res = validateConfigObject({ channels: { telegram: { streamMode: "off" } } });
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.telegram?.streaming).toBe(false);
|
||||
expect(res.config.channels?.telegram?.streamMode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
it("migrates legacy telegram.streamMode=block to streaming=true", async () => {
|
||||
const res = validateConfigObject({ channels: { telegram: { streamMode: "block" } } });
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.telegram?.streaming).toBe(true);
|
||||
expect(res.config.channels?.telegram?.streamMode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
it("migrates legacy telegram.accounts.*.streamMode to streaming", async () => {
|
||||
const res = validateConfigObject({
|
||||
channels: {
|
||||
telegram: {
|
||||
accounts: {
|
||||
ops: {
|
||||
streamMode: "off",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.telegram?.accounts?.ops?.streaming).toBe(false);
|
||||
expect(res.config.channels?.telegram?.accounts?.ops?.streamMode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
it('rejects whatsapp.dmPolicy="open" without allowFrom "*"', async () => {
|
||||
|
||||
@@ -391,14 +391,8 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"Debounce window (ms) for batching rapid inbound messages from the same sender (0 to disable).",
|
||||
"channels.telegram.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
|
||||
"channels.telegram.streamMode":
|
||||
"Live stream preview mode for Telegram replies (off | partial | block). Separate from block streaming; uses sendMessage + editMessageText.",
|
||||
"channels.telegram.draftChunk.minChars":
|
||||
'Minimum chars before emitting a Telegram stream preview update when channels.telegram.streamMode="block" (default: 200).',
|
||||
"channels.telegram.draftChunk.maxChars":
|
||||
'Target max size for a Telegram stream preview chunk when channels.telegram.streamMode="block" (default: 800; clamped to channels.telegram.textChunkLimit).',
|
||||
"channels.telegram.draftChunk.breakPreference":
|
||||
"Preferred breakpoints for Telegram draft chunks (paragraph | newline | sentence). Default: paragraph.",
|
||||
"channels.telegram.streaming":
|
||||
"Enable Telegram live stream preview via message edits (default: true; legacy streamMode auto-maps here).",
|
||||
"channels.discord.streamMode":
|
||||
"Live stream preview mode for Discord replies (off | partial | block). Separate from block streaming; uses sendMessage + editMessage.",
|
||||
"channels.discord.draftChunk.minChars":
|
||||
|
||||
@@ -263,10 +263,7 @@ export const FIELD_LABELS: Record<string, string> = {
|
||||
...IRC_FIELD_LABELS,
|
||||
"channels.telegram.botToken": "Telegram Bot Token",
|
||||
"channels.telegram.dmPolicy": "Telegram DM Policy",
|
||||
"channels.telegram.streamMode": "Telegram Stream Mode",
|
||||
"channels.telegram.draftChunk.minChars": "Telegram Draft Chunk Min Chars",
|
||||
"channels.telegram.draftChunk.maxChars": "Telegram Draft Chunk Max Chars",
|
||||
"channels.telegram.draftChunk.breakPreference": "Telegram Draft Chunk Break Preference",
|
||||
"channels.telegram.streaming": "Telegram Streaming",
|
||||
"channels.telegram.retry.attempts": "Telegram Retry Attempts",
|
||||
"channels.telegram.retry.minDelayMs": "Telegram Retry Min Delay (ms)",
|
||||
"channels.telegram.retry.maxDelayMs": "Telegram Retry Max Delay (ms)",
|
||||
|
||||
@@ -95,13 +95,15 @@ export type TelegramAccountConfig = {
|
||||
textChunkLimit?: number;
|
||||
/** Chunking mode: "length" (default) splits by size; "newline" splits on every newline. */
|
||||
chunkMode?: "length" | "newline";
|
||||
/** Enable live stream preview via message edits (default: true). */
|
||||
streaming?: boolean;
|
||||
/** Disable block streaming for this account. */
|
||||
blockStreaming?: boolean;
|
||||
/** Chunking config for Telegram stream previews in `streamMode: "block"`. */
|
||||
/** @deprecated Legacy chunking config from `streamMode: "block"`; ignored after migration. */
|
||||
draftChunk?: BlockStreamingChunkConfig;
|
||||
/** Merge streamed block replies before sending. */
|
||||
blockStreamingCoalesce?: BlockStreamingCoalesceConfig;
|
||||
/** Telegram stream preview mode (off|partial|block). Default: partial. */
|
||||
/** @deprecated Legacy key; migrated automatically to `streaming` boolean. */
|
||||
streamMode?: "off" | "partial" | "block";
|
||||
mediaMaxMb?: number;
|
||||
/** Telegram API client timeout in seconds (grammY ApiClientOptions). */
|
||||
|
||||
@@ -99,6 +99,27 @@ const validateTelegramCustomCommands = (
|
||||
}
|
||||
};
|
||||
|
||||
function normalizeTelegramStreamingConfig(value: {
|
||||
streaming?: boolean;
|
||||
streamMode?: "off" | "partial" | "block";
|
||||
}) {
|
||||
if (typeof value.streaming === "boolean") {
|
||||
delete value.streamMode;
|
||||
return;
|
||||
}
|
||||
if (value.streamMode === "off") {
|
||||
value.streaming = false;
|
||||
delete value.streamMode;
|
||||
return;
|
||||
}
|
||||
if (value.streamMode === "partial" || value.streamMode === "block") {
|
||||
value.streaming = true;
|
||||
delete value.streamMode;
|
||||
return;
|
||||
}
|
||||
value.streaming = true;
|
||||
}
|
||||
|
||||
export const TelegramAccountSchemaBase = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
@@ -122,10 +143,12 @@ export const TelegramAccountSchemaBase = z
|
||||
dms: z.record(z.string(), DmConfigSchema.optional()).optional(),
|
||||
textChunkLimit: z.number().int().positive().optional(),
|
||||
chunkMode: z.enum(["length", "newline"]).optional(),
|
||||
streaming: z.boolean().optional(),
|
||||
blockStreaming: z.boolean().optional(),
|
||||
draftChunk: BlockStreamingChunkSchema.optional(),
|
||||
blockStreamingCoalesce: BlockStreamingCoalesceSchema.optional(),
|
||||
streamMode: z.enum(["off", "partial", "block"]).optional().default("partial"),
|
||||
// Legacy key kept for automatic migration to `streaming`.
|
||||
streamMode: z.enum(["off", "partial", "block"]).optional(),
|
||||
mediaMaxMb: z.number().positive().optional(),
|
||||
timeoutSeconds: z.number().int().positive().optional(),
|
||||
retry: RetryConfigSchema,
|
||||
@@ -159,6 +182,7 @@ export const TelegramAccountSchemaBase = z
|
||||
.strict();
|
||||
|
||||
export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((value, ctx) => {
|
||||
normalizeTelegramStreamingConfig(value);
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
@@ -173,6 +197,7 @@ export const TelegramAccountSchema = TelegramAccountSchemaBase.superRefine((valu
|
||||
export const TelegramConfigSchema = TelegramAccountSchemaBase.extend({
|
||||
accounts: z.record(z.string(), TelegramAccountSchema.optional()).optional(),
|
||||
}).superRefine((value, ctx) => {
|
||||
normalizeTelegramStreamingConfig(value);
|
||||
requireOpenAllowFrom({
|
||||
policy: value.dmPolicy,
|
||||
allowFrom: value.allowFrom,
|
||||
|
||||
@@ -193,7 +193,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
expect(draftStream.clear).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("keeps a higher initial debounce threshold in block stream mode", async () => {
|
||||
it("uses immediate preview updates for legacy block stream mode", async () => {
|
||||
const draftStream = createDraftStream();
|
||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
@@ -209,7 +209,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
|
||||
expect(createTelegramDraftStream).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
minInitialChars: 30,
|
||||
minInitialChars: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -445,7 +445,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("forces new message when new assistant message starts after previous output", async () => {
|
||||
it("does not force new message for legacy block stream mode", async () => {
|
||||
const draftStream = createDraftStream(999);
|
||||
createTelegramDraftStream.mockReturnValue(draftStream);
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(
|
||||
@@ -464,8 +464,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
|
||||
await dispatchWithContext({ context: createContext(), streamMode: "block" });
|
||||
|
||||
// Should force new message when assistant message starts after previous output
|
||||
expect(draftStream.forceNewMessage).toHaveBeenCalled();
|
||||
expect(draftStream.forceNewMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not force new message in partial mode when assistant message restarts", async () => {
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
modelSupportsVision,
|
||||
} from "../agents/model-catalog.js";
|
||||
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
|
||||
import { EmbeddedBlockChunker } from "../agents/pi-embedded-block-chunker.js";
|
||||
import { resolveChunkMode } from "../auto-reply/chunk.js";
|
||||
import { clearHistoryEntriesIfEnabled } from "../auto-reply/reply/history.js";
|
||||
import { dispatchReplyWithBufferedBlockDispatcher } from "../auto-reply/reply/provider-dispatcher.js";
|
||||
@@ -26,7 +25,6 @@ import type { TelegramBotOptions } from "./bot.js";
|
||||
import { deliverReplies } from "./bot/delivery.js";
|
||||
import type { TelegramStreamMode } from "./bot/types.js";
|
||||
import type { TelegramInlineButtons } from "./button-types.js";
|
||||
import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
|
||||
import { createTelegramDraftStream } from "./draft-stream.js";
|
||||
import { renderTelegramHtmlText } from "./format.js";
|
||||
import {
|
||||
@@ -143,21 +141,20 @@ export const dispatchTelegramMessage = async ({
|
||||
});
|
||||
const forceBlockStreamingForReasoning = resolvedReasoningLevel === "on";
|
||||
const streamReasoningDraft = resolvedReasoningLevel === "stream";
|
||||
const previewStreamingEnabled = streamMode !== "off";
|
||||
const canStreamAnswerDraft =
|
||||
streamMode !== "off" && !accountBlockStreamingEnabled && !forceBlockStreamingForReasoning;
|
||||
previewStreamingEnabled && !accountBlockStreamingEnabled && !forceBlockStreamingForReasoning;
|
||||
const canStreamReasoningDraft = canStreamAnswerDraft || streamReasoningDraft;
|
||||
const draftReplyToMessageId =
|
||||
replyToMode !== "off" && typeof msg.message_id === "number" ? msg.message_id : undefined;
|
||||
const draftMinInitialChars =
|
||||
streamMode === "partial" || streamReasoningDraft ? 1 : DRAFT_MIN_INITIAL_CHARS;
|
||||
previewStreamingEnabled || streamReasoningDraft ? 1 : DRAFT_MIN_INITIAL_CHARS;
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, route.agentId);
|
||||
type LaneName = "answer" | "reasoning";
|
||||
type DraftLaneState = {
|
||||
stream: ReturnType<typeof createTelegramDraftStream> | undefined;
|
||||
lastPartialText: string;
|
||||
draftText: string;
|
||||
hasStreamedMessage: boolean;
|
||||
chunker: EmbeddedBlockChunker | undefined;
|
||||
};
|
||||
const createDraftLane = (enabled: boolean): DraftLaneState => {
|
||||
const stream = enabled
|
||||
@@ -173,16 +170,10 @@ export const dispatchTelegramMessage = async ({
|
||||
warn: logVerbose,
|
||||
})
|
||||
: undefined;
|
||||
const chunker =
|
||||
stream && streamMode === "block"
|
||||
? new EmbeddedBlockChunker(resolveTelegramDraftStreamingChunking(cfg, route.accountId))
|
||||
: undefined;
|
||||
return {
|
||||
stream,
|
||||
lastPartialText: "",
|
||||
draftText: "",
|
||||
hasStreamedMessage: false,
|
||||
chunker,
|
||||
};
|
||||
};
|
||||
const lanes: Record<LaneName, DraftLaneState> = {
|
||||
@@ -207,9 +198,7 @@ export const dispatchTelegramMessage = async ({
|
||||
};
|
||||
const resetDraftLaneState = (lane: DraftLaneState) => {
|
||||
lane.lastPartialText = "";
|
||||
lane.draftText = "";
|
||||
lane.hasStreamedMessage = false;
|
||||
lane.chunker?.reset();
|
||||
};
|
||||
const updateDraftFromPartial = (lane: DraftLaneState, text: string | undefined) => {
|
||||
const laneStream = lane.stream;
|
||||
@@ -221,46 +210,18 @@ export const dispatchTelegramMessage = async ({
|
||||
}
|
||||
// Mark that we've received streaming content (for forceNewMessage decision).
|
||||
lane.hasStreamedMessage = true;
|
||||
if (streamMode === "partial") {
|
||||
// Some providers briefly emit a shorter prefix snapshot (for example
|
||||
// "Sure." -> "Sure" -> "Sure."). Keep the longer preview to avoid
|
||||
// visible punctuation flicker.
|
||||
if (
|
||||
lane.lastPartialText &&
|
||||
lane.lastPartialText.startsWith(text) &&
|
||||
text.length < lane.lastPartialText.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
lane.lastPartialText = text;
|
||||
laneStream.update(text);
|
||||
// Some providers briefly emit a shorter prefix snapshot (for example
|
||||
// "Sure." -> "Sure" -> "Sure."). Keep the longer preview to avoid
|
||||
// visible punctuation flicker.
|
||||
if (
|
||||
lane.lastPartialText &&
|
||||
lane.lastPartialText.startsWith(text) &&
|
||||
text.length < lane.lastPartialText.length
|
||||
) {
|
||||
return;
|
||||
}
|
||||
let delta = text;
|
||||
if (text.startsWith(lane.lastPartialText)) {
|
||||
delta = text.slice(lane.lastPartialText.length);
|
||||
} else {
|
||||
// Streaming buffer reset (or non-monotonic stream). Start fresh.
|
||||
lane.chunker?.reset();
|
||||
lane.draftText = "";
|
||||
}
|
||||
lane.lastPartialText = text;
|
||||
if (!delta) {
|
||||
return;
|
||||
}
|
||||
if (!lane.chunker) {
|
||||
lane.draftText = text;
|
||||
laneStream.update(lane.draftText);
|
||||
return;
|
||||
}
|
||||
lane.chunker.append(delta);
|
||||
lane.chunker.drain({
|
||||
force: false,
|
||||
emit: (chunk) => {
|
||||
lane.draftText += chunk;
|
||||
laneStream.update(lane.draftText);
|
||||
},
|
||||
});
|
||||
laneStream.update(text);
|
||||
};
|
||||
const ingestDraftLaneSegments = (text: string | undefined) => {
|
||||
for (const segment of splitTextIntoLaneSegments(text)) {
|
||||
@@ -275,31 +236,18 @@ export const dispatchTelegramMessage = async ({
|
||||
if (!lane.stream) {
|
||||
return;
|
||||
}
|
||||
if (lane.chunker?.hasBuffered()) {
|
||||
lane.chunker.drain({
|
||||
force: true,
|
||||
emit: (chunk) => {
|
||||
lane.draftText += chunk;
|
||||
},
|
||||
});
|
||||
lane.chunker.reset();
|
||||
if (lane.draftText) {
|
||||
lane.stream.update(lane.draftText);
|
||||
}
|
||||
}
|
||||
await lane.stream.flush();
|
||||
};
|
||||
|
||||
const disableBlockStreaming =
|
||||
streamMode === "off"
|
||||
? true
|
||||
: forceBlockStreamingForReasoning
|
||||
? false
|
||||
: typeof telegramCfg.blockStreaming === "boolean"
|
||||
? !telegramCfg.blockStreaming
|
||||
: canStreamAnswerDraft
|
||||
? true
|
||||
: undefined;
|
||||
const disableBlockStreaming = !previewStreamingEnabled
|
||||
? true
|
||||
: forceBlockStreamingForReasoning
|
||||
? false
|
||||
: typeof telegramCfg.blockStreaming === "boolean"
|
||||
? !telegramCfg.blockStreaming
|
||||
: canStreamAnswerDraft
|
||||
? true
|
||||
: undefined;
|
||||
|
||||
const { onModelSelected, ...prefixOptions } = createReplyPrefixOptions({
|
||||
cfg,
|
||||
@@ -395,8 +343,7 @@ export const dispatchTelegramMessage = async ({
|
||||
linkPreview: telegramCfg.linkPreview,
|
||||
replyQuoteText,
|
||||
};
|
||||
const getLanePreviewText = (lane: DraftLaneState) =>
|
||||
streamMode === "block" ? lane.draftText : lane.lastPartialText;
|
||||
const getLanePreviewText = (lane: DraftLaneState) => lane.lastPartialText;
|
||||
const tryUpdatePreviewForLane = async (params: {
|
||||
lane: DraftLaneState;
|
||||
laneName: LaneName;
|
||||
@@ -449,7 +396,6 @@ export const dispatchTelegramMessage = async ({
|
||||
});
|
||||
if (updateLaneSnapshot) {
|
||||
lane.lastPartialText = text;
|
||||
lane.draftText = text;
|
||||
}
|
||||
deliveryState.delivered = true;
|
||||
return true;
|
||||
@@ -684,10 +630,6 @@ export const dispatchTelegramMessage = async ({
|
||||
onAssistantMessageStart: answerLane.stream
|
||||
? () => {
|
||||
reasoningStepState.resetForNextStep();
|
||||
// Keep answer blocks separated in block mode; partial mode keeps one answer lane.
|
||||
if (streamMode === "block" && answerLane.hasStreamedMessage) {
|
||||
answerLane.stream?.forceNewMessage();
|
||||
}
|
||||
resetDraftLaneState(answerLane);
|
||||
}
|
||||
: undefined,
|
||||
|
||||
@@ -154,11 +154,18 @@ export function buildTypingThreadParams(messageThreadId?: number) {
|
||||
}
|
||||
|
||||
export function resolveTelegramStreamMode(telegramCfg?: {
|
||||
streaming?: boolean;
|
||||
streamMode?: TelegramStreamMode;
|
||||
}): TelegramStreamMode {
|
||||
if (typeof telegramCfg?.streaming === "boolean") {
|
||||
return telegramCfg.streaming ? "partial" : "off";
|
||||
}
|
||||
const raw = telegramCfg?.streamMode?.trim().toLowerCase();
|
||||
if (raw === "off" || raw === "partial" || raw === "block") {
|
||||
return raw;
|
||||
if (raw === "off") {
|
||||
return "off";
|
||||
}
|
||||
if (raw === "partial" || raw === "block") {
|
||||
return "partial";
|
||||
}
|
||||
return "partial";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user