This commit is contained in:
tianlei.qjb
2026-03-28 01:10:52 +08:00
2 changed files with 217 additions and 38 deletions
+187 -38
View File
@@ -15,7 +15,9 @@ package app
import (
"context"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
@@ -52,15 +54,29 @@ func buildAuthCommand() *cobra.Command {
newAuthStatusCommand(),
newAuthImportCommand(),
newAuthExchangeCommand(),
newAuthResetCommand(),
)
return cmd
}
func newAuthLoginCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "login",
Short: "登录钉钉(自动刷新 token,必要时扫码)",
Use: "login",
Short: "登录钉钉(自动刷新 token,必要时扫码)",
Long: `登录钉钉并获取认证凭证。
支持的登录方式:
- OAuth 设备流 (默认): 通过钉钉扫码授权登录
- 直接提供 Token: 通过 --token 参数传入已有 token
不支持的登录方式:
- 邮箱/密码登录
- 手机号/验证码登录
- 应用凭证 (AppKey/AppSecret) 直接登录
示例:
dws auth login # 扫码登录
dws auth login --force # 强制重新登录 (忽略缓存 token)
dws auth login --token xxx # 使用指定 token`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
cfg, err := resolveAuthLoginConfig(cmd)
@@ -105,6 +121,14 @@ func newAuthLoginCommand() *cobra.Command {
clearCompatCache()
w := cmd.OutOrStdout()
// Check if JSON output is requested
format, _ := cmd.Root().PersistentFlags().GetString("format")
if strings.EqualFold(strings.TrimSpace(format), "json") {
return writeAuthLoginJSON(w, tokenData, cfg.Force)
}
// Default table output
fmt.Fprintln(w)
if !cfg.Device && tokenData != nil && tokenData.IsAccessTokenValid() && !cfg.Force {
fmt.Fprintf(w, "[OK] Token 有效,无需重新登录\n")
@@ -130,22 +154,40 @@ func newAuthLoginCommand() *cobra.Command {
},
}
cmd.Flags().String("token", "", "Access token")
cmd.Flags().Bool("device", false, "Use device authorization flow (compatibility flag)")
cmd.Flags().Bool("force", false, "Force interactive login flow (compatibility flag)")
cmd.Flags().String("redirect-url", "", "Loopback redirect URL compatibility flag")
cmd.Flags().Bool("device", false, "Use device authorization flow")
cmd.Flags().Bool("force", false, "Force interactive login (ignore cached token)")
// Hidden compatibility flags
cmd.Flags().String("redirect-url", "", "Loopback redirect URL")
cmd.Flags().String("scopes", "", "Space-separated DingTalk OAuth scopes")
cmd.Flags().String("authorize-url", "", "Override DingTalk authorization URL")
cmd.Flags().String("token-url", "", "Override DingTalk token exchange URL")
cmd.Flags().String("refresh-url", "", "Override DingTalk refresh token URL")
cmd.Flags().Int("login-timeout", 0, "Compatibility flag for login timeout seconds")
cmd.Flags().Bool("no-browser", false, "Compatibility flag for browser launch suppression")
cmd.Flags().Int("login-timeout", 0, "Login timeout seconds")
cmd.Flags().Bool("no-browser", false, "Suppress browser launch")
_ = cmd.Flags().MarkHidden("redirect-url")
_ = cmd.Flags().MarkHidden("scopes")
_ = cmd.Flags().MarkHidden("authorize-url")
_ = cmd.Flags().MarkHidden("token-url")
_ = cmd.Flags().MarkHidden("refresh-url")
_ = cmd.Flags().MarkHidden("login-timeout")
_ = cmd.Flags().MarkHidden("no-browser")
return cmd
}
func newAuthLogoutCommand() *cobra.Command {
return &cobra.Command{
Use: "logout",
Short: "清除认证信息",
Use: "logout",
Short: "清除认证信息",
Long: `清除本地认证信息并撤销远程授权。
执行操作:
1. 向服务端发送撤销请求 (使 token 失效)
2. 删除本地存储的认证数据
3. 清除缓存
使用场景:
- 完全登出当前账号
- 遇到 "AUTH_TOKEN_EXPIRED" 或 "认证信息损坏" 错误时`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
configDir := defaultConfigDir()
@@ -159,7 +201,16 @@ func newAuthLogoutCommand() *cobra.Command {
_ = os.Remove(filepath.Join(configDir, "mcp_url"))
_ = os.Remove(filepath.Join(configDir, "token"))
clearCompatCache()
w := cmd.OutOrStdout()
// Check if JSON output is requested
format, _ := cmd.Root().PersistentFlags().GetString("format")
if strings.EqualFold(strings.TrimSpace(format), "json") {
return writeAuthLogoutJSON(w)
}
// Default table output
fmt.Fprintln(w, "[OK] 已清除所有认证信息")
fmt.Fprintln(w, "请运行 dws auth login 重新登录")
return nil
@@ -169,36 +220,51 @@ func newAuthLogoutCommand() *cobra.Command {
func newAuthStatusCommand() *cobra.Command {
return &cobra.Command{
Use: "status",
Short: "查看认证状态",
Use: "status",
Aliases: []string{"whoami"},
Short: "查看认证状态",
Long: `查看当前认证状态和用户信息。
示例:
dws auth status # 查看认证状态
dws auth status -f json # JSON 格式输出
dws auth whoami # 别名,与 status 相同`,
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
configDir := defaultConfigDir()
authenticated := false
updatedAt := ""
refreshed := false
var tokenData *authpkg.TokenData
provider := authpkg.NewOAuthProvider(configDir, nil)
configureOAuthProviderCompatibility(provider, configDir)
if data, err := provider.Status(); err == nil {
tokenData = data
if !data.IsAccessTokenValid() && data.IsRefreshTokenValid() {
refreshCtx, cancel := context.WithTimeout(cmd.Context(), 15*time.Second)
_, refreshErr := provider.GetAccessToken(refreshCtx)
cancel()
if refreshErr == nil {
if updatedData, statusErr := provider.Status(); statusErr == nil {
data = updatedData
tokenData = updatedData
refreshed = true
}
}
}
if authStatusAuthenticated(data) {
if authStatusAuthenticated(tokenData) {
authenticated = true
updatedAt = authStatusUpdatedAt(data)
}
}
w := cmd.OutOrStdout()
// Check if JSON output is requested
format, _ := cmd.Root().PersistentFlags().GetString("format")
if strings.EqualFold(strings.TrimSpace(format), "json") {
return writeAuthStatusJSON(w, authenticated, refreshed, tokenData)
}
// Default table output
if authenticated {
if refreshed {
fmt.Fprintf(w, "%-16s%s\n", "状态:", "已登录 ✅")
@@ -206,7 +272,7 @@ func newAuthStatusCommand() *cobra.Command {
} else {
fmt.Fprintf(w, "%-16s%s\n", "状态:", "已登录 ✅")
}
if updatedAt != "" {
if updatedAt := authStatusUpdatedAt(tokenData); updatedAt != "" {
fmt.Fprintf(w, "%-16s%s\n", "有效期:", updatedAt)
}
} else {
@@ -218,6 +284,110 @@ func newAuthStatusCommand() *cobra.Command {
}
}
// authStatusResponse is the JSON response for auth status command.
type authStatusResponse struct {
Success bool `json:"success"`
Authenticated bool `json:"authenticated"`
Message string `json:"message,omitempty"`
Refreshed bool `json:"refreshed,omitempty"`
TokenValid bool `json:"token_valid,omitempty"`
RefreshTokenValid bool `json:"refresh_token_valid,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
RefreshExpiresAt string `json:"refresh_expires_at,omitempty"`
CorpID string `json:"corp_id,omitempty"`
CorpName string `json:"corp_name,omitempty"`
UserID string `json:"user_id,omitempty"`
UserName string `json:"user_name,omitempty"`
}
func writeAuthStatusJSON(w io.Writer, authenticated, refreshed bool, data *authpkg.TokenData) error {
resp := authStatusResponse{
Success: true,
Authenticated: authenticated,
}
if !authenticated {
resp.Message = "未登录"
} else if data != nil {
resp.Refreshed = refreshed
resp.TokenValid = data.IsAccessTokenValid()
resp.RefreshTokenValid = data.IsRefreshTokenValid()
if !data.ExpiresAt.IsZero() {
resp.ExpiresAt = data.ExpiresAt.Format(time.RFC3339Nano)
}
if !data.RefreshExpAt.IsZero() {
resp.RefreshExpiresAt = data.RefreshExpAt.Format(time.RFC3339Nano)
}
resp.CorpID = data.CorpID
resp.CorpName = data.CorpName
resp.UserID = data.UserID
resp.UserName = data.UserName
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(resp)
}
// authLoginResponse is the JSON response for auth login command.
type authLoginResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
TokenValid bool `json:"token_valid,omitempty"`
RefreshTokenValid bool `json:"refresh_token_valid,omitempty"`
ExpiresAt string `json:"expires_at,omitempty"`
RefreshExpiresAt string `json:"refresh_expires_at,omitempty"`
CorpID string `json:"corp_id,omitempty"`
CorpName string `json:"corp_name,omitempty"`
UserID string `json:"user_id,omitempty"`
UserName string `json:"user_name,omitempty"`
}
func writeAuthLoginJSON(w io.Writer, data *authpkg.TokenData, forced bool) error {
resp := authLoginResponse{
Success: true,
Message: "登录成功",
}
if data != nil {
if data.IsAccessTokenValid() && !forced {
resp.Message = "Token 有效,无需重新登录"
}
resp.TokenValid = data.IsAccessTokenValid()
resp.RefreshTokenValid = data.IsRefreshTokenValid()
if !data.ExpiresAt.IsZero() {
resp.ExpiresAt = data.ExpiresAt.Format(time.RFC3339Nano)
}
if !data.RefreshExpAt.IsZero() {
resp.RefreshExpiresAt = data.RefreshExpAt.Format(time.RFC3339Nano)
}
resp.CorpID = data.CorpID
resp.CorpName = data.CorpName
resp.UserID = data.UserID
resp.UserName = data.UserName
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(resp)
}
// authLogoutResponse is the JSON response for auth logout command.
type authLogoutResponse struct {
Success bool `json:"success"`
Message string `json:"message"`
}
func writeAuthLogoutJSON(w io.Writer) error {
resp := authLogoutResponse{
Success: true,
Message: "已清除所有认证信息",
}
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
return enc.Encode(resp)
}
func newAuthImportCommand() *cobra.Command {
return &cobra.Command{
Use: "import <file>",
@@ -314,27 +484,6 @@ func newAuthExchangeCommand() *cobra.Command {
return cmd
}
func newAuthResetCommand() *cobra.Command {
return &cobra.Command{
Use: "reset",
Short: "重置认证信息(清除本地 Token,触发重新授权)",
DisableAutoGenTag: true,
RunE: func(cmd *cobra.Command, args []string) error {
configDir := defaultConfigDir()
if err := authpkg.DeleteTokenData(configDir); err != nil {
return apperrors.NewInternal(fmt.Sprintf("failed to reset token data: %v", err))
}
_ = os.Remove(filepath.Join(configDir, "mcp_url"))
_ = os.Remove(filepath.Join(configDir, "token"))
clearCompatCache()
w := cmd.OutOrStdout()
fmt.Fprintln(w, "[OK] 认证信息已重置")
fmt.Fprintln(w, "请运行 dws auth login 重新登录")
return nil
},
}
}
func timeOrEmpty(t time.Time) string {
if t.IsZero() {
return ""
+30
View File
@@ -69,6 +69,33 @@ func isUnknownCommandError(err error) bool {
return err != nil && strings.Contains(err.Error(), "unknown command")
}
// flagErrorWithSuggestions provides helpful suggestions for common flag mistakes.
func flagErrorWithSuggestions(cmd *cobra.Command, err error) error {
errMsg := err.Error()
// Common flag aliases and suggestions
suggestions := map[string]string{
"--json": "提示: 请使用 --format json 或 -f json 来输出 JSON 格式",
"--method": "提示: dws auth login 默认使用 OAuth 设备流登录,无需指定 --method",
"--device-flow": "提示: dws auth login 默认已使用设备流,无需 --device-flow 参数",
"--email": "提示: dws 不支持邮箱/密码登录,请使用 dws auth login 进行扫码登录",
"--code": "提示: dws 不支持验证码登录,请使用 dws auth login 进行扫码登录",
"--corp-id": "提示: corp-id 会在登录时自动获取,无需手动指定",
"--password": "提示: dws 不支持密码登录,请使用 dws auth login 进行扫码登录",
"--phone": "提示: dws 不支持手机号登录,请使用 dws auth login 进行扫码登录",
"--app-key": "提示: 请使用环境变量 DWS_CLIENT_ID 或 --client-id 设置 AppKey",
"--app-secret": "提示: 请使用环境变量 DWS_CLIENT_SECRET 或 --client-secret 设置 AppSecret",
}
for flag, suggestion := range suggestions {
if strings.Contains(errMsg, "unknown flag: "+flag) {
return fmt.Errorf("%w\n%s", err, suggestion)
}
}
return err
}
func printExecutionError(root *cobra.Command, stdout, stderr io.Writer, err error) error {
if wantsJSONErrors(root) {
return apperrors.PrintJSON(stdout, err)
@@ -170,6 +197,9 @@ func NewRootCommand(ctx ...context.Context) *cobra.Command {
},
}
// Set custom flag error handler for better UX
root.SetFlagErrorFunc(flagErrorWithSuggestions)
bindPersistentFlags(root, flags)
schemaCmd := newSchemaCommand(loader)