Retire-Cluster 采用 CLI 优先的 Web 界面设计,在通过终端风格美学保持出色的人机可用性的同时,优先考虑机器可读性和 AI 可访问性。
┌─────────────────────────────────────┐
│ 展示层 │
├─────────────┬───────────────────────┤
│ Web终端 │ CLI/文本访问 │
│ (xterm.js) │ (w3m, lynx, curl) │
├─────────────┼───────────────────────┤
│ /web/* │ /cli/* │
│ (富文本UI) │ (纯文本) │
├─────────────┴───────────────────────┤
│ API层 │
│ /api/v1/* (JSON) │
│ /text/* (纯文本) │
│ /stream/* (SSE/WebSocket) │
└─────────────────────────────────────┘
| 路径模式 | 内容类型 | 用途 | 示例 |
|---|---|---|---|
/api/v1/* |
application/json |
编程访问 | /api/v1/devices |
/text/* |
text/plain |
CLI/AI访问 | /text/devices |
/cli/* |
text/html |
终端UI | /cli/dashboard |
/stream/* |
text/event-stream |
实时更新 | /stream/logs |
<动词> <名词> [选项] [参数]
示例:
list devices --status=onlineshow device android-001submit task --type=echo --payload="test"monitor logs --device=laptop-002# 帮助和导航
help [command] # 显示命令帮助
clear # 清除屏幕
exit # 退出界面
# 集群管理
cluster status # 显示集群状态
cluster health # 健康检查
cluster metrics # 性能指标
cluster config # 配置
# 设备操作
devices list [--status=] [--role=] # 列出设备
devices show <id> # 显示设备详细信息
devices ping <id> # ping设备
devices remove <id> # 移除设备
devices monitor # 实时监控
# 任务管理
tasks submit <type> [--payload=] # 提交任务
tasks list [--status=] [--device=] # 列出任务
tasks show <id> # 显示任务详细信息
tasks cancel <id> # 取消任务
tasks retry <id> # 重试任务
tasks monitor # 监控任务
# 监控
monitor devices # 设备状态流
monitor tasks # 任务执行流
monitor logs # 日志流
monitor metrics # 指标流
# 导出
export devices --format=csv # 导出设备到CSV
export tasks --format=json # 导出任务到JSON
export logs --since=1h # 导出最近1小时的日志
export config # 导出配置
核心功能:
实现特性:
// 终端配置
const terminal = new Terminal({
theme: {
background: '#000000',
foreground: '#00ff00', // 绿色文本(Matrix主题)
},
fontSize: 14,
fontFamily: 'Consolas, Monaco, monospace',
cursorBlink: true,
allowTransparency: true
});
// 自动补全
terminal.onData((input) => {
if (input === '\t') {
// 触发命令补全
requestCompletion(currentLine);
}
});
ASCII艺术界面:
┌─────────────────────────────────────────────────────────────┐
│ RETIRE-CLUSTER v1.1.0 │
├─────────────────────────────────────────────────────────────┤
│ 集群状态: HEALTHY │ 在线设备: 5/7 │ 活动任务: 12 │
│ 运行时间: 15d 6h │ CPU使用: 37% │ 内存使用: 42% │
├─────────────────────────────────────────────────────────────┤
│ 设备网格 │
│ ┌─────┬─────┬─────┬─────┐ │
│ │ ✓01 │ ✓02 │ ✗03 │ ✓04 │ ✓ 在线 ✗ 离线 ⚠ 警告 │
│ │ ✓05 │ ⚠06 │ ✓07 │ --- │ │
│ └─────┴─────┴─────┴─────┘ │
├─────────────────────────────────────────────────────────────┤
│ 最近活动 │
│ [10:30:15] INFO: 设备 android-001 已连接 │
│ [10:30:16] ERROR: 任务 task-abc123 执行失败 │
│ [10:30:17] WARNING: 设备 raspi-003 心跳超时 │
└─────────────────────────────────────────────────────────────┘
表格格式(人类可读):
设备ID 状态 CPU 内存 任务 角色 平台 运行时间
─────────────────────────────────────────────────────────────────
android-001 在线 42% 2.1GB 3 mobile android 2d 14h 32m
laptop-002 在线 15% 4.5GB 1 compute linux 5d 08h 15m
raspi-003 离线 0% 0GB 0 storage linux 0m
管道分隔格式(机器可读):
android-001|online|42|2.1|3|mobile|android|2d14h32m
laptop-002|online|15|4.5|1|compute|linux|5d08h15m
raspi-003|offline|0|0|0|storage|linux|0m
实时任务流:
任务ID 状态 设备 类型 进度 运行时间
─────────────────────────────────────────────────────────────────
task-abc123 运行中 android-001 python_eval [████████░░] 80% 00:02:15
task-def456 排队 - echo [░░░░░░░░░░] 0% 00:00:00
task-ghi789 完成 laptop-002 system_info [██████████] 100% 00:00:03
彩色日志输出:
# 实时日志流
logs follow
# 输出示例
[2024-01-15 10:30:15] INFO 主节点启动成功 (端口: 8080)
[2024-01-15 10:30:16] DEBUG 等待设备连接...
[2024-01-15 10:30:17] INFO 设备注册: android-001 (8核, 8GB内存)
[2024-01-15 10:30:18] WARN 设备 raspi-003 心跳延迟 (延迟: 120s)
[2024-01-15 10:30:19] ERROR 任务执行失败: task-abc123 (原因: 超时)
┌─────────────────────────────────────────────────────────────┐
│ Retire-Cluster Dashboard [≡] [◐] [✕] │
├─────────────────────────────────────────────────────────────┤
│ ┌───────────────┐ ┌─────────────────────────────────────────┐ │
│ │ 侧边栏 │ │ 主终端区域 │ │
│ │ │ │ │ │
│ │ • 仪表板 │ │ cluster@main:~$ devices list │ │
│ │ • 设备管理 │ │ 设备ID 状态 CPU 内存 │ │
│ │ • 任务管理 │ │ ───────────────────────────────────── │ │
│ │ • 监控 │ │ android-001 在线 42% 2.1GB │ │
│ │ • 日志 │ │ laptop-002 在线 15% 4.5GB │ │
│ │ • 设置 │ │ raspi-003 离线 0% 0GB │ │
│ │ │ │ │ │
│ │ │ │ cluster@main:~$ █ │ │
│ └───────────────┘ └─────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 状态栏: 5设备在线 | 12个活动任务 | CPU: 37% | 内存: 42% │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────┐
│ [☰] Retire-Cluster [⚙] │
├─────────────────────────────┤
│ │
│ cluster@main:~$ devices │
│ │
│ android-001 在线 42% │
│ laptop-002 在线 15% │
│ raspi-003 离线 0% │
│ │
│ cluster@main:~$ █ │
│ │
├─────────────────────────────┤
│ [仪表板] [设备] [任务] [日志] │
└─────────────────────────────┘
:root {
--bg-primary: #000000;
--bg-secondary: #001100;
--text-primary: #00ff00;
--text-secondary: #00aa00;
--accent: #00ff41;
--warning: #ffff00;
--error: #ff0000;
--border: #004400;
}
:root {
--bg-primary: #1a0f00;
--bg-secondary: #2a1500;
--text-primary: #ffb000;
--text-secondary: #cc8800;
--accent: #ffd700;
--warning: #ff8800;
--error: #ff4400;
--border: #664400;
}
:root {
--bg-primary: #001122;
--bg-secondary: #002244;
--text-primary: #00aaff;
--text-secondary: #0088cc;
--accent: #00ccff;
--warning: #ffaa00;
--error: #ff4444;
--border: #004466;
}
设备列表:
# 默认格式(管道分隔)
GET /text/devices
Content-Type: text/plain
android-001|online|42|2.1|3
laptop-002|online|15|4.5|1
raspi-003|offline|0|0|0
# CSV格式
GET /text/devices
Accept: text/csv
id,status,cpu,memory,tasks
android-001,online,42,2.1,3
laptop-002,online,15,4.5,1
raspi-003,offline,0,0,0
# TSV格式
GET /text/devices
Accept: text/tab-separated-values
id status cpu memory tasks
android-001 online 42 2.1 3
laptop-002 online 15 4.5 1
raspi-003 offline 0 0 0
集群状态:
GET /text/status
Content-Type: text/plain
STATUS: healthy
NODES: 2/3 online
CPU: 48 cores (37% utilized)
MEMORY: 128GB (42% used)
TASKS: 12 active, 45 completed
UPTIME: 15d 6h 42m
Prometheus指标:
GET /text/metrics
Content-Type: text/plain
# HELP cluster_devices_total 集群中的总设备数
# TYPE cluster_devices_total gauge
cluster_devices_total 3
# HELP cluster_devices_online 在线设备数
# TYPE cluster_devices_online gauge
cluster_devices_online 2
# HELP cluster_cpu_usage_percent CPU使用率百分比
# TYPE cluster_cpu_usage_percent gauge
cluster_cpu_usage_percent 37
# HELP cluster_memory_usage_percent 内存使用率百分比
# TYPE cluster_memory_usage_percent gauge
cluster_memory_usage_percent 42
设备状态流:
GET /stream/devices
Accept: text/event-stream
data: {"event": "device_update", "device_id": "android-001", "status": "online", "cpu": 42}
data: {"event": "device_offline", "device_id": "raspi-003", "timestamp": "2024-01-15T10:30:17Z"}
data: {"event": "device_online", "device_id": "raspi-003", "timestamp": "2024-01-15T10:35:22Z"}
日志流:
GET /stream/logs
Accept: text/event-stream
data: {"timestamp": "2024-01-15T10:30:15Z", "level": "INFO", "message": "设备 android-001 已连接"}
data: {"timestamp": "2024-01-15T10:30:16Z", "level": "ERROR", "message": "任务 task-abc123 执行失败"}
全局快捷键:
Ctrl+/: 显示帮助Ctrl+K: 命令面板Ctrl+L: 清除屏幕Esc: 取消当前操作Tab: 命令补全↑/↓: 命令历史导航快捷键:
Alt+1: 仪表板Alt+2: 设备管理Alt+3: 任务管理Alt+4: 监控Alt+5: 日志<!-- 语义化HTML结构 -->
<main role="main" aria-label="集群管理控制台">
<section aria-label="设备状态">
<h2>设备状态</h2>
<table role="grid" aria-label="设备列表">
<thead>
<tr role="row">
<th role="columnheader">设备ID</th>
<th role="columnheader">状态</th>
</tr>
</thead>
<tbody>
<tr role="row" aria-describedby="device-android-001-status">
<td role="gridcell">android-001</td>
<td role="gridcell" id="device-android-001-status">在线</td>
</tr>
</tbody>
</table>
</section>
</main>
@media (prefers-contrast: high) {
:root {
--bg-primary: #000000;
--text-primary: #ffffff;
--accent: #ffff00;
--border: #ffffff;
}
}
对于大型数据集(>1000项),实现虚拟滚动:
class VirtualScrollList {
constructor(container, itemHeight, data) {
this.container = container;
this.itemHeight = itemHeight;
this.data = data;
this.visibleStart = 0;
this.visibleEnd = 0;
this.init();
}
init() {
this.container.style.height = `${this.data.length * this.itemHeight}px`;
this.container.addEventListener('scroll', this.onScroll.bind(this));
this.render();
}
onScroll() {
const scrollTop = this.container.scrollTop;
this.visibleStart = Math.floor(scrollTop / this.itemHeight);
this.visibleEnd = Math.min(
this.visibleStart + Math.ceil(this.container.clientHeight / this.itemHeight),
this.data.length
);
this.render();
}
render() {
// 只渲染可见项目
const visibleItems = this.data.slice(this.visibleStart, this.visibleEnd);
// 渲染逻辑...
}
}
class DataCache {
constructor(ttl = 30000) { // 30秒TTL
this.cache = new Map();
this.ttl = ttl;
}
set(key, data) {
this.cache.set(key, {
data,
timestamp: Date.now()
});
}
get(key) {
const item = this.cache.get(key);
if (!item) return null;
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key);
return null;
}
return item.data;
}
}
// 命令解析器测试
describe('CommandParser', () => {
test('解析简单命令', () => {
const parser = new CommandParser();
const result = parser.parse('devices list');
expect(result).toEqual({
verb: 'devices',
noun: 'list',
options: {},
arguments: []
});
});
test('解析带选项的命令', () => {
const parser = new CommandParser();
const result = parser.parse('devices list --status=online');
expect(result.options).toEqual({ status: 'online' });
});
});
// API集成测试
describe('API Integration', () => {
test('设备列表端点', async () => {
const response = await fetch('/text/devices');
expect(response.status).toBe(200);
expect(response.headers.get('content-type')).toBe('text/plain');
const text = await response.text();
const lines = text.trim().split('\n');
expect(lines.length).toBeGreaterThan(0);
});
});
// Playwright E2E测试
test('完整工作流程', async ({ page }) => {
await page.goto('/cli');
// 等待终端加载
await page.waitForSelector('.xterm-screen');
// 输入命令
await page.keyboard.type('devices list');
await page.keyboard.press('Enter');
// 验证输出
await expect(page.locator('.xterm-screen')).toContainText('android-001');
});
class CommandValidator {
static validate(command) {
// 防止命令注入
const dangerous = /[;&|`$(){}[\]]/;
if (dangerous.test(command)) {
throw new Error('不安全的命令字符');
}
// 限制命令长度
if (command.length > 1000) {
throw new Error('命令过长');
}
return true;
}
}
// CSRF令牌验证
app.use((req, res, next) => {
if (req.method === 'POST') {
const token = req.headers['x-csrf-token'];
if (!validateCSRFToken(token)) {
return res.status(403).json({ error: 'CSRF令牌无效' });
}
}
next();
});
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15分钟
max: 100, // 限制每个IP 100次请求
message: '请求过于频繁,请稍后再试'
});
app.use('/api/', limiter);
# Nginx配置示例
location /static/ {
expires 1y;
add_header Cache-Control "public, immutable";
gzip_static on;
}
location /cli/ {
try_files $uri $uri/ /cli/index.html;
}
<!-- 使用CDN加速静态资源 -->
<script src="https://cdn.jsdelivr.net/npm/xterm@5.0.0/lib/xterm.min.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm@5.0.0/css/xterm.css">
// 性能监控
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.entryType === 'navigation') {
console.log('页面加载时间:', entry.loadEventEnd - entry.loadEventStart);
}
});
});
observer.observe({ entryTypes: ['navigation', 'resource'] });
这个设计文档为Retire-Cluster的Web界面提供了全面的指导,确保既满足人类用户的需求,又对AI和自动化系统友好。通过CLI优先的方法,我们创建了一个真正通用的管理界面。