- md 页签从裸 textarea 升级 CodeMirror 6:markdown 语法高亮、行内历史,粘贴/拖图直传; markdown 仍是唯一真相源,转存/切换用 syncCM 全量灌回,工具栏选区辅助改为面向 CM - POST /api/admin/files/import:外链抓取复用 linkmeta SSRF 防护拨号与跳转限制, 二进制传输放宽 30s;类型按内容嗅探反查白名单,与手动上传共用去重/落盘(persistFile 抽取共享) - 上传走 XHR 进度回调 + 编辑器 toast 栈:类型/大小前置校验(与后端白名单一致)、逐文件进度 - persistFile 去重路径补 URL 解析——此前命中去重返回的行没有 URL,转存替换会把正文图链清空(已修复受损数据) - hub nil 安全(测试环境未装配时不 panic)
58 lines
1.4 KiB
Go
58 lines
1.4 KiB
Go
// Package hub 是一个极简的进程内发布/订阅:按主题(文章 ID)广播
|
|
// 「有变更」信号。订阅者只拿到一个空 struct 信号——不携带内容,
|
|
// 消费方(SSE 前端)收到后自己 refetch,未审核的评论内容不会经由
|
|
// 这条通道外泄。
|
|
package hub
|
|
|
|
import "sync"
|
|
|
|
type Hub struct {
|
|
mu sync.Mutex
|
|
subs map[int64]map[chan struct{}]struct{}
|
|
}
|
|
|
|
func New() *Hub {
|
|
return &Hub{subs: make(map[int64]map[chan struct{}]struct{})}
|
|
}
|
|
|
|
// Subscribe 订阅某主题;返回信号 channel 和退订函数。
|
|
// channel 容量 1:订阅者处理不过来时新信号直接丢弃(合并刷新)。
|
|
func (h *Hub) Subscribe(topic int64) (<-chan struct{}, func()) {
|
|
// nil hub(测试/未装配场景)静默降级:永远收不到信号的通道
|
|
if h == nil {
|
|
ch := make(chan struct{})
|
|
return ch, func() {}
|
|
}
|
|
ch := make(chan struct{}, 1)
|
|
h.mu.Lock()
|
|
if h.subs[topic] == nil {
|
|
h.subs[topic] = make(map[chan struct{}]struct{})
|
|
}
|
|
h.subs[topic][ch] = struct{}{}
|
|
h.mu.Unlock()
|
|
off := func() {
|
|
h.mu.Lock()
|
|
delete(h.subs[topic], ch)
|
|
if len(h.subs[topic]) == 0 {
|
|
delete(h.subs, topic)
|
|
}
|
|
h.mu.Unlock()
|
|
}
|
|
return ch, off
|
|
}
|
|
|
|
// Broadcast 唤醒某主题的全部订阅者;积压的订阅者不阻塞。
|
|
func (h *Hub) Broadcast(topic int64) {
|
|
if h == nil {
|
|
return
|
|
}
|
|
h.mu.Lock()
|
|
defer h.mu.Unlock()
|
|
for ch := range h.subs[topic] {
|
|
select {
|
|
case ch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
}
|