mirror of https://github.com/openclaw/openclaw.git
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:
parent
3025760867
commit
5369ea53be
|
|
@ -0,0 +1 @@
|
|||
export { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
||||
|
|
@ -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 { resolveCommandAuthorization } from "../command-auth.js";
|
||||
import { normalizeCommandBody } from "../commands-registry.js";
|
||||
import type { MsgContext } from "../templating.js";
|
||||
import type { CommandContext } from "./commands-types.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: {
|
||||
ctx: MsgContext;
|
||||
cfg: OpenClawConfig;
|
||||
|
|
@ -53,9 +24,9 @@ export function buildCommandContext(params: {
|
|||
const channel = (ctx.Provider ?? surface).trim().toLowerCase();
|
||||
const abortKey = sessionKey ?? (auth.from || undefined) ?? (auth.to || undefined);
|
||||
const rawBodyNormalized = triggerBodyNormalized;
|
||||
const commandBodyNormalized = normalizeCommandBodyLite(
|
||||
const commandBodyNormalized = normalizeCommandBody(
|
||||
isGroup ? stripMentions(rawBodyNormalized, ctx, cfg, agentId) : rawBodyNormalized,
|
||||
ctx.BotUsername,
|
||||
{ botUsername: ctx.BotUsername },
|
||||
);
|
||||
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -6,44 +6,26 @@ import { getGlobalHookRunner } from "../../plugins/hook-runner-global.js";
|
|||
import { isAcpSessionKey, resolveAgentIdFromSessionKey } from "../../routing/session-key.js";
|
||||
import { resolveSendPolicy } from "../../sessions/send-policy.js";
|
||||
import { shouldHandleTextCommands } from "../commands-registry.js";
|
||||
import { handleAcpCommand } from "./commands-acp.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 {
|
||||
CommandHandler,
|
||||
CommandHandlerResult,
|
||||
HandleCommandsParams,
|
||||
} 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;
|
||||
|
||||
|
|
@ -82,6 +64,7 @@ export async function emitResetCommandHooks(params: {
|
|||
const to = params.ctx.OriginatingTo || params.command.from || params.command.to;
|
||||
|
||||
if (channel && to) {
|
||||
const { routeReply } = await loadRouteReplyRuntime();
|
||||
const hookReply = { text: hookEvent.messages.join("\n\n") };
|
||||
await routeReply({
|
||||
payload: hookReply,
|
||||
|
|
@ -174,37 +157,7 @@ function resolveSessionEntryForHookSessionKey(
|
|||
|
||||
export async function handleCommands(params: HandleCommandsParams): Promise<CommandHandlerResult> {
|
||||
if (HANDLERS === null) {
|
||||
HANDLERS = [
|
||||
// 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,
|
||||
];
|
||||
HANDLERS = (await loadCommandHandlersRuntime()).loadCommandHandlers();
|
||||
}
|
||||
const resetMatch = params.command.commandBodyNormalized.match(/^\/(new|reset)(?:\s|$)/);
|
||||
const resetRequested = Boolean(resetMatch);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
@ -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", () => ({
|
||||
isRoutableChannel: (channel: string | undefined) =>
|
||||
Boolean(
|
||||
|
|
@ -122,7 +140,7 @@ vi.mock("./route-reply.js", () => ({
|
|||
routeReply: mocks.routeReply,
|
||||
}));
|
||||
|
||||
vi.mock("./abort.js", () => ({
|
||||
vi.mock("./abort.runtime.js", () => ({
|
||||
tryFastAbortFromMessage: mocks.tryFastAbortFromMessage,
|
||||
formatAbortReplyText: (stoppedSubagents?: number) => {
|
||||
if (typeof stoppedSubagents !== "number" || stoppedSubagents <= 0) {
|
||||
|
|
@ -138,15 +156,21 @@ vi.mock("../../logging/diagnostic.js", () => ({
|
|||
logMessageProcessed: diagnosticMocks.logMessageProcessed,
|
||||
logSessionStateChange: diagnosticMocks.logSessionStateChange,
|
||||
}));
|
||||
vi.mock("../../config/sessions.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../config/sessions.js")>();
|
||||
vi.mock("../../config/sessions/store.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../../config/sessions/store.js")>();
|
||||
return {
|
||||
...actual,
|
||||
loadSessionStore: sessionStoreMocks.loadSessionStore,
|
||||
resolveStorePath: sessionStoreMocks.resolveStorePath,
|
||||
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", () => ({
|
||||
getGlobalHookRunner: () => hookMocks.runner,
|
||||
|
|
@ -192,6 +216,13 @@ vi.mock("../../tts/tts.js", () => ({
|
|||
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
|
||||
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 emptyConfig = {} as OpenClawConfig;
|
||||
|
|
|
|||
|
|
@ -6,13 +6,10 @@ import {
|
|||
} from "../../bindings/records.js";
|
||||
import { shouldSuppressLocalExecApprovalPrompt } from "../../channels/plugins/exec-approval-local.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import {
|
||||
loadSessionStore,
|
||||
parseSessionThreadInfo,
|
||||
resolveSessionStoreEntry,
|
||||
resolveStorePath,
|
||||
type SessionEntry,
|
||||
} from "../../config/sessions.js";
|
||||
import { parseSessionThreadInfo } from "../../config/sessions/delivery-info.js";
|
||||
import { resolveStorePath } from "../../config/sessions/paths.js";
|
||||
import { loadSessionStore, resolveSessionStoreEntry } from "../../config/sessions/store.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
||||
import { createInternalHookEvent, triggerInternalHook } from "../../hooks/internal-hooks.js";
|
||||
|
|
@ -41,19 +38,47 @@ import {
|
|||
} from "../../plugins/conversation-binding.js";
|
||||
import { getGlobalHookRunner, getGlobalPluginRegistry } from "../../plugins/hook-runner-global.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 { getReplyFromConfig } from "../reply.js";
|
||||
import type { FinalizedMsgContext } from "../templating.js";
|
||||
import type { GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { formatAbortReplyText, tryFastAbortFromMessage } from "./abort.js";
|
||||
import { shouldBypassAcpDispatchForCommand, tryDispatchAcpReply } from "./dispatch-acp.js";
|
||||
import type { BlockReplyContext, GetReplyOptions, ReplyPayload } from "../types.js";
|
||||
import { shouldSkipDuplicateInbound } from "./inbound-dedupe.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";
|
||||
|
||||
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_HEADER_RE = /^\[Audio\b/i;
|
||||
const normalizeMediaType = (value: string): string => value.split(";")[0]?.trim().toLowerCase();
|
||||
|
|
@ -126,7 +151,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
cfg: OpenClawConfig;
|
||||
dispatcher: ReplyDispatcher;
|
||||
replyOptions?: Omit<GetReplyOptions, "onToolResult" | "onBlockReply">;
|
||||
replyResolver?: typeof getReplyFromConfig;
|
||||
replyResolver?: typeof import("./get-reply-from-config.runtime.js").getReplyFromConfig;
|
||||
}): Promise<DispatchFromConfigResult> {
|
||||
const { ctx, cfg, dispatcher } = params;
|
||||
const diagnosticsEnabled = isDiagnosticsEnabled(cfg);
|
||||
|
|
@ -230,9 +255,10 @@ export async function dispatchReplyFromConfig(params: {
|
|||
currentSurface === INTERNAL_MESSAGE_CHANNEL &&
|
||||
(surfaceChannel === INTERNAL_MESSAGE_CHANNEL || !surfaceChannel) &&
|
||||
ctx.ExplicitDeliverRoute !== true;
|
||||
const routeReplyRuntime = await loadRouteReplyRuntime();
|
||||
const shouldRouteToOriginating = Boolean(
|
||||
!isInternalWebchatTurn &&
|
||||
isRoutableChannel(originatingChannel) &&
|
||||
routeReplyRuntime.isRoutableChannel(originatingChannel) &&
|
||||
originatingTo &&
|
||||
originatingChannel !== currentSurface,
|
||||
);
|
||||
|
|
@ -259,7 +285,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
if (abortSignal?.aborted) {
|
||||
return;
|
||||
}
|
||||
const result = await routeReply({
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
payload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
|
|
@ -282,7 +308,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
mode: "additive" | "terminal",
|
||||
): Promise<boolean> => {
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
const result = await routeReply({
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
payload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
|
|
@ -418,15 +444,16 @@ export async function dispatchReplyFromConfig(params: {
|
|||
markProcessing();
|
||||
|
||||
try {
|
||||
const fastAbort = await tryFastAbortFromMessage({ ctx, cfg });
|
||||
const abortRuntime = await loadAbortRuntime();
|
||||
const fastAbort = await abortRuntime.tryFastAbortFromMessage({ ctx, cfg });
|
||||
if (fastAbort.handled) {
|
||||
const payload = {
|
||||
text: formatAbortReplyText(fastAbort.stoppedSubagents),
|
||||
text: abortRuntime.formatAbortReplyText(fastAbort.stoppedSubagents),
|
||||
} satisfies ReplyPayload;
|
||||
let queuedFinal = false;
|
||||
let routedFinalCount = 0;
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
const result = await routeReply({
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
payload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
|
|
@ -456,7 +483,8 @@ export async function dispatchReplyFromConfig(params: {
|
|||
return { queuedFinal, counts };
|
||||
}
|
||||
|
||||
const bypassAcpForCommand = shouldBypassAcpDispatchForCommand(ctx, cfg);
|
||||
const dispatchAcpRuntime = await loadDispatchAcpRuntime();
|
||||
const bypassAcpForCommand = dispatchAcpRuntime.shouldBypassAcpDispatchForCommand(ctx, cfg);
|
||||
|
||||
const sendPolicy = resolveSendPolicy({
|
||||
cfg,
|
||||
|
|
@ -481,7 +509,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
}
|
||||
|
||||
const shouldSendToolSummaries = ctx.ChatType !== "group" && ctx.CommandSource !== "native";
|
||||
const acpDispatch = await tryDispatchAcpReply({
|
||||
const acpDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
|
||||
ctx,
|
||||
cfg,
|
||||
dispatcher,
|
||||
|
|
@ -508,6 +536,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
// TTS audio separately from the accumulated block content.
|
||||
let accumulatedBlockText = "";
|
||||
let blockCount = 0;
|
||||
const { maybeApplyTtsToPayload } = await loadTtsRuntime();
|
||||
|
||||
const resolveToolDeliveryPayload = (payload: ReplyPayload): ReplyPayload | null => {
|
||||
if (
|
||||
|
|
@ -547,7 +576,9 @@ export async function dispatchReplyFromConfig(params: {
|
|||
systemEvent: shouldRouteToOriginating,
|
||||
});
|
||||
|
||||
const replyResult = await (params.replyResolver ?? getReplyFromConfig)(
|
||||
const replyResolver =
|
||||
params.replyResolver ?? (await loadGetReplyFromConfigRuntime()).getReplyFromConfig;
|
||||
const replyResult = await replyResolver(
|
||||
ctx,
|
||||
{
|
||||
...params.replyOptions,
|
||||
|
|
@ -575,12 +606,12 @@ export async function dispatchReplyFromConfig(params: {
|
|||
};
|
||||
return run();
|
||||
},
|
||||
onBlockReply: (payload: ReplyPayload, context) => {
|
||||
onBlockReply: (payload: ReplyPayload, context?: BlockReplyContext) => {
|
||||
const run = async () => {
|
||||
// Suppress reasoning payloads — channels using this generic dispatch
|
||||
// path (WhatsApp, web, etc.) do not have a dedicated reasoning lane.
|
||||
// Telegram has its own dispatch path that handles reasoning splitting.
|
||||
if (shouldSuppressReasoningPayload(payload)) {
|
||||
if (payload.isReasoning === true) {
|
||||
return;
|
||||
}
|
||||
// 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.
|
||||
// Route that tail through ACP now (same turn) instead of embedded dispatch.
|
||||
ctx.AcpDispatchTailAfterReset = false;
|
||||
const acpTailDispatch = await tryDispatchAcpReply({
|
||||
const acpTailDispatch = await dispatchAcpRuntime.tryDispatchAcpReply({
|
||||
ctx,
|
||||
cfg,
|
||||
dispatcher,
|
||||
|
|
@ -647,7 +678,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
for (const reply of replies) {
|
||||
// Suppress reasoning payloads from channel delivery — channels using this
|
||||
// generic dispatch path do not have a dedicated reasoning lane.
|
||||
if (shouldSuppressReasoningPayload(reply)) {
|
||||
if (reply.isReasoning === true) {
|
||||
continue;
|
||||
}
|
||||
const ttsReply = await maybeApplyTtsToPayload({
|
||||
|
|
@ -660,7 +691,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
});
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
// Route final reply to originating channel.
|
||||
const result = await routeReply({
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
payload: ttsReply,
|
||||
channel: originatingChannel,
|
||||
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).
|
||||
// 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.
|
||||
|
|
@ -712,7 +743,7 @@ export async function dispatchReplyFromConfig(params: {
|
|||
audioAsVoice: ttsSyntheticReply.audioAsVoice,
|
||||
};
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
const result = await routeReply({
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
payload: ttsOnlyPayload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
export { getReplyFromConfig } from "../reply.js";
|
||||
|
|
@ -1 +1 @@
|
|||
export { routeReply } from "./route-reply.js";
|
||||
export { isRoutableChannel, routeReply } from "./route-reply.js";
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
export { maybeApplyTtsToPayload } from "./tts.js";
|
||||
Loading…
Reference in New Issue