mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc2fc35cc9 | ||
|
|
993d5395b2 | ||
|
|
67f0b828ea | ||
|
|
879c1267b6 | ||
|
|
46c53e36a6 | ||
|
|
09f6605cfe | ||
|
|
12cee4027a | ||
|
|
979fea0d60 | ||
|
|
94553c54bd | ||
|
|
248db31c8b | ||
|
|
24bfa1ca15 | ||
|
|
1e97a8febc | ||
|
|
2742f2e1ff | ||
|
|
0a5ae70a7c | ||
|
|
22d53bd36e | ||
|
|
ebf68929ce | ||
|
|
ef3b373211 | ||
|
|
5012a51e1d | ||
|
|
fb1f476a3c | ||
|
|
1a31c8cd1d | ||
|
|
3e491c7d79 | ||
|
|
33345817d2 | ||
|
|
82973a685a | ||
|
|
e1d245068c | ||
|
|
b74517ffb0 | ||
|
|
231ae8dfa6 | ||
|
|
4ee9a47d41 | ||
|
|
d8bf5cdedf | ||
|
|
755e0edd44 | ||
|
|
178df82693 | ||
|
|
a88c3b2032 | ||
|
|
513396b905 | ||
|
|
9885d4758b |
@@ -227,7 +227,7 @@ public interface Constant {
|
|||||||
/**
|
/**
|
||||||
* 版本号
|
* 版本号
|
||||||
*/
|
*/
|
||||||
public static final String VERSION = "0.5.6";
|
public static final String VERSION = "0.5.8";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 无效固件URL
|
* 无效固件URL
|
||||||
|
|||||||
+3
@@ -56,6 +56,9 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
|||||||
wrapper.set("tts_model_id", modelId);
|
wrapper.set("tts_model_id", modelId);
|
||||||
wrapper.set("tts_voice_id", null);
|
wrapper.set("tts_voice_id", null);
|
||||||
break;
|
break;
|
||||||
|
case "VLLM":
|
||||||
|
wrapper.set("vllm_model_id", modelId);
|
||||||
|
break;
|
||||||
case "MEMORY":
|
case "MEMORY":
|
||||||
wrapper.set("mem_model_id", modelId);
|
wrapper.set("mem_model_id", modelId);
|
||||||
break;
|
break;
|
||||||
|
|||||||
+11
-4
@@ -4,6 +4,7 @@ import java.util.List;
|
|||||||
|
|
||||||
import org.apache.commons.lang3.StringUtils;
|
import org.apache.commons.lang3.StringUtils;
|
||||||
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
import org.apache.shiro.authz.annotation.RequiresPermissions;
|
||||||
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
@@ -14,6 +15,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import xiaozhi.common.exception.ErrorCode;
|
import xiaozhi.common.exception.ErrorCode;
|
||||||
import xiaozhi.common.redis.RedisKeys;
|
import xiaozhi.common.redis.RedisKeys;
|
||||||
@@ -22,6 +24,7 @@ import xiaozhi.common.user.UserDetail;
|
|||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||||
|
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||||
import xiaozhi.modules.device.service.DeviceService;
|
import xiaozhi.modules.device.service.DeviceService;
|
||||||
import xiaozhi.modules.security.user.SecurityUser;
|
import xiaozhi.modules.security.user.SecurityUser;
|
||||||
@@ -80,15 +83,19 @@ public class DeviceController {
|
|||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/enableOta/{id}/{status}")
|
@PutMapping("/update/{id}")
|
||||||
@Operation(summary = "启用/关闭OTA自动升级")
|
@Operation(summary = "更新设备信息")
|
||||||
@RequiresPermissions("sys:role:normal")
|
@RequiresPermissions("sys:role:normal")
|
||||||
public Result<Void> enableOtaUpgrade(@PathVariable String id, @PathVariable Integer status) {
|
public Result<Void> updateDeviceInfo(@PathVariable String id, @Valid @RequestBody DeviceUpdateDTO deviceUpdateDTO) {
|
||||||
DeviceEntity entity = deviceService.selectById(id);
|
DeviceEntity entity = deviceService.selectById(id);
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return new Result<Void>().error("设备不存在");
|
return new Result<Void>().error("设备不存在");
|
||||||
}
|
}
|
||||||
entity.setAutoUpdate(status);
|
UserDetail user = SecurityUser.getUser();
|
||||||
|
if (!entity.getUserId().equals(user.getId())) {
|
||||||
|
return new Result<Void>().error("设备不存在");
|
||||||
|
}
|
||||||
|
BeanUtils.copyProperties(deviceUpdateDTO, entity);
|
||||||
deviceService.updateById(entity);
|
deviceService.updateById(entity);
|
||||||
return new Result<Void>();
|
return new Result<Void>();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package xiaozhi.modules.device.dto;
|
||||||
|
|
||||||
|
import jakarta.validation.constraints.Max;
|
||||||
|
import jakarta.validation.constraints.Min;
|
||||||
|
import jakarta.validation.constraints.Size;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备更新DTO
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class DeviceUpdateDTO implements Serializable {
|
||||||
|
/**
|
||||||
|
* 自动更新状态
|
||||||
|
*/
|
||||||
|
@Max(1)
|
||||||
|
@Min(0)
|
||||||
|
private Integer autoUpdate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设备别名
|
||||||
|
*/
|
||||||
|
@Size(max = 64)
|
||||||
|
private String alias;
|
||||||
|
|
||||||
|
private static final long serialVersionUID = 1L;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE ai_agent_plugin_mapping CONVERT TO CHARACTER SET utf8mb4;
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
-- LLM意图识别配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = 'LLM意图识别配置说明:
|
||||||
|
1. 使用独立的LLM进行意图识别
|
||||||
|
2. 默认使用selected_module.LLM的模型
|
||||||
|
3. 可以配置使用独立的LLM(如免费的ChatGLMLLM)
|
||||||
|
4. 通用性强,但会增加处理时间
|
||||||
|
配置说明:
|
||||||
|
1. 在llm字段中指定使用的LLM模型
|
||||||
|
2. 如果不指定,则使用selected_module.LLM的模型' WHERE `id` = 'Intent_intent_llm';
|
||||||
|
|
||||||
|
-- 函数调用意图识别配置说明
|
||||||
|
UPDATE `ai_model_config` SET
|
||||||
|
`doc_link` = NULL,
|
||||||
|
`remark` = '函数调用意图识别配置说明:
|
||||||
|
1. 使用LLM的function_call功能进行意图识别
|
||||||
|
2. 需要所选择的LLM支持function_call
|
||||||
|
3. 按需调用工具,处理速度快' WHERE `id` = 'Intent_function_call';
|
||||||
@@ -205,3 +205,17 @@ databaseChangeLog:
|
|||||||
- sqlFile:
|
- sqlFile:
|
||||||
encoding: utf8
|
encoding: utf8
|
||||||
path: classpath:db/changelog/202506080955.sql
|
path: classpath:db/changelog/202506080955.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202506161101
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202506161101.sql
|
||||||
|
- changeSet:
|
||||||
|
id: 202506191643
|
||||||
|
author: hrz
|
||||||
|
changes:
|
||||||
|
- sqlFile:
|
||||||
|
encoding: utf8
|
||||||
|
path: classpath:db/changelog/202506191643.sql
|
||||||
@@ -51,10 +51,11 @@ export default {
|
|||||||
});
|
});
|
||||||
}).send();
|
}).send();
|
||||||
},
|
},
|
||||||
enableOtaUpgrade(id, status, callback) {
|
updateDeviceInfo(id, payload, callback) {
|
||||||
RequestService.sendRequest()
|
RequestService.sendRequest()
|
||||||
.url(`${getServiceUrl()}/device/enableOta/${id}/${status}`)
|
.url(`${getServiceUrl()}/device/update/${id}`)
|
||||||
.method('PUT')
|
.method('PUT')
|
||||||
|
.data(payload)
|
||||||
.success((res) => {
|
.success((res) => {
|
||||||
RequestService.clearRequestTime()
|
RequestService.clearRequestTime()
|
||||||
callback(res)
|
callback(res)
|
||||||
@@ -63,7 +64,7 @@ export default {
|
|||||||
console.error('更新OTA状态失败:', err)
|
console.error('更新OTA状态失败:', err)
|
||||||
this.$message.error(err.msg || '更新OTA状态失败')
|
this.$message.error(err.msg || '更新OTA状态失败')
|
||||||
RequestService.reAjaxFun(() => {
|
RequestService.reAjaxFun(() => {
|
||||||
this.enableOtaUpgrade(id, status, callback)
|
this.updateDeviceInfo(id, payload, callback)
|
||||||
})
|
})
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar/>
|
||||||
|
|
||||||
<div class="operation-bar">
|
<div class="operation-bar">
|
||||||
<h2 class="page-title">设备管理</h2>
|
<h2 class="page-title">设备管理</h2>
|
||||||
<div class="right-operations">
|
<div class="right-operations">
|
||||||
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
<el-input placeholder="请输入设备型号或Mac地址查询" v-model="searchKeyword" class="search-input"
|
||||||
@keyup.enter.native="handleSearch" clearable />
|
@keyup.enter.native="handleSearch" clearable/>
|
||||||
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
<el-button class="btn-search" @click="handleSearch">搜索</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -16,8 +16,9 @@
|
|||||||
<div class="content-area">
|
<div class="content-area">
|
||||||
<el-card class="device-card" shadow="never">
|
<el-card class="device-card" shadow="never">
|
||||||
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
||||||
:header-cell-class-name="headerCellClassName" v-loading="loading" element-loading-text="拼命加载中"
|
:header-cell-class-name="headerCellClassName" v-loading="loading"
|
||||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
element-loading-text="拼命加载中"
|
||||||
|
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||||
<el-table-column label="选择" align="center" width="120">
|
<el-table-column label="选择" align="center" width="120">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
<el-checkbox v-model="scope.row.selected"></el-checkbox>
|
||||||
@@ -33,22 +34,32 @@
|
|||||||
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
<el-table-column label="绑定时间" prop="bindTime" align="center"></el-table-column>
|
||||||
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
<el-table-column label="最近对话" prop="lastConversation" align="center"></el-table-column>
|
||||||
<el-table-column label="备注" align="center">
|
<el-table-column label="备注" align="center">
|
||||||
<template slot-scope="scope">
|
<template #default="{ row }">
|
||||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
<el-input
|
||||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
v-show="row.isEdit"
|
||||||
<span v-else>
|
v-model="row.remark"
|
||||||
<i v-if="!scope.row.remark" class="el-icon-edit"
|
size="mini"
|
||||||
@click="startEditRemark(scope.$index, scope.row)"></i>
|
maxlength="64"
|
||||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
show-word-limit
|
||||||
{{ scope.row.remark }}
|
@blur="onRemarkBlur(row)"
|
||||||
</span>
|
@keyup.enter.native="onRemarkEnter(row)"
|
||||||
|
/>
|
||||||
|
<span v-show="!row.isEdit" class="remark-view">
|
||||||
|
<i
|
||||||
|
class="el-icon-edit"
|
||||||
|
@click="row.isEdit = true"
|
||||||
|
style="cursor: pointer;"
|
||||||
|
></i>
|
||||||
|
<span @click="row.isEdit = true">
|
||||||
|
{{ row.remark || '—' }}
|
||||||
</span>
|
</span>
|
||||||
|
</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="OTA升级" align="center">
|
<el-table-column label="OTA升级" align="center">
|
||||||
<template slot-scope="scope">
|
<template slot-scope="scope">
|
||||||
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
<el-switch v-model="scope.row.otaSwitch" size="mini" active-color="#13ce66" inactive-color="#ff4949"
|
||||||
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
@change="handleOtaSwitchChange(scope.row)"></el-switch>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
<el-table-column label="操作" align="center">
|
<el-table-column label="操作" align="center">
|
||||||
@@ -78,7 +89,7 @@
|
|||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goFirst">首页</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
<button class="pagination-btn" :disabled="currentPage === 1" @click="goPrev">上一页</button>
|
||||||
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
<button v-for="page in visiblePages" :key="page" class="pagination-btn"
|
||||||
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
:class="{ active: page === currentPage }" @click="goToPage(page)">
|
||||||
{{ page }}
|
{{ page }}
|
||||||
</button>
|
</button>
|
||||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||||
@@ -91,7 +102,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||||
@refresh="fetchBindDevices(currentAgentId)" />
|
@refresh="fetchBindDevices(currentAgentId)"/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -102,7 +113,7 @@ import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
|||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { HeaderBar, AddDeviceDialog },
|
components: {HeaderBar, AddDeviceDialog},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
@@ -125,18 +136,15 @@ export default {
|
|||||||
const keyword = this.activeSearchKeyword.toLowerCase();
|
const keyword = this.activeSearchKeyword.toLowerCase();
|
||||||
if (!keyword) return this.deviceList;
|
if (!keyword) return this.deviceList;
|
||||||
return this.deviceList.filter(device =>
|
return this.deviceList.filter(device =>
|
||||||
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
paginatedDeviceList() {
|
paginatedDeviceList() {
|
||||||
const start = (this.currentPage - 1) * this.pageSize;
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
const end = start + this.pageSize;
|
const end = start + this.pageSize;
|
||||||
return this.filteredDeviceList.slice(start, end).map(item => ({
|
return this.filteredDeviceList.slice(start, end);
|
||||||
...item,
|
|
||||||
selected: false
|
|
||||||
}));
|
|
||||||
},
|
},
|
||||||
pageCount() {
|
pageCount() {
|
||||||
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
||||||
@@ -212,11 +220,10 @@ export default {
|
|||||||
this.batchUnbindDevices(deviceIds);
|
this.batchUnbindDevices(deviceIds);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
batchUnbindDevices(deviceIds) {
|
batchUnbindDevices(deviceIds) {
|
||||||
const promises = deviceIds.map(id => {
|
const promises = deviceIds.map(id => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
Api.device.unbindDevice(id, ({ data }) => {
|
Api.device.unbindDevice(id, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
resolve();
|
resolve();
|
||||||
} else {
|
} else {
|
||||||
@@ -225,33 +232,61 @@ export default {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
Promise.all(promises)
|
Promise.all(promises)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: `成功解绑 ${deviceIds.length} 台设备`,
|
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||||
showClose: true
|
showClose: true
|
||||||
|
});
|
||||||
|
this.fetchBindDevices(this.currentAgentId);
|
||||||
|
this.selectedDevices = [];
|
||||||
|
this.isAllSelected = false;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
this.$message.error({
|
||||||
|
message: error || '批量解绑过程中出现错误',
|
||||||
|
showClose: true
|
||||||
|
});
|
||||||
});
|
});
|
||||||
this.fetchBindDevices(this.currentAgentId);
|
|
||||||
this.selectedDevices = [];
|
|
||||||
this.isAllSelected = false;
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
this.$message.error({
|
|
||||||
message: error || '批量解绑过程中出现错误',
|
|
||||||
showClose: true
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
handleAddDevice() {
|
handleAddDevice() {
|
||||||
this.addDeviceDialogVisible = true;
|
this.addDeviceDialogVisible = true;
|
||||||
},
|
},
|
||||||
startEditRemark(index, row) {
|
submitRemark(row) {
|
||||||
this.deviceList[index].isEdit = true;
|
if (row._submitting) return;
|
||||||
|
|
||||||
|
const text = (row.remark || '').trim();
|
||||||
|
if (text.length > 64) {
|
||||||
|
this.$message.warning('备注不能超过 64 字符');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (text === row._originalRemark) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
row._submitting = true;
|
||||||
|
this.updateDeviceInfo(row.device_id, { alias: text }, (ok, resp) => {
|
||||||
|
if (ok) {
|
||||||
|
row._originalRemark = text;
|
||||||
|
this.$message.success('备注已保存');
|
||||||
|
} else {
|
||||||
|
row.remark = row._originalRemark;
|
||||||
|
this.$message.error(resp.msg || '备注保存失败');
|
||||||
|
}
|
||||||
|
row._submitting = false;
|
||||||
|
});
|
||||||
},
|
},
|
||||||
stopEditRemark(index) {
|
// 备注输入框:失焦时提交
|
||||||
this.deviceList[index].isEdit = false;
|
onRemarkBlur(row) {
|
||||||
|
row.isEdit = false;
|
||||||
|
setTimeout(() => {
|
||||||
|
this.submitRemark(row);
|
||||||
|
}, 100); // 延迟 100ms,避开 enter+blur 同时触发的窗口
|
||||||
|
},
|
||||||
|
// 备注输入框:按回车时提交
|
||||||
|
onRemarkEnter(row) {
|
||||||
|
row.isEdit = false;
|
||||||
|
this.submitRemark(row);
|
||||||
},
|
},
|
||||||
handleUnbind(device_id) {
|
handleUnbind(device_id) {
|
||||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||||
@@ -259,7 +294,7 @@ export default {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
Api.device.unbindDevice(device_id, ({ data }) => {
|
Api.device.unbindDevice(device_id, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '设备解绑成功',
|
message: '设备解绑成功',
|
||||||
@@ -290,7 +325,7 @@ export default {
|
|||||||
|
|
||||||
fetchBindDevices(agentId) {
|
fetchBindDevices(agentId) {
|
||||||
this.loading = true;
|
this.loading = true;
|
||||||
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
Api.device.getAgentBindDevices(agentId, ({data}) => {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.deviceList = data.data.map(device => {
|
this.deviceList = data.data.map(device => {
|
||||||
@@ -302,12 +337,14 @@ export default {
|
|||||||
bindTime: device.createDate,
|
bindTime: device.createDate,
|
||||||
lastConversation: device.lastConnectedAt,
|
lastConversation: device.lastConnectedAt,
|
||||||
remark: device.alias,
|
remark: device.alias,
|
||||||
|
_originalRemark: device.alias,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
|
_submitting: false,
|
||||||
otaSwitch: device.autoUpdate === 1,
|
otaSwitch: device.autoUpdate === 1,
|
||||||
rawBindTime: new Date(device.createDate).getTime()
|
rawBindTime: new Date(device.createDate).getTime()
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||||
this.activeSearchKeyword = "";
|
this.activeSearchKeyword = "";
|
||||||
this.searchKeyword = "";
|
this.searchKeyword = "";
|
||||||
} else {
|
} else {
|
||||||
@@ -315,7 +352,7 @@ export default {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
headerCellClassName({ columnIndex }) {
|
headerCellClassName({columnIndex}) {
|
||||||
if (columnIndex === 0) {
|
if (columnIndex === 0) {
|
||||||
return "custom-selection-header";
|
return "custom-selection-header";
|
||||||
}
|
}
|
||||||
@@ -325,14 +362,19 @@ export default {
|
|||||||
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
const firmwareType = this.firmwareTypes.find(item => item.key === type)
|
||||||
return firmwareType ? firmwareType.name : type
|
return firmwareType ? firmwareType.name : type
|
||||||
},
|
},
|
||||||
|
updateDeviceInfo(device_id, payload, callback) {
|
||||||
|
return Api.device.updateDeviceInfo(device_id, payload, ({data}) => {
|
||||||
|
callback(data.code === 0, data);
|
||||||
|
})
|
||||||
|
},
|
||||||
handleOtaSwitchChange(row) {
|
handleOtaSwitchChange(row) {
|
||||||
Api.device.enableOtaUpgrade(row.device_id, row.otaSwitch ? 1 : 0, ({ data }) => {
|
this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => {
|
||||||
if (data.code === 0) {
|
if (result) {
|
||||||
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级')
|
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级');
|
||||||
} else {
|
return;
|
||||||
row.otaSwitch = !row.otaSwitch
|
|
||||||
this.$message.error(data.msg || '操作失败')
|
|
||||||
}
|
}
|
||||||
|
row.otaSwitch = !row.otaSwitch
|
||||||
|
this.$message.error(msg || '操作失败')
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -645,7 +687,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:deep(.el-table .el-button--text) {
|
:deep(.el-table .el-button--text) {
|
||||||
color: #7079aa;
|
color: #7079aa;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
|||||||
from config.settings import check_config_file
|
from config.settings import check_config_file
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
|
||||||
SERVER_VERSION = "0.5.6"
|
SERVER_VERSION = "0.5.8"
|
||||||
_logger_initialized = False
|
_logger_initialized = False
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from config.config_loader import get_private_config_from_api
|
|||||||
from core.utils.auth import AuthToken
|
from core.utils.auth import AuthToken
|
||||||
import base64
|
import base64
|
||||||
from typing import Tuple, Optional
|
from typing import Tuple, Optional
|
||||||
|
from plugins_func.register import Action
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
@@ -122,7 +123,8 @@ class VisionHandler:
|
|||||||
|
|
||||||
return_json = {
|
return_json = {
|
||||||
"success": True,
|
"success": True,
|
||||||
"result": result,
|
"action": Action.RESPONSE.name,
|
||||||
|
"response": result,
|
||||||
}
|
}
|
||||||
|
|
||||||
response = web.Response(
|
response = web.Response(
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ class ConnectionHandler:
|
|||||||
self.client_have_voice_last_time = 0.0
|
self.client_have_voice_last_time = 0.0
|
||||||
self.client_no_voice_last_time = 0.0
|
self.client_no_voice_last_time = 0.0
|
||||||
self.client_voice_stop = False
|
self.client_voice_stop = False
|
||||||
|
self.client_voice_frame_count = 0
|
||||||
|
|
||||||
# asr相关变量
|
# asr相关变量
|
||||||
# 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR
|
# 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR
|
||||||
@@ -627,8 +628,8 @@ class ConnectionHandler:
|
|||||||
)
|
)
|
||||||
memory_str = future.result()
|
memory_str = future.result()
|
||||||
|
|
||||||
uuid_str = str(uuid.uuid4()).replace("-", "")
|
self.sentence_id = str(uuid.uuid4().hex)
|
||||||
self.sentence_id = uuid_str
|
|
||||||
|
|
||||||
if self.intent_type == "function_call" and functions is not None:
|
if self.intent_type == "function_call" and functions is not None:
|
||||||
# 使用支持functions的streaming接口
|
# 使用支持functions的streaming接口
|
||||||
@@ -751,9 +752,31 @@ class ConnectionHandler:
|
|||||||
self.loop,
|
self.loop,
|
||||||
).result()
|
).result()
|
||||||
self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}")
|
self.logger.bind(tag=TAG).debug(f"MCP工具调用结果: {result}")
|
||||||
result = ActionResponse(
|
|
||||||
action=Action.REQLLM, result=result, response=""
|
resultJson = None
|
||||||
)
|
if isinstance(result, str):
|
||||||
|
try:
|
||||||
|
resultJson = json.loads(result)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"解析MCP工具返回结果失败: {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 视觉大模型不经过二次LLM处理
|
||||||
|
if (
|
||||||
|
resultJson is not None
|
||||||
|
and isinstance(resultJson, dict)
|
||||||
|
and "action" in resultJson
|
||||||
|
):
|
||||||
|
result = ActionResponse(
|
||||||
|
action=Action[resultJson["action"]],
|
||||||
|
result=None,
|
||||||
|
response=resultJson.get("response", ""),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = ActionResponse(
|
||||||
|
action=Action.REQLLM, result=result, response=""
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
|
self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
|
||||||
result = ActionResponse(
|
result = ActionResponse(
|
||||||
|
|||||||
@@ -22,6 +22,10 @@ class FunctionHandler:
|
|||||||
self.register_config_functions()
|
self.register_config_functions()
|
||||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||||
self.finish_init = True
|
self.finish_init = True
|
||||||
|
|
||||||
|
def upload_functions_desc(self):
|
||||||
|
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||||
|
|
||||||
|
|
||||||
def current_support_functions(self):
|
def current_support_functions(self):
|
||||||
func_names = []
|
func_names = []
|
||||||
|
|||||||
@@ -338,6 +338,8 @@ async def handleIotDescriptors(conn, descriptors):
|
|||||||
|
|
||||||
# 如果注册了新函数,更新function描述列表
|
# 如果注册了新函数,更新function描述列表
|
||||||
if functions_changed and hasattr(conn, "func_handler"):
|
if functions_changed and hasattr(conn, "func_handler"):
|
||||||
|
conn.func_handler.upload_functions_desc()
|
||||||
|
|
||||||
func_names = conn.func_handler.current_support_functions()
|
func_names = conn.func_handler.current_support_functions()
|
||||||
conn.logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
conn.logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||||
conn.logger.bind(tag=TAG).info(
|
conn.logger.bind(tag=TAG).info(
|
||||||
|
|||||||
@@ -80,6 +80,27 @@ class MCPClient:
|
|||||||
fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop)
|
fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||||
return await asyncio.wrap_future(fut)
|
return await asyncio.wrap_future(fut)
|
||||||
|
|
||||||
|
def is_connected(self) -> bool:
|
||||||
|
"""检查MCP客户端是否连接正常
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: 如果客户端已连接并正常工作,返回True,否则返回False
|
||||||
|
"""
|
||||||
|
# 检查工作任务是否存在
|
||||||
|
if self._worker_task is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查工作任务是否已经完成或取消
|
||||||
|
if self._worker_task.done():
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 检查会话是否存在
|
||||||
|
if self.session is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 所有检查都通过,连接正常
|
||||||
|
return True
|
||||||
|
|
||||||
async def _worker(self):
|
async def _worker(self):
|
||||||
async with AsyncExitStack() as stack:
|
async with AsyncExitStack() as stack:
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ class MCPManager:
|
|||||||
self.conn.logger.bind(tag=TAG).error(
|
self.conn.logger.bind(tag=TAG).error(
|
||||||
f"Failed to initialize MCP server {name}: {e}"
|
f"Failed to initialize MCP server {name}: {e}"
|
||||||
)
|
)
|
||||||
|
self.conn.func_handler.upload_functions_desc()
|
||||||
|
|
||||||
def get_all_tools(self) -> List[Dict[str, Any]]:
|
def get_all_tools(self) -> List[Dict[str, Any]]:
|
||||||
"""获取所有服务的工具function定义
|
"""获取所有服务的工具function定义
|
||||||
@@ -99,7 +100,7 @@ class MCPManager:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
||||||
"""执行工具调用
|
"""执行工具调用,失败时会尝试重新连接
|
||||||
Args:
|
Args:
|
||||||
tool_name: 工具名称
|
tool_name: 工具名称
|
||||||
arguments: 工具参数
|
arguments: 工具参数
|
||||||
@@ -111,11 +112,64 @@ class MCPManager:
|
|||||||
self.conn.logger.bind(tag=TAG).info(
|
self.conn.logger.bind(tag=TAG).info(
|
||||||
f"Executing tool {tool_name} with arguments: {arguments}"
|
f"Executing tool {tool_name} with arguments: {arguments}"
|
||||||
)
|
)
|
||||||
for client in self.client.values():
|
|
||||||
|
max_retries = 3 # 最大重试次数
|
||||||
|
retry_interval = 2 # 重试间隔(秒)
|
||||||
|
|
||||||
|
# 找到对应的客户端
|
||||||
|
client_name = None
|
||||||
|
target_client = None
|
||||||
|
for name, client in self.client.items():
|
||||||
if client.has_tool(tool_name):
|
if client.has_tool(tool_name):
|
||||||
return await client.call_tool(tool_name, arguments)
|
client_name = name
|
||||||
|
target_client = client
|
||||||
raise ValueError(f"Tool {tool_name} not found in any MCP server")
|
break
|
||||||
|
|
||||||
|
if not target_client:
|
||||||
|
raise ValueError(f"Tool {tool_name} not found in any MCP server")
|
||||||
|
|
||||||
|
# 带重试机制的工具调用
|
||||||
|
for attempt in range(max_retries):
|
||||||
|
try:
|
||||||
|
return await target_client.call_tool(tool_name, arguments)
|
||||||
|
except Exception as e:
|
||||||
|
# 最后一次尝试失败时直接抛出异常
|
||||||
|
if attempt == max_retries - 1:
|
||||||
|
raise
|
||||||
|
|
||||||
|
self.conn.logger.bind(tag=TAG).warning(
|
||||||
|
f"执行工具 {tool_name} 失败 (尝试 {attempt+1}/{max_retries}): {e}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 尝试重新连接
|
||||||
|
self.conn.logger.bind(tag=TAG).info(
|
||||||
|
f"重试前尝试重新连接 MCP 客户端 {client_name}"
|
||||||
|
)
|
||||||
|
try:
|
||||||
|
# 关闭旧的连接
|
||||||
|
await target_client.cleanup()
|
||||||
|
|
||||||
|
# 重新初始化客户端
|
||||||
|
config = self.load_config()
|
||||||
|
if client_name in config:
|
||||||
|
client = MCPClient(config[client_name])
|
||||||
|
await client.initialize()
|
||||||
|
self.client[client_name] = client
|
||||||
|
target_client = client
|
||||||
|
self.conn.logger.bind(tag=TAG).info(
|
||||||
|
f"成功重新连接 MCP 客户端: {client_name}"
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.conn.logger.bind(tag=TAG).error(
|
||||||
|
f"Cannot reconnect MCP client {client_name}: config not found"
|
||||||
|
)
|
||||||
|
except Exception as reconnect_error:
|
||||||
|
self.conn.logger.bind(tag=TAG).error(
|
||||||
|
f"Failed to reconnect MCP client {client_name}: {reconnect_error}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 等待一段时间再重试
|
||||||
|
await asyncio.sleep(retry_interval)
|
||||||
|
|
||||||
async def cleanup_all(self) -> None:
|
async def cleanup_all(self) -> None:
|
||||||
"""依次关闭所有 MCPClient,不让异常阻断整体流程。"""
|
"""依次关闭所有 MCPClient,不让异常阻断整体流程。"""
|
||||||
|
|||||||
@@ -93,9 +93,7 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
# 检查初始化响应
|
# 检查初始化响应
|
||||||
if "code" in result and result["code"] != 1000:
|
if "code" in result and result["code"] != 1000:
|
||||||
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('message', '未知错误')}"
|
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('error', '未知错误')}"
|
||||||
if "payload_msg" in result:
|
|
||||||
error_msg += f"\n详细错误信息: {json.dumps(result['payload_msg'], ensure_ascii=False)}"
|
|
||||||
logger.bind(tag=TAG).error(error_msg)
|
logger.bind(tag=TAG).error(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
@@ -256,7 +254,6 @@ class ASRProvider(ASRProviderBase):
|
|||||||
"X-Api-Access-Key": self.access_token,
|
"X-Api-Access-Key": self.access_token,
|
||||||
"X-Api-Resource-Id": "volc.bigasr.sauc.duration",
|
"X-Api-Resource-Id": "volc.bigasr.sauc.duration",
|
||||||
"X-Api-Connect-Id": str(uuid.uuid4()),
|
"X-Api-Connect-Id": str(uuid.uuid4()),
|
||||||
"Host": "openspeech.bytedance.com",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def generate_header(
|
def generate_header(
|
||||||
@@ -309,9 +306,14 @@ class ASRProvider(ASRProviderBase):
|
|||||||
|
|
||||||
# 如果是错误响应
|
# 如果是错误响应
|
||||||
if message_type == 0x0F: # SERVER_ERROR_RESPONSE
|
if message_type == 0x0F: # SERVER_ERROR_RESPONSE
|
||||||
code = int.from_bytes(header[4:8], "big", signed=False)
|
code = int.from_bytes(res[4:8], "big", signed=False)
|
||||||
error_msg = res[8:].decode("utf-8")
|
msg_length = int.from_bytes(res[8:12], "big", signed=False)
|
||||||
return {"code": code, "error": error_msg}
|
error_msg = json.loads(res[12:].decode("utf-8"))
|
||||||
|
return {
|
||||||
|
"code": code,
|
||||||
|
"msg_length": msg_length,
|
||||||
|
"payload_msg": error_msg,
|
||||||
|
}
|
||||||
|
|
||||||
# 获取JSON数据(跳过12字节头部)
|
# 获取JSON数据(跳过12字节头部)
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -172,7 +172,11 @@ class IntentProvider(IntentProviderBase):
|
|||||||
music_file_names = music_config["music_file_names"]
|
music_file_names = music_config["music_file_names"]
|
||||||
prompt_music = f"{self.promot}\n<musicNames>{music_file_names}\n</musicNames>"
|
prompt_music = f"{self.promot}\n<musicNames>{music_file_names}\n</musicNames>"
|
||||||
|
|
||||||
devices = conn.config["plugins"]["home_assistant"].get("devices", [])
|
home_assistant_cfg = conn.config["plugins"].get("home_assistant")
|
||||||
|
if home_assistant_cfg:
|
||||||
|
devices = home_assistant_cfg.get("devices", [])
|
||||||
|
else:
|
||||||
|
devices = []
|
||||||
if len(devices) > 0:
|
if len(devices) > 0:
|
||||||
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||||
for device in devices:
|
for device in devices:
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os
|
|||||||
import yaml
|
import yaml
|
||||||
from config.config_loader import get_project_dir
|
from config.config_loader import get_project_dir
|
||||||
from config.manage_api_client import save_mem_local_short
|
from config.manage_api_client import save_mem_local_short
|
||||||
|
from core.utils.util import check_model_key
|
||||||
|
|
||||||
|
|
||||||
short_term_memory_prompt = """
|
short_term_memory_prompt = """
|
||||||
@@ -145,6 +146,10 @@ class MemoryProvider(MemoryProviderBase):
|
|||||||
# 打印使用的模型信息
|
# 打印使用的模型信息
|
||||||
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
|
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
|
||||||
logger.bind(tag=TAG).debug(f"使用记忆保存模型: {model_info}")
|
logger.bind(tag=TAG).debug(f"使用记忆保存模型: {model_info}")
|
||||||
|
api_key = getattr(self.llm, "api_key", None)
|
||||||
|
memory_key_msg = check_model_key("记忆总结专用LLM", api_key)
|
||||||
|
if memory_key_msg:
|
||||||
|
logger.bind(tag=TAG).error(memory_key_msg)
|
||||||
if self.llm is None:
|
if self.llm is None:
|
||||||
logger.bind(tag=TAG).error("LLM is not set for memory provider")
|
logger.bind(tag=TAG).error("LLM is not set for memory provider")
|
||||||
return None
|
return None
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ from core.utils.util import check_model_key
|
|||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
from core.handle.abortHandle import handleAbortMessage
|
from core.handle.abortHandle import handleAbortMessage
|
||||||
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
|
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
|
||||||
|
from asyncio import Task
|
||||||
|
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -141,6 +143,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.ws = None
|
self.ws = None
|
||||||
self.interface_type = InterfaceType.DUAL_STREAM
|
self.interface_type = InterfaceType.DUAL_STREAM
|
||||||
|
self._monitor_task = None # 监听任务引用
|
||||||
self.appId = config.get("appid")
|
self.appId = config.get("appid")
|
||||||
self.access_token = config.get("access_token")
|
self.access_token = config.get("access_token")
|
||||||
self.cluster = config.get("cluster")
|
self.cluster = config.get("cluster")
|
||||||
@@ -270,8 +273,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
try:
|
try:
|
||||||
# 建立新连接
|
# 建立新连接
|
||||||
if self.ws is None:
|
if self.ws is None:
|
||||||
await handleAbortMessage(self.conn)
|
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
|
||||||
logger.bind(tag=TAG).error(f"WebSocket连接不存在,终止发送文本")
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# 过滤Markdown
|
# 过滤Markdown
|
||||||
@@ -293,6 +295,25 @@ class TTSProvider(TTSProviderBase):
|
|||||||
async def start_session(self, session_id):
|
async def start_session(self, session_id):
|
||||||
logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
|
logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
|
||||||
try:
|
try:
|
||||||
|
task = self._monitor_task
|
||||||
|
if (
|
||||||
|
task is not None
|
||||||
|
and isinstance(task, Task)
|
||||||
|
and not task.done()
|
||||||
|
):
|
||||||
|
logger.bind(tag=TAG).info("等待上一个监听任务结束...")
|
||||||
|
if self.ws is not None:
|
||||||
|
logger.bind(tag=TAG).info("强制关闭上一个WebSocket连接以唤醒监听任务...")
|
||||||
|
try:
|
||||||
|
await self.ws.close()
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).warning(f"关闭上一个ws异常: {e}")
|
||||||
|
self.ws = None
|
||||||
|
try:
|
||||||
|
await asyncio.wait_for(task, timeout=8)
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).warning(f"等待监听任务异常: {e}")
|
||||||
|
self._monitor_task = None
|
||||||
# 建立新连接
|
# 建立新连接
|
||||||
await self._ensure_connection()
|
await self._ensure_connection()
|
||||||
|
|
||||||
@@ -463,6 +484,8 @@ class TTSProvider(TTSProviderBase):
|
|||||||
except:
|
except:
|
||||||
pass
|
pass
|
||||||
self.ws = None
|
self.ws = None
|
||||||
|
# 监听任务退出时清理引用
|
||||||
|
self._monitor_task = None
|
||||||
|
|
||||||
async def send_event(
|
async def send_event(
|
||||||
self,
|
self,
|
||||||
|
|||||||
@@ -35,6 +35,10 @@ class VADProvider(VADProviderBase):
|
|||||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||||
|
|
||||||
|
# 初始化帧计数器
|
||||||
|
if not hasattr(conn, "client_voice_frame_count"):
|
||||||
|
conn.client_voice_frame_count = 0
|
||||||
|
|
||||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||||
client_have_voice = False
|
client_have_voice = False
|
||||||
while len(conn.client_audio_buffer) >= 512 * 2:
|
while len(conn.client_audio_buffer) >= 512 * 2:
|
||||||
@@ -50,7 +54,15 @@ class VADProvider(VADProviderBase):
|
|||||||
# 检测语音活动
|
# 检测语音活动
|
||||||
with torch.no_grad():
|
with torch.no_grad():
|
||||||
speech_prob = self.model(audio_tensor, 16000).item()
|
speech_prob = self.model(audio_tensor, 16000).item()
|
||||||
client_have_voice = speech_prob >= self.vad_threshold
|
is_voice = speech_prob >= self.vad_threshold
|
||||||
|
|
||||||
|
if is_voice:
|
||||||
|
conn.client_voice_frame_count += 1
|
||||||
|
else:
|
||||||
|
conn.client_voice_frame_count = 0
|
||||||
|
|
||||||
|
# 只有连续4帧检测到语音才认为有语音
|
||||||
|
client_have_voice = conn.client_voice_frame_count >= 4
|
||||||
|
|
||||||
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
|
# 如果之前有声音,但本次没有声音,且与上次有声音的时间差已经超过了静默阈值,则认为已经说完一句话
|
||||||
if conn.client_have_voice and not client_have_voice:
|
if conn.client_have_voice and not client_have_voice:
|
||||||
|
|||||||
@@ -31,7 +31,8 @@ hass_set_state_function_desc = {
|
|||||||
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false",
|
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false",
|
||||||
},
|
},
|
||||||
"rgb_color": {
|
"rgb_color": {
|
||||||
"type": "list",
|
"type": "array",
|
||||||
|
"items": {"type": "integer"},
|
||||||
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值",
|
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user