





















@@ -90,6 +90,7 @@ const mocks = vi.hoisted(() => ({
9090executePollAction: vi.fn(),
9191callGatewayLeastPrivilege: vi.fn(),
9292randomIdempotencyKey: vi.fn(() => "idem-gateway-action"),
93+maybeApplyTtsToPayload: vi.fn(async (params: { payload: unknown }) => params.payload),
9394}));
94959596vi.mock("./channel-resolution.js", () => ({
@@ -107,6 +108,10 @@ vi.mock("./message.gateway.runtime.js", () => ({
107108randomIdempotencyKey: mocks.randomIdempotencyKey,
108109}));
109110111+vi.mock("../../tts/tts.runtime.js", () => ({
112+maybeApplyTtsToPayload: mocks.maybeApplyTtsToPayload,
113+}));
114+110115vi.mock("./outbound-session.js", () => ({
111116ensureOutboundSessionEntry: vi.fn(async () => undefined),
112117resolveOutboundSessionRoute: vi.fn(async () => null),
@@ -268,6 +273,10 @@ describe("runMessageAction plugin dispatch", () => {
268273);
269274mocks.callGatewayLeastPrivilege.mockReset();
270275mocks.randomIdempotencyKey.mockClear();
276+mocks.maybeApplyTtsToPayload.mockReset();
277+mocks.maybeApplyTtsToPayload.mockImplementation(
278+async (params: { payload: unknown }) => params.payload,
279+);
271280});
272281273282describe("alias-based plugin action dispatch", () => {
@@ -697,6 +706,151 @@ describe("runMessageAction plugin dispatch", () => {
697706);
698707});
699708709+it("applies TTS before gateway-executed plugin sends", async () => {
710+const gatewayPlugin = createGatewayActionPlugin({
711+pluginId: "gatewaychat",
712+label: "Gateway Chat",
713+blurb: "Gateway Chat send test plugin.",
714+actions: ["send"],
715+messaging: {
716+targetResolver: {
717+looksLikeId: () => true,
718+},
719+},
720+handleAction: vi.fn(async () => jsonResult({ ok: true, local: true })),
721+});
722+setActivePluginRegistry(
723+createTestRegistry([
724+{
725+pluginId: "gatewaychat",
726+source: "test",
727+plugin: gatewayPlugin,
728+},
729+]),
730+);
731+mocks.callGatewayLeastPrivilege.mockResolvedValue({
732+ok: true,
733+messageId: "gw-send-tts",
734+});
735+mocks.maybeApplyTtsToPayload.mockResolvedValueOnce({
736+mediaUrl: "file:///tmp/openclaw-voice.ogg",
737+audioAsVoice: true,
738+spokenText: "hello there",
739+});
740+741+await runMessageAction({
742+cfg: {
743+channels: {
744+gatewaychat: {
745+enabled: true,
746+},
747+},
748+messages: {
749+tts: {
750+auto: "tagged",
751+},
752+},
753+} as OpenClawConfig,
754+action: "send",
755+params: {
756+channel: "gatewaychat",
757+target: "user-123",
758+message: "[[tts:text]]hello there[[/tts:text]]",
759+},
760+gateway: {
761+clientName: "cli",
762+mode: "cli",
763+},
764+dryRun: false,
765+});
766+767+const gatewayCall = readMockCallArg(
768+mocks.callGatewayLeastPrivilege,
769+"gateway least privilege call",
770+);
771+const gatewayParams = readRecordField(gatewayCall, "params", "gateway call params");
772+expectRecordFields(
773+readRecordField(gatewayParams, "params", "gateway message params"),
774+{
775+message: "",
776+media: "file:///tmp/openclaw-voice.ogg",
777+mediaUrl: "file:///tmp/openclaw-voice.ogg",
778+asVoice: true,
779+audioAsVoice: true,
780+},
781+"gateway message params",
782+);
783+expect(mocks.executeSendAction).not.toHaveBeenCalled();
784+});
785+786+it("applies TTS before local plugin send fallback dispatch", async () => {
787+const handleAction = vi.fn(async ({ params }: { params: Record<string, unknown> }) =>
788+jsonResult({ ok: true, params }),
789+);
790+const localPlugin = createGatewayActionPlugin({
791+pluginId: "localchat",
792+label: "Local Chat",
793+blurb: "Local Chat send test plugin.",
794+actions: ["send"],
795+gatewayActions: [],
796+messaging: {
797+targetResolver: {
798+looksLikeId: () => true,
799+},
800+},
801+ handleAction,
802+});
803+setActivePluginRegistry(
804+createTestRegistry([
805+{
806+pluginId: "localchat",
807+source: "test",
808+plugin: localPlugin,
809+},
810+]),
811+);
812+mocks.maybeApplyTtsToPayload.mockResolvedValueOnce({
813+mediaUrl: "file:///tmp/openclaw-voice.ogg",
814+audioAsVoice: true,
815+spokenText: "hello there",
816+});
817+818+await runMessageAction({
819+cfg: {
820+channels: {
821+localchat: {
822+enabled: true,
823+},
824+},
825+messages: {
826+tts: {
827+auto: "tagged",
828+},
829+},
830+} as OpenClawConfig,
831+action: "send",
832+params: {
833+channel: "localchat",
834+target: "user-123",
835+message: "[[tts:text]]hello there[[/tts:text]]",
836+},
837+dryRun: false,
838+});
839+840+const call = readFirstPluginCall(handleAction);
841+expectRecordFields(
842+readRecordField(call, "params", "local plugin params"),
843+{
844+message: "",
845+media: "file:///tmp/openclaw-voice.ogg",
846+mediaUrl: "file:///tmp/openclaw-voice.ogg",
847+asVoice: true,
848+audioAsVoice: true,
849+},
850+"local plugin params",
851+);
852+});
853+700854it("uses requester session channel policy for host-media reads", async () => {
701855const handlePolicyCheckedAction = vi.fn(async ({ mediaAccess }) =>
702856jsonResult({
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。