mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-12 17:51:22 +00:00
perf: avoid heavy reply runtime imports
This commit is contained in:
@@ -65,10 +65,11 @@ function normalizeEmoji(raw: string) {
|
||||
}
|
||||
|
||||
async function getClient(opts: SlackActionClientOpts = {}, mode: "read" | "write" = "read") {
|
||||
if (opts.client) {
|
||||
return opts.client;
|
||||
}
|
||||
const token = resolveToken(opts.token, opts.accountId);
|
||||
return (
|
||||
opts.client ?? (mode === "write" ? createSlackWriteClient(token) : createSlackWebClient(token))
|
||||
);
|
||||
return mode === "write" ? createSlackWriteClient(token) : createSlackWebClient(token);
|
||||
}
|
||||
|
||||
async function resolveBotUserId(client: WebClient) {
|
||||
|
||||
@@ -244,6 +244,7 @@ vi.mock("./monitor/conversation.runtime.js", async () => {
|
||||
...actual,
|
||||
readChannelAllowFromStore: (...args: unknown[]) =>
|
||||
slackTestState.readAllowFromStoreMock(...args),
|
||||
recordInboundSession: vi.fn().mockResolvedValue(undefined),
|
||||
upsertChannelPairingRequest: (...args: unknown[]) =>
|
||||
slackTestState.upsertPairingRequestMock(...args),
|
||||
};
|
||||
|
||||
@@ -22,6 +22,10 @@ import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/text-runti
|
||||
import { reactSlackMessage, removeSlackReaction } from "../../actions.js";
|
||||
import { createSlackDraftStream } from "../../draft-stream.js";
|
||||
import { normalizeSlackOutboundText } from "../../format.js";
|
||||
import {
|
||||
compileSlackInteractiveReplies,
|
||||
isSlackInteractiveRepliesEnabled,
|
||||
} from "../../interactive-replies.js";
|
||||
import { SLACK_TEXT_LIMIT } from "../../limits.js";
|
||||
import { recordSlackThreadParticipation } from "../../sent-thread-cache.js";
|
||||
import {
|
||||
@@ -306,6 +310,10 @@ export async function dispatchPreparedSlackMessage(prepared: PreparedSlackMessag
|
||||
agentId: route.agentId,
|
||||
channel: "slack",
|
||||
accountId: route.accountId,
|
||||
transformReplyPayload: (payload) =>
|
||||
isSlackInteractiveRepliesEnabled({ cfg, accountId: route.accountId })
|
||||
? compileSlackInteractiveReplies(payload)
|
||||
: payload,
|
||||
typing: {
|
||||
start: async () => {
|
||||
didSetStatus = true;
|
||||
|
||||
@@ -175,6 +175,8 @@ vi.mock("../../logging/diagnostic.js", () => ({
|
||||
vi.mock("../../config/sessions/thread-info.js", () => ({
|
||||
parseSessionThreadInfo: (sessionKey: string | undefined) =>
|
||||
threadInfoMocks.parseSessionThreadInfo(sessionKey),
|
||||
parseSessionThreadInfoFast: (sessionKey: string | undefined) =>
|
||||
threadInfoMocks.parseSessionThreadInfo(sessionKey),
|
||||
}));
|
||||
vi.mock("./dispatch-from-config.runtime.js", () => ({
|
||||
createInternalHookEvent: internalHookMocks.createInternalHookEvent,
|
||||
@@ -279,6 +281,7 @@ vi.mock("./dispatch-acp-session.runtime.js", () => ({
|
||||
vi.mock("../../tts/tts-config.js", () => ({
|
||||
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
|
||||
resolveConfiguredTtsMode: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg).mode,
|
||||
shouldAttemptTtsPayload: () => true,
|
||||
}));
|
||||
|
||||
export const noAbortResult = { handled: false, aborted: false } as const;
|
||||
|
||||
@@ -230,6 +230,8 @@ vi.mock("../../logging/diagnostic.js", () => ({
|
||||
vi.mock("../../config/sessions/thread-info.js", () => ({
|
||||
parseSessionThreadInfo: (sessionKey: string | undefined) =>
|
||||
threadInfoMocks.parseSessionThreadInfo(sessionKey),
|
||||
parseSessionThreadInfoFast: (sessionKey: string | undefined) =>
|
||||
threadInfoMocks.parseSessionThreadInfo(sessionKey),
|
||||
}));
|
||||
vi.mock("./dispatch-from-config.runtime.js", () => ({
|
||||
createInternalHookEvent: internalHookMocks.createInternalHookEvent,
|
||||
@@ -342,6 +344,7 @@ vi.mock("./dispatch-acp-session.runtime.js", () => ({
|
||||
vi.mock("../../tts/tts-config.js", () => ({
|
||||
normalizeTtsAutoMode: (value: unknown) => ttsMocks.normalizeTtsAutoMode(value),
|
||||
resolveConfiguredTtsMode: (cfg: OpenClawConfig) => ttsMocks.resolveTtsConfig(cfg).mode,
|
||||
shouldAttemptTtsPayload: () => true,
|
||||
}));
|
||||
|
||||
const noAbortResult = { handled: false, aborted: false } as const;
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
} from "../../bindings/records.js";
|
||||
import { shouldSuppressLocalExecApprovalPrompt } from "../../channels/plugins/exec-approval-local.js";
|
||||
import type { OpenClawConfig } from "../../config/config.js";
|
||||
import { parseSessionThreadInfo } from "../../config/sessions/thread-info.js";
|
||||
import { parseSessionThreadInfoFast } from "../../config/sessions/thread-info.js";
|
||||
import type { SessionEntry } from "../../config/sessions/types.js";
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import { fireAndForgetHook } from "../../hooks/fire-and-forget.js";
|
||||
@@ -42,8 +42,12 @@ import {
|
||||
normalizeOptionalLowercaseString,
|
||||
normalizeOptionalString,
|
||||
} from "../../shared/string-coerce.js";
|
||||
import { normalizeTtsAutoMode, resolveConfiguredTtsMode } from "../../tts/tts-config.js";
|
||||
import { normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import {
|
||||
normalizeTtsAutoMode,
|
||||
resolveConfiguredTtsMode,
|
||||
shouldAttemptTtsPayload,
|
||||
} from "../../tts/tts-config.js";
|
||||
import { INTERNAL_MESSAGE_CHANNEL, normalizeMessageChannel } from "../../utils/message-channel.js";
|
||||
import type { FinalizedMsgContext } from "../templating.js";
|
||||
import { normalizeVerboseLevel } from "../thinking.js";
|
||||
import {
|
||||
@@ -94,6 +98,9 @@ function loadTtsRuntime() {
|
||||
async function maybeApplyTtsToReplyPayload(
|
||||
params: Parameters<Awaited<ReturnType<typeof loadTtsRuntime>>["maybeApplyTtsToPayload"]>[0],
|
||||
) {
|
||||
if (!shouldAttemptTtsPayload({ cfg: params.cfg, ttsAuto: params.ttsAuto })) {
|
||||
return params.payload;
|
||||
}
|
||||
const { maybeApplyTtsToPayload } = await loadTtsRuntime();
|
||||
return maybeApplyTtsToPayload(params);
|
||||
}
|
||||
@@ -283,7 +290,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
// folded back into lastThreadId/deliveryContext during store normalisation and resurrect a
|
||||
// stale route after thread delivery was intentionally cleared.
|
||||
const routeThreadId =
|
||||
ctx.MessageThreadId ?? parseSessionThreadInfo(acpDispatchSessionKey).threadId;
|
||||
ctx.MessageThreadId ?? parseSessionThreadInfoFast(acpDispatchSessionKey).threadId;
|
||||
const inboundAudio = isInboundAudioContext(ctx);
|
||||
const sessionTtsAuto = normalizeTtsAutoMode(sessionStoreEntry.entry?.ttsAuto);
|
||||
const hookRunner = getGlobalHookRunner();
|
||||
@@ -310,7 +317,22 @@ export async function dispatchReplyFromConfig(params: {
|
||||
//
|
||||
// Debug: `pnpm test src/auto-reply/reply/dispatch-from-config.test.ts`
|
||||
const suppressAcpChildUserDelivery = isParentOwnedBackgroundAcpSession(sessionStoreEntry.entry);
|
||||
const routeReplyRuntime = await loadRouteReplyRuntime();
|
||||
const normalizedOriginatingChannel = normalizeMessageChannel(ctx.OriginatingChannel);
|
||||
const normalizedProviderChannel = normalizeMessageChannel(ctx.Provider);
|
||||
const normalizedSurfaceChannel = normalizeMessageChannel(ctx.Surface);
|
||||
const normalizedCurrentSurface = normalizedProviderChannel ?? normalizedSurfaceChannel;
|
||||
const isInternalWebchatTurn =
|
||||
normalizedCurrentSurface === INTERNAL_MESSAGE_CHANNEL &&
|
||||
(normalizedSurfaceChannel === INTERNAL_MESSAGE_CHANNEL || !normalizedSurfaceChannel) &&
|
||||
ctx.ExplicitDeliverRoute !== true;
|
||||
const hasRouteReplyCandidate = Boolean(
|
||||
!suppressAcpChildUserDelivery &&
|
||||
!isInternalWebchatTurn &&
|
||||
normalizedOriginatingChannel &&
|
||||
ctx.OriginatingTo &&
|
||||
normalizedOriginatingChannel !== normalizedCurrentSurface,
|
||||
);
|
||||
const routeReplyRuntime = hasRouteReplyCandidate ? await loadRouteReplyRuntime() : undefined;
|
||||
const { originatingChannel, currentSurface, shouldRouteToOriginating, shouldSuppressTyping } =
|
||||
resolveReplyRoutingDecision({
|
||||
provider: ctx.Provider,
|
||||
@@ -319,7 +341,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
originatingChannel: ctx.OriginatingChannel,
|
||||
originatingTo: ctx.OriginatingTo,
|
||||
suppressDirectUserDelivery: suppressAcpChildUserDelivery,
|
||||
isRoutableChannel: routeReplyRuntime.isRoutableChannel,
|
||||
isRoutableChannel: routeReplyRuntime?.isRoutableChannel ?? (() => false),
|
||||
});
|
||||
const originatingTo = ctx.OriginatingTo;
|
||||
const ttsChannel = shouldRouteToOriginating ? originatingChannel : currentSurface;
|
||||
@@ -343,7 +365,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
if (abortSignal?.aborted) {
|
||||
return;
|
||||
}
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
const result = await routeReplyRuntime!.routeReply({
|
||||
payload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
@@ -370,7 +392,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
mode: "additive" | "terminal",
|
||||
): Promise<boolean> => {
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
const result = await routeReplyRuntime!.routeReply({
|
||||
payload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
@@ -1025,7 +1047,7 @@ export async function dispatchReplyFromConfig(params: {
|
||||
audioAsVoice: ttsSyntheticReply.audioAsVoice,
|
||||
};
|
||||
if (shouldRouteToOriginating && originatingChannel && originatingTo) {
|
||||
const result = await routeReplyRuntime.routeReply({
|
||||
const result = await routeReplyRuntime!.routeReply({
|
||||
payload: ttsOnlyPayload,
|
||||
channel: originatingChannel,
|
||||
to: originatingTo,
|
||||
|
||||
@@ -78,4 +78,16 @@ describe("createChannelReplyPipeline", () => {
|
||||
expect(onReplyStart).toHaveBeenCalledTimes(1);
|
||||
expect(onIdle).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("uses an explicit reply transform without resolving the channel plugin", () => {
|
||||
const transformReplyPayload = vi.fn((payload) => payload);
|
||||
const pipeline = createChannelReplyPipeline({
|
||||
cfg: {},
|
||||
agentId: "main",
|
||||
channel: "slack",
|
||||
transformReplyPayload,
|
||||
});
|
||||
|
||||
expect(pipeline.transformReplyPayload).toBe(transformReplyPayload);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -29,19 +29,26 @@ export function createChannelReplyPipeline(params: {
|
||||
accountId?: string;
|
||||
typing?: CreateTypingCallbacksParams;
|
||||
typingCallbacks?: TypingCallbacks;
|
||||
transformReplyPayload?: (payload: ReplyPayload) => ReplyPayload | null;
|
||||
}): ChannelReplyPipeline {
|
||||
const channelId = params.channel
|
||||
? (normalizeChannelId(params.channel) ?? params.channel)
|
||||
: undefined;
|
||||
const plugin = channelId ? getChannelPlugin(channelId) : undefined;
|
||||
const transformReplyPayload = plugin?.messaging?.transformReplyPayload
|
||||
? (payload: ReplyPayload) =>
|
||||
plugin.messaging?.transformReplyPayload?.({
|
||||
payload,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
}) ?? payload
|
||||
: undefined;
|
||||
const plugin = params.transformReplyPayload
|
||||
? undefined
|
||||
: channelId
|
||||
? getChannelPlugin(channelId)
|
||||
: undefined;
|
||||
const transformReplyPayload =
|
||||
params.transformReplyPayload ??
|
||||
(plugin?.messaging?.transformReplyPayload
|
||||
? (payload: ReplyPayload) =>
|
||||
plugin.messaging?.transformReplyPayload?.({
|
||||
payload,
|
||||
cfg: params.cfg,
|
||||
accountId: params.accountId,
|
||||
}) ?? payload
|
||||
: undefined);
|
||||
return {
|
||||
...createReplyPrefixOptions({
|
||||
cfg: params.cfg,
|
||||
|
||||
52
src/tts/tts-config.test.ts
Normal file
52
src/tts/tts-config.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import { shouldAttemptTtsPayload } from "./tts-config.js";
|
||||
|
||||
describe("shouldAttemptTtsPayload", () => {
|
||||
let originalPrefsPath: string | undefined;
|
||||
let dir: string;
|
||||
let prefsPath: string;
|
||||
|
||||
beforeEach(() => {
|
||||
originalPrefsPath = process.env.OPENCLAW_TTS_PREFS;
|
||||
dir = mkdtempSync(path.join(tmpdir(), "openclaw-tts-config-"));
|
||||
prefsPath = path.join(dir, "tts.json");
|
||||
process.env.OPENCLAW_TTS_PREFS = prefsPath;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
if (originalPrefsPath === undefined) {
|
||||
delete process.env.OPENCLAW_TTS_PREFS;
|
||||
} else {
|
||||
process.env.OPENCLAW_TTS_PREFS = originalPrefsPath;
|
||||
}
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("skips TTS when config, prefs, and session state leave auto mode off", () => {
|
||||
expect(shouldAttemptTtsPayload({ cfg: {} as OpenClawConfig })).toBe(false);
|
||||
});
|
||||
|
||||
it("honors session auto state before prefs and config", () => {
|
||||
writeFileSync(prefsPath, JSON.stringify({ tts: { auto: "off" } }));
|
||||
const cfg = { messages: { tts: { auto: "off" } } } as OpenClawConfig;
|
||||
|
||||
expect(shouldAttemptTtsPayload({ cfg, ttsAuto: "always" })).toBe(true);
|
||||
expect(shouldAttemptTtsPayload({ cfg, ttsAuto: "off" })).toBe(false);
|
||||
});
|
||||
|
||||
it("uses local prefs before config auto mode", () => {
|
||||
const cfg = { messages: { tts: { auto: "off" } } } as OpenClawConfig;
|
||||
|
||||
writeFileSync(prefsPath, JSON.stringify({ tts: { enabled: true } }));
|
||||
expect(shouldAttemptTtsPayload({ cfg })).toBe(true);
|
||||
|
||||
writeFileSync(prefsPath, JSON.stringify({ tts: { auto: "off" } }));
|
||||
expect(
|
||||
shouldAttemptTtsPayload({ cfg: { messages: { tts: { enabled: true } } } as OpenClawConfig }),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -1,7 +1,65 @@
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { TtsMode } from "../config/types.tts.js";
|
||||
import type { TtsAutoMode, TtsMode } from "../config/types.tts.js";
|
||||
import { resolveConfigDir, resolveUserPath } from "../utils.js";
|
||||
import { normalizeTtsAutoMode } from "./tts-auto-mode.js";
|
||||
export { normalizeTtsAutoMode } from "./tts-auto-mode.js";
|
||||
|
||||
export function resolveConfiguredTtsMode(cfg: OpenClawConfig): TtsMode {
|
||||
return cfg.messages?.tts?.mode ?? "final";
|
||||
}
|
||||
|
||||
function resolveTtsPrefsPathValue(prefsPath: string | undefined): string {
|
||||
if (prefsPath?.trim()) {
|
||||
return resolveUserPath(prefsPath.trim());
|
||||
}
|
||||
const envPath = process.env.OPENCLAW_TTS_PREFS?.trim();
|
||||
if (envPath) {
|
||||
return resolveUserPath(envPath);
|
||||
}
|
||||
return path.join(resolveConfigDir(process.env), "settings", "tts.json");
|
||||
}
|
||||
|
||||
function readTtsPrefsAutoMode(prefsPath: string): TtsAutoMode | undefined {
|
||||
try {
|
||||
if (!existsSync(prefsPath)) {
|
||||
return undefined;
|
||||
}
|
||||
const prefs = JSON.parse(readFileSync(prefsPath, "utf8")) as {
|
||||
tts?: { auto?: unknown; enabled?: unknown };
|
||||
};
|
||||
const auto = normalizeTtsAutoMode(prefs.tts?.auto);
|
||||
if (auto) {
|
||||
return auto;
|
||||
}
|
||||
if (typeof prefs.tts?.enabled === "boolean") {
|
||||
return prefs.tts.enabled ? "always" : "off";
|
||||
}
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function shouldAttemptTtsPayload(params: {
|
||||
cfg: OpenClawConfig;
|
||||
ttsAuto?: string;
|
||||
}): boolean {
|
||||
const sessionAuto = normalizeTtsAutoMode(params.ttsAuto);
|
||||
if (sessionAuto) {
|
||||
return sessionAuto !== "off";
|
||||
}
|
||||
|
||||
const raw = params.cfg.messages?.tts;
|
||||
const prefsAuto = readTtsPrefsAutoMode(resolveTtsPrefsPathValue(raw?.prefsPath));
|
||||
if (prefsAuto) {
|
||||
return prefsAuto !== "off";
|
||||
}
|
||||
|
||||
const configuredAuto = normalizeTtsAutoMode(raw?.auto);
|
||||
if (configuredAuto) {
|
||||
return configuredAuto !== "off";
|
||||
}
|
||||
return raw?.enabled === true;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user