Files
knowledge-governance/cmd/release-diag/main.go
T
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

314 lines
11 KiB
Go

// Command release-diag replays the read-only steps of the release export path
// (source PG snapshot, source ES snapshot, source object read) and additionally
// lists candidate OSS key prefixes, to pinpoint a failing export step with its
// real underlying error. Credentials are read from files only and never printed.
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"io"
"net/url"
"os"
"time"
"unicode/utf8"
"github.com/aws/aws-sdk-go-v2/aws"
awsconfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/jackc/pgx/v5/pgxpool"
"knowledge-governance/internal/platform/weknora"
"knowledge-governance/internal/releaseadapter/v071"
)
type objectCfg struct {
Provider string `json:"provider"`
StorageBackendID string `json:"storage_backend_id"`
Endpoint string `json:"endpoint"`
Region string `json:"region"`
Bucket string `json:"bucket"`
AccessKeyFile string `json:"access_key_file"`
SecretKeyFile string `json:"secret_key_file"`
}
type endpointCfg struct {
DatabaseURLFile string `json:"database_url_file"`
Elasticsearch struct {
BaseURL string `json:"base_url"`
Index string `json:"index"`
} `json:"elasticsearch"`
Object objectCfg `json:"object"`
WeKnora struct {
BaseURL string `json:"base_url"`
APIKeyFile string `json:"api_key_file"`
} `json:"weknora"`
}
type config struct {
Routes []struct {
TenantID uint64 `json:"tenant_id"`
KnowledgeBaseID string `json:"knowledge_base_id"`
EmbeddingModel string `json:"embedding_model"`
Source endpointCfg `json:"source"`
Target endpointCfg `json:"target"`
} `json:"routes"`
}
func secret(path string) string {
data, err := os.ReadFile(path)
if err != nil {
fmt.Println("FATAL read secret:", err)
os.Exit(1)
}
return string(data)
}
func main() {
configPath := flag.String("config", "", "release config json")
knowledgeID := flag.String("knowledge", "", "knowledge id to diagnose")
flag.Parse()
raw, err := os.ReadFile(*configPath)
if err != nil {
panic(err)
}
var cfg config
if err := json.Unmarshal(raw, &cfg); err != nil || len(cfg.Routes) != 1 {
panic(err)
}
route := cfg.Routes[0]
fmt.Println("route:", route.KnowledgeBaseID, "tenant:", route.TenantID)
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
defer cancel()
pool, err := pgxpool.New(ctx, secret(route.Source.DatabaseURLFile))
if err != nil {
panic(err)
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
fmt.Println("STEP source PG connect: FAIL", err)
return
}
fmt.Println("STEP source PG connect: ok")
postgres, err := v071.NewPostgresExporter(pool)
if err != nil {
panic(err)
}
snapshot, err := postgres.Export(ctx, v071.PostgresIdentity{TenantID: route.TenantID, KnowledgeBaseID: route.KnowledgeBaseID, KnowledgeID: *knowledgeID})
if err != nil {
fmt.Println("STEP postgres export: FAIL", err)
return
}
names := make([]string, 0, len(snapshot.Tables))
for _, table := range snapshot.Tables {
names = append(names, fmt.Sprintf("%s:%d", table.Name, table.RowCount))
}
fmt.Println("STEP postgres export: ok tables", names)
search, err := v071.NewSearchExporter(v071.SearchExporterConfig{BaseURL: route.Source.Elasticsearch.BaseURL, Index: route.Source.Elasticsearch.Index, PageSize: 500})
if err != nil {
panic(err)
}
searchSnapshot, err := search.Export(ctx, v071.SearchIdentity{TenantID: route.TenantID, KnowledgeBaseID: route.KnowledgeBaseID, KnowledgeID: *knowledgeID})
if err != nil {
fmt.Println("STEP elasticsearch export: FAIL", err)
return
}
fmt.Printf("STEP elasticsearch export: ok documents=%d\n", len(searchSnapshot.Documents))
store, err := newStore(ctx, route.Source.Object)
if err != nil {
fmt.Println("STEP object store build: FAIL", err)
return
}
exporter, err := v071.NewObjectExporter(store, v071.ObjectExporterLimits{})
if err != nil {
panic(err)
}
objectSnapshot, err := exporter.Export(ctx, snapshot)
if err != nil {
fmt.Println("STEP object export: FAIL", err)
} else {
fmt.Printf("STEP object export: ok objects=%d bytes=%d\n", len(objectSnapshot.Objects), objectSnapshot.TotalBytes)
}
client, err := newRawClient(ctx, route.Source.Object)
if err != nil {
panic(err)
}
for _, prefix := range []string{"weknora/", "oss://", ""} {
listed, err := client.ListObjectsV2(ctx, &s3.ListObjectsV2Input{Bucket: aws.String(route.Source.Object.Bucket), Prefix: aws.String(prefix), MaxKeys: aws.Int32(3)})
if err != nil {
fmt.Printf("STEP list prefix %q: FAIL %v\n", prefix, err)
continue
}
fmt.Printf("STEP list prefix %q: %d objects\n", prefix, len(listed.Contents))
for _, object := range listed.Contents {
fmt.Println(" ", aws.ToString(object.Key))
}
}
knowledge := snapshot.Table("knowledges")
for _, row := range knowledge.Rows {
var typed struct {
Source string `json:"source"`
}
_ = json.Unmarshal(row, &typed)
fmt.Println("knowledge.source:", typed.Source)
}
for _, raw := range snapshot.Table("resources").Rows {
var typed struct {
PhysicalPath string `json:"physical_path"`
}
_ = json.Unmarshal(raw, &typed)
fmt.Println("resource physical_path:", typed.PhysicalPath)
}
fmt.Println("STEP target object stage probe:")
targetStore, err := newStore(ctx, route.Target.Object)
if err != nil {
fmt.Println(" target store build FAIL", err)
return
}
probePath := ".governance-release-diag/probe.txt"
payload := []byte("release-sim-probe")
if err := targetStore.Put(ctx, route.Target.Object.Provider, probePath, bytes.NewReader(payload), int64(len(payload))); err != nil {
fmt.Println(" target Put FAIL:", err)
} else {
size, digest, err := targetStore.Stat(ctx, route.Target.Object.Provider, probePath)
fmt.Println(" target Put ok; Stat:", size, digest[:8], "err:", err)
}
fmt.Println("STEP real ObjectImporter Stage/Promote with exported snapshot:")
importer, err := v071.NewObjectImporter(targetStore, v071.TargetObjectConfig{StorageBackendID: route.Target.Object.StorageBackendID, Provider: route.Target.Object.Provider})
if err != nil {
fmt.Println(" importer build FAIL", err)
return
}
stage, err := importer.Stage(ctx, "diag-stage", objectSnapshot)
if err != nil {
fmt.Println(" Stage FAIL:", err)
return
}
fmt.Println(" Stage ok objects:", len(stage.Objects))
if err := importer.Promote(ctx, stage); err != nil {
fmt.Println(" Promote FAIL:", err)
return
}
fmt.Println(" Promote ok")
fmt.Println("STEP target Postgres replace:")
targetPool, err := pgxpool.New(ctx, secret(route.Target.DatabaseURLFile))
if err != nil {
fmt.Println(" target pool FAIL", err)
return
}
defer targetPool.Close()
if err := targetPool.Ping(ctx); err != nil {
fmt.Println(" target PG connect FAIL", err)
return
}
pgImporter, err := v071.NewPostgresImporter(targetPool, v071.PostgresImporterConfig{TargetStorageBackendID: route.Target.Object.StorageBackendID, TargetProvider: route.Target.Object.Provider})
if err != nil {
fmt.Println(" importer config FAIL", err)
return
}
identity := v071.PostgresIdentity{TenantID: route.TenantID, KnowledgeBaseID: route.KnowledgeBaseID, KnowledgeID: *knowledgeID}
if err := pgImporter.Replace(ctx, identity, snapshot); err != nil {
fmt.Println(" Replace FAIL:", err)
return
}
fmt.Println(" Replace ok")
fmt.Println("STEP target ES replace:")
targetSearch, err := v071.NewSearchImporter(v071.SearchExporterConfig{BaseURL: route.Target.Elasticsearch.BaseURL, Index: route.Target.Elasticsearch.Index, PageSize: 500})
if err != nil {
fmt.Println(" importer build FAIL", err)
return
}
if _, err := targetSearch.Replace(ctx, v071.SearchIdentity{TenantID: route.TenantID, KnowledgeBaseID: route.KnowledgeBaseID, KnowledgeID: *knowledgeID}, searchSnapshot); err != nil {
fmt.Println(" ES Replace FAIL:", err)
return
}
fmt.Println(" ES Replace ok")
fmt.Println("STEP target WeKnora verification replay:")
wknoraClient, err := weknora.NewClient(weknora.ClientConfig{BaseURL: route.Target.WeKnora.BaseURL, APIKey: secret(route.Target.WeKnora.APIKeyFile), Timeout: 30 * time.Second, AllowDockerHTTP: true})
if err != nil {
fmt.Println(" client build FAIL", err)
return
}
knowledgeRemote, err := wknoraClient.GetKnowledge(ctx, *knowledgeID)
fmt.Println(" GetKnowledge err:", err, "id:", knowledgeRemote.ID, "kb:", knowledgeRemote.KnowledgeBaseID, "parse:", knowledgeRemote.ParseStatus)
chunksRemote, total, err := wknoraClient.ListChunks(ctx, *knowledgeID, 1, 5, nil)
fmt.Println(" ListChunks err:", err, "total:", total, "page:", len(chunksRemote))
probe := ""
for _, document := range searchSnapshot.Documents {
var fields struct {
Content string `json:"content"`
}
_ = json.Unmarshal(document.Source, &fields)
if utf8.RuneCountInString(fields.Content) >= 12 {
probe = string([]rune(fields.Content)[:min(48, utf8.RuneCountInString(fields.Content))])
break
}
}
results, err := wknoraClient.SearchKnowledge(ctx, weknora.SearchRequest{Query: probe, KnowledgeIDs: []string{*knowledgeID}})
fmt.Println(" SearchKnowledge err:", err, "probe_len:", len(probe), "results:", len(results))
for index, result := range results {
if index >= 3 {
break
}
fmt.Printf(" hit knowledge=%s score=%.3f type=%s\n", result.KnowledgeID, result.Score, result.ChunkType)
}
if len(results) == 0 && probe != "" {
results, err = wknoraClient.SearchKnowledge(ctx, weknora.SearchRequest{Query: "CRA 620mm", KnowledgeIDs: []string{*knowledgeID}})
fmt.Println(" short-probe SearchKnowledge err:", err, "results:", len(results))
}
}
func min(a, b int) int {
if a < b {
return a
}
return b
}
func newStore(ctx context.Context, object objectCfg) (*v071.S3ObjectStore, error) {
parsed, err := url.Parse(object.Endpoint)
if err != nil {
return nil, err
}
return v071.NewS3ObjectStore(v071.S3ObjectStoreConfig{
Provider: object.Provider, Endpoint: object.Endpoint, Region: object.Region,
AccessKeyID: secret(object.AccessKeyFile), SecretAccessKey: secret(object.SecretKeyFile),
Bucket: object.Bucket, Secure: parsed.Scheme == "https",
})
}
func newRawClient(ctx context.Context, object objectCfg) (*s3.Client, error) {
parsed, err := url.Parse(object.Endpoint)
if err != nil {
return nil, err
}
awsCfg, err := awsconfig.LoadDefaultConfig(ctx, awsconfig.WithRegion(object.Region), awsconfig.WithCredentialsProvider(
credentials.NewStaticCredentialsProvider(secret(object.AccessKeyFile), secret(object.SecretKeyFile), "")))
if err != nil {
return nil, err
}
return s3.NewFromConfig(awsCfg, func(options *s3.Options) {
options.BaseEndpoint = aws.String(object.Endpoint)
options.UsePathStyle = false
_ = parsed
}), nil
}
var _ = io.Discard