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": {
|
"workflowRuns": {
|
||||||
"title": "Run History",
|
"title": "Run History",
|
||||||
"description": "View AI workflow execution records and node logs",
|
"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": {
|
"workflowRuns": {
|
||||||
"title": "执行历史",
|
"title": "执行历史",
|
||||||
"description": "查看 AI 工作流的运行记录与节点执行日志",
|
"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": {
|
"workflowRuns": {
|
||||||
"title": "執行歷史",
|
"title": "執行歷史",
|
||||||
"description": "查看 AI 工作流的運行記錄與節點執行日誌",
|
"description": "查看 AI 工作流的運行記錄與節點執行日誌",
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { getAllMenusApi } from '#/api/core/menu';
|
|||||||
import { BasicLayout, IFrameView } from '#/layouts';
|
import { BasicLayout, IFrameView } from '#/layouts';
|
||||||
import { useAppContextStore } from '#/store/app-context';
|
import { useAppContextStore } from '#/store/app-context';
|
||||||
|
|
||||||
|
import { createAdminRuntimePage } from './admin-runtime-page';
|
||||||
import { isLightMenuName } from './light-menu';
|
import { isLightMenuName } from './light-menu';
|
||||||
|
|
||||||
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
const forbiddenComponent = () => import('#/views/_core/fallback/forbidden.vue');
|
||||||
@@ -71,16 +72,29 @@ function createLightAiMenuRoute(
|
|||||||
return route as RouteRecordStringComponent;
|
return route as RouteRecordStringComponent;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PREFETCH_LIGHT_PAGE_KEYS = [
|
const ADMIN_RUNTIME_PAGE_KEYS = new Set([
|
||||||
'../views/_core/agent-chat/index.vue',
|
|
||||||
'../views/_core/menu/index.vue',
|
'../views/_core/menu/index.vue',
|
||||||
'../views/_core/page-render/index.vue',
|
'../views/_core/permission/index.vue',
|
||||||
'../views/_core/role/index.vue',
|
'../views/_core/role/index.vue',
|
||||||
'../views/_core/user/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-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;
|
let lightPagesPrefetched = false;
|
||||||
@@ -89,23 +103,25 @@ function scheduleIdleTask(callback: () => void) {
|
|||||||
if (typeof window === 'undefined') return;
|
if (typeof window === 'undefined') return;
|
||||||
const requestIdle = (window as any).requestIdleCallback;
|
const requestIdle = (window as any).requestIdleCallback;
|
||||||
if (typeof requestIdle === 'function') {
|
if (typeof requestIdle === 'function') {
|
||||||
requestIdle(callback, { timeout: 3000 });
|
requestIdle(callback, { timeout: 8000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
window.setTimeout(callback, 1200);
|
window.setTimeout(callback, 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function prefetchLightPages(pageMap: ComponentRecordType) {
|
function prefetchLightPages(pageMap: ComponentRecordType) {
|
||||||
if (lightPagesPrefetched) return;
|
if (lightPagesPrefetched) return;
|
||||||
lightPagesPrefetched = true;
|
lightPagesPrefetched = true;
|
||||||
scheduleIdleTask(() => {
|
window.setTimeout(() => {
|
||||||
for (const key of PREFETCH_LIGHT_PAGE_KEYS) {
|
scheduleIdleTask(() => {
|
||||||
const loader = pageMap[key];
|
for (const key of PREFETCH_LIGHT_PAGE_KEYS) {
|
||||||
if (typeof loader === 'function') {
|
const loader = pageMap[key];
|
||||||
void loader().catch(() => undefined);
|
if (typeof loader === 'function') {
|
||||||
|
void loader().catch(() => undefined);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}, 12_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
const LIGHT_AI_PLATFORM_MENU_ROUTES = [
|
const LIGHT_AI_PLATFORM_MENU_ROUTES = [
|
||||||
@@ -225,7 +241,7 @@ function filterAvailableRoutes<
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function generateAccess(options: GenerateMenuAndRoutesOptions) {
|
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/account-settings/index.vue',
|
||||||
'../views/_core/agent-chat/**/*.vue',
|
'../views/_core/agent-chat/**/*.vue',
|
||||||
'../views/_core/announcement/index.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/index.vue',
|
||||||
'../views/ai-platform/workflow-runs/index.vue',
|
'../views/ai-platform/workflow-runs/index.vue',
|
||||||
]);
|
]);
|
||||||
|
const pageMap = withAdminRuntimePages(rawPageMap);
|
||||||
|
|
||||||
const layoutMap: ComponentRecordType = {
|
const layoutMap: ComponentRecordType = {
|
||||||
BasicLayout,
|
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);
|
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() {
|
function warmAdminRuntimeAfterPaint() {
|
||||||
const run = () => {
|
const run = () => {
|
||||||
void ensureAdminRuntime();
|
void ensureAdminRuntime();
|
||||||
@@ -192,9 +184,6 @@ function setupAccessGuard(router: Router) {
|
|||||||
|
|
||||||
// 是否已经生成过动态路由
|
// 是否已经生成过动态路由
|
||||||
if (accessStore.isAccessChecked) {
|
if (accessStore.isAccessChecked) {
|
||||||
if (shouldLoadAdminRuntime(to.path)) {
|
|
||||||
await ensureAdminRuntime();
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,11 +68,11 @@ function scheduleIdle(callback: () => void) {
|
|||||||
const browserWindow = window as WindowWithIdleCallback;
|
const browserWindow = window as WindowWithIdleCallback;
|
||||||
|
|
||||||
if (browserWindow.requestIdleCallback) {
|
if (browserWindow.requestIdleCallback) {
|
||||||
browserWindow.requestIdleCallback(callback, { timeout: 3000 });
|
browserWindow.requestIdleCallback(callback, { timeout: 8000 });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
window.setTimeout(callback, 1200);
|
window.setTimeout(callback, 8000);
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPrefetch() {
|
function canPrefetch() {
|
||||||
@@ -160,7 +160,7 @@ function prefetchAiPlatformPages(currentPath = '') {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (isSystemRoute(currentPath)) {
|
if (isSystemRoute(currentPath)) {
|
||||||
delayedPrefetch(`core:${currentPath}`, corePrefetchTasks, 8000);
|
delayedPrefetch(`core:${currentPath}`, corePrefetchTasks, 18_000);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,7 +168,7 @@ function prefetchAiPlatformPages(currentPath = '') {
|
|||||||
delayedPrefetch(
|
delayedPrefetch(
|
||||||
`agent-chat:${currentPath}`,
|
`agent-chat:${currentPath}`,
|
||||||
[aiPrefetchTasks.agentChat, aiPrefetchTasks.aiChatPanel],
|
[aiPrefetchTasks.agentChat, aiPrefetchTasks.aiChatPanel],
|
||||||
6000,
|
14_000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -177,12 +177,12 @@ function prefetchAiPlatformPages(currentPath = '') {
|
|||||||
delayedPrefetch(
|
delayedPrefetch(
|
||||||
`ai:${currentPath}`,
|
`ai:${currentPath}`,
|
||||||
getAiRoutePrefetchTasks(currentPath),
|
getAiRoutePrefetchTasks(currentPath),
|
||||||
6000,
|
14_000,
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
delayedPrefetch('core:home', corePrefetchTasks, 10_000);
|
delayedPrefetch('core:home', corePrefetchTasks, 22_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
export { prefetchAiPlatformPages };
|
export { prefetchAiPlatformPages };
|
||||||
|
|||||||
@@ -18,6 +18,10 @@ async function setupAdminRuntime() {
|
|||||||
initialized = true;
|
initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isAdminRuntimeReady() {
|
||||||
|
return initialized;
|
||||||
|
}
|
||||||
|
|
||||||
async function ensureAdminRuntime() {
|
async function ensureAdminRuntime() {
|
||||||
if (initialized) return;
|
if (initialized) return;
|
||||||
|
|
||||||
@@ -28,4 +32,4 @@ async function ensureAdminRuntime() {
|
|||||||
await initializing;
|
await initializing;
|
||||||
}
|
}
|
||||||
|
|
||||||
export { ensureAdminRuntime };
|
export { ensureAdminRuntime, isAdminRuntimeReady };
|
||||||
|
|||||||
Reference in New Issue
Block a user