feat(Go_Updater): 添加全新 Go 语言实现的自动更新器

- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等
- 实现了与 MirrorChyan API 交互的客户端逻辑
- 添加了版本检查、更新检测和下载 URL 生成等功能
- 嵌入了配置模板和资源文件系统
- 提供了完整的构建和发布流程
This commit is contained in:
2025-07-20 16:30:14 +08:00
parent 97c283797a
commit 228e66315c
32 changed files with 8226 additions and 0 deletions

332
Go_Updater/api/client.go Normal file
View File

@@ -0,0 +1,332 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// MirrorResponse represents the response from MirrorChyan API
type MirrorResponse struct {
Code int `json:"code"`
Msg string `json:"msg"`
Data struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"` // Only present when using CDK
SHA256 string `json:"sha256,omitempty"` // Only present when using CDK
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"` // Only present when using CDK
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"` // Only present when using CDK
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"` // Only present when using CDK
} `json:"data"`
}
// UpdateCheckParams represents parameters for update checking
type UpdateCheckParams struct {
ResourceID string
CurrentVersion string
Channel string
CDK string
UserAgent string
}
// MirrorClient interface defines the methods for Mirror API client
type MirrorClient interface {
CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error)
CheckUpdateLegacy(resourceID, currentVersion, cdk, userAgent string) (*MirrorResponse, error)
IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool
GetOfficialDownloadURL(versionName string) string
}
// Client implements MirrorClient interface
type Client struct {
httpClient *http.Client
baseURL string
}
// NewClient creates a new Mirror API client
func NewClient() *Client {
return &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: "https://mirrorchyan.com/api/resources",
}
}
// CheckUpdate calls MirrorChyan API to check for updates with new parameter structure
func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) {
// Construct the API URL
apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, params.ResourceID)
// Parse URL to add query parameters
u, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("failed to parse API URL: %w", err)
}
// Add query parameters
q := u.Query()
q.Set("current_version", params.CurrentVersion)
q.Set("channel", params.Channel)
q.Set("os", "") // Empty for cross-platform
q.Set("arch", "") // Empty for cross-platform
if params.CDK != "" {
q.Set("cdk", params.CDK)
}
u.RawQuery = q.Encode()
// Create HTTP request
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set User-Agent header
if params.UserAgent != "" {
req.Header.Set("User-Agent", params.UserAgent)
} else {
req.Header.Set("User-Agent", "LightweightUpdater/1.0")
}
// Make HTTP request
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %w", err)
}
defer resp.Body.Close()
// Check HTTP status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned non-200 status code: %d", resp.StatusCode)
}
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Parse JSON response
var mirrorResp MirrorResponse
if err := json.Unmarshal(body, &mirrorResp); err != nil {
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
}
return &mirrorResp, nil
}
// CheckUpdateLegacy calls Mirror API to check for updates (legacy method for backward compatibility)
func (c *Client) CheckUpdateLegacy(resourceID, currentVersion, cdk, userAgent string) (*MirrorResponse, error) {
// Construct the API URL
apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, resourceID)
// Parse URL to add query parameters
u, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("failed to parse API URL: %w", err)
}
// Add query parameters
q := u.Query()
q.Set("current_version", currentVersion)
if cdk != "" {
q.Set("cdk", cdk)
}
u.RawQuery = q.Encode()
// Create HTTP request
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("failed to create HTTP request: %w", err)
}
// Set User-Agent header
if userAgent != "" {
req.Header.Set("User-Agent", userAgent)
} else {
req.Header.Set("User-Agent", "LightweightUpdater/1.0")
}
// Make HTTP request
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("failed to make HTTP request: %w", err)
}
defer resp.Body.Close()
// Check HTTP status code
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned non-200 status code: %d", resp.StatusCode)
}
// Read response body
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
// Parse JSON response
var mirrorResp MirrorResponse
if err := json.Unmarshal(body, &mirrorResp); err != nil {
return nil, fmt.Errorf("failed to parse JSON response: %w", err)
}
return &mirrorResp, nil
}
// IsUpdateAvailable compares current version with the latest version from API response
func (c *Client) IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool {
// Check if API response is successful
if response.Code != 0 {
return false
}
// Get latest version from response
latestVersion := response.Data.VersionName
if latestVersion == "" {
return false
}
// Convert version formats for comparison
currentVersionNormalized := c.normalizeVersionForComparison(currentVersion)
latestVersionNormalized := c.normalizeVersionForComparison(latestVersion)
// Compare versions using semantic version comparison
return compareVersions(currentVersionNormalized, latestVersionNormalized) < 0
}
// normalizeVersionForComparison converts different version formats to comparable format
func (c *Client) normalizeVersionForComparison(version string) string {
// Handle AUTO_MAA version format: "4.4.1.3" -> "v4.4.1-beta3"
if !strings.HasPrefix(version, "v") && strings.Count(version, ".") == 3 {
parts := strings.Split(version, ".")
if len(parts) == 4 {
major, minor, patch, beta := parts[0], parts[1], parts[2], parts[3]
if beta == "0" {
return fmt.Sprintf("v%s.%s.%s", major, minor, patch)
} else {
return fmt.Sprintf("v%s.%s.%s-beta%s", major, minor, patch, beta)
}
}
}
// Return as-is if already in standard format
return version
}
// compareVersions compares two semantic version strings
// Returns: -1 if v1 < v2, 0 if v1 == v2, 1 if v1 > v2
func compareVersions(v1, v2 string) int {
// Normalize versions by removing 'v' prefix if present
v1 = normalizeVersion(v1)
v2 = normalizeVersion(v2)
// Parse version components
parts1 := parseVersionParts(v1)
parts2 := parseVersionParts(v2)
// Compare each component
maxLen := len(parts1)
if len(parts2) > maxLen {
maxLen = len(parts2)
}
for i := 0; i < maxLen; i++ {
var p1, p2 int
if i < len(parts1) {
p1 = parts1[i]
}
if i < len(parts2) {
p2 = parts2[i]
}
if p1 < p2 {
return -1
} else if p1 > p2 {
return 1
}
}
return 0
}
// normalizeVersion removes 'v' prefix and handles common version formats
func normalizeVersion(version string) string {
if len(version) > 0 && (version[0] == 'v' || version[0] == 'V') {
return version[1:]
}
return version
}
// parseVersionParts parses version string into numeric components
func parseVersionParts(version string) []int {
if version == "" {
return []int{0}
}
parts := make([]int, 0, 3)
current := 0
for _, char := range version {
if char >= '0' && char <= '9' {
current = current*10 + int(char-'0')
} else if char == '.' {
parts = append(parts, current)
current = 0
} else {
// Stop parsing at non-numeric, non-dot characters (like pre-release identifiers)
break
}
}
// Add the last component
parts = append(parts, current)
// Ensure at least 3 components (major.minor.patch)
for len(parts) < 3 {
parts = append(parts, 0)
}
return parts
}
// GetOfficialDownloadURL generates the official download URL based on version name
func (c *Client) GetOfficialDownloadURL(versionName string) string {
// Official download site base URL
baseURL := "http://221.236.27.82:10197/d/AUTO_MAA"
// Convert version name to filename format
// e.g., "v4.4.0" -> "AUTO_MAA_v4.4.0.zip"
// e.g., "v4.4.1-beta3" -> "AUTO_MAA_v4.4.1-beta.3.zip"
filename := fmt.Sprintf("AUTO_MAA_%s.zip", versionName)
// Handle beta versions: convert "beta3" to "beta.3"
if strings.Contains(filename, "-beta") && !strings.Contains(filename, "-beta.") {
filename = strings.Replace(filename, "-beta", "-beta.", 1)
}
return fmt.Sprintf("%s/%s", baseURL, filename)
}
// HasCDKDownloadURL checks if the response contains a CDK download URL
func (c *Client) HasCDKDownloadURL(response *MirrorResponse) bool {
return response != nil && response.Data.URL != ""
}
// GetDownloadURL returns the appropriate download URL based on available options
func (c *Client) GetDownloadURL(response *MirrorResponse) string {
if c.HasCDKDownloadURL(response) {
return response.Data.URL
}
return c.GetOfficialDownloadURL(response.Data.VersionName)
}

