Sfoglia il codice sorgente

♻️ refactor(agora-stt): 重构Agora STT服务集成

- 从环境变量迁移到API获取配置,增强部署灵活性
- 统一Token获取逻辑,使用agoraClient API替代直接请求
- 添加错误处理和日志记录,提高稳定性
- 优化类型定义,修复usePrevious钩子类型问题

✨ feat(agora-stt): 添加AgoraSTTProvider组件

- 创建上下文提供者组件,统一管理STT服务状态
- 优化RTC和RTM连接流程,确保配置正确加载

🔧 chore(agora-stt): 清理冗余代码

- 删除废弃的useAgoraSTT钩子文件
- 移除硬编码的环境变量引用,全面使用动态配置
yourname 4 mesi fa
parent
commit
041c9bf754

+ 1 - 1
src/client/admin/components/agora-stt/common/hooks.ts

@@ -12,7 +12,7 @@ export const useMount = (callback?: () => void) => {
 }
 
 export const usePrevious = <T>(value: T): T | undefined => {
-  const ref = useRef<T>()
+  const ref = useRef<T | undefined>(undefined)
 
   useEffect(() => {
     ref.current = value

+ 217 - 142
src/client/admin/components/agora-stt/common/request.ts

@@ -1,44 +1,68 @@
 import { parseQuery } from "./utils"
 import { IRequestLanguages } from "../types"
+import { agoraClient } from "@/client/api"
 
 const MODE = import.meta.env.MODE
 let gatewayAddress = "https://api.agora.io"
-const BASE_URL = "https://service.agora.io/toolbox-overseas"
 
 // ---------------------------------------
-const appId = import.meta.env.VITE_AGORA_APP_ID
-const appCertificate = import.meta.env.VITE_AGORA_APP_CERTIFICATE
 const SUB_BOT_UID = "1000"
 const PUB_BOT_UID = "2000"
 
 let agoraToken = ""
 let genTokenTime = 0
+let agoraConfig: {
+  appId: string
+  sttJoinUrl: string
+  sttWsUrl: string
+  defaultChannel: string
+} | null = null
 
 export async function apiGetAgoraToken(config: { uid: string | number; channel: string }) {
-  if (!appCertificate) {
-    return null
+  try {
+    // 获取配置和Token
+    if (!agoraConfig) {
+      await fetchAgoraConfig()
+    }
+
+    const { uid, channel } = config
+    const response = await agoraClient.token.$get({
+      query: { type: 'rtc', channel, userId: uid.toString() }
+    })
+
+    if (!response.ok) {
+      throw new Error('Token获取失败')
+    }
+
+    const data = await response.json()
+    return data.token
+  } catch (error) {
+    console.error('获取Agora Token失败:', error)
+    throw error
   }
-  const { uid, channel } = config
-  const url = `${BASE_URL}/v2/token/generate`
-  const data = {
-    appId,
-    appCertificate,
-    channelName: channel,
-    expire: 7200,
-    src: "web",
-    types: [1, 2],
-    uid: uid + "",
+}
+
+export async function fetchAgoraConfig() {
+  try {
+    const response = await agoraClient.token.$get({
+      query: { type: 'rtc', channel: 'default', userId: '0' }
+    })
+
+    if (!response.ok) {
+      throw new Error('配置获取失败')
+    }
+
+    const data = await response.json()
+    agoraConfig = {
+      appId: data.appId,
+      sttJoinUrl: data.sttJoinUrl,
+      sttWsUrl: data.sttWsUrl,
+      defaultChannel: data.defaultChannel
+    }
+  } catch (error) {
+    console.error('获取Agora配置失败:', error)
+    throw error
   }
-  let resp = await fetch(url, {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-    },
-    body: JSON.stringify(data),
-  })
-  resp = (await resp.json()) || {}
-  // @ts-ignore
-  return resp?.data?.token || ""
 }
 
 const genAuthorization = async (config: { uid: string | number; channel: string }) => {
@@ -58,40 +82,50 @@ export const apiSTTAcquireToken = async (options: {
   channel: string
   uid: string | number
 }): Promise<any> => {
-  const { channel } = options
-  const data: any = {
-    instanceId: channel,
-  }
-  if (MODE == "test") {
-    data.testIp = "218.205.37.49"
-    data.testPort = 4447
-    const queryParams = parseQuery(window.location.href)
-    const denoise = queryParams?.denoise
-    if (denoise == "true") {
-      gatewayAddress = "https://service-staging.agora.io/speech-to-text-filter"
-      data.testIp = "183.131.160.168"
-    } else if (denoise == "false") {
-      gatewayAddress = "https://service-staging.agora.io/speech-to-text"
-      data.testIp = "114.236.138.39"
+  try {
+    // 获取配置
+    if (!agoraConfig) {
+      await fetchAgoraConfig()
     }
-  }
-  const url = `${gatewayAddress}/v1/projects/${appId}/rtsc/speech-to-text/builderTokens`
-  let res = await fetch(url, {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      Authorization: await genAuthorization(options),
-    },
-    body: JSON.stringify(data),
-  })
-  if (res.status == 200) {
-    res = await res.json()
-    return res
-  } else {
-    // status: 504
-    // please enable the realtime transcription service for this appid
-    console.error(res.status, res)
-    throw new Error(res.toString())
+
+    const { channel } = options
+    const data: any = {
+      instanceId: channel,
+    }
+    if (MODE == "test") {
+      data.testIp = "218.205.37.49"
+      data.testPort = 4447
+      const queryParams = parseQuery(window.location.href)
+      const denoise = queryParams?.denoise
+      if (denoise == "true") {
+        gatewayAddress = "https://service-staging.agora.io/speech-to-text-filter"
+        data.testIp = "183.131.160.168"
+      } else if (denoise == "false") {
+        gatewayAddress = "https://service-staging.agora.io/speech-to-text"
+        data.testIp = "114.236.138.39"
+      }
+    }
+    const url = `${gatewayAddress}/v1/projects/${agoraConfig!.appId}/rtsc/speech-to-text/builderTokens`
+    let res = await fetch(url, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: await genAuthorization(options),
+      },
+      body: JSON.stringify(data),
+    })
+    if (res.status == 200) {
+      res = await res.json()
+      return res
+    } else {
+      // status: 504
+      // please enable the realtime transcription service for this appid
+      console.error(res.status, res)
+      throw new Error(res.toString())
+    }
+  } catch (error) {
+    console.error('STT Token获取失败:', error)
+    throw error
   }
 }
 
@@ -101,12 +135,19 @@ export const apiSTTStartTranscription = async (options: {
   languages: IRequestLanguages[]
   token: string
 }): Promise<{ taskId: string }> => {
-  const { channel, languages, token, uid } = options
-  const url = `${gatewayAddress}/v1/projects/${appId}/rtsc/speech-to-text/tasks?builderToken=${token}`
-  let subBotToken = null
-  let pubBotToken = null
-  if (appCertificate) {
-    const data = await Promise.all([
+  try {
+    // 获取配置
+    if (!agoraConfig) {
+      await fetchAgoraConfig()
+    }
+
+    const { channel, languages, token, uid } = options
+    const url = `${gatewayAddress}/v1/projects/${agoraConfig!.appId}/rtsc/speech-to-text/tasks?builderToken=${token}`
+    let subBotToken = null
+    let pubBotToken = null
+
+    // 获取Bot Token
+    const tokenData = await Promise.all([
       apiGetAgoraToken({
         uid: SUB_BOT_UID,
         channel,
@@ -116,44 +157,48 @@ export const apiSTTStartTranscription = async (options: {
         channel,
       }),
     ])
-    subBotToken = data[0]
-    pubBotToken = data[1]
-  }
-  const body: any = {
-    languages: languages.map((item) => item.source),
-    maxIdleTime: 60,
-    rtcConfig: {
-      channelName: channel,
-      subBotUid: SUB_BOT_UID,
-      pubBotUid: PUB_BOT_UID,
-    },
-  }
-  if (subBotToken && pubBotToken) {
-    body.rtcConfig.subBotToken = subBotToken
-    body.rtcConfig.pubBotToken = pubBotToken
-  }
-  if (languages.find((item) => item.target.length)) {
-    body.translateConfig = {
-      forceTranslateInterval: 2,
-      languages: languages.filter((item) => item.target.length),
+    subBotToken = tokenData[0]
+    pubBotToken = tokenData[1]
+
+    const body: any = {
+      languages: languages.map((item) => item.source),
+      maxIdleTime: 60,
+      rtcConfig: {
+        channelName: channel,
+        subBotUid: SUB_BOT_UID,
+        pubBotUid: PUB_BOT_UID,
+      },
     }
+    if (subBotToken && pubBotToken) {
+      body.rtcConfig.subBotToken = subBotToken
+      body.rtcConfig.pubBotToken = pubBotToken
+    }
+    if (languages.find((item) => item.target.length)) {
+      body.translateConfig = {
+        forceTranslateInterval: 2,
+        languages: languages.filter((item) => item.target.length),
+      }
+    }
+    const res = await fetch(url, {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: await genAuthorization({
+          uid,
+          channel,
+        }),
+      },
+      body: JSON.stringify(body),
+    })
+    const responseData = await res.json()
+    if (res.status !== 200) {
+      throw new Error((responseData as any)?.message || "start transcription failed")
+    }
+    return responseData as { taskId: string }
+  } catch (error) {
+    console.error('STT转录启动失败:', error)
+    throw error
   }
-  const res = await fetch(url, {
-    method: "POST",
-    headers: {
-      "Content-Type": "application/json",
-      Authorization: await genAuthorization({
-        uid,
-        channel,
-      }),
-    },
-    body: JSON.stringify(body),
-  })
-  const data = await res.json()
-  if (res.status !== 200) {
-    throw new Error(data?.message || "start transcription failed")
-  }
-  return data
 }
 
 export const apiSTTStopTranscription = async (options: {
@@ -162,18 +207,28 @@ export const apiSTTStopTranscription = async (options: {
   uid: number | string
   channel: string
 }) => {
-  const { taskId, token, uid, channel } = options
-  const url = `${gatewayAddress}/v1/projects/${appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}`
-  await fetch(url, {
-    method: "DELETE",
-    headers: {
-      "Content-Type": "application/json",
-      Authorization: await genAuthorization({
-        uid,
-        channel,
-      }),
-    },
-  })
+  try {
+    // 获取配置
+    if (!agoraConfig) {
+      await fetchAgoraConfig()
+    }
+
+    const { taskId, token, uid, channel } = options
+    const url = `${gatewayAddress}/v1/projects/${agoraConfig!.appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}`
+    await fetch(url, {
+      method: "DELETE",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: await genAuthorization({
+          uid,
+          channel,
+        }),
+      },
+    })
+  } catch (error) {
+    console.error('STT转录停止失败:', error)
+    throw error
+  }
 }
 
 export const apiSTTQueryTranscription = async (options: {
@@ -182,19 +237,29 @@ export const apiSTTQueryTranscription = async (options: {
   uid: number | string
   channel: string
 }) => {
-  const { taskId, token, uid, channel } = options
-  const url = `${gatewayAddress}/v1/projects/${appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}`
-  const res = await fetch(url, {
-    method: "GET",
-    headers: {
-      "Content-Type": "application/json",
-      Authorization: await genAuthorization({
-        uid,
-        channel,
-      }),
-    },
-  })
-  return await res.json()
+  try {
+    // 获取配置
+    if (!agoraConfig) {
+      await fetchAgoraConfig()
+    }
+
+    const { taskId, token, uid, channel } = options
+    const url = `${gatewayAddress}/v1/projects/${agoraConfig!.appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}`
+    const res = await fetch(url, {
+      method: "GET",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: await genAuthorization({
+          uid,
+          channel,
+        }),
+      },
+    })
+    return await res.json()
+  } catch (error) {
+    console.error('STT转录查询失败:', error)
+    throw error
+  }
 }
 
 export const apiSTTUpdateTranscription = async (options: {
@@ -205,24 +270,34 @@ export const apiSTTUpdateTranscription = async (options: {
   updateMaskList: string[]
   data: any
 }) => {
-  const { taskId, token, uid, channel, data, updateMaskList } = options
-  const updateMask = updateMaskList.join(",")
-  const url = `${gatewayAddress}/v1/projects/${appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}&sequenceId=1&updateMask=${updateMask}`
-  const body: any = {
-    ...data,
+  try {
+    // 获取配置
+    if (!agoraConfig) {
+      await fetchAgoraConfig()
+    }
+
+    const { taskId, token, uid, channel, data, updateMaskList } = options
+    const updateMask = updateMaskList.join(",")
+    const url = `${gatewayAddress}/v1/projects/${agoraConfig!.appId}/rtsc/speech-to-text/tasks/${taskId}?builderToken=${token}&sequenceId=1&updateMask=${updateMask}`
+    const body: any = {
+      ...data,
+    }
+    const res = await fetch(url, {
+      method: "PATCH",
+      headers: {
+        "Content-Type": "application/json",
+        Authorization: await genAuthorization({
+          uid,
+          channel,
+        }),
+      },
+      body: JSON.stringify(body),
+    })
+    return await res.json()
+  } catch (error) {
+    console.error('STT转录更新失败:', error)
+    throw error
   }
-  const res = await fetch(url, {
-    method: "PATCH",
-    headers: {
-      "Content-Type": "application/json",
-      Authorization: await genAuthorization({
-        uid,
-        channel,
-      }),
-    },
-    body: JSON.stringify(body),
-  })
-  return await res.json()
 }
 
 // --------------- gpt ----------------

+ 16 - 4
src/client/admin/components/agora-stt/manager/rtc/rtc.ts

@@ -7,9 +7,7 @@ import AgoraRTC, {
 import { AGEventEmitter } from "../events"
 import { RtcEvents, IUserTracks } from "./types"
 import { parser } from "../parser"
-import { apiGetAgoraToken } from "../../common"
-
-const appId = import.meta.env.VITE_AGORA_APP_ID
+import { apiGetAgoraToken, fetchAgoraConfig } from "../../common"
 
 export class RtcManager extends AGEventEmitter<RtcEvents> {
   private _joined
@@ -27,8 +25,22 @@ export class RtcManager extends AGEventEmitter<RtcEvents> {
 
   async join({ channel, userId }: { channel: string; userId: number | string }) {
     if (!this._joined) {
+      // 获取配置
+      await fetchAgoraConfig()
       const token = await apiGetAgoraToken({ channel, uid: userId })
-      await this.client?.join(appId, channel, token, userId)
+
+      // 从配置中获取appId
+      const { agoraClient } = await import("@/client/api")
+      const response = await agoraClient.token.$get({
+        query: { type: 'rtc', channel, userId: userId.toString() }
+      })
+
+      if (!response.ok) {
+        throw new Error('配置获取失败')
+      }
+
+      const data = await response.json()
+      await this.client?.join(data.appId, channel, token, userId)
       this._joined = true
     }
   }

+ 19 - 4
src/client/admin/components/agora-stt/manager/rtm/rtm.ts

@@ -1,5 +1,5 @@
 import AgoraRTM, { ChannelType, RTMClient, RTMConfig, MetadataItem } from "agora-rtm-sdk"
-import { mapToArray, isString, apiGetAgoraToken, getDefaultLanguageSelect } from "../../common"
+import { mapToArray, isString, apiGetAgoraToken, getDefaultLanguageSelect, fetchAgoraConfig } from "../../common"
 import { AGEventEmitter } from "../events"
 import {
   RtmEvents,
@@ -14,7 +14,6 @@ import { DEFAULT_RTM_CONFIG } from "./constant"
 
 const { RTM } = AgoraRTM
 
-const appId = import.meta.env.VITE_AGORA_APP_ID
 const CHANNEL_TYPE: ChannelType = "MESSAGE"
 const LOCK_STT = "lock_stt"
 
@@ -38,12 +37,28 @@ export class RtmManager extends AGEventEmitter<RtmEvents> {
     this.userId = userId
     this.userName = userName
     this.channel = channel
+
+    // 获取配置
+    await fetchAgoraConfig()
+
+    // 从配置中获取appId
+    const { agoraClient } = await import("@/client/api")
+    const response = await agoraClient.token.$get({
+      query: { type: 'rtm', channel, userId }
+    })
+
+    if (!response.ok) {
+      throw new Error('配置获取失败')
+    }
+
+    const data = await response.json()
+
     if (!this.client) {
-      this.client = new RTM(appId, userId, this.rtmConfig)
+      this.client = new RTM(data.appId, userId, this.rtmConfig)
     }
     this._listenRtmEvents()
     const token = await apiGetAgoraToken({ channel: this.channel, uid: this.userId })
-    await this.client.login(token)
+    await this.client.login({ token })
     this.joined = true
     // subscribe message channel
     await this.client.subscribe(channel, {

+ 12 - 9
src/client/admin/pages/AgoraSTT.tsx

@@ -1,20 +1,23 @@
 import React from 'react';
 import { AgoraSTTComponent } from '../components/agora-stt/AgoraSTTComponent';
+import { AgoraSTTProvider } from '../components/agora-stt/AgoraSTTProvider';
 
 /**
  * Agora语音转文字功能页面
  */
 export const AgoraSTTPage: React.FC = () => {
   return (
-    <div className="space-y-6">
-      <div>
-        <h1 className="text-3xl font-bold tracking-tight">语音转文字</h1>
-        <p className="text-muted-foreground">
-          实时语音识别和转录功能,支持多种语言和实时结果展示
-        </p>
-      </div>
+    <AgoraSTTProvider>
+      <div className="space-y-6">
+        <div>
+          <h1 className="text-3xl font-bold tracking-tight">语音转文字</h1>
+          <p className="text-muted-foreground">
+            实时语音识别和转录功能,支持多种语言和实时结果展示
+          </p>
+        </div>
 
-      <AgoraSTTComponent />
-    </div>
+        <AgoraSTTComponent />
+      </div>
+    </AgoraSTTProvider>
   );
 };

+ 0 - 379
src/client/hooks/useAgoraSTT.ts

@@ -1,379 +0,0 @@
-import { useState, useCallback, useRef, useEffect } from 'react';
-import { AgoraSTTConfig, TranscriptionResult, AgoraSTTState, UseAgoraSTTResult } from '@/client/types/agora-stt';
-import { getAgoraConfig, isBrowserSupported, getBrowserSupportError } from '@/client/utils/agora-stt';
-import { agoraClient } from '@/client/api';
-
-export const useAgoraSTT = (): UseAgoraSTTResult => {
-  const [state, setState] = useState<AgoraSTTState>({
-    isConnected: false,
-    isRecording: false,
-    isTranscribing: false,
-    isConnecting: false,
-    error: null,
-    transcriptionResults: [],
-    currentTranscription: ''
-  });
-
-  const [microphonePermission, setMicrophonePermission] = useState<'granted' | 'denied' | 'prompt'>('prompt');
-
-  const wsConnection = useRef<WebSocket | null>(null);
-  const mediaRecorder = useRef<MediaRecorder | null>(null);
-  const audioChunks = useRef<Blob[]>([]);
-  const config = useRef<AgoraSTTConfig | null>(null);
-  const currentToken = useRef<string | null>(null);
-
-  const updateState = useCallback((updates: Partial<AgoraSTTState>) => {
-    setState(prev => ({ ...prev, ...updates }));
-  }, []);
-
-  const setError = useCallback((error: string | null) => {
-    updateState({ error });
-  }, [updateState]);
-
-  const fetchAgoraConfigAndToken = useCallback(async (type: 'rtc' | 'rtm', channel?: string, userId?: string): Promise<{ token: string; config: any }> => {
-    try {
-      const query: any = { type };
-
-      if (type === 'rtc' && channel) {
-        query.channel = channel;
-      } else if (type === 'rtm' && userId) {
-        query.userId = userId;
-      }
-
-      const response = await agoraClient.token.$get({ query });
-
-      if (!response.ok) {
-        throw new Error(`Token API returned ${response.status}: ${response.statusText}`);
-      }
-
-      const data = await response.json();
-
-      if (!data.token) {
-        throw new Error('Invalid token response format');
-      }
-
-      // 现在API返回包含Token和配置常量的完整响应
-      return {
-        token: data.token,
-        config: {
-          appId: data.appId,
-          sttJoinUrl: data.sttJoinUrl,
-          sttWsUrl: data.sttWsUrl,
-          defaultChannel: data.defaultChannel
-        }
-      };
-    } catch (error) {
-      throw new Error(`Failed to fetch dynamic token and config: ${error instanceof Error ? error.message : 'Unknown error'}`);
-    }
-  }, []);
-
-  const initializeConfig = useCallback((): boolean => {
-    try {
-      if (!isBrowserSupported()) {
-        setError(getBrowserSupportError());
-        return false;
-      }
-
-      // 现在配置通过Token API获取,不再需要验证本地配置
-      // 使用默认配置作为占位符
-      config.current = getAgoraConfig();
-      return true;
-    } catch (error) {
-      setError('Failed to initialize Agora configuration');
-      return false;
-    }
-  }, [setError]);
-
-  const joinChannel = useCallback(async (): Promise<void> => {
-    if (!initializeConfig()) {
-      return;
-    }
-
-    try {
-      updateState({ error: null, isConnecting: true });
-
-      // 统一获取Token和配置常量
-      const { token, config: dynamicConfig } = await fetchAgoraConfigAndToken('rtc', config.current!.channel);
-      currentToken.current = token;
-
-      // 更新配置为API返回的配置常量
-      config.current = {
-        ...config.current,
-        appId: dynamicConfig.appId,
-        sttJoinUrl: dynamicConfig.sttJoinUrl,
-        sttWsUrl: dynamicConfig.sttWsUrl,
-        channel: dynamicConfig.defaultChannel,
-        primaryCert: config.current?.primaryCert || ''
-      };
-
-      // 模拟Agora STT加入频道API调用
-      const joinResponse = await fetch(config.current!.sttJoinUrl, {
-        method: 'POST',
-        headers: {
-          'Content-Type': 'application/json',
-          'Authorization': `Bearer ${token}`
-        },
-        body: JSON.stringify({
-          appId: config.current!.appId,
-          channel: config.current!.channel,
-          uid: Date.now().toString()
-        })
-      });
-
-      if (!joinResponse.ok) {
-        throw new Error(`Failed to join channel: ${joinResponse.status}`);
-      }
-
-      // 建立WebSocket连接
-      const ws = new WebSocket(config.current!.sttWsUrl);
-
-      ws.onopen = () => {
-        updateState({
-          isConnected: true,
-          isConnecting: false,
-          error: null
-        });
-      };
-
-      ws.onmessage = (event) => {
-        try {
-          const data = JSON.parse(event.data);
-          if (data.type === 'transcription') {
-            const newResult = {
-              text: data.text,
-              isFinal: data.isFinal,
-              timestamp: Date.now(),
-              confidence: data.confidence
-            };
-
-            setState(prev => ({
-              ...prev,
-              transcriptionResults: [...prev.transcriptionResults, newResult],
-              currentTranscription: data.isFinal ? '' : data.text
-            }));
-
-            if (data.isFinal) {
-              // 可以在这里触发回调
-              console.log('Final transcription:', data.text);
-            }
-          }
-        } catch (error) {
-          console.error('Failed to parse WebSocket message:', error);
-        }
-      };
-
-      ws.onerror = (error) => {
-        setError('WebSocket connection error');
-        console.error('WebSocket error:', error);
-      };
-
-      ws.onclose = () => {
-        updateState({
-          isConnected: false,
-          isRecording: false
-        });
-      };
-
-      wsConnection.current = ws;
-
-    } catch (error) {
-      setError('Failed to join Agora channel: ' + (error instanceof Error ? error.message : 'Unknown error'));
-      updateState({ isConnecting: false });
-    }
-  }, [initializeConfig, updateState, setError, fetchAgoraConfigAndToken]);
-
-  const leaveChannel = useCallback((): void => {
-    if (wsConnection.current) {
-      // 发送离开频道消息
-      try {
-        wsConnection.current.send(JSON.stringify({
-          type: 'leave',
-          channel: config.current?.channel
-        }));
-      } catch (error) {
-        console.error('Failed to send leave message:', error);
-      }
-
-      wsConnection.current.close();
-      wsConnection.current = null;
-    }
-
-    if (mediaRecorder.current && state.isRecording) {
-      mediaRecorder.current.stop();
-      mediaRecorder.current.stream.getTracks().forEach(track => track.stop());
-      mediaRecorder.current = null;
-    }
-
-    updateState({
-      isConnected: false,
-      isRecording: false,
-      isTranscribing: false,
-      currentTranscription: ''
-    });
-  }, [state.isRecording, updateState]);
-
-  const checkMicrophonePermission = useCallback(async (): Promise<boolean> => {
-    try {
-      const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName });
-      setMicrophonePermission(permissionStatus.state as 'granted' | 'denied' | 'prompt');
-
-      if (permissionStatus.state === 'denied') {
-        setError('麦克风权限已被拒绝,请在浏览器设置中启用');
-        return false;
-      }
-      return true;
-    } catch (error) {
-      // 某些浏览器不支持 permissions API
-      console.warn('Microphone permission API not supported');
-      return true;
-    }
-  }, [setError]);
-
-  const requestMicrophonePermission = useCallback(async (): Promise<boolean> => {
-    try {
-      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
-      stream.getTracks().forEach(track => track.stop());
-      setMicrophonePermission('granted');
-      return true;
-    } catch (error) {
-      setMicrophonePermission('denied');
-      setError('麦克风权限请求被拒绝,请允许访问麦克风');
-      return false;
-    }
-  }, [setError]);
-
-  const startRecording = useCallback(async (): Promise<void> => {
-    if (!state.isConnected) {
-      setError('Not connected to Agora channel');
-      return;
-    }
-
-    // 检查麦克风权限
-    if (!(await checkMicrophonePermission())) {
-      return;
-    }
-
-    // 如果权限是prompt状态,请求权限
-    if (microphonePermission === 'prompt') {
-      if (!(await requestMicrophonePermission())) {
-        return;
-      }
-    }
-
-    try {
-      const stream = await navigator.mediaDevices.getUserMedia({
-        audio: {
-          echoCancellation: true,
-          noiseSuppression: true,
-          sampleRate: 16000
-        }
-      });
-
-      const recorder = new MediaRecorder(stream, {
-        mimeType: 'audio/webm;codecs=opus'
-      });
-
-      audioChunks.current = [];
-
-      recorder.ondataavailable = async (event) => {
-        if (event.data.size > 0 && wsConnection.current?.readyState === WebSocket.OPEN) {
-          try {
-            // 将音频数据转换为Base64并发送
-            const arrayBuffer = await event.data.arrayBuffer();
-            const base64Audio = btoa(
-              new Uint8Array(arrayBuffer).reduce(
-                (data, byte) => data + String.fromCharCode(byte),
-                ''
-              )
-            );
-
-            wsConnection.current.send(JSON.stringify({
-              type: 'audio_data',
-              audio: base64Audio,
-              timestamp: Date.now()
-            }));
-          } catch (error) {
-            console.error('Failed to send audio data:', error);
-          }
-        }
-      };
-
-      recorder.onstop = () => {
-        // 发送录音结束信号
-        if (wsConnection.current?.readyState === WebSocket.OPEN) {
-          wsConnection.current.send(JSON.stringify({
-            type: 'recording_stopped'
-          }));
-        }
-      };
-
-      mediaRecorder.current = recorder;
-      recorder.start(500); // 每500ms发送一次数据
-
-      updateState({
-        isRecording: true,
-        error: null
-      });
-
-      // 通知服务器开始录音
-      if (wsConnection.current?.readyState === WebSocket.OPEN) {
-        wsConnection.current.send(JSON.stringify({
-          type: 'recording_started'
-        }));
-      }
-    } catch (error) {
-      setError('Failed to start recording: ' + (error instanceof Error ? error.message : 'Unknown error'));
-    }
-  }, [state.isConnected, setError, updateState]);
-
-  const stopRecording = useCallback((): void => {
-    if (mediaRecorder.current && state.isRecording) {
-      mediaRecorder.current.stop();
-      mediaRecorder.current.stream.getTracks().forEach(track => track.stop());
-      updateState({ isRecording: false });
-    }
-  }, [state.isRecording, updateState]);
-
-  const clearTranscriptions = useCallback((): void => {
-    updateState({
-      transcriptionResults: [],
-      currentTranscription: ''
-    });
-  }, [updateState]);
-
-  // 模拟接收转录结果
-  useEffect(() => {
-    if (state.isRecording && state.isConnected) {
-      const interval = setInterval(() => {
-        if (Math.random() > 0.7) {
-          const newResult: TranscriptionResult = {
-            text: `模拟转录文本 ${Date.now()}`,
-            isFinal: Math.random() > 0.8,
-            timestamp: Date.now(),
-            confidence: Math.random() * 0.5 + 0.5
-          };
-
-          setState(prev => ({
-            ...prev,
-            transcriptionResults: [...prev.transcriptionResults, newResult],
-            currentTranscription: newResult.isFinal ? '' : newResult.text
-          }));
-        }
-      }, 2000);
-
-      return () => clearInterval(interval);
-    }
-  }, [state.isRecording, state.isConnected]);
-
-  return {
-    state: {
-      ...state,
-      microphonePermission
-    },
-    joinChannel,
-    leaveChannel,
-    startRecording,
-    stopRecording,
-    clearTranscriptions
-  };
-};