mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 01:53:53 +08:00
update:优化测试web工具
This commit is contained in:
+3
@@ -19,6 +19,7 @@ import xiaozhi.common.page.TokenDTO;
|
|||||||
import xiaozhi.common.user.UserDetail;
|
import xiaozhi.common.user.UserDetail;
|
||||||
import xiaozhi.common.utils.Result;
|
import xiaozhi.common.utils.Result;
|
||||||
import xiaozhi.common.validator.AssertUtils;
|
import xiaozhi.common.validator.AssertUtils;
|
||||||
|
import xiaozhi.common.validator.ValidatorUtils;
|
||||||
import xiaozhi.modules.security.dto.LoginDTO;
|
import xiaozhi.modules.security.dto.LoginDTO;
|
||||||
import xiaozhi.modules.security.password.PasswordUtils;
|
import xiaozhi.modules.security.password.PasswordUtils;
|
||||||
import xiaozhi.modules.security.service.CaptchaService;
|
import xiaozhi.modules.security.service.CaptchaService;
|
||||||
@@ -104,6 +105,8 @@ public class LoginController {
|
|||||||
@PutMapping("/change-password")
|
@PutMapping("/change-password")
|
||||||
@Operation(summary = "修改用户密码")
|
@Operation(summary = "修改用户密码")
|
||||||
public Result<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
|
public Result<?> changePassword(@RequestBody PasswordDTO passwordDTO) {
|
||||||
|
// 判断非空
|
||||||
|
ValidatorUtils.validateEntity(passwordDTO);
|
||||||
Long userId = SecurityUser.getUserId();
|
Long userId = SecurityUser.getUserId();
|
||||||
sysUserTokenService.changePassword(userId, passwordDTO);
|
sysUserTokenService.changePassword(userId, passwordDTO);
|
||||||
return new Result<>();
|
return new Result<>();
|
||||||
|
|||||||
@@ -90,7 +90,6 @@ public class Oauth2Filter extends AuthenticatingFilter {
|
|||||||
String json = JsonUtils.toJsonString(r);
|
String json = JsonUtils.toJsonString(r);
|
||||||
httpResponse.getWriter().print(json);
|
httpResponse.getWriter().print(json);
|
||||||
} catch (IOException e1) {
|
} catch (IOException e1) {
|
||||||
logger.error("onLoginFailure:登录失败! msg:{}", e1.getMessage(), e1);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html lang="zh-CN">
|
||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
@@ -11,36 +12,45 @@
|
|||||||
padding: 20px;
|
padding: 20px;
|
||||||
background-color: #f5f5f5;
|
background-color: #f5f5f5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.container {
|
.container {
|
||||||
max-width: 800px;
|
max-width: 800px;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
padding: 20px;
|
padding: 10px 20px 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
color: #333;
|
color: #333;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
padding: 15px;
|
padding: 15px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
background-color: #f9f9f9;
|
background-color: #f9f9f9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section h2 {
|
.section h2 {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
color: #444;
|
color: #444;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.control-panel {
|
.control-panel {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-top: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -50,36 +60,43 @@
|
|||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: background-color 0.2s;
|
transition: background-color 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:hover {
|
button:hover {
|
||||||
background-color: #3367d6;
|
background-color: #3367d6;
|
||||||
}
|
}
|
||||||
|
|
||||||
button:disabled {
|
button:disabled {
|
||||||
background-color: #cccccc;
|
background-color: #cccccc;
|
||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
#serverUrl {
|
#serverUrl {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message-input {
|
.message-input {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-bottom: 20px;
|
margin-bottom: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#messageInput {
|
#messageInput {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#nfcCardId {
|
#nfcCardId {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
border: 1px solid #ddd;
|
border: 1px solid #ddd;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation {
|
.conversation {
|
||||||
max-height: 300px;
|
max-height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
@@ -87,36 +104,45 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
flex: 1;
|
||||||
|
margin-right: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.message {
|
.message {
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
padding: 8px 12px;
|
padding: 8px 12px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
max-width: 80%;
|
max-width: 80%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.user {
|
.user {
|
||||||
background-color: #e2f2ff;
|
background-color: #e2f2ff;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
text-align: right;
|
text-align: right;
|
||||||
}
|
}
|
||||||
|
|
||||||
.server {
|
.server {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
margin-right: auto;
|
margin-right: auto;
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.status {
|
.status {
|
||||||
color: #666;
|
color: #666;
|
||||||
font-style: italic;
|
font-style: italic;
|
||||||
text-align: center;
|
font-size: 14px;
|
||||||
margin-bottom: 10px;
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-controls {
|
.audio-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.audio-visualizer {
|
.audio-visualizer {
|
||||||
height: 60px;
|
height: 60px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -125,45 +151,72 @@
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
background-color: #fafafa;
|
background-color: #fafafa;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-button {
|
.record-button {
|
||||||
background-color: #db4437;
|
background-color: #db4437;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-button:hover {
|
.record-button:hover {
|
||||||
background-color: #c53929;
|
background-color: #c53929;
|
||||||
}
|
}
|
||||||
|
|
||||||
.record-button.recording {
|
.record-button.recording {
|
||||||
animation: pulse 1.5s infinite;
|
animation: pulse 1.5s infinite;
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
@keyframes pulse {
|
||||||
0% { background-color: #db4437; }
|
0% {
|
||||||
50% { background-color: #ff6659; }
|
background-color: #db4437;
|
||||||
100% { background-color: #db4437; }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
background-color: #ff6659;
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
background-color: #db4437;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#logContainer {
|
#logContainer {
|
||||||
margin-top: 20px;
|
margin-top: 0;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
height: 150px;
|
height: 300px;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
margin-left: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.log-entry {
|
.log-entry {
|
||||||
margin: 5px 0;
|
margin: 5px 0;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
}
|
}
|
||||||
.log-info { color: #333; }
|
|
||||||
.log-error { color: #db4437; }
|
.log-info {
|
||||||
.log-success { color: #0f9d58; }
|
color: #333;
|
||||||
.info-box {
|
}
|
||||||
background-color: #e8f4ff;
|
|
||||||
border-left: 4px solid #4285f4;
|
.log-error {
|
||||||
width: auto;
|
color: #db4437;
|
||||||
display: none;
|
}
|
||||||
margin: 10px 0;
|
|
||||||
padding: 10px;
|
.log-success {
|
||||||
font-size: 14px;
|
color: #0f9d58;
|
||||||
line-height: 1.5;
|
}
|
||||||
|
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translate(-50%, -60%);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-status {
|
.script-status {
|
||||||
@@ -174,9 +227,17 @@
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.script-loaded { background-color: #0f9d58; }
|
.script-loaded {
|
||||||
.script-loading { background-color: #f4b400; }
|
background-color: #0f9d58;
|
||||||
.script-error { background-color: #db4437; }
|
}
|
||||||
|
|
||||||
|
.script-loading {
|
||||||
|
background-color: #f4b400;
|
||||||
|
}
|
||||||
|
|
||||||
|
.script-error {
|
||||||
|
background-color: #db4437;
|
||||||
|
}
|
||||||
|
|
||||||
.script-list {
|
.script-list {
|
||||||
margin: 10px 0;
|
margin: 10px 0;
|
||||||
@@ -187,14 +248,6 @@
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 状态消息样式 */
|
|
||||||
#scriptStatus {
|
|
||||||
padding: 10px 15px;
|
|
||||||
border-radius: 5px;
|
|
||||||
margin-bottom: 15px;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
#scriptStatus.success {
|
#scriptStatus.success {
|
||||||
background-color: #e6f4ea;
|
background-color: #e6f4ea;
|
||||||
color: #0f9d58;
|
color: #0f9d58;
|
||||||
@@ -212,57 +265,106 @@
|
|||||||
color: #f4b400;
|
color: #f4b400;
|
||||||
border-left: 4px solid #f4b400;
|
border-left: 4px solid #f4b400;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 标签页样式 */
|
||||||
|
.tabs {
|
||||||
|
display: flex;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
border-bottom: 2px solid #e0e0e0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: 10px 20px;
|
||||||
|
cursor: pointer;
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
font-size: 16px;
|
||||||
|
color: #666;
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab:hover {
|
||||||
|
color: #4285f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
color: #4285f4;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #4285f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab-content.active {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<h1>小智服务器测试页面</h1>
|
<h1>小智服务器测试页面</h1>
|
||||||
|
|
||||||
<div id="scriptStatus" class="info-box"></div>
|
<div id="scriptStatus" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%);">
|
||||||
|
正在加载Opus库...</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>WebSocket连接</h2>
|
<h2>WebSocket连接 <span id="connectionStatus" class="status">未连接</span></h2>
|
||||||
<div class="control-panel">
|
<div class="control-panel">
|
||||||
<input type="text" id="serverUrl" value="ws://localhost:8000" placeholder="WebSocket服务器地址">
|
<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="disconnectButton" disabled>断开</button>
|
|
||||||
<button id="authTestButton">测试认证</button>
|
<button id="authTestButton">测试认证</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="connectionStatus" class="status">未连接</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>文本消息</h2>
|
<h2>消息发送</h2>
|
||||||
<div class="message-input">
|
<div class="tabs">
|
||||||
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
|
<button class="tab active" data-tab="text">文本消息</button>
|
||||||
<button id="sendTextButton" disabled>发送</button>
|
<button class="tab" data-tab="voice">语音消息</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="tab-content active" id="textTab">
|
||||||
<h2>NFC卡片模拟</h2>
|
<div class="message-input">
|
||||||
<div class="message-input">
|
<input type="text" id="messageInput" placeholder="输入消息..." disabled>
|
||||||
<input type="text" id="nfcCardId" placeholder="输入NFC卡片ID..." value="ABCD1234EFGH5678" disabled>
|
<button id="sendTextButton" disabled>发送</button>
|
||||||
<button id="sendNfcButton" disabled>模拟NFC卡片</button>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="section">
|
<div class="tab-content" id="voiceTab">
|
||||||
<h2>语音消息</h2>
|
<div class="audio-controls">
|
||||||
<div class="audio-controls">
|
<button id="recordButton" class="record-button" disabled>开始录音</button>
|
||||||
<button id="recordButton" class="record-button" disabled>开始录音</button>
|
</div>
|
||||||
<button id="stopButton" disabled>停止录音并发送</button>
|
<canvas id="audioVisualizer" class="audio-visualizer"></canvas>
|
||||||
</div>
|
</div>
|
||||||
<canvas id="audioVisualizer" class="audio-visualizer"></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="section">
|
<div class="section">
|
||||||
<h2>会话记录</h2>
|
<h2>会话记录</h2>
|
||||||
<div id="conversation" class="conversation"></div>
|
<div class="flex-container">
|
||||||
</div>
|
<div id="conversation" class="conversation"></div>
|
||||||
|
<div id="logContainer">
|
||||||
<div id="logContainer">
|
<div class="log-entry log-info">准备就绪,请连接服务器开始测试...</div>
|
||||||
<div class="log-entry log-info">准备就绪,请连接服务器开始测试...</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -283,48 +385,42 @@
|
|||||||
|
|
||||||
// 检查Opus库是否已加载
|
// 检查Opus库是否已加载
|
||||||
function checkOpusLoaded() {
|
function checkOpusLoaded() {
|
||||||
// setTimeout(() => {
|
try {
|
||||||
try {
|
// 检查Module是否存在(本地库导出的全局变量)
|
||||||
// 检查Module是否存在(本地库导出的全局变量)
|
if (typeof Module === 'undefined') {
|
||||||
if (typeof Module === 'undefined') {
|
throw new Error('Opus库未加载,Module对象不存在');
|
||||||
throw new Error('Opus库未加载,Module对象不存在');
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试先使用Module.instance(libopus.js最后一行导出方式)
|
|
||||||
if (typeof Module.instance !== 'undefined' && typeof Module.instance._opus_decoder_get_size === 'function') {
|
|
||||||
// 使用Module.instance对象替换全局Module对象
|
|
||||||
window.ModuleInstance = Module.instance;
|
|
||||||
log('Opus库加载成功(使用Module.instance)', 'success');
|
|
||||||
updateScriptStatus('Opus库加载成功', 'success');
|
|
||||||
|
|
||||||
// 3秒后隐藏状态
|
|
||||||
setTimeout(() => {
|
|
||||||
const statusElement = document.getElementById('scriptStatus');
|
|
||||||
if (statusElement) statusElement.style.display = 'none';
|
|
||||||
}, 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有Module.instance,检查全局Module函数
|
|
||||||
if (typeof Module._opus_decoder_get_size === 'function') {
|
|
||||||
window.ModuleInstance = Module;
|
|
||||||
log('Opus库加载成功(使用全局Module)', 'success');
|
|
||||||
updateScriptStatus('Opus库加载成功', 'success');
|
|
||||||
|
|
||||||
// 3秒后隐藏状态
|
|
||||||
setTimeout(() => {
|
|
||||||
const statusElement = document.getElementById('scriptStatus');
|
|
||||||
if (statusElement) statusElement.style.display = 'none';
|
|
||||||
}, 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error('Opus解码函数未找到,可能Module结构不正确');
|
|
||||||
} catch (err) {
|
|
||||||
log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');
|
|
||||||
updateScriptStatus('Opus库加载失败,请检查libopus.js文件是否存在且正确', 'error');
|
|
||||||
}
|
}
|
||||||
// }, 1000); // 延迟1秒检查,确保脚本有时间加载
|
|
||||||
|
// 尝试先使用Module.instance(libopus.js最后一行导出方式)
|
||||||
|
if (typeof Module.instance !== 'undefined' && typeof Module.instance._opus_decoder_get_size === 'function') {
|
||||||
|
// 使用Module.instance对象替换全局Module对象
|
||||||
|
window.ModuleInstance = Module.instance;
|
||||||
|
log('Opus库加载成功(使用Module.instance)', 'success');
|
||||||
|
updateScriptStatus('Opus库加载成功', 'success');
|
||||||
|
|
||||||
|
// 3秒后隐藏状态
|
||||||
|
const statusElement = document.getElementById('scriptStatus');
|
||||||
|
if (statusElement) statusElement.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果没有Module.instance,检查全局Module函数
|
||||||
|
if (typeof Module._opus_decoder_get_size === 'function') {
|
||||||
|
window.ModuleInstance = Module;
|
||||||
|
log('Opus库加载成功(使用全局Module)', 'success');
|
||||||
|
updateScriptStatus('Opus库加载成功', 'success');
|
||||||
|
|
||||||
|
// 3秒后隐藏状态
|
||||||
|
const statusElement = document.getElementById('scriptStatus');
|
||||||
|
if (statusElement) statusElement.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error('Opus解码函数未找到,可能Module结构不正确');
|
||||||
|
} catch (err) {
|
||||||
|
log(`Opus库加载失败,请检查libopus.js文件是否存在且正确: ${err.message}`, 'error');
|
||||||
|
updateScriptStatus('Opus库加载失败,请检查libopus.js文件是否存在且正确', 'error');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新脚本状态显示
|
// 更新脚本状态显示
|
||||||
@@ -358,13 +454,10 @@
|
|||||||
|
|
||||||
// DOM元素
|
// DOM元素
|
||||||
const connectButton = document.getElementById('connectButton');
|
const connectButton = document.getElementById('connectButton');
|
||||||
const disconnectButton = document.getElementById('disconnectButton');
|
|
||||||
const serverUrlInput = document.getElementById('serverUrl');
|
const serverUrlInput = document.getElementById('serverUrl');
|
||||||
const connectionStatus = document.getElementById('connectionStatus');
|
const connectionStatus = document.getElementById('connectionStatus');
|
||||||
const messageInput = document.getElementById('messageInput');
|
const messageInput = document.getElementById('messageInput');
|
||||||
const sendTextButton = document.getElementById('sendTextButton');
|
const sendTextButton = document.getElementById('sendTextButton');
|
||||||
const nfcCardIdInput = document.getElementById('nfcCardId');
|
|
||||||
const sendNfcButton = document.getElementById('sendNfcButton');
|
|
||||||
const recordButton = document.getElementById('recordButton');
|
const recordButton = document.getElementById('recordButton');
|
||||||
const stopButton = document.getElementById('stopButton');
|
const stopButton = document.getElementById('stopButton');
|
||||||
const conversationDiv = document.getElementById('conversation');
|
const conversationDiv = document.getElementById('conversation');
|
||||||
@@ -433,7 +526,6 @@
|
|||||||
conversationDiv.scrollTop = conversationDiv.scrollHeight;
|
conversationDiv.scrollTop = conversationDiv.scrollHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// 播放Opus音频数据
|
// 播放Opus音频数据
|
||||||
function playOpusData(opusBuffers) {
|
function playOpusData(opusBuffers) {
|
||||||
if (!opusBuffers || opusBuffers.length === 0) {
|
if (!opusBuffers || opusBuffers.length === 0) {
|
||||||
@@ -482,7 +574,7 @@
|
|||||||
module: window.ModuleInstance, // 保存模块实例的引用
|
module: window.ModuleInstance, // 保存模块实例的引用
|
||||||
|
|
||||||
// 使用ModuleInstance提供的函数来解码
|
// 使用ModuleInstance提供的函数来解码
|
||||||
decode_float: function(opusData) {
|
decode_float: function (opusData) {
|
||||||
try {
|
try {
|
||||||
const mod = this.module; // 简写引用
|
const mod = this.module; // 简写引用
|
||||||
|
|
||||||
@@ -527,7 +619,7 @@
|
|||||||
// 从内存中复制PCM数据
|
// 从内存中复制PCM数据
|
||||||
const pcmData = new Float32Array(samplesDecoded);
|
const pcmData = new Float32Array(samplesDecoded);
|
||||||
for (let i = 0; i < samplesDecoded; i++) {
|
for (let i = 0; i < samplesDecoded; i++) {
|
||||||
pcmData[i] = mod.HEAPF32[pcmPtr/4 + i];
|
pcmData[i] = mod.HEAPF32[pcmPtr / 4 + i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// 释放内存
|
// 释放内存
|
||||||
@@ -562,7 +654,7 @@
|
|||||||
const opusBuffer = opusBuffers[i];
|
const opusBuffer = opusBuffers[i];
|
||||||
// 确保这是Uint8Array
|
// 确保这是Uint8Array
|
||||||
const opusArray = opusBuffer instanceof Uint8Array ?
|
const opusArray = opusBuffer instanceof Uint8Array ?
|
||||||
opusBuffer : new Uint8Array(opusBuffer);
|
opusBuffer : new Uint8Array(opusBuffer);
|
||||||
|
|
||||||
// 解码Opus帧为PCM数据
|
// 解码Opus帧为PCM数据
|
||||||
const decoded = opusDecoder.decode_float(opusArray);
|
const decoded = opusDecoder.decode_float(opusArray);
|
||||||
@@ -571,7 +663,7 @@
|
|||||||
successCount++;
|
successCount++;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`Opus帧 #${i+1} 解码错误: ${error.message}`, 'error');
|
log(`Opus帧 #${i + 1} 解码错误: ${error.message}`, 'error');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -616,7 +708,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
source.start();
|
source.start();
|
||||||
log(`开始播放PCM音频,时长: ${(combinedPCM.length/16000).toFixed(2)}秒`, 'debug');
|
log(`开始播放PCM音频,时长: ${(combinedPCM.length / 16000).toFixed(2)}秒`, 'debug');
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
log(`播放Opus音频错误: ${error.message},尝试使用简单播放`, 'error');
|
log(`播放Opus音频错误: ${error.message},尝试使用简单播放`, 'error');
|
||||||
@@ -911,12 +1003,10 @@
|
|||||||
// 连接成功后发送hello消息
|
// 连接成功后发送hello消息
|
||||||
await sendHelloMessage();
|
await sendHelloMessage();
|
||||||
|
|
||||||
connectButton.disabled = true;
|
connectButton.textContent = '断开';
|
||||||
disconnectButton.disabled = false;
|
connectButton.onclick = disconnectFromServer;
|
||||||
messageInput.disabled = false;
|
messageInput.disabled = false;
|
||||||
sendTextButton.disabled = false;
|
sendTextButton.disabled = false;
|
||||||
nfcCardIdInput.disabled = false;
|
|
||||||
sendNfcButton.disabled = false;
|
|
||||||
|
|
||||||
const audioInitialized = await initAudio();
|
const audioInitialized = await initAudio();
|
||||||
if (audioInitialized) {
|
if (audioInitialized) {
|
||||||
@@ -929,14 +1019,12 @@
|
|||||||
connectionStatus.textContent = '已断开';
|
connectionStatus.textContent = '已断开';
|
||||||
connectionStatus.style.color = 'red';
|
connectionStatus.style.color = 'red';
|
||||||
|
|
||||||
connectButton.disabled = false;
|
connectButton.textContent = '连接';
|
||||||
disconnectButton.disabled = true;
|
connectButton.onclick = connectToServer;
|
||||||
messageInput.disabled = true;
|
messageInput.disabled = true;
|
||||||
sendTextButton.disabled = true;
|
sendTextButton.disabled = true;
|
||||||
recordButton.disabled = true;
|
recordButton.disabled = true;
|
||||||
stopButton.disabled = true;
|
stopButton.disabled = true;
|
||||||
nfcCardIdInput.disabled = true;
|
|
||||||
sendNfcButton.disabled = true;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
websocket.onerror = (error) => {
|
websocket.onerror = (error) => {
|
||||||
@@ -945,7 +1033,7 @@
|
|||||||
connectionStatus.style.color = 'red';
|
connectionStatus.style.color = 'red';
|
||||||
};
|
};
|
||||||
|
|
||||||
websocket.onmessage = function(event) {
|
websocket.onmessage = function (event) {
|
||||||
try {
|
try {
|
||||||
// 检查是否为文本消息
|
// 检查是否为文本消息
|
||||||
if (typeof event.data === 'string') {
|
if (typeof event.data === 'string') {
|
||||||
@@ -989,14 +1077,6 @@
|
|||||||
if (message.text && message.text !== '😊') {
|
if (message.text && message.text !== '😊') {
|
||||||
addMessage(message.text);
|
addMessage(message.text);
|
||||||
}
|
}
|
||||||
} else if (message.type === 'nfc_card_response') {
|
|
||||||
// NFC卡片响应
|
|
||||||
log(`NFC卡片响应: ${message.status}`, message.status === 'success' ? 'success' : 'error');
|
|
||||||
if (message.content) {
|
|
||||||
addMessage(`[NFC卡片内容] ${message.content}`);
|
|
||||||
} else if (message.message) {
|
|
||||||
addMessage(`[NFC卡片错误] ${message.message}`);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 未知消息类型
|
// 未知消息类型
|
||||||
log(`未知消息类型: ${message.type}`, 'info');
|
log(`未知消息类型: ${message.type}`, 'info');
|
||||||
@@ -1104,41 +1184,37 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 发送NFC卡片消息
|
|
||||||
function sendNfcCardMessage() {
|
|
||||||
const cardId = nfcCardIdInput.value.trim();
|
|
||||||
if (cardId === '' || !websocket || websocket.readyState !== WebSocket.OPEN) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 构造NFC卡片消息
|
|
||||||
const nfcMessage = {
|
|
||||||
type: 'nfc_card_detected',
|
|
||||||
card_id: cardId
|
|
||||||
};
|
|
||||||
|
|
||||||
websocket.send(JSON.stringify(nfcMessage));
|
|
||||||
log(`发送NFC卡片消息, 卡片ID: ${cardId}`, 'info');
|
|
||||||
addMessage(`[NFC卡片] ${cardId}`, true);
|
|
||||||
} catch (error) {
|
|
||||||
log(`发送NFC卡片消息错误: ${error.message}`, 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化事件监听器
|
// 初始化事件监听器
|
||||||
function initEventListeners() {
|
function initEventListeners() {
|
||||||
connectButton.addEventListener('click', connectToServer);
|
connectButton.addEventListener('click', connectToServer);
|
||||||
disconnectButton.addEventListener('click', disconnectFromServer);
|
|
||||||
document.getElementById('authTestButton').addEventListener('click', testAuthentication);
|
document.getElementById('authTestButton').addEventListener('click', testAuthentication);
|
||||||
|
|
||||||
|
// 标签页切换
|
||||||
|
const tabs = document.querySelectorAll('.tab');
|
||||||
|
tabs.forEach(tab => {
|
||||||
|
tab.addEventListener('click', () => {
|
||||||
|
// 移除所有标签页的active类
|
||||||
|
tabs.forEach(t => t.classList.remove('active'));
|
||||||
|
document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
|
||||||
|
|
||||||
|
// 添加当前标签页的active类
|
||||||
|
tab.classList.add('active');
|
||||||
|
document.getElementById(`${tab.dataset.tab}Tab`).classList.add('active');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
sendTextButton.addEventListener('click', sendTextMessage);
|
sendTextButton.addEventListener('click', sendTextMessage);
|
||||||
messageInput.addEventListener('keypress', (e) => {
|
messageInput.addEventListener('keypress', (e) => {
|
||||||
if (e.key === 'Enter') sendTextMessage();
|
if (e.key === 'Enter') sendTextMessage();
|
||||||
});
|
});
|
||||||
|
|
||||||
sendNfcButton.addEventListener('click', sendNfcCardMessage);
|
recordButton.addEventListener('click', () => {
|
||||||
|
if (isRecording) {
|
||||||
recordButton.addEventListener('click', startRecording);
|
stopRecording();
|
||||||
stopButton.addEventListener('click', stopRecording);
|
} else {
|
||||||
|
startRecording();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener('resize', initVisualizer);
|
window.addEventListener('resize', initVisualizer);
|
||||||
}
|
}
|
||||||
@@ -1253,8 +1329,6 @@
|
|||||||
return window.btoa(binary);
|
return window.btoa(binary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// 使用libopus创建一个Opus编码器
|
// 使用libopus创建一个Opus编码器
|
||||||
let opusEncoder = null;
|
let opusEncoder = null;
|
||||||
function initOpusEncoder() {
|
function initOpusEncoder() {
|
||||||
@@ -1283,7 +1357,7 @@
|
|||||||
module: mod,
|
module: mod,
|
||||||
|
|
||||||
// 初始化编码器
|
// 初始化编码器
|
||||||
init: function() {
|
init: function () {
|
||||||
try {
|
try {
|
||||||
// 获取编码器大小
|
// 获取编码器大小
|
||||||
const encoderSize = mod._opus_encoder_get_size(this.channels);
|
const encoderSize = mod._opus_encoder_get_size(this.channels);
|
||||||
@@ -1329,7 +1403,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 编码PCM数据为Opus
|
// 编码PCM数据为Opus
|
||||||
encode: function(pcmData) {
|
encode: function (pcmData) {
|
||||||
if (!this.encoderPtr) {
|
if (!this.encoderPtr) {
|
||||||
if (!this.init()) {
|
if (!this.init()) {
|
||||||
return null;
|
return null;
|
||||||
@@ -1381,7 +1455,7 @@
|
|||||||
},
|
},
|
||||||
|
|
||||||
// 销毁编码器
|
// 销毁编码器
|
||||||
destroy: function() {
|
destroy: function () {
|
||||||
if (this.encoderPtr) {
|
if (this.encoderPtr) {
|
||||||
this.module._free(this.encoderPtr);
|
this.module._free(this.encoderPtr);
|
||||||
this.encoderPtr = null;
|
this.encoderPtr = null;
|
||||||
@@ -1733,15 +1807,14 @@
|
|||||||
let recordingSeconds = 0;
|
let recordingSeconds = 0;
|
||||||
const recordingTimer = setInterval(() => {
|
const recordingTimer = setInterval(() => {
|
||||||
recordingSeconds += 0.1;
|
recordingSeconds += 0.1;
|
||||||
recordButton.textContent = `录音中... ${recordingSeconds.toFixed(1)}秒`;
|
recordButton.textContent = `停止录音 ${recordingSeconds.toFixed(1)}秒`;
|
||||||
}, 100);
|
}, 100);
|
||||||
|
|
||||||
// 保存计时器,以便在停止时清除
|
// 保存计时器,以便在停止时清除
|
||||||
window.recordingTimer = recordingTimer;
|
window.recordingTimer = recordingTimer;
|
||||||
|
|
||||||
recordButton.classList.add('recording');
|
recordButton.classList.add('recording');
|
||||||
recordButton.disabled = true;
|
recordButton.disabled = false;
|
||||||
stopButton.disabled = false;
|
|
||||||
|
|
||||||
log('开始PCM直接录音', 'success');
|
log('开始PCM直接录音', 'success');
|
||||||
return true;
|
return true;
|
||||||
@@ -1813,7 +1886,6 @@
|
|||||||
recordButton.textContent = '开始录音';
|
recordButton.textContent = '开始录音';
|
||||||
recordButton.classList.remove('recording');
|
recordButton.classList.remove('recording');
|
||||||
recordButton.disabled = false;
|
recordButton.disabled = false;
|
||||||
stopButton.disabled = true;
|
|
||||||
|
|
||||||
log('停止PCM直接录音', 'success');
|
log('停止PCM直接录音', 'success');
|
||||||
return true;
|
return true;
|
||||||
@@ -1858,4 +1930,5 @@
|
|||||||
initApp();
|
initApp();
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
Reference in New Issue
Block a user