feat: improve workflow observability and lighten admin frontend

This commit is contained in:
2026-06-22 06:34:53 +08:00
parent 1b835a4aad
commit e1ef4c1a70
11 changed files with 360 additions and 416 deletions
@@ -226,6 +226,7 @@ def _make_execution_log_entry(
**extra: Any, **extra: Any,
) -> dict: ) -> dict:
extra.setdefault('timestamp', datetime.now().isoformat()) extra.setdefault('timestamp', datetime.now().isoformat())
events = extra.pop('events', None)
metadata = dict(extra.pop('metadata', {}) or {}) metadata = dict(extra.pop('metadata', {}) or {})
event_type = _execution_log_event_type(extra.get('status')) event_type = _execution_log_event_type(extra.get('status'))
collaboration = _node_collaboration_metadata(node_map, node_id, node_type) collaboration = _node_collaboration_metadata(node_map, node_id, node_type)
@@ -249,19 +250,106 @@ def _make_execution_log_entry(
error=extra.get('error'), error=extra.get('error'),
) )
metadata['communication']['timestamp'] = extra.get('timestamp') metadata['communication']['timestamp'] = extra.get('timestamp')
return { entry = {
'node_id': node_id, 'node_id': node_id,
'node_type': node_type, 'node_type': node_type,
'node_label': _node_label_from_map(node_map, node_id), 'node_label': _node_label_from_map(node_map, node_id),
'metadata': metadata, 'metadata': metadata,
**extra, **extra,
} }
if events:
entry['events'] = events
return entry
def _event_log_status(event_type: str, event: Dict[str, Any]) -> str:
if event_type == 'error':
return 'failed'
if event_type == 'waiting_input':
return 'waiting'
if event_type in ('start', 'node_start', 'parallel_start'):
return 'running'
status = event.get('status') or 'completed'
if status == 'success':
return 'completed'
if status == 'failed':
return 'failed'
return status
def _make_stream_event_log_entry(event: Dict[str, Any]) -> Optional[dict]:
event_type = event.get('type')
if not event_type or event_type == 'llm_chunk':
return None
event_data = copy.deepcopy(event)
timestamp = event_data.get('timestamp') or datetime.now().isoformat()
event_data.setdefault('timestamp', timestamp)
communication = dict(event_data.get('communication') or {})
communication.setdefault('event', event_type)
communication.setdefault('timestamp', timestamp)
node_id = str(event_data.get('node_id') or event_data.get('run_id') or 'workflow')
node_type = str(event_data.get('node_type') or 'workflow')
node_label = (
event_data.get('node_label')
or event_data.get('workflow_name')
or communication.get('message')
or event_type
)
output = event_data.get('outputs')
if output is None:
output = event_data.get('content') or event_data.get('message')
error = event_data.get('error_message') or event_data.get('error')
return {
'type': event_type,
'node_id': node_id,
'node_type': node_type,
'node_label': str(node_label),
'status': _event_log_status(event_type, event_data),
'timestamp': timestamp,
'elapsed_time': event_data.get('elapsed_time'),
'tokens_used': event_data.get('tokens_used') or 0,
'output': output,
'error': error,
'event': event_data,
'events': [event_data],
'metadata': {
'stream_event': True,
'collaboration': event_data.get('collaboration') or {},
'communication': communication,
},
}
def _log_timestamp(log: Dict[str, Any]) -> str:
metadata = log.get('metadata') or {}
communication = metadata.get('communication') or {}
return log.get('timestamp') or communication.get('timestamp') or ''
def _merge_execution_logs(node_logs: List[dict], event_logs: List[dict]) -> List[dict]:
base_logs = [
log for log in (node_logs or [])
if not (log.get('metadata') or {}).get('stream_event')
]
merged = base_logs + list(event_logs or [])
return sorted(
merged,
key=lambda item: (_log_timestamp(item), 0 if item.get('metadata', {}).get('stream_event') else 1),
)
def _node_result_metadata(result: NodeResult) -> dict: def _node_result_metadata(result: NodeResult) -> dict:
return dict(result.metadata or {}) return dict(result.metadata or {})
def _node_result_events(result: NodeResult) -> List[Dict[str, Any]]:
events = getattr(result, 'events', None) or []
return copy.deepcopy(events) if events else []
def _event_collaboration_metadata( def _event_collaboration_metadata(
node_map: Dict[str, Any], node_map: Dict[str, Any],
node_id: str, node_id: str,
@@ -842,6 +930,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id, target_node_id=target_node_id,
)) ))
@@ -1024,6 +1113,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
branch=branch_id, branch=branch_id,
branch_label=branch_labels.get(branch_id, branch_id), branch_label=branch_labels.get(branch_id, branch_id),
@@ -1252,6 +1342,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
branch=branch_id, branch=branch_id,
branch_label=branch_labels.get(branch_id, branch_id), branch_label=branch_labels.get(branch_id, branch_id),
)) ))
@@ -1545,6 +1636,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
loop_iteration=iteration, loop_iteration=iteration,
)) ))
@@ -1593,7 +1685,10 @@ class AIWorkflowService:
current_id, current_id,
node_type, node_type,
status='waiting', status='waiting',
output=result.output,
elapsed_time=elapsed, elapsed_time=elapsed,
metadata=_node_result_metadata(result),
events=_node_result_events(result),
loop_iteration=iteration, loop_iteration=iteration,
)) ))
@@ -1733,15 +1828,20 @@ class AIWorkflowService:
) )
self._db.add(run) self._db.add(run)
await self._db.flush() await self._db.flush()
event_logs: List[dict] = []
try: try:
# 使用异步生成器执行工作流 # 使用异步生成器执行工作流
async for event in self._execute_workflow_stream(workflow, inputs, run, conversation_history, use_draft): async for event in self._execute_workflow_stream(workflow, inputs, run, conversation_history, use_draft):
yield event yield event
event_log = _make_stream_event_log_entry(event)
if event_log:
event_logs.append(event_log)
run.execution_log = _merge_execution_logs(run.execution_log or [], event_logs)
# 在关键事件后提交数据库状态 # 在关键事件后提交数据库状态
event_type = event.get('type') event_type = event.get('type')
if event_type in ('node_complete', 'waiting_input', 'complete', 'error'): if event_log or event_type in ('node_complete', 'waiting_input', 'complete', 'error'):
await self._db.commit() await self._db.commit()
# 更新运行记录 # 更新运行记录
@@ -1771,13 +1871,19 @@ class AIWorkflowService:
await self._db.commit() await self._db.commit()
# 发送错误事件给前端 # 发送错误事件给前端
yield { error_event = {
'type': 'error', 'type': 'error',
'message': str(e), 'message': str(e),
'error_message': str(e), 'error_message': str(e),
'run_id': run.id, 'run_id': run.id,
'workflow_id': workflow.id, 'workflow_id': workflow.id,
} }
error_log = _make_stream_event_log_entry(error_event)
if error_log:
event_logs.append(error_log)
run.execution_log = _merge_execution_logs(run.execution_log or [], event_logs)
await self._db.commit()
yield error_event
async def _execute_workflow_stream( async def _execute_workflow_stream(
self, self,
@@ -2026,6 +2132,7 @@ class AIWorkflowService:
output=result.output, output=result.output,
elapsed_time=elapsed, elapsed_time=elapsed,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id, target_node_id=target_node_id,
) )
@@ -2067,6 +2174,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
inputs=copy.deepcopy(node_inputs), inputs=copy.deepcopy(node_inputs),
target_node_id=target_node_id, target_node_id=target_node_id,
) )
@@ -2557,6 +2665,7 @@ class AIWorkflowService:
output=result.output, output=result.output,
elapsed_time=elapsed, elapsed_time=elapsed,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
target_node_id=target_node_id, target_node_id=target_node_id,
) )
logs.append(log_entry) logs.append(log_entry)
@@ -2595,6 +2704,7 @@ class AIWorkflowService:
elapsed_time=elapsed, elapsed_time=elapsed,
tokens_used=result.tokens_used, tokens_used=result.tokens_used,
metadata=_node_result_metadata(result), metadata=_node_result_metadata(result),
events=_node_result_events(result),
target_node_id=target_node_id, target_node_id=target_node_id,
) )
logs.append(log_entry) logs.append(log_entry)
@@ -271,6 +271,14 @@ export interface WorkflowListItem {
} }
/** 工作流运行记录 */ /** 工作流运行记录 */
export interface WorkflowRunEvent {
type?: string;
content?: string;
message?: string;
message_type?: string;
[key: string]: any;
}
export interface ExecutionLogEntry { export interface ExecutionLogEntry {
node_id: string; node_id: string;
node_type: string; node_type: string;
@@ -285,6 +293,7 @@ export interface ExecutionLogEntry {
branch?: string; branch?: string;
branch_label?: string; branch_label?: string;
metadata?: Record<string, any>; metadata?: Record<string, any>;
events?: WorkflowRunEvent[];
} }
export interface WorkflowRunListItem { export interface WorkflowRunListItem {
-19
View File
@@ -1,22 +1,3 @@
export * from '../online-dev/form-data-api';
export * from '../online-dev/form-manager';
// 注意: login-log 模块请直接导入以避免 PaginatedResponse 类型冲突
// import { getLoginLogListApi, ... } from '#/api/core/login-log';
// 注意: ai-platform 模块请直接导入以避免类型冲突
// import { ... } from '#/api/core/ai-platform';
export * from '../online-dev/screen-design';
export * from './application';
export * from './auth'; export * from './auth';
export * from './data-source';
export * from './database-connection';
export * from './database-manager';
export * from './dept';
export * from './dict';
export * from './menu';
export * from './oauth'; export * from './oauth';
export * from './permission';
export * from './post';
export * from './role';
export * from './scheduler';
export * from './server-monitor';
export * from './user'; export * from './user';
+27 -1
View File
@@ -22,7 +22,33 @@ import zhTwLocale from 'element-plus/es/locale/lang/zh-tw';
const elementLocale = ref<Language>(defaultLocale); const elementLocale = ref<Language>(defaultLocale);
const modules = import.meta.glob('./langs/**/*.json'); const modules = import.meta.glob([
'./langs/**/account-settings.json',
'./langs/**/ai-platform.json',
'./langs/**/announcement.json',
'./langs/**/authentication.json',
'./langs/**/chat.json',
'./langs/**/common.json',
'./langs/**/core.json',
'./langs/**/database-connection.json',
'./langs/**/database-manager.json',
'./langs/**/dept.json',
'./langs/**/dict.json',
'./langs/**/file-manager.json',
'./langs/**/menu-title.json',
'./langs/**/menu.json',
'./langs/**/message.json',
'./langs/**/page.json',
'./langs/**/permission.json',
'./langs/**/post.json',
'./langs/**/role.json',
'./langs/**/system.json',
'./langs/**/system-config.json',
'./langs/**/ui.json',
'./langs/**/user-avatar.json',
'./langs/**/user.json',
'./langs/**/workflow.json',
]);
const localesMap = loadLocalesMapFromDir( const localesMap = loadLocalesMapFromDir(
/\.\/langs\/([^/]+)\/(.*)\.json$/, /\.\/langs\/([^/]+)\/(.*)\.json$/,
-14
View File
@@ -175,27 +175,13 @@ async function generateAccess(options: GenerateMenuAndRoutesOptions) {
'../views/_core/agent-chat/**/*.vue', '../views/_core/agent-chat/**/*.vue',
'../views/_core/announcement/index.vue', '../views/_core/announcement/index.vue',
'../views/_core/announcement/list.vue', '../views/_core/announcement/list.vue',
'../views/_core/application/index.vue',
'../views/_core/authentication/login.vue', '../views/_core/authentication/login.vue',
'../views/_core/chat/**/*.vue',
'../views/_core/database-monitor/**/*.vue',
'../views/_core/dept/index.vue',
'../views/_core/dict/index.vue',
'../views/_core/fallback/**/*.vue', '../views/_core/fallback/**/*.vue',
'../views/_core/file-manager/index.vue',
'../views/_core/file-preview/index.vue',
'../views/_core/login-log/index.vue',
'../views/_core/menu/index.vue', '../views/_core/menu/index.vue',
'../views/_core/message/index.vue', '../views/_core/message/index.vue',
'../views/_core/org-chart/index.vue',
'../views/_core/page-render/index.vue', '../views/_core/page-render/index.vue',
'../views/_core/permission/index.vue', '../views/_core/permission/index.vue',
'../views/_core/post/index.vue',
'../views/_core/role/index.vue', '../views/_core/role/index.vue',
'../views/_core/redis-monitor/**/*.vue',
'../views/_core/server-monitor/index.vue',
'../views/_core/system-config/index.vue',
'../views/_core/ui-config/index.vue',
'../views/_core/user/index.vue', '../views/_core/user/index.vue',
'../views/ai-platform/agent/editor/index.vue', '../views/ai-platform/agent/editor/index.vue',
'../views/ai-platform/agent/index.vue', '../views/ai-platform/agent/index.vue',
+14 -38
View File
@@ -1,50 +1,42 @@
export const LIGHT_MENU_NAMES = new Set([ export const LIGHT_MENU_NAMES = new Set([
'AccountSettings', 'AccountSettings',
'Application',
'ControlCenter',
'AIAgent', 'AIAgent',
'AIAgentEditor',
'AgentChat',
'AIKnowledgeDetail', 'AIKnowledgeDetail',
'AIKnowledgeLegacyDetailRedirect',
'AIKnowledgeLegacyRedirect',
'AIModelConfig', 'AIModelConfig',
'AIPlatform', 'AIPlatform',
'AIWorkflow', 'AIWorkflow',
'AIWorkflowEditor',
'AIWorkflowRuns', 'AIWorkflowRuns',
'AnnouncementList', 'AnnouncementList',
'AnnouncementManage', 'AnnouncementManage',
'CodexAgentChat', 'CodexAgentChat',
'FileManager',
'Message', 'Message',
'MessageCenter', 'MessageCenter',
'MessageList', 'MessageList',
'KnowledgeBase', 'KnowledgeBase',
'LoginLog',
'OrgNode',
'DatabaseMonitor',
'RedisMonitor',
'ServerMonitor',
'StartChat',
'SystemDept',
'SystemDict',
'SystemFileManager',
'SystemLoginLog',
'SystemManagement', 'SystemManagement',
'SystemOrgChart',
'SystemTools',
'SystemConfig',
'SystemConfigManager',
'SystemMonitoring',
'SystemMenu', 'SystemMenu',
'SystemPermission', 'SystemPermission',
'SystemPost',
'SystemRole', 'SystemRole',
'UIConfig', 'PageRenderMainHome',
'UIConfigManager', 'SubAppAIAgentEditor',
'SubAppAgentChat',
'SubAppAIKnowledgeDetail',
'SubAppAIKnowledgeLegacyDetailRedirect',
'SubAppAIKnowledgeLegacyRedirect',
'SubAppAIModelConfig',
'SubAppAIWorkflowEditor',
'SubAppAIWorkflowRuns',
'UserManagement', 'UserManagement',
'userCenter', 'userCenter',
'userSettings', 'userSettings',
]); ]);
const LIGHT_ROUTE_PATH_PREFIXES = [ const LIGHT_ROUTE_PATH_PREFIXES = [
'/control-center',
'/ai-platform/agent', '/ai-platform/agent',
'/ai-platform/knowledge', '/ai-platform/knowledge',
'/ai-platform/knowledge-base', '/ai-platform/knowledge-base',
@@ -52,30 +44,14 @@ const LIGHT_ROUTE_PATH_PREFIXES = [
'/ai-platform/workflow', '/ai-platform/workflow',
'/ai-platform/workflow-runs', '/ai-platform/workflow-runs',
'/agent-chat', '/agent-chat',
'/application',
'/chat',
'/message/list', '/message/list',
'/message/announcement', '/message/announcement',
'/message/announcement-list', '/message/announcement-list',
'/page-render/main_home', '/page-render/main_home',
'/monitor',
'/core/ui-config',
'/system-config',
'/system/dept',
'/system/dict',
'/system/file-manager',
'/system/login-log',
'/system/org-chart',
'/system/post',
'/system/config',
'/system/menu', '/system/menu',
'/system/new-permission', '/system/new-permission',
'/system/role', '/system/role',
'/system/ui-config',
'/system/user', '/system/user',
'/tool/dict',
'/tool/file',
'/log/login-log',
'/user-center', '/user-center',
]; ];
@@ -1,7 +1,8 @@
<script lang="ts" setup> <script lang="ts" setup>
import { defineAsyncComponent, onMounted, ref, watch } from 'vue'; import { computed, defineAsyncComponent, onMounted, ref, watch } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute, useRouter } from 'vue-router';
import { IconifyIcon } from '@vben/icons';
import { ElEmpty, ElMessage, ElScrollbar } from 'element-plus'; import { ElEmpty, ElMessage, ElScrollbar } from 'element-plus';
import { getPageByCodeApi } from '#/api/core/page-manager'; import { getPageByCodeApi } from '#/api/core/page-manager';
@@ -9,6 +10,7 @@ import { getPageByCodeApi } from '#/api/core/page-manager';
defineOptions({ name: 'PageRender' }); defineOptions({ name: 'PageRender' });
const route = useRoute(); const route = useRoute();
const router = useRouter();
const DashboardRenderer = defineAsyncComponent( const DashboardRenderer = defineAsyncComponent(
() => import('#/components/dashboard-design/DashboardRenderer.vue'), () => import('#/components/dashboard-design/DashboardRenderer.vue'),
); );
@@ -127,9 +129,30 @@ const opsHomeMenus = [
}, },
{ {
icon: 'lucide:sliders-horizontal', icon: 'lucide:sliders-horizontal',
id: 'ui-config', id: 'model-config',
path: '/system/ui-config', path: '/ai-platform/model',
title: '界面配置', title: '模型配置',
},
];
const homeSections = [
{
description: '智能体、流程、知识库、模型和运行历史',
id: 'ai',
menus: aiHomeMenus,
title: 'AI 协作',
},
{
description: '用户、角色、菜单、权限和公告消息',
id: 'admin',
menus: adminHomeMenus,
title: '基础管理',
},
{
description: '需求分析、协作流程、可观测性和模型配置',
id: 'ops',
menus: opsHomeMenus,
title: '协作运维',
}, },
]; ];
@@ -231,6 +254,12 @@ function getPageCode(): string {
return ''; return '';
} }
const isMainHome = computed(() => getPageCode() === 'main_home');
function goTo(path: string) {
router.push(path);
}
async function loadPageData() { async function loadPageData() {
const code = getPageCode(); const code = getPageCode();
if (!code) { if (!code) {
@@ -238,6 +267,12 @@ async function loadPageData() {
return; return;
} }
if (code === 'main_home') {
pageConfig.value = '';
loading.value = false;
return;
}
loading.value = true; loading.value = true;
try { try {
const page = await getPageByCodeApi(code); const page = await getPageByCodeApi(code);
@@ -269,7 +304,62 @@ watch(
<template> <template>
<ElScrollbar class="rounded-[8px] py-3"> <ElScrollbar class="rounded-[8px] py-3">
<div v-loading="loading" class="h-[calc(100vh-120px)] rounded-[8px] px-3"> <div
v-if="isMainHome"
class="min-h-[calc(100vh-120px)] rounded-[8px] px-3 pb-6"
>
<div class="mb-4 px-1">
<h1 class="text-foreground text-xl font-semibold">AI Agent Admin</h1>
<p class="text-muted-foreground mt-1 text-sm">
面向智能体协作的轻量后台
</p>
</div>
<div class="grid gap-3 xl:grid-cols-3">
<section
v-for="section in homeSections"
:key="section.id"
class="border-border bg-card rounded-[8px] border p-4"
>
<div class="mb-4">
<h2 class="text-foreground text-sm font-semibold">
{{ section.title }}
</h2>
<p class="text-muted-foreground mt-1 text-xs">
{{ section.description }}
</p>
</div>
<div class="grid grid-cols-2 gap-2">
<button
v-for="item in section.menus"
:key="item.id"
class="border-border hover:border-primary/70 hover:bg-accent group flex min-h-20 flex-col items-start justify-between rounded-[8px] border p-3 text-left transition-colors"
type="button"
@click="goTo(item.path)"
>
<span
class="flex size-8 items-center justify-center rounded-[6px] text-white shadow-sm"
:style="{ background: item.color || item.bgColor }"
>
<IconifyIcon :icon="item.icon" class="size-4" />
</span>
<span
class="text-foreground group-hover:text-primary mt-3 text-sm font-medium"
>
{{ item.title }}
</span>
</button>
</div>
</section>
</div>
</div>
<div
v-else
v-loading="loading"
class="h-[calc(100vh-120px)] rounded-[8px] px-3"
>
<DashboardRenderer v-if="pageConfig" :config="pageConfig" /> <DashboardRenderer v-if="pageConfig" :config="pageConfig" />
<ElEmpty v-else-if="!loading" description="暂无页面配置" /> <ElEmpty v-else-if="!loading" description="暂无页面配置" />
</div> </div>
@@ -1,9 +1,18 @@
<script setup lang="ts"> <script setup lang="ts">
import type { ExecutionLogEntry } from '#/api/ai-platform/ai-platform'; import type {
ExecutionLogEntry,
WorkflowRunEvent,
} from '#/api/ai-platform/ai-platform';
import { $t } from '@vben/locales'; import { $t } from '@vben/locales';
import { ElEmpty, ElScrollbar, ElTag, ElTimeline, ElTimelineItem } from 'element-plus'; import {
ElEmpty,
ElScrollbar,
ElTag,
ElTimeline,
ElTimelineItem,
} from 'element-plus';
import RunStatusTag from './RunStatusTag.vue'; import RunStatusTag from './RunStatusTag.vue';
@@ -31,9 +40,14 @@ function stringifyBrief(value: any, maxLength = 180) {
return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text; return text.length > maxLength ? `${text.slice(0, maxLength)}...` : text;
} }
function getCommunication(log: ExecutionLogEntry) {
return log.metadata?.communication || {};
}
function getMetaItems(log: ExecutionLogEntry) { function getMetaItems(log: ExecutionLogEntry) {
const metadata = log.metadata || {}; const metadata = log.metadata || {};
const items: Array<{ label: string; type?: 'info' | 'primary' | 'success' | 'warning' }> = []; const items: Array<{ label: string; type?: 'info' | 'primary' | 'success' | 'warning' }> = [];
const communication = getCommunication(log);
if (log.branch_label || log.branch) { if (log.branch_label || log.branch) {
items.push({ label: `分支: ${log.branch_label || log.branch}`, type: 'primary' }); items.push({ label: `分支: ${log.branch_label || log.branch}`, type: 'primary' });
@@ -41,9 +55,15 @@ function getMetaItems(log: ExecutionLogEntry) {
if (metadata.subflow_name) { if (metadata.subflow_name) {
items.push({ label: `子流程: ${metadata.subflow_name}`, type: 'success' }); items.push({ label: `子流程: ${metadata.subflow_name}`, type: 'success' });
} }
if (metadata.model || metadata.model_id) {
items.push({ label: `模型: ${metadata.model || metadata.model_id}`, type: 'info' });
}
if (log.tokens_used) { if (log.tokens_used) {
items.push({ label: `Token ${log.tokens_used}`, type: 'info' }); items.push({ label: `Token ${log.tokens_used}`, type: 'info' });
} }
if (communication.channel) {
items.push({ label: getChannelLabel(communication.channel), type: 'info' });
}
if (log.status === 'waiting') { if (log.status === 'waiting') {
items.push({ label: '等待用户输入', type: 'warning' }); items.push({ label: '等待用户输入', type: 'warning' });
} }
@@ -51,12 +71,63 @@ function getMetaItems(log: ExecutionLogEntry) {
return items; return items;
} }
function getChannelLabel(channel: string) {
const labels: Record<string, string> = {
human_input: '人机协作',
parallel_branch: '并行分支',
subflow: '子流程',
workflow_edge: '流程传递',
};
return labels[channel] || channel;
}
function getEventTypeLabel(type?: string) {
const labels: Record<string, string> = {
answer: '回复',
complete: '完成',
error: '错误',
llm_chunk: 'LLM 输出',
message: '消息',
node_complete: '节点完成',
node_event: '节点事件',
node_start: '节点开始',
parallel_complete: '并行完成',
parallel_start: '并行开始',
start: '开始',
waiting_input: '等待输入',
};
return labels[type || ''] || type || '事件';
}
function getEventSummary(event: WorkflowRunEvent) {
if (event.content) return event.content;
if (event.message) return event.message;
if (event.error_message) return event.error_message;
if (event.error) return event.error;
if (event.event) return getEventSummary(event.event);
return stringifyBrief(event, 140);
}
function getLogEvents(log: ExecutionLogEntry): WorkflowRunEvent[] {
const directEvents = Array.isArray(log.events) ? log.events : [];
const metadataEvents = Array.isArray(log.metadata?.events)
? log.metadata.events
: [];
return [...directEvents, ...metadataEvents].slice(0, 4);
}
function getLogDetail(log: ExecutionLogEntry) { function getLogDetail(log: ExecutionLogEntry) {
const communication = getCommunication(log);
if (log.error) return log.error; if (log.error) return log.error;
if (log.status === 'waiting') { if (log.status === 'waiting') {
return stringifyBrief(log.metadata?.waiting_config || log.metadata?.config || log.output); return stringifyBrief(log.metadata?.waiting_config || log.metadata?.config || log.output);
} }
return stringifyBrief(log.output || log.metadata?.branch_results || log.metadata?.result); return stringifyBrief(
log.output
|| log.metadata?.branch_results
|| log.metadata?.result
|| communication.message,
);
} }
</script> </script>
@@ -104,6 +175,26 @@ function getLogDetail(log: ExecutionLogEntry) {
> >
{{ getLogDetail(log) }} {{ getLogDetail(log) }}
</div> </div>
<div
v-if="getLogEvents(log).length"
class="bg-muted/40 border-border mt-2 rounded border px-2 py-1.5"
>
<div class="text-muted-foreground mb-1 text-xs font-medium">
运行事件
</div>
<div
v-for="(event, eventIndex) in getLogEvents(log)"
:key="`${log.node_id}-event-${eventIndex}`"
class="text-muted-foreground flex gap-2 text-xs"
>
<span class="shrink-0 font-medium">
{{ getEventTypeLabel(event.type || event.event?.type) }}
</span>
<span class="line-clamp-2 min-w-0 whitespace-pre-wrap">
{{ getEventSummary(event) }}
</span>
</div>
</div>
</button> </button>
</ElTimelineItem> </ElTimelineItem>
</ElTimeline> </ElTimeline>
@@ -54,79 +54,12 @@ const components: Record<string, any> = {
), ),
// 循环节点 // 循环节点
loop: defineAsyncComponent(() => import('../panels/LoopPanel.vue')), loop: defineAsyncComponent(() => import('../panels/LoopPanel.vue')),
// 表单节点
form_basic_info: defineAsyncComponent(
() => import('../panels/FormBasicInfoPanel.vue'),
),
form_database_design: defineAsyncComponent(
() => import('../panels/FormDatabaseDesignPanel.vue'),
),
form_database_create: defineAsyncComponent(
() => import('../panels/FormDatabaseCreatePanel.vue'),
),
form_ui_design: defineAsyncComponent(
() => import('../panels/FormUIDesignPanel.vue'),
),
form_list_design: defineAsyncComponent(
() => import('../panels/FormListDesignPanel.vue'),
),
form_create: defineAsyncComponent(
() => import('../panels/FormCreatePanel.vue'),
),
form_publish: defineAsyncComponent(
() => import('../panels/FormPublishPanel.vue'),
),
// 应用节点
app_create: defineAsyncComponent(
() => import('../panels/AppCreatePanel.vue'),
),
app_design: defineAsyncComponent(
() => import('../panels/AppDesignPanel.vue'),
),
app_settings: defineAsyncComponent(
() => import('../panels/AppSettingsPanel.vue'),
),
app_update: defineAsyncComponent(
() => import('../panels/AppUpdatePanel.vue'),
),
// 仪表盘节点
dashboard_basic_info: defineAsyncComponent(
() => import('../panels/DashboardBasicInfoPanel.vue'),
),
dashboard_design: defineAsyncComponent(
() => import('../panels/DashboardDesignPanel.vue'),
),
dashboard_create: defineAsyncComponent(
() => import('../panels/DashboardCreatePanel.vue'),
),
dashboard_publish: defineAsyncComponent(
() => import('../panels/DashboardPublishPanel.vue'),
),
// 系统总结节点 // 系统总结节点
system_summary: defineAsyncComponent( system_summary: defineAsyncComponent(
() => import('../panels/SystemSummaryPanel.vue'), () => import('../panels/SystemSummaryPanel.vue'),
), ),
// 子流程节点 // 子流程节点
subflow: defineAsyncComponent(() => import('../panels/SubflowPanel.vue')), subflow: defineAsyncComponent(() => import('../panels/SubflowPanel.vue')),
// 表单数据节点
form_data_create: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_read: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_update: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_delete: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_data_list: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
form_schema_to_llm: defineAsyncComponent(
() => import('../panels/FormDataPanel.vue'),
),
// Text-to-SQL 节点 // Text-to-SQL 节点
text_to_sql: defineAsyncComponent( text_to_sql: defineAsyncComponent(
() => import('../panels/TextToSqlPanel.vue'), () => import('../panels/TextToSqlPanel.vue'),
@@ -27,14 +27,11 @@ import {
Database, Database,
DatabaseBackup, DatabaseBackup,
DatabaseZap, DatabaseZap,
FilePlus,
FileText,
Fullscreen, Fullscreen,
GitBranch, GitBranch,
GitFork, GitFork,
Globe, Globe,
HelpCircle, HelpCircle,
LayoutDashboard,
LayoutTemplate, LayoutTemplate,
ListChecks, ListChecks,
Map, Map,
@@ -45,13 +42,9 @@ import {
Plus, Plus,
Redo2, Redo2,
RefreshCw, RefreshCw,
Save,
Search, Search,
Send,
Settings,
Snowflake, Snowflake,
Square, Square,
TableProperties,
Trash2, Trash2,
Undo2, Undo2,
Upload, Upload,
@@ -84,32 +77,16 @@ import ContextMenu from './components/ContextMenu.vue';
import Panel from './components/Panel.vue'; import Panel from './components/Panel.vue';
import VersionHistoryPanel from './components/VersionHistoryPanel.vue'; import VersionHistoryPanel from './components/VersionHistoryPanel.vue';
import AddButtonEdge from './edges/AddButtonEdge.vue'; import AddButtonEdge from './edges/AddButtonEdge.vue';
import AppCreateNode from './nodes/AppCreateNode.vue';
import AppDesignNode from './nodes/AppDesignNode.vue';
import AppSettingsNode from './nodes/AppSettingsNode.vue';
import AppUpdateNode from './nodes/AppUpdateNode.vue';
import ChoiceNode from './nodes/ChoiceNode.vue'; import ChoiceNode from './nodes/ChoiceNode.vue';
import CodeNode from './nodes/CodeNode.vue'; import CodeNode from './nodes/CodeNode.vue';
import ConditionNode from './nodes/ConditionNode.vue'; import ConditionNode from './nodes/ConditionNode.vue';
import ConfirmNode from './nodes/ConfirmNode.vue'; import ConfirmNode from './nodes/ConfirmNode.vue';
import DashboardBasicInfoNode from './nodes/DashboardBasicInfoNode.vue';
import DashboardCreateNode from './nodes/DashboardCreateNode.vue';
import DashboardDesignNode from './nodes/DashboardDesignNode.vue';
import DashboardPublishNode from './nodes/DashboardPublishNode.vue';
import DbDeleteNode from './nodes/DbDeleteNode.vue'; import DbDeleteNode from './nodes/DbDeleteNode.vue';
import DbInsertNode from './nodes/DbInsertNode.vue'; import DbInsertNode from './nodes/DbInsertNode.vue';
import DbQueryNode from './nodes/DbQueryNode.vue'; import DbQueryNode from './nodes/DbQueryNode.vue';
import DbSqlNode from './nodes/DbSqlNode.vue'; import DbSqlNode from './nodes/DbSqlNode.vue';
import DbUpdateNode from './nodes/DbUpdateNode.vue'; import DbUpdateNode from './nodes/DbUpdateNode.vue';
import EndNode from './nodes/EndNode.vue'; import EndNode from './nodes/EndNode.vue';
import FormBasicInfoNode from './nodes/FormBasicInfoNode.vue';
import FormCreateNode from './nodes/FormCreateNode.vue';
import FormDatabaseCreateNode from './nodes/FormDatabaseCreateNode.vue';
import FormDatabaseDesignNode from './nodes/FormDatabaseDesignNode.vue';
import FormDataNode from './nodes/FormDataNode.vue';
import FormListDesignNode from './nodes/FormListDesignNode.vue';
import FormPublishNode from './nodes/FormPublishNode.vue';
import FormUIDesignNode from './nodes/FormUIDesignNode.vue';
import HttpNode from './nodes/HttpNode.vue'; import HttpNode from './nodes/HttpNode.vue';
import IntentNode from './nodes/IntentNode.vue'; import IntentNode from './nodes/IntentNode.vue';
import KnowledgeRetrievalNode from './nodes/KnowledgeRetrievalNode.vue'; import KnowledgeRetrievalNode from './nodes/KnowledgeRetrievalNode.vue';
@@ -184,35 +161,10 @@ const nodeTypes = {
snowflake_cortex_analyst: markRaw(SnowflakeCortexAnalystNode), snowflake_cortex_analyst: markRaw(SnowflakeCortexAnalystNode),
// 循环节点 // 循环节点
loop: markRaw(LoopNode), loop: markRaw(LoopNode),
// 表单节点
form_basic_info: markRaw(FormBasicInfoNode),
form_database_design: markRaw(FormDatabaseDesignNode),
form_database_create: markRaw(FormDatabaseCreateNode),
form_ui_design: markRaw(FormUIDesignNode),
form_list_design: markRaw(FormListDesignNode),
form_create: markRaw(FormCreateNode),
form_publish: markRaw(FormPublishNode),
// 应用管理节点
app_create: markRaw(AppCreateNode),
app_design: markRaw(AppDesignNode),
app_settings: markRaw(AppSettingsNode),
app_update: markRaw(AppUpdateNode),
// 仪表盘节点
dashboard_basic_info: markRaw(DashboardBasicInfoNode),
dashboard_design: markRaw(DashboardDesignNode),
dashboard_create: markRaw(DashboardCreateNode),
dashboard_publish: markRaw(DashboardPublishNode),
// 系统总结节点 // 系统总结节点
system_summary: markRaw(SystemSummaryNode), system_summary: markRaw(SystemSummaryNode),
// 子流程节点 // 子流程节点
subflow: markRaw(SubflowNode), subflow: markRaw(SubflowNode),
// 表单数据节点
form_data_create: markRaw(FormDataNode),
form_data_read: markRaw(FormDataNode),
form_data_update: markRaw(FormDataNode),
form_data_delete: markRaw(FormDataNode),
form_data_list: markRaw(FormDataNode),
form_schema_to_llm: markRaw(FormDataNode),
// Text-to-SQL 节点 // Text-to-SQL 节点
text_to_sql: markRaw(TextToSqlNode), text_to_sql: markRaw(TextToSqlNode),
// 知识库节点 // 知识库节点
@@ -390,94 +342,6 @@ const nodeCategories = computed(() => [
}, },
], ],
}, },
{
name: $t('ai-platform.workflow.editor.categories.formDesign'),
nodes: [
{
type: 'form_basic_info',
label: getNodeLabel('form_basic_info'),
icon: 'FileText',
color: 'purple',
},
{
type: 'form_database_design',
label: getNodeLabel('form_database_design'),
icon: 'Database',
color: 'purple',
},
{
type: 'form_database_create',
label: getNodeLabel('form_database_create'),
icon: 'DatabaseZap',
color: 'purple',
},
{
type: 'form_ui_design',
label: getNodeLabel('form_ui_design'),
icon: 'LayoutTemplate',
color: 'purple',
},
{
type: 'form_list_design',
label: getNodeLabel('form_list_design'),
icon: 'TableProperties',
color: 'purple',
},
{
type: 'form_create',
label: getNodeLabel('form_create'),
icon: 'FilePlus',
color: 'purple',
},
{
type: 'form_publish',
label: getNodeLabel('form_publish'),
icon: 'Send',
color: 'purple',
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.formData'),
nodes: [
{
type: 'form_data_create',
label: getNodeLabel('form_data_create'),
icon: 'FilePlus',
color: 'green',
},
{
type: 'form_data_read',
label: getNodeLabel('form_data_read'),
icon: 'FileText',
color: 'blue',
},
{
type: 'form_data_update',
label: getNodeLabel('form_data_update'),
icon: 'FileText',
color: 'orange',
},
{
type: 'form_data_delete',
label: getNodeLabel('form_data_delete'),
icon: 'Trash2',
color: 'red',
},
{
type: 'form_data_list',
label: getNodeLabel('form_data_list'),
icon: 'Search',
color: 'blue',
},
{
type: 'form_schema_to_llm',
label: getNodeLabel('form_schema_to_llm'),
icon: 'Code',
color: 'purple',
},
],
},
{ {
name: $t('ai-platform.workflow.editor.categories.dialog'), name: $t('ai-platform.workflow.editor.categories.dialog'),
nodes: [ nodes: [
@@ -587,6 +451,12 @@ const nodeCategories = computed(() => [
icon: 'Workflow', icon: 'Workflow',
color: 'purple', color: 'purple',
}, },
{
type: 'system_summary',
label: getNodeLabel('system_summary'),
icon: 'ClipboardCheck',
color: 'purple',
},
{ {
type: 'end', type: 'end',
label: getNodeLabel('end'), label: getNodeLabel('end'),
@@ -612,72 +482,6 @@ const nodeCategories = computed(() => [
}, },
], ],
}, },
{
name: $t('ai-platform.workflow.editor.categories.appManage'),
nodes: [
{
type: 'app_create',
label: getNodeLabel('app_create'),
icon: 'LayoutTemplate',
color: 'indigo',
},
{
type: 'app_design',
label: getNodeLabel('app_design'),
icon: 'LayoutDashboard',
color: 'indigo',
},
{
type: 'app_settings',
label: getNodeLabel('app_settings'),
icon: 'Settings',
color: 'indigo',
},
{
type: 'app_update',
label: getNodeLabel('app_update'),
icon: 'Save',
color: 'indigo',
},
],
},
{
name: $t('ai-platform.workflow.editor.categories.dashboard'),
icon: 'LayoutDashboard',
color: 'cyan',
nodes: [
{
type: 'dashboard_basic_info',
label: getNodeLabel('dashboard_basic_info'),
icon: 'FileText',
color: 'cyan',
},
{
type: 'dashboard_design',
label: getNodeLabel('dashboard_design'),
icon: 'LayoutDashboard',
color: 'cyan',
},
{
type: 'dashboard_create',
label: getNodeLabel('dashboard_create'),
icon: 'FilePlus',
color: 'cyan',
},
{
type: 'dashboard_publish',
label: getNodeLabel('dashboard_publish'),
icon: 'Upload',
color: 'cyan',
},
{
type: 'system_summary',
label: getNodeLabel('system_summary'),
icon: 'ClipboardCheck',
color: 'cyan',
},
],
},
{ {
name: $t('ai-platform.workflow.editor.categories.knowledge'), name: $t('ai-platform.workflow.editor.categories.knowledge'),
nodes: [ nodes: [
@@ -720,17 +524,6 @@ const iconComponents: Record<string, any> = {
// 循环节点图标 // 循环节点图标
RefreshCw, RefreshCw,
CornerDownLeft, CornerDownLeft,
// 表单节点图标
FileText,
TableProperties,
Send,
// 应用节点图标
LayoutDashboard,
Settings,
Save,
// 仪表盘节点图标
FilePlus,
Upload,
// 系统总结节点图标 // 系统总结节点图标
ClipboardCheck, ClipboardCheck,
// 子流程节点图标 // 子流程节点图标
@@ -741,21 +534,7 @@ const iconComponents: Record<string, any> = {
// 过滤后的节点分类 // 过滤后的节点分类
const filteredNodeCategories = computed(() => { const filteredNodeCategories = computed(() => {
// 首先根据工作流类型过滤分类 const categories = nodeCategories.value;
let categories = nodeCategories.value;
// 如果是通用类型,排除表单设计、应用管理、仪表盘分组
if (workflow.value?.workflow_type === 'general') {
const excludeNames = new Set([
$t('ai-platform.workflow.editor.categories.appManage'),
$t('ai-platform.workflow.editor.categories.dashboard'),
$t('ai-platform.workflow.editor.categories.formDesign'),
]);
categories = nodeCategories.value.filter(
(category: { name: string; nodes: any[] }) =>
!excludeNames.has(category.name),
);
}
// 再根据搜索关键词过滤 // 再根据搜索关键词过滤
if (!nodeSearchQuery.value.trim()) { if (!nodeSearchQuery.value.trim()) {
@@ -1,32 +1,16 @@
import { markRaw } from 'vue'; import { markRaw } from 'vue';
import AddButtonEdge from '../editor/edges/AddButtonEdge.vue'; import AddButtonEdge from '../editor/edges/AddButtonEdge.vue';
import AppCreateNode from '../editor/nodes/AppCreateNode.vue';
import AppDesignNode from '../editor/nodes/AppDesignNode.vue';
import AppSettingsNode from '../editor/nodes/AppSettingsNode.vue';
import AppUpdateNode from '../editor/nodes/AppUpdateNode.vue';
import ChoiceNode from '../editor/nodes/ChoiceNode.vue'; import ChoiceNode from '../editor/nodes/ChoiceNode.vue';
import CodeNode from '../editor/nodes/CodeNode.vue'; import CodeNode from '../editor/nodes/CodeNode.vue';
import ConditionNode from '../editor/nodes/ConditionNode.vue'; import ConditionNode from '../editor/nodes/ConditionNode.vue';
import ConfirmNode from '../editor/nodes/ConfirmNode.vue'; import ConfirmNode from '../editor/nodes/ConfirmNode.vue';
import DashboardBasicInfoNode from '../editor/nodes/DashboardBasicInfoNode.vue';
import DashboardCreateNode from '../editor/nodes/DashboardCreateNode.vue';
import DashboardDesignNode from '../editor/nodes/DashboardDesignNode.vue';
import DashboardPublishNode from '../editor/nodes/DashboardPublishNode.vue';
import DbDeleteNode from '../editor/nodes/DbDeleteNode.vue'; import DbDeleteNode from '../editor/nodes/DbDeleteNode.vue';
import DbInsertNode from '../editor/nodes/DbInsertNode.vue'; import DbInsertNode from '../editor/nodes/DbInsertNode.vue';
import DbQueryNode from '../editor/nodes/DbQueryNode.vue'; import DbQueryNode from '../editor/nodes/DbQueryNode.vue';
import DbSqlNode from '../editor/nodes/DbSqlNode.vue'; import DbSqlNode from '../editor/nodes/DbSqlNode.vue';
import DbUpdateNode from '../editor/nodes/DbUpdateNode.vue'; import DbUpdateNode from '../editor/nodes/DbUpdateNode.vue';
import EndNode from '../editor/nodes/EndNode.vue'; import EndNode from '../editor/nodes/EndNode.vue';
import FormBasicInfoNode from '../editor/nodes/FormBasicInfoNode.vue';
import FormCreateNode from '../editor/nodes/FormCreateNode.vue';
import FormDatabaseCreateNode from '../editor/nodes/FormDatabaseCreateNode.vue';
import FormDatabaseDesignNode from '../editor/nodes/FormDatabaseDesignNode.vue';
import FormDataNode from '../editor/nodes/FormDataNode.vue';
import FormListDesignNode from '../editor/nodes/FormListDesignNode.vue';
import FormPublishNode from '../editor/nodes/FormPublishNode.vue';
import FormUIDesignNode from '../editor/nodes/FormUIDesignNode.vue';
import HttpNode from '../editor/nodes/HttpNode.vue'; import HttpNode from '../editor/nodes/HttpNode.vue';
import IntentNode from '../editor/nodes/IntentNode.vue'; import IntentNode from '../editor/nodes/IntentNode.vue';
import KnowledgeRetrievalNode from '../editor/nodes/KnowledgeRetrievalNode.vue'; import KnowledgeRetrievalNode from '../editor/nodes/KnowledgeRetrievalNode.vue';
@@ -67,29 +51,8 @@ export const workflowNodeTypes = {
snowflake_cortex_llm: markRaw(SnowflakeCortexLLMNode), snowflake_cortex_llm: markRaw(SnowflakeCortexLLMNode),
snowflake_cortex_analyst: markRaw(SnowflakeCortexAnalystNode), snowflake_cortex_analyst: markRaw(SnowflakeCortexAnalystNode),
loop: markRaw(LoopNode), loop: markRaw(LoopNode),
form_basic_info: markRaw(FormBasicInfoNode),
form_database_design: markRaw(FormDatabaseDesignNode),
form_database_create: markRaw(FormDatabaseCreateNode),
form_ui_design: markRaw(FormUIDesignNode),
form_list_design: markRaw(FormListDesignNode),
form_create: markRaw(FormCreateNode),
form_publish: markRaw(FormPublishNode),
app_create: markRaw(AppCreateNode),
app_design: markRaw(AppDesignNode),
app_settings: markRaw(AppSettingsNode),
app_update: markRaw(AppUpdateNode),
dashboard_basic_info: markRaw(DashboardBasicInfoNode),
dashboard_design: markRaw(DashboardDesignNode),
dashboard_create: markRaw(DashboardCreateNode),
dashboard_publish: markRaw(DashboardPublishNode),
system_summary: markRaw(SystemSummaryNode), system_summary: markRaw(SystemSummaryNode),
subflow: markRaw(SubflowNode), subflow: markRaw(SubflowNode),
form_data_create: markRaw(FormDataNode),
form_data_read: markRaw(FormDataNode),
form_data_update: markRaw(FormDataNode),
form_data_delete: markRaw(FormDataNode),
form_data_list: markRaw(FormDataNode),
form_schema_to_llm: markRaw(FormDataNode),
text_to_sql: markRaw(TextToSqlNode), text_to_sql: markRaw(TextToSqlNode),
knowledge_retrieval: markRaw(KnowledgeRetrievalNode), knowledge_retrieval: markRaw(KnowledgeRetrievalNode),
}; };