perf: improve admin page cold starts
This commit is contained in:
@@ -2444,6 +2444,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelConfig": {
|
||||
"providerManagement": "Model Providers",
|
||||
"addProvider": "Add Provider",
|
||||
"modelManagement": "Model Management",
|
||||
"addModel": "Add Model",
|
||||
"addedModels": "Added Models",
|
||||
"availableModels": "Available Models",
|
||||
"providerName": "Provider Name",
|
||||
"providerType": "Provider Type",
|
||||
"apiKey": "API Key",
|
||||
"apiBase": "API Base",
|
||||
"ollamaHost": "Ollama Host",
|
||||
"providerDescription": "Provider Description",
|
||||
"descriptionPlaceholder": "Enter description",
|
||||
"modelName": "Model Name",
|
||||
"displayName": "Display Name",
|
||||
"modelType": "Model Type",
|
||||
"chat": "Chat",
|
||||
"embedding": "Embedding",
|
||||
"rerank": "Rerank",
|
||||
"chatModel": "Chat Model",
|
||||
"embeddingModel": "Embedding Model",
|
||||
"completionModel": "Completion Model",
|
||||
"rerankModel": "Rerank Model",
|
||||
"maxTokens": "Max Tokens",
|
||||
"supportsVision": "Supports Vision",
|
||||
"supportsFunction": "Supports Function Calling",
|
||||
"enabled": "Enabled",
|
||||
"test": "Test",
|
||||
"editProvider": "Edit Provider",
|
||||
"editModel": "Edit Model",
|
||||
"selectProvider": "Select provider",
|
||||
"sourceApi": "API",
|
||||
"sourceDefault": "Default",
|
||||
"searchModel": "Search model",
|
||||
"refreshModels": "Refresh Models",
|
||||
"addAll": "Add All",
|
||||
"addDefaultModels": "Add Default Models",
|
||||
"addSelected": "Add Selected",
|
||||
"added": "Added",
|
||||
"noDefaultModels": "No default models available",
|
||||
"fetchFromApiSuccess": "Model list refreshed from API",
|
||||
"fetchFromApiFallback": "API returned no models, using default model list"
|
||||
},
|
||||
"workflowRuns": {
|
||||
"title": "Run History",
|
||||
"description": "View AI workflow execution records and node logs",
|
||||
|
||||
@@ -2444,6 +2444,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelConfig": {
|
||||
"providerManagement": "模型提供商",
|
||||
"addProvider": "新增提供商",
|
||||
"modelManagement": "模型管理",
|
||||
"addModel": "新增模型",
|
||||
"addedModels": "已添加模型",
|
||||
"availableModels": "可用模型",
|
||||
"providerName": "提供商名称",
|
||||
"providerType": "提供商类型",
|
||||
"apiKey": "API Key",
|
||||
"apiBase": "API 地址",
|
||||
"ollamaHost": "Ollama 地址",
|
||||
"providerDescription": "提供商描述",
|
||||
"descriptionPlaceholder": "请输入描述",
|
||||
"modelName": "模型名称",
|
||||
"displayName": "显示名称",
|
||||
"modelType": "模型类型",
|
||||
"chat": "对话",
|
||||
"embedding": "向量",
|
||||
"rerank": "重排",
|
||||
"chatModel": "对话模型",
|
||||
"embeddingModel": "向量模型",
|
||||
"completionModel": "补全模型",
|
||||
"rerankModel": "重排模型",
|
||||
"maxTokens": "最大 Token",
|
||||
"supportsVision": "支持视觉",
|
||||
"supportsFunction": "支持函数调用",
|
||||
"enabled": "已启用",
|
||||
"test": "测试",
|
||||
"editProvider": "编辑提供商",
|
||||
"editModel": "编辑模型",
|
||||
"selectProvider": "请选择提供商",
|
||||
"sourceApi": "接口获取",
|
||||
"sourceDefault": "默认列表",
|
||||
"searchModel": "搜索模型",
|
||||
"refreshModels": "刷新模型",
|
||||
"addAll": "全部添加",
|
||||
"addDefaultModels": "添加默认模型",
|
||||
"addSelected": "添加选中",
|
||||
"added": "已添加",
|
||||
"noDefaultModels": "暂无可用默认模型",
|
||||
"fetchFromApiSuccess": "已从接口刷新模型列表",
|
||||
"fetchFromApiFallback": "接口未返回模型,已使用默认模型列表"
|
||||
},
|
||||
"workflowRuns": {
|
||||
"title": "执行历史",
|
||||
"description": "查看 AI 工作流的运行记录与节点执行日志",
|
||||
|
||||
@@ -2444,6 +2444,50 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"modelConfig": {
|
||||
"providerManagement": "模型提供商",
|
||||
"addProvider": "新增提供商",
|
||||
"modelManagement": "模型管理",
|
||||
"addModel": "新增模型",
|
||||
"addedModels": "已新增模型",
|
||||
"availableModels": "可用模型",
|
||||
"providerName": "提供商名稱",
|
||||
"providerType": "提供商類型",
|
||||
"apiKey": "API Key",
|
||||
"apiBase": "API 位址",
|
||||
"ollamaHost": "Ollama 位址",
|
||||
"providerDescription": "提供商描述",
|
||||
"descriptionPlaceholder": "請輸入描述",
|
||||
"modelName": "模型名稱",
|
||||
"displayName": "顯示名稱",
|
||||
"modelType": "模型類型",
|
||||
"chat": "對話",
|
||||
"embedding": "向量",
|
||||
"rerank": "重排",
|
||||
"chatModel": "對話模型",
|
||||
"embeddingModel": "向量模型",
|
||||
"completionModel": "補全模型",
|
||||
"rerankModel": "重排模型",
|
||||
"maxTokens": "最大 Token",
|
||||
"supportsVision": "支援視覺",
|
||||
"supportsFunction": "支援函式呼叫",
|
||||
"enabled": "已啟用",
|
||||
"test": "測試",
|
||||
"editProvider": "編輯提供商",
|
||||
"editModel": "編輯模型",
|
||||
"selectProvider": "請選擇提供商",
|
||||
"sourceApi": "介面取得",
|
||||
"sourceDefault": "預設清單",
|
||||
"searchModel": "搜尋模型",
|
||||
"refreshModels": "重新整理模型",
|
||||
"addAll": "全部新增",
|
||||
"addDefaultModels": "新增預設模型",
|
||||
"addSelected": "新增選中",
|
||||
"added": "已新增",
|
||||
"noDefaultModels": "暫無可用預設模型",
|
||||
"fetchFromApiSuccess": "已從介面重新整理模型清單",
|
||||
"fetchFromApiFallback": "介面未返回模型,已使用預設模型清單"
|
||||
},
|
||||
"workflowRuns": {
|
||||
"title": "執行歷史",
|
||||
"description": "查看 AI 工作流的運行記錄與節點執行日誌",
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getAllMenusApi } from '#/api/core/menu';
|
||||
import { BasicLayout, IFrameView } from '#/layouts';
|
||||
import { useAppContextStore } from '#/store/app-context';
|
||||
|
||||
import { createAdminRuntimePage } from './admin-runtime-page';
|
||||
import { isLightMenuName } from './light-menu';
|
||||
|
||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||
@@ -71,16 +72,29 @@ function createLightAiMenuRoute(
|
||||
return route as RouteRecordStringComponent;
|
||||
}
|
||||
|
||||
const PREFETCH_LIGHT_PAGE_KEYS = [
|
||||
'../views/_core/agent-chat/index.vue',
|
||||
const ADMIN_RUNTIME_PAGE_KEYS = new Set([
|
||||
'../views/_core/menu/index.vue',
|
||||
'../views/_core/page-render/index.vue',
|
||||
'../views/_core/permission/index.vue',
|
||||
'../views/_core/role/index.vue',
|
||||
'../views/_core/user/index.vue',
|
||||
'../views/ai-platform/agent/index.vue',
|
||||
'../views/ai-platform/model/index.vue',
|
||||
'../views/ai-platform/workflow-runs/index.vue',
|
||||
'../views/ai-platform/workflow/index.vue',
|
||||
]);
|
||||
|
||||
function withAdminRuntimePages(pageMap: ComponentRecordType) {
|
||||
return Object.fromEntries(
|
||||
Object.entries(pageMap).map(([key, loader]) => [
|
||||
key,
|
||||
ADMIN_RUNTIME_PAGE_KEYS.has(key) && typeof loader === 'function'
|
||||
? createAdminRuntimePage(loader as () => Promise<{ default: any }>)
|
||||
: loader,
|
||||
]),
|
||||
) as ComponentRecordType;
|
||||
}
|
||||
|
||||
const PREFETCH_LIGHT_PAGE_KEYS = [
|
||||
'../views/_core/menu/index.vue',
|
||||
'../views/_core/role/index.vue',
|
||||
'../views/_core/user/index.vue',
|
||||
];
|
||||
|
||||
let lightPagesPrefetched = false;
|
||||
@@ -89,23 +103,25 @@ function scheduleIdleTask(callback: () => void) {
|
||||
if (typeof window === 'undefined') return;
|
||||
const requestIdle = (window as any).requestIdleCallback;
|
||||
if (typeof requestIdle === 'function') {
|
||||
requestIdle(callback, { timeout: 3000 });
|
||||
requestIdle(callback, { timeout: 8000 });
|
||||
return;
|
||||
}
|
||||
window.setTimeout(callback, 1200);
|
||||
window.setTimeout(callback, 8000);
|
||||
}
|
||||
|
||||
function prefetchLightPages(pageMap: ComponentRecordType) {
|
||||
if (lightPagesPrefetched) return;
|
||||
lightPagesPrefetched = true;
|
||||
scheduleIdleTask(() => {
|
||||
for (const key of PREFETCH_LIGHT_PAGE_KEYS) {
|
||||
const loader = pageMap[key];
|
||||
if (typeof loader === 'function') {
|
||||
void loader().catch(() => undefined);
|
||||
window.setTimeout(() => {
|
||||
scheduleIdleTask(() => {
|
||||
for (const key of PREFETCH_LIGHT_PAGE_KEYS) {
|
||||
const loader = pageMap[key];
|
||||
if (typeof loader === 'function') {
|
||||
void loader().catch(() => undefined);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, 12_000);
|
||||
}
|
||||
|
||||
const LIGHT_AI_PLATFORM_MENU_ROUTES = [
|
||||
@@ -225,7 +241,7 @@ function filterAvailableRoutes<
|
||||
}
|
||||
|
||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
const pageMap: ComponentRecordType = import.meta.glob([
|
||||
const rawPageMap: ComponentRecordType = import.meta.glob([
|
||||
'../views/_core/account-settings/index.vue',
|
||||
'../views/_core/agent-chat/**/*.vue',
|
||||
'../views/_core/announcement/index.vue',
|
||||
@@ -247,6 +263,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
||||
'../views/ai-platform/workflow/index.vue',
|
||||
'../views/ai-platform/workflow-runs/index.vue',
|
||||
]);
|
||||
const pageMap = withAdminRuntimePages(rawPageMap);
|
||||
|
||||
const layoutMap: ComponentRecordType = {
|
||||
BasicLayout,
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { Component } from 'vue';
|
||||
|
||||
import { defineComponent, h, onMounted, ref, shallowRef } from 'vue';
|
||||
|
||||
import { ensureAdminRuntime, isAdminRuntimeReady } from '#/runtime/admin';
|
||||
|
||||
type RouteComponentModule = {
|
||||
default: Component;
|
||||
};
|
||||
|
||||
type RouteComponentLoader = () => Promise<RouteComponentModule>;
|
||||
|
||||
function renderSkeleton(message = 'Loading page...') {
|
||||
const rows = Array.from({ length: 7 }, (_, index) =>
|
||||
h('div', {
|
||||
key: index,
|
||||
style: {
|
||||
background: 'hsl(var(--muted))',
|
||||
borderRadius: '6px',
|
||||
height: index === 0 ? '28px' : '16px',
|
||||
opacity: index === 0 ? '0.55' : '0.36',
|
||||
width: index === 0 ? '220px' : `${82 - index * 6}%`,
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
return h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '14px',
|
||||
height: '100%',
|
||||
padding: '18px',
|
||||
},
|
||||
},
|
||||
[
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
color: 'hsl(var(--muted-foreground))',
|
||||
fontSize: '13px',
|
||||
},
|
||||
},
|
||||
message,
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{
|
||||
style: {
|
||||
background: 'hsl(var(--card))',
|
||||
border: '1px solid hsl(var(--border))',
|
||||
borderRadius: '8px',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '14px',
|
||||
minHeight: '260px',
|
||||
padding: '18px',
|
||||
},
|
||||
},
|
||||
rows,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
function createAdminRuntimePage(loader: RouteComponentLoader) {
|
||||
return async () => ({
|
||||
default: defineComponent({
|
||||
name: 'AdminRuntimePage',
|
||||
inheritAttrs: false,
|
||||
setup(_, { attrs, slots }) {
|
||||
const loadedComponent = shallowRef<Component>();
|
||||
const errorMessage = ref('');
|
||||
|
||||
async function loadPage() {
|
||||
try {
|
||||
if (!isAdminRuntimeReady()) {
|
||||
await ensureAdminRuntime();
|
||||
}
|
||||
const module = await loader();
|
||||
loadedComponent.value = module.default;
|
||||
} catch (error) {
|
||||
errorMessage.value =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
void loadPage();
|
||||
});
|
||||
|
||||
return () => {
|
||||
const Loaded = loadedComponent.value;
|
||||
if (Loaded) return h(Loaded, attrs, slots);
|
||||
if (errorMessage.value) {
|
||||
return renderSkeleton(`Page load failed: ${errorMessage.value}`);
|
||||
}
|
||||
return renderSkeleton();
|
||||
};
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
export { createAdminRuntimePage };
|
||||
@@ -29,14 +29,6 @@ function isMainHomeRoute(path: string) {
|
||||
return /\/page-render\/main_home$/.test(path);
|
||||
}
|
||||
|
||||
function shouldLoadAdminRuntime(path: string) {
|
||||
if (isMainHomeRoute(path)) return false;
|
||||
if (path === '/agent-chat/codex') return false;
|
||||
if (path.startsWith('/auth/')) return false;
|
||||
if (path.startsWith('/file-preview/')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
function warmAdminRuntimeAfterPaint() {
|
||||
const run = () => {
|
||||
void ensureAdminRuntime();
|
||||
@@ -192,9 +184,6 @@ function setupAccessGuard(router: Router) {
|
||||
|
||||
// 是否已经生成过动态路由
|
||||
if (accessStore.isAccessChecked) {
|
||||
if (shouldLoadAdminRuntime(to.path)) {
|
||||
await ensureAdminRuntime();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -68,11 +68,11 @@ function scheduleIdle(callback: () => void) {
|
||||
const browserWindow = window as WindowWithIdleCallback;
|
||||
|
||||
if (browserWindow.requestIdleCallback) {
|
||||
browserWindow.requestIdleCallback(callback, { timeout: 3000 });
|
||||
browserWindow.requestIdleCallback(callback, { timeout: 8000 });
|
||||
return;
|
||||
}
|
||||
|
||||
window.setTimeout(callback, 1200);
|
||||
window.setTimeout(callback, 8000);
|
||||
}
|
||||
|
||||
function canPrefetch() {
|
||||
@@ -160,7 +160,7 @@ function prefetchAiPlatformPages(currentPath = '') {
|
||||
}
|
||||
|
||||
if (isSystemRoute(currentPath)) {
|
||||
delayedPrefetch(`core:${currentPath}`, corePrefetchTasks, 8000);
|
||||
delayedPrefetch(`core:${currentPath}`, corePrefetchTasks, 18_000);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -168,7 +168,7 @@ function prefetchAiPlatformPages(currentPath = '') {
|
||||
delayedPrefetch(
|
||||
`agent-chat:${currentPath}`,
|
||||
[aiPrefetchTasks.agentChat, aiPrefetchTasks.aiChatPanel],
|
||||
6000,
|
||||
14_000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -177,12 +177,12 @@ function prefetchAiPlatformPages(currentPath = '') {
|
||||
delayedPrefetch(
|
||||
`ai:${currentPath}`,
|
||||
getAiRoutePrefetchTasks(currentPath),
|
||||
6000,
|
||||
14_000,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
delayedPrefetch('core:home', corePrefetchTasks, 10_000);
|
||||
delayedPrefetch('core:home', corePrefetchTasks, 22_000);
|
||||
}
|
||||
|
||||
export { prefetchAiPlatformPages };
|
||||
|
||||
@@ -18,6 +18,10 @@ async function setupAdminRuntime() {
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
function isAdminRuntimeReady() {
|
||||
return initialized;
|
||||
}
|
||||
|
||||
async function ensureAdminRuntime() {
|
||||
if (initialized) return;
|
||||
|
||||
@@ -28,4 +32,4 @@ async function ensureAdminRuntime() {
|
||||
await initializing;
|
||||
}
|
||||
|
||||
export { ensureAdminRuntime };
|
||||
export { ensureAdminRuntime, isAdminRuntimeReady };
|
||||
|
||||
Reference in New Issue
Block a user