diff --git a/backend/README.md b/backend/README.md index da58449..ecf13c8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -84,3 +84,32 @@ curl -X POST http://localhost:10085/api/display/command \ - 大屏数据由后端模拟引擎产生(`sim_engine.py`,与原前端 `parkData.js` 逻辑一致);前端离线时也有本地兜底 - 媒体文件存放于 `backend/media/`,`/file/...` 直接返回 - 安全:`backend/.env` 与根 `.env.local` 已加入 `.gitignore`,含阿里云密钥与 MQTT 凭据,勿提交 + + +## Docker 部署(推荐) + +一键构建并启动 **后端 + EMQX Broker**: + +```bash +# 1) (可选)配置部署参数 +export MQTT_PUBLIC_HOST=192.168.1.50 # 展播端可访问的服务器局域网 IP(Broker WebSocket) +export EMQX_USER=dpmserver +export EMQX_PASS=你的密码 +export DASHSCOPE_API_KEY=sk-xxx # AI/语音密钥(未配置时 AI 走本地规则引擎) +export DPM_S2S_ENABLED=1 # 实时语音对话开关 + +# 2) 构建并启动 +docker compose up -d --build + +# 3) 验证 +curl http://127.0.0.1:10085/api/health # {"ok":true,...} +docker compose logs -f backend +``` + +- 端口:`10085`(API/后台/媒体)、`8765`(s2s 语音)、`1883/8083/18083`(EMQX) +- 数据卷:`dpm_media`(媒体)、`dpm_knowledge`(知识库 park.md,可热更新)、`dpm_data`(设置/播放列表) +- 展播端(Tauri 大屏)启动时经 `GET /api/config` 自动获取 MQTT WebSocket 地址 +- 常用命令:`docker compose down` 停止;`docker compose up -d --build backend` 仅重建后端; + `docker compose ps` 查看状态 + +> 单独构建镜像:`docker build -f backend/Dockerfile -t dpm-backend .` diff --git a/backend/app/admin.py b/backend/app/admin.py index 9bc4b69..e026051 100644 --- a/backend/app/admin.py +++ b/backend/app/admin.py @@ -72,6 +72,7 @@ async def admin_page(request: Request): "play_mode": s.get("play_mode", "sequential"), "image_duration": s.get("image_duration", 5), "volume": s.get("volume", 80), + "sfx_volume": s.get("sfx_volume", 60), "fullscreen": s.get("fullscreen", True), "autostart": s.get("autostart", False), "from_screen": request.query_params.get("from") == "screen", diff --git a/backend/app/routers.py b/backend/app/routers.py index d0ef884..64926b6 100644 --- a/backend/app/routers.py +++ b/backend/app/routers.py @@ -38,6 +38,7 @@ def _media_type(path): class SettingsBody(BaseModel): volume: int | None = None + sfx_volume: int | None = None play_mode: str | None = None image_duration: int | None = None fullscreen: bool | None = None @@ -106,7 +107,7 @@ async def login(request: Request): @router.get("/api/settings") async def get_settings(): s = storage.get_settings() - return {k: s[k] for k in ("volume", "play_mode", "image_duration", "fullscreen", "autostart")} + return {k: s[k] for k in ("volume", "sfx_volume", "play_mode", "image_duration", "fullscreen", "autostart")} @router.get("/api/config") @@ -135,7 +136,9 @@ async def runtime_config(): async def update_settings(body: SettingsBody): storage.update_settings(**body.model_dump(exclude_none=True)) s = storage.get_settings() - hub.publish_command("settings_changed", {"volume": s["volume"], "play_mode": s["play_mode"]}) + hub.publish_command("settings_changed", { + "volume": s["volume"], "sfx_volume": s["sfx_volume"], "play_mode": s["play_mode"], + }) return {"ok": True} diff --git a/backend/app/storage.py b/backend/app/storage.py index cf3dc6f..28f3a8d 100644 --- a/backend/app/storage.py +++ b/backend/app/storage.py @@ -12,6 +12,7 @@ DEFAULT_SETTINGS = { "username": "admin", "password": "123456", "volume": 80, + "sfx_volume": 60, "auto_play": True, "play_mode": "sequential", "image_duration": 5, diff --git a/backend/main.py b/backend/main.py index 9f25cba..6a429be 100644 --- a/backend/main.py +++ b/backend/main.py @@ -67,7 +67,7 @@ def main(): print(f" 本地资源 : TORCH_HOME={os.environ.get('TORCH_HOME')}") print(f" NLTK_DATA={os.environ.get('NLTK_DATA')}") print("=" * 64) - uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=True) + uvicorn.run("app.main:app", host=settings.HOST, port=settings.PORT, reload=False) if __name__ == "__main__": diff --git a/backend/static/admin.js b/backend/static/admin.js index d6a4191..ca8fffe 100644 --- a/backend/static/admin.js +++ b/backend/static/admin.js @@ -222,7 +222,20 @@ volumeLabel.textContent = volumeRange.value + '%'; if (volTimer) clearTimeout(volTimer); volTimer = setTimeout(function () { - post('/api/settings', { volume: Number(volumeRange.value) }, function () { toast('音量已更新'); }); + post('/api/settings', { volume: Number(volumeRange.value) }, function () { toast('媒体音量已更新'); }); + }, 300); + }); + } + + var sfxVolumeRange = document.getElementById('sfxVolumeRange'); + var sfxVolumeLabel = document.getElementById('sfxVolumeLabel'); + var sfxVolTimer = null; + if (sfxVolumeRange && sfxVolumeLabel) { + sfxVolumeRange.addEventListener('input', function () { + sfxVolumeLabel.textContent = sfxVolumeRange.value + '%'; + if (sfxVolTimer) clearTimeout(sfxVolTimer); + sfxVolTimer = setTimeout(function () { + post('/api/settings', { sfx_volume: Number(sfxVolumeRange.value) }, function () { toast('音效音量已更新'); }); }, 300); }); } diff --git a/backend/templates/admin.html b/backend/templates/admin.html index 653ff55..7357c40 100644 --- a/backend/templates/admin.html +++ b/backend/templates/admin.html @@ -230,10 +230,15 @@