refactor: 命令树移入 internal/cli,去掉 DefaultRun 注入
- cmd/server/cmd → internal/cli,与 internal/api、internal/proxy 平级 - serve() 移入 cli 包作为默认子命令,main.go 退化为纯入口 - 消除 cmd/cmd 冗余层级和 main → cmd 的回调注入
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
// Package cli 命令行入口。
|
||||
package cli
|
||||
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Package cli 命令行入口(Cobra 子命令分发)。
|
||||
package cli
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// 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 重置管理员密码(交互式,无参数时自动提示)`,
|
||||
RunE: func(c *cobra.Command, args []string) error {
|
||||
return serve()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
// Package cli 命令行入口。
|
||||
package cli
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/openteam/server/internal/api"
|
||||
"github.com/openteam/server/internal/app"
|
||||
"github.com/openteam/server/internal/config"
|
||||
"github.com/openteam/server/internal/proxy"
|
||||
)
|
||||
|
||||
// serve 启动 HTTP 服务(默认子命令)。
|
||||
func serve() error {
|
||||
cfg, err := config.Load()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
a, err := app.New(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer a.Shutdown(context.Background())
|
||||
|
||||
gw := proxy.NewGateway(a.DB, a.Enc, a.Usage, a.Limit, cfg.RateLimit.UserRPS, cfg.Proxy.LogRaw)
|
||||
router := api.NewRouter(a, gw)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":" + strconv.Itoa(cfg.Port),
|
||||
Handler: router,
|
||||
ReadHeaderTimeout: 10 * time.Second,
|
||||
}
|
||||
|
||||
go func() {
|
||||
log.Printf("openteam listening on %s (env=%s)", srv.Addr, cfg.Env)
|
||||
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Fatalf("server: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
|
||||
log.Println("shutting down...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Printf("server shutdown: %v", err)
|
||||
}
|
||||
a.Usage.Close()
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user