Files
Uni-Lab-OS/unilabos/app/web/templates/registry_editor.html
Xuwznln 9aeffebde1 0.10.7 Update (#101)
* Cleanup registry to be easy-understanding (#76)

* delete deprecated mock devices

* rename categories

* combine chromatographic devices

* rename rviz simulation nodes

* organic virtual devices

* parse vessel_id

* run registry completion before merge

---------

Co-authored-by: Xuwznln <18435084+Xuwznln@users.noreply.github.com>

* fix: workstation handlers and vessel_id parsing

* fix: working dir error when input config path
feat: report publish topic when error

* modify default discovery_interval to 15s

* feat: add trace log level

* feat: 添加ChinWe设备控制类,支持串口通信和电机控制功能 (#79)

* fix: drop_tips not using auto resource select

* fix: discard_tips error

* fix: discard_tips

* fix: prcxi_res

* add: prcxi res
fix: startup slow

* feat: workstation example

* fix pumps and liquid_handler handle

* feat: 优化protocol node节点运行日志

* fix all protocol_compilers and remove deprecated devices

* feat: 新增use_remote_resource参数

* fix and remove redundant info

* bugfixes on organic protocols

* fix filter protocol

* fix protocol node

* 临时兼容错误的driver写法

* fix: prcxi import error

* use call_async in all service to avoid deadlock

* fix: figure_resource

* Update recipe.yaml

* add workstation template and battery example

* feat: add sk & ak

* update workstation base

* Create workstation_architecture.md

* refactor: workstation_base 重构为仅含业务逻辑,通信和子设备管理交给 ProtocolNode

* refactor: ProtocolNode→WorkstationNode

* Add:msgs.action (#83)

* update: Workstation dev 将版本号从 0.10.3 更新为 0.10.4 (#84)

* Add:msgs.action

* update: 将版本号从 0.10.3 更新为 0.10.4

* simplify resource system

* uncompleted refactor

* example for use WorkstationBase

* feat: websocket

* feat: websocket test

* feat: workstation example

* feat: action status

* fix: station自己的方法注册错误

* fix: 还原protocol node处理方法

* fix: build

* fix: missing job_id key

* ws test version 1

* ws test version 2

* ws protocol

* 增加物料关系上传日志

* 增加物料关系上传日志

* 修正物料关系上传

* 修复工站的tracker实例追踪失效问题

* 增加handle检测,增加material edge关系上传

* 修复event loop错误

* 修复edge上报错误

* 修复async错误

* 更新schema的title字段

* 主机节点信息等支持自动刷新

* 注册表编辑器

* 修复status密集发送时,消息出错

* 增加addr参数

* fix: addr param

* fix: addr param

* 取消labid 和 强制config输入

* Add action definitions for LiquidHandlerSetGroup and LiquidHandlerTransferGroup

- Created LiquidHandlerSetGroup.action with fields for group name, wells, and volumes.
- Created LiquidHandlerTransferGroup.action with fields for source and target group names and unit volume.
- Both actions include response fields for return information and success status.

* Add LiquidHandlerSetGroup and LiquidHandlerTransferGroup actions to CMakeLists

* Add set_group and transfer_group methods to PRCXI9300Handler and update liquid_handler.yaml

* result_info改为字典类型

* 新增uat的地址替换

* runze multiple pump support

(cherry picked from commit 49354fcf39)

* remove runze multiple software obtainer

(cherry picked from commit 8bcc92a394)

* support multiple backbone

(cherry picked from commit 4771ff2347)

* Update runze pump format

* Correct runze multiple backbone

* Update runze_multiple_backbone

* Correct runze pump multiple receive method.

* Correct runze pump multiple receive method.

* 对于PRCXI9320的transfer_group,一对多和多对多

* 移除MQTT,更新launch文档,提供注册表示例文件,更新到0.10.5

* fix import error

* fix dupe upload registry

* refactor ws client

* add server timeout

* Fix: run-column with correct vessel id (#86)

* fix run_column

* Update run_column_protocol.py

(cherry picked from commit e5aa4d940a)

* resource_update use resource_add

* 新增版位推荐功能

* 重新规定了版位推荐的入参

* update registry with nested obj

* fix protocol node log_message, added create_resource return value

* fix protocol node log_message, added create_resource return value

* try fix add protocol

* fix resource_add

* 修复移液站错误的aspirate注册表

* Feature/xprbalance-zhida (#80)

* feat(devices): add Zhida GC/MS pretreatment automation workstation

* feat(devices): add mettler_toledo xpr balance

* balance

* 重新补全zhida注册表

* PRCXI9320 json

* PRCXI9320 json

* PRCXI9320 json

* fix resource download

* remove class for resource

* bump version to 0.10.6

* 更新所有注册表

* 修复protocolnode的兼容性

* 修复protocolnode的兼容性

* Update install md

* Add Defaultlayout

* 更新物料接口

* fix dict to tree/nested-dict converter

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* frontend_docs

* create/update resources with POST/PUT for big amount/ small amount data

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

* Workstation templates: Resources and its CRUD, and workstation tasks (#95)

* coin_cell_station draft

* refactor: rename "station_resource" to "deck"

* add standardized BIOYOND resources: bottle_carrier, bottle

* refactor and add BIOYOND resources tests

* add BIOYOND deck assignment and pass all tests

* fix: update resource with correct structure; remove deprecated liquid_handler set_group action

* feat: 将新威电池测试系统驱动与配置文件并入 workstation_dev_YB2 (#92)

* feat: 新威电池测试系统驱动与注册文件

* feat: bring neware driver & battery.json into workstation_dev_YB2

* add bioyond studio draft

* bioyond station with communication init and resource sync

* fix bioyond station and registry

* create/update resources with POST/PUT for big amount/ small amount data

* refactor: add itemized_carrier instead of carrier consists of ResourceHolder

* create warehouse by factory func

* update bioyond launch json

* add child_size for itemized_carrier

* fix bioyond resource io

---------

Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>

* 更新物料接口

* Workstation dev yb2 (#100)

* Refactor and extend reaction station action messages

* Refactor dispensing station tasks to enhance parameter clarity and add batch processing capabilities

- Updated `create_90_10_vial_feeding_task` to include detailed parameters for 90%/10% vial feeding, improving clarity and usability.
- Introduced `create_batch_90_10_vial_feeding_task` for batch processing of 90%/10% vial feeding tasks with JSON formatted input.
- Added `create_batch_diamine_solution_task` for batch preparation of diamine solution, also utilizing JSON formatted input.
- Refined `create_diamine_solution_task` to include additional parameters for better task configuration.
- Enhanced schema descriptions and default values for improved user guidance.

* 修复to_plr_resources

* add update remove

* 支持选择器注册表自动生成
支持转运物料

* 修复资源添加

* 修复transfer_resource_to_another生成

* 更新transfer_resource_to_another参数,支持spot入参

* 新增test_resource动作

* fix host_node error

* fix host_node test_resource error

* fix host_node test_resource error

* 过滤本地动作

* 移动内部action以兼容host node

* 修复同步任务报错不显示的bug

* feat: 允许返回非本节点物料,后面可以通过decoration进行区分,就不进行warning了

* update todo

* modify bioyond/plr converter, bioyond resource registry, and tests

* pass the tests

* update todo

* add conda-pack-build.yml

* add auto install script for conda-pack-build.yml

(cherry picked from commit 172599adcf)

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* update conda-pack-build.yml

* Add version in __init__.py
Update conda-pack-build.yml
Add create_zip_archive.py

* Update conda-pack-build.yml

* Update conda-pack-build.yml (with mamba)

* Update conda-pack-build.yml

* Fix FileNotFoundError

* Try fix 'charmap' codec can't encode characters in position 16-23: character maps to <undefined>

* Fix unilabos msgs search error

* Fix environment_check.py

* Update recipe.yaml

* Update registry. Update uuid loop figure method. Update install docs.

* Fix nested conda pack

* Fix one-key installation path error

* Bump version to 0.10.7

* Workshop bj (#99)

* Add LaiYu Liquid device integration and tests

Introduce LaiYu Liquid device implementation, including backend, controllers, drivers, configuration, and resource files. Add hardware connection, tip pickup, and simplified test scripts, as well as experiment and registry configuration for LaiYu Liquid. Documentation and .gitignore for the device are also included.

* feat(LaiYu_Liquid): 重构设备模块结构并添加硬件文档

refactor: 重新组织LaiYu_Liquid模块目录结构
docs: 添加SOPA移液器和步进电机控制指令文档
fix: 修正设备配置中的最大体积默认值
test: 新增工作台配置测试用例
chore: 删除过时的测试脚本和配置文件

* add

* 重构: 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py 并更新所有导入引用

- 使用 git mv 将 LaiYu_Liquid.py 重命名为 laiyu_liquid_main.py
- 更新所有相关文件中的导入引用
- 保持代码功能不变,仅改善命名一致性
- 测试确认所有导入正常工作

* 修复: 在 core/__init__.py 中添加 LaiYuLiquidBackend 导出

- 添加 LaiYuLiquidBackend 到导入列表
- 添加 LaiYuLiquidBackend 到 __all__ 导出列表
- 确保所有主要类都可以正确导入

* 修复大小写文件夹名字

* 电池装配工站二次开发教程(带目录)上传至dev (#94)

* 电池装配工站二次开发教程

* Update intro.md

* 物料教程

* 更新物料教程,json格式注释

* Update prcxi driver & fix transfer_liquid mix_times (#90)

* Update prcxi driver & fix transfer_liquid mix_times

* fix: correct mix_times type

* Update liquid_handler registry

* test: prcxi.py

* Update registry from pr

* fix ony-key script not exist

* clean files

---------

Co-authored-by: Junhan Chang <changjh@dp.tech>
Co-authored-by: ZiWei <131428629+ZiWei09@users.noreply.github.com>
Co-authored-by: Guangxin Zhang <guangxin.zhang.bio@gmail.com>
Co-authored-by: Xie Qiming <97236197+Andy6M@users.noreply.github.com>
Co-authored-by: h840473807 <47357934+h840473807@users.noreply.github.com>
Co-authored-by: LccLink <1951855008@qq.com>
Co-authored-by: lixinyu1011 <61094742+lixinyu1011@users.noreply.github.com>
Co-authored-by: shiyubo0410 <shiyubo@dp.tech>
2025-10-12 23:34:26 +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 %}