89 lines
4.0 KiB
React
89 lines
4.0 KiB
React
import React from "react";
|
||
import ReactDOM from "react-dom/client";
|
||
import App from "./App";
|
||
import ErrorBoundary from "./components/ErrorBoundary";
|
||
import { ThemeProvider } from "@appica/ui-react/providers/theme-provider";
|
||
import { getApiBase } from "./config";
|
||
import { invoke } from "@tauri-apps/api/core";
|
||
import "./styles/global.css";
|
||
|
||
/* 全局致命错误捕获:任何模块加载/运行/异步错误 → 显示到页面(替代白屏),便于副屏等定位 */
|
||
function installFatalErrorDiagnostics() {
|
||
const show = (label, err) => {
|
||
const el = document.getElementById("fatal-error");
|
||
if (!el || el.style.display === "block") return;
|
||
const text = document.getElementById("fatal-error-text");
|
||
if (text) {
|
||
const msg = (err && (err.stack || err.message)) || String(err);
|
||
text.textContent = `${label}\n${msg}`;
|
||
}
|
||
el.style.display = "block";
|
||
};
|
||
window.addEventListener("error", (e) => show("加载错误:", e.error || e.message));
|
||
window.addEventListener("unhandledrejection", (e) => show("未处理异常:", e.reason));
|
||
}
|
||
installFatalErrorDiagnostics();
|
||
|
||
/* =========================================================
|
||
启动引导(打包部署关键)—— 地址解析优先级:
|
||
1. exe 旁 config.json(Tauri 命令读取,部署免重打包改 IP)★
|
||
2. 后端 GET /api/config(MQTT 地址/账号,后端 .env 为准)
|
||
3. 内置默认(Tauri 打包回退 192.168.1.9 / 浏览器用当前主机)
|
||
结果写入 window.__DPM_*__,api.js / mqtt.js 惰性读取生效。
|
||
========================================================= */
|
||
async function bootstrapRuntimeConfig() {
|
||
// 1) exe 旁 config.json(最高优先):{api_base, mqtt_url, mqtt_username, mqtt_password}
|
||
try {
|
||
const raw = await invoke("read_runtime_config");
|
||
if (raw) {
|
||
const cfg = JSON.parse(raw);
|
||
if (cfg.api_base) window.__DPM_API__ = cfg.api_base;
|
||
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}`);
|
||
}
|
||
} catch {
|
||
/* 浏览器开发环境或未注册命令:跳过 */
|
||
}
|
||
|
||
// AudioWorklet 兼容:打包环境(WebView2)无法从 tauri.localhost 自定义协议加载
|
||
// worklet 模块,统一改从后端真实 HTTP 服务加载(backend/static/voice-worklets/)
|
||
if (!window.VOICE_WORKLETS_BASE) {
|
||
window.VOICE_WORKLETS_BASE = `${getApiBase()}/static/voice-worklets/`;
|
||
}
|
||
|
||
// 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 (!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}${bakedMqtt ? '(已烘焙,忽略)' : ''}`);
|
||
}
|
||
} catch {
|
||
/* 后端不可达:使用内置默认地址 */
|
||
}
|
||
}
|
||
|
||
bootstrapRuntimeConfig().then(() => {
|
||
ReactDOM.createRoot(document.getElementById("root")).render(
|
||
<React.StrictMode>
|
||
<ErrorBoundary>
|
||
<ThemeProvider>
|
||
<App />
|
||
</ThemeProvider>
|
||
</ErrorBoundary>
|
||
</React.StrictMode>,
|
||
);
|
||
});
|