显示 API:自发现可视化系统
概述
显示 API 提供了基于插件的自发现可视化架构。与在 web_user 和 tui 中硬编码显示逻辑不同,插件现在可以注册自定义"显示提供程序",定义如何提取和格式化数据以进行可视化。
主要好处:
- 无硬编码 - 插件定义自己的显示部分
- 自动发现 - web_user 和 tui 自动发现所有已注册的提供程序
- 多种格式 - 每个提供程序都可以支持 JSON、HTML、文本、表格或树形输出
- 可扩展 - 新插件可注册新显示部分,无需修改核心代码
- 统一 API - Web 和终端 UI 的单一接口
架构
核心组件
-
DisplayProvider(抽象基类)
- 插件通过子类化来提供自定义显示部分
- 实现
extract_data()以获取相关信息 - 提供格式方法:
format_json()、format_html()、format_text()、format_table()、format_tree()
-
DisplayRegistry(全局单例)
- 维护所有已注册显示提供程序的注册表
- 支持按类别和优先级过滤
- 线程安全操作
-
web_user 集成
- 新 HTTP 端点:
/api/display/providers、/api/display/render/{section}、/api/display/all - 自动调用显示提供程序以构建仪表板
- 通过查询参数支持多种输出格式
- 新 HTTP 端点:
-
tui 集成
- 动态节列表,包含内置和提供程序节
- 新命令:
[m]用于元数据,[s]用于搜索 - 无缝将提供程序输出集成到 BIOS 控制台
创建显示提供程序插件
最小示例
from opensynaptic.services.display_api import DisplayProvider, register_display_provider
class MyCustomDisplayProvider(DisplayProvider):
"""自定义显示提供程序。"""
def __init__(self):
super().__init__(
plugin_name='my_plugin',
section_id='custom_metrics',
display_name='自定义指标仪表板'
)
self.category = 'custom'
self.priority = 75
self.refresh_interval_s = 5.0
def extract_data(self, node=None, **kwargs):
"""从节点提取数据。"""
return {
'total_items': 42,
'active': True,
'timestamp': int(__import__('time').time()),
}
def auto_load(config=None):
"""插件自动加载函数。"""
register_display_provider(MyCustomDisplayProvider())
return True
完整示例,包含自定义格式化
from opensynaptic.services.display_api import DisplayProvider, register_display_provider
class MetricsDisplayProvider(DisplayProvider):
def __init__(self):
super().__init__(
plugin_name='metrics_plugin',
section_id='system_metrics',
display_name='系统指标'
)
self.category = 'metrics'
self.priority = 80
def extract_data(self, node=None, **kwargs):
# 自定义数据提取逻辑
return {
'cpu_usage': 45.2,
'memory_usage': 78.5,
'network_io': 1024,
'disk_io': 512,
}
def format_html(self, data):
"""自定义 HTML 渲染。"""
return f"""
<div style="padding: 15px; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border-radius: 8px; color: white;">
<h2>系统指标</h2>
<table style="width: 100%; color: white;">
<tr><td>CPU</td><td>{data['cpu_usage']:.1f}%</td></tr>
<tr><td>内存</td><td>{data['memory_usage']:.1f}%</td></tr>
<tr><td>网络 I/O</td><td>{data['network_io']} 字节/秒</td></tr>
<tr><td>磁盘 I/O</td><td>{data['disk_io']} 字节/秒</td></tr>
</table>
</div>
"""
def format_text(self, data):
"""为终端提供自定义文本渲染。"""
lines = [
"╔════════════════════════════════╗",
"║ 系统指标 ║",
"╠════════════════════════════════╣",
f"║ CPU 使用率: {data['cpu_usage']:>6.1f}% ║",
f"║ 内存使用率: {data['memory_usage']:>6.1f}% ║",
f"║ 网络 I/O: {data['network_io']:>6} 字节/秒 ║",
f"║ 磁盘 I/O: {data['disk_io']:>6} 字节/秒 ║",
"╚════════════════════════════════╝"
]
return "\n".join(lines)
def auto_load(config=None):
register_display_provider(MetricsDisplayProvider())
return True
API 参考
DisplayProvider 类
构造函数
DisplayProvider(plugin_name: str, section_id: str, display_name: str = None)
属性:
plugin_name- 插件名称(例如 'id_allocator'、'test_plugin')section_id- 插件内的唯一部分标识符(例如 'metrics'、'status')display_name- 人类可读的显示名称(默认为 section_id)category- 分类标签(默认:'plugin')priority- 显示优先级 0-100,数值越高越优先显示(默认:50)refresh_interval_s- 建议的刷新时间间隔(默认:2.0)
抽象方法
extract_data(self, node=None, **kwargs) -> Dict[str, Any]
必须由子类实现。返回包含提取数据的字典。
格式化方法
所有格式化方法都接收 extract_data() 返回的字典,并返回格式化的输出。
def format_json(self, data: Dict) -> Dict:
"""返回 JSON 可序列化字典。默认:原样返回数据。"""
return data
def format_html(self, data: Dict) -> str:
"""返回 HTML 字符串。默认:生成基本表格。"""
return "<table>...</table>"
def format_text(self, data: Dict) -> str:
"""返回纯文本。默认:美化打印 JSON。"""
return "key: value\n..."
def format_table(self, data: Dict) -> List[Dict]:
"""返回表格数据(行)。默认:将数据包装在列表中。"""
return [data]
def format_tree(self, data: Dict) -> Dict:
"""返回分层结构。默认:原样返回数据。"""
return data
def supports_format(self, fmt: DisplayFormat) -> bool:
"""检查是否支持格式。默认:支持所有。"""
return True
注册表函数
from opensynaptic.services.display_api import (
get_display_registry,
register_display_provider,
render_section,
collect_all_sections,
DisplayFormat,
)
# 注册提供程序
register_display_provider(MyProvider())
# 获取全局注册表
registry = get_display_registry()
# 获取提供程序元数据
metadata = registry.get_metadata()
metadata = registry.get_metadata(plugin_name='my_plugin')
# 列出提供程序
providers = registry.list_all() # 按优先级排序
providers = registry.list_by_category('metrics')
# 列出类别
categories = registry.list_categories()
# 渲染特定部分
output = render_section(
plugin_name='my_plugin',
section_id='metrics',
fmt=DisplayFormat.JSON,
node=node_instance
)
# 收集所有部分
all_data = collect_all_sections(fmt=DisplayFormat.JSON, node=node_instance)
web_user HTTP API
新端点
GET /api/display/providers
返回所有已注册显示提供程序的元数据。
响应:
{
"ok": true,
"metadata": {
"total_providers": 3,
"categories": ["core", "metrics", "transport"],
"providers": [
{
"plugin_name": "id_allocator",
"section_id": "lease_metrics",
"display_name": "ID 租赁指标",
"category": "metrics",
"priority": 80,
"refresh_interval_s": 5.0
}
]
}
}
GET /api/display/render/{plugin_name}:{section_id}?format=json
渲染特定显示部分。
查询参数:
format- 输出格式:json、html、text、table、tree(默认:json)
响应:
{
"ok": true,
"section": "my_plugin:metrics",
"format": "json",
"data": { "metric1": 42, "metric2": 100 }
}
GET /api/display/all?format=json
收集所有已注册的显示部分。
查询参数:
format- 输出格式(默认:json)
响应:
{
"ok": true,
"format": "json",
"sections": {
"core": {
"node_stats": { "device_id": "sensor-001", "assigned_id": 12345 },
"pipeline_metrics": { "cache_entries": 256 }
},
"metrics": {
"system_metrics": { "cpu": 45.2, "memory": 78.5 }
}
},
"timestamp": 1711864234
}
tui 集成
动态部分发现
tui 现在自动将显示提供程序部分包含在其部分列表中:
| OpenSynaptic BIOS 控制台(包含显示 API 提供程序)
| 内置部分:
| * 1. 配置
| 2. 传输
| 3. 管道
| 4. 插件
| 5. 数据库
| 6. 身份
| 显示 API 提供程序:
| 7. id_allocator:lease_metrics
| 8. test_plugin:performance
| 9. example_display:node_stats
新命令
[1-N]- 按编号切换到部分(包括提供程序)[m]- 显示所有提供程序的元数据[s]- 按部分名称搜索部分[a]- 显示所有部分(内置 + 提供程序)[r]- 刷新当前部分[j]- 将当前部分打印为 JSON[auto N]- 自动刷新 N 个周期[i SEC]- 设置刷新间隔[p]- 列表可用插件[h]- 显示帮助[q]- 退出
示例会话
bios> m
{
"builtin": ["config", "transport", "pipeline", "plugins", "db", "identity"],
"providers": {
"total_providers": 3,
"categories": ["metrics", "custom"],
"providers": [...]
}
}
bios> s metrics
{
"matching": [
"id_allocator:lease_metrics",
"test_plugin:performance"
]
}
bios> 7
[显示部分:id_allocator:lease_metrics]
示例:带显示提供程序的完整插件
文件:plugins/my_analytics_plugin.py
"""
具有显示提供程序的分析插件。
通过显示 API 提供自定义指标可视化。
"""
from typing import Dict, Any
from opensynaptic.services.display_api import DisplayProvider, register_display_provider
import time
class AnalyticsMetricsDisplayProvider(DisplayProvider):
"""显示分析指标。"""
def __init__(self):
super().__init__(
plugin_name='my_analytics',
section_id='metrics',
display_name='分析指标'
)
self.category = 'metrics'
self.priority = 85
self.refresh_interval_s = 5.0
def extract_data(self, node=None, **kwargs) -> Dict[str, Any]:
# 示例:从节点或任何其他来源提取
return {
'requests_total': 15234,
'requests_per_second': 42.5,
'errors': 12,
'avg_latency_ms': 145.3,
'p99_latency_ms': 523.8,
'timestamp': int(time.time()),
}
def format_html(self, data: Dict[str, Any]) -> str:
return f"""
<div style="padding: 15px; background: #f0f4f8; border-radius: 8px;">
<h3>分析</h3>
<ul>
<li><strong>总请求:</strong> {data['requests_total']}</li>
<li><strong>RPS:</strong> {data['requests_per_second']}</li>
<li><strong>错误:</strong> {data['errors']}</li>
<li><strong>平均延迟:</strong> {data['avg_latency_ms']:.1f}ms</li>
<li><strong>P99 延迟:</strong> {data['p99_latency_ms']:.1f}ms</li>
</ul>
</div>
"""
class AnalyticsStatusDisplayProvider(DisplayProvider):
"""显示分析系统状态。"""
def __init__(self):
super().__init__(
plugin_name='my_analytics',
section_id='status',
display_name='分析状态'
)
self.category = 'custom'
self.priority = 70
def extract_data(self, node=None, **kwargs) -> Dict[str, Any]:
return {
'system_status': '健康',
'backend_connected': True,
'db_connected': True,
'cache_size_mb': 512,
}
def auto_load(config=None):
"""插件加载时注册显示提供程序。"""
register_display_provider(AnalyticsMetricsDisplayProvider())
register_display_provider(AnalyticsStatusDisplayProvider())
from opensynaptic.utils import os_log
os_log.info(
'MY_ANALYTICS',
'DISPLAY_PROVIDERS_REGISTERED',
'分析显示提供程序已注册',
{'providers': ['metrics', 'status']}
)
return True
最佳实践
- 保持 extract_data() 快速 - 它在刷新期间被频繁调用
- 使用适当的类别 - 'metrics'、'core'、'transport'、'custom' 等
- 设置合理的优先级 - 较高优先级的部分优先显示
- 选择性实现格式方法 - 仅覆盖需要的内容
- 返回 JSON 可序列化数据 - JSON 格式是默认值
- 优雅处理缺失 node -
node在某些上下文中可能为 None - 使用一致的显示名称 - 使其用户友好
- 同时使用 web_user 和 tui 进行测试 - 确保输出适用于两个 UI
测试显示提供程序
命令行
# 列出所有提供程序
python -u src/main.py example_display list
# 渲染特定部分
python -u src/main.py example_display render example_display node_stats --format json
# 收集所有部分
python -u src/main.py example_display collect --format json
# 渲染为 HTML
python -u src/main.py example_display render my_plugin metrics --format html
web_user
# 查看提供程序元数据
curl http://localhost:8765/api/display/providers
# 渲染特定部分
curl http://localhost:8765/api/display/render/my_plugin:metrics?format=json
# 获取所有部分作为 HTML
curl http://localhost:8765/api/display/all?format=html
tui
# 进入 TUI 并按 'm' 查看提供程序元数据
python -u src/main.py tui interactive --section identity
# 在 BIOS 内,使用 's' 搜索部分
# 使用 '7'、'8'、'9' 等查看提供程序部分
迁移指南:从硬编码到显示 API
之前(在 tui 中硬编码)
def _section_custom(self):
return {
'metric1': get_metric1(),
'metric2': get_metric2(),
}
_SECTION_METHODS = {
...
'custom': '_section_custom',
}
之后(显示提供程序)
# 在你的插件中
class MyDisplayProvider(DisplayProvider):
def __init__(self):
super().__init__('my_plugin', 'metrics')
def extract_data(self, node=None, **kwargs):
return {
'metric1': get_metric1(),
'metric2': get_metric2(),
}
def auto_load(config=None):
register_display_provider(MyDisplayProvider())
return True
现在 tui 和 web_user 自动发现和渲染它,无需任何硬编码!
故障排除
提供程序未出现在 web_user 或 tui 中:
- 检查插件是否被加载(在
plugin-test或 tui[p]命令中可见) - 验证在
auto_load()中调用了register_display_provider() - 使用 curl 检查注册表:
GET /api/display/providers
渲染部分时出错:
- 检查
extract_data()是否不抛出异常 - 确保返回的数据是 JSON 可序列化的
- 检查日志:搜索
DISPLAY_RENDER_ERROR
HTML 格式渲染不正确:
- 在
curl中测试:GET /api/display/render/plugin:section?format=html - 验证 HTML 是否正确转义
- 检查浏览器控制台中的渲染错误
性能问题:
- 减少提供程序中的 refresh_interval_s
- 优化 extract_data() 以更快
- 考虑在提供程序内缓存数据
另请参阅
src/opensynaptic/services/display_api.py- 核心显示 API 实现src/opensynaptic/services/example_display_plugin.py- 参考示例src/opensynaptic/services/tui/main.py- tui 集成src/opensynaptic/services/web_user/main.py- web_user 集成Config.json- web_user 中的expose_sections配置