Parcourir la source

✨ feat(umd): add umd format sdk test page and related features

- add umd test page components and styles
- implement umd sdk loading and initialization logic
- add stt transcription management functions
- add test log recording and display features
- add real-time transcription display component

♻️ refactor(sdk): modify umd build configuration

- remove agora-rtm from external dependencies to bundle it in umd
- clear global variable configuration for umd build
- add vite plugin to copy umd files to dist directory after build

🔧 chore(router): add route for umd test page

- register umd-test route in router configuration
- add navigation function between pages
yourname il y a 2 mois
Parent
commit
aa62da9f0b

+ 3 - 9
packages/stt-sdk-core/vite.config.ts

@@ -16,11 +16,10 @@ export default defineConfig({
       formats: ['es', 'cjs', 'umd'],
     },
     rollupOptions: {
-      external: ['agora-rtm'],
+      // 在UMD格式中,将agora-rtm打包进去
+      external: [],
       output: {
-        globals: {
-          'agora-rtm': 'AgoraRTM',
-        },
+        globals: {},
       },
     },
     sourcemap: true,
@@ -32,9 +31,4 @@ export default defineConfig({
       exclude: ['tests/**'],
     }),
   ],
-  test: {
-    globals: true,
-    environment: 'jsdom',
-    setupFiles: ['./tests/setup.ts'],
-  },
 })

+ 83 - 0
src/pages/umd-test/index.module.scss

