provider.registry.ts 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183
  1. /**
  2. * TTS Provider 注册表
  3. *
  4. * 集中管理所有语音合成供应商,提供熔断保护和轮转调度。
  5. * 与 LLM 采用相同逻辑:平等轮转,无优先级。
  6. */
  7. import { ProviderRegistry } from '../../common/provider-registry';
  8. import { ITtsProvider } from './provider.interface';
  9. import { AliyunTtsProvider } from './aliyun.provider';
  10. import { MockTtsProvider } from './mock.provider';
  11. import { ttsLogger } from './tts-logger';
  12. import { config } from '../../config';
  13. let _registry: ProviderRegistry<ITtsProvider> | null = null;
  14. const TTS_PROVIDER_MAP: Record<string, new (vendorKey: string, apiKey?: string, modelId?: string, ttsApiPath?: string, maxTextLength?: number) => ITtsProvider> = {
  15. bailian: AliyunTtsProvider,
  16. };
  17. /**
  18. * 初始化 TTS Provider 注册表(懒加载,服务启动时调用一次)
  19. */
  20. export function initTtsRegistry(): ProviderRegistry<ITtsProvider> {
  21. if (_registry) return _registry;
  22. _registry = new ProviderRegistry<ITtsProvider>();
  23. const vendors = (config.models as any).vendors;
  24. // 遍历所有 vendor,为有 TTS 模型的供应商注册 TTS Provider
  25. if (vendors) {
  26. for (const [vendorKey, vendorData] of Object.entries(vendors) as [string, any][]) {
  27. const hasTts = vendorData.models?.some((m: any) => m.input?.includes('tts') && m.enabled);
  28. if (!hasTts) continue;
  29. // 1. 匹配映射表中的 key
  30. const ProviderClass = TTS_PROVIDER_MAP[vendorKey];
  31. if (!ProviderClass) {
  32. ttsLogger.registry('skip', vendorKey, '无对应 Provider 实现');
  33. continue;
  34. }
  35. try {
  36. // 从 models.json 提取 TTS 模型配置
  37. const ttsModels = (vendorData.models || [])
  38. .filter((m: any) => m.input?.includes('tts') && m.enabled);
  39. const ttsModel = ttsModels[0]; // 取第一个启用的 TTS 模型
  40. const modelId = ttsModel?.id;
  41. // 模型有独立 apiPath 时用它,否则用 vendor 级 ttsApiPath
  42. const ttsApiPath = ttsModel?.apiPath
  43. ? `https://dashscope.aliyuncs.com${ttsModel.apiPath}`
  44. : vendorData.ttsApiPath;
  45. const maxTextLen = ttsModel?.maxTextLength;
  46. const provider = new ProviderClass(vendorKey, vendorData.apiKey, modelId, ttsApiPath, maxTextLen);
  47. _registry.register(provider, {
  48. enabled: true,
  49. breakerConfig: { name: `${vendorKey}-tts`, failureThreshold: 3, cooldownMs: 60000 },
  50. });
  51. ttsLogger.registry('register', provider.name, `model=${modelId}, vendor=${vendorKey}`);
  52. } catch (e) {
  53. ttsLogger.error(`注册 ${vendorKey} TTS 失败`, e as Error);
  54. }
  55. }
  56. }
  57. // Mock Provider 兜底(当前已禁用,需要时改为 enabled: true)
  58. if (!_registry.get('mock-tts')) {
  59. const mockProvider = new MockTtsProvider();
  60. _registry.register(mockProvider, {
  61. enabled: false,
  62. breakerConfig: { name: 'mock-tts', failureThreshold: 999, cooldownMs: 1000 },
  63. });
  64. ttsLogger.registry('register', mockProvider.name, '已禁用');
  65. }
  66. ttsLogger.info(`TTS Registry 初始化完成,共 ${_registry.size} 个 Provider`);
  67. return _registry;
  68. }
  69. /**
  70. * 获取 TTS Provider 注册表
  71. */
  72. export function getTtsRegistry(): ProviderRegistry<ITtsProvider> {
  73. if (!_registry) {
  74. return initTtsRegistry();
  75. }
  76. return _registry;
  77. }
  78. /**
  79. * 按名称获取特定 TTS Provider(带熔断保护)
  80. * @param preferredName 优先使用的 Provider 名称
  81. * @returns 返回可用的 Provider 节点,如果优先的已熔断则返回下一个
  82. */
  83. export function getAvailableTtsProvider(preferredName?: string): {
  84. provider: ITtsProvider;
  85. node: import('../../common/provider-registry').ProviderNode<ITtsProvider>;
  86. } {
  87. const registry = getTtsRegistry();
  88. if (preferredName) {
  89. const preferredNode = registry.get(preferredName);
  90. if (preferredNode && preferredNode.enabled && !preferredNode.breaker.isOpen() && !preferredNode.exhausted) {
  91. return { provider: preferredNode.provider, node: preferredNode };
  92. }
  93. // 优先的不可用,尝试下一个
  94. const next = registry.getNextAvailable(preferredName);
  95. if (next) {
  96. ttsLogger.fallback(preferredName, next.provider.name, '优先 Provider 不可用');
  97. return { provider: next.provider, node: next };
  98. }
  99. }
  100. // 无偏好,用最高优先级
  101. const primary = registry.getPrimary();
  102. if (!primary) {
  103. throw new Error('没有可用的 TTS Provider');
  104. }
  105. return { provider: primary.provider, node: primary };
  106. }
  107. // ============ 定时健康检查 ============
  108. let _healthCheckTimer: ReturnType<typeof setInterval> | null = null;
  109. const HEALTH_CHECK_INTERVAL_MS = 30_000; // 30s
  110. /** 启动 TTS 定时健康检查 */
  111. export function startTtsHealthCheck(): void {
  112. if (_healthCheckTimer) return;
  113. _healthCheckTimer = setInterval(async () => {
  114. const registry = getTtsRegistry();
  115. // 检查熔断恢复
  116. const broken = registry.listBroken();
  117. for (const node of broken) {
  118. try {
  119. const state = node.breaker.getState();
  120. if (state === 'HALF_OPEN') {
  121. ttsLogger.healthCheck(node.provider.name, 'still_down', '熔断冷却期已过,尝试探测...');
  122. const healthy = await node.provider.healthCheck?.();
  123. if (healthy) {
  124. node.breaker.reset();
  125. ttsLogger.circuitBreaker(node.provider.name, 'closed', '探测成功');
  126. } else {
  127. ttsLogger.healthCheck(node.provider.name, 'still_down', '探测失败,继续熔断');
  128. }
  129. }
  130. } catch {
  131. // 探测异常,保持熔断
  132. }
  133. }
  134. // 检查额度耗尽恢复(已标记 exhausted 的供应商恢复探测)
  135. const exhausted = registry.listExhausted();
  136. for (const node of exhausted) {
  137. try {
  138. // 额度耗尽不带 TTL,由健康检查探测恢复
  139. const healthy = await node.provider.healthCheck?.();
  140. if (healthy) {
  141. registry.clearExhausted(node.provider.name);
  142. ttsLogger.quota(node.provider.name, 'recovered');
  143. }
  144. } catch {
  145. // 探测失败,保持耗尽
  146. }
  147. }
  148. }, HEALTH_CHECK_INTERVAL_MS);
  149. ttsLogger.info(`TTS HealthCheck 已启动,间隔 ${HEALTH_CHECK_INTERVAL_MS / 1000}s`);
  150. }
  151. /** 停止 TTS 定时健康检查 */
  152. export function stopTtsHealthCheck(): void {
  153. if (_healthCheckTimer) {
  154. clearInterval(_healthCheckTimer);
  155. _healthCheckTimer = null;
  156. ttsLogger.info('TTS HealthCheck 已停止');
  157. }
  158. }