- Go gin 后端:流水线/节点/连线/运行/日志/GitOps/插件 REST API, SQLite(GORM) 自动建表 + seed,模拟执行引擎 + SSE 实时日志推送, 前端产物 go:embed 嵌入单二进制,一个端口同时服务 API 与页面 - React 19 前端:画布式流水线编辑器(React Flow + Tekton 任务库)、 GitOps 部署、批量发布、发布记录等 14 个模块页面, tRPC 全部替换为 REST(TanStack Query) - 首次启动生成随机密码管理员账号(日志 + data/initial-admin-password.txt) - run.sh 一键构建启动(默认端口 9527,支持 start/stop/restart/build/dev)
57 lines
1.1 KiB
Go
57 lines
1.1 KiB
Go
package handler
|
||
|
||
import (
|
||
"encoding/json"
|
||
"fmt"
|
||
"sync"
|
||
)
|
||
|
||
// Hub 按 runId 分组的 SSE 订阅中心
|
||
type Hub struct {
|
||
mu sync.Mutex
|
||
subs map[string]map[chan []byte]struct{}
|
||
}
|
||
|
||
func NewHub() *Hub {
|
||
return &Hub{subs: make(map[string]map[chan []byte]struct{})}
|
||
}
|
||
|
||
func (h *Hub) Subscribe(runId string) chan []byte {
|
||
ch := make(chan []byte, 64)
|
||
h.mu.Lock()
|
||
defer h.mu.Unlock()
|
||
if h.subs[runId] == nil {
|
||
h.subs[runId] = make(map[chan []byte]struct{})
|
||
}
|
||
h.subs[runId][ch] = struct{}{}
|
||
return ch
|
||
}
|
||
|
||
func (h *Hub) Unsubscribe(runId string, ch chan []byte) {
|
||
h.mu.Lock()
|
||
defer h.mu.Unlock()
|
||
if set := h.subs[runId]; set != nil {
|
||
delete(set, ch)
|
||
if len(set) == 0 {
|
||
delete(h.subs, runId)
|
||
}
|
||
}
|
||
}
|
||
|
||
// Publish 向 runId 的所有订阅者推送一条 SSE 帧(event + data)
|
||
func (h *Hub) Publish(runId, event string, payload any) {
|
||
data, err := json.Marshal(payload)
|
||
if err != nil {
|
||
return
|
||
}
|
||
frame := []byte(fmt.Sprintf("event: %s\ndata: %s\n\n", event, data))
|
||
h.mu.Lock()
|
||
defer h.mu.Unlock()
|
||
for ch := range h.subs[runId] {
|
||
select {
|
||
case ch <- frame:
|
||
default: // 订阅者消费过慢则丢弃,避免阻塞模拟引擎
|
||
}
|
||
}
|
||
}
|