diff --git a/extensions/telegram/src/auto-topic-label-config.ts b/extensions/telegram/src/auto-topic-label-config.ts new file mode 100644 index 00000000000..0049aa75a30 --- /dev/null +++ b/extensions/telegram/src/auto-topic-label-config.ts @@ -0,0 +1,24 @@ +import type { + TelegramAccountConfig, + TelegramDirectConfig, +} from "openclaw/plugin-sdk/config-runtime"; + +export const AUTO_TOPIC_LABEL_DEFAULT_PROMPT = + "Generate a very short topic label (2-4 words, max 25 chars) for a chat conversation based on the user's first message below. No emoji. Use the same language as the message. Be concise and descriptive. Return ONLY the topic name, nothing else."; + +export function resolveAutoTopicLabelConfig( + directConfig?: TelegramDirectConfig["autoTopicLabel"], + accountConfig?: TelegramAccountConfig["autoTopicLabel"], +): { enabled: true; prompt: string } | null { + const config = directConfig ?? accountConfig; + if (config === undefined || config === true) { + return { enabled: true, prompt: AUTO_TOPIC_LABEL_DEFAULT_PROMPT }; + } + if (config === false || config.enabled === false) { + return null; + } + return { + enabled: true, + prompt: config.prompt?.trim() || AUTO_TOPIC_LABEL_DEFAULT_PROMPT, + }; +} diff --git a/extensions/telegram/src/auto-topic-label.test.ts b/extensions/telegram/src/auto-topic-label.test.ts index 0ff80793dbd..b5ecaf4a7c7 100644 --- a/extensions/telegram/src/auto-topic-label.test.ts +++ b/extensions/telegram/src/auto-topic-label.test.ts @@ -8,9 +8,9 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({ import { AUTO_TOPIC_LABEL_DEFAULT_PROMPT, - generateTelegramTopicLabel, resolveAutoTopicLabelConfig, -} from "./auto-topic-label.js"; +} from "./auto-topic-label-config.js"; +import { generateTelegramTopicLabel } from "./auto-topic-label.js"; describe("resolveAutoTopicLabelConfig", () => { it("returns enabled with default prompt when configs are undefined", () => { diff --git a/extensions/telegram/src/auto-topic-label.ts b/extensions/telegram/src/auto-topic-label.ts index 004509b2b47..22095ebf5d0 100644 --- a/extensions/telegram/src/auto-topic-label.ts +++ b/extensions/telegram/src/auto-topic-label.ts @@ -1,29 +1,9 @@ -import type { - OpenClawConfig, - TelegramAccountConfig, - TelegramDirectConfig, -} from "openclaw/plugin-sdk/config-runtime"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { generateConversationLabel } from "openclaw/plugin-sdk/reply-runtime"; - -export const AUTO_TOPIC_LABEL_DEFAULT_PROMPT = - "Generate a very short topic label (2-4 words, max 25 chars) for a chat conversation based on the user's first message below. No emoji. Use the same language as the message. Be concise and descriptive. Return ONLY the topic name, nothing else."; - -export function resolveAutoTopicLabelConfig( - directConfig?: TelegramDirectConfig["autoTopicLabel"], - accountConfig?: TelegramAccountConfig["autoTopicLabel"], -): { enabled: true; prompt: string } | null { - const config = directConfig ?? accountConfig; - if (config === undefined || config === true) { - return { enabled: true, prompt: AUTO_TOPIC_LABEL_DEFAULT_PROMPT }; - } - if (config === false || config.enabled === false) { - return null; - } - return { - enabled: true, - prompt: config.prompt?.trim() || AUTO_TOPIC_LABEL_DEFAULT_PROMPT, - }; -} +export { + AUTO_TOPIC_LABEL_DEFAULT_PROMPT, + resolveAutoTopicLabelConfig, +} from "./auto-topic-label-config.js"; export async function generateTelegramTopicLabel(params: { userMessage: string; diff --git a/extensions/telegram/src/bot-message-dispatch.test.ts b/extensions/telegram/src/bot-message-dispatch.test.ts index 5010550823d..8277db0a7d6 100644 --- a/extensions/telegram/src/bot-message-dispatch.test.ts +++ b/extensions/telegram/src/bot-message-dispatch.test.ts @@ -5,7 +5,7 @@ import { resolveMarkdownTableMode as resolveMarkdownTableModeRuntime } from "../ import { resolveSessionStoreEntry as resolveSessionStoreEntryRuntime } from "../../../src/config/sessions/store.js"; import type { OpenClawConfig } from "../../../src/config/types.openclaw.js"; import { getAgentScopedMediaLocalRoots as getAgentScopedMediaLocalRootsRuntime } from "../../../src/media/local-roots.js"; -import { resolveAutoTopicLabelConfig as resolveAutoTopicLabelConfigRuntime } from "./auto-topic-label.js"; +import { resolveAutoTopicLabelConfig as resolveAutoTopicLabelConfigRuntime } from "./auto-topic-label-config.js"; import type { TelegramBotDeps } from "./bot-deps.js"; import { createSequencedTestDraftStream, diff --git a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts index 613e53bd176..6b7d803bc9c 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test-harness.ts @@ -1,10 +1,5 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { - createReplyDispatcher, - resetInboundDedupe, - type GetReplyOptions, - type MsgContext, -} from "openclaw/plugin-sdk/reply-runtime"; +import type { GetReplyOptions, MsgContext } from "openclaw/plugin-sdk/reply-runtime"; import type { MockFn } from "openclaw/plugin-sdk/testing"; import { beforeEach, vi } from "vitest"; import type { TelegramBotDeps } from "./bot-deps.js"; @@ -31,12 +26,6 @@ type ReplyPayloadLike = { replyToId?: string; }; -const _EMPTY_REPLY_COUNTS: DispatchReplyWithBufferedBlockDispatcherResult["counts"] = { - block: 0, - final: 0, - tool: 0, -}; - const { sessionStorePath } = vi.hoisted(() => ({ sessionStorePath: `/tmp/openclaw-telegram-${process.pid}-${process.env.VITEST_POOL_ID ?? "0"}.json`, })); @@ -133,29 +122,22 @@ async function dispatchHarnessReplies( const reply = await runReply(params); const payloads: ReplyPayloadLike[] = reply === undefined ? [] : Array.isArray(reply) ? reply : [reply]; - const dispatcher = createReplyDispatcher({ - deliver: async (payload, info) => { - await params.dispatcherOptions.deliver?.(payload, info); - }, - responsePrefix: params.dispatcherOptions.responsePrefix, - responsePrefixContextProvider: params.dispatcherOptions.responsePrefixContextProvider, - responsePrefixContext: params.dispatcherOptions.responsePrefixContext, - onHeartbeatStrip: params.dispatcherOptions.onHeartbeatStrip, - onSkip: (payload, info) => { - params.dispatcherOptions.onSkip?.(payload, info); - }, - onError: (err, info) => { - params.dispatcherOptions.onError?.(err, info); - }, - }); let finalCount = 0; for (const payload of payloads) { - if (dispatcher.sendFinalReply(payload)) { + const text = + typeof payload.text === "string" && + params.dispatcherOptions.responsePrefix && + !payload.text.startsWith(params.dispatcherOptions.responsePrefix) + ? `${params.dispatcherOptions.responsePrefix} ${payload.text}` + : payload.text; + const finalPayload = text === payload.text ? payload : { ...payload, text }; + try { + await params.dispatcherOptions.deliver?.(finalPayload, { kind: "final" }); finalCount += 1; + } catch (err) { + params.dispatcherOptions.onError?.(err, { kind: "final" }); } } - dispatcher.markComplete(); - await dispatcher.waitForIdle(); return { queuedFinal: finalCount > 0, counts: { @@ -472,7 +454,6 @@ export function makeForumGroupMessageCtx(params?: { } beforeEach(() => { - resetInboundDedupe(); loadConfig.mockReset(); loadConfig.mockReturnValue(DEFAULT_TELEGRAM_TEST_CONFIG); sessionStoreEntries.value = {}; diff --git a/extensions/telegram/src/bot.create-telegram-bot.test.ts b/extensions/telegram/src/bot.create-telegram-bot.test.ts index fceb0da8546..afa2ef3401f 100644 --- a/extensions/telegram/src/bot.create-telegram-bot.test.ts +++ b/extensions/telegram/src/bot.create-telegram-bot.test.ts @@ -1,5 +1,4 @@ import type { GetReplyOptions, MsgContext } from "openclaw/plugin-sdk/reply-runtime"; -import { withEnvAsync } from "openclaw/plugin-sdk/testing"; import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { escapeRegExp, formatEnvelopeTimestamp } from "../../../test/helpers/envelope-timestamp.js"; const harness = await import("./bot.create-telegram-bot.test-harness.js"); @@ -124,6 +123,29 @@ function mockTelegramConfigWrites() { return vi.spyOn(configRuntime, "writeConfigFile").mockResolvedValue(undefined); } +async function withEnvAsync(env: Record, fn: () => Promise) { + const previous = new Map(); + for (const [key, value] of Object.entries(env)) { + previous.set(key, process.env[key]); + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + try { + await fn(); + } finally { + for (const [key, value] of previous) { + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } + } +} + describe("createTelegramBot", () => { beforeAll(() => { process.env.TZ = "UTC";