尧图网站建设 尧图网络
  • 首页
  • 关于我们
  • 服务项目
  • 案例展示
  • 建站流程
  • 资讯中心
  • 联系我们
首页/资讯中心/详情

前端语音识别实战:基于百度ASR的Web应用集成与优化指南

前端语音识别实战:基于百度ASR的Web应用集成与优化指南
📅 发布时间:2026/7/31 16:45:52

1. 项目概述:前端语音识别为何选择百度ASR

最近在做一个智能表单项目,其中有个需求是用户可以通过说话来填写表格里的数字,比如“收入五千”、“年龄二十八”这种。团队一开始讨论技术方案,有人提直接用浏览器的Web Speech API,也有人建议上科大讯飞或者阿里云。但几轮PK下来,我们最终还是敲定了百度智能云的语音识别(ASR)服务。今天就来聊聊,为什么在前端场景下,百度ASR成了一个相当“能打”的选择,以及从零开始接入它,你会遇到哪些坑,又有哪些技巧能让体验更丝滑。

简单说,百度ASR就是一个能把用户说的话实时转成文字的服务。对于前端开发者而言,它的核心价值在于提供了稳定、高精度的识别能力,并且通过WebSocket或HTTP接口,可以很方便地集成到H5、小程序或者任何Web应用中。相比浏览器原生的语音识别接口(兼容性差、识别率因浏览器和网络环境波动大),第三方服务在准确率、稳定性、功能定制化(如领域模型、标点预测)上优势明显。而对比其他云服务商,百度ASR在中文场景下的积累、文档的友好度以及免费额度的慷慨程度上,对个人开发者和中小团队尤其友好。

这个方案特别适合需要语音交互的Web应用,比如语音输入搜索、智能客服对话、会议实时转写、教育类应用的语音评测,或者像我做的这种通过语音填充表单的场景。无论你是想快速给应用加个语音输入按钮,还是构建复杂的实时语音交互界面,这套流程都能给你一个扎实的起点。

2. 核心方案选型与设计思路拆解

2.1 为何是百度ASR?—— 一份前端视角的评估清单

当我们需要在前端引入语音识别时,摆在面前的路主要有三条:浏览器原生API、开源库、第三方云服务。我们逐一分析:

  1. 浏览器原生 Web Speech API (SpeechRecognition):

    • 优点:零依赖,无需后端中转,理论上延迟最低。
    • 致命缺点:兼容性是硬伤。除了Chrome和Edge支持较好,其他浏览器尤其是移动端浏览器支持度参差不齐。识别引擎和质量取决于浏览器实现,不可控。在国内网络环境下,稳定性堪忧。对于追求稳定交付的商业项目,它基本是第一个被排除的选项。
  2. 开源语音识别库(如TensorFlow.js + 预训练模型):

    • 优点:完全离线,数据隐私性最好。
    • 缺点:模型体积巨大(动辄几十MB甚至上百MB),严重影响前端应用的加载速度。识别精度和速度通常远低于云端大模型,且需要开发者具备一定的机器学习知识来处理模型优化和推理。这更适合对网络和隐私有极端要求、且能接受性能妥协的特定场景。
  3. 第三方云服务(百度、阿里、腾讯、科大讯飞等):

    • 优点:识别精度高、稳定性好、功能丰富(如方言、垂直领域优化)、有专业团队维护和升级。
    • 缺点:产生费用(但有免费额度),需要网络请求,涉及数据出网需要考虑合规性。

我们最终选择百度ASR,是基于下面这份详细的对比清单:

评估维度百度ASR阿里云/腾讯云科大讯飞浏览器原生API
中文识别精度优秀,在通用和多个垂直领域有深度优化优秀顶尖,尤其在教育、医疗等专业领域一般,不稳定
前端接入复杂度简单,SDK封装良好,文档示例丰富中等中等简单(但功能有限)
免费额度非常慷慨,每月有一定免费时长,适合学习和初期项目有免费额度通常有免费额度但可能更严格免费
实时语音识别支持,WebSocket流式传输,延迟低支持支持支持(但实现差)
定制化能力支持热词、自训练模型(需付费)支持支持深度定制不支持
文档与社区中文文档清晰,社区活跃,问题容易找到解答文档齐全文档齐全W3C标准,资料分散
综合成本考量对于大多数应用,免费额度足够覆盖;超出部分单价有竞争力类似可能略高,尤其在专业领域无直接成本

