diff --git a/backend/app/config.py b/backend/app/config.py index f353cc6..3777048 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -82,6 +82,11 @@ class Settings: S2S_BUILD_RETRIES = int(_env("DPM_S2S_BUILD_RETRIES", "4")) S2S_BUILD_RETRY_DELAY = float(_env("DPM_S2S_BUILD_RETRY_DELAY", "10")) S2S_LOG_LEVEL = _env("DPM_S2S_LOG_LEVEL", "INFO").upper() + # 对外可访问的 s2s 实时语音 WS 地址(供 /api/config 下发;DPM_VOICE_WS 可显式覆盖) + S2S_WS_URL = _env( + "DPM_VOICE_WS", + f"ws://{_env('MQTT_PUBLIC_HOST', _env('MQTT_BROKER_HOST', 'localhost'))}:8765/v1/realtime", + ) # 语音助手基础提示词(.env 设 DPM_S2S_INSTRUCTIONS 可覆盖;知识库内容由 rag 自动拼接) S2S_INSTRUCTIONS = _env( "DPM_S2S_INSTRUCTIONS", diff --git a/backend/app/routers.py b/backend/app/routers.py index 64926b6..fe9f2ba 100644 --- a/backend/app/routers.py +++ b/backend/app/routers.py @@ -127,6 +127,7 @@ async def runtime_config(): "mqtt_url": ws, "mqtt_username": settings.MQTT_USERNAME or "", "mqtt_password": settings.MQTT_PASSWORD or "", + "voice_url": settings.S2S_WS_URL, "api_base": f"http://{host}:{settings.PORT}", "broker_host": host, } diff --git a/docs/win-deploy.md b/docs/win-deploy.md index 63ee5f2..a78b2ac 100644 --- a/docs/win-deploy.md +++ b/docs/win-deploy.md @@ -10,7 +10,8 @@ "api_base": "http://192.168.1.9:10085", "mqtt_url": "ws://192.168.1.3:8083/mqtt", "mqtt_username": "dpm", - "mqtt_password": "123456" + "mqtt_password": "123456", + "voice_url": "ws://192.168.1.3:8765/v1/realtime" } ``` @@ -29,6 +30,7 @@ DPM_MQTT_HOST=192.168.1.3 DPM_MQTT_PORT=1883 DPM_MQTT_WS=ws://192.168.1.3:8083/mqtt # 展播端可访问的 WebSocket 地址 +DPM_VOICE_WS=ws://192.168.1.3:8765/v1/realtime # s2s 语音 WS(可选,默认按主机推导) MQTT_USERNAME=dpmserver MQTT_PASSWORD=你的密码 ``` diff --git a/src/config.js b/src/config.js index 389e4cc..33f294d 100644 --- a/src/config.js +++ b/src/config.js @@ -41,6 +41,12 @@ export function getMqttCredentials() { }; } +/** 惰性读取 s2s 实时语音 WebSocket 地址(打包部署:tauri.localhost → 127.0.0.1) */ +export function getVoiceWsUrl() { + const baked = env.VITE_VOICE_WS || `ws://${resolveHost()}:8765/v1/realtime`; + return window.__DPM_VOICE_WS__ || baked; +} + export const MQTT_TOPIC_COMMAND = 'opc/display/command'; export const MQTT_TOPIC_TICK = 'opc/dashboard/tick'; export const MQTT_TOPIC_HEARTBEAT = 'opc/display/heartbeat'; diff --git a/src/main.jsx b/src/main.jsx index 983e56a..9218d66 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -23,6 +23,7 @@ async function bootstrapRuntimeConfig() { if (cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url; if (cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username; if (cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password; + if (cfg.voice_url) window.__DPM_VOICE_WS__ = cfg.voice_url; // eslint-disable-next-line no-console if (cfg.mqtt_url) console.info(`[bootstrap] config.json MQTT -> ${cfg.mqtt_url}`); } @@ -31,15 +32,19 @@ async function bootstrapRuntimeConfig() { } // 2) 后端 /api/config(MQTT 地址/账号;API 基址以前端实际可达地址为准) + // 优先级保护:若构建期已用 .env.local 烘焙了 VITE_MQTT_URL, + // 则后端 /api/config 不再覆盖(避免后端默认 localhost 冲掉正确地址)。 + const bakedMqtt = import.meta.env.VITE_MQTT_URL; try { const res = await fetch(`${getApiBase()}/api/config`, { cache: 'no-store' }); if (res.ok) { const cfg = await res.json(); - if (cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url; - if (cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username; - if (cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password; + if (!bakedMqtt && cfg.mqtt_url) window.__DPM_MQTT__ = cfg.mqtt_url; + if (!bakedMqtt && cfg.mqtt_username) window.__DPM_MQTT_USER__ = cfg.mqtt_username; + if (!bakedMqtt && cfg.mqtt_password) window.__DPM_MQTT_PASS__ = cfg.mqtt_password; + if (!import.meta.env.VITE_VOICE_WS && cfg.voice_url) window.__DPM_VOICE_WS__ = cfg.voice_url; // eslint-disable-next-line no-console - if (cfg.mqtt_url) console.info(`[bootstrap] /api/config MQTT -> ${cfg.mqtt_url}`); + if (cfg.mqtt_url) console.info(`[bootstrap] /api/config MQTT -> ${cfg.mqtt_url}${bakedMqtt ? '(已烘焙,忽略)' : ''}`); } } catch { /* 后端不可达:使用内置默认地址 */ diff --git a/src/pages/VoiceAssistant.jsx b/src/pages/VoiceAssistant.jsx index 28a4fce..7237f14 100644 --- a/src/pages/VoiceAssistant.jsx +++ b/src/pages/VoiceAssistant.jsx @@ -8,12 +8,13 @@ import { useCallback, useEffect, useRef, useState } from 'react'; import { S2sWsRealtimeClient } from '../voice/s2s-ws-client.js'; import useVisionDetection, { VISION_DWELL_MS } from '../voice/useVisionDetection.js'; -import { getApiBase as API_BASE } from '../config'; +import { getApiBase as API_BASE, getVoiceWsUrl } from '../config'; import '../styles/datascreen.css'; import './voice-original.css'; import './voice-overrides.css'; -const VOICE_URL = window.__DPM_VOICE_WS__ || `ws://${window.location.hostname}:8765/v1/realtime`; +// 惰性读取 s2s 地址:打包部署 tauri.localhost → 127.0.0.1,且可在 config.json 覆盖 +const VOICE_URL = getVoiceWsUrl(); // 基础提示词由后端统一管理(GET /api/s2s/instructions:配置提示词 + 园区知识库), // 前端不硬编码;此处仅为后端不可用时的兜底 const FALLBACK_INSTRUCTIONS = '你是 PineSound 园区智能语音助手,请用简洁专业的中文回答,不超过三句话。';