diff --git a/apps/jingrow/frontend/src/core/features/flows/AIAgentFlowBuilder.vue b/apps/jingrow/frontend/src/core/features/flows/AIAgentFlowBuilder.vue index fc97f16..6fb5e13 100644 --- a/apps/jingrow/frontend/src/core/features/flows/AIAgentFlowBuilder.vue +++ b/apps/jingrow/frontend/src/core/features/flows/AIAgentFlowBuilder.vue @@ -9,7 +9,7 @@ import { useFlowStore } from './store/flowStore.js'; import { flowExecutor } from './executors/flowExecutor.js'; import NodePalette from './components/Sidebar/NodePalette.vue'; import ExecutionResults from './components/ExecutionResults.vue'; -import { getNodeComponents, getNodeMetadataByType } from './utils/nodeMetadata.js'; +import { getNodeComponents, getNodeMetadataByType, preloadNodeMetadata } from './utils/nodeMetadata.js'; import { t } from '@/shared/i18n' // Props @@ -136,8 +136,24 @@ const onPaneReady = () => { } }; -// 自动化获取所有节点组件,使用markRaw避免响应式 -const nodeTypes = markRaw(getNodeComponents()); +// 节点组件映射(响应式) +const nodeTypes = ref({}); +const nodeTypesLoaded = ref(false); + +// 预加载节点元数据 +onMounted(async () => { + await preloadNodeMetadata(); + // 更新节点组件映射 + const components = getNodeComponents(); + nodeTypes.value = markRaw(components); + nodeTypesLoaded.value = true; + + // 节点元数据加载完成后,再初始化流程数据 + nextTick(async () => { + initFlowData(); + store.saveToHistory(); + }); +}); // 获取节点颜色函数 const getNodeColor = (nodeType) => { @@ -325,11 +341,6 @@ const onKeyDown = (event) => { // 生命周期 onMounted(() => { - nextTick(async () => { - // 初始化流程数据 - initFlowData(); - store.saveToHistory(); - }); // 暴露 teleport 目标到全局,供节点组件使用 window.nodePropertyTeleportTarget = { @@ -617,6 +628,7 @@ const showBubbleTip = (msg, type = '') => { { + + +
+
+ +

正在加载节点类型...

