feat: add new Flowme and FlowmeChannel management with CRUD operations and UI components

This commit is contained in:
2026-02-01 03:57:20 +08:00
parent a4e17023d0
commit cc466f7bd4
12 changed files with 1117 additions and 4 deletions

214
public/sse-test.html Normal file
View File

@@ -0,0 +1,214 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>SSE 测试</title>
<style>
body {
font-family: Arial, sans-serif;
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.status {
padding: 10px;
margin: 10px 0;
border-radius: 4px;
}
.status.connected { background: #d4edda; color: #155724; }
.status.connecting { background: #fff3cd; color: #856404; }
.status.disconnected { background: #f8d7da; color: #721c24; }
.events {
border: 1px solid #ddd;
padding: 10px;
height: 400px;
overflow-y: auto;
background: #f9f9f9;
}
.event-item {
padding: 8px;
margin: 5px 0;
background: #fff;
border: 1px solid #eee;
border-radius: 4px;
}
.event-item .time {
color: #888;
font-size: 12px;
}
.event-item .data {
white-space: pre-wrap;
word-break: break-all;
}
</style>
</head>
<body>
<h1>SSE / HTTP Stream 测试</h1>
<div>
<label>URL: </label>
<input type="text" id="url" value="http://localhost:4005/root/v3/" style="width: 400px;">
<label>类型: </label>
<select id="streamType">
<option value="sse">SSE (EventSource)</option>
<option value="stream">HTTP Stream (Fetch)</option>
</select>
<button onclick="connect()">连接</button>
<button onclick="disconnect()">断开</button>
</div>
<div id="status" class="status disconnected">未连接</div>
<h3>事件日志</h3>
<div id="events" class="events"></div>
<script>
let eventSource = null;
let abortController = null;
async function connect() {
const url = document.getElementById('url').value;
const type = document.getElementById('streamType').value;
if (!url) {
alert('请输入 URL');
return;
}
disconnect();
const eventsDiv = document.getElementById('events');
eventsDiv.innerHTML = '';
updateStatus('connecting', '正在连接...');
if (type === 'sse') {
connectSSE(url);
} else {
connectStream(url);
}
}
function connectSSE(url) {
try {
eventSource = new EventSource(url);
eventSource.onopen = function() {
updateStatus('connected', '已连接 (SSE)');
addEvent('系统', 'SSE 连接已建立');
};
eventSource.onmessage = function(event) {
addEvent('消息', event.data, 'message');
};
eventSource.onerror = function(error) {
// 如果 readyState 是 CLOSED说明是后端主动关闭不重连
if (eventSource.readyState === EventSource.CLOSED) {
updateStatus('disconnected', '连接已关闭');
addEvent('系统', '后端已关闭连接,无须重连');
return;
}
// 如果是其他错误状态,显示错误信息,不自动重连
updateStatus('disconnected', '连接错误');
addEvent('错误', 'SSE 连接发生错误');
};
eventSource.addEventListener('data', function(event) {
addEvent('自定义事件', event.data, 'data');
});
} catch (e) {
updateStatus('disconnected', '连接失败: ' + e.message);
addEvent('错误', e.message);
}
}
async function connectStream(url) {
try {
abortController = new AbortController();
updateStatus('connected', '已连接 (Stream)');
addEvent('系统', 'HTTP Stream 连接已建立');
const response = await fetch(url, {
signal: abortController.signal,
headers: {
'Accept': 'text/plain, text/event-stream'
}
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) {
addEvent('系统', 'Stream 连接已关闭');
break;
}
// 解码并处理数据
buffer += decoder.decode(value, { stream: true });
// 按行处理
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (line.trim()) {
addEvent('数据', line, 'stream');
}
}
}
} catch (e) {
if (e.name !== 'AbortError') {
updateStatus('disconnected', '连接错误: ' + e.message);
addEvent('错误', e.message);
}
}
}
function disconnect() {
if (eventSource) {
eventSource.close();
eventSource = null;
}
if (abortController) {
abortController.abort();
abortController = null;
}
updateStatus('disconnected', '已断开');
addEvent('系统', '连接已关闭');
}
function updateStatus(type, message) {
const statusDiv = document.getElementById('status');
statusDiv.className = 'status ' + type;
statusDiv.textContent = message;
}
function addEvent(type, data, eventType = '') {
const eventsDiv = document.getElementById('events');
const item = document.createElement('div');
item.className = 'event-item';
const time = new Date().toLocaleTimeString();
item.innerHTML = `
<div class="time">[${time}] ${eventType ? '[' + eventType + '] ' : ''}${type}</div>
<div class="data">${escapeHtml(data)}</div>
`;
eventsDiv.insertBefore(item, eventsDiv.firstChild);
}
function escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
</script>
</body>
</html>