// Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package app import ( "context" "fmt" "io" "net/http" "time" authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/keychain" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/output" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/tui" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/upgrade" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition" "github.com/spf13/cobra" ) var ( doctorKeychainDiagnose = keychain.Diagnose doctorAuthStatus = (*authpkg.OAuthProvider).Status doctorAuthAccessToken = (*authpkg.OAuthProvider).GetAccessToken doctorHTTPDo = (*http.Client).Do doctorFetchLatestRelease = func() (*upgrade.ReleaseInfo, error) { return upgrade.NewClient().FetchLatestRelease() } doctorNeedsUpgrade = upgrade.NeedsUpgrade ) // checkStatus represents the outcome of a single doctor check. type checkStatus string const ( statusPass checkStatus = "pass" statusWarn checkStatus = "warn" statusFail checkStatus = "fail" ) // checkResult holds the outcome of a single doctor check. type checkResult struct { Name string `json:"name"` Status checkStatus `json:"status"` Message string `json:"message"` Hint string `json:"hint,omitempty"` Detail any `json:"detail,omitempty"` } func newDoctorCommand() *cobra.Command { cmd := &cobra.Command{ Use: "doctor", Short: "环境健康检查", Long: "一键检查登录态、网络连通性和版本更新,快速定位常见问题。", Args: cobra.NoArgs, DisableAutoGenTag: true, RunE: runDoctor, } cmd.Flags().Bool("json", false, "以 JSON 格式输出") cmd.Flags().Int("timeout", 10, "网络检查超时时间 (秒)") cmd.Flags().Bool("perf", false, "额外展示最近一次性能报告") return cmd } func runDoctor(cmd *cobra.Command, _ []string) error { jsonOut, _ := cmd.Flags().GetBool("json") timeout, _ := cmd.Flags().GetInt("timeout") if timeout <= 0 { timeout = 10 } networkTimeout := time.Duration(timeout) * time.Second w := cmd.OutOrStdout() checks := make([]checkResult, 0, 4) authResult := doctorCheckAuth(cmd.Context(), w, jsonOut) checks = append(checks, authResult) keychainResult := doctorCheckKeychain(w, jsonOut) checks = append(checks, keychainResult) networkResult := doctorCheckNetwork(cmd.Context(), w, jsonOut, networkTimeout) checks = append(checks, networkResult) versionResult := doctorCheckVersion(w, jsonOut, networkTimeout) checks = append(checks, versionResult) showPerf, _ := cmd.Flags().GetBool("perf") if showPerf { perfResult := doctorCheckPerf(w, jsonOut) checks = append(checks, perfResult) } pass, warn, fail := countResults(checks) if jsonOut { result := map[string]any{ "kind": "doctor", "checks": checks, "summary": map[string]int{ "pass": pass, "warn": warn, "fail": fail, }, } if showPerf { if report, err := LoadLatestReport(); err == nil { result["perf_report"] = report } } return output.WriteJSON(w, result) } fmt.Fprintf(w, "\n%s\n", tui.Header("Doctor", fmt.Sprintf("%d pass · %d warn · %d fail", pass, warn, fail))) fmt.Fprintf(w, "%s 诊断完成: %d 项通过, %d 项警告, %d 项失败\n", tui.StateMark("ok"), pass, warn, fail) if fail > 0 { return fmt.Errorf("诊断发现 %d 项失败", fail) } return nil } // ── Auth check ────────────────────────────────────────────────────────── func doctorCheckAuth(ctx context.Context, w io.Writer, jsonOut bool) checkResult { if !jsonOut { fmt.Fprint(w, tui.Dim("检查登录状态... ")) } configDir := defaultConfigDir() provider := authpkg.NewOAuthProvider(configDir, nil) configureOAuthProviderCompatibility(provider, configDir) data, err := doctorAuthStatus(provider) if err != nil || data == nil { if diagnostic := authStatusDiagnosticFromError(err); diagnostic != nil { r := checkResult{ Name: "auth", Status: statusFail, Message: diagnostic.Message, Hint: diagnostic.Hint, Detail: map[string]string{"reason": diagnostic.Reason}, } if !jsonOut { printCheckResult(w, r) } return r } r := checkResult{Name: "auth", Status: statusFail, Message: "未登录"} if !edition.Get().IsEmbedded { r.Hint = "运行 dws auth login 进行登录" } if !jsonOut { printCheckResult(w, r) } return r } if data.IsAccessTokenValid() || data.IsRefreshTokenValid() { if !data.IsAccessTokenValid() { refreshCtx, cancel := context.WithTimeout(ctx, 15*time.Second) _, refreshErr := doctorAuthAccessToken(provider, refreshCtx) cancel() if refreshErr != nil { r := checkResult{ Name: "auth", Status: statusWarn, Message: "Refresh Token 有效, 但自动刷新 Access Token 失败", Hint: "运行 dws auth login 重新登录", } if !jsonOut { printCheckResult(w, r) } return r } } r := checkResult{ Name: "auth", Status: statusPass, Message: "已登录", } if !jsonOut { printCheckResult(w, r) } return r } r := checkResult{Name: "auth", Status: statusFail, Message: "登录已过期"} if !edition.Get().IsEmbedded { r.Hint = "运行 dws auth login 重新登录" } if !jsonOut { printCheckResult(w, r) } return r } // ── Keychain check ───────────────────────────────────────────────────── func doctorCheckKeychain(w io.Writer, jsonOut bool) checkResult { if !jsonOut { fmt.Fprint(w, tui.Dim("检查钥匙串状态... ")) } diagnostic := doctorKeychainDiagnose() r := checkResult{ Name: "keychain", Status: statusPass, Message: diagnostic.Message, Detail: diagnostic.Detail, } if !diagnostic.OK { r.Status = statusFail r.Hint = diagnostic.Hint if diagnostic.Detail == nil { r.Detail = map[string]string{"reason": diagnostic.Reason} } else if diagnostic.Reason != "" { detail := make(map[string]string, len(diagnostic.Detail)+1) for k, v := range diagnostic.Detail { detail[k] = v } detail["reason"] = diagnostic.Reason r.Detail = detail } } if !jsonOut { printCheckResult(w, r) } return r } // ── Network check ─────────────────────────────────────────────────────── func doctorCheckNetwork(ctx context.Context, w io.Writer, jsonOut bool, timeout time.Duration) checkResult { if !jsonOut { fmt.Fprint(w, tui.Dim("检查网络连通性... ")) } baseURL := config.GetMCPBaseURL() httpClient := &http.Client{Timeout: timeout} start := time.Now() reqCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() req, err := http.NewRequestWithContext(reqCtx, http.MethodGet, baseURL, nil) if err != nil { r := checkResult{ Name: "network", Status: statusFail, Message: fmt.Sprintf("%s 不可达: %v", baseURL, err), Hint: "请检查网络连接或代理设置", } if !jsonOut { printCheckResult(w, r) } return r } resp, err := doctorHTTPDo(httpClient, req) latency := time.Since(start) if err != nil { r := checkResult{ Name: "network", Status: statusFail, Message: fmt.Sprintf("%s 不可达: %v", baseURL, err), Hint: "请检查网络连接或代理设置", } if !jsonOut { printCheckResult(w, r) } return r } resp.Body.Close() r := checkResult{ Name: "network", Status: statusPass, Message: fmt.Sprintf("%s 可达 (延迟 %dms)", baseURL, latency.Milliseconds()), } if !jsonOut { printCheckResult(w, r) } return r } // ── Version check ─────────────────────────────────────────────────────── func doctorCheckVersion(w io.Writer, jsonOut bool, timeout time.Duration) checkResult { if !jsonOut { fmt.Fprint(w, tui.Dim("检查版本更新... ")) } currentVer := version latest, err := doctorFetchLatestRelease() if err != nil { r := checkResult{ Name: "version", Status: statusFail, Message: fmt.Sprintf("无法获取最新版本: %v", err), Hint: "请检查网络连接", } if !jsonOut { printCheckResult(w, r) } return r } if doctorNeedsUpgrade(currentVer, latest.Version) { r := checkResult{ Name: "version", Status: statusWarn, Message: fmt.Sprintf("有新版本 (当前 %s, 最新 v%s)", ensureV(currentVer), latest.Version), Hint: "运行 dws upgrade 升级到最新版本", } if !jsonOut { printCheckResult(w, r) } return r } r := checkResult{ Name: "version", Status: statusPass, Message: fmt.Sprintf("已是最新版本 %s", ensureV(currentVer)), } if !jsonOut { printCheckResult(w, r) } return r } // ── Output helpers ────────────────────────────────────────────────────── func printCheckResult(w io.Writer, r checkResult) { icon := statusIcon(r.Status) message := r.Message switch r.Status { case statusPass: message = tui.Success(message) case statusWarn: message = tui.Warning(message) case statusFail: message = tui.Danger(message) } fmt.Fprintf(w, "%s %s\n", icon, message) if r.Hint != "" { fmt.Fprintf(w, " %s\n", tui.Dim(r.Hint)) } } func statusIcon(s checkStatus) string { switch s { case statusPass: return "✅" case statusWarn: return "⚠️" case statusFail: return "❌" default: return "?" } } func countResults(checks []checkResult) (pass, warn, fail int) { for _, c := range checks { switch c.Status { case statusPass: pass++ case statusWarn: warn++ case statusFail: fail++ } } return } // ── Perf report check ────────────────────────────────────────────────── func doctorCheckPerf(w io.Writer, jsonOut bool) checkResult { if !jsonOut { fmt.Fprint(w, tui.Dim("检查性能报告... ")) } report, err := LoadLatestReport() if err != nil { r := checkResult{ Name: "perf", Status: statusWarn, Message: "未找到性能报告", Hint: "设置 DWS_PERF_REPORT=auto 后运行任意命令生成报告", } if !jsonOut { printCheckResult(w, r) } return r } r := checkResult{ Name: "perf", Status: statusPass, Message: fmt.Sprintf("报告可用 (%s, %s)", report.Command, report.Timestamp.Local().Format("2006-01-02 15:04")), } if !jsonOut { printCheckResult(w, r) printPerfReportSummary(w, report) } return r } func printPerfReportSummary(w io.Writer, report *PerfReport) { fmt.Fprintf(w, "\n最近一次性能报告 (%s, %s):\n", report.Command, report.Timestamp.Local().Format("2006-01-02 15:04")) for _, p := range report.Phases { marker := "" if p.Name == report.Slowest { marker = " ← 最慢" } fmt.Fprintf(w, " %-25s %dms%s\n", p.Name, p.DurationMs, marker) } fmt.Fprintf(w, " %-25s ─────────\n", "─────────────────────────") fmt.Fprintf(w, " %-25s %dms (框架开销 %dms)\n", "总耗时", report.TotalMs, report.OverheadMs) }