feat: migrate dashboard api runtime

This commit is contained in:
LIghtJUNction
2026-06-26 10:17:08 +08:00
parent 5d22db51ad
commit fcfe5ca41f
12 changed files with 937 additions and 1162 deletions

View File

@@ -19,6 +19,7 @@
},
"dependencies": {
"@guolao/vue-monaco-editor": "^1.6.0",
"@hey-api/client-axios": "^0.9.1",
"@tiptap/starter-kit": "3.20.5",
"@tiptap/vue-3": "3.20.5",
"apexcharts": "5.10.4",

File diff suppressed because it is too large Load Diff

View File

@@ -1,11 +1,10 @@
/* Auto-generated MDI subset 279 icons */
/* Auto-generated MDI subset 281 icons */
/* Do not edit manually. Run: pnpm run subset-icons */
@font-face {
font-family: "Material Design Icons";
src:
url("./materialdesignicons-webfont-subset.woff2") format("woff2"),
url("./materialdesignicons-webfont-subset.woff") format("woff");
src: url("./materialdesignicons-webfont-subset.woff2") format("woff2"),
url("./materialdesignicons-webfont-subset.woff") format("woff");
font-weight: normal;
font-style: normal;
}
@@ -13,7 +12,7 @@
.mdi:before,
.mdi-set {
display: inline-block;
font: normal normal normal 24px / 1 "Material Design Icons";
font: normal normal normal 24px/1 "Material Design Icons";
font-size: inherit;
text-rendering: auto;
line-height: inherit;
@@ -61,10 +60,6 @@
content: "\F002A";
}
.mdi-api::before {
content: "\F109B";
}
.mdi-api-off::before {
content: "\F1257";
}
@@ -121,10 +116,6 @@
content: "\F09D1";
}
.mdi-brightness-6::before {
content: "\F00DF";
}
.mdi-broadcast::before {
content: "\F1720";
}
@@ -157,6 +148,10 @@
content: "\F00F3";
}
.mdi-calendar-range::before {
content: "\F0679";
}
.mdi-chat::before {
content: "\F0B79";
}
@@ -193,6 +188,10 @@
content: "\F0132";
}
.mdi-checkbox-multiple-marked-outline::before {
content: "\F0139";
}
.mdi-chevron-double-left::before {
content: "\F013D";
}
@@ -257,10 +256,6 @@
content: "\F0167";
}
.mdi-code-braces::before {
content: "\F0169";
}
.mdi-code-json::before {
content: "\F0626";
}
@@ -341,6 +336,10 @@
content: "\F164B";
}
.mdi-database-import::before {
content: "\F095D";
}
.mdi-database-off::before {
content: "\F1640";
}
@@ -349,10 +348,6 @@
content: "\F1634";
}
.mdi-database-search-outline::before {
content: "\F1636";
}
.mdi-delete::before {
content: "\F01B4";
}
@@ -385,6 +380,10 @@
content: "\F0B8F";
}
.mdi-earth::before {
content: "\F01E7";
}
.mdi-emoticon::before {
content: "\F0C68";
}
@@ -453,10 +452,6 @@
content: "\F021C";
}
.mdi-file-music-outline::before {
content: "\F0E2A";
}
.mdi-file-outline::before {
content: "\F0224";
}
@@ -481,10 +476,6 @@
content: "\F0A4D";
}
.mdi-file-video-outline::before {
content: "\F0E2C";
}
.mdi-file-word-box::before {
content: "\F022D";
}
@@ -493,6 +484,10 @@
content: "\F0234";
}
.mdi-filter-variant::before {
content: "\F0236";
}
.mdi-folder::before {
content: "\F024B";
}
@@ -585,10 +580,6 @@
content: "\F0EFE";
}
.mdi-image-outline::before {
content: "\F0976";
}
.mdi-import::before {
content: "\F02FA";
}
@@ -617,6 +608,10 @@
content: "\F030B";
}
.mdi-keyboard-outline::before {
content: "\F097B";
}
.mdi-label::before {
content: "\F0315";
}
@@ -633,10 +628,6 @@
content: "\F031D";
}
.mdi-language-java::before {
content: "\F0B37";
}
.mdi-language-javascript::before {
content: "\F031E";
}
@@ -725,6 +716,10 @@
content: "\F035F";
}
.mdi-message-off-outline::before {
content: "\F164E";
}
.mdi-message-outline::before {
content: "\F0365";
}
@@ -733,6 +728,10 @@
content: "\F0369";
}
.mdi-message-text-outline::before {
content: "\F036A";
}
.mdi-microphone::before {
content: "\F036C";
}
@@ -753,6 +752,10 @@
content: "\F0A07";
}
.mdi-moon-waning-crescent::before {
content: "\F0F65";
}
.mdi-music-note-outline::before {
content: "\F0F74";
}
@@ -781,6 +784,10 @@
content: "\F0B3C";
}
.mdi-numeric-4::before {
content: "\F0B3D";
}
.mdi-open-in-new::before {
content: "\F03CC";
}
@@ -789,10 +796,6 @@
content: "\F03D6";
}
.mdi-package-variant-closed::before {
content: "\F03D7";
}
.mdi-page-first::before {
content: "\F0600";
}
@@ -801,8 +804,8 @@
content: "\F0601";
}
.mdi-palette-outline::before {
content: "\F0E0C";
.mdi-palette::before {
content: "\F03D8";
}
.mdi-paperclip::before {
@@ -813,6 +816,10 @@
content: "\F03E4";
}
.mdi-pause-circle-outline::before {
content: "\F03E6";
}
.mdi-pencil::before {
content: "\F03EB";
}
@@ -941,6 +948,10 @@
content: "\F0832";
}
.mdi-share-variant::before {
content: "\F0497";
}
.mdi-shield-check::before {
content: "\F0565";
}
@@ -953,10 +964,6 @@
content: "\F0BC4";
}
.mdi-shield-lock-outline::before {
content: "\F0CCC";
}
.mdi-shuffle-variant::before {
content: "\F049F";
}
@@ -1045,10 +1052,6 @@
content: "\F13B8";
}
.mdi-theme-light-dark::before {
content: "\F050E";
}
.mdi-timeline-text-outline::before {
content: "\F0BD4";
}
@@ -1073,10 +1076,6 @@
content: "\F0A7A";
}
.mdi-tune-variant::before {
content: "\F1542";
}
.mdi-unfold-more-horizontal::before {
content: "\F054F";
}
@@ -1105,6 +1104,14 @@
content: "\F056E";
}
.mdi-view-grid::before {
content: "\F0570";
}
.mdi-view-list::before {
content: "\F0572";
}
.mdi-volume-high::before {
content: "\F057E";
}
@@ -1145,23 +1152,19 @@
visibility: hidden;
}
.mdi-18px.mdi-set,
.mdi-18px.mdi:before {
.mdi-18px.mdi-set, .mdi-18px.mdi:before {
font-size: 18px;
}
.mdi-24px.mdi-set,
.mdi-24px.mdi:before {
.mdi-24px.mdi-set, .mdi-24px.mdi:before {
font-size: 24px;
}
.mdi-36px.mdi-set,
.mdi-36px.mdi:before {
.mdi-36px.mdi-set, .mdi-36px.mdi:before {
font-size: 36px;
}
.mdi-48px.mdi-set,
.mdi-48px.mdi:before {
.mdi-48px.mdi-set, .mdi-48px.mdi:before {
font-size: 48px;
}
@@ -1182,21 +1185,21 @@
}
.mdi-rotate-45 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(45deg);
transform: scaleX(-1) rotate(45deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(45deg);
-ms-transform: rotate(45deg);
transform: scaleY(-1) rotate(45deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(45deg);
transform: scaleX(-1) rotate(45deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(45deg);
-ms-transform: rotate(45deg);
transform: scaleY(-1) rotate(45deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
@@ -1207,21 +1210,21 @@
}
.mdi-rotate-90 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(90deg);
transform: scaleX(-1) rotate(90deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(90deg);
-ms-transform: rotate(90deg);
transform: scaleY(-1) rotate(90deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(90deg);
transform: scaleX(-1) rotate(90deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(90deg);
-ms-transform: rotate(90deg);
transform: scaleY(-1) rotate(90deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
@@ -1232,21 +1235,21 @@
}
.mdi-rotate-135 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(135deg);
transform: scaleX(-1) rotate(135deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(135deg);
-ms-transform: rotate(135deg);
transform: scaleY(-1) rotate(135deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(135deg);
transform: scaleX(-1) rotate(135deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(135deg);
-ms-transform: rotate(135deg);
transform: scaleY(-1) rotate(135deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
@@ -1257,21 +1260,21 @@
}
.mdi-rotate-180 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(180deg);
transform: scaleX(-1) rotate(180deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(180deg);
-ms-transform: rotate(180deg);
transform: scaleY(-1) rotate(180deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(180deg);
transform: scaleX(-1) rotate(180deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(180deg);
-ms-transform: rotate(180deg);
transform: scaleY(-1) rotate(180deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
@@ -1282,21 +1285,21 @@
}
.mdi-rotate-225 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(225deg);
transform: scaleX(-1) rotate(225deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(225deg);
-ms-transform: rotate(225deg);
transform: scaleY(-1) rotate(225deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(225deg);
transform: scaleX(-1) rotate(225deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(225deg);
-ms-transform: rotate(225deg);
transform: scaleY(-1) rotate(225deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
@@ -1307,21 +1310,21 @@
}
.mdi-rotate-270 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(270deg);
transform: scaleX(-1) rotate(270deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(270deg);
-ms-transform: rotate(270deg);
transform: scaleY(-1) rotate(270deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(270deg);
transform: scaleX(-1) rotate(270deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(270deg);
-ms-transform: rotate(270deg);
transform: scaleY(-1) rotate(270deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}
@@ -1332,21 +1335,21 @@
}
.mdi-rotate-315 {
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(315deg);
transform: scaleX(-1) rotate(315deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(315deg);
-ms-transform: rotate(315deg);
transform: scaleY(-1) rotate(315deg);
filter: FlipV;
-ms-filter: "FlipV";
}
/*
// Not included in production
&.mdi-flip-h:before {
-webkit-transform: scaleX(-1) rotate(315deg);
transform: scaleX(-1) rotate(315deg);
filter: FlipH;
-ms-filter: "FlipH";
}
&.mdi-flip-v:before {
-webkit-transform: scaleY(-1) rotate(315deg);
-ms-transform: rotate(315deg);
transform: scaleY(-1) rotate(315deg);
filter: FlipV;
-ms-filter: "FlipV";
}
*/
}

View File

@@ -48,7 +48,6 @@
</template>
<script setup lang="ts">
import axios, { type AxiosRequestConfig } from 'axios';
import { onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router';
@@ -58,6 +57,7 @@ import {
type ApiEnvelope,
type VersionData,
} from '@/api/v1';
import request, { resolveApiUrl } from '@/utils/request';
import { useI18n } from '@/i18n/composables';
type StartTimeData = {
@@ -81,7 +81,7 @@ const initialStartTime = ref<number | string | null>(null);
let restartTimer: ReturnType<typeof setInterval> | null = null;
let detecting = false;
const recoveryClient = axios.create();
const recoveryClient = request;
function normalizeVersion(version?: string | null) {
return (version || '').trim().replace(/^v/i, '');
@@ -116,7 +116,7 @@ function getDismissKey() {
return `astrbot-upgrade-recovery-dismissed:${coreVersion.value}:${dashboardVersion.value}`;
}
function recoveryRequestConfig(validateStatus = false): AxiosRequestConfig {
function recoveryRequestConfig(validateStatus = false) {
const headers: Record<string, string> = {};
const token =
localStorage.getItem('token') ||
@@ -136,7 +136,7 @@ function recoveryRequestConfig(validateStatus = false): AxiosRequestConfig {
async function fetchLegacyStartTime() {
const response = await recoveryClient.get<ApiEnvelope<StartTimeData>>(
'/api/stat/start-time',
resolveApiUrl('/api/stat/start-time'),
recoveryRequestConfig(),
);
return response.data?.data?.start_time ?? null;
@@ -196,7 +196,7 @@ async function restartCore() {
initialStartTime.value =
initialStartTime.value ?? (await fetchLegacyStartTime());
await recoveryClient.post<ApiEnvelope<unknown>>(
'/api/stat/restart-core',
resolveApiUrl('/api/stat/restart-core'),
undefined,
recoveryRequestConfig(),
);
@@ -239,7 +239,7 @@ async function detectUpgradeMismatch() {
detecting = true;
try {
const v1Response = await recoveryClient.get<ApiEnvelope<unknown>>(
'/api/v1/auth/setup-status',
resolveApiUrl('/api/v1/auth/setup-status'),
recoveryRequestConfig(true),
);
if (!isMissingApiKeyResponse(v1Response)) {
@@ -247,7 +247,7 @@ async function detectUpgradeMismatch() {
}
const legacyResponse = await recoveryClient.get<ApiEnvelope<VersionData>>(
'/api/stat/version',
resolveApiUrl('/api/stat/version'),
recoveryRequestConfig(true),
);
if (legacyResponse.status === 401 || legacyResponse.status >= 400) {

View File

@@ -18,7 +18,7 @@ interface AuthStore {
password: string,
code?: string,
trustDeviceToken?: boolean,
): Promise<undefined | "totp_required">;
): Promise<undefined | "totp_required" | "upgrade_recovery_required">;
logout(): void;
has_token(): boolean;
}

View File

@@ -1,7 +1,15 @@
import { defineStore } from "pinia";
import { router } from "@/router";
import { createLoginProof, type LoginChallenge } from "@/utils/authLoginProof";
import axios from "@/utils/request";
import {
authApi,
providerApi,
systemConfigApi,
UPGRADE_RECOVERY_EVENT,
UPGRADE_RECOVERY_TOKEN_KEY,
type ApiEnvelope,
type VersionData,
} from "@/api/v1";
import { httpClient } from "@/api/http";
export const useAuthStore = defineStore("auth", {
state: () => ({
@@ -14,17 +22,18 @@ export const useAuthStore = defineStore("auth", {
localStorage.setItem("user", this.username);
localStorage.setItem("token", data.token);
const passwordUpgradeRequired = !!data?.password_upgrade_required;
const passwordWarning = !!data?.change_pwd_hint || (!!data?.legacy_pwd_hint && !passwordUpgradeRequired);
const md5PwdHint = !!data?.md5_pwd_hint;
const passwordWarning = !!data?.change_pwd_hint || (md5PwdHint && !passwordUpgradeRequired);
if (passwordWarning) {
localStorage.setItem("change_pwd_hint", "true");
if (data?.legacy_pwd_hint && !passwordUpgradeRequired) {
localStorage.setItem("legacy_pwd_hint", "true");
if (md5PwdHint && !passwordUpgradeRequired) {
localStorage.setItem("md5_pwd_hint", "true");
} else {
localStorage.removeItem("legacy_pwd_hint");
localStorage.removeItem("md5_pwd_hint");
}
} else {
localStorage.removeItem("change_pwd_hint");
localStorage.removeItem("legacy_pwd_hint");
localStorage.removeItem("md5_pwd_hint");
}
if (passwordUpgradeRequired) {
localStorage.setItem("password_upgrade_required", "true");
@@ -49,64 +58,74 @@ export const useAuthStore = defineStore("auth", {
password: string,
code?: string,
trustDeviceToken = false,
): Promise<"totp_required" | undefined> {
): Promise<"totp_required" | "upgrade_recovery_required" | void> {
try {
const challengeRes = await axios.post("/api/auth/login/challenge");
const challenge = challengeRes.data?.data as LoginChallenge | undefined;
if (!challenge) {
return Promise.reject("Failed to initialize secure login");
}
let res;
if (challenge.algorithm === "argon2") {
res = await axios.post(
"/api/auth/login",
{
username: username,
password: password,
code: code,
trust_device_flag: trustDeviceToken,
},
{
validateStatus: (status) => (status >= 200 && status < 300) || status === 401,
},
);
} else {
const passwordProof = await createLoginProof(password, challenge);
res = await axios.post(
"/api/auth/login",
{
username: username,
challenge_id: challenge.challenge_id,
password_proof: passwordProof,
code: code,
trust_device_flag: trustDeviceToken,
},
{
validateStatus: (status) => (status >= 200 && status < 300) || status === 401,
},
);
}
if (res.status === 401 && res.data?.data?.totp_required) {
return "totp_required";
}
const res = await authApi.login({
username,
password,
code,
trust_device_flag: trustDeviceToken,
});
if (res.data.status === "error") {
return Promise.reject(res.data.message);
}
const legacyToken = String(res.data.data?.token || "");
if (res.legacyFallback && legacyToken) {
const versionRes = await httpClient.get<ApiEnvelope<VersionData>>(
"/api/stat/version",
{
headers: {
Authorization: `Bearer ${legacyToken}`,
},
validateStatus: () => true,
},
);
const versionData = versionRes.data?.data || {};
const coreVersion = String(versionData.version || "")
.trim()
.replace(/^v/i, "");
const dashboardVersion = String(versionData.dashboard_version || "")
.trim()
.replace(/^v/i, "");
if (
versionRes.status < 400 &&
coreVersion &&
dashboardVersion &&
coreVersion !== dashboardVersion
) {
sessionStorage.setItem(UPGRADE_RECOVERY_TOKEN_KEY, legacyToken);
window.dispatchEvent(
new CustomEvent(UPGRADE_RECOVERY_EVENT, {
detail: {
version: versionData.version,
dashboard_version: versionData.dashboard_version,
blocking: true,
},
}),
);
return "upgrade_recovery_required";
}
}
await this.finishAuthenticatedSession(res.data.data);
} catch (error) {
return Promise.reject(error);
} catch (error: any) {
if (error?.response?.status === 401 && error.response?.data?.data?.totp_required) {
return "totp_required";
}
return Promise.reject(error?.response?.data?.message || error);
}
},
async setup(username: string, password: string, confirmPassword: string): Promise<void> {
async setup(
username: string,
password: string,
confirmPassword: string,
): Promise<void> {
try {
const endpoint = this.has_token() ? "/api/auth/setup-authenticated" : "/api/auth/setup";
const res = await axios.post(endpoint, {
username: username,
password: password,
const res = await authApi.setup({
username,
password,
confirm_password: confirmPassword,
});
@@ -122,12 +141,13 @@ export const useAuthStore = defineStore("auth", {
async checkOnboardingCompleted(): Promise<boolean> {
try {
// 1. 检查平台配置
const platformRes = await axios.get("/api/config/get");
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
const platformRes = await systemConfigApi.get();
const systemConfig = (platformRes.data.data as any).config || {};
const hasPlatform = (systemConfig.platform || []).length > 0;
if (!hasPlatform) return false;
// 2. 检查提供者配置
const providerRes = await axios.get("/api/config/provider/template");
const providerRes = await providerApi.schema();
const providers = providerRes.data.data?.providers || [];
const sources = providerRes.data.data?.provider_sources || [];
const sourceMap = new Map();
@@ -153,9 +173,9 @@ export const useAuthStore = defineStore("auth", {
localStorage.removeItem("user");
localStorage.removeItem("token");
localStorage.removeItem("change_pwd_hint");
localStorage.removeItem("legacy_pwd_hint");
localStorage.removeItem("md5_pwd_hint");
localStorage.removeItem("password_upgrade_required");
void axios.post("/api/auth/logout").catch(() => undefined);
void authApi.logout().catch(() => undefined);
router.push("/auth/login");
},
has_token(): boolean {

View File

@@ -31,7 +31,7 @@ export function getPlatformIcon(name) {
} else if (name === "kook") {
return new URL("@/assets/images/platform_logos/kook.png", import.meta.url).href;
} else if (name === "vocechat") {
return new URL("@/assets/images/platform_logos/vocechat.png", import.meta.url).href;
return new URL("@/assets/images/platform_logos/qq.png", import.meta.url).href;
} else if (name === "satori" || name === "Satori") {
return new URL("@/assets/images/platform_logos/satori.png", import.meta.url).href;
} else if (name === "misskey") {