Просмотр исходного кода

✨ feat(agora-stt): 集成 Agora RTC 和 RTM SDK 实现语音转文字功能

- 添加 agora-rtc-sdk-ng 和 agora-rtm-sdk 依赖包
- 实现 AgoraSTTProvider 上下文管理器,统一管理 RTC、RTM 和 STT 实例
- 重构 AgoraSTTComponent 组件,支持多语言选择和权限管理
- 新增 useAgoraSTTManager hook 封装业务逻辑状态管理
- 完善类型定义和测试用例,提升代码健壮性

📦 build(deps): 更新依赖配置

- 在 package.json 中添加 agora SDK 相关依赖
- 更新 pnpm-lock.yaml 锁定依赖版本
- 在 .gitignore 中排除 Agora 演示项目文件

♻️ refactor(agora-stt): 重构项目结构和代码组织

- 将类型定义移至本地 types 目录,减少外部依赖
- 重构工具函数和常量定义,提高模块内聚性
- 优化错误处理机制,使用 toast 替代 message 组件
- 改进组件测试用例,模拟新的管理器架构
yourname 4 месяцев назад
Родитель
Сommit
2d4021acfd

+ 1 - 0
.gitignore

@@ -49,3 +49,4 @@ coverage/
 backups/
 backups/
 scripts/time_logger.sh
 scripts/time_logger.sh
 loop.txt
 loop.txt
+Agora-RTT-Demo-main

+ 3 - 0
package.json

@@ -68,6 +68,8 @@
     "@radix-ui/react-tooltip": "^1.2.7",
     "@radix-ui/react-tooltip": "^1.2.7",
     "@tanstack/react-query": "^5.83.0",
     "@tanstack/react-query": "^5.83.0",
     "@types/node-cron": "^3.0.11",
     "@types/node-cron": "^3.0.11",
+    "agora-rtc-sdk-ng": "^4.24.0",
+    "agora-rtm-sdk": "^2.2.2",
     "agora-token": "^2.0.5",
     "agora-token": "^2.0.5",
     "axios": "^1.11.0",
     "axios": "^1.11.0",
     "bcrypt": "^6.0.0",
     "bcrypt": "^6.0.0",
@@ -91,6 +93,7 @@
     "node-cron": "^4.2.1",
     "node-cron": "^4.2.1",
     "pg": "^8.16.3",
     "pg": "^8.16.3",
     "pg-dump-restore": "1.0.13",
     "pg-dump-restore": "1.0.13",
+    "protobufjs": "^7.5.4",
     "rc-upload": "^4.9.2",
     "rc-upload": "^4.9.2",
     "react": "^19.1.0",
     "react": "^19.1.0",
     "react-day-picker": "^9.8.1",
     "react-day-picker": "^9.8.1",

+ 200 - 2
pnpm-lock.yaml

@@ -113,6 +113,12 @@ importers:
       '@types/node-cron':
       '@types/node-cron':
         specifier: ^3.0.11
         specifier: ^3.0.11
         version: 3.0.11
         version: 3.0.11
+      agora-rtc-sdk-ng:
+        specifier: ^4.24.0
+        version: 4.24.0(debug@4.4.1)
+      agora-rtm-sdk:
+        specifier: ^2.2.2
+        version: 2.2.2(agora-rtc-sdk-ng@4.24.0(debug@4.4.1))
       agora-token:
       agora-token:
         specifier: ^2.0.5
         specifier: ^2.0.5
         version: 2.0.5
         version: 2.0.5
@@ -182,6 +188,9 @@ importers:
       pg-dump-restore:
       pg-dump-restore:
         specifier: 1.0.13
         specifier: 1.0.13
         version: 1.0.13
         version: 1.0.13
+      protobufjs:
+        specifier: ^7.5.4
+        version: 7.5.4
       rc-upload:
       rc-upload:
         specifier: ^4.9.2
         specifier: ^4.9.2
         version: 4.9.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
         version: 4.9.2(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
@@ -333,6 +342,15 @@ packages:
   '@adobe/css-tools@4.4.4':
   '@adobe/css-tools@4.4.4':
     resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
     resolution: {integrity: sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==}
 
 
+  '@agora-js/media@4.24.0':
+    resolution: {integrity: sha512-foii2klr5+qonLznxN0ZZFejoxLt/W8do79wmIsADPZLw2uZjRP35m0lqUGiLXBKeQ8u3i4UygPzEdFaY26hrw==}
+
+  '@agora-js/report@4.24.0':
+    resolution: {integrity: sha512-MYbtkdY1Ls0KW0iagUzrPzyvqMWlyCWSC5odEb1SQaraAl7DJeDUkf91a3wxKzrjVah+LCxFxsS4lCFDxvKgNA==}
+
+  '@agora-js/shared@4.24.0':
+    resolution: {integrity: sha512-Vj67ZcTHZI+1ctWusrEPSSGLM3l6CFiAze/Bi8r7YHRMLivzhZR79nV6GiKvHS3muLAON2YAExznvjPIly6lcg==}
+
   '@ampproject/remapping@2.3.0':
   '@ampproject/remapping@2.3.0':
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==}
     engines: {node: '>=6.0.0'}
     engines: {node: '>=6.0.0'}
@@ -713,6 +731,36 @@ packages:
   '@polka/url@1.0.0-next.29':
   '@polka/url@1.0.0-next.29':
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
     resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
 
 
+  '@protobufjs/aspromise@1.1.2':
+    resolution: {integrity: sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==}
+
+  '@protobufjs/base64@1.1.2':
+    resolution: {integrity: sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==}
+
+  '@protobufjs/codegen@2.0.4':
+    resolution: {integrity: sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==}
+
+  '@protobufjs/eventemitter@1.1.0':
+    resolution: {integrity: sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==}
+
+  '@protobufjs/fetch@1.1.0':
+    resolution: {integrity: sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==}
+
+  '@protobufjs/float@1.0.2':
+    resolution: {integrity: sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==}
+
+  '@protobufjs/inquire@1.1.0':
+    resolution: {integrity: sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==}
+
+  '@protobufjs/path@1.1.2':
+    resolution: {integrity: sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==}
+
+  '@protobufjs/pool@1.1.0':
+    resolution: {integrity: sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==}
+
+  '@protobufjs/utf8@1.1.0':
+    resolution: {integrity: sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==}
+
   '@radix-ui/number@1.1.1':
   '@radix-ui/number@1.1.1':
     resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
     resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
 
 
@@ -1850,6 +1898,17 @@ packages:
     engines: {node: '>=0.4.0'}
     engines: {node: '>=0.4.0'}
     hasBin: true
     hasBin: true
 
 
