Merge ca9d043e36ddcdbc3dd9c0ac7a567b333c865251 into 8a05c05596ca9ba0735dafd8e359885de4c2c969
This commit is contained in:
commit
d892563468
@ -5,12 +5,14 @@ import {
|
||||
Row,
|
||||
StringSelectMenu,
|
||||
TextDisplay,
|
||||
type AutocompleteInteraction,
|
||||
type ButtonInteraction,
|
||||
type CommandInteraction,
|
||||
type ComponentData,
|
||||
type StringSelectMenuInteraction,
|
||||
} from "@buape/carbon";
|
||||
import { ButtonStyle } from "discord-api-types/v10";
|
||||
import { resolveDefaultModelForAgent } from "openclaw/plugin-sdk/agent-runtime";
|
||||
import {
|
||||
buildCommandTextFromArgs,
|
||||
findCommandByNativeName,
|
||||
@ -25,6 +27,7 @@ import {
|
||||
} from "openclaw/plugin-sdk/command-auth";
|
||||
import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { loadSessionStore, resolveStorePath } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveConfiguredBindingRoute } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import type { ResolvedAgentRoute } from "openclaw/plugin-sdk/routing";
|
||||
import { logVerbose } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { chunkItems, withTimeout } from "openclaw/plugin-sdk/text-runtime";
|
||||
@ -45,7 +48,10 @@ import {
|
||||
toDiscordModelPickerMessagePayload,
|
||||
type DiscordModelPickerCommandContext,
|
||||
} from "./model-picker.js";
|
||||
import { resolveDiscordBoundConversationRoute } from "./route-resolution.js";
|
||||
import {
|
||||
resolveDiscordBoundConversationRoute,
|
||||
resolveDiscordEffectiveRoute,
|
||||
} from "./route-resolution.js";
|
||||
import type { ThreadBindingManager } from "./thread-bindings.js";
|
||||
import { resolveDiscordThreadParentInfo } from "./threading.js";
|
||||
|
||||
@ -218,7 +224,11 @@ function buildDiscordModelPickerNoticePayload(message: string): { components: Co
|
||||
}
|
||||
|
||||
async function resolveDiscordModelPickerRoute(params: {
|
||||
interaction: CommandInteraction | ButtonInteraction | StringSelectMenuInteraction;
|
||||
interaction:
|
||||
| CommandInteraction
|
||||
| ButtonInteraction
|
||||
| StringSelectMenuInteraction
|
||||
| AutocompleteInteraction;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
accountId: string;
|
||||
threadBindings: ThreadBindingManager;
|
||||
@ -252,10 +262,7 @@ async function resolveDiscordModelPickerRoute(params: {
|
||||
threadParentId = parentInfo.id;
|
||||
}
|
||||
|
||||
const threadBinding = isThreadChannel
|
||||
? params.threadBindings.getByThreadId(rawChannelId)
|
||||
: undefined;
|
||||
return resolveDiscordBoundConversationRoute({
|
||||
const route = resolveDiscordBoundConversationRoute({
|
||||
cfg,
|
||||
accountId,
|
||||
guildId: interaction.guild?.id ?? undefined,
|
||||
@ -265,8 +272,74 @@ async function resolveDiscordModelPickerRoute(params: {
|
||||
directUserId: interaction.user?.id ?? rawChannelId,
|
||||
conversationId: rawChannelId,
|
||||
parentConversationId: threadParentId,
|
||||
boundSessionKey: threadBinding?.targetSessionKey,
|
||||
});
|
||||
const threadBinding = isThreadChannel
|
||||
? params.threadBindings.getByThreadId(rawChannelId)
|
||||
: undefined;
|
||||
const configuredRoute =
|
||||
threadBinding == null
|
||||
? resolveConfiguredBindingRoute({
|
||||
cfg,
|
||||
route,
|
||||
conversation: {
|
||||
channel: "discord",
|
||||
accountId,
|
||||
conversationId: rawChannelId,
|
||||
parentConversationId: threadParentId,
|
||||
},
|
||||
})
|
||||
: null;
|
||||
const configuredBinding = configuredRoute?.bindingResolution ?? null;
|
||||
const configuredBoundSessionKey = configuredRoute?.boundSessionKey?.trim() || undefined;
|
||||
const boundSessionKey = threadBinding?.targetSessionKey?.trim() || configuredBoundSessionKey;
|
||||
return resolveDiscordEffectiveRoute({
|
||||
route,
|
||||
boundSessionKey,
|
||||
configuredRoute,
|
||||
matchedBy: configuredBinding ? "binding.channel" : undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export async function resolveDiscordNativeChoiceContext(params: {
|
||||
interaction: AutocompleteInteraction;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
accountId: string;
|
||||
threadBindings: ThreadBindingManager;
|
||||
}): Promise<{ provider?: string; model?: string } | null> {
|
||||
try {
|
||||
const route = await resolveDiscordModelPickerRoute({
|
||||
interaction: params.interaction,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
threadBindings: params.threadBindings,
|
||||
});
|
||||
const fallback = resolveDefaultModelForAgent({
|
||||
cfg: params.cfg,
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const storePath = resolveStorePath(params.cfg.session?.store, {
|
||||
agentId: route.agentId,
|
||||
});
|
||||
const sessionStore = loadSessionStore(storePath);
|
||||
const sessionEntry = sessionStore[route.sessionKey];
|
||||
const override = resolveStoredModelOverride({
|
||||
sessionEntry,
|
||||
sessionStore,
|
||||
sessionKey: route.sessionKey,
|
||||
});
|
||||
if (!override?.model) {
|
||||
return {
|
||||
provider: fallback.provider,
|
||||
model: fallback.model,
|
||||
};
|
||||
}
|
||||
return {
|
||||
provider: override.provider || fallback.provider,
|
||||
model: override.model,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function resolveDiscordModelPickerCurrentModel(params: {
|
||||
|
||||
@ -0,0 +1,101 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { ChannelType, type AutocompleteInteraction } from "@buape/carbon";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
findCommandByNativeName,
|
||||
resolveCommandArgChoices,
|
||||
} from "../../../../src/auto-reply/commands-registry.js";
|
||||
import type { OpenClawConfig, loadConfig } from "../../../../src/config/config.js";
|
||||
import { clearSessionStoreCacheForTest } from "../../../../src/config/sessions/store.js";
|
||||
import { resolveDiscordNativeChoiceContext } from "./native-command-ui.js";
|
||||
import { createNoopThreadBindingManager } from "./thread-bindings.js";
|
||||
|
||||
const STORE_PATH = path.join(
|
||||
os.tmpdir(),
|
||||
`openclaw-discord-think-autocomplete-${process.pid}.json`,
|
||||
);
|
||||
const SESSION_KEY = "agent:main:main";
|
||||
|
||||
describe("discord native /think autocomplete", () => {
|
||||
beforeEach(() => {
|
||||
clearSessionStoreCacheForTest();
|
||||
fs.mkdirSync(path.dirname(STORE_PATH), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
STORE_PATH,
|
||||
JSON.stringify({
|
||||
[SESSION_KEY]: {
|
||||
updatedAt: Date.now(),
|
||||
providerOverride: "openai-codex",
|
||||
modelOverride: "gpt-5.4",
|
||||
},
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearSessionStoreCacheForTest();
|
||||
try {
|
||||
fs.unlinkSync(STORE_PATH);
|
||||
} catch {}
|
||||
});
|
||||
|
||||
it("uses the session override context for /think choices", async () => {
|
||||
const cfg = {
|
||||
agents: {
|
||||
defaults: {
|
||||
model: {
|
||||
primary: "anthropic/claude-sonnet-4.5",
|
||||
},
|
||||
},
|
||||
},
|
||||
session: {
|
||||
store: STORE_PATH,
|
||||
},
|
||||
} as ReturnType<typeof loadConfig>;
|
||||
const interaction = {
|
||||
options: {
|
||||
getFocused: () => ({ value: "xh" }),
|
||||
},
|
||||
respond: async (_choices: Array<{ name: string; value: string }>) => {},
|
||||
rawData: {},
|
||||
channel: { id: "D1", type: ChannelType.DM },
|
||||
user: { id: "U1" },
|
||||
guild: undefined,
|
||||
client: {},
|
||||
} as unknown as AutocompleteInteraction & {
|
||||
respond: (choices: Array<{ name: string; value: string }>) => Promise<void>;
|
||||
};
|
||||
|
||||
const command = findCommandByNativeName("think", "discord");
|
||||
expect(command).toBeTruthy();
|
||||
const levelArg = command?.args?.find((entry) => entry.name === "level");
|
||||
expect(levelArg).toBeTruthy();
|
||||
if (!command || !levelArg) {
|
||||
return;
|
||||
}
|
||||
|
||||
const context = await resolveDiscordNativeChoiceContext({
|
||||
interaction,
|
||||
cfg,
|
||||
accountId: "default",
|
||||
threadBindings: createNoopThreadBindingManager("default"),
|
||||
});
|
||||
expect(context).toEqual({
|
||||
provider: "openai-codex",
|
||||
model: "gpt-5.4",
|
||||
});
|
||||
|
||||
const choices = resolveCommandArgChoices({
|
||||
command,
|
||||
arg: levelArg,
|
||||
cfg,
|
||||
provider: context?.provider,
|
||||
model: context?.model,
|
||||
});
|
||||
const values = choices.map((choice) => choice.value);
|
||||
expect(values).toContain("xhigh");
|
||||
});
|
||||
});
|
||||
@ -73,6 +73,7 @@ import {
|
||||
createDiscordModelPickerFallbackButton as createDiscordModelPickerFallbackButtonUi,
|
||||
createDiscordModelPickerFallbackSelect as createDiscordModelPickerFallbackSelectUi,
|
||||
replyWithDiscordModelPickerProviders,
|
||||
resolveDiscordNativeChoiceContext,
|
||||
shouldOpenDiscordModelPickerFromCommand,
|
||||
type DiscordCommandArgContext,
|
||||
type DiscordModelPickerContext,
|
||||
@ -124,8 +125,11 @@ function resolveDiscordNativeCommandAllowlistAccess(params: {
|
||||
function buildDiscordCommandOptions(params: {
|
||||
command: ChatCommandDefinition;
|
||||
cfg: ReturnType<typeof loadConfig>;
|
||||
resolveChoiceContext?: (
|
||||
interaction: AutocompleteInteraction,
|
||||
) => Promise<{ provider?: string; model?: string } | null>;
|
||||
}): CommandOptions | undefined {
|
||||
const { command, cfg } = params;
|
||||
const { command, cfg, resolveChoiceContext } = params;
|
||||
const args = command.args;
|
||||
if (!args || args.length === 0) {
|
||||
return undefined;
|
||||
@ -158,7 +162,17 @@ function buildDiscordCommandOptions(params: {
|
||||
const focused = interaction.options.getFocused();
|
||||
const focusValue =
|
||||
typeof focused?.value === "string" ? focused.value.trim().toLowerCase() : "";
|
||||
const choices = resolveCommandArgChoices({ command, arg, cfg });
|
||||
const context =
|
||||
typeof arg.choices === "function" && resolveChoiceContext
|
||||
? await resolveChoiceContext(interaction)
|
||||
: null;
|
||||
const choices = resolveCommandArgChoices({
|
||||
command,
|
||||
arg,
|
||||
cfg,
|
||||
provider: context?.provider,
|
||||
model: context?.model,
|
||||
});
|
||||
const filtered = focusValue
|
||||
? choices.filter((choice) => choice.label.toLowerCase().includes(focusValue))
|
||||
: choices;
|
||||
@ -297,6 +311,13 @@ export function createDiscordNativeCommand(params: {
|
||||
const commandOptions = buildDiscordCommandOptions({
|
||||
command: commandDefinition,
|
||||
cfg,
|
||||
resolveChoiceContext: async (interaction) =>
|
||||
resolveDiscordNativeChoiceContext({
|
||||
interaction,
|
||||
cfg,
|
||||
accountId,
|
||||
threadBindings,
|
||||
}),
|
||||
});
|
||||
const options = commandOptions
|
||||
? (commandOptions satisfies CommandOptions)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user