-
-
Notifications
You must be signed in to change notification settings - Fork 2k
feat(dashboard): Restore configurable API Base URL support #7306
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,139 +1,193 @@ | ||
| import { createApp } from 'vue'; | ||
| import { createPinia } from 'pinia'; | ||
| import App from './App.vue'; | ||
| import { router } from './router'; | ||
| import vuetify from './plugins/vuetify'; | ||
| import confirmPlugin from './plugins/confirmPlugin'; | ||
| import { setupI18n } from './i18n/composables'; | ||
| import '@/scss/style.scss'; | ||
| import VueApexCharts from 'vue3-apexcharts'; | ||
|
|
||
| import print from 'vue3-print-nb'; | ||
| import { loader } from '@guolao/vue-monaco-editor' | ||
| import * as monaco from 'monaco-editor'; | ||
| import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker'; | ||
| import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker'; | ||
| import cssWorker from 'monaco-editor/esm/vs/language/css/css.worker?worker'; | ||
| import htmlWorker from 'monaco-editor/esm/vs/language/html/html.worker?worker'; | ||
| import tsWorker from 'monaco-editor/esm/vs/language/typescript/ts.worker?worker'; | ||
| import axios from 'axios'; | ||
| import { waitForRouterReadyInBackground } from './utils/routerReadiness.mjs'; | ||
| import { createApp } from "vue"; | ||
| import { createPinia } from "pinia"; | ||
| import App from "./App.vue"; | ||
| import { router } from "./router"; | ||
| import vuetify from "./plugins/vuetify"; | ||
| import confirmPlugin from "./plugins/confirmPlugin"; | ||
| import { setupI18n } from "./i18n/composables"; | ||
| import "@/scss/style.scss"; | ||
| import VueApexCharts from "vue3-apexcharts"; | ||
|
|
||
| import print from "vue3-print-nb"; | ||
| import { loader } from "@guolao/vue-monaco-editor"; | ||
| import * as monaco from "monaco-editor"; | ||
| import editorWorker from "monaco-editor/esm/vs/editor/editor.worker?worker"; | ||
| import jsonWorker from "monaco-editor/esm/vs/language/json/json.worker?worker"; | ||
| import cssWorker from "monaco-editor/esm/vs/language/css/css.worker?worker"; | ||
| import htmlWorker from "monaco-editor/esm/vs/language/html/html.worker?worker"; | ||
| import tsWorker from "monaco-editor/esm/vs/language/typescript/ts.worker?worker"; | ||
| import axios from "axios"; | ||
| import { waitForRouterReadyInBackground } from "./utils/routerReadiness.mjs"; | ||
| import { | ||
| getApiBaseUrl, | ||
| resolveApiUrl, | ||
| resolvePublicUrl, | ||
| setApiBaseUrl, | ||
| } from "@/utils/request"; | ||
|
|
||
| (self as any).MonacoEnvironment = { | ||
| getWorker(_: string, label: string) { | ||
| if (label === 'json') { | ||
| if (label === "json") { | ||
| return new jsonWorker(); | ||
| } | ||
| if (label === 'css' || label === 'scss' || label === 'less') { | ||
| if (label === "css" || label === "scss" || label === "less") { | ||
| return new cssWorker(); | ||
| } | ||
| if (label === 'html' || label === 'handlebars' || label === 'razor') { | ||
| if (label === "html" || label === "handlebars" || label === "razor") { | ||
| return new htmlWorker(); | ||
| } | ||
| if (label === 'typescript' || label === 'javascript') { | ||
| if (label === "typescript" || label === "javascript") { | ||
| return new tsWorker(); | ||
| } | ||
| return new editorWorker(); | ||
| }, | ||
| }; | ||
|
|
||
| // 初始化新的i18n系统,等待完成后再挂载应用 | ||
| setupI18n().then(async () => { | ||
| console.log('🌍 新i18n系统初始化完成'); | ||
|
|
||
| const app = createApp(App); | ||
| const pinia = createPinia(); | ||
| app.use(pinia); | ||
| app.use(router); | ||
| app.use(print); | ||
| app.use(VueApexCharts); | ||
| app.use(vuetify); | ||
| app.use(confirmPlugin); | ||
| await router.isReady(); | ||
| app.mount('#app'); | ||
|
|
||
| // 挂载后同步 Vuetify 主题 | ||
| import('./stores/customizer').then(({ useCustomizerStore }) => { | ||
| const customizer = useCustomizerStore(pinia); | ||
| vuetify.theme.global.name.value = customizer.uiTheme; | ||
| const storedPrimary = localStorage.getItem('themePrimary'); | ||
| const storedSecondary = localStorage.getItem('themeSecondary'); | ||
| if (storedPrimary || storedSecondary) { | ||
| const themes = vuetify.theme.themes.value; | ||
| ['PurpleTheme', 'PurpleThemeDark'].forEach((name) => { | ||
| const theme = themes[name]; | ||
| if (!theme?.colors) return; | ||
| if (storedPrimary) theme.colors.primary = storedPrimary; | ||
| if (storedSecondary) theme.colors.secondary = storedSecondary; | ||
| if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary; | ||
| if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary; | ||
| }); | ||
| } | ||
| }); | ||
| }).catch(error => { | ||
| console.error('❌ 新i18n系统初始化失败:', error); | ||
|
|
||
| // 即使i18n初始化失败,也要挂载应用(使用回退机制) | ||
| const app = createApp(App); | ||
| const pinia = createPinia(); | ||
| app.use(pinia); | ||
| app.use(router); | ||
| app.use(print); | ||
| app.use(VueApexCharts); | ||
| app.use(vuetify); | ||
| app.use(confirmPlugin); | ||
| app.mount('#app'); | ||
| waitForRouterReadyInBackground(router); | ||
|
|
||
| // 挂载后同步 Vuetify 主题 | ||
| import('./stores/customizer').then(({ useCustomizerStore }) => { | ||
| const customizer = useCustomizerStore(pinia); | ||
| vuetify.theme.global.name.value = customizer.uiTheme; | ||
| const storedPrimary = localStorage.getItem('themePrimary'); | ||
| const storedSecondary = localStorage.getItem('themeSecondary'); | ||
| if (storedPrimary || storedSecondary) { | ||
| const themes = vuetify.theme.themes.value; | ||
| ['PurpleTheme', 'PurpleThemeDark'].forEach((name) => { | ||
| const theme = themes[name]; | ||
| if (!theme?.colors) return; | ||
| if (storedPrimary) theme.colors.primary = storedPrimary; | ||
| if (storedSecondary) theme.colors.secondary = storedSecondary; | ||
| if (storedPrimary && theme.colors.darkprimary) theme.colors.darkprimary = storedPrimary; | ||
| if (storedSecondary && theme.colors.darksecondary) theme.colors.darksecondary = storedSecondary; | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
| setupI18n() | ||
| .then(async () => { | ||
| console.log("🌍 新i18n系统初始化完成"); | ||
|
|
||
| const app = createApp(App); | ||
| const pinia = createPinia(); | ||
| app.use(pinia); | ||
| app.use(router); | ||
| app.use(print); | ||
| app.use(VueApexCharts); | ||
| app.use(vuetify); | ||
| app.use(confirmPlugin); | ||
| await router.isReady(); | ||
| app.mount("#app"); | ||
|
|
||
| // 挂载后同步 Vuetify 主题 | ||
| import("./stores/customizer").then(({ useCustomizerStore }) => { | ||
| const customizer = useCustomizerStore(pinia); | ||
| vuetify.theme.global.name.value = customizer.uiTheme; | ||
| const storedPrimary = localStorage.getItem("themePrimary"); | ||
| const storedSecondary = localStorage.getItem("themeSecondary"); | ||
| if (storedPrimary || storedSecondary) { | ||
| const themes = vuetify.theme.themes.value; | ||
| ["PurpleTheme", "PurpleThemeDark"].forEach((name) => { | ||
| const theme = themes[name]; | ||
| if (!theme?.colors) return; | ||
| if (storedPrimary) theme.colors.primary = storedPrimary; | ||
| if (storedSecondary) theme.colors.secondary = storedSecondary; | ||
| if (storedPrimary && theme.colors.darkprimary) | ||
| theme.colors.darkprimary = storedPrimary; | ||
| if (storedSecondary && theme.colors.darksecondary) | ||
| theme.colors.darksecondary = storedSecondary; | ||
| }); | ||
| } | ||
| }); | ||
| }) | ||
| .catch((error) => { | ||
| console.error("❌ 新i18n系统初始化失败:", error); | ||
|
|
||
| // 即使i18n初始化失败,也要挂载应用(使用回退机制) | ||
| const app = createApp(App); | ||
| const pinia = createPinia(); | ||
| app.use(pinia); | ||
| app.use(router); | ||
| app.use(print); | ||
| app.use(VueApexCharts); | ||
| app.use(vuetify); | ||
| app.use(confirmPlugin); | ||
| app.mount("#app"); | ||
| waitForRouterReadyInBackground(router); | ||
|
|
||
| // 挂载后同步 Vuetify 主题 | ||
| import("./stores/customizer").then(({ useCustomizerStore }) => { | ||
| const customizer = useCustomizerStore(pinia); | ||
| vuetify.theme.global.name.value = customizer.uiTheme; | ||
| const storedPrimary = localStorage.getItem("themePrimary"); | ||
| const storedSecondary = localStorage.getItem("themeSecondary"); | ||
| if (storedPrimary || storedSecondary) { | ||
| const themes = vuetify.theme.themes.value; | ||
| ["PurpleTheme", "PurpleThemeDark"].forEach((name) => { | ||
| const theme = themes[name]; | ||
| if (!theme?.colors) return; | ||
| if (storedPrimary) theme.colors.primary = storedPrimary; | ||
| if (storedSecondary) theme.colors.secondary = storedSecondary; | ||
| if (storedPrimary && theme.colors.darkprimary) | ||
| theme.colors.darkprimary = storedPrimary; | ||
| if (storedSecondary && theme.colors.darksecondary) | ||
| theme.colors.darksecondary = storedSecondary; | ||
| }); | ||
| } | ||
| }); | ||
| }); | ||
|
|
||
| axios.interceptors.request.use((config) => { | ||
| const token = localStorage.getItem('token'); | ||
| const token = localStorage.getItem("token"); | ||
| if (token) { | ||
| config.headers['Authorization'] = `Bearer ${token}`; | ||
| config.headers["Authorization"] = `Bearer ${token}`; | ||
| } | ||
| const locale = localStorage.getItem('astrbot-locale'); | ||
| const locale = localStorage.getItem("astrbot-locale"); | ||
| if (locale) { | ||
| config.headers['Accept-Language'] = locale; | ||
| config.headers["Accept-Language"] = locale; | ||
| } | ||
| return config; | ||
|
Comment on lines
120
to
129
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. suggestion (bug_risk): There are now two Axios instances with separate interceptors, which can diverge behavior. In this file you configure interceptors on the default Suggested implementation:
|
||
| }); | ||
|
Comment on lines
120
to
130
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This global Axios interceptor is redundant. The application uses a custom Axios instance ( |
||
|
|
||
| // Keep fetch() calls consistent with axios by automatically attaching the JWT. | ||
| // Some parts of the UI use fetch directly; without this, those requests will 401. | ||
| const _origFetch = window.fetch.bind(window); | ||
| window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { | ||
| const token = localStorage.getItem('token'); | ||
| if (!token) return _origFetch(input, init); | ||
|
|
||
| const headers = new Headers(init?.headers || (typeof input !== 'string' && 'headers' in input ? (input as Request).headers : undefined)); | ||
| if (!headers.has('Authorization')) { | ||
| headers.set('Authorization', `Bearer ${token}`); | ||
| // 1. 定义加载配置的函数 | ||
| async function loadAppConfig() { | ||
|
Comment on lines
124
to
+133
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): Relative This wrapper only rewrites URLs when |
||
| try { | ||
| const configUrl = new URL(resolvePublicUrl("config.json")); | ||
| configUrl.searchParams.set("t", `${Date.now()}`); | ||
| const response = await fetch(configUrl.toString()); | ||
| if (!response.ok) { | ||
| throw new Error(`HTTP error! status: ${response.status}`); | ||
| } | ||
| return await response.json(); | ||
|
Comment on lines
+133
to
+141
|
||
| } catch (error) { | ||
| console.warn("Failed to load config.json, falling back to default.", error); | ||
| return {}; | ||
| } | ||
| const locale = localStorage.getItem('astrbot-locale'); | ||
| if (locale && !headers.has('Accept-Language')) { | ||
| headers.set('Accept-Language', locale); | ||
| } | ||
|
|
||
| async function initApp() { | ||
| const config = await loadAppConfig(); | ||
| const configApiUrl = config.apiBaseUrl || ""; | ||
| const envApiUrl = import.meta.env.VITE_API_BASE || ""; | ||
|
|
||
| const localApiUrl = localStorage.getItem("apiBaseUrl"); | ||
| const apiBaseUrl = | ||
| localApiUrl !== null ? localApiUrl : configApiUrl || envApiUrl; | ||
|
|
||
| if (apiBaseUrl) { | ||
|
Comment on lines
+148
to
+157
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. issue (bug_risk): App initialization order may use an outdated API base URL before Because |
||
| console.log(`API Base URL set to: ${apiBaseUrl}`); | ||
| } | ||
| return _origFetch(input, { ...init, headers }); | ||
| }; | ||
|
|
||
| loader.config({ monaco }) | ||
| setApiBaseUrl(apiBaseUrl); | ||
|
Comment on lines
+157
to
+161
|
||
|
|
||
| // Keep fetch() calls consistent with axios by automatically attaching the JWT. | ||
| // Some parts of the UI use fetch directly; without this, those requests will 401. | ||
| const _origFetch = window.fetch.bind(window); | ||
| window.fetch = (input: RequestInfo | URL, init?: RequestInit) => { | ||
| let url = input; | ||
| if (typeof input === "string" && input.startsWith("/api")) { | ||
| url = resolveApiUrl(input, getApiBaseUrl()); | ||
| } | ||
|
Comment on lines
+168
to
+170
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
|
||
| const token = localStorage.getItem("token"); | ||
| const headers = new Headers( | ||
| init?.headers || | ||
| (typeof input !== "string" && "headers" in input | ||
| ? (input as Request).headers | ||
| : undefined), | ||
| ); | ||
|
|
||
| if (token && !headers.has("Authorization")) { | ||
| headers.set("Authorization", `Bearer ${token}`); | ||
| } | ||
| const locale = localStorage.getItem("astrbot-locale"); | ||
| if (locale && !headers.has("Accept-Language")) { | ||
| headers.set("Accept-Language", locale); | ||
| } | ||
| return _origFetch(url, { ...init, headers }); | ||
| }; | ||
| } | ||
|
|
||
| initApp(); | ||
|
|
||
| loader.config({ monaco }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is a race condition between
initApp()andsetupI18n().initAppis an asynchronous function that fetches the dynamic configuration and sets up thefetchwrapper, but it is currently called at the end of the file without being awaited. This allowssetupI18nand the application mounting process to begin before the API base URL is correctly initialized, which could lead to failed requests during the initial load.