mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:10:49 +00:00
fix(discord): return native status replies directly (#66434)
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/subagents: emit the subagent registry lazy-runtime stub on the stable dist path that both source and bundled runtime imports resolve, so the follow-up dist fix no longer still fails with `ERR_MODULE_NOT_FOUND` at runtime. (#66420) Thanks @obviyus.
|
||||
- Browser: keep loopback CDP readiness checks reachable under strict SSRF defaults so OpenClaw can reconnect to locally started managed Chrome. (#66354) Thanks @hxy91819.
|
||||
- Agents/context engine: compact engine-owned sessions from the first tool-loop delta and preserve ingest fallback when `afterTurn` is absent, so long-running tool loops can stay bounded without dropping engine state. (#63555) Thanks @Bikkies.
|
||||
- Discord/native commands: return the real status card for native `/status` interactions instead of falling through to the synthetic `✅ Done.` ack when the generic dispatcher produces no visible reply. (#54629) Thanks @tkozzer and @vincentkoc.
|
||||
|
||||
## 2026.4.14-beta.1
|
||||
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
7003e0d0ba1cddb7eb388204825ac892206209a4a9c795e76c4e34b5fc7b50f0 plugin-sdk-api-baseline.json
|
||||
14e39520459abc7db7993a700a4f07adfa0855d9233d123c4725477b91f1cb13 plugin-sdk-api-baseline.jsonl
|
||||
7b121e2b694f80433fa91ce9037527ca58be546a7f18798470a4ade66593e5e1 plugin-sdk-api-baseline.json
|
||||
7b802cc04f0eac0b498b50711e39a7afe93bbb6b682a2013d2c303583fb73f40 plugin-sdk-api-baseline.jsonl
|
||||
|
||||
@@ -19,6 +19,7 @@ const runtimeModuleMocks = vi.hoisted(() => ({
|
||||
matchPluginCommand: vi.fn(),
|
||||
executePluginCommand: vi.fn(),
|
||||
dispatchReplyWithDispatcher: vi.fn(),
|
||||
resolveDirectStatusReplyForSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/plugin-runtime", async () => {
|
||||
@@ -43,6 +44,11 @@ vi.mock("openclaw/plugin-sdk/reply-runtime", async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/command-status-runtime", () => ({
|
||||
resolveDirectStatusReplyForSession: (...args: unknown[]) =>
|
||||
runtimeModuleMocks.resolveDirectStatusReplyForSession(...args),
|
||||
}));
|
||||
|
||||
function createInteraction(params?: {
|
||||
channelType?: ChannelType;
|
||||
channelId?: string;
|
||||
@@ -306,35 +312,24 @@ function createDispatchSpy() {
|
||||
} as never);
|
||||
}
|
||||
|
||||
function expectBoundSessionDispatch(
|
||||
dispatchSpy: ReturnType<typeof createDispatchSpy>,
|
||||
expectedPattern: RegExp,
|
||||
) {
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as {
|
||||
ctx?: { SessionKey?: string; CommandTargetSessionKey?: string };
|
||||
};
|
||||
if (!dispatchCall.ctx?.SessionKey || !dispatchCall.ctx.CommandTargetSessionKey) {
|
||||
throw new Error("native command dispatch did not include bound session context");
|
||||
}
|
||||
expect(dispatchCall.ctx.SessionKey).toMatch(expectedPattern);
|
||||
expect(dispatchCall.ctx.CommandTargetSessionKey).toMatch(expectedPattern);
|
||||
}
|
||||
|
||||
async function expectBoundStatusCommandDispatch(params: {
|
||||
async function expectBoundStatusCommandDirectReply(params: {
|
||||
cfg: OpenClawConfig;
|
||||
interaction: MockCommandInteraction;
|
||||
expectedPattern: RegExp;
|
||||
}) {
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(null);
|
||||
const dispatchSpy = createDispatchSpy();
|
||||
const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher;
|
||||
const statusSpy = runtimeModuleMocks.resolveDirectStatusReplyForSession;
|
||||
const command = await createStatusCommand(params.cfg);
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(
|
||||
params.interaction as unknown,
|
||||
);
|
||||
|
||||
expectBoundSessionDispatch(dispatchSpy, params.expectedPattern);
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
expect(statusSpy).toHaveBeenCalledTimes(1);
|
||||
const statusCall = statusSpy.mock.calls[0]?.[0] as { sessionKey?: string };
|
||||
expect(statusCall.sessionKey).toMatch(params.expectedPattern);
|
||||
}
|
||||
|
||||
describe("Discord native plugin command dispatch", () => {
|
||||
@@ -366,6 +361,10 @@ describe("Discord native plugin command dispatch", () => {
|
||||
tool: 0,
|
||||
},
|
||||
} as never);
|
||||
runtimeModuleMocks.resolveDirectStatusReplyForSession.mockReset();
|
||||
runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({
|
||||
text: "status reply",
|
||||
});
|
||||
discordNativeCommandTesting.setMatchPluginCommand(
|
||||
runtimeModuleMocks.matchPluginCommand as typeof import("openclaw/plugin-sdk/plugin-runtime").matchPluginCommand,
|
||||
);
|
||||
@@ -632,7 +631,7 @@ describe("Discord native plugin command dispatch", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await expectBoundStatusCommandDispatch({
|
||||
await expectBoundStatusCommandDirectReply({
|
||||
cfg,
|
||||
interaction,
|
||||
expectedPattern: /^agent:codex:acp:binding:discord:default:/,
|
||||
@@ -683,7 +682,8 @@ describe("Discord native plugin command dispatch", () => {
|
||||
}),
|
||||
);
|
||||
runtimeModuleMocks.matchPluginCommand.mockReturnValue(null);
|
||||
const dispatchSpy = createDispatchSpy();
|
||||
const dispatchSpy = runtimeModuleMocks.dispatchReplyWithDispatcher;
|
||||
const statusSpy = runtimeModuleMocks.resolveDirectStatusReplyForSession;
|
||||
const command = await createStatusCommand(cfg);
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async () => ({
|
||||
route: {
|
||||
@@ -712,14 +712,10 @@ describe("Discord native plugin command dispatch", () => {
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
||||
const dispatchCall = dispatchSpy.mock.calls[0]?.[0] as {
|
||||
ctx?: { SessionKey?: string; CommandTargetSessionKey?: string };
|
||||
};
|
||||
expect(dispatchCall.ctx?.SessionKey).toBe("agent:qwen:discord:slash:owner");
|
||||
expect(dispatchCall.ctx?.CommandTargetSessionKey).toBe(
|
||||
"agent:qwen:discord:channel:1478836151241412759",
|
||||
);
|
||||
expect(dispatchSpy).not.toHaveBeenCalled();
|
||||
expect(statusSpy).toHaveBeenCalledTimes(1);
|
||||
const statusCall = statusSpy.mock.calls[0]?.[0] as { sessionKey?: string };
|
||||
expect(statusCall.sessionKey).toBe("agent:qwen:discord:channel:1478836151241412759");
|
||||
});
|
||||
|
||||
it("routes Discord DM native slash commands through configured ACP bindings", async () => {
|
||||
@@ -735,7 +731,7 @@ describe("Discord native plugin command dispatch", () => {
|
||||
}),
|
||||
);
|
||||
|
||||
await expectBoundStatusCommandDispatch({
|
||||
await expectBoundStatusCommandDirectReply({
|
||||
cfg,
|
||||
interaction,
|
||||
expectedPattern: /^agent:codex:acp:binding:discord:default:/,
|
||||
|
||||
@@ -0,0 +1,200 @@
|
||||
import { ChannelType } from "discord-api-types/v10";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createMockCommandInteraction,
|
||||
type MockCommandInteraction,
|
||||
} from "./native-command.test-helpers.js";
|
||||
import { createNoopThreadBindingManager } from "./thread-bindings.js";
|
||||
|
||||
const runtimeModuleMocks = vi.hoisted(() => ({
|
||||
dispatchReplyWithDispatcher: vi.fn(),
|
||||
resolveDirectStatusReplyForSession: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/reply-dispatch-runtime", async () => {
|
||||
const actual = await vi.importActual<typeof import("openclaw/plugin-sdk/reply-dispatch-runtime")>(
|
||||
"openclaw/plugin-sdk/reply-dispatch-runtime",
|
||||
);
|
||||
return {
|
||||
...actual,
|
||||
dispatchReplyWithDispatcher: (...args: unknown[]) =>
|
||||
runtimeModuleMocks.dispatchReplyWithDispatcher(...args),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/command-status-runtime", () => ({
|
||||
resolveDirectStatusReplyForSession: (...args: unknown[]) =>
|
||||
runtimeModuleMocks.resolveDirectStatusReplyForSession(...args),
|
||||
}));
|
||||
|
||||
let createDiscordNativeCommand: typeof import("./native-command.js").createDiscordNativeCommand;
|
||||
let discordNativeCommandTesting: typeof import("./native-command.js").__testing;
|
||||
|
||||
function createInteraction(params?: {
|
||||
channelType?: ChannelType;
|
||||
channelId?: string;
|
||||
threadParentId?: string | null;
|
||||
guildId?: string | null;
|
||||
guildName?: string;
|
||||
}): MockCommandInteraction {
|
||||
return createMockCommandInteraction({
|
||||
userId: "owner",
|
||||
username: "tester",
|
||||
globalName: "Tester",
|
||||
channelType: params?.channelType ?? ChannelType.DM,
|
||||
channelId: params?.channelId ?? "dm-1",
|
||||
threadParentId: params?.threadParentId,
|
||||
guildId: params?.guildId ?? null,
|
||||
guildName: params?.guildName,
|
||||
interactionId: "interaction-1",
|
||||
});
|
||||
}
|
||||
|
||||
function createConfig(params?: { requireMention?: boolean }): OpenClawConfig {
|
||||
return {
|
||||
commands: {
|
||||
useAccessGroups: false,
|
||||
},
|
||||
channels: {
|
||||
discord: {
|
||||
dm: { enabled: true, policy: "open" },
|
||||
guilds: {
|
||||
guild1: {
|
||||
requireMention: true,
|
||||
channels: {
|
||||
chan1: {
|
||||
allow: true,
|
||||
requireMention: params?.requireMention ?? true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
}
|
||||
|
||||
async function createStatusCommand(cfg: OpenClawConfig) {
|
||||
return createDiscordNativeCommand({
|
||||
command: {
|
||||
name: "status",
|
||||
description: "Status",
|
||||
acceptsArgs: false,
|
||||
},
|
||||
cfg,
|
||||
discordConfig: cfg.channels?.discord ?? {},
|
||||
accountId: "default",
|
||||
sessionPrefix: "discord:slash",
|
||||
ephemeralDefault: true,
|
||||
threadBindings: createNoopThreadBindingManager("default"),
|
||||
});
|
||||
}
|
||||
|
||||
function setDefaultRouteState() {
|
||||
discordNativeCommandTesting.setResolveDiscordNativeInteractionRouteState(async (params) => ({
|
||||
route: {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: params.accountId ?? "default",
|
||||
sessionKey: "agent:main:main",
|
||||
mainSessionKey: "agent:main:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "default",
|
||||
},
|
||||
effectiveRoute: {
|
||||
agentId: "main",
|
||||
channel: "discord",
|
||||
accountId: params.accountId ?? "default",
|
||||
sessionKey: "agent:main:main",
|
||||
mainSessionKey: "agent:main:main",
|
||||
lastRoutePolicy: "session",
|
||||
matchedBy: "default",
|
||||
},
|
||||
boundSessionKey: undefined,
|
||||
configuredRoute: null,
|
||||
configuredBinding: null,
|
||||
bindingReadiness: null,
|
||||
}));
|
||||
}
|
||||
|
||||
function firstStatusCall(): {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
channel: string;
|
||||
isGroup: boolean;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
} {
|
||||
const call = runtimeModuleMocks.resolveDirectStatusReplyForSession.mock.calls[0]?.[0];
|
||||
if (!call) {
|
||||
throw new Error("expected resolveDirectStatusReplyForSession to be called");
|
||||
}
|
||||
return call as {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
channel: string;
|
||||
isGroup: boolean;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
};
|
||||
}
|
||||
|
||||
describe("discord native /status", () => {
|
||||
beforeAll(async () => {
|
||||
({ createDiscordNativeCommand, __testing: discordNativeCommandTesting } =
|
||||
await import("./native-command.js"));
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
runtimeModuleMocks.dispatchReplyWithDispatcher.mockResolvedValue({
|
||||
counts: {
|
||||
final: 0,
|
||||
block: 0,
|
||||
tool: 0,
|
||||
},
|
||||
queuedFinal: false,
|
||||
} as never);
|
||||
runtimeModuleMocks.resolveDirectStatusReplyForSession.mockResolvedValue({
|
||||
text: "status reply",
|
||||
});
|
||||
discordNativeCommandTesting.setDispatchReplyWithDispatcher(
|
||||
runtimeModuleMocks.dispatchReplyWithDispatcher as typeof import("openclaw/plugin-sdk/reply-dispatch-runtime").dispatchReplyWithDispatcher,
|
||||
);
|
||||
setDefaultRouteState();
|
||||
});
|
||||
|
||||
it("returns a direct status reply without falling through the generic dispatcher", async () => {
|
||||
const cfg = createConfig();
|
||||
const command = await createStatusCommand(cfg);
|
||||
const interaction = createInteraction();
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
expect(runtimeModuleMocks.resolveDirectStatusReplyForSession).toHaveBeenCalledTimes(1);
|
||||
expect(runtimeModuleMocks.dispatchReplyWithDispatcher).not.toHaveBeenCalled();
|
||||
expect(interaction.followUp).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
content: "status reply",
|
||||
}),
|
||||
);
|
||||
expect(interaction.reply).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("passes through the effective guild activation when requireMention is disabled", async () => {
|
||||
const cfg = createConfig({ requireMention: false });
|
||||
const command = await createStatusCommand(cfg);
|
||||
const interaction = createInteraction({
|
||||
channelType: ChannelType.GuildText,
|
||||
channelId: "chan1",
|
||||
guildId: "guild1",
|
||||
guildName: "Guild One",
|
||||
});
|
||||
|
||||
await (command as { run: (interaction: unknown) => Promise<void> }).run(interaction as unknown);
|
||||
|
||||
const statusCall = firstStatusCall();
|
||||
expect(statusCall.channel).toBe("discord");
|
||||
expect(statusCall.isGroup).toBe(true);
|
||||
expect(statusCall.defaultGroupActivation()).toBe("always");
|
||||
});
|
||||
});
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
resolveCommandAuthorizedFromAuthorizers,
|
||||
resolveNativeCommandSessionTargets,
|
||||
} from "openclaw/plugin-sdk/command-auth-native";
|
||||
import { resolveDirectStatusReplyForSession } from "openclaw/plugin-sdk/command-status-runtime";
|
||||
import type { OpenClawConfig, loadConfig } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { buildPairingReply } from "openclaw/plugin-sdk/conversation-runtime";
|
||||
import { isDangerousNameMatchingEnabled } from "openclaw/plugin-sdk/dangerous-name-runtime";
|
||||
@@ -755,6 +756,7 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
threadBindings,
|
||||
suppressReplies,
|
||||
} = params;
|
||||
const commandName = command.nativeName ?? command.key;
|
||||
const respond = async (content: string, options?: { ephemeral?: boolean }) => {
|
||||
const payload = {
|
||||
content,
|
||||
@@ -869,15 +871,10 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
conversationId: rawChannelId || "unknown",
|
||||
parentConversationId: threadParentId,
|
||||
threadBinding: isThreadChannel ? threadBindings.getByThreadId(rawChannelId) : undefined,
|
||||
enforceConfiguredBindingReadiness: !shouldBypassConfiguredAcpEnsure(
|
||||
command.nativeName ?? command.key,
|
||||
),
|
||||
enforceConfiguredBindingReadiness: !shouldBypassConfiguredAcpEnsure(commandName),
|
||||
}));
|
||||
const canBypassConfiguredAcpGuildGuards = async () => {
|
||||
if (
|
||||
!interaction.guild ||
|
||||
!shouldBypassConfiguredAcpGuildGuards(command.nativeName ?? command.key)
|
||||
) {
|
||||
if (!interaction.guild || !shouldBypassConfiguredAcpGuildGuards(commandName)) {
|
||||
return false;
|
||||
}
|
||||
const routeState = await getNativeRouteState();
|
||||
@@ -1131,6 +1128,36 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
targetSessionKey: effectiveRoute.sessionKey,
|
||||
boundSessionKey,
|
||||
});
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId);
|
||||
if (!suppressReplies && commandName === "status") {
|
||||
const statusReply = await resolveDirectStatusReplyForSession({
|
||||
cfg,
|
||||
sessionKey: commandTargetSessionKey?.trim() || sessionKey,
|
||||
channel: "discord",
|
||||
senderId: sender.id,
|
||||
senderIsOwner: ownerOk,
|
||||
isAuthorizedSender: commandAuthorized,
|
||||
isGroup: isGuild || isGroupDm,
|
||||
defaultGroupActivation: () =>
|
||||
!isGuild ? "always" : channelConfig?.requireMention === false ? "always" : "mention",
|
||||
});
|
||||
if (statusReply && hasRenderableReplyPayload(statusReply)) {
|
||||
await deliverDiscordInteractionReply({
|
||||
interaction,
|
||||
payload: statusReply,
|
||||
mediaLocalRoots,
|
||||
textLimit: resolveTextChunkLimit(cfg, "discord", accountId, {
|
||||
fallbackLimit: 2000,
|
||||
}),
|
||||
maxLinesPerMessage: resolveDiscordMaxLinesPerMessage({ cfg, discordConfig, accountId }),
|
||||
preferFollowUp,
|
||||
chunkMode: resolveChunkMode(cfg, "discord", accountId),
|
||||
});
|
||||
return;
|
||||
}
|
||||
await respond("Status unavailable.");
|
||||
return;
|
||||
}
|
||||
const ctxPayload = buildDiscordNativeCommandContext({
|
||||
prompt,
|
||||
commandArgs: commandArgs ?? {},
|
||||
@@ -1164,7 +1191,6 @@ async function dispatchDiscordCommandInteraction(params: {
|
||||
channel: "discord",
|
||||
accountId: effectiveRoute.accountId,
|
||||
});
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(cfg, effectiveRoute.agentId);
|
||||
const blockStreamingEnabled = resolveChannelStreamingBlockEnabled(discordConfig);
|
||||
|
||||
let didReply = false;
|
||||
|
||||
@@ -482,6 +482,10 @@
|
||||
"types": "./dist/plugin-sdk/command-status.d.ts",
|
||||
"default": "./dist/plugin-sdk/command-status.js"
|
||||
},
|
||||
"./plugin-sdk/command-status-runtime": {
|
||||
"types": "./dist/plugin-sdk/command-status-runtime.d.ts",
|
||||
"default": "./dist/plugin-sdk/command-status-runtime.js"
|
||||
},
|
||||
"./plugin-sdk/command-detection": {
|
||||
"types": "./dist/plugin-sdk/command-detection.d.ts",
|
||||
"default": "./dist/plugin-sdk/command-detection.js"
|
||||
|
||||
@@ -65,6 +65,9 @@ export const pluginSdkDocMetadata = {
|
||||
"command-status": {
|
||||
category: "channel",
|
||||
},
|
||||
"command-status-runtime": {
|
||||
category: "runtime",
|
||||
},
|
||||
"secret-input": {
|
||||
category: "channel",
|
||||
},
|
||||
|
||||
@@ -108,6 +108,7 @@
|
||||
"command-auth",
|
||||
"command-auth-native",
|
||||
"command-status",
|
||||
"command-status-runtime",
|
||||
"command-detection",
|
||||
"command-surface",
|
||||
"collection-runtime",
|
||||
|
||||
13
src/plugin-sdk/command-status-runtime.ts
Normal file
13
src/plugin-sdk/command-status-runtime.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { createLazyRuntimeMethodBinder, createLazyRuntimeModule } from "../shared/lazy-runtime.js";
|
||||
|
||||
type CommandStatusRuntime = typeof import("./command-status.runtime.js");
|
||||
|
||||
const loadCommandStatusRuntime = createLazyRuntimeModule(
|
||||
() => import("./command-status.runtime.js"),
|
||||
);
|
||||
const bindCommandStatusRuntime = createLazyRuntimeMethodBinder(loadCommandStatusRuntime);
|
||||
|
||||
export type { ResolveDirectStatusReplyForSessionParams } from "./command-status.runtime.js";
|
||||
|
||||
export const resolveDirectStatusReplyForSession: CommandStatusRuntime["resolveDirectStatusReplyForSession"] =
|
||||
bindCommandStatusRuntime((runtime) => runtime.resolveDirectStatusReplyForSession);
|
||||
125
src/plugin-sdk/command-status.runtime.ts
Normal file
125
src/plugin-sdk/command-status.runtime.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { listAgentEntries, resolveSessionAgentId } from "../agents/agent-scope.js";
|
||||
import { resolveDefaultModelForAgent } from "../agents/model-selection.js";
|
||||
import { buildStatusReply } from "../auto-reply/reply/commands-status.js";
|
||||
import type { CommandContext } from "../auto-reply/reply/commands-types.js";
|
||||
import { resolveDefaultModel } from "../auto-reply/reply/directive-handling.defaults.js";
|
||||
import { resolveCurrentDirectiveLevels } from "../auto-reply/reply/directive-handling.levels.js";
|
||||
import { createModelSelectionState } from "../auto-reply/reply/model-selection.js";
|
||||
import type { ReplyPayload } from "../auto-reply/types.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { loadSessionEntry } from "../gateway/session-utils.js";
|
||||
|
||||
export type ResolveDirectStatusReplyForSessionParams = {
|
||||
cfg: OpenClawConfig;
|
||||
sessionKey: string;
|
||||
channel: string;
|
||||
senderId?: string;
|
||||
senderIsOwner: boolean;
|
||||
isAuthorizedSender: boolean;
|
||||
isGroup: boolean;
|
||||
defaultGroupActivation: () => "always" | "mention";
|
||||
};
|
||||
|
||||
export async function resolveDirectStatusReplyForSession(
|
||||
params: ResolveDirectStatusReplyForSessionParams,
|
||||
): Promise<ReplyPayload | undefined> {
|
||||
const requestedSessionKey = params.sessionKey.trim();
|
||||
if (!requestedSessionKey) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const statusLoaded = loadSessionEntry(requestedSessionKey);
|
||||
const statusCfg = statusLoaded.cfg ?? params.cfg;
|
||||
const statusSessionKey = statusLoaded.canonicalKey;
|
||||
const statusEntry = statusLoaded.entry;
|
||||
const statusAgentId = resolveSessionAgentId({
|
||||
sessionKey: statusSessionKey,
|
||||
config: statusCfg,
|
||||
});
|
||||
const agentCfg = statusCfg.agents?.defaults;
|
||||
const agentEntry = listAgentEntries(statusCfg).find(
|
||||
(entry) => entry.id?.trim().toLowerCase() === statusAgentId,
|
||||
);
|
||||
const statusModel = resolveDefaultModelForAgent({
|
||||
cfg: statusCfg,
|
||||
agentId: statusAgentId,
|
||||
});
|
||||
const { defaultProvider, defaultModel } = resolveDefaultModel({
|
||||
cfg: statusCfg,
|
||||
agentId: statusAgentId,
|
||||
});
|
||||
const selectedProvider =
|
||||
statusEntry?.providerOverride?.trim() ||
|
||||
statusEntry?.modelProvider?.trim() ||
|
||||
statusModel.provider;
|
||||
const selectedModel =
|
||||
statusEntry?.modelOverride?.trim() || statusEntry?.model?.trim() || statusModel.model;
|
||||
const modelState = await createModelSelectionState({
|
||||
cfg: statusCfg,
|
||||
agentId: statusAgentId,
|
||||
agentCfg,
|
||||
sessionEntry: statusEntry,
|
||||
sessionStore: statusLoaded.store,
|
||||
sessionKey: statusSessionKey,
|
||||
parentSessionKey: statusEntry?.parentSessionKey,
|
||||
storePath: statusLoaded.storePath,
|
||||
defaultProvider,
|
||||
defaultModel,
|
||||
provider: selectedProvider,
|
||||
model: selectedModel,
|
||||
hasModelDirective: false,
|
||||
});
|
||||
const {
|
||||
currentThinkLevel,
|
||||
currentFastMode,
|
||||
currentVerboseLevel,
|
||||
currentReasoningLevel,
|
||||
currentElevatedLevel,
|
||||
} = await resolveCurrentDirectiveLevels({
|
||||
sessionEntry: statusEntry,
|
||||
agentEntry,
|
||||
agentCfg,
|
||||
resolveDefaultThinkingLevel: () => modelState.resolveDefaultThinkingLevel(),
|
||||
});
|
||||
let resolvedReasoningLevel = currentReasoningLevel;
|
||||
const hasAgentReasoningDefault =
|
||||
agentEntry?.reasoningDefault !== undefined && agentEntry.reasoningDefault !== null;
|
||||
const reasoningExplicitlySet =
|
||||
(statusEntry?.reasoningLevel !== undefined && statusEntry.reasoningLevel !== null) ||
|
||||
hasAgentReasoningDefault;
|
||||
if (!reasoningExplicitlySet && resolvedReasoningLevel === "off" && currentThinkLevel === "off") {
|
||||
resolvedReasoningLevel = await modelState.resolveDefaultReasoningLevel();
|
||||
}
|
||||
|
||||
const command: CommandContext = {
|
||||
surface: params.channel,
|
||||
channel: params.channel,
|
||||
ownerList: [],
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
isAuthorizedSender: params.isAuthorizedSender,
|
||||
senderId: params.senderId,
|
||||
rawBodyNormalized: "/status",
|
||||
commandBodyNormalized: "/status",
|
||||
};
|
||||
|
||||
return await buildStatusReply({
|
||||
cfg: statusCfg,
|
||||
command,
|
||||
sessionEntry: statusEntry,
|
||||
sessionKey: statusSessionKey,
|
||||
parentSessionKey: statusEntry?.parentSessionKey,
|
||||
sessionScope: statusCfg.session?.scope,
|
||||
storePath: statusLoaded.storePath,
|
||||
provider: selectedProvider,
|
||||
model: selectedModel,
|
||||
contextTokens: statusEntry?.contextTokens ?? 0,
|
||||
resolvedThinkLevel: currentThinkLevel,
|
||||
resolvedFastMode: currentFastMode,
|
||||
resolvedVerboseLevel: currentVerboseLevel ?? "off",
|
||||
resolvedReasoningLevel,
|
||||
resolvedElevatedLevel: currentElevatedLevel,
|
||||
resolveDefaultThinkingLevel: () => modelState.resolveDefaultThinkingLevel(),
|
||||
isGroup: params.isGroup,
|
||||
defaultGroupActivation: params.defaultGroupActivation,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user