From 9647a227da7b38474334b61a60157b31e7f9bfd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?cls=5F=E5=AE=81=E6=B3=A2=E6=9C=AC=E6=9C=BA?= <908705107@qq.com> Date: Sun, 21 Jun 2026 21:01:38 +0800 Subject: [PATCH] perf: lighten routes and improve ai runtime events --- .../src/components/ChatBox/MessageBubble.vue | 6 ++ .../web-ele/src/components/ChatBox/types.ts | 4 +- .../composables/useEventHandler.ts | 32 +++++++-- .../web-ele/src/components/zq-table/api.ts | 6 -- web/apps/web-ele/src/router/access.ts | 3 - web/apps/web-ele/src/router/light-menu.ts | 4 -- web/apps/web-ele/src/router/prefetch.ts | 67 ++++++++++++++++--- web/apps/web-ele/src/router/routes/core.ts | 18 +++++ web/apps/web-ele/src/router/routes/index.ts | 2 - .../src/router/routes/modules/ai-platform.ts | 10 +-- .../workflow/editor/components/ChatPanel.vue | 18 +++-- .../workflow/editor/components/RunPanel.vue | 5 +- .../ai-platform/workflow/editor/index.vue | 29 -------- 13 files changed, 130 insertions(+), 74 deletions(-) diff --git a/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue b/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue index d9e56f8..a929fe4 100644 --- a/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue +++ b/web/apps/web-ele/src/components/ChatBox/MessageBubble.vue @@ -139,6 +139,9 @@ const getStepLabel = (step: ReasoningStep) => { if (step.status === 'running') { return '执行中'; } + if (step.status === 'failed') { + return '执行失败'; + } return '已完成'; } @@ -169,6 +172,9 @@ const getStepColor = (step: ReasoningStep) => { if (step.status === 'running') { return 'text-blue-500'; } + if (step.status === 'failed') { + return 'text-red-500'; + } return 'text-emerald-500'; } diff --git a/web/apps/web-ele/src/components/ChatBox/types.ts b/web/apps/web-ele/src/components/ChatBox/types.ts index 7e4e527..93e3779 100644 --- a/web/apps/web-ele/src/components/ChatBox/types.ts +++ b/web/apps/web-ele/src/components/ChatBox/types.ts @@ -83,8 +83,8 @@ export interface ReasoningStep { target?: Record; }; output?: any; - /** 步骤状态:running 执行中,completed 已完成 */ - status?: 'completed' | 'running'; + /** 步骤状态:running 执行中,completed 已完成,failed 执行失败 */ + status?: 'completed' | 'failed' | 'running'; timestamp?: string; } diff --git a/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts b/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts index c3168c8..ee28cef 100644 --- a/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts +++ b/web/apps/web-ele/src/components/ai-chat-panel/composables/useEventHandler.ts @@ -214,6 +214,18 @@ export function useEventHandler(config: EventHandlerConfig) { }; }; + const getStreamErrorMessage = (event: StreamEvent) => + event.error_message || + event.message || + event.error || + event.content || + '执行失败'; + + const isSuccessEvent = (event: StreamEvent) => + !event.error_message && + !event.error && + (!event.status || event.status === 'success' || event.status === 'completed'); + /** 处理流式事件 */ const handleStreamEvent = (event: StreamEvent, msgId: string) => { switch (event.type) { @@ -357,18 +369,19 @@ export function useEventHandler(config: EventHandlerConfig) { } case 'error': { + const errorMessage = getStreamErrorMessage(event); currentSteps.value.push({ type: 'error', - content: event.message || event.content || '执行失败', + content: errorMessage, node_id: event.node_id, node_type: event.node_type, ...buildReasoningMeta(event), - status: 'completed', + status: 'failed', timestamp: new Date().toISOString(), }); updateAssistantMessage(msgId, { status: 'failed', - error_message: event.message || event.content || '执行失败', + error_message: errorMessage, reasoning_steps: [...currentSteps.value], }); running.value = false; @@ -535,25 +548,30 @@ export function useEventHandler(config: EventHandlerConfig) { event.node_type, event.node_label, ); + const nodeSucceeded = isSuccessEvent(event); + const nodeStatus = nodeSucceeded ? 'completed' : 'failed'; + const nodeContent = nodeSucceeded + ? completeLabel + : `${completeLabel || event.node_id || '节点'}:${getStreamErrorMessage(event)}`; if (existingIndex === -1) { currentSteps.value.push({ type: 'node_complete', - content: completeLabel, + content: nodeContent, node_id: event.node_id, node_type: event.node_type, ...buildReasoningMeta(event), output: event.outputs, - status: 'completed', + status: nodeStatus, timestamp: new Date().toISOString(), }); } else { currentSteps.value[existingIndex] = { ...currentSteps.value[existingIndex]!, type: 'node_complete', - content: completeLabel, + content: nodeContent, ...buildReasoningMeta(event), output: event.outputs, - status: 'completed', + status: nodeStatus, }; } updateAssistantMessage(msgId, { diff --git a/web/apps/web-ele/src/components/zq-table/api.ts b/web/apps/web-ele/src/components/zq-table/api.ts index 0dae6f1..b0455df 100644 --- a/web/apps/web-ele/src/components/zq-table/api.ts +++ b/web/apps/web-ele/src/components/zq-table/api.ts @@ -165,18 +165,12 @@ export class ZqTableApi = any> { // 核心数据加载逻辑 async reload(params: Record = {}) { - console.log('[ZqTable] reload called, state:', this.state?.gridOptions); const { proxyConfig } = this.state?.gridOptions || {}; if (!proxyConfig?.ajax?.query) { - console.warn( - '[ZqTable] No ajax.query configured, proxyConfig:', - proxyConfig, - ); this.setLoading(false); return; } - console.log('[ZqTable] Starting data load...'); this.setLoading(true); try { // 1. 获取表单数据(带超时保护) diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts index 4493dd8..c2938f6 100644 --- a/web/apps/web-ele/src/router/access.ts +++ b/web/apps/web-ele/src/router/access.ts @@ -9,7 +9,6 @@ import { preferences } from '@vben/preferences'; import { getAllMenusApi } from '#/api/core/menu'; import { BasicLayout, IFrameView } from '#/layouts'; -import { $t } from '#/locales'; import { useAppContextStore } from '#/store/app-context'; import { isLightMenuName } from './light-menu'; @@ -194,8 +193,6 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { '../views/ai-platform/workflow/editor/index.vue', '../views/ai-platform/workflow/index.vue', '../views/ai-platform/workflow-runs/index.vue', - '../views/dashboard/analytics/index.vue', - '../views/dashboard/workspace/index.vue', ]); const layoutMap: ComponentRecordType = { diff --git a/web/apps/web-ele/src/router/light-menu.ts b/web/apps/web-ele/src/router/light-menu.ts index e59a3ff..7e05b2f 100644 --- a/web/apps/web-ele/src/router/light-menu.ts +++ b/web/apps/web-ele/src/router/light-menu.ts @@ -1,8 +1,5 @@ export const LIGHT_MENU_NAMES = new Set([ 'ControlCenter', - 'Dashboard', - 'DashboardAnalytics', - 'DashboardWorkspace', 'AIAgent', 'AIKnowledgeDetail', 'AIModelConfig', @@ -26,7 +23,6 @@ export const LIGHT_MENU_NAMES = new Set([ const LIGHT_ROUTE_PATH_PREFIXES = [ '/control-center', - '/dashboard', '/ai-platform/agent', '/ai-platform/knowledge', '/ai-platform/knowledge-base', diff --git a/web/apps/web-ele/src/router/prefetch.ts b/web/apps/web-ele/src/router/prefetch.ts index 97127df..73e286a 100644 --- a/web/apps/web-ele/src/router/prefetch.ts +++ b/web/apps/web-ele/src/router/prefetch.ts @@ -11,20 +11,32 @@ type WindowWithIdleCallback = Window & { ) => IdleCallbackHandle; }; +type NavigatorWithConnection = Navigator & { + connection?: { + effectiveType?: string; + saveData?: boolean; + }; +}; + let prefetched = false; -const criticalPrefetchTasks = [ - () => import('#/views/_core/agent-chat/index.vue'), - () => import('#/views/ai-platform/workflow-runs/index.vue'), - () => import('#/components/ai-chat-panel/AiChatPanel.vue'), - () => import('#/views/ai-platform/workflow-runs/modules/detail-dialog.vue'), +const corePrefetchTasks = [ + () => import('#/views/_core/user/index.vue'), + () => import('#/views/_core/menu/index.vue'), + () => import('#/views/_core/role/index.vue'), ]; -const deferredPrefetchTasks = [ +const aiInteractivePrefetchTasks = [ + () => import('#/views/_core/agent-chat/index.vue'), + () => import('#/components/ai-chat-panel/AiChatPanel.vue'), +]; + +const aiManagementPrefetchTasks = [ () => import('#/views/ai-platform/agent/index.vue'), () => import('#/views/ai-platform/workflow/index.vue'), () => import('#/views/ai-platform/model/index.vue'), () => import('#/views/ai-platform/knowledge/index.vue'), + () => import('#/views/ai-platform/workflow-runs/index.vue'), ]; function scheduleIdle(callback: () => void) { @@ -38,6 +50,28 @@ function scheduleIdle(callback: () => void) { window.setTimeout(callback, 1200); } +function canPrefetch() { + if (typeof document !== 'undefined' && document.visibilityState !== 'visible') { + return false; + } + + const connection = (navigator as NavigatorWithConnection).connection; + if (connection?.saveData) { + return false; + } + + return !/(^|-)2g$/.test(connection?.effectiveType || ''); +} + +async function runTasksSequentially(tasks: Array<() => Promise>) { + for (const task of tasks) { + if (!canPrefetch()) { + return; + } + await task().catch(() => undefined); + } +} + function prefetchAiPlatformPages() { if (prefetched || typeof window === 'undefined') { return; @@ -45,11 +79,22 @@ function prefetchAiPlatformPages() { prefetched = true; window.setTimeout(() => { - void Promise.allSettled(criticalPrefetchTasks.map((task) => task())); - }, 300); - scheduleIdle(() => { - void Promise.allSettled(deferredPrefetchTasks.map((task) => task())); - }); + scheduleIdle(() => { + void runTasksSequentially(corePrefetchTasks); + }); + }, 800); + + window.setTimeout(() => { + scheduleIdle(() => { + void runTasksSequentially(aiInteractivePrefetchTasks); + }); + }, 4000); + + window.setTimeout(() => { + scheduleIdle(() => { + void runTasksSequentially(aiManagementPrefetchTasks); + }); + }, 9000); } export { prefetchAiPlatformPages }; diff --git a/web/apps/web-ele/src/router/routes/core.ts b/web/apps/web-ele/src/router/routes/core.ts index e27c0b6..e63fdac 100644 --- a/web/apps/web-ele/src/router/routes/core.ts +++ b/web/apps/web-ele/src/router/routes/core.ts @@ -32,6 +32,24 @@ const coreRoutes: RouteRecordRaw[] = [ redirect: preferences.app.defaultHomePath, children: [], }, + { + meta: { hideInMenu: true, title: '控制中心' }, + name: 'DashboardLegacyRedirect', + path: '/dashboard', + redirect: '/page-render/main_home', + }, + { + meta: { hideInMenu: true, title: '控制中心' }, + name: 'DashboardAnalyticsLegacyRedirect', + path: '/dashboard/analytics', + redirect: '/page-render/main_home', + }, + { + meta: { hideInMenu: true, title: '控制中心' }, + name: 'DashboardWorkspaceLegacyRedirect', + path: '/dashboard/workspace', + redirect: '/page-render/main_home', + }, { component: AuthPageLayout, meta: { diff --git a/web/apps/web-ele/src/router/routes/index.ts b/web/apps/web-ele/src/router/routes/index.ts index 1a20f1e..880a361 100644 --- a/web/apps/web-ele/src/router/routes/index.ts +++ b/web/apps/web-ele/src/router/routes/index.ts @@ -4,7 +4,6 @@ import { mergeRouteModules, traverseTreeValues } from '@vben/utils'; import { coreRoutes, fallbackNotFoundRoute } from './core'; import aiPlatformRoutes from './modules/ai-platform'; -import dashboardRoutes from './modules/dashboard'; // 有需要可以自行打开注释,并创建文件夹 // const externalRouteFiles = import.meta.glob('./external/**/*.ts', { eager: true }); @@ -13,7 +12,6 @@ import dashboardRoutes from './modules/dashboard'; /** 动态路由 */ const dynamicRoutes: RouteRecordRaw[] = mergeRouteModules({ './modules/ai-platform.ts': { default: aiPlatformRoutes }, - './modules/dashboard.ts': { default: dashboardRoutes }, }); /** 外部路由列表,访问这些页面可以不需要Layout,可能用于内嵌在别的系统(不会显示在菜单中) */ diff --git a/web/apps/web-ele/src/router/routes/modules/ai-platform.ts b/web/apps/web-ele/src/router/routes/modules/ai-platform.ts index ed6042f..6a53500 100644 --- a/web/apps/web-ele/src/router/routes/modules/ai-platform.ts +++ b/web/apps/web-ele/src/router/routes/modules/ai-platform.ts @@ -2,7 +2,7 @@ import type { RouteRecordRaw } from 'vue-router'; const routes: RouteRecordRaw[] = [ { - meta: { hideInMenu: true }, + meta: { hideInMenu: true, title: '控制中心' }, name: 'ControlCenterLegacyRedirect', path: '/control-center', redirect: '/page-render/main_home', @@ -17,13 +17,13 @@ const routes: RouteRecordRaw[] = [ component: () => import('#/views/_core/page-render/index.vue'), }, { - meta: { hideInMenu: true }, + meta: { hideInMenu: true, title: '知识库' }, name: 'AIKnowledgeLegacyRedirect', path: '/ai-platform/knowledge', redirect: '/ai-platform/knowledge-base', }, { - meta: { hideInMenu: true }, + meta: { hideInMenu: true, title: '知识库' }, name: 'SubAppAIKnowledgeLegacyRedirect', path: '/app/:appCode/ai-platform/knowledge', redirect: (to) => `/app/${to.params.appCode}/ai-platform/knowledge-base`, @@ -65,13 +65,13 @@ const routes: RouteRecordRaw[] = [ component: () => import('#/views/ai-platform/model/index.vue'), }, { - meta: { hideInMenu: true }, + meta: { hideInMenu: true, title: '知识库详情' }, name: 'AIKnowledgeLegacyDetailRedirect', path: '/ai-platform/knowledge/detail/:id', redirect: (to) => `/ai-platform/knowledge-base/detail/${to.params.id}`, }, { - meta: { hideInMenu: true }, + meta: { hideInMenu: true, title: '知识库详情' }, name: 'SubAppAIKnowledgeLegacyDetailRedirect', path: '/app/:appCode/ai-platform/knowledge/detail/:id', redirect: (to) => diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/ChatPanel.vue b/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/ChatPanel.vue index 5c19c58..8fddb14 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/ChatPanel.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/ChatPanel.vue @@ -98,6 +98,11 @@ const getStreamErrorMessage = ( event?.content || $t('ai-platform.workflow.editor.chatPanel.execFailed'); +const isSuccessEvent = (event: WorkflowStreamEvent) => + !event.error_message && + !event.error && + (!event.status || event.status === 'success' || event.status === 'completed'); + // 发送消息 const handleSend = (text: string) => { if (!text || running.value) return; @@ -448,15 +453,20 @@ const handleStreamEvent = (event: WorkflowStreamEvent, msgId: string) => { event.node_type, event.node_label, ); + const nodeSucceeded = isSuccessEvent(event); + const nodeStatus = nodeSucceeded ? 'completed' : 'failed'; + const nodeContent = nodeSucceeded + ? nodeLabel + : `${nodeLabel || event.node_id || '节点'}:${getStreamErrorMessage(event)}`; if (existingIndex === -1) { // 如果没有找到对应的 start,添加新的 complete 步骤 currentSteps.value.push({ type: 'node_complete', - content: nodeLabel, + content: nodeContent, node_id: event.node_id, node_type: event.node_type, output: event.outputs, - status: 'completed', + status: nodeStatus, timestamp: new Date().toISOString(), }); } else { @@ -464,9 +474,9 @@ const handleStreamEvent = (event: WorkflowStreamEvent, msgId: string) => { currentSteps.value[existingIndex] = { ...currentSteps.value[existingIndex], type: 'node_complete', - content: nodeLabel, + content: nodeContent, output: event.outputs, - status: 'completed', + status: nodeStatus, }; } updateAssistantMessage(msgId, { diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue b/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue index 3493370..2a05806 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow/editor/components/RunPanel.vue @@ -296,7 +296,10 @@ const handleRun = () => { // 更新节点完成状态(包含输出数据) const state = nodeStates.value.get(event.node_id || ''); if (state) { - state.status = event.status === 'success' ? 'success' : 'failed'; + state.status = + event.status === 'success' || event.status === 'completed' + ? 'success' + : 'failed'; state.elapsed_time = event.elapsed_time; state.tokens_used = event.tokens_used; state.error_message = event.error_message; diff --git a/web/apps/web-ele/src/views/ai-platform/workflow/editor/index.vue b/web/apps/web-ele/src/views/ai-platform/workflow/editor/index.vue index 71302c9..33f42bc 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow/editor/index.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow/editor/index.vue @@ -151,10 +151,6 @@ watch( ); if (orphanedChildren.length > 0) { - console.log( - '[Loop Delete] Cascade deleting orphaned children:', - orphanedChildren.map((n) => n.id), - ); nextTick(() => { removeNodes(orphanedChildren.map((n) => n.id)); }); @@ -483,10 +479,6 @@ const handleChildNodeDrag = (changes: any[]) => { // 查找被删除节点的子节点(在节点被删除前,子节点的 parentNode 仍然指向它) const childNodes = nodes.value.filter((n) => n.parentNode === change.id); if (childNodes.length > 0) { - console.log( - '[Loop Delete] Found child nodes to delete:', - childNodes.map((n) => n.id), - ); for (const child of childNodes) { childNodesToDelete.push(child.id); } @@ -1095,26 +1087,6 @@ const onDrop = (event: DragEvent) => { position.y < nodeY + nodeHeight; const isInside = isInsideX && isInsideY; - console.log('[Loop Drop Check]', { - dropPosition: position, - loopNode: { - id: node.id, - x: nodeX, - y: nodeY, - width: nodeWidth, - height: nodeHeight, - }, - bounds: { - left: nodeX, - right: nodeX + nodeWidth, - top: nodeY + loopHeaderHeight, - bottom: nodeY + nodeHeight, - }, - isInsideX, - isInsideY, - isInside, - }); - if (isInside) { parentNodeId = node.id; // 计算相对于父节点的位置(子节点位置是相对于父节点左上角的) @@ -1126,7 +1098,6 @@ const onDrop = (event: DragEvent) => { x: Math.min(Math.max(20, relX), nodeWidth - 280), // 限制在容器宽度内 y: Math.min(Math.max(loopHeaderHeight, relY), nodeHeight - 50), // 限制在容器高度内 }; - console.log('[Loop Child Position]', { relativePosition, relX, relY }); break; } }