mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-30 19:20:22 +00:00
perf(inbound): trim dispatch and command startup imports (#52374)
* perf(inbound): trim dispatch and command startup imports * fix(reply): restore command alias canonicalization * style(reply): format command context * fix(reply): restore runtime shim exports * test(reply): mock ACP route seam * fix(reply): repair dispatch type seams
This commit is contained in:
1
src/auto-reply/reply/abort.runtime.ts
Normal file
1
src/auto-reply/reply/abort.runtime.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
||||||
29
src/auto-reply/reply/commands-context.test.ts
Normal file
29
src/auto-reply/reply/commands-context.test.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { buildCommandContext } from "./commands-context.js";
|
||||||
|
import { buildTestCtx } from "./test-ctx.js";
|
||||||
|
|
||||||
|
describe("buildCommandContext", () => {
|
||||||
|
it("canonicalizes registered aliases like /id to their primary command", () => {
|
||||||
|
const ctx = buildTestCtx({
|
||||||
|
Provider: "discord",
|
||||||
|
Surface: "discord",
|
||||||
|
From: "user",
|
||||||
|
To: "bot",
|
||||||
|
Body: "/id",
|
||||||
|
RawBody: "/id",
|
||||||
|
CommandBody: "/id",
|
||||||
|
BodyForCommands: "/id",
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = buildCommandContext({
|
||||||
|
ctx,
|
||||||
|
cfg: {} as OpenClawConfig,
|
||||||
|
isGroup: false,
|
||||||
|
triggerBodyNormalized: "/id",
|
||||||
|
commandAuthorized: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.commandBodyNormalized).toBe("/whoami");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,39 +1,10 @@
|
|||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import { resolveCommandAuthorization } from "../command-auth.js";
|
import { resolveCommandAuthorization } from "../command-auth.js";
|
||||||
|
import { normalizeCommandBody } from "../commands-registry.js";
|
||||||
import type { MsgContext } from "../templating.js";
|
import type { MsgContext } from "../templating.js";
|
||||||
import type { CommandContext } from "./commands-types.js";
|
import type { CommandContext } from "./commands-types.js";
|
||||||
import { stripMentions } from "./mentions.js";
|
import { stripMentions } from "./mentions.js";
|
||||||
|
|
||||||
function normalizeCommandBodyLite(raw: string, botUsername?: string): string {
|
|
||||||
const trimmed = raw.trim();
|
|
||||||
if (!trimmed.startsWith("/")) {
|
|
||||||
return trimmed;
|
|
||||||
}
|
|
||||||
|
|
||||||
const newline = trimmed.indexOf("\n");
|
|
||||||
const singleLine = newline === -1 ? trimmed : trimmed.slice(0, newline).trim();
|
|
||||||
const colonMatch = singleLine.match(/^\/([^\s:]+)\s*:(.*)$/);
|
|
||||||
const normalized = colonMatch
|
|
||||||
? (() => {
|
|
||||||
const [, command, rest] = colonMatch;
|
|
||||||
const normalizedRest = rest.trimStart();
|
|
||||||
return normalizedRest ? `/${command} ${normalizedRest}` : `/${command}`;
|
|
||||||
})()
|
|
||||||
: singleLine;
|
|
||||||
|
|
||||||
const normalizedBotUsername = botUsername?.trim().toLowerCase();
|
|
||||||
const mentionMatch = normalizedBotUsername
|
|
||||||
? normalized.match(/^\/([^\s@]+)@([^\s]+)(.*)$/)
|
|
||||||
: null;
|
|
||||||
const mentionNormalized =
|
|
||||||
mentionMatch && mentionMatch[2].toLowerCase() === normalizedBotUsername
|
|
||||||
? `/${mentionMatch[1]}${mentionMatch[3] ?? ""}`
|
|
||||||
: normalized;
|
|
||||||
return mentionNormalized.replace(/^\/([^\s]+)(.*)$/, (_, command: string, rest: string) => {
|
|
||||||
return `/${command.toLowerCase()}${rest ?? ""}`;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function buildCommandContext(params: {
|
export function buildCommandContext(params: {
|
||||||
ctx: MsgContext;
|
ctx: MsgContext;
|
||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
@@ -53,9 +24,9 @@ export function buildCommandContext(params: {
|
|||||||
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
|
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
|
||||||
const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||||
const rawBodyNormalized = triggerBodyNormalized;
|
const rawBodyNormalized = triggerBodyNormalized;
|
||||||
const commandBodyNormalized = normalizeCommandBodyLite(
|
const commandBodyNormalized = normalizeCommandBody(
|
||||||
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized,
|
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized,
|
||||||
ctx.BotUsername,
|
{ botUsername: ctx.BotUsername },
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,44 +6,26 @@ import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
|||||||
import { isAcpSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
import { isAcpSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||||
import { shouldHandleTextCommands } from "../commands-registry.js";
|
import { shouldHandleTextCommands } from "../commands-registry.js";
|
||||||
import { handleAcpCommand } from "./commands-acp.js";
|
|
||||||
import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
|
import { resolveBoundAcpThreadSessionKey } from "./commands-acp/targets.js";
|
||||||
import { handleAllowlistCommand } from "./commands-allowlist.js";
|
|
||||||
import { handleApproveCommand } from "./commands-approve.js";
|
|
||||||
import { handleBashCommand } from "./commands-bash.js";
|
|
||||||
import { handleBtwCommand } from "./commands-btw.js";
|
|
||||||
import { handleCompactCommand } from "./commands-compact.js";
|
|
||||||
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
|
|
||||||
import {
|
|
||||||
handleCommandsListCommand,
|
|
||||||
handleContextCommand,
|
|
||||||
handleExportSessionCommand,
|
|
||||||
handleHelpCommand,
|
|
||||||
handleStatusCommand,
|
|
||||||
handleWhoamiCommand,
|
|
||||||
} from "./commands-info.js";
|
|
||||||
import { handleMcpCommand } from "./commands-mcp.js";
|
|
||||||
import { handleModelsCommand } from "./commands-models.js";
|
|
||||||
import { handlePluginCommand } from "./commands-plugin.js";
|
|
||||||
import { handlePluginsCommand } from "./commands-plugins.js";
|
|
||||||
import {
|
|
||||||
handleAbortTrigger,
|
|
||||||
handleActivationCommand,
|
|
||||||
handleFastCommand,
|
|
||||||
handleRestartCommand,
|
|
||||||
handleSessionCommand,
|
|
||||||
handleSendPolicyCommand,
|
|
||||||
handleStopCommand,
|
|
||||||
handleUsageCommand,
|
|
||||||
} from "./commands-session.js";
|
|
||||||
import { handleSubagentsCommand } from "./commands-subagents.js";
|
|
||||||
import { handleTtsCommands } from "./commands-tts.js";
|
|
||||||
import type {
|
import type {
|
||||||
CommandHandler,
|
CommandHandler,
|
||||||
CommandHandlerResult,
|
CommandHandlerResult,
|
||||||
HandleCommandsParams,
|
HandleCommandsParams,
|
||||||
} from "./commands-types.js";
|
} from "./commands-types.js";
|
||||||
import { routeReply } from "./route-reply.js";
|
|
||||||
|
let routeReplyRuntimePromise: Promise<typeof import("./route-reply.runtime.js")> | null = null;
|
||||||
|
let commandHandlersRuntimePromise: Promise<typeof import("./commands-handlers.runtime.js")> | null =
|
||||||
|
null;
|
||||||
|
|
||||||
|
function loadRouteReplyRuntime() {
|
||||||
|
routeReplyRuntimePromise ??= import("./route-reply.runtime.js");
|
||||||
|
return routeReplyRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadCommandHandlersRuntime() {
|
||||||
|
commandHandlersRuntimePromise ??= import("./commands-handlers.runtime.js");
|
||||||
|
return commandHandlersRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
let HANDLERS: CommandHandler[] | null = null;
|
let HANDLERS: CommandHandler[] | null = null;
|
||||||
|
|
||||||
@@ -82,6 +64,7 @@ export async function emitResetCommandHooks(params: {
|
|||||||
const to = params.ctx.OriginatingTo || params.command.from || params.command.to;
|
const to = params.ctx.OriginatingTo || params.command.from || params.command.to;
|
||||||
|
|
||||||
if (channel && to) {
|
if (channel && to) {
|
||||||
|
const { routeReply } = await loadRouteReplyRuntime();
|
||||||
const hookReply = { text: hookEvent.messages.join("\n\n") };
|
const hookReply = { text: hookEvent.messages.join("\n\n") };
|
||||||
await routeReply({
|
await routeReply({
|
||||||
payload: hookReply,
|
payload: hookReply,
|
||||||
@@ -174,37 +157,7 @@ function resolveSessionEntryForHookSessionKey(
|
|||||||
|
|
||||||
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
|
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
|
||||||
if (HANDLERS === null) {
|
if (HANDLERS === null) {
|
||||||
HANDLERS = [
|
HANDLERS = (await loadCommandHandlersRuntime()).loadCommandHandlers();
|
||||||
// Plugin commands are processed first, before built-in commands
|
|
||||||
handlePluginCommand,
|
|
||||||
handleBtwCommand,
|
|
||||||
handleBashCommand,
|
|
||||||
handleActivationCommand,
|
|
||||||
handleSendPolicyCommand,
|
|
||||||
handleFastCommand,
|
|
||||||
handleUsageCommand,
|
|
||||||
handleSessionCommand,
|
|
||||||
handleRestartCommand,
|
|
||||||
handleTtsCommands,
|
|
||||||
handleHelpCommand,
|
|
||||||
handleCommandsListCommand,
|
|
||||||
handleStatusCommand,
|
|
||||||
handleAllowlistCommand,
|
|
||||||
handleApproveCommand,
|
|
||||||
handleContextCommand,
|
|
||||||
handleExportSessionCommand,
|
|
||||||
handleWhoamiCommand,
|
|
||||||
handleSubagentsCommand,
|
|
||||||
handleAcpCommand,
|
|
||||||
handleMcpCommand,
|
|
||||||
handlePluginsCommand,
|
|
||||||
handleConfigCommand,
|
|
||||||
handleDebugCommand,
|
|
||||||
handleModelsCommand,
|
|
||||||
handleStopCommand,
|
|
||||||
handleCompactCommand,
|
|
||||||
handleAbortTrigger,
|
|
||||||
];
|
|
||||||
}
|
}
|
||||||
const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/);
|
const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/);
|
||||||
const resetRequested = Boolean(resetMatch);
|
const resetRequested = Boolean(resetMatch);
|
||||||
|
|||||||
65
src/auto-reply/reply/commands-handlers.runtime.ts
Normal file
65
src/auto-reply/reply/commands-handlers.runtime.ts
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { handleAcpCommand } from "./commands-acp.js";
|
||||||
|
import { handleAllowlistCommand } from "./commands-allowlist.js";
|
||||||
|
import { handleApproveCommand } from "./commands-approve.js";
|
||||||
|
import { handleBashCommand } from "./commands-bash.js";
|
||||||
|
import { handleBtwCommand } from "./commands-btw.js";
|
||||||
|
import { handleCompactCommand } from "./commands-compact.js";
|
||||||
|
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
|
||||||
|
import {
|
||||||
|
handleCommandsListCommand,
|
||||||
|
handleContextCommand,
|
||||||
|
handleExportSessionCommand,
|
||||||
|
handleHelpCommand,
|
||||||
|
handleStatusCommand,
|
||||||
|
handleWhoamiCommand,
|
||||||
|
} from "./commands-info.js";
|
||||||
|
import { handleMcpCommand } from "./commands-mcp.js";
|
||||||
|
import { handleModelsCommand } from "./commands-models.js";
|
||||||
|
import { handlePluginCommand } from "./commands-plugin.js";
|
||||||
|
import { handlePluginsCommand } from "./commands-plugins.js";
|
||||||
|
import {
|
||||||
|
handleAbortTrigger,
|
||||||
|
handleActivationCommand,
|
||||||
|
handleFastCommand,
|
||||||
|
handleRestartCommand,
|
||||||
|
handleSendPolicyCommand,
|
||||||
|
handleSessionCommand,
|
||||||
|
handleStopCommand,
|
||||||
|
handleUsageCommand,
|
||||||
|
} from "./commands-session.js";
|
||||||
|
import { handleSubagentsCommand } from "./commands-subagents.js";
|
||||||
|
import { handleTtsCommands } from "./commands-tts.js";
|
||||||
|
import type { CommandHandler } from "./commands-types.js";
|
||||||
|
|
||||||
|
export function loadCommandHandlers(): CommandHandler[] {
|
||||||
|
return [
|
||||||
|
handlePluginCommand,
|
||||||
|
handleBtwCommand,
|
||||||
|
handleBashCommand,
|
||||||
|
handleActivationCommand,
|
||||||
|
handleSendPolicyCommand,
|
||||||
|
handleFastCommand,
|
||||||
|
handleUsageCommand,
|
||||||
|
handleSessionCommand,
|
||||||
|
handleRestartCommand,
|
||||||
|
handleTtsCommands,
|
||||||
|
handleHelpCommand,
|
||||||
|
handleCommandsListCommand,
|
||||||
|
handleStatusCommand,
|
||||||
|
handleAllowlistCommand,
|
||||||
|
handleApproveCommand,
|
||||||
|
handleContextCommand,
|
||||||
|
handleExportSessionCommand,
|
||||||
|
handleWhoamiCommand,
|
||||||
|
handleSubagentsCommand,
|
||||||
|
handleAcpCommand,
|
||||||
|
handleMcpCommand,
|
||||||
|
handlePluginsCommand,
|
||||||
|
handleConfigCommand,
|
||||||
|
handleDebugCommand,
|
||||||
|
handleModelsCommand,
|
||||||
|
handleStopCommand,
|
||||||
|
handleCompactCommand,
|
||||||
|
handleAbortTrigger,
|
||||||
|
];
|
||||||
|
}
|
||||||
1
src/auto-reply/reply/dispatch-acp.runtime.ts
Normal file
1
src/auto-reply/reply/dispatch-acp.runtime.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { shouldBypassAcpDispatchForCommand, tryDispatchAcpReply } from "./dispatch-acp.js";
|
||||||
@@ -104,6 +104,24 @@ const ttsMocks = vi.hoisted(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
vi.mock("./route-reply.runtime.js", () => ({
|
||||||
|
isRoutableChannel: (channel: string | undefined) =>
|
||||||
|
Boolean(
|
||||||
|
channel &&
|
||||||
|
[
|
||||||
|
"telegram",
|
||||||
|
"slack",
|
||||||
|
"discord",
|
||||||
|
"signal",
|
||||||
|
"imessage",
|
||||||
|
"whatsapp",
|
||||||
|
"feishu",
|
||||||
|
"mattermost",
|
||||||
|
].includes(channel),
|
||||||
|
),
|
||||||
|
routeReply: mocks.routeReply,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("./route-reply.js", () => ({
|
vi.mock("./route-reply.js", () => ({
|
||||||
isRoutableChannel: (channel: string | undefined) =>
|
isRoutableChannel: (channel: string | undefined) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
@@ -122,7 +140,7 @@ vi.mock("./route-reply.js", () => ({
|
|||||||
routeReply: mocks.routeReply,
|
routeReply: mocks.routeReply,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./abort.js", () => ({
|
vi.mock("./abort.runtime.js", () => ({
|
||||||
tryFastAbortFromMessage: mocks.tryFastAbortFromMessage,
|
tryFastAbortFromMessage: mocks.tryFastAbortFromMessage,
|
||||||
formatAbortReplyText: (stoppedSubagents?: number) => {
|
formatAbortReplyText: (stoppedSubagents?: number) => {
|
||||||
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
||||||
@@ -138,15 +156,21 @@ vi.mock("../../logging/diagnostic.js", () => ({
|
|||||||
logMessageProcessed: diagnosticMocks.logMessageProcessed,
|
logMessageProcessed: diagnosticMocks.logMessageProcessed,
|
||||||
logSessionStateChange: diagnosticMocks.logSessionStateChange,
|
logSessionStateChange: diagnosticMocks.logSessionStateChange,
|
||||||
}));
|
}));
|
||||||
vi.mock("../../config/sessions.js", async (importOriginal) => {
|
vi.mock("../../config/sessions/store.js", async (importOriginal) => {
|
||||||
const actual = await importOriginal<typeof import("../../config/sessions.js")>();
|
const actual = await importOriginal<typeof import("../../config/sessions/store.js")>();
|
||||||
return {
|
return {
|
||||||
...actual,
|
...actual,
|
||||||
loadSessionStore: sessionStoreMocks.loadSessionStore,
|
loadSessionStore: sessionStoreMocks.loadSessionStore,
|
||||||
resolveStorePath: sessionStoreMocks.resolveStorePath,
|
|
||||||
resolveSessionStoreEntry: sessionStoreMocks.resolveSessionStoreEntry,
|
resolveSessionStoreEntry: sessionStoreMocks.resolveSessionStoreEntry,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
vi.mock("../../config/sessions/paths.js", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("../../config/sessions/paths.js")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
resolveStorePath: sessionStoreMocks.resolveStorePath,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
vi.mock("../../plugins/hook-runner-global.js", () => ({
|
vi.mock("../../plugins/hook-runner-global.js", () => ({
|
||||||
getGlobalHookRunner: () => hookMocks.runner,
|
getGlobalHookRunner: () => hookMocks.runner,
|
||||||
@@ -192,6 +216,13 @@ vi.mock("../../tts/tts.js", () => ({
|
|||||||
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
|
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
|
||||||
resolveTtsConfig: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg),
|
resolveTtsConfig: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg),
|
||||||
}));
|
}));
|
||||||
|
vi.mock("../../tts/tts.runtime.js", () => ({
|
||||||
|
maybeApplyTtsToPayload: (params: unknown) => ttsMocks.maybeApplyTtsToPayload(params),
|
||||||
|
}));
|
||||||
|
vi.mock("../../tts/tts-config.js", () => ({
|
||||||
|
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
|
||||||
|
resolveConfiguredTtsMode: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg).mode,
|
||||||
|
}));
|
||||||
|
|
||||||
const noAbortResult = { handled: false, aborted: false } as const;
|
const noAbortResult = { handled: false, aborted: false } as const;
|
||||||
const emptyConfig = {} as OpenClawConfig;
|
const emptyConfig = {} as OpenClawConfig;
|
||||||
|
|||||||
@@ -6,13 +6,10 @@ import {
|
|||||||
} from "../../bindings/records.js";
|
} from "../../bindings/records.js";
|
||||||
import { shouldSuppressLocalExecApprovalPrompt } from "../../channels/plugins/exec-approval-local.js";
|
import { shouldSuppressLocalExecApprovalPrompt } from "../../channels/plugins/exec-approval-local.js";
|
||||||
import type { OpenClawConfig } from "../../config/config.js";
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
import {
|
import { parseSessionThreadInfo } from "../../config/sessions/delivery-info.js";
|
||||||
loadSessionStore,
|
import { resolveStorePath } from "../../config/sessions/paths.js";
|
||||||
parseSessionThreadInfo,
|
import { loadSessionStore, resolveSessionStoreEntry } from "../../config/sessions/store.js";
|
||||||
resolveSessionStoreEntry,
|
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||||
resolveStorePath,
|
|
||||||
type SessionEntry,
|
|
||||||
} from "../../config/sessions.js";
|
|
||||||
import { logVerbose } from "../../globals.js";
|
import { logVerbose } from "../../globals.js";
|
||||||
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
||||||
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||||
@@ -41,19 +38,47 @@ import {
|
|||||||
} from "../../plugins/conversation-binding.js";
|
} from "../../plugins/conversation-binding.js";
|
||||||
import { getGlobalHookRunner, getGlobalPluginRegistry } from "../../plugins/hook-runner-global.js";
|
import { getGlobalHookRunner, getGlobalPluginRegistry } from "../../plugins/hook-runner-global.js";
|
||||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||||
import { maybeApplyTtsToPayload, normalizeTtsAutoMode, resolveTtsConfig } from "../../tts/tts.js";
|
import { normalizeTtsAutoMode, resolveConfiguredTtsMode } from "../../tts/tts-config.js";
|
||||||
import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js";
|
import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||||
import { getReplyFromConfig } from "../reply.js";
|
|
||||||
import type { FinalizedMsgContext } from "../templating.js";
|
import type { FinalizedMsgContext } from "../templating.js";
|
||||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
import type { BlockReplyContext, GetReplyOptions, ReplyPayload } from "../types.js";
|
||||||
import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
|
||||||
import { shouldBypassAcpDispatchForCommand, tryDispatchAcpReply } from "./dispatch-acp.js";
|
|
||||||
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.js";
|
||||||
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
import type { ReplyDispatcher, ReplyDispatchKind } from "./reply-dispatcher.js";
|
||||||
import { shouldSuppressReasoningPayload } from "./reply-payloads.js";
|
|
||||||
import { isRoutableChannel, routeReply } from "./route-reply.js";
|
|
||||||
import { resolveRunTypingPolicy } from "./typing-policy.js";
|
import { resolveRunTypingPolicy } from "./typing-policy.js";
|
||||||
|
|
||||||
|
let routeReplyRuntimePromise: Promise<typeof import("./route-reply.runtime.js")> | null = null;
|
||||||
|
let getReplyFromConfigRuntimePromise: Promise<
|
||||||
|
typeof import("./get-reply-from-config.runtime.js")
|
||||||
|
> | null = null;
|
||||||
|
let abortRuntimePromise: Promise<typeof import("./abort.runtime.js")> | null = null;
|
||||||
|
let dispatchAcpRuntimePromise: Promise<typeof import("./dispatch-acp.runtime.js")> | null = null;
|
||||||
|
let ttsRuntimePromise: Promise<typeof import("../../tts/tts.runtime.js")> | null = null;
|
||||||
|
|
||||||
|
function loadRouteReplyRuntime() {
|
||||||
|
routeReplyRuntimePromise ??= import("./route-reply.runtime.js");
|
||||||
|
return routeReplyRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadGetReplyFromConfigRuntime() {
|
||||||
|
getReplyFromConfigRuntimePromise ??= import("./get-reply-from-config.runtime.js");
|
||||||
|
return getReplyFromConfigRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadAbortRuntime() {
|
||||||
|
abortRuntimePromise ??= import("./abort.runtime.js");
|
||||||
|
return abortRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadDispatchAcpRuntime() {
|
||||||
|
dispatchAcpRuntimePromise ??= import("./dispatch-acp.runtime.js");
|
||||||
|
return dispatchAcpRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadTtsRuntime() {
|
||||||
|
ttsRuntimePromise ??= import("../../tts/tts.runtime.js");
|
||||||
|
return ttsRuntimePromise;
|
||||||
|
}
|
||||||
|
|
||||||
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
|
const AUDIO_PLACEHOLDER_RE = /^<media:audio>(\s*\([^)]*\))?$/i;
|
||||||
const AUDIO_HEADER_RE = /^\[Audio\b/i;
|
const AUDIO_HEADER_RE = /^\[Audio\b/i;
|
||||||
const normalizeMediaType = (value: string): string => value.split(";")[0]?.trim().toLowerCase();
|
const normalizeMediaType = (value: string): string => value.split(";")[0]?.trim().toLowerCase();
|
||||||
@@ -126,7 +151,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
cfg: OpenClawConfig;
|
cfg: OpenClawConfig;
|
||||||
dispatcher: ReplyDispatcher;
|
dispatcher: ReplyDispatcher;
|
||||||
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
||||||
replyResolver?: typeof getReplyFromConfig;
|
replyResolver?: typeof import("./get-reply-from-config.runtime.js").getReplyFromConfig;
|
||||||
}): Promise<DispatchFromConfigResult> {
|
}): Promise<DispatchFromConfigResult> {
|
||||||
const { ctx, cfg, dispatcher } = params;
|
const { ctx, cfg, dispatcher } = params;
|
||||||
const diagnosticsEnabled = isDiagnosticsEnabled(cfg);
|
const diagnosticsEnabled = isDiagnosticsEnabled(cfg);
|
||||||
@@ -230,9 +255,10 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
currentSurface === INTERNAL_MESSAGE_CHANNEL &&
|
currentSurface === INTERNAL_MESSAGE_CHANNEL &&
|
||||||
(surfaceChannel === INTERNAL_MESSAGE_CHANNEL || !surfaceChannel) &&
|
(surfaceChannel === INTERNAL_MESSAGE_CHANNEL || !surfaceChannel) &&
|
||||||
ctx.ExplicitDeliverRoute !== true;
|
ctx.ExplicitDeliverRoute !== true;
|
||||||
|
const routeReplyRuntime = await loadRouteReplyRuntime();
|
||||||
const shouldRouteToOriginating = Boolean(
|
const shouldRouteToOriginating = Boolean(
|
||||||
!isInternalWebchatTurn &&
|
!isInternalWebchatTurn &&
|
||||||
isRoutableChannel(originatingChannel) &&
|
routeReplyRuntime.isRoutableChannel(originatingChannel) &&
|
||||||
originatingTo &&
|
originatingTo &&
|
||||||
originatingChannel !== currentSurface,
|
originatingChannel !== currentSurface,
|
||||||
);
|
);
|
||||||
@@ -259,7 +285,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
if (abortSignal?.aborted) {
|
if (abortSignal?.aborted) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const result = await routeReply({
|
const result = await routeReplyRuntime.routeReply({
|
||||||
payload,
|
payload,
|
||||||
channel: originatingChannel,
|
channel: originatingChannel,
|
||||||
to: originatingTo,
|
to: originatingTo,
|
||||||
@@ -282,7 +308,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
mode: "additive" | "terminal",
|
mode: "additive" | "terminal",
|
||||||
): Promise<boolean> => {
|
): Promise<boolean> => {
|
||||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||||
const result = await routeReply({
|
const result = await routeReplyRuntime.routeReply({
|
||||||
payload,
|
payload,
|
||||||
channel: originatingChannel,
|
channel: originatingChannel,
|
||||||
to: originatingTo,
|
to: originatingTo,
|
||||||
@@ -418,15 +444,16 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
markProcessing();
|
markProcessing();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fastAbort = await tryFastAbortFromMessage({ ctx, cfg });
|
const abortRuntime = await loadAbortRuntime();
|
||||||
|
const fastAbort = await abortRuntime.tryFastAbortFromMessage({ ctx, cfg });
|
||||||
if (fastAbort.handled) {
|
if (fastAbort.handled) {
|
||||||
const payload = {
|
const payload = {
|
||||||
text: formatAbortReplyText(fastAbort.stoppedSubagents),
|
text: abortRuntime.formatAbortReplyText(fastAbort.stoppedSubagents),
|
||||||
} satisfies ReplyPayload;
|
} satisfies ReplyPayload;
|
||||||
let queuedFinal = false;
|
let queuedFinal = false;
|
||||||
let routedFinalCount = 0;
|
let routedFinalCount = 0;
|
||||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||||
const result = await routeReply({
|
const result = await routeReplyRuntime.routeReply({
|
||||||
payload,
|
payload,
|
||||||
channel: originatingChannel,
|
channel: originatingChannel,
|
||||||
to: originatingTo,
|
to: originatingTo,
|
||||||
@@ -456,7 +483,8 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
return { queuedFinal, counts };
|
return { queuedFinal, counts };
|
||||||
}
|
}
|
||||||
|
|
||||||
const bypassAcpForCommand = shouldBypassAcpDispatchForCommand(ctx, cfg);
|
const dispatchAcpRuntime = await loadDispatchAcpRuntime();
|
||||||
|
const bypassAcpForCommand = dispatchAcpRuntime.shouldBypassAcpDispatchForCommand(ctx, cfg);
|
||||||
|
|
||||||
const sendPolicy = resolveSendPolicy({
|
const sendPolicy = resolveSendPolicy({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -481,7 +509,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native";
|
const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native";
|
||||||
const acpDispatch = await tryDispatchAcpReply({
|
const acpDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
|
||||||
ctx,
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
@@ -508,6 +536,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
// TTS audio separately from the accumulated block content.
|
// TTS audio separately from the accumulated block content.
|
||||||
let accumulatedBlockText = "";
|
let accumulatedBlockText = "";
|
||||||
let blockCount = 0;
|
let blockCount = 0;
|
||||||
|
const { maybeApplyTtsToPayload } = await loadTtsRuntime();
|
||||||
|
|
||||||
const resolveToolDeliveryPayload = (payload: ReplyPayload): ReplyPayload | null => {
|
const resolveToolDeliveryPayload = (payload: ReplyPayload): ReplyPayload | null => {
|
||||||
if (
|
if (
|
||||||
@@ -547,7 +576,9 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
systemEvent: shouldRouteToOriginating,
|
systemEvent: shouldRouteToOriginating,
|
||||||
});
|
});
|
||||||
|
|
||||||
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
|
const replyResolver =
|
||||||
|
params.replyResolver ?? (await loadGetReplyFromConfigRuntime()).getReplyFromConfig;
|
||||||
|
const replyResult = await replyResolver(
|
||||||
ctx,
|
ctx,
|
||||||
{
|
{
|
||||||
...params.replyOptions,
|
...params.replyOptions,
|
||||||
@@ -575,12 +606,12 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
};
|
};
|
||||||
return run();
|
return run();
|
||||||
},
|
},
|
||||||
onBlockReply: (payload: ReplyPayload, context) => {
|
onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => {
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
// Suppress reasoning payloads — channels using this generic dispatch
|
// Suppress reasoning payloads — channels using this generic dispatch
|
||||||
// path (WhatsApp, web, etc.) do not have a dedicated reasoning lane.
|
// path (WhatsApp, web, etc.) do not have a dedicated reasoning lane.
|
||||||
// Telegram has its own dispatch path that handles reasoning splitting.
|
// Telegram has its own dispatch path that handles reasoning splitting.
|
||||||
if (shouldSuppressReasoningPayload(payload)) {
|
if (payload.isReasoning === true) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// Accumulate block text for TTS generation after streaming.
|
// Accumulate block text for TTS generation after streaming.
|
||||||
@@ -617,7 +648,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
// Command handling prepared a trailing prompt after ACP in-place reset.
|
// Command handling prepared a trailing prompt after ACP in-place reset.
|
||||||
// Route that tail through ACP now (same turn) instead of embedded dispatch.
|
// Route that tail through ACP now (same turn) instead of embedded dispatch.
|
||||||
ctx.AcpDispatchTailAfterReset = false;
|
ctx.AcpDispatchTailAfterReset = false;
|
||||||
const acpTailDispatch = await tryDispatchAcpReply({
|
const acpTailDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
|
||||||
ctx,
|
ctx,
|
||||||
cfg,
|
cfg,
|
||||||
dispatcher,
|
dispatcher,
|
||||||
@@ -647,7 +678,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
for (const reply of replies) {
|
for (const reply of replies) {
|
||||||
// Suppress reasoning payloads from channel delivery — channels using this
|
// Suppress reasoning payloads from channel delivery — channels using this
|
||||||
// generic dispatch path do not have a dedicated reasoning lane.
|
// generic dispatch path do not have a dedicated reasoning lane.
|
||||||
if (shouldSuppressReasoningPayload(reply)) {
|
if (reply.isReasoning === true) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const ttsReply = await maybeApplyTtsToPayload({
|
const ttsReply = await maybeApplyTtsToPayload({
|
||||||
@@ -660,7 +691,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
});
|
});
|
||||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||||
// Route final reply to originating channel.
|
// Route final reply to originating channel.
|
||||||
const result = await routeReply({
|
const result = await routeReplyRuntime.routeReply({
|
||||||
payload: ttsReply,
|
payload: ttsReply,
|
||||||
channel: originatingChannel,
|
channel: originatingChannel,
|
||||||
to: originatingTo,
|
to: originatingTo,
|
||||||
@@ -685,7 +716,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ttsMode = resolveTtsConfig(cfg).mode ?? "final";
|
const ttsMode = resolveConfiguredTtsMode(cfg);
|
||||||
// Generate TTS-only reply after block streaming completes (when there's no final reply).
|
// Generate TTS-only reply after block streaming completes (when there's no final reply).
|
||||||
// This handles the case where block streaming succeeds and drops final payloads,
|
// This handles the case where block streaming succeeds and drops final payloads,
|
||||||
// but we still want TTS audio to be generated from the accumulated block content.
|
// but we still want TTS audio to be generated from the accumulated block content.
|
||||||
@@ -712,7 +743,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||||||
audioAsVoice: ttsSyntheticReply.audioAsVoice,
|
audioAsVoice: ttsSyntheticReply.audioAsVoice,
|
||||||
};
|
};
|
||||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||||
const result = await routeReply({
|
const result = await routeReplyRuntime.routeReply({
|
||||||
payload: ttsOnlyPayload,
|
payload: ttsOnlyPayload,
|
||||||
channel: originatingChannel,
|
channel: originatingChannel,
|
||||||
to: originatingTo,
|
to: originatingTo,
|
||||||
|
|||||||
1
src/auto-reply/reply/get-reply-from-config.runtime.ts
Normal file
1
src/auto-reply/reply/get-reply-from-config.runtime.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { getReplyFromConfig } from "../reply.js";
|
||||||
@@ -1 +1 @@
|
|||||||
export { routeReply } from "./route-reply.js";
|
export { isRoutableChannel, routeReply } from "./route-reply.js";
|
||||||
|
|||||||
19
src/tts/tts-config.ts
Normal file
19
src/tts/tts-config.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
|
import type { TtsAutoMode, TtsMode } from "../config/types.tts.js";
|
||||||
|
|
||||||
|
const TTS_AUTO_MODES = new Set<TtsAutoMode>(["off", "always", "inbound", "tagged"]);
|
||||||
|
|
||||||
|
export function normalizeTtsAutoMode(value: unknown): TtsAutoMode | undefined {
|
||||||
|
if (typeof value !== "string") {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const normalized = value.trim().toLowerCase();
|
||||||
|
if (TTS_AUTO_MODES.has(normalized as TtsAutoMode)) {
|
||||||
|
return normalized as TtsAutoMode;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveConfiguredTtsMode(cfg: OpenClawConfig): TtsMode {
|
||||||
|
return cfg.messages?.tts?.mode ?? "final";
|
||||||
|
}
|
||||||
1
src/tts/tts.runtime.ts
Normal file
1
src/tts/tts.runtime.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export { maybeApplyTtsToPayload } from "./tts.js";
|
||||||
Reference in New Issue
Block a user