mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-05 03:10:22 +00:00
feat(telegram): auto-rename DM topics on first message (#51502)
* feat(telegram): auto-rename DM topics on first message fix(telegram): use bot.api for topic rename to avoid SecretRef resolution * fix(telegram): address security + test review feedback - Fix test assertion: DEFAULT_PROMPT_SUBSTRING matches 'very short' - Use RawBody instead of Body (no envelope metadata to LLM) - Truncate user message to 500 chars for LLM prompt - Remove user-derived content from verbose logs - Remove redundant threadSpec.id null check - Fix AutoTopicLabelParams type to match generateTopicLabel * fix(telegram): use effective dm auto-topic config * fix(telegram): detect direct auto-topic overrides * fix: auto-rename Telegram DM topics on first message (#51502) (thanks @Lukavyi) --------- Co-authored-by: Ayaan Zaidi <hi@obviy.us>
This commit is contained in:
@@ -442,6 +442,7 @@ export const buildTelegramMessageContext = async ({
|
||||
msg,
|
||||
chatId,
|
||||
isGroup,
|
||||
groupConfig,
|
||||
resolvedThreadId,
|
||||
threadSpec,
|
||||
replyThreadId,
|
||||
|
||||
@@ -40,11 +40,20 @@ const listSkillCommandsForAgents = vi.hoisted(() => vi.fn(() => []));
|
||||
const wasSentByBot = vi.hoisted(() => vi.fn(() => false));
|
||||
const loadSessionStore = vi.hoisted(() => vi.fn());
|
||||
const resolveStorePath = vi.hoisted(() => vi.fn(() => "/tmp/sessions.json"));
|
||||
const generateTopicLabel = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("./draft-stream.js", () => ({
|
||||
createTelegramDraftStream,
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/reply-runtime", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("openclaw/plugin-sdk/reply-runtime")>();
|
||||
return {
|
||||
...actual,
|
||||
generateTopicLabel,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("./bot/delivery.js", () => ({
|
||||
deliverReplies,
|
||||
emitInternalMessageSentHook,
|
||||
@@ -123,6 +132,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
wasSentByBot.mockClear();
|
||||
loadSessionStore.mockClear();
|
||||
resolveStorePath.mockClear();
|
||||
generateTopicLabel.mockClear();
|
||||
loadConfig.mockReturnValue({});
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({
|
||||
queuedFinal: false,
|
||||
@@ -130,6 +140,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
});
|
||||
resolveStorePath.mockReturnValue("/tmp/sessions.json");
|
||||
loadSessionStore.mockReturnValue({});
|
||||
generateTopicLabel.mockResolvedValue("Topic label");
|
||||
});
|
||||
|
||||
const createDraftStream = (messageId?: number) => createTestDraftStream({ messageId });
|
||||
@@ -156,6 +167,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
},
|
||||
chatId: 123,
|
||||
isGroup: false,
|
||||
groupConfig: undefined,
|
||||
resolvedThreadId: undefined,
|
||||
replyThreadId: 777,
|
||||
threadSpec: { id: 777, scope: "dm" },
|
||||
@@ -196,6 +208,7 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
sendMessage: vi.fn(),
|
||||
editMessageText: vi.fn(),
|
||||
deleteMessage: vi.fn().mockResolvedValue(true),
|
||||
editForumTopic: vi.fn().mockResolvedValue(true),
|
||||
},
|
||||
} as unknown as Bot;
|
||||
}
|
||||
@@ -2385,4 +2398,36 @@ describe("dispatchTelegramMessage draft streaming", () => {
|
||||
statusReactionController.setThinking.mock.invocationCallOrder[1],
|
||||
);
|
||||
});
|
||||
|
||||
it("uses resolved DM config for auto-topic-label overrides", async () => {
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockResolvedValue({ queuedFinal: true });
|
||||
loadSessionStore.mockReturnValue({ s1: {} });
|
||||
const bot = createBot();
|
||||
|
||||
await dispatchWithContext({
|
||||
bot,
|
||||
context: createContext({
|
||||
ctxPayload: {
|
||||
SessionKey: "s1",
|
||||
RawBody: "Need help with invoices",
|
||||
} as TelegramMessageContext["ctxPayload"],
|
||||
groupConfig: {
|
||||
autoTopicLabel: false,
|
||||
} as TelegramMessageContext["groupConfig"],
|
||||
}),
|
||||
telegramCfg: { autoTopicLabel: true },
|
||||
cfg: {
|
||||
channels: {
|
||||
telegram: {
|
||||
direct: {
|
||||
"123": { autoTopicLabel: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(generateTopicLabel).not.toHaveBeenCalled();
|
||||
expect(bot.api.editForumTopic).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,12 +22,14 @@ import type {
|
||||
OpenClawConfig,
|
||||
ReplyToMode,
|
||||
TelegramAccountConfig,
|
||||
TelegramDirectConfig,
|
||||
} from "openclaw/plugin-sdk/config-runtime";
|
||||
import { getAgentScopedMediaLocalRoots } from "openclaw/plugin-sdk/media-runtime";
|
||||
import { clearHistoryEntriesIfEnabled } from "openclaw/plugin-sdk/reply-history";
|
||||
import { resolveSendableOutboundReplyParts } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { resolveChunkMode } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { resolveAutoTopicLabelConfig, generateTopicLabel } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import { danger, logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { defaultTelegramBotDeps, type TelegramBotDeps } from "./bot-deps.js";
|
||||
@@ -160,6 +162,7 @@ export const dispatchTelegramMessage = async ({
|
||||
msg,
|
||||
chatId,
|
||||
isGroup,
|
||||
groupConfig,
|
||||
threadSpec,
|
||||
historyKey,
|
||||
historyLimit,
|
||||
@@ -532,6 +535,28 @@ export const dispatchTelegramMessage = async ({
|
||||
let queuedFinal = false;
|
||||
let hadErrorReplyFailureOrSkip = false;
|
||||
|
||||
// Determine if this is the first turn in session (for auto-topic-label).
|
||||
const isDmTopic = !isGroup && threadSpec.scope === "dm" && threadSpec.id != null;
|
||||
|
||||
let isFirstTurnInSession = false;
|
||||
if (isDmTopic) {
|
||||
try {
|
||||
const storePath = resolveStorePath(cfg.session?.store, { agentId: route.agentId });
|
||||
const store = loadSessionStore(storePath, { skipCache: true });
|
||||
const sessionKey = ctxPayload.SessionKey;
|
||||
if (sessionKey) {
|
||||
const entry = resolveSessionStoreEntry({ store, sessionKey }).existing;
|
||||
isFirstTurnInSession = !entry?.systemSent;
|
||||
} else {
|
||||
logVerbose("auto-topic-label: SessionKey is absent, skipping first-turn detection");
|
||||
}
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`auto-topic-label: session store error: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (statusReactionController) {
|
||||
void statusReactionController.setThinking();
|
||||
}
|
||||
@@ -859,6 +884,46 @@ export const dispatchTelegramMessage = async ({
|
||||
return;
|
||||
}
|
||||
|
||||
// Fire-and-forget: auto-rename DM topic on first message.
|
||||
if (isDmTopic && isFirstTurnInSession) {
|
||||
const userMessage = (ctxPayload.RawBody ?? ctxPayload.Body ?? "").slice(0, 500);
|
||||
if (userMessage.trim()) {
|
||||
const agentDir = resolveAgentDir(cfg, route.agentId);
|
||||
const directConfig = !isGroup ? (groupConfig as TelegramDirectConfig | undefined) : undefined;
|
||||
const directAutoTopicLabel = directConfig?.autoTopicLabel;
|
||||
const accountAutoTopicLabel = telegramCfg?.autoTopicLabel;
|
||||
const autoTopicConfig = resolveAutoTopicLabelConfig(
|
||||
directAutoTopicLabel,
|
||||
accountAutoTopicLabel,
|
||||
);
|
||||
if (autoTopicConfig) {
|
||||
const topicThreadId = threadSpec.id!;
|
||||
void (async () => {
|
||||
try {
|
||||
const label = await generateTopicLabel({
|
||||
userMessage,
|
||||
prompt: autoTopicConfig.prompt,
|
||||
cfg,
|
||||
agentId: route.agentId,
|
||||
agentDir,
|
||||
});
|
||||
if (!label) {
|
||||
logVerbose("auto-topic-label: LLM returned empty label");
|
||||
return;
|
||||
}
|
||||
logVerbose(`auto-topic-label: generated label (len=${label.length})`);
|
||||
await bot.api.editForumTopic(chatId, topicThreadId, { name: label });
|
||||
logVerbose(`auto-topic-label: renamed topic ${chatId}/${topicThreadId}`);
|
||||
} catch (err) {
|
||||
logVerbose(
|
||||
`auto-topic-label: failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (statusReactionController) {
|
||||
void statusReactionController.setDone().catch((err) => {
|
||||
logVerbose(`telegram: status reaction finalize failed: ${String(err)}`);
|
||||
|
||||
Reference in New Issue
Block a user