| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183 |
- /**
- * TTS Provider 注册表
- *
- * 集中管理所有语音合成供应商,提供熔断保护和轮转调度。
- * 与 LLM 采用相同逻辑:平等轮转,无优先级。
- */
- import { ProviderRegistry } from '../../common/provider-registry';
- import { ITtsProvider } from './provider.interface';
- import { AliyunTtsProvider } from './aliyun.provider';
- import { MockTtsProvider } from './mock.provider';
- import { ttsLogger } from './tts-logger';
- import { config } from '../../config';
- let _registry: ProviderRegistry<ITtsProvider> | null = null;
- const TTS_PROVIDER_MAP: Record<string, new (vendorKey: string, apiKey?: string, modelId?: string, ttsApiPath?: string, maxTextLength?: number) => ITtsProvider> = {
- bailian: AliyunTtsProvider,
- };
- /**
- * 初始化 TTS Provider 注册表(懒加载,服务启动时调用一次)
- */
- export function initTtsRegistry(): ProviderRegistry<ITtsProvider> {
- if (_registry) return _registry;
- _registry = new ProviderRegistry<ITtsProvider>();
- const vendors = (config.models as any).vendors;
- // 遍历所有 vendor,为有 TTS 模型的供应商注册 TTS Provider
- if (vendors) {
- for (const [vendorKey, vendorData] of Object.entries(vendors) as [string, any][]) {
- const hasTts = vendorData.models?.some((m: any) => m.input?.includes('tts') && m.enabled);
- if (!hasTts) continue;
- // 1. 匹配映射表中的 key
- const ProviderClass = TTS_PROVIDER_MAP[vendorKey];
- if (!ProviderClass) {
- ttsLogger.registry('skip', vendorKey, '无对应 Provider 实现');
- continue;
- }
- try {
- // 从 models.json 提取 TTS 模型配置
- const ttsModels = (vendorData.models || [])
- .filter((m: any) => m.input?.includes('tts') && m.enabled);
- const ttsModel = ttsModels[0]; // 取第一个启用的 TTS 模型
- const modelId = ttsModel?.id;
- // 模型有独立 apiPath 时用它,否则用 vendor 级 ttsApiPath
- const ttsApiPath = ttsModel?.apiPath
- ? `https://dashscope.aliyuncs.com${ttsModel.apiPath}`
- : vendorData.ttsApiPath;
- const maxTextLen = ttsModel?.maxTextLength;
- const provider = new ProviderClass(vendorKey, vendorData.apiKey, modelId, ttsApiPath, maxTextLen);
- _registry.register(provider, {
- enabled: true,
- breakerConfig: { name: `${vendorKey}-tts`, failureThreshold: 3, cooldownMs: 60000 },
- });
- ttsLogger.registry('register', provider.name, `model=${modelId}, vendor=${vendorKey}`);
- } catch (e) {
- ttsLogger.error(`注册 ${vendorKey} TTS 失败`, e as Error);
- }
- }
- }
- // Mock Provider 兜底(当前已禁用,需要时改为 enabled: true)
- if (!_registry.get('mock-tts')) {
- const mockProvider = new MockTtsProvider();
- _registry.register(mockProvider, {
- enabled: false,
- breakerConfig: { name: 'mock-tts', failureThreshold: 999, cooldownMs: 1000 },
- });
- ttsLogger.registry('register', mockProvider.name, '已禁用');
- }
- ttsLogger.info(`TTS Registry 初始化完成,共 ${_registry.size} 个 Provider`);
- return _registry;
- }
- /**
- * 获取 TTS Provider 注册表
- */
- export function getTtsRegistry(): ProviderRegistry<ITtsProvider> {
- if (!_registry) {
- return initTtsRegistry();
- }
- return _registry;
- }
- /**
- * 按名称获取特定 TTS Provider(带熔断保护)
- * @param preferredName 优先使用的 Provider 名称
- * @returns 返回可用的 Provider 节点,如果优先的已熔断则返回下一个
- */
- export function getAvailableTtsProvider(preferredName?: string): {
- provider: ITtsProvider;
- node: import('../../common/provider-registry').ProviderNode<ITtsProvider>;
- } {
- const registry = getTtsRegistry();
- if (preferredName) {
- const preferredNode = registry.get(preferredName);
- if (preferredNode && preferredNode.enabled && !preferredNode.breaker.isOpen() && !preferredNode.exhausted) {
- return { provider: preferredNode.provider, node: preferredNode };
- }
- // 优先的不可用,尝试下一个
- const next = registry.getNextAvailable(preferredName);
- if (next) {
- ttsLogger.fallback(preferredName, next.provider.name, '优先 Provider 不可用');
- return { provider: next.provider, node: next };
- }
- }
- // 无偏好,用最高优先级
- const primary = registry.getPrimary();
- if (!primary) {
- throw new Error('没有可用的 TTS Provider');
- }
- return { provider: primary.provider, node: primary };
- }
- // ============ 定时健康检查 ============
- let _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
- const HEALTH_CHECK_INTERVAL_MS = 30_000; // 30s
- /** 启动 TTS 定时健康检查 */
- export function startTtsHealthCheck(): void {
- if (_healthCheckTimer) return;
- _healthCheckTimer = setInterval(async () => {
- const registry = getTtsRegistry();
- // 检查熔断恢复
- const broken = registry.listBroken();
- for (const node of broken) {
- try {
- const state = node.breaker.getState();
- if (state === 'HALF_OPEN') {
- ttsLogger.healthCheck(node.provider.name, 'still_down', '熔断冷却期已过,尝试探测...');
- const healthy = await node.provider.healthCheck?.();
- if (healthy) {
- node.breaker.reset();
- ttsLogger.circuitBreaker(node.provider.name, 'closed', '探测成功');
- } else {
- ttsLogger.healthCheck(node.provider.name, 'still_down', '探测失败,继续熔断');
- }
- }
- } catch {
- // 探测异常,保持熔断
- }
- }
- // 检查额度耗尽恢复(已标记 exhausted 的供应商恢复探测)
- const exhausted = registry.listExhausted();
- for (const node of exhausted) {
- try {
- // 额度耗尽不带 TTL,由健康检查探测恢复
- const healthy = await node.provider.healthCheck?.();
- if (healthy) {
- registry.clearExhausted(node.provider.name);
- ttsLogger.quota(node.provider.name, 'recovered');
- }
- } catch {
- // 探测失败,保持耗尽
- }
- }
- }, HEALTH_CHECK_INTERVAL_MS);
- ttsLogger.info(`TTS HealthCheck 已启动,间隔 ${HEALTH_CHECK_INTERVAL_MS / 1000}s`);
- }
- /** 停止 TTS 定时健康检查 */
- export function stopTtsHealthCheck(): void {
- if (_healthCheckTimer) {
- clearInterval(_healthCheckTimer);
- _healthCheckTimer = null;
- ttsLogger.info('TTS HealthCheck 已停止');
- }
- }
|