Files
Uni-Lab-OS/unilabos/app/web/templates/registry_editor.html
2025-09-07 20:57:48 +08:00

1412 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{% extends "base.html" %} {% block title %}注册表编辑器 - UniLab{% endblock %}
{% block header %}注册表编辑器{% endblock %} {% block nav %} {% endblock %} {%
block scripts %}
<style>
.editor-container {
max-width: 100%;
margin: 0;
padding: 10px;
position: relative;
}
.form-section {
background: rgb(212, 226, 243);
border-radius: 8px;
padding: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 2px rgba(0, 0, 0, 0.1);
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 5px;
font-weight: bold;
color: #333;
}
.form-control {
width: 100%;
padding: 10px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
box-sizing: border-box;
}
.btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
margin-right: 10px;
transition: background-color 0.2s;
}
.btn-primary {
background-color: #2196f3;
color: white;
}
.btn-primary:hover {
background-color: #0b7dda;
}
.btn-secondary {
background-color: #6c757d;
color: white;
}
.btn-secondary:hover {
background-color: #545b62;
}
.radio-group {
display: flex;
gap: 15px;
margin-top: 8px;
}
.radio-option {
display: flex;
align-items: center;
gap: 5px;
}
.log-container {
background: #1e1e1e;
color: #ffffff;
border-radius: 4px;
padding: 15px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
max-height: 400px;
overflow-y: auto;
white-space: pre-wrap;
word-wrap: break-word;
border: 1px solid #444;
}
.status-indicator {
display: inline-block;
width: 12px;
height: 12px;
border-radius: 50%;
margin-right: 8px;
}
.status-disconnected {
background-color: #f44336;
}
.status-connected {
background-color: #4caf50;
}
.status-processing {
background-color: #ff9800;
}
.results-section {
background: white;
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
.results-tabs {
display: flex;
margin-bottom: 15px;
border-bottom: 1px solid #ddd;
}
.result-tab {
padding: 10px 20px;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s;
}
.result-tab.active {
border-bottom-color: #2196f3;
color: #2196f3;
font-weight: bold;
}
.result-tab:hover {
background-color: #f5f5f5;
}
.result-content {
display: none;
}
.result-content.active {
display: block;
}
.schema-viewer {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 4px;
padding: 15px;
font-family: 'Consolas', 'Monaco', monospace;
font-size: 13px;
max-height: 500px;
overflow: auto;
white-space: pre-wrap;
}
.error-message {
background-color: #f8d7da;
border: 1px solid #f5c6cb;
color: #721c24;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.success-message {
background-color: #d4edda;
border: 1px solid #c3e6cb;
color: #155724;
padding: 10px;
border-radius: 4px;
margin-bottom: 15px;
}
.file-info {
background: #e9f7fe;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-size: 13px;
color: #555;
}
.file-input-group {
display: flex;
gap: 10px;
align-items: center;
}
.file-browse-btn {
white-space: nowrap;
min-width: 100px;
}
.form-text {
display: block;
margin-top: 5px;
font-size: 12px;
color: #6c757d;
}
.file-analysis-status {
background: #fff3cd;
border: 1px solid #ffeaa7;
color: #856404;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-size: 13px;
}
.class-info {
background: #d1ecf1;
border: 1px solid #bee5eb;
color: #0c5460;
padding: 10px;
border-radius: 4px;
margin-top: 10px;
font-size: 13px;
}
.file-browser-container {
border: 1px solid #ddd;
border-radius: 4px;
background: white;
}
.current-path {
padding: 8px 12px;
background: #f8f9fa;
border-bottom: 1px solid #ddd;
font-size: 13px;
color: #555;
}
.file-browser {
max-height: 300px;
overflow-y: auto;
padding: 0;
}
.file-item {
padding: 8px 12px;
border-bottom: 1px solid #eee;
cursor: pointer;
display: flex;
align-items: center;
transition: background-color 0.2s;
}
.file-item:hover {
background-color: #f8f9fa;
}
.file-item.selected {
background-color: #e3f2fd;
border-left: 3px solid #2196f3;
}
.file-icon {
margin-right: 8px;
font-size: 16px;
width: 20px;
text-align: center;
}
.file-name {
flex: 1;
font-size: 14px;
}
.file-size {
font-size: 12px;
color: #666;
margin-left: 8px;
}
.directory-icon {
color: #ff9800;
}
.python-file-icon {
color: #4caf50;
}
.other-file-icon {
color: #666;
}
.selected-file {
padding: 8px 12px;
background: #e8f5e8;
border-top: 1px solid #ddd;
font-size: 13px;
}
.loading-indicator {
padding: 20px;
text-align: center;
color: #666;
font-style: italic;
}
.yaml-container {
border: 1px solid #ddd;
border-radius: 4px;
background: white;
}
.yaml-header {
padding: 10px 12px;
background: #f8f9fa;
border-bottom: 1px solid #ddd;
display: flex;
justify-content: space-between;
align-items: center;
}
.copy-yaml-btn {
font-size: 12px;
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;
top: 80px;
right: 20px;
background: white;
padding: 10px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
z-index: 1000;
border: 1px solid #ddd;
}
/* 主要内容区域两栏布局 */
.main-content {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.left-column {
flex: 1;
min-width: 0;
}
.right-column {
flex: 1;
min-width: 0;
}
/* 日志区域在底部 */
.log-section-bottom {
background: white;
border-top: 2px solid #e0e0e0;
padding: 15px;
margin-top: 20px;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
/* 响应式设计 */
@media (max-width: 1024px) {
.main-content {
flex-direction: column;
}
.connection-status-fixed {
position: relative;
top: auto;
right: auto;
margin-bottom: 20px;
}
}
</style>
{% endblock %} {% block content %}
<div class="editor-container">
<!-- 连接状态 - 右上角固定 -->
<div class="connection-status-fixed">
<div id="connection-status">
<span class="status-indicator status-disconnected" id="status-dot"></span>
<span id="status-text">未连接</span>
</div>
</div>
<!-- 主要内容区域 -->
<div class="main-content">
<!-- 左栏:文件导入配置 -->
<div class="left-column">
<div class="form-section">
<h3>文件导入配置</h3>
<div class="form-group">
<label for="registry-type">注册表类型</label>
<div class="radio-group">
<div class="radio-option">
<input
type="radio"
id="type-device"
name="registry-type"
value="device"
checked
/>
<label for="type-device">设备 (Device)</label>
</div>
<div class="radio-option">
<input
type="radio"
id="type-resource"
name="registry-type"
value="resource"
/>
<label for="type-resource">资源 (Resource)</label>
</div>
</div>
</div>
<div class="form-group">
<label for="file-browser">选择Python文件</label>
<div class="file-browser-container">
<!-- 当前路径显示 -->
<div class="current-path">
<strong>当前路径:</strong>
<span id="current-path-display">加载中...</span>
</div>
<!-- 文件浏览器 -->
<div class="file-browser" id="file-browser">
<div class="loading-indicator">正在加载文件列表...</div>
</div>
<!-- 选择的文件显示 -->
<div class="selected-file" id="selected-file" style="display: none">
<strong>已选择:</strong>
<span id="selected-file-name"></span>
<span id="selected-file-size" class="file-size"></span>
</div>
</div>
<small class="form-text">只显示 .py 文件和文件夹</small>
</div>
<div class="form-group">
<button
class="btn btn-secondary"
onclick="analyzeFile()"
id="analyze-btn"
disabled
>
分析文件
</button>
<small class="form-text">首先分析文件以获取可用的类列表</small>
</div>
<div
class="form-group"
id="class-selection-group"
style="display: none"
>
<label for="class-name" id="class-selection-label">选择类名</label>
<select id="class-name" class="form-control">
<option value="" id="class-selection-option">请选择类...</option>
</select>
</div>
<div class="form-group">
<button
class="btn btn-primary"
onclick="importFile()"
id="import-btn"
disabled
>
生成注册表
</button>
<button class="btn btn-secondary" onclick="clearResults()">
重置
</button>
</div>
<div id="workflow-tips" class="class-info">
<strong>使用流程:</strong><br />
1. 选择注册表类型(设备/资源)<br />
2. 在文件浏览器中导航并选择Python文件<br />
3. 点击"分析文件"获取可用类列表<br />
4. 从下拉列表中选择要注册的类<br />
5. 点击"生成注册表"获得YAML配置文件
</div>
<div id="file-info" class="file-info" style="display: none">
<strong>文件信息:</strong>
<div id="file-details"></div>
</div>
</div>
</div>
<!-- 右栏:注册表配置结果 -->
<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">
<div class="yaml-header">
<strong>YAML格式注册表配置</strong>
<button
class="btn btn-secondary copy-yaml-btn"
onclick="copyYamlToClipboard()"
>
复制YAML
</button>
</div>
<div class="schema-viewer" id="yaml-registry-viewer">等待数据...</div>
</div>
</div>
</div>
</div>
<!-- 处理日志 - 底部 -->
<div class="log-section-bottom">
<h3>处理日志</h3>
<div id="log-output" class="log-container">等待操作...</div>
</div>
</div>
<script>
let ws = null;
let isConnected = false;
let currentPath = '';
let selectedFilePath = null;
let handleCounter = 0;
// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', function () {
initWebSocket();
loadFileBrowser(''); // 加载根目录
initRegistryTypeListener();
});
// 初始化注册表类型监听器
function initRegistryTypeListener() {
const radioButtons = document.querySelectorAll(
'input[name="registry-type"]'
);
radioButtons.forEach((radio) => {
radio.addEventListener('change', function () {
updateClassSelectionLabel(this.value);
});
});
// 初始化标签文字
const checkedRadio = document.querySelector(
'input[name="registry-type"]:checked'
);
if (checkedRadio) {
updateClassSelectionLabel(checkedRadio.value);
}
}
// 更新类选择标签文字
function updateClassSelectionLabel(registryType) {
const label = document.getElementById('class-selection-label');
const option = document.getElementById('class-selection-option');
if (registryType === 'resource') {
label.textContent = '选择类名/可执行函数';
option.textContent = '请选择类或函数...';
} else {
label.textContent = '选择类名';
option.textContent = '请选择类...';
}
}
// 初始化WebSocket连接
function initWebSocket() {
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsUrl = `${protocol}//${window.location.host}/api/v1/ws/registry_editor`;
ws = new WebSocket(wsUrl);
ws.onopen = function (event) {
isConnected = true;
updateConnectionStatus('connected', '已连接');
addLog('WebSocket连接已建立', 'info');
};
ws.onmessage = function (event) {
try {
const data = JSON.parse(event.data);
handleWebSocketMessage(data);
} catch (e) {
addLog(`解析消息失败: ${e.message}`, 'error');
}
};
ws.onclose = function (event) {
isConnected = false;
updateConnectionStatus('disconnected', '连接已断开');
addLog('WebSocket连接已断开', 'warning');
// 5秒后尝试重连
setTimeout(initWebSocket, 5000);
};
ws.onerror = function (error) {
addLog('WebSocket连接错误', 'error');
updateConnectionStatus('disconnected', '连接错误');
};
}
// 更新连接状态
function updateConnectionStatus(status, text) {
const statusDot = document.getElementById('status-dot');
const statusText = document.getElementById('status-text');
statusDot.className = `status-indicator status-${status}`;
statusText.textContent = text;
}
// 处理WebSocket消息
function handleWebSocketMessage(data) {
switch (data.type) {
case 'log':
addLog(data.message, data.level || 'info');
break;
case 'progress':
updateConnectionStatus('processing', data.message);
break;
case 'result':
handleAnalysisResult(data.data);
break;
case 'file_analysis_result':
handleFileAnalysisResult(data.data);
break;
case 'error':
addLog(data.message, 'error');
updateConnectionStatus('connected', '已连接');
break;
default:
addLog(`收到未知消息类型: ${data.type}`, 'warning');
}
}
// 处理分析结果
function handleAnalysisResult(result) {
updateConnectionStatus('connected', '已连接');
// 显示结果区域
document.getElementById('results-section').style.display = 'block';
// 显示YAML格式的注册表配置
if (result.registry_schema) {
// 直接显示后端返回的YAML字符串
const yamlViewer = document.getElementById('yaml-registry-viewer');
yamlViewer.textContent = result.registry_schema;
// 保存YAML内容供复制使用
window.registryYaml = result.registry_schema;
addLog(
`生成的${result.registry_type === 'resource' ? '资源' : '设备'}ID: ${
result.item_id || result.device_id
}`,
'info'
);
}
addLog('注册表生成完成!', 'success');
}
// 复制YAML到剪贴板
function copyYamlToClipboard() {
if (!window.registryYaml) {
addLog('没有可复制的YAML内容', 'error');
return;
}
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard
.writeText(window.registryYaml)
.then(() => {
addLog('YAML配置已复制到剪贴板', 'success');
// 视觉反馈
const copyBtn = document.querySelector('.copy-yaml-btn');
const originalText = copyBtn.textContent;
copyBtn.textContent = '已复制!';
copyBtn.style.backgroundColor = '#4caf50';
setTimeout(() => {
copyBtn.textContent = originalText;
copyBtn.style.backgroundColor = '';
}, 2000);
})
.catch((err) => {
addLog('复制失败,请手动复制', 'error');
});
} else {
// 后备方案:选择文本
const yamlViewer = document.getElementById('yaml-registry-viewer');
const range = document.createRange();
range.selectNodeContents(yamlViewer);
const selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
addLog('已选择YAML文本请按Ctrl+C复制', 'info');
}
}
// 添加日志
function addLog(message, level = 'info') {
const logOutput = document.getElementById('log-output');
const timestamp = new Date().toLocaleTimeString();
let prefix = '';
switch (level) {
case 'error':
prefix = '[ERROR]';
break;
case 'warning':
prefix = '[WARN]';
break;
case 'success':
prefix = '[SUCCESS]';
break;
case 'info':
default:
prefix = '[INFO]';
break;
}
const logLine = `${timestamp} ${prefix} ${message}\n`;
logOutput.textContent += logLine;
logOutput.scrollTop = logOutput.scrollHeight;
}
// 加载文件浏览器
async function loadFileBrowser(path) {
try {
const response = await fetch(
`/api/v1/file-browser?path=${encodeURIComponent(path)}`
);
const result = await response.json();
if (result.code === 0) {
const data = result.data;
currentPath = data.current_path;
renderFileBrowser(data);
} else {
addLog(`加载目录失败: ${result.message}`, 'error');
}
} catch (error) {
addLog(`加载目录时出错: ${error}`, 'error');
}
}
// 渲染文件浏览器
function renderFileBrowser(data) {
const fileBrowser = document.getElementById('file-browser');
const currentPathDisplay = document.getElementById('current-path-display');
// 更新当前路径显示
currentPathDisplay.textContent = data.current_path || data.working_dir;
// 清空文件浏览器
fileBrowser.innerHTML = '';
if (data.items.length === 0) {
if (fileBrowser.children.length === 0) {
fileBrowser.innerHTML =
'<div class="loading-indicator">此目录为空</div>';
}
return;
}
// 渲染文件和目录
data.items.forEach((item) => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.dataset.path = item.path;
fileItem.dataset.type = item.type;
let icon = '';
let iconClass = '';
if (item.type === 'directory') {
icon = '📁';
iconClass = 'directory-icon';
} else if (item.is_python) {
icon = '🐍';
iconClass = 'python-file-icon';
} else {
return; // 只显示Python文件和目录
}
let sizeText = '';
if (item.type === 'file') {
sizeText = `<span class="file-size">(${formatFileSize(
item.size
)})</span>`;
}
fileItem.innerHTML = `
<span class="file-icon ${iconClass}">${icon}</span>
<span class="file-name">${item.name}</span>
${sizeText}
`;
// 添加点击事件
if (item.type === 'directory') {
fileItem.onclick = () => loadFileBrowser(item.path);
} else if (item.is_python) {
fileItem.onclick = () => selectFile(item);
}
fileBrowser.appendChild(fileItem);
});
}
// 选择文件
function selectFile(fileInfo) {
// 清除之前的选择
document.querySelectorAll('.file-item').forEach((item) => {
item.classList.remove('selected');
});
// 标记当前选择
event.target.closest('.file-item').classList.add('selected');
// 保存选择的文件信息
selectedFilePath = fileInfo.path;
window.selectedFileInfo = fileInfo;
// 显示选择的文件信息
const selectedFileDiv = document.getElementById('selected-file');
const selectedFileName = document.getElementById('selected-file-name');
const selectedFileSize = document.getElementById('selected-file-size');
selectedFileName.textContent = fileInfo.name;
selectedFileSize.textContent = `(${formatFileSize(fileInfo.size)})`;
selectedFileDiv.style.display = 'block';
// 启用分析按钮
const analyzeBtn = document.getElementById('analyze-btn');
analyzeBtn.disabled = false;
addLog(`已选择文件: ${fileInfo.name}`, 'info');
addLog('点击"分析文件"按钮开始分析', 'success');
}
// 判断是否为根路径
function isRootPath(path) {
if (!path || path === '') return true;
// Windows根路径如 C:, D:, C:\, D:\
if (/^[A-Za-z]:?\\?$/.test(path)) return true;
// Unix根路径
if (path === '/') return true;
// 网络路径根(\\server 或 //server
if (/^[\\\/]{2}[^\\\/]+$/.test(path)) return true;
return false;
}
// 格式化文件大小
function formatFileSize(bytes) {
if (bytes === 0) return '0 B';
const k = 1024;
const sizes = ['B', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + ' ' + sizes[i];
}
// 分析文件
function analyzeFile() {
if (!isConnected) {
addLog('WebSocket未连接请等待连接建立', 'error');
return;
}
if (!selectedFilePath) {
addLog('请先选择文件', 'error');
return;
}
const analyzeBtn = document.getElementById('analyze-btn');
analyzeBtn.disabled = true;
analyzeBtn.textContent = '分析中...';
const request = {
type: 'analyze_file',
data: {
file_path: selectedFilePath,
},
};
ws.send(JSON.stringify(request));
addLog(`开始分析文件: ${selectedFilePath}`, 'info');
}
// 处理文件分析结果
function handleFileAnalysisResult(analysisResult) {
const analyzeBtn = document.getElementById('analyze-btn');
const classSelectionGroup = document.getElementById(
'class-selection-group'
);
const classSelect = document.getElementById('class-name');
analyzeBtn.disabled = false;
analyzeBtn.textContent = '重新分析';
if (analysisResult.success) {
const classes = analysisResult.classes || [];
const registryType = document.querySelector(
'input[name="registry-type"]:checked'
).value;
// 清空现有选项并设置默认选项
const defaultText =
registryType === 'resource' ? '请选择类或函数...' : '请选择类...';
classSelect.innerHTML = `<option value="">${defaultText}</option>`;
// 添加找到的类
classes.forEach((cls) => {
const option = document.createElement('option');
option.value = cls.name;
option.textContent = `${cls.name} - ${cls.docstring || '无描述'}`;
classSelect.appendChild(option);
});
if (classes.length > 0) {
classSelectionGroup.style.display = 'block';
addLog(
`找到 ${classes.length} 个类: ${classes
.map((c) => c.name)
.join(', ')}`,
'success'
);
// 显示分析状态
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 {
addLog('文件中未找到任何类定义', 'warning');
classSelectionGroup.style.display = 'none';
}
} else {
addLog(`文件分析失败: ${analysisResult.error}`, 'error');
classSelectionGroup.style.display = 'none';
}
}
// 显示文件分析状态
function showFileAnalysisStatus(analysisResult) {
// 移除现有的分析状态显示
const existingStatus = document.getElementById('file-analysis-status');
if (existingStatus) {
existingStatus.remove();
}
// 创建新的状态显示
const statusDiv = document.createElement('div');
statusDiv.id = 'file-analysis-status';
statusDiv.className = 'file-analysis-status';
let statusContent = `
<strong>文件分析结果:</strong><br>
文件路径: ${analysisResult.file_path}<br>
模块名: ${analysisResult.module_name}<br>
找到类数量: ${analysisResult.classes.length}
`;
if (analysisResult.classes.length > 0) {
statusContent += '<br><strong>可用类:</strong>';
analysisResult.classes.forEach((cls) => {
statusContent += `<br>• ${cls.name}: ${cls.docstring || '无描述'}`;
});
}
statusDiv.innerHTML = statusContent;
// 插入到类选择组前面
const classSelectionGroup = document.getElementById(
'class-selection-group'
);
classSelectionGroup.parentNode.insertBefore(statusDiv, classSelectionGroup);
}
// 导入文件
function importFile() {
if (!isConnected) {
addLog('WebSocket未连接请等待连接建立', 'error');
return;
}
if (!selectedFilePath) {
addLog('请先选择文件', 'error');
return;
}
const classSelect = document.getElementById('class-name');
const className = classSelect.value.trim();
if (!className) {
addLog('请先分析文件并选择要导入的类', 'error');
return;
}
const registryType = document.querySelector(
'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 = {
type: 'import_file',
data: {
file_path: selectedFilePath,
registry_type: registryType,
class_name: className,
module_name: null, // 模块名自动生成
description: description,
safe_class_name: safeClassName,
icon: iconInput,
module_prefix: modulePrefix,
handles: handlesConfig,
},
};
ws.send(JSON.stringify(request));
addLog(`开始导入文件: ${selectedFilePath}`, 'info');
addLog(`注册表类型: ${registryType}`, 'info');
addLog(`选择的类: ${className}`, 'info');
// 显示文件信息
showFileInfo(
selectedFilePath,
registryType,
className,
safeClassName,
iconInput,
modulePrefix,
handlesConfig
);
}
// 显示文件信息
function showFileInfo(
filePath,
registryType,
className,
safeClassName,
icon,
modulePrefix,
handles
) {
const fileInfo = document.getElementById('file-info');
const fileDetails = document.getElementById('file-details');
let details = `
<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';
}
// 清空结果
function clearResults() {
// 重置文件选择
selectedFilePath = null;
window.selectedFileInfo = null;
// 清除文件选择状态
document.querySelectorAll('.file-item').forEach((item) => {
item.classList.remove('selected');
});
// 隐藏选择的文件显示
document.getElementById('selected-file').style.display = 'none';
// 重置分析按钮
const analyzeBtn = document.getElementById('analyze-btn');
analyzeBtn.disabled = true;
analyzeBtn.textContent = '分析文件';
// 重置导入按钮
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(
'input[name="registry-type"]:checked'
).value;
const defaultText =
registryType === 'resource' ? '请选择类或函数...' : '请选择类...';
document.getElementById(
'class-name'
).innerHTML = `<option value="">${defaultText}</option>`;
// 移除分析状态显示
const analysisStatus = document.getElementById('file-analysis-status');
if (analysisStatus) {
analysisStatus.remove();
}
// 清空其他显示区域
document.getElementById('results-section').style.display = 'none';
document.getElementById('file-info').style.display = 'none';
document.getElementById('log-output').textContent = '等待操作...';
// 清空YAML查看器
document.getElementById('yaml-registry-viewer').textContent = '等待数据...';
window.registryYaml = null;
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 %}