mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-24 07:31:44 +00:00
fix(telegram): suppress NO_REPLY partial leaks and default streaming off
This commit is contained in:
@@ -28,7 +28,7 @@ import {
|
||||
import { stripHeartbeatToken } from "../heartbeat.js";
|
||||
import type { TemplateContext } from "../templating.js";
|
||||
import type { VerboseLevel } from "../thinking.js";
|
||||
import { isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import { isSilentReplyPrefixText, isSilentReplyText, SILENT_REPLY_TOKEN } from "../tokens.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import {
|
||||
buildEmbeddedRunBaseParams,
|
||||
@@ -138,7 +138,10 @@ export async function runAgentTurnWithFallback(params: {
|
||||
}
|
||||
text = stripped.text;
|
||||
}
|
||||
if (isSilentReplyText(text, SILENT_REPLY_TOKEN)) {
|
||||
if (
|
||||
isSilentReplyText(text, SILENT_REPLY_TOKEN) ||
|
||||
isSilentReplyPrefixText(text, SILENT_REPLY_TOKEN)
|
||||
) {
|
||||
return { skip: true };
|
||||
}
|
||||
if (!text) {
|
||||
|
||||
@@ -383,6 +383,26 @@ describe("runReplyAgent typing (heartbeat)", () => {
|
||||
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses partial streaming for NO_REPLY prefixes", async () => {
|
||||
const onPartialReply = vi.fn();
|
||||
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
|
||||
await params.onPartialReply?.({ text: "NO_" });
|
||||
await params.onPartialReply?.({ text: "NO_RE" });
|
||||
await params.onPartialReply?.({ text: "NO_REPLY" });
|
||||
return { payloads: [{ text: "NO_REPLY" }], meta: {} };
|
||||
});
|
||||
|
||||
const { run, typing } = createMinimalRun({
|
||||
opts: { isHeartbeat: false, onPartialReply },
|
||||
typingMode: "message",
|
||||
});
|
||||
await run();
|
||||
|
||||
expect(onPartialReply).not.toHaveBeenCalled();
|
||||
expect(typing.startTypingOnText).not.toHaveBeenCalled();
|
||||
expect(typing.startTypingLoop).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not start typing on assistant message start without prior text in message mode", async () => {
|
||||
state.runEmbeddedPiAgentMock.mockImplementationOnce(async (params: AgentRunParams) => {
|
||||
await params.onAssistantMessageStart?.();
|
||||
|
||||
@@ -18,3 +18,17 @@ export function isSilentReplyText(
|
||||
const suffix = new RegExp(`\\b${escaped}\\b\\W*$`);
|
||||
return suffix.test(text);
|
||||
}
|
||||
|
||||
export function isSilentReplyPrefixText(
|
||||
text: string | undefined,
|
||||
token: string = SILENT_REPLY_TOKEN,
|
||||
): boolean {
|
||||
if (!text) {
|
||||
return false;
|
||||
}
|
||||
const normalized = text.trimStart().toUpperCase();
|
||||
if (!normalized) {
|
||||
return false;
|
||||
}
|
||||
return token.toUpperCase().startsWith(normalized);
|
||||
}
|
||||
|
||||
@@ -378,11 +378,11 @@ describe("legacy config detection", () => {
|
||||
expect(res.config.channels?.telegram?.groupPolicy).toBe("allowlist");
|
||||
}
|
||||
});
|
||||
it("defaults telegram.streaming to true when telegram section exists", async () => {
|
||||
it("defaults telegram.streaming to false when telegram section exists", async () => {
|
||||
const res = validateConfigObject({ channels: { telegram: {} } });
|
||||
expect(res.ok).toBe(true);
|
||||
if (res.ok) {
|
||||
expect(res.config.channels?.telegram?.streaming).toBe(true);
|
||||
expect(res.config.channels?.telegram?.streaming).toBe(false);
|
||||
expect(res.config.channels?.telegram?.streamMode).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -392,7 +392,7 @@ export const FIELD_HELP: Record<string, string> = {
|
||||
"channels.telegram.dmPolicy":
|
||||
'Direct message access control ("pairing" recommended). "open" requires channels.telegram.allowFrom=["*"].',
|
||||
"channels.telegram.streaming":
|
||||
"Enable Telegram live stream preview via message edits (default: true; legacy streamMode auto-maps here).",
|
||||
"Enable Telegram live stream preview via message edits (default: false; 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":
|
||||
|
||||
@@ -117,7 +117,7 @@ function normalizeTelegramStreamingConfig(value: {
|
||||
delete value.streamMode;
|
||||
return;
|
||||
}
|
||||
value.streaming = true;
|
||||
value.streaming = false;
|
||||
}
|
||||
|
||||
export const TelegramAccountSchemaBase = z
|
||||
|
||||
20
src/telegram/bot.helpers.test.ts
Normal file
20
src/telegram/bot.helpers.test.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolveTelegramStreamMode } from "./bot/helpers.js";
|
||||
|
||||
describe("resolveTelegramStreamMode", () => {
|
||||
it("defaults to off when telegram streaming is unset", () => {
|
||||
expect(resolveTelegramStreamMode(undefined)).toBe("off");
|
||||
expect(resolveTelegramStreamMode({})).toBe("off");
|
||||
});
|
||||
|
||||
it("prefers explicit streaming boolean", () => {
|
||||
expect(resolveTelegramStreamMode({ streaming: true })).toBe("partial");
|
||||
expect(resolveTelegramStreamMode({ streaming: false })).toBe("off");
|
||||
});
|
||||
|
||||
it("maps legacy streamMode values", () => {
|
||||
expect(resolveTelegramStreamMode({ streamMode: "off" })).toBe("off");
|
||||
expect(resolveTelegramStreamMode({ streamMode: "partial" })).toBe("partial");
|
||||
expect(resolveTelegramStreamMode({ streamMode: "block" })).toBe("partial");
|
||||
});
|
||||
});
|
||||
@@ -167,7 +167,7 @@ export function resolveTelegramStreamMode(telegramCfg?: {
|
||||
if (raw === "partial" || raw === "block") {
|
||||
return "partial";
|
||||
}
|
||||
return "partial";
|
||||
return "off";
|
||||
}
|
||||
|
||||
export function buildTelegramGroupPeerId(chatId: number | string, messageThreadId?: number) {
|
||||
|
||||
Reference in New Issue
Block a user