注册表编辑器

This commit is contained in:
Xuwznln
2025-09-07 20:57:48 +08:00
parent c25283ae04
commit 361eae2f6d
5 changed files with 571 additions and 103 deletions

View File

@@ -7,6 +7,8 @@ API模块
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
import asyncio
import yaml
from unilabos.app.controler import devices, job_add, job_info
from unilabos.app.model import (
Resp,
@@ -19,6 +21,8 @@ from unilabos.app.model import (
JobFinishReq,
)
from unilabos.app.web.utils.host_utils import get_host_node_info
from unilabos.registry.registry import lab_registry
from unilabos.utils.type_check import NoAliasDumper
# 创建API路由器
api = APIRouter()
@@ -603,6 +607,7 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
file_size = request_data.get("file_size", 0)
registry_type = request_data.get("registry_type", "device")
class_name = request_data.get("class_name")
module_prefix = request_data.get("module_prefix", "")
async def send_log(message: str, level: str = "info"):
"""发送日志消息到客户端"""
@@ -656,7 +661,12 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
# 确定模块名
module_name = file_name.replace(".py", "").replace("-", "_").replace(" ", "_")
# 如果有 module_prefix则使用完整的模块路径
full_module_name = f"{module_prefix}.{module_name}" if module_prefix else module_name
await send_log(f"使用模块名: {module_name}")
if module_prefix:
await send_log(f"完整模块路径: {full_module_name}")
# 导入模块
try:
@@ -697,7 +707,7 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
from unilabos.utils.import_manager import get_enhanced_class_info
# 分析类信息
enhanced_info = get_enhanced_class_info(f"{module_name}:{class_name}", use_dynamic=True)
enhanced_info = get_enhanced_class_info(f"{full_module_name}:{class_name}", use_dynamic=True)
if not enhanced_info.get("dynamic_import_success", False):
await send_error("动态导入类信息失败")
@@ -705,47 +715,102 @@ async def handle_file_content_import(websocket: WebSocket, request_data: dict):
await send_log("成功分析类信息")
# 生成注册表schema
registry_schema = {
"class_name": class_name,
"module": f"{module_name}:{class_name}",
"type": "python",
"description": enhanced_info.get("class_docstring", ""),
"version": "1.0.0",
"category": [registry_type],
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
"action_value_mappings": {},
"init_param_schema": {},
"registry_type": registry_type,
"file_path": f"uploaded_file://{file_name}",
}
# 处理动作方法
for method_name, method_info in enhanced_info["action_methods"].items():
registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
"type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
"goal": {},
"feedback": {},
"result": {},
"args": method_info["args"],
"description": method_info.get("docstring", ""),
# 根据注册表类型生成不同的schema
if registry_type == "resource":
# 资源类型的简单结构
category_name = file_name.replace(".py", "") if file_name else "unknown"
registry_schema = {
"description": enhanced_info.get("class_docstring", ""),
"category": [category_name],
"class": {
"module": f"{full_module_name}:{class_name}",
"type": "python",
},
"handles": [],
"icon": "",
"init_param_schema": {},
"registry_type": "resource",
"version": "1.0.0",
"file_path": f"uploaded_file://{file_name}",
}
else:
# 设备类型的复杂结构
registry_schema = {
"description": enhanced_info.get("class_docstring", ""),
"class": {
"module": f"{full_module_name}:{class_name}",
"type": "python",
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
"action_value_mappings": {},
},
"version": "1.0.0",
"handles": [],
"init_param_schema": {},
"registry_type": "device",
"file_path": f"uploaded_file://{file_name}",
}
# 处理动作方法(仅对设备类型)
for method_name, method_info in enhanced_info["action_methods"].items():
registry_schema["class"]["action_value_mappings"][f"auto-{method_name}"] = {
"type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
"goal": {},
"feedback": {},
"result": {},
"args": method_info["args"],
"description": method_info.get("docstring", ""),
}
await send_log("成功生成注册表schema")
# 格式化状态方法信息
status_info = {}
for status_name, status_data in enhanced_info.get("status_methods", {}).items():
status_info[status_name] = {
"return_type": status_data.get("return_type", "未知类型"),
"docstring": status_data.get("docstring", "无描述"),
"is_property": status_data.get("is_property", False),
}
# 格式化动作方法信息
action_info = {}
for action_name, action_data in enhanced_info.get("action_methods", {}).items():
args = action_data.get("args", [])
action_info[action_name] = {
"param_count": len(args),
"params": [
{"name": arg.get("name", ""), "type": arg.get("type", ""), "default": arg.get("default")}
for arg in args
],
"is_async": action_data.get("is_async", False),
"docstring": action_data.get("docstring", "无描述"),
"return_suggestion": "建议返回字典类型 (dict) 以便更好地结构化结果数据",
}
# 准备结果数据
result = {
"class_info": {
"class_name": class_name,
"module_name": module_name,
"module_prefix": module_prefix,
"full_module_name": full_module_name,
"file_name": file_name,
"file_size": file_size,
"docstring": enhanced_info.get("class_docstring", ""),
"dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
"registry_type": registry_type,
},
"registry_schema": registry_schema,
"action_methods": enhanced_info["action_methods"],
"status_methods": enhanced_info["status_methods"],
"class_analysis": {
"status_methods": status_info,
"action_methods": action_info,
"init_params": enhanced_info.get("init_params", []),
"status_methods_count": len(status_info),
"action_methods_count": len(action_info),
},
# 保持向后兼容
"action_methods": enhanced_info.get("action_methods", {}),
"status_methods": enhanced_info.get("status_methods", {}),
}
# 发送结果
@@ -794,6 +859,11 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
registry_type = request_data.get("registry_type", "device")
class_name = request_data.get("class_name")
module_name = request_data.get("module_name")
description = request_data.get("description", "")
safe_class_name = request_data.get("safe_class_name", "")
icon = request_data.get("icon", "")
module_prefix = request_data.get("module_prefix", "")
handles = request_data.get("handles", [])
async def send_log(message: str, level: str = "info"):
"""发送日志消息到客户端"""
@@ -862,7 +932,12 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
# 确定模块名
if not module_name:
module_name = full_file_path.stem
# 如果有 module_prefix则使用完整的模块路径
full_module_name = f"{module_prefix}.{module_name}" if module_prefix else module_name
await send_log(f"使用模块名: {module_name}")
if module_prefix:
await send_log(f"完整模块路径: {full_module_name}")
# 导入模块
try:
@@ -930,7 +1005,7 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
from unilabos.utils.import_manager import get_enhanced_class_info
# 分析类信息
enhanced_info = get_enhanced_class_info(f"{module_name}:{target_class_name}", use_dynamic=True)
enhanced_info = get_enhanced_class_info(f"{full_module_name}:{target_class_name}", use_dynamic=True)
if not enhanced_info.get("dynamic_import_success", False):
await send_error("动态导入类信息失败")
@@ -938,55 +1013,124 @@ async def handle_file_import(websocket: WebSocket, request_data: dict):
await send_log("成功分析类信息")
# 生成注册表schema
registry_schema = {
"class_name": target_class_name,
"module": f"{module_name}:{target_class_name}",
"type": "python",
"description": enhanced_info.get("class_docstring", ""),
"version": "1.0.0",
"category": [registry_type],
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
"action_value_mappings": {},
"init_param_schema": {},
"registry_type": registry_type,
"file_path": str(full_file_path),
}
# 处理动作方法
for method_name, method_info in enhanced_info["action_methods"].items():
registry_schema["action_value_mappings"][f"auto-{method_name}"] = {
"type": "UniLabJsonCommandAsync" if method_info["is_async"] else "UniLabJsonCommand",
"goal": {},
"feedback": {},
"result": {},
"args": method_info["args"],
"description": method_info.get("docstring", ""),
# 根据注册表类型生成不同的schema
if registry_type == "resource":
# 资源类型的简单结构
category_name = Path(file_path).stem if file_path else "unknown"
registry_schema = {
"description": description or enhanced_info.get("class_docstring", ""),
"category": [category_name],
"class": {
"module": f"{full_module_name}:{target_class_name}",
"type": "python",
},
"handles": handles,
"icon": icon,
"init_param_schema": {},
"registry_type": "resource",
"version": "1.0.0",
}
else:
# 设备类型的复杂结构
registry_schema = {
"description": description or enhanced_info.get("class_docstring", ""),
"class": {
"module": f"{full_module_name}:{target_class_name}",
"type": "python",
"status_types": {k: v["return_type"] for k, v in enhanced_info["status_methods"].items()},
"action_value_mappings": {
f"auto-{k}": {
"type": "UniLabJsonCommandAsync" if v["is_async"] else "UniLabJsonCommand",
"goal": {},
"feedback": {},
"result": {},
"schema": lab_registry._generate_unilab_json_command_schema(v["args"], k),
"goal_default": {i["name"]: i["default"] for i in v["args"]},
"handles": [],
}
# 不生成已配置action的动作
for k, v in enhanced_info["action_methods"].items()
},
},
"version": "1.0.0",
"handles": handles,
"icon": icon,
"init_param_schema": {
"config": lab_registry._generate_unilab_json_command_schema(
enhanced_info["init_params"], "__init__"
)["properties"]["goal"],
"data": lab_registry._generate_status_types_schema(enhanced_info["status_methods"]),
},
"registry_type": "device",
}
await send_log("成功生成注册表schema")
# 转换为YAML格式
import yaml
from unilabos.utils.type_check import NoAliasDumper
# 创建最终的YAML配置使用设备ID作为根键
class_name_safe = class_name or "unknown"
suffix = "_device" if registry_type == "device" else "_resource"
device_id = f"{class_name_safe.lower()}{suffix}"
final_config = {device_id: registry_schema}
# 创建最终的YAML配置使用ID作为根键
if safe_class_name:
item_id = safe_class_name
else:
class_name_safe = (target_class_name or "unknown").lower()
if registry_type == "resource":
# 资源ID通常直接使用类名不加后缀
item_id = class_name_safe
else:
# 设备ID使用类名加_device后缀
item_id = f"{class_name_safe}_device"
final_config = {item_id: registry_schema}
yaml_content = yaml.dump(
final_config, allow_unicode=True, default_flow_style=False, Dumper=NoAliasDumper, sort_keys=True
)
# 准备结果数据只保留YAML结果
# 格式化状态方法信息
status_info = {}
for status_name, status_data in enhanced_info.get("status_methods", {}).items():
status_info[status_name] = {
"return_type": status_data.get("return_type", "未知类型"),
"docstring": status_data.get("docstring", "无描述"),
"is_property": status_data.get("is_property", False),
}
# 格式化动作方法信息
action_info = {}
for action_name, action_data in enhanced_info.get("action_methods", {}).items():
args = action_data.get("args", [])
action_info[action_name] = {
"param_count": len(args),
"params": [
{"name": arg.get("name", ""), "type": arg.get("type", ""), "default": arg.get("default")}
for arg in args
],
"is_async": action_data.get("is_async", False),
"docstring": action_data.get("docstring", "无描述"),
"return_suggestion": "建议返回字典类型 (dict) 以便更好地结构化结果数据",
}
# 准备结果数据(包含详细的类分析信息)
result = {
"registry_schema": yaml_content,
"device_id": device_id,
"class_name": class_name,
"item_id": item_id,
"registry_type": registry_type,
"class_name": target_class_name,
"module_name": module_name,
"file_path": file_path,
"config_params": {
"safe_class_name": safe_class_name or item_id,
"description": description,
"icon": icon,
"module_prefix": module_prefix,
"full_module_name": full_module_name,
"handles_count": len(handles),
"handles": handles,
},
"class_analysis": {
"class_docstring": enhanced_info.get("class_docstring", ""),
"status_methods": status_info,
"action_methods": action_info,
"init_params": enhanced_info.get("init_params", []),
"dynamic_import_success": enhanced_info.get("dynamic_import_success", False),
},
}
# 发送结果
@@ -1034,12 +1178,11 @@ def get_file_browser_data(path: str = ""):
items = []
parent_path = target_path.parent
relative_parent = parent_path.relative_to(working_dir)
items.append(
{
"name": "..",
"type": "directory",
"path": str(relative_parent) if relative_parent != Path(".") else "",
"path": str(parent_path),
"size": 0,
"is_parent": True,
}
@@ -1048,16 +1191,12 @@ def get_file_browser_data(path: str = ""):
# 获取子目录和文件
try:
for item in sorted(target_path.iterdir(), key=lambda x: (not x.is_dir(), x.name.lower())):
if item.name.startswith("."): # 跳过隐藏文件
continue
item_type = "directory" if item.is_dir() else "file"
relative_path = item.relative_to(working_dir)
item_info = {
"name": item.name,
"type": item_type,
"path": str(relative_path),
"path": str(item),
"size": item.stat().st_size if item.is_file() else 0,
"is_python": item.suffix == ".py" if item.is_file() else False,
"is_parent": False,
@@ -1068,7 +1207,7 @@ def get_file_browser_data(path: str = ""):
return Resp(
data={
"current_path": str(target_path.relative_to(working_dir)) if target_path != working_dir else "",
"current_path": str(target_path),
"working_dir": str(working_dir),
"items": items,
}

View File

@@ -8,7 +8,7 @@ header %}UniLab API{% endblock %} {% block nav %}
target="_blank"
>主页</a
>
class="nav-tab">状态</a>
<a href="/status" class="nav-tab">状态</a>
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
</div>
{% endblock %} {% block content %}

View File

@@ -1,6 +1,6 @@
{% extends "base.html" %} {% block title %}注册表编辑器 - UniLab{% endblock %}
{% block header %}注册表编辑器{% endblock %} {% block nav %}
{% endblock %} {% block scripts %}
{% block header %}注册表编辑器{% endblock %} {% block nav %} {% endblock %} {%
block scripts %}
<style>
.editor-container {
max-width: 100%;
@@ -9,11 +9,11 @@
position: relative;
}
.form-section {
background: white;
background: rgb(212, 226, 243);
border-radius: 8px;
padding: 20px;
padding: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 15px;
@@ -280,6 +280,77 @@
padding: 4px 12px;
}
/* Handle 配置相关样式 */
.handle-item {
border: 1px solid #ddd;
border-radius: 4px;
padding: 15px;
margin-bottom: 10px;
background: #f8f9fa;
position: relative;
}
.handle-item-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.handle-item-title {
font-weight: bold;
color: #333;
}
.remove-handle-btn {
background-color: #dc3545;
color: white;
border: none;
border-radius: 50%;
width: 24px;
height: 24px;
font-size: 12px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.remove-handle-btn:hover {
background-color: #c82333;
}
.handle-form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 10px;
}
.handle-form-row.full-width {
grid-template-columns: 1fr;
}
.handle-form-group {
display: flex;
flex-direction: column;
}
.handle-form-group label {
font-size: 12px;
font-weight: bold;
color: #555;
margin-bottom: 3px;
}
.handle-form-group input,
.handle-form-group select {
padding: 6px 8px;
border: 1px solid #ccc;
border-radius: 3px;
font-size: 13px;
}
/* 连接状态右上角定位 */
.connection-status-fixed {
position: fixed;
@@ -455,6 +526,84 @@
<!-- 右栏:注册表配置结果 -->
<div class="right-column">
<!-- 配置参数输入区域 -->
<div
id="config-params-section"
class="form-section"
style="display: none"
>
<h3>配置参数</h3>
<!-- Module Prefix 输入框 -->
<div class="form-group">
<label for="module-prefix">Module Prefix</label>
<input
type="text"
id="module-prefix"
class="form-control"
placeholder="例如: unilabos.devices.pumps"
/>
<small class="form-text">模块路径前缀,指定类所在的包位置</small>
</div>
<!-- Safe Class Name 输入框 -->
<div class="form-group">
<label for="safe-class-name">Safe Class Name</label>
<input
type="text"
id="safe-class-name"
class="form-control"
placeholder="自动生成,可自定义"
/>
<small class="form-text"
>将作为注册表中的ID使用必须是有效的标识符</small
>
</div>
<!-- 设备/资源描述 -->
<div class="form-group">
<label for="description-input">设备/资源描述</label>
<textarea
id="description-input"
class="form-control"
rows="3"
placeholder="请输入设备或资源的描述信息..."
></textarea>
<small class="form-text"
>描述信息将显示在注册表中,帮助用户了解设备功能</small
>
</div>
<!-- Icon 输入框 -->
<div class="form-group">
<label for="icon-input">图标文件名</label>
<input
type="text"
id="icon-input"
class="form-control"
placeholder="icon_xxx.webp"
/>
<small class="form-text">图标文件名例如icon_pump.webp</small>
</div>
<!-- Handles 配置区域 -->
<div class="form-group">
<label>Handles 配置</label>
<div id="handles-container">
<!-- 动态添加的handles项目会在这里显示 -->
</div>
<button
type="button"
class="btn btn-secondary"
onclick="addHandleItem()"
>
添加 Handle
</button>
<small class="form-text">配置设备/资源的输入输出接口</small>
</div>
</div>
<!-- YAML结果显示区域 -->
<div id="results-section" class="form-section" style="display: none">
<h3>注册表配置结果</h3>
<div class="yaml-container">
@@ -485,6 +634,7 @@
let isConnected = false;
let currentPath = '';
let selectedFilePath = null;
let handleCounter = 0;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function () {
@@ -613,7 +763,12 @@
// 保存YAML内容供复制使用
window.registryYaml = result.registry_schema;
addLog(`生成的设备ID: ${result.device_id}`, 'info');
addLog(
`生成的${result.registry_type === 'resource' ? '资源' : '设备'}ID: ${
result.item_id || result.device_id
}`,
'info'
);
}
addLog('注册表生成完成!', 'success');
@@ -717,24 +872,6 @@
// 清空文件浏览器
fileBrowser.innerHTML = '';
// 添加返回上级目录选项(除了根路径)
const currentPath = data.current_path || data.working_dir;
if (currentPath && currentPath !== '' && !isRootPath(currentPath)) {
const parentItem = document.createElement('div');
parentItem.className = 'file-item';
parentItem.innerHTML = `
<span class="file-icon directory-icon">⬆️</span>
<span class="file-name">..</span>
<span class="file-size">(返回上级目录)</span>
`;
// 计算上级目录路径
const parentPath = currentPath.split(/[/\\]/).slice(0, -1).join('/');
parentItem.onclick = () => loadFileBrowser(parentPath);
fileBrowser.appendChild(parentItem);
}
if (data.items.length === 0) {
if (fileBrowser.children.length === 0) {
fileBrowser.innerHTML =
@@ -912,14 +1049,31 @@
// 显示分析状态
showFileAnalysisStatus(analysisResult);
// 监听类选择变化,启用导入按钮
// 监听类选择变化,启用导入按钮和显示配置参数区域
classSelect.onchange = function () {
const importBtn = document.getElementById('import-btn');
const configParamsSection = document.getElementById(
'config-params-section'
);
const safeClassNameInput = document.getElementById('safe-class-name');
if (this.value) {
importBtn.disabled = false;
configParamsSection.style.display = 'block';
// 自动生成Safe Class Name
const registryType = document.querySelector(
'input[name="registry-type"]:checked'
).value;
const suffix = registryType === 'device' ? '_device' : '';
const safeClassName = this.value.toLowerCase() + suffix;
safeClassNameInput.value = safeClassName;
addLog(`选择了类: ${this.value}`, 'info');
addLog(`自动生成Safe Class Name: ${safeClassName}`, 'info');
} else {
importBtn.disabled = true;
configParamsSection.style.display = 'none';
}
};
} else {
@@ -992,6 +1146,17 @@
'input[name="registry-type"]:checked'
).value;
// 获取用户输入的所有配置参数
const description = document
.getElementById('description-input')
.value.trim();
const safeClassName = document
.getElementById('safe-class-name')
.value.trim();
const iconInput = document.getElementById('icon-input').value.trim();
const modulePrefix = document.getElementById('module-prefix').value.trim();
const handlesConfig = getHandlesConfig();
updateConnectionStatus('processing', '正在生成注册表...');
const request = {
@@ -1001,6 +1166,11 @@
registry_type: registryType,
class_name: className,
module_name: null, // 模块名自动生成
description: description,
safe_class_name: safeClassName,
icon: iconInput,
module_prefix: modulePrefix,
handles: handlesConfig,
},
};
@@ -1010,11 +1180,27 @@
addLog(`选择的类: ${className}`, 'info');
// 显示文件信息
showFileInfo(selectedFilePath, registryType, className);
showFileInfo(
selectedFilePath,
registryType,
className,
safeClassName,
iconInput,
modulePrefix,
handlesConfig
);
}
// 显示文件信息
function showFileInfo(filePath, registryType, className) {
function showFileInfo(
filePath,
registryType,
className,
safeClassName,
icon,
modulePrefix,
handles
) {
const fileInfo = document.getElementById('file-info');
const fileDetails = document.getElementById('file-details');
@@ -1022,8 +1208,24 @@
<div><strong>文件路径:</strong> ${filePath}</div>
<div><strong>注册表类型:</strong> ${registryType}</div>
<div><strong>选择的类:</strong> ${className}</div>
<div><strong>Safe Class Name:</strong> ${
safeClassName || '自动生成'
}</div>
<div><strong>Module Prefix:</strong> ${modulePrefix || '无'}</div>
<div><strong>图标:</strong> ${icon || '无'}</div>
<div><strong>Handles 数量:</strong> ${handles.length}</div>
`;
if (handles.length > 0) {
details += '<div><strong>Handles 配置:</strong><ul>';
handles.forEach((handle, index) => {
details += `<li>${index + 1}. ${handle.data_key} (${
handle.io_type
})</li>`;
});
details += '</ul></div>';
}
fileDetails.innerHTML = details;
fileInfo.style.display = 'block';
}
@@ -1051,8 +1253,20 @@
const importBtn = document.getElementById('import-btn');
importBtn.disabled = true;
// 隐藏类选择
// 隐藏类选择和配置参数区域
document.getElementById('class-selection-group').style.display = 'none';
document.getElementById('config-params-section').style.display = 'none';
// 清空所有输入框
document.getElementById('description-input').value = '';
document.getElementById('safe-class-name').value = '';
document.getElementById('icon-input').value = '';
document.getElementById('module-prefix').value = '';
// 清空handles配置
const handlesContainer = document.getElementById('handles-container');
handlesContainer.innerHTML = '';
handleCounter = 0;
// 根据当前注册表类型设置默认选项文字
const registryType = document.querySelector(
@@ -1081,5 +1295,117 @@
addLog('已重置所有设置', 'info');
}
// 添加Handle项目
function addHandleItem() {
handleCounter++;
const handlesContainer = document.getElementById('handles-container');
const handleItem = document.createElement('div');
handleItem.className = 'handle-item';
handleItem.id = `handle-item-${handleCounter}`;
handleItem.innerHTML = `
<div class="handle-item-header">
<span class="handle-item-title">Handle ${handleCounter}</span>
<button type="button" class="remove-handle-btn" onclick="removeHandleItem(${handleCounter})" title="删除">×</button>
</div>
<div class="handle-form-row">
<div class="handle-form-group">
<label>Data Key</label>
<input type="text" placeholder="例如: fluid_port_1" data-field="data_key">
</div>
<div class="handle-form-group">
<label>Handler Key</label>
<input type="text" placeholder="例如: port_1" data-field="handler_key">
</div>
</div>
<div class="handle-form-row">
<div class="handle-form-group">
<label>Data Source</label>
<input type="text" placeholder="例如: executor、handle" data-field="data_source">
</div>
<div class="handle-form-group">
<label>Data Type</label>
<input type="text" placeholder="例如: fluid" data-field="data_type">
</div>
</div>
<div class="handle-form-row">
<div class="handle-form-group">
<label>IO Type</label>
<select data-field="io_type">
<option value="">请选择</option>
<option value="source">source输出右侧</option>
<option value="target">target输入左侧</option>
</select>
</div>
<div class="handle-form-group">
<label>Side</label>
<select data-field="side">
<option value="">请选择</option>
<option value="NORTH">NORTH</option>
<option value="SOUTH">SOUTH</option>
<option value="EAST">EAST</option>
<option value="WEST">WEST</option>
</select>
</div>
</div>
<div class="handle-form-row">
<div class="handle-form-group">
<label>Label</label>
<input type="text" placeholder="显示名称,例如: port_1" data-field="label">
</div>
</div>
<div class="handle-form-row full-width">
<div class="handle-form-group">
<label>Description</label>
<input type="text" placeholder="例如: 八通阀门端口1" data-field="description">
</div>
</div>
`;
handlesContainer.appendChild(handleItem);
addLog(`添加了 Handle ${handleCounter}`, 'info');
}
// 删除Handle项目
function removeHandleItem(id) {
const handleItem = document.getElementById(`handle-item-${id}`);
if (handleItem) {
handleItem.remove();
addLog(`删除了 Handle ${id}`, 'info');
}
}
// 获取所有Handles配置
function getHandlesConfig() {
const handles = [];
const handleItems = document.querySelectorAll('.handle-item');
handleItems.forEach((item) => {
const handleData = {};
const inputs = item.querySelectorAll('[data-field]');
inputs.forEach((input) => {
const field = input.getAttribute('data-field');
const value = input.value.trim();
if (value) {
handleData[field] = value;
}
});
// 只有当至少有data_key时才添加
if (handleData.data_key) {
handles.push(handleData);
}
});
return handles;
}
</script>
{% endblock %}

View File

@@ -7,6 +7,7 @@ block header %}UniLab System Status{% endblock %} {% block top_info %}
href="/status"
class="nav-tab"
style="background-color: #4caf50; color: white"
target="_blank"
>状态</a
>
<a href="/registry-editor" class="nav-tab" target="_blank">注册表编辑</a>
@@ -1720,9 +1721,10 @@ ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_con
const navTabs = document.querySelectorAll('.nav-tab');
navTabs.forEach((tab) => {
tab.addEventListener('click', function (e) {
e.preventDefault();
const targetId = this.getAttribute('href');
// 只对内部锚链接(以#开头)应用滚动行为
if (targetId && targetId.startsWith('#')) {
e.preventDefault();
const targetElement = document.querySelector(targetId);
if (targetElement) {
targetElement.scrollIntoView({
@@ -1730,6 +1732,7 @@ ros2 action send_goal {{ action_info.action_path }} {{ action_info.type_name_con
});
}
}
// 对于外部链接(如 /status, /registry-editor不调用preventDefault(),让浏览器处理默认行为
});
});
}

View File

@@ -576,7 +576,7 @@ class Registry:
}
device_config["file_path"] = str(file.absolute()).replace("\\", "/")
device_config["registry_type"] = "device"
logger.trace(
logger.trace( # type: ignore
f"[UniLab Registry] Device-{current_device_number} File-{i+1}/{len(files)} Add {device_id} "
+ f"[{data[device_id].get('name', '未命名设备')}]"
)