Files
story/cmd/agent-eval-register/main.go
T

147 lines
4.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"context"
"encoding/json"
"errors"
"flag"
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
"data-terminal-cloud/internal/agenteval"
"github.com/google/uuid"
)
func main() {
os.Exit(run(os.Args[1:], os.Stdout, os.Stderr))
}
func run(args []string, stdout, stderr io.Writer) int {
flags := flag.NewFlagSet("agent-eval-register", flag.ContinueOnError)
flags.SetOutput(stderr)
suitePath := flags.String("suite", "", "评测集JSON路径")
observationsPath := flags.String("observations", "", "生成本报告的脱敏观测JSON路径")
reportPath := flags.String("report", "", "agent-eval生成的完整报告路径")
tenantID := flags.String("tenant", "", "运营范围客户组织UUID")
configID := flags.String("config", "", "待登记证据的运行配置版本UUID")
apiBaseURL := flags.String("api", "http://127.0.0.1:58080", "SaaS API基础地址")
tokenFile := flags.String("access-token-file", "", "OIDC访问令牌文件;省略时读取AGENT_EVAL_ACCESS_TOKEN")
allowFailed := flags.Bool("allow-failed", false, "明确允许登记FAILED报告;FAILED不会满足发布门禁")
dryRun := flags.Bool("dry-run", false, "只校验并显示登记摘要,不发送请求")
if err := flags.Parse(args); err != nil {
return 2
}
if *suitePath == "" || *observationsPath == "" || *reportPath == "" || *tenantID == "" || *configID == "" || flags.NArg() != 0 {
fmt.Fprintln(stderr, "usage: agent-eval-register -suite <suite.json> -observations <observations.json> -report <report.json> -tenant <uuid> -config <uuid> [-api <url>] [-access-token-file <path>] [-allow-failed] [-dry-run]")
return 2
}
if _, err := uuid.Parse(*tenantID); err != nil {
fmt.Fprintln(stderr, "tenant必须是有效UUID")
return 2
}
if _, err := uuid.Parse(*configID); err != nil {
fmt.Fprintln(stderr, "config必须是有效UUID")
return 2
}
var suite agenteval.Suite
if err := decodeFile(*suitePath, &suite); err != nil {
fmt.Fprintf(stderr, "读取评测集失败: %v\n", err)
return 2
}
var artifact agenteval.Artifact
if err := decodeFile(*reportPath, &artifact); err != nil {
fmt.Fprintf(stderr, "读取评测报告失败: %v\n", err)
return 2
}
var observations agenteval.ObservationRun
if err := decodeFile(*observationsPath, &observations); err != nil {
fmt.Fprintf(stderr, "读取评测观测失败: %v\n", err)
return 2
}
if err := agenteval.ValidateArtifact(suite, observations, artifact, *allowFailed); err != nil {
fmt.Fprintf(stderr, "评测报告不能登记: %v\n", err)
return 2
}
key, err := agenteval.RegistrationIdempotencyKey(*configID, artifact.RegistrationPayload)
if err != nil {
fmt.Fprintf(stderr, "生成幂等键失败: %v\n", err)
return 2
}
if *dryRun {
summary := map[string]any{
"dry_run": true, "tenant_id": *tenantID, "config_version_id": *configID,
"suite_version": artifact.Report.SuiteVersion, "suite_sha256": artifact.Report.SuiteSHA256,
"agent_version": artifact.Report.AgentVersion, "result": artifact.Report.Result,
"report_ref": artifact.RegistrationPayload.ReportRef, "idempotency_key": key,
}
_ = json.NewEncoder(stdout).Encode(summary)
return 0
}
token, err := readToken(*tokenFile)
if err != nil {
fmt.Fprintf(stderr, "读取访问令牌失败: %v\n", err)
return 2
}
registrar, err := agenteval.NewRegistrar(*apiBaseURL, token, &http.Client{Timeout: 15 * time.Second})
if err != nil {
fmt.Fprintf(stderr, "创建登记客户端失败: %v\n", err)
return 2
}
result, err := registrar.Register(context.Background(), *tenantID, *configID, artifact.RegistrationPayload)
if err != nil {
fmt.Fprintf(stderr, "登记评测证据失败: %v;request_id=%s\n", err, result.RequestID)
return 1
}
_ = json.NewEncoder(stdout).Encode(map[string]any{
"evaluation_id": result.EvaluationID, "status_code": result.StatusCode,
"idempotency_replayed": result.Replayed, "request_id": result.RequestID,
"idempotency_key": key,
})
return 0
}
func readToken(path string) (string, error) {
if strings.TrimSpace(path) == "" {
value := strings.TrimSpace(os.Getenv("AGENT_EVAL_ACCESS_TOKEN"))
if value == "" {
return "", errors.New("set AGENT_EVAL_ACCESS_TOKEN or -access-token-file")
}
return value, nil
}
content, err := os.ReadFile(path)
if err != nil {
return "", err
}
value := strings.TrimSpace(string(content))
if value == "" || strings.ContainsAny(value, "\r\n\x00") {
return "", errors.New("token file must contain one non-empty line")
}
return value, nil
}
func decodeFile(path string, target any) error {
file, err := os.Open(path)
if err != nil {
return err
}
defer file.Close()
decoder := json.NewDecoder(file)
decoder.DisallowUnknownFields()
if err = decoder.Decode(target); err != nil {
return err
}
var trailing any
if err = decoder.Decode(&trailing); !errors.Is(err, io.EOF) {
if err == nil {
return errors.New("JSON文件包含多个顶层值")
}
return err
}
return nil
}