diff --git a/internal/app/hostcaps.go b/internal/app/hostcaps.go index c4c90f8..7032ff2 100644 --- a/internal/app/hostcaps.go +++ b/internal/app/hostcaps.go @@ -3,7 +3,6 @@ package app import ( "agent-system-backend/pkg/extension" "context" - "errors" "fmt" "sync" @@ -140,14 +139,42 @@ func (h inprocHost) KnowledgeWrite(ctx context.Context, docs []core.KnowledgeDoc return h.caps.pipe.IndexChunks(ctx, core.ID(h.owner), docs) } -// KnowledgePublish/KnowledgeDelete EXT-9 幂等发布与撤销(本提交为占位—— -// 红测先行,实现在后续提交落地)。 +// KnowledgePublish 幂等发布(EXT-9):按 docs 的 ref 整批替换——先清该 owner +// 下同 ref 旧行与旧向量(跨来源也清:ref 是发布单元),再走 KnowledgeWrite +// 语义(Owner/Source 强制同彼)。重试产生相同结果。 func (h inprocHost) KnowledgePublish(ctx context.Context, docs []core.KnowledgeDoc, embed bool) error { - return errors.New("ext-9: knowledge publish not implemented yet") + if len(docs) == 0 { + return nil + } + refs := map[string]bool{} + for i := range docs { + docs[i].Owner = core.ID(h.owner) + if docs[i].Source == "" { + docs[i].Source = "plugin:" + h.pluginID + } + if docs[i].Ref != "" { + refs[docs[i].Ref] = true + } + } + for ref := range refs { + if err := h.caps.st.DeleteKnowledgeByRef(h.owner, ref); err != nil { + return err + } + if err := h.caps.st.DeleteKnowledgeVectors(ctx, h.owner, ref); err != nil { + return err + } + } + return h.KnowledgeWrite(ctx, docs, embed) } +// KnowledgeDelete 撤销本插件来源(EXT-9):前缀强制 plugin:<自身ID>——manual +// 或他人来源拒绝(fail-closed);删行+向量,幂等。 func (h inprocHost) KnowledgeDelete(ctx context.Context, sourcePrefix string) error { - return errors.New("ext-9: knowledge delete not implemented yet") + if sourcePrefix != "plugin:"+h.pluginID { + return fmt.Errorf("ext-9: plugins may only delete their own source prefix, want %q", "plugin:"+h.pluginID) + } + _, err := h.caps.st.DeleteKnowledgeBySource(ctx, h.owner, sourcePrefix) + return err } // TaskCreate/TaskUpdate 后台任务台账:基座只记进度/状态,执行体由下游自跑(H5)。 diff --git a/internal/server/server.go b/internal/server/server.go index 4bce8da..930429e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -1058,11 +1058,14 @@ func (s *Server) upload(w http.ResponseWriter, r *http.Request) { _ = s.Cards.Update(owner, ingestCard) } // 文本可解析的进检索索引(管线分块+全文+可选向量)。 - if plain := doc.PlainText(); plain != "" && len(plain) < 200000 { - if s.Indexer != nil { - _ = s.Indexer.IndexDocument(owner, projectID, "upload", art.Name, art.ID, plain) - } else { - _ = s.st.AddKnowledge(core.KnowledgeDoc{Owner: owner, ProjectID: projectID, Source: "upload", Title: art.Name, Ref: art.ID, Chunk: plain}) + // EXT-9:宿主关闭自动索引时跳过(发布决定移交下游命令层;原件与摄取卡不变)。 + if !s.DisableUploadIndexing { + if plain := doc.PlainText(); plain != "" && len(plain) < 200000 { + if s.Indexer != nil { + _ = s.Indexer.IndexDocument(owner, projectID, "upload", art.Name, art.ID, plain) + } else { + _ = s.st.AddKnowledge(core.KnowledgeDoc{Owner: owner, ProjectID: projectID, Source: "upload", Title: art.Name, Ref: art.ID, Chunk: plain}) + } } } writeJSON(w, http.StatusOK, core.UploadInfo{ArtifactID: art.ID, Filename: art.Name, MIME: mime, Size: art.Size, Parsed: &doc}) diff --git a/internal/store/content.go b/internal/store/content.go index 792d49d..bdc17ef 100644 --- a/internal/store/content.go +++ b/internal/store/content.go @@ -312,6 +312,40 @@ func (s *Store) DeleteKnowledgeByRef(owner, ref string) error { return err } +// DeleteKnowledgeBySource 按来源前缀撤销知识(EXT-9):删除该 owner 下 source 匹配 +// 前缀的行,并经 ref 关联清理向量;幂等(无匹配返回 0, nil)。供宿主在 +// KnowledgeDelete(前缀已强制 plugin:<插件ID>)后调用——不作用他人来源。 +func (s *Store) DeleteKnowledgeBySource(ctx context.Context, owner, sourcePrefix string) (int64, error) { + rows, err := s.db.QueryContext(ctx, s.q(`SELECT DISTINCT ref FROM knowledge WHERE owner=? AND source LIKE ?`), owner, sourcePrefix+"%") + if err != nil { + return 0, err + } + refs := []string{} + for rows.Next() { + var r string + if err := rows.Scan(&r); err != nil { + rows.Close() + return 0, err + } + refs = append(refs, r) + } + rows.Close() + if err := rows.Err(); err != nil { + return 0, err + } + for _, r := range refs { + if err := s.DeleteKnowledgeVectors(ctx, owner, r); err != nil { + return 0, err + } + } + res, err := s.db.ExecContext(ctx, s.q(`DELETE FROM knowledge WHERE owner=? AND source LIKE ?`), owner, sourcePrefix+"%") + if err != nil { + return 0, err + } + n, _ := res.RowsAffected() + return n, nil +} + func (s *Store) SearchKnowledge(owner string, q core.RetrievalQuery) ([]core.RetrievalHit, error) { if s.dialect.name() == "postgres" { return s.searchKnowledgePG(owner, q)