@@ -0,0 +1,83 @@
+.umdTestPage {
+  padding: 24px;
+  min-height: 100vh;
+  background: #f5f5f5;
+
+  .header {
+    text-align: center;
+    margin-bottom: 24px;
+
+    h2 {
+      margin-bottom: 8px;
+    }
+  }
+
+  .content {
+    display: grid;
+    grid-template-columns: 1fr 400px;
+    gap: 24px;
+    max-width: 1400px;
+    margin: 0 auto;
+
+    @media (max-width: 1200px) {
+      grid-template-columns: 1fr;
+    }
+  }
+
+  .leftPanel {
+    display: flex;
+    flex-direction: column;
+    gap: 24px;
+  }
+
+  .rightPanel {
+    display: flex;
+    flex-direction: column;
+    gap: 24px;
+  }
+
+  .loadCard,
+  .configCard,
+  .testCard,
+  .logCard,
+  .infoCard,
+  .captionCard {
+    :global(.ant-card-head) {
+      border-bottom: 1px solid #f0f0f0;
+    }
+
+    :global(.ant-card-body) {
+      padding: 24px;
+    }
+  }
+
+  .logContainer {
+    height: 300px;
+    overflow-y: auto;
+    border: 1px solid #d9d9d9;
+    border-radius: 6px;
+    padding: 12px;
+    background: #fafafa;
+
+    .logEntry {
+      font-family: "Monaco", "Menlo", "Ubuntu Mono", monospace;
+      font-size: 12px;
+      line-height: 1.5;
+      margin-bottom: 4px;
+      word-break: break-all;
+
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+
+  .captionCard {
+    :global(.ant-card-body) {
+      min-height: 200px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+  }
+}

+ 579 - 0
src/pages/umd-test/index.tsx

@@ -0,0 +1,579 @@
+import { useState, useEffect } from "react"
+import { Card, Button, Input, Form, message, Space, Typography, Divider, Alert } from "antd"
+import {
+  InfoCircleOutlined,
+  PlayCircleOutlined,
+  StopOutlined,
+  ReloadOutlined,
+  EyeOutlined,
+  EyeInvisibleOutlined,
+} from "@ant-design/icons"
+import { useNavigate } from "react-router-dom"
+
+import styles from "./index.module.scss"
+
+const { Title, Text } = Typography
+
+// UMD格式SDK的全局变量声明
+declare global {
+  interface Window {
+    SttSdkCore: any
+  }
+}
+
+const UmdTestPage = () => {
+  const nav = useNavigate()
+  const [messageApi, contextHolder] = message.useMessage()
+
+  const [form] = Form.useForm()
+  const [sdk, setSdk] = useState<any>(null)
+  const [sttManager, setSttManager] = useState<any>(null)
+  const [rtmManager, setRtmManager] = useState<any>(null)
+  const [isSdkInitialized, setIsSdkInitialized] = useState(false)
+  const [isSttManagerInitialized, setIsSttManagerInitialized] = useState(false)
+  const [isTranscriptionActive, setIsTranscriptionActive] = useState(false)
+  const [transcriptionStatus, setTranscriptionStatus] = useState<string>("idle")
+  const [testResults, setTestResults] = useState<string[]>([])
+  const [sttData, setSttData] = useState<any>({})
+  const [captionVisible, setCaptionVisible] = useState(true)
+  const [umdLoaded, setUmdLoaded] = useState(false)
+
+  // 添加测试日志
+  const addTestLog = (log: string) => {
+    setTestResults((prev) => [...prev, `${new Date().toLocaleTimeString()}: ${log}`])
+  }
+
+  // 动态加载UMD格式的SDK
+  const loadUmdSdk = async () => {
+    try {
+      addTestLog("开始加载UMD格式SDK...")
+
+      // 检查是否已加载
+      if (window.SttSdkCore) {
+        addTestLog("✅ UMD SDK已加载")
+        setUmdLoaded(true)
+        return
+      }
+
+      // 动态创建script标签加载UMD SDK
+      const script = document.createElement("script")
+      script.src = "/packages/stt-sdk-core/dist/index.umd.js"
+      script.type = "text/javascript"
+      script.async = true
+
+      script.onload = () => {
+        addTestLog("✅ UMD SDK加载成功")
+        setUmdLoaded(true)
+        messageApi.success("UMD SDK加载成功")
+      }
+
+      script.onerror = () => {
+        addTestLog("❌ UMD SDK加载失败")
+        messageApi.error("UMD SDK加载失败,请检查文件路径")
+      }
+
+      document.head.appendChild(script)
+    } catch (error) {
+      addTestLog(`❌ UMD SDK加载异常: ${error}`)
+      messageApi.error(`UMD SDK加载异常: ${error}`)
+    }
+  }
+
+  // 监听STT数据变化
+  const onSttDataChanged = (data: any) => {
+    console.log("[UMD Test] sttDataChanged:", data)
+    setSttData(data)
+
+    if (data.status === "start") {
+      setIsTranscriptionActive(true)
+      setTranscriptionStatus("active")
+      addTestLog("📢 转录状态更新: 转录已开始")
+    } else if (data.status === "end") {
+      setIsTranscriptionActive(false)
+      setTranscriptionStatus("stopped")
+      addTestLog("📢 转录状态更新: 转录已停止")
+    }
+
+    // 如果有转录内容,记录到日志
+    if (data.transcribe1 || data.transcribe2) {
+      addTestLog(`📝 转录结果: ${data.transcribe1 || data.transcribe2}`)
+    }
+
+    // 如果有翻译内容,记录到日志
+    if (data.translate1List?.length) {
+      data.translate1List.forEach((text: string, index: number) => {
+        if (text) {
+          addTestLog(`🌐 翻译${index + 1}: ${text}`)
+        }
+      })
+    }
+  }
+
+  // 初始化SDK(使用UMD格式)
+  const initializeSdk = async (values: {
+    appId: string
+    certificate: string
+    channel: string
+    userName: string
+  }) => {
+    try {
+      if (!window.SttSdkCore) {
+        addTestLog("❌ UMD SDK未加载,请先加载SDK")
+        messageApi.error("UMD SDK未加载,请先加载SDK")
+        return
+      }
+
+      addTestLog("开始初始化UMD SDK...")
+
+      // 使用UMD格式的SDK
+      const SttSdk = window.SttSdkCore.default || window.SttSdkCore.SttSdk
+      const newSdk = new SttSdk()
+
+      await newSdk.initialize({
+        appId: values.appId,
+        certificate: values.certificate,
+        logLevel: "info",
+      })
+
+      setSdk(newSdk)
+      setIsSdkInitialized(true)
+      addTestLog("✅ UMD SDK初始化成功")
+
+      // 创建管理器
+      const rtmManager = newSdk.createRtmManager()
+      const sttManager = newSdk.createSttManager(rtmManager)
+
+      // 监听RTM管理器的事件
+      rtmManager.on("sttDataChanged", onSttDataChanged)
+
+      setSttManager(sttManager)
+      setRtmManager(rtmManager)
+      addTestLog("✅ STT和RTM管理器创建成功")
+
+      messageApi.success("UMD SDK初始化成功")
+    } catch (error) {
+      addTestLog(`❌ UMD SDK初始化失败: ${error}`)
+      messageApi.error(`UMD SDK初始化失败: ${error}`)
+    }
+  }
+
+  // 初始化STT管理器
+  const initializeSttManager = async () => {
+    if (!sttManager || !form) return
+
+    try {
+      const values = form.getFieldsValue()
+      addTestLog("开始初始化STT管理器...")
+
+      // 生成随机用户ID
+      const genRandomUserId = () => Math.floor(Math.random() * 1000000)
+
+      await sttManager.init({
+        userId: genRandomUserId(),
+        channel: values.channel,
+        userName: values.userName,
+      })
+
+      setIsSttManagerInitialized(true)
+      addTestLog("✅ STT管理器初始化成功(已自动加入RTM频道)")
+      messageApi.success("STT管理器初始化成功")
+    } catch (error) {
+      addTestLog(`❌ STT管理器初始化失败: ${error}`)
+      messageApi.error(`STT管理器初始化失败: ${error}`)
+    }
+  }
+
+  // 开始转录
+  const startTranscription = async () => {
+    if (!sttManager) return
+
+    try {
+      addTestLog("开始语音转录...")
+      setTranscriptionStatus("starting")
+
+      await sttManager.startTranscription({
+        languages: [{ source: "zh-CN" }],
+      })
+
+      setIsTranscriptionActive(true)
+      setTranscriptionStatus("active")
+      addTestLog("✅ 语音转录已开始")
+      messageApi.success("语音转录已开始")
+    } catch (error) {
+      addTestLog(`❌ 语音转录启动失败: ${error}`)
+      setTranscriptionStatus("error")
+      messageApi.error(`语音转录启动失败: ${error}`)
+    }
+  }
+
+  // 停止转录
+  const stopTranscription = async () => {
+    if (!sttManager) return
+
+    try {
+      addTestLog("停止语音转录...")
+      setTranscriptionStatus("stopping")
+
+      await sttManager.stopTranscription()
+
+      setIsTranscriptionActive(false)
+      setTranscriptionStatus("stopped")
+      addTestLog("✅ 语音转录已停止")
+      messageApi.success("语音转录已停止")
+    } catch (error) {
+      addTestLog(`❌ 语音转录停止失败: ${error}`)
+      setTranscriptionStatus("error")
+      messageApi.error(`语音转录停止失败: ${error}`)
+    }
+  }
+
+  // 查询转录状态
+  const queryTranscription = async () => {
+    if (!sttManager) return
+
+    try {
+      addTestLog("查询转录状态...")
+
+      const result = await sttManager.queryTranscription()
+      addTestLog(`📊 转录状态查询结果: ${JSON.stringify(result)}`)
+      messageApi.info("转录状态查询完成,查看日志了解详情")
+    } catch (error) {
+      addTestLog(`❌ 转录状态查询失败: ${error}`)
+      messageApi.error(`转录状态查询失败: ${error}`)
+    }
+  }
+
+  // 清理资源
+  const cleanup = async () => {
+    try {
+      addTestLog("开始清理资源...")
+
+      if (isTranscriptionActive && sttManager) {
+        await stopTranscription()
+      }
+
+      if (sttManager) {
+        await sttManager.destroy()
+        setSttManager(null)
+        setIsSttManagerInitialized(false)
+      }
+
+      if (rtmManager) {
+        rtmManager.off("sttDataChanged", onSttDataChanged)
+        await rtmManager.destroy()
+        setRtmManager(null)
+      }
+
+      if (sdk) {
+        await sdk.destroy()
+        setSdk(null)
+        setIsSdkInitialized(false)
+      }
+
+      addTestLog("✅ 资源清理完成")
+      setTranscriptionStatus("idle")
+      messageApi.success("资源清理完成")
+    } catch (error) {
+      addTestLog(`❌ 资源清理失败: ${error}`)
+      messageApi.error(`资源清理失败: ${error}`)
+    }
+  }
+
+  // 切换字幕显示
+  const toggleCaptionVisibility = () => {
+    setCaptionVisible(!captionVisible)
+    addTestLog(`📺 字幕显示: ${!captionVisible ? "开启" : "关闭"}`)
+  }
+
+  // 返回主应用
+  const goToMainApp = () => {
+    nav("/home")
+  }
+
+  // 组件挂载时尝试加载UMD SDK
+  useEffect(() => {
+    loadUmdSdk()
+  }, [])
+
+  // 组件卸载时清理资源
+  useEffect(() => {
+    return () => {
+      if (sdk) {
+        cleanup()
+      }
+    }
+  }, [sdk])
+
+  return (
+    <div className={styles.umdTestPage}>
+      {contextHolder}
+
+      <div className={styles.header}>
+        <Title level={2}>🌐 UMD 格式 SDK 测试页面</Title>
+        <Text type="secondary">测试 UMD 格式 SDK 在 AMD 环境中的使用</Text>
+      </div>
+
+      <div className={styles.content}>
+        <div className={styles.leftPanel}>
+          <Card title="📦 SDK 加载状态" className={styles.loadCard}>
+            <Space direction="vertical" style={{ width: "100%" }}>
+              <Alert
+                message={umdLoaded ? "✅ UMD SDK 已加载" : "⏳ 正在加载 UMD SDK..."}
+                type={umdLoaded ? "success" : "info"}
+                showIcon
+              />
+
+              <Button onClick={loadUmdSdk} disabled={umdLoaded}>
+                {umdLoaded ? "✅ 已加载" : "重新加载 SDK"}
+              </Button>
+            </Space>
+          </Card>
+
+          <Card title="🔧 SDK 配置" className={styles.configCard}>
+            <Form
+              form={form}
+              layout="vertical"
+              initialValues={{
+                appId: import.meta.env.VITE_AGORA_APP_ID || "",
+                certificate: import.meta.env.VITE_AGORA_APP_CERTIFICATE || "",
+                channel: `umd-test-channel-${Date.now()}`,
+                userName: `UMDTestUser-${Date.now()}`,
+              }}
+            >
+              <Form.Item
+                label="App ID"
+                name="appId"
+                rules={[{ required: true, message: "请输入 App ID" }]}
+              >
+                <Input placeholder="请输入 Agora App ID" />
+              </Form.Item>
+
+              <Form.Item
+                label="Certificate"
+                name="certificate"
+                rules={[{ required: true, message: "请输入 Certificate" }]}
+              >
+                <Input placeholder="请输入 Agora Certificate" />
+              </Form.Item>
+
+              <Form.Item
+                label="频道名称"
+                name="channel"
+                rules={[{ required: true, message: "请输入频道名称" }]}
+              >
+                <Input placeholder="请输入频道名称" />
+              </Form.Item>
+
+              <Form.Item
+                label="用户名称"
+                name="userName"
+                rules={[{ required: true, message: "请输入用户名称" }]}
+              >
+                <Input placeholder="请输入用户名称" />
+              </Form.Item>
+
+              <Form.Item>
+                <Space>
+                  <Button
+                    type="primary"
+                    onClick={() => initializeSdk(form.getFieldsValue())}
+                    disabled={!umdLoaded || isSdkInitialized}
+                  >
+                    {isSdkInitialized ? "✅ 已初始化" : "初始化 SDK"}
+                  </Button>
+
+                  <Button onClick={cleanup} danger disabled={!isSdkInitialized}>
+                    清理资源
+                  </Button>
+                </Space>
+              </Form.Item>
+            </Form>
+          </Card>
+
+          <Card title="🚀 功能测试" className={styles.testCard}>
+            <Space direction="vertical" style={{ width: "100%" }}>
+              <Button
+                onClick={initializeSttManager}
+                disabled={!isSdkInitialized || isSttManagerInitialized}
+                block
+              >
+                {isSttManagerInitialized ? "✅ STT管理器已初始化" : "初始化STT管理器"}
+              </Button>
+
+              <Divider />
+
+              <Button
+                type="primary"
+                icon={<PlayCircleOutlined />}
+                onClick={startTranscription}
+                disabled={!isSttManagerInitialized || isTranscriptionActive}
+                block
+              >
+                {isTranscriptionActive ? "转录进行中..." : "开始转录"}
+              </Button>
+
+              <Button
+                danger
+                icon={<StopOutlined />}
+                onClick={stopTranscription}
+                disabled={!isTranscriptionActive}
+                block
+              >
+                停止转录
+              </Button>
+
+              <Button
+                icon={<ReloadOutlined />}
+                onClick={queryTranscription}
+                disabled={!isTranscriptionActive}
+                block
+              >
+                查询状态
+              </Button>
+            </Space>
+
+            <Divider />
+
+            <Alert
+              message="转录状态"
+              description={
+                <div>
+                  <div>
+                    {transcriptionStatus === "idle"
+                      ? "等待开始转录"
+                      : transcriptionStatus === "starting"
+                        ? "正在启动转录..."
+                        : transcriptionStatus === "active"
+                          ? "转录进行中"
+                          : transcriptionStatus === "stopping"
+                            ? "正在停止转录..."
+                            : transcriptionStatus === "stopped"
+                              ? "转录已停止"
+                              : "状态异常"}
+                  </div>
+                  {sttData.status && (
+                    <div style={{ marginTop: 8, fontSize: "12px", color: "#666" }}>
+                      <div>任务ID: {sttData.taskId || "未设置"}</div>
+                      <div>
+                        开始时间:{" "}
+                        {sttData.startTime
+                          ? new Date(sttData.startTime).toLocaleTimeString()
+                          : "未开始"}
+                      </div>
+                      <div>
+                        持续时间: {sttData.duration ? `${sttData.duration / 1000}秒` : "未设置"}
+                      </div>
+                    </div>
+                  )}
+                </div>
+              }
+              type={
+                transcriptionStatus === "active"
+                  ? "success"
+                  : transcriptionStatus === "error"
+                    ? "error"
+                    : "info"
+              }
+              showIcon
+            />
+          </Card>
+        </div>
+
+        <div className={styles.rightPanel}>
+          <Card title="📋 测试日志" className={styles.logCard}>
+            <div className={styles.logContainer}>
+              {testResults.length === 0 ? (
+                <Text type="secondary">暂无测试日志,请开始测试...</Text>
+              ) : (
+                testResults.map((log, index) => (
+                  <div key={index} className={styles.logEntry}>
+                    {log}
+                  </div>
+                ))
+              )}
+            </div>
+
+            {testResults.length > 0 && (
+              <Button onClick={() => setTestResults([])} size="small" style={{ marginTop: 16 }}>
+                清空日志
+              </Button>
+            )}
+          </Card>
+
+          <Card title="💡 使用说明" className={styles.infoCard}>
+            <Space direction="vertical" style={{ width: "100%" }}>
+              <Text type="secondary">
+                <InfoCircleOutlined /> UMD格式测试说明:
+              </Text>
+              <ol>
+                <li>UMD SDK通过动态script标签加载</li>
+                <li>SDK作为全局变量 window.SttSdkCore 可用</li>
+                <li>支持AMD/CommonJS/全局变量使用方式</li>
+                <li>功能与ES模块版本完全一致</li>
+              </ol>
+
+              <Text type="secondary">
+                <InfoCircleOutlined /> 测试步骤:
+              </Text>
+              <ol>
+                <li>等待UMD SDK加载完成</li>
+                <li>填写 App ID 和 Certificate</li>
+                <li>点击"初始化 SDK"</li>
+                <li>初始化 STT 管理器</li>
+                <li>开始/停止转录测试</li>
+                <li>查看测试日志了解详细结果</li>
+              </ol>
+
+              <Button type="link" onClick={goToMainApp}>
+                返回主应用
+              </Button>
+            </Space>
+          </Card>
+
+          <Card
+            title={
+              <Space>
+                <span>📺 实时转录显示</span>
+                <Button
+                  type="text"
+                  size="small"
+                  icon={captionVisible ? <EyeOutlined /> : <EyeInvisibleOutlined />}
+                  onClick={toggleCaptionVisibility}
+                >
+                  {captionVisible ? "隐藏" : "显示"}
+                </Button>
+              </Space>
+            }
+            className={styles.captionCard}
+          >
+            {captionVisible ? (
+              <div style={{ padding: 16, background: "#f5f5f5", borderRadius: 4, minHeight: 100 }}>
+                {sttData.transcribe1 ? (
+                  <div>
+                    <Text strong>原文:</Text>
+                    <div style={{ marginTop: 8 }}>{sttData.transcribe1}</div>
+                  </div>
+                ) : (
+                  <Text type="secondary">等待转录结果...</Text>
+                )}
+
+                {sttData.translate1List?.map((trans: any, index: number) => (
+                  <div key={index} style={{ marginTop: 16 }}>
+                    <Text strong>
+                      翻译 {index + 1} ({trans.lang}):
+                    </Text>
+                    <div style={{ marginTop: 8 }}>{trans.text}</div>
+                  </div>
+                ))}
+              </div>
+            ) : (
+              <Text type="secondary">字幕显示已隐藏</Text>
+            )}
+          </Card>
+        </div>
+      </div>
+    </div>
+  )
+}
+
+export default UmdTestPage

+ 2 - 0
src/router/index.tsx

@@ -5,12 +5,14 @@ const HomePage = lazy(() => import("../pages/home"))
 const LoginPage = lazy(() => import("../pages/login"))
 const NotFoundPage = lazy(() => import("../pages/404"))
 const SdkTestPage = lazy(() => import("../pages/sdk-test"))
+const UmdTestPage = lazy(() => import("../pages/umd-test"))
 
 const routerItems = [
   <Route path="/" element={<LoginPage />} />,
   <Route path="/home" element={<HomePage />} />,
   <Route path="/login" element={<LoginPage />} />,
   <Route path="/sdk-test" element={<SdkTestPage />} />,
+  <Route path="/umd-test" element={<UmdTestPage />} />,
   <Route path="*" element={<NotFoundPage />} />,
 ]
 

+ 22 - 0
vite.config.ts

@@ -2,6 +2,8 @@ import { defineConfig } from "vite"
 import react from "@vitejs/plugin-react"
 import svgr from "vite-plugin-svgr"
 import { name } from "./package.json"
+import { copyFileSync, existsSync, mkdirSync } from "fs"
+import { join } from "path"
 
 const genBaseUrl = (mode) => {
   if (mode !== "development") {
@@ -43,6 +45,26 @@ export default defineConfig(({ mode }) => {
           // typescript: true,
         },
       }),
+      {
+        name: "copy-umd-files",
+        apply: "build",
+        closeBundle() {
+          const umdSource = join(__dirname, "packages/stt-sdk-core/dist/index.umd.js")
+          const umdDest = join(__dirname, "dist/packages/stt-sdk-core/dist/index.umd.js")
+
+          if (existsSync(umdSource)) {
+            // 确保目标目录存在
+            const destDir = join(__dirname, "dist/packages/stt-sdk-core/dist")
+            if (!existsSync(destDir)) {
+              mkdirSync(destDir, { recursive: true })
+            }
+
+            // 复制UMD文件
+            copyFileSync(umdSource, umdDest)
+            console.log("✅ UMD文件已复制到dist目录")
+          }
+        },
+      },
     ],
     css: {
       preprocessorOptions: {