Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
274 changes: 164 additions & 110 deletions dashboard/src/main.ts
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()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

There is a race condition between initApp() and setupI18n(). initApp is an asynchronous function that fetches the dynamic configuration and sets up the fetch wrapper, but it is currently called at the end of the file without being awaited. This allows setupI18n and the application mounting process to begin before the API base URL is correctly initialized, which could lead to failed requests during the initial load.

Suggested change
setupI18n()
initApp().then(() => 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 axios export, while src/utils/request.ts defines its own service instance with different interceptor logic (including URL normalization). Depending on which import callers use, requests may see different headers/URL handling. Consider consolidating on a single configured Axios instance and reusing its interceptors to prevent behavioral drift.

Suggested implementation:

  1. Ensure that all API calls in the app use the configured Axios instance from src/utils/request.ts (usually something like import service from "@/utils/request";) instead of importing the default axios from axios.
  2. If dashboard/src/main.ts imports axios only to add these interceptors, remove that import to avoid an unused import.
  3. If there are any remaining places importing axios directly for HTTP calls, consider switching them to use the shared service instance so that URL normalization, headers, and other shared behavior are consistent across the app.

});
Comment on lines 120 to 130
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This global Axios interceptor is redundant. The application uses a custom Axios instance (service) defined in @/utils/request.ts, which already implements token and locale injection in its own interceptor. Interceptors added to the global axios object do not affect instances created via axios.create(). Removing this block will simplify the code and avoid confusion.


// 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Relative /api URLs in Request objects are not rewritten to the API base URL.

This wrapper only rewrites URLs when input is a string starting with /api. If a caller passes new Request("/api/foo"), url remains the original Request, so its relative /api/... URL is never resolved to the configured API base URL. Please also handle Request inputs (e.g., by checking request.url and cloning with a resolved URL) so fetch behaves consistently regardless of how it’s called.

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
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

loadAppConfig() fetches config.json from the built public root, but the repository’s dashboard/public/ directory doesn’t include a config.json file. This will make the feature silently fall back (and emit a warning) in default builds unless deployments are manually providing that file.

If runtime config via config.json is intended, consider adding a checked-in template (e.g. public/config.json with empty/default values) or documenting the required deployment step/location.

Copilot uses AI. Check for mistakes.
} 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The 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 config.json is applied.

Because initApp() is not awaited before mounting, any early use of Axios or resolveApiUrl can see a baseURL derived only from VITE_API_BASE, with config.json/localStorage overrides applied later. If those overrides must apply to all requests, ensure initApp() completes before mounting the app (or before any code that uses the API client runs).

console.log(`API Base URL set to: ${apiBaseUrl}`);
}
return _origFetch(input, { ...init, headers });
};

loader.config({ monaco })
setApiBaseUrl(apiBaseUrl);
Comment on lines +157 to +161
Copy link

Copilot AI Apr 2, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

setApiBaseUrl() only updates the custom axios instance in @/utils/request, but most of the dashboard code imports and uses the default axios instance directly (e.g. many axios.get('/api/...') calls). As a result, changing the API Base URL here won’t actually affect the majority of API requests, and /api will still be resolved against the dashboard origin.

Consider either (1) configuring the global axios.defaults.baseURL + a request interceptor to rewrite /api/... paths based on the configured base, or (2) migrating call sites to use the exported request service instance consistently.

Copilot uses AI. Check for mistakes.

// 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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The fetch wrapper currently only resolves URLs when the input is a string. If the application uses URL or Request objects for API calls, the dynamic API base URL resolution will be skipped. Consider updating this logic to handle URL and Request objects to ensure all fetch calls are correctly routed.


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 });
Loading
Loading