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 eb5a04e..5366a11 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,8 +1,31 @@
+# Python-generated files
__pycache__/
+*.py[oc]
+build/
+dist/
+wheels/
+logs/
+*.egg-info
+
+# Virtual environments
+.venv
+.python-version
+list/
+uv.lock
+
+# IDE and editors
+.vscode/
+.idea/
+*.swp
+*.swo
+.DS_Store
+
+# User files
+config/
data/
debug/
history/
script/
-resources/notice.json
-resources/theme_image.json
-resources/images/Home/BannerTheme.jpg
\ No newline at end of file
+res/notice.json
+res/theme_image.json
+res/images/Home/BannerTheme.jpg
\ No newline at end of file
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/README.md b/README.md
index ea0da17..e69de29 100644
--- a/README.md
+++ b/README.md
@@ -1,119 +0,0 @@
-AUTO_MAA
-
- MAA多账号管理与自动化软件
-
-
-
----
-
-
-
-
-
-
-
-
-
-
-
-
-## 软件介绍
-
-### 性质
-
-本软件是明日方舟第三方软件`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/)。
-
-## 贡献者
-
-感谢以下贡献者对本项目做出的贡献
-
-
-
-
-
-
-
-
-
-## Star History
-
-[](https://star-history.com/#DLmaster361/AUTO_MAA&Date)
-
-## 交流与赞助
-
-欢迎加入AUTO_MAA项目组,欢迎反馈bug
-
-- QQ交流群:[957750551](https://qm.qq.com/q/bd9fISNoME)
-
----
-
-如果喜欢这个项目的话,给作者来杯咖啡吧!
-
-
diff --git a/app/__init__.py b/app/__init__.py
index b2f64f6..29fe0ce 100644
--- a/app/__init__.py
+++ b/app/__init__.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,32 +19,15 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA主程序包
-v4.4
-作者:DLmaster_361
-"""
-
-__version__ = "4.2.0"
+__version__ = "5.0.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",
-]
+from .api import *
+from .core import *
+from .models import *
+from .services import *
+from .utils import *
+
+__all__ = ["api", "core", "models", "services", "utils"]
diff --git a/app/core/logger.py b/app/api/__init__.py
similarity index 55%
rename from app/core/logger.py
rename to app/api/__init__.py
index aa942dd..9a3f295 100644
--- a/app/core/logger.py
+++ b/app/api/__init__.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,17 +19,26 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA日志组件
-v4.4
-作者:DLmaster_361
-"""
+__version__ = "5.0.0"
+__author__ = "DLmaster361 "
+__license__ = "GPL-3.0 license"
-from loguru import logger as _logger
+from .core import router as core_router
+from .info import router as info_router
+from .scripts import router as scripts_router
+from .plan import router as plan_router
+from .queue import router as queue_router
+from .dispatch import router as dispatch_router
+from .history import router as history_router
+from .setting import router as setting_router
-# 设置日志 module 字段默认值
-logger = _logger.patch(
- lambda record: record["extra"].setdefault("module", "未知模块") or True
-)
-logger.remove(0)
+__all__ = [
+ "core_router",
+ "info_router",
+ "scripts_router",
+ "plan_router",
+ "queue_router",
+ "dispatch_router",
+ "history_router",
+ "setting_router",
+]
diff --git a/app/api/core.py b/app/api/core.py
new file mode 100644
index 0000000..25331cd
--- /dev/null
+++ b/app/api/core.py
@@ -0,0 +1,49 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+import asyncio
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect
+
+from app.core import Config, Broadcast
+from app.services import System
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/core", tags=["核心信息"])
+
+
+@router.websocket("/ws")
+async def connect_websocket(websocket: WebSocket):
+ await websocket.accept()
+ Config.websocket = websocket
+ while True:
+ try:
+ data = await asyncio.wait_for(websocket.receive_json(), timeout=30.0)
+ await Broadcast.put(data)
+ except asyncio.TimeoutError:
+ await websocket.send_json(
+ WebSocketMessage(
+ id="Main", type="Signal", data={"Ping": "无描述"}
+ ).model_dump()
+ )
+ except WebSocketDisconnect:
+ break
+ await System.set_power("KillSelf")
diff --git a/app/api/dispatch.py b/app/api/dispatch.py
new file mode 100644
index 0000000..77160a8
--- /dev/null
+++ b/app/api/dispatch.py
@@ -0,0 +1,72 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+import uuid
+import asyncio
+from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Body, Path
+
+from app.core import TaskManager, Broadcast
+from app.services import System
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/dispatch", tags=["任务调度"])
+
+
+@router.post(
+ "/start", summary="添加任务", response_model=TaskCreateOut, status_code=200
+)
+async def add_task(task: TaskCreateIn = Body(...)) -> TaskCreateOut:
+
+ try:
+ task_id = await TaskManager.add_task(task.mode, task.taskId)
+ except Exception as e:
+ return TaskCreateOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ websocketId="",
+ )
+ return TaskCreateOut(websocketId=str(task_id))
+
+
+@router.post("/stop", summary="中止任务", response_model=OutBase, status_code=200)
+async def stop_task(task: DispatchIn = Body(...)) -> OutBase:
+
+ try:
+ await TaskManager.stop_task(task.taskId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post("/power", summary="电源操作", response_model=OutBase, status_code=200)
+async def power_task(task: PowerIn = Body(...)) -> OutBase:
+
+ try:
+ await System.set_power(task.signal)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
diff --git a/app/api/history.py b/app/api/history.py
new file mode 100644
index 0000000..a7e3bd7
--- /dev/null
+++ b/app/api/history.py
@@ -0,0 +1,84 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+from datetime import datetime
+from pathlib import Path
+from fastapi import APIRouter, Body
+
+from app.core import Config
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/history", tags=["历史记录"])
+
+
+@router.post(
+ "/search",
+ summary="搜索历史记录总览信息",
+ response_model=HistorySearchOut,
+ status_code=200,
+)
+async def search_history(history: HistorySearchIn) -> HistorySearchOut:
+
+ try:
+ data = await Config.search_history(
+ history.mode,
+ datetime.strptime(history.start_date, "%Y-%m-%d").date(),
+ datetime.strptime(history.end_date, "%Y-%m-%d").date(),
+ )
+ for date, users in data.items():
+ for user, records in users.items():
+ record = await Config.merge_statistic_info(records)
+ record["index"] = [HistoryIndexItem(**_) for _ in record["index"]]
+ record = HistoryData(**record)
+ data[date][user] = record
+ except Exception as e:
+ return HistorySearchOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ data={},
+ )
+ return HistorySearchOut(data=data)
+
+
+@router.post(
+ "/data",
+ summary="从指定文件内获取历史记录数据",
+ response_model=HistoryDataGetOut,
+ status_code=200,
+)
+async def get_history_data(history: HistoryDataGetIn = Body(...)) -> HistoryDataGetOut:
+
+ try:
+ path = Path(history.jsonPath)
+ data = await Config.merge_statistic_info([path])
+ data.pop("index", None)
+ data["log_content"] = path.with_suffix(".log").read_text(encoding="utf-8")
+ data = HistoryData(**data)
+ except Exception as e:
+ return HistoryDataGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ data=HistoryData(**{}),
+ )
+ return HistoryDataGetOut(data=data)
diff --git a/app/api/info.py b/app/api/info.py
new file mode 100644
index 0000000..e7194ac
--- /dev/null
+++ b/app/api/info.py
@@ -0,0 +1,203 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+from fastapi import APIRouter, Body
+
+from app.core import Config
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/info", tags=["信息获取"])
+
+
+@router.post(
+ "/combox/stage",
+ summary="获取关卡号下拉框信息",
+ response_model=ComboBoxOut,
+ status_code=200,
+)
+async def get_stage_combox(
+ stage: GetStageIn = Body(..., description="关卡号类型")
+) -> ComboBoxOut:
+
+ try:
+ raw_data = await Config.get_stage_info(stage.type)
+ data = (
+ [ComboBoxItem(**item) for item in raw_data if isinstance(item, dict)]
+ if raw_data
+ else []
+ )
+ except Exception as e:
+ return ComboBoxOut(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
+ )
+ return ComboBoxOut(data=data)
+
+
+@router.post(
+ "/combox/script",
+ summary="获取脚本下拉框信息",
+ response_model=ComboBoxOut,
+ status_code=200,
+)
+async def get_script_combox() -> ComboBoxOut:
+
+ try:
+ raw_data = await Config.get_script_combox()
+ data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
+ except Exception as e:
+ return ComboBoxOut(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
+ )
+ return ComboBoxOut(data=data)
+
+
+@router.post(
+ "/combox/task",
+ summary="获取可选任务下拉框信息",
+ response_model=ComboBoxOut,
+ status_code=200,
+)
+async def get_task_combox() -> ComboBoxOut:
+
+ try:
+ raw_data = await Config.get_task_combox()
+ data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
+ except Exception as e:
+ return ComboBoxOut(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
+ )
+ return ComboBoxOut(data=data)
+
+
+@router.post(
+ "/combox/plan",
+ summary="获取可选计划下拉框信息",
+ response_model=ComboBoxOut,
+ status_code=200,
+)
+async def get_plan_combox() -> ComboBoxOut:
+
+ try:
+ raw_data = await Config.get_plan_combox()
+ data = [ComboBoxItem(**item) for item in raw_data] if raw_data else []
+ except Exception as e:
+ return ComboBoxOut(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data=[]
+ )
+ return ComboBoxOut(data=data)
+
+
+@router.post(
+ "/notice/get", summary="获取通知信息", response_model=NoticeOut, status_code=200
+)
+async def get_notice_info() -> NoticeOut:
+
+ try:
+ if_need_show, data = await Config.get_notice()
+ except Exception as e:
+ return NoticeOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ if_need_show=False,
+ data={},
+ )
+ return NoticeOut(if_need_show=if_need_show, data=data)
+
+
+@router.post(
+ "/notice/confirm", summary="确认通知", response_model=OutBase, status_code=200
+)
+async def confirm_notice() -> OutBase:
+
+ try:
+ await Config.set("Data", "IfShowNotice", False)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+# @router.post(
+# "/apps_info", summary="获取可下载应用信息", response_model=InfoOut, status_code=200
+# )
+# async def get_apps_info() -> InfoOut:
+
+# try:
+# data = await Config.get_server_info("apps_info")
+# except Exception as e:
+# return InfoOut(
+# code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
+# )
+# return InfoOut(data=data)
+
+
+@router.post(
+ "/startuptask",
+ summary="获取启动时运行的队列ID",
+ response_model=InfoOut,
+ status_code=200,
+)
+async def get_startup_task() -> InfoOut:
+
+ try:
+ data = await Config.get_startup_task()
+ except Exception as e:
+ return InfoOut(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
+ )
+ return InfoOut(data={"queueIdList": data})
+
+
+@router.post(
+ "/webconfig",
+ summary="获取配置分享中心的配置信息",
+ response_model=InfoOut,
+ status_code=200,
+)
+async def get_web_config() -> InfoOut:
+
+ try:
+ data = await Config.get_web_config()
+ except Exception as e:
+ return InfoOut(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}", data={}
+ )
+ return InfoOut(data={"WebConfig": data})
+
+
+@router.post(
+ "/get/overview", summary="信息总览", response_model=InfoOut, status_code=200
+)
+async def get_overview() -> InfoOut:
+ try:
+ stage = await Config.get_stage_info("Info")
+ proxy = await Config.get_proxy_overview()
+ except Exception as e:
+ return InfoOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ data={"Stage": [], "Proxy": []},
+ )
+ return InfoOut(data={"Stage": stage, "Proxy": proxy})
diff --git a/app/api/plan.py b/app/api/plan.py
new file mode 100644
index 0000000..d418a8c
--- /dev/null
+++ b/app/api/plan.py
@@ -0,0 +1,105 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+from fastapi import APIRouter, Body
+
+from app.core import Config
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/plan", tags=["计划管理"])
+
+
+@router.post(
+ "/add", summary="添加计划表", response_model=PlanCreateOut, status_code=200
+)
+async def add_plan(plan: PlanCreateIn = Body(...)) -> PlanCreateOut:
+
+ try:
+ uid, config = await Config.add_plan(plan.type)
+ data = MaaPlanConfig(**(await config.toDict()))
+ except Exception as e:
+ return PlanCreateOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ planId="",
+ data=MaaPlanConfig(**{}),
+ )
+ return PlanCreateOut(planId=str(uid), data=data)
+
+
+@router.post("/get", summary="查询计划表", response_model=PlanGetOut, status_code=200)
+async def get_plan(plan: PlanGetIn = Body(...)) -> PlanGetOut:
+
+ try:
+ index, data = await Config.get_plan(plan.planId)
+ index = [PlanIndexItem(**_) for _ in index]
+ data = {uid: MaaPlanConfig(**cfg) for uid, cfg in data.items()}
+ except Exception as e:
+ return PlanGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ index=[],
+ data={},
+ )
+ return PlanGetOut(index=index, data=data)
+
+
+@router.post(
+ "/update", summary="更新计划表配置信息", response_model=OutBase, status_code=200
+)
+async def update_plan(plan: PlanUpdateIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.update_plan(plan.planId, plan.data.model_dump(exclude_unset=True))
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post("/delete", summary="删除计划表", response_model=OutBase, status_code=200)
+async def delete_plan(plan: PlanDeleteIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.del_plan(plan.planId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/order", summary="重新排序计划表", response_model=OutBase, status_code=200
+)
+async def reorder_plan(plan: PlanReorderIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.reorder_plan(plan.indexList)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
diff --git a/app/api/queue.py b/app/api/queue.py
new file mode 100644
index 0000000..8fb39de
--- /dev/null
+++ b/app/api/queue.py
@@ -0,0 +1,258 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+from fastapi import APIRouter, Body
+
+from app.core import Config
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/queue", tags=["调度队列管理"])
+
+
+@router.post(
+ "/add", summary="添加调度队列", response_model=QueueCreateOut, status_code=200
+)
+async def add_queue() -> QueueCreateOut:
+
+ try:
+ uid, config = await Config.add_queue()
+ data = QueueConfig(**(await config.toDict()))
+ except Exception as e:
+ return QueueCreateOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ queueId="",
+ data=QueueConfig(**{}),
+ )
+ return QueueCreateOut(queueId=str(uid), data=data)
+
+
+@router.post(
+ "/get", summary="查询调度队列配置信息", response_model=QueueGetOut, status_code=200
+)
+async def get_queues(queue: QueueGetIn = Body(...)) -> QueueGetOut:
+
+ try:
+ index, config = await Config.get_queue(queue.queueId)
+ index = [QueueIndexItem(**_) for _ in index]
+ data = {uid: QueueConfig(**cfg) for uid, cfg in config.items()}
+ except Exception as e:
+ return QueueGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ index=[],
+ data={},
+ )
+ return QueueGetOut(index=index, data=data)
+
+
+@router.post(
+ "/update", summary="更新调度队列配置信息", response_model=OutBase, status_code=200
+)
+async def update_queue(queue: QueueUpdateIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.update_queue(
+ queue.queueId, queue.data.model_dump(exclude_unset=True)
+ )
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post("/delete", summary="删除调度队列", response_model=OutBase, status_code=200)
+async def delete_queue(queue: QueueDeleteIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.del_queue(queue.queueId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post("/order", summary="重新排序", response_model=OutBase, status_code=200)
+async def reorder_queue(script: QueueReorderIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.reorder_queue(script.indexList)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/time/get", summary="查询定时项", response_model=TimeSetGetOut, status_code=200
+)
+async def get_time_set(time: TimeSetGetIn = Body(...)) -> TimeSetGetOut:
+
+ try:
+ index, data = await Config.get_time_set(time.queueId, time.timeSetId)
+ index = [TimeSetIndexItem(**_) for _ in index]
+ data = {uid: TimeSet(**cfg) for uid, cfg in data.items()}
+ except Exception as e:
+ return TimeSetGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ index=[],
+ data={},
+ )
+ return TimeSetGetOut(index=index, data=data)
+
+
+@router.post(
+ "/time/add", summary="添加定时项", response_model=TimeSetCreateOut, status_code=200
+)
+async def add_time_set(time: QueueSetInBase = Body(...)) -> TimeSetCreateOut:
+
+ uid, config = await Config.add_time_set(time.queueId)
+ data = TimeSet(**(await config.toDict()))
+ return TimeSetCreateOut(timeSetId=str(uid), data=data)
+
+
+@router.post(
+ "/time/update", summary="更新定时项", response_model=OutBase, status_code=200
+)
+async def update_time_set(time: TimeSetUpdateIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.update_time_set(
+ time.queueId, time.timeSetId, time.data.model_dump(exclude_unset=True)
+ )
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/time/delete", summary="删除定时项", response_model=OutBase, status_code=200
+)
+async def delete_time_set(time: TimeSetDeleteIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.del_time_set(time.queueId, time.timeSetId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/time/order", summary="重新排序定时项", response_model=OutBase, status_code=200
+)
+async def reorder_time_set(time: TimeSetReorderIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.reorder_time_set(time.queueId, time.indexList)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/item/get", summary="查询队列项", response_model=QueueItemGetOut, status_code=200
+)
+async def get_item(item: QueueItemGetIn = Body(...)) -> QueueItemGetOut:
+
+ try:
+ index, data = await Config.get_queue_item(item.queueId, item.queueItemId)
+ index = [QueueItemIndexItem(**_) for _ in index]
+ data = {uid: QueueItem(**cfg) for uid, cfg in data.items()}
+ except Exception as e:
+ return QueueItemGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ index=[],
+ data={},
+ )
+ return QueueItemGetOut(index=index, data=data)
+
+
+@router.post(
+ "/item/add",
+ summary="添加队列项",
+ response_model=QueueItemCreateOut,
+ status_code=200,
+)
+async def add_item(item: QueueSetInBase = Body(...)) -> QueueItemCreateOut:
+
+ uid, config = await Config.add_queue_item(item.queueId)
+ data = QueueItem(**(await config.toDict()))
+ return QueueItemCreateOut(queueItemId=str(uid), data=data)
+
+
+@router.post(
+ "/item/update", summary="更新队列项", response_model=OutBase, status_code=200
+)
+async def update_item(item: QueueItemUpdateIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.update_queue_item(
+ item.queueId, item.queueItemId, item.data.model_dump(exclude_unset=True)
+ )
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/item/delete", summary="删除队列项", response_model=OutBase, status_code=200
+)
+async def delete_item(item: QueueItemDeleteIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.del_queue_item(item.queueId, item.queueItemId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/item/order", summary="重新排序队列项", response_model=OutBase, status_code=200
+)
+async def reorder_item(item: QueueItemReorderIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.reorder_queue_item(item.queueId, item.indexList)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
diff --git a/app/api/scripts.py b/app/api/scripts.py
new file mode 100644
index 0000000..82719e3
--- /dev/null
+++ b/app/api/scripts.py
@@ -0,0 +1,282 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+import uuid
+from fastapi import APIRouter, Body
+
+from app.core import Config
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/scripts", tags=["脚本管理"])
+
+
+SCRIPT_BOOK = {"MaaConfig": MaaConfig, "GeneralConfig": GeneralConfig}
+USER_BOOK = {"MaaConfig": MaaUserConfig, "GeneralConfig": GeneralUserConfig}
+
+
+@router.post(
+ "/add", summary="添加脚本", response_model=ScriptCreateOut, status_code=200
+)
+async def add_script(script: ScriptCreateIn = Body(...)) -> ScriptCreateOut:
+
+ try:
+ uid, config = await Config.add_script(script.type)
+ data = SCRIPT_BOOK[type(config).__name__](**(await config.toDict()))
+ except Exception as e:
+ return ScriptCreateOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ scriptId="",
+ data=GeneralConfig(**{}),
+ )
+ return ScriptCreateOut(scriptId=str(uid), data=data)
+
+
+@router.post(
+ "/get", summary="查询脚本配置信息", response_model=ScriptGetOut, status_code=200
+)
+async def get_scripts(script: ScriptGetIn = Body(...)) -> ScriptGetOut:
+
+ try:
+ index, data = await Config.get_script(script.scriptId)
+ index = [ScriptIndexItem(**_) for _ in index]
+ data = {
+ uid: SCRIPT_BOOK[next((_.type for _ in index if _.uid == uid), "General")](
+ **cfg
+ )
+ for uid, cfg in data.items()
+ }
+ except Exception as e:
+ return ScriptGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ index=[],
+ data={},
+ )
+ return ScriptGetOut(index=index, data=data)
+
+
+@router.post(
+ "/update", summary="更新脚本配置信息", response_model=OutBase, status_code=200
+)
+async def update_script(script: ScriptUpdateIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.update_script(
+ script.scriptId, script.data.model_dump(exclude_unset=True)
+ )
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post("/delete", summary="删除脚本", response_model=OutBase, status_code=200)
+async def delete_script(script: ScriptDeleteIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.del_script(script.scriptId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post("/order", summary="重新排序脚本", response_model=OutBase, status_code=200)
+async def reorder_script(script: ScriptReorderIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.reorder_script(script.indexList)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/import/file", summary="从文件加载脚本", response_model=OutBase, status_code=200
+)
+async def import_script_from_file(script: ScriptFileIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.import_script_from_file(script.scriptId, script.jsonFile)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/export/file", summary="导出脚本到文件", response_model=OutBase, status_code=200
+)
+async def export_script_to_file(script: ScriptFileIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.export_script_to_file(script.scriptId, script.jsonFile)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/import/web", summary="从网络加载脚本", response_model=OutBase, status_code=200
+)
+async def import_script_from_web(script: ScriptUrlIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.import_script_from_web(script.scriptId, script.url)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/Upload/web", summary="上传脚本配置到网络", response_model=OutBase, status_code=200
+)
+async def upload_script_to_web(script: ScriptUploadIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.upload_script_to_web(
+ script.scriptId, script.config_name, script.author, script.description
+ )
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/user/get", summary="查询用户", response_model=UserGetOut, status_code=200
+)
+async def get_user(user: UserGetIn = Body(...)) -> UserGetOut:
+
+ try:
+ index, data = await Config.get_user(user.scriptId, user.userId)
+ index = [UserIndexItem(**_) for _ in index]
+ data = {
+ uid: USER_BOOK[
+ type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__
+ ](**cfg)
+ for uid, cfg in data.items()
+ }
+ except Exception as e:
+ return UserGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ index=[],
+ data={},
+ )
+ return UserGetOut(index=index, data=data)
+
+
+@router.post(
+ "/user/add", summary="添加用户", response_model=UserCreateOut, status_code=200
+)
+async def add_user(user: UserInBase = Body(...)) -> UserCreateOut:
+
+ try:
+ uid, config = await Config.add_user(user.scriptId)
+ data = USER_BOOK[type(Config.ScriptConfig[uuid.UUID(user.scriptId)]).__name__](
+ **(await config.toDict())
+ )
+ except Exception as e:
+ return UserCreateOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ userId="",
+ data=GeneralUserConfig(**{}),
+ )
+ return UserCreateOut(userId=str(uid), data=data)
+
+
+@router.post(
+ "/user/update", summary="更新用户配置信息", response_model=OutBase, status_code=200
+)
+async def update_user(user: UserUpdateIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.update_user(
+ user.scriptId, user.userId, user.data.model_dump(exclude_unset=True)
+ )
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/user/delete", summary="删除用户", response_model=OutBase, status_code=200
+)
+async def delete_user(user: UserDeleteIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.del_user(user.scriptId, user.userId)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/user/order", summary="重新排序用户", response_model=OutBase, status_code=200
+)
+async def reorder_user(user: UserReorderIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.reorder_user(user.scriptId, user.indexList)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
+
+
+@router.post(
+ "/user/infrastructure",
+ summary="导入基建配置文件",
+ response_model=OutBase,
+ status_code=200,
+)
+async def import_infrastructure(user: UserSetIn = Body(...)) -> OutBase:
+
+ try:
+ await Config.set_infrastructure(user.scriptId, user.userId, user.jsonFile)
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
diff --git a/app/api/setting.py b/app/api/setting.py
new file mode 100644
index 0000000..f6f664b
--- /dev/null
+++ b/app/api/setting.py
@@ -0,0 +1,82 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+import os
+from pathlib import Path
+import shutil
+from fastapi import APIRouter, Body
+
+from app.core import Config
+from app.services import System
+from app.models.schema import *
+
+router = APIRouter(prefix="/api/setting", tags=["全局设置"])
+
+
+@router.post("/get", summary="查询配置", response_model=SettingGetOut, status_code=200)
+async def get_scripts() -> SettingGetOut:
+ """查询配置"""
+
+ try:
+ data = await Config.get_setting()
+ except Exception as e:
+ return SettingGetOut(
+ code=500,
+ status="error",
+ message=f"{type(e).__name__}: {str(e)}",
+ data=GlobalConfig(**{}),
+ )
+ return SettingGetOut(data=GlobalConfig(**data))
+
+
+@router.post("/update", summary="更新配置", response_model=OutBase, status_code=200)
+async def update_script(script: SettingUpdateIn = Body(...)) -> OutBase:
+ """更新配置"""
+
+ try:
+ data = script.data.model_dump(exclude_unset=True)
+ await Config.update_setting(data)
+
+ if data.get("Start", {}).get("IfSelfStart", None) is not None:
+ await System.set_SelfStart()
+ if data.get("Function", None) is not None:
+ function = data["Function"]
+ if function.get("IfAllowSleep", None) is not None:
+ await System.set_Sleep()
+ if function.get("IfSkipMumuSplashAds", None) is not None:
+ MuMu_splash_ads_path = (
+ Path(os.getenv("APPDATA") or "")
+ / "Netease/MuMuPlayer-12.0/data/startupImage"
+ )
+ if Config.get("Function", "IfSkipMumuSplashAds"):
+ if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_dir():
+ shutil.rmtree(MuMu_splash_ads_path)
+ MuMu_splash_ads_path.touch()
+ else:
+ if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
+ MuMu_splash_ads_path.unlink()
+
+ except Exception as e:
+ return OutBase(
+ code=500, status="error", message=f"{type(e).__name__}: {str(e)}"
+ )
+ return OutBase()
diff --git a/app/core/__init__.py b/app/core/__init__.py
index 9240bc7..f9b827c 100644
--- a/app/core/__init__.py
+++ b/app/core/__init__.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,46 +19,22 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA核心组件包
-v4.4
-作者:DLmaster_361
-"""
-
-__version__ = "4.2.0"
+__version__ = "5.0.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 .broadcast import Broadcast
+from .config import Config, MaaConfig, GeneralConfig, MaaUserConfig, GeneralUserConfig
from .timer import MainTimer
+from .task_manager import TaskManager
__all__ = [
+ "Broadcast",
"Config",
- "QueueConfig",
"MaaConfig",
- "MaaUserConfig",
- "MaaPlanConfig",
"GeneralConfig",
- "GeneralSubConfig",
- "logger",
- "MainInfoBar",
- "Network",
- "SoundPlayer",
- "Task",
- "TaskManager",
"MainTimer",
+ "TaskManager",
+ "MaaUserConfig",
+ "GeneralUserConfig",
]
diff --git a/app/core/broadcast.py b/app/core/broadcast.py
new file mode 100644
index 0000000..d145deb
--- /dev/null
+++ b/app/core/broadcast.py
@@ -0,0 +1,51 @@
+# 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
+
+
+import asyncio
+from copy import deepcopy
+from typing import Set
+
+from app.utils import get_logger
+
+
+logger = get_logger("消息广播")
+
+
+class _Broadcast:
+
+ def __init__(self):
+ self.__subscribers: Set[asyncio.Queue] = set()
+
+ async def subscribe(self, queue: asyncio.Queue):
+ """订阅者注册"""
+ self.__subscribers.add(queue)
+
+ async def unsubscribe(self, queue: asyncio.Queue):
+ """取消订阅"""
+ self.__subscribers.remove(queue)
+
+ async def put(self, item):
+ """向所有订阅者广播消息"""
+ for subscriber in self.__subscribers:
+ await subscriber.put(deepcopy(item))
+
+
+Broadcast = _Broadcast()
diff --git a/app/core/config.py b/app/core/config.py
index d734263..b52e3bf 100644
--- a/app/core/config.py
+++ b/app/core/config.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,269 +19,153 @@
# 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 shutil
+import asyncio
+import uvicorn
+import sqlite3
import calendar
-from datetime import datetime, timedelta, date
-from collections import defaultdict
+import requests
+import truststore
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 fastapi import WebSocket
+from urllib.parse import quote
+from collections import defaultdict
+from datetime import datetime, timedelta, date, timezone
+from typing import Literal, Optional
-from .logger import logger
-from .network import Network
+from app.models.ConfigBase import *
+from app.utils.constants import *
+from app.utils import get_logger
+
+logger = get_logger("配置管理")
-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):
+class GlobalConfig(ConfigBase):
"""全局配置"""
+ Function_HistoryRetentionTime = ConfigItem(
+ "Function",
+ "HistoryRetentionTime",
+ 0,
+ OptionsValidator([7, 15, 30, 60, 90, 180, 365, 0]),
+ )
+ Function_IfAllowSleep = ConfigItem(
+ "Function", "IfAllowSleep", False, BoolValidator()
+ )
+ Function_IfSilence = ConfigItem("Function", "IfSilence", False, BoolValidator())
+ Function_BossKey = ConfigItem("Function", "BossKey", "")
+ Function_IfAgreeBilibili = ConfigItem(
+ "Function", "IfAgreeBilibili", False, BoolValidator()
+ )
+ Function_IfSkipMumuSplashAds = ConfigItem(
+ "Function", "IfSkipMumuSplashAds", False, BoolValidator()
+ )
+
+ Voice_Enabled = ConfigItem("Voice", "Enabled", False, BoolValidator())
+ Voice_Type = ConfigItem(
+ "Voice", "Type", "simple", OptionsValidator(["simple", "noisy"])
+ )
+
+ Start_IfSelfStart = ConfigItem("Start", "IfSelfStart", False, BoolValidator())
+ Start_IfMinimizeDirectly = ConfigItem(
+ "Start", "IfMinimizeDirectly", False, BoolValidator()
+ )
+
+ UI_IfShowTray = ConfigItem("UI", "IfShowTray", False, BoolValidator())
+ UI_IfToTray = ConfigItem("UI", "IfToTray", False, BoolValidator())
+
+ Notify_SendTaskResultTime = ConfigItem(
+ "Notify",
+ "SendTaskResultTime",
+ "不推送",
+ OptionsValidator(["不推送", "任何时刻", "仅失败时"]),
+ )
+ Notify_IfSendStatistic = ConfigItem(
+ "Notify", "IfSendStatistic", False, BoolValidator()
+ )
+ Notify_IfSendSixStar = ConfigItem("Notify", "IfSendSixStar", False, BoolValidator())
+ Notify_IfPushPlyer = ConfigItem("Notify", "IfPushPlyer", False, BoolValidator())
+ Notify_IfSendMail = ConfigItem("Notify", "IfSendMail", False, BoolValidator())
+ Notify_SMTPServerAddress = ConfigItem("Notify", "SMTPServerAddress", "")
+ Notify_AuthorizationCode = ConfigItem(
+ "Notify", "AuthorizationCode", "", EncryptValidator()
+ )
+ Notify_FromAddress = ConfigItem("Notify", "FromAddress", "")
+ Notify_ToAddress = ConfigItem("Notify", "ToAddress", "")
+ Notify_IfServerChan = ConfigItem("Notify", "IfServerChan", False, BoolValidator())
+ Notify_ServerChanKey = ConfigItem("Notify", "ServerChanKey", "")
+ Notify_IfCompanyWebHookBot = ConfigItem(
+ "Notify", "IfCompanyWebHookBot", False, BoolValidator()
+ )
+ Notify_CompanyWebHookBotUrl = ConfigItem("Notify", "CompanyWebHookBotUrl", "")
+
+ Update_IfAutoUpdate = ConfigItem("Update", "IfAutoUpdate", False, BoolValidator())
+ Update_UpdateType = ConfigItem(
+ "Update", "UpdateType", "stable", OptionsValidator(["stable", "beta"])
+ )
+ Update_Source = ConfigItem(
+ "Update",
+ "Source",
+ "GitHub",
+ OptionsValidator(["GitHub", "MirrorChyan", "AutoSite"]),
+ )
+ Update_ProxyAddress = ConfigItem("Update", "ProxyAddress", "")
+ Update_MirrorChyanCDK = ConfigItem(
+ "Update", "MirrorChyanCDK", "", EncryptValidator()
+ )
+
+ Data_LastStageUpdated = ConfigItem(
+ "Data", "LastStageUpdated", "2000-01-01 00:00:00"
+ )
+ Data_StageTimeStamp = ConfigItem("Data", "StageTimeStamp", "2000-01-01 00:00:00")
+ Data_Stage = ConfigItem("Data", "Stage", "{ }")
+ Data_LastNoticeUpdated = ConfigItem(
+ "Data", "LastNoticeUpdated", "2000-01-01 00:00:00"
+ )
+ Data_IfShowNotice = ConfigItem("Data", "IfShowNotice", True, BoolValidator())
+ Data_Notice = ConfigItem("Data", "Notice", "{ }")
+ Data_LastWebConfigUpdated = ConfigItem(
+ "Data", "LastWebConfigUpdated", "2000-01-01 00:00:00"
+ )
+ Data_WebConfig = ConfigItem("Data", "WebConfig", "{ }")
+
+
+class QueueItem(ConfigBase):
+ """队列项配置"""
+
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", "")
+ self.Info_ScriptId = ConfigItem("Info", "ScriptId", None, UidValidator())
-class QueueConfig(LQConfig):
+class TimeSet(ConfigBase):
+ """时间设置配置"""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.Info_Enabled = ConfigItem("Info", "Enabled", False, BoolValidator())
+ self.Info_Time = ConfigItem("Info", "Time", "00:00")
+
+
+class QueueConfig(ConfigBase):
"""队列配置"""
def __init__(self) -> None:
super().__init__()
- self.QueueSet_Name = ConfigItem("QueueSet", "Name", "")
- self.QueueSet_TimeEnabled = ConfigItem(
- "QueueSet", "TimeEnabled", False, BoolValidator()
+ self.Info_Name = ConfigItem("Info", "Name", "新队列")
+ self.Info_TimeEnabled = ConfigItem(
+ "Info", "TimeEnabled", False, BoolValidator()
)
- self.QueueSet_StartUpEnabled = ConfigItem(
- "QueueSet", "StartUpEnabled", False, BoolValidator()
+ self.Info_StartUpEnabled = ConfigItem(
+ "Info", "StartUpEnabled", False, BoolValidator()
)
- self.QueueSet_AfterAccomplish = OptionsConfigItem(
- "QueueSet",
+ self.Info_AfterAccomplish = ConfigItem(
+ "Info",
"AfterAccomplish",
"NoAction",
OptionsValidator(
@@ -295,78 +180,11 @@ class QueueConfig(LQConfig):
),
)
- 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", "暂无历史运行记录"
- )
+ self.TimeSet = MultipleConfig([TimeSet])
+ self.QueueItem = MultipleConfig([QueueItem])
-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):
+class MaaUserConfig(ConfigBase):
"""MAA用户配置"""
def __init__(self) -> None:
@@ -374,11 +192,11 @@ class MaaUserConfig(LQConfig):
self.Info_Name = ConfigItem("Info", "Name", "新用户")
self.Info_Id = ConfigItem("Info", "Id", "")
- self.Info_Mode = OptionsConfigItem(
+ self.Info_Mode = ConfigItem(
"Info", "Mode", "简洁", OptionsValidator(["简洁", "详细"])
)
- self.Info_StageMode = ConfigItem("Info", "StageMode", "固定")
- self.Info_Server = OptionsConfigItem(
+ self.Info_StageMode = ConfigItem("Info", "StageMode", "Fixed")
+ self.Info_Server = ConfigItem(
"Info",
"Server",
"Official",
@@ -388,9 +206,9 @@ class MaaUserConfig(LQConfig):
)
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
self.Info_RemainedDay = ConfigItem(
- "Info", "RemainedDay", -1, RangeValidator(-1, 1024)
+ "Info", "RemainedDay", -1, RangeValidator(-1, 9999)
)
- self.Info_Annihilation = OptionsConfigItem(
+ self.Info_Annihilation = ConfigItem(
"Info",
"Annihilation",
"Annihilation",
@@ -405,18 +223,19 @@ class MaaUserConfig(LQConfig):
),
)
self.Info_Routine = ConfigItem("Info", "Routine", True, BoolValidator())
- self.Info_InfrastMode = OptionsConfigItem(
+ self.Info_InfrastMode = ConfigItem(
"Info",
"InfrastMode",
"Normal",
OptionsValidator(["Normal", "Rotation", "Custom"]),
)
- self.Info_Password = ConfigItem("Info", "Password", "")
+ self.Info_InfrastPath = ConfigItem("Info", "InfrastPath", ".", FileValidator())
+ self.Info_Password = ConfigItem("Info", "Password", "", EncryptValidator())
self.Info_Notes = ConfigItem("Info", "Notes", "无")
self.Info_MedicineNumb = ConfigItem(
- "Info", "MedicineNumb", 0, RangeValidator(0, 1024)
+ "Info", "MedicineNumb", 0, RangeValidator(0, 9999)
)
- self.Info_SeriesNumb = OptionsConfigItem(
+ self.Info_SeriesNumb = ConfigItem(
"Info",
"SeriesNumb",
"0",
@@ -428,7 +247,9 @@ class MaaUserConfig(LQConfig):
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.Info_SklandToken = ConfigItem(
+ "Info", "SklandToken", "", EncryptValidator()
+ )
self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01")
self.Data_LastAnnihilationDate = ConfigItem(
@@ -436,7 +257,7 @@ class MaaUserConfig(LQConfig):
)
self.Data_LastSklandDate = ConfigItem("Data", "LastSklandDate", "2000-01-01")
self.Data_ProxyTimes = ConfigItem(
- "Data", "ProxyTimes", 0, RangeValidator(0, 1024)
+ "Data", "ProxyTimes", 0, RangeValidator(0, 9999)
)
self.Data_IfPassCheck = ConfigItem("Data", "IfPassCheck", True, BoolValidator())
self.Data_CustomInfrastPlanIndex = ConfigItem(
@@ -473,8 +294,6 @@ class MaaUserConfig(LQConfig):
"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()
)
@@ -485,37 +304,77 @@ class MaaUserConfig(LQConfig):
def get_plan_info(self) -> Dict[str, Union[str, int]]:
"""获取当前的计划下信息"""
- if self.get(self.Info_StageMode) == "固定":
+ if self.get("Info", "StageMode") == "Fixed":
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")),
+ "MedicineNumb": self.get("Info", "MedicineNumb"),
+ "SeriesNumb": self.get("Info", "SeriesNumb"),
+ "Stage": self.get("Info", "Stage"),
+ "Stage_1": self.get("Info", "Stage_1"),
+ "Stage_2": self.get("Info", "Stage_2"),
+ "Stage_3": self.get("Info", "Stage_3"),
+ "Stage_Remain": self.get("Info", "Stage_Remain"),
}
+ else:
+ plan = Config.PlanConfig[uuid.UUID(self.get("Info", "StageMode"))]
+ if isinstance(plan, MaaPlanConfig):
+ return {
+ "MedicineNumb": plan.get_current_info("MedicineNumb").getValue(),
+ "SeriesNumb": plan.get_current_info("SeriesNumb").getValue(),
+ "Stage": plan.get_current_info("Stage").getValue(),
+ "Stage_1": plan.get_current_info("Stage_1").getValue(),
+ "Stage_2": plan.get_current_info("Stage_2").getValue(),
+ "Stage_3": plan.get_current_info("Stage_3").getValue(),
+ "Stage_Remain": plan.get_current_info("Stage_Remain").getValue(),
+ }
+ else:
+ raise ValueError("不存在的计划表配置")
-class MaaPlanConfig(LQConfig):
+class MaaConfig(ConfigBase):
+ """MAA配置"""
+
+ def __init__(self) -> None:
+ super().__init__()
+
+ self.Info_Name = ConfigItem("Info", "Name", "新 MAA 脚本")
+ self.Info_Path = ConfigItem("Info", "Path", ".", FolderValidator())
+
+ self.Run_TaskTransitionMethod = ConfigItem(
+ "Run",
+ "TaskTransitionMethod",
+ "ExitEmulator",
+ OptionsValidator(["NoAction", "ExitGame", "ExitEmulator"]),
+ )
+ self.Run_ProxyTimesLimit = ConfigItem(
+ "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
+ )
+ self.Run_ADBSearchRange = ConfigItem(
+ "Run", "ADBSearchRange", 0, RangeValidator(0, 3)
+ )
+ self.Run_RunTimesLimit = ConfigItem(
+ "Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
+ )
+ self.Run_AnnihilationTimeLimit = ConfigItem(
+ "Run", "AnnihilationTimeLimit", 40, RangeValidator(1, 9999)
+ )
+ self.Run_RoutineTimeLimit = ConfigItem(
+ "Run", "RoutineTimeLimit", 10, RangeValidator(1, 9999)
+ )
+ self.Run_AnnihilationWeeklyLimit = ConfigItem(
+ "Run", "AnnihilationWeeklyLimit", True, BoolValidator()
+ )
+
+ self.UserData = MultipleConfig([MaaUserConfig])
+
+
+class MaaPlanConfig(ConfigBase):
"""MAA计划表配置"""
def __init__(self) -> None:
super().__init__()
- self.Info_Name = ConfigItem("Info", "Name", "")
- self.Info_Mode = OptionsConfigItem(
+ self.Info_Name = ConfigItem("Info", "Name", "新 MAA 计划表")
+ self.Info_Mode = ConfigItem(
"Info", "Mode", "ALL", OptionsValidator(["ALL", "Weekly"])
)
@@ -534,9 +393,9 @@ class MaaPlanConfig(LQConfig):
self.config_item_dict[group] = {}
self.config_item_dict[group]["MedicineNumb"] = ConfigItem(
- group, "MedicineNumb", 0, RangeValidator(0, 1024)
+ group, "MedicineNumb", 0, RangeValidator(0, 9999)
)
- self.config_item_dict[group]["SeriesNumb"] = OptionsConfigItem(
+ self.config_item_dict[group]["SeriesNumb"] = ConfigItem(
group,
"SeriesNumb",
"0",
@@ -564,11 +423,11 @@ class MaaPlanConfig(LQConfig):
def get_current_info(self, name: str) -> ConfigItem:
"""获取当前的计划表配置项"""
- if self.get(self.Info_Mode) == "ALL":
+ if self.get("Info", "Mode") == "ALL":
return self.config_item_dict["ALL"][name]
- elif self.get(self.Info_Mode) == "Weekly":
+ elif self.get("Info", "Mode") == "Weekly":
dt = datetime.now()
if dt.time() < datetime.min.time().replace(hour=4):
@@ -580,86 +439,20 @@ class MaaPlanConfig(LQConfig):
else:
return self.config_item_dict["ALL"][name]
+ else:
+ raise ValueError("非法的计划表模式")
-class GeneralConfig(LQConfig):
- """通用配置"""
+
+class GeneralUserConfig(ConfigBase):
+ """通用脚本用户配置"""
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_Name = ConfigItem("Info", "Name", "新用户")
self.Info_Status = ConfigItem("Info", "Status", True, BoolValidator())
self.Info_RemainedDay = ConfigItem(
- "Info", "RemainedDay", -1, RangeValidator(-1, 1024)
+ "Info", "RemainedDay", -1, RangeValidator(-1, 9999)
)
self.Info_IfScriptBeforeTask = ConfigItem(
"Info", "IfScriptBeforeTask", False, BoolValidator()
@@ -677,7 +470,7 @@ class GeneralSubConfig(LQConfig):
self.Data_LastProxyDate = ConfigItem("Data", "LastProxyDate", "2000-01-01")
self.Data_ProxyTimes = ConfigItem(
- "Data", "ProxyTimes", 0, RangeValidator(0, 1024)
+ "Data", "ProxyTimes", 0, RangeValidator(0, 9999)
)
self.Notify_Enabled = ConfigItem("Notify", "Enabled", False, BoolValidator())
@@ -692,8 +485,6 @@ class GeneralSubConfig(LQConfig):
"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()
)
@@ -702,121 +493,129 @@ class GeneralSubConfig(LQConfig):
)
-class AppConfig(GlobalConfig):
-
- VERSION = "4.4.1.0"
-
- stage_refreshed = Signal()
- PASSWORD_refreshed = Signal()
- sub_info_changed = Signal()
- power_sign_changed = Signal()
+class GeneralConfig(ConfigBase):
+ """通用配置"""
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.Info_Name = ConfigItem("Info", "Name", "新通用脚本")
+ self.Info_RootPath = ConfigItem("Info", "RootPath", ".", FileValidator())
- 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",
+ self.Script_ScriptPath = ConfigItem(
+ "Script", "ScriptPath", ".", FileValidator()
)
- parser.add_argument(
- "--mode",
- choices=["gui", "cli"],
- default="gui",
- help="使用UI界面或命令行模式运行程序",
+ self.Script_Arguments = ConfigItem("Script", "Arguments", "")
+ self.Script_IfTrackProcess = ConfigItem(
+ "Script", "IfTrackProcess", False, BoolValidator()
)
- parser.add_argument(
- "--config",
- nargs="+",
- choices=list(self.script_dict.keys()) + list(self.queue_dict.keys()),
- help="指定需要运行哪些配置项",
+ self.Script_ConfigPath = ConfigItem(
+ "Script", "ConfigPath", ".", FileValidator()
)
- self.args = parser.parse_args()
+ self.Script_ConfigPathMode = ConfigItem(
+ "Script", "ConfigPathMode", "File", OptionsValidator(["File", "Folder"])
+ )
+ self.Script_UpdateConfigMode = ConfigItem(
+ "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, 9999)
+ )
+ self.Script_LogTimeEnd = ConfigItem(
+ "Script", "LogTimeEnd", 1, RangeValidator(1, 9999)
+ )
+ self.Script_LogTimeFormat = ConfigItem(
+ "Script", "LogTimeFormat", "%Y-%m-%d %H:%M:%S"
+ )
+ self.Script_SuccessLog = ConfigItem("Script", "SuccessLog", "")
+ self.Script_ErrorLog = ConfigItem("Script", "ErrorLog", "")
- logger.info(
- f"运行模式: {'图形化界面' if self.args.mode == 'gui' else '命令行界面'},配置项: {self.args.config if self.args.config else '启动时运行的调度队列'}",
- module="配置管理",
+ self.Game_Enabled = ConfigItem("Game", "Enabled", False, BoolValidator())
+ self.Game_Type = ConfigItem(
+ "Game", "Type", "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, 9999))
+ self.Game_IfForceClose = ConfigItem(
+ "Game", "IfForceClose", False, BoolValidator()
)
- def initialize(self) -> None:
- """初始化程序配置管理模块"""
+ self.Run_ProxyTimesLimit = ConfigItem(
+ "Run", "ProxyTimesLimit", 0, RangeValidator(0, 9999)
+ )
+ self.Run_RunTimesLimit = ConfigItem(
+ "Run", "RunTimesLimit", 3, RangeValidator(1, 9999)
+ )
+ self.Run_RunTimeLimit = ConfigItem(
+ "Run", "RunTimeLimit", 10, RangeValidator(1, 9999)
+ )
+ self.UserData = MultipleConfig([GeneralUserConfig])
+
+
+CLASS_BOOK = {"MAA": MaaConfig, "MaaPlan": MaaPlanConfig, "General": GeneralConfig}
+TYPE_BOOK = {"MaaConfig": "MAA", "GeneralConfig": "通用"}
+
+
+class AppConfig(GlobalConfig):
+
+ VERSION = "5.0.0.1"
+
+ def __init__(self) -> None:
+ super().__init__(if_save_multi_config=False)
+
+ logger.info("")
+ logger.info("===================================")
+ logger.info("AUTO_MAA 后端应用程序")
+ logger.info(f"版本号: v{self.VERSION}")
+ logger.info(f"工作目录: {Path.cwd()}")
+ logger.info("===================================")
+
+ self.log_path = Path.cwd() / "debug/app.log"
+ self.database_path = Path.cwd() / "data/data.db"
+ self.config_path = Path.cwd() / "config"
+ self.history_path = Path.cwd() / "history"
# 检查目录
- (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.log_path.parent.mkdir(parents=True, exist_ok=True)
+ self.database_path.parent.mkdir(parents=True, exist_ok=True)
+ self.config_path.mkdir(parents=True, exist_ok=True)
+ self.history_path.mkdir(parents=True, exist_ok=True)
- self.load(self.config_path, self)
- self.save()
+ self.server: Optional[uvicorn.Server] = None
+ self.websocket: Optional[WebSocket] = None
+ self.silence_dict: Dict[Path, datetime] = {}
+ self.if_ignore_silence: List[uuid.UUID] = []
+ self.temp_task: List[asyncio.Task] = []
- self.init_logger()
- self.check_data()
- logger.info("程序初始化完成", module="配置管理")
+ self.ScriptConfig = MultipleConfig([MaaConfig, GeneralConfig])
+ self.PlanConfig = MultipleConfig([MaaPlanConfig])
+ self.QueueConfig = MultipleConfig([QueueConfig])
- def init_logger(self) -> None:
- """初始化日志记录器"""
+ truststore.inject_into_ssl()
- 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,
- )
+ async def init_config(self) -> None:
+ """初始化配置管理"""
- 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="配置管理")
+ await self.check_data()
- def check_data(self) -> None:
+ await self.connect(self.config_path / "Config.json")
+ await self.ScriptConfig.connect(self.config_path / "ScriptConfig.json")
+ await self.PlanConfig.connect(self.config_path / "PlanConfig.json")
+ await self.QueueConfig.connect(self.config_path / "QueueConfig.json")
+
+ from .task_manager import TaskManager
+
+ self.task_dict = TaskManager.task_dict
+
+ logger.info("程序初始化完成")
+
+ async def check_data(self) -> None:
"""检查用户数据文件并处理数据文件版本更新"""
# 生成主数据库
@@ -824,7 +623,7 @@ class AppConfig(GlobalConfig):
db = sqlite3.connect(self.database_path)
cur = db.cursor()
cur.execute("CREATE TABLE version(v text)")
- cur.execute("INSERT INTO version VALUES(?)", ("v1.8",))
+ cur.execute("INSERT INTO version VALUES(?)", ("v1.9",))
db.commit()
cur.close()
db.close()
@@ -835,244 +634,20 @@ class AppConfig(GlobalConfig):
cur.execute("SELECT * FROM version WHERE True")
version = cur.fetchall()
- if version[0][0] != "v1.8":
- logger.info("数据文件版本更新开始", module="配置管理")
+ if version[0][0] != "v1.9":
+ logger.info(
+ "数据文件版本更新开始",
+ )
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="配置管理")
+ logger.info(
+ "数据文件版本更新: v1.7-->v1.8",
+ )
if_streaming = True
- if (self.app_path / "config/QueueConfig").exists():
- for QueueConfig in (self.app_path / "config/QueueConfig").glob(
+ if (Path.cwd() / "config/QueueConfig").exists():
+ for QueueConfig in (Path.cwd() / "config/QueueConfig").glob(
"*.json"
):
with QueueConfig.open(encoding="utf-8") as f:
@@ -1099,91 +674,819 @@ class AppConfig(GlobalConfig):
cur.execute("DELETE FROM version WHERE v = ?", ("v1.7",))
cur.execute("INSERT INTO version VALUES(?)", ("v1.8",))
db.commit()
+ # v1.8-->v1.9
+ if version[0][0] == "v1.8" or if_streaming:
+ logger.info(
+ "数据文件版本更新: v1.8-->v1.9",
+ )
+ if_streaming = True
+
+ await self.ScriptConfig.connect(self.config_path / "ScriptConfig.json")
+ await self.PlanConfig.connect(self.config_path / "PlanConfig.json")
+ await self.QueueConfig.connect(self.config_path / "QueueConfig.json")
+
+ if (Path.cwd() / "config/config.json").exists():
+ (Path.cwd() / "config/config.json").rename(
+ Path.cwd() / "config/Config.json"
+ )
+ await self.connect(self.config_path / "Config.json")
+
+ plan_dict = {"固定": "Fixed"}
+
+ if (Path.cwd() / "config/MaaPlanConfig").exists():
+ for MaaPlanConfig in (
+ Path.cwd() / "config/MaaPlanConfig"
+ ).iterdir():
+ if (
+ MaaPlanConfig.is_dir()
+ and (MaaPlanConfig / "config.json").exists()
+ ):
+
+ maa_plan_config = json.loads(
+ (MaaPlanConfig / "config.json").read_text(
+ encoding="utf-8"
+ )
+ )
+ uid, pc = await self.add_plan("MaaPlan")
+ plan_dict[MaaPlanConfig.name] = str(uid)
+
+ await pc.load(maa_plan_config)
+
+ await self.PlanConfig.save()
+
+ script_dict: Dict[str, Optional[str]] = {"禁用": None}
+
+ if (Path.cwd() / "config/MaaConfig").exists():
+
+ for MaaConfig in (Path.cwd() / "config/MaaConfig").iterdir():
+ if MaaConfig.is_dir():
+
+ maa_config = json.loads(
+ (MaaConfig / "config.json").read_text(encoding="utf-8")
+ )
+ maa_config["Info"] = maa_config["MaaSet"]
+ maa_config["Run"] = maa_config["RunSet"]
+
+ uid, sc = await self.add_script("MAA")
+ script_dict[MaaConfig.name] = str(uid)
+ await sc.load(maa_config)
+
+ if (MaaConfig / "Default/gui.json").exists():
+ (Path.cwd() / f"data/{uid}/Default/ConfigFile").mkdir(
+ parents=True, exist_ok=True
+ )
+ shutil.copy(
+ MaaConfig / "Default/gui.json",
+ Path.cwd()
+ / f"data/{uid}/Default/ConfigFile/gui.json",
+ )
+
+ for user in (MaaConfig / "UserData").iterdir():
+ if user.is_dir() and (user / "config.json").exists():
+ user_config = json.loads(
+ (user / "config.json").read_text(
+ encoding="utf-8"
+ )
+ )
+
+ user_config["Info"]["StageMode"] = plan_dict.get(
+ user_config["Info"]["StageMode"], "Fixed"
+ )
+ user_config["Info"]["Password"] = ""
+
+ user_uid, uc = await self.add_user(str(uid))
+ await uc.load(user_config)
+
+ if (user / "Routine/gui.json").exists():
+ (
+ Path.cwd()
+ / f"data/{uid}/{user_uid}/ConfigFile"
+ ).mkdir(parents=True, exist_ok=True)
+ shutil.copy(
+ user / "Routine/gui.json",
+ Path.cwd()
+ / f"data/{uid}/{user_uid}/ConfigFile/gui.json",
+ )
+ if (
+ user / "Infrastructure/infrastructure.json"
+ ).exists():
+ (
+ Path.cwd()
+ / f"data/{uid}/{user_uid}/Infrastructure"
+ ).mkdir(parents=True, exist_ok=True)
+ shutil.copy(
+ user / "Infrastructure/infrastructure.json",
+ Path.cwd()
+ / f"data/{uid}/{user_uid}/Infrastructure/infrastructure.json",
+ )
+
+ if (Path.cwd() / "config/GeneralConfig").exists():
+
+ for GeneralConfig in (
+ Path.cwd() / "config/GeneralConfig"
+ ).iterdir():
+ if GeneralConfig.is_dir():
+
+ general_config = json.loads(
+ (GeneralConfig / "config.json").read_text(
+ encoding="utf-8"
+ )
+ )
+ general_config["Info"] = {
+ "Name": general_config["Script"]["Name"],
+ "RootPath": general_config["Script"]["RootPath"],
+ }
+
+ uid, sc = await self.add_script("General")
+ script_dict[GeneralConfig.name] = str(uid)
+ await sc.load(general_config)
+
+ for user in (GeneralConfig / "SubData").iterdir():
+ if user.is_dir() and (user / "config.json").exists():
+ user_config = json.loads(
+ (user / "config.json").read_text(
+ encoding="utf-8"
+ )
+ )
+
+ user_uid, uc = await self.add_user(str(uid))
+ await uc.load(user_config)
+
+ if (user / "ConfigFiles").exists():
+ (Path.cwd() / f"data/{uid}/{user_uid}").mkdir(
+ parents=True, exist_ok=True
+ )
+ shutil.move(
+ user / "ConfigFiles",
+ Path.cwd()
+ / f"data/{uid}/{user_uid}/ConfigFile",
+ )
+
+ await self.ScriptConfig.save()
+
+ if (Path.cwd() / "config/QueueConfig").exists():
+ for QueueConfig in (Path.cwd() / "config/QueueConfig").glob(
+ "*.json"
+ ):
+ queue_config = json.loads(
+ QueueConfig.read_text(encoding="utf-8")
+ )
+
+ uid, qc = await self.add_queue()
+
+ queue_config["Info"] = queue_config["QueueSet"]
+ await qc.load(queue_config)
+
+ for i in range(10):
+ item_uid, item = await self.add_queue_item(str(uid))
+ time_uid, time = await self.add_time_set(str(uid))
+
+ await time.load(
+ {
+ "Info": {
+ "Enabled": queue_config["Time"][f"Enabled_{i}"],
+ "Time": queue_config["Time"][f"Set_{i}"],
+ }
+ }
+ )
+ await item.load(
+ {
+ "Info": {
+ "ScriptId": script_dict.get(
+ queue_config["Queue"][f"Script_{i}"], None
+ )
+ }
+ }
+ )
+ await self.QueueConfig.save()
+
+ if (Path.cwd() / "config/QueueConfig").exists():
+ shutil.rmtree(Path.cwd() / "config/QueueConfig")
+ if (Path.cwd() / "config/MaaPlanConfig").exists():
+ shutil.rmtree(Path.cwd() / "config/MaaPlanConfig")
+ if (Path.cwd() / "config/MaaConfig").exists():
+ shutil.rmtree(Path.cwd() / "config/MaaConfig")
+ if (Path.cwd() / "config/GeneralConfig").exists():
+ shutil.rmtree(Path.cwd() / "config/GeneralConfig")
+ if (Path.cwd() / "data/gameid.txt").exists():
+ (Path.cwd() / "data/gameid.txt").unlink()
+ if (Path.cwd() / "data/key").exists():
+ shutil.rmtree(Path.cwd() / "data/key")
+
+ cur.execute("DELETE FROM version WHERE v = ?", ("v1.8",))
+ cur.execute("INSERT INTO version VALUES(?)", ("v1.9",))
+ db.commit()
cur.close()
db.close()
- logger.success("数据文件版本更新完成", module="配置管理")
+ logger.success("数据文件版本更新完成")
- 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"]
- )
+ async def send_json(self, data: dict) -> None:
+ """通过WebSocket发送JSON数据"""
+ if Config.websocket is None:
+ raise RuntimeError("WebSocket 未连接")
else:
- logger.warning(
- f"无法从MAA服务器获取活动关卡信息:{network_result['error_message']}",
- module="配置管理",
- )
- stage_infos = []
+ await Config.websocket.send_json(data)
- ss_stage_dict = {"value": [], "text": []}
+ async def add_script(
+ self, script: Literal["MAA", "General"]
+ ) -> tuple[uuid.UUID, ConfigBase]:
+ """添加脚本配置"""
- for stage_info in stage_infos:
+ logger.info(f"添加脚本配置: {script}")
- 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"
- )
+ return await self.ScriptConfig.add(CLASS_BOOK[script])
+
+ async def get_script(self, script_id: Optional[str]) -> tuple[list, dict]:
+ """获取脚本配置"""
+
+ logger.info(f"获取脚本配置: {script_id}")
+
+ if script_id is None:
+ data = await self.ScriptConfig.toDict()
+ else:
+ data = await self.ScriptConfig.get(uuid.UUID(script_id))
+
+ index = data.pop("instances", [])
+
+ return list(index), data
+
+ async def update_script(
+ self, script_id: str, data: Dict[str, Dict[str, Any]]
+ ) -> None:
+ """更新脚本配置"""
+
+ logger.info(f"更新脚本配置: {script_id}")
+
+ uid = uuid.UUID(script_id)
+
+ if uid in self.task_dict:
+ raise RuntimeError(f"脚本 {script_id} 正在运行, 无法更新配置项")
+
+ for group, items in data.items():
+ for name, value in items.items():
+ logger.debug(f"更新脚本配置: {script_id} - {group}.{name} = {value}")
+ await self.ScriptConfig[uid].set(group, name, value)
+
+ await self.ScriptConfig.save()
+
+ async def del_script(self, script_id: str) -> None:
+ """删除脚本配置"""
+
+ logger.info(f"删除脚本配置: {script_id}")
+
+ uid = uuid.UUID(script_id)
+
+ if uid in self.task_dict:
+ raise RuntimeError(f"脚本 {script_id} 正在运行, 无法删除")
+
+ await self.ScriptConfig.remove(uid)
+
+ async def reorder_script(self, index_list: list[str]) -> None:
+ """重新排序脚本"""
+
+ logger.info(f"重新排序脚本: {index_list}")
+
+ await self.ScriptConfig.setOrder([uuid.UUID(_) for _ in index_list])
+
+ async def import_script_from_file(self, script_id: str, jsonFile: str) -> None:
+ """从文件加载脚本配置"""
+
+ logger.info(f"从文件加载脚本配置: {script_id} - {jsonFile}")
+ uid = uuid.UUID(script_id)
+ file_path = Path(jsonFile)
+
+ if uid not in self.ScriptConfig:
+ logger.error(f"{script_id} 不存在")
+ raise KeyError(f"脚本 {script_id} 不存在")
+ if not isinstance(self.ScriptConfig[uid], GeneralConfig):
+ logger.error(f"{script_id} 不是通用脚本配置")
+ raise TypeError(f"脚本 {script_id} 不是通用脚本配置")
+ if not Path(file_path).exists():
+ logger.error(f"文件不存在: {file_path}")
+ raise FileNotFoundError(f"文件不存在: {file_path}")
+
+ data = json.loads(file_path.read_text(encoding="utf-8"))
+ await self.ScriptConfig[uid].load(data)
+ await self.ScriptConfig.save()
+
+ logger.success(f"{script_id} 配置加载成功")
+
+ async def export_script_to_file(self, script_id: str, jsonFile: str):
+ """导出脚本配置到文件"""
+
+ logger.info(f"导出配置到文件: {script_id} - {jsonFile}")
+
+ uid = uuid.UUID(script_id)
+ file_path = Path(jsonFile)
+
+ if uid not in self.ScriptConfig:
+ logger.error(f"{script_id} 不存在")
+ raise KeyError(f"脚本 {script_id} 不存在")
+ if not isinstance(self.ScriptConfig[uid], GeneralConfig):
+ logger.error(f"{script_id} 不是通用脚本配置")
+ raise TypeError(f"脚本 {script_id} 不是通用脚本配置")
+
+ temp = await self.ScriptConfig[uid].toDict(
+ ignore_multi_config=True, if_decrypt=False
+ )
+
+ # 移除配置中可能存在的隐私信息
+ temp["Info"]["Name"] = Path(file_path).stem
+ for path in ["ScriptPath", "ConfigPath", "LogPath"]:
+
+ if Path(temp["Script"][path]).is_relative_to(
+ Path(temp["Info"]["RootPath"])
):
- 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]},
- ]
+ temp["Script"][path] = str(
+ Path(r"C:/脚本根目录")
+ / Path(temp["Script"][path]).relative_to(
+ Path(temp["Info"]["RootPath"])
+ )
+ )
+ temp["Info"]["RootPath"] = str(Path(r"C:/脚本根目录"))
- for day in range(0, 8):
+ file_path.write_text(
+ json.dumps(temp, ensure_ascii=False, indent=4), encoding="utf-8"
+ )
- today_stage_dict = {"value": [], "text": []}
+ logger.success(f"{script_id} 配置导出成功")
- for stage_info in stage_daily_info:
+ async def import_script_from_web(self, script_id: str, url: str):
+ """从「AUTO_MAA 配置分享中心」导入配置"""
- 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"])
+ logger.info(f"从网络加载脚本配置: {script_id} - {url}")
+ uid = uuid.UUID(script_id)
- 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"],
- }
+ if uid not in self.ScriptConfig:
+ logger.error(f"{script_id} 不存在")
+ raise KeyError(f"脚本 {script_id} 不存在")
+ if not isinstance(self.ScriptConfig[uid], GeneralConfig):
+ logger.error(f"{script_id} 不是通用脚本配置")
+ raise TypeError(f"脚本 {script_id} 不是通用脚本配置")
- self.stage_refreshed.emit()
+ response = requests.get(url, timeout=10, proxies=self.get_proxies())
+ if response.status_code == 200:
+ data = response.json()
+ else:
+ logger.warning(f"无法从 AUTO_MAA 服务器获取配置内容: {response.text}")
+ raise ConnectionError(
+ f"无法从 AUTO_MAA 服务器获取配置内容: {response.status_code}"
+ )
- logger.success("活动关卡信息更新完成", module="配置管理")
+ await self.ScriptConfig[uid].load(data)
+ await self.ScriptConfig.save()
+
+ logger.success(f"{script_id} 配置加载成功")
+
+ async def upload_script_to_web(
+ self, script_id: str, config_name: str, author: str, description: str
+ ):
+ """上传配置到「AUTO_MAA 配置分享中心」"""
+
+ logger.info(f"上传配置到网络: {script_id} - {config_name} - {author}")
+
+ uid = uuid.UUID(script_id)
+
+ if uid not in self.ScriptConfig:
+ logger.error(f"{script_id} 不存在")
+ raise KeyError(f"脚本 {script_id} 不存在")
+ if not isinstance(self.ScriptConfig[uid], GeneralConfig):
+ logger.error(f"{script_id} 不是通用脚本配置")
+ raise TypeError(f"脚本 {script_id} 不是通用脚本配置")
+
+ temp = await self.ScriptConfig[uid].toDict(
+ ignore_multi_config=True, if_decrypt=False
+ )
+
+ # 移除配置中可能存在的隐私信息
+ temp["Info"]["Name"] = config_name
+ for path in ["ScriptPath", "ConfigPath", "LogPath"]:
+ if Path(temp["Script"][path]).is_relative_to(
+ Path(temp["Info"]["RootPath"])
+ ):
+ temp["Script"][path] = str(
+ Path(r"C:/脚本根目录")
+ / Path(temp["Script"][path]).relative_to(
+ Path(temp["Info"]["RootPath"])
+ )
+ )
+ temp["Info"]["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}
+
+ response = requests.post(
+ "http://221.236.27.82:10023/api/upload/share",
+ files=files,
+ data=data,
+ timeout=10,
+ proxies=self.get_proxies(),
+ )
+
+ if response.status_code == 200:
+ logger.success("配置上传成功")
+ else:
+ logger.error(f"无法上传配置到 AUTO_MAA 服务器: {response.text}")
+ raise ConnectionError(
+ f"无法上传配置到 AUTO_MAA 服务器: {response.status_code} - {response.text}"
+ )
+
+ async def get_user(
+ self, script_id: str, user_id: Optional[str]
+ ) -> tuple[list, dict]:
+ """获取用户配置"""
+
+ logger.info(f"获取用户配置: {script_id} - {user_id}")
+
+ uid = uuid.UUID(script_id)
+ sc = self.ScriptConfig[uid]
+
+ if isinstance(sc, (MaaConfig | GeneralConfig)):
+ if user_id is None:
+ data = await sc.UserData.toDict()
+ else:
+ data = await sc.UserData.get(uuid.UUID(user_id))
+ else:
+ logger.error(f"不支持的脚本配置类型: {type(sc)}")
+ raise TypeError(f"不支持的脚本配置类型: {type(sc)}")
+
+ index = data.pop("instances", [])
+
+ return list(index), data
+
+ async def add_user(self, script_id: str) -> tuple[uuid.UUID, ConfigBase]:
+ """添加用户配置"""
+
+ logger.info(f"{script_id} 添加用户配置")
+
+ script_config = self.ScriptConfig[uuid.UUID(script_id)]
+
+ if isinstance(script_config, MaaConfig):
+ uid, config = await script_config.UserData.add(MaaUserConfig)
+ elif isinstance(script_config, GeneralConfig):
+ uid, config = await script_config.UserData.add(GeneralUserConfig)
+ else:
+ raise TypeError(f"不支持的脚本配置类型: {type(script_config)}")
+
+ await self.ScriptConfig.save()
+ return uid, config
+
+ async def update_user(
+ self, script_id: str, user_id: str, data: Dict[str, Dict[str, Any]]
+ ) -> None:
+ """更新用户配置"""
+
+ logger.info(f"{script_id} 更新用户配置: {user_id}")
+
+ script_config = self.ScriptConfig[uuid.UUID(script_id)]
+ uid = uuid.UUID(user_id)
+
+ for group, items in data.items():
+ for name, value in items.items():
+ logger.debug(f"更新脚本配置: {script_id} - {group}.{name} = {value}")
+ if isinstance(script_config, (MaaConfig | GeneralConfig)):
+ await script_config.UserData[uid].set(group, name, value)
+
+ await self.ScriptConfig.save()
+
+ async def del_user(self, script_id: str, user_id: str) -> None:
+ """删除用户配置"""
+
+ logger.info(f"{script_id} 删除用户配置: {user_id}")
+
+ script_config = self.ScriptConfig[uuid.UUID(script_id)]
+ uid = uuid.UUID(user_id)
+
+ if isinstance(script_config, (MaaConfig | GeneralConfig)):
+ await script_config.UserData.remove(uid)
+ await self.ScriptConfig.save()
+
+ async def reorder_user(self, script_id: str, index_list: list[str]) -> None:
+ """重新排序用户"""
+
+ logger.info(f"{script_id} 重新排序用户: {index_list}")
+
+ script_config = self.ScriptConfig[uuid.UUID(script_id)]
+
+ if isinstance(script_config, (MaaConfig | GeneralConfig)):
+ await script_config.UserData.setOrder([uuid.UUID(_) for _ in index_list])
+ await self.ScriptConfig.save()
+
+ async def set_infrastructure(
+ self, script_id: str, user_id: str, jsonFile: str
+ ) -> None:
+
+ logger.info(f"{script_id} - {user_id} 设置基建配置: {jsonFile}")
+
+ script_config = self.ScriptConfig[uuid.UUID(script_id)]
+ uid = uuid.UUID(user_id)
+ json_path = Path(jsonFile)
+
+ if not json_path.exists():
+ raise FileNotFoundError(f"文件未找到: {json_path}")
+
+ (Path.cwd() / f"data/{script_id}/{user_id}/Infrastructure").mkdir(
+ parents=True, exist_ok=True
+ )
+ shutil.copy(
+ json_path,
+ Path.cwd()
+ / f"data/{script_id}/{user_id}/Infrastructure/infrastructure.json",
+ )
+
+ if isinstance(script_config, (MaaConfig)):
+ await script_config.UserData[uid].set("Info", "InfrastPath", str(json_path))
+
+ async def add_plan(
+ self, script: Literal["MaaPlan"]
+ ) -> tuple[uuid.UUID, ConfigBase]:
+ """添加计划表"""
+
+ logger.info(f"添加计划表: {script}")
+
+ return await self.PlanConfig.add(CLASS_BOOK[script])
+
+ async def get_plan(self, plan_id: Optional[str]) -> tuple[list, dict]:
+ """获取计划表配置"""
+
+ logger.info(f"获取计划表配置: {plan_id}")
+
+ if plan_id is None:
+ data = await self.PlanConfig.toDict()
+ else:
+ data = await self.PlanConfig.get(uuid.UUID(plan_id))
+
+ index = data.pop("instances", [])
+
+ return list(index), data
+
+ async def update_plan(self, plan_id: str, data: Dict[str, Dict[str, Any]]) -> None:
+ """更新计划表配置"""
+
+ logger.info(f"更新计划表配置: {plan_id}")
+
+ uid = uuid.UUID(plan_id)
+
+ for group, items in data.items():
+ for name, value in items.items():
+ logger.debug(f"更新计划表配置: {plan_id} - {group}.{name} = {value}")
+ await self.PlanConfig[uid].set(group, name, value)
+
+ await self.PlanConfig.save()
+
+ async def del_plan(self, plan_id: str) -> None:
+ """删除计划表配置"""
+
+ logger.info(f"删除计划表配置: {plan_id}")
+
+ await self.PlanConfig.remove(uuid.UUID(plan_id))
+
+ async def reorder_plan(self, index_list: list[str]) -> None:
+ """重新排序计划表"""
+
+ logger.info(f"重新排序计划表: {index_list}")
+
+ await self.PlanConfig.setOrder([uuid.UUID(_) for _ in index_list])
+
+ async def add_queue(self) -> tuple[uuid.UUID, ConfigBase]:
+ """添加调度队列"""
+
+ logger.info("添加调度队列")
+
+ return await self.QueueConfig.add(QueueConfig)
+
+ async def get_queue(self, queue_id: Optional[str]) -> tuple[list, dict]:
+ """获取调度队列配置"""
+
+ logger.info(f"获取调度队列配置: {queue_id}")
+
+ if queue_id is None:
+ data = await self.QueueConfig.toDict()
+ else:
+ data = await self.QueueConfig.get(uuid.UUID(queue_id))
+
+ index = data.pop("instances", [])
+
+ return list(index), data
+
+ async def update_queue(
+ self, queue_id: str, data: Dict[str, Dict[str, Any]]
+ ) -> None:
+ """更新调度队列配置"""
+
+ logger.info(f"更新调度队列配置: {queue_id}")
+
+ uid = uuid.UUID(queue_id)
+
+ for group, items in data.items():
+ for name, value in items.items():
+ logger.debug(f"更新调度队列配置: {queue_id} - {group}.{name} = {value}")
+ await self.QueueConfig[uid].set(group, name, value)
+
+ await self.QueueConfig.save()
+
+ async def del_queue(self, queue_id: str) -> None:
+ """删除调度队列配置"""
+
+ logger.info(f"删除调度队列配置: {queue_id}")
+
+ await self.QueueConfig.remove(uuid.UUID(queue_id))
+
+ async def reorder_queue(self, index_list: list[str]) -> None:
+ """重新排序调度队列"""
+
+ logger.info(f"重新排序调度队列: {index_list}")
+
+ await self.QueueConfig.setOrder([uuid.UUID(_) for _ in index_list])
+
+ async def get_time_set(
+ self, queue_id: str, time_set_id: Optional[str]
+ ) -> tuple[list, dict]:
+ """获取时间设置配置"""
+
+ logger.info(f"Get time set of queue: {queue_id} - {time_set_id}")
+
+ uid = uuid.UUID(queue_id)
+ qc = self.QueueConfig[uid]
+
+ if isinstance(qc, QueueConfig):
+ if time_set_id is None:
+ data = await qc.TimeSet.toDict()
+ else:
+ data = await qc.TimeSet.get(uuid.UUID(time_set_id))
+ else:
+ logger.error(f"不支持的队列配置类型: {type(qc)}")
+ raise TypeError(f"不支持的队列配置类型: {type(qc)}")
+
+ index = data.pop("instances", [])
+
+ return list(index), data
+
+ async def add_time_set(self, queue_id: str) -> tuple[uuid.UUID, ConfigBase]:
+ """添加时间设置配置"""
+
+ logger.info(f"{queue_id} 添加时间设置配置")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+
+ if isinstance(queue_config, QueueConfig):
+ uid, config = await queue_config.TimeSet.add(TimeSet)
+ else:
+ raise TypeError(f"不支持的队列配置类型: {type(queue_config)}")
+
+ await self.QueueConfig.save()
+ return uid, config
+
+ async def update_time_set(
+ self, queue_id: str, time_set_id: str, data: Dict[str, Dict[str, Any]]
+ ) -> None:
+ """更新时间设置配置"""
+
+ logger.info(f"{queue_id} 更新时间设置配置: {time_set_id}")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+ uid = uuid.UUID(time_set_id)
+
+ for group, items in data.items():
+ for name, value in items.items():
+ logger.debug(f"更新时间设置配置: {queue_id} - {group}.{name} = {value}")
+ if isinstance(queue_config, QueueConfig):
+ await queue_config.TimeSet[uid].set(group, name, value)
+
+ await self.QueueConfig.save()
+
+ async def del_time_set(self, queue_id: str, time_set_id: str) -> None:
+ """删除时间设置配置"""
+
+ logger.info(f"{queue_id} 删除时间设置配置: {time_set_id}")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+ uid = uuid.UUID(time_set_id)
+
+ if isinstance(queue_config, QueueConfig):
+ await queue_config.TimeSet.remove(uid)
+ await self.QueueConfig.save()
+
+ async def reorder_time_set(self, queue_id: str, index_list: list[str]) -> None:
+ """重新排序时间设置"""
+
+ logger.info(f"{queue_id} 重新排序时间设置: {index_list}")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+
+ if isinstance(queue_config, QueueConfig):
+ await queue_config.TimeSet.setOrder([uuid.UUID(_) for _ in index_list])
+ await self.QueueConfig.save()
+
+ async def get_queue_item(
+ self, queue_id: str, queue_item_id: Optional[str]
+ ) -> tuple[list, dict]:
+ """获取队列项配置"""
+
+ logger.info(f"Get queue item of queue: {queue_id} - {queue_item_id}")
+
+ uid = uuid.UUID(queue_id)
+ qc = self.QueueConfig[uid]
+
+ if isinstance(qc, QueueConfig):
+ if queue_item_id is None:
+ data = await qc.QueueItem.toDict()
+ else:
+ data = await qc.QueueItem.get(uuid.UUID(queue_item_id))
+ else:
+ logger.error(f"不支持的队列配置类型: {type(qc)}")
+ raise TypeError(f"不支持的队列配置类型: {type(qc)}")
+
+ index = data.pop("instances", [])
+
+ return list(index), data
+
+ async def add_queue_item(self, queue_id: str) -> tuple[uuid.UUID, ConfigBase]:
+ """添加队列项配置"""
+
+ logger.info(f"{queue_id} 添加队列项配置")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+
+ if isinstance(queue_config, QueueConfig):
+ uid, config = await queue_config.QueueItem.add(QueueItem)
+ else:
+ logger.warning(f"不支持的队列配置类型: {type(queue_config)}")
+ raise TypeError(f"不支持的队列配置类型: {type(queue_config)}")
+
+ await self.QueueConfig.save()
+ return uid, config
+
+ async def update_queue_item(
+ self, queue_id: str, queue_item_id: str, data: Dict[str, Dict[str, Any]]
+ ) -> None:
+ """更新队列项配置"""
+
+ logger.info(f"{queue_id} 更新队列项配置: {queue_item_id}")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+ uid = uuid.UUID(queue_item_id)
+
+ for group, items in data.items():
+ for name, value in items.items():
+ if uuid.UUID(value) not in self.ScriptConfig:
+ logger.warning(f"脚本 {value} 不存在")
+ raise ValueError(f"脚本 {value} 不存在")
+ logger.debug(f"更新队列项配置: {queue_id} - {group}.{name} = {value}")
+ if isinstance(queue_config, QueueConfig):
+ await queue_config.QueueItem[uid].set(group, name, value)
+
+ await self.QueueConfig.save()
+
+ async def del_queue_item(self, queue_id: str, queue_item_id: str) -> None:
+ """删除队列项配置"""
+
+ logger.info(f"{queue_id} 删除队列项配置: {queue_item_id}")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+ uid = uuid.UUID(queue_item_id)
+
+ if isinstance(queue_config, QueueConfig):
+ await queue_config.QueueItem.remove(uid)
+ await self.QueueConfig.save()
+
+ async def reorder_queue_item(self, queue_id: str, index_list: list[str]) -> None:
+ """重新排序队列项"""
+
+ logger.info(f"{queue_id} 重新排序队列项: {index_list}")
+
+ queue_config = self.QueueConfig[uuid.UUID(queue_id)]
+
+ if isinstance(queue_config, QueueConfig):
+ await queue_config.QueueItem.setOrder([uuid.UUID(_) for _ in index_list])
+ await self.QueueConfig.save()
+
+ async def get_setting(self) -> Dict[str, Any]:
+ """获取全局设置"""
+
+ logger.info("获取全局设置")
+
+ return await self.toDict(ignore_multi_config=True)
+
+ async def update_setting(self, data: Dict[str, Dict[str, Any]]) -> None:
+ """更新全局设置"""
+
+ logger.info("更新全局设置")
+
+ for group, items in data.items():
+ for name, value in items.items():
+ logger.debug(f"更新全局设置 - {group}.{name} = {value}")
+ await self.set(group, name, value)
+
+ logger.success("全局设置更新成功")
def server_date(self) -> date:
"""
@@ -1198,338 +1501,412 @@ class AppConfig(GlobalConfig):
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="配置管理")
+ def get_proxies(self) -> Dict[str, str]:
+ """获取代理设置"""
+ return {
+ "http": self.get("Update", "ProxyAddress"),
+ "https": self.get("Update", "ProxyAddress"),
+ }
+
+ async def get_stage_info(
+ self,
+ type: Literal[
+ "Today",
+ "ALL",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday",
+ "Info",
+ ],
+ ):
+ """获取关卡信息"""
+
+ if type == "Today":
+ dt = self.server_date()
+ index = dt.strftime("%A")
else:
- logger.warning(f"保存历史记录时未找到调度队列: {key}")
+ index = type
- def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool:
+ if json.loads(self.get("Data", "Stage")) != {}:
+ task = asyncio.create_task(self.get_stage())
+ self.temp_task.append(task)
+ task.add_done_callback(lambda t: self.temp_task.remove(t))
+ else:
+ await self.get_stage()
+
+ if index == "Info":
+ today = self.server_date().isoweekday()
+ res_stage_info = []
+ for stage in RESOURCE_STAGE_INFO:
+ if (
+ today in stage["days"]
+ and stage["value"] in RESOURCE_STAGE_DROP_INFO
+ ):
+ res_stage_info.append(RESOURCE_STAGE_DROP_INFO[stage["value"]])
+ return {
+ "Activity": json.loads(self.get("Data", "Stage")).get("Info", []),
+ "Resource": res_stage_info,
+ }
+ else:
+ return json.loads(self.get("Data", "Stage")).get(index, [])
+
+ async def get_proxy_overview(self) -> Dict[str, Any]:
+ """获取代理情况概览信息"""
+
+ logger.info("获取代理情况概览信息")
+
+ history_index = await self.search_history(
+ "按日合并", self.server_date(), self.server_date()
+ )
+ if self.server_date().strftime("%Y年 %m月 %d日") not in history_index:
+ return {}
+ history_data = {
+ k: await self.merge_statistic_info(v)
+ for k, v in history_index[
+ self.server_date().strftime("%Y年 %m月 %d日")
+ ].items()
+ }
+ overview = {}
+ for user, data in history_data.items():
+ last_proxy_date = max(
+ datetime.strptime(_["date"], "%Y年%m月%d日 %H:%M:%S")
+ for _ in data.get("index", [])
+ ).strftime("%Y年%m月%d日 %H:%M:%S")
+ proxy_times = len(data.get("index", []))
+ error_info = data.get("error_info", {})
+ error_times = len(error_info)
+ overview[user] = {
+ "LastProxyDate": last_proxy_date,
+ "ProxyTimes": proxy_times,
+ "ErrorTimes": error_times,
+ "ErrorInfo": error_info,
+ }
+ return overview
+
+ async def get_stage(
+ self, if_start: bool = False
+ ) -> Optional[Dict[str, List[Dict[str, str]]]]:
+ """更新活动关卡信息"""
+
+ if datetime.now() - timedelta(hours=1) < datetime.strptime(
+ self.get("Data", "LastStageUpdated"), "%Y-%m-%d %H:%M:%S"
+ ):
+ logger.info("一小时内已进行过一次检查, 直接使用缓存的活动关卡信息")
+ return json.loads(self.get("Data", "Stage"))
+
+ logger.info("开始获取活动关卡信息")
+
+ try:
+ response = requests.get(
+ "https://api.maa.plus/MaaAssistantArknights/api/stageAndTasksUpdateTime.json",
+ timeout=3 if if_start else 10,
+ proxies=self.get_proxies(),
+ )
+ if response.status_code == 200:
+ remote_time_stamp = datetime.strptime(
+ str(response.json().get("timestamp", 20000101000000)),
+ "%Y%m%d%H%M%S",
+ )
+ else:
+ logger.warning(f"无法从MAA服务器获取活动关卡时间戳:{response.text}")
+ remote_time_stamp = datetime.fromtimestamp(0)
+ except Exception as e:
+ logger.warning(f"无法从MAA服务器获取活动关卡时间戳: {e}")
+ remote_time_stamp = datetime.fromtimestamp(0)
+
+ local_time_stamp = datetime.strptime(
+ self.get("Data", "StageTimeStamp"), "%Y-%m-%d %H:%M:%S"
+ )
+
+ # 本地关卡信息无需更新, 直接返回本地数据
+ if datetime.fromtimestamp(0) < remote_time_stamp <= local_time_stamp:
+
+ logger.info("使用本地关卡信息")
+ await self.set(
+ "Data", "LastStageUpdated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
+ return json.loads(self.get("Data", "Stage"))
+
+ # 需要更新关卡信息
+ logger.info("从远端更新关卡信息")
+
+ try:
+ response = requests.get(
+ "https://api.maa.plus/MaaAssistantArknights/api/gui/StageActivity.json",
+ timeout=3 if if_start else 10,
+ proxies=self.get_proxies(),
+ )
+ if response.status_code == 200:
+ remote_activity_stage_info = (
+ response.json().get("Official", {}).get("sideStoryStage", [])
+ )
+ if_get_maa_stage = True
+ else:
+ logger.warning(f"无法从MAA服务器获取活动关卡信息:{response.text}")
+ if_get_maa_stage = False
+ remote_activity_stage_info = []
+ except Exception as e:
+ logger.warning(f"无法从MAA服务器获取活动关卡信息: {e}")
+ if_get_maa_stage = False
+ remote_activity_stage_info = []
+
+ def normalize_drop(value: str) -> str:
+ # 去前后空格与常见零宽字符
+ s = str(value).strip()
+ s = re.sub(r"[\u200b\u200c\u200d\ufeff]", "", s)
+ return s
+
+ def parse_utc(dt_str: str) -> datetime:
+ return datetime.strptime(dt_str, "%Y/%m/%d %H:%M:%S").replace(
+ tzinfo=timezone.utc
+ )
+
+ now_utc = datetime.now(timezone.utc)
+ activity_stage_drop_info: List[Dict[str, Any]] = []
+
+ for stage in remote_activity_stage_info:
+ if "SSReopen" in stage.get("Display", ""):
+ continue
+ act = stage.get("Activity", {}) or {}
+ try:
+ start_utc = parse_utc(act["UtcStartTime"])
+ expire_utc = parse_utc(act["UtcExpireTime"])
+ except Exception:
+ continue
+
+ if start_utc <= now_utc < expire_utc:
+ raw_drop = stage.get("Drop", "")
+ drop_id = normalize_drop(raw_drop)
+
+ if drop_id.isdigit():
+ drop_name = MATERIALS_MAP.get(drop_id, "未知材料")
+ else:
+ drop_name = (
+ "DESC:" + drop_id
+ ) # 非纯数字, 直接用文本.加一个DESC前缀方便前端区分
+
+ activity_stage_drop_info.append(
+ {
+ "Display": stage.get("Display", ""),
+ "Value": stage.get("Value", ""),
+ "Drop": raw_drop,
+ "DropName": drop_name,
+ "Activity": stage.get("Activity", {}),
+ }
+ )
+
+ activity_stage_combox = []
+
+ for stage in remote_activity_stage_info:
+
+ if (
+ datetime.strptime(
+ stage["Activity"]["UtcStartTime"], "%Y/%m/%d %H:%M:%S"
+ )
+ < datetime.now()
+ < datetime.strptime(
+ stage["Activity"]["UtcExpireTime"], "%Y/%m/%d %H:%M:%S"
+ )
+ ):
+ activity_stage_combox.append(
+ {"label": stage["Value"], "value": stage["Value"]}
+ )
+
+ stage_data = {}
+
+ for day in range(0, 8):
+
+ res_stage = []
+
+ for stage in RESOURCE_STAGE_INFO:
+
+ if day in stage["days"] or day == 0:
+ res_stage.append({"label": stage["text"], "value": stage["value"]})
+
+ stage_data[calendar.day_name[day - 1] if day > 0 else "ALL"] = (
+ res_stage[0:1] + activity_stage_combox + res_stage[1:]
+ )
+
+ stage_data["Info"] = activity_stage_drop_info
+
+ if if_get_maa_stage:
+
+ logger.success("成功获取远端活动关卡信息")
+ await self.set(
+ "Data", "LastStageUpdated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
+ await self.set(
+ "Data",
+ "StageTimeStamp",
+ remote_time_stamp.strftime("%Y-%m-%d %H:%M:%S"),
+ )
+ await self.set("Data", "Stage", json.dumps(stage_data, ensure_ascii=False))
+
+ return stage_data
+
+ async def get_script_combox(self):
+ """获取脚本下拉框信息"""
+
+ logger.info("Getting script combo box information...")
+ data = [{"label": "未选择", "value": None}]
+ for uid, script in self.ScriptConfig.items():
+ data.append(
+ {
+ "label": f"{TYPE_BOOK[type(script).__name__]} - {script.get('Info', 'Name')}",
+ "value": str(uid),
+ }
+ )
+ logger.success("Script combo box information retrieved successfully.")
+
+ return data
+
+ async def get_task_combox(self):
+ """获取任务下拉框信息"""
+
+ logger.info("开始获取任务下拉框信息")
+ data = [{"label": "未选择", "value": None}]
+ for uid, queue in self.QueueConfig.items():
+ data.append(
+ {
+ "label": f"队列 - {queue.get('Info', 'Name')}",
+ "value": str(uid),
+ }
+ )
+ for uid, script in self.ScriptConfig.items():
+ data.append(
+ {
+ "label": f"脚本 - {TYPE_BOOK[type(script).__name__]} - {script.get('Info', 'Name')}",
+ "value": str(uid),
+ }
+ )
+ logger.success("任务下拉框信息获取成功")
+
+ return data
+
+ async def get_plan_combox(self):
+ """获取计划下拉框信息"""
+
+ logger.info("开始获取计划下拉框信息")
+ data = [{"label": "固定", "value": "Fixed"}]
+ for uid, plan in self.PlanConfig.items():
+ data.append({"label": plan.get("Info", "Name"), "value": str(uid)})
+ logger.success("计划下拉框信息获取成功")
+
+ return data
+
+ async def get_notice(self) -> tuple[bool, Dict[str, str]]:
+ """获取公告信息"""
+
+ local_notice = json.loads(self.get("Data", "Notice"))
+ if datetime.now() - timedelta(hours=1) < datetime.strptime(
+ self.get("Data", "LastNoticeUpdated"), "%Y-%m-%d %H:%M:%S"
+ ):
+ logger.info("一小时内已进行过一次检查, 直接使用缓存的公告信息")
+ return False, local_notice.get("notice_dict", {})
+
+ logger.info(f"开始从 AUTO_MAA 服务器获取公告信息")
+
+ try:
+ response = requests.get(
+ "https://download.auto-mas.top/d/AUTO_MAA/Server/notice.json",
+ timeout=10,
+ proxies=self.get_proxies(),
+ )
+ if response.status_code == 200:
+ remote_notice = response.json()
+ else:
+ logger.warning(f"无法从 AUTO_MAA 服务器获取公告信息:{response.text}")
+ remote_notice = None
+ except Exception as e:
+ logger.warning(f"无法从 AUTO_MAA 服务器获取公告信息: {e}")
+ remote_notice = None
+
+ if remote_notice is None:
+ logger.warning("使用本地公告信息")
+ return False, local_notice.get("notice_dict", {})
+
+ await self.set(
+ "Data", "LastNoticeUpdated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
+
+ local_time_stamp = datetime.strptime(
+ local_notice.get("time", "2000-01-01 00:00"), "%Y-%m-%d %H:%M"
+ )
+ remote_time_stamp = datetime.strptime(
+ remote_notice.get("time", "2000-01-01 00:00"), "%Y-%m-%d %H:%M"
+ )
+
+ # 本地公告信息需更新且持续展示
+ if local_time_stamp < remote_time_stamp < datetime.now():
+
+ logger.info("要求展示本地公告信息")
+ await self.set(
+ "Data", "Notice", json.dumps(remote_notice, ensure_ascii=False)
+ )
+ await self.set("Data", "IfShowNotice", True)
+
+ return self.get("Data", "IfShowNotice"), remote_notice.get("notice_dict", {})
+
+ async def get_web_config(self):
+ """获取「AUTO_MAA 配置分享中心」配置"""
+
+ local_web_config = json.loads(self.get("Data", "WebConfig"))
+ if datetime.now() - timedelta(hours=1) < datetime.strptime(
+ self.get("Data", "LastWebConfigUpdated"), "%Y-%m-%d %H:%M:%S"
+ ):
+ logger.info("一小时内已进行过一次检查, 直接使用缓存的配置分享中心信息")
+ return local_web_config
+
+ logger.info(f"开始从 AUTO_MAA 服务器获取配置分享中心信息")
+
+ try:
+ response = requests.get(
+ "http://221.236.27.82:10023/api/list/config/general",
+ timeout=10,
+ proxies=self.get_proxies(),
+ )
+ if response.status_code == 200:
+ remote_web_config = response.json()
+ else:
+ logger.warning(
+ f"无法从 AUTO_MAA 服务器获取配置分享中心信息:{response.text}"
+ )
+ remote_web_config = None
+ except Exception as e:
+ logger.warning(f"无法从 AUTO_MAA 服务器获取配置分享中心信息: {e}")
+ remote_web_config = None
+
+ if remote_web_config is None:
+ logger.warning("使用本地配置分享中心信息")
+ return local_web_config
+
+ await self.set(
+ "Data", "LastWebConfigUpdated", datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+ )
+ await self.set(
+ "Data", "WebConfig", json.dumps(remote_web_config, ensure_ascii=False)
+ )
+
+ return remote_web_config
+
+ async def get_startup_task(self):
+ """获取启动时需要运行的队列信息"""
+
+ logger.info("获取启动时需要运行的队列信息")
+ data = [
+ str(uid)
+ for uid, queue in self.QueueConfig.items()
+ if queue.get("Info", "StartUpEnabled")
+ ]
+ logger.success("启动时需要运行的队列信息获取成功")
+
+ return data
+
+ async def save_maa_log(self, log_path: Path, logs: list, maa_result: str) -> bool:
"""
保存MAA日志并生成对应统计数据
@@ -1543,12 +1920,9 @@ class AppConfig(GlobalConfig):
:rtype: bool
"""
- logger.info(
- f"开始处理 MAA 日志,日志长度: {len(logs)},日志标记:{maa_result}",
- module="配置管理",
- )
+ logger.info(f"开始处理 MAA 日志, 日志长度: {len(logs)}, 日志标记: {maa_result}")
- data: Dict[str, Union[str, Dict[str, Union[int, dict]]]] = {
+ data = {
"recruit_statistics": defaultdict(int),
"drop_statistics": defaultdict(dict),
"maa_result": maa_result,
@@ -1579,7 +1953,7 @@ class AppConfig(GlobalConfig):
if confirmed_recruit and current_star_level:
data["recruit_statistics"][current_star_level] += 1
- confirmed_recruit = False # 重置,等待下一次公招
+ confirmed_recruit = False # 重置, 等待下一次公招
current_star_level = None # 清空已处理的星级
i += 1
@@ -1598,13 +1972,13 @@ class AppConfig(GlobalConfig):
if "完成任务: Fight" in logs[j] or "完成任务: 刷理智" in logs[j]:
end_index = j
break
- # 如果遇到新的Fight任务开始,则当前任务没有正常结束
+ # 如果遇到新的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))
@@ -1618,15 +1992,15 @@ class AppConfig(GlobalConfig):
current_stage = None
for line in task_logs:
- # 匹配掉落统计行,如"1-7 掉落统计:"
+ # 匹配掉落统计行, 如"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,]+\))?",
@@ -1647,7 +2021,7 @@ class AppConfig(GlobalConfig):
]:
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] = {}
@@ -1667,11 +2041,13 @@ class AppConfig(GlobalConfig):
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="配置管理")
+ logger.success(f"MAA 日志统计完成, 日志路径: {log_path}")
return if_six_star
- def save_general_log(self, log_path: Path, logs: list, general_result: str) -> None:
+ async def save_general_log(
+ self, log_path: Path, logs: list, general_result: str
+ ) -> None:
"""
保存通用日志并生成对应统计数据
@@ -1681,8 +2057,7 @@ class AppConfig(GlobalConfig):
"""
logger.info(
- f"开始处理通用日志,日志长度: {len(logs)},日志标记:{general_result}",
- module="配置管理",
+ f"开始处理通用日志, 日志长度: {len(logs)}, 日志标记: {general_result}"
)
data: Dict[str, str] = {"general_result": general_result}
@@ -1694,12 +2069,9 @@ class AppConfig(GlobalConfig):
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="配置管理",
- )
+ logger.success(f"通用日志统计完成, 日志路径: {log_path.with_suffix('.log')}")
- def merge_statistic_info(self, statistic_path_list: List[Path]) -> dict:
+ async def merge_statistic_info(self, statistic_path_list: List[Path]) -> dict:
"""
合并指定数据统计信息文件
@@ -1707,19 +2079,14 @@ class AppConfig(GlobalConfig):
:return: 合并后的统计信息字典
"""
- logger.info(
- f"开始合并统计信息文件,共计 {len(statistic_path_list)} 个文件",
- module="配置管理",
- )
+ logger.info(f"开始合并统计信息文件, 共计 {len(statistic_path_list)} 个文件")
- data = {"index": {}}
+ data: Dict[str, Any] = {"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)
- )
+ single_data = json.load(f)
for key in single_data.keys():
@@ -1765,27 +2132,25 @@ class AppConfig(GlobalConfig):
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["error_info"][
+ actual_date.strftime("%Y年%m月%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"][actual_date] = {
+ "date": actual_date.strftime("%Y年%m月%d日 %H:%M:%S"),
+ "status": (
+ "完成" if single_data[key] == "Success!" else "异常"
+ ),
+ "jsonFile": str(json_file),
+ }
data["index"] = [data["index"][_] for _ in sorted(data["index"])]
- logger.success(
- f"统计信息合并完成,共计 {len(data['index'])} 条记录", module="配置管理"
- )
+ logger.success(f"统计信息合并完成, 共计 {len(data['index'])} 条记录")
return {k: v for k, v in data.items() if v}
- def search_history(
- self, mode: str, start_date: datetime, end_date: datetime
- ) -> dict:
+ async def search_history(self, mode: str, start_date: date, end_date: date) -> dict:
"""
搜索指定范围内的历史记录
@@ -1796,19 +2161,18 @@ class AppConfig(GlobalConfig):
"""
logger.info(
- f"开始搜索历史记录,合并模式:{mode},日期范围:{start_date} 至 {end_date}",
- module="配置管理",
+ f"开始搜索历史记录, 合并模式: {mode}, 日期范围: {start_date} 至 {end_date}"
)
history_dict = {}
- for date_folder in (Config.app_path / "history").iterdir():
+ for date_folder in self.history_path.iterdir():
if not date_folder.is_dir():
continue # 只处理日期文件夹
try:
- date = datetime.strptime(date_folder.name, "%Y-%m-%d")
+ date = datetime.strptime(date_folder.name, "%Y-%m-%d").date()
if not (start_date <= date <= end_date):
continue # 只统计在范围内的日期
@@ -1840,27 +2204,25 @@ class AppConfig(GlobalConfig):
except ValueError:
logger.warning(f"非日期格式的目录: {date_folder}")
- logger.success(
- f"历史记录搜索完成,共计 {len(history_dict)} 条记录", module="配置管理"
- )
+ logger.success(f"历史记录搜索完成, 共计 {len(history_dict)} 条记录")
return {
k: v
for k, v in sorted(history_dict.items(), key=lambda x: x[0], reverse=True)
}
- def clean_old_history(self):
+ async def clean_old_history(self):
"""删除超过用户设定天数的历史记录文件(基于目录日期)"""
- if self.get(self.function_HistoryRetentionTime) == 0:
- logger.info("历史记录永久保留,跳过历史记录清理", module="配置管理")
+ if self.get("Function", "HistoryRetentionTime") == 0:
+ logger.info("历史记录永久保留, 跳过历史记录清理")
return
- logger.info("开始清理超过设定天数的历史记录", module="配置管理")
+ logger.info("开始清理超过设定天数的历史记录")
deleted_count = 0
- for date_folder in (self.app_path / "history").iterdir():
+ for date_folder in self.history_path.iterdir():
if not date_folder.is_dir():
continue # 只处理日期文件夹
@@ -1868,15 +2230,15 @@ class AppConfig(GlobalConfig):
# 只检查 `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)
+ days=self.get("Function", "HistoryRetentionTime")
):
shutil.rmtree(date_folder, ignore_errors=True)
deleted_count += 1
- logger.info(f"已删除超期日志目录: {date_folder}", module="配置管理")
+ logger.info(f"已删除超期日志目录: {date_folder}")
except ValueError:
- logger.warning(f"非日期格式的目录: {date_folder}", module="配置管理")
+ logger.warning(f"非日期格式的目录: {date_folder}")
- logger.success(f"清理完成: {deleted_count} 个日期目录", module="配置管理")
+ logger.success(f"清理完成: {deleted_count} 个日期目录")
Config = AppConfig()
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
index c044767..fec90e0 100644
--- a/app/core/task_manager.py
+++ b/app/core/task_manager.py
@@ -18,443 +18,249 @@
# 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
+import uuid
+import asyncio
+from functools import partial
+from typing import Dict, Optional
-from .logger import logger
-from .config import Config
-from .main_info_bar import MainInfoBar
-from .network import Network
-from .sound_player import SoundPlayer
-from app.models import MaaManager, GeneralManager
+from .config import Config, MaaConfig, GeneralConfig, QueueConfig
+from app.models.schema import WebSocketMessage
+from app.utils import get_logger
+from app.task import *
-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()
+logger = get_logger("业务调度")
-class _TaskManager(QObject):
+class _TaskManager:
"""业务调度器"""
- create_gui = Signal(Task)
- connect_gui = Signal(Task)
-
def __init__(self):
- super(_TaskManager, self).__init__()
+ super().__init__()
- self.task_dict: Dict[str, Task] = {}
+ self.task_dict: Dict[uuid.UUID, asyncio.Task] = {}
- def add_task(
- self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
- ):
+ async def add_task(self, mode: str, uid: str) -> uuid.UUID:
"""
添加任务
:param mode: 任务模式
- :param name: 任务名称
- :param info: 任务信息
+ :param uid: 任务UID
"""
- if name in Config.running_list or name in self.task_dict:
+ actual_id = uuid.UUID(uid)
- logger.warning(f"任务已存在:{name}")
- MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
- return None
+ if mode == "设置脚本":
+ if actual_id in Config.ScriptConfig:
+ task_id = actual_id
+ actual_id = None
+ else:
+ for script_id, script in Config.ScriptConfig.items():
+ if (
+ isinstance(script, (MaaConfig | GeneralConfig))
+ and actual_id in script.UserData
+ ):
+ task_id = script_id
+ break
+ else:
+ raise ValueError(f"任务 {uid} 无法找到对应脚本配置")
+ elif actual_id in Config.QueueConfig:
+ task_id = actual_id
+ actual_id = None
+ elif actual_id in Config.ScriptConfig:
+ task_id = uuid.uuid4()
+ else:
+ raise ValueError(f"任务 {uid} 无法找到对应脚本配置")
- logger.info(f"任务开始:{name},模式:{mode}", module="业务调度")
- MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
- SoundPlayer.play("任务开始")
+ if task_id in self.task_dict or (
+ actual_id is not None and actual_id in self.task_dict
+ ):
- # 标记任务为运行中
- Config.running_list.append(name)
+ raise RuntimeError(f"任务 {task_id} 已在运行")
- # 创建任务实例并连接信号
- 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)
+ logger.info(f"创建任务: {task_id}, 模式: {mode}")
+ self.task_dict[task_id] = asyncio.create_task(
+ self.run_task(mode, task_id, actual_id)
)
- 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)
+ self.task_dict[task_id].add_done_callback(
+ lambda t: asyncio.create_task(self.remove_task(t, mode, task_id))
)
- # 向UI发送信号以创建或连接GUI
- if "新调度台" in mode:
- self.create_gui.emit(self.task_dict[name])
+ return task_id
- elif "主调度台" in mode:
- self.connect_gui.emit(self.task_dict[name])
+ # @logger.catch
+ async def run_task(
+ self, mode: str, task_id: uuid.UUID, actual_id: Optional[uuid.UUID]
+ ):
- # 启动任务线程
- self.task_dict[name].start()
+ logger.info(f"开始运行任务: {task_id}, 模式: {mode}")
- def stop_task(self, name: str) -> None:
+ if mode == "设置脚本":
+
+ if isinstance(Config.ScriptConfig[task_id], MaaConfig):
+ task_item = MaaManager(mode, task_id, actual_id, str(task_id))
+ elif isinstance(Config.ScriptConfig[task_id], GeneralConfig):
+ task_item = GeneralManager(mode, task_id, actual_id, str(task_id))
+ else:
+ logger.error(
+ f"不支持的脚本类型: {type(Config.ScriptConfig[task_id]).__name__}"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=str(task_id),
+ type="Info",
+ data={"Error": "脚本类型不支持"},
+ ).model_dump()
+ )
+ return
+
+ uid = actual_id or uuid.uuid4()
+ self.task_dict[uid] = asyncio.create_task(task_item.run())
+ self.task_dict[uid].add_done_callback(
+ lambda t: asyncio.create_task(task_item.final_task(t))
+ )
+ self.task_dict[uid].add_done_callback(partial(self.task_dict.pop, uid))
+ await self.task_dict[uid]
+
+ else:
+
+ if task_id in Config.QueueConfig:
+
+ queue = Config.QueueConfig[task_id]
+ if not isinstance(queue, QueueConfig):
+ logger.error(
+ f"不支持的队列类型: {type(Config.QueueConfig[task_id]).__name__}"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=str(task_id),
+ type="Info",
+ data={"Error": "队列类型不支持"},
+ ).model_dump()
+ )
+ return
+
+ task_list = []
+ for queue_item in queue.QueueItem.values():
+ if queue_item.get("Info", "ScriptId") is None:
+ continue
+ uid = uuid.UUID(queue_item.get("Info", "ScriptId"))
+ task_list.append(
+ {
+ "script_id": str(uid),
+ "status": "等待",
+ "name": Config.ScriptConfig[uid].get("Info", "Name"),
+ }
+ )
+
+ elif actual_id is not None and actual_id in Config.ScriptConfig:
+
+ task_list = [{"script_id": str(actual_id), "status": "等待"}]
+
+ for task in task_list:
+
+ script_id = uuid.UUID(task["script_id"])
+
+ # 检查任务是否在运行列表中
+ if script_id in self.task_dict:
+
+ task["status"] = "跳过"
+ await Config.send_json(
+ WebSocketMessage(
+ id=str(task_id),
+ type="Update",
+ data={"task_list": task_list},
+ ).model_dump()
+ )
+ logger.info(f"跳过任务: {script_id}, 该任务已在运行列表中")
+ continue
+
+ # 标记为运行中
+ task["status"] = "运行"
+ await Config.send_json(
+ WebSocketMessage(
+ id=str(task_id),
+ type="Update",
+ data={"task_list": task_list},
+ ).model_dump()
+ )
+ logger.info(f"任务开始: {script_id}")
+
+ if isinstance(Config.ScriptConfig[script_id], MaaConfig):
+ task_item = MaaManager(mode, script_id, None, str(task_id))
+ elif isinstance(Config.ScriptConfig[script_id], GeneralConfig):
+ task_item = GeneralManager(mode, script_id, actual_id, str(task_id))
+ else:
+ logger.error(
+ f"不支持的脚本类型: {type(Config.ScriptConfig[script_id]).__name__}"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=str(task_id),
+ type="Info",
+ data={"Error": "脚本类型不支持"},
+ ).model_dump()
+ )
+ continue
+
+ self.task_dict[script_id] = asyncio.create_task(task_item.run())
+ self.task_dict[script_id].add_done_callback(
+ lambda t: asyncio.create_task(task_item.final_task(t))
+ )
+ self.task_dict[script_id].add_done_callback(
+ partial(self.task_dict.pop, script_id)
+ )
+ await self.task_dict[script_id]
+
+ async def stop_task(self, task_id: str) -> None:
"""
中止任务
- :param name: 任务名称
+ :param task_id: 任务ID
"""
- logger.info(f"中止任务:{name}", module="业务调度")
- MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
+ logger.info(f"中止任务: {task_id}")
- if name == "ALL":
+ if task_id == "ALL":
+ for task in self.task_dict.values():
+ task.cancel()
+ else:
+ uid = uuid.UUID(task_id)
+ if uid not in self.task_dict:
+ raise ValueError(f"任务 {uid} 未在运行")
+ self.task_dict[uid].cancel()
- 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:
+ async def remove_task(
+ self, task: asyncio.Task, mode: str, task_id: uuid.UUID
+ ) -> None:
"""
处理任务结束后的收尾工作
- :param mode: 任务模式
- :param name: 任务名称
- :param logs: 任务日志
+ Parameters
+ ----------
+ task : asyncio.Task
+ 任务对象
+ mode : str
+ 任务模式
+ task_id : uuid.UUID
+ 任务ID
"""
- logger.info(f"任务结束:{name}", module="业务调度")
- MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
- SoundPlayer.play("任务结束")
+ logger.info(f"任务结束: {task_id}")
- # 删除任务线程,移除运行中标记
- self.task_dict[name].deleteLater()
- self.task_dict.pop(name)
- Config.running_list.remove(name)
+ # 从任务字典中移除任务
+ try:
+ await task
+ except asyncio.CancelledError:
+ logger.info(f"任务 {task_id} 已结束")
+ self.task_dict.pop(task_id)
- 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",
+ await Config.send_json(
+ WebSocketMessage(
+ id=str(task_id), type="Signal", data={"Accomplish": "无描述"}
+ ).model_dump()
)
- 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
index 66d8dee..55ad9a6 100644
--- a/app/core/timer.py
+++ b/app/core/timer.py
@@ -18,99 +18,40 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA主业务定时器
-v4.4
-作者:DLmaster_361
-"""
-
-from PySide6.QtCore import QObject, QTimer
-from datetime import datetime
+import asyncio
import keyboard
+from datetime import datetime
-from .logger import logger
-from .config import Config
-from .task_manager import TaskManager
from app.services import System
+from app.utils import get_logger
+from .config import Config
-class _MainTimer(QObject):
+logger = get_logger("主业务定时器")
- 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)
+class _MainTimer:
- self.LongTimer = QTimer()
- self.LongTimer.timeout.connect(self.long_timed_task)
+ async def second_task(self):
+ """每秒定期任务"""
+ logger.info("每秒定期任务启动")
- def start(self):
- """启动定时器"""
+ while True:
- logger.info("启动主定时器", module="主业务定时器")
- self.Timer.start(1000)
- self.LongTimer.start(3600000)
+ await self.set_silence()
- def stop(self):
- """停止定时器"""
+ await asyncio.sleep(1)
- 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):
- """设置静默模式"""
+ async def set_silence(self):
+ """静默模式通过模拟老板键来隐藏模拟器窗口"""
if (
- not Config.if_ignore_silence
- and Config.get(Config.function_IfSilence)
- and Config.get(Config.function_BossKey) != ""
+ len(Config.if_ignore_silence) > 0
+ and Config.get("Function", "IfSilence")
+ and Config.get("Function", "BossKey") != ""
):
- windows = System.get_window_info()
+ windows = await System.get_window_info()
emulator_windows = []
for window in windows:
@@ -124,52 +65,17 @@ class _MainTimer(QObject):
if emulator_windows:
- logger.info(
- f"检测到模拟器窗口:{emulator_windows}", module="主业务定时器"
- )
+ logger.info(f"检测到模拟器窗口: {emulator_windows}")
try:
keyboard.press_and_release(
"+".join(
_.strip().lower()
- for _ in Config.get(Config.function_BossKey).split("+")
+ for _ in Config.get("Function", "BossKey").split("+")
)
)
- logger.info(
- f"模拟按键:{Config.get(Config.function_BossKey)}",
- module="主业务定时器",
- )
+ logger.info(f"模拟按键: {Config.get('Function', 'BossKey')}")
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")
+ logger.exception(f"模拟按键时出错: {e}")
MainTimer = _MainTimer()
diff --git a/app/models/ConfigBase.py b/app/models/ConfigBase.py
new file mode 100644
index 0000000..d49a43d
--- /dev/null
+++ b/app/models/ConfigBase.py
@@ -0,0 +1,736 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+import json
+import uuid
+import win32com.client
+from copy import deepcopy
+from pathlib import Path
+from typing import List, Any, Dict, Union
+
+
+from app.utils import dpapi_encrypt, dpapi_decrypt
+
+
+class ConfigValidator:
+ """基础配置验证器"""
+
+ def validate(self, value: Any) -> bool:
+ """验证值是否合法"""
+ return True
+
+ def correct(self, value: Any) -> Any:
+ """修正非法值"""
+ return value
+
+
+class RangeValidator(ConfigValidator):
+ """范围验证器"""
+
+ def __init__(self, min: int | float, max: int | float):
+ self.min = min
+ self.max = max
+ self.range = (min, max)
+
+ def validate(self, value: Any) -> bool:
+ if not isinstance(value, (int | float)):
+ return False
+ return self.min <= value <= self.max
+
+ def correct(self, value: Any) -> int | float:
+ if not isinstance(value, (int, float)):
+ try:
+ value = float(value)
+ except TypeError:
+ return self.min
+ return min(max(self.min, value), self.max)
+
+
+class OptionsValidator(ConfigValidator):
+ """选项验证器"""
+
+ def __init__(self, options: list):
+ if not options:
+ raise ValueError("可选项不能为空")
+
+ self.options = options
+
+ def validate(self, value: Any) -> bool:
+ return value in self.options
+
+ def correct(self, value: Any) -> Any:
+ return value if self.validate(value) else self.options[0]
+
+
+class UidValidator(ConfigValidator):
+ """UID验证器"""
+
+ def validate(self, value: Any) -> bool:
+ if value is None:
+ return True
+ try:
+ uuid.UUID(value)
+ return True
+ except (TypeError, ValueError):
+ return False
+
+ def correct(self, value: Any) -> Any:
+ return value if self.validate(value) else None
+
+
+class EncryptValidator(ConfigValidator):
+ """加数据验证器"""
+
+ def validate(self, value: Any) -> bool:
+ if not isinstance(value, str):
+ return False
+ try:
+ dpapi_decrypt(value)
+ return True
+ except:
+ return False
+
+ def correct(self, value: Any) -> Any:
+ return value if self.validate(value) else dpapi_encrypt("数据损坏, 请重新设置")
+
+
+class BoolValidator(OptionsValidator):
+ """布尔值验证器"""
+
+ def __init__(self):
+ super().__init__([True, False])
+
+
+class FileValidator(ConfigValidator):
+ """文件路径验证器"""
+
+ def validate(self, value: Any) -> bool:
+ if not isinstance(value, str):
+ return False
+ if not Path(value).is_absolute():
+ return False
+ if Path(value).suffix == ".lnk":
+ return False
+ return True
+
+ def correct(self, value: Any) -> str:
+ if not isinstance(value, str):
+ value = "."
+ if not Path(value).is_absolute():
+ value = Path(value).resolve().as_posix()
+ if Path(value).suffix == ".lnk":
+ try:
+ shell = win32com.client.Dispatch("WScript.Shell")
+ shortcut = shell.CreateShortcut(value)
+ value = shortcut.TargetPath
+ except:
+ pass
+ return Path(value).resolve().as_posix()
+
+
+class FolderValidator(ConfigValidator):
+ """文件夹路径验证器"""
+
+ def validate(self, value: Any) -> bool:
+ if not isinstance(value, str):
+ return False
+ if not Path(value).is_absolute():
+ return False
+ return True
+
+ def correct(self, value: Any) -> str:
+ if not isinstance(value, str):
+ value = "."
+ return Path(value).resolve().as_posix()
+
+
+class ConfigItem:
+ """配置项"""
+
+ def __init__(
+ self,
+ group: str,
+ name: str,
+ default: Any,
+ validator: None | ConfigValidator = None,
+ ):
+ """
+ Parameters
+ ----------
+ group: str
+ 配置项分组名称
+
+ name: str
+ 配置项字段名称
+
+ default: Any
+ 配置项默认值
+
+ validator: ConfigValidator
+ 配置项验证器, 默认为 None, 表示不进行验证
+ """
+ super().__init__()
+ self.group = group
+ self.name = name
+ self.value: Any = default
+ self.validator = validator or ConfigValidator()
+ self.is_locked = False
+
+ self.setValue(default)
+
+ def setValue(self, value: Any):
+ """
+ 设置配置项值, 将自动进行验证和修正
+
+ Parameters
+ ----------
+ value: Any
+ 要设置的值, 可以是任何合法类型
+ """
+
+ if (
+ dpapi_decrypt(self.value)
+ if isinstance(self.validator, EncryptValidator)
+ else self.value
+ ) == value:
+ return
+
+ if self.is_locked:
+ raise ValueError(f"配置项 '{self.group}.{self.name}' 已锁定, 无法修改")
+
+ # deepcopy new value
+ try:
+ self.value = deepcopy(value)
+ except:
+ self.value = value
+
+ if isinstance(self.validator, EncryptValidator):
+ if self.validator.validate(self.value):
+ self.value = self.value
+ else:
+ self.value = dpapi_encrypt(self.value)
+
+ if not self.validator.validate(self.value):
+ self.value = self.validator.correct(self.value)
+
+ def getValue(self) -> Any:
+ """
+ 获取配置项值
+ """
+
+ if isinstance(self.validator, EncryptValidator):
+ return dpapi_decrypt(self.value)
+ return self.value
+
+ def lock(self):
+ """
+ 锁定配置项, 锁定后无法修改配置项值
+ """
+ self.is_locked = True
+
+ def unlock(self):
+ """
+ 解锁配置项, 解锁后可以修改配置项值
+ """
+ self.is_locked = False
+
+
+class ConfigBase:
+ """
+ 配置基类
+
+ 这个类提供了基本的配置项管理功能, 包括连接配置文件、加载配置数据、获取和设置配置项值等。
+
+ 此类不支持直接实例化, 必须通过子类来实现具体的配置项, 请继承此类并在子类中定义具体的配置项。
+ 若将配置项设为类属性, 则所有实例都会共享同一份配置项数据。
+ 若将配置项设为实例属性, 则每个实例都会有独立的配置项数据。
+ 子配置项可以是 `MultipleConfig` 的实例。
+ """
+
+ def __init__(self, if_save_multi_config: bool = True):
+
+ self.file: None | Path = None
+ self.if_save_multi_config = if_save_multi_config
+ self.is_locked = False
+
+ async def connect(self, path: Path):
+ """
+ 将配置文件连接到指定配置文件
+
+ Parameters
+ ----------
+ path: Path
+ 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建
+ """
+
+ if path.suffix != ".json":
+ raise ValueError("配置文件必须是扩展名为 '.json' 的 JSON 文件")
+
+ if self.is_locked:
+ raise ValueError("配置已锁定, 无法修改")
+
+ self.file = path
+
+ if not self.file.exists():
+ self.file.parent.mkdir(parents=True, exist_ok=True)
+ self.file.touch()
+
+ with self.file.open("r", encoding="utf-8") as f:
+ try:
+ data = json.load(f)
+ except json.JSONDecodeError:
+ data = {}
+
+ await self.load(data)
+
+ async def load(self, data: dict):
+ """
+ 从字典加载配置数据
+
+ 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigItem 实例中。
+ 如果字典中包含 "SubConfigsInfo" 键, 则会加载子配置项, 这些子配置项应该是 MultipleConfig 的实例。
+
+ Parameters
+ ----------
+ data: dict
+ 配置数据字典
+ """
+
+ if self.is_locked:
+ raise ValueError("配置已锁定, 无法修改")
+
+ # update the value of config item
+ if data.get("SubConfigsInfo"):
+ for k, v in data["SubConfigsInfo"].items():
+ if hasattr(self, k):
+ sub_config = getattr(self, k)
+ if isinstance(sub_config, MultipleConfig):
+ await sub_config.load(v)
+ data.pop("SubConfigsInfo")
+
+ for group, info in data.items():
+ for name, value in info.items():
+ if hasattr(self, f"{group}_{name}"):
+ configItem = getattr(self, f"{group}_{name}")
+ if isinstance(configItem, ConfigItem):
+ configItem.setValue(value)
+
+ if self.file:
+ await self.save()
+
+ async def toDict(
+ self, ignore_multi_config: bool = False, if_decrypt: bool = True
+ ) -> Dict[str, Any]:
+ """将配置项转换为字典"""
+
+ data = {}
+ for name in dir(self):
+ item = getattr(self, name)
+
+ if isinstance(item, ConfigItem):
+
+ if not data.get(item.group):
+ data[item.group] = {}
+ if item.name:
+ data[item.group][item.name] = (
+ item.getValue() if if_decrypt else item.value
+ )
+
+ elif not ignore_multi_config and isinstance(item, MultipleConfig):
+
+ if not data.get("SubConfigsInfo"):
+ data["SubConfigsInfo"] = {}
+ data["SubConfigsInfo"][name] = await item.toDict()
+
+ return data
+
+ def get(self, group: str, name: str) -> Any:
+ """获取配置项的值"""
+
+ if not hasattr(self, f"{group}_{name}"):
+ raise AttributeError(f"配置项 '{group}.{name}' 不存在")
+
+ configItem = getattr(self, f"{group}_{name}")
+ if isinstance(configItem, ConfigItem):
+ return configItem.getValue()
+ else:
+ raise TypeError(f"配置项 '{group}.{name}' 不是 ConfigItem 实例")
+
+ async def set(self, group: str, name: str, value: Any):
+ """
+ 设置配置项的值
+
+ Parameters
+ ----------
+ group: str
+ 配置项分组名称
+ name: str
+ 配置项名称
+ value: Any
+ 配置项新值
+ """
+
+ if not hasattr(self, f"{group}_{name}"):
+ raise AttributeError(f"配置项 '{group}.{name}' 不存在")
+
+ configItem = getattr(self, f"{group}_{name}")
+ if isinstance(configItem, ConfigItem):
+ configItem.setValue(value)
+ if self.file:
+ await self.save()
+ else:
+ raise TypeError(f"配置项 '{group}.{name}' 不是 ConfigItem 实例")
+
+ async def save(self):
+ """保存配置"""
+
+ if not self.file:
+ raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件")
+
+ self.file.parent.mkdir(parents=True, exist_ok=True)
+ with self.file.open("w", encoding="utf-8") as f:
+ json.dump(
+ await self.toDict(not self.if_save_multi_config, if_decrypt=False),
+ f,
+ ensure_ascii=False,
+ indent=4,
+ )
+
+ async def lock(self):
+ """
+ 锁定配置项, 锁定后无法修改配置项值
+ """
+
+ self.is_locked = True
+
+ for name in dir(self):
+ item = getattr(self, name)
+ if isinstance(item, ConfigItem):
+ item.lock()
+ elif isinstance(item, MultipleConfig):
+ await item.lock()
+
+ async def unlock(self):
+ """
+ 解锁配置项, 解锁后可以修改配置项值
+ """
+
+ self.is_locked = False
+
+ for name in dir(self):
+ item = getattr(self, name)
+ if isinstance(item, ConfigItem):
+ item.unlock()
+ elif isinstance(item, MultipleConfig):
+ await item.unlock()
+
+
+class MultipleConfig:
+ """
+ 多配置项管理类
+
+ 这个类允许管理多个配置项实例, 可以添加、删除、修改配置项, 并将其保存到 JSON 文件中。
+ 允许通过 `config[uuid]` 访问配置项, 使用 `uuid in config` 检查是否存在配置项, 使用 `len(config)` 获取配置项数量。
+
+ Parameters
+ ----------
+ sub_config_type: List[type]
+ 子配置项的类型列表, 必须是 ConfigBase 的子类
+ """
+
+ def __init__(self, sub_config_type: List[type]):
+
+ if not sub_config_type:
+ raise ValueError("子配置项类型列表不能为空")
+
+ for config_type in sub_config_type:
+ if not issubclass(config_type, ConfigBase):
+ raise TypeError(
+ f"配置类型 {config_type.__name__} 必须是 ConfigBase 的子类"
+ )
+
+ self.sub_config_type = sub_config_type
+ self.file: None | Path = None
+ self.order: List[uuid.UUID] = []
+ self.data: Dict[uuid.UUID, ConfigBase] = {}
+ self.is_locked = False
+
+ def __getitem__(self, key: uuid.UUID) -> ConfigBase:
+ """允许通过 config[uuid] 访问配置项"""
+ if key not in self.data:
+ raise KeyError(f"配置项 '{key}' 不存在")
+ return self.data[key]
+
+ def __contains__(self, key: uuid.UUID) -> bool:
+ """允许使用 uuid in config 检查是否存在"""
+ return key in self.data
+
+ def __len__(self) -> int:
+ """允许使用 len(config) 获取配置项数量"""
+ return len(self.data)
+
+ def __repr__(self) -> str:
+ """更好的字符串表示"""
+ return f"MultipleConfig(items={len(self.data)}, types={[t.__name__ for t in self.sub_config_type]})"
+
+ def __str__(self) -> str:
+ """用户友好的字符串表示"""
+ return f"MultipleConfig with {len(self.data)} items"
+
+ async def connect(self, path: Path):
+ """
+ 将配置文件连接到指定配置文件
+
+ Parameters
+ ----------
+ path: Path
+ 配置文件路径, 必须为 JSON 文件, 如果不存在则会创建
+ """
+
+ if path.suffix != ".json":
+ raise ValueError("配置文件必须是带有 '.json' 扩展名的 JSON 文件。")
+
+ if self.is_locked:
+ raise ValueError("配置已锁定, 无法修改")
+
+ self.file = path
+
+ if not self.file.exists():
+ self.file.parent.mkdir(parents=True, exist_ok=True)
+ self.file.touch()
+
+ with self.file.open("r", encoding="utf-8") as f:
+ try:
+ data = json.load(f)
+ except json.JSONDecodeError:
+ data = {}
+
+ await self.load(data)
+
+ async def load(self, data: dict):
+ """
+ 从字典加载配置数据
+
+ 这个方法会遍历字典中的配置项, 并将其设置到对应的 ConfigBase 实例中。
+ 如果字典中包含 "instances" 键, 则会加载子配置项, 这些子配置项应该是 ConfigBase 子类的实例。
+ 如果字典中没有 "instances" 键, 则清空当前配置项。
+
+ Parameters
+ ----------
+ data: dict
+ 配置数据字典
+ """
+
+ if self.is_locked:
+ raise ValueError("配置已锁定, 无法修改")
+
+ if not data.get("instances"):
+ self.order = []
+ self.data = {}
+ return
+
+ self.order = []
+ self.data = {}
+
+ for instance in data["instances"]:
+
+ if not isinstance(instance, dict) or not data.get(instance.get("uid")):
+ continue
+
+ type_name = instance.get("type", self.sub_config_type[0].__name__)
+
+ for class_type in self.sub_config_type:
+
+ if class_type.__name__ == type_name:
+ self.order.append(uuid.UUID(instance["uid"]))
+ self.data[self.order[-1]] = class_type()
+ await self.data[self.order[-1]].load(data[instance["uid"]])
+ break
+
+ else:
+
+ raise ValueError(f"未知的子配置类型: {type_name}")
+
+ if self.file:
+ await self.save()
+
+ async def toDict(self) -> Dict[str, Union[list, dict]]:
+ """
+ 将配置项转换为字典
+
+ 返回一个字典, 包含所有配置项的 UID 和类型, 以及每个配置项的具体数据。
+ """
+
+ data: Dict[str, Union[list, dict]] = {
+ "instances": [
+ {"uid": str(_), "type": type(self.data[_]).__name__} for _ in self.order
+ ]
+ }
+ for uid, config in self.items():
+ data[str(uid)] = await config.toDict()
+ return data
+
+ async def get(self, uid: uuid.UUID) -> Dict[str, Union[list, dict]]:
+ """
+ 获取指定 UID 的配置项
+
+ Parameters
+ ----------
+ uid: uuid.UUID
+ 要获取的配置项的唯一标识符
+ Returns
+ -------
+ Dict[str, Union[list, dict]]
+ 对应的配置项数据字典
+ """
+
+ if uid not in self.data:
+ raise ValueError(f"配置项 '{uid}' 不存在。")
+
+ data: Dict[str, Union[list, dict]] = {
+ "instances": [
+ {"uid": str(_), "type": type(self.data[_]).__name__}
+ for _ in self.order
+ if _ == uid
+ ]
+ }
+ data[str(uid)] = await self.data[uid].toDict()
+
+ return data
+
+ async def save(self):
+ """保存配置"""
+
+ if not self.file:
+ raise ValueError("文件路径未设置, 请先调用 `connect` 方法连接配置文件")
+
+ self.file.parent.mkdir(parents=True, exist_ok=True)
+ with self.file.open("w", encoding="utf-8") as f:
+ json.dump(await self.toDict(), f, ensure_ascii=False, indent=4)
+
+ async def add(self, config_type: type) -> tuple[uuid.UUID, ConfigBase]:
+ """
+ 添加一个新的配置项
+
+ Parameters
+ ----------
+ config_type: type
+ 配置项的类型, 必须是初始化时已声明的 ConfigBase 子类
+
+ Returns
+ -------
+ tuple[uuid.UUID, ConfigBase]
+ 新创建的配置项的唯一标识符和实例
+ """
+
+ if config_type not in self.sub_config_type:
+ raise ValueError(f"配置类型 {config_type.__name__} 不被允许")
+
+ uid = uuid.uuid4()
+ self.order.append(uid)
+ self.data[uid] = config_type()
+
+ if self.file:
+ await self.save()
+
+ return uid, self.data[uid]
+
+ async def remove(self, uid: uuid.UUID):
+ """
+ 移除配置项
+
+ Parameters
+ ----------
+ uid: uuid.UUID
+ 要移除的配置项的唯一标识符
+ """
+
+ if self.is_locked:
+ raise ValueError("配置已锁定, 无法修改")
+
+ if uid not in self.data:
+ raise ValueError(f"配置项 '{uid}' 不存在")
+
+ if self.data[uid].is_locked:
+ raise ValueError(f"配置项 '{uid}' 已锁定, 无法移除")
+
+ self.data.pop(uid)
+ self.order.remove(uid)
+
+ if self.file:
+ await self.save()
+
+ async def setOrder(self, order: List[uuid.UUID]):
+ """
+ 设置配置项的顺序
+
+ Parameters
+ ----------
+ order: List[uuid.UUID]
+ 新的配置项顺序
+ """
+
+ if set(order) != set(self.data.keys()):
+ raise ValueError("顺序与当前配置项不匹配")
+
+ self.order = order
+
+ if self.file:
+ await self.save()
+
+ async def lock(self):
+ """
+ 锁定配置项, 锁定后无法修改配置项值
+ """
+
+ self.is_locked = True
+
+ for item in self.values():
+ await item.lock()
+
+ async def unlock(self):
+ """
+ 解锁配置项, 解锁后可以修改配置项值
+ """
+
+ self.is_locked = False
+
+ for item in self.values():
+ await item.unlock()
+
+ def keys(self):
+ """返回配置项的所有唯一标识符"""
+
+ return iter(self.order)
+
+ def values(self):
+ """返回配置项的所有实例"""
+
+ if not self.data:
+ return iter([])
+
+ return iter([self.data[_] for _ in self.order])
+
+ def items(self):
+ """返回配置项的所有唯一标识符和实例的元组"""
+
+ return zip(self.keys(), self.values())
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
index fdb7653..73060c4 100644
--- a/app/models/__init__.py
+++ b/app/models/__init__.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,18 +19,11 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA模组包
-v4.4
-作者:DLmaster_361
-"""
-
-__version__ = "4.2.0"
+__version__ = "5.0.0"
__author__ = "DLmaster361 "
__license__ = "GPL-3.0 license"
-from .general import GeneralManager
-from .MAA import MaaManager
+from .ConfigBase import *
+from .schema import *
-__all__ = ["GeneralManager", "MaaManager"]
+__all__ = ["ConfigBase", "schema"]
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/models/schema.py b/app/models/schema.py
new file mode 100644
index 0000000..9aa87f2
--- /dev/null
+++ b/app/models/schema.py
@@ -0,0 +1,779 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+from pydantic import BaseModel, Field
+from typing import Any, Dict, List, Union, Optional, Literal
+
+
+class OutBase(BaseModel):
+ code: int = Field(default=200, description="状态码")
+ status: str = Field(default="success", description="操作状态")
+ message: str = Field(default="操作成功", description="操作消息")
+
+
+class InfoOut(OutBase):
+ data: Dict[str, Any] = Field(..., description="收到的服务器数据")
+
+
+class NoticeOut(OutBase):
+ if_need_show: bool = Field(..., description="是否需要显示公告")
+ data: Dict[str, str] = Field(
+ ..., description="公告信息, key为公告标题, value为公告内容"
+ )
+
+
+class ComboBoxItem(BaseModel):
+ label: str = Field(..., description="展示值")
+ value: Optional[str] = Field(..., description="实际值")
+
+
+class ComboBoxOut(OutBase):
+ data: List[ComboBoxItem] = Field(..., description="下拉框选项")
+
+
+class GetStageIn(BaseModel):
+ type: Literal[
+ "Today",
+ "ALL",
+ "Monday",
+ "Tuesday",
+ "Wednesday",
+ "Thursday",
+ "Friday",
+ "Saturday",
+ "Sunday",
+ ] = Field(
+ ...,
+ description="选择的日期类型, Today为当天, ALL为包含当天未开放关卡在内的所有项",
+ )
+
+
+class GlobalConfig_Function(BaseModel):
+ HistoryRetentionTime: Optional[Literal[7, 15, 30, 60, 90, 180, 365, 0]] = Field(
+ None, description="历史记录保留时间, 0表示永久保存"
+ )
+ IfAllowSleep: Optional[bool] = Field(default=None, description="允许休眠")
+ IfSilence: Optional[bool] = Field(default=None, description="静默模式")
+ BossKey: Optional[str] = Field(default=None, description="模拟器老板键")
+ IfAgreeBilibili: Optional[bool] = Field(
+ default=None, description="同意哔哩哔哩用户协议"
+ )
+ IfSkipMumuSplashAds: Optional[bool] = Field(
+ default=None, description="跳过Mumu模拟器启动广告"
+ )
+
+
+class GlobalConfig_Voice(BaseModel):
+ Enabled: Optional[bool] = Field(default=None, description="语音功能是否启用")
+ Type: Optional[Literal["simple", "noisy"]] = Field(
+ default=None, description="语音类型, simple为简洁, noisy为聒噪"
+ )
+
+
+class GlobalConfig_Start(BaseModel):
+ IfSelfStart: Optional[bool] = Field(
+ default=None, description="是否在系统启动时自动运行"
+ )
+ IfMinimizeDirectly: Optional[bool] = Field(
+ default=None, description="启动时是否直接最小化到托盘而不显示主窗口"
+ )
+
+
+class GlobalConfig_UI(BaseModel):
+ IfShowTray: Optional[bool] = Field(default=None, description="是否常态显示托盘图标")
+ IfToTray: Optional[bool] = Field(default=None, description="是否最小化到托盘")
+
+
+class GlobalConfig_Notify(BaseModel):
+ SendTaskResultTime: Optional[Literal["不推送", "任何时刻", "仅失败时"]] = Field(
+ default=None, description="任务结果推送时机"
+ )
+ IfSendStatistic: Optional[bool] = Field(
+ default=None, description="是否发送统计信息"
+ )
+ IfSendSixStar: Optional[bool] = Field(
+ default=None, description="是否发送公招六星通知"
+ )
+ IfPushPlyer: Optional[bool] = Field(default=None, description="是否推送系统通知")
+ IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
+ SMTPServerAddress: Optional[str] = Field(default=None, description="SMTP服务器地址")
+ AuthorizationCode: Optional[str] = Field(default=None, description="SMTP授权码")
+ FromAddress: Optional[str] = Field(default=None, description="邮件发送地址")
+ ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
+ IfServerChan: Optional[bool] = Field(
+ default=None, description="是否使用ServerChan推送"
+ )
+ ServerChanKey: Optional[str] = Field(default=None, description="ServerChan推送密钥")
+ IfCompanyWebHookBot: Optional[bool] = Field(
+ default=None, description="是否使用企微Webhook推送"
+ )
+ CompanyWebHookBotUrl: Optional[str] = Field(
+ default=None, description="企微Webhook Bot URL"
+ )
+
+
+class GlobalConfig_Update(BaseModel):
+ IfAutoUpdate: Optional[bool] = Field(default=None, description="是否自动更新")
+ UpdateType: Optional[Literal["stable", "beta"]] = Field(
+ default=None, description="更新类型, stable为稳定版, beta为测试版"
+ )
+ Source: Optional[Literal["GitHub", "MirrorChyan", "AutoSite"]] = Field(
+ default=None, description="更新源: GitHub源, Mirror酱源, 自建源"
+ )
+ ProxyAddress: Optional[str] = Field(default=None, description="网络代理地址")
+ MirrorChyanCDK: Optional[str] = Field(default=None, description="Mirror酱CDK")
+
+
+class GlobalConfig(BaseModel):
+ Function: Optional[GlobalConfig_Function] = Field(
+ default=None, description="功能相关配置"
+ )
+ Voice: Optional[GlobalConfig_Voice] = Field(
+ default=None, description="语音相关配置"
+ )
+ Start: Optional[GlobalConfig_Start] = Field(
+ default=None, description="启动相关配置"
+ )
+ UI: Optional[GlobalConfig_UI] = Field(default=None, description="界面相关配置")
+ Notify: Optional[GlobalConfig_Notify] = Field(
+ default=None, description="通知相关配置"
+ )
+ Update: Optional[GlobalConfig_Update] = Field(
+ default=None, description="更新相关配置"
+ )
+
+
+class QueueIndexItem(BaseModel):
+ uid: str = Field(..., description="唯一标识符")
+ type: Literal["QueueConfig"] = Field(..., description="配置类型")
+
+
+class QueueItemIndexItem(BaseModel):
+ uid: str = Field(..., description="唯一标识符")
+ type: Literal["QueueItem"] = Field(..., description="配置类型")
+
+
+class TimeSetIndexItem(BaseModel):
+ uid: str = Field(..., description="唯一标识符")
+ type: Literal["TimeSet"] = Field(..., description="配置类型")
+
+
+class QueueItem_Info(BaseModel):
+ ScriptId: Optional[str] = Field(
+ default=None, description="任务所对应的脚本ID, 为None时表示未选择"
+ )
+
+
+class QueueItem(BaseModel):
+ Info: Optional[QueueItem_Info] = Field(default=None, description="队列项")
+
+
+class TimeSet_Info(BaseModel):
+ Enabled: Optional[bool] = Field(default=None, description="是否启用")
+ Time: Optional[str] = Field(default=None, description="时间设置, 格式为HH:MM")
+
+
+class TimeSet(BaseModel):
+ Info: Optional[TimeSet_Info] = Field(default=None, description="时间项")
+
+
+class QueueConfig_Info(BaseModel):
+ Name: Optional[str] = Field(default=None, description="队列名称")
+ TimeEnabled: Optional[bool] = Field(default=None, description="是否启用定时")
+ StartUpEnabled: Optional[bool] = Field(default=None, description="是否启动时运行")
+ AfterAccomplish: Optional[
+ Literal[
+ "NoAction", "KillSelf", "Sleep", "Hibernate", "Shutdown", "ShutdownForce"
+ ]
+ ] = Field(default=None, description="完成后操作")
+
+
+class QueueConfig(BaseModel):
+ Info: Optional[QueueConfig_Info] = Field(default=None, description="队列信息")
+
+
+class ScriptIndexItem(BaseModel):
+ uid: str = Field(..., description="唯一标识符")
+ type: Literal["MaaConfig", "GeneralConfig"] = Field(..., description="配置类型")
+
+
+class UserIndexItem(BaseModel):
+ uid: str = Field(..., description="唯一标识符")
+ type: Literal["MaaUserConfig", "GeneralUserConfig"] = Field(
+ ..., description="配置类型"
+ )
+
+
+class MaaUserConfig_Info(BaseModel):
+ Name: Optional[str] = Field(default=None, description="用户名")
+ Id: Optional[str] = Field(default=None, description="用户ID")
+ Mode: Optional[Literal["简洁", "详细"]] = Field(
+ default=None, description="用户配置模式"
+ )
+ StageMode: Optional[str] = Field(default=None, description="关卡配置模式")
+ Server: Optional[
+ Literal["Official", "Bilibili", "YoStarEN", "YoStarJP", "YoStarKR", "txwy"]
+ ] = Field(default=None, description="服务器")
+ Status: Optional[bool] = Field(default=None, description="用户状态")
+ RemainedDay: Optional[int] = Field(default=None, description="剩余天数")
+ Annihilation: Optional[
+ Literal[
+ "Close",
+ "Annihilation",
+ "Chernobog@Annihilation",
+ "LungmenOutskirts@Annihilation",
+ "LungmenDowntown@Annihilation",
+ ]
+ ] = Field(default=None, description="剿灭模式")
+ Routine: Optional[bool] = Field(default=None, description="是否启用日常")
+ InfrastMode: Optional[Literal["Normal", "Rotation", "Custom"]] = Field(
+ default=None, description="基建模式"
+ )
+ InfrastPath: Optional[str] = Field(default=None, description="自定义基建文件路径")
+ Password: Optional[str] = Field(default=None, description="密码")
+ Notes: Optional[str] = Field(default=None, description="备注")
+ MedicineNumb: Optional[int] = Field(default=None, description="吃理智药数量")
+ SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field(
+ default=None, description="连战次数"
+ )
+ Stage: Optional[str] = Field(default=None, description="关卡选择")
+ Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1")
+ Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2")
+ Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3")
+ Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡")
+ IfSkland: Optional[bool] = Field(default=None, description="是否启用森空岛签到")
+ SklandToken: Optional[str] = Field(default=None, description="SklandToken")
+
+
+class MaaUserConfig_Data(BaseModel):
+ LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期")
+ LastAnnihilationDate: Optional[str] = Field(
+ default=None, description="上次剿灭日期"
+ )
+ LastSklandDate: Optional[str] = Field(
+ default=None, description="上次森空岛签到日期"
+ )
+ ProxyTimes: Optional[int] = Field(default=None, description="代理次数")
+ IfPassCheck: Optional[bool] = Field(default=None, description="是否通过人工排查")
+
+
+class MaaUserConfig_Task(BaseModel):
+ IfWakeUp: Optional[bool] = Field(default=None, description="开始唤醒")
+ IfRecruiting: Optional[bool] = Field(default=None, description="自动公招")
+ IfBase: Optional[bool] = Field(default=None, description="基建换班")
+ IfCombat: Optional[bool] = Field(default=None, description="刷理智")
+ IfMall: Optional[bool] = Field(default=None, description="获取信用及购物")
+ IfMission: Optional[bool] = Field(default=None, description="领取奖励")
+ IfAutoRoguelike: Optional[bool] = Field(default=None, description="自动肉鸽")
+ IfReclamation: Optional[bool] = Field(default=None, description="生息演算")
+
+
+class UserConfig_Notify(BaseModel):
+ Enabled: Optional[bool] = Field(default=None, description="是否启用通知")
+ IfSendStatistic: Optional[bool] = Field(
+ default=None, description="是否发送统计信息"
+ )
+ IfSendSixStar: Optional[bool] = Field(default=None, description="是否发送高资喜报")
+ IfSendMail: Optional[bool] = Field(default=None, description="是否发送邮件通知")
+ ToAddress: Optional[str] = Field(default=None, description="邮件接收地址")
+ IfServerChan: Optional[bool] = Field(
+ default=None, description="是否使用Server酱推送"
+ )
+ ServerChanKey: Optional[str] = Field(default=None, description="ServerChanKey")
+ IfCompanyWebHookBot: Optional[bool] = Field(
+ default=None, description="是否使用Webhook推送"
+ )
+ CompanyWebHookBotUrl: Optional[str] = Field(
+ default=None, description="企微Webhook Bot URL"
+ )
+
+
+class MaaUserConfig(BaseModel):
+ Info: Optional[MaaUserConfig_Info] = Field(default=None, description="基础信息")
+ Data: Optional[MaaUserConfig_Data] = Field(default=None, description="用户数据")
+ Task: Optional[MaaUserConfig_Task] = Field(default=None, description="任务列表")
+ Notify: Optional[UserConfig_Notify] = Field(default=None, description="单独通知")
+
+
+class MaaConfig_Info(BaseModel):
+ Name: Optional[str] = Field(default=None, description="脚本名称")
+ Path: Optional[str] = Field(default=None, description="脚本路径")
+
+
+class MaaConfig_Run(BaseModel):
+ TaskTransitionMethod: Optional[Literal["NoAction", "ExitGame", "ExitEmulator"]] = (
+ Field(default=None, description="简洁任务间切换方式")
+ )
+ ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制")
+ ADBSearchRange: Optional[int] = Field(default=None, description="ADB端口搜索范围")
+ RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制")
+ AnnihilationTimeLimit: Optional[int] = Field(
+ default=None, description="剿灭超时限制"
+ )
+ RoutineTimeLimit: Optional[int] = Field(default=None, description="日常超时限制")
+ AnnihilationWeeklyLimit: Optional[bool] = Field(
+ default=None, description="剿灭每周仅代理至上限"
+ )
+
+
+class MaaConfig(BaseModel):
+ Info: Optional[MaaConfig_Info] = Field(default=None, description="脚本基础信息")
+ Run: Optional[MaaConfig_Run] = Field(default=None, description="脚本运行配置")
+
+
+class GeneralUserConfig_Info(BaseModel):
+
+ Name: Optional[str] = Field(default=None, description="用户名")
+ Status: Optional[bool] = Field(default=None, description="用户状态")
+ RemainedDay: Optional[int] = Field(default=None, description="剩余天数")
+ IfScriptBeforeTask: Optional[bool] = Field(
+ default=None, description="是否在任务前执行脚本"
+ )
+ ScriptBeforeTask: Optional[str] = Field(default=None, description="任务前脚本路径")
+ IfScriptAfterTask: Optional[bool] = Field(
+ default=None, description="是否在任务后执行脚本"
+ )
+ ScriptAfterTask: Optional[str] = Field(default=None, description="任务后脚本路径")
+ Notes: Optional[str] = Field(default=None, description="备注")
+
+
+class GeneralUserConfig_Data(BaseModel):
+ LastProxyDate: Optional[str] = Field(default=None, description="上次代理日期")
+ ProxyTimes: Optional[int] = Field(default=None, description="代理次数")
+
+
+class GeneralUserConfig(BaseModel):
+ Info: Optional[GeneralUserConfig_Info] = Field(default=None, description="用户信息")
+ Data: Optional[GeneralUserConfig_Data] = Field(default=None, description="用户数据")
+ Notify: Optional[UserConfig_Notify] = Field(default=None, description="单独通知")
+
+
+class GeneralConfig_Info(BaseModel):
+ Name: Optional[str] = Field(default=None, description="脚本名称")
+ RootPath: Optional[str] = Field(default=None, description="脚本根目录")
+
+
+class GeneralConfig_Script(BaseModel):
+ ScriptPath: Optional[str] = Field(default=None, description="脚本可执行文件路径")
+ Arguments: Optional[str] = Field(default=None, description="脚本启动附加命令参数")
+ IfTrackProcess: Optional[bool] = Field(
+ default=None, description="是否追踪脚本子进程"
+ )
+ ConfigPath: Optional[str] = Field(default=None, description="配置文件路径")
+ ConfigPathMode: Optional[Literal["File", "Folder"]] = Field(
+ default=None, description="配置文件类型: 单个文件, 文件夹"
+ )
+ UpdateConfigMode: Optional[Literal["Never", "Success", "Failure", "Always"]] = (
+ Field(
+ default=None,
+ description="更新配置时机, 从不, 仅成功时, 仅失败时, 任务结束时",
+ )
+ )
+ LogPath: Optional[str] = Field(default=None, description="日志文件路径")
+ LogPathFormat: Optional[str] = Field(default=None, description="日志文件名格式")
+ LogTimeStart: Optional[int] = Field(default=None, description="日志时间戳开始位置")
+ LogTimeEnd: Optional[int] = Field(default=None, description="日志时间戳结束位置")
+ LogTimeFormat: Optional[str] = Field(default=None, description="日志时间戳格式")
+ SuccessLog: Optional[str] = Field(default=None, description="成功时日志")
+ ErrorLog: Optional[str] = Field(default=None, description="错误时日志")
+
+
+class GeneralConfig_Game(BaseModel):
+ Enabled: Optional[bool] = Field(
+ default=None, description="游戏/模拟器相关功能是否启用"
+ )
+ Type: Optional[Literal["Emulator", "Client"]] = Field(
+ default=None, description="类型: 模拟器, PC端"
+ )
+ Path: Optional[str] = Field(default=None, description="游戏/模拟器程序路径")
+ Arguments: Optional[str] = Field(default=None, description="游戏/模拟器启动参数")
+ WaitTime: Optional[int] = Field(default=None, description="游戏/模拟器等待启动时间")
+ IfForceClose: Optional[bool] = Field(
+ default=None, description="是否强制关闭游戏/模拟器进程"
+ )
+
+
+class GeneralConfig_Run(BaseModel):
+ ProxyTimesLimit: Optional[int] = Field(default=None, description="每日代理次数限制")
+ RunTimesLimit: Optional[int] = Field(default=None, description="重试次数限制")
+ RunTimeLimit: Optional[int] = Field(default=None, description="日志超时限制")
+
+
+class GeneralConfig(BaseModel):
+
+ Info: Optional[GeneralConfig_Info] = Field(default=None, description="脚本基础信息")
+ Script: Optional[GeneralConfig_Script] = Field(default=None, description="脚本配置")
+ Game: Optional[GeneralConfig_Game] = Field(default=None, description="游戏配置")
+ Run: Optional[GeneralConfig_Run] = Field(default=None, description="运行配置")
+
+
+class PlanIndexItem(BaseModel):
+ uid: str = Field(..., description="唯一标识符")
+ type: Literal["MaaPlanConfig"] = Field(..., description="配置类型")
+
+
+class MaaPlanConfig_Info(BaseModel):
+ Name: Optional[str] = Field(default=None, description="计划表名称")
+ Mode: Optional[Literal["ALL", "Weekly"]] = Field(
+ default=None, description="计划表模式"
+ )
+
+
+class MaaPlanConfig_Item(BaseModel):
+ MedicineNumb: Optional[int] = Field(default=None, description="吃理智药")
+ SeriesNumb: Optional[Literal["0", "6", "5", "4", "3", "2", "1", "-1"]] = Field(
+ None, description="连战次数"
+ )
+ Stage: Optional[str] = Field(default=None, description="关卡选择")
+ Stage_1: Optional[str] = Field(default=None, description="备选关卡 - 1")
+ Stage_2: Optional[str] = Field(default=None, description="备选关卡 - 2")
+ Stage_3: Optional[str] = Field(default=None, description="备选关卡 - 3")
+ Stage_Remain: Optional[str] = Field(default=None, description="剩余理智关卡")
+
+
+class MaaPlanConfig(BaseModel):
+
+ Info: Optional[MaaPlanConfig_Info] = Field(default=None, description="基础信息")
+ ALL: Optional[MaaPlanConfig_Item] = Field(default=None, description="全局")
+ Monday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周一")
+ Tuesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周二")
+ Wednesday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周三")
+ Thursday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周四")
+ Friday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周五")
+ Saturday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周六")
+ Sunday: Optional[MaaPlanConfig_Item] = Field(default=None, description="周日")
+
+
+class HistoryIndexItem(BaseModel):
+ date: str = Field(..., description="日期")
+ status: Literal["完成", "异常"] = Field(..., description="状态")
+ jsonFile: str = Field(..., description="对应JSON文件")
+
+
+class HistoryData(BaseModel):
+ index: Optional[List[HistoryIndexItem]] = Field(
+ default=None, description="历史记录索引列表"
+ )
+ recruit_statistics: Optional[Dict[str, int]] = Field(
+ default=None, description="公招统计数据, key为星级, value为对应的公招数量"
+ )
+ drop_statistics: Optional[Dict[str, Dict[str, int]]] = Field(
+ default=None,
+ description="掉落统计数据, 格式为 { '关卡号': { '掉落物': 数量 } }",
+ )
+ error_info: Optional[Dict[str, str]] = Field(
+ default=None, description="报错信息, key为时间戳, value为错误描述"
+ )
+ log_content: Optional[str] = Field(
+ default=None, description="日志内容, 仅在提取单条历史记录数据时返回"
+ )
+
+
+class ScriptCreateIn(BaseModel):
+ type: Literal["MAA", "General"] = Field(
+ ..., description="脚本类型: MAA脚本, 通用脚本"
+ )
+
+
+class ScriptCreateOut(OutBase):
+ scriptId: str = Field(..., description="新创建的脚本ID")
+ data: Union[MaaConfig, GeneralConfig] = Field(..., description="脚本配置数据")
+
+
+class ScriptGetIn(BaseModel):
+ scriptId: Optional[str] = Field(
+ default=None, description="脚本ID, 未携带时表示获取所有脚本数据"
+ )
+
+
+class ScriptGetOut(OutBase):
+ index: List[ScriptIndexItem] = Field(..., description="脚本索引列表")
+ data: Dict[str, Union[MaaConfig, GeneralConfig]] = Field(
+ ..., description="脚本数据字典, key来自于index列表的uid"
+ )
+
+
+class ScriptUpdateIn(BaseModel):
+ scriptId: str = Field(..., description="脚本ID")
+ data: Union[MaaConfig, GeneralConfig] = Field(..., description="脚本更新数据")
+
+
+class ScriptDeleteIn(BaseModel):
+ scriptId: str = Field(..., description="脚本ID")
+
+
+class ScriptReorderIn(BaseModel):
+ indexList: List[str] = Field(..., description="脚本ID列表, 按新顺序排列")
+
+
+class ScriptFileIn(BaseModel):
+ scriptId: str = Field(..., description="脚本ID")
+ jsonFile: str = Field(..., description="配置文件路径")
+
+
+class ScriptUrlIn(BaseModel):
+ scriptId: str = Field(..., description="脚本ID")
+ url: str = Field(..., description="配置文件URL")
+
+
+class ScriptUploadIn(BaseModel):
+ scriptId: str = Field(..., description="脚本ID")
+ config_name: str = Field(..., description="配置名称")
+ author: str = Field(..., description="作者")
+ description: str = Field(..., description="描述")
+
+
+class UserInBase(BaseModel):
+ scriptId: str = Field(..., description="所属脚本ID")
+
+
+class UserGetIn(UserInBase):
+ userId: Optional[str] = Field(
+ default=None, description="用户ID, 未携带时表示获取所有用户数据"
+ )
+
+
+class UserGetOut(OutBase):
+ index: List[UserIndexItem] = Field(..., description="用户索引列表")
+ data: Dict[str, Union[MaaUserConfig, GeneralUserConfig]] = Field(
+ ..., description="用户数据字典, key来自于index列表的uid"
+ )
+
+
+class UserCreateOut(OutBase):
+ userId: str = Field(..., description="新创建的用户ID")
+ data: Union[MaaUserConfig, GeneralUserConfig] = Field(
+ ..., description="用户配置数据"
+ )
+
+
+class UserUpdateIn(UserInBase):
+ userId: str = Field(..., description="用户ID")
+ data: Union[MaaUserConfig, GeneralUserConfig] = Field(
+ ..., description="用户更新数据"
+ )
+
+
+class UserDeleteIn(UserInBase):
+ userId: str = Field(..., description="用户ID")
+
+
+class UserReorderIn(UserInBase):
+ indexList: List[str] = Field(..., description="用户ID列表, 按新顺序排列")
+
+
+class UserSetIn(UserInBase):
+ userId: str = Field(..., description="用户ID")
+ jsonFile: str = Field(..., description="JSON文件路径, 用于导入自定义基建文件")
+
+
+class PlanCreateIn(BaseModel):
+ type: Literal["MaaPlan"]
+
+
+class PlanCreateOut(OutBase):
+ planId: str = Field(..., description="新创建的计划ID")
+ data: MaaPlanConfig = Field(..., description="计划配置数据")
+
+
+class PlanGetIn(BaseModel):
+ planId: Optional[str] = Field(
+ default=None, description="计划ID, 未携带时表示获取所有计划数据"
+ )
+
+
+class PlanGetOut(OutBase):
+ index: List[PlanIndexItem] = Field(..., description="计划索引列表")
+ data: Dict[str, MaaPlanConfig] = Field(..., description="计划列表或单个计划数据")
+
+
+class PlanUpdateIn(BaseModel):
+ planId: str = Field(..., description="计划ID")
+ data: MaaPlanConfig = Field(..., description="计划更新数据")
+
+
+class PlanDeleteIn(BaseModel):
+ planId: str = Field(..., description="计划ID")
+
+
+class PlanReorderIn(BaseModel):
+ indexList: List[str] = Field(..., description="计划ID列表, 按新顺序排列")
+
+
+class QueueCreateOut(OutBase):
+ queueId: str = Field(..., description="新创建的队列ID")
+ data: QueueConfig = Field(..., description="队列配置数据")
+
+
+class QueueGetIn(BaseModel):
+ queueId: Optional[str] = Field(
+ default=None, description="队列ID, 未携带时表示获取所有队列数据"
+ )
+
+
+class QueueGetOut(OutBase):
+ index: List[QueueIndexItem] = Field(..., description="队列索引列表")
+ data: Dict[str, QueueConfig] = Field(
+ ..., description="队列数据字典, key来自于index列表的uid"
+ )
+
+
+class QueueUpdateIn(BaseModel):
+ queueId: str = Field(..., description="队列ID")
+ data: QueueConfig = Field(..., description="队列更新数据")
+
+
+class QueueDeleteIn(BaseModel):
+ queueId: str = Field(..., description="队列ID")
+
+
+class QueueReorderIn(BaseModel):
+ indexList: List[str] = Field(..., description="按新顺序排列的调度队列UID列表")
+
+
+class QueueSetInBase(BaseModel):
+ queueId: str = Field(..., description="所属队列ID")
+
+
+class TimeSetGetIn(QueueSetInBase):
+ timeSetId: Optional[str] = Field(
+ default=None, description="时间设置ID, 未携带时表示获取所有时间设置数据"
+ )
+
+
+class TimeSetGetOut(OutBase):
+ index: List[TimeSetIndexItem] = Field(..., description="时间设置索引列表")
+ data: Dict[str, TimeSet] = Field(
+ ..., description="时间设置数据字典, key来自于index列表的uid"
+ )
+
+
+class TimeSetCreateOut(OutBase):
+ timeSetId: str = Field(..., description="新创建的时间设置ID")
+ data: TimeSet = Field(..., description="时间设置配置数据")
+
+
+class TimeSetUpdateIn(QueueSetInBase):
+ timeSetId: str = Field(..., description="时间设置ID")
+ data: TimeSet = Field(..., description="时间设置更新数据")
+
+
+class TimeSetDeleteIn(QueueSetInBase):
+ timeSetId: str = Field(..., description="时间设置ID")
+
+
+class TimeSetReorderIn(QueueSetInBase):
+ indexList: List[str] = Field(..., description="时间设置ID列表, 按新顺序排列")
+
+
+class QueueItemGetIn(QueueSetInBase):
+ queueItemId: Optional[str] = Field(
+ default=None, description="队列项ID, 未携带时表示获取所有队列项数据"
+ )
+
+
+class QueueItemGetOut(OutBase):
+ index: List[QueueItemIndexItem] = Field(..., description="队列项索引列表")
+ data: Dict[str, QueueItem] = Field(
+ ..., description="队列项数据字典, key来自于index列表的uid"
+ )
+
+
+class QueueItemCreateOut(OutBase):
+ queueItemId: str = Field(..., description="新创建的队列项ID")
+ data: QueueItem = Field(..., description="队列项配置数据")
+
+
+class QueueItemUpdateIn(QueueSetInBase):
+ queueItemId: str = Field(..., description="队列项ID")
+ data: QueueItem = Field(..., description="队列项更新数据")
+
+
+class QueueItemDeleteIn(QueueSetInBase):
+ queueItemId: str = Field(..., description="队列项ID")
+
+
+class QueueItemReorderIn(QueueSetInBase):
+ indexList: List[str] = Field(..., description="队列项ID列表, 按新顺序排列")
+
+
+class DispatchIn(BaseModel):
+ taskId: str = Field(
+ ...,
+ description="目标任务ID, 设置类任务可选对应脚本ID或用户ID, 代理类任务可选对应队列ID或脚本ID",
+ )
+
+
+class TaskCreateIn(DispatchIn):
+ mode: Literal["自动代理", "人工排查", "设置脚本"] = Field(
+ ..., description="任务模式"
+ )
+
+
+class TaskCreateOut(OutBase):
+ websocketId: str = Field(..., description="新创建的任务ID")
+
+
+class WebSocketMessage(BaseModel):
+ id: str = Field(..., description="消息ID, 为Main时表示消息来自主进程")
+ type: Literal["Update", "Message", "Info", "Signal"] = Field(
+ ...,
+ description="消息类型 Update: 更新数据, Message: 请求弹出对话框, Info: 需要在UI显示的消息, Signal: 程序信号",
+ )
+ data: Dict[str, Any] = Field(..., description="消息数据, 具体内容根据type类型而定")
+
+
+class PowerIn(BaseModel):
+ signal: Literal[
+ "NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
+ ] = Field(..., description="电源操作信号")
+
+
+class HistorySearchIn(BaseModel):
+ mode: Literal["按日合并", "按周合并", "按月合并"] = Field(
+ ..., description="合并模式"
+ )
+ start_date: str = Field(..., description="开始日期, 格式YYYY-MM-DD")
+ end_date: str = Field(..., description="结束日期, 格式YYYY-MM-DD")
+
+
+class HistorySearchOut(OutBase):
+ data: Dict[str, Dict[str, HistoryData]] = Field(
+ ...,
+ description="历史记录索引数据字典, 格式为 { '日期': { '用户名': [历史记录信息] } }",
+ )
+
+
+class HistoryDataGetIn(BaseModel):
+ jsonPath: str = Field(..., description="需要提取数据的历史记录JSON文件")
+
+
+class HistoryDataGetOut(OutBase):
+ data: HistoryData = Field(..., description="历史记录数据")
+
+
+class SettingGetOut(OutBase):
+ data: GlobalConfig = Field(..., description="全局设置数据")
+
+
+class SettingUpdateIn(BaseModel):
+ data: GlobalConfig = Field(..., description="全局设置需要更新的数据")
diff --git a/app/services/__init__.py b/app/services/__init__.py
index 0bdbea6..bab395d 100644
--- a/app/services/__init__.py
+++ b/app/services/__init__.py
@@ -18,20 +18,11 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA服务包
-v4.4
-作者:DLmaster_361
-"""
-
-__version__ = "4.2.0"
+__version__ = "5.0.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"]
+__all__ = ["Notify", "System"]
diff --git a/app/services/notification.py b/app/services/notification.py
index 0fd13f4..0c1519b 100644
--- a/app/services/notification.py
+++ b/app/services/notification.py
@@ -18,39 +18,28 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA通知服务
-v4.4
-作者:DLmaster_361
-"""
import re
import smtplib
-import time
+import requests
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
+from app.core import Config
+from app.utils import get_logger, ImageUtils
+
+logger = get_logger("通知服务")
-class Notification(QObject):
+class Notification:
- push_info_bar = Signal(str, str, str, int)
-
- def __init__(self, parent=None):
- super().__init__(parent)
+ def __init__(self):
+ super().__init__()
def push_plyer(self, title, message, ticker, t) -> bool:
"""
@@ -63,19 +52,22 @@ class Notification(QObject):
:return: bool
"""
- if Config.get(Config.notify_IfPushPlyer):
+ if Config.get("Notify", "IfPushPlyer"):
- logger.info(f"推送系统通知:{title}", module="通知服务")
+ logger.info(f"推送系统通知: {title}")
- 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,
- )
+ if notification.notify is not None:
+ notification.notify(
+ title=title,
+ message=message,
+ app_name="AUTO_MAA",
+ app_icon=(Path.cwd() / "res/icons/AUTO_MAA.ico").as_posix(),
+ timeout=t,
+ ticker=ticker,
+ toast=True,
+ )
+ else:
+ logger.error("plyer.notification 未正确导入, 无法推送系统通知")
return True
@@ -83,19 +75,19 @@ class Notification(QObject):
"""
推送邮件通知
- :param mode: 邮件内容模式,支持 "文本" 和 "网页"
+ :param mode: 邮件内容模式, 支持 "文本" 和 "网页"
:param title: 邮件标题
:param content: 邮件内容
:param to_address: 收件人地址
"""
if (
- Config.get(Config.notify_SMTPServerAddress) == ""
- or Config.get(Config.notify_AuthorizationCode) == ""
+ Config.get("Notify", "SMTPServerAddress") == ""
+ or Config.get("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),
+ Config.get("Notify", "FromAddress"),
)
)
or not bool(
@@ -106,379 +98,183 @@ class Notification(QObject):
)
):
logger.error(
- "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
- module="通知服务",
+ "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
)
- self.push_info_bar.emit(
- "error",
- "邮件通知推送异常",
- "请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
- -1,
+ raise ValueError(
+ "邮件通知的SMTP服务器地址、授权码、发件人地址或收件人地址未正确配置"
)
- 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)),
+ # 定义邮件正文
+ if mode == "文本":
+ message = MIMEText(content, "plain", "utf-8")
+ elif mode == "网页":
+ message = MIMEMultipart("alternative")
+ message["From"] = formataddr(
+ (
+ Header("AUTO_MAA通知服务", "utf-8").encode(),
+ Config.get("Notify", "FromAddress"),
)
- 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)
+ ) # 发件人显示的名字
+ message["To"] = formataddr(
+ (Header("AUTO_MAA用户", "utf-8").encode(), to_address)
+ ) # 收件人显示的名字
+ message["Subject"] = str(Header(title, "utf-8"))
- def ServerChanPush(
- self, title, content, send_key, tag, channel
- ) -> Union[bool, str]:
+ if mode == "网页":
+ message.attach(MIMEText(content, "html", "utf-8"))
+
+ smtpObj = smtplib.SMTP_SSL(Config.get("Notify", "SMTPServerAddress"), 465)
+ smtpObj.login(
+ Config.get("Notify", "FromAddress"),
+ Config.get("Notify", "AuthorizationCode"),
+ )
+ smtpObj.sendmail(
+ Config.get("Notify", "FromAddress"), to_address, message.as_string()
+ )
+ smtpObj.quit()
+ logger.success(f"邮件发送成功: {title}")
+
+ def ServerChanPush(self, title, content, send_key) -> None:
"""
使用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
+ raise ValueError("ServerChan SendKey 不能为空")
- 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)")
+ # 构造 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:
- url = f"https://sctapi.ftqq.com/{send_key}.send"
+ 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("|")))
- )
+ # 请求发送
+ params = {"title": title, "desp": content}
+ headers = {"Content-Type": "application/json;charset=utf-8"}
- tags = "|".join(_.strip() for _ in tag.split("|"))
- channels = "|".join(_.strip() for _ in channel.split("|"))
+ response = requests.post(
+ url, json=params, headers=headers, timeout=10, proxies=Config.get_proxies()
+ )
+ result = response.json()
- 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 result.get("code") == 0:
+ logger.success(f"Server酱推送通知成功: {title}")
+ else:
+ raise Exception(f"ServerChan 推送通知失败: {response.text}")
- 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]:
+ def WebHookPush(self, title, content, webhook_url) -> None:
"""
- 使用企业微信群机器人推送通知
+ WebHook 推送通知
:param title: 通知标题
:param content: 通知内容
- :param webhook_url: 企业微信群机器人的WebHook地址
- :return: bool or str
+ :param webhook_url: WebHook地址
"""
- if webhook_url == "":
- logger.error("请正确设置企业微信群机器人的WebHook地址", module="通知服务")
- self.push_info_bar.emit(
- "error",
- "企业微信群机器人通知推送异常",
- "请正确设置企业微信群机器人的WebHook地址",
- -1,
- )
- return None
+ if not webhook_url:
+ raise ValueError("WebHook 地址不能为空")
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
+ response = requests.post(
+ url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies()
+ )
+ info = response.json()
if info["errcode"] == 0:
- logger.success(f"企业微信群机器人推送通知成功:{title}", module="通知服务")
- return True
+ logger.success(f"WebHook 推送通知成功: {title}")
else:
- logger.error(f"企业微信群机器人推送通知失败:{info}", module="通知服务")
- self.push_info_bar.emit(
- "error",
- "企业微信群机器人通知推送失败",
- f"使用企业微信群机器人推送通知时出错:{err}",
- -1,
- )
- return f"使用企业微信群机器人推送通知时出错:{err}"
+ raise Exception(f"WebHook 推送通知失败: {response.text}")
- def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> bool:
+ def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> None:
"""
使用企业微信群机器人推送图片通知
:param image_path: 图片文件路径
:param webhook_url: 企业微信群机器人的WebHook地址
- :return: bool
"""
- try:
- # 压缩图片
- ImageUtils.compress_image_if_needed(image_path)
+ if not webhook_url:
+ raise ValueError("webhook URL 不能为空")
- # 检查图片是否存在
- if not image_path.exists():
- logger.error(
- "图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确",
- module="通知服务",
- )
- self.push_info_bar.emit(
- "error",
- "企业微信群机器人通知推送异常",
- "图片不存在或者压缩失败,请检查图片路径是否正确",
- -1,
- )
- return False
+ # 压缩图片
+ ImageUtils.compress_image_if_needed(image_path)
- if not webhook_url:
- logger.error(
- "请正确设置企业微信群机器人的WebHook地址", module="通知服务"
- )
- self.push_info_bar.emit(
- "error",
- "企业微信群机器人通知推送异常",
- "请正确设置企业微信群机器人的WebHook地址",
- -1,
- )
- return False
+ # 检查图片是否存在
+ if not image_path.exists():
+ raise FileNotFoundError(f"文件未找到: {image_path}")
- # 获取图片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
+ # 获取图片base64和md5
+ image_base64 = ImageUtils.get_base64_from_file(str(image_path))
+ image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
- data = {
- "msgtype": "image",
- "image": {"base64": image_base64, "md5": image_md5},
- }
+ 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
+ response = requests.post(
+ url=webhook_url, json=data, timeout=10, proxies=Config.get_proxies()
+ )
+ info = response.json()
- 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
+ if info.get("errcode") == 0:
+ logger.success(f"企业微信群机器人推送图片成功: {image_path.name}")
+ else:
+ raise Exception(f"企业微信群机器人推送图片失败: {response.text}")
- except Exception as e:
- logger.error(f"推送企业微信群机器人图片时发生未知异常:{e}")
- self.push_info_bar.emit(
- "error",
- "企业微信群机器人图片推送失败",
- f"发生未知异常:{e}",
- -1,
- )
- return False
-
- def send_test_notification(self):
+ def send_test_notification(self) -> None:
"""发送测试通知到所有已启用的通知渠道"""
- logger.info("发送测试通知到所有已启用的通知渠道", module="通知服务")
+ logger.info("发送测试通知到所有已启用的通知渠道")
# 发送系统通知
self.push_plyer(
"测试通知",
- "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
+ "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容, 说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
"测试通知",
3,
)
# 发送邮件通知
- if Config.get(Config.notify_IfSendMail):
+ if Config.get("Notify", "IfSendMail"):
self.send_mail(
"文本",
"AUTO_MAA测试通知",
- "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
- Config.get(Config.notify_ToAddress),
+ "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容, 说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
+ Config.get("Notify", "ToAddress"),
)
# 发送Server酱通知
- if Config.get(Config.notify_IfServerChan):
+ if Config.get("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),
+ "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容, 说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
+ Config.get("Notify", "ServerChanKey"),
)
- # 发送企业微信机器人通知
- if Config.get(Config.notify_IfCompanyWebHookBot):
- self.CompanyWebHookBotPush(
+ # 发送WebHook通知
+ if Config.get("Notify", "IfCompanyWebHookBot"):
+ self.WebHookPush(
"AUTO_MAA测试通知",
- "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
- Config.get(Config.notify_CompanyWebHookBotUrl),
+ "这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容, 说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
)
Notify.CompanyWebHookBotPushImage(
- Config.app_path / "resources/images/notification/test_notify.png",
- Config.get(Config.notify_CompanyWebHookBotUrl),
+ Path.cwd() / "res/images/notification/test_notify.png",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
)
- logger.info("测试通知发送完成", module="通知服务")
-
- return True
+ logger.success("测试通知发送完成")
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/system.py b/app/services/system.py
index 36a9fba..707c4d9 100644
--- a/app/services/system.py
+++ b/app/services/system.py
@@ -18,14 +18,7 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA系统服务
-v4.4
-作者:DLmaster_361
-"""
-from PySide6.QtWidgets import QApplication
import sys
import ctypes
import win32gui
@@ -36,8 +29,12 @@ import tempfile
import getpass
from datetime import datetime
from pathlib import Path
+from typing import Literal
-from app.core import Config, logger
+from app.core import Config
+from app.utils.logger import get_logger
+
+logger = get_logger("系统服务")
class _SystemHandler:
@@ -45,15 +42,10 @@ class _SystemHandler:
ES_CONTINUOUS = 0x80000000
ES_SYSTEM_REQUIRED = 0x00000001
- def __init__(self):
-
- self.set_Sleep()
- self.set_SelfStart()
-
- def set_Sleep(self) -> None:
+ async def set_Sleep(self) -> None:
"""同步系统休眠状态"""
- if Config.get(Config.function_IfAllowSleep):
+ if Config.get("Function", "IfAllowSleep"):
# 设置系统电源状态
ctypes.windll.kernel32.SetThreadExecutionState(
self.ES_CONTINUOUS | self.ES_SYSTEM_REQUIRED
@@ -62,10 +54,10 @@ class _SystemHandler:
# 恢复系统电源状态
ctypes.windll.kernel32.SetThreadExecutionState(self.ES_CONTINUOUS)
- def set_SelfStart(self) -> None:
+ async def set_SelfStart(self) -> None:
"""同步开机自启"""
- if Config.get(Config.start_IfSelfStart) and not self.is_startup():
+ if Config.get("Start", "IfSelfStart") and not await self.is_startup():
# 创建任务计划
try:
@@ -116,7 +108,7 @@ class _SystemHandler:
- "{Config.app_path_sys}"
+ "{Path.cwd() / 'AUTO_MAA.exe'}"
"""
@@ -147,14 +139,10 @@ class _SystemHandler:
if result.returncode == 0:
logger.success(
- f"程序自启动任务计划已创建: {Config.app_path_sys}",
- module="系统服务",
+ f"程序自启动任务计划已创建: {Path.cwd() / 'AUTO_MAA.exe'}"
)
else:
- logger.error(
- f"程序自启动任务计划创建失败: {result.stderr}",
- module="系统服务",
- )
+ logger.error(f"程序自启动任务计划创建失败: {result.stderr}")
finally:
# 删除临时文件
@@ -164,9 +152,9 @@ class _SystemHandler:
pass
except Exception as e:
- logger.exception(f"程序自启动任务计划创建失败: {e}", module="系统服务")
+ logger.exception(f"程序自启动任务计划创建失败: {e}")
- elif not Config.get(Config.start_IfSelfStart) and self.is_startup():
+ elif not Config.get("Start", "IfSelfStart") and await self.is_startup():
try:
@@ -179,90 +167,88 @@ class _SystemHandler:
)
if result.returncode == 0:
- logger.success("程序自启动任务计划已删除", module="系统服务")
+ logger.success("程序自启动任务计划已删除")
else:
- logger.error(
- f"程序自启动任务计划删除失败: {result.stderr}",
- module="系统服务",
- )
+ logger.error(f"程序自启动任务计划删除失败: {result.stderr}")
except Exception as e:
- logger.exception(f"程序自启动任务计划删除失败: {e}", module="系统服务")
+ logger.exception(f"程序自启动任务计划删除失败: {e}")
- def set_power(self, mode) -> None:
+ async def set_power(
+ self,
+ mode: Literal[
+ "NoAction", "Shutdown", "ShutdownForce", "Hibernate", "Sleep", "KillSelf"
+ ],
+ ) -> None:
"""
执行系统电源操作
- :param mode: 电源操作模式,支持 "NoAction", "Shutdown", "Hibernate", "Sleep", "KillSelf", "ShutdownForce"
+ :param mode: 电源操作
"""
if sys.platform.startswith("win"):
if mode == "NoAction":
- logger.info("不执行系统电源操作", module="系统服务")
+ logger.info("不执行系统电源操作")
elif mode == "Shutdown":
- self.kill_emulator_processes()
- logger.info("执行关机操作", module="系统服务")
+ await self.kill_emulator_processes()
+ logger.info("执行关机操作")
subprocess.run(["shutdown", "/s", "/t", "0"])
elif mode == "ShutdownForce":
- logger.info("执行强制关机操作", module="系统服务")
+ logger.info("执行强制关机操作")
subprocess.run(["shutdown", "/s", "/t", "0", "/f"])
elif mode == "Hibernate":
- logger.info("执行休眠操作", module="系统服务")
+ logger.info("执行休眠操作")
subprocess.run(["shutdown", "/h"])
elif mode == "Sleep":
- logger.info("执行睡眠操作", module="系统服务")
+ logger.info("执行睡眠操作")
subprocess.run(
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
)
- elif mode == "KillSelf":
+ elif mode == "KillSelf" and Config.server is not None:
- logger.info("执行退出主程序操作", module="系统服务")
- Config.main_window.close()
- QApplication.quit()
- sys.exit(0)
+ logger.info("执行退出主程序操作")
+ Config.server.should_exit = True
elif sys.platform.startswith("linux"):
if mode == "NoAction":
- logger.info("不执行系统电源操作", module="系统服务")
+ logger.info("不执行系统电源操作")
elif mode == "Shutdown":
- logger.info("执行关机操作", module="系统服务")
+ logger.info("执行关机操作")
subprocess.run(["shutdown", "-h", "now"])
elif mode == "Hibernate":
- logger.info("执行休眠操作", module="系统服务")
+ logger.info("执行休眠操作")
subprocess.run(["systemctl", "hibernate"])
elif mode == "Sleep":
- logger.info("执行睡眠操作", module="系统服务")
+ logger.info("执行睡眠操作")
subprocess.run(["systemctl", "suspend"])
- elif mode == "KillSelf":
+ elif mode == "KillSelf" and Config.server is not None:
- logger.info("执行退出主程序操作", module="系统服务")
- Config.main_window.close()
- QApplication.quit()
- sys.exit(0)
+ logger.info("执行退出主程序操作")
+ Config.server.should_exit = True
- def kill_emulator_processes(self):
+ async def kill_emulator_processes(self):
"""这里暂时仅支持 MuMu 模拟器"""
- logger.info("正在清除模拟器进程", module="系统服务")
+ logger.info("正在清除模拟器进程")
keywords = ["Nemu", "nemu", "emulator", "MuMu"]
for proc in psutil.process_iter(["pid", "name"]):
@@ -270,16 +256,13 @@ class _SystemHandler:
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="系统服务",
- )
+ logger.info(f"已关闭 MuMu 模拟器进程: {proc.info['name']}")
except (psutil.NoSuchProcess, psutil.AccessDenied):
continue
- logger.success("模拟器进程清除完成", module="系统服务")
+ logger.success("模拟器进程清除完成")
- def is_startup(self) -> bool:
+ async def is_startup(self) -> bool:
"""判断程序是否已经开机自启"""
try:
@@ -292,10 +275,10 @@ class _SystemHandler:
)
return result.returncode == 0
except Exception as e:
- logger.exception(f"检查任务计划程序失败: {e}", module="系统服务")
+ logger.exception(f"检查任务计划程序失败: {e}")
return False
- def get_window_info(self) -> list:
+ async def get_window_info(self) -> list:
"""获取当前前台窗口信息"""
def callback(hwnd, window_info):
@@ -309,16 +292,16 @@ class _SystemHandler:
win32gui.EnumWindows(callback, window_info)
return window_info
- def kill_process(self, path: Path) -> None:
+ async def kill_process(self, path: Path) -> None:
"""
根据路径中止进程
:param path: 进程路径
"""
- logger.info(f"开始中止进程: {path}", module="系统服务")
+ logger.info(f"开始中止进程: {path}")
- for pid in self.search_pids(path):
+ for pid in await self.search_pids(path):
killprocess = subprocess.Popen(
f"taskkill /F /T /PID {pid}",
shell=True,
@@ -326,9 +309,9 @@ class _SystemHandler:
)
killprocess.wait()
- logger.success(f"进程已中止: {path}", module="系统服务")
+ logger.success(f"进程已中止: {path}")
- def search_pids(self, path: Path) -> list:
+ async def search_pids(self, path: Path) -> list:
"""
根据路径查找进程PID
@@ -336,7 +319,7 @@ class _SystemHandler:
:return: 匹配的进程PID列表
"""
- logger.info(f"开始查找进程 PID: {path}", module="系统服务")
+ logger.info(f"开始查找进程 PID: {path}")
pids = []
for proc in psutil.process_iter(["pid", "exe"]):
@@ -344,7 +327,7 @@ class _SystemHandler:
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
diff --git a/app/task/MAA.py b/app/task/MAA.py
new file mode 100644
index 0000000..c339246
--- /dev/null
+++ b/app/task/MAA.py
@@ -0,0 +1,2100 @@
+# 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
+
+
+import json
+import uuid
+import asyncio
+import subprocess
+import shutil
+import win32com.client
+from pathlib import Path
+from datetime import datetime, timedelta
+from jinja2 import Environment, FileSystemLoader
+from typing import List, Dict, Optional
+
+from app.core import Broadcast, Config, MaaConfig, MaaUserConfig
+from app.models.schema import WebSocketMessage
+from app.models.ConfigBase import MultipleConfig
+from app.services import Notify, System
+from app.utils import get_logger, LogMonitor, ProcessManager
+from .skland import skland_sign_in
+
+
+logger = get_logger("MAA 调度器")
+
+
+METHOD_BOOK = {"NoAction": "8", "ExitGame": "9", "ExitEmulator": "12"}
+MOOD_BOOK = {"Annihilation": "剿灭", "Routine": "日常"}
+
+
+class MaaManager:
+ """MAA控制器"""
+
+ def __init__(
+ self, mode: str, script_id: uuid.UUID, user_id: Optional[uuid.UUID], ws_id: str
+ ):
+ super().__init__()
+
+ self.mode = mode
+ self.script_id = script_id
+ self.user_id = user_id
+ self.ws_id = ws_id
+
+ self.emulator_process_manager = ProcessManager()
+ self.maa_process_manager = ProcessManager()
+ self.wait_event = asyncio.Event()
+ self.message_queue = asyncio.Queue()
+
+ self.maa_logs = []
+ self.maa_result = "Wait"
+ self.maa_update_package = ""
+
+ async def configure(self):
+ """提取配置信息"""
+
+ await Broadcast.subscribe(self.message_queue)
+
+ await Config.ScriptConfig[self.script_id].lock()
+
+ self.script_config = Config.ScriptConfig[self.script_id]
+ if isinstance(self.script_config, MaaConfig):
+ self.user_config = MultipleConfig([MaaUserConfig])
+ await self.user_config.load(await self.script_config.UserData.toDict())
+
+ self.maa_root_path = Path(self.script_config.get("Info", "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.script_config.get("Run", "ADBSearchRange"))
+ ]
+ self.maa_log_monitor = LogMonitor(
+ (1, 20), "%Y-%m-%d %H:%M:%S", self.check_maa_log
+ )
+
+ logger.success(f"{self.script_id}已锁定, MAA配置提取完成")
+
+ def check_config(self) -> str:
+ """检查配置是否可用"""
+
+ if not self.maa_exe_path.exists():
+ return "MAA.exe文件不存在, 请检查MAA路径设置!"
+ if not self.maa_set_path.exists():
+ return "MAA配置文件不存在, 请检查MAA路径设置!"
+ if (self.mode != "设置脚本" or self.user_id is not None) and not (
+ Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json"
+ ).exists():
+ return "未完成 MAA 全局设置, 请先设置 MAA!"
+ return "Success!"
+
+ async def run(self):
+ """主进程, 运行MAA代理进程"""
+
+ self.current_date = datetime.now().strftime("%m-%d")
+ self.curdate = Config.server_date().strftime("%Y-%m-%d")
+ self.begin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+ await self.configure()
+ self.check_result = self.check_config()
+ if self.check_result != "Success!":
+ logger.error(f"未通过配置检查: {self.check_result}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id, type="Info", data={"Error": self.check_result}
+ ).model_dump()
+ )
+ return
+
+ # 记录 MAA 配置文件
+ logger.info(f"记录 MAA 配置文件: {self.maa_set_path}")
+ (Path.cwd() / f"data/{self.script_id}/Temp").mkdir(parents=True, exist_ok=True)
+ if self.maa_set_path.exists():
+ shutil.copy(
+ self.maa_set_path, Path.cwd() / f"data/{self.script_id}/Temp/gui.json"
+ )
+
+ # 整理用户数据, 筛选需代理的用户
+ if self.mode != "设置脚本":
+
+ self.user_list: List[Dict[str, str]] = [
+ {
+ "user_id": str(uid),
+ "status": "等待",
+ "name": config.get("Info", "Name"),
+ }
+ for uid, config in self.user_config.items()
+ if config.get("Info", "Status")
+ and config.get("Info", "RemainedDay") != 0
+ ]
+ self.user_list = sorted(
+ self.user_list,
+ key=lambda x: (
+ self.user_config[uuid.UUID(x["user_id"])].get("Info", "Mode")
+ ),
+ )
+
+ logger.info(f"用户列表创建完成, 已筛选用户数: {len(self.user_list)}")
+
+ # 自动代理模式
+ if self.mode == "自动代理":
+
+ # 标记是否需要重启模拟器
+ self.if_open_emulator = True
+ # # 执行情况预处理
+ for _ in self.user_list:
+ if (
+ self.user_config[uuid.UUID(_["user_id"])].get(
+ "Data", "LastProxyDate"
+ )
+ != self.curdate
+ ):
+ await self.user_config[uuid.UUID(_["user_id"])].set(
+ "Data", "LastProxyDate", self.curdate
+ )
+ await self.user_config[uuid.UUID(_["user_id"])].set(
+ "Data", "ProxyTimes", 0
+ )
+
+ # 开始代理
+ for self.index, user in enumerate(self.user_list):
+
+ self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
+
+ if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
+ self.cur_user_data.get("Data", "ProxyTimes")
+ < self.script_config.get("Run", "ProxyTimesLimit")
+ ):
+ user["status"] = "运行"
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"user_list": self.user_list},
+ ).model_dump()
+ )
+ else:
+ user["status"] = "跳过"
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"user_list": self.user_list},
+ ).model_dump()
+ )
+ continue
+
+ logger.info(f"开始代理用户: {user['user_id']}")
+
+ # 详细模式用户首次代理需打开模拟器
+ if self.cur_user_data.get("Info", "Mode") == "详细":
+ self.if_open_emulator = True
+
+ # 初始化代理情况记录和模式替换表
+ self.run_book = {
+ "Annihilation": bool(
+ self.cur_user_data.get("Info", "Annihilation") == "Close"
+ ),
+ "Routine": self.cur_user_data.get("Info", "Mode") == "复杂"
+ and not self.cur_user_data.get("Info", "Routine"),
+ }
+
+ self.user_logs_list = []
+ self.user_start_time = datetime.now()
+
+ if self.cur_user_data.get(
+ "Info", "IfSkland"
+ ) and self.cur_user_data.get("Info", "SklandToken"):
+
+ if self.cur_user_data.get(
+ "Data", "LastSklandDate"
+ ) != datetime.now().strftime("%Y-%m-%d"):
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"log": "正在执行森空岛签到中\n请稍候~"},
+ ).model_dump()
+ )
+
+ skland_result = await skland_sign_in(
+ self.cur_user_data.get("Info", "SklandToken")
+ )
+
+ for type, user_list in skland_result.items():
+
+ if type != "总计" and len(user_list) > 0:
+
+ logger.info(
+ f"用户: {user['user_id']} - 森空岛签到{type}: {'、'.join(user_list)}"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ (
+ "Info" if type != "失败" else "Error"
+ ): f"用户 {user['name']} 森空岛签到{type}: {'、'.join(user_list)}"
+ },
+ ).model_dump()
+ )
+ if skland_result["总计"] == 0:
+ logger.info(f"用户: {user['user_id']} - 森空岛签到失败")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ "Error": f"用户 {user['name']} 森空岛签到失败",
+ },
+ ).model_dump()
+ )
+
+ if (
+ skland_result["总计"] > 0
+ and len(skland_result["失败"]) == 0
+ ):
+ await self.cur_user_data.set(
+ "Data",
+ "LastSklandDate",
+ datetime.now().strftime("%Y-%m-%d"),
+ )
+
+ elif self.cur_user_data.get("Info", "IfSkland"):
+ logger.warning(
+ f"用户: {user['user_id']} - 未配置森空岛签到Token, 跳过森空岛签到"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ "Warning": f"用户 {user['name']} 未配置森空岛签到Token, 跳过森空岛签到"
+ },
+ ).model_dump()
+ )
+
+ # 剿灭-日常模式循环
+ for mode in ["Annihilation", "Routine"]:
+
+ if self.run_book[mode]:
+ continue
+
+ # 剿灭模式;满足条件跳过剿灭
+ if (
+ mode == "Annihilation"
+ and self.script_config.get("Run", "AnnihilationWeeklyLimit")
+ and datetime.strptime(
+ self.cur_user_data.get("Data", "LastAnnihilationDate"),
+ "%Y-%m-%d",
+ ).isocalendar()[:2]
+ == datetime.strptime(self.curdate, "%Y-%m-%d").isocalendar()[:2]
+ ):
+ logger.info(
+ f"用户: {user['user_id']} - 本周剿灭模式已达上限, 跳过执行剿灭任务"
+ )
+ self.run_book[mode] = True
+ continue
+ else:
+ self.weekly_annihilation_limit_reached = False
+
+ if (
+ self.cur_user_data.get("Info", "Mode") == "详细"
+ and not (
+ Path.cwd()
+ / f"data/{self.script_id}/{user['user_id']}/ConfigFile/gui.json"
+ ).exists()
+ ):
+ logger.error(
+ f"用户: {user['user_id']} - 未找到日常详细配置文件"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={"Error": f"未找到 {user['name']} 的详细配置文件"},
+ ).model_dump()
+ )
+ self.run_book[mode] = False
+ break
+
+ # 更新当前模式到界面
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "user_status": {
+ "user_id": user["user_id"],
+ "type": mode,
+ }
+ },
+ ).model_dump()
+ )
+
+ # 解析任务构成
+ if mode == "Routine":
+
+ self.task_dict = {
+ "WakeUp": str(self.cur_user_data.get("Task", "IfWakeUp")),
+ "Recruiting": str(
+ self.cur_user_data.get("Task", "IfRecruiting")
+ ),
+ "Base": str(self.cur_user_data.get("Task", "IfBase")),
+ "Combat": str(self.cur_user_data.get("Task", "IfCombat")),
+ "Mission": str(self.cur_user_data.get("Task", "IfMission")),
+ "Mall": str(self.cur_user_data.get("Task", "IfMall")),
+ "AutoRoguelike": str(
+ self.cur_user_data.get("Task", "IfAutoRoguelike")
+ ),
+ "Reclamation": str(
+ self.cur_user_data.get("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['name']} - 模式: {mode} - 任务列表: {self.task_dict.values()}"
+ )
+
+ # 尝试次数循环
+ for i in range(self.script_config.get("Run", "RunTimesLimit")):
+
+ if self.run_book[mode]:
+ break
+
+ logger.info(
+ f"用户 {user['name']} - 模式: {mode} - 尝试次数: {i + 1}/{self.script_config.get('Run','RunTimesLimit')}"
+ )
+
+ # 配置MAA
+ set = await self.set_maa(mode)
+ # 记录当前时间
+ 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}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ "Error": f"解析快捷方式时出现异常: {e}",
+ },
+ ).model_dump()
+ )
+ self.if_open_emulator = True
+ break
+ elif not self.emulator_path.exists():
+ logger.error(f"模拟器快捷方式不存在: {self.emulator_path}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ "Error": f"模拟器快捷方式 {self.emulator_path} 不存在",
+ },
+ ).model_dump()
+ )
+ 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}")
+ subprocess.run(
+ [self.ADB_path, "disconnect", self.ADB_address],
+ creationflags=subprocess.CREATE_NO_WINDOW,
+ )
+ except subprocess.CalledProcessError as e:
+ # 忽略错误,因为可能本来就没有连接
+ logger.warning(f"释放ADB时出现异常: {e}")
+ except Exception as e:
+ logger.exception(f"释放ADB时出现异常: {e}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={"Warning": f"释放ADB时出现异常: {e}"},
+ ).model_dump()
+ )
+
+ if self.if_open_emulator_process:
+ try:
+ logger.info(
+ f"启动模拟器: {self.emulator_path}, 参数: {self.emulator_arguments}"
+ )
+ await self.emulator_process_manager.open_process(
+ self.emulator_path, self.emulator_arguments, 0
+ )
+ except Exception as e:
+ logger.exception(f"启动模拟器时出现异常: {e}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ "Error": "启动模拟器时出现异常, 请检查MAA中模拟器路径设置"
+ },
+ ).model_dump()
+ )
+ self.if_open_emulator = True
+ break
+
+ # 更新静默进程标记有效时间
+ logger.info(
+ f"更新静默进程标记: {self.emulator_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.wait_time + 10)}"
+ )
+ Config.silence_dict[self.emulator_path] = (
+ datetime.now() + timedelta(seconds=self.wait_time + 10)
+ )
+
+ await self.search_ADB_address()
+
+ # 创建MAA任务
+ logger.info(f"启动MAA进程: {self.maa_exe_path}")
+ await self.maa_process_manager.open_process(
+ self.maa_exe_path, [], 0
+ )
+
+ # 监测MAA运行状态
+ self.log_check_mode = mode
+ await self.maa_log_monitor.start(
+ self.maa_log_path, self.log_start_time
+ )
+
+ self.wait_event.clear()
+ await self.wait_event.wait()
+
+ await self.maa_log_monitor.stop()
+
+ # 处理MAA结果
+ if self.maa_result == "Success!":
+
+ # 标记任务完成
+ self.run_book[mode] = True
+
+ logger.info(
+ f"用户: {user['user_id']} - MAA进程完成代理任务"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": "检测到MAA进程完成代理任务\n正在等待相关程序结束\n请等待10s"
+ },
+ ).model_dump()
+ )
+
+ else:
+ logger.error(
+ f"用户: {user['user_id']} - 代理任务异常: {self.maa_result}"
+ )
+ # 打印中止信息
+ # 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": f"{self.maa_result}\n正在中止相关程序\n请等待10s"
+ },
+ ).model_dump()
+ )
+ # 无命令行中止MAA与其子程序
+ logger.info(f"中止MAA进程: {self.maa_exe_path}")
+ await self.maa_process_manager.kill(if_force=True)
+ await System.kill_process(self.maa_exe_path)
+
+ # 中止模拟器进程
+ logger.info(
+ f"中止模拟器进程: {list(self.emulator_process_manager.tracked_pids)}"
+ )
+ await self.emulator_process_manager.kill()
+
+ self.if_open_emulator = True
+
+ # 推送异常通知
+ Notify.push_plyer(
+ "用户自动代理出现异常!",
+ f"用户 {user['name']} 的{MOOD_BOOK[mode]}部分出现一次异常",
+ f"{user['name']}的{MOOD_BOOK[mode]}出现异常",
+ 3,
+ )
+
+ await asyncio.sleep(10)
+
+ # 任务结束后释放ADB
+ try:
+ logger.info(f"释放ADB: {self.ADB_address}")
+ subprocess.run(
+ [self.ADB_path, "disconnect", self.ADB_address],
+ creationflags=subprocess.CREATE_NO_WINDOW,
+ )
+ except subprocess.CalledProcessError as e:
+ # 忽略错误,因为可能本来就没有连接
+ logger.warning(f"释放ADB时出现异常: {e}")
+ except Exception as e:
+ logger.exception(f"释放ADB时出现异常: {e}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={"Error": f"释放ADB时出现异常: {e}"},
+ ).model_dump()
+ )
+ # 任务结束后再次手动中止模拟器进程, 防止退出不彻底
+ if self.if_kill_emulator:
+ logger.info(
+ f"任务结束后再次中止模拟器进程: {list(self.emulator_process_manager.tracked_pids)}"
+ )
+ await 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)
+
+ # 记录自定义基建索引
+ await self.cur_user_data.set(
+ "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
+ ):
+ await self.cur_user_data.set(
+ "Data", "LastAnnihilationDate", self.curdate
+ )
+ # 保存运行日志以及统计信息
+ if_six_star = await Config.save_maa_log(
+ Path.cwd()
+ / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
+ self.maa_logs,
+ self.maa_result,
+ )
+ self.user_logs_list.append(
+ Path.cwd()
+ / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json"
+ )
+ if if_six_star:
+ await self.push_notification(
+ "公招六星",
+ f"喜报: 用户 {user['name']} 公招出六星啦!",
+ {
+ "user_name": user["name"],
+ },
+ )
+
+ # 执行MAA解压更新动作
+ if self.maa_update_package:
+
+ logger.info(f"检测到MAA更新, 正在执行更新动作")
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": "检测到MAA存在更新\nMAA正在执行更新动作\n请等待10s"
+ },
+ ).model_dump()
+ )
+ await self.set_maa("Update")
+ subprocess.Popen(
+ [self.maa_exe_path],
+ creationflags=subprocess.CREATE_NO_WINDOW,
+ )
+ await asyncio.sleep(10)
+ await System.kill_process(self.maa_exe_path)
+
+ self.maa_update_package = ""
+
+ logger.info(f"更新动作结束")
+
+ await self.result_record()
+
+ # 人工排查模式
+ elif self.mode == "人工排查":
+
+ # 人工排查时, 屏蔽静默操作
+ logger.info("人工排查任务开始, 屏蔽静默操作")
+ Config.if_ignore_silence.append(self.script_id)
+
+ # 标记是否需要启动模拟器
+ self.if_open_emulator = True
+
+ # 开始排查
+ for self.index, user in enumerate(self.user_list):
+
+ self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
+
+ logger.info(f"开始排查用户: {user['user_id']}")
+
+ user["status"] = "运行"
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"user_list": self.user_list},
+ ).model_dump()
+ )
+
+ if self.cur_user_data.get("Info", "Mode") == "详细":
+ self.if_open_emulator = True
+
+ self.run_book = {"SignIn": False, "PassCheck": False}
+
+ # 启动重试循环
+ while True:
+
+ # 配置MAA
+ await self.set_maa("人工排查")
+
+ # 记录当前时间
+ self.log_start_time = datetime.now()
+ # 创建MAA任务
+ logger.info(f"启动MAA进程: {self.maa_exe_path}")
+ await self.maa_process_manager.open_process(
+ self.maa_exe_path, [], 0
+ )
+
+ # 监测MAA运行状态
+ self.log_check_mode = "人工排查"
+ await self.maa_log_monitor.start(
+ self.maa_log_path, self.log_start_time
+ )
+
+ self.wait_event.clear()
+ await self.wait_event.wait()
+
+ await self.maa_log_monitor.stop()
+
+ logger.info(
+ f"用户: {user['user_id']} - MAA进程登录PRTS结果: {self.maa_result}"
+ )
+
+ if self.maa_result == "Success!":
+ logger.info(f"用户: {user['user_id']} - MAA进程成功登录PRTS")
+ self.run_book["SignIn"] = True
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"log": "检测到MAA进程成功登录PRTS"},
+ ).model_dump()
+ )
+ else:
+ logger.error(
+ f"用户: {user['user_id']} - MAA未能正确登录到PRTS: {self.maa_result}"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": f"{self.maa_result}\n正在中止相关程序\n请等待10s"
+ },
+ ).model_dump()
+ )
+ # 无命令行中止MAA与其子程序
+ logger.info(f"中止MAA进程: {self.maa_exe_path}")
+ await self.maa_process_manager.kill(if_force=True)
+ await System.kill_process(self.maa_exe_path)
+ self.if_open_emulator = True
+ await asyncio.sleep(10)
+
+ # 登录成功, 结束循环
+ if self.run_book["SignIn"]:
+ break
+ # 登录失败, 询问是否结束循环
+ else:
+
+ uid = str(uuid.uuid4())
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Message",
+ data={
+ "message_id": uid,
+ "type": "Question",
+ "title": "操作提示",
+ "message": "MAA未能正确登录到PRTS, 是否重试?",
+ },
+ ).model_dump()
+ )
+ result = await self.get_message(uid)
+ if result.get("choice", False):
+ break
+
+ # 登录成功, 录入人工排查情况
+ if self.run_book["SignIn"]:
+
+ uid = str(uuid.uuid4())
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Message",
+ data={
+ "message_id": uid,
+ "type": "Question",
+ "title": "操作提示",
+ "message": "请检查用户代理情况, 该用户是否正确完成代理任务?",
+ },
+ ).model_dump()
+ )
+ result = await self.get_message(uid)
+ if result.get("choice", False):
+ self.run_book["PassCheck"] = True
+
+ await self.result_record()
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"user_list": self.user_list},
+ ).model_dump()
+ )
+
+ # 设置MAA模式
+ elif self.mode == "设置脚本":
+
+ # 配置MAA
+ await self.set_maa(self.mode)
+ # 创建MAA任务
+ logger.info(f"启动MAA进程: {self.maa_exe_path}")
+ await self.maa_process_manager.open_process(self.maa_exe_path, [], 0)
+ # 记录当前时间
+ self.log_start_time = datetime.now()
+
+ # 监测MAA运行状态
+ self.log_check_mode = "设置脚本"
+ await self.maa_log_monitor.start(self.maa_log_path, self.log_start_time)
+ self.wait_event.clear()
+ await self.wait_event.wait()
+ await self.maa_log_monitor.stop()
+
+ async def result_record(self):
+ """记录用户结果信息"""
+
+ if self.mode == "自动代理":
+ # 发送统计信息
+ statistics = await Config.merge_statistic_info(self.user_logs_list)
+ statistics["user_info"] = self.user_list[self.index]["name"]
+ statistics["start_time"] = self.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 (self.run_book["Annihilation"] and self.run_book["Routine"])
+ else "代理任务未全部完成"
+ )
+ await self.push_notification(
+ "统计信息",
+ f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
+ statistics,
+ )
+
+ if self.run_book["Annihilation"] and self.run_book["Routine"]:
+ # 成功完成代理的用户修改相关参数
+ if (
+ self.cur_user_data.get("Data", "ProxyTimes") == 0
+ and self.cur_user_data.get("Info", "RemainedDay") != -1
+ ):
+ await self.cur_user_data.set(
+ "Info",
+ "RemainedDay",
+ self.cur_user_data.get("Info", "RemainedDay") - 1,
+ )
+ await self.cur_user_data.set(
+ "Data",
+ "ProxyTimes",
+ self.cur_user_data.get("Data", "ProxyTimes") + 1,
+ )
+ self.user_list[self.index]["status"] = "完成"
+ logger.success(
+ f"用户 {self.user_list[self.index]['user_id']} 的自动代理任务已完成"
+ )
+ Notify.push_plyer(
+ "成功完成一个自动代理任务!",
+ f"已完成用户 {self.user_list[self.index]['name']} 的自动代理任务",
+ f"已完成 {self.user_list[self.index]['name']} 的自动代理任务",
+ 3,
+ )
+ else:
+ # 录入代理失败的用户
+ logger.error(
+ f"用户 {self.user_list[self.index]['user_id']} 的自动代理任务未完成"
+ )
+ self.user_list[self.index]["status"] = "异常"
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id, type="Update", data={"user_list": self.user_list}
+ ).model_dump()
+ )
+
+ elif self.mode == "人工排查":
+
+ if self.run_book["SignIn"] and self.run_book["PassCheck"]:
+ logger.info(
+ f"用户 {self.user_list[self.index]['user_id']} 通过人工排查"
+ )
+ await self.cur_user_data.set("Data", "IfPassCheck", True)
+ self.user_list[self.index]["status"] = "完成"
+ else:
+ logger.info(
+ f"用户 {self.user_list[self.index]['user_id']} 未通过人工排查"
+ )
+ await self.cur_user_data.set("Data", "IfPassCheck", False)
+ self.user_list[self.index]["status"] = "异常"
+
+ async def final_task(self, task: asyncio.Task):
+ """结束时的收尾工作"""
+
+ logger.info("MAA 主任务已结束, 开始执行后续操作")
+
+ await Config.ScriptConfig[self.script_id].unlock()
+ logger.success(f"已解锁脚本配置 {self.script_id}")
+
+ # 结束各子任务
+ await Broadcast.unsubscribe(self.message_queue)
+ await self.maa_process_manager.kill(if_force=True)
+ await System.kill_process(self.maa_exe_path)
+ await self.emulator_process_manager.kill()
+ await self.maa_log_monitor.stop()
+ del self.maa_process_manager
+ del self.emulator_process_manager
+ del self.maa_log_monitor
+
+ if self.check_result != "Success!":
+ return self.check_result
+
+ if self.mode == "人工排查":
+
+ # 解除静默操作屏蔽
+ logger.info("人工排查任务结束, 解除静默操作屏蔽")
+ if self.script_id in Config.if_ignore_silence:
+ Config.if_ignore_silence.remove(self.script_id)
+
+ if self.mode == "自动代理" and self.user_list[self.index]["status"] == "运行":
+
+ if not self.maa_update_package:
+
+ self.maa_result = "用户手动中止任务"
+
+ # 保存运行日志以及统计信息
+ if_six_star = await Config.save_maa_log(
+ Path.cwd()
+ / f"history/{self.curdate}/{self.user_list[self.index]['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
+ self.maa_logs,
+ self.maa_result,
+ )
+ self.user_logs_list.append(
+ Path.cwd()
+ / f"history/{self.curdate}/{self.user_list[self.index]['name']}/{self.log_start_time.strftime('%H-%M-%S')}.json"
+ )
+ if if_six_star:
+ await self.push_notification(
+ "公招六星",
+ f"喜报: 用户 {self.user_list[self.index]['name']} 公招出六星啦!",
+ {
+ "user_name": self.user_list[self.index]["name"],
+ },
+ )
+
+ await self.result_record()
+
+ elif self.mode == "人工排查" and self.user_list[self.index]["status"] == "运行":
+
+ await self.result_record()
+
+ # 导出结果
+ if self.mode in ["自动代理", "人工排查"]:
+
+ # 更新用户数据
+ sc = Config.ScriptConfig[self.script_id]
+ if isinstance(sc, MaaConfig):
+ await sc.UserData.load(await self.user_config.toDict())
+ await Config.ScriptConfig.save()
+
+ error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
+ over_user = [_["name"] for _ in self.user_list if _["status"] == "完成"]
+ wait_user = [_["name"] for _ in self.user_list if _["status"] == "等待"]
+
+ # 保存运行日志
+ title = (
+ f"{self.current_date} | {self.script_config.get("Info", "Name")}的{self.mode}任务报告"
+ if self.script_config.get("Info", "Name") != ""
+ else f"{self.current_date} | {self.mode}任务报告"
+ )
+ result = {
+ "title": f"{self.mode}任务报告",
+ "script_name": (
+ self.script_config.get("Info", "Name")
+ if self.script_config.get("Info", "Name") != ""
+ else "空白"
+ ),
+ "start_time": self.begin_time,
+ "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "completed_count": len(over_user),
+ "uncompleted_count": len(error_user) + len(wait_user),
+ "failed_user": error_user,
+ "waiting_user": wait_user,
+ }
+
+ # 生成结果文本
+ 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}未成功的用户: \n{"\n".join(result["failed_user"])}\n"
+ )
+ if len(result["waiting_user"]) > 0:
+ result_text += f"\n未开始{self.mode}的用户: \n{"\n".join(result["waiting_user"])}\n"
+
+ # 推送代理结果通知
+ Notify.push_plyer(
+ title.replace("报告", "已完成!"),
+ f"已完成用户数: {len(over_user)}, 未完成用户数: {len(error_user) + len(wait_user)}",
+ f"已完成用户数: {len(over_user)}, 未完成用户数: {len(error_user) + len(wait_user)}",
+ 10,
+ )
+ await self.push_notification("代理结果", title, result)
+
+ elif self.mode == "设置脚本":
+ (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id if self.user_id else 'Default'}/ConfigFile"
+ ).mkdir(parents=True, exist_ok=True)
+ shutil.copy(
+ self.maa_set_path,
+ (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id if self.user_id else 'Default'}/ConfigFile/gui.json"
+ ),
+ )
+
+ result_text = ""
+
+ # 复原 MAA 配置文件
+ logger.info(f"复原 MAA 配置文件: {Path.cwd() / f'data/{self.script_id}/Temp'}")
+ if (Path.cwd() / f"data/{self.script_id}/Temp/gui.json").exists():
+ shutil.copy(
+ Path.cwd() / f"data/{self.script_id}/Temp/gui.json", self.maa_set_path
+ )
+ shutil.rmtree(Path.cwd() / f"data/{self.script_id}/Temp")
+
+ self.agree_bilibili(False)
+ return result_text
+
+ async def get_message(self, message_id: str):
+ """获取当前任务的属性值"""
+
+ logger.info(f"等待客户端回应消息: {message_id}")
+
+ while True:
+ message = await self.message_queue.get()
+ if message.get("message_id") == message_id:
+ self.message_queue.task_done()
+ logger.success(f"收到客户端回应消息: {message_id}")
+ return message
+ else:
+ self.message_queue.task_done()
+
+ async def search_ADB_address(self) -> None:
+ """搜索ADB实际地址"""
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": f"即将搜索ADB实际地址\n正在等待模拟器完成启动\n请等待{self.wait_time}s"
+ },
+ ).model_dump()
+ )
+
+ await asyncio.sleep(self.wait_time)
+
+ 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}"
+ )
+
+ 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}")
+
+ # 断开连接
+ logger.info(f"断开ADB连接: {ADB_address}")
+ 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}")
+ await self.maa_process_manager.kill(if_force=True)
+ await 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)
+
+ return None
+
+ else:
+ logger.info(f"无法连接到ADB地址: {ADB_address}")
+ else:
+ logger.info(f"无法连接到ADB地址: {ADB_address}")
+
+ async def check_maa_log(self, log_content: List[str]) -> None:
+ """获取MAA日志并检查以判断MAA程序运行状态"""
+
+ self.maa_logs = log_content
+ log = "".join(log_content)
+
+ # 更新MAA日志
+ if await self.maa_process_manager.is_running():
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id, type="Update", data={"log": log}
+ ).model_dump()
+ )
+
+ if self.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}")
+
+ if self.log_check_mode == "Annihilation" and "任务出错: 刷理智" in log:
+ self.weekly_annihilation_limit_reached = True
+ else:
+ self.weekly_annihilation_limit_reached = False
+
+ if "任务出错: StartUp" in log or "任务出错: 开始唤醒" 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 (
+ self.log_check_mode == "Annihilation"
+ and "任务出错: 刷理智" 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 await self.maa_process_manager.is_running()
+ ):
+ self.maa_result = "MAA在完成任务前退出"
+
+ elif datetime.now() - latest_time > timedelta(
+ minutes=self.script_config.get("Run", f"{self.log_check_mode}TimeLimit")
+ ):
+ self.maa_result = "MAA进程超时"
+
+ else:
+ self.maa_result = "Wait"
+
+ elif self.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 await self.maa_process_manager.is_running()
+ ):
+ self.maa_result = "MAA在完成任务前退出"
+ else:
+ self.maa_result = "Wait"
+
+ elif self.mode == "设置脚本":
+ if (
+ "MaaAssistantArknights GUI exited" in log
+ or not await self.maa_process_manager.is_running()
+ ):
+ self.maa_result = "Success!"
+ else:
+ self.maa_result = "Wait"
+
+ logger.debug(f"MAA 日志分析结果: {self.maa_result}")
+
+ if self.maa_result != "Wait":
+
+ logger.info(f"MAA 任务结果: {self.maa_result}, 日志锁已释放")
+ self.wait_event.set()
+
+ async def set_maa(self, mode: str) -> dict:
+ """配置MAA运行参数"""
+ logger.info(f"开始配置MAA运行参数: {mode}")
+
+ if self.mode != "设置脚本" and mode != "Update":
+
+ if self.cur_user_data.get("Info", "Server") == "Bilibili":
+ self.agree_bilibili(True)
+ else:
+ self.agree_bilibili(False)
+
+ # 配置MAA前关闭可能未正常退出的MAA进程
+ await self.maa_process_manager.kill(if_force=True)
+ await System.kill_process(self.maa_exe_path)
+
+ # 预导入MAA配置文件
+ if self.mode in ["自动代理", "人工排查"]:
+ if self.cur_user_data.get("Info", "Mode") == "简洁":
+ shutil.copy(
+ (Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json"),
+ self.maa_set_path,
+ )
+ elif self.cur_user_data.get("Info", "Mode") == "详细":
+ shutil.copy(
+ (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json"
+ ),
+ self.maa_set_path,
+ )
+ elif self.mode == "设置脚本":
+ if (
+ self.user_id is None
+ and (
+ Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json"
+ ).exists()
+ ):
+ shutil.copy(
+ (Path.cwd() / f"data/{self.script_id}/Default/ConfigFile/gui.json"),
+ self.maa_set_path,
+ )
+ elif self.user_id is not None:
+ if (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json"
+ ).exists():
+ shutil.copy(
+ (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/ConfigFile/gui.json"
+ ),
+ self.maa_set_path,
+ )
+ else:
+ shutil.copy(
+ (
+ Path.cwd()
+ / f"data/{self.script_id}/Default/ConfigFile/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 self.mode == "自动代理" and mode in ["Annihilation", "Routine"]:
+
+ if (self.index == len(self.user_list) - 1) or (
+ self.user_config[
+ uuid.UUID(self.user_list[self.index + 1]["user_id"])
+ ].get("Info", "Mode")
+ == "详细"
+ ):
+ data["Configurations"]["Default"][
+ "MainFunction.PostActions"
+ ] = "12" # 完成后退出MAA和模拟器
+ else:
+
+ data["Configurations"]["Default"]["MainFunction.PostActions"] = (
+ METHOD_BOOK[self.script_config.get("Run", "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("Function", "IfSilence"):
+ data["Global"]["Start.MinimizeDirectly"] = "True" # 启动MAA后直接最小化
+ data["Global"]["GUI.UseTray"] = "True" # 显示托盘图标
+ data["Global"]["GUI.MinimizeToTray"] = "True" # 最小化时隐藏至托盘
+
+ # 客户端类型
+ data["Configurations"]["Default"]["Start.ClientType"] = (
+ self.cur_user_data.get("Info", "Server")
+ )
+
+ # 账号切换
+ if self.cur_user_data.get("Info", "Server") == "Official":
+ data["Configurations"]["Default"]["Start.AccountName"] = (
+ f"{self.cur_user_data.get("Info", "Id")[:3]}****{self.cur_user_data.get("Info", "Id")[7:]}"
+ if len(self.cur_user_data.get("Info", "Id")) == 11
+ else self.cur_user_data.get("Info", "Id")
+ )
+ elif self.cur_user_data.get("Info", "Server") == "Bilibili":
+ data["Configurations"]["Default"]["Start.AccountName"] = (
+ self.cur_user_data.get("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 (
+ mode == "Annihilation"
+ or self.cur_user_data.get("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"
+
+ if isinstance(self.cur_user_data, MaaUserConfig):
+ try:
+ plan_data = self.cur_user_data.get_plan_info()
+ except Exception as e:
+ logger.error(
+ f"获取用户 {self.user_list[self.index]['user_id']} 的代理计划信息失败: {e}"
+ )
+ plan_data = {}
+ else:
+ plan_data = {}
+
+ data["Configurations"]["Default"]["MainFunction.UseMedicine"] = (
+ "False" if plan_data.get("MedicineNumb", 0) == 0 else "True"
+ ) # 吃理智药
+ data["Configurations"]["Default"]["MainFunction.UseMedicine.Quantity"] = (
+ str(plan_data.get("MedicineNumb", 0))
+ ) # 吃理智药数量
+ data["Configurations"]["Default"]["MainFunction.Series.Quantity"] = (
+ plan_data.get("SeriesNumb", "0")
+ ) # 连战次数
+
+ if mode == "Annihilation":
+
+ 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"] = (
+ self.cur_user_data.get("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 mode == "Routine":
+
+ data["Configurations"]["Default"]["MainFunction.Stage1"] = (
+ plan_data.get("Stage") if plan_data.get("Stage", "-") != "-" else ""
+ ) # 主关卡
+ data["Configurations"]["Default"]["MainFunction.Stage2"] = (
+ plan_data.get("Stage_1")
+ if plan_data.get("Stage_1", "-") != "-"
+ else ""
+ ) # 备选关卡1
+ data["Configurations"]["Default"]["MainFunction.Stage3"] = (
+ plan_data.get("Stage_2")
+ if plan_data.get("Stage_2", "-") != "-"
+ else ""
+ ) # 备选关卡2
+ data["Configurations"]["Default"]["MainFunction.Stage4"] = (
+ plan_data.get("Stage_3")
+ if plan_data.get("Stage_3", "-") != "-"
+ else ""
+ ) # 备选关卡3
+ data["Configurations"]["Default"]["Fight.RemainingSanityStage"] = (
+ plan_data.get("Stage_Remain")
+ if plan_data.get("Stage_Remain", "-") != "-"
+ else ""
+ ) # 剩余理智关卡
+ data["Configurations"]["Default"][
+ "GUI.UseAlternateStage"
+ ] = "True" # 备选关卡
+ data["Configurations"]["Default"]["Fight.UseRemainingSanityStage"] = (
+ "True" if plan_data.get("Stage_Remain", "-") != "-" else "False"
+ ) # 使用剩余理智
+
+ if self.cur_user_data.get("Info", "Mode") == "简洁":
+
+ data["Configurations"]["Default"][
+ "Penguin.IsDrGrandet"
+ ] = "False" # 博朗台模式
+ data["Configurations"]["Default"][
+ "GUI.CustomStageCode"
+ ] = "True" # 手动输入关卡名
+ data["Configurations"]["Default"][
+ "Fight.UseExpiringMedicine"
+ ] = "True" # 无限吃48小时内过期的理智药
+ # 自定义基建配置
+ if self.cur_user_data.get("Info", "InfrastMode") == "Custom":
+
+ if (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json"
+ ).exists():
+
+ data["Configurations"]["Default"][
+ "Infrast.InfrastMode"
+ ] = "Custom" # 基建模式
+ data["Configurations"]["Default"][
+ "Infrast.CustomInfrastPlanIndex"
+ ] = self.cur_user_data.get(
+ "Data", "CustomInfrastPlanIndex"
+ ) # 自定义基建配置索引
+ data["Configurations"]["Default"][
+ "Infrast.DefaultInfrast"
+ ] = "user_defined" # 内置配置
+ data["Configurations"]["Default"][
+ "Infrast.IsCustomInfrastFileReadOnly"
+ ] = "False" # 自定义基建配置文件只读
+ data["Configurations"]["Default"][
+ "Infrast.CustomInfrastFile"
+ ] = str(
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/Infrastructure/infrastructure.json"
+ ) # 自定义基建配置文件地址
+ else:
+ logger.warning(
+ f"未选择用户 {self.cur_user_data.get('Info', 'Name')} 的自定义基建配置文件"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={
+ "warning": f"未选择用户 {self.cur_user_data.get('Info', 'Name')} 的自定义基建配置文件"
+ },
+ ).model_dump()
+ )
+ data["Configurations"]["Default"][
+ "Infrast.CustomInfrastEnabled"
+ ] = "Normal" # 基建模式
+ else:
+ data["Configurations"]["Default"]["Infrast.InfrastMode"] = (
+ self.cur_user_data.get("Info", "InfrastMode")
+ ) # 基建模式
+
+ elif self.cur_user_data.get("Info", "Mode") == "详细":
+
+ # 基建模式
+ if (
+ data["Configurations"]["Default"]["Infrast.InfrastMode"]
+ == "Custom"
+ ):
+ data["Configurations"]["Default"][
+ "Infrast.CustomInfrastPlanIndex"
+ ] = self.cur_user_data.get(
+ "Data", "CustomInfrastPlanIndex"
+ ) # 自定义基建配置索引
+
+ # 人工排查配置
+ elif self.mode == "人工排查" and self.cur_user_data is not None:
+
+ 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"] = (
+ self.cur_user_data.get("Info", "Server")
+ )
+
+ # 账号切换
+ if self.cur_user_data.get("Info", "Server") == "Official":
+ data["Configurations"]["Default"]["Start.AccountName"] = (
+ f"{self.cur_user_data.get('Info', 'Id')[:3]}****{self.cur_user_data.get('Info', 'Id')[7:]}"
+ if len(self.cur_user_data.get("Info", "Id")) == 11
+ else self.cur_user_data.get("Info", "Id")
+ )
+ elif self.cur_user_data.get("Info", "Server") == "Bilibili":
+ data["Configurations"]["Default"]["Start.AccountName"] = (
+ self.cur_user_data.get("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" # 生息演算
+
+ # 设置脚本配置
+ elif self.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("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 == "Update":
+
+ 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 self.mode != "设置脚本" and mode != "Update" 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}")
+
+ return data
+
+ def agree_bilibili(self, if_agree):
+ """向MAA写入Bilibili协议相关任务"""
+ logger.info(f"Bilibili协议相关任务状态: {'启用' if if_agree else '禁用'}")
+
+ with self.maa_tasks_path.open(mode="r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ if if_agree and Config.get("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)
+
+ async def push_notification(self, mode: str, title: str, message) -> None:
+ """通过所有渠道推送通知"""
+ logger.info(f"开始推送通知, 模式: {mode}, 标题: {title}")
+
+ env = Environment(loader=FileSystemLoader(str(Path.cwd() / "res/html")))
+
+ if mode == "代理结果" and (
+ Config.get("Notify", "SendTaskResultTime") == "任何时刻"
+ or (
+ Config.get("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("Notify", "IfSendMail"):
+ Notify.send_mail(
+ "网页", title, message_html, Config.get("Notify", "ToAddress")
+ )
+
+ if Config.get("Notify", "IfServerChan"):
+ Notify.ServerChanPush(
+ title,
+ f"{serverchan_message}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "ServerChanKey"),
+ )
+
+ if Config.get("Notify", "IfCompanyWebHookBot"):
+ Notify.WebHookPush(
+ title,
+ f"{message_text}\n\nAUTO_MAA 敬上",
+ Config.get("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("Notify", "IfSendStatistic"):
+
+ if Config.get("Notify", "IfSendMail"):
+ Notify.send_mail(
+ "网页", title, message_html, Config.get("Notify", "ToAddress")
+ )
+
+ if Config.get("Notify", "IfServerChan"):
+ Notify.ServerChanPush(
+ title,
+ f"{serverchan_message}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "ServerChanKey"),
+ )
+
+ if Config.get("Notify", "IfCompanyWebHookBot"):
+ Notify.WebHookPush(
+ title,
+ f"{message_text}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
+ )
+
+ # 发送用户单独通知
+ if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
+ "Notify", "IfSendStatistic"
+ ):
+
+ # 发送邮件通知
+ if self.cur_user_data.get("Notify", "IfSendMail"):
+ if self.cur_user_data.get("Notify", "ToAddress"):
+ Notify.send_mail(
+ "网页",
+ title,
+ message_html,
+ self.cur_user_data.get("Notify", "ToAddress"),
+ )
+ else:
+ logger.error(f"用户邮箱地址为空, 无法发送用户单独的邮件通知")
+
+ # 发送ServerChan通知
+ if self.cur_user_data.get("Notify", "IfServerChan"):
+ if self.cur_user_data.get("Notify", "ServerChanKey"):
+ Notify.ServerChanPush(
+ title,
+ f"{serverchan_message}\n\nAUTO_MAA 敬上",
+ self.cur_user_data.get("Notify", "ServerChanKey"),
+ )
+ else:
+ logger.error(
+ "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知"
+ )
+
+ # 推送CompanyWebHookBot通知
+ if self.cur_user_data.get("Notify", "IfCompanyWebHookBot"):
+ if self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"):
+ Notify.WebHookPush(
+ title,
+ f"{message_text}\n\nAUTO_MAA 敬上",
+ self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"),
+ )
+ else:
+ logger.error(
+ "用户CompanyWebHookBot密钥为空, 无法发送用户单独的CompanyWebHookBot通知"
+ )
+
+ elif mode == "公招六星":
+
+ # 生成HTML通知内容
+ template = env.get_template("MAA_six_star.html")
+
+ message_html = template.render(message)
+
+ # 发送全局通知
+ if Config.get("Notify", "IfSendSixStar"):
+
+ if Config.get("Notify", "IfSendMail"):
+ Notify.send_mail(
+ "网页", title, message_html, Config.get("Notify", "ToAddress")
+ )
+
+ if Config.get("Notify", "IfServerChan"):
+ Notify.ServerChanPush(
+ title,
+ "好羡慕~\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "ServerChanKey"),
+ )
+
+ if Config.get("Notify", "IfCompanyWebHookBot"):
+ Notify.WebHookPush(
+ title,
+ "好羡慕~\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
+ )
+ Notify.CompanyWebHookBotPushImage(
+ Path.cwd() / "res/images/notification/six_star.png",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
+ )
+
+ # 发送用户单独通知
+ if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
+ "Notify", "IfSendSixStar"
+ ):
+
+ # 发送邮件通知
+ if self.cur_user_data.get("Notify", "IfSendMail"):
+ if self.cur_user_data.get("Notify", "ToAddress"):
+ Notify.send_mail(
+ "网页",
+ title,
+ message_html,
+ self.cur_user_data.get("Notify", "ToAddress"),
+ )
+ else:
+ logger.error("用户邮箱地址为空, 无法发送用户单独的邮件通知")
+
+ # 发送ServerChan通知
+ if self.cur_user_data.get("Notify", "IfServerChan"):
+
+ if self.cur_user_data.get("Notify", "ServerChanKey"):
+ Notify.ServerChanPush(
+ title,
+ "好羡慕~\n\nAUTO_MAA 敬上",
+ self.cur_user_data.get("Notify", "ServerChanKey"),
+ )
+ else:
+ logger.error(
+ "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知"
+ )
+
+ # 推送CompanyWebHookBot通知
+ if self.cur_user_data.get("Notify", "IfCompanyWebHookBot"):
+ if self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"):
+ Notify.WebHookPush(
+ title,
+ "好羡慕~\n\nAUTO_MAA 敬上",
+ self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"),
+ )
+ Notify.CompanyWebHookBotPushImage(
+ Path.cwd() / "res/images/notification/six_star.png",
+ self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"),
+ )
+ else:
+ logger.error(
+ "用户CompanyWebHookBot密钥为空, 无法发送用户单独的CompanyWebHookBot通知"
+ )
+
+ return None
diff --git a/app/ui/__init__.py b/app/task/__init__.py
similarity index 80%
rename from app/ui/__init__.py
rename to app/task/__init__.py
index ea7d35a..9a347ca 100644
--- a/app/ui/__init__.py
+++ b/app/task/__init__.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,18 +19,13 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA图形化界面包
-v4.4
-作者:DLmaster_361
-"""
-
-__version__ = "4.2.0"
+__version__ = "5.0.0"
__author__ = "DLmaster361 "
__license__ = "GPL-3.0 license"
-from .main_window import AUTO_MAA
-from .Widget import ProgressRingMessageBox
-__all__ = ["AUTO_MAA", "ProgressRingMessageBox"]
+from .skland import skland_sign_in
+from .general import GeneralManager
+from .MAA import MaaManager
+
+__all__ = ["skland_sign_in", "GeneralManager", "MaaManager"]
diff --git a/app/task/general.py b/app/task/general.py
new file mode 100644
index 0000000..aa99917
--- /dev/null
+++ b/app/task/general.py
@@ -0,0 +1,1080 @@
+# 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
+
+
+import os
+import sys
+import uuid
+import shutil
+import asyncio
+import subprocess
+from pathlib import Path
+from fastapi import WebSocket
+from datetime import datetime, timedelta
+from jinja2 import Environment, FileSystemLoader
+from typing import Union, List, Dict, Optional
+
+
+from app.core import Config, GeneralConfig, GeneralUserConfig
+from app.models.schema import WebSocketMessage
+from app.models.ConfigBase import MultipleConfig
+from app.services import Notify, System
+from app.utils import get_logger, LogMonitor, ProcessManager, strptime
+
+
+logger = get_logger("通用调度器")
+
+
+class GeneralManager:
+ """通用脚本通用控制器"""
+
+ def __init__(
+ self, mode: str, script_id: uuid.UUID, user_id: Optional[uuid.UUID], ws_id: str
+ ):
+ super(GeneralManager, self).__init__()
+
+ self.mode = mode
+ self.script_id = script_id
+ self.user_id = user_id
+ self.ws_id = ws_id
+
+ self.game_process_manager = ProcessManager()
+ self.general_process_manager = ProcessManager()
+ self.wait_event = asyncio.Event()
+
+ self.general_logs = []
+ self.general_result = "Wait"
+
+ async def configure(self):
+ """提取配置信息"""
+
+ await Config.ScriptConfig[self.script_id].lock()
+
+ self.script_config = Config.ScriptConfig[self.script_id]
+ if isinstance(self.script_config, GeneralConfig):
+ self.user_config = MultipleConfig([GeneralUserConfig])
+ await self.user_config.load(await self.script_config.UserData.toDict())
+
+ self.script_root_path = Path(self.script_config.get("Info", "RootPath"))
+ self.script_path = Path(self.script_config.get("Script", "ScriptPath"))
+
+ arguments_list = []
+ path_list = []
+
+ for argument in [
+ _.strip()
+ for _ in str(self.script_config.get("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.script_config.get("Script", "ConfigPath"))
+ self.script_log_path = (
+ Path(self.script_config.get("Script", "LogPath")).with_stem(
+ datetime.now().strftime(
+ self.script_config.get("Script", "LogPathFormat")
+ )
+ )
+ if self.script_config.get("Script", "LogPathFormat")
+ else Path(self.script_config.get("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.script_config.get("Game", "Path"))
+ self.log_time_range = (
+ self.script_config.get("Script", "LogTimeStart") - 1,
+ self.script_config.get("Script", "LogTimeEnd"),
+ )
+ self.success_log = (
+ [
+ _.strip()
+ for _ in self.script_config.get("Script", "SuccessLog").split("|")
+ ]
+ if self.script_config.get("Script", "SuccessLog")
+ else []
+ )
+ self.error_log = [
+ _.strip() for _ in self.script_config.get("Script", "ErrorLog").split("|")
+ ]
+ self.general_log_monitor = LogMonitor(
+ self.log_time_range,
+ self.script_config.get("Script", "LogTimeFormat"),
+ self.check_general_log,
+ )
+
+ logger.success(f"{self.script_id}已锁定, 通用配置提取完成")
+
+ def check_config(self) -> str:
+ """检查配置是否可用"""
+
+ if self.mode == "人工排查":
+ return "通用脚本不支持人工排查模式"
+ if self.mode == "设置脚本" and self.user_id is None:
+ return "设置脚本模式下用户ID不能为空"
+
+ return "Success!"
+
+ async def run(self):
+ """主进程, 运行通用脚本代理进程"""
+
+ self.current_date = datetime.now().strftime("%m-%d")
+ self.curdate = Config.server_date().strftime("%Y-%m-%d")
+ self.begin_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
+
+ await self.configure()
+ self.check_result = self.check_config()
+ if self.check_result != "Success!":
+ logger.error(f"未通过配置检查: {self.check_result}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id, type="Info", data={"Error": self.check_result}
+ ).model_dump()
+ )
+ return
+
+ # 记录配置文件
+ logger.info(f"记录通用脚本配置文件: {self.script_config_path}")
+ (Path.cwd() / f"data/{self.script_id}/Temp").mkdir(parents=True, exist_ok=True)
+ if self.script_config_path.exists():
+ if self.script_config.get("Script", "ConfigPathMode") == "Folder":
+ shutil.copytree(
+ self.script_config_path,
+ Path.cwd() / f"data/{self.script_id}/Temp",
+ dirs_exist_ok=True,
+ )
+ elif self.script_config.get("Script", "ConfigPathMode") == "File":
+ shutil.copy(
+ self.script_config_path,
+ Path.cwd() / f"data/{self.script_id}/Temp/config.temp",
+ )
+
+ # 整理用户数据, 筛选需代理的用户
+ if self.mode != "设置脚本":
+
+ self.user_list: List[Dict[str, str]] = [
+ {
+ "user_id": str(uid),
+ "status": "等待",
+ "name": config.get("Info", "Name"),
+ }
+ for uid, config in self.user_config.items()
+ if config.get("Info", "Status")
+ and config.get("Info", "RemainedDay") != 0
+ ]
+
+ logger.info(f"用户列表创建完成, 已筛选子配置数: {len(self.user_list)}")
+
+ # 自动代理模式
+ if self.mode == "自动代理":
+
+ # 执行情况预处理
+ for _ in self.user_list:
+ if (
+ self.user_config[uuid.UUID(_["user_id"])].get(
+ "Data", "LastProxyDate"
+ )
+ != self.curdate
+ ):
+ await self.user_config[uuid.UUID(_["user_id"])].set(
+ "Data", "LastProxyDate", self.curdate
+ )
+ await self.user_config[uuid.UUID(_["user_id"])].set(
+ "Data", "ProxyTimes", 0
+ )
+
+ # 开始代理
+ for self.index, user in enumerate(self.user_list):
+
+ self.cur_user_data = self.user_config[uuid.UUID(user["user_id"])]
+
+ if (self.script_config.get("Run", "ProxyTimesLimit") == 0) or (
+ self.cur_user_data.get("Data", "ProxyTimes")
+ < self.script_config.get("Run", "ProxyTimesLimit")
+ ):
+ user["status"] = "运行"
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"user_list": self.user_list},
+ ).model_dump()
+ )
+ else:
+ user["status"] = "跳过"
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={"user_list": self.user_list},
+ ).model_dump()
+ )
+ continue
+
+ logger.info(f"开始代理用户: {user['user_id']}")
+
+ self.user_start_time = datetime.now()
+
+ self.run_book = False
+
+ if not (
+ Path.cwd() / f"data/{self.script_id}/{user['user_id']}/ConfigFile"
+ ).exists():
+
+ logger.error(f"用户: {user['user_id']} - 未找到配置文件")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={"Error": f"未找到 {user['user_id']} 的配置文件"},
+ ).model_dump()
+ )
+ self.run_book = False
+ continue
+
+ # 尝试次数循环
+ for i in range(self.script_config.get("Run", "RunTimesLimit")):
+
+ if self.run_book:
+ break
+
+ logger.info(
+ f"用户 {user['user_id']} - 尝试次数: {i + 1}/{self.script_config.get('Run','RunTimesLimit')}"
+ )
+
+ # 配置脚本
+ await self.set_general()
+ # 记录当前时间
+ self.log_start_time = datetime.now()
+
+ # 执行任务前脚本
+ if (
+ self.cur_user_data.get("Info", "IfScriptBeforeTask")
+ and Path(
+ self.cur_user_data.get("Info", "ScriptBeforeTask")
+ ).exists()
+ ):
+ await self.execute_script_task(
+ Path(self.cur_user_data.get("Info", "ScriptBeforeTask")),
+ "脚本前任务",
+ )
+
+ # 启动游戏/模拟器
+ if self.script_config.get("Game", "Enabled"):
+
+ try:
+ logger.info(
+ f"启动游戏/模拟器: {self.game_path}, 参数: {self.script_config.get('Game','Arguments')}"
+ )
+ await self.game_process_manager.open_process(
+ self.game_path,
+ str(self.script_config.get("Game", "Arguments")).split(
+ " "
+ ),
+ 0,
+ )
+ except Exception as e:
+ logger.exception(f"启动游戏/模拟器时出现异常: {e}")
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Info",
+ data={"Error": f"启动游戏/模拟器时出现异常: {e}"},
+ ).model_dump()
+ )
+ self.general_result = "游戏/模拟器启动失败"
+ break
+
+ # 更新静默进程标记有效时间
+ if self.script_config.get("Game", "Type") == "Emulator":
+ logger.info(
+ f"更新静默进程标记: {self.game_path}, 标记有效时间: {datetime.now() + timedelta(seconds=self.script_config.get('Game', 'WaitTime') + 10)}"
+ )
+ Config.silence_dict[
+ self.game_path
+ ] = datetime.now() + timedelta(
+ seconds=self.script_config.get("Game", "WaitTime") + 10
+ )
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": f"正在等待游戏/模拟器完成启动\n请等待{self.script_config.get('Game', 'WaitTime')}s"
+ },
+ ).model_dump()
+ )
+ await asyncio.sleep(self.script_config.get("Game", "WaitTime"))
+
+ # 运行脚本任务
+ logger.info(
+ f"运行脚本任务: {self.script_exe_path}, 参数: {self.script_arguments}"
+ )
+ await self.general_process_manager.open_process(
+ self.script_exe_path,
+ self.script_arguments,
+ tracking_time=(
+ 60
+ if self.script_config.get("Script", "IfTrackProcess")
+ else 0
+ ),
+ )
+
+ # 监测运行状态
+ await self.general_log_monitor.start(
+ self.script_log_path, self.log_start_time
+ )
+ self.wait_event.clear()
+ await self.wait_event.wait()
+
+ await self.general_log_monitor.stop()
+
+ # 处理通用脚本结果
+ if self.general_result == "Success!":
+
+ # 标记任务完成
+ self.run_book = True
+
+ logger.info(
+ f"用户: {user['user_id']} - 通用脚本进程完成代理任务"
+ )
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": "检测到通用脚本进程完成代理任务\n正在等待相关程序结束\n请等待10s"
+ },
+ ).model_dump()
+ )
+
+ # 中止相关程序
+ logger.info(f"中止相关程序: {self.script_exe_path}")
+ await self.general_process_manager.kill()
+ await System.kill_process(self.script_exe_path)
+ if self.script_config.get("Game", "Enabled"):
+ logger.info(
+ f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
+ )
+ await self.game_process_manager.kill()
+ if self.script_config.get("Game", "IfForceClose"):
+ await System.kill_process(self.game_path)
+
+ await asyncio.sleep(10)
+
+ # 更新脚本配置文件
+ if self.script_config.get("Script", "UpdateConfigMode") in [
+ "Success",
+ "Always",
+ ]:
+
+ if (
+ self.script_config.get("Script", "ConfigPathMode")
+ == "Folder"
+ ):
+ shutil.copytree(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{user['user_id']}/ConfigFile",
+ dirs_exist_ok=True,
+ )
+ elif (
+ self.script_config.get("Script", "ConfigPathMode")
+ == "File"
+ ):
+ shutil.copy(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{user['user_id']}/ConfigFile"
+ / self.script_config_path.name,
+ )
+ logger.success("通用脚本配置文件已更新")
+
+ else:
+ logger.error(
+ f"配置: {user['user_id']} - 代理任务异常: {self.general_result}"
+ )
+ # 打印中止信息
+ # 此时, log变量内存储的就是出现异常的日志信息, 可以保存或发送用于问题排查
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id,
+ type="Update",
+ data={
+ "log": f"{self.general_result}\n正在中止相关程序\n请等待10s"
+ },
+ ).model_dump()
+ )
+
+ # 中止相关程序
+ logger.info(f"中止相关程序: {self.script_exe_path}")
+ await self.general_process_manager.kill()
+ await System.kill_process(self.script_exe_path)
+ if self.script_config.get("Game", "Enabled"):
+ logger.info(
+ f"中止游戏/模拟器进程: {list(self.game_process_manager.tracked_pids)}"
+ )
+ await self.game_process_manager.kill()
+ if self.script_config.get("Game", "IfForceClose"):
+ await System.kill_process(self.game_path)
+
+ # 推送异常通知
+ Notify.push_plyer(
+ "用户自动代理出现异常!",
+ f"用户 {user['name']} 的自动代理出现一次异常",
+ f"{user['name']} 的自动代理出现异常",
+ 3,
+ )
+
+ await asyncio.sleep(10)
+
+ # 更新脚本配置文件
+ if self.script_config.get("Script", "UpdateConfigMode") in [
+ "Failure",
+ "Always",
+ ]:
+
+ if (
+ self.script_config.get("Script", "ConfigPathMode")
+ == "Folder"
+ ):
+ shutil.copytree(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{user['user_id']}/ConfigFile",
+ dirs_exist_ok=True,
+ )
+ elif (
+ self.script_config.get("Script", "ConfigPathMode")
+ == "File"
+ ):
+ shutil.copy(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{user['user_id']}/ConfigFile"
+ / self.script_config_path.name,
+ )
+ logger.success("通用脚本配置文件已更新")
+
+ # 执行任务后脚本
+ if (
+ self.cur_user_data.get("Info", "IfScriptAfterTask")
+ and Path(
+ self.cur_user_data.get("Info", "ScriptAfterTask")
+ ).exists()
+ ):
+ await self.execute_script_task(
+ Path(self.cur_user_data.get("Info", "ScriptAfterTask")),
+ "脚本后任务",
+ )
+
+ # 保存运行日志以及统计信息
+ await Config.save_general_log(
+ Path.cwd()
+ / f"history/{self.curdate}/{user['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
+ self.general_logs,
+ self.general_result,
+ )
+
+ await self.result_record()
+
+ # 设置通用脚本模式
+ elif self.mode == "设置脚本":
+
+ # 配置通用脚本
+ await self.set_general()
+ # 创建通用脚本任务
+ logger.info(
+ f"运行脚本任务: {self.script_set_exe_path}, 参数: {self.script_set_arguments}"
+ )
+ await self.general_process_manager.open_process(
+ self.script_set_exe_path,
+ self.script_set_arguments,
+ tracking_time=(
+ 60 if self.script_config.get("Script", "IfTrackProcess") else 0
+ ),
+ )
+ # 记录当前时间
+ self.log_start_time = datetime.now()
+
+ # 监测MAA运行状态
+ await self.general_log_monitor.start(
+ self.script_log_path, self.log_start_time
+ )
+ self.wait_event.clear()
+ await self.wait_event.wait()
+ await self.general_log_monitor.stop()
+
+ async def result_record(self) -> None:
+ """记录用户结果信息"""
+
+ # 发送统计信息
+ statistics = {
+ "user_info": self.user_list[self.index]["name"],
+ "start_time": self.user_start_time.strftime("%Y-%m-%d %H:%M:%S"),
+ "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "user_result": "代理成功" if self.run_book else self.general_result,
+ }
+ await self.push_notification(
+ "统计信息",
+ f"{self.current_date} | 用户 {self.user_list[self.index]['name']} 的自动代理统计报告",
+ statistics,
+ )
+
+ if self.run_book:
+ # 成功完成代理的用户修改相关参数
+ if (
+ self.cur_user_data.get("Data", "ProxyTimes") == 0
+ and self.cur_user_data.get("Info", "RemainedDay") != -1
+ ):
+ await self.cur_user_data.set(
+ "Info",
+ "RemainedDay",
+ self.cur_user_data.get("Info", "RemainedDay") - 1,
+ )
+ await self.cur_user_data.set(
+ "Data",
+ "ProxyTimes",
+ self.cur_user_data.get("Data", "ProxyTimes") + 1,
+ )
+ self.user_list[self.index]["status"] = "完成"
+ logger.success(
+ f"用户 {self.user_list[self.index]['user_id']} 的自动代理任务已完成"
+ )
+ Notify.push_plyer(
+ "成功完成一个自动代理任务!",
+ f"已完成用户 {self.user_list[self.index]['name']} 的自动代理任务",
+ f"已完成 {self.user_list[self.index]['name']} 的自动代理任务",
+ 3,
+ )
+ else:
+ # 录入代理失败的用户
+ logger.error(
+ f"用户 {self.user_list[self.index]['user_id']} 的自动代理任务未完成"
+ )
+ self.user_list[self.index]["status"] = "异常"
+
+ async def final_task(self, task: asyncio.Task):
+ """结束时的收尾工作"""
+
+ logger.info("MAA 主任务已结束, 开始执行后续操作")
+
+ await Config.ScriptConfig[self.script_id].unlock()
+ logger.success(f"已解锁脚本配置 {self.script_id}")
+
+ # 结束各子任务
+ await self.general_process_manager.kill(if_force=True)
+ await System.kill_process(self.script_exe_path)
+ await System.kill_process(self.script_set_exe_path)
+ await self.game_process_manager.kill()
+ await self.general_log_monitor.stop()
+ del self.general_process_manager
+ del self.game_process_manager
+ del self.general_log_monitor
+
+ if self.check_result != "Success!":
+ return self.check_result
+
+ if self.mode == "自动代理" and self.user_list[self.index]["status"] == "运行":
+
+ self.general_result = "用户手动中止任务"
+
+ # 更新脚本配置文件
+ if self.script_config.get("Script", "UpdateConfigMode") in [
+ "Failure",
+ "Always",
+ ]:
+
+ if self.script_config.get("Script", "ConfigPathMode") == "Folder":
+ shutil.copytree(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/ConfigFile",
+ dirs_exist_ok=True,
+ )
+ elif self.script_config.get("Script", "ConfigPathMode") == "File":
+ shutil.copy(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/ConfigFile"
+ / self.script_config_path.name,
+ )
+ logger.success("通用脚本配置文件已更新")
+
+ # 执行任务后脚本
+ if (
+ self.cur_user_data.get("Info", "IfScriptAfterTask")
+ and Path(self.cur_user_data.get("Info", "ScriptAfterTask")).exists()
+ ):
+ await self.execute_script_task(
+ Path(self.cur_user_data.get("Info", "ScriptAfterTask")),
+ "脚本后任务",
+ )
+
+ # 保存运行日志以及统计信息
+ await Config.save_general_log(
+ Path.cwd()
+ / f"history/{self.curdate}/{self.user_list[self.index]['name']}/{self.log_start_time.strftime('%H-%M-%S')}.log",
+ self.general_logs,
+ self.general_result,
+ )
+
+ await self.result_record()
+
+ # 导出结果
+ if self.mode == "自动代理":
+
+ # 更新用户数据
+ sc = Config.ScriptConfig[self.script_id]
+ if isinstance(sc, GeneralConfig):
+ await sc.UserData.load(await self.user_config.toDict())
+ await Config.ScriptConfig.save()
+
+ error_user = [_["name"] for _ in self.user_list if _["status"] == "异常"]
+ over_user = [_["name"] for _ in self.user_list if _["status"] == "完成"]
+ wait_user = [_["name"] for _ in self.user_list if _["status"] == "等待"]
+
+ # 保存运行日志
+ title = (
+ f"{self.current_date} | {self.script_config.get("Info", "Name")}的{self.mode}任务报告"
+ if self.script_config.get("Info", "Name") != ""
+ else f"{self.current_date} | {self.mode}任务报告"
+ )
+ result = {
+ "title": f"{self.mode}任务报告",
+ "script_name": (
+ self.script_config.get("Info", "Name")
+ if self.script_config.get("Info", "Name") != ""
+ else "空白"
+ ),
+ "start_time": self.begin_time,
+ "end_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
+ "completed_count": len(over_user),
+ "uncompleted_count": len(error_user) + len(wait_user),
+ "failed_user": error_user,
+ "waiting_user": wait_user,
+ }
+
+ # 生成结果文本
+ 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}未成功的用户: \n{"\n".join(result['failed_user'])}\n"
+ )
+ if len(result["waiting_user"]) > 0:
+ result_text += f"\n未开始{self.mode}的用户: \n{"\n".join(result['waiting_user'])}\n"
+
+ # 推送代理结果通知
+ Notify.push_plyer(
+ title.replace("报告", "已完成!"),
+ f"已完成配置数: {len(over_user)}, 未完成配置数: {len(error_user) + len(wait_user)}",
+ f"已完成配置数: {len(over_user)}, 未完成配置数: {len(error_user) + len(wait_user)}",
+ 10,
+ )
+ await self.push_notification("代理结果", title, result)
+
+ elif self.mode == "设置脚本":
+
+ (Path.cwd() / f"data/{self.script_id}/{self.user_id}/ConfigFile").mkdir(
+ parents=True, exist_ok=True
+ )
+ if self.script_config.get("Script", "ConfigPathMode") == "Folder":
+ shutil.copytree(
+ self.script_config_path,
+ Path.cwd() / f"data/{self.script_id}/{self.user_id}/ConfigFile",
+ dirs_exist_ok=True,
+ )
+ logger.success(
+ f"通用脚本配置已保存到: {Path.cwd() / f'data/{self.script_id}/{self.user_id}/ConfigFile'}"
+ )
+ elif self.script_config.get("Script", "ConfigPathMode") == "File":
+ shutil.copy(
+ self.script_config_path,
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/ConfigFile"
+ / self.script_config_path.name,
+ )
+ logger.success(
+ f"通用脚本配置已保存到: {Path.cwd() / f'data/{self.script_id}/{self.user_id}/ConfigFile' / self.script_config_path.name}"
+ )
+ result_text = ""
+
+ # 复原通用脚本配置文件
+ if (
+ self.script_config.get("Script", "ConfigPathMode") == "Folder"
+ and (Path.cwd() / f"data/{self.script_id}/Temp").exists()
+ ):
+ logger.info(
+ f"复原通用脚本配置文件: {Path.cwd() / f"data/{self.script_id}/Temp"}"
+ )
+ shutil.copytree(
+ Path.cwd() / f"data/{self.script_id}/Temp",
+ self.script_config_path,
+ dirs_exist_ok=True,
+ )
+ shutil.rmtree(Path.cwd() / f"data/{self.script_id}/Temp")
+ elif (
+ self.script_config.get("Script", "ConfigPathMode") == "File"
+ and (Path.cwd() / f"data/{self.script_id}/Temp/config.temp").exists()
+ ):
+ logger.info(
+ f"复原通用脚本配置文件: {Path.cwd() / f"data/{self.script_id}/Temp/config.temp"}"
+ )
+ shutil.copy(
+ Path.cwd() / f"data/{self.script_id}/Temp/config.temp",
+ self.script_config_path,
+ )
+ shutil.rmtree(Path.cwd() / f"data/{self.script_id}/Temp")
+
+ return result_text
+
+ async def check_general_log(self, log_content: List[str]) -> None:
+ """获取脚本日志并检查以判断脚本程序运行状态"""
+
+ self.general_logs = log_content
+ log = "".join(log_content)
+
+ # 更新日志
+ if await self.general_process_manager.is_running():
+
+ await Config.send_json(
+ WebSocketMessage(
+ id=self.ws_id, type="Update", data={"log": log}
+ ).model_dump()
+ )
+
+ if "自动代理" in self.mode:
+
+ # 获取最近一条日志的时间
+ latest_time = self.log_start_time
+ for _ in self.general_logs[::-1]:
+ try:
+ latest_time = strptime(
+ _[self.log_time_range[0] : self.log_time_range[1]],
+ self.script_config.get("Script", "LogTimeFormat"),
+ self.log_start_time,
+ )
+ break
+ except ValueError:
+ pass
+
+ logger.info(f"通用脚本最近一条日志时间: {latest_time}")
+
+ for success_sign in self.success_log:
+ if success_sign in log:
+ self.general_result = "Success!"
+ break
+ else:
+
+ if datetime.now() - latest_time > timedelta(
+ minutes=self.script_config.get("Run", "RunTimeLimit")
+ ):
+ self.general_result = "脚本进程超时"
+ else:
+ for error_sign in self.error_log:
+ if error_sign in log:
+ self.general_result = f"异常日志: {error_sign}"
+ break
+ else:
+ if await self.general_process_manager.is_running():
+ self.general_result = "Wait"
+ elif self.success_log:
+ self.general_result = "脚本在完成任务前退出"
+ else:
+ self.general_result = "Success!"
+
+ elif self.mode == "设置通用脚本":
+ if await self.general_process_manager.is_running():
+ self.general_result = "Wait"
+ else:
+ self.general_result = "Success!"
+
+ logger.info(f"通用脚本日志分析结果: {self.general_result}")
+
+ if self.general_result != "Wait":
+
+ logger.info(f"MAA 任务结果: {self.general_result}, 日志锁已释放")
+ self.wait_event.set()
+
+ async def set_general(self) -> None:
+ """配置通用脚本运行参数"""
+ logger.info(f"开始配置脚本运行参数: {self.mode}")
+
+ # 配置前关闭可能未正常退出的脚本进程
+ if self.mode == "自动代理":
+ await System.kill_process(self.script_exe_path)
+ elif self.mode == "设置脚本":
+ await System.kill_process(self.script_set_exe_path)
+
+ # 预导入配置文件
+ if self.mode == "设置脚本":
+ if (
+ self.script_config.get("Script", "ConfigPathMode") == "Folder"
+ and (
+ Path.cwd() / f"data/{self.script_id}/{self.user_id}/ConfigFile"
+ ).exists()
+ ):
+ shutil.copytree(
+ Path.cwd() / f"data/{self.script_id}/{self.user_id}/ConfigFile",
+ self.script_config_path,
+ dirs_exist_ok=True,
+ )
+ elif (
+ self.script_config.get("Script", "ConfigPathMode") == "File"
+ and (
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/ConfigFile"
+ / self.script_config_path.name
+ ).exists()
+ ):
+ shutil.copy(
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_id}/ConfigFile"
+ / self.script_config_path.name,
+ self.script_config_path,
+ )
+ else:
+ if self.script_config.get("Script", "ConfigPathMode") == "Folder":
+ shutil.copytree(
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/ConfigFile",
+ self.script_config_path,
+ dirs_exist_ok=True,
+ )
+ elif self.script_config.get("Script", "ConfigPathMode") == "File":
+ shutil.copy(
+ Path.cwd()
+ / f"data/{self.script_id}/{self.user_list[self.index]['user_id']}/ConfigFile"
+ / self.script_config_path.name,
+ self.script_config_path,
+ )
+
+ logger.info(f"脚本运行参数配置完成: {self.mode}")
+
+ async def execute_script_task(self, script_path: Path, task_name: str) -> bool:
+ """执行脚本任务并等待结束"""
+
+ try:
+ logger.info(f"开始执行{task_name}: {script_path}")
+
+ # 根据文件类型选择执行方式
+ 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}脚本没有指定后缀名, 无法执行")
+ 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("Function", "IfSilence")
+ else 0
+ ),
+ timeout=600,
+ capture_output=True,
+ errors="ignore",
+ )
+
+ if result.returncode == 0:
+ logger.info(f"{task_name}执行成功")
+ if result.stdout.strip():
+ logger.info(f"{task_name}输出: {result.stdout}")
+ return True
+ else:
+ logger.error(f"{task_name}执行失败, 返回码: {result.returncode}")
+ if result.stderr.strip():
+ logger.error(f"{task_name}错误输出: {result.stderr}")
+ return False
+
+ except subprocess.TimeoutExpired:
+ logger.error(f"{task_name}执行超时")
+ return False
+ except Exception as e:
+ logger.exception(f"执行{task_name}时出现异常: {e}")
+ return False
+
+ async def push_notification(self, mode: str, title: str, message) -> None:
+ """通过所有渠道推送通知"""
+
+ logger.info(f"开始推送通知, 模式: {mode}, 标题: {title}")
+
+ env = Environment(loader=FileSystemLoader(str(Path.cwd() / "res/html")))
+
+ if mode == "代理结果" and (
+ Config.get("Notify", "SendTaskResultTime") == "任何时刻"
+ or (
+ Config.get("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("general_result.html")
+ message_html = template.render(message)
+
+ # ServerChan的换行是两个换行符。故而将\n替换为\n\n
+ serverchan_message = message_text.replace("\n", "\n\n")
+
+ # 发送全局通知
+
+ if Config.get("Notify", "IfSendMail"):
+ Notify.send_mail(
+ "网页", title, message_html, Config.get("Notify", "ToAddress")
+ )
+
+ if Config.get("Notify", "IfServerChan"):
+ Notify.ServerChanPush(
+ title,
+ f"{serverchan_message}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "ServerChanKey"),
+ )
+
+ if Config.get("Notify", "IfCompanyWebHookBot"):
+ Notify.WebHookPush(
+ title,
+ f"{message_text}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
+ )
+
+ elif mode == "统计信息":
+
+ message_text = (
+ f"开始时间: {message['start_time']}\n"
+ f"结束时间: {message['end_time']}\n"
+ f"通用脚本执行结果: {message['user_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("Notify", "IfSendStatistic"):
+
+ if Config.get("Notify", "IfSendMail"):
+ Notify.send_mail(
+ "网页", title, message_html, Config.get("Notify", "ToAddress")
+ )
+
+ if Config.get("Notify", "IfServerChan"):
+ Notify.ServerChanPush(
+ title,
+ f"{serverchan_message}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "ServerChanKey"),
+ )
+
+ if Config.get("Notify", "IfCompanyWebHookBot"):
+ Notify.WebHookPush(
+ title,
+ f"{message_text}\n\nAUTO_MAA 敬上",
+ Config.get("Notify", "CompanyWebHookBotUrl"),
+ )
+
+ # 发送用户单独通知
+ if self.cur_user_data.get("Notify", "Enabled") and self.cur_user_data.get(
+ "Notify", "IfSendStatistic"
+ ):
+
+ # 发送邮件通知
+ if self.cur_user_data.get("Notify", "IfSendMail"):
+ if self.cur_user_data.get("Notify", "ToAddress"):
+ Notify.send_mail(
+ "网页",
+ title,
+ message_html,
+ self.cur_user_data.get("Notify", "ToAddress"),
+ )
+ else:
+ logger.error(f"用户邮箱地址为空, 无法发送用户单独的邮件通知")
+
+ # 发送ServerChan通知
+ if self.cur_user_data.get("Notify", "IfServerChan"):
+ if self.cur_user_data.get("Notify", "ServerChanKey"):
+ Notify.ServerChanPush(
+ title,
+ f"{serverchan_message}\n\nAUTO_MAA 敬上",
+ self.cur_user_data.get("Notify", "ServerChanKey"),
+ )
+ else:
+ logger.error(
+ "用户ServerChan密钥为空, 无法发送用户单独的ServerChan通知"
+ )
+
+ # 推送CompanyWebHookBot通知
+ if self.cur_user_data.get("Notify", "IfCompanyWebHookBot"):
+ if self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"):
+ Notify.WebHookPush(
+ title,
+ f"{message_text}\n\nAUTO_MAA 敬上",
+ self.cur_user_data.get("Notify", "CompanyWebHookBotUrl"),
+ )
+ else:
+ logger.error(
+ "用户CompanyWebHookBot密钥为空, 无法发送用户单独的CompanyWebHookBot通知"
+ )
+
+ return None
diff --git a/app/services/skland.py b/app/task/skland.py
similarity index 77%
rename from app/services/skland.py
rename to app/task/skland.py
index 901a352..2ecf8ea 100644
--- a/app/services/skland.py
+++ b/app/task/skland.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 ClozyA
# This file incorporates work covered by the following copyright and
# permission notice:
@@ -25,24 +26,21 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA森空岛服务
-v4.4
-作者:DLmaster_361、ClozyA
-"""
-
import time
import json
import hmac
+import asyncio
import hashlib
import requests
from urllib import parse
-from app.core import Config, logger
+from app.core import Config
+from app.utils.logger import get_logger
+
+logger = get_logger("森空岛签到任务")
-def skland_sign_in(token) -> dict:
+async def skland_sign_in(token) -> dict:
"""森空岛签到"""
app_code = "4ca99fa6b56cc2ba"
@@ -76,11 +74,11 @@ def skland_sign_in(token) -> dict:
:param token_for_sign: 用于加密的token
:param path: 请求路径(如 /api/v1/game/player/binding)
- :param body_or_query: GET用query字符串,POST用body字符串
+ :param body_or_query: GET用query字符串, POST用body字符串
:return: (sign, 新的header_for_sign字典)
"""
- t = str(int(time.time()) - 2) # 时间戳,-2秒以防服务器时间不一致
+ 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
@@ -127,7 +125,7 @@ def skland_sign_in(token) -> dict:
v["cred"] = cred
return v
- def login_by_token(token_code):
+ async def login_by_token(token_code):
"""
使用token一步步拿到cred和sign_token
@@ -140,10 +138,10 @@ def skland_sign_in(token) -> dict:
token_code = t["data"]["content"]
except:
pass
- grant_code = get_grant_code(token_code)
- return get_cred(grant_code)
+ grant_code = await get_grant_code(token_code)
+ return await get_cred(grant_code)
- def get_cred(grant):
+ async def get_cred(grant):
"""
通过grant code获取cred和sign_token
@@ -155,18 +153,15 @@ def skland_sign_in(token) -> dict:
cred_code_url,
json={"code": grant, "kind": 1},
headers=header_login,
- proxies={
- "http": Config.get(Config.update_ProxyAddress),
- "https": Config.get(Config.update_ProxyAddress),
- },
+ proxies=Config.get_proxies(),
).json()
if rsp["code"] != 0:
- raise Exception(f'获得cred失败:{rsp.get("messgae")}')
+ raise Exception(f"获得cred失败: {rsp.get('message')}")
sign_token = rsp["data"]["token"]
cred = rsp["data"]["cred"]
return cred, sign_token
- def get_grant_code(token):
+ async def get_grant_code(token):
"""
通过token获取grant code
@@ -177,18 +172,15 @@ def skland_sign_in(token) -> dict:
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),
- },
+ proxies=Config.get_proxies(),
).json()
if rsp["status"] != 0:
raise Exception(
- f'使用token: {token[:3]}******{token[-3:]} 获得认证代码失败:{rsp.get("msg")}'
+ f"使用token: {token[:3]}******{token[-3:]} 获得认证代码失败: {rsp.get('msg')}"
)
return rsp["data"]["code"]
- def get_binding_list(cred, sign_token):
+ async def get_binding_list(cred, sign_token):
"""
查询已绑定的角色列表
@@ -202,21 +194,12 @@ def skland_sign_in(token) -> dict:
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),
- },
+ proxies=Config.get_proxies(),
).json()
if rsp["code"] != 0:
- logger.error(
- f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}",
- module="森空岛签到",
- )
+ logger.error(f"请求角色列表出现问题: {rsp['message']}")
if rsp.get("message") == "用户未登录":
- logger.error(
- f"森空岛服务 | 用户登录可能失效了,请重新登录!",
- module="森空岛签到",
- )
+ logger.error(f"用户登录可能失效了, 请重新登录!")
return v
# 只取明日方舟(arknights)的绑定账号
for i in rsp["data"]["list"]:
@@ -225,7 +208,7 @@ def skland_sign_in(token) -> dict:
v.extend(i.get("bindingList"))
return v
- def do_sign(cred, sign_token) -> dict:
+ async def do_sign(cred, sign_token) -> dict:
"""
对所有绑定的角色进行签到
@@ -234,7 +217,7 @@ def skland_sign_in(token) -> dict:
:return: 签到结果字典
"""
- characters = get_binding_list(cred, sign_token)
+ characters = await get_binding_list(cred, sign_token)
result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)}
for character in characters:
@@ -249,10 +232,7 @@ def skland_sign_in(token) -> dict:
sign_url, "post", body, copy_header(cred), sign_token
),
json=body,
- proxies={
- "http": Config.get(Config.update_ProxyAddress),
- "https": Config.get(Config.update_ProxyAddress),
- },
+ proxies=Config.get_proxies(),
).json()
if rsp["code"] != 0:
@@ -276,10 +256,10 @@ def skland_sign_in(token) -> dict:
# 主流程
try:
# 拿到cred和sign_token
- cred, sign_token = login_by_token(token)
- time.sleep(1)
+ cred, sign_token = await login_by_token(token)
+ await asyncio.sleep(1)
# 依次签到
- return do_sign(cred, sign_token)
+ return await do_sign(cred, sign_token)
except Exception as e:
- logger.exception(f"森空岛服务 | 森空岛签到失败: {e}", module="森空岛签到")
+ logger.exception(f"森空岛签到失败: {e}")
return {"成功": [], "重复": [], "失败": [], "总计": 0}
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/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
index 2631dc9..7d49f14 100644
--- a/app/utils/ImageUtils.py
+++ b/app/utils/ImageUtils.py
@@ -18,12 +18,6 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA图像组件
-v4.4
-作者:ClozyA
-"""
import base64
import hashlib
@@ -54,12 +48,10 @@ class ImageUtils:
@staticmethod
def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path:
"""
- 如果图片大于max_size_mb,则压缩并覆盖原文件,返回原始路径(Path对象)
+ 如果图片大于max_size_mb, 则压缩并覆盖原文件, 返回原始路径(Path对象)
"""
- if hasattr(Image, "Resampling"): # Pillow 9.1.0及以后
- RESAMPLE = Image.Resampling.LANCZOS
- else:
- RESAMPLE = Image.ANTIALIAS
+
+ RESAMPLE = Image.Resampling.LANCZOS # Pillow 9.1.0及以后
max_size = max_size_mb * 1024 * 1024
if image_path.stat().st_size <= max_size:
@@ -70,7 +62,7 @@ class ImageUtils:
quality = 90 if suffix in [".jpg", ".jpeg"] else None
step = 5
- if suffix in [".jpg", ".jpeg"]:
+ if quality is not None:
while True:
img.save(image_path, quality=quality, optimize=True)
if image_path.stat().st_size <= max_size or quality <= 10:
@@ -90,6 +82,6 @@ class ImageUtils:
height = int(height * 0.95)
img = img.resize((width, height), RESAMPLE)
else:
- raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩。")
+ raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩")
return image_path
diff --git a/app/utils/LogMonitor.py b/app/utils/LogMonitor.py
new file mode 100644
index 0000000..91118f4
--- /dev/null
+++ b/app/utils/LogMonitor.py
@@ -0,0 +1,156 @@
+import asyncio
+import aiofiles
+import os
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Callable, Optional, List, Awaitable
+
+from .logger import get_logger
+
+logger = get_logger("日志监控器")
+
+TIME_FIELDS = {
+ "%Y": "year",
+ "%m": "month",
+ "%d": "day",
+ "%H": "hour",
+ "%M": "minute",
+ "%S": "second",
+ "%f": "microsecond",
+}
+"""时间字段映射表"""
+
+
+def strptime(date_string: str, format: str, default_date: datetime) -> datetime:
+ """根据指定格式解析日期字符串"""
+
+ 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)
+
+
+class LogMonitor:
+ def __init__(
+ self,
+ time_stamp_range: tuple[int, int],
+ time_format: str,
+ callback: Callable[[List[str]], Awaitable[None]],
+ encoding: str = "utf-8",
+ ):
+ self.time_stamp_range = time_stamp_range
+ self.time_format = time_format
+ self.callback = callback
+ self.encoding = encoding
+ self.log_file_path: Optional[Path] = None
+ self.log_start_time: datetime = datetime.now()
+ self.last_callback_time: datetime = datetime.now()
+ self.log_contents: List[str] = []
+ self.task: Optional[asyncio.Task] = None
+ self.__is_running = False
+
+ async def monitor_log(self):
+ """监控日志文件的主循环"""
+ if self.log_file_path is None or not self.log_file_path.exists():
+ raise ValueError("日志文件路径未设置或文件不存在")
+
+ logger.info(f"开始监控日志文件: {self.log_file_path}")
+
+ while self.__is_running:
+ logger.debug("正在检查日志文件...")
+ log_contents = []
+ if_log_start = False
+
+ # 检查文件是否仍然存在
+ if not self.log_file_path.exists():
+ logger.warning(f"日志文件不存在: {self.log_file_path}")
+ continue
+
+ # 尝试读取文件
+ try:
+ async with aiofiles.open(
+ self.log_file_path, "r", encoding=self.encoding
+ ) as f:
+ async for line in f:
+ if not if_log_start:
+ try:
+ entry_time = strptime(
+ line[
+ self.time_stamp_range[
+ 0
+ ] : self.time_stamp_range[1]
+ ],
+ self.time_format,
+ self.last_callback_time,
+ )
+ if entry_time > self.log_start_time:
+ if_log_start = True
+ log_contents.append(line)
+ except (ValueError, IndexError):
+ continue
+ else:
+ log_contents.append(line)
+
+ except (FileNotFoundError, PermissionError) as e:
+ logger.warning(f"文件访问错误: {e}")
+ await asyncio.sleep(5)
+ continue
+ except UnicodeDecodeError as e:
+ logger.error(f"文件编码错误: {e}")
+ await asyncio.sleep(10)
+ continue
+
+ # 调用回调
+ if (
+ log_contents != self.log_contents
+ or datetime.now() - self.last_callback_time > timedelta(minutes=1)
+ ):
+ self.log_contents = log_contents
+ self.last_callback_time = datetime.now()
+
+ # 安全调用回调函数
+ try:
+ await self.callback(log_contents)
+ except Exception as e:
+ logger.error(f"回调函数执行失败: {e}")
+
+ await asyncio.sleep(1)
+
+ async def start(self, log_file_path: Path, start_time: datetime) -> None:
+ """启动监控"""
+
+ if log_file_path.is_dir():
+ raise ValueError(f"日志文件不能是目录: {log_file_path}")
+
+ if self.task is not None and not self.task.done():
+ await self.stop()
+
+ self.__is_running = True
+ self.log_contents = []
+ self.log_file_path = log_file_path
+ self.log_start_time = start_time
+ self.task = asyncio.create_task(self.monitor_log())
+ logger.info(f"日志监控已启动: {self.log_file_path}")
+
+ async def stop(self):
+ """停止监控"""
+
+ logger.info("请求取消日志监控任务")
+
+ if self.task is not None and not self.task.done():
+ self.task.cancel()
+
+ try:
+ await self.task
+ except asyncio.CancelledError:
+ logger.info("日志监控任务已中止")
+
+ logger.success("日志监控任务已停止")
+ self.task = None
diff --git a/app/utils/ProcessManager.py b/app/utils/ProcessManager.py
index a57406d..07eff3e 100644
--- a/app/utils/ProcessManager.py
+++ b/app/utils/ProcessManager.py
@@ -18,44 +18,36 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA进程管理组件
-v4.4
-作者:DLmaster_361
-"""
-
+import asyncio
import psutil
import subprocess
+from datetime import datetime, timedelta
from pathlib import Path
-from datetime import datetime
-
-from PySide6.QtCore import QTimer, QObject, Signal
-class ProcessManager(QObject):
- """进程监视器类,用于跟踪主进程及其所有子进程的状态"""
-
- processClosed = Signal()
+class ProcessManager:
+ """进程监视器类, 用于跟踪主进程及其所有子进程的状态"""
def __init__(self):
super().__init__()
self.main_pid = None
self.tracked_pids = set()
+ self.check_task = None
+ self.track_end_time = datetime.now()
- 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:
+ async def open_process(
+ self, path: Path, args: list = [], tracking_time: int = 60
+ ) -> None:
"""
- 启动一个新进程并返回其pid,并开始监视该进程
+ 启动一个新进程并返回其pid, 并开始监视该进程
- :param path: 可执行文件的路径
- :param args: 启动参数列表
- :param tracking_time: 子进程追踪持续时间(秒)
- :return: 新进程的PID
+ Parameters
+ ----------
+ path: 可执行文件的路径
+ args: 启动参数列表
+ tracking_time: 子进程追踪持续时间(秒)
"""
process = subprocess.Popen(
@@ -67,17 +59,17 @@ class ProcessManager(QObject):
stderr=subprocess.DEVNULL,
)
- self.start_monitoring(process.pid, tracking_time)
+ await self.start_monitoring(process.pid, tracking_time)
- def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
+ async def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
"""
- 启动进程监视器,跟踪指定的主进程及其子进程
+ 启动进程监视器, 跟踪指定的主进程及其子进程
:param pid: 被监视进程的PID
:param tracking_time: 子进程追踪持续时间(秒)
"""
- self.clear()
+ await self.clear()
self.main_pid = pid
self.tracking_time = tracking_time
@@ -96,16 +88,15 @@ class ProcessManager(QObject):
except psutil.NoSuchProcess:
pass
- # 启动持续追踪机制
- self.start_time = datetime.now()
- self.check_timer.start(100)
+ # 启动持续追踪任务
+ if tracking_time > 0:
+ self.track_end_time = datetime.now() + timedelta(seconds=tracking_time)
+ self.check_task = asyncio.create_task(self.track_processes())
- def check_processes(self) -> None:
- """检查跟踪的进程是否仍在运行,并更新子进程列表"""
-
- # 仅在时限内持续更新跟踪的进程列表,发现新的子进程
- if (datetime.now() - self.start_time).total_seconds() < self.tracking_time:
+ async def track_processes(self) -> None:
+ """更新子进程列表"""
+ while datetime.now() < self.track_end_time:
current_pids = set(self.tracked_pids)
for pid in current_pids:
try:
@@ -116,12 +107,9 @@ class ProcessManager(QObject):
self.tracked_pids.add(child.pid)
except psutil.NoSuchProcess:
continue
+ await asyncio.sleep(0.1)
- if not self.is_running():
- self.clear()
- self.processClosed.emit()
-
- def is_running(self) -> bool:
+ async def is_running(self) -> bool:
"""检查所有跟踪的进程是否还在运行"""
for pid in self.tracked_pids:
@@ -134,11 +122,9 @@ class ProcessManager(QObject):
return False
- def kill(self, if_force: bool = False) -> None:
+ async def kill(self, if_force: bool = False) -> None:
"""停止监视器并中止所有跟踪的进程"""
- self.check_timer.stop()
-
for pid in self.tracked_pids:
try:
proc = psutil.Process(pid)
@@ -152,13 +138,18 @@ class ProcessManager(QObject):
except psutil.NoSuchProcess:
continue
- if self.main_pid:
- self.processClosed.emit()
- self.clear()
+ await self.clear()
- def clear(self) -> None:
+ async def clear(self) -> None:
"""清空跟踪的进程列表"""
+ if self.check_task is not None and not self.check_task.done():
+ self.check_task.cancel()
+
+ try:
+ await self.check_task
+ except asyncio.CancelledError:
+ pass
+
self.main_pid = None
- self.check_timer.stop()
self.tracked_pids.clear()
diff --git a/app/utils/__init__.py b/app/utils/__init__.py
index ead5c44..c471269 100644
--- a/app/utils/__init__.py
+++ b/app/utils/__init__.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,18 +19,25 @@
# Contact: DLmaster_361@163.com
-"""
-AUTO_MAA
-AUTO_MAA工具包
-v4.4
-作者:DLmaster_361
-"""
-
-__version__ = "4.2.0"
+__version__ = "5.0.0"
__author__ = "DLmaster361 "
__license__ = "GPL-3.0 license"
-from .ImageUtils import ImageUtils
-from .ProcessManager import ProcessManager
-__all__ = ["ImageUtils", "ProcessManager"]
+from .constants import *
+from .logger import get_logger
+from .ImageUtils import ImageUtils
+from .LogMonitor import LogMonitor, strptime
+from .ProcessManager import ProcessManager
+from .security import dpapi_encrypt, dpapi_decrypt
+
+__all__ = [
+ "constants",
+ "get_logger",
+ "ImageUtils",
+ "LogMonitor",
+ "ProcessManager",
+ "dpapi_encrypt",
+ "dpapi_decrypt",
+ "strptime",
+]
diff --git a/app/utils/constants.py b/app/utils/constants.py
new file mode 100644
index 0000000..1291491
--- /dev/null
+++ b/app/utils/constants.py
@@ -0,0 +1,214 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# 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
+
+
+RESOURCE_STAGE_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]},
+]
+"""常规资源关信息"""
+
+
+RESOURCE_STAGE_DROP_INFO = {
+ "CE-6": {
+ "Display": "CE-6",
+ "Value": "CE-6",
+ "Drop": "4001",
+ "DropName": "龙门币",
+ "Activity": {"Tip": "二四六日", "StageName": "资源关卡"},
+ },
+ "AP-5": {
+ "Display": "AP-5",
+ "Value": "AP-5",
+ "Drop": "4006",
+ "DropName": "采购凭证",
+ "Activity": {"Tip": "一四六日", "StageName": "资源关卡"},
+ },
+ "CA-5": {
+ "Display": "CA-5",
+ "Value": "CA-5",
+ "Drop": "3303",
+ "DropName": "技巧概要",
+ "Activity": {"Tip": "二三五日", "StageName": "资源关卡"},
+ },
+ "LS-6": {
+ "Display": "LS-6",
+ "Value": "LS-6",
+ "Drop": "2004",
+ "DropName": "作战记录",
+ "Activity": {"Tip": "常驻开放", "StageName": "资源关卡"},
+ },
+ "SK-5": {
+ "Display": "SK-5",
+ "Value": "SK-5",
+ "Drop": "3114",
+ "DropName": "碳素组",
+ "Activity": {"Tip": "一三五六", "StageName": "资源关卡"},
+ },
+ "PR-A-1": {
+ "Display": "PR-A",
+ "Value": "PR-A",
+ "Drop": "PR-A",
+ "DropName": "医疗/重装芯片",
+ "Activity": {"Tip": "一四五日", "StageName": "资源关卡"},
+ },
+ "PR-B-1": {
+ "Display": "PR-B",
+ "Value": "PR-B",
+ "Drop": "PR-B",
+ "DropName": "术师/狙击芯片",
+ "Activity": {"Tip": "一二五六", "StageName": "资源关卡"},
+ },
+ "PR-C-1": {
+ "Display": "PR-C",
+ "Value": "PR-C",
+ "Drop": "PR-C",
+ "DropName": "先锋/辅助芯片",
+ "Activity": {"Tip": "三四六日", "StageName": "资源关卡"},
+ },
+ "PR-D-1": {
+ "Display": "PR-D",
+ "Value": "PR-D",
+ "Drop": "PR-D",
+ "DropName": "近卫/特种芯片",
+ "Activity": {"Tip": "二三六日", "StageName": "资源关卡"},
+ },
+}
+"""常规资源关掉落信息"""
+
+MATERIALS_MAP = {
+ "4001": "龙门币",
+ "4006": "采购凭证",
+ "2004": "高级作战记录",
+ "2003": "中级作战记录",
+ "2002": "初级作战记录",
+ "2001": "基础作战记录",
+ "3303": "技巧概要·卷3",
+ "3302": "技巧概要·卷2",
+ "3301": "技巧概要·卷1",
+ "30165": "重相位对映体",
+ "30155": "烧结核凝晶",
+ "30145": "晶体电子单元",
+ "30135": "D32钢",
+ "30125": "双极纳米片",
+ "30115": "聚合剂",
+ "31094": "手性屈光体",
+ "31093": "类凝结核",
+ "31084": "环烃预制体",
+ "31083": "环烃聚质",
+ "31074": "固化纤维板",
+ "31073": "褐素纤维",
+ "31064": "转质盐聚块",
+ "31063": "转质盐组",
+ "31054": "切削原液",
+ "31053": "化合切削液",
+ "31044": "精炼溶剂",
+ "31043": "半自然溶剂",
+ "31034": "晶体电路",
+ "31033": "晶体元件",
+ "31024": "炽合金块",
+ "31023": "炽合金",
+ "31014": "聚合凝胶",
+ "31013": "凝胶",
+ "30074": "白马醇",
+ "30073": "扭转醇",
+ "30084": "三水锰矿",
+ "30083": "轻锰矿",
+ "30094": "五水研磨石",
+ "30093": "研磨石",
+ "30104": "RMA70-24",
+ "30103": "RMA70-12",
+ "30014": "提纯源岩",
+ "30013": "固源岩组",
+ "30012": "固源岩",
+ "30011": "源岩",
+ "30064": "改量装置",
+ "30063": "全新装置",
+ "30062": "装置",
+ "30061": "破损装置",
+ "30034": "聚酸酯块",
+ "30033": "聚酸酯组",
+ "30032": "聚酸酯",
+ "30031": "酯原料",
+ "30024": "糖聚块",
+ "30023": "糖组",
+ "30022": "糖",
+ "30021": "代糖",
+ "30044": "异铁块",
+ "30043": "异铁组",
+ "30042": "异铁",
+ "30041": "异铁碎片",
+ "30054": "酮阵列",
+ "30053": "酮凝集组",
+ "30052": "酮凝集",
+ "30051": "双酮",
+ "3114": "碳素组",
+ "3113": "碳素",
+ "3112": "碳",
+ "3213": "先锋双芯片",
+ "3223": "近卫双芯片",
+ "3233": "重装双芯片",
+ "3243": "狙击双芯片",
+ "3253": "术师双芯片",
+ "3263": "医疗双芯片",
+ "3273": "辅助双芯片",
+ "3283": "特种双芯片",
+ "3212": "先锋芯片组",
+ "3222": "近卫芯片组",
+ "3232": "重装芯片组",
+ "3242": "狙击芯片组",
+ "3252": "术师芯片组",
+ "3262": "医疗芯片组",
+ "3272": "辅助芯片组",
+ "3282": "特种芯片组",
+ "3211": "先锋芯片",
+ "3221": "近卫芯片",
+ "3231": "重装芯片",
+ "3241": "狙击芯片",
+ "3251": "术师芯片",
+ "3261": "医疗芯片",
+ "3271": "辅助芯片",
+ "3281": "特种芯片",
+ "PR-A": "医疗/重装芯片",
+ "PR-B": "术师/狙击芯片",
+ "PR-C": "先锋/辅助芯片",
+ "PR-D": "近卫/特种芯片",
+}
+"""掉落物索引表"""
diff --git a/app/utils/logger.py b/app/utils/logger.py
new file mode 100644
index 0000000..bd541f8
--- /dev/null
+++ b/app/utils/logger.py
@@ -0,0 +1,69 @@
+# AUTO_MAA:A MAA Multi Account Management and Automation Tool
+# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
+
+# 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
+
+
+from loguru import logger as _logger
+import sys
+from pathlib import Path
+
+(Path.cwd() / "debug").mkdir(parents=True, exist_ok=True)
+
+
+_logger.remove()
+
+
+_logger.add(
+ sink=Path.cwd() / "debug/app.log",
+ level="INFO",
+ 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 = _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"]
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/app/utils/security.py b/app/utils/security.py
new file mode 100644
index 0000000..6dad28a
--- /dev/null
+++ b/app/utils/security.py
@@ -0,0 +1,69 @@
+# 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
+
+
+import base64
+import win32crypt
+
+
+def dpapi_encrypt(
+ note: str, description: None | str = None, entropy: None | 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 dpapi_decrypt(note: str, entropy: None | 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")
diff --git a/dev.md b/dev.md
new file mode 100644
index 0000000..24348f2
--- /dev/null
+++ b/dev.md
@@ -0,0 +1,23 @@
+
+## 创建环境
+```bash
+uv venv
+```
+
+## 安装依赖
+```bash
+uv pip install -e .
+```
+
+
+## 添加依赖
+```bash
+uv add
+```
+
+## 删除依赖
+```bash
+uv remove
+```
+
+> 💡 推荐使用uv作为默认包管理器, 支持现代Python项目管理特性
\ No newline at end of file
diff --git a/main.py b/main.py
index f255c39..d52b73d 100644
--- a/main.py
+++ b/main.py
@@ -1,5 +1,6 @@
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
# Copyright © 2024-2025 DLmaster361
+# Copyright © 2025 MoeSnowyFox
# This file is part of AUTO_MAA.
@@ -18,39 +19,38 @@
# 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
+import logging
+from pathlib import Path
-from app.core.logger import logger
+current_dir = Path(__file__).resolve().parent
+if str(current_dir) not in sys.path:
+ sys.path.insert(0, str(current_dir))
+
+from app.utils import get_logger
+
+logger = get_logger("主程序")
+
+
+class InterceptHandler(logging.Handler):
+ def emit(self, record):
+ # 获取对应 loguru 的 level
+ try:
+ level = logger.level(record.levelname).name
+ except ValueError:
+ level = record.levelno
+ # 转发日志
+ logger.opt(depth=6, exception=record.exc_info).log(level, record.getMessage())
+
+
+# 拦截标准 logging
+logging.basicConfig(handlers=[InterceptHandler()], level=0, force=True)
+for name in ("uvicorn", "uvicorn.error", "uvicorn.access", "fastapi"):
+ logging.getLogger(name).handlers = [InterceptHandler()]
+ logging.getLogger(name).propagate = False
def is_admin() -> bool:
@@ -61,28 +61,105 @@ def is_admin() -> bool:
return False
-@logger.catch
+# @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()
+
+ import asyncio
+ import uvicorn
+ from fastapi import FastAPI
+ from fastapi.staticfiles import StaticFiles
+ from contextlib import asynccontextmanager
+
+ @asynccontextmanager
+ async def lifespan(app: FastAPI):
+
+ from app.core import Config, MainTimer, TaskManager
+ from app.services import System
+
+ await Config.init_config()
+ await Config.get_stage(if_start=True)
+ await Config.clean_old_history()
+ main_timer = asyncio.create_task(MainTimer.second_task())
+ await System.set_Sleep()
+ await System.set_SelfStart()
+
+ yield
+
+ await TaskManager.stop_task("ALL")
+ main_timer.cancel()
+ try:
+ await main_timer
+ except asyncio.CancelledError:
+ logger.info("主业务定时器已关闭")
+
+ logger.info("AUTO_MAA 后端程序关闭")
+
+ from fastapi.middleware.cors import CORSMiddleware
+ from app.api import (
+ core_router,
+ info_router,
+ scripts_router,
+ plan_router,
+ queue_router,
+ dispatch_router,
+ history_router,
+ setting_router,
+ )
+
+ app = FastAPI(
+ title="AUTO_MAA",
+ description="API for managing automation scripts, plans, and tasks",
+ version="1.0.0",
+ lifespan=lifespan,
+ )
+
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=["*"], # 允许所有域名跨域访问
+ allow_credentials=True,
+ allow_methods=["*"], # 允许所有请求方法, 如 GET、POST、PUT、DELETE
+ allow_headers=["*"], # 允许所有请求头
+ )
+
+ app.include_router(core_router)
+ app.include_router(info_router)
+ app.include_router(scripts_router)
+ app.include_router(plan_router)
+ app.include_router(queue_router)
+ app.include_router(dispatch_router)
+ app.include_router(history_router)
+ app.include_router(setting_router)
+
+ app.mount(
+ "/api/res/materials",
+ StaticFiles(directory=str(Path.cwd() / "res/images/materials")),
+ name="materials",
+ )
+
+ async def run_server():
+
+ config = uvicorn.Config(
+ app, host="0.0.0.0", port=36163, log_level="info", log_config=None
+ )
+ server = uvicorn.Server(config)
+
+ from app.core import Config
+
+ Config.server = server
+ await server.serve()
+
+ asyncio.run(run_server())
+
else:
+
ctypes.windll.shell32.ShellExecuteW(
None, "runas", sys.executable, os.path.realpath(sys.argv[0]), None, 1
)
sys.exit(0)
+
+
+if __name__ == "__main__":
+
+ main()
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..9e08ffc
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,21 @@
+[project]
+name = "AUTO_MAA"
+version = "4.0.0.1"
+description = "AUTO_MAA~"
+readme = "README.md"
+requires-python = ">=3.13"
+dependencies = [
+ "loguru==0.7.3",
+ "fastapi==0.116.1",
+ "pydantic==2.11.7",
+ "uvicorn==0.35.0",
+ "plyer==2.1.0",
+ "psutil==7.0.0",
+ "jinja2==3.1.6",
+ "pywin32==310",
+ "keyboard==0.13.5",
+ "truststore==0.10.1",
+ "requests==2.32.4",
+ "pillow==11.3.0",
+ "packaging==25.0",
+]
diff --git a/requirements.txt b/requirements.txt
index 49ef1b2..e987640 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,16 +1,15 @@
loguru==0.7.3
+fastapi==0.116.1
+pydantic==2.11.7
+uvicorn==0.35.0
+websockets==15.0.1
+aiofiles==24.1.0
plyer==2.1.0
-PySide6==6.9.1
-PySide6-Fluent-Widgets[full]==1.8.3
psutil==7.0.0
+jinja2==3.1.6
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
+packaging==25.0
\ No newline at end of file
diff --git a/resources/docs/ChineseSimplified.isl b/res/docs/ChineseSimplified.isl
similarity index 100%
rename from resources/docs/ChineseSimplified.isl
rename to res/docs/ChineseSimplified.isl
diff --git a/resources/docs/MAA_config_info.txt b/res/docs/MAA_config_info.txt
similarity index 100%
rename from resources/docs/MAA_config_info.txt
rename to res/docs/MAA_config_info.txt
diff --git a/resources/html/MAA_result.html b/res/html/MAA_result.html
similarity index 100%
rename from resources/html/MAA_result.html
rename to res/html/MAA_result.html
diff --git a/resources/html/MAA_six_star.html b/res/html/MAA_six_star.html
similarity index 100%
rename from resources/html/MAA_six_star.html
rename to res/html/MAA_six_star.html
diff --git a/resources/html/MAA_statistics.html b/res/html/MAA_statistics.html
similarity index 100%
rename from resources/html/MAA_statistics.html
rename to res/html/MAA_statistics.html
diff --git a/resources/html/general_result.html b/res/html/general_result.html
similarity index 100%
rename from resources/html/general_result.html
rename to res/html/general_result.html
diff --git a/resources/html/general_statistics.html b/res/html/general_statistics.html
similarity index 100%
rename from resources/html/general_statistics.html
rename to res/html/general_statistics.html
diff --git a/resources/icons/AUTO_MAA.ico b/res/icons/AUTO_MAA.ico
similarity index 100%
rename from resources/icons/AUTO_MAA.ico
rename to res/icons/AUTO_MAA.ico
diff --git a/resources/icons/AUTO_MAA_Updater.ico b/res/icons/AUTO_MAA_Updater.ico
similarity index 100%
rename from resources/icons/AUTO_MAA_Updater.ico
rename to res/icons/AUTO_MAA_Updater.ico
diff --git a/res/icons/AUTO_MAS.ico b/res/icons/AUTO_MAS.ico
new file mode 100644
index 0000000..8339571
Binary files /dev/null and b/res/icons/AUTO_MAS.ico differ
diff --git a/resources/icons/MirrorChyan.ico b/res/icons/MirrorChyan.ico
similarity index 100%
rename from resources/icons/MirrorChyan.ico
rename to res/icons/MirrorChyan.ico
diff --git a/resources/images/AUTO_MAA.png b/res/images/AUTO_MAA.png
similarity index 100%
rename from resources/images/AUTO_MAA.png
rename to res/images/AUTO_MAA.png
diff --git a/resources/images/Home/BannerDefault.png b/res/images/Home/BannerDefault.png
similarity index 100%
rename from resources/images/Home/BannerDefault.png
rename to res/images/Home/BannerDefault.png
diff --git a/resources/images/README/payid.png b/res/images/README/payid.png
similarity index 100%
rename from resources/images/README/payid.png
rename to res/images/README/payid.png
diff --git a/res/images/materials/2001.png b/res/images/materials/2001.png
new file mode 100644
index 0000000..7211599
Binary files /dev/null and b/res/images/materials/2001.png differ
diff --git a/res/images/materials/2002.png b/res/images/materials/2002.png
new file mode 100644
index 0000000..9a682dd
Binary files /dev/null and b/res/images/materials/2002.png differ
diff --git a/res/images/materials/2003.png b/res/images/materials/2003.png
new file mode 100644
index 0000000..dce123c
Binary files /dev/null and b/res/images/materials/2003.png differ
diff --git a/res/images/materials/2004.png b/res/images/materials/2004.png
new file mode 100644
index 0000000..c9a09bc
Binary files /dev/null and b/res/images/materials/2004.png differ
diff --git a/res/images/materials/30011.png b/res/images/materials/30011.png
new file mode 100644
index 0000000..196997d
Binary files /dev/null and b/res/images/materials/30011.png differ
diff --git a/res/images/materials/30012.png b/res/images/materials/30012.png
new file mode 100644
index 0000000..667f1c7
Binary files /dev/null and b/res/images/materials/30012.png differ
diff --git a/res/images/materials/30013.png b/res/images/materials/30013.png
new file mode 100644
index 0000000..c203fc9
Binary files /dev/null and b/res/images/materials/30013.png differ
diff --git a/res/images/materials/30014.png b/res/images/materials/30014.png
new file mode 100644
index 0000000..dcbef60
Binary files /dev/null and b/res/images/materials/30014.png differ
diff --git a/res/images/materials/30021.png b/res/images/materials/30021.png
new file mode 100644
index 0000000..9dd787e
Binary files /dev/null and b/res/images/materials/30021.png differ
diff --git a/res/images/materials/30022.png b/res/images/materials/30022.png
new file mode 100644
index 0000000..be43280
Binary files /dev/null and b/res/images/materials/30022.png differ
diff --git a/res/images/materials/30023.png b/res/images/materials/30023.png
new file mode 100644
index 0000000..387992a
Binary files /dev/null and b/res/images/materials/30023.png differ
diff --git a/res/images/materials/30024.png b/res/images/materials/30024.png
new file mode 100644
index 0000000..17fae0a
Binary files /dev/null and b/res/images/materials/30024.png differ
diff --git a/res/images/materials/30031.png b/res/images/materials/30031.png
new file mode 100644
index 0000000..29eb94a
Binary files /dev/null and b/res/images/materials/30031.png differ
diff --git a/res/images/materials/30032.png b/res/images/materials/30032.png
new file mode 100644
index 0000000..79d866b
Binary files /dev/null and b/res/images/materials/30032.png differ
diff --git a/res/images/materials/30033.png b/res/images/materials/30033.png
new file mode 100644
index 0000000..3599d8d
Binary files /dev/null and b/res/images/materials/30033.png differ
diff --git a/res/images/materials/30034.png b/res/images/materials/30034.png
new file mode 100644
index 0000000..5ccafb2
Binary files /dev/null and b/res/images/materials/30034.png differ
diff --git a/res/images/materials/30041.png b/res/images/materials/30041.png
new file mode 100644
index 0000000..12639b0
Binary files /dev/null and b/res/images/materials/30041.png differ
diff --git a/res/images/materials/30042.png b/res/images/materials/30042.png
new file mode 100644
index 0000000..d28be03
Binary files /dev/null and b/res/images/materials/30042.png differ
diff --git a/res/images/materials/30043.png b/res/images/materials/30043.png
new file mode 100644
index 0000000..e4ba4cc
Binary files /dev/null and b/res/images/materials/30043.png differ
diff --git a/res/images/materials/30044.png b/res/images/materials/30044.png
new file mode 100644
index 0000000..aa06c52
Binary files /dev/null and b/res/images/materials/30044.png differ
diff --git a/res/images/materials/30051.png b/res/images/materials/30051.png
new file mode 100644
index 0000000..79b2d96
Binary files /dev/null and b/res/images/materials/30051.png differ
diff --git a/res/images/materials/30052.png b/res/images/materials/30052.png
new file mode 100644
index 0000000..3f26b14
Binary files /dev/null and b/res/images/materials/30052.png differ
diff --git a/res/images/materials/30053.png b/res/images/materials/30053.png
new file mode 100644
index 0000000..62026f8
Binary files /dev/null and b/res/images/materials/30053.png differ
diff --git a/res/images/materials/30054.png b/res/images/materials/30054.png
new file mode 100644
index 0000000..05acc92
Binary files /dev/null and b/res/images/materials/30054.png differ
diff --git a/res/images/materials/30061.png b/res/images/materials/30061.png
new file mode 100644
index 0000000..f1b58ce
Binary files /dev/null and b/res/images/materials/30061.png differ
diff --git a/res/images/materials/30062.png b/res/images/materials/30062.png
new file mode 100644
index 0000000..ddc298c
Binary files /dev/null and b/res/images/materials/30062.png differ
diff --git a/res/images/materials/30063.png b/res/images/materials/30063.png
new file mode 100644
index 0000000..16345b3
Binary files /dev/null and b/res/images/materials/30063.png differ
diff --git a/res/images/materials/30064.png b/res/images/materials/30064.png
new file mode 100644
index 0000000..f43def1
Binary files /dev/null and b/res/images/materials/30064.png differ
diff --git a/res/images/materials/30073.png b/res/images/materials/30073.png
new file mode 100644
index 0000000..64b88b6
Binary files /dev/null and b/res/images/materials/30073.png differ
diff --git a/res/images/materials/30074.png b/res/images/materials/30074.png
new file mode 100644
index 0000000..f5359c6
Binary files /dev/null and b/res/images/materials/30074.png differ
diff --git a/res/images/materials/30083.png b/res/images/materials/30083.png
new file mode 100644
index 0000000..1e2f87f
Binary files /dev/null and b/res/images/materials/30083.png differ
diff --git a/res/images/materials/30084.png b/res/images/materials/30084.png
new file mode 100644
index 0000000..746cb96
Binary files /dev/null and b/res/images/materials/30084.png differ
diff --git a/res/images/materials/30093.png b/res/images/materials/30093.png
new file mode 100644
index 0000000..a68e16b
Binary files /dev/null and b/res/images/materials/30093.png differ
diff --git a/res/images/materials/30094.png b/res/images/materials/30094.png
new file mode 100644
index 0000000..bc6c56b
Binary files /dev/null and b/res/images/materials/30094.png differ
diff --git a/res/images/materials/30103.png b/res/images/materials/30103.png
new file mode 100644
index 0000000..de0cba6
Binary files /dev/null and b/res/images/materials/30103.png differ
diff --git a/res/images/materials/30104.png b/res/images/materials/30104.png
new file mode 100644
index 0000000..5fcd144
Binary files /dev/null and b/res/images/materials/30104.png differ
diff --git a/res/images/materials/30115.png b/res/images/materials/30115.png
new file mode 100644
index 0000000..ac065be
Binary files /dev/null and b/res/images/materials/30115.png differ
diff --git a/res/images/materials/30125.png b/res/images/materials/30125.png
new file mode 100644
index 0000000..4502fec
Binary files /dev/null and b/res/images/materials/30125.png differ
diff --git a/res/images/materials/30135.png b/res/images/materials/30135.png
new file mode 100644
index 0000000..f856865
Binary files /dev/null and b/res/images/materials/30135.png differ
diff --git a/res/images/materials/30145.png b/res/images/materials/30145.png
new file mode 100644
index 0000000..84c37b0
Binary files /dev/null and b/res/images/materials/30145.png differ
diff --git a/res/images/materials/30155.png b/res/images/materials/30155.png
new file mode 100644
index 0000000..27f3890
Binary files /dev/null and b/res/images/materials/30155.png differ
diff --git a/res/images/materials/30165.png b/res/images/materials/30165.png
new file mode 100644
index 0000000..47b38ca
Binary files /dev/null and b/res/images/materials/30165.png differ
diff --git a/res/images/materials/31013.png b/res/images/materials/31013.png
new file mode 100644
index 0000000..7ea5d52
Binary files /dev/null and b/res/images/materials/31013.png differ
diff --git a/res/images/materials/31014.png b/res/images/materials/31014.png
new file mode 100644
index 0000000..9c7765b
Binary files /dev/null and b/res/images/materials/31014.png differ
diff --git a/res/images/materials/31023.png b/res/images/materials/31023.png
new file mode 100644
index 0000000..b06e1fc
Binary files /dev/null and b/res/images/materials/31023.png differ
diff --git a/res/images/materials/31024.png b/res/images/materials/31024.png
new file mode 100644
index 0000000..caff072
Binary files /dev/null and b/res/images/materials/31024.png differ
diff --git a/res/images/materials/31033.png b/res/images/materials/31033.png
new file mode 100644
index 0000000..721103f
Binary files /dev/null and b/res/images/materials/31033.png differ
diff --git a/res/images/materials/31034.png b/res/images/materials/31034.png
new file mode 100644
index 0000000..d5990c8
Binary files /dev/null and b/res/images/materials/31034.png differ
diff --git a/res/images/materials/31043.png b/res/images/materials/31043.png
new file mode 100644
index 0000000..8059ac8
Binary files /dev/null and b/res/images/materials/31043.png differ
diff --git a/res/images/materials/31044.png b/res/images/materials/31044.png
new file mode 100644
index 0000000..e5b2bb7
Binary files /dev/null and b/res/images/materials/31044.png differ
diff --git a/res/images/materials/31053.png b/res/images/materials/31053.png
new file mode 100644
index 0000000..bb589f8
Binary files /dev/null and b/res/images/materials/31053.png differ
diff --git a/res/images/materials/31054.png b/res/images/materials/31054.png
new file mode 100644
index 0000000..29a01dc
Binary files /dev/null and b/res/images/materials/31054.png differ
diff --git a/res/images/materials/31063.png b/res/images/materials/31063.png
new file mode 100644
index 0000000..a707b2a
Binary files /dev/null and b/res/images/materials/31063.png differ
diff --git a/res/images/materials/31064.png b/res/images/materials/31064.png
new file mode 100644
index 0000000..33fbdc6
Binary files /dev/null and b/res/images/materials/31064.png differ
diff --git a/res/images/materials/31073.png b/res/images/materials/31073.png
new file mode 100644
index 0000000..f7063bf
Binary files /dev/null and b/res/images/materials/31073.png differ
diff --git a/res/images/materials/31074.png b/res/images/materials/31074.png
new file mode 100644
index 0000000..62af816
Binary files /dev/null and b/res/images/materials/31074.png differ
diff --git a/res/images/materials/31083.png b/res/images/materials/31083.png
new file mode 100644
index 0000000..2e1b55f
Binary files /dev/null and b/res/images/materials/31083.png differ
diff --git a/res/images/materials/31084.png b/res/images/materials/31084.png
new file mode 100644
index 0000000..306deab
Binary files /dev/null and b/res/images/materials/31084.png differ
diff --git a/res/images/materials/31093.png b/res/images/materials/31093.png
new file mode 100644
index 0000000..cd8fd82
Binary files /dev/null and b/res/images/materials/31093.png differ
diff --git a/res/images/materials/31094.png b/res/images/materials/31094.png
new file mode 100644
index 0000000..a20b0c7
Binary files /dev/null and b/res/images/materials/31094.png differ
diff --git a/res/images/materials/3112.png b/res/images/materials/3112.png
new file mode 100644
index 0000000..a240b7b
Binary files /dev/null and b/res/images/materials/3112.png differ
diff --git a/res/images/materials/3113.png b/res/images/materials/3113.png
new file mode 100644
index 0000000..4202368
Binary files /dev/null and b/res/images/materials/3113.png differ
diff --git a/res/images/materials/3114.png b/res/images/materials/3114.png
new file mode 100644
index 0000000..3e71f3d
Binary files /dev/null and b/res/images/materials/3114.png differ
diff --git a/res/images/materials/3211.png b/res/images/materials/3211.png
new file mode 100644
index 0000000..c3b3150
Binary files /dev/null and b/res/images/materials/3211.png differ
diff --git a/res/images/materials/3212.png b/res/images/materials/3212.png
new file mode 100644
index 0000000..7618c2c
Binary files /dev/null and b/res/images/materials/3212.png differ
diff --git a/res/images/materials/3213.png b/res/images/materials/3213.png
new file mode 100644
index 0000000..cbda9ce
Binary files /dev/null and b/res/images/materials/3213.png differ
diff --git a/res/images/materials/3221.png b/res/images/materials/3221.png
new file mode 100644
index 0000000..0f19ea0
Binary files /dev/null and b/res/images/materials/3221.png differ
diff --git a/res/images/materials/3222.png b/res/images/materials/3222.png
new file mode 100644
index 0000000..1acee90
Binary files /dev/null and b/res/images/materials/3222.png differ
diff --git a/res/images/materials/3223.png b/res/images/materials/3223.png
new file mode 100644
index 0000000..9837951
Binary files /dev/null and b/res/images/materials/3223.png differ
diff --git a/res/images/materials/3231.png b/res/images/materials/3231.png
new file mode 100644
index 0000000..8ae7344
Binary files /dev/null and b/res/images/materials/3231.png differ
diff --git a/res/images/materials/3232.png b/res/images/materials/3232.png
new file mode 100644
index 0000000..e75f531
Binary files /dev/null and b/res/images/materials/3232.png differ
diff --git a/res/images/materials/3233.png b/res/images/materials/3233.png
new file mode 100644
index 0000000..b0c9f21
Binary files /dev/null and b/res/images/materials/3233.png differ
diff --git a/res/images/materials/3241.png b/res/images/materials/3241.png
new file mode 100644
index 0000000..32fc401
Binary files /dev/null and b/res/images/materials/3241.png differ
diff --git a/res/images/materials/3242.png b/res/images/materials/3242.png
new file mode 100644
index 0000000..51d60ba
Binary files /dev/null and b/res/images/materials/3242.png differ
diff --git a/res/images/materials/3243.png b/res/images/materials/3243.png
new file mode 100644
index 0000000..09425bb
Binary files /dev/null and b/res/images/materials/3243.png differ
diff --git a/res/images/materials/3251.png b/res/images/materials/3251.png
new file mode 100644
index 0000000..96abeb4
Binary files /dev/null and b/res/images/materials/3251.png differ
diff --git a/res/images/materials/3252.png b/res/images/materials/3252.png
new file mode 100644
index 0000000..cf9357b
Binary files /dev/null and b/res/images/materials/3252.png differ
diff --git a/res/images/materials/3253.png b/res/images/materials/3253.png
new file mode 100644
index 0000000..d9a42f8
Binary files /dev/null and b/res/images/materials/3253.png differ
diff --git a/res/images/materials/3261.png b/res/images/materials/3261.png
new file mode 100644
index 0000000..409c1a6
Binary files /dev/null and b/res/images/materials/3261.png differ
diff --git a/res/images/materials/3262.png b/res/images/materials/3262.png
new file mode 100644
index 0000000..a298d23
Binary files /dev/null and b/res/images/materials/3262.png differ
diff --git a/res/images/materials/3263.png b/res/images/materials/3263.png
new file mode 100644
index 0000000..cc9a8d9
Binary files /dev/null and b/res/images/materials/3263.png differ
diff --git a/res/images/materials/3271.png b/res/images/materials/3271.png
new file mode 100644
index 0000000..8de0cc7
Binary files /dev/null and b/res/images/materials/3271.png differ
diff --git a/res/images/materials/3272.png b/res/images/materials/3272.png
new file mode 100644
index 0000000..5556cd2
Binary files /dev/null and b/res/images/materials/3272.png differ
diff --git a/res/images/materials/3273.png b/res/images/materials/3273.png
new file mode 100644
index 0000000..7e700d8
Binary files /dev/null and b/res/images/materials/3273.png differ
diff --git a/res/images/materials/3281.png b/res/images/materials/3281.png
new file mode 100644
index 0000000..5207057
Binary files /dev/null and b/res/images/materials/3281.png differ
diff --git a/res/images/materials/3282.png b/res/images/materials/3282.png
new file mode 100644
index 0000000..0bd6cbe
Binary files /dev/null and b/res/images/materials/3282.png differ
diff --git a/res/images/materials/3283.png b/res/images/materials/3283.png
new file mode 100644
index 0000000..6a1e43c
Binary files /dev/null and b/res/images/materials/3283.png differ
diff --git a/res/images/materials/3301.png b/res/images/materials/3301.png
new file mode 100644
index 0000000..6977bc4
Binary files /dev/null and b/res/images/materials/3301.png differ
diff --git a/res/images/materials/3302.png b/res/images/materials/3302.png
new file mode 100644
index 0000000..48ca15b
Binary files /dev/null and b/res/images/materials/3302.png differ
diff --git a/res/images/materials/3303.png b/res/images/materials/3303.png
new file mode 100644
index 0000000..c132cbf
Binary files /dev/null and b/res/images/materials/3303.png differ
diff --git a/res/images/materials/4001.png b/res/images/materials/4001.png
new file mode 100644
index 0000000..2f7691a
Binary files /dev/null and b/res/images/materials/4001.png differ
diff --git a/res/images/materials/4006.png b/res/images/materials/4006.png
new file mode 100644
index 0000000..cb0238a
Binary files /dev/null and b/res/images/materials/4006.png differ
diff --git a/res/images/materials/PR-A.png b/res/images/materials/PR-A.png
new file mode 100644
index 0000000..1367a59
Binary files /dev/null and b/res/images/materials/PR-A.png differ
diff --git a/res/images/materials/PR-B.png b/res/images/materials/PR-B.png
new file mode 100644
index 0000000..46b9995
Binary files /dev/null and b/res/images/materials/PR-B.png differ
diff --git a/res/images/materials/PR-C.png b/res/images/materials/PR-C.png
new file mode 100644
index 0000000..9b4bb3d
Binary files /dev/null and b/res/images/materials/PR-C.png differ
diff --git a/res/images/materials/PR-D.png b/res/images/materials/PR-D.png
new file mode 100644
index 0000000..a175026
Binary files /dev/null and b/res/images/materials/PR-D.png differ
diff --git a/resources/images/notification/six_star.png b/res/images/notification/six_star.png
similarity index 100%
rename from resources/images/notification/six_star.png
rename to res/images/notification/six_star.png
diff --git a/resources/images/notification/test_notify.png b/res/images/notification/test_notify.png
similarity index 100%
rename from resources/images/notification/test_notify.png
rename to res/images/notification/test_notify.png
diff --git a/resources/sounds/both/删除用户.wav b/res/sounds/both/删除用户.wav
similarity index 100%
rename from resources/sounds/both/删除用户.wav
rename to res/sounds/both/删除用户.wav
diff --git a/resources/sounds/both/删除脚本实例.wav b/res/sounds/both/删除脚本实例.wav
similarity index 100%
rename from resources/sounds/both/删除脚本实例.wav
rename to res/sounds/both/删除脚本实例.wav
diff --git a/resources/sounds/both/删除计划表.wav b/res/sounds/both/删除计划表.wav
similarity index 100%
rename from resources/sounds/both/删除计划表.wav
rename to res/sounds/both/删除计划表.wav
diff --git a/resources/sounds/both/删除调度队列.wav b/res/sounds/both/删除调度队列.wav
similarity index 100%
rename from resources/sounds/both/删除调度队列.wav
rename to res/sounds/both/删除调度队列.wav
diff --git a/resources/sounds/both/欢迎回来.wav b/res/sounds/both/欢迎回来.wav
similarity index 100%
rename from resources/sounds/both/欢迎回来.wav
rename to res/sounds/both/欢迎回来.wav
diff --git a/resources/sounds/both/添加用户.wav b/res/sounds/both/添加用户.wav
similarity index 100%
rename from resources/sounds/both/添加用户.wav
rename to res/sounds/both/添加用户.wav
diff --git a/resources/sounds/both/添加脚本实例.wav b/res/sounds/both/添加脚本实例.wav
similarity index 100%
rename from resources/sounds/both/添加脚本实例.wav
rename to res/sounds/both/添加脚本实例.wav
diff --git a/resources/sounds/both/添加计划表.wav b/res/sounds/both/添加计划表.wav
similarity index 100%
rename from resources/sounds/both/添加计划表.wav
rename to res/sounds/both/添加计划表.wav
diff --git a/resources/sounds/both/添加调度队列.wav b/res/sounds/both/添加调度队列.wav
similarity index 100%
rename from resources/sounds/both/添加调度队列.wav
rename to res/sounds/both/添加调度队列.wav
diff --git a/resources/sounds/noisy/ADB失败.wav b/res/sounds/noisy/ADB失败.wav
similarity index 100%
rename from resources/sounds/noisy/ADB失败.wav
rename to res/sounds/noisy/ADB失败.wav
diff --git a/resources/sounds/noisy/ADB成功.wav b/res/sounds/noisy/ADB成功.wav
similarity index 100%
rename from resources/sounds/noisy/ADB成功.wav
rename to res/sounds/noisy/ADB成功.wav
diff --git a/resources/sounds/noisy/MAA在完成任务前中止.wav b/res/sounds/noisy/MAA在完成任务前中止.wav
similarity index 100%
rename from resources/sounds/noisy/MAA在完成任务前中止.wav
rename to res/sounds/noisy/MAA在完成任务前中止.wav
diff --git a/resources/sounds/noisy/MAA在完成任务前退出.wav b/res/sounds/noisy/MAA在完成任务前退出.wav
similarity index 100%
rename from resources/sounds/noisy/MAA在完成任务前退出.wav
rename to res/sounds/noisy/MAA在完成任务前退出.wav
diff --git a/resources/sounds/noisy/MAA更新.wav b/res/sounds/noisy/MAA更新.wav
similarity index 100%
rename from resources/sounds/noisy/MAA更新.wav
rename to res/sounds/noisy/MAA更新.wav
diff --git a/resources/sounds/noisy/MAA未检测到任何模拟器.wav b/res/sounds/noisy/MAA未检测到任何模拟器.wav
similarity index 100%
rename from resources/sounds/noisy/MAA未检测到任何模拟器.wav
rename to res/sounds/noisy/MAA未检测到任何模拟器.wav
diff --git a/resources/sounds/noisy/MAA未能正确登录PRTS.wav b/res/sounds/noisy/MAA未能正确登录PRTS.wav
similarity index 100%
rename from resources/sounds/noisy/MAA未能正确登录PRTS.wav
rename to res/sounds/noisy/MAA未能正确登录PRTS.wav
diff --git a/resources/sounds/noisy/MAA的ADB连接异常.wav b/res/sounds/noisy/MAA的ADB连接异常.wav
similarity index 100%
rename from resources/sounds/noisy/MAA的ADB连接异常.wav
rename to res/sounds/noisy/MAA的ADB连接异常.wav
diff --git a/resources/sounds/noisy/MAA进程超时.wav b/res/sounds/noisy/MAA进程超时.wav
similarity index 100%
rename from resources/sounds/noisy/MAA进程超时.wav
rename to res/sounds/noisy/MAA进程超时.wav
diff --git a/resources/sounds/noisy/MAA部分任务执行失败.wav b/res/sounds/noisy/MAA部分任务执行失败.wav
similarity index 100%
rename from resources/sounds/noisy/MAA部分任务执行失败.wav
rename to res/sounds/noisy/MAA部分任务执行失败.wav
diff --git a/resources/sounds/noisy/任务开始.wav b/res/sounds/noisy/任务开始.wav
similarity index 100%
rename from resources/sounds/noisy/任务开始.wav
rename to res/sounds/noisy/任务开始.wav
diff --git a/resources/sounds/noisy/任务结束.wav b/res/sounds/noisy/任务结束.wav
similarity index 100%
rename from resources/sounds/noisy/任务结束.wav
rename to res/sounds/noisy/任务结束.wav
diff --git a/resources/sounds/noisy/公告展示.wav b/res/sounds/noisy/公告展示.wav
similarity index 100%
rename from resources/sounds/noisy/公告展示.wav
rename to res/sounds/noisy/公告展示.wav
diff --git a/resources/sounds/noisy/公告通知.wav b/res/sounds/noisy/公告通知.wav
similarity index 100%
rename from resources/sounds/noisy/公告通知.wav
rename to res/sounds/noisy/公告通知.wav
diff --git a/resources/sounds/noisy/六星喜报.wav b/res/sounds/noisy/六星喜报.wav
similarity index 100%
rename from resources/sounds/noisy/六星喜报.wav
rename to res/sounds/noisy/六星喜报.wav
diff --git a/resources/sounds/noisy/历史记录查询.wav b/res/sounds/noisy/历史记录查询.wav
similarity index 100%
rename from resources/sounds/noisy/历史记录查询.wav
rename to res/sounds/noisy/历史记录查询.wav
diff --git a/resources/sounds/noisy/发生异常.wav b/res/sounds/noisy/发生异常.wav
similarity index 100%
rename from resources/sounds/noisy/发生异常.wav
rename to res/sounds/noisy/发生异常.wav
diff --git a/resources/sounds/noisy/发生错误.wav b/res/sounds/noisy/发生错误.wav
similarity index 100%
rename from resources/sounds/noisy/发生错误.wav
rename to res/sounds/noisy/发生错误.wav
diff --git a/resources/sounds/noisy/子任务失败.wav b/res/sounds/noisy/子任务失败.wav
similarity index 100%
rename from resources/sounds/noisy/子任务失败.wav
rename to res/sounds/noisy/子任务失败.wav
diff --git a/resources/sounds/noisy/排查录入.wav b/res/sounds/noisy/排查录入.wav
similarity index 100%
rename from resources/sounds/noisy/排查录入.wav
rename to res/sounds/noisy/排查录入.wav
diff --git a/resources/sounds/noisy/排查重试.wav b/res/sounds/noisy/排查重试.wav
similarity index 100%
rename from resources/sounds/noisy/排查重试.wav
rename to res/sounds/noisy/排查重试.wav
diff --git a/resources/sounds/noisy/无新版本.wav b/res/sounds/noisy/无新版本.wav
similarity index 100%
rename from resources/sounds/noisy/无新版本.wav
rename to res/sounds/noisy/无新版本.wav
diff --git a/resources/sounds/noisy/有新版本.wav b/res/sounds/noisy/有新版本.wav
similarity index 100%
rename from resources/sounds/noisy/有新版本.wav
rename to res/sounds/noisy/有新版本.wav
diff --git a/resources/sounds/noisy/森空岛签到失败.wav b/res/sounds/noisy/森空岛签到失败.wav
similarity index 100%
rename from resources/sounds/noisy/森空岛签到失败.wav
rename to res/sounds/noisy/森空岛签到失败.wav
diff --git a/resources/sounds/noisy/森空岛签到成功.wav b/res/sounds/noisy/森空岛签到成功.wav
similarity index 100%
rename from resources/sounds/noisy/森空岛签到成功.wav
rename to res/sounds/noisy/森空岛签到成功.wav
diff --git a/resources/sounds/simple/任务开始.wav b/res/sounds/simple/任务开始.wav
similarity index 100%
rename from resources/sounds/simple/任务开始.wav
rename to res/sounds/simple/任务开始.wav
diff --git a/resources/sounds/simple/任务结束.wav b/res/sounds/simple/任务结束.wav
similarity index 100%
rename from resources/sounds/simple/任务结束.wav
rename to res/sounds/simple/任务结束.wav
diff --git a/resources/sounds/simple/公告展示.wav b/res/sounds/simple/公告展示.wav
similarity index 100%
rename from resources/sounds/simple/公告展示.wav
rename to res/sounds/simple/公告展示.wav
diff --git a/resources/sounds/simple/公告通知.wav b/res/sounds/simple/公告通知.wav
similarity index 100%
rename from resources/sounds/simple/公告通知.wav
rename to res/sounds/simple/公告通知.wav
diff --git a/resources/sounds/simple/历史记录查询.wav b/res/sounds/simple/历史记录查询.wav
similarity index 100%
rename from resources/sounds/simple/历史记录查询.wav
rename to res/sounds/simple/历史记录查询.wav
diff --git a/resources/sounds/simple/发生异常.wav b/res/sounds/simple/发生异常.wav
similarity index 100%
rename from resources/sounds/simple/发生异常.wav
rename to res/sounds/simple/发生异常.wav
diff --git a/resources/sounds/simple/发生错误.wav b/res/sounds/simple/发生错误.wav
similarity index 100%
rename from resources/sounds/simple/发生错误.wav
rename to res/sounds/simple/发生错误.wav
diff --git a/resources/sounds/simple/无新版本.wav b/res/sounds/simple/无新版本.wav
similarity index 100%
rename from resources/sounds/simple/无新版本.wav
rename to res/sounds/simple/无新版本.wav
diff --git a/resources/sounds/simple/有新版本.wav b/res/sounds/simple/有新版本.wav
similarity index 100%
rename from resources/sounds/simple/有新版本.wav
rename to res/sounds/simple/有新版本.wav
diff --git a/resources/version.json b/res/version.json
similarity index 92%
rename from resources/version.json
rename to res/version.json
index 3d6eb9a..189c808 100644
--- a/resources/version.json
+++ b/res/version.json
@@ -18,7 +18,7 @@
],
"程序优化": [
"优化调度队列配置逻辑",
- "优化静默进程标记逻辑,避免未能及时移除导致相关功能持续开启",
+ "优化静默进程标记逻辑, 避免未能及时移除导致相关功能持续开启",
"MAA 代理时更新改为强制开启",
"移除 MAA 详细配置模式中的剿灭项"
]
@@ -54,7 +54,7 @@
"修复模拟器界面被异常关闭且无法重新打开的问题"
],
"程序优化": [
- "重构日志记录,载入更多日志记录项",
+ "重构日志记录, 载入更多日志记录项",
"优化日志监看启停逻辑",
"SpinBox和TimeEdit组件忽视滚轮事件"
]
diff --git a/updater.py b/updater.py
new file mode 100644
index 0000000..1904ea9
--- /dev/null
+++ b/updater.py
@@ -0,0 +1,379 @@
+# 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
+
+
+import re
+import sys
+import time
+import json
+import psutil
+import base64
+import zipfile
+import requests
+import argparse
+import truststore
+import subprocess
+import win32crypt
+
+from packaging import version
+from pathlib import Path
+from typing import List, Dict
+
+current_dir = Path(__file__).resolve().parent
+if str(current_dir) not in sys.path:
+ sys.path.insert(0, str(current_dir))
+
+
+MIRROR_ERROR_INFO = {
+ 1001: "获取版本信息的URL参数不正确",
+ 7001: "填入的 CDK 已过期",
+ 7002: "填入的 CDK 错误",
+ 7003: "填入的 CDK 今日下载次数已达上限",
+ 7004: "填入的 CDK 类型和待下载的资源不匹配",
+ 7005: "填入的 CDK 已被封禁",
+ 8001: "对应架构和系统下的资源不存在",
+ 8002: "错误的系统参数",
+ 8003: "错误的架构参数",
+ 8004: "错误的更新通道参数",
+ 1: "未知错误类型",
+}
+
+
+def dpapi_decrypt(note: str, entropy: None | 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 kill_process(path: Path) -> None:
+ """
+ 根据路径中止进程
+
+ :param path: 进程路径
+ """
+
+ print(f"开始中止进程: {path}")
+
+ for pid in search_pids(path):
+ killprocess = subprocess.Popen(
+ f"taskkill /F /T /PID {pid}",
+ shell=True,
+ creationflags=subprocess.CREATE_NO_WINDOW,
+ )
+ killprocess.wait()
+
+ print(f"进程已中止: {path}")
+
+
+def search_pids(path: Path) -> list:
+ """
+ 根据路径查找进程PID
+
+ :param path: 进程路径
+ :return: 匹配的进程PID列表
+ """
+
+ print(f"开始查找进程 PID: {path}")
+
+ 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
+
+
+truststore.inject_into_ssl()
+parser = argparse.ArgumentParser(
+ prog="AUTO-MAS更新器", description="为AUTO-MAS前端提供更新服务"
+)
+parser.add_argument(
+ "--version", "-v", type=str, required=False, default=None, help="前端程序版本号"
+)
+args = parser.parse_args()
+
+if (Path.cwd() / "config/Config.json").exists():
+ config = json.loads(
+ (Path.cwd() / "config/Config.json").read_text(encoding="utf-8")
+ ).get(
+ "Update",
+ {
+ "MirrorChyanCDK": "",
+ "ProxyAddress": "",
+ "Source": "GitHub",
+ "UpdateType": "stable",
+ },
+ )
+else:
+ config = {
+ "MirrorChyanCDK": "",
+ "ProxyAddress": "",
+ "Source": "GitHub",
+ "UpdateType": "stable",
+ }
+
+if (
+ config.get("Source", "GitHub") == "MirrorChyan"
+ and dpapi_decrypt(config.get("MirrorChyanCDK", "")) == ""
+):
+ print("使用 MirrorChyan源但未填写 MirrorChyanCDK, 转用 GitHub 源")
+ config["Source"] = "GitHub"
+ config["MirrorChyanCDK"] = ""
+
+print(f"当前配置: {config}")
+
+
+download_source = config.get("Source", "GitHub")
+proxies = {
+ "http": config.get("ProxyAddress", ""),
+ "https": config.get("ProxyAddress", ""),
+}
+
+if args.version:
+ current_version = args.version
+else:
+ current_version = "v0.0.0"
+
+print(f"当前版本: {current_version}")
+
+
+response = requests.get(
+ f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaGui¤t_version={current_version}&cdk={dpapi_decrypt(config.get('MirrorChyanCDK', ''))}&channel={config.get('UpdateType', 'stable')}",
+ timeout=10,
+ proxies=proxies,
+)
+if response.status_code == 200:
+ version_info = response.json()
+else:
+ try:
+ result = response.json()
+
+ if result["code"] != 0:
+ if result["code"] in MIRROR_ERROR_INFO:
+ print(f"获取版本信息时出错: {MIRROR_ERROR_INFO[result['code']]}")
+ else:
+ print(
+ "获取版本信息时出错: 意料之外的错误, 请及时联系项目组以获取来自 Mirror 酱的技术支持"
+ )
+ print(f" {result['msg']}")
+ sys.exit(1)
+ except Exception:
+ print(f"获取版本信息时出错: {response.text}")
+
+ sys.exit(1)
+
+
+remote_version = version_info["data"]["version_name"]
+
+if version.parse(remote_version) > version.parse(current_version):
+
+ # 版本更新信息
+ print(f"发现新版本: {remote_version}, 当前版本: {current_version}")
+
+ version_info_json: Dict[str, Dict[str, List[str]]] = json.loads(
+ re.sub(
+ r"^$",
+ r"\1",
+ version_info["data"]["release_note"].splitlines()[0],
+ )
+ )
+
+ update_version_info = {}
+ for v_i in [
+ info
+ for ver, info in version_info_json.items()
+ if version.parse(ver) > version.parse(current_version)
+ ]:
+
+ for key, value in v_i.items():
+ if key not in update_version_info:
+ update_version_info[key] = []
+ update_version_info[key] += value
+
+ for key, value in update_version_info.items():
+ print(f"{key}: ")
+ for v in value:
+ print(f" - {v}")
+
+ if download_source == "GitHub":
+
+ download_url = f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{remote_version}/AUTO_MAA_{remote_version}.zip"
+
+ elif download_source == "MirrorChyan":
+ if "url" in version_info["data"]:
+ with requests.get(
+ version_info["data"]["url"],
+ allow_redirects=True,
+ timeout=10,
+ stream=True,
+ proxies=proxies,
+ ) as response:
+ if response.status_code == 200:
+ download_url = response.url
+ else:
+ print(f"MirrorChyan 未返回下载链接, 使用自建下载站")
+ download_url = f"https://download.auto-mas.top/d/AUTO_MAA/AUTO_MAA_{remote_version}.zip"
+
+ elif download_source == "AutoSite":
+ download_url = (
+ f"https://download.auto-mas.top/d/AUTO_MAA/AUTO_MAA_{remote_version}.zip"
+ )
+
+ else:
+ print(f"未知的下载源: {download_source}, 请检查配置文件")
+ sys.exit(1)
+
+ print(f"开始下载: {download_url}")
+
+ # 清理可能存在的临时文件
+ if (Path.cwd() / "download.temp").exists():
+ (Path.cwd() / "download.temp").unlink()
+
+ check_times = 3
+ while check_times != 0:
+
+ try:
+
+ start_time = time.time()
+
+ response = requests.get(
+ download_url, timeout=10, stream=True, proxies=proxies
+ )
+
+ if response.status_code not in [200, 206]:
+
+ if check_times != -1:
+ check_times -= 1
+
+ print(
+ f"连接失败: {download_url}, 状态码: {response.status_code}, 剩余重试次数: {check_times}",
+ )
+
+ time.sleep(1)
+ continue
+
+ print(f"连接成功: {download_url}, 状态码: {response.status_code}")
+
+ file_size = int(response.headers.get("content-length", 0))
+ downloaded_size = 0
+ last_download_size = 0
+ last_time = time.time()
+ with (Path.cwd() / "download.temp").open(mode="wb") as f:
+
+ for chunk in response.iter_content(chunk_size=8192):
+
+ f.write(chunk)
+ downloaded_size += len(chunk)
+
+ # 更新指定线程的下载进度, 每秒更新一次
+ if time.time() - last_time >= 1.0:
+ speed = (
+ (downloaded_size - last_download_size)
+ / (time.time() - last_time)
+ / 1024
+ )
+ last_download_size = downloaded_size
+ last_time = time.time()
+
+ if speed >= 1024:
+ print(
+ f"正在下载: AUTO-MAS 已下载: {downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB ({downloaded_size / file_size * 100:.2f}%) 下载速度: {speed / 1024:.2f} MB/s",
+ )
+ else:
+ print(
+ f"正在下载: AUTO-MAS 已下载: {downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB ({downloaded_size / file_size * 100:.2f}%) 下载速度: {speed:.2f} KB/s",
+ )
+
+ print(
+ f"下载完成: {download_url}, 实际下载大小: {downloaded_size} 字节, 耗时: {time.time() - start_time:.2f} 秒",
+ )
+
+ break
+
+ except Exception as e:
+
+ if check_times != -1:
+ check_times -= 1
+
+ print(
+ f"下载出错: {download_url}, 错误信息: {e}, 剩余重试次数: {check_times}",
+ )
+ time.sleep(1)
+
+ else:
+
+ if (Path.cwd() / "download.temp").exists():
+ (Path.cwd() / "download.temp").unlink()
+ print(f"下载失败: {download_url}")
+ sys.exit(1)
+
+ print(f"开始解压: {Path.cwd() / 'download.temp'} 到 {Path.cwd()}")
+
+ while True:
+
+ try:
+ with zipfile.ZipFile(Path.cwd() / "download.temp", "r") as zip_ref:
+ zip_ref.extractall(Path.cwd())
+ print(f"解压完成: {Path.cwd() / 'download.temp'} 到 {Path.cwd()}")
+ break
+ except PermissionError:
+ print(f"解压出错: AUTO_MAA正在运行, 正在尝试将其关闭")
+ kill_process(Path.cwd() / "AUTO_MAA.exe")
+ time.sleep(1)
+
+ print("正在删除临时文件")
+ if (Path.cwd() / "changes.json").exists():
+ (Path.cwd() / "changes.json").unlink()
+ if (Path.cwd() / "download.temp").exists():
+ (Path.cwd() / "download.temp").unlink()
+
+ print("正在启动AUTO_MAA")
+ subprocess.Popen(
+ [Path.cwd() / "AUTO_MAA.exe"],
+ creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
+ | subprocess.DETACHED_PROCESS
+ | subprocess.CREATE_NO_WINDOW,
+ )
+
+ print("更新完成")
+ sys.exit(0)
+
+else:
+
+ print(f"当前版本为最新版本: {current_version}")
+ sys.exit(0)