refactor: 改用 Cobra 命令行框架,reset-admin-password 带交互提示

- root.go 作为 Cobra 入口,DefaultRun 由 main 注入 serve()
- reset_admin.go 改为 Cobra 子命令,支持 --user/--password 参数
- 交互式:多管理员时选择目标、密码支持手动输入或随机生成(term.ReadPassword 不回显)
- 帮助输出自动生成,无需手写 Usage
- 删除旧的 flag 实现
This commit is contained in:
Sakurasan
2026-08-26 18:03:03 +08:00
parent 2983c89b42
commit 98f296101e
6 changed files with 232 additions and 87 deletions
+178
View File
@@ -0,0 +1,178 @@
// Package cmd 命令行入口。
package cmd
import (
"crypto/rand"
"fmt"
"log"
"math/big"
"os"
"strings"
"github.com/openteam/server/internal/config"
"github.com/openteam/server/internal/pkg/crypto"
"github.com/openteam/server/internal/store"
"github.com/spf13/cobra"
"golang.org/x/term"
"gorm.io/gorm"
)
// pwAlphabet 密码随机串字符集(去掉 0/O/1/l/I 等易混淆字符)。
const pwAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
var (
resetUser string
resetPassword string
)
var resetAdminPasswordCmd = &cobra.Command{
Use: "reset-admin-password",
Short: "重置管理员密码",
Long: `重置管理员密码(忘记密码时用,需在可加载配置的目录下执行)。
交互式引导(默认):
go run ./cmd/server reset-admin-password
非交互式(用于脚本):
go run ./cmd/server reset-admin-password --user admin --password "新密码"`,
Run: func(cmd *cobra.Command, args []string) {
runResetAdminPassword()
},
}
func init() {
rootCmd.AddCommand(resetAdminPasswordCmd)
resetAdminPasswordCmd.Flags().StringVar(&resetUser, "user", "", "目标管理员用户名(留空则交互选择)")
resetAdminPasswordCmd.Flags().StringVar(&resetPassword, "password", "", "新密码,至少 8 位(留空则交互输入)")
}
func runResetAdminPassword() {
// --- 阶段 1:加载配置与数据库 ---
cfg, err := config.Load()
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN)
if err != nil {
log.Fatalf("连接数据库失败: %v", err)
}
// --- 阶段 2:列出管理员 ---
var admins []struct {
ID uint64
Username string
}
db.Model(&store.User{}).Select("id, username").Where("role = ?", store.RoleAdmin).Find(&admins)
if len(admins) == 0 {
log.Fatalf("系统中没有管理员用户")
}
// --- 阶段 3:选择目标管理员 ---
target := targetAdmin(db, admins)
// --- 阶段 4:获取新密码 ---
var pw string
if resetPassword != "" {
pw = resetPassword
} else {
pw = promptPassword()
}
if pw == "" {
log.Fatalf("密码不能为空")
}
if len(pw) < 8 || len(pw) > 72 {
log.Fatalf("密码长度必须在 8 到 72 位之间")
}
// --- 阶段 5:加密并更新 ---
hasher := crypto.NewPasswordHasher(cfg.Auth.Argon2Time, cfg.Auth.Argon2Memory, cfg.Auth.Argon2Threads, cfg.Auth.Argon2KeyLen, cfg.Auth.SaltLen)
hash, err := hasher.HashPassword(pw)
if err != nil {
log.Fatalf("加密密码失败: %v", err)
}
if err := db.Model(&store.User{}).Where("id = ?", target.ID).Update("password_hash", hash).Error; err != nil {
log.Fatalf("更新密码失败: %v", err)
}
fmt.Printf("✅ 已重置管理员 %q 的密码!\n", target.Username)
}
// targetAdmin 解析 --user 或交互选择目标管理员。
func targetAdmin(db *gorm.DB, admins []struct {
ID uint64
Username string
}) *store.User {
if resetUser != "" {
for i := range admins {
if admins[i].Username == resetUser {
return &store.User{ID: admins[i].ID, Username: admins[i].Username}
}
}
log.Fatalf("管理员 %q 不存在", resetUser)
}
if len(admins) == 1 {
return &store.User{ID: admins[0].ID, Username: admins[0].Username}
}
return selectAdmin(admins)
}
// selectAdmin 交互式选择管理员用户。
func selectAdmin(admins []struct {
ID uint64
Username string
}) *store.User {
fmt.Println("检测到多个管理员用户,请选择要重置密码的目标:")
for i, a := range admins {
fmt.Printf(" [%d] %s\n", i+1, a.Username)
}
fmt.Printf("请输入编号 (1-%d): ", len(admins))
var n int
if _, err := fmt.Scanf("%d", &n); err != nil || n < 1 || n > len(admins) {
log.Fatalf("无效输入")
}
return &store.User{ID: admins[n-1].ID, Username: admins[n-1].Username}
}
// promptPassword 交互式输入密码,并提供两种方式选择。
func promptPassword() string {
fmt.Println("请选择密码设置方式:")
fmt.Println(" [1] 手动输入密码")
fmt.Println(" [2] 自动生成随机密码(16 位,不含易混淆字符)")
fmt.Printf("请输入编号 (1-2): ")
var n int
if _, err := fmt.Scanf("%d", &n); err != nil || n < 1 || n > 2 {
log.Fatalf("无效输入,请输入 1 或 2")
}
if n == 2 {
pw, err := randomPassword(16)
if err != nil {
log.Fatalf("生成随机密码失败: %v", err)
}
fmt.Printf("🔑 生成的随机密码:%s\n", pw)
fmt.Println("(请立即保存,不会再次显示)")
return pw
}
// 手动输入:静默输入(不回显)
fmt.Print("请输入新密码(至少 8 位):")
raw, err := term.ReadPassword(int(os.Stdin.Fd()))
fmt.Println()
if err != nil {
log.Fatalf("读取密码失败: %v", err)
}
return strings.TrimSpace(string(raw))
}
// randomPassword 用 crypto/rand 从 pwAlphabet 生成 n 位随机密码。
func randomPassword(n int) (string, error) {
buf := make([]byte, n)
for i := range buf {
k, err := rand.Int(rand.Reader, big.NewInt(int64(len(pwAlphabet))))
if err != nil {
return "", err
}
buf[i] = pwAlphabet[k.Int64()]
}
return string(buf), nil
}
+33
View File
@@ -0,0 +1,33 @@
// Package cmd 命令行入口(Cobra 子命令分发)。
package cmd
import (
"github.com/spf13/cobra"
)
// DefaultRun 无子命令时的默认行为(由 main 注入服务启动函数)。
var DefaultRun func()
// Execute 启动根命令。
func Execute() {
cobra.CheckErr(rootCmd.Execute())
}
var rootCmd = &cobra.Command{
Use: "openteam",
Short: "openteam · LLM API 中转站",
Long: `openteam 大模型中转站 API 服务。
直接运行(无子命令)启动 HTTP 服务:
go run ./cmd/server
子命令:
reset-admin-password 重置管理员密码(交互式,无参数时自动提示)`,
Run: func(c *cobra.Command, args []string) {
if DefaultRun != nil {
DefaultRun()
} else {
_ = c.Help()
}
},
}
+9 -6
View File
@@ -13,19 +13,22 @@ import (
"syscall" "syscall"
"time" "time"
"github.com/openteam/server/cmd/server/cmd"
"github.com/openteam/server/internal/api" "github.com/openteam/server/internal/api"
"github.com/openteam/server/internal/app" "github.com/openteam/server/internal/app"
"github.com/openteam/server/internal/config" "github.com/openteam/server/internal/config"
"github.com/openteam/server/internal/proxy" "github.com/openteam/server/internal/proxy"
) )
func main() { func init() {
// 子命令分发:`openteam reset-admin-password ...`(无子命令时启动服务) cmd.DefaultRun = serve
if len(os.Args) > 1 && os.Args[1] == "reset-admin-password" { }
runResetAdminPassword(os.Args[2:])
return
}
func main() {
cmd.Execute()
}
func serve() {
cfg, err := config.Load() cfg, err := config.Load()
if err != nil { if err != nil {
log.Fatalf("config: %v", err) log.Fatalf("config: %v", err)
-81
View File
@@ -1,81 +0,0 @@
// 子命令 reset-admin-password:重置管理员密码(忘记密码时用,需在 server/ 目录或配置可加载处执行)。
// 用法:go run ./cmd/server reset-admin-password [-user admin] [-password 新密码]
package main
import (
"crypto/rand"
"flag"
"fmt"
"log"
"math/big"
"github.com/openteam/server/internal/config"
"github.com/openteam/server/internal/pkg/crypto"
"github.com/openteam/server/internal/store"
)
// pwAlphabet 密码随机串字符集(去掉 0/O/1/l/I 等易混淆字符)。
const pwAlphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz"
func runResetAdminPassword(args []string) {
fs := flag.NewFlagSet("reset-admin-password", flag.ExitOnError)
user := fs.String("user", "admin", "目标管理员用户名")
password := fs.String("password", "", "新密码(至少 8 位;留空则随机生成并打印一次)")
_ = fs.Parse(args)
cfg, err := config.Load()
if err != nil {
log.Fatalf("config: %v", err)
}
db, err := store.Open(cfg.DB.Driver, cfg.DB.DSN)
if err != nil {
log.Fatalf("db: %v", err)
}
var u store.User
if err := db.Where("username = ? AND role = ?", *user, store.RoleAdmin).First(&u).Error; err != nil {
log.Fatalf("admin user %q not found", *user)
}
pw := *password
generated := false
if pw == "" {
pw, err = randomPassword(16)
if err != nil {
log.Fatalf("generate password: %v", err)
}
generated = true
}
if len(pw) < 8 || len(pw) > 72 {
log.Fatal("password length must be between 8 and 72")
}
hasher := crypto.NewPasswordHasher(cfg.Auth.Argon2Time, cfg.Auth.Argon2Memory, cfg.Auth.Argon2Threads, cfg.Auth.Argon2KeyLen, cfg.Auth.SaltLen)
hash, err := hasher.HashPassword(pw)
if err != nil {
log.Fatalf("hash password: %v", err)
}
if err := db.Model(&store.User{}).Where("id = ?", u.ID).Update("password_hash", hash).Error; err != nil {
log.Fatalf("update password: %v", err)
}
if generated {
fmt.Printf("已重置 %q 的密码:%s\n", u.Username, pw)
fmt.Println("(请立即保存,不会再次显示)")
} else {
fmt.Printf("已重置 %q 的密码。\n", u.Username)
}
}
// randomPassword 用 crypto/rand 从 pwAlphabet 生成 n 位随机密码。
func randomPassword(n int) (string, error) {
buf := make([]byte, n)
for i := range buf {
k, err := rand.Int(rand.Reader, big.NewInt(int64(len(pwAlphabet))))
if err != nil {
return "", err
}
buf[i] = pwAlphabet[k.Int64()]
}
return string(buf), nil
}
+3
View File
@@ -7,9 +7,11 @@ require (
github.com/glebarez/sqlite v1.11.0 github.com/glebarez/sqlite v1.11.0
github.com/go-webauthn/webauthn v0.17.4 github.com/go-webauthn/webauthn v0.17.4
github.com/golang-jwt/jwt/v5 v5.3.1 github.com/golang-jwt/jwt/v5 v5.3.1
github.com/spf13/cobra v1.10.2
github.com/spf13/viper v1.21.0 github.com/spf13/viper v1.21.0
github.com/tiktoken-go/tokenizer v0.8.1 github.com/tiktoken-go/tokenizer v0.8.1
golang.org/x/crypto v0.55.0 golang.org/x/crypto v0.55.0
golang.org/x/term v0.45.0
gorm.io/driver/postgres v1.6.2 gorm.io/driver/postgres v1.6.2
gorm.io/gorm v1.31.2 gorm.io/gorm v1.31.2
) )
@@ -35,6 +37,7 @@ require (
github.com/goccy/go-yaml v1.19.2 // indirect github.com/goccy/go-yaml v1.19.2 // indirect
github.com/google/go-tpm v0.9.8 // indirect github.com/google/go-tpm v0.9.8 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.10.0 // indirect github.com/jackc/pgx/v5 v5.10.0 // indirect
+9
View File
@@ -6,6 +6,7 @@ github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiD
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -60,6 +61,8 @@ github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbu
github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo= github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
@@ -106,6 +109,7 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik= github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw= github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
@@ -114,6 +118,9 @@ github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg= github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY= github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU= github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
@@ -159,6 +166,8 @@ golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.45.0 h1:NwWyBmoJCbfTHpxrWoZ9C6/VxOf7ic219I8xZZFdrf0=
golang.org/x/term v0.45.0/go.mod h1:9aqxs0blBcrm/n0L9QW0aRVD+ktan8ssZromtqJC43w=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=