package app import ( "context" "encoding/json" "errors" "fmt" "io" "net/http" "regexp" "strconv" "strings" "time" ) const coreReleaseEndpoint = "https://api.github.com/repos/MetaCubeX/mihomo/releases/latest" var stableVersionPattern = regexp.MustCompile(`^v?(\d+)\.(\d+)\.(\d+)$`) type coreRelease struct { Tag string `json:"tag_name"` Published string `json:"published_at"` Draft bool `json:"draft"` Prerelease bool `json:"prerelease"` } func stableVersion(version string) ([3]int, bool) { var parts [3]int match := stableVersionPattern.FindStringSubmatch(strings.TrimSpace(version)) if len(match) != 4 { return parts, false } for i := range parts { n, err := strconv.Atoi(match[i+1]) if err != nil { return parts, false } parts[i] = n } return parts, true } func newerStableVersion(current, latest string) *bool { a, currentOK := stableVersion(current) b, latestOK := stableVersion(latest) if !currentOK || !latestOK { return nil } newer := false for i := range a { if b[i] != a[i] { newer = b[i] > a[i] break } } return &newer } func fetchCoreRelease(ctx context.Context, client *http.Client) (*coreRelease, error) { ctx, cancel := context.WithTimeout(ctx, 10*time.Second) defer cancel() req, err := http.NewRequestWithContext(ctx, http.MethodGet, coreReleaseEndpoint, nil) if err != nil { return nil, err } req.Header.Set("Accept", "application/vnd.github+json") req.Header.Set("User-Agent", "ClashWeb-update-check") resp, err := client.Do(req) if err != nil { return nil, errors.New("无法连接 GitHub,请检查网络后重试") } defer resp.Body.Close() if resp.StatusCode == http.StatusForbidden || resp.StatusCode == http.StatusTooManyRequests { return nil, errors.New("GitHub 更新检查暂时受限,请稍后重试") } if resp.StatusCode != http.StatusOK { return nil, fmt.Errorf("更新检查失败(HTTP %d)", resp.StatusCode) } data, err := io.ReadAll(io.LimitReader(resp.Body, (2<<20)+1)) if err != nil || len(data) > 2<<20 { return nil, errors.New("版本信息读取失败") } var release coreRelease if err := json.Unmarshal(data, &release); err != nil { return nil, errors.New("版本信息格式错误") } if _, ok := stableVersion(release.Tag); !ok || release.Draft || release.Prerelease { return nil, errors.New("未获取到有效的 Mihomo 稳定版本") } return &release, nil } func (a *App) latestCoreRelease(ctx context.Context) (*coreRelease, time.Time, error) { a.updatesMu.Lock() defer a.updatesMu.Unlock() if a.updateCache != nil && time.Since(a.updateTime) < 10*time.Minute { return a.updateCache, a.updateTime, nil } release, err := fetchCoreRelease(ctx, a.download) if err != nil { return nil, time.Time{}, err } a.updateCache, a.updateTime = release, time.Now() return release, a.updateTime, nil } func (a *App) coreUpdates(w http.ResponseWriter, r *http.Request) { release, checked, err := a.latestCoreRelease(r.Context()) if err != nil { writeError(w, http.StatusBadGateway, err.Error()) return } var version struct { Version string `json:"version"` } ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second) defer cancel() _ = a.core.Request(ctx, http.MethodGet, "/version", nil, &version) jsonResponse(w, http.StatusOK, map[string]any{ "currentVersion": version.Version, "latestVersion": release.Tag, "updateAvailable": newerStableVersion(version.Version, release.Tag), "checkedAt": checked.Format(time.RFC3339), "publishedAt": release.Published, "releaseURL": "https://github.com/MetaCubeX/mihomo/releases/tag/" + release.Tag, }) }