Files
knowledge-governance/internal/workflows/testweknora/remove.go
zheromiku dcdc8d1ddd feat: 知识治理平台(含已验证发布/回滚契约)
全栈知识治理平台首次入库(P1-P10 + 发布链路模拟验证闭环):

- 治理核心:源目录/变更集/人工审核/发布批次/发布执行(fail-closed 闸口)
- 测试通道:WeKnora 测试 KB 上传/轮询/验收;poll 完成时写入
  parser_generation/index_generation,修复 manual/seed 路径发布闸口永久拦截
- 发布适配器 v071(本地 source=测试角色 / 云端 target=生产角色 模拟验证通过):
  * ES 导出/导入 mapping 自适应(.keyword 子字段、可选 tenant_id)
  * OSS locator 支持嵌套 scheme;chunks.seq_id 由目标序列分配
  * bundle 归档同身份重试原子替换;verifier 按 bundle chunk_type 计数
  * 回滚 job payload 支持 prior_execution_id(strict 解码)
- 三通道发布+回滚演练实测:publish 4/4 verified,检索命中,
  rollback 4/4 rolled_back,目标端 PG is_enabled=false、ES is_enabled=false
- 前端 A·Data-console 风格全站统一,120 用例通过
- 门禁:go vet / go test 全包通过,gofmt 干净
2026-09-30 10:54:06 +08:00

118 lines
3.7 KiB
Go

package testweknora
import (
"context"
"errors"
"net/http"
"time"
"knowledge-governance/internal/jobs"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/platform/weknora"
)
var (
ErrInvalidRemoveJobPayload = errors.New("test_remove_invalid_payload")
ErrRemoveRemoteFailed = errors.New("test_remove_remote_failed")
ErrRemoveRemoteIdentity = errors.New("test_remove_remote_identity_invalid")
ErrRemovePending = errors.New("test_remove_pending")
ErrRemoveStateConflict = errors.New("test_remove_state_conflict")
)
type RemoveStore interface {
LoadPublicationForRemoval(context.Context, string) (postgresstore.PublicationForRemoval, error)
RecordRemovalCompleted(context.Context, string, time.Time) error
RecordRemovalFailure(context.Context, string, string, time.Time) error
}
type RemoveClient interface {
GetKnowledge(context.Context, string) (weknora.Knowledge, error)
DeleteKnowledge(context.Context, string) error
}
type RemoveHandlerConfig struct {
Store RemoveStore
Client RemoveClient
Now func() time.Time
}
type RemoveHandler struct {
store RemoveStore
client RemoveClient
now func() time.Time
}
type removePayload struct {
PublicationID string `json:"publication_id"`
}
func NewRemoveHandler(config RemoveHandlerConfig) (*RemoveHandler, error) {
if config.Store == nil || config.Client == nil {
return nil, errors.New("invalid test removal handler configuration")
}
if config.Now == nil {
config.Now = time.Now
}
return &RemoveHandler{store: config.Store, client: config.Client, now: config.Now}, nil
}
func (handler *RemoveHandler) Handle(ctx context.Context, job jobs.Job) error {
var payload removePayload
if err := decodeStrictJSON(job.Payload, &payload); err != nil || payload.PublicationID == "" {
return jobs.Permanent(jobs.ErrCodeHandlerFallback, ErrInvalidRemoveJobPayload)
}
candidate, err := handler.store.LoadPublicationForRemoval(ctx, payload.PublicationID)
if err != nil {
return ErrRemoveStateConflict
}
knowledge, err := handler.client.GetKnowledge(ctx, candidate.KnowledgeID)
if isNotFound(err) {
return handler.complete(ctx, candidate.ID)
}
if err != nil {
return ErrRemoveRemoteFailed
}
if knowledge.ID != candidate.KnowledgeID || knowledge.KnowledgeBaseID != candidate.TestKBID {
return handler.failClosed(ctx, candidate.ID, "removal_identity_invalid")
}
if err := handler.client.DeleteKnowledge(ctx, candidate.KnowledgeID); err != nil {
if isNotFound(err) {
return handler.complete(ctx, candidate.ID)
}
if !weknora.IsRetryable(err) {
return handler.failClosed(ctx, candidate.ID, "removal_remote_rejected")
}
return ErrRemoveRemoteFailed
}
knowledge, err = handler.client.GetKnowledge(ctx, candidate.KnowledgeID)
if isNotFound(err) {
return handler.complete(ctx, candidate.ID)
}
if err != nil {
return ErrRemoveRemoteFailed
}
if knowledge.ID != candidate.KnowledgeID || knowledge.KnowledgeBaseID != candidate.TestKBID {
return handler.failClosed(ctx, candidate.ID, "removal_identity_invalid")
}
return ErrRemovePending
}
func (handler *RemoveHandler) complete(ctx context.Context, publicationID string) error {
if err := handler.store.RecordRemovalCompleted(ctx, publicationID, handler.now()); err != nil {
return ErrRemoveStateConflict
}
return nil
}
func (handler *RemoveHandler) failClosed(ctx context.Context, publicationID, code string) error {
if err := handler.store.RecordRemovalFailure(ctx, publicationID, code, handler.now()); err != nil {
return ErrRemoveStateConflict
}
return nil
}
func isNotFound(err error) bool {
var httpErr *weknora.HTTPError
return errors.As(err, &httpErr) && httpErr.StatusCode == http.StatusNotFound
}