feat(backthink/admin): ③b — rag/delete 真实现 + D1 推送出口钩子(自建优先→个推兜底) + api-reference 补 D2/D3/D5 补丁
- B5: POST /admin/rag/delete 真做(DeleteDocument→DeleteProject 级联删章节+索引清理, owner+审计, 200/404/500, 500降级兜底); users/iot scenarios+rules 诚实501(无用户表/IoT场景规则模型, 消息写明缺什么, 不伪造), 锁住 501+not_implemented
- D1: backthink/push_outlet.go PushExporter{Export}; pushOutlet 自建HTTP(2xx=送达, token仅Authorization头+error脱敏)优先→Getui兜底(未接入, 诚实日志'getui not integrated'+(false,nil)); backthink.Deps.Exporter → pusher.withExporter, Push 在WS/IM后 exportAsync(detached+5s超时, 失败仅日志, 不破坏现有投递, 限流不上报); config PushOutletConfig(PUSH_EXPORTER_* + GETUI_*占位)
- 前端 Rag.tsx/api.ts 删除后 reload + 类型; Docs/api-client v0.31 + api-reference(#23 owner/200-404, §8.4, 端点表PATCH/DELETE messages + pinned + messages?q=)
- 测试(push_outlet/admin_rag_delete/admin_honest501/knowledge store), 后端+前端build全绿+3平台交叉编译
- 刻意: Getui SDK 未接入(无SDK不硬造), 主人自建服务填 PUSH_EXPORTER_URL/TOKEN 即可
This commit is contained in:
1 parent
844b2a74c1
commit
2dee372fb2
23 files changed
+927
-95
No files matched your search
@@ -128,20 +128,34 @@ type Pusher interface {
|
||||
|
||||
// pusher is the concrete Pusher: it consults the gate, then routes the message to
|
||||
// exactly one target — the active client for a "web" hint, otherwise the IM bridge.
|
||||
// After the local delivery it mirrors the message to the optional push outlet
|
||||
// (self-hosted service → Getui fallback) without blocking that delivery.
|
||||
type pusher struct {
|
||||
active ActiveClient
|
||||
im IMAdapter
|
||||
gate PushGate
|
||||
log *slog.Logger
|
||||
active ActiveClient
|
||||
im IMAdapter
|
||||
gate PushGate
|
||||
exporter PushExporter
|
||||
log *slog.Logger
|
||||
// exportDone, when non-nil, receives one value after each asynchronous export
|
||||
// completes. It exists so tests can deterministically observe the hook; the
|
||||
// send is non-blocking, so a full channel never stalls the exporter.
|
||||
exportDone chan struct{}
|
||||
}
|
||||
|
||||
// newPusher builds a Pusher from the optional active-client sink, IM sink and
|
||||
// rate gate. Any of them may be nil; a nil target simply makes that route
|
||||
// unavailable.
|
||||
// unavailable. The push outlet is attached separately with withExporter.
|
||||
func newPusher(active ActiveClient, im IMAdapter, gate PushGate, log *slog.Logger) *pusher {
|
||||
return &pusher{active: active, im: im, gate: gate, log: log}
|
||||
}
|
||||
|
||||
// withExporter attaches the push outlet hook and returns the pusher for fluent
|
||||
// use. A nil exporter leaves the hook disabled.
|
||||
func (p *pusher) withExporter(exporter PushExporter) *pusher {
|
||||
p.exporter = exporter
|
||||
return p
|
||||
}
|
||||
|
||||
// Push implements Pusher: it applies the rate gate, then routes the message to a
|
||||
// single target. A "web" hint reaches the active web/native client; every other
|
||||
// hint ("im", "auto", or a concrete platform) goes over the IM bridge once, where
|
||||
@@ -192,6 +206,11 @@ func (p *pusher) Push(ctx context.Context, req PushRequest) PushResult {
|
||||
if res.Pushed && p.gate != nil && !eventDriven {
|
||||
p.gate.RecordPush(kind)
|
||||
}
|
||||
// D1: mirror the proactive message to the external push outlet (self-hosted
|
||||
// unified push service → Getui fallback). Fire-and-forget: a slow or failing
|
||||
// outlet never delays or breaks the local delivery above, and a rate-limited
|
||||
// push returned before reaching here.
|
||||
p.exportAsync(msg)
|
||||
res = annotateHint(res, req.ChannelHint)
|
||||
if eventDriven {
|
||||
res.Bypassed = true
|
||||
@@ -200,6 +219,33 @@ func (p *pusher) Push(ctx context.Context, req PushRequest) PushResult {
|
||||
return res
|
||||
}
|
||||
|
||||
// exportAsync reports msg to the push outlet on a detached, bounded goroutine so
|
||||
// the proactive path never blocks on the outlet. A nil exporter (hook disabled)
|
||||
// or a nil outlet result is a no-op; a delivery miss or error is only logged.
|
||||
func (p *pusher) exportAsync(msg adapter.UnifiedMessage) {
|
||||
if p.exporter == nil {
|
||||
return
|
||||
}
|
||||
timeout := defaultExportTimeout
|
||||
go func() {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
defer cancel()
|
||||
delivered, err := p.exporter.Export(ctx, msg)
|
||||
switch {
|
||||
case err != nil:
|
||||
p.log.Warn("push outlet export failed", "error", err)
|
||||
case !delivered:
|
||||
p.log.Info("push outlet did not deliver", "hint", "getui fallback not integrated")
|
||||
}
|
||||
if p.exportDone != nil {
|
||||
select {
|
||||
case p.exportDone <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// pushActive delivers only to the active web/native client.
|
||||
func (p *pusher) pushActive(ctx context.Context, msg adapter.UnifiedMessage) PushResult {
|
||||
if p.active != nil {
|
||||
|
||||
Reference in new issue
Block a user