+  agora-rtc-sdk-ng@4.24.0:
+    resolution: {integrity: sha512-2apG/07EtsuX21ncSF77q+dr6/kDgu9B/RpKtstCtaq46l4/Eraoecewi4zXRUCY3Im+8dzTIXx6jUwyPdxdHQ==}
+
+  agora-rte-extension@1.2.4:
+    resolution: {integrity: sha512-0ovZz1lbe30QraG1cU+ji7EnQ8aUu+Hf3F+a8xPml3wPOyUQEK6CTdxV9kMecr9t+fIDrGeW7wgJTsM1DQE7Nw==}
+
+  agora-rtm-sdk@2.2.2:
+    resolution: {integrity: sha512-A1vT/JmX4le70SO0QVgqBic6FAvR4RbdswXa8r+nea6Sd0wWGYg9HqA3poBve9mjRyK56AaOL38Z4vxZLRBcFA==}
+    peerDependencies:
+      agora-rtc-sdk-ng: 4.23.0
+
   agora-token@2.0.5:
   agora-token@2.0.5:
     resolution: {integrity: sha512-0NcbzC3iuutlksv3b4bCMKHrW3pko6gdiGEMRo6APDice24kfXAuWyAlG9hRBrrPBVDShwm9/GUz2Scd3zuZQw==}
     resolution: {integrity: sha512-0NcbzC3iuutlksv3b4bCMKHrW3pko6gdiGEMRo6APDice24kfXAuWyAlG9hRBrrPBVDShwm9/GUz2Scd3zuZQw==}
 
 
@@ -2448,6 +2507,10 @@ packages:
       picomatch:
       picomatch:
         optional: true
         optional: true
 
 
+  fetch-blob@3.2.0:
+    resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==}
+    engines: {node: ^12.20 || >= 14.13}
+
   file-entry-cache@8.0.0:
   file-entry-cache@8.0.0:
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==}
     engines: {node: '>=16.0.0'}
     engines: {node: '>=16.0.0'}
@@ -2496,6 +2559,10 @@ packages:
     resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
     resolution: {integrity: sha512-8e1++BCiTzUno9v5IZ2J6bv4RU+3UKDmqWUQD0MIMVCd9AdhWkO1gw57oo1mNEX1dMq2EGI+FbWz4B92pscSQg==}
     engines: {node: '>= 18'}
     engines: {node: '>= 18'}
 
 
+  formdata-polyfill@4.0.10:
+    resolution: {integrity: sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==}
+    engines: {node: '>=12.20.0'}
+
   fsevents@2.3.2:
   fsevents@2.3.2:
     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
     engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
@@ -3119,6 +3186,11 @@ packages:
     resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
     resolution: {integrity: sha512-lgimEHPE/QDgFlywTd8yTR61ptugX3Qer29efeyWw2rv259HtGBNn1vZVmp8lB9uo9wC0t/AT4iGqXxia+CJFg==}
     engines: {node: '>=6.0.0'}
     engines: {node: '>=6.0.0'}
 
 
+  node-domexception@1.0.0:
+    resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
+    engines: {node: '>=10.5.0'}
+    deprecated: Use your platform's native DOMException instead
+
   node-gyp-build@4.8.4:
   node-gyp-build@4.8.4:
     resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
     resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==}
     hasBin: true
     hasBin: true
@@ -3185,6 +3257,9 @@ packages:
   package-json-from-dist@1.0.1:
   package-json-from-dist@1.0.1:
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
     resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
 
 
+  pako@2.1.0:
+    resolution: {integrity: sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug==}
+
   parent-module@1.0.1:
   parent-module@1.0.1:
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
     engines: {node: '>=6'}
     engines: {node: '>=6'}
@@ -3304,6 +3379,10 @@ packages:
   prop-types@15.8.1:
   prop-types@15.8.1:
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
     resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==}
 
 
+  protobufjs@7.5.4:
+    resolution: {integrity: sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==}
+    engines: {node: '>=12.0.0'}
+
   proxy-from-env@1.1.0:
   proxy-from-env@1.1.0:
     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
     resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
 
 
@@ -3519,6 +3598,9 @@ packages:
   scheduler@0.26.0:
   scheduler@0.26.0:
     resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
     resolution: {integrity: sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==}
 
 
+  sdp@3.2.1:
+    resolution: {integrity: sha512-lwsAIzOPlH8/7IIjjz3K0zYBk7aBVVcvjMwt3M4fLxpjMYyy7i3I97SLHebgn4YBjirkzfp3RvRDWSKsh/+WFw==}
+
   semver@6.3.1:
   semver@6.3.1:
     resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
     resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==}
     hasBin: true
     hasBin: true
@@ -3861,6 +3943,10 @@ packages:
     engines: {node: '>=14.17'}
     engines: {node: '>=14.17'}
     hasBin: true
     hasBin: true
 
 
+  ua-parser-js@0.7.41:
+    resolution: {integrity: sha512-O3oYyCMPYgNNHuO7Jjk3uacJWZF8loBgwrfd/5LE/HyZ3lUIOdniQ7DNXJcIgZbwioZxk0fLfI4EVnetdiX5jg==}
+    hasBin: true
+
   unbox-primitive@1.1.0:
   unbox-primitive@1.1.0:
     resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
     resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==}
     engines: {node: '>= 0.4'}
     engines: {node: '>= 0.4'}
@@ -4008,6 +4094,14 @@ packages:
   web-encoding@1.1.5:
   web-encoding@1.1.5:
     resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
     resolution: {integrity: sha512-HYLeVCdJ0+lBYV2FvNZmv3HJ2Nt0QYXqZojk3d9FJOLkwnuhzM9tmamh8d7HPM8QqjKH8DeHkFTx+CFlWpZZDA==}
 
 
+  web-streams-polyfill@3.3.3:
+    resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==}
+    engines: {node: '>= 8'}
+
+  webrtc-adapter@8.2.0:
+    resolution: {integrity: sha512-umxCMgedPAVq4Pe/jl3xmelLXLn4XZWFEMR5Iipb5wJ+k1xMX0yC4ZY9CueZUU1MjapFxai1tFGE7R/kotH6Ww==}
+    engines: {node: '>=6.0.0', npm: '>=3.10.0'}
+
   whatwg-mimetype@3.0.0:
   whatwg-mimetype@3.0.0:
     resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
     resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==}
     engines: {node: '>=12'}
     engines: {node: '>=12'}
@@ -4094,6 +4188,30 @@ snapshots:
 
 
   '@adobe/css-tools@4.4.4': {}
   '@adobe/css-tools@4.4.4': {}
 
 
+  '@agora-js/media@4.24.0(debug@4.4.1)':
+    dependencies:
+      '@agora-js/report': 4.24.0(debug@4.4.1)
+      '@agora-js/shared': 4.24.0(debug@4.4.1)
+      agora-rte-extension: 1.2.4
+      axios: 1.11.0(debug@4.4.1)
+      webrtc-adapter: 8.2.0
+    transitivePeerDependencies:
+      - debug
+
+  '@agora-js/report@4.24.0(debug@4.4.1)':
+    dependencies:
+      '@agora-js/shared': 4.24.0(debug@4.4.1)
+      axios: 1.11.0(debug@4.4.1)
+    transitivePeerDependencies:
+      - debug
+
+  '@agora-js/shared@4.24.0(debug@4.4.1)':
+    dependencies:
+      axios: 1.11.0(debug@4.4.1)
+      ua-parser-js: 0.7.41
+    transitivePeerDependencies:
+      - debug
+
   '@ampproject/remapping@2.3.0':
   '@ampproject/remapping@2.3.0':
     dependencies:
     dependencies:
       '@jridgewell/gen-mapping': 0.3.12
       '@jridgewell/gen-mapping': 0.3.12