注意:选择时一定要查看服务商最新的定价策略和免费额度详情。百度ASR的“语音技术”产品,通常新用户赠送的免费包完全足够原型开发和早期用户使用。

2.2 整体架构设计:从前端到云端的协同

一个健壮的前端语音识别方案,绝不是简单调个API。你需要考虑音频流的采集、处理、传输、接收结果和UI反馈的完整链条。我们的设计思路如下:

  1. 音频采集端(前端):

    • 核心:使用navigator.mediaDevices.getUserMediaAPI 获取用户的麦克风权限和音频流。
    • 优化:根据场景(如高保真录音 vs 语音通话)设置合适的音频约束(sampleRate,channelCount)。例如,对于语音识别,16kHz采样率、单声道通常已足够,这能减少数据量。
    • 处理:将获取到的MediaStream通过AudioContext或ScriptProcessorNode(已废弃) /AudioWorklet进行处理,可以进行降噪、增益、或直接编码。
  2. 音频编码与传输:

    • 格式:百度ASR的实时接口支持pcm、wav、opus、speex等多种格式。pcm最原始,数据量大;opus压缩率高、音质好,是推荐选择。
    • 传输:采用WebSocket建立长连接,进行流式传输。这比HTTP分片上传的延迟低得多,能实现“边说边出文字”的实时效果。前端需要将采集到的音频数据块(ArrayBuffer)按一定时间间隔或大小通过WebSocket发送。
  3. 服务端(可选但推荐):

    • 为什么需要?直接将百度ASR的API密钥和SecretKey放在前端是极度危险的,会瞬间被恶意利用导致天价账单。
    • 角色:搭建一个轻量的中继/鉴权服务。前端连接自己的服务端,服务端负责:
      • 向百度认证服务器获取访问令牌(access_token)。
      • 建立与百度ASR服务的WebSocket连接。
      • 在前端和百度服务之间转发音频数据和识别结果。
      • (可选)在此层做限流、日志、计费统计。
  4. 识别结果处理与UI交互:

    • 流式结果:百度ASR会返回中间结果(result.sn为partial)和最终结果(result.sn为final)。前端需要妥善处理,用中间结果实现“实时上屏”的流畅感,用最终结果进行业务逻辑提交。
    • UI反馈:需要设计麦克风动画、音量波动图、实时文本显示区、错误状态提示(如“请说话”、“网络错误”、“识别中...”)等,以提供良好的用户体验。

这个架构确保了安全性、实时性和可扩展性。下面,我们就进入具体的实操环节。

3. 从零到一:前端接入百度ASR全流程实操

3.1 前期准备:账号、应用与密钥

第一步不是写代码,而是去百度AI开放平台进行配置。

  1. 注册与登录:访问百度AI开放平台,完成实名认证。这是使用任何百度AI服务的前提。
  2. 创建应用:在控制台找到“语音技术”产品,创建一个新应用。创建时,注意选择正确的“接口选择”,确保包含“短语音识别标准版”和“实时语音识别”等你需要的功能。
  3. 获取密钥:应用创建成功后,你会得到API Key和Secret Key。请立刻将它们存入后台环境变量,绝对不要提交到前端代码仓库!
  4. 了解配额:在控制台查看该应用的“语音识别”免费调用量,明确额度限制。

3.2 核心实现:基于WebSocket的实时语音识别

我们采用“前端 -> 自有后端 -> 百度服务”的架构。这里以前端Vue.js + Node.js后端为例。

后端部分(Node.js + Express):

后端核心任务是生成access_token并代理WebSocket连接。

