mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:50:43 +00:00
fix(telegram): pass session files to native plugin commands
Pass persisted session file context into Telegram native plugin commands so topic-scoped /codex bind can attach to the active OpenClaw session. Thanks @MatthewSchleder. Validation: - pnpm plugin-sdk:api:check - pnpm test extensions/telegram/src/bot-native-commands.session-meta.test.ts extensions/telegram/src/bot-native-commands.test.ts -- --reporter=verbose - OPENCLAW_TESTBOX=1 pnpm check:changed (tbx_01kqm8kzwkdxs2ntgck6vmyrgr)
This commit is contained in:
@@ -32,6 +32,7 @@ Docs: https://docs.openclaw.ai
|
||||
|
||||
### Fixes
|
||||
|
||||
- Telegram/native commands: pass persisted session files into plugin commands for topic-bound sessions, so `/codex bind` works from Telegram forum topics. Refs #75845 and #76049. Thanks @MatthewSchleder.
|
||||
- Telegram: honor runtime conversation bindings for native slash commands in bound top-level groups, so commands like `/status@bot` route to the active non-`main` session instead of falling back to the default route. Fixes #75405; supersedes #75558. Thanks @ziptbm and @yfge.
|
||||
- Models CLI: restore `openclaw models list --provider <id>` catalog and registry fallback rows for unconfigured providers, so provider-specific verification commands no longer report "No models found." Fixes #75517; supersedes #75615. Thanks @lotsoftick and @koshaji.
|
||||
- Gateway/macOS: write LaunchAgent services with a canonical system PATH and stop preserving old plist PATH entries, so Volta, asdf, fnm, and pnpm shell paths no longer affect gateway child-process Node resolution. Fixes #75233; supersedes #75246. Thanks @nphyde2.
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
1b91ea9cadcedacd0c7e7cf9ca2e48739bd8f99a107cb59ba8b0798d0729b374 plugin-sdk-api-baseline.json
|
||||
f323d1b6e71b9e65555c13e22dcdad0cd9c9db24243dad4c7da27855d2b69888 plugin-sdk-api-baseline.jsonl
|
||||
b0424fd44d888d28f7f4ab0f653e5ae37f6ae61aad298b759ea0531edccb4405 plugin-sdk-api-baseline.json
|
||||
82a080f2ec0455f1496391dc35534545b07181655ef5d3845e8c86eda7979501 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -27,6 +27,7 @@ type DispatchReplyWithBufferedBlockDispatcherResult = Awaited<
|
||||
>;
|
||||
type DeliverRepliesFn = typeof import("./bot/delivery.js").deliverReplies;
|
||||
type DeliverRepliesParams = Parameters<DeliverRepliesFn>[0];
|
||||
type MatchPluginCommandFn = typeof import("./bot-native-commands.runtime.js").matchPluginCommand;
|
||||
|
||||
const dispatchReplyResult: DispatchReplyWithBufferedBlockDispatcherResult = {
|
||||
queuedFinal: false,
|
||||
@@ -45,11 +46,16 @@ const persistentBindingMocks = vi.hoisted(() => ({
|
||||
const sessionMocks = vi.hoisted(() => ({
|
||||
loadSessionStore: vi.fn(),
|
||||
recordSessionMetaFromInbound: vi.fn(),
|
||||
resolveAndPersistSessionFile: vi.fn(),
|
||||
resolveStorePath: vi.fn(),
|
||||
}));
|
||||
const commandAuthMocks = vi.hoisted(() => ({
|
||||
resolveCommandArgMenu: vi.fn(),
|
||||
}));
|
||||
const pluginRuntimeMocks = vi.hoisted(() => ({
|
||||
executePluginCommand: vi.fn(async () => ({ text: "ok" })),
|
||||
matchPluginCommand: vi.fn<MatchPluginCommandFn>(() => null),
|
||||
}));
|
||||
const replyMocks = vi.hoisted(() => ({
|
||||
dispatchReplyWithBufferedBlockDispatcher: vi.fn<DispatchReplyWithBufferedBlockDispatcherFn>(
|
||||
async () => dispatchReplyResult,
|
||||
@@ -148,6 +154,7 @@ vi.mock("openclaw/plugin-sdk/session-store-runtime", async () => {
|
||||
return {
|
||||
...actual,
|
||||
loadSessionStore: sessionMocks.loadSessionStore,
|
||||
resolveAndPersistSessionFile: sessionMocks.resolveAndPersistSessionFile,
|
||||
resolveStorePath: sessionMocks.resolveStorePath,
|
||||
};
|
||||
});
|
||||
@@ -178,8 +185,8 @@ vi.mock("openclaw/plugin-sdk/plugin-runtime", async () => {
|
||||
return {
|
||||
...actual,
|
||||
getPluginCommandSpecs: vi.fn(() => []),
|
||||
matchPluginCommand: vi.fn(() => null),
|
||||
executePluginCommand: vi.fn(async () => ({ text: "ok" })),
|
||||
matchPluginCommand: pluginRuntimeMocks.matchPluginCommand,
|
||||
executePluginCommand: pluginRuntimeMocks.executePluginCommand,
|
||||
};
|
||||
});
|
||||
vi.mock("./bot/delivery.js", () => ({
|
||||
@@ -192,6 +199,9 @@ vi.mock("./bot/delivery.replies.js", () => ({
|
||||
let registerTelegramNativeCommands: typeof import("./bot-native-commands.js").registerTelegramNativeCommands;
|
||||
|
||||
type TelegramCommandHandler = (ctx: unknown) => Promise<void>;
|
||||
type TelegramPluginCommandSpecs = ReturnType<
|
||||
NonNullable<TelegramNativeCommandDeps["getPluginCommandSpecs"]>
|
||||
>;
|
||||
|
||||
function registerAndResolveStatusHandler(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -233,6 +243,7 @@ function registerAndResolveCommandHandlerBase(params: {
|
||||
useAccessGroups: boolean;
|
||||
telegramCfg?: NativeCommandTestParams["telegramCfg"];
|
||||
resolveTelegramGroupConfig?: RegisterTelegramHandlerParams["resolveTelegramGroupConfig"];
|
||||
pluginCommandSpecs?: TelegramPluginCommandSpecs;
|
||||
}): {
|
||||
handler: TelegramCommandHandler;
|
||||
sendMessage: ReturnType<typeof vi.fn>;
|
||||
@@ -246,6 +257,7 @@ function registerAndResolveCommandHandlerBase(params: {
|
||||
useAccessGroups,
|
||||
telegramCfg,
|
||||
resolveTelegramGroupConfig,
|
||||
pluginCommandSpecs,
|
||||
} = params;
|
||||
const commandHandlers = new Map<string, TelegramCommandHandler>();
|
||||
const sendMessage = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -253,7 +265,7 @@ function registerAndResolveCommandHandlerBase(params: {
|
||||
getRuntimeConfig: vi.fn(() => cfg),
|
||||
readChannelAllowFromStore: vi.fn(async () => storeAllowFrom ?? []),
|
||||
dispatchReplyWithBufferedBlockDispatcher: replyMocks.dispatchReplyWithBufferedBlockDispatcher,
|
||||
getPluginCommandSpecs: vi.fn(() => []),
|
||||
getPluginCommandSpecs: vi.fn(() => pluginCommandSpecs ?? []),
|
||||
listSkillCommandsForAgents: vi.fn(() => []),
|
||||
syncTelegramMenuCommands: vi.fn(),
|
||||
};
|
||||
@@ -292,6 +304,7 @@ function registerAndResolveCommandHandler(params: {
|
||||
useAccessGroups?: boolean;
|
||||
telegramCfg?: NativeCommandTestParams["telegramCfg"];
|
||||
resolveTelegramGroupConfig?: RegisterTelegramHandlerParams["resolveTelegramGroupConfig"];
|
||||
pluginCommandSpecs?: TelegramPluginCommandSpecs;
|
||||
}): {
|
||||
handler: TelegramCommandHandler;
|
||||
sendMessage: ReturnType<typeof vi.fn>;
|
||||
@@ -305,6 +318,7 @@ function registerAndResolveCommandHandler(params: {
|
||||
useAccessGroups,
|
||||
telegramCfg,
|
||||
resolveTelegramGroupConfig,
|
||||
pluginCommandSpecs,
|
||||
} = params;
|
||||
return registerAndResolveCommandHandlerBase({
|
||||
commandName,
|
||||
@@ -315,6 +329,7 @@ function registerAndResolveCommandHandler(params: {
|
||||
useAccessGroups: useAccessGroups ?? true,
|
||||
telegramCfg,
|
||||
resolveTelegramGroupConfig,
|
||||
pluginCommandSpecs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -450,7 +465,22 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
commandAuthMocks.resolveCommandArgMenu.mockClear();
|
||||
sessionMocks.loadSessionStore.mockClear().mockReturnValue({});
|
||||
sessionMocks.recordSessionMetaFromInbound.mockClear().mockResolvedValue(undefined);
|
||||
sessionMocks.resolveAndPersistSessionFile.mockClear().mockImplementation(async (params) => {
|
||||
const sessionFile =
|
||||
params.fallbackSessionFile ?? `/tmp/openclaw-sessions/${params.sessionId}.jsonl`;
|
||||
return {
|
||||
sessionFile,
|
||||
sessionEntry: {
|
||||
...params.sessionEntry,
|
||||
sessionId: params.sessionId,
|
||||
sessionFile,
|
||||
updatedAt: Date.now(),
|
||||
},
|
||||
};
|
||||
});
|
||||
sessionMocks.resolveStorePath.mockClear().mockReturnValue("/tmp/openclaw-sessions.json");
|
||||
pluginRuntimeMocks.executePluginCommand.mockClear().mockResolvedValue({ text: "ok" });
|
||||
pluginRuntimeMocks.matchPluginCommand.mockClear().mockReturnValue(null);
|
||||
replyMocks.dispatchReplyWithBufferedBlockDispatcher
|
||||
.mockClear()
|
||||
.mockResolvedValue(dispatchReplyResult);
|
||||
@@ -1019,4 +1049,61 @@ describe("registerTelegramNativeCommands — session metadata", () => {
|
||||
|
||||
expectUnauthorizedNewCommandBlocked(sendMessage);
|
||||
});
|
||||
|
||||
it("passes a persisted topic session file to plugin commands", async () => {
|
||||
sessionMocks.resolveStorePath.mockReturnValue("/tmp/openclaw-sessions/sessions.json");
|
||||
sessionMocks.loadSessionStore.mockReturnValue({
|
||||
"agent:main:telegram:group:-1001234567890:topic:42": {
|
||||
sessionId: "sess-topic",
|
||||
updatedAt: 1,
|
||||
},
|
||||
});
|
||||
|
||||
const { handler } = registerAndResolveCommandHandler({
|
||||
commandName: "codex",
|
||||
cfg: { commands: { allowFrom: { telegram: ["200"] } } } as OpenClawConfig,
|
||||
groupAllowFrom: ["-1001234567890"],
|
||||
useAccessGroups: false,
|
||||
pluginCommandSpecs: [
|
||||
{
|
||||
name: "codex",
|
||||
description: "Codex",
|
||||
acceptsArgs: true,
|
||||
},
|
||||
] as TelegramPluginCommandSpecs,
|
||||
});
|
||||
pluginRuntimeMocks.matchPluginCommand.mockReturnValue({
|
||||
command: {
|
||||
name: "codex",
|
||||
description: "Codex",
|
||||
handler: vi.fn(),
|
||||
pluginId: "openclaw-codex-app-server",
|
||||
pluginName: "Codex",
|
||||
requireAuth: true,
|
||||
},
|
||||
args: "bind --cwd /tmp/work",
|
||||
});
|
||||
|
||||
await handler(
|
||||
createTelegramTopicCommandContext({ match: "bind --cwd /tmp/work", threadId: 42 }),
|
||||
);
|
||||
|
||||
expect(sessionMocks.resolveAndPersistSessionFile).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionId: "sess-topic",
|
||||
sessionKey: "agent:main:telegram:group:-1001234567890:topic:42",
|
||||
storePath: "/tmp/openclaw-sessions/sessions.json",
|
||||
sessionsDir: "/tmp/openclaw-sessions",
|
||||
fallbackSessionFile: "/tmp/openclaw-sessions/sess-topic-topic-42.jsonl",
|
||||
}),
|
||||
);
|
||||
expect(pluginRuntimeMocks.executePluginCommand).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
sessionKey: "agent:main:telegram:group:-1001234567890:topic:42",
|
||||
sessionId: "sess-topic",
|
||||
sessionFile: "/tmp/openclaw-sessions/sess-topic-topic-42.jsonl",
|
||||
messageThreadId: 42,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import path from "node:path";
|
||||
import type { Bot, Context } from "grammy";
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import { resolveChannelStreamingBlockEnabled } from "openclaw/plugin-sdk/channel-streaming";
|
||||
@@ -34,7 +36,9 @@ import { getChildLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
loadSessionStore,
|
||||
resolveAndPersistSessionFile,
|
||||
resolveSessionStoreEntry,
|
||||
resolveSessionTranscriptPathInDir,
|
||||
resolveStorePath,
|
||||
} from "openclaw/plugin-sdk/session-store-runtime";
|
||||
import {
|
||||
@@ -157,6 +161,43 @@ function resolveTelegramProgressPlaceholder(command: {
|
||||
return text ? text : null;
|
||||
}
|
||||
|
||||
async function resolveTelegramCommandSessionFile(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
sessionKey: string;
|
||||
threadId?: string | number;
|
||||
}): Promise<{ sessionId?: string; sessionFile?: string }> {
|
||||
const sessionKey = params.sessionKey.trim();
|
||||
if (!sessionKey) {
|
||||
return {};
|
||||
}
|
||||
try {
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, { agentId: params.agentId });
|
||||
const store = loadSessionStore(storePath);
|
||||
const resolved = resolveSessionStoreEntry({ store, sessionKey });
|
||||
const sessionId = resolved.existing?.sessionId?.trim() || randomUUID();
|
||||
const sessionsDir = path.dirname(storePath);
|
||||
const fallbackSessionFile = resolveSessionTranscriptPathInDir(
|
||||
sessionId,
|
||||
sessionsDir,
|
||||
params.threadId,
|
||||
);
|
||||
const persisted = await resolveAndPersistSessionFile({
|
||||
sessionId,
|
||||
sessionKey: resolved.normalizedKey,
|
||||
sessionStore: store,
|
||||
storePath,
|
||||
sessionEntry: resolved.existing,
|
||||
agentId: params.agentId,
|
||||
sessionsDir,
|
||||
fallbackSessionFile,
|
||||
});
|
||||
return { sessionId, sessionFile: persisted.sessionFile };
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function resolveTelegramCommandMenuModelContext(params: {
|
||||
cfg: OpenClawConfig;
|
||||
agentId: string;
|
||||
@@ -1228,6 +1269,13 @@ export const registerTelegramNativeCommands = ({
|
||||
}
|
||||
}
|
||||
|
||||
const sessionFileContext = await resolveTelegramCommandSessionFile({
|
||||
cfg: runtimeCfg,
|
||||
agentId: route.agentId,
|
||||
sessionKey: route.sessionKey,
|
||||
threadId: threadSpec.id,
|
||||
});
|
||||
|
||||
const result = await nativeCommandRuntime.executePluginCommand({
|
||||
command: match.command,
|
||||
args: match.args,
|
||||
@@ -1236,6 +1284,8 @@ export const registerTelegramNativeCommands = ({
|
||||
isAuthorizedSender: commandAuthorized,
|
||||
senderIsOwner,
|
||||
sessionKey: route.sessionKey,
|
||||
sessionId: sessionFileContext.sessionId,
|
||||
sessionFile: sessionFileContext.sessionFile,
|
||||
commandBody,
|
||||
config: runtimeCfg,
|
||||
from,
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
export { loadSessionStore } from "../config/sessions/store-load.js";
|
||||
export { resolveSessionStoreEntry } from "../config/sessions/store-entry.js";
|
||||
export { resolveStorePath } from "../config/sessions/paths.js";
|
||||
export { resolveSessionTranscriptPathInDir, resolveStorePath } from "../config/sessions/paths.js";
|
||||
export { resolveAndPersistSessionFile } from "../config/sessions/session-file.js";
|
||||
export { resolveSessionKey } from "../config/sessions/session-key.js";
|
||||
export { resolveGroupSessionKey } from "../config/sessions/group.js";
|
||||
export { canonicalizeMainSessionAlias } from "../config/sessions/main-session.js";
|
||||
|
||||
Reference in New Issue
Block a user