@@ -4389,6 +4507,29 @@ snapshots:
 
 
   '@polka/url@1.0.0-next.29': {}
   '@polka/url@1.0.0-next.29': {}
 
 
+  '@protobufjs/aspromise@1.1.2': {}
+
+  '@protobufjs/base64@1.1.2': {}
+
+  '@protobufjs/codegen@2.0.4': {}
+
+  '@protobufjs/eventemitter@1.1.0': {}
+
+  '@protobufjs/fetch@1.1.0':
+    dependencies:
+      '@protobufjs/aspromise': 1.1.2
+      '@protobufjs/inquire': 1.1.0
+
+  '@protobufjs/float@1.0.2': {}
+
+  '@protobufjs/inquire@1.1.0': {}
+
+  '@protobufjs/path@1.1.2': {}
+
+  '@protobufjs/pool@1.1.0': {}
+
+  '@protobufjs/utf8@1.1.0': {}
+
   '@radix-ui/number@1.1.1': {}
   '@radix-ui/number@1.1.1': {}
 
 
   '@radix-ui/primitive@1.1.2': {}
   '@radix-ui/primitive@1.1.2': {}
@@ -5521,6 +5662,26 @@ snapshots:
 
 
   acorn@8.15.0: {}
   acorn@8.15.0: {}
 
 
+  agora-rtc-sdk-ng@4.24.0(debug@4.4.1):
+    dependencies:
+      '@agora-js/media': 4.24.0(debug@4.4.1)
+      '@agora-js/report': 4.24.0(debug@4.4.1)
+      '@agora-js/shared': 4.24.0(debug@4.4.1)
+      agora-rte-extension: 1.2.4
+      axios: 1.11.0(debug@4.4.1)
+      formdata-polyfill: 4.0.10
+      pako: 2.1.0
+      ua-parser-js: 0.7.41
+      webrtc-adapter: 8.2.0
+    transitivePeerDependencies:
+      - debug
+
+  agora-rte-extension@1.2.4: {}
+
+  agora-rtm-sdk@2.2.2(agora-rtc-sdk-ng@4.24.0(debug@4.4.1)):
+    dependencies:
+      agora-rtc-sdk-ng: 4.24.0(debug@4.4.1)
+
   agora-token@2.0.5:
   agora-token@2.0.5:
     dependencies:
     dependencies:
       crc-32: 1.2.2
       crc-32: 1.2.2
@@ -6240,6 +6401,11 @@ snapshots:
     optionalDependencies:
     optionalDependencies:
       picomatch: 4.0.3
       picomatch: 4.0.3
 
 
+  fetch-blob@3.2.0:
+    dependencies:
+      node-domexception: 1.0.0
+      web-streams-polyfill: 3.3.3
+
   file-entry-cache@8.0.0:
   file-entry-cache@8.0.0:
     dependencies:
     dependencies:
       flat-cache: 4.0.1
       flat-cache: 4.0.1
@@ -6285,6 +6451,10 @@ snapshots:
 
 
   formdata-node@6.0.3: {}
   formdata-node@6.0.3: {}
 
 
+  formdata-polyfill@4.0.10:
+    dependencies:
+      fetch-blob: 3.2.0
+
   fsevents@2.3.2:
   fsevents@2.3.2:
     optional: true
     optional: true
 
 
@@ -6750,8 +6920,7 @@ snapshots:
 
 
   lodash@4.17.21: {}
   lodash@4.17.21: {}
 
 
-  long@5.3.2:
-    optional: true
+  long@5.3.2: {}
 
 
   loose-envify@1.4.0:
   loose-envify@1.4.0:
     dependencies:
     dependencies:
@@ -6888,6 +7057,8 @@ snapshots:
 
 
   node-cron@4.2.1: {}
   node-cron@4.2.1: {}
 
 
+  node-domexception@1.0.0: {}
+
   node-gyp-build@4.8.4: {}
   node-gyp-build@4.8.4: {}
 
 
   npm-run-path@4.0.1:
   npm-run-path@4.0.1:
@@ -6965,6 +7136,8 @@ snapshots:
 
 
   package-json-from-dist@1.0.1: {}
   package-json-from-dist@1.0.1: {}
 
 
+  pako@2.1.0: {}
+
   parent-module@1.0.1:
   parent-module@1.0.1:
     dependencies:
     dependencies:
       callsites: 3.1.0
       callsites: 3.1.0
@@ -7069,6 +7242,21 @@ snapshots:
       object-assign: 4.1.1
       object-assign: 4.1.1
       react-is: 16.13.1
       react-is: 16.13.1
 
 
+  protobufjs@7.5.4:
+    dependencies:
+      '@protobufjs/aspromise': 1.1.2
+      '@protobufjs/base64': 1.1.2
+      '@protobufjs/codegen': 2.0.4
+      '@protobufjs/eventemitter': 1.1.0
+      '@protobufjs/fetch': 1.1.0
+      '@protobufjs/float': 1.0.2
+      '@protobufjs/inquire': 1.1.0
+      '@protobufjs/path': 1.1.2
+      '@protobufjs/pool': 1.1.0
+      '@protobufjs/utf8': 1.1.0
+      '@types/node': 24.1.0
+      long: 5.3.2
+
   proxy-from-env@1.1.0: {}
   proxy-from-env@1.1.0: {}
 
 
   punycode@2.3.1: {}
   punycode@2.3.1: {}
@@ -7318,6 +7506,8 @@ snapshots:
 
 
   scheduler@0.26.0: {}
   scheduler@0.26.0: {}
 
 
+  sdp@3.2.1: {}
+
   semver@6.3.1: {}
   semver@6.3.1: {}
 
 
   semver@7.7.2: {}
   semver@7.7.2: {}
@@ -7657,6 +7847,8 @@ snapshots:
 
 
   typescript@5.8.3: {}
   typescript@5.8.3: {}
 
 
+  ua-parser-js@0.7.41: {}
+
   unbox-primitive@1.1.0:
   unbox-primitive@1.1.0:
     dependencies:
     dependencies:
       call-bound: 1.0.4
       call-bound: 1.0.4
@@ -7826,6 +8018,12 @@ snapshots:
     optionalDependencies:
     optionalDependencies:
       '@zxing/text-encoding': 0.9.0
       '@zxing/text-encoding': 0.9.0
 
 
