Compare commits

...

40 Commits

Author SHA1 Message Date
DLmaster361
6b37ba0ce3 fix(maa): 修复 MAA 开始唤醒识别错误问题 2025-08-12 16:26:16 +08:00
23b3691a13 feat(version): 优化版本比较和显示逻辑
- 修改版本比较逻辑,优先比较主版本号,然后是次版本号和修订号
- 增加对 beta版本的特殊处理,正式版比 beta 版更新
- 优化版本显示格式,移除不必要的 "v" 前缀
- 在主界面和日志中使用优化后的版本显示格式
2025-08-04 00:12:36 +08:00
DLmaster361
8948d0fb18 feat: 发布v4.4.1 2025-08-01 11:02:08 +08:00
DLmaster361
908da0bc47 fix: 修复计划表未能按照鹰历获取关卡号的问题 2025-08-01 09:35:54 +08:00
DLmaster361
aae17208b0 fix: 修正部分文案 2025-08-01 01:09:10 +08:00
DLmaster361
cbd8918c61 feat(core): 启动时支持直接运行复数调度队列 2025-07-31 23:18:36 +08:00
DLmaster361
f2b4f9e8fc ci: fix ci 2025-07-31 22:07:21 +08:00
DLmaster361
a2a11647bb fix: ci 2025-07-31 21:58:53 +08:00
DLmaster361
1d9c275b61 ci: fix 2025-07-31 21:53:44 +08:00
DLmaster361
52468928c6 feat: 新增 Go_Updater 独立更新器 2025-07-31 20:47:49 +08:00
DLmaster361
3d9471779a Merge branch 'ClozyA_GoUpdater_dev' into dev 2025-07-31 20:42:52 +08:00
DLmaster361
155c4b00d5 ci: 适配Go_Updater 2025-07-31 20:42:36 +08:00
DLmaster361
f4b45e9eae feat(general): 通用脚本启动附加命令添加额外的语法以适应UI可执行文件与任务可执行文件不同的情况 2025-07-31 20:01:32 +08:00
DLmaster361
d836b58b2d feat: 新增完成任务后自动复原脚本配置 2025-07-31 17:06:08 +08:00
DLmaster361
4fc747f1c6 feat(maa): 适配 MAA 长期开放剿灭关卡 #58 2025-07-31 16:13:35 +08:00
DLmaster361
b27f2c43ae feat: MAA代理时更新改为强制开启 2025-07-31 13:50:19 +08:00
DLmaster361
c5f947e14a fix: 优化静默进程标记逻辑 2025-07-31 13:39:38 +08:00
DLmaster361
67c41ab3ee chore(core): 优化调度队列配置逻辑 2025-07-31 01:01:30 +08:00
DLmaster
9f330e2245 Merge pull request #62 from Zrief:dev
修复依赖缺失
2025-07-31 00:49:39 +08:00
Zrief
d785262312 依赖相关问题
在 app/core/task_manager.py 文件中,第 31 行使用了 packaging 模块。其并非标准库,因此需要确保它被包含在项目依赖中。
2025-07-31 00:44:07 +08:00
DLmaster361
27faaabcf2 fix: 修复脚本下载器卡初始化 2025-07-27 23:30:34 +08:00
226d68cb1c feat(api): 修复beta版本的更新检查 2025-07-27 14:12:54 +08:00
DLmaster361
e776cc2319 fix: 修复一般更新方案卡初始化 2025-07-26 16:23:22 +08:00
DLmaster361
3cdb1e511d fix: 修正部分格式 2025-07-26 13:11:23 +08:00
a4f867665f feat(power): 添加强制关机功能并优化关机流程 2025-07-26 00:27:41 +08:00
DLmaster361
6b0583b139 fix: 移除部分调试代码 2025-07-25 20:42:50 +08:00
2aad0c65c2 chore: 添加无更新可用时的延迟退出功能 2025-07-22 23:03:23 +08:00
6b646378b6 refactor(updater): 重构 Go 版本更新器
- 更新项目名称为 AUTO_MAA_Go_Updater
- 重构代码结构,优化函数命名和逻辑
- 移除 CDK 相关的冗余代码
- 调整版本号为 git commit hash
- 更新构建配置和脚本
- 优化 API 客户端实现
2025-07-22 21:51:58 +08:00
747ad6387b feat(config): 添加配置管理功能 2025-07-20 18:12:35 +08:00
228e66315c feat(Go_Updater): 添加全新 Go 语言实现的自动更新器
- 新增多个源文件和目录,包括 app.rc、assets、build 脚本等
- 实现了与 MirrorChyan API 交互的客户端逻辑
- 添加了版本检查、更新检测和下载 URL 生成等功能
- 嵌入了配置模板和资源文件系统
- 提供了完整的构建和发布流程
2025-07-20 16:30:14 +08:00
DLmaster361
97c283797a fix: 移除崩溃弹窗机制 2025-07-19 22:23:59 +08:00
DLmaster361
eb1fade6f5 feat: AUTO_MAA 配置分享中心上线 2025-07-19 21:38:38 +08:00
DLmaster361
8d6071f794 fix: 修复 QTimer.singleShot 参数问题 2025-07-19 17:08:57 +08:00
DLmaster361
8a109e34f8 refactor(module): 重构日志读取兜底机制 2025-07-19 10:27:19 +08:00
DLmaster361
fd72d72692 fix(module): 日志读取添加兜底机制 2025-07-19 10:13:48 +08:00
DLmaster361
63ffacff96 fix(ui): SpinBox和TimeEdit组件忽视滚轮事件 2025-07-18 21:59:18 +08:00
DLmaster361
1b4bb6fccc feat(general): 通用脚本支持在选定的时机自动更新配置文件 2025-07-18 21:48:39 +08:00
DLmaster361
9b492b5e0d feat(core): 重构日志记录,载入更多日志记录项 2025-07-18 18:12:47 +08:00
DLmaster361
8427bd9f6b Merge branch 'inno_dev' into dev 2025-07-16 20:13:45 +08:00
DLmaster361
b9281b68ab feat(utils): 安装包适配管理员权限 2025-07-10 04:53:32 +08:00
65 changed files with 11638 additions and 1786 deletions

View File

@@ -51,6 +51,18 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.24'
- name: Build go updater
shell: pwsh
run: |
cd Go_Updater
go install github.com/akavel/rsrc@latest
.\build.ps1
- name: Set up Python
uses: actions/setup-python@v5
with:
@@ -59,16 +71,8 @@ jobs:
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
pip install -r requirements.txt
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Get version
id: get_version
run: |
@@ -122,6 +126,8 @@ jobs:
$ver = "${{ steps.get_version.outputs.main_version }}"
Copy-Item "$root/app" "$root/AUTO_MAA/app" -Recurse
Copy-Item "$root/resources" "$root/AUTO_MAA/resources" -Recurse
Copy-Item "$root/Go_Updater" "$root/AUTO_MAA/Go_Updater" -Recurse
Move-Item "$root/AUTO_MAA/Go_Updater/build/AUTO_MAA_Go_Updater.exe" "$root/AUTO_MAA/AUTO_MAA_Go_Updater_install.exe"
Copy-Item "$root/main.py" "$root/AUTO_MAA/"
Copy-Item "$root/requirements.txt" "$root/AUTO_MAA/"
Copy-Item "$root/README.md" "$root/AUTO_MAA/"

116
Go_Updater/Makefile Normal file
View File

@@ -0,0 +1,116 @@
# AUTO_MAA_Go_Updater Makefile
# Build variables
VERSION ?= 1.0.0
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
OUTPUT_NAME := AUTO_MAA_Go_Updater
BUILD_DIR := build
DIST_DIR := dist
# Go build flags
LDFLAGS := -s -w -X AUTO_MAA_Go_Updater/version.Version=$(VERSION) -X AUTO_MAA_Go_Updater/version.BuildTime=$(BUILD_TIME) -X AUTO_MAA_Go_Updater/version.GitCommit=$(GIT_COMMIT)
# Default target
.PHONY: all
all: clean build
# Clean build artifacts
.PHONY: clean
clean:
@echo "Cleaning build artifacts..."
@rm -rf $(BUILD_DIR) $(DIST_DIR)
@mkdir -p $(BUILD_DIR) $(DIST_DIR)
# Build for Windows 64-bit
.PHONY: build
build: clean
@echo "========================================="
@echo "Building AUTO_MAA_Go_Updater"
@echo "========================================="
@echo "Version: $(VERSION)"
@echo "Build Time: $(BUILD_TIME)"
@echo "Git Commit: $(GIT_COMMIT)"
@echo "Target: Windows 64-bit"
@echo ""
@echo "Building application..."
@GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(OUTPUT_NAME).exe .
@echo "Build completed successfully!"
@echo ""
@echo "Build Results:"
@ls -lh $(BUILD_DIR)/$(OUTPUT_NAME).exe
@cp $(BUILD_DIR)/$(OUTPUT_NAME).exe $(DIST_DIR)/$(OUTPUT_NAME).exe
@echo "Copied to: $(DIST_DIR)/$(OUTPUT_NAME).exe"
# Build with UPX compression
.PHONY: build-compressed
build-compressed: build
@echo ""
@echo "Compressing with UPX..."
@if command -v upx >/dev/null 2>&1; then \
upx --best $(BUILD_DIR)/$(OUTPUT_NAME).exe; \
echo "Compression completed!"; \
ls -lh $(BUILD_DIR)/$(OUTPUT_NAME).exe; \
cp $(BUILD_DIR)/$(OUTPUT_NAME).exe $(DIST_DIR)/$(OUTPUT_NAME).exe; \
else \
echo "UPX not found. Skipping compression."; \
fi
# Run tests
.PHONY: test
test:
@echo "Running tests..."
@go test -v ./...
# Run with version flag
.PHONY: version
version: build
@echo ""
@echo "Testing version information:"
@$(BUILD_DIR)/$(OUTPUT_NAME).exe -version
# Install dependencies
.PHONY: deps
deps:
@echo "Installing dependencies..."
@go mod tidy
@go mod download
# Format code
.PHONY: fmt
fmt:
@echo "Formatting code..."
@go fmt ./...
# Lint code
.PHONY: lint
lint:
@echo "Linting code..."
@if command -v golangci-lint >/dev/null 2>&1; then \
golangci-lint run; \
else \
echo "golangci-lint not found. Install it with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
fi
# Development build (faster, no optimizations)
.PHONY: dev
dev:
@echo "Building development version..."
@go build -o $(BUILD_DIR)/$(OUTPUT_NAME)-dev.exe .
@echo "Development build completed: $(BUILD_DIR)/$(OUTPUT_NAME)-dev.exe"
# Help
.PHONY: help
help:
@echo "Available targets:"
@echo " all - Clean and build (default)"
@echo " build - Build for Windows 64-bit"
@echo " build-compressed - Build and compress with UPX"
@echo " clean - Clean build artifacts"
@echo " test - Run tests"
@echo " version - Build and show version"
@echo " deps - Install dependencies"
@echo " fmt - Format code"
@echo " lint - Lint code"
@echo " dev - Development build"
@echo " help - Show this help"

15
Go_Updater/README.MD Normal file
View File

@@ -0,0 +1,15 @@
# 用Go语言实现的一个AUTO_MAA下载器
用于直接下载AUTO_MAA软件本体在Python版本出现问题时使用。
## 使用方法
1. 下载并安装Go语言环境需要配置环境变量
2. 运行 `go mod tidy` 命令,安装依赖包。
3. 运行 `go run main.go` 命令程序会自动下载并安装AUTO_MAA软件。
## 构建
运行 `.\build.ps1` 脚本即可完成构建。
参数说明:
-Version指定要构建的版本号
运行命令: `.\build.ps1 -Version "1.0.8"`

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

@@ -0,0 +1,312 @@
package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// MirrorResponse 表示 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"`
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"`
} `json:"data"`
}
// UpdateCheckParams 表示更新检查的参数
type UpdateCheckParams struct {
ResourceID string
CurrentVersion string
Channel string
UserAgent string
}
// MirrorClient 定义 Mirror API 客户端的接口方法
type MirrorClient interface {
CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error)
IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool
GetDownloadURL(versionName string) string
}
// Client 实现 MirrorClient 接口
type Client struct {
httpClient *http.Client
baseURL string
downloadURL string
}
// NewClient 创建新的 Mirror API 客户端
func NewClient() *Client {
return &Client{
httpClient: &http.Client{
Timeout: 30 * time.Second,
},
baseURL: "https://mirrorchyan.com/api/resources",
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
}
}
// CheckUpdate 调用 MirrorChyan API 检查更新
func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) {
// 构建 API URL
apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, params.ResourceID)
// 解析 URL 并添加查询参数
u, err := url.Parse(apiURL)
if err != nil {
return nil, fmt.Errorf("解析 API URL 失败: %w", err)
}
// 添加查询参数
q := u.Query()
q.Set("current_version", params.CurrentVersion)
q.Set("channel", params.Channel)
q.Set("os", "") // 跨平台为空
q.Set("arch", "") // 跨平台为空
u.RawQuery = q.Encode()
// 创建 HTTP 请求
req, err := http.NewRequest("GET", u.String(), nil)
if err != nil {
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
}
// 设置 User-Agent 头
if params.UserAgent != "" {
req.Header.Set("User-Agent", params.UserAgent)
} else {
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
}
// 发送 HTTP 请求
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
}
defer resp.Body.Close()
// 检查 HTTP 状态码
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API 返回非 200 状态码: %d", resp.StatusCode)
}
// 读取响应体
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应体失败: %w", err)
}
// 解析 JSON 响应
var mirrorResp MirrorResponse
if err := json.Unmarshal(body, &mirrorResp); err != nil {
return nil, fmt.Errorf("解析 JSON 响应失败: %w", err)
}
return &mirrorResp, nil
}
// IsUpdateAvailable 比较当前版本与 API 响应中的最新版本
func (c *Client) IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool {
// 检查 API 响应是否成功
if response.Code != 0 {
return false
}
// 从响应中获取最新版本
latestVersion := response.Data.VersionName
if latestVersion == "" {
return false
}
// 转换版本格式以便比较
currentVersionNormalized := c.normalizeVersionForComparison(currentVersion)
latestVersionNormalized := c.normalizeVersionForComparison(latestVersion)
// 调试输出
// fmt.Printf("Current: %s -> %s\n", currentVersion, currentVersionNormalized)
// fmt.Printf("Latest: %s -> %s\n", latestVersion, latestVersionNormalized)
// fmt.Printf("Compare result: %d\n", compareVersions(currentVersionNormalized, latestVersionNormalized))
// 使用语义版本比较
return compareVersions(currentVersionNormalized, latestVersionNormalized) < 0
}
// normalizeVersionForComparison 将不同版本格式转换为可比较格式
func (c *Client) normalizeVersionForComparison(version string) string {
// 处理 AUTO_MAA 版本格式: "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 version
}
// compareVersions 比较两个语义版本字符串
// 返回值: -1 如果 v1 < v2, 0 如果 v1 == v2, 1 如果 v1 > v2
func compareVersions(v1, v2 string) int {
// 通过移除 'v' 前缀来标准化版本
v1 = normalizeVersion(v1)
v2 = normalizeVersion(v2)
// 解析版本组件
parts1 := parseVersionParts(v1)
parts2 := parseVersionParts(v2)
// 比较前三个组件 (major.minor.patch)
for i := 0; i < 3; 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
}
}
// 如果前三个组件相同比较beta版本号
var beta1, beta2 int
if len(parts1) > 3 {
beta1 = parts1[3]
}
if len(parts2) > 3 {
beta2 = parts2[3]
}
// 特殊处理beta版本比较
// - 如果一个是正式版(beta=0)另一个是beta版(beta>0),正式版更新
// - 如果都是beta版比较beta版本号
if beta1 == 0 && beta2 > 0 {
return 1 // 正式版比beta版更新
}
if beta1 > 0 && beta2 == 0 {
return -1 // beta版比正式版旧
}
// 都是正式版或都是beta版直接比较
if beta1 < beta2 {
return -1
} else if beta1 > beta2 {
return 1
}
return 0
}
// normalizeVersion 移除 'v' 前缀并处理常见版本格式
func normalizeVersion(version string) string {
if len(version) > 0 && (version[0] == 'v' || version[0] == 'V') {
return version[1:]
}
return version
}
// parseVersionParts 将版本字符串解析为数字组件包括beta版本号
func parseVersionParts(version string) []int {
if version == "" {
return []int{0}
}
parts := make([]int, 0, 4)
current := 0
// 先检查是否包含 -beta
betaIndex := strings.Index(version, "-beta")
var mainVersion, betaVersion string
if betaIndex != -1 {
mainVersion = version[:betaIndex]
betaVersion = version[betaIndex+5:] // 跳过 "-beta"
} else {
mainVersion = version
betaVersion = ""
}
// 解析主版本号 (major.minor.patch)
for _, char := range mainVersion {
if char >= '0' && char <= '9' {
current = current*10 + int(char-'0')
} else if char == '.' {
parts = append(parts, current)
current = 0
} else {
// 遇到非数字非点字符,停止解析
break
}
}
// 添加最后一个主版本组件
parts = append(parts, current)
// 确保至少有 3 个组件 (major.minor.patch)
for len(parts) < 3 {
parts = append(parts, 0)
}
// 解析beta版本号
if betaVersion != "" {
// 跳过可能的点号
if strings.HasPrefix(betaVersion, ".") {
betaVersion = betaVersion[1:]
}
betaNum := 0
for _, char := range betaVersion {
if char >= '0' && char <= '9' {
betaNum = betaNum*10 + int(char-'0')
} else {
break
}
}
// Beta版本号保持正数但在比较时会特殊处理
parts = append(parts, betaNum)
} else {
// 非beta版本添加0作为beta版本号
parts = append(parts, 0)
}
return parts
}
// GetDownloadURL 根据版本名生成下载站的下载 URL
func (c *Client) GetDownloadURL(versionName string) string {
// 将版本名转换为文件名格式
// 例如: "v4.4.0" -> "AUTO_MAA_v4.4.0.zip"
// 例如: "v4.4.1-beta3" -> "AUTO_MAA_v4.4.1-beta.3.zip"
filename := fmt.Sprintf("AUTO_MAA_%s.zip", versionName)
// 处理 beta 版本: 将 "beta3" 转换为 "beta.3"
if strings.Contains(filename, "-beta") && !strings.Contains(filename, "-beta.") {
filename = strings.Replace(filename, "-beta", "-beta.", 1)
}
return fmt.Sprintf("%s/%s", c.downloadURL, filename)
}

View File

@@ -0,0 +1,186 @@
package api
import (
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestNewClient(t *testing.T) {
client := NewClient()
if client == nil {
t.Fatal("NewClient() 返回 nil")
}
if client.httpClient == nil {
t.Fatal("HTTP 客户端为 nil")
}
if client.baseURL != "https://mirrorchyan.com/api/resources" {
t.Errorf("期望基础 URL 'https://mirrorchyan.com/api/resources',得到 '%s'", client.baseURL)
}
if client.downloadURL != "http://221.236.27.82:10197/d/AUTO_MAA" {
t.Errorf("期望下载 URL 'http://221.236.27.82:10197/d/AUTO_MAA',得到 '%s'", client.downloadURL)
}
}
func TestGetDownloadURL(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"},
}
for _, test := range tests {
result := client.GetDownloadURL(test.versionName)
if result != test.expected {
t.Errorf("版本 %s期望 %s得到 %s", test.versionName, test.expected, result)
}
}
}
func TestCheckUpdate(t *testing.T) {
// 创建测试服务器
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
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"`
}{
VersionName: "v4.4.1",
VersionNumber: 48,
Channel: "stable",
ReleaseNote: "测试发布说明",
},
}
w.Header().Set("Content-Type", "application/json")
err := json.NewEncoder(w).Encode(response)
if err != nil {
return
}
}))
defer server.Close()
// 使用测试服务器 URL 创建客户端
client := &Client{
httpClient: &http.Client{},
baseURL: server.URL,
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
}
// 测试更新检查
params := UpdateCheckParams{
ResourceID: "AUTO_MAA",
CurrentVersion: "4.4.0.0",
Channel: "stable",
UserAgent: "TestAgent/1.0",
}
response, err := client.CheckUpdate(params)
if err != nil {
t.Fatalf("CheckUpdate 失败: %v", err)
}
if response.Code != 0 {
t.Errorf("期望代码 0得到 %d", response.Code)
}
if response.Data.VersionName != "v4.4.1" {
t.Errorf("期望版本 v4.4.1,得到 %s", response.Data.VersionName)
}
}
func TestIsUpdateAvailable(t *testing.T) {
client := NewClient()
tests := []struct {
name string
response *MirrorResponse
currentVersion string
expected bool
}{
{
name: "有可用更新",
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"`
}{VersionName: "v4.4.1"},
},
currentVersion: "4.4.0.0",
expected: true,
},
{
name: "无可用更新",
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"`
}{VersionName: "v4.4.0"},
},
currentVersion: "4.4.0.0",
expected: false,
},
{
name: "beta版本有更新",
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"`
}{VersionName: "v4.4.1-beta.4"},
},
currentVersion: "4.4.1.3",
expected: true,
},
}
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("期望 %t得到 %t", test.expected, result)
}
})
}
}

34
Go_Updater/app.rc Normal file
View File

@@ -0,0 +1,34 @@
#include <windows.h>
// Application icon
IDI_ICON1 ICON "icon/AUTO_MAA_Go_Updater.ico"
// Version information
VS_VERSION_INFO VERSIONINFO
FILEVERSION 1,0,0,0
PRODUCTVERSION 1,0,0,0
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
FILEFLAGS 0x0L
FILEOS VOS__WINDOWS32
FILETYPE VFT_APP
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904B0"
BEGIN
VALUE "CompanyName", "AUTO MAA Team"
VALUE "FileDescription", "AUTO MAA Go Updater"
VALUE "FileVersion", "1.0.0.0"
VALUE "InternalName", "AUTO_MAA_Go_Updater"
VALUE "LegalCopyright", "Copyright (C) 2025"
VALUE "OriginalFilename", "AUTO_MAA_Go_Updater.exe"
VALUE "ProductName", "AUTO MAA Go Updater"
VALUE "ProductVersion", "1.0.0.0"
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

BIN
Go_Updater/app.syso Normal file

Binary file not shown.

View File

@@ -0,0 +1,34 @@
package assets
import (
"embed"
"io/fs"
)
//go:embed config_template.yaml
var EmbeddedAssets embed.FS
// GetConfigTemplate 返回嵌入的配置模板
func GetConfigTemplate() ([]byte, error) {
return EmbeddedAssets.ReadFile("config_template.yaml")
}
// GetAssetFS 返回嵌入的文件系统
func GetAssetFS() fs.FS {
return EmbeddedAssets
}
// ListAssets 返回所有嵌入资源的列表
func ListAssets() ([]string, error) {
var assets []string
err := fs.WalkDir(EmbeddedAssets, ".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if !d.IsDir() {
assets = append(assets, path)
}
return nil
})
return assets, err
}

View File

@@ -0,0 +1,100 @@
package assets
import (
"testing"
)
func TestGetConfigTemplate(t *testing.T) {
data, err := GetConfigTemplate()
if err != nil {
t.Fatalf("Failed to get config template: %v", err)
}
if len(data) == 0 {
t.Fatal("Config template is empty")
}
// Check that it contains expected content
content := string(data)
if !contains(content, "resource_id") {
t.Error("Config template should contain 'resource_id'")
}
if !contains(content, "current_version") {
t.Error("Config template should contain 'current_version'")
}
if !contains(content, "user_agent") {
t.Error("Config template should contain 'user_agent'")
}
}
func TestListAssets(t *testing.T) {
assets, err := ListAssets()
if err != nil {
t.Fatalf("Failed to list assets: %v", err)
}
if len(assets) == 0 {
t.Fatal("No assets found")
}
// Check that config template is in the list
found := false
for _, asset := range assets {
if asset == "config_template.yaml" {
found = true
break
}
}
if !found {
t.Error("config_template.yaml should be in the assets list")
}
}
func TestGetAssetFS(t *testing.T) {
fs := GetAssetFS()
if fs == nil {
t.Fatal("Asset filesystem should not be nil")
}
// Try to open the config template
file, err := fs.Open("config_template.yaml")
if err != nil {
t.Fatalf("Failed to open config template from filesystem: %v", err)
}
defer file.Close()
// Check that we can read from it
buffer := make([]byte, 100)
n, err := file.Read(buffer)
if err != nil && err.Error() != "EOF" {
t.Fatalf("Failed to read from config template: %v", err)
}
if n == 0 {
t.Fatal("Config template appears to be empty")
}
}
// Helper function to check if string contains substring
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
s[len(s)-len(substr):] == substr ||
containsAt(s, substr, 1))))
}
func containsAt(s, substr string, start int) bool {
if start >= len(s) {
return false
}
if start+len(substr) > len(s) {
return containsAt(s, substr, start+1)
}
if s[start:start+len(substr)] == substr {
return true
}
return containsAt(s, substr, start+1)
}

View File

@@ -0,0 +1,7 @@
resource_id: "AUTO_MAA"
current_version: "v1.0.0"
user_agent: "AUTO_MAA_Go_Updater/1.0"
backup_url: "https://backup-download-site.com/releases"
log_level: "info"
auto_check: true
check_interval: 3600 # seconds

View File

@@ -0,0 +1,55 @@
# Build Configuration for AUTO_MAA_Go_Updater
project:
name: "AUTO_MAA_Go_Updater"
module: "AUTO_MAA_Go_Updater"
description: "AUTO_MAA_Go版本更新器"
version:
default: "1.0.0"
build_time_format: "2006-01-02T15:04:05Z"
targets:
- name: "windows-amd64"
goos: "windows"
goarch: "amd64"
cgo_enabled: true
output: "AUTO_MAA_Go_Updater.exe"
build:
flags:
ldflags: "-s -w"
tags: []
optimization:
strip_debug: true
strip_symbols: true
upx_compression: false # Optional, requires UPX
size_requirements:
max_size_mb: 10
warn_size_mb: 8
assets:
embed:
- "assets/config_template.yaml"
directories:
build: "build"
dist: "dist"
temp: "temp"
version_injection:
package: "AUTO_MAA_Go_Updater/version"
variables:
- name: "Version"
source: "version"
- name: "BuildTime"
source: "build_time"
- name: "GitCommit"
source: "git_commit"
quality:
run_tests: true
run_lint: false # Optional
format_code: true

93
Go_Updater/build.bat Normal file
View File

@@ -0,0 +1,93 @@
@echo off
setlocal enabledelayedexpansion
echo ========================================
echo AUTO_MAA_Go_Updater Build Script
echo ========================================
:: Set build variables
set OUTPUT_NAME=AUTO_MAA_Go_Updater.exe
set BUILD_DIR=build
set DIST_DIR=dist
:: Get current datetime for build time
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%"
set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%"
set "BUILD_TIME=%YYYY%-%MM%-%DD%T%HH%:%Min%:%Sec%Z"
:: Get git commit hash (if available)
git rev-parse --short HEAD > temp_commit.txt 2>nul
if exist temp_commit.txt (
set /p GIT_COMMIT=<temp_commit.txt
del temp_commit.txt
) else (
set GIT_COMMIT=unknown
)
:: Use commit hash as version
set VERSION=%GIT_COMMIT%
echo Build Information:
echo - Version: %VERSION%
echo - Build Time: %BUILD_TIME%
echo - Git Commit: %GIT_COMMIT%
echo - Target: Windows 64-bit
echo.
:: Create build directories
if not exist %BUILD_DIR% mkdir %BUILD_DIR%
if not exist %DIST_DIR% mkdir %DIST_DIR%
:: Set build flags
set LDFLAGS=-s -w -X AUTO_MAA_Go_Updater/version.Version=%VERSION% -X AUTO_MAA_Go_Updater/version.BuildTime=%BUILD_TIME% -X AUTO_MAA_Go_Updater/version.GitCommit=%GIT_COMMIT%
echo Building application...
:: Ensure icon resource is compiled
if not exist app.syso (
echo Compiling icon resource...
where rsrc >nul 2>&1
if !ERRORLEVEL! equ 0 (
rsrc -ico icon\AUTO_MAA_Go_Updater.ico -o app.syso
if !ERRORLEVEL! equ 0 (
echo Icon resource compiled successfully
) else (
echo Warning: Failed to compile icon resource
)
) else (
echo Warning: rsrc not found. Install with: go install github.com/akavel/rsrc@latest
)
)
:: Set environment variables for Go build
set GOOS=windows
set GOARCH=amd64
set CGO_ENABLED=1
:: Build the application
go build -ldflags="%LDFLAGS%" -o %BUILD_DIR%\%OUTPUT_NAME% .
if %ERRORLEVEL% neq 0 (
echo Build failed!
exit /b 1
)
echo Build completed successfully!
:: Get file size
for %%A in (%BUILD_DIR%\%OUTPUT_NAME%) do set FILE_SIZE=%%~zA
set /a FILE_SIZE_MB=%FILE_SIZE%/1024/1024
echo.
echo Build Results:
echo - Output: %BUILD_DIR%\%OUTPUT_NAME%
echo - Size: %FILE_SIZE% bytes (~%FILE_SIZE_MB% MB)
:: Copy to dist directory
copy %BUILD_DIR%\%OUTPUT_NAME% %DIST_DIR%\%OUTPUT_NAME% >nul
echo - Copied to: %DIST_DIR%\%OUTPUT_NAME%
echo.
echo Build script completed successfully!
echo ========================================

105
Go_Updater/build.ps1 Normal file
View File