+
+
@@ -1219,4 +1239,29 @@ const showBubbleTip = (msg, type = '') => { .ai-agent-flow-builder.fullscreen .bubble-tip-global { z-index: 100000; } + +/* 加载提示样式 */ +.loading-container { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + min-height: 400px; +} + +.loading-spinner { + text-align: center; + color: #6b7280; +} + +.loading-spinner i { + font-size: 24px; + margin-bottom: 12px; + display: block; +} + +.loading-spinner p { + margin: 0; + font-size: 14px; +} \ No newline at end of file diff --git a/apps/jingrow/frontend/src/core/features/flows/utils/nodeMetadata.js b/apps/jingrow/frontend/src/core/features/flows/utils/nodeMetadata.js index 2e123b4..bddcbf4 100644 --- a/apps/jingrow/frontend/src/core/features/flows/utils/nodeMetadata.js +++ b/apps/jingrow/frontend/src/core/features/flows/utils/nodeMetadata.js @@ -16,29 +16,27 @@ function resolveComponent(componentType) { return COMPONENT_MAP[componentType] || GenericNode } -// 发现所有节点的元数据(从本地JSON文件) -function discoverNodeMetadata() { - // 使用 import.meta.glob 获取所有节点JSON文件 - // 注意:Vite 要求以 './' 或 '/' 开头,别名在 glob 中不可用 - // 从当前文件路径 ../../../../../../../ 指向到项目根,再进入 apps/jingrow/nodes 目录(core 比原 features 深一层) - const modulesNew = import.meta.glob('../../../../../../../../apps/jingrow/jingrow/ai/pagetype/local_ai_agent/nodes/**/*.json', { eager: true }) +// 发现所有节点的元数据(从API获取) +async function discoverNodeMetadata() { + try { + const response = await fetch('/jingrow/node-definitions/metadata'); - const metadataMap = {} - - Object.keys(modulesNew).forEach(path => { - try { - const module = modulesNew[path] - const data = module.default || module - - // 检查是否有 metadata 字段 - if (data && typeof data === 'object' && data.metadata && data.metadata.type) { - metadataMap[data.metadata.type] = data.metadata - } - } catch (error) { - // 静默失败,避免污染控制台 + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); } - }) - return metadataMap + + const result = await response.json(); + + if (result.success && result.data) { + return result.data; + } else { + console.warn('获取节点元数据失败:', result); + return {}; + } + } catch (error) { + console.error('获取节点元数据失败:', error); + return {}; + } } // 自动生成节点分组 @@ -60,36 +58,65 @@ function generateNodeGroups(metadataMap) { return Object.values(groupMap) } -// 节点元数据映射(同步初始化) -const NODE_METADATA_MAP = {} -const LOCAL_METADATA = discoverNodeMetadata() +// 节点元数据映射(异步初始化) +let NODE_METADATA_MAP = {} +let NODE_GROUPS = [] +let isInitialized = false -// 构建最终的元数据映射 -Object.keys(LOCAL_METADATA).forEach(type => { - const metadata = LOCAL_METADATA[type] - NODE_METADATA_MAP[type] = { - ...metadata, - component: resolveComponent(metadata.component_type) +// 初始化节点元数据 +async function initializeNodeMetadata() { + if (isInitialized) return + + try { + const LOCAL_METADATA = await discoverNodeMetadata() + + // 构建最终的元数据映射 + NODE_METADATA_MAP = {} + Object.keys(LOCAL_METADATA).forEach(type => { + const metadata = LOCAL_METADATA[type] + NODE_METADATA_MAP[type] = { + ...metadata, + component: resolveComponent(metadata.component) + } + }) + + // 自动生成节点分组 + NODE_GROUPS = generateNodeGroups(NODE_METADATA_MAP) + isInitialized = true + } catch (error) { + console.error('初始化节点元数据失败:', error) + NODE_METADATA_MAP = {} + NODE_GROUPS = [] } -}) +} -// 自动生成节点分组 -const NODE_GROUPS = generateNodeGroups(NODE_METADATA_MAP) - -// 导出函数 +// 导出函数(保持同步接口) export function getNodeMetadataByType(type) { + if (!isInitialized) { + // 如果还没初始化,返回null,组件会处理这种情况 + return null + } return NODE_METADATA_MAP[type] } export function getAllNodeTypes() { + if (!isInitialized) { + return [] + } return Object.values(NODE_METADATA_MAP) } export function getNodeGroups() { + if (!isInitialized) { + return [] + } return NODE_GROUPS } export function getNodeComponents() { + if (!isInitialized) { + return {} + } const components = {} Object.keys(NODE_METADATA_MAP).forEach(type => { if (NODE_METADATA_MAP[type].component) { @@ -99,5 +126,10 @@ export function getNodeComponents() { return components } +// 预加载函数 +export async function preloadNodeMetadata() { + await initializeNodeMetadata() +} + // 导出常量 export { NODE_METADATA_MAP, NODE_GROUPS } \ No newline at end of file diff --git a/apps/jingrow/frontend/src/core/features/flows/utils/schemaLoader.js b/apps/jingrow/frontend/src/core/features/flows/utils/schemaLoader.js index 2b1dfc0..9001e1b 100644 --- a/apps/jingrow/frontend/src/core/features/flows/utils/schemaLoader.js +++ b/apps/jingrow/frontend/src/core/features/flows/utils/schemaLoader.js @@ -4,29 +4,20 @@ * @returns {Promise} Schema配置对象 */ -const LOCAL_NODE_SCHEMAS_NEW = import.meta.glob('../../../../../../../../apps/jingrow/jingrow/ai/pagetype/local_ai_agent/nodes/*/*.json', { eager: true }); - -function loadLocalSchemaByConvention(nodeType) { - try { - const suffix = `/${nodeType}/${nodeType}.json`; - for (const path of Object.keys(LOCAL_NODE_SCHEMAS_NEW)) { - if (path.endsWith(suffix)) { - const mod = LOCAL_NODE_SCHEMAS_NEW[path]; - const data = mod?.default || mod; - if (data && data.properties) return data; - } - } - } catch (_) {} - return null; -} - - export async function loadNodeSchema(nodeType) { try { - const localByPath = loadLocalSchemaByConvention(nodeType); - return localByPath || {}; + const response = await fetch(`/jingrow/node-definitions/schema/${nodeType}`); + const result = await response.json(); + + if (result.success && result.data) { + return result.data; + } else { + console.warn(`获取节点Schema失败: ${nodeType}`, result); + return {}; + } } catch (error) { - throw error; + console.error(`Schema加载失败: ${nodeType}`, error); + return {}; } } diff --git a/apps/jingrow/jingrow/api/node_definitions.py b/apps/jingrow/jingrow/api/node_definitions.py index 23f097d..1323d95 100644 --- a/apps/jingrow/jingrow/api/node_definitions.py +++ b/apps/jingrow/jingrow/api/node_definitions.py @@ -24,7 +24,7 @@ async def export_node_definition(payload: Dict[str, Any]): export_data = {"metadata": metadata, **(schema or {})} current_file = Path(__file__).resolve() - jingrow_root = current_file.parents[2] + jingrow_root = current_file.parents[1] # 修正路径层级 new_root = jingrow_root / "ai" / "pagetype" / "local_ai_agent" / "nodes" target = new_root / node_type / f"{node_type}.json" atomic_write_json(target, export_data) @@ -40,7 +40,7 @@ async def import_local_node_definitions(): """ try: current_file = Path(__file__).resolve() - jingrow_root = current_file.parents[2] + jingrow_root = current_file.parents[1] # 修正路径层级 nodes_root = jingrow_root / "ai" / "pagetype" / "local_ai_agent" / "nodes" if not nodes_root.exists(): return {"success": True, "matched": 0, "imported": 0, "skipped_existing": 0} @@ -110,3 +110,80 @@ async def import_local_node_definitions(): } except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/jingrow/node-definitions/metadata") +async def get_all_node_metadata(): + """ + 获取所有节点的元数据,用于流程编排界面 + """ + try: + current_file = Path(__file__).resolve() + jingrow_root = current_file.parents[1] # 修正路径层级 + nodes_root = jingrow_root / "ai" / "pagetype" / "local_ai_agent" / "nodes" + + if not nodes_root.exists(): + return {"success": True, "data": {}} + + metadata_map = {} + + for node_dir in nodes_root.iterdir(): + if not node_dir.is_dir(): + continue + json_file = node_dir / f"{node_dir.name}.json" + if not json_file.exists(): + continue + + try: + with open(json_file, "r", encoding="utf-8") as f: + data = json.load(f) + + metadata = data.get("metadata") or {} + node_type = metadata.get("type") + if not node_type: + continue + + metadata_map[node_type] = { + "type": node_type, + "label": metadata.get("label") or node_type, + "icon": metadata.get("icon") or "fa-cube", + "color": metadata.get("color") or "#6b7280", + "description": metadata.get("description") or "", + "group": metadata.get("group") or "其他", + "component": metadata.get("component_type") or "GenericNode" + } + except Exception: + continue + + return {"success": True, "data": metadata_map} + + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/jingrow/node-definitions/schema/{node_type}") +async def get_node_schema(node_type: str): + """ + 获取指定节点类型的Schema配置 + """ + try: + current_file = Path(__file__).resolve() + jingrow_root = current_file.parents[1] + nodes_root = jingrow_root / "ai" / "pagetype" / "local_ai_agent" / "nodes" + json_file = nodes_root / node_type / f"{node_type}.json" + + if not json_file.exists(): + raise HTTPException(status_code=404, detail=f"节点类型 {node_type} 不存在") + + with open(json_file, "r", encoding="utf-8") as f: + data = json.load(f) + + schema = dict(data) + schema.pop("metadata", None) + + return {"success": True, "data": schema} + + except FileNotFoundError: + raise HTTPException(status_code=404, detail=f"节点类型 {node_type} 不存在") + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/test_api.py b/test_api.py deleted file mode 100644 index 86b2b96..0000000 --- a/test_api.py +++ /dev/null @@ -1,221 +0,0 @@ -#!/usr/bin/env python3 -""" -测试本地版REST API和钩子功能 -""" - -import requests -import json -import time - -# 配置 -BASE_URL = "http://localhost:9001" -TEST_PAGETYPE = "Test Page" -TEST_NAME = "TEST-001" - -def test_create_record(): - """测试创建记录""" - print("=== 测试创建记录 ===") - - data = { - "name": TEST_NAME, - "title": "测试记录", - "status": "Active" - } - - try: - response = requests.post( - f"{BASE_URL}/api/data/{TEST_PAGETYPE}", - json=data, - headers={"Content-Type": "application/json"} - ) - - print(f"状态码: {response.status_code}") - print(f"响应: {response.json()}") - - if response.status_code == 200: - print("✅ 创建记录成功") - return True - else: - print("❌ 创建记录失败") - return False - - except Exception as e: - print(f"❌ 创建记录异常: {e}") - return False - -def test_update_record(): - """测试更新记录""" - print("\n=== 测试更新记录 ===") - - data = { - "title": "更新后的标题", - "status": "Inactive" - } - - try: - response = requests.put( - f"{BASE_URL}/api/data/{TEST_PAGETYPE}/{TEST_NAME}", - json=data, - headers={"Content-Type": "application/json"} - ) - - print(f"状态码: {response.status_code}") - print(f"响应: {response.json()}") - - if response.status_code == 200: - print("✅ 更新记录成功") - return True - else: - print("❌ 更新记录失败") - return False - - except Exception as e: - print(f"❌ 更新记录异常: {e}") - return False - -def test_get_record(): - """测试获取记录""" - print("\n=== 测试获取记录 ===") - - try: - response = requests.get( - f"{BASE_URL}/api/data/{TEST_PAGETYPE}/{TEST_NAME}" - ) - - print(f"状态码: {response.status_code}") - print(f"响应: {response.json()}") - - if response.status_code == 200: - print("✅ 获取记录成功") - return True - else: - print("❌ 获取记录失败") - return False - - except Exception as e: - print(f"❌ 获取记录异常: {e}") - return False - -def test_get_records(): - """测试获取记录列表""" - print("\n=== 测试获取记录列表 ===") - - try: - response = requests.get( - f"{BASE_URL}/api/data/{TEST_PAGETYPE}", - params={ - "limit_page_length": 10 - } - ) - - print(f"状态码: {response.status_code}") - print(f"响应: {response.json()}") - - if response.status_code == 200: - print("✅ 获取记录列表成功") - return True - else: - print("❌ 获取记录列表失败") - return False - - except Exception as e: - print(f"❌ 获取记录列表异常: {e}") - return False - -def test_delete_record(): - """测试删除记录""" - print("\n=== 测试删除记录 ===") - - try: - response = requests.delete( - f"{BASE_URL}/api/data/{TEST_PAGETYPE}/{TEST_NAME}" - ) - - print(f"状态码: {response.status_code}") - print(f"响应: {response.json()}") - - if response.status_code == 200: - print("✅ 删除记录成功") - return True - else: - print("❌ 删除记录失败") - return False - - except Exception as e: - print(f"❌ 删除记录异常: {e}") - return False - -def test_hook_execution(): - """测试钩子执行""" - print("\n=== 测试钩子执行 ===") - - data = { - "pagetype": TEST_PAGETYPE, - "name": TEST_NAME, - "hook_name": "on_update", - "data": {"test": "hook data"} - } - - try: - response = requests.post( - f"{BASE_URL}/api/hooks/execute", - json=data, - headers={"Content-Type": "application/json"} - ) - - print(f"状态码: {response.status_code}") - print(f"响应: {response.json()}") - - if response.status_code == 200: - print("✅ 钩子执行成功") - return True - else: - print("❌ 钩子执行失败") - return False - - except Exception as e: - print(f"❌ 钩子执行异常: {e}") - return False - -def main(): - """主测试函数""" - print("开始测试本地版REST API和钩子功能...") - print(f"测试目标: {BASE_URL}") - print(f"测试PageType: {TEST_PAGETYPE}") - print(f"测试记录名: {TEST_NAME}") - - # 等待服务启动 - print("\n等待服务启动...") - time.sleep(2) - - # 执行测试 - tests = [ - test_create_record, - test_update_record, - test_get_record, - test_get_records, - test_hook_execution, - test_delete_record - ] - - passed = 0 - total = len(tests) - - for test in tests: - try: - if test(): - passed += 1 - except Exception as e: - print(f"❌ 测试异常: {e}") - - print(f"\n=== 测试结果 ===") - print(f"通过: {passed}/{total}") - print(f"成功率: {passed/total*100:.1f}%") - - if passed == total: - print("🎉 所有测试通过!") - else: - print("⚠️ 部分测试失败,请检查日志") - -if __name__ == "__main__": - main()