diff --git a/.github/workflows/build-app.yml b/.github/workflows/build-app.yml deleted file mode 100644 index a2da0b6..0000000 --- a/.github/workflows/build-app.yml +++ /dev/null @@ -1,283 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -name: Build AUTO_MAA - -on: - workflow_dispatch: - -permissions: - contents: write - actions: write - -jobs: - - pre_check: - name: Pre Checks - runs-on: ubuntu-latest - - steps: - - name: Repo Check - id: repo_check - run: | - if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then - echo "When forking this repository to make your own builds, you have to adjust this check." - exit 1 - fi - exit 0 - - build_AUTO_MAA: - runs-on: windows-latest - needs: pre_check - - steps: - - 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: - python-version: '3.12' - - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r requirements.txt - - - name: Get version - id: get_version - run: | - $version = (Get-Content resources/version.json | ConvertFrom-Json).main_version - echo "main_version=$version" >> $env:GITHUB_OUTPUT - - - name: Nuitka build main program - uses: Nuitka/Nuitka-Action@main - with: - script-name: main.py - mode: app - enable-plugins: pyside6 - onefile-tempdir-spec: "{TEMP}/AUTO_MAA" - windows-console-mode: attach - windows-icon-from-ico: resources/icons/AUTO_MAA.ico - windows-uac-admin: true - company-name: AUTO_MAA Team - product-name: AUTO_MAA - file-version: ${{ steps.get_version.outputs.main_version }} - product-version: ${{ steps.get_version.outputs.main_version }} - file-description: AUTO_MAA Component - copyright: Copyright © 2024-2025 DLmaster361 - assume-yes-for-downloads: true - output-file: AUTO_MAA - output-dir: AUTO_MAA - - - name: Upload unsigned main program - id: upload-unsigned-main-program - uses: actions/upload-artifact@v4 - with: - name: AUTO_MAA - path: AUTO_MAA/AUTO_MAA.exe - - - name: Sign main program - id: sign_main_program - uses: signpath/github-action-submit-signing-request@v1.2 - with: - api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' - organization-id: '787a1d5f-6177-4f30-9559-d2646473584a' - project-slug: 'AUTO_MAA' - signing-policy-slug: 'release-signing' - artifact-configuration-slug: "AUTO_MAA" - github-artifact-id: '${{ steps.upload-unsigned-main-program.outputs.artifact-id }}' - wait-for-completion: true - output-artifact-directory: 'AUTO_MAA' - - - name: Add other resources - shell: pwsh - run: | - $root = "${{ github.workspace }}" - $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/" - Copy-Item "$root/LICENSE" "$root/AUTO_MAA/" - - - name: Create Inno Setup script - shell: pwsh - run: | - $root = "${{ github.workspace }}" - $ver = "${{ steps.get_version.outputs.main_version }}" - $iss = Get-Content "$root/app/utils/AUTO_MAA.iss" -Raw - $iss = $iss -replace '#define MyAppVersion ""', "#define MyAppVersion `"$ver`"" - $iss = $iss -replace '#define MyAppPath ""', "#define MyAppPath `"$root/AUTO_MAA`"" - $iss = $iss -replace '#define OutputDir ""', "#define OutputDir `"$root`"" - Set-Content -Path "$root/AUTO_MAA.iss" -Value $iss - - - name: Build setup program - uses: Minionguyjpro/Inno-Setup-Action@v1.2.5 - with: - path: AUTO_MAA.iss - - - name: Upload unsigned setup program - id: upload-unsigned-setup-program - uses: actions/upload-artifact@v4 - with: - name: AUTO_MAA-Setup - path: AUTO_MAA-Setup.exe - - - name: Sign setup program - id: sign_setup_program - uses: signpath/github-action-submit-signing-request@v1.2 - with: - api-token: '${{ secrets.SIGNPATH_API_TOKEN }}' - organization-id: '787a1d5f-6177-4f30-9559-d2646473584a' - project-slug: 'AUTO_MAA' - signing-policy-slug: 'release-signing' - artifact-configuration-slug: "AUTO_MAA-Setup" - github-artifact-id: '${{ steps.upload-unsigned-setup-program.outputs.artifact-id }}' - wait-for-completion: true - output-artifact-directory: 'AUTO_MAA_Setup' - - - name: Compress setup exe - shell: pwsh - run: Compress-Archive -Path AUTO_MAA_Setup/* -DestinationPath AUTO_MAA_${{ steps.get_version.outputs.main_version }}.zip - - - name: Generate version info - shell: python - run: | - import json - from pathlib import Path - def version_text(version_numb): - while len(version_numb) < 4: - version_numb.append(0) - if version_numb[3] == 0: - return f"v{'.'.join(str(_) for _ in version_numb[0:3])}" - else: - return f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" - def version_info_markdown(info): - version_info = "" - for key, value in info.items(): - version_info += f"## {key}\n" - for v in value: - version_info += f"- {v}\n" - return version_info - root_path = Path(".") - version = json.loads((root_path / "resources/version.json").read_text(encoding="utf-8")) - main_version_numb = list(map(int, version["main_version"].split("."))) - all_version_info = {} - for v_i in version["version_info"].values(): - for key, value in v_i.items(): - if key in all_version_info: - all_version_info[key] += value.copy() - else: - all_version_info[key] = value.copy() - (root_path / "version_info.txt").write_text( - f"{version_text(main_version_numb)}\n\n\n{version_info_markdown(all_version_info)}", - encoding="utf-8", - ) - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: AUTO_MAA_${{ steps.get_version.outputs.main_version }} - path: AUTO_MAA_${{ steps.get_version.outputs.main_version }}.zip - - - name: Upload Version_Info Artifact - uses: actions/upload-artifact@v4 - with: - name: version_info - path: version_info.txt - - publish_release: - name: Publish release - needs: build_AUTO_MAA - runs-on: ubuntu-latest - - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Download artifacts - uses: actions/download-artifact@v4 - with: - pattern: AUTO_MAA_* - merge-multiple: true - path: artifacts - - - name: Download Version_Info - uses: actions/download-artifact@v4 - with: - name: version_info - path: ./ - - - name: Create release - id: create_release - run: | - set -xe - shopt -s nullglob - NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))" - TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))" - NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))" - NOTES="$NOTES_MAIN - - ## 代码签名策略(Code signing policy) - - Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) - - - 审批人(Approvers): [DLmaster (@DLmaster361)](https://github.com/DLmaster361) - - ## 隐私政策(Privacy policy) - - 除非用户、安装者或使用者特别要求,否则本程序不会将任何信息传输到其他网络系统。 - - This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. - - [已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-release) - - \`\`\`本release通过GitHub Actions自动构建\`\`\`" - if [ "${{ github.ref_name }}" == "main" ]; then - PRERELEASE_FLAG="" - else - PRERELEASE_FLAG="--prerelease" - fi - gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" $PRERELEASE_FLAG artifacts/* - env: - GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }} - - - name: Trigger MirrorChyanUploading - run: | - gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan - gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/mirrorchyan.yml b/.github/workflows/mirrorchyan.yml deleted file mode 100644 index 604328c..0000000 --- a/.github/workflows/mirrorchyan.yml +++ /dev/null @@ -1,21 +0,0 @@ -name: mirrorchyan - -on: - workflow_dispatch: - -jobs: - mirrorchyan: - runs-on: macos-latest - - steps: - - id: uploading - uses: MirrorChyan/uploading-action@v1 - with: - filetype: latest-release - filename: "AUTO_MAA*.zip" - mirrorchyan_rid: AUTO_MAA - - owner: DLmaster361 - repo: AUTO_MAA - github_token: ${{ secrets.GITHUB_TOKEN }} - upload_token: ${{ secrets.MirrorChyanUploadToken }} diff --git a/.github/workflows/mirrorchyan_release_note.yml b/.github/workflows/mirrorchyan_release_note.yml deleted file mode 100644 index deb0274..0000000 --- a/.github/workflows/mirrorchyan_release_note.yml +++ /dev/null @@ -1,19 +0,0 @@ -name: mirrorchyan_release_note - -on: - workflow_dispatch: - release: - types: [edited] - -jobs: - mirrorchyan: - runs-on: macos-latest - - steps: - - id: uploading - uses: MirrorChyan/release-note-action@v1 - with: - mirrorchyan_rid: AUTO_MAA - - upload_token: ${{ secrets.MirrorChyanUploadToken }} - github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index c3f664a..b4c6769 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,18 @@ +# Python-generated files __pycache__/ -config/ -data/ -debug/ -history/ -script/ -resources/notice.json -resources/theme_image.json -resources/images/Home/BannerTheme.jpg \ No newline at end of file +*.py[oc] +build/ +dist/ +wheels/ +logs/ +*.egg-info + +# Virtual environments +.venv + +# IDE and editors +.vscode/ +.idea/ +*.swp +*.swo +.DS_Store diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..24ee5b1 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.13 diff --git a/Go_Updater/Makefile b/Go_Updater/Makefile deleted file mode 100644 index 34bacca..0000000 --- a/Go_Updater/Makefile +++ /dev/null @@ -1,116 +0,0 @@ -# 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" \ No newline at end of file diff --git a/Go_Updater/README.MD b/Go_Updater/README.MD deleted file mode 100644 index b5832d6..0000000 --- a/Go_Updater/README.MD +++ /dev/null @@ -1,15 +0,0 @@ -# 用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"` \ No newline at end of file diff --git a/Go_Updater/api/client.go b/Go_Updater/api/client.go deleted file mode 100644 index ef28e82..0000000 --- a/Go_Updater/api/client.go +++ /dev/null @@ -1,290 +0,0 @@ -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) - - // 比较每个组件 - maxLen := len(parts1) - if len(parts2) > maxLen { - maxLen = len(parts2) - } - - for i := 0; i < maxLen; i++ { - var p1, p2 int - if i < len(parts1) { - p1 = parts1[i] - } - if i < len(parts2) { - p2 = parts2[i] - } - - if p1 < p2 { - return -1 - } else if p1 > p2 { - return 1 - } - } - - return 0 -} - -// normalizeVersion 移除 '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 - } - } - 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) -} diff --git a/Go_Updater/api/client_test.go b/Go_Updater/api/client_test.go deleted file mode 100644 index bdb9f72..0000000 --- a/Go_Updater/api/client_test.go +++ /dev/null @@ -1,186 +0,0 @@ -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) - } - }) - } -} diff --git a/Go_Updater/app.rc b/Go_Updater/app.rc deleted file mode 100644 index 7cc1f3b..0000000 --- a/Go_Updater/app.rc +++ /dev/null @@ -1,34 +0,0 @@ -#include - -// 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 \ No newline at end of file diff --git a/Go_Updater/app.syso b/Go_Updater/app.syso deleted file mode 100644 index 6bf5237..0000000 Binary files a/Go_Updater/app.syso and /dev/null differ diff --git a/Go_Updater/assets/assets.go b/Go_Updater/assets/assets.go deleted file mode 100644 index 557ccaf..0000000 --- a/Go_Updater/assets/assets.go +++ /dev/null @@ -1,34 +0,0 @@ -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 -} diff --git a/Go_Updater/assets/assets_test.go b/Go_Updater/assets/assets_test.go deleted file mode 100644 index 0bcab5a..0000000 --- a/Go_Updater/assets/assets_test.go +++ /dev/null @@ -1,100 +0,0 @@ -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) -} diff --git a/Go_Updater/assets/config_template.yaml b/Go_Updater/assets/config_template.yaml deleted file mode 100644 index bac2b03..0000000 --- a/Go_Updater/assets/config_template.yaml +++ /dev/null @@ -1,7 +0,0 @@ -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 \ No newline at end of file diff --git a/Go_Updater/build-config.yaml b/Go_Updater/build-config.yaml deleted file mode 100644 index a15503e..0000000 --- a/Go_Updater/build-config.yaml +++ /dev/null @@ -1,55 +0,0 @@ -# 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 \ No newline at end of file diff --git a/Go_Updater/build.bat b/Go_Updater/build.bat deleted file mode 100644 index f71fee2..0000000 --- a/Go_Updater/build.bat +++ /dev/null @@ -1,93 +0,0 @@ -@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=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 ======================================== diff --git a/Go_Updater/build.ps1 b/Go_Updater/build.ps1 deleted file mode 100644 index b4bff73..0000000 --- a/Go_Updater/build.ps1 +++ /dev/null @@ -1,105 +0,0 @@ -# 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 \ No newline at end of file diff --git a/Go_Updater/config/config.go b/Go_Updater/config/config.go deleted file mode 100644 index ded8ea0..0000000 --- a/Go_Updater/config/config.go +++ /dev/null @@ -1,198 +0,0 @@ -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 "." -} diff --git a/Go_Updater/config/config.json b/Go_Updater/config/config.json deleted file mode 100644 index 2a13ef4..0000000 --- a/Go_Updater/config/config.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "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" - } -} \ No newline at end of file diff --git a/Go_Updater/config/config_test.go b/Go_Updater/config/config_test.go deleted file mode 100644 index 36c6f26..0000000 --- a/Go_Updater/config/config_test.go +++ /dev/null @@ -1,153 +0,0 @@ -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) - } -} diff --git a/Go_Updater/download/manager.go b/Go_Updater/download/manager.go deleted file mode 100644 index 07cfd5d..0000000 --- a/Go_Updater/download/manager.go +++ /dev/null @@ -1,224 +0,0 @@ -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 -} \ No newline at end of file diff --git a/Go_Updater/download/manager_test.go b/Go_Updater/download/manager_test.go deleted file mode 100644 index f406e10..0000000 --- a/Go_Updater/download/manager_test.go +++ /dev/null @@ -1,1392 +0,0 @@ -package download - -import ( - "crypto/sha256" - "encoding/hex" - "fmt" - "net/http" - "net/http/httptest" - "os" - "path/filepath" - "strings" - "testing" - "time" -) - -func TestNewManager(t *testing.T) { - manager := NewManager() - if manager == nil { - t.Fatal("NewManager() returned nil") - } - if manager.client == nil { - t.Fatal("Manager client is nil") - } - if manager.timeout != 30*time.Second { - t.Errorf("Expected timeout 30s, got %v", manager.timeout) - } -} - -func TestDownload(t *testing.T) { - // Create test content - testContent := "This is test content for download" - - // Create test server - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - // Create temporary directory - tempDir, err := os.MkdirTemp("", "download_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - // Test download - manager := NewManager() - destPath := filepath.Join(tempDir, "test_file.txt") - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - } - - err = manager.Download(server.URL, destPath, progressCallback) - if err != nil { - t.Fatalf("Download failed: %v", err) - } - - // Verify file exists and content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content %q, got %q", testContent, string(content)) - } - - // Verify progress updates - if len(progressUpdates) == 0 { - t.Error("No progress updates received") - } - - // Check final progress - finalProgress := progressUpdates[len(progressUpdates)-1] - if finalProgress.Percentage != 100 { - t.Errorf("Expected final percentage 100, got %f", finalProgress.Percentage) - } - if finalProgress.BytesDownloaded != int64(len(testContent)) { - t.Errorf("Expected bytes downloaded %d, got %d", len(testContent), finalProgress.BytesDownloaded) - } -} - -func TestDownloadWithResume(t *testing.T) { - testContent := "This is a longer test content for resume functionality testing" - partialContent := testContent[:20] // First 20 bytes - remainingContent := testContent[20:] // Remaining bytes - - // Create test server that supports range requests - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rangeHeader := r.Header.Get("Range") - - if rangeHeader != "" { - // Handle range request - if strings.HasPrefix(rangeHeader, "bytes=20-") { - w.Header().Set("Content-Range", fmt.Sprintf("bytes 20-%d/%d", len(testContent)-1, len(testContent))) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(remainingContent))) - w.WriteHeader(http.StatusPartialContent) - w.Write([]byte(remainingContent)) - return - } - } - - // Handle normal request - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - // Create temporary directory - tempDir, err := os.MkdirTemp("", "download_resume_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - destPath := filepath.Join(tempDir, "test_resume_file.txt") - - // Create partial file - err = os.WriteFile(destPath, []byte(partialContent), 0644) - if err != nil { - t.Fatal(err) - } - - // Test resume download - manager := NewManager() - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - } - - err = manager.DownloadWithResume(server.URL, destPath, progressCallback) - if err != nil { - t.Fatalf("Resume download failed: %v", err) - } - - // Verify complete file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read resumed file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content %q, got %q", testContent, string(content)) - } -} - -func TestValidateChecksum(t *testing.T) { - // Create test content and calculate its checksum - testContent := "Test content for checksum validation" - hash := sha256.Sum256([]byte(testContent)) - expectedChecksum := hex.EncodeToString(hash[:]) - - // Create temporary file - tempDir, err := os.MkdirTemp("", "checksum_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - testFile := filepath.Join(tempDir, "test_checksum.txt") - err = os.WriteFile(testFile, []byte(testContent), 0644) - if err != nil { - t.Fatal(err) - } - - manager := NewManager() - - // Test valid checksum - err = manager.ValidateChecksum(testFile, expectedChecksum) - if err != nil { - t.Errorf("Valid checksum validation failed: %v", err) - } - - // Test invalid checksum - invalidChecksum := "invalid_checksum_value" - err = manager.ValidateChecksum(testFile, invalidChecksum) - if err == nil { - t.Error("Invalid checksum validation should have failed") - } - - // Test empty checksum (should pass) - err = manager.ValidateChecksum(testFile, "") - if err != nil { - t.Errorf("Empty checksum validation failed: %v", err) - } - - // Test non-existent file - err = manager.ValidateChecksum("non_existent_file.txt", expectedChecksum) - if err == nil { - t.Error("Non-existent file validation should have failed") - } -} - -func TestDownloadError(t *testing.T) { - manager := NewManager() - - // Test invalid URL - err := manager.Download("invalid-url", "/tmp/test", nil) - if err == nil { - t.Error("Download with invalid URL should have failed") - } - - // Test server error - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "download_error_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - destPath := filepath.Join(tempDir, "error_test.txt") - err = manager.Download(server.URL, destPath, nil) - if err == nil { - t.Error("Download with server error should have failed") - } -} - -func TestProgressCallback(t *testing.T) { - testContent := strings.Repeat("A", 1024*100) // 100KB content for more progress updates - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - - // Write content in smaller chunks to trigger multiple progress updates - writer := w.(http.Flusher) - for i := 0; i < len(testContent); i += 1024 { - end := i + 1024 - if end > len(testContent) { - end = len(testContent) - } - w.Write([]byte(testContent[i:end])) - writer.Flush() - time.Sleep(50 * time.Millisecond) // Longer delay to ensure progress updates - } - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "progress_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "progress_test.txt") - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - - // Validate progress values - if progress.BytesDownloaded < 0 { - t.Errorf("Negative bytes downloaded: %d", progress.BytesDownloaded) - } - if progress.Percentage < 0 || progress.Percentage > 100 { - t.Errorf("Invalid percentage: %f", progress.Percentage) - } - if progress.Speed < 0 { - t.Errorf("Negative speed: %d", progress.Speed) - } - } - - err = manager.Download(server.URL, destPath, progressCallback) - if err != nil { - t.Fatalf("Download failed: %v", err) - } - - // Should have received at least one progress update (final one is guaranteed) - if len(progressUpdates) < 1 { - t.Errorf("Expected at least one progress update, got %d", len(progressUpdates)) - } - - // Final progress should be 100% - finalProgress := progressUpdates[len(progressUpdates)-1] - if finalProgress.Percentage != 100 { - t.Errorf("Expected final percentage 100, got %f", finalProgress.Percentage) - } - - // Verify that we got the correct total bytes - if finalProgress.BytesDownloaded != int64(len(testContent)) { - t.Errorf("Expected bytes downloaded %d, got %d", len(testContent), finalProgress.BytesDownloaded) - } -} - -func TestDownloadWithSources(t *testing.T) { - testContent := "Test content for multi-source download" - - // Create primary server (Mirror酱 - higher priority) - primaryServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer primaryServer.Close() - - // Create backup server (regular download site - lower priority) - backupServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer backupServer.Close() - - tempDir, err := os.MkdirTemp("", "multi_source_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "multi_source_test.txt") - - // Test with multiple sources - should use primary (lower priority number) - sources := []DownloadSource{ - {URL: backupServer.URL, Priority: 2, Name: "Backup Server"}, - {URL: primaryServer.URL, Priority: 1, Name: "Mirror Server"}, // Higher priority - } - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - } - - err = manager.DownloadWithSources(sources, destPath, progressCallback) - if err != nil { - t.Fatalf("Multi-source download failed: %v", err) - } - - // Verify file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content %q, got %q", testContent, string(content)) - } - - // Verify progress updates - if len(progressUpdates) == 0 { - t.Error("No progress updates received") - } -} - -func TestDownloadWithSourcesFallback(t *testing.T) { - testContent := "Test content for fallback download" - - // Create failing primary server - failingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer failingServer.Close() - - // Create working backup server - workingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer workingServer.Close() - - tempDir, err := os.MkdirTemp("", "fallback_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "fallback_test.txt") - - // Test fallback - primary fails, backup succeeds - sources := []DownloadSource{ - {URL: failingServer.URL, Priority: 1, Name: "Failing Server"}, - {URL: workingServer.URL, Priority: 2, Name: "Working Server"}, - } - - err = manager.DownloadWithSources(sources, destPath, nil) - if err != nil { - t.Fatalf("Fallback download failed: %v", err) - } - - // Verify file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content %q, got %q", testContent, string(content)) - } -} - -func TestDownloadWithSourcesAllFail(t *testing.T) { - // Create two failing servers - failingServer1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusInternalServerError) - })) - defer failingServer1.Close() - - failingServer2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.WriteHeader(http.StatusNotFound) - })) - defer failingServer2.Close() - - tempDir, err := os.MkdirTemp("", "all_fail_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "all_fail_test.txt") - - // Test when all sources fail - sources := []DownloadSource{ - {URL: failingServer1.URL, Priority: 1, Name: "Failing Server 1"}, - {URL: failingServer2.URL, Priority: 2, Name: "Failing Server 2"}, - } - - err = manager.DownloadWithSources(sources, destPath, nil) - if err == nil { - t.Error("Expected download to fail when all sources fail") - } - - // Verify error message contains information about all sources failing - if !strings.Contains(err.Error(), "all download sources failed") { - t.Errorf("Expected error message about all sources failing, got: %v", err) - } -} - -func TestDownloadSourcePriority(t *testing.T) { - testContent1 := "Content from server 1" - testContent2 := "Content from server 2" - - // Create two working servers with different content - server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent1))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent1)) - })) - defer server1.Close() - - server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent2))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent2)) - })) - defer server2.Close() - - tempDir, err := os.MkdirTemp("", "priority_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "priority_test.txt") - - // Test priority ordering - server2 has higher priority (lower number) - sources := []DownloadSource{ - {URL: server1.URL, Priority: 5, Name: "Server 1"}, - {URL: server2.URL, Priority: 1, Name: "Server 2"}, // Higher priority - } - - err = manager.DownloadWithSources(sources, destPath, nil) - if err != nil { - t.Fatalf("Priority download failed: %v", err) - } - - // Should have downloaded from server2 (higher priority) - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent2 { - t.Errorf("Expected content from server 2 %q, got %q", testContent2, string(content)) - } -} - -func TestSetTimeout(t *testing.T) { - manager := NewManager() - - // Test default timeout - if manager.timeout != 30*time.Second { - t.Errorf("Expected default timeout 30s, got %v", manager.timeout) - } - - // Test setting custom timeout - customTimeout := 60 * time.Second - manager.SetTimeout(customTimeout) - - if manager.timeout != customTimeout { - t.Errorf("Expected timeout %v, got %v", customTimeout, manager.timeout) - } - - if manager.client.Timeout != customTimeout { - t.Errorf("Expected client timeout %v, got %v", customTimeout, manager.client.Timeout) - } -} - -func TestDownloadWithSourcesEmptyList(t *testing.T) { - manager := NewManager() - tempDir, err := os.MkdirTemp("", "empty_sources_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - destPath := filepath.Join(tempDir, "empty_test.txt") - - // Test with empty sources list - var sources []DownloadSource - err = manager.DownloadWithSources(sources, destPath, nil) - - if err == nil { - t.Error("Expected error when no download sources provided") - } - - if !strings.Contains(err.Error(), "no download sources provided") { - t.Errorf("Expected error about no sources, got: %v", err) - } -} - -func TestDownloadWithInvalidDestination(t *testing.T) { - testContent := "Test content" - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - manager := NewManager() - - // Test with invalid destination path (directory that can't be created) - invalidPath := string([]byte{0}) + "/invalid/path/file.txt" - - err := manager.Download(server.URL, invalidPath, nil) - if err == nil { - t.Error("Expected error with invalid destination path") - } -} - -func TestDownloadWithTimeout(t *testing.T) { - // Create a server that delays response - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - time.Sleep(2 * time.Second) // Delay longer than timeout - w.WriteHeader(http.StatusOK) - w.Write([]byte("delayed content")) - })) - defer server.Close() - - manager := NewManager() - manager.SetTimeout(500 * time.Millisecond) // Short timeout - - tempDir, err := os.MkdirTemp("", "timeout_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - destPath := filepath.Join(tempDir, "timeout_test.txt") - - err = manager.Download(server.URL, destPath, nil) - if err == nil { - t.Error("Expected timeout error") - } - - // Check that it's a timeout-related error - if !strings.Contains(err.Error(), "timeout") && !strings.Contains(err.Error(), "context deadline exceeded") { - t.Errorf("Expected timeout error, got: %v", err) - } -} - -func TestDownloadWithLargeFile(t *testing.T) { - // Create large test content (1MB) - largeContent := strings.Repeat("A", 1024*1024) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(largeContent))) - w.WriteHeader(http.StatusOK) - - // Write in chunks to simulate real download - chunkSize := 8192 - for i := 0; i < len(largeContent); i += chunkSize { - end := i + chunkSize - if end > len(largeContent) { - end = len(largeContent) - } - w.Write([]byte(largeContent[i:end])) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - time.Sleep(1 * time.Millisecond) // Small delay to allow progress updates - } - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "large_file_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "large_file.txt") - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - } - - err = manager.Download(server.URL, destPath, progressCallback) - if err != nil { - t.Fatalf("Large file download failed: %v", err) - } - - // Verify file size - stat, err := os.Stat(destPath) - if err != nil { - t.Fatalf("Failed to stat downloaded file: %v", err) - } - - if stat.Size() != int64(len(largeContent)) { - t.Errorf("Expected file size %d, got %d", len(largeContent), stat.Size()) - } - - // Verify we got multiple progress updates - if len(progressUpdates) < 2 { - t.Errorf("Expected multiple progress updates for large file, got %d", len(progressUpdates)) - } - - // Verify final progress is 100% - if len(progressUpdates) > 0 { - finalProgress := progressUpdates[len(progressUpdates)-1] - if finalProgress.Percentage != 100 { - t.Errorf("Expected final percentage 100, got %f", finalProgress.Percentage) - } - } -} - -func TestDownloadResumeWithExistingFile(t *testing.T) { - fullContent := "This is the complete file content for resume testing" - partialContent := fullContent[:20] // First 20 bytes - remainingContent := fullContent[20:] // Remaining bytes - - // Create test server that supports range requests - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rangeHeader := r.Header.Get("Range") - - if rangeHeader != "" { - // Handle range request - if strings.HasPrefix(rangeHeader, "bytes=20-") { - w.Header().Set("Content-Range", fmt.Sprintf("bytes 20-%d/%d", len(fullContent)-1, len(fullContent))) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(remainingContent))) - w.WriteHeader(http.StatusPartialContent) - w.Write([]byte(remainingContent)) - return - } - } - - // Handle normal request (shouldn't happen in resume test) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(fullContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(fullContent)) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "resume_existing_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - destPath := filepath.Join(tempDir, "resume_existing.txt") - - // Create partial file first - err = os.WriteFile(destPath, []byte(partialContent), 0644) - if err != nil { - t.Fatal(err) - } - - manager := NewManager() - - // Test resume download - err = manager.DownloadWithResume(server.URL, destPath, nil) - if err != nil { - t.Fatalf("Resume download failed: %v", err) - } - - // Verify complete file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read resumed file: %v", err) - } - - if string(content) != fullContent { - t.Errorf("Expected complete content %q, got %q", fullContent, string(content)) - } -} - -func TestDownloadWithInvalidChecksum(t *testing.T) { - testContent := "Test content for checksum validation" - - tempDir, err := os.MkdirTemp("", "checksum_invalid_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - testFile := filepath.Join(tempDir, "checksum_test.txt") - err = os.WriteFile(testFile, []byte(testContent), 0644) - if err != nil { - t.Fatal(err) - } - - manager := NewManager() - - // Test with completely wrong checksum - wrongChecksum := "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" - err = manager.ValidateChecksum(testFile, wrongChecksum) - if err == nil { - t.Error("Expected checksum validation to fail with wrong checksum") - } - - if !strings.Contains(err.Error(), "checksum mismatch") { - t.Errorf("Expected checksum mismatch error, got: %v", err) - } -} - -func TestDownloadSourcesSorting(t *testing.T) { - testContent := "Test content for source sorting" - - // Create multiple servers with different priorities - server1 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fullContent := testContent + " from server1" - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(fullContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(fullContent)) - })) - defer server1.Close() - - server2 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fullContent := testContent + " from server2" - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(fullContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(fullContent)) - })) - defer server2.Close() - - server3 := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - fullContent := testContent + " from server3" - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(fullContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(fullContent)) - })) - defer server3.Close() - - tempDir, err := os.MkdirTemp("", "sorting_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "sorting_test.txt") - - // Test with sources in random order - should use highest priority (lowest number) - sources := []DownloadSource{ - {URL: server1.URL, Priority: 10, Name: "Server 1"}, // Lowest priority - {URL: server2.URL, Priority: 1, Name: "Server 2"}, // Highest priority - {URL: server3.URL, Priority: 5, Name: "Server 3"}, // Medium priority - } - - err = manager.DownloadWithSources(sources, destPath, nil) - if err != nil { - t.Fatalf("Download with sources failed: %v", err) - } - - // Should have downloaded from server2 (highest priority) - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if !strings.Contains(string(content), "from server2") { - t.Errorf("Expected content from server2, got: %s", string(content)) - } -} - -func TestDownloadProgressAccuracy(t *testing.T) { - // Create content with known size - contentSize := 50000 // 50KB - testContent := strings.Repeat("X", contentSize) - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - - // Write in small chunks to get more progress updates - chunkSize := 1024 - for i := 0; i < len(testContent); i += chunkSize { - end := i + chunkSize - if end > len(testContent) { - end = len(testContent) - } - w.Write([]byte(testContent[i:end])) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - time.Sleep(10 * time.Millisecond) // Small delay for progress updates - } - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "progress_accuracy_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "progress_test.txt") - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - - // Validate progress values are reasonable - if progress.BytesDownloaded < 0 { - t.Errorf("Negative bytes downloaded: %d", progress.BytesDownloaded) - } - if progress.Percentage < 0 || progress.Percentage > 100 { - t.Errorf("Invalid percentage: %f", progress.Percentage) - } - if progress.TotalBytes > 0 && progress.BytesDownloaded > progress.TotalBytes { - t.Errorf("Downloaded bytes (%d) exceed total bytes (%d)", progress.BytesDownloaded, progress.TotalBytes) - } - if progress.Speed < 0 { - t.Errorf("Negative speed: %d", progress.Speed) - } - } - - err = manager.Download(server.URL, destPath, progressCallback) - if err != nil { - t.Fatalf("Download failed: %v", err) - } - - // Verify we got progress updates - if len(progressUpdates) == 0 { - t.Error("Expected at least one progress update") - } - - // Verify progress is monotonically increasing - for i := 1; i < len(progressUpdates); i++ { - if progressUpdates[i].BytesDownloaded < progressUpdates[i-1].BytesDownloaded { - t.Errorf("Progress went backwards: %d -> %d", - progressUpdates[i-1].BytesDownloaded, - progressUpdates[i].BytesDownloaded) - } - } - - // Verify final progress - if len(progressUpdates) > 0 { - final := progressUpdates[len(progressUpdates)-1] - if final.Percentage != 100 { - t.Errorf("Expected final percentage 100, got %f", final.Percentage) - } - if final.BytesDownloaded != int64(contentSize) { - t.Errorf("Expected final bytes %d, got %d", contentSize, final.BytesDownloaded) - } - } -} - -func TestTestSpeeds(t *testing.T) { - testContent := strings.Repeat("A", 64*1024) // 64KB content for speed testing (smaller size) - - // Create fast server - fastServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Connection", "close") // Ensure connection is closed - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer fastServer.Close() - - // Create slow server - slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Connection", "close") // Ensure connection is closed - w.WriteHeader(http.StatusOK) - - // Write slowly - chunkSize := 1024 - for i := 0; i < len(testContent); i += chunkSize { - end := i + chunkSize - if end > len(testContent) { - end = len(testContent) - } - w.Write([]byte(testContent[i:end])) - if f, ok := w.(http.Flusher); ok { - f.Flush() - } - time.Sleep(10 * time.Millisecond) // Reduced delay - } - })) - defer slowServer.Close() - - // Create failing server - failingServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Connection", "close") // Ensure connection is closed - w.WriteHeader(http.StatusInternalServerError) - })) - defer failingServer.Close() - - manager := NewManager() - - sources := []DownloadSource{ - {URL: fastServer.URL, Priority: 1, Name: "Fast Server"}, - {URL: slowServer.URL, Priority: 2, Name: "Slow Server"}, - {URL: failingServer.URL, Priority: 3, Name: "Failing Server"}, - } - - testSize := int64(32 * 1024) // 32KB test size (smaller) - timeout := 5 * time.Second // Shorter timeout - - results, err := manager.TestSpeeds(sources, testSize, timeout) - if err != nil { - t.Fatalf("Speed test failed: %v", err) - } - - if len(results) != len(sources) { - t.Errorf("Expected %d results, got %d", len(sources), len(results)) - } - - // Results should be sorted by speed (descending) - for i := 1; i < len(results); i++ { - if results[i-1].Error == nil && results[i].Error == nil { - if results[i-1].Speed < results[i].Speed { - t.Errorf("Results not sorted by speed: %f < %f", results[i-1].Speed, results[i].Speed) - } - } - } - - // Fast server should have higher speed than slow server (if both succeed) - var fastResult, slowResult *SpeedTestResult - for _, result := range results { - if result.Source.Name == "Fast Server" { - fastResult = &result - } else if result.Source.Name == "Slow Server" { - slowResult = &result - } - } - - if fastResult != nil && slowResult != nil { - if fastResult.Error == nil && slowResult.Error == nil { - if fastResult.Speed <= slowResult.Speed { - t.Logf("Fast server speed: %f MB/s, Slow server speed: %f MB/s", - fastResult.Speed, slowResult.Speed) - // Note: Due to the small test size, speeds might be similar, so we'll just log instead of failing - } - } - } - - // Failing server should have an error - var failingResult *SpeedTestResult - for _, result := range results { - if result.Source.Name == "Failing Server" { - failingResult = &result - } - } - - if failingResult != nil && failingResult.Error == nil { - t.Error("Failing server should have an error") - } - - // Give servers time to close connections properly - time.Sleep(100 * time.Millisecond) -} - -func TestDownloadMultiThreaded(t *testing.T) { - // Create large test content (1MB) - contentSize := 1024 * 1024 - testContent := strings.Repeat("B", contentSize) - - // Create server that supports range requests - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rangeHeader := r.Header.Get("Range") - - if rangeHeader != "" { - // Parse range header (simplified for testing) - var start, end int64 - if n, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); n == 2 && err == nil { - if start >= 0 && end < int64(len(testContent)) && start <= end { - content := testContent[start:end+1] - w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(testContent))) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) - w.WriteHeader(http.StatusPartialContent) - w.Write([]byte(content)) - return - } - } - } - - // Handle HEAD request - if r.Method == "HEAD" { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Accept-Ranges", "bytes") - w.WriteHeader(http.StatusOK) - return - } - - // Handle normal GET request - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Accept-Ranges", "bytes") - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "multithread_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "multithread_test.txt") - - config := MultiThreadConfig{ - ThreadCount: 4, - ChunkSize: 0, // Use default chunk size - } - - var progressUpdates []DownloadProgress - progressCallback := func(progress DownloadProgress) { - progressUpdates = append(progressUpdates, progress) - } - - err = manager.DownloadMultiThreaded(server.URL, destPath, config, progressCallback) - if err != nil { - t.Fatalf("Multi-threaded download failed: %v", err) - } - - // Verify file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if len(content) != contentSize { - t.Errorf("Expected content size %d, got %d", contentSize, len(content)) - } - - if string(content) != testContent { - t.Error("Downloaded content doesn't match original") - } - - // Verify progress updates - if len(progressUpdates) == 0 { - t.Error("No progress updates received") - } - - // Final progress should be 100% - if len(progressUpdates) > 0 { - finalProgress := progressUpdates[len(progressUpdates)-1] - if finalProgress.Percentage != 100 { - t.Errorf("Expected final percentage 100, got %f", finalProgress.Percentage) - } - } -} - -func TestDownloadMultiThreadedFallback(t *testing.T) { - testContent := "Test content for fallback" - - // Create server that doesn't support range requests - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == "HEAD" { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - // Don't set Accept-Ranges header - w.WriteHeader(http.StatusOK) - return - } - - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "multithread_fallback_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "fallback_test.txt") - - config := MultiThreadConfig{ - ThreadCount: 4, - } - - // Should fallback to single-threaded download - err = manager.DownloadMultiThreaded(server.URL, destPath, config, nil) - if err != nil { - t.Fatalf("Fallback download failed: %v", err) - } - - // Verify file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content %q, got %q", testContent, string(content)) - } -} - -func TestDownloadMultiThreadedNoContentLength(t *testing.T) { - testContent := "Test content without content length" - - // Create server that doesn't provide content length - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if r.Method == "HEAD" { - // Don't set Content-Length header - w.WriteHeader(http.StatusOK) - return - } - - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "multithread_no_length_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "no_length_test.txt") - - config := MultiThreadConfig{ - ThreadCount: 4, - } - - // Should fallback to single-threaded download - err = manager.DownloadMultiThreaded(server.URL, destPath, config, nil) - if err != nil { - t.Fatalf("No content length download failed: %v", err) - } - - // Verify file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content %q, got %q", testContent, string(content)) - } -} - -func TestSpeedTestTimeout(t *testing.T) { - // Create slow server that will timeout - slowServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Connection", "close") // Ensure connection is closed - time.Sleep(2 * time.Second) // Longer than timeout - w.WriteHeader(http.StatusOK) - w.Write([]byte("slow content")) - })) - defer slowServer.Close() - - manager := NewManager() - - sources := []DownloadSource{ - {URL: slowServer.URL, Priority: 1, Name: "Slow Server"}, - } - - testSize := int64(1024) - timeout := 500 * time.Millisecond // Short timeout - - results, err := manager.TestSpeeds(sources, testSize, timeout) - if err != nil { - t.Fatalf("Speed test failed: %v", err) - } - - if len(results) != 1 { - t.Errorf("Expected 1 result, got %d", len(results)) - } - - // Should have timed out - if results[0].Error == nil { - t.Error("Expected timeout error") - } - - // Give server time to close connections properly - time.Sleep(100 * time.Millisecond) -} - -func TestDownloadMultiThreadedChunkMerging(t *testing.T) { - // Create content with distinct patterns for each chunk - chunk1 := strings.Repeat("1", 1024) - chunk2 := strings.Repeat("2", 1024) - chunk3 := strings.Repeat("3", 1024) - chunk4 := strings.Repeat("4", 1024) - testContent := chunk1 + chunk2 + chunk3 + chunk4 - - // Create server that supports range requests - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rangeHeader := r.Header.Get("Range") - - if rangeHeader != "" { - var start, end int64 - if n, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); n == 2 && err == nil { - if start >= 0 && end < int64(len(testContent)) && start <= end { - content := testContent[start:end+1] - w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(testContent))) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) - w.WriteHeader(http.StatusPartialContent) - w.Write([]byte(content)) - return - } - } - } - - if r.Method == "HEAD" { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Accept-Ranges", "bytes") - w.WriteHeader(http.StatusOK) - return - } - - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Accept-Ranges", "bytes") - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "chunk_merge_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "chunk_merge_test.txt") - - config := MultiThreadConfig{ - ThreadCount: 4, - ChunkSize: 1024, // Each chunk is exactly 1024 bytes - } - - err = manager.DownloadMultiThreaded(server.URL, destPath, config, nil) - if err != nil { - t.Fatalf("Multi-threaded download failed: %v", err) - } - - // Verify file content is correctly merged - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Error("Chunks were not merged correctly") - - // Debug: check each chunk - if len(content) >= 1024 && string(content[0:1024]) != chunk1 { - t.Error("Chunk 1 incorrect") - } - if len(content) >= 2048 && string(content[1024:2048]) != chunk2 { - t.Error("Chunk 2 incorrect") - } - if len(content) >= 3072 && string(content[2048:3072]) != chunk3 { - t.Error("Chunk 3 incorrect") - } - if len(content) >= 4096 && string(content[3072:4096]) != chunk4 { - t.Error("Chunk 4 incorrect") - } - } - - // Verify no temporary chunk files remain - for i := 0; i < 4; i++ { - chunkFile := fmt.Sprintf("%s.part%d", destPath, i) - if _, err := os.Stat(chunkFile); !os.IsNotExist(err) { - t.Errorf("Temporary chunk file %s should have been removed", chunkFile) - } - } -} - -func TestSpeedTestEmptySources(t *testing.T) { - manager := NewManager() - - var sources []DownloadSource - testSize := int64(1024) - timeout := 10 * time.Second - - results, err := manager.TestSpeeds(sources, testSize, timeout) - if err == nil { - t.Error("Expected error for empty sources") - } - - if results != nil { - t.Error("Expected nil results for empty sources") - } - - if !strings.Contains(err.Error(), "no sources provided") { - t.Errorf("Expected 'no sources provided' error, got: %v", err) - } -} - -func TestDownloadMultiThreadedDefaultConfig(t *testing.T) { - testContent := strings.Repeat("C", 8192) // 8KB content - - server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - rangeHeader := r.Header.Get("Range") - - if rangeHeader != "" { - var start, end int64 - if n, err := fmt.Sscanf(rangeHeader, "bytes=%d-%d", &start, &end); n == 2 && err == nil { - if start >= 0 && end < int64(len(testContent)) && start <= end { - content := testContent[start:end+1] - w.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, len(testContent))) - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(content))) - w.WriteHeader(http.StatusPartialContent) - w.Write([]byte(content)) - return - } - } - } - - if r.Method == "HEAD" { - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Accept-Ranges", "bytes") - w.WriteHeader(http.StatusOK) - return - } - - w.Header().Set("Content-Length", fmt.Sprintf("%d", len(testContent))) - w.Header().Set("Accept-Ranges", "bytes") - w.WriteHeader(http.StatusOK) - w.Write([]byte(testContent)) - })) - defer server.Close() - - tempDir, err := os.MkdirTemp("", "default_config_test") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDir) - - manager := NewManager() - destPath := filepath.Join(tempDir, "default_config_test.txt") - - // Test with zero thread count (should default to 4) - config := MultiThreadConfig{ - ThreadCount: 0, - } - - err = manager.DownloadMultiThreaded(server.URL, destPath, config, nil) - if err != nil { - t.Fatalf("Default config download failed: %v", err) - } - - // Verify file content - content, err := os.ReadFile(destPath) - if err != nil { - t.Fatalf("Failed to read downloaded file: %v", err) - } - - if string(content) != testContent { - t.Errorf("Expected content length %d, got %d", len(testContent), len(content)) - } -} \ No newline at end of file diff --git a/Go_Updater/errors/errors.go b/Go_Updater/errors/errors.go deleted file mode 100644 index f6a7742..0000000 --- a/Go_Updater/errors/errors.go +++ /dev/null @@ -1,219 +0,0 @@ -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 "发生未知错误,请联系技术支持" -} \ No newline at end of file diff --git a/Go_Updater/errors/errors_test.go b/Go_Updater/errors/errors_test.go deleted file mode 100644 index bb8dd3f..0000000 --- a/Go_Updater/errors/errors_test.go +++ /dev/null @@ -1,287 +0,0 @@ -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) - } - }) -} \ No newline at end of file diff --git a/Go_Updater/go.mod b/Go_Updater/go.mod deleted file mode 100644 index 143c5e4..0000000 --- a/Go_Updater/go.mod +++ /dev/null @@ -1,42 +0,0 @@ -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 -) diff --git a/Go_Updater/go.sum b/Go_Updater/go.sum deleted file mode 100644 index 83677a5..0000000 --- a/Go_Updater/go.sum +++ /dev/null @@ -1,80 +0,0 @@ -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= diff --git a/Go_Updater/gui/manager.go b/Go_Updater/gui/manager.go deleted file mode 100644 index d2b8350..0000000 --- a/Go_Updater/gui/manager.go +++ /dev/null @@ -1,513 +0,0 @@ -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() - } -} diff --git a/Go_Updater/gui/manager_test.go b/Go_Updater/gui/manager_test.go deleted file mode 100644 index c03be1b..0000000 --- a/Go_Updater/gui/manager_test.go +++ /dev/null @@ -1,227 +0,0 @@ -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)) - } -} \ No newline at end of file diff --git a/Go_Updater/icon/AUTO_MAA_Go_Updater.ico b/Go_Updater/icon/AUTO_MAA_Go_Updater.ico deleted file mode 100644 index 5520beb..0000000 Binary files a/Go_Updater/icon/AUTO_MAA_Go_Updater.ico and /dev/null differ diff --git a/Go_Updater/icon/AUTO_MAA_Go_Updater.png b/Go_Updater/icon/AUTO_MAA_Go_Updater.png deleted file mode 100644 index 630a52b..0000000 Binary files a/Go_Updater/icon/AUTO_MAA_Go_Updater.png and /dev/null differ diff --git a/Go_Updater/install/manager.go b/Go_Updater/install/manager.go deleted file mode 100644 index 88fe26a..0000000 --- a/Go_Updater/install/manager.go +++ /dev/null @@ -1,474 +0,0 @@ -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 - }) -} diff --git a/Go_Updater/install/manager_test.go b/Go_Updater/install/manager_test.go deleted file mode 100644 index 5bf9381..0000000 --- a/Go_Updater/install/manager_test.go +++ /dev/null @@ -1,1033 +0,0 @@ -package install - -import ( - "archive/zip" - "fmt" - "os" - "path/filepath" - "testing" -) - -func TestNewManager(t *testing.T) { - manager := NewManager() - if manager == nil { - t.Fatal("NewManager() returned nil") - } - if manager.tempDirs == nil { - t.Fatal("tempDirs slice not initialized") - } -} - -func TestCreateTempDir(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - - // Verify directory exists - if _, err := os.Stat(tempDir); os.IsNotExist(err) { - t.Fatalf("Temp directory was not created: %s", tempDir) - } - - // Verify it's tracked - if len(manager.tempDirs) != 1 || manager.tempDirs[0] != tempDir { - t.Fatalf("Temp directory not properly tracked") - } - - // Cleanup - defer manager.CleanupTempDir(tempDir) -} - -func TestCleanupTempDir(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - - // Create a test file in temp directory - testFile := filepath.Join(tempDir, "test.txt") - if err := os.WriteFile(testFile, []byte("test"), 0644); err != nil { - t.Fatalf("Failed to create test file: %v", err) - } - - // Cleanup - err = manager.CleanupTempDir(tempDir) - if err != nil { - t.Fatalf("CleanupTempDir() failed: %v", err) - } - - // Verify directory is removed - if _, err := os.Stat(tempDir); !os.IsNotExist(err) { - t.Fatalf("Temp directory was not removed: %s", tempDir) - } - - // Verify it's no longer tracked - if len(manager.tempDirs) != 0 { - t.Fatalf("Temp directory still tracked after cleanup") - } -} - -func TestExtractZip(t *testing.T) { - manager := NewManager() - - // Create a temporary ZIP file for testing - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - zipPath := filepath.Join(tempDir, "test.zip") - extractDir := filepath.Join(tempDir, "extract") - - // Create test ZIP file - if err := createTestZip(zipPath); err != nil { - t.Fatalf("Failed to create test ZIP: %v", err) - } - - // Extract ZIP - err = manager.ExtractZip(zipPath, extractDir) - if err != nil { - t.Fatalf("ExtractZip() failed: %v", err) - } - - // Verify extracted files - testFile := filepath.Join(extractDir, "test.txt") - if _, err := os.Stat(testFile); os.IsNotExist(err) { - t.Fatalf("Extracted file not found: %s", testFile) - } - - // Verify file contents - content, err := os.ReadFile(testFile) - if err != nil { - t.Fatalf("Failed to read extracted file: %v", err) - } - - expected := "Hello, World!" - if string(content) != expected { - t.Fatalf("File content mismatch. Expected: %s, Got: %s", expected, string(content)) - } - - // Verify directory structure - subDir := filepath.Join(extractDir, "subdir") - if _, err := os.Stat(subDir); os.IsNotExist(err) { - t.Fatalf("Extracted subdirectory not found: %s", subDir) - } - - subFile := filepath.Join(subDir, "sub.txt") - if _, err := os.Stat(subFile); os.IsNotExist(err) { - t.Fatalf("Extracted subdirectory file not found: %s", subFile) - } -} - -func TestExtractZipInvalidPath(t *testing.T) { - manager := NewManager() - - // Test with non-existent ZIP file - err := manager.ExtractZip("nonexistent.zip", "dest") - if err == nil { - t.Fatal("ExtractZip() should fail with non-existent ZIP file") - } -} - -func TestExtractZipDirectoryTraversal(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - zipPath := filepath.Join(tempDir, "malicious.zip") - extractDir := filepath.Join(tempDir, "extract") - - // Create ZIP with directory traversal attempt - if err := createMaliciousZip(zipPath); err != nil { - t.Fatalf("Failed to create malicious ZIP: %v", err) - } - - // Extract should fail or sanitize the path - err = manager.ExtractZip(zipPath, extractDir) - if err != nil { - // This is expected behavior - the extraction should fail - return - } - - // If extraction succeeded, verify no files were created outside extract dir - parentDir := filepath.Dir(extractDir) - maliciousFile := filepath.Join(parentDir, "malicious.txt") - if _, err := os.Stat(maliciousFile); !os.IsNotExist(err) { - t.Fatal("Directory traversal attack succeeded - malicious file created outside extract directory") - } -} - -// Helper function to create a test ZIP file -func createTestZip(zipPath string) error { - file, err := os.Create(zipPath) - if err != nil { - return err - } - defer file.Close() - - writer := zip.NewWriter(file) - defer writer.Close() - - // Add a test file - f1, err := writer.Create("test.txt") - if err != nil { - return err - } - _, err = f1.Write([]byte("Hello, World!")) - if err != nil { - return err - } - - // Add a subdirectory and file - f2, err := writer.Create("subdir/sub.txt") - if err != nil { - return err - } - _, err = f2.Write([]byte("Subdirectory file")) - if err != nil { - return err - } - - return nil -} - -func TestProcessChanges(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Test with valid changes.json - changesPath := filepath.Join(tempDir, "changes.json") - changesData := `{ - "deleted": ["old_file.txt", "deprecated/module.dll"], - "added": ["new_file.txt", "features/new_module.dll"], - "modified": ["main.exe", "config.ini"] - }` - - if err := os.WriteFile(changesPath, []byte(changesData), 0644); err != nil { - t.Fatalf("Failed to create test changes.json: %v", err) - } - - changes, err := manager.ProcessChanges(changesPath) - if err != nil { - t.Fatalf("ProcessChanges() failed: %v", err) - } - - // Verify parsed data - if len(changes.Deleted) != 2 { - t.Fatalf("Expected 2 deleted files, got %d", len(changes.Deleted)) - } - if changes.Deleted[0] != "old_file.txt" { - t.Fatalf("Expected first deleted file to be 'old_file.txt', got '%s'", changes.Deleted[0]) - } - - if len(changes.Added) != 2 { - t.Fatalf("Expected 2 added files, got %d", len(changes.Added)) - } - - if len(changes.Modified) != 2 { - t.Fatalf("Expected 2 modified files, got %d", len(changes.Modified)) - } -} - -func TestProcessChangesNonExistent(t *testing.T) { - manager := NewManager() - - // Test with non-existent changes.json - changes, err := manager.ProcessChanges("nonexistent.json") - if err != nil { - t.Fatalf("ProcessChanges() should not fail with non-existent file: %v", err) - } - - // Should return empty changes - if len(changes.Deleted) != 0 || len(changes.Added) != 0 || len(changes.Modified) != 0 { - t.Fatalf("Expected empty changes for non-existent file") - } -} - -func TestProcessChangesInvalidJSON(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Test with invalid JSON - changesPath := filepath.Join(tempDir, "invalid.json") - invalidData := `{"deleted": ["file1.txt", "file2.txt"` // Missing closing bracket - - if err := os.WriteFile(changesPath, []byte(invalidData), 0644); err != nil { - t.Fatalf("Failed to create invalid JSON file: %v", err) - } - - _, err = manager.ProcessChanges(changesPath) - if err == nil { - t.Fatal("ProcessChanges() should fail with invalid JSON") - } -} - -func TestHandleRunningProcess(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create a test executable file - testExe := filepath.Join(tempDir, "test.exe") - if err := os.WriteFile(testExe, []byte("test executable"), 0755); err != nil { - t.Fatalf("Failed to create test executable: %v", err) - } - - // Test handling non-existent process - err = manager.HandleRunningProcess("nonexistent.exe") - if err != nil { - t.Fatalf("HandleRunningProcess() should not fail with non-existent process: %v", err) - } -} - -func TestDeleteMarkedFiles(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create test files to be deleted - testFile1 := filepath.Join(tempDir, "file1.txt") - testFile2 := filepath.Join(tempDir, "file2.txt") - - if err := os.WriteFile(testFile1, []byte("test1"), 0644); err != nil { - t.Fatalf("Failed to create test file1: %v", err) - } - if err := os.WriteFile(testFile2, []byte("test2"), 0644); err != nil { - t.Fatalf("Failed to create test file2: %v", err) - } - - // Create marker files - marker1 := testFile1 + ".delete_on_restart" - marker2 := testFile2 + ".delete_on_restart" - - if err := os.WriteFile(marker1, []byte(testFile1), 0644); err != nil { - t.Fatalf("Failed to create marker file1: %v", err) - } - if err := os.WriteFile(marker2, []byte(testFile2), 0644); err != nil { - t.Fatalf("Failed to create marker file2: %v", err) - } - - // Delete marked files - err = manager.DeleteMarkedFiles(tempDir) - if err != nil { - t.Fatalf("DeleteMarkedFiles() failed: %v", err) - } - - // Verify files are deleted - if _, err := os.Stat(testFile1); !os.IsNotExist(err) { - t.Fatalf("Test file1 should be deleted") - } - if _, err := os.Stat(testFile2); !os.IsNotExist(err) { - t.Fatalf("Test file2 should be deleted") - } - - // Verify marker files are deleted - if _, err := os.Stat(marker1); !os.IsNotExist(err) { - t.Fatalf("Marker file1 should be deleted") - } - if _, err := os.Stat(marker2); !os.IsNotExist(err) { - t.Fatalf("Marker file2 should be deleted") - } -} - -func TestApplyUpdate(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create source and target directories - sourceDir := filepath.Join(tempDir, "source") - targetDir := filepath.Join(tempDir, "target") - - if err := os.MkdirAll(sourceDir, 0755); err != nil { - t.Fatalf("Failed to create source directory: %v", err) - } - if err := os.MkdirAll(targetDir, 0755); err != nil { - t.Fatalf("Failed to create target directory: %v", err) - } - - // Create test files in source directory - newFile := filepath.Join(sourceDir, "new_file.txt") - modifiedFile := filepath.Join(sourceDir, "modified_file.txt") - - if err := os.WriteFile(newFile, []byte("new content"), 0644); err != nil { - t.Fatalf("Failed to create new file: %v", err) - } - if err := os.WriteFile(modifiedFile, []byte("updated content"), 0644); err != nil { - t.Fatalf("Failed to create modified file: %v", err) - } - - // Create existing files in target directory - existingModified := filepath.Join(targetDir, "modified_file.txt") - existingDeleted := filepath.Join(targetDir, "deleted_file.txt") - - if err := os.WriteFile(existingModified, []byte("old content"), 0644); err != nil { - t.Fatalf("Failed to create existing modified file: %v", err) - } - if err := os.WriteFile(existingDeleted, []byte("to be deleted"), 0644); err != nil { - t.Fatalf("Failed to create file to be deleted: %v", err) - } - - // Define changes - changes := &ChangesInfo{ - Added: []string{"new_file.txt"}, - Modified: []string{"modified_file.txt"}, - Deleted: []string{"deleted_file.txt"}, - } - - // Apply update - err = manager.ApplyUpdate(sourceDir, targetDir, changes) - if err != nil { - t.Fatalf("ApplyUpdate() failed: %v", err) - } - - // Verify new file was added - newTargetFile := filepath.Join(targetDir, "new_file.txt") - if _, err := os.Stat(newTargetFile); os.IsNotExist(err) { - t.Fatalf("New file was not added to target directory") - } - - // Verify modified file was updated - content, err := os.ReadFile(existingModified) - if err != nil { - t.Fatalf("Failed to read modified file: %v", err) - } - if string(content) != "updated content" { - t.Fatalf("Modified file content incorrect. Expected: 'updated content', Got: '%s'", string(content)) - } - - // Verify deleted file was removed - if _, err := os.Stat(existingDeleted); !os.IsNotExist(err) { - t.Fatalf("Deleted file still exists") - } -} - -func TestApplyUpdateWithRollback(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create source and target directories - sourceDir := filepath.Join(tempDir, "source") - targetDir := filepath.Join(tempDir, "target") - - if err := os.MkdirAll(sourceDir, 0755); err != nil { - t.Fatalf("Failed to create source directory: %v", err) - } - if err := os.MkdirAll(targetDir, 0755); err != nil { - t.Fatalf("Failed to create target directory: %v", err) - } - - // Create existing file in target directory - existingFile := filepath.Join(targetDir, "existing_file.txt") - originalContent := "original content" - if err := os.WriteFile(existingFile, []byte(originalContent), 0644); err != nil { - t.Fatalf("Failed to create existing file: %v", err) - } - - // Create a source file that will cause a copy failure by making target read-only - sourceFile := filepath.Join(sourceDir, "existing_file.txt") - if err := os.WriteFile(sourceFile, []byte("new content"), 0644); err != nil { - t.Fatalf("Failed to create source file: %v", err) - } - - // Make target directory read-only to cause copy failure - readOnlyDir := filepath.Join(targetDir, "readonly") - if err := os.MkdirAll(readOnlyDir, 0755); err != nil { - t.Fatalf("Failed to create readonly directory: %v", err) - } - - // Create a file in readonly directory that we'll try to modify - readOnlyFile := filepath.Join(readOnlyDir, "readonly_file.txt") - if err := os.WriteFile(readOnlyFile, []byte("readonly content"), 0644); err != nil { - t.Fatalf("Failed to create readonly file: %v", err) - } - - // Create source file for readonly file - sourceReadOnlyFile := filepath.Join(sourceDir, "readonly", "readonly_file.txt") - if err := os.MkdirAll(filepath.Dir(sourceReadOnlyFile), 0755); err != nil { - t.Fatalf("Failed to create source readonly directory: %v", err) - } - if err := os.WriteFile(sourceReadOnlyFile, []byte("new readonly content"), 0644); err != nil { - t.Fatalf("Failed to create source readonly file: %v", err) - } - - // Make the readonly directory read-only (Windows specific) - if err := os.Chmod(readOnlyDir, 0444); err != nil { - t.Fatalf("Failed to make directory read-only: %v", err) - } - - // Restore permissions after test - defer func() { - os.Chmod(readOnlyDir, 0755) - os.RemoveAll(readOnlyDir) - }() - - // Define changes that will cause failure due to read-only directory - changes := &ChangesInfo{ - Modified: []string{"existing_file.txt", "readonly/readonly_file.txt"}, - } - - // Apply update (should fail and rollback) - err = manager.ApplyUpdate(sourceDir, targetDir, changes) - if err == nil { - // On some systems, the read-only test might not work as expected - // Let's just verify the update completed successfully in this case - t.Log("Update completed successfully (read-only test may not work on this system)") - return - } - - // Verify rollback occurred - original file should be restored - content, err := os.ReadFile(existingFile) - if err != nil { - t.Fatalf("Failed to read file after rollback: %v", err) - } - if string(content) != originalContent { - t.Fatalf("Rollback failed. Expected: '%s', Got: '%s'", originalContent, string(content)) - } -} - -func TestCopyFileWithDirs(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create source file - srcFile := filepath.Join(tempDir, "source.txt") - content := "test content" - if err := os.WriteFile(srcFile, []byte(content), 0644); err != nil { - t.Fatalf("Failed to create source file: %v", err) - } - - // Copy to destination with nested directories - dstFile := filepath.Join(tempDir, "nested", "dir", "destination.txt") - - err = manager.copyFileWithDirs(srcFile, dstFile) - if err != nil { - t.Fatalf("copyFileWithDirs() failed: %v", err) - } - - // Verify file was copied - if _, err := os.Stat(dstFile); os.IsNotExist(err) { - t.Fatalf("Destination file was not created") - } - - // Verify content - dstContent, err := os.ReadFile(dstFile) - if err != nil { - t.Fatalf("Failed to read destination file: %v", err) - } - if string(dstContent) != content { - t.Fatalf("File content mismatch. Expected: '%s', Got: '%s'", content, string(dstContent)) - } - - // Verify parent directories were created - parentDir := filepath.Join(tempDir, "nested", "dir") - if _, err := os.Stat(parentDir); os.IsNotExist(err) { - t.Fatalf("Parent directories were not created") - } -} - -// Helper function to create a malicious ZIP file with directory traversal -func createMaliciousZip(zipPath string) error { - file, err := os.Create(zipPath) - if err != nil { - return err - } - defer file.Close() - - writer := zip.NewWriter(file) - defer writer.Close() - - // Add a file with directory traversal path - f1, err := writer.Create("../malicious.txt") - if err != nil { - return err - } - _, err = f1.Write([]byte("This should not be extracted outside the target directory")) - if err != nil { - return err - } - - return nil -} - -func TestCleanupAllTempDirs(t *testing.T) { - manager := NewManager() - - // Create multiple temp directories - tempDir1, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("Failed to create temp dir 1: %v", err) - } - - tempDir2, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("Failed to create temp dir 2: %v", err) - } - - tempDir3, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("Failed to create temp dir 3: %v", err) - } - - // Verify all directories exist - for _, dir := range []string{tempDir1, tempDir2, tempDir3} { - if _, err := os.Stat(dir); os.IsNotExist(err) { - t.Fatalf("Temp directory should exist: %s", dir) - } - } - - // Verify manager is tracking all directories - if len(manager.tempDirs) != 3 { - t.Fatalf("Expected 3 tracked temp dirs, got %d", len(manager.tempDirs)) - } - - // Cleanup all temp directories - err = manager.CleanupAllTempDirs() - if err != nil { - t.Fatalf("CleanupAllTempDirs failed: %v", err) - } - - // Verify all directories are removed - for _, dir := range []string{tempDir1, tempDir2, tempDir3} { - if _, err := os.Stat(dir); !os.IsNotExist(err) { - t.Fatalf("Temp directory should be removed: %s", dir) - } - } - - // Verify manager is no longer tracking directories - if len(manager.tempDirs) != 0 { - t.Fatalf("Expected 0 tracked temp dirs after cleanup, got %d", len(manager.tempDirs)) - } -} - -func TestExtractZipWithNestedDirectories(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - zipPath := filepath.Join(tempDir, "nested.zip") - extractDir := filepath.Join(tempDir, "extract") - - // Create ZIP with nested directory structure - if err := createNestedZip(zipPath); err != nil { - t.Fatalf("Failed to create nested ZIP: %v", err) - } - - // Extract ZIP - err = manager.ExtractZip(zipPath, extractDir) - if err != nil { - t.Fatalf("ExtractZip() failed: %v", err) - } - - // Verify nested structure was created - expectedFiles := []string{ - "level1/file1.txt", - "level1/level2/file2.txt", - "level1/level2/level3/file3.txt", - } - - for _, expectedFile := range expectedFiles { - fullPath := filepath.Join(extractDir, expectedFile) - if _, err := os.Stat(fullPath); os.IsNotExist(err) { - t.Fatalf("Expected nested file not found: %s", expectedFile) - } - - // Verify file content - content, err := os.ReadFile(fullPath) - if err != nil { - t.Fatalf("Failed to read nested file %s: %v", expectedFile, err) - } - - expectedContent := fmt.Sprintf("Content of %s", filepath.Base(expectedFile)) - if string(content) != expectedContent { - t.Fatalf("File content mismatch for %s. Expected: %s, Got: %s", - expectedFile, expectedContent, string(content)) - } - } -} - -func TestProcessChangesWithComplexStructure(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create complex changes.json with nested paths - changesPath := filepath.Join(tempDir, "complex_changes.json") - changesData := `{ - "deleted": [ - "old/legacy/file1.txt", - "deprecated/module.dll", - "temp/cache.dat" - ], - "added": [ - "new/features/feature1.dll", - "resources/icons/icon.png", - "config/new_settings.json" - ], - "modified": [ - "core/main.exe", - "lib/utils.dll", - "data/database.db" - ] - }` - - if err := os.WriteFile(changesPath, []byte(changesData), 0644); err != nil { - t.Fatalf("Failed to create complex changes.json: %v", err) - } - - changes, err := manager.ProcessChanges(changesPath) - if err != nil { - t.Fatalf("ProcessChanges() failed: %v", err) - } - - // Verify all changes were parsed correctly - expectedDeleted := []string{"old/legacy/file1.txt", "deprecated/module.dll", "temp/cache.dat"} - expectedAdded := []string{"new/features/feature1.dll", "resources/icons/icon.png", "config/new_settings.json"} - expectedModified := []string{"core/main.exe", "lib/utils.dll", "data/database.db"} - - if len(changes.Deleted) != len(expectedDeleted) { - t.Fatalf("Expected %d deleted files, got %d", len(expectedDeleted), len(changes.Deleted)) - } - - for i, expected := range expectedDeleted { - if changes.Deleted[i] != expected { - t.Errorf("Deleted[%d]: expected %s, got %s", i, expected, changes.Deleted[i]) - } - } - - if len(changes.Added) != len(expectedAdded) { - t.Fatalf("Expected %d added files, got %d", len(expectedAdded), len(changes.Added)) - } - - for i, expected := range expectedAdded { - if changes.Added[i] != expected { - t.Errorf("Added[%d]: expected %s, got %s", i, expected, changes.Added[i]) - } - } - - if len(changes.Modified) != len(expectedModified) { - t.Fatalf("Expected %d modified files, got %d", len(expectedModified), len(changes.Modified)) - } - - for i, expected := range expectedModified { - if changes.Modified[i] != expected { - t.Errorf("Modified[%d]: expected %s, got %s", i, expected, changes.Modified[i]) - } - } -} - -func TestApplyUpdateWithNestedPaths(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create source and target directories - sourceDir := filepath.Join(tempDir, "source") - targetDir := filepath.Join(tempDir, "target") - - if err := os.MkdirAll(sourceDir, 0755); err != nil { - t.Fatalf("Failed to create source directory: %v", err) - } - if err := os.MkdirAll(targetDir, 0755); err != nil { - t.Fatalf("Failed to create target directory: %v", err) - } - - // Create nested source files - nestedFiles := map[string]string{ - "level1/new_file.txt": "New file content", - "level1/level2/modified.txt": "Modified content", - "features/feature1/config.json": `{"enabled": true}`, - } - - for filePath, content := range nestedFiles { - fullPath := filepath.Join(sourceDir, filePath) - if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { - t.Fatalf("Failed to create source directory for %s: %v", filePath, err) - } - if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { - t.Fatalf("Failed to create source file %s: %v", filePath, err) - } - } - - // Create existing target files - existingFiles := map[string]string{ - "level1/level2/modified.txt": "Old content", - "old/deprecated.txt": "To be deleted", - } - - for filePath, content := range existingFiles { - fullPath := filepath.Join(targetDir, filePath) - if err := os.MkdirAll(filepath.Dir(fullPath), 0755); err != nil { - t.Fatalf("Failed to create target directory for %s: %v", filePath, err) - } - if err := os.WriteFile(fullPath, []byte(content), 0644); err != nil { - t.Fatalf("Failed to create target file %s: %v", filePath, err) - } - } - - // Define changes with nested paths - changes := &ChangesInfo{ - Added: []string{"level1/new_file.txt", "features/feature1/config.json"}, - Modified: []string{"level1/level2/modified.txt"}, - Deleted: []string{"old/deprecated.txt"}, - } - - // Apply update - err = manager.ApplyUpdate(sourceDir, targetDir, changes) - if err != nil { - t.Fatalf("ApplyUpdate() failed: %v", err) - } - - // Verify added files - for _, addedFile := range changes.Added { - targetFile := filepath.Join(targetDir, addedFile) - if _, err := os.Stat(targetFile); os.IsNotExist(err) { - t.Fatalf("Added file not found: %s", addedFile) - } - - // Verify content matches source - sourceFile := filepath.Join(sourceDir, addedFile) - sourceContent, _ := os.ReadFile(sourceFile) - targetContent, _ := os.ReadFile(targetFile) - - if string(sourceContent) != string(targetContent) { - t.Fatalf("Content mismatch for added file %s", addedFile) - } - } - - // Verify modified files - modifiedFile := filepath.Join(targetDir, "level1/level2/modified.txt") - content, err := os.ReadFile(modifiedFile) - if err != nil { - t.Fatalf("Failed to read modified file: %v", err) - } - if string(content) != "Modified content" { - t.Fatalf("Modified file content incorrect. Expected: 'Modified content', Got: '%s'", string(content)) - } - - // Verify deleted files - deletedFile := filepath.Join(targetDir, "old/deprecated.txt") - if _, err := os.Stat(deletedFile); !os.IsNotExist(err) { - t.Fatalf("Deleted file still exists: %s", deletedFile) - } -} - -func TestMarkFileForDeletion(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - // Create a test file - testFile := filepath.Join(tempDir, "test.exe") - if err := os.WriteFile(testFile, []byte("test executable"), 0755); err != nil { - t.Fatalf("Failed to create test file: %v", err) - } - - // Mark file for deletion - err = manager.markFileForDeletion(testFile) - if err != nil { - t.Fatalf("markFileForDeletion() failed: %v", err) - } - - // Verify marker file was created - markerFile := testFile + ".delete_on_restart" - if _, err := os.Stat(markerFile); os.IsNotExist(err) { - t.Fatalf("Marker file was not created: %s", markerFile) - } - - // Verify marker file contains correct path - content, err := os.ReadFile(markerFile) - if err != nil { - t.Fatalf("Failed to read marker file: %v", err) - } - - if string(content) != testFile { - t.Fatalf("Marker file content incorrect. Expected: %s, Got: %s", testFile, string(content)) - } -} - -func TestIsFileInUse(t *testing.T) { - // Test with nil error - if isFileInUse(nil) { - t.Error("isFileInUse(nil) should return false") - } - - // Test with regular error - regularErr := fmt.Errorf("regular error") - if isFileInUse(regularErr) { - t.Error("isFileInUse with regular error should return false") - } - - // Test with file in use error message - fileInUseErr := fmt.Errorf("file is being used by another process") - if !isFileInUse(fileInUseErr) { - t.Error("isFileInUse with 'being used by another process' should return true") - } - - // Test with access denied error message - accessDeniedErr := fmt.Errorf("access is denied") - if !isFileInUse(accessDeniedErr) { - t.Error("isFileInUse with 'access is denied' should return true") - } -} - -func TestExtractFileEdgeCases(t *testing.T) { - manager := NewManager() - - tempDir, err := manager.CreateTempDir() - if err != nil { - t.Fatalf("CreateTempDir() failed: %v", err) - } - defer manager.CleanupTempDir(tempDir) - - zipPath := filepath.Join(tempDir, "edge_cases.zip") - extractDir := filepath.Join(tempDir, "extract") - - // Create ZIP with edge cases - if err := createEdgeCaseZip(zipPath); err != nil { - t.Fatalf("Failed to create edge case ZIP: %v", err) - } - - // Extract ZIP - err = manager.ExtractZip(zipPath, extractDir) - if err != nil { - t.Fatalf("ExtractZip() failed: %v", err) - } - - // Verify files with special names were extracted - specialFiles := []string{ - "file with spaces.txt", - "file-with-dashes.txt", - "file_with_underscores.txt", - "UPPERCASE.TXT", - } - - for _, fileName := range specialFiles { - filePath := filepath.Join(extractDir, fileName) - if _, err := os.Stat(filePath); os.IsNotExist(err) { - t.Fatalf("Special file not extracted: %s", fileName) - } - } -} - -// Helper function to create a ZIP with nested directories -func createNestedZip(zipPath string) error { - file, err := os.Create(zipPath) - if err != nil { - return err - } - defer file.Close() - - writer := zip.NewWriter(file) - defer writer.Close() - - // Create nested structure - files := map[string]string{ - "level1/file1.txt": "Content of file1.txt", - "level1/level2/file2.txt": "Content of file2.txt", - "level1/level2/level3/file3.txt": "Content of file3.txt", - } - - for filePath, content := range files { - f, err := writer.Create(filePath) - if err != nil { - return err - } - _, err = f.Write([]byte(content)) - if err != nil { - return err - } - } - - return nil -} - -// Helper function to create a ZIP with edge case file names -func createEdgeCaseZip(zipPath string) error { - file, err := os.Create(zipPath) - if err != nil { - return err - } - defer file.Close() - - writer := zip.NewWriter(file) - defer writer.Close() - - // Create files with special names - files := []string{ - "file with spaces.txt", - "file-with-dashes.txt", - "file_with_underscores.txt", - "UPPERCASE.TXT", - } - - for _, fileName := range files { - f, err := writer.Create(fileName) - if err != nil { - return err - } - _, err = f.Write([]byte(fmt.Sprintf("Content of %s", fileName))) - if err != nil { - return err - } - } - - return nil -} \ No newline at end of file diff --git a/Go_Updater/integration_test.go b/Go_Updater/integration_test.go deleted file mode 100644 index 508cdc8..0000000 --- a/Go_Updater/integration_test.go +++ /dev/null @@ -1,12 +0,0 @@ -package main - -import ( - "testing" -) - -// 集成测试将在此处实现 -// 此文件目前是占位符 - -func TestIntegrationPlaceholder(t *testing.T) { - t.Skip("集成测试尚未实现") -} diff --git a/Go_Updater/logger/logger.go b/Go_Updater/logger/logger.go deleted file mode 100644 index 68b2f50..0000000 --- a/Go_Updater/logger/logger.go +++ /dev/null @@ -1,438 +0,0 @@ -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() -} diff --git a/Go_Updater/logger/logger_test.go b/Go_Updater/logger/logger_test.go deleted file mode 100644 index be99ae9..0000000 --- a/Go_Updater/logger/logger_test.go +++ /dev/null @@ -1,300 +0,0 @@ -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") - } -} \ No newline at end of file diff --git a/Go_Updater/main.go b/Go_Updater/main.go deleted file mode 100644 index 45459d6..0000000 --- a/Go_Updater/main.go +++ /dev/null @@ -1,1035 +0,0 @@ -package main - -import ( - "context" - "encoding/json" - "flag" - "fmt" - "os" - "os/exec" - "os/signal" - "path/filepath" - "strings" - "sync" - "syscall" - "time" - - "AUTO_MAA_Go_Updater/api" - "AUTO_MAA_Go_Updater/config" - "AUTO_MAA_Go_Updater/download" - "AUTO_MAA_Go_Updater/errors" - "AUTO_MAA_Go_Updater/install" - "AUTO_MAA_Go_Updater/logger" - appversion "AUTO_MAA_Go_Updater/version" -) - -// UpdateState 表示更新过程的当前状态 -type UpdateState int - -const ( - StateIdle UpdateState = iota - StateChecking - StateUpdateAvailable - StateDownloading - StateInstalling - StateCompleted - StateError -) - -// String 返回更新状态的字符串表示 -func (s UpdateState) String() string { - switch s { - case StateIdle: - return "Idle" - case StateChecking: - return "Checking" - case StateUpdateAvailable: - return "UpdateAvailable" - case StateDownloading: - return "Downloading" - case StateInstalling: - return "Installing" - case StateCompleted: - return "Completed" - case StateError: - return "Error" - default: - return "Unknown" - } -} - -// GUIManager 可选 GUI 功能的接口 -type GUIManager interface { - ShowMainWindow() - UpdateStatus(status int, message string) - ShowProgress(percentage float64) - ShowError(errorMsg string) - Close() -} - -// UpdateInfo 包含可用更新的信息 -type UpdateInfo struct { - CurrentVersion string - NewVersion string - DownloadURL string - ReleaseNotes string - IsAvailable bool -} - -// Application 表示主应用程序实例 -type Application struct { - config *config.Config - configManager config.ConfigManager - apiClient api.MirrorClient - downloadManager download.DownloadManager - installManager install.InstallManager - guiManager GUIManager - logger logger.Logger - errorHandler errors.ErrorHandler - ctx context.Context - cancel context.CancelFunc - wg sync.WaitGroup - - // 更新流程状态 - currentState UpdateState - stateMutex sync.RWMutex - updateInfo *UpdateInfo - userConfirmed chan bool -} - -// 命令行标志 -var ( - configPath = flag.String("config", "", "Path to configuration file") - logLevel = flag.String("log-level", "info", "Log level (debug, info, warn, error)") - noGUI = flag.Bool("no-gui", false, "Run without GUI (command line mode)") - version = flag.Bool("version", false, "Show version information") - help = flag.Bool("help", false, "Show help information") - channel = flag.String("channel", "", "Update channel (stable or beta)") - currentVersion = flag.String("current-version", "", "Current version to check against") -) - -// 版本信息现在由 version 包处理 - -func main() { - // 解析命令行参数 - flag.Parse() - - // 显示版本信息 - if *version { - showVersion() - return - } - - // 显示帮助信息 - if *help { - showHelp() - return - } - - // 检查单实例运行 - if err := ensureSingleInstance(); err != nil { - fmt.Fprintf(os.Stderr, "另一个实例已在运行: %v\n", err) - os.Exit(1) - } - - // 初始化应用程序 - app, err := initializeApplication() - if err != nil { - fmt.Fprintf(os.Stderr, "初始化应用程序失败: %v\n", err) - os.Exit(1) - } - defer app.cleanup() - - // 处理启动时标记删除的文件清理 - if err := app.handleStartupCleanup(); err != nil { - app.logger.Warn("清理标记文件失败: %v", err) - } - - // 设置信号处理 - app.setupSignalHandling() - - // 启动应用程序 - if err := app.run(); err != nil { - app.logger.Error("应用程序错误: %v", err) - os.Exit(1) - } -} - -// initializeApplication 初始化所有应用程序组件 -func initializeApplication() (*Application, error) { - // 创建优雅关闭的上下文 - ctx, cancel := context.WithCancel(context.Background()) - - // 首先初始化日志记录器 - loggerConfig := logger.DefaultLoggerConfig() - - // 从命令行设置日志级别 - switch *logLevel { - case "debug": - loggerConfig.Level = logger.DEBUG - case "info": - loggerConfig.Level = logger.INFO - case "warn": - loggerConfig.Level = logger.WARN - case "error": - loggerConfig.Level = logger.ERROR - } - - var appLogger logger.Logger - fileLogger, err := logger.NewFileLogger(loggerConfig) - if err != nil { - // 回退到控制台日志记录器 - appLogger = logger.NewConsoleLogger(os.Stdout) - } else { - appLogger = fileLogger - } - - appLogger.Info("正在初始化 AUTO_MAA_Go_Updater v%s", appversion.Version) - - // 初始化配置管理器 - var configManager config.ConfigManager - if *configPath != "" { - // 自定义配置路径尚未在配置包中实现 - // 目前使用默认管理器 - configManager = config.NewConfigManager() - appLogger.Warn("自定义配置路径尚未完全支持,使用默认配置") - } else { - configManager = config.NewConfigManager() - } - - // 加载配置 - cfg, err := configManager.Load() - if err != nil { - appLogger.Error("加载配置失败: %v", err) - return nil, fmt.Errorf("加载配置失败: %w", err) - } - - appLogger.Info("配置加载成功") - - // 初始化 API 客户端 - apiClient := api.NewClient() - - // 初始化下载管理器 - downloadManager := download.NewManager() - - // 初始化安装管理器 - installManager := install.NewManager() - - // 初始化错误处理器 - errorHandler := errors.NewDefaultErrorHandler() - - // 初始化 GUI 管理器(如果不是无 GUI 模式) - var guiManager GUIManager - if !*noGUI { - // GUI 将在 GUI 依赖项可用时实现 - appLogger.Info("请求 GUI 模式但此构建中不可用") - guiManager = nil - } else { - appLogger.Info("运行在无 GUI 模式") - } - - app := &Application{ - config: cfg, - configManager: configManager, - apiClient: apiClient, - downloadManager: downloadManager, - installManager: installManager, - guiManager: guiManager, - logger: appLogger, - errorHandler: errorHandler, - ctx: ctx, - cancel: cancel, - currentState: StateIdle, - userConfirmed: make(chan bool, 1), - } - - appLogger.Info("应用程序初始化成功") - return app, nil -} - -// run 启动主应用程序逻辑 -func (app *Application) run() error { - app.logger.Info("启动应用程序") - - if app.guiManager != nil { - // 使用 GUI 运行 - return app.runWithGUI() - } else { - // 在命令行模式下运行 - return app.runCommandLine() - } -} - -// runWithGUI 使用 GUI 运行应用程序 -func (app *Application) runWithGUI() error { - app.logger.Info("启动 GUI 模式") - - // 设置 GUI 回调 - app.setupGUICallbacks() - - // 显示主窗口(这将阻塞直到窗口关闭) - app.guiManager.ShowMainWindow() - - return nil -} - -// runCommandLine 在命令行模式下运行应用程序 -func (app *Application) runCommandLine() error { - app.logger.Info("启动命令行模式") - - // 开始完整的更新流程 - return app.executeUpdateFlow() -} - -// setupGUICallbacks 为 GUI 交互设置回调 -func (app *Application) setupGUICallbacks() { - if app.guiManager == nil { - return - } - - // GUI 回调将在 GUI 可用时实现 - app.logger.Info("请求 GUI 回调设置但 GUI 不可用") - - // 目前,我们将设置基本的交互处理 - // 实际的 GUI 集成将在 GUI 依赖项解决后完成 -} - -// handleStartupCleanup 处理启动时标记删除的文件清理 -func (app *Application) handleStartupCleanup() error { - app.logger.Info("执行启动清理") - - // 获取当前可执行文件目录 - exePath, err := os.Executable() - if err != nil { - return fmt.Errorf("获取可执行文件路径失败: %w", err) - } - - exeDir := filepath.Dir(exePath) - - // 删除标记删除的文件 - if installMgr, ok := app.installManager.(*install.Manager); ok { - if err := installMgr.DeleteMarkedFiles(exeDir); err != nil { - return fmt.Errorf("删除标记文件失败: %w", err) - } - } - - app.logger.Info("启动清理完成") - return nil -} - -// setupSignalHandling 设置系统信号的优雅关闭 -func (app *Application) setupSignalHandling() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - - go func() { - sig := <-sigChan - app.logger.Info("接收到信号: %v", sig) - app.logger.Info("启动优雅关闭...") - app.cancel() - }() -} - -// cleanup 执行应用程序清理 -func (app *Application) cleanup() { - app.logger.Info("清理应用程序资源") - - // 取消上下文以停止所有操作 - app.cancel() - - // 等待所有 goroutine 完成 - app.wg.Wait() - - // 清理安装管理器临时目录 - if installMgr, ok := app.installManager.(*install.Manager); ok { - if err := installMgr.CleanupAllTempDirs(); err != nil { - app.logger.Error("清理临时目录失败: %v", err) - } - } - - app.logger.Info("应用程序清理完成") - - // 最后关闭日志记录器 - if err := app.logger.Close(); err != nil { - fmt.Fprintf(os.Stderr, "关闭日志记录器失败: %v\n", err) - } -} - -// ensureSingleInstance 确保应用程序只有一个实例在运行 -func ensureSingleInstance() error { - // 在临时目录中创建锁文件 - tempDir := os.TempDir() - lockFile := filepath.Join(tempDir, "AUTO_MAA_Go_Updater.lock") - - // 尝试独占创建锁文件 - file, err := os.OpenFile(lockFile, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644) - if err != nil { - if os.IsExist(err) { - // 检查进程是否仍在运行 - if isProcessRunning(lockFile) { - return fmt.Errorf("另一个实例已在运行") - } - // 删除过期的锁文件并重试 - os.Remove(lockFile) - return ensureSingleInstance() - } - return fmt.Errorf("创建锁文件失败: %w", err) - } - - // 将当前进程 ID 写入锁文件 - fmt.Fprintf(file, "%d", os.Getpid()) - file.Close() - - // 退出时删除锁文件 - go func() { - sigChan := make(chan os.Signal, 1) - signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) - <-sigChan - os.Remove(lockFile) - }() - - return nil -} - -// isProcessRunning 检查锁文件中的进程是否仍在运行 -func isProcessRunning(lockFile string) bool { - data, err := os.ReadFile(lockFile) - if err != nil { - return false - } - - var pid int - if _, err := fmt.Sscanf(string(data), "%d", &pid); err != nil { - return false - } - - // 检查进程是否存在(Windows 特定) - process, err := os.FindProcess(pid) - if err != nil { - return false - } - - // 在 Windows 上,FindProcess 总是成功,所以我们需要不同的检查方式 - // 尝试发送信号 0 来检查进程是否存在 - err = process.Signal(syscall.Signal(0)) - return err == nil -} - -// showVersion 显示版本信息 -func showVersion() { - fmt.Printf("AUTO_MAA_Go_Updater\n") - fmt.Printf("Version: %s\n", appversion.Version) - fmt.Printf("Build Time: %s\n", appversion.BuildTime) - fmt.Printf("Git Commit: %s\n", appversion.GitCommit) -} - -// showHelp 显示帮助信息 -func showHelp() { - fmt.Printf("AUTO_MAA_Go_Updater\n\n") - fmt.Printf("Usage: %s [options]\n\n", os.Args[0]) - fmt.Printf("Options:\n") - flag.PrintDefaults() - fmt.Printf("\nExamples:\n") - fmt.Printf(" %s # 使用 GUI 运行\n", os.Args[0]) - fmt.Printf(" %s -no-gui # 在命令行模式下运行\n", os.Args[0]) - fmt.Printf(" %s -log-level debug # 使用调试日志运行\n", os.Args[0]) - fmt.Printf(" %s -version # 显示版本信息\n", os.Args[0]) -} - -// executeUpdateFlow 执行完整的更新流程和状态机管理 -func (app *Application) executeUpdateFlow() error { - app.logger.Info("开始执行更新流程") - - // 执行状态机 - for { - select { - case <-app.ctx.Done(): - app.logger.Info("更新流程已取消") - return app.ctx.Err() - default: - } - - // 获取当前状态 - state := app.getCurrentState() - app.logger.Debug("当前状态: %s", state.String()) - - // 执行状态逻辑 - nextState, err := app.executeState(state) - if err != nil { - app.logger.Error("状态执行失败: %v", err) - app.setState(StateError) - return err - } - - // 检查是否完成 - if nextState == StateCompleted || nextState == StateError { - app.setState(nextState) - break - } - - // 转换到下一个状态 - app.setState(nextState) - } - - finalState := app.getCurrentState() - app.logger.Info("更新流程完成,状态: %s", finalState.String()) - - if finalState == StateError { - return fmt.Errorf("更新流程失败") - } - - return nil -} - -// executeState 执行当前状态的逻辑并返回下一个状态 -func (app *Application) executeState(state UpdateState) (UpdateState, error) { - switch state { - case StateIdle: - return app.executeIdleState() - case StateChecking: - return app.executeCheckingState() - case StateUpdateAvailable: - return app.executeUpdateAvailableState() - case StateDownloading: - return app.executeDownloadingState() - case StateInstalling: - return app.executeInstallingState() - case StateCompleted: - return StateCompleted, nil - case StateError: - return StateError, nil - default: - return StateError, fmt.Errorf("未知状态: %s", state.String()) - } -} - -// executeIdleState 处理空闲状态 -func (app *Application) executeIdleState() (UpdateState, error) { - app.logger.Info("开始更新检查...") - fmt.Println("正在检查更新...") - return StateChecking, nil -} - -// executeCheckingState 处理检查状态 -func (app *Application) executeCheckingState() (UpdateState, error) { - app.logger.Info("检查更新中") - - // 确定要使用的版本和渠道 - var currentVer, updateChannel string - var err error - - // 优先级: 命令行参数 > 版本文件 > 配置 - if *currentVersion != "" { - currentVer = *currentVersion - app.logger.Info("使用命令行当前版本: %s", currentVer) - } else { - // 尝试从 resources/version.json 加载版本 - versionManager := appversion.NewVersionManager() - versionInfo, err := versionManager.LoadVersionFromFile() - if err != nil { - app.logger.Warn("从文件加载版本失败: %v,使用配置版本", err) - currentVer = app.config.CurrentVersion - } else { - currentVer = versionInfo.MainVersion - app.logger.Info("使用版本文件中的当前版本: %s", currentVer) - } - } - - // 确定渠道 - if *channel != "" { - updateChannel = *channel - app.logger.Info("使用命令行渠道: %s", updateChannel) - } else { - // 尝试从 config.json 加载渠道 - updateChannel = app.loadChannelFromConfig() - app.logger.Info("使用配置中的渠道: %s", updateChannel) - } - - // 准备 API 参数 - params := api.UpdateCheckParams{ - ResourceID: "AUTO_MAA", // AUTO_MAA 的固定资源 ID - CurrentVersion: currentVer, - Channel: updateChannel, - UserAgent: app.config.UserAgent, - } - - // 调用 MirrorChyan API 检查更新 - response, err := app.apiClient.CheckUpdate(params) - switch updateChannel { - case "beta": - fmt.Println("检查更新类别:公测版") - case "stable": - fmt.Println("检查更新类别:稳定版") - default: - fmt.Printf("检查更新类别:%v\n", updateChannel) - } - fmt.Printf("当前版本:%v\n", currentVer) - app.logger.Info("当前更新类别:" + updateChannel + ";当前版本:" + currentVer) - if err != nil { - app.logger.Error("检查更新失败: %v", err) - fmt.Printf("检查更新失败: %v\n", err) - return StateError, fmt.Errorf("检查更新失败: %w", err) - } - - // 检查是否有可用更新 - isUpdateAvailable := app.apiClient.IsUpdateAvailable(response, currentVer) - - if !isUpdateAvailable { - app.logger.Info("无可用更新") - fmt.Println("当前已是最新版本") - - // 延迟 5 秒再退出 - fmt.Println("5 秒后自动退出...") - time.Sleep(5 * time.Second) - - return StateCompleted, nil - } - - // 使用下载站获取下载链接 - downloadURL := app.apiClient.GetDownloadURL(response.Data.VersionName) - app.logger.Info("使用下载站 URL: %s", downloadURL) - - // 存储更新信息 - app.updateInfo = &UpdateInfo{ - CurrentVersion: currentVer, - NewVersion: response.Data.VersionName, - DownloadURL: downloadURL, - ReleaseNotes: response.Data.ReleaseNote, - IsAvailable: true, - } - - app.logger.Info("有可用更新: %s -> %s", currentVer, response.Data.VersionName) - fmt.Printf("发现新版本: %s -> %s\n", currentVer, response.Data.VersionName) - - return StateUpdateAvailable, nil -} - -// executeUpdateAvailableState 处理更新可用状态 -func (app *Application) executeUpdateAvailableState() (UpdateState, error) { - app.logger.Info("有可用更新,自动开始下载") - - // 自动开始下载,无需用户确认 - fmt.Println("开始下载更新...") - return StateDownloading, nil -} - -// executeDownloadingState 处理下载状态 -func (app *Application) executeDownloadingState() (UpdateState, error) { - app.logger.Info("开始下载") - - if app.updateInfo == nil || app.updateInfo.DownloadURL == "" { - return StateError, fmt.Errorf("无可用下载 URL") - } - - // 获取当前可执行文件目录 - exePath, err := os.Executable() - if err != nil { - return StateError, fmt.Errorf("获取可执行文件路径失败: %w", err) - } - exeDir := filepath.Dir(exePath) - - // 为下载创建 AUTOMAA_UPDATE_TEMP 目录 - tempDir := filepath.Join(exeDir, "AUTOMAA_UPDATE_TEMP") - if err := os.MkdirAll(tempDir, 0755); err != nil { - return StateError, fmt.Errorf("创建临时目录失败: %w", err) - } - - // 下载文件 - downloadPath := filepath.Join(tempDir, "update.zip") - - fmt.Println("正在下载更新包...") - - // 创建进度回调 - progressCallback := func(progress download.DownloadProgress) { - if progress.TotalBytes > 0 { - fmt.Printf("\r下载进度: %.1f%% (%s/s)", - progress.Percentage, - app.formatBytes(progress.Speed)) - } - } - - // 下载更新文件 - downloadErr := app.downloadManager.Download(app.updateInfo.DownloadURL, downloadPath, progressCallback) - - fmt.Println() // 进度后换行 - - if downloadErr != nil { - app.logger.Error("下载失败: %v", downloadErr) - fmt.Printf("下载失败: %v\n", downloadErr) - return StateError, fmt.Errorf("下载失败: %w", downloadErr) - } - - app.logger.Info("下载成功完成") - fmt.Println("下载完成") - - // 存储下载路径用于安装 - app.updateInfo.DownloadURL = downloadPath - - return StateInstalling, nil -} - -// executeInstallingState 处理安装状态 -func (app *Application) executeInstallingState() (UpdateState, error) { - app.logger.Info("开始安装") - fmt.Println("正在安装更新...") - - if app.updateInfo == nil || app.updateInfo.DownloadURL == "" { - return StateError, fmt.Errorf("无可用下载文件") - } - - downloadPath := app.updateInfo.DownloadURL - - // 为解压创建临时目录 - tempDir, err := app.installManager.CreateTempDir() - if err != nil { - return StateError, fmt.Errorf("创建临时目录失败: %w", err) - } - - // 解压下载的 zip 文件 - app.logger.Info("解压更新包") - if err := app.installManager.ExtractZip(downloadPath, tempDir); err != nil { - app.logger.Error("解压 zip 失败: %v", err) - return StateError, fmt.Errorf("解压更新包失败: %w", err) - } - - // 如果存在 changes.json 则处理(供将来使用) - changesPath := filepath.Join(tempDir, "changes.json") - _, err = app.installManager.ProcessChanges(changesPath) - if err != nil { - app.logger.Warn("处理变更失败(非关键): %v", err) - // 这对于 AUTO_MAA-Setup.exe 安装不是关键的 - } - - // 获取当前可执行文件目录 - exePath, err := os.Executable() - if err != nil { - return StateError, fmt.Errorf("获取可执行文件路径失败: %w", err) - } - targetDir := filepath.Dir(exePath) - - // 处理正在运行的进程(但跳过更新器本身) - updaterName := filepath.Base(exePath) - if err := app.handleRunningProcesses(targetDir, updaterName); err != nil { - app.logger.Warn("处理正在运行的进程失败: %v", err) - // 继续安装,这不是关键的 - } - - // 在解压的文件中查找 AUTO_MAA-Setup.exe - setupExePath := filepath.Join(tempDir, "AUTO_MAA-Setup.exe") - if _, err := os.Stat(setupExePath); err != nil { - app.logger.Error("在更新包中未找到 AUTO_MAA-Setup.exe: %v", err) - return StateError, fmt.Errorf("在更新包中未找到 AUTO_MAA-Setup.exe: %w", err) - } - - // 运行安装可执行文件 - app.logger.Info("运行 AUTO_MAA-Setup.exe") - fmt.Println("正在运行安装程序...") - - if err := app.runSetupExecutable(setupExePath); err != nil { - app.logger.Error("运行安装可执行文件失败: %v", err) - return StateError, fmt.Errorf("运行安装可执行文件失败: %w", err) - } - - // 使用新版本更新 version.json 文件 - if err := app.updateVersionFile(app.updateInfo.NewVersion); err != nil { - app.logger.Warn("更新版本文件失败: %v", err) - // 这不是关键的,继续 - } - - // 安装后清理 AUTOMAA_UPDATE_TEMP 目录 - if err := os.RemoveAll(tempDir); err != nil { - app.logger.Warn("清理临时目录失败: %v", err) - // 这不是关键的,继续 - } else { - app.logger.Info("清理临时目录: %s", tempDir) - } - - app.logger.Info("安装成功完成") - fmt.Println("安装完成") - fmt.Printf("已更新到版本: %s\n", app.updateInfo.NewVersion) - - return StateCompleted, nil -} - -// getCurrentState 线程安全地返回当前状态 -func (app *Application) getCurrentState() UpdateState { - app.stateMutex.RLock() - defer app.stateMutex.RUnlock() - return app.currentState -} - -// setState 线程安全地设置当前状态 -func (app *Application) setState(state UpdateState) { - app.stateMutex.Lock() - defer app.stateMutex.Unlock() - - app.logger.Debug("状态转换: %s -> %s", app.currentState.String(), state.String()) - app.currentState = state - - // 如果可用则更新 GUI - if app.guiManager != nil { - app.updateGUIStatus(state) - } -} - -// updateGUIStatus 根据当前状态更新 GUI -func (app *Application) updateGUIStatus(state UpdateState) { - if app.guiManager == nil { - return - } - - switch state { - case StateIdle: - app.guiManager.UpdateStatus(0, "准备检查更新...") - case StateChecking: - app.guiManager.UpdateStatus(1, "正在检查更新...") - case StateUpdateAvailable: - if app.updateInfo != nil { - message := fmt.Sprintf("发现新版本: %s", app.updateInfo.NewVersion) - app.guiManager.UpdateStatus(2, message) - } - case StateDownloading: - app.guiManager.UpdateStatus(3, "正在下载更新...") - case StateInstalling: - app.guiManager.UpdateStatus(4, "正在安装更新...") - case StateCompleted: - app.guiManager.UpdateStatus(5, "更新完成") - case StateError: - app.guiManager.UpdateStatus(6, "更新失败") - } -} - -// formatBytes 将字节格式化为人类可读格式 -func (app *Application) formatBytes(bytes int64) string { - const unit = 1024 - if bytes < unit { - return fmt.Sprintf("%d B", bytes) - } - div, exp := int64(unit), 0 - for n := bytes / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %cB", float64(bytes)/float64(div), "KMGTPE"[exp]) -} - -// handleUserInteraction 处理 GUI 模式的用户交互 -func (app *Application) handleUserInteraction(action string) { - switch action { - case "confirm_update": - select { - case app.userConfirmed <- true: - default: - } - case "cancel_update": - select { - case app.userConfirmed <- false: - default: - } - case "check_update": - // 在 goroutine 中启动更新流程 - app.wg.Add(1) - go func() { - defer app.wg.Done() - if err := app.executeUpdateFlow(); err != nil { - app.logger.Error("更新流程失败: %v", err) - } - }() - } -} - -// updateVersionFile 使用新版本更新目标软件的 version.json 文件 -func (app *Application) updateVersionFile(newVersion string) error { - // 获取当前可执行文件目录(目标软件所在位置) - exePath, err := os.Executable() - if err != nil { - return fmt.Errorf("获取可执行文件路径失败: %w", err) - } - targetDir := filepath.Dir(exePath) - - // 目标软件版本文件的路径 - versionFilePath := filepath.Join(targetDir, "resources", "version.json") - - // 尝试加载现有版本文件 - versionManager := appversion.NewVersionManager() - versionInfo, err := versionManager.LoadVersionFromFile() - if err != nil { - app.logger.Warn("无法加载现有版本文件,创建新文件: %v", err) - // 创建基本版本信息结构 - versionInfo = &appversion.VersionInfo{ - MainVersion: newVersion, - VersionInfo: make(map[string]map[string][]string), - } - } - - // 解析新版本以获取正确格式 - parsedVersion, err := appversion.ParseVersion(newVersion) - if err != nil { - // 如果无法从 API 响应解析版本,尝试从显示格式提取 - if strings.HasPrefix(newVersion, "v") { - // 将 "v4.4.1-beta3" 转换为 "4.4.1.3" 格式 - versionStr := strings.TrimPrefix(newVersion, "v") - if strings.Contains(versionStr, "-beta") { - parts := strings.Split(versionStr, "-beta") - if len(parts) == 2 { - baseVersion := parts[0] - betaNum := parts[1] - versionInfo.MainVersion = fmt.Sprintf("%s.%s", baseVersion, betaNum) - } else { - versionInfo.MainVersion = versionStr + ".0" - } - } else { - versionInfo.MainVersion = versionStr + ".0" - } - } else { - versionInfo.MainVersion = newVersion - } - } else { - // 使用解析的版本创建正确格式 - versionInfo.MainVersion = parsedVersion.ToVersionString() - } - - // 如果 resources 目录不存在则创建 - resourcesDir := filepath.Join(targetDir, "resources") - if err := os.MkdirAll(resourcesDir, 0755); err != nil { - return fmt.Errorf("创建 resources 目录失败: %w", err) - } - - // 写入更新的版本文件 - data, err := json.MarshalIndent(versionInfo, "", " ") - if err != nil { - return fmt.Errorf("序列化版本信息失败: %w", err) - } - - if err := os.WriteFile(versionFilePath, data, 0644); err != nil { - return fmt.Errorf("写入版本文件失败: %w", err) - } - - app.logger.Info("更新版本文件: %s -> %s", versionFilePath, versionInfo.MainVersion) - return nil -} - -// handleRunningProcesses 处理正在运行的进程但排除更新器本身 -func (app *Application) handleRunningProcesses(targetDir, updaterName string) error { - app.logger.Info("处理正在运行的进程,排除更新器: %s", updaterName) - - // 获取目标目录中的可执行文件列表 - files, err := os.ReadDir(targetDir) - if err != nil { - return fmt.Errorf("读取目标目录失败: %w", err) - } - - for _, file := range files { - if file.IsDir() { - continue - } - - fileName := file.Name() - - // 跳过更新器本身 - if fileName == updaterName { - app.logger.Info("跳过更新器文件: %s", fileName) - continue - } - - // 只处理 .exe 文件 - if !strings.HasSuffix(strings.ToLower(fileName), ".exe") { - continue - } - - // 处理此可执行文件 - if err := app.installManager.HandleRunningProcess(fileName); err != nil { - app.logger.Warn("处理正在运行的进程 %s 失败: %v", fileName, err) - // 继续处理其他文件,不要让整个过程失败 - } - } - - return nil -} - -// runSetupExecutable 使用适当参数运行安装可执行文件 -func (app *Application) runSetupExecutable(setupExePath string) error { - app.logger.Info("执行安装文件: %s", setupExePath) - - // 获取当前可执行文件目录作为安装目录 - exePath, err := os.Executable() - if err != nil { - return fmt.Errorf("获取可执行文件路径失败: %w", err) - } - installDir := filepath.Dir(exePath) - - // 设置与 Python 实现匹配的命令参数 - args := []string{ - "/SP-", // 跳过欢迎页面 - "/SILENT", // 静默安装 - "/NOCANCEL", // 无取消按钮 - "/FORCECLOSEAPPLICATIONS", // 强制关闭应用程序 - "/LANG=Chinese", // 中文语言 - fmt.Sprintf("/DIR=%s", installDir), // 安装目录 - } - - app.logger.Info("使用参数运行安装程序: %v", args) - - // 使用参数创建命令 - cmd := exec.Command(setupExePath, args...) - - // 设置工作目录为安装文件的目录 - cmd.Dir = filepath.Dir(setupExePath) - - // 运行命令并等待完成 - if err := cmd.Run(); err != nil { - return fmt.Errorf("执行安装程序失败: %w", err) - } - - app.logger.Info("安装可执行文件成功完成") - return nil -} - -// AutoMAAConfig 表示 config/config.json 的结构 -type AutoMAAConfig struct { - Update struct { - UpdateType string `json:"UpdateType"` - } `json:"Update"` -} - -// loadChannelFromConfig 从 config/config.json 加载更新渠道 -func (app *Application) loadChannelFromConfig() string { - // 获取当前可执行文件目录 - exePath, err := os.Executable() - if err != nil { - app.logger.Warn("获取可执行文件路径失败: %v", err) - return "stable" - } - - configPath := filepath.Join(filepath.Dir(exePath), "config", "config.json") - - // 检查配置文件是否存在 - if _, err := os.Stat(configPath); os.IsNotExist(err) { - app.logger.Info("配置文件未找到: %s,使用默认渠道", configPath) - return "stable" - } - - // 读取配置文件 - data, err := os.ReadFile(configPath) - if err != nil { - app.logger.Warn("读取配置文件失败: %v,使用默认渠道", err) - return "stable" - } - - // 解析 JSON - var config AutoMAAConfig - if err := json.Unmarshal(data, &config); err != nil { - app.logger.Warn("解析配置文件失败: %v,使用默认渠道", err) - return "stable" - } - - // 获取更新渠道 - updateType := config.Update.UpdateType - if updateType == "" { - app.logger.Info("配置中未找到 UpdateType,使用默认渠道") - return "stable" - } - - app.logger.Info("从配置加载更新渠道: %s", updateType) - return updateType -} diff --git a/Go_Updater/version/manager.go b/Go_Updater/version/manager.go deleted file mode 100644 index 9f708d9..0000000 --- a/Go_Updater/version/manager.go +++ /dev/null @@ -1,178 +0,0 @@ -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 将版本转换为显示格式 (v4.4.0 或 v4.4.1-beta3) -func (pv *ParsedVersion) ToDisplayVersion() string { - if pv.Beta == 0 { - return fmt.Sprintf("v%d.%d.%d", pv.Major, pv.Minor, pv.Patch) - } - return fmt.Sprintf("v%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 - } - return pv.Beta > other.Beta -} diff --git a/Go_Updater/version/manager_test.go b/Go_Updater/version/manager_test.go deleted file mode 100644 index 41a3f7c..0000000 --- a/Go_Updater/version/manager_test.go +++ /dev/null @@ -1,366 +0,0 @@ -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)) - } -} \ No newline at end of file diff --git a/Go_Updater/version/version.go b/Go_Updater/version/version.go deleted file mode 100644 index 7eda7f7..0000000 --- a/Go_Updater/version/version.go +++ /dev/null @@ -1,19 +0,0 @@ -package version - -import ( - "runtime" -) - -var ( - // Version 应用程序的当前版本 - Version = "1.0.0" - - // BuildTime 在构建时设置 - BuildTime = "unknown" - - // GitCommit 在构建时设置 - GitCommit = "unknown" - - // GoVersion 用于构建的 Go 版本 - GoVersion = runtime.Version() -) diff --git a/LICENSE b/LICENSE deleted file mode 100644 index f288702..0000000 --- a/LICENSE +++ /dev/null @@ -1,674 +0,0 @@ - GNU GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU General Public License is a free, copyleft license for -software and other kinds of works. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -the GNU General Public License is intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. We, the Free Software Foundation, use the -GNU General Public License for most of our software; it applies also to -any other work released this way by its authors. You can apply it to -your programs, too. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - To protect your rights, we need to prevent others from denying you -these rights or asking you to surrender the rights. Therefore, you have -certain responsibilities if you distribute copies of the software, or if -you modify it: responsibilities to respect the freedom of others. - - For example, if you distribute copies of such a program, whether -gratis or for a fee, you must pass on to the recipients the same -freedoms that you received. You must make sure that they, too, receive -or can get the source code. And you must show them these terms so they -know their rights. - - Developers that use the GNU GPL protect your rights with two steps: -(1) assert copyright on the software, and (2) offer you this License -giving you legal permission to copy, distribute and/or modify it. - - For the developers' and authors' protection, the GPL clearly explains -that there is no warranty for this free software. For both users' and -authors' sake, the GPL requires that modified versions be marked as -changed, so that their problems will not be attributed erroneously to -authors of previous versions. - - Some devices are designed to deny users access to install or run -modified versions of the software inside them, although the manufacturer -can do so. This is fundamentally incompatible with the aim of -protecting users' freedom to change the software. The systematic -pattern of such abuse occurs in the area of products for individuals to -use, which is precisely where it is most unacceptable. Therefore, we -have designed this version of the GPL to prohibit the practice for those -products. If such problems arise substantially in other domains, we -stand ready to extend this provision to those domains in future versions -of the GPL, as needed to protect the freedom of users. - - Finally, every program is threatened constantly by software patents. -States should not allow patents to restrict development and use of -software on general-purpose computers, but in those that do, we wish to -avoid the special danger that patents applied to a free program could -make it effectively proprietary. To prevent this, the GPL assures that -patents cannot be used to render the program non-free. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Use with the GNU Affero General Public License. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU Affero General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the special requirements of the GNU Affero General Public License, -section 13, concerning interaction through a network will apply to the -combination as such. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU General Public License from time to time. Such new versions will -be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) - - This program 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. - - This program 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 this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If the program does terminal interaction, make it output a short -notice like this when it starts in an interactive mode: - - Copyright (C) - This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. - This is free software, and you are welcome to redistribute it - under certain conditions; type `show c' for details. - -The hypothetical commands `show w' and `show c' should show the appropriate -parts of the General Public License. Of course, your program's commands -might be different; for a GUI interface, you would use an "about box". - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU GPL, see -. - - The GNU General Public License does not permit incorporating your program -into proprietary programs. If your program is a subroutine library, you -may consider it more useful to permit linking proprietary applications with -the library. If this is what you want to do, use the GNU Lesser General -Public License instead of this License. But first, please read -. diff --git a/README.md b/README.md index ea0da17..e69de29 100644 --- a/README.md +++ b/README.md @@ -1,119 +0,0 @@ -

AUTO_MAA

-

- MAA多账号管理与自动化软件

- 软件图标 -

- ---- - -

- GitHub Stars - GitHub Forks - GitHub Downloads - GitHub Issues - GitHub Contributors - GitHub License - DeepWiki - mirrorc -

- -## 软件介绍 - -### 性质 - -本软件是明日方舟第三方软件`MAA`的第三方工具,即第33方软件。旨在优化MAA多账号功能体验,并通过一些方法解决MAA项目未能解决的部分问题,提高代理的稳定性。 - -- **集中管理**:一站式管理多个MAA脚本与多个用户配置,和凌乱的散装脚本窗口说再见! -- **无人值守**:自动处理MAA相关报错,再也不用为代理任务卡死时自己不在电脑旁烦恼啦! -- **配置灵活**:通过调度队列与脚本的组合,自由实现您能想到的所有调度需求! -- **信息统计**:自动统计用户的公招与关卡掉落物,看看这个月您的收益是多少! - -### 原理 - -本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能: - -1. **配置:** 根据对应用户的配置信息,生成配置文件并将其导入MAA。 -2. **监测:** 在MAA开始代理后,持续读取MAA的日志以判断其运行状态。当软件认定MAA出现异常时,通过重启MAA使之仍能继续完成任务。 -3. **循环:** 重复上述步骤,使MAA依次完成各个用户的自动代理任务。 - -### 优势 - -- **高效稳定**:通过日志监测、异常处理等机制,保障代理任务顺利完成。 -- **简洁易用**:无需手动修改配置文件,实现自动化调度与多开管理。 -- **兼容扩展**:支持 MAA 几乎所有的配置选项,满足不同用户需求。 - -## 重要声明 - -本开发团队承诺,不会修改明日方舟游戏本体与相关配置文件。本项目使用GPL开源,相关细则如下: - -- **作者:** AUTO_MAA软件作者为DLmaster、DLmaster361或DLmaster_361,以上均指代同一人。 -- **使用:** AUTO_MAA使用者可以按自己的意愿自由使用本软件。依据GPL,对于由此可能产生的损失,AUTO_MAA项目组不负任何责任。 -- **分发:** AUTO_MAA允许任何人自由分发本软件,包括进行商业活动牟利。若为直接分发本软件,必须遵循GPL向接收者提供本软件项目地址、完整的软件源码与GPL协议原文(件);若为修改软件后进行分发,必须遵循GPL向接收者提供本软件项目地址、修改前的完整软件源码副本与GPL协议原文(件),违反者可能会被追究法律责任。 -- **传播:** AUTO_MAA原则上允许传播者自由传播本软件,但无论在何种传播过程中,不得删除项目作者与开发者所留版权声明,不得隐瞒项目作者与相关开发者的存在。由于软件性质,项目组不希望发现任何人在明日方舟官方媒体(包括官方媒体账号与森空岛社区等)或明日方舟游戏相关内容(包括同好群、线下活动与游戏内容讨论等)下提及AUTO_MAA或MAA,希望各位理解。 -- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL,相关成果再次分发时也必须使用GPL或兼容的协议开源。 -- **贡献:** 不论是直接参与软件的维护编写,或是撰写文档、测试、反馈BUG、给出建议、参与讨论,都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例,发布Issues参与项目。避免私信或私发邮件(安全性漏洞或敏感问题除外),以帮助更多用户。 -- **图像:** `AUTO_MAA主页默认图像` 并不适用开源协议,著作权归 [NARINpopo](https://space.bilibili.com/1877154) 画师所有,商业使用权归 [DLmaster (@DLmaster361)](https://github.com/DLmaster361) 所有,软件用户仅拥有非商业使用权。不得以开源协议已授权为由在未经授权的情况下使用 `AUTO_MAA主页默认图像`,不得在未经授权的情况下将 `AUTO_MAA主页默认图像` 用于任何商业用途。 - -以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准,发生冲突的以本细则为准。如有不清楚的部分,请发Issues询问。若发生纠纷,相关内容也没有在Issues上提及的,项目组拥有最终解释权。 - ---- - -# 使用方法 - -访问AUTO_MAA官方文档站以获取使用指南和项目相关信息 - -- [AUTO_MAA官方文档站](https://clozya.github.io/AUTOMAA_docs) - ---- - -# 关于 - -## 项目开发情况 - -可在[《AUTO_MAA开发者协作文档》](https://docs.qq.com/aio/DQ3Z5eHNxdmxFQmZX)的`开发任务`页面中查看开发进度。 - -## 代码签名策略(Code signing policy) - -Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/) - -- 审批人(Approvers): [DLmaster (@DLmaster361)](https://github.com/DLmaster361) - -## 隐私政策(Privacy policy) - -除非用户、安装者或使用者特别要求,否则本程序不会将任何信息传输到其他网络系统。 - -This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it. - -## 特别鸣谢 - -- 下载服务器:由[AoXuan (@ClozyA)](https://github.com/ClozyA) 个人为项目赞助。 - -- EXE签名: 由 [SignPath.io](https://signpath.io/)提供免费代码签名,签名来自[SignPath Foundation](https://signpath.org/)。 - -## 贡献者 - -感谢以下贡献者对本项目做出的贡献 - - - - - - - -![Alt](https://repobeats.axiom.co/api/embed/6c2f834141eff1ac297db70d12bd11c6236a58a5.svg "Repobeats analytics image") - -## Star History - -[![Star History Chart](https://api.star-history.com/svg?repos=DLmaster361/AUTO_MAA&type=Date)](https://star-history.com/#DLmaster361/AUTO_MAA&Date) - -## 交流与赞助 - -欢迎加入AUTO_MAA项目组,欢迎反馈bug - -- QQ交流群:[957750551](https://qm.qq.com/q/bd9fISNoME) - ---- - -如果喜欢这个项目的话,给作者来杯咖啡吧! - -![payid](resources/images/README/payid.png "payid") diff --git a/app/__init__.py b/app/__init__.py deleted file mode 100644 index b2f64f6..0000000 --- a/app/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA主程序包 -v4.4 -作者:DLmaster_361 -""" - -__version__ = "4.2.0" -__author__ = "DLmaster361 " -__license__ = "GPL-3.0 license" - -from .core import QueueConfig, MaaConfig, MaaUserConfig, Task, TaskManager, MainTimer -from .models import MaaManager -from .services import Notify, Crypto, System -from .ui import AUTO_MAA - -__all__ = [ - "QueueConfig", - "MaaConfig", - "MaaUserConfig", - "Task", - "TaskManager", - "MainTimer", - "MaaManager", - "Notify", - "Crypto", - "System", - "AUTO_MAA", -] diff --git a/app/api/api.py b/app/api/api.py new file mode 100644 index 0000000..6b18bce --- /dev/null +++ b/app/api/api.py @@ -0,0 +1,408 @@ +from fastapi import FastAPI, HTTPException, Path, Body +from pydantic import BaseModel +from typing import Dict, Any, List, Optional +from datetime import datetime + +app = FastAPI( + title="auto_maa", + description="API for managing automation scripts, plans, and tasks", + version="1.0.0" +) + +#此文件由ai生成 返回值非最终版本 + + +# ====================== +# Data Models +# ====================== + +# Script Models +class ScriptCreate(BaseModel): + name: str + type: str # "MAA" or "通用" + content: str + description: Optional[str] = None + +class ScriptUpdate(BaseModel): + name: Optional[str] = None + content: Optional[str] = None + description: Optional[str] = None + +class ScriptUser(BaseModel): + userId: str + config: Dict[str, Any] = {} + +# Plan Models +class PlanDayConfig(BaseModel): + 吃理智药: int + 连战次数: str + 关卡选择: str + 备选_1: str + 备选_2: str + 备选_3: str + 剩余理智: str + +class PlanDetails(BaseModel): + 周一: PlanDayConfig + 周二: PlanDayConfig + 周三: PlanDayConfig + 周四: PlanDayConfig + 周五: PlanDayConfig + 周六: PlanDayConfig + 周日: PlanDayConfig + +class PlanCreate(BaseModel): + name: str + mode: str # "全局" or "周计划" + details: PlanDetails + +class PlanUpdate(BaseModel): + name: Optional[str] = None + mode: Optional[str] = None + details: Optional[PlanDetails] = None + +class PlanModeUpdate(BaseModel): + mode: str # "全局" or "周计划" + +# Queue Models +class QueueCreate(BaseModel): + name: str + scripts: List[str] + schedule: str + description: Optional[str] = None + +class QueueUpdate(BaseModel): + name: Optional[str] = None + scripts: Optional[List[str]] = None + schedule: Optional[str] = None + description: Optional[str] = None + +# Task Models +class TaskCreate(BaseModel): + name: str + scriptId: str + planId: str + queueId: Optional[str] = None + priority: int = 0 + parameters: Dict[str, Any] = {} + +# Settings Models +class SettingsUpdate(BaseModel): + key: str + value: Any + +# ====================== +# API Endpoints +# ====================== + +@app.get("/api/activity/latest", summary="获取最新活动内容") +async def get_latest_activity(): + """ + 获取最新活动内容 + """ + # 实现获取最新活动的逻辑 + return {"status": "success", "data": {}} + + +@app.post("/api/add/scripts", summary="添加脚本") +async def add_script(script: ScriptCreate = Body(...)): + """ + 添加脚本 + 这里后端需要支持两种脚本,MAA和通用 + """ + # 实现添加脚本的逻辑 + return {"status": "success", "scriptId": "new_script_id"} + + +@app.post("/api/get/scripts", summary="查询脚本") +async def get_scripts(): + """ + 查询脚本 + """ + # 实现查询脚本的逻辑 + return {"status": "success", "data": []} + + +@app.post("/api/get/scripts/{scriptId}", summary="查询单个脚本") +async def get_script( + scriptId: str = Path(..., description="脚本ID") +): + """ + 查询单个脚本 + """ + # 实现查询单个脚本的逻辑 + return {"status": "success", "data": {}} + + +@app.post("/api/update/scripts/{scriptId}", summary="更新脚本") +async def update_script( + scriptId: str = Path(..., description="脚本ID"), + update_data: ScriptUpdate = Body(...) +): + """ + 更新脚本 + """ + # 实现更新脚本的逻辑 + return {"status": "success"} + + +@app.post("/api/delete/scripts/{scriptId}", summary="删除脚本") +async def delete_script( + scriptId: str = Path(..., description="脚本ID") +): + """ + 删除脚本 + """ + # 实现删除脚本的逻辑 + return {"status": "success"} + + +@app.post("/api/scripts/{scriptId}/users", summary="为脚本添加用户") +async def add_script_user( + scriptId: str = Path(..., description="脚本ID"), + user: ScriptUser = Body(...) +): + """ + 为脚本添加用户 + """ + # 实现为脚本添加用户的逻辑 + return {"status": "success"} + + +@app.get("/api/scripts/{scriptId}/users", summary="查询脚本的所有下属用户") +async def get_script_users( + scriptId: str = Path(..., description="脚本ID") +): + """ + 查询脚本的所有下属用户 + """ + # 实现查询脚本的所有下属用户的逻辑 + return {"status": "success", "data": []} + + +@app.get("/api/scripts/{scriptId}/users/{userId}", summary="查询脚本下的单个下属用户") +async def get_script_user( + scriptId: str = Path(..., description="脚本ID"), + userId: str = Path(..., description="用户ID") +): + """ + 查询脚本下的单个下属用户 + """ + # 实现查询脚本下的单个下属用户的逻辑 + return {"status": "success", "data": {}} + + +@app.put("/api/scripts/{scriptId}/users/{userId}", summary="更新脚本下属用户的关联信息") +async def update_script_user( + scriptId: str = Path(..., description="脚本ID"), + userId: str = Path(..., description="用户ID"), + config: Dict[str, Any] = Body(...) +): + """ + 更新脚本下属用户的关联信息 + """ + # 实现更新脚本下属用户的关联信息的逻辑 + return {"status": "success"} + + +@app.delete("/api/scripts/{scriptId}/users/{userId}", summary="从脚本移除用户") +async def remove_script_user( + scriptId: str = Path(..., description="脚本ID"), + userId: str = Path(..., description="用户ID") +): + """ + 从脚本移除用户 + """ + # 实现从脚本移除用户的逻辑 + return {"status": "success"} + + +@app.post("/api/add/plans", summary="创建计划") +async def add_plan(plan: PlanCreate = Body(...)): + """ + 创建计划 + { + "name": "计划 1", + "mode": "全局", // 或 "周计划" + "details": { + "周一": { + "吃理智药": 0, + "连战次数": "AUTO", + "关卡选择": "当前/上次", + "备选-1": "当前/上次", + "备选-2": "当前/上次", + "备选-3": "当前/上次", + "剩余理智": "不使用" + }, + // 其他天数... + } + } + """ + # 实现创建计划的逻辑 + return {"status": "success", "planId": "new_plan_id"} + + +@app.post("/api/get/plans", summary="查询所有计划") +async def get_plans(): + """ + 查询所有计划 + """ + # 实现查询所有计划的逻辑 + return {"status": "success", "data": []} + + +@app.post("/api/get/plans/{planId}", summary="查询单个计划") +async def get_plan( + planId: str = Path(..., description="计划ID") +): + """ + 查询单个计划 + """ + # 实现查询单个计划的逻辑 + return {"status": "success", "data": {}} + + +@app.post("/api/update/plans/{planId}", summary="更新计划") +async def update_plan( + planId: str = Path(..., description="计划ID"), + update_data: PlanUpdate = Body(...) +): + """ + 更新计划 + """ + # 实现更新计划的逻辑 + return {"status": "success"} + + +@app.post("/api/delete/plans/{planId}", summary="删除计划") +async def delete_plan( + planId: str = Path(..., description="计划ID") +): + """ + 删除计划 + """ + # 实现删除计划的逻辑 + return {"status": "success"} + + +@app.post("/api/update/plans/{planId}/mode", summary="切换计划模式") +async def update_plan_mode( + planId: str = Path(..., description="计划ID"), + mode_data: PlanModeUpdate = Body(...) +): + """ + 切换计划模式 + { + "mode": "周计划" + } + """ + # 实现切换计划模式的逻辑 + return {"status": "success"} + + +@app.post("/api/add/queues", summary="创建调度队列") +async def add_queue(queue: QueueCreate = Body(...)): + """ + 创建调度队列 + """ + # 实现创建调度队列的逻辑 + return {"status": "success", "queueId": "new_queue_id"} + + +@app.post("/api/get/queues", summary="查询所有调度队列") +async def get_queues(): + """ + 查询所有调度队列 + """ + # 实现查询所有调度队列的逻辑 + return {"status": "success", "data": []} + + +@app.post("/api/get/queues/{queueId}", summary="查询单个调度队列详情") +async def get_queue( + queueId: str = Path(..., description="调度队列ID") +): + """ + 查询单个调度队列详情 + """ + # 实现查询单个调度队列详情的逻辑 + return {"status": "success", "data": {}} + + +@app.post("/api/update/queues/{queueId}", summary="更新调度队列") +async def update_queue( + queueId: str = Path(..., description="调度队列ID"), + update_data: QueueUpdate = Body(...) +): + """ + 更新调度队列 + """ + # 实现更新调度队列的逻辑 + return {"status": "success"} + + +@app.post("/api/delete/queues/{queueId}", summary="删除调度队列") +async def delete_queue( + queueId: str = Path(..., description="调度队列ID") +): + """ + 删除调度队列 + """ + # 实现删除调度队列的逻辑 + return {"status": "success"} + + +@app.post("/api/add/tasks", summary="添加任务") +async def add_task(task: TaskCreate = Body(...)): + """ + 添加任务 + """ + # 实现添加任务的逻辑 + return {"status": "success", "taskId": "new_task_id"} + + +@app.post("/api/tasks/{taskId}/start", summary="开始任务") +async def start_task( + taskId: str = Path(..., description="任务ID") +): + """ + 开始任务 + """ + # 实现开始任务的逻辑 + return {"status": "success"} + + +@app.post("/api/get/history", summary="查询历史记录") +async def get_history(): + """ + 查询历史记录 + """ + # 实现查询历史记录的逻辑 + return {"status": "success", "data": []} + + +@app.post("/api/update/settings", summary="更新部分设置") +async def update_settings(settings: SettingsUpdate = Body(...)): + """ + 更新部分设置 + """ + # 实现更新部分设置的逻辑 + return {"status": "success"} + + +# ====================== +# Error Handlers +# ====================== + +@app.exception_handler(HTTPException) +async def http_exception_handler(request, exc): + return { + "status": "error", + "code": exc.status_code, + "message": exc.detail + } + + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/app/core/MAA.py b/app/core/MAA.py new file mode 100644 index 0000000..e69de29 diff --git a/app/core/__init__.py b/app/core/__init__.py deleted file mode 100644 index 9240bc7..0000000 --- a/app/core/__init__.py +++ /dev/null @@ -1,63 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA核心组件包 -v4.4 -作者:DLmaster_361 -""" - -__version__ = "4.2.0" -__author__ = "DLmaster361 " -__license__ = "GPL-3.0 license" - -from .config import ( - QueueConfig, - MaaConfig, - MaaUserConfig, - MaaPlanConfig, - GeneralConfig, - GeneralSubConfig, - Config, -) -from .logger import logger -from .main_info_bar import MainInfoBar -from .network import Network -from .sound_player import SoundPlayer -from .task_manager import Task, TaskManager -from .timer import MainTimer - -__all__ = [ - "Config", - "QueueConfig", - "MaaConfig", - "MaaUserConfig", - "MaaPlanConfig", - "GeneralConfig", - "GeneralSubConfig", - "logger", - "MainInfoBar", - "Network", - "SoundPlayer", - "Task", - "TaskManager", - "MainTimer", -] diff --git a/app/core/config.py b/app/core/config.py deleted file mode 100644 index dccade8..0000000 --- a/app/core/config.py +++ /dev/null @@ -1,1882 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA配置管理 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtCore import Signal -import argparse -import sqlite3 -import json -import sys -import shutil -import re -import base64 -import calendar -from datetime import datetime, timedelta, date -from collections import defaultdict -from pathlib import Path -from qfluentwidgets import ( - QConfig, - ConfigItem, - OptionsConfigItem, - RangeConfigItem, - ConfigValidator, - FolderValidator, - BoolValidator, - RangeValidator, - OptionsValidator, - exceptionHandler, -) -from urllib.parse import urlparse -from typing import Union, Dict, List - -from .logger import logger -from .network import Network - - -class FileValidator(ConfigValidator): - """File validator""" - - def validate(self, value): - return Path(value).exists() - - def correct(self, value): - path = Path(value) - return str(path.absolute()).replace("\\", "/") - - -class UrlListValidator(ConfigValidator): - """Url list validator""" - - def validate(self, value): - - try: - result = urlparse(value) - return all([result.scheme, result.netloc]) - except ValueError: - return False - - def correct(self, value: List[str]): - - urls = [] - - for url in [_ for _ in value if _ != ""]: - if url[-1] != "/": - urls.append(f"{url}/") - else: - urls.append(url) - - return list(set([_ for _ in urls if self.validate(_)])) - - -class LQConfig(QConfig): - """局域配置类""" - - def __init__(self) -> None: - super().__init__() - - def toDict(self, serialize=True): - """convert config items to `dict`""" - items = {} - for name in dir(self._cfg): - item = getattr(self._cfg, name) - if not isinstance(item, ConfigItem): - continue - - value = item.serialize() if serialize else item.value - if not items.get(item.group): - if not item.name: - items[item.group] = value - else: - items[item.group] = {} - - if item.name: - items[item.group][item.name] = value - - return items - - @exceptionHandler() - def load(self, file=None, config=None): - """load config - - Parameters - ---------- - file: str or Path - the path of json config file - - config: Config - config object to be initialized - """ - if isinstance(config, QConfig): - self._cfg = config - self._cfg.themeChanged.connect(self.themeChanged) - - if isinstance(file, (str, Path)): - self._cfg.file = Path(file) - - try: - with open(self._cfg.file, encoding="utf-8") as f: - cfg = json.load(f) - except: - cfg = {} - - # map config items'key to item - items = {} - for name in dir(self._cfg): - item = getattr(self._cfg, name) - if isinstance(item, ConfigItem): - items[item.key] = item - - # update the value of config item - for k, v in cfg.items(): - if not isinstance(v, dict) and items.get(k) is not None: - items[k].deserializeFrom(v) - elif isinstance(v, dict): - for key, value in v.items(): - key = k + "." + key - if items.get(key) is not None: - items[key].deserializeFrom(value) - - self.theme = self.get(self._cfg.themeMode) - - -class GlobalConfig(LQConfig): - """全局配置""" - - def __init__(self) -> None: - super().__init__() - - self.function_HomeImageMode = OptionsConfigItem( - "Function", - "HomeImageMode", - "默认", - OptionsValidator(["默认", "自定义", "主题图像"]), - ) - self.function_HistoryRetentionTime = OptionsConfigItem( - "Function", - "HistoryRetentionTime", - 0, - OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]), - ) - self.function_IfAllowSleep = ConfigItem( - "Function", "IfAllowSleep", False, BoolValidator() - ) - self.function_IfSilence = ConfigItem( - "Function", "IfSilence", False, BoolValidator() - ) - self.function_BossKey = ConfigItem("Function", "BossKey", "") - self.function_UnattendedMode = ConfigItem( - "Function", "UnattendedMode", False, BoolValidator() - ) - self.function_IfAgreeBilibili = ConfigItem( - "Function", "IfAgreeBilibili", False, BoolValidator() - ) - self.function_IfSkipMumuSplashAds = ConfigItem( - "Function", "IfSkipMumuSplashAds", False, BoolValidator() - ) - - self.voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator()) - self.voice_Type = OptionsConfigItem( - "Voice", "Type", "simple", OptionsValidator(["simple", "noisy"]) - ) - - self.start_IfSelfStart = ConfigItem( - "Start", "IfSelfStart", False, BoolValidator() - ) - self.start_IfMinimizeDirectly = ConfigItem( - "Start", "IfMinimizeDirectly", False, BoolValidator() - ) - - self.ui_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator()) - self.ui_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator()) - self.ui_size = ConfigItem("UI", "size", "1200x700") - self.ui_location = ConfigItem("UI", "location", "100x100") - self.ui_maximized = ConfigItem("UI", "maximized", False, BoolValidator()) - - self.notify_SendTaskResultTime = OptionsConfigItem( - "Notify", - "SendTaskResultTime", - "不推送", - OptionsValidator(["不推送", "任何时刻", "仅失败时"]), - ) - self.notify_IfSendStatistic = ConfigItem( - "Notify", "IfSendStatistic", False, BoolValidator() - ) - self.notify_IfSendSixStar = ConfigItem( - "Notify", "IfSendSixStar", False, BoolValidator() - ) - self.notify_IfPushPlyer = ConfigItem( - "Notify", "IfPushPlyer", False, BoolValidator() - ) - self.notify_IfSendMail = ConfigItem( - "Notify", "IfSendMail", False, BoolValidator() - ) - self.notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "") - self.notify_AuthorizationCode = ConfigItem("Notify", "AuthorizationCode", "") - self.notify_FromAddress = ConfigItem("Notify", "FromAddress", "") - self.notify_ToAddress = ConfigItem("Notify", "ToAddress", "") - self.notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - self.notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - self.notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") - self.notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "") - self.notify_IfCompanyWebHookBot = ConfigItem( - "Notify", "IfCompanyWebHookBot", False, BoolValidator() - ) - self.notify_CompanyWebHookBotUrl = ConfigItem( - "Notify", "CompanyWebHookBotUrl", "" - ) - - self.update_IfAutoUpdate = ConfigItem( - "Update", "IfAutoUpdate", False, BoolValidator() - ) - self.update_UpdateType = OptionsConfigItem( - "Update", "UpdateType", "stable", OptionsValidator(["stable", "beta"]) - ) - self.update_ThreadNumb = RangeConfigItem( - "Update", "ThreadNumb", 8, RangeValidator(1, 32) - ) - self.update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "") - self.update_ProxyUrlList = ConfigItem( - "Update", "ProxyUrlList", [], UrlListValidator() - ) - self.update_MirrorChyanCDK = ConfigItem("Update", "MirrorChyanCDK", "") - - -class QueueConfig(LQConfig): - """队列配置""" - - def __init__(self) -> None: - super().__init__() - - self.QueueSet_Name = ConfigItem("QueueSet", "Name", "") - self.QueueSet_TimeEnabled = ConfigItem( - "QueueSet", "TimeEnabled", False, BoolValidator() - ) - self.QueueSet_StartUpEnabled = ConfigItem( - "QueueSet", "StartUpEnabled", False, BoolValidator() - ) - self.QueueSet_AfterAccomplish = OptionsConfigItem( - "QueueSet", - "AfterAccomplish", - "NoAction", - OptionsValidator( - [ - "NoAction", - "KillSelf", - "Sleep", - "Hibernate", - "Shutdown", - "ShutdownForce", - ] - ), - ) - - self.config_item_dict: dict[str, Dict[str, ConfigItem]] = { - "Queue": {}, - "Time": {}, - } - - for i in range(10): - - self.config_item_dict["Time"][f"Enabled_{i}"] = ConfigItem( - "Time", f"Enabled_{i}", False, BoolValidator() - ) - self.config_item_dict["Time"][f"Set_{i}"] = ConfigItem( - "Time", f"Set_{i}", "00:00" - ) - self.config_item_dict["Queue"][f"Script_{i}"] = OptionsConfigItem( - "Queue", f"Script_{i}", "禁用" - ) - - setattr( - self, f"Time_Enabled_{i}", self.config_item_dict["Time"][f"Enabled_{i}"] - ) - setattr(self, f"Time_Set_{i}", self.config_item_dict["Time"][f"Set_{i}"]) - setattr( - self, f"Queue_Script_{i}", self.config_item_dict["Queue"][f"Script_{i}"] - ) - - self.Data_LastProxyTime = ConfigItem( - "Data", "LastProxyTime", "2000-01-01 00:00:00" - ) - self.Data_LastProxyHistory = ConfigItem( - "Data", "LastProxyHistory", "暂无历史运行记录" - ) - - -class MaaConfig(LQConfig): - """MAA配置""" - - def __init__(self) -> None: - super().__init__() - - self.MaaSet_Name = ConfigItem("MaaSet", "Name", "") - self.MaaSet_Path = ConfigItem("MaaSet", "Path", ".", FolderValidator()) - - self.RunSet_TaskTransitionMethod = OptionsConfigItem( - "RunSet", - "TaskTransitionMethod", - "ExitEmulator", - OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]), - ) - self.RunSet_ProxyTimesLimit = RangeConfigItem( - "RunSet", "ProxyTimesLimit", 0, RangeValidator(0, 1024) - ) - self.RunSet_ADBSearchRange = RangeConfigItem( - "RunSet", "ADBSearchRange", 0, RangeValidator(0, 3) - ) - self.RunSet_RunTimesLimit = RangeConfigItem( - "RunSet", "RunTimesLimit", 3, RangeValidator(1, 1024) - ) - self.RunSet_AnnihilationTimeLimit = RangeConfigItem( - "RunSet", "AnnihilationTimeLimit", 40, RangeValidator(1, 1024) - ) - self.RunSet_RoutineTimeLimit = RangeConfigItem( - "RunSet", "RoutineTimeLimit", 10, RangeValidator(1, 1024) - ) - self.RunSet_AnnihilationWeeklyLimit = ConfigItem( - "RunSet", "AnnihilationWeeklyLimit", True, BoolValidator() - ) - - def get_name(self) -> str: - return self.get(self.MaaSet_Name) - - -class MaaUserConfig(LQConfig): - """MAA用户配置""" - - def __init__(self) -> None: - super().__init__() - - self.Info_Name = ConfigItem("Info", "Name", "新用户") - self.Info_Id = ConfigItem("Info", "Id", "") - self.Info_Mode = OptionsConfigItem( - "Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"]) - ) - self.Info_StageMode = ConfigItem("Info", "StageMode", "固定") - self.Info_Server = OptionsConfigItem( - "Info", - "Server", - "Official", - OptionsValidator( - ["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"] - ), - ) - self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) - self.Info_RemainedDay = ConfigItem( - "Info", "RemainedDay", -1, RangeValidator(-1, 1024) - ) - self.Info_Annihilation = OptionsConfigItem( - "Info", - "Annihilation", - "Annihilation", - OptionsValidator( - [ - "Close", - "Annihilation", - "Chernobog@Annihilation", - "LungmenOutskirts@Annihilation", - "LungmenDowntown@Annihilation", - ] - ), - ) - self.Info_Routine = ConfigItem("Info", "Routine", True, BoolValidator()) - self.Info_InfrastMode = OptionsConfigItem( - "Info", - "InfrastMode", - "Normal", - OptionsValidator(["Normal", "Rotation", "Custom"]), - ) - self.Info_Password = ConfigItem("Info", "Password", "") - self.Info_Notes = ConfigItem("Info", "Notes", "无") - self.Info_MedicineNumb = ConfigItem( - "Info", "MedicineNumb", 0, RangeValidator(0, 1024) - ) - self.Info_SeriesNumb = OptionsConfigItem( - "Info", - "SeriesNumb", - "0", - OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]), - ) - self.Info_Stage = ConfigItem("Info", "Stage", "-") - self.Info_Stage_1 = ConfigItem("Info", "Stage_1", "-") - self.Info_Stage_2 = ConfigItem("Info", "Stage_2", "-") - self.Info_Stage_3 = ConfigItem("Info", "Stage_3", "-") - self.Info_Stage_Remain = ConfigItem("Info", "Stage_Remain", "-") - self.Info_IfSkland = ConfigItem("Info", "IfSkland", False, BoolValidator()) - self.Info_SklandToken = ConfigItem("Info", "SklandToken", "") - - self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01") - self.Data_LastAnnihilationDate = ConfigItem( - "Data", "LastAnnihilationDate", "2000-01-01" - ) - self.Data_LastSklandDate = ConfigItem("Data", "LastSklandDate", "2000-01-01") - self.Data_ProxyTimes = ConfigItem( - "Data", "ProxyTimes", 0, RangeValidator(0, 1024) - ) - self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator()) - self.Data_CustomInfrastPlanIndex = ConfigItem( - "Data", "CustomInfrastPlanIndex", "0" - ) - - self.Task_IfWakeUp = ConfigItem("Task", "IfWakeUp", True, BoolValidator()) - self.Task_IfRecruiting = ConfigItem( - "Task", "IfRecruiting", True, BoolValidator() - ) - self.Task_IfBase = ConfigItem("Task", "IfBase", True, BoolValidator()) - self.Task_IfCombat = ConfigItem("Task", "IfCombat", True, BoolValidator()) - self.Task_IfMall = ConfigItem("Task", "IfMall", True, BoolValidator()) - self.Task_IfMission = ConfigItem("Task", "IfMission", True, BoolValidator()) - self.Task_IfAutoRoguelike = ConfigItem( - "Task", "IfAutoRoguelike", False, BoolValidator() - ) - self.Task_IfReclamation = ConfigItem( - "Task", "IfReclamation", False, BoolValidator() - ) - - self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator()) - self.Notify_IfSendStatistic = ConfigItem( - "Notify", "IfSendStatistic", False, BoolValidator() - ) - self.Notify_IfSendSixStar = ConfigItem( - "Notify", "IfSendSixStar", False, BoolValidator() - ) - self.Notify_IfSendMail = ConfigItem( - "Notify", "IfSendMail", False, BoolValidator() - ) - self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "") - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - self.Notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") - self.Notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "") - self.Notify_IfCompanyWebHookBot = ConfigItem( - "Notify", "IfCompanyWebHookBot", False, BoolValidator() - ) - self.Notify_CompanyWebHookBotUrl = ConfigItem( - "Notify", "CompanyWebHookBotUrl", "" - ) - - def get_plan_info(self) -> Dict[str, Union[str, int]]: - """获取当前的计划下信息""" - - if self.get(self.Info_StageMode) == "固定": - return { - "MedicineNumb": self.get(self.Info_MedicineNumb), - "SeriesNumb": self.get(self.Info_SeriesNumb), - "Stage": self.get(self.Info_Stage), - "Stage_1": self.get(self.Info_Stage_1), - "Stage_2": self.get(self.Info_Stage_2), - "Stage_3": self.get(self.Info_Stage_3), - "Stage_Remain": self.get(self.Info_Stage_Remain), - } - elif "计划" in self.get(self.Info_StageMode): - plan = Config.plan_dict[self.get(self.Info_StageMode)]["Config"] - return { - "MedicineNumb": plan.get(plan.get_current_info("MedicineNumb")), - "SeriesNumb": plan.get(plan.get_current_info("SeriesNumb")), - "Stage": plan.get(plan.get_current_info("Stage")), - "Stage_1": plan.get(plan.get_current_info("Stage_1")), - "Stage_2": plan.get(plan.get_current_info("Stage_2")), - "Stage_3": plan.get(plan.get_current_info("Stage_3")), - "Stage_Remain": plan.get(plan.get_current_info("Stage_Remain")), - } - - -class MaaPlanConfig(LQConfig): - """MAA计划表配置""" - - def __init__(self) -> None: - super().__init__() - - self.Info_Name = ConfigItem("Info", "Name", "") - self.Info_Mode = OptionsConfigItem( - "Info", "Mode", "ALL", OptionsValidator(["ALL", "Weekly"]) - ) - - self.config_item_dict: dict[str, Dict[str, ConfigItem]] = {} - - for group in [ - "ALL", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - "Sunday", - ]: - self.config_item_dict[group] = {} - - self.config_item_dict[group]["MedicineNumb"] = ConfigItem( - group, "MedicineNumb", 0, RangeValidator(0, 1024) - ) - self.config_item_dict[group]["SeriesNumb"] = OptionsConfigItem( - group, - "SeriesNumb", - "0", - OptionsValidator(["0", "6", "5", "4", "3", "2", "1", "-1"]), - ) - self.config_item_dict[group]["Stage"] = ConfigItem(group, "Stage", "-") - self.config_item_dict[group]["Stage_1"] = ConfigItem(group, "Stage_1", "-") - self.config_item_dict[group]["Stage_2"] = ConfigItem(group, "Stage_2", "-") - self.config_item_dict[group]["Stage_3"] = ConfigItem(group, "Stage_3", "-") - self.config_item_dict[group]["Stage_Remain"] = ConfigItem( - group, "Stage_Remain", "-" - ) - - for name in [ - "MedicineNumb", - "SeriesNumb", - "Stage", - "Stage_1", - "Stage_2", - "Stage_3", - "Stage_Remain", - ]: - setattr(self, f"{group}_{name}", self.config_item_dict[group][name]) - - def get_current_info(self, name: str) -> ConfigItem: - """获取当前的计划表配置项""" - - if self.get(self.Info_Mode) == "ALL": - - return self.config_item_dict["ALL"][name] - - elif self.get(self.Info_Mode) == "Weekly": - - dt = datetime.now() - if dt.time() < datetime.min.time().replace(hour=4): - dt = dt - timedelta(days=1) - today = dt.strftime("%A") - - if today in self.config_item_dict: - return self.config_item_dict[today][name] - else: - return self.config_item_dict["ALL"][name] - - -class GeneralConfig(LQConfig): - """通用配置""" - - def __init__(self) -> None: - super().__init__() - - self.Script_Name = ConfigItem("Script", "Name", "") - self.Script_RootPath = ConfigItem("Script", "RootPath", ".", FileValidator()) - self.Script_ScriptPath = ConfigItem( - "Script", "ScriptPath", ".", FileValidator() - ) - self.Script_Arguments = ConfigItem("Script", "Arguments", "") - self.Script_IfTrackProcess = ConfigItem( - "Script", "IfTrackProcess", False, BoolValidator() - ) - self.Script_ConfigPath = ConfigItem( - "Script", "ConfigPath", ".", FileValidator() - ) - self.Script_ConfigPathMode = OptionsConfigItem( - "Script", - "ConfigPathMode", - "所有文件 (*)", - OptionsValidator(["所有文件 (*)", "文件夹"]), - ) - self.Script_UpdateConfigMode = OptionsConfigItem( - "Script", - "UpdateConfigMode", - "Never", - OptionsValidator(["Never", "Success", "Failure", "Always"]), - ) - self.Script_LogPath = ConfigItem("Script", "LogPath", ".", FileValidator()) - self.Script_LogPathFormat = ConfigItem("Script", "LogPathFormat", "%Y-%m-%d") - self.Script_LogTimeStart = ConfigItem( - "Script", "LogTimeStart", 1, RangeValidator(1, 1024) - ) - self.Script_LogTimeEnd = ConfigItem( - "Script", "LogTimeEnd", 1, RangeValidator(1, 1024) - ) - self.Script_LogTimeFormat = ConfigItem( - "Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S" - ) - self.Script_SuccessLog = ConfigItem("Script", "SuccessLog", "") - self.Script_ErrorLog = ConfigItem("Script", "ErrorLog", "") - - self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator()) - self.Game_Style = OptionsConfigItem( - "Game", "Style", "Emulator", OptionsValidator(["Emulator", "Client"]) - ) - self.Game_Path = ConfigItem("Game", "Path", ".", FileValidator()) - self.Game_Arguments = ConfigItem("Game", "Arguments", "") - self.Game_WaitTime = ConfigItem("Game", "WaitTime", 0, RangeValidator(0, 1024)) - self.Game_IfForceClose = ConfigItem( - "Game", "IfForceClose", False, BoolValidator() - ) - - self.Run_ProxyTimesLimit = RangeConfigItem( - "Run", "ProxyTimesLimit", 0, RangeValidator(0, 1024) - ) - self.Run_RunTimesLimit = RangeConfigItem( - "Run", "RunTimesLimit", 3, RangeValidator(1, 1024) - ) - self.Run_RunTimeLimit = RangeConfigItem( - "Run", "RunTimeLimit", 10, RangeValidator(1, 1024) - ) - - def get_name(self) -> str: - return self.get(self.Script_Name) - - -class GeneralSubConfig(LQConfig): - """通用子配置""" - - def __init__(self) -> None: - super().__init__() - - self.Info_Name = ConfigItem("Info", "Name", "新配置") - self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator()) - self.Info_RemainedDay = ConfigItem( - "Info", "RemainedDay", -1, RangeValidator(-1, 1024) - ) - self.Info_IfScriptBeforeTask = ConfigItem( - "Info", "IfScriptBeforeTask", False, BoolValidator() - ) - self.Info_ScriptBeforeTask = ConfigItem( - "Info", "ScriptBeforeTask", "", FileValidator() - ) - self.Info_IfScriptAfterTask = ConfigItem( - "Info", "IfScriptAfterTask", False, BoolValidator() - ) - self.Info_ScriptAfterTask = ConfigItem( - "Info", "ScriptAfterTask", "", FileValidator() - ) - self.Info_Notes = ConfigItem("Info", "Notes", "无") - - self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01") - self.Data_ProxyTimes = ConfigItem( - "Data", "ProxyTimes", 0, RangeValidator(0, 1024) - ) - - self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator()) - self.Notify_IfSendStatistic = ConfigItem( - "Notify", "IfSendStatistic", False, BoolValidator() - ) - self.Notify_IfSendMail = ConfigItem( - "Notify", "IfSendMail", False, BoolValidator() - ) - self.Notify_ToAddress = ConfigItem("Notify", "ToAddress", "") - self.Notify_IfServerChan = ConfigItem( - "Notify", "IfServerChan", False, BoolValidator() - ) - self.Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "") - self.Notify_ServerChanChannel = ConfigItem("Notify", "ServerChanChannel", "") - self.Notify_ServerChanTag = ConfigItem("Notify", "ServerChanTag", "") - self.Notify_IfCompanyWebHookBot = ConfigItem( - "Notify", "IfCompanyWebHookBot", False, BoolValidator() - ) - self.Notify_CompanyWebHookBotUrl = ConfigItem( - "Notify", "CompanyWebHookBotUrl", "" - ) - - -class AppConfig(GlobalConfig): - - VERSION = "4.4.1.6" - - stage_refreshed = Signal() - PASSWORD_refreshed = Signal() - sub_info_changed = Signal() - power_sign_changed = Signal() - - def __init__(self) -> None: - super().__init__() - - self.app_path = Path(sys.argv[0]).resolve().parent - self.app_path_sys = Path(sys.argv[0]).resolve() - - self.log_path = self.app_path / "debug/AUTO_MAA.log" - self.database_path = self.app_path / "data/data.db" - self.config_path = self.app_path / "config/config.json" - self.key_path = self.app_path / "data/key" - - self.main_window = None - self.PASSWORD = "" - self.running_list = [] - self.silence_dict: Dict[Path, datetime] = {} - self.info_bar_list = [] - self.stage_dict = { - "ALL": {"value": [], "text": []}, - "Monday": {"value": [], "text": []}, - "Tuesday": {"value": [], "text": []}, - "Wednesday": {"value": [], "text": []}, - "Thursday": {"value": [], "text": []}, - "Friday": {"value": [], "text": []}, - "Saturday": {"value": [], "text": []}, - "Sunday": {"value": [], "text": []}, - } - self.power_sign = "NoAction" - self.if_ignore_silence = False - self.if_database_opened = False - - self.initialize() - - self.search_script() - self.search_queue() - - parser = argparse.ArgumentParser( - prog="AUTO_MAA", - description="A MAA Multi Account Management and Automation Tool", - ) - parser.add_argument( - "--mode", - choices=["gui", "cli"], - default="gui", - help="使用UI界面或命令行模式运行程序", - ) - parser.add_argument( - "--config", - nargs="+", - choices=list(self.script_dict.keys()) + list(self.queue_dict.keys()), - help="指定需要运行哪些配置项", - ) - self.args = parser.parse_args() - - logger.info( - f"运行模式: {'图形化界面' if self.args.mode == 'gui' else '命令行界面'},配置项: {self.args.config if self.args.config else '启动时运行的调度队列'}", - module="配置管理", - ) - - def initialize(self) -> None: - """初始化程序配置管理模块""" - - # 检查目录 - (self.app_path / "config").mkdir(parents=True, exist_ok=True) - (self.app_path / "data").mkdir(parents=True, exist_ok=True) - (self.app_path / "debug").mkdir(parents=True, exist_ok=True) - (self.app_path / "history").mkdir(parents=True, exist_ok=True) - - self.load(self.config_path, self) - self.save() - - self.init_logger() - self.check_data() - logger.info("程序初始化完成", module="配置管理") - - def init_logger(self) -> None: - """初始化日志记录器""" - - logger.add( - sink=self.log_path, - level="DEBUG", - format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {extra[module]} | {message}", - enqueue=True, - backtrace=True, - diagnose=True, - rotation="1 week", - retention="1 month", - compression="zip", - ) - logger.add( - sink=sys.stderr, - level="DEBUG", - format="{time:YYYY-MM-DD HH:mm:ss.SSS} | {level: <8} | {extra[module]} | {message}", - enqueue=True, - backtrace=True, - diagnose=True, - colorize=True, - ) - - logger.info("", module="配置管理") - logger.info("===================================", module="配置管理") - logger.info("AUTO_MAA 主程序", module="配置管理") - logger.info(f"版本号: v{self.VERSION}", module="配置管理") - logger.info(f"根目录: {self.app_path}", module="配置管理") - logger.info("===================================", module="配置管理") - - def check_data(self) -> None: - """检查用户数据文件并处理数据文件版本更新""" - - # 生成主数据库 - if not self.database_path.exists(): - db = sqlite3.connect(self.database_path) - cur = db.cursor() - cur.execute("CREATE TABLE version(v text)") - cur.execute("INSERT INTO version VALUES(?)", ("v1.8",)) - db.commit() - cur.close() - db.close() - - # 数据文件版本更新 - db = sqlite3.connect(self.database_path) - cur = db.cursor() - cur.execute("SELECT * FROM version WHERE True") - version = cur.fetchall() - - if version[0][0] != "v1.8": - logger.info("数据文件版本更新开始", module="配置管理") - if_streaming = False - # v1.4-->v1.5 - if version[0][0] == "v1.4" or if_streaming: - logger.info("数据文件版本更新:v1.4-->v1.5", module="配置管理") - if_streaming = True - - member_dict: Dict[str, Dict[str, Union[str, Path]]] = {} - if (self.app_path / "config/MaaConfig").exists(): - for maa_dir in (self.app_path / "config/MaaConfig").iterdir(): - if maa_dir.is_dir(): - member_dict[maa_dir.name] = { - "Type": "Maa", - "Path": maa_dir, - } - - member_dict = dict( - sorted(member_dict.items(), key=lambda x: int(x[0][3:])) - ) - - for name, config in member_dict.items(): - if config["Type"] == "Maa": - - _db = sqlite3.connect(config["Path"] / "user_data.db") - _cur = _db.cursor() - _cur.execute("SELECT * FROM adminx WHERE True") - data = _cur.fetchall() - data = [list(row) for row in data] - data = sorted(data, key=lambda x: (-len(x[15]), x[16])) - _cur.close() - _db.close() - - (config["Path"] / "user_data.db").unlink() - - (config["Path"] / f"UserData").mkdir( - parents=True, exist_ok=True - ) - - for i in range(len(data)): - - info = { - "Data": { - "IfPassCheck": True, - "LastAnnihilationDate": "2000-01-01", - "LastProxyDate": data[i][5], - "ProxyTimes": data[i][14], - }, - "Info": { - "Annihilation": bool(data[i][10] == "y"), - "GameId": data[i][6], - "GameIdMode": "固定", - "GameId_1": data[i][7], - "GameId_2": data[i][8], - "Id": data[i][1], - "Infrastructure": bool(data[i][11] == "y"), - "MedicineNumb": 0, - "Mode": ( - "简洁" if data[i][15] == "simple" else "详细" - ), - "Name": data[i][0], - "Notes": data[i][13], - "Password": base64.b64encode(data[i][12]).decode( - "utf-8" - ), - "RemainedDay": data[i][3], - "Routine": bool(data[i][9] == "y"), - "Server": data[i][2], - "Status": bool(data[i][4] == "y"), - }, - } - - (config["Path"] / f"UserData/用户_{i + 1}").mkdir( - parents=True, exist_ok=True - ) - with ( - config["Path"] / f"UserData/用户_{i + 1}/config.json" - ).open(mode="w", encoding="utf-8") as f: - json.dump(info, f, ensure_ascii=False, indent=4) - - if ( - self.app_path - / f"config/MaaConfig/{name}/{data[i][15]}/{data[i][16]}/annihilation/gui.json" - ).exists(): - ( - config["Path"] - / f"UserData/用户_{i + 1}/Annihilation" - ).mkdir(parents=True, exist_ok=True) - shutil.move( - self.app_path - / f"config/MaaConfig/{name}/{data[i][15]}/{data[i][16]}/annihilation/gui.json", - config["Path"] - / f"UserData/用户_{i + 1}/Annihilation/gui.json", - ) - if ( - self.app_path - / f"config/MaaConfig/{name}/{data[i][15]}/{data[i][16]}/routine/gui.json" - ).exists(): - ( - config["Path"] / f"UserData/用户_{i + 1}/Routine" - ).mkdir(parents=True, exist_ok=True) - shutil.move( - self.app_path - / f"config/MaaConfig/{name}/{data[i][15]}/{data[i][16]}/routine/gui.json", - config["Path"] - / f"UserData/用户_{i + 1}/Routine/gui.json", - ) - if ( - self.app_path - / f"config/MaaConfig/{name}/{data[i][15]}/{data[i][16]}/infrastructure/infrastructure.json" - ).exists(): - ( - config["Path"] - / f"UserData/用户_{i + 1}/Infrastructure" - ).mkdir(parents=True, exist_ok=True) - shutil.move( - self.app_path - / f"config/MaaConfig/{name}/{data[i][15]}/{data[i][16]}/infrastructure/infrastructure.json", - config["Path"] - / f"UserData/用户_{i + 1}/Infrastructure/infrastructure.json", - ) - - if (config["Path"] / f"simple").exists(): - shutil.rmtree(config["Path"] / f"simple") - if (config["Path"] / f"beta").exists(): - shutil.rmtree(config["Path"] / f"beta") - - cur.execute("DELETE FROM version WHERE v = ?", ("v1.4",)) - cur.execute("INSERT INTO version VALUES(?)", ("v1.5",)) - db.commit() - # v1.5-->v1.6 - if version[0][0] == "v1.5" or if_streaming: - logger.info("数据文件版本更新:v1.5-->v1.6", module="配置管理") - if_streaming = True - cur.execute("DELETE FROM version WHERE v = ?", ("v1.5",)) - cur.execute("INSERT INTO version VALUES(?)", ("v1.6",)) - db.commit() - # 删除旧的注册表项 - import winreg - - key = winreg.OpenKey( - winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Run", - 0, - winreg.KEY_READ, - ) - - try: - value, _ = winreg.QueryValueEx(key, "AUTO_MAA") - winreg.CloseKey(key) - key = winreg.OpenKey( - winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Run", - winreg.KEY_SET_VALUE, - winreg.KEY_ALL_ACCESS - | winreg.KEY_WRITE - | winreg.KEY_CREATE_SUB_KEY, - ) - winreg.DeleteValue(key, "AUTO_MAA") - winreg.CloseKey(key) - except FileNotFoundError: - pass - # v1.6-->v1.7 - if version[0][0] == "v1.6" or if_streaming: - logger.info("数据文件版本更新:v1.6-->v1.7", module="配置管理") - if_streaming = True - - if (self.app_path / "config/MaaConfig").exists(): - - for MaaConfig in (self.app_path / "config/MaaConfig").iterdir(): - if MaaConfig.is_dir(): - for user in (MaaConfig / "UserData").iterdir(): - if user.is_dir(): - if (user / "config.json").exists(): - with (user / "config.json").open( - encoding="utf-8" - ) as f: - user_config = json.load(f) - user_config["Info"]["Stage"] = user_config[ - "Info" - ]["GameId"] - user_config["Info"]["StageMode"] = user_config[ - "Info" - ]["GameIdMode"] - user_config["Info"]["Stage_1"] = user_config[ - "Info" - ]["GameId_1"] - user_config["Info"]["Stage_2"] = user_config[ - "Info" - ]["GameId_2"] - user_config["Info"]["Stage_Remain"] = ( - user_config["Info"]["GameId_Remain"] - ) - with (user / "config.json").open( - "w", encoding="utf-8" - ) as f: - json.dump( - user_config, - f, - ensure_ascii=False, - indent=4, - ) - - if (self.app_path / "config/MaaPlanConfig").exists(): - for MaaPlanConfig in ( - self.app_path / "config/MaaPlanConfig" - ).iterdir(): - if ( - MaaPlanConfig.is_dir() - and (MaaPlanConfig / "config.json").exists() - ): - with (MaaPlanConfig / "config.json").open( - encoding="utf-8" - ) as f: - plan_config = json.load(f) - - for k in self.stage_dict.keys(): - plan_config[k]["Stage"] = plan_config[k]["GameId"] - plan_config[k]["Stage_1"] = plan_config[k]["GameId_1"] - plan_config[k]["Stage_2"] = plan_config[k]["GameId_2"] - plan_config[k]["Stage_Remain"] = plan_config[k][ - "GameId_Remain" - ] - with (MaaPlanConfig / "config.json").open( - "w", encoding="utf-8" - ) as f: - json.dump(plan_config, f, ensure_ascii=False, indent=4) - - cur.execute("DELETE FROM version WHERE v = ?", ("v1.6",)) - cur.execute("INSERT INTO version VALUES(?)", ("v1.7",)) - db.commit() - # v1.7-->v1.8 - if version[0][0] == "v1.7" or if_streaming: - logger.info("数据文件版本更新:v1.7-->v1.8", module="配置管理") - if_streaming = True - - if (self.app_path / "config/QueueConfig").exists(): - for QueueConfig in (self.app_path / "config/QueueConfig").glob( - "*.json" - ): - with QueueConfig.open(encoding="utf-8") as f: - queue_config = json.load(f) - - queue_config["QueueSet"]["TimeEnabled"] = queue_config[ - "QueueSet" - ]["Enabled"] - - for i in range(10): - queue_config["Queue"][f"Script_{i}"] = queue_config[ - "Queue" - ][f"Member_{i + 1}"] - queue_config["Time"][f"Enabled_{i}"] = queue_config["Time"][ - f"TimeEnabled_{i}" - ] - queue_config["Time"][f"Set_{i}"] = queue_config["Time"][ - f"TimeSet_{i}" - ] - - with QueueConfig.open("w", encoding="utf-8") as f: - json.dump(queue_config, f, ensure_ascii=False, indent=4) - - cur.execute("DELETE FROM version WHERE v = ?", ("v1.7",)) - cur.execute("INSERT INTO version VALUES(?)", ("v1.8",)) - db.commit() - - cur.close() - db.close() - logger.success("数据文件版本更新完成", module="配置管理") - - def get_stage(self) -> None: - """从MAA服务器更新活动关卡信息""" - - logger.info("开始获取活动关卡信息", module="配置管理") - network = Network.add_task( - mode="get", - url="https://api.maa.plus/MaaAssistantArknights/api/gui/StageActivity.json", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - stage_infos: List[Dict[str, Union[str, Dict[str, Union[str, int]]]]] = ( - network_result["response_json"]["Official"]["sideStoryStage"] - ) - else: - logger.warning( - f"无法从MAA服务器获取活动关卡信息:{network_result['error_message']}", - module="配置管理", - ) - stage_infos = [] - - ss_stage_dict = {"value": [], "text": []} - - for stage_info in stage_infos: - - if ( - datetime.strptime( - stage_info["Activity"]["UtcStartTime"], "%Y/%m/%d %H:%M:%S" - ) - < datetime.now() - < datetime.strptime( - stage_info["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S" - ) - ): - ss_stage_dict["value"].append(stage_info["Value"]) - ss_stage_dict["text"].append(stage_info["Value"]) - - # 生成每日关卡信息 - stage_daily_info = [ - {"value": "-", "text": "当前/上次", "days": [1, 2, 3, 4, 5, 6, 7]}, - {"value": "1-7", "text": "1-7", "days": [1, 2, 3, 4, 5, 6, 7]}, - {"value": "R8-11", "text": "R8-11", "days": [1, 2, 3, 4, 5, 6, 7]}, - { - "value": "12-17-HARD", - "text": "12-17-HARD", - "days": [1, 2, 3, 4, 5, 6, 7], - }, - {"value": "CE-6", "text": "龙门币-6/5", "days": [2, 4, 6, 7]}, - {"value": "AP-5", "text": "红票-5", "days": [1, 4, 6, 7]}, - {"value": "CA-5", "text": "技能-5", "days": [2, 3, 5, 7]}, - {"value": "LS-6", "text": "经验-6/5", "days": [1, 2, 3, 4, 5, 6, 7]}, - {"value": "SK-5", "text": "碳-5", "days": [1, 3, 5, 6]}, - {"value": "PR-A-1", "text": "奶/盾芯片", "days": [1, 4, 5, 7]}, - {"value": "PR-A-2", "text": "奶/盾芯片组", "days": [1, 4, 5, 7]}, - {"value": "PR-B-1", "text": "术/狙芯片", "days": [1, 2, 5, 6]}, - {"value": "PR-B-2", "text": "术/狙芯片组", "days": [1, 2, 5, 6]}, - {"value": "PR-C-1", "text": "先/辅芯片", "days": [3, 4, 6, 7]}, - {"value": "PR-C-2", "text": "先/辅芯片组", "days": [3, 4, 6, 7]}, - {"value": "PR-D-1", "text": "近/特芯片", "days": [2, 3, 6, 7]}, - {"value": "PR-D-2", "text": "近/特芯片组", "days": [2, 3, 6, 7]}, - ] - - for day in range(0, 8): - - today_stage_dict = {"value": [], "text": []} - - for stage_info in stage_daily_info: - - if day in stage_info["days"] or day == 0: - today_stage_dict["value"].append(stage_info["value"]) - today_stage_dict["text"].append(stage_info["text"]) - - self.stage_dict[calendar.day_name[day - 1] if day > 0 else "ALL"] = { - "value": today_stage_dict["value"] + ss_stage_dict["value"], - "text": today_stage_dict["text"] + ss_stage_dict["text"], - } - - self.stage_refreshed.emit() - - logger.success("活动关卡信息更新完成", module="配置管理") - - def server_date(self) -> date: - """ - 获取当前的服务器日期 - - :return: 当前的服务器日期 - :rtype: date - """ - - dt = datetime.now() - if dt.time() < datetime.min.time().replace(hour=4): - dt = dt - timedelta(days=1) - return dt.date() - - def search_script(self) -> None: - """更新脚本实例配置信息""" - - logger.info("开始搜索并读入脚本实例配置", module="配置管理") - self.script_dict: Dict[ - str, - Dict[ - str, - Union[ - str, - Path, - Union[MaaConfig, GeneralConfig], - Dict[str, Dict[str, Union[Path, MaaUserConfig, GeneralSubConfig]]], - ], - ], - ] = {} - if (self.app_path / "config/MaaConfig").exists(): - for maa_dir in (self.app_path / "config/MaaConfig").iterdir(): - if maa_dir.is_dir(): - - maa_config = MaaConfig() - maa_config.load(maa_dir / "config.json", maa_config) - maa_config.save() - - self.script_dict[maa_dir.name] = { - "Type": "Maa", - "Path": maa_dir, - "Config": maa_config, - "UserData": None, - } - if (self.app_path / "config/GeneralConfig").exists(): - for general_dir in (self.app_path / "config/GeneralConfig").iterdir(): - if general_dir.is_dir(): - - general_config = GeneralConfig() - general_config.load(general_dir / "config.json", general_config) - general_config.save() - - self.script_dict[general_dir.name] = { - "Type": "General", - "Path": general_dir, - "Config": general_config, - "SubData": None, - } - - self.script_dict = dict( - sorted(self.script_dict.items(), key=lambda x: int(x[0][3:])) - ) - - logger.success( - f"脚本实例配置搜索完成,共找到 {len(self.script_dict)} 个实例", - module="配置管理", - ) - - def search_maa_user(self, name: str) -> None: - """ - 更新指定 MAA 脚本实例的用户信息 - - :param name: 脚本实例名称 - :type name: str - """ - - logger.info(f"开始搜索并读入 MAA 脚本实例 {name} 的用户信息", module="配置管理") - - user_dict: Dict[str, Dict[str, Union[Path, MaaUserConfig]]] = {} - for user_dir in (Config.script_dict[name]["Path"] / "UserData").iterdir(): - if user_dir.is_dir(): - - user_config = MaaUserConfig() - user_config.load(user_dir / "config.json", user_config) - user_config.save() - - user_dict[user_dir.stem] = {"Path": user_dir, "Config": user_config} - - self.script_dict[name]["UserData"] = dict( - sorted(user_dict.items(), key=lambda x: int(x[0][3:])) - ) - - logger.success( - f"MAA 脚本实例 {name} 的用户信息搜索完成,共找到 {len(user_dict)} 个用户", - module="配置管理", - ) - - def search_general_sub(self, name: str) -> None: - """ - 更新指定通用脚本实例的子配置信息 - - :param name: 脚本实例名称 - :type name: str - """ - - logger.info( - f"开始搜索并读入通用脚本实例 {name} 的子配置信息", module="配置管理" - ) - - user_dict: Dict[str, Dict[str, Union[Path, GeneralSubConfig]]] = {} - for sub_dir in (Config.script_dict[name]["Path"] / "SubData").iterdir(): - if sub_dir.is_dir(): - - sub_config = GeneralSubConfig() - sub_config.load(sub_dir / "config.json", sub_config) - sub_config.save() - - user_dict[sub_dir.stem] = {"Path": sub_dir, "Config": sub_config} - - self.script_dict[name]["SubData"] = dict( - sorted(user_dict.items(), key=lambda x: int(x[0][3:])) - ) - - logger.success( - f"通用脚本实例 {name} 的子配置信息搜索完成,共找到 {len(user_dict)} 个子配置", - module="配置管理", - ) - - def search_plan(self) -> None: - """更新计划表配置信息""" - - logger.info("开始搜索并读入计划表配置", module="配置管理") - - self.plan_dict: Dict[str, Dict[str, Union[str, Path, MaaPlanConfig]]] = {} - if (self.app_path / "config/MaaPlanConfig").exists(): - for maa_plan_dir in (self.app_path / "config/MaaPlanConfig").iterdir(): - if maa_plan_dir.is_dir(): - - maa_plan_config = MaaPlanConfig() - maa_plan_config.load(maa_plan_dir / "config.json", maa_plan_config) - maa_plan_config.save() - - self.plan_dict[maa_plan_dir.name] = { - "Type": "Maa", - "Path": maa_plan_dir, - "Config": maa_plan_config, - } - - self.plan_dict = dict( - sorted(self.plan_dict.items(), key=lambda x: int(x[0][3:])) - ) - - logger.success( - f"计划表配置搜索完成,共找到 {len(self.plan_dict)} 个计划表", - module="配置管理", - ) - - def search_queue(self): - """更新调度队列实例配置信息""" - - logger.info("开始搜索并读入调度队列配置", module="配置管理") - - self.queue_dict: Dict[str, Dict[str, Union[Path, QueueConfig]]] = {} - - if (self.app_path / "config/QueueConfig").exists(): - for json_file in (self.app_path / "config/QueueConfig").glob("*.json"): - - queue_config = QueueConfig() - queue_config.load(json_file, queue_config) - queue_config.save() - - self.queue_dict[json_file.stem] = { - "Path": json_file, - "Config": queue_config, - } - - self.queue_dict = dict( - sorted(self.queue_dict.items(), key=lambda x: int(x[0][5:])) - ) - - logger.success( - f"调度队列配置搜索完成,共找到 {len(self.queue_dict)} 个调度队列", - module="配置管理", - ) - - def change_queue(self, old: str, new: str) -> None: - """ - 修改调度队列配置文件的队列参数 - - :param old: 旧脚本名 - :param new: 新脚本名 - """ - - logger.info(f"开始修改调度队列参数:{old} -> {new}", module="配置管理") - - for queue in self.queue_dict.values(): - - for i in range(10): - - if ( - queue["Config"].get( - queue["Config"].config_item_dict["Queue"][f"Script_{i}"] - ) - == old - ): - queue["Config"].set( - queue["Config"].config_item_dict["Queue"][f"Script_{i}"], new - ) - - logger.success(f"调度队列参数修改完成:{old} -> {new}", module="配置管理") - - def change_plan(self, old: str, new: str) -> None: - """ - 修改脚本管理所有下属用户的计划表配置参数 - - :param old: 旧计划表名 - :param new: 新计划表名 - """ - - logger.info(f"开始修改计划表参数:{old} -> {new}", module="配置管理") - - for script in self.script_dict.values(): - - for user in script["UserData"].values(): - - if user["Config"].get(user["Config"].Info_StageMode) == old: - user["Config"].set(user["Config"].Info_StageMode, new) - - logger.success(f"计划表参数修改完成:{old} -> {new}", module="配置管理") - - def change_maa_user_info( - self, name: str, user_data: Dict[str, Dict[str, Union[str, Path, dict]]] - ) -> None: - """ - 保存代理完成后发生改动的用户信息 - - :param name: 脚本实例名称 - :type name: str - :param user_data: 用户信息字典,包含用户名称和对应的配置信息 - :type user_data: Dict[str, Dict[str, Union[str, Path, dict]]] - """ - - logger.info(f"开始保存 MAA 脚本实例 {name} 的用户信息变动", module="配置管理") - - for user, info in user_data.items(): - - user_config = self.script_dict[name]["UserData"][user]["Config"] - - user_config.set( - user_config.Info_RemainedDay, info["Config"]["Info"]["RemainedDay"] - ) - user_config.set( - user_config.Data_LastProxyDate, info["Config"]["Data"]["LastProxyDate"] - ) - user_config.set( - user_config.Data_LastAnnihilationDate, - info["Config"]["Data"]["LastAnnihilationDate"], - ) - user_config.set( - user_config.Data_LastSklandDate, - info["Config"]["Data"]["LastSklandDate"], - ) - user_config.set( - user_config.Data_ProxyTimes, info["Config"]["Data"]["ProxyTimes"] - ) - user_config.set( - user_config.Data_IfPassCheck, info["Config"]["Data"]["IfPassCheck"] - ) - user_config.set( - user_config.Data_CustomInfrastPlanIndex, - info["Config"]["Data"]["CustomInfrastPlanIndex"], - ) - - self.sub_info_changed.emit() - - logger.success(f"MAA 脚本实例 {name} 的用户信息变动保存完成", module="配置管理") - - def change_general_sub_info( - self, name: str, sub_data: Dict[str, Dict[str, Union[str, Path, dict]]] - ) -> None: - """ - 保存代理完成后发生改动的配置信息 - - :param name: 脚本实例名称 - :type name: str - :param sub_data: 子配置信息字典,包含子配置名称和对应的配置信息 - :type sub_data: Dict[str, Dict[str, Union[str, Path, dict]]] - """ - - logger.info(f"开始保存通用脚本实例 {name} 的子配置信息变动", module="配置管理") - - for sub, info in sub_data.items(): - - sub_config = self.script_dict[name]["SubData"][sub]["Config"] - - sub_config.set( - sub_config.Info_RemainedDay, info["Config"]["Info"]["RemainedDay"] - ) - sub_config.set( - sub_config.Data_LastProxyDate, info["Config"]["Data"]["LastProxyDate"] - ) - sub_config.set( - sub_config.Data_ProxyTimes, info["Config"]["Data"]["ProxyTimes"] - ) - - self.sub_info_changed.emit() - - logger.success( - f"通用脚本实例 {name} 的子配置信息变动保存完成", module="配置管理" - ) - - def set_power_sign(self, sign: str) -> None: - """ - 设置当前电源状态 - - :param sign: 电源状态标志 - """ - - self.power_sign = sign - self.power_sign_changed.emit() - - logger.info(f"电源状态已更改为: {sign}", module="配置管理") - - def save_history(self, key: str, content: dict) -> None: - """ - 保存历史记录 - - :param key: 调度队列的键 - :type key: str - :param content: 包含时间和历史记录内容的字典 - :type content: dict - """ - - if key in self.queue_dict: - logger.info(f"保存调度队列 {key} 的历史记录", module="配置管理") - self.queue_dict[key]["Config"].set( - self.queue_dict[key]["Config"].Data_LastProxyTime, content["Time"] - ) - self.queue_dict[key]["Config"].set( - self.queue_dict[key]["Config"].Data_LastProxyHistory, content["History"] - ) - logger.success(f"调度队列 {key} 的历史记录已保存", module="配置管理") - else: - logger.warning(f"保存历史记录时未找到调度队列: {key}") - - def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool: - """ - 保存MAA日志并生成对应统计数据 - - :param log_path: 日志文件保存路径 - :type log_path: Path - :param logs: 日志内容列表 - :type logs: list - :param maa_result: MAA 结果 - :type maa_result: str - :return: 是否包含6★招募 - :rtype: bool - """ - - logger.info( - f"开始处理 MAA 日志,日志长度: {len(logs)},日志标记:{maa_result}", - module="配置管理", - ) - - data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = { - "recruit_statistics": defaultdict(int), - "drop_statistics": defaultdict(dict), - "maa_result": maa_result, - } - - if_six_star = False - - # 公招统计(仅统计招募到的) - confirmed_recruit = False - current_star_level = None - i = 0 - while i < len(logs): - if "公招识别结果:" in logs[i]: - current_star_level = None # 每次识别公招时清空之前的星级 - i += 1 - while i < len(logs) and "Tags" not in logs[i]: # 读取所有公招标签 - i += 1 - - if i < len(logs) and "Tags" in logs[i]: # 识别星级 - star_match = re.search(r"(\d+)\s*★ Tags", logs[i]) - if star_match: - current_star_level = f"{star_match.group(1)}★" - if current_star_level == "6★": - if_six_star = True - - if "已确认招募" in logs[i]: # 只有确认招募后才统计 - confirmed_recruit = True - - if confirmed_recruit and current_star_level: - data["recruit_statistics"][current_star_level] += 1 - confirmed_recruit = False # 重置,等待下一次公招 - current_star_level = None # 清空已处理的星级 - - i += 1 - - # 掉落统计 - # 存储所有关卡的掉落统计 - all_stage_drops = {} - - # 查找所有Fight任务的开始和结束位置 - fight_tasks = [] - for i, line in enumerate(logs): - if "开始任务: Fight" in line or "开始任务: 刷理智" in line: - # 查找对应的任务结束位置 - end_index = -1 - for j in range(i + 1, len(logs)): - if "完成任务: Fight" in logs[j] or "完成任务: 刷理智" in logs[j]: - end_index = j - break - # 如果遇到新的Fight任务开始,则当前任务没有正常结束 - if j < len(logs) and ( - "开始任务: Fight" in logs[j] or "开始任务: 刷理智" in logs[j] - ): - break - - # 如果找到了结束位置,记录这个任务的范围 - if end_index != -1: - fight_tasks.append((i, end_index)) - - # 处理每个Fight任务 - for start_idx, end_idx in fight_tasks: - # 提取当前任务的日志 - task_logs = logs[start_idx : end_idx + 1] - - # 查找任务中的最后一次掉落统计 - last_drop_stats = {} - current_stage = None - - for line in task_logs: - # 匹配掉落统计行,如"1-7 掉落统计:" - drop_match = re.search(r"([A-Za-z0-9\-]+) 掉落统计:", line) - if drop_match: - # 发现新的掉落统计,重置当前关卡的掉落数据 - current_stage = drop_match.group(1) - last_drop_stats = {} - continue - - # 如果已经找到了关卡,处理掉落物 - if current_stage: - item_match: List[str] = re.findall( - r"^(?!\[)(\S+?)\s*:\s*([\d,]+)(?:\s*\(\+[\d,]+\))?", - line, - re.M, - ) - for item, total in item_match: - # 解析数值时去掉逗号 (如 2,160 -> 2160) - total = int(total.replace(",", "")) - - # 黑名单 - if item not in [ - "当前次数", - "理智", - "最快截图耗时", - "专精等级", - "剩余时间", - ]: - last_drop_stats[item] = total - - # 如果任务中有掉落统计,更新总统计 - if current_stage and last_drop_stats: - if current_stage not in all_stage_drops: - all_stage_drops[current_stage] = {} - - # 累加掉落数据 - for item, count in last_drop_stats.items(): - all_stage_drops[current_stage].setdefault(item, 0) - all_stage_drops[current_stage][item] += count - - # 将累加后的掉落数据保存到结果中 - data["drop_statistics"] = all_stage_drops - - # 保存日志 - log_path.parent.mkdir(parents=True, exist_ok=True) - with log_path.open("w", encoding="utf-8") as f: - f.writelines(logs) - with log_path.with_suffix(".json").open("w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - logger.success(f"MAA 日志统计完成,日志路径:{log_path}", module="配置管理") - - return if_six_star - - def save_general_log(self, log_path: Path, logs: list, general_result: str) -> None: - """ - 保存通用日志并生成对应统计数据 - - :param log_path: 日志文件保存路径 - :param logs: 日志内容列表 - :param general_result: 待保存的日志结果信息 - """ - - logger.info( - f"开始处理通用日志,日志长度: {len(logs)},日志标记:{general_result}", - module="配置管理", - ) - - data: Dict[str, str] = {"general_result": general_result} - - # 保存日志 - log_path.parent.mkdir(parents=True, exist_ok=True) - with log_path.with_suffix(".log").open("w", encoding="utf-8") as f: - f.writelines(logs) - with log_path.with_suffix(".json").open("w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - logger.success( - f"通用日志统计完成,日志路径:{log_path.with_suffix('.log')}", - module="配置管理", - ) - - def merge_statistic_info(self, statistic_path_list: List[Path]) -> dict: - """ - 合并指定数据统计信息文件 - - :param statistic_path_list: 需要合并的统计信息文件路径列表 - :return: 合并后的统计信息字典 - """ - - logger.info( - f"开始合并统计信息文件,共计 {len(statistic_path_list)} 个文件", - module="配置管理", - ) - - data = {"index": {}} - - for json_file in statistic_path_list: - - with json_file.open("r", encoding="utf-8") as f: - single_data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = ( - json.load(f) - ) - - for key in single_data.keys(): - - if key not in data: - data[key] = {} - - # 合并公招统计 - if key == "recruit_statistics": - - for star_level, count in single_data[key].items(): - if star_level not in data[key]: - data[key][star_level] = 0 - data[key][star_level] += count - - # 合并掉落统计 - elif key == "drop_statistics": - - for stage, drops in single_data[key].items(): - if stage not in data[key]: - data[key][stage] = {} # 初始化关卡 - - for item, count in drops.items(): - - if item not in data[key][stage]: - data[key][stage][item] = 0 - data[key][stage][item] += count - - # 录入运行结果 - elif key in ["maa_result", "general_result"]: - - actual_date = datetime.strptime( - f"{json_file.parent.parent.name} {json_file.stem}", - "%Y-%m-%d %H-%M-%S", - ) + timedelta( - days=( - 1 - if datetime.strptime(json_file.stem, "%H-%M-%S").time() - < datetime.min.time().replace(hour=4) - else 0 - ) - ) - - if single_data[key] != "Success!": - if "error_info" not in data: - data["error_info"] = {} - data["error_info"][actual_date.strftime("%d日 %H:%M:%S")] = ( - single_data[key] - ) - - data["index"][actual_date] = [ - actual_date.strftime("%d日 %H:%M:%S"), - ("完成" if single_data[key] == "Success!" else "异常"), - json_file, - ] - - data["index"] = [data["index"][_] for _ in sorted(data["index"])] - - logger.success( - f"统计信息合并完成,共计 {len(data['index'])} 条记录", module="配置管理" - ) - - return {k: v for k, v in data.items() if v} - - def search_history( - self, mode: str, start_date: datetime, end_date: datetime - ) -> dict: - """ - 搜索指定范围内的历史记录 - - :param mode: 合并模式(按日合并、按周合并、按月合并) - :param start_date: 开始日期 - :param end_date: 结束日期 - :return: 搜索到的历史记录字典 - """ - - logger.info( - f"开始搜索历史记录,合并模式:{mode},日期范围:{start_date} 至 {end_date}", - module="配置管理", - ) - - history_dict = {} - - for date_folder in (Config.app_path / "history").iterdir(): - if not date_folder.is_dir(): - continue # 只处理日期文件夹 - - try: - - date = datetime.strptime(date_folder.name, "%Y-%m-%d") - - if not (start_date <= date <= end_date): - continue # 只统计在范围内的日期 - - if mode == "按日合并": - date_name = date.strftime("%Y年 %m月 %d日") - elif mode == "按周合并": - year, week, _ = date.isocalendar() - date_name = f"{year}年 第{week}周" - elif mode == "按月合并": - date_name = date.strftime("%Y年 %m月") - - if date_name not in history_dict: - history_dict[date_name] = {} - - for user_folder in date_folder.iterdir(): - if not user_folder.is_dir(): - continue # 只处理用户文件夹 - - if user_folder.stem not in history_dict[date_name]: - history_dict[date_name][user_folder.stem] = list( - user_folder.with_suffix("").glob("*.json") - ) - else: - history_dict[date_name][user_folder.stem] += list( - user_folder.with_suffix("").glob("*.json") - ) - - except ValueError: - logger.warning(f"非日期格式的目录: {date_folder}") - - logger.success( - f"历史记录搜索完成,共计 {len(history_dict)} 条记录", module="配置管理" - ) - - return { - k: v - for k, v in sorted(history_dict.items(), key=lambda x: x[0], reverse=True) - } - - def clean_old_history(self): - """删除超过用户设定天数的历史记录文件(基于目录日期)""" - - if self.get(self.function_HistoryRetentionTime) == 0: - logger.info("历史记录永久保留,跳过历史记录清理", module="配置管理") - return - - logger.info("开始清理超过设定天数的历史记录", module="配置管理") - - deleted_count = 0 - - for date_folder in (self.app_path / "history").iterdir(): - if not date_folder.is_dir(): - continue # 只处理日期文件夹 - - try: - # 只检查 `YYYY-MM-DD` 格式的文件夹 - folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d") - if datetime.now() - folder_date > timedelta( - days=self.get(self.function_HistoryRetentionTime) - ): - shutil.rmtree(date_folder, ignore_errors=True) - deleted_count += 1 - logger.info(f"已删除超期日志目录: {date_folder}", module="配置管理") - except ValueError: - logger.warning(f"非日期格式的目录: {date_folder}", module="配置管理") - - logger.success(f"清理完成: {deleted_count} 个日期目录", module="配置管理") - - -Config = AppConfig() diff --git a/app/core/logger.py b/app/core/logger.py deleted file mode 100644 index aa942dd..0000000 --- a/app/core/logger.py +++ /dev/null @@ -1,34 +0,0 @@ -# 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 . - -# 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) diff --git a/app/core/main_info_bar.py b/app/core/main_info_bar.py deleted file mode 100644 index 745050a..0000000 --- a/app/core/main_info_bar.py +++ /dev/null @@ -1,109 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA信息通知栏 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtCore import Qt -from qfluentwidgets import InfoBar, InfoBarPosition - -from .logger import logger -from .config import Config -from .sound_player import SoundPlayer - - -class _MainInfoBar: - """信息通知栏""" - - # 模式到 InfoBar 方法的映射 - mode_mapping = { - "success": InfoBar.success, - "warning": InfoBar.warning, - "error": InfoBar.error, - "info": InfoBar.info, - } - - 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("信息通知栏未设置父窗口", module="吐司通知栏") - return None - - # 根据 mode 获取对应的 InfoBar 方法 - info_bar_method = self.mode_mapping.get(mode) - - if not info_bar_method: - logger.error(f"未知的通知栏模式: {mode}", module="吐司通知栏") - return None - - if Config.main_window.isVisible(): - # 主窗口可见时直接推送通知 - info_bar_method( - title=title, - content=content, - orient=Qt.Horizontal, - isClosable=True, - position=InfoBarPosition.TOP_RIGHT, - duration=time, - parent=Config.main_window, - ) - - elif if_force: - # 如果主窗口不可见且强制推送,则录入消息队列等待窗口显示后推送 - info_bar_item = { - "mode": mode, - "title": title, - "content": content, - "time": time, - } - 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": - SoundPlayer.play("发生错误") - - -MainInfoBar = _MainInfoBar() diff --git a/app/core/network.py b/app/core/network.py deleted file mode 100644 index 9d63fe2..0000000 --- a/app/core/network.py +++ /dev/null @@ -1,308 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA网络请求线程 -v4.4 -作者:DLmaster_361 -""" - -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): - """网络请求线程类""" - - max_retries = 3 - timeout = 10 - backoff_factor = 0.1 - - 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 - - self.proxies = { - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - } - - self.status_code = None - self.response_json = None - self.error_message = None - - self.loop = QEventLoop() - - truststore.inject_into_ssl() # 信任系统证书 - - @logger.catch - def run(self) -> None: - """运行网络请求线程""" - - if self.mode == "get": - 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数据 - - :param url: 请求的URL - """ - - logger.info(f"子线程 {self.objectName()} 开始网络请求", module="网络请求子线程") - - response = None - - for _ in range(self.max_retries): - try: - response = requests.get(url, timeout=self.timeout, proxies=self.proxies) - self.status_code = response.status_code - self.response_json = response.json() - 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() - - def get_file(self, url: str, path: Path) -> None: - """ - 通过get方法下载文件到指定路径 - - :param url: 请求的URL - :param path: 下载文件的保存路径 - """ - - logger.info(f"子线程 {self.objectName()} 开始下载文件", module="网络请求子线程") - - response = None - - try: - 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 = 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, - files: Dict = None, - data: Dict = None, - ) -> NetworkThread: - """ - 添加网络请求任务 - - :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) - - network_thread.start() - - 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, - "response_json": network_thread.response_json, - "error_message": ( - re.sub(r"(&cdk=)[^&]+(&)", r"\1******\2", network_thread.error_message) - if network_thread.error_message - else None - ), - } - - network_thread.quit() - network_thread.wait() - self.task_queue.remove(network_thread) - network_thread.deleteLater() - - logger.info( - f"网络请求结果: {result['status_code']},请求子线程已结束", - module="网络请求", - ) - - return result - - -Network = _Network() diff --git a/app/core/sound_player.py b/app/core/sound_player.py deleted file mode 100644 index e8f5268..0000000 --- a/app/core/sound_player.py +++ /dev/null @@ -1,79 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA音效播放器 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtCore import QObject, QUrl -from PySide6.QtMultimedia import QSoundEffect -from pathlib import Path - - -from .logger import logger -from .config import Config - - -class _SoundPlayer(QObject): - - def __init__(self): - super().__init__() - - 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 - - if (self.sounds_path / f"both/{sound_name}.wav").exists(): - - self.play_voice(self.sounds_path / f"both/{sound_name}.wav") - - elif ( - self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav" - ).exists(): - - self.play_voice( - self.sounds_path / Config.get(Config.voice_Type) / f"{sound_name}.wav" - ) - - def play_voice(self, sound_path: Path): - """ - 播放音效文件 - - :param sound_path: 音效文件的完整路径 - """ - - effect = QSoundEffect(self) - effect.setVolume(1) - effect.setSource(QUrl.fromLocalFile(sound_path)) - effect.play() - - -SoundPlayer = _SoundPlayer() diff --git a/app/core/task_manager.py b/app/core/task_manager.py deleted file mode 100644 index c044767..0000000 --- a/app/core/task_manager.py +++ /dev/null @@ -1,460 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA业务调度器 -v4.4 -作者:DLmaster_361 -""" - -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 - - -class Task(QThread): - """业务线程""" - - check_maa_version = Signal(str) - push_info_bar = Signal(str, str, str, int) - play_sound = Signal(str) - question = Signal(str, str) - question_response = Signal(bool) - update_maa_user_info = Signal(str, dict) - update_general_sub_info = Signal(str, dict) - create_task_list = Signal(list) - create_user_list = Signal(list) - update_task_list = Signal(list) - update_user_list = Signal(list) - update_log_text = Signal(str) - accomplish = Signal(list) - - def __init__( - self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]] - ): - super(Task, self).__init__() - - self.setObjectName(f"Task-{mode}-{name}") - - self.mode = mode - self.name = name - self.info = info - - self.logs = [] - - self.question_response.connect(lambda: print("response")) - - @logger.catch - def run(self): - - if "设置MAA" in self.mode: - - 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.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) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.accomplish.connect(lambda: self.accomplish.emit([])) - - try: - self.task.run() - except Exception as 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}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "设置通用脚本", self.name, 3000) - - self.task = GeneralManager( - self.mode, - Config.script_dict[self.name], - self.info["SetSubInfo"]["Path"], - ) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.accomplish.connect(lambda: self.accomplish.emit([])) - - try: - self.task.run() - except Exception as 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.script_dict[value]["Config"].get_name() == "" - else f"{value} - {Config.script_dict[value]["Config"].get_name()}" - ), - "等待", - value, - ] - for _, value in sorted( - self.info["Queue"].items(), key=lambda x: int(x[0][7:]) - ) - if value != "禁用" - ] - - self.create_task_list.emit(self.task_list) - - for task in self.task_list: - - if self.isInterruptionRequested(): - break - - 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]},该任务已在运行列表中", - 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]}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "任务开始", task[0], 3000) - - if Config.script_dict[task[2]]["Type"] == "Maa": - - self.task = MaaManager( - self.mode[0:4], - Config.script_dict[task[2]], - ) - - self.task.check_maa_version.connect(self.check_maa_version.emit) - self.task.question.connect(self.question.emit) - self.question_response.disconnect() - self.question_response.connect(self.task.question_response.emit) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.create_user_list.connect(self.create_user_list.emit) - self.task.update_user_list.connect(self.update_user_list.emit) - self.task.update_log_text.connect(self.update_log_text.emit) - self.task.update_user_info.connect(self.update_maa_user_info.emit) - self.task.accomplish.connect( - lambda log: self.task_accomplish(task[2], log) - ) - - elif Config.script_dict[task[2]]["Type"] == "General": - - self.task = GeneralManager( - self.mode[0:4], - Config.script_dict[task[2]], - ) - - self.task.question.connect(self.question.emit) - self.question_response.disconnect() - self.question_response.connect(self.task.question_response.emit) - self.task.push_info_bar.connect(self.push_info_bar.emit) - self.task.play_sound.connect(self.play_sound.emit) - self.task.create_user_list.connect(self.create_user_list.emit) - self.task.update_user_list.connect(self.update_user_list.emit) - self.task.update_log_text.connect(self.update_log_text.emit) - self.task.update_sub_info.connect(self.update_general_sub_info.emit) - self.task.accomplish.connect( - lambda log: self.task_accomplish(task[2], log) - ) - - try: - self.task.run() # 运行任务业务 - - task[1] = "完成" - self.update_task_list.emit(self.task_list) - logger.info(f"任务完成:{task[0]}", module=f"业务 {self.name}") - self.push_info_bar.emit("info", "任务完成", task[0], 3000) - - except Exception as e: - - self.task_accomplish( - task[2], - { - "Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "History": f"任务异常,异常简报:{e}", - }, - ) - - task[1] = "异常" - self.update_task_list.emit(self.task_list) - 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() - - -class _TaskManager(QObject): - """业务调度器""" - - create_gui = Signal(Task) - connect_gui = Signal(Task) - - def __init__(self): - super(_TaskManager, self).__init__() - - self.task_dict: Dict[str, Task] = {} - - 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: - - logger.warning(f"任务已存在:{name}") - MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000) - return None - - 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( - lambda title, content: self.push_dialog(name, title, content) - ) - self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar) - self.task_dict[name].play_sound.connect(SoundPlayer.play) - self.task_dict[name].update_maa_user_info.connect(Config.change_maa_user_info) - self.task_dict[name].update_general_sub_info.connect( - Config.change_general_sub_info - ) - self.task_dict[name].accomplish.connect( - 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) -> None: - """ - 中止任务 - - :param name: 任务名称 - """ - - logger.info(f"中止任务:{name}", module="业务调度") - MainInfoBar.push_info_bar("info", "中止任务", name, 3000) - - if name == "ALL": - - for name in self.task_dict: - - self.task_dict[name].task.requestInterruption() - self.task_dict[name].requestInterruption() - self.task_dict[name].quit() - self.task_dict[name].wait() - - elif name in self.task_dict: - - self.task_dict[name].task.requestInterruption() - self.task_dict[name].requestInterruption() - self.task_dict[name].quit() - self.task_dict[name].wait() - - def remove_task(self, mode: str, name: str, logs: list) -> None: - """ - 处理任务结束后的收尾工作 - - :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 = "" - for log in logs: - history += f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n" - Config.save_history(name, {"Time": time, "History": history}) - else: - Config.save_history( - name, - { - "Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "History": "没有任务被执行", - }, - ) - - # 根据调度队列情况设置电源状态 - if ( - Config.queue_dict[name]["Config"].get( - 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 - ) - ) - - if Config.args.mode == "cli" and Config.power_sign == "NoAction": - Config.set_power_sign("KillSelf") - - 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", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - maa_info = network_result["response_json"] - else: - logger.warning( - f"获取MAA版本信息时出错:{network_result['error_message']}", - module="业务调度", - ) - MainInfoBar.push_info_bar( - "warning", - "获取MAA版本信息时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - if version.parse(maa_info["data"]["version_name"]) > version.parse(v): - - logger.info( - f"检测到MAA版本过低:{v},最新版本:{maa_info['data']['version_name']}", - module="业务调度", - ) - MainInfoBar.push_info_bar( - "info", - "MAA版本过低", - f"当前版本:{v},最新稳定版:{maa_info['data']['version_name']}", - -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("是") - choice.cancelButton.setText("否") - - self.task_dict[name].question_response.emit(bool(choice.exec())) - - -TaskManager = _TaskManager() diff --git a/app/core/timer.py b/app/core/timer.py deleted file mode 100644 index 66d8dee..0000000 --- a/app/core/timer.py +++ /dev/null @@ -1,175 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA主业务定时器 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtCore import QObject, QTimer -from datetime import datetime -import keyboard - -from .logger import logger -from .config import Config -from .task_manager import TaskManager -from app.services import System - - -class _MainTimer(QObject): - - def __init__(self, parent=None): - super().__init__(parent) - - self.Timer = QTimer() - self.Timer.timeout.connect(self.timed_start) - self.Timer.timeout.connect(self.set_silence) - self.Timer.timeout.connect(self.check_power) - - 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): - Config.main_window.setting.check_update() - - def timed_start(self): - """定时启动代理任务""" - - for name, info in Config.queue_dict.items(): - - if not info["Config"].get(info["Config"].QueueSet_TimeEnabled): - continue - - data = info["Config"].toDict() - - time_set = [ - data["Time"][f"Set_{_}"] - for _ in range(10) - if data["Time"][f"Enabled_{_}"] - ] - # 按时间调起代理任务 - curtime = datetime.now().strftime("%Y-%m-%d %H:%M") - if ( - curtime[11:16] in time_set - and curtime - != info["Config"].get(info["Config"].Data_LastProxyTime)[:16] - and name not in Config.running_list - ): - - logger.info(f"定时唤起任务:{name}。", module="主业务定时器") - TaskManager.add_task("自动代理_新调度台", name, data) - - def set_silence(self): - """设置静默模式""" - - if ( - not Config.if_ignore_silence - and Config.get(Config.function_IfSilence) - and Config.get(Config.function_BossKey) != "" - ): - - windows = System.get_window_info() - - 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( - _.strip().lower() - for _ in Config.get(Config.function_BossKey).split("+") - ) - ) - logger.info( - f"模拟按键:{Config.get(Config.function_BossKey)}", - module="主业务定时器", - ) - except Exception as 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 = { - "KillSelf": "退出软件", - "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") - - -MainTimer = _MainTimer() diff --git a/app/core/user_config.py b/app/core/user_config.py new file mode 100644 index 0000000..4ec51cb --- /dev/null +++ b/app/core/user_config.py @@ -0,0 +1,358 @@ +import json +import secrets +import string +import asyncio +import aiofiles +from pathlib import Path +from typing import Any, TypeVar, Generic, cast +from pydantic import BaseModel + +from app.utils.logger import get_logger + +T = TypeVar('T', bound=BaseModel) + +class ConfigManager(Generic[T]): + """ + 异步配置管理基类,支持自动保存和Pydantic数据验证 + 子类需定义具体的配置模型类型 + """ + + def __init__(self, file_path: str, log_name: str, model_type: type[T]): + """ + 初始化配置管理器 + + Args: + file_path: 配置文件路径 + log_name: 日志名称 + model_type: 配置项的Pydantic模型类型 + """ + self.file_path = Path(file_path) + self.logger = get_logger(log_name) + self.model_type = model_type + self.data: dict[str, Any] = { + "instance_order": [], + "instances": {} + } + self._lock = asyncio.Lock() + self._save_task: asyncio.Task|None = None + self._pending_save = False + self._load_task = asyncio.create_task(self._load_async()) + + async def _load_async(self) -> None: + """异步加载配置文件 - 带健壮的错误处理""" + async with self._lock: + try: + # 检查文件是否存在 + if not self.file_path.exists(): + self.logger.info(f"配置文件 {self.file_path} 不存在,创建新配置") + self.file_path.parent.mkdir(parents=True, exist_ok=True) + # 初始化空配置 + self.data = { + "instance_order": [], + "instances": {} + } + return + + # 检查文件是否为空 + if self.file_path.stat().st_size == 0: + self.logger.warning(f"配置文件 {self.file_path} 为空,初始化新配置") + self.data = { + "instance_order": [], + "instances": {} + } + return + + # 读取并解析配置 + async with aiofiles.open(self.file_path, 'r', encoding='utf-8') as f: + content = await f.read() + + # 尝试解析JSON + try: + raw_data = json.loads(content) + except json.JSONDecodeError as e: + self.logger.error(f"配置文件 {self.file_path} JSON解析失败: {e}") + # 尝试备份损坏的配置文件 + await self._backup_corrupted_config() + # 初始化空配置 + self.data = { + "instance_order": [], + "instances": {} + } + return + + # 验证并加载实例 + instance_order = raw_data.get("instance_order", []) + instances_raw = raw_data.get("instances", {}) + + instances = {} + for uid, config_data in instances_raw.items(): + try: + # 使用Pydantic验证配置数据 + instances[uid] = self.model_type(**config_data) + except Exception as e: + self.logger.error(f"配置项 {uid} 验证失败: {e}") + # 不中断整个加载过程,跳过无效配置 + continue + + # 确保instance_order与现有实例匹配 + valid_order = [uid for uid in instance_order if uid in instances] + self.data = { + "instance_order": valid_order, + "instances": instances + } + self.logger.info(f"成功加载 {len(instances)} 个配置实例") + + except Exception as e: + self.logger.error(f"配置加载失败: {e}", exc_info=True) + # 初始化空配置作为安全措施 + self.data = { + "instance_order": [], + "instances": {} + } + # 尝试备份损坏的配置文件 + await self._backup_corrupted_config() + + async def _backup_corrupted_config(self) -> None: + """备份损坏的配置文件""" + try: + backup_path = self.file_path.with_suffix(f"{self.file_path.suffix}.bak") + counter = 1 + while backup_path.exists(): + backup_path = self.file_path.with_suffix(f"{self.file_path.suffix}.bak{counter}") + counter += 1 + + if self.file_path.exists(): + async with aiofiles.open(self.file_path, 'rb') as src, \ + aiofiles.open(backup_path, 'wb') as dst: + content = await src.read() + await dst.write(content) + + self.logger.warning(f"已备份损坏的配置文件到: {backup_path}") + except Exception as e: + self.logger.error(f"备份损坏配置失败: {e}") + + async def _save_async(self) -> None: + """异步保存配置到文件""" + async with self._lock: + try: + serializable = { + "instance_order": self.data["instance_order"], + "instances": { + uid: instance.model_dump(mode='json') + for uid, instance in self.data["instances"].items() + } + } + + # 确保目录存在 + self.file_path.parent.mkdir(parents=True, exist_ok=True) + + async with aiofiles.open(self.file_path, 'w', encoding='utf-8') as f: + await f.write(json.dumps(serializable, indent=2, ensure_ascii=False)) + + self.logger.debug(f"配置已异步保存到: {self.file_path}") + except Exception as e: + self.logger.error(f"配置保存失败: {e}", exc_info=True) + raise + finally: + self._save_task = None + self._pending_save = False + + def _schedule_save(self) -> None: + """ + 调度配置保存(避免频繁保存) + 使用防抖技术,确保短时间内多次修改只保存一次 + """ + if self._save_task and not self._save_task.done(): + # 已有保存任务在运行,标记需要再次保存 + self._pending_save = True + return + + async def save_with_debounce(): + # 等待短暂时间,合并多次修改 + await asyncio.sleep(0.1) + + # 如果有新的保存请求,递归处理 + if self._pending_save: + self._pending_save = False + await save_with_debounce() + return + + await self._save_async() + + self._save_task = asyncio.create_task(save_with_debounce()) + + @staticmethod + def generate_uid(length: int = 8) -> str: + """生成8位随机UID""" + alphabet = string.ascii_letters + string.digits + return ''.join(secrets.choice(alphabet) for _ in range(length)) + + async def create(self, **kwargs) -> str: + """创建新的配置实例""" + async with self._lock: + # 确保配置已加载完成 + if not self._load_task.done(): + await self._load_task + + # 生成唯一UID + uid = self.generate_uid() + while uid in self.data["instances"]: + uid = self.generate_uid() + + try: + # 使用Pydantic模型验证数据 + new_config = self.model_type(**kwargs) + except Exception as e: + self.logger.error(f"无效的配置数据: {e}") + raise + + self.data["instances"][uid] = new_config + self.data["instance_order"].append(uid) + self._schedule_save() + self.logger.info(f"创建新的配置实例: {uid}") + return uid + + # 实现所需魔法方法(同步方法) + def __getitem__(self, uid: str) -> T: + """获取配置项(同步)""" + # 确保配置已加载完成 + if not self._load_task.done(): + raise RuntimeError("配置尚未加载完成,请等待初始化完成") + + return cast(T, self.data["instances"][uid]) + + def __setitem__(self, uid: str, value: T | dict[str, Any]) -> None: + """ + 设置配置项(同步) + 注意:此方法是同步的,但会触发异步保存 + + 支持两种用法: + 1. config[uid] = config_model_instance + 2. config[uid] = {"name": "value", ...} # 字典形式 + """ + # 确保配置已加载完成 + if not self._load_task.done(): + raise RuntimeError("配置尚未加载完成,请等待初始化完成") + + # 如果传入的是字典,转换为模型实例 + if isinstance(value, dict): + try: + value = self.model_type(**value) + except Exception as e: + self.logger.error(f"配置数据转换失败: {e}") + raise ValueError("无效的配置数据") from e + + if not isinstance(value, self.model_type): + raise TypeError(f"值必须是 {self.model_type.__name__} 类型或字典") + + # 更新内存数据 + if uid not in self.data["instances"]: + self.data["instance_order"].append(uid) + + self.data["instances"][uid] = value + self._schedule_save() + + def __delitem__(self, uid: str) -> None: + """删除配置项(同步)""" + # 确保配置已加载完成 + if not self._load_task.done(): + raise RuntimeError("配置尚未加载完成,请等待初始化完成") + + if uid in self.data["instances"]: + del self.data["instances"][uid] + if uid in self.data["instance_order"]: + self.data["instance_order"].remove(uid) + self._schedule_save() + else: + raise KeyError(uid) + + def __contains__(self, uid: str) -> bool: + """检查UID是否存在(同步)""" + # 确保配置已加载完成 + if not self._load_task.done(): + return False # 配置未加载完成时,认为不存在 + + return uid in self.data["instances"] + + def __len__(self) -> int: + """返回配置实例数量(同步)""" + # 确保配置已加载完成 + if not self._load_task.done(): + return 0 + + return len(self.data["instance_order"]) + + def get_instance_order(self) -> list[str]: + """获取实例顺序列表(同步)""" + # 确保配置已加载完成 + if not self._load_task.done(): + return [] + + return self.data["instance_order"].copy() + + def get_all_instances(self) -> dict[str, T]: + """获取所有配置实例(同步)""" + # 确保配置已加载完成 + if not self._load_task.done(): + return {} + + return cast(dict[str, T], self.data["instances"].copy()) + + async def wait_until_ready(self) -> None: + """等待配置加载完成""" + await self._load_task + + async def save_now(self) -> None: + """立即保存配置(等待保存完成)""" + if self._save_task: + await self._save_task + else: + await self._save_async() + + def is_ready(self) -> bool: + """检查配置是否已加载完成""" + return self._load_task.done() and not self._load_task.cancelled() + + + +''' +初始化 +ConfigManager(file_path: str, log_name: str, model_type: type[T]) + file_path: 配置文件路径 + log_name: 日志记录器名称 + model_type: 配置模型类型(继承自 Pydantic BaseModel) +主要方法 + create(**kwargs) -> str + 异步创建新的配置实例,返回唯一标识符(UID) + + wait_until_ready() -> None + 异步等待配置加载完成 + + save_now() -> None + 立即保存配置到文件 + + is_ready() -> bool + 检查配置是否已加载完成 + + get_instance_order() -> list[str] + 获取配置实例的顺序列表 + + get_all_instances() -> dict[str, T] + 获取所有配置实例 + +魔法方法 + getitem(uid: str) -> T + 通过 UID 获取配置实例 + + setitem(uid: str, value: T | dict[str, Any]) -> None + 通过 UID 设置配置实例 + + delitem(uid: str) -> None + 通过 UID 删除配置实例 + + contains(uid: str) -> bool + 检查是否存在指定 UID 的配置实例 + + len() -> int + 获取配置实例数量 +''' \ No newline at end of file diff --git a/app/models/MAA.py b/app/models/MAA.py deleted file mode 100644 index 3df88d5..0000000 --- a/app/models/MAA.py +++ /dev/null @@ -1,2161 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -MAA功能组件 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtCore import QObject, Signal, QEventLoop, QFileSystemWatcher, QTimer -import json -import subprocess -import shutil -import re -import win32com.client -from functools import partial -from datetime import datetime, timedelta -from pathlib import Path -from jinja2 import Environment, FileSystemLoader -from typing import Union, List, Dict - -from app.core import Config, MaaConfig, MaaUserConfig, logger -from app.services import Notify, Crypto, System, skland_sign_in -from app.utils import ProcessManager - - -class MaaManager(QObject): - """MAA控制器""" - - check_maa_version = Signal(str) - question = Signal(str, str) - question_response = Signal(bool) - update_user_info = Signal(str, dict) - push_info_bar = Signal(str, str, str, int) - play_sound = Signal(str) - create_user_list = Signal(list) - update_user_list = Signal(list) - update_log_text = Signal(str) - interrupt = Signal() - accomplish = Signal(dict) - - def __init__( - self, - mode: str, - config: Dict[ - str, - Union[ - str, - Path, - MaaConfig, - Dict[str, Dict[str, Union[Path, MaaUserConfig]]], - ], - ], - user_config_path: Path = None, - ): - super(MaaManager, self).__init__() - - self.user_list = "" - self.mode = mode - self.config_path = config["Path"] - self.name = config["Config"].get(config["Config"].MaaSet_Name) - self.user_config_path = user_config_path - - self.emulator_process_manager = ProcessManager() - self.maa_process_manager = ProcessManager() - - self.log_monitor = QFileSystemWatcher() - self.log_monitor.fileChanged.connect(self.check_maa_log) - self.log_monitor_timer = QTimer() - self.log_monitor_timer.timeout.connect(self.refresh_maa_log) - self.monitor_loop = QEventLoop() - self.log_start_time = datetime.now() - self.log_check_mode = None - self.maa_logs = [] - self.maa_result = "Wait" - - self.maa_process_manager.processClosed.connect(self.check_maa_log) - - self.question_loop = QEventLoop() - self.question_response.connect(self.__capture_response) - self.question_response.connect(self.question_loop.quit) - - self.wait_loop = QEventLoop() - - self.isInterruptionRequested = False - self.interrupt.connect(self.quit_monitor) - - self.maa_version = None - self.maa_update_package = "" - self.task_dict = {} - self.set = config["Config"].toDict() - - self.data = {} - if "设置MAA" not in self.mode: - for name, info in config["UserData"].items(): - self.data[name] = { - "Path": info["Path"], - "Config": info["Config"].toDict(), - } - planed_info = info["Config"].get_plan_info() - for key, value in planed_info.items(): - self.data[name]["Config"]["Info"][key] = value - - self.data = dict(sorted(self.data.items(), key=lambda x: int(x[0][3:]))) - - logger.success( - f"MAA控制器初始化完成,当前模式: {self.mode}", - module=f"MAA调度器-{self.name}", - ) - - def configure(self): - """提取配置信息""" - - self.maa_root_path = Path(self.set["MaaSet"]["Path"]) - self.maa_set_path = self.maa_root_path / "config/gui.json" - self.maa_log_path = self.maa_root_path / "debug/gui.log" - self.maa_exe_path = self.maa_root_path / "MAA.exe" - self.maa_tasks_path = self.maa_root_path / "resource/tasks/tasks.json" - self.port_range = [0] + [ - (i // 2 + 1) * (-1 if i % 2 else 1) - for i in range(0, 2 * self.set["RunSet"]["ADBSearchRange"]) - ] - - logger.success("MAA配置提取完成", module=f"MAA调度器-{self.name}") - - def run(self): - """主进程,运行MAA代理进程""" - - current_date = datetime.now().strftime("%m-%d") - curdate = Config.server_date().strftime("%Y-%m-%d") - begin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - - self.configure() - # 检查MAA路径是否可用 - if not self.maa_exe_path.exists() or not self.maa_set_path.exists(): - - logger.error( - "未正确配置MAA路径,MAA代理进程中止", module=f"MAA调度器-{self.name}" - ) - self.push_info_bar.emit( - "error", "启动MAA代理进程失败", "您还未正确配置MAA路径!", -1 - ) - self.accomplish.emit( - { - "Time": begin_time, - "History": "由于未正确配置MAA路径,MAA代理进程中止", - } - ) - return None - - # 记录 MAA 配置文件 - logger.info( - f"记录 MAA 配置文件:{self.maa_set_path}", - module=f"MAA调度器-{self.name}", - ) - (self.config_path / "Temp").mkdir(parents=True, exist_ok=True) - if self.maa_set_path.exists(): - shutil.copy(self.maa_set_path, self.config_path / "Temp/gui.json") - - # 整理用户数据,筛选需代理的用户 - if "设置MAA" not in self.mode: - - self.data = dict( - sorted( - self.data.items(), - key=lambda x: (x[1]["Config"]["Info"]["Mode"], int(x[0][3:])), - ) - ) - self.user_list: List[List[str, str, str]] = [ - [_["Config"]["Info"]["Name"], "等待", index] - for index, _ in self.data.items() - if ( - _["Config"]["Info"]["RemainedDay"] != 0 - and _["Config"]["Info"]["Status"] - ) - ] - self.create_user_list.emit(self.user_list) - - logger.info( - f"用户列表创建完成,已筛选用户数:{len(self.user_list)}", - module=f"MAA调度器-{self.name}", - ) - - # 自动代理模式 - if self.mode == "自动代理": - - # 标记是否需要重启模拟器 - self.if_open_emulator = True - # 执行情况预处理 - for _ in self.user_list: - if self.data[_[2]]["Config"]["Data"]["LastProxyDate"] != curdate: - self.data[_[2]]["Config"]["Data"]["LastProxyDate"] = curdate - self.data[_[2]]["Config"]["Data"]["ProxyTimes"] = 0 - _[ - 0 - ] += f" - 第{self.data[_[2]]["Config"]["Data"]["ProxyTimes"] + 1}次代理" - - # 开始代理 - for user in self.user_list: - - user_data = self.data[user[2]]["Config"] - - if self.isInterruptionRequested: - break - - if ( - self.set["RunSet"]["ProxyTimesLimit"] == 0 - or user_data["Data"]["ProxyTimes"] - < self.set["RunSet"]["ProxyTimesLimit"] - ): - user[1] = "运行" - self.update_user_list.emit(self.user_list) - else: - user[1] = "跳过" - self.update_user_list.emit(self.user_list) - continue - - logger.info(f"开始代理用户: {user[0]}", module=f"MAA调度器-{self.name}") - - # 简洁模式用户默认开启日常选项 - if user_data["Info"]["Mode"] == "简洁": - user_data["Info"]["Routine"] = True - # 详细模式用户首次代理需打开模拟器 - elif user_data["Info"]["Mode"] == "详细": - self.if_open_emulator = True - - # 初始化代理情况记录和模式替换表 - run_book = { - "Annihilation": bool(user_data["Info"]["Annihilation"] == "Close"), - "Routine": not user_data["Info"]["Routine"], - } - mode_book = { - "Annihilation": "自动代理_剿灭", - "Routine": "自动代理_日常", - } - - user_logs_list = [] - user_start_time = datetime.now() - - if user_data["Info"]["IfSkland"] and user_data["Info"]["SklandToken"]: - - if user_data["Data"]["LastSklandDate"] != datetime.now().strftime( - "%Y-%m-%d" - ): - - self.update_log_text.emit("正在执行森空岛签到中\n请稍候~") - - skland_result = skland_sign_in( - Crypto.win_decryptor(user_data["Info"]["SklandToken"]) - ) - - for type, user_list in skland_result.items(): - - if type != "总计" and len(user_list) > 0: - - logger.info( - f"用户: {user[0]} - 森空岛签到{type}: {'、'.join(user_list)}", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "info", - f"森空岛签到{type}", - "、".join(user_list), - -1 if type == "失败" else 5000, - ) - - if skland_result["总计"] == 0: - self.push_info_bar.emit( - "info", "森空岛签到失败", user[0], -1 - ) - - if ( - skland_result["总计"] > 0 - and len(skland_result["失败"]) == 0 - ): - user_data["Data"][ - "LastSklandDate" - ] = datetime.now().strftime("%Y-%m-%d") - logger.success( - f"用户: {user[0]} - 森空岛签到成功", - module=f"MAA调度器-{self.name}", - ) - self.play_sound.emit("森空岛签到成功") - else: - logger.warning( - f"用户: {user[0]} - 森空岛签到失败", - module=f"MAA调度器-{self.name}", - ) - self.play_sound.emit("森空岛签到失败") - - elif user_data["Info"]["IfSkland"]: - logger.warning( - f"用户: {user[0]} - 未配置森空岛签到Token,跳过森空岛签到", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "warning", "森空岛签到失败", "未配置鹰角网络通行证登录凭证", -1 - ) - - # 剿灭-日常模式循环 - for mode in ["Annihilation", "Routine"]: - - if self.isInterruptionRequested: - break - - if run_book[mode]: - continue - - # 剿灭模式;满足条件跳过剿灭 - if ( - mode == "Annihilation" - and self.set["RunSet"]["AnnihilationWeeklyLimit"] - and datetime.strptime( - user_data["Data"]["LastAnnihilationDate"], "%Y-%m-%d" - ).isocalendar()[:2] - == datetime.strptime(curdate, "%Y-%m-%d").isocalendar()[:2] - ): - logger.info( - f"用户: {user_data['Info']['Name']} - 本周剿灭模式已达上限,跳过执行剿灭任务", - module=f"MAA调度器-{self.name}", - ) - run_book[mode] = True - continue - else: - self.weekly_annihilation_limit_reached = False - - if ( - user_data["Info"]["Mode"] == "详细" - and not ( - self.data[user[2]]["Path"] / "Routine/gui.json" - ).exists() - ): - logger.error( - f"用户: {user[0]} - 未找到日常详细配置文件", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "启动MAA代理进程失败", - f"未找到{user[0]}的详细配置文件!", - -1, - ) - run_book[mode] = False - break - - # 更新当前模式到界面 - self.update_user_list.emit( - [ - ( - [f"{_[0]} - {mode_book[mode][5:7]}", _[1], _[2]] - if _[2] == user[2] - else _ - ) - for _ in self.user_list - ] - ) - - # 解析任务构成 - if mode == "Routine": - - self.task_dict = { - "WakeUp": str(user_data["Task"]["IfWakeUp"]), - "Recruiting": str(user_data["Task"]["IfRecruiting"]), - "Base": str(user_data["Task"]["IfBase"]), - "Combat": str(user_data["Task"]["IfCombat"]), - "Mission": str(user_data["Task"]["IfMission"]), - "Mall": str(user_data["Task"]["IfMall"]), - "AutoRoguelike": str(user_data["Task"]["IfAutoRoguelike"]), - "Reclamation": str(user_data["Task"]["IfReclamation"]), - } - - elif mode == "Annihilation": - - self.task_dict = { - "WakeUp": "True", - "Recruiting": "False", - "Base": "False", - "Combat": "True", - "Mission": "False", - "Mall": "False", - "AutoRoguelike": "False", - "Reclamation": "False", - } - - logger.info( - f"用户: {user[0]} - 模式: {mode_book[mode]} - 任务列表: {self.task_dict.values()}", - module=f"MAA调度器-{self.name}", - ) - - # 尝试次数循环 - for i in range(self.set["RunSet"]["RunTimesLimit"]): - - if self.isInterruptionRequested: - break - - if run_book[mode]: - break - - logger.info( - f"用户: {user[0]} - 模式: {mode_book[mode]} - 尝试次数: {i + 1}/{self.set["RunSet"]["RunTimesLimit"]}", - module=f"MAA调度器-{self.name}", - ) - - # 配置MAA - set = self.set_maa(mode_book[mode], user[2]) - # 记录当前时间 - self.log_start_time = datetime.now() - - # 记录模拟器与ADB路径 - self.emulator_path = Path( - set["Configurations"]["Default"]["Start.EmulatorPath"] - ) - self.emulator_arguments = set["Configurations"]["Default"][ - "Start.EmulatorAddCommand" - ].split() - # 如果是快捷方式,进行解析 - if ( - self.emulator_path.suffix == ".lnk" - and self.emulator_path.exists() - ): - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(str(self.emulator_path)) - self.emulator_path = Path(shortcut.TargetPath) - self.emulator_arguments = shortcut.Arguments.split() - except Exception as e: - logger.exception( - f"解析快捷方式时出现异常:{e}", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "解析快捷方式时出现异常", - "请检查快捷方式", - -1, - ) - self.if_open_emulator = True - break - elif not self.emulator_path.exists(): - logger.error( - f"模拟器快捷方式不存在:{self.emulator_path}", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "启动模拟器时出现异常", - "模拟器快捷方式不存在", - -1, - ) - self.if_open_emulator = True - break - - self.wait_time = int( - set["Configurations"]["Default"][ - "Start.EmulatorWaitSeconds" - ] - ) - - self.ADB_path = Path( - set["Configurations"]["Default"]["Connect.AdbPath"] - ) - self.ADB_path = ( - self.ADB_path - if self.ADB_path.is_absolute() - else self.maa_root_path / self.ADB_path - ) - self.ADB_address = set["Configurations"]["Default"][ - "Connect.Address" - ] - self.if_kill_emulator = bool( - set["Configurations"]["Default"]["MainFunction.PostActions"] - == "12" - ) - self.if_open_emulator_process = bool( - set["Configurations"]["Default"][ - "Start.OpenEmulatorAfterLaunch" - ] - == "True" - ) - - # 任务开始前释放ADB - try: - logger.info( - f"释放ADB:{self.ADB_address}", - module=f"MAA调度器-{self.name}", - ) - subprocess.run( - [self.ADB_path, "disconnect", self.ADB_address], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - except subprocess.CalledProcessError as e: - # 忽略错误,因为可能本来就没有连接 - logger.warning( - f"释放ADB时出现异常:{e}", - module=f"MAA调度器-{self.name}", - ) - except Exception as e: - logger.exception( - f"释放ADB时出现异常:{e}", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "释放ADB时出现异常", - "请检查MAA中ADB路径设置", - -1, - ) - - if self.if_open_emulator_process: - try: - logger.info( - f"启动模拟器:{self.emulator_path},参数:{self.emulator_arguments}", - module=f"MAA调度器-{self.name}", - ) - self.emulator_process_manager.open_process( - self.emulator_path, self.emulator_arguments, 0 - ) - except Exception as e: - logger.exception( - f"启动模拟器时出现异常:{e}", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "启动模拟器时出现异常", - "请检查MAA中模拟器路径设置", - -1, - ) - self.if_open_emulator = True - break - - # 更新静默进程标记有效时间 - logger.info( - f"更新静默进程标记:{self.emulator_path},标记有效时间:{datetime.now() + timedelta(seconds=self.wait_time + 10)}", - module=f"MAA调度器-{self.name}", - ) - Config.silence_dict[self.emulator_path] = ( - datetime.now() + timedelta(seconds=self.wait_time + 10) - ) - - self.search_ADB_address() - - # 创建MAA任务 - logger.info( - f"启动MAA进程:{self.maa_exe_path}", - module=f"MAA调度器-{self.name}", - ) - self.maa_process_manager.open_process(self.maa_exe_path, [], 0) - - # 监测MAA运行状态 - self.log_check_mode = mode_book[mode] - self.start_monitor() - - # 处理MAA结果 - if self.maa_result == "Success!": - - # 标记任务完成 - run_book[mode] = True - - logger.info( - f"用户: {user[0]} - MAA进程完成代理任务", - module=f"MAA调度器-{self.name}", - ) - self.update_log_text.emit( - "检测到MAA进程完成代理任务\n正在等待相关程序结束\n请等待10s" - ) - - else: - logger.error( - f"用户: {user[0]} - 代理任务异常: {self.maa_result}", - module=f"MAA调度器-{self.name}", - ) - # 打印中止信息 - # 此时,log变量内存储的就是出现异常的日志信息,可以保存或发送用于问题排查 - self.update_log_text.emit( - f"{self.maa_result}\n正在中止相关程序\n请等待10s" - ) - # 无命令行中止MAA与其子程序 - logger.info( - f"中止MAA进程:{self.maa_exe_path}", - module=f"MAA调度器-{self.name}", - ) - self.maa_process_manager.kill(if_force=True) - System.kill_process(self.maa_exe_path) - - # 中止模拟器进程 - logger.info( - f"中止模拟器进程:{list(self.emulator_process_manager.tracked_pids)}", - module=f"MAA调度器-{self.name}", - ) - self.emulator_process_manager.kill() - - self.if_open_emulator = True - - # 推送异常通知 - Notify.push_plyer( - "用户自动代理出现异常!", - f"用户 {user[0].replace("_", " 今天的")}的{mode_book[mode][5:7]}部分出现一次异常", - f"{user[0].replace("_", " ")}的{mode_book[mode][5:7]}出现异常", - 1, - ) - if i == self.set["RunSet"]["RunTimesLimit"] - 1: - self.play_sound.emit("子任务失败") - else: - self.play_sound.emit(self.maa_result) - - self.sleep(10) - - # 任务结束后释放ADB - try: - logger.info( - f"释放ADB:{self.ADB_address}", - module=f"MAA调度器-{self.name}", - ) - subprocess.run( - [self.ADB_path, "disconnect", self.ADB_address], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - except subprocess.CalledProcessError as e: - # 忽略错误,因为可能本来就没有连接 - logger.warning( - f"释放ADB时出现异常:{e}", - module=f"MAA调度器-{self.name}", - ) - except Exception as e: - logger.exception( - f"释放ADB时出现异常:{e}", - module=f"MAA调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "释放ADB时出现异常", - "请检查MAA中ADB路径设置", - -1, - ) - # 任务结束后再次手动中止模拟器进程,防止退出不彻底 - if self.if_kill_emulator: - logger.info( - f"任务结束后再次中止模拟器进程:{list(self.emulator_process_manager.tracked_pids)}", - module=f"MAA调度器-{self.name}", - ) - self.emulator_process_manager.kill() - self.if_open_emulator = True - - # 从配置文件中解析所需信息 - with self.maa_set_path.open(mode="r", encoding="utf-8") as f: - data = json.load(f) - - # 记录自定义基建索引 - user_data["Data"]["CustomInfrastPlanIndex"] = data[ - "Configurations" - ]["Default"]["Infrast.CustomInfrastPlanIndex"] - - # 记录更新包路径 - if ( - data["Global"]["VersionUpdate.package"] - and ( - self.maa_root_path - / data["Global"]["VersionUpdate.package"] - ).exists() - ): - self.maa_update_package = data["Global"][ - "VersionUpdate.package" - ] - - # 记录剿灭情况 - if ( - mode == "Annihilation" - and self.weekly_annihilation_limit_reached - ): - user_data["Data"]["LastAnnihilationDate"] = curdate - # 保存运行日志以及统计信息 - if_six_star = Config.save_maa_log( - Config.app_path - / f"history/{curdate}/{user_data["Info"]["Name"]}/{self.log_start_time.strftime("%H-%M-%S")}.log", - self.maa_logs, - self.maa_result, - ) - user_logs_list.append( - Config.app_path - / f"history/{curdate}/{user_data["Info"]["Name"]}/{self.log_start_time.strftime("%H-%M-%S")}.json", - ) - if if_six_star: - self.push_notification( - "公招六星", - f"喜报:用户 {user[0]} 公招出六星啦!", - { - "user_name": user_data["Info"]["Name"], - }, - user_data, - ) - self.play_sound.emit("六星喜报") - - # 执行MAA解压更新动作 - if self.maa_update_package: - - logger.info( - f"检测到MAA更新,正在执行更新动作", - module=f"MAA调度器-{self.name}", - ) - - self.update_log_text.emit( - f"检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s" - ) - self.play_sound.emit("MAA更新") - self.set_maa("更新MAA", None) - subprocess.Popen( - [self.maa_exe_path], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - self.sleep(10) - System.kill_process(self.maa_exe_path) - - self.maa_update_package = "" - - logger.info( - f"更新动作结束", module=f"MAA调度器-{self.name}" - ) - - # 发送统计信息 - statistics = Config.merge_statistic_info(user_logs_list) - statistics["user_index"] = user[2] - statistics["user_info"] = user[0] - statistics["start_time"] = user_start_time.strftime("%Y-%m-%d %H:%M:%S") - statistics["end_time"] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") - statistics["maa_result"] = ( - "代理任务全部完成" - if (run_book["Annihilation"] and run_book["Routine"]) - else "代理任务未全部完成" - ) - self.push_notification( - "统计信息", - f"{current_date} | 用户 {user[0]} 的自动代理统计报告", - statistics, - user_data, - ) - - if run_book["Annihilation"] and run_book["Routine"]: - # 成功完成代理的用户修改相关参数 - if ( - user_data["Data"]["ProxyTimes"] == 0 - and user_data["Info"]["RemainedDay"] != -1 - ): - user_data["Info"]["RemainedDay"] -= 1 - user_data["Data"]["ProxyTimes"] += 1 - user[1] = "完成" - logger.success( - f"用户 {user[0]} 的自动代理任务已完成", - module=f"MAA调度器-{self.name}", - ) - Notify.push_plyer( - "成功完成一个自动代理任务!", - f"已完成用户 {user[0].replace("_", " 今天的")}任务", - f"已完成 {user[0].replace("_", " 的")}", - 3, - ) - else: - # 录入代理失败的用户 - logger.error( - f"用户 {user[0]} 的自动代理任务未完成", - module=f"MAA调度器-{self.name}", - ) - user[1] = "异常" - - self.update_user_list.emit(self.user_list) - - # 人工排查模式 - elif self.mode == "人工排查": - - # 人工排查时,屏蔽静默操作 - logger.info( - "人工排查任务开始,屏蔽静默操作", module=f"MAA调度器-{self.name}" - ) - Config.if_ignore_silence = True - - # 标记是否需要启动模拟器 - self.if_open_emulator = True - # 标识排查模式 - for _ in self.user_list: - _[0] += "_排查模式" - - # 开始排查 - for user in self.user_list: - - user_data = self.data[user[2]]["Config"] - - if self.isInterruptionRequested: - break - - logger.info(f"开始排查用户: {user[0]}", module=f"MAA调度器-{self.name}") - - user[1] = "运行" - self.update_user_list.emit(self.user_list) - - if user_data["Info"]["Mode"] == "详细": - self.if_open_emulator = True - - run_book = [False for _ in range(2)] - - # 启动重试循环 - while not self.isInterruptionRequested: - - # 配置MAA - self.set_maa("人工排查", user[2]) - - # 记录当前时间 - self.log_start_time = datetime.now() - # 创建MAA任务 - logger.info( - f"启动MAA进程:{self.maa_exe_path}", - module=f"MAA调度器-{self.name}", - ) - self.maa_process_manager.open_process(self.maa_exe_path, [], 0) - - # 监测MAA运行状态 - self.log_check_mode = "人工排查" - self.start_monitor() - - if self.maa_result == "Success!": - logger.info( - f"用户: {user[0]} - MAA进程成功登录PRTS", - module=f"MAA调度器-{self.name}", - ) - run_book[0] = True - self.update_log_text.emit("检测到MAA进程成功登录PRTS") - else: - logger.error( - f"用户: {user[0]} - MAA未能正确登录到PRTS: {self.maa_result}", - module=f"MAA调度器-{self.name}", - ) - self.update_log_text.emit( - f"{self.maa_result}\n正在中止相关程序\n请等待10s" - ) - # 无命令行中止MAA与其子程序 - logger.info( - f"中止MAA进程:{self.maa_exe_path}", - module=f"MAA调度器-{self.name}", - ) - self.maa_process_manager.kill(if_force=True) - System.kill_process(self.maa_exe_path) - self.if_open_emulator = True - self.sleep(10) - - # 登录成功,结束循环 - if run_book[0]: - break - # 登录失败,询问是否结束循环 - elif not self.isInterruptionRequested: - - self.play_sound.emit("排查重试") - if not self.push_question( - "操作提示", "MAA未能正确登录到PRTS,是否重试?" - ): - break - - # 登录成功,录入人工排查情况 - if run_book[0] and not self.isInterruptionRequested: - - self.play_sound.emit("排查录入") - if self.push_question( - "操作提示", "请检查用户代理情况,该用户是否正确完成代理任务?" - ): - run_book[1] = True - - # 结果录入 - if run_book[0] and run_book[1]: - logger.info( - f"用户 {user[0]} 通过人工排查", module=f"MAA调度器-{self.name}" - ) - user_data["Data"]["IfPassCheck"] = True - user[1] = "完成" - else: - logger.info( - f"用户 {user[0]} 未通过人工排查", - module=f"MAA调度器-{self.name}", - ) - user_data["Data"]["IfPassCheck"] = False - user[1] = "异常" - - self.update_user_list.emit(self.user_list) - - # 解除静默操作屏蔽 - logger.info( - "人工排查任务结束,解除静默操作屏蔽", module=f"MAA调度器-{self.name}" - ) - Config.if_ignore_silence = False - - # 设置MAA模式 - elif "设置MAA" in self.mode: - - # 配置MAA - self.set_maa(self.mode, "") - # 创建MAA任务 - logger.info( - f"启动MAA进程:{self.maa_exe_path}", module=f"MAA调度器-{self.name}" - ) - self.maa_process_manager.open_process(self.maa_exe_path, [], 0) - # 记录当前时间 - self.log_start_time = datetime.now() - - # 监测MAA运行状态 - self.log_check_mode = "设置MAA" - self.start_monitor() - - if "全局" in self.mode: - (self.config_path / "Default").mkdir(parents=True, exist_ok=True) - shutil.copy(self.maa_set_path, self.config_path / "Default") - logger.success( - f"全局MAA配置文件已保存到 {self.config_path / 'Default/gui.json'}", - module=f"MAA调度器-{self.name}", - ) - - elif "用户" in self.mode: - self.user_config_path.mkdir(parents=True, exist_ok=True) - shutil.copy(self.maa_set_path, self.user_config_path) - logger.success( - f"用户MAA配置文件已保存到 {self.user_config_path}", - module=f"MAA调度器-{self.name}", - ) - - result_text = "" - - # 导出结果 - if self.mode in ["自动代理", "人工排查"]: - - # 关闭可能未正常退出的MAA进程 - if self.isInterruptionRequested: - logger.info( - f"关闭可能未正常退出的MAA进程:{self.maa_exe_path}", - module=f"MAA调度器-{self.name}", - ) - self.maa_process_manager.kill(if_force=True) - System.kill_process(self.maa_exe_path) - - # 更新用户数据 - updated_info = {_[2]: self.data[_[2]] for _ in self.user_list} - self.update_user_info.emit(self.config_path.name, updated_info) - - error_index = [_[2] for _ in self.user_list if _[1] == "异常"] - over_index = [_[2] for _ in self.user_list if _[1] == "完成"] - wait_index = [_[2] for _ in self.user_list if _[1] == "等待"] - - # 保存运行日志 - title = ( - f"{current_date} | {self.set["MaaSet"]["Name"]}的{self.mode[:4]}任务报告" - if self.set["MaaSet"]["Name"] != "" - else f"{current_date} | {self.mode[:4]}任务报告" - ) - result = { - "title": f"{self.mode[:4]}任务报告", - "script_name": ( - self.set["MaaSet"]["Name"] - if self.set["MaaSet"]["Name"] != "" - else "空白" - ), - "start_time": begin_time, - "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "completed_count": len(over_index), - "uncompleted_count": len(error_index) + len(wait_index), - "failed_user": [ - self.data[_]["Config"]["Info"]["Name"] for _ in error_index - ], - "waiting_user": [ - self.data[_]["Config"]["Info"]["Name"] for _ in wait_index - ], - } - - # 生成结果文本 - result_text = ( - f"任务开始时间:{result["start_time"]},结束时间:{result["end_time"]}\n" - f"已完成数:{result["completed_count"]},未完成数:{result["uncompleted_count"]}\n\n" - ) - if len(result["failed_user"]) > 0: - result_text += f"{self.mode[2:4]}未成功的用户:\n{"\n".join(result["failed_user"])}\n" - if len(result["waiting_user"]) > 0: - result_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(result["waiting_user"])}\n" - - # 推送代理结果通知 - Notify.push_plyer( - title.replace("报告", "已完成!"), - f"已完成用户数:{len(over_index)},未完成用户数:{len(error_index) + len(wait_index)}", - f"已完成用户数:{len(over_index)},未完成用户数:{len(error_index) + len(wait_index)}", - 10, - ) - self.push_notification("代理结果", title, result) - - # 复原 MAA 配置文件 - logger.info( - f"复原 MAA 配置文件:{self.config_path / 'Temp/gui.json'}", - module=f"MAA调度器-{self.name}", - ) - if (self.config_path / "Temp/gui.json").exists(): - shutil.copy(self.config_path / "Temp/gui.json", self.maa_set_path) - shutil.rmtree(self.config_path / "Temp") - - self.agree_bilibili(False) - 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"收到任务中止申请", module=f"MAA调度器-{self.name}") - - if len(self.log_monitor.files()) != 0: - self.interrupt.emit() - - self.maa_result = "任务被手动中止" - self.isInterruptionRequested = True - self.wait_loop.quit() - - def push_question(self, title: str, message: str) -> bool: - """推送询问窗口""" - - logger.info( - f"推送询问窗口:{title} - {message}", module=f"MAA调度器-{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"MAA调度器-{self.name}") - self.response = response - - def sleep(self, time: int) -> None: - """非阻塞型等待""" - - logger.info(f"等待 {time} 秒", module=f"MAA调度器-{self.name}") - QTimer.singleShot(time * 1000, self.wait_loop.quit) - self.wait_loop.exec() - - def search_ADB_address(self) -> None: - """搜索ADB实际地址""" - - self.update_log_text.emit( - f"即将搜索ADB实际地址\n正在等待模拟器完成启动\n请等待{self.wait_time}s" - ) - - self.sleep(self.wait_time) - - if self.isInterruptionRequested: - return None - - if "-" in self.ADB_address: - ADB_ip = f"{self.ADB_address.split("-")[0]}-" - ADB_port = int(self.ADB_address.split("-")[1]) - - elif ":" in self.ADB_address: - ADB_ip = f"{self.ADB_address.split(':')[0]}:" - ADB_port = int(self.ADB_address.split(":")[1]) - - logger.info( - f"正在搜索ADB实际地址,ADB前缀:{ADB_ip},初始端口:{ADB_port},搜索范围:{self.port_range}", - module=f"MAA调度器-{self.name}", - ) - - for port in self.port_range: - - ADB_address = f"{ADB_ip}{ADB_port + port}" - - # 尝试通过ADB连接到指定地址 - connect_result = subprocess.run( - [self.ADB_path, "connect", ADB_address], - creationflags=subprocess.CREATE_NO_WINDOW, - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - encoding="utf-8", - ) - - if "connected" in connect_result.stdout: - - # 检查连接状态 - devices_result = subprocess.run( - [self.ADB_path, "devices"], - creationflags=subprocess.CREATE_NO_WINDOW, - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - encoding="utf-8", - ) - if ADB_address in devices_result.stdout: - - logger.info( - f"ADB实际地址:{ADB_address}", module=f"MAA调度器-{self.name}" - ) - - # 断开连接 - logger.info( - f"断开ADB连接:{ADB_address}", module=f"MAA调度器-{self.name}" - ) - subprocess.run( - [self.ADB_path, "disconnect", ADB_address], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - - self.ADB_address = ADB_address - - # 覆写当前ADB地址 - logger.info( - f"开始使用实际 ADB 地址覆写:{self.ADB_address}", - module=f"MAA调度器-{self.name}", - ) - self.maa_process_manager.kill(if_force=True) - System.kill_process(self.maa_exe_path) - with self.maa_set_path.open(mode="r", encoding="utf-8") as f: - data = json.load(f) - data["Configurations"]["Default"][ - "Connect.Address" - ] = self.ADB_address - data["Configurations"]["Default"]["Start.EmulatorWaitSeconds"] = "0" - with self.maa_set_path.open(mode="w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - self.play_sound.emit("ADB成功") - return None - - else: - logger.info( - f"无法连接到ADB地址:{ADB_address}", - module=f"MAA调度器-{self.name}", - ) - else: - logger.info( - f"无法连接到ADB地址:{ADB_address}", module=f"MAA调度器-{self.name}" - ) - - if not self.isInterruptionRequested: - self.play_sound.emit("ADB失败") - - def refresh_maa_log(self) -> None: - """刷新MAA日志""" - - if self.maa_log_path.exists(): - with self.maa_log_path.open(mode="r", encoding="utf-8") as f: - logger.debug( - f"刷新MAA日志:{self.maa_log_path}", module=f"MAA调度器-{self.name}" - ) - else: - logger.warning( - f"MAA日志文件不存在:{self.maa_log_path}", - module=f"MAA调度器-{self.name}", - ) - - # 一分钟内未执行日志变化检查,强制检查一次 - if datetime.now() - self.last_check_time > timedelta(minutes=1): - logger.info("触发 1 分钟超时检查", module=f"MAA调度器-{self.name}") - self.check_maa_log() - - def check_maa_log(self) -> None: - """获取MAA日志并检查以判断MAA程序运行状态""" - - self.last_check_time = datetime.now() - - # 获取日志 - if self.maa_log_path.exists(): - self.maa_logs = [] - if_log_start = False - with self.maa_log_path.open(mode="r", encoding="utf-8") as f: - for entry in f: - if not if_log_start: - try: - entry_time = datetime.strptime( - entry[1:20], "%Y-%m-%d %H:%M:%S" - ) - if entry_time > self.log_start_time: - if_log_start = True - self.maa_logs.append(entry) - except ValueError: - pass - else: - self.maa_logs.append(entry) - else: - logger.warning( - f"MAA日志文件不存在:{self.maa_log_path}", - module=f"MAA调度器-{self.name}", - ) - return None - - log = "".join(self.maa_logs) - - # 更新MAA日志 - if self.maa_process_manager.is_running(): - - self.update_log_text.emit( - "".join(self.maa_logs) - if len(self.maa_logs) < 100 - else "".join(self.maa_logs[-100:]) - ) - - if "自动代理" in self.log_check_mode: - - # 获取最近一条日志的时间 - latest_time = self.log_start_time - for _ in self.maa_logs[::-1]: - try: - if "如果长时间无进一步日志更新,可能需要手动干预。" in _: - continue - latest_time = datetime.strptime(_[1:20], "%Y-%m-%d %H:%M:%S") - break - except ValueError: - pass - - logger.info( - f"MAA最近一条日志时间:{latest_time}", module=f"MAA调度器-{self.name}" - ) - - time_book = { - "自动代理_剿灭": "AnnihilationTimeLimit", - "自动代理_日常": "RoutineTimeLimit", - } - - if self.log_check_mode == "自动代理_剿灭" and "剿灭任务失败" in log: - self.weekly_annihilation_limit_reached = True - else: - self.weekly_annihilation_limit_reached = False - - if "任务出错: StartUp" in log: - self.maa_result = "MAA未能正确登录PRTS" - - elif "任务已全部完成!" in log: - - if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log: - self.task_dict["WakeUp"] = "False" - if "完成任务: Recruit" in log or "完成任务: 自动公招" in log: - self.task_dict["Recruiting"] = "False" - if "完成任务: Infrast" in log or "完成任务: 基建换班" in log: - self.task_dict["Base"] = "False" - if ( - "完成任务: Fight" in log - or "完成任务: 刷理智" in log - or "剿灭任务失败" in log - ): - self.task_dict["Combat"] = "False" - if "完成任务: Mall" in log or "完成任务: 获取信用及购物" in log: - self.task_dict["Mall"] = "False" - if "完成任务: Award" in log or "完成任务: 领取奖励" in log: - self.task_dict["Mission"] = "False" - if "完成任务: Roguelike" in log or "完成任务: 自动肉鸽" in log: - self.task_dict["AutoRoguelike"] = "False" - if "完成任务: Reclamation" in log or "完成任务: 生息演算" in log: - self.task_dict["Reclamation"] = "False" - - if all(v == "False" for v in self.task_dict.values()): - self.maa_result = "Success!" - else: - self.maa_result = "MAA部分任务执行失败" - - elif "请 「检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑」" in log: - self.maa_result = "MAA的ADB连接异常" - - elif "未检测到任何模拟器" in log: - self.maa_result = "MAA未检测到任何模拟器" - - elif "已停止" in log: - self.maa_result = "MAA在完成任务前中止" - - elif ( - "MaaAssistantArknights GUI exited" in log - or not self.maa_process_manager.is_running() - ): - self.maa_result = "MAA在完成任务前退出" - - elif datetime.now() - latest_time > timedelta( - minutes=self.set["RunSet"][time_book[self.log_check_mode]] - ): - self.maa_result = "MAA进程超时" - - elif self.isInterruptionRequested: - self.maa_result = "任务被手动中止" - - else: - self.maa_result = "Wait" - - elif self.log_check_mode == "人工排查": - if "完成任务: StartUp" in log or "完成任务: 开始唤醒" in log: - self.maa_result = "Success!" - elif "请 「检查连接设置」 → 「尝试重启模拟器与 ADB」 → 「重启电脑」" in log: - self.maa_result = "MAA的ADB连接异常" - elif "未检测到任何模拟器" in log: - self.maa_result = "MAA未检测到任何模拟器" - elif "已停止" in log: - self.maa_result = "MAA在完成任务前中止" - elif ( - "MaaAssistantArknights GUI exited" in log - or not self.maa_process_manager.is_running() - ): - self.maa_result = "MAA在完成任务前退出" - elif self.isInterruptionRequested: - self.maa_result = "任务被手动中止" - else: - self.maa_result = "Wait" - - elif self.log_check_mode == "设置MAA": - if ( - "MaaAssistantArknights GUI exited" in log - or not self.maa_process_manager.is_running() - ): - self.maa_result = "Success!" - else: - self.maa_result = "Wait" - - logger.info( - f"MAA日志分析结果:{self.maa_result}", module=f"MAA调度器-{self.name}" - ) - - if self.maa_result != "Wait": - - self.quit_monitor() - - def start_monitor(self) -> None: - """开始监视MAA日志""" - - logger.info( - f"开始监视MAA日志,路径:{self.maa_log_path},日志起始时间:{self.log_start_time},模式:{self.log_check_mode}", - module=f"MAA调度器-{self.name}", - ) - self.log_monitor.addPath(str(self.maa_log_path)) - self.log_monitor_timer.start(1000) - self.last_check_time = datetime.now() - self.monitor_loop.exec() - - def quit_monitor(self) -> None: - """退出MAA日志监视进程""" - - if len(self.log_monitor.files()) != 0: - - logger.info( - f"MAA日志监视器移除路径:{self.maa_log_path}", - module=f"MAA调度器-{self.name}", - ) - self.log_monitor.removePath(str(self.maa_log_path)) - - else: - logger.warning( - f"MAA日志监视器没有正在监看的路径:{self.log_monitor.files()}", - module=f"MAA调度器-{self.name}", - ) - - self.log_monitor_timer.stop() - self.last_check_time = None - self.monitor_loop.quit() - - logger.info("MAA日志监视锁已释放", module=f"MAA调度器-{self.name}") - - def set_maa(self, mode, index) -> dict: - """配置MAA运行参数""" - logger.info( - f"开始配置MAA运行参数: {mode}/{index}", module=f"MAA调度器-{self.name}" - ) - - if "设置MAA" not in self.mode and "更新MAA" not in mode: - - user_data = self.data[index]["Config"] - - if user_data["Info"]["Server"] == "Bilibili": - self.agree_bilibili(True) - else: - self.agree_bilibili(False) - - # 配置MAA前关闭可能未正常退出的MAA进程 - self.maa_process_manager.kill(if_force=True) - System.kill_process(self.maa_exe_path) - - # 预导入MAA配置文件 - if mode == "设置MAA_用户": - if self.user_config_path.exists(): - shutil.copy(self.user_config_path / "gui.json", self.maa_set_path) - else: - shutil.copy( - self.config_path / "Default/gui.json", - self.maa_set_path, - ) - elif (mode in ["设置MAA_全局", "更新MAA"]) or ( - ("自动代理" in mode or "人工排查" in mode) - and user_data["Info"]["Mode"] == "简洁" - ): - shutil.copy( - self.config_path / "Default/gui.json", - self.maa_set_path, - ) - elif "自动代理" in mode and user_data["Info"]["Mode"] == "详细": - shutil.copy( - self.data[index]["Path"] / "Routine/gui.json", self.maa_set_path - ) - elif "人工排查" in mode and user_data["Info"]["Mode"] == "详细": - shutil.copy( - self.data[index]["Path"] / "Routine/gui.json", - self.maa_set_path, - ) - with self.maa_set_path.open(mode="r", encoding="utf-8") as f: - data = json.load(f) - - # 切换配置 - if data["Current"] != "Default": - - data["Configurations"]["Default"] = data["Configurations"][data["Current"]] - data["Current"] = "Default" - - # 时间设置 - for i in range(1, 9): - data["Global"][f"Timer.Timer{i}"] = "False" - - # 自动代理配置 - if "自动代理" in mode: - - if ( - next((i for i, _ in enumerate(self.user_list) if _[2] == index), None) - == len(self.user_list) - 1 - ) or ( - self.data[ - self.user_list[ - next( - (i for i, _ in enumerate(self.user_list) if _[2] == index), - None, - ) - + 1 - ][2] - ]["Config"]["Info"]["Mode"] - == "详细" - ): - data["Configurations"]["Default"][ - "MainFunction.PostActions" - ] = "12" # 完成后退出MAA和模拟器 - else: - method_dict = {"NoAction": "8", "ExitGame": "9", "ExitEmulator": "12"} - data["Configurations"]["Default"]["MainFunction.PostActions"] = ( - method_dict[self.set["RunSet"]["TaskTransitionMethod"]] - ) # 完成后行为 - - data["Configurations"]["Default"][ - "Start.RunDirectly" - ] = "True" # 启动MAA后直接运行 - data["Configurations"]["Default"]["Start.OpenEmulatorAfterLaunch"] = str( - self.if_open_emulator - ) # 启动MAA后自动开启模拟器 - - data["Global"][ - "VersionUpdate.ScheduledUpdateCheck" - ] = "False" # 定时检查更新 - data["Global"][ - "VersionUpdate.AutoDownloadUpdatePackage" - ] = "True" # 自动下载更新包 - data["Global"][ - "VersionUpdate.AutoInstallUpdatePackage" - ] = "False" # 自动安装更新包 - - if Config.get(Config.function_IfSilence): - data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化 - data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标 - data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘 - - # 客户端类型 - data["Configurations"]["Default"]["Start.ClientType"] = user_data["Info"][ - "Server" - ] - - # 账号切换 - if user_data["Info"]["Server"] == "Official": - data["Configurations"]["Default"]["Start.AccountName"] = ( - f"{user_data["Info"]["Id"][:3]}****{user_data["Info"]["Id"][7:]}" - if len(user_data["Info"]["Id"]) == 11 - else user_data["Info"]["Id"] - ) - elif user_data["Info"]["Server"] == "Bilibili": - data["Configurations"]["Default"]["Start.AccountName"] = user_data[ - "Info" - ]["Id"] - - # 按预设设定任务 - data["Configurations"]["Default"][ - "TaskQueue.WakeUp.IsChecked" - ] = "True" # 开始唤醒 - data["Configurations"]["Default"]["TaskQueue.Recruiting.IsChecked"] = ( - self.task_dict["Recruiting"] - ) # 自动公招 - data["Configurations"]["Default"]["TaskQueue.Base.IsChecked"] = ( - self.task_dict["Base"] - ) # 基建换班 - data["Configurations"]["Default"]["TaskQueue.Combat.IsChecked"] = ( - self.task_dict["Combat"] - ) # 刷理智 - data["Configurations"]["Default"]["TaskQueue.Mission.IsChecked"] = ( - self.task_dict["Mission"] - ) # 领取奖励 - data["Configurations"]["Default"]["TaskQueue.Mall.IsChecked"] = ( - self.task_dict["Mall"] - ) # 获取信用及购物 - data["Configurations"]["Default"]["TaskQueue.AutoRoguelike.IsChecked"] = ( - self.task_dict["AutoRoguelike"] - ) # 自动肉鸽 - data["Configurations"]["Default"]["TaskQueue.Reclamation.IsChecked"] = ( - self.task_dict["Reclamation"] - ) # 生息演算 - - # 整理任务顺序 - if "剿灭" in mode or user_data["Info"]["Mode"] == "简洁": - - data["Configurations"]["Default"]["TaskQueue.Order.WakeUp"] = "0" - data["Configurations"]["Default"]["TaskQueue.Order.Recruiting"] = "1" - data["Configurations"]["Default"]["TaskQueue.Order.Base"] = "2" - data["Configurations"]["Default"]["TaskQueue.Order.Combat"] = "3" - data["Configurations"]["Default"]["TaskQueue.Order.Mall"] = "4" - data["Configurations"]["Default"]["TaskQueue.Order.Mission"] = "5" - data["Configurations"]["Default"]["TaskQueue.Order.AutoRoguelike"] = "6" - data["Configurations"]["Default"]["TaskQueue.Order.Reclamation"] = "7" - - data["Configurations"]["Default"]["MainFunction.UseMedicine"] = ( - "False" if user_data["Info"]["MedicineNumb"] == 0 else "True" - ) # 吃理智药 - data["Configurations"]["Default"]["MainFunction.UseMedicine.Quantity"] = ( - str(user_data["Info"]["MedicineNumb"]) - ) # 吃理智药数量 - data["Configurations"]["Default"][ - "MainFunction.Series.Quantity" - ] = user_data["Info"][ - "SeriesNumb" - ] # 连战次数 - - if "剿灭" in mode: - - data["Configurations"]["Default"][ - "MainFunction.Stage1" - ] = "Annihilation" # 主关卡 - data["Configurations"]["Default"][ - "MainFunction.Stage2" - ] = "" # 备选关卡1 - data["Configurations"]["Default"][ - "MainFunction.Stage3" - ] = "" # 备选关卡2 - data["Configurations"]["Default"][ - "Fight.RemainingSanityStage" - ] = "" # 剩余理智关卡 - data["Configurations"]["Default"][ - "MainFunction.Series.Quantity" - ] = "1" # 连战次数 - data["Configurations"]["Default"][ - "MainFunction.Annihilation.UseCustom" - ] = "True" # 自定义剿灭关卡 - data["Configurations"]["Default"][ - "MainFunction.Annihilation.Stage" - ] = user_data["Info"][ - "Annihilation" - ] # 自定义剿灭关卡号 - data["Configurations"]["Default"][ - "Penguin.IsDrGrandet" - ] = "False" # 博朗台模式 - data["Configurations"]["Default"][ - "GUI.CustomStageCode" - ] = "True" # 手动输入关卡名 - data["Configurations"]["Default"][ - "GUI.UseAlternateStage" - ] = "False" # 使用备选关卡 - data["Configurations"]["Default"][ - "Fight.UseRemainingSanityStage" - ] = "False" # 使用剩余理智 - data["Configurations"]["Default"][ - "Fight.UseExpiringMedicine" - ] = "True" # 无限吃48小时内过期的理智药 - data["Configurations"]["Default"][ - "GUI.HideSeries" - ] = "False" # 隐藏连战次数 - - elif "日常" in mode: - - data["Configurations"]["Default"]["MainFunction.Stage1"] = ( - user_data["Info"]["Stage"] - if user_data["Info"]["Stage"] != "-" - else "" - ) # 主关卡 - data["Configurations"]["Default"]["MainFunction.Stage2"] = ( - user_data["Info"]["Stage_1"] - if user_data["Info"]["Stage_1"] != "-" - else "" - ) # 备选关卡1 - data["Configurations"]["Default"]["MainFunction.Stage3"] = ( - user_data["Info"]["Stage_2"] - if user_data["Info"]["Stage_2"] != "-" - else "" - ) # 备选关卡2 - data["Configurations"]["Default"]["MainFunction.Stage4"] = ( - user_data["Info"]["Stage_3"] - if user_data["Info"]["Stage_3"] != "-" - else "" - ) # 备选关卡3 - data["Configurations"]["Default"]["Fight.RemainingSanityStage"] = ( - user_data["Info"]["Stage_Remain"] - if user_data["Info"]["Stage_Remain"] != "-" - else "" - ) # 剩余理智关卡 - data["Configurations"]["Default"][ - "GUI.UseAlternateStage" - ] = "True" # 备选关卡 - data["Configurations"]["Default"]["Fight.UseRemainingSanityStage"] = ( - "True" if user_data["Info"]["Stage_Remain"] != "-" else "False" - ) # 使用剩余理智 - - if user_data["Info"]["Mode"] == "简洁": - - data["Configurations"]["Default"][ - "Penguin.IsDrGrandet" - ] = "False" # 博朗台模式 - data["Configurations"]["Default"][ - "GUI.CustomStageCode" - ] = "True" # 手动输入关卡名 - data["Configurations"]["Default"][ - "Fight.UseExpiringMedicine" - ] = "True" # 无限吃48小时内过期的理智药 - # 自定义基建配置 - if user_data["Info"]["InfrastMode"] == "Custom": - - if ( - self.data[index]["Path"] - / "Infrastructure/infrastructure.json" - ).exists(): - - data["Configurations"]["Default"][ - "Infrast.InfrastMode" - ] = "Custom" # 基建模式 - data["Configurations"]["Default"][ - "Infrast.CustomInfrastPlanIndex" - ] = user_data["Data"][ - "CustomInfrastPlanIndex" - ] # 自定义基建配置索引 - data["Configurations"]["Default"][ - "Infrast.DefaultInfrast" - ] = "user_defined" # 内置配置 - data["Configurations"]["Default"][ - "Infrast.IsCustomInfrastFileReadOnly" - ] = "False" # 自定义基建配置文件只读 - data["Configurations"]["Default"][ - "Infrast.CustomInfrastFile" - ] = str( - self.data[index]["Path"] - / "Infrastructure/infrastructure.json" - ) # 自定义基建配置文件地址 - else: - logger.warning( - f"未选择用户 {user_data["Info"]["Name"]} 的自定义基建配置文件" - ) - self.push_info_bar.emit( - "warning", - "启用自定义基建失败", - f"未选择用户 {user_data["Info"]["Name"]} 的自定义基建配置文件", - -1, - ) - data["Configurations"]["Default"][ - "Infrast.CustomInfrastEnabled" - ] = "Normal" # 基建模式 - else: - data["Configurations"]["Default"][ - "Infrast.InfrastMode" - ] = user_data["Info"][ - "InfrastMode" - ] # 基建模式 - - elif user_data["Info"]["Mode"] == "详细": - - # 基建模式 - if ( - data["Configurations"]["Default"]["Infrast.InfrastMode"] - == "Custom" - ): - data["Configurations"]["Default"][ - "Infrast.CustomInfrastPlanIndex" - ] = user_data["Data"][ - "CustomInfrastPlanIndex" - ] # 自定义基建配置索引 - - # 人工排查配置 - elif "人工排查" in mode: - - data["Configurations"]["Default"][ - "MainFunction.PostActions" - ] = "8" # 完成后退出MAA - data["Configurations"]["Default"][ - "Start.RunDirectly" - ] = "True" # 启动MAA后直接运行 - data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化 - data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标 - data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘 - data["Configurations"]["Default"]["Start.OpenEmulatorAfterLaunch"] = str( - self.if_open_emulator - ) # 启动MAA后自动开启模拟器 - data["Global"][ - "VersionUpdate.ScheduledUpdateCheck" - ] = "False" # 定时检查更新 - data["Global"][ - "VersionUpdate.AutoDownloadUpdatePackage" - ] = "False" # 自动下载更新包 - data["Global"][ - "VersionUpdate.AutoInstallUpdatePackage" - ] = "False" # 自动安装更新包 - - # 客户端类型 - data["Configurations"]["Default"]["Start.ClientType"] = user_data["Info"][ - "Server" - ] - - # 账号切换 - if user_data["Info"]["Server"] == "Official": - data["Configurations"]["Default"]["Start.AccountName"] = ( - f"{user_data["Info"]["Id"][:3]}****{user_data["Info"]["Id"][7:]}" - if len(user_data["Info"]["Id"]) == 11 - else user_data["Info"]["Id"] - ) - elif user_data["Info"]["Server"] == "Bilibili": - data["Configurations"]["Default"]["Start.AccountName"] = user_data[ - "Info" - ]["Id"] - - data["Configurations"]["Default"][ - "TaskQueue.WakeUp.IsChecked" - ] = "True" # 开始唤醒 - data["Configurations"]["Default"][ - "TaskQueue.Recruiting.IsChecked" - ] = "False" # 自动公招 - data["Configurations"]["Default"][ - "TaskQueue.Base.IsChecked" - ] = "False" # 基建换班 - data["Configurations"]["Default"][ - "TaskQueue.Combat.IsChecked" - ] = "False" # 刷理智 - data["Configurations"]["Default"][ - "TaskQueue.Mission.IsChecked" - ] = "False" # 领取奖励 - data["Configurations"]["Default"][ - "TaskQueue.Mall.IsChecked" - ] = "False" # 获取信用及购物 - data["Configurations"]["Default"][ - "TaskQueue.AutoRoguelike.IsChecked" - ] = "False" # 自动肉鸽 - data["Configurations"]["Default"][ - "TaskQueue.Reclamation.IsChecked" - ] = "False" # 生息演算 - - # 设置MAA配置 - elif "设置MAA" in mode: - - data["Configurations"]["Default"][ - "MainFunction.PostActions" - ] = "0" # 完成后无动作 - data["Configurations"]["Default"][ - "Start.RunDirectly" - ] = "False" # 启动MAA后直接运行 - data["Configurations"]["Default"][ - "Start.OpenEmulatorAfterLaunch" - ] = "False" # 启动MAA后自动开启模拟器 - data["Global"][ - "VersionUpdate.ScheduledUpdateCheck" - ] = "False" # 定时检查更新 - data["Global"][ - "VersionUpdate.AutoDownloadUpdatePackage" - ] = "False" # 自动下载更新包 - data["Global"][ - "VersionUpdate.AutoInstallUpdatePackage" - ] = "False" # 自动安装更新包 - - if Config.get(Config.function_IfSilence): - data["Global"][ - "Start.MinimizeDirectly" - ] = "False" # 启动MAA后直接最小化 - - data["Configurations"]["Default"][ - "TaskQueue.WakeUp.IsChecked" - ] = "False" # 开始唤醒 - data["Configurations"]["Default"][ - "TaskQueue.Recruiting.IsChecked" - ] = "False" # 自动公招 - data["Configurations"]["Default"][ - "TaskQueue.Base.IsChecked" - ] = "False" # 基建换班 - data["Configurations"]["Default"][ - "TaskQueue.Combat.IsChecked" - ] = "False" # 刷理智 - data["Configurations"]["Default"][ - "TaskQueue.Mission.IsChecked" - ] = "False" # 领取奖励 - data["Configurations"]["Default"][ - "TaskQueue.Mall.IsChecked" - ] = "False" # 获取信用及购物 - data["Configurations"]["Default"][ - "TaskQueue.AutoRoguelike.IsChecked" - ] = "False" # 自动肉鸽 - data["Configurations"]["Default"][ - "TaskQueue.Reclamation.IsChecked" - ] = "False" # 生息演算 - - elif mode == "更新MAA": - - data["Configurations"]["Default"][ - "MainFunction.PostActions" - ] = "0" # 完成后无动作 - data["Configurations"]["Default"][ - "Start.RunDirectly" - ] = "False" # 启动MAA后直接运行 - data["Configurations"]["Default"][ - "Start.OpenEmulatorAfterLaunch" - ] = "False" # 启动MAA后自动开启模拟器 - data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化 - data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标 - data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘 - data["Global"][ - "VersionUpdate.package" - ] = self.maa_update_package # 更新包路径 - - data["Global"][ - "VersionUpdate.ScheduledUpdateCheck" - ] = "False" # 定时检查更新 - data["Global"][ - "VersionUpdate.AutoDownloadUpdatePackage" - ] = "False" # 自动下载更新包 - data["Global"][ - "VersionUpdate.AutoInstallUpdatePackage" - ] = "True" # 自动安装更新包 - data["Configurations"]["Default"][ - "TaskQueue.WakeUp.IsChecked" - ] = "False" # 开始唤醒 - data["Configurations"]["Default"][ - "TaskQueue.Recruiting.IsChecked" - ] = "False" # 自动公招 - data["Configurations"]["Default"][ - "TaskQueue.Base.IsChecked" - ] = "False" # 基建换班 - data["Configurations"]["Default"][ - "TaskQueue.Combat.IsChecked" - ] = "False" # 刷理智 - data["Configurations"]["Default"][ - "TaskQueue.Mission.IsChecked" - ] = "False" # 领取奖励 - data["Configurations"]["Default"][ - "TaskQueue.Mall.IsChecked" - ] = "False" # 获取信用及购物 - data["Configurations"]["Default"][ - "TaskQueue.AutoRoguelike.IsChecked" - ] = "False" # 自动肉鸽 - data["Configurations"]["Default"][ - "TaskQueue.Reclamation.IsChecked" - ] = "False" # 生息演算 - - # 启动模拟器仅生效一次 - if "设置MAA" not in mode and "更新MAA" not in mode and self.if_open_emulator: - self.if_open_emulator = False - - # 覆写配置文件 - with self.maa_set_path.open(mode="w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - logger.success( - f"MAA运行参数配置完成: {mode}/{index}", module=f"MAA调度器-{self.name}" - ) - - return data - - def agree_bilibili(self, if_agree): - """向MAA写入Bilibili协议相关任务""" - logger.info( - f"Bilibili协议相关任务状态: {'启用' if if_agree else '禁用'}", - module=f"MAA调度器-{self.name}", - ) - - with self.maa_tasks_path.open(mode="r", encoding="utf-8") as f: - data = json.load(f) - - if if_agree and Config.get(Config.function_IfAgreeBilibili): - data["BilibiliAgreement_AUTO"] = { - "algorithm": "OcrDetect", - "action": "ClickSelf", - "text": ["同意"], - "maxTimes": 5, - "Doc": "关闭B服用户协议", - "next": ["StartUpThemes#next"], - } - if "BilibiliAgreement_AUTO" not in data["StartUpThemes"]["next"]: - data["StartUpThemes"]["next"].insert(0, "BilibiliAgreement_AUTO") - else: - if "BilibiliAgreement_AUTO" in data: - data.pop("BilibiliAgreement_AUTO") - if "BilibiliAgreement_AUTO" in data["StartUpThemes"]["next"]: - data["StartUpThemes"]["next"].remove("BilibiliAgreement_AUTO") - - with self.maa_tasks_path.open(mode="w", encoding="utf-8") as f: - json.dump(data, f, ensure_ascii=False, indent=4) - - def push_notification( - self, - mode: str, - title: str, - message: Union[str, dict], - user_data: Dict[str, Dict[str, Union[str, int, bool]]] = None, - ) -> None: - """通过所有渠道推送通知""" - logger.info( - f"开始推送通知,模式:{mode},标题:{title}", - module=f"MAA调度器-{self.name}", - ) - - env = Environment( - loader=FileSystemLoader(str(Config.app_path / "resources/html")) - ) - - if mode == "代理结果" and ( - Config.get(Config.notify_SendTaskResultTime) == "任何时刻" - or ( - Config.get(Config.notify_SendTaskResultTime) == "仅失败时" - and message["uncompleted_count"] != 0 - ) - ): - # 生成文本通知内容 - message_text = ( - f"任务开始时间:{message["start_time"]},结束时间:{message["end_time"]}\n" - f"已完成数:{message["completed_count"]},未完成数:{message["uncompleted_count"]}\n\n" - ) - - if len(message["failed_user"]) > 0: - message_text += f"{self.mode[2:4]}未成功的用户:\n{"\n".join(message["failed_user"])}\n" - if len(message["waiting_user"]) > 0: - message_text += f"\n未开始{self.mode[2:4]}的用户:\n{"\n".join(message["waiting_user"])}\n" - - # 生成HTML通知内容 - message["failed_user"] = "、".join(message["failed_user"]) - message["waiting_user"] = "、".join(message["waiting_user"]) - - template = env.get_template("MAA_result.html") - message_html = template.render(message) - - # ServerChan的换行是两个换行符。故而将\n替换为\n\n - serverchan_message = message_text.replace("\n", "\n\n") - - # 发送全局通知 - - if Config.get(Config.notify_IfSendMail): - Notify.send_mail( - "网页", title, message_html, Config.get(Config.notify_ToAddress) - ) - - if Config.get(Config.notify_IfServerChan): - Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), - ) - - if Config.get(Config.notify_IfCompanyWebHookBot): - Notify.CompanyWebHookBotPush( - title, - f"{message_text}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - - elif mode == "统计信息": - - # 生成文本通知内容 - formatted = [] - if "drop_statistics" in message: - for stage, items in message["drop_statistics"].items(): - formatted.append(f"掉落统计({stage}):") - for item, quantity in items.items(): - formatted.append(f" {item}: {quantity}") - drop_text = "\n".join(formatted) - - formatted = ["招募统计:"] - if "recruit_statistics" in message: - for star, count in message["recruit_statistics"].items(): - formatted.append(f" {star}: {count}") - recruit_text = "\n".join(formatted) - - message_text = ( - f"开始时间: {message['start_time']}\n" - f"结束时间: {message['end_time']}\n" - f"MAA执行结果: {message['maa_result']}\n\n" - f"{recruit_text}\n" - f"{drop_text}" - ) - - # 生成HTML通知内容 - template = env.get_template("MAA_statistics.html") - message_html = template.render(message) - - # ServerChan的换行是两个换行符。故而将\n替换为\n\n - serverchan_message = message_text.replace("\n", "\n\n") - - # 发送全局通知 - if Config.get(Config.notify_IfSendStatistic): - - if Config.get(Config.notify_IfSendMail): - Notify.send_mail( - "网页", title, message_html, Config.get(Config.notify_ToAddress) - ) - - if Config.get(Config.notify_IfServerChan): - Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), - ) - - if Config.get(Config.notify_IfCompanyWebHookBot): - Notify.CompanyWebHookBotPush( - title, - f"{message_text}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - - # 发送用户单独通知 - if ( - user_data["Notify"]["Enabled"] - and user_data["Notify"]["IfSendStatistic"] - ): - - # 发送邮件通知 - if user_data["Notify"]["IfSendMail"]: - if user_data["Notify"]["ToAddress"]: - Notify.send_mail( - "网页", - title, - message_html, - user_data["Notify"]["ToAddress"], - ) - else: - logger.error(f"用户邮箱地址为空,无法发送用户单独的邮件通知") - - # 发送ServerChan通知 - if user_data["Notify"]["IfServerChan"]: - if user_data["Notify"]["ServerChanKey"]: - Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO_MAA 敬上", - user_data["Notify"]["ServerChanKey"], - user_data["Notify"]["ServerChanTag"], - user_data["Notify"]["ServerChanChannel"], - ) - else: - logger.error( - f"{self.name} |用户ServerChan密钥为空,无法发送用户单独的ServerChan通知" - ) - - # 推送CompanyWebHookBot通知 - if user_data["Notify"]["IfCompanyWebHookBot"]: - if user_data["Notify"]["CompanyWebHookBotUrl"]: - Notify.CompanyWebHookBotPush( - title, - f"{message_text}\n\nAUTO_MAA 敬上", - user_data["Notify"]["CompanyWebHookBotUrl"], - ) - else: - logger.error( - f"{self.name} |用户CompanyWebHookBot密钥为空,无法发送用户单独的CompanyWebHookBot通知" - ) - - elif mode == "公招六星": - - # 生成HTML通知内容 - template = env.get_template("MAA_six_star.html") - - message_html = template.render(message) - - # 发送全局通知 - if Config.get(Config.notify_IfSendSixStar): - - if Config.get(Config.notify_IfSendMail): - Notify.send_mail( - "网页", title, message_html, Config.get(Config.notify_ToAddress) - ) - - if Config.get(Config.notify_IfServerChan): - Notify.ServerChanPush( - title, - "好羡慕~\n\nAUTO_MAA 敬上", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), - ) - - if Config.get(Config.notify_IfCompanyWebHookBot): - Notify.CompanyWebHookBotPush( - title, - "好羡慕~\n\nAUTO_MAA 敬上", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - Notify.CompanyWebHookBotPushImage( - Config.app_path / "resources/images/notification/six_star.png", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - - # 发送用户单独通知 - if user_data["Notify"]["Enabled"] and user_data["Notify"]["IfSendSixStar"]: - - # 发送邮件通知 - if user_data["Notify"]["IfSendMail"]: - if user_data["Notify"]["ToAddress"]: - Notify.send_mail( - "网页", - title, - message_html, - user_data["Notify"]["ToAddress"], - ) - else: - logger.error(f"用户邮箱地址为空,无法发送用户单独的邮件通知") - - # 发送ServerChan通知 - if user_data["Notify"]["IfServerChan"]: - - if user_data["Notify"]["ServerChanKey"]: - Notify.ServerChanPush( - title, - "好羡慕~\n\nAUTO_MAA 敬上", - user_data["Notify"]["ServerChanKey"], - user_data["Notify"]["ServerChanTag"], - user_data["Notify"]["ServerChanChannel"], - ) - else: - logger.error( - f"{self.name} |用户ServerChan密钥为空,无法发送用户单独的ServerChan通知" - ) - - # 推送CompanyWebHookBot通知 - if user_data["Notify"]["IfCompanyWebHookBot"]: - if user_data["Notify"]["CompanyWebHookBotUrl"]: - Notify.CompanyWebHookBotPush( - title, - "好羡慕~\n\nAUTO_MAA 敬上", - user_data["Notify"]["CompanyWebHookBotUrl"], - ) - Notify.CompanyWebHookBotPushImage( - Config.app_path - / "resources/images/notification/six_star.png", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - else: - logger.error( - f"{self.name} |用户CompanyWebHookBot密钥为空,无法发送用户单独的CompanyWebHookBot通知" - ) - - return None diff --git a/app/models/__init__.py b/app/models/__init__.py deleted file mode 100644 index fdb7653..0000000 --- a/app/models/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA模组包 -v4.4 -作者:DLmaster_361 -""" - -__version__ = "4.2.0" -__author__ = "DLmaster361 " -__license__ = "GPL-3.0 license" - -from .general import GeneralManager -from .MAA import MaaManager - -__all__ = ["GeneralManager", "MaaManager"] diff --git a/app/models/general.py b/app/models/general.py deleted file mode 100644 index 8ef8d15..0000000 --- a/app/models/general.py +++ /dev/null @@ -1,1201 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -通用功能组件 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtCore import QObject, Signal, QEventLoop, QFileSystemWatcher, QTimer -import os -import sys -import shutil -import subprocess -from functools import partial -from datetime import datetime, timedelta -from pathlib import Path -from jinja2 import Environment, FileSystemLoader -from typing import Union, List, Dict - -from app.core import Config, GeneralConfig, GeneralSubConfig, logger -from app.services import Notify, System -from app.utils import ProcessManager - - -class GeneralManager(QObject): - """通用脚本通用控制器""" - - question = Signal(str, str) - question_response = Signal(bool) - update_sub_info = Signal(str, dict) - push_info_bar = Signal(str, str, str, int) - play_sound = Signal(str) - create_user_list = Signal(list) - update_user_list = Signal(list) - update_log_text = Signal(str) - interrupt = Signal() - accomplish = Signal(dict) - - def __init__( - self, - mode: str, - config: Dict[ - str, - Union[ - str, - Path, - GeneralConfig, - Dict[str, Dict[str, Union[Path, GeneralSubConfig]]], - ], - ], - sub_config_path: Path = None, - ): - super(GeneralManager, self).__init__() - - 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(self.check_script_log) - - self.question_loop = QEventLoop() - self.question_response.connect(self.__capture_response) - self.question_response.connect(self.question_loop.quit) - - self.wait_loop = QEventLoop() - - self.isInterruptionRequested = False - self.interrupt.connect(self.quit_monitor) - - self.task_dict = {} - self.set = config["Config"].toDict() - - self.data: Dict[str, Dict[str, Union[Path, dict]]] = {} - if self.mode != "设置通用脚本": - for name, info in config["SubData"].items(): - self.data[name] = { - "Path": info["Path"], - "Config": info["Config"].toDict(), - } - - 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: - """检查配置完整性""" - - if not ( - Path(self.set["Script"]["RootPath"]).exists() - and Path(self.set["Script"]["ScriptPath"]).exists() - and Path(self.set["Script"]["ConfigPath"]).exists() - and Path(self.set["Script"]["LogPath"]).parent.exists() - and self.set["Script"]["LogTimeFormat"] - and self.set["Script"]["ErrorLog"] - ) or ( - self.set["Game"]["Enabled"] and not Path(self.set["Game"]["Path"]).exists() - ): - logger.error("脚本配置缺失", module=f"通用调度器-{self.name}") - self.push_info_bar.emit("error", "脚本配置缺失", "请检查脚本配置!", -1) - return False - - return True - - def configure(self): - """提取配置信息""" - - self.script_root_path = Path(self.set["Script"]["RootPath"]) - 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( - datetime.now().strftime(self.set["Script"]["LogPathFormat"]) - ) - if self.set["Script"]["LogPathFormat"] - else Path(self.set["Script"]["LogPath"]) - ) - if not self.script_log_path.exists(): - self.script_log_path.parent.mkdir(parents=True, exist_ok=True) - self.script_log_path.touch(exist_ok=True) - self.game_path = Path(self.set["Game"]["Path"]) - self.log_time_range = [ - self.set["Script"]["LogTimeStart"] - 1, - self.set["Script"]["LogTimeEnd"], - ] - self.success_log = ( - [_.strip() for _ in self.set["Script"]["SuccessLog"].split("|")] - if self.set["Script"]["SuccessLog"] - else [] - ) - self.error_log = [_.strip() for _ in self.set["Script"]["ErrorLog"].split("|")] - - def run(self): - """主进程,运行通用脚本代理进程""" - - current_date = datetime.now().strftime("%m-%d") - 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": "由于配置不完整,通用代理进程中止"} - ) - return None - - 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 != "设置通用脚本": - - self.data = dict(sorted(self.data.items(), key=lambda x: int(x[0][3:]))) - self.sub_list: List[List[str, str, str]] = [ - [_["Config"]["Info"]["Name"], "等待", index] - for index, _ in self.data.items() - if ( - _["Config"]["Info"]["RemainedDay"] != 0 - and _["Config"]["Info"]["Status"] - ) - ] - self.create_user_list.emit(self.sub_list) - - logger.info( - f"配置列表创建完成,已筛选子配置数:{len(self.sub_list)}", - module=f"通用调度器-{self.name}", - ) - - # 自动代理模式 - if self.mode == "自动代理": - - # 执行情况预处理 - for _ in self.sub_list: - if self.data[_[2]]["Config"]["Data"]["LastProxyDate"] != curdate: - self.data[_[2]]["Config"]["Data"]["LastProxyDate"] = curdate - self.data[_[2]]["Config"]["Data"]["ProxyTimes"] = 0 - _[ - 0 - ] += f" - 第{self.data[_[2]]['Config']['Data']['ProxyTimes'] + 1}次代理" - - # 开始代理 - for sub in self.sub_list: - - sub_data = self.data[sub[2]]["Config"] - - if self.isInterruptionRequested: - break - - if ( - self.set["Run"]["ProxyTimesLimit"] == 0 - or sub_data["Data"]["ProxyTimes"] - < self.set["Run"]["ProxyTimesLimit"] - ): - sub[1] = "运行" - self.update_user_list.emit(self.sub_list) - else: - sub[1] = "跳过" - self.update_user_list.emit(self.sub_list) - continue - - 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"配置: {sub[0]} - 未找到配置文件", - module=f"通用调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "启动通用代理进程失败", - f"未找到{sub[0]}的配置文件!", - -1, - ) - run_book = False - continue - - # 尝试次数循环 - for i in range(self.set["Run"]["RunTimesLimit"]): - - if self.isInterruptionRequested or run_book: - break - - logger.info( - f"用户: {sub[0]} - 尝试次数: {i + 1}/{self.set['Run']['RunTimesLimit']}", - module=f"通用调度器-{self.name}", - ) - - # 记录当前时间 - self.log_start_time = datetime.now() - # 配置脚本 - self.set_sub(sub[2]) - # 执行任务前脚本 - if ( - sub_data["Info"]["IfScriptBeforeTask"] - and Path(sub_data["Info"]["ScriptBeforeTask"]).exists() - ): - self.execute_script_task( - Path(sub_data["Info"]["ScriptBeforeTask"]), "脚本前任务" - ) - - # 启动游戏/模拟器 - if self.set["Game"]["Enabled"]: - - try: - logger.info( - f"启动游戏/模拟器:{self.game_path},参数:{self.set['Game']['Arguments']}", - module=f"通用调度器-{self.name}", - ) - self.game_process_manager.open_process( - self.game_path, - str(self.set["Game"]["Arguments"]).split(" "), - 0, - ) - except Exception as e: - logger.exception( - f"启动游戏/模拟器时出现异常:{e}", - module=f"通用调度器-{self.name}", - ) - self.push_info_bar.emit( - "error", - "启动游戏/模拟器时出现异常", - "请检查游戏/模拟器路径设置", - -1, - ) - self.script_result = "游戏/模拟器启动失败" - break - - # 更新静默进程标记 - if self.set["Game"]["Style"] == "Emulator": - 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" - ) - - self.sleep(self.set["Game"]["WaitTime"]) - - # 运行脚本任务 - logger.info( - f"运行脚本任务:{self.script_exe_path},参数:{self.script_arguments}", - module=f"通用调度器-{self.name}", - ) - self.script_process_manager.open_process( - self.script_exe_path, - self.script_arguments, - tracking_time=60 if self.set["Script"]["IfTrackProcess"] else 0, - ) - - # 监测运行状态 - self.start_monitor() - - if self.script_result == "Success!": - - # 标记任务完成 - 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"配置: {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"配置: {sub[0]} - 代理任务异常: {self.script_result}", - module=f"通用调度器-{self.name}", - ) - # 打印中止信息 - # 此时,log变量内存储的就是出现异常的日志信息,可以保存或发送用于问题排查 - self.update_log_text.emit( - f"{self.script_result}\n正在中止相关程序\n请等待10s" - ) - - # 中止相关程序 - 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) - - # 推送异常通知 - Notify.push_plyer( - "用户自动代理出现异常!", - f"用户 {sub[0].replace("_", " 今天的")}出现一次异常", - f"{sub[0].replace("_", " ")}出现异常", - 1, - ) - if i == self.set["Run"]["RunTimesLimit"] - 1: - 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"] - and Path(sub_data["Info"]["ScriptAfterTask"]).exists() - ): - self.execute_script_task( - Path(sub_data["Info"]["ScriptAfterTask"]), "脚本后任务" - ) - - # 保存运行日志以及统计信息 - Config.save_general_log( - Config.app_path - / f"history/{curdate}/{sub_data['Info']['Name']}/{self.log_start_time.strftime("%H-%M-%S")}.log", - self.script_logs, - self.script_result, - ) - - # 发送统计信息 - statistics = { - "sub_index": sub[2], - "sub_info": sub[0], - "start_time": sub_start_time.strftime("%Y-%m-%d %H:%M:%S"), - "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "sub_result": "代理成功" if run_book else self.script_result, - } - self.push_notification( - "统计信息", - f"{current_date} | 配置 {sub[0]} 的自动代理统计报告", - statistics, - sub_data, - ) - - if run_book: - # 成功完成代理的用户修改相关参数 - if ( - sub_data["Data"]["ProxyTimes"] == 0 - and sub_data["Info"]["RemainedDay"] != -1 - ): - 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("_", " 今天的")}任务", - f"已完成 {sub[0].replace("_", " 的")}", - 3, - ) - else: - # 录入代理失败的用户 - sub[1] = "异常" - logger.error( - f"配置: {sub[0]} - 代理任务异常: {self.script_result}", - module=f"通用调度器-{self.name}", - ) - - self.update_user_list.emit(self.sub_list) - - # 设置通用脚本模式 - elif self.mode == "设置通用脚本": - - # 配置通用脚本 - self.set_sub() - - try: - # 创建通用脚本任务 - logger.info( - f"运行脚本任务:{self.script_set_exe_path},参数:{self.script_set_arguments}", - module=f"通用调度器-{self.name}", - ) - self.script_process_manager.open_process( - self.script_set_exe_path, - self.script_set_arguments, - tracking_time=60 if self.set["Script"]["IfTrackProcess"] else 0, - ) - - # 记录当前时间 - self.log_start_time = datetime.now() - - # 监测通用脚本运行状态 - self.start_monitor() - - self.sub_config_path.mkdir(parents=True, exist_ok=True) - if self.set["Script"]["ConfigPathMode"] == "文件夹": - shutil.copytree( - self.script_config_path, - 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.exception( - f"启动通用脚本时出现异常:{e}", module=f"通用调度器-{self.name}" - ) - self.push_info_bar.emit( - "error", "启动通用脚本时出现异常", "请检查相关设置", -1 - ) - - result_text = "" - - # 导出结果 - if self.mode in ["自动代理"]: - - # 关闭可能未正常退出的通用脚本进程 - 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) - - # 更新用户数据 - updated_info = {_[2]: self.data[_[2]] for _ in self.sub_list} - self.update_sub_info.emit(self.config_path.name, updated_info) - - error_index = [_[2] for _ in self.sub_list if _[1] == "异常"] - over_index = [_[2] for _ in self.sub_list if _[1] == "完成"] - wait_index = [_[2] for _ in self.sub_list if _[1] == "等待"] - - # 保存运行日志 - title = ( - f"{current_date} | {self.name}的{self.mode[:4]}任务报告" - if self.name != "" - else f"{current_date} | {self.mode[:4]}任务报告" - ) - result = { - "title": f"{self.mode[:4]}任务报告", - "script_name": (self.name if self.name != "" else "空白"), - "start_time": begin_time, - "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), - "completed_count": len(over_index), - "uncompleted_count": len(error_index) + len(wait_index), - "failed_sub": [ - self.data[_]["Config"]["Info"]["Name"] for _ in error_index - ], - "waiting_sub": [ - self.data[_]["Config"]["Info"]["Name"] for _ in wait_index - ], - } - - # 生成结果文本 - result_text = ( - f"任务开始时间:{result['start_time']},结束时间:{result['end_time']}\n" - f"已完成数:{result['completed_count']},未完成数:{result['uncompleted_count']}\n\n" - ) - if len(result["failed_sub"]) > 0: - result_text += f"{self.mode[2:4]}未成功的配置:\n{"\n".join(result['failed_sub'])}\n" - if len(result["waiting_sub"]) > 0: - result_text += f"\n未开始{self.mode[2:4]}的配置:\n{"\n".join(result['waiting_sub'])}\n" - - # 推送代理结果通知 - Notify.push_plyer( - title.replace("报告", "已完成!"), - f"已完成配置数:{len(over_index)},未完成配置数:{len(error_index) + len(wait_index)}", - f"已完成配置数:{len(over_index)},未完成配置数:{len(error_index) + len(wait_index)}", - 10, - ) - 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"收到任务中止申请", module=f"通用调度器-{self.name}") - - if len(self.log_monitor.files()) != 0: - self.interrupt.emit() - - self.script_result = "任务被手动中止" - self.isInterruptionRequested = True - 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: - """刷新脚本日志""" - - 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: - logger.info("触发 1 分钟超时检查", module=f"通用调度器-{self.name}") - self.check_script_log() - - def strptime( - self, date_string: str, format: str, default_date: datetime - ) -> datetime: - """根据指定格式解析日期字符串""" - - # 时间字段映射表 - time_fields = { - "%Y": "year", - "%m": "month", - "%d": "day", - "%H": "hour", - "%M": "minute", - "%S": "second", - "%f": "microsecond", - } - - date = datetime.strptime(date_string, format) - - # 构建参数字典 - datetime_kwargs = {} - for format_code, field_name in time_fields.items(): - if format_code in format: - datetime_kwargs[field_name] = getattr(date, field_name) - else: - datetime_kwargs[field_name] = getattr(default_date, field_name) - - return datetime(**datetime_kwargs) - - def check_script_log(self) -> None: - """获取脚本日志并检查以判断脚本程序运行状态""" - - self.last_check_time = datetime.now() - - # 获取日志 - 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 > 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 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 = 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]], - self.set["Script"]["LogTimeFormat"], - self.last_check_time, - ) - break - 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!" - break - else: - - if self.isInterruptionRequested: - self.script_result = "任务被手动中止" - elif datetime.now() - latest_time > timedelta( - minutes=self.set["Run"]["RunTimeLimit"] - ): - self.script_result = "脚本进程超时" - else: - for error_sign in self.error_log: - if error_sign in log: - self.script_result = f"异常日志:{error_sign}" - break - else: - if self.script_process_manager.is_running(): - self.script_result = "Wait" - elif self.success_log: - self.script_result = "脚本在完成任务前退出" - else: - self.script_result = "Success!" - - elif self.mode == "设置通用脚本": - if self.script_process_manager.is_running(): - self.script_result = "Wait" - else: - self.script_result = "Success!" - - logger.info( - f"通用脚本日志分析结果:{self.script_result}", - module=f"通用调度器-{self.name}", - ) - - if self.script_result != "Wait": - - self.quit_monitor() - - def start_monitor(self) -> None: - """开始监视通用脚本日志""" - - 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_timer.start(1000) - self.last_check_time = datetime.now() - self.monitor_loop.exec() - - def quit_monitor(self) -> None: - """退出通用脚本日志监视进程""" - - if len(self.log_monitor.files()) != 0: - - logger.info( - f"通用脚本日志监视器移除路径:{self.script_log_path}", - module=f"通用调度器-{self.name}", - ) - self.log_monitor.removePath(str(self.script_log_path)) - - 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"开始配置脚本运行参数:{index}", module=f"通用调度器-{self.name}") - - # 配置前关闭可能未正常退出的脚本进程 - if self.mode == "自动代理": - System.kill_process(self.script_exe_path) - elif self.mode == "设置通用脚本": - System.kill_process(self.script_set_exe_path) - - # 预导入配置文件 - if self.mode == "设置通用脚本": - if self.sub_config_path.exists(): - if self.set["Script"]["ConfigPathMode"] == "文件夹": - shutil.copytree( - self.sub_config_path, - self.script_config_path, - dirs_exist_ok=True, - ) - elif (self.sub_config_path / self.script_config_path.name).exists(): - shutil.copy( - self.sub_config_path / self.script_config_path.name, - self.script_config_path, - ) - else: - if self.set["Script"]["ConfigPathMode"] == "文件夹": - shutil.copytree( - self.data[index]["Path"] / "ConfigFiles", - self.script_config_path, - dirs_exist_ok=True, - ) - else: - shutil.copy( - self.data[index]["Path"] - / "ConfigFiles" - / self.script_config_path.name, - 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"开始执行{task_name}: {script_path}", module=f"通用调度器-{self.name}" - ) - - # 根据文件类型选择执行方式 - if script_path.suffix.lower() == ".py": - cmd = [sys.executable, script_path] - elif script_path.suffix.lower() in [".bat", ".cmd", ".exe"]: - cmd = [str(script_path)] - elif script_path.suffix.lower() == "": - logger.warning( - f"{task_name}脚本没有指定后缀名,无法执行", - module=f"通用调度器-{self.name}", - ) - return False - else: - # 使用系统默认程序打开 - os.startfile(str(script_path)) - return True - - # 执行脚本并等待结束 - result = subprocess.run( - cmd, - cwd=script_path.parent, - stdin=subprocess.DEVNULL, - creationflags=( - subprocess.CREATE_NO_WINDOW - if Config.get(Config.function_IfSilence) - else 0 - ), - timeout=600, - capture_output=True, - errors="ignore", - ) - - if result.returncode == 0: - logger.info(f"{task_name}执行成功", module=f"通用调度器-{self.name}") - if result.stdout.strip(): - logger.info( - f"{task_name}输出: {result.stdout}", - module=f"通用调度器-{self.name}", - ) - return True - else: - logger.error( - f"{task_name}执行失败,返回码: {result.returncode}", - module=f"通用调度器-{self.name}", - ) - if result.stderr.strip(): - logger.error( - f"{task_name}错误输出: {result.stderr}", - module=f"通用调度器-{self.name}", - ) - return False - - except subprocess.TimeoutExpired: - logger.error(f"{task_name}执行超时", module=f"通用调度器-{self.name}") - return False - except Exception as e: - logger.exception( - f"执行{task_name}时出现异常: {e}", module=f"通用调度器-{self.name}" - ) - return False - - def push_notification( - self, - mode: str, - title: str, - message: Union[str, dict], - sub_data: Dict[str, Dict[str, Union[str, int, bool]]] = None, - ) -> None: - """通过所有渠道推送通知""" - - logger.info( - f"开始推送通知,模式:{mode},标题:{title}", - module=f"通用调度器-{self.name}", - ) - - env = Environment( - loader=FileSystemLoader(str(Config.app_path / "resources/html")) - ) - - if mode == "代理结果" and ( - Config.get(Config.notify_SendTaskResultTime) == "任何时刻" - or ( - Config.get(Config.notify_SendTaskResultTime) == "仅失败时" - and message["uncompleted_count"] != 0 - ) - ): - # 生成文本通知内容 - message_text = ( - f"任务开始时间:{message['start_time']},结束时间:{message['end_time']}\n" - f"已完成数:{message['completed_count']},未完成数:{message['uncompleted_count']}\n\n" - ) - - if len(message["failed_sub"]) > 0: - message_text += f"{self.mode[2:4]}未成功的配置:\n{"\n".join(message['failed_sub'])}\n" - if len(message["waiting_sub"]) > 0: - message_text += f"\n未开始{self.mode[2:4]}的配置:\n{"\n".join(message['waiting_sub'])}\n" - - # 生成HTML通知内容 - message["failed_sub"] = "、".join(message["failed_sub"]) - message["waiting_sub"] = "、".join(message["waiting_sub"]) - - template = env.get_template("general_result.html") - message_html = template.render(message) - - # ServerChan的换行是两个换行符。故而将\n替换为\n\n - serverchan_message = message_text.replace("\n", "\n\n") - - # 发送全局通知 - - if Config.get(Config.notify_IfSendMail): - Notify.send_mail( - "网页", title, message_html, Config.get(Config.notify_ToAddress) - ) - - if Config.get(Config.notify_IfServerChan): - Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), - ) - - if Config.get(Config.notify_IfCompanyWebHookBot): - Notify.CompanyWebHookBotPush( - title, - f"{message_text}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - - elif mode == "统计信息": - - message_text = ( - f"开始时间: {message['start_time']}\n" - f"结束时间: {message['end_time']}\n" - f"通用脚本执行结果: {message['sub_result']}\n\n" - ) - - # 生成HTML通知内容 - template = env.get_template("general_statistics.html") - message_html = template.render(message) - - # ServerChan的换行是两个换行符。故而将\n替换为\n\n - serverchan_message = message_text.replace("\n", "\n\n") - - # 发送全局通知 - if Config.get(Config.notify_IfSendStatistic): - - if Config.get(Config.notify_IfSendMail): - Notify.send_mail( - "网页", title, message_html, Config.get(Config.notify_ToAddress) - ) - - if Config.get(Config.notify_IfServerChan): - Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), - ) - - if Config.get(Config.notify_IfCompanyWebHookBot): - Notify.CompanyWebHookBotPush( - title, - f"{message_text}\n\nAUTO_MAA 敬上", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - - # 发送用户单独通知 - if sub_data["Notify"]["Enabled"] and sub_data["Notify"]["IfSendStatistic"]: - - # 发送邮件通知 - if sub_data["Notify"]["IfSendMail"]: - if sub_data["Notify"]["ToAddress"]: - Notify.send_mail( - "网页", - title, - message_html, - sub_data["Notify"]["ToAddress"], - ) - else: - logger.error(f"用户邮箱地址为空,无法发送用户单独的邮件通知") - - # 发送ServerChan通知 - if sub_data["Notify"]["IfServerChan"]: - if sub_data["Notify"]["ServerChanKey"]: - Notify.ServerChanPush( - title, - f"{serverchan_message}\n\nAUTO_MAA 敬上", - sub_data["Notify"]["ServerChanKey"], - sub_data["Notify"]["ServerChanTag"], - sub_data["Notify"]["ServerChanChannel"], - ) - else: - logger.error( - f"{self.name} |用户ServerChan密钥为空,无法发送用户单独的ServerChan通知" - ) - - # 推送CompanyWebHookBot通知 - if sub_data["Notify"]["IfCompanyWebHookBot"]: - if sub_data["Notify"]["CompanyWebHookBotUrl"]: - Notify.CompanyWebHookBotPush( - title, - f"{message_text}\n\nAUTO_MAA 敬上", - sub_data["Notify"]["CompanyWebHookBotUrl"], - ) - else: - logger.error( - f"{self.name} |用户CompanyWebHookBot密钥为空,无法发送用户单独的CompanyWebHookBot通知" - ) - - return None diff --git a/app/services/__init__.py b/app/services/__init__.py deleted file mode 100644 index 0bdbea6..0000000 --- a/app/services/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA服务包 -v4.4 -作者:DLmaster_361 -""" - -__version__ = "4.2.0" -__author__ = "DLmaster361 " -__license__ = "GPL-3.0 license" - -from .notification import Notify -from .security import Crypto -from .system import System -from .skland import skland_sign_in - -__all__ = ["Notify", "Crypto", "System", "skland_sign_in"] diff --git a/app/services/notification.py b/app/services/notification.py deleted file mode 100644 index 0fd13f4..0000000 --- a/app/services/notification.py +++ /dev/null @@ -1,484 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA通知服务 -v4.4 -作者:DLmaster_361 -""" - -import re -import smtplib -import time -from email.header import Header -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 plyer import notification - -from app.core import Config, logger -from app.services.security import Crypto -from app.utils.ImageUtils import ImageUtils - - -class Notification(QObject): - - push_info_bar = Signal(str, str, str, int) - - def __init__(self, parent=None): - super().__init__(parent) - - 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, - app_name="AUTO_MAA", - app_icon=str(Config.app_path / "resources/icons/AUTO_MAA.ico"), - timeout=t, - ticker=ticker, - toast=True, - ) - - 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) == "" - or not bool( - re.match( - r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", - Config.get(Config.notify_FromAddress), - ) - ) - or not bool( - re.match( - r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", - to_address, - ) - ) - ): - logger.error( - "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址", - module="通知服务", - ) - self.push_info_bar.emit( - "error", - "邮件通知推送异常", - "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址", - -1, - ) - return None - - try: - # 定义邮件正文 - if mode == "文本": - message = MIMEText(content, "plain", "utf-8") - elif mode == "网页": - message = MIMEMultipart("alternative") - message["From"] = formataddr( - ( - Header("AUTO_MAA通知服务", "utf-8").encode(), - Config.get(Config.notify_FromAddress), - ) - ) # 发件人显示的名字 - message["To"] = formataddr( - (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.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() - ) - smtpObj.quit() - logger.success(f"邮件发送成功:{title}", module="通知服务") - except Exception as e: - logger.exception(f"发送邮件时出错:{e}", module="通知服务") - self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1) - - 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", module="通知服务") - self.push_info_bar.emit( - "error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1 - ) - return None - - try: - # 构造 URL - if send_key.startswith("sctp"): - match = re.match(r"^sctp(\d+)t", send_key) - if match: - url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send" - else: - raise ValueError("SendKey 格式错误(sctp)") - else: - url = f"https://sctapi.ftqq.com/{send_key}.send" - - # 构建 tags 和 channel - def is_valid(s): - return s == "" or ( - s == "|".join(s.split("|")) - and (s.count("|") == 0 or all(s.split("|"))) - ) - - tags = "|".join(_.strip() for _ in tag.split("|")) - channels = "|".join(_.strip() for _ in channel.split("|")) - - options = {} - if is_valid(tags): - options["tags"] = tags - else: - logger.warning("Server酱 Tag 配置不正确,将被忽略", module="通知服务") - self.push_info_bar.emit( - "warning", - "Server酱通知推送异常", - "请正确设置 ServerChan 的 Tag", - -1, - ) - - if is_valid(channels): - options["channel"] = channels - else: - logger.warning( - "Server酱 Channel 配置不正确,将被忽略", module="通知服务" - ) - self.push_info_bar.emit( - "warning", - "Server酱通知推送异常", - "请正确设置 ServerChan 的 Channel", - -1, - ) - - # 请求发送 - params = {"title": title, "desp": content, **options} - headers = {"Content-Type": "application/json;charset=utf-8"} - - response = requests.post( - url, - json=params, - headers=headers, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - result = response.json() - - if result.get("code") == 0: - logger.success(f"Server酱推送通知成功:{title}", module="通知服务") - return True - else: - error_code = result.get("code", "-1") - 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(f"Server酱通知推送异常:{e}", module="通知服务") - self.push_info_bar.emit( - "error", - "Server酱通知推送异常", - "请检查相关设置和网络连接。如全部配置正确,请稍后再试。", - -1, - ) - return f"Server酱通知推送异常:{str(e)}" - - 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地址", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - "请正确设置企业微信群机器人的WebHook地址", - -1, - ) - return None - - content = f"{title}\n{content}" - data = {"msgtype": "text", "text": {"content": content}} - - for _ in range(3): - try: - response = requests.post( - url=webhook_url, - json=data, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - info = response.json() - break - except Exception as e: - err = e - time.sleep(0.1) - else: - logger.error(f"推送企业微信群机器人时出错:{err}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送失败", - f"使用企业微信群机器人推送通知时出错:{err}", - -1, - ) - return None - - if info["errcode"] == 0: - logger.success(f"企业微信群机器人推送通知成功:{title}", module="通知服务") - return True - else: - logger.error(f"企业微信群机器人推送通知失败:{info}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送失败", - f"使用企业微信群机器人推送通知时出错:{err}", - -1, - ) - 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) - - # 检查图片是否存在 - if not image_path.exists(): - logger.error( - "图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确", - module="通知服务", - ) - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - "图片不存在或者压缩失败,请检查图片路径是否正确", - -1, - ) - return False - - if not webhook_url: - logger.error( - "请正确设置企业微信群机器人的WebHook地址", module="通知服务" - ) - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - "请正确设置企业微信群机器人的WebHook地址", - -1, - ) - return False - - # 获取图片base64和md5 - try: - 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}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人通知推送异常", - f"图片编码或MD5计算失败:{e}", - -1, - ) - return False - - data = { - "msgtype": "image", - "image": {"base64": image_base64, "md5": image_md5}, - } - - for _ in range(3): - try: - response = requests.post( - url=webhook_url, - json=data, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - info = response.json() - break - except requests.RequestException as e: - err = e - logger.exception( - f"推送企业微信群机器人图片第{_+1}次失败:{e}", module="通知服务" - ) - time.sleep(0.1) - else: - logger.error("推送企业微信群机器人图片时出错", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人图片推送失败", - f"使用企业微信群机器人推送图片时出错:{err}", - -1, - ) - return False - - if info.get("errcode") == 0: - logger.success( - f"企业微信群机器人推送图片成功:{image_path.name}", - module="通知服务", - ) - return True - else: - logger.error(f"企业微信群机器人推送图片失败:{info}", module="通知服务") - self.push_info_bar.emit( - "error", - "企业微信群机器人图片推送失败", - f"使用企业微信群机器人推送图片时出错:{info}", - -1, - ) - return False - - except Exception as e: - logger.error(f"推送企业微信群机器人图片时发生未知异常:{e}") - self.push_info_bar.emit( - "error", - "企业微信群机器人图片推送失败", - f"发生未知异常:{e}", - -1, - ) - return False - - def send_test_notification(self): - """发送测试通知到所有已启用的通知渠道""" - - logger.info("发送测试通知到所有已启用的通知渠道", module="通知服务") - - # 发送系统通知 - self.push_plyer( - "测试通知", - "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - "测试通知", - 3, - ) - - # 发送邮件通知 - if Config.get(Config.notify_IfSendMail): - self.send_mail( - "文本", - "AUTO_MAA测试通知", - "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - Config.get(Config.notify_ToAddress), - ) - - # 发送Server酱通知 - if Config.get(Config.notify_IfServerChan): - self.ServerChanPush( - "AUTO_MAA测试通知", - "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - Config.get(Config.notify_ServerChanKey), - Config.get(Config.notify_ServerChanTag), - Config.get(Config.notify_ServerChanChannel), - ) - - # 发送企业微信机器人通知 - if Config.get(Config.notify_IfCompanyWebHookBot): - self.CompanyWebHookBotPush( - "AUTO_MAA测试通知", - "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - Notify.CompanyWebHookBotPushImage( - Config.app_path / "resources/images/notification/test_notify.png", - Config.get(Config.notify_CompanyWebHookBotUrl), - ) - - logger.info("测试通知发送完成", module="通知服务") - - return True - - -Notify = Notification() diff --git a/app/services/security.py b/app/services/security.py deleted file mode 100644 index d597f60..0000000 --- a/app/services/security.py +++ /dev/null @@ -1,270 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA安全服务 -v4.4 -作者:DLmaster_361 -""" - -import hashlib -import random -import secrets -import base64 -import win32crypt -from Crypto.Cipher import AES -from Crypto.PublicKey import RSA -from Crypto.Cipher import PKCS1_OAEP -from Crypto.Util.Padding import pad, unpad - -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) - - # 生成RSA密钥对 - key = RSA.generate(2048) - public_key_local = key.publickey() - private_key = key - # 保存RSA公钥 - (Config.app_path / "data/key/public_key.pem").write_bytes( - public_key_local.exportKey() - ) - # 生成密钥转换与校验随机盐 - PASSWORD_salt = secrets.token_hex(random.randint(32, 1024)) - (Config.app_path / "data/key/PASSWORDsalt.txt").write_text( - PASSWORD_salt, - encoding="utf-8", - ) - verify_salt = secrets.token_hex(random.randint(32, 1024)) - (Config.app_path / "data/key/verifysalt.txt").write_text( - verify_salt, - encoding="utf-8", - ) - # 将管理密钥转化为AES-256密钥 - AES_password = hashlib.sha256( - (PASSWORD + PASSWORD_salt).encode("utf-8") - ).digest() - # 生成AES-256密钥校验哈希值并保存 - AES_password_verify = hashlib.sha256( - AES_password + verify_salt.encode("utf-8") - ).digest() - (Config.app_path / "data/key/AES_password_verify.bin").write_bytes( - AES_password_verify - ) - # AES-256加密RSA私钥并保存密文 - AES_key = AES.new(AES_password, AES.MODE_ECB) - private_key_local = AES_key.encrypt(pad(private_key.exportKey(), 32)) - (Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local) - - def AUTO_encryptor(self, note: str) -> str: - """ - 使用AUTO_MAA的算法加密数据 - - :param note: 数据明文 - :type note: str - """ - - if note == "": - return "" - - # 读取RSA公钥 - public_key_local = RSA.import_key( - (Config.app_path / "data/key/public_key.pem").read_bytes() - ) - # 使用RSA公钥对数据进行加密 - cipher = PKCS1_OAEP.new(public_key_local) - encrypted = cipher.encrypt(note.encode("utf-8")) - return base64.b64encode(encrypted).decode("utf-8") - - def AUTO_decryptor(self, note: str, PASSWORD: str) -> str: - """ - 使用AUTO_MAA的算法解密数据 - - :param note: 数据密文 - :type note: str - :param PASSWORD: 管理密钥 - :type PASSWORD: str - :return: 解密后的明文 - :rtype: str - """ - - if note == "": - return "" - - # 读入RSA私钥密文、盐与校验哈希值 - private_key_local = ( - (Config.app_path / "data/key/private_key.bin").read_bytes().strip() - ) - PASSWORD_salt = ( - (Config.app_path / "data/key/PASSWORDsalt.txt") - .read_text(encoding="utf-8") - .strip() - ) - verify_salt = ( - (Config.app_path / "data/key/verifysalt.txt") - .read_text(encoding="utf-8") - .strip() - ) - AES_password_verify = ( - (Config.app_path / "data/key/AES_password_verify.bin").read_bytes().strip() - ) - # 将管理密钥转化为AES-256密钥并验证 - AES_password = hashlib.sha256( - (PASSWORD + PASSWORD_salt).encode("utf-8") - ).digest() - AES_password_SHA = hashlib.sha256( - AES_password + verify_salt.encode("utf-8") - ).digest() - if AES_password_SHA != AES_password_verify: - return "管理密钥错误" - else: - # AES解密RSA私钥 - AES_key = AES.new(AES_password, AES.MODE_ECB) - private_key_pem = unpad(AES_key.decrypt(private_key_local), 32) - private_key = RSA.import_key(private_key_pem) - # 使用RSA私钥解密数据 - decrypter = PKCS1_OAEP.new(private_key) - note = decrypter.decrypt(base64.b64decode(note)).decode("utf-8") - return note - - def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None: - """ - 修改管理密钥 - - :param PASSWORD_old: 旧管理密钥 - :type PASSWORD_old: str - :param PASSWORD_new: 新管理密钥 - :type PASSWORD_new: str - """ - - for script in Config.script_dict.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 script in Config.script_dict.values(): - - # 使用新管理密钥重新加密 - if script["Type"] == "Maa": - for user in script["UserData"].values(): - user["Config"].set( - user["Config"].Info_Password, - self.AUTO_encryptor(user["Password"]), - ) - user["Password"] = None - del user["Password"] - - def reset_PASSWORD(self, PASSWORD_new: str) -> None: - """ - 重置管理密钥 - - :param PASSWORD_new: 新管理密钥 - :type PASSWORD_new: str - """ - - self.get_PASSWORD(PASSWORD_new) - - for script in Config.script_dict.values(): - - 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加密数据 - - :param note: 数据明文 - :type note: str - :param description: 描述信息 - :type description: str - :param entropy: 随机熵 - :type entropy: bytes - :return: 加密后的数据 - :rtype: str - """ - - if note == "": - return "" - - encrypted = win32crypt.CryptProtectData( - note.encode("utf-8"), description, entropy, None, None, 0 - ) - return base64.b64encode(encrypted).decode("utf-8") - - def win_decryptor(self, note: str, entropy: bytes = None) -> str: - """ - 使用Windows DPAPI解密数据 - - :param note: 数据密文 - :type note: str - :param entropy: 随机熵 - :type entropy: bytes - :return: 解密后的明文 - :rtype: str - """ - - if note == "": - return "" - - decrypted = win32crypt.CryptUnprotectData( - base64.b64decode(note), entropy, None, None, 0 - ) - return decrypted[1].decode("utf-8") - - def check_PASSWORD(self, PASSWORD: str) -> bool: - """ - 验证管理密钥 - - :param PASSWORD: 管理密钥 - :type PASSWORD: str - :return: 是否验证通过 - :rtype: bool - """ - - return bool( - self.AUTO_decryptor(self.AUTO_encryptor("-"), PASSWORD) != "管理密钥错误" - ) - - -Crypto = CryptoHandler() diff --git a/app/services/skland.py b/app/services/skland.py deleted file mode 100644 index 901a352..0000000 --- a/app/services/skland.py +++ /dev/null @@ -1,285 +0,0 @@ -# AUTO_MAA:A MAA Multi Account Management and Automation Tool -# Copyright © 2024-2025 DLmaster361 - -# This file incorporates work covered by the following copyright and -# permission notice: -# -# skland-checkin-ghaction Copyright © 2023 Yanstory -# https://github.com/Yanstory/skland-checkin-ghaction - -# 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 . - -# Contact: DLmaster_361@163.com - - -""" -AUTO_MAA -AUTO_MAA森空岛服务 -v4.4 -作者:DLmaster_361、ClozyA -""" - -import time -import json -import hmac -import hashlib -import requests -from urllib import parse - -from app.core import Config, logger - - -def skland_sign_in(token) -> dict: - """森空岛签到""" - - app_code = "4ca99fa6b56cc2ba" - # 用于获取grant code - grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant" - # 用于获取cred - cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code" - # 查询角色绑定 - binding_url = "https://zonai.skland.com/api/v1/game/player/binding" - # 签到接口 - sign_url = "https://zonai.skland.com/api/v1/game/attendance" - - # 基础请求头 - header = { - "cred": "", - "User-Agent": "Skland/1.5.1 (com.hypergryph.skland; build:100501001; Android 34;) Okhttp/4.11.0", - "Accept-Encoding": "gzip", - "Connection": "close", - } - header_login = header.copy() - header_for_sign = { - "platform": "1", - "timestamp": "", - "dId": "", - "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) - header_ca["timestamp"] = t - header_ca_str = json.dumps(header_ca, separators=(",", ":")) - s = path + body_or_query + t + header_ca_str # 拼接原始字符串 - # HMAC-SHA256 + MD5得到最终sign - hex_s = hmac.new(token_bytes, s.encode("utf-8"), hashlib.sha256).hexdigest() - md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest() - return md5, header_ca - - def get_sign_header(url: str, method, body, old_header, sign_token): - """ - 获取带签名的请求头 - - :param url: 请求完整url - :param method: 请求方式 GET/POST - :param body: POST请求体或GET时为None - :param old_header: 原始请求头 - :param sign_token: 当前会话的签名token - :return: 新请求头 - """ - - h = json.loads(json.dumps(old_header)) - p = parse.urlparse(url) - if method.lower() == "get": - sign, header_ca = generate_signature(sign_token, p.path, p.query) - else: - sign, header_ca = generate_signature( - sign_token, p.path, json.dumps(body) if body else "" - ) - h["sign"] = sign - for i in header_ca: - h[i] = header_ca[i] - return h - - def copy_header(cred): - """ - 复制请求头并添加cred - - :param cred: 当前会话的cred - :return: 新的请求头 - """ - v = json.loads(json.dumps(header)) - v["cred"] = cred - return v - - def login_by_token(token_code): - """ - 使用token一步步拿到cred和sign_token - - :param token_code: 你的skyland token - :return: (cred, sign_token) - """ - try: - # token为json对象时提取data.content - t = json.loads(token_code) - token_code = t["data"]["content"] - except: - pass - grant_code = get_grant_code(token_code) - return get_cred(grant_code) - - 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}, - headers=header_login, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ).json() - if rsp["code"] != 0: - raise Exception(f'获得cred失败:{rsp.get("messgae")}') - sign_token = rsp["data"]["token"] - cred = rsp["data"]["cred"] - return cred, sign_token - - 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}, - headers=header_login, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ).json() - if rsp["status"] != 0: - raise Exception( - f'使用token: {token[:3]}******{token[-3:]} 获得认证代码失败:{rsp.get("msg")}' - ) - return rsp["data"]["code"] - - def get_binding_list(cred, sign_token): - """ - 查询已绑定的角色列表 - - :param cred: 当前cred - :param sign_token: 当前sign_token - :return: 角色列表 - """ - v = [] - rsp = requests.get( - binding_url, - headers=get_sign_header( - binding_url, "get", None, copy_header(cred), sign_token - ), - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ).json() - if rsp["code"] != 0: - logger.error( - f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}", - module="森空岛签到", - ) - if rsp.get("message") == "用户未登录": - logger.error( - f"森空岛服务 | 用户登录可能失效了,请重新登录!", - module="森空岛签到", - ) - return v - # 只取明日方舟(arknights)的绑定账号 - for i in rsp["data"]["list"]: - if i.get("appCode") != "arknights": - continue - v.extend(i.get("bindingList")) - return v - - def do_sign(cred, sign_token) -> dict: - """ - 对所有绑定的角色进行签到 - - :param cred: 当前cred - :param sign_token: 当前sign_token - :return: 签到结果字典 - """ - - characters = get_binding_list(cred, sign_token) - result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)} - - for character in characters: - - body = { - "uid": character.get("uid"), - "gameId": character.get("channelMasterId"), - } - rsp = requests.post( - sign_url, - headers=get_sign_header( - sign_url, "post", body, copy_header(cred), sign_token - ), - json=body, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ).json() - - if rsp["code"] != 0: - - result[ - "重复" if rsp.get("message") == "请勿重复签到!" else "失败" - ].append( - f"{character.get("nickName")}({character.get("channelName")})" - ) - - else: - - result["成功"].append( - f"{character.get("nickName")}({character.get("channelName")})" - ) - - time.sleep(3) - - return result - - # 主流程 - try: - # 拿到cred和sign_token - cred, sign_token = login_by_token(token) - time.sleep(1) - # 依次签到 - return do_sign(cred, sign_token) - except Exception as e: - logger.exception(f"森空岛服务 | 森空岛签到失败: {e}", module="森空岛签到") - return {"成功": [], "重复": [], "失败": [], "总计": 0} diff --git a/app/services/system.py b/app/services/system.py deleted file mode 100644 index 36a9fba..0000000 --- a/app/services/system.py +++ /dev/null @@ -1,352 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA系统服务 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import QApplication -import sys -import ctypes -import win32gui -import win32process -import psutil -import subprocess -import tempfile -import getpass -from datetime import datetime -from pathlib import Path - -from app.core import Config, logger - - -class _SystemHandler: - - ES_CONTINUOUS = 0x80000000 - ES_SYSTEM_REQUIRED = 0x00000001 - - def __init__(self): - - self.set_Sleep() - self.set_SelfStart() - - def set_Sleep(self) -> None: - """同步系统休眠状态""" - - if Config.get(Config.function_IfAllowSleep): - # 设置系统电源状态 - ctypes.windll.kernel32.SetThreadExecutionState( - self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED - ) - else: - # 恢复系统电源状态 - ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS) - - def set_SelfStart(self) -> None: - """同步开机自启""" - - if Config.get(Config.start_IfSelfStart) and not self.is_startup(): - - # 创建任务计划 - try: - - # 获取当前用户和时间 - current_user = getpass.getuser() - current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") - - # XML 模板 - xml_content = f""" - - - {current_time} - {current_user} - AUTO_MAA自启动服务 - \\AUTO_MAA_AutoStart - - - - {current_time} - true - - - - - InteractiveToken - HighestAvailable - - - - IgnoreNew - false - false - false - true - false - - false - false - - true - true - false - false - false - PT0S - 7 - - - - "{Config.app_path_sys}" - - - """ - - # 创建临时 XML 文件并执行 - with tempfile.NamedTemporaryFile( - mode="w", suffix=".xml", delete=False, encoding="utf-16" - ) as f: - f.write(xml_content) - xml_file = f.name - - try: - result = subprocess.run( - [ - "schtasks", - "/create", - "/tn", - "AUTO_MAA_AutoStart", - "/xml", - xml_file, - "/f", - ], - creationflags=subprocess.CREATE_NO_WINDOW, - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - ) - - if result.returncode == 0: - logger.success( - f"程序自启动任务计划已创建: {Config.app_path_sys}", - module="系统服务", - ) - else: - logger.error( - f"程序自启动任务计划创建失败: {result.stderr}", - module="系统服务", - ) - - finally: - # 删除临时文件 - try: - Path(xml_file).unlink() - except: - pass - - except Exception as e: - logger.exception(f"程序自启动任务计划创建失败: {e}", module="系统服务") - - elif not Config.get(Config.start_IfSelfStart) and self.is_startup(): - - try: - - result = subprocess.run( - ["schtasks", "/delete", "/tn", "AUTO_MAA_AutoStart", "/f"], - creationflags=subprocess.CREATE_NO_WINDOW, - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - ) - - if result.returncode == 0: - logger.success("程序自启动任务计划已删除", module="系统服务") - else: - logger.error( - f"程序自启动任务计划删除失败: {result.stderr}", - module="系统服务", - ) - - except Exception as 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("不执行系统电源操作", module="系统服务") - - elif mode == "Shutdown": - - 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("执行休眠操作", module="系统服务") - subprocess.run(["shutdown", "/h"]) - - elif mode == "Sleep": - - 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) - - elif sys.platform.startswith("linux"): - - if mode == "NoAction": - - logger.info("不执行系统电源操作", module="系统服务") - - elif mode == "Shutdown": - - logger.info("执行关机操作", module="系统服务") - subprocess.run(["shutdown", "-h", "now"]) - - elif mode == "Hibernate": - - logger.info("执行休眠操作", module="系统服务") - subprocess.run(["systemctl", "hibernate"]) - - elif mode == "Sleep": - - 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: - """判断程序是否已经开机自启""" - - try: - result = subprocess.run( - ["schtasks", "/query", "/tn", "AUTO_MAA_AutoStart"], - creationflags=subprocess.CREATE_NO_WINDOW, - stdin=subprocess.DEVNULL, - capture_output=True, - text=True, - ) - return result.returncode == 0 - except Exception as 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): - _, pid = win32process.GetWindowThreadProcessId(hwnd) - process = psutil.Process(pid) - window_info.append((win32gui.GetWindowText(hwnd), process.exe())) - return True - - window_info = [] - win32gui.EnumWindows(callback, window_info) - 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( - f"taskkill /F /T /PID {pid}", - shell=True, - creationflags=subprocess.CREATE_NO_WINDOW, - ) - killprocess.wait() - - logger.success(f"进程已中止: {path}", module="系统服务") - - def search_pids(self, path: Path) -> list: - """ - 根据路径查找进程PID - - :param path: 进程路径 - :return: 匹配的进程PID列表 - """ - - logger.info(f"开始查找进程 PID: {path}", module="系统服务") - - pids = [] - for proc in psutil.process_iter(["pid", "exe"]): - try: - if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower(): - pids.append(proc.info["pid"]) - except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): - # 进程可能在此期间已结束或无法访问,忽略这些异常 - pass - return pids - - -System = _SystemHandler() diff --git a/app/ui/Widget.py b/app/ui/Widget.py deleted file mode 100644 index 7af06f5..0000000 --- a/app/ui/Widget.py +++ /dev/null @@ -1,2207 +0,0 @@ -# AUTO_MAA:A MAA Multi Account Management and Automation Tool -# Copyright © 2024-2025 DLmaster361 - -# This file incorporates work covered by the following copyright and -# permission notice: -# -# ZenlessZoneZero-OneDragon Copyright © 2024-2025 DoctorReid -# https://github.com/DoctorReid/ZenlessZoneZero-OneDragon - -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA组件 -v4.4 -作者:DLmaster_361 -""" - -import os -import re -import win32com.client -from pathlib import Path -from datetime import datetime -from functools import partial -from typing import Optional, Union, List, Dict -from urllib.parse import urlparse - -import markdown -from PySide6.QtCore import Qt, QTime, QTimer, QEvent, QSize -from PySide6.QtGui import QIcon, QPixmap, QPainter, QPainterPath -from PySide6.QtWidgets import ( - QApplication, - QWidget, - QLabel, - QHBoxLayout, - QVBoxLayout, - QSizePolicy, - QFileDialog, -) -from qfluentwidgets import ( - LineEdit, - PasswordLineEdit, - MessageBoxBase, - MessageBox, - SubtitleLabel, - SettingCard, - FluentIconBase, - Signal, - ComboBox, - EditableComboBox, - CheckBox, - IconWidget, - FluentIcon, - CardWidget, - BodyLabel, - QConfig, - ConfigItem, - OptionsConfigItem, - TeachingTip, - TransparentToolButton, - TeachingTipTailPosition, - ExpandSettingCard, - ExpandGroupSettingCard, - ToolButton, - PushButton, - PrimaryPushButton, - ProgressRing, - TextBrowser, - HeaderCardWidget, - SwitchButton, - IndicatorPosition, - Slider, - ScrollArea, - Pivot, - PivotItem, - FlyoutViewBase, - PushSettingCard, -) -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): - """输入对话框""" - - def __init__(self, parent, title: str, content: Union[str, None], mode: str): - super().__init__(parent) - self.title = SubtitleLabel(title) - - if mode == "明文": - self.input = LineEdit() - self.input.setClearButtonEnabled(True) - elif mode == "密码": - self.input = PasswordLineEdit() - - self.input.returnPressed.connect(self.yesButton.click) - self.input.setPlaceholderText(content) - - # 将组件添加到布局中 - self.viewLayout.addWidget(self.title) - self.viewLayout.addWidget(self.input) - - self.input.setFocus() - - -class ComboBoxMessageBox(MessageBoxBase): - """选择对话框""" - - def __init__( - self, - parent, - title: str, - content: List[str], - text_list: List[List[str]], - data_list: List[List[str]] = None, - ): - super().__init__(parent) - self.title = SubtitleLabel(title) - - Widget = QWidget() - Layout = QHBoxLayout(Widget) - - self.input: List[ComboBox] = [] - - for i in range(len(content)): - - self.input.append(ComboBox()) - if data_list: - for j in range(len(text_list[i])): - self.input[i].addItem(text_list[i][j], userData=data_list[i][j]) - else: - self.input[i].addItems(text_list[i]) - self.input[i].setCurrentIndex(-1) - self.input[i].setPlaceholderText(content[i]) - Layout.addWidget(self.input[i]) - - # 将组件添加到布局中 - self.viewLayout.addWidget(self.title) - self.viewLayout.addWidget(Widget) - - -class ProgressRingMessageBox(MessageBoxBase): - """进度环倒计时对话框""" - - def __init__(self, parent, title: str): - super().__init__(parent) - self.title = SubtitleLabel(title) - - self.time = 100 if Config.args.mode == "gui" else 1 - Widget = QWidget() - Layout = QHBoxLayout(Widget) - self.ring = ProgressRing() - self.ring.setRange(0, 100) - self.ring.setValue(100) - self.ring.setTextVisible(True) - self.ring.setFormat("%p 秒") - self.ring.setFixedSize(100, 100) - self.ring.setStrokeWidth(4) - Layout.addWidget(self.ring) - - self.yesButton.hide() - self.cancelButton.clicked.connect(self.__quit_timer) - self.buttonLayout.insertStretch(1) - - # 将组件添加到布局中 - self.viewLayout.addWidget(self.title) - self.viewLayout.addWidget(Widget) - - self.timer = QTimer(self) - self.timer.timeout.connect(self.__update_time) - self.timer.start(1000) - - def __update_time(self): - - self.time -= 1 - self.ring.setValue(self.time) - - if self.time == 0: - self.timer.stop() - self.timer.deleteLater() - self.yesButton.click() - - def __quit_timer(self): - self.timer.stop() - self.timer.deleteLater() - - -class NoticeMessageBox(MessageBoxBase): - """公告对话框""" - - def __init__(self, parent, title: str, content: Dict[str, str]): - super().__init__(parent) - - self.index = self.NoticeIndexCard(title, content, self) - self.text = TextBrowser(self) - self.text.setOpenExternalLinks(True) - self.button_yes = PrimaryPushButton("确认", self) - self.button_cancel = PrimaryPushButton("取消", self) - - self.buttonGroup.hide() - - self.v_layout = QVBoxLayout() - self.v_layout.addWidget(self.text) - self.button_layout = QHBoxLayout() - self.button_layout.addWidget(self.button_yes) - self.button_layout.addWidget(self.button_cancel) - self.v_layout.addLayout(self.button_layout) - - self.h_layout = QHBoxLayout() - self.h_layout.addWidget(self.index) - self.h_layout.addLayout(self.v_layout) - self.h_layout.setStretch(0, 1) - self.h_layout.setStretch(1, 3) - - # 将组件添加到布局中 - self.viewLayout.addLayout(self.h_layout) - self.widget.setFixedSize(800, 600) - - self.index.index_changed.connect(self.__update_text) - self.button_yes.clicked.connect(self.yesButton.click) - self.button_cancel.clicked.connect(self.cancelButton.click) - self.index.index_cards[0].clicked.emit() - - def __update_text(self, index: int, text: str): - - self.currentIndex = index - - html = markdown.markdown(text).replace("\n", "") - html = re.sub( - r"(.*?)", - r"\1", - html, - ) - html = re.sub( - r'(]*href="[^"]+"[^>]*)>', r'\1 style="color: #009faa;">', html - ) - html = re.sub(r"
  • (.*?)

  • ", r"

    \1

    ", html) - html = re.sub(r"
      (.*?)
    ", r"\1", html) - - self.text.setHtml(f"{html}") - - class NoticeIndexCard(HeaderCardWidget): - - index_changed = Signal(int, str) - - def __init__(self, title: str, content: Dict[str, str], parent=None): - super().__init__(parent) - self.setTitle(title) - - self.Layout = QVBoxLayout() - self.viewLayout.addLayout(self.Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - self.index_cards: List[QuantifiedItemCard] = [] - - for index, text in content.items(): - - self.index_cards.append(QuantifiedItemCard([index, ""])) - self.index_cards[-1].clicked.connect( - 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) - - -class SettingFlyoutView(FlyoutViewBase): - """设置卡二级菜单弹出组件""" - - def __init__( - self, - parent, - title: str, - setting_cards: List[Union[SettingCard, HeaderCardWidget]], - ): - super().__init__(parent) - - self.title = SubtitleLabel(title) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setSpacing(0) - content_layout.setContentsMargins(0, 0, 11, 0) - for setting_card in setting_cards: - content_layout.addWidget(setting_card) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - self.viewLayout = QVBoxLayout(self) - self.viewLayout.setSpacing(12) - self.viewLayout.setContentsMargins(20, 16, 9, 16) - self.viewLayout.addWidget(self.title) - self.viewLayout.addWidget(scrollArea) - - self.setVisible(False) - - -class SwitchSettingCard(SettingCard): - """Setting card with switch button""" - - checkedChanged = Signal(bool) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.switchButton = SwitchButton(self.tr("Off"), self, IndicatorPosition.RIGHT) - - if configItem: - self.setValue(self.qconfig.get(configItem)) - configItem.valueChanged.connect(self.setValue) - - # add switch button to layout - self.hBoxLayout.addWidget(self.switchButton, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.switchButton.checkedChanged.connect(self.__onCheckedChanged) - - def __onCheckedChanged(self, isChecked: bool): - """switch button checked state changed slot""" - self.setValue(isChecked) - self.checkedChanged.emit(isChecked) - - def setValue(self, isChecked: bool): - if self.configItem: - self.qconfig.set(self.configItem, isChecked) - - self.switchButton.setChecked(isChecked) - self.switchButton.setText(self.tr("On") if isChecked else self.tr("Off")) - - def setChecked(self, isChecked: bool): - self.setValue(isChecked) - - def isChecked(self): - return self.switchButton.isChecked() - - -class RangeSettingCard(SettingCard): - """Setting card with a slider""" - - valueChanged = Signal(int) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.slider = Slider(Qt.Horizontal, self) - self.valueLabel = QLabel(self) - self.slider.setMinimumWidth(268) - - self.slider.setSingleStep(1) - self.slider.setRange(*configItem.range) - self.slider.setValue(configItem.value) - self.valueLabel.setNum(configItem.value) - - self.hBoxLayout.addStretch(1) - self.hBoxLayout.addWidget(self.valueLabel, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(6) - self.hBoxLayout.addWidget(self.slider, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.valueLabel.setObjectName("valueLabel") - configItem.valueChanged.connect(self.setValue) - self.slider.valueChanged.connect(self.__onValueChanged) - - def __onValueChanged(self, value: int): - """slider value changed slot""" - self.setValue(value) - self.valueChanged.emit(value) - - def setValue(self, value): - self.qconfig.set(self.configItem, value) - self.valueLabel.setNum(value) - self.valueLabel.adjustSize() - self.slider.setValue(value) - - -class ComboBoxSettingCard(SettingCard): - """Setting card with a combo box""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - texts: List[str], - qconfig: QConfig, - configItem: OptionsConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.comboBox = ComboBox(self) - self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.optionToText = {o: t for o, t in zip(configItem.options, texts)} - for text, option in zip(texts, configItem.options): - self.comboBox.addItem(text, userData=option) - - self.comboBox.setCurrentText(self.optionToText[self.qconfig.get(configItem)]) - self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set(self.configItem, self.comboBox.itemData(index)) - - def setValue(self, value): - if value not in self.optionToText: - return - - self.comboBox.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - -class LineEditSettingCard(SettingCard): - """Setting card with LineEdit""" - - textChanged = Signal(str) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.LineEdit = LineEdit(self) - self.LineEdit.setMinimumWidth(250) - self.LineEdit.setPlaceholderText(text) - - self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.configItem.valueChanged.connect(self.setValue) - self.LineEdit.textChanged.connect(self.__textChanged) - - self.setValue(self.qconfig.get(configItem)) - - def __textChanged(self, content: str): - - self.configItem.valueChanged.disconnect(self.setValue) - self.qconfig.set(self.configItem, content.strip()) - self.configItem.valueChanged.connect(self.setValue) - - self.textChanged.emit(content.strip()) - - def setValue(self, content: str): - - self.LineEdit.textChanged.disconnect(self.__textChanged) - self.LineEdit.setText(content.strip()) - self.LineEdit.textChanged.connect(self.__textChanged) - - -class PasswordLineEditSettingCard(SettingCard): - """Setting card with PasswordLineEdit""" - - textChanged = Signal() - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - algorithm: str, - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.algorithm = algorithm - self.qconfig = qconfig - self.configItem = configItem - self.LineEdit = PasswordLineEdit(self) - self.LineEdit.setMinimumWidth(200) - self.LineEdit.setPlaceholderText(text) - if algorithm == "AUTO": - self.LineEdit.setViewPasswordButtonVisible(False) - - self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.configItem.valueChanged.connect(self.setValue) - self.LineEdit.textChanged.connect(self.__textChanged) - - self.setValue(self.qconfig.get(configItem)) - - def __textChanged(self, content: str): - - self.configItem.valueChanged.disconnect(self.setValue) - if self.algorithm == "DPAPI": - self.qconfig.set(self.configItem, Crypto.win_encryptor(content)) - elif self.algorithm == "AUTO": - self.qconfig.set(self.configItem, Crypto.AUTO_encryptor(content)) - self.configItem.valueChanged.connect(self.setValue) - - self.textChanged.emit() - - def setValue(self, content: str): - - self.LineEdit.textChanged.disconnect(self.__textChanged) - if self.algorithm == "DPAPI": - self.LineEdit.setText(Crypto.win_decryptor(content)) - elif self.algorithm == "AUTO": - if Crypto.check_PASSWORD(Config.PASSWORD): - self.LineEdit.setText(Crypto.AUTO_decryptor(content, Config.PASSWORD)) - self.LineEdit.setPasswordVisible(True) - self.LineEdit.setReadOnly(False) - elif Config.PASSWORD: - self.LineEdit.setText("管理密钥错误") - self.LineEdit.setPasswordVisible(True) - self.LineEdit.setReadOnly(True) - else: - self.LineEdit.setText("************") - self.LineEdit.setPasswordVisible(False) - self.LineEdit.setReadOnly(True) - self.LineEdit.textChanged.connect(self.__textChanged) - - -class PathSettingCard(PushSettingCard): - - pathChanged = Signal(Path, Path) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - mode: Union[str, OptionsConfigItem], - text: str, - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - super().__init__(text, icon, title, "未设置", parent) - - self.title = title - self.mode = mode - self.qconfig = qconfig - self.configItem = configItem - - if isinstance(mode, OptionsConfigItem): - - self.ComboBox = ComboBox(self) - self.hBoxLayout.insertWidget(5, self.ComboBox, 0, Qt.AlignRight) - - for option in mode.options: - self.ComboBox.addItem(option, userData=option) - - self.ComboBox.setCurrentText(self.qconfig.get(mode)) - self.ComboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - mode.valueChanged.connect(self.setValue) - - self.setContent(self.qconfig.get(self.configItem)) - - self.clicked.connect(self.ChoosePath) - self.configItem.valueChanged.connect( - lambda: self.setContent(self.qconfig.get(self.configItem)) - ) - - def ChoosePath(self): - """选择文件或文件夹路径""" - - old_path = Path(self.qconfig.get(self.configItem)) - - if self.get_mode() == "文件夹": - - folder = QFileDialog.getExistingDirectory( - self, "选择文件夹", self.qconfig.get(self.configItem) - ) - if folder: - self.qconfig.set(self.configItem, folder) - self.pathChanged.emit(old_path, Path(folder)) - - else: - - file_path, _ = QFileDialog.getOpenFileName( - self, "打开文件", self.qconfig.get(self.configItem), self.get_mode() - ) - if file_path: - file_path = self.analysis_lnk(file_path) - self.qconfig.set(self.configItem, str(file_path)) - self.pathChanged.emit(old_path, file_path) - - def analysis_lnk(self, path: str) -> Path: - """快捷方式解析""" - - lnk_path = Path(path) - if lnk_path.suffix == ".lnk": - try: - shell = win32com.client.Dispatch("WScript.Shell") - shortcut = shell.CreateShortcut(str(lnk_path)) - return Path(shortcut.TargetPath) - except Exception as e: - return lnk_path - else: - return lnk_path - - def get_mode(self) -> str: - """获取当前模式""" - if isinstance(self.mode, OptionsConfigItem): - return self.qconfig.get(self.mode) - return self.mode - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set(self.mode, self.ComboBox.itemData(index)) - - def setValue(self, value): - - self.ComboBox.setCurrentText(value) - self.qconfig.set(self.mode, value) - - -class PushAndSwitchButtonSettingCard(SettingCard): - """Setting card with push & switch button""" - - checkedChanged = Signal(bool) - clicked = Signal() - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.switchButton = SwitchButton("关", self, IndicatorPosition.RIGHT) - self.button = PushButton(text, self) - self.hBoxLayout.addWidget(self.button, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - self.button.clicked.connect(self.clicked) - - if configItem: - self.setValue(self.qconfig.get(configItem)) - configItem.valueChanged.connect(self.setValue) - - # add switch button to layout - self.hBoxLayout.addWidget(self.switchButton, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.switchButton.checkedChanged.connect(self.__onCheckedChanged) - - def __onCheckedChanged(self, isChecked: bool): - """switch button checked state changed slot""" - self.setValue(isChecked) - self.checkedChanged.emit(isChecked) - - def setValue(self, isChecked: bool): - if self.configItem: - self.qconfig.set(self.configItem, isChecked) - - self.switchButton.setChecked(isChecked) - self.switchButton.setText("开" if isChecked else "关") - - -class PasswordLineAndSwitchButtonSettingCard(SettingCard): - """Setting card with PasswordLineEdit and SwitchButton""" - - textChanged = Signal() - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - algorithm: str, - qconfig: QConfig, - configItem_bool: ConfigItem, - configItem_info: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.algorithm = algorithm - self.qconfig = qconfig - self.configItem_bool = configItem_bool - self.configItem_info = configItem_info - self.LineEdit = PasswordLineEdit(self) - self.LineEdit.setMinimumWidth(200) - self.LineEdit.setPlaceholderText(text) - if algorithm == "AUTO": - self.LineEdit.setViewPasswordButtonVisible(False) - self.SwitchButton = SwitchButton(self) - - self.hBoxLayout.addWidget(self.LineEdit, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - self.hBoxLayout.addWidget(self.SwitchButton, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.configItem_info.valueChanged.connect(self.setInfo) - self.LineEdit.textChanged.connect(self.__textChanged) - self.configItem_bool.valueChanged.connect(self.SwitchButton.setChecked) - self.SwitchButton.checkedChanged.connect( - lambda isChecked: self.qconfig.set(self.configItem_bool, isChecked) - ) - - self.setInfo(self.qconfig.get(configItem_info)) - self.SwitchButton.setChecked(self.qconfig.get(configItem_bool)) - - def __textChanged(self, content: str): - - self.configItem_info.valueChanged.disconnect(self.setInfo) - if self.algorithm == "DPAPI": - self.qconfig.set(self.configItem_info, Crypto.win_encryptor(content)) - elif self.algorithm == "AUTO": - self.qconfig.set(self.configItem_info, Crypto.AUTO_encryptor(content)) - self.configItem_info.valueChanged.connect(self.setInfo) - - self.textChanged.emit() - - def setInfo(self, content: str): - - self.LineEdit.textChanged.disconnect(self.__textChanged) - if self.algorithm == "DPAPI": - self.LineEdit.setText(Crypto.win_decryptor(content)) - elif self.algorithm == "AUTO": - if Crypto.check_PASSWORD(Config.PASSWORD): - self.LineEdit.setText(Crypto.AUTO_decryptor(content, Config.PASSWORD)) - self.LineEdit.setPasswordVisible(True) - self.LineEdit.setReadOnly(False) - elif Config.PASSWORD: - self.LineEdit.setText("管理密钥错误") - self.LineEdit.setPasswordVisible(True) - self.LineEdit.setReadOnly(True) - else: - self.LineEdit.setText("************") - self.LineEdit.setPasswordVisible(False) - self.LineEdit.setReadOnly(True) - self.LineEdit.textChanged.connect(self.__textChanged) - - -class PushAndComboBoxSettingCard(SettingCard): - """Setting card with push & combo box""" - - clicked = Signal() - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - texts: List[str], - qconfig: QConfig, - configItem: OptionsConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.comboBox = ComboBox(self) - self.button = PushButton(text, self) - self.hBoxLayout.addWidget(self.button, 0, Qt.AlignRight) - self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - self.button.clicked.connect(self.clicked) - - self.optionToText = {o: t for o, t in zip(configItem.options, texts)} - for text, option in zip(texts, configItem.options): - self.comboBox.addItem(text, userData=option) - - self.comboBox.setCurrentText(self.optionToText[self.qconfig.get(configItem)]) - self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set(self.configItem, self.comboBox.itemData(index)) - - def setValue(self, value): - if value not in self.optionToText: - return - - self.comboBox.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - -class SpinBoxSettingCard(SettingCard): - """Setting card with SpinBox""" - - textChanged = Signal(int) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - range: tuple[int, int], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.SpinBox = SpinBox(self) - self.SpinBox.setRange(range[0], range[1]) - self.SpinBox.setMinimumWidth(150) - - if configItem: - self.setValue(qconfig.get(configItem)) - configItem.valueChanged.connect(self.setValue) - - self.hBoxLayout.addWidget(self.SpinBox, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.SpinBox.valueChanged.connect(self.__valueChanged) - - def __valueChanged(self, value: int): - self.setValue(value) - self.textChanged.emit(value) - - def setValue(self, value: int): - if self.configItem: - self.qconfig.set(self.configItem, value) - - self.SpinBox.setValue(value) - - -class NoOptionComboBoxSettingCard(SettingCard): - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - value: List[str], - texts: List[str], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.comboBox = ComboBox(self) - self.comboBox.setMinimumWidth(250) - self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.comboBox.addItem(text, userData=option) - - self.comboBox.setCurrentText(self.optionToText[self.qconfig.get(configItem)]) - self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set(self.configItem, self.comboBox.itemData(index)) - - def setValue(self, value): - if value not in self.optionToText: - return - - self.comboBox.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - def reLoadOptions(self, value: List[str], texts: List[str]): - - self.comboBox.currentIndexChanged.disconnect(self._onCurrentIndexChanged) - self.comboBox.clear() - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.comboBox.addItem(text, userData=option) - self.comboBox.setCurrentText( - self.optionToText[self.qconfig.get(self.configItem)] - ) - self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - - -class EditableComboBoxSettingCard(SettingCard): - """Setting card with EditableComboBox""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - value: List[str], - texts: List[str], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.comboBox = self._EditableComboBox(self) - self.comboBox.setMinimumWidth(100) - self.hBoxLayout.addWidget(self.comboBox, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.comboBox.addItem(text, userData=option) - - if qconfig.get(configItem) not in self.optionToText: - self.optionToText[qconfig.get(configItem)] = qconfig.get(configItem) - self.comboBox.addItem( - qconfig.get(configItem), userData=qconfig.get(configItem) - ) - - self.comboBox.setCurrentText(self.optionToText[qconfig.get(configItem)]) - self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set( - self.configItem, - ( - self.comboBox.itemData(index) - if self.comboBox.itemData(index) - else self.comboBox.itemText(index) - ), - ) - - def setValue(self, value): - if value not in self.optionToText: - self.optionToText[value] = value - if self.comboBox.findText(value) == -1: - self.comboBox.addItem(value, userData=value) - else: - self.comboBox.setItemData(self.comboBox.findText(value), value) - - self.comboBox.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - def reLoadOptions(self, value: List[str], texts: List[str]): - - self.comboBox.currentIndexChanged.disconnect(self._onCurrentIndexChanged) - self.comboBox.clear() - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.comboBox.addItem(text, userData=option) - if self.qconfig.get(self.configItem) not in self.optionToText: - self.optionToText[self.qconfig.get(self.configItem)] = self.qconfig.get( - self.configItem - ) - self.comboBox.addItem( - self.qconfig.get(self.configItem), - userData=self.qconfig.get(self.configItem), - ) - self.comboBox.setCurrentText( - self.optionToText[self.qconfig.get(self.configItem)] - ) - self.comboBox.currentIndexChanged.connect(self._onCurrentIndexChanged) - - class _EditableComboBox(EditableComboBox): - """EditableComboBox""" - - def __init__(self, parent=None): - super().__init__(parent) - - def _onReturnPressed(self): - if not self.text(): - return - - index = self.findText(self.text()) - if index >= 0 and index != self.currentIndex(): - self._currentIndex = index - self.currentIndexChanged.emit(index) - elif index == -1: - self.addItem(self.text()) - self.setCurrentIndex(self.count() - 1) - self.currentIndexChanged.emit(self.count() - 1) - - -class SpinBoxWithPlanSettingCard(SpinBoxSettingCard): - - textChanged = Signal(int) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - range: tuple[int, int], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, range, qconfig, configItem, parent) - - self.configItem_plan = None - - self.LineEdit = LineEdit(self) - self.LineEdit.setMinimumWidth(150) - self.LineEdit.setReadOnly(True) - self.LineEdit.setVisible(False) - - self.hBoxLayout.insertWidget(5, self.LineEdit, 0, Qt.AlignRight) - - def setText(self, value: int) -> None: - self.LineEdit.setText(str(value)) - - def switch_mode(self, mode: str) -> None: - """切换模式""" - - if mode == "固定": - - self.LineEdit.setVisible(False) - self.SpinBox.setVisible(True) - - elif mode == "计划": - - self.SpinBox.setVisible(False) - self.LineEdit.setVisible(True) - - def change_plan(self, configItem_plan: ConfigItem) -> None: - """切换计划""" - - if self.configItem_plan is not None: - self.configItem_plan.valueChanged.disconnect(self.setText) - self.configItem_plan = configItem_plan - self.configItem_plan.valueChanged.connect(self.setText) - self.setText(self.qconfig.get(self.configItem_plan)) - - -class ComboBoxWithPlanSettingCard(ComboBoxSettingCard): - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - texts: List[str], - qconfig: QConfig, - configItem: OptionsConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, texts, qconfig, configItem, parent) - - self.configItem_plan = None - - self.LineEdit = LineEdit(self) - self.LineEdit.setMinimumWidth(150) - self.LineEdit.setReadOnly(True) - self.LineEdit.setVisible(False) - - self.hBoxLayout.insertWidget(5, self.LineEdit, 0, Qt.AlignRight) - - def setText(self, value: str) -> None: - - if value not in self.optionToText: - self.optionToText[value] = value - - self.LineEdit.setText(self.optionToText[value]) - - def switch_mode(self, mode: str) -> None: - """切换模式""" - - if mode == "固定": - - self.LineEdit.setVisible(False) - self.comboBox.setVisible(True) - - elif mode == "计划": - - self.comboBox.setVisible(False) - self.LineEdit.setVisible(True) - - def change_plan(self, configItem_plan: ConfigItem) -> None: - """切换计划""" - - if self.configItem_plan is not None: - self.configItem_plan.valueChanged.disconnect(self.setText) - self.configItem_plan = configItem_plan - self.configItem_plan.valueChanged.connect(self.setText) - self.setText(self.qconfig.get(self.configItem_plan)) - - -class EditableComboBoxWithPlanSettingCard(EditableComboBoxSettingCard): - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - value: List[str], - texts: List[str], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__( - icon, title, content, value, texts, qconfig, configItem, parent - ) - - self.configItem_plan = None - - self.LineEdit = LineEdit(self) - self.LineEdit.setMinimumWidth(150) - self.LineEdit.setReadOnly(True) - self.LineEdit.setVisible(False) - - self.hBoxLayout.insertWidget(5, self.LineEdit, 0, Qt.AlignRight) - - def setText(self, value: str) -> None: - - if value not in self.optionToText: - self.optionToText[value] = value - - self.LineEdit.setText(self.optionToText[value]) - - def switch_mode(self, mode: str) -> None: - """切换模式""" - - if mode == "固定": - - self.LineEdit.setVisible(False) - self.comboBox.setVisible(True) - - elif mode == "计划": - - self.comboBox.setVisible(False) - self.LineEdit.setVisible(True) - - def change_plan(self, configItem_plan: ConfigItem) -> None: - """切换计划""" - - if self.configItem_plan is not None: - self.configItem_plan.valueChanged.disconnect(self.setText) - self.configItem_plan = configItem_plan - self.configItem_plan.valueChanged.connect(self.setText) - self.setText(self.qconfig.get(self.configItem_plan)) - - -class TimeEditSettingCard(SettingCard): - - enabledChanged = Signal(bool) - timeChanged = Signal(str) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - qconfig: QConfig, - configItem_bool: ConfigItem, - configItem_time: ConfigItem, - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem_bool = configItem_bool - self.configItem_time = configItem_time - self.CheckBox = CheckBox(self) - self.CheckBox.setTristate(False) - self.TimeEdit = TimeEdit(self) - self.TimeEdit.setDisplayFormat("HH:mm") - self.TimeEdit.setMinimumWidth(150) - - if configItem_bool: - self.setValue_bool(qconfig.get(configItem_bool)) - configItem_bool.valueChanged.connect(self.setValue_bool) - - if configItem_time: - self.setValue_time(qconfig.get(configItem_time)) - configItem_time.valueChanged.connect(self.setValue_time) - - self.hBoxLayout.addWidget(self.CheckBox, 0, Qt.AlignRight) - self.hBoxLayout.addWidget(self.TimeEdit, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - self.CheckBox.stateChanged.connect(self.__enableChanged) - self.TimeEdit.timeChanged.connect(self.__timeChanged) - - def __timeChanged(self, value: QTime): - self.setValue_time(value.toString("HH:mm")) - self.timeChanged.emit(value.toString("HH:mm")) - - def __enableChanged(self, value: int): - if value == 0: - self.setValue_bool(False) - self.enabledChanged.emit(False) - else: - self.setValue_bool(True) - self.enabledChanged.emit(True) - - def setValue_bool(self, value: bool): - if self.configItem_bool: - self.qconfig.set(self.configItem_bool, value) - - self.CheckBox.setChecked(value) - - def setValue_time(self, value: str): - if self.configItem_time: - self.qconfig.set(self.configItem_time, value) - - self.TimeEdit.setTime(QTime.fromString(value, "HH:mm")) - - -class SubLableSettingCard(SettingCard): - """Setting card with Sub's Lable""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - qconfig: QConfig, - configItems: Dict[str, ConfigItem], - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItems = configItems - self.Lable = SubtitleLabel(self) - - if configItems: - for configItem in configItems.values(): - configItem.valueChanged.connect(self.setValue) - self.setValue() - - self.hBoxLayout.addWidget(self.Lable, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - def setValue(self): - - text_list = [] - - if self.configItems: - - text_list.append( - f"今日已代理{self.qconfig.get(self.configItems["ProxyTimes"])}次" - if Config.server_date().strftime("%Y-%m-%d") - == self.qconfig.get(self.configItems["LastProxyDate"]) - else "今日未进行代理" - ) - - self.Lable.setText(" | ".join(text_list)) - - -class UserLableSettingCard(SettingCard): - """Setting card with User's Lable""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - qconfig: QConfig, - configItems: Dict[str, ConfigItem], - parent=None, - ): - - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItems = configItems - self.Lable = SubtitleLabel(self) - - if configItems: - for configItem in configItems.values(): - configItem.valueChanged.connect(self.setValue) - self.setValue() - - self.hBoxLayout.addWidget(self.Lable, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - def setValue(self): - - text_list = [] - - if self.configItems: - - if not self.qconfig.get(self.configItems["IfPassCheck"]): - text_list.append("未通过人工排查") - text_list.append( - f"今日已代理{self.qconfig.get(self.configItems["ProxyTimes"])}次" - if Config.server_date().strftime("%Y-%m-%d") - == self.qconfig.get(self.configItems["LastProxyDate"]) - else "今日未进行代理" - ) - text_list.append( - "本周剿灭已完成" - if datetime.strptime( - self.qconfig.get(self.configItems["LastAnnihilationDate"]), - "%Y-%m-%d", - ).isocalendar()[:2] - == Config.server_date().isocalendar()[:2] - else "本周剿灭未完成" - ) - if self.qconfig.get(self.configItems["IfSkland"]): - text_list.append( - "森空岛已签到" - if datetime.now().strftime("%Y-%m-%d") - == self.qconfig.get(self.configItems["LastSklandDate"]) - else "森空岛未签到" - ) - - self.Lable.setText(" | ".join(text_list)) - - -class UserTaskSettingCard(PushSettingCard): - """Setting card with User's Task""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - qconfig: QConfig, - configItems: Dict[str, ConfigItem], - parent=None, - ): - - super().__init__(text, icon, title, content, parent) - self.qconfig = qconfig - self.configItems = configItems - self.Lable = SubtitleLabel(self) - - if configItems: - for config_item in configItems.values(): - config_item.valueChanged.connect(self.setValues) - self.setValues() - - self.hBoxLayout.addWidget(self.Lable, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - def setValues(self): - - text_list = [] - - if self.configItems: - - if self.qconfig.get(self.configItems["IfWakeUp"]): - text_list.append("开始唤醒") - if self.qconfig.get(self.configItems["IfRecruiting"]): - text_list.append("自动公招") - if self.qconfig.get(self.configItems["IfBase"]): - text_list.append("基建换班") - if self.qconfig.get(self.configItems["IfCombat"]): - text_list.append("刷理智") - if self.qconfig.get(self.configItems["IfMall"]): - text_list.append("获取信用及购物") - if self.qconfig.get(self.configItems["IfMission"]): - text_list.append("领取奖励") - if self.qconfig.get(self.configItems["IfAutoRoguelike"]): - text_list.append("自动肉鸽") - if self.qconfig.get(self.configItems["IfReclamation"]): - text_list.append("生息演算") - - if text_list: - self.setContent(f"任务序列:{" - ".join(text_list)}") - else: - self.setContent("未启用任何任务项") - - -class UserNoticeSettingCard(PushAndSwitchButtonSettingCard): - """Setting card with User's Notice""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - text: str, - qconfig: QConfig, - configItem: ConfigItem, - configItems: Dict[str, ConfigItem], - parent=None, - ): - - super().__init__(icon, title, content, text, qconfig, configItem, parent) - self.qconfig = qconfig - self.configItems = configItems - self.Lable = SubtitleLabel(self) - - if configItems: - for config_item in configItems.values(): - config_item.valueChanged.connect(self.setValues) - self.setValues() - - self.hBoxLayout.addWidget(self.Lable, 0, Qt.AlignRight) - self.hBoxLayout.addSpacing(16) - - def setValues(self): - - def short_str(s: str) -> str: - if s.startswith(("SC", "sc")): - # SendKey:首4 + 末4 - return f"{s[:4]}***{s[-4:]}" if len(s) > 8 else s - - elif s.startswith(("http://", "https://")): - # Webhook URL:域名 + 路径尾3 - parsed_url = urlparse(s) - domain = parsed_url.netloc - path_tail = ( - parsed_url.path[-3:] - if len(parsed_url.path) > 3 - else parsed_url.path - ) - return f"{domain}***{path_tail}" - - elif "@" in s: - # 邮箱:@前3/6 + 域名 - username, domain = s.split("@", 1) - displayed_name = f"{username[:3]}***" if len(username) > 6 else username - return f"{displayed_name}@{domain}" - - else: - # 普通字符串:末尾3字符 - return f"***{s[-3:]}" if len(s) > 3 else s - - text_list = [] - - if self.configItems: - - if not ( - self.qconfig.get(self.configItems["IfSendStatistic"]) - or ( - "IfSendSixStar" in self.configItems - and self.qconfig.get(self.configItems["IfSendSixStar"]) - ) - ): - text_list.append("未启用任何通知项") - - if self.qconfig.get(self.configItems["IfSendStatistic"]): - text_list.append("统计信息已启用") - if "IfSendSixStar" in self.configItems and self.qconfig.get( - self.configItems["IfSendSixStar"] - ): - text_list.append("六星喜报已启用") - - if self.qconfig.get(self.configItems["IfSendMail"]): - text_list.append( - f"邮箱通知:{short_str(self.qconfig.get(self.configItems["ToAddress"]))}" - ) - if self.qconfig.get(self.configItems["IfServerChan"]): - text_list.append( - f"Server酱通知:{short_str(self.qconfig.get(self.configItems["ServerChanKey"]))}" - ) - if self.qconfig.get(self.configItems["IfCompanyWebHookBot"]): - text_list.append( - f"企业微信通知:{short_str(self.qconfig.get(self.configItems["CompanyWebHookBotUrl"]))}" - ) - - self.setContent(" | ".join(text_list)) - - -class StatusSwitchSetting(SwitchButton): - - def __init__( - self, - qconfig: QConfig, - configItem_check: ConfigItem, - configItem_enable: ConfigItem, - parent=None, - ): - super().__init__(parent) - self.qconfig = qconfig - self.configItem_check = configItem_check - self.configItem_enable = configItem_enable - self.setOffText("") - self.setOnText("") - - if configItem_check: - self.setValue(self.qconfig.get(configItem_check)) - configItem_check.valueChanged.connect(self.setValue) - if configItem_enable: - self.setEnabled(self.qconfig.get(configItem_enable)) - configItem_enable.valueChanged.connect(self.setEnabled) - - self.checkedChanged.connect(self.setValue) - - def setValue(self, isChecked: bool): - if self.configItem_check: - self.qconfig.set(self.configItem_check, isChecked) - - self.setChecked(isChecked) - - -class ComboBoxSetting(ComboBox): - - def __init__( - self, - texts: List[str], - qconfig: QConfig, - configItem: OptionsConfigItem, - parent=None, - ): - - super().__init__(parent) - self.qconfig = qconfig - self.configItem = configItem - - self.optionToText = {o: t for o, t in zip(configItem.options, texts)} - for text, option in zip(texts, configItem.options): - self.addItem(text, userData=option) - - self.setCurrentText(self.optionToText[self.qconfig.get(configItem)]) - self.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set(self.configItem, self.itemData(index)) - - def setValue(self, value): - if value not in self.optionToText: - return - - self.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - -class NoOptionComboBoxSetting(ComboBox): - - def __init__( - self, - value: List[str], - texts: List[str], - qconfig: QConfig, - configItem: OptionsConfigItem, - parent=None, - ): - - super().__init__(parent) - self.qconfig = qconfig - self.configItem = configItem - - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.addItem(text, userData=option) - - self.setCurrentText(self.optionToText[self.qconfig.get(configItem)]) - self.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set(self.configItem, self.itemData(index)) - - def setValue(self, value): - if value not in self.optionToText: - return - - self.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - def reLoadOptions(self, value: List[str], texts: List[str]): - - self.currentIndexChanged.disconnect(self._onCurrentIndexChanged) - self.clear() - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.addItem(text, userData=option) - self.setCurrentText(self.optionToText[self.qconfig.get(self.configItem)]) - self.currentIndexChanged.connect(self._onCurrentIndexChanged) - - -class EditableComboBoxSetting(EditableComboBox): - - def __init__( - self, - value: List[str], - texts: List[str], - qconfig: QConfig, - configItem: OptionsConfigItem, - parent=None, - ): - - super().__init__(parent) - self.qconfig = qconfig - self.configItem = configItem - - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.addItem(text, userData=option) - - if qconfig.get(configItem) not in self.optionToText: - self.optionToText[qconfig.get(configItem)] = qconfig.get(configItem) - self.addItem(qconfig.get(configItem), userData=qconfig.get(configItem)) - - self.setCurrentText(self.optionToText[qconfig.get(configItem)]) - self.currentIndexChanged.connect(self._onCurrentIndexChanged) - configItem.valueChanged.connect(self.setValue) - - def _onCurrentIndexChanged(self, index: int): - - self.qconfig.set( - self.configItem, - (self.itemData(index) if self.itemData(index) else self.itemText(index)), - ) - - def setValue(self, value): - if value not in self.optionToText: - self.optionToText[value] = value - if self.findText(value) == -1: - self.addItem(value, userData=value) - else: - self.setItemData(self.findText(value), value) - - self.setCurrentText(self.optionToText[value]) - self.qconfig.set(self.configItem, value) - - def reLoadOptions(self, value: List[str], texts: List[str]): - - self.currentIndexChanged.disconnect(self._onCurrentIndexChanged) - self.clear() - self.optionToText = {o: t for o, t in zip(value, texts)} - for text, option in zip(texts, value): - self.addItem(text, userData=option) - if self.qconfig.get(self.configItem) not in self.optionToText: - self.optionToText[self.qconfig.get(self.configItem)] = self.qconfig.get( - self.configItem - ) - self.addItem( - self.qconfig.get(self.configItem), - userData=self.qconfig.get(self.configItem), - ) - self.setCurrentText(self.optionToText[self.qconfig.get(self.configItem)]) - self.currentIndexChanged.connect(self._onCurrentIndexChanged) - - def _onReturnPressed(self): - if not self.text(): - return - - index = self.findText(self.text()) - if index >= 0 and index != self.currentIndex(): - self._currentIndex = index - self.currentIndexChanged.emit(index) - elif index == -1: - self.addItem(self.text()) - self.setCurrentIndex(self.count() - 1) - self.currentIndexChanged.emit(self.count() - 1) - - -class SpinBoxSetting(SpinBox): - - def __init__( - self, - range: tuple[int, int], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - - super().__init__(parent) - self.qconfig = qconfig - self.configItem = configItem - self.setRange(range[0], range[1]) - - if configItem: - self.set_value(qconfig.get(configItem)) - configItem.valueChanged.connect(self.set_value) - - self.valueChanged.connect(self.set_value) - - def set_value(self, value: int): - if self.configItem: - self.qconfig.set(self.configItem, value) - - self.setValue(value) - - -class HistoryCard(HeaderCardWidget): - - def __init__(self, qconfig: QConfig, configItem: ConfigItem, parent=None): - super().__init__(parent) - self.setTitle("历史运行记录") - - self.qconfig = qconfig - self.configItem = configItem - - self.text = TextBrowser() - self.text.setMinimumHeight(300) - - if configItem: - self.setValue(self.qconfig.get(configItem)) - configItem.valueChanged.connect(self.setValue) - - self.viewLayout.addWidget(self.text) - - def setValue(self, content: str): - if self.configItem: - self.qconfig.set(self.configItem, content) - - self.text.setPlainText(content) - - -class UrlItem(QWidget): - """Url item""" - - removed = Signal(QWidget) - - def __init__(self, url: str, parent=None): - super().__init__(parent=parent) - self.url = url - self.hBoxLayout = QHBoxLayout(self) - self.folderLabel = QLabel(url, self) - self.removeButton = ToolButton(FluentIcon.CLOSE, self) - - self.removeButton.setFixedSize(39, 29) - self.removeButton.setIconSize(QSize(12, 12)) - - self.setFixedHeight(53) - self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Fixed) - self.hBoxLayout.setContentsMargins(48, 0, 60, 0) - self.hBoxLayout.addWidget(self.folderLabel, 0, Qt.AlignLeft) - self.hBoxLayout.addSpacing(16) - self.hBoxLayout.addStretch(1) - self.hBoxLayout.addWidget(self.removeButton, 0, Qt.AlignRight) - self.hBoxLayout.setAlignment(Qt.AlignVCenter) - - self.removeButton.clicked.connect(lambda: self.removed.emit(self)) - - -class UrlListSettingCard(ExpandSettingCard): - """Url list setting card""" - - urlChanged = Signal(list) - - def __init__( - self, - icon: Union[str, QIcon, FluentIconBase], - title: str, - content: Union[str, None], - qconfig: QConfig, - configItem: ConfigItem, - parent=None, - ): - super().__init__(icon, title, content, parent) - self.qconfig = qconfig - self.configItem = configItem - self.addUrlButton = PushButton("添加代理网址", self) - - self.urls: List[str] = self.qconfig.get(configItem).copy() - self.__initWidget() - - def __initWidget(self): - self.addWidget(self.addUrlButton) - - # initialize layout - self.viewLayout.setSpacing(0) - self.viewLayout.setAlignment(Qt.AlignTop) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - for url in self.urls: - self.__addUrlItem(url) - - self.addUrlButton.clicked.connect(self.__showUrlDialog) - - def __showUrlDialog(self): - """show url dialog""" - - choice = LineEditMessageBox( - self.window(), "添加代理网址", "请输入代理网址", "明文" - ) - if choice.exec() and self.__validate(choice.input.text()): - - if choice.input.text()[-1] == "/": - url = choice.input.text() - else: - url = f"{choice.input.text()}/" - - if url in self.urls: - return - - self.__addUrlItem(url) - self.urls.append(url) - self.qconfig.set(self.configItem, self.urls) - self.urlChanged.emit(self.urls) - - def __addUrlItem(self, url: str): - """add url item""" - item = UrlItem(url, self.view) - item.removed.connect(self.__showConfirmDialog) - self.viewLayout.addWidget(item) - item.show() - self._adjustViewSize() - - def __showConfirmDialog(self, item: UrlItem): - """show confirm dialog""" - - choice = MessageBox( - "确认", f"确定要删除 {item.url} 代理网址吗?", self.window() - ) - if choice.exec(): - self.__removeUrl(item) - - def __removeUrl(self, item: UrlItem): - """remove folder""" - if item.url not in self.urls: - return - - self.urls.remove(item.url) - self.viewLayout.removeWidget(item) - item.deleteLater() - self._adjustViewSize() - - self.urlChanged.emit(self.urls) - self.qconfig.set(self.configItem, self.urls) - - def __validate(self, value): - - try: - result = urlparse(value) - return all([result.scheme, result.netloc]) - except ValueError: - return False - - -class StatefulItemCard(CardWidget): - - def __init__(self, item: list, parent=None): - super().__init__(parent) - - self.Layout = QHBoxLayout(self) - - self.Label = BodyLabel(item[0], self) - self.icon = IconWidget(FluentIcon.MORE, self) - self.icon.setFixedSize(16, 16) - self.update_status(item[1]) - - self.Layout.addWidget(self.icon) - self.Layout.addWidget(self.Label) - self.Layout.addStretch(1) - - def update_status(self, status: str): - - if status == "完成": - self.icon.setIcon(FluentIcon.ACCEPT) - self.Label.setTextColor("#0eb840", "#0eb840") - elif status == "等待": - self.icon.setIcon(FluentIcon.MORE) - self.Label.setTextColor("#161823", "#e3f9fd") - elif status == "运行": - self.icon.setIcon(FluentIcon.PLAY) - self.Label.setTextColor("#177cb0", "#70f3ff") - elif status == "跳过": - self.icon.setIcon(FluentIcon.REMOVE) - self.Label.setTextColor("#75878a", "#7397ab") - elif status == "异常": - self.icon.setIcon(FluentIcon.CLOSE) - self.Label.setTextColor("#ff2121", "#ff2121") - - -class QuantifiedItemCard(CardWidget): - - def __init__(self, item: list, parent=None): - super().__init__(parent) - - self.Layout = QHBoxLayout(self) - - self.Name = BodyLabel(item[0], self) - self.Numb = BodyLabel(str(item[1]), self) - - self.Layout.addWidget(self.Name) - self.Layout.addStretch(1) - self.Layout.addWidget(self.Numb) - - -class PivotArea(ScrollArea): - def __init__(self, parent=None): - super().__init__(parent) - - # 创建中间容器并设置布局 - self.center_container = QWidget() - self.center_layout = QHBoxLayout(self.center_container) - self.center_layout.setContentsMargins(0, 0, 0, 0) - self.center_layout.setSpacing(0) - self.center_container.setStyleSheet("background: transparent; border: none;") - self.center_container.setFixedHeight(45) - - self.pivot = self._Pivot(self) - self.pivot.ItemNumbChanged.connect( - lambda: QTimer.singleShot( - 100, - lambda: ( - self.center_container.setFixedWidth( - max(self.width() - 2, self.pivot.width()) - ) - ), - ) - ) - - self.center_layout.addStretch(1) - self.center_layout.addWidget(self.pivot) - self.center_layout.addStretch(1) - - self.setWidgetResizable(False) - self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff) - self.viewport().setCursor(Qt.ArrowCursor) - self.setStyleSheet("background: transparent; border: none;") - self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) - self.setWidget(self.center_container) - - def wheelEvent(self, event): - scroll_bar = self.horizontalScrollBar() - if scroll_bar.maximum() > 0: - delta = event.angleDelta().y() - scroll_bar.setValue(scroll_bar.value() - delta // 15) - event.ignore() - - def resizeEvent(self, event): - super().resizeEvent(event) - - self.center_container.setFixedWidth(max(self.width() - 2, self.pivot.width())) - QTimer.singleShot( - 100, - lambda: ( - self.center_container.setFixedWidth( - max(self.width() - 2, self.pivot.width()) - ) - ), - ) - - class _Pivot(Pivot): - - ItemNumbChanged = Signal() - - def __init__(self, parent=None): - super().__init__(parent) - - def insertWidget( - self, index: int, routeKey: str, widget: PivotItem, onClick=None - ): - super().insertWidget(index, routeKey, widget, onClick) - self.ItemNumbChanged.emit() - - def removeWidget(self, routeKey: str): - super().removeWidget(routeKey) - self.ItemNumbChanged.emit() - - def clear(self): - super().clear() - self.ItemNumbChanged.emit() - - -class QuickExpandGroupCard(ExpandGroupSettingCard): - """全局配置""" - - def __init__( - self, - icon: Union[str, QIcon, FluentIcon], - title: str, - content: str = None, - parent=None, - ): - super().__init__(icon, title, content, parent) - - def setExpand(self, isExpand: bool): - """set the expand status of card""" - if self.isExpand == isExpand: - return - - # update style sheet - self.isExpand = isExpand - self.setProperty("isExpand", isExpand) - self.setStyle(QApplication.style()) - - # start expand animation - if isExpand: - h = self.viewLayout.sizeHint().height() - self.verticalScrollBar().setValue(h) - self.expandAni.setStartValue(h) - self.expandAni.setEndValue(0) - self.expandAni.start() - else: - self.setFixedHeight(self.viewportMargins().top()) - - self.card.expandButton.setExpand(isExpand) - - -class IconButton(TransparentToolButton): - """包含下拉框的自定义设置卡片类。""" - - @singledispatchmethod - def __init__(self, parent: QWidget = None): - TransparentToolButton.__init__(self, parent) - - self._tooltip: Optional[TeachingTip] = None - - @__init__.register - def _(self, icon: Union[str, QIcon, FluentIconBase], parent: QWidget = None): - self.__init__(parent) - self.setIcon(icon) - - @__init__.register - def _( - self, - icon: Union[str, QIcon, FluentIconBase], - isTooltip: bool, - tip_title: str, - tip_content: Union[str, None], - parent: QWidget = None, - ): - self.__init__(parent) - self.setIcon(icon) - - # 处理工具提示 - if isTooltip: - self.installEventFilter(self) - - self.tip_title: str = tip_title - self.tip_content: str = tip_content - - def eventFilter(self, obj, event: QEvent) -> bool: - """处理鼠标事件。""" - if event.type() == QEvent.Type.Enter: - self._show_tooltip() - elif event.type() == QEvent.Type.Leave: - self._hide_tooltip() - return super().eventFilter(obj, event) - - def _show_tooltip(self) -> None: - """显示工具提示。""" - self._tooltip = TeachingTip.create( - target=self, - title=self.tip_title, - content=self.tip_content, - tailPosition=TeachingTipTailPosition.RIGHT, - isClosable=False, - duration=-1, - parent=self, - ) - # 设置偏移 - if self._tooltip: - tooltip_pos = self.mapToGlobal(self.rect().topRight()) - - tooltip_pos.setX( - tooltip_pos.x() - self._tooltip.size().width() - 40 - ) # 水平偏移 - tooltip_pos.setY( - tooltip_pos.y() - self._tooltip.size().height() / 2 + 35 - ) # 垂直偏移 - - self._tooltip.move(tooltip_pos) - - def _hide_tooltip(self) -> None: - """隐藏工具提示。""" - if self._tooltip: - self._tooltip.close() - self._tooltip = None - - def __hash__(self): - return id(self) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return NotImplemented - return self is other - - -class Banner(QWidget): - """展示带有圆角的固定大小横幅小部件""" - - def __init__(self, image_path: str = None, parent=None): - QWidget.__init__(self, parent) - self.image_path = None - self.banner_image = None - self.scaled_image = None - - if image_path: - self.set_banner_image(image_path) - - def set_banner_image(self, image_path: str): - """设置横幅图片""" - self.image_path = image_path - self.banner_image = self.load_banner_image(image_path) - self.update_scaled_image() - - def load_banner_image(self, image_path: str) -> QPixmap: - """加载横幅图片,或创建渐变备用图片""" - if os.path.isfile(image_path): - return QPixmap(image_path) - return self._create_fallback_image() - - def _create_fallback_image(self): - """创建渐变备用图片""" - fallback_image = QPixmap(2560, 1280) # 使用原始图片的大小 - fallback_image.fill(Qt.GlobalColor.gray) - return fallback_image - - def update_scaled_image(self): - """按高度缩放图片,宽度保持比例,超出裁剪""" - if self.banner_image: - self.scaled_image = self.banner_image.scaled( - self.size(), - Qt.AspectRatioMode.KeepAspectRatioByExpanding, - Qt.TransformationMode.SmoothTransformation, - ) - self.update() - - def paintEvent(self, event): - """重载 paintEvent 以绘制缩放后的图片""" - if self.scaled_image: - painter = QPainter(self) - painter.setRenderHint(QPainter.RenderHint.Antialiasing) - painter.setRenderHint(QPainter.RenderHint.SmoothPixmapTransform) - - # 创建圆角路径 - path = QPainterPath() - path.addRoundedRect(self.rect(), 20, 20) - painter.setClipPath(path) - - # 计算绘制位置,使图片居中 - x = (self.width() - self.scaled_image.width()) // 2 - y = (self.height() - self.scaled_image.height()) // 2 - - # 绘制缩放后的图片 - painter.drawPixmap(x, y, self.scaled_image) - - def resizeEvent(self, event): - """重载 resizeEvent 以更新缩放后的图片""" - self.update_scaled_image() - QWidget.resizeEvent(self, event) - - def set_percentage_size(self, width_percentage, height_percentage): - """设置 Banner 的大小为窗口大小的百分比""" - parent = self.parentWidget() - if parent: - new_width = int(parent.width() * width_percentage) - new_height = int(parent.height() * height_percentage) - self.setFixedSize(new_width, new_height) - self.update_scaled_image() diff --git a/app/ui/__init__.py b/app/ui/__init__.py deleted file mode 100644 index ea7d35a..0000000 --- a/app/ui/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA图形化界面包 -v4.4 -作者:DLmaster_361 -""" - -__version__ = "4.2.0" -__author__ = "DLmaster361 " -__license__ = "GPL-3.0 license" - -from .main_window import AUTO_MAA -from .Widget import ProgressRingMessageBox - -__all__ = ["AUTO_MAA", "ProgressRingMessageBox"] diff --git a/app/ui/dispatch_center.py b/app/ui/dispatch_center.py deleted file mode 100644 index 3993dfa..0000000 --- a/app/ui/dispatch_center.py +++ /dev/null @@ -1,625 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA调度中枢界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QStackedWidget, - QHBoxLayout, -) -from qfluentwidgets import ( - BodyLabel, - CardWidget, - ScrollArea, - FluentIcon, - HeaderCardWidget, - FluentIcon, - TextBrowser, - ComboBox, - SubtitleLabel, - PushButton, -) -from PySide6.QtGui import QTextCursor -from typing import List, Dict - - -from app.core import Config, TaskManager, Task, MainInfoBar, logger -from .Widget import StatefulItemCard, ComboBoxMessageBox, PivotArea - - -class DispatchCenter(QWidget): - - def __init__(self, parent=None): - super().__init__(parent) - - 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["主调度台"]) - self.pivot.addItem( - routeKey="主调度台", - text="主调度台", - onClick=self.update_top_bar, - icon=FluentIcon.CAFE, - ) - - # 顶栏组合 - h_layout = QHBoxLayout() - h_layout.addWidget(self.multi_button) - h_layout.addWidget(self.pivotArea) - h_layout.addWidget(BodyLabel("全部完成后", self)) - h_layout.addWidget(self.power_combox) - h_layout.setContentsMargins(11, 5, 11, 0) - - self.Layout = QVBoxLayout(self) - self.Layout.addLayout(h_layout) - self.Layout.addWidget(self.stackedWidget) - self.Layout.setContentsMargins(0, 0, 0, 0) - - self.pivot.currentItemChanged.connect( - lambda index: self.stackedWidget.setCurrentWidget(self.script_list[index]) - ) - - def add_board(self, task: Task) -> None: - """ - 为任务添加一个调度台界面并绑定信号 - - :param task: 任务对象 - """ - - logger.info(f"添加调度台:{task.name}", module="调度中枢") - - dispatch_box = self.DispatchBox(task.name, self) - - dispatch_box.top_bar.main_button.clicked.connect( - lambda: TaskManager.stop_task(task.name) - ) - - task.create_task_list.connect(dispatch_box.info.task.create_task) - task.create_user_list.connect(dispatch_box.info.user.create_user) - task.update_task_list.connect(dispatch_box.info.task.update_task) - task.update_user_list.connect(dispatch_box.info.user.update_user) - task.update_log_text.connect(dispatch_box.info.log_text.text.setText) - task.accomplish.connect(lambda: self.del_board(f"调度台_{task.name}")) - - self.script_list[f"调度台_{task.name}"] = dispatch_box - - self.stackedWidget.addWidget(self.script_list[f"调度台_{task.name}"]) - - 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('_主调度台','')}模式" - ) - self.script_list["主调度台"].top_bar.Lable.show() - self.script_list["主调度台"].top_bar.object.hide() - self.script_list["主调度台"].top_bar.mode.hide() - self.script_list["主调度台"].top_bar.main_button.clicked.disconnect() - self.script_list["主调度台"].top_bar.main_button.setText("中止任务") - self.script_list["主调度台"].top_bar.main_button.clicked.connect( - lambda: TaskManager.stop_task(task.name) - ) - task.create_task_list.connect( - self.script_list["主调度台"].info.task.create_task - ) - task.create_user_list.connect( - self.script_list["主调度台"].info.user.create_user - ) - task.update_task_list.connect( - self.script_list["主调度台"].info.task.update_task - ) - task.update_user_list.connect( - self.script_list["主调度台"].info.user.update_user - ) - task.update_log_text.connect( - self.script_list["主调度台"].info.log_text.text.setText - ) - task.accomplish.connect( - 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() - self.script_list["主调度台"].top_bar.mode.show() - self.script_list["主调度台"].top_bar.main_button.clicked.disconnect() - self.script_list["主调度台"].top_bar.main_button.setText("开始任务") - self.script_list["主调度台"].top_bar.main_button.clicked.connect( - self.script_list["主调度台"].top_bar.start_main_task - ) - if len(logs) > 0: - history = "" - for log in logs: - history += ( - f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n" - ) - self.script_list["主调度台"].info.log_text.text.setText(history) - else: - self.script_list["主调度台"].info.log_text.text.setText("没有任务被执行") - - logger.success(f"主调度台成功断开:{name}", module="调度中枢") - - def update_top_bar(self): - """更新顶栏""" - - self.script_list["主调度台"].top_bar.object.clear() - - for name, info in Config.queue_dict.items(): - self.script_list["主调度台"].top_bar.object.addItem( - ( - "队列" - if info["Config"].get(info["Config"].QueueSet_Name) == "" - else f"队列 - {info["Config"].get(info["Config"].QueueSet_Name)}" - ), - userData=name, - ) - - for name, info in Config.script_dict.items(): - self.script_list["主调度台"].top_bar.object.addItem( - ( - f"实例 - {info['Type']}" - if info["Config"].get_name() == "" - else f"实例 - {info['Type']} - {info['Config'].get_name()}" - ), - userData=name, - ) - - if len(Config.queue_dict) == 1: - self.script_list["主调度台"].top_bar.object.setCurrentIndex(0) - elif len(Config.script_dict) == 1: - self.script_list["主调度台"].top_bar.object.setCurrentIndex( - len(Config.queue_dict) - ) - else: - self.script_list["主调度台"].top_bar.object.setCurrentIndex(-1) - - self.script_list["主调度台"].top_bar.mode.clear() - self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"]) - self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0) - - def update_power_sign(self) -> None: - """更新电源设置""" - - mode_book = { - "NoAction": "无动作", - "KillSelf": "退出软件", - "Sleep": "睡眠", - "Hibernate": "休眠", - "Shutdown": "关机", - "ShutdownForce": "关机(强制)", - } - self.power_combox.currentIndexChanged.disconnect() - self.power_combox.setCurrentText(mode_book[Config.power_sign]) - self.power_combox.currentIndexChanged.connect(self.set_power_sign) - - def set_power_sign(self) -> None: - """设置所有任务完成后动作""" - - if not Config.running_list: - - self.power_combox.currentIndexChanged.disconnect() - self.power_combox.setCurrentText("无动作") - self.power_combox.currentIndexChanged.connect(self.set_power_sign) - logger.warning("没有正在运行的任务,无法设置任务完成后动作") - MainInfoBar.push_info_bar( - "warning", "没有正在运行的任务", "无法设置任务完成后动作", 5000 - ) - - else: - - Config.set_power_sign(self.power_combox.currentData()) - - def start_multi_task(self) -> None: - """开始多开任务""" - - # 获取所有可用的队列和实例 - text_list = [] - data_list = [] - for name, info in Config.queue_dict.items(): - if name in Config.running_list: - continue - text_list.append( - "队列" - 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.script_dict.items(): - if name in Config.running_list: - continue - text_list.append( - f"实例 - {info['Type']}" - if info["Config"].get_name() == "" - else f"实例 - {info['Type']} - {info['Config'].get_name()}" - ) - data_list.append(name) - - choice = ComboBoxMessageBox( - self.window(), - "选择一个对象以添加相应多开任务", - ["选择调度对象"], - [text_list], - [data_list], - ) - - 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()}", module="调度中枢" - ) - MainInfoBar.push_info_bar( - "warning", "任务已存在", choice.input[0].currentData(), 5000 - ) - return None - - if "调度队列" in choice.input[0].currentData(): - - logger.info( - f"用户添加任务:{choice.input[0].currentData()}", module="调度中枢" - ) - TaskManager.add_task( - "自动代理_新调度台", - choice.input[0].currentData(), - Config.queue_dict[choice.input[0].currentData()]["Config"].toDict(), - ) - - elif "脚本" in choice.input[0].currentData(): - - logger.info( - f"用户添加任务:{choice.input[0].currentData()}", module="调度中枢" - ) - TaskManager.add_task( - "自动代理_新调度台", - f"自定义队列 - {choice.input[0].currentData()}", - {"Queue": {"Script_0": choice.input[0].currentData()}}, - ) - - class DispatchBox(QWidget): - - def __init__(self, name: str, parent=None): - super().__init__(parent) - - self.setObjectName(name) - - self.top_bar = self.DispatchTopBar(self, name) - self.info = self.DispatchInfoCard(self) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.addWidget(self.top_bar) - content_layout.addWidget(self.info) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(scrollArea) - - class DispatchTopBar(CardWidget): - - def __init__(self, parent=None, name: str = None): - super().__init__(parent) - - Layout = QHBoxLayout(self) - - if name == "主调度台": - - self.Lable = SubtitleLabel("", self) - self.Lable.hide() - self.object = ComboBox() - self.object.setPlaceholderText("请选择调度对象") - self.mode = ComboBox() - self.mode.setPlaceholderText("请选择调度模式") - - self.main_button = PushButton("开始任务") - self.main_button.clicked.connect(self.start_main_task) - - Layout.addWidget(self.Lable) - Layout.addWidget(self.object) - Layout.addWidget(self.mode) - Layout.addStretch(1) - Layout.addWidget(self.main_button) - - else: - - self.Lable = SubtitleLabel(name, self) - self.main_button = PushButton("中止任务") - - Layout.addWidget(self.Lable) - Layout.addStretch(1) - Layout.addWidget(self.main_button) - - def start_main_task(self): - """从主调度台开始任务""" - - if self.object.currentIndex() == -1: - logger.warning("未选择调度对象", module="调度中枢") - MainInfoBar.push_info_bar( - "warning", "未选择调度对象", "请选择后再开始任务", 5000 - ) - return None - - if self.mode.currentIndex() == -1: - 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()}", module="调度中枢" - ) - MainInfoBar.push_info_bar( - "warning", "任务已存在", self.object.currentData(), 5000 - ) - return None - - if ( - "脚本" in self.object.currentData() - and Config.script_dict[self.object.currentData()]["Type"] - == "General" - and self.mode.currentData() == "人工排查" - ): - logger.warning("通用脚本类型不存在人工排查功能", module="调度中枢") - MainInfoBar.push_info_bar( - "warning", "不支持的任务", "通用脚本无人工排查功能", 5000 - ) - return None - - if "调度队列" in self.object.currentData(): - - logger.info( - f"用户添加任务:{self.object.currentData()}", module="调度中枢" - ) - TaskManager.add_task( - f"{self.mode.currentText()}_主调度台", - self.object.currentData(), - Config.queue_dict[self.object.currentData()]["Config"].toDict(), - ) - - elif "脚本" in self.object.currentData(): - - logger.info( - f"用户添加任务:{self.object.currentData()}", module="调度中枢" - ) - TaskManager.add_task( - f"{self.mode.currentText()}_主调度台", - "自定义队列", - {"Queue": {"Script_0": self.object.currentData()}}, - ) - - class DispatchInfoCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - - self.setTitle("调度信息") - - self.task = self.TaskInfoCard(self) - self.user = self.UserInfoCard(self) - self.log_text = self.LogCard(self) - - self.viewLayout.addWidget(self.task) - self.viewLayout.addWidget(self.user) - self.viewLayout.addWidget(self.log_text) - - self.viewLayout.setStretch(0, 1) - self.viewLayout.setStretch(1, 1) - self.viewLayout.setStretch(2, 5) - - def update_board(self, task_list: list, user_list: list, log: str): - """更新调度信息""" - - self.task.update_task(task_list) - self.user.update_user(user_list) - self.log_text.text.setText(log) - - class TaskInfoCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("任务队列") - - self.Layout = QVBoxLayout() - self.viewLayout.addLayout(self.Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - 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) - if item.spacerItem(): - self.Layout.removeItem(item.spacerItem()) - elif item.widget(): - item.widget().deleteLater() - - self.task_cards = [] - - for task in task_list: - - self.task_cards.append(StatefulItemCard(task)) - self.Layout.addWidget(self.task_cards[-1]) - - self.Layout.addStretch(1) - - def update_task(self, task_list: list): - """ - 更新任务队列信息 - - :param task_list: 包含任务信息的任务列表 - """ - - for i in range(len(task_list)): - - self.task_cards[i].update_status(task_list[i][1]) - - class UserInfoCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("用户队列") - - self.Layout = QVBoxLayout() - self.viewLayout.addLayout(self.Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - 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) - if item.spacerItem(): - self.Layout.removeItem(item.spacerItem()) - elif item.widget(): - item.widget().deleteLater() - - self.user_cards = [] - - for user in user_list: - - self.user_cards.append(StatefulItemCard(user)) - self.Layout.addWidget(self.user_cards[-1]) - - self.Layout.addStretch(1) - - def update_user(self, user_list: list): - """ - 更新用户队列信息 - - :param user_list: 包含用户信息的用户列表 - """ - - for i in range(len(user_list)): - - self.user_cards[i].Label.setText(user_list[i][0]) - self.user_cards[i].update_status(user_list[i][1]) - - class LogCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("日志") - - self.text = TextBrowser() - self.viewLayout.setContentsMargins(3, 0, 3, 3) - self.viewLayout.addWidget(self.text) - - self.text.textChanged.connect(self.to_end) - - def to_end(self): - """滚动到底部""" - - self.text.moveCursor(QTextCursor.End) - self.text.ensureCursorVisible() diff --git a/app/ui/downloader.py b/app/ui/downloader.py deleted file mode 100644 index e9b3ae4..0000000 --- a/app/ui/downloader.py +++ /dev/null @@ -1,729 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA更新器 -v4.4 -作者:DLmaster_361 -""" - -import zipfile -import requests -import subprocess -import time -from functools import partial -from pathlib import Path - -from PySide6.QtWidgets import QDialog, QVBoxLayout -from qfluentwidgets import ( - ProgressBar, - IndeterminateProgressBar, - BodyLabel, - setTheme, - Theme, -) -from PySide6.QtGui import QCloseEvent -from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop - -from typing import List, Dict, Union - -from app.core import Config, logger -from app.services import System - - -def version_text(version_numb: list) -> str: - """将版本号列表转为可读的文本信息""" - - while len(version_numb) < 4: - version_numb.append(0) - - if version_numb[3] == 0: - version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}" - else: - version = ( - f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" - ) - return version - - -class DownloadProcess(QThread): - """分段下载子线程""" - - progress = Signal(int) - accomplish = Signal(float) - - def __init__( - self, - url: str, - start_byte: int, - end_byte: int, - download_path: Path, - check_times: int = -1, - ) -> None: - super(DownloadProcess, self).__init__() - - 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 - self.download_path = download_path - self.check_times = check_times - - @logger.catch - def run(self) -> None: - - # 清理可能存在的临时文件 - if self.download_path.exists(): - self.download_path.unlink() - - logger.info( - f"开始下载:{self.url},范围:{self.start_byte}-{self.end_byte},存储地址:{self.download_path}", - module="下载子线程", - ) - - headers = ( - {"Range": f"bytes={self.start_byte}-{self.end_byte}"} - if not (self.start_byte == -1 or self.end_byte == -1) - else None - ) - - while not self.isInterruptionRequested() and self.check_times != 0: - - try: - - start_time = time.time() - - response = requests.get( - self.url, - headers=headers, - timeout=10, - stream=True, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - - if response.status_code not in [200, 206]: - - if self.check_times != -1: - self.check_times -= 1 - - logger.error( - f"连接失败:{self.url},状态码:{response.status_code},剩余重试次数:{self.check_times}", - module="下载子线程", - ) - - time.sleep(1) - continue - - logger.info( - f"连接成功:{self.url},状态码:{response.status_code}", - module="下载子线程", - ) - - downloaded_size = 0 - with self.download_path.open(mode="wb") as f: - - for chunk in response.iter_content(chunk_size=8192): - - if self.isInterruptionRequested(): - break - - f.write(chunk) - downloaded_size += len(chunk) - - self.progress.emit(downloaded_size) - - if self.isInterruptionRequested(): - - if self.download_path.exists(): - self.download_path.unlink() - self.accomplish.emit(0) - 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} 秒", - module="下载子线程", - ) - - break - - except Exception as e: - - if self.check_times != -1: - self.check_times -= 1 - - logger.exception( - f"下载出错:{self.url},错误信息:{e},剩余重试次数:{self.check_times}", - module="下载子线程", - ) - time.sleep(1) - - else: - - if self.download_path.exists(): - self.download_path.unlink() - self.accomplish.emit(0) - logger.error(f"下载失败:{self.url}", module="下载子线程") - - -class ZipExtractProcess(QThread): - """解压子线程""" - - info = Signal(str) - accomplish = Signal() - - def __init__(self, name: str, app_path: Path, download_path: Path) -> None: - super(ZipExtractProcess, self).__init__() - - self.setObjectName(f"ZipExtractProcess-{name}") - - logger.info(f"创建解压子线程:{self.objectName()}", module="解压子线程") - - self.name = name - self.app_path = app_path - self.download_path = download_path - - @logger.catch - def run(self) -> None: - - try: - - logger.info( - f"开始解压:{self.download_path} 到 {self.app_path}", - module="解压子线程", - ) - - while True: - - if self.isInterruptionRequested(): - self.download_path.unlink() - return None - try: - 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}", - module="解压子线程", - ) - break - except PermissionError: - if self.name == "AUTO_MAA": - self.info.emit(f"解压出错:AUTO_MAA正在运行,正在尝试将其关闭") - System.kill_process(self.app_path / "AUTO_MAA.exe") - else: - self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭") - logger.warning( - f"解压出错:{self.name}正在运行,正在等待其关闭", - module="解压子线程", - ) - time.sleep(1) - - except Exception as e: - - 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}", module="解压子线程") - return None - - -class DownloadManager(QDialog): - """下载管理器""" - - speed_test_accomplish = Signal() - download_accomplish = Signal() - download_process_clear = Signal() - - isInterruptionRequested = False - - def __init__(self, app_path: Path, name: str, version: list, config: dict) -> None: - super().__init__() - - self.app_path = app_path - self.name = name - self.version = version - self.config = config - self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径 - self.download_process_dict: Dict[str, DownloadProcess] = {} - self.timer_dict: Dict[str, QTimer] = {} - self.if_speed_test_accomplish = False - - self.resize(700, 70) - - setTheme(Theme.AUTO, lazy=True) - - # 创建垂直布局 - self.Layout = QVBoxLayout(self) - - self.info = BodyLabel("正在初始化", self) - self.progress_1 = IndeterminateProgressBar(self) - self.progress_2 = ProgressBar(self) - - self.update_progress(0, 0, 0) - - self.Layout.addWidget(self.info) - self.Layout.addStretch(1) - self.Layout.addWidget(self.progress_1) - self.Layout.addWidget(self.progress_2) - self.Layout.addStretch(1) - - 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.start_test_speed() - self.speed_test_accomplish.connect(self.start_download) - elif self.config["mode"] == "MirrorChyan": - self.start_download() - elif self.config["mode"] == "MirrorChyan": - self.start_download() - - def get_download_url(self, mode: str) -> Union[str, Dict[str, str]]: - """ - 生成下载链接 - - :param mode: "测速" 或 "下载" - :return: 测速模式返回 url 字典,下载模式返回 url 字符串 - """ - - url_dict = {} - - if mode == "测速": - - url_dict["GitHub站"] = ( - f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" - ) - url_dict["官方镜像站"] = ( - f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" - ) - for name, download_url_head in self.config["download_dict"].items(): - url_dict[name] = ( - f"{download_url_head}AUTO_MAA_{version_text(self.version)}.zip" - ) - for proxy_url in self.config["proxy_list"]: - url_dict[proxy_url] = ( - f"{proxy_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" - ) - return url_dict - - elif mode == "下载": - - if self.name == "AUTO_MAA": - - if self.config["mode"] == "Proxy": - - if "selected" in self.config: - selected_url = self.config["selected"] - elif "speed_result" in self.config: - selected_url = max( - self.config["speed_result"], - key=self.config["speed_result"].get, - ) - - if selected_url == "GitHub站": - return f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" - elif selected_url == "官方镜像站": - return f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" - elif selected_url in self.config["download_dict"].keys(): - return f"{self.config["download_dict"][selected_url]}AUTO_MAA_{version_text(self.version)}.zip" - else: - return f"{selected_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip" - - elif self.config["mode"] == "MirrorChyan": - - with requests.get( - self.config["url"], - allow_redirects=True, - timeout=10, - stream=True, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) as response: - if response.status_code == 200: - return response.url - - elif self.config["mode"] == "MirrorChyan": - - with requests.get( - self.config["url"], - allow_redirects=True, - timeout=10, - stream=True, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) as response: - if response.status_code == 200: - return response.url - - def start_test_speed(self) -> None: - """启动测速任务,下载4MB文件以测试下载速度""" - - if self.isInterruptionRequested: - return None - - url_dict = self.get_download_url("测速") - self.test_speed_result: Dict[str, float] = {} - - logger.info( - f"开始测速任务,链接:{list(url_dict.items())}", module="下载管理器" - ) - - for name, url in url_dict.items(): - - if self.isInterruptionRequested: - break - - # 创建测速线程,下载4MB文件以测试下载速度 - self.download_process_dict[name] = DownloadProcess( - url, - 0, - 4194304, - self.app_path / f"{name.replace('/','').replace(':','')}.zip", - 10, - ) - self.test_speed_result[name] = -1 - self.download_process_dict[name].accomplish.connect( - 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)) - timer.start(30000) - self.timer_dict[name] = timer - - self.update_info("正在测速,预计用时30秒") - 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 check_test_speed(self, name: str, t: float) -> None: - """ - 更新测速子任务wc信息,并检查测速任务是否允许结束 - - :param name: 测速任务的名称 - :param t: 测速任务的耗时 - """ - - # 计算下载速度 - if self.isInterruptionRequested: - self.update_info(f"已中止测速进程:{name}") - self.test_speed_result[name] = 0 - elif t != 0: - self.update_info(f"{name}:{ 4 / t:.2f} MB/s") - self.test_speed_result[name] = 4 / t - else: - self.update_info(f"{name}:{ 0:.2f} MB/s") - self.test_speed_result[name] = 0 - self.update_progress( - 0, - len(self.test_speed_result), - sum(1 for speed in self.test_speed_result.values() if speed != -1), - ) - - # 删除临时文件 - if (self.app_path / f"{name.replace('/','').replace(':','')}.zip").exists(): - (self.app_path / f"{name.replace('/','').replace(':','')}.zip").unlink() - - # 清理下载线程 - self.timer_dict[name].stop() - self.timer_dict[name].deleteLater() - self.timer_dict.pop(name) - self.download_process_dict[name].requestInterruption() - self.download_process_dict[name].quit() - self.download_process_dict[name].wait() - self.download_process_dict[name].deleteLater() - self.download_process_dict.pop(name) - if not self.download_process_dict: - self.download_process_clear.emit() - - # 当有速度大于1 MB/s的链接或存在3个即以上链接测速完成时,停止其他测速 - if not self.if_speed_test_accomplish and ( - sum(1 for speed in self.test_speed_result.values() if speed > 0) >= 3 - or any(speed > 1 for speed in self.test_speed_result.values()) - ): - self.if_speed_test_accomplish = True - for timer in self.timer_dict.values(): - timer.timeout.emit() - - if any(speed == -1 for _, speed in self.test_speed_result.items()): - return None - - # 保存测速结果 - self.config["speed_result"] = self.test_speed_result - logger.success( - f"测速完成,结果:{list(self.test_speed_result.items())}", - module="下载管理器", - ) - - self.update_info("测速完成!") - self.speed_test_accomplish.emit() - - def start_download(self) -> None: - """开始下载任务""" - - if self.isInterruptionRequested: - return None - - url = self.get_download_url("下载") - self.downloaded_size_list: List[List[int, bool]] = [] - - logger.info(f"开始下载任务,链接:{url}", module="下载管理器") - - response = requests.head( - url, - timeout=10, - proxies={ - "http": Config.get(Config.update_ProxyAddress), - "https": Config.get(Config.update_ProxyAddress), - }, - ) - - self.file_size = int(response.headers.get("content-length", 0)) - part_size = self.file_size // self.config["thread_numb"] - self.downloaded_size = 0 - self.last_download_size = 0 - self.last_time = time.time() - self.speed = 0 - - # 拆分下载任务,启用多线程下载 - for i in range(self.config["thread_numb"]): - - if self.isInterruptionRequested: - break - - # 计算单任务下载范围 - start_byte = i * part_size - end_byte = ( - (i + 1) * part_size - 1 - if (i != self.config["thread_numb"] - 1) - else self.file_size - 1 - ) - - # 创建下载子线程 - self.download_process_dict[f"part{i}"] = DownloadProcess( - url, - -1 if self.config["mode"] == "MirrorChyan" else start_byte, - -1 if self.config["mode"] == "MirrorChyan" else end_byte, - self.download_path.with_suffix(f".part{i}"), - 1 if self.config["mode"] == "MirrorChyan" else -1, - ) - self.downloaded_size_list.append([0, False]) - self.download_process_dict[f"part{i}"].progress.connect( - partial(self.update_download, i) - ) - self.download_process_dict[f"part{i}"].accomplish.connect( - partial(self.check_download, i) - ) - self.download_process_dict[f"part{i}"].start() - - 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) - / (time.time() - self.last_time) - / 1024 - ) - self.last_download_size = self.downloaded_size - self.last_time = time.time() - - if self.speed >= 1024: - self.update_info( - f"正在下载:{self.name} 已下载:{self.downloaded_size / 1048576:.2f}/{self.file_size / 1048576:.2f} MB ({self.downloaded_size / self.file_size * 100:.2f}%) 下载速度:{self.speed / 1024:.2f} MB/s", - ) - else: - self.update_info( - 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 check_download(self, index: str, t: float) -> None: - """ - 更新下载子任务完成信息,检查下载任务是否完成,完成后自动执行后续处理任务 - - :param index: 下载任务的索引 - :param t: 下载任务的耗时 - """ - - # 标记下载线程完成 - self.downloaded_size_list[index][1] = True - - # 清理下载线程 - self.download_process_dict[f"part{index}"].requestInterruption() - self.download_process_dict[f"part{index}"].quit() - self.download_process_dict[f"part{index}"].wait() - self.download_process_dict[f"part{index}"].deleteLater() - self.download_process_dict.pop(f"part{index}") - if not self.download_process_dict: - self.download_process_clear.emit() - - if ( - any([not _[1] for _ in self.downloaded_size_list]) - or self.isInterruptionRequested - ): - return None - - # 合并下载的分段文件 - logger.info( - f"所有分段下载完成:{self.name},开始合并分段文件到 {self.download_path}", - module="下载管理器", - ) - with self.download_path.open(mode="wb") as outfile: - for i in range(self.config["thread_numb"]): - with self.download_path.with_suffix(f".part{i}").open( - mode="rb" - ) as infile: - outfile.write(infile.read()) - self.download_path.with_suffix(f".part{i}").unlink() - - logger.success( - f"合并完成:{self.name},下载文件大小:{self.download_path.stat().st_size} 字节", - module="下载管理器", - ) - - self.update_info("正在解压更新文件") - self.update_progress(0, 0, 0) - - # 创建解压线程 - self.zip_extract = ZipExtractProcess( - self.name, self.app_path, self.download_path - ) - self.zip_loop = QEventLoop() - self.zip_extract.info.connect(self.update_info) - self.zip_extract.accomplish.connect(self.zip_loop.quit) - self.zip_extract.start() - self.zip_loop.exec() - - self.update_info("正在删除临时文件") - self.update_progress(0, 0, 0) - if (self.app_path / "changes.json").exists(): - (self.app_path / "changes.json").unlink() - if self.download_path.exists(): - self.download_path.unlink() - - # 下载完成后打开对应程序 - if not self.isInterruptionRequested and self.name == "MAA": - subprocess.Popen( - [self.app_path / "MAA.exe"], - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.DETACHED_PROCESS - | subprocess.CREATE_NO_WINDOW, - ) - if self.name == "AUTO_MAA": - self.update_info(f"即将安装{self.name}") - else: - self.update_info(f"{self.name}下载成功!") - self.update_progress(0, 100, 100) - 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) - self.progress_1.setVisible(True) - else: - self.progress_1.setVisible(False) - self.progress_2.setVisible(True) - self.progress_2.setRange(begin, end) - self.progress_2.setValue(current) - - def requestInterruption(self) -> None: - """请求中断下载任务""" - - logger.info("收到下载任务中止请求", module="下载管理器") - - self.isInterruptionRequested = True - - if hasattr(self, "zip_extract") and self.zip_extract: - self.zip_extract.requestInterruption() - - if hasattr(self, "zip_loop") and self.zip_loop: - self.zip_loop.quit() - - for process in self.download_process_dict.values(): - process.requestInterruption() - - if self.download_process_dict: - loop = QEventLoop() - self.download_process_clear.connect(loop.quit) - loop.exec() - - def closeEvent(self, event: QCloseEvent): - """清理残余进程""" - - self.requestInterruption() - - event.accept() diff --git a/app/ui/history.py b/app/ui/history.py deleted file mode 100644 index d0d45b3..0000000 --- a/app/ui/history.py +++ /dev/null @@ -1,398 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA历史记录界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QHBoxLayout, -) -from qfluentwidgets import ( - ScrollArea, - FluentIcon, - HeaderCardWidget, - PushButton, - TextBrowser, - CardWidget, - ComboBox, - ZhDatePicker, - SubtitleLabel, -) -from PySide6.QtCore import Signal, QDate -import os -import subprocess -from datetime import datetime, timedelta -from functools import partial -from pathlib import Path -from typing import List, Dict - - -from app.core import Config, SoundPlayer, logger -from .Widget import StatefulItemCard, QuantifiedItemCard, QuickExpandGroupCard - - -class History(QWidget): - """历史记录界面""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setObjectName("历史记录") - - self.history_top_bar = self.HistoryTopBar(self) - self.history_top_bar.search_history.connect(self.reload_history) - - content_widget = QWidget() - self.content_layout = QVBoxLayout(content_widget) - self.content_layout.setContentsMargins(0, 0, 11, 0) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(self.history_top_bar) - layout.addWidget(scrollArea) - - 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(): - self.content_layout.removeItem(item.spacerItem()) - elif item.widget(): - item.widget().deleteLater() - - self.history_card_list = [] - - history_dict = Config.search_history( - mode, - datetime(start_date.year(), start_date.month(), start_date.day()), - 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)) - self.content_layout.addWidget(self.history_card_list[-1]) - - self.content_layout.addStretch(1) - - class HistoryTopBar(CardWidget): - """历史记录顶部工具栏""" - - search_history = Signal(str, QDate, QDate) - - def __init__(self, parent=None): - super().__init__(parent) - - Layout = QHBoxLayout(self) - - self.lable_1 = SubtitleLabel("查询范围:") - self.start_date = ZhDatePicker() - self.start_date.setDate(QDate(2019, 5, 1)) - self.lable_2 = SubtitleLabel("→") - self.end_date = ZhDatePicker() - server_date = Config.server_date() - self.end_date.setDate( - QDate(server_date.year, server_date.month, server_date.day) - ) - self.mode = ComboBox() - self.mode.setPlaceholderText("请选择查询模式") - self.mode.addItems(["按日合并", "按周合并", "按月合并"]) - - self.select_month = PushButton(FluentIcon.TAG, "最近一月") - self.select_week = PushButton(FluentIcon.TAG, "最近一周") - self.search = PushButton(FluentIcon.SEARCH, "查询") - self.select_month.clicked.connect(lambda: self.select_date("month")) - self.select_week.clicked.connect(lambda: self.select_date("week")) - self.search.clicked.connect( - lambda: self.search_history.emit( - self.mode.currentText(), - self.start_date.getDate(), - self.end_date.getDate(), - ) - ) - - Layout.addWidget(self.lable_1) - Layout.addWidget(self.start_date) - Layout.addWidget(self.lable_2) - Layout.addWidget(self.end_date) - Layout.addWidget(self.mode) - Layout.addStretch(1) - Layout.addWidget(self.select_month) - Layout.addWidget(self.select_week) - 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": - begin_date = server_date - timedelta(weeks=1) - elif date == "month": - begin_date = server_date - timedelta(days=30) - - self.start_date.setDate( - QDate(begin_date.year, begin_date.month, begin_date.day) - ) - self.end_date.setDate( - QDate(server_date.year, server_date.month, server_date.day) - ) - - self.search.clicked.emit() - - class HistoryCard(QuickExpandGroupCard): - """历史记录卡片""" - - def __init__(self, date: str, user_dict: Dict[str, List[Path]], parent=None): - super().__init__( - FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - self.user_history_card_list = [] - - # 生成用户历史记录卡片并添加到布局中 - for user, info in user_dict.items(): - self.user_history_card_list.append( - self.UserHistoryCard(user, info, self) - ) - Layout.addWidget(self.user_history_card_list[-1]) - - class UserHistoryCard(HeaderCardWidget): - """用户历史记录卡片""" - - def __init__(self, name: str, user_history: List[Path], parent=None): - super().__init__(parent) - self.setTitle(name) - - self.user_history = user_history - - self.index_card = self.IndexCard(self.user_history, self) - self.index_card.index_changed.connect(self.update_info) - - self.statistics_card = QHBoxLayout() - self.log_card = self.LogCard(self) - - self.viewLayout.addWidget(self.index_card) - self.viewLayout.addLayout(self.statistics_card) - self.viewLayout.addWidget(self.log_card) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.viewLayout.setStretch(0, 1) - self.viewLayout.setStretch(2, 4) - - self.update_info("数据总览") - - def get_statistics(self, mode: str) -> dict: - """ - 生成GUI相应结构化统计数据 - - :param mode: 查询模式 - :return: 结构化统计数据 - """ - - history_info = Config.merge_statistic_info( - self.user_history if mode == "数据总览" else [Path(mode)] - ) - - statistics_info = {} - - if "recruit_statistics" in history_info: - statistics_info["公招统计"] = list( - history_info["recruit_statistics"].items() - ) - - if "drop_statistics" in history_info: - for game_id, drops in history_info["drop_statistics"].items(): - statistics_info[f"掉落统计:{game_id}"] = list(drops.items()) - - if mode == "数据总览" and "error_info" in history_info: - statistics_info["报错汇总"] = list( - history_info["error_info"].items() - ) - - return statistics_info - - def update_info(self, index: str) -> None: - """ - 更新信息到UI界面 - - :param index: 选择的索引 - """ - - # 移除已有统计信息UI组件 - while self.statistics_card.count() > 0: - item = self.statistics_card.takeAt(0) - if item.spacerItem(): - self.statistics_card.removeItem(item.spacerItem()) - 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) - self.statistics_card.addWidget(statistics_card) - - self.log_card.hide() - - else: - - 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() - - self.log_card.text.setText(log) - self.log_card.open_file.clicked.disconnect() - self.log_card.open_file.clicked.connect( - lambda: os.startfile(log_path) - ) - self.log_card.open_dir.clicked.disconnect() - self.log_card.open_dir.clicked.connect( - lambda: subprocess.Popen(["explorer", "/select,", log_path]) - ) - self.log_card.show() - - self.viewLayout.setStretch(1, self.statistics_card.count()) - - self.setMinimumHeight(300) - - class IndexCard(HeaderCardWidget): - """历史记录索引卡片组""" - - index_changed = Signal(str) - - def __init__(self, history_list: List[Path], parent=None): - super().__init__(parent) - self.setTitle("记录条目") - - self.Layout = QVBoxLayout() - self.viewLayout.addLayout(self.Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - 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])) - self.index_cards[-1].clicked.connect( - partial(self.index_changed.emit, str(index[2])) - ) - self.Layout.addWidget(self.index_cards[-1]) - - self.Layout.addStretch(1) - - class StatisticsCard(HeaderCardWidget): - """历史记录统计信息卡片组""" - - def __init__(self, name: str, item_list: list, parent=None): - super().__init__(parent) - self.setTitle(name) - - self.Layout = QVBoxLayout() - self.viewLayout.addLayout(self.Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - self.item_cards: List[QuantifiedItemCard] = [] - - for item in item_list: - - self.item_cards.append(QuantifiedItemCard(item)) - self.Layout.addWidget(self.item_cards[-1]) - - if len(item_list) == 0: - self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""])) - - self.Layout.addStretch(1) - - class LogCard(HeaderCardWidget): - """历史记录日志卡片""" - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("日志") - - self.text = TextBrowser(self) - self.open_file = PushButton("打开日志文件", self) - self.open_file.clicked.connect(lambda: print("打开日志文件")) - self.open_dir = PushButton("打开所在目录", self) - self.open_dir.clicked.connect(lambda: print("打开所在文件")) - - Layout = QVBoxLayout() - h_layout = QHBoxLayout() - h_layout.addWidget(self.open_file) - h_layout.addWidget(self.open_dir) - Layout.addWidget(self.text) - Layout.addLayout(h_layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - self.viewLayout.addLayout(Layout) diff --git a/app/ui/home.py b/app/ui/home.py deleted file mode 100644 index 7cde6f6..0000000 --- a/app/ui/home.py +++ /dev/null @@ -1,416 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA主界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QHBoxLayout, - QSpacerItem, - QSizePolicy, - QFileDialog, -) -from PySide6.QtCore import Qt, QSize, QUrl -from PySide6.QtGui import QDesktopServices, QColor -from qfluentwidgets import ( - FluentIcon, - ScrollArea, - SimpleCardWidget, - PrimaryToolButton, - TextBrowser, -) -import re -import shutil -import json -from datetime import datetime -from pathlib import Path - -from app.core import Config, MainInfoBar, Network, logger -from .Widget import Banner, IconButton - - -class Home(QWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setObjectName("主页") - - self.banner = Banner() - self.banner_text = TextBrowser() - - v_layout = QVBoxLayout(self.banner) - v_layout.setContentsMargins(0, 0, 0, 15) - v_layout.setSpacing(5) - v_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - # 空白占位符 - v_layout.addItem( - QSpacerItem(10, 20, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - ) - - # 顶部部分 (按钮组) - h1_layout = QHBoxLayout() - h1_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - # 左边留白区域 - h1_layout.addStretch() - - # 按钮组 - buttonGroup = ButtonGroup() - buttonGroup.setMaximumHeight(320) - h1_layout.addWidget(buttonGroup) - - # 空白占位符 - h1_layout.addItem( - QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - ) - - # 将顶部水平布局添加到垂直布局 - v_layout.addLayout(h1_layout) - - # 中间留白区域 - v_layout.addItem( - QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - ) - v_layout.addStretch() - - # 中间留白区域 - v_layout.addItem( - QSpacerItem(10, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - ) - v_layout.addStretch() - - # 底部部分 (图片切换按钮) - h2_layout = QHBoxLayout() - h2_layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - # 左边留白区域 - h2_layout.addItem( - QSpacerItem(20, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - ) - - # # 公告卡片 - # noticeCard = NoticeCard() - # h2_layout.addWidget(noticeCard) - - h2_layout.addStretch() - - # 自定义图像按钮布局 - self.imageButton = PrimaryToolButton(FluentIcon.IMAGE_EXPORT) - self.imageButton.setFixedSize(56, 56) - self.imageButton.setIconSize(QSize(32, 32)) - self.imageButton.clicked.connect(self.get_home_image) - - v1_layout = QVBoxLayout() - v1_layout.addWidget(self.imageButton, alignment=Qt.AlignmentFlag.AlignBottom) - - h2_layout.addLayout(v1_layout) - - # 空白占位符 - h2_layout.addItem( - QSpacerItem(25, 10, QSizePolicy.Policy.Fixed, QSizePolicy.Policy.Minimum) - ) - - # 将底部水平布局添加到垂直布局 - v_layout.addLayout(h2_layout) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 0, 0) - content_layout.addWidget(self.banner) - content_layout.addWidget(self.banner_text) - content_layout.setStretch(0, 2) - content_layout.setStretch(1, 3) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(scrollArea) - - self.set_banner() - - def get_home_image(self) -> None: - """获取主页图片""" - - logger.info("获取主页图片", module="主页") - - if Config.get(Config.function_HomeImageMode) == "默认": - - logger.info("使用默认主页图片", module="主页") - - elif Config.get(Config.function_HomeImageMode) == "自定义": - - file_path, _ = QFileDialog.getOpenFileName( - self, "打开自定义主页图片", "", "图片文件 (*.png *.jpg *.bmp)" - ) - if file_path: - - for file in Config.app_path.glob( - "resources/images/Home/BannerCustomize.*" - ): - file.unlink() - - shutil.copy( - file_path, - Config.app_path - / f"resources/images/Home/BannerCustomize{Path(file_path).suffix}", - ) - - logger.info(f"自定义主页图片更换成功:{file_path}", module="主页") - MainInfoBar.push_info_bar( - "success", - "主页图片更换成功", - "自定义主页图片更换成功!", - 3000, - ) - - else: - logger.warning("自定义主页图片更换失败:未选择图片文件", module="主页") - MainInfoBar.push_info_bar( - "warning", - "主页图片更换失败", - "未选择图片文件!", - 5000, - ) - elif Config.get(Config.function_HomeImageMode) == "主题图像": - - # 从远程服务器获取最新主题图像信息 - network = Network.add_task( - mode="get", - url="http://221.236.27.82:10197/d/AUTO_MAA/Server/theme_image.json", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - theme_image = network_result["response_json"] - else: - logger.warning( - f"获取最新主题图像时出错:{network_result['error_message']}", - module="主页", - ) - MainInfoBar.push_info_bar( - "warning", - "获取最新主题图像时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - if (Config.app_path / "resources/theme_image.json").exists(): - with (Config.app_path / "resources/theme_image.json").open( - mode="r", encoding="utf-8" - ) as f: - theme_image_local = json.load(f) - time_local = datetime.strptime( - theme_image_local["time"], "%Y-%m-%d %H:%M" - ) - 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 ( - datetime.now() - > datetime.strptime(theme_image["time"], "%Y-%m-%d %H:%M") - > time_local - ): - - network = Network.add_task( - mode="get_file", - url=theme_image["url"], - path=Config.app_path / "resources/images/Home/BannerTheme.jpg", - ) - network.loop.exec() - network_result = Network.get_result(network) - - if network_result["status_code"] == 200: - - with (Config.app_path / "resources/theme_image.json").open( - mode="w", encoding="utf-8" - ) as f: - json.dump(theme_image, f, ensure_ascii=False, indent=4) - - logger.success( - f"主题图像「{theme_image["name"]}」下载成功", module="主页" - ) - MainInfoBar.push_info_bar( - "success", - "主题图像下载成功", - f"「{theme_image["name"]}」下载成功!", - 3000, - ) - - else: - - logger.warning( - f"下载最新主题图像时出错:{network_result['error_message']}", - module="主页", - ) - MainInfoBar.push_info_bar( - "warning", - "下载最新主题图像时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - - else: - - logger.info("主题图像已是最新", module="主页") - MainInfoBar.push_info_bar( - "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") - ) - self.imageButton.hide() - self.banner_text.setVisible(False) - elif Config.get(Config.function_HomeImageMode) == "自定义": - for file in Config.app_path.glob("resources/images/Home/BannerCustomize.*"): - self.banner.set_banner_image(str(file)) - break - self.imageButton.show() - self.banner_text.setVisible(False) - elif Config.get(Config.function_HomeImageMode) == "主题图像": - self.banner.set_banner_image( - str(Config.app_path / "resources/images/Home/BannerTheme.jpg") - ) - self.imageButton.show() - self.banner_text.setVisible(True) - - if (Config.app_path / "resources/theme_image.json").exists(): - with (Config.app_path / "resources/theme_image.json").open( - mode="r", encoding="utf-8" - ) as f: - theme_image = json.load(f) - html_content = theme_image["html"] - else: - html_content = "

    主题图像

    主题图像信息未知

    " - - self.banner_text.setHtml(re.sub(r"]*>", "", html_content)) - - -class ButtonGroup(SimpleCardWidget): - """显示主页和 GitHub 按钮的竖直按钮组""" - - def __init__(self, parent=None): - super().__init__(parent=parent) - - self.setFixedSize(56, 180) - - layout = QVBoxLayout(self) - layout.setAlignment(Qt.AlignmentFlag.AlignTop) - - # 创建主页按钮 - home_button = IconButton( - FluentIcon.HOME.icon(color=QColor("#fff")), - tip_title="AUTO_MAA官网", - tip_content="AUTO_MAA官方文档站", - isTooltip=True, - ) - home_button.setIconSize(QSize(32, 32)) - home_button.clicked.connect(self.open_home) - layout.addWidget(home_button) - - # 创建 GitHub 按钮 - github_button = IconButton( - FluentIcon.GITHUB.icon(color=QColor("#fff")), - tip_title="Github仓库", - tip_content="如果本项目有帮助到您~\n不妨给项目点一个Star⭐", - isTooltip=True, - ) - github_button.setIconSize(QSize(32, 32)) - github_button.clicked.connect(self.open_github) - layout.addWidget(github_button) - - # # 创建 文档 按钮 - # doc_button = IconButton( - # FluentIcon.DICTIONARY.icon(color=QColor("#fff")), - # tip_title="自助排障文档", - # tip_content="点击打开自助排障文档,好孩子都能看懂", - # isTooltip=True, - # ) - # doc_button.setIconSize(QSize(32, 32)) - # doc_button.clicked.connect(self.open_doc) - # layout.addWidget(doc_button) - - # 创建 Q群 按钮 - doc_button = IconButton( - FluentIcon.CHAT.icon(color=QColor("#fff")), - tip_title="官方社群", - tip_content="加入官方群聊「AUTO_MAA绝赞DeBug中!」", - isTooltip=True, - ) - doc_button.setIconSize(QSize(32, 32)) - doc_button.clicked.connect(self.open_chat) - layout.addWidget(doc_button) - - # 创建 MirrorChyan 按钮 - doc_button = IconButton( - FluentIcon.SHOPPING_CART.icon(color=QColor("#fff")), - tip_title="非官方店铺", - tip_content="获取 MirrorChyan CDK,更新快人一步", - isTooltip=True, - ) - doc_button.setIconSize(QSize(32, 32)) - doc_button.clicked.connect(self.open_sales) - layout.addWidget(doc_button) - - def _normalBackgroundColor(self): - return QColor(0, 0, 0, 96) - - def open_home(self): - """打开主页链接""" - QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs")) - - def open_github(self): - """打开 GitHub 链接""" - QDesktopServices.openUrl(QUrl("https://github.com/DLmaster361/AUTO_MAA")) - - def open_chat(self): - """打开 Q群 链接""" - QDesktopServices.openUrl(QUrl("https://qm.qq.com/q/bd9fISNoME")) - - def open_doc(self): - """打开 文档 链接""" - QDesktopServices.openUrl(QUrl("https://clozya.github.io/AUTOMAA_docs")) - - def open_sales(self): - """打开 MirrorChyan 链接""" - QDesktopServices.openUrl( - QUrl("https://mirrorchyan.com/zh/get-start?source=auto_maa-home") - ) diff --git a/app/ui/main_window.py b/app/ui/main_window.py deleted file mode 100644 index 63715f6..0000000 --- a/app/ui/main_window.py +++ /dev/null @@ -1,488 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA主界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import QApplication, QSystemTrayIcon -from qfluentwidgets import ( - Action, - SystemTrayMenu, - SplashScreen, - FluentIcon, - setTheme, - isDarkTheme, - SystemThemeListener, - Theme, - MSFluentWindow, - NavigationItemPosition, -) -from PySide6.QtGui import QIcon, QCloseEvent -from PySide6.QtCore import QTimer -import darkdetect - -from app.core import Config, logger, TaskManager, MainTimer, MainInfoBar, SoundPlayer -from app.services import Notify, Crypto, System -from .home import Home -from .script_manager import ScriptManager -from .plan_manager import PlanManager -from .queue_manager import QueueManager -from .dispatch_center import DispatchCenter -from .history import History -from .setting import Setting - - -class AUTO_MAA(MSFluentWindow): - """AUTO_MAA主界面""" - - def __init__(self): - super().__init__() - - self.setWindowIcon(QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico"))) - - version_numb = list(map(int, Config.VERSION.split("."))) - version_text = ( - f"v{'.'.join(str(_) for _ in version_numb[0:3])}" - if version_numb[3] == 0 - else f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" - ) - - self.setWindowTitle(f"AUTO_MAA - {version_text}") - - self.switch_theme() - - 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.script_manager = ScriptManager(self) - self.queue_manager = QueueManager(self) - self.dispatch_center = DispatchCenter(self) - self.history = History(self) - self.setting = Setting(self) - - self.addSubInterface( - self.home, - FluentIcon.HOME, - "主页", - FluentIcon.HOME, - NavigationItemPosition.TOP, - ) - self.addSubInterface( - self.script_manager, - FluentIcon.ROBOT, - "脚本管理", - FluentIcon.ROBOT, - NavigationItemPosition.TOP, - ) - self.addSubInterface( - self.plan_manager, - FluentIcon.CALENDAR, - "计划管理", - FluentIcon.CALENDAR, - NavigationItemPosition.TOP, - ) - self.addSubInterface( - self.queue_manager, - FluentIcon.BOOK_SHELF, - "调度队列", - FluentIcon.BOOK_SHELF, - NavigationItemPosition.TOP, - ) - self.addSubInterface( - self.dispatch_center, - FluentIcon.IOT, - "调度中心", - FluentIcon.IOT, - NavigationItemPosition.TOP, - ) - self.addSubInterface( - self.history, - FluentIcon.HISTORY, - "历史记录", - FluentIcon.HISTORY, - NavigationItemPosition.BOTTOM, - ) - self.addSubInterface( - self.setting, - FluentIcon.SETTING, - "设置", - FluentIcon.SETTING, - 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 - ) - self.tray.setToolTip("AUTO_MAA") - self.tray_menu = SystemTrayMenu("AUTO_MAA", self) - - # 显示主界面菜单项 - self.tray_menu.addAction( - Action( - FluentIcon.CAFE, - "显示主界面", - triggered=lambda: self.show_ui("显示主窗口"), - ) - ) - self.tray_menu.addSeparator() - - # 开始任务菜单项 - self.tray_menu.addActions( - [ - Action(FluentIcon.PLAY, "运行启动时队列", triggered=self.start_up_task), - Action( - FluentIcon.PAUSE, - "中止所有任务", - triggered=lambda: TaskManager.stop_task("ALL"), - ), - ] - ) - self.tray_menu.addSeparator() - - # 退出主程序菜单项 - self.tray_menu.addAction( - Action( - FluentIcon.POWER_BUTTON, - "退出主程序", - triggered=lambda: System.set_power("KillSelf"), - ) - ) - - # 设置托盘菜单 - 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.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) - Notify.push_info_bar.connect(MainInfoBar.push_info_bar) - self.setting.ui.card_IfShowTray.checkedChanged.connect( - lambda: self.show_ui("配置托盘") - ) - self.setting.ui.card_IfToTray.checkedChanged.connect(self.set_min_method) - self.setting.function.card_HomeImageMode.comboBox.currentIndexChanged.connect( - lambda index: ( - self.home.get_home_image() if index == 2 else self.home.set_banner() - ) - ) - - self.splashScreen.finish() - - self.themeListener = SystemThemeListener(self) - self.themeListener.systemThemeChanged.connect(self.switch_theme) - self.themeListener.start() - - logger.success("AUTO_MAA主程序初始化完成", module="主窗口") - - def switch_theme(self) -> None: - """切换主题""" - - setTheme( - Theme(darkdetect.theme()) if darkdetect.theme() else Theme.LIGHT, lazy=True - ) - QTimer.singleShot(300, lambda: setTheme(Theme.AUTO, lazy=True)) - - # 云母特效启用时需要增加重试机制 - # 云母特效不兼容Win10,如果True则通过云母进行主题转换,False则根据当前主题设置背景颜色 - if self.isMicaEffectEnabled(): - QTimer.singleShot( - 300, - lambda: self.windowEffect.setMicaEffect(self.winId(), isDarkTheme()), - ) - - else: - # 根据当前主题设置背景颜色 - if isDarkTheme(): - self.setStyleSheet( - """ - CardWidget {background-color: #313131;} - HeaderCardWidget {background-color: #313131;} - background-color: #313131; - """ - ) - else: - self.setStyleSheet("background-color: #ffffff;") - - def set_min_method(self) -> None: - """设置最小化方法""" - - if Config.get(Config.ui_IfToTray): - - self.titleBar.minBtn.clicked.disconnect() - self.titleBar.minBtn.clicked.connect(lambda: self.show_ui("隐藏到托盘")) - - else: - - self.titleBar.minBtn.clicked.disconnect() - self.titleBar.minBtn.clicked.connect(self.window().showMinimized) - - def on_tray_activated(self, reason): - """双击返回主界面""" - if reason == QSystemTrayIcon.DoubleClick: - self.show_ui("显示主窗口") - - def show_ui( - self, mode: str, if_quick: bool = False, if_start: bool = False - ) -> None: - """配置窗口状态""" - - if Config.args.mode != "gui": - return None - - self.switch_theme() - - if mode == "显示主窗口": - - # 配置主窗口 - if not self.window().isVisible(): - size = list( - map( - int, - Config.get(Config.ui_size).split("x"), - ) - ) - location = list( - map( - int, - Config.get(Config.ui_location).split("x"), - ) - ) - if self.window().isMaximized(): - self.window().showNormal() - self.window().setGeometry(location[0], location[1], size[0], size[1]) - self.window().show() - if not if_quick: - if ( - Config.get(Config.ui_maximized) - and not self.window().isMaximized() - ): - self.titleBar.maxBtn.click() - SoundPlayer.play("欢迎回来") - self.show_ui("配置托盘") - elif if_start: - if Config.get(Config.ui_maximized) and not self.window().isMaximized(): - self.titleBar.maxBtn.click() - self.show_ui("配置托盘") - - # 如果窗口不在屏幕内,则重置窗口位置 - if not any( - self.window().geometry().intersects(screen.availableGeometry()) - for screen in QApplication.screens() - ): - self.window().showNormal() - self.window().setGeometry(100, 100, 1200, 700) - - self.window().raise_() - self.window().activateWindow() - - while Config.info_bar_list: - info_bar_item = Config.info_bar_list.pop(0) - MainInfoBar.push_info_bar( - info_bar_item["mode"], - info_bar_item["title"], - info_bar_item["content"], - info_bar_item["time"], - ) - - elif mode == "配置托盘": - - if Config.get(Config.ui_IfShowTray): - self.tray.show() - else: - self.tray.hide() - - elif mode == "隐藏到托盘": - - # 保存窗口相关属性 - if not self.window().isMaximized(): - - Config.set( - Config.ui_size, - f"{self.geometry().width()}x{self.geometry().height()}", - ) - Config.set( - Config.ui_location, - f"{self.geometry().x()}x{self.geometry().y()}", - ) - - Config.set(Config.ui_maximized, self.window().isMaximized()) - Config.save() - - # 隐藏主窗口 - if not if_quick: - - self.window().hide() - self.tray.show() - - def start_up_task(self) -> None: - """启动时任务""" - - logger.info("开始执行启动时任务", module="主窗口") - - # 清理旧历史记录 - Config.clean_old_history() - - # 清理安装包 - if (Config.app_path / "AUTO_MAA-Setup.exe").exists(): - try: - (Config.app_path / "AUTO_MAA-Setup.exe").unlink() - 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() - - # 获取关卡号信息 - Config.get_stage() - - # 获取主题图像 - if Config.get(Config.function_HomeImageMode) == "主题图像": - self.home.get_home_image() - - # 启动定时器 - MainTimer.start() - - # 获取公告 - self.setting.show_notice(if_first=True) - - # 检查更新 - if Config.get(Config.update_IfAutoUpdate): - self.setting.check_update(if_first=True) - - # 直接最小化 - if Config.get(Config.start_IfMinimizeDirectly): - - self.titleBar.minBtn.click() - - if Config.args.config: - - for config in [_ for _ in Config.args.config if _ in Config.queue_dict]: - - TaskManager.add_task( - "自动代理_新调度台", - config, - Config.queue_dict["调度队列_1"]["Config"].toDict(), - ) - - for config in [_ for _ in Config.args.config if _ in Config.script_dict]: - - TaskManager.add_task( - "自动代理_新调度台", - "自定义队列", - {"Queue": {"Script_0": config}}, - ) - - if not any( - _ in (list(Config.script_dict.keys()) + list(Config.queue_dict.keys())) - for _ in Config.args.config - ): - - logger.warning( - "当前运行模式为命令行模式,由于您使用了错误的 --config 参数进行配置,程序自动退出" - ) - System.set_power("KillSelf") - - elif Config.args.mode == "cli": - - logger.warning( - "当前运行模式为命令行模式,由于您未使用 --config 参数进行配置,程序自动退出" - ) - System.set_power("KillSelf") - - elif Config.args.mode == "gui": - - self.start_up_queue() - - logger.success("启动时任务执行完成", module="主窗口") - - def start_up_queue(self) -> None: - """启动时运行的调度队列""" - - logger.info("开始调度启动时运行的调度队列", module="主窗口") - - for name, queue in Config.queue_dict.items(): - - if queue["Config"].get(queue["Config"].QueueSet_StartUpEnabled): - - logger.info(f"自动添加任务:{name}", module="主窗口") - TaskManager.add_task( - "自动代理_新调度台", name, queue["Config"].toDict() - ) - - logger.success("开始调度启动时运行的调度队列启动完成", module="主窗口") - - def __currentChanged(self, index: int) -> None: - """切换界面时任务""" - - if index == 1: - self.script_manager.reload_plan_name() - elif index == 3: - self.queue_manager.reload_script_name() - elif index == 4: - self.dispatch_center.pivot.setCurrentItem("主调度台") - self.dispatch_center.update_top_bar() - - def closeEvent(self, event: QCloseEvent): - """清理残余进程""" - - logger.info("保存窗口位置与大小信息", module="主窗口") - self.show_ui("隐藏到托盘", if_quick=True) - - # 清理各功能线程 - MainTimer.stop() - TaskManager.stop_task("ALL") - - # 关闭主题监听 - self.themeListener.terminate() - self.themeListener.deleteLater() - - logger.info("AUTO_MAA主程序关闭", module="主窗口") - logger.info("----------------END----------------", module="主窗口") - - event.accept() diff --git a/app/ui/plan_manager.py b/app/ui/plan_manager.py deleted file mode 100644 index 3ee8cc0..0000000 --- a/app/ui/plan_manager.py +++ /dev/null @@ -1,515 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA计划管理界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QStackedWidget, - QHeaderView, -) -from qfluentwidgets import ( - Action, - FluentIcon, - MessageBox, - HeaderCardWidget, - CommandBar, - TableWidget, -) -from typing import List, Dict, Union -import shutil - -from app.core import Config, MainInfoBar, MaaPlanConfig, SoundPlayer, logger -from .Widget import ( - ComboBoxMessageBox, - LineEditSettingCard, - ComboBoxSettingCard, - SpinBoxSetting, - EditableComboBoxSetting, - ComboBoxSetting, - PivotArea, -) - - -class PlanManager(QWidget): - """计划管理父界面""" - - def __init__(self, parent=None): - super().__init__(parent) - - self.setObjectName("计划管理") - - layout = QVBoxLayout(self) - - self.tools = CommandBar() - self.plan_manager = self.PlanSettingBox(self) - - # 逐个添加动作 - self.tools.addActions( - [ - 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_plan), - Action(FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_plan), - ] - ) - self.tools.addSeparator() - - layout.addWidget(self.tools) - layout.addWidget(self.plan_manager) - - def add_plan(self): - """添加一个计划表""" - - choice = ComboBoxMessageBox( - self.window(), - "选择一个计划类型以添加相应计划表", - ["选择计划类型"], - [["MAA"]], - ) - if choice.exec() and choice.input[0].currentIndex() != -1: - - if choice.input[0].currentText() == "MAA": - - index = len(Config.plan_dict) + 1 - - # 初始化 MaaPlanConfig - maa_plan_config = MaaPlanConfig() - maa_plan_config.load( - Config.app_path / f"config/MaaPlanConfig/计划_{index}/config.json", - maa_plan_config, - ) - maa_plan_config.save() - - Config.plan_dict[f"计划_{index}"] = { - "Type": "Maa", - "Path": Config.app_path / f"config/MaaPlanConfig/计划_{index}", - "Config": maa_plan_config, - } - - # 添加计划表到界面 - self.plan_manager.add_MaaPlanSettingBox(index) - self.plan_manager.switch_SettingBox(index) - - logger.success(f"计划管理 计划_{index} 添加成功", module="计划管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"添加计划表 计划_{index}", 3000 - ) - SoundPlayer.play("添加计划表") - - def del_plan(self): - """删除一个计划表""" - - name = self.plan_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("删除计划表时未选择计划表", module="计划管理") - MainInfoBar.push_info_bar( - "warning", "未选择计划表", "请选择一个计划表", 5000 - ) - return None - - if len(Config.running_list) > 0: - logger.warning("删除计划表时调度队列未停止运行", module="计划管理") - MainInfoBar.push_info_bar( - "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 - ) - return None - - 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): - if Config.plan_dict[f"计划_{i}"]["Path"].exists(): - Config.plan_dict[f"计划_{i}"]["Path"].rename( - Config.plan_dict[f"计划_{i}"]["Path"].with_name(f"计划_{i-1}") - ) - Config.change_plan(f"计划_{i}", f"计划_{i-1}") - - self.plan_manager.show_SettingBox(max(int(name[3:]) - 1, 1)) - - logger.success(f"计划表 {name} 删除成功", module="计划管理") - MainInfoBar.push_info_bar("success", "操作成功", f"删除计划表 {name}", 3000) - SoundPlayer.play("删除计划表") - - def left_plan(self): - """向左移动计划表""" - - name = self.plan_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("向左移动计划表时未选择计划表", module="计划管理") - MainInfoBar.push_info_bar( - "warning", "未选择计划表", "请选择一个计划表", 5000 - ) - return None - - index = int(name[3:]) - - if index == 1: - logger.warning("向左移动计划表时已到达最左端", module="计划管理") - MainInfoBar.push_info_bar( - "warning", "已经是第一个计划表", "无法向左移动", 5000 - ) - return None - - if len(Config.running_list) > 0: - 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") - ) - Config.change_plan(name, "计划_0") - Config.plan_dict[f"计划_{index-1}"]["Path"].rename( - Config.plan_dict[name]["Path"] - ) - Config.change_plan(f"计划_{index-1}", name) - Config.plan_dict[name]["Path"].with_name("计划_0").rename( - Config.plan_dict[f"计划_{index-1}"]["Path"] - ) - Config.change_plan("计划_0", f"计划_{index-1}") - - self.plan_manager.show_SettingBox(index - 1) - - logger.success(f"计划表 {name} 左移成功", module="计划管理") - MainInfoBar.push_info_bar("success", "操作成功", f"左移计划表 {name}", 3000) - - def right_plan(self): - """向右移动计划表""" - - name = self.plan_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("向右移动计划表时未选择计划表", module="计划管理") - MainInfoBar.push_info_bar( - "warning", "未选择计划表", "请选择一个计划表", 5000 - ) - return None - - index = int(name[3:]) - - if index == len(Config.plan_dict): - logger.warning("向右移动计划表时已到达最右端", module="计划管理") - MainInfoBar.push_info_bar( - "warning", "已经是最后一个计划表", "无法向右移动", 5000 - ) - return None - - if len(Config.running_list) > 0: - 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") - ) - Config.change_plan(name, "计划_0") - Config.plan_dict[f"计划_{index+1}"]["Path"].rename( - Config.plan_dict[name]["Path"] - ) - Config.change_plan(f"计划_{index+1}", name) - Config.plan_dict[name]["Path"].with_name("计划_0").rename( - Config.plan_dict[f"计划_{index+1}"]["Path"] - ) - Config.change_plan("计划_0", f"计划_{index+1}") - - self.plan_manager.show_SettingBox(index + 1) - - logger.success(f"计划表 {name} 右移成功", module="计划管理") - MainInfoBar.push_info_bar("success", "操作成功", f"右移计划表 {name}", 3000) - - class PlanSettingBox(QWidget): - """计划管理子页面组""" - - def __init__(self, parent=None): - super().__init__(parent) - - self.setObjectName("计划管理页面组") - - 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: List[PlanManager.PlanSettingBox.MaaPlanSettingBox] = [] - - self.Layout = QVBoxLayout(self) - self.Layout.addWidget(self.pivotArea) - self.Layout.addWidget(self.stackedWidget) - self.Layout.setContentsMargins(0, 0, 0, 0) - - self.pivot.currentItemChanged.connect( - lambda index: self.switch_SettingBox( - int(index[3:]), if_chang_pivot=False - ) - ) - - self.show_SettingBox(1) - - def show_SettingBox(self, index) -> None: - """ - 加载所有子界面并切换到指定的子界面 - - :param index: 要显示的子界面索引 - """ - - Config.search_plan() - - for name, info in Config.plan_dict.items(): - if info["Type"] == "Maa": - self.add_MaaPlanSettingBox(int(name[3:])) - - 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 - - if index > len(Config.plan_dict): - return None - - if if_chang_pivot: - self.pivot.setCurrentItem(self.script_list[index - 1].objectName()) - self.stackedWidget.setCurrentWidget(self.script_list[index - 1]) - - def clear_SettingBox(self) -> None: - """清空所有子界面""" - - for sub_interface in self.script_list: - Config.stage_refreshed.disconnect(sub_interface.refresh_stage) - self.stackedWidget.removeWidget(sub_interface) - sub_interface.deleteLater() - self.script_list.clear() - self.pivot.clear() - - def add_MaaPlanSettingBox(self, uid: int) -> None: - """ - 添加一个MAA设置界面 - - :param uid: MAA计划表的唯一标识符 - """ - - maa_plan_setting_box = self.MaaPlanSettingBox(uid, self) - - self.script_list.append(maa_plan_setting_box) - - self.stackedWidget.addWidget(self.script_list[-1]) - - self.pivot.addItem(routeKey=f"计划_{uid}", text=f"计划 {uid}") - - class MaaPlanSettingBox(HeaderCardWidget): - """MAA类计划设置界面""" - - def __init__(self, uid: int, parent=None): - super().__init__(parent) - - self.setObjectName(f"计划_{uid}") - self.setTitle("MAA计划表") - self.config = Config.plan_dict[f"计划_{uid}"]["Config"] - - self.card_Name = LineEditSettingCard( - icon=FluentIcon.EDIT, - title="计划表名称", - content="用于标识计划表的名称", - text="请输入计划表名称", - qconfig=self.config, - configItem=self.config.Info_Name, - parent=self, - ) - self.card_Mode = ComboBoxSettingCard( - icon=FluentIcon.DICTIONARY, - title="计划模式", - content="全局模式下计划内容固定,周计划模式下计划按周一到周日切换", - texts=["全局", "周计划"], - qconfig=self.config, - configItem=self.config.Info_Mode, - parent=self, - ) - - self.table = TableWidget(self) - self.table.setColumnCount(8) - self.table.setRowCount(7) - self.table.setHorizontalHeaderLabels( - ["全局", "周一", "周二", "周三", "周四", "周五", "周六", "周日"] - ) - self.table.setVerticalHeaderLabels( - [ - "吃理智药", - "连战次数", - "关卡选择", - "备选 - 1", - "备选 - 2", - "备选 - 3", - "剩余理智", - ] - ) - self.table.setAlternatingRowColors(False) - self.table.setEditTriggers(TableWidget.NoEditTriggers) - for col in range(8): - self.table.horizontalHeader().setSectionResizeMode( - col, QHeaderView.ResizeMode.Stretch - ) - for row in range(7): - self.table.verticalHeader().setSectionResizeMode( - row, QHeaderView.ResizeMode.ResizeToContents - ) - - self.item_dict: Dict[ - str, - Dict[ - str, - Union[SpinBoxSetting, ComboBoxSetting, EditableComboBoxSetting], - ], - ] = {} - - for col, (group, name_dict) in enumerate( - self.config.config_item_dict.items() - ): - - self.item_dict[group] = {} - - for row, (name, configItem) in enumerate(name_dict.items()): - - if name == "MedicineNumb": - self.item_dict[group][name] = SpinBoxSetting( - range=(0, 1024), - qconfig=self.config, - configItem=configItem, - parent=self, - ) - elif name == "SeriesNumb": - self.item_dict[group][name] = ComboBoxSetting( - texts=["AUTO", "6", "5", "4", "3", "2", "1", "不选择"], - qconfig=self.config, - configItem=configItem, - parent=self, - ) - elif name == "Stage_Remain": - self.item_dict[group][name] = EditableComboBoxSetting( - value=Config.stage_dict[group]["value"], - texts=[ - "不使用" if _ == "当前/上次" else _ - for _ in Config.stage_dict[group]["text"] - ], - qconfig=self.config, - configItem=configItem, - parent=self, - ) - elif "Stage" in name: - self.item_dict[group][name] = EditableComboBoxSetting( - value=Config.stage_dict[group]["value"], - texts=Config.stage_dict[group]["text"], - qconfig=self.config, - configItem=configItem, - parent=self, - ) - - self.table.setCellWidget(row, col, self.item_dict[group][name]) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_Name) - Layout.addWidget(self.card_Mode) - Layout.addWidget(self.table) - - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - self.card_Mode.comboBox.currentIndexChanged.connect(self.switch_mode) - Config.stage_refreshed.connect(self.refresh_stage) - - self.switch_mode() - - def switch_mode(self) -> None: - """切换计划模式""" - - for group, name_dict in self.item_dict.items(): - for name, setting_item in name_dict.items(): - setting_item.setEnabled( - (group == "ALL") - == (self.config.get(self.config.Info_Mode) == "ALL") - ) - - def refresh_stage(self): - """刷新关卡列表""" - - for group, name_dict in self.item_dict.items(): - - for name, setting_item in name_dict.items(): - - if name == "Stage_Remain": - - setting_item.reLoadOptions( - Config.stage_dict[group]["value"], - [ - "不使用" if _ == "当前/上次" else _ - for _ in Config.stage_dict[group]["text"] - ], - ) - - elif "Stage" in name: - - setting_item.reLoadOptions( - Config.stage_dict[group]["value"], - Config.stage_dict[group]["text"], - ) diff --git a/app/ui/queue_manager.py b/app/ui/queue_manager.py deleted file mode 100644 index 16159b3..0000000 --- a/app/ui/queue_manager.py +++ /dev/null @@ -1,533 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA调度队列界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import ( - QWidget, - QVBoxLayout, - QStackedWidget, - QHBoxLayout, -) -from qfluentwidgets import ( - Action, - ScrollArea, - FluentIcon, - MessageBox, - HeaderCardWidget, - CommandBar, -) -from typing import List, Dict - -from app.core import QueueConfig, Config, MainInfoBar, SoundPlayer, logger -from .Widget import ( - SwitchSettingCard, - ComboBoxSettingCard, - LineEditSettingCard, - TimeEditSettingCard, - NoOptionComboBoxSettingCard, - HistoryCard, - PivotArea, -) - - -class QueueManager(QWidget): - - def __init__(self, parent=None): - super().__init__(parent) - - self.setObjectName("调度队列") - - layout = QVBoxLayout(self) - - self.tools = CommandBar() - - self.queue_manager = self.QueueSettingBox(self) - - # 逐个添加动作 - self.tools.addActions( - [ - Action(FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_queue), - Action( - FluentIcon.REMOVE_FROM, "删除调度队列", triggered=self.del_queue - ), - ] - ) - self.tools.addSeparator() - self.tools.addActions( - [ - 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_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 - ) - queue_config.save() - - Config.queue_dict[f"调度队列_{index}"] = { - "Path": Config.app_path / f"config/QueueConfig/调度队列_{index}.json", - "Config": queue_config, - } - - # 添加到配置界面 - self.queue_manager.add_SettingBox(index) - self.queue_manager.switch_SettingBox(index) - - logger.success(f"调度队列_{index} 添加成功", module="队列管理") - MainInfoBar.push_info_bar("success", "操作成功", f"添加 调度队列_{index}", 3000) - SoundPlayer.play("添加调度队列") - - def del_queue(self): - """删除一个调度队列实例""" - - name = self.queue_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择调度队列", module="队列管理") - MainInfoBar.push_info_bar( - "warning", "未选择调度队列", "请先选择一个调度队列", 5000 - ) - return None - - if name in Config.running_list: - logger.warning("调度队列正在运行", module="队列管理") - MainInfoBar.push_info_bar( - "warning", "调度队列正在运行", "请先停止调度队列", 5000 - ) - return None - - 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(): - Config.queue_dict[f"调度队列_{i}"]["Path"].rename( - Config.queue_dict[f"调度队列_{i}"]["Path"].with_name( - f"调度队列_{i-1}.json" - ) - ) - - self.queue_manager.show_SettingBox(max(int(name[5:]) - 1, 1)) - - logger.success(f"{name} 删除成功", module="队列管理") - MainInfoBar.push_info_bar("success", "操作成功", f"删除 {name}", 3000) - SoundPlayer.play("删除调度队列") - - def left_queue(self): - """向左移动调度队列实例""" - - name = self.queue_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择调度队列", module="队列管理") - MainInfoBar.push_info_bar( - "warning", "未选择调度队列", "请先选择一个调度队列", 5000 - ) - return None - - index = int(name[5:]) - - if index == 1: - 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("相关调度队列正在运行", 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") - ) - Config.queue_dict[f"调度队列_{index-1}"]["Path"].rename( - Config.queue_dict[name]["Path"] - ) - Config.queue_dict[name]["Path"].with_name("调度队列_0.json").rename( - Config.queue_dict[f"调度队列_{index-1}"]["Path"] - ) - - self.queue_manager.show_SettingBox(index - 1) - - logger.success(f"{name} 左移成功", module="队列管理") - MainInfoBar.push_info_bar("success", "操作成功", f"左移 {name}", 3000) - - def right_queue(self): - """向右移动调度队列实例""" - - name = self.queue_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择调度队列", module="队列管理") - MainInfoBar.push_info_bar( - "warning", "未选择调度队列", "请先选择一个调度队列", 5000 - ) - return None - - index = int(name[5:]) - - if index == len(Config.queue_dict): - 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("相关调度队列正在运行", 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") - ) - Config.queue_dict[f"调度队列_{index+1}"]["Path"].rename( - Config.queue_dict[name]["Path"] - ) - Config.queue_dict[name]["Path"].with_name("调度队列_0.json").rename( - Config.queue_dict[f"调度队列_{index+1}"]["Path"] - ) - - self.queue_manager.show_SettingBox(index + 1) - - logger.success(f"{name} 右移成功", module="队列管理") - MainInfoBar.push_info_bar("success", "操作成功", f"右移 {name}", 3000) - - def reload_script_name(self): - """刷新调度队列脚本成员名称""" - - # 获取脚本成员列表 - script_list = [ - ["禁用"] + [_ for _ in Config.script_dict.keys()], - ["未启用"] - + [ - ( - k - if v["Config"].get_name() == "" - else f"{k} - {v["Config"].get_name()}" - ) - for k, v in Config.script_dict.items() - ], - ] - for script in self.queue_manager.script_list: - for card in script.task.card_dict.values(): - card.reLoadOptions(value=script_list[0], texts=script_list[1]) - - class QueueSettingBox(QWidget): - - def __init__(self, parent=None): - super().__init__(parent) - - self.setObjectName("调度队列管理") - - self.pivotArea = PivotArea() - 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: List[ - QueueManager.QueueSettingBox.QueueMemberSettingBox - ] = [] - - self.Layout = QVBoxLayout(self) - self.Layout.addWidget(self.pivotArea) - self.Layout.addWidget(self.stackedWidget) - self.Layout.setContentsMargins(0, 0, 0, 0) - - self.pivot.currentItemChanged.connect( - lambda index: self.switch_SettingBox( - int(index[5:]), if_change_pivot=False - ) - ) - - self.show_SettingBox(1) - - def show_SettingBox(self, index) -> None: - """加载所有子界面""" - - Config.search_queue() - - for name in Config.queue_dict.keys(): - self.add_SettingBox(int(name[5:])) - - 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 - - if index > len(Config.queue_dict): - return None - - if if_change_pivot: - self.pivot.setCurrentItem(self.script_list[index - 1].objectName()) - self.stackedWidget.setCurrentWidget(self.script_list[index - 1]) - - def clear_SettingBox(self) -> None: - """清空所有子界面""" - - for sub_interface in self.script_list: - self.stackedWidget.removeWidget(sub_interface) - sub_interface.deleteLater() - self.script_list.clear() - self.pivot.clear() - - def add_SettingBox(self, uid: int) -> None: - """添加一个调度队列设置界面""" - - setting_box = self.QueueMemberSettingBox(uid, self) - - self.script_list.append(setting_box) - - self.stackedWidget.addWidget(self.script_list[-1]) - - self.pivot.addItem(routeKey=f"调度队列_{uid}", text=f"调度队列 {uid}") - - class QueueMemberSettingBox(QWidget): - - def __init__(self, uid: int, parent=None): - super().__init__(parent) - - self.setObjectName(f"调度队列_{uid}") - self.config = Config.queue_dict[f"调度队列_{uid}"]["Config"] - - self.queue_set = self.QueueSetSettingCard(self.config, self) - self.time = self.TimeSettingCard(self.config, self) - self.task = self.TaskSettingCard(self.config, self) - self.history = HistoryCard( - qconfig=self.config, - configItem=self.config.Data_LastProxyHistory, - parent=self, - ) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 11, 0) - content_layout.addWidget(self.queue_set) - content_layout.addWidget(self.time) - content_layout.addWidget(self.task) - content_layout.addWidget(self.history) - content_layout.addStretch(1) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(scrollArea) - - class QueueSetSettingCard(HeaderCardWidget): - - def __init__(self, config: QueueConfig, parent=None): - super().__init__(parent) - - self.setTitle("队列设置") - self.config = config - - self.card_Name = LineEditSettingCard( - icon=FluentIcon.EDIT, - title="调度队列名称", - content="用于标识调度队列的名称", - text="请输入调度队列名称", - qconfig=self.config, - configItem=self.config.QueueSet_Name, - parent=self, - ) - self.card_StartUpEnabled = SwitchSettingCard( - icon=FluentIcon.CHECKBOX, - title="启动时运行", - content="调度队列启动时运行状态,启用后将在软件启动时自动运行本队列", - qconfig=self.config, - 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( - icon=FluentIcon.POWER_BUTTON, - title="调度队列结束后", - content="选择调度队列结束后的操作", - texts=[ - "无动作", - "退出AUTO_MAA", - "睡眠(win系统需禁用休眠)", - "休眠", - "关机", - "关机(强制)", - ], - qconfig=self.config, - configItem=self.config.QueueSet_AfterAccomplish, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_Name) - Layout.addWidget(self.card_StartUpEnabled) - Layout.addWidget(self.card_TimeEnable) - Layout.addWidget(self.card_AfterAccomplish) - - self.viewLayout.addLayout(Layout) - - class TimeSettingCard(HeaderCardWidget): - - def __init__(self, config: QueueConfig, parent=None): - super().__init__(parent) - - self.setTitle("定时设置") - self.config = config - - widget_1 = QWidget() - Layout_1 = QVBoxLayout(widget_1) - widget_2 = QWidget() - Layout_2 = QVBoxLayout(widget_2) - Layout = QHBoxLayout() - - 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.addWidget(widget_1) - Layout.addWidget(widget_2) - - self.viewLayout.addLayout(Layout) - - class TaskSettingCard(HeaderCardWidget): - - def __init__(self, config: QueueConfig, parent=None): - super().__init__(parent) - - self.setTitle("任务队列") - self.config = config - - script_list = [ - ["禁用"] + [_ for _ in Config.script_dict.keys()], - ["未启用"] - + [ - ( - k - if v["Config"].get_name() == "" - else f"{k} - {v["Config"].get_name()}" - ) - for k, v in Config.script_dict.items() - ], - ] - - self.card_dict: Dict[ - str, - NoOptionComboBoxSettingCard, - ] = {} - - Layout = QVBoxLayout() - - 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) diff --git a/app/ui/script_manager.py b/app/ui/script_manager.py deleted file mode 100644 index 9c2d5a7..0000000 --- a/app/ui/script_manager.py +++ /dev/null @@ -1,3876 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA脚本管理界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import ( - QWidget, - QFileDialog, - QHBoxLayout, - QVBoxLayout, - QStackedWidget, - QTableWidgetItem, - QHeaderView, -) -from PySide6.QtGui import QIcon, Qt -from qfluentwidgets import ( - Action, - ConfigItem, - ScrollArea, - FluentIcon, - MessageBox, - HeaderCardWidget, - CommandBar, - ExpandGroupSettingCard, - PushSettingCard, - TableWidget, - PrimaryToolButton, - Flyout, - FlyoutAnimationType, -) -from PySide6.QtCore import Signal -from datetime import datetime -from functools import partial -from pathlib import Path -from typing import List, Dict, Union, Type -import shutil -import json - -from app.core import ( - Config, - logger, - MainInfoBar, - TaskManager, - MaaConfig, - MaaUserConfig, - GeneralConfig, - GeneralSubConfig, - Network, - SoundPlayer, -) -from app.services import Crypto -from .downloader import DownloadManager -from .Widget import ( - LineEditMessageBox, - LineEditSettingCard, - SpinBoxSettingCard, - ComboBoxMessageBox, - SettingFlyoutView, - NoOptionComboBoxSettingCard, - ComboBoxWithPlanSettingCard, - EditableComboBoxWithPlanSettingCard, - SpinBoxWithPlanSettingCard, - PasswordLineEditSettingCard, - PasswordLineAndSwitchButtonSettingCard, - UserLableSettingCard, - UserTaskSettingCard, - SubLableSettingCard, - ComboBoxSettingCard, - SwitchSettingCard, - PathSettingCard, - PushAndSwitchButtonSettingCard, - PushAndComboBoxSettingCard, - StatusSwitchSetting, - UserNoticeSettingCard, - NoticeMessageBox, - PivotArea, -) - - -class ScriptManager(QWidget): - """脚本管理父界面""" - - def __init__(self, parent=None): - super().__init__(parent) - - self.setObjectName("脚本管理") - - layout = QVBoxLayout(self) - - self.tools = CommandBar() - self.script_manager = self.ScriptSettingBox(self) - - # 逐个添加动作 - self.tools.addActions( - [ - Action(FluentIcon.ADD_TO, "新建脚本实例", triggered=self.add_script), - Action( - FluentIcon.REMOVE_FROM, "删除脚本实例", triggered=self.del_script - ), - ] - ) - self.tools.addSeparator() - self.tools.addActions( - [ - Action(FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_script), - Action(FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_script), - ] - ) - self.tools.addSeparator() - self.tools.addAction( - Action( - FluentIcon.DOWNLOAD, - "脚本下载器", - triggered=self.script_downloader, - ) - ) - self.tools.addSeparator() - self.key = Action( - FluentIcon.HIDE, - "显示/隐藏密码", - checkable=True, - triggered=self.show_password, - ) - self.tools.addAction(self.key) - - layout.addWidget(self.tools) - layout.addWidget(self.script_manager) - - def add_script(self): - """添加一个脚本实例""" - - choice = ComboBoxMessageBox( - self.window(), - "选择一个脚本类型以添加相应脚本实例", - ["选择脚本类型"], - [["MAA", "通用"]], - ) - if choice.exec() and choice.input[0].currentIndex() != -1: - - logger.info( - f"添加脚本实例: {choice.input[0].currentText()}", module="脚本管理" - ) - - if choice.input[0].currentText() == "MAA": - - index = len(Config.script_dict) + 1 - - # 初始化 MAA 配置 - maa_config = MaaConfig() - maa_config.load( - Config.app_path / f"config/MaaConfig/脚本_{index}/config.json", - maa_config, - ) - maa_config.save() - (Config.app_path / f"config/MaaConfig/脚本_{index}/UserData").mkdir( - parents=True, exist_ok=True - ) - - Config.script_dict[f"脚本_{index}"] = { - "Type": "Maa", - "Path": Config.app_path / f"config/MaaConfig/脚本_{index}", - "Config": maa_config, - "UserData": {}, - } - - # 添加 MAA 实例设置界面 - self.script_manager.add_SettingBox( - index, self.ScriptSettingBox.MaaSettingBox - ) - self.script_manager.switch_SettingBox(index) - - logger.success(f"MAA实例 脚本_{index} 添加成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"添加 MAA 实例 脚本_{index}", 3000 - ) - SoundPlayer.play("添加脚本实例") - - elif choice.input[0].currentText() == "通用": - - index = len(Config.script_dict) + 1 - - # 初始化通用配置 - general_config = GeneralConfig() - general_config.load( - Config.app_path / f"config/GeneralConfig/脚本_{index}/config.json", - general_config, - ) - general_config.save() - (Config.app_path / f"config/GeneralConfig/脚本_{index}/SubData").mkdir( - parents=True, exist_ok=True - ) - - Config.script_dict[f"脚本_{index}"] = { - "Type": "General", - "Path": Config.app_path / f"config/GeneralConfig/脚本_{index}", - "Config": general_config, - "SubData": {}, - } - - # 添加通用实例设置界面 - self.script_manager.add_SettingBox( - index, self.ScriptSettingBox.GeneralSettingBox - ) - self.script_manager.switch_SettingBox(index) - - logger.success(f"通用实例 脚本_{index} 添加成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"添加通用实例 脚本_{index}", 3000 - ) - SoundPlayer.play("添加脚本实例") - - def del_script(self): - """删除一个脚本实例""" - - name = self.script_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("删除脚本实例时未选择脚本实例", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择脚本实例", "请选择一个脚本实例", 5000 - ) - return None - - if len(Config.running_list) > 0: - logger.warning("删除脚本实例时调度队列未停止运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 - ) - return None - - choice = MessageBox("确认", f"确定要删除 {name} 实例吗?", self.window()) - if choice.exec(): - - logger.info(f"正在删除脚本实例: {name}", module="脚本管理") - - self.script_manager.clear_SettingBox() - - # 删除脚本实例的配置文件并同步修改相应配置项 - shutil.rmtree(Config.script_dict[name]["Path"]) - Config.change_queue(name, "禁用") - for i in range(int(name[3:]) + 1, len(Config.script_dict) + 1): - if Config.script_dict[f"脚本_{i}"]["Path"].exists(): - Config.script_dict[f"脚本_{i}"]["Path"].rename( - Config.script_dict[f"脚本_{i}"]["Path"].with_name(f"脚本_{i-1}") - ) - Config.change_queue(f"脚本_{i}", f"脚本_{i-1}") - - self.script_manager.show_SettingBox(max(int(name[3:]) - 1, 1)) - - logger.success(f"脚本实例 {name} 删除成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"删除脚本实例 {name}", 3000 - ) - SoundPlayer.play("删除脚本实例") - - def left_script(self): - """向左移动脚本实例""" - - name = self.script_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("向左移动脚本实例时未选择脚本实例", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择脚本实例", "请选择一个脚本实例", 5000 - ) - return None - - index = int(name[3:]) - - if index == 1: - logger.warning("向左移动脚本实例时已到达最左端", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "已经是第一个脚本实例", "无法向左移动", 5000 - ) - return None - - if len(Config.running_list) > 0: - logger.warning("向左移动脚本实例时调度队列未停止运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 - ) - return None - - logger.info(f"正在向左移动脚本实例: {name}", module="脚本管理") - - self.script_manager.clear_SettingBox() - - # 移动脚本实例配置文件并同步修改配置项 - Config.script_dict[name]["Path"].rename( - Config.script_dict[name]["Path"].with_name("脚本_0") - ) - Config.change_queue(name, "脚本_0") - Config.script_dict[f"脚本_{index-1}"]["Path"].rename( - Config.script_dict[f"脚本_{index-1}"]["Path"].with_name(name) - ) - Config.change_queue(f"脚本_{index-1}", name) - Config.script_dict[name]["Path"].with_name("脚本_0").rename( - Config.script_dict[name]["Path"].with_name(f"脚本_{index-1}") - ) - Config.change_queue("脚本_0", f"脚本_{index-1}") - - self.script_manager.show_SettingBox(index - 1) - - logger.success(f"脚本实例 {name} 左移成功", module="脚本管理") - MainInfoBar.push_info_bar("success", "操作成功", f"左移脚本实例 {name}", 3000) - - def right_script(self): - """向右移动脚本实例""" - - name = self.script_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("向右移动脚本实例时未选择脚本实例", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择脚本实例", "请选择一个脚本实例", 5000 - ) - return None - - index = int(name[3:]) - - if index == len(Config.script_dict): - logger.warning("向右移动脚本实例时已到达最右端", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "已经是最后一个脚本实例", "无法向右移动", 5000 - ) - return None - - if len(Config.running_list) > 0: - logger.warning("向右移动脚本实例时调度队列未停止运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "调度中心正在执行任务", "请等待或手动中止任务", 5000 - ) - return None - - logger.info(f"正在向右移动脚本实例: {name}", module="脚本管理") - - self.script_manager.clear_SettingBox() - - # 移动脚本实例配置文件并同步修改配置项 - Config.script_dict[name]["Path"].rename( - Config.script_dict[name]["Path"].with_name("脚本_0") - ) - Config.change_queue(name, "脚本_0") - Config.script_dict[f"脚本_{index+1}"]["Path"].rename( - Config.script_dict[f"脚本_{index+1}"]["Path"].with_name(name) - ) - Config.change_queue(f"脚本_{index+1}", name) - Config.script_dict[name]["Path"].with_name("脚本_0").rename( - Config.script_dict[name]["Path"].with_name(f"脚本_{index+1}") - ) - Config.change_queue("脚本_0", f"脚本_{index+1}") - - self.script_manager.show_SettingBox(index + 1) - - logger.success(f"脚本实例 {name} 右移成功", module="脚本管理") - MainInfoBar.push_info_bar("success", "操作成功", f"右移脚本实例 {name}", 3000) - - def script_downloader(self): - """脚本下载器""" - - if not Config.get(Config.update_MirrorChyanCDK): - - logger.warning("脚本下载器未设置CDK", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", - "未设置Mirror酱CDK", - "下载器依赖于Mirror酱,未设置CDK时无法使用", - 5000, - ) - return None - - # 从远程服务器获取应用列表 - network = Network.add_task( - mode="get", - url="http://221.236.27.82:10197/d/AUTO_MAA/Server/apps_info.json", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - apps_info = network_result["response_json"] - else: - logger.warning( - f"获取应用列表时出错:{network_result['error_message']}", - module="脚本管理", - ) - MainInfoBar.push_info_bar( - "warning", - "获取应用列表时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - choice = ComboBoxMessageBox( - self.window(), - "选择一个脚本类型以下载相应脚本", - ["选择脚本类型"], - [list(apps_info.keys())], - ) - if choice.exec() and choice.input[0].currentIndex() != -1: - - app_name = choice.input[0].currentText() - app_rid = apps_info[app_name]["rid"] - - (Config.app_path / f"script/{app_rid}").mkdir(parents=True, exist_ok=True) - folder = QFileDialog.getExistingDirectory( - self, - f"选择{app_name}下载目录", - str(Config.app_path / f"script/{app_rid}"), - ) - if not folder: - logger.warning( - f"选择{app_name}下载目录时未选择文件夹", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "warning", "警告", f"未选择{app_name}下载目录", 5000 - ) - return None - - # 从mirrorc服务器获取最新版本信息 - network = Network.add_task( - mode="get", - url=f"https://mirrorchyan.com/api/resources/{app_rid}/latest?user_agent=AutoMaaGui&cdk={Crypto.win_decryptor(Config.get(Config.update_MirrorChyanCDK))}&os={apps_info[app_name]["os"]}&arch={apps_info[app_name]["arch"]}&channel=stable", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - app_info = network_result["response_json"] - else: - - if network_result["response_json"]: - - app_info = network_result["response_json"] - - if app_info["code"] != 0: - - logger.error( - f"获取应用版本信息时出错:{app_info["msg"]}", - module="脚本管理", - ) - - error_remark_dict = { - 1001: "获取版本信息的URL参数不正确", - 7001: "填入的 CDK 已过期", - 7002: "填入的 CDK 错误", - 7003: "填入的 CDK 今日下载次数已达上限", - 7004: "填入的 CDK 类型和待下载的资源不匹配", - 7005: "填入的 CDK 已被封禁", - 8001: "对应架构和系统下的资源不存在", - 8002: "错误的系统参数", - 8003: "错误的架构参数", - 8004: "错误的更新通道参数", - 1: app_info["msg"], - } - - if app_info["code"] in error_remark_dict: - MainInfoBar.push_info_bar( - "error", - "获取版本信息时出错", - error_remark_dict[app_info["code"]], - -1, - ) - else: - MainInfoBar.push_info_bar( - "error", - "获取版本信息时出错", - "意料之外的错误,请及时联系项目组以获取来自 Mirror 酱的技术支持", - -1, - ) - - return None - - logger.warning( - f"获取版本信息时出错:{network_result['error_message']}", - module="脚本管理", - ) - MainInfoBar.push_info_bar( - "warning", - "获取版本信息时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - # 创建下载管理器并开始下载 - logger.info(f"开始下载{app_name},下载目录:{folder}", module="脚本管理") - self.downloader = DownloadManager( - Path(folder), - app_rid, - [], - { - "mode": "MirrorChyan", - "thread_numb": 1, - "url": app_info["data"]["url"], - }, - ) - self.downloader.setWindowTitle("AUTO_MAA下载器 - Mirror酱渠道") - self.downloader.setWindowIcon( - QIcon(str(Config.app_path / "resources/icons/MirrorChyan.ico")) - ) - self.downloader.show() - self.downloader.run() - - def show_password(self): - """显示或隐藏密码""" - - if Config.PASSWORD == "": - choice = LineEditMessageBox( - self.window(), - "请输入管理密钥", - "管理密钥", - "密码", - ) - if choice.exec() and choice.input.text() != "": - Config.PASSWORD = choice.input.text() - Config.PASSWORD_refreshed.emit() - self.key.setIcon(FluentIcon.VIEW) - self.key.setChecked(True) - else: - Config.PASSWORD = "" - Config.PASSWORD_refreshed.emit() - self.key.setIcon(FluentIcon.HIDE) - self.key.setChecked(False) - else: - Config.PASSWORD = "" - Config.PASSWORD_refreshed.emit() - self.key.setIcon(FluentIcon.HIDE) - self.key.setChecked(False) - - def reload_plan_name(self): - """刷新计划表名称""" - - # 生成计划列表信息 - plan_list = [ - ["固定"] + [_ for _ in Config.plan_dict.keys()], - ["固定"] - + [ - ( - k - if v["Config"].get(v["Config"].Info_Name) == "" - else f"{k} - {v["Config"].get(v["Config"].Info_Name)}" - ) - for k, v in Config.plan_dict.items() - ], - ] - - # 刷新所有脚本实例的计划表名称 - for script in self.script_manager.script_list: - - if isinstance(script, ScriptManager.ScriptSettingBox.MaaSettingBox): - - for user_setting in script.user_setting.user_manager.script_list: - - user_setting.card_StageMode.comboBox.currentIndexChanged.disconnect( - user_setting.switch_stage_mode - ) - user_setting.card_StageMode.reLoadOptions( - plan_list[0], plan_list[1] - ) - user_setting.card_StageMode.comboBox.currentIndexChanged.connect( - user_setting.switch_stage_mode - ) - - self.refresh_plan_info() - - def refresh_dashboard(self): - """刷新所有脚本实例的仪表盘""" - - for script in self.script_manager.script_list: - - if isinstance(script, ScriptManager.ScriptSettingBox.MaaSettingBox): - script.user_setting.user_manager.user_dashboard.load_info() - elif isinstance(script, ScriptManager.ScriptSettingBox.GeneralSettingBox): - script.branch_manager.sub_manager.sub_dashboard.load_info() - - def refresh_plan_info(self): - """刷新所有计划信息""" - - for script in self.script_manager.script_list: - - if isinstance(script, ScriptManager.ScriptSettingBox.MaaSettingBox): - - script.user_setting.user_manager.user_dashboard.load_info() - for user_setting in script.user_setting.user_manager.script_list: - user_setting.switch_stage_mode() - - class ScriptSettingBox(QWidget): - """脚本管理子页面组""" - - def __init__(self, parent=None): - super().__init__(parent) - - self.setObjectName("脚本管理页面组") - - 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: List[ - Union[ - ScriptManager.ScriptSettingBox.MaaSettingBox, - ScriptManager.ScriptSettingBox.GeneralSettingBox, - ] - ] = [] - - self.Layout = QVBoxLayout(self) - self.Layout.addWidget(self.pivotArea) - self.Layout.addWidget(self.stackedWidget) - self.Layout.setContentsMargins(0, 0, 0, 0) - - self.pivot.currentItemChanged.connect( - lambda index: self.switch_SettingBox( - int(index[3:]), if_chang_pivot=False - ) - ) - - self.show_SettingBox(1) - - def show_SettingBox(self, index) -> None: - """ - 加载所有子界面并切换到指定子界面 - - :param index: 要切换到的子界面索引 - :type index: int - """ - - Config.search_script() - - for name, info in Config.script_dict.items(): - if info["Type"] == "Maa": - self.add_SettingBox(int(name[3:]), self.MaaSettingBox) - elif info["Type"] == "General": - self.add_SettingBox(int(name[3:]), self.GeneralSettingBox) - - self.switch_SettingBox(index) - - def switch_SettingBox(self, index: int, if_chang_pivot: bool = True) -> None: - """ - 切换到指定的子界面 - - :param index: 要切换到的子界面索引 - :type index: int - :param if_chang_pivot: 是否更改导航栏的当前项 - :type if_chang_pivot: bool - """ - - if len(Config.script_dict) == 0: - return None - - if index > len(Config.script_dict): - return None - - if if_chang_pivot: - self.pivot.setCurrentItem(self.script_list[index - 1].objectName()) - self.stackedWidget.setCurrentWidget(self.script_list[index - 1]) - - if isinstance( - self.script_list[index - 1], - ScriptManager.ScriptSettingBox.MaaSettingBox, - ): - self.script_list[index - 1].user_setting.user_manager.switch_SettingBox( - "用户仪表盘" - ) - elif isinstance( - self.script_list[index - 1], - ScriptManager.ScriptSettingBox.GeneralSettingBox, - ): - self.script_list[ - index - 1 - ].branch_manager.sub_manager.switch_SettingBox("配置仪表盘") - - def clear_SettingBox(self) -> None: - """清空所有子界面""" - - for sub_interface in self.script_list: - self.stackedWidget.removeWidget(sub_interface) - sub_interface.deleteLater() - self.script_list.clear() - self.pivot.clear() - - def add_SettingBox(self, uid: int, type: Type) -> None: - """ - 添加指定类型设置子界面 - - :param uid: 脚本实例的唯一标识符 - :type uid: int - :param type: 要添加的设置子界面类型 - :type type: Type - """ - - if type == self.MaaSettingBox: - setting_box = self.MaaSettingBox(uid, self) - elif type == self.GeneralSettingBox: - setting_box = self.GeneralSettingBox(uid, self) - else: - return None - - self.script_list.append(setting_box) - self.stackedWidget.addWidget(self.script_list[-1]) - self.pivot.addItem(routeKey=f"脚本_{uid}", text=f"脚本 {uid}") - - class MaaSettingBox(QWidget): - """MAA类脚本设置界面""" - - def __init__(self, uid: int, parent=None): - super().__init__(parent) - - self.setObjectName(f"脚本_{uid}") - self.config = Config.script_dict[f"脚本_{uid}"]["Config"] - - self.app_setting = self.AppSettingCard(f"脚本_{uid}", self.config, self) - self.user_setting = self.UserManager(f"脚本_{uid}", self) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 11, 0) - content_layout.addWidget(self.app_setting) - content_layout.addWidget(self.user_setting) - content_layout.addStretch(1) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(scrollArea) - - class AppSettingCard(HeaderCardWidget): - - def __init__(self, name: str, config: MaaConfig, parent=None): - super().__init__(parent) - - self.setTitle("MAA实例") - - self.name = name - self.config = config - - Layout = QVBoxLayout() - - self.card_Name = LineEditSettingCard( - icon=FluentIcon.EDIT, - title="实例名称", - content="用于标识MAA实例的名称", - text="请输入实例名称", - qconfig=self.config, - configItem=self.config.MaaSet_Name, - parent=self, - ) - self.card_Path = PushSettingCard( - text="选择文件夹", - icon=FluentIcon.FOLDER, - title="MAA目录", - content=self.config.get(self.config.MaaSet_Path), - parent=self, - ) - self.card_Set = PushSettingCard( - text="设置", - icon=FluentIcon.HOME, - title="MAA全局配置", - content="简洁模式下MAA将继承全局配置", - parent=self, - ) - self.RunSet = self.RunSetSettingCard(self.config, self) - - self.card_Path.clicked.connect(self.PathClicked) - self.config.MaaSet_Path.valueChanged.connect( - lambda: self.card_Path.setContent( - self.config.get(self.config.MaaSet_Path) - ) - ) - self.card_Set.clicked.connect( - lambda: TaskManager.add_task("设置MAA_全局", self.name, None) - ) - - Layout.addWidget(self.card_Name) - Layout.addWidget(self.card_Path) - Layout.addWidget(self.card_Set) - Layout.addWidget(self.RunSet) - - self.viewLayout.addLayout(Layout) - - def PathClicked(self): - """选择MAA目录并验证""" - - folder = QFileDialog.getExistingDirectory( - self, - "选择MAA目录", - self.config.get(self.config.MaaSet_Path), - ) - if not folder or self.config.get(self.config.MaaSet_Path) == folder: - logger.warning( - "选择MAA目录时未选择文件夹或未更改文件夹", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "warning", "警告", "未选择文件夹或未更改文件夹", 5000 - ) - return None - elif ( - not (Path(folder) / "config/gui.json").exists() - or not (Path(folder) / "MAA.exe").exists() - ): - logger.warning( - "选择MAA目录时未找到MAA程序或配置文件", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "warning", "警告", "未找到MAA程序或配置文件", 5000 - ) - return None - - (Config.script_dict[self.name]["Path"] / "Default").mkdir( - parents=True, exist_ok=True - ) - shutil.copy( - Path(folder) / "config/gui.json", - Config.script_dict[self.name]["Path"] / "Default/gui.json", - ) - self.config.set(self.config.MaaSet_Path, folder) - - class RunSetSettingCard(ExpandGroupSettingCard): - - def __init__(self, config: MaaConfig, parent=None): - super().__init__( - FluentIcon.SETTING, "运行", "MAA运行调控选项", parent - ) - self.config = config - - self.card_TaskTransitionMethod = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="任务切换方式", - content="相邻两个任务间的切换方式,使用「详细」配置的用户固定为「重启模拟器」", - texts=["直接切换账号", "重启明日方舟", "重启模拟器"], - qconfig=self.config, - configItem=self.config.RunSet_TaskTransitionMethod, - parent=self, - ) - self.card_ProxyTimesLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户单日代理次数上限", - content="当用户本日代理成功次数达到该阈值时跳过代理,阈值为「0」时视为无代理次数上限", - range=(0, 1024), - qconfig=self.config, - configItem=self.config.RunSet_ProxyTimesLimit, - parent=self, - ) - self.card_ADBSearchRange = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="ADB端口号搜索范围", - content="在【±端口号范围】内搜索实际ADB端口号", - range=(0, 3), - qconfig=self.config, - configItem=self.config.RunSet_ADBSearchRange, - parent=self, - ) - self.card_RunTimesLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="代理重试次数限制", - content="若超过该次数限制仍未完成代理,视为代理失败", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.RunSet_RunTimesLimit, - parent=self, - ) - self.card_AnnihilationTimeLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="剿灭代理超时限制", - content="MAA日志无变化时间超过该阈值视为超时,单位为分钟", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.RunSet_AnnihilationTimeLimit, - parent=self, - ) - self.card_RoutineTimeLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="自动代理超时限制", - content="MAA日志无变化时间超过该阈值视为超时,单位为分钟", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.RunSet_RoutineTimeLimit, - parent=self, - ) - self.card_AnnihilationWeeklyLimit = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="每周剿灭仅执行到上限", - content="每周剿灭模式执行到上限,本周剩下时间不再执行剿灭任务", - qconfig=self.config, - configItem=self.config.RunSet_AnnihilationWeeklyLimit, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_TaskTransitionMethod) - Layout.addWidget(self.card_ProxyTimesLimit) - Layout.addWidget(self.card_ADBSearchRange) - Layout.addWidget(self.card_RunTimesLimit) - Layout.addWidget(self.card_AnnihilationTimeLimit) - Layout.addWidget(self.card_RoutineTimeLimit) - Layout.addWidget(self.card_AnnihilationWeeklyLimit) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class UserManager(HeaderCardWidget): - """用户管理父页面""" - - def __init__(self, name: str, parent=None): - super().__init__(parent) - - self.setObjectName(f"{name}_用户管理") - self.setTitle("下属用户") - self.name = name - - self.tools = CommandBar() - self.user_manager = self.UserSettingBox(self.name, self) - - # 逐个添加动作 - self.tools.addActions( - [ - Action( - FluentIcon.ADD_TO, "新建用户", triggered=self.add_user - ), - Action( - FluentIcon.REMOVE_FROM, - "删除用户", - triggered=self.del_user, - ), - ] - ) - self.tools.addSeparator() - self.tools.addActions( - [ - Action( - FluentIcon.LEFT_ARROW, - "向前移动", - triggered=self.left_user, - ), - Action( - FluentIcon.RIGHT_ARROW, - "向后移动", - triggered=self.right_user, - ), - ] - ) - - layout = QVBoxLayout() - layout.addWidget(self.tools) - layout.addWidget(self.user_manager) - self.viewLayout.addLayout(layout) - - def add_user(self): - """添加一个用户""" - - index = len(Config.script_dict[self.name]["UserData"]) + 1 - - logger.info(f"正在添加 {self.name} 用户_{index}", module="脚本管理") - - # 初始化用户配置信息 - user_config = MaaUserConfig() - user_config.load( - Config.script_dict[self.name]["Path"] - / f"UserData/用户_{index}/config.json", - user_config, - ) - user_config.save() - - Config.script_dict[self.name]["UserData"][f"用户_{index}"] = { - "Path": Config.script_dict[self.name]["Path"] - / f"UserData/用户_{index}", - "Config": user_config, - } - - # 添加用户设置面板 - self.user_manager.add_userSettingBox(index) - self.user_manager.switch_SettingBox(f"用户_{index}") - - logger.success( - f"{self.name} 用户_{index} 添加成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 添加 用户_{index}", 3000 - ) - SoundPlayer.play("添加用户") - - def del_user(self): - """删除一个用户""" - - name = self.user_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择用户", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择用户", "请先选择一个用户", 5000 - ) - return None - if name == "用户仪表盘": - logger.warning("试图删除用户仪表盘", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择用户", "请勿尝试删除用户仪表盘", 5000 - ) - return None - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - choice = MessageBox( - "确认", f"确定要删除 {name} 吗?", self.window() - ) - if choice.exec(): - - logger.info(f"正在删除 {self.name} {name}", module="脚本管理") - - self.user_manager.clear_SettingBox() - - # 删除用户配置文件并同步修改相应配置项 - shutil.rmtree( - Config.script_dict[self.name]["UserData"][name]["Path"] - ) - for i in range( - int(name[3:]) + 1, - len(Config.script_dict[self.name]["UserData"]) + 1, - ): - if Config.script_dict[self.name]["UserData"][f"用户_{i}"][ - "Path" - ].exists(): - Config.script_dict[self.name]["UserData"][f"用户_{i}"][ - "Path" - ].rename( - Config.script_dict[self.name]["UserData"][ - f"用户_{i}" - ]["Path"].with_name(f"用户_{i-1}") - ) - - self.user_manager.show_SettingBox( - f"用户_{max(int(name[3:]) - 1, 1)}" - ) - - logger.success( - f"{self.name} {name} 删除成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 删除 {name}", 3000 - ) - SoundPlayer.play("删除用户") - - def left_user(self): - """向前移动用户""" - - name = self.user_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择用户", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择用户", "请先选择一个用户", 5000 - ) - return None - if name == "用户仪表盘": - logger.warning("试图移动用户仪表盘", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择用户", "请勿尝试移动用户仪表盘", 5000 - ) - return None - - index = int(name[3:]) - - if index == 1: - logger.warning("向前移动用户时已到达最左端", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "已经是第一个用户", "无法向前移动", 5000 - ) - return None - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - logger.info(f"正在向前移动 {self.name} {name}", module="脚本管理") - - self.user_manager.clear_SettingBox() - - # 移动用户配置文件并同步修改配置项 - Config.script_dict[self.name]["UserData"][name]["Path"].rename( - Config.script_dict[self.name]["UserData"][name][ - "Path" - ].with_name("用户_0") - ) - Config.script_dict[self.name]["UserData"][f"用户_{index-1}"][ - "Path" - ].rename(Config.script_dict[self.name]["UserData"][name]["Path"]) - Config.script_dict[self.name]["UserData"][name]["Path"].with_name( - "用户_0" - ).rename( - Config.script_dict[self.name]["UserData"][f"用户_{index-1}"][ - "Path" - ] - ) - - self.user_manager.show_SettingBox(f"用户_{index - 1}") - - logger.success(f"{self.name} {name} 前移成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 前移 {name}", 3000 - ) - - def right_user(self): - """向后移动用户""" - - name = self.user_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择用户", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择用户", "请先选择一个用户", 5000 - ) - return None - if name == "用户仪表盘": - logger.warning("试图删除用户仪表盘", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择用户", "请勿尝试移动用户仪表盘", 5000 - ) - return None - - index = int(name[3:]) - - if index == len(Config.script_dict[self.name]["UserData"]): - logger.warning("向后移动用户时已到达最右端", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "已经是最后一个用户", "无法向后移动", 5000 - ) - return None - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - logger.info(f"正在向后移动 {self.name} {name}", module="脚本管理") - - self.user_manager.clear_SettingBox() - - Config.script_dict[self.name]["UserData"][name]["Path"].rename( - Config.script_dict[self.name]["UserData"][name][ - "Path" - ].with_name("用户_0") - ) - Config.script_dict[self.name]["UserData"][f"用户_{index+1}"][ - "Path" - ].rename(Config.script_dict[self.name]["UserData"][name]["Path"]) - Config.script_dict[self.name]["UserData"][name]["Path"].with_name( - "用户_0" - ).rename( - Config.script_dict[self.name]["UserData"][f"用户_{index+1}"][ - "Path" - ] - ) - - self.user_manager.show_SettingBox(f"用户_{index + 1}") - - logger.success(f"{self.name} {name} 后移成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 后移 {name}", 3000 - ) - - class UserSettingBox(QWidget): - """用户管理子页面组""" - - def __init__(self, name: str, parent=None): - super().__init__(parent) - - self.setObjectName("用户管理") - self.name = name - - 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: List[ - ScriptManager.ScriptSettingBox.MaaSettingBox.UserManager.UserSettingBox.UserMemberSettingBox - ] = [] - - self.user_dashboard = self.UserDashboard(self.name, self) - self.user_dashboard.switch_to.connect(self.switch_SettingBox) - self.stackedWidget.addWidget(self.user_dashboard) - self.pivot.addItem(routeKey="用户仪表盘", text="用户仪表盘") - - self.Layout = QVBoxLayout(self) - self.Layout.addWidget(self.pivotArea) - self.Layout.addWidget(self.stackedWidget) - self.Layout.setContentsMargins(0, 0, 0, 0) - - self.pivot.currentItemChanged.connect( - lambda index: self.switch_SettingBox( - index, if_change_pivot=False - ) - ) - - self.show_SettingBox("用户仪表盘") - - def show_SettingBox(self, index: str) -> None: - """ - 加载所有子界面并切换到指定子界面 - - :param index: 要切换到的子界面索引或名称 - :type index: str - """ - - Config.search_maa_user(self.name) - - for name in Config.script_dict[self.name]["UserData"].keys(): - self.add_userSettingBox(name[3:]) - - self.switch_SettingBox(index) - - def switch_SettingBox( - self, index: str, if_change_pivot: bool = True - ) -> None: - """ - 切换到指定的子界面 - - :param index: 要切换到的子界面索引或名称 - :type index: str - :param if_change_pivot: 是否更改导航栏的当前项 - :type if_change_pivot: bool - """ - - if len(Config.script_dict[self.name]["UserData"]) == 0: - index = "用户仪表盘" - - if index != "用户仪表盘" and int(index[3:]) > len( - Config.script_dict[self.name]["UserData"] - ): - return None - - if index == "用户仪表盘": - self.user_dashboard.load_info() - - if if_change_pivot: - self.pivot.setCurrentItem(index) - self.stackedWidget.setCurrentWidget( - self.user_dashboard - if index == "用户仪表盘" - else self.script_list[int(index[3:]) - 1] - ) - - def clear_SettingBox(self) -> None: - """清空除用户仪表盘外所有子界面""" - - for sub_interface in self.script_list: - Config.stage_refreshed.disconnect( - sub_interface.refresh_stage - ) - Config.PASSWORD_refreshed.disconnect( - sub_interface.refresh_password - ) - self.stackedWidget.removeWidget(sub_interface) - sub_interface.deleteLater() - self.script_list.clear() - self.pivot.clear() - self.user_dashboard.dashboard.setRowCount(0) - self.stackedWidget.addWidget(self.user_dashboard) - self.pivot.addItem(routeKey="用户仪表盘", text="用户仪表盘") - - def add_userSettingBox(self, uid: int) -> None: - """ - 添加一个用户设置界面 - - :param uid: 用户的唯一标识符 - :type uid: int - """ - - setting_box = self.UserMemberSettingBox(self.name, uid, self) - - self.script_list.append(setting_box) - - self.stackedWidget.addWidget(self.script_list[-1]) - - self.pivot.addItem(routeKey=f"用户_{uid}", text=f"用户 {uid}") - - class UserDashboard(HeaderCardWidget): - """用户仪表盘页面""" - - switch_to = Signal(str) - - def __init__(self, name: str, parent=None): - super().__init__(parent) - self.setObjectName("用户仪表盘") - self.setTitle("用户仪表盘") - self.name = name - - self.dashboard = TableWidget(self) - self.dashboard.setColumnCount(12) - self.dashboard.setHorizontalHeaderLabels( - [ - "用户名", - "账号ID", - "密码", - "状态", - "代理情况", - "给药量", - "关卡选择", - "备选 - 1", - "备选 - 2", - "备选 - 3", - "剩余理智", - "详", - ] - ) - self.dashboard.setEditTriggers(TableWidget.NoEditTriggers) - self.dashboard.verticalHeader().setVisible(False) - for col in range(6): - self.dashboard.horizontalHeader().setSectionResizeMode( - col, QHeaderView.ResizeMode.ResizeToContents - ) - for col in range(6, 11): - self.dashboard.horizontalHeader().setSectionResizeMode( - col, QHeaderView.ResizeMode.Stretch - ) - self.dashboard.horizontalHeader().setSectionResizeMode( - 11, QHeaderView.ResizeMode.Fixed - ) - self.dashboard.setColumnWidth(11, 32) - - self.viewLayout.addWidget(self.dashboard) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - Config.PASSWORD_refreshed.connect(self.load_info) - - def load_info(self): - """加载用户信息到仪表盘""" - - logger.info( - f"正在加载 {self.name} 用户信息到仪表盘", - module="脚本管理", - ) - - self.user_data = Config.script_dict[self.name]["UserData"] - - self.dashboard.setRowCount(len(self.user_data)) - - for name, info in self.user_data.items(): - - config = info["Config"] - - text_list = [] - if not config.get(config.Data_IfPassCheck): - text_list.append("未通过人工排查") - text_list.append( - f"今日已代理{config.get(config.Data_ProxyTimes)}次" - if Config.server_date().strftime("%Y-%m-%d") - == config.get(config.Data_LastProxyDate) - else "今日未进行代理" - ) - text_list.append( - "本周剿灭已完成" - if datetime.strptime( - config.get(config.Data_LastAnnihilationDate), - "%Y-%m-%d", - ).isocalendar()[:2] - == Config.server_date().isocalendar()[:2] - else "本周剿灭未完成" - ) - - stage_info = config.get_plan_info() - - button = PrimaryToolButton( - FluentIcon.CHEVRON_RIGHT, self - ) - button.setFixedSize(32, 32) - button.clicked.connect( - partial(self.switch_to.emit, name) - ) - - self.dashboard.setItem( - int(name[3:]) - 1, - 0, - QTableWidgetItem(config.get(config.Info_Name)), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 1, - QTableWidgetItem(config.get(config.Info_Id)), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 2, - QTableWidgetItem( - Crypto.AUTO_decryptor( - config.get(config.Info_Password), - Config.PASSWORD, - ) - if Config.PASSWORD - else "******" - ), - ) - self.dashboard.setCellWidget( - int(name[3:]) - 1, - 3, - StatusSwitchSetting( - qconfig=config, - configItem_check=config.Info_Status, - configItem_enable=config.Info_RemainedDay, - parent=self, - ), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 4, - QTableWidgetItem(" | ".join(text_list)), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 5, - QTableWidgetItem(str(stage_info["MedicineNumb"])), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 6, - QTableWidgetItem( - Config.stage_dict["ALL"]["text"][ - Config.stage_dict["ALL"]["value"].index( - stage_info["Stage"] - ) - ] - if stage_info["Stage"] - in Config.stage_dict["ALL"]["value"] - else stage_info["Stage"] - ), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 7, - QTableWidgetItem( - Config.stage_dict["ALL"]["text"][ - Config.stage_dict["ALL"]["value"].index( - stage_info["Stage_1"] - ) - ] - if stage_info["Stage_1"] - in Config.stage_dict["ALL"]["value"] - else stage_info["Stage_1"] - ), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 8, - QTableWidgetItem( - Config.stage_dict["ALL"]["text"][ - Config.stage_dict["ALL"]["value"].index( - stage_info["Stage_2"] - ) - ] - if stage_info["Stage_2"] - in Config.stage_dict["ALL"]["value"] - else stage_info["Stage_2"] - ), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 9, - QTableWidgetItem( - Config.stage_dict["ALL"]["text"][ - Config.stage_dict["ALL"]["value"].index( - stage_info["Stage_3"] - ) - ] - if stage_info["Stage_3"] - in Config.stage_dict["ALL"]["value"] - else stage_info["Stage_3"] - ), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 10, - QTableWidgetItem( - "不使用" - if stage_info["Stage_Remain"] == "-" - else ( - ( - Config.stage_dict["ALL"]["text"][ - Config.stage_dict["ALL"][ - "value" - ].index(stage_info["Stage_Remain"]) - ] - ) - if stage_info["Stage_Remain"] - in Config.stage_dict["ALL"]["value"] - else stage_info["Stage_Remain"] - ) - ), - ) - self.dashboard.setCellWidget( - int(name[3:]) - 1, 11, button - ) - - logger.success( - f"{self.name} 用户仪表盘成功加载信息", module="脚本管理" - ) - - class UserMemberSettingBox(HeaderCardWidget): - """用户管理子页面""" - - def __init__(self, name: str, uid: int, parent=None): - super().__init__(parent) - - self.setObjectName(f"用户_{uid}") - self.setTitle(f"用户 {uid}") - self.name = name - self.config = Config.script_dict[self.name]["UserData"][ - f"用户_{uid}" - ]["Config"] - self.user_path = Config.script_dict[self.name]["UserData"][ - f"用户_{uid}" - ]["Path"] - - plan_list = [ - ["固定"] + [_ for _ in Config.plan_dict.keys()], - ["固定"] - + [ - ( - k - if v["Config"].get(v["Config"].Info_Name) == "" - else f"{k} - {v["Config"].get(v["Config"].Info_Name)}" - ) - for k, v in Config.plan_dict.items() - ], - ] - - self.card_Name = LineEditSettingCard( - icon=FluentIcon.PEOPLE, - title="用户名", - content="用户的昵称", - text="请输入用户名", - qconfig=self.config, - configItem=self.config.Info_Name, - parent=self, - ) - self.card_Id = LineEditSettingCard( - icon=FluentIcon.PEOPLE, - title="账号ID", - content="官服输入手机号,B服输入B站ID", - text="请输入账号ID", - qconfig=self.config, - configItem=self.config.Info_Id, - parent=self, - ) - self.card_Mode = ComboBoxSettingCard( - icon=FluentIcon.DICTIONARY, - title="用户配置模式", - content="用户信息配置模式", - texts=["简洁", "详细"], - qconfig=self.config, - configItem=self.config.Info_Mode, - parent=self, - ) - self.card_Server = ComboBoxSettingCard( - icon=FluentIcon.PROJECTOR, - title="服务器", - content="选择服务器类型", - texts=[ - "官服", - "B服", - "悠星国际服", - "悠星日服", - "悠星韩服", - "繁中服", - ], - qconfig=self.config, - configItem=self.config.Info_Server, - parent=self, - ) - self.card_Status = SwitchSettingCard( - icon=FluentIcon.CHECKBOX, - title="用户状态", - content="启用或禁用该用户", - qconfig=self.config, - configItem=self.config.Info_Status, - parent=self, - ) - self.card_RemainedDay = SpinBoxSettingCard( - icon=FluentIcon.CALENDAR, - title="剩余天数", - content="剩余代理天数,-1表示无限代理", - range=(-1, 1024), - qconfig=self.config, - configItem=self.config.Info_RemainedDay, - parent=self, - ) - self.card_Annihilation = ComboBoxSettingCard( - icon=FluentIcon.CAFE, - title="剿灭代理", - content="剿灭代理子任务相关设置", - texts=[ - "关闭", - "当期剿灭", - "切尔诺伯格", - "龙门外环", - "龙门市区", - ], - qconfig=self.config, - configItem=self.config.Info_Annihilation, - parent=self, - ) - self.card_Routine = PushAndSwitchButtonSettingCard( - icon=FluentIcon.CAFE, - title="日常代理", - content="日常代理子任务相关设置", - text="设置具体配置", - qconfig=self.config, - configItem=self.config.Info_Routine, - parent=self, - ) - self.card_InfrastMode = PushAndComboBoxSettingCard( - icon=FluentIcon.CAFE, - title="基建模式", - content="自定义基建配置文件未生效", - text="选择配置文件", - texts=[ - "常规模式", - "一键轮休", - "自定义基建", - ], - qconfig=self.config, - configItem=self.config.Info_InfrastMode, - parent=self, - ) - self.card_Password = PasswordLineEditSettingCard( - icon=FluentIcon.VPN, - title="密码", - content="仅用于用户密码记录", - text="请输入用户密码", - algorithm="AUTO", - qconfig=self.config, - configItem=self.config.Info_Password, - parent=self, - ) - self.card_Notes = LineEditSettingCard( - icon=FluentIcon.PENCIL_INK, - title="备注", - content="用户备注信息", - text="请输入备注", - qconfig=self.config, - configItem=self.config.Info_Notes, - parent=self, - ) - self.card_MedicineNumb = SpinBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="吃理智药", - content="吃理智药次数,输入0以关闭", - range=(0, 1024), - qconfig=self.config, - configItem=self.config.Info_MedicineNumb, - parent=self, - ) - self.card_SeriesNumb = ComboBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="连战次数", - content="连战次数较大时建议搭配剩余理智关卡使用", - texts=["AUTO", "6", "5", "4", "3", "2", "1", "不选择"], - qconfig=self.config, - configItem=self.config.Info_SeriesNumb, - parent=self, - ) - self.card_SeriesNumb.comboBox.setMinimumWidth(150) - self.card_StageMode = NoOptionComboBoxSettingCard( - icon=FluentIcon.DICTIONARY, - title="关卡配置模式", - content="刷理智关卡号的配置模式", - value=plan_list[0], - texts=plan_list[1], - qconfig=self.config, - configItem=self.config.Info_StageMode, - parent=self, - ) - self.card_StageMode.comboBox.setMinimumWidth(150) - self.card_Stage = EditableComboBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="关卡选择", - content="按下回车以添加自定义关卡号", - value=Config.stage_dict["ALL"]["value"], - texts=Config.stage_dict["ALL"]["text"], - qconfig=self.config, - configItem=self.config.Info_Stage, - parent=self, - ) - self.card_Stage_1 = EditableComboBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="备选关卡 - 1", - content="按下回车以添加自定义关卡号", - value=Config.stage_dict["ALL"]["value"], - texts=Config.stage_dict["ALL"]["text"], - qconfig=self.config, - configItem=self.config.Info_Stage_1, - parent=self, - ) - self.card_Stage_2 = EditableComboBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="备选关卡 - 2", - content="按下回车以添加自定义关卡号", - value=Config.stage_dict["ALL"]["value"], - texts=Config.stage_dict["ALL"]["text"], - qconfig=self.config, - configItem=self.config.Info_Stage_2, - parent=self, - ) - self.card_Stage_3 = EditableComboBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="备选关卡 - 3", - content="按下回车以添加自定义关卡号", - value=Config.stage_dict["ALL"]["value"], - texts=Config.stage_dict["ALL"]["text"], - qconfig=self.config, - configItem=self.config.Info_Stage_3, - parent=self, - ) - self.card_Stage_Remain = ( - EditableComboBoxWithPlanSettingCard( - icon=FluentIcon.GAME, - title="剩余理智关卡", - content="按下回车以添加自定义关卡号", - value=Config.stage_dict["ALL"]["value"], - texts=[ - "不使用" if _ == "当前/上次" else _ - for _ in Config.stage_dict["ALL"]["text"] - ], - qconfig=self.config, - configItem=self.config.Info_Stage_Remain, - parent=self, - ) - ) - self.card_Skland = PasswordLineAndSwitchButtonSettingCard( - icon=FluentIcon.CERTIFICATE, - title="森空岛签到", - content="此功能具有一定风险,请谨慎使用!获取登录凭证请查阅「文档-进阶功能」。", - text="鹰角网络通行证登录凭证", - algorithm="DPAPI", - qconfig=self.config, - configItem_bool=self.config.Info_IfSkland, - configItem_info=self.config.Info_SklandToken, - parent=self, - ) - self.card_Skland.LineEdit.setMinimumWidth(250) - - self.card_UserLable = UserLableSettingCard( - icon=FluentIcon.INFO, - title="状态信息", - content="用户的代理情况汇总", - qconfig=self.config, - configItems={ - "LastProxyDate": self.config.Data_LastProxyDate, - "LastAnnihilationDate": self.config.Data_LastAnnihilationDate, - "ProxyTimes": self.config.Data_ProxyTimes, - "IfPassCheck": self.config.Data_IfPassCheck, - "IfSkland": self.config.Info_IfSkland, - "LastSklandDate": self.config.Data_LastSklandDate, - }, - parent=self, - ) - - # 单独任务卡片 - self.card_TaskSet = UserTaskSettingCard( - icon=FluentIcon.LIBRARY, - title="自动日常代理任务序列", - content="未启用任何任务项", - text="设置", - qconfig=self.config, - configItems={ - "IfWakeUp": self.config.Task_IfWakeUp, - "IfRecruiting": self.config.Task_IfRecruiting, - "IfBase": self.config.Task_IfBase, - "IfCombat": self.config.Task_IfCombat, - "IfMall": self.config.Task_IfMall, - "IfMission": self.config.Task_IfMission, - "IfAutoRoguelike": self.config.Task_IfAutoRoguelike, - "IfReclamation": self.config.Task_IfReclamation, - }, - parent=self, - ) - self.card_IfWakeUp = SwitchSettingCard( - icon=FluentIcon.TILES, - title="开始唤醒", - content="", - qconfig=self.config, - configItem=self.config.Task_IfWakeUp, - parent=self, - ) - self.card_IfRecruiting = SwitchSettingCard( - icon=FluentIcon.TILES, - title="自动公招", - content="", - qconfig=self.config, - configItem=self.config.Task_IfRecruiting, - parent=self, - ) - self.card_IfBase = SwitchSettingCard( - icon=FluentIcon.TILES, - title="基建换班", - content="", - qconfig=self.config, - configItem=self.config.Task_IfBase, - parent=self, - ) - self.card_IfCombat = SwitchSettingCard( - icon=FluentIcon.TILES, - title="刷理智", - content="", - qconfig=self.config, - configItem=self.config.Task_IfCombat, - parent=self, - ) - self.card_IfMall = SwitchSettingCard( - icon=FluentIcon.TILES, - title="获取信用及购物", - content="", - qconfig=self.config, - configItem=self.config.Task_IfMall, - parent=self, - ) - self.card_IfMission = SwitchSettingCard( - icon=FluentIcon.TILES, - title="领取奖励", - content="", - qconfig=self.config, - configItem=self.config.Task_IfMission, - parent=self, - ) - self.card_IfAutoRoguelike = SwitchSettingCard( - icon=FluentIcon.TILES, - title="自动肉鸽", - content="", - qconfig=self.config, - configItem=self.config.Task_IfAutoRoguelike, - parent=self, - ) - self.card_IfReclamation = SwitchSettingCard( - icon=FluentIcon.TILES, - title="生息演算", - content="", - qconfig=self.config, - configItem=self.config.Task_IfReclamation, - parent=self, - ) - - self.TaskSetCard = SettingFlyoutView( - self, - "自动日常代理任务序列设置", - [ - self.card_IfWakeUp, - self.card_IfRecruiting, - self.card_IfBase, - self.card_IfCombat, - self.card_IfMall, - self.card_IfMission, - self.card_IfAutoRoguelike, - self.card_IfReclamation, - ], - ) - - # 单独通知卡片 - self.card_NotifySet = UserNoticeSettingCard( - icon=FluentIcon.MAIL, - title="用户单独通知设置", - content="未启用任何通知项", - text="设置", - qconfig=self.config, - configItem=self.config.Notify_Enabled, - configItems={ - "IfSendStatistic": self.config.Notify_IfSendStatistic, - "IfSendSixStar": self.config.Notify_IfSendSixStar, - "IfSendMail": self.config.Notify_IfSendMail, - "ToAddress": self.config.Notify_ToAddress, - "IfServerChan": self.config.Notify_IfServerChan, - "ServerChanKey": self.config.Notify_ServerChanKey, - "IfCompanyWebHookBot": self.config.Notify_IfCompanyWebHookBot, - "CompanyWebHookBotUrl": self.config.Notify_CompanyWebHookBotUrl, - }, - parent=self, - ) - self.card_NotifyContent = self.NotifyContentSettingCard( - self.config, self - ) - self.card_EMail = self.EMailSettingCard(self.config, self) - self.card_ServerChan = self.ServerChanSettingCard( - self.config, self - ) - self.card_CompanyWebhookBot = ( - self.CompanyWechatPushSettingCard(self.config, self) - ) - - self.NotifySetCard = SettingFlyoutView( - self, - "用户通知设置", - [ - self.card_NotifyContent, - self.card_EMail, - self.card_ServerChan, - self.card_CompanyWebhookBot, - ], - ) - - h1_layout = QHBoxLayout() - h1_layout.addWidget(self.card_Name) - h1_layout.addWidget(self.card_Id) - h2_layout = QHBoxLayout() - h2_layout.addWidget(self.card_Mode) - h2_layout.addWidget(self.card_Server) - h3_layout = QHBoxLayout() - h3_layout.addWidget(self.card_Status) - h3_layout.addWidget(self.card_RemainedDay) - h4_layout = QHBoxLayout() - h4_layout.addWidget(self.card_Annihilation) - h4_layout.addWidget(self.card_Routine) - h4_layout.addWidget(self.card_InfrastMode) - h5_layout = QHBoxLayout() - h5_layout.addWidget(self.card_Password) - h5_layout.addWidget(self.card_Notes) - h6_layout = QHBoxLayout() - h6_layout.addWidget(self.card_MedicineNumb) - h6_layout.addWidget(self.card_SeriesNumb) - h7_layout = QHBoxLayout() - h7_layout.addWidget(self.card_StageMode) - h7_layout.addWidget(self.card_Stage) - h8_layout = QHBoxLayout() - h8_layout.addWidget(self.card_Stage_1) - h8_layout.addWidget(self.card_Stage_2) - h9_layout = QHBoxLayout() - h9_layout.addWidget(self.card_Stage_3) - h9_layout.addWidget(self.card_Stage_Remain) - - Layout = QVBoxLayout() - Layout.addLayout(h1_layout) - Layout.addLayout(h2_layout) - Layout.addLayout(h3_layout) - Layout.addWidget(self.card_UserLable) - Layout.addLayout(h4_layout) - Layout.addLayout(h5_layout) - Layout.addLayout(h6_layout) - Layout.addLayout(h7_layout) - Layout.addLayout(h8_layout) - Layout.addLayout(h9_layout) - Layout.addWidget(self.card_Skland) - Layout.addWidget(self.card_TaskSet) - Layout.addWidget(self.card_NotifySet) - - self.viewLayout.addLayout(Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - self.card_Mode.comboBox.currentIndexChanged.connect( - self.switch_mode - ) - self.card_InfrastMode.comboBox.currentIndexChanged.connect( - self.switch_infrastructure - ) - self.card_Routine.clicked.connect( - lambda: self.set_maa("Routine") - ) - self.card_InfrastMode.clicked.connect( - self.set_infrastructure - ) - self.card_TaskSet.clicked.connect(self.set_task) - self.card_NotifySet.clicked.connect(self.set_notify) - self.card_StageMode.comboBox.currentIndexChanged.connect( - self.switch_stage_mode - ) - Config.stage_refreshed.connect(self.refresh_stage) - Config.PASSWORD_refreshed.connect(self.refresh_password) - - self.switch_mode() - self.switch_stage_mode() - self.switch_infrastructure() - - def switch_mode(self) -> None: - """切换用户配置模式""" - - if self.config.get(self.config.Info_Mode) == "简洁": - - self.card_Routine.setVisible(False) - self.card_InfrastMode.setVisible(True) - - elif self.config.get(self.config.Info_Mode) == "详细": - - self.card_InfrastMode.setVisible(False) - self.card_Routine.setVisible(True) - - def switch_stage_mode(self) -> None: - """切换关卡配置模式""" - - for card, name in zip( - [ - self.card_MedicineNumb, - self.card_SeriesNumb, - self.card_Stage, - self.card_Stage_1, - self.card_Stage_2, - self.card_Stage_3, - self.card_Stage_Remain, - ], - [ - "MedicineNumb", - "SeriesNumb", - "Stage", - "Stage_1", - "Stage_2", - "Stage_3", - "Stage_Remain", - ], - ): - - card.switch_mode( - self.config.get(self.config.Info_StageMode)[:2] - ) - if ( - self.config.get(self.config.Info_StageMode) - != "固定" - ): - card.change_plan( - Config.plan_dict[ - self.config.get(self.config.Info_StageMode) - ]["Config"].get_current_info(name) - ) - - def switch_infrastructure(self) -> None: - """切换基建配置模式""" - - if ( - self.config.get(self.config.Info_InfrastMode) - == "Custom" - ): - self.card_InfrastMode.button.setVisible(True) - with ( - self.user_path - / "Infrastructure/infrastructure.json" - ).open(mode="r", encoding="utf-8") as f: - infrastructure = json.load(f) - self.card_InfrastMode.setContent( - f"当前基建配置:{infrastructure.get("title","未命名")}" - ) - else: - self.card_InfrastMode.button.setVisible(False) - self.card_InfrastMode.setContent( - "自定义基建配置文件未生效" - ) - - def refresh_stage(self): - """刷新关卡配置""" - - self.card_Stage.reLoadOptions( - Config.stage_dict["ALL"]["value"], - Config.stage_dict["ALL"]["text"], - ) - self.card_Stage_1.reLoadOptions( - Config.stage_dict["ALL"]["value"], - Config.stage_dict["ALL"]["text"], - ) - self.card_Stage_2.reLoadOptions( - Config.stage_dict["ALL"]["value"], - Config.stage_dict["ALL"]["text"], - ) - self.card_Stage_3.reLoadOptions( - Config.stage_dict["ALL"]["value"], - Config.stage_dict["ALL"]["text"], - ) - self.card_Stage_Remain.reLoadOptions( - Config.stage_dict["ALL"]["value"], - Config.stage_dict["ALL"]["text"], - ) - - def refresh_password(self): - """刷新密码配置""" - - self.card_Password.setValue( - self.card_Password.qconfig.get( - self.card_Password.configItem - ) - ) - - def set_infrastructure(self) -> None: - """配置自定义基建""" - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - file_path, _ = QFileDialog.getOpenFileName( - self, - "选择自定义基建文件", - ".", - "JSON 文件 (*.json)", - ) - if file_path != "": - (self.user_path / "Infrastructure").mkdir( - parents=True, exist_ok=True - ) - shutil.copy( - file_path, - self.user_path - / "Infrastructure/infrastructure.json", - ) - self.switch_infrastructure() - else: - logger.warning("未选择自定义基建文件") - MainInfoBar.push_info_bar( - "warning", "警告", "未选择自定义基建文件", 5000 - ) - - def set_maa(self, mode: str) -> None: - """配置MAA子配置""" - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - TaskManager.add_task( - "设置MAA_用户", - self.name, - { - "SetMaaInfo": { - "Path": self.user_path / mode, - } - }, - ) - - def set_task(self) -> None: - """设置用户任务序列相关配置""" - - self.TaskSetCard.setVisible(True) - Flyout.make( - self.TaskSetCard, - self.card_TaskSet, - self, - aniType=FlyoutAnimationType.PULL_UP, - isDeleteOnClose=False, - ) - - def set_notify(self) -> None: - """设置用户通知相关配置""" - - self.NotifySetCard.setVisible(True) - Flyout.make( - self.NotifySetCard, - self.card_NotifySet, - self, - aniType=FlyoutAnimationType.PULL_UP, - isDeleteOnClose=False, - ) - - class NotifyContentSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户通知内容选项") - - self.config = config - - self.card_IfSendStatistic = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送统计信息", - content="推送自动代理统计信息的通知", - qconfig=self.config, - configItem=self.config.Notify_IfSendStatistic, - parent=self, - ) - self.card_IfSendSixStar = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送公招高资喜报", - content="公招出现六星词条时推送喜报", - qconfig=self.config, - configItem=self.config.Notify_IfSendSixStar, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfSendStatistic) - Layout.addWidget(self.card_IfSendSixStar) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class EMailSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户邮箱通知") - - self.config = config - - self.card_IfSendMail = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送用户邮件通知", - content="是否启用用户邮件通知功能", - qconfig=self.config, - configItem=self.config.Notify_IfSendMail, - parent=self, - ) - self.card_ToAddress = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户收信邮箱地址", - content="接收用户通知的邮箱地址", - text="请输入用户收信邮箱地址", - qconfig=self.config, - configItem=self.config.Notify_ToAddress, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfSendMail) - Layout.addWidget(self.card_ToAddress) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class ServerChanSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户ServerChan通知") - - self.config = config - - self.card_IfServerChan = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送用户Server酱通知", - content="是否启用用户Server酱通知功能", - qconfig=self.config, - configItem=self.config.Notify_IfServerChan, - parent=self, - ) - self.card_ServerChanKey = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户SendKey", - content="SC3与SCT均须填写", - text="请输入用户SendKey", - qconfig=self.config, - configItem=self.config.Notify_ServerChanKey, - parent=self, - ) - self.card_ServerChanChannel = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户ServerChanChannel代码", - content="留空则默认,多个请使用「|」隔开", - text="请输入Channel代码,仅SCT生效", - qconfig=self.config, - configItem=self.config.Notify_ServerChanChannel, - parent=self, - ) - self.card_ServerChanTag = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户Tag内容", - content="留空则默认,多个请使用「|」隔开", - text="请输入加入推送的Tag,仅SC3生效", - qconfig=self.config, - configItem=self.config.Notify_ServerChanTag, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfServerChan) - Layout.addWidget(self.card_ServerChanKey) - Layout.addWidget(self.card_ServerChanChannel) - Layout.addWidget(self.card_ServerChanTag) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class CompanyWechatPushSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户企业微信推送") - - self.config = config - - self.card_IfCompanyWebHookBot = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送用户企业微信机器人通知", - content="是否启用用户企微机器人通知功能", - qconfig=self.config, - configItem=self.config.Notify_IfCompanyWebHookBot, - parent=self, - ) - self.card_CompanyWebHookBotUrl = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="WebhookUrl", - content="用户企微群机器人Webhook地址", - text="请输入用户Webhook的Url", - qconfig=self.config, - configItem=self.config.Notify_CompanyWebHookBotUrl, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfCompanyWebHookBot) - Layout.addWidget(self.card_CompanyWebHookBotUrl) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class GeneralSettingBox(QWidget): - """通用脚本设置界面""" - - def __init__(self, uid: int, parent=None): - super().__init__(parent) - - self.setObjectName(f"脚本_{uid}") - self.config = Config.script_dict[f"脚本_{uid}"]["Config"] - - self.app_setting = self.AppSettingCard(f"脚本_{uid}", self.config, self) - self.branch_manager = self.BranchManager(f"脚本_{uid}", self) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 11, 0) - content_layout.addWidget(self.app_setting) - content_layout.addWidget(self.branch_manager) - content_layout.addStretch(1) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(scrollArea) - - class AppSettingCard(HeaderCardWidget): - - def __init__(self, name: str, config: GeneralConfig, parent=None): - super().__init__(parent) - - self.setTitle("通用实例") - - self.name = name - self.config = config - - Layout = QVBoxLayout() - - self.card_Name = LineEditSettingCard( - icon=FluentIcon.EDIT, - title="实例名称", - content="用于标识通用实例的名称", - text="请输入实例名称", - qconfig=self.config, - configItem=self.config.Script_Name, - parent=self, - ) - self.card_Script = self.ScriptSettingCard(self.config, self) - self.card_Game = self.GameSettingCard(self.config, self) - self.card_Run = self.RunSettingCard(self.config, self) - self.card_Config = self.ConfigSettingCard( - self.name, self.config, self - ) - - Layout.addWidget(self.card_Name) - Layout.addWidget(self.card_Script) - Layout.addWidget(self.card_Game) - Layout.addWidget(self.card_Run) - Layout.addWidget(self.card_Config) - self.viewLayout.addLayout(Layout) - - class ScriptSettingCard(ExpandGroupSettingCard): - - def __init__(self, config: GeneralConfig, parent=None): - super().__init__( - FluentIcon.SETTING, "脚本设置", "脚本属性配置选项", parent - ) - self.config = config - - self.card_RootPath = PathSettingCard( - icon=FluentIcon.FOLDER, - title="脚本根目录 - [必填]", - mode="文件夹", - text="选择文件夹", - qconfig=self.config, - configItem=self.config.Script_RootPath, - parent=self, - ) - self.card_ScriptPath = PathSettingCard( - icon=FluentIcon.FOLDER, - title="脚本路径 - [必填]", - mode="可执行文件 (*.exe *.bat)", - text="选择程序", - qconfig=self.config, - configItem=self.config.Script_ScriptPath, - parent=self, - ) - self.card_Arguments = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本启动参数", - content="脚本启动时的附加参数", - text="请输入脚本参数", - qconfig=self.config, - configItem=self.config.Script_Arguments, - parent=self, - ) - self.card_IfTrackProcess = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="追踪脚本子进程", - content="启用后将在脚本启动后 60s 内追踪其子进程,并仅在所有子进程结束后判定脚本中止", - qconfig=self.config, - configItem=self.config.Script_IfTrackProcess, - parent=self, - ) - self.card_ConfigPath = PathSettingCard( - icon=FluentIcon.FOLDER, - title="脚本配置文件路径 - [必填]", - mode=self.config.Script_ConfigPathMode, - text="选择路径", - qconfig=self.config, - configItem=self.config.Script_ConfigPath, - parent=self, - ) - self.card_UpdateConfigMode = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本配置文件更新时机", - content="在选定的时机自动更新程序保存的配置文件", - texts=[ - "从不", - "仅任务成功后", - "仅任务失败后", - "任务完成后", - ], - qconfig=self.config, - configItem=self.config.Script_UpdateConfigMode, - parent=self, - ) - self.card_LogPath = PathSettingCard( - icon=FluentIcon.FOLDER, - title="脚本日志文件路径 - [必填]", - mode="所有文件 (*)", - text="选择文件", - qconfig=self.config, - configItem=self.config.Script_LogPath, - parent=self, - ) - self.card_LogPathFormat = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本日志文件名格式", - content="若脚本日志文件名中随时间变化,请填入时间格式文本,留空则不启用", - text="请输入脚本日志文件名格式", - qconfig=self.config, - configItem=self.config.Script_LogPathFormat, - parent=self, - ) - self.card_LogTimeStart = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本日志时间戳起始位置 - [必填]", - content="脚本日志中时间戳的起始位置,单位为字符", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.Script_LogTimeStart, - parent=self, - ) - self.card_LogTimeEnd = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本日志时间戳结束位置 - [必填]", - content="脚本日志中时间戳的结束位置,单位为字符", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.Script_LogTimeEnd, - parent=self, - ) - self.card_LogTimeFormat = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本日志时间戳格式 - [必填]", - content="脚本日志中时间戳的格式", - text="请输入脚本日志时间格式", - qconfig=self.config, - configItem=self.config.Script_LogTimeFormat, - parent=self, - ) - self.card_SuccessLog = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本成功日志", - content="任务成功完成时出现的日志,多条请使用「|」隔开", - text="请输入脚本成功日志内容", - qconfig=self.config, - configItem=self.config.Script_SuccessLog, - parent=self, - ) - self.card_ErrorLog = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="脚本异常日志 - [必填]", - content="脚本运行异常时的日志内容,多条请使用「|」隔开", - text="请输入脚本异常日志内容", - qconfig=self.config, - configItem=self.config.Script_ErrorLog, - parent=self, - ) - - self.card_RootPath.pathChanged.connect(self.change_path) - self.card_ScriptPath.pathChanged.connect( - lambda old, new: self.check_path( - self.config.Script_ScriptPath, old, new - ) - ) - self.card_ConfigPath.pathChanged.connect( - lambda old, new: self.check_path( - self.config.Script_ConfigPath, old, new - ) - ) - self.card_LogPath.pathChanged.connect( - lambda old, new: self.check_path( - self.config.Script_LogPath, old, new - ) - ) - - h_layout = QHBoxLayout() - h_layout.addWidget(self.card_LogTimeStart) - h_layout.addWidget(self.card_LogTimeEnd) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_RootPath) - Layout.addWidget(self.card_ScriptPath) - Layout.addWidget(self.card_Arguments) - Layout.addWidget(self.card_IfTrackProcess) - Layout.addWidget(self.card_ConfigPath) - Layout.addWidget(self.card_UpdateConfigMode) - Layout.addWidget(self.card_LogPath) - Layout.addWidget(self.card_LogPathFormat) - Layout.addLayout(h_layout) - Layout.addWidget(self.card_LogTimeFormat) - Layout.addWidget(self.card_SuccessLog) - Layout.addWidget(self.card_ErrorLog) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - def change_path(self, old_path: Path, new_path: Path) -> None: - """ - 根据脚本根目录重新计算配置文件路径 - - :param old_path: 旧路径 - :param new_path: 新路径 - """ - - path_list = [ - self.config.Script_ScriptPath, - self.config.Script_ConfigPath, - self.config.Script_LogPath, - ] - - for path in path_list: - - if Path(self.config.get(path)).is_relative_to(old_path): - - relative_path = Path(self.config.get(path)).relative_to( - old_path - ) - self.config.set(path, str(new_path / relative_path)) - - def check_path( - self, configItem: ConfigItem, old_path: Path, new_path: Path - ) -> None: - """检查配置路径是否合法""" - - if not new_path.is_relative_to( - Path(self.config.get(self.config.Script_RootPath)) - ): - - self.config.set(configItem, str(old_path)) - logger.warning( - f"配置路径 {new_path} 不在脚本根目录下,已重置为 {old_path}", - module="脚本管理", - ) - MainInfoBar.push_info_bar( - "warning", "路径异常", "所选路径不在脚本根目录下", 5000 - ) - - class GameSettingCard(ExpandGroupSettingCard): - - def __init__(self, config: GeneralConfig, parent=None): - super().__init__( - FluentIcon.SETTING, - "游戏设置", - "游戏/模拟器属性配置选项", - parent, - ) - self.config = config - - self.card_Enabled = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="游戏/模拟器相关功能", - content="是否由AUTO_MAA管理游戏/模拟器相关进程", - qconfig=self.config, - configItem=self.config.Game_Enabled, - parent=self, - ) - self.card_Style = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="游戏平台类型", - content="游戏运行在安卓模拟器还是客户端上", - texts=["安卓模拟器", "客户端"], - qconfig=self.config, - configItem=self.config.Game_Style, - parent=self, - ) - self.card_Path = PathSettingCard( - icon=FluentIcon.FOLDER, - title="游戏/模拟器路径", - mode="可执行文件 (*.exe *.bat)", - text="选择文件", - qconfig=self.config, - configItem=self.config.Game_Path, - parent=self, - ) - self.card_Arguments = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="游戏/模拟器启动参数", - content="游戏/模拟器启动时的附加参数", - text="请输入游戏/模拟器参数", - qconfig=self.config, - configItem=self.config.Game_Arguments, - parent=self, - ) - self.card_WaitTime = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="等待游戏/模拟器启动时间", - content="启动游戏/模拟器与启动对应脚本的间隔时间,单位为秒", - range=(0, 1024), - qconfig=self.config, - configItem=self.config.Game_WaitTime, - parent=self, - ) - self.card_IfForceClose = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="游戏/模拟器强制关闭", - content="是否强制结束所有同路径进程", - qconfig=self.config, - configItem=self.config.Game_IfForceClose, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_Enabled) - Layout.addWidget(self.card_Style) - Layout.addWidget(self.card_Path) - Layout.addWidget(self.card_Arguments) - Layout.addWidget(self.card_WaitTime) - Layout.addWidget(self.card_IfForceClose) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class RunSettingCard(ExpandGroupSettingCard): - - def __init__(self, config: GeneralConfig, parent=None): - super().__init__( - FluentIcon.SETTING, "运行设置", "运行调控配置选项", parent - ) - self.config = config - - self.card_ProxyTimesLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="子配置单日代理次数上限", - content="当子配置本日代理成功次数达到该阈值时跳过代理,阈值为「0」时视为无代理次数上限", - range=(0, 1024), - qconfig=self.config, - configItem=self.config.Run_ProxyTimesLimit, - parent=self, - ) - - self.card_RunTimesLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="代理重试次数限制", - content="若超过该次数限制仍未完成代理,视为代理失败", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.Run_RunTimesLimit, - parent=self, - ) - self.card_RunTimeLimit = SpinBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="自动代理超时限制", - content="脚本日志无变化时间超过该阈值视为超时,单位为分钟", - range=(1, 1024), - qconfig=self.config, - configItem=self.config.Run_RunTimeLimit, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_ProxyTimesLimit) - Layout.addWidget(self.card_RunTimesLimit) - Layout.addWidget(self.card_RunTimeLimit) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class ConfigSettingCard(ExpandGroupSettingCard): - - def __init__(self, name: str, config: GeneralConfig, parent=None): - super().__init__( - FluentIcon.SETTING, - "配置管理", - "使用配置模板文件快速设置脚本", - parent, - ) - self.name = name - self.config = config - - self.card_ImportFromFile = PushSettingCard( - text="从文件导入", - icon=FluentIcon.PAGE_RIGHT, - title="从文件导入通用配置", - content="选择一个配置文件,导入其中的配置信息", - parent=self, - ) - self.card_ExportToFile = PushSettingCard( - text="导出到文件", - icon=FluentIcon.PAGE_RIGHT, - title="导出通用配置到文件", - content="选择一个保存路径,将当前配置信息导出到文件", - parent=self, - ) - self.card_ImportFromWeb = PushSettingCard( - text="查看", - icon=FluentIcon.PAGE_RIGHT, - title="从「AUTO_MAA 配置分享中心」导入", - content="从「AUTO_MAA 配置分享中心」选择一个用户分享的通用配置模板,导入其中的配置信息", - parent=self, - ) - self.card_UploadToWeb = PushSettingCard( - text="上传", - icon=FluentIcon.PAGE_RIGHT, - title="上传到「AUTO_MAA 配置分享中心」", - content="将当前通用配置分享到「AUTO_MAA 配置分享中心」,通过审核后可供其他用户下载使用", - parent=self, - ) - - self.card_ImportFromFile.clicked.connect(self.import_from_file) - self.card_ExportToFile.clicked.connect(self.export_to_file) - self.card_ImportFromWeb.clicked.connect(self.import_from_web) - self.card_UploadToWeb.clicked.connect(self.upload_to_web) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_ImportFromFile) - Layout.addWidget(self.card_ExportToFile) - Layout.addWidget(self.card_ImportFromWeb) - Layout.addWidget(self.card_UploadToWeb) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - def import_from_file(self): - """从文件导入配置""" - - file_path, _ = QFileDialog.getOpenFileName( - self, "选择配置文件", "", "JSON Files (*.json)" - ) - if file_path: - - shutil.copy( - file_path, - Config.script_dict[self.name]["Path"] / "config.json", - ) - self.config.load( - Config.script_dict[self.name]["Path"] / "config.json" - ) - - logger.success( - f"{self.name} 配置导入成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", - "操作成功", - f"{self.name} 配置导入成功", - 3000, - ) - - def export_to_file(self): - """导出配置到文件""" - - file_path, _ = QFileDialog.getSaveFileName( - self, "选择保存路径", "", "JSON Files (*.json)" - ) - if file_path: - - temp = self.config.toDict() - - # 移除配置中可能存在的隐私信息 - temp["Script"]["Name"] = Path(file_path).stem - for path in ["ScriptPath", "ConfigPath", "LogPath"]: - - if Path(temp["Script"][path]).is_relative_to( - Path(temp["Script"]["RootPath"]) - ): - - temp["Script"][path] = str( - Path(r"C:/脚本根目录") - / Path(temp["Script"][path]).relative_to( - Path(temp["Script"]["RootPath"]) - ) - ) - temp["Script"]["RootPath"] = str(Path(r"C:/脚本根目录")) - - with open(file_path, "w", encoding="utf-8") as file: - json.dump(temp, file, ensure_ascii=False, indent=4) - - logger.success( - f"{self.name} 配置导出成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", - "操作成功", - f"{self.name} 配置导出成功", - 3000, - ) - - def import_from_web(self): - """从「AUTO_MAA 配置分享中心」导入配置""" - - # 从远程服务器获取配置列表 - network = Network.add_task( - mode="get", - url="http://221.236.27.82:10023/api/list/config/general", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - config_info: List[Dict[str, str]] = network_result[ - "response_json" - ] - else: - logger.warning( - f"获取配置列表时出错:{network_result['error_message']}", - module="脚本管理", - ) - MainInfoBar.push_info_bar( - "warning", - "获取配置列表时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - choice = NoticeMessageBox( - self.window(), - "配置分享中心", - { - _[ - "configName" - ]: f""" -# {_['configName']} - -- **作者**: {_['author']} - -- **发布时间**:{_['createTime']} - -- **描述**:{_['description']} -""" - for _ in config_info - }, - ) - if choice.exec() and choice.currentIndex != 0: - - # 从远程服务器获取具体配置 - network = Network.add_task( - mode="get", - url=config_info[choice.currentIndex - 1]["downloadUrl"], - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - config_data = network_result["response_json"] - else: - logger.warning( - f"获取配置列表时出错:{network_result['error_message']}", - module="脚本管理", - ) - MainInfoBar.push_info_bar( - "warning", - "获取配置列表时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - with ( - Config.script_dict[self.name]["Path"] / "config.json" - ).open("w", encoding="utf-8") as file: - json.dump( - config_data, file, ensure_ascii=False, indent=4 - ) - self.config.load( - Config.script_dict[self.name]["Path"] / "config.json" - ) - - logger.success( - f"{self.name} 配置导入成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", - "操作成功", - f"{self.name} 配置导入成功", - 3000, - ) - - def upload_to_web(self): - """上传配置到「AUTO_MAA 配置分享中心」""" - - choice = LineEditMessageBox( - self.window(), "请输入你的用户名", "用户名", "明文" - ) - choice.input.setMinimumWidth(200) - if choice.exec() and choice.input.text() != "": - - author = choice.input.text() - - choice = LineEditMessageBox( - self.window(), "请输入配置名称", "配置名称", "明文" - ) - choice.input.setMinimumWidth(200) - if choice.exec() and choice.input.text() != "": - - config_name = choice.input.text() - - choice = LineEditMessageBox( - self.window(), - "请描述一下您要分享的配置", - "配置描述", - "明文", - ) - choice.input.setMinimumWidth(300) - if choice.exec() and choice.input.text() != "": - - description = choice.input.text() - - temp = self.config.toDict() - - # 移除配置中可能存在的隐私信息 - temp["Script"]["Name"] = config_name - for path in ["ScriptPath", "ConfigPath", "LogPath"]: - if Path(temp["Script"][path]).is_relative_to( - Path(temp["Script"]["RootPath"]) - ): - temp["Script"][path] = str( - Path(r"C:/脚本根目录") - / Path( - temp["Script"][path] - ).relative_to( - Path(temp["Script"]["RootPath"]) - ) - ) - temp["Script"]["RootPath"] = str( - Path(r"C:/脚本根目录") - ) - - files = { - "file": ( - f"{config_name}&&{author}&&{description}&&{int(datetime.now().timestamp() * 1000)}.json", - json.dumps(temp, ensure_ascii=False), - "application/json", - ) - } - data = { - "username": author, - "description": description, - } - - # 配置上传至远程服务器 - network = Network.add_task( - "upload_file", - "http://221.236.27.82:10023/api/upload/share", - files=files, - data=data, - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - response = network_result["response_json"] - else: - logger.warning( - f"上传配置时出错:{network_result['error_message']}", - module="脚本管理", - ) - MainInfoBar.push_info_bar( - "warning", - "上传配置时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - logger.success( - f"{self.name} 配置上传成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", - "上传配置成功", - ( - response["message"] - if "message" in response - else response["text"] - ), - 5000, - ) - - class BranchManager(HeaderCardWidget): - """分支管理父页面""" - - def __init__(self, name: str, parent=None): - super().__init__(parent) - - self.setObjectName(f"{name}_分支管理") - self.setTitle("下属配置") - self.name = name - - self.tools = CommandBar() - self.sub_manager = self.SubConfigSettingBox(self.name, self) - - # 逐个添加动作 - self.tools.addActions( - [ - Action( - FluentIcon.ADD_TO, "新建配置", triggered=self.add_sub - ), - Action( - FluentIcon.REMOVE_FROM, - "删除配置", - triggered=self.del_sub, - ), - ] - ) - self.tools.addSeparator() - self.tools.addActions( - [ - Action( - FluentIcon.LEFT_ARROW, - "向前移动", - triggered=self.left_sub, - ), - Action( - FluentIcon.RIGHT_ARROW, - "向后移动", - triggered=self.right_sub, - ), - ] - ) - - layout = QVBoxLayout() - layout.addWidget(self.tools) - layout.addWidget(self.sub_manager) - self.viewLayout.addLayout(layout) - - def add_sub(self): - """添加一个配置""" - - index = len(Config.script_dict[self.name]["SubData"]) + 1 - - logger.info( - f"正在添加 {self.name} 的配置_{index}", module="脚本管理" - ) - - # 初始化通用配置 - sub_config = GeneralSubConfig() - sub_config.load( - Config.script_dict[self.name]["Path"] - / f"SubData/配置_{index}/config.json", - sub_config, - ) - sub_config.save() - - Config.script_dict[self.name]["SubData"][f"配置_{index}"] = { - "Path": Config.script_dict[self.name]["Path"] - / f"SubData/配置_{index}", - "Config": sub_config, - } - - # 添加通用配置页面 - self.sub_manager.add_SettingBox(index) - self.sub_manager.switch_SettingBox(f"配置_{index}") - - logger.success( - f"{self.name} 配置_{index} 添加成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 添加 配置_{index}", 3000 - ) - SoundPlayer.play("添加配置") - - def del_sub(self): - """删除一个配置""" - - name = self.sub_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择配置", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择配置", "请先选择一个配置", 5000 - ) - return None - if name == "配置仪表盘": - logger.warning("试图删除配置仪表盘", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择配置", "请勿尝试删除配置仪表盘", 5000 - ) - return None - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - choice = MessageBox( - "确认", f"确定要删除 {name} 吗?", self.window() - ) - if choice.exec(): - - logger.info( - f"正在删除 {self.name} 的配置_{name}", module="脚本管理" - ) - - self.sub_manager.clear_SettingBox() - - # 删除配置文件并同步到相关配置项 - shutil.rmtree( - Config.script_dict[self.name]["SubData"][name]["Path"] - ) - for i in range( - int(name[3:]) + 1, - len(Config.script_dict[self.name]["SubData"]) + 1, - ): - if Config.script_dict[self.name]["SubData"][f"配置_{i}"][ - "Path" - ].exists(): - Config.script_dict[self.name]["SubData"][f"配置_{i}"][ - "Path" - ].rename( - Config.script_dict[self.name]["SubData"][ - f"配置_{i}" - ]["Path"].with_name(f"配置_{i-1}") - ) - - self.sub_manager.show_SettingBox( - f"配置_{max(int(name[3:]) - 1, 1)}" - ) - - logger.success( - f"{self.name} {name} 删除成功", module="脚本管理" - ) - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 删除 {name}", 3000 - ) - SoundPlayer.play("删除配置") - - def left_sub(self): - """向前移动配置""" - - name = self.sub_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择配置", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择配置", "请先选择一个配置", 5000 - ) - return None - if name == "配置仪表盘": - logger.warning("试图移动配置仪表盘", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择配置", "请勿尝试移动配置仪表盘", 5000 - ) - return None - - index = int(name[3:]) - - if index == 1: - logger.warning("向前移动配置时已到达最左端", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "已经是第一个配置", "无法向前移动", 5000 - ) - return None - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - logger.info( - f"正在将 {self.name} 的配置_{name} 前移", module="脚本管理" - ) - - self.sub_manager.clear_SettingBox() - - # 移动配置文件并同步到相关配置项 - Config.script_dict[self.name]["SubData"][name]["Path"].rename( - Config.script_dict[self.name]["SubData"][name][ - "Path" - ].with_name("配置_0") - ) - Config.script_dict[self.name]["SubData"][f"配置_{index-1}"][ - "Path" - ].rename(Config.script_dict[self.name]["SubData"][name]["Path"]) - Config.script_dict[self.name]["SubData"][name]["Path"].with_name( - "配置_0" - ).rename( - Config.script_dict[self.name]["SubData"][f"配置_{index-1}"][ - "Path" - ] - ) - - self.sub_manager.show_SettingBox(f"配置_{index - 1}") - - logger.success(f"{self.name} {name} 前移成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 前移 {name}", 3000 - ) - - def right_sub(self): - """向后移动配置""" - - name = self.sub_manager.pivot.currentRouteKey() - - if name is None: - logger.warning("未选择配置", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择配置", "请先选择一个配置", 5000 - ) - return None - if name == "配置仪表盘": - logger.warning("试图删除配置仪表盘", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "未选择配置", "请勿尝试移动配置仪表盘", 5000 - ) - return None - - index = int(name[3:]) - - if index == len(Config.script_dict[self.name]["SubData"]): - logger.warning("向后移动配置时已到达最右端", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "已经是最后一个配置", "无法向后移动", 5000 - ) - return None - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - logger.info( - f"正在将 {self.name} 的配置_{name} 后移", module="脚本管理" - ) - - self.sub_manager.clear_SettingBox() - - # 移动配置文件并同步到相关配置项 - Config.script_dict[self.name]["SubData"][name]["Path"].rename( - Config.script_dict[self.name]["SubData"][name][ - "Path" - ].with_name("配置_0") - ) - Config.script_dict[self.name]["SubData"][f"配置_{index+1}"][ - "Path" - ].rename(Config.script_dict[self.name]["SubData"][name]["Path"]) - Config.script_dict[self.name]["SubData"][name]["Path"].with_name( - "配置_0" - ).rename( - Config.script_dict[self.name]["SubData"][f"配置_{index+1}"][ - "Path" - ] - ) - - self.sub_manager.show_SettingBox(f"配置_{index + 1}") - - logger.success(f"{self.name} {name} 后移成功", module="脚本管理") - MainInfoBar.push_info_bar( - "success", "操作成功", f"{self.name} 后移 {name}", 3000 - ) - - class SubConfigSettingBox(QWidget): - """配置管理子页面组""" - - def __init__(self, name: str, parent=None): - super().__init__(parent) - - self.setObjectName("配置管理") - self.name = name - - 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: List[ - ScriptManager.ScriptSettingBox.GeneralSettingBox.BranchManager.SubConfigSettingBox.SubMemberSettingBox - ] = [] - - self.sub_dashboard = self.SubDashboard(self.name, self) - self.sub_dashboard.switch_to.connect(self.switch_SettingBox) - self.stackedWidget.addWidget(self.sub_dashboard) - self.pivot.addItem(routeKey="配置仪表盘", text="配置仪表盘") - - self.Layout = QVBoxLayout(self) - self.Layout.addWidget(self.pivotArea) - self.Layout.addWidget(self.stackedWidget) - self.Layout.setContentsMargins(0, 0, 0, 0) - - self.pivot.currentItemChanged.connect( - lambda index: self.switch_SettingBox( - index, if_change_pivot=False - ) - ) - - self.show_SettingBox("配置仪表盘") - - def show_SettingBox(self, index: str) -> None: - """ - 加载所有子界面 - - :param index: 要显示的子界面索引 - """ - - Config.search_general_sub(self.name) - - for name in Config.script_dict[self.name]["SubData"].keys(): - self.add_SettingBox(name[3:]) - - self.switch_SettingBox(index) - - def switch_SettingBox( - self, index: str, if_change_pivot: bool = True - ) -> None: - """ - 切换到指定的子界面 - - :param index: 要切换到的子界面索引 - :param if_change_pivot: 是否更改 pivot 的当前项 - """ - - if len(Config.script_dict[self.name]["SubData"]) == 0: - index = "配置仪表盘" - - if index != "配置仪表盘" and int(index[3:]) > len( - Config.script_dict[self.name]["SubData"] - ): - return None - - if index == "配置仪表盘": - self.sub_dashboard.load_info() - - if if_change_pivot: - self.pivot.setCurrentItem(index) - self.stackedWidget.setCurrentWidget( - self.sub_dashboard - if index == "配置仪表盘" - else self.script_list[int(index[3:]) - 1] - ) - - def clear_SettingBox(self) -> None: - """清空所有子界面""" - - for sub_interface in self.script_list: - self.stackedWidget.removeWidget(sub_interface) - sub_interface.deleteLater() - self.script_list.clear() - self.pivot.clear() - self.sub_dashboard.dashboard.setRowCount(0) - self.stackedWidget.addWidget(self.sub_dashboard) - self.pivot.addItem(routeKey="配置仪表盘", text="配置仪表盘") - - def add_SettingBox(self, uid: int) -> None: - """ - 添加一个配置设置界面 - - :param uid: 配置的唯一标识符 - """ - - setting_box = self.SubMemberSettingBox(self.name, uid, self) - - self.script_list.append(setting_box) - - self.stackedWidget.addWidget(self.script_list[-1]) - - self.pivot.addItem(routeKey=f"配置_{uid}", text=f"配置 {uid}") - - class SubDashboard(HeaderCardWidget): - """配置仪表盘页面""" - - switch_to = Signal(str) - - def __init__(self, name: str, parent=None): - super().__init__(parent) - self.setObjectName("配置仪表盘") - self.setTitle("配置仪表盘") - self.name = name - - self.dashboard = TableWidget(self) - self.dashboard.setColumnCount(5) - self.dashboard.setHorizontalHeaderLabels( - ["配置名", "状态", "代理情况", "备注", "详"] - ) - self.dashboard.setEditTriggers(TableWidget.NoEditTriggers) - self.dashboard.verticalHeader().setVisible(False) - for col in range(2): - self.dashboard.horizontalHeader().setSectionResizeMode( - col, QHeaderView.ResizeMode.ResizeToContents - ) - for col in range(2, 4): - self.dashboard.horizontalHeader().setSectionResizeMode( - col, QHeaderView.ResizeMode.Stretch - ) - self.dashboard.horizontalHeader().setSectionResizeMode( - 4, QHeaderView.ResizeMode.Fixed - ) - self.dashboard.setColumnWidth(4, 32) - - self.viewLayout.addWidget(self.dashboard) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - Config.PASSWORD_refreshed.connect(self.load_info) - - def load_info(self): - """加载配置仪表盘信息""" - - logger.info( - f"正在加载 {self.name} 的配置仪表盘信息", - module="脚本管理", - ) - - self.sub_data = Config.script_dict[self.name]["SubData"] - - self.dashboard.setRowCount(len(self.sub_data)) - - for name, info in self.sub_data.items(): - - config = info["Config"] - - text_list = [] - text_list.append( - f"今日已代理{config.get(config.Data_ProxyTimes)}次" - if Config.server_date().strftime("%Y-%m-%d") - == config.get(config.Data_LastProxyDate) - else "今日未进行代理" - ) - - button = PrimaryToolButton( - FluentIcon.CHEVRON_RIGHT, self - ) - button.setFixedSize(32, 32) - button.clicked.connect( - partial(self.switch_to.emit, name) - ) - - self.dashboard.setItem( - int(name[3:]) - 1, - 0, - QTableWidgetItem(config.get(config.Info_Name)), - ) - self.dashboard.setCellWidget( - int(name[3:]) - 1, - 1, - StatusSwitchSetting( - qconfig=config, - configItem_check=config.Info_Status, - configItem_enable=config.Info_RemainedDay, - parent=self, - ), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 2, - QTableWidgetItem(" | ".join(text_list)), - ) - self.dashboard.setItem( - int(name[3:]) - 1, - 3, - QTableWidgetItem(config.get(config.Info_Notes)), - ) - self.dashboard.setCellWidget( - int(name[3:]) - 1, 4, button - ) - - logger.success( - f"{self.name} 配置仪表盘信息加载成功", module="脚本管理" - ) - - class SubMemberSettingBox(HeaderCardWidget): - """配置管理子页面""" - - def __init__(self, name: str, uid: int, parent=None): - super().__init__(parent) - - self.setObjectName(f"配置_{uid}") - self.setTitle(f"配置 {uid}") - self.name = name - self.config = Config.script_dict[self.name]["SubData"][ - f"配置_{uid}" - ]["Config"] - self.sub_path = Config.script_dict[self.name]["SubData"][ - f"配置_{uid}" - ]["Path"] - - self.card_Name = LineEditSettingCard( - icon=FluentIcon.PEOPLE, - title="配置名", - content="用于标识配置", - text="请输入配置名", - qconfig=self.config, - configItem=self.config.Info_Name, - parent=self, - ) - self.card_SetConfig = PushSettingCard( - text="设置具体配置", - icon=FluentIcon.CAFE, - title="具体配置", - content="在脚本原始界面中查看具体配置内容", - parent=self, - ) - self.card_Status = SwitchSettingCard( - icon=FluentIcon.CHECKBOX, - title="配置状态", - content="启用或禁用该配置", - qconfig=self.config, - configItem=self.config.Info_Status, - parent=self, - ) - self.card_RemainedDay = SpinBoxSettingCard( - icon=FluentIcon.CALENDAR, - title="剩余天数", - content="剩余代理天数,-1表示无限代理", - range=(-1, 1024), - qconfig=self.config, - configItem=self.config.Info_RemainedDay, - parent=self, - ) - self.item_IfScriptBeforeTask = StatusSwitchSetting( - qconfig=self.config, - configItem_check=self.config.Info_IfScriptBeforeTask, - configItem_enable=None, - parent=self, - ) - self.card_ScriptBeforeTask = PathSettingCard( - icon=FluentIcon.FOLDER, - title="脚本前置任务", - mode="脚本文件 (*.py *.bat *.cmd *.exe)", - text="选择脚本文件", - qconfig=self.config, - configItem=self.config.Info_ScriptBeforeTask, - parent=self, - ) - self.item_IfScriptAfterTask = StatusSwitchSetting( - qconfig=self.config, - configItem_check=self.config.Info_IfScriptAfterTask, - configItem_enable=None, - parent=self, - ) - self.card_ScriptAfterTask = PathSettingCard( - icon=FluentIcon.FOLDER, - title="脚本后置任务", - mode="脚本文件 (*.py *.bat *.cmd *.exe)", - text="选择脚本文件", - qconfig=self.config, - configItem=self.config.Info_ScriptAfterTask, - parent=self, - ) - self.card_Notes = LineEditSettingCard( - icon=FluentIcon.PENCIL_INK, - title="备注", - content="配置备注信息", - text="请输入备注", - qconfig=self.config, - configItem=self.config.Info_Notes, - parent=self, - ) - - self.card_UserLable = SubLableSettingCard( - icon=FluentIcon.INFO, - title="状态信息", - content="配置的代理情况汇总", - qconfig=self.config, - configItems={ - "LastProxyDate": self.config.Data_LastProxyDate, - "ProxyTimes": self.config.Data_ProxyTimes, - }, - parent=self, - ) - - self.card_ScriptBeforeTask.hBoxLayout.insertWidget( - 5, self.item_IfScriptBeforeTask, 0, Qt.AlignRight - ) - self.card_ScriptAfterTask.hBoxLayout.insertWidget( - 5, self.item_IfScriptAfterTask, 0, Qt.AlignRight - ) - - # 单独通知卡片 - self.card_NotifySet = UserNoticeSettingCard( - icon=FluentIcon.MAIL, - title="用户单独通知设置", - content="未启用任何通知项", - text="设置", - qconfig=self.config, - configItem=self.config.Notify_Enabled, - configItems={ - "IfSendStatistic": self.config.Notify_IfSendStatistic, - "IfSendMail": self.config.Notify_IfSendMail, - "ToAddress": self.config.Notify_ToAddress, - "IfServerChan": self.config.Notify_IfServerChan, - "ServerChanKey": self.config.Notify_ServerChanKey, - "IfCompanyWebHookBot": self.config.Notify_IfCompanyWebHookBot, - "CompanyWebHookBotUrl": self.config.Notify_CompanyWebHookBotUrl, - }, - parent=self, - ) - self.card_NotifyContent = self.NotifyContentSettingCard( - self.config, self - ) - self.card_EMail = self.EMailSettingCard(self.config, self) - self.card_ServerChan = self.ServerChanSettingCard( - self.config, self - ) - self.card_CompanyWebhookBot = ( - self.CompanyWechatPushSettingCard(self.config, self) - ) - - self.NotifySetCard = SettingFlyoutView( - self, - "用户通知设置", - [ - self.card_NotifyContent, - self.card_EMail, - self.card_ServerChan, - self.card_CompanyWebhookBot, - ], - ) - - h1_layout = QHBoxLayout() - h1_layout.addWidget(self.card_Name) - h1_layout.addWidget(self.card_SetConfig) - h2_layout = QHBoxLayout() - h2_layout.addWidget(self.card_Status) - h2_layout.addWidget(self.card_RemainedDay) - - Layout = QVBoxLayout() - Layout.addLayout(h1_layout) - Layout.addLayout(h2_layout) - Layout.addWidget(self.card_UserLable) - Layout.addWidget(self.card_ScriptBeforeTask) - Layout.addWidget(self.card_ScriptAfterTask) - Layout.addWidget(self.card_Notes) - Layout.addWidget(self.card_NotifySet) - - self.viewLayout.addLayout(Layout) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - self.card_SetConfig.clicked.connect(self.set_sub) - self.card_NotifySet.clicked.connect(self.set_notify) - - def set_sub(self) -> None: - """配置子配置""" - - if self.name in Config.running_list: - logger.warning("所属脚本正在运行", module="脚本管理") - MainInfoBar.push_info_bar( - "warning", "所属脚本正在运行", "请先停止任务", 5000 - ) - return None - - TaskManager.add_task( - "设置通用脚本", - self.name, - {"SetSubInfo": {"Path": self.sub_path / "ConfigFiles"}}, - ) - - def set_notify(self) -> None: - """设置用户通知相关配置""" - - self.NotifySetCard.setVisible(True) - Flyout.make( - self.NotifySetCard, - self.card_NotifySet, - self, - aniType=FlyoutAnimationType.PULL_UP, - isDeleteOnClose=False, - ) - - class NotifyContentSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户通知内容选项") - - self.config = config - - self.card_IfSendStatistic = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送统计信息", - content="推送自动代理统计信息的通知", - qconfig=self.config, - configItem=self.config.Notify_IfSendStatistic, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfSendStatistic) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class EMailSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户邮箱通知") - - self.config = config - - self.card_IfSendMail = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送用户邮件通知", - content="是否启用用户邮件通知功能", - qconfig=self.config, - configItem=self.config.Notify_IfSendMail, - parent=self, - ) - self.card_ToAddress = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户收信邮箱地址", - content="接收用户通知的邮箱地址", - text="请输入用户收信邮箱地址", - qconfig=self.config, - configItem=self.config.Notify_ToAddress, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfSendMail) - Layout.addWidget(self.card_ToAddress) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class ServerChanSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户ServerChan通知") - - self.config = config - - self.card_IfServerChan = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送用户Server酱通知", - content="是否启用用户Server酱通知功能", - qconfig=self.config, - configItem=self.config.Notify_IfServerChan, - parent=self, - ) - self.card_ServerChanKey = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户SendKey", - content="SC3与SCT均须填写", - text="请输入用户SendKey", - qconfig=self.config, - configItem=self.config.Notify_ServerChanKey, - parent=self, - ) - self.card_ServerChanChannel = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户ServerChanChannel代码", - content="留空则默认,多个请使用「|」隔开", - text="请输入Channel代码,仅SCT生效", - qconfig=self.config, - configItem=self.config.Notify_ServerChanChannel, - parent=self, - ) - self.card_ServerChanTag = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="用户Tag内容", - content="留空则默认,多个请使用「|」隔开", - text="请输入加入推送的Tag,仅SC3生效", - qconfig=self.config, - configItem=self.config.Notify_ServerChanTag, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfServerChan) - Layout.addWidget(self.card_ServerChanKey) - Layout.addWidget(self.card_ServerChanChannel) - Layout.addWidget(self.card_ServerChanTag) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) - - class CompanyWechatPushSettingCard(HeaderCardWidget): - - def __init__(self, config: MaaUserConfig, parent=None): - super().__init__(parent) - self.setTitle("用户企业微信推送") - - self.config = config - - self.card_IfCompanyWebHookBot = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送用户企业微信机器人通知", - content="是否启用用户企微机器人通知功能", - qconfig=self.config, - configItem=self.config.Notify_IfCompanyWebHookBot, - parent=self, - ) - self.card_CompanyWebHookBotUrl = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="WebhookUrl", - content="用户企微群机器人Webhook地址", - text="请输入用户Webhook的Url", - qconfig=self.config, - configItem=self.config.Notify_CompanyWebHookBotUrl, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfCompanyWebHookBot) - Layout.addWidget(self.card_CompanyWebHookBotUrl) - self.viewLayout.addLayout(Layout) - self.viewLayout.setSpacing(3) - self.viewLayout.setContentsMargins(3, 0, 3, 3) diff --git a/app/ui/setting.py b/app/ui/setting.py deleted file mode 100644 index 48c90f4..0000000 --- a/app/ui/setting.py +++ /dev/null @@ -1,1313 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA设置界面 -v4.4 -作者:DLmaster_361 -""" - -from PySide6.QtWidgets import QWidget, QVBoxLayout -from PySide6.QtGui import QIcon -from PySide6.QtCore import Qt -from qfluentwidgets import ( - ScrollArea, - FluentIcon, - MessageBox, - HyperlinkCard, - HeaderCardWidget, - ExpandGroupSettingCard, - PushSettingCard, - HyperlinkButton, -) -import os -import re -import json -import shutil -import subprocess -from datetime import datetime -from packaging import version -from pathlib import Path -from typing import Dict, Union - -from app.core import Config, MainInfoBar, Network, SoundPlayer, logger -from app.services import Crypto, System, Notify -from .downloader import DownloadManager -from .Widget import ( - SwitchSettingCard, - RangeSettingCard, - ComboBoxSettingCard, - LineEditMessageBox, - LineEditSettingCard, - PasswordLineEditSettingCard, - UrlListSettingCard, - NoticeMessageBox, -) - - -class Setting(QWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setObjectName("设置") - - self.function = FunctionSettingCard(self) - self.voice = VoiceSettingCard(self) - self.start = StartSettingCard(self) - self.ui = UiSettingCard(self) - self.notification = NotifySettingCard(self) - self.security = SecuritySettingCard(self) - self.updater = UpdaterSettingCard(self) - self.other = OtherSettingCard(self) - - self.function.card_IfAllowSleep.checkedChanged.connect(System.set_Sleep) - self.function.card_IfAgreeBilibili.checkedChanged.connect(self.agree_bilibili) - self.function.card_IfSkipMumuSplashAds.checkedChanged.connect( - self.skip_MuMu_splash_ads - ) - self.start.card_IfSelfStart.checkedChanged.connect(System.set_SelfStart) - self.security.card_changePASSWORD.clicked.connect(self.change_PASSWORD) - self.security.card_resetPASSWORD.clicked.connect(self.reset_PASSWORD) - self.updater.card_CheckUpdate.clicked.connect( - lambda: self.check_update(if_show=True) - ) - self.other.card_Notice.clicked.connect(lambda: self.show_notice(if_show=True)) - - content_widget = QWidget() - content_layout = QVBoxLayout(content_widget) - content_layout.setContentsMargins(0, 0, 11, 0) - content_layout.addWidget(self.function) - content_layout.addWidget(self.voice) - content_layout.addWidget(self.start) - content_layout.addWidget(self.ui) - content_layout.addWidget(self.notification) - content_layout.addWidget(self.security) - content_layout.addWidget(self.updater) - content_layout.addWidget(self.other) - - scrollArea = ScrollArea() - scrollArea.setWidgetResizable(True) - scrollArea.setContentsMargins(0, 0, 0, 0) - scrollArea.setStyleSheet("background: transparent; border: none;") - scrollArea.setWidget(content_widget) - - layout = QVBoxLayout(self) - layout.addWidget(scrollArea) - - def agree_bilibili(self) -> None: - """授权bilibili游戏隐私政策""" - - if Config.get(Config.function_IfAgreeBilibili): - - choice = MessageBox( - "授权声明", - "开启「托管bilibili游戏隐私政策」功能,即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》,并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?", - self.window(), - ) - if choice.exec(): - logger.success("确认授权bilibili游戏隐私政策", module="设置界面") - MainInfoBar.push_info_bar( - "success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000 - ) - else: - Config.set(Config.function_IfAgreeBilibili, False) - else: - - logger.info("取消授权bilibili游戏隐私政策", module="设置界面") - MainInfoBar.push_info_bar( - "info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000 - ) - - def skip_MuMu_splash_ads(self) -> None: - """跳过MuMu启动广告""" - - MuMu_splash_ads_path = ( - Path(os.getenv("APPDATA")) / "Netease/MuMuPlayer-12.0/data/startupImage" - ) - - if Config.get(Config.function_IfSkipMumuSplashAds): - - choice = MessageBox( - "风险声明", - "开启「跳过MuMu启动广告」功能,即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告,并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?", - self.window(), - ) - if choice.exec(): - - if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_dir(): - shutil.rmtree(MuMu_splash_ads_path) - - MuMu_splash_ads_path.touch() - - logger.success("开启跳过MuMu启动广告功能", module="设置界面") - MainInfoBar.push_info_bar( - "success", "操作成功", "已开启跳过MuMu启动广告功能", 3000 - ) - else: - Config.set(Config.function_IfSkipMumuSplashAds, False) - - else: - - if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file(): - MuMu_splash_ads_path.unlink() - - logger.info("关闭跳过MuMu启动广告功能", module="设置界面") - MainInfoBar.push_info_bar( - "info", "操作成功", "已关闭跳过MuMu启动广告功能", 3000 - ) - - def check_PASSWORD(self) -> None: - """检查并配置管理密钥""" - - if Config.key_path.exists(): - return None - - logger.info("未设置管理密钥,开始要求用户进行设置", module="设置界面") - - while True: - - choice = LineEditMessageBox( - self.window(), "请设置您的管理密钥", "管理密钥", "密码" - ) - if choice.exec() and choice.input.text() != "": - Crypto.get_PASSWORD(choice.input.text()) - logger.success("成功设置管理密钥", module="设置界面") - break - else: - choice = MessageBox( - "警告", - "您没有设置管理密钥,无法使用本软件,请先设置管理密钥", - self.window(), - ) - choice.cancelButton.hide() - choice.buttonLayout.insertStretch(1) - choice.exec() - - def change_PASSWORD(self) -> None: - """修改管理密钥""" - - if_change = True - - while if_change: - - choice = LineEditMessageBox( - self.window(), "请输入旧的管理密钥", "旧管理密钥", "密码" - ) - if choice.exec() and choice.input.text() != "": - - # 验证旧管理密钥 - if Crypto.check_PASSWORD(choice.input.text()): - - PASSWORD_old = choice.input.text() - # 获取新的管理密钥 - while True: - - choice = LineEditMessageBox( - self.window(), - "请输入新的管理密钥", - "新管理密钥", - "密码", - ) - if choice.exec() and choice.input.text() != "": - - # 修改管理密钥 - Crypto.change_PASSWORD(PASSWORD_old, choice.input.text()) - logger.success("成功修改管理密钥", module="设置界面") - MainInfoBar.push_info_bar( - "success", "操作成功", "管理密钥修改成功", 3000 - ) - if_change = False - break - - else: - - choice = MessageBox( - "确认", - "您没有输入新的管理密钥,是否取消修改管理密钥?", - self.window(), - ) - if choice.exec(): - if_change = False - break - - else: - choice = MessageBox("错误", "管理密钥错误", self.window()) - choice.cancelButton.hide() - choice.buttonLayout.insertStretch(1) - choice.exec() - else: - choice = MessageBox( - "确认", "您没有输入管理密钥,是否取消修改管理密钥?", self.window() - ) - if choice.exec(): - break - - def reset_PASSWORD(self) -> None: - """重置管理密钥""" - - choice = MessageBox( - "确认", - "重置管理密钥将清空所有使用管理密钥加密的数据,您确认要重置管理密钥吗?", - self.window(), - ) - if choice.exec(): - choice = LineEditMessageBox( - self.window(), - "请输入文本提示框内的验证信息", - "AUTO_MAA绝赞DeBug中!", - "明文", - ) - - if choice.exec() and choice.input.text() in [ - "AUTO_MAA绝赞DeBug中!", - "AUTO_MAA绝赞DeBug中!", - ]: - - # 获取新的管理密钥 - while True: - - choice = LineEditMessageBox( - self.window(), "请输入新的管理密钥", "新管理密钥", "密码" - ) - if choice.exec() and choice.input.text() != "": - - # 重置管理密钥 - Crypto.reset_PASSWORD(choice.input.text()) - logger.success("成功重置管理密钥", module="设置界面") - MainInfoBar.push_info_bar( - "success", "操作成功", "管理密钥重置成功", 3000 - ) - break - - else: - - choice = MessageBox( - "确认", - "您没有输入新的管理密钥,是否取消修改管理密钥?", - self.window(), - ) - if choice.exec(): - break - - else: - - MainInfoBar.push_info_bar( - "info", - "验证未通过", - "请输入「AUTO_MAA绝赞DeBug中!」后单击确认键", - 3000, - ) - - 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("."))) - - # 从远程服务器获取最新版本信息 - network = Network.add_task( - mode="get", - url=f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaGui¤t_version={version_text(current_version)}&cdk={Crypto.win_decryptor(Config.get(Config.update_MirrorChyanCDK))}&channel={Config.get(Config.update_UpdateType)}", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - version_info: Dict[str, Union[int, str, Dict[str, str]]] = network_result[ - "response_json" - ] - else: - - if network_result["response_json"]: - - version_info = network_result["response_json"] - - if version_info["code"] != 0: - - logger.error( - f"获取版本信息时出错:{version_info['msg']}", module="设置界面" - ) - - error_remark_dict = { - 1001: "获取版本信息的URL参数不正确", - 7001: "填入的 CDK 已过期", - 7002: "填入的 CDK 错误", - 7003: "填入的 CDK 今日下载次数已达上限", - 7004: "填入的 CDK 类型和待下载的资源不匹配", - 7005: "填入的 CDK 已被封禁", - 8001: "对应架构和系统下的资源不存在", - 8002: "错误的系统参数", - 8003: "错误的架构参数", - 8004: "错误的更新通道参数", - 1: version_info["msg"], - } - - if version_info["code"] in error_remark_dict: - MainInfoBar.push_info_bar( - "error", - "获取版本信息时出错", - error_remark_dict[version_info["code"]], - -1, - ) - else: - MainInfoBar.push_info_bar( - "error", - "获取版本信息时出错", - "意料之外的错误,请及时联系项目组以获取来自 Mirror 酱的技术支持", - -1, - ) - - return None - - logger.warning( - f"获取版本信息时出错:{network_result['error_message']}", - module="设置界面", - ) - MainInfoBar.push_info_bar( - "warning", - "获取版本信息时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - remote_version = list( - map( - int, - version_info["data"]["version_name"][1:] - .replace("-beta", "") - .split("."), - ) - ) - - if ( - if_show - or ( - not if_show - and if_first - and not Config.get(Config.function_UnattendedMode) - ) - ) and version.parse(version_text(remote_version)) > version.parse( - version_text(current_version) - ): - - version_info_json: Dict[str, Dict[str, str]] = json.loads( - re.sub( - r"^$", - r"\1", - version_info["data"]["release_note"].splitlines()[0], - ) - ) - - # 生成版本更新信息 - main_version_info = f"## 主程序:{version_text(current_version)} --> {version_text(remote_version)}" - - update_version_info = {} - all_version_info = {} - for v_i in [ - info - for ver, info in version_info_json.items() - if version.parse(version_text(list(map(int, ver.split("."))))) - > version.parse(version_text(current_version)) - ]: - - for key, value in v_i.items(): - if key in update_version_info: - update_version_info[key] += value.copy() - else: - update_version_info[key] = value.copy() - for v_i in version_info_json.values(): - for key, value in v_i.items(): - if key in all_version_info: - all_version_info[key] += value.copy() - else: - all_version_info[key] = value.copy() - - # 询问是否开始版本更新 - SoundPlayer.play("有新版本") - choice = NoticeMessageBox( - self.window(), - "版本更新", - { - "更新总览": f"{main_version_info}\n\n{version_info_markdown(update_version_info)}", - "ALL~版本信息": version_info_markdown(all_version_info), - **{ - version_text( - list(map(int, k.split("."))) - ): version_info_markdown(v) - for k, v in version_info_json.items() - }, - }, - ) - if choice.exec(): - - if "url" in version_info["data"]: - download_config = { - "mode": "MirrorChyan", - "thread_numb": 1, - "url": version_info["data"]["url"], - } - else: - - # 从远程服务器获取代理信息 - network = Network.add_task( - mode="get", - url="http://221.236.27.82:10197/d/AUTO_MAA/Server/download_info.json", - ) - network.loop.exec() - network_result = Network.get_result(network) - if network_result["status_code"] == 200: - download_info = network_result["response_json"] - else: - logger.warning( - f"获取下载信息时出错:{network_result['error_message']}", - module="设置界面", - ) - MainInfoBar.push_info_bar( - "warning", - "获取下载信息时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - download_config = { - "mode": "Proxy", - "thread_numb": Config.get(Config.update_ThreadNumb), - "proxy_list": list( - set( - Config.get(Config.update_ProxyUrlList) - + download_info["proxy_list"] - ) - ), - "download_dict": download_info["download_dict"], - } - - logger.info("开始执行更新任务", module="设置界面") - - self.downloader = DownloadManager( - Config.app_path, "AUTO_MAA", remote_version, download_config - ) - self.downloader.setWindowTitle("AUTO_MAA更新器") - self.downloader.setWindowIcon( - QIcon(str(Config.app_path / "resources/icons/AUTO_MAA_Updater.ico")) - ) - self.downloader.download_accomplish.connect(self.start_setup) - self.downloader.show() - self.downloader.run() - - elif ( - if_show - or if_first - or version.parse(version_text(remote_version)) - > version.parse(version_text(current_version)) - ): - - if version.parse(version_text(remote_version)) > version.parse( - version_text(current_version) - ): - MainInfoBar.push_info_bar( - "info", - "发现新版本", - f"{version_text(current_version)} --> {version_text(remote_version)}", - 3600000, - if_force=True, - ) - SoundPlayer.play("有新版本") - else: - MainInfoBar.push_info_bar("success", "更新检查", "已是最新版本~", 3000) - SoundPlayer.play("无新版本") - - def start_setup(self) -> None: - """启动安装程序""" - - logger.info("启动安装程序", module="设置界面") - subprocess.Popen( - [ - Config.app_path / "AUTO_MAA-Setup.exe", - "/SP-", - "/SILENT", - "/NOCANCEL", - "/FORCECLOSEAPPLICATIONS", - "/LANG=Chinese", - f"/DIR={Config.app_path}", - ], - creationflags=subprocess.CREATE_NEW_PROCESS_GROUP - | subprocess.DETACHED_PROCESS - | subprocess.CREATE_NO_WINDOW, - ) - System.set_power("KillSelf") - - def show_notice(self, if_show: bool = False, if_first: bool = False) -> None: - """显示公告""" - - # 从远程服务器获取最新公告 - network = Network.add_task( - mode="get", - 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']}", - module="设置界面", - ) - MainInfoBar.push_info_bar( - "warning", - "获取最新公告时出错", - f"网络错误:{network_result['status_code']}", - 5000, - ) - return None - - if (Config.app_path / "resources/notice.json").exists(): - with (Config.app_path / "resources/notice.json").open( - mode="r", encoding="utf-8" - ) as f: - notice_local = json.load(f) - time_local = datetime.strptime(notice_local["time"], "%Y-%m-%d %H:%M") - else: - time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M") - - notice["notice_dict"] = { - "ALL~公告": "\n---\n".join( - [str(_) for _ in notice["notice_dict"].values() if isinstance(_, str)] - ), - **notice["notice_dict"], - } - - if if_show or ( - if_first - and datetime.now() - > datetime.strptime(notice["time"], "%Y-%m-%d %H:%M") - > time_local - and not Config.get(Config.function_UnattendedMode) - ): - - choice = NoticeMessageBox(self.window(), "公告", notice["notice_dict"]) - choice.button_cancel.hide() - choice.button_layout.insertStretch(0, 1) - SoundPlayer.play("公告展示") - if choice.exec(): - with (Config.app_path / "resources/notice.json").open( - mode="w", encoding="utf-8" - ) as f: - json.dump(notice, f, ensure_ascii=False, indent=4) - else: - import random - - if random.random() < 0.1: - cc = NoticeMessageBox( - self.window(), - "用户守则", - { - "用户守则 - 第一版": """ -0. 用户守则的每一条都应该清晰可读、不含任何语法错误。如果发现任何一条不符合以上描述,请忽视它。 -1. AUTO_MAA 的所有版本均包含完整源代码与 LICENSE 文件,若发现此内容缺失,请立即关闭软件,并联系最近的 AUTO_MAA 开发者。 -2. AUTO_MAA 不会对您许下任何承诺,请自行保护好自己的数据,若软件运行过程中发生了数据损坏,项目组不负任何责任。 -3. AUTO_MAA 只会注册一个启动项,若发现两个 AUTO_MAA 同时自启动,请立即使用系统或杀软的 **启动项管理** 功能删除所有名为 AUTO_MAA 的启动项后重启软件。 -4. AUTO_MAA 正式版不应该包含命令行窗口,如果您看到了它,请立即关闭软件,通过 AUTO_MAA.exe 文件重新打开软件。 -5. 深色模式是危险的,但并非无法使用。 -6. 第 0 条规则不存在。如果你看到了,请忘记它,并正常使用软件 -7. **Mirror 酱** 是善良的,你只要付出小小的代价,就能得到祂的庇护。 -8. AUTO_MAA 没有实时合成语音的能力,软件所有语音都存储在本地。如果听到本地不存在的语音,立即关闭扬声器,并检查是否有未知脚本在运行。 -9. AUTO_MAA 不会在周六凌晨更新。如果收到更新提示,请忽略,不要查看更新内容,直到第二天天亮。 -10. 用户守则仅有一页""", - "--- 标记文档中止 ---": "xdfv-serfcx-jiol,m: !1 $bad food of do $5b 9630-300 $daad 100-1\n\n// 0 == o //\n\n∠( °ω°)/", - }, - ) - cc.button_cancel.hide() - cc.button_layout.insertStretch(0, 1) - cc.exec() - - elif ( - datetime.now() - > datetime.strptime(notice["time"], "%Y-%m-%d %H:%M") - > time_local - ): - - MainInfoBar.push_info_bar( - "info", "有新公告", "请前往设置界面查看公告", 3600000, if_force=True - ) - SoundPlayer.play("公告通知") - return None - - -class FunctionSettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("功能") - - self.card_HomeImageMode = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="主页背景图模式", - content="选择主页背景图的来源", - texts=["默认", "自定义", "主题图像"], - qconfig=Config, - configItem=Config.function_HomeImageMode, - parent=self, - ) - self.card_HistoryRetentionTime = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="历史记录保留时间", - content="选择历史记录的保留时间,超期自动清理", - texts=["7 天", "15 天", "30 天", "60 天", "90 天", "半年", "一年", "永久"], - qconfig=Config, - configItem=Config.function_HistoryRetentionTime, - parent=self, - ) - self.card_IfAllowSleep = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="启动时阻止系统休眠", - content="仅阻止电脑自动休眠,不会影响屏幕是否熄灭", - qconfig=Config, - configItem=Config.function_IfAllowSleep, - parent=self, - ) - self.card_IfSilence = self.SilenceSettingCard(self) - self.card_UnattendedMode = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="无人值守模式", - content="开启后AUTO_MAA不再主动弹出对话框,以免影响代理任务运行", - qconfig=Config, - configItem=Config.function_UnattendedMode, - parent=self, - ) - self.card_IfAgreeBilibili = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="托管bilibili游戏隐私政策", - content="授权AUTO_MAA同意bilibili游戏隐私政策", - qconfig=Config, - configItem=Config.function_IfAgreeBilibili, - parent=self, - ) - self.card_IfSkipMumuSplashAds = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="跳过MuMu启动广告", - content="启动MuMu模拟器时屏蔽启动广告", - qconfig=Config, - configItem=Config.function_IfSkipMumuSplashAds, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_HomeImageMode) - Layout.addWidget(self.card_HistoryRetentionTime) - Layout.addWidget(self.card_IfAllowSleep) - Layout.addWidget(self.card_IfSilence) - Layout.addWidget(self.card_UnattendedMode) - Layout.addWidget(self.card_IfAgreeBilibili) - Layout.addWidget(self.card_IfSkipMumuSplashAds) - self.viewLayout.addLayout(Layout) - - class SilenceSettingCard(ExpandGroupSettingCard): - - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, - "静默模式", - "将各代理窗口置于后台运行,减少对前台的干扰", - parent, - ) - - self.card_IfSilence = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="静默模式", - content="是否启用静默模式", - qconfig=Config, - configItem=Config.function_IfSilence, - parent=self, - ) - self.card_BossKey = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="模拟器老板键", - content="请输入对应的模拟器老板键,请直接输入文字,多个键位之间请用「+」隔开。如:「Alt+Q」", - text="请以文字形式输入模拟器老板快捷键", - qconfig=Config, - configItem=Config.function_BossKey, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_IfSilence) - Layout.addWidget(self.card_BossKey) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - -class VoiceSettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("音效") - - self.card_Enabled = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="音效开关", - content="是否启用音效", - qconfig=Config, - configItem=Config.voice_Enabled, - parent=self, - ) - self.card_Type = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="音效模式", - content="选择音效的播放模式", - texts=["简洁", "聒噪"], - qconfig=Config, - configItem=Config.voice_Type, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_Enabled) - Layout.addWidget(self.card_Type) - self.viewLayout.addLayout(Layout) - - -class StartSettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("启动") - - self.card_IfSelfStart = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="开机时自动启动", - content="将AUTO_MAA添加到开机启动项", - qconfig=Config, - configItem=Config.start_IfSelfStart, - parent=self, - ) - self.card_IfMinimizeDirectly = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="启动后直接最小化", - content="启动AUTO_MAA后直接最小化", - qconfig=Config, - configItem=Config.start_IfMinimizeDirectly, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfSelfStart) - Layout.addWidget(self.card_IfMinimizeDirectly) - self.viewLayout.addLayout(Layout) - - -class UiSettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("界面") - - self.card_IfShowTray = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="显示托盘图标", - content="常态显示托盘图标", - qconfig=Config, - configItem=Config.ui_IfShowTray, - parent=self, - ) - self.card_IfToTray = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="最小化到托盘", - content="最小化时隐藏到托盘", - qconfig=Config, - configItem=Config.ui_IfToTray, - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_IfShowTray) - Layout.addWidget(self.card_IfToTray) - self.viewLayout.addLayout(Layout) - - -class NotifySettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - - self.setTitle("通知") - - self.card_NotifyContent = self.NotifyContentSettingCard(self) - self.card_Plyer = self.PlyerSettingCard(self) - self.card_EMail = self.EMailSettingCard(self) - self.card_ServerChan = self.ServerChanSettingCard(self) - self.card_CompanyWebhookBot = self.CompanyWechatPushSettingCard(self) - self.card_TestNotification = PushSettingCard( - text="发送测试通知", - icon=FluentIcon.SEND, - title="测试通知", - content="发送测试通知到所有已启用的通知渠道", - parent=self, - ) - self.card_TestNotification.clicked.connect(self.send_test_notification) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_NotifyContent) - Layout.addWidget(self.card_Plyer) - Layout.addWidget(self.card_EMail) - Layout.addWidget(self.card_ServerChan) - Layout.addWidget(self.card_CompanyWebhookBot) - Layout.addWidget(self.card_TestNotification) - self.viewLayout.addLayout(Layout) - - def send_test_notification(self): - """发送测试通知到所有已启用的通知渠道""" - if Notify.send_test_notification(): - MainInfoBar.push_info_bar( - "success", - "测试通知已发送", - "请检查已配置的通知渠道是否正常收到消息", - 3000, - ) - - class NotifyContentSettingCard(ExpandGroupSettingCard): - - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, "通知内容选项", "选择需要推送的通知内容", parent - ) - - self.card_SendTaskResultTime = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送任务结果选项", - content="选择推送自动代理与人工排查任务结果的时机", - texts=["不推送", "任何时刻", "仅失败时"], - qconfig=Config, - configItem=Config.notify_SendTaskResultTime, - parent=self, - ) - self.card_IfSendStatistic = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送统计信息", - content="推送自动代理统计信息的通知", - qconfig=Config, - configItem=Config.notify_IfSendStatistic, - parent=self, - ) - self.card_IfSendSixStar = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送公招高资喜报", - content="公招出现六星词条时推送喜报", - qconfig=Config, - configItem=Config.notify_IfSendSixStar, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_SendTaskResultTime) - Layout.addWidget(self.card_IfSendStatistic) - Layout.addWidget(self.card_IfSendSixStar) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class PlyerSettingCard(ExpandGroupSettingCard): - - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, "推送系统通知", "Plyer系统通知推送渠道", parent - ) - - self.card_IfPushPlyer = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送系统通知", - content="使用Plyer推送系统级通知,不会在通知中心停留", - qconfig=Config, - configItem=Config.notify_IfPushPlyer, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_IfPushPlyer) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class EMailSettingCard(ExpandGroupSettingCard): - - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, "推送邮件通知", "电子邮箱通知推送渠道", parent - ) - - self.card_IfSendMail = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送邮件通知", - content="是否启用邮件通知功能", - qconfig=Config, - configItem=Config.notify_IfSendMail, - parent=self, - ) - self.card_SMTPServerAddress = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="SMTP服务器地址", - content="发信邮箱的SMTP服务器地址", - text="请输入SMTP服务器地址", - qconfig=Config, - configItem=Config.notify_SMTPServerAddress, - parent=self, - ) - self.card_FromAddress = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="发信邮箱地址", - content="发送通知的邮箱地址", - text="请输入发信邮箱地址", - qconfig=Config, - configItem=Config.notify_FromAddress, - parent=self, - ) - self.card_AuthorizationCode = PasswordLineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="发信邮箱授权码", - content="发送通知的邮箱授权码", - text="请输入发信邮箱授权码", - algorithm="DPAPI", - qconfig=Config, - configItem=Config.notify_AuthorizationCode, - parent=self, - ) - self.card_ToAddress = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="收信邮箱地址", - content="接收通知的邮箱地址", - text="请输入收信邮箱地址", - qconfig=Config, - configItem=Config.notify_ToAddress, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_IfSendMail) - Layout.addWidget(self.card_SMTPServerAddress) - Layout.addWidget(self.card_FromAddress) - Layout.addWidget(self.card_AuthorizationCode) - Layout.addWidget(self.card_ToAddress) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class ServerChanSettingCard(ExpandGroupSettingCard): - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, - "推送ServerChan通知", - "ServerChan通知推送渠道", - parent, - ) - - self.card_IfServerChan = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送SeverChan通知", - content="是否启用SeverChan通知功能", - qconfig=Config, - configItem=Config.notify_IfServerChan, - parent=self, - ) - self.card_ServerChanKey = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="SendKey", - content="Server酱的SendKey(SC3与SCT都可以)", - text="请输入SendKey", - qconfig=Config, - configItem=Config.notify_ServerChanKey, - parent=self, - ) - self.card_ServerChanChannel = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="ServerChanChannel代码", - content="可以留空,留空则默认。可以多个,请使用「|」隔开", - text="请输入需要推送的Channel代码(SCT生效)", - qconfig=Config, - configItem=Config.notify_ServerChanChannel, - parent=self, - ) - self.card_ServerChanTag = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="Tag内容", - content="可以留空,留空则默认。可以多个,请使用「|」隔开", - text="请输入加入推送的Tag(SC3生效)", - qconfig=Config, - configItem=Config.notify_ServerChanTag, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_IfServerChan) - Layout.addWidget(self.card_ServerChanKey) - Layout.addWidget(self.card_ServerChanChannel) - Layout.addWidget(self.card_ServerChanTag) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - class CompanyWechatPushSettingCard(ExpandGroupSettingCard): - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, - "推送企业微信机器人通知", - "企业微信机器人Webhook通知推送渠道", - parent, - ) - - self.card_IfCompanyWechat = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="推送企业微信机器人通知", - content="是否启用企业微信机器人通知功能", - qconfig=Config, - configItem=Config.notify_IfCompanyWebHookBot, - parent=self, - ) - self.card_CompanyWebHookBotUrl = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="WebhookUrl", - content="企业微信群机器人的Webhook地址", - text="请输入Webhook的Url", - qconfig=Config, - configItem=Config.notify_CompanyWebHookBotUrl, - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_IfCompanyWechat) - Layout.addWidget(self.card_CompanyWebHookBotUrl) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - -class SecuritySettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("安全") - - self.card_changePASSWORD = PushSettingCard( - text="修改", - icon=FluentIcon.VPN, - title="修改管理密钥", - content="修改用于解密用户密码的管理密钥", - parent=self, - ) - self.card_resetPASSWORD = PushSettingCard( - text="重置", - icon=FluentIcon.VPN, - title="重置管理密钥", - content="重置用于解密用户密码的管理密钥", - parent=self, - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_changePASSWORD) - Layout.addWidget(self.card_resetPASSWORD) - self.viewLayout.addLayout(Layout) - - -class UpdaterSettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("更新") - - self.card_CheckUpdate = PushSettingCard( - text="检查更新", - icon=FluentIcon.UPDATE, - title="获取最新版本", - content="检查AUTO_MAA是否有新版本", - parent=self, - ) - self.card_IfAutoUpdate = SwitchSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="自动检查更新", - content="将在启动时自动检查AUTO_MAA是否有新版本", - qconfig=Config, - configItem=Config.update_IfAutoUpdate, - parent=self, - ) - self.card_UpdateType = ComboBoxSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="版本更新类别", - content="选择AUTO_MAA的更新类别", - texts=["稳定版", "公测版"], - qconfig=Config, - configItem=Config.update_UpdateType, - parent=self, - ) - self.card_ThreadNumb = RangeSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="下载器线程数", - content="更新器的下载线程数,建议仅在下载速度较慢时适量拉高", - qconfig=Config, - configItem=Config.update_ThreadNumb, - parent=self, - ) - self.card_ProxyAddress = LineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="网络代理地址", - content="使用网络代理软件时,若出现网络连接问题,请尝试设置代理地址,此设置全局生效", - text="请输入代理地址", - qconfig=Config, - configItem=Config.update_ProxyAddress, - parent=self, - ) - self.card_ProxyUrlList = UrlListSettingCard( - icon=FluentIcon.SETTING, - title="代理地址列表", - content="更新器代理地址列表", - qconfig=Config, - configItem=Config.update_ProxyUrlList, - parent=self, - ) - self.card_MirrorChyanCDK = PasswordLineEditSettingCard( - icon=FluentIcon.PAGE_RIGHT, - title="Mirror酱CDK", - content="填写后改为使用由Mirror酱提供的下载服务", - text="请输入Mirror酱CDK", - algorithm="DPAPI", - qconfig=Config, - configItem=Config.update_MirrorChyanCDK, - parent=self, - ) - mirrorchyan_url = HyperlinkButton( - "https://mirrorchyan.com/zh/get-start?source=auto_maa-setting", - "获取Mirror酱CDK", - self, - ) - self.card_MirrorChyanCDK.hBoxLayout.insertWidget( - 5, mirrorchyan_url, 0, Qt.AlignRight - ) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_CheckUpdate) - Layout.addWidget(self.card_IfAutoUpdate) - Layout.addWidget(self.card_UpdateType) - Layout.addWidget(self.card_ThreadNumb) - Layout.addWidget(self.card_ProxyAddress) - Layout.addWidget(self.card_ProxyUrlList) - Layout.addWidget(self.card_MirrorChyanCDK) - self.viewLayout.addLayout(Layout) - - -class OtherSettingCard(HeaderCardWidget): - - def __init__(self, parent=None): - super().__init__(parent) - self.setTitle("其他") - - self.card_Notice = PushSettingCard( - text="查看", - icon=FluentIcon.PAGE_RIGHT, - title="公告", - content="查看AUTO_MAA的最新公告", - parent=self, - ) - self.card_UserDocs = HyperlinkCard( - url="https://clozya.github.io/AUTOMAA_docs", - text="查看指南", - icon=FluentIcon.PAGE_RIGHT, - title="用户指南", - content="访问AUTO_MAA的官方文档站,获取使用指南和项目相关信息", - parent=self, - ) - self.card_Association = self.AssociationSettingCard(self) - - Layout = QVBoxLayout() - Layout.addWidget(self.card_Notice) - Layout.addWidget(self.card_UserDocs) - Layout.addWidget(self.card_Association) - self.viewLayout.addLayout(Layout) - - class AssociationSettingCard(ExpandGroupSettingCard): - - def __init__(self, parent=None): - super().__init__( - FluentIcon.SETTING, - "AUTO_MAA官方社群", - "加入AUTO_MAA官方社群,获取更多帮助", - parent, - ) - - self.card_GitHubRepository = HyperlinkCard( - url="https://github.com/DLmaster361/AUTO_MAA", - text="访问GitHub仓库", - icon=FluentIcon.GITHUB, - title="GitHub", - content="查看AUTO_MAA的源代码,提交问题和建议,欢迎参与开发", - parent=self, - ) - self.card_QQGroup = HyperlinkCard( - url="https://qm.qq.com/q/bd9fISNoME", - text="加入官方QQ交流群", - icon=FluentIcon.CHAT, - title="QQ群", - content="与AUTO_MAA开发者和用户交流", - parent=self, - ) - - widget = QWidget() - Layout = QVBoxLayout(widget) - Layout.addWidget(self.card_GitHubRepository) - Layout.addWidget(self.card_QQGroup) - self.viewLayout.setContentsMargins(0, 0, 0, 0) - self.viewLayout.setSpacing(0) - self.addGroupWidget(widget) - - -def version_text(version_numb: list) -> str: - """将版本号列表转为可读的文本信息""" - - while len(version_numb) < 4: - version_numb.append(0) - - if version_numb[3] == 0: - version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}" - else: - version = ( - f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" - ) - return version - - -def version_info_markdown(info: dict) -> str: - """将版本信息字典转为markdown信息""" - - version_info = "" - for key, value in info.items(): - version_info += f"### {key}\n\n" - for v in value: - version_info += f"- {v}\n\n" - return version_info diff --git a/app/utils/AUTO_MAA.iss b/app/utils/AUTO_MAA.iss deleted file mode 100644 index 5dec878..0000000 --- a/app/utils/AUTO_MAA.iss +++ /dev/null @@ -1,93 +0,0 @@ -; Script generated by the Inno Setup Script Wizard. -; SEE THE DOCUMENTATION FOR DETAILS ON CREATING INNO SETUP SCRIPT FILES! - -#define MyAppName "AUTO_MAA" -#define MyAppVersion "" -#define MyAppPublisher "AUTO_MAA Team" -#define MyAppURL "https://doc.automaa.xyz/" -#define MyAppExeName "AUTO_MAA.exe" -#define MyAppPath "" -#define OutputDir "" - -[Setup] -; NOTE: The value of AppId uniquely identifies this application. Do not use the same AppId value in installers for other applications. -; (To generate a new GUID, click Tools | Generate GUID inside the IDE.) -AppId={{D116A92A-E174-4699-B777-61C5FD837B19} -AppName={#MyAppName} -AppVersion={#MyAppVersion} -AppVerName={#MyAppName} -AppPublisher={#MyAppPublisher} -AppPublisherURL={#MyAppURL} -AppSupportURL={#MyAppURL} -AppUpdatesURL={#MyAppURL} -DefaultDirName={autopf}\{#MyAppName} -UninstallDisplayIcon={app}\{#MyAppExeName} -; "ArchitecturesAllowed=x64compatible" specifies that Setup cannot run -; on anything but x64 and Windows 11 on Arm. -ArchitecturesAllowed=x64compatible -; "ArchitecturesInstallIn64BitMode=x64compatible" requests that the -; install be done in "64-bit mode" on x64 or Windows 11 on Arm, -; meaning it should use the native 64-bit Program Files directory and -; the 64-bit view of the registry. -ArchitecturesInstallIn64BitMode=x64compatible -DisableProgramGroupPage=yes -LicenseFile={#MyAppPath}\LICENSE -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" -Name: "English"; MessagesFile: "compiler:Default.isl" - -[Tasks] -Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - -[Files] -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 -Source: "{#MyAppPath}\LICENSE"; DestDir: "{app}"; Flags: ignoreversion -; NOTE: Don't use "Flags: ignoreversion" on any shared system files - -[Icons] -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 runascurrentuser - -[Code] -var - DeleteDataQuestion: Boolean; - -function InitializeUninstall: Boolean; -begin - DeleteDataQuestion := MsgBox('您确认要完全移除 AUTO_MAA 的所有配置、用户数据与子组件吗?' + #13#10 + - '选择"是"将删除所有配置文件、数据与子组件程序。' + #13#10 + - '选择"否"将保留数据文件与子组件。', - mbConfirmation, MB_YESNO) = IDYES; - Result := True; -end; - -procedure CurUninstallStepChanged(CurUninstallStep: TUninstallStep); -begin - if CurUninstallStep = usPostUninstall then - begin - DelTree(ExpandConstant('{app}\app'), True, True, True); - DelTree(ExpandConstant('{app}\resources'), True, True, True); - if DeleteDataQuestion then - begin - DelTree(ExpandConstant('{app}'), True, True, True); - end; - end; -end; diff --git a/app/utils/ImageUtils.py b/app/utils/ImageUtils.py deleted file mode 100644 index 2631dc9..0000000 --- a/app/utils/ImageUtils.py +++ /dev/null @@ -1,95 +0,0 @@ -# AUTO_MAA:A MAA Multi Account Management and Automation Tool -# Copyright © 2025 ClozyA - -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA图像组件 -v4.4 -作者:ClozyA -""" - -import base64 -import hashlib -from pathlib import Path - -from PIL import Image - - -class ImageUtils: - @staticmethod - def get_base64_from_file(image_path): - """从本地文件读取并返回base64编码字符串""" - with open(image_path, "rb") as f: - return base64.b64encode(f.read()).decode("utf-8") - - @staticmethod - def calculate_md5_from_file(image_path): - """从本地文件读取并返回md5值(hex字符串)""" - with open(image_path, "rb") as f: - return hashlib.md5(f.read()).hexdigest() - - @staticmethod - def calculate_md5_from_base64(base64_content): - """从base64字符串计算md5""" - image_data = base64.b64decode(base64_content) - return hashlib.md5(image_data).hexdigest() - - @staticmethod - def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path: - """ - 如果图片大于max_size_mb,则压缩并覆盖原文件,返回原始路径(Path对象) - """ - if hasattr(Image, "Resampling"): # Pillow 9.1.0及以后 - RESAMPLE = Image.Resampling.LANCZOS - else: - RESAMPLE = Image.ANTIALIAS - - max_size = max_size_mb * 1024 * 1024 - if image_path.stat().st_size <= max_size: - return image_path - - img = Image.open(image_path) - suffix = image_path.suffix.lower() - quality = 90 if suffix in [".jpg", ".jpeg"] else None - step = 5 - - if suffix in [".jpg", ".jpeg"]: - while True: - img.save(image_path, quality=quality, optimize=True) - if image_path.stat().st_size <= max_size or quality <= 10: - break - quality -= step - elif suffix == ".png": - width, height = img.size - while True: - img.save(image_path, optimize=True) - if ( - image_path.stat().st_size <= max_size - or width <= 200 - or height <= 200 - ): - break - width = int(width * 0.95) - height = int(height * 0.95) - img = img.resize((width, height), RESAMPLE) - else: - raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩。") - - return image_path diff --git a/app/utils/ProcessManager.py b/app/utils/ProcessManager.py deleted file mode 100644 index a57406d..0000000 --- a/app/utils/ProcessManager.py +++ /dev/null @@ -1,164 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA进程管理组件 -v4.4 -作者:DLmaster_361 -""" - - -import psutil -import subprocess -from pathlib import Path -from datetime import datetime - -from PySide6.QtCore import QTimer, QObject, Signal - - -class ProcessManager(QObject): - """进程监视器类,用于跟踪主进程及其所有子进程的状态""" - - processClosed = Signal() - - def __init__(self): - super().__init__() - - self.main_pid = None - self.tracked_pids = set() - - 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: - """ - 启动一个新进程并返回其pid,并开始监视该进程 - - :param path: 可执行文件的路径 - :param args: 启动参数列表 - :param tracking_time: 子进程追踪持续时间(秒) - :return: 新进程的PID - """ - - process = subprocess.Popen( - [path, *args], - cwd=path.parent, - creationflags=subprocess.CREATE_NO_WINDOW, - stdin=subprocess.DEVNULL, - stdout=subprocess.DEVNULL, - stderr=subprocess.DEVNULL, - ) - - self.start_monitoring(process.pid, tracking_time) - - def start_monitoring(self, pid: int, tracking_time: int = 60) -> None: - """ - 启动进程监视器,跟踪指定的主进程及其子进程 - - :param pid: 被监视进程的PID - :param tracking_time: 子进程追踪持续时间(秒) - """ - - self.clear() - - self.main_pid = pid - self.tracking_time = tracking_time - - # 扫描并记录所有相关进程 - try: - # 获取主进程 - main_proc = psutil.Process(self.main_pid) - self.tracked_pids.add(self.main_pid) - - # 递归获取所有子进程 - if tracking_time: - for child in main_proc.children(recursive=True): - self.tracked_pids.add(child.pid) - - except psutil.NoSuchProcess: - pass - - # 启动持续追踪机制 - self.start_time = datetime.now() - self.check_timer.start(100) - - def check_processes(self) -> None: - """检查跟踪的进程是否仍在运行,并更新子进程列表""" - - # 仅在时限内持续更新跟踪的进程列表,发现新的子进程 - if (datetime.now() - self.start_time).total_seconds() < self.tracking_time: - - current_pids = set(self.tracked_pids) - for pid in current_pids: - try: - proc = psutil.Process(pid) - for child in proc.children(): - if child.pid not in self.tracked_pids: - # 新发现的子进程 - self.tracked_pids.add(child.pid) - except psutil.NoSuchProcess: - continue - - if not self.is_running(): - self.clear() - self.processClosed.emit() - - def is_running(self) -> bool: - """检查所有跟踪的进程是否还在运行""" - - for pid in self.tracked_pids: - try: - proc = psutil.Process(pid) - if proc.is_running(): - return True - except psutil.NoSuchProcess: - continue - - return False - - def kill(self, if_force: bool = False) -> None: - """停止监视器并中止所有跟踪的进程""" - - self.check_timer.stop() - - for pid in self.tracked_pids: - try: - proc = psutil.Process(pid) - if if_force: - kill_process = subprocess.Popen( - ["taskkill", "/F", "/T", "/PID", str(pid)], - creationflags=subprocess.CREATE_NO_WINDOW, - ) - kill_process.wait() - proc.terminate() - except psutil.NoSuchProcess: - continue - - if self.main_pid: - self.processClosed.emit() - self.clear() - - def clear(self) -> None: - """清空跟踪的进程列表""" - - self.main_pid = None - self.check_timer.stop() - self.tracked_pids.clear() diff --git a/app/utils/__init__.py b/app/utils/__init__.py deleted file mode 100644 index ead5c44..0000000 --- a/app/utils/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA工具包 -v4.4 -作者:DLmaster_361 -""" - -__version__ = "4.2.0" -__author__ = "DLmaster361 " -__license__ = "GPL-3.0 license" - -from .ImageUtils import ImageUtils -from .ProcessManager import ProcessManager - -__all__ = ["ImageUtils", "ProcessManager"] diff --git a/app/utils/config.py b/app/utils/config.py new file mode 100644 index 0000000..0946275 --- /dev/null +++ b/app/utils/config.py @@ -0,0 +1,77 @@ +''' +配置文件管理, 使用yaml格式\n +注意: 此文件仅处理程序配置项而非用户配置 +''' + +from pathlib import Path +from typing import Any, ClassVar +import yaml +from pydantic import BaseModel + +class BaseYAMLConfig(BaseModel): + # 必要项 + CONFIG_PATH: ClassVar[Path] + + @classmethod + def load(cls, config_path: Path | None = None) -> "BaseYAMLConfig": + """ + 从 YAML 文件加载配置。如果文件不存在或为空,则使用类中定义的默认值。 + """ + # 使用传入路径或类变量路径 + if not cls.CONFIG_PATH: + raise ValueError("CONFIG_PATH必须被设置") + path = (config_path or cls.CONFIG_PATH).resolve() + + if not path.exists(): + # print(f"[Warning] Config file '{path}' not found. Using default values.") + data: dict[str, Any] = {} + else: + try: + with open(path, "r", encoding="utf-8") as f: + data = yaml.safe_load(f) + if data is None: + data = {} + if not isinstance(data, dict): + raise ValueError(f"Expected dict in {path}, got {type(data)}") + except Exception as e: + raise RuntimeError(f"Failed to load config from {path}: {e}") + + try: + return cls.model_validate(data) + except Exception as e: + raise ValueError(f"Invalid config for {cls.__name__}: {e}") + + +class LogConfig (BaseModel): + """ + 日志配置 + """ + CONFIG_PATH:ClassVar[Path] = Path("config.yaml") + + level: str = "DEBUG" + writelevel: str = "INFO" + log_dir: str = "logs" + max_size: int = 10 + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/utils/logger.py b/app/utils/logger.py new file mode 100644 index 0000000..073bdf4 --- /dev/null +++ b/app/utils/logger.py @@ -0,0 +1,70 @@ +from loguru import logger as _logger +import sys +import os +from app.utils.config import LogConfig + +config = LogConfig() +LOG_DIR = config.log_dir +os.makedirs(LOG_DIR, exist_ok=True) + + +_logger.remove() + +console_format = ( + "{time:YYYY-MM-DD HH:mm:ss}| " + "{level: <8} | " + "{extra[module]}- {message}" +) + +_logger.add( + sink=sys.stderr, + format=console_format, + level=config.level, + colorize=True, + enqueue=True, +) + + + + +file_format = "{time:YYYY-MM-DD HH:mm:ss}| {level: <8} | {extra[module]}- {message}" + +_logger.add( + sink=os.path.join(LOG_DIR, "app_{time:YYYY-MM-DD}.log"), + format=file_format, + level=config.writelevel, + rotation="00:00", + retention="7 days", + colorize=False, + encoding="utf-8", + enqueue=True, +) + +_logger = _logger.patch(lambda record: record["extra"].setdefault("module", "未知模块")) + + +def get_logger(module_name: str): + """ + 获取一个绑定 module 名的日志器 + :param module_name: 模块名称,如 "用户管理" + :return: 绑定后的 logger + """ + return _logger.bind(module=module_name) + + +__all__ = ["get_logger"] + + +if __name__ == "__main__": + logger = get_logger("test1") + logger.debug("调试信息(只在控制台显示)") + logger.info("这是普通信息(控制台 + 文件)") + logger.warning("这是警告") + logger.error("发生错误") + logger2 = get_logger("test2") + logger2.error("发生错误") + + try: + _ = 1 / 0 + except Exception: + logger.exception("捕获异常") diff --git a/app/utils/package.py b/app/utils/package.py deleted file mode 100644 index 59d2d20..0000000 --- a/app/utils/package.py +++ /dev/null @@ -1,143 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA打包程序 -v4.4 -作者:DLmaster_361 -""" - -import os -import sys -import json -import shutil -from pathlib import Path - - -def version_text(version_numb: list) -> str: - """将版本号列表转为可读的文本信息""" - - while len(version_numb) < 4: - version_numb.append(0) - - if version_numb[3] == 0: - version = f"v{'.'.join(str(_) for _ in version_numb[0:3])}" - else: - version = ( - f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}" - ) - return version - - -def version_info_markdown(info: dict) -> str: - """将版本信息字典转为markdown信息""" - - version_info = "" - for key, value in info.items(): - version_info += f"## {key}\n" - for v in value: - version_info += f"- {v}\n" - return version_info - - -if __name__ == "__main__": - - root_path = Path(sys.argv[0]).resolve().parent - - with (root_path / "resources/version.json").open(mode="r", encoding="utf-8") as f: - version = json.load(f) - - main_version_numb = list(map(int, version["main_version"].split("."))) - - print("Packaging AUTO_MAA main program ...") - - os.system( - "powershell -Command python -m nuitka --standalone --onefile --mingw64 --windows-uac-admin" - " --enable-plugins=pyside6 --windows-console-mode=attach" - " --onefile-tempdir-spec='{TEMP}\\AUTO_MAA'" - " --windows-icon-from-ico=resources\\icons\\AUTO_MAA.ico" - " --company-name='AUTO_MAA Team' --product-name=AUTO_MAA" - f" --file-version={version['main_version']}" - f" --product-version={version['main_version']}" - " --file-description='AUTO_MAA Component'" - " --copyright='Copyright © 2024-2025 DLmaster361'" - " --assume-yes-for-downloads --output-filename=AUTO_MAA" - " --remove-output main.py" - ) - - print("AUTO_MAA main program packaging completed !") - - print("start to create setup program ...") - - (root_path / "AUTO_MAA").mkdir(parents=True, exist_ok=True) - shutil.move(root_path / "AUTO_MAA.exe", root_path / "AUTO_MAA/") - shutil.copytree(root_path / "app", root_path / "AUTO_MAA/app") - shutil.copytree(root_path / "resources", root_path / "AUTO_MAA/resources") - shutil.copy(root_path / "main.py", root_path / "AUTO_MAA/") - shutil.copy(root_path / "requirements.txt", root_path / "AUTO_MAA/") - shutil.copy(root_path / "README.md", root_path / "AUTO_MAA/") - shutil.copy(root_path / "LICENSE", root_path / "AUTO_MAA/") - - with (root_path / "app/utils/AUTO_MAA.iss").open(mode="r", encoding="utf-8") as f: - iss = f.read() - iss = ( - iss.replace( - '#define MyAppVersion ""', - f'#define MyAppVersion "{version["main_version"]}"', - ) - .replace( - '#define MyAppPath ""', f'#define MyAppPath "{root_path / "AUTO_MAA"}"' - ) - .replace('#define OutputDir ""', f'#define OutputDir "{root_path}"') - ) - with (root_path / "AUTO_MAA.iss").open(mode="w", encoding="utf-8") as f: - f.write(iss) - - os.system(f'ISCC "{root_path / "AUTO_MAA.iss"}"') - - (root_path / "AUTO_MAA_Setup").mkdir(parents=True, exist_ok=True) - shutil.move(root_path / "AUTO_MAA-Setup.exe", root_path / "AUTO_MAA_Setup") - - shutil.make_archive( - base_name=root_path / f"AUTO_MAA_{version_text(main_version_numb)}", - format="zip", - root_dir=root_path / "AUTO_MAA_Setup", - base_dir=".", - ) - - print("setup program created !") - - (root_path / "AUTO_MAA.iss").unlink(missing_ok=True) - shutil.rmtree(root_path / "AUTO_MAA") - shutil.rmtree(root_path / "AUTO_MAA_Setup") - - all_version_info = {} - for v_i in version["version_info"].values(): - for key, value in v_i.items(): - if key in all_version_info: - all_version_info[key] += value.copy() - else: - all_version_info[key] = value.copy() - - (root_path / "version_info.txt").write_text( - f"{version_text(main_version_numb)}\n\n\n{version_info_markdown(all_version_info)}", - encoding="utf-8", - ) diff --git a/config/data_config.json b/config/data_config.json new file mode 100644 index 0000000..2d6259c --- /dev/null +++ b/config/data_config.json @@ -0,0 +1,4 @@ +{ + "instance_order": [], + "instances": {} +} \ No newline at end of file diff --git a/dev.md b/dev.md new file mode 100644 index 0000000..f875c5b --- /dev/null +++ b/dev.md @@ -0,0 +1,24 @@ +# 开发指南 + + +## 安装依赖 +```bash +uv install +``` + +## 开发启动 +```bash +uv run main.py +``` + +## 添加依赖 +```bash +uv add +``` + +## 删除依赖 +```bash +uv remove +``` + +> 💡 推荐使用uv作为默认包管理器,支持现代Python项目管理特性 \ No newline at end of file diff --git a/list/Scripts/python.exe b/list/Scripts/python.exe new file mode 100644 index 0000000..b654f2a Binary files /dev/null and b/list/Scripts/python.exe differ diff --git a/list/Scripts/pythonw.exe b/list/Scripts/pythonw.exe new file mode 100644 index 0000000..0e27747 Binary files /dev/null and b/list/Scripts/pythonw.exe differ diff --git a/main.py b/main.py index f255c39..e69de29 100644 --- a/main.py +++ b/main.py @@ -1,88 +0,0 @@ -# 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 . - -# Contact: DLmaster_361@163.com - -""" -AUTO_MAA -AUTO_MAA主程序 -v4.4 -作者:DLmaster_361 -""" - -# 屏蔽广告 -import builtins - -original_print = builtins.print - - -def no_print(*args, **kwargs): - if ( - args - and isinstance(args[0], str) - and "QFluentWidgets Pro is now released." in args[0] - ): - return - return original_print(*args, **kwargs) - - -builtins.print = no_print - - -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: - """检查当前程序是否以管理员身份运行""" - try: - return ctypes.windll.shell32.IsUserAnAdmin() - except: - return False - - -@logger.catch -def main(): - - application = QApplication(sys.argv) - - translator = FluentTranslator() - application.installTranslator(translator) - - from app.ui.main_window import AUTO_MAA - - window = AUTO_MAA() - window.show_ui("显示主窗口", if_start=True) - window.start_up_task() - sys.exit(application.exec()) - - -if __name__ == "__main__": - - if is_admin(): - main() - else: - ctypes.windll.shell32.ShellExecuteW( - None, "runas", sys.executable, os.path.realpath(sys.argv[0]), None, 1 - ) - sys.exit(0) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..53b4767 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,14 @@ +[project] +name = "maa-auto" +version = "0.1.0" +description = "maa-auto~" +readme = "README.md" +requires-python = ">=3.13" +dependencies = [ + "aiofiles>=24.1.0", + "fastapi>=0.116.1", + "loguru>=0.7.3", + "pydantic>=2.11.7", + "pyyaml>=6.0.2", + "uvicorn>=0.35.0", +] diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 49ef1b2..0000000 --- a/requirements.txt +++ /dev/null @@ -1,16 +0,0 @@ -loguru==0.7.3 -plyer==2.1.0 -PySide6==6.9.1 -PySide6-Fluent-Widgets[full]==1.8.3 -psutil==7.0.0 -pywin32==310 -keyboard==0.13.5 -pycryptodome==3.23.0 -certifi==2025.4.26 -truststore==0.10.1 -requests==2.32.4 -markdown==3.8.2 -Jinja2==3.1.6 -nuitka==2.7.12 -pillow==11.3.0 -packaging==25.0 diff --git a/resources/docs/ChineseSimplified.isl b/resources/docs/ChineseSimplified.isl deleted file mode 100644 index 855da1a..0000000 --- a/resources/docs/ChineseSimplified.isl +++ /dev/null @@ -1,403 +0,0 @@ -; *** Inno Setup version 6.4.0+ Chinese Simplified messages *** -; -; To download user-contributed translations of this file, go to: -; https://jrsoftware.org/files/istrans/ -; -; Note: When translating this text, do not add periods (.) to the end of -; messages that didn't have them already, because on those messages Inno -; Setup adds the periods automatically (appending a period would result in -; two periods being displayed). -; -; Maintained by Zhenghan Yang -; Email: 847320916@QQ.com -; Translation based on network resource -; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation -; - -[LangOptions] -; The following three entries are very important. Be sure to read and -; understand the '[LangOptions] section' topic in the help file. -LanguageName=简体中文 -; If Language Name display incorrect, uncomment next line -; LanguageName=<7B80><4F53><4E2D><6587> -; About LanguageID, to reference link: -; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c -LanguageID=$0804 -; About CodePage, to reference link: -; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers -LanguageCodePage=936 -; If the language you are translating to requires special font faces or -; sizes, uncomment any of the following entries and change them accordingly. -;DialogFontName= -;DialogFontSize=8 -;WelcomeFontName=Verdana -;WelcomeFontSize=12 -;TitleFontName=Arial -;TitleFontSize=29 -;CopyrightFontName=Arial -;CopyrightFontSize=8 - -[Messages] - -; *** 应用程序标题 -SetupAppTitle=安装 -SetupWindowTitle=安装 - %1 -UninstallAppTitle=卸载 -UninstallAppFullTitle=%1 卸载 - -; *** Misc. common -InformationTitle=信息 -ConfirmTitle=确认 -ErrorTitle=错误 - -; *** SetupLdr messages -SetupLdrStartupMessage=现在将安装 %1。您想要继续吗? -LdrCannotCreateTemp=无法创建临时文件。安装程序已中止 -LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止 -HelpTextNote= - -; *** 启动错误消息 -LastErrorMessage=%1。%n%n错误 %2: %3 -SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。 -SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。 -SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。 -InvalidParameter=无效的命令行参数:%n%n%1 -SetupAlreadyRunning=安装程序正在运行。 -WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。 -WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。 -NotOnThisPlatform=此程序不能在 %1 上运行。 -OnlyOnThisPlatform=此程序只能在 %1 上运行。 -OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1 -WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。 -WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。 -AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。 -PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。 -SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 -UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。 - -; *** 启动问题 -PrivilegesRequiredOverrideTitle=选择安装程序模式 -PrivilegesRequiredOverrideInstruction=选择安装模式 -PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。 -PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。 -PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A) -PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项) -PrivilegesRequiredOverrideCurrentUser=只为我安装(&M) -PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项) - -; *** 其他错误 -ErrorCreatingDir=安装程序无法创建目录“%1” -ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件 - -; *** 安装程序公共消息 -ExitSetupTitle=退出安装程序 -ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗? -AboutSetupMenuItem=关于安装程序(&A)... -AboutSetupTitle=关于安装程序 -AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4 -AboutSetupNote= -TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation - -; *** 按钮 -ButtonBack=< 上一步(&B) -ButtonNext=下一步(&N) > -ButtonInstall=安装(&I) -ButtonOK=确定 -ButtonCancel=取消 -ButtonYes=是(&Y) -ButtonYesToAll=全是(&A) -ButtonNo=否(&N) -ButtonNoToAll=全否(&O) -ButtonFinish=完成(&F) -ButtonBrowse=浏览(&B)... -ButtonWizardBrowse=浏览(&R)... -ButtonNewFolder=新建文件夹(&M) - -; *** “选择语言”对话框消息 -SelectLanguageTitle=选择安装语言 -SelectLanguageLabel=选择安装时使用的语言。 - -; *** 公共向导文字 -ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。 -BeveledLabel= -BrowseDialogTitle=浏览文件夹 -BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。 -NewFolderName=新建文件夹 - -; *** “欢迎”向导页 -WelcomeLabel1=欢迎使用 [name] 安装向导 -WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。 - -; *** “密码”向导页 -WizardPassword=密码 -PasswordLabel1=这个安装程序有密码保护。 -PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。 -PasswordEditLabel=密码(&P): -IncorrectPassword=您输入的密码不正确,请重新输入。 - -; *** “许可协议”向导页 -WizardLicense=许可协议 -LicenseLabel=请在继续安装前阅读以下重要信息。 -LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。 -LicenseAccepted=我同意此协议(&A) -LicenseNotAccepted=我不同意此协议(&D) - -; *** “信息”向导页 -WizardInfoBefore=信息 -InfoBeforeLabel=请在继续安装前阅读以下重要信息。 -InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。 -WizardInfoAfter=信息 -InfoAfterLabel=请在继续安装前阅读以下重要信息。 -InfoAfterClickLabel=准备好继续安装后,点击“下一步”。 - -; *** “用户信息”向导页 -WizardUserInfo=用户信息 -UserInfoDesc=请输入您的信息。 -UserInfoName=用户名(&U): -UserInfoOrg=组织(&O): -UserInfoSerial=序列号(&S): -UserInfoNameRequired=您必须输入用户名。 - -; *** “选择目标目录”向导页 -WizardSelectDir=选择目标位置 -SelectDirDesc=您想将 [name] 安装在哪里? -SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。 -SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 -DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。 -DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。 -CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。 -CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。 -InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share -InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。 -DiskSpaceWarningTitle=磁盘空间不足 -DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗? -DirNameTooLong=文件夹名称或路径太长。 -InvalidDirName=文件夹名称无效。 -BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1 -DirExistsTitle=文件夹已存在 -DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗? -DirDoesntExistTitle=文件夹不存在 -DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗? - -; *** “选择组件”向导页 -WizardSelectComponents=选择组件 -SelectComponentsDesc=您想安装哪些程序组件? -SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。 -FullInstallation=完全安装 -; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language) -CompactInstallation=简洁安装 -CustomInstallation=自定义安装 -NoUninstallWarningTitle=组件已存在 -NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗? -ComponentSize1=%1 KB -ComponentSize2=%1 MB -ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。 -ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。 - -; *** “选择附加任务”向导页 -WizardSelectTasks=选择附加任务 -SelectTasksDesc=您想要安装程序执行哪些附加任务? -SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。 - -; *** “选择开始菜单文件夹”向导页 -WizardSelectProgramGroup=选择开始菜单文件夹 -SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式? -SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。 -SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。 -MustEnterGroupName=您必须输入一个文件夹名。 -GroupNameTooLong=文件夹名或路径太长。 -InvalidGroupName=无效的文件夹名字。 -BadGroupName=文件夹名不能包含下列任何字符:%n%n%1 -NoProgramGroupCheck2=不创建开始菜单文件夹(&D) - -; *** “准备安装”向导页 -WizardReady=准备安装 -ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。 -ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。 -ReadyLabel2b=点击“安装”继续此安装程序。 -ReadyMemoUserInfo=用户信息: -ReadyMemoDir=目标位置: -ReadyMemoType=安装类型: -ReadyMemoComponents=已选择组件: -ReadyMemoGroup=开始菜单文件夹: -ReadyMemoTasks=附加任务: - -; *** TExtractionWizardPage wizard page and Extract7ZipArchive -ExtractionLabel=正在提取附加文件... -ButtonStopExtraction=停止提取(&S) -StopExtraction=您确定要停止提取吗? -ErrorExtractionAborted=提取已中止 -ErrorExtractionFailed=提取失败:%1 - -; *** TDownloadWizardPage wizard page and DownloadTemporaryFile -DownloadingLabel=正在下载附加文件... -ButtonStopDownload=停止下载(&S) -StopDownload=您确定要停止下载吗? -ErrorDownloadAborted=下载已中止 -ErrorDownloadFailed=下载失败:%1 %2 -ErrorDownloadSizeFailed=获取下载大小失败:%1 %2 -ErrorFileHash1=校验文件哈希失败:%1 -ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2 -ErrorProgress=无效的进度:%1 / %2 -ErrorFileSize=文件大小错误:预期 %1,实际 %2 - -; *** “正在准备安装”向导页 -WizardPreparing=正在准备安装 -PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。 -PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。 -CannotContinue=安装程序不能继续。请点击“取消”退出。 -ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。 -ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。 -CloseApplications=自动关闭应用程序(&A) -DontCloseApplications=不要关闭应用程序(&D) -ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。 -PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动? - -; *** “正在安装”向导页 -WizardInstalling=正在安装 -InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。 - -; *** “安装完成”向导页 -FinishedHeadingLabel=[name] 安装完成 -FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。 -FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。 -ClickFinish=点击“完成”退出安装程序。 -FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗? -FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗? -ShowReadmeCheck=是,我想查阅自述文件 -YesRadio=是,立即重启电脑(&Y) -NoRadio=否,稍后重启电脑(&N) -; used for example as 'Run MyProg.exe' -RunEntryExec=运行 %1 -; used for example as 'View Readme.txt' -RunEntryShellExec=查阅 %1 - -; *** “安装程序需要下一张磁盘”提示 -ChangeDiskTitle=安装程序需要下一张磁盘 -SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。 -PathLabel=路径(&P): -FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。 -SelectDirectoryLabel=请指定下一张磁盘的位置。 - -; *** 安装状态消息 -SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。 -AbortRetryIgnoreSelectAction=选择操作 -AbortRetryIgnoreRetry=重试(&T) -AbortRetryIgnoreIgnore=忽略错误并继续(&I) -AbortRetryIgnoreCancel=关闭安装程序 - -; *** 安装状态消息 -StatusClosingApplications=正在关闭应用程序... -StatusCreateDirs=正在创建目录... -StatusExtractFiles=正在解压缩文件... -StatusCreateIcons=正在创建快捷方式... -StatusCreateIniEntries=正在创建 INI 条目... -StatusCreateRegistryEntries=正在创建注册表条目... -StatusRegisterFiles=正在注册文件... -StatusSavingUninstall=正在保存卸载信息... -StatusRunProgram=正在完成安装... -StatusRestartingApplications=正在重启应用程序... -StatusRollback=正在撤销更改... - -; *** 其他错误 -ErrorInternal2=内部错误:%1 -ErrorFunctionFailedNoCode=%1 失败 -ErrorFunctionFailed=%1 失败;错误代码 %2 -ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3 -ErrorExecutingProgram=无法执行文件:%n%1 - -; *** 注册表错误 -ErrorRegOpenKey=打开注册表项时出错:%n%1\%2 -ErrorRegCreateKey=创建注册表项时出错:%n%1\%2 -ErrorRegWriteKey=写入注册表项时出错:%n%1\%2 - -; *** INI 错误 -ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。 - -; *** 文件复制错误 -FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐) -FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐) -SourceIsCorrupted=源文件已损坏 -SourceDoesntExist=源文件“%1”不存在 -ExistingFileReadOnly2=无法替换现有文件,它是只读的。 -ExistingFileReadOnlyRetry=移除只读属性并重试(&R) -ExistingFileReadOnlyKeepExisting=保留现有文件(&K) -ErrorReadingExistingDest=尝试读取现有文件时出错: -FileExistsSelectAction=选择操作 -FileExists2=文件已经存在。 -FileExistsOverwriteExisting=覆盖已存在的文件(&O) -FileExistsKeepExisting=保留现有的文件(&K) -FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) -ExistingFileNewerSelectAction=选择操作 -ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。 -ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O) -ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐) -ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D) -ErrorChangingAttr=尝试更改下列现有文件的属性时出错: -ErrorCreatingTemp=尝试在目标目录创建文件时出错: -ErrorReadingSource=尝试读取下列源文件时出错: -ErrorCopying=尝试复制下列文件时出错: -ErrorReplacingExistingFile=尝试替换现有文件时出错: -ErrorRestartReplace=重启并替换失败: -ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错: -ErrorRegisterServer=无法注册 DLL/OCX:%1 -ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1 -ErrorRegisterTypeLib=无法注册类库:%1 - -; *** 卸载显示名字标记 -; used for example as 'My Program (32-bit)' -UninstallDisplayNameMark=%1 (%2) -; used for example as 'My Program (32-bit, All users)' -UninstallDisplayNameMarks=%1 (%2, %3) -UninstallDisplayNameMark32Bit=32 位 -UninstallDisplayNameMark64Bit=64 位 -UninstallDisplayNameMarkAllUsers=所有用户 -UninstallDisplayNameMarkCurrentUser=当前用户 - -; *** 安装后错误 -ErrorOpeningReadme=尝试打开自述文件时出错。 -ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。 - -; *** 卸载消息 -UninstallNotFound=文件“%1”不存在。无法卸载。 -UninstallOpenError=文件“%1”不能被打开。无法卸载。 -UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载 -UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1) -ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗? -UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。 -OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。 -UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。 -UninstalledAll=已顺利从您的电脑中移除 %1。 -UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。 -UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗? -UninstallDataCorrupted=文件“%1”已损坏。无法卸载 - -; *** 卸载状态消息 -ConfirmDeleteSharedFileTitle=删除共享的文件吗? -ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。 -SharedFileNameLabel=文件名: -SharedFileLocationLabel=位置: -WizardUninstalling=卸载状态 -StatusUninstalling=正在卸载 %1... - -; *** Shutdown block reasons -ShutdownBlockReasonInstallingApp=正在安装 %1。 -ShutdownBlockReasonUninstallingApp=正在卸载 %1。 - -; The custom messages below aren't used by Setup itself, but if you make -; use of them in your scripts, you'll want to translate them. - -[CustomMessages] - -NameAndVersion=%1 版本 %2 -AdditionalIcons=附加快捷方式: -CreateDesktopIcon=创建桌面快捷方式(&D) -CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q) -ProgramOnTheWeb=%1 网站 -UninstallProgram=卸载 %1 -LaunchProgram=运行 %1 -AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A) -AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联... -AutoStartProgramGroupDescription=启动: -AutoStartProgram=自动启动 %1 -AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗? \ No newline at end of file diff --git a/resources/docs/MAA_config_info.txt b/resources/docs/MAA_config_info.txt deleted file mode 100644 index a19847f..0000000 --- a/resources/docs/MAA_config_info.txt +++ /dev/null @@ -1,65 +0,0 @@ -#主界面 -"MainFunction.PostActions": "8" #完成后退出MAA -"MainFunction.PostActions": "9" #完成后退出MAA和游戏 -"MainFunction.PostActions": "12" #完成后退出MAA和模拟器 -"TaskQueue.WakeUp.IsChecked": "True" #开始唤醒 -"TaskQueue.Recruiting.IsChecked": "True" #自动公招 -"TaskQueue.Base.IsChecked": "True" #基建换班 -"TaskQueue.Combat.IsChecked": "True" #刷理智 -"TaskQueue.Mall.IsChecked": "True" #获取信用及购物 -"TaskQueue.Mission.IsChecked": "True" #领取奖励 -"TaskQueue.AutoRoguelike.IsChecked": "False" #自动肉鸽 -"TaskQueue.Reclamation.IsChecked": "False" #生息演算 -"TaskQueue.Order.WakeUp": "0" -"TaskQueue.Order.Recruiting": "1" -"TaskQueue.Order.Base": "2" -"TaskQueue.Order.Combat": "3" -"TaskQueue.Order.Mall": "4" -"TaskQueue.Order.Mission": "5" -"TaskQueue.Order.AutoRoguelike": "6" -"TaskQueue.Order.Reclamation": "7" -#刷理智 -"MainFunction.UseMedicine": "True" #吃理智药 -"MainFunction.UseMedicine.Quantity": "999" #吃理智药数量 -"MainFunction.Stage1": "" #主关卡 -"MainFunction.Stage2": "" #备选关卡1 -"MainFunction.Stage3": "" #备选关卡2 -"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" #使用备选关卡 -"Fight.UseRemainingSanityStage": "True" #使用剩余理智 -"GUI.AllowUseStoneSave": "False" #允许吃源石保持状态 -"Fight.UseExpiringMedicine": "False" #无限吃48小时内过期的理智药 -"GUI.HideUnavailableStage": "False" #隐藏当日不开放关卡 -"GUI.HideSeries": "False" #隐藏连战次数 -"Infrast.CustomInfrastPlanShowInFightSettings": "False" #显示基建计划 -"Penguin.EnablePenguin": "True" #上报企鹅物流 -"Yituliu.EnableYituliu": "True" #上报一图流 -#基建换班 -"Infrast.InfrastMode": "Normal"、"Rotation"、"Custom" #基建模式 -"Infrast.CustomInfrastPlanIndex": "1" #自定义基建配置索引号 -"Infrast.DefaultInfrast": "user_defined" #内置配置 -"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读 -"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址 -#设置 -"Start.ClientType": "Official"、"Bilibili"、"YoStarEN"、"YoStarJP"、"YoStarKR"、"txwy" #服务器 -G"Timer.Timer1": "False" #时间设置1 -"Connect.AdbPath" #ADB路径 -"Connect.Address": "127.0.0.1:16448" #连接地址 -G"VersionUpdate.ScheduledUpdateCheck": "True" #定时检查更新 -G"VersionUpdate.AutoDownloadUpdatePackage": "True" #自动下载更新包 -G"VersionUpdate.AutoInstallUpdatePackage": "True" #自动安装更新包 -G"Start.MinimizeDirectly": "True" #启动MAA后直接最小化 -"Start.RunDirectly": "True" #启动MAA后直接运行 -"Start.OpenEmulatorAfterLaunch": "True" #启动MAA后自动开启模拟器 -G"GUI.UseTray": "True" #显示托盘图标 -G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘 -"Start.EmulatorPath" #模拟器路径 -"Start.EmulatorAddCommand": "-v 2" #附加命令 -"Start.EmulatorWaitSeconds": "10" #等待模拟器启动时间 -G"VersionUpdate.package": "MirrorChyanAppv5.15.6.zip" #更新包标识 \ No newline at end of file diff --git a/resources/html/MAA_result.html b/resources/html/MAA_result.html deleted file mode 100644 index 78328d2..0000000 --- a/resources/html/MAA_result.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - -
    -
    -

    {{ title }}

    - - -
    - -
    -

    脚本实例名称:{{ script_name }}

    -

    任务开始时间:{{ start_time }}

    -

    任务结束时间:{{ end_time }}

    -

    已完成数:{{ completed_count }}

    - {% if uncompleted_count %} -

    未完成数:{{ uncompleted_count }}

    - {% endif %} - {% if failed_user %} -

    代理未成功的用户: {{ failed_user }}

    - {% endif %} - {% if waiting_user %} -

    未开始代理的用户: {{ waiting_user }}

    - {% endif %} -
    - -

    AUTO_MAA 敬上

    - - - -
    - - - \ No newline at end of file diff --git a/resources/html/MAA_six_star.html b/resources/html/MAA_six_star.html deleted file mode 100644 index ef38222..0000000 --- a/resources/html/MAA_six_star.html +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - Base64 Image - - - \ No newline at end of file diff --git a/resources/html/MAA_statistics.html b/resources/html/MAA_statistics.html deleted file mode 100644 index 561faa4..0000000 --- a/resources/html/MAA_statistics.html +++ /dev/null @@ -1,233 +0,0 @@ - - - - - - - - -
    -
    -

    自动代理统计报告

    - - -
    - -
    -

    用户代理信息:{{ user_info }}

    -

    任务开始时间:{{ start_time }}

    -

    任务结束时间:{{ end_time }}

    -

    MAA执行结果: - {% if maa_result == '代理任务全部完成' %} - {{ maa_result }} - {% elif maa_result == '代理任务未全部完成' %} - {{ maa_result }} - {% else %} - {{ maa_result }} - {% endif %} -

    - - {% if recruit_statistics %} -

    公招统计

    - - - - - - {% for star, count in recruit_statistics.items() %} - - - - - {% endfor %} -
    星级数量
    {{ star }}{{ count }}
    - {% endif %} - - {% if drop_statistics %} - {% for stage, items in drop_statistics.items() %} -

    掉落统计({{ stage }})

    - - - - - - {% for item, amount in items.items() %} - - - - - {% endfor %} -
    物品数量
    {{ item }}{{ amount }}
    - {% endfor %} - {% endif %} -
    - -

    AUTO_MAA 敬上

    - - - -
    - - - \ No newline at end of file diff --git a/resources/html/general_result.html b/resources/html/general_result.html deleted file mode 100644 index 1a1c842..0000000 --- a/resources/html/general_result.html +++ /dev/null @@ -1,160 +0,0 @@ - - - - - - - - -
    -
    -

    {{ title }}

    - - -
    - -
    -

    脚本实例名称:{{ script_name }}

    -

    任务开始时间:{{ start_time }}

    -

    任务结束时间:{{ end_time }}

    -

    已完成数:{{ completed_count }}

    - {% if uncompleted_count %} -

    未完成数:{{ uncompleted_count }}

    - {% endif %} - {% if failed_sub %} -

    代理未成功的配置: {{ failed_sub }}

    - {% endif %} - {% if waiting_sub %} -

    未开始代理的配置: {{ waiting_sub }}

    - {% endif %} -
    - -

    AUTO_MAA 敬上

    - - - -
    - - - \ No newline at end of file diff --git a/resources/html/general_statistics.html b/resources/html/general_statistics.html deleted file mode 100644 index 9e577e0..0000000 --- a/resources/html/general_statistics.html +++ /dev/null @@ -1,200 +0,0 @@ - - - - - - - - -
    -
    -

    自动代理统计报告

    - - -
    - -
    -

    用户代理信息:{{ sub_info }}

    -

    任务开始时间:{{ start_time }}

    -

    任务结束时间:{{ end_time }}

    -

    脚本执行结果: - {% if sub_result == '代理成功' %} - {{ sub_result }} - {% elif sub_result == '代理失败' %} - {{ sub_result }} - {% else %} - {{ sub_result }} - {% endif %} -

    - -
    - -

    AUTO_MAA 敬上

    - - - -
    - - - \ No newline at end of file diff --git a/resources/icons/AUTO_MAA.ico b/resources/icons/AUTO_MAA.ico deleted file mode 100644 index 6f9c7ae..0000000 Binary files a/resources/icons/AUTO_MAA.ico and /dev/null differ diff --git a/resources/icons/AUTO_MAA_Updater.ico b/resources/icons/AUTO_MAA_Updater.ico deleted file mode 100644 index 120746a..0000000 Binary files a/resources/icons/AUTO_MAA_Updater.ico and /dev/null differ diff --git a/resources/icons/MirrorChyan.ico b/resources/icons/MirrorChyan.ico deleted file mode 100644 index 2223251..0000000 Binary files a/resources/icons/MirrorChyan.ico and /dev/null differ diff --git a/resources/images/AUTO_MAA.png b/resources/images/AUTO_MAA.png deleted file mode 100644 index 669770c..0000000 Binary files a/resources/images/AUTO_MAA.png and /dev/null differ diff --git a/resources/images/Home/BannerDefault.png b/resources/images/Home/BannerDefault.png deleted file mode 100644 index ac4c489..0000000 Binary files a/resources/images/Home/BannerDefault.png and /dev/null differ diff --git a/resources/images/README/payid.png b/resources/images/README/payid.png deleted file mode 100644 index 91b671e..0000000 Binary files a/resources/images/README/payid.png and /dev/null differ diff --git a/resources/images/notification/six_star.png b/resources/images/notification/six_star.png deleted file mode 100644 index 2ef8c38..0000000 Binary files a/resources/images/notification/six_star.png and /dev/null differ diff --git a/resources/images/notification/test_notify.png b/resources/images/notification/test_notify.png deleted file mode 100644 index 6429431..0000000 Binary files a/resources/images/notification/test_notify.png and /dev/null differ diff --git a/resources/sounds/both/删除用户.wav b/resources/sounds/both/删除用户.wav deleted file mode 100644 index 779c63d..0000000 Binary files a/resources/sounds/both/删除用户.wav and /dev/null differ diff --git a/resources/sounds/both/删除脚本实例.wav b/resources/sounds/both/删除脚本实例.wav deleted file mode 100644 index 5bfad34..0000000 Binary files a/resources/sounds/both/删除脚本实例.wav and /dev/null differ diff --git a/resources/sounds/both/删除计划表.wav b/resources/sounds/both/删除计划表.wav deleted file mode 100644 index 5a58ad6..0000000 Binary files a/resources/sounds/both/删除计划表.wav and /dev/null differ diff --git a/resources/sounds/both/删除调度队列.wav b/resources/sounds/both/删除调度队列.wav deleted file mode 100644 index 3abe7e1..0000000 Binary files a/resources/sounds/both/删除调度队列.wav and /dev/null differ diff --git a/resources/sounds/both/欢迎回来.wav b/resources/sounds/both/欢迎回来.wav deleted file mode 100644 index 47e410b..0000000 Binary files a/resources/sounds/both/欢迎回来.wav and /dev/null differ diff --git a/resources/sounds/both/添加用户.wav b/resources/sounds/both/添加用户.wav deleted file mode 100644 index 96eafea..0000000 Binary files a/resources/sounds/both/添加用户.wav and /dev/null differ diff --git a/resources/sounds/both/添加脚本实例.wav b/resources/sounds/both/添加脚本实例.wav deleted file mode 100644 index 223170b..0000000 Binary files a/resources/sounds/both/添加脚本实例.wav and /dev/null differ diff --git a/resources/sounds/both/添加计划表.wav b/resources/sounds/both/添加计划表.wav deleted file mode 100644 index 1059282..0000000 Binary files a/resources/sounds/both/添加计划表.wav and /dev/null differ diff --git a/resources/sounds/both/添加调度队列.wav b/resources/sounds/both/添加调度队列.wav deleted file mode 100644 index c1159d9..0000000 Binary files a/resources/sounds/both/添加调度队列.wav and /dev/null differ diff --git a/resources/sounds/noisy/ADB失败.wav b/resources/sounds/noisy/ADB失败.wav deleted file mode 100644 index a432d67..0000000 Binary files a/resources/sounds/noisy/ADB失败.wav and /dev/null differ diff --git a/resources/sounds/noisy/ADB成功.wav b/resources/sounds/noisy/ADB成功.wav deleted file mode 100644 index 39f5022..0000000 Binary files a/resources/sounds/noisy/ADB成功.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA在完成任务前中止.wav b/resources/sounds/noisy/MAA在完成任务前中止.wav deleted file mode 100644 index 23c1b8f..0000000 Binary files a/resources/sounds/noisy/MAA在完成任务前中止.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA在完成任务前退出.wav b/resources/sounds/noisy/MAA在完成任务前退出.wav deleted file mode 100644 index 9aed847..0000000 Binary files a/resources/sounds/noisy/MAA在完成任务前退出.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA更新.wav b/resources/sounds/noisy/MAA更新.wav deleted file mode 100644 index f14afeb..0000000 Binary files a/resources/sounds/noisy/MAA更新.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA未检测到任何模拟器.wav b/resources/sounds/noisy/MAA未检测到任何模拟器.wav deleted file mode 100644 index 675361d..0000000 Binary files a/resources/sounds/noisy/MAA未检测到任何模拟器.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA未能正确登录PRTS.wav b/resources/sounds/noisy/MAA未能正确登录PRTS.wav deleted file mode 100644 index e0b55fe..0000000 Binary files a/resources/sounds/noisy/MAA未能正确登录PRTS.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA的ADB连接异常.wav b/resources/sounds/noisy/MAA的ADB连接异常.wav deleted file mode 100644 index 0db037b..0000000 Binary files a/resources/sounds/noisy/MAA的ADB连接异常.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA进程超时.wav b/resources/sounds/noisy/MAA进程超时.wav deleted file mode 100644 index cf5b5b4..0000000 Binary files a/resources/sounds/noisy/MAA进程超时.wav and /dev/null differ diff --git a/resources/sounds/noisy/MAA部分任务执行失败.wav b/resources/sounds/noisy/MAA部分任务执行失败.wav deleted file mode 100644 index 563fd32..0000000 Binary files a/resources/sounds/noisy/MAA部分任务执行失败.wav and /dev/null differ diff --git a/resources/sounds/noisy/任务开始.wav b/resources/sounds/noisy/任务开始.wav deleted file mode 100644 index acda117..0000000 Binary files a/resources/sounds/noisy/任务开始.wav and /dev/null differ diff --git a/resources/sounds/noisy/任务结束.wav b/resources/sounds/noisy/任务结束.wav deleted file mode 100644 index d1e8eac..0000000 Binary files a/resources/sounds/noisy/任务结束.wav and /dev/null differ diff --git a/resources/sounds/noisy/公告展示.wav b/resources/sounds/noisy/公告展示.wav deleted file mode 100644 index 91910f2..0000000 Binary files a/resources/sounds/noisy/公告展示.wav and /dev/null differ diff --git a/resources/sounds/noisy/公告通知.wav b/resources/sounds/noisy/公告通知.wav deleted file mode 100644 index e3b833e..0000000 Binary files a/resources/sounds/noisy/公告通知.wav and /dev/null differ diff --git a/resources/sounds/noisy/六星喜报.wav b/resources/sounds/noisy/六星喜报.wav deleted file mode 100644 index f6732f8..0000000 Binary files a/resources/sounds/noisy/六星喜报.wav and /dev/null differ diff --git a/resources/sounds/noisy/历史记录查询.wav b/resources/sounds/noisy/历史记录查询.wav deleted file mode 100644 index 7f1e7b5..0000000 Binary files a/resources/sounds/noisy/历史记录查询.wav and /dev/null differ diff --git a/resources/sounds/noisy/发生异常.wav b/resources/sounds/noisy/发生异常.wav deleted file mode 100644 index dca513c..0000000 Binary files a/resources/sounds/noisy/发生异常.wav and /dev/null differ diff --git a/resources/sounds/noisy/发生错误.wav b/resources/sounds/noisy/发生错误.wav deleted file mode 100644 index da8733a..0000000 Binary files a/resources/sounds/noisy/发生错误.wav and /dev/null differ diff --git a/resources/sounds/noisy/子任务失败.wav b/resources/sounds/noisy/子任务失败.wav deleted file mode 100644 index 1b798f7..0000000 Binary files a/resources/sounds/noisy/子任务失败.wav and /dev/null differ diff --git a/resources/sounds/noisy/排查录入.wav b/resources/sounds/noisy/排查录入.wav deleted file mode 100644 index 9c40147..0000000 Binary files a/resources/sounds/noisy/排查录入.wav and /dev/null differ diff --git a/resources/sounds/noisy/排查重试.wav b/resources/sounds/noisy/排查重试.wav deleted file mode 100644 index 185f190..0000000 Binary files a/resources/sounds/noisy/排查重试.wav and /dev/null differ diff --git a/resources/sounds/noisy/无新版本.wav b/resources/sounds/noisy/无新版本.wav deleted file mode 100644 index d03ed3c..0000000 Binary files a/resources/sounds/noisy/无新版本.wav and /dev/null differ diff --git a/resources/sounds/noisy/有新版本.wav b/resources/sounds/noisy/有新版本.wav deleted file mode 100644 index 7de578f..0000000 Binary files a/resources/sounds/noisy/有新版本.wav and /dev/null differ diff --git a/resources/sounds/noisy/森空岛签到失败.wav b/resources/sounds/noisy/森空岛签到失败.wav deleted file mode 100644 index 4ad31d5..0000000 Binary files a/resources/sounds/noisy/森空岛签到失败.wav and /dev/null differ diff --git a/resources/sounds/noisy/森空岛签到成功.wav b/resources/sounds/noisy/森空岛签到成功.wav deleted file mode 100644 index fd6319f..0000000 Binary files a/resources/sounds/noisy/森空岛签到成功.wav and /dev/null differ diff --git a/resources/sounds/simple/任务开始.wav b/resources/sounds/simple/任务开始.wav deleted file mode 100644 index 37b86cc..0000000 Binary files a/resources/sounds/simple/任务开始.wav and /dev/null differ diff --git a/resources/sounds/simple/任务结束.wav b/resources/sounds/simple/任务结束.wav deleted file mode 100644 index 3821fad..0000000 Binary files a/resources/sounds/simple/任务结束.wav and /dev/null differ diff --git a/resources/sounds/simple/公告展示.wav b/resources/sounds/simple/公告展示.wav deleted file mode 100644 index fc61355..0000000 Binary files a/resources/sounds/simple/公告展示.wav and /dev/null differ diff --git a/resources/sounds/simple/公告通知.wav b/resources/sounds/simple/公告通知.wav deleted file mode 100644 index 7dd88e9..0000000 Binary files a/resources/sounds/simple/公告通知.wav and /dev/null differ diff --git a/resources/sounds/simple/历史记录查询.wav b/resources/sounds/simple/历史记录查询.wav deleted file mode 100644 index ac45b0d..0000000 Binary files a/resources/sounds/simple/历史记录查询.wav and /dev/null differ diff --git a/resources/sounds/simple/发生异常.wav b/resources/sounds/simple/发生异常.wav deleted file mode 100644 index 8822c69..0000000 Binary files a/resources/sounds/simple/发生异常.wav and /dev/null differ diff --git a/resources/sounds/simple/发生错误.wav b/resources/sounds/simple/发生错误.wav deleted file mode 100644 index 633c5a9..0000000 Binary files a/resources/sounds/simple/发生错误.wav and /dev/null differ diff --git a/resources/sounds/simple/无新版本.wav b/resources/sounds/simple/无新版本.wav deleted file mode 100644 index 37509d9..0000000 Binary files a/resources/sounds/simple/无新版本.wav and /dev/null differ diff --git a/resources/sounds/simple/有新版本.wav b/resources/sounds/simple/有新版本.wav deleted file mode 100644 index c49d409..0000000 Binary files a/resources/sounds/simple/有新版本.wav and /dev/null differ diff --git a/resources/version.json b/resources/version.json deleted file mode 100644 index 8204af9..0000000 --- a/resources/version.json +++ /dev/null @@ -1,63 +0,0 @@ -{ - "main_version": "4.4.1.6", - "version_info": { - "4.4.1.6": { - "新增功能": [ - "启动时支持直接运行复数调度队列" - ], - "修复BUG": [ - "修复计划表未能按照鹰历获取关卡号的问题" - ] - }, - "4.4.1.5": { - "新增功能": [ - "适配 MAA 长期开放剿灭关卡", - "新增完成任务后自动复原脚本配置", - "通用脚本启动附加命令添加额外的语法以适应UI可执行文件与任务可执行文件不同的情况", - "新增 Go_Updater 独立更新器" - ], - "程序优化": [ - "优化调度队列配置逻辑", - "优化静默进程标记逻辑,避免未能及时移除导致相关功能持续开启", - "MAA 代理时更新改为强制开启", - "移除 MAA 详细配置模式中的剿灭项" - ] - }, - "4.4.1.4": { - "修复BUG": [ - "添加强制关机功能并优化关机流程" - ] - }, - "4.4.1.3": { - "修复BUG": [ - "移除崩溃弹窗机制" - ] - }, - "4.4.1.2": { - "新增功能": [ - "AUTO_MAA 配置分享中心上线" - ], - "修复BUG": [ - "日志读取添加兜底机制", - "修复 QTimer.singleShot 参数问题" - ], - "程序优化": [ - "小文件配置信息转移至AUTO_MAA自建服务" - ] - }, - "4.4.1.1": { - "新增功能": [ - "通用脚本支持在选定的时机自动更新配置文件" - ], - "修复BUG": [ - "修复MAA掉落物统计功能", - "修复模拟器界面被异常关闭且无法重新打开的问题" - ], - "程序优化": [ - "重构日志记录,载入更多日志记录项", - "优化日志监看启停逻辑", - "SpinBox和TimeEdit组件忽视滚轮事件" - ] - } - } -} \ No newline at end of file diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..7de4a46 --- /dev/null +++ b/uv.lock @@ -0,0 +1,247 @@ +version = 1 +revision = 2 +requires-python = ">=3.13" + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "click" +version = "8.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/87/105111999772ec9730e3d4d910c723ea9763ece2ec441533a5cea1e87e3c/click-8.2.2.tar.gz", hash = "sha256:068616e6ef9705a07b6db727cb9c248f4eb9dae437a30239f56fa94b18b852ef", size = 263977, upload-time = "2025-08-02T02:23:41.102Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/85/e7297e34133ae1cfde3bffd30c24e1ef055248251baa877834e048687a28/click-8.2.2-py3-none-any.whl", hash = "sha256:52e1e9f5d3db8c85aa76968c7c67ed41ddbacb167f43201511c8fd61eb5ba2ca", size = 103900, upload-time = "2025-08-02T02:23:39.299Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "fastapi" +version = "0.116.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/d7/6c8b3bfe33eeffa208183ec037fee0cce9f7f024089ab1c5d12ef04bd27c/fastapi-0.116.1.tar.gz", hash = "sha256:ed52cbf946abfd70c5a0dccb24673f0670deeb517a88b3544d03c2a6bf283143", size = 296485, upload-time = "2025-07-11T16:22:32.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "loguru" +version = "0.7.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "win32-setctime", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/05/a1dae3dffd1116099471c643b8924f5aa6524411dc6c63fdae648c4f1aca/loguru-0.7.3.tar.gz", hash = "sha256:19480589e77d47b8d85b2c827ad95d49bf31b0dcde16593892eb51dd18706eb6", size = 63559, upload-time = "2024-12-06T11:20:56.608Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/29/0348de65b8cc732daa3e33e67806420b2ae89bdce2b04af740289c5c6c8c/loguru-0.7.3-py3-none-any.whl", hash = "sha256:31a33c10c8e1e10422bfd431aeb5d351c7cf7fa671e3c4df004162264b28220c", size = 61595, upload-time = "2024-12-06T11:20:54.538Z" }, +] + +[[package]] +name = "maa-auto" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "aiofiles" }, + { name = "fastapi" }, + { name = "loguru" }, + { name = "pydantic" }, + { name = "pyyaml" }, + { name = "uvicorn" }, +] + +[package.metadata] +requires-dist = [ + { name = "aiofiles", specifier = ">=24.1.0" }, + { name = "fastapi", specifier = ">=0.116.1" }, + { name = "loguru", specifier = ">=0.7.3" }, + { name = "pydantic", specifier = ">=2.11.7" }, + { name = "pyyaml", specifier = ">=6.0.2" }, + { name = "uvicorn", specifier = ">=0.35.0" }, +] + +[[package]] +name = "pydantic" +version = "2.11.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/00/dd/4325abf92c39ba8623b5af936ddb36ffcfe0beae70405d456ab1fb2f5b8c/pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db", size = 788350, upload-time = "2025-06-14T08:33:17.137Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/c0/ec2b1c8712ca690e5d61979dee872603e92b8a32f94cc1b72d53beab008a/pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b", size = 444782, upload-time = "2025-06-14T08:33:14.905Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "starlette" +version = "0.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/57/d062573f391d062710d4088fa1369428c38d51460ab6fedff920efef932e/starlette-0.47.2.tar.gz", hash = "sha256:6ae9aa5db235e4846decc1e7b79c4f346adf41e9777aebeb49dfd09bbd7023d8", size = 2583948, upload-time = "2025-07-20T17:31:58.522Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/1f/b876b1f83aef204198a42dc101613fefccb32258e5428b5f9259677864b4/starlette-0.47.2-py3-none-any.whl", hash = "sha256:c5847e96134e5c5371ee9fac6fdf1a67336d5815e09eb2a01fdb57a351ef915b", size = 72984, upload-time = "2025-07-20T17:31:56.738Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.35.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/42/e0e305207bb88c6b8d3061399c6a961ffe5fbb7e2aa63c9234df7259e9cd/uvicorn-0.35.0.tar.gz", hash = "sha256:bc662f087f7cf2ce11a1d7fd70b90c9f98ef2e2831556dd078d131b96cc94a01", size = 78473, upload-time = "2025-06-28T16:15:46.058Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/e2/dc81b1bd1dcfe91735810265e9d26bc8ec5da45b4c0f6237e286819194c3/uvicorn-0.35.0-py3-none-any.whl", hash = "sha256:197535216b25ff9b785e29a0b79199f55222193d47f820816e7da751e9bc8d4a", size = 66406, upload-time = "2025-06-28T16:15:44.816Z" }, +] + +[[package]] +name = "win32-setctime" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b3/8f/705086c9d734d3b663af0e9bb3d4de6578d08f46b1b101c2442fd9aecaa2/win32_setctime-1.2.0.tar.gz", hash = "sha256:ae1fdf948f5640aae05c511ade119313fb6a30d7eabe25fef9764dca5873c4c0", size = 4867, upload-time = "2024-12-07T15:28:28.314Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/07/c6fe3ad3e685340704d314d765b7912993bcb8dc198f0e7a89382d37974b/win32_setctime-1.2.0-py3-none-any.whl", hash = "sha256:95d644c4e708aba81dc3704a116d8cbc974d70b3bdb8be1d150e36be6e9d1390", size = 4083, upload-time = "2024-12-07T15:28:26.465Z" }, +]