feat(traffic): 正文入库 + 全文搜索,删除改为 SQL 事务
流量录制不再镜像 URL 路径写文件树,改为整条 exchange 落 SQLite: - record: 元数据/头/正文写库;>256KB 的 body 才溢出到 _blobs(单层 桶、按 hash 去重),库里留 8KB 预览 + 指针。文本正文全量喂 trigram FTS 索引(二进制按 content-type 跳过),支持任意子串与中文全文搜索。 - 删除退化为一个 SQL 事务级联删四张表,host 树后台异步回收,不再在 写锁内 RemoveAll + 全树扫描——删除从数小时降到毫秒级,期间不再停摆录制。 - gcBlobs 改走 blob_refs 反查,顺带清理空桶目录;保留对旧文件树的 引用扫描与 Get 回落,历史数据免迁移仍可读、可搜(元数据)、可删。 - 新增 traffic_blob 工具与 GET /api/traffic/blob 流式下载大 body; traffic_search 增加 body_contains 全文参数。 新表均为 CREATE TABLE IF NOT EXISTS,旧库自动补建,无需 ALTER 迁移。 Claude-Session: https://claude.ai/code/session_01RtPQYJ8W1mcaace3sHWGDm
This commit is contained in:
1 parent
22ae280584
commit
fb666c03cc
4 files changed
+1174
-193
No files matched your search
@@ -717,6 +717,7 @@ func (s *Server) Handler() http.Handler {
|
||||
mux.HandleFunc("DELETE /api/traffic", s.deleteTraffic)
|
||||
mux.HandleFunc("DELETE /api/traffic/hosts", s.deleteTrafficHosts)
|
||||
mux.HandleFunc("GET /api/traffic/exchange", s.getTrafficExchange)
|
||||
mux.HandleFunc("GET /api/traffic/blob", s.getTrafficBlob)
|
||||
mux.HandleFunc("GET /api/commands", s.pgListCommands)
|
||||
mux.HandleFunc("GET /api/llm/records", s.pgListLLMRecords)
|
||||
mux.HandleFunc("DELETE /api/llm/records", s.pgDeleteLLMRecords)
|
||||
@@ -2934,6 +2935,35 @@ func (s *Server) getTrafficExchange(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, 200, map[string]any{"req": req, "resp": resp})
|
||||
}
|
||||
|
||||
// getTrafficBlob streams one oversized body by its sha256. Bodies past the inline
|
||||
// threshold are not carried by the exchange endpoint — it returns a preview plus
|
||||
// an "@blob sha256:<hash>" pointer — so this is how the UI fetches them whole.
|
||||
// Streamed rather than buffered: these are the bodies too large to hold in memory.
|
||||
func (s *Server) getTrafficBlob(w http.ResponseWriter, r *http.Request) {
|
||||
tr := s.m.Traffic()
|
||||
if tr == nil {
|
||||
writeErr(w, 404, "traffic disabled")
|
||||
return
|
||||
}
|
||||
hash := strings.TrimSpace(r.URL.Query().Get("hash"))
|
||||
if hash == "" {
|
||||
writeErr(w, 400, "missing hash")
|
||||
return
|
||||
}
|
||||
f, size, err := tr.Blob(hash)
|
||||
if err != nil {
|
||||
writeErr(w, 404, err.Error())
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
w.Header().Set("Content-Type", "application/octet-stream")
|
||||
w.Header().Set("Content-Length", strconv.FormatInt(size, 10))
|
||||
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", hash+".bin"))
|
||||
if _, err := io.Copy(w, f); err != nil {
|
||||
log.Printf("[traffic] 下载 blob %s 中断:%v", hash, err)
|
||||
}
|
||||
}
|
||||
|
||||
// getSettings returns the runtime app settings the UI toggles. The Brave API key
|
||||
// is returned as a boolean presence flag (brave_key_set), never the value itself,
|
||||
// so the UI can show "configured" without echoing the secret back.
|
||||
|
||||
Reference in new issue
Block a user