流程编排界面节点元数据和Schema配置改为通过api获取
This commit is contained in:
parent
1d00939152
commit
24eb9f6199
@ -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>
|
||||
@ -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 }
|
||||
@ -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 {};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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))
|
||||
|
||||
221
test_api.py
221
test_api.py
@ -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()
|
||||
Loading…
x
Reference in New Issue
Block a user