// server.js (部分关键代码) const express = require('express'); const axios = require('axios'); const WebSocket = require('ws'); const { createProxyServer } = require('http-proxy'); const app = express(); const API_KEY = process.env.BAIDU_API_KEY; const SECRET_KEY = process.env.BAIDU_SECRET_KEY; const TOKEN_URL = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${API_KEY}&client_secret=${SECRET_KEY}`; // 1. 获取access_token (需要缓存,避免频繁请求) let cachedToken = { token: '', expireTime: 0 }; async function getAccessToken() { const now = Date.now(); if (cachedToken.token && cachedToken.expireTime > now) { return cachedToken.token; } try { const response = await axios.post(TOKEN_URL); cachedToken.token = response.data.access_token; cachedToken.expireTime = now + (response.data.expires_in - 300) * 1000; // 提前5分钟过期 return cachedToken.token; } catch (error) { console.error('Failed to get Baidu access token:', error); throw error; } } // 2. 创建与百度ASR的WebSocket代理 app.get('/api/ws-proxy', async (req, res) => { // 这里通常需要添加用户身份验证(如JWT) const token = await getAccessToken(); // 百度实时语音识别WebSocket地址 const baiduWsUrl = `wss://vop.baidu.com/pro_api?dev_pid=80001&cuid=YOUR_DEVICE_ID&token=${token}`; // 理论上应使用WebSocket代理库,以下为逻辑示意 // 实际项目中,可使用 `http-proxy-ws` 或类似库建立透明代理 res.json({ wsUrl: baiduWsUrl }); // 将构造好的URL传给前端,由前端直连(仍有密钥暴露风险,不推荐) // 更安全的做法是后端建立双WebSocket中继,代码略复杂。 }); // 更安全的方案:后端建立中继WebSocket服务器 const wss = new WebSocket.Server({ noServer: true }); wss.on('connection', async function connection(frontendWs) { const token = await getAccessToken(); const baiduWsUrl = `wss://vop.baidu.com/pro_api?dev_pid=80001&cuid=${require('crypto').randomBytes(8).toString('hex')}&token=${token}`; const baiduWs = new WebSocket(baiduWsUrl); // 前端 <-> 后端 <-> 百度 双向转发消息 frontendWs.on('message', (data) => baiduWs.send(data)); baiduWs.on('message', (data) => frontendWs.send(data)); // 处理错误和关闭事件... }); // 启动服务器 const server = app.listen(3000, () => console.log('Server running on port 3000')); server.on('upgrade', (request, socket, head) => { wss.handleUpgrade(request, socket, head, (ws) => { wss.emit('connection', ws, request); }); });

前端部分(Vue 3 + Composition API):

前端负责音频采集、编码、传输和结果展示。

