mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 11:55:47 -06:00
refactor(webui): read slash commands through the chat metadata store (#124931)
This commit is contained in:
committed by
GitHub
parent
562ac194e0
commit
e95300fa3e
@@ -2,13 +2,21 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { ApplicationGatewaySnapshot } from "../../app/gateway.ts";
|
||||
import {
|
||||
invalidateChatMetadataStore,
|
||||
rememberChatMetadata,
|
||||
} from "../../lib/chat/chat-metadata-store.ts";
|
||||
import {
|
||||
SLASH_COMMANDS,
|
||||
getSlashCommandCategoryLabel,
|
||||
getSlashCommandDescription,
|
||||
type SlashCommandDef,
|
||||
} from "../../lib/chat/commands.ts";
|
||||
import { dispatchChatSlashCommand, refreshSlashCommands } from "./chat-commands.ts";
|
||||
import {
|
||||
applyRemoteSlashCommandsResult,
|
||||
dispatchChatSlashCommand,
|
||||
refreshSlashCommands,
|
||||
} from "./chat-commands.ts";
|
||||
|
||||
function requireCommandByName(name: string): Record<string, unknown> {
|
||||
const command = SLASH_COMMANDS.find((entry) => entry.name === name);
|
||||
@@ -36,6 +44,17 @@ function legacyConnectedSessionAccess() {
|
||||
};
|
||||
}
|
||||
|
||||
function remoteCommand(name: string, description: string) {
|
||||
return {
|
||||
name,
|
||||
textAliases: [`/${name}`],
|
||||
description,
|
||||
source: "plugin" as const,
|
||||
scope: "text" as const,
|
||||
acceptsArgs: false,
|
||||
};
|
||||
}
|
||||
|
||||
describe("refreshSlashCommands", () => {
|
||||
it("resolves localized UI command metadata", () => {
|
||||
const clear = SLASH_COMMANDS.find((entry) => entry.name === "clear");
|
||||
@@ -252,6 +271,67 @@ describe("refreshSlashCommands", () => {
|
||||
description: "Generate setup codes.",
|
||||
});
|
||||
});
|
||||
|
||||
it("reads commands from the chat metadata store without requesting commands.list", async () => {
|
||||
const request = vi.fn();
|
||||
const client = { request } as never;
|
||||
rememberChatMetadata(client, "main", {
|
||||
commands: [remoteCommand("metadata-command", "Loaded from chat metadata.")],
|
||||
});
|
||||
|
||||
await refreshSlashCommands({ client, agentId: "main" });
|
||||
|
||||
expect(request).not.toHaveBeenCalled();
|
||||
expectRecordFields(requireCommandByName("metadata-command"), "metadata command", {
|
||||
description: "Loaded from chat metadata.",
|
||||
executeLocal: false,
|
||||
});
|
||||
});
|
||||
|
||||
it("prefers stored metadata after the commands.list cache expires", async () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
const request = vi.fn().mockResolvedValue({
|
||||
commands: [remoteCommand("cached-command", "Loaded from commands.list.")],
|
||||
});
|
||||
const client = { request } as never;
|
||||
|
||||
await refreshSlashCommands({ client, agentId: "main" });
|
||||
vi.advanceTimersByTime(60_001);
|
||||
rememberChatMetadata(client, "main", {
|
||||
commands: [remoteCommand("metadata-command", "Loaded from chat metadata.")],
|
||||
});
|
||||
|
||||
await refreshSlashCommands({ client, agentId: "main" });
|
||||
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expectRecordFields(requireCommandByName("metadata-command"), "metadata command", {
|
||||
description: "Loaded from chat metadata.",
|
||||
});
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
});
|
||||
|
||||
it("does not retain applied metadata commands in the commands.list cache", async () => {
|
||||
const request = vi.fn().mockResolvedValue({
|
||||
commands: [remoteCommand("requested-command", "Loaded after metadata invalidation.")],
|
||||
});
|
||||
const client = { request } as never;
|
||||
const metadata = {
|
||||
commands: [remoteCommand("metadata-command", "Loaded from chat metadata.")],
|
||||
};
|
||||
rememberChatMetadata(client, "main", metadata);
|
||||
applyRemoteSlashCommandsResult({ client, agentId: "main", result: metadata });
|
||||
|
||||
invalidateChatMetadataStore(client);
|
||||
await refreshSlashCommands({ client, agentId: "main" });
|
||||
|
||||
expect(request).toHaveBeenCalledOnce();
|
||||
expectRecordFields(requireCommandByName("requested-command"), "requested command", {
|
||||
description: "Loaded after metadata invalidation.",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("conversation reset confirmation", () => {
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CommandsListResult } from "../../../../packages/gateway-protocol/s
|
||||
import type { GatewayBrowserClient } from "../../api/gateway.ts";
|
||||
import type { ModelCatalogEntry, SessionsListResult } from "../../api/types.ts";
|
||||
import type { ApplicationGatewaySnapshot } from "../../app/gateway.ts";
|
||||
import { peekChatMetadata } from "../../lib/chat/chat-metadata-store.ts";
|
||||
import type { ChatQueueItem } from "../../lib/chat/chat-types.ts";
|
||||
import {
|
||||
buildFallbackSlashCommands,
|
||||
@@ -200,17 +201,6 @@ function getRemoteSlashCommandCache(
|
||||
return cache;
|
||||
}
|
||||
|
||||
function storeRemoteSlashCommands(
|
||||
client: GatewayBrowserClient,
|
||||
agentId: string | undefined,
|
||||
commands: SlashCommandDef[],
|
||||
) {
|
||||
getRemoteSlashCommandCache(client).set(remoteSlashCommandCacheKey(agentId), {
|
||||
commands,
|
||||
expiresAt: Date.now() + REMOTE_SLASH_COMMAND_CACHE_TTL_MS,
|
||||
});
|
||||
}
|
||||
|
||||
async function requestRemoteSlashCommands(
|
||||
client: GatewayBrowserClient,
|
||||
agentId: string | undefined,
|
||||
@@ -226,7 +216,10 @@ async function requestRemoteSlashCommands(
|
||||
return buildFallbackSlashCommands();
|
||||
}
|
||||
const commands = buildSlashCommandsFromEntries(getRemoteCommandEntries(result));
|
||||
storeRemoteSlashCommands(client, agentId, commands);
|
||||
getRemoteSlashCommandCache(client).set(remoteSlashCommandCacheKey(agentId), {
|
||||
commands,
|
||||
expiresAt: Date.now() + REMOTE_SLASH_COMMAND_CACHE_TTL_MS,
|
||||
});
|
||||
return commands;
|
||||
} catch {
|
||||
return fallback ?? buildFallbackSlashCommands();
|
||||
@@ -237,6 +230,12 @@ function loadRemoteSlashCommands(
|
||||
client: GatewayBrowserClient,
|
||||
agentId: string | undefined,
|
||||
): Promise<SlashCommandDef[]> {
|
||||
const metadata = peekChatMetadata(client, agentId);
|
||||
// Store-held metadata carries app-level invalidation on config changes and logical reconnects,
|
||||
// so no TTL applies here. The cache below owns only commands.list-derived entries.
|
||||
if (Array.isArray(metadata?.commands)) {
|
||||
return Promise.resolve(buildSlashCommandsFromEntries(getRemoteCommandEntries(metadata)));
|
||||
}
|
||||
const cache = getRemoteSlashCommandCache(client);
|
||||
const key = remoteSlashCommandCacheKey(agentId);
|
||||
const cached = cache.get(key);
|
||||
@@ -269,11 +268,7 @@ export function applyRemoteSlashCommandsResult(params: {
|
||||
if (!Array.isArray(params.result?.commands)) {
|
||||
return false;
|
||||
}
|
||||
const agentId = params.agentId?.trim();
|
||||
const commands = buildSlashCommandsFromEntries(getRemoteCommandEntries(params.result));
|
||||
if (params.client) {
|
||||
storeRemoteSlashCommands(params.client, agentId, commands);
|
||||
}
|
||||
refreshSeq += 1;
|
||||
replaceSlashCommands(commands);
|
||||
return true;
|
||||
|
||||
Reference in New Issue
Block a user