Compare commits

...
33 Commits
Author SHA1 Message Date
欣南科技andGitHub bc2fc35cc9 Merge pull request #1642 from xinnan-tech/py_tts_listen
update:更新版本号
2025-06-20 17:42:53 +08:00
hrz 993d5395b2 update:更新版本号 2025-06-20 17:42:14 +08:00
欣南科技andGitHub 67f0b828ea Merge pull request #1641 from xinnan-tech/py_tts_listen
update:修复部分doubaoasr出现400错误的问题
2025-06-20 17:20:51 +08:00
hrz 879c1267b6 update:修复部分doubaoasr出现400错误的问题 2025-06-20 17:20:08 +08:00
hrzandGitHub 46c53e36a6 Merge pull request #1640 from xinnan-tech/py_tts_listen
fix: 反复打断任务没有被清除,vad四帧语音识别
2025-06-20 17:17:19 +08:00
hrz 09f6605cfe update:更新版本号 2025-06-20 17:17:09 +08:00
Sakura-RanChen 12cee4027a fix: 反复打断任务没有被清除,vad四帧语音识别 2025-06-20 16:25:08 +08:00
欣南科技andGitHub 979fea0d60 Merge pull request #1627 from xinnan-tech/update-remark
更新智控台两款意图识别的说明
2025-06-19 16:56:38 +08:00
hrz 94553c54bd 更新智控台两款意图识别的说明 2025-06-19 16:53:39 +08:00
hrzandGitHub 248db31c8b Merge pull request #1618 from xinnan-tech/py_bug_fix
update:添加MCP重连机制
2025-06-18 22:40:25 +08:00
hrzandGitHub 24bfa1ca15 Merge pull request #1615 from xinnan-tech/py_fix_type
fix: 豆包流式decode错误
2025-06-18 17:46:47 +08:00
欣南科技andGitHub 1e97a8febc Merge pull request #1620 from xinnan-tech/hot-fix
update:修复mcp返回json的bug
2025-06-18 17:28:51 +08:00
hrz 2742f2e1ff update:修复mcp返回json的bug 2025-06-18 17:28:21 +08:00
Sakura-RanChen 0a5ae70a7c 二次错误提醒 2025-06-18 16:32:58 +08:00
CGD 22d53bd36e update:添加MCP重连机制 2025-06-18 16:29:28 +08:00
Sakura-RanChen ebf68929ce update: 错误信息读取 2025-06-18 14:03:18 +08:00
Sakura-RanChen ef3b373211 fix: 豆包流式decode错误 2025-06-18 11:06:11 +08:00
hrzandGitHub 5012a51e1d Merge pull request #1609 from xinnan-tech/py_fix_type
Py fix type
2025-06-18 09:39:38 +08:00
Sakura-RanChen fb1f476a3c fix: 智控台切换默认视觉模型报错 2025-06-18 09:24:52 +08:00
Sakura-RanChen 1a31c8cd1d update: 视觉模块直接识别输出,不经过二次LLM 2025-06-17 16:13:24 +08:00
欣南科技andGitHub 3e491c7d79 Merge pull request #1602 from xinnan-tech/py_fix_bug
update:更新版本号
2025-06-17 00:11:52 +08:00
hrz 33345817d2 update:更新版本号 2025-06-17 00:10:55 +08:00
hrzandGitHub 82973a685a Merge pull request #1601 from xinnan-tech/py_fix_bug
fix:修复“设备使用iot协议,意图识别无法正确调用”的问题
2025-06-16 23:49:58 +08:00
hrz e1d245068c update:优化权限 2025-06-16 23:47:26 +08:00
hrzandGitHub b74517ffb0 Merge pull request #1591 from CaixyPromise/fix/comment-invalid
fix: 智控台备注功能无效
2025-06-16 23:34:24 +08:00
hrz 231ae8dfa6 update:修复iot不更新的bug 2025-06-16 23:16:40 +08:00
CGD 4ee9a47d41 fix:修复“设备使用iot协议,意图识别无法正确调用”的问题 2025-06-16 18:09:13 +08:00
Sakura-RanChen d8bf5cdedf fix: openAi schema不支持list 2025-06-16 16:26:49 +08:00
Sakura-RanChenandGitHub 755e0edd44 Merge pull request #1596 from xinnan-tech/py_fix_bug
fix:修复意图识别因缺少 home_assistant 配置导致部分功能错误将意图识别成“home_assistant ”的bug
2025-06-16 14:52:53 +08:00
CGD 178df82693 fix:修复意图识别因缺少 home_assistant 配置导致功能全部失效的 bug 2025-06-16 14:48:52 +08:00
Sakura-RanChenandGitHub a88c3b2032 Merge pull request #1594 from xinnan-tech/py_fix_bug
fix:修复数据库中两个字符串的排序规则(collation)不一致问题
2025-06-16 11:10:18 +08:00
CGD 513396b905 fix:修复数据库中两个字符串的排序规则(collation)不一致问题 2025-06-16 11:09:01 +08:00
caixypromise 9885d4758b fix: 修复设备管理无法提交备注信息的问题。
chore: 将设备自动更新状态接口重构为设备信息更新接口,以此实现备注、自动更新合并在一个接口内
2025-06-16 03:27:12 +08:00
22 changed files with 356 additions and 88 deletions
@@ -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
@@ -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;
@@ -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
+4 -3
View File
@@ -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()
}, },
+97 -56
View File
@@ -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;
} }
+1 -1
View File
@@ -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(
+28 -5
View File
@@ -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(
@@ -23,6 +23,10 @@ class FunctionHandler:
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 = []
for func in self.functions_desc: for func in self.functions_desc:
@@ -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(
+21
View File
@@ -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:
+59 -5
View File
@@ -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():
if client.has_tool(tool_name):
return await client.call_tool(tool_name, arguments)
raise ValueError(f"Tool {tool_name} not found in any MCP server") 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):
client_name = name
target_client = client
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值",
}, },
}, },