mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-26 17:13:54 +08:00
Merge branch 'main' into KnowledgeBase_fix
This commit is contained in:
@@ -246,7 +246,7 @@ Websocket接口地址: wss://2662r3426b.vicp.fun/xiaozhi/v1/
|
|||||||
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
|
| 声纹识别 | 支持多用户声纹注册、管理和识别,与ASR并行处理,实时识别说话人身份并传递给LLM进行个性化回应 |
|
||||||
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
| 智能对话 | 支持多种LLM(大语言模型),实现智能对话 |
|
||||||
| 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 |
|
| 视觉感知 | 支持多种VLLM(视觉大模型),实现多模态交互 |
|
||||||
| 意图识别 | 支持LLM意图识别、Function Call函数调用,提供插件化意图处理机制 |
|
| 意图识别 | 支持外挂的大模型意图识别、大模型自主函数调用,提供插件化意图处理机制 |
|
||||||
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
|
| 记忆系统 | 支持本地短期记忆、mem0ai接口记忆,具备记忆总结功能 |
|
||||||
| 知识库 | 支持RAGFlow知识库,让大模型判断需要调度知识库后再回答 |
|
| 知识库 | 支持RAGFlow知识库,让大模型判断需要调度知识库后再回答 |
|
||||||
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
|
| 工具调用 | 支持客户端IOT协议、客户MCP协议、服务端MCP协议、MCP接入点协议、自定义工具函数 |
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ http://homeassistant.local:8123
|
|||||||
|
|
||||||
使用管理员账号,登录`智控台`。在`智能体管理`,找到你的智能体,再点击`配置角色`。
|
使用管理员账号,登录`智控台`。在`智能体管理`,找到你的智能体,再点击`配置角色`。
|
||||||
|
|
||||||
将意图识别设置成`函数调用`或`LLM意图识别`。这时你会看到右侧有一个`编辑功能`。点击`编辑功能`按钮,会弹出`功能管理`的框。
|
将意图识别设置成`外挂的大模型意图识别`或`大模型自主函数调用`。这时你会看到右侧有一个`编辑功能`。点击`编辑功能`按钮,会弹出`功能管理`的框。
|
||||||
|
|
||||||
在`功能管理`的框里,你需要勾选`HomeAssistant设备状态查询`和`HomeAssistant设备状态修改`。
|
在`功能管理`的框里,你需要勾选`HomeAssistant设备状态查询`和`HomeAssistant设备状态修改`。
|
||||||
|
|
||||||
|
|||||||
@@ -90,12 +90,11 @@ FLUSH PRIVILEGES;
|
|||||||
|
|
||||||
你需要在你电脑找一个文件夹,用来存放ragflow项目。例如我在`/home/system/xiaozhi`文件夹。
|
你需要在你电脑找一个文件夹,用来存放ragflow项目。例如我在`/home/system/xiaozhi`文件夹。
|
||||||
|
|
||||||
你可以使用`git`命令,将ragflow项目下载到这个文件夹,本教程使用的是`2025年11月11日`提交的`dd5b8e2e1a877211d17aae5552d8895b1bca763d`版本进行安装部署。
|
你可以使用`git`命令,将ragflow项目下载到这个文件夹,本教程使用的是`v0.22.0`版本进行安装部署。
|
||||||
```
|
```
|
||||||
git clone https://ghfast.top/https://github.com/infiniflow/ragflow.git
|
git clone https://ghfast.top/https://github.com/infiniflow/ragflow.git
|
||||||
cd ragflow
|
cd ragflow
|
||||||
git reset --hard dd5b8e2e1a877211d17aae5552d8895b1bca763d
|
git checkout v0.22.0
|
||||||
|
|
||||||
```
|
```
|
||||||
下载完后,进入`docker`文件夹。
|
下载完后,进入`docker`文件夹。
|
||||||
``` shell
|
``` shell
|
||||||
@@ -158,8 +157,6 @@ services:
|
|||||||
编辑`ragflow/docker`文件夹下的`.env`文件,找到以下配置,逐个搜索,逐个修改!逐个搜索,逐个修改!
|
编辑`ragflow/docker`文件夹下的`.env`文件,找到以下配置,逐个搜索,逐个修改!逐个搜索,逐个修改!
|
||||||
|
|
||||||
``` env
|
``` env
|
||||||
# ragflow镜像
|
|
||||||
RAGFLOW_IMAGE=infiniflow/ragflow@sha256:d9ff494d1bf72138ce6fe0eaf4962cf4bea471c8c266d12b8bfb9d10ac3a2f6e
|
|
||||||
# 端口设置
|
# 端口设置
|
||||||
SVR_WEB_HTTP_PORT=8008 # HTTP端口
|
SVR_WEB_HTTP_PORT=8008 # HTTP端口
|
||||||
SVR_WEB_HTTPS_PORT=8009 # HTTPS端口
|
SVR_WEB_HTTPS_PORT=8009 # HTTPS端口
|
||||||
@@ -173,7 +170,7 @@ MYSQL_DBNAME=rag_flow # 数据库名称
|
|||||||
# Redis配置 - 修改为您本地Redis的信息
|
# Redis配置 - 修改为您本地Redis的信息
|
||||||
REDIS_HOST=host.docker.internal # 使用host.docker.internal让容器访问主机服务
|
REDIS_HOST=host.docker.internal # 使用host.docker.internal让容器访问主机服务
|
||||||
REDIS_PORT=6379 # 本地Redis端口
|
REDIS_PORT=6379 # 本地Redis端口
|
||||||
REDIS_PASSWORD= # 如果你的Redis没有设置密码,这里填写None,否则填写密码
|
REDIS_PASSWORD= # 如果你的Redis没有设置密码,就按这样子填写,否则填写密码
|
||||||
```
|
```
|
||||||
|
|
||||||
注意,如果你的Redis没有设置密码,还要修改`ragflow/docker`文件夹下`service_conf.yaml.template`,将`infini_rag_flow`替换成空字符串。
|
注意,如果你的Redis没有设置密码,还要修改`ragflow/docker`文件夹下`service_conf.yaml.template`,将`infini_rag_flow`替换成空字符串。
|
||||||
@@ -259,4 +256,4 @@ docker-compose -f docker-compose.yml up -d
|
|||||||
# 第三步 让小智使用ragflow知识库
|
# 第三步 让小智使用ragflow知识库
|
||||||
登录到智控台。在顶部导航栏中,点击`智能体`,找到你要配置的智能体,点击`配置角色`按钮。
|
登录到智控台。在顶部导航栏中,点击`智能体`,找到你要配置的智能体,点击`配置角色`按钮。
|
||||||
|
|
||||||
在意图识别左侧,点击`编辑功能`按钮,弹出一个弹框。在弹框中选择你要添加的知识库。保存即可。
|
在意图识别左侧,点击`编辑功能`按钮,弹出一个弹框。在弹框中选择你要添加的知识库。保存即可。
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = 'https://github.com/infiniflow/ragflow/blob/main/README_zh.md',
|
||||||
|
`remark` = '详细部署教程参考:https://github.com/xinnan-tech/xiaozhi-esp32-server/blob/main/docs/ragflow-integration.md
|
||||||
|
部署成功,注冊登录后,点击右上角头像,获得RAGFlow的API KEY和API服务器地址。使用RAGFlow前请在Model Provider中添加模型和设置默认模型。' WHERE `id` = 'RAG_RAGFlow';
|
||||||
|
|
||||||
|
update `ai_model_config` set `model_name` = '外挂的大模型意图识别' where `id` = 'Intent_intent_llm';
|
||||||
|
update `ai_model_config` set `model_name` = '大模型自主函数调用' where `id` = 'Intent_function_call';
|
||||||
|
|
||||||
|
update `ai_model_provider` set `name` = '外挂的大模型意图识别' , `fields` = '[{"key":"llm","label":"引用的LLM模型","type":"string"}]' where id = 'SYSTEM_Intent_intent_llm';
|
||||||
|
update `ai_model_provider` set `name` = '大模型自主函数调用' where id = 'SYSTEM_Intent_function_call';
|
||||||
|
|
||||||
@@ -416,3 +416,10 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202510251150.sql
|
path: classpath:db/changelog/202510251150.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202511131020
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202511131020.sql
|
||||||
|
|||||||
@@ -13,7 +13,9 @@
|
|||||||
<div class="function-column">
|
<div class="function-column">
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<h4 class="column-title">{{ $t('functionDialog.unselectedFunctions') }}</h4>
|
<h4 class="column-title">{{ $t('functionDialog.unselectedFunctions') }}</h4>
|
||||||
<el-button type="text" @click="selectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
|
<el-button type="text" @click="selectAll" class="select-all-btn">
|
||||||
|
{{ $t('functionDialog.selectAll') }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="function-list">
|
<div class="function-list">
|
||||||
<div v-if="unselected.length">
|
<div v-if="unselected.length">
|
||||||
@@ -21,7 +23,7 @@
|
|||||||
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
|
||||||
@click.native.stop></el-checkbox>
|
@click.native.stop></el-checkbox>
|
||||||
<div class="func-tag" @click="handleFunctionClick(func)">
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||||
<div class="color-dot" :style="{ backgroundColor: getFunctionColor(func.name) }"></div>
|
<div class="color-dot"></div>
|
||||||
<span>{{ func.name }}</span>
|
<span>{{ func.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -36,7 +38,9 @@
|
|||||||
<div class="function-column">
|
<div class="function-column">
|
||||||
<div class="column-header">
|
<div class="column-header">
|
||||||
<h4 class="column-title">{{ $t('functionDialog.selectedFunctions') }}</h4>
|
<h4 class="column-title">{{ $t('functionDialog.selectedFunctions') }}</h4>
|
||||||
<el-button type="text" @click="deselectAll" class="select-all-btn">{{ $t('functionDialog.selectAll') }}</el-button>
|
<el-button type="text" @click="deselectAll" class="select-all-btn">
|
||||||
|
{{ $t('functionDialog.selectAll') }}
|
||||||
|
</el-button>
|
||||||
</div>
|
</div>
|
||||||
<div class="function-list">
|
<div class="function-list">
|
||||||
<div v-if="selectedList.length > 0">
|
<div v-if="selectedList.length > 0">
|
||||||
@@ -44,7 +48,7 @@
|
|||||||
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
|
<el-checkbox :label="func.name" v-model="selectedNames" @change="(val) => handleCheckboxChange(func, val)"
|
||||||
@click.native.stop></el-checkbox>
|
@click.native.stop></el-checkbox>
|
||||||
<div class="func-tag" @click="handleFunctionClick(func)">
|
<div class="func-tag" @click="handleFunctionClick(func)">
|
||||||
<div class="color-dot" :style="{ backgroundColor: getFunctionColor(func.name) }"></div>
|
<div class="color-dot"></div>
|
||||||
<span>{{ func.name }}</span>
|
<span>{{ func.name }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -57,7 +61,9 @@
|
|||||||
|
|
||||||
<!-- 右侧:参数配置 -->
|
<!-- 右侧:参数配置 -->
|
||||||
<div class="params-column">
|
<div class="params-column">
|
||||||
<h4 v-if="currentFunction" class="column-title">{{ $t('functionDialog.paramConfig') }} - {{ currentFunction.name }}</h4>
|
<h4 v-if="currentFunction" class="column-title">
|
||||||
|
{{ $t('functionDialog.paramConfig') }} - {{ currentFunction.name }}
|
||||||
|
</h4>
|
||||||
<div v-if="currentFunction" class="params-container">
|
<div v-if="currentFunction" class="params-container">
|
||||||
<el-form :model="currentFunction" class="param-form">
|
<el-form :model="currentFunction" class="param-form">
|
||||||
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
<!-- 遍历 fieldsMeta,而不是 params 的 keys -->
|
||||||
@@ -119,8 +125,8 @@
|
|||||||
<el-input v-model="mcpUrl" readonly class="url-input">
|
<el-input v-model="mcpUrl" readonly class="url-input">
|
||||||
<template #suffix>
|
<template #suffix>
|
||||||
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
<el-button @click="copyUrl" class="inner-copy-btn" icon="el-icon-document-copy">
|
||||||
{{ $t('functionDialog.copy') }}
|
{{ $t('functionDialog.copy') }}
|
||||||
</el-button>
|
</el-button>
|
||||||
</template>
|
</template>
|
||||||
</el-input>
|
</el-input>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,10 +197,6 @@ export default {
|
|||||||
selectedNames: [],
|
selectedNames: [],
|
||||||
currentFunction: null,
|
currentFunction: null,
|
||||||
modifiedFunctions: {},
|
modifiedFunctions: {},
|
||||||
functionColorMap: [
|
|
||||||
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
|
||||||
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
|
||||||
],
|
|
||||||
tempFunctions: {},
|
tempFunctions: {},
|
||||||
// 添加一个标志位来跟踪是否已经保存
|
// 添加一个标志位来跟踪是否已经保存
|
||||||
hasSaved: false,
|
hasSaved: false,
|
||||||
@@ -405,10 +407,6 @@ export default {
|
|||||||
// 通知父组件对话框已关闭且已保存
|
// 通知父组件对话框已关闭且已保存
|
||||||
this.$emit('dialog-closed', true);
|
this.$emit('dialog-closed', true);
|
||||||
},
|
},
|
||||||
getFunctionColor(name) {
|
|
||||||
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
||||||
return this.functionColorMap[hash % this.functionColorMap.length];
|
|
||||||
},
|
|
||||||
fieldRemark(field) {
|
fieldRemark(field) {
|
||||||
let description = (field && field.label) ? field.label : '';
|
let description = (field && field.label) ? field.label : '';
|
||||||
if (field.default) {
|
if (field.default) {
|
||||||
@@ -527,6 +525,7 @@ export default {
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
|
background-color: #5778ff;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -104,7 +104,7 @@
|
|||||||
<div slot="content">
|
<div slot="content">
|
||||||
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
<div><strong>功能名称:</strong> {{ func.name }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="icon-dot" :style="{ backgroundColor: getFunctionColor(func.name) }">
|
<div class="icon-dot">
|
||||||
{{ getFunctionDisplayChar(func.name) }}
|
{{ getFunctionDisplayChar(func.name) }}
|
||||||
</div>
|
</div>
|
||||||
</el-tooltip>
|
</el-tooltip>
|
||||||
@@ -189,10 +189,6 @@ export default {
|
|||||||
voiceOptions: [],
|
voiceOptions: [],
|
||||||
showFunctionDialog: false,
|
showFunctionDialog: false,
|
||||||
currentFunctions: [],
|
currentFunctions: [],
|
||||||
functionColorMap: [
|
|
||||||
'#FF6B6B', '#4ECDC4', '#45B7D1',
|
|
||||||
'#96CEB4', '#FFEEAD', '#D4A5A5', '#A2836E'
|
|
||||||
],
|
|
||||||
allFunctions: [],
|
allFunctions: [],
|
||||||
originalFunctions: [],
|
originalFunctions: [],
|
||||||
}
|
}
|
||||||
@@ -428,20 +424,16 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
getFunctionColor(name) {
|
|
||||||
const hash = [...name].reduce((acc, char) => acc + char.charCodeAt(0), 0);
|
|
||||||
return this.functionColorMap[hash % this.functionColorMap.length];
|
|
||||||
},
|
|
||||||
getFunctionDisplayChar(name) {
|
getFunctionDisplayChar(name) {
|
||||||
if (!name || name.length === 0) return '';
|
if (!name || name.length === 0) return '';
|
||||||
|
|
||||||
for (let i = 0; i < name.length; i++) {
|
for (let i = 0; i < name.length; i++) {
|
||||||
const char = name[i];
|
const char = name[i];
|
||||||
if (/[\u4e00-\u9fa5a-zA-Z0-9]/.test(char)) {
|
if (/[\u4e00-\u9fa5a-zA-Z0-9]/.test(char)) {
|
||||||
return char;
|
return char;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有找到有效字符,返回第一个字符
|
// 如果没有找到有效字符,返回第一个字符
|
||||||
return name.charAt(0);
|
return name.charAt(0);
|
||||||
},
|
},
|
||||||
@@ -768,11 +760,12 @@ export default {
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
color: white;
|
color: #5778ff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: #e6ebff;
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .el-form-item__label {
|
::v-deep .el-form-item__label {
|
||||||
|
|||||||
@@ -55,7 +55,6 @@ class IntentProvider(IntentProviderBase):
|
|||||||
prompt = (
|
prompt = (
|
||||||
"【严格格式要求】你必须只能返回JSON格式,绝对不能返回任何自然语言!\n\n"
|
"【严格格式要求】你必须只能返回JSON格式,绝对不能返回任何自然语言!\n\n"
|
||||||
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n"
|
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图并调用相应的函数。\n\n"
|
||||||
|
|
||||||
"【重要规则】以下类型的查询请直接返回result_for_context,无需调用函数:\n"
|
"【重要规则】以下类型的查询请直接返回result_for_context,无需调用函数:\n"
|
||||||
"- 询问当前时间(如:现在几点、当前时间、查询时间等)\n"
|
"- 询问当前时间(如:现在几点、当前时间、查询时间等)\n"
|
||||||
"- 询问今天日期(如:今天几号、今天星期几、今天是什么日期等)\n"
|
"- 询问今天日期(如:今天几号、今天星期几、今天是什么日期等)\n"
|
||||||
@@ -202,7 +201,7 @@ class IntentProvider(IntentProviderBase):
|
|||||||
# 记录LLM调用完成时间
|
# 记录LLM调用完成时间
|
||||||
llm_time = time.time() - llm_start_time
|
llm_time = time.time() - llm_start_time
|
||||||
logger.bind(tag=TAG).debug(
|
logger.bind(tag=TAG).debug(
|
||||||
f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒"
|
f"外挂的大模型意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 记录后处理开始时间
|
# 记录后处理开始时间
|
||||||
@@ -238,8 +237,10 @@ class IntentProvider(IntentProviderBase):
|
|||||||
# 处理不同类型的意图
|
# 处理不同类型的意图
|
||||||
if function_name == "result_for_context":
|
if function_name == "result_for_context":
|
||||||
# 处理基础信息查询,直接从context构建结果
|
# 处理基础信息查询,直接从context构建结果
|
||||||
logger.bind(tag=TAG).info("检测到result_for_context意图,将使用上下文信息直接回答")
|
logger.bind(tag=TAG).info(
|
||||||
|
"检测到result_for_context意图,将使用上下文信息直接回答"
|
||||||
|
)
|
||||||
|
|
||||||
elif function_name == "continue_chat":
|
elif function_name == "continue_chat":
|
||||||
# 处理普通对话
|
# 处理普通对话
|
||||||
# 保留非工具相关的消息
|
# 保留非工具相关的消息
|
||||||
@@ -249,7 +250,7 @@ class IntentProvider(IntentProviderBase):
|
|||||||
if msg.role not in ["tool", "function"]
|
if msg.role not in ["tool", "function"]
|
||||||
]
|
]
|
||||||
conn.dialogue.dialogue = clean_history
|
conn.dialogue.dialogue = clean_history
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 处理函数调用
|
# 处理函数调用
|
||||||
logger.bind(tag=TAG).info(f"检测到函数调用意图: {function_name}")
|
logger.bind(tag=TAG).info(f"检测到函数调用意图: {function_name}")
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.conn.sentence_id = uuid.uuid4().hex
|
self.conn.sentence_id = uuid.uuid4().hex
|
||||||
logger.bind(tag=TAG).debug(f"自动生成新的 会话ID: {self.conn.sentence_id}")
|
logger.bind(tag=TAG).debug(f"自动生成新的 会话ID: {self.conn.sentence_id}")
|
||||||
|
|
||||||
logger.bind(tag=TAG).debu("开始启动TTS会话...")
|
logger.bind(tag=TAG).debug("开始启动TTS会话...")
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self.start_session(self.conn.sentence_id),
|
self.start_session(self.conn.sentence_id),
|
||||||
loop=self.conn.loop,
|
loop=self.conn.loop,
|
||||||
|
|||||||
@@ -1430,6 +1430,13 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
log('使用AudioWorklet处理音频', 'success');
|
log('使用AudioWorklet处理音频', 'success');
|
||||||
|
|
||||||
|
// 需要连接输出,否则不会触发处理
|
||||||
|
// 我们创建一个静音通道
|
||||||
|
const silent = audioContext.createGain();
|
||||||
|
silent.gain.value = 0;
|
||||||
|
audioProcessor.connect(silent);
|
||||||
|
silent.connect(audioContext.destination);
|
||||||
return { node: audioProcessor, type: 'worklet' };
|
return { node: audioProcessor, type: 'worklet' };
|
||||||
} else {
|
} else {
|
||||||
// 使用旧版ScriptProcessorNode作为回退方案
|
// 使用旧版ScriptProcessorNode作为回退方案
|
||||||
@@ -1604,6 +1611,11 @@
|
|||||||
// 创建音频上下文和分析器
|
// 创建音频上下文和分析器
|
||||||
audioContext = getAudioContextInstance();
|
audioContext = getAudioContextInstance();
|
||||||
|
|
||||||
|
// 浏览器出于安全和性能考虑,新创建的 AudioContext 默认处于 suspended 状态,必须显式调用 resume() 才会开始处理音频。
|
||||||
|
if (audioContext.state === 'suspended') {
|
||||||
|
await audioContext.resume();
|
||||||
|
}
|
||||||
|
|
||||||
// 创建音频处理器
|
// 创建音频处理器
|
||||||
const processorResult = await createAudioProcessor();
|
const processorResult = await createAudioProcessor();
|
||||||
if (!processorResult) {
|
if (!processorResult) {
|
||||||
|
|||||||
Reference in New Issue
Block a user