重构主控编排与运行时预热链路,统一工作区提示词/专家调度协议并补齐 wiki 记忆注入与写回闭环。

同时收敛启动与运维脚本默认行为(含 wiki worker)、更新 Admin 可观测性与相关测试,降低首轮时延并提高运行稳定性。

Made-with: Cursor
This commit is contained in:
oliver 2026-04-26 08:34:33 +08:00
parent 4a23b715a2
commit dbbe3add6a
14438 changed files with 2693620 additions and 2546 deletions

View file

@ -0,0 +1,140 @@
import { rmSync } from "node:fs";
import path from "node:path";
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-payload";
import type { SpeechProviderPlugin, SpeechSynthesisRequest } from "openclaw/plugin-sdk/speech-core";
import { afterEach, describe, expect, it, vi } from "vitest";
type MockSpeechSynthesisResult = Awaited<ReturnType<SpeechProviderPlugin["synthesize"]>>;
const synthesizeMock = vi.hoisted(() =>
vi.fn(
async (request: SpeechSynthesisRequest): Promise<MockSpeechSynthesisResult> => ({
audioBuffer: Buffer.from("voice"),
fileExtension: ".ogg",
outputFormat: "ogg",
voiceCompatible: request.target === "voice-note",
}),
),
);
const listSpeechProvidersMock = vi.hoisted(() => vi.fn());
const getSpeechProviderMock = vi.hoisted(() => vi.fn());
vi.mock("openclaw/plugin-sdk/channel-targets", () => ({
normalizeChannelId: (channel: string | undefined) => channel?.trim().toLowerCase() ?? null,
}));
vi.mock("../api.js", async () => {
const actual = await vi.importActual<typeof import("../api.js")>("../api.js");
const mockProvider: SpeechProviderPlugin = {
id: "mock",
label: "Mock",
autoSelectOrder: 1,
isConfigured: () => true,
synthesize: synthesizeMock,
};
listSpeechProvidersMock.mockImplementation(() => [mockProvider]);
getSpeechProviderMock.mockImplementation((providerId: string) =>
providerId === "mock" ? mockProvider : null,
);
return {
...actual,
canonicalizeSpeechProviderId: (providerId: string | undefined) =>
providerId?.trim().toLowerCase() || undefined,
normalizeSpeechProviderId: (providerId: string | undefined) =>
providerId?.trim().toLowerCase() || undefined,
getSpeechProvider: getSpeechProviderMock,
listSpeechProviders: listSpeechProvidersMock,
scheduleCleanup: vi.fn(),
};
});
const { _test, maybeApplyTtsToPayload } = await import("./tts.js");
const nativeVoiceNoteChannels = ["discord", "feishu", "matrix", "telegram", "whatsapp"] as const;
function createTtsConfig(prefsName: string): OpenClawConfig {
return {
messages: {
tts: {
enabled: true,
provider: "mock",
prefsPath: `/tmp/${prefsName}.json`,
},
},
};
}
describe("speech-core native voice-note routing", () => {
afterEach(() => {
synthesizeMock.mockClear();
});
it("keeps native voice-note channel support centralized", () => {
for (const channel of nativeVoiceNoteChannels) {
expect(_test.supportsNativeVoiceNoteTts(channel)).toBe(true);
expect(_test.supportsNativeVoiceNoteTts(channel.toUpperCase())).toBe(true);
}
expect(_test.supportsNativeVoiceNoteTts("slack")).toBe(false);
expect(_test.supportsNativeVoiceNoteTts(undefined)).toBe(false);
});
it("marks Discord auto TTS replies as native voice messages", async () => {
const cfg = createTtsConfig("openclaw-speech-core-tts-test");
const payload: ReplyPayload = {
text: "This Discord reply should be delivered as a native voice note.",
};
let mediaDir: string | undefined;
try {
const result = await maybeApplyTtsToPayload({
payload,
cfg,
channel: "discord",
kind: "final",
});
expect(synthesizeMock).toHaveBeenCalledWith(
expect.objectContaining({ target: "voice-note" }),
);
expect(result.audioAsVoice).toBe(true);
expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/);
mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined;
} finally {
if (mediaDir) {
rmSync(mediaDir, { recursive: true, force: true });
}
}
});
it("keeps non-native voice-note channels as regular audio files", async () => {
const cfg = createTtsConfig("openclaw-speech-core-tts-slack-test");
const payload: ReplyPayload = {
text: "Slack replies should be delivered as regular audio attachments.",
};
let mediaDir: string | undefined;
try {
const result = await maybeApplyTtsToPayload({
payload,
cfg,
channel: "slack",
kind: "final",
});
expect(synthesizeMock).toHaveBeenCalledWith(
expect.objectContaining({ target: "audio-file" }),
);
expect(result.audioAsVoice).toBeUndefined();
expect(result.mediaUrl).toMatch(/voice-\d+\.ogg$/);
mediaDir = result.mediaUrl ? path.dirname(result.mediaUrl) : undefined;
} finally {
if (mediaDir) {
rmSync(mediaDir, { recursive: true, force: true });
}
}
});
});

File diff suppressed because it is too large Load diff