


























@@ -1,3 +1,6 @@
1+import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
2+import { tmpdir } from "node:os";
3+import path from "node:path";
14import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2536const runFfmpegMock = vi.hoisted(() => vi.fn());
@@ -8,6 +11,13 @@ vi.mock("openclaw/plugin-sdk/media-runtime", () => ({
811912import { buildMinimaxSpeechProvider } from "./speech-provider.js";
101314+function clearMinimaxAuthEnv() {
15+delete process.env.MINIMAX_API_KEY;
16+delete process.env.MINIMAX_OAUTH_TOKEN;
17+delete process.env.MINIMAX_CODE_PLAN_KEY;
18+delete process.env.MINIMAX_CODING_API_KEY;
19+}
20+1121describe("buildMinimaxSpeechProvider", () => {
1222const provider = buildMinimaxSpeechProvider();
1323@@ -23,15 +33,28 @@ describe("buildMinimaxSpeechProvider", () => {
23332434it("exposes models and voices", () => {
2535expect(provider.models).toContain("speech-2.8-hd");
36+expect(provider.models).toEqual(expect.arrayContaining(["speech-2.6-hd", "speech-02-hd"]));
2637expect(provider.voices).toContain("English_expressive_narrator");
2738});
2839});
29403041describe("isConfigured", () => {
3142const savedEnv = { ...process.env };
43+let tempStateDir: string;
44+let tempAgentDir: string;
324533-afterEach(() => {
46+beforeEach(async () => {
47+tempStateDir = await mkdtemp(path.join(tmpdir(), "openclaw-minimax-tts-auth-"));
48+tempAgentDir = path.join(tempStateDir, "agents", "main", "agent");
49+await mkdir(tempAgentDir, { recursive: true });
50+process.env.OPENCLAW_STATE_DIR = tempStateDir;
51+process.env.OPENCLAW_AGENT_DIR = tempAgentDir;
52+clearMinimaxAuthEnv();
53+});
54+55+afterEach(async () => {
3456process.env = { ...savedEnv };
57+await rm(tempStateDir, { recursive: true, force: true });
3558});
36593760it("returns true when apiKey is in provider config", () => {
@@ -41,14 +64,36 @@ describe("buildMinimaxSpeechProvider", () => {
4164});
42654366it("returns false when no apiKey anywhere", () => {
44-delete process.env.MINIMAX_API_KEY;
4567expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(false);
4668});
47694870it("returns true when MINIMAX_API_KEY env var is set", () => {
4971process.env.MINIMAX_API_KEY = "sk-env";
5072expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true);
5173});
74+75+it("returns true when a MiniMax Token Plan env var is set", () => {
76+process.env.MINIMAX_CODING_API_KEY = "sk-cp-env";
77+expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true);
78+});
79+80+it("returns true when a MiniMax portal auth profile is available", async () => {
81+await writeFile(
82+path.join(tempAgentDir, "auth-profiles.json"),
83+JSON.stringify({
84+version: 1,
85+profiles: {
86+"minimax-portal:test": {
87+type: "token",
88+provider: "minimax-portal",
89+token: "portal-token",
90+},
91+},
92+}),
93+);
94+95+expect(provider.isConfigured({ providerConfig: {}, timeoutMs: 30000 })).toBe(true);
96+});
5297});
53985499describe("resolveConfig", () => {
@@ -94,14 +139,30 @@ describe("buildMinimaxSpeechProvider", () => {
94139});
9514096141it("keeps trusted MINIMAX_API_HOST fallback for TTS baseUrl", () => {
97-process.env.MINIMAX_API_HOST = "https://env.api.com";
142+process.env.MINIMAX_API_HOST = "https://api.minimax.io/anthropic";
98143process.env.MINIMAX_TTS_MODEL = "speech-01-240228";
99144process.env.MINIMAX_TTS_VOICE_ID = "Chinese (Mandarin)_Gentle_Boy";
100145const config = provider.resolveConfig!({ rawConfig: {}, cfg: {} as never, timeoutMs: 30000 });
101-expect(config.baseUrl).toBe("https://env.api.com");
146+expect(config.baseUrl).toBe("https://api.minimax.io");
102147expect(config.model).toBe("speech-01-240228");
103148expect(config.voiceId).toBe("Chinese (Mandarin)_Gentle_Boy");
104149});
150+151+it("derives the TTS host from minimax-portal OAuth config", () => {
152+delete process.env.MINIMAX_API_HOST;
153+const config = provider.resolveConfig!({
154+rawConfig: {},
155+cfg: {
156+models: {
157+providers: {
158+"minimax-portal": { baseUrl: "https://api.minimaxi.com/anthropic" },
159+},
160+},
161+} as never,
162+timeoutMs: 30000,
163+});
164+expect(config.baseUrl).toBe("https://api.minimaxi.com");
165+});
105166});
106167107168describe("parseDirectiveToken", () => {
@@ -217,15 +278,29 @@ describe("buildMinimaxSpeechProvider", () => {
217278218279describe("synthesize", () => {
219280const savedFetch = globalThis.fetch;
220-221-beforeEach(() => {
281+const savedEnv = { ...process.env };
282+let tempStateDir: string;
283+let tempAgentDir: string;
284+285+beforeEach(async () => {
286+tempStateDir = await mkdtemp(path.join(tmpdir(), "openclaw-minimax-tts-synth-"));
287+tempAgentDir = path.join(tempStateDir, "agents", "main", "agent");
288+await mkdir(tempAgentDir, { recursive: true });
289+process.env = {
290+ ...savedEnv,
291+OPENCLAW_AGENT_DIR: tempAgentDir,
292+OPENCLAW_STATE_DIR: tempStateDir,
293+};
294+clearMinimaxAuthEnv();
222295vi.stubGlobal("fetch", vi.fn());
223296runFfmpegMock.mockReset();
224297});
225298226-afterEach(() => {
299+afterEach(async () => {
227300globalThis.fetch = savedFetch;
301+process.env = { ...savedEnv };
228302vi.restoreAllMocks();
303+await rm(tempStateDir, { recursive: true, force: true });
229304});
230305231306it("makes correct API call and decodes hex response", async () => {
@@ -328,24 +403,74 @@ describe("buildMinimaxSpeechProvider", () => {
328403expect(body.voice_setting.pitch).toBe(0);
329404});
330405406+it("uses a MiniMax Token Plan env var when no API key is configured", async () => {
407+process.env.MINIMAX_CODING_API_KEY = "sk-cp-env";
408+const hexAudio = Buffer.from("audio").toString("hex");
409+vi.mocked(globalThis.fetch).mockResolvedValueOnce(
410+new Response(JSON.stringify({ data: { audio: hexAudio } }), { status: 200 }),
411+);
412+413+await provider.synthesize({
414+text: "Token plan TTS",
415+cfg: {} as never,
416+providerConfig: {},
417+target: "audio-file",
418+timeoutMs: 30000,
419+});
420+421+const [, init] = vi.mocked(globalThis.fetch).mock.calls[0];
422+expect(init?.headers).toMatchObject({ Authorization: "Bearer sk-cp-env" });
423+});
424+425+it("uses a minimax-portal auth profile before env API keys", async () => {
426+process.env.MINIMAX_API_KEY = "sk-env";
427+await writeFile(
428+path.join(tempAgentDir, "auth-profiles.json"),
429+JSON.stringify({
430+version: 1,
431+profiles: {
432+"minimax-portal:test": {
433+type: "token",
434+provider: "minimax-portal",
435+token: "portal-token",
436+},
437+},
438+}),
439+);
440+const hexAudio = Buffer.from("audio").toString("hex");
441+vi.mocked(globalThis.fetch).mockResolvedValueOnce(
442+new Response(JSON.stringify({ data: { audio: hexAudio } }), { status: 200 }),
443+);
444+445+await provider.synthesize({
446+text: "Portal TTS",
447+cfg: {
448+models: {
449+providers: {
450+"minimax-portal": { baseUrl: "https://api.minimaxi.com/anthropic" },
451+},
452+},
453+} as never,
454+providerConfig: {},
455+target: "audio-file",
456+timeoutMs: 30000,
457+});
458+459+const [url, init] = vi.mocked(globalThis.fetch).mock.calls[0];
460+expect(url).toBe("https://api.minimaxi.com/v1/t2a_v2");
461+expect(init?.headers).toMatchObject({ Authorization: "Bearer portal-token" });
462+});
463+331464it("throws when API key is missing", async () => {
332-const savedKey = process.env.MINIMAX_API_KEY;
333-delete process.env.MINIMAX_API_KEY;
334-try {
335-await expect(
336-provider.synthesize({
337-text: "Test",
338-cfg: {} as never,
339-providerConfig: {},
340-target: "audio-file",
341-timeoutMs: 30000,
342-}),
343-).rejects.toThrow("MiniMax API key missing");
344-} finally {
345-if (savedKey) {
346-process.env.MINIMAX_API_KEY = savedKey;
347-}
348-}
465+await expect(
466+provider.synthesize({
467+text: "Test",
468+cfg: {} as never,
469+providerConfig: {},
470+target: "audio-file",
471+timeoutMs: 30000,
472+}),
473+).rejects.toThrow("MiniMax TTS auth missing");
349474});
350475351476it("throws on API error with response body", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。