mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 21:14:46 +00:00
Fixes #76852. Co-authored-by: jindongfu <jindongfu@microsoft.com> Co-authored-by: Frank Yang <frank.ekn@gmail.com>
This commit is contained in:
@@ -29,6 +29,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Gateway/session history: carry monotonic transcript message sequence through live updates and refresh SSE history when stale sequence input would otherwise append bad incremental state. (#81474) Thanks @samzong.
|
||||
- Memory/daily-files: widen the daily-memory file matcher used by Dreaming, rem-backfill, rem-harness, the doctor sweep, and short-term promotion so `memory/YYYY-MM-DD-<slug>.md` files written by the bundled session-memory hook (and any future slugged variants) are discovered alongside the date-only `memory/YYYY-MM-DD.md` shape. Date extraction still uses the leading `YYYY-MM-DD` capture group, so per-day ingestion/promotion semantics are unchanged for existing date-only files; slugged files now flow through the same paths instead of being silently skipped. Fixes #69536. Thanks @jack-stormentswe.
|
||||
- macOS/Gateway: fail managed LaunchAgent stop and restart when the configured gateway port remains busy after cleanup instead of reporting success while a listener survives. Fixes #73132. Thanks @BunsDev.
|
||||
- Telegram: reuse the sticky IPv4 Bot API transport for periodic getMe health checks, so IPv4-working hosts with broken IPv6 egress stop logging repeated probe timeouts. Fixes #76852. (#76856) Thanks @SymbolStar.
|
||||
- Telegram: ship the isolated polling worker at the root dist path used by the bundled worker loader, avoiding startup failures looking for `dist/telegram-ingress-worker.runtime.js`.
|
||||
- Telegram: skip unmentioned group media before download when `requireMention` is active, avoiding failed media-download replies for messages that should be ignored. Fixes #81181. (#81785) Thanks @joshavant.
|
||||
- Control UI/Gateway: stop stale token-mismatch reconnect loops when no trusted device-token retry is available, and cap rendered chat history by raw tool-output size so dashboard auth/history work cannot keep degrading channel sockets. Fixes #72139. Thanks @BunsDev.
|
||||
|
||||
@@ -2,11 +2,11 @@ import { withFetchPreconnect } from "openclaw/plugin-sdk/test-env";
|
||||
import { afterEach, describe, expect, it, vi, type Mock } from "vitest";
|
||||
import { probeTelegram, resetTelegramProbeFetcherCacheForTests } from "./probe.js";
|
||||
|
||||
const resolveTelegramFetch = vi.hoisted(() => vi.fn());
|
||||
const resolveTelegramTransport = vi.hoisted(() => vi.fn());
|
||||
const makeProxyFetch = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./fetch.js", () => ({
|
||||
resolveTelegramFetch,
|
||||
resolveTelegramTransport,
|
||||
resolveTelegramApiBase: (apiRoot?: string) =>
|
||||
apiRoot?.trim()?.replace(/\/+$/, "") || "https://api.telegram.org",
|
||||
}));
|
||||
@@ -19,11 +19,18 @@ describe("probeTelegram retry logic", () => {
|
||||
const token = "test-token";
|
||||
const timeoutMs = 5000;
|
||||
const originalFetch = global.fetch;
|
||||
let forceFallbackMock: Mock;
|
||||
|
||||
const installFetchMock = (): Mock => {
|
||||
const fetchMock = vi.fn();
|
||||
global.fetch = withFetchPreconnect(fetchMock);
|
||||
resolveTelegramFetch.mockImplementation((proxyFetch?: typeof fetch) => proxyFetch ?? fetch);
|
||||
forceFallbackMock = vi.fn().mockReturnValue(true);
|
||||
resolveTelegramTransport.mockImplementation((proxyFetch?: typeof fetch) => ({
|
||||
fetch: proxyFetch ?? fetch,
|
||||
sourceFetch: proxyFetch ?? fetch,
|
||||
forceFallback: forceFallbackMock,
|
||||
close: async () => {},
|
||||
}));
|
||||
makeProxyFetch.mockImplementation(() => fetchMock as unknown as typeof fetch);
|
||||
return fetchMock;
|
||||
};
|
||||
@@ -72,7 +79,7 @@ describe("probeTelegram retry logic", () => {
|
||||
|
||||
afterEach(() => {
|
||||
resetTelegramProbeFetcherCacheForTests();
|
||||
resolveTelegramFetch.mockReset();
|
||||
resolveTelegramTransport.mockReset();
|
||||
makeProxyFetch.mockReset();
|
||||
vi.unstubAllEnvs();
|
||||
vi.clearAllMocks();
|
||||
@@ -151,7 +158,12 @@ describe("probeTelegram retry logic", () => {
|
||||
});
|
||||
});
|
||||
global.fetch = withFetchPreconnect(fetchMock as unknown as typeof fetch);
|
||||
resolveTelegramFetch.mockImplementation((proxyFetch?: typeof fetch) => proxyFetch ?? fetch);
|
||||
resolveTelegramTransport.mockImplementation((proxyFetch?: typeof fetch) => ({
|
||||
fetch: proxyFetch ?? fetch,
|
||||
sourceFetch: proxyFetch ?? fetch,
|
||||
forceFallback: vi.fn().mockReturnValue(true),
|
||||
close: async () => {},
|
||||
}));
|
||||
makeProxyFetch.mockImplementation(() => fetchMock as unknown as typeof fetch);
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
@@ -226,7 +238,7 @@ describe("probeTelegram retry logic", () => {
|
||||
});
|
||||
|
||||
expect(makeProxyFetch).toHaveBeenCalledWith("http://127.0.0.1:8888");
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledWith(fetchMock, {
|
||||
expect(resolveTelegramTransport).toHaveBeenCalledWith(fetchMock, {
|
||||
network: {
|
||||
autoSelectFamily: false,
|
||||
dnsResultOrder: "ipv4first",
|
||||
@@ -257,7 +269,7 @@ describe("probeTelegram retry logic", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledTimes(1);
|
||||
expect(resolveTelegramTransport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not reuse probe fetcher cache when network settings differ", async () => {
|
||||
@@ -283,7 +295,7 @@ describe("probeTelegram retry logic", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledTimes(2);
|
||||
expect(resolveTelegramTransport).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("reuses probe fetcher cache across token rotation when accountId is stable", async () => {
|
||||
@@ -311,6 +323,48 @@ describe("probeTelegram retry logic", () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(resolveTelegramFetch).toHaveBeenCalledTimes(1);
|
||||
expect(resolveTelegramTransport).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("calls forceFallback on the transport when getMe times out so subsequent probes use IPv4", async () => {
|
||||
const fetchMock = vi.fn();
|
||||
const localForceFallback = vi.fn().mockReturnValue(true);
|
||||
resolveTelegramTransport.mockImplementation(() => ({
|
||||
fetch: withFetchPreconnect(fetchMock),
|
||||
sourceFetch: fetchMock,
|
||||
forceFallback: localForceFallback,
|
||||
close: async () => {},
|
||||
}));
|
||||
|
||||
// First call: timeout (simulate IPv6 hang)
|
||||
const timeoutError = new Error("request timed out");
|
||||
timeoutError.name = "TimeoutError";
|
||||
fetchMock.mockRejectedValueOnce(timeoutError);
|
||||
// Second call (retry after forceFallback): success on IPv4
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
result: { id: 1, is_bot: true, first_name: "Bot", username: "bot" },
|
||||
}),
|
||||
});
|
||||
// Webhook info
|
||||
fetchMock.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: vi.fn().mockResolvedValue({ ok: true, result: { url: "" } }),
|
||||
});
|
||||
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const probePromise = probeTelegram(token, 30_000);
|
||||
await vi.advanceTimersByTimeAsync(1000);
|
||||
|
||||
const result = await probePromise;
|
||||
expect(result.ok).toBe(true);
|
||||
expect(localForceFallback).toHaveBeenCalledWith("probe timeout/network error");
|
||||
expect(fetchMock).toHaveBeenCalledTimes(3); // 1 failed + 1 getMe success + 1 webhook
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,11 @@ import type { TelegramNetworkConfig } from "openclaw/plugin-sdk/config-contracts
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
import { fetchWithTimeout } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import type { TelegramBotInfo } from "./bot-info.js";
|
||||
import { resolveTelegramApiBase, resolveTelegramFetch } from "./fetch.js";
|
||||
import {
|
||||
resolveTelegramApiBase,
|
||||
resolveTelegramTransport,
|
||||
type TelegramTransport,
|
||||
} from "./fetch.js";
|
||||
import { makeProxyFetch } from "./proxy.js";
|
||||
|
||||
export type TelegramProbe = BaseProbeResult & {
|
||||
@@ -35,11 +39,11 @@ export type TelegramProbeOptions = {
|
||||
includeWebhookInfo?: boolean;
|
||||
};
|
||||
|
||||
const probeFetcherCache = new Map<string, typeof fetch>();
|
||||
const MAX_PROBE_FETCHER_CACHE_SIZE = 64;
|
||||
const probeTransportCache = new Map<string, TelegramTransport>();
|
||||
const MAX_PROBE_TRANSPORT_CACHE_SIZE = 64;
|
||||
|
||||
export function resetTelegramProbeFetcherCacheForTests(): void {
|
||||
probeFetcherCache.clear();
|
||||
probeTransportCache.clear();
|
||||
}
|
||||
|
||||
function resolveProbeOptions(
|
||||
@@ -54,11 +58,11 @@ function resolveProbeOptions(
|
||||
return proxyOrOptions;
|
||||
}
|
||||
|
||||
function shouldUseProbeFetcherCache(): boolean {
|
||||
function shouldUseProbeTransportCache(): boolean {
|
||||
return !process.env.VITEST && process.env.NODE_ENV !== "test";
|
||||
}
|
||||
|
||||
function buildProbeFetcherCacheKey(token: string, options?: TelegramProbeOptions): string {
|
||||
function buildProbeTransportCacheKey(token: string, options?: TelegramProbeOptions): string {
|
||||
const cacheIdentity = options?.accountId?.trim() || token;
|
||||
const cacheIdentityKind = options?.accountId?.trim() ? "account" : "token";
|
||||
const proxyKey = options?.proxyUrl?.trim() ?? "";
|
||||
@@ -70,37 +74,40 @@ function buildProbeFetcherCacheKey(token: string, options?: TelegramProbeOptions
|
||||
return `${cacheIdentityKind}:${cacheIdentity}::${proxyKey}::${autoSelectFamilyKey}::${dnsResultOrderKey}::${apiRootKey}`;
|
||||
}
|
||||
|
||||
function setCachedProbeFetcher(cacheKey: string, fetcher: typeof fetch): typeof fetch {
|
||||
probeFetcherCache.set(cacheKey, fetcher);
|
||||
if (probeFetcherCache.size > MAX_PROBE_FETCHER_CACHE_SIZE) {
|
||||
const oldestKey = probeFetcherCache.keys().next().value;
|
||||
function setCachedProbeTransport(
|
||||
cacheKey: string,
|
||||
transport: TelegramTransport,
|
||||
): TelegramTransport {
|
||||
probeTransportCache.set(cacheKey, transport);
|
||||
if (probeTransportCache.size > MAX_PROBE_TRANSPORT_CACHE_SIZE) {
|
||||
const oldestKey = probeTransportCache.keys().next().value;
|
||||
if (oldestKey !== undefined) {
|
||||
probeFetcherCache.delete(oldestKey);
|
||||
probeTransportCache.delete(oldestKey);
|
||||
}
|
||||
}
|
||||
return fetcher;
|
||||
return transport;
|
||||
}
|
||||
|
||||
function resolveProbeFetcher(token: string, options?: TelegramProbeOptions): typeof fetch {
|
||||
const cacheEnabled = shouldUseProbeFetcherCache();
|
||||
const cacheKey = cacheEnabled ? buildProbeFetcherCacheKey(token, options) : null;
|
||||
function resolveProbeTransport(token: string, options?: TelegramProbeOptions): TelegramTransport {
|
||||
const cacheEnabled = shouldUseProbeTransportCache();
|
||||
const cacheKey = cacheEnabled ? buildProbeTransportCacheKey(token, options) : null;
|
||||
if (cacheKey) {
|
||||
const cachedFetcher = probeFetcherCache.get(cacheKey);
|
||||
if (cachedFetcher) {
|
||||
return cachedFetcher;
|
||||
const cached = probeTransportCache.get(cacheKey);
|
||||
if (cached) {
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
|
||||
const proxyUrl = options?.proxyUrl?.trim();
|
||||
const proxyFetch = proxyUrl ? makeProxyFetch(proxyUrl) : undefined;
|
||||
const resolved = resolveTelegramFetch(proxyFetch, {
|
||||
const transport = resolveTelegramTransport(proxyFetch, {
|
||||
network: options?.network,
|
||||
});
|
||||
|
||||
if (cacheKey) {
|
||||
return setCachedProbeFetcher(cacheKey, resolved);
|
||||
return setCachedProbeTransport(cacheKey, transport);
|
||||
}
|
||||
return resolved;
|
||||
return transport;
|
||||
}
|
||||
|
||||
function normalizeBoolean(value: unknown): boolean | null {
|
||||
@@ -148,7 +155,8 @@ export async function probeTelegram(
|
||||
const deadlineMs = started + timeoutBudgetMs;
|
||||
const options = resolveProbeOptions(proxyOrOptions);
|
||||
const includeWebhookInfo = options?.includeWebhookInfo !== false;
|
||||
const fetcher = resolveProbeFetcher(token, options);
|
||||
const transport = resolveProbeTransport(token, options);
|
||||
const fetcher = transport.fetch;
|
||||
const apiBase = resolveTelegramApiBase(options?.apiRoot);
|
||||
const base = `${apiBase}/bot${token}`;
|
||||
const retryDelayMs = Math.max(50, Math.min(1000, Math.floor(timeoutBudgetMs / 5)));
|
||||
@@ -181,6 +189,10 @@ export async function probeTelegram(
|
||||
break;
|
||||
} catch (err) {
|
||||
fetchError = err;
|
||||
// On timeout or network error, promote the transport to its IPv4
|
||||
// fallback dispatcher so the next retry (and all future probes
|
||||
// sharing this cached transport) skip the stalled IPv6 path.
|
||||
transport.forceFallback?.("probe timeout/network error");
|
||||
if (i < 2) {
|
||||
const remainingAfterAttemptMs = resolveRemainingBudgetMs();
|
||||
if (remainingAfterAttemptMs <= 0) {
|
||||
|
||||
Reference in New Issue
Block a user