dcdc8d1ddd
全栈知识治理平台首次入库(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 干净
181 lines
6.8 KiB
Go
181 lines
6.8 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"knowledge-governance/internal/governance"
|
|
"knowledge-governance/internal/jobs"
|
|
)
|
|
|
|
var ErrOperationRequestConflict = errors.New("operation request conflicts with an existing request")
|
|
|
|
type operationExecutor func(pgx.Tx) (json.RawMessage, error)
|
|
|
|
func (s *Store) RouteCatalogFilesWithOperation(ctx context.Context, request governance.CatalogRouteRequest, operationID string) (governance.CatalogRouteResult, bool, error) {
|
|
if err := validateCatalogRouteRequest(request); err != nil {
|
|
return governance.CatalogRouteResult{}, false, err
|
|
}
|
|
fingerprint, err := catalogRouteFingerprint(request)
|
|
if err != nil {
|
|
return governance.CatalogRouteResult{}, false, err
|
|
}
|
|
resultJSON, replayed, err := s.executeOperation(ctx, "catalog.route", operationID, fingerprint, request.Now, func(tx pgx.Tx) (json.RawMessage, error) {
|
|
result, err := routeCatalogFilesTx(ctx, tx, request)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
encoded, err := json.Marshal(result)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode catalog route result: %w", err)
|
|
}
|
|
return encoded, nil
|
|
})
|
|
if err != nil {
|
|
return governance.CatalogRouteResult{}, false, err
|
|
}
|
|
var result governance.CatalogRouteResult
|
|
if err := json.Unmarshal(resultJSON, &result); err != nil {
|
|
return governance.CatalogRouteResult{}, false, fmt.Errorf("decode stored catalog route result: %w", err)
|
|
}
|
|
return result, replayed, nil
|
|
}
|
|
|
|
func catalogRouteFingerprint(request governance.CatalogRouteRequest) (string, error) {
|
|
targets := request.TargetKnowledgeBaseIDs
|
|
if len(targets) == 0 && request.TargetKnowledgeBaseID != "" {
|
|
targets = []string{request.TargetKnowledgeBaseID}
|
|
}
|
|
payload, err := json.Marshal(struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
NodeIDs []string `json:"node_ids"`
|
|
Visibility governance.Visibility `json:"visibility"`
|
|
Targets []string `json:"target_knowledge_base_ids"`
|
|
OperatorRef string `json:"operator_ref"`
|
|
Reason string `json:"reason"`
|
|
}{
|
|
ConnectionID: request.ConnectionID,
|
|
NodeIDs: request.NodeIDs,
|
|
Visibility: request.Visibility,
|
|
Targets: targets,
|
|
OperatorRef: strings.TrimSpace(request.OperatorRef),
|
|
Reason: strings.TrimSpace(request.Reason),
|
|
})
|
|
if err != nil {
|
|
return "", fmt.Errorf("encode catalog route fingerprint: %w", err)
|
|
}
|
|
digest := sha256.Sum256(payload)
|
|
return hex.EncodeToString(digest[:]), nil
|
|
}
|
|
|
|
func (s *Store) EnqueueCatalogRefreshWithOperation(ctx context.Context, source governance.CatalogSource, operationID string, now time.Time) (jobs.Job, bool, error) {
|
|
if !validRouteID(source.ConnectionID) || strings.TrimSpace(source.Name) == "" || now.IsZero() {
|
|
return jobs.Job{}, false, errors.New("invalid catalog refresh request")
|
|
}
|
|
fingerprint, err := operationFingerprint(struct {
|
|
ConnectionID string `json:"connection_id"`
|
|
}{ConnectionID: source.ConnectionID})
|
|
if err != nil {
|
|
return jobs.Job{}, false, err
|
|
}
|
|
resultJSON, replayed, err := s.executeOperation(ctx, "catalog.refresh", operationID, fingerprint, now, func(tx pgx.Tx) (json.RawMessage, error) {
|
|
jobID, err := newOpaqueID("job-")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
payload, err := json.Marshal(map[string]any{"connection_id": source.ConnectionID, "source_type": source.SourceType, "source_name": source.Name, "stage": "queued", "progress": 0, "message": "等待后台刷新", "processed": 0, "total": 0})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode catalog refresh job: %w", err)
|
|
}
|
|
job, err := scanJob(tx.QueryRow(ctx, enqueueJobSQL, jobID, "source.catalog.refresh", payload, 20, now, 3, "catalog-refresh:"+jobID, now))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("enqueue catalog refresh job: %w", err)
|
|
}
|
|
encoded, err := json.Marshal(job)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("encode catalog refresh result: %w", err)
|
|
}
|
|
return encoded, nil
|
|
})
|
|
if err != nil {
|
|
return jobs.Job{}, false, err
|
|
}
|
|
var result jobs.Job
|
|
if err := json.Unmarshal(resultJSON, &result); err != nil {
|
|
return jobs.Job{}, false, fmt.Errorf("decode stored catalog refresh result: %w", err)
|
|
}
|
|
return result, replayed, nil
|
|
}
|
|
|
|
func operationFingerprint(value any) (string, error) {
|
|
payload, err := json.Marshal(value)
|
|
if err != nil {
|
|
return "", fmt.Errorf("encode operation fingerprint: %w", err)
|
|
}
|
|
digest := sha256.Sum256(payload)
|
|
return hex.EncodeToString(digest[:]), nil
|
|
}
|
|
|
|
func (s *Store) executeOperation(ctx context.Context, operationType, operationID, fingerprint string, now time.Time, execute operationExecutor) (json.RawMessage, bool, error) {
|
|
if strings.TrimSpace(operationType) == "" || len(operationType) > 100 || strings.TrimSpace(operationID) == "" || len(operationID) > 128 || len(fingerprint) != 64 || now.IsZero() || execute == nil {
|
|
return nil, false, errors.New("invalid operation request")
|
|
}
|
|
beginner, ok := s.db.(interface {
|
|
Begin(context.Context) (pgx.Tx, error)
|
|
})
|
|
if !ok {
|
|
return nil, false, errors.New("operation request transactions are unavailable")
|
|
}
|
|
tx, err := beginner.Begin(ctx)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("begin operation request: %w", err)
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
tag, err := tx.Exec(ctx, `INSERT INTO operation_requests(operation_type,operation_id,request_fingerprint_sha256,state,result,created_at,completed_at)
|
|
VALUES($1,$2,$3,'completed','{}'::jsonb,$4,$4)
|
|
ON CONFLICT(operation_type,operation_id) DO NOTHING`, operationType, operationID, fingerprint, now)
|
|
if err != nil {
|
|
return nil, false, fmt.Errorf("create operation request: %w", err)
|
|
}
|
|
if tag.RowsAffected() == 0 {
|
|
var storedFingerprint string
|
|
var storedResult json.RawMessage
|
|
if err := tx.QueryRow(ctx, `SELECT request_fingerprint_sha256,result
|
|
FROM operation_requests
|
|
WHERE operation_type=$1 AND operation_id=$2
|
|
FOR UPDATE`, operationType, operationID).Scan(&storedFingerprint, &storedResult); err != nil {
|
|
return nil, false, fmt.Errorf("load operation request: %w", err)
|
|
}
|
|
if storedFingerprint != fingerprint {
|
|
return nil, false, ErrOperationRequestConflict
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, false, fmt.Errorf("commit replay operation request: %w", err)
|
|
}
|
|
return storedResult, true, nil
|
|
}
|
|
result, err := execute(tx)
|
|
if err != nil {
|
|
return nil, false, err
|
|
}
|
|
if !json.Valid(result) {
|
|
return nil, false, errors.New("operation result must be valid JSON")
|
|
}
|
|
if _, err := tx.Exec(ctx, `UPDATE operation_requests SET result=$3::jsonb,completed_at=$4
|
|
WHERE operation_type=$1 AND operation_id=$2`, operationType, operationID, result, now); err != nil {
|
|
return nil, false, fmt.Errorf("complete operation request: %w", err)
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return nil, false, fmt.Errorf("commit operation request: %w", err)
|
|
}
|
|
return result, false, nil
|
|
}
|