Implement EXT-9 idempotent publish, prefix-enforced revoke, and the upload index switch
ci / vet-and-test (pull_request) Successful in 41s
ci / vet-and-test (pull_request) Successful in 41s
Why: The red acceptance tests pin the EXT-9 semantics - held material must not enter retrieval until published, revoke must stop retrieval immediately and idempotently, manual sources must stay out of plugin reach. Evidence: TestKnowledgePublishIdempotentAndRevoke and TestUploadPreIndexSwitch green; go test ./... 32/32 packages ok; rows follow on the task card. Impact: hostcaps KnowledgePublish (per-ref row+vector cleanup then existing write path) and KnowledgeDelete (plugin:<id> prefix enforced); store DeleteKnowledgeBySource with vector cleanup; upload indexing gated by DisableUploadIndexing (default false = unchanged). Refs: EXT-9 proposal; red run at surface commit.
This commit is contained in:
@@ -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)。
|
||||
|
||||
@@ -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})
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user