<template> <div> <button @click="toggleRecording" :disabled="isConnecting"> {{ isRecording ? '停止识别' : '开始语音识别' }} </button> <p>状态:{{ statusText }}</p> <p>实时结果:<strong>{{ interimText }}</strong></p> <p>最终结果:<strong>{{ finalText }}</strong></p> <!-- 可以添加一个音频可视化组件 --> </div> </template> <script setup> import { ref, onUnmounted } from 'vue'; const isRecording = ref(false); const isConnecting = ref(false); const statusText = ref('点击开始'); const interimText = ref(''); // 中间结果 const finalText = ref(''); // 最终结果 let mediaStream = null; let audioContext = null; let processor = null; let ws = null; let recorder = null; // 如果使用MediaRecorder const toggleRecording = async () => { if (isRecording.value) { stopRecording(); } else { await startRecording(); } }; const startRecording = async () => { try { statusText.value = '正在获取麦克风权限...'; // 1. 获取麦克风音频流 mediaStream = await navigator.mediaDevices.getUserMedia({ audio: { sampleRate: 16000, // 推荐16kHz channelCount: 1, // 单声道 echoCancellation: true, noiseSuppression: true, }, }); // 2. 连接我们自己的WebSocket中继服务 statusText.value = '正在连接服务器...'; isConnecting.value = true; // 假设我们的中继WebSocket在 /ws-asr ws = new WebSocket('ws://localhost:3000/ws-asr'); ws.onopen = () => { console.log('WebSocket连接成功'); statusText.value = '请开始说话...'; isRecording.value = true; isConnecting.value = false; // 3. 开始处理音频流并发送 startAudioProcessing(mediaStream, ws); }; ws.onmessage = (event) => { const result = JSON.parse(event.data); // 根据百度ASR返回格式处理 if (result.err_no === 0) { const text = result.result.join(' '); if (result.sn === 'partial') { // 中间结果 interimText.value = text; } else if (result.sn === 'final') { // 最终结果 finalText.value = text; interimText.value = ''; // 清空中间结果 // 可以在这里触发业务逻辑,如填充表单 console.log('识别最终结果:', text); } } else { console.error('识别错误:', result); statusText.value = `识别错误: ${result.err_msg}`; } }; ws.onerror = (error) => { console.error('WebSocket错误:', error); statusText.value = '连接错误'; stopRecording(); }; ws.onclose = () => { console.log('WebSocket连接关闭'); if (isRecording.value) { statusText.value = '连接意外关闭'; } }; } catch (error) { console.error('启动录音失败:', error); statusText.value = `失败: ${error.message}`; isConnecting.value = false; } }; const startAudioProcessing = (stream, websocket) => { // 方案A: 使用AudioContext + ScriptProcessorNode (已废弃但简单) audioContext = new (window.AudioContext || window.webkitAudioContext)({ sampleRate: 16000, }); const source = audioContext.createMediaStreamSource(stream); processor = audioContext.createScriptProcessor(4096, 1, 1); // 缓冲区大小,输入通道数,输出通道数 processor.onaudioprocess = (event) => { // 获取PCM数据 const inputData = event.inputBuffer.getChannelData(0); // 将Float32Array的PCM数据转换为Int16Array (百度ASR常见要求) const int16Data = floatTo16BitPCM(inputData); // 通过WebSocket发送 if (websocket.readyState === WebSocket.OPEN) { websocket.send(int16Data.buffer); // 发送ArrayBuffer } }; source.connect(processor); processor.connect(audioContext.destination); // 方案B: 使用MediaRecorder录制opus格式 (更高效,但需处理mimeType支持) // 代码略复杂,但能减少数据量。需要后端ASR服务支持opus格式。 }; const floatTo16BitPCM = (input) => { const output = new Int16Array(input.length); for (let i = 0; i < input.length; i++) { const s = Math.max(-1, Math.min(1, input[i])); output[i] = s < 0 ? s * 0x8000 : s * 0x7FFF; } return output; }; const stopRecording = () => { isRecording.value = false; statusText.value = '已停止'; // 关闭WebSocket if (ws && ws.readyState === WebSocket.OPEN) { ws.close(); } // 停止所有音频轨道 if (mediaStream) { mediaStream.getTracks().forEach(track => track.stop()); } // 关闭AudioContext if (audioContext && audioContext.state !== 'closed') { audioContext.close(); } if (processor) { processor.disconnect(); } mediaStream = null; audioContext = null; processor = null; ws = null; }; // 组件卸载时清理资源 onUnmounted(() => { if (isRecording.value) { stopRecording(); } }); </script>

实操心得:音频处理部分是最容易出问题的地方。AudioContext的采样率最好与getUserMedia中请求的采样率一致,避免重采样带来的性能损失和质量下降。如果使用MediaRecorder编码成opus,务必检查浏览器兼容性,并且确认百度ASR接口支持该格式的负载。

3.3 关键参数解析与优化配置

在连接百度ASR时,URL中的参数和发送的数据格式至关重要。

  • dev_pid:语言模型参数,决定识别的基础语言和领域。

    • 80001:普通话输入法模型(默认,推荐)。
    • 1737:英语模型。
    • 1637:粤语模型。
    • 1837:四川话模型。
    • 选择错误的dev_pid会显著降低识别率。
  • cuid:用户唯一标识,用于区分不同用户。可以传入设备ID、用户ID的哈希值等。不要使用容易猜测的固定值。

  • 音频格式:必须与发送的音频数据格式严格匹配。在WebSocket的初始握手帧或HTTP请求的头部中,需要通过format等参数指定。PCM需要同时指定rate(采样率,如16000)、dev_pid和cuid。

  • 热词功能:对于项目中的专业词汇(如产品名、特定术语),可以在请求中通过hotword参数传入一个热词列表(JSON字符串)。百度ASR会优先识别这些词,大幅提升特定词汇的准确率。这对于“通过语音识别数字达到表格的自动填写”这类场景非常有用,你可以把表格字段名、可能的数值范围作为热词。

// 在建立连接时,可以将热词作为参数传递(具体参数名需查阅最新文档) const hotwordList = ['张三', '李四', '收入', '支出', '一万', '五千']; const hotwordParam = encodeURIComponent(JSON.stringify(hotwordList)); // 将 hotwordParam 拼接到WebSocket URL或后续的配置帧中

4. 避坑指南与高级技巧

4.1 常见问题与排查清单

在实际开发中,我踩过不少坑。这里列一个速查表:

