Implement shared knowledge retrieval, card mirror switch, and SSE project gate
ci / vet-and-test (pull_request) Successful in 40s

Why: The three pinned reds define EXT-8 slice 2 - adapter-granted cross-owner knowledge must be retrievable, conversation cards must stay out of public project streams when the host opts out, and project-stream subscription must honor the authorizer.
Evidence: TestConvCardMirrorSwitch, TestSharedKnowledgeRetrieval, TestProjectSSEScopeGate green; go test ./... 32/32 packages ok; rows follow on the task card.
Impact: store gains predicate-based SearchKnowledgeShared (FTS/LIKE shared variants + PG); searchKnowledge appends adapter-authorized hits; cardsvc emit gate (no publish = no persistence = clean replay); events project scope consults authorizeSharedProject. Default behavior unchanged everywhere.
Refs: EXT-8/EXT-12 proposals; red run at surface commit.
This commit is contained in:
2026-10-08 16:18:07 +00:00
parent d3381b208b
commit 1de663a8d3
5 changed files with 189 additions and 15 deletions
+95 -6
View File
@@ -353,7 +353,72 @@ func (s *Store) SearchKnowledge(owner string, q core.RetrievalQuery) ([]core.Ret
return s.searchKnowledgeSQLite(owner, q)
}
// knowledgePred 命中可见性谓词(owner 作用域或共享作用域由调用方决定)。
type knowledgePred func(d core.KnowledgeDoc) bool
func ownerVisible(owner string, q core.RetrievalQuery) knowledgePred {
return func(d core.KnowledgeDoc) bool { return knowledgeVisible(owner, q, d) }
}
// sharedVisible EXT-8 切片 2:他人 owner 且其 ProjectID 经授权回调放行的行可见;
// 无 ProjectID 的行无法授权(拒绝);项目/来源过滤沿用查询口径。
func sharedVisible(requester string, q core.RetrievalQuery, allow func(docOwner, projectID core.ID) bool) knowledgePred {
return func(d core.KnowledgeDoc) bool {
if d.Owner == "" || d.Owner == requester || d.ProjectID == "" {
return false
}
if q.ProjectID != "" && d.ProjectID != q.ProjectID {
return false
}
if len(q.Sources) > 0 && !containsStr(q.Sources, d.Source) {
return false
}
return allow(d.Owner, d.ProjectID)
}
}
// SearchKnowledgeShared 共享检索(EXT-8 切片 2):返回他人 owner 的项目知识中,
// 经 allow(docOwner, projectID) 放行的命中(全文通道;向量通道共享仍留后续切片)。
// allow 出错由调用方前置处理——本层只见布尔。
func (s *Store) SearchKnowledgeShared(requester string, q core.RetrievalQuery, allow func(docOwner, projectID core.ID) bool) ([]core.RetrievalHit, error) {
pred := sharedVisible(requester, q, allow)
if s.dialect.name() == "postgres" {
return s.searchKnowledgePGShared(requester, q, pred)
}
return s.searchKnowledgeSQLiteShared(requester, q, pred)
}
func (s *Store) searchKnowledgeSQLite(owner string, q core.RetrievalQuery) ([]core.RetrievalHit, error) {
out, err := s.searchKnowledgeFTS(ownerVisible(owner, q), q)
if err != nil {
return nil, err
}
// CJK 兜底:unicode61 分词把中文整句当单 token,FTS 前缀匹配失效时用 LIKE 子串。
if len(out) == 0 && q.Text != "" {
topK := q.TopK
if topK <= 0 {
topK = 5
}
return s.searchKnowledgeLike(owner, q, topK)
}
return out, nil
}
// searchKnowledgeSQLiteShared 共享版:FTS SQL 本身无 owner 预过滤(可见性原为
// 后置判定),直接换谓词即可;LIKE 兜底换“他人行”预过滤。
func (s *Store) searchKnowledgeSQLiteShared(requester string, q core.RetrievalQuery, pred knowledgePred) ([]core.RetrievalHit, error) {
out, err := s.searchKnowledgeFTS(pred, q)
if err != nil {
return nil, err
}
if len(out) == 0 && q.Text != "" {
return s.searchKnowledgeLikeShared(requester, q, pred)
}
return out, nil
}
// searchKnowledgeFTS FTS 通道(owner 作用域与共享作用域共用:可见性谓词由调用方注入)。
func (s *Store) searchKnowledgeFTS(pred knowledgePred, q core.RetrievalQuery) ([]core.RetrievalHit, error) {
topK := q.TopK
if topK <= 0 {
topK = 5
@@ -372,7 +437,7 @@ func (s *Store) searchKnowledgeSQLite(owner string, q core.RetrievalQuery) ([]co
if err != nil {
return nil, err
}
if !knowledgeVisible(owner, q, h.Doc) {
if !pred(h.Doc) {
continue
}
out = append(out, h)
@@ -380,14 +445,38 @@ func (s *Store) searchKnowledgeSQLite(owner string, q core.RetrievalQuery) ([]co
break
}
}
if err := rows.Err(); err != nil {
return out, rows.Err()
}
func (s *Store) searchKnowledgeLikeShared(requester string, q core.RetrievalQuery, pred knowledgePred) ([]core.RetrievalHit, error) {
topK := q.TopK
if topK <= 0 {
topK = 5
}
rows, err := s.db.Query(s.q(`SELECT id,owner,project_id,source,title,ref,chunk,created_at FROM knowledge WHERE owner<>? AND owner<>'' AND (chunk LIKE ? OR title LIKE ?) LIMIT ?`),
requester, "%"+q.Text+"%", "%"+q.Text+"%", topK)
if err != nil {
return nil, err
}
// CJK 兜底:unicode61 分词把中文整句当单 token,FTS 前缀匹配失效时用 LIKE 子串。
if len(out) == 0 && q.Text != "" {
return s.searchKnowledgeLike(owner, q, topK)
defer rows.Close()
out := []core.RetrievalHit{}
for rows.Next() {
var d core.KnowledgeDoc
var own, ca string
if err := rows.Scan(&d.ID, &own, &d.ProjectID, &d.Source, &d.Title, &d.Ref, &d.Chunk, &ca); err != nil {
return nil, err
}
d.Owner = own
d.CreatedAt, _ = parseTime(ca)
if !pred(d) {
continue
}
out = append(out, core.RetrievalHit{Doc: d, Score: 1.0, Kind: "fulltext"})
if len(out) >= topK {
break
}
}
return out, nil
return out, rows.Err()
}
func (s *Store) searchKnowledgeLike(owner string, q core.RetrievalQuery, topK int) ([]core.RetrievalHit, error) {