From cc04a082b189b9e3d98137044b352847abbda414 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=8F=E8=8D=A3?= Date: Fri, 27 Mar 2026 21:12:49 +0800 Subject: [PATCH 1/2] init --- internal/app/auth_command.go | 84 +++++++++++++++++++++++++++++++++--- internal/app/root.go | 30 +++++++++++++ 2 files changed, 107 insertions(+), 7 deletions(-) diff --git a/internal/app/auth_command.go b/internal/app/auth_command.go index 2ca3b636..1a98b428 100644 --- a/internal/app/auth_command.go +++ b/internal/app/auth_command.go @@ -15,7 +15,9 @@ package app import ( "context" + "encoding/json" "fmt" + "io" "os" "path/filepath" "strings" @@ -59,8 +61,23 @@ func buildAuthCommand() *cobra.Command { 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) @@ -176,29 +193,37 @@ func newAuthStatusCommand() *cobra.Command { 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 +231,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 +243,51 @@ 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) +} + func newAuthImportCommand() *cobra.Command { return &cobra.Command{ Use: "import ", diff --git a/internal/app/root.go b/internal/app/root.go index 899b267f..864013b0 100644 --- a/internal/app/root.go +++ b/internal/app/root.go @@ -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) From 045a3dcf311415fdd4bc544452f0817212101118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9F=8F=E8=8D=A3?= Date: Fri, 27 Mar 2026 22:13:02 +0800 Subject: [PATCH 2/2] =?UTF-8?q?=E4=BC=98=E5=8C=96auth=E5=91=BD=E4=BB=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/app/auth_command.go | 141 +++++++++++++++++++++++++++-------- 1 file changed, 110 insertions(+), 31 deletions(-) diff --git a/internal/app/auth_command.go b/internal/app/auth_command.go index 1a98b428..86ad1d7f 100644 --- a/internal/app/auth_command.go +++ b/internal/app/auth_command.go @@ -54,7 +54,6 @@ func buildAuthCommand() *cobra.Command { newAuthStatusCommand(), newAuthImportCommand(), newAuthExchangeCommand(), - newAuthResetCommand(), ) return cmd } @@ -122,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") @@ -147,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() @@ -176,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 @@ -186,8 +220,15 @@ 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() @@ -288,6 +329,65 @@ func writeAuthStatusJSON(w io.Writer, authenticated, refreshed bool, data *authp 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 ", @@ -384,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 ""