问题现象可能原因排查步骤与解决方案
获取麦克风权限失败1. 浏览器安全限制(非HTTPS)。
2. 用户拒绝授权。
3. 麦克风被其他应用占用。
1.本地开发用localhost或127.0.0.1,线上必须使用HTTPS。
2. 引导用户检查浏览器设置,并设计友好的授权提示UI。
3. 提示用户关闭其他可能占用麦克风的软件(如会议软件)。
WebSocket连接立即断开/报错1.access_token无效或过期。
2. 参数错误(如dev_pid不支持)。
3. 网络策略(CORS/防火墙)。
1. 检查后端获取和刷新token的逻辑,确保发送的是有效token。
2. 核对百度ASR文档,确认所有参数名和值正确。
3. 后端服务需正确设置WebSocket的CORS头,服务器防火墙开放相应端口。
能连接但识别不出文字1. 音频格式或采样率不匹配。
2. 音频数据发送格式错误。
3. 环境噪音太大或音量过低。
1.重中之重:确认前端采集、后端转发、百度接收三处的音频格式(PCM/OPUS)、采样率(16000)、编码完全一致。用音频工具录制发送的数据包进行比对。
2. 检查发送的是否是纯音频数据的ArrayBuffer,而不是Blob或Base64字符串(除非接口明确要求)。
3. 前端可添加音量检测,过低时提示用户大声点。考虑集成前端降噪库(如RNNoise)。
识别结果延迟高1. 网络延迟。
2. 前端音频缓冲区过大。
3. 服务器处理慢。
1. 选择离用户近的云服务区域(百度ASR服务端位置)。
2. 调整ScriptProcessorNode的缓冲区大小(如从4096改为2048),但太小会增加CPU负担。
3. 确保后端中继服务只是简单转发,没有复杂的阻塞操作。
移动端(特别是iOS)表现异常1. iOS Safari对getUserMedia和AudioContext的自动播放策略严格。
2. 页面退到后台音频上下文被挂起。
1.必须在用户触摸事件(如click)回调中启动audioContext.resume()和getUserMedia,这是iOS的强制要求。
2. 监听pagehide和pageshow事件,妥善处理audioContext.suspend()和resume()。

4.2 性能与体验优化技巧

  1. VAD(语音活动检测):一直发送音频浪费流量和算力。可以集成VAD,只在检测到人声时才将音频数据发送给ASR。可以使用webrtcvad的JavaScript移植版,或者一些简单的能量阈值法在onaudioprocess中实现。

  2. 前端音频预处理:在发送前,对音频数据进行简单的增益(提高音量)或使用Web Audio API的BiquadFilterNode进行高频增强,能在一定程度上提升嘈杂环境下的识别率。

  3. 结果后处理:百度ASR返回的文本可能没有标点或格式不佳。可以编写简单的规则进行后处理,例如:

    • 数字规范化:将“一万五千”转换为“15000”。
    • 添加标点:根据停顿或关键词插入逗号、句号。
    • 纠错:结合业务场景的热词表进行纠错(例如,用户说“收入五钱”,但热词里有“五千”,可以自动纠正)。
  4. 优雅降级与兼容性处理:

    • 检测浏览器是否支持WebSocket和getUserMedia。
    • 如果不支持实时识别,可以降级为“录音-上传-识别”的模式,使用百度的短语音识别HTTP接口。
    • 对于完全不支持的环境,隐藏语音按钮,提供纯文本输入。
  5. 使用Web Worker:音频处理(如PCM转换、VAD)是CPU密集型任务,放在主线程会阻塞UI。可以将这些操作放入Web Worker,保证页面流畅。这也是应对“前端H5开发过程中,如何开启多线程”的一个经典案例。

// 主线程 const audioWorker = new Worker('./audio-processor.worker.js'); audioWorker.postMessage({ command: 'init', sampleRate: 16000 }); processor.onaudioprocess = (event) => { const inputData = event.inputBuffer.getChannelData(0); // 将数据传递给Worker处理 audioWorker.postMessage({ command: 'process', buffer: inputData.buffer }, [inputData.buffer]); }; audioWorker.onmessage = (e) => { if (e.data.command === 'processedData') { // 从Worker收到处理好的Int16Array数据 if (ws.readyState === WebSocket.OPEN) { ws.send(e.data.buffer); } } };

