feat: 添加后端部署和API基本检索的Docker Compose配置
- 引入新的`docker-compose.yml`文件,便于后端和MQTT代理(EMQX)的部署。 - 更新多个组件中的API基本检索,使用函数进行动态解析。 - 加强了多个组件中API调用的错误处理和日志记录。 - 优化了AI聊天面板离线场景的回退响应。 - 更新DataScreen和MediaScreen组件中的数据显示和统计,以反映准确的指标。 - 重构MQTT连接逻辑,以支持动态凭证和客户端ID。
This commit is contained in:
@@ -8,15 +8,18 @@ import { useParkSim } from '../utils/parkData';
|
||||
昆明市大学生创业园 · OPC 智能园区数字运营中心
|
||||
—— 数据大屏(真实数据展示)
|
||||
---------------------------------------------------------
|
||||
数据口径(用户指定,仅两个权威来源):
|
||||
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》
|
||||
实际入驻 39 家 / 可容纳 49 个 / 累计投入 560 万 /
|
||||
带动就业 213 人 / 累计生产经营总额 252.15 万 / 累计税利 1.03 万 / 面积 3000㎡
|
||||
数据口径(标准资料,多个权威来源):
|
||||
· 《昆明市大学生创业园运行情况统计表(2026年7月).xls》(官方统计)
|
||||
实际入驻 38 家 / 可容纳 49 个 / 累计投入 560 万 /
|
||||
当年累计带动就业 213 人 / 当年生产经营总额 252.15 万 / 当年税利 1.03 万 / 面积 3000㎡
|
||||
· 《附件1.省级创业孵化载体相关情况简介材料》(省级绩效累计口径,截至2026年5月)
|
||||
累计孵化 181 家 / 在孵 31 家 / 累计就业 1,529 人 / 累计经营收入 21,911.14 万
|
||||
· 《昆明市创业园宣传册.pdf》
|
||||
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域 / 28 家企业名录
|
||||
测算口径(用户确认):
|
||||
2009 年成立 / 2012 年省级创业园 / 民航路 229 号 / 四大孵化区域
|
||||
· 《入驻企业信息表》39 家名录(含创始人/简介/场地安排)
|
||||
测算口径(AI 平台业务锚点,非政府统计口径):
|
||||
· TOKEN:月均 200 亿 tokens → 日均 ≈ 6.67 亿,实时 ≈ 7,700 t/s,近30日累计 200 亿
|
||||
· 工具调用:按每次调用约 2 万 tokens 折算(今日 ≈ 3.3 万次)
|
||||
· 工具调用:按每次调用约 15 万 tokens 折算
|
||||
TOKEN/工具为基于真实锚点的测算演示,其余为真实统计值
|
||||
========================================================= */
|
||||
|
||||
|
||||
+62
-13
@@ -1,6 +1,7 @@
|
||||
import { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import * as api from '../utils/api';
|
||||
import { getApiBase } from '../config';
|
||||
import { API_BASE } from '../utils/api';
|
||||
import { getCurrentWindow } from '@tauri-apps/api/window';
|
||||
import { enable, disable } from '@tauri-apps/plugin-autostart';
|
||||
@@ -58,7 +59,8 @@ export default function MediaScreen() {
|
||||
useEffect(() => { soundBlockedRef.current = soundBlocked; }, [soundBlocked]);
|
||||
|
||||
const getUrl = useCallback((item) => {
|
||||
return item.source === 'url' ? item.relative_path : `${API_BASE}/file/${item.relative_path}`;
|
||||
// 惰性读取 API 基址:打包部署引导覆盖后媒体文件地址同步生效
|
||||
return item.source === 'url' ? item.relative_path : `${getApiBase()}/file/${item.relative_path}`;
|
||||
}, []);
|
||||
|
||||
// ============ 核心播放控制 ============
|
||||
@@ -106,14 +108,19 @@ export default function MediaScreen() {
|
||||
const url = getUrl(item);
|
||||
video.src = url;
|
||||
video.load();
|
||||
// 打包部署:WebView2 已加 --autoplay-policy=no-user-gesture-required,
|
||||
// 这里仍先尝试带声音播放;仅真实自动播放策略拦截(NotAllowedError)才进入静音待解锁
|
||||
video.muted = soundBlocked;
|
||||
video.volume = volume;
|
||||
const p = video.play();
|
||||
if (p !== undefined) {
|
||||
p.catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
p.catch((err) => {
|
||||
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
// 其他错误(网络/解码等)不强制静音,等重试
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@@ -132,15 +139,54 @@ export default function MediaScreen() {
|
||||
if (!item || item.type !== 'video') return;
|
||||
const video = videoRef.current;
|
||||
if (video && video.paused && video.src) {
|
||||
video.play().catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
video.play().catch((err) => {
|
||||
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
video.play().catch(() => {});
|
||||
}
|
||||
});
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [paused]);
|
||||
|
||||
// ============ 声音自动恢复(打包部署无需人工点击) ============
|
||||
// 1) 视频真正开始播放(playing)→ 解除静音标记
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const onPlaying = () => {
|
||||
setSoundBlocked(false);
|
||||
if (video.muted && !soundBlockedRef.current) {
|
||||
video.muted = false;
|
||||
}
|
||||
};
|
||||
video.addEventListener('playing', onPlaying);
|
||||
return () => video.removeEventListener('playing', onPlaying);
|
||||
}, [mediaKey, loaded]);
|
||||
|
||||
// 2) 若因自动播放策略被静音,每 3 秒自动尝试解静音播放(无需用户点击)
|
||||
useEffect(() => {
|
||||
if (!loaded) return undefined;
|
||||
const iv = setInterval(() => {
|
||||
if (!soundBlockedRef.current) return;
|
||||
const video = videoRef.current;
|
||||
if (!video || !video.src) return;
|
||||
video.muted = false;
|
||||
const p = video.play();
|
||||
if (p !== undefined) {
|
||||
p.then(() => {
|
||||
setSoundBlocked(false);
|
||||
const hint = document.getElementById('soundHint');
|
||||
if (hint) hint.style.display = 'none';
|
||||
}).catch(() => {
|
||||
video.muted = true;
|
||||
});
|
||||
}
|
||||
}, 3000);
|
||||
return () => clearInterval(iv);
|
||||
}, [loaded]);
|
||||
|
||||
// ============ 事件监听 ============
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
@@ -176,9 +222,12 @@ export default function MediaScreen() {
|
||||
testPlay.then(() => {
|
||||
setSoundBlocked(false);
|
||||
video.pause();
|
||||
}).catch(() => {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
}).catch((err) => {
|
||||
if (err && (err.name === 'NotAllowedError' || /autoplay|gesture/i.test(String(err.message || '')))) {
|
||||
setSoundBlocked(true);
|
||||
video.muted = true;
|
||||
}
|
||||
// 其他错误不强制静音(自动重试由 3s 定时器接管)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -188,7 +237,7 @@ export default function MediaScreen() {
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const es = new EventSource(`${API_BASE}/api/events`);
|
||||
const es = new EventSource(`${getApiBase()}/api/events`);
|
||||
es.onmessage = (e) => {
|
||||
try {
|
||||
const msg = JSON.parse(e.data);
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
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 { API_BASE } from '../config';
|
||||
import { getApiBase as API_BASE } from '../config';
|
||||
import '../styles/datascreen.css';
|
||||
import './voice-original.css';
|
||||
import './voice-overrides.css';
|
||||
@@ -222,7 +222,7 @@ export default function VoiceAssistant() {
|
||||
pushMessage('assistant', `🔧 调用工具:${name}`, false);
|
||||
let output;
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/api/tools/exec`, {
|
||||
const res = await fetch(`${API_BASE()}/api/tools/exec`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ name, args }),
|
||||
@@ -303,7 +303,7 @@ export default function VoiceAssistant() {
|
||||
// 基础提示词:从后端拉取(配置 + 园区知识库已由服务端组装);失败用极简兜底
|
||||
let instructions = FALLBACK_INSTRUCTIONS;
|
||||
try {
|
||||
const insRes = await fetch(`${API_BASE}/api/s2s/instructions`, { cache: 'no-store' });
|
||||
const insRes = await fetch(`${API_BASE()}/api/s2s/instructions`, { cache: 'no-store' });
|
||||
const insData = await insRes.json();
|
||||
if (insData?.ok && insData.instructions) instructions = insData.instructions;
|
||||
} catch { /* 后端不可用不影响对话 */ }
|
||||
@@ -452,7 +452,7 @@ export default function VoiceAssistant() {
|
||||
} else {
|
||||
void start(); // start 连接成功后检查 pendingGreetingRef 再发问候
|
||||
}
|
||||
fetch(`${API_BASE}/api/vision/event`, {
|
||||
fetch(`${API_BASE()}/api/vision/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ event: 'triggered', faces: vision.faces, dwell_ms: VISION_DWELL_MS }),
|
||||
@@ -466,7 +466,7 @@ export default function VoiceAssistant() {
|
||||
// 摄像头识别状态变化 → 上报后端(详细日志定位)
|
||||
useEffect(() => {
|
||||
if (!vision.status || vision.status === 'off' || vision.status === 'loading') return;
|
||||
fetch(`${API_BASE}/api/vision/event`, {
|
||||
fetch(`${API_BASE()}/api/vision/event`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
|
||||
Reference in New Issue
Block a user