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
|
||||
|
||||
+3
@@ -56,6 +56,9 @@ public class AgentTemplateServiceImpl extends ServiceImpl<AgentTemplateDao, Agen
|
||||
wrapper.set("tts_model_id", modelId);
|
||||
wrapper.set("tts_voice_id", null);
|
||||
break;
|
||||
case "VLLM":
|
||||
wrapper.set("vllm_model_id", modelId);
|
||||
break;
|
||||
case "MEMORY":
|
||||
wrapper.set("mem_model_id", modelId);
|
||||
break;
|
||||
|
||||
+11
-4
@@ -4,6 +4,7 @@ import java.util.List;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
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.PathVariable;
|
||||
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.tags.Tag;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.AllArgsConstructor;
|
||||
import xiaozhi.common.exception.ErrorCode;
|
||||
import xiaozhi.common.redis.RedisKeys;
|
||||
@@ -22,6 +24,7 @@ import xiaozhi.common.user.UserDetail;
|
||||
import xiaozhi.common.utils.Result;
|
||||
import xiaozhi.modules.device.dto.DeviceRegisterDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUnBindDTO;
|
||||
import xiaozhi.modules.device.dto.DeviceUpdateDTO;
|
||||
import xiaozhi.modules.device.entity.DeviceEntity;
|
||||
import xiaozhi.modules.device.service.DeviceService;
|
||||
import xiaozhi.modules.security.user.SecurityUser;
|
||||
@@ -80,15 +83,19 @@ public class DeviceController {
|
||||
return new Result<Void>();
|
||||
}
|
||||
|
||||
@PutMapping("/enableOta/{id}/{status}")
|
||||
@Operation(summary = "启用/关闭OTA自动升级")
|
||||
@PutMapping("/update/{id}")
|
||||
@Operation(summary = "更新设备信息")
|
||||
@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);
|
||||
if (entity == null) {
|
||||
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);
|
||||
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:
|
||||
encoding: utf8
|
||||
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();
|
||||
},
|
||||
enableOtaUpgrade(id, status, callback) {
|
||||
updateDeviceInfo(id, payload, callback) {
|
||||
RequestService.sendRequest()
|
||||
.url(`${getServiceUrl()}/device/enableOta/${id}/${status}`)
|
||||
.url(`${getServiceUrl()}/device/update/${id}`)
|
||||
.method('PUT')
|
||||
.data(payload)
|
||||
.success((res) => {
|
||||
RequestService.clearRequestTime()
|
||||
callback(res)
|
||||
@@ -63,7 +64,7 @@ export default {
|
||||
console.error('更新OTA状态失败:', err)
|
||||
this.$message.error(err.msg || '更新OTA状态失败')
|
||||
RequestService.reAjaxFun(() => {
|
||||
this.enableOtaUpgrade(id, status, callback)
|
||||
this.updateDeviceInfo(id, payload, callback)
|
||||
})
|
||||
}).send()
|
||||
},
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<HeaderBar/>
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">设备管理</h2>
|
||||
<div class="right-operations">
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -16,8 +16,9 @@
|
||||
<div class="content-area">
|
||||
<el-card class="device-card" shadow="never">
|
||||
<el-table ref="deviceTable" :data="paginatedDeviceList" class="transparent-table"
|
||||
:header-cell-class-name="headerCellClassName" v-loading="loading" element-loading-text="拼命加载中"
|
||||
element-loading-spinner="el-icon-loading" element-loading-background="rgba(255, 255, 255, 0.7)">
|
||||
:header-cell-class-name="headerCellClassName" v-loading="loading"
|
||||
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">
|
||||
<template slot-scope="scope">
|
||||
<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="lastConversation" align="center"></el-table-column>
|
||||
<el-table-column label="备注" align="center">
|
||||
<template slot-scope="scope">
|
||||
<el-input v-if="scope.row.isEdit" v-model="scope.row.remark" size="mini"
|
||||
@blur="stopEditRemark(scope.$index)"></el-input>
|
||||
<span v-else>
|
||||
<i v-if="!scope.row.remark" class="el-icon-edit"
|
||||
@click="startEditRemark(scope.$index, scope.row)"></i>
|
||||
<span v-else @click="startEditRemark(scope.$index, scope.row)">
|
||||
{{ scope.row.remark }}
|
||||
</span>
|
||||
<template #default="{ row }">
|
||||
<el-input
|
||||
v-show="row.isEdit"
|
||||
v-model="row.remark"
|
||||
size="mini"
|
||||
maxlength="64"
|
||||
show-word-limit
|
||||
@blur="onRemarkBlur(row)"
|
||||
@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>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="OTA升级" align="center">
|
||||
<template slot-scope="scope">
|
||||
<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>
|
||||
</el-table-column>
|
||||
<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="goPrev">上一页</button>
|
||||
<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 }}
|
||||
</button>
|
||||
<button class="pagination-btn" :disabled="currentPage === pageCount" @click="goNext">下一页</button>
|
||||
@@ -91,7 +102,7 @@
|
||||
</div>
|
||||
|
||||
<AddDeviceDialog :visible.sync="addDeviceDialogVisible" :agent-id="currentAgentId"
|
||||
@refresh="fetchBindDevices(currentAgentId)" />
|
||||
@refresh="fetchBindDevices(currentAgentId)"/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -102,7 +113,7 @@ import AddDeviceDialog from "@/components/AddDeviceDialog.vue";
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
export default {
|
||||
components: { HeaderBar, AddDeviceDialog },
|
||||
components: {HeaderBar, AddDeviceDialog},
|
||||
data() {
|
||||
return {
|
||||
addDeviceDialogVisible: false,
|
||||
@@ -125,18 +136,15 @@ export default {
|
||||
const keyword = this.activeSearchKeyword.toLowerCase();
|
||||
if (!keyword) return this.deviceList;
|
||||
return this.deviceList.filter(device =>
|
||||
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||
(device.model && device.model.toLowerCase().includes(keyword)) ||
|
||||
(device.macAddress && device.macAddress.toLowerCase().includes(keyword))
|
||||
);
|
||||
},
|
||||
|
||||
paginatedDeviceList() {
|
||||
const start = (this.currentPage - 1) * this.pageSize;
|
||||
const end = start + this.pageSize;
|
||||
return this.filteredDeviceList.slice(start, end).map(item => ({
|
||||
...item,
|
||||
selected: false
|
||||
}));
|
||||
return this.filteredDeviceList.slice(start, end);
|
||||
},
|
||||
pageCount() {
|
||||
return Math.ceil(this.filteredDeviceList.length / this.pageSize);
|
||||
@@ -212,11 +220,10 @@ export default {
|
||||
this.batchUnbindDevices(deviceIds);
|
||||
});
|
||||
},
|
||||
|
||||
batchUnbindDevices(deviceIds) {
|
||||
const promises = deviceIds.map(id => {
|
||||
return new Promise((resolve, reject) => {
|
||||
Api.device.unbindDevice(id, ({ data }) => {
|
||||
Api.device.unbindDevice(id, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
resolve();
|
||||
} else {
|
||||
@@ -225,33 +232,61 @@ export default {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Promise.all(promises)
|
||||
.then(() => {
|
||||
this.$message.success({
|
||||
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||
showClose: true
|
||||
.then(() => {
|
||||
this.$message.success({
|
||||
message: `成功解绑 ${deviceIds.length} 台设备`,
|
||||
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() {
|
||||
this.addDeviceDialogVisible = true;
|
||||
},
|
||||
startEditRemark(index, row) {
|
||||
this.deviceList[index].isEdit = true;
|
||||
submitRemark(row) {
|
||||
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) {
|
||||
this.$confirm('确认要解绑该设备吗?', '警告', {
|
||||
@@ -259,7 +294,7 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
Api.device.unbindDevice(device_id, ({ data }) => {
|
||||
Api.device.unbindDevice(device_id, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success({
|
||||
message: '设备解绑成功',
|
||||
@@ -290,7 +325,7 @@ export default {
|
||||
|
||||
fetchBindDevices(agentId) {
|
||||
this.loading = true;
|
||||
Api.device.getAgentBindDevices(agentId, ({ data }) => {
|
||||
Api.device.getAgentBindDevices(agentId, ({data}) => {
|
||||
this.loading = false;
|
||||
if (data.code === 0) {
|
||||
this.deviceList = data.data.map(device => {
|
||||
@@ -302,12 +337,14 @@ export default {
|
||||
bindTime: device.createDate,
|
||||
lastConversation: device.lastConnectedAt,
|
||||
remark: device.alias,
|
||||
_originalRemark: device.alias,
|
||||
isEdit: false,
|
||||
_submitting: false,
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
rawBindTime: new Date(device.createDate).getTime()
|
||||
};
|
||||
})
|
||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||
.sort((a, b) => a.rawBindTime - b.rawBindTime);
|
||||
this.activeSearchKeyword = "";
|
||||
this.searchKeyword = "";
|
||||
} else {
|
||||
@@ -315,7 +352,7 @@ export default {
|
||||
}
|
||||
});
|
||||
},
|
||||
headerCellClassName({ columnIndex }) {
|
||||
headerCellClassName({columnIndex}) {
|
||||
if (columnIndex === 0) {
|
||||
return "custom-selection-header";
|
||||
}
|
||||
@@ -325,14 +362,19 @@ export default {
|
||||
const firmwareType = this.firmwareTypes.find(item => item.key === 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) {
|
||||
Api.device.enableOtaUpgrade(row.device_id, row.otaSwitch ? 1 : 0, ({ data }) => {
|
||||
if (data.code === 0) {
|
||||
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级')
|
||||
} else {
|
||||
row.otaSwitch = !row.otaSwitch
|
||||
this.$message.error(data.msg || '操作失败')
|
||||
this.updateDeviceInfo(row.device_id, {autoUpdate: row.otaSwitch ? 1 : 0}, (result, {msg}) => {
|
||||
if (result) {
|
||||
this.$message.success(row.otaSwitch ? '已设置成自动升级' : '已关闭自动升级');
|
||||
return;
|
||||
}
|
||||
row.otaSwitch = !row.otaSwitch
|
||||
this.$message.error(msg || '操作失败')
|
||||
})
|
||||
},
|
||||
}
|
||||
@@ -645,7 +687,6 @@ export default {
|
||||
}
|
||||
|
||||
|
||||
|
||||
:deep(.el-table .el-button--text) {
|
||||
color: #7079aa;
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ from config.config_loader import load_config
|
||||
from config.settings import check_config_file
|
||||
from datetime import datetime
|
||||
|
||||
SERVER_VERSION = "0.5.6"
|
||||
SERVER_VERSION = "0.5.8"
|
||||
_logger_initialized = False
|
||||
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ from config.config_loader import get_private_config_from_api
|
||||
from core.utils.auth import AuthToken
|
||||
import base64
|
||||
from typing import Tuple, Optional
|
||||
from plugins_func.register import Action
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -122,7 +123,8 @@ class VisionHandler:
|
||||
|
||||
return_json = {
|
||||
"success": True,
|
||||
"result": result,
|
||||
"action": Action.RESPONSE.name,
|
||||
"response": result,
|
||||
}
|
||||
|
||||
response = web.Response(
|
||||
|
||||
@@ -115,6 +115,7 @@ class ConnectionHandler:
|
||||
self.client_have_voice_last_time = 0.0
|
||||
self.client_no_voice_last_time = 0.0
|
||||
self.client_voice_stop = False
|
||||
self.client_voice_frame_count = 0
|
||||
|
||||
# asr相关变量
|
||||
# 因为实际部署时可能会用到公共的本地ASR,不能把变量暴露给公共ASR
|
||||
@@ -627,8 +628,8 @@ class ConnectionHandler:
|
||||
)
|
||||
memory_str = future.result()
|
||||
|
||||
uuid_str = str(uuid.uuid4()).replace("-", "")
|
||||
self.sentence_id = uuid_str
|
||||
self.sentence_id = str(uuid.uuid4().hex)
|
||||
|
||||
|
||||
if self.intent_type == "function_call" and functions is not None:
|
||||
# 使用支持functions的streaming接口
|
||||
@@ -751,9 +752,31 @@ class ConnectionHandler:
|
||||
self.loop,
|
||||
).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:
|
||||
self.logger.bind(tag=TAG).error(f"MCP工具调用失败: {e}")
|
||||
result = ActionResponse(
|
||||
|
||||
@@ -22,6 +22,10 @@ class FunctionHandler:
|
||||
self.register_config_functions()
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
self.finish_init = True
|
||||
|
||||
def upload_functions_desc(self):
|
||||
self.functions_desc = self.function_registry.get_all_function_desc()
|
||||
|
||||
|
||||
def current_support_functions(self):
|
||||
func_names = []
|
||||
|
||||
@@ -338,6 +338,8 @@ async def handleIotDescriptors(conn, descriptors):
|
||||
|
||||
# 如果注册了新函数,更新function描述列表
|
||||
if functions_changed and hasattr(conn, "func_handler"):
|
||||
conn.func_handler.upload_functions_desc()
|
||||
|
||||
func_names = conn.func_handler.current_support_functions()
|
||||
conn.logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
|
||||
@@ -80,6 +80,27 @@ class MCPClient:
|
||||
fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
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 with AsyncExitStack() as stack:
|
||||
try:
|
||||
|
||||
@@ -75,6 +75,7 @@ class MCPManager:
|
||||
self.conn.logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: {e}"
|
||||
)
|
||||
self.conn.func_handler.upload_functions_desc()
|
||||
|
||||
def get_all_tools(self) -> List[Dict[str, Any]]:
|
||||
"""获取所有服务的工具function定义
|
||||
@@ -99,7 +100,7 @@ class MCPManager:
|
||||
return False
|
||||
|
||||
async def execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
|
||||
"""执行工具调用
|
||||
"""执行工具调用,失败时会尝试重新连接
|
||||
Args:
|
||||
tool_name: 工具名称
|
||||
arguments: 工具参数
|
||||
@@ -111,11 +112,64 @@ class MCPManager:
|
||||
self.conn.logger.bind(tag=TAG).info(
|
||||
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):
|
||||
return await client.call_tool(tool_name, arguments)
|
||||
|
||||
raise ValueError(f"Tool {tool_name} not found in any MCP server")
|
||||
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:
|
||||
"""依次关闭所有 MCPClient,不让异常阻断整体流程。"""
|
||||
|
||||
@@ -93,9 +93,7 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 检查初始化响应
|
||||
if "code" in result and result["code"] != 1000:
|
||||
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('message', '未知错误')}"
|
||||
if "payload_msg" in result:
|
||||
error_msg += f"\n详细错误信息: {json.dumps(result['payload_msg'], ensure_ascii=False)}"
|
||||
error_msg = f"ASR服务初始化失败: {result.get('payload_msg', {}).get('error', '未知错误')}"
|
||||
logger.bind(tag=TAG).error(error_msg)
|
||||
raise Exception(error_msg)
|
||||
|
||||
@@ -256,7 +254,6 @@ class ASRProvider(ASRProviderBase):
|
||||
"X-Api-Access-Key": self.access_token,
|
||||
"X-Api-Resource-Id": "volc.bigasr.sauc.duration",
|
||||
"X-Api-Connect-Id": str(uuid.uuid4()),
|
||||
"Host": "openspeech.bytedance.com",
|
||||
}
|
||||
|
||||
def generate_header(
|
||||
@@ -309,9 +306,14 @@ class ASRProvider(ASRProviderBase):
|
||||
|
||||
# 如果是错误响应
|
||||
if message_type == 0x0F: # SERVER_ERROR_RESPONSE
|
||||
code = int.from_bytes(header[4:8], "big", signed=False)
|
||||
error_msg = res[8:].decode("utf-8")
|
||||
return {"code": code, "error": error_msg}
|
||||
code = int.from_bytes(res[4:8], "big", signed=False)
|
||||
msg_length = int.from_bytes(res[8:12], "big", signed=False)
|
||||
error_msg = json.loads(res[12:].decode("utf-8"))
|
||||
return {
|
||||
"code": code,
|
||||
"msg_length": msg_length,
|
||||
"payload_msg": error_msg,
|
||||
}
|
||||
|
||||
# 获取JSON数据(跳过12字节头部)
|
||||
try:
|
||||
|
||||
@@ -172,7 +172,11 @@ class IntentProvider(IntentProviderBase):
|
||||
music_file_names = music_config["music_file_names"]
|
||||
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:
|
||||
hass_prompt = "\n下面是我家智能设备列表(位置,设备名,entity_id),可以通过homeassistant控制\n"
|
||||
for device in devices:
|
||||
|
||||
@@ -5,6 +5,7 @@ import os
|
||||
import yaml
|
||||
from config.config_loader import get_project_dir
|
||||
from config.manage_api_client import save_mem_local_short
|
||||
from core.utils.util import check_model_key
|
||||
|
||||
|
||||
short_term_memory_prompt = """
|
||||
@@ -145,6 +146,10 @@ class MemoryProvider(MemoryProviderBase):
|
||||
# 打印使用的模型信息
|
||||
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
|
||||
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:
|
||||
logger.bind(tag=TAG).error("LLM is not set for memory provider")
|
||||
return None
|
||||
|
||||
@@ -12,6 +12,8 @@ from core.utils.util import check_model_key
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.providers.tts.dto.dto import SentenceType, ContentType, InterfaceType
|
||||
from asyncio import Task
|
||||
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -141,6 +143,7 @@ class TTSProvider(TTSProviderBase):
|
||||
super().__init__(config, delete_audio_file)
|
||||
self.ws = None
|
||||
self.interface_type = InterfaceType.DUAL_STREAM
|
||||
self._monitor_task = None # 监听任务引用
|
||||
self.appId = config.get("appid")
|
||||
self.access_token = config.get("access_token")
|
||||
self.cluster = config.get("cluster")
|
||||
@@ -270,8 +273,7 @@ class TTSProvider(TTSProviderBase):
|
||||
try:
|
||||
# 建立新连接
|
||||
if self.ws is None:
|
||||
await handleAbortMessage(self.conn)
|
||||
logger.bind(tag=TAG).error(f"WebSocket连接不存在,终止发送文本")
|
||||
logger.bind(tag=TAG).warning(f"WebSocket连接不存在,终止发送文本")
|
||||
return
|
||||
|
||||
# 过滤Markdown
|
||||
@@ -293,6 +295,25 @@ class TTSProvider(TTSProviderBase):
|
||||
async def start_session(self, session_id):
|
||||
logger.bind(tag=TAG).info(f"开始会话~~{session_id}")
|
||||
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()
|
||||
|
||||
@@ -463,6 +484,8 @@ class TTSProvider(TTSProviderBase):
|
||||
except:
|
||||
pass
|
||||
self.ws = None
|
||||
# 监听任务退出时清理引用
|
||||
self._monitor_task = None
|
||||
|
||||
async def send_event(
|
||||
self,
|
||||
|
||||
@@ -35,6 +35,10 @@ class VADProvider(VADProviderBase):
|
||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||
|
||||
# 初始化帧计数器
|
||||
if not hasattr(conn, "client_voice_frame_count"):
|
||||
conn.client_voice_frame_count = 0
|
||||
|
||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||
client_have_voice = False
|
||||
while len(conn.client_audio_buffer) >= 512 * 2:
|
||||
@@ -50,7 +54,15 @@ class VADProvider(VADProviderBase):
|
||||
# 检测语音活动
|
||||
with torch.no_grad():
|
||||
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:
|
||||
|
||||
@@ -31,7 +31,8 @@ hass_set_state_function_desc = {
|
||||
"description": "只有在设置静音操作时才需要,设置静音的时候该值为true,取消静音时该值为false",
|
||||
},
|
||||
"rgb_color": {
|
||||
"type": "list",
|
||||
"type": "array",
|
||||
"items": {"type": "integer"},
|
||||
"description": "只有在设置颜色时需要,这里填目标颜色的rgb值",
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user