

























1-// Telegram tests cover bot plugin behavior.
2-import { rm } from "node:fs/promises";
1+import { rm, writeFile } from "node:fs/promises";
2+import path from "node:path";
33import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
44import {
55clearPluginInteractiveHandlers,
@@ -14,6 +14,10 @@ import type { MsgContext } from "openclaw/plugin-sdk/reply-runtime";
1414import { loadSessionStore } from "openclaw/plugin-sdk/session-store-runtime";
1515import { mockPinnedHostnameResolution } from "openclaw/plugin-sdk/test-env";
1616import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
17+import {
18+resolveTelegramConversationBaseSessionKey,
19+resolveTelegramConversationRoute,
20+} from "./conversation-route.js";
1721import type { TelegramInteractiveHandlerContext } from "./interactive-dispatch.js";
1822import { buildTelegramOpaqueCallbackData } from "./native-command-callback-data.js";
1923import { clearTelegramRuntime, setTelegramRuntime } from "./runtime.js";
@@ -145,6 +149,59 @@ function mockMsgContextArg(
145149return mockArg(source, callIndex, argIndex, label) as MsgContext;
146150}
147151152+async function writeDirectTelegramTranscriptContext(params: {
153+cfg: OpenClawConfig;
154+storePath: string;
155+chatId: number;
156+senderId: number;
157+sessionId: string;
158+text: string;
159+timestamp: number;
160+}) {
161+const route = resolveTelegramConversationRoute({
162+cfg: params.cfg,
163+accountId: "default",
164+chatId: params.chatId,
165+isGroup: false,
166+senderId: params.senderId,
167+}).route;
168+const sessionKey = resolveTelegramConversationBaseSessionKey({
169+cfg: params.cfg,
170+ route,
171+chatId: params.chatId,
172+isGroup: false,
173+senderId: params.senderId,
174+});
175+await writeFile(
176+params.storePath,
177+JSON.stringify({
178+[sessionKey]: {
179+sessionId: params.sessionId,
180+chatType: "direct",
181+channel: "telegram",
182+},
183+}),
184+"utf-8",
185+);
186+await writeFile(
187+path.join(path.dirname(params.storePath), `${params.sessionId}.jsonl`),
188+[
189+JSON.stringify({ type: "session", id: params.sessionId }),
190+JSON.stringify({
191+id: "transcript-user-1",
192+type: "message",
193+message: {
194+role: "user",
195+content: params.text,
196+timestamp: params.timestamp,
197+},
198+}),
199+"",
200+].join("\n"),
201+"utf-8",
202+);
203+}
204+148205function execApprovalCall(index = 0) {
149206return requireRecord(
150207mockArg(resolveExecApprovalSpy as unknown as MockCallSource, index, 0, "exec approval call"),
@@ -2026,6 +2083,153 @@ describe("createTelegramBot", () => {
20262083expect(messagesById.get("35016")?.body).not.toBe("K");
20272084});
202820852086+it("keeps direct Telegram media context when transcript context exists", async () => {
2087+onSpy.mockClear();
2088+replySpy.mockClear();
2089+2090+const storePath = `/tmp/openclaw-telegram-dm-media-context-${process.pid}-${Date.now()}.json`;
2091+const config = {
2092+channels: {
2093+telegram: {
2094+dmPolicy: "open",
2095+allowFrom: ["*"],
2096+},
2097+},
2098+session: {
2099+store: storePath,
2100+},
2101+} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
2102+2103+await rm(storePath, { force: true });
2104+try {
2105+loadConfig.mockReturnValue(config);
2106+createTelegramBot({ token: "tok", config });
2107+const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
2108+const baseCtx = {
2109+me: { id: 999, username: "openclaw_bot" },
2110+getFile: async () => ({ download: async () => new Uint8Array() }),
2111+};
2112+2113+await handler({
2114+ ...baseCtx,
2115+message: {
2116+chat: { id: 7771, type: "private" },
2117+caption: "the reference image",
2118+date: 1778474800,
2119+message_id: 100,
2120+from: { id: 202, is_bot: false, first_name: "Kesava" },
2121+photo: [{ file_id: "reference-photo-1", width: 1, height: 1 }],
2122+},
2123+});
2124+2125+await writeDirectTelegramTranscriptContext({
2126+cfg: config,
2127+ storePath,
2128+chatId: 7771,
2129+senderId: 202,
2130+sessionId: "telegram-dm-media-context-session",
2131+text: "remember the launch checklist",
2132+timestamp: 1778474700000,
2133+});
2134+2135+replySpy.mockClear();
2136+await handler({
2137+ ...baseCtx,
2138+message: {
2139+chat: { id: 7771, type: "private" },
2140+text: "what about the image above?",
2141+date: 1778474850,
2142+message_id: 101,
2143+from: { id: 202, is_bot: false, first_name: "Kesava" },
2144+},
2145+});
2146+2147+expect(replySpy).toHaveBeenCalledTimes(1);
2148+const payload = mockMsgContextArg(
2149+replySpy as unknown as MockCallSource,
2150+0,
2151+0,
2152+"replySpy call",
2153+);
2154+const [conversationContext] = requireArray(
2155+payload.UntrustedStructuredContext,
2156+"structured context",
2157+);
2158+const contextRecord = requireRecord(conversationContext, "conversation context");
2159+const contextPayload = requireRecord(contextRecord.payload, "conversation context payload");
2160+const messages = requireArray(contextPayload.messages, "conversation context messages").map(
2161+(message, index) => requireRecord(message, `conversation context message ${index + 1}`),
2162+);
2163+expect(messages.some((message) => message.body === "remember the launch checklist")).toBe(
2164+true,
2165+);
2166+const photoMessage = messages.find((message) => message.message_id === "100");
2167+expect(photoMessage?.body).toBe("the reference image");
2168+expect(photoMessage?.media_ref).toBe("telegram:file/reference-photo-1");
2169+} finally {
2170+await rm(storePath, { force: true });
2171+}
2172+});
2173+2174+it("skips direct transcript context for hard reset messages", async () => {
2175+onSpy.mockClear();
2176+replySpy.mockClear();
2177+2178+const storePath = `/tmp/openclaw-telegram-dm-reset-context-${process.pid}-${Date.now()}.json`;
2179+const config = {
2180+channels: {
2181+telegram: {
2182+dmPolicy: "open",
2183+allowFrom: ["*"],
2184+},
2185+},
2186+session: {
2187+store: storePath,
2188+},
2189+} satisfies NonNullable<Parameters<typeof createTelegramBot>[0]["config"]>;
2190+2191+await rm(storePath, { force: true });
2192+try {
2193+loadConfig.mockReturnValue(config);
2194+createTelegramBot({ token: "tok", config });
2195+await writeDirectTelegramTranscriptContext({
2196+cfg: config,
2197+ storePath,
2198+chatId: 7772,
2199+senderId: 202,
2200+sessionId: "telegram-dm-reset-context-session",
2201+text: "old private transcript text",
2202+timestamp: 1778474700000,
2203+});
2204+2205+const handler = getOnHandler("message") as (ctx: Record<string, unknown>) => Promise<void>;
2206+await handler({
2207+me: { id: 999, username: "openclaw_bot" },
2208+getFile: async () => ({ download: async () => new Uint8Array() }),
2209+message: {
2210+chat: { id: 7772, type: "private" },
2211+text: "/reset summarize my workspace",
2212+date: 1778474850,
2213+message_id: 101,
2214+from: { id: 202, is_bot: false, first_name: "Kesava" },
2215+},
2216+});
2217+2218+expect(replySpy).toHaveBeenCalledTimes(1);
2219+const payload = mockMsgContextArg(
2220+replySpy as unknown as MockCallSource,
2221+0,
2222+0,
2223+"replySpy call",
2224+);
2225+expect(JSON.stringify(payload.UntrustedStructuredContext ?? [])).not.toContain(
2226+"old private transcript text",
2227+);
2228+} finally {
2229+await rm(storePath, { force: true });
2230+}
2231+});
2232+20292233it("uses quote text when a Telegram partial reply is received", async () => {
20302234onSpy.mockClear();
20312235sendMessageSpy.mockClear();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。