View File

@@ -0,0 +1,423 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewClient(t *testing.T) {
client := NewClient()
if client == nil {
t.Fatal("NewClient() returned nil")
}
if client.httpClient == nil {
t.Fatal("HTTP client is nil")
}
if client.baseURL != "https://mirrorchyan.com/api/resources" {
t.Errorf("Expected base URL 'https://mirrorchyan.com/api/resources', got '%s'", client.baseURL)
}
}
func TestGetOfficialDownloadURL(t *testing.T) {
client := NewClient()
tests := []struct {
versionName string
expected string
}{
{"v4.4.0", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.0.zip"},
{"v4.4.1-beta3", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.1-beta.3.zip"},
{"v1.2.3", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v1.2.3.zip"},
{"v1.2.3-beta1", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v1.2.3-beta.1.zip"},
}
for _, test := range tests {
result := client.GetOfficialDownloadURL(test.versionName)
if result != test.expected {
t.Errorf("For version %s, expected %s, got %s", test.versionName, test.expected, result)
}
}
}
func TestNormalizeVersionForComparison(t *testing.T) {
client := NewClient()
tests := []struct {
input string
expected string
}{
{"4.4.0.0", "v4.4.0"},
{"4.4.1.3", "v4.4.1-beta3"},
{"v4.4.0", "v4.4.0"},
{"v4.4.1-beta3", "v4.4.1-beta3"},
{"1.2.3", "1.2.3"}, // Not 4-part version, return as-is
}
for _, test := range tests {
result := client.normalizeVersionForComparison(test.input)
if result != test.expected {
t.Errorf("For input %s, expected %s, got %s", test.input, test.expected, result)
}
}
}
func TestCheckUpdate(t *testing.T) {
// Create test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify request parameters
if r.URL.Query().Get("current_version") != "4.4.0.0" {
t.Errorf("Expected current_version=4.4.0.0, got %s", r.URL.Query().Get("current_version"))
}
if r.URL.Query().Get("channel") != "stable" {
t.Errorf("Expected channel=stable, got %s", r.URL.Query().Get("channel"))
}
// Return mock response
response := MirrorResponse{
Code: 0,
Msg: "success",
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{
VersionName: "v4.4.1",
VersionNumber: 48,
Channel: "stable",
OS: "",
Arch: "",
ReleaseNote: "Test release notes",
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Create client with test server URL
client := &Client{
httpClient: &http.Client{},
baseURL: server.URL,
}
// Test update check
params := UpdateCheckParams{
ResourceID: "AUTO_MAA",
CurrentVersion: "4.4.0.0",
Channel: "stable",
CDK: "",
UserAgent: "TestAgent/1.0",
}
response, err := client.CheckUpdate(params)
if err != nil {
t.Fatalf("CheckUpdate failed: %v", err)
}
if response.Code != 0 {
t.Errorf("Expected code 0, got %d", response.Code)
}
if response.Data.VersionName != "v4.4.1" {
t.Errorf("Expected version v4.4.1, got %s", response.Data.VersionName)
}
if response.Data.Channel != "stable" {
t.Errorf("Expected channel stable, got %s", response.Data.Channel)
}
}
func TestCheckUpdateWithCDK(t *testing.T) {
// Create test server
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Verify CDK parameter
if r.URL.Query().Get("cdk") != "test_cdk_123" {
t.Errorf("Expected cdk=test_cdk_123, got %s", r.URL.Query().Get("cdk"))
}
// Return mock response with CDK download URL
response := MirrorResponse{
Code: 0,
Msg: "success",
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{
VersionName: "v4.4.1",
VersionNumber: 48,
URL: "https://mirrorchyan.com/api/resources/download/test123",
SHA256: "abcd1234",
Channel: "stable",
OS: "",
Arch: "",
UpdateType: "full",
ReleaseNote: "Test release notes",
FileSize: 12345678,
CDKExpiredTime: 1776013593,
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}))
defer server.Close()
// Create client with test server URL
client := &Client{
httpClient: &http.Client{},
baseURL: server.URL,
}
// Test update check with CDK
params := UpdateCheckParams{
ResourceID: "AUTO_MAA",
CurrentVersion: "4.4.0.0",
Channel: "stable",
CDK: "test_cdk_123",
UserAgent: "TestAgent/1.0",
}
response, err := client.CheckUpdate(params)
if err != nil {
t.Fatalf("CheckUpdate with CDK failed: %v", err)
}
if response.Data.URL == "" {
t.Error("Expected CDK download URL, but got empty")
}
if response.Data.SHA256 == "" {
t.Error("Expected SHA256 hash, but got empty")
}
if response.Data.FileSize == 0 {
t.Error("Expected file size, but got 0")
}
}
func TestIsUpdateAvailable(t *testing.T) {
client := NewClient()
tests := []struct {
name string
response *MirrorResponse
currentVersion string
expected bool
}{
{
name: "Update available - stable",
response: &MirrorResponse{
Code: 0,
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{VersionName: "v4.4.1"},
},
currentVersion: "4.4.0.0",
expected: true,
},
{
name: "No update available - same version",
response: &MirrorResponse{
Code: 0,
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{VersionName: "v4.4.0"},
},
currentVersion: "4.4.0.0",
expected: false,
},
{
name: "API error",
response: &MirrorResponse{
Code: 1,
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{VersionName: "v4.4.1"},
},
currentVersion: "4.4.0.0",
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := client.IsUpdateAvailable(test.response, test.currentVersion)
if result != test.expected {
t.Errorf("Expected %t, got %t", test.expected, result)
}
})
}
}
func TestHasCDKDownloadURL(t *testing.T) {
client := NewClient()
tests := []struct {
name string
response *MirrorResponse
expected bool
}{
{
name: "Has CDK URL",
response: &MirrorResponse{
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{URL: "https://mirrorchyan.com/download/test"},
},
expected: true,
},
{
name: "No CDK URL",
response: &MirrorResponse{
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{URL: ""},
},
expected: false,
},
{
name: "Nil response",
response: nil,
expected: false,
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := client.HasCDKDownloadURL(test.response)
if result != test.expected {
t.Errorf("Expected %t, got %t", test.expected, result)
}
})
}
}
func TestGetDownloadURL(t *testing.T) {
client := NewClient()
tests := []struct {
name string
response *MirrorResponse
expected string
}{
{
name: "CDK URL available",
response: &MirrorResponse{
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{
VersionName: "v4.4.1",
URL: "https://mirrorchyan.com/download/test",
},
},
expected: "https://mirrorchyan.com/download/test",
},
{
name: "Official URL fallback",
response: &MirrorResponse{
Data: struct {
VersionName string `json:"version_name"`
VersionNumber int `json:"version_number"`
URL string `json:"url,omitempty"`
SHA256 string `json:"sha256,omitempty"`
Channel string `json:"channel"`
OS string `json:"os"`
Arch string `json:"arch"`
UpdateType string `json:"update_type,omitempty"`
ReleaseNote string `json:"release_note"`
FileSize int64 `json:"filesize,omitempty"`
CDKExpiredTime int64 `json:"cdk_expired_time,omitempty"`
}{
VersionName: "v4.4.1",
URL: "",
},
},
expected: "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.1.zip",
},
}
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
result := client.GetDownloadURL(test.response)
if result != test.expected {
t.Errorf("Expected %s, got %s", test.expected, result)
}
})
}
}