feat(space): 共享工作区增量3b —— KB 知识库按 Space 共享(存储 re-key)

把 3a 的 Space 作用域推到 KB 层:KB/Doc/DocLink owner→space_id,作用域键
owner/name→space_id/name,同空间成员共享知识库(检索/入库/文库/双链/图谱)。

后端:
- store: KB/Doc/DocLink 加 space_id,唯一索引 (owner,*)→(space_id,*),owner 降级创建人;
  查询全改 space 作用域;SaveDoc/ListVault/GetDocByID/DeleteDocByID/ReplaceDocLinks/
  ResolveInboundLinks/ListLinks 改 space;tenantIDForSpace 补异步入库租户
- MigrateKBSpaces 启动迁移(space_id 回填 + 唯一索引换新,同 Agent 顺序坑规避)
- scopedKB owner/name→space_id/name;IngestJob 契约加 SpaceID;enqueueIngest/runIngest
  穿 space;MinIO 对象键改 space/kb/doc(避免跨空间同名撞键,老键不透明不迁)
- KB 写路由(create/ingest/ingest_file/note/delete)挂 RequireSpaceRole(member):viewer 只读
- 存储层重灌迁移端点 POST /admin/migrate-kb-storage(异步):为存量文档入队新 space
  作用域的重灌作业(复用 JetStream 入库 worker 池),先删旧键;避免同步重嵌撑爆 HTTP 超时

桌面端:
- KbView 收 spaceId(变则重拉库)+spaceReadOnly(viewer 禁建库/入库/文件/笔记);VaultPanel 同

验证(gateway+mcp-go+Milvus/Neo4j/embedding 全栈):
- PG 迁移: 20/21 KB + 50/54 doc 回填 space_id(4 未迁=pre-多租户 owner='wt' 空租户遗留,
  正确跳过),唯一索引 idx_kb_sn/idx_doc_skn 换新、旧索引删除
- KB 共享: member 见共享库 / viewer 建库·入库 403 / 切回个人空间隔离(看不到)
- 全向量链路: RagA 入库(真 dashscope embedding)→ RagB(空间member)检索命中 RagA 内容
- 存储重灌: 端点异步入队 49 作业(worker 池背压处理),重灌后老文档在新 space 键可检索

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-13 13:00:34 +08:00
parent addaa1b34f
commit a8c0bb42a7
9 changed files with 218 additions and 97 deletions
+65
View File
@@ -45,6 +45,18 @@ const (
// ---- 基础查询 ----
// tenantIDForSpace 返回某空间的租户 id(异步入库路径按 space 补 tenant_id 用)。
func (p *Postgres) tenantIDForSpace(ctx context.Context, spaceID string) string {
if p.db == nil || spaceID == "" {
return ""
}
var s Space
if err := p.db.WithContext(ctx).Select("tenant_id").First(&s, "id = ?", spaceID).Error; err != nil {
return ""
}
return s.TenantID
}
// GetSpace 按 id 取空间(不存在返回 nil)。
func (p *Postgres) GetSpace(ctx context.Context, id string) (*Space, error) {
if p.db == nil {
@@ -335,3 +347,56 @@ func (p *Postgres) MigrateAgentSpaces(ctx context.Context) error {
}
return nil
}
// DocForRestorage 是存储层重灌迁移用的一条文档视图(拿回原文所需的键)。
type DocForRestorage struct {
ID string
SpaceID string
Owner string
KB string
Name string
Content string
ObjectKey string
}
// DocsForRestorage 列出全部有 space_id 的存量文档(系统级跨租户;供存储层 owner/kb→space/kb 重灌)。
func (p *Postgres) DocsForRestorage(ctx context.Context) ([]DocForRestorage, error) {
if p.db == nil {
return nil, errStoreDisabled
}
var rows []DocForRestorage
err := p.db.WithContext(WithoutTenant(ctx)).Table("sundynix_doc").
Select("id, space_id, owner, kb, name, content, object_key").
Where("space_id <> '' AND owner <> ''").Order("created_at asc").Scan(&rows).Error
return rows, err
}
// MigrateKBSpaces 把存量 KB/Doc/DocLink 的 space_id 回填为其 (tenant_id, owner) 个人空间,
// 再 DROP 旧唯一索引、建新唯一索引(同 MigrateAgentSpaces 的顺序坑规避)。全程幂等。
// 注:仅迁移 PG 元数据作用域;Milvus/Bleve/Neo4j 的存量向量/全文/图谱另由 kb 存储重灌迁移。
func (p *Postgres) MigrateKBSpaces(ctx context.Context) error {
if p.db == nil {
return errStoreDisabled
}
sub := "SELECT id FROM sundynix_space s WHERE s.tenant_id = t.tenant_id AND s.creator = t.owner AND s.kind = 'personal' ORDER BY s.created_at ASC LIMIT 1"
for _, tbl := range []string{"sundynix_kb", "sundynix_doc", "sundynix_doc_link"} {
if err := p.db.WithContext(ctx).Exec(
"UPDATE "+tbl+" t SET space_id = ("+sub+") WHERE (t.space_id IS NULL OR t.space_id = '') AND t.owner <> ''",
).Error; err != nil {
return err
}
}
// 旧唯一索引 DROP + 新唯一索引建(回填后,安全)。DocLink 无唯一约束,不涉及。
stmts := []string{
"DROP INDEX IF EXISTS idx_kb_owner_name",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_kb_sn ON sundynix_kb(space_id, name)",
"DROP INDEX IF EXISTS idx_doc_okn",
"CREATE UNIQUE INDEX IF NOT EXISTS idx_doc_skn ON sundynix_doc(space_id, kb, name)",
}
for _, s := range stmts {
if err := p.db.WithContext(ctx).Exec(s).Error; err != nil {
return err
}
}
return nil
}