update:前端增加可配置的本地唤醒词运行时

This commit is contained in:
3030332422
2026-04-14 16:54:37 +08:00
parent 705b5732a6
commit d5b883817c
18 changed files with 1037 additions and 150 deletions
@@ -1,39 +1,56 @@
import { uiController } from '../../ui/controller.js?v=0205';
import { log } from '../../utils/logger.js?v=0205';
const BRIDGE_URL_CANDIDATES = [
`${window.location.origin}/events`
];
let wakewordSocket = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
let shouldReconnect = true;
let wakewordRequestSeq = 0;
let wakewordEventSource = null;
const pendingWakewordRequests = new Map();
export function startWakewordBridgeListener() {
if (wakewordEventSource) {
return wakewordEventSource;
if (wakewordSocket) {
return wakewordSocket;
}
shouldReconnect = true;
log('正在连接本地唤醒事件桥...', 'info');
tryConnect(0);
return wakewordEventSource;
tryConnect();
return wakewordSocket;
}
function tryConnect(index) {
if (index >= BRIDGE_URL_CANDIDATES.length) {
log('未能连接到本地唤醒事件桥,请确认 test runtime 已启动', 'warning');
return null;
}
const bridgeUrl = BRIDGE_URL_CANDIDATES[index];
function tryConnect() {
const bridgeUrl = buildWakewordBridgeUrl();
try {
wakewordEventSource = new EventSource(bridgeUrl);
wakewordEventSource.onopen = () => {
wakewordSocket = new WebSocket(bridgeUrl);
wakewordSocket.onopen = () => {
reconnectAttempts = 0;
log(`本地唤醒事件桥已连接: ${bridgeUrl}`, 'success');
// 连接成功后自动保存地址,刷新后仍能记住
localStorage.setItem('xz_tester_wakewordWsUrl', bridgeUrl);
const urlInput = document.getElementById('wakewordWsUrl');
if (urlInput) urlInput.value = bridgeUrl;
};
wakewordEventSource.onmessage = async (event) => {
wakewordSocket.onerror = () => {
log(`本地唤醒事件桥连接失败: ${bridgeUrl}`, 'error');
};
wakewordSocket.onmessage = async (event) => {
try {
const message = JSON.parse(event.data);
const message = parseWakewordBridgeMessage(event.data);
if (message.requestId && pendingWakewordRequests.has(message.requestId)) {
settleWakewordRequest(message);
return;
}
if (message.success === false) {
log(`本地唤醒事件桥返回错误: ${message.error || '未知错误'}`, 'error');
return;
}
if (message.type === 'bridge_connected') {
log('本地唤醒监听已就绪', 'info');
return;
@@ -44,6 +61,12 @@ function tryConnect(index) {
return;
}
if (message.type === 'wakeword_config') {
uiController.applyWakewordConfig(message.payload || {});
log('已同步本地唤醒词配置', 'info');
return;
}
if (message.type === 'service_stopping') {
log('本地唤醒服务正在停止', 'warning');
return;
@@ -59,34 +82,126 @@ function tryConnect(index) {
}
};
wakewordEventSource.onerror = () => {
log(`本地唤醒事件桥连接异常: ${bridgeUrl}`, 'warning');
if (wakewordEventSource) {
wakewordEventSource.close();
wakewordEventSource = null;
wakewordSocket.onclose = () => {
if (wakewordSocket) {
wakewordSocket = null;
}
if (index + 1 < BRIDGE_URL_CANDIDATES.length) {
log(`尝试备用地址: ${BRIDGE_URL_CANDIDATES[index + 1]}`, 'info');
tryConnect(index + 1);
rejectAllWakewordRequests('本地唤醒事件桥已断开');
if (!shouldReconnect) {
return;
}
log('请确认当前页面由 test runtime 启动,并且 /events 可访问', 'warning');
if (reconnectTimer) {
return;
}
reconnectAttempts += 1;
const delay = Math.min(1000 * reconnectAttempts, 5000);
log(`本地唤醒事件桥将在 ${delay}ms 后重连: ${bridgeUrl}`, 'warning');
reconnectTimer = window.setTimeout(() => {
reconnectTimer = null;
tryConnect();
}, delay);
};
return wakewordEventSource;
return wakewordSocket;
} catch (error) {
log(`启动本地唤醒监听失败: ${error.message}`, 'error');
return tryConnect(index + 1);
return null;
}
}
export function stopWakewordBridgeListener() {
if (!wakewordEventSource) {
shouldReconnect = false;
if (reconnectTimer) {
window.clearTimeout(reconnectTimer);
reconnectTimer = null;
}
if (!wakewordSocket) {
return;
}
wakewordEventSource.close();
wakewordEventSource = null;
wakewordSocket.close();
wakewordSocket = null;
}
export function sendWakewordBridgeMessage(type, payload = {}, requestId = null) {
if (!wakewordSocket || wakewordSocket.readyState !== WebSocket.OPEN) {
log('本地唤醒事件桥未连接,无法发送消息', 'warning');
return false;
}
wakewordSocket.send(JSON.stringify({
type,
requestId,
payload,
}));
return true;
}
export function requestWakewordBridge(type, payload = {}, timeout = 5000) {
const requestId = `wakeword-${Date.now()}-${++wakewordRequestSeq}`;
return new Promise((resolve, reject) => {
const timer = window.setTimeout(() => {
pendingWakewordRequests.delete(requestId);
reject(new Error('本地唤醒服务响应超时'));
}, timeout);
pendingWakewordRequests.set(requestId, { resolve, reject, timer });
if (!sendWakewordBridgeMessage(type, payload, requestId)) {
window.clearTimeout(timer);
pendingWakewordRequests.delete(requestId);
reject(new Error('本地唤醒事件桥未连接'));
}
});
}
function buildWakewordBridgeUrl() {
const configured = localStorage.getItem('xz_tester_wakewordWsUrl');
if (configured && configured.trim()) {
return configured.trim();
}
return 'ws://127.0.0.1:8006/wakeword-ws';
}
function parseWakewordBridgeMessage(rawData) {
const message = JSON.parse(rawData);
return {
type: message.type || '',
requestId: message.requestId || null,
success: message.success !== false,
payload: message.payload || {},
error: message.error || null,
};
}
function settleWakewordRequest(message) {
const pendingRequest = pendingWakewordRequests.get(message.requestId);
if (!pendingRequest) {
return;
}
window.clearTimeout(pendingRequest.timer);
pendingWakewordRequests.delete(message.requestId);
if (message.success === false) {
pendingRequest.reject(new Error(message.error || '本地唤醒服务返回失败'));
return;
}
pendingRequest.resolve(message);
}
function rejectAllWakewordRequests(errorMessage) {
pendingWakewordRequests.forEach((pendingRequest) => {
window.clearTimeout(pendingRequest.timer);
pendingRequest.reject(new Error(errorMessage));
});
pendingWakewordRequests.clear();
}