feat: expand intelligent investigation workspace
Generic Container CI/CD / test-build-publish (push) Successful in 1m52s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 1m52s

This commit is contained in:
2026-10-30 17:30:19 +08:00
parent ddd03dbff6
commit c0e4231b83
223 changed files with 13155 additions and 5304 deletions
+54 -17
View File
@@ -1,7 +1,10 @@
import type { IncomingMessage, ServerResponse } from "node:http";
import { Communicate } from "edge-tts-ts";
import { EdgeTTS } from "node-edge-tts";
import { speechVoiceIdSchema, type SpeechVoice } from "../src/shared/tts/voices";
import { createEdgeTtsVoiceCatalog } from "./edge-tts-voices";
const EDGE_TTS_PATH = "/api/tts/edge";
const EDGE_TTS_VOICES_PATH = `${EDGE_TTS_PATH}/voices`;
const DEFAULT_VOICE = "zh-CN-XiaoxiaoNeural";
const MAX_REQUEST_BYTES = 8 * 1024;
const MAX_TEXT_LENGTH = 520;
@@ -32,6 +35,7 @@ export type EdgeTtsMiddlewareOptions = {
synthesize?: EdgeTtsSynthesizer;
timeoutMs?: number;
trustProxy?: boolean;
listVoices?: () => Promise<SpeechVoice[]>;
};
function sendJson(response: ServerResponse, status: number, message: string) {
@@ -76,14 +80,15 @@ function getClientKey(request: IncomingMessage, trustProxy: boolean) {
}
export async function synthesizeEdgeSpeech(text: string, voice: string): Promise<Uint8Array> {
const communicate = new Communicate(text, { voice });
const communicate = new EdgeTTS({ voice, timeout: DEFAULT_TIMEOUT_MS });
const chunks: Uint8Array[] = [];
let byteLength = 0;
for await (const chunk of communicate.stream()) {
if (chunk.type !== "audio") continue;
chunks.push(chunk.data);
byteLength += chunk.data.byteLength;
for await (const chunk of communicate.streamAudio(text)) {
if (!Buffer.isBuffer(chunk)) continue;
chunks.push(chunk);
byteLength += chunk.byteLength;
if (byteLength > MAX_AUDIO_BYTES) throw new Error("语音服务返回的音频过大");
}
if (!byteLength) throw new Error("语音服务返回了空音频");
@@ -99,27 +104,32 @@ export async function synthesizeEdgeSpeech(text: string, voice: string): Promise
export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {}) {
const synthesize = options.synthesize ?? synthesizeEdgeSpeech;
const defaultVoice = options.defaultVoice ?? process.env.EDGE_TTS_VOICE ?? DEFAULT_VOICE;
const configuredVoice = options.defaultVoice ?? process.env.EDGE_TTS_VOICE ?? DEFAULT_VOICE;
const defaultVoice = speechVoiceIdSchema.safeParse(configuredVoice).success ? configuredVoice : DEFAULT_VOICE;
const maxConcurrent = options.maxConcurrent ?? DEFAULT_MAX_CONCURRENT;
const maxRequestsPerWindow = options.maxRequestsPerWindow ?? DEFAULT_RATE_LIMIT;
const rateWindowMs = options.rateWindowMs ?? DEFAULT_RATE_WINDOW_MS;
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
const trustProxy = options.trustProxy ?? false;
const now = options.now ?? Date.now;
const listVoices = createEdgeTtsVoiceCatalog(options.listVoices, now);
const rateBuckets = new Map<string, RateBucket>();
let activeRequests = 0;
return async (request: IncomingMessage, response: ServerResponse, next?: () => void) => {
if (request.url?.split("?", 1)[0] !== EDGE_TTS_PATH) {
const path = request.url?.split("?", 1)[0];
const voicesRequest = path === EDGE_TTS_VOICES_PATH;
if (path !== EDGE_TTS_PATH && !voicesRequest) {
next?.();
return;
}
if (request.method !== "POST") {
response.setHeader("Allow", "POST");
sendJson(response, 405, "仅支持 POST 请求");
const method = voicesRequest ? "GET" : "POST";
if (request.method !== method) {
response.setHeader("Allow", method);
sendJson(response, 405, `仅支持 ${method} 请求`);
return;
}
if (!request.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
if (!voicesRequest && !request.headers["content-type"]?.toLowerCase().startsWith("application/json")) {
sendJson(response, 415, "请求必须使用 application/json");
return;
}
@@ -143,6 +153,19 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
return;
}
if (voicesRequest) {
try {
const voices = await listVoices();
response.statusCode = 200;
response.setHeader("Content-Type", "application/json; charset=utf-8");
response.setHeader("Cache-Control", "no-store");
response.end(JSON.stringify({ voices, defaultVoice }));
} catch {
sendJson(response, 503, "音色列表暂时不可用,请稍后重试");
}
return;
}
let payload: EdgeTtsPayload;
try {
payload = await readJson(request);
@@ -151,10 +174,6 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
return;
}
if (Object.prototype.hasOwnProperty.call(payload, "voice")) {
sendJson(response, 400, "语音角色只能由服务端配置");
return;
}
const text = typeof payload.text === "string" ? payload.text.trim() : "";
if (!text) {
sendJson(response, 400, "缺少待朗读文本");
@@ -164,6 +183,24 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
sendJson(response, 413, `单次文本不能超过 ${MAX_TEXT_LENGTH} 字`);
return;
}
let voice = defaultVoice;
if (Object.prototype.hasOwnProperty.call(payload, "voice")) {
const selectedVoice = speechVoiceIdSchema.safeParse(payload.voice);
if (!selectedVoice.success) {
sendJson(response, 400, "请选择有效音色");
return;
}
try {
if (!(await listVoices()).some((item) => item.id === selectedVoice.data)) {
sendJson(response, 400, "所选音色不可用,请重新选择");
return;
}
voice = selectedVoice.data;
} catch {
sendJson(response, 503, "无法确认音色是否可用,请稍后重试");
return;
}
}
if (activeRequests >= maxConcurrent) {
response.setHeader("Retry-After", "1");
sendJson(response, 429, "语音服务繁忙,请稍后重试");
@@ -171,7 +208,7 @@ export function createEdgeTtsMiddleware(options: EdgeTtsMiddlewareOptions = {})
}
activeRequests += 1;
const synthesisTask = Promise.resolve().then(() => synthesize(text, defaultVoice));
const synthesisTask = Promise.resolve().then(() => synthesize(text, voice));
void synthesisTask.finally(() => {
activeRequests -= 1;
}).catch(() => undefined);