+  web-streams-polyfill@3.3.3: {}
+
+  webrtc-adapter@8.2.0:
+    dependencies:
+      sdp: 3.2.1
+
   whatwg-mimetype@3.0.0: {}
   whatwg-mimetype@3.0.0: {}
 
 
   which-boxed-primitive@1.1.1:
   which-boxed-primitive@1.1.1:

+ 37 - 5
src/client/admin/components/agora-stt/AgoraSTTComponent.tsx

@@ -1,10 +1,12 @@
-import React from 'react';
-import { useAgoraSTT } from '@/client/hooks/useAgoraSTT';
+import React, { useState } from 'react';
+import { useAgoraSTTManager } from './hooks/useAgoraSTTManager';
 import { Button } from '@/client/components/ui/button';
 import { Button } from '@/client/components/ui/button';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
 import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/client/components/ui/card';
 import { Badge } from '@/client/components/ui/badge';
 import { Badge } from '@/client/components/ui/badge';
 import { Alert, AlertDescription } from '@/client/components/ui/alert';
 import { Alert, AlertDescription } from '@/client/components/ui/alert';
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/client/components/ui/select';
 import { Mic, MicOff, Play, Square, Trash2, Wifi, WifiOff } from 'lucide-react';
 import { Mic, MicOff, Play, Square, Trash2, Wifi, WifiOff } from 'lucide-react';
