Files
AskaEth 2a43035f5d
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
refactor+feat(documents,channelbridge): 共享存储核心 + IM 入站附件摄取(主人 2.3)
- 抽共享存储核心:internal/runtime/documents/store.go(22 项白名单 + 字节嗅探唯一裁决 + 内容寻址 sha256[:32]+ext + 去重 + 限额 + 路径双检)+ store_meta.go(documents 表唯一写入实现)。

- gateway 改为调用 store,**对外行为零变化**(机器证明:既有 documents_test/chat_documents_test/chat_documents_bounds_test 三个文件 0 行改动且全绿;状态码/响应字段/错误码/审计动作名/限额文档口径全部逐字保留;maxChatDocuments 改为引用 documents.MaxPerMessage)。

- IM 入站附件摄取(channelbridge):文件类媒体 → store.Save → 挂 model.DocumentMessage(RoleUser) 部件 ⇒ 后续 resolveDocuments 自动抽取/注入/清单;挂载点在「会话解析 + 黑白名单 + 最小打扰」三重闸门之后(被拦消息绝不在盘上留文件);**不支持的格式不静默**(诚实提示行 + 带来源日志,只记 name/mime/size,名字做展示净化防伪造行);图片/语音保持既有链路。

- 主会话独立验收:隔离 worktree(M1 的 12 文件)build/vet 绿、全树 59 包 0 FAIL、双平台交叉编译绿、12 条 Inbound 用例 PASS;变异独立复现(关闭 IM 摄取 ⇒ 5 条红,含「落盘文件数 = [] want 1」)。

- ⚠️ 真机边界(需裁决):现网四个适配器上报的文件媒体**只带 URL**(telegram FileID / qq-obv11 file|url / weixin·kook CDN URL),Media.Data 恒空 ⇒ 本切片只摄**内联字节**,URL 一律「诚实提示,不代取远程」(避免第三份 SSRF 实现)。真机要让「她读到 IM 文件」,需各适配器入站时下载并内联(凭据在各自渠道里,推荐)或新增共享受守卫抓取器。

- 另登记:IM 附件暂未写 chat_messages.document 列(二手回合看不到引用,建议并入 S7);channelbridge 既有测试夹具在 -race 下有数据竞争(stubSessions.GetOrCreate,与本次无关)。
2026-10-04 19:25:00 +08:00

63 lines
2.7 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package documents
// 本文件归属 `documents` 元数据表(迁移 0014)的**唯一写入实现**(design/24
// §3.2;切片 M1)。S1 把这条 SQL 写在网关里;抽到共享存储核心之后,Web 上传与
// IM 入站附件必须共用同一份写入逻辑,否则「哪条来源写了元数据、哪条没写」会
// 分叉,S7 的引用计数与 GC 也就失去单一事实来源。
//
// 沿用 S1 的两条既有事实(刻意不改,改动属另一片):
// - 每次写入 `db.Open` 开一条新连接(design/24 §9.2-5 已登记的技术债);
// - 同一存储名重复上传 `ON CONFLICT(name) DO NOTHING` —— 首次的
// uploader/created_at 是权威,去重不刷新它们。
import (
"context"
"errors"
"fmt"
"log/slog"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/db"
)
// ErrMetadataNoPath 报告元数据写入器没有数据库路径(未接库的构建/测试)。
// 调用方据此不构造写入器(Store.Meta 留 nil),而不是拿到一个写不进去的替身。
var ErrMetadataNoPath = errors.New("documents: 元数据写入器缺少数据库路径")
// SQLMetadataWriter 把元数据行写进 SQLite 的 `documents` 表。它实现
// MetadataWriter,供网关(Web 上传)与组合根(IM 入站附件)共用。
type SQLMetadataWriter struct {
path string
log *slog.Logger
}
// MetadataWriterFor 按数据库路径给出元数据写入面;路径为空(或全空白)时返回
// **真正的 nil 接口**,调用方可以直接把它放进 `StoreConfig.Meta` 而不会踩
// 「接口里装着一个 nil 具体值」的陷阱(那会让「未注入」判定失效,把一次成功的
// 落盘变成 500)。未接库的构建/测试因此照旧可用:Store 按「未注入」跳过并留痕。
func MetadataWriterFor(path string, log *slog.Logger) MetadataWriter {
if strings.TrimSpace(path) == "" {
return nil
}
return &SQLMetadataWriter{path: path, log: log}
}
// WriteDocument 写入一行文档元数据。参数化 SQL、无拼接,错误按项目规范 wrap。
func (w *SQLMetadataWriter) WriteDocument(ctx context.Context, rec Record) error {
if w == nil || w.path == "" {
return fmt.Errorf("documents: 写元数据: %w", ErrMetadataNoPath)
}
conn, err := db.Open(ctx, w.path, w.log)
if err != nil {
return fmt.Errorf("documents: 打开元数据库: %w", err)
}
defer func() { _ = conn.Close() }()
const query = `INSERT INTO documents (name, sha256, mime, size, uploader)
VALUES (?, ?, ?, ?, ?)
ON CONFLICT(name) DO NOTHING`
if _, err := conn.ExecContext(ctx, query, rec.Name, rec.SHA256, rec.MIME, rec.Size, rec.Uploader); err != nil {
return fmt.Errorf("documents: 插入元数据行: %w", err)
}
return nil
}