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 | |
|---|---|---|---|
|
|
de5eaf4467 | ||
|
|
ca884833d1 | ||
|
|
0da2da83a5 | ||
|
|
bfdfa44edd | ||
|
|
ed1cfe5eb6 | ||
|
|
b696f3f5db | ||
|
|
7a0cf5ef9a | ||
|
|
d1b7cb6a36 | ||
|
|
b21d699472 | ||
|
|
029fadd778 | ||
|
|
215d50235b | ||
|
|
000e676397 | ||
|
|
ee20f59a8e | ||
|
|
925cf3dabb | ||
|
|
0049d0e926 | ||
|
|
56959808a9 | ||
|
|
8667a9680e |
@@ -38,10 +38,13 @@ public class OTAController {
|
|||||||
public ResponseEntity<String> checkOTAVersion(
|
public ResponseEntity<String> checkOTAVersion(
|
||||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||||
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
@Parameter(name = "Device-Id", description = "设备唯一标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Device-Id") String deviceId,
|
||||||
@Parameter(name = "Client-Id", description = "客户端标识", required = true, in = ParameterIn.HEADER) @RequestHeader("Client-Id") String clientId) {
|
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
if (StringUtils.isBlank(deviceId)) {
|
||||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||||
}
|
}
|
||||||
|
if (StringUtils.isBlank(clientId)) {
|
||||||
|
clientId = deviceId;
|
||||||
|
}
|
||||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||||
|
|||||||
+1
-1
@@ -121,7 +121,7 @@ public class LoginController {
|
|||||||
@Operation(summary = "公共配置")
|
@Operation(summary = "公共配置")
|
||||||
public Result<Map<String, Object>> pubConfig() {
|
public Result<Map<String, Object>> pubConfig() {
|
||||||
Map<String, Object> config = new HashMap<>();
|
Map<String, Object> config = new HashMap<>();
|
||||||
config.put("version", "0.3.4");
|
config.put("version", "0.3.5");
|
||||||
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||||
return new Result<Map<String, Object>>().ok(config);
|
return new Result<Map<String, Object>>().ok(config);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" center>
|
<el-dialog :visible="visible" @close="handleClose" width="400px" center>
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -79,7 +79,10 @@ export default {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.deviceCode = ""
|
this.deviceCode = ""
|
||||||
}
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@
|
|||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
<el-form-item label="排序号" prop="sortOrder" style="flex: 1;">
|
||||||
<el-input v-model="formData.sort" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
<el-input v-model="formData.sort" type="number" placeholder="请输入排序号" class="custom-input-bg"></el-input>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
|
<el-dialog :visible="visible" @close="handleClose" width="400px" center @open="handleOpen">
|
||||||
<div
|
<div
|
||||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||||
<div
|
<div
|
||||||
@@ -66,7 +66,10 @@ export default {
|
|||||||
cancel() {
|
cancel() {
|
||||||
this.$emit('update:visible', false)
|
this.$emit('update:visible', false)
|
||||||
this.wisdomBodyName = ""
|
this.wisdomBodyName = ""
|
||||||
}
|
},
|
||||||
|
handleClose() {
|
||||||
|
this.$emit('update:visible', false);
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -83,7 +83,8 @@ export default {
|
|||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
color: #5778ff;
|
color: #5778ff;
|
||||||
background: #e6ebff;
|
background: #e6ebff;
|
||||||
width: 57px;
|
width: auto;
|
||||||
|
padding: 0 12px;
|
||||||
height: 21px;
|
height: 21px;
|
||||||
line-height: 21px;
|
line-height: 21px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|||||||
@@ -551,28 +551,26 @@ export default {
|
|||||||
|
|
||||||
/* 备注文本 */
|
/* 备注文本 */
|
||||||
::v-deep .remark-input .el-textarea__inner {
|
::v-deep .remark-input .el-textarea__inner {
|
||||||
background-color: #f5f5f5;
|
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
border: 1px solid #e6e6e6;
|
border: 1px solid #e6e6e6;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
resize: none;
|
resize: none;
|
||||||
max-height: 40px !important;
|
max-height: 40px !important;
|
||||||
line-height: 1.5;
|
line-height: 1.5;
|
||||||
}
|
background-color: transparent !important;
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
|
||||||
color: black !important;
|
|
||||||
opacity: 0.7;
|
|
||||||
}
|
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner {
|
|
||||||
background-color: #f4f6fa;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
::v-deep .remark-input .el-textarea__inner:focus {
|
::v-deep .remark-input .el-textarea__inner:focus {
|
||||||
background-color: #edeffb;
|
border-color: #409EFF !important;
|
||||||
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||||
|
color: #c0c4cc !important;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* 滚动容器 */
|
/* 滚动容器 */
|
||||||
.scroll-wrapper {
|
.scroll-wrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -650,6 +648,12 @@ export default {
|
|||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
padding-top: 10px;
|
padding-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-buttons .el-button {
|
||||||
|
padding: 8px 15px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
.edit-btn,
|
.edit-btn,
|
||||||
.delete-btn,
|
.delete-btn,
|
||||||
.save-btn {
|
.save-btn {
|
||||||
|
|||||||
@@ -115,8 +115,8 @@ export default {
|
|||||||
activeSearchKeyword: "",
|
activeSearchKeyword: "",
|
||||||
currentAgentId: this.$route.query.agentId || '',
|
currentAgentId: this.$route.query.agentId || '',
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
deviceList: [],
|
deviceList: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
userApi: null,
|
userApi: null,
|
||||||
@@ -283,13 +283,18 @@ export default {
|
|||||||
this.deviceList = data.data.map(device => {
|
this.deviceList = data.data.map(device => {
|
||||||
const bindDate = new Date(device.createDate);
|
const bindDate = new Date(device.createDate);
|
||||||
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
const formattedBindTime = `${bindDate.getFullYear()}-${(bindDate.getMonth() + 1).toString().padStart(2, '0')}-${bindDate.getDate().toString().padStart(2, '0')} ${bindDate.getHours().toString().padStart(2, '0')}:${bindDate.getMinutes().toString().padStart(2, '0')}:${bindDate.getSeconds().toString().padStart(2, '0')}`;
|
||||||
|
let formattedLastConversation = '';
|
||||||
|
if (device.lastConnectedAt) {
|
||||||
|
const lastConvoDate = new Date(device.lastConnectedAt);
|
||||||
|
formattedLastConversation = `${lastConvoDate.getFullYear()}-${(lastConvoDate.getMonth() + 1).toString().padStart(2, '0')}-${lastConvoDate.getDate().toString().padStart(2, '0')} ${lastConvoDate.getHours().toString().padStart(2, '0')}:${lastConvoDate.getMinutes().toString().padStart(2, '0')}:${lastConvoDate.getSeconds().toString().padStart(2, '0')}`;
|
||||||
|
}
|
||||||
return {
|
return {
|
||||||
device_id: device.id,
|
device_id: device.id,
|
||||||
model: device.board,
|
model: device.board,
|
||||||
firmwareVersion: device.appVersion,
|
firmwareVersion: device.appVersion,
|
||||||
macAddress: device.macAddress,
|
macAddress: device.macAddress,
|
||||||
bindTime: formattedBindTime,
|
bindTime: formattedBindTime,
|
||||||
lastConversation: device.lastConnectedAt,
|
lastConversation: formattedLastConversation,
|
||||||
remark: device.alias,
|
remark: device.alias,
|
||||||
isEdit: false,
|
isEdit: false,
|
||||||
otaSwitch: device.autoUpdate === 1,
|
otaSwitch: device.autoUpdate === 1,
|
||||||
@@ -331,7 +336,7 @@ export default {
|
|||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: calc(100vh - 200px);
|
min-height: calc(100vh - 24vh);
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
@@ -628,12 +633,12 @@ export default {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
max-height: calc(100vh - 330px);
|
max-height: calc(100vh - 40vh);
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.el-table__body-wrapper) {
|
:deep(.el-table__body-wrapper) {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -152,7 +152,7 @@ export default {
|
|||||||
ttsDialogVisible: false,
|
ttsDialogVisible: false,
|
||||||
selectedTtsModelId: '',
|
selectedTtsModelId: '',
|
||||||
modelList: [],
|
modelList: [],
|
||||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 10,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
@@ -461,7 +461,7 @@ export default {
|
|||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: calc(100vh - 235px);
|
min-height: calc(100vh - 24vh);
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
@@ -656,7 +656,6 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.table-footer {
|
.table-footer {
|
||||||
margin-top: 24px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -904,7 +903,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.data-table {
|
.data-table {
|
||||||
--table-max-height: calc(100vh - 450px);
|
--table-max-height: calc(100vh - 45vh);
|
||||||
max-height: var(--table-max-height);
|
max-height: var(--table-max-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,8 @@ export default {
|
|||||||
searchCode: "",
|
searchCode: "",
|
||||||
paramsList: [],
|
paramsList: [],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
total: 0,
|
total: 0,
|
||||||
dialogVisible: false,
|
dialogVisible: false,
|
||||||
dialogTitle: "新增参数",
|
dialogTitle: "新增参数",
|
||||||
@@ -330,7 +330,7 @@ export default {
|
|||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: calc(100vh - 350px);
|
min-height: calc(100vh - 24vh);
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
@@ -516,7 +516,7 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
.el-table__body-wrapper {
|
.el-table__body-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,7 +649,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-table {
|
.el-table {
|
||||||
--table-max-height: calc(100vh - 400px);
|
--table-max-height: calc(100vh - 40vh);
|
||||||
max-height: var(--table-max-height);
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
.el-table__body-wrapper {
|
.el-table__body-wrapper {
|
||||||
|
|||||||
@@ -99,9 +99,9 @@ export default {
|
|||||||
currentPassword: "",
|
currentPassword: "",
|
||||||
searchPhone: "",
|
searchPhone: "",
|
||||||
userList: [],
|
userList: [],
|
||||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
pageSizeOptions: [10, 20, 50, 100],
|
||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
pageSize: 5,
|
pageSize: 10,
|
||||||
total: 0,
|
total: 0,
|
||||||
isAllSelected: false
|
isAllSelected: false
|
||||||
};
|
};
|
||||||
@@ -355,7 +355,7 @@ export default {
|
|||||||
.main-wrapper {
|
.main-wrapper {
|
||||||
margin: 5px 22px;
|
margin: 5px 22px;
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
min-height: calc(100vh - 350px);
|
min-height: calc(100vh - 24vh);
|
||||||
height: auto;
|
height: auto;
|
||||||
max-height: 80vh;
|
max-height: 80vh;
|
||||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
@@ -557,7 +557,7 @@ export default {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
.el-table__body-wrapper {
|
.el-table__body-wrapper {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow: auto;
|
overflow-y: auto;
|
||||||
max-height: none !important;
|
max-height: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -674,7 +674,7 @@ export default {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.el-table {
|
.el-table {
|
||||||
--table-max-height: calc(100vh - 400px);
|
--table-max-height: calc(100vh - 40vh);
|
||||||
max-height: var(--table-max-height);
|
max-height: var(--table-max-height);
|
||||||
|
|
||||||
.el-table__body-wrapper {
|
.el-table__body-wrapper {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome" @keyup.enter="register">
|
||||||
<el-container style="height: 100%;">
|
<el-container style="height: 100%;">
|
||||||
<!-- 保持相同的头部 -->
|
<!-- 保持相同的头部 -->
|
||||||
<el-header>
|
<el-header>
|
||||||
|
|||||||
@@ -1,88 +1,126 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="welcome">
|
<div class="welcome">
|
||||||
<HeaderBar />
|
<HeaderBar/>
|
||||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
|
||||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
<div class="operation-bar">
|
||||||
<div
|
<h2 class="page-title">角色配置</h2>
|
||||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
</div>
|
||||||
<div
|
|
||||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
<div class="main-wrapper">
|
||||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
|
<div class="content-panel">
|
||||||
</div>
|
<div class="content-area">
|
||||||
{{ form.agentName }}
|
<el-card class="config-card" shadow="never">
|
||||||
</div>
|
<div class="config-header">
|
||||||
<div style="height: 1px;background: #e8f0ff;" />
|
<div class="header-icon">
|
||||||
<el-form ref="form" :model="form" label-width="72px">
|
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||||
<div style="padding: 16px 24px;">
|
|
||||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 24px;">
|
|
||||||
<div>
|
|
||||||
<el-form-item label="助手昵称:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.agentName" />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色模版:">
|
|
||||||
<div style="display: flex;gap: 8px;flex-wrap: wrap;">
|
|
||||||
<div v-for="(template, index) in templates" :key="`template-${index}`" class="template-item"
|
|
||||||
:class="{ 'template-loading': loadingTemplate }" @click="selectTemplate(template)">
|
|
||||||
{{ template.agentName }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色介绍:">
|
|
||||||
<div class="textarea-box">
|
|
||||||
<el-input type="textarea" rows="5" resize="none" placeholder="请输入内容" v-model="form.systemPrompt"
|
|
||||||
maxlength="2000" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="语言编码:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.langCode" placeholder="请输入语言编码,如:zh_CN" maxlength="10" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="交互语种:">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-input v-model="form.language" placeholder="请输入交互语种,如:中文" maxlength="10" show-word-limit />
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<el-form-item v-for="(model, index) in models" :key="`model-${index}`" :label="model.label"
|
|
||||||
class="model-item">
|
|
||||||
<el-select v-model="form.model[model.key]" filterable placeholder="请选择" class="select-field">
|
|
||||||
<el-option v-for="(item, optionIndex) in modelOptions[model.type]"
|
|
||||||
:key="`option-${index}-${optionIndex}`" :label="item.label" :value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="角色音色:">
|
|
||||||
<div style="display: flex;gap: 8px;align-items: center;">
|
|
||||||
<div class="input-46" style="width: 100%;">
|
|
||||||
<el-select v-model="form.ttsVoiceId" placeholder="请选择" style="width: 100%;">
|
|
||||||
<el-option v-for="(item, index) in voiceOptions" :key="`voice-${index}`" :label="item.label"
|
|
||||||
:value="item.value" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-form-item>
|
|
||||||
</div>
|
</div>
|
||||||
|
<span class="header-title">{{ form.agentName }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="divider"></div>
|
||||||
</el-form>
|
|
||||||
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
|
<el-form ref="form" :model="form" label-width="72px">
|
||||||
<div class="save-btn" @click="saveConfig">
|
<div class="form-content">
|
||||||
保存配置
|
<div class="form-grid">
|
||||||
</div>
|
<div class="form-column">
|
||||||
<div class="reset-btn" @click="resetConfig">
|
<el-form-item label="助手昵称:">
|
||||||
重制
|
<el-input v-model="form.agentName" class="form-input"/>
|
||||||
</div>
|
</el-form-item>
|
||||||
<div class="clear-text">
|
<el-form-item label="角色模版:">
|
||||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;" />
|
<div class="template-container">
|
||||||
保存配置后,需要重启设备,新的配置才会生效。
|
<div
|
||||||
</div>
|
v-for="(template, index) in templates"
|
||||||
|
:key="`template-${index}`"
|
||||||
|
class="template-item"
|
||||||
|
:class="{ 'template-loading': loadingTemplate }"
|
||||||
|
@click="selectTemplate(template)"
|
||||||
|
>
|
||||||
|
{{ template.agentName }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色介绍:">
|
||||||
|
<el-input
|
||||||
|
type="textarea"
|
||||||
|
rows="5"
|
||||||
|
resize="none"
|
||||||
|
placeholder="请输入内容"
|
||||||
|
v-model="form.systemPrompt"
|
||||||
|
maxlength="2000"
|
||||||
|
show-word-limit
|
||||||
|
class="form-textarea"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="语言编码:">
|
||||||
|
<el-input
|
||||||
|
v-model="form.langCode"
|
||||||
|
placeholder="请输入语言编码,如:zh_CN"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="交互语种:">
|
||||||
|
<el-input
|
||||||
|
v-model="form.language"
|
||||||
|
placeholder="请输入交互语种,如:中文"
|
||||||
|
maxlength="10"
|
||||||
|
show-word-limit
|
||||||
|
class="form-input"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<div class="action-bar">
|
||||||
|
<el-button type="primary" class="save-btn" @click="saveConfig">保存配置</el-button>
|
||||||
|
<el-button class="reset-btn" @click="resetConfig">重置</el-button>
|
||||||
|
<div class="hint-text">
|
||||||
|
<img loading="lazy" src="@/assets/home/red-info.png" alt="">
|
||||||
|
<span>保存配置后,需要重启设备,新的配置才会生效。</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-column">
|
||||||
|
<el-form-item
|
||||||
|
v-for="(model, index) in models"
|
||||||
|
:key="`model-${index}`"
|
||||||
|
:label="model.label"
|
||||||
|
class="model-item"
|
||||||
|
>
|
||||||
|
<el-select
|
||||||
|
v-model="form.model[model.key]"
|
||||||
|
filterable
|
||||||
|
placeholder="请选择"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, optionIndex) in modelOptions[model.type]"
|
||||||
|
:key="`option-${index}-${optionIndex}`"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="角色音色:">
|
||||||
|
<el-select
|
||||||
|
v-model="form.ttsVoiceId"
|
||||||
|
placeholder="请选择"
|
||||||
|
class="form-select"
|
||||||
|
>
|
||||||
|
<el-option
|
||||||
|
v-for="(item, index) in voiceOptions"
|
||||||
|
:key="`voice-${index}`"
|
||||||
|
:label="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
/>
|
||||||
|
</el-select>
|
||||||
|
</el-form-item>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-form>
|
||||||
|
|
||||||
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</el-main>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -90,10 +128,9 @@
|
|||||||
import Api from '@/apis/api';
|
import Api from '@/apis/api';
|
||||||
import HeaderBar from "@/components/HeaderBar.vue";
|
import HeaderBar from "@/components/HeaderBar.vue";
|
||||||
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RoleConfigPage',
|
name: 'RoleConfigPage',
|
||||||
components: { HeaderBar },
|
components: {HeaderBar},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
form: {
|
form: {
|
||||||
@@ -114,12 +151,12 @@ export default {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
models: [
|
models: [
|
||||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
{label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD'},
|
||||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
{label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR'},
|
||||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
{label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM'},
|
||||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
{label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent'},
|
||||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
{label: '记忆(Memory)', key: 'memModelId', type: 'Memory'},
|
||||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
|
{label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS'},
|
||||||
],
|
],
|
||||||
modelOptions: {},
|
modelOptions: {},
|
||||||
templates: [],
|
templates: [],
|
||||||
@@ -144,7 +181,7 @@ export default {
|
|||||||
language: this.form.language,
|
language: this.form.language,
|
||||||
sort: this.form.sort
|
sort: this.form.sort
|
||||||
};
|
};
|
||||||
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({ data }) => {
|
Api.agent.updateAgentConfig(this.$route.query.agentId, configData, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$message.success({
|
this.$message.success({
|
||||||
message: '配置保存成功',
|
message: '配置保存成功',
|
||||||
@@ -164,7 +201,6 @@ export default {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
type: 'warning'
|
type: 'warning'
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
// 重置表单
|
|
||||||
this.form = {
|
this.form = {
|
||||||
agentCode: "",
|
agentCode: "",
|
||||||
agentName: "",
|
agentName: "",
|
||||||
@@ -187,10 +223,10 @@ export default {
|
|||||||
showClose: true
|
showClose: true
|
||||||
})
|
})
|
||||||
}).catch(() => {
|
}).catch(() => {
|
||||||
})
|
});
|
||||||
},
|
},
|
||||||
fetchTemplates() {
|
fetchTemplates() {
|
||||||
Api.agent.getAgentTemplate(({ data }) => {
|
Api.agent.getAgentTemplate(({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.templates = data.data;
|
this.templates = data.data;
|
||||||
} else {
|
} else {
|
||||||
@@ -235,7 +271,7 @@ export default {
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
fetchAgentConfig(agentId) {
|
fetchAgentConfig(agentId) {
|
||||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
Api.agent.getDeviceConfig(agentId, ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.form = {
|
this.form = {
|
||||||
...this.form,
|
...this.form,
|
||||||
@@ -255,9 +291,8 @@ export default {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
fetchModelOptions() {
|
fetchModelOptions() {
|
||||||
// 为每个模型类型获取选项
|
|
||||||
this.models.forEach(model => {
|
this.models.forEach(model => {
|
||||||
Api.model.getModelNames(model.type, '', ({ data }) => {
|
Api.model.getModelNames(model.type, '', ({data}) => {
|
||||||
if (data.code === 0) {
|
if (data.code === 0) {
|
||||||
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
||||||
value: item.id,
|
value: item.id,
|
||||||
@@ -274,7 +309,7 @@ export default {
|
|||||||
this.voiceOptions = [];
|
this.voiceOptions = [];
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Api.model.getModelVoices(modelId, '', ({ data }) => {
|
Api.model.getModelVoices(modelId, '', ({data}) => {
|
||||||
if (data.code === 0 && data.data) {
|
if (data.code === 0 && data.data) {
|
||||||
this.voiceOptions = data.data.map(voice => ({
|
this.voiceOptions = data.data.map(voice => ({
|
||||||
value: voice.id,
|
value: voice.id,
|
||||||
@@ -289,7 +324,6 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
'form.model.ttsModelId': {
|
'form.model.ttsModelId': {
|
||||||
handler(newVal, oldVal) {
|
handler(newVal, oldVal) {
|
||||||
console.log('TTS模型变化:', newVal);
|
|
||||||
if (oldVal && newVal !== oldVal) {
|
if (oldVal && newVal !== oldVal) {
|
||||||
this.form.ttsVoiceId = '';
|
this.form.ttsVoiceId = '';
|
||||||
this.fetchVoiceOptions(newVal);
|
this.fetchVoiceOptions(newVal);
|
||||||
@@ -325,60 +359,131 @@ export default {
|
|||||||
min-height: 506px;
|
min-height: 506px;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
position: relative;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||||
background-size: cover;
|
background-size: cover;
|
||||||
/* 确保背景图像覆盖整个元素 */
|
|
||||||
background-position: center;
|
|
||||||
/* 从顶部中心对齐 */
|
|
||||||
-webkit-background-size: cover;
|
-webkit-background-size: cover;
|
||||||
/* 兼容老版本WebKit浏览器 */
|
|
||||||
-o-background-size: cover;
|
-o-background-size: cover;
|
||||||
/* 兼容老版本Opera浏览器 */
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.el-form-item ::v-deep .el-form-item__label {
|
.operation-bar {
|
||||||
font-size: 10px !important;
|
display: flex;
|
||||||
color: #3d4566 !important;
|
justify-content: space-between;
|
||||||
font-weight: 400;
|
align-items: center;
|
||||||
line-height: 22px;
|
padding: 16px 24px;
|
||||||
padding-bottom: 2px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.select-field {
|
.page-title {
|
||||||
width: 100%;
|
font-size: 24px;
|
||||||
max-width: 720px;
|
margin: 0;
|
||||||
border: 1px solid #e4e6ef;
|
color: #2c3e50;
|
||||||
background: #f6f8fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 36px !important;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-box {
|
.main-wrapper {
|
||||||
|
margin: 5px 22px;
|
||||||
|
border-radius: 15px;
|
||||||
|
height: calc(100vh - 24vh);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||||
|
position: relative;
|
||||||
|
background: rgba(237, 242, 255, 0.5);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
height: 37px;
|
display: flex;
|
||||||
border-radius: 20px;
|
overflow: hidden;
|
||||||
border: 1px solid #e4e6ef;
|
height: 100%;
|
||||||
|
border-radius: 15px;
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid #fff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-btn {
|
.content-area {
|
||||||
width: 48px;
|
flex: 1;
|
||||||
height: 19px;
|
height: 100%;
|
||||||
background: #fd8383;
|
min-width: 600px;
|
||||||
border-radius: 10px;
|
overflow: auto;
|
||||||
line-height: 19px;
|
background-color: white;
|
||||||
font-size: 11px;
|
display: flex;
|
||||||
color: #fff;
|
flex-direction: column;
|
||||||
cursor: pointer;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.clear-text {
|
.config-card {
|
||||||
color: #979db1;
|
background: white;
|
||||||
font-size: 11px;
|
border: none;
|
||||||
|
box-shadow: none;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 13px;
|
||||||
|
padding: 0 0 5px 0;
|
||||||
|
font-weight: 700;
|
||||||
|
font-size: 19px;
|
||||||
|
color: #3d4566;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon {
|
||||||
|
width: 37px;
|
||||||
|
height: 37px;
|
||||||
|
background: #5778ff;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header-icon img {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.divider {
|
||||||
|
height: 1px;
|
||||||
|
background: #e8f0ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-content {
|
||||||
|
padding: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-column {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-select {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.template-container {
|
||||||
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
margin-left: 16px;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.template-item {
|
.template-item {
|
||||||
@@ -399,47 +504,59 @@ export default {
|
|||||||
background-color: #d0d8ff;
|
background-color: #d0d8ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prompt-bottom {
|
.action-bar {
|
||||||
margin-bottom: 4px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
flex-wrap: wrap;
|
||||||
padding: 0 16px;
|
gap: 8px;
|
||||||
|
margin-top: 20px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-46 {
|
|
||||||
border: 1px solid #e4e6ef;
|
|
||||||
background: #f6f8fb;
|
|
||||||
border-radius: 8px;
|
|
||||||
height: 36px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn,
|
|
||||||
.reset-btn {
|
|
||||||
width: 112px;
|
|
||||||
height: 37px;
|
|
||||||
border-radius: 18px;
|
|
||||||
line-height: 37px;
|
|
||||||
box-sizing: border-box;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 11px
|
|
||||||
}
|
|
||||||
|
|
||||||
.save-btn {
|
.save-btn {
|
||||||
border-radius: 18px;
|
|
||||||
background: #5778ff;
|
background: #5778ff;
|
||||||
color: #fff;
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.reset-btn {
|
.reset-btn {
|
||||||
border: 1px solid #adbdff;
|
|
||||||
background: #e6ebff;
|
background: #e6ebff;
|
||||||
color: #5778ff;
|
color: #5778ff;
|
||||||
|
border: 1px solid #adbdff;
|
||||||
|
border-radius: 18px;
|
||||||
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.textarea-box {
|
.hint-text {
|
||||||
border: 1px solid #e4e6ef;
|
display: flex;
|
||||||
border-radius: 8px;
|
align-items: center;
|
||||||
background: #f6f8fb;
|
gap: 8px;
|
||||||
|
color: #979db1;
|
||||||
|
font-size: 11px;
|
||||||
|
margin-left: 16px;
|
||||||
}
|
}
|
||||||
</style>
|
|
||||||
|
.hint-text img {
|
||||||
|
width: 19px;
|
||||||
|
height: 19px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-form-item__label {
|
||||||
|
font-size: 10px !important;
|
||||||
|
color: #3d4566 !important;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 22px;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
::v-deep .el-textarea .el-input__count {
|
||||||
|
color: #909399;
|
||||||
|
background: none;
|
||||||
|
position: absolute;
|
||||||
|
font-size: 12px;
|
||||||
|
bottom: -10px;
|
||||||
|
right: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
@@ -3,7 +3,7 @@ import sys
|
|||||||
from loguru import logger
|
from loguru import logger
|
||||||
from config.config_loader import load_config
|
from config.config_loader import load_config
|
||||||
|
|
||||||
SERVER_VERSION = "0.3.4"
|
SERVER_VERSION = "0.3.5"
|
||||||
|
|
||||||
|
|
||||||
def get_module_abbreviation(module_name, module_dict):
|
def get_module_abbreviation(module_name, module_dict):
|
||||||
|
|||||||
@@ -116,6 +116,27 @@ class ConnectionHandler:
|
|||||||
try:
|
try:
|
||||||
# 获取并验证headers
|
# 获取并验证headers
|
||||||
self.headers = dict(ws.request.headers)
|
self.headers = dict(ws.request.headers)
|
||||||
|
|
||||||
|
if self.headers.get("device-id", None) is None:
|
||||||
|
# 尝试从 URL 的查询参数中获取 device-id
|
||||||
|
from urllib.parse import parse_qs, urlparse
|
||||||
|
|
||||||
|
# 从 WebSocket 请求中获取路径
|
||||||
|
request_path = ws.request.path
|
||||||
|
if not request_path:
|
||||||
|
self.logger.bind(tag=TAG).error("无法获取请求路径")
|
||||||
|
return
|
||||||
|
parsed_url = urlparse(request_path)
|
||||||
|
query_params = parse_qs(parsed_url.query)
|
||||||
|
if "device-id" in query_params:
|
||||||
|
self.headers["device-id"] = query_params["device-id"][0]
|
||||||
|
self.headers["client-id"] = query_params["client-id"][0]
|
||||||
|
else:
|
||||||
|
self.logger.bind(tag=TAG).error(
|
||||||
|
"无法从请求头和URL查询参数中获取device-id"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
# 获取客户端ip地址
|
# 获取客户端ip地址
|
||||||
self.client_ip = ws.remote_address[0]
|
self.client_ip = ws.remote_address[0]
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
@@ -184,7 +205,6 @@ class ConnectionHandler:
|
|||||||
self._initialize_models()
|
self._initialize_models()
|
||||||
|
|
||||||
"""加载提示词"""
|
"""加载提示词"""
|
||||||
self.prompt = self.config["prompt"]
|
|
||||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||||
|
|
||||||
"""加载记忆"""
|
"""加载记忆"""
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ async def wakeupWordsResponse(conn):
|
|||||||
"""唤醒词响应"""
|
"""唤醒词响应"""
|
||||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||||
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
||||||
|
if result is None or result == "":
|
||||||
|
return
|
||||||
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
|
tts_file = await asyncio.to_thread(conn.tts.to_tts, result)
|
||||||
|
|
||||||
if tts_file is not None and os.path.exists(tts_file):
|
if tts_file is not None and os.path.exists(tts_file):
|
||||||
|
|||||||
@@ -144,33 +144,35 @@ class IotDescriptor:
|
|||||||
self.methods = []
|
self.methods = []
|
||||||
|
|
||||||
# 根据描述创建属性
|
# 根据描述创建属性
|
||||||
for key, value in properties.items():
|
if properties is not None:
|
||||||
property_item = globals()[key] = {}
|
for key, value in properties.items():
|
||||||
property_item["name"] = key
|
property_item = globals()[key] = {}
|
||||||
property_item["description"] = value["description"]
|
property_item["name"] = key
|
||||||
if value["type"] == "number":
|
property_item["description"] = value["description"]
|
||||||
property_item["value"] = 0
|
if value["type"] == "number":
|
||||||
elif value["type"] == "boolean":
|
property_item["value"] = 0
|
||||||
property_item["value"] = False
|
elif value["type"] == "boolean":
|
||||||
else:
|
property_item["value"] = False
|
||||||
property_item["value"] = ""
|
else:
|
||||||
self.properties.append(property_item)
|
property_item["value"] = ""
|
||||||
|
self.properties.append(property_item)
|
||||||
|
|
||||||
# 根据描述创建方法
|
# 根据描述创建方法
|
||||||
for key, value in methods.items():
|
if methods is not None:
|
||||||
method = globals()[key] = {}
|
for key, value in methods.items():
|
||||||
method["description"] = value["description"]
|
method = globals()[key] = {}
|
||||||
method["name"] = key
|
method["description"] = value["description"]
|
||||||
for k, v in value["parameters"].items():
|
method["name"] = key
|
||||||
method[k] = {}
|
for k, v in value["parameters"].items():
|
||||||
method[k]["description"] = v["description"]
|
method[k] = {}
|
||||||
if v["type"] == "number":
|
method[k]["description"] = v["description"]
|
||||||
method[k]["value"] = 0
|
if v["type"] == "number":
|
||||||
elif v["type"] == "boolean":
|
method[k]["value"] = 0
|
||||||
method[k]["value"] = False
|
elif v["type"] == "boolean":
|
||||||
else:
|
method[k]["value"] = False
|
||||||
method[k]["value"] = ""
|
else:
|
||||||
self.methods.append(method)
|
method[k]["value"] = ""
|
||||||
|
self.methods.append(method)
|
||||||
|
|
||||||
|
|
||||||
def register_device_type(descriptor):
|
def register_device_type(descriptor):
|
||||||
|
|||||||
@@ -115,7 +115,7 @@ async def check_bind_device(conn):
|
|||||||
digit = conn.bind_code[i]
|
digit = conn.bind_code[i]
|
||||||
num_path = f"config/assets/bind_code/{digit}.wav"
|
num_path = f"config/assets/bind_code/{digit}.wav"
|
||||||
num_packets, _ = conn.tts.audio_to_opus_data(num_path)
|
num_packets, _ = conn.tts.audio_to_opus_data(num_path)
|
||||||
conn.audio_play_queue.put((num_packets, text, i + 1))
|
conn.audio_play_queue.put((num_packets, None, i + 1))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import json
|
|||||||
import asyncio
|
import asyncio
|
||||||
import time
|
import time
|
||||||
from core.utils.util import (
|
from core.utils.util import (
|
||||||
remove_punctuation_and_length,
|
|
||||||
get_string_no_punctuation_or_emoji,
|
get_string_no_punctuation_or_emoji,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,17 @@ import json
|
|||||||
import re
|
import re
|
||||||
from core.providers.llm.base import LLMProviderBase
|
from core.providers.llm.base import LLMProviderBase
|
||||||
import os
|
import os
|
||||||
|
|
||||||
# official coze sdk for Python [cozepy](https://github.com/coze-dev/coze-py)
|
# official coze sdk for Python [cozepy](https://github.com/coze-dev/coze-py)
|
||||||
from cozepy import COZE_CN_BASE_URL
|
from cozepy import COZE_CN_BASE_URL
|
||||||
from cozepy import Coze, TokenAuth, Message, ChatStatus, MessageContentType, ChatEventType # noqa
|
from cozepy import (
|
||||||
|
Coze,
|
||||||
|
TokenAuth,
|
||||||
|
Message,
|
||||||
|
ChatStatus,
|
||||||
|
MessageContentType,
|
||||||
|
ChatEventType,
|
||||||
|
) # noqa
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -15,8 +23,8 @@ logger = setup_logging()
|
|||||||
class LLMProvider(LLMProviderBase):
|
class LLMProvider(LLMProviderBase):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.personal_access_token = config.get("personal_access_token")
|
self.personal_access_token = config.get("personal_access_token")
|
||||||
self.bot_id = config.get("bot_id")
|
self.bot_id = str(config.get("bot_id"))
|
||||||
self.user_id = config.get("user_id")
|
self.user_id = str(config.get("user_id"))
|
||||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||||
|
|
||||||
def response(self, session_id, dialogue):
|
def response(self, session_id, dialogue):
|
||||||
@@ -24,16 +32,13 @@ class LLMProvider(LLMProviderBase):
|
|||||||
coze_api_base = COZE_CN_BASE_URL
|
coze_api_base = COZE_CN_BASE_URL
|
||||||
|
|
||||||
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
last_msg = next(m for m in reversed(dialogue) if m["role"] == "user")
|
||||||
|
|
||||||
coze = Coze(auth=TokenAuth(token=coze_api_token), base_url=coze_api_base)
|
coze = Coze(auth=TokenAuth(token=coze_api_token), base_url=coze_api_base)
|
||||||
conversation_id = self.session_conversation_map.get(session_id)
|
conversation_id = self.session_conversation_map.get(session_id)
|
||||||
|
|
||||||
# 如果没有找到conversation_id,则创建新的对话
|
# 如果没有找到conversation_id,则创建新的对话
|
||||||
if not conversation_id:
|
if not conversation_id:
|
||||||
conversation = coze.conversations.create(
|
conversation = coze.conversations.create(messages=[])
|
||||||
messages=[
|
|
||||||
]
|
|
||||||
)
|
|
||||||
conversation_id = conversation.id
|
conversation_id = conversation.id
|
||||||
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
self.session_conversation_map[session_id] = conversation_id # 更新映射
|
||||||
|
|
||||||
@@ -47,4 +52,4 @@ class LLMProvider(LLMProviderBase):
|
|||||||
):
|
):
|
||||||
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
|
if event.event == ChatEventType.CONVERSATION_MESSAGE_DELTA:
|
||||||
print(event.message.content, end="", flush=True)
|
print(event.message.content, end="", flush=True)
|
||||||
yield event.message.content
|
yield event.message.content
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, Field, conint, model_validator
|
|||||||
from typing_extensions import Annotated
|
from typing_extensions import Annotated
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
from core.utils.util import check_model_key
|
from core.utils.util import check_model_key, parse_string_to_list
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
|
|
||||||
@@ -86,8 +86,8 @@ class TTSProvider(TTSProviderBase):
|
|||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
self.reference_id = config.get("reference_id")
|
self.reference_id = config.get("reference_id")
|
||||||
self.reference_audio = config.get("reference_audio", [])
|
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
||||||
self.reference_text = config.get("reference_text", [])
|
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
||||||
self.format = config.get("format", "wav")
|
self.format = config.get("format", "wav")
|
||||||
self.channels = int(config.get("channels", 1))
|
self.channels = int(config.get("channels", 1))
|
||||||
self.rate = int(config.get("rate", 44100))
|
self.rate = int(config.get("rate", 44100))
|
||||||
@@ -101,9 +101,13 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.top_p = float(config.get("top_p", 0.7))
|
self.top_p = float(config.get("top_p", 0.7))
|
||||||
self.repetition_penalty = float(config.get("repetition_penalty", 1.2))
|
self.repetition_penalty = float(config.get("repetition_penalty", 1.2))
|
||||||
self.temperature = float(config.get("temperature", 0.7))
|
self.temperature = float(config.get("temperature", 0.7))
|
||||||
self.streaming = bool(config.get("streaming", False))
|
self.streaming = str(config.get("streaming", False)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.use_memory_cache = config.get("use_memory_cache", "on")
|
self.use_memory_cache = config.get("use_memory_cache", "on")
|
||||||
self.seed = config.get("seed")
|
self.seed = config.get("seed") or None
|
||||||
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import requests
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from core.utils.util import parse_string_to_list
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -25,14 +26,33 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.text_split_method = config.get("text_split_method", "cut0")
|
self.text_split_method = config.get("text_split_method", "cut0")
|
||||||
self.batch_size = int(config.get("batch_size", 1))
|
self.batch_size = int(config.get("batch_size", 1))
|
||||||
self.batch_threshold = float(config.get("batch_threshold", 0.75))
|
self.batch_threshold = float(config.get("batch_threshold", 0.75))
|
||||||
self.split_bucket = bool(config.get("split_bucket", True))
|
|
||||||
self.return_fragment = bool(config.get("return_fragment", False))
|
self.split_bucket = str(config.get("split_bucket", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
|
self.return_fragment = str(config.get("return_fragment", False)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.speed_factor = float(config.get("speed_factor", 1.0))
|
self.speed_factor = float(config.get("speed_factor", 1.0))
|
||||||
self.streaming_mode = bool(config.get("streaming_mode", False))
|
self.streaming_mode = str(config.get("streaming_mode", False)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.seed = int(config.get("seed", -1))
|
self.seed = int(config.get("seed", -1))
|
||||||
self.parallel_infer = bool(config.get("parallel_infer", True))
|
self.parallel_infer = str(config.get("parallel_infer", True)).lower() in (
|
||||||
|
"true",
|
||||||
|
"1",
|
||||||
|
"yes",
|
||||||
|
)
|
||||||
self.repetition_penalty = float(config.get("repetition_penalty", 1.35))
|
self.repetition_penalty = float(config.get("repetition_penalty", 1.35))
|
||||||
self.aux_ref_audio_paths = config.get("aux_ref_audio_paths", [])
|
self.aux_ref_audio_paths = parse_string_to_list(
|
||||||
|
config.get("aux_ref_audio_paths")
|
||||||
|
)
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import requests
|
|||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from core.utils.util import parse_string_to_list
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -22,9 +23,9 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.temperature = float(config.get("temperature", 1.0))
|
self.temperature = float(config.get("temperature", 1.0))
|
||||||
self.cut_punc = config.get("cut_punc", "")
|
self.cut_punc = config.get("cut_punc", "")
|
||||||
self.speed = float(config.get("speed", 1.0))
|
self.speed = float(config.get("speed", 1.0))
|
||||||
self.inp_refs = config.get("inp_refs", [])
|
self.inp_refs = parse_string_to_list(config.get("inp_refs"))
|
||||||
self.sample_steps = int(config.get("sample_steps", 32))
|
self.sample_steps = int(config.get("sample_steps", 32))
|
||||||
self.if_sr = bool(config.get("if_sr", False))
|
self.if_sr = str(config.get("if_sr", False)).lower() in ("true", "1", "yes")
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(
|
return os.path.join(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import json
|
|||||||
import requests
|
import requests
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from core.providers.tts.base import TTSProviderBase
|
from core.providers.tts.base import TTSProviderBase
|
||||||
|
from core.utils.util import parse_string_to_list
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
@@ -40,7 +41,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
**config.get("pronunciation_dict", {}),
|
**config.get("pronunciation_dict", {}),
|
||||||
}
|
}
|
||||||
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
self.audio_setting = {**defult_audio_setting, **config.get("audio_setting", {})}
|
||||||
self.timber_weights = config.get("timber_weights", [])
|
self.timber_weights = parse_string_to_list(config.get("timber_weights"))
|
||||||
|
|
||||||
if self.voice_id:
|
if self.voice_id:
|
||||||
self.voice_setting["voice_id"] = self.voice_id
|
self.voice_setting["voice_id"] = self.voice_id
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class TTSProvider(TTSProviderBase):
|
|||||||
self.to_lang = config.get("to_lang")
|
self.to_lang = config.get("to_lang")
|
||||||
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
||||||
self.speed_factor = int(config.get("speed_factor", 1))
|
self.speed_factor = int(config.get("speed_factor", 1))
|
||||||
self.stream = bool(config.get("stream", False))
|
self.stream = str(config.get("stream", False)).lower() in ("true", "1", "yes")
|
||||||
self.output_file = config.get("output_dir")
|
self.output_file = config.get("output_dir")
|
||||||
self.pitch_factor = int(config.get("pitch_factor", 0))
|
self.pitch_factor = int(config.get("pitch_factor", 0))
|
||||||
self.format = config.get("format", "mp3")
|
self.format = config.get("format", "mp3")
|
||||||
|
|||||||
@@ -163,6 +163,24 @@ def check_model_key(modelType, modelKey):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def parse_string_to_list(value, separator=";"):
|
||||||
|
"""
|
||||||
|
将输入值转换为列表
|
||||||
|
Args:
|
||||||
|
value: 输入值,可以是 None、字符串或列表
|
||||||
|
separator: 分隔符,默认为分号
|
||||||
|
Returns:
|
||||||
|
list: 处理后的列表
|
||||||
|
"""
|
||||||
|
if value is None or value == "":
|
||||||
|
return []
|
||||||
|
elif isinstance(value, str):
|
||||||
|
return [item.strip() for item in value.split(separator) if item.strip()]
|
||||||
|
elif isinstance(value, list):
|
||||||
|
return value
|
||||||
|
return []
|
||||||
|
|
||||||
|
|
||||||
def check_ffmpeg_installed():
|
def check_ffmpeg_installed():
|
||||||
ffmpeg_installed = False
|
ffmpeg_installed = False
|
||||||
try:
|
try:
|
||||||
@@ -231,7 +249,7 @@ def initialize_modules(
|
|||||||
modules["tts"] = tts.create_instance(
|
modules["tts"] = tts.create_instance(
|
||||||
tts_type,
|
tts_type,
|
||||||
config["TTS"][select_tts_module],
|
config["TTS"][select_tts_module],
|
||||||
bool(config.get("delete_audio", True)),
|
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: tts成功 {select_tts_module}")
|
logger.bind(tag=TAG).info(f"初始化组件: tts成功 {select_tts_module}")
|
||||||
|
|
||||||
@@ -302,7 +320,7 @@ def initialize_modules(
|
|||||||
modules["asr"] = asr.create_instance(
|
modules["asr"] = asr.create_instance(
|
||||||
asr_type,
|
asr_type,
|
||||||
config["ASR"][select_asr_module],
|
config["ASR"][select_asr_module],
|
||||||
bool(config.get("delete_audio", True)),
|
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
||||||
)
|
)
|
||||||
logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
|
logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,50 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section h2 .toggle-button {
|
||||||
|
margin-left: auto;
|
||||||
|
padding: 4px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-left: 20px;
|
||||||
|
padding: 0 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info span {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device-info strong {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel {
|
||||||
|
display: none;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
margin-top: 5px;
|
||||||
|
padding: 5px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-panel.expanded {
|
||||||
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
@@ -70,7 +114,8 @@
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#serverUrl {
|
#serverUrl,
|
||||||
|
#otaUrl {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
@@ -316,6 +361,76 @@
|
|||||||
gap: 20px;
|
gap: 20px;
|
||||||
margin-top: 10px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.config-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item label {
|
||||||
|
width: 100px;
|
||||||
|
text-align: right;
|
||||||
|
margin-right: 10px;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item input {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 6px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-controls input {
|
||||||
|
flex: 1;
|
||||||
|
padding: 8px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 5px;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-controls button {
|
||||||
|
white-space: nowrap;
|
||||||
|
padding: 8px 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 20px;
|
||||||
|
margin-left: 20px;
|
||||||
|
padding: 0 15px;
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 28px;
|
||||||
|
line-height: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status span {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.connection-status .status {
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
@@ -326,17 +441,56 @@
|
|||||||
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
||||||
正在加载Opus库...</div>
|
正在加载Opus库...</div>
|
||||||
|
|
||||||
|
<!-- 添加配置面板 -->
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>WebSocket连接 <span id="connectionStatus" class="status">未连接</span></h2>
|
<h2>
|
||||||
<div class="control-panel">
|
设备配置
|
||||||
<input type="text" id="serverUrl" value="ws://127.0.0.1:8000/xiaozhi/v1/" placeholder="WebSocket服务器地址">
|
<span class="device-info">
|
||||||
|
<span>MAC: <strong id="displayMac">00:11:22:33:44:55</strong></span>
|
||||||
|
<span>客户端: <strong id="displayClient">web_test_client</strong></span>
|
||||||
|
</span>
|
||||||
|
<button class="toggle-button" id="toggleConfig">编辑</button>
|
||||||
|
</h2>
|
||||||
|
<div class="config-panel" id="configPanel">
|
||||||
|
<div class="control-panel">
|
||||||
|
<div class="config-item">
|
||||||
|
<label for="deviceMac">设备MAC:</label>
|
||||||
|
<input type="text" id="deviceMac" value="00:11:22:33:44:55" placeholder="设备MAC地址">
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<label for="deviceName">设备名称:</label>
|
||||||
|
<input type="text" id="deviceName" value="Web测试设备" placeholder="设备名称">
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<label for="clientId">客户端ID:</label>
|
||||||
|
<input type="text" id="clientId" value="web_test_client" placeholder="客户端ID">
|
||||||
|
</div>
|
||||||
|
<div class="config-item">
|
||||||
|
<label for="token">认证Token:</label>
|
||||||
|
<input type="text" id="token" value="your-token1" placeholder="认证Token">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section">
|
||||||
|
<h2>
|
||||||
|
连接信息
|
||||||
|
<span class="connection-status">
|
||||||
|
<span>OTA: <span id="otaStatus" class="status">ota未连接</span></span>
|
||||||
|
<span>WS: <span id="connectionStatus" class="status">ws未连接</span></span>
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div class="connection-controls">
|
||||||
|
<input type="text" id="otaUrl" value="http://127.0.0.1:8002/xiaozhi/ota/" placeholder="OTA服务器地址" />
|
||||||
|
<input type="text" id="serverUrl" value="ws://127.0.0.1:8000/xiaozhi/v1/"
|
||||||
|
placeholder="WebSocket服务器地址" />
|
||||||
<button id="connectButton">连接</button>
|
<button id="connectButton">连接</button>
|
||||||
<button id="authTestButton">测试认证</button>
|
<button id="authTestButton">测试认证</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>消息发送</h2>
|
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<button class="tab active" data-tab="text">文本消息</button>
|
<button class="tab active" data-tab="text">文本消息</button>
|
||||||
<button class="tab" data-tab="voice">语音消息</button>
|
<button class="tab" data-tab="voice">语音消息</button>
|
||||||
@@ -554,16 +708,16 @@
|
|||||||
// 开始音频缓冲过程
|
// 开始音频缓冲过程
|
||||||
function startAudioBuffering() {
|
function startAudioBuffering() {
|
||||||
if (isAudioBuffering || isAudioPlaying) return;
|
if (isAudioBuffering || isAudioPlaying) return;
|
||||||
|
|
||||||
isAudioBuffering = true;
|
isAudioBuffering = true;
|
||||||
log("开始音频缓冲...", 'info');
|
log("开始音频缓冲...", 'info');
|
||||||
|
|
||||||
// 先尝试初始化解码器,以便在播放时已准备好
|
// 先尝试初始化解码器,以便在播放时已准备好
|
||||||
initOpusDecoder().catch(error => {
|
initOpusDecoder().catch(error => {
|
||||||
log(`预初始化Opus解码器失败: ${error.message}`, 'warning');
|
log(`预初始化Opus解码器失败: ${error.message}`, 'warning');
|
||||||
// 继续缓冲,我们会在播放时再次尝试初始化
|
// 继续缓冲,我们会在播放时再次尝试初始化
|
||||||
});
|
});
|
||||||
|
|
||||||
// 设置超时,如果在一定时间内没有收集到足够的音频包,就开始播放
|
// 设置超时,如果在一定时间内没有收集到足够的音频包,就开始播放
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (isAudioBuffering && audioBufferQueue.length > 0) {
|
if (isAudioBuffering && audioBufferQueue.length > 0) {
|
||||||
@@ -571,14 +725,14 @@
|
|||||||
playBufferedAudio();
|
playBufferedAudio();
|
||||||
}
|
}
|
||||||
}, 300); // 300ms超时
|
}, 300); // 300ms超时
|
||||||
|
|
||||||
// 监控缓冲进度
|
// 监控缓冲进度
|
||||||
const bufferCheckInterval = setInterval(() => {
|
const bufferCheckInterval = setInterval(() => {
|
||||||
if (!isAudioBuffering) {
|
if (!isAudioBuffering) {
|
||||||
clearInterval(bufferCheckInterval);
|
clearInterval(bufferCheckInterval);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 当累积了足够的音频包,开始播放
|
// 当累积了足够的音频包,开始播放
|
||||||
if (audioBufferQueue.length >= BUFFER_THRESHOLD) {
|
if (audioBufferQueue.length >= BUFFER_THRESHOLD) {
|
||||||
clearInterval(bufferCheckInterval);
|
clearInterval(bufferCheckInterval);
|
||||||
@@ -591,10 +745,10 @@
|
|||||||
// 播放已缓冲的音频
|
// 播放已缓冲的音频
|
||||||
function playBufferedAudio() {
|
function playBufferedAudio() {
|
||||||
if (isAudioPlaying || audioBufferQueue.length === 0) return;
|
if (isAudioPlaying || audioBufferQueue.length === 0) return;
|
||||||
|
|
||||||
isAudioPlaying = true;
|
isAudioPlaying = true;
|
||||||
isAudioBuffering = false;
|
isAudioBuffering = false;
|
||||||
|
|
||||||
// 确保Opus解码器已初始化
|
// 确保Opus解码器已初始化
|
||||||
const initDecoderAndPlay = async () => {
|
const initDecoderAndPlay = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -605,7 +759,7 @@
|
|||||||
});
|
});
|
||||||
log('创建音频上下文,采样率: ' + SAMPLE_RATE + 'Hz', 'debug');
|
log('创建音频上下文,采样率: ' + SAMPLE_RATE + 'Hz', 'debug');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确保解码器已初始化
|
// 确保解码器已初始化
|
||||||
if (!opusDecoder) {
|
if (!opusDecoder) {
|
||||||
log('初始化Opus解码器...', 'info');
|
log('初始化Opus解码器...', 'info');
|
||||||
@@ -621,7 +775,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建流式播放上下文
|
// 创建流式播放上下文
|
||||||
if (!streamingContext) {
|
if (!streamingContext) {
|
||||||
streamingContext = {
|
streamingContext = {
|
||||||
@@ -631,16 +785,16 @@
|
|||||||
source: null, // 当前音频源
|
source: null, // 当前音频源
|
||||||
totalSamples: 0, // 累积的总样本数
|
totalSamples: 0, // 累积的总样本数
|
||||||
lastPlayTime: 0, // 上次播放的时间戳
|
lastPlayTime: 0, // 上次播放的时间戳
|
||||||
|
|
||||||
// 将Opus数据解码为PCM
|
// 将Opus数据解码为PCM
|
||||||
decodeOpusFrames: async function(opusFrames) {
|
decodeOpusFrames: async function (opusFrames) {
|
||||||
if (!opusDecoder) {
|
if (!opusDecoder) {
|
||||||
log('Opus解码器未初始化,无法解码', 'error');
|
log('Opus解码器未初始化,无法解码', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let decodedSamples = [];
|
let decodedSamples = [];
|
||||||
|
|
||||||
for (const frame of opusFrames) {
|
for (const frame of opusFrames) {
|
||||||
try {
|
try {
|
||||||
// 使用Opus解码器解码
|
// 使用Opus解码器解码
|
||||||
@@ -654,12 +808,12 @@
|
|||||||
log("Opus解码失败: " + error.message, 'error');
|
log("Opus解码失败: " + error.message, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (decodedSamples.length > 0) {
|
if (decodedSamples.length > 0) {
|
||||||
// 添加到解码队列
|
// 添加到解码队列
|
||||||
this.queue.push(...decodedSamples);
|
this.queue.push(...decodedSamples);
|
||||||
this.totalSamples += decodedSamples.length;
|
this.totalSamples += decodedSamples.length;
|
||||||
|
|
||||||
// 如果累积了至少0.2秒的音频,开始播放
|
// 如果累积了至少0.2秒的音频,开始播放
|
||||||
const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION;
|
const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION;
|
||||||
if (!this.playing && this.queue.length >= minSamples) {
|
if (!this.playing && this.queue.length >= minSamples) {
|
||||||
@@ -669,50 +823,50 @@
|
|||||||
log('没有成功解码的样本', 'warning');
|
log('没有成功解码的样本', 'warning');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 开始播放音频
|
// 开始播放音频
|
||||||
startPlaying: function() {
|
startPlaying: function () {
|
||||||
if (this.playing || this.queue.length === 0) return;
|
if (this.playing || this.queue.length === 0) return;
|
||||||
|
|
||||||
this.playing = true;
|
this.playing = true;
|
||||||
|
|
||||||
// 创建新的音频缓冲区
|
// 创建新的音频缓冲区
|
||||||
const minPlaySamples = Math.min(this.queue.length, SAMPLE_RATE); // 最多播放1秒
|
const minPlaySamples = Math.min(this.queue.length, SAMPLE_RATE); // 最多播放1秒
|
||||||
const currentSamples = this.queue.splice(0, minPlaySamples);
|
const currentSamples = this.queue.splice(0, minPlaySamples);
|
||||||
|
|
||||||
const audioBuffer = audioContext.createBuffer(CHANNELS, currentSamples.length, SAMPLE_RATE);
|
const audioBuffer = audioContext.createBuffer(CHANNELS, currentSamples.length, SAMPLE_RATE);
|
||||||
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
|
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
|
||||||
|
|
||||||
// 创建音频源
|
// 创建音频源
|
||||||
this.source = audioContext.createBufferSource();
|
this.source = audioContext.createBufferSource();
|
||||||
this.source.buffer = audioBuffer;
|
this.source.buffer = audioBuffer;
|
||||||
|
|
||||||
// 创建增益节点用于平滑过渡
|
// 创建增益节点用于平滑过渡
|
||||||
const gainNode = audioContext.createGain();
|
const gainNode = audioContext.createGain();
|
||||||
|
|
||||||
// 应用淡入淡出效果避免爆音
|
// 应用淡入淡出效果避免爆音
|
||||||
const fadeDuration = 0.02; // 20毫秒
|
const fadeDuration = 0.02; // 20毫秒
|
||||||
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||||
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + fadeDuration);
|
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + fadeDuration);
|
||||||
|
|
||||||
const duration = audioBuffer.duration;
|
const duration = audioBuffer.duration;
|
||||||
if (duration > fadeDuration * 2) {
|
if (duration > fadeDuration * 2) {
|
||||||
gainNode.gain.setValueAtTime(1, audioContext.currentTime + duration - fadeDuration);
|
gainNode.gain.setValueAtTime(1, audioContext.currentTime + duration - fadeDuration);
|
||||||
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration);
|
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 连接节点并开始播放
|
// 连接节点并开始播放
|
||||||
this.source.connect(gainNode);
|
this.source.connect(gainNode);
|
||||||
gainNode.connect(audioContext.destination);
|
gainNode.connect(audioContext.destination);
|
||||||
|
|
||||||
this.lastPlayTime = audioContext.currentTime;
|
this.lastPlayTime = audioContext.currentTime;
|
||||||
log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)} 秒`, 'info');
|
log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)} 秒`, 'info');
|
||||||
|
|
||||||
// 播放结束后的处理
|
// 播放结束后的处理
|
||||||
this.source.onended = () => {
|
this.source.onended = () => {
|
||||||
this.source = null;
|
this.source = null;
|
||||||
this.playing = false;
|
this.playing = false;
|
||||||
|
|
||||||
// 如果队列中还有数据或者缓冲区有新数据,继续播放
|
// 如果队列中还有数据或者缓冲区有新数据,继续播放
|
||||||
if (this.queue.length > 0) {
|
if (this.queue.length > 0) {
|
||||||
setTimeout(() => this.startPlaying(), 10);
|
setTimeout(() => this.startPlaying(), 10);
|
||||||
@@ -743,26 +897,26 @@
|
|||||||
}, 500); // 500ms超时
|
}, 500); // 500ms超时
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
this.source.start();
|
this.source.start();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 开始处理缓冲的数据
|
// 开始处理缓冲的数据
|
||||||
const frames = [...audioBufferQueue];
|
const frames = [...audioBufferQueue];
|
||||||
audioBufferQueue = []; // 清空缓冲队列
|
audioBufferQueue = []; // 清空缓冲队列
|
||||||
|
|
||||||
// 解码并播放
|
// 解码并播放
|
||||||
await streamingContext.decodeOpusFrames(frames);
|
await streamingContext.decodeOpusFrames(frames);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`播放已缓冲的音频出错: ${error.message}`, 'error');
|
log(`播放已缓冲的音频出错: ${error.message}`, 'error');
|
||||||
isAudioPlaying = false;
|
isAudioPlaying = false;
|
||||||
streamingContext = null;
|
streamingContext = null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 执行初始化和播放
|
// 执行初始化和播放
|
||||||
initDecoderAndPlay();
|
initDecoderAndPlay();
|
||||||
}
|
}
|
||||||
@@ -780,7 +934,7 @@
|
|||||||
// 初始化Opus解码器 - 确保完全初始化完成后才返回
|
// 初始化Opus解码器 - 确保完全初始化完成后才返回
|
||||||
async function initOpusDecoder() {
|
async function initOpusDecoder() {
|
||||||
if (opusDecoder) return opusDecoder; // 已经初始化
|
if (opusDecoder) return opusDecoder; // 已经初始化
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 检查ModuleInstance是否存在
|
// 检查ModuleInstance是否存在
|
||||||
if (typeof window.ModuleInstance === 'undefined') {
|
if (typeof window.ModuleInstance === 'undefined') {
|
||||||
@@ -792,9 +946,9 @@
|
|||||||
throw new Error('Opus库未加载,ModuleInstance和Module对象都不存在');
|
throw new Error('Opus库未加载,ModuleInstance和Module对象都不存在');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const mod = window.ModuleInstance;
|
const mod = window.ModuleInstance;
|
||||||
|
|
||||||
// 创建解码器对象
|
// 创建解码器对象
|
||||||
opusDecoder = {
|
opusDecoder = {
|
||||||
channels: CHANNELS,
|
channels: CHANNELS,
|
||||||
@@ -802,55 +956,55 @@
|
|||||||
frameSize: FRAME_SIZE,
|
frameSize: FRAME_SIZE,
|
||||||
module: mod,
|
module: mod,
|
||||||
decoderPtr: null, // 初始为null
|
decoderPtr: null, // 初始为null
|
||||||
|
|
||||||
// 初始化解码器
|
// 初始化解码器
|
||||||
init: function() {
|
init: function () {
|
||||||
if (this.decoderPtr) return true; // 已经初始化
|
if (this.decoderPtr) return true; // 已经初始化
|
||||||
|
|
||||||
// 获取解码器大小
|
// 获取解码器大小
|
||||||
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
||||||
log(`Opus解码器大小: ${decoderSize}字节`, 'debug');
|
log(`Opus解码器大小: ${decoderSize}字节`, 'debug');
|
||||||
|
|
||||||
// 分配内存
|
// 分配内存
|
||||||
this.decoderPtr = mod._malloc(decoderSize);
|
this.decoderPtr = mod._malloc(decoderSize);
|
||||||
if (!this.decoderPtr) {
|
if (!this.decoderPtr) {
|
||||||
throw new Error("无法分配解码器内存");
|
throw new Error("无法分配解码器内存");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化解码器
|
// 初始化解码器
|
||||||
const err = mod._opus_decoder_init(
|
const err = mod._opus_decoder_init(
|
||||||
this.decoderPtr,
|
this.decoderPtr,
|
||||||
this.rate,
|
this.rate,
|
||||||
this.channels
|
this.channels
|
||||||
);
|
);
|
||||||
|
|
||||||
if (err < 0) {
|
if (err < 0) {
|
||||||
this.destroy(); // 清理资源
|
this.destroy(); // 清理资源
|
||||||
throw new Error(`Opus解码器初始化失败: ${err}`);
|
throw new Error(`Opus解码器初始化失败: ${err}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
log("Opus解码器初始化成功", 'success');
|
log("Opus解码器初始化成功", 'success');
|
||||||
return true;
|
return true;
|
||||||
},
|
},
|
||||||
|
|
||||||
// 解码方法
|
// 解码方法
|
||||||
decode: function(opusData) {
|
decode: function (opusData) {
|
||||||
if (!this.decoderPtr) {
|
if (!this.decoderPtr) {
|
||||||
if (!this.init()) {
|
if (!this.init()) {
|
||||||
throw new Error("解码器未初始化且无法初始化");
|
throw new Error("解码器未初始化且无法初始化");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const mod = this.module;
|
const mod = this.module;
|
||||||
|
|
||||||
// 为Opus数据分配内存
|
// 为Opus数据分配内存
|
||||||
const opusPtr = mod._malloc(opusData.length);
|
const opusPtr = mod._malloc(opusData.length);
|
||||||
mod.HEAPU8.set(opusData, opusPtr);
|
mod.HEAPU8.set(opusData, opusPtr);
|
||||||
|
|
||||||
// 为PCM输出分配内存
|
// 为PCM输出分配内存
|
||||||
const pcmPtr = mod._malloc(this.frameSize * 2); // Int16 = 2字节
|
const pcmPtr = mod._malloc(this.frameSize * 2); // Int16 = 2字节
|
||||||
|
|
||||||
// 解码
|
// 解码
|
||||||
const decodedSamples = mod._opus_decode(
|
const decodedSamples = mod._opus_decode(
|
||||||
this.decoderPtr,
|
this.decoderPtr,
|
||||||
@@ -860,46 +1014,46 @@
|
|||||||
this.frameSize,
|
this.frameSize,
|
||||||
0 // 不使用FEC
|
0 // 不使用FEC
|
||||||
);
|
);
|
||||||
|
|
||||||
if (decodedSamples < 0) {
|
if (decodedSamples < 0) {
|
||||||
mod._free(opusPtr);
|
mod._free(opusPtr);
|
||||||
mod._free(pcmPtr);
|
mod._free(pcmPtr);
|
||||||
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 复制解码后的数据
|
// 复制解码后的数据
|
||||||
const decodedData = new Int16Array(decodedSamples);
|
const decodedData = new Int16Array(decodedSamples);
|
||||||
for (let i = 0; i < decodedSamples; i++) {
|
for (let i = 0; i < decodedSamples; i++) {
|
||||||
decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];
|
decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 释放内存
|
// 释放内存
|
||||||
mod._free(opusPtr);
|
mod._free(opusPtr);
|
||||||
mod._free(pcmPtr);
|
mod._free(pcmPtr);
|
||||||
|
|
||||||
return decodedData;
|
return decodedData;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Opus解码错误: ${error.message}`, 'error');
|
log(`Opus解码错误: ${error.message}`, 'error');
|
||||||
return new Int16Array(0);
|
return new Int16Array(0);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
// 销毁方法
|
// 销毁方法
|
||||||
destroy: function() {
|
destroy: function () {
|
||||||
if (this.decoderPtr) {
|
if (this.decoderPtr) {
|
||||||
this.module._free(this.decoderPtr);
|
this.module._free(this.decoderPtr);
|
||||||
this.decoderPtr = null;
|
this.decoderPtr = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 初始化解码器
|
// 初始化解码器
|
||||||
if (!opusDecoder.init()) {
|
if (!opusDecoder.init()) {
|
||||||
throw new Error("Opus解码器初始化失败");
|
throw new Error("Opus解码器初始化失败");
|
||||||
}
|
}
|
||||||
|
|
||||||
return opusDecoder;
|
return opusDecoder;
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Opus解码器初始化失败: ${error.message}`, 'error');
|
log(`Opus解码器初始化失败: ${error.message}`, 'error');
|
||||||
opusDecoder = null; // 重置为null,以便下次重试
|
opusDecoder = null; // 重置为null,以便下次重试
|
||||||
@@ -1132,25 +1286,100 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 连接WebSocket服务器
|
// 连接WebSocket服务器
|
||||||
function connectToServer() {
|
async function connectToServer() {
|
||||||
const url = serverUrlInput.value.trim();
|
const url = serverUrlInput.value.trim();
|
||||||
if (url === '') return;
|
if (url === '') return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 获取并验证配置
|
||||||
|
const config = getConfig();
|
||||||
|
if (!validateConfig(config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// 检查URL格式
|
// 检查URL格式
|
||||||
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
||||||
log('URL格式错误,必须以ws://或wss://开头', 'error');
|
log('URL格式错误,必须以ws://或wss://开头', 'error');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 先检查OTA状态
|
||||||
|
log('正在检查OTA状态...', 'info');
|
||||||
|
const otaUrl = document.getElementById('otaUrl').value.trim();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const otaResponse = await fetch(otaUrl, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'Device-Id': config.deviceId,
|
||||||
|
'Client-Id': config.clientId
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
"version": 0,
|
||||||
|
"uuid": "",
|
||||||
|
"application": {
|
||||||
|
"name": "xiaozhi-web-test",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"compile_time": "2025-04-16 10:00:00",
|
||||||
|
"idf_version": "4.4.3",
|
||||||
|
"elf_sha256": "1234567890abcdef1234567890abcdef1234567890abcdef"
|
||||||
|
},
|
||||||
|
"ota": {
|
||||||
|
"label": "xiaozhi-web-test",
|
||||||
|
},
|
||||||
|
"board": {
|
||||||
|
"type": "xiaozhi-web-test",
|
||||||
|
"ssid": "xiaozhi-web-test",
|
||||||
|
"rssi": 0,
|
||||||
|
"channel": 0,
|
||||||
|
"ip": "192.168.1.1",
|
||||||
|
"mac": config.deviceMac
|
||||||
|
},
|
||||||
|
"flash_size": 0,
|
||||||
|
"minimum_free_heap_size": 0,
|
||||||
|
"mac_address": config.deviceMac,
|
||||||
|
"chip_model_name": "",
|
||||||
|
"chip_info": {
|
||||||
|
"model": 0,
|
||||||
|
"cores": 0,
|
||||||
|
"revision": 0,
|
||||||
|
"features": 0
|
||||||
|
},
|
||||||
|
"partition_table": [
|
||||||
|
{
|
||||||
|
"label": "",
|
||||||
|
"type": 0,
|
||||||
|
"subtype": 0,
|
||||||
|
"address": 0,
|
||||||
|
"size": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!otaResponse.ok) {
|
||||||
|
throw new Error(`OTA检查失败: ${otaResponse.status} ${otaResponse.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const otaResult = await otaResponse.json();
|
||||||
|
log(`OTA检查结果: ${JSON.stringify(otaResult)}`, 'info');
|
||||||
|
|
||||||
|
log('OTA检查通过,开始连接WebSocket...', 'success');
|
||||||
|
document.getElementById('otaStatus').textContent = 'ota已连接';
|
||||||
|
document.getElementById('otaStatus').style.color = 'green';
|
||||||
|
} catch (error) {
|
||||||
|
log(`OTA检查错误: ${error.message}`, 'error');
|
||||||
|
document.getElementById('otaStatus').textContent = 'ota未连接';
|
||||||
|
document.getElementById('otaStatus').style.color = 'red';
|
||||||
|
}
|
||||||
|
|
||||||
// 使用自定义WebSocket实现以添加认证头信息
|
// 使用自定义WebSocket实现以添加认证头信息
|
||||||
// 注意:浏览器原生WebSocket不支持自定义头,需要通过服务器添加一个代理层
|
|
||||||
// 但我们可以通过URL参数模拟认证信息
|
|
||||||
let connUrl = new URL(url);
|
let connUrl = new URL(url);
|
||||||
|
|
||||||
// 添加认证参数
|
// 添加认证参数
|
||||||
connUrl.searchParams.append('device_id', 'web_test_device');
|
connUrl.searchParams.append('device-id', config.deviceId);
|
||||||
connUrl.searchParams.append('device_mac', '00:11:22:33:44:55');
|
connUrl.searchParams.append('client-id', config.clientId);
|
||||||
|
|
||||||
log(`正在连接: ${connUrl.toString()}`, 'info');
|
log(`正在连接: ${connUrl.toString()}`, 'info');
|
||||||
websocket = new WebSocket(connUrl.toString());
|
websocket = new WebSocket(connUrl.toString());
|
||||||
@@ -1160,7 +1389,7 @@
|
|||||||
|
|
||||||
websocket.onopen = async () => {
|
websocket.onopen = async () => {
|
||||||
log(`已连接到服务器: ${url}`, 'success');
|
log(`已连接到服务器: ${url}`, 'success');
|
||||||
connectionStatus.textContent = '已连接';
|
connectionStatus.textContent = 'ws已连接';
|
||||||
connectionStatus.style.color = 'green';
|
connectionStatus.style.color = 'green';
|
||||||
|
|
||||||
// 连接成功后发送hello消息
|
// 连接成功后发送hello消息
|
||||||
@@ -1181,7 +1410,7 @@
|
|||||||
|
|
||||||
websocket.onclose = () => {
|
websocket.onclose = () => {
|
||||||
log('已断开连接', 'info');
|
log('已断开连接', 'info');
|
||||||
connectionStatus.textContent = '已断开';
|
connectionStatus.textContent = 'ws已断开';
|
||||||
connectionStatus.style.color = 'red';
|
connectionStatus.style.color = 'red';
|
||||||
|
|
||||||
connectButton.textContent = '连接';
|
connectButton.textContent = '连接';
|
||||||
@@ -1196,7 +1425,7 @@
|
|||||||
|
|
||||||
websocket.onerror = (error) => {
|
websocket.onerror = (error) => {
|
||||||
log(`WebSocket错误: ${error.message || '未知错误'}`, 'error');
|
log(`WebSocket错误: ${error.message || '未知错误'}`, 'error');
|
||||||
connectionStatus.textContent = '连接错误';
|
connectionStatus.textContent = 'ws未连接';
|
||||||
connectionStatus.style.color = 'red';
|
connectionStatus.style.color = 'red';
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1262,11 +1491,11 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
connectionStatus.textContent = '正在连接...';
|
connectionStatus.textContent = 'ws未连接';
|
||||||
connectionStatus.style.color = 'orange';
|
connectionStatus.style.color = 'orange';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`连接错误: ${error.message}`, 'error');
|
log(`连接错误: ${error.message}`, 'error');
|
||||||
connectionStatus.textContent = '连接失败';
|
connectionStatus.textContent = 'ws未连接';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1275,13 +1504,15 @@
|
|||||||
if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
|
if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
// 设置设备信息
|
// 设置设备信息
|
||||||
const helloMessage = {
|
const helloMessage = {
|
||||||
type: 'hello',
|
type: 'hello',
|
||||||
device_id: 'web_test_device',
|
device_id: config.deviceId,
|
||||||
device_name: 'Web测试设备',
|
device_name: config.deviceName,
|
||||||
device_mac: '00:11:22:33:44:55',
|
device_mac: config.deviceMac,
|
||||||
token: 'your-token1' // 使用config.yaml中配置的token
|
token: config.token
|
||||||
};
|
};
|
||||||
|
|
||||||
log('发送hello握手消息', 'info');
|
log('发送hello握手消息', 'info');
|
||||||
@@ -1356,6 +1587,34 @@
|
|||||||
connectButton.addEventListener('click', connectToServer);
|
connectButton.addEventListener('click', connectToServer);
|
||||||
document.getElementById('authTestButton').addEventListener('click', testAuthentication);
|
document.getElementById('authTestButton').addEventListener('click', testAuthentication);
|
||||||
|
|
||||||
|
// 设备配置面板折叠/展开
|
||||||
|
const toggleButton = document.getElementById('toggleConfig');
|
||||||
|
const configPanel = document.getElementById('configPanel');
|
||||||
|
const deviceMacInput = document.getElementById('deviceMac');
|
||||||
|
const clientIdInput = document.getElementById('clientId');
|
||||||
|
const displayMac = document.getElementById('displayMac');
|
||||||
|
const displayClient = document.getElementById('displayClient');
|
||||||
|
|
||||||
|
// 更新显示的值
|
||||||
|
function updateDisplayValues() {
|
||||||
|
displayMac.textContent = deviceMacInput.value;
|
||||||
|
displayClient.textContent = clientIdInput.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听输入变化
|
||||||
|
deviceMacInput.addEventListener('input', updateDisplayValues);
|
||||||
|
clientIdInput.addEventListener('input', updateDisplayValues);
|
||||||
|
|
||||||
|
// 初始更新显示值
|
||||||
|
updateDisplayValues();
|
||||||
|
|
||||||
|
// 切换面板显示
|
||||||
|
toggleButton.addEventListener('click', () => {
|
||||||
|
const isExpanded = configPanel.classList.contains('expanded');
|
||||||
|
configPanel.classList.toggle('expanded');
|
||||||
|
toggleButton.textContent = isExpanded ? '编辑' : '收起';
|
||||||
|
});
|
||||||
|
|
||||||
// 标签页切换
|
// 标签页切换
|
||||||
const tabs = document.querySelectorAll('.tab');
|
const tabs = document.querySelectorAll('.tab');
|
||||||
tabs.forEach(tab => {
|
tabs.forEach(tab => {
|
||||||
@@ -1390,12 +1649,14 @@
|
|||||||
async function testAuthentication() {
|
async function testAuthentication() {
|
||||||
log('开始测试认证...', 'info');
|
log('开始测试认证...', 'info');
|
||||||
|
|
||||||
|
const config = getConfig();
|
||||||
|
|
||||||
// 显示服务器配置
|
// 显示服务器配置
|
||||||
log('-------- 服务器认证配置检查 --------', 'info');
|
log('-------- 服务器认证配置检查 --------', 'info');
|
||||||
log('请确认config.yaml中的auth配置:', 'info');
|
log('请确认config.yaml中的auth配置:', 'info');
|
||||||
log('1. server.auth.enabled 为 false 或服务器已正确配置认证', 'info');
|
log('1. server.auth.enabled 为 false 或服务器已正确配置认证', 'info');
|
||||||
log('2. 如果启用了认证,请确认使用了正确的token', 'info');
|
log('2. 如果启用了认证,请确认使用了正确的token', 'info');
|
||||||
log('3. 或者在allowed_devices中添加了测试设备MAC:00:11:22:33:44:55', 'info');
|
log(`3. 或者在allowed_devices中添加了测试设备MAC:${config.deviceMac}`, 'info');
|
||||||
|
|
||||||
const serverUrl = serverUrlInput.value.trim();
|
const serverUrl = serverUrlInput.value.trim();
|
||||||
if (!serverUrl) {
|
if (!serverUrl) {
|
||||||
@@ -1436,9 +1697,9 @@
|
|||||||
log('测试2: 尝试带token参数连接...', 'info');
|
log('测试2: 尝试带token参数连接...', 'info');
|
||||||
|
|
||||||
let url = new URL(serverUrl);
|
let url = new URL(serverUrl);
|
||||||
url.searchParams.append('token', 'your-token1');
|
url.searchParams.append('token', config.token);
|
||||||
url.searchParams.append('device_id', 'web_test_device');
|
url.searchParams.append('device_id', config.deviceId);
|
||||||
url.searchParams.append('device_mac', '00:11:22:33:44:55');
|
url.searchParams.append('device_mac', config.deviceMac);
|
||||||
|
|
||||||
const ws2 = new WebSocket(url.toString());
|
const ws2 = new WebSocket(url.toString());
|
||||||
|
|
||||||
@@ -1448,9 +1709,9 @@
|
|||||||
// 尝试发送hello消息
|
// 尝试发送hello消息
|
||||||
const helloMsg = {
|
const helloMsg = {
|
||||||
type: 'hello',
|
type: 'hello',
|
||||||
device_id: 'web_test_device',
|
device_id: config.deviceId,
|
||||||
device_mac: '00:11:22:33:44:55',
|
device_mac: config.deviceMac,
|
||||||
token: 'your-token1'
|
token: config.token
|
||||||
};
|
};
|
||||||
|
|
||||||
ws2.send(JSON.stringify(helloMsg));
|
ws2.send(JSON.stringify(helloMsg));
|
||||||
@@ -2092,19 +2353,19 @@
|
|||||||
if (opusData.length > 0) {
|
if (opusData.length > 0) {
|
||||||
// 将数据添加到缓冲队列
|
// 将数据添加到缓冲队列
|
||||||
audioBufferQueue.push(opusData);
|
audioBufferQueue.push(opusData);
|
||||||
|
|
||||||
// 如果收到的是第一个音频包,开始缓冲过程
|
// 如果收到的是第一个音频包,开始缓冲过程
|
||||||
if (audioBufferQueue.length === 1 && !isAudioBuffering && !isAudioPlaying) {
|
if (audioBufferQueue.length === 1 && !isAudioBuffering && !isAudioPlaying) {
|
||||||
startAudioBuffering();
|
startAudioBuffering();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log('收到空音频数据帧,可能是结束标志', 'warning');
|
log('收到空音频数据帧,可能是结束标志', 'warning');
|
||||||
|
|
||||||
// 如果缓冲队列中有数据且没有在播放,立即开始播放
|
// 如果缓冲队列中有数据且没有在播放,立即开始播放
|
||||||
if (audioBufferQueue.length > 0 && !isAudioPlaying) {
|
if (audioBufferQueue.length > 0 && !isAudioPlaying) {
|
||||||
playBufferedAudio();
|
playBufferedAudio();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果正在播放,发送结束信号
|
// 如果正在播放,发送结束信号
|
||||||
if (isAudioPlaying && streamingContext) {
|
if (isAudioPlaying && streamingContext) {
|
||||||
streamingContext.endOfStream = true;
|
streamingContext.endOfStream = true;
|
||||||
@@ -2115,6 +2376,31 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 获取配置值
|
||||||
|
function getConfig() {
|
||||||
|
const deviceMac = document.getElementById('deviceMac').value.trim();
|
||||||
|
return {
|
||||||
|
deviceId: deviceMac, // 使用MAC地址作为deviceId
|
||||||
|
deviceName: document.getElementById('deviceName').value.trim(),
|
||||||
|
deviceMac: deviceMac,
|
||||||
|
clientId: document.getElementById('clientId').value.trim(),
|
||||||
|
token: document.getElementById('token').value.trim()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证配置
|
||||||
|
function validateConfig(config) {
|
||||||
|
if (!config.deviceMac) {
|
||||||
|
log('设备MAC地址不能为空', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!config.clientId) {
|
||||||
|
log('客户端ID不能为空', 'error');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
initApp();
|
initApp();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user