+import { LANGUAGE_OPTIONS } from './common/constant';
 
 
 interface AgoraSTTComponentProps {
 interface AgoraSTTComponentProps {
   className?: string;
   className?: string;
@@ -14,6 +16,8 @@ interface AgoraSTTComponentProps {
 export const AgoraSTTComponent: React.FC<AgoraSTTComponentProps> = ({
 export const AgoraSTTComponent: React.FC<AgoraSTTComponentProps> = ({
   className = ''
   className = ''
 }) => {
 }) => {
+  const [selectedLanguages, setSelectedLanguages] = useState([LANGUAGE_OPTIONS[0]]);
+
   const {
   const {
     state,
     state,
     joinChannel,
     joinChannel,
@@ -21,11 +25,14 @@ export const AgoraSTTComponent: React.FC<AgoraSTTComponentProps> = ({
     startRecording,
     startRecording,
     stopRecording,
     stopRecording,
     clearTranscriptions
     clearTranscriptions
-  } = useAgoraSTT();
+  } = useAgoraSTTManager();
 
 
   const handleJoinChannel = async () => {
   const handleJoinChannel = async () => {
     try {
     try {
-      await joinChannel();
+      const userId = Date.now().toString();
+      const channel = 'default-channel';
+      const userName = 'User';
+      await joinChannel(userId, channel, userName);
     } catch (_error) {
     } catch (_error) {
       // Failed to join channel
       // Failed to join channel
     }
     }
@@ -33,7 +40,7 @@ export const AgoraSTTComponent: React.FC<AgoraSTTComponentProps> = ({
 
 
   const handleStartRecording = async () => {
   const handleStartRecording = async () => {
     try {
     try {
-      await startRecording();
+      await startRecording(selectedLanguages);
     } catch (_error) {
     } catch (_error) {
       // Failed to start recording
       // Failed to start recording
     }
     }
@@ -109,6 +116,31 @@ export const AgoraSTTComponent: React.FC<AgoraSTTComponentProps> = ({
           </Alert>
           </Alert>
         )}
         )}
 
 
+        {/* 语言选择器 */}
+        {!state.isConnected && (
+          <div className="flex flex-col gap-2">
+            <label className="text-sm font-medium">选择识别语言:</label>
+            <Select
+              value={selectedLanguages[0]?.value}
+              onValueChange={(value) => {
+                const lang = LANGUAGE_OPTIONS.find(l => l.value === value);
+                if (lang) setSelectedLanguages([lang]);
+              }}
+            >
+              <SelectTrigger className="w-full">
+                <SelectValue placeholder="选择语言" />
+              </SelectTrigger>
+              <SelectContent>
+                {LANGUAGE_OPTIONS.map((lang) => (
+                  <SelectItem key={lang.value} value={lang.value}>
+                    {lang.label}
+                  </SelectItem>
+                ))}
+              </SelectContent>
+            </Select>
+          </div>
+        )}
+
         {/* 控制按钮 */}
         {/* 控制按钮 */}
         <div className="flex flex-col sm:flex-row flex-wrap gap-2">
         <div className="flex flex-col sm:flex-row flex-wrap gap-2">
           {!state.isConnected ? (
           {!state.isConnected ? (

+ 68 - 0
src/client/admin/components/agora-stt/AgoraSTTProvider.tsx

@@ -0,0 +1,68 @@
+import React, { createContext, useContext, useRef, useEffect } from 'react'
+import { RtcManager } from './manager/rtc'
+import { RtmManager } from './manager/rtm'
+import { SttManager } from './manager/stt'
+import { toast } from 'sonner'
+
+interface AgoraSTTContextType {
+  rtcManager: RtcManager
+  rtmManager: RtmManager
+  sttManager: SttManager
+}
+
+const AgoraSTTContext = createContext<AgoraSTTContextType | null>(null)
+
+export const useAgoraSTT = () => {
+  const context = useContext(AgoraSTTContext)
+  if (!context) {
+    throw new Error('useAgoraSTT must be used within AgoraSTTProvider')
+  }
+  return context
+}
+
+interface AgoraSTTProviderProps {
+  children: React.ReactNode
+}
+
+export const AgoraSTTProvider: React.FC<AgoraSTTProviderProps> = ({ children }) => {
+  const rtcManagerRef = useRef<RtcManager>(new RtcManager())
+  const rtmManagerRef = useRef<RtmManager>(new RtmManager())
+  const sttManagerRef = useRef<SttManager>(new SttManager({
+    rtmManager: rtmManagerRef.current
+  }))
+
+  // 错误处理 - 替换原来的 useCatchError
+  useEffect(() => {
+    const handleError = (e: ErrorEvent) => {
+      if (e.error?.message) {
+        toast.error(e.error.message)
+      }
+    }
+
+    const handleUnhandledRejection = (e: PromiseRejectionEvent) => {
+      if (e.reason?.message) {
+        toast.error(e.reason.message)
+      }
+    }
+
+    window.addEventListener('error', handleError, true)
+    window.addEventListener('unhandledrejection', handleUnhandledRejection)
+
+    return () => {
+      window.removeEventListener('error', handleError, true)
+      window.removeEventListener('unhandledrejection', handleUnhandledRejection)
+    }
+  }, [])
+
+  const value = {
+    rtcManager: rtcManagerRef.current,
+    rtmManager: rtmManagerRef.current,
+    sttManager: sttManagerRef.current,
+  }
+
+  return (
+    <AgoraSTTContext.Provider value={value}>
+      {children}
+    </AgoraSTTContext.Provider>
+  )
+}

+ 28 - 21
src/client/admin/components/agora-stt/__tests__/AgoraSTTComponent.test.tsx

@@ -2,17 +2,9 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
 import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { render, screen, fireEvent, waitFor } from '@testing-library/react';
 import { AgoraSTTComponent } from '../AgoraSTTComponent';
 import { AgoraSTTComponent } from '../AgoraSTTComponent';
 
 
-// Mock the useAgoraSTT hook
-vi.mock('@/client/hooks/useAgoraSTT', () => ({
-  useAgoraSTT: vi.fn()
-}));
-
-// Mock the utils
-vi.mock('@/client/utils/agora-stt', () => ({
-  getAgoraConfig: vi.fn(),
-  validateAgoraConfig: vi.fn(),
-  isBrowserSupported: vi.fn(() => true),
-  getBrowserSupportError: vi.fn(() => null)
+// Mock the useAgoraSTTManager hook
+vi.mock('../hooks/useAgoraSTTManager', () => ({
+  useAgoraSTTManager: vi.fn()
 }));
 }));
 
 
 // Mock UI components
 // Mock UI components
@@ -61,9 +53,23 @@ vi.mock('lucide-react', () => ({
   WifiOff: () => <span>WifiOff</span>
   WifiOff: () => <span>WifiOff</span>
 }));
 }));
 
 
-import { useAgoraSTT } from '@/client/hooks/useAgoraSTT';
+// Mock Select components
+vi.mock('@/client/components/ui/select', () => ({
+  Select: ({ children, value, onValueChange }: any) => (
+    <div>
+      <div data-testid="select-value">{value}</div>
+      {children}
+    </div>
+  ),
+  SelectTrigger: ({ children }: any) => <div>{children}</div>,
+  SelectValue: ({ placeholder }: any) => <span>{placeholder}</span>,
+  SelectContent: ({ children }: any) => <div>{children}</div>,
+  SelectItem: ({ children, value }: any) => <div data-value={value}>{children}</div>
+}));
+
+import { useAgoraSTTManager } from '../hooks/useAgoraSTTManager';
 
 
-const mockUseAgoraSTT = vi.mocked(useAgoraSTT);
+const mockUseAgoraSTTManager = vi.mocked(useAgoraSTTManager);
 
 
 describe('AgoraSTTComponent', () => {
 describe('AgoraSTTComponent', () => {
   const defaultState = {
   const defaultState = {
@@ -73,7 +79,8 @@ describe('AgoraSTTComponent', () => {
     isConnecting: false,
     isConnecting: false,
     error: null,
     error: null,
     transcriptionResults: [],
     transcriptionResults: [],
-    currentTranscription: ''
+    currentTranscription: '',
+    microphonePermission: 'prompt' as const
   };
   };
 
 
   const mockJoinChannel = vi.fn();
   const mockJoinChannel = vi.fn();
@@ -85,7 +92,7 @@ describe('AgoraSTTComponent', () => {
   beforeEach(() => {
   beforeEach(() => {
     vi.clearAllMocks();
     vi.clearAllMocks();
 
 
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState },
       state: { ...defaultState },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,
@@ -104,7 +111,7 @@ describe('AgoraSTTComponent', () => {
   });
   });
 
 
   it('shows connection status when connected', () => {
   it('shows connection status when connected', () => {
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState, isConnected: true, isConnecting: false },
       state: { ...defaultState, isConnected: true, isConnecting: false },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,
@@ -120,7 +127,7 @@ describe('AgoraSTTComponent', () => {
   });
   });
 
 
   it('shows recording status when recording', () => {
   it('shows recording status when recording', () => {
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState, isConnected: true, isRecording: true, isConnecting: false },
       state: { ...defaultState, isConnected: true, isRecording: true, isConnecting: false },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,
@@ -136,7 +143,7 @@ describe('AgoraSTTComponent', () => {
   });
   });
 
 
   it('shows error message when there is an error', () => {
   it('shows error message when there is an error', () => {
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState, error: 'Configuration error', isConnecting: false },
       state: { ...defaultState, error: 'Configuration error', isConnecting: false },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,
@@ -162,7 +169,7 @@ describe('AgoraSTTComponent', () => {
   });
   });
 
 
   it('calls startRecording when start button is clicked', async () => {
   it('calls startRecording when start button is clicked', async () => {
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState, isConnected: true, isConnecting: false },
       state: { ...defaultState, isConnected: true, isConnecting: false },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,
@@ -191,7 +198,7 @@ describe('AgoraSTTComponent', () => {
       }
       }
     ];
     ];
 
 
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState, isConnected: true, transcriptionResults, isConnecting: false },
       state: { ...defaultState, isConnected: true, transcriptionResults, isConnecting: false },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,
@@ -216,7 +223,7 @@ describe('AgoraSTTComponent', () => {
       }
       }
     ];
     ];
 
 
-    mockUseAgoraSTT.mockReturnValue({
+    mockUseAgoraSTTManager.mockReturnValue({
       state: { ...defaultState, isConnected: true, transcriptionResults, isConnecting: false },
       state: { ...defaultState, isConnected: true, transcriptionResults, isConnecting: false },
       joinChannel: mockJoinChannel,
       joinChannel: mockJoinChannel,
       leaveChannel: mockLeaveChannel,
       leaveChannel: mockLeaveChannel,

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

@@ -1,64 +1,6 @@
-import { RefObject, useEffect, useRef, useState, useMemo } from "react"
-import { Button, message } from "antd"
-import { RootState, AppDispatch } from "@/store"
-import { removeMessage, setPageInfo } from "@/store/reducers/global"
-import { useDispatch, useSelector, TypedUseSelectorHook } from "react-redux"
-import { TOAST_DURATION } from "@/common"
+import { RefObject, useEffect, useRef, useState } from "react"
 
 
-export const useAppDispatch = () => useDispatch<AppDispatch>()
-
-export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector
-
-export const useCatchError = () => {
-  const _showError = (error: Error) => {
-    if (error?.message) {
-      message.error(error.message, TOAST_DURATION)
-    }
-  }
-
-  const handleError = (e: ErrorEvent) => {
-    const { error } = e
-    _showError(error)
-  }
-
-  const unhandledRejection = (e: PromiseRejectionEvent) => {
-    const { reason } = e
-    _showError(reason)
-  }
-
-  useEffect(() => {
-    window.addEventListener("error", handleError, true)
-    window.addEventListener("unhandledrejection", unhandledRejection)
-
-    return () => {
-      window.removeEventListener("error", handleError, true)
-      window.removeEventListener("unhandledrejection", unhandledRejection)
-    }
-  }, [])
-}
-
-export const useScreenResize = () => {
-  const dispatch = useDispatch()
-
-  const onResize = () => {
-    dispatch(
-      setPageInfo({
-        width: window.innerWidth,
-        height: window.innerHeight,
-      }),
-    )
-  }
-
-  useEffect(() => {
-    onResize()
-    window.addEventListener("resize", onResize)
-    return () => {
-      window.removeEventListener("resize", onResize)
-    }
-  }, [])
-}
-
-export const useMount = (callback?: () => {}) => {
+export const useMount = (callback?: () => void) => {
   const isMountRef = useRef(false)
   const isMountRef = useRef(false)
 
 
   useEffect(() => {
   useEffect(() => {
@@ -69,8 +11,8 @@ export const useMount = (callback?: () => {}) => {
   return isMountRef.current
   return isMountRef.current
 }
 }
 
 
-export const usePrevious = (value: any) => {
-  const ref = useRef()
+export const usePrevious = <T>(value: T): T | undefined => {
+  const ref = useRef<T>()
 
 
   useEffect(() => {
   useEffect(() => {
     ref.current = value
     ref.current = value
@@ -79,31 +21,7 @@ export const usePrevious = (value: any) => {
   return ref.current
   return ref.current
 }
 }
 
 
-export const useMessage = () => {
-  const dispatch = useDispatch()
-  const messageList = useSelector((state: RootState) => state.global.messageList)
-  const [messageApi, contextHolder] = message.useMessage()
-
-  useEffect(() => {
-    if (messageList.length) {
-      const first = messageList[0]
-      if (first) {
-        messageApi.open({
-          content: first.content,
-          type: first.type,
-          duration: first.duration || 3,
-        })
-        if (first.key) {
-          dispatch(removeMessage(first.key))
-        }
-      }
-    }
-  }, [messageList])
-
-  return { contextHolder }
-}
-
-export const useResizeObserver = (ref: RefObject<React.ReactNode | HTMLElement>) => {
+export const useResizeObserver = (ref: RefObject<HTMLElement>) => {
   const [dimensions, setDimensions] = useState<Omit<DOMRectReadOnly, "toJSON">>({
   const [dimensions, setDimensions] = useState<Omit<DOMRectReadOnly, "toJSON">>({
     x: 0,
     x: 0,
     y: 0,
     y: 0,
@@ -114,7 +32,7 @@ export const useResizeObserver = (ref: RefObject<React.ReactNode | HTMLElement>)
     bottom: 0,
     bottom: 0,
     left: 0,
     left: 0,
   })
   })
-  const resizeObserverRef = useRef<any>(null)
+  const resizeObserverRef = useRef<ResizeObserver | null>(null)
 
 
   useEffect(() => {
   useEffect(() => {
     resizeObserverRef.current = new ResizeObserver((entries) => {
     resizeObserverRef.current = new ResizeObserver((entries) => {
@@ -130,7 +48,7 @@ export const useResizeObserver = (ref: RefObject<React.ReactNode | HTMLElement>)
     }
     }
 
 
     return () => {
     return () => {
-      resizeObserverRef.current.disconnect()
+      resizeObserverRef.current?.disconnect()
     }
     }
   }, [ref])
   }, [ref])
 
 

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

@@ -1,5 +1,5 @@
 import { genRandomUserId } from "./utils"
 import { genRandomUserId } from "./utils"
-import { IUserInfo, IChatItem, IUICaptionData } from "@/types"
+import { IUserInfo, IChatItem, IUICaptionData } from "../types"
 
 
 const SENTENCES = [
 const SENTENCES = [
   "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
   "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",

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

@@ -1,6 +1,5 @@
-import store from "@/store"
 import { parseQuery } from "./utils"
 import { parseQuery } from "./utils"
-import { IRequestLanguages } from "@/types"
+import { IRequestLanguages } from "../types"
 
 
 const MODE = import.meta.env.MODE
 const MODE = import.meta.env.MODE
 let gatewayAddress = "https://api.agora.io"
 let gatewayAddress = "https://api.agora.io"

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

@@ -1,4 +1,4 @@
-import { IUserInfo, IOptions } from "@/types"
+import { IUserInfo, IOptions } from "../types"
 import { getDefaultLanguage } from "./utils"
 import { getDefaultLanguage } from "./utils"
 
 
 const USER_INFO_KEY = "__user_info__"
 const USER_INFO_KEY = "__user_info__"

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

@@ -1,6 +1,6 @@
 import { getUserInfoFromLocal } from "./storage"
 import { getUserInfoFromLocal } from "./storage"
 import { CAPTION_SCROLL_PX_LIST } from "./constant"
 import { CAPTION_SCROLL_PX_LIST } from "./constant"
-import { ITextItem, ILanguageSelect } from "@/types"
+import { ITextItem, ILanguageSelect } from "../types"
 
 
 function _pad(num: number) {
 function _pad(num: number) {
   return num.toString().padStart(2, "0")
   return num.toString().padStart(2, "0")

+ 233 - 0
src/client/admin/components/agora-stt/hooks/useAgoraSTTManager.ts

@@ -0,0 +1,233 @@
+import { useState, useCallback, useEffect } from 'react';
+import { useAgoraSTT } from '../AgoraSTTProvider';
+import { toast } from 'sonner';
+
+export interface TranscriptionResult {
+  text: string;
+  isFinal: boolean;
+  timestamp: number;
+  confidence?: number;
+}
+
+export interface AgoraSTTState {
+  isConnected: boolean;
+  isRecording: boolean;
+  isTranscribing: boolean;
+  isConnecting: boolean;
+  error: string | null;
+  transcriptionResults: TranscriptionResult[];
+  currentTranscription: string;
+  microphonePermission: 'granted' | 'denied' | 'prompt';
+}
+
+export interface UseAgoraSTTManagerResult {
+  state: AgoraSTTState;
+  joinChannel: (userId: string, channel: string, userName: string) => Promise<void>;
+  leaveChannel: () => Promise<void>;
+  startRecording: (languages: { value: string; label: string }[]) => Promise<void>;
+  stopRecording: () => Promise<void>;
+  clearTranscriptions: () => void;
+}
+
+export const useAgoraSTTManager = (): UseAgoraSTTManagerResult => {
+  const { sttManager } = useAgoraSTT();
+
+  const [state, setState] = useState<AgoraSTTState>({
+    isConnected: false,
+    isRecording: false,
+    isTranscribing: false,
+    isConnecting: false,
+    error: null,
+    transcriptionResults: [],
+    currentTranscription: '',
+    microphonePermission: 'prompt'
+  });
+
+  const updateState = useCallback((updates: Partial<AgoraSTTState>) => {
+    setState(prev => ({ ...prev, ...updates }));
+  }, []);
+
+  const setError = useCallback((error: string | null) => {
+    updateState({ error });
+  }, [updateState]);
+
+  const joinChannel = useCallback(async (userId: string, channel: string, userName: string): Promise<void> => {
+    try {
+      updateState({ error: null, isConnecting: true });
+
+      // 初始化 STT 管理器
+      await sttManager.init({ userId, channel, userName });
+
+      updateState({
+        isConnected: true,
+        isConnecting: false,
+        error: null
+      });
+
+      toast.success('成功加入频道');
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误';
+      setError(`加入频道失败: ${errorMessage}`);
+      updateState({ isConnecting: false });
+      toast.error('加入频道失败');
+    }
+  }, [sttManager, updateState, setError]);
+
+  const leaveChannel = useCallback(async (): Promise<void> => {
+    try {
+      await sttManager.destroy();
+      updateState({
+        isConnected: false,
+        isRecording: false,
+        isTranscribing: false,
+        currentTranscription: ''
+      });
+      toast.success('已离开频道');
+    } catch (error) {
+      console.error('离开频道失败:', error);
+      toast.error('离开频道失败');
+    }
+  }, [sttManager, updateState]);
+
+  const checkMicrophonePermission = useCallback(async (): Promise<boolean> => {
+    try {
+      const permissionStatus = await navigator.permissions.query({ name: 'microphone' as PermissionName });
+      updateState({ microphonePermission: permissionStatus.state as 'granted' | 'denied' | 'prompt' });
+
+      if (permissionStatus.state === 'denied') {
+        setError('麦克风权限已被拒绝,请在浏览器设置中启用');
+        return false;
+      }
+      return true;
+    } catch (error) {
+      console.warn('Microphone permission API not supported');
+      return true;
+    }
+  }, [updateState, setError]);
+
+  const requestMicrophonePermission = useCallback(async (): Promise<boolean> => {
+    try {
+      const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
+      stream.getTracks().forEach(track => track.stop());
+      updateState({ microphonePermission: 'granted' });
+      return true;
+    } catch (error) {
+      updateState({ microphonePermission: 'denied' });
+      setError('麦克风权限请求被拒绝,请允许访问麦克风');
+      return false;
+    }
+  }, [updateState, setError]);
+
+  const startRecording = useCallback(async (languages: { value: string; label: string }[]): Promise<void> => {
+    if (!state.isConnected) {
+      setError('未连接到 Agora 频道');
+      return;
+    }
+
+    // 检查麦克风权限
+    if (!(await checkMicrophonePermission())) {
+      return;
+    }
+
+    // 如果权限是prompt状态,请求权限
+    if (state.microphonePermission === 'prompt') {
+      if (!(await requestMicrophonePermission())) {
+        return;
+      }
+    }
+
+    try {
+      // 转换语言格式为 STT 管理器期望的格式
+      const sttLanguages = languages.map(lang => ({
+        source: lang.value,
+        target: []
+      }));
+
+      // 开始转录
+      await sttManager.startTranscription({ languages: sttLanguages });
+
+      updateState({
+        isRecording: true,
+        isTranscribing: true,
+        error: null
+      });
+
+      toast.success('开始录音');
+    } catch (error) {
+      const errorMessage = error instanceof Error ? error.message : '未知错误';
+      setError(`开始录音失败: ${errorMessage}`);
+      toast.error('开始录音失败');
+    }
+  }, [state.isConnected, state.microphonePermission, sttManager, checkMicrophonePermission, requestMicrophonePermission, updateState, setError]);
+
+  const stopRecording = useCallback(async (): Promise<void> => {
+    try {
+      await sttManager.stopTranscription();
+      updateState({
+        isRecording: false,
+        isTranscribing: false
+      });
+      toast.success('停止录音');
+    } catch (error) {
+      console.error('停止录音失败:', error);
+      toast.error('停止录音失败');
+    }
+  }, [sttManager, updateState]);
+
+  const clearTranscriptions = useCallback((): void => {
+    updateState({
+      transcriptionResults: [],
+      currentTranscription: ''
+    });
+  }, [updateState]);
+
+  // 监听转录结果
+  useEffect(() => {
+    // TODO: 实现监听 STT 管理器的转录结果事件
+    // sttManager.on('transcription', (result) => {
+    //   const newResult: TranscriptionResult = {
+    //     text: result.text,
+    //     isFinal: result.isFinal,
+    //     timestamp: Date.now(),
+    //     confidence: result.confidence
+    //   };
+    //
+    //   setState(prev => ({
+    //     ...prev,
+    //     transcriptionResults: [...prev.transcriptionResults, newResult],
+    //     currentTranscription: result.isFinal ? '' : result.text
+    //   }));
+    // });
+
+    // 临时模拟转录结果
+    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,
+    joinChannel,
+    leaveChannel,
+    startRecording,
+    stopRecording,
+    clearTranscriptions
+  };
+};

+ 8 - 1
src/client/admin/components/agora-stt/index.ts

@@ -1 +1,8 @@
-export { AgoraSTTComponent } from './AgoraSTTComponent';
+export { AgoraSTTComponent } from './AgoraSTTComponent';
+export { AgoraSTTProvider } from './AgoraSTTProvider';
+export { useAgoraSTT } from './AgoraSTTProvider';
+export { useAgoraSTTManager } from './hooks/useAgoraSTTManager';
+
+export * from './common/constant';
+export * from './common/hooks';
+export * from './types';

+ 1 - 1
src/client/admin/components/agora-stt/manager/parser/parser.ts

@@ -1,6 +1,6 @@
 import { ITextstream, ParserEvents, ITranslationItem } from "./types"
 import { ITextstream, ParserEvents, ITranslationItem } from "./types"
 import { AGEventEmitter } from "../events"
 import { AGEventEmitter } from "../events"
-import protoRoot from "@/protobuf/SttMessage.js"
+import protoRoot from "../../protobuf/SttMessage.js"
 
 
 export class Parser extends AGEventEmitter<ParserEvents> {
 export class Parser extends AGEventEmitter<ParserEvents> {
   constructor() {
   constructor() {

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

@@ -7,7 +7,7 @@ import AgoraRTC, {
 import { AGEventEmitter } from "../events"
 import { AGEventEmitter } from "../events"
 import { RtcEvents, IUserTracks } from "./types"
 import { RtcEvents, IUserTracks } from "./types"
 import { parser } from "../parser"
 import { parser } from "../parser"
-import { apiGetAgoraToken } from "@/common"
+import { apiGetAgoraToken } from "../../common"
 
 
 const appId = import.meta.env.VITE_AGORA_APP_ID
 const appId = import.meta.env.VITE_AGORA_APP_ID
 
 

+ 1 - 1
src/client/admin/components/agora-stt/manager/rtm/constant.ts

@@ -1,4 +1,4 @@
-import { RTMConfig } from "agora-rtm"
+import { RTMConfig } from "agora-rtm-sdk"
 
 
 export const DEFAULT_RTM_CONFIG: RTMConfig = {
 export const DEFAULT_RTM_CONFIG: RTMConfig = {
   logLevel: "debug",
   logLevel: "debug",

+ 6 - 9
src/client/admin/components/agora-stt/manager/rtm/rtm.ts

@@ -1,5 +1,5 @@
-import AgoraRTM, { RTMEvents, ChannelType, RTMClient, RTMConfig, MetadataItem } from "agora-rtm"
-import { mapToArray, isString, apiGetAgoraToken, getDefaultLanguageSelect } from "@/common"
+import AgoraRTM, { RTMEvents, ChannelType, RTMClient, RTMConfig, MetadataItem } from "agora-rtm-sdk"
+import { mapToArray, isString, apiGetAgoraToken, getDefaultLanguageSelect } from "../../common"
 import { AGEventEmitter } from "../events"
 import { AGEventEmitter } from "../events"
 import {
 import {
   RtmEvents,
   RtmEvents,
@@ -9,10 +9,10 @@ import {
   ValueOf,
   ValueOf,
   ILanguageItem,
   ILanguageItem,
 } from "./types"
 } from "./types"
-import { ISttData, Role, ILanguageSelect } from "@/types"
+import { ISttData, Role, ILanguageSelect } from "../../types"
 import { DEFAULT_RTM_CONFIG } from "./constant"
 import { DEFAULT_RTM_CONFIG } from "./constant"
 
 
-const { RTM, constantsType, setParameter } = AgoraRTM
+const { RTM } = AgoraRTM
 
 
 const appId = import.meta.env.VITE_AGORA_APP_ID
 const appId = import.meta.env.VITE_AGORA_APP_ID
 const CHANNEL_TYPE: ChannelType = "MESSAGE"
 const CHANNEL_TYPE: ChannelType = "MESSAGE"
@@ -39,14 +39,11 @@ export class RtmManager extends AGEventEmitter<RtmEvents> {
     this.userName = userName
     this.userName = userName
     this.channel = channel
     this.channel = channel
     if (!this.client) {
     if (!this.client) {
-      const token = await apiGetAgoraToken({ channel: this.channel, uid: this.userId })
-      if (token) {
-        this.rtmConfig.token = token
-      }
       this.client = new RTM(appId, userId, this.rtmConfig)
       this.client = new RTM(appId, userId, this.rtmConfig)
     }
     }
     this._listenRtmEvents()
     this._listenRtmEvents()
-    await this.client.login()
+    const token = await apiGetAgoraToken({ channel: this.channel, uid: this.userId })
+    await this.client.login(token)
     this.joined = true
     this.joined = true
     // subscribe message channel
     // subscribe message channel
     await this.client.subscribe(channel, {
     await this.client.subscribe(channel, {

+ 2 - 7
src/client/admin/components/agora-stt/manager/rtm/types.ts

@@ -1,5 +1,4 @@
-import { ILanguageSelect, ISttData, Role } from "@/types"
-import { RTMEvents } from "agora-rtm"
+import { ILanguageSelect, ISttData, Role } from "../../types"
 
 
 export interface ISimpleUserInfo {
 export interface ISimpleUserInfo {
   userName: string
   userName: string
@@ -12,11 +11,7 @@ export interface ILanguageChangedItem {
 }
 }
 
 
 export interface RtmEvents {
 export interface RtmEvents {
-  status: (
-    status:
-      | RTMEvents.RTMConnectionStatusChangeEvent
-      | RTMEvents.StreamChannelConnectionStatusChangeEvent,
-  ) => void
+  status: (status: any) => void
   userListChanged: (userList: ISimpleUserInfo[]) => void
   userListChanged: (userList: ISimpleUserInfo[]) => void
   languagesChanged: (languages: ILanguageSelect) => void
   languagesChanged: (languages: ILanguageSelect) => void
   sttDataChanged: (status: ISttData) => void
   sttDataChanged: (status: ISttData) => void

+ 2 - 2
src/client/admin/components/agora-stt/manager/stt/stt.ts

@@ -5,11 +5,11 @@ import {
   apiSTTUpdateTranscription,
   apiSTTUpdateTranscription,
   apiSTTAcquireToken,
   apiSTTAcquireToken,
   EXPERIENCE_DURATION,
   EXPERIENCE_DURATION,
-} from "@/common"
+} from "../../common"
 import { AGEventEmitter } from "../events"
 import { AGEventEmitter } from "../events"
 import { STTEvents, STTManagerStartOptions, STTManagerOptions, STTManagerInitData } from "./types"
 import { STTEvents, STTManagerStartOptions, STTManagerOptions, STTManagerInitData } from "./types"
 import { RtmManager } from "../rtm"
 import { RtmManager } from "../rtm"
-import { IRequestLanguages } from "@/types"
+import { IRequestLanguages } from "../../types"
 
 
 export class SttManager extends AGEventEmitter<STTEvents> {
 export class SttManager extends AGEventEmitter<STTEvents> {
   option?: STTManagerOptions
   option?: STTManagerOptions

+ 1 - 1
src/client/admin/components/agora-stt/manager/stt/types.ts

@@ -1,5 +1,5 @@
 import { RtmManager } from "../rtm"
 import { RtmManager } from "../rtm"
-import { IRequestLanguages } from "@/types"
+import { IRequestLanguages } from "../../types"
 
 
 export interface STTEvents {}
 export interface STTEvents {}
 
 

+ 96 - 0
src/client/admin/components/agora-stt/types/index.ts

@@ -0,0 +1,96 @@
+export type MenuType = "AI" | "DialogRecord"
+export type STTDataType = "transcribe" | "translate"
+export type DialogLanguageType = "live" | "translate"
+export type InputStatuses = "warning" | "error" | ""
+export type Role = "host" | "audience"
+
+export interface ISttData {
+  taskId?: string
+  token?: string
+  startTime?: number // ms
+  duration?: number // ms
+  status?: "start" | "end"
+}
+
+export interface IUserInfo {
+  userName: string
+  userId: number | string
+}
+
+export type LangDataType = "transcribe" | "translate"
+
+export interface IOptions {
+  language: string
+  channel: string
+}
+
+export interface IUserData extends IUserInfo {
+  isHost?: boolean
+  isLocal: boolean
+  order: number
+}
+
+export interface IRequestLanguages {
+  source: string;
+  target: string[];
+}
+
+export interface ITranslationItem {
+  lang: string;
+  text: string;
+}
+
+export interface ITranscriptionItem {
+  lang: string;
+  text: string;
+  isFinal: boolean;
+  confidence?: number;
+}
+
+export interface ISttLanguage {
+  label: string;
+  code: string;
+}
+
+export interface ITextItem {
+  dataType: LangDataType
+  uid: string | number
+  lang: string
+  time: number
+  text: string
+  isFinal: boolean
+  username: string
+  startTextTs: number // start time
+  textTs: number // end time
+  translations?: ITranslationItem[]
+}
+
+export interface IChatItem {
+  userName: string
+  content: string
+  translations: ITranslationItem[]
+  startTextTs: string | number
+  textTs: string | number
+  time: string | number
+}
+
+export interface IUICaptionData {
+  content: string
+  translate?: string
+  userName: string
+  translations?: ITranslationItem[]
+}
+
+export interface ILanguageSelect {
+  transcribe1?: string
+  translate1List?: string[]
+  transcribe2?: string
+  translate2List?: string[]
+}
+
+export interface IMessage {
+  key?: number
+  content: string
+  type: "success" | "error" | "warning" | "info"
+  duration?: number // s
+}