流程编排界面节点元数据和Schema配置改为通过api获取

This commit is contained in:
jingrow 2025-10-25 20:26:49 +08:00
parent 1d00939152
commit 24eb9f6199
5 changed files with 210 additions and 286 deletions

View File

@ -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 = '') => {
</div>
</transition>
<VueFlow
v-if="nodeTypesLoaded"
ref="vueFlowInstance"
:nodes="nodes"
:edges="edges"
@ -677,6 +689,14 @@ const showBubbleTip = (msg, type = '') => {
<!-- 顶部面板 -->
<!-- Panel position="top-center" class="status-panel" 移除 -->
</VueFlow>
<!-- 加载提示 -->
<div v-else class="loading-container">
<div class="loading-spinner">
<i class="fa fa-spinner fa-spin"></i>
<p>正在加载节点类型...</p>
</div>
</div>
</div>
<!-- 调试显示 edges nodes JSON -->
<!-- 模板数据弹窗 -->
@ -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;
}
</style>

View File

@ -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 })
const metadataMap = {}
Object.keys(modulesNew).forEach(path => {
// 发现所有节点的元数据从API获取
async function discoverNodeMetadata() {
try {
const module = modulesNew[path]
const data = module.default || module
const response = await fetch('/jingrow/node-definitions/metadata');
// 检查是否有 metadata 字段
if (data && typeof data === 'object' && data.metadata && data.metadata.type) {
metadataMap[data.metadata.type] = data.metadata
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const result = await response.json();
if (result.success && result.data) {
return result.data;
} else {
console.warn('获取节点元数据失败:', result);
return {};
}
} catch (error) {
// 静默失败,避免污染控制台
console.error('获取节点元数据失败:', error);
return {};
}
})
return metadataMap
}
// 自动生成节点分组
@ -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 => {
// 初始化节点元数据
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_type)
component: resolveComponent(metadata.component)
}
})
})
// 自动生成节点分组
const NODE_GROUPS = generateNodeGroups(NODE_METADATA_MAP)
// 自动生成节点分组
NODE_GROUPS = generateNodeGroups(NODE_METADATA_MAP)
isInitialized = true
} catch (error) {
console.error('初始化节点元数据失败:', error)
NODE_METADATA_MAP = {}
NODE_GROUPS = []
}
}
// 导出函数
// 导出函数(保持同步接口)
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 }

View File

@ -4,29 +4,20 @@
* @returns {Promise<Object>} 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 {};
}
}

View File

@ -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))

View File

@ -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()