fix(telegram): warn on selected quote tool progress

This commit is contained in:
Peter Steinberger
2026-05-03 16:13:38 +01:00
parent b336efdd9c
commit e2c8db2cad
5 changed files with 173 additions and 2 deletions

View File

@@ -6,10 +6,12 @@ import {
collectTelegramEmptyAllowlistExtraWarnings,
collectTelegramGroupPolicyWarnings,
collectTelegramMissingEnvTokenWarnings,
collectTelegramSelectedQuoteToolProgressWarnings,
maybeRepairTelegramApiRoots,
maybeRepairTelegramAllowFromUsernames,
scanTelegramBotEndpointApiRoots,
scanTelegramInvalidAllowFromEntries,
scanTelegramSelectedQuoteToolProgressWarnings,
telegramDoctor,
} from "./doctor.js";
@@ -329,6 +331,112 @@ describe("telegram doctor", () => {
]);
});
it("warns when selected quote replies can suppress Telegram tool-progress preview", async () => {
const cfg = {
channels: {
telegram: {
replyToMode: "first",
},
},
} as unknown as OpenClawConfig;
const hits = scanTelegramSelectedQuoteToolProgressWarnings(cfg);
expect(hits).toEqual([{ path: "channels.telegram", replyToMode: "first" }]);
const warnings = collectTelegramSelectedQuoteToolProgressWarnings({ hits });
expect(warnings[0]).toContain("selected quote replies");
expect(warnings[0]).toContain('"Working..." tool-progress preview');
expect(warnings[0]).toContain("Current-message replies without selected quote text");
expect(warnings[1]).toContain("streaming.preview.toolProgress: false");
expect(
await telegramDoctor.collectPreviewWarnings?.({
cfg,
doctorFixCommand: "openclaw doctor --fix",
}),
).toEqual(expect.arrayContaining([expect.stringContaining("selected quote replies")]));
});
it("warns for the implicit default Telegram account when accounts is empty", () => {
const cfg = {
channels: {
telegram: {
replyToMode: "all",
accounts: {},
},
},
} as unknown as OpenClawConfig;
expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([
{ path: "channels.telegram", replyToMode: "all" },
]);
});
it("uses merged Telegram account config for selected quote tool-progress warnings", () => {
listTelegramAccountIdsMock.mockReturnValue(["work", "quiet"]);
const cfg = {
channels: {
telegram: {
replyToMode: "batched",
accounts: {
work: {},
quiet: {
replyToMode: "off",
},
},
},
},
} as unknown as OpenClawConfig;
expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([
{ path: "channels.telegram.accounts.work", replyToMode: "batched" },
]);
});
it("skips selected quote tool-progress warning when preview progress is disabled", () => {
const cfg = {
channels: {
telegram: {
replyToMode: "first",
streaming: {
preview: {
toolProgress: false,
},
},
},
},
} as unknown as OpenClawConfig;
expect(scanTelegramSelectedQuoteToolProgressWarnings(cfg)).toEqual([]);
});
it("skips selected quote tool-progress warning when preview streaming is off or block streaming owns delivery", () => {
expect(
scanTelegramSelectedQuoteToolProgressWarnings({
channels: {
telegram: {
replyToMode: "first",
streaming: false,
},
},
} as unknown as OpenClawConfig),
).toEqual([]);
expect(
scanTelegramSelectedQuoteToolProgressWarnings({
channels: {
telegram: {
replyToMode: "first",
},
},
agents: {
defaults: {
blockStreamingDefault: "on",
},
},
} as unknown as OpenClawConfig),
).toEqual([]);
});
it("wires apiRoot preview warnings and repair through the doctor adapter", async () => {
const cfg = {
channels: {

View File

@@ -2,12 +2,17 @@ import {
type ChannelDoctorAdapter,
type ChannelDoctorEmptyAllowlistAccountContext,
} from "openclaw/plugin-sdk/channel-contract";
import {
resolveChannelStreamingBlockEnabled,
resolveChannelStreamingPreviewToolProgress,
} from "openclaw/plugin-sdk/channel-streaming";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime";
import { inspectTelegramAccount } from "./account-inspect.js";
import {
listTelegramAccountIds,
mergeTelegramAccountConfig,
resolveDefaultTelegramAccountId,
resolveTelegramAccount,
} from "./accounts.js";
@@ -18,8 +23,10 @@ import {
legacyConfigRules as TELEGRAM_LEGACY_CONFIG_RULES,
normalizeCompatibilityConfig as normalizeTelegramCompatibilityConfig,
} from "./doctor-contract.js";
import { resolveTelegramPreviewStreamMode } from "./preview-streaming.js";
type TelegramAllowFromInvalidHit = { path: string; entry: string };
type TelegramSelectedQuoteToolProgressHit = { path: string; replyToMode: string };
type TelegramApiRootBotEndpointHit = {
path: string;
pathSegments: string[];
@@ -196,6 +203,58 @@ export function collectTelegramApiRootWarnings(params: {
];
}
function formatTelegramAccountConfigPath(cfg: OpenClawConfig, accountId: string): string {
const telegram = asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.telegram);
const accounts = asObjectRecord(telegram?.accounts);
if (!accounts || Object.keys(accounts).length === 0) {
return "channels.telegram";
}
return accountId === "default" ? "channels.telegram" : `channels.telegram.accounts.${accountId}`;
}
export function scanTelegramSelectedQuoteToolProgressWarnings(
cfg: OpenClawConfig,
): TelegramSelectedQuoteToolProgressHit[] {
if (!asObjectRecord((cfg.channels as Record<string, unknown> | undefined)?.telegram)) {
return [];
}
return listTelegramAccountIds(cfg).flatMap((accountId) => {
const account = mergeTelegramAccountConfig(cfg, accountId);
const replyToMode = account.replyToMode ?? "off";
if (replyToMode === "off") {
return [];
}
if (resolveTelegramPreviewStreamMode(account) === "off") {
return [];
}
const blockStreamingEnabled =
resolveChannelStreamingBlockEnabled(account) ??
cfg.agents?.defaults?.blockStreamingDefault === "on";
if (blockStreamingEnabled || !resolveChannelStreamingPreviewToolProgress(account)) {
return [];
}
return [
{
path: formatTelegramAccountConfigPath(cfg, accountId),
replyToMode,
},
];
});
}
export function collectTelegramSelectedQuoteToolProgressWarnings(params: {
hits: TelegramSelectedQuoteToolProgressHit[];
}): string[] {
if (params.hits.length === 0) {
return [];
}
const sample = params.hits[0] ?? { path: "channels.telegram", replyToMode: "first" };
return [
`- ${sanitizeForLog(sample.path)} has replyToMode: "${sanitizeForLog(sample.replyToMode)}" while Telegram preview tool-progress is enabled. Telegram selected quote replies must send the final answer through the native quote-reply path, so those turns skip the short "Working..." tool-progress preview. Current-message replies without selected quote text still keep preview streaming.`,
'- Set replyToMode: "off" when tool-progress preview matters more than native quote replies, or set streaming.preview.toolProgress: false to keep quote replies and silence this warning.',
];
}
export function maybeRepairTelegramApiRoots(cfg: OpenClawConfig): {
config: OpenClawConfig;
changes: string[];
@@ -506,6 +565,9 @@ export const telegramDoctor: ChannelDoctorAdapter = {
hits: scanTelegramBotEndpointApiRoots(cfg),
doctorFixCommand,
}),
...collectTelegramSelectedQuoteToolProgressWarnings({
hits: scanTelegramSelectedQuoteToolProgressWarnings(cfg),
}),
],
repairConfig: async ({ cfg }) => await repairTelegramConfig({ cfg }),
collectEmptyAllowlistExtraWarnings: collectTelegramEmptyAllowlistExtraWarnings,