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 干净
154 lines
4.3 KiB
Go
154 lines
4.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
ppconnector "knowledge-governance/internal/connectors/pp"
|
|
postgresstore "knowledge-governance/internal/platform/postgres"
|
|
)
|
|
|
|
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, stdout io.Writer) error {
|
|
flags := flag.NewFlagSet("pp-ledger-import", flag.ContinueOnError)
|
|
flags.SetOutput(io.Discard)
|
|
ledgerPath := flags.String("ledger", "", "path to the legacy PP state JSONL")
|
|
manifestPath := flags.String("manifest", "", "path to the validated download manifest JSON")
|
|
dryRun := flags.Bool("dry-run", false, "validate and print aggregate counts without database access")
|
|
if err := flags.Parse(args); err != nil {
|
|
return fmt.Errorf("parse arguments: %w", err)
|
|
}
|
|
if *ledgerPath == "" || *manifestPath == "" {
|
|
return fmt.Errorf("-ledger and -manifest are required")
|
|
}
|
|
|
|
ledger, err := os.Open(*ledgerPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open PP ledger: %w", err)
|
|
}
|
|
snapshot, foldErr := ppconnector.FoldLedger(ledger)
|
|
closeErr := ledger.Close()
|
|
if foldErr != nil {
|
|
return foldErr
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("close PP ledger: %w", closeErr)
|
|
}
|
|
|
|
manifest, err := os.Open(*manifestPath)
|
|
if err != nil {
|
|
return fmt.Errorf("open PP download manifest: %w", err)
|
|
}
|
|
recovered, recovery, recoveryErr := ppconnector.RecoverMissingSHA256(snapshot, manifest)
|
|
closeErr = manifest.Close()
|
|
if recoveryErr != nil {
|
|
return recoveryErr
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("close PP download manifest: %w", closeErr)
|
|
}
|
|
overLimitManifest, err := os.Open(*manifestPath)
|
|
if err != nil {
|
|
return fmt.Errorf("reopen PP download manifest: %w", err)
|
|
}
|
|
complete, overLimit, overLimitErr := ppconnector.MergeOverLimitManifest(recovered, overLimitManifest)
|
|
closeErr = overLimitManifest.Close()
|
|
if overLimitErr != nil {
|
|
return overLimitErr
|
|
}
|
|
if closeErr != nil {
|
|
return fmt.Errorf("close PP download manifest: %w", closeErr)
|
|
}
|
|
if err := ppconnector.ValidateSnapshot(complete); err != nil {
|
|
return fmt.Errorf("validate PP snapshot: %w", err)
|
|
}
|
|
plan, err := ppconnector.BuildImportPlan(complete)
|
|
if err != nil {
|
|
return fmt.Errorf("build PP import plan: %w", err)
|
|
}
|
|
|
|
completed := 0
|
|
aliases := 0
|
|
for _, item := range complete.Items {
|
|
switch item.Status {
|
|
case ppconnector.StatusCompleted:
|
|
completed++
|
|
case ppconnector.StatusContentAlias:
|
|
aliases++
|
|
}
|
|
}
|
|
_, _ = fmt.Fprintf(stdout,
|
|
"events=%d\nsource_items=%d\ncompleted=%d\ncontent_aliases=%d\nover_limit=%d\nrecovered_sha256=%d\nverified_bytes=%d\ncontent_objects=%d\nversions=%d\npublications=%d\n",
|
|
complete.EventCount,
|
|
len(plan.SourceItems),
|
|
completed,
|
|
aliases,
|
|
overLimit.Added,
|
|
recovery.Recovered,
|
|
recovery.VerifiedBytes,
|
|
len(plan.ContentObjects),
|
|
len(plan.Versions),
|
|
len(plan.Publications),
|
|
)
|
|
if *dryRun {
|
|
_, _ = fmt.Fprintln(stdout, "dry_run=true")
|
|
return nil
|
|
}
|
|
|
|
databaseURL := strings.TrimSpace(getenv("DATABASE_URL"))
|
|
if databaseURL == "" {
|
|
return fmt.Errorf("DATABASE_URL is required unless -dry-run is used")
|
|
}
|
|
pool, err := pgxpool.New(ctx, databaseURL)
|
|
if err != nil {
|
|
return fmt.Errorf("open governance database: %w", err)
|
|
}
|
|
defer pool.Close()
|
|
if err := pool.Ping(ctx); err != nil {
|
|
return fmt.Errorf("ping governance database: %w", err)
|
|
}
|
|
if err := postgresstore.ApplyMigrations(ctx, pool); err != nil {
|
|
return err
|
|
}
|
|
report, err := postgresstore.ImportPPLegacy(ctx, pool, plan)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(stdout,
|
|
"inserted_connections=%d\ninserted_source_items=%d\ninserted_content_objects=%d\ninserted_versions=%d\ninserted_publications=%d\n",
|
|
report.Connections,
|
|
report.SourceItems,
|
|
report.ContentObjects,
|
|
report.Versions,
|
|
report.Publications,
|
|
)
|
|
current, err := postgresstore.LoadPPCurrentItems(ctx, pool, plan.Connection.ID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
reconciliation, err := ppconnector.PlanReconciliation(complete, current)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, _ = fmt.Fprintf(stdout,
|
|
"reconcile_unchanged=%d\nreconcile_adopted=%d\nreconcile_over_limit=%d\nplanned_uploads=%d\n",
|
|
reconciliation.Unchanged,
|
|
reconciliation.Adopted,
|
|
reconciliation.OverLimit,
|
|
reconciliation.UploadRequired,
|
|
)
|
|
return nil
|
|
}
|