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(
|
||||
@RequestBody DeviceReportReqDTO deviceReportReqDTO,
|
||||
@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) {
|
||||
if (StringUtils.isAnyBlank(deviceId, clientId)) {
|
||||
@Parameter(name = "Client-Id", description = "客户端标识", required = false, in = ParameterIn.HEADER) @RequestHeader(value = "Client-Id", required = false) String clientId) {
|
||||
if (StringUtils.isBlank(deviceId)) {
|
||||
return createResponse(DeviceReportRespDTO.createError("Device ID is required"));
|
||||
}
|
||||
if (StringUtils.isBlank(clientId)) {
|
||||
clientId = deviceId;
|
||||
}
|
||||
String macAddress = deviceReportReqDTO.getMacAddress();
|
||||
boolean macAddressValid = NetworkUtil.isMacAddressValid(macAddress);
|
||||
// 设备Id和Mac地址应是一致的, 并且必须需要application字段
|
||||
|
||||
+1
-1
@@ -121,7 +121,7 @@ public class LoginController {
|
||||
@Operation(summary = "公共配置")
|
||||
public Result<Map<String, Object>> pubConfig() {
|
||||
Map<String, Object> config = new HashMap<>();
|
||||
config.put("version", "0.3.4");
|
||||
config.put("version", "0.3.5");
|
||||
config.put("allowUserRegister", sysUserService.getAllowUserRegister());
|
||||
return new Result<Map<String, Object>>().ok(config);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" center>
|
||||
<el-dialog :visible="visible" @close="handleClose" width="400px" center>
|
||||
<div
|
||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div
|
||||
@@ -79,7 +79,10 @@ export default {
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.deviceCode = ""
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<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>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<el-dialog :visible.sync="visible" width="400px" center @open="handleOpen">
|
||||
<el-dialog :visible="visible" @close="handleClose" width="400px" center @open="handleOpen">
|
||||
<div
|
||||
style="margin: 0 10px 10px;display: flex;align-items: center;gap: 10px;font-weight: 700;font-size: 20px;text-align: left;color: #3d4566;">
|
||||
<div
|
||||
@@ -66,7 +66,10 @@ export default {
|
||||
cancel() {
|
||||
this.$emit('update:visible', false)
|
||||
this.wisdomBodyName = ""
|
||||
}
|
||||
},
|
||||
handleClose() {
|
||||
this.$emit('update:visible', false);
|
||||
},
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -83,7 +83,8 @@ export default {
|
||||
font-size: 10px;
|
||||
color: #5778ff;
|
||||
background: #e6ebff;
|
||||
width: 57px;
|
||||
width: auto;
|
||||
padding: 0 12px;
|
||||
height: 21px;
|
||||
line-height: 21px;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -551,28 +551,26 @@ export default {
|
||||
|
||||
/* 备注文本 */
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f5f5f5;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #e6e6e6;
|
||||
padding: 8px 12px;
|
||||
resize: none;
|
||||
max-height: 40px !important;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner::placeholder {
|
||||
color: black !important;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
::v-deep .remark-input .el-textarea__inner {
|
||||
background-color: #f4f6fa;
|
||||
background-color: transparent !important;
|
||||
}
|
||||
|
||||
::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 {
|
||||
display: flex;
|
||||
@@ -650,6 +648,12 @@ export default {
|
||||
bottom: 20px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
.action-buttons .el-button {
|
||||
padding: 8px 15px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.edit-btn,
|
||||
.delete-btn,
|
||||
.save-btn {
|
||||
|
||||
@@ -115,8 +115,8 @@ export default {
|
||||
activeSearchKeyword: "",
|
||||
currentAgentId: this.$route.query.agentId || '',
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
deviceList: [],
|
||||
loading: false,
|
||||
userApi: null,
|
||||
@@ -283,13 +283,18 @@ export default {
|
||||
this.deviceList = data.data.map(device => {
|
||||
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')}`;
|
||||
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 {
|
||||
device_id: device.id,
|
||||
model: device.board,
|
||||
firmwareVersion: device.appVersion,
|
||||
macAddress: device.macAddress,
|
||||
bindTime: formattedBindTime,
|
||||
lastConversation: device.lastConnectedAt,
|
||||
lastConversation: formattedLastConversation,
|
||||
remark: device.alias,
|
||||
isEdit: false,
|
||||
otaSwitch: device.autoUpdate === 1,
|
||||
@@ -331,7 +336,7 @@ export default {
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 200px);
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
@@ -628,12 +633,12 @@ export default {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: calc(100vh - 330px);
|
||||
max-height: calc(100vh - 40vh);
|
||||
}
|
||||
|
||||
:deep(.el-table__body-wrapper) {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
|
||||
@@ -152,7 +152,7 @@ export default {
|
||||
ttsDialogVisible: false,
|
||||
selectedTtsModelId: '',
|
||||
modelList: [],
|
||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
@@ -461,7 +461,7 @@ export default {
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 235px);
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
@@ -656,7 +656,6 @@ export default {
|
||||
}
|
||||
|
||||
.table-footer {
|
||||
margin-top: 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
@@ -904,7 +903,7 @@ export default {
|
||||
}
|
||||
|
||||
.data-table {
|
||||
--table-max-height: calc(100vh - 450px);
|
||||
--table-max-height: calc(100vh - 45vh);
|
||||
max-height: var(--table-max-height);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,8 +92,8 @@ export default {
|
||||
searchCode: "",
|
||||
paramsList: [],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
||||
pageSize: 10,
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
total: 0,
|
||||
dialogVisible: false,
|
||||
dialogTitle: "新增参数",
|
||||
@@ -330,7 +330,7 @@ export default {
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 350px);
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
@@ -516,7 +516,7 @@ export default {
|
||||
flex-direction: column;
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
@@ -649,7 +649,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 400px);
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
|
||||
@@ -99,9 +99,9 @@ export default {
|
||||
currentPassword: "",
|
||||
searchPhone: "",
|
||||
userList: [],
|
||||
pageSizeOptions: [5, 10, 20, 50, 100],
|
||||
pageSizeOptions: [10, 20, 50, 100],
|
||||
currentPage: 1,
|
||||
pageSize: 5,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
isAllSelected: false
|
||||
};
|
||||
@@ -355,7 +355,7 @@ export default {
|
||||
.main-wrapper {
|
||||
margin: 5px 22px;
|
||||
border-radius: 15px;
|
||||
min-height: calc(100vh - 350px);
|
||||
min-height: calc(100vh - 24vh);
|
||||
height: auto;
|
||||
max-height: 80vh;
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
||||
@@ -557,7 +557,7 @@ export default {
|
||||
flex-direction: column;
|
||||
.el-table__body-wrapper {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
overflow-y: auto;
|
||||
max-height: none !important;
|
||||
}
|
||||
|
||||
@@ -674,7 +674,7 @@ export default {
|
||||
}
|
||||
|
||||
.el-table {
|
||||
--table-max-height: calc(100vh - 400px);
|
||||
--table-max-height: calc(100vh - 40vh);
|
||||
max-height: var(--table-max-height);
|
||||
|
||||
.el-table__body-wrapper {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<div class="welcome" @keyup.enter="register">
|
||||
<el-container style="height: 100%;">
|
||||
<!-- 保持相同的头部 -->
|
||||
<el-header>
|
||||
|
||||
@@ -1,88 +1,126 @@
|
||||
<template>
|
||||
<div class="welcome">
|
||||
<HeaderBar />
|
||||
<el-main style="padding: 16px;display: flex;flex-direction: column;">
|
||||
<div style="border-radius: 16px;background: #fafcfe; border: 1px solid #e8f0ff;">
|
||||
<div
|
||||
style="padding: 15px 24px;font-weight: 700;font-size: 19px;text-align: left;color: #3d4566;display: flex;gap: 13px;align-items: center;">
|
||||
<div
|
||||
style="width: 37px;height: 37px;background: #5778ff;border-radius: 50%;display: flex;align-items: center;justify-content: center;">
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="" style="width: 19px;height: 19px;" />
|
||||
</div>
|
||||
{{ form.agentName }}
|
||||
</div>
|
||||
<div style="height: 1px;background: #e8f0ff;" />
|
||||
<el-form ref="form" :model="form" label-width="72px">
|
||||
<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>
|
||||
<HeaderBar/>
|
||||
|
||||
<div class="operation-bar">
|
||||
<h2 class="page-title">角色配置</h2>
|
||||
</div>
|
||||
|
||||
<div class="main-wrapper">
|
||||
<div class="content-panel">
|
||||
<div class="content-area">
|
||||
<el-card class="config-card" shadow="never">
|
||||
<div class="config-header">
|
||||
<div class="header-icon">
|
||||
<img loading="lazy" src="@/assets/home/setting-user.png" alt="">
|
||||
</div>
|
||||
<span class="header-title">{{ form.agentName }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</el-form>
|
||||
<div style="display: flex;padding: 16px;gap: 8px;align-items: center;">
|
||||
<div class="save-btn" @click="saveConfig">
|
||||
保存配置
|
||||
</div>
|
||||
<div class="reset-btn" @click="resetConfig">
|
||||
重制
|
||||
</div>
|
||||
<div class="clear-text">
|
||||
<img loading="lazy" src="@/assets/home/red-info.png" alt="" style="width: 19px;height: 19px;" />
|
||||
保存配置后,需要重启设备,新的配置才会生效。
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
|
||||
<el-form ref="form" :model="form" label-width="72px">
|
||||
<div class="form-content">
|
||||
<div class="form-grid">
|
||||
<div class="form-column">
|
||||
<el-form-item label="助手昵称:">
|
||||
<el-input v-model="form.agentName" class="form-input"/>
|
||||
</el-form-item>
|
||||
<el-form-item label="角色模版:">
|
||||
<div class="template-container">
|
||||
<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>
|
||||
|
||||
</el-main>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,10 +128,9 @@
|
||||
import Api from '@/apis/api';
|
||||
import HeaderBar from "@/components/HeaderBar.vue";
|
||||
|
||||
|
||||
export default {
|
||||
name: 'RoleConfigPage',
|
||||
components: { HeaderBar },
|
||||
components: {HeaderBar},
|
||||
data() {
|
||||
return {
|
||||
form: {
|
||||
@@ -114,12 +151,12 @@ export default {
|
||||
}
|
||||
},
|
||||
models: [
|
||||
{ label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD' },
|
||||
{ label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR' },
|
||||
{ label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM' },
|
||||
{ label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent' },
|
||||
{ label: '记忆(Memory)', key: 'memModelId', type: 'Memory' },
|
||||
{ label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS' },
|
||||
{label: '语音活动检测(VAD)', key: 'vadModelId', type: 'VAD'},
|
||||
{label: '语音识别(ASR)', key: 'asrModelId', type: 'ASR'},
|
||||
{label: '大语言模型(LLM)', key: 'llmModelId', type: 'LLM'},
|
||||
{label: '意图识别(Intent)', key: 'intentModelId', type: 'Intent'},
|
||||
{label: '记忆(Memory)', key: 'memModelId', type: 'Memory'},
|
||||
{label: '语音合成(TTS)', key: 'ttsModelId', type: 'TTS'},
|
||||
],
|
||||
modelOptions: {},
|
||||
templates: [],
|
||||
@@ -144,7 +181,7 @@ export default {
|
||||
language: this.form.language,
|
||||
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) {
|
||||
this.$message.success({
|
||||
message: '配置保存成功',
|
||||
@@ -164,7 +201,6 @@ export default {
|
||||
cancelButtonText: '取消',
|
||||
type: 'warning'
|
||||
}).then(() => {
|
||||
// 重置表单
|
||||
this.form = {
|
||||
agentCode: "",
|
||||
agentName: "",
|
||||
@@ -187,10 +223,10 @@ export default {
|
||||
showClose: true
|
||||
})
|
||||
}).catch(() => {
|
||||
})
|
||||
});
|
||||
},
|
||||
fetchTemplates() {
|
||||
Api.agent.getAgentTemplate(({ data }) => {
|
||||
Api.agent.getAgentTemplate(({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.templates = data.data;
|
||||
} else {
|
||||
@@ -235,7 +271,7 @@ export default {
|
||||
};
|
||||
},
|
||||
fetchAgentConfig(agentId) {
|
||||
Api.agent.getDeviceConfig(agentId, ({ data }) => {
|
||||
Api.agent.getDeviceConfig(agentId, ({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.form = {
|
||||
...this.form,
|
||||
@@ -255,9 +291,8 @@ export default {
|
||||
});
|
||||
},
|
||||
fetchModelOptions() {
|
||||
// 为每个模型类型获取选项
|
||||
this.models.forEach(model => {
|
||||
Api.model.getModelNames(model.type, '', ({ data }) => {
|
||||
Api.model.getModelNames(model.type, '', ({data}) => {
|
||||
if (data.code === 0) {
|
||||
this.$set(this.modelOptions, model.type, data.data.map(item => ({
|
||||
value: item.id,
|
||||
@@ -274,7 +309,7 @@ export default {
|
||||
this.voiceOptions = [];
|
||||
return;
|
||||
}
|
||||
Api.model.getModelVoices(modelId, '', ({ data }) => {
|
||||
Api.model.getModelVoices(modelId, '', ({data}) => {
|
||||
if (data.code === 0 && data.data) {
|
||||
this.voiceOptions = data.data.map(voice => ({
|
||||
value: voice.id,
|
||||
@@ -289,7 +324,6 @@ export default {
|
||||
watch: {
|
||||
'form.model.ttsModelId': {
|
||||
handler(newVal, oldVal) {
|
||||
console.log('TTS模型变化:', newVal);
|
||||
if (oldVal && newVal !== oldVal) {
|
||||
this.form.ttsVoiceId = '';
|
||||
this.fetchVoiceOptions(newVal);
|
||||
@@ -325,60 +359,131 @@ export default {
|
||||
min-height: 506px;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
position: relative;
|
||||
flex-direction: column;
|
||||
background: linear-gradient(145deg, #e6eeff, #eff0ff);
|
||||
background: linear-gradient(to bottom right, #dce8ff, #e4eeff, #e6cbfd);
|
||||
background-size: cover;
|
||||
/* 确保背景图像覆盖整个元素 */
|
||||
background-position: center;
|
||||
/* 从顶部中心对齐 */
|
||||
-webkit-background-size: cover;
|
||||
/* 兼容老版本WebKit浏览器 */
|
||||
-o-background-size: cover;
|
||||
/* 兼容老版本Opera浏览器 */
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.el-form-item ::v-deep .el-form-item__label {
|
||||
font-size: 10px !important;
|
||||
color: #3d4566 !important;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
padding-bottom: 2px;
|
||||
.operation-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
.select-field {
|
||||
width: 100%;
|
||||
max-width: 720px;
|
||||
border: 1px solid #e4e6ef;
|
||||
background: #f6f8fb;
|
||||
border-radius: 8px;
|
||||
height: 36px !important;
|
||||
.page-title {
|
||||
font-size: 24px;
|
||||
margin: 0;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.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;
|
||||
height: 37px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid #e4e6ef;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
border-radius: 15px;
|
||||
background: transparent;
|
||||
border: 1px solid #fff;
|
||||
}
|
||||
|
||||
.clear-btn {
|
||||
width: 48px;
|
||||
height: 19px;
|
||||
background: #fd8383;
|
||||
border-radius: 10px;
|
||||
line-height: 19px;
|
||||
font-size: 11px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
.content-area {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
min-width: 600px;
|
||||
overflow: auto;
|
||||
background-color: white;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.clear-text {
|
||||
color: #979db1;
|
||||
font-size: 11px;
|
||||
.config-card {
|
||||
background: white;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.config-header {
|
||||
display: flex;
|
||||
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;
|
||||
margin-left: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.template-item {
|
||||
@@ -399,47 +504,59 @@ export default {
|
||||
background-color: #d0d8ff;
|
||||
}
|
||||
|
||||
.prompt-bottom {
|
||||
margin-bottom: 4px;
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
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 {
|
||||
border-radius: 18px;
|
||||
background: #5778ff;
|
||||
color: #fff;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 18px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.reset-btn {
|
||||
border: 1px solid #adbdff;
|
||||
background: #e6ebff;
|
||||
color: #5778ff;
|
||||
border: 1px solid #adbdff;
|
||||
border-radius: 18px;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
.textarea-box {
|
||||
border: 1px solid #e4e6ef;
|
||||
border-radius: 8px;
|
||||
background: #f6f8fb;
|
||||
.hint-text {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 config.config_loader import load_config
|
||||
|
||||
SERVER_VERSION = "0.3.4"
|
||||
SERVER_VERSION = "0.3.5"
|
||||
|
||||
|
||||
def get_module_abbreviation(module_name, module_dict):
|
||||
|
||||
@@ -116,6 +116,27 @@ class ConnectionHandler:
|
||||
try:
|
||||
# 获取并验证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地址
|
||||
self.client_ip = ws.remote_address[0]
|
||||
self.logger.bind(tag=TAG).info(
|
||||
@@ -184,7 +205,6 @@ class ConnectionHandler:
|
||||
self._initialize_models()
|
||||
|
||||
"""加载提示词"""
|
||||
self.prompt = self.config["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"])
|
||||
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)
|
||||
|
||||
if tts_file is not None and os.path.exists(tts_file):
|
||||
|
||||
@@ -144,33 +144,35 @@ class IotDescriptor:
|
||||
self.methods = []
|
||||
|
||||
# 根据描述创建属性
|
||||
for key, value in properties.items():
|
||||
property_item = globals()[key] = {}
|
||||
property_item["name"] = key
|
||||
property_item["description"] = value["description"]
|
||||
if value["type"] == "number":
|
||||
property_item["value"] = 0
|
||||
elif value["type"] == "boolean":
|
||||
property_item["value"] = False
|
||||
else:
|
||||
property_item["value"] = ""
|
||||
self.properties.append(property_item)
|
||||
if properties is not None:
|
||||
for key, value in properties.items():
|
||||
property_item = globals()[key] = {}
|
||||
property_item["name"] = key
|
||||
property_item["description"] = value["description"]
|
||||
if value["type"] == "number":
|
||||
property_item["value"] = 0
|
||||
elif value["type"] == "boolean":
|
||||
property_item["value"] = False
|
||||
else:
|
||||
property_item["value"] = ""
|
||||
self.properties.append(property_item)
|
||||
|
||||
# 根据描述创建方法
|
||||
for key, value in methods.items():
|
||||
method = globals()[key] = {}
|
||||
method["description"] = value["description"]
|
||||
method["name"] = key
|
||||
for k, v in value["parameters"].items():
|
||||
method[k] = {}
|
||||
method[k]["description"] = v["description"]
|
||||
if v["type"] == "number":
|
||||
method[k]["value"] = 0
|
||||
elif v["type"] == "boolean":
|
||||
method[k]["value"] = False
|
||||
else:
|
||||
method[k]["value"] = ""
|
||||
self.methods.append(method)
|
||||
if methods is not None:
|
||||
for key, value in methods.items():
|
||||
method = globals()[key] = {}
|
||||
method["description"] = value["description"]
|
||||
method["name"] = key
|
||||
for k, v in value["parameters"].items():
|
||||
method[k] = {}
|
||||
method[k]["description"] = v["description"]
|
||||
if v["type"] == "number":
|
||||
method[k]["value"] = 0
|
||||
elif v["type"] == "boolean":
|
||||
method[k]["value"] = False
|
||||
else:
|
||||
method[k]["value"] = ""
|
||||
self.methods.append(method)
|
||||
|
||||
|
||||
def register_device_type(descriptor):
|
||||
|
||||
@@ -115,7 +115,7 @@ async def check_bind_device(conn):
|
||||
digit = conn.bind_code[i]
|
||||
num_path = f"config/assets/bind_code/{digit}.wav"
|
||||
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:
|
||||
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
||||
continue
|
||||
|
||||
@@ -3,7 +3,6 @@ import json
|
||||
import asyncio
|
||||
import time
|
||||
from core.utils.util import (
|
||||
remove_punctuation_and_length,
|
||||
get_string_no_punctuation_or_emoji,
|
||||
)
|
||||
|
||||
|
||||
@@ -4,9 +4,17 @@ import json
|
||||
import re
|
||||
from core.providers.llm.base import LLMProviderBase
|
||||
import os
|
||||
|
||||
# official coze sdk for Python [cozepy](https://github.com/coze-dev/coze-py)
|
||||
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__
|
||||
logger = setup_logging()
|
||||
@@ -15,8 +23,8 @@ logger = setup_logging()
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
self.personal_access_token = config.get("personal_access_token")
|
||||
self.bot_id = config.get("bot_id")
|
||||
self.user_id = config.get("user_id")
|
||||
self.bot_id = str(config.get("bot_id"))
|
||||
self.user_id = str(config.get("user_id"))
|
||||
self.session_conversation_map = {} # 存储session_id和conversation_id的映射
|
||||
|
||||
def response(self, session_id, dialogue):
|
||||
@@ -24,16 +32,13 @@ class LLMProvider(LLMProviderBase):
|
||||
coze_api_base = COZE_CN_BASE_URL
|
||||
|
||||
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)
|
||||
conversation_id = self.session_conversation_map.get(session_id)
|
||||
|
||||
# 如果没有找到conversation_id,则创建新的对话
|
||||
if not conversation_id:
|
||||
conversation = coze.conversations.create(
|
||||
messages=[
|
||||
]
|
||||
)
|
||||
conversation = coze.conversations.create(messages=[])
|
||||
conversation_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:
|
||||
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 datetime import datetime
|
||||
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 config.logger import setup_logging
|
||||
|
||||
@@ -86,8 +86,8 @@ class TTSProvider(TTSProviderBase):
|
||||
super().__init__(config, delete_audio_file)
|
||||
|
||||
self.reference_id = config.get("reference_id")
|
||||
self.reference_audio = config.get("reference_audio", [])
|
||||
self.reference_text = config.get("reference_text", [])
|
||||
self.reference_audio = parse_string_to_list(config.get("reference_audio"))
|
||||
self.reference_text = parse_string_to_list(config.get("reference_text"))
|
||||
self.format = config.get("format", "wav")
|
||||
self.channels = int(config.get("channels", 1))
|
||||
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.repetition_penalty = float(config.get("repetition_penalty", 1.2))
|
||||
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.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")
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
|
||||
@@ -6,6 +6,7 @@ import requests
|
||||
from config.logger import setup_logging
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.utils.util import parse_string_to_list
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -25,14 +26,33 @@ class TTSProvider(TTSProviderBase):
|
||||
self.text_split_method = config.get("text_split_method", "cut0")
|
||||
self.batch_size = int(config.get("batch_size", 1))
|
||||
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.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.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.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"):
|
||||
return os.path.join(
|
||||
|
||||
@@ -4,6 +4,7 @@ import requests
|
||||
from config.logger import setup_logging
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.utils.util import parse_string_to_list
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -22,9 +23,9 @@ class TTSProvider(TTSProviderBase):
|
||||
self.temperature = float(config.get("temperature", 1.0))
|
||||
self.cut_punc = config.get("cut_punc", "")
|
||||
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.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"):
|
||||
return os.path.join(
|
||||
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from core.utils.util import parse_string_to_list
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
@@ -40,7 +41,7 @@ class TTSProvider(TTSProviderBase):
|
||||
**config.get("pronunciation_dict", {}),
|
||||
}
|
||||
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:
|
||||
self.voice_setting["voice_id"] = self.voice_id
|
||||
|
||||
@@ -22,7 +22,7 @@ class TTSProvider(TTSProviderBase):
|
||||
self.to_lang = config.get("to_lang")
|
||||
self.volume_change_dB = int(config.get("volume_change_dB", 0))
|
||||
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.pitch_factor = int(config.get("pitch_factor", 0))
|
||||
self.format = config.get("format", "mp3")
|
||||
|
||||
@@ -163,6 +163,24 @@ def check_model_key(modelType, modelKey):
|
||||
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():
|
||||
ffmpeg_installed = False
|
||||
try:
|
||||
@@ -231,7 +249,7 @@ def initialize_modules(
|
||||
modules["tts"] = tts.create_instance(
|
||||
tts_type,
|
||||
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}")
|
||||
|
||||
@@ -302,7 +320,7 @@ def initialize_modules(
|
||||
modules["asr"] = asr.create_instance(
|
||||
asr_type,
|
||||
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}")
|
||||
|
||||
|
||||
@@ -42,6 +42,50 @@
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
@@ -70,7 +114,8 @@
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
#serverUrl {
|
||||
#serverUrl,
|
||||
#otaUrl {
|
||||
flex-grow: 1;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
@@ -316,6 +361,76 @@
|
||||
gap: 20px;
|
||||
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>
|
||||
</head>
|
||||
|
||||
@@ -326,17 +441,56 @@
|
||||
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
||||
正在加载Opus库...</div>
|
||||
|
||||
<!-- 添加配置面板 -->
|
||||
<div class="section">
|
||||
<h2>WebSocket连接 <span id="connectionStatus" class="status">未连接</span></h2>
|
||||
<div class="control-panel">
|
||||
<input type="text" id="serverUrl" value="ws://127.0.0.1:8000/xiaozhi/v1/" placeholder="WebSocket服务器地址">
|
||||
<h2>
|
||||
设备配置
|
||||
<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="authTestButton">测试认证</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h2>消息发送</h2>
|
||||
<div class="tabs">
|
||||
<button class="tab active" data-tab="text">文本消息</button>
|
||||
<button class="tab" data-tab="voice">语音消息</button>
|
||||
@@ -554,16 +708,16 @@
|
||||
// 开始音频缓冲过程
|
||||
function startAudioBuffering() {
|
||||
if (isAudioBuffering || isAudioPlaying) return;
|
||||
|
||||
|
||||
isAudioBuffering = true;
|
||||
log("开始音频缓冲...", 'info');
|
||||
|
||||
|
||||
// 先尝试初始化解码器,以便在播放时已准备好
|
||||
initOpusDecoder().catch(error => {
|
||||
log(`预初始化Opus解码器失败: ${error.message}`, 'warning');
|
||||
// 继续缓冲,我们会在播放时再次尝试初始化
|
||||
});
|
||||
|
||||
|
||||
// 设置超时,如果在一定时间内没有收集到足够的音频包,就开始播放
|
||||
setTimeout(() => {
|
||||
if (isAudioBuffering && audioBufferQueue.length > 0) {
|
||||
@@ -571,14 +725,14 @@
|
||||
playBufferedAudio();
|
||||
}
|
||||
}, 300); // 300ms超时
|
||||
|
||||
|
||||
// 监控缓冲进度
|
||||
const bufferCheckInterval = setInterval(() => {
|
||||
if (!isAudioBuffering) {
|
||||
clearInterval(bufferCheckInterval);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
// 当累积了足够的音频包,开始播放
|
||||
if (audioBufferQueue.length >= BUFFER_THRESHOLD) {
|
||||
clearInterval(bufferCheckInterval);
|
||||
@@ -591,10 +745,10 @@
|
||||
// 播放已缓冲的音频
|
||||
function playBufferedAudio() {
|
||||
if (isAudioPlaying || audioBufferQueue.length === 0) return;
|
||||
|
||||
|
||||
isAudioPlaying = true;
|
||||
isAudioBuffering = false;
|
||||
|
||||
|
||||
// 确保Opus解码器已初始化
|
||||
const initDecoderAndPlay = async () => {
|
||||
try {
|
||||
@@ -605,7 +759,7 @@
|
||||
});
|
||||
log('创建音频上下文,采样率: ' + SAMPLE_RATE + 'Hz', 'debug');
|
||||
}
|
||||
|
||||
|
||||
// 确保解码器已初始化
|
||||
if (!opusDecoder) {
|
||||
log('初始化Opus解码器...', 'info');
|
||||
@@ -621,7 +775,7 @@
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 创建流式播放上下文
|
||||
if (!streamingContext) {
|
||||
streamingContext = {
|
||||
@@ -631,16 +785,16 @@
|
||||
source: null, // 当前音频源
|
||||
totalSamples: 0, // 累积的总样本数
|
||||
lastPlayTime: 0, // 上次播放的时间戳
|
||||
|
||||
|
||||
// 将Opus数据解码为PCM
|
||||
decodeOpusFrames: async function(opusFrames) {
|
||||
decodeOpusFrames: async function (opusFrames) {
|
||||
if (!opusDecoder) {
|
||||
log('Opus解码器未初始化,无法解码', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
let decodedSamples = [];
|
||||
|
||||
|
||||
for (const frame of opusFrames) {
|
||||
try {
|
||||
// 使用Opus解码器解码
|
||||
@@ -654,12 +808,12 @@
|
||||
log("Opus解码失败: " + error.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (decodedSamples.length > 0) {
|
||||
// 添加到解码队列
|
||||
this.queue.push(...decodedSamples);
|
||||
this.totalSamples += decodedSamples.length;
|
||||
|
||||
|
||||
// 如果累积了至少0.2秒的音频,开始播放
|
||||
const minSamples = SAMPLE_RATE * MIN_AUDIO_DURATION;
|
||||
if (!this.playing && this.queue.length >= minSamples) {
|
||||
@@ -669,50 +823,50 @@
|
||||
log('没有成功解码的样本', 'warning');
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 开始播放音频
|
||||
startPlaying: function() {
|
||||
startPlaying: function () {
|
||||
if (this.playing || this.queue.length === 0) return;
|
||||
|
||||
|
||||
this.playing = true;
|
||||
|
||||
|
||||
// 创建新的音频缓冲区
|
||||
const minPlaySamples = Math.min(this.queue.length, SAMPLE_RATE); // 最多播放1秒
|
||||
const currentSamples = this.queue.splice(0, minPlaySamples);
|
||||
|
||||
|
||||
const audioBuffer = audioContext.createBuffer(CHANNELS, currentSamples.length, SAMPLE_RATE);
|
||||
audioBuffer.copyToChannel(new Float32Array(currentSamples), 0);
|
||||
|
||||
|
||||
// 创建音频源
|
||||
this.source = audioContext.createBufferSource();
|
||||
this.source.buffer = audioBuffer;
|
||||
|
||||
|
||||
// 创建增益节点用于平滑过渡
|
||||
const gainNode = audioContext.createGain();
|
||||
|
||||
|
||||
// 应用淡入淡出效果避免爆音
|
||||
const fadeDuration = 0.02; // 20毫秒
|
||||
gainNode.gain.setValueAtTime(0, audioContext.currentTime);
|
||||
gainNode.gain.linearRampToValueAtTime(1, audioContext.currentTime + fadeDuration);
|
||||
|
||||
|
||||
const duration = audioBuffer.duration;
|
||||
if (duration > fadeDuration * 2) {
|
||||
gainNode.gain.setValueAtTime(1, audioContext.currentTime + duration - fadeDuration);
|
||||
gainNode.gain.linearRampToValueAtTime(0, audioContext.currentTime + duration);
|
||||
}
|
||||
|
||||
|
||||
// 连接节点并开始播放
|
||||
this.source.connect(gainNode);
|
||||
gainNode.connect(audioContext.destination);
|
||||
|
||||
|
||||
this.lastPlayTime = audioContext.currentTime;
|
||||
log(`开始播放 ${currentSamples.length} 个样本,约 ${(currentSamples.length / SAMPLE_RATE).toFixed(2)} 秒`, 'info');
|
||||
|
||||
|
||||
// 播放结束后的处理
|
||||
this.source.onended = () => {
|
||||
this.source = null;
|
||||
this.playing = false;
|
||||
|
||||
|
||||
// 如果队列中还有数据或者缓冲区有新数据,继续播放
|
||||
if (this.queue.length > 0) {
|
||||
setTimeout(() => this.startPlaying(), 10);
|
||||
@@ -743,26 +897,26 @@
|
||||
}, 500); // 500ms超时
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
this.source.start();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// 开始处理缓冲的数据
|
||||
const frames = [...audioBufferQueue];
|
||||
audioBufferQueue = []; // 清空缓冲队列
|
||||
|
||||
|
||||
// 解码并播放
|
||||
await streamingContext.decodeOpusFrames(frames);
|
||||
|
||||
|
||||
} catch (error) {
|
||||
log(`播放已缓冲的音频出错: ${error.message}`, 'error');
|
||||
isAudioPlaying = false;
|
||||
streamingContext = null;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 执行初始化和播放
|
||||
initDecoderAndPlay();
|
||||
}
|
||||
@@ -780,7 +934,7 @@
|
||||
// 初始化Opus解码器 - 确保完全初始化完成后才返回
|
||||
async function initOpusDecoder() {
|
||||
if (opusDecoder) return opusDecoder; // 已经初始化
|
||||
|
||||
|
||||
try {
|
||||
// 检查ModuleInstance是否存在
|
||||
if (typeof window.ModuleInstance === 'undefined') {
|
||||
@@ -792,9 +946,9 @@
|
||||
throw new Error('Opus库未加载,ModuleInstance和Module对象都不存在');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const mod = window.ModuleInstance;
|
||||
|
||||
|
||||
// 创建解码器对象
|
||||
opusDecoder = {
|
||||
channels: CHANNELS,
|
||||
@@ -802,55 +956,55 @@
|
||||
frameSize: FRAME_SIZE,
|
||||
module: mod,
|
||||
decoderPtr: null, // 初始为null
|
||||
|
||||
|
||||
// 初始化解码器
|
||||
init: function() {
|
||||
init: function () {
|
||||
if (this.decoderPtr) return true; // 已经初始化
|
||||
|
||||
|
||||
// 获取解码器大小
|
||||
const decoderSize = mod._opus_decoder_get_size(this.channels);
|
||||
log(`Opus解码器大小: ${decoderSize}字节`, 'debug');
|
||||
|
||||
|
||||
// 分配内存
|
||||
this.decoderPtr = mod._malloc(decoderSize);
|
||||
if (!this.decoderPtr) {
|
||||
throw new Error("无法分配解码器内存");
|
||||
}
|
||||
|
||||
|
||||
// 初始化解码器
|
||||
const err = mod._opus_decoder_init(
|
||||
this.decoderPtr,
|
||||
this.rate,
|
||||
this.channels
|
||||
);
|
||||
|
||||
|
||||
if (err < 0) {
|
||||
this.destroy(); // 清理资源
|
||||
throw new Error(`Opus解码器初始化失败: ${err}`);
|
||||
}
|
||||
|
||||
|
||||
log("Opus解码器初始化成功", 'success');
|
||||
return true;
|
||||
},
|
||||
|
||||
|
||||
// 解码方法
|
||||
decode: function(opusData) {
|
||||
decode: function (opusData) {
|
||||
if (!this.decoderPtr) {
|
||||
if (!this.init()) {
|
||||
throw new Error("解码器未初始化且无法初始化");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
const mod = this.module;
|
||||
|
||||
|
||||
// 为Opus数据分配内存
|
||||
const opusPtr = mod._malloc(opusData.length);
|
||||
mod.HEAPU8.set(opusData, opusPtr);
|
||||
|
||||
|
||||
// 为PCM输出分配内存
|
||||
const pcmPtr = mod._malloc(this.frameSize * 2); // Int16 = 2字节
|
||||
|
||||
|
||||
// 解码
|
||||
const decodedSamples = mod._opus_decode(
|
||||
this.decoderPtr,
|
||||
@@ -860,46 +1014,46 @@
|
||||
this.frameSize,
|
||||
0 // 不使用FEC
|
||||
);
|
||||
|
||||
|
||||
if (decodedSamples < 0) {
|
||||
mod._free(opusPtr);
|
||||
mod._free(pcmPtr);
|
||||
throw new Error(`Opus解码失败: ${decodedSamples}`);
|
||||
}
|
||||
|
||||
|
||||
// 复制解码后的数据
|
||||
const decodedData = new Int16Array(decodedSamples);
|
||||
for (let i = 0; i < decodedSamples; i++) {
|
||||
decodedData[i] = mod.HEAP16[(pcmPtr >> 1) + i];
|
||||
}
|
||||
|
||||
|
||||
// 释放内存
|
||||
mod._free(opusPtr);
|
||||
mod._free(pcmPtr);
|
||||
|
||||
|
||||
return decodedData;
|
||||
} catch (error) {
|
||||
log(`Opus解码错误: ${error.message}`, 'error');
|
||||
return new Int16Array(0);
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
// 销毁方法
|
||||
destroy: function() {
|
||||
destroy: function () {
|
||||
if (this.decoderPtr) {
|
||||
this.module._free(this.decoderPtr);
|
||||
this.decoderPtr = null;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
// 初始化解码器
|
||||
if (!opusDecoder.init()) {
|
||||
throw new Error("Opus解码器初始化失败");
|
||||
}
|
||||
|
||||
|
||||
return opusDecoder;
|
||||
|
||||
|
||||
} catch (error) {
|
||||
log(`Opus解码器初始化失败: ${error.message}`, 'error');
|
||||
opusDecoder = null; // 重置为null,以便下次重试
|
||||
@@ -1132,25 +1286,100 @@
|
||||
}
|
||||
|
||||
// 连接WebSocket服务器
|
||||
function connectToServer() {
|
||||
async function connectToServer() {
|
||||
const url = serverUrlInput.value.trim();
|
||||
if (url === '') return;
|
||||
|
||||
try {
|
||||
// 获取并验证配置
|
||||
const config = getConfig();
|
||||
if (!validateConfig(config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 检查URL格式
|
||||
if (!url.startsWith('ws://') && !url.startsWith('wss://')) {
|
||||
log('URL格式错误,必须以ws://或wss://开头', 'error');
|
||||
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不支持自定义头,需要通过服务器添加一个代理层
|
||||
// 但我们可以通过URL参数模拟认证信息
|
||||
let connUrl = new URL(url);
|
||||
|
||||
// 添加认证参数
|
||||
connUrl.searchParams.append('device_id', 'web_test_device');
|
||||
connUrl.searchParams.append('device_mac', '00:11:22:33:44:55');
|
||||
connUrl.searchParams.append('device-id', config.deviceId);
|
||||
connUrl.searchParams.append('client-id', config.clientId);
|
||||
|
||||
log(`正在连接: ${connUrl.toString()}`, 'info');
|
||||
websocket = new WebSocket(connUrl.toString());
|
||||
@@ -1160,7 +1389,7 @@
|
||||
|
||||
websocket.onopen = async () => {
|
||||
log(`已连接到服务器: ${url}`, 'success');
|
||||
connectionStatus.textContent = '已连接';
|
||||
connectionStatus.textContent = 'ws已连接';
|
||||
connectionStatus.style.color = 'green';
|
||||
|
||||
// 连接成功后发送hello消息
|
||||
@@ -1181,7 +1410,7 @@
|
||||
|
||||
websocket.onclose = () => {
|
||||
log('已断开连接', 'info');
|
||||
connectionStatus.textContent = '已断开';
|
||||
connectionStatus.textContent = 'ws已断开';
|
||||
connectionStatus.style.color = 'red';
|
||||
|
||||
connectButton.textContent = '连接';
|
||||
@@ -1196,7 +1425,7 @@
|
||||
|
||||
websocket.onerror = (error) => {
|
||||
log(`WebSocket错误: ${error.message || '未知错误'}`, 'error');
|
||||
connectionStatus.textContent = '连接错误';
|
||||
connectionStatus.textContent = 'ws未连接';
|
||||
connectionStatus.style.color = 'red';
|
||||
};
|
||||
|
||||
@@ -1262,11 +1491,11 @@
|
||||
}
|
||||
};
|
||||
|
||||
connectionStatus.textContent = '正在连接...';
|
||||
connectionStatus.textContent = 'ws未连接';
|
||||
connectionStatus.style.color = 'orange';
|
||||
} catch (error) {
|
||||
log(`连接错误: ${error.message}`, 'error');
|
||||
connectionStatus.textContent = '连接失败';
|
||||
connectionStatus.textContent = 'ws未连接';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1275,13 +1504,15 @@
|
||||
if (!websocket || websocket.readyState !== WebSocket.OPEN) return;
|
||||
|
||||
try {
|
||||
const config = getConfig();
|
||||
|
||||
// 设置设备信息
|
||||
const helloMessage = {
|
||||
type: 'hello',
|
||||
device_id: 'web_test_device',
|
||||
device_name: 'Web测试设备',
|
||||
device_mac: '00:11:22:33:44:55',
|
||||
token: 'your-token1' // 使用config.yaml中配置的token
|
||||
device_id: config.deviceId,
|
||||
device_name: config.deviceName,
|
||||
device_mac: config.deviceMac,
|
||||
token: config.token
|
||||
};
|
||||
|
||||
log('发送hello握手消息', 'info');
|
||||
@@ -1356,6 +1587,34 @@
|
||||
connectButton.addEventListener('click', connectToServer);
|
||||
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');
|
||||
tabs.forEach(tab => {
|
||||
@@ -1390,12 +1649,14 @@
|
||||
async function testAuthentication() {
|
||||
log('开始测试认证...', 'info');
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
// 显示服务器配置
|
||||
log('-------- 服务器认证配置检查 --------', 'info');
|
||||
log('请确认config.yaml中的auth配置:', 'info');
|
||||
log('1. server.auth.enabled 为 false 或服务器已正确配置认证', '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();
|
||||
if (!serverUrl) {
|
||||
@@ -1436,9 +1697,9 @@
|
||||
log('测试2: 尝试带token参数连接...', 'info');
|
||||
|
||||
let url = new URL(serverUrl);
|
||||
url.searchParams.append('token', 'your-token1');
|
||||
url.searchParams.append('device_id', 'web_test_device');
|
||||
url.searchParams.append('device_mac', '00:11:22:33:44:55');
|
||||
url.searchParams.append('token', config.token);
|
||||
url.searchParams.append('device_id', config.deviceId);
|
||||
url.searchParams.append('device_mac', config.deviceMac);
|
||||
|
||||
const ws2 = new WebSocket(url.toString());
|
||||
|
||||
@@ -1448,9 +1709,9 @@
|
||||
// 尝试发送hello消息
|
||||
const helloMsg = {
|
||||
type: 'hello',
|
||||
device_id: 'web_test_device',
|
||||
device_mac: '00:11:22:33:44:55',
|
||||
token: 'your-token1'
|
||||
device_id: config.deviceId,
|
||||
device_mac: config.deviceMac,
|
||||
token: config.token
|
||||
};
|
||||
|
||||
ws2.send(JSON.stringify(helloMsg));
|
||||
@@ -2092,19 +2353,19 @@
|
||||
if (opusData.length > 0) {
|
||||
// 将数据添加到缓冲队列
|
||||
audioBufferQueue.push(opusData);
|
||||
|
||||
|
||||
// 如果收到的是第一个音频包,开始缓冲过程
|
||||
if (audioBufferQueue.length === 1 && !isAudioBuffering && !isAudioPlaying) {
|
||||
startAudioBuffering();
|
||||
}
|
||||
} else {
|
||||
log('收到空音频数据帧,可能是结束标志', 'warning');
|
||||
|
||||
|
||||
// 如果缓冲队列中有数据且没有在播放,立即开始播放
|
||||
if (audioBufferQueue.length > 0 && !isAudioPlaying) {
|
||||
playBufferedAudio();
|
||||
}
|
||||
|
||||
|
||||
// 如果正在播放,发送结束信号
|
||||
if (isAudioPlaying && streamingContext) {
|
||||
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();
|
||||
</script>
|
||||
</body>
|
||||
|
||||
Reference in New Issue
Block a user