Files
knowledge-governance/cmd/test-publication-seed/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

132 lines
4.2 KiB
Go

package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"log"
"os"
"strconv"
"time"
"github.com/jackc/pgx/v5/pgxpool"
postgresstore "knowledge-governance/internal/platform/postgres"
"knowledge-governance/internal/platform/spool"
)
type manualSeeder interface {
SeedManualPublication(context.Context, postgresstore.ManualPublicationSeedRequest) (postgresstore.ManualPublicationSeedResult, error)
}
type seedConfig struct {
SpoolRoot string
SpoolRef string
Format string
Title string
TestKBID string
MaxBytes int64
Now time.Time
}
func main() {
if err := run(context.Background(), os.Args[1:], os.Getenv, os.Stdout); err != nil {
log.Fatal(err)
}
}
func run(ctx context.Context, args []string, getenv func(string) string, output io.Writer) error {
flags := flag.NewFlagSet("test-publication-seed", flag.ContinueOnError)
flags.SetOutput(io.Discard)
spoolRef := flags.String("spool-ref", "", "opaque relative spool reference")
format := flags.String("format", "md", "validated artifact format")
title := flags.String("title", "", "safe display title")
if err := flags.Parse(args); err != nil {
return errors.New("invalid seed arguments")
}
databaseURL := getenv("DATABASE_URL")
spoolRoot := getenv("GOVERNANCE_SPOOL_ROOT")
if spoolRoot == "" {
spoolRoot = "/var/lib/governance/spool"
}
testKBID := getenv("TEST_WEKNORA_KB_ID")
maxBytes := int64(50 * 1024 * 1024)
if raw := getenv("TEST_WEKNORA_MAX_UPLOAD_BYTES"); raw != "" {
parsed, err := strconv.ParseInt(raw, 10, 64)
if err != nil || parsed <= 0 || parsed > 50*1024*1024 {
return errors.New("invalid TEST_WEKNORA_MAX_UPLOAD_BYTES")
}
maxBytes = parsed
}
if databaseURL == "" || testKBID == "" || *spoolRef == "" || *title == "" {
return errors.New("DATABASE_URL, TEST_WEKNORA_KB_ID, --spool-ref and --title are required")
}
pool, err := pgxpool.New(ctx, databaseURL)
if err != nil {
return errors.New("open governance database")
}
defer pool.Close()
if err := pool.Ping(ctx); err != nil {
return errors.New("connect governance database")
}
if err := postgresstore.ApplyMigrations(ctx, pool); err != nil {
return errors.New("apply governance migrations")
}
result, err := seedFromSpool(ctx, postgresstore.NewStore(pool), seedConfig{
SpoolRoot: spoolRoot,
SpoolRef: *spoolRef,
Format: *format,
Title: *title,
TestKBID: testKBID,
MaxBytes: maxBytes,
Now: time.Now().UTC(),
})
if err != nil {
return err
}
return json.NewEncoder(output).Encode(result)
}
func seedFromSpool(ctx context.Context, store manualSeeder, config seedConfig) (postgresstore.ManualPublicationSeedResult, error) {
if store == nil {
return postgresstore.ManualPublicationSeedResult{}, errors.New("manual publication store is required")
}
expectation, err := spool.InspectVerified(config.SpoolRoot, config.SpoolRef, config.Format, config.MaxBytes)
if err != nil {
return postgresstore.ManualPublicationSeedResult{}, errors.New("inspect manual spool artifact")
}
mime, ok := mimeForFormat(config.Format)
if !ok {
return postgresstore.ManualPublicationSeedResult{}, errors.New("unsupported manual publication format")
}
result, err := store.SeedManualPublication(ctx, postgresstore.ManualPublicationSeedRequest{
TestKBID: config.TestKBID,
Title: config.Title,
SHA256: expectation.SHA256,
SizeBytes: expectation.SizeBytes,
MIME: mime,
ValidatedFormat: expectation.Format,
LocalSpoolRef: expectation.RelativeRef,
Now: config.Now,
})
if err != nil {
return postgresstore.ManualPublicationSeedResult{}, fmt.Errorf("seed manual publication: %w", err)
}
return result, nil
}
func mimeForFormat(format string) (string, bool) {
mimes := map[string]string{
"md": "text/markdown", "txt": "text/plain", "csv": "text/csv", "json": "application/json",
"pdf": "application/pdf", "docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
"doc": "application/msword", "xls": "application/vnd.ms-excel", "ppt": "application/vnd.ms-powerpoint",
}
value, ok := mimes[format]
return value, ok
}