@@ -0,0 +1,105 @@
# AUTO_MAA_Go_Updater Build Script (PowerShell)
param(
[string]$OutputName = "AUTO_MAA_Go_Updater.exe",
[switch]$Compress = $false
)
Write-Host "========================================" -ForegroundColor Cyan
Write-Host "AUTO_MAA_Go_Updater Build Script" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan
# Set build variables
$BuildDir = "build"
$DistDir = "dist"
$BuildTime = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
# Get git commit hash
try {
$GitCommit = (git rev-parse --short HEAD 2>$null).Trim()
if (-not $GitCommit) { $GitCommit = "unknown" }
} catch {
$GitCommit = "unknown"
}
Write-Host "Build Information:" -ForegroundColor Yellow
Write-Host "- Version: $GitCommit"
Write-Host "- Build Time: $BuildTime"
Write-Host "- Git Commit: $GitCommit"
Write-Host "- Target: Windows 64-bit"
Write-Host ""
# Create build directories
if (-not (Test-Path $BuildDir)) { New-Item -ItemType Directory -Path $BuildDir | Out-Null }
if (-not (Test-Path $DistDir)) { New-Item -ItemType Directory -Path $DistDir | Out-Null }
# Set environment variables
$env:GOOS = "windows"
$env:GOARCH = "amd64"
$env:CGO_ENABLED = "1"
# Set build flags
$LdFlags = "-s -w -X AUTO_MAA_Go_Updater/version.Version=$Version -X AUTO_MAA_Go_Updater/version.BuildTime=$BuildTime -X AUTO_MAA_Go_Updater/version.GitCommit=$GitCommit"
Write-Host "Building application..." -ForegroundColor Green
# Ensure icon resource is compiled
if (-not (Test-Path "app.syso")) {
Write-Host "Compiling icon resource..." -ForegroundColor Yellow
if (Get-Command rsrc -ErrorAction SilentlyContinue) {
rsrc -ico icon/AUTO_MAA_Go_Updater.ico -o app.syso
if ($LASTEXITCODE -ne 0) {
Write-Host "Warning: Failed to compile icon resource" -ForegroundColor Yellow
} else {
Write-Host "Icon resource compiled successfully" -ForegroundColor Green
}
} else {
Write-Host "Warning: rsrc not found. Install with: go install github.com/akavel/rsrc@latest" -ForegroundColor Yellow
}
}
# Build the application
$BuildCommand = "go build -ldflags=`"$LdFlags`" -o $BuildDir\$OutputName ."
Invoke-Expression $BuildCommand
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed!" -ForegroundColor Red
exit 1
}
Write-Host "Build completed successfully!" -ForegroundColor Green
# Get file information
$OutputFile = Get-Item "$BuildDir\$OutputName"
$FileSizeMB = [math]::Round($OutputFile.Length / 1MB, 2)
Write-Host ""
Write-Host "Build Results:" -ForegroundColor Yellow
Write-Host "- Output: $($OutputFile.FullName)"
Write-Host "- Size: $($OutputFile.Length) bytes (~$FileSizeMB MB)"
# Optional UPX compression
if ($Compress) {
Write-Host ""
Write-Host "Compressing with UPX..." -ForegroundColor Yellow
if (Get-Command upx -ErrorAction SilentlyContinue) {
upx --best "$BuildDir\$OutputName"
$CompressedFile = Get-Item "$BuildDir\$OutputName"
$CompressedSizeMB = [math]::Round($CompressedFile.Length / 1MB, 2)
Write-Host "- Compressed Size: $($CompressedFile.Length) bytes (~$CompressedSizeMB MB)" -ForegroundColor Green
} else {
Write-Host "UPX not found. Skipping compression." -ForegroundColor Yellow
}
}
# Copy to dist directory
Copy-Item "$BuildDir\$OutputName" "$DistDir\$OutputName" -Force
Write-Host "- Copied to: $DistDir\$OutputName"
Write-Host ""
Write-Host "Build script completed successfully!" -ForegroundColor Cyan
Write-Host "========================================" -ForegroundColor Cyan

198
Go_Updater/config/config.go Normal file
View File

@@ -0,0 +1,198 @@
package config
import (
"fmt"
"os"
"path/filepath"
"AUTO_MAA_Go_Updater/assets"
"gopkg.in/yaml.v3"
)
// Config 表示应用程序配置
type Config struct {
ResourceID string `yaml:"resource_id"`
CurrentVersion string `yaml:"current_version"`
UserAgent string `yaml:"user_agent"`
BackupURL string `yaml:"backup_url"`
LogLevel string `yaml:"log_level"`
AutoCheck bool `yaml:"auto_check"`
CheckInterval int `yaml:"check_interval"` // 秒
}
// ConfigManager 定义配置管理的接口方法
type ConfigManager interface {
Load() (*Config, error)
Save(config *Config) error
GetConfigPath() string
}
// DefaultConfigManager 实现 ConfigManager 接口
type DefaultConfigManager struct {
configPath string
}
// NewConfigManager 创建新的配置管理器
func NewConfigManager() ConfigManager {
configDir := getConfigDir()
configPath := filepath.Join(configDir, "config.yaml")
return &DefaultConfigManager{
configPath: configPath,
}
}
// GetConfigPath 返回配置文件的路径
func (cm *DefaultConfigManager) GetConfigPath() string {
return cm.configPath
}
// Load 读取并解析配置文件
func (cm *DefaultConfigManager) Load() (*Config, error) {
// 如果配置目录不存在则创建
configDir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return nil, fmt.Errorf("创建配置目录失败: %w", err)
}
// 如果配置文件不存在,创建默认配置
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
defaultConfig := getDefaultConfig()
if err := cm.Save(defaultConfig); err != nil {
return nil, fmt.Errorf("创建默认配置失败: %w", err)
}
return defaultConfig, nil
}
// 读取现有配置文件
data, err := os.ReadFile(cm.configPath)
if err != nil {
return nil, fmt.Errorf("读取配置文件失败: %w", err)
}
var config Config
if err := yaml.Unmarshal(data, &config); err != nil {
return nil, fmt.Errorf("解析配置文件失败: %w", err)
}
// 验证并应用缺失字段的默认值
if err := validateAndApplyDefaults(&config); err != nil {
return nil, fmt.Errorf("配置验证失败: %w", err)
}
return &config, nil
}
// Save 将配置写入文件
func (cm *DefaultConfigManager) Save(config *Config) error {
// 保存前验证配置
if err := validateConfig(config); err != nil {
return fmt.Errorf("配置验证失败: %w", err)
}
// 如果配置目录不存在则创建
configDir := filepath.Dir(cm.configPath)
if err := os.MkdirAll(configDir, 0755); err != nil {
return fmt.Errorf("创建配置目录失败: %w", err)
}
// 将配置序列化为 YAML
data, err := yaml.Marshal(config)
if err != nil {
return fmt.Errorf("序列化配置失败: %w", err)
}
// 写入文件
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
return fmt.Errorf("写入配置文件失败: %w", err)
}
return nil
}
// getDefaultConfig 返回带有默认值的配置
func getDefaultConfig() *Config {
// 首先尝试从嵌入模板加载
if templateData, err := assets.GetConfigTemplate(); err == nil {
var config Config
if err := yaml.Unmarshal(templateData, &config); err == nil {
return &config
}
}
// 如果模板加载失败则回退到硬编码默认值
return &Config{
ResourceID: "M9A", // 默认资源 ID
CurrentVersion: "v1.0.0",
UserAgent: "AUTO_MAA_Go_Updater/1.0",
BackupURL: "",
LogLevel: "info",
AutoCheck: true,
CheckInterval: 3600, // 1 小时
}
}
// validateConfig 验证配置值
func validateConfig(config *Config) error {
if config == nil {
return fmt.Errorf("配置不能为空")
}
if config.ResourceID == "" {
return fmt.Errorf("resource_id 不能为空")
}
if config.CurrentVersion == "" {
return fmt.Errorf("current_version 不能为空")
}
if config.UserAgent == "" {
return fmt.Errorf("user_agent 不能为空")
}
validLogLevels := map[string]bool{
"debug": true,
"info": true,
"warn": true,
"error": true,
}
if !validLogLevels[config.LogLevel] {
return fmt.Errorf("无效的 log_level: %s (必须是 debug, info, warn 或 error)", config.LogLevel)
}
if config.CheckInterval < 60 {
return fmt.Errorf("check_interval 必须至少为 60 秒")
}
return nil
}
// validateAndApplyDefaults 验证配置并为缺失字段应用默认值
func validateAndApplyDefaults(config *Config) error {
defaults := getDefaultConfig()
// 为空字段应用默认值
if config.UserAgent == "" {
config.UserAgent = defaults.UserAgent
}
if config.LogLevel == "" {
config.LogLevel = defaults.LogLevel
}
if config.CheckInterval == 0 {
config.CheckInterval = defaults.CheckInterval
}
if config.CurrentVersion == "" {
config.CurrentVersion = defaults.CurrentVersion
}
// 应用默认值后进行验证
return validateConfig(config)
}
// getConfigDir 返回配置目录路径
func getConfigDir() string {
// 在 Windows 上使用 APPDATA回退到当前目录
if appData := os.Getenv("APPDATA"); appData != "" {
return filepath.Join(appData, "AUTO_MAA_Go_Updater")
}
return "."
}

View File

@@ -0,0 +1,55 @@
{
"Function": {
"BossKey": "",
"HistoryRetentionTime": 0,
"HomeImageMode": "默认",
"IfAgreeBilibili": true,
"IfAllowSleep": false,
"IfSilence": false,
"IfSkipMumuSplashAds": false,
"UnattendedMode": false
},
"Notify": {
"AuthorizationCode": "",
"CompanyWebHookBotUrl": "",
"FromAddress": "",
"IfCompanyWebHookBot": false,
"IfPushPlyer": false,
"IfSendMail": false,
"IfSendSixStar": false,
"IfSendStatistic": false,
"IfServerChan": false,
"SMTPServerAddress": "",
"SendTaskResultTime": "不推送",
"ServerChanChannel": "",
"ServerChanKey": "",
"ServerChanTag": "",
"ToAddress": ""
},
"Start": {
"IfMinimizeDirectly": false,
"IfRunDirectly": false,
"IfSelfStart": false
},
"QFluentWidgets": {
"ThemeColor": "#ff009faa",
"ThemeMode": "Dark"
},
"UI": {
"IfShowTray": false,
"IfToTray": false,
"location": "100x100",
"maximized": false,
"size": "1200x700"
},
"Update": {
"IfAutoUpdate": false,
"ProxyUrlList": [],
"ThreadNumb": 8,
"UpdateType": "stable"
},
"Voice": {
"Enabled": false,
"Type": "simple"
}
}

View File

@@ -0,0 +1,153 @@
package config
import (
"os"
"path/filepath"
"testing"
)
func TestConfigManagerLoadSave(t *testing.T) {
// 为测试创建临时目录
tempDir := t.TempDir()
// 使用临时路径创建配置管理器
cm := &DefaultConfigManager{
configPath: filepath.Join(tempDir, "test-config.yaml"),
}
// 测试加载不存在的配置(应创建默认配置)
config, err := cm.Load()
if err != nil {
t.Errorf("加载配置失败: %v", err)
}
if config == nil {
t.Errorf("配置不应为 nil")
}
// 验证默认值
if config.CurrentVersion != "v1.0.0" {
t.Errorf("期望默认版本 v1.0.0,得到 %s", config.CurrentVersion)
}
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
t.Errorf("期望默认用户代理,得到 %s", config.UserAgent)
}
// 设置一些值
config.ResourceID = "TEST123"
// 保存配置
err = cm.Save(config)
if err != nil {
t.Errorf("保存配置失败: %v", err)
}
// 再次加载配置
loadedConfig, err := cm.Load()
if err != nil {
t.Errorf("加载已保存配置失败: %v", err)
}
// 验证值
if loadedConfig.ResourceID != "TEST123" {
t.Errorf("期望 ResourceID TEST123得到 %s", loadedConfig.ResourceID)
}
}
func TestConfigValidation(t *testing.T) {
tests := []struct {
name string
config *Config
expectError bool
}{
{
name: "空配置",
config: nil,
expectError: true,
},
{
name: "空 ResourceID",
config: &Config{
ResourceID: "",
CurrentVersion: "v1.0.0",
UserAgent: "Test/1.0",
LogLevel: "info",
CheckInterval: 3600,
},
expectError: true,
},
{
name: "有效配置",
config: &Config{
ResourceID: "TEST",
CurrentVersion: "v1.0.0",
UserAgent: "Test/1.0",
LogLevel: "info",
CheckInterval: 3600,
},
expectError: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateConfig(tt.config)
if tt.expectError && err == nil {
t.Errorf("期望错误但没有得到")
}
if !tt.expectError && err != nil {
t.Errorf("期望无错误但得到: %v", err)
}
})
}
}
func TestGetDefaultConfig(t *testing.T) {
config := getDefaultConfig()
if config == nil {
t.Fatal("getDefaultConfig() 返回 nil")
}
// 验证默认值
if config.ResourceID != "AUTO_MAA" {
t.Errorf("期望 ResourceID 'AUTO_MAA',得到 %s", config.ResourceID)
}
if config.CurrentVersion != "v1.0.0" {
t.Errorf("期望 CurrentVersion 'v1.0.0',得到 %s", config.CurrentVersion)
}
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
t.Errorf("期望 UserAgent 'AUTO_MAA_Go_Updater/1.0',得到 %s", config.UserAgent)
}
if config.LogLevel != "info" {
t.Errorf("期望 LogLevel 'info',得到 %s", config.LogLevel)
}
if config.CheckInterval != 3600 {
t.Errorf("期望 CheckInterval 3600得到 %d", config.CheckInterval)
}
if !config.AutoCheck {
t.Errorf("期望 AutoCheck true得到 %v", config.AutoCheck)
}
}
func TestGetConfigDir(t *testing.T) {
// 保存原始 APPDATA
originalAppData := os.Getenv("APPDATA")
defer os.Setenv("APPDATA", originalAppData)
// 测试设置了 APPDATA
os.Setenv("APPDATA", "C:\\Users\\Test\\AppData\\Roaming")
dir := getConfigDir()
expected := "C:\\Users\\Test\\AppData\\Roaming\\AUTO_MAA_Go_Updater"
if dir != expected {
t.Errorf("期望 %s得到 %s", expected, dir)
}
// 测试没有 APPDATA
os.Unsetenv("APPDATA")
dir = getConfigDir()
if dir != "." {
t.Errorf("期望当前目录,得到 %s", dir)
}
}

View File

@@ -0,0 +1,224 @@
package download
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"time"
)
// DownloadProgress 表示当前下载进度
type DownloadProgress struct {
BytesDownloaded int64
TotalBytes int64
Percentage float64
Speed int64 // 每秒字节数
}
// ProgressCallback 在下载过程中调用以报告进度
type ProgressCallback func(DownloadProgress)
// DownloadManager 定义下载操作的接口
type DownloadManager interface {
Download(url, destination string, progressCallback ProgressCallback) error
DownloadWithResume(url, destination string, progressCallback ProgressCallback) error
ValidateChecksum(filePath, expectedChecksum string) error
SetTimeout(timeout time.Duration)
}
// Manager 实现 DownloadManager 接口
type Manager struct {
client *http.Client
timeout time.Duration
}
// NewManager 创建新的下载管理器
func NewManager() *Manager {
return &Manager{
client: &http.Client{
Timeout: 30 * time.Second,
},
timeout: 30 * time.Second,
}
}
// Download 从给定 URL 下载文件到目标路径
func (m *Manager) Download(url, destination string, progressCallback ProgressCallback) error {
return m.downloadWithContext(context.Background(), url, destination, progressCallback, false)
}
// DownloadWithResume 下载文件并支持断点续传
func (m *Manager) DownloadWithResume(url, destination string, progressCallback ProgressCallback) error {
return m.downloadWithContext(context.Background(), url, destination, progressCallback, true)
}
// downloadWithContext 执行实际的下载并支持上下文
func (m *Manager) downloadWithContext(ctx context.Context, url, destination string, progressCallback ProgressCallback, resume bool) error {
// 如果目标目录不存在则创建
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
return fmt.Errorf("创建目标目录失败: %w", err)
}
// 检查文件是否存在以支持断点续传
var existingSize int64
if resume {
if stat, err := os.Stat(destination); err == nil {
existingSize = stat.Size()
}
}
// 创建 HTTP 请求
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
return fmt.Errorf("创建请求失败: %w", err)
}
// 为断点续传添加范围头
if resume && existingSize > 0 {
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", existingSize))
}
// 执行请求
resp, err := m.client.Do(req)
if err != nil {
return fmt.Errorf("执行请求失败: %w", err)
}
defer resp.Body.Close()
// 检查响应状态
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
return fmt.Errorf("意外的状态码: %d", resp.StatusCode)
}
// 获取总大小
totalSize := existingSize
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil {
totalSize += size
}
}
// 打开目标文件
var file *os.File
if resume && existingSize > 0 {
file, err = os.OpenFile(destination, os.O_WRONLY|os.O_APPEND, 0644)
} else {
file, err = os.Create(destination)
existingSize = 0
}
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer file.Close()
// 下载并跟踪进度
return m.copyWithProgress(resp.Body, file, existingSize, totalSize, progressCallback)
}
// copyWithProgress 复制数据并跟踪进度
func (m *Manager) copyWithProgress(src io.Reader, dst io.Writer, startBytes, totalBytes int64, progressCallback ProgressCallback) error {
buffer := make([]byte, 32*1024) // 32KB 缓冲区
downloaded := startBytes
startTime := time.Now()
lastUpdate := startTime
for {
n, err := src.Read(buffer)
if n > 0 {
if _, writeErr := dst.Write(buffer[:n]); writeErr != nil {
return fmt.Errorf("写入目标失败: %w", writeErr)
}
downloaded += int64(n)
// 每 100ms 更新一次进度
now := time.Now()
if progressCallback != nil && now.Sub(lastUpdate) >= 100*time.Millisecond {
elapsed := now.Sub(startTime).Seconds()
speed := int64(0)
if elapsed > 0 {
speed = int64(float64(downloaded-startBytes) / elapsed)
}
percentage := float64(0)
if totalBytes > 0 {
percentage = float64(downloaded) / float64(totalBytes) * 100
}
progressCallback(DownloadProgress{
BytesDownloaded: downloaded,
TotalBytes: totalBytes,
Percentage: percentage,
Speed: speed,
})
lastUpdate = now
}
}
if err == io.EOF {
break
}
if err != nil {
return fmt.Errorf("从源读取失败: %w", err)
}
}
// 最终进度更新
if progressCallback != nil {
elapsed := time.Since(startTime).Seconds()
speed := int64(0)
if elapsed > 0 {
speed = int64(float64(downloaded-startBytes) / elapsed)
}
percentage := float64(100)
if totalBytes > 0 {
percentage = float64(downloaded) / float64(totalBytes) * 100
}
progressCallback(DownloadProgress{
BytesDownloaded: downloaded,
TotalBytes: totalBytes,
Percentage: percentage,
Speed: speed,
})
}
return nil
}
// ValidateChecksum 验证文件的 SHA256 校验和
func (m *Manager) ValidateChecksum(filePath, expectedChecksum string) error {
if expectedChecksum == "" {
return nil // 没有校验和需要验证
}
file, err := os.Open(filePath)
if err != nil {
return fmt.Errorf("打开文件进行校验和验证失败: %w", err)
}
defer file.Close()
hash := sha256.New()
if _, err := io.Copy(hash, file); err != nil {
return fmt.Errorf("计算校验和失败: %w", err)
}
actualChecksum := hex.EncodeToString(hash.Sum(nil))
if actualChecksum != expectedChecksum {
return fmt.Errorf("校验和不匹配: 期望 %s得到 %s", expectedChecksum, actualChecksum)
}
return nil
}
// SetTimeout 设置下载操作的超时时间
func (m *Manager) SetTimeout(timeout time.Duration) {
m.timeout = timeout
m.client.Timeout = timeout
}

File diff suppressed because it is too large Load Diff

219
Go_Updater/errors/errors.go Normal file
View File

@@ -0,0 +1,219 @@
package errors
import (
"fmt"
"time"
)
// ErrorType 定义错误类型枚举
type ErrorType int
const (
NetworkError ErrorType = iota
APIError
FileError
ConfigError
InstallError
)
// String 返回错误类型的字符串表示
func (et ErrorType) String() string {
switch et {
case NetworkError:
return "NetworkError"
case APIError:
return "APIError"
case FileError:
return "FileError"
case ConfigError:
return "ConfigError"
case InstallError:
return "InstallError"
default:
return "UnknownError"
}
}
// UpdaterError 统一的错误结构体
type UpdaterError struct {
Type ErrorType
Message string
Cause error
Timestamp time.Time
Context map[string]interface{}
}
// Error 实现error接口
func (ue *UpdaterError) Error() string {
if ue.Cause != nil {
return fmt.Sprintf("[%s] %s: %v", ue.Type, ue.Message, ue.Cause)
}
return fmt.Sprintf("[%s] %s", ue.Type, ue.Message)
}
// Unwrap 支持错误链
func (ue *UpdaterError) Unwrap() error {
return ue.Cause
}
// NewUpdaterError 创建新的UpdaterError
func NewUpdaterError(errorType ErrorType, message string, cause error) *UpdaterError {
return &UpdaterError{
Type: errorType,
Message: message,
Cause: cause,
Timestamp: time.Now(),
Context: make(map[string]interface{}),
}
}
// WithContext 添加上下文信息
func (ue *UpdaterError) WithContext(key string, value interface{}) *UpdaterError {
ue.Context[key] = value
return ue
}
// GetUserFriendlyMessage 获取用户友好的错误消息
func (ue *UpdaterError) GetUserFriendlyMessage() string {
switch ue.Type {
case NetworkError:
return "网络连接失败,请检查网络连接后重试"
case APIError:
return "服务器响应异常,请稍后重试或联系技术支持"
case FileError:
return "文件操作失败,请检查文件权限和磁盘空间"
case ConfigError:
return "配置文件错误,程序将使用默认配置"
case InstallError:
return "安装过程中出现错误,程序将尝试回滚更改"
default:
return "发生未知错误,请联系技术支持"
}
}
// RetryConfig 重试配置
type RetryConfig struct {
MaxRetries int
InitialDelay time.Duration
MaxDelay time.Duration
BackoffFactor float64
RetryableErrors []ErrorType
}
// DefaultRetryConfig 默认重试配置
func DefaultRetryConfig() *RetryConfig {
return &RetryConfig{
MaxRetries: 3,
InitialDelay: time.Second,
MaxDelay: 30 * time.Second,
BackoffFactor: 2.0,
RetryableErrors: []ErrorType{NetworkError, APIError},
}
}
// IsRetryable 检查错误是否可重试
func (rc *RetryConfig) IsRetryable(err error) bool {
if ue, ok := err.(*UpdaterError); ok {
for _, retryableType := range rc.RetryableErrors {
if ue.Type == retryableType {
return true
}
}
}
return false
}
// CalculateDelay 计算重试延迟时间
func (rc *RetryConfig) CalculateDelay(attempt int) time.Duration {
delay := time.Duration(float64(rc.InitialDelay) * pow(rc.BackoffFactor, float64(attempt)))
if delay > rc.MaxDelay {
delay = rc.MaxDelay
}
return delay
}
// pow 简单的幂运算实现
func pow(base, exp float64) float64 {
result := 1.0
for i := 0; i < int(exp); i++ {
result *= base
}
return result
}
// RetryableOperation 可重试的操作函数类型
type RetryableOperation func() error
// ExecuteWithRetry 执行带重试的操作
func ExecuteWithRetry(operation RetryableOperation, config *RetryConfig) error {
var lastErr error
for attempt := 0; attempt <= config.MaxRetries; attempt++ {
err := operation()
if err == nil {
return nil
}
lastErr = err
// 如果不是可重试的错误,直接返回
if !config.IsRetryable(err) {
return err
}
// 如果已经是最后一次尝试,不再等待
if attempt == config.MaxRetries {
break
}
// 计算延迟时间并等待
delay := config.CalculateDelay(attempt)
time.Sleep(delay)
}
return lastErr
}
// ErrorHandler 错误处理器接口
type ErrorHandler interface {
HandleError(err error) error
ShouldRetry(err error) bool
GetUserMessage(err error) string
}
// DefaultErrorHandler 默认错误处理器
type DefaultErrorHandler struct {
retryConfig *RetryConfig
}
// NewDefaultErrorHandler 创建默认错误处理器
func NewDefaultErrorHandler() *DefaultErrorHandler {
return &DefaultErrorHandler{
retryConfig: DefaultRetryConfig(),
}
}
// HandleError 处理错误
func (h *DefaultErrorHandler) HandleError(err error) error {
if ue, ok := err.(*UpdaterError); ok {
// 记录错误上下文
ue.WithContext("handled_at", time.Now())
return ue
}
// 将普通错误包装为UpdaterError
return NewUpdaterError(NetworkError, "未分类错误", err)
}
// ShouldRetry 判断是否应该重试
func (h *DefaultErrorHandler) ShouldRetry(err error) bool {
return h.retryConfig.IsRetryable(err)
}
// GetUserMessage 获取用户友好的错误消息
func (h *DefaultErrorHandler) GetUserMessage(err error) string {
if ue, ok := err.(*UpdaterError); ok {
return ue.GetUserFriendlyMessage()
}
return "发生未知错误,请联系技术支持"
}

View File

@@ -0,0 +1,287 @@
package errors
import (
"fmt"
"testing"
"time"
)
func TestUpdaterError_Error(t *testing.T) {
tests := []struct {
name string
err *UpdaterError
expected string
}{
{
name: "error with cause",
err: &UpdaterError{
Type: NetworkError,
Message: "connection failed",
Cause: fmt.Errorf("timeout"),
},
expected: "[NetworkError] connection failed: timeout",
},
{
name: "error without cause",
err: &UpdaterError{
Type: APIError,
Message: "invalid response",
Cause: nil,
},
expected: "[APIError] invalid response",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.expected {
t.Errorf("UpdaterError.Error() = %v, want %v", got, tt.expected)
}
})
}
}
func TestNewUpdaterError(t *testing.T) {
cause := fmt.Errorf("original error")
err := NewUpdaterError(FileError, "test message", cause)
if err.Type != FileError {
t.Errorf("Expected type %v, got %v", FileError, err.Type)
}
if err.Message != "test message" {
t.Errorf("Expected message 'test message', got '%v'", err.Message)
}
if err.Cause != cause {
t.Errorf("Expected cause %v, got %v", cause, err.Cause)
}
if err.Context == nil {
t.Error("Expected context to be initialized")
}
}
func TestUpdaterError_WithContext(t *testing.T) {
err := NewUpdaterError(ConfigError, "test", nil)
err.WithContext("key1", "value1").WithContext("key2", 42)
if err.Context["key1"] != "value1" {
t.Errorf("Expected context key1 to be 'value1', got %v", err.Context["key1"])
}
if err.Context["key2"] != 42 {
t.Errorf("Expected context key2 to be 42, got %v", err.Context["key2"])
}
}
func TestUpdaterError_GetUserFriendlyMessage(t *testing.T) {
tests := []struct {
errorType ErrorType
expected string
}{
{NetworkError, "网络连接失败,请检查网络连接后重试"},
{APIError, "服务器响应异常,请稍后重试或联系技术支持"},
{FileError, "文件操作失败,请检查文件权限和磁盘空间"},
{ConfigError, "配置文件错误,程序将使用默认配置"},
{InstallError, "安装过程中出现错误,程序将尝试回滚更改"},
}
for _, tt := range tests {
t.Run(tt.errorType.String(), func(t *testing.T) {
err := NewUpdaterError(tt.errorType, "test", nil)
if got := err.GetUserFriendlyMessage(); got != tt.expected {
t.Errorf("GetUserFriendlyMessage() = %v, want %v", got, tt.expected)
}
})
}
}
func TestRetryConfig_IsRetryable(t *testing.T) {
config := DefaultRetryConfig()
tests := []struct {
name string
err error
expected bool
}{
{
name: "retryable network error",
err: NewUpdaterError(NetworkError, "test", nil),
expected: true,
},
{
name: "retryable api error",
err: NewUpdaterError(APIError, "test", nil),
expected: true,
},
{
name: "non-retryable file error",
err: NewUpdaterError(FileError, "test", nil),
expected: false,
},
{
name: "regular error",
err: fmt.Errorf("regular error"),
expected: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := config.IsRetryable(tt.err); got != tt.expected {
t.Errorf("IsRetryable() = %v, want %v", got, tt.expected)
}
})
}
}
func TestRetryConfig_CalculateDelay(t *testing.T) {
config := DefaultRetryConfig()
tests := []struct {
attempt int
expected time.Duration
}{
{0, time.Second},
{1, 2 * time.Second},
{2, 4 * time.Second},
{10, 30 * time.Second}, // should be capped at MaxDelay
}
for _, tt := range tests {
t.Run(fmt.Sprintf("attempt_%d", tt.attempt), func(t *testing.T) {
if got := config.CalculateDelay(tt.attempt); got != tt.expected {
t.Errorf("CalculateDelay(%d) = %v, want %v", tt.attempt, got, tt.expected)
}
})
}
}
func TestExecuteWithRetry(t *testing.T) {
config := DefaultRetryConfig()
config.InitialDelay = time.Millisecond // 加快测试速度
t.Run("success on first try", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return nil
}
err := ExecuteWithRetry(operation, config)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if attempts != 1 {
t.Errorf("Expected 1 attempt, got %d", attempts)
}
})
t.Run("success after retries", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
if attempts < 3 {
return NewUpdaterError(NetworkError, "temporary failure", nil)
}
return nil
}
err := ExecuteWithRetry(operation, config)
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
if attempts != 3 {
t.Errorf("Expected 3 attempts, got %d", attempts)
}
})
t.Run("non-retryable error", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return NewUpdaterError(FileError, "file not found", nil)
}
err := ExecuteWithRetry(operation, config)
if err == nil {
t.Error("Expected error, got nil")
}
if attempts != 1 {
t.Errorf("Expected 1 attempt, got %d", attempts)
}
})
t.Run("max retries exceeded", func(t *testing.T) {
attempts := 0
operation := func() error {
attempts++
return NewUpdaterError(NetworkError, "persistent failure", nil)
}
err := ExecuteWithRetry(operation, config)
if err == nil {
t.Error("Expected error, got nil")
}
expectedAttempts := config.MaxRetries + 1
if attempts != expectedAttempts {
t.Errorf("Expected %d attempts, got %d", expectedAttempts, attempts)
}
})
}
func TestDefaultErrorHandler(t *testing.T) {
handler := NewDefaultErrorHandler()
t.Run("handle updater error", func(t *testing.T) {
originalErr := NewUpdaterError(NetworkError, "test", nil)
handledErr := handler.HandleError(originalErr)
if handledErr != originalErr {
t.Error("Expected same error instance")
}
if originalErr.Context["handled_at"] == nil {
t.Error("Expected handled_at context to be set")
}
})
t.Run("handle regular error", func(t *testing.T) {
originalErr := fmt.Errorf("regular error")
handledErr := handler.HandleError(originalErr)
if ue, ok := handledErr.(*UpdaterError); ok {
if ue.Type != NetworkError {
t.Errorf("Expected NetworkError, got %v", ue.Type)
}
if ue.Cause != originalErr {
t.Error("Expected original error as cause")
}
} else {
t.Error("Expected UpdaterError")
}
})
t.Run("should retry", func(t *testing.T) {
retryableErr := NewUpdaterError(NetworkError, "test", nil)
nonRetryableErr := NewUpdaterError(FileError, "test", nil)
if !handler.ShouldRetry(retryableErr) {
t.Error("Expected network error to be retryable")
}
if handler.ShouldRetry(nonRetryableErr) {
t.Error("Expected file error to not be retryable")
}
})
t.Run("get user message", func(t *testing.T) {
updaterErr := NewUpdaterError(NetworkError, "test", nil)
regularErr := fmt.Errorf("regular error")
userMsg1 := handler.GetUserMessage(updaterErr)
userMsg2 := handler.GetUserMessage(regularErr)
if userMsg1 != "网络连接失败,请检查网络连接后重试" {
t.Errorf("Unexpected user message: %s", userMsg1)
}
if userMsg2 != "发生未知错误,请联系技术支持" {
t.Errorf("Unexpected user message: %s", userMsg2)
}
})
}

42
Go_Updater/go.mod Normal file
View File

@@ -0,0 +1,42 @@
module AUTO_MAA_Go_Updater
go 1.24.5
require (
fyne.io/fyne/v2 v2.6.1
gopkg.in/yaml.v3 v3.0.1
)
require (
fyne.io/systray v1.11.0 // indirect
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/fredbi/uri v1.1.0 // indirect
github.com/fsnotify/fsnotify v1.7.0 // indirect
github.com/fyne-io/gl-js v0.1.0 // indirect
github.com/fyne-io/glfw-js v0.2.0 // indirect
github.com/fyne-io/image v0.1.1 // indirect
github.com/fyne-io/oksvg v0.1.0 // indirect
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
github.com/go-text/render v0.2.0 // indirect
github.com/go-text/typesetting v0.2.1 // indirect
github.com/godbus/dbus/v5 v5.1.0 // indirect
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.0 // indirect
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rymdport/portal v0.4.1 // indirect
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
github.com/stretchr/testify v1.10.0 // indirect
github.com/yuin/goldmark v1.7.8 // indirect
golang.org/x/image v0.24.0 // indirect
golang.org/x/net v0.35.0 // indirect
golang.org/x/sys v0.30.0 // indirect
golang.org/x/text v0.22.0 // indirect
)

80
Go_Updater/go.sum Normal file
View File

@@ -0,0 +1,80 @@
fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

513
Go_Updater/gui/manager.go Normal file
View File

@@ -0,0 +1,513 @@
package gui
import (
"fmt"
"fyne.io/fyne/v2"
"fyne.io/fyne/v2/app"
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
// UpdateStatus 表示更新过程的当前状态
type UpdateStatus int
const (
StatusChecking UpdateStatus = iota
StatusUpdateAvailable
StatusDownloading
StatusInstalling
StatusCompleted
StatusError
)
// Config 表示 GUI 的配置结构
type Config struct {
ResourceID string
CurrentVersion string
UserAgent string
BackupURL string
}
// GUIManager 定义 GUI 管理的接口方法
type GUIManager interface {
ShowMainWindow()
UpdateStatus(status UpdateStatus, message string)
ShowProgress(percentage float64)
ShowError(errorMsg string)
ShowConfigDialog() (*Config, error)
Close()
}
// Manager 实现 GUIManager 接口
type Manager struct {
app fyne.App
window fyne.Window
statusLabel *widget.Label
progressBar *widget.ProgressBar
actionButton *widget.Button
versionLabel *widget.Label
releaseNotes *widget.RichText
currentStatus UpdateStatus
onCheckUpdate func()
onCancel func()
}
// NewManager creates a new GUI manager instance
func NewManager() *Manager {
a := app.New()
a.SetIcon(theme.ComputerIcon())
w := a.NewWindow("AUTO_MAA_Go_Updater")
w.Resize(fyne.NewSize(500, 400))
w.SetFixedSize(false)
w.CenterOnScreen()
return &Manager{
app: a,
window: w,
}
}
// SetCallbacks sets the callback functions for user actions
func (m *Manager) SetCallbacks(onCheckUpdate, onCancel func()) {
m.onCheckUpdate = onCheckUpdate
m.onCancel = onCancel
}
// ShowMainWindow displays the main application window
func (m *Manager) ShowMainWindow() {
// Create UI components
m.createUIComponents()
// Create main layout
content := m.createMainLayout()
m.window.SetContent(content)
m.window.ShowAndRun()
}
// createUIComponents initializes all UI components
func (m *Manager) createUIComponents() {
// Status label
m.statusLabel = widget.NewLabel("准备检查更新...")
m.statusLabel.Alignment = fyne.TextAlignCenter
// Progress bar
m.progressBar = widget.NewProgressBar()
m.progressBar.Hide()
// Version label
m.versionLabel = widget.NewLabel("当前版本: 未知")
m.versionLabel.TextStyle = fyne.TextStyle{Italic: true}
// Release notes
m.releaseNotes = widget.NewRichText()
m.releaseNotes.Hide()
// Action button
m.actionButton = widget.NewButton("检查更新", func() {
if m.onCheckUpdate != nil {
m.onCheckUpdate()
}
})
m.actionButton.Importance = widget.HighImportance
}
// createMainLayout creates the main window layout
func (m *Manager) createMainLayout() *fyne.Container {
// Header section
header := container.NewVBox(
widget.NewCard("", "", container.NewVBox(
widget.NewLabelWithStyle("AUTO_MAA_Go_Updater", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
m.versionLabel,
)),
)
// Status section
statusSection := container.NewVBox(
m.statusLabel,
m.progressBar,
)
// Release notes section
releaseNotesCard := widget.NewCard("更新日志", "", container.NewScroll(m.releaseNotes))
releaseNotesCard.Hide()
// Button section
buttonSection := container.NewHBox(
widget.NewButton("配置", func() {
m.showConfigDialog()
}),
widget.NewSpacer(),
m.actionButton,
)
// Main layout
return container.NewVBox(
header,
widget.NewSeparator(),
statusSection,
releaseNotesCard,
widget.NewSeparator(),
buttonSection,
)
}
// UpdateStatus updates the current status and UI accordingly
func (m *Manager) UpdateStatus(status UpdateStatus, message string) {
m.currentStatus = status
m.statusLabel.SetText(message)
switch status {
case StatusChecking:
m.actionButton.SetText("检查中...")
m.actionButton.Disable()
m.progressBar.Hide()
case StatusUpdateAvailable:
m.actionButton.SetText("开始更新")
m.actionButton.Enable()
m.progressBar.Hide()
case StatusDownloading:
m.actionButton.SetText("下载中...")
m.actionButton.Disable()
m.progressBar.Show()
case StatusInstalling:
m.actionButton.SetText("安装中...")
m.actionButton.Disable()
m.progressBar.Show()
case StatusCompleted:
m.actionButton.SetText("完成")
m.actionButton.Enable()
m.progressBar.Hide()
case StatusError:
m.actionButton.SetText("重试")
m.actionButton.Enable()
m.progressBar.Hide()
}
}
// ShowProgress updates the progress bar
func (m *Manager) ShowProgress(percentage float64) {
if percentage < 0 {
percentage = 0
}
if percentage > 100 {
percentage = 100
}
m.progressBar.SetValue(percentage / 100.0)
m.progressBar.Show()
}
// ShowError displays an error dialog
func (m *Manager) ShowError(errorMsg string) {
dialog.ShowError(fmt.Errorf(errorMsg), m.window)
}
// ShowConfigDialog displays the configuration dialog
func (m *Manager) ShowConfigDialog() (*Config, error) {
return m.showConfigDialog()
}
// showConfigDialog creates and shows the configuration dialog
func (m *Manager) showConfigDialog() (*Config, error) {
// Create form entries
resourceIDEntry := widget.NewEntry()
resourceIDEntry.SetPlaceHolder("例如: M9A")
versionEntry := widget.NewEntry()
versionEntry.SetPlaceHolder("例如: v1.0.0")
userAgentEntry := widget.NewEntry()
userAgentEntry.SetText("AUTO_MAA_Go_Updater/1.0")
backupURLEntry := widget.NewEntry()
backupURLEntry.SetPlaceHolder("备用下载地址(可选)")
// Create form
form := &widget.Form{
Items: []*widget.FormItem{
{Text: "资源ID:", Widget: resourceIDEntry},
{Text: "当前版本:", Widget: versionEntry},
{Text: "用户代理:", Widget: userAgentEntry},
{Text: "备用下载地址:", Widget: backupURLEntry},
},
}
// Create result channel
resultChan := make(chan *Config, 1)
errorChan := make(chan error, 1)
// Create dialog
configDialog := dialog.NewCustomConfirm(
"配置设置",
"保存",
"取消",
form,
func(confirmed bool) {
if confirmed {
config := &Config{
ResourceID: resourceIDEntry.Text,
CurrentVersion: versionEntry.Text,
UserAgent: userAgentEntry.Text,
BackupURL: backupURLEntry.Text,
}
// Basic validation
if config.ResourceID == "" {
errorChan <- fmt.Errorf("资源ID不能为空")
return
}
if config.CurrentVersion == "" {
errorChan <- fmt.Errorf("当前版本不能为空")
return
}
resultChan <- config
} else {
errorChan <- fmt.Errorf("用户取消了配置")
}
},
m.window,
)
// Add help text
helpText := widget.NewRichTextFromMarkdown(`
**配置说明:**
- **资源ID**: Mirror酱服务中的资源标识符
- **当前版本**: 当前软件的版本号
- **用户代理**: HTTP请求的用户代理字符串
- **备用下载地址**: 当Mirror酱服务不可用时的备用下载地址
`)
// Create container with help text
dialogContent := container.NewVBox(
form,
widget.NewSeparator(),
helpText,
)
configDialog.SetContent(dialogContent)
configDialog.Resize(fyne.NewSize(600, 500))
configDialog.Show()
// Wait for result
select {
case config := <-resultChan:
return config, nil
case err := <-errorChan:
return nil, err
}
}
// SetVersionInfo updates the version display
func (m *Manager) SetVersionInfo(version string) {
m.versionLabel.SetText(fmt.Sprintf("当前版本: %s", version))
}
// ShowReleaseNotes displays the release notes
func (m *Manager) ShowReleaseNotes(notes string) {
if notes != "" {
m.releaseNotes.ParseMarkdown(notes)
// Find the release notes card and show it
if parent := m.window.Content().(*container.VBox); parent != nil {
for _, obj := range parent.Objects {
if card, ok := obj.(*widget.Card); ok && card.Title == "更新日志" {
card.Show()
break
}
}
}
}
}
// UpdateStatusWithDetails updates status with detailed information
func (m *Manager) UpdateStatusWithDetails(status UpdateStatus, message string, details map[string]string) {
m.UpdateStatus(status, message)
// Update version info if provided
if version, ok := details["version"]; ok {
m.SetVersionInfo(version)
}
// Show release notes if provided
if notes, ok := details["release_notes"]; ok {
m.ShowReleaseNotes(notes)
}
// Update progress if provided
if progress, ok := details["progress"]; ok {
if p, err := fmt.Sscanf(progress, "%f", new(float64)); err == nil && p == 1 {
var progressValue float64
fmt.Sscanf(progress, "%f", &progressValue)
m.ShowProgress(progressValue)
}
}
}
// ShowProgressWithSpeed shows progress with download speed information
func (m *Manager) ShowProgressWithSpeed(percentage float64, speed int64, eta string) {
m.ShowProgress(percentage)
// Update status with speed and ETA information
speedText := m.formatSpeed(speed)
statusText := fmt.Sprintf("下载中... %.1f%% (%s)", percentage, speedText)
if eta != "" {
statusText += fmt.Sprintf(" - 剩余时间: %s", eta)
}
m.statusLabel.SetText(statusText)
}
// formatSpeed formats the download speed for display
func (m *Manager) formatSpeed(bytesPerSecond int64) string {
if bytesPerSecond < 1024 {
return fmt.Sprintf("%d B/s", bytesPerSecond)
} else if bytesPerSecond < 1024*1024 {
return fmt.Sprintf("%.1f KB/s", float64(bytesPerSecond)/1024)
} else {
return fmt.Sprintf("%.1f MB/s", float64(bytesPerSecond)/(1024*1024))
}
}
// ShowConfirmDialog shows a confirmation dialog
func (m *Manager) ShowConfirmDialog(title, message string, callback func(bool)) {
dialog.ShowConfirm(title, message, callback, m.window)
}
// ShowInfoDialog shows an information dialog
func (m *Manager) ShowInfoDialog(title, message string) {
dialog.ShowInformation(title, message, m.window)
}
// ShowUpdateAvailableDialog shows a dialog when update is available
func (m *Manager) ShowUpdateAvailableDialog(currentVersion, newVersion, releaseNotes string, onConfirm func()) {
content := container.NewVBox(
widget.NewLabel(fmt.Sprintf("发现新版本: %s", newVersion)),
widget.NewLabel(fmt.Sprintf("当前版本: %s", currentVersion)),
widget.NewSeparator(),
)
if releaseNotes != "" {
notesWidget := widget.NewRichText()
notesWidget.ParseMarkdown(releaseNotes)
notesScroll := container.NewScroll(notesWidget)
notesScroll.SetMinSize(fyne.NewSize(400, 200))
content.Add(widget.NewLabel("更新内容:"))
content.Add(notesScroll)
}
dialog.ShowCustomConfirm(
"发现新版本",
"立即更新",
"稍后提醒",
content,
func(confirmed bool) {
if confirmed && onConfirm != nil {
onConfirm()
}
},
m.window,
)
}
// SetActionButtonCallback sets the callback for the main action button
func (m *Manager) SetActionButtonCallback(callback func()) {
if m.actionButton != nil {
m.actionButton.OnTapped = callback
}
}
// EnableActionButton enables or disables the action button
func (m *Manager) EnableActionButton(enabled bool) {
if m.actionButton != nil {
if enabled {
m.actionButton.Enable()
} else {
m.actionButton.Disable()
}
}
}
// SetActionButtonText sets the text of the action button
func (m *Manager) SetActionButtonText(text string) {
if m.actionButton != nil {
m.actionButton.SetText(text)
}
}
// ShowErrorWithRetry shows an error with retry option
func (m *Manager) ShowErrorWithRetry(errorMsg string, onRetry func()) {
dialog.ShowCustomConfirm(
"错误",
"重试",
"取消",
widget.NewLabel(errorMsg),
func(retry bool) {
if retry && onRetry != nil {
onRetry()
}
},
m.window,
)
}
// UpdateProgressBar updates the progress bar with custom styling
func (m *Manager) UpdateProgressBar(percentage float64, color string) {
m.ShowProgress(percentage)
// Note: Fyne doesn't support custom colors easily, but we keep the interface for future enhancement
}
// HideProgressBar hides the progress bar
func (m *Manager) HideProgressBar() {
if m.progressBar != nil {
m.progressBar.Hide()
}
}
// ShowProgressBar shows the progress bar
func (m *Manager) ShowProgressBar() {
if m.progressBar != nil {
m.progressBar.Show()
}
}
// SetWindowTitle sets the window title
func (m *Manager) SetWindowTitle(title string) {
if m.window != nil {
m.window.SetTitle(title)
}
}
// GetCurrentStatus returns the current update status
func (m *Manager) GetCurrentStatus() UpdateStatus {
return m.currentStatus
}
// IsWindowVisible returns whether the window is currently visible
func (m *Manager) IsWindowVisible() bool {
return m.window != nil && m.window.Content() != nil
}
// RefreshUI refreshes the user interface
func (m *Manager) RefreshUI() {
if m.window != nil && m.window.Content() != nil {
m.window.Content().Refresh()
}
}
// Close closes the application
func (m *Manager) Close() {
if m.window != nil {
m.window.Close()
}
}

View File

@@ -0,0 +1,227 @@
package gui
import (
"testing"
"time"
)
func TestNewManager(t *testing.T) {
manager := NewManager()
if manager == nil {
t.Fatal("NewManager() returned nil")
}
if manager.app == nil {
t.Error("Manager app is nil")
}
if manager.window == nil {
t.Error("Manager window is nil")
}
}
func TestUpdateStatus(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Test different status updates
testCases := []struct {
status UpdateStatus
message string
}{
{StatusChecking, "检查更新中..."},
{StatusUpdateAvailable, "发现新版本"},
{StatusDownloading, "下载中..."},
{StatusInstalling, "安装中..."},
{StatusCompleted, "更新完成"},
{StatusError, "更新失败"},
}
for _, tc := range testCases {
manager.UpdateStatus(tc.status, tc.message)
if manager.GetCurrentStatus() != tc.status {
t.Errorf("Expected status %v, got %v", tc.status, manager.GetCurrentStatus())
}
if manager.statusLabel.Text != tc.message {
t.Errorf("Expected message '%s', got '%s'", tc.message, manager.statusLabel.Text)
}
}
}
func TestShowProgress(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Test progress values
testValues := []float64{0, 25.5, 50, 75.8, 100, 150, -10}
expectedValues := []float64{0, 25.5, 50, 75.8, 100, 100, 0}
for i, value := range testValues {
manager.ShowProgress(value)
expected := expectedValues[i] / 100.0
if manager.progressBar.Value != expected {
t.Errorf("Expected progress %.2f, got %.2f", expected, manager.progressBar.Value)
}
}
}
func TestSetVersionInfo(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
version := "v1.2.3"
manager.SetVersionInfo(version)
expectedText := "当前版本: v1.2.3"
if manager.versionLabel.Text != expectedText {
t.Errorf("Expected version text '%s', got '%s'", expectedText, manager.versionLabel.Text)
}
}
func TestFormatSpeed(t *testing.T) {
manager := NewManager()
testCases := []struct {
speed int64
expected string
}{
{512, "512 B/s"},
{1536, "1.5 KB/s"},
{1048576, "1.0 MB/s"},
{2621440, "2.5 MB/s"},
}
for _, tc := range testCases {
result := manager.formatSpeed(tc.speed)
if result != tc.expected {
t.Errorf("Expected speed format '%s', got '%s'", tc.expected, result)
}
}
}
func TestShowProgressWithSpeed(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
percentage := 45.5
speed := int64(1048576) // 1 MB/s
eta := "2分钟"
manager.ShowProgressWithSpeed(percentage, speed, eta)
expectedProgress := percentage / 100.0
if manager.progressBar.Value != expectedProgress {
t.Errorf("Expected progress %.2f, got %.2f", expectedProgress, manager.progressBar.Value)
}
expectedStatus := "下载中... 45.5% (1.0 MB/s) - 剩余时间: 2分钟"
if manager.statusLabel.Text != expectedStatus {
t.Errorf("Expected status '%s', got '%s'", expectedStatus, manager.statusLabel.Text)
}
}
func TestActionButtonStates(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Test enabling/disabling
manager.EnableActionButton(false)
if !manager.actionButton.Disabled() {
t.Error("Action button should be disabled")
}
manager.EnableActionButton(true)
if manager.actionButton.Disabled() {
t.Error("Action button should be enabled")
}
// Test text setting
testText := "测试按钮"
manager.SetActionButtonText(testText)
if manager.actionButton.Text != testText {
t.Errorf("Expected button text '%s', got '%s'", testText, manager.actionButton.Text)
}
}
func TestProgressBarVisibility(t *testing.T) {
manager := NewManager()
manager.createUIComponents()
// Initially hidden
if manager.progressBar.Visible() {
t.Error("Progress bar should be initially hidden")
}
// Show progress bar
manager.ShowProgressBar()
if !manager.progressBar.Visible() {
t.Error("Progress bar should be visible after ShowProgressBar()")
}
// Hide progress bar
manager.HideProgressBar()
if manager.progressBar.Visible() {
t.Error("Progress bar should be hidden after HideProgressBar()")
}
}
func TestSetCallbacks(t *testing.T) {
manager := NewManager()
checkUpdateCalled := false
cancelCalled := false
onCheckUpdate := func() {
checkUpdateCalled = true
}
onCancel := func() {
cancelCalled = true
}
manager.SetCallbacks(onCheckUpdate, onCancel)
// Verify callbacks are set
if manager.onCheckUpdate == nil {
t.Error("onCheckUpdate callback not set")
}
if manager.onCancel == nil {
t.Error("onCancel callback not set")
}
// Test callback execution
manager.onCheckUpdate()
if !checkUpdateCalled {
t.Error("onCheckUpdate callback was not called")
}
manager.onCancel()
if !cancelCalled {
t.Error("onCancel callback was not called")
}
}
// Benchmark tests for performance
func BenchmarkUpdateStatus(b *testing.B) {
manager := NewManager()
manager.createUIComponents()
b.ResetTimer()
for i := 0; i < b.N; i++ {
manager.UpdateStatus(StatusDownloading, "下载中...")
}
}
func BenchmarkShowProgress(b *testing.B) {
manager := NewManager()
manager.createUIComponents()
b.ResetTimer()
for i := 0; i < b.N; i++ {
manager.ShowProgress(float64(i % 100))
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

View File

@@ -0,0 +1,474 @@
package install
import (
"archive/zip"
"encoding/json"
"fmt"
"io"
"os"
"path/filepath"
"strings"
"syscall"
)
// ChangesInfo 表示 changes.json 文件的结构
type ChangesInfo struct {
Deleted []string `json:"deleted"`
Added []string `json:"added"`
Modified []string `json:"modified"`
}
// InstallManager 定义安装操作的接口契约
type InstallManager interface {
ExtractZip(zipPath, destPath string) error
ProcessChanges(changesPath string) (*ChangesInfo, error)
ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error
HandleRunningProcess(processName string) error
CreateTempDir() (string, error)
CleanupTempDir(tempDir string) error
}
// Manager 实现 InstallManager 接口
type Manager struct {
tempDirs []string // 跟踪临时目录以便清理
}
// NewManager 创建新的安装管理器实例
func NewManager() *Manager {
return &Manager{
tempDirs: make([]string, 0),
}
}
// CreateTempDir 为解压创建临时目录
func (m *Manager) CreateTempDir() (string, error) {
tempDir, err := os.MkdirTemp("", "updater_*")
if err != nil {
return "", fmt.Errorf("创建临时目录失败: %w", err)
}
// 跟踪临时目录以便清理
m.tempDirs = append(m.tempDirs, tempDir)
return tempDir, nil
}
// CleanupTempDir 删除临时目录及其内容
func (m *Manager) CleanupTempDir(tempDir string) error {
if tempDir == "" {
return nil
}
err := os.RemoveAll(tempDir)
if err != nil {
return fmt.Errorf("清理临时目录 %s 失败: %w", tempDir, err)
}
// 从跟踪列表中删除
for i, dir := range m.tempDirs {
if dir == tempDir {
m.tempDirs = append(m.tempDirs[:i], m.tempDirs[i+1:]...)
break
}
}
return nil
}
// CleanupAllTempDirs 删除所有跟踪的临时目录
func (m *Manager) CleanupAllTempDirs() error {
var errors []string
for _, tempDir := range m.tempDirs {
if err := os.RemoveAll(tempDir); err != nil {
errors = append(errors, fmt.Sprintf("清理 %s 失败: %v", tempDir, err))
}
}
m.tempDirs = m.tempDirs[:0] // 清空切片
if len(errors) > 0 {
return fmt.Errorf("清理错误: %s", strings.Join(errors, "; "))
}
return nil
}
// ExtractZip 将 ZIP 文件解压到指定的目标目录
func (m *Manager) ExtractZip(zipPath, destPath string) error {
// 打开 ZIP 文件进行读取
reader, err := zip.OpenReader(zipPath)
if err != nil {
return fmt.Errorf("打开 ZIP 文件 %s 失败: %w", zipPath, err)
}
defer reader.Close()
// 如果目标目录不存在则创建
if err := os.MkdirAll(destPath, 0755); err != nil {
return fmt.Errorf("创建目标目录 %s 失败: %w", destPath, err)
}
// 解压文件
for _, file := range reader.File {
if err := m.extractFile(file, destPath); err != nil {
return fmt.Errorf("解压文件 %s 失败: %w", file.Name, err)
}
}
return nil
}
// extractFile 从 ZIP 归档中解压单个文件
func (m *Manager) extractFile(file *zip.File, destPath string) error {
// 清理文件路径以防止目录遍历攻击
cleanPath := filepath.Clean(file.Name)
if strings.Contains(cleanPath, "..") {
return fmt.Errorf("无效的文件路径: %s", file.Name)
}
// 创建完整的目标路径
destFile := filepath.Join(destPath, cleanPath)
// 如果需要则创建目录结构
if file.FileInfo().IsDir() {
return os.MkdirAll(destFile, file.FileInfo().Mode())
}
// 创建父目录
if err := os.MkdirAll(filepath.Dir(destFile), 0755); err != nil {
return fmt.Errorf("创建父目录失败: %w", err)
}
// 打开 ZIP 归档中的文件
rc, err := file.Open()
if err != nil {
return fmt.Errorf("打开归档中的文件失败: %w", err)
}
defer rc.Close()
// 创建目标文件
outFile, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer outFile.Close()
// 复制文件内容
_, err = io.Copy(outFile, rc)
if err != nil {
return fmt.Errorf("复制文件内容失败: %w", err)
}
return nil
}
// ProcessChanges 读取并解析 changes.json 文件
func (m *Manager) ProcessChanges(changesPath string) (*ChangesInfo, error) {
// 检查 changes.json 是否存在
if _, err := os.Stat(changesPath); os.IsNotExist(err) {
// 如果 changes.json 不存在,返回空的变更信息
return &ChangesInfo{
Deleted: []string{},
Added: []string{},
Modified: []string{},
}, nil
}
// 读取 changes.json 文件
data, err := os.ReadFile(changesPath)
if err != nil {
return nil, fmt.Errorf("读取变更文件 %s 失败: %w", changesPath, err)
}
// 解析 JSON
var changes ChangesInfo
if err := json.Unmarshal(data, &changes); err != nil {
return nil, fmt.Errorf("解析变更 JSON 失败: %w", err)
}
return &changes, nil
}
// HandleRunningProcess 通过重命名正在使用的文件来处理正在运行的进程
func (m *Manager) HandleRunningProcess(processName string) error {
// 获取当前可执行文件路径
exePath, err := os.Executable()
if err != nil {
return fmt.Errorf("获取可执行文件路径失败: %w", err)
}
exeDir := filepath.Dir(exePath)
targetFile := filepath.Join(exeDir, processName)
// 检查目标文件是否存在
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
// 文件不存在,无需处理
return nil
}
// 尝试重命名文件以指示应在下次启动时删除
oldFile := targetFile + ".old"
// 如果存在现有的 .old 文件则删除
if _, err := os.Stat(oldFile); err == nil {
if err := os.Remove(oldFile); err != nil {
return fmt.Errorf("删除现有旧文件 %s 失败: %w", oldFile, err)
}
}
// 将当前文件重命名为 .old
if err := os.Rename(targetFile, oldFile); err != nil {
// 如果重命名失败,进程可能正在运行
// 在 Windows 上,我们无法重命名正在运行的可执行文件
if isFileInUse(err) {
// 标记文件在下次重启时删除Windows 特定)
return m.markFileForDeletion(targetFile)
}
return fmt.Errorf("重命名正在运行的进程文件 %s 失败: %w", targetFile, err)
}
return nil
}
// isFileInUse 检查错误是否表示文件正在使用中
func isFileInUse(err error) bool {
if err == nil {
return false
}
// 检查 Windows 特定的"文件正在使用"错误
if pathErr, ok := err.(*os.PathError); ok {
if errno, ok := pathErr.Err.(syscall.Errno); ok {
// ERROR_SHARING_VIOLATION (32) 或 ERROR_ACCESS_DENIED (5)
return errno == syscall.Errno(32) || errno == syscall.Errno(5)
}
}
return strings.Contains(err.Error(), "being used by another process") ||
strings.Contains(err.Error(), "access is denied")
}
// markFileForDeletion 标记文件在下次系统重启时删除Windows 特定)
func (m *Manager) markFileForDeletion(filePath string) error {
// 这是 Windows 特定的实现
// 目前,我们将创建一个可由主应用程序处理的标记文件
markerFile := filePath + ".delete_on_restart"
// 创建标记文件
file, err := os.Create(markerFile)
if err != nil {
return fmt.Errorf("创建删除标记文件失败: %w", err)
}
defer file.Close()
// 将目标文件路径写入标记文件
_, err = file.WriteString(filePath)
if err != nil {
return fmt.Errorf("写入标记文件失败: %w", err)
}
return nil
}
// DeleteMarkedFiles 删除标记为删除的文件
func (m *Manager) DeleteMarkedFiles(directory string) error {
// 查找所有 .delete_on_restart 文件
pattern := filepath.Join(directory, "*.delete_on_restart")
matches, err := filepath.Glob(pattern)
if err != nil {
return fmt.Errorf("查找标记文件失败: %w", err)
}
var errors []string
for _, markerFile := range matches {
// 读取目标文件路径
data, err := os.ReadFile(markerFile)
if err != nil {
errors = append(errors, fmt.Sprintf("读取标记文件 %s 失败: %v", markerFile, err))
continue
}
targetFile := strings.TrimSpace(string(data))
// 尝试删除目标文件
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
errors = append(errors, fmt.Sprintf("删除标记文件 %s 失败: %v", targetFile, err))
}
// 删除标记文件
if err := os.Remove(markerFile); err != nil {
errors = append(errors, fmt.Sprintf("删除标记文件 %s 失败: %v", markerFile, err))
}
}
if len(errors) > 0 {
return fmt.Errorf("删除错误: %s", strings.Join(errors, "; "))
}
return nil
}
// ApplyUpdate 通过从源目录复制文件到目标目录来应用更新
func (m *Manager) ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error {
// 创建备份目录
backupDir, err := m.createBackupDir(targetPath)
if err != nil {
return fmt.Errorf("创建备份目录失败: %w", err)
}
// 在应用更新前备份现有文件
if err := m.backupFiles(targetPath, backupDir, changes); err != nil {
return fmt.Errorf("备份文件失败: %w", err)
}
// 应用更新
if err := m.applyUpdateFiles(sourcePath, targetPath, changes); err != nil {
// 失败时回滚
if rollbackErr := m.rollbackUpdate(targetPath, backupDir); rollbackErr != nil {
return fmt.Errorf("更新失败且回滚失败: 更新错误: %w, 回滚错误: %v", err, rollbackErr)
}
return fmt.Errorf("更新失败已回滚: %w", err)
}
// 成功更新后清理备份目录
if err := os.RemoveAll(backupDir); err != nil {
// 记录警告但不让更新失败
fmt.Printf("警告: 清理备份目录 %s 失败: %v\n", backupDir, err)
}
return nil
}
// createBackupDir 为更新创建备份目录
func (m *Manager) createBackupDir(targetPath string) (string, error) {
backupDir := filepath.Join(targetPath, ".backup_"+fmt.Sprintf("%d", os.Getpid()))
if err := os.MkdirAll(backupDir, 0755); err != nil {
return "", fmt.Errorf("创建备份目录失败: %w", err)
}
return backupDir, nil
}
// backupFiles 创建将被修改或删除的文件的备份
func (m *Manager) backupFiles(targetPath, backupDir string, changes *ChangesInfo) error {
// 备份将被修改的文件
for _, file := range changes.Modified {
srcFile := filepath.Join(targetPath, file)
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
continue // 文件不存在,跳过备份
}
backupFile := filepath.Join(backupDir, file)
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
return fmt.Errorf("备份修改文件 %s 失败: %w", file, err)
}
}
// 备份将被删除的文件
for _, file := range changes.Deleted {
srcFile := filepath.Join(targetPath, file)
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
continue // 文件不存在,跳过备份
}
backupFile := filepath.Join(backupDir, file)
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
return fmt.Errorf("备份删除文件 %s 失败: %w", file, err)
}
}
return nil
}
// applyUpdateFiles 应用实际的文件更改
func (m *Manager) applyUpdateFiles(sourcePath, targetPath string, changes *ChangesInfo) error {
// 删除标记为删除的文件
for _, file := range changes.Deleted {
targetFile := filepath.Join(targetPath, file)
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("删除文件 %s 失败: %w", file, err)
}
}
// 复制新文件和修改的文件
filesToCopy := append(changes.Added, changes.Modified...)
for _, file := range filesToCopy {
srcFile := filepath.Join(sourcePath, file)
targetFile := filepath.Join(targetPath, file)
// 检查源文件是否存在
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
continue // 源文件不存在,跳过
}
if err := m.copyFileWithDirs(srcFile, targetFile); err != nil {
return fmt.Errorf("复制文件 %s 失败: %w", file, err)
}
}
return nil
}
// copyFileWithDirs 复制文件并创建必要的目录
func (m *Manager) copyFileWithDirs(src, dst string) error {
// 创建父目录
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
return fmt.Errorf("创建父目录失败: %w", err)
}
// 打开源文件
srcFile, err := os.Open(src)
if err != nil {
return fmt.Errorf("打开源文件失败: %w", err)
}
defer srcFile.Close()
// 获取源文件信息
srcInfo, err := srcFile.Stat()
if err != nil {
return fmt.Errorf("获取源文件信息失败: %w", err)
}
// 创建目标文件
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
if err != nil {
return fmt.Errorf("创建目标文件失败: %w", err)
}
defer dstFile.Close()
// 复制文件内容
_, err = io.Copy(dstFile, srcFile)
if err != nil {
return fmt.Errorf("复制文件内容失败: %w", err)
}
return nil
}
// rollbackUpdate 在更新失败时从备份恢复文件
func (m *Manager) rollbackUpdate(targetPath, backupDir string) error {
// 遍历备份目录并恢复文件
return filepath.Walk(backupDir, func(backupFile string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() {
return nil // 跳过目录
}
// 计算相对路径
relPath, err := filepath.Rel(backupDir, backupFile)
if err != nil {
return fmt.Errorf("计算相对路径失败: %w", err)
}
// 将文件恢复到目标位置
targetFile := filepath.Join(targetPath, relPath)
if err := m.copyFileWithDirs(backupFile, targetFile); err != nil {
return fmt.Errorf("恢复文件 %s 失败: %w", relPath, err)
}
return nil
})
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,12 @@
package main
import (
"testing"
)
// 集成测试将在此处实现
// 此文件目前是占位符
func TestIntegrationPlaceholder(t *testing.T) {
t.Skip("集成测试尚未实现")
}

438
Go_Updater/logger/logger.go Normal file
View File

@@ -0,0 +1,438 @@
package logger
import (
"fmt"
"io"
"log"
"os"
"path/filepath"
"sync"
"time"
)
// LogLevel 日志级别
type LogLevel int
const (
DEBUG LogLevel = iota
INFO
WARN
ERROR
)
// String 返回日志级别的字符串表示
func (l LogLevel) String() string {
switch l {
case DEBUG:
return "DEBUG"
case INFO:
return "INFO"
case WARN:
return "WARN"
case ERROR:
return "ERROR"
default:
return "UNKNOWN"
}
}
// Logger 日志记录器接口
type Logger interface {
Debug(msg string, fields ...interface{})
Info(msg string, fields ...interface{})
Warn(msg string, fields ...interface{})
Error(msg string, fields ...interface{})
SetLevel(level LogLevel)
Close() error
}
// FileLogger 文件日志记录器
type FileLogger struct {
mu sync.RWMutex
file *os.File
logger *log.Logger
level LogLevel
maxSize int64 // 最大文件大小(字节)
maxBackups int // 最大备份文件数
logDir string // 日志目录
filename string // 日志文件名
currentSize int64 // 当前文件大小
}
// LoggerConfig 日志配置
type LoggerConfig struct {
Level LogLevel
MaxSize int64 // 最大文件大小字节默认10MB
MaxBackups int // 最大备份文件数默认5
LogDir string // 日志目录
Filename string // 日志文件名
}
// DefaultLoggerConfig 默认日志配置
func DefaultLoggerConfig() *LoggerConfig {
// 获取当前可执行文件目录
exePath, err := os.Executable()
var logDir string
if err != nil {
logDir = "debug"
} else {
exeDir := filepath.Dir(exePath)
logDir = filepath.Join(exeDir, "debug")
}
return &LoggerConfig{
Level: INFO,
MaxSize: 10 * 1024 * 1024, // 10MB
MaxBackups: 5,
LogDir: logDir,
Filename: "AUTO_MAA_Go_Updater.log",
}
}
// NewFileLogger 创建新的文件日志记录器
func NewFileLogger(config *LoggerConfig) (*FileLogger, error) {
if config == nil {
config = DefaultLoggerConfig()
}
// 创建日志目录
if err := os.MkdirAll(config.LogDir, 0755); err != nil {
return nil, fmt.Errorf("failed to create log directory: %w", err)
}
logPath := filepath.Join(config.LogDir, config.Filename)
// 打开或创建日志文件
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
return nil, fmt.Errorf("failed to open log file: %w", err)
}
// 获取当前文件大小
stat, err := file.Stat()
if err != nil {
file.Close()
return nil, fmt.Errorf("failed to get file stats: %w", err)
}
logger := &FileLogger{
file: file,
logger: log.New(file, "", 0), // 我们自己处理格式
level: config.Level,
maxSize: config.MaxSize,
maxBackups: config.MaxBackups,
logDir: config.LogDir,
filename: config.Filename,
currentSize: stat.Size(),
}
return logger, nil
}
// formatMessage 格式化日志消息
func (fl *FileLogger) formatMessage(level LogLevel, msg string, fields ...interface{}) string {
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
if len(fields) > 0 {
msg = fmt.Sprintf(msg, fields...)
}
return fmt.Sprintf("[%s] %s %s\n", timestamp, level.String(), msg)
}
// writeLog 写入日志
func (fl *FileLogger) writeLog(level LogLevel, msg string, fields ...interface{}) {
fl.mu.Lock()
defer fl.mu.Unlock()
// 检查日志级别
if level < fl.level {
return
}
formattedMsg := fl.formatMessage(level, msg, fields...)
// 检查是否需要轮转
if fl.currentSize+int64(len(formattedMsg)) > fl.maxSize {
if err := fl.rotate(); err != nil {
// 轮转失败尝试写入stderr
fmt.Fprintf(os.Stderr, "Failed to rotate log: %v\n", err)
}
}
// 写入日志
n, err := fl.file.WriteString(formattedMsg)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to write log: %v\n", err)
return
}
fl.currentSize += int64(n)
fl.file.Sync() // 确保写入磁盘
}
// rotate 轮转日志文件
func (fl *FileLogger) rotate() error {
// 关闭当前文件
if err := fl.file.Close(); err != nil {
return fmt.Errorf("failed to close current log file: %w", err)
}
// 轮转备份文件
if err := fl.rotateBackups(); err != nil {
return fmt.Errorf("failed to rotate backups: %w", err)
}
// 创建新的日志文件
logPath := filepath.Join(fl.logDir, fl.filename)
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return fmt.Errorf("failed to create new log file: %w", err)
}
fl.file = file
fl.logger.SetOutput(file)
fl.currentSize = 0
return nil
}
// rotateBackups 轮转备份文件
func (fl *FileLogger) rotateBackups() error {
basePath := filepath.Join(fl.logDir, fl.filename)
// 删除最老的备份文件
if fl.maxBackups > 0 {
oldestBackup := fmt.Sprintf("%s.%d", basePath, fl.maxBackups)
os.Remove(oldestBackup) // 忽略错误,文件可能不存在
}
// 重命名现有备份文件
for i := fl.maxBackups - 1; i > 0; i-- {
oldName := fmt.Sprintf("%s.%d", basePath, i)
newName := fmt.Sprintf("%s.%d", basePath, i+1)
os.Rename(oldName, newName) // 忽略错误,文件可能不存在
}
// 将当前日志文件重命名为第一个备份
if fl.maxBackups > 0 {
backupName := fmt.Sprintf("%s.1", basePath)
return os.Rename(basePath, backupName)
}
return nil
}
// Debug 记录调试级别日志
func (fl *FileLogger) Debug(msg string, fields ...interface{}) {
fl.writeLog(DEBUG, msg, fields...)
}
// Info 记录信息级别日志
func (fl *FileLogger) Info(msg string, fields ...interface{}) {
fl.writeLog(INFO, msg, fields...)
}
// Warn 记录警告级别日志
func (fl *FileLogger) Warn(msg string, fields ...interface{}) {
fl.writeLog(WARN, msg, fields...)
}
// Error 记录错误级别日志
func (fl *FileLogger) Error(msg string, fields ...interface{}) {
fl.writeLog(ERROR, msg, fields...)
}
// SetLevel 设置日志级别
func (fl *FileLogger) SetLevel(level LogLevel) {
fl.mu.Lock()
defer fl.mu.Unlock()
fl.level = level
}
// Close 关闭日志记录器
func (fl *FileLogger) Close() error {
fl.mu.Lock()
defer fl.mu.Unlock()
if fl.file != nil {
return fl.file.Close()
}
return nil
}
// MultiLogger 多输出日志记录器
type MultiLogger struct {
loggers []Logger
level LogLevel
}
// NewMultiLogger 创建多输出日志记录器
func NewMultiLogger(loggers ...Logger) *MultiLogger {
return &MultiLogger{
loggers: loggers,
level: INFO,
}
}
// Debug 记录调试级别日志
func (ml *MultiLogger) Debug(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Debug(msg, fields...)
}
}
// Info 记录信息级别日志
func (ml *MultiLogger) Info(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Info(msg, fields...)
}
}
// Warn 记录警告级别日志
func (ml *MultiLogger) Warn(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Warn(msg, fields...)
}
}
// Error 记录错误级别日志
func (ml *MultiLogger) Error(msg string, fields ...interface{}) {
for _, logger := range ml.loggers {
logger.Error(msg, fields...)
}
}
// SetLevel 设置日志级别
func (ml *MultiLogger) SetLevel(level LogLevel) {
ml.level = level
for _, logger := range ml.loggers {
logger.SetLevel(level)
}
}
// Close 关闭所有日志记录器
func (ml *MultiLogger) Close() error {
var lastErr error
for _, logger := range ml.loggers {
if err := logger.Close(); err != nil {
lastErr = err
}
}
return lastErr
}
// ConsoleLogger 控制台日志记录器
type ConsoleLogger struct {
writer io.Writer
level LogLevel
}
// NewConsoleLogger 创建控制台日志记录器
func NewConsoleLogger(writer io.Writer) *ConsoleLogger {
if writer == nil {
writer = os.Stdout
}
return &ConsoleLogger{
writer: writer,
level: INFO,
}
}
// formatMessage 格式化控制台日志消息
func (cl *ConsoleLogger) formatMessage(level LogLevel, msg string, fields ...interface{}) string {
timestamp := time.Now().Format("15:04:05")
if len(fields) > 0 {
msg = fmt.Sprintf(msg, fields...)
}
return fmt.Sprintf("[%s] %s %s\n", timestamp, level.String(), msg)
}
// writeLog 写入控制台日志
func (cl *ConsoleLogger) writeLog(level LogLevel, msg string, fields ...interface{}) {
if level < cl.level {
return
}
formattedMsg := cl.formatMessage(level, msg, fields...)
fmt.Fprint(cl.writer, formattedMsg)
}
// Debug 记录调试级别日志
func (cl *ConsoleLogger) Debug(msg string, fields ...interface{}) {
cl.writeLog(DEBUG, msg, fields...)
}
// Info 记录信息级别日志
func (cl *ConsoleLogger) Info(msg string, fields ...interface{}) {
cl.writeLog(INFO, msg, fields...)
}
// Warn 记录警告级别日志
func (cl *ConsoleLogger) Warn(msg string, fields ...interface{}) {
cl.writeLog(WARN, msg, fields...)
}
// Error 记录错误级别日志
func (cl *ConsoleLogger) Error(msg string, fields ...interface{}) {
cl.writeLog(ERROR, msg, fields...)
}
// SetLevel 设置日志级别
func (cl *ConsoleLogger) SetLevel(level LogLevel) {
cl.level = level
}
// Close 关闭控制台日志记录器(无操作)
func (cl *ConsoleLogger) Close() error {
return nil
}
// 全局日志记录器实例
var (
defaultLogger Logger
once sync.Once
)
// GetDefaultLogger 获取默认日志记录器
func GetDefaultLogger() Logger {
once.Do(func() {
fileLogger, err := NewFileLogger(DefaultLoggerConfig())
if err != nil {
// 如果文件日志创建失败,使用控制台日志
defaultLogger = NewConsoleLogger(os.Stderr)
} else {
// 同时输出到文件和控制台
consoleLogger := NewConsoleLogger(os.Stdout)
defaultLogger = NewMultiLogger(fileLogger, consoleLogger)
}
})
return defaultLogger
}
// 便捷函数
func Debug(msg string, fields ...interface{}) {
GetDefaultLogger().Debug(msg, fields...)
}
func Info(msg string, fields ...interface{}) {
GetDefaultLogger().Info(msg, fields...)
}
func Warn(msg string, fields ...interface{}) {
GetDefaultLogger().Warn(msg, fields...)
}
func Error(msg string, fields ...interface{}) {
GetDefaultLogger().Error(msg, fields...)
}
func SetLevel(level LogLevel) {
GetDefaultLogger().SetLevel(level)
}
func Close() error {
return GetDefaultLogger().Close()
}

View File

@@ -0,0 +1,300 @@
package logger
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
)
func TestLogLevel_String(t *testing.T) {
tests := []struct {
level LogLevel
expected string
}{
{DEBUG, "DEBUG"},
{INFO, "INFO"},
{WARN, "WARN"},
{ERROR, "ERROR"},
{LogLevel(999), "UNKNOWN"},
}
for _, tt := range tests {
t.Run(tt.expected, func(t *testing.T) {
if got := tt.level.String(); got != tt.expected {
t.Errorf("LogLevel.String() = %v, want %v", got, tt.expected)
}
})
}
}
func TestDefaultLoggerConfig(t *testing.T) {
config := DefaultLoggerConfig()
if config.Level != INFO {
t.Errorf("Expected default level INFO, got %v", config.Level)
}
if config.MaxSize != 10*1024*1024 {
t.Errorf("Expected default max size 10MB, got %v", config.MaxSize)
}
if config.MaxBackups != 5 {
t.Errorf("Expected default max backups 5, got %v", config.MaxBackups)
}
if config.Filename != "updater.log" {
t.Errorf("Expected default filename 'updater.log', got %v", config.Filename)
}
}
func TestConsoleLogger(t *testing.T) {
var buf bytes.Buffer
logger := NewConsoleLogger(&buf)
t.Run("log levels", func(t *testing.T) {
logger.SetLevel(DEBUG)
logger.Debug("debug message")
logger.Info("info message")
logger.Warn("warn message")
logger.Error("error message")
output := buf.String()
if !strings.Contains(output, "DEBUG debug message") {
t.Error("Expected debug message in output")
}
if !strings.Contains(output, "INFO info message") {
t.Error("Expected info message in output")
}
if !strings.Contains(output, "WARN warn message") {
t.Error("Expected warn message in output")
}
if !strings.Contains(output, "ERROR error message") {
t.Error("Expected error message in output")
}
})
t.Run("log level filtering", func(t *testing.T) {
buf.Reset()
logger.SetLevel(WARN)
logger.Debug("debug message")
logger.Info("info message")
logger.Warn("warn message")
logger.Error("error message")
output := buf.String()
if strings.Contains(output, "DEBUG") {
t.Error("Debug message should be filtered out")
}
if strings.Contains(output, "INFO") {
t.Error("Info message should be filtered out")
}
if !strings.Contains(output, "WARN warn message") {
t.Error("Expected warn message in output")
}
if !strings.Contains(output, "ERROR error message") {
t.Error("Expected error message in output")
}
})
t.Run("formatted messages", func(t *testing.T) {
buf.Reset()
logger.SetLevel(DEBUG)
logger.Info("formatted message: %s %d", "test", 42)
output := buf.String()
if !strings.Contains(output, "formatted message: test 42") {
t.Error("Expected formatted message in output")
}
})
}
func TestFileLogger(t *testing.T) {
// 创建临时目录
tempDir := t.TempDir()
config := &LoggerConfig{
Level: DEBUG,
MaxSize: 1024, // 1KB for testing rotation
MaxBackups: 3,
LogDir: tempDir,
Filename: "test.log",
}
logger, err := NewFileLogger(config)
if err != nil {
t.Fatalf("Failed to create file logger: %v", err)
}
defer logger.Close()
t.Run("basic logging", func(t *testing.T) {
logger.Info("test message")
logger.Error("error message with %s", "formatting")
// 读取日志文件
logPath := filepath.Join(tempDir, "test.log")
content, err := os.ReadFile(logPath)
if err != nil {
t.Fatalf("Failed to read log file: %v", err)
}
output := string(content)
if !strings.Contains(output, "INFO test message") {
t.Error("Expected info message in log file")
}
if !strings.Contains(output, "ERROR error message with formatting") {
t.Error("Expected formatted error message in log file")
}
})
t.Run("log rotation", func(t *testing.T) {
// 写入大量数据触发轮转
longMessage := strings.Repeat("a", 200)
for i := 0; i < 10; i++ {
logger.Info("Long message %d: %s", i, longMessage)
}
// 检查是否创建了备份文件
logPath := filepath.Join(tempDir, "test.log")
backupPath := filepath.Join(tempDir, "test.log.1")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
t.Error("Main log file should exist")
}
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
t.Error("Backup log file should exist after rotation")
}
})
}
func TestMultiLogger(t *testing.T) {
var buf1, buf2 bytes.Buffer
logger1 := NewConsoleLogger(&buf1)
logger2 := NewConsoleLogger(&buf2)
multiLogger := NewMultiLogger(logger1, logger2)
multiLogger.SetLevel(INFO)
multiLogger.Info("test message")
multiLogger.Error("error message")
// 检查两个logger都收到了消息
output1 := buf1.String()
output2 := buf2.String()
if !strings.Contains(output1, "INFO test message") {
t.Error("Expected info message in first logger")
}
if !strings.Contains(output1, "ERROR error message") {
t.Error("Expected error message in first logger")
}
if !strings.Contains(output2, "INFO test message") {
t.Error("Expected info message in second logger")
}
if !strings.Contains(output2, "ERROR error message") {
t.Error("Expected error message in second logger")
}
}
func TestFileLoggerRotation(t *testing.T) {
tempDir := t.TempDir()
config := &LoggerConfig{
Level: DEBUG,
MaxSize: 100, // Very small for testing
MaxBackups: 2,
LogDir: tempDir,
Filename: "rotation_test.log",
}
logger, err := NewFileLogger(config)
if err != nil {
t.Fatalf("Failed to create file logger: %v", err)
}
defer logger.Close()
// 写入足够的数据触发多次轮转
for i := 0; i < 20; i++ {
logger.Info("Message %d: %s", i, strings.Repeat("x", 50))
}
// 检查文件存在性
logPath := filepath.Join(tempDir, "rotation_test.log")
backup1Path := filepath.Join(tempDir, "rotation_test.log.1")
backup2Path := filepath.Join(tempDir, "rotation_test.log.2")
backup3Path := filepath.Join(tempDir, "rotation_test.log.3")
if _, err := os.Stat(logPath); os.IsNotExist(err) {
t.Error("Main log file should exist")
}
if _, err := os.Stat(backup1Path); os.IsNotExist(err) {
t.Error("First backup should exist")
}
if _, err := os.Stat(backup2Path); os.IsNotExist(err) {
t.Error("Second backup should exist")
}
// 第三个备份不应该存在MaxBackups=2
if _, err := os.Stat(backup3Path); !os.IsNotExist(err) {
t.Error("Third backup should not exist (exceeds MaxBackups)")
}
}
func TestGlobalLoggerFunctions(t *testing.T) {
// 这个测试比较简单主要确保全局函数不会panic
Debug("debug message")
Info("info message")
Warn("warn message")
Error("error message")
SetLevel(ERROR)
// 这些调用不应该panic
Debug("filtered debug")
Info("filtered info")
Error("visible error")
}
func TestFileLoggerErrorHandling(t *testing.T) {
t.Run("invalid directory", func(t *testing.T) {
// 使用一个真正无效的路径
config := &LoggerConfig{
Level: INFO,
MaxSize: 1024,
MaxBackups: 3,
LogDir: string([]byte{0}), // 无效的路径字符
Filename: "test.log",
}
_, err := NewFileLogger(config)
if err == nil {
t.Error("Expected error when creating logger with invalid directory")
}
})
}
func TestLoggerFormatting(t *testing.T) {
var buf bytes.Buffer
logger := NewConsoleLogger(&buf)
logger.SetLevel(DEBUG)
// 测试时间戳格式
logger.Info("test message")
output := buf.String()
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) == 0 {
t.Fatal("Expected at least one log line")
}
// 检查格式:[HH:MM:SS] LEVEL message
line := lines[0]
if !strings.Contains(line, "INFO test message") {
t.Errorf("Expected 'INFO test message' in output, got: %s", line)
}
// 检查时间戳格式(简单检查)
if !strings.HasPrefix(line, "[") {
t.Error("Expected log line to start with timestamp in brackets")
}
}

1049
Go_Updater/main.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,189 @@
package version
import (
"encoding/json"
"fmt"
"os"
"path/filepath"
"strconv"
"strings"
"AUTO_MAA_Go_Updater/logger"
)
// VersionInfo 表示来自 version.json 的版本信息
type VersionInfo struct {
MainVersion string `json:"main_version"`
VersionInfo map[string]map[string][]string `json:"version_info"`
}
// ParsedVersion 表示解析后的版本,包含主版本号、次版本号、补丁版本号和测试版本号组件
type ParsedVersion struct {
Major int
Minor int
Patch int
Beta int
}
// VersionManager 处理版本相关操作
type VersionManager struct {
executableDir string
logger logger.Logger
}
// NewVersionManager 创建新的版本管理器
func NewVersionManager() *VersionManager {
execPath, _ := os.Executable()
execDir := filepath.Dir(execPath)
return &VersionManager{
executableDir: execDir,
logger: logger.GetDefaultLogger(),
}
}
// createDefaultVersion 创建默认版本结构 v0.0.0
func (vm *VersionManager) createDefaultVersion() *VersionInfo {
return &VersionInfo{
MainVersion: "0.0.0.0", // 对应 v0.0.0
VersionInfo: make(map[string]map[string][]string),
}
}
// LoadVersionFromFile 从 resources/version.json 加载版本信息并处理回退
func (vm *VersionManager) LoadVersionFromFile() (*VersionInfo, error) {
versionPath := filepath.Join(vm.executableDir, "resources", "version.json")
data, err := os.ReadFile(versionPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Println("未读取到版本信息,使用默认版本进行更新。")
return vm.createDefaultVersion(), nil
}
vm.logger.Warn("读取版本文件 %s 失败: %v将使用默认版本", versionPath, err)
return vm.createDefaultVersion(), nil
}
var versionInfo VersionInfo
if err := json.Unmarshal(data, &versionInfo); err != nil {
vm.logger.Warn("解析版本文件 %s 失败: %v将使用默认版本", versionPath, err)
return vm.createDefaultVersion(), nil
}
vm.logger.Debug("成功从 %s 加载版本信息", versionPath)
return &versionInfo, nil
}
// LoadVersionWithDefault 加载版本信息并保证回退到默认版本
func (vm *VersionManager) LoadVersionWithDefault() *VersionInfo {
versionInfo, err := vm.LoadVersionFromFile()
if err != nil {
// 这在更新的 LoadVersionFromFile 中不应该发生,但添加作为额外安全措施
vm.logger.Error("加载版本文件时出现意外错误: %v使用默认版本", err)
return vm.createDefaultVersion()
}
// 验证我们有一个有效的版本结构
if versionInfo == nil {
vm.logger.Warn("版本信息为空,使用默认版本")
return vm.createDefaultVersion()
}
if versionInfo.MainVersion == "" {
vm.logger.Warn("版本信息主版本为空,使用默认版本")
return vm.createDefaultVersion()
}
if versionInfo.VersionInfo == nil {
vm.logger.Debug("版本信息映射为空,初始化空映射")
versionInfo.VersionInfo = make(map[string]map[string][]string)
}
return versionInfo
}
// ParseVersion 解析版本字符串如 "4.4.1.3" 为组件
func ParseVersion(versionStr string) (*ParsedVersion, error) {
parts := strings.Split(versionStr, ".")
if len(parts) < 3 || len(parts) > 4 {
return nil, fmt.Errorf("无效的版本格式: %s", versionStr)
}
major, err := strconv.Atoi(parts[0])
if err != nil {
return nil, fmt.Errorf("无效的主版本号: %s", parts[0])
}
minor, err := strconv.Atoi(parts[1])
if err != nil {
return nil, fmt.Errorf("无效的次版本号: %s", parts[1])
}
patch, err := strconv.Atoi(parts[2])
if err != nil {
return nil, fmt.Errorf("无效的补丁版本号: %s", parts[2])
}
beta := 0
if len(parts) == 4 {
beta, err = strconv.Atoi(parts[3])
if err != nil {
return nil, fmt.Errorf("无效的测试版本号: %s", parts[3])
}
}
return &ParsedVersion{
Major: major,
Minor: minor,
Patch: patch,
Beta: beta,
}, nil
}
// ToVersionString 将 ParsedVersion 转换回版本字符串格式
func (pv *ParsedVersion) ToVersionString() string {
if pv.Beta == 0 {
return fmt.Sprintf("%d.%d.%d.0", pv.Major, pv.Minor, pv.Patch)
}
return fmt.Sprintf("%d.%d.%d.%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
}
// ToDisplayVersion 将版本转换为显示格式 (4.4.0 或 4.4.1-beta3)
func (pv *ParsedVersion) ToDisplayVersion() string {
if pv.Beta == 0 {
return fmt.Sprintf("%d.%d.%d", pv.Major, pv.Minor, pv.Patch)
}
return fmt.Sprintf("%d.%d.%d-beta%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
}
// GetChannel 根据版本返回渠道 (stable 或 beta)
func (pv *ParsedVersion) GetChannel() string {
if pv.Beta == 0 {
return "stable"
}
return "beta"
}
// IsNewer 检查此版本是否比其他版本更新
func (pv *ParsedVersion) IsNewer(other *ParsedVersion) bool {
if pv.Major != other.Major {
return pv.Major > other.Major
}
if pv.Minor != other.Minor {
return pv.Minor > other.Minor
}
if pv.Patch != other.Patch {
return pv.Patch > other.Patch
}
// 对于相同的主版本号正式版本Beta=0比beta版本Beta>0更新
// 例如4.4.1.0(正式版)> 4.4.1.3beta3
if pv.Beta == 0 && other.Beta > 0 {
return true // 正式版比beta版更新
}
if pv.Beta > 0 && other.Beta == 0 {
return false // beta版比正式版旧
}
// 如果都是beta版本比较beta版本号
return pv.Beta > other.Beta
}

View File

@@ -0,0 +1,366 @@
package version
import (
"encoding/json"
"os"
"path/filepath"
"testing"
)
func TestParseVersion(t *testing.T) {
tests := []struct {
input string
expected *ParsedVersion
hasError bool
}{
{"4.4.0.0", &ParsedVersion{4, 4, 0, 0}, false},
{"4.4.1.3", &ParsedVersion{4, 4, 1, 3}, false},
{"1.2.3", &ParsedVersion{1, 2, 3, 0}, false},
{"invalid", nil, true},
{"1.2", nil, true},
{"1.2.3.4.5", nil, true},
}
for _, test := range tests {
result, err := ParseVersion(test.input)
if test.hasError {
if err == nil {
t.Errorf("Expected error for input %s, but got none", test.input)
}
continue
}
if err != nil {
t.Errorf("Unexpected error for input %s: %v", test.input, err)
continue
}
if result.Major != test.expected.Major ||
result.Minor != test.expected.Minor ||
result.Patch != test.expected.Patch ||
result.Beta != test.expected.Beta {
t.Errorf("For input %s, expected %+v, got %+v", test.input, test.expected, result)
}
}
}
func TestToDisplayVersion(t *testing.T) {
tests := []struct {
version *ParsedVersion
expected string
}{
{&ParsedVersion{4, 4, 0, 0}, "v4.4.0"},
{&ParsedVersion{4, 4, 1, 3}, "v4.4.1-beta3"},
{&ParsedVersion{1, 2, 3, 0}, "v1.2.3"},
{&ParsedVersion{1, 2, 3, 5}, "v1.2.3-beta5"},
}
for _, test := range tests {
result := test.version.ToDisplayVersion()
if result != test.expected {
t.Errorf("For version %+v, expected %s, got %s", test.version, test.expected, result)
}
}
}
func TestGetChannel(t *testing.T) {
tests := []struct {
version *ParsedVersion
expected string
}{
{&ParsedVersion{4, 4, 0, 0}, "stable"},
{&ParsedVersion{4, 4, 1, 3}, "beta"},
{&ParsedVersion{1, 2, 3, 0}, "stable"},
{&ParsedVersion{1, 2, 3, 1}, "beta"},
}
for _, test := range tests {
result := test.version.GetChannel()
if result != test.expected {
t.Errorf("For version %+v, expected channel %s, got %s", test.version, test.expected, result)
}
}
}
func TestIsNewer(t *testing.T) {
tests := []struct {
v1 *ParsedVersion
v2 *ParsedVersion
expected bool
}{
{&ParsedVersion{4, 4, 1, 0}, &ParsedVersion{4, 4, 0, 0}, true},
{&ParsedVersion{4, 4, 0, 0}, &ParsedVersion{4, 4, 1, 0}, false},
{&ParsedVersion{4, 4, 1, 3}, &ParsedVersion{4, 4, 1, 2}, true},
{&ParsedVersion{4, 4, 1, 2}, &ParsedVersion{4, 4, 1, 3}, false},
{&ParsedVersion{4, 4, 1, 0}, &ParsedVersion{4, 4, 1, 0}, false},
}
for _, test := range tests {
result := test.v1.IsNewer(test.v2)
if result != test.expected {
t.Errorf("For %+v.IsNewer(%+v), expected %t, got %t", test.v1, test.v2, test.expected, result)
}
}
}
func TestLoadVersionFromFile(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create test version file
versionData := VersionInfo{
MainVersion: "4.4.1.3",
VersionInfo: map[string]map[string][]string{
"4.4.1.3": {
"修复BUG": {"移除崩溃弹窗机制"},
},
},
}
data, err := json.Marshal(versionData)
if err != nil {
t.Fatal(err)
}
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, data, 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory and logger
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version
result, err := vm.LoadVersionFromFile()
if err != nil {
t.Fatalf("Failed to load version: %v", err)
}
if result.MainVersion != "4.4.1.3" {
t.Errorf("Expected main version 4.4.1.3, got %s", result.MainVersion)
}
if len(result.VersionInfo) != 1 {
t.Errorf("Expected 1 version info entry, got %d", len(result.VersionInfo))
}
}
func TestLoadVersionFromFileNotFound(t *testing.T) {
// Create a temporary directory without version file
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create version manager with custom executable directory and logger
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version (should now return default version instead of error)
result, err := vm.LoadVersionFromFile()
if err != nil {
t.Errorf("Expected no error with fallback mechanism, but got: %v", err)
}
// Should return default version
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map, got nil")
}
}
func TestLoadVersionWithDefault(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version with default (no file exists)
result := vm.LoadVersionWithDefault()
if result == nil {
t.Fatal("Expected non-nil result from LoadVersionWithDefault")
}
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map, got nil")
}
}
func TestLoadVersionWithDefaultValidFile(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create test version file
versionData := VersionInfo{
MainVersion: "4.4.1.3",
VersionInfo: map[string]map[string][]string{
"4.4.1.3": {
"修复BUG": {"移除崩溃弹窗机制"},
},
},
}
data, err := json.Marshal(versionData)
if err != nil {
t.Fatal(err)
}
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, data, 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version with default (valid file exists)
result := vm.LoadVersionWithDefault()
if result == nil {
t.Fatal("Expected non-nil result from LoadVersionWithDefault")
}
if result.MainVersion != "4.4.1.3" {
t.Errorf("Expected version 4.4.1.3, got %s", result.MainVersion)
}
if len(result.VersionInfo) != 1 {
t.Errorf("Expected 1 version info entry, got %d", len(result.VersionInfo))
}
}
func TestLoadVersionFromFileCorrupted(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create corrupted version file
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, []byte("invalid json content"), 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version (should return default version for corrupted file)
result, err := vm.LoadVersionFromFile()
if err != nil {
t.Errorf("Expected no error with fallback mechanism for corrupted file, but got: %v", err)
}
// Should return default version
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0 for corrupted file, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map for corrupted file, got nil")
}
}
func TestLoadVersionWithDefaultCorrupted(t *testing.T) {
// Create a temporary directory
tempDir, err := os.MkdirTemp("", "version_test")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Create resources directory
resourcesDir := filepath.Join(tempDir, "resources")
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
t.Fatal(err)
}
// Create corrupted version file
versionFile := filepath.Join(resourcesDir, "version.json")
if err := os.WriteFile(versionFile, []byte("invalid json content"), 0644); err != nil {
t.Fatal(err)
}
// Create version manager with custom executable directory
vm := NewVersionManager()
vm.executableDir = tempDir
// Test loading version with default (corrupted file)
result := vm.LoadVersionWithDefault()
if result == nil {
t.Fatal("Expected non-nil result from LoadVersionWithDefault for corrupted file")
}
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0 for corrupted file, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map for corrupted file, got nil")
}
}
func TestCreateDefaultVersion(t *testing.T) {
vm := NewVersionManager()
result := vm.createDefaultVersion()
if result == nil {
t.Fatal("Expected non-nil result from createDefaultVersion")
}
if result.MainVersion != "0.0.0.0" {
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
}
if result.VersionInfo == nil {
t.Error("Expected initialized VersionInfo map, got nil")
}
if len(result.VersionInfo) != 0 {
t.Errorf("Expected empty VersionInfo map, got %d entries", len(result.VersionInfo))
}
}

View File

@@ -0,0 +1,19 @@
package version
import (
"runtime"
)
var (
// Version 应用程序的当前版本
Version = "1.0.0"
// BuildTime 在构建时设置
BuildTime = "unknown"
// GitCommit 在构建时设置
GitCommit = "unknown"
// GoVersion 用于构建的 Go 版本
GoVersion = runtime.Version()
)

View File

@@ -38,6 +38,7 @@ from .config import (
GeneralSubConfig,
Config,
)
from .logger import logger
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
@@ -52,6 +53,7 @@ __all__ = [
"MaaPlanConfig",
"GeneralConfig",
"GeneralSubConfig",
"logger",
"MainInfoBar",
"Network",
"SoundPlayer",

File diff suppressed because it is too large Load Diff

34
app/core/logger.py Normal file
View File

@@ -0,0 +1,34 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
# This file is part of AUTO_MAA.
# AUTO_MAA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
# AUTO_MAA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
# the GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
# Contact: DLmaster_361@163.com
"""
AUTO_MAA
AUTO_MAA日志组件
v4.4
作者DLmaster_361
"""
from loguru import logger as _logger
# 设置日志 module 字段默认值
logger = _logger.patch(
lambda record: record["extra"].setdefault("module", "未知模块") or True
)
logger.remove(0)

View File

@@ -25,10 +25,10 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import Qt
from qfluentwidgets import InfoBar, InfoBarPosition
from .logger import logger
from .config import Config
from .sound_player import SoundPlayer
@@ -46,20 +46,34 @@ class _MainInfoBar:
def push_info_bar(
self, mode: str, title: str, content: str, time: int, if_force: bool = False
):
"""推送到信息通知栏"""
) -> None:
"""
推送消息到吐司通知栏
:param mode: 通知栏模式,支持 "success", "warning", "error", "info"
:param title: 通知栏标题
:type title: str
:param content: 通知栏内容
:type content: str
:param time: 显示时长,单位为毫秒
:type time: int
:param if_force: 是否强制推送
:type if_force: bool
"""
if Config.main_window is None:
logger.error("信息通知栏未设置父窗口")
logger.error("信息通知栏未设置父窗口", module="吐司通知栏")
return None
# 根据 mode 获取对应的 InfoBar 方法
info_bar_method = self.mode_mapping.get(mode)
if not info_bar_method:
logger.error(f"未知的通知栏模式: {mode}")
logger.error(f"未知的通知栏模式: {mode}", module="吐司通知栏")
return None
if Config.main_window.isVisible():
# 主窗口可见时直接推送通知
info_bar_method(
title=title,
content=content,
@@ -69,6 +83,7 @@ class _MainInfoBar:
duration=time,
parent=Config.main_window,
)
elif if_force:
# 如果主窗口不可见且强制推送,则录入消息队列等待窗口显示后推送
info_bar_item = {
@@ -80,6 +95,11 @@ class _MainInfoBar:
if info_bar_item not in Config.info_bar_list:
Config.info_bar_list.append(info_bar_item)
logger.info(
f"主窗口不可见,已将通知栏消息录入队列: {info_bar_item}",
module="吐司通知栏",
)
if mode == "warning":
SoundPlayer.play("发生异常")
if mode == "error":

View File

@@ -25,13 +25,15 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, QThread, QEventLoop
import re
import time
import requests
import truststore
from pathlib import Path
from typing import Dict
from .logger import logger
class NetworkThread(QThread):
@@ -41,16 +43,27 @@ class NetworkThread(QThread):
timeout = 10
backoff_factor = 0.1
def __init__(self, mode: str, url: str, path: Path = None) -> None:
def __init__(
self,
mode: str,
url: str,
path: Path = None,
files: Dict = None,
data: Dict = None,
) -> None:
super().__init__()
self.setObjectName(
f"NetworkThread-{mode}-{re.sub(r'(&cdk=)[^&]+(&)', r'\1******\2', url)}"
)
logger.info(f"创建网络请求线程: {self.objectName()}", module="网络请求子线程")
self.mode = mode
self.url = url
self.path = path
self.files = files
self.data = data
from .config import Config
@@ -65,7 +78,7 @@ class NetworkThread(QThread):
self.loop = QEventLoop()
truststore.inject_into_ssl()
truststore.inject_into_ssl() # 信任系统证书
@logger.catch
def run(self) -> None:
@@ -75,9 +88,17 @@ class NetworkThread(QThread):
self.get_json(self.url)
elif self.mode == "get_file":
self.get_file(self.url, self.path)
elif self.mode == "upload_file":
self.upload_file(self.url, self.files, self.data)
def get_json(self, url: str) -> None:
"""通过get方法获取json数据"""
"""
通过get方法获取json数据
:param url: 请求的URL
"""
logger.info(f"子线程 {self.objectName()} 开始网络请求", module="网络请求子线程")
response = None
@@ -92,44 +113,123 @@ class NetworkThread(QThread):
self.status_code = response.status_code if response else None
self.response_json = None
self.error_message = str(e)
logger.exception(
f"子线程 {self.objectName()} 网络请求失败:{e},第{_+1}次尝试",
module="网络请求子线程",
)
time.sleep(self.backoff_factor)
self.loop.quit()
def get_file(self, url: str, path: Path) -> None:
"""通过get方法下载文件"""
"""
通过get方法下载文件到指定路径
:param url: 请求的URL
:param path: 下载文件的保存路径
"""
logger.info(f"子线程 {self.objectName()} 开始下载文件", module="网络请求子线程")
response = None
try:
response = requests.get(url, timeout=10, proxies=self.proxies)
response = requests.get(url, timeout=self.timeout, proxies=self.proxies)
if response.status_code == 200:
with open(path, "wb") as file:
file.write(response.content)
self.status_code = response.status_code
self.error_message = None
else:
self.status_code = response.status_code
self.error_message = "下载失败"
self.error_message = f"下载失败,状态码: {response.status_code}"
except Exception as e:
self.status_code = response.status_code if response else None
self.error_message = str(e)
logger.exception(
f"子线程 {self.objectName()} 网络请求失败:{e}", module="网络请求子线程"
)
self.loop.quit()
def upload_file(self, url: str, files: Dict, data: Dict = None) -> None:
"""
通过POST方法上传文件
:param url: 请求的URL
:param files: 文件字典,格式为 {'file': ('filename', file_obj, 'content_type')}
:param data: 表单数据字典
"""
logger.info(f"子线程 {self.objectName()} 开始上传文件", module="网络请求子线程")
response = None
for _ in range(self.max_retries):
try:
response = requests.post(
url,
files=files,
data=data,
timeout=self.timeout,
proxies=self.proxies,
)
self.status_code = response.status_code
# 尝试解析JSON响应
try:
self.response_json = response.json()
except ValueError:
# 如果不是JSON格式保存文本内容
self.response_json = {"text": response.text}
self.error_message = None
break
except Exception as e:
self.status_code = response.status_code if response else None
self.response_json = None
self.error_message = str(e)
logger.exception(
f"子线程 {self.objectName()} 文件上传失败:{e},第{_+1}次尝试",
module="网络请求子线程",
)
time.sleep(self.backoff_factor)
self.loop.quit()
class _Network(QObject):
"""网络请求线程类"""
"""网络请求线程管理"""
def __init__(self) -> None:
super().__init__()
self.task_queue = []
def add_task(self, mode: str, url: str, path: Path = None) -> NetworkThread:
"""添加网络请求任务"""
def add_task(
self,
mode: str,
url: str,
path: Path = None,
files: Dict = None,
data: Dict = None,
) -> NetworkThread:
"""
添加网络请求任务
network_thread = NetworkThread(mode, url, path)
:param mode: 请求模式,支持 "get", "get_file", "upload_file"
:param url: 请求的URL
:param path: 下载文件的保存路径,仅在 mode 为 "get_file" 时有效
:param files: 上传文件字典,仅在 mode 为 "upload_file" 时有效
:param data: 表单数据字典,仅在 mode 为 "upload_file" 时有效
:return: 返回创建的 NetworkThread 实例
"""
logger.info(f"添加网络请求任务: {mode} {url} {path}", module="网络请求")
network_thread = NetworkThread(mode, url, path, files, data)
self.task_queue.append(network_thread)
@@ -137,8 +237,50 @@ class _Network(QObject):
return network_thread
def upload_config_file(
self, file_path: Path, username: str = "", description: str = ""
) -> NetworkThread:
"""
上传配置文件到分享服务器
:param file_path: 要上传的文件路径
:param username: 用户名(可选)
:param description: 文件描述(必填)
:return: 返回创建的 NetworkThread 实例
"""
if not file_path.exists():
raise FileNotFoundError(f"文件不存在: {file_path}")
if not description:
raise ValueError("文件描述不能为空")
# 准备上传的文件
with open(file_path, "rb") as f:
files = {"file": (file_path.name, f.read(), "application/json")}
# 准备表单数据
data = {"description": description}
if username:
data["username"] = username
url = "http://221.236.27.82:10023/api/upload/share"
logger.info(
f"准备上传配置文件: {file_path.name},用户: {username or '匿名'},描述: {description}",
extra={"module": "网络请求"},
)
return self.add_task("upload_file", url, files=files, data=data)
def get_result(self, network_thread: NetworkThread) -> dict:
"""获取网络请求结果"""
"""
获取网络请求结果
:param network_thread: 网络请求线程实例
:return: 包含状态码、响应JSON和错误信息的字典
"""
result = {
"status_code": network_thread.status_code,
@@ -155,6 +297,11 @@ class _Network(QObject):
self.task_queue.remove(network_thread)
network_thread.deleteLater()
logger.info(
f"网络请求结果: {result['status_code']},请求子线程已结束",
module="网络请求",
)
return result

View File

@@ -25,12 +25,12 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, QUrl
from PySide6.QtMultimedia import QSoundEffect
from pathlib import Path
from .logger import logger
from .config import Config
@@ -42,6 +42,11 @@ class _SoundPlayer(QObject):
self.sounds_path = Config.app_path / "resources/sounds"
def play(self, sound_name: str):
"""
播放指定名称的音效
:param sound_name: 音效文件名(不带扩展名)
"""
if not Config.get(Config.voice_Enabled):
return
@@ -59,6 +64,11 @@ class _SoundPlayer(QObject):
)
def play_voice(self, sound_path: Path):
"""
播放音效文件
:param sound_path: 音效文件的完整路径
"""
effect = QSoundEffect(self)
effect.setVolume(1)

View File

@@ -25,19 +25,18 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QThread, QObject, Signal
from qfluentwidgets import MessageBox
from datetime import datetime
from packaging import version
from typing import Dict, Union
from .logger import logger
from .config import Config
from .main_info_bar import MainInfoBar
from .network import Network
from .sound_player import SoundPlayer
from app.models import MaaManager, GeneralManager
from app.services import System
class Task(QThread):
@@ -77,12 +76,12 @@ class Task(QThread):
if "设置MAA" in self.mode:
logger.info(f"任务开始:设置{self.name}")
logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "设置MAA", self.name, 3000)
self.task = MaaManager(
self.mode,
Config.member_dict[self.name],
Config.script_dict[self.name],
(None if "全局" in self.mode else self.info["SetMaaInfo"]["Path"]),
)
self.task.check_maa_version.connect(self.check_maa_version.emit)
@@ -93,17 +92,19 @@ class Task(QThread):
try:
self.task.run()
except Exception as e:
logger.exception(f"任务异常:{self.name},错误信息:{e}")
logger.exception(
f"任务异常:{self.name},错误信息:{e}", module=f"业务 {self.name}"
)
self.push_info_bar.emit("error", "任务异常", self.name, -1)
elif self.mode == "设置通用脚本":
logger.info(f"任务开始:设置{self.name}")
logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "设置通用脚本", self.name, 3000)
self.task = GeneralManager(
self.mode,
Config.member_dict[self.name],
Config.script_dict[self.name],
self.info["SetSubInfo"]["Path"],
)
self.task.push_info_bar.connect(self.push_info_bar.emit)
@@ -113,17 +114,20 @@ class Task(QThread):
try:
self.task.run()
except Exception as e:
logger.exception(f"任务异常:{self.name},错误信息:{e}")
logger.exception(
f"任务异常:{self.name},错误信息:{e}", module=f"业务 {self.name}"
)
self.push_info_bar.emit("error", "任务异常", self.name, -1)
else:
logger.info(f"任务开始:{self.name}", module=f"业务 {self.name}")
self.task_list = [
[
(
value
if Config.member_dict[value]["Config"].get_name() == ""
else f"{value} - {Config.member_dict[value]["Config"].get_name()}"
if Config.script_dict[value]["Config"].get_name() == ""
else f"{value} - {Config.script_dict[value]["Config"].get_name()}"
),
"等待",
value,
@@ -144,23 +148,28 @@ class Task(QThread):
task[1] = "运行"
self.update_task_list.emit(self.task_list)
# 检查任务是否在运行列表中
if task[2] in Config.running_list:
task[1] = "跳过"
self.update_task_list.emit(self.task_list)
logger.info(f"跳过任务:{task[0]}")
logger.info(
f"跳过任务:{task[0]},该任务已在运行列表中",
module=f"业务 {self.name}",
)
self.push_info_bar.emit("info", "跳过任务", task[0], 3000)
continue
# 标记为运行中
Config.running_list.append(task[2])
logger.info(f"任务开始:{task[0]}")
logger.info(f"任务开始:{task[0]}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "任务开始", task[0], 3000)
if Config.member_dict[task[2]]["Type"] == "Maa":
if Config.script_dict[task[2]]["Type"] == "Maa":
self.task = MaaManager(
self.mode[0:4],
Config.member_dict[task[2]],
Config.script_dict[task[2]],
)
self.task.check_maa_version.connect(self.check_maa_version.emit)
@@ -177,11 +186,11 @@ class Task(QThread):
lambda log: self.task_accomplish(task[2], log)
)
elif Config.member_dict[task[2]]["Type"] == "General":
elif Config.script_dict[task[2]]["Type"] == "General":
self.task = GeneralManager(
self.mode[0:4],
Config.member_dict[task[2]],
Config.script_dict[task[2]],
)
self.task.question.connect(self.question.emit)
@@ -198,11 +207,11 @@ class Task(QThread):
)
try:
self.task.run()
self.task.run() # 运行任务业务
task[1] = "完成"
self.update_task_list.emit(self.task_list)
logger.info(f"任务完成:{task[0]}")
logger.info(f"任务完成:{task[0]}", module=f"业务 {self.name}")
self.push_info_bar.emit("info", "任务完成", task[0], 3000)
except Exception as e:
@@ -217,15 +226,29 @@ class Task(QThread):
task[1] = "异常"
self.update_task_list.emit(self.task_list)
logger.exception(f"任务异常:{task[0]},错误信息:{e}")
logger.exception(
f"任务异常:{task[0]},错误信息:{e}",
module=f"业务 {self.name}",
)
self.push_info_bar.emit("error", "任务异常", task[0], -1)
# 任务结束后从运行列表中移除
Config.running_list.remove(task[2])
self.accomplish.emit(self.logs)
def task_accomplish(self, name: str, log: dict):
"""保存任务结果"""
"""
销毁任务线程并保存任务结果
:param name: 任务名称
:param log: 任务日志记录
"""
logger.info(
f"任务完成:{name},日志记录:{list(log.values())}",
module=f"业务 {self.name}",
)
self.logs.append([name, log])
self.task.deleteLater()
@@ -245,7 +268,13 @@ class _TaskManager(QObject):
def add_task(
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
):
"""添加任务"""
"""
添加任务
:param mode: 任务模式
:param name: 任务名称
:param info: 任务信息
"""
if name in Config.running_list or name in self.task_dict:
@@ -253,11 +282,14 @@ class _TaskManager(QObject):
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
return None
logger.info(f"任务开始:{name}")
logger.info(f"任务开始:{name},模式:{mode}", module="业务调度")
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
SoundPlayer.play("任务开始")
# 标记任务为运行中
Config.running_list.append(name)
# 创建任务实例并连接信号
self.task_dict[name] = Task(mode, name, info)
self.task_dict[name].check_maa_version.connect(self.check_maa_version)
self.task_dict[name].question.connect(
@@ -273,18 +305,24 @@ class _TaskManager(QObject):
lambda logs: self.remove_task(mode, name, logs)
)
# 向UI发送信号以创建或连接GUI
if "新调度台" in mode:
self.create_gui.emit(self.task_dict[name])
elif "主调度台" in mode:
self.connect_gui.emit(self.task_dict[name])
# 启动任务线程
self.task_dict[name].start()
def stop_task(self, name: str):
"""中止任务"""
def stop_task(self, name: str) -> None:
"""
中止任务
logger.info(f"中止任务:{name}")
:param name: 任务名称
"""
logger.info(f"中止任务:{name}", module="业务调度")
MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
if name == "ALL":
@@ -303,19 +341,27 @@ class _TaskManager(QObject):
self.task_dict[name].quit()
self.task_dict[name].wait()
def remove_task(self, mode: str, name: str, logs: list):
"""任务结束后的处理"""
def remove_task(self, mode: str, name: str, logs: list) -> None:
"""
处理任务结束后的收尾工作
logger.info(f"任务结束:{name}")
:param mode: 任务模式
:param name: 任务名称
:param logs: 任务日志
"""
logger.info(f"任务结束:{name}", module="业务调度")
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
SoundPlayer.play("任务结束")
# 删除任务线程,移除运行中标记
self.task_dict[name].deleteLater()
self.task_dict.pop(name)
Config.running_list.remove(name)
if "调度队列" in name and "人工排查" not in mode:
# 保存调度队列历史记录
if len(logs) > 0:
time = logs[0][1]["Time"]
history = ""
@@ -331,25 +377,31 @@ class _TaskManager(QObject):
},
)
# 根据调度队列情况设置电源状态
if (
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
)
!= "NoAction"
and Config.power_sign == "NoAction"
):
Config.set_power_sign(
Config.queue_dict[name]["Config"].get(
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
)
)
if Config.args.mode == "cli" and Config.power_sign == "NoAction":
Config.set_power_sign("KillSelf")
def check_maa_version(self, v: str):
"""检查MAA版本"""
def check_maa_version(self, v: str) -> None:
"""
检查MAA版本如果版本过低则推送通知
:param v: 当前MAA版本
"""
logger.info(f"检查MAA版本{v}", module="业务调度")
network = Network.add_task(
mode="get",
url="https://mirrorchyan.com/api/resources/MAA/latest?user_agent=AutoMaaGui&os=win&arch=x64&channel=stable",
@@ -359,7 +411,10 @@ class _TaskManager(QObject):
if network_result["status_code"] == 200:
maa_info = network_result["response_json"]
else:
logger.warning(f"获取MAA版本信息时出错{network_result['error_message']}")
logger.warning(
f"获取MAA版本信息时出错{network_result['error_message']}",
module="业务调度",
)
MainInfoBar.push_info_bar(
"warning",
"获取MAA版本信息时出错",
@@ -371,7 +426,8 @@ class _TaskManager(QObject):
if version.parse(maa_info["data"]["version_name"]) > version.parse(v):
logger.info(
f"检测到MAA版本过低{v},最新版本:{maa_info['data']['version_name']}"
f"检测到MAA版本过低{v},最新版本:{maa_info['data']['version_name']}",
module="业务调度",
)
MainInfoBar.push_info_bar(
"info",
@@ -380,8 +436,19 @@ class _TaskManager(QObject):
-1,
)
logger.success(
f"MAA版本检查完成{v},最新版本:{maa_info['data']['version_name']}",
module="业务调度",
)
def push_dialog(self, name: str, title: str, content: str):
"""推送对话框"""
"""
推送来自任务线程的对话框
:param name: 任务名称
:param title: 对话框标题
:param content: 对话框内容
"""
choice = MessageBox(title, content, Config.main_window)
choice.yesButton.setText("")

View File

@@ -25,12 +25,11 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, QTimer
from datetime import datetime
from pathlib import Path
import keyboard
from .logger import logger
from .config import Config
from .task_manager import TaskManager
from app.services import System
@@ -45,14 +44,31 @@ class _MainTimer(QObject):
self.Timer.timeout.connect(self.timed_start)
self.Timer.timeout.connect(self.set_silence)
self.Timer.timeout.connect(self.check_power)
self.Timer.start(1000)
self.LongTimer = QTimer()
self.LongTimer.timeout.connect(self.long_timed_task)
def start(self):
"""启动定时器"""
logger.info("启动主定时器", module="主业务定时器")
self.Timer.start(1000)
self.LongTimer.start(3600000)
def stop(self):
"""停止定时器"""
logger.info("停止主定时器", module="主业务定时器")
self.Timer.stop()
self.Timer.deleteLater()
self.LongTimer.stop()
self.LongTimer.deleteLater()
def long_timed_task(self):
"""长时间定期检定任务"""
logger.info("执行长时间定期检定任务", module="主业务定时器")
Config.get_stage()
Config.main_window.setting.show_notice()
if Config.get(Config.update_IfAutoUpdate):
@@ -63,15 +79,15 @@ class _MainTimer(QObject):
for name, info in Config.queue_dict.items():
if not info["Config"].get(info["Config"].queueSet_Enabled):
if not info["Config"].get(info["Config"].QueueSet_TimeEnabled):
continue
data = info["Config"].toDict()
time_set = [
data["Time"][f"TimeSet_{_}"]
data["Time"][f"Set_{_}"]
for _ in range(10)
if data["Time"][f"TimeEnabled_{_}"]
if data["Time"][f"Enabled_{_}"]
]
# 按时间调起代理任务
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
@@ -82,7 +98,7 @@ class _MainTimer(QObject):
and name not in Config.running_list
):
logger.info(f"定时任务:{name}")
logger.info(f"定时唤起任务:{name}", module="主业务定时器")
TaskManager.add_task("自动代理_新调度台", name, data)
def set_silence(self):
@@ -96,12 +112,21 @@ class _MainTimer(QObject):
windows = System.get_window_info()
# 此处排除雷电名为新通知的窗口
if any(
str(emulator_path) in window and window[0] != "新通知"
for window in windows
for emulator_path in Config.silence_list
):
emulator_windows = []
for window in windows:
for emulator_path, endtime in Config.silence_dict.items():
if (
datetime.now() < endtime
and str(emulator_path) in window
and window[0] != "新通知" # 此处排除雷电名为新通知的窗口
):
emulator_windows.append(window)
if emulator_windows:
logger.info(
f"检测到模拟器窗口:{emulator_windows}", module="主业务定时器"
)
try:
keyboard.press_and_release(
"+".join(
@@ -109,13 +134,20 @@ class _MainTimer(QObject):
for _ in Config.get(Config.function_BossKey).split("+")
)
)
logger.info(
f"模拟按键:{Config.get(Config.function_BossKey)}",
module="主业务定时器",
)
except Exception as e:
logger.error(f"模拟按键时出错:{e}")
logger.exception(f"模拟按键时出错:{e}", module="主业务定时器")
def check_power(self):
"""检查电源操作"""
if Config.power_sign != "NoAction" and not Config.running_list:
logger.info(f"触发电源操作:{Config.power_sign}", module="主业务定时器")
from app.ui import ProgressRingMessageBox
mode_book = {
@@ -123,15 +155,20 @@ class _MainTimer(QObject):
"Sleep": "睡眠",
"Hibernate": "休眠",
"Shutdown": "关机",
"ShutdownForce": "关机(强制)",
}
choice = ProgressRingMessageBox(
Config.main_window, f"{mode_book[Config.power_sign]}倒计时"
)
if choice.exec():
logger.info(
f"确认执行电源操作:{Config.power_sign}", module="主业务定时器"
)
System.set_power(Config.power_sign)
Config.set_power_sign("NoAction")
else:
logger.info(f"取消电源操作:{Config.power_sign}", module="主业务定时器")
Config.set_power_sign("NoAction")

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtCore import QObject, Signal, QEventLoop, QFileSystemWatcher, QTimer
import os
import sys
@@ -37,7 +36,7 @@ from pathlib import Path
from jinja2 import Environment, FileSystemLoader
from typing import Union, List, Dict
from app.core import Config, GeneralConfig, GeneralSubConfig
from app.core import Config, GeneralConfig, GeneralSubConfig, logger
from app.services import Notify, System
from app.utils import ProcessManager
@@ -75,19 +74,22 @@ class GeneralManager(QObject):
self.sub_list = []
self.mode = mode
self.config_path = config["Path"]
self.name = config["Config"].get(config["Config"].Script_Name)
self.sub_config_path = sub_config_path
self.game_process_manager = ProcessManager()
self.script_process_manager = ProcessManager()
self.log_monitor = QFileSystemWatcher()
self.log_monitor.fileChanged.connect(self.check_script_log)
self.log_monitor_timer = QTimer()
self.log_monitor_timer.timeout.connect(self.refresh_log)
self.monitor_loop = QEventLoop()
self.loge_start_time = datetime.now()
self.script_logs = []
self.script_result = "Wait"
self.script_process_manager.processClosed.connect(
lambda: self.log_monitor.fileChanged.emit("进程结束检查")
)
self.script_process_manager.processClosed.connect(self.check_script_log)
self.question_loop = QEventLoop()
self.question_response.connect(self.__capture_response)
@@ -111,6 +113,10 @@ class GeneralManager(QObject):
self.data = dict(sorted(self.data.items(), key=lambda x: int(x[0][3:])))
logger.success(
f"初始化通用调度器,模式:{self.mode}", module=f"通用调度器-{self.name}"
)
def check_config_info(self) -> bool:
"""检查配置完整性"""
@@ -124,7 +130,7 @@ class GeneralManager(QObject):
) or (
self.set["Game"]["Enabled"] and not Path(self.set["Game"]["Path"]).exists()
):
logger.error("脚本配置缺失")
logger.error("脚本配置缺失", module=f"通用调度器-{self.name}")
self.push_info_bar.emit("error", "脚本配置缺失", "请检查脚本配置!", -1)
return False
@@ -133,9 +139,36 @@ class GeneralManager(QObject):
def configure(self):
"""提取配置信息"""
self.name = self.set["Script"]["Name"]
self.script_root_path = Path(self.set["Script"]["RootPath"])
self.script_exe_path = Path(self.set["Script"]["ScriptPath"])
self.script_path = Path(self.set["Script"]["ScriptPath"])
arguments_list = []
path_list = []
for argument in [
_.strip()
for _ in str(self.set["Script"]["Arguments"]).split("|")
if _.strip()
]:
arg = [_.strip() for _ in argument.split("%") if _.strip()]
if len(arg) > 1:
path_list.append((self.script_path / arg[0]).resolve())
arguments_list.append(
[_.strip() for _ in arg[1].split(" ") if _.strip()]
)
elif len(arg) > 0:
path_list.append(self.script_path)
arguments_list.append(
[_.strip() for _ in arg[0].split(" ") if _.strip()]
)
self.script_exe_path = path_list[0] if len(path_list) > 0 else self.script_path
self.script_arguments = arguments_list[0] if len(arguments_list) > 0 else []
self.script_set_exe_path = (
path_list[1] if len(path_list) > 1 else self.script_path
)
self.script_set_arguments = arguments_list[1] if len(arguments_list) > 1 else []
self.script_config_path = Path(self.set["Script"]["ConfigPath"])
self.script_log_path = (
Path(self.set["Script"]["LogPath"]).with_stem(
@@ -166,9 +199,23 @@ class GeneralManager(QObject):
curdate = Config.server_date().strftime("%Y-%m-%d")
begin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
if self.mode == "人工排查":
logger.error("通用脚本不支持人工排查模式", module=f"通用调度器-{self.name}")
self.accomplish.emit(
{
"Time": begin_time,
"History": "通用脚本不支持人工排查模式,通用代理进程中止",
}
)
return None
# 检查配置完整性
if not self.check_config_info():
logger.error(
"配置不完整,无法启动通用代理进程", module=f"通用调度器-{self.name}"
)
self.accomplish.emit(
{"Time": begin_time, "History": "由于配置不完整,通用代理进程中止"}
)
@@ -176,6 +223,22 @@ class GeneralManager(QObject):
self.configure()
# 记录配置文件
logger.info(
f"记录通用脚本配置文件:{self.script_config_path}",
module=f"通用调度器-{self.name}",
)
(self.config_path / "Temp").mkdir(parents=True, exist_ok=True)
if self.set["Script"]["ConfigPathMode"] == "文件夹":
if self.script_config_path.exists():
shutil.copytree(
self.script_config_path,
self.config_path / "Temp",
dirs_exist_ok=True,
)
elif self.script_config_path.exists():
shutil.copy(self.script_config_path, self.config_path / "Temp/config.temp")
# 整理用户数据,筛选需代理的用户
if self.mode != "设置通用脚本":
@@ -190,6 +253,11 @@ class GeneralManager(QObject):
]
self.create_user_list.emit(self.sub_list)
logger.info(
f"配置列表创建完成,已筛选子配置数:{len(self.sub_list)}",
module=f"通用调度器-{self.name}",
)
# 自动代理模式
if self.mode == "自动代理":
@@ -222,14 +290,17 @@ class GeneralManager(QObject):
self.update_user_list.emit(self.sub_list)
continue
logger.info(f"{self.name} | 开始代理配置: {sub[0]}")
logger.info(f"开始代理配置: {sub[0]}", module=f"通用调度器-{self.name}")
sub_start_time = datetime.now()
run_book = False
if not (self.data[sub[2]]["Path"] / "ConfigFiles").exists():
logger.error(f"{self.name} | 配置: {sub[0]} - 未找到配置文件")
logger.error(
f"配置: {sub[0]} - 未找到配置文件",
module=f"通用调度器-{self.name}",
)
self.push_info_bar.emit(
"error",
"启动通用代理进程失败",
@@ -246,11 +317,12 @@ class GeneralManager(QObject):
break
logger.info(
f"{self.name} | 用户: {sub[0]} - 尝试次数: {i + 1}/{self.set['Run']['RunTimesLimit']}"
f"用户: {sub[0]} - 尝试次数: {i + 1}/{self.set['Run']['RunTimesLimit']}",
module=f"通用调度器-{self.name}",
)
# 记录当前时间
start_time = datetime.now()
self.log_start_time = datetime.now()
# 配置脚本
self.set_sub(sub[2])
# 执行任务前脚本
@@ -267,7 +339,8 @@ class GeneralManager(QObject):
try:
logger.info(
f"{self.name} | 启动游戏/模拟器:{self.game_path},参数:{self.set['Game']['Arguments']}"
f"启动游戏/模拟器:{self.game_path},参数:{self.set['Game']['Arguments']}",
module=f"通用调度器-{self.name}",
)
self.game_process_manager.open_process(
self.game_path,
@@ -275,8 +348,9 @@ class GeneralManager(QObject):
0,
)
except Exception as e:
logger.error(
f"{self.name} | 启动游戏/模拟器时出现异常:{e}"
logger.exception(
f"启动游戏/模拟器时出现异常:{e}",
module=f"通用调度器-{self.name}",
)
self.push_info_bar.emit(
"error",
@@ -287,9 +361,17 @@ class GeneralManager(QObject):
self.script_result = "游戏/模拟器启动失败"
break
# 添加静默进程标记
# 更新静默进程标记
if self.set["Game"]["Style"] == "Emulator":
Config.silence_list.append(self.game_path)
logger.info(
f"更新静默进程标记:{self.game_path},标记有效时间:{datetime.now() + timedelta(seconds=self.set['Game']['WaitTime'] + 10)}",
module=f"通用调度器-{self.name}",
)
Config.silence_dict[
self.game_path
] = datetime.now() + timedelta(
seconds=self.set["Game"]["WaitTime"] + 10
)
self.update_log_text.emit(
f"正在等待游戏/模拟器完成启动\n请等待{self.set['Game']['WaitTime']}s"
@@ -297,25 +379,19 @@ class GeneralManager(QObject):
self.sleep(self.set["Game"]["WaitTime"])
# 10s后移除静默进程标记
if self.set["Game"]["Style"] == "Emulator":
QTimer.singleShot(
10000,
partial(Config.silence_list.remove, self.game_path),
)
# 运行脚本任务
logger.info(
f"{self.name} | 运行脚本任务:{self.script_exe_path},参数:{self.set['Script']['Arguments']}"
f"运行脚本任务:{self.script_exe_path},参数:{self.script_arguments}",
module=f"通用调度器-{self.name}",
)
self.script_process_manager.open_process(
self.script_exe_path,
str(self.set["Script"]["Arguments"]).split(" "),
self.script_arguments,
tracking_time=60 if self.set["Script"]["IfTrackProcess"] else 0,
)
# 监测运行状态
self.start_monitor(start_time)
self.start_monitor()
if self.script_result == "Success!":
@@ -323,24 +399,59 @@ class GeneralManager(QObject):
run_book = True
# 中止相关程序
logger.info(
f"中止相关程序:{self.script_exe_path}",
module=f"通用调度器-{self.name}",
)
self.script_process_manager.kill()
System.kill_process(self.script_exe_path)
if self.set["Game"]["Enabled"]:
logger.info(
f"中止游戏/模拟器进程:{list(self.game_process_manager.tracked_pids)}",
module=f"通用调度器-{self.name}",
)
self.game_process_manager.kill()
if self.set["Game"]["IfForceClose"]:
System.kill_process(self.game_path)
logger.info(
f"{self.name} | 配置: {sub[0]} - 通用脚本进程完成代理任务"
f"配置: {sub[0]} - 通用脚本进程完成代理任务",
module=f"通用调度器-{self.name}",
)
self.update_log_text.emit(
"检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
)
self.sleep(10)
# 更新脚本配置文件
if self.set["Script"]["UpdateConfigMode"] in [
"Success",
"Always",
]:
if self.set["Script"]["ConfigPathMode"] == "文件夹":
shutil.copytree(
self.script_config_path,
self.data[sub[2]]["Path"] / "ConfigFiles",
dirs_exist_ok=True,
)
else:
shutil.copy(
self.script_config_path,
self.data[sub[2]]["Path"]
/ "ConfigFiles"
/ self.script_config_path.name,
)
logger.success(
"通用脚本配置文件已更新",
module=f"通用调度器-{self.name}",
)
else:
logger.error(
f"{self.name} | 配置: {sub[0]} - 代理任务异常: {self.script_result}"
f"配置: {sub[0]} - 代理任务异常: {self.script_result}",
module=f"通用调度器-{self.name}",
)
# 打印中止信息
# 此时log变量内存储的就是出现异常的日志信息可以保存或发送用于问题排查
@@ -349,8 +460,17 @@ class GeneralManager(QObject):
)
# 中止相关程序
logger.info(
f"中止相关程序:{self.script_exe_path}",
module=f"通用调度器-{self.name}",
)
self.script_process_manager.kill()
System.kill_process(self.script_exe_path)
if self.set["Game"]["Enabled"]:
logger.info(
f"中止游戏/模拟器进程:{list(self.game_process_manager.tracked_pids)}",
module=f"通用调度器-{self.name}",
)
self.game_process_manager.kill()
if self.set["Game"]["IfForceClose"]:
System.kill_process(self.game_path)
@@ -366,8 +486,33 @@ class GeneralManager(QObject):
self.play_sound.emit("子任务失败")
else:
self.play_sound.emit(self.script_result)
self.sleep(10)
# 更新脚本配置文件
if self.set["Script"]["UpdateConfigMode"] in [
"Failure",
"Always",
]:
if self.set["Script"]["ConfigPathMode"] == "文件夹":
shutil.copytree(
self.script_config_path,
self.data[sub[2]]["Path"] / "ConfigFiles",
dirs_exist_ok=True,
)
else:
shutil.copy(
self.script_config_path,
self.data[sub[2]]["Path"]
/ "ConfigFiles"
/ self.script_config_path.name,
)
logger.success(
"通用脚本配置文件已更新",
module=f"通用调度器-{self.name}",
)
# 执行任务后脚本
if (
sub_data["Info"]["IfScriptAfterTask"]
@@ -380,8 +525,8 @@ class GeneralManager(QObject):
# 保存运行日志以及统计信息
Config.save_general_log(
Config.app_path
/ f"history/{curdate}/{sub_data['Info']['Name']}/{start_time.strftime("%H-%M-%S")}.log",
self.check_script_log(start_time),
/ f"history/{curdate}/{sub_data['Info']['Name']}/{self.log_start_time.strftime("%H-%M-%S")}.log",
self.script_logs,
self.script_result,
)
@@ -409,6 +554,10 @@ class GeneralManager(QObject):
sub_data["Info"]["RemainedDay"] -= 1
sub_data["Data"]["ProxyTimes"] += 1
sub[1] = "完成"
logger.success(
f"配置: {sub[0]} - 代理任务完成",
module=f"通用调度器-{self.name}",
)
Notify.push_plyer(
"成功完成一个自动代理任务!",
f"已完成配置 {sub[0].replace("_", " 今天的")}任务",
@@ -418,6 +567,10 @@ class GeneralManager(QObject):
else:
# 录入代理失败的用户
sub[1] = "异常"
logger.error(
f"配置: {sub[0]} - 代理任务异常: {self.script_result}",
module=f"通用调度器-{self.name}",
)
self.update_user_list.emit(self.sub_list)
@@ -429,17 +582,21 @@ class GeneralManager(QObject):
try:
# 创建通用脚本任务
logger.info(f"{self.name} | 无参数启动通用脚本:{self.script_exe_path}")
logger.info(
f"运行脚本任务:{self.script_set_exe_path},参数:{self.script_set_arguments}",
module=f"通用调度器-{self.name}",
)
self.script_process_manager.open_process(
self.script_exe_path,
self.script_set_exe_path,
self.script_set_arguments,
tracking_time=60 if self.set["Script"]["IfTrackProcess"] else 0,
)
# 记录当前时间
start_time = datetime.now()
self.log_start_time = datetime.now()
# 监测通用脚本运行状态
self.start_monitor(start_time)
self.start_monitor()
self.sub_config_path.mkdir(parents=True, exist_ok=True)
if self.set["Script"]["ConfigPathMode"] == "文件夹":
@@ -448,16 +605,23 @@ class GeneralManager(QObject):
self.sub_config_path,
dirs_exist_ok=True,
)
logger.success(
f"通用脚本配置已保存到:{self.sub_config_path}",
module=f"通用调度器-{self.name}",
)
else:
shutil.copy(self.script_config_path, self.sub_config_path)
logger.success(
f"通用脚本配置已保存到:{self.sub_config_path}",
module=f"通用调度器-{self.name}",
)
except Exception as e:
logger.error(f"{self.name} | 启动通用脚本时出现异常:{e}")
logger.exception(
f"启动通用脚本时出现异常:{e}", module=f"通用调度器-{self.name}"
)
self.push_info_bar.emit(
"error",
"启动通用脚本时出现异常",
"请检查相关设置",
-1,
"error", "启动通用脚本时出现异常", "请检查相关设置", -1
)
result_text = ""
@@ -467,9 +631,17 @@ class GeneralManager(QObject):
# 关闭可能未正常退出的通用脚本进程
if self.isInterruptionRequested:
logger.info(
f"关闭可能未正常退出的通用脚本进程:{self.script_exe_path}",
module=f"通用调度器-{self.name}",
)
self.script_process_manager.kill(if_force=True)
System.kill_process(self.script_exe_path)
if self.set["Game"]["Enabled"]:
logger.info(
f"关闭可能未正常退出的游戏/模拟器进程:{list(self.game_process_manager.tracked_pids)}",
module=f"通用调度器-{self.name}",
)
self.game_process_manager.kill(if_force=True)
if self.set["Game"]["IfForceClose"]:
System.kill_process(self.game_path)
@@ -522,12 +694,30 @@ class GeneralManager(QObject):
)
self.push_notification("代理结果", title, result)
# 复原通用脚本配置文件
logger.info(
f"复原通用脚本配置文件:{self.config_path / 'Temp'}",
module=f"通用调度器-{self.name}",
)
if self.set["Script"]["ConfigPathMode"] == "文件夹":
if (self.config_path / "Temp").exists():
shutil.copytree(
self.config_path / "Temp",
self.script_config_path,
dirs_exist_ok=True,
)
elif (self.config_path / "Temp/config.temp").exists():
shutil.copy(self.config_path / "Temp/config.temp", self.script_config_path)
shutil.rmtree(self.config_path / "Temp")
self.log_monitor.deleteLater()
self.log_monitor_timer.deleteLater()
self.accomplish.emit({"Time": begin_time, "History": result_text})
def requestInterruption(self) -> None:
logger.info(f"{self.name} | 收到任务中止申请")
"""请求中止通用脚本任务"""
logger.info(f"收到任务中止申请", module=f"通用调度器-{self.name}")
if len(self.log_monitor.files()) != 0:
self.interrupt.emit()
@@ -537,29 +727,49 @@ class GeneralManager(QObject):
self.wait_loop.quit()
def push_question(self, title: str, message: str) -> bool:
"""推送问题询问"""
logger.info(
f"推送问题询问:{title} - {message}", module=f"通用调度器-{self.name}"
)
self.question.emit(title, message)
self.question_loop.exec()
return self.response
def __capture_response(self, response: bool) -> None:
"""捕获问题询问的响应"""
logger.info(f"捕获问题询问的响应:{response}", module=f"通用调度器-{self.name}")
self.response = response
def sleep(self, time: int) -> None:
"""非阻塞型等待"""
logger.info(f"等待 {time}", module=f"通用调度器-{self.name}")
QTimer.singleShot(time * 1000, self.wait_loop.quit)
self.wait_loop.exec()
def refresh_log(self) -> None:
"""刷新脚本日志"""
with self.script_log_path.open(mode="r", encoding="utf-8") as f:
pass
if self.script_log_path.exists():
with self.script_log_path.open(mode="r", encoding="utf-8") as f:
logger.debug(
f"刷新通用脚本日志:{self.script_log_path}",
module=f"通用调度器-{self.name}",
)
else:
logger.warning(
f"通用脚本日志文件不存在:{self.script_log_path}",
module=f"通用调度器-{self.name}",
)
# 一分钟内未执行日志变化检查,强制检查一次
if (datetime.now() - self.last_check_time).total_seconds() > 60:
self.log_monitor.fileChanged.emit("1分钟超时检查")
logger.info("触发 1 分钟超时检查", module=f"通用调度器-{self.name}")
self.check_script_log()
def strptime(
self, date_string: str, format: str, default_date: datetime
@@ -589,44 +799,55 @@ class GeneralManager(QObject):
return datetime(**datetime_kwargs)
def check_script_log(self, start_time: datetime) -> list:
def check_script_log(self) -> None:
"""获取脚本日志并检查以判断脚本程序运行状态"""
self.last_check_time = datetime.now()
# 获取日志
logs = []
if_log_start = False
with self.script_log_path.open(mode="r", encoding="utf-8") as f:
for entry in f:
if not if_log_start:
try:
entry_time = self.strptime(
entry[self.log_time_range[0] : self.log_time_range[1]],
self.set["Script"]["LogTimeFormat"],
self.last_check_time,
)
if self.script_log_path.exists():
self.script_logs = []
if_log_start = False
with self.script_log_path.open(mode="r", encoding="utf-8") as f:
for entry in f:
if not if_log_start:
try:
entry_time = self.strptime(
entry[self.log_time_range[0] : self.log_time_range[1]],
self.set["Script"]["LogTimeFormat"],
self.last_check_time,
)
if entry_time > start_time:
if_log_start = True
logs.append(entry)
except ValueError:
pass
else:
logs.append(entry)
log = "".join(logs)
if entry_time > self.log_start_time:
if_log_start = True
self.script_logs.append(entry)
except ValueError:
pass
else:
self.script_logs.append(entry)
else:
logger.warning(
f"通用脚本日志文件不存在:{self.script_log_path}",
module=f"通用调度器-{self.name}",
)
return None
log = "".join(self.script_logs)
# 更新日志
if len(logs) > 100:
self.update_log_text.emit("".join(logs[-100:]))
else:
self.update_log_text.emit("".join(logs))
if self.script_process_manager.is_running():
self.update_log_text.emit(
"".join(self.script_logs)
if len(self.script_logs) < 100
else "".join(self.script_logs[-100:])
)
if "自动代理" in self.mode:
# 获取最近一条日志的时间
latest_time = start_time
for _ in logs[::-1]:
latest_time = self.log_start_time
for _ in self.script_logs[::-1]:
try:
latest_time = self.strptime(
_[self.log_time_range[0] : self.log_time_range[1]],
@@ -637,6 +858,11 @@ class GeneralManager(QObject):
except ValueError:
pass
logger.info(
f"通用脚本最近一条日志时间:{latest_time}",
module=f"通用调度器-{self.name}",
)
for success_sign in self.success_log:
if success_sign in log:
self.script_result = "Success!"
@@ -668,18 +894,23 @@ class GeneralManager(QObject):
else:
self.script_result = "Success!"
logger.info(
f"通用脚本日志分析结果:{self.script_result}",
module=f"通用调度器-{self.name}",
)
if self.script_result != "Wait":
self.quit_monitor()
return logs
def start_monitor(self, start_time: datetime) -> None:
def start_monitor(self) -> None:
"""开始监视通用脚本日志"""
logger.info(f"{self.name} | 开始监视通用脚本日志")
logger.info(
f"开始监视通用脚本日志,路径:{self.script_log_path},日志起始时间:{self.log_start_time}",
module=f"通用调度器-{self.name}",
)
self.log_monitor.addPath(str(self.script_log_path))
self.log_monitor.fileChanged.connect(lambda: self.check_script_log(start_time))
self.log_monitor_timer.start(1000)
self.last_check_time = datetime.now()
self.monitor_loop.exec()
@@ -689,19 +920,33 @@ class GeneralManager(QObject):
if len(self.log_monitor.files()) != 0:
logger.info(f"{self.name} | 退出通用脚本日志监视")
logger.info(
f"通用脚本日志监视器移除路径:{self.script_log_path}",
module=f"通用调度器-{self.name}",
)
self.log_monitor.removePath(str(self.script_log_path))
self.log_monitor.fileChanged.disconnect()
self.log_monitor_timer.stop()
self.last_check_time = None
self.monitor_loop.quit()
else:
logger.warning(
f"通用脚本日志监视器没有正在监看的路径:{self.log_monitor.files()}",
module=f"通用调度器-{self.name}",
)
self.log_monitor_timer.stop()
self.last_check_time = None
self.monitor_loop.quit()
logger.info("通用脚本日志监视锁已释放", module=f"通用调度器-{self.name}")
def set_sub(self, index: str = "") -> dict:
"""配置通用脚本运行参数"""
logger.info(f"{self.name} | 配置脚本运行参数: {index}")
logger.info(f"开始配置脚本运行参数{index}", module=f"通用调度器-{self.name}")
# 配置前关闭可能未正常退出的脚本进程
System.kill_process(self.script_exe_path)
if self.mode == "自动代理":
System.kill_process(self.script_exe_path)
elif self.mode == "设置通用脚本":
System.kill_process(self.script_set_exe_path)
# 预导入配置文件
if self.mode == "设置通用脚本":
@@ -732,11 +977,15 @@ class GeneralManager(QObject):
self.script_config_path,
)
logger.info(f"脚本运行参数配置完成:{index}", module=f"通用调度器-{self.name}")
def execute_script_task(self, script_path: Path, task_name: str) -> bool:
"""执行脚本任务并等待结束"""
try:
logger.info(f"{self.name} | 开始执行{task_name}: {script_path}")
logger.info(
f"开始执行{task_name}: {script_path}", module=f"通用调度器-{self.name}"
)
# 根据文件类型选择执行方式
if script_path.suffix.lower() == ".py":
@@ -744,7 +993,10 @@ class GeneralManager(QObject):
elif script_path.suffix.lower() in [".bat", ".cmd", ".exe"]:
cmd = [str(script_path)]
elif script_path.suffix.lower() == "":
logger.warning(f"{self.name} | {task_name}脚本没有指定后缀名,无法执行")
logger.warning(
f"{task_name}脚本没有指定后缀名,无法执行",
module=f"通用调度器-{self.name}",
)
return False
else:
# 使用系统默认程序打开
@@ -767,23 +1019,32 @@ class GeneralManager(QObject):
)
if result.returncode == 0:
logger.info(f"{self.name} | {task_name}执行成功")
logger.info(f"{task_name}执行成功", module=f"通用调度器-{self.name}")
if result.stdout.strip():
logger.info(f"{self.name} | {task_name}输出: {result.stdout}")
logger.info(
f"{task_name}输出: {result.stdout}",
module=f"通用调度器-{self.name}",
)
return True
else:
logger.error(
f"{self.name} | {task_name}执行失败,返回码: {result.returncode}"
f"{task_name}执行失败,返回码: {result.returncode}",
module=f"通用调度器-{self.name}",
)
if result.stderr.strip():
logger.error(f"{self.name} | {task_name}错误输出: {result.stderr}")
logger.error(
f"{task_name}错误输出: {result.stderr}",
module=f"通用调度器-{self.name}",
)
return False
except subprocess.TimeoutExpired:
logger.error(f"{self.name} | {task_name}执行超时")
logger.error(f"{task_name}执行超时", module=f"通用调度器-{self.name}")
return False
except Exception as e:
logger.exception(f"{self.name} | 执行{task_name}时出现异常: {e}")
logger.exception(
f"执行{task_name}时出现异常: {e}", module=f"通用调度器-{self.name}"
)
return False
def push_notification(
@@ -795,6 +1056,11 @@ class GeneralManager(QObject):
) -> None:
"""通过所有渠道推送通知"""
logger.info(
f"开始推送通知,模式:{mode},标题:{title}",
module=f"通用调度器-{self.name}",
)
env = Environment(
loader=FileSystemLoader(str(Config.app_path / "resources/html"))
)
@@ -902,9 +1168,7 @@ class GeneralManager(QObject):
sub_data["Notify"]["ToAddress"],
)
else:
logger.error(
f"{self.name} | 用户邮箱地址为空,无法发送用户单独的邮件通知"
)
logger.error(f"用户邮箱地址为空,无法发送用户单独的邮件通知")
# 发送ServerChan通知
if sub_data["Notify"]["IfServerChan"]:

View File

@@ -33,13 +33,14 @@ from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from email.utils import formataddr
from pathlib import Path
from typing import Union
import requests
from PySide6.QtCore import QObject, Signal
from loguru import logger
from plyer import notification
from app.core import Config
from app.core import Config, logger
from app.services.security import Crypto
from app.utils.ImageUtils import ImageUtils
@@ -51,11 +52,21 @@ class Notification(QObject):
def __init__(self, parent=None):
super().__init__(parent)
def push_plyer(self, title, message, ticker, t):
"""推送系统通知"""
def push_plyer(self, title, message, ticker, t) -> bool:
"""
推送系统通知
:param title: 通知标题
:param message: 通知内容
:param ticker: 通知横幅
:param t: 通知持续时间
:return: bool
"""
if Config.get(Config.notify_IfPushPlyer):
logger.info(f"推送系统通知:{title}", module="通知服务")
notification.notify(
title=title,
message=message,
@@ -69,7 +80,15 @@ class Notification(QObject):
return True
def send_mail(self, mode, title, content, to_address) -> None:
"""推送邮件通知"""
"""
推送邮件通知
:param mode: 邮件内容模式,支持 "文本""网页"
:param title: 邮件标题
:param content: 邮件内容
:param to_address: 收件人地址
"""
if (
Config.get(Config.notify_SMTPServerAddress) == ""
or Config.get(Config.notify_AuthorizationCode) == ""
@@ -87,7 +106,8 @@ class Notification(QObject):
)
):
logger.error(
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
module="通知服务",
)
self.push_info_bar.emit(
"error",
@@ -110,42 +130,43 @@ class Notification(QObject):
)
) # 发件人显示的名字
message["To"] = formataddr(
(
Header("AUTO_MAA用户", "utf-8").encode(),
to_address,
)
(Header("AUTO_MAA用户", "utf-8").encode(), to_address)
) # 收件人显示的名字
message["Subject"] = Header(title, "utf-8")
if mode == "网页":
message.attach(MIMEText(content, "html", "utf-8"))
smtpObj = smtplib.SMTP_SSL(
Config.get(Config.notify_SMTPServerAddress),
465,
)
smtpObj = smtplib.SMTP_SSL(Config.get(Config.notify_SMTPServerAddress), 465)
smtpObj.login(
Config.get(Config.notify_FromAddress),
Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)),
)
smtpObj.sendmail(
Config.get(Config.notify_FromAddress),
to_address,
message.as_string(),
Config.get(Config.notify_FromAddress), to_address, message.as_string()
)
smtpObj.quit()
logger.success("邮件发送成功")
return None
logger.success(f"邮件发送成功{title}", module="通知服务")
except Exception as e:
logger.error(f"发送邮件时出错:\n{e}")
logger.exception(f"发送邮件时出错:{e}", module="通知服务")
self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1)
return None
return None
def ServerChanPush(self, title, content, send_key, tag, channel):
"""使用Server酱推送通知"""
def ServerChanPush(
self, title, content, send_key, tag, channel
) -> Union[bool, str]:
"""
使用Server酱推送通知
:param title: 通知标题
:param content: 通知内容
:param send_key: Server酱的SendKey
:param tag: 通知标签
:param channel: 通知频道
:return: bool or str
"""
if not send_key:
logger.error("请正确设置Server酱的SendKey")
logger.error("请正确设置Server酱的SendKey", module="通知服务")
self.push_info_bar.emit(
"error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1
)
@@ -176,7 +197,7 @@ class Notification(QObject):
if is_valid(tags):
options["tags"] = tags
else:
logger.warning("Server酱 Tag 配置不正确,将被忽略")
logger.warning("Server酱 Tag 配置不正确,将被忽略", module="通知服务")
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
@@ -187,7 +208,9 @@ class Notification(QObject):
if is_valid(channels):
options["channel"] = channels
else:
logger.warning("Server酱 Channel 配置不正确,将被忽略")
logger.warning(
"Server酱 Channel 配置不正确,将被忽略", module="通知服务"
)
self.push_info_bar.emit(
"warning",
"Server酱通知推送异常",
@@ -212,18 +235,20 @@ class Notification(QObject):
result = response.json()
if result.get("code") == 0:
logger.info("Server酱推送通知成功")
logger.success(f"Server酱推送通知成功{title}", module="通知服务")
return True
else:
error_code = result.get("code", "-1")
logger.error(f"Server酱通知推送失败响应码{error_code}")
logger.exception(
f"Server酱通知推送失败响应码{error_code}", module="通知服务"
)
self.push_info_bar.emit(
"error", "Server酱通知推送失败", f"响应码:{error_code}", -1
)
return f"Server酱通知推送失败{error_code}"
except Exception as e:
logger.exception("Server酱通知推送异常")
logger.exception(f"Server酱通知推送异常{e}", module="通知服务")
self.push_info_bar.emit(
"error",
"Server酱通知推送异常",
@@ -232,10 +257,18 @@ class Notification(QObject):
)
return f"Server酱通知推送异常{str(e)}"
def CompanyWebHookBotPush(self, title, content, webhook_url):
"""使用企业微信群机器人推送通知"""
def CompanyWebHookBotPush(self, title, content, webhook_url) -> Union[bool, str]:
"""
使用企业微信群机器人推送通知
:param title: 通知标题
:param content: 通知内容
:param webhook_url: 企业微信群机器人的WebHook地址
:return: bool or str
"""
if webhook_url == "":
logger.error("请正确设置企业微信群机器人的WebHook地址")
logger.error("请正确设置企业微信群机器人的WebHook地址", module="通知服务")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
@@ -264,7 +297,7 @@ class Notification(QObject):
err = e
time.sleep(0.1)
else:
logger.error(f"推送企业微信群机器人时出错:{err}")
logger.error(f"推送企业微信群机器人时出错:{err}", module="通知服务")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
@@ -274,10 +307,10 @@ class Notification(QObject):
return None
if info["errcode"] == 0:
logger.info("企业微信群机器人推送通知成功")
logger.success(f"企业微信群机器人推送通知成功{title}", module="通知服务")
return True
else:
logger.error(f"企业微信群机器人推送通知失败:{info}")
logger.error(f"企业微信群机器人推送通知失败:{info}", module="通知服务")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送失败",
@@ -287,7 +320,14 @@ class Notification(QObject):
return f"使用企业微信群机器人推送通知时出错:{err}"
def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> bool:
"""使用企业微信群机器人推送图片通知"""
"""
使用企业微信群机器人推送图片通知
:param image_path: 图片文件路径
:param webhook_url: 企业微信群机器人的WebHook地址
:return: bool
"""
try:
# 压缩图片
ImageUtils.compress_image_if_needed(image_path)
@@ -295,7 +335,8 @@ class Notification(QObject):
# 检查图片是否存在
if not image_path.exists():
logger.error(
"图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确"
"图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确",
module="通知服务",
)
self.push_info_bar.emit(
"error",
@@ -306,7 +347,9 @@ class Notification(QObject):
return False
if not webhook_url:
logger.error("请正确设置企业微信群机器人的WebHook地址")
logger.error(
"请正确设置企业微信群机器人的WebHook地址", module="通知服务"
)
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
@@ -320,7 +363,7 @@ class Notification(QObject):
image_base64 = ImageUtils.get_base64_from_file(str(image_path))
image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
except Exception as e:
logger.exception(f"图片编码或MD5计算失败{e}")
logger.exception(f"图片编码或MD5计算失败{e}", module="通知服务")
self.push_info_bar.emit(
"error",
"企业微信群机器人通知推送异常",
@@ -349,10 +392,12 @@ class Notification(QObject):
break
except requests.RequestException as e:
err = e
logger.warning(f"推送企业微信群机器人图片第{_+1}次失败:{e}")
logger.exception(
f"推送企业微信群机器人图片第{_+1}次失败:{e}", module="通知服务"
)
time.sleep(0.1)
else:
logger.error(f"推送企业微信群机器人图片时出错{err}")
logger.error("推送企业微信群机器人图片时出错", module="通知服务")
self.push_info_bar.emit(
"error",
"企业微信群机器人图片推送失败",
@@ -362,10 +407,13 @@ class Notification(QObject):
return False
if info.get("errcode") == 0:
logger.info("企业微信群机器人推送图片成功")
logger.success(
f"企业微信群机器人推送图片成功:{image_path.name}",
module="通知服务",
)
return True
else:
logger.error(f"企业微信群机器人推送图片失败:{info}")
logger.error(f"企业微信群机器人推送图片失败:{info}", module="通知服务")
self.push_info_bar.emit(
"error",
"企业微信群机器人图片推送失败",
@@ -386,6 +434,9 @@ class Notification(QObject):
def send_test_notification(self):
"""发送测试通知到所有已启用的通知渠道"""
logger.info("发送测试通知到所有已启用的通知渠道", module="通知服务")
# 发送系统通知
self.push_plyer(
"测试通知",
@@ -425,6 +476,8 @@ class Notification(QObject):
Config.get(Config.notify_CompanyWebHookBotUrl),
)
logger.info("测试通知发送完成", module="通知服务")
return True

View File

@@ -25,18 +25,15 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
import hashlib
import random
import secrets
import base64
import win32crypt
from pathlib import Path
from Crypto.Cipher import AES
from Crypto.PublicKey import RSA
from Crypto.Cipher import PKCS1_OAEP
from Crypto.Util.Padding import pad, unpad
from typing import List, Dict, Union
from app.core import Config
@@ -44,7 +41,12 @@ from app.core import Config
class CryptoHandler:
def get_PASSWORD(self, PASSWORD: str) -> None:
"""配置管理密钥"""
"""
配置管理密钥
:param PASSWORD: 管理密钥
:type PASSWORD: str
"""
# 生成目录
Config.key_path.mkdir(parents=True, exist_ok=True)
@@ -85,7 +87,12 @@ class CryptoHandler:
(Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local)
def AUTO_encryptor(self, note: str) -> str:
"""使用AUTO_MAA的算法加密数据"""
"""
使用AUTO_MAA的算法加密数据
:param note: 数据明文
:type note: str
"""
if note == "":
return ""
@@ -100,7 +107,16 @@ class CryptoHandler:
return base64.b64encode(encrypted).decode("utf-8")
def AUTO_decryptor(self, note: str, PASSWORD: str) -> str:
"""使用AUTO_MAA的算法解密数据"""
"""
使用AUTO_MAA的算法解密数据
:param note: 数据密文
:type note: str
:param PASSWORD: 管理密钥
:type PASSWORD: str
:return: 解密后的明文
:rtype: str
"""
if note == "":
return ""
@@ -142,24 +158,31 @@ class CryptoHandler:
return note
def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None:
"""修改管理密钥"""
"""
修改管理密钥
for member in Config.member_dict.values():
:param PASSWORD_old: 旧管理密钥
:type PASSWORD_old: str
:param PASSWORD_new: 新管理密钥
:type PASSWORD_new: str
"""
for script in Config.script_dict.values():
# 使用旧管理密钥解密
if member["Type"] == "Maa":
for user in member["UserData"].values():
if script["Type"] == "Maa":
for user in script["UserData"].values():
user["Password"] = self.AUTO_decryptor(
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
)
self.get_PASSWORD(PASSWORD_new)
for member in Config.member_dict.values():
for script in Config.script_dict.values():
# 使用新管理密钥重新加密
if member["Type"] == "Maa":
for user in member["UserData"].values():
if script["Type"] == "Maa":
for user in script["UserData"].values():
user["Config"].set(
user["Config"].Info_Password,
self.AUTO_encryptor(user["Password"]),
@@ -168,20 +191,38 @@ class CryptoHandler:
del user["Password"]
def reset_PASSWORD(self, PASSWORD_new: str) -> None:
"""重置管理密钥"""
"""
重置管理密钥
:param PASSWORD_new: 新管理密钥
:type PASSWORD_new: str
"""
self.get_PASSWORD(PASSWORD_new)
for member in Config.member_dict.values():
for script in Config.script_dict.values():
if member["Type"] == "Maa":
for user in member["UserData"].values():
user["Config"].set(user["Config"].Info_Password, "")
if script["Type"] == "Maa":
for user in script["UserData"].values():
user["Config"].set(
user["Config"].Info_Password, self.AUTO_encryptor("数据已重置")
)
def win_encryptor(
self, note: str, description: str = None, entropy: bytes = None
) -> str:
"""使用Windows DPAPI加密数据"""
"""
使用Windows DPAPI加密数据
:param note: 数据明文
:type note: str
:param description: 描述信息
:type description: str
:param entropy: 随机熵
:type entropy: bytes
:return: 加密后的数据
:rtype: str
"""
if note == "":
return ""
@@ -192,7 +233,16 @@ class CryptoHandler:
return base64.b64encode(encrypted).decode("utf-8")
def win_decryptor(self, note: str, entropy: bytes = None) -> str:
"""使用Windows DPAPI解密数据"""
"""
使用Windows DPAPI解密数据
:param note: 数据密文
:type note: str
:param entropy: 随机熵
:type entropy: bytes
:return: 解密后的明文
:rtype: str
"""
if note == "":
return ""
@@ -202,21 +252,15 @@ class CryptoHandler:
)
return decrypted[1].decode("utf-8")
def search_member(self) -> List[Dict[str, Union[Path, list]]]:
"""搜索所有脚本实例及其用户数据库路径"""
member_list = []
if (Config.app_path / "config/MaaConfig").exists():
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
if subdir.is_dir():
member_list.append({"Path": subdir / "user_data.db"})
return member_list
def check_PASSWORD(self, PASSWORD: str) -> bool:
"""验证管理密钥"""
"""
验证管理密钥
:param PASSWORD: 管理密钥
:type PASSWORD: str
:return: 是否验证通过
:rtype: bool
"""
return bool(
self.AUTO_decryptor(self.AUTO_encryptor("-"), PASSWORD) != "管理密钥错误"

View File

@@ -32,7 +32,6 @@ v4.4
作者DLmaster_361、ClozyA
"""
from loguru import logger
import time
import json
import hmac
@@ -40,7 +39,7 @@ import hashlib
import requests
from urllib import parse
from app.core import Config
from app.core import Config, logger
def skland_sign_in(token) -> dict:
@@ -71,15 +70,16 @@ def skland_sign_in(token) -> dict:
"vName": "1.5.1",
}
# 生成签名
def generate_signature(token_for_sign: str, path, body_or_query):
"""
生成请求签名
:param token_for_sign: 用于加密的token
:param path: 请求路径(如 /api/v1/game/player/binding
:param body_or_query: GET用query字符串POST用body字符串
:return: (sign, 新的header_for_sign字典)
"""
t = str(int(time.time()) - 2) # 时间戳,-2秒以防服务器时间不一致
token_bytes = token_for_sign.encode("utf-8")
header_ca = dict(header_for_sign)
@@ -91,10 +91,10 @@ def skland_sign_in(token) -> dict:
md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest()
return md5, header_ca
# 获取带签名的header
def get_sign_header(url: str, method, body, old_header, sign_token):
"""
获取带签名的请求头
:param url: 请求完整url
:param method: 请求方式 GET/POST
:param body: POST请求体或GET时为None
@@ -102,6 +102,7 @@ def skland_sign_in(token) -> dict:
:param sign_token: 当前会话的签名token
:return: 新请求头
"""
h = json.loads(json.dumps(old_header))
p = parse.urlparse(url)
if method.lower() == "get":
@@ -115,15 +116,21 @@ def skland_sign_in(token) -> dict:
h[i] = header_ca[i]
return h
# 复制请求头并添加cred
def copy_header(cred):
"""
复制请求头并添加cred
:param cred: 当前会话的cred
:return: 新的请求头
"""
v = json.loads(json.dumps(header))
v["cred"] = cred
return v
# 使用token一步步拿到cred和sign_token
def login_by_token(token_code):
"""
使用token一步步拿到cred和sign_token
:param token_code: 你的skyland token
:return: (cred, sign_token)
"""
@@ -136,8 +143,14 @@ def skland_sign_in(token) -> dict:
grant_code = get_grant_code(token_code)
return get_cred(grant_code)
# 通过grant code换cred和sign_token
def get_cred(grant):
"""
通过grant code获取cred和sign_token
:param grant: grant code
:return: (cred, sign_token)
"""
rsp = requests.post(
cred_code_url,
json={"code": grant, "kind": 1},
@@ -153,8 +166,13 @@ def skland_sign_in(token) -> dict:
cred = rsp["data"]["cred"]
return cred, sign_token
# 通过token换grant code
def get_grant_code(token):
"""
通过token获取grant code
:param token: 你的skyland token
:return: grant code
"""
rsp = requests.post(
grant_code_url,
json={"appCode": app_code, "token": token, "type": 0},
@@ -170,10 +188,10 @@ def skland_sign_in(token) -> dict:
)
return rsp["data"]["code"]
# 获取已绑定的角色列表
def get_binding_list(cred, sign_token):
"""
查询绑定的角色
查询绑定的角色列表
:param cred: 当前cred
:param sign_token: 当前sign_token
:return: 角色列表
@@ -190,9 +208,15 @@ def skland_sign_in(token) -> dict:
},
).json()
if rsp["code"] != 0:
logger.error(f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}")
logger.error(
f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}",
module="森空岛签到",
)
if rsp.get("message") == "用户未登录":
logger.error(f"森空岛服务 | 用户登录可能失效了,请重新登录!")
logger.error(
f"森空岛服务 | 用户登录可能失效了,请重新登录!",
module="森空岛签到",
)
return v
# 只取明日方舟arknights的绑定账号
for i in rsp["data"]["list"]:
@@ -201,10 +225,10 @@ def skland_sign_in(token) -> dict:
v.extend(i.get("bindingList"))
return v
# 执行签到
def do_sign(cred, sign_token) -> dict:
"""
对所有绑定的角色进行签到
:param cred: 当前cred
:param sign_token: 当前sign_token
:return: 签到结果字典
@@ -257,5 +281,5 @@ def skland_sign_in(token) -> dict:
# 依次签到
return do_sign(cred, sign_token)
except Exception as e:
logger.error(f"森空岛服务 | 森空岛签到失败: {e}")
logger.exception(f"森空岛服务 | 森空岛签到失败: {e}", module="森空岛签到")
return {"成功": [], "重复": [], "失败": [], "总计": 0}

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QApplication
import sys
import ctypes
@@ -38,7 +37,7 @@ import getpass
from datetime import datetime
from pathlib import Path
from app.core import Config
from app.core import Config, logger
class _SystemHandler:
@@ -147,9 +146,15 @@ class _SystemHandler:
)
if result.returncode == 0:
logger.info(f"任务计划程序自启动已创建: {Config.app_path_sys}")
logger.success(
f"程序自启动任务计划已创建: {Config.app_path_sys}",
module="系统服务",
)
else:
logger.error(f"创建任务计划失败: {result.stderr}")
logger.error(
f"程序自启动任务计划创建失败: {result.stderr}",
module="系统服务",
)
finally:
# 删除临时文件
@@ -159,7 +164,7 @@ class _SystemHandler:
pass
except Exception as e:
logger.exception(f"设置任务计划程序自启动失败: {e}")
logger.exception(f"程序自启动任务计划创建失败: {e}", module="系统服务")
elif not Config.get(Config.start_IfSelfStart) and self.is_startup():
@@ -174,40 +179,54 @@ class _SystemHandler:
)
if result.returncode == 0:
logger.info("任务计划程序自启动已删除")
logger.success("程序自启动任务计划已删除", module="系统服务")
else:
logger.error(f"删除任务计划失败: {result.stderr}")
logger.error(
f"程序自启动任务计划删除失败: {result.stderr}",
module="系统服务",
)
except Exception as e:
logger.exception(f"删除任务计划程序自启动失败: {e}")
logger.exception(f"程序自启动任务计划删除失败: {e}", module="系统服务")
def set_power(self, mode) -> None:
"""
执行系统电源操作
:param mode: 电源操作模式,支持 "NoAction", "Shutdown", "Hibernate", "Sleep", "KillSelf", "ShutdownForce"
"""
if sys.platform.startswith("win"):
if mode == "NoAction":
logger.info("不执行系统电源操作")
logger.info("不执行系统电源操作", module="系统服务")
elif mode == "Shutdown":
logger.info("执行关机操作")
self.kill_emulator_processes()
logger.info("执行关机操作", module="系统服务")
subprocess.run(["shutdown", "/s", "/t", "0"])
elif mode == "ShutdownForce":
logger.info("执行强制关机操作", module="系统服务")
subprocess.run(["shutdown", "/s", "/t", "0", "/f"])
elif mode == "Hibernate":
logger.info("执行休眠操作")
logger.info("执行休眠操作", module="系统服务")
subprocess.run(["shutdown", "/h"])
elif mode == "Sleep":
logger.info("执行睡眠操作")
logger.info("执行睡眠操作", module="系统服务")
subprocess.run(
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
)
elif mode == "KillSelf":
logger.info("执行退出主程序操作", module="系统服务")
Config.main_window.close()
QApplication.quit()
sys.exit(0)
@@ -216,29 +235,50 @@ class _SystemHandler:
if mode == "NoAction":
logger.info("不执行系统电源操作")
logger.info("不执行系统电源操作", module="系统服务")
elif mode == "Shutdown":
logger.info("执行关机操作")
logger.info("执行关机操作", module="系统服务")
subprocess.run(["shutdown", "-h", "now"])
elif mode == "Hibernate":
logger.info("执行休眠操作")
logger.info("执行休眠操作", module="系统服务")
subprocess.run(["systemctl", "hibernate"])
elif mode == "Sleep":
logger.info("执行睡眠操作")
logger.info("执行睡眠操作", module="系统服务")
subprocess.run(["systemctl", "suspend"])
elif mode == "KillSelf":
logger.info("执行退出主程序操作", module="系统服务")
Config.main_window.close()
QApplication.quit()
sys.exit(0)
def kill_emulator_processes(self):
"""这里暂时仅支持 MuMu 模拟器"""
logger.info("正在清除模拟器进程", module="系统服务")
keywords = ["Nemu", "nemu", "emulator", "MuMu"]
for proc in psutil.process_iter(["pid", "name"]):
try:
pname = proc.info["name"].lower()
if any(keyword.lower() in pname for keyword in keywords):
proc.kill()
logger.info(
f"已关闭 MuMu 模拟器进程: {proc.info['name']}",
module="系统服务",
)
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
logger.success("模拟器进程清除完成", module="系统服务")
def is_startup(self) -> bool:
"""判断程序是否已经开机自启"""
@@ -252,11 +292,11 @@ class _SystemHandler:
)
return result.returncode == 0
except Exception as e:
logger.error(f"检查任务计划程序失败: {e}")
logger.exception(f"检查任务计划程序失败: {e}", module="系统服务")
return False
def get_window_info(self) -> list:
"""获取当前窗口信息"""
"""获取当前前台窗口信息"""
def callback(hwnd, window_info):
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
@@ -270,7 +310,13 @@ class _SystemHandler:
return window_info
def kill_process(self, path: Path) -> None:
"""根据路径中止进程"""
"""
根据路径中止进程
:param path: 进程路径
"""
logger.info(f"开始中止进程: {path}", module="系统服务")
for pid in self.search_pids(path):
killprocess = subprocess.Popen(
@@ -280,8 +326,17 @@ class _SystemHandler:
)
killprocess.wait()
logger.success(f"进程已中止: {path}", module="系统服务")
def search_pids(self, path: Path) -> list:
"""根据路径查找进程PID"""
"""
根据路径查找进程PID
:param path: 进程路径
:return: 匹配的进程PID列表
"""
logger.info(f"开始查找进程 PID: {path}", module="系统服务")
pids = []
for proc in psutil.process_iter(["pid", "exe"]):

View File

@@ -59,7 +59,6 @@ from qfluentwidgets import (
MessageBox,
SubtitleLabel,
SettingCard,
SpinBox,
FluentIconBase,
Signal,
ComboBox,
@@ -71,7 +70,6 @@ from qfluentwidgets import (
BodyLabel,
QConfig,
ConfigItem,
TimeEdit,
OptionsConfigItem,
TeachingTip,
TransparentToolButton,
@@ -98,6 +96,23 @@ from qfluentwidgets.common.overload import singledispatchmethod
from app.core import Config
from app.services import Crypto
from qfluentwidgets import SpinBox as SpinBoxBase
from qfluentwidgets import TimeEdit as TimeEditBase
class SpinBox(SpinBoxBase):
"""忽视滚轮事件的SpinBox"""
def wheelEvent(self, event):
event.ignore()
class TimeEdit(TimeEditBase):
"""忽视滚轮事件的TimeEdit"""
def wheelEvent(self, event):
event.ignore()
class LineEditMessageBox(MessageBoxBase):
"""输入对话框"""
@@ -240,7 +255,9 @@ class NoticeMessageBox(MessageBoxBase):
self.button_cancel.clicked.connect(self.cancelButton.click)
self.index.index_cards[0].clicked.emit()
def __update_text(self, text: str):
def __update_text(self, index: int, text: str):
self.currentIndex = index
html = markdown.markdown(text).replace("\n", "")
html = re.sub(
@@ -258,7 +275,7 @@ class NoticeMessageBox(MessageBoxBase):
class NoticeIndexCard(HeaderCardWidget):
index_changed = Signal(str)
index_changed = Signal(int, str)
def __init__(self, title: str, content: Dict[str, str], parent=None):
super().__init__(parent)
@@ -274,12 +291,13 @@ class NoticeMessageBox(MessageBoxBase):
self.index_cards.append(QuantifiedItemCard([index, ""]))
self.index_cards[-1].clicked.connect(
partial(self.index_changed.emit, text)
partial(self.index_changed.emit, len(self.index_cards), text)
)
self.Layout.addWidget(self.index_cards[-1])
if not content:
self.Layout.addWidget(QuantifiedItemCard(["暂无信息", ""]))
self.currentIndex = 0
self.Layout.addStretch(1)

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
@@ -48,7 +47,7 @@ from PySide6.QtGui import QTextCursor
from typing import List, Dict
from app.core import Config, TaskManager, Task, MainInfoBar, SoundPlayer
from app.core import Config, TaskManager, Task, MainInfoBar, logger
from .Widget import StatefulItemCard, ComboBoxMessageBox, PivotArea
@@ -59,28 +58,34 @@ class DispatchCenter(QWidget):
self.setObjectName("调度中枢")
# 添加任务按钮
self.multi_button = PushButton(FluentIcon.ADD, "添加任务", self)
self.multi_button.setToolTip("添加任务")
self.multi_button.clicked.connect(self.start_multi_task)
# 电源动作设置组件
self.power_combox = ComboBox()
self.power_combox.addItem("无动作", userData="NoAction")
self.power_combox.addItem("退出软件", userData="KillSelf")
self.power_combox.addItem("睡眠", userData="Sleep")
self.power_combox.addItem("休眠", userData="Hibernate")
self.power_combox.addItem("关机", userData="Shutdown")
self.power_combox.addItem("关机(强制)", userData="ShutdownForce")
self.power_combox.setCurrentText("无动作")
self.power_combox.currentIndexChanged.connect(self.set_power_sign)
# 导航栏
self.pivotArea = PivotArea(self)
self.pivot = self.pivotArea.pivot
# 导航页面组
self.stackedWidget = QStackedWidget(self)
self.stackedWidget.setContentsMargins(0, 0, 0, 0)
self.stackedWidget.setStyleSheet("background: transparent; border: none;")
self.script_list: Dict[str, DispatchCenter.DispatchBox] = {}
# 添加主调度台
dispatch_box = self.DispatchBox("主调度台", self)
self.script_list["主调度台"] = dispatch_box
self.stackedWidget.addWidget(self.script_list["主调度台"])
@@ -91,6 +96,7 @@ class DispatchCenter(QWidget):
icon=FluentIcon.CAFE,
)
# 顶栏组合
h_layout = QHBoxLayout()
h_layout.addWidget(self.multi_button)
h_layout.addWidget(self.pivotArea)
@@ -108,7 +114,13 @@ class DispatchCenter(QWidget):
)
def add_board(self, task: Task) -> None:
"""添加一个调度台界面"""
"""
为任务添加一个调度台界面并绑定信号
:param task: 任务对象
"""
logger.info(f"添加调度台:{task.name}", module="调度中枢")
dispatch_box = self.DispatchBox(task.name, self)
@@ -129,19 +141,36 @@ class DispatchCenter(QWidget):
self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}")
logger.success(f"调度台 {task.name} 添加成功", module="调度中枢")
def del_board(self, name: str) -> None:
"""删除指定子界面"""
"""
删除指定子界面
:param name: 子界面名称
"""
logger.info(f"删除调度台:{name}", module="调度中枢")
self.pivot.setCurrentItem("主调度台")
self.stackedWidget.removeWidget(self.script_list[name])
self.script_list[name].deleteLater()
self.script_list.pop(name)
self.pivot.removeWidget(name)
logger.success(f"调度台 {name} 删除成功", module="调度中枢")
def connect_main_board(self, task: Task) -> None:
"""连接主调度台"""
"""
将任务连接到主调度台
:param task: 任务对象
"""
logger.info(f"主调度台载入任务:{task.name}", module="调度中枢")
self.script_list["主调度台"].top_bar.Lable.setText(
f"{task.name} - {task.mode.replace("_主调度台","")}模式"
f"{task.name} - {task.mode.replace('_主调度台','')}模式"
)
self.script_list["主调度台"].top_bar.Lable.show()
self.script_list["主调度台"].top_bar.object.hide()
@@ -170,8 +199,17 @@ class DispatchCenter(QWidget):
lambda logs: self.disconnect_main_board(task.name, logs)
)
logger.success(f"主调度台成功载入:{task.name} ", module="调度中枢")
def disconnect_main_board(self, name: str, logs: list) -> None:
"""断开主调度台"""
"""
断开主调度台
:param name: 任务名称
:param logs: 任务日志列表
"""
logger.info(f"主调度台断开任务:{name}", module="调度中枢")
self.script_list["主调度台"].top_bar.Lable.hide()
self.script_list["主调度台"].top_bar.object.show()
@@ -191,6 +229,8 @@ class DispatchCenter(QWidget):
else:
self.script_list["主调度台"].info.log_text.text.setText("没有任务被执行")
logger.success(f"主调度台成功断开:{name}", module="调度中枢")
def update_top_bar(self):
"""更新顶栏"""
@@ -200,13 +240,13 @@ class DispatchCenter(QWidget):
self.script_list["主调度台"].top_bar.object.addItem(
(
"队列"
if info["Config"].get(info["Config"].queueSet_Name) == ""
else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}"
if info["Config"].get(info["Config"].QueueSet_Name) == ""
else f"队列 - {info["Config"].get(info["Config"].QueueSet_Name)}"
),
userData=name,
)
for name, info in Config.member_dict.items():
for name, info in Config.script_dict.items():
self.script_list["主调度台"].top_bar.object.addItem(
(
f"实例 - {info['Type']}"
@@ -218,7 +258,7 @@ class DispatchCenter(QWidget):
if len(Config.queue_dict) == 1:
self.script_list["主调度台"].top_bar.object.setCurrentIndex(0)
elif len(Config.member_dict) == 1:
elif len(Config.script_dict) == 1:
self.script_list["主调度台"].top_bar.object.setCurrentIndex(
len(Config.queue_dict)
)
@@ -238,6 +278,7 @@ class DispatchCenter(QWidget):
"Sleep": "睡眠",
"Hibernate": "休眠",
"Shutdown": "关机",
"ShutdownForce": "关机(强制)",
}
self.power_combox.currentIndexChanged.disconnect()
self.power_combox.setCurrentText(mode_book[Config.power_sign])
@@ -253,10 +294,7 @@ class DispatchCenter(QWidget):
self.power_combox.currentIndexChanged.connect(self.set_power_sign)
logger.warning("没有正在运行的任务,无法设置任务完成后动作")
MainInfoBar.push_info_bar(
"warning",
"没有正在运行的任务",
"无法设置任务完成后动作",
5000,
"warning", "没有正在运行的任务", "无法设置任务完成后动作", 5000
)
else:
@@ -264,7 +302,7 @@ class DispatchCenter(QWidget):
Config.set_power_sign(self.power_combox.currentData())
def start_multi_task(self) -> None:
"""开始任务"""
"""开始多开任务"""
# 获取所有可用的队列和实例
text_list = []
@@ -274,12 +312,12 @@ class DispatchCenter(QWidget):
continue
text_list.append(
"队列"
if info["Config"].get(info["Config"].queueSet_Name) == ""
else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}"
if info["Config"].get(info["Config"].QueueSet_Name) == ""
else f"队列 - {info["Config"].get(info["Config"].QueueSet_Name)}"
)
data_list.append(name)
for name, info in Config.member_dict.items():
for name, info in Config.script_dict.items():
if name in Config.running_list:
continue
text_list.append(
@@ -300,7 +338,9 @@ class DispatchCenter(QWidget):
if choice.exec() and choice.input[0].currentIndex() != -1:
if choice.input[0].currentData() in Config.running_list:
logger.warning(f"任务已存在:{choice.input[0].currentData()}")
logger.warning(
f"任务已存在:{choice.input[0].currentData()}", module="调度中枢"
)
MainInfoBar.push_info_bar(
"warning", "任务已存在", choice.input[0].currentData(), 5000
)
@@ -308,7 +348,9 @@ class DispatchCenter(QWidget):
if "调度队列" in choice.input[0].currentData():
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
logger.info(
f"用户添加任务:{choice.input[0].currentData()}", module="调度中枢"
)
TaskManager.add_task(
"自动代理_新调度台",
choice.input[0].currentData(),
@@ -317,11 +359,13 @@ class DispatchCenter(QWidget):
elif "脚本" in choice.input[0].currentData():
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
logger.info(
f"用户添加任务:{choice.input[0].currentData()}", module="调度中枢"
)
TaskManager.add_task(
"自动代理_新调度台",
f"自定义队列 - {choice.input[0].currentData()}",
{"Queue": {"Member_1": choice.input[0].currentData()}},
{"Queue": {"Script_0": choice.input[0].currentData()}},
)
class DispatchBox(QWidget):
@@ -384,24 +428,26 @@ class DispatchCenter(QWidget):
Layout.addWidget(self.main_button)
def start_main_task(self):
"""开始任务"""
"""从主调度台开始任务"""
if self.object.currentIndex() == -1:
logger.warning("未选择调度对象")
logger.warning("未选择调度对象", module="调度中枢")
MainInfoBar.push_info_bar(
"warning", "未选择调度对象", "请选择后再开始任务", 5000
)
return None
if self.mode.currentIndex() == -1:
logger.warning("未选择调度模式")
logger.warning("未选择调度模式", module="调度中枢")
MainInfoBar.push_info_bar(
"warning", "未选择调度模式", "请选择后再开始任务", 5000
)
return None
if self.object.currentData() in Config.running_list:
logger.warning(f"任务已存在:{self.object.currentData()}")
logger.warning(
f"任务已存在:{self.object.currentData()}", module="调度中枢"
)
MainInfoBar.push_info_bar(
"warning", "任务已存在", self.object.currentData(), 5000
)
@@ -409,11 +455,11 @@ class DispatchCenter(QWidget):
if (
"脚本" in self.object.currentData()
and Config.member_dict[self.object.currentData()]["Type"]
and Config.script_dict[self.object.currentData()]["Type"]
== "General"
and self.mode.currentData() == "人工排查"
):
logger.warning("通用脚本类型不存在人工排查功能")
logger.warning("通用脚本类型不存在人工排查功能", module="调度中枢")
MainInfoBar.push_info_bar(
"warning", "不支持的任务", "通用脚本无人工排查功能", 5000
)
@@ -421,7 +467,9 @@ class DispatchCenter(QWidget):
if "调度队列" in self.object.currentData():
logger.info(f"用户添加任务:{self.object.currentData()}")
logger.info(
f"用户添加任务:{self.object.currentData()}", module="调度中枢"
)
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台",
self.object.currentData(),
@@ -430,11 +478,13 @@ class DispatchCenter(QWidget):
elif "脚本" in self.object.currentData():
logger.info(f"用户添加任务:{self.object.currentData()}")
logger.info(
f"用户添加任务:{self.object.currentData()}", module="调度中枢"
)
TaskManager.add_task(
f"{self.mode.currentText()}_主调度台",
"自定义队列",
{"Queue": {"Member_1": self.object.currentData()}},
{"Queue": {"Script_0": self.object.currentData()}},
)
class DispatchInfoCard(HeaderCardWidget):
@@ -476,7 +526,11 @@ class DispatchCenter(QWidget):
self.task_cards: List[StatefulItemCard] = []
def create_task(self, task_list: list):
"""创建任务队列"""
"""
创建任务队列
:param task_list: 包含任务信息的任务列表
"""
while self.Layout.count() > 0:
item = self.Layout.takeAt(0)
@@ -495,7 +549,11 @@ class DispatchCenter(QWidget):
self.Layout.addStretch(1)
def update_task(self, task_list: list):
"""更新任务队列"""
"""
更新任务队列信息
:param task_list: 包含任务信息的任务列表
"""
for i in range(len(task_list)):
@@ -514,7 +572,11 @@ class DispatchCenter(QWidget):
self.user_cards: List[StatefulItemCard] = []
def create_user(self, user_list: list):
"""创建用户队列"""
"""
创建用户队列
:param user_list: 包含用户信息的用户列表
"""
while self.Layout.count() > 0:
item = self.Layout.takeAt(0)
@@ -533,7 +595,11 @@ class DispatchCenter(QWidget):
self.Layout.addStretch(1)
def update_user(self, user_list: list):
"""更新用户队列"""
"""
更新用户队列信息
:param user_list: 包含用户信息的用户列表
"""
for i in range(len(user_list)):

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
import zipfile
import requests
import subprocess
@@ -46,7 +45,7 @@ from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop
from typing import List, Dict, Union
from app.core import Config
from app.core import Config, logger
from app.services import System
@@ -83,6 +82,8 @@ class DownloadProcess(QThread):
self.setObjectName(f"DownloadProcess-{url}-{start_byte}-{end_byte}")
logger.info(f"创建下载子线程:{self.objectName()}", module="下载子线程")
self.url = url
self.start_byte = start_byte
self.end_byte = end_byte
@@ -97,7 +98,8 @@ class DownloadProcess(QThread):
self.download_path.unlink()
logger.info(
f"开始下载:{self.url},范围:{self.start_byte}-{self.end_byte},存储地址:{self.download_path}"
f"开始下载:{self.url},范围:{self.start_byte}-{self.end_byte},存储地址:{self.download_path}",
module="下载子线程",
)
headers = (
@@ -129,13 +131,17 @@ class DownloadProcess(QThread):
self.check_times -= 1
logger.error(
f"连接失败:{self.url},状态码:{response.status_code},剩余重试次数:{self.check_times}"
f"连接失败:{self.url},状态码:{response.status_code},剩余重试次数:{self.check_times}",
module="下载子线程",
)
time.sleep(1)
continue
logger.info(f"连接成功:{self.url},状态码:{response.status_code}")
logger.info(
f"连接成功:{self.url},状态码:{response.status_code}",
module="下载子线程",
)
downloaded_size = 0
with self.download_path.open(mode="wb") as f:
@@ -155,13 +161,14 @@ class DownloadProcess(QThread):
if self.download_path.exists():
self.download_path.unlink()
self.accomplish.emit(0)
logger.info(f"下载中止:{self.url}")
logger.info(f"下载中止:{self.url}", module="下载子线程")
else:
self.accomplish.emit(time.time() - start_time)
logger.success(
f"下载完成:{self.url},实际下载大小:{downloaded_size} 字节,耗时:{time.time() - start_time:.2f}"
f"下载完成:{self.url},实际下载大小:{downloaded_size} 字节,耗时:{time.time() - start_time:.2f}",
module="下载子线程",
)
break
@@ -172,7 +179,8 @@ class DownloadProcess(QThread):
self.check_times -= 1
logger.exception(
f"下载出错:{self.url},错误信息:{e},剩余重试次数:{self.check_times}"
f"下载出错:{self.url},错误信息:{e},剩余重试次数:{self.check_times}",
module="下载子线程",
)
time.sleep(1)
@@ -181,7 +189,7 @@ class DownloadProcess(QThread):
if self.download_path.exists():
self.download_path.unlink()
self.accomplish.emit(0)
logger.error(f"下载失败:{self.url}")
logger.error(f"下载失败:{self.url}", module="下载子线程")
class ZipExtractProcess(QThread):
@@ -195,6 +203,8 @@ class ZipExtractProcess(QThread):
self.setObjectName(f"ZipExtractProcess-{name}")
logger.info(f"创建解压子线程:{self.objectName()}", module="解压子线程")
self.name = name
self.app_path = app_path
self.download_path = download_path
@@ -204,7 +214,10 @@ class ZipExtractProcess(QThread):
try:
logger.info(f"开始解压:{self.download_path}{self.app_path}")
logger.info(
f"开始解压:{self.download_path}{self.app_path}",
module="解压子线程",
)
while True:
@@ -215,7 +228,10 @@ class ZipExtractProcess(QThread):
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
zip_ref.extractall(self.app_path)
self.accomplish.emit()
logger.success(f"解压完成:{self.download_path}{self.app_path}")
logger.success(
f"解压完成:{self.download_path}{self.app_path}",
module="解压子线程",
)
break
except PermissionError:
if self.name == "AUTO_MAA":
@@ -223,7 +239,10 @@ class ZipExtractProcess(QThread):
System.kill_process(self.app_path / "AUTO_MAA.exe")
else:
self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭")
logger.warning(f"解压出错:{self.name}正在运行,正在等待其关闭")
logger.warning(
f"解压出错:{self.name}正在运行,正在等待其关闭",
module="解压子线程",
)
time.sleep(1)
except Exception as e:
@@ -231,7 +250,7 @@ class ZipExtractProcess(QThread):
e = str(e)
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
self.info.emit(f"解压更新时出错:\n{e}")
logger.exception(f"解压更新时出错:{e}")
logger.exception(f"解压更新时出错:{e}", module="解压子线程")
return None
@@ -277,17 +296,27 @@ class DownloadManager(QDialog):
def run(self) -> None:
logger.info(
f"开始执行下载任务:{self.name},版本:{version_text(self.version)}",
module="下载管理器",
)
if self.name == "AUTO_MAA":
if self.config["mode"] == "Proxy":
self.test_speed_task1()
self.speed_test_accomplish.connect(self.download_task1)
self.start_test_speed()
self.speed_test_accomplish.connect(self.start_download)
elif self.config["mode"] == "MirrorChyan":
self.download_task1()
self.start_download()
elif self.config["mode"] == "MirrorChyan":
self.download_task1()
self.start_download()
def get_download_url(self, mode: str) -> Union[str, Dict[str, str]]:
"""获取下载链接"""
"""
生成下载链接
:param mode: "测速""下载"
:return: 测速模式返回 url 字典,下载模式返回 url 字符串
"""
url_dict = {}
@@ -362,7 +391,8 @@ class DownloadManager(QDialog):
if response.status_code == 200:
return response.url
def test_speed_task1(self) -> None:
def start_test_speed(self) -> None:
"""启动测速任务下载4MB文件以测试下载速度"""
if self.isInterruptionRequested:
return None
@@ -370,7 +400,9 @@ class DownloadManager(QDialog):
url_dict = self.get_download_url("测速")
self.test_speed_result: Dict[str, float] = {}
logger.info(f"测速链接:{url_dict}")
logger.info(
f"开始测速任务,链接:{list(url_dict.items())}", module="下载管理器"
)
for name, url in url_dict.items():
@@ -387,10 +419,11 @@ class DownloadManager(QDialog):
)
self.test_speed_result[name] = -1
self.download_process_dict[name].accomplish.connect(
partial(self.test_speed_task2, name)
partial(self.check_test_speed, name)
)
self.download_process_dict[name].start()
# 创建防超时定时器30秒后强制停止测速
timer = QTimer(self)
timer.setSingleShot(True)
timer.timeout.connect(partial(self.kill_speed_test, name))
@@ -401,11 +434,22 @@ class DownloadManager(QDialog):
self.update_progress(0, 1, 0)
def kill_speed_test(self, name: str) -> None:
"""
强制停止测速任务
:param name: 测速任务的名称
"""
if name in self.download_process_dict:
self.download_process_dict[name].requestInterruption()
def test_speed_task2(self, name: str, t: float) -> None:
def check_test_speed(self, name: str, t: float) -> None:
"""
更新测速子任务wc信息并检查测速任务是否允许结束
:param name: 测速任务的名称
:param t: 测速任务的耗时
"""
# 计算下载速度
if self.isInterruptionRequested:
@@ -453,12 +497,16 @@ class DownloadManager(QDialog):
# 保存测速结果
self.config["speed_result"] = self.test_speed_result
logger.info(f"测速结果:{self.test_speed_result}")
logger.success(
f"测速完成,结果:{list(self.test_speed_result.items())}",
module="下载管理器",
)
self.update_info("测速完成!")
self.speed_test_accomplish.emit()
def download_task1(self) -> None:
def start_download(self) -> None:
"""开始下载任务"""
if self.isInterruptionRequested:
return None
@@ -466,6 +514,8 @@ class DownloadManager(QDialog):
url = self.get_download_url("下载")
self.downloaded_size_list: List[List[int, bool]] = []
logger.info(f"开始下载任务,链接:{url}", module="下载管理器")
response = requests.head(
url,
timeout=10,
@@ -506,20 +556,27 @@ class DownloadManager(QDialog):
)
self.downloaded_size_list.append([0, False])
self.download_process_dict[f"part{i}"].progress.connect(
partial(self.download_task2, i)
partial(self.update_download, i)
)
self.download_process_dict[f"part{i}"].accomplish.connect(
partial(self.download_task3, i)
partial(self.check_download, i)
)
self.download_process_dict[f"part{i}"].start()
def download_task2(self, index: str, current: int) -> None:
"""更新下载进度"""
def update_download(self, index: str, current: int) -> None:
"""
更新子任务下载进度,将信息更新到 UI 上
:param index: 下载任务的索引
:param current: 当前下载大小
"""
# 更新指定线程的下载进度
self.downloaded_size_list[index][0] = current
self.downloaded_size = sum([_[0] for _ in self.downloaded_size_list])
self.update_progress(0, self.file_size, self.downloaded_size)
# 速度每秒更新一次
if time.time() - self.last_time >= 1.0:
self.speed = (
(self.downloaded_size - self.last_download_size)
@@ -538,7 +595,13 @@ class DownloadManager(QDialog):
f"正在下载:{self.name} 已下载:{self.downloaded_size / 1048576:.2f}/{self.file_size / 1048576:.2f} MB {self.downloaded_size / self.file_size * 100:.2f}% 下载速度:{self.speed:.2f} KB/s",
)
def download_task3(self, index: str, t: float) -> None:
def check_download(self, index: str, t: float) -> None:
"""
更新下载子任务完成信息,检查下载任务是否完成,完成后自动执行后续处理任务
:param index: 下载任务的索引
:param t: 下载任务的耗时
"""
# 标记下载线程完成
self.downloaded_size_list[index][1] = True
@@ -560,7 +623,8 @@ class DownloadManager(QDialog):
# 合并下载的分段文件
logger.info(
f"所有分段下载完成:{self.name},开始合并分段文件到 {self.download_path}"
f"所有分段下载完成:{self.name},开始合并分段文件到 {self.download_path}",
module="下载管理器",
)
with self.download_path.open(mode="wb") as outfile:
for i in range(self.config["thread_numb"]):
@@ -571,7 +635,8 @@ class DownloadManager(QDialog):
self.download_path.with_suffix(f".part{i}").unlink()
logger.success(
f"合并完成:{self.name},下载文件大小:{self.download_path.stat().st_size} 字节"
f"合并完成:{self.name},下载文件大小:{self.download_path.stat().st_size} 字节",
module="下载管理器",
)
self.update_info("正在解压更新文件")
@@ -610,9 +675,21 @@ class DownloadManager(QDialog):
self.download_accomplish.emit()
def update_info(self, text: str) -> None:
"""
更新信息文本
:param text: 要显示的信息文本
"""
self.info.setText(text)
def update_progress(self, begin: int, end: int, current: int) -> None:
"""
更新进度条
:param begin: 进度条起始值
:param end: 进度条结束值
:param current: 进度条当前值
"""
if begin == 0 and end == 0:
self.progress_2.setVisible(False)
@@ -626,7 +703,7 @@ class DownloadManager(QDialog):
def requestInterruption(self) -> None:
"""请求中断下载任务"""
logger.info("收到下载任务中止请求")
logger.info("收到下载任务中止请求", module="下载管理器")
self.isInterruptionRequested = True

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
@@ -51,11 +50,12 @@ from pathlib import Path
from typing import List, Dict
from app.core import Config, SoundPlayer
from app.core import Config, SoundPlayer, logger
from .Widget import StatefulItemCard, QuantifiedItemCard, QuickExpandGroupCard
class History(QWidget):
"""历史记录界面"""
def __init__(self, parent=None):
super().__init__(parent)
@@ -81,10 +81,21 @@ class History(QWidget):
self.history_card_list = []
def reload_history(self, mode: str, start_date: QDate, end_date: QDate) -> None:
"""加载历史记录界面"""
"""
加载历史记录界面
:param mode: 查询模式
:param start_date: 查询范围起始日期
:param end_date: 查询范围结束日期
"""
logger.info(
f"查询历史记录: {mode}, {start_date.toString()}, {end_date.toString()}",
module="历史记录",
)
SoundPlayer.play("历史记录查询")
# 清空已有的历史记录卡片
while self.content_layout.count() > 0:
item = self.content_layout.takeAt(0)
if item.spacerItem():
@@ -100,6 +111,7 @@ class History(QWidget):
datetime(end_date.year(), end_date.month(), end_date.day()),
)
# 生成历史记录卡片并添加到布局中
for date, user_dict in history_dict.items():
self.history_card_list.append(self.HistoryCard(date, user_dict, self))
@@ -154,7 +166,13 @@ class History(QWidget):
Layout.addWidget(self.search)
def select_date(self, date: str) -> None:
"""选中最近一段时间并启动查询"""
"""
选中最近一段时间并启动查询
:param date: 选择的时间段("week""month"
"""
logger.info(f"选择最近{date}的记录并开始查询", module="历史记录")
server_date = Config.server_date()
if date == "week":
@@ -187,6 +205,7 @@ class History(QWidget):
self.user_history_card_list = []
# 生成用户历史记录卡片并添加到布局中
for user, info in user_dict.items():
self.user_history_card_list.append(
self.UserHistoryCard(user, info, self)
@@ -219,7 +238,12 @@ class History(QWidget):
self.update_info("数据总览")
def get_statistics(self, mode: str) -> dict:
"""生成GUI相应结构化统计数据"""
"""
生成GUI相应结构化统计数据
:param mode: 查询模式
:return: 结构化统计数据
"""
history_info = Config.merge_statistic_info(
self.user_history if mode == "数据总览" else [Path(mode)]
@@ -244,7 +268,11 @@ class History(QWidget):
return statistics_info
def update_info(self, index: str) -> None:
"""更新信息"""
"""
更新信息到UI界面
:param index: 选择的索引
"""
# 移除已有统计信息UI组件
while self.statistics_card.count() > 0:
@@ -254,8 +282,10 @@ class History(QWidget):
elif item.widget():
item.widget().deleteLater()
# 统计信息上传至 UI
if index == "数据总览":
# 生成数据统计信息卡片组
for name, item_list in self.get_statistics("数据总览").items():
statistics_card = self.StatisticsCard(name, item_list, self)
@@ -268,10 +298,12 @@ class History(QWidget):
single_history = self.get_statistics(index)
log_path = Path(index).with_suffix(".log")
# 生成单个历史记录的统计信息卡片组
for name, item_list in single_history.items():
statistics_card = self.StatisticsCard(name, item_list, self)
self.statistics_card.addWidget(statistics_card)
# 显示日志信息并绑定点击事件
with log_path.open("r", encoding="utf-8") as f:
log = f.read()
@@ -291,6 +323,7 @@ class History(QWidget):
self.setMinimumHeight(300)
class IndexCard(HeaderCardWidget):
"""历史记录索引卡片组"""
index_changed = Signal(str)
@@ -304,9 +337,11 @@ class History(QWidget):
self.index_cards: List[StatefulItemCard] = []
# 生成索引卡片信息
index_list = Config.merge_statistic_info(history_list)["index"]
index_list.insert(0, ["数据总览", "运行", "数据总览"])
# 生成索引卡片组件并绑定点击事件
for index in index_list:
self.index_cards.append(StatefulItemCard(index[:2]))
@@ -318,6 +353,7 @@ class History(QWidget):
self.Layout.addStretch(1)
class StatisticsCard(HeaderCardWidget):
"""历史记录统计信息卡片组"""
def __init__(self, name: str, item_list: list, parent=None):
super().__init__(parent)
@@ -340,6 +376,7 @@ class History(QWidget):
self.Layout.addStretch(1)
class LogCard(HeaderCardWidget):
"""历史记录日志卡片"""
def __init__(self, parent=None):
super().__init__(parent)

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
@@ -49,7 +48,7 @@ import json
from datetime import datetime
from pathlib import Path
from app.core import Config, MainInfoBar, Network
from app.core import Config, MainInfoBar, Network, logger
from .Widget import Banner, IconButton
@@ -160,8 +159,12 @@ class Home(QWidget):
def get_home_image(self) -> None:
"""获取主页图片"""
logger.info("获取主页图片", module="主页")
if Config.get(Config.function_HomeImageMode) == "默认":
pass
logger.info("使用默认主页图片", module="主页")
elif Config.get(Config.function_HomeImageMode) == "自定义":
file_path, _ = QFileDialog.getOpenFileName(
@@ -180,7 +183,7 @@ class Home(QWidget):
/ f"resources/images/Home/BannerCustomize{Path(file_path).suffix}",
)
logger.info(f"自定义主页图片更换成功:{file_path}")
logger.info(f"自定义主页图片更换成功:{file_path}", module="主页")
MainInfoBar.push_info_bar(
"success",
"主页图片更换成功",
@@ -189,7 +192,7 @@ class Home(QWidget):
)
else:
logger.warning("自定义主页图片更换失败:未选择图片文件")
logger.warning("自定义主页图片更换失败:未选择图片文件", module="主页")
MainInfoBar.push_info_bar(
"warning",
"主页图片更换失败",
@@ -198,10 +201,10 @@ class Home(QWidget):
)
elif Config.get(Config.function_HomeImageMode) == "主题图像":
# 从远程服务器获取最新主题图像
# 从远程服务器获取最新主题图像信息
network = Network.add_task(
mode="get",
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json",
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/theme_image.json",
)
network.loop.exec()
network_result = Network.get_result(network)
@@ -209,7 +212,8 @@ class Home(QWidget):
theme_image = network_result["response_json"]
else:
logger.warning(
f"获取最新主题图像时出错:{network_result['error_message']}"
f"获取最新主题图像时出错:{network_result['error_message']}",
module="主页",
)
MainInfoBar.push_info_bar(
"warning",
@@ -230,6 +234,7 @@ class Home(QWidget):
else:
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
# 检查主题图像是否需要更新
if not (
Config.app_path / "resources/images/Home/BannerTheme.jpg"
).exists() or (
@@ -253,7 +258,9 @@ class Home(QWidget):
) as f:
json.dump(theme_image, f, ensure_ascii=False, indent=4)
logger.success(f"主题图像「{theme_image["name"]}」下载成功")
logger.success(
f"主题图像「{theme_image["name"]}」下载成功", module="主页"
)
MainInfoBar.push_info_bar(
"success",
"主题图像下载成功",
@@ -264,7 +271,8 @@ class Home(QWidget):
else:
logger.warning(
f"下载最新主题图像时出错:{network_result['error_message']}"
f"下载最新主题图像时出错:{network_result['error_message']}",
module="主页",
)
MainInfoBar.push_info_bar(
"warning",
@@ -275,18 +283,16 @@ class Home(QWidget):
else:
logger.info("主题图像已是最新")
logger.info("主题图像已是最新", module="主页")
MainInfoBar.push_info_bar(
"info",
"主题图像已是最新",
"主题图像已是最新!",
3000,
"info", "主题图像已是最新", "主题图像已是最新!", 3000
)
self.set_banner()
def set_banner(self):
"""设置主页图像"""
if Config.get(Config.function_HomeImageMode) == "默认":
self.banner.set_banner_image(
str(Config.app_path / "resources/images/Home/BannerDefault.png")
@@ -366,7 +372,7 @@ class ButtonGroup(SimpleCardWidget):
doc_button = IconButton(
FluentIcon.CHAT.icon(color=QColor("#fff")),
tip_title="官方社群",
tip_content="加入官方群聊AUTO_MAA绝赞DeBug中",
tip_content="加入官方群聊AUTO_MAA绝赞DeBug中",
isTooltip=True,
)
doc_button.setIconSize(QSize(32, 32))

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
from qfluentwidgets import (
Action,
@@ -41,14 +40,12 @@ from qfluentwidgets import (
)
from PySide6.QtGui import QIcon, QCloseEvent
from PySide6.QtCore import QTimer
from datetime import datetime, timedelta
import shutil
import darkdetect
from app.core import Config, TaskManager, MainTimer, MainInfoBar, SoundPlayer
from app.core import Config, logger, TaskManager, MainTimer, MainInfoBar, SoundPlayer
from app.services import Notify, Crypto, System
from .home import Home
from .member_manager import MemberManager
from .script_manager import ScriptManager
from .plan_manager import PlanManager
from .queue_manager import QueueManager
from .dispatch_center import DispatchCenter
@@ -57,6 +54,7 @@ from .setting import Setting
class AUTO_MAA(MSFluentWindow):
"""AUTO_MAA主界面"""
def __init__(self):
super().__init__()
@@ -77,12 +75,14 @@ class AUTO_MAA(MSFluentWindow):
self.splashScreen = SplashScreen(self.windowIcon(), self)
self.show_ui("显示主窗口", if_quick=True)
# 设置主窗口的引用,便于各组件访问
Config.main_window = self.window()
# 创建窗口
# 创建各子窗口
logger.info("正在创建各子窗口", module="主窗口")
self.home = Home(self)
self.plan_manager = PlanManager(self)
self.member_manager = MemberManager(self)
self.script_manager = ScriptManager(self)
self.queue_manager = QueueManager(self)
self.dispatch_center = DispatchCenter(self)
self.history = History(self)
@@ -96,7 +96,7 @@ class AUTO_MAA(MSFluentWindow):
NavigationItemPosition.TOP,
)
self.addSubInterface(
self.member_manager,
self.script_manager,
FluentIcon.ROBOT,
"脚本管理",
FluentIcon.ROBOT,
@@ -138,8 +138,10 @@ class AUTO_MAA(MSFluentWindow):
NavigationItemPosition.BOTTOM,
)
self.stackedWidget.currentChanged.connect(self.__currentChanged)
logger.success("各子窗口创建完成", module="主窗口")
# 创建系统托盘及其菜单
logger.info("正在创建系统托盘", module="主窗口")
self.tray = QSystemTrayIcon(
QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), self
)
@@ -159,7 +161,7 @@ class AUTO_MAA(MSFluentWindow):
# 开始任务菜单项
self.tray_menu.addActions(
[
Action(FluentIcon.PLAY, "运行自动代理", triggered=self.start_main_task),
Action(FluentIcon.PLAY, "运行启动时队列", triggered=self.start_up_task),
Action(
FluentIcon.PAUSE,
"中止所有任务",
@@ -181,10 +183,12 @@ class AUTO_MAA(MSFluentWindow):
# 设置托盘菜单
self.tray.setContextMenu(self.tray_menu)
self.tray.activated.connect(self.on_tray_activated)
logger.success("系统托盘创建完成", module="主窗口")
self.set_min_method()
Config.sub_info_changed.connect(self.member_manager.refresh_dashboard)
# 绑定各组件信号
Config.sub_info_changed.connect(self.script_manager.refresh_dashboard)
Config.power_sign_changed.connect(self.dispatch_center.update_power_sign)
TaskManager.create_gui.connect(self.dispatch_center.add_board)
TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board)
@@ -205,6 +209,8 @@ class AUTO_MAA(MSFluentWindow):
self.themeListener.systemThemeChanged.connect(self.switch_theme)
self.themeListener.start()
logger.success("AUTO_MAA主程序初始化完成", module="主窗口")
def switch_theme(self) -> None:
"""切换主题"""
@@ -348,8 +354,10 @@ class AUTO_MAA(MSFluentWindow):
def start_up_task(self) -> None:
"""启动时任务"""
# 清理旧日志
self.clean_old_logs()
logger.info("开始执行启动时任务", module="主窗口")
# 清理旧历史记录
Config.clean_old_history()
# 清理安装包
if (Config.app_path / "AUTO_MAA-Setup.exe").exists():
@@ -358,6 +366,15 @@ class AUTO_MAA(MSFluentWindow):
except Exception:
pass
# 恢复Go_Updater独立更新器
if (Config.app_path / "AUTO_MAA_Go_Updater_install.exe").exists():
try:
(Config.app_path / "AUTO_MAA_Go_Updater_install.exe").rename(
"AUTO_MAA_Go_Updater.exe"
)
except Exception:
pass
# 检查密码
self.setting.check_PASSWORD()
@@ -368,10 +385,8 @@ class AUTO_MAA(MSFluentWindow):
if Config.get(Config.function_HomeImageMode) == "主题图像":
self.home.get_home_image()
# 直接运行主任务
if Config.get(Config.start_IfRunDirectly):
self.start_main_task()
# 启动定时器
MainTimer.start()
# 获取公告
self.setting.show_notice(if_first=True)
@@ -395,16 +410,16 @@ class AUTO_MAA(MSFluentWindow):
Config.queue_dict["调度队列_1"]["Config"].toDict(),
)
for config in [_ for _ in Config.args.config if _ in Config.member_dict]:
for config in [_ for _ in Config.args.config if _ in Config.script_dict]:
TaskManager.add_task(
"自动代理_新调度台",
"自定义队列",
{"Queue": {"Member_1": config}},
{"Queue": {"Script_0": config}},
)
if not any(
_ in (list(Config.member_dict.keys()) + list(Config.queue_dict.keys()))
_ in (list(Config.script_dict.keys()) + list(Config.queue_dict.keys()))
for _ in Config.args.config
):
@@ -420,68 +435,35 @@ class AUTO_MAA(MSFluentWindow):
)
System.set_power("KillSelf")
def clean_old_logs(self):
"""
删除超过用户设定天数的日志文件(基于目录日期)
"""
elif Config.args.mode == "gui":
if Config.get(Config.function_HistoryRetentionTime) == 0:
logger.info("由于用户设置日志永久保留,跳过日志清理")
return
self.start_up_queue()
deleted_count = 0
logger.success("启动时任务执行完成", module="主窗口")
for date_folder in (Config.app_path / "history").iterdir():
if not date_folder.is_dir():
continue # 只处理日期文件夹
def start_up_queue(self) -> None:
"""启动时运行的调度队列"""
try:
# 只检查 `YYYY-MM-DD` 格式的文件夹
folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d")
if datetime.now() - folder_date > timedelta(
days=Config.get(Config.function_HistoryRetentionTime)
):
shutil.rmtree(date_folder, ignore_errors=True)
deleted_count += 1
logger.info(f"已删除超期日志目录: {date_folder}")
except ValueError:
logger.warning(f"非日期格式的目录: {date_folder}")
logger.info("开始调度启动时运行的调度队列", module="主窗口")
logger.info(f"清理完成: {deleted_count} 个日期目录")
for name, queue in Config.queue_dict.items():
def start_main_task(self) -> None:
"""启动主任务"""
if queue["Config"].get(queue["Config"].QueueSet_StartUpEnabled):
if "调度队列_1" in Config.queue_dict:
logger.info(f"自动添加任务:{name}", module="主窗口")
TaskManager.add_task(
"自动代理_新调度台", name, queue["Config"].toDict()
)
logger.info("自动添加任务调度队列_1")
TaskManager.add_task(
"自动代理_主调度台",
"调度队列_1",
Config.queue_dict["调度队列_1"]["Config"].toDict(),
)
elif "脚本_1" in Config.member_dict:
logger.info("自动添加任务脚本_1")
TaskManager.add_task(
"自动代理_主调度台", "自定义队列", {"Queue": {"Member_1": "脚本_1"}}
)
else:
logger.warning("启动主任务失败:未找到有效的主任务配置文件")
MainInfoBar.push_info_bar(
"warning", "启动主任务失败", "「调度队列_1」与「脚本_1」均不存在", -1
)
logger.success("开始调度启动时运行的调度队列启动完成", module="主窗口")
def __currentChanged(self, index: int) -> None:
"""切换界面时任务"""
if index == 1:
self.member_manager.reload_plan_name()
self.script_manager.reload_plan_name()
elif index == 3:
self.queue_manager.reload_member_name()
self.queue_manager.reload_script_name()
elif index == 4:
self.dispatch_center.pivot.setCurrentItem("主调度台")
self.dispatch_center.update_top_bar()
@@ -489,20 +471,18 @@ class AUTO_MAA(MSFluentWindow):
def closeEvent(self, event: QCloseEvent):
"""清理残余进程"""
logger.info("保存窗口位置与大小信息", module="主窗口")
self.show_ui("隐藏到托盘", if_quick=True)
# 清理各功能线程
MainTimer.Timer.stop()
MainTimer.Timer.deleteLater()
MainTimer.LongTimer.stop()
MainTimer.LongTimer.deleteLater()
MainTimer.stop()
TaskManager.stop_task("ALL")
# 关闭主题监听
self.themeListener.terminate()
self.themeListener.deleteLater()
logger.info("AUTO_MAA主程序关闭")
logger.info("----------------END----------------")
logger.info("AUTO_MAA主程序关闭", module="主窗口")
logger.info("----------------END----------------", module="主窗口")
event.accept()

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
@@ -43,7 +42,7 @@ from qfluentwidgets import (
from typing import List, Dict, Union
import shutil
from app.core import Config, MainInfoBar, MaaPlanConfig, SoundPlayer
from app.core import Config, MainInfoBar, MaaPlanConfig, SoundPlayer, logger
from .Widget import (
ComboBoxMessageBox,
LineEditSettingCard,
@@ -66,27 +65,20 @@ class PlanManager(QWidget):
layout = QVBoxLayout(self)
self.tools = CommandBar()
self.plan_manager = self.PlanSettingBox(self)
# 逐个添加动作
self.tools.addActions(
[
Action(FluentIcon.ADD_TO, "新建计划表", triggered=self.add_setting_box),
Action(
FluentIcon.REMOVE_FROM, "删除计划表", triggered=self.del_setting_box
),
Action(FluentIcon.ADD_TO, "新建计划表", triggered=self.add_plan),
Action(FluentIcon.REMOVE_FROM, "删除计划表", triggered=self.del_plan),
]
)
self.tools.addSeparator()
self.tools.addActions(
[
Action(
FluentIcon.LEFT_ARROW, "移动", triggered=self.left_setting_box
),
Action(
FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box
),
Action(FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_plan),
Action(FluentIcon.RIGHT_ARROW, "移动", triggered=self.right_plan),
]
)
self.tools.addSeparator()
@@ -94,7 +86,7 @@ class PlanManager(QWidget):
layout.addWidget(self.tools)
layout.addWidget(self.plan_manager)
def add_setting_box(self):
def add_plan(self):
"""添加一个计划表"""
choice = ComboBoxMessageBox(
@@ -109,6 +101,7 @@ class PlanManager(QWidget):
index = len(Config.plan_dict) + 1
# 初始化 MaaPlanConfig
maa_plan_config = MaaPlanConfig()
maa_plan_config.load(
Config.app_path / f"config/MaaPlanConfig/计划_{index}/config.json",
@@ -122,29 +115,30 @@ class PlanManager(QWidget):
"Config": maa_plan_config,
}
# 添加计划表到界面
self.plan_manager.add_MaaPlanSettingBox(index)
self.plan_manager.switch_SettingBox(index)
logger.success(f"计划管理 计划_{index} 添加成功")
logger.success(f"计划管理 计划_{index} 添加成功", module="计划管理")
MainInfoBar.push_info_bar(
"success", "操作成功", f"添加计划表 计划_{index}", 3000
)
SoundPlayer.play("添加计划表")
def del_setting_box(self):
def del_plan(self):
"""删除一个计划表"""
name = self.plan_manager.pivot.currentRouteKey()
if name is None:
logger.warning("删除计划表时未选择计划表")
logger.warning("删除计划表时未选择计划表", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "未选择计划表", "请选择一个计划表", 5000
)
return None
if len(Config.running_list) > 0:
logger.warning("删除计划表时调度队列未停止运行")
logger.warning("删除计划表时调度队列未停止运行", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000
)
@@ -153,8 +147,11 @@ class PlanManager(QWidget):
choice = MessageBox("确认", f"确定要删除 {name} 吗?", self.window())
if choice.exec():
logger.info(f"正在删除计划表 {name}", module="计划管理")
self.plan_manager.clear_SettingBox()
# 删除计划表配置文件并同步到相关配置项
shutil.rmtree(Config.plan_dict[name]["Path"])
Config.change_plan(name, "固定")
for i in range(int(name[3:]) + 1, len(Config.plan_dict) + 1):
@@ -166,17 +163,17 @@ class PlanManager(QWidget):
self.plan_manager.show_SettingBox(max(int(name[3:]) - 1, 1))
logger.success(f"计划表 {name} 删除成功")
logger.success(f"计划表 {name} 删除成功", module="计划管理")
MainInfoBar.push_info_bar("success", "操作成功", f"删除计划表 {name}", 3000)
SoundPlayer.play("删除计划表")
def left_setting_box(self):
def left_plan(self):
"""向左移动计划表"""
name = self.plan_manager.pivot.currentRouteKey()
if name is None:
logger.warning("向左移动计划表时未选择计划表")
logger.warning("向左移动计划表时未选择计划表", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "未选择计划表", "请选择一个计划表", 5000
)
@@ -185,21 +182,24 @@ class PlanManager(QWidget):
index = int(name[3:])
if index == 1:
logger.warning("向左移动计划表时已到达最左端")
logger.warning("向左移动计划表时已到达最左端", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "已经是第一个计划表", "无法向左移动", 5000
)
return None
if len(Config.running_list) > 0:
logger.warning("向左移动计划表时调度队列未停止运行")
logger.warning("向左移动计划表时调度队列未停止运行", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000
)
return None
logger.info(f"正在左移计划表 {name}", module="计划管理")
self.plan_manager.clear_SettingBox()
# 移动配置文件并同步到相关配置项
Config.plan_dict[name]["Path"].rename(
Config.plan_dict[name]["Path"].with_name("计划_0")
)
@@ -215,16 +215,16 @@ class PlanManager(QWidget):
self.plan_manager.show_SettingBox(index - 1)
logger.success(f"计划表 {name} 左移成功")
logger.success(f"计划表 {name} 左移成功", module="计划管理")
MainInfoBar.push_info_bar("success", "操作成功", f"左移计划表 {name}", 3000)
def right_setting_box(self):
def right_plan(self):
"""向右移动计划表"""
name = self.plan_manager.pivot.currentRouteKey()
if name is None:
logger.warning("向右移动计划表时未选择计划表")
logger.warning("向右移动计划表时未选择计划表", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "未选择计划表", "请选择一个计划表", 5000
)
@@ -233,21 +233,24 @@ class PlanManager(QWidget):
index = int(name[3:])
if index == len(Config.plan_dict):
logger.warning("向右移动计划表时已到达最右端")
logger.warning("向右移动计划表时已到达最右端", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "已经是最后一个计划表", "无法向右移动", 5000
)
return None
if len(Config.running_list) > 0:
logger.warning("向右移动计划表时调度队列未停止运行")
logger.warning("向右移动计划表时调度队列未停止运行", module="计划管理")
MainInfoBar.push_info_bar(
"warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000
)
return None
logger.info(f"正在右移计划表 {name}", module="计划管理")
self.plan_manager.clear_SettingBox()
# 移动配置文件并同步到相关配置项
Config.plan_dict[name]["Path"].rename(
Config.plan_dict[name]["Path"].with_name("计划_0")
)
@@ -263,7 +266,7 @@ class PlanManager(QWidget):
self.plan_manager.show_SettingBox(index + 1)
logger.success(f"计划表 {name} 右移成功")
logger.success(f"计划表 {name} 右移成功", module="计划管理")
MainInfoBar.push_info_bar("success", "操作成功", f"右移计划表 {name}", 3000)
class PlanSettingBox(QWidget):
@@ -297,7 +300,11 @@ class PlanManager(QWidget):
self.show_SettingBox(1)
def show_SettingBox(self, index) -> None:
"""加载所有子界面"""
"""
加载所有子界面并切换到指定的子界面
:param index: 要显示的子界面索引
"""
Config.search_plan()
@@ -308,7 +315,12 @@ class PlanManager(QWidget):
self.switch_SettingBox(index)
def switch_SettingBox(self, index: int, if_chang_pivot: bool = True) -> None:
"""切换到指定的子界面"""
"""
切换到指定的子界面
:param index: 要切换到的子界面索引
:param if_chang_pivot: 是否更改 pivot 的当前项
"""
if len(Config.plan_dict) == 0:
return None
@@ -331,7 +343,11 @@ class PlanManager(QWidget):
self.pivot.clear()
def add_MaaPlanSettingBox(self, uid: int) -> None:
"""添加一个MAA设置界面"""
"""
添加一个MAA设置界面
:param uid: MAA计划表的唯一标识符
"""
maa_plan_setting_box = self.MaaPlanSettingBox(uid, self)
@@ -475,6 +491,7 @@ class PlanManager(QWidget):
)
def refresh_stage(self):
"""刷新关卡列表"""
for group, name_dict in self.item_dict.items():

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
@@ -40,9 +39,9 @@ from qfluentwidgets import (
HeaderCardWidget,
CommandBar,
)
from typing import List
from typing import List, Dict
from app.core import QueueConfig, Config, MainInfoBar, SoundPlayer
from app.core import QueueConfig, Config, MainInfoBar, SoundPlayer, logger
from .Widget import (
SwitchSettingCard,
ComboBoxSettingCard,
@@ -70,36 +69,31 @@ class QueueManager(QWidget):
# 逐个添加动作
self.tools.addActions(
[
Action(FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_queue),
Action(
FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_setting_box
),
Action(
FluentIcon.REMOVE_FROM,
"删除调度队列",
triggered=self.del_setting_box,
FluentIcon.REMOVE_FROM, "删除调度队列", triggered=self.del_queue
),
]
)
self.tools.addSeparator()
self.tools.addActions(
[
Action(
FluentIcon.LEFT_ARROW, "移动", triggered=self.left_setting_box
),
Action(
FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box
),
Action(FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_queue),
Action(FluentIcon.RIGHT_ARROW, "移动", triggered=self.right_queue),
]
)
layout.addWidget(self.tools)
layout.addWidget(self.queue_manager)
def add_setting_box(self):
def add_queue(self):
"""添加一个调度队列"""
index = len(Config.queue_dict) + 1
logger.info(f"正在添加调度队列_{index}", module="队列管理")
# 初始化队列配置
queue_config = QueueConfig()
queue_config.load(
Config.app_path / f"config/QueueConfig/调度队列_{index}.json", queue_config
@@ -111,27 +105,28 @@ class QueueManager(QWidget):
"Config": queue_config,
}
# 添加到配置界面
self.queue_manager.add_SettingBox(index)
self.queue_manager.switch_SettingBox(index)
logger.success(f"调度队列_{index} 添加成功")
logger.success(f"调度队列_{index} 添加成功", module="队列管理")
MainInfoBar.push_info_bar("success", "操作成功", f"添加 调度队列_{index}", 3000)
SoundPlayer.play("添加调度队列")
def del_setting_box(self):
def del_queue(self):
"""删除一个调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name is None:
logger.warning("未选择调度队列")
logger.warning("未选择调度队列", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
)
return None
if name in Config.running_list:
logger.warning("调度队列正在运行")
logger.warning("调度队列正在运行", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "调度队列正在运行", "请先停止调度队列", 5000
)
@@ -140,8 +135,11 @@ class QueueManager(QWidget):
choice = MessageBox("确认", f"确定要删除 {name} 吗?", self.window())
if choice.exec():
logger.info(f"正在删除调度队列 {name}", module="队列管理")
self.queue_manager.clear_SettingBox()
# 删除队列配置文件并同步到相关配置项
Config.queue_dict[name]["Path"].unlink()
for i in range(int(name[5:]) + 1, len(Config.queue_dict) + 1):
if Config.queue_dict[f"调度队列_{i}"]["Path"].exists():
@@ -153,17 +151,17 @@ class QueueManager(QWidget):
self.queue_manager.show_SettingBox(max(int(name[5:]) - 1, 1))
logger.success(f"{name} 删除成功")
logger.success(f"{name} 删除成功", module="队列管理")
MainInfoBar.push_info_bar("success", "操作成功", f"删除 {name}", 3000)
SoundPlayer.play("删除调度队列")
def left_setting_box(self):
def left_queue(self):
"""向左移动调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name is None:
logger.warning("未选择调度队列")
logger.warning("未选择调度队列", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
)
@@ -172,21 +170,24 @@ class QueueManager(QWidget):
index = int(name[5:])
if index == 1:
logger.warning("向左移动调度队列时已到达最左端")
logger.warning("向左移动调度队列时已到达最左端", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "已经是第一个调度队列", "无法向左移动", 5000
)
return None
if name in Config.running_list or f"调度队列_{index-1}" in Config.running_list:
logger.warning("相关调度队列正在运行")
logger.warning("相关调度队列正在运行", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
)
return None
logger.info(f"正在左移调度队列 {name}", module="队列管理")
self.queue_manager.clear_SettingBox()
# 移动配置文件并同步到相关配置项
Config.queue_dict[name]["Path"].rename(
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
)
@@ -199,16 +200,16 @@ class QueueManager(QWidget):
self.queue_manager.show_SettingBox(index - 1)
logger.success(f"{name} 左移成功")
logger.success(f"{name} 左移成功", module="队列管理")
MainInfoBar.push_info_bar("success", "操作成功", f"左移 {name}", 3000)
def right_setting_box(self):
def right_queue(self):
"""向右移动调度队列实例"""
name = self.queue_manager.pivot.currentRouteKey()
if name is None:
logger.warning("未选择调度队列")
logger.warning("未选择调度队列", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
)
@@ -217,21 +218,24 @@ class QueueManager(QWidget):
index = int(name[5:])
if index == len(Config.queue_dict):
logger.warning("向右移动调度队列时已到达最右端")
logger.warning("向右移动调度队列时已到达最右端", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "已经是最后一个调度队列", "无法向右移动", 5000
)
return None
if name in Config.running_list or f"调度队列_{index+1}" in Config.running_list:
logger.warning("相关调度队列正在运行")
logger.warning("相关调度队列正在运行", module="队列管理")
MainInfoBar.push_info_bar(
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
)
return None
logger.info(f"正在右移调度队列 {name}", module="队列管理")
self.queue_manager.clear_SettingBox()
# 移动配置文件并同步到相关配置项
Config.queue_dict[name]["Path"].rename(
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
)
@@ -244,14 +248,15 @@ class QueueManager(QWidget):
self.queue_manager.show_SettingBox(index + 1)
logger.success(f"{name} 右移成功")
logger.success(f"{name} 右移成功", module="队列管理")
MainInfoBar.push_info_bar("success", "操作成功", f"右移 {name}", 3000)
def reload_member_name(self):
"""刷新调度队列成员"""
def reload_script_name(self):
"""刷新调度队列脚本成员名称"""
member_list = [
["禁用"] + [_ for _ in Config.member_dict.keys()],
# 获取脚本成员列表
script_list = [
["禁用"] + [_ for _ in Config.script_dict.keys()],
["未启用"]
+ [
(
@@ -259,41 +264,12 @@ class QueueManager(QWidget):
if v["Config"].get_name() == ""
else f"{k} - {v["Config"].get_name()}"
)
for k, v in Config.member_dict.items()
for k, v in Config.script_dict.items()
],
]
for script in self.queue_manager.script_list:
script.task.card_Member_1.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_2.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_3.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_4.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_5.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_6.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_7.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_8.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_9.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
script.task.card_Member_10.reLoadOptions(
value=member_list[0], texts=member_list[1]
)
for card in script.task.card_dict.values():
card.reLoadOptions(value=script_list[0], texts=script_list[1])
class QueueSettingBox(QWidget):
@@ -337,7 +313,12 @@ class QueueManager(QWidget):
self.switch_SettingBox(index)
def switch_SettingBox(self, index: int, if_change_pivot: bool = True) -> None:
"""切换到指定的子界面"""
"""
切换到指定的子界面并切换到指定的子页面
:param index: 要切换到的子界面索引
:param if_change_pivot: 是否更改导航栏当前项
"""
if len(Config.queue_dict) == 0:
return None
@@ -418,15 +399,23 @@ class QueueManager(QWidget):
content="用于标识调度队列的名称",
text="请输入调度队列名称",
qconfig=self.config,
configItem=self.config.queueSet_Name,
configItem=self.config.QueueSet_Name,
parent=self,
)
self.card_Enable = SwitchSettingCard(
icon=FluentIcon.HOME,
title="状态",
content="调度队列状态,启用时会执行定时任务",
self.card_StartUpEnabled = SwitchSettingCard(
icon=FluentIcon.CHECKBOX,
title="启动时运行",
content="调度队列启动时运行状态,启用后将在软件启动时自动运行本队列",
qconfig=self.config,
configItem=self.config.queueSet_Enabled,
configItem=self.config.QueueSet_StartUpEnabled,
parent=self,
)
self.card_TimeEnable = SwitchSettingCard(
icon=FluentIcon.CHECKBOX,
title="定时运行",
content="调度队列定时运行状态,启用时会执行定时任务",
qconfig=self.config,
configItem=self.config.QueueSet_TimeEnabled,
parent=self,
)
self.card_AfterAccomplish = ComboBoxSettingCard(
@@ -439,15 +428,17 @@ class QueueManager(QWidget):
"睡眠win系统需禁用休眠",
"休眠",
"关机",
"关机(强制)",
],
qconfig=self.config,
configItem=self.config.queueSet_AfterAccomplish,
configItem=self.config.QueueSet_AfterAccomplish,
parent=self,
)
Layout = QVBoxLayout()
Layout.addWidget(self.card_Name)
Layout.addWidget(self.card_Enable)
Layout.addWidget(self.card_StartUpEnabled)
Layout.addWidget(self.card_TimeEnable)
Layout.addWidget(self.card_AfterAccomplish)
self.viewLayout.addLayout(Layout)
@@ -466,107 +457,29 @@ class QueueManager(QWidget):
Layout_2 = QVBoxLayout(widget_2)
Layout = QHBoxLayout()
self.card_Time_0 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 1",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_0,
configItem_time=self.config.time_TimeSet_0,
parent=self,
)
self.card_Time_1 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 2",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_1,
configItem_time=self.config.time_TimeSet_1,
parent=self,
)
self.card_Time_2 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 3",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_2,
configItem_time=self.config.time_TimeSet_2,
parent=self,
)
self.card_Time_3 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 4",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_3,
configItem_time=self.config.time_TimeSet_3,
parent=self,
)
self.card_Time_4 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 5",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_4,
configItem_time=self.config.time_TimeSet_4,
parent=self,
)
self.card_Time_5 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 6",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_5,
configItem_time=self.config.time_TimeSet_5,
parent=self,
)
self.card_Time_6 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 7",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_6,
configItem_time=self.config.time_TimeSet_6,
parent=self,
)
self.card_Time_7 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 8",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_7,
configItem_time=self.config.time_TimeSet_7,
parent=self,
)
self.card_Time_8 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 9",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_8,
configItem_time=self.config.time_TimeSet_8,
parent=self,
)
self.card_Time_9 = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title="定时 10",
content=None,
qconfig=self.config,
configItem_bool=self.config.time_TimeEnabled_9,
configItem_time=self.config.time_TimeSet_9,
parent=self,
)
self.card_dict: Dict[str, TimeEditSettingCard] = {}
for i in range(10):
self.card_dict[f"Time_{i}"] = TimeEditSettingCard(
icon=FluentIcon.STOP_WATCH,
title=f"定时 {i + 1}",
content=None,
qconfig=self.config,
configItem_bool=self.config.config_item_dict["Time"][
f"Enabled_{i}"
],
configItem_time=self.config.config_item_dict["Time"][
f"Set_{i}"
],
parent=self,
)
if i < 5:
Layout_1.addWidget(self.card_dict[f"Time_{i}"])
else:
Layout_2.addWidget(self.card_dict[f"Time_{i}"])
Layout_1.addWidget(self.card_Time_0)
Layout_1.addWidget(self.card_Time_1)
Layout_1.addWidget(self.card_Time_2)
Layout_1.addWidget(self.card_Time_3)
Layout_1.addWidget(self.card_Time_4)
Layout_2.addWidget(self.card_Time_5)
Layout_2.addWidget(self.card_Time_6)
Layout_2.addWidget(self.card_Time_7)
Layout_2.addWidget(self.card_Time_8)
Layout_2.addWidget(self.card_Time_9)
Layout.addWidget(widget_1)
Layout.addWidget(widget_2)
@@ -580,8 +493,8 @@ class QueueManager(QWidget):
self.setTitle("任务队列")
self.config = config
member_list = [
["禁用"] + [_ for _ in Config.member_dict.keys()],
script_list = [
["禁用"] + [_ for _ in Config.script_dict.keys()],
["未启用"]
+ [
(
@@ -589,121 +502,32 @@ class QueueManager(QWidget):
if v["Config"].get_name() == ""
else f"{k} - {v["Config"].get_name()}"
)
for k, v in Config.member_dict.items()
for k, v in Config.script_dict.items()
],
]
self.card_Member_1 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 1",
content="第一个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_1,
parent=self,
)
self.card_Member_2 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 2",
content="第二个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_2,
parent=self,
)
self.card_Member_3 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 3",
content="第三个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_3,
parent=self,
)
self.card_Member_4 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 4",
content="第四个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_4,
parent=self,
)
self.card_Member_5 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 5",
content="第五个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_5,
parent=self,
)
self.card_Member_6 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 6",
content="第六个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_6,
parent=self,
)
self.card_Member_7 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 7",
content="第七个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_7,
parent=self,
)
self.card_Member_8 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 8",
content="第八个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_8,
parent=self,
)
self.card_Member_9 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 9",
content="第九个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_9,
parent=self,
)
self.card_Member_10 = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title="任务实例 10",
content="第十个调起的脚本任务实例",
value=member_list[0],
texts=member_list[1],
qconfig=self.config,
configItem=self.config.queue_Member_10,
parent=self,
)
self.card_dict: Dict[
str,
NoOptionComboBoxSettingCard,
] = {}
Layout = QVBoxLayout()
Layout.addWidget(self.card_Member_1)
Layout.addWidget(self.card_Member_2)
Layout.addWidget(self.card_Member_3)
Layout.addWidget(self.card_Member_4)
Layout.addWidget(self.card_Member_5)
Layout.addWidget(self.card_Member_6)
Layout.addWidget(self.card_Member_7)
Layout.addWidget(self.card_Member_8)
Layout.addWidget(self.card_Member_9)
Layout.addWidget(self.card_Member_10)
for i in range(10):
self.card_dict[f"Script_{i}"] = NoOptionComboBoxSettingCard(
icon=FluentIcon.APPLICATION,
title=f"任务实例 {i + 1}",
content=f"{i + 1}个调起的脚本任务实例",
value=script_list[0],
texts=script_list[1],
qconfig=self.config,
configItem=self.config.config_item_dict["Queue"][
f"Script_{i}"
],
parent=self,
)
Layout.addWidget(self.card_dict[f"Script_{i}"])
self.viewLayout.addLayout(Layout)

File diff suppressed because it is too large Load Diff

View File

@@ -25,7 +25,6 @@ v4.4
作者DLmaster_361
"""
from loguru import logger
from PySide6.QtWidgets import QWidget, QVBoxLayout
from PySide6.QtGui import QIcon
from PySide6.QtCore import Qt
@@ -49,7 +48,7 @@ from packaging import version
from pathlib import Path
from typing import Dict, Union
from app.core import Config, MainInfoBar, Network, SoundPlayer
from app.core import Config, MainInfoBar, Network, SoundPlayer, logger
from app.services import Crypto, System, Notify
from .downloader import DownloadManager
from .Widget import (
@@ -124,7 +123,7 @@ class Setting(QWidget):
self.window(),
)
if choice.exec():
logger.success("确认授权bilibili游戏隐私政策")
logger.success("确认授权bilibili游戏隐私政策", module="设置界面")
MainInfoBar.push_info_bar(
"success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000
)
@@ -132,7 +131,7 @@ class Setting(QWidget):
Config.set(Config.function_IfAgreeBilibili, False)
else:
logger.info("取消授权bilibili游戏隐私政策")
logger.info("取消授权bilibili游戏隐私政策", module="设置界面")
MainInfoBar.push_info_bar(
"info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000
)
@@ -158,7 +157,7 @@ class Setting(QWidget):
MuMu_splash_ads_path.touch()
logger.success("开启跳过MuMu启动广告功能")
logger.success("开启跳过MuMu启动广告功能", module="设置界面")
MainInfoBar.push_info_bar(
"success", "操作成功", "已开启跳过MuMu启动广告功能", 3000
)
@@ -170,7 +169,7 @@ class Setting(QWidget):
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
MuMu_splash_ads_path.unlink()
logger.info("关闭跳过MuMu启动广告功能")
logger.info("关闭跳过MuMu启动广告功能", module="设置界面")
MainInfoBar.push_info_bar(
"info", "操作成功", "已关闭跳过MuMu启动广告功能", 3000
)
@@ -181,6 +180,8 @@ class Setting(QWidget):
if Config.key_path.exists():
return None
logger.info("未设置管理密钥,开始要求用户进行设置", module="设置界面")
while True:
choice = LineEditMessageBox(
@@ -188,6 +189,7 @@ class Setting(QWidget):
)
if choice.exec() and choice.input.text() != "":
Crypto.get_PASSWORD(choice.input.text())
logger.success("成功设置管理密钥", module="设置界面")
break
else:
choice = MessageBox(
@@ -207,10 +209,7 @@ class Setting(QWidget):
while if_change:
choice = LineEditMessageBox(
self.window(),
"请输入旧的管理密钥",
"旧管理密钥",
"密码",
self.window(), "请输入旧的管理密钥", "旧管理密钥", "密码"
)
if choice.exec() and choice.input.text() != "":
@@ -231,6 +230,7 @@ class Setting(QWidget):
# 修改管理密钥
Crypto.change_PASSWORD(PASSWORD_old, choice.input.text())
logger.success("成功修改管理密钥", module="设置界面")
MainInfoBar.push_info_bar(
"success", "操作成功", "管理密钥修改成功", 3000
)
@@ -291,6 +291,7 @@ class Setting(QWidget):
# 重置管理密钥
Crypto.reset_PASSWORD(choice.input.text())
logger.success("成功重置管理密钥", module="设置界面")
MainInfoBar.push_info_bar(
"success", "操作成功", "管理密钥重置成功", 3000
)
@@ -316,7 +317,12 @@ class Setting(QWidget):
)
def check_update(self, if_show: bool = False, if_first: bool = False) -> None:
"""检查版本更新,调起文件下载进程"""
"""
检查版本更新,调起更新线程
:param if_show: 是否显示更新信息
:param if_first: 是否为启动时检查更新
"""
current_version = list(map(int, Config.VERSION.split(".")))
@@ -339,7 +345,9 @@ class Setting(QWidget):
if version_info["code"] != 0:
logger.error(f"获取版本信息时出错:{version_info['msg']}")
logger.error(
f"获取版本信息时出错:{version_info['msg']}", module="设置界面"
)
error_remark_dict = {
1001: "获取版本信息的URL参数不正确",
@@ -372,7 +380,10 @@ class Setting(QWidget):
return None
logger.warning(f"获取版本信息时出错:{network_result['error_message']}")
logger.warning(
f"获取版本信息时出错:{network_result['error_message']}",
module="设置界面",
)
MainInfoBar.push_info_bar(
"warning",
"获取版本信息时出错",
@@ -462,7 +473,7 @@ class Setting(QWidget):
# 从远程服务器获取代理信息
network = Network.add_task(
mode="get",
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/download_info.json",
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/download_info.json",
)
network.loop.exec()
network_result = Network.get_result(network)
@@ -470,11 +481,12 @@ class Setting(QWidget):
download_info = network_result["response_json"]
else:
logger.warning(
f"获取应用列表时出错:{network_result['error_message']}"
f"获取下载信息时出错:{network_result['error_message']}",
module="设置界面",
)
MainInfoBar.push_info_bar(
"warning",
"获取应用列表时出错",
"获取下载信息时出错",
f"网络错误:{network_result['status_code']}",
5000,
)
@@ -492,6 +504,8 @@ class Setting(QWidget):
"download_dict": download_info["download_dict"],
}
logger.info("开始执行更新任务", module="设置界面")
self.downloader = DownloadManager(
Config.app_path, "AUTO_MAA", remote_version, download_config
)
@@ -526,6 +540,9 @@ class Setting(QWidget):
SoundPlayer.play("无新版本")
def start_setup(self) -> None:
"""启动安装程序"""
logger.info("启动安装程序", module="设置界面")
subprocess.Popen(
[
Config.app_path / "AUTO_MAA-Setup.exe",
@@ -548,14 +565,17 @@ class Setting(QWidget):
# 从远程服务器获取最新公告
network = Network.add_task(
mode="get",
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json",
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/notice.json",
)
network.loop.exec()
network_result = Network.get_result(network)
if network_result["status_code"] == 200:
notice = network_result["response_json"]
else:
logger.warning(f"获取最新公告时出错:{network_result['error_message']}")
logger.warning(
f"获取最新公告时出错:{network_result['error_message']}",
module="设置界面",
)
MainInfoBar.push_info_bar(
"warning",
"获取最新公告时出错",
@@ -786,14 +806,6 @@ class StartSettingCard(HeaderCardWidget):
configItem=Config.start_IfSelfStart,
parent=self,
)
self.card_IfRunDirectly = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="启动后直接运行主任务",
content="启动AUTO_MAA后自动运行自动代理任务优先级调度队列 1 > 脚本 1",
qconfig=Config,
configItem=Config.start_IfRunDirectly,
parent=self,
)
self.card_IfMinimizeDirectly = SwitchSettingCard(
icon=FluentIcon.PAGE_RIGHT,
title="启动后直接最小化",
@@ -805,7 +817,6 @@ class StartSettingCard(HeaderCardWidget):
Layout = QVBoxLayout()
Layout.addWidget(self.card_IfSelfStart)
Layout.addWidget(self.card_IfRunDirectly)
Layout.addWidget(self.card_IfMinimizeDirectly)
self.viewLayout.addLayout(Layout)
@@ -1192,7 +1203,7 @@ class UpdaterSettingCard(HeaderCardWidget):
parent=self,
)
mirrorchyan_url = HyperlinkButton(
"https://mirrorchyan.com/zh/get-start?source=auto_maa-setting_card",
"https://mirrorchyan.com/zh/get-start?source=auto_maa-setting",
"获取Mirror酱CDK",
self,
)

View File

@@ -20,7 +20,7 @@ AppPublisher={#MyAppPublisher}
AppPublisherURL={#MyAppURL}
AppSupportURL={#MyAppURL}
AppUpdatesURL={#MyAppURL}
DefaultDirName=D:\{#MyAppName}
DefaultDirName={autopf}\{#MyAppName}
UninstallDisplayIcon={app}\{#MyAppExeName}
; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run
; on anything but x64 and Windows 11 on Arm.
@@ -32,13 +32,13 @@ ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible
DisableProgramGroupPage=yes
LicenseFile={#MyAppPath}\LICENSE
; Remove the following line to run in administrative install mode (install for all users).
PrivilegesRequired=lowest
PrivilegesRequired=admin
OutputDir={#OutputDir}
OutputBaseFilename=AUTO_MAA-Setup
SetupIconFile={#MyAppPath}\resources\icons\AUTO_MAA.ico
SolidCompression=yes
WizardStyle=modern
AppMutex=AUTO_MAA_Installer_Mutex
[Languages]
Name: "Chinese"; MessagesFile: "{#MyAppPath}\resources\docs\ChineseSimplified.isl"
@@ -51,6 +51,8 @@ Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{
Source: "{#MyAppPath}\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\app\*"; DestDir: "{app}\app"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\resources\*"; DestDir: "{app}\resources"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\Go_Updater\*"; DestDir: "{app}\Go_Updater"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "{#MyAppPath}\AUTO_MAA_Go_Updater_install.exe"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\main.py"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\requirements.txt"; DestDir: "{app}"; Flags: ignoreversion
Source: "{#MyAppPath}\README.md"; DestDir: "{app}"; Flags: ignoreversion
@@ -62,7 +64,7 @@ Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall
Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall runascurrentuser
[Code]
var
@@ -70,7 +72,10 @@ var
function InitializeUninstall: Boolean;
begin
DeleteDataQuestion := MsgBox('您确认要完全移除 AUTO_MAA 的所有用户数据文件与子组件吗?', mbConfirmation, MB_YESNO) = IDYES;
DeleteDataQuestion := MsgBox('您确认要完全移除 AUTO_MAA 的所有配置、用户数据与子组件吗?' + #13#10 +
'选择"是"将删除所有配置文件、数据与子组件程序。' + #13#10 +
'选择"否"将保留数据文件与子组件。',
mbConfirmation, MB_YESNO) = IDYES;
Result := True;
end;

View File

@@ -48,12 +48,7 @@ class ProcessManager(QObject):
self.check_timer = QTimer()
self.check_timer.timeout.connect(self.check_processes)
def open_process(
self,
path: Path,
args: list = [],
tracking_time: int = 60,
) -> int:
def open_process(self, path: Path, args: list = [], tracking_time: int = 60) -> int:
"""
启动一个新进程并返回其pid并开始监视该进程
@@ -89,7 +84,7 @@ class ProcessManager(QObject):
# 扫描并记录所有相关进程
try:
# 获取主进程及其子进程
# 获取主进程
main_proc = psutil.Process(self.main_pid)
self.tracked_pids.add(self.main_pid)

View File

@@ -44,13 +44,14 @@ def no_print(*args, **kwargs):
builtins.print = no_print
from loguru import logger
import os
import sys
import ctypes
from PySide6.QtWidgets import QApplication
from qfluentwidgets import FluentTranslator
from app.core.logger import logger
def is_admin() -> bool:
"""检查当前程序是否以管理员身份运行"""

View File

@@ -1,7 +1,7 @@
loguru==0.7.3
plyer==2.1.0
PySide6==6.9.1
PySide6-Fluent-Widgets[full]
PySide6-Fluent-Widgets[full]==1.8.3
psutil==7.0.0
pywin32==310
keyboard==0.13.5
@@ -12,4 +12,5 @@ requests==2.32.4
markdown==3.8.2
Jinja2==3.1.6
nuitka==2.7.12
pillow==11.3.0
pillow==11.3.0
packaging==25.0

View File

@@ -27,6 +27,8 @@
"MainFunction.Stage4": "" #备选关卡3
"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡
"MainFunction.Series.Quantity": "1" #连战次数
"MainFunction.Annihilation.UseCustom": "True" #自定义剿灭关卡
"MainFunction.Annihilation.Stage": "Annihilation"、"Chernobog@Annihilation"、"LungmenOutskirts@Annihilation"、"LungmenDowntown@Annihilation" #自定义剿灭关卡号
"Penguin.IsDrGrandet": "True" #博朗台模式
"GUI.CustomStageCode": "False" #手动输入关卡名
"GUI.UseAlternateStage": "False" #使用备选关卡
@@ -45,7 +47,7 @@
"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读
"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址
#设置
"Start.ClientType": "Bilibili"、 "Official" #服务器
"Start.ClientType": "Official"、"Bilibili"、"YoStarEN"、"YoStarJP"、"YoStarKR"、"txwy" #服务器
G"Timer.Timer1": "False" #时间设置1
"Connect.AdbPath" #ADB路径
"Connect.Address": "127.0.0.1:16448" #连接地址

View File

@@ -1,60 +1,71 @@
{
"main_version": "4.4.0.0",
"main_version": "4.4.2.0",
"version_info": {
"4.4.0.0": {
"新增功能": [
"通用配置模式接入日志系统"
],
"4.4.2.0": {
"修复BUG": [
"信任系统证书,并添加网络代理地址配置项 #50",
"适配 MAA 任务及基建设施日志翻译"
"修复独立更新器版本号比对问题",
"修复 MAA 开始唤醒识别错误问题"
],
"程序优化": [
"重构历史记录保存与载入逻辑"
"历史记录显示效果优化"
]
},
"4.4.0.5": {
"4.4.1.0": {
"新增功能": [
"添加导入导出通用配置功能"
"启动时支持直接运行复数调度队列"
],
"修复BUG": [
"修复开机自启相关功能"
"修复计划表未能按照鹰历获取关卡号的问题"
]
},
"4.4.0.4": {
"4.4.1.5": {
"新增功能": [
"添加重置管理密钥功能"
],
"修复BUG": [
"修复无计划表时数据系统无法正常升级到v1.7的问题"
]
},
"4.4.0.3": {
"修复BUG": [
"适配 MAA 备选关卡字段修改",
"修复无成功日志时的脚本判定逻辑"
"适配 MAA 长期开放剿灭关卡",
"新增完成任务后自动复原脚本配置",
"通用脚本启动附加命令添加额外的语法以适应UI可执行文件与任务可执行文件不同的情况",
"新增 Go_Updater 独立更新器"
],
"程序优化": [
"`GameId`字段改为 `Stage`,与 MAA 保持一致"
"优化调度队列配置逻辑",
"优化静默进程标记逻辑,避免未能及时移除导致相关功能持续开启",
"MAA 代理时更新改为强制开启",
"移除 MAA 详细配置模式中的剿灭项"
]
},
"4.4.0.2": {
"新增功能": [
"进一步适配三月七相关配置项"
],
"4.4.1.4": {
"修复BUG": [
"适配 Mirror 酱 平台下载策略调整"
"添加强制关机功能并优化关机流程"
]
},
"4.4.0.1": {
"4.4.1.3": {
"修复BUG": [
"移除崩溃弹窗机制"
]
},
"4.4.1.2": {
"新增功能": [
"初步完成通用调度模块"
"AUTO_MAA 配置分享中心上线"
],
"修复BUG": [
"修复了程序BUG较少的BUG"
"日志读取添加兜底机制",
"修复 QTimer.singleShot 参数问题"
],
"程序优化": [
"子线程卡死不再阻塞调度任务"
"小文件配置信息转移至AUTO_MAA自建服务"
]
},
"4.4.1.1": {
"新增功能": [
"通用脚本支持在选定的时机自动更新配置文件"
],
"修复BUG": [
"修复MAA掉落物统计功能",
"修复模拟器界面被异常关闭且无法重新打开的问题"
],
"程序优化": [
"重构日志记录,载入更多日志记录项",
"优化日志监看启停逻辑",
"SpinBox和TimeEdit组件忽视滚轮事件"
]
}
}