diff --git a/backend-fastapi/ai_platform/nodes/builtin/text_to_sql_node.py b/backend-fastapi/ai_platform/nodes/builtin/text_to_sql_node.py index 8264b73..10deefc 100644 --- a/backend-fastapi/ai_platform/nodes/builtin/text_to_sql_node.py +++ b/backend-fastapi/ai_platform/nodes/builtin/text_to_sql_node.py @@ -225,11 +225,10 @@ class TextToSqlNode(BaseNode): error='请输入要查询的问题', ) - if not model_id: - return NodeResult( - success=False, - error='请选择 LLM 模型', - ) + from ai_platform.services.llm_service import LLMService + + llm_service = LLMService(context.db_session) + model_id = await llm_service.resolve_chat_model_id(model_id) # Step 1: 获取数据库 Schema @@ -248,10 +247,7 @@ class TextToSqlNode(BaseNode): # Step 2: 调用 LLM 生成 SQL from datetime import datetime - from ai_platform.services.llm_service import LLMService - db_type = await self._get_db_type(db_connection) - llm_service = LLMService(context.db_session) llm_result = None use_function_calling = self.config.get('use_function_calling', True) @@ -354,6 +350,7 @@ class TextToSqlNode(BaseNode): tokens_used=response.total_tokens, elapsed_time=elapsed_time, metadata={ + 'model_id': str(model_id), 'model': response.model, 'thought': thought, 'suggested_next_node': 'db_sql', @@ -404,13 +401,22 @@ class TextToSqlNode(BaseNode): ) return NodeResult(success=False, error='请输入要查询的问题') - if not model_id: + import asyncio + from ai_platform.services.llm_service import LLMService + + llm_service = LLMService(context.db_session) + try: + model_id = asyncio.get_event_loop().run_until_complete( + llm_service.resolve_chat_model_id(model_id) + ) + except Exception as e: + error_message = str(e) yield TextToSqlStreamEvent( event_type='error', - content='请选择 LLM 模型', + content=error_message, is_finished=True, ) - return NodeResult(success=False, error='请选择 LLM 模型') + return NodeResult(success=False, error=error_message) # Step 1: 获取数据库 Schema yield TextToSqlStreamEvent( @@ -420,7 +426,6 @@ class TextToSqlNode(BaseNode): table_relations = self.config.get('table_relations', []) # 手动指定的表关系 - import asyncio schema_context = asyncio.get_event_loop().run_until_complete( self._get_schema_context( db_connection, @@ -449,13 +454,10 @@ class TextToSqlNode(BaseNode): ) from datetime import datetime - from ai_platform.services.llm_service import LLMService - db_type = asyncio.get_event_loop().run_until_complete( self._get_db_type(db_connection) ) - llm_service = LLMService() accumulated_content = '' total_tokens = 0 llm_result = None @@ -566,6 +568,7 @@ class TextToSqlNode(BaseNode): tokens_used=total_tokens, elapsed_time=elapsed_time, metadata={ + 'model_id': str(model_id), 'thought': thought, 'suggested_next_node': 'db_sql', }, diff --git a/web/apps/web-ele/src/router/access.ts b/web/apps/web-ele/src/router/access.ts index f5be129..f3f76e4 100644 --- a/web/apps/web-ele/src/router/access.ts +++ b/web/apps/web-ele/src/router/access.ts @@ -71,6 +71,43 @@ function createLightAiMenuRoute( return route as RouteRecordStringComponent; } +const PREFETCH_LIGHT_PAGE_KEYS = [ + '../views/_core/agent-chat/index.vue', + '../views/_core/menu/index.vue', + '../views/_core/page-render/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', +]; + +let lightPagesPrefetched = false; + +function scheduleIdleTask(callback: () => void) { + if (typeof window === 'undefined') return; + const requestIdle = (window as any).requestIdleCallback; + if (typeof requestIdle === 'function') { + requestIdle(callback, { timeout: 3000 }); + return; + } + window.setTimeout(callback, 1200); +} + +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); + } + } + }); +} + const LIGHT_AI_PLATFORM_MENU_ROUTES = [ createLightAiMenuRoute({ component: '/_core/agent-chat/index', @@ -220,7 +257,7 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { }; const routePageMap = normalizePageMap(pageMap); - return await generateAccessible(preferences.app.accessMode, { + const accessible = await generateAccessible(preferences.app.accessMode, { ...options, fetchMenuListAsync: async () => { const appContextStore = useAppContextStore(); @@ -235,6 +272,8 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) { layoutMap, pageMap, }); + prefetchLightPages(pageMap); + return accessible; } export { generateAccess }; diff --git a/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue b/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue index 2a71af6..7f642ce 100644 --- a/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue +++ b/web/apps/web-ele/src/views/ai-platform/workflow-runs/components/RunLogTimeline.vue @@ -25,6 +25,8 @@ const emit = defineEmits<{ select: [nodeId: string]; }>(); +type TagType = 'danger' | 'info' | 'primary' | 'success' | 'warning'; + function formatDuration(ms?: number) { if (!ms && ms !== 0) return '-'; if (ms < 1000) return `${ms}ms`; @@ -50,15 +52,19 @@ function getCommunication(log: ExecutionLogEntry) { return log.metadata?.communication || log.event?.communication || {}; } +function getCollaboration(log: ExecutionLogEntry) { + return log.metadata?.collaboration || log.event?.collaboration || {}; +} + +function getEventCollaboration(event: WorkflowRunEvent) { + return event.collaboration || event.event?.collaboration || {}; +} + function getMetaItems(log: ExecutionLogEntry) { const metadata = log.metadata || {}; - const items: Array<{ - label: string; - type?: 'danger' | 'info' | 'primary' | 'success' | 'warning'; - }> = []; + const items: Array<{ label: string; type?: TagType }> = []; const communication = getCommunication(log); - const collaboration = - metadata.collaboration || log.event?.collaboration || {}; + const collaboration = getCollaboration(log); const branchLabel = log.branch_label || log.branch || @@ -88,24 +94,14 @@ function getMetaItems(log: ExecutionLogEntry) { if (log.type || metadata.stream_event) { items.push({ label: getEventTypeLabel(log.type || log.event?.type), - type: log.status === 'failed' ? 'danger' : 'info', + type: getLogTone(log), }); } - if (branchLabel) { - items.push({ label: `分支: ${branchLabel}`, type: 'primary' }); - } - if (subflowName) { - items.push({ label: `子流程: ${subflowName}`, type: 'success' }); - } - if (providerName) { - items.push({ label: `提供商: ${providerName}`, type: 'info' }); - } - if (modelName) { - items.push({ label: `模型: ${modelName}`, type: 'info' }); - } - if (log.tokens_used) { - items.push({ label: `Token ${log.tokens_used}`, type: 'info' }); - } + if (branchLabel) items.push({ label: `分支: ${branchLabel}`, type: 'primary' }); + if (subflowName) items.push({ label: `子流程: ${subflowName}`, type: 'success' }); + if (providerName) items.push({ label: `提供商: ${providerName}`, type: 'info' }); + if (modelName) items.push({ label: `模型: ${modelName}`, type: 'info' }); + if (log.tokens_used) items.push({ label: `Token ${log.tokens_used}`, type: 'info' }); if (communication.channel) { items.push({ label: getChannelLabel(communication.channel), type: 'info' }); } @@ -118,6 +114,7 @@ function getMetaItems(log: ExecutionLogEntry) { function getChannelLabel(channel: string) { const labels: Record = { + agent_chat: '智能体对话', human_input: '人机协作', parallel_branch: '并行分支', subflow: '子流程', @@ -132,12 +129,18 @@ function getEventTypeLabel(type?: string) { complete: '完成', error: '错误', llm_chunk: 'LLM 输出', + loop_complete: '循环完成', + loop_iteration_complete: '循环迭代完成', + loop_iteration_error: '循环迭代失败', + loop_iteration_start: '循环迭代开始', + loop_start: '循环开始', message: '消息', node_complete: '节点完成', node_event: '节点事件', node_start: '节点开始', parallel_complete: '并行完成', parallel_start: '并行开始', + resume: '恢复执行', start: '开始', waiting_input: '等待输入', }; @@ -150,22 +153,24 @@ function countItems(value: any) { return 0; } +function getWaitingSummary(event: WorkflowRunEvent) { + const waitingConfig = event.waiting_config || event.config; + return stringifyBrief( + waitingConfig?.title || + waitingConfig?.question || + waitingConfig?.content || + waitingConfig || + '等待用户输入', + 160, + ); +} + function getEventSummary(event: WorkflowRunEvent) { const eventType = event.type || event.event?.type; - const waitingConfig = event.waiting_config || event.config; if (event.error_message) return event.error_message; if (event.error) return event.error; - if (eventType === 'waiting_input') { - return stringifyBrief( - waitingConfig?.title || - waitingConfig?.question || - waitingConfig?.content || - waitingConfig || - '等待用户输入', - 160, - ); - } + if (eventType === 'waiting_input') return getWaitingSummary(event); if (eventType === 'parallel_start') { return `启动 ${countItems(event.branches || event.branch_labels)} 个并行分支`; } @@ -196,13 +201,56 @@ function getEventSummary(event: WorkflowRunEvent) { return stringifyBrief(event, 140); } +function getEventMetaItems(event: WorkflowRunEvent) { + const collaboration = getEventCollaboration(event); + const items: Array<{ label: string; type?: TagType }> = []; + const branchLabel = + event.branch_label || + event.branch_id || + collaboration.branch_label || + collaboration.branch_id; + const subflowName = event.subflow_name || collaboration.subflow_name; + const providerName = event.provider_name || collaboration.provider_name; + const modelName = + event.model || + event.model_name || + event.model_id || + collaboration.model || + collaboration.model_name || + collaboration.model_id; + + if (branchLabel) items.push({ label: `分支 ${branchLabel}`, type: 'primary' }); + if (subflowName) items.push({ label: `子流程 ${subflowName}`, type: 'success' }); + if (providerName) items.push({ label: providerName, type: 'info' }); + if (modelName) items.push({ label: `模型 ${modelName}`, type: 'info' }); + if (event.tokens_used || event.total_tokens) { + items.push({ + label: `Token ${event.tokens_used || event.total_tokens}`, + type: 'info', + }); + } + + return items; +} + function getLogEvents(log: ExecutionLogEntry): WorkflowRunEvent[] { - const primaryEvent = log.event ? [log.event] : []; - const directEvents = Array.isArray(log.events) ? log.events : []; - const metadataEvents = Array.isArray(log.metadata?.events) - ? log.metadata.events - : []; - return [...primaryEvent, ...directEvents, ...metadataEvents].filter(Boolean); + const events = [ + ...(log.event ? [log.event] : []), + ...(Array.isArray(log.events) ? log.events : []), + ...(Array.isArray(log.metadata?.events) ? log.metadata.events : []), + ].filter(Boolean); + const seen = new Set(); + return events.filter((event) => { + const key = [ + event.type || event.event?.type || '', + event.node_id || event.event?.node_id || '', + event.timestamp || event.event?.timestamp || '', + stringifyBrief(event.content || event.message || event.error_message, 80), + ].join('|'); + if (seen.has(key)) return false; + seen.add(key); + return true; + }); } function getVisibleEvents(log: ExecutionLogEntry) { @@ -229,6 +277,33 @@ function getLogDetail(log: ExecutionLogEntry) { communication.message, ); } + +function getLogTitle(log: ExecutionLogEntry) { + if (log.node_label) return log.node_label; + if (log.node_id && log.node_id !== 'workflow') return log.node_id; + return getEventTypeLabel(log.type || log.event?.type); +} + +function getLogTone(log: ExecutionLogEntry): TagType { + if (log.status === 'failed' || log.type === 'error') return 'danger'; + if (log.status === 'waiting' || log.type === 'waiting_input') return 'warning'; + if (log.status === 'completed' || log.type === 'complete') return 'success'; + if (log.status === 'running') return 'primary'; + return 'info'; +} + +function getEventTone(event: WorkflowRunEvent): TagType { + const type = event.type || event.event?.type; + if (type === 'error' || event.error || event.error_message) return 'danger'; + if (type === 'waiting_input') return 'warning'; + if (type === 'complete' || type === 'node_complete' || type === 'parallel_complete') { + return 'success'; + } + if (type === 'start' || type === 'node_start' || type === 'parallel_start') { + return 'primary'; + } + return 'info'; +}