4.3 安全与成本控制

  1. 密钥安全:重申一遍,API Key和Secret Key必须放在后端。前端只能接触由后端签发的临时令牌或直接连接后端代理。
  2. 请求限流:在后端服务中对用户的语音识别请求进行频率和时长限制,防止恶意调用消耗配额。
  3. 监控与告警:在后端记录调用日志,监控每日用量。当用量接近免费额度时设置告警,避免产生意外费用。
  4. 数据合规:如果处理用户语音数据,需在隐私政策中明确说明,并获取用户同意。考虑是否需要对音频数据进行匿名化或脱敏处理。

5. 项目复盘与扩展思考

回过头看这个“前端使用百度ASR”的项目,它远不止是调用一个API那么简单。它涉及了前端音频处理、实时网络通信、服务端代理、安全设计等多个环节。最大的挑战往往在于调试音频流水线——从麦克风到最终识别文字,任何一个环节的格式不匹配都会导致失败。

我个人最深刻的体会是:一定要搭建一个可视化的调试工具。比如,一个页面能实时显示采集到的音频波形、显示发送的数据包大小、并打印出百度服务返回的原始JSON。这能帮你快速定位问题是出在采集、发送、还是服务端识别阶段。

这个基础框架搭建好后,可以做的扩展非常多。比如,结合语音合成(TTS),实现完整的语音对话;加入语义理解(NLP),让语音指令不仅能转成文字,还能理解用户意图并执行操作(如“把上一条记录删掉”);或者利用声纹识别做一些简单的用户身份验证。

对于“通过语音识别数字达到表格的自动填写”这个具体场景,下一步的优化方向可以是结合NLP进行语义槽位填充。不仅仅是识别出“五千”,还要能判断这个“五千”对应的是表格里的“收入”字段还是“支出”字段。这需要更复杂的上下文理解和对话管理,可能是另一个有趣的挑战了。

相关新闻

  • AI教材编写新利器!实测5款AI工具,高效生成高校专业教材! - AI写论文
  • Atlas1337上下文管理升级:从单次指令到连续任务理解
  • 多Agent如何高效协同?阿里、快手、宝马等最佳实践

最新新闻

  • 从视频中智能提取PPT:如何用计算机视觉技术将视频内容转化为可编辑文档
  • 30分钟从零搭建AI Agent:LangChain实战指南与核心原理
  • 杭州银行与品牌金店黄金回收模式全面对比参考 - 日常比对手册
  • Python并发爬虫实战:多线程与多进程架构设计与性能优化
  • 2026 年沈阳值得关注的财税代办服务机构盘点:辽宁省信誉分第一的头部财税公司深度盘点 - 财税推荐官
  • 【编译链接】

日新闻

  • 7步掌握KMS智能激活工具:Windows和Office永久激活完整方案
  • 如何在Windows上运行iOS应用:ipasim跨平台模拟器终极指南
  • 2026年重庆工伤赔偿律师口碑推荐:洪家木律师用专业赢得信赖 - 本地品牌推荐

周新闻

  • 大连理工大学与东京大学联手打造的“主动型AI助手“
  • 170.2026年国家级科研瓶颈:超精密单点金刚石切削(SPDT)光学表面生成
  • SongBloom:革命性歌曲生成框架深度解析——如何通过交织自回归与扩散模型创作完整音乐

月新闻

  • 2026年6月公司网站搭建最新热门渠道测评:四大低成本/零代码平台对比+避坑
  • 【Linux】Linux arm 编译QT程序,出现expected “}“报错
  • 【MATLAB例程】四基站二维AOA定位与距离辅助增强对比仿真。基于角度观测和测距修正的固定目标平面定位精度分析

关于尧图

  • 公司简介
  • 团队介绍
  • 企业文化
  • 荣誉资质

服务项目

  • 定制开发
  • 电商建站
  • UI 设计
  • 运维服务

快速链接

  • 案例展示
  • 建站流程
  • 常见问题
  • 资讯中心

联系方式

  • 📍北京市朝阳区互联网产业园 A 座 10 层
  • 📞400-888-8888
  • ✉️contact@rkmt.cn
  • 🕐周一至周日 9:00-21:00

© 2024 北京尧图网络科技有限公司 版权所有 | 京 ICP 备 XXXXXXXX 号