Compare commits
184 Commits
v4.3.6-bet
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6b37ba0ce3 | ||
| 23b3691a13 | |||
|
|
8948d0fb18 | ||
|
|
908da0bc47 | ||
|
|
aae17208b0 | ||
|
|
cbd8918c61 | ||
|
|
f2b4f9e8fc | ||
|
|
a2a11647bb | ||
|
|
1d9c275b61 | ||
|
|
52468928c6 | ||
|
|
3d9471779a | ||
|
|
155c4b00d5 | ||
|
|
f4b45e9eae | ||
|
|
d836b58b2d | ||
|
|
4fc747f1c6 | ||
|
|
b27f2c43ae | ||
|
|
c5f947e14a | ||
|
|
67c41ab3ee | ||
|
|
9f330e2245 | ||
|
|
d785262312 | ||
|
|
27faaabcf2 | ||
| 226d68cb1c | |||
|
|
e776cc2319 | ||
|
|
3cdb1e511d | ||
| a4f867665f | |||
|
|
6b0583b139 | ||
| 2aad0c65c2 | |||
| 6b646378b6 | |||
| 747ad6387b | |||
| 228e66315c | |||
|
|
97c283797a | ||
|
|
eb1fade6f5 | ||
|
|
8d6071f794 | ||
|
|
8a109e34f8 | ||
|
|
fd72d72692 | ||
|
|
63ffacff96 | ||
|
|
1b4bb6fccc | ||
|
|
9b492b5e0d | ||
|
|
8427bd9f6b | ||
|
|
2c915161d5 | ||
|
|
75b06ca770 | ||
|
|
c3468a3387 | ||
|
|
a2f4adb647 | ||
|
|
403f69df8b | ||
|
|
12cf10f97a | ||
|
|
6084befe2c | ||
|
|
1aa99ea613 | ||
|
|
d539c0f808 | ||
|
|
bc509806fb | ||
|
|
c52820550f | ||
|
|
98b30f90a1 | ||
|
|
4efbafc174 | ||
|
|
6d3fda50d3 | ||
|
|
70b936012f | ||
|
|
54917fbe6d | ||
|
|
abeb9f054d | ||
|
|
c6d6c5fb2a | ||
|
|
5b0d7f0012 | ||
|
|
d9043aab0a | ||
|
|
b9281b68ab | ||
|
|
5c6a20be4e | ||
|
|
1c0a65957d | ||
|
|
7c315624b1 | ||
|
|
0572caa528 | ||
|
|
4233040585 | ||
|
|
c27dc8e380 | ||
|
|
e746756e56 | ||
|
|
1829d1cd0b | ||
|
|
fb979e5639 | ||
|
|
e7d0a85ad5 | ||
|
|
a384711327 | ||
|
|
3fd4778a48 | ||
|
|
4841dc09b3 | ||
|
|
b3aa4fc776 | ||
|
|
a9b3b8b6f4 | ||
|
|
56ef196695 | ||
| 242238d341 | |||
| f66f6d38fe | |||
| d58077f58b | |||
| 4d4d6dbedf | |||
| f60b276916 | |||
| 87857fd499 | |||
| 3c371cd079 | |||
|
|
428b849bcc | ||
|
|
85f3b4f607 | ||
|
|
916396f855 | ||
|
|
211c8d2b04 | ||
|
|
92e274d3fd | ||
|
|
d511ea48d5 | ||
|
|
1aa4da1adf | ||
|
|
0e8b6b0b6b | ||
|
|
1a2c1b976f | ||
|
|
1cc242fa51 | ||
|
|
18dfdba15d | ||
|
|
b04ac4eec6 | ||
|
|
c009f0c891 | ||
|
|
d2dc0bd295 | ||
|
|
ddbb5b7f19 | ||
|
|
954c25090b | ||
|
|
0b6cc59de1 | ||
|
|
2271b5741d | ||
|
|
8a438b041f | ||
|
|
dd92fcc4d8 | ||
|
|
8f66ca0e16 | ||
|
|
895ba1d24a | ||
| e49b807bef | |||
|
|
73c15b5e93 | ||
|
|
e505ea8c51 | ||
|
|
21e7df7c3e | ||
|
|
2d72ca66a4 | ||
|
|
4725a30165 | ||
|
|
f3c977f1b3 | ||
|
|
9a0e7265c6 | ||
|
|
3f8e2fbe6b | ||
|
|
590b13e916 | ||
|
|
0f6aee56e5 | ||
|
|
daf18e7295 | ||
|
|
9bcc87f663 | ||
|
|
e7205ce0aa | ||
|
|
e3c4b2edc8 | ||
|
|
222a3b35a2 | ||
|
|
cd5dfd56b2 | ||
|
|
7d5c6b8222 | ||
|
|
4dbf4736e4 | ||
|
|
d50504181e | ||
|
|
c7e94dfcd1 | ||
|
|
a752b67ca1 | ||
|
|
078736337d | ||
|
|
de1058a28c | ||
|
|
740797a689 | ||
|
|
26328920a2 | ||
|
|
9c447bbdf9 | ||
|
|
fac85a889f | ||
|
|
f5d898c89e | ||
| 974a4b634a | |||
| 3127c83603 | |||
|
|
8d69e43f72 | ||
|
|
86df9e7a50 | ||
| 59ff9bf818 | |||
|
|
1641e32e3d | ||
|
|
a482087abd | ||
| bc5b15cec2 | |||
| 3787c25a77 | |||
|
|
0b06b499e4 | ||
|
|
04079dd57b | ||
|
|
34ac0c5ab3 | ||
| 0d904b229e | |||
| c0f887ff9d | |||
|
|
cf95075d01 | ||
|
|
d78a764d87 | ||
|
|
a368f4b722 | ||
|
|
803fe4568f | ||
|
|
1162d5dcc1 | ||
|
|
aa83058e39 | ||
|
|
061f205224 | ||
|
|
5d966f98df | ||
|
|
0037914db8 | ||
|
|
13d0115475 | ||
|
|
5bdb5ad2bf | ||
| a5d733c8df | |||
|
|
0b038e2997 | ||
|
|
5e46040db6 | ||
|
|
f2b04dd0f6 | ||
|
|
2177c1b40e | ||
|
|
d1f4cffe8f | ||
|
|
74ce441b90 | ||
|
|
5893aa2426 | ||
|
|
cb7e7bf9d4 | ||
|
|
fbfdc6aa12 | ||
|
|
e7b6743e10 | ||
|
|
ff4283e917 | ||
|
|
890886d62d | ||
|
|
fd75dda2b1 | ||
|
|
f22c1aeae3 | ||
|
|
d68d49a469 | ||
|
|
1900d4eaf5 | ||
|
|
02833209d5 | ||
| 2058c0218c | |||
|
|
8896e723eb | ||
|
|
edcc614833 | ||
|
|
23fe1ff0be | ||
|
|
19d1dc9f28 | ||
|
|
24b93cfcad | ||
|
|
d3298fac8a |
210
.github/workflows/build-app.yml
vendored
@@ -28,9 +28,11 @@ permissions:
|
||||
actions: write
|
||||
|
||||
jobs:
|
||||
|
||||
pre_check:
|
||||
name: Pre Checks
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Repo Check
|
||||
id: repo_check
|
||||
@@ -40,68 +42,205 @@ jobs:
|
||||
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 Python 3.12
|
||||
|
||||
- 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"
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install flake8 pytest
|
||||
pip install -r requirements.txt
|
||||
- name: Lint with flake8
|
||||
|
||||
- name: Get version
|
||||
id: get_version
|
||||
run: |
|
||||
# stop the build if there are Python syntax errors or undefined names
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
|
||||
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
|
||||
- name: Package
|
||||
id: package
|
||||
$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: |
|
||||
copy app\utils\package.py .\
|
||||
python package.py
|
||||
- name: Read version
|
||||
id: read_version
|
||||
$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: |
|
||||
$MAIN_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 1).Trim()
|
||||
"AUTO_MAA_version=$MAIN_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
$UPDATER_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 2 | Select-Object -Index 1).Trim()
|
||||
"updater_version=$UPDATER_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
$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<!--{json.dumps(version['version_info'], ensure_ascii=False)}-->\n{version_info_markdown(all_version_info)}",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
- name: Upload Artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
|
||||
path: |
|
||||
AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
|
||||
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: |
|
||||
@@ -110,8 +249,23 @@ jobs:
|
||||
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_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
||||
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
|
||||
@@ -120,18 +274,10 @@ jobs:
|
||||
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 }}
|
||||
- name: Setup SSH Key
|
||||
run: |
|
||||
mkdir -p ~/.ssh
|
||||
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
|
||||
chmod 600 ~/.ssh/id_rsa
|
||||
ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
|
||||
- name: Upload Release to Server
|
||||
run: |
|
||||
scp -r artifacts/* ${{ secrets.SERVER_USER }}@${{ secrets.SERVER_IP }}:/home/user/files/AUTO_MAA/
|
||||
|
||||
1
.gitignore
vendored
@@ -3,6 +3,7 @@ config/
|
||||
data/
|
||||
debug/
|
||||
history/
|
||||
script/
|
||||
resources/notice.json
|
||||
resources/theme_image.json
|
||||
resources/images/Home/BannerTheme.jpg
|
||||
116
Go_Updater/Makefile
Normal file
@@ -0,0 +1,116 @@
|
||||
# AUTO_MAA_Go_Updater Makefile
|
||||
|
||||
# Build variables
|
||||
VERSION ?= 1.0.0
|
||||
BUILD_TIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||
GIT_COMMIT := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||
OUTPUT_NAME := AUTO_MAA_Go_Updater
|
||||
BUILD_DIR := build
|
||||
DIST_DIR := dist
|
||||
|
||||
# Go build flags
|
||||
LDFLAGS := -s -w -X AUTO_MAA_Go_Updater/version.Version=$(VERSION) -X AUTO_MAA_Go_Updater/version.BuildTime=$(BUILD_TIME) -X AUTO_MAA_Go_Updater/version.GitCommit=$(GIT_COMMIT)
|
||||
|
||||
# Default target
|
||||
.PHONY: all
|
||||
all: clean build
|
||||
|
||||
# Clean build artifacts
|
||||
.PHONY: clean
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
@rm -rf $(BUILD_DIR) $(DIST_DIR)
|
||||
@mkdir -p $(BUILD_DIR) $(DIST_DIR)
|
||||
|
||||
# Build for Windows 64-bit
|
||||
.PHONY: build
|
||||
build: clean
|
||||
@echo "========================================="
|
||||
@echo "Building AUTO_MAA_Go_Updater"
|
||||
@echo "========================================="
|
||||
@echo "Version: $(VERSION)"
|
||||
@echo "Build Time: $(BUILD_TIME)"
|
||||
@echo "Git Commit: $(GIT_COMMIT)"
|
||||
@echo "Target: Windows 64-bit"
|
||||
@echo ""
|
||||
@echo "Building application..."
|
||||
@GOOS=windows GOARCH=amd64 CGO_ENABLED=1 go build -ldflags="$(LDFLAGS)" -o $(BUILD_DIR)/$(OUTPUT_NAME).exe .
|
||||
@echo "Build completed successfully!"
|
||||
@echo ""
|
||||
@echo "Build Results:"
|
||||
@ls -lh $(BUILD_DIR)/$(OUTPUT_NAME).exe
|
||||
@cp $(BUILD_DIR)/$(OUTPUT_NAME).exe $(DIST_DIR)/$(OUTPUT_NAME).exe
|
||||
@echo "Copied to: $(DIST_DIR)/$(OUTPUT_NAME).exe"
|
||||
|
||||
# Build with UPX compression
|
||||
.PHONY: build-compressed
|
||||
build-compressed: build
|
||||
@echo ""
|
||||
@echo "Compressing with UPX..."
|
||||
@if command -v upx >/dev/null 2>&1; then \
|
||||
upx --best $(BUILD_DIR)/$(OUTPUT_NAME).exe; \
|
||||
echo "Compression completed!"; \
|
||||
ls -lh $(BUILD_DIR)/$(OUTPUT_NAME).exe; \
|
||||
cp $(BUILD_DIR)/$(OUTPUT_NAME).exe $(DIST_DIR)/$(OUTPUT_NAME).exe; \
|
||||
else \
|
||||
echo "UPX not found. Skipping compression."; \
|
||||
fi
|
||||
|
||||
# Run tests
|
||||
.PHONY: test
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
@go test -v ./...
|
||||
|
||||
# Run with version flag
|
||||
.PHONY: version
|
||||
version: build
|
||||
@echo ""
|
||||
@echo "Testing version information:"
|
||||
@$(BUILD_DIR)/$(OUTPUT_NAME).exe -version
|
||||
|
||||
# Install dependencies
|
||||
.PHONY: deps
|
||||
deps:
|
||||
@echo "Installing dependencies..."
|
||||
@go mod tidy
|
||||
@go mod download
|
||||
|
||||
# Format code
|
||||
.PHONY: fmt
|
||||
fmt:
|
||||
@echo "Formatting code..."
|
||||
@go fmt ./...
|
||||
|
||||
# Lint code
|
||||
.PHONY: lint
|
||||
lint:
|
||||
@echo "Linting code..."
|
||||
@if command -v golangci-lint >/dev/null 2>&1; then \
|
||||
golangci-lint run; \
|
||||
else \
|
||||
echo "golangci-lint not found. Install it with: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest"; \
|
||||
fi
|
||||
|
||||
# Development build (faster, no optimizations)
|
||||
.PHONY: dev
|
||||
dev:
|
||||
@echo "Building development version..."
|
||||
@go build -o $(BUILD_DIR)/$(OUTPUT_NAME)-dev.exe .
|
||||
@echo "Development build completed: $(BUILD_DIR)/$(OUTPUT_NAME)-dev.exe"
|
||||
|
||||
# Help
|
||||
.PHONY: help
|
||||
help:
|
||||
@echo "Available targets:"
|
||||
@echo " all - Clean and build (default)"
|
||||
@echo " build - Build for Windows 64-bit"
|
||||
@echo " build-compressed - Build and compress with UPX"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " test - Run tests"
|
||||
@echo " version - Build and show version"
|
||||
@echo " deps - Install dependencies"
|
||||
@echo " fmt - Format code"
|
||||
@echo " lint - Lint code"
|
||||
@echo " dev - Development build"
|
||||
@echo " help - Show this help"
|
||||
15
Go_Updater/README.MD
Normal file
@@ -0,0 +1,15 @@
|
||||
# 用Go语言实现的一个AUTO_MAA下载器
|
||||
用于直接下载AUTO_MAA软件本体,在Python版本出现问题时使用。
|
||||
|
||||
## 使用方法
|
||||
1. 下载并安装Go语言环境(需要配置环境变量)
|
||||
2. 运行 `go mod tidy` 命令,安装依赖包。
|
||||
3. 运行 `go run main.go` 命令,程序会自动下载并安装AUTO_MAA软件。
|
||||
|
||||
## 构建
|
||||
运行 `.\build.ps1` 脚本即可完成构建。
|
||||
|
||||
参数说明:
|
||||
-Version:指定要构建的版本号
|
||||
|
||||
运行命令: `.\build.ps1 -Version "1.0.8"`
|
||||
312
Go_Updater/api/client.go
Normal file
@@ -0,0 +1,312 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// MirrorResponse 表示 MirrorChyan API 的响应结构
|
||||
type MirrorResponse struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
VersionName string `json:"version_name"`
|
||||
VersionNumber int `json:"version_number"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
UpdateType string `json:"update_type,omitempty"`
|
||||
ReleaseNote string `json:"release_note"`
|
||||
FileSize int64 `json:"filesize,omitempty"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// UpdateCheckParams 表示更新检查的参数
|
||||
type UpdateCheckParams struct {
|
||||
ResourceID string
|
||||
CurrentVersion string
|
||||
Channel string
|
||||
UserAgent string
|
||||
}
|
||||
|
||||
// MirrorClient 定义 Mirror API 客户端的接口方法
|
||||
type MirrorClient interface {
|
||||
CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error)
|
||||
IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool
|
||||
GetDownloadURL(versionName string) string
|
||||
}
|
||||
|
||||
// Client 实现 MirrorClient 接口
|
||||
type Client struct {
|
||||
httpClient *http.Client
|
||||
baseURL string
|
||||
downloadURL string
|
||||
}
|
||||
|
||||
// NewClient 创建新的 Mirror API 客户端
|
||||
func NewClient() *Client {
|
||||
return &Client{
|
||||
httpClient: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
baseURL: "https://mirrorchyan.com/api/resources",
|
||||
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
|
||||
}
|
||||
}
|
||||
|
||||
// CheckUpdate 调用 MirrorChyan API 检查更新
|
||||
func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) {
|
||||
// 构建 API URL
|
||||
apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, params.ResourceID)
|
||||
|
||||
// 解析 URL 并添加查询参数
|
||||
u, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("解析 API URL 失败: %w", err)
|
||||
}
|
||||
|
||||
// 添加查询参数
|
||||
q := u.Query()
|
||||
q.Set("current_version", params.CurrentVersion)
|
||||
q.Set("channel", params.Channel)
|
||||
q.Set("os", "") // 跨平台为空
|
||||
q.Set("arch", "") // 跨平台为空
|
||||
u.RawQuery = q.Encode()
|
||||
|
||||
// 创建 HTTP 请求
|
||||
req, err := http.NewRequest("GET", u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 设置 User-Agent 头
|
||||
if params.UserAgent != "" {
|
||||
req.Header.Set("User-Agent", params.UserAgent)
|
||||
} else {
|
||||
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
|
||||
}
|
||||
|
||||
// 发送 HTTP 请求
|
||||
resp, err := c.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查 HTTP 状态码
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API 返回非 200 状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 读取响应体
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取响应体失败: %w", err)
|
||||
}
|
||||
|
||||
// 解析 JSON 响应
|
||||
var mirrorResp MirrorResponse
|
||||
if err := json.Unmarshal(body, &mirrorResp); err != nil {
|
||||
return nil, fmt.Errorf("解析 JSON 响应失败: %w", err)
|
||||
}
|
||||
|
||||
return &mirrorResp, nil
|
||||
}
|
||||
|
||||
// IsUpdateAvailable 比较当前版本与 API 响应中的最新版本
|
||||
func (c *Client) IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool {
|
||||
// 检查 API 响应是否成功
|
||||
if response.Code != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 从响应中获取最新版本
|
||||
latestVersion := response.Data.VersionName
|
||||
if latestVersion == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 转换版本格式以便比较
|
||||
currentVersionNormalized := c.normalizeVersionForComparison(currentVersion)
|
||||
latestVersionNormalized := c.normalizeVersionForComparison(latestVersion)
|
||||
|
||||
// 调试输出
|
||||
// fmt.Printf("Current: %s -> %s\n", currentVersion, currentVersionNormalized)
|
||||
// fmt.Printf("Latest: %s -> %s\n", latestVersion, latestVersionNormalized)
|
||||
// fmt.Printf("Compare result: %d\n", compareVersions(currentVersionNormalized, latestVersionNormalized))
|
||||
|
||||
// 使用语义版本比较
|
||||
return compareVersions(currentVersionNormalized, latestVersionNormalized) < 0
|
||||
}
|
||||
|
||||
// normalizeVersionForComparison 将不同版本格式转换为可比较格式
|
||||
func (c *Client) normalizeVersionForComparison(version string) string {
|
||||
// 处理 AUTO_MAA 版本格式: "4.4.1.3" -> "v4.4.1-beta3"
|
||||
if !strings.HasPrefix(version, "v") && strings.Count(version, ".") == 3 {
|
||||
parts := strings.Split(version, ".")
|
||||
if len(parts) == 4 {
|
||||
major, minor, patch, beta := parts[0], parts[1], parts[2], parts[3]
|
||||
if beta == "0" {
|
||||
return fmt.Sprintf("v%s.%s.%s", major, minor, patch)
|
||||
} else {
|
||||
return fmt.Sprintf("v%s.%s.%s-beta%s", major, minor, patch, beta)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果已经是标准格式则直接返回
|
||||
return version
|
||||
}
|
||||
|
||||
// compareVersions 比较两个语义版本字符串
|
||||
// 返回值: -1 如果 v1 < v2, 0 如果 v1 == v2, 1 如果 v1 > v2
|
||||
func compareVersions(v1, v2 string) int {
|
||||
// 通过移除 'v' 前缀来标准化版本
|
||||
v1 = normalizeVersion(v1)
|
||||
v2 = normalizeVersion(v2)
|
||||
|
||||
// 解析版本组件
|
||||
parts1 := parseVersionParts(v1)
|
||||
parts2 := parseVersionParts(v2)
|
||||
|
||||
// 比较前三个组件 (major.minor.patch)
|
||||
for i := 0; i < 3; i++ {
|
||||
var p1, p2 int
|
||||
if i < len(parts1) {
|
||||
p1 = parts1[i]
|
||||
}
|
||||
if i < len(parts2) {
|
||||
p2 = parts2[i]
|
||||
}
|
||||
|
||||
if p1 < p2 {
|
||||
return -1
|
||||
} else if p1 > p2 {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
// 如果前三个组件相同,比较beta版本号
|
||||
var beta1, beta2 int
|
||||
if len(parts1) > 3 {
|
||||
beta1 = parts1[3]
|
||||
}
|
||||
if len(parts2) > 3 {
|
||||
beta2 = parts2[3]
|
||||
}
|
||||
|
||||
// 特殊处理beta版本比较:
|
||||
// - 如果一个是正式版(beta=0),另一个是beta版(beta>0),正式版更新
|
||||
// - 如果都是beta版,比较beta版本号
|
||||
if beta1 == 0 && beta2 > 0 {
|
||||
return 1 // 正式版比beta版更新
|
||||
}
|
||||
if beta1 > 0 && beta2 == 0 {
|
||||
return -1 // beta版比正式版旧
|
||||
}
|
||||
|
||||
// 都是正式版或都是beta版,直接比较
|
||||
if beta1 < beta2 {
|
||||
return -1
|
||||
} else if beta1 > beta2 {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
// normalizeVersion 移除 'v' 前缀并处理常见版本格式
|
||||
func normalizeVersion(version string) string {
|
||||
if len(version) > 0 && (version[0] == 'v' || version[0] == 'V') {
|
||||
return version[1:]
|
||||
}
|
||||
return version
|
||||
}
|
||||
|
||||
// parseVersionParts 将版本字符串解析为数字组件,包括beta版本号
|
||||
func parseVersionParts(version string) []int {
|
||||
if version == "" {
|
||||
return []int{0}
|
||||
}
|
||||
|
||||
parts := make([]int, 0, 4)
|
||||
current := 0
|
||||
|
||||
// 先检查是否包含 -beta
|
||||
betaIndex := strings.Index(version, "-beta")
|
||||
var mainVersion, betaVersion string
|
||||
|
||||
if betaIndex != -1 {
|
||||
mainVersion = version[:betaIndex]
|
||||
betaVersion = version[betaIndex+5:] // 跳过 "-beta"
|
||||
} else {
|
||||
mainVersion = version
|
||||
betaVersion = ""
|
||||
}
|
||||
|
||||
// 解析主版本号 (major.minor.patch)
|
||||
for _, char := range mainVersion {
|
||||
if char >= '0' && char <= '9' {
|
||||
current = current*10 + int(char-'0')
|
||||
} else if char == '.' {
|
||||
parts = append(parts, current)
|
||||
current = 0
|
||||
} else {
|
||||
// 遇到非数字非点字符,停止解析
|
||||
break
|
||||
}
|
||||
}
|
||||
// 添加最后一个主版本组件
|
||||
parts = append(parts, current)
|
||||
|
||||
// 确保至少有 3 个组件 (major.minor.patch)
|
||||
for len(parts) < 3 {
|
||||
parts = append(parts, 0)
|
||||
}
|
||||
|
||||
// 解析beta版本号
|
||||
if betaVersion != "" {
|
||||
// 跳过可能的点号
|
||||
if strings.HasPrefix(betaVersion, ".") {
|
||||
betaVersion = betaVersion[1:]
|
||||
}
|
||||
|
||||
betaNum := 0
|
||||
for _, char := range betaVersion {
|
||||
if char >= '0' && char <= '9' {
|
||||
betaNum = betaNum*10 + int(char-'0')
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Beta版本号保持正数,但在比较时会特殊处理
|
||||
parts = append(parts, betaNum)
|
||||
} else {
|
||||
// 非beta版本,添加0作为beta版本号
|
||||
parts = append(parts, 0)
|
||||
}
|
||||
|
||||
return parts
|
||||
}
|
||||
|
||||
// GetDownloadURL 根据版本名生成下载站的下载 URL
|
||||
func (c *Client) GetDownloadURL(versionName string) string {
|
||||
// 将版本名转换为文件名格式
|
||||
// 例如: "v4.4.0" -> "AUTO_MAA_v4.4.0.zip"
|
||||
// 例如: "v4.4.1-beta3" -> "AUTO_MAA_v4.4.1-beta.3.zip"
|
||||
filename := fmt.Sprintf("AUTO_MAA_%s.zip", versionName)
|
||||
|
||||
// 处理 beta 版本: 将 "beta3" 转换为 "beta.3"
|
||||
if strings.Contains(filename, "-beta") && !strings.Contains(filename, "-beta.") {
|
||||
filename = strings.Replace(filename, "-beta", "-beta.", 1)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s/%s", c.downloadURL, filename)
|
||||
}
|
||||
186
Go_Updater/api/client_test.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
client := NewClient()
|
||||
if client == nil {
|
||||
t.Fatal("NewClient() 返回 nil")
|
||||
}
|
||||
if client.httpClient == nil {
|
||||
t.Fatal("HTTP 客户端为 nil")
|
||||
}
|
||||
if client.baseURL != "https://mirrorchyan.com/api/resources" {
|
||||
t.Errorf("期望基础 URL 'https://mirrorchyan.com/api/resources',得到 '%s'", client.baseURL)
|
||||
}
|
||||
if client.downloadURL != "http://221.236.27.82:10197/d/AUTO_MAA" {
|
||||
t.Errorf("期望下载 URL 'http://221.236.27.82:10197/d/AUTO_MAA',得到 '%s'", client.downloadURL)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDownloadURL(t *testing.T) {
|
||||
client := NewClient()
|
||||
|
||||
tests := []struct {
|
||||
versionName string
|
||||
expected string
|
||||
}{
|
||||
{"v4.4.0", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.0.zip"},
|
||||
{"v4.4.1-beta3", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v4.4.1-beta.3.zip"},
|
||||
{"v1.2.3", "http://221.236.27.82:10197/d/AUTO_MAA/AUTO_MAA_v1.2.3.zip"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := client.GetDownloadURL(test.versionName)
|
||||
if result != test.expected {
|
||||
t.Errorf("版本 %s,期望 %s,得到 %s", test.versionName, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckUpdate(t *testing.T) {
|
||||
// 创建测试服务器
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response := MirrorResponse{
|
||||
Code: 0,
|
||||
Msg: "success",
|
||||
Data: struct {
|
||||
VersionName string `json:"version_name"`
|
||||
VersionNumber int `json:"version_number"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
UpdateType string `json:"update_type,omitempty"`
|
||||
ReleaseNote string `json:"release_note"`
|
||||
FileSize int64 `json:"filesize,omitempty"`
|
||||
}{
|
||||
VersionName: "v4.4.1",
|
||||
VersionNumber: 48,
|
||||
Channel: "stable",
|
||||
ReleaseNote: "测试发布说明",
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
err := json.NewEncoder(w).Encode(response)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
// 使用测试服务器 URL 创建客户端
|
||||
client := &Client{
|
||||
httpClient: &http.Client{},
|
||||
baseURL: server.URL,
|
||||
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
|
||||
}
|
||||
|
||||
// 测试更新检查
|
||||
params := UpdateCheckParams{
|
||||
ResourceID: "AUTO_MAA",
|
||||
CurrentVersion: "4.4.0.0",
|
||||
Channel: "stable",
|
||||
UserAgent: "TestAgent/1.0",
|
||||
}
|
||||
|
||||
response, err := client.CheckUpdate(params)
|
||||
if err != nil {
|
||||
t.Fatalf("CheckUpdate 失败: %v", err)
|
||||
}
|
||||
|
||||
if response.Code != 0 {
|
||||
t.Errorf("期望代码 0,得到 %d", response.Code)
|
||||
}
|
||||
if response.Data.VersionName != "v4.4.1" {
|
||||
t.Errorf("期望版本 v4.4.1,得到 %s", response.Data.VersionName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsUpdateAvailable(t *testing.T) {
|
||||
client := NewClient()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
response *MirrorResponse
|
||||
currentVersion string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "有可用更新",
|
||||
response: &MirrorResponse{
|
||||
Code: 0,
|
||||
Data: struct {
|
||||
VersionName string `json:"version_name"`
|
||||
VersionNumber int `json:"version_number"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
UpdateType string `json:"update_type,omitempty"`
|
||||
ReleaseNote string `json:"release_note"`
|
||||
FileSize int64 `json:"filesize,omitempty"`
|
||||
}{VersionName: "v4.4.1"},
|
||||
},
|
||||
currentVersion: "4.4.0.0",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "无可用更新",
|
||||
response: &MirrorResponse{
|
||||
Code: 0,
|
||||
Data: struct {
|
||||
VersionName string `json:"version_name"`
|
||||
VersionNumber int `json:"version_number"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
UpdateType string `json:"update_type,omitempty"`
|
||||
ReleaseNote string `json:"release_note"`
|
||||
FileSize int64 `json:"filesize,omitempty"`
|
||||
}{VersionName: "v4.4.0"},
|
||||
},
|
||||
currentVersion: "4.4.0.0",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "beta版本有更新",
|
||||
response: &MirrorResponse{
|
||||
Code: 0,
|
||||
Data: struct {
|
||||
VersionName string `json:"version_name"`
|
||||
VersionNumber int `json:"version_number"`
|
||||
URL string `json:"url,omitempty"`
|
||||
SHA256 string `json:"sha256,omitempty"`
|
||||
Channel string `json:"channel"`
|
||||
OS string `json:"os"`
|
||||
Arch string `json:"arch"`
|
||||
UpdateType string `json:"update_type,omitempty"`
|
||||
ReleaseNote string `json:"release_note"`
|
||||
FileSize int64 `json:"filesize,omitempty"`
|
||||
}{VersionName: "v4.4.1-beta.4"},
|
||||
},
|
||||
currentVersion: "4.4.1.3",
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
result := client.IsUpdateAvailable(test.response, test.currentVersion)
|
||||
if result != test.expected {
|
||||
t.Errorf("期望 %t,得到 %t", test.expected, result)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
34
Go_Updater/app.rc
Normal file
@@ -0,0 +1,34 @@
|
||||
#include <windows.h>
|
||||
|
||||
// Application icon
|
||||
IDI_ICON1 ICON "icon/AUTO_MAA_Go_Updater.ico"
|
||||
|
||||
// Version information
|
||||
VS_VERSION_INFO VERSIONINFO
|
||||
FILEVERSION 1,0,0,0
|
||||
PRODUCTVERSION 1,0,0,0
|
||||
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
|
||||
FILEFLAGS 0x0L
|
||||
FILEOS VOS__WINDOWS32
|
||||
FILETYPE VFT_APP
|
||||
FILESUBTYPE VFT2_UNKNOWN
|
||||
BEGIN
|
||||
BLOCK "StringFileInfo"
|
||||
BEGIN
|
||||
BLOCK "040904B0"
|
||||
BEGIN
|
||||
VALUE "CompanyName", "AUTO MAA Team"
|
||||
VALUE "FileDescription", "AUTO MAA Go Updater"
|
||||
VALUE "FileVersion", "1.0.0.0"
|
||||
VALUE "InternalName", "AUTO_MAA_Go_Updater"
|
||||
VALUE "LegalCopyright", "Copyright (C) 2025"
|
||||
VALUE "OriginalFilename", "AUTO_MAA_Go_Updater.exe"
|
||||
VALUE "ProductName", "AUTO MAA Go Updater"
|
||||
VALUE "ProductVersion", "1.0.0.0"
|
||||
END
|
||||
END
|
||||
BLOCK "VarFileInfo"
|
||||
BEGIN
|
||||
VALUE "Translation", 0x409, 1200
|
||||
END
|
||||
END
|
||||
BIN
Go_Updater/app.syso
Normal file
34
Go_Updater/assets/assets.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed config_template.yaml
|
||||
var EmbeddedAssets embed.FS
|
||||
|
||||
// GetConfigTemplate 返回嵌入的配置模板
|
||||
func GetConfigTemplate() ([]byte, error) {
|
||||
return EmbeddedAssets.ReadFile("config_template.yaml")
|
||||
}
|
||||
|
||||
// GetAssetFS 返回嵌入的文件系统
|
||||
func GetAssetFS() fs.FS {
|
||||
return EmbeddedAssets
|
||||
}
|
||||
|
||||
// ListAssets 返回所有嵌入资源的列表
|
||||
func ListAssets() ([]string, error) {
|
||||
var assets []string
|
||||
err := fs.WalkDir(EmbeddedAssets, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !d.IsDir() {
|
||||
assets = append(assets, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return assets, err
|
||||
}
|
||||
100
Go_Updater/assets/assets_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package assets
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetConfigTemplate(t *testing.T) {
|
||||
data, err := GetConfigTemplate()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get config template: %v", err)
|
||||
}
|
||||
|
||||
if len(data) == 0 {
|
||||
t.Fatal("Config template is empty")
|
||||
}
|
||||
|
||||
// Check that it contains expected content
|
||||
content := string(data)
|
||||
if !contains(content, "resource_id") {
|
||||
t.Error("Config template should contain 'resource_id'")
|
||||
}
|
||||
|
||||
if !contains(content, "current_version") {
|
||||
t.Error("Config template should contain 'current_version'")
|
||||
}
|
||||
|
||||
if !contains(content, "user_agent") {
|
||||
t.Error("Config template should contain 'user_agent'")
|
||||
}
|
||||
}
|
||||
|
||||
func TestListAssets(t *testing.T) {
|
||||
assets, err := ListAssets()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to list assets: %v", err)
|
||||
}
|
||||
|
||||
if len(assets) == 0 {
|
||||
t.Fatal("No assets found")
|
||||
}
|
||||
|
||||
// Check that config template is in the list
|
||||
found := false
|
||||
for _, asset := range assets {
|
||||
if asset == "config_template.yaml" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("config_template.yaml should be in the assets list")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAssetFS(t *testing.T) {
|
||||
fs := GetAssetFS()
|
||||
if fs == nil {
|
||||
t.Fatal("Asset filesystem should not be nil")
|
||||
}
|
||||
|
||||
// Try to open the config template
|
||||
file, err := fs.Open("config_template.yaml")
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to open config template from filesystem: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Check that we can read from it
|
||||
buffer := make([]byte, 100)
|
||||
n, err := file.Read(buffer)
|
||||
if err != nil && err.Error() != "EOF" {
|
||||
t.Fatalf("Failed to read from config template: %v", err)
|
||||
}
|
||||
|
||||
if n == 0 {
|
||||
t.Fatal("Config template appears to be empty")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if string contains substring
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > len(substr) && (s[:len(substr)] == substr ||
|
||||
s[len(s)-len(substr):] == substr ||
|
||||
containsAt(s, substr, 1))))
|
||||
}
|
||||
|
||||
func containsAt(s, substr string, start int) bool {
|
||||
if start >= len(s) {
|
||||
return false
|
||||
}
|
||||
if start+len(substr) > len(s) {
|
||||
return containsAt(s, substr, start+1)
|
||||
}
|
||||
if s[start:start+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
return containsAt(s, substr, start+1)
|
||||
}
|
||||
7
Go_Updater/assets/config_template.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
resource_id: "AUTO_MAA"
|
||||
current_version: "v1.0.0"
|
||||
user_agent: "AUTO_MAA_Go_Updater/1.0"
|
||||
backup_url: "https://backup-download-site.com/releases"
|
||||
log_level: "info"
|
||||
auto_check: true
|
||||
check_interval: 3600 # seconds
|
||||
55
Go_Updater/build-config.yaml
Normal file
@@ -0,0 +1,55 @@
|
||||
# Build Configuration for AUTO_MAA_Go_Updater
|
||||
|
||||
project:
|
||||
name: "AUTO_MAA_Go_Updater"
|
||||
module: "AUTO_MAA_Go_Updater"
|
||||
description: "AUTO_MAA_Go版本更新器"
|
||||
|
||||
version:
|
||||
default: "1.0.0"
|
||||
build_time_format: "2006-01-02T15:04:05Z"
|
||||
|
||||
targets:
|
||||
- name: "windows-amd64"
|
||||
goos: "windows"
|
||||
goarch: "amd64"
|
||||
cgo_enabled: true
|
||||
output: "AUTO_MAA_Go_Updater.exe"
|
||||
|
||||
build:
|
||||
flags:
|
||||
ldflags: "-s -w"
|
||||
tags: []
|
||||
|
||||
optimization:
|
||||
strip_debug: true
|
||||
strip_symbols: true
|
||||
upx_compression: false # Optional, requires UPX
|
||||
|
||||
size_requirements:
|
||||
max_size_mb: 10
|
||||
warn_size_mb: 8
|
||||
|
||||
assets:
|
||||
embed:
|
||||
- "assets/config_template.yaml"
|
||||
|
||||
directories:
|
||||
build: "build"
|
||||
dist: "dist"
|
||||
temp: "temp"
|
||||
|
||||
version_injection:
|
||||
package: "AUTO_MAA_Go_Updater/version"
|
||||
variables:
|
||||
- name: "Version"
|
||||
source: "version"
|
||||
- name: "BuildTime"
|
||||
source: "build_time"
|
||||
- name: "GitCommit"
|
||||
source: "git_commit"
|
||||
|
||||
quality:
|
||||
run_tests: true
|
||||
run_lint: false # Optional
|
||||
format_code: true
|
||||
93
Go_Updater/build.bat
Normal file
@@ -0,0 +1,93 @@
|
||||
@echo off
|
||||
setlocal enabledelayedexpansion
|
||||
|
||||
echo ========================================
|
||||
echo AUTO_MAA_Go_Updater Build Script
|
||||
echo ========================================
|
||||
|
||||
:: Set build variables
|
||||
set OUTPUT_NAME=AUTO_MAA_Go_Updater.exe
|
||||
set BUILD_DIR=build
|
||||
set DIST_DIR=dist
|
||||
|
||||
:: Get current datetime for build time
|
||||
for /f "tokens=2 delims==" %%a in ('wmic OS Get localdatetime /value') do set "dt=%%a"
|
||||
set "YYYY=%dt:~0,4%" & set "MM=%dt:~4,2%" & set "DD=%dt:~6,2%"
|
||||
set "HH=%dt:~8,2%" & set "Min=%dt:~10,2%" & set "Sec=%dt:~12,2%"
|
||||
set "BUILD_TIME=%YYYY%-%MM%-%DD%T%HH%:%Min%:%Sec%Z"
|
||||
|
||||
:: Get git commit hash (if available)
|
||||
git rev-parse --short HEAD > temp_commit.txt 2>nul
|
||||
if exist temp_commit.txt (
|
||||
set /p GIT_COMMIT=<temp_commit.txt
|
||||
del temp_commit.txt
|
||||
) else (
|
||||
set GIT_COMMIT=unknown
|
||||
)
|
||||
|
||||
:: Use commit hash as version
|
||||
set VERSION=%GIT_COMMIT%
|
||||
|
||||
echo Build Information:
|
||||
echo - Version: %VERSION%
|
||||
echo - Build Time: %BUILD_TIME%
|
||||
echo - Git Commit: %GIT_COMMIT%
|
||||
echo - Target: Windows 64-bit
|
||||
echo.
|
||||
|
||||
:: Create build directories
|
||||
if not exist %BUILD_DIR% mkdir %BUILD_DIR%
|
||||
if not exist %DIST_DIR% mkdir %DIST_DIR%
|
||||
|
||||
:: Set build flags
|
||||
set LDFLAGS=-s -w -X AUTO_MAA_Go_Updater/version.Version=%VERSION% -X AUTO_MAA_Go_Updater/version.BuildTime=%BUILD_TIME% -X AUTO_MAA_Go_Updater/version.GitCommit=%GIT_COMMIT%
|
||||
|
||||
echo Building application...
|
||||
|
||||
:: Ensure icon resource is compiled
|
||||
if not exist app.syso (
|
||||
echo Compiling icon resource...
|
||||
where rsrc >nul 2>&1
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
rsrc -ico icon\AUTO_MAA_Go_Updater.ico -o app.syso
|
||||
if !ERRORLEVEL! equ 0 (
|
||||
echo Icon resource compiled successfully
|
||||
) else (
|
||||
echo Warning: Failed to compile icon resource
|
||||
)
|
||||
) else (
|
||||
echo Warning: rsrc not found. Install with: go install github.com/akavel/rsrc@latest
|
||||
)
|
||||
)
|
||||
|
||||
:: Set environment variables for Go build
|
||||
set GOOS=windows
|
||||
set GOARCH=amd64
|
||||
set CGO_ENABLED=1
|
||||
|
||||
:: Build the application
|
||||
go build -ldflags="%LDFLAGS%" -o %BUILD_DIR%\%OUTPUT_NAME% .
|
||||
|
||||
if %ERRORLEVEL% neq 0 (
|
||||
echo Build failed!
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
echo Build completed successfully!
|
||||
|
||||
:: Get file size
|
||||
for %%A in (%BUILD_DIR%\%OUTPUT_NAME%) do set FILE_SIZE=%%~zA
|
||||
set /a FILE_SIZE_MB=%FILE_SIZE%/1024/1024
|
||||
|
||||
echo.
|
||||
echo Build Results:
|
||||
echo - Output: %BUILD_DIR%\%OUTPUT_NAME%
|
||||
echo - Size: %FILE_SIZE% bytes (~%FILE_SIZE_MB% MB)
|
||||
|
||||
:: Copy to dist directory
|
||||
copy %BUILD_DIR%\%OUTPUT_NAME% %DIST_DIR%\%OUTPUT_NAME% >nul
|
||||
echo - Copied to: %DIST_DIR%\%OUTPUT_NAME%
|
||||
|
||||
echo.
|
||||
echo Build script completed successfully!
|
||||
echo ========================================
|
||||
105
Go_Updater/build.ps1
Normal file
@@ -0,0 +1,105 @@
|
||||
# AUTO_MAA_Go_Updater Build Script (PowerShell)
|
||||
param(
|
||||
[string]$OutputName = "AUTO_MAA_Go_Updater.exe",
|
||||
[switch]$Compress = $false
|
||||
)
|
||||
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host "AUTO_MAA_Go_Updater Build Script" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
|
||||
# Set build variables
|
||||
$BuildDir = "build"
|
||||
$DistDir = "dist"
|
||||
$BuildTime = (Get-Date).ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||||
|
||||
|
||||
# Get git commit hash
|
||||
try {
|
||||
$GitCommit = (git rev-parse --short HEAD 2>$null).Trim()
|
||||
if (-not $GitCommit) { $GitCommit = "unknown" }
|
||||
} catch {
|
||||
$GitCommit = "unknown"
|
||||
}
|
||||
|
||||
Write-Host "Build Information:" -ForegroundColor Yellow
|
||||
Write-Host "- Version: $GitCommit"
|
||||
Write-Host "- Build Time: $BuildTime"
|
||||
Write-Host "- Git Commit: $GitCommit"
|
||||
Write-Host "- Target: Windows 64-bit"
|
||||
Write-Host ""
|
||||
|
||||
# Create build directories
|
||||
if (-not (Test-Path $BuildDir)) { New-Item -ItemType Directory -Path $BuildDir | Out-Null }
|
||||
if (-not (Test-Path $DistDir)) { New-Item -ItemType Directory -Path $DistDir | Out-Null }
|
||||
|
||||
# Set environment variables
|
||||
$env:GOOS = "windows"
|
||||
$env:GOARCH = "amd64"
|
||||
$env:CGO_ENABLED = "1"
|
||||
|
||||
# Set build flags
|
||||
$LdFlags = "-s -w -X AUTO_MAA_Go_Updater/version.Version=$Version -X AUTO_MAA_Go_Updater/version.BuildTime=$BuildTime -X AUTO_MAA_Go_Updater/version.GitCommit=$GitCommit"
|
||||
|
||||
Write-Host "Building application..." -ForegroundColor Green
|
||||
|
||||
# Ensure icon resource is compiled
|
||||
if (-not (Test-Path "app.syso")) {
|
||||
Write-Host "Compiling icon resource..." -ForegroundColor Yellow
|
||||
if (Get-Command rsrc -ErrorAction SilentlyContinue) {
|
||||
rsrc -ico icon/AUTO_MAA_Go_Updater.ico -o app.syso
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Warning: Failed to compile icon resource" -ForegroundColor Yellow
|
||||
} else {
|
||||
Write-Host "Icon resource compiled successfully" -ForegroundColor Green
|
||||
}
|
||||
} else {
|
||||
Write-Host "Warning: rsrc not found. Install with: go install github.com/akavel/rsrc@latest" -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Build the application
|
||||
$BuildCommand = "go build -ldflags=`"$LdFlags`" -o $BuildDir\$OutputName ."
|
||||
Invoke-Expression $BuildCommand
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Host "Build failed!" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Build completed successfully!" -ForegroundColor Green
|
||||
|
||||
# Get file information
|
||||
$OutputFile = Get-Item "$BuildDir\$OutputName"
|
||||
$FileSizeMB = [math]::Round($OutputFile.Length / 1MB, 2)
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build Results:" -ForegroundColor Yellow
|
||||
Write-Host "- Output: $($OutputFile.FullName)"
|
||||
Write-Host "- Size: $($OutputFile.Length) bytes (~$FileSizeMB MB)"
|
||||
|
||||
|
||||
# Optional UPX compression
|
||||
if ($Compress) {
|
||||
Write-Host ""
|
||||
Write-Host "Compressing with UPX..." -ForegroundColor Yellow
|
||||
|
||||
if (Get-Command upx -ErrorAction SilentlyContinue) {
|
||||
upx --best "$BuildDir\$OutputName"
|
||||
|
||||
$CompressedFile = Get-Item "$BuildDir\$OutputName"
|
||||
$CompressedSizeMB = [math]::Round($CompressedFile.Length / 1MB, 2)
|
||||
|
||||
Write-Host "- Compressed Size: $($CompressedFile.Length) bytes (~$CompressedSizeMB MB)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "UPX not found. Skipping compression." -ForegroundColor Yellow
|
||||
}
|
||||
}
|
||||
|
||||
# Copy to dist directory
|
||||
Copy-Item "$BuildDir\$OutputName" "$DistDir\$OutputName" -Force
|
||||
Write-Host "- Copied to: $DistDir\$OutputName"
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build script completed successfully!" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
198
Go_Updater/config/config.go
Normal file
@@ -0,0 +1,198 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"AUTO_MAA_Go_Updater/assets"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Config 表示应用程序配置
|
||||
type Config struct {
|
||||
ResourceID string `yaml:"resource_id"`
|
||||
CurrentVersion string `yaml:"current_version"`
|
||||
UserAgent string `yaml:"user_agent"`
|
||||
BackupURL string `yaml:"backup_url"`
|
||||
LogLevel string `yaml:"log_level"`
|
||||
AutoCheck bool `yaml:"auto_check"`
|
||||
CheckInterval int `yaml:"check_interval"` // 秒
|
||||
}
|
||||
|
||||
// ConfigManager 定义配置管理的接口方法
|
||||
type ConfigManager interface {
|
||||
Load() (*Config, error)
|
||||
Save(config *Config) error
|
||||
GetConfigPath() string
|
||||
}
|
||||
|
||||
// DefaultConfigManager 实现 ConfigManager 接口
|
||||
type DefaultConfigManager struct {
|
||||
configPath string
|
||||
}
|
||||
|
||||
// NewConfigManager 创建新的配置管理器
|
||||
func NewConfigManager() ConfigManager {
|
||||
configDir := getConfigDir()
|
||||
configPath := filepath.Join(configDir, "config.yaml")
|
||||
return &DefaultConfigManager{
|
||||
configPath: configPath,
|
||||
}
|
||||
}
|
||||
|
||||
// GetConfigPath 返回配置文件的路径
|
||||
func (cm *DefaultConfigManager) GetConfigPath() string {
|
||||
return cm.configPath
|
||||
}
|
||||
|
||||
// Load 读取并解析配置文件
|
||||
func (cm *DefaultConfigManager) Load() (*Config, error) {
|
||||
// 如果配置目录不存在则创建
|
||||
configDir := filepath.Dir(cm.configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("创建配置目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果配置文件不存在,创建默认配置
|
||||
if _, err := os.Stat(cm.configPath); os.IsNotExist(err) {
|
||||
defaultConfig := getDefaultConfig()
|
||||
if err := cm.Save(defaultConfig); err != nil {
|
||||
return nil, fmt.Errorf("创建默认配置失败: %w", err)
|
||||
}
|
||||
return defaultConfig, nil
|
||||
}
|
||||
|
||||
// 读取现有配置文件
|
||||
data, err := os.ReadFile(cm.configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 验证并应用缺失字段的默认值
|
||||
if err := validateAndApplyDefaults(&config); err != nil {
|
||||
return nil, fmt.Errorf("配置验证失败: %w", err)
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
|
||||
// Save 将配置写入文件
|
||||
func (cm *DefaultConfigManager) Save(config *Config) error {
|
||||
// 保存前验证配置
|
||||
if err := validateConfig(config); err != nil {
|
||||
return fmt.Errorf("配置验证失败: %w", err)
|
||||
}
|
||||
|
||||
// 如果配置目录不存在则创建
|
||||
configDir := filepath.Dir(cm.configPath)
|
||||
if err := os.MkdirAll(configDir, 0755); err != nil {
|
||||
return fmt.Errorf("创建配置目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 将配置序列化为 YAML
|
||||
data, err := yaml.Marshal(config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("序列化配置失败: %w", err)
|
||||
}
|
||||
|
||||
// 写入文件
|
||||
if err := os.WriteFile(cm.configPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("写入配置文件失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDefaultConfig 返回带有默认值的配置
|
||||
func getDefaultConfig() *Config {
|
||||
// 首先尝试从嵌入模板加载
|
||||
if templateData, err := assets.GetConfigTemplate(); err == nil {
|
||||
var config Config
|
||||
if err := yaml.Unmarshal(templateData, &config); err == nil {
|
||||
return &config
|
||||
}
|
||||
}
|
||||
|
||||
// 如果模板加载失败则回退到硬编码默认值
|
||||
return &Config{
|
||||
ResourceID: "M9A", // 默认资源 ID
|
||||
CurrentVersion: "v1.0.0",
|
||||
UserAgent: "AUTO_MAA_Go_Updater/1.0",
|
||||
BackupURL: "",
|
||||
LogLevel: "info",
|
||||
AutoCheck: true,
|
||||
CheckInterval: 3600, // 1 小时
|
||||
}
|
||||
}
|
||||
|
||||
// validateConfig 验证配置值
|
||||
func validateConfig(config *Config) error {
|
||||
if config == nil {
|
||||
return fmt.Errorf("配置不能为空")
|
||||
}
|
||||
|
||||
if config.ResourceID == "" {
|
||||
return fmt.Errorf("resource_id 不能为空")
|
||||
}
|
||||
|
||||
if config.CurrentVersion == "" {
|
||||
return fmt.Errorf("current_version 不能为空")
|
||||
}
|
||||
|
||||
if config.UserAgent == "" {
|
||||
return fmt.Errorf("user_agent 不能为空")
|
||||
}
|
||||
|
||||
validLogLevels := map[string]bool{
|
||||
"debug": true,
|
||||
"info": true,
|
||||
"warn": true,
|
||||
"error": true,
|
||||
}
|
||||
if !validLogLevels[config.LogLevel] {
|
||||
return fmt.Errorf("无效的 log_level: %s (必须是 debug, info, warn 或 error)", config.LogLevel)
|
||||
}
|
||||
|
||||
if config.CheckInterval < 60 {
|
||||
return fmt.Errorf("check_interval 必须至少为 60 秒")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateAndApplyDefaults 验证配置并为缺失字段应用默认值
|
||||
func validateAndApplyDefaults(config *Config) error {
|
||||
defaults := getDefaultConfig()
|
||||
|
||||
// 为空字段应用默认值
|
||||
if config.UserAgent == "" {
|
||||
config.UserAgent = defaults.UserAgent
|
||||
}
|
||||
if config.LogLevel == "" {
|
||||
config.LogLevel = defaults.LogLevel
|
||||
}
|
||||
if config.CheckInterval == 0 {
|
||||
config.CheckInterval = defaults.CheckInterval
|
||||
}
|
||||
if config.CurrentVersion == "" {
|
||||
config.CurrentVersion = defaults.CurrentVersion
|
||||
}
|
||||
|
||||
// 应用默认值后进行验证
|
||||
return validateConfig(config)
|
||||
}
|
||||
|
||||
// getConfigDir 返回配置目录路径
|
||||
func getConfigDir() string {
|
||||
// 在 Windows 上使用 APPDATA,回退到当前目录
|
||||
if appData := os.Getenv("APPDATA"); appData != "" {
|
||||
return filepath.Join(appData, "AUTO_MAA_Go_Updater")
|
||||
}
|
||||
return "."
|
||||
}
|
||||
55
Go_Updater/config/config.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"Function": {
|
||||
"BossKey": "",
|
||||
"HistoryRetentionTime": 0,
|
||||
"HomeImageMode": "默认",
|
||||
"IfAgreeBilibili": true,
|
||||
"IfAllowSleep": false,
|
||||
"IfSilence": false,
|
||||
"IfSkipMumuSplashAds": false,
|
||||
"UnattendedMode": false
|
||||
},
|
||||
"Notify": {
|
||||
"AuthorizationCode": "",
|
||||
"CompanyWebHookBotUrl": "",
|
||||
"FromAddress": "",
|
||||
"IfCompanyWebHookBot": false,
|
||||
"IfPushPlyer": false,
|
||||
"IfSendMail": false,
|
||||
"IfSendSixStar": false,
|
||||
"IfSendStatistic": false,
|
||||
"IfServerChan": false,
|
||||
"SMTPServerAddress": "",
|
||||
"SendTaskResultTime": "不推送",
|
||||
"ServerChanChannel": "",
|
||||
"ServerChanKey": "",
|
||||
"ServerChanTag": "",
|
||||
"ToAddress": ""
|
||||
},
|
||||
"Start": {
|
||||
"IfMinimizeDirectly": false,
|
||||
"IfRunDirectly": false,
|
||||
"IfSelfStart": false
|
||||
},
|
||||
"QFluentWidgets": {
|
||||
"ThemeColor": "#ff009faa",
|
||||
"ThemeMode": "Dark"
|
||||
},
|
||||
"UI": {
|
||||
"IfShowTray": false,
|
||||
"IfToTray": false,
|
||||
"location": "100x100",
|
||||
"maximized": false,
|
||||
"size": "1200x700"
|
||||
},
|
||||
"Update": {
|
||||
"IfAutoUpdate": false,
|
||||
"ProxyUrlList": [],
|
||||
"ThreadNumb": 8,
|
||||
"UpdateType": "stable"
|
||||
},
|
||||
"Voice": {
|
||||
"Enabled": false,
|
||||
"Type": "simple"
|
||||
}
|
||||
}
|
||||
153
Go_Updater/config/config_test.go
Normal file
@@ -0,0 +1,153 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestConfigManagerLoadSave(t *testing.T) {
|
||||
// 为测试创建临时目录
|
||||
tempDir := t.TempDir()
|
||||
|
||||
// 使用临时路径创建配置管理器
|
||||
cm := &DefaultConfigManager{
|
||||
configPath: filepath.Join(tempDir, "test-config.yaml"),
|
||||
}
|
||||
|
||||
// 测试加载不存在的配置(应创建默认配置)
|
||||
config, err := cm.Load()
|
||||
if err != nil {
|
||||
t.Errorf("加载配置失败: %v", err)
|
||||
}
|
||||
|
||||
if config == nil {
|
||||
t.Errorf("配置不应为 nil")
|
||||
}
|
||||
|
||||
// 验证默认值
|
||||
if config.CurrentVersion != "v1.0.0" {
|
||||
t.Errorf("期望默认版本 v1.0.0,得到 %s", config.CurrentVersion)
|
||||
}
|
||||
|
||||
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
|
||||
t.Errorf("期望默认用户代理,得到 %s", config.UserAgent)
|
||||
}
|
||||
|
||||
// 设置一些值
|
||||
config.ResourceID = "TEST123"
|
||||
|
||||
// 保存配置
|
||||
err = cm.Save(config)
|
||||
if err != nil {
|
||||
t.Errorf("保存配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 再次加载配置
|
||||
loadedConfig, err := cm.Load()
|
||||
if err != nil {
|
||||
t.Errorf("加载已保存配置失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证值
|
||||
if loadedConfig.ResourceID != "TEST123" {
|
||||
t.Errorf("期望 ResourceID TEST123,得到 %s", loadedConfig.ResourceID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigValidation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
config *Config
|
||||
expectError bool
|
||||
}{
|
||||
{
|
||||
name: "空配置",
|
||||
config: nil,
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "空 ResourceID",
|
||||
config: &Config{
|
||||
ResourceID: "",
|
||||
CurrentVersion: "v1.0.0",
|
||||
UserAgent: "Test/1.0",
|
||||
LogLevel: "info",
|
||||
CheckInterval: 3600,
|
||||
},
|
||||
expectError: true,
|
||||
},
|
||||
{
|
||||
name: "有效配置",
|
||||
config: &Config{
|
||||
ResourceID: "TEST",
|
||||
CurrentVersion: "v1.0.0",
|
||||
UserAgent: "Test/1.0",
|
||||
LogLevel: "info",
|
||||
CheckInterval: 3600,
|
||||
},
|
||||
expectError: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := validateConfig(tt.config)
|
||||
if tt.expectError && err == nil {
|
||||
t.Errorf("期望错误但没有得到")
|
||||
}
|
||||
if !tt.expectError && err != nil {
|
||||
t.Errorf("期望无错误但得到: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetDefaultConfig(t *testing.T) {
|
||||
config := getDefaultConfig()
|
||||
|
||||
if config == nil {
|
||||
t.Fatal("getDefaultConfig() 返回 nil")
|
||||
}
|
||||
|
||||
// 验证默认值
|
||||
if config.ResourceID != "AUTO_MAA" {
|
||||
t.Errorf("期望 ResourceID 'AUTO_MAA',得到 %s", config.ResourceID)
|
||||
}
|
||||
if config.CurrentVersion != "v1.0.0" {
|
||||
t.Errorf("期望 CurrentVersion 'v1.0.0',得到 %s", config.CurrentVersion)
|
||||
}
|
||||
if config.UserAgent != "AUTO_MAA_Go_Updater/1.0" {
|
||||
t.Errorf("期望 UserAgent 'AUTO_MAA_Go_Updater/1.0',得到 %s", config.UserAgent)
|
||||
}
|
||||
if config.LogLevel != "info" {
|
||||
t.Errorf("期望 LogLevel 'info',得到 %s", config.LogLevel)
|
||||
}
|
||||
if config.CheckInterval != 3600 {
|
||||
t.Errorf("期望 CheckInterval 3600,得到 %d", config.CheckInterval)
|
||||
}
|
||||
if !config.AutoCheck {
|
||||
t.Errorf("期望 AutoCheck true,得到 %v", config.AutoCheck)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetConfigDir(t *testing.T) {
|
||||
// 保存原始 APPDATA
|
||||
originalAppData := os.Getenv("APPDATA")
|
||||
defer os.Setenv("APPDATA", originalAppData)
|
||||
|
||||
// 测试设置了 APPDATA
|
||||
os.Setenv("APPDATA", "C:\\Users\\Test\\AppData\\Roaming")
|
||||
dir := getConfigDir()
|
||||
expected := "C:\\Users\\Test\\AppData\\Roaming\\AUTO_MAA_Go_Updater"
|
||||
if dir != expected {
|
||||
t.Errorf("期望 %s,得到 %s", expected, dir)
|
||||
}
|
||||
|
||||
// 测试没有 APPDATA
|
||||
os.Unsetenv("APPDATA")
|
||||
dir = getConfigDir()
|
||||
if dir != "." {
|
||||
t.Errorf("期望当前目录,得到 %s", dir)
|
||||
}
|
||||
}
|
||||
224
Go_Updater/download/manager.go
Normal file
@@ -0,0 +1,224 @@
|
||||
package download
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
// DownloadProgress 表示当前下载进度
|
||||
type DownloadProgress struct {
|
||||
BytesDownloaded int64
|
||||
TotalBytes int64
|
||||
Percentage float64
|
||||
Speed int64 // 每秒字节数
|
||||
}
|
||||
|
||||
// ProgressCallback 在下载过程中调用以报告进度
|
||||
type ProgressCallback func(DownloadProgress)
|
||||
|
||||
// DownloadManager 定义下载操作的接口
|
||||
type DownloadManager interface {
|
||||
Download(url, destination string, progressCallback ProgressCallback) error
|
||||
DownloadWithResume(url, destination string, progressCallback ProgressCallback) error
|
||||
ValidateChecksum(filePath, expectedChecksum string) error
|
||||
SetTimeout(timeout time.Duration)
|
||||
}
|
||||
|
||||
// Manager 实现 DownloadManager 接口
|
||||
type Manager struct {
|
||||
client *http.Client
|
||||
timeout time.Duration
|
||||
}
|
||||
|
||||
// NewManager 创建新的下载管理器
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
client: &http.Client{
|
||||
Timeout: 30 * time.Second,
|
||||
},
|
||||
timeout: 30 * time.Second,
|
||||
}
|
||||
}
|
||||
|
||||
// Download 从给定 URL 下载文件到目标路径
|
||||
func (m *Manager) Download(url, destination string, progressCallback ProgressCallback) error {
|
||||
return m.downloadWithContext(context.Background(), url, destination, progressCallback, false)
|
||||
}
|
||||
|
||||
// DownloadWithResume 下载文件并支持断点续传
|
||||
func (m *Manager) DownloadWithResume(url, destination string, progressCallback ProgressCallback) error {
|
||||
return m.downloadWithContext(context.Background(), url, destination, progressCallback, true)
|
||||
}
|
||||
|
||||
// downloadWithContext 执行实际的下载并支持上下文
|
||||
func (m *Manager) downloadWithContext(ctx context.Context, url, destination string, progressCallback ProgressCallback, resume bool) error {
|
||||
// 如果目标目录不存在则创建
|
||||
if err := os.MkdirAll(filepath.Dir(destination), 0755); err != nil {
|
||||
return fmt.Errorf("创建目标目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 检查文件是否存在以支持断点续传
|
||||
var existingSize int64
|
||||
if resume {
|
||||
if stat, err := os.Stat(destination); err == nil {
|
||||
existingSize = stat.Size()
|
||||
}
|
||||
}
|
||||
|
||||
// 创建 HTTP 请求
|
||||
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建请求失败: %w", err)
|
||||
}
|
||||
|
||||
// 为断点续传添加范围头
|
||||
if resume && existingSize > 0 {
|
||||
req.Header.Set("Range", fmt.Sprintf("bytes=%d-", existingSize))
|
||||
}
|
||||
|
||||
// 执行请求
|
||||
resp, err := m.client.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("执行请求失败: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
// 检查响应状态
|
||||
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusPartialContent {
|
||||
return fmt.Errorf("意外的状态码: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
// 获取总大小
|
||||
totalSize := existingSize
|
||||
if contentLength := resp.Header.Get("Content-Length"); contentLength != "" {
|
||||
if size, err := strconv.ParseInt(contentLength, 10, 64); err == nil {
|
||||
totalSize += size
|
||||
}
|
||||
}
|
||||
|
||||
// 打开目标文件
|
||||
var file *os.File
|
||||
if resume && existingSize > 0 {
|
||||
file, err = os.OpenFile(destination, os.O_WRONLY|os.O_APPEND, 0644)
|
||||
} else {
|
||||
file, err = os.Create(destination)
|
||||
existingSize = 0
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 下载并跟踪进度
|
||||
return m.copyWithProgress(resp.Body, file, existingSize, totalSize, progressCallback)
|
||||
}
|
||||
|
||||
// copyWithProgress 复制数据并跟踪进度
|
||||
func (m *Manager) copyWithProgress(src io.Reader, dst io.Writer, startBytes, totalBytes int64, progressCallback ProgressCallback) error {
|
||||
buffer := make([]byte, 32*1024) // 32KB 缓冲区
|
||||
downloaded := startBytes
|
||||
startTime := time.Now()
|
||||
lastUpdate := startTime
|
||||
|
||||
for {
|
||||
n, err := src.Read(buffer)
|
||||
if n > 0 {
|
||||
if _, writeErr := dst.Write(buffer[:n]); writeErr != nil {
|
||||
return fmt.Errorf("写入目标失败: %w", writeErr)
|
||||
}
|
||||
downloaded += int64(n)
|
||||
|
||||
// 每 100ms 更新一次进度
|
||||
now := time.Now()
|
||||
if progressCallback != nil && now.Sub(lastUpdate) >= 100*time.Millisecond {
|
||||
elapsed := now.Sub(startTime).Seconds()
|
||||
speed := int64(0)
|
||||
if elapsed > 0 {
|
||||
speed = int64(float64(downloaded-startBytes) / elapsed)
|
||||
}
|
||||
|
||||
percentage := float64(0)
|
||||
if totalBytes > 0 {
|
||||
percentage = float64(downloaded) / float64(totalBytes) * 100
|
||||
}
|
||||
|
||||
progressCallback(DownloadProgress{
|
||||
BytesDownloaded: downloaded,
|
||||
TotalBytes: totalBytes,
|
||||
Percentage: percentage,
|
||||
Speed: speed,
|
||||
})
|
||||
lastUpdate = now
|
||||
}
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("从源读取失败: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 最终进度更新
|
||||
if progressCallback != nil {
|
||||
elapsed := time.Since(startTime).Seconds()
|
||||
speed := int64(0)
|
||||
if elapsed > 0 {
|
||||
speed = int64(float64(downloaded-startBytes) / elapsed)
|
||||
}
|
||||
|
||||
percentage := float64(100)
|
||||
if totalBytes > 0 {
|
||||
percentage = float64(downloaded) / float64(totalBytes) * 100
|
||||
}
|
||||
|
||||
progressCallback(DownloadProgress{
|
||||
BytesDownloaded: downloaded,
|
||||
TotalBytes: totalBytes,
|
||||
Percentage: percentage,
|
||||
Speed: speed,
|
||||
})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateChecksum 验证文件的 SHA256 校验和
|
||||
func (m *Manager) ValidateChecksum(filePath, expectedChecksum string) error {
|
||||
if expectedChecksum == "" {
|
||||
return nil // 没有校验和需要验证
|
||||
}
|
||||
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开文件进行校验和验证失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return fmt.Errorf("计算校验和失败: %w", err)
|
||||
}
|
||||
|
||||
actualChecksum := hex.EncodeToString(hash.Sum(nil))
|
||||
if actualChecksum != expectedChecksum {
|
||||
return fmt.Errorf("校验和不匹配: 期望 %s,得到 %s", expectedChecksum, actualChecksum)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetTimeout 设置下载操作的超时时间
|
||||
func (m *Manager) SetTimeout(timeout time.Duration) {
|
||||
m.timeout = timeout
|
||||
m.client.Timeout = timeout
|
||||
}
|
||||
1392
Go_Updater/download/manager_test.go
Normal file
219
Go_Updater/errors/errors.go
Normal file
@@ -0,0 +1,219 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrorType 定义错误类型枚举
|
||||
type ErrorType int
|
||||
|
||||
const (
|
||||
NetworkError ErrorType = iota
|
||||
APIError
|
||||
FileError
|
||||
ConfigError
|
||||
InstallError
|
||||
)
|
||||
|
||||
// String 返回错误类型的字符串表示
|
||||
func (et ErrorType) String() string {
|
||||
switch et {
|
||||
case NetworkError:
|
||||
return "NetworkError"
|
||||
case APIError:
|
||||
return "APIError"
|
||||
case FileError:
|
||||
return "FileError"
|
||||
case ConfigError:
|
||||
return "ConfigError"
|
||||
case InstallError:
|
||||
return "InstallError"
|
||||
default:
|
||||
return "UnknownError"
|
||||
}
|
||||
}
|
||||
|
||||
// UpdaterError 统一的错误结构体
|
||||
type UpdaterError struct {
|
||||
Type ErrorType
|
||||
Message string
|
||||
Cause error
|
||||
Timestamp time.Time
|
||||
Context map[string]interface{}
|
||||
}
|
||||
|
||||
// Error 实现error接口
|
||||
func (ue *UpdaterError) Error() string {
|
||||
if ue.Cause != nil {
|
||||
return fmt.Sprintf("[%s] %s: %v", ue.Type, ue.Message, ue.Cause)
|
||||
}
|
||||
return fmt.Sprintf("[%s] %s", ue.Type, ue.Message)
|
||||
}
|
||||
|
||||
// Unwrap 支持错误链
|
||||
func (ue *UpdaterError) Unwrap() error {
|
||||
return ue.Cause
|
||||
}
|
||||
|
||||
// NewUpdaterError 创建新的UpdaterError
|
||||
func NewUpdaterError(errorType ErrorType, message string, cause error) *UpdaterError {
|
||||
return &UpdaterError{
|
||||
Type: errorType,
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
Timestamp: time.Now(),
|
||||
Context: make(map[string]interface{}),
|
||||
}
|
||||
}
|
||||
|
||||
// WithContext 添加上下文信息
|
||||
func (ue *UpdaterError) WithContext(key string, value interface{}) *UpdaterError {
|
||||
ue.Context[key] = value
|
||||
return ue
|
||||
}
|
||||
|
||||
// GetUserFriendlyMessage 获取用户友好的错误消息
|
||||
func (ue *UpdaterError) GetUserFriendlyMessage() string {
|
||||
switch ue.Type {
|
||||
case NetworkError:
|
||||
return "网络连接失败,请检查网络连接后重试"
|
||||
case APIError:
|
||||
return "服务器响应异常,请稍后重试或联系技术支持"
|
||||
case FileError:
|
||||
return "文件操作失败,请检查文件权限和磁盘空间"
|
||||
case ConfigError:
|
||||
return "配置文件错误,程序将使用默认配置"
|
||||
case InstallError:
|
||||
return "安装过程中出现错误,程序将尝试回滚更改"
|
||||
default:
|
||||
return "发生未知错误,请联系技术支持"
|
||||
}
|
||||
}
|
||||
|
||||
// RetryConfig 重试配置
|
||||
type RetryConfig struct {
|
||||
MaxRetries int
|
||||
InitialDelay time.Duration
|
||||
MaxDelay time.Duration
|
||||
BackoffFactor float64
|
||||
RetryableErrors []ErrorType
|
||||
}
|
||||
|
||||
// DefaultRetryConfig 默认重试配置
|
||||
func DefaultRetryConfig() *RetryConfig {
|
||||
return &RetryConfig{
|
||||
MaxRetries: 3,
|
||||
InitialDelay: time.Second,
|
||||
MaxDelay: 30 * time.Second,
|
||||
BackoffFactor: 2.0,
|
||||
RetryableErrors: []ErrorType{NetworkError, APIError},
|
||||
}
|
||||
}
|
||||
|
||||
// IsRetryable 检查错误是否可重试
|
||||
func (rc *RetryConfig) IsRetryable(err error) bool {
|
||||
if ue, ok := err.(*UpdaterError); ok {
|
||||
for _, retryableType := range rc.RetryableErrors {
|
||||
if ue.Type == retryableType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// CalculateDelay 计算重试延迟时间
|
||||
func (rc *RetryConfig) CalculateDelay(attempt int) time.Duration {
|
||||
delay := time.Duration(float64(rc.InitialDelay) * pow(rc.BackoffFactor, float64(attempt)))
|
||||
if delay > rc.MaxDelay {
|
||||
delay = rc.MaxDelay
|
||||
}
|
||||
return delay
|
||||
}
|
||||
|
||||
// pow 简单的幂运算实现
|
||||
func pow(base, exp float64) float64 {
|
||||
result := 1.0
|
||||
for i := 0; i < int(exp); i++ {
|
||||
result *= base
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// RetryableOperation 可重试的操作函数类型
|
||||
type RetryableOperation func() error
|
||||
|
||||
// ExecuteWithRetry 执行带重试的操作
|
||||
func ExecuteWithRetry(operation RetryableOperation, config *RetryConfig) error {
|
||||
var lastErr error
|
||||
|
||||
for attempt := 0; attempt <= config.MaxRetries; attempt++ {
|
||||
err := operation()
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
|
||||
// 如果不是可重试的错误,直接返回
|
||||
if !config.IsRetryable(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果已经是最后一次尝试,不再等待
|
||||
if attempt == config.MaxRetries {
|
||||
break
|
||||
}
|
||||
|
||||
// 计算延迟时间并等待
|
||||
delay := config.CalculateDelay(attempt)
|
||||
time.Sleep(delay)
|
||||
}
|
||||
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// ErrorHandler 错误处理器接口
|
||||
type ErrorHandler interface {
|
||||
HandleError(err error) error
|
||||
ShouldRetry(err error) bool
|
||||
GetUserMessage(err error) string
|
||||
}
|
||||
|
||||
// DefaultErrorHandler 默认错误处理器
|
||||
type DefaultErrorHandler struct {
|
||||
retryConfig *RetryConfig
|
||||
}
|
||||
|
||||
// NewDefaultErrorHandler 创建默认错误处理器
|
||||
func NewDefaultErrorHandler() *DefaultErrorHandler {
|
||||
return &DefaultErrorHandler{
|
||||
retryConfig: DefaultRetryConfig(),
|
||||
}
|
||||
}
|
||||
|
||||
// HandleError 处理错误
|
||||
func (h *DefaultErrorHandler) HandleError(err error) error {
|
||||
if ue, ok := err.(*UpdaterError); ok {
|
||||
// 记录错误上下文
|
||||
ue.WithContext("handled_at", time.Now())
|
||||
return ue
|
||||
}
|
||||
|
||||
// 将普通错误包装为UpdaterError
|
||||
return NewUpdaterError(NetworkError, "未分类错误", err)
|
||||
}
|
||||
|
||||
// ShouldRetry 判断是否应该重试
|
||||
func (h *DefaultErrorHandler) ShouldRetry(err error) bool {
|
||||
return h.retryConfig.IsRetryable(err)
|
||||
}
|
||||
|
||||
// GetUserMessage 获取用户友好的错误消息
|
||||
func (h *DefaultErrorHandler) GetUserMessage(err error) string {
|
||||
if ue, ok := err.(*UpdaterError); ok {
|
||||
return ue.GetUserFriendlyMessage()
|
||||
}
|
||||
return "发生未知错误,请联系技术支持"
|
||||
}
|
||||
287
Go_Updater/errors/errors_test.go
Normal file
@@ -0,0 +1,287 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestUpdaterError_Error(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
err *UpdaterError
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "error with cause",
|
||||
err: &UpdaterError{
|
||||
Type: NetworkError,
|
||||
Message: "connection failed",
|
||||
Cause: fmt.Errorf("timeout"),
|
||||
},
|
||||
expected: "[NetworkError] connection failed: timeout",
|
||||
},
|
||||
{
|
||||
name: "error without cause",
|
||||
err: &UpdaterError{
|
||||
Type: APIError,
|
||||
Message: "invalid response",
|
||||
Cause: nil,
|
||||
},
|
||||
expected: "[APIError] invalid response",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.err.Error(); got != tt.expected {
|
||||
t.Errorf("UpdaterError.Error() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewUpdaterError(t *testing.T) {
|
||||
cause := fmt.Errorf("original error")
|
||||
err := NewUpdaterError(FileError, "test message", cause)
|
||||
|
||||
if err.Type != FileError {
|
||||
t.Errorf("Expected type %v, got %v", FileError, err.Type)
|
||||
}
|
||||
if err.Message != "test message" {
|
||||
t.Errorf("Expected message 'test message', got '%v'", err.Message)
|
||||
}
|
||||
if err.Cause != cause {
|
||||
t.Errorf("Expected cause %v, got %v", cause, err.Cause)
|
||||
}
|
||||
if err.Context == nil {
|
||||
t.Error("Expected context to be initialized")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdaterError_WithContext(t *testing.T) {
|
||||
err := NewUpdaterError(ConfigError, "test", nil)
|
||||
err.WithContext("key1", "value1").WithContext("key2", 42)
|
||||
|
||||
if err.Context["key1"] != "value1" {
|
||||
t.Errorf("Expected context key1 to be 'value1', got %v", err.Context["key1"])
|
||||
}
|
||||
if err.Context["key2"] != 42 {
|
||||
t.Errorf("Expected context key2 to be 42, got %v", err.Context["key2"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdaterError_GetUserFriendlyMessage(t *testing.T) {
|
||||
tests := []struct {
|
||||
errorType ErrorType
|
||||
expected string
|
||||
}{
|
||||
{NetworkError, "网络连接失败,请检查网络连接后重试"},
|
||||
{APIError, "服务器响应异常,请稍后重试或联系技术支持"},
|
||||
{FileError, "文件操作失败,请检查文件权限和磁盘空间"},
|
||||
{ConfigError, "配置文件错误,程序将使用默认配置"},
|
||||
{InstallError, "安装过程中出现错误,程序将尝试回滚更改"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.errorType.String(), func(t *testing.T) {
|
||||
err := NewUpdaterError(tt.errorType, "test", nil)
|
||||
if got := err.GetUserFriendlyMessage(); got != tt.expected {
|
||||
t.Errorf("GetUserFriendlyMessage() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryConfig_IsRetryable(t *testing.T) {
|
||||
config := DefaultRetryConfig()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "retryable network error",
|
||||
err: NewUpdaterError(NetworkError, "test", nil),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "retryable api error",
|
||||
err: NewUpdaterError(APIError, "test", nil),
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "non-retryable file error",
|
||||
err: NewUpdaterError(FileError, "test", nil),
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "regular error",
|
||||
err: fmt.Errorf("regular error"),
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := config.IsRetryable(tt.err); got != tt.expected {
|
||||
t.Errorf("IsRetryable() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryConfig_CalculateDelay(t *testing.T) {
|
||||
config := DefaultRetryConfig()
|
||||
|
||||
tests := []struct {
|
||||
attempt int
|
||||
expected time.Duration
|
||||
}{
|
||||
{0, time.Second},
|
||||
{1, 2 * time.Second},
|
||||
{2, 4 * time.Second},
|
||||
{10, 30 * time.Second}, // should be capped at MaxDelay
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(fmt.Sprintf("attempt_%d", tt.attempt), func(t *testing.T) {
|
||||
if got := config.CalculateDelay(tt.attempt); got != tt.expected {
|
||||
t.Errorf("CalculateDelay(%d) = %v, want %v", tt.attempt, got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecuteWithRetry(t *testing.T) {
|
||||
config := DefaultRetryConfig()
|
||||
config.InitialDelay = time.Millisecond // 加快测试速度
|
||||
|
||||
t.Run("success on first try", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
return nil
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("Expected 1 attempt, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("success after retries", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
if attempts < 3 {
|
||||
return NewUpdaterError(NetworkError, "temporary failure", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error, got %v", err)
|
||||
}
|
||||
if attempts != 3 {
|
||||
t.Errorf("Expected 3 attempts, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-retryable error", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
return NewUpdaterError(FileError, "file not found", nil)
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Errorf("Expected 1 attempt, got %d", attempts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("max retries exceeded", func(t *testing.T) {
|
||||
attempts := 0
|
||||
operation := func() error {
|
||||
attempts++
|
||||
return NewUpdaterError(NetworkError, "persistent failure", nil)
|
||||
}
|
||||
|
||||
err := ExecuteWithRetry(operation, config)
|
||||
if err == nil {
|
||||
t.Error("Expected error, got nil")
|
||||
}
|
||||
expectedAttempts := config.MaxRetries + 1
|
||||
if attempts != expectedAttempts {
|
||||
t.Errorf("Expected %d attempts, got %d", expectedAttempts, attempts)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDefaultErrorHandler(t *testing.T) {
|
||||
handler := NewDefaultErrorHandler()
|
||||
|
||||
t.Run("handle updater error", func(t *testing.T) {
|
||||
originalErr := NewUpdaterError(NetworkError, "test", nil)
|
||||
handledErr := handler.HandleError(originalErr)
|
||||
|
||||
if handledErr != originalErr {
|
||||
t.Error("Expected same error instance")
|
||||
}
|
||||
if originalErr.Context["handled_at"] == nil {
|
||||
t.Error("Expected handled_at context to be set")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("handle regular error", func(t *testing.T) {
|
||||
originalErr := fmt.Errorf("regular error")
|
||||
handledErr := handler.HandleError(originalErr)
|
||||
|
||||
if ue, ok := handledErr.(*UpdaterError); ok {
|
||||
if ue.Type != NetworkError {
|
||||
t.Errorf("Expected NetworkError, got %v", ue.Type)
|
||||
}
|
||||
if ue.Cause != originalErr {
|
||||
t.Error("Expected original error as cause")
|
||||
}
|
||||
} else {
|
||||
t.Error("Expected UpdaterError")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("should retry", func(t *testing.T) {
|
||||
retryableErr := NewUpdaterError(NetworkError, "test", nil)
|
||||
nonRetryableErr := NewUpdaterError(FileError, "test", nil)
|
||||
|
||||
if !handler.ShouldRetry(retryableErr) {
|
||||
t.Error("Expected network error to be retryable")
|
||||
}
|
||||
if handler.ShouldRetry(nonRetryableErr) {
|
||||
t.Error("Expected file error to not be retryable")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("get user message", func(t *testing.T) {
|
||||
updaterErr := NewUpdaterError(NetworkError, "test", nil)
|
||||
regularErr := fmt.Errorf("regular error")
|
||||
|
||||
userMsg1 := handler.GetUserMessage(updaterErr)
|
||||
userMsg2 := handler.GetUserMessage(regularErr)
|
||||
|
||||
if userMsg1 != "网络连接失败,请检查网络连接后重试" {
|
||||
t.Errorf("Unexpected user message: %s", userMsg1)
|
||||
}
|
||||
if userMsg2 != "发生未知错误,请联系技术支持" {
|
||||
t.Errorf("Unexpected user message: %s", userMsg2)
|
||||
}
|
||||
})
|
||||
}
|
||||
42
Go_Updater/go.mod
Normal file
@@ -0,0 +1,42 @@
|
||||
module AUTO_MAA_Go_Updater
|
||||
|
||||
go 1.24.5
|
||||
|
||||
require (
|
||||
fyne.io/fyne/v2 v2.6.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
require (
|
||||
fyne.io/systray v1.11.0 // indirect
|
||||
github.com/BurntSushi/toml v1.4.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/fredbi/uri v1.1.0 // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.1.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.2.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.1.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-text/render v0.2.0 // indirect
|
||||
github.com/go-text/typesetting v0.2.1 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rymdport/portal v0.4.1 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/stretchr/testify v1.10.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
80
Go_Updater/go.sum
Normal file
@@ -0,0 +1,80 @@
|
||||
fyne.io/fyne/v2 v2.6.1 h1:kjPJD4/rBS9m2nHJp+npPSuaK79yj6ObMTuzR6VQ1Is=
|
||||
fyne.io/fyne/v2 v2.6.1/go.mod h1:YZt7SksjvrSNJCwbWFV32WON3mE1Sr7L41D29qMZ/lU=
|
||||
fyne.io/systray v1.11.0 h1:D9HISlxSkx+jHSniMBR6fCFOUjk1x/OOOJLa9lJYAKg=
|
||||
fyne.io/systray v1.11.0/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
|
||||
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/fredbi/uri v1.1.0 h1:OqLpTXtyRg9ABReqvDGdJPqZUxs8cyBDOMXBbskCaB8=
|
||||
github.com/fredbi/uri v1.1.0/go.mod h1:aYTUoAXBOq7BLfVJ8GnKmfcuURosB1xyHDIfWeC/iW4=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/fyne-io/gl-js v0.1.0 h1:8luJzNs0ntEAJo+8x8kfUOXujUlP8gB3QMOxO2mUdpM=
|
||||
github.com/fyne-io/gl-js v0.1.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.2.0 h1:8GUZtN2aCoTPNqgRDxK5+kn9OURINhBEBc7M4O1KrmM=
|
||||
github.com/fyne-io/glfw-js v0.2.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.1.0 h1:7EUKk3HV3Y2E+qypp3nWqMXD7mum0hCw2KEGhI1fnBw=
|
||||
github.com/fyne-io/oksvg v0.1.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-text/render v0.2.0 h1:LBYoTmp5jYiJ4NPqDc2pz17MLmA3wHw1dZSVGcOdeAc=
|
||||
github.com/go-text/render v0.2.0/go.mod h1:CkiqfukRGKJA5vZZISkjSYrcdtgKQWRa2HIzvwNN5SU=
|
||||
github.com/go-text/typesetting v0.2.1 h1:x0jMOGyO3d1qFAPI0j4GSsh7M0Q3Ypjzr4+CEVg82V8=
|
||||
github.com/go-text/typesetting v0.2.1/go.mod h1:mTOxEwasOFpAMBjEQDhdWRckoLLeI/+qrQeBCTGEt6M=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066 h1:qCuYC+94v2xrb1PoS4NIDe7DGYtLnU2wWiQe9a1B1c0=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20241103174707-87a29e9e6066/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd h1:1FjCyPC+syAzJ5/2S8fqdZK1R22vvA0J7JZKcuOIQ7Y=
|
||||
github.com/google/pprof v0.0.0-20211214055906-6f57359322fd/go.mod h1:KgnwoLYCZ8IQu3XUZ8Nc/bM9CCZFOyjUNOSygVozoDg=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08 h1:wMeVzrPO3mfHIWLZtDcSaGAe2I4PW9B/P5nMkRSwCAc=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20241217141322-fcc2cadd6f08/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
|
||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rymdport/portal v0.4.1 h1:2dnZhjf5uEaeDjeF/yBIeeRo6pNI2QAKm7kq1w/kbnA=
|
||||
github.com/rymdport/portal v0.4.1/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
|
||||
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
513
Go_Updater/gui/manager.go
Normal file
@@ -0,0 +1,513 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
)
|
||||
|
||||
// UpdateStatus 表示更新过程的当前状态
|
||||
type UpdateStatus int
|
||||
|
||||
const (
|
||||
StatusChecking UpdateStatus = iota
|
||||
StatusUpdateAvailable
|
||||
StatusDownloading
|
||||
StatusInstalling
|
||||
StatusCompleted
|
||||
StatusError
|
||||
)
|
||||
|
||||
// Config 表示 GUI 的配置结构
|
||||
type Config struct {
|
||||
ResourceID string
|
||||
CurrentVersion string
|
||||
UserAgent string
|
||||
BackupURL string
|
||||
}
|
||||
|
||||
// GUIManager 定义 GUI 管理的接口方法
|
||||
type GUIManager interface {
|
||||
ShowMainWindow()
|
||||
UpdateStatus(status UpdateStatus, message string)
|
||||
ShowProgress(percentage float64)
|
||||
ShowError(errorMsg string)
|
||||
ShowConfigDialog() (*Config, error)
|
||||
Close()
|
||||
}
|
||||
|
||||
// Manager 实现 GUIManager 接口
|
||||
type Manager struct {
|
||||
app fyne.App
|
||||
window fyne.Window
|
||||
statusLabel *widget.Label
|
||||
progressBar *widget.ProgressBar
|
||||
actionButton *widget.Button
|
||||
versionLabel *widget.Label
|
||||
releaseNotes *widget.RichText
|
||||
currentStatus UpdateStatus
|
||||
onCheckUpdate func()
|
||||
onCancel func()
|
||||
}
|
||||
|
||||
// NewManager creates a new GUI manager instance
|
||||
func NewManager() *Manager {
|
||||
a := app.New()
|
||||
a.SetIcon(theme.ComputerIcon())
|
||||
|
||||
w := a.NewWindow("AUTO_MAA_Go_Updater")
|
||||
w.Resize(fyne.NewSize(500, 400))
|
||||
w.SetFixedSize(false)
|
||||
w.CenterOnScreen()
|
||||
|
||||
return &Manager{
|
||||
app: a,
|
||||
window: w,
|
||||
}
|
||||
}
|
||||
|
||||
// SetCallbacks sets the callback functions for user actions
|
||||
func (m *Manager) SetCallbacks(onCheckUpdate, onCancel func()) {
|
||||
m.onCheckUpdate = onCheckUpdate
|
||||
m.onCancel = onCancel
|
||||
}
|
||||
|
||||
// ShowMainWindow displays the main application window
|
||||
func (m *Manager) ShowMainWindow() {
|
||||
// Create UI components
|
||||
m.createUIComponents()
|
||||
|
||||
// Create main layout
|
||||
content := m.createMainLayout()
|
||||
|
||||
m.window.SetContent(content)
|
||||
m.window.ShowAndRun()
|
||||
}
|
||||
|
||||
// createUIComponents initializes all UI components
|
||||
func (m *Manager) createUIComponents() {
|
||||
// Status label
|
||||
m.statusLabel = widget.NewLabel("准备检查更新...")
|
||||
m.statusLabel.Alignment = fyne.TextAlignCenter
|
||||
|
||||
// Progress bar
|
||||
m.progressBar = widget.NewProgressBar()
|
||||
m.progressBar.Hide()
|
||||
|
||||
// Version label
|
||||
m.versionLabel = widget.NewLabel("当前版本: 未知")
|
||||
m.versionLabel.TextStyle = fyne.TextStyle{Italic: true}
|
||||
|
||||
// Release notes
|
||||
m.releaseNotes = widget.NewRichText()
|
||||
m.releaseNotes.Hide()
|
||||
|
||||
// Action button
|
||||
m.actionButton = widget.NewButton("检查更新", func() {
|
||||
if m.onCheckUpdate != nil {
|
||||
m.onCheckUpdate()
|
||||
}
|
||||
})
|
||||
m.actionButton.Importance = widget.HighImportance
|
||||
}
|
||||
|
||||
// createMainLayout creates the main window layout
|
||||
func (m *Manager) createMainLayout() *fyne.Container {
|
||||
// Header section
|
||||
header := container.NewVBox(
|
||||
widget.NewCard("", "", container.NewVBox(
|
||||
widget.NewLabelWithStyle("AUTO_MAA_Go_Updater", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||
m.versionLabel,
|
||||
)),
|
||||
)
|
||||
|
||||
// Status section
|
||||
statusSection := container.NewVBox(
|
||||
m.statusLabel,
|
||||
m.progressBar,
|
||||
)
|
||||
|
||||
// Release notes section
|
||||
releaseNotesCard := widget.NewCard("更新日志", "", container.NewScroll(m.releaseNotes))
|
||||
releaseNotesCard.Hide()
|
||||
|
||||
// Button section
|
||||
buttonSection := container.NewHBox(
|
||||
widget.NewButton("配置", func() {
|
||||
m.showConfigDialog()
|
||||
}),
|
||||
widget.NewSpacer(),
|
||||
m.actionButton,
|
||||
)
|
||||
|
||||
// Main layout
|
||||
return container.NewVBox(
|
||||
header,
|
||||
widget.NewSeparator(),
|
||||
statusSection,
|
||||
releaseNotesCard,
|
||||
widget.NewSeparator(),
|
||||
buttonSection,
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateStatus updates the current status and UI accordingly
|
||||
func (m *Manager) UpdateStatus(status UpdateStatus, message string) {
|
||||
m.currentStatus = status
|
||||
m.statusLabel.SetText(message)
|
||||
|
||||
switch status {
|
||||
case StatusChecking:
|
||||
m.actionButton.SetText("检查中...")
|
||||
m.actionButton.Disable()
|
||||
m.progressBar.Hide()
|
||||
|
||||
case StatusUpdateAvailable:
|
||||
m.actionButton.SetText("开始更新")
|
||||
m.actionButton.Enable()
|
||||
m.progressBar.Hide()
|
||||
|
||||
case StatusDownloading:
|
||||
m.actionButton.SetText("下载中...")
|
||||
m.actionButton.Disable()
|
||||
m.progressBar.Show()
|
||||
|
||||
case StatusInstalling:
|
||||
m.actionButton.SetText("安装中...")
|
||||
m.actionButton.Disable()
|
||||
m.progressBar.Show()
|
||||
|
||||
case StatusCompleted:
|
||||
m.actionButton.SetText("完成")
|
||||
m.actionButton.Enable()
|
||||
m.progressBar.Hide()
|
||||
|
||||
case StatusError:
|
||||
m.actionButton.SetText("重试")
|
||||
m.actionButton.Enable()
|
||||
m.progressBar.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
// ShowProgress updates the progress bar
|
||||
func (m *Manager) ShowProgress(percentage float64) {
|
||||
if percentage < 0 {
|
||||
percentage = 0
|
||||
}
|
||||
if percentage > 100 {
|
||||
percentage = 100
|
||||
}
|
||||
|
||||
m.progressBar.SetValue(percentage / 100.0)
|
||||
m.progressBar.Show()
|
||||
}
|
||||
|
||||
// ShowError displays an error dialog
|
||||
func (m *Manager) ShowError(errorMsg string) {
|
||||
dialog.ShowError(fmt.Errorf(errorMsg), m.window)
|
||||
}
|
||||
|
||||
// ShowConfigDialog displays the configuration dialog
|
||||
func (m *Manager) ShowConfigDialog() (*Config, error) {
|
||||
return m.showConfigDialog()
|
||||
}
|
||||
|
||||
// showConfigDialog creates and shows the configuration dialog
|
||||
func (m *Manager) showConfigDialog() (*Config, error) {
|
||||
// Create form entries
|
||||
resourceIDEntry := widget.NewEntry()
|
||||
resourceIDEntry.SetPlaceHolder("例如: M9A")
|
||||
|
||||
versionEntry := widget.NewEntry()
|
||||
versionEntry.SetPlaceHolder("例如: v1.0.0")
|
||||
|
||||
userAgentEntry := widget.NewEntry()
|
||||
userAgentEntry.SetText("AUTO_MAA_Go_Updater/1.0")
|
||||
|
||||
backupURLEntry := widget.NewEntry()
|
||||
backupURLEntry.SetPlaceHolder("备用下载地址(可选)")
|
||||
|
||||
// Create form
|
||||
form := &widget.Form{
|
||||
Items: []*widget.FormItem{
|
||||
{Text: "资源ID:", Widget: resourceIDEntry},
|
||||
{Text: "当前版本:", Widget: versionEntry},
|
||||
{Text: "用户代理:", Widget: userAgentEntry},
|
||||
{Text: "备用下载地址:", Widget: backupURLEntry},
|
||||
},
|
||||
}
|
||||
|
||||
// Create result channel
|
||||
resultChan := make(chan *Config, 1)
|
||||
errorChan := make(chan error, 1)
|
||||
|
||||
// Create dialog
|
||||
configDialog := dialog.NewCustomConfirm(
|
||||
"配置设置",
|
||||
"保存",
|
||||
"取消",
|
||||
form,
|
||||
func(confirmed bool) {
|
||||
if confirmed {
|
||||
config := &Config{
|
||||
ResourceID: resourceIDEntry.Text,
|
||||
CurrentVersion: versionEntry.Text,
|
||||
UserAgent: userAgentEntry.Text,
|
||||
BackupURL: backupURLEntry.Text,
|
||||
}
|
||||
|
||||
// Basic validation
|
||||
if config.ResourceID == "" {
|
||||
errorChan <- fmt.Errorf("资源ID不能为空")
|
||||
return
|
||||
}
|
||||
if config.CurrentVersion == "" {
|
||||
errorChan <- fmt.Errorf("当前版本不能为空")
|
||||
return
|
||||
}
|
||||
|
||||
resultChan <- config
|
||||
} else {
|
||||
errorChan <- fmt.Errorf("用户取消了配置")
|
||||
}
|
||||
},
|
||||
m.window,
|
||||
)
|
||||
|
||||
// Add help text
|
||||
helpText := widget.NewRichTextFromMarkdown(`
|
||||
**配置说明:**
|
||||
- **资源ID**: Mirror酱服务中的资源标识符
|
||||
- **当前版本**: 当前软件的版本号
|
||||
- **用户代理**: HTTP请求的用户代理字符串
|
||||
- **备用下载地址**: 当Mirror酱服务不可用时的备用下载地址
|
||||
`)
|
||||
|
||||
// Create container with help text
|
||||
dialogContent := container.NewVBox(
|
||||
form,
|
||||
widget.NewSeparator(),
|
||||
helpText,
|
||||
)
|
||||
|
||||
configDialog.SetContent(dialogContent)
|
||||
configDialog.Resize(fyne.NewSize(600, 500))
|
||||
configDialog.Show()
|
||||
|
||||
// Wait for result
|
||||
select {
|
||||
case config := <-resultChan:
|
||||
return config, nil
|
||||
case err := <-errorChan:
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// SetVersionInfo updates the version display
|
||||
func (m *Manager) SetVersionInfo(version string) {
|
||||
m.versionLabel.SetText(fmt.Sprintf("当前版本: %s", version))
|
||||
}
|
||||
|
||||
// ShowReleaseNotes displays the release notes
|
||||
func (m *Manager) ShowReleaseNotes(notes string) {
|
||||
if notes != "" {
|
||||
m.releaseNotes.ParseMarkdown(notes)
|
||||
// Find the release notes card and show it
|
||||
if parent := m.window.Content().(*container.VBox); parent != nil {
|
||||
for _, obj := range parent.Objects {
|
||||
if card, ok := obj.(*widget.Card); ok && card.Title == "更新日志" {
|
||||
card.Show()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateStatusWithDetails updates status with detailed information
|
||||
func (m *Manager) UpdateStatusWithDetails(status UpdateStatus, message string, details map[string]string) {
|
||||
m.UpdateStatus(status, message)
|
||||
|
||||
// Update version info if provided
|
||||
if version, ok := details["version"]; ok {
|
||||
m.SetVersionInfo(version)
|
||||
}
|
||||
|
||||
// Show release notes if provided
|
||||
if notes, ok := details["release_notes"]; ok {
|
||||
m.ShowReleaseNotes(notes)
|
||||
}
|
||||
|
||||
// Update progress if provided
|
||||
if progress, ok := details["progress"]; ok {
|
||||
if p, err := fmt.Sscanf(progress, "%f", new(float64)); err == nil && p == 1 {
|
||||
var progressValue float64
|
||||
fmt.Sscanf(progress, "%f", &progressValue)
|
||||
m.ShowProgress(progressValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ShowProgressWithSpeed shows progress with download speed information
|
||||
func (m *Manager) ShowProgressWithSpeed(percentage float64, speed int64, eta string) {
|
||||
m.ShowProgress(percentage)
|
||||
|
||||
// Update status with speed and ETA information
|
||||
speedText := m.formatSpeed(speed)
|
||||
statusText := fmt.Sprintf("下载中... %.1f%% (%s)", percentage, speedText)
|
||||
if eta != "" {
|
||||
statusText += fmt.Sprintf(" - 剩余时间: %s", eta)
|
||||
}
|
||||
|
||||
m.statusLabel.SetText(statusText)
|
||||
}
|
||||
|
||||
// formatSpeed formats the download speed for display
|
||||
func (m *Manager) formatSpeed(bytesPerSecond int64) string {
|
||||
if bytesPerSecond < 1024 {
|
||||
return fmt.Sprintf("%d B/s", bytesPerSecond)
|
||||
} else if bytesPerSecond < 1024*1024 {
|
||||
return fmt.Sprintf("%.1f KB/s", float64(bytesPerSecond)/1024)
|
||||
} else {
|
||||
return fmt.Sprintf("%.1f MB/s", float64(bytesPerSecond)/(1024*1024))
|
||||
}
|
||||
}
|
||||
|
||||
// ShowConfirmDialog shows a confirmation dialog
|
||||
func (m *Manager) ShowConfirmDialog(title, message string, callback func(bool)) {
|
||||
dialog.ShowConfirm(title, message, callback, m.window)
|
||||
}
|
||||
|
||||
// ShowInfoDialog shows an information dialog
|
||||
func (m *Manager) ShowInfoDialog(title, message string) {
|
||||
dialog.ShowInformation(title, message, m.window)
|
||||
}
|
||||
|
||||
// ShowUpdateAvailableDialog shows a dialog when update is available
|
||||
func (m *Manager) ShowUpdateAvailableDialog(currentVersion, newVersion, releaseNotes string, onConfirm func()) {
|
||||
content := container.NewVBox(
|
||||
widget.NewLabel(fmt.Sprintf("发现新版本: %s", newVersion)),
|
||||
widget.NewLabel(fmt.Sprintf("当前版本: %s", currentVersion)),
|
||||
widget.NewSeparator(),
|
||||
)
|
||||
|
||||
if releaseNotes != "" {
|
||||
notesWidget := widget.NewRichText()
|
||||
notesWidget.ParseMarkdown(releaseNotes)
|
||||
|
||||
notesScroll := container.NewScroll(notesWidget)
|
||||
notesScroll.SetMinSize(fyne.NewSize(400, 200))
|
||||
|
||||
content.Add(widget.NewLabel("更新内容:"))
|
||||
content.Add(notesScroll)
|
||||
}
|
||||
|
||||
dialog.ShowCustomConfirm(
|
||||
"发现新版本",
|
||||
"立即更新",
|
||||
"稍后提醒",
|
||||
content,
|
||||
func(confirmed bool) {
|
||||
if confirmed && onConfirm != nil {
|
||||
onConfirm()
|
||||
}
|
||||
},
|
||||
m.window,
|
||||
)
|
||||
}
|
||||
|
||||
// SetActionButtonCallback sets the callback for the main action button
|
||||
func (m *Manager) SetActionButtonCallback(callback func()) {
|
||||
if m.actionButton != nil {
|
||||
m.actionButton.OnTapped = callback
|
||||
}
|
||||
}
|
||||
|
||||
// EnableActionButton enables or disables the action button
|
||||
func (m *Manager) EnableActionButton(enabled bool) {
|
||||
if m.actionButton != nil {
|
||||
if enabled {
|
||||
m.actionButton.Enable()
|
||||
} else {
|
||||
m.actionButton.Disable()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetActionButtonText sets the text of the action button
|
||||
func (m *Manager) SetActionButtonText(text string) {
|
||||
if m.actionButton != nil {
|
||||
m.actionButton.SetText(text)
|
||||
}
|
||||
}
|
||||
|
||||
// ShowErrorWithRetry shows an error with retry option
|
||||
func (m *Manager) ShowErrorWithRetry(errorMsg string, onRetry func()) {
|
||||
dialog.ShowCustomConfirm(
|
||||
"错误",
|
||||
"重试",
|
||||
"取消",
|
||||
widget.NewLabel(errorMsg),
|
||||
func(retry bool) {
|
||||
if retry && onRetry != nil {
|
||||
onRetry()
|
||||
}
|
||||
},
|
||||
m.window,
|
||||
)
|
||||
}
|
||||
|
||||
// UpdateProgressBar updates the progress bar with custom styling
|
||||
func (m *Manager) UpdateProgressBar(percentage float64, color string) {
|
||||
m.ShowProgress(percentage)
|
||||
// Note: Fyne doesn't support custom colors easily, but we keep the interface for future enhancement
|
||||
}
|
||||
|
||||
// HideProgressBar hides the progress bar
|
||||
func (m *Manager) HideProgressBar() {
|
||||
if m.progressBar != nil {
|
||||
m.progressBar.Hide()
|
||||
}
|
||||
}
|
||||
|
||||
// ShowProgressBar shows the progress bar
|
||||
func (m *Manager) ShowProgressBar() {
|
||||
if m.progressBar != nil {
|
||||
m.progressBar.Show()
|
||||
}
|
||||
}
|
||||
|
||||
// SetWindowTitle sets the window title
|
||||
func (m *Manager) SetWindowTitle(title string) {
|
||||
if m.window != nil {
|
||||
m.window.SetTitle(title)
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentStatus returns the current update status
|
||||
func (m *Manager) GetCurrentStatus() UpdateStatus {
|
||||
return m.currentStatus
|
||||
}
|
||||
|
||||
// IsWindowVisible returns whether the window is currently visible
|
||||
func (m *Manager) IsWindowVisible() bool {
|
||||
return m.window != nil && m.window.Content() != nil
|
||||
}
|
||||
|
||||
// RefreshUI refreshes the user interface
|
||||
func (m *Manager) RefreshUI() {
|
||||
if m.window != nil && m.window.Content() != nil {
|
||||
m.window.Content().Refresh()
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes the application
|
||||
func (m *Manager) Close() {
|
||||
if m.window != nil {
|
||||
m.window.Close()
|
||||
}
|
||||
}
|
||||
227
Go_Updater/gui/manager_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestNewManager(t *testing.T) {
|
||||
manager := NewManager()
|
||||
if manager == nil {
|
||||
t.Fatal("NewManager() returned nil")
|
||||
}
|
||||
|
||||
if manager.app == nil {
|
||||
t.Error("Manager app is nil")
|
||||
}
|
||||
|
||||
if manager.window == nil {
|
||||
t.Error("Manager window is nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateStatus(t *testing.T) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
// Test different status updates
|
||||
testCases := []struct {
|
||||
status UpdateStatus
|
||||
message string
|
||||
}{
|
||||
{StatusChecking, "检查更新中..."},
|
||||
{StatusUpdateAvailable, "发现新版本"},
|
||||
{StatusDownloading, "下载中..."},
|
||||
{StatusInstalling, "安装中..."},
|
||||
{StatusCompleted, "更新完成"},
|
||||
{StatusError, "更新失败"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
manager.UpdateStatus(tc.status, tc.message)
|
||||
|
||||
if manager.GetCurrentStatus() != tc.status {
|
||||
t.Errorf("Expected status %v, got %v", tc.status, manager.GetCurrentStatus())
|
||||
}
|
||||
|
||||
if manager.statusLabel.Text != tc.message {
|
||||
t.Errorf("Expected message '%s', got '%s'", tc.message, manager.statusLabel.Text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowProgress(t *testing.T) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
// Test progress values
|
||||
testValues := []float64{0, 25.5, 50, 75.8, 100, 150, -10}
|
||||
expectedValues := []float64{0, 25.5, 50, 75.8, 100, 100, 0}
|
||||
|
||||
for i, value := range testValues {
|
||||
manager.ShowProgress(value)
|
||||
expected := expectedValues[i] / 100.0
|
||||
|
||||
if manager.progressBar.Value != expected {
|
||||
t.Errorf("Expected progress %.2f, got %.2f", expected, manager.progressBar.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetVersionInfo(t *testing.T) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
version := "v1.2.3"
|
||||
manager.SetVersionInfo(version)
|
||||
|
||||
expectedText := "当前版本: v1.2.3"
|
||||
if manager.versionLabel.Text != expectedText {
|
||||
t.Errorf("Expected version text '%s', got '%s'", expectedText, manager.versionLabel.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatSpeed(t *testing.T) {
|
||||
manager := NewManager()
|
||||
|
||||
testCases := []struct {
|
||||
speed int64
|
||||
expected string
|
||||
}{
|
||||
{512, "512 B/s"},
|
||||
{1536, "1.5 KB/s"},
|
||||
{1048576, "1.0 MB/s"},
|
||||
{2621440, "2.5 MB/s"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := manager.formatSpeed(tc.speed)
|
||||
if result != tc.expected {
|
||||
t.Errorf("Expected speed format '%s', got '%s'", tc.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestShowProgressWithSpeed(t *testing.T) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
percentage := 45.5
|
||||
speed := int64(1048576) // 1 MB/s
|
||||
eta := "2分钟"
|
||||
|
||||
manager.ShowProgressWithSpeed(percentage, speed, eta)
|
||||
|
||||
expectedProgress := percentage / 100.0
|
||||
if manager.progressBar.Value != expectedProgress {
|
||||
t.Errorf("Expected progress %.2f, got %.2f", expectedProgress, manager.progressBar.Value)
|
||||
}
|
||||
|
||||
expectedStatus := "下载中... 45.5% (1.0 MB/s) - 剩余时间: 2分钟"
|
||||
if manager.statusLabel.Text != expectedStatus {
|
||||
t.Errorf("Expected status '%s', got '%s'", expectedStatus, manager.statusLabel.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestActionButtonStates(t *testing.T) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
// Test enabling/disabling
|
||||
manager.EnableActionButton(false)
|
||||
if !manager.actionButton.Disabled() {
|
||||
t.Error("Action button should be disabled")
|
||||
}
|
||||
|
||||
manager.EnableActionButton(true)
|
||||
if manager.actionButton.Disabled() {
|
||||
t.Error("Action button should be enabled")
|
||||
}
|
||||
|
||||
// Test text setting
|
||||
testText := "测试按钮"
|
||||
manager.SetActionButtonText(testText)
|
||||
if manager.actionButton.Text != testText {
|
||||
t.Errorf("Expected button text '%s', got '%s'", testText, manager.actionButton.Text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProgressBarVisibility(t *testing.T) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
// Initially hidden
|
||||
if manager.progressBar.Visible() {
|
||||
t.Error("Progress bar should be initially hidden")
|
||||
}
|
||||
|
||||
// Show progress bar
|
||||
manager.ShowProgressBar()
|
||||
if !manager.progressBar.Visible() {
|
||||
t.Error("Progress bar should be visible after ShowProgressBar()")
|
||||
}
|
||||
|
||||
// Hide progress bar
|
||||
manager.HideProgressBar()
|
||||
if manager.progressBar.Visible() {
|
||||
t.Error("Progress bar should be hidden after HideProgressBar()")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSetCallbacks(t *testing.T) {
|
||||
manager := NewManager()
|
||||
|
||||
checkUpdateCalled := false
|
||||
cancelCalled := false
|
||||
|
||||
onCheckUpdate := func() {
|
||||
checkUpdateCalled = true
|
||||
}
|
||||
|
||||
onCancel := func() {
|
||||
cancelCalled = true
|
||||
}
|
||||
|
||||
manager.SetCallbacks(onCheckUpdate, onCancel)
|
||||
|
||||
// Verify callbacks are set
|
||||
if manager.onCheckUpdate == nil {
|
||||
t.Error("onCheckUpdate callback not set")
|
||||
}
|
||||
|
||||
if manager.onCancel == nil {
|
||||
t.Error("onCancel callback not set")
|
||||
}
|
||||
|
||||
// Test callback execution
|
||||
manager.onCheckUpdate()
|
||||
if !checkUpdateCalled {
|
||||
t.Error("onCheckUpdate callback was not called")
|
||||
}
|
||||
|
||||
manager.onCancel()
|
||||
if !cancelCalled {
|
||||
t.Error("onCancel callback was not called")
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests for performance
|
||||
func BenchmarkUpdateStatus(b *testing.B) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
manager.UpdateStatus(StatusDownloading, "下载中...")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkShowProgress(b *testing.B) {
|
||||
manager := NewManager()
|
||||
manager.createUIComponents()
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
manager.ShowProgress(float64(i % 100))
|
||||
}
|
||||
}
|
||||
BIN
Go_Updater/icon/AUTO_MAA_Go_Updater.ico
Normal file
|
After Width: | Height: | Size: 41 KiB |
BIN
Go_Updater/icon/AUTO_MAA_Go_Updater.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
474
Go_Updater/install/manager.go
Normal file
@@ -0,0 +1,474 @@
|
||||
package install
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ChangesInfo 表示 changes.json 文件的结构
|
||||
type ChangesInfo struct {
|
||||
Deleted []string `json:"deleted"`
|
||||
Added []string `json:"added"`
|
||||
Modified []string `json:"modified"`
|
||||
}
|
||||
|
||||
// InstallManager 定义安装操作的接口契约
|
||||
type InstallManager interface {
|
||||
ExtractZip(zipPath, destPath string) error
|
||||
ProcessChanges(changesPath string) (*ChangesInfo, error)
|
||||
ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error
|
||||
HandleRunningProcess(processName string) error
|
||||
CreateTempDir() (string, error)
|
||||
CleanupTempDir(tempDir string) error
|
||||
}
|
||||
|
||||
// Manager 实现 InstallManager 接口
|
||||
type Manager struct {
|
||||
tempDirs []string // 跟踪临时目录以便清理
|
||||
}
|
||||
|
||||
// NewManager 创建新的安装管理器实例
|
||||
func NewManager() *Manager {
|
||||
return &Manager{
|
||||
tempDirs: make([]string, 0),
|
||||
}
|
||||
}
|
||||
|
||||
// CreateTempDir 为解压创建临时目录
|
||||
func (m *Manager) CreateTempDir() (string, error) {
|
||||
tempDir, err := os.MkdirTemp("", "updater_*")
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("创建临时目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 跟踪临时目录以便清理
|
||||
m.tempDirs = append(m.tempDirs, tempDir)
|
||||
return tempDir, nil
|
||||
}
|
||||
|
||||
// CleanupTempDir 删除临时目录及其内容
|
||||
func (m *Manager) CleanupTempDir(tempDir string) error {
|
||||
if tempDir == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
err := os.RemoveAll(tempDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("清理临时目录 %s 失败: %w", tempDir, err)
|
||||
}
|
||||
|
||||
// 从跟踪列表中删除
|
||||
for i, dir := range m.tempDirs {
|
||||
if dir == tempDir {
|
||||
m.tempDirs = append(m.tempDirs[:i], m.tempDirs[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CleanupAllTempDirs 删除所有跟踪的临时目录
|
||||
func (m *Manager) CleanupAllTempDirs() error {
|
||||
var errors []string
|
||||
|
||||
for _, tempDir := range m.tempDirs {
|
||||
if err := os.RemoveAll(tempDir); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("清理 %s 失败: %v", tempDir, err))
|
||||
}
|
||||
}
|
||||
|
||||
m.tempDirs = m.tempDirs[:0] // 清空切片
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("清理错误: %s", strings.Join(errors, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExtractZip 将 ZIP 文件解压到指定的目标目录
|
||||
func (m *Manager) ExtractZip(zipPath, destPath string) error {
|
||||
// 打开 ZIP 文件进行读取
|
||||
reader, err := zip.OpenReader(zipPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开 ZIP 文件 %s 失败: %w", zipPath, err)
|
||||
}
|
||||
defer reader.Close()
|
||||
|
||||
// 如果目标目录不存在则创建
|
||||
if err := os.MkdirAll(destPath, 0755); err != nil {
|
||||
return fmt.Errorf("创建目标目录 %s 失败: %w", destPath, err)
|
||||
}
|
||||
|
||||
// 解压文件
|
||||
for _, file := range reader.File {
|
||||
if err := m.extractFile(file, destPath); err != nil {
|
||||
return fmt.Errorf("解压文件 %s 失败: %w", file.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// extractFile 从 ZIP 归档中解压单个文件
|
||||
func (m *Manager) extractFile(file *zip.File, destPath string) error {
|
||||
// 清理文件路径以防止目录遍历攻击
|
||||
cleanPath := filepath.Clean(file.Name)
|
||||
if strings.Contains(cleanPath, "..") {
|
||||
return fmt.Errorf("无效的文件路径: %s", file.Name)
|
||||
}
|
||||
|
||||
// 创建完整的目标路径
|
||||
destFile := filepath.Join(destPath, cleanPath)
|
||||
|
||||
// 如果需要则创建目录结构
|
||||
if file.FileInfo().IsDir() {
|
||||
return os.MkdirAll(destFile, file.FileInfo().Mode())
|
||||
}
|
||||
|
||||
// 创建父目录
|
||||
if err := os.MkdirAll(filepath.Dir(destFile), 0755); err != nil {
|
||||
return fmt.Errorf("创建父目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 打开 ZIP 归档中的文件
|
||||
rc, err := file.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开归档中的文件失败: %w", err)
|
||||
}
|
||||
defer rc.Close()
|
||||
|
||||
// 创建目标文件
|
||||
outFile, err := os.OpenFile(destFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, file.FileInfo().Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||
}
|
||||
defer outFile.Close()
|
||||
|
||||
// 复制文件内容
|
||||
_, err = io.Copy(outFile, rc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("复制文件内容失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ProcessChanges 读取并解析 changes.json 文件
|
||||
func (m *Manager) ProcessChanges(changesPath string) (*ChangesInfo, error) {
|
||||
// 检查 changes.json 是否存在
|
||||
if _, err := os.Stat(changesPath); os.IsNotExist(err) {
|
||||
// 如果 changes.json 不存在,返回空的变更信息
|
||||
return &ChangesInfo{
|
||||
Deleted: []string{},
|
||||
Added: []string{},
|
||||
Modified: []string{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// 读取 changes.json 文件
|
||||
data, err := os.ReadFile(changesPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取变更文件 %s 失败: %w", changesPath, err)
|
||||
}
|
||||
|
||||
// 解析 JSON
|
||||
var changes ChangesInfo
|
||||
if err := json.Unmarshal(data, &changes); err != nil {
|
||||
return nil, fmt.Errorf("解析变更 JSON 失败: %w", err)
|
||||
}
|
||||
|
||||
return &changes, nil
|
||||
}
|
||||
|
||||
// HandleRunningProcess 通过重命名正在使用的文件来处理正在运行的进程
|
||||
func (m *Manager) HandleRunningProcess(processName string) error {
|
||||
// 获取当前可执行文件路径
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取可执行文件路径失败: %w", err)
|
||||
}
|
||||
|
||||
exeDir := filepath.Dir(exePath)
|
||||
targetFile := filepath.Join(exeDir, processName)
|
||||
|
||||
// 检查目标文件是否存在
|
||||
if _, err := os.Stat(targetFile); os.IsNotExist(err) {
|
||||
// 文件不存在,无需处理
|
||||
return nil
|
||||
}
|
||||
|
||||
// 尝试重命名文件以指示应在下次启动时删除
|
||||
oldFile := targetFile + ".old"
|
||||
|
||||
// 如果存在现有的 .old 文件则删除
|
||||
if _, err := os.Stat(oldFile); err == nil {
|
||||
if err := os.Remove(oldFile); err != nil {
|
||||
return fmt.Errorf("删除现有旧文件 %s 失败: %w", oldFile, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 将当前文件重命名为 .old
|
||||
if err := os.Rename(targetFile, oldFile); err != nil {
|
||||
// 如果重命名失败,进程可能正在运行
|
||||
// 在 Windows 上,我们无法重命名正在运行的可执行文件
|
||||
if isFileInUse(err) {
|
||||
// 标记文件在下次重启时删除(Windows 特定)
|
||||
return m.markFileForDeletion(targetFile)
|
||||
}
|
||||
return fmt.Errorf("重命名正在运行的进程文件 %s 失败: %w", targetFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// isFileInUse 检查错误是否表示文件正在使用中
|
||||
func isFileInUse(err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
// 检查 Windows 特定的"文件正在使用"错误
|
||||
if pathErr, ok := err.(*os.PathError); ok {
|
||||
if errno, ok := pathErr.Err.(syscall.Errno); ok {
|
||||
// ERROR_SHARING_VIOLATION (32) 或 ERROR_ACCESS_DENIED (5)
|
||||
return errno == syscall.Errno(32) || errno == syscall.Errno(5)
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Contains(err.Error(), "being used by another process") ||
|
||||
strings.Contains(err.Error(), "access is denied")
|
||||
}
|
||||
|
||||
// markFileForDeletion 标记文件在下次系统重启时删除(Windows 特定)
|
||||
func (m *Manager) markFileForDeletion(filePath string) error {
|
||||
// 这是 Windows 特定的实现
|
||||
// 目前,我们将创建一个可由主应用程序处理的标记文件
|
||||
markerFile := filePath + ".delete_on_restart"
|
||||
|
||||
// 创建标记文件
|
||||
file, err := os.Create(markerFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建删除标记文件失败: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 将目标文件路径写入标记文件
|
||||
_, err = file.WriteString(filePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("写入标记文件失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteMarkedFiles 删除标记为删除的文件
|
||||
func (m *Manager) DeleteMarkedFiles(directory string) error {
|
||||
// 查找所有 .delete_on_restart 文件
|
||||
pattern := filepath.Join(directory, "*.delete_on_restart")
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找标记文件失败: %w", err)
|
||||
}
|
||||
|
||||
var errors []string
|
||||
for _, markerFile := range matches {
|
||||
// 读取目标文件路径
|
||||
data, err := os.ReadFile(markerFile)
|
||||
if err != nil {
|
||||
errors = append(errors, fmt.Sprintf("读取标记文件 %s 失败: %v", markerFile, err))
|
||||
continue
|
||||
}
|
||||
|
||||
targetFile := strings.TrimSpace(string(data))
|
||||
|
||||
// 尝试删除目标文件
|
||||
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
|
||||
errors = append(errors, fmt.Sprintf("删除标记文件 %s 失败: %v", targetFile, err))
|
||||
}
|
||||
|
||||
// 删除标记文件
|
||||
if err := os.Remove(markerFile); err != nil {
|
||||
errors = append(errors, fmt.Sprintf("删除标记文件 %s 失败: %v", markerFile, err))
|
||||
}
|
||||
}
|
||||
|
||||
if len(errors) > 0 {
|
||||
return fmt.Errorf("删除错误: %s", strings.Join(errors, "; "))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ApplyUpdate 通过从源目录复制文件到目标目录来应用更新
|
||||
func (m *Manager) ApplyUpdate(sourcePath, targetPath string, changes *ChangesInfo) error {
|
||||
// 创建备份目录
|
||||
backupDir, err := m.createBackupDir(targetPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建备份目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 在应用更新前备份现有文件
|
||||
if err := m.backupFiles(targetPath, backupDir, changes); err != nil {
|
||||
return fmt.Errorf("备份文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 应用更新
|
||||
if err := m.applyUpdateFiles(sourcePath, targetPath, changes); err != nil {
|
||||
// 失败时回滚
|
||||
if rollbackErr := m.rollbackUpdate(targetPath, backupDir); rollbackErr != nil {
|
||||
return fmt.Errorf("更新失败且回滚失败: 更新错误: %w, 回滚错误: %v", err, rollbackErr)
|
||||
}
|
||||
return fmt.Errorf("更新失败已回滚: %w", err)
|
||||
}
|
||||
|
||||
// 成功更新后清理备份目录
|
||||
if err := os.RemoveAll(backupDir); err != nil {
|
||||
// 记录警告但不让更新失败
|
||||
fmt.Printf("警告: 清理备份目录 %s 失败: %v\n", backupDir, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// createBackupDir 为更新创建备份目录
|
||||
func (m *Manager) createBackupDir(targetPath string) (string, error) {
|
||||
backupDir := filepath.Join(targetPath, ".backup_"+fmt.Sprintf("%d", os.Getpid()))
|
||||
|
||||
if err := os.MkdirAll(backupDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("创建备份目录失败: %w", err)
|
||||
}
|
||||
|
||||
return backupDir, nil
|
||||
}
|
||||
|
||||
// backupFiles 创建将被修改或删除的文件的备份
|
||||
func (m *Manager) backupFiles(targetPath, backupDir string, changes *ChangesInfo) error {
|
||||
// 备份将被修改的文件
|
||||
for _, file := range changes.Modified {
|
||||
srcFile := filepath.Join(targetPath, file)
|
||||
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
|
||||
continue // 文件不存在,跳过备份
|
||||
}
|
||||
|
||||
backupFile := filepath.Join(backupDir, file)
|
||||
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
|
||||
return fmt.Errorf("备份修改文件 %s 失败: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 备份将被删除的文件
|
||||
for _, file := range changes.Deleted {
|
||||
srcFile := filepath.Join(targetPath, file)
|
||||
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
|
||||
continue // 文件不存在,跳过备份
|
||||
}
|
||||
|
||||
backupFile := filepath.Join(backupDir, file)
|
||||
if err := m.copyFileWithDirs(srcFile, backupFile); err != nil {
|
||||
return fmt.Errorf("备份删除文件 %s 失败: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// applyUpdateFiles 应用实际的文件更改
|
||||
func (m *Manager) applyUpdateFiles(sourcePath, targetPath string, changes *ChangesInfo) error {
|
||||
// 删除标记为删除的文件
|
||||
for _, file := range changes.Deleted {
|
||||
targetFile := filepath.Join(targetPath, file)
|
||||
if err := os.Remove(targetFile); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("删除文件 %s 失败: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 复制新文件和修改的文件
|
||||
filesToCopy := append(changes.Added, changes.Modified...)
|
||||
for _, file := range filesToCopy {
|
||||
srcFile := filepath.Join(sourcePath, file)
|
||||
targetFile := filepath.Join(targetPath, file)
|
||||
|
||||
// 检查源文件是否存在
|
||||
if _, err := os.Stat(srcFile); os.IsNotExist(err) {
|
||||
continue // 源文件不存在,跳过
|
||||
}
|
||||
|
||||
if err := m.copyFileWithDirs(srcFile, targetFile); err != nil {
|
||||
return fmt.Errorf("复制文件 %s 失败: %w", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// copyFileWithDirs 复制文件并创建必要的目录
|
||||
func (m *Manager) copyFileWithDirs(src, dst string) error {
|
||||
// 创建父目录
|
||||
if err := os.MkdirAll(filepath.Dir(dst), 0755); err != nil {
|
||||
return fmt.Errorf("创建父目录失败: %w", err)
|
||||
}
|
||||
|
||||
// 打开源文件
|
||||
srcFile, err := os.Open(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf("打开源文件失败: %w", err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// 获取源文件信息
|
||||
srcInfo, err := srcFile.Stat()
|
||||
if err != nil {
|
||||
return fmt.Errorf("获取源文件信息失败: %w", err)
|
||||
}
|
||||
|
||||
// 创建目标文件
|
||||
dstFile, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, srcInfo.Mode())
|
||||
if err != nil {
|
||||
return fmt.Errorf("创建目标文件失败: %w", err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
// 复制文件内容
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("复制文件内容失败: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rollbackUpdate 在更新失败时从备份恢复文件
|
||||
func (m *Manager) rollbackUpdate(targetPath, backupDir string) error {
|
||||
// 遍历备份目录并恢复文件
|
||||
return filepath.Walk(backupDir, func(backupFile string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
return nil // 跳过目录
|
||||
}
|
||||
|
||||
// 计算相对路径
|
||||
relPath, err := filepath.Rel(backupDir, backupFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("计算相对路径失败: %w", err)
|
||||
}
|
||||
|
||||
// 将文件恢复到目标位置
|
||||
targetFile := filepath.Join(targetPath, relPath)
|
||||
if err := m.copyFileWithDirs(backupFile, targetFile); err != nil {
|
||||
return fmt.Errorf("恢复文件 %s 失败: %w", relPath, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
1033
Go_Updater/install/manager_test.go
Normal file
12
Go_Updater/integration_test.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 集成测试将在此处实现
|
||||
// 此文件目前是占位符
|
||||
|
||||
func TestIntegrationPlaceholder(t *testing.T) {
|
||||
t.Skip("集成测试尚未实现")
|
||||
}
|
||||
438
Go_Updater/logger/logger.go
Normal file
@@ -0,0 +1,438 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LogLevel 日志级别
|
||||
type LogLevel int
|
||||
|
||||
const (
|
||||
DEBUG LogLevel = iota
|
||||
INFO
|
||||
WARN
|
||||
ERROR
|
||||
)
|
||||
|
||||
// String 返回日志级别的字符串表示
|
||||
func (l LogLevel) String() string {
|
||||
switch l {
|
||||
case DEBUG:
|
||||
return "DEBUG"
|
||||
case INFO:
|
||||
return "INFO"
|
||||
case WARN:
|
||||
return "WARN"
|
||||
case ERROR:
|
||||
return "ERROR"
|
||||
default:
|
||||
return "UNKNOWN"
|
||||
}
|
||||
}
|
||||
|
||||
// Logger 日志记录器接口
|
||||
type Logger interface {
|
||||
Debug(msg string, fields ...interface{})
|
||||
Info(msg string, fields ...interface{})
|
||||
Warn(msg string, fields ...interface{})
|
||||
Error(msg string, fields ...interface{})
|
||||
SetLevel(level LogLevel)
|
||||
Close() error
|
||||
}
|
||||
|
||||
// FileLogger 文件日志记录器
|
||||
type FileLogger struct {
|
||||
mu sync.RWMutex
|
||||
file *os.File
|
||||
logger *log.Logger
|
||||
level LogLevel
|
||||
maxSize int64 // 最大文件大小(字节)
|
||||
maxBackups int // 最大备份文件数
|
||||
logDir string // 日志目录
|
||||
filename string // 日志文件名
|
||||
currentSize int64 // 当前文件大小
|
||||
}
|
||||
|
||||
// LoggerConfig 日志配置
|
||||
type LoggerConfig struct {
|
||||
Level LogLevel
|
||||
MaxSize int64 // 最大文件大小(字节),默认10MB
|
||||
MaxBackups int // 最大备份文件数,默认5
|
||||
LogDir string // 日志目录
|
||||
Filename string // 日志文件名
|
||||
}
|
||||
|
||||
// DefaultLoggerConfig 默认日志配置
|
||||
func DefaultLoggerConfig() *LoggerConfig {
|
||||
// 获取当前可执行文件目录
|
||||
exePath, err := os.Executable()
|
||||
var logDir string
|
||||
if err != nil {
|
||||
logDir = "debug"
|
||||
} else {
|
||||
exeDir := filepath.Dir(exePath)
|
||||
logDir = filepath.Join(exeDir, "debug")
|
||||
}
|
||||
|
||||
return &LoggerConfig{
|
||||
Level: INFO,
|
||||
MaxSize: 10 * 1024 * 1024, // 10MB
|
||||
MaxBackups: 5,
|
||||
LogDir: logDir,
|
||||
Filename: "AUTO_MAA_Go_Updater.log",
|
||||
}
|
||||
}
|
||||
|
||||
// NewFileLogger 创建新的文件日志记录器
|
||||
func NewFileLogger(config *LoggerConfig) (*FileLogger, error) {
|
||||
if config == nil {
|
||||
config = DefaultLoggerConfig()
|
||||
}
|
||||
|
||||
// 创建日志目录
|
||||
if err := os.MkdirAll(config.LogDir, 0755); err != nil {
|
||||
return nil, fmt.Errorf("failed to create log directory: %w", err)
|
||||
}
|
||||
|
||||
logPath := filepath.Join(config.LogDir, config.Filename)
|
||||
|
||||
// 打开或创建日志文件
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open log file: %w", err)
|
||||
}
|
||||
|
||||
// 获取当前文件大小
|
||||
stat, err := file.Stat()
|
||||
if err != nil {
|
||||
file.Close()
|
||||
return nil, fmt.Errorf("failed to get file stats: %w", err)
|
||||
}
|
||||
|
||||
logger := &FileLogger{
|
||||
file: file,
|
||||
logger: log.New(file, "", 0), // 我们自己处理格式
|
||||
level: config.Level,
|
||||
maxSize: config.MaxSize,
|
||||
maxBackups: config.MaxBackups,
|
||||
logDir: config.LogDir,
|
||||
filename: config.Filename,
|
||||
currentSize: stat.Size(),
|
||||
}
|
||||
|
||||
return logger, nil
|
||||
}
|
||||
|
||||
// formatMessage 格式化日志消息
|
||||
func (fl *FileLogger) formatMessage(level LogLevel, msg string, fields ...interface{}) string {
|
||||
timestamp := time.Now().Format("2006-01-02 15:04:05.000")
|
||||
|
||||
if len(fields) > 0 {
|
||||
msg = fmt.Sprintf(msg, fields...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s] %s %s\n", timestamp, level.String(), msg)
|
||||
}
|
||||
|
||||
// writeLog 写入日志
|
||||
func (fl *FileLogger) writeLog(level LogLevel, msg string, fields ...interface{}) {
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
|
||||
// 检查日志级别
|
||||
if level < fl.level {
|
||||
return
|
||||
}
|
||||
|
||||
formattedMsg := fl.formatMessage(level, msg, fields...)
|
||||
|
||||
// 检查是否需要轮转
|
||||
if fl.currentSize+int64(len(formattedMsg)) > fl.maxSize {
|
||||
if err := fl.rotate(); err != nil {
|
||||
// 轮转失败,尝试写入stderr
|
||||
fmt.Fprintf(os.Stderr, "Failed to rotate log: %v\n", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 写入日志
|
||||
n, err := fl.file.WriteString(formattedMsg)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Failed to write log: %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fl.currentSize += int64(n)
|
||||
fl.file.Sync() // 确保写入磁盘
|
||||
}
|
||||
|
||||
// rotate 轮转日志文件
|
||||
func (fl *FileLogger) rotate() error {
|
||||
// 关闭当前文件
|
||||
if err := fl.file.Close(); err != nil {
|
||||
return fmt.Errorf("failed to close current log file: %w", err)
|
||||
}
|
||||
|
||||
// 轮转备份文件
|
||||
if err := fl.rotateBackups(); err != nil {
|
||||
return fmt.Errorf("failed to rotate backups: %w", err)
|
||||
}
|
||||
|
||||
// 创建新的日志文件
|
||||
logPath := filepath.Join(fl.logDir, fl.filename)
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create new log file: %w", err)
|
||||
}
|
||||
|
||||
fl.file = file
|
||||
fl.logger.SetOutput(file)
|
||||
fl.currentSize = 0
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// rotateBackups 轮转备份文件
|
||||
func (fl *FileLogger) rotateBackups() error {
|
||||
basePath := filepath.Join(fl.logDir, fl.filename)
|
||||
|
||||
// 删除最老的备份文件
|
||||
if fl.maxBackups > 0 {
|
||||
oldestBackup := fmt.Sprintf("%s.%d", basePath, fl.maxBackups)
|
||||
os.Remove(oldestBackup) // 忽略错误,文件可能不存在
|
||||
}
|
||||
|
||||
// 重命名现有备份文件
|
||||
for i := fl.maxBackups - 1; i > 0; i-- {
|
||||
oldName := fmt.Sprintf("%s.%d", basePath, i)
|
||||
newName := fmt.Sprintf("%s.%d", basePath, i+1)
|
||||
os.Rename(oldName, newName) // 忽略错误,文件可能不存在
|
||||
}
|
||||
|
||||
// 将当前日志文件重命名为第一个备份
|
||||
if fl.maxBackups > 0 {
|
||||
backupName := fmt.Sprintf("%s.1", basePath)
|
||||
return os.Rename(basePath, backupName)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Debug 记录调试级别日志
|
||||
func (fl *FileLogger) Debug(msg string, fields ...interface{}) {
|
||||
fl.writeLog(DEBUG, msg, fields...)
|
||||
}
|
||||
|
||||
// Info 记录信息级别日志
|
||||
func (fl *FileLogger) Info(msg string, fields ...interface{}) {
|
||||
fl.writeLog(INFO, msg, fields...)
|
||||
}
|
||||
|
||||
// Warn 记录警告级别日志
|
||||
func (fl *FileLogger) Warn(msg string, fields ...interface{}) {
|
||||
fl.writeLog(WARN, msg, fields...)
|
||||
}
|
||||
|
||||
// Error 记录错误级别日志
|
||||
func (fl *FileLogger) Error(msg string, fields ...interface{}) {
|
||||
fl.writeLog(ERROR, msg, fields...)
|
||||
}
|
||||
|
||||
// SetLevel 设置日志级别
|
||||
func (fl *FileLogger) SetLevel(level LogLevel) {
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
fl.level = level
|
||||
}
|
||||
|
||||
// Close 关闭日志记录器
|
||||
func (fl *FileLogger) Close() error {
|
||||
fl.mu.Lock()
|
||||
defer fl.mu.Unlock()
|
||||
|
||||
if fl.file != nil {
|
||||
return fl.file.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// MultiLogger 多输出日志记录器
|
||||
type MultiLogger struct {
|
||||
loggers []Logger
|
||||
level LogLevel
|
||||
}
|
||||
|
||||
// NewMultiLogger 创建多输出日志记录器
|
||||
func NewMultiLogger(loggers ...Logger) *MultiLogger {
|
||||
return &MultiLogger{
|
||||
loggers: loggers,
|
||||
level: INFO,
|
||||
}
|
||||
}
|
||||
|
||||
// Debug 记录调试级别日志
|
||||
func (ml *MultiLogger) Debug(msg string, fields ...interface{}) {
|
||||
for _, logger := range ml.loggers {
|
||||
logger.Debug(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// Info 记录信息级别日志
|
||||
func (ml *MultiLogger) Info(msg string, fields ...interface{}) {
|
||||
for _, logger := range ml.loggers {
|
||||
logger.Info(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// Warn 记录警告级别日志
|
||||
func (ml *MultiLogger) Warn(msg string, fields ...interface{}) {
|
||||
for _, logger := range ml.loggers {
|
||||
logger.Warn(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// Error 记录错误级别日志
|
||||
func (ml *MultiLogger) Error(msg string, fields ...interface{}) {
|
||||
for _, logger := range ml.loggers {
|
||||
logger.Error(msg, fields...)
|
||||
}
|
||||
}
|
||||
|
||||
// SetLevel 设置日志级别
|
||||
func (ml *MultiLogger) SetLevel(level LogLevel) {
|
||||
ml.level = level
|
||||
for _, logger := range ml.loggers {
|
||||
logger.SetLevel(level)
|
||||
}
|
||||
}
|
||||
|
||||
// Close 关闭所有日志记录器
|
||||
func (ml *MultiLogger) Close() error {
|
||||
var lastErr error
|
||||
for _, logger := range ml.loggers {
|
||||
if err := logger.Close(); err != nil {
|
||||
lastErr = err
|
||||
}
|
||||
}
|
||||
return lastErr
|
||||
}
|
||||
|
||||
// ConsoleLogger 控制台日志记录器
|
||||
type ConsoleLogger struct {
|
||||
writer io.Writer
|
||||
level LogLevel
|
||||
}
|
||||
|
||||
// NewConsoleLogger 创建控制台日志记录器
|
||||
func NewConsoleLogger(writer io.Writer) *ConsoleLogger {
|
||||
if writer == nil {
|
||||
writer = os.Stdout
|
||||
}
|
||||
return &ConsoleLogger{
|
||||
writer: writer,
|
||||
level: INFO,
|
||||
}
|
||||
}
|
||||
|
||||
// formatMessage 格式化控制台日志消息
|
||||
func (cl *ConsoleLogger) formatMessage(level LogLevel, msg string, fields ...interface{}) string {
|
||||
timestamp := time.Now().Format("15:04:05")
|
||||
|
||||
if len(fields) > 0 {
|
||||
msg = fmt.Sprintf(msg, fields...)
|
||||
}
|
||||
|
||||
return fmt.Sprintf("[%s] %s %s\n", timestamp, level.String(), msg)
|
||||
}
|
||||
|
||||
// writeLog 写入控制台日志
|
||||
func (cl *ConsoleLogger) writeLog(level LogLevel, msg string, fields ...interface{}) {
|
||||
if level < cl.level {
|
||||
return
|
||||
}
|
||||
|
||||
formattedMsg := cl.formatMessage(level, msg, fields...)
|
||||
fmt.Fprint(cl.writer, formattedMsg)
|
||||
}
|
||||
|
||||
// Debug 记录调试级别日志
|
||||
func (cl *ConsoleLogger) Debug(msg string, fields ...interface{}) {
|
||||
cl.writeLog(DEBUG, msg, fields...)
|
||||
}
|
||||
|
||||
// Info 记录信息级别日志
|
||||
func (cl *ConsoleLogger) Info(msg string, fields ...interface{}) {
|
||||
cl.writeLog(INFO, msg, fields...)
|
||||
}
|
||||
|
||||
// Warn 记录警告级别日志
|
||||
func (cl *ConsoleLogger) Warn(msg string, fields ...interface{}) {
|
||||
cl.writeLog(WARN, msg, fields...)
|
||||
}
|
||||
|
||||
// Error 记录错误级别日志
|
||||
func (cl *ConsoleLogger) Error(msg string, fields ...interface{}) {
|
||||
cl.writeLog(ERROR, msg, fields...)
|
||||
}
|
||||
|
||||
// SetLevel 设置日志级别
|
||||
func (cl *ConsoleLogger) SetLevel(level LogLevel) {
|
||||
cl.level = level
|
||||
}
|
||||
|
||||
// Close 关闭控制台日志记录器(无操作)
|
||||
func (cl *ConsoleLogger) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 全局日志记录器实例
|
||||
var (
|
||||
defaultLogger Logger
|
||||
once sync.Once
|
||||
)
|
||||
|
||||
// GetDefaultLogger 获取默认日志记录器
|
||||
func GetDefaultLogger() Logger {
|
||||
once.Do(func() {
|
||||
fileLogger, err := NewFileLogger(DefaultLoggerConfig())
|
||||
if err != nil {
|
||||
// 如果文件日志创建失败,使用控制台日志
|
||||
defaultLogger = NewConsoleLogger(os.Stderr)
|
||||
} else {
|
||||
// 同时输出到文件和控制台
|
||||
consoleLogger := NewConsoleLogger(os.Stdout)
|
||||
defaultLogger = NewMultiLogger(fileLogger, consoleLogger)
|
||||
}
|
||||
})
|
||||
return defaultLogger
|
||||
}
|
||||
|
||||
// 便捷函数
|
||||
func Debug(msg string, fields ...interface{}) {
|
||||
GetDefaultLogger().Debug(msg, fields...)
|
||||
}
|
||||
|
||||
func Info(msg string, fields ...interface{}) {
|
||||
GetDefaultLogger().Info(msg, fields...)
|
||||
}
|
||||
|
||||
func Warn(msg string, fields ...interface{}) {
|
||||
GetDefaultLogger().Warn(msg, fields...)
|
||||
}
|
||||
|
||||
func Error(msg string, fields ...interface{}) {
|
||||
GetDefaultLogger().Error(msg, fields...)
|
||||
}
|
||||
|
||||
func SetLevel(level LogLevel) {
|
||||
GetDefaultLogger().SetLevel(level)
|
||||
}
|
||||
|
||||
func Close() error {
|
||||
return GetDefaultLogger().Close()
|
||||
}
|
||||
300
Go_Updater/logger/logger_test.go
Normal file
@@ -0,0 +1,300 @@
|
||||
package logger
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestLogLevel_String(t *testing.T) {
|
||||
tests := []struct {
|
||||
level LogLevel
|
||||
expected string
|
||||
}{
|
||||
{DEBUG, "DEBUG"},
|
||||
{INFO, "INFO"},
|
||||
{WARN, "WARN"},
|
||||
{ERROR, "ERROR"},
|
||||
{LogLevel(999), "UNKNOWN"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.expected, func(t *testing.T) {
|
||||
if got := tt.level.String(); got != tt.expected {
|
||||
t.Errorf("LogLevel.String() = %v, want %v", got, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultLoggerConfig(t *testing.T) {
|
||||
config := DefaultLoggerConfig()
|
||||
|
||||
if config.Level != INFO {
|
||||
t.Errorf("Expected default level INFO, got %v", config.Level)
|
||||
}
|
||||
if config.MaxSize != 10*1024*1024 {
|
||||
t.Errorf("Expected default max size 10MB, got %v", config.MaxSize)
|
||||
}
|
||||
if config.MaxBackups != 5 {
|
||||
t.Errorf("Expected default max backups 5, got %v", config.MaxBackups)
|
||||
}
|
||||
if config.Filename != "updater.log" {
|
||||
t.Errorf("Expected default filename 'updater.log', got %v", config.Filename)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConsoleLogger(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := NewConsoleLogger(&buf)
|
||||
|
||||
t.Run("log levels", func(t *testing.T) {
|
||||
logger.SetLevel(DEBUG)
|
||||
|
||||
logger.Debug("debug message")
|
||||
logger.Info("info message")
|
||||
logger.Warn("warn message")
|
||||
logger.Error("error message")
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "DEBUG debug message") {
|
||||
t.Error("Expected debug message in output")
|
||||
}
|
||||
if !strings.Contains(output, "INFO info message") {
|
||||
t.Error("Expected info message in output")
|
||||
}
|
||||
if !strings.Contains(output, "WARN warn message") {
|
||||
t.Error("Expected warn message in output")
|
||||
}
|
||||
if !strings.Contains(output, "ERROR error message") {
|
||||
t.Error("Expected error message in output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("log level filtering", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
logger.SetLevel(WARN)
|
||||
|
||||
logger.Debug("debug message")
|
||||
logger.Info("info message")
|
||||
logger.Warn("warn message")
|
||||
logger.Error("error message")
|
||||
|
||||
output := buf.String()
|
||||
if strings.Contains(output, "DEBUG") {
|
||||
t.Error("Debug message should be filtered out")
|
||||
}
|
||||
if strings.Contains(output, "INFO") {
|
||||
t.Error("Info message should be filtered out")
|
||||
}
|
||||
if !strings.Contains(output, "WARN warn message") {
|
||||
t.Error("Expected warn message in output")
|
||||
}
|
||||
if !strings.Contains(output, "ERROR error message") {
|
||||
t.Error("Expected error message in output")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("formatted messages", func(t *testing.T) {
|
||||
buf.Reset()
|
||||
logger.SetLevel(DEBUG)
|
||||
|
||||
logger.Info("formatted message: %s %d", "test", 42)
|
||||
|
||||
output := buf.String()
|
||||
if !strings.Contains(output, "formatted message: test 42") {
|
||||
t.Error("Expected formatted message in output")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestFileLogger(t *testing.T) {
|
||||
// 创建临时目录
|
||||
tempDir := t.TempDir()
|
||||
|
||||
config := &LoggerConfig{
|
||||
Level: DEBUG,
|
||||
MaxSize: 1024, // 1KB for testing rotation
|
||||
MaxBackups: 3,
|
||||
LogDir: tempDir,
|
||||
Filename: "test.log",
|
||||
}
|
||||
|
||||
logger, err := NewFileLogger(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create file logger: %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
t.Run("basic logging", func(t *testing.T) {
|
||||
logger.Info("test message")
|
||||
logger.Error("error message with %s", "formatting")
|
||||
|
||||
// 读取日志文件
|
||||
logPath := filepath.Join(tempDir, "test.log")
|
||||
content, err := os.ReadFile(logPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to read log file: %v", err)
|
||||
}
|
||||
|
||||
output := string(content)
|
||||
if !strings.Contains(output, "INFO test message") {
|
||||
t.Error("Expected info message in log file")
|
||||
}
|
||||
if !strings.Contains(output, "ERROR error message with formatting") {
|
||||
t.Error("Expected formatted error message in log file")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("log rotation", func(t *testing.T) {
|
||||
// 写入大量数据触发轮转
|
||||
longMessage := strings.Repeat("a", 200)
|
||||
for i := 0; i < 10; i++ {
|
||||
logger.Info("Long message %d: %s", i, longMessage)
|
||||
}
|
||||
|
||||
// 检查是否创建了备份文件
|
||||
logPath := filepath.Join(tempDir, "test.log")
|
||||
backupPath := filepath.Join(tempDir, "test.log.1")
|
||||
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
t.Error("Main log file should exist")
|
||||
}
|
||||
if _, err := os.Stat(backupPath); os.IsNotExist(err) {
|
||||
t.Error("Backup log file should exist after rotation")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestMultiLogger(t *testing.T) {
|
||||
var buf1, buf2 bytes.Buffer
|
||||
logger1 := NewConsoleLogger(&buf1)
|
||||
logger2 := NewConsoleLogger(&buf2)
|
||||
|
||||
multiLogger := NewMultiLogger(logger1, logger2)
|
||||
multiLogger.SetLevel(INFO)
|
||||
|
||||
multiLogger.Info("test message")
|
||||
multiLogger.Error("error message")
|
||||
|
||||
// 检查两个logger都收到了消息
|
||||
output1 := buf1.String()
|
||||
output2 := buf2.String()
|
||||
|
||||
if !strings.Contains(output1, "INFO test message") {
|
||||
t.Error("Expected info message in first logger")
|
||||
}
|
||||
if !strings.Contains(output1, "ERROR error message") {
|
||||
t.Error("Expected error message in first logger")
|
||||
}
|
||||
if !strings.Contains(output2, "INFO test message") {
|
||||
t.Error("Expected info message in second logger")
|
||||
}
|
||||
if !strings.Contains(output2, "ERROR error message") {
|
||||
t.Error("Expected error message in second logger")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileLoggerRotation(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
config := &LoggerConfig{
|
||||
Level: DEBUG,
|
||||
MaxSize: 100, // Very small for testing
|
||||
MaxBackups: 2,
|
||||
LogDir: tempDir,
|
||||
Filename: "rotation_test.log",
|
||||
}
|
||||
|
||||
logger, err := NewFileLogger(config)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to create file logger: %v", err)
|
||||
}
|
||||
defer logger.Close()
|
||||
|
||||
// 写入足够的数据触发多次轮转
|
||||
for i := 0; i < 20; i++ {
|
||||
logger.Info("Message %d: %s", i, strings.Repeat("x", 50))
|
||||
}
|
||||
|
||||
// 检查文件存在性
|
||||
logPath := filepath.Join(tempDir, "rotation_test.log")
|
||||
backup1Path := filepath.Join(tempDir, "rotation_test.log.1")
|
||||
backup2Path := filepath.Join(tempDir, "rotation_test.log.2")
|
||||
backup3Path := filepath.Join(tempDir, "rotation_test.log.3")
|
||||
|
||||
if _, err := os.Stat(logPath); os.IsNotExist(err) {
|
||||
t.Error("Main log file should exist")
|
||||
}
|
||||
if _, err := os.Stat(backup1Path); os.IsNotExist(err) {
|
||||
t.Error("First backup should exist")
|
||||
}
|
||||
if _, err := os.Stat(backup2Path); os.IsNotExist(err) {
|
||||
t.Error("Second backup should exist")
|
||||
}
|
||||
// 第三个备份不应该存在(MaxBackups=2)
|
||||
if _, err := os.Stat(backup3Path); !os.IsNotExist(err) {
|
||||
t.Error("Third backup should not exist (exceeds MaxBackups)")
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalLoggerFunctions(t *testing.T) {
|
||||
// 这个测试比较简单,主要确保全局函数不会panic
|
||||
Debug("debug message")
|
||||
Info("info message")
|
||||
Warn("warn message")
|
||||
Error("error message")
|
||||
|
||||
SetLevel(ERROR)
|
||||
|
||||
// 这些调用不应该panic
|
||||
Debug("filtered debug")
|
||||
Info("filtered info")
|
||||
Error("visible error")
|
||||
}
|
||||
|
||||
func TestFileLoggerErrorHandling(t *testing.T) {
|
||||
t.Run("invalid directory", func(t *testing.T) {
|
||||
// 使用一个真正无效的路径
|
||||
config := &LoggerConfig{
|
||||
Level: INFO,
|
||||
MaxSize: 1024,
|
||||
MaxBackups: 3,
|
||||
LogDir: string([]byte{0}), // 无效的路径字符
|
||||
Filename: "test.log",
|
||||
}
|
||||
|
||||
_, err := NewFileLogger(config)
|
||||
if err == nil {
|
||||
t.Error("Expected error when creating logger with invalid directory")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestLoggerFormatting(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
logger := NewConsoleLogger(&buf)
|
||||
logger.SetLevel(DEBUG)
|
||||
|
||||
// 测试时间戳格式
|
||||
logger.Info("test message")
|
||||
|
||||
output := buf.String()
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
if len(lines) == 0 {
|
||||
t.Fatal("Expected at least one log line")
|
||||
}
|
||||
|
||||
// 检查格式:[HH:MM:SS] LEVEL message
|
||||
line := lines[0]
|
||||
if !strings.Contains(line, "INFO test message") {
|
||||
t.Errorf("Expected 'INFO test message' in output, got: %s", line)
|
||||
}
|
||||
|
||||
// 检查时间戳格式(简单检查)
|
||||
if !strings.HasPrefix(line, "[") {
|
||||
t.Error("Expected log line to start with timestamp in brackets")
|
||||
}
|
||||
}
|
||||
1049
Go_Updater/main.go
Normal file
189
Go_Updater/version/manager.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"AUTO_MAA_Go_Updater/logger"
|
||||
)
|
||||
|
||||
// VersionInfo 表示来自 version.json 的版本信息
|
||||
type VersionInfo struct {
|
||||
MainVersion string `json:"main_version"`
|
||||
VersionInfo map[string]map[string][]string `json:"version_info"`
|
||||
}
|
||||
|
||||
// ParsedVersion 表示解析后的版本,包含主版本号、次版本号、补丁版本号和测试版本号组件
|
||||
type ParsedVersion struct {
|
||||
Major int
|
||||
Minor int
|
||||
Patch int
|
||||
Beta int
|
||||
}
|
||||
|
||||
// VersionManager 处理版本相关操作
|
||||
type VersionManager struct {
|
||||
executableDir string
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
// NewVersionManager 创建新的版本管理器
|
||||
func NewVersionManager() *VersionManager {
|
||||
execPath, _ := os.Executable()
|
||||
execDir := filepath.Dir(execPath)
|
||||
return &VersionManager{
|
||||
executableDir: execDir,
|
||||
logger: logger.GetDefaultLogger(),
|
||||
}
|
||||
}
|
||||
|
||||
// createDefaultVersion 创建默认版本结构 v0.0.0
|
||||
func (vm *VersionManager) createDefaultVersion() *VersionInfo {
|
||||
return &VersionInfo{
|
||||
MainVersion: "0.0.0.0", // 对应 v0.0.0
|
||||
VersionInfo: make(map[string]map[string][]string),
|
||||
}
|
||||
}
|
||||
|
||||
// LoadVersionFromFile 从 resources/version.json 加载版本信息并处理回退
|
||||
func (vm *VersionManager) LoadVersionFromFile() (*VersionInfo, error) {
|
||||
versionPath := filepath.Join(vm.executableDir, "resources", "version.json")
|
||||
|
||||
data, err := os.ReadFile(versionPath)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
fmt.Println("未读取到版本信息,使用默认版本进行更新。")
|
||||
return vm.createDefaultVersion(), nil
|
||||
}
|
||||
vm.logger.Warn("读取版本文件 %s 失败: %v,将使用默认版本", versionPath, err)
|
||||
return vm.createDefaultVersion(), nil
|
||||
}
|
||||
|
||||
var versionInfo VersionInfo
|
||||
if err := json.Unmarshal(data, &versionInfo); err != nil {
|
||||
vm.logger.Warn("解析版本文件 %s 失败: %v,将使用默认版本", versionPath, err)
|
||||
return vm.createDefaultVersion(), nil
|
||||
}
|
||||
|
||||
vm.logger.Debug("成功从 %s 加载版本信息", versionPath)
|
||||
return &versionInfo, nil
|
||||
}
|
||||
|
||||
// LoadVersionWithDefault 加载版本信息并保证回退到默认版本
|
||||
func (vm *VersionManager) LoadVersionWithDefault() *VersionInfo {
|
||||
versionInfo, err := vm.LoadVersionFromFile()
|
||||
if err != nil {
|
||||
// 这在更新的 LoadVersionFromFile 中不应该发生,但添加作为额外安全措施
|
||||
vm.logger.Error("加载版本文件时出现意外错误: %v,使用默认版本", err)
|
||||
return vm.createDefaultVersion()
|
||||
}
|
||||
|
||||
// 验证我们有一个有效的版本结构
|
||||
if versionInfo == nil {
|
||||
vm.logger.Warn("版本信息为空,使用默认版本")
|
||||
return vm.createDefaultVersion()
|
||||
}
|
||||
|
||||
if versionInfo.MainVersion == "" {
|
||||
vm.logger.Warn("版本信息主版本为空,使用默认版本")
|
||||
return vm.createDefaultVersion()
|
||||
}
|
||||
|
||||
if versionInfo.VersionInfo == nil {
|
||||
vm.logger.Debug("版本信息映射为空,初始化空映射")
|
||||
versionInfo.VersionInfo = make(map[string]map[string][]string)
|
||||
}
|
||||
|
||||
return versionInfo
|
||||
}
|
||||
|
||||
// ParseVersion 解析版本字符串如 "4.4.1.3" 为组件
|
||||
func ParseVersion(versionStr string) (*ParsedVersion, error) {
|
||||
parts := strings.Split(versionStr, ".")
|
||||
if len(parts) < 3 || len(parts) > 4 {
|
||||
return nil, fmt.Errorf("无效的版本格式: %s", versionStr)
|
||||
}
|
||||
|
||||
major, err := strconv.Atoi(parts[0])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的主版本号: %s", parts[0])
|
||||
}
|
||||
|
||||
minor, err := strconv.Atoi(parts[1])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的次版本号: %s", parts[1])
|
||||
}
|
||||
|
||||
patch, err := strconv.Atoi(parts[2])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的补丁版本号: %s", parts[2])
|
||||
}
|
||||
|
||||
beta := 0
|
||||
if len(parts) == 4 {
|
||||
beta, err = strconv.Atoi(parts[3])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("无效的测试版本号: %s", parts[3])
|
||||
}
|
||||
}
|
||||
|
||||
return &ParsedVersion{
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
Patch: patch,
|
||||
Beta: beta,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// ToVersionString 将 ParsedVersion 转换回版本字符串格式
|
||||
func (pv *ParsedVersion) ToVersionString() string {
|
||||
if pv.Beta == 0 {
|
||||
return fmt.Sprintf("%d.%d.%d.0", pv.Major, pv.Minor, pv.Patch)
|
||||
}
|
||||
return fmt.Sprintf("%d.%d.%d.%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
|
||||
}
|
||||
|
||||
// ToDisplayVersion 将版本转换为显示格式 (4.4.0 或 4.4.1-beta3)
|
||||
func (pv *ParsedVersion) ToDisplayVersion() string {
|
||||
if pv.Beta == 0 {
|
||||
return fmt.Sprintf("%d.%d.%d", pv.Major, pv.Minor, pv.Patch)
|
||||
}
|
||||
return fmt.Sprintf("%d.%d.%d-beta%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
|
||||
}
|
||||
|
||||
// GetChannel 根据版本返回渠道 (stable 或 beta)
|
||||
func (pv *ParsedVersion) GetChannel() string {
|
||||
if pv.Beta == 0 {
|
||||
return "stable"
|
||||
}
|
||||
return "beta"
|
||||
}
|
||||
|
||||
// IsNewer 检查此版本是否比其他版本更新
|
||||
func (pv *ParsedVersion) IsNewer(other *ParsedVersion) bool {
|
||||
if pv.Major != other.Major {
|
||||
return pv.Major > other.Major
|
||||
}
|
||||
if pv.Minor != other.Minor {
|
||||
return pv.Minor > other.Minor
|
||||
}
|
||||
if pv.Patch != other.Patch {
|
||||
return pv.Patch > other.Patch
|
||||
}
|
||||
|
||||
// 对于相同的主版本号,正式版本(Beta=0)比beta版本(Beta>0)更新
|
||||
// 例如:4.4.1.0(正式版)> 4.4.1.3(beta3)
|
||||
if pv.Beta == 0 && other.Beta > 0 {
|
||||
return true // 正式版比beta版更新
|
||||
}
|
||||
if pv.Beta > 0 && other.Beta == 0 {
|
||||
return false // beta版比正式版旧
|
||||
}
|
||||
|
||||
// 如果都是beta版本,比较beta版本号
|
||||
return pv.Beta > other.Beta
|
||||
}
|
||||
366
Go_Updater/version/manager_test.go
Normal file
@@ -0,0 +1,366 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
input string
|
||||
expected *ParsedVersion
|
||||
hasError bool
|
||||
}{
|
||||
{"4.4.0.0", &ParsedVersion{4, 4, 0, 0}, false},
|
||||
{"4.4.1.3", &ParsedVersion{4, 4, 1, 3}, false},
|
||||
{"1.2.3", &ParsedVersion{1, 2, 3, 0}, false},
|
||||
{"invalid", nil, true},
|
||||
{"1.2", nil, true},
|
||||
{"1.2.3.4.5", nil, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result, err := ParseVersion(test.input)
|
||||
|
||||
if test.hasError {
|
||||
if err == nil {
|
||||
t.Errorf("Expected error for input %s, but got none", test.input)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for input %s: %v", test.input, err)
|
||||
continue
|
||||
}
|
||||
|
||||
if result.Major != test.expected.Major ||
|
||||
result.Minor != test.expected.Minor ||
|
||||
result.Patch != test.expected.Patch ||
|
||||
result.Beta != test.expected.Beta {
|
||||
t.Errorf("For input %s, expected %+v, got %+v", test.input, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestToDisplayVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
version *ParsedVersion
|
||||
expected string
|
||||
}{
|
||||
{&ParsedVersion{4, 4, 0, 0}, "v4.4.0"},
|
||||
{&ParsedVersion{4, 4, 1, 3}, "v4.4.1-beta3"},
|
||||
{&ParsedVersion{1, 2, 3, 0}, "v1.2.3"},
|
||||
{&ParsedVersion{1, 2, 3, 5}, "v1.2.3-beta5"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := test.version.ToDisplayVersion()
|
||||
if result != test.expected {
|
||||
t.Errorf("For version %+v, expected %s, got %s", test.version, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetChannel(t *testing.T) {
|
||||
tests := []struct {
|
||||
version *ParsedVersion
|
||||
expected string
|
||||
}{
|
||||
{&ParsedVersion{4, 4, 0, 0}, "stable"},
|
||||
{&ParsedVersion{4, 4, 1, 3}, "beta"},
|
||||
{&ParsedVersion{1, 2, 3, 0}, "stable"},
|
||||
{&ParsedVersion{1, 2, 3, 1}, "beta"},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := test.version.GetChannel()
|
||||
if result != test.expected {
|
||||
t.Errorf("For version %+v, expected channel %s, got %s", test.version, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsNewer(t *testing.T) {
|
||||
tests := []struct {
|
||||
v1 *ParsedVersion
|
||||
v2 *ParsedVersion
|
||||
expected bool
|
||||
}{
|
||||
{&ParsedVersion{4, 4, 1, 0}, &ParsedVersion{4, 4, 0, 0}, true},
|
||||
{&ParsedVersion{4, 4, 0, 0}, &ParsedVersion{4, 4, 1, 0}, false},
|
||||
{&ParsedVersion{4, 4, 1, 3}, &ParsedVersion{4, 4, 1, 2}, true},
|
||||
{&ParsedVersion{4, 4, 1, 2}, &ParsedVersion{4, 4, 1, 3}, false},
|
||||
{&ParsedVersion{4, 4, 1, 0}, &ParsedVersion{4, 4, 1, 0}, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
result := test.v1.IsNewer(test.v2)
|
||||
if result != test.expected {
|
||||
t.Errorf("For %+v.IsNewer(%+v), expected %t, got %t", test.v1, test.v2, test.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionFromFile(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "version_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create resources directory
|
||||
resourcesDir := filepath.Join(tempDir, "resources")
|
||||
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test version file
|
||||
versionData := VersionInfo{
|
||||
MainVersion: "4.4.1.3",
|
||||
VersionInfo: map[string]map[string][]string{
|
||||
"4.4.1.3": {
|
||||
"修复BUG": {"移除崩溃弹窗机制"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(versionData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
versionFile := filepath.Join(resourcesDir, "version.json")
|
||||
if err := os.WriteFile(versionFile, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create version manager with custom executable directory and logger
|
||||
vm := NewVersionManager()
|
||||
vm.executableDir = tempDir
|
||||
|
||||
// Test loading version
|
||||
result, err := vm.LoadVersionFromFile()
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to load version: %v", err)
|
||||
}
|
||||
|
||||
if result.MainVersion != "4.4.1.3" {
|
||||
t.Errorf("Expected main version 4.4.1.3, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if len(result.VersionInfo) != 1 {
|
||||
t.Errorf("Expected 1 version info entry, got %d", len(result.VersionInfo))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionFromFileNotFound(t *testing.T) {
|
||||
// Create a temporary directory without version file
|
||||
tempDir, err := os.MkdirTemp("", "version_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create version manager with custom executable directory and logger
|
||||
vm := NewVersionManager()
|
||||
vm.executableDir = tempDir
|
||||
|
||||
// Test loading version (should now return default version instead of error)
|
||||
result, err := vm.LoadVersionFromFile()
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error with fallback mechanism, but got: %v", err)
|
||||
}
|
||||
|
||||
// Should return default version
|
||||
if result.MainVersion != "0.0.0.0" {
|
||||
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if result.VersionInfo == nil {
|
||||
t.Error("Expected initialized VersionInfo map, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionWithDefault(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "version_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create version manager with custom executable directory
|
||||
vm := NewVersionManager()
|
||||
vm.executableDir = tempDir
|
||||
|
||||
// Test loading version with default (no file exists)
|
||||
result := vm.LoadVersionWithDefault()
|
||||
if result == nil {
|
||||
t.Fatal("Expected non-nil result from LoadVersionWithDefault")
|
||||
}
|
||||
|
||||
if result.MainVersion != "0.0.0.0" {
|
||||
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if result.VersionInfo == nil {
|
||||
t.Error("Expected initialized VersionInfo map, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionWithDefaultValidFile(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "version_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create resources directory
|
||||
resourcesDir := filepath.Join(tempDir, "resources")
|
||||
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create test version file
|
||||
versionData := VersionInfo{
|
||||
MainVersion: "4.4.1.3",
|
||||
VersionInfo: map[string]map[string][]string{
|
||||
"4.4.1.3": {
|
||||
"修复BUG": {"移除崩溃弹窗机制"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data, err := json.Marshal(versionData)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
versionFile := filepath.Join(resourcesDir, "version.json")
|
||||
if err := os.WriteFile(versionFile, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create version manager with custom executable directory
|
||||
vm := NewVersionManager()
|
||||
vm.executableDir = tempDir
|
||||
|
||||
// Test loading version with default (valid file exists)
|
||||
result := vm.LoadVersionWithDefault()
|
||||
if result == nil {
|
||||
t.Fatal("Expected non-nil result from LoadVersionWithDefault")
|
||||
}
|
||||
|
||||
if result.MainVersion != "4.4.1.3" {
|
||||
t.Errorf("Expected version 4.4.1.3, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if len(result.VersionInfo) != 1 {
|
||||
t.Errorf("Expected 1 version info entry, got %d", len(result.VersionInfo))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionFromFileCorrupted(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "version_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create resources directory
|
||||
resourcesDir := filepath.Join(tempDir, "resources")
|
||||
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create corrupted version file
|
||||
versionFile := filepath.Join(resourcesDir, "version.json")
|
||||
if err := os.WriteFile(versionFile, []byte("invalid json content"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create version manager with custom executable directory
|
||||
vm := NewVersionManager()
|
||||
vm.executableDir = tempDir
|
||||
|
||||
// Test loading version (should return default version for corrupted file)
|
||||
result, err := vm.LoadVersionFromFile()
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error with fallback mechanism for corrupted file, but got: %v", err)
|
||||
}
|
||||
|
||||
// Should return default version
|
||||
if result.MainVersion != "0.0.0.0" {
|
||||
t.Errorf("Expected default version 0.0.0.0 for corrupted file, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if result.VersionInfo == nil {
|
||||
t.Error("Expected initialized VersionInfo map for corrupted file, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadVersionWithDefaultCorrupted(t *testing.T) {
|
||||
// Create a temporary directory
|
||||
tempDir, err := os.MkdirTemp("", "version_test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
// Create resources directory
|
||||
resourcesDir := filepath.Join(tempDir, "resources")
|
||||
if err := os.MkdirAll(resourcesDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create corrupted version file
|
||||
versionFile := filepath.Join(resourcesDir, "version.json")
|
||||
if err := os.WriteFile(versionFile, []byte("invalid json content"), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create version manager with custom executable directory
|
||||
vm := NewVersionManager()
|
||||
vm.executableDir = tempDir
|
||||
|
||||
// Test loading version with default (corrupted file)
|
||||
result := vm.LoadVersionWithDefault()
|
||||
if result == nil {
|
||||
t.Fatal("Expected non-nil result from LoadVersionWithDefault for corrupted file")
|
||||
}
|
||||
|
||||
if result.MainVersion != "0.0.0.0" {
|
||||
t.Errorf("Expected default version 0.0.0.0 for corrupted file, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if result.VersionInfo == nil {
|
||||
t.Error("Expected initialized VersionInfo map for corrupted file, got nil")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreateDefaultVersion(t *testing.T) {
|
||||
vm := NewVersionManager()
|
||||
|
||||
result := vm.createDefaultVersion()
|
||||
if result == nil {
|
||||
t.Fatal("Expected non-nil result from createDefaultVersion")
|
||||
}
|
||||
|
||||
if result.MainVersion != "0.0.0.0" {
|
||||
t.Errorf("Expected default version 0.0.0.0, got %s", result.MainVersion)
|
||||
}
|
||||
|
||||
if result.VersionInfo == nil {
|
||||
t.Error("Expected initialized VersionInfo map, got nil")
|
||||
}
|
||||
|
||||
if len(result.VersionInfo) != 0 {
|
||||
t.Errorf("Expected empty VersionInfo map, got %d entries", len(result.VersionInfo))
|
||||
}
|
||||
}
|
||||
19
Go_Updater/version/version.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package version
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
)
|
||||
|
||||
var (
|
||||
// Version 应用程序的当前版本
|
||||
Version = "1.0.0"
|
||||
|
||||
// BuildTime 在构建时设置
|
||||
BuildTime = "unknown"
|
||||
|
||||
// GitCommit 在构建时设置
|
||||
GitCommit = "unknown"
|
||||
|
||||
// GoVersion 用于构建的 Go 版本
|
||||
GoVersion = runtime.Version()
|
||||
)
|
||||
41
README.md
@@ -13,7 +13,8 @@
|
||||
<a href="https://github.com/DLmaster361/AUTO_MAA/issues"><img alt="GitHub Issues" src="https://img.shields.io/github/issues/DLmaster361/AUTO_MAA?style=flat-square"></a>
|
||||
<a href="https://github.com/DLmaster361/AUTO_MAA/graphs/contributors"><img alt="GitHub Contributors" src="https://img.shields.io/github/contributors/DLmaster361/AUTO_MAA?style=flat-square"></a>
|
||||
<a href="https://github.com/DLmaster361/AUTO_MAA/blob/main/LICENSE"><img alt="GitHub License" src="https://img.shields.io/github/license/DLmaster361/AUTO_MAA?style=flat-square"></a>
|
||||
<a href="https://mirrorchyan.com/zh/projects?rid=AUTO_MAA"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
|
||||
<a href="https://deepwiki.com/DLmaster361/AUTO_MAA"><img alt="DeepWiki" src="https://deepwiki.com/badge.svg"></a>
|
||||
<a href="https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-readme"><img alt="mirrorc" src="https://img.shields.io/badge/Mirror%E9%85%B1-%239af3f6?logo=countingworkspro&logoColor=4f46e5"></a>
|
||||
</p>
|
||||
|
||||
## 软件介绍
|
||||
@@ -22,6 +23,11 @@
|
||||
|
||||
本软件是明日方舟第三方软件`MAA`的第三方工具,即第3<sup>3</sup>方软件。旨在优化MAA多账号功能体验,并通过一些方法解决MAA项目未能解决的部分问题,提高代理的稳定性。
|
||||
|
||||
- **集中管理**:一站式管理多个MAA脚本与多个用户配置,和凌乱的散装脚本窗口说再见!
|
||||
- **无人值守**:自动处理MAA相关报错,再也不用为代理任务卡死时自己不在电脑旁烦恼啦!
|
||||
- **配置灵活**:通过调度队列与脚本的组合,自由实现您能想到的所有调度需求!
|
||||
- **信息统计**:自动统计用户的公招与关卡掉落物,看看这个月您的收益是多少!
|
||||
|
||||
### 原理
|
||||
|
||||
本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能:
|
||||
@@ -32,11 +38,9 @@
|
||||
|
||||
### 优势
|
||||
|
||||
- **节省运行开销:** 只需要一份MAA软件与一个模拟器,无需多开就能完成多账号代理,羸弱的电脑也能代理日常。
|
||||
- **自定义空间大:** 依靠高级用户配置模式,支持MAA几乎所有设置选项自定义,支持模拟器多开。
|
||||
- **调度方法自由:** 通过调度队列功能,自由实现MAA多开等多种调度方式。
|
||||
- **一键代理无忧:** 无须中途手动修改MAA配置,将繁琐交给AUTO_MAA,把游戏留给自己。
|
||||
- **代理结果复核:** 通过人工排查功能核实各用户代理情况,堵住自动代理的最后一丝风险。
|
||||
- **高效稳定**:通过日志监测、异常处理等机制,保障代理任务顺利完成。
|
||||
- **简洁易用**:无需手动修改配置文件,实现自动化调度与多开管理。
|
||||
- **兼容扩展**:支持 MAA 几乎所有的配置选项,满足不同用户需求。
|
||||
|
||||
## 重要声明
|
||||
|
||||
@@ -48,13 +52,10 @@
|
||||
- **传播:** 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上提及的,项目组拥有最终解释权。
|
||||
|
||||
**注意**
|
||||
|
||||
- 由于本软件有修改其它目录JSON文件等行为,使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录,避免被误杀。
|
||||
|
||||
---
|
||||
|
||||
# 使用方法
|
||||
@@ -71,6 +72,24 @@
|
||||
|
||||
可在[《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/)。
|
||||
|
||||
## 贡献者
|
||||
|
||||
感谢以下贡献者对本项目做出的贡献
|
||||
@@ -83,8 +102,6 @@
|
||||
|
||||

|
||||
|
||||
感谢 [AoXuan (@ClozyA)](https://github.com/ClozyA) 为本项目提供的下载服务器
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#DLmaster361/AUTO_MAA&Date)
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主程序包
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
@@ -33,7 +33,6 @@ from .core import QueueConfig, MaaConfig, MaaUserConfig, Task, TaskManager, Main
|
||||
from .models import MaaManager
|
||||
from .services import Notify, Crypto, System
|
||||
from .ui import AUTO_MAA
|
||||
from .utils import DownloadManager
|
||||
|
||||
__all__ = [
|
||||
"QueueConfig",
|
||||
@@ -47,5 +46,4 @@ __all__ = [
|
||||
"Crypto",
|
||||
"System",
|
||||
"AUTO_MAA",
|
||||
"DownloadManager",
|
||||
]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA核心组件包
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
@@ -29,9 +29,19 @@ __version__ = "4.2.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .config import QueueConfig, MaaConfig, MaaUserConfig, Config
|
||||
from .config import (
|
||||
QueueConfig,
|
||||
MaaConfig,
|
||||
MaaUserConfig,
|
||||
MaaPlanConfig,
|
||||
GeneralConfig,
|
||||
GeneralSubConfig,
|
||||
Config,
|
||||
)
|
||||
from .logger import logger
|
||||
from .main_info_bar import MainInfoBar
|
||||
from .network import Network
|
||||
from .sound_player import SoundPlayer
|
||||
from .task_manager import Task, TaskManager
|
||||
from .timer import MainTimer
|
||||
|
||||
@@ -40,8 +50,13 @@ __all__ = [
|
||||
"QueueConfig",
|
||||
"MaaConfig",
|
||||
"MaaUserConfig",
|
||||
"MaaPlanConfig",
|
||||
"GeneralConfig",
|
||||
"GeneralSubConfig",
|
||||
"logger",
|
||||
"MainInfoBar",
|
||||
"Network",
|
||||
"SoundPlayer",
|
||||
"Task",
|
||||
"TaskManager",
|
||||
"MainTimer",
|
||||
|
||||
2001
app/core/config.py
34
app/core/logger.py
Normal file
@@ -0,0 +1,34 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA日志组件
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger as _logger
|
||||
|
||||
# 设置日志 module 字段默认值
|
||||
logger = _logger.patch(
|
||||
lambda record: record["extra"].setdefault("module", "未知模块") or True
|
||||
)
|
||||
logger.remove(0)
|
||||
@@ -21,37 +21,59 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA信息通知栏
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtCore import Qt
|
||||
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||
|
||||
from .logger import logger
|
||||
from .config import Config
|
||||
from .sound_player import SoundPlayer
|
||||
|
||||
|
||||
class _MainInfoBar:
|
||||
"""信息通知栏"""
|
||||
|
||||
def push_info_bar(self, mode: str, title: str, content: str, time: int):
|
||||
"""推送到信息通知栏"""
|
||||
# 模式到 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("信息通知栏未设置父窗口")
|
||||
logger.error("信息通知栏未设置父窗口", module="吐司通知栏")
|
||||
return None
|
||||
|
||||
# 定义模式到 InfoBar 方法的映射
|
||||
mode_mapping = {
|
||||
"success": InfoBar.success,
|
||||
"warning": InfoBar.warning,
|
||||
"error": InfoBar.error,
|
||||
"info": InfoBar.info,
|
||||
}
|
||||
|
||||
# 根据 mode 获取对应的 InfoBar 方法
|
||||
info_bar_method = mode_mapping.get(mode)
|
||||
if info_bar_method:
|
||||
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,
|
||||
@@ -61,8 +83,27 @@ class _MainInfoBar:
|
||||
duration=time,
|
||||
parent=Config.main_window,
|
||||
)
|
||||
else:
|
||||
logger.error(f"未知的通知栏模式: {mode}")
|
||||
|
||||
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()
|
||||
|
||||
@@ -21,100 +21,288 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA网络请求线程
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtCore import QThread, QEventLoop, QTimer
|
||||
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 _Network(QThread):
|
||||
class NetworkThread(QThread):
|
||||
"""网络请求线程类"""
|
||||
|
||||
max_retries = 3
|
||||
timeout = 10
|
||||
backoff_factor = 0.1
|
||||
|
||||
def __init__(self) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
mode: str,
|
||||
url: str,
|
||||
path: Path = None,
|
||||
files: Dict = None,
|
||||
data: Dict = None,
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.if_running = False
|
||||
self.mode = None
|
||||
self.url = None
|
||||
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()
|
||||
self.wait_loop = QEventLoop()
|
||||
|
||||
truststore.inject_into_ssl() # 信任系统证书
|
||||
|
||||
@logger.catch
|
||||
def run(self) -> None:
|
||||
"""运行网络请求线程"""
|
||||
|
||||
self.if_running = True
|
||||
|
||||
if self.mode == "get":
|
||||
self.get_json(self.url)
|
||||
elif self.mode == "get_file":
|
||||
self.get_file(self.url, self.path)
|
||||
|
||||
self.if_running = False
|
||||
|
||||
def set_info(self, mode: str, url: str, path: Path = None) -> None:
|
||||
"""设置网络请求信息"""
|
||||
|
||||
while self.if_running:
|
||||
QTimer.singleShot(self.backoff_factor * 1000, self.wait_loop.quit)
|
||||
self.wait_loop.exec()
|
||||
|
||||
self.mode = mode
|
||||
self.url = url
|
||||
self.path = path
|
||||
|
||||
self.stutus_code = None
|
||||
self.response_json = None
|
||||
self.error_message = None
|
||||
elif self.mode == "upload_file":
|
||||
self.upload_file(self.url, self.files, self.data)
|
||||
|
||||
def get_json(self, url: str) -> None:
|
||||
"""通过get方法获取json数据"""
|
||||
"""
|
||||
通过get方法获取json数据
|
||||
|
||||
:param url: 请求的URL
|
||||
"""
|
||||
|
||||
logger.info(f"子线程 {self.objectName()} 开始网络请求", module="网络请求子线程")
|
||||
|
||||
response = None
|
||||
|
||||
for _ in range(self.max_retries):
|
||||
try:
|
||||
response = requests.get(url, timeout=self.timeout)
|
||||
self.stutus_code = response.status_code
|
||||
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.stutus_code = response.status_code if response else None
|
||||
self.status_code = response.status_code if response else None
|
||||
self.response_json = None
|
||||
self.error_message = str(e)
|
||||
logger.exception(
|
||||
f"子线程 {self.objectName()} 网络请求失败:{e},第{_+1}次尝试",
|
||||
module="网络请求子线程",
|
||||
)
|
||||
time.sleep(self.backoff_factor)
|
||||
|
||||
self.loop.quit()
|
||||
|
||||
def get_file(self, url: str, path: Path) -> None:
|
||||
"""通过get方法下载文件"""
|
||||
"""
|
||||
通过get方法下载文件到指定路径
|
||||
|
||||
:param url: 请求的URL
|
||||
:param path: 下载文件的保存路径
|
||||
"""
|
||||
|
||||
logger.info(f"子线程 {self.objectName()} 开始下载文件", module="网络请求子线程")
|
||||
|
||||
response = None
|
||||
|
||||
try:
|
||||
response = requests.get(url, timeout=10)
|
||||
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.stutus_code = response.status_code
|
||||
self.status_code = response.status_code
|
||||
self.error_message = None
|
||||
else:
|
||||
self.stutus_code = response.status_code
|
||||
self.error_message = "下载失败"
|
||||
self.status_code = response.status_code
|
||||
self.error_message = f"下载失败,状态码: {response.status_code}"
|
||||
|
||||
except Exception as e:
|
||||
self.stutus_code = response.status_code if response else None
|
||||
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()
|
||||
|
||||
79
app/core/sound_player.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA音效播放器
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from 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()
|
||||
@@ -21,22 +21,22 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA业务调度器
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtCore import QThread, QObject, Signal
|
||||
from qfluentwidgets import MessageBox
|
||||
from datetime import datetime
|
||||
from packaging import version
|
||||
from typing import Dict, Union
|
||||
|
||||
from .logger import logger
|
||||
from .config import Config
|
||||
from .main_info_bar import MainInfoBar
|
||||
from .network import Network
|
||||
from app.models import MaaManager
|
||||
from app.services import System
|
||||
from .sound_player import SoundPlayer
|
||||
from app.models import MaaManager, GeneralManager
|
||||
|
||||
|
||||
class Task(QThread):
|
||||
@@ -44,9 +44,11 @@ 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_user_info = Signal(str, dict)
|
||||
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)
|
||||
@@ -59,6 +61,8 @@ class Task(QThread):
|
||||
):
|
||||
super(Task, self).__init__()
|
||||
|
||||
self.setObjectName(f"Task-{mode}-{name}")
|
||||
|
||||
self.mode = mode
|
||||
self.name = name
|
||||
self.info = info
|
||||
@@ -72,31 +76,58 @@ class Task(QThread):
|
||||
|
||||
if "设置MAA" in self.mode:
|
||||
|
||||
logger.info(f"任务开始:设置{self.name}")
|
||||
logger.info(f"任务开始:设置{self.name}", module=f"业务 {self.name}")
|
||||
self.push_info_bar.emit("info", "设置MAA", self.name, 3000)
|
||||
|
||||
self.task = MaaManager(
|
||||
self.mode,
|
||||
Config.member_dict[self.name],
|
||||
Config.script_dict[self.name],
|
||||
(None if "全局" in self.mode else self.info["SetMaaInfo"]["Path"]),
|
||||
)
|
||||
self.task.check_maa_version.connect(self.check_maa_version.emit)
|
||||
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([]))
|
||||
|
||||
self.task.run()
|
||||
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.member_dict[value]["Config"].get(
|
||||
Config.member_dict[value]["Config"].MaaSet_Name
|
||||
)
|
||||
== ""
|
||||
else f"{value} - {Config.member_dict[value]["Config"].get(Config.member_dict[value]["Config"].MaaSet_Name)}"
|
||||
if Config.script_dict[value]["Config"].get_name() == ""
|
||||
else f"{value} - {Config.script_dict[value]["Config"].get_name()}"
|
||||
),
|
||||
"等待",
|
||||
value,
|
||||
@@ -117,23 +148,28 @@ class Task(QThread):
|
||||
task[1] = "运行"
|
||||
self.update_task_list.emit(self.task_list)
|
||||
|
||||
# 检查任务是否在运行列表中
|
||||
if task[2] in Config.running_list:
|
||||
|
||||
task[1] = "跳过"
|
||||
self.update_task_list.emit(self.task_list)
|
||||
logger.info(f"跳过任务:{task[0]}")
|
||||
logger.info(
|
||||
f"跳过任务:{task[0]},该任务已在运行列表中",
|
||||
module=f"业务 {self.name}",
|
||||
)
|
||||
self.push_info_bar.emit("info", "跳过任务", task[0], 3000)
|
||||
continue
|
||||
|
||||
# 标记为运行中
|
||||
Config.running_list.append(task[2])
|
||||
logger.info(f"任务开始:{task[0]}")
|
||||
logger.info(f"任务开始:{task[0]}", module=f"业务 {self.name}")
|
||||
self.push_info_bar.emit("info", "任务开始", task[0], 3000)
|
||||
|
||||
if Config.member_dict[task[2]]["Type"] == "Maa":
|
||||
if Config.script_dict[task[2]]["Type"] == "Maa":
|
||||
|
||||
self.task = MaaManager(
|
||||
self.mode[0:4],
|
||||
Config.member_dict[task[2]],
|
||||
Config.script_dict[task[2]],
|
||||
)
|
||||
|
||||
self.task.check_maa_version.connect(self.check_maa_version.emit)
|
||||
@@ -141,27 +177,78 @@ class Task(QThread):
|
||||
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_user_info.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)
|
||||
)
|
||||
|
||||
self.task.run()
|
||||
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])
|
||||
|
||||
task[1] = "完成"
|
||||
self.update_task_list.emit(self.task_list)
|
||||
logger.info(f"任务完成:{task[0]}")
|
||||
self.push_info_bar.emit("info", "任务完成", task[0], 3000)
|
||||
|
||||
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()
|
||||
@@ -181,7 +268,13 @@ class _TaskManager(QObject):
|
||||
def add_task(
|
||||
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
|
||||
):
|
||||
"""添加任务"""
|
||||
"""
|
||||
添加任务
|
||||
|
||||
:param mode: 任务模式
|
||||
:param name: 任务名称
|
||||
:param info: 任务信息
|
||||
"""
|
||||
|
||||
if name in Config.running_list or name in self.task_dict:
|
||||
|
||||
@@ -189,33 +282,47 @@ class _TaskManager(QObject):
|
||||
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
|
||||
return None
|
||||
|
||||
logger.info(f"任务开始:{name}")
|
||||
logger.info(f"任务开始:{name},模式:{mode}", module="业务调度")
|
||||
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
|
||||
SoundPlayer.play("任务开始")
|
||||
|
||||
# 标记任务为运行中
|
||||
Config.running_list.append(name)
|
||||
|
||||
# 创建任务实例并连接信号
|
||||
self.task_dict[name] = Task(mode, name, info)
|
||||
self.task_dict[name].check_maa_version.connect(self.check_maa_version)
|
||||
self.task_dict[name].question.connect(
|
||||
lambda title, content: self.push_dialog(name, title, content)
|
||||
)
|
||||
self.task_dict[name].push_info_bar.connect(MainInfoBar.push_info_bar)
|
||||
self.task_dict[name].update_user_info.connect(Config.change_user_info)
|
||||
self.task_dict[name].play_sound.connect(SoundPlayer.play)
|
||||
self.task_dict[name].update_maa_user_info.connect(Config.change_maa_user_info)
|
||||
self.task_dict[name].update_general_sub_info.connect(
|
||||
Config.change_general_sub_info
|
||||
)
|
||||
self.task_dict[name].accomplish.connect(
|
||||
lambda logs: self.remove_task(mode, name, logs)
|
||||
)
|
||||
|
||||
# 向UI发送信号以创建或连接GUI
|
||||
if "新调度台" in mode:
|
||||
self.create_gui.emit(self.task_dict[name])
|
||||
|
||||
elif "主调度台" in mode:
|
||||
self.connect_gui.emit(self.task_dict[name])
|
||||
|
||||
# 启动任务线程
|
||||
self.task_dict[name].start()
|
||||
|
||||
def stop_task(self, name: str):
|
||||
"""中止任务"""
|
||||
def stop_task(self, name: str) -> None:
|
||||
"""
|
||||
中止任务
|
||||
|
||||
logger.info(f"中止任务:{name}")
|
||||
:param name: 任务名称
|
||||
"""
|
||||
|
||||
logger.info(f"中止任务:{name}", module="业务调度")
|
||||
MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
|
||||
|
||||
if name == "ALL":
|
||||
@@ -234,18 +341,27 @@ class _TaskManager(QObject):
|
||||
self.task_dict[name].quit()
|
||||
self.task_dict[name].wait()
|
||||
|
||||
def remove_task(self, mode: str, name: str, logs: list):
|
||||
"""任务结束后的处理"""
|
||||
def remove_task(self, mode: str, name: str, logs: list) -> None:
|
||||
"""
|
||||
处理任务结束后的收尾工作
|
||||
|
||||
logger.info(f"任务结束:{name}")
|
||||
:param mode: 任务模式
|
||||
:param name: 任务名称
|
||||
:param logs: 任务日志
|
||||
"""
|
||||
|
||||
logger.info(f"任务结束:{name}", module="业务调度")
|
||||
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
|
||||
SoundPlayer.play("任务结束")
|
||||
|
||||
# 删除任务线程,移除运行中标记
|
||||
self.task_dict[name].deleteLater()
|
||||
self.task_dict.pop(name)
|
||||
Config.running_list.remove(name)
|
||||
|
||||
if "调度队列" in name and "人工排查" not in mode:
|
||||
|
||||
# 保存调度队列历史记录
|
||||
if len(logs) > 0:
|
||||
time = logs[0][1]["Time"]
|
||||
history = ""
|
||||
@@ -261,50 +377,48 @@ class _TaskManager(QObject):
|
||||
},
|
||||
)
|
||||
|
||||
# 根据调度队列情况设置电源状态
|
||||
if (
|
||||
Config.queue_dict[name]["Config"].get(
|
||||
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
|
||||
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
|
||||
)
|
||||
!= "None"
|
||||
!= "NoAction"
|
||||
and Config.power_sign == "NoAction"
|
||||
):
|
||||
|
||||
from app.ui import ProgressRingMessageBox
|
||||
|
||||
mode_book = {
|
||||
"Shutdown": "关机",
|
||||
"Hibernate": "休眠",
|
||||
"Sleep": "睡眠",
|
||||
"KillSelf": "关闭AUTO_MAA",
|
||||
}
|
||||
|
||||
choice = ProgressRingMessageBox(
|
||||
Config.main_window,
|
||||
f"{mode_book[Config.queue_dict[name]["Config"].get(Config.queue_dict[name]["Config"].queueSet_AfterAccomplish)]}倒计时",
|
||||
)
|
||||
if choice.exec():
|
||||
System.set_power(
|
||||
Config.queue_dict[name]["Config"].get(
|
||||
Config.queue_dict[name]["Config"].queueSet_AfterAccomplish
|
||||
)
|
||||
Config.set_power_sign(
|
||||
Config.queue_dict[name]["Config"].get(
|
||||
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
|
||||
)
|
||||
)
|
||||
|
||||
def check_maa_version(self, v: str):
|
||||
"""检查MAA版本"""
|
||||
if Config.args.mode == "cli" and Config.power_sign == "NoAction":
|
||||
Config.set_power_sign("KillSelf")
|
||||
|
||||
Network.set_info(
|
||||
def check_maa_version(self, v: str) -> None:
|
||||
"""
|
||||
检查MAA版本,如果版本过低则推送通知
|
||||
|
||||
:param v: 当前MAA版本
|
||||
"""
|
||||
|
||||
logger.info(f"检查MAA版本:{v}", module="业务调度")
|
||||
network = Network.add_task(
|
||||
mode="get",
|
||||
url="https://mirrorchyan.com/api/resources/MAA/latest?user_agent=AutoMaaGui&os=win&arch=x64&channel=stable",
|
||||
)
|
||||
Network.start()
|
||||
Network.loop.exec()
|
||||
if Network.stutus_code == 200:
|
||||
maa_info = Network.response_json
|
||||
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.error_message}")
|
||||
logger.warning(
|
||||
f"获取MAA版本信息时出错:{network_result['error_message']}",
|
||||
module="业务调度",
|
||||
)
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"获取MAA版本信息时出错",
|
||||
f"网络错误:{Network.stutus_code}",
|
||||
f"网络错误:{network_result['status_code']}",
|
||||
5000,
|
||||
)
|
||||
return None
|
||||
@@ -312,7 +426,8 @@ class _TaskManager(QObject):
|
||||
if version.parse(maa_info["data"]["version_name"]) > version.parse(v):
|
||||
|
||||
logger.info(
|
||||
f"检测到MAA版本过低:{v},最新版本:{maa_info['data']['version_name']}"
|
||||
f"检测到MAA版本过低:{v},最新版本:{maa_info['data']['version_name']}",
|
||||
module="业务调度",
|
||||
)
|
||||
MainInfoBar.push_info_bar(
|
||||
"info",
|
||||
@@ -321,8 +436,19 @@ class _TaskManager(QObject):
|
||||
-1,
|
||||
)
|
||||
|
||||
logger.success(
|
||||
f"MAA版本检查完成:{v},最新版本:{maa_info['data']['version_name']}",
|
||||
module="业务调度",
|
||||
)
|
||||
|
||||
def push_dialog(self, name: str, title: str, content: str):
|
||||
"""推送对话框"""
|
||||
"""
|
||||
推送来自任务线程的对话框
|
||||
|
||||
:param name: 任务名称
|
||||
:param title: 对话框标题
|
||||
:param content: 对话框内容
|
||||
"""
|
||||
|
||||
choice = MessageBox(title, content, Config.main_window)
|
||||
choice.yesButton.setText("是")
|
||||
|
||||
@@ -21,40 +21,55 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主业务定时器
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from PySide6.QtCore import QTimer
|
||||
from PySide6.QtCore import QObject, QTimer
|
||||
from datetime import datetime
|
||||
import pyautogui
|
||||
import keyboard
|
||||
|
||||
from .logger import logger
|
||||
from .config import Config
|
||||
from .task_manager import TaskManager
|
||||
from app.services import System
|
||||
|
||||
|
||||
class _MainTimer(QWidget):
|
||||
class _MainTimer(QObject):
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.if_FailSafeException = False
|
||||
|
||||
self.Timer = QTimer()
|
||||
self.Timer.timeout.connect(self.timed_start)
|
||||
self.Timer.timeout.connect(self.set_silence)
|
||||
self.Timer.start(1000)
|
||||
self.Timer.timeout.connect(self.check_power)
|
||||
|
||||
self.LongTimer = QTimer()
|
||||
self.LongTimer.timeout.connect(self.long_timed_task)
|
||||
|
||||
def start(self):
|
||||
"""启动定时器"""
|
||||
|
||||
logger.info("启动主定时器", module="主业务定时器")
|
||||
self.Timer.start(1000)
|
||||
self.LongTimer.start(3600000)
|
||||
|
||||
def stop(self):
|
||||
"""停止定时器"""
|
||||
|
||||
logger.info("停止主定时器", module="主业务定时器")
|
||||
self.Timer.stop()
|
||||
self.Timer.deleteLater()
|
||||
self.LongTimer.stop()
|
||||
self.LongTimer.deleteLater()
|
||||
|
||||
def long_timed_task(self):
|
||||
"""长时间定期检定任务"""
|
||||
|
||||
Config.get_gameid()
|
||||
logger.info("执行长时间定期检定任务", module="主业务定时器")
|
||||
|
||||
Config.get_stage()
|
||||
Config.main_window.setting.show_notice()
|
||||
if Config.get(Config.update_IfAutoUpdate):
|
||||
Config.main_window.setting.check_update()
|
||||
@@ -64,15 +79,15 @@ class _MainTimer(QWidget):
|
||||
|
||||
for name, info in Config.queue_dict.items():
|
||||
|
||||
if not info["Config"].get(info["Config"].queueSet_Enabled):
|
||||
if not info["Config"].get(info["Config"].QueueSet_TimeEnabled):
|
||||
continue
|
||||
|
||||
data = info["Config"].toDict()
|
||||
|
||||
time_set = [
|
||||
data["Time"][f"TimeSet_{_}"]
|
||||
data["Time"][f"Set_{_}"]
|
||||
for _ in range(10)
|
||||
if data["Time"][f"TimeEnabled_{_}"]
|
||||
if data["Time"][f"Enabled_{_}"]
|
||||
]
|
||||
# 按时间调起代理任务
|
||||
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
@@ -83,7 +98,7 @@ class _MainTimer(QWidget):
|
||||
and name not in Config.running_list
|
||||
):
|
||||
|
||||
logger.info(f"定时任务:{name}")
|
||||
logger.info(f"定时唤起任务:{name}。", module="主业务定时器")
|
||||
TaskManager.add_task("自动代理_新调度台", name, data)
|
||||
|
||||
def set_silence(self):
|
||||
@@ -96,22 +111,65 @@ class _MainTimer(QWidget):
|
||||
):
|
||||
|
||||
windows = System.get_window_info()
|
||||
if any(
|
||||
str(emulator_path) in window
|
||||
for window in windows
|
||||
for emulator_path in Config.silence_list
|
||||
):
|
||||
|
||||
emulator_windows = []
|
||||
for window in windows:
|
||||
for emulator_path, endtime in Config.silence_dict.items():
|
||||
if (
|
||||
datetime.now() < endtime
|
||||
and str(emulator_path) in window
|
||||
and window[0] != "新通知" # 此处排除雷电名为新通知的窗口
|
||||
):
|
||||
emulator_windows.append(window)
|
||||
|
||||
if emulator_windows:
|
||||
|
||||
logger.info(
|
||||
f"检测到模拟器窗口:{emulator_windows}", module="主业务定时器"
|
||||
)
|
||||
try:
|
||||
pyautogui.hotkey(
|
||||
*[
|
||||
keyboard.press_and_release(
|
||||
"+".join(
|
||||
_.strip().lower()
|
||||
for _ in Config.get(Config.function_BossKey).split("+")
|
||||
]
|
||||
)
|
||||
)
|
||||
except pyautogui.FailSafeException as e:
|
||||
if not self.if_FailSafeException:
|
||||
logger.warning(f"FailSafeException: {e}")
|
||||
self.if_FailSafeException = True
|
||||
logger.info(
|
||||
f"模拟按键:{Config.get(Config.function_BossKey)}",
|
||||
module="主业务定时器",
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"模拟按键时出错:{e}", module="主业务定时器")
|
||||
|
||||
def check_power(self):
|
||||
"""检查电源操作"""
|
||||
|
||||
if Config.power_sign != "NoAction" and not Config.running_list:
|
||||
|
||||
logger.info(f"触发电源操作:{Config.power_sign}", module="主业务定时器")
|
||||
|
||||
from app.ui import ProgressRingMessageBox
|
||||
|
||||
mode_book = {
|
||||
"KillSelf": "退出软件",
|
||||
"Sleep": "睡眠",
|
||||
"Hibernate": "休眠",
|
||||
"Shutdown": "关机",
|
||||
"ShutdownForce": "关机(强制)",
|
||||
}
|
||||
|
||||
choice = ProgressRingMessageBox(
|
||||
Config.main_window, f"{mode_book[Config.power_sign]}倒计时"
|
||||
)
|
||||
if choice.exec():
|
||||
logger.info(
|
||||
f"确认执行电源操作:{Config.power_sign}", module="主业务定时器"
|
||||
)
|
||||
System.set_power(Config.power_sign)
|
||||
Config.set_power_sign("NoAction")
|
||||
else:
|
||||
logger.info(f"取消电源操作:{Config.power_sign}", module="主业务定时器")
|
||||
Config.set_power_sign("NoAction")
|
||||
|
||||
|
||||
MainTimer = _MainTimer()
|
||||
|
||||
1596
app/models/MAA.py
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA模组包
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
@@ -29,6 +29,7 @@ __version__ = "4.2.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .general import GeneralManager
|
||||
from .MAA import MaaManager
|
||||
|
||||
__all__ = ["MaaManager"]
|
||||
__all__ = ["GeneralManager", "MaaManager"]
|
||||
|
||||
1201
app/models/general.py
Normal file
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA服务包
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
@@ -32,5 +32,6 @@ __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"]
|
||||
__all__ = ["Notify", "Crypto", "System", "skland_sign_in"]
|
||||
|
||||
@@ -21,38 +21,52 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA通知服务
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from PySide6.QtWidgets import QWidget
|
||||
from PySide6.QtCore import Signal
|
||||
import requests
|
||||
import time
|
||||
from loguru import logger
|
||||
from plyer import notification
|
||||
import re
|
||||
import smtplib
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
import time
|
||||
from email.header import Header
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr
|
||||
from app.core import Config
|
||||
from pathlib import Path
|
||||
from typing import Union
|
||||
|
||||
import requests
|
||||
from PySide6.QtCore import QObject, Signal
|
||||
|
||||
from plyer import notification
|
||||
|
||||
from app.core import Config, logger
|
||||
from app.services.security import Crypto
|
||||
from app.utils.ImageUtils import ImageUtils
|
||||
|
||||
|
||||
class Notification(QWidget):
|
||||
class Notification(QObject):
|
||||
|
||||
push_info_bar = Signal(str, str, str, int)
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
def push_plyer(self, title, message, ticker, t):
|
||||
"""推送系统通知"""
|
||||
def push_plyer(self, title, message, ticker, t) -> bool:
|
||||
"""
|
||||
推送系统通知
|
||||
|
||||
:param title: 通知标题
|
||||
:param message: 通知内容
|
||||
:param ticker: 通知横幅
|
||||
:param t: 通知持续时间
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
if Config.get(Config.notify_IfPushPlyer):
|
||||
|
||||
logger.info(f"推送系统通知:{title}", module="通知服务")
|
||||
|
||||
notification.notify(
|
||||
title=title,
|
||||
message=message,
|
||||
@@ -65,221 +79,364 @@ class Notification(QWidget):
|
||||
|
||||
return True
|
||||
|
||||
def send_mail(self, mode, title, content) -> None:
|
||||
"""推送邮件通知"""
|
||||
def send_mail(self, mode, title, content, to_address) -> None:
|
||||
"""
|
||||
推送邮件通知
|
||||
|
||||
if Config.get(Config.notify_IfSendMail):
|
||||
:param mode: 邮件内容模式,支持 "文本" 和 "网页"
|
||||
:param title: 邮件标题
|
||||
:param content: 邮件内容
|
||||
:param to_address: 收件人地址
|
||||
"""
|
||||
|
||||
if (
|
||||
Config.get(Config.notify_SMTPServerAddress) == ""
|
||||
or Config.get(Config.notify_AuthorizationCode) == ""
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get(Config.notify_FromAddress),
|
||||
)
|
||||
if (
|
||||
Config.get(Config.notify_SMTPServerAddress) == ""
|
||||
or Config.get(Config.notify_AuthorizationCode) == ""
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get(Config.notify_FromAddress),
|
||||
)
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
Config.get(Config.notify_ToAddress),
|
||||
)
|
||||
)
|
||||
or not bool(
|
||||
re.match(
|
||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
||||
to_address,
|
||||
)
|
||||
):
|
||||
)
|
||||
):
|
||||
logger.error(
|
||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
|
||||
module="通知服务",
|
||||
)
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"邮件通知推送异常",
|
||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
|
||||
-1,
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 定义邮件正文
|
||||
if mode == "文本":
|
||||
message = MIMEText(content, "plain", "utf-8")
|
||||
elif mode == "网页":
|
||||
message = MIMEMultipart("alternative")
|
||||
message["From"] = formataddr(
|
||||
(
|
||||
Header("AUTO_MAA通知服务", "utf-8").encode(),
|
||||
Config.get(Config.notify_FromAddress),
|
||||
)
|
||||
) # 发件人显示的名字
|
||||
message["To"] = formataddr(
|
||||
(Header("AUTO_MAA用户", "utf-8").encode(), to_address)
|
||||
) # 收件人显示的名字
|
||||
message["Subject"] = Header(title, "utf-8")
|
||||
|
||||
if mode == "网页":
|
||||
message.attach(MIMEText(content, "html", "utf-8"))
|
||||
|
||||
smtpObj = smtplib.SMTP_SSL(Config.get(Config.notify_SMTPServerAddress), 465)
|
||||
smtpObj.login(
|
||||
Config.get(Config.notify_FromAddress),
|
||||
Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)),
|
||||
)
|
||||
smtpObj.sendmail(
|
||||
Config.get(Config.notify_FromAddress), to_address, message.as_string()
|
||||
)
|
||||
smtpObj.quit()
|
||||
logger.success(f"邮件发送成功:{title}", module="通知服务")
|
||||
except Exception as e:
|
||||
logger.exception(f"发送邮件时出错:{e}", module="通知服务")
|
||||
self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1)
|
||||
|
||||
def ServerChanPush(
|
||||
self, title, content, send_key, tag, channel
|
||||
) -> Union[bool, str]:
|
||||
"""
|
||||
使用Server酱推送通知
|
||||
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param send_key: Server酱的SendKey
|
||||
:param tag: 通知标签
|
||||
:param channel: 通知频道
|
||||
:return: bool or str
|
||||
"""
|
||||
|
||||
if not send_key:
|
||||
logger.error("请正确设置Server酱的SendKey", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 构造 URL
|
||||
if send_key.startswith("sctp"):
|
||||
match = re.match(r"^sctp(\d+)t", send_key)
|
||||
if match:
|
||||
url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send"
|
||||
else:
|
||||
raise ValueError("SendKey 格式错误(sctp)")
|
||||
else:
|
||||
url = f"https://sctapi.ftqq.com/{send_key}.send"
|
||||
|
||||
# 构建 tags 和 channel
|
||||
def is_valid(s):
|
||||
return s == "" or (
|
||||
s == "|".join(s.split("|"))
|
||||
and (s.count("|") == 0 or all(s.split("|")))
|
||||
)
|
||||
|
||||
tags = "|".join(_.strip() for _ in tag.split("|"))
|
||||
channels = "|".join(_.strip() for _ in channel.split("|"))
|
||||
|
||||
options = {}
|
||||
if is_valid(tags):
|
||||
options["tags"] = tags
|
||||
else:
|
||||
logger.warning("Server酱 Tag 配置不正确,将被忽略", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"warning",
|
||||
"Server酱通知推送异常",
|
||||
"请正确设置 ServerChan 的 Tag",
|
||||
-1,
|
||||
)
|
||||
|
||||
if is_valid(channels):
|
||||
options["channel"] = channels
|
||||
else:
|
||||
logger.warning(
|
||||
"Server酱 Channel 配置不正确,将被忽略", module="通知服务"
|
||||
)
|
||||
self.push_info_bar.emit(
|
||||
"warning",
|
||||
"Server酱通知推送异常",
|
||||
"请正确设置 ServerChan 的 Channel",
|
||||
-1,
|
||||
)
|
||||
|
||||
# 请求发送
|
||||
params = {"title": title, "desp": content, **options}
|
||||
headers = {"Content-Type": "application/json;charset=utf-8"}
|
||||
|
||||
response = requests.post(
|
||||
url,
|
||||
json=params,
|
||||
headers=headers,
|
||||
timeout=10,
|
||||
proxies={
|
||||
"http": Config.get(Config.update_ProxyAddress),
|
||||
"https": Config.get(Config.update_ProxyAddress),
|
||||
},
|
||||
)
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 0:
|
||||
logger.success(f"Server酱推送通知成功:{title}", module="通知服务")
|
||||
return True
|
||||
else:
|
||||
error_code = result.get("code", "-1")
|
||||
logger.exception(
|
||||
f"Server酱通知推送失败:响应码:{error_code}", module="通知服务"
|
||||
)
|
||||
self.push_info_bar.emit(
|
||||
"error", "Server酱通知推送失败", f"响应码:{error_code}", -1
|
||||
)
|
||||
return f"Server酱通知推送失败:{error_code}"
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"Server酱通知推送异常:{e}", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"Server酱通知推送异常",
|
||||
"请检查相关设置和网络连接。如全部配置正确,请稍后再试。",
|
||||
-1,
|
||||
)
|
||||
return f"Server酱通知推送异常:{str(e)}"
|
||||
|
||||
def CompanyWebHookBotPush(self, title, content, webhook_url) -> Union[bool, str]:
|
||||
"""
|
||||
使用企业微信群机器人推送通知
|
||||
|
||||
:param title: 通知标题
|
||||
:param content: 通知内容
|
||||
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||
:return: bool or str
|
||||
"""
|
||||
|
||||
if webhook_url == "":
|
||||
logger.error("请正确设置企业微信群机器人的WebHook地址", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送异常",
|
||||
"请正确设置企业微信群机器人的WebHook地址",
|
||||
-1,
|
||||
)
|
||||
return None
|
||||
|
||||
content = f"{title}\n{content}"
|
||||
data = {"msgtype": "text", "text": {"content": content}}
|
||||
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.post(
|
||||
url=webhook_url,
|
||||
json=data,
|
||||
timeout=10,
|
||||
proxies={
|
||||
"http": Config.get(Config.update_ProxyAddress),
|
||||
"https": Config.get(Config.update_ProxyAddress),
|
||||
},
|
||||
)
|
||||
info = response.json()
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
logger.error(f"推送企业微信群机器人时出错:{err}", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送失败",
|
||||
f"使用企业微信群机器人推送通知时出错:{err}",
|
||||
-1,
|
||||
)
|
||||
return None
|
||||
|
||||
if info["errcode"] == 0:
|
||||
logger.success(f"企业微信群机器人推送通知成功:{title}", module="通知服务")
|
||||
return True
|
||||
else:
|
||||
logger.error(f"企业微信群机器人推送通知失败:{info}", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送失败",
|
||||
f"使用企业微信群机器人推送通知时出错:{err}",
|
||||
-1,
|
||||
)
|
||||
return f"使用企业微信群机器人推送通知时出错:{err}"
|
||||
|
||||
def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> bool:
|
||||
"""
|
||||
使用企业微信群机器人推送图片通知
|
||||
|
||||
:param image_path: 图片文件路径
|
||||
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||
:return: bool
|
||||
"""
|
||||
|
||||
try:
|
||||
# 压缩图片
|
||||
ImageUtils.compress_image_if_needed(image_path)
|
||||
|
||||
# 检查图片是否存在
|
||||
if not image_path.exists():
|
||||
logger.error(
|
||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
|
||||
"图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确",
|
||||
module="通知服务",
|
||||
)
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"邮件通知推送异常",
|
||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址",
|
||||
"企业微信群机器人通知推送异常",
|
||||
"图片不存在或者压缩失败,请检查图片路径是否正确",
|
||||
-1,
|
||||
)
|
||||
return None
|
||||
return False
|
||||
|
||||
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(),
|
||||
Config.get(Config.notify_ToAddress),
|
||||
)
|
||||
) # 收件人显示的名字
|
||||
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,
|
||||
if not webhook_url:
|
||||
logger.error(
|
||||
"请正确设置企业微信群机器人的WebHook地址", module="通知服务"
|
||||
)
|
||||
smtpObj.login(
|
||||
Config.get(Config.notify_FromAddress),
|
||||
Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)),
|
||||
)
|
||||
smtpObj.sendmail(
|
||||
Config.get(Config.notify_FromAddress),
|
||||
Config.get(Config.notify_ToAddress),
|
||||
message.as_string(),
|
||||
)
|
||||
smtpObj.quit()
|
||||
logger.success("邮件发送成功")
|
||||
except Exception as e:
|
||||
logger.error(f"发送邮件时出错:\n{e}")
|
||||
self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1)
|
||||
|
||||
def ServerChanPush(self, title, content):
|
||||
"""使用Server酱推送通知(支持 tag 和 channel,避免使用SDK)"""
|
||||
if Config.get(Config.notify_IfServerChan):
|
||||
send_key = Config.get(Config.notify_ServerChanKey)
|
||||
|
||||
if not send_key:
|
||||
logger.error("请正确设置Server酱的SendKey")
|
||||
self.push_info_bar.emit(
|
||||
"error", "Server酱通知推送异常", "请正确设置Server酱的SendKey", -1
|
||||
)
|
||||
return None
|
||||
|
||||
try:
|
||||
# 构造 URL
|
||||
if send_key.startswith("sctp"):
|
||||
match = re.match(r"^sctp(\d+)t", send_key)
|
||||
if match:
|
||||
url = f"https://{match.group(1)}.push.ft07.com/send/{send_key}.send"
|
||||
else:
|
||||
raise ValueError("SendKey 格式错误(sctp)")
|
||||
else:
|
||||
url = f"https://sctapi.ftqq.com/{send_key}.send"
|
||||
|
||||
# 构建 tags 和 channel
|
||||
def is_valid(s):
|
||||
return s == "" or (
|
||||
s == "|".join(s.split("|"))
|
||||
and (s.count("|") == 0 or all(s.split("|")))
|
||||
)
|
||||
|
||||
tags = "|".join(
|
||||
_.strip()
|
||||
for _ in Config.get(Config.notify_ServerChanTag).split("|")
|
||||
)
|
||||
channels = "|".join(
|
||||
_.strip()
|
||||
for _ in Config.get(Config.notify_ServerChanChannel).split("|")
|
||||
)
|
||||
|
||||
options = {}
|
||||
if is_valid(tags):
|
||||
options["tags"] = tags
|
||||
else:
|
||||
logger.warning("Server酱 Tag 配置不正确,将被忽略")
|
||||
self.push_info_bar.emit(
|
||||
"warning",
|
||||
"Server酱通知推送异常",
|
||||
"请正确设置 ServerChan 的 Tag",
|
||||
-1,
|
||||
)
|
||||
|
||||
if is_valid(channels):
|
||||
options["channel"] = channels
|
||||
else:
|
||||
logger.warning("Server酱 Channel 配置不正确,将被忽略")
|
||||
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)
|
||||
result = response.json()
|
||||
|
||||
if result.get("code") == 0:
|
||||
logger.info("Server酱推送通知成功")
|
||||
return True
|
||||
else:
|
||||
error_code = result.get("code", "-1")
|
||||
logger.error(f"Server酱通知推送失败:响应码:{error_code}")
|
||||
self.push_info_bar.emit(
|
||||
"error", "Server酱通知推送失败", f"响应码:{error_code}", -1
|
||||
)
|
||||
return f"Server酱通知推送失败:{error_code}"
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Server酱通知推送异常")
|
||||
self.push_info_bar.emit(
|
||||
"error", "Server酱通知推送异常", f"请检查相关设置,如还有问题可联系开发者", -1
|
||||
)
|
||||
return f"Server酱通知推送异常:{str(e)}"
|
||||
|
||||
def CompanyWebHookBotPush(self, title, content):
|
||||
"""使用企业微信群机器人推送通知"""
|
||||
if Config.get(Config.notify_IfCompanyWebHookBot):
|
||||
|
||||
if Config.get(Config.notify_CompanyWebHookBotUrl) == "":
|
||||
logger.error("请正确设置企业微信群机器人的WebHook地址")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送异常",
|
||||
"请正确设置企业微信群机器人的WebHook地址",
|
||||
-1,
|
||||
)
|
||||
return None
|
||||
return False
|
||||
|
||||
# 获取图片base64和md5
|
||||
try:
|
||||
image_base64 = ImageUtils.get_base64_from_file(str(image_path))
|
||||
image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
|
||||
except Exception as e:
|
||||
logger.exception(f"图片编码或MD5计算失败:{e}", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送异常",
|
||||
f"图片编码或MD5计算失败:{e}",
|
||||
-1,
|
||||
)
|
||||
return False
|
||||
|
||||
data = {
|
||||
"msgtype": "image",
|
||||
"image": {"base64": image_base64, "md5": image_md5},
|
||||
}
|
||||
|
||||
content = f"{title}\n{content}"
|
||||
data = {"msgtype": "text", "text": {"content": content}}
|
||||
# 从远程服务器获取最新主题图像
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.post(
|
||||
url=Config.get(Config.notify_CompanyWebHookBotUrl),
|
||||
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:
|
||||
except requests.RequestException as e:
|
||||
err = e
|
||||
logger.exception(
|
||||
f"推送企业微信群机器人图片第{_+1}次失败:{e}", module="通知服务"
|
||||
)
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
logger.error(f"推送企业微信群机器人时出错:{err}")
|
||||
logger.error("推送企业微信群机器人图片时出错", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送失败",
|
||||
f'使用企业微信群机器人推送通知时出错:{info["errmsg"]}',
|
||||
"企业微信群机器人图片推送失败",
|
||||
f"使用企业微信群机器人推送图片时出错:{err}",
|
||||
-1,
|
||||
)
|
||||
return None
|
||||
return False
|
||||
|
||||
if info["errcode"] == 0:
|
||||
logger.info("企业微信群机器人推送通知成功")
|
||||
if info.get("errcode") == 0:
|
||||
logger.success(
|
||||
f"企业微信群机器人推送图片成功:{image_path.name}",
|
||||
module="通知服务",
|
||||
)
|
||||
return True
|
||||
else:
|
||||
logger.error(f"企业微信群机器人推送通知失败:{info}")
|
||||
logger.error(f"企业微信群机器人推送图片失败:{info}", module="通知服务")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人通知推送失败",
|
||||
f'使用企业微信群机器人推送通知时出错:{info["errmsg"]}',
|
||||
"企业微信群机器人图片推送失败",
|
||||
f"使用企业微信群机器人推送图片时出错:{info}",
|
||||
-1,
|
||||
)
|
||||
return f'使用企业微信群机器人推送通知时出错:{info["errmsg"]}'
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"推送企业微信群机器人图片时发生未知异常:{e}")
|
||||
self.push_info_bar.emit(
|
||||
"error",
|
||||
"企业微信群机器人图片推送失败",
|
||||
f"发生未知异常:{e}",
|
||||
-1,
|
||||
)
|
||||
return False
|
||||
|
||||
def send_test_notification(self):
|
||||
"""发送测试通知到所有已启用的通知渠道"""
|
||||
|
||||
logger.info("发送测试通知到所有已启用的通知渠道", module="通知服务")
|
||||
|
||||
# 发送系统通知
|
||||
self.push_plyer(
|
||||
"测试通知",
|
||||
@@ -294,6 +451,7 @@ class Notification(QWidget):
|
||||
"文本",
|
||||
"AUTO_MAA测试通知",
|
||||
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
||||
Config.get(Config.notify_ToAddress),
|
||||
)
|
||||
|
||||
# 发送Server酱通知
|
||||
@@ -301,6 +459,9 @@ class Notification(QWidget):
|
||||
self.ServerChanPush(
|
||||
"AUTO_MAA测试通知",
|
||||
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
||||
Config.get(Config.notify_ServerChanKey),
|
||||
Config.get(Config.notify_ServerChanTag),
|
||||
Config.get(Config.notify_ServerChanChannel),
|
||||
)
|
||||
|
||||
# 发送企业微信机器人通知
|
||||
@@ -308,7 +469,14 @@ class Notification(QWidget):
|
||||
self.CompanyWebHookBotPush(
|
||||
"AUTO_MAA测试通知",
|
||||
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
||||
Config.get(Config.notify_CompanyWebHookBotUrl),
|
||||
)
|
||||
Notify.CompanyWebHookBotPushImage(
|
||||
Config.app_path / "resources/images/notification/test_notify.png",
|
||||
Config.get(Config.notify_CompanyWebHookBotUrl),
|
||||
)
|
||||
|
||||
logger.info("测试通知发送完成", module="通知服务")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -21,22 +21,19 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA安全服务
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
import hashlib
|
||||
import random
|
||||
import secrets
|
||||
import base64
|
||||
import win32crypt
|
||||
from pathlib import Path
|
||||
from Crypto.Cipher import AES
|
||||
from Crypto.PublicKey import RSA
|
||||
from Crypto.Cipher import PKCS1_OAEP
|
||||
from Crypto.Util.Padding import pad, unpad
|
||||
from typing import List, Dict, Union
|
||||
|
||||
from app.core import Config
|
||||
|
||||
@@ -44,7 +41,12 @@ from app.core import Config
|
||||
class CryptoHandler:
|
||||
|
||||
def get_PASSWORD(self, PASSWORD: str) -> None:
|
||||
"""配置管理密钥"""
|
||||
"""
|
||||
配置管理密钥
|
||||
|
||||
:param PASSWORD: 管理密钥
|
||||
:type PASSWORD: str
|
||||
"""
|
||||
|
||||
# 生成目录
|
||||
Config.key_path.mkdir(parents=True, exist_ok=True)
|
||||
@@ -85,7 +87,12 @@ class CryptoHandler:
|
||||
(Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local)
|
||||
|
||||
def AUTO_encryptor(self, note: str) -> str:
|
||||
"""使用AUTO_MAA的算法加密数据"""
|
||||
"""
|
||||
使用AUTO_MAA的算法加密数据
|
||||
|
||||
:param note: 数据明文
|
||||
:type note: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
@@ -100,7 +107,16 @@ class CryptoHandler:
|
||||
return base64.b64encode(encrypted).decode("utf-8")
|
||||
|
||||
def AUTO_decryptor(self, note: str, PASSWORD: str) -> str:
|
||||
"""使用AUTO_MAA的算法解密数据"""
|
||||
"""
|
||||
使用AUTO_MAA的算法解密数据
|
||||
|
||||
:param note: 数据密文
|
||||
:type note: str
|
||||
:param PASSWORD: 管理密钥
|
||||
:type PASSWORD: str
|
||||
:return: 解密后的明文
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
@@ -142,32 +158,71 @@ class CryptoHandler:
|
||||
return note
|
||||
|
||||
def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None:
|
||||
"""修改管理密钥"""
|
||||
"""
|
||||
修改管理密钥
|
||||
|
||||
for member in Config.member_dict.values():
|
||||
:param PASSWORD_old: 旧管理密钥
|
||||
:type PASSWORD_old: str
|
||||
:param PASSWORD_new: 新管理密钥
|
||||
:type PASSWORD_new: str
|
||||
"""
|
||||
|
||||
for script in Config.script_dict.values():
|
||||
|
||||
# 使用旧管理密钥解密
|
||||
for user in member["UserData"].values():
|
||||
user["Password"] = self.AUTO_decryptor(
|
||||
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
|
||||
)
|
||||
if script["Type"] == "Maa":
|
||||
for user in script["UserData"].values():
|
||||
user["Password"] = self.AUTO_decryptor(
|
||||
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
|
||||
)
|
||||
|
||||
self.get_PASSWORD(PASSWORD_new)
|
||||
|
||||
for member in Config.member_dict.values():
|
||||
for script in Config.script_dict.values():
|
||||
|
||||
# 使用新管理密钥重新加密
|
||||
for user in member["UserData"].values():
|
||||
user["Config"].set(
|
||||
user["Config"].Info_Password, self.AUTO_encryptor(user["Password"])
|
||||
)
|
||||
user["Password"] = None
|
||||
del user["Password"]
|
||||
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加密数据"""
|
||||
"""
|
||||
使用Windows DPAPI加密数据
|
||||
|
||||
:param note: 数据明文
|
||||
:type note: str
|
||||
:param description: 描述信息
|
||||
:type description: str
|
||||
:param entropy: 随机熵
|
||||
:type entropy: bytes
|
||||
:return: 加密后的数据
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
@@ -178,7 +233,16 @@ class CryptoHandler:
|
||||
return base64.b64encode(encrypted).decode("utf-8")
|
||||
|
||||
def win_decryptor(self, note: str, entropy: bytes = None) -> str:
|
||||
"""使用Windows DPAPI解密数据"""
|
||||
"""
|
||||
使用Windows DPAPI解密数据
|
||||
|
||||
:param note: 数据密文
|
||||
:type note: str
|
||||
:param entropy: 随机熵
|
||||
:type entropy: bytes
|
||||
:return: 解密后的明文
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
@@ -188,21 +252,15 @@ class CryptoHandler:
|
||||
)
|
||||
return decrypted[1].decode("utf-8")
|
||||
|
||||
def search_member(self) -> List[Dict[str, Union[Path, list]]]:
|
||||
"""搜索所有脚本实例及其用户数据库路径"""
|
||||
|
||||
member_list = []
|
||||
|
||||
if (Config.app_path / "config/MaaConfig").exists():
|
||||
for subdir in (Config.app_path / "config/MaaConfig").iterdir():
|
||||
if subdir.is_dir():
|
||||
|
||||
member_list.append({"Path": subdir / "user_data.db"})
|
||||
|
||||
return member_list
|
||||
|
||||
def check_PASSWORD(self, PASSWORD: str) -> bool:
|
||||
"""验证管理密钥"""
|
||||
"""
|
||||
验证管理密钥
|
||||
|
||||
:param PASSWORD: 管理密钥
|
||||
:type PASSWORD: str
|
||||
:return: 是否验证通过
|
||||
:rtype: bool
|
||||
"""
|
||||
|
||||
return bool(
|
||||
self.AUTO_decryptor(self.AUTO_encryptor("-"), PASSWORD) != "管理密钥错误"
|
||||
|
||||
285
app/services/skland.py
Normal file
@@ -0,0 +1,285 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
|
||||
# This file incorporates work covered by the following copyright and
|
||||
# permission notice:
|
||||
#
|
||||
# skland-checkin-ghaction Copyright © 2023 Yanstory
|
||||
# https://github.com/Yanstory/skland-checkin-ghaction
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA森空岛服务
|
||||
v4.4
|
||||
作者:DLmaster_361、ClozyA
|
||||
"""
|
||||
|
||||
import time
|
||||
import json
|
||||
import hmac
|
||||
import hashlib
|
||||
import requests
|
||||
from urllib import parse
|
||||
|
||||
from app.core import Config, logger
|
||||
|
||||
|
||||
def skland_sign_in(token) -> dict:
|
||||
"""森空岛签到"""
|
||||
|
||||
app_code = "4ca99fa6b56cc2ba"
|
||||
# 用于获取grant code
|
||||
grant_code_url = "https://as.hypergryph.com/user/oauth2/v2/grant"
|
||||
# 用于获取cred
|
||||
cred_code_url = "https://zonai.skland.com/api/v1/user/auth/generate_cred_by_code"
|
||||
# 查询角色绑定
|
||||
binding_url = "https://zonai.skland.com/api/v1/game/player/binding"
|
||||
# 签到接口
|
||||
sign_url = "https://zonai.skland.com/api/v1/game/attendance"
|
||||
|
||||
# 基础请求头
|
||||
header = {
|
||||
"cred": "",
|
||||
"User-Agent": "Skland/1.5.1 (com.hypergryph.skland; build:100501001; Android 34;) Okhttp/4.11.0",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Connection": "close",
|
||||
}
|
||||
header_login = header.copy()
|
||||
header_for_sign = {
|
||||
"platform": "1",
|
||||
"timestamp": "",
|
||||
"dId": "",
|
||||
"vName": "1.5.1",
|
||||
}
|
||||
|
||||
def generate_signature(token_for_sign: str, path, body_or_query):
|
||||
"""
|
||||
生成请求签名
|
||||
|
||||
:param token_for_sign: 用于加密的token
|
||||
:param path: 请求路径(如 /api/v1/game/player/binding)
|
||||
:param body_or_query: GET用query字符串,POST用body字符串
|
||||
:return: (sign, 新的header_for_sign字典)
|
||||
"""
|
||||
|
||||
t = str(int(time.time()) - 2) # 时间戳,-2秒以防服务器时间不一致
|
||||
token_bytes = token_for_sign.encode("utf-8")
|
||||
header_ca = dict(header_for_sign)
|
||||
header_ca["timestamp"] = t
|
||||
header_ca_str = json.dumps(header_ca, separators=(",", ":"))
|
||||
s = path + body_or_query + t + header_ca_str # 拼接原始字符串
|
||||
# HMAC-SHA256 + MD5得到最终sign
|
||||
hex_s = hmac.new(token_bytes, s.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||
md5 = hashlib.md5(hex_s.encode("utf-8")).hexdigest()
|
||||
return md5, header_ca
|
||||
|
||||
def get_sign_header(url: str, method, body, old_header, sign_token):
|
||||
"""
|
||||
获取带签名的请求头
|
||||
|
||||
:param url: 请求完整url
|
||||
:param method: 请求方式 GET/POST
|
||||
:param body: POST请求体或GET时为None
|
||||
:param old_header: 原始请求头
|
||||
:param sign_token: 当前会话的签名token
|
||||
:return: 新请求头
|
||||
"""
|
||||
|
||||
h = json.loads(json.dumps(old_header))
|
||||
p = parse.urlparse(url)
|
||||
if method.lower() == "get":
|
||||
sign, header_ca = generate_signature(sign_token, p.path, p.query)
|
||||
else:
|
||||
sign, header_ca = generate_signature(
|
||||
sign_token, p.path, json.dumps(body) if body else ""
|
||||
)
|
||||
h["sign"] = sign
|
||||
for i in header_ca:
|
||||
h[i] = header_ca[i]
|
||||
return h
|
||||
|
||||
def copy_header(cred):
|
||||
"""
|
||||
复制请求头并添加cred
|
||||
|
||||
:param cred: 当前会话的cred
|
||||
:return: 新的请求头
|
||||
"""
|
||||
v = json.loads(json.dumps(header))
|
||||
v["cred"] = cred
|
||||
return v
|
||||
|
||||
def login_by_token(token_code):
|
||||
"""
|
||||
使用token一步步拿到cred和sign_token
|
||||
|
||||
:param token_code: 你的skyland token
|
||||
:return: (cred, sign_token)
|
||||
"""
|
||||
try:
|
||||
# token为json对象时提取data.content
|
||||
t = json.loads(token_code)
|
||||
token_code = t["data"]["content"]
|
||||
except:
|
||||
pass
|
||||
grant_code = get_grant_code(token_code)
|
||||
return get_cred(grant_code)
|
||||
|
||||
def get_cred(grant):
|
||||
"""
|
||||
通过grant code获取cred和sign_token
|
||||
|
||||
:param grant: grant code
|
||||
:return: (cred, sign_token)
|
||||
"""
|
||||
|
||||
rsp = requests.post(
|
||||
cred_code_url,
|
||||
json={"code": grant, "kind": 1},
|
||||
headers=header_login,
|
||||
proxies={
|
||||
"http": Config.get(Config.update_ProxyAddress),
|
||||
"https": Config.get(Config.update_ProxyAddress),
|
||||
},
|
||||
).json()
|
||||
if rsp["code"] != 0:
|
||||
raise Exception(f'获得cred失败:{rsp.get("messgae")}')
|
||||
sign_token = rsp["data"]["token"]
|
||||
cred = rsp["data"]["cred"]
|
||||
return cred, sign_token
|
||||
|
||||
def get_grant_code(token):
|
||||
"""
|
||||
通过token获取grant code
|
||||
|
||||
:param token: 你的skyland token
|
||||
:return: grant code
|
||||
"""
|
||||
rsp = requests.post(
|
||||
grant_code_url,
|
||||
json={"appCode": app_code, "token": token, "type": 0},
|
||||
headers=header_login,
|
||||
proxies={
|
||||
"http": Config.get(Config.update_ProxyAddress),
|
||||
"https": Config.get(Config.update_ProxyAddress),
|
||||
},
|
||||
).json()
|
||||
if rsp["status"] != 0:
|
||||
raise Exception(
|
||||
f'使用token: {token[:3]}******{token[-3:]} 获得认证代码失败:{rsp.get("msg")}'
|
||||
)
|
||||
return rsp["data"]["code"]
|
||||
|
||||
def get_binding_list(cred, sign_token):
|
||||
"""
|
||||
查询已绑定的角色列表
|
||||
|
||||
:param cred: 当前cred
|
||||
:param sign_token: 当前sign_token
|
||||
:return: 角色列表
|
||||
"""
|
||||
v = []
|
||||
rsp = requests.get(
|
||||
binding_url,
|
||||
headers=get_sign_header(
|
||||
binding_url, "get", None, copy_header(cred), sign_token
|
||||
),
|
||||
proxies={
|
||||
"http": Config.get(Config.update_ProxyAddress),
|
||||
"https": Config.get(Config.update_ProxyAddress),
|
||||
},
|
||||
).json()
|
||||
if rsp["code"] != 0:
|
||||
logger.error(
|
||||
f"森空岛服务 | 请求角色列表出现问题:{rsp['message']}",
|
||||
module="森空岛签到",
|
||||
)
|
||||
if rsp.get("message") == "用户未登录":
|
||||
logger.error(
|
||||
f"森空岛服务 | 用户登录可能失效了,请重新登录!",
|
||||
module="森空岛签到",
|
||||
)
|
||||
return v
|
||||
# 只取明日方舟(arknights)的绑定账号
|
||||
for i in rsp["data"]["list"]:
|
||||
if i.get("appCode") != "arknights":
|
||||
continue
|
||||
v.extend(i.get("bindingList"))
|
||||
return v
|
||||
|
||||
def do_sign(cred, sign_token) -> dict:
|
||||
"""
|
||||
对所有绑定的角色进行签到
|
||||
|
||||
:param cred: 当前cred
|
||||
:param sign_token: 当前sign_token
|
||||
:return: 签到结果字典
|
||||
"""
|
||||
|
||||
characters = get_binding_list(cred, sign_token)
|
||||
result = {"成功": [], "重复": [], "失败": [], "总计": len(characters)}
|
||||
|
||||
for character in characters:
|
||||
|
||||
body = {
|
||||
"uid": character.get("uid"),
|
||||
"gameId": character.get("channelMasterId"),
|
||||
}
|
||||
rsp = requests.post(
|
||||
sign_url,
|
||||
headers=get_sign_header(
|
||||
sign_url, "post", body, copy_header(cred), sign_token
|
||||
),
|
||||
json=body,
|
||||
proxies={
|
||||
"http": Config.get(Config.update_ProxyAddress),
|
||||
"https": Config.get(Config.update_ProxyAddress),
|
||||
},
|
||||
).json()
|
||||
|
||||
if rsp["code"] != 0:
|
||||
|
||||
result[
|
||||
"重复" if rsp.get("message") == "请勿重复签到!" else "失败"
|
||||
].append(
|
||||
f"{character.get("nickName")}({character.get("channelName")})"
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
result["成功"].append(
|
||||
f"{character.get("nickName")}({character.get("channelName")})"
|
||||
)
|
||||
|
||||
time.sleep(3)
|
||||
|
||||
return result
|
||||
|
||||
# 主流程
|
||||
try:
|
||||
# 拿到cred和sign_token
|
||||
cred, sign_token = login_by_token(token)
|
||||
time.sleep(1)
|
||||
# 依次签到
|
||||
return do_sign(cred, sign_token)
|
||||
except Exception as e:
|
||||
logger.exception(f"森空岛服务 | 森空岛签到失败: {e}", module="森空岛签到")
|
||||
return {"成功": [], "重复": [], "失败": [], "总计": 0}
|
||||
@@ -21,22 +21,23 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA系统服务
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QApplication, QWidget
|
||||
from PySide6.QtWidgets import QApplication
|
||||
import sys
|
||||
import ctypes
|
||||
import win32gui
|
||||
import win32process
|
||||
import winreg
|
||||
import psutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import getpass
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app.core import Config
|
||||
from app.core import Config, logger
|
||||
|
||||
|
||||
class _SystemHandler:
|
||||
@@ -65,100 +66,237 @@ class _SystemHandler:
|
||||
"""同步开机自启"""
|
||||
|
||||
if Config.get(Config.start_IfSelfStart) and not self.is_startup():
|
||||
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.SetValueEx(key, "AUTO_MAA", 0, winreg.REG_SZ, Config.app_path_sys)
|
||||
winreg.CloseKey(key)
|
||||
|
||||
# 创建任务计划
|
||||
try:
|
||||
|
||||
# 获取当前用户和时间
|
||||
current_user = getpass.getuser()
|
||||
current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||
|
||||
# XML 模板
|
||||
xml_content = f"""<?xml version="1.0" encoding="UTF-16"?>
|
||||
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
||||
<RegistrationInfo>
|
||||
<Date>{current_time}</Date>
|
||||
<Author>{current_user}</Author>
|
||||
<Description>AUTO_MAA自启动服务</Description>
|
||||
<URI>\\AUTO_MAA_AutoStart</URI>
|
||||
</RegistrationInfo>
|
||||
<Triggers>
|
||||
<LogonTrigger>
|
||||
<StartBoundary>{current_time}</StartBoundary>
|
||||
<Enabled>true</Enabled>
|
||||
</LogonTrigger>
|
||||
</Triggers>
|
||||
<Principals>
|
||||
<Principal id="Author">
|
||||
<LogonType>InteractiveToken</LogonType>
|
||||
<RunLevel>HighestAvailable</RunLevel>
|
||||
</Principal>
|
||||
</Principals>
|
||||
<Settings>
|
||||
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
||||
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
||||
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
||||
<AllowHardTerminate>false</AllowHardTerminate>
|
||||
<StartWhenAvailable>true</StartWhenAvailable>
|
||||
<RunOnlyIfNetworkAvailable>false</RunOnlyIfNetworkAvailable>
|
||||
<IdleSettings>
|
||||
<StopOnIdleEnd>false</StopOnIdleEnd>
|
||||
<RestartOnIdle>false</RestartOnIdle>
|
||||
</IdleSettings>
|
||||
<AllowStartOnDemand>true</AllowStartOnDemand>
|
||||
<Enabled>true</Enabled>
|
||||
<Hidden>false</Hidden>
|
||||
<RunOnlyIfIdle>false</RunOnlyIfIdle>
|
||||
<WakeToRun>false</WakeToRun>
|
||||
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
||||
<Priority>7</Priority>
|
||||
</Settings>
|
||||
<Actions Context="Author">
|
||||
<Exec>
|
||||
<Command>"{Config.app_path_sys}"</Command>
|
||||
</Exec>
|
||||
</Actions>
|
||||
</Task>"""
|
||||
|
||||
# 创建临时 XML 文件并执行
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".xml", delete=False, encoding="utf-16"
|
||||
) as f:
|
||||
f.write(xml_content)
|
||||
xml_file = f.name
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
[
|
||||
"schtasks",
|
||||
"/create",
|
||||
"/tn",
|
||||
"AUTO_MAA_AutoStart",
|
||||
"/xml",
|
||||
xml_file,
|
||||
"/f",
|
||||
],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.success(
|
||||
f"程序自启动任务计划已创建: {Config.app_path_sys}",
|
||||
module="系统服务",
|
||||
)
|
||||
else:
|
||||
logger.error(
|
||||
f"程序自启动任务计划创建失败: {result.stderr}",
|
||||
module="系统服务",
|
||||
)
|
||||
|
||||
finally:
|
||||
# 删除临时文件
|
||||
try:
|
||||
Path(xml_file).unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"程序自启动任务计划创建失败: {e}", module="系统服务")
|
||||
|
||||
elif not Config.get(Config.start_IfSelfStart) and self.is_startup():
|
||||
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)
|
||||
|
||||
try:
|
||||
|
||||
result = subprocess.run(
|
||||
["schtasks", "/delete", "/tn", "AUTO_MAA_AutoStart", "/f"],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
|
||||
if result.returncode == 0:
|
||||
logger.success("程序自启动任务计划已删除", module="系统服务")
|
||||
else:
|
||||
logger.error(
|
||||
f"程序自启动任务计划删除失败: {result.stderr}",
|
||||
module="系统服务",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception(f"程序自启动任务计划删除失败: {e}", module="系统服务")
|
||||
|
||||
def set_power(self, mode) -> None:
|
||||
"""
|
||||
执行系统电源操作
|
||||
|
||||
:param mode: 电源操作模式,支持 "NoAction", "Shutdown", "Hibernate", "Sleep", "KillSelf", "ShutdownForce"
|
||||
"""
|
||||
|
||||
if sys.platform.startswith("win"):
|
||||
|
||||
if mode == "None":
|
||||
if mode == "NoAction":
|
||||
|
||||
logger.info("不执行系统电源操作")
|
||||
logger.info("不执行系统电源操作", module="系统服务")
|
||||
|
||||
elif mode == "Shutdown":
|
||||
|
||||
logger.info("执行关机操作")
|
||||
self.kill_emulator_processes()
|
||||
logger.info("执行关机操作", module="系统服务")
|
||||
subprocess.run(["shutdown", "/s", "/t", "0"])
|
||||
|
||||
elif mode == "ShutdownForce":
|
||||
logger.info("执行强制关机操作", module="系统服务")
|
||||
subprocess.run(["shutdown", "/s", "/t", "0", "/f"])
|
||||
|
||||
elif mode == "Hibernate":
|
||||
|
||||
logger.info("执行休眠操作")
|
||||
logger.info("执行休眠操作", module="系统服务")
|
||||
subprocess.run(["shutdown", "/h"])
|
||||
|
||||
elif mode == "Sleep":
|
||||
|
||||
logger.info("执行睡眠操作")
|
||||
logger.info("执行睡眠操作", module="系统服务")
|
||||
subprocess.run(
|
||||
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
|
||||
)
|
||||
|
||||
elif mode == "KillSelf":
|
||||
|
||||
logger.info("执行退出主程序操作", module="系统服务")
|
||||
Config.main_window.close()
|
||||
QApplication.quit()
|
||||
sys.exit(0)
|
||||
|
||||
elif sys.platform.startswith("linux"):
|
||||
|
||||
if mode == "None":
|
||||
if mode == "NoAction":
|
||||
|
||||
logger.info("不执行系统电源操作")
|
||||
logger.info("不执行系统电源操作", module="系统服务")
|
||||
|
||||
elif mode == "Shutdown":
|
||||
|
||||
logger.info("执行关机操作")
|
||||
logger.info("执行关机操作", module="系统服务")
|
||||
subprocess.run(["shutdown", "-h", "now"])
|
||||
|
||||
elif mode == "Hibernate":
|
||||
|
||||
logger.info("执行休眠操作")
|
||||
logger.info("执行休眠操作", module="系统服务")
|
||||
subprocess.run(["systemctl", "hibernate"])
|
||||
|
||||
elif mode == "Sleep":
|
||||
|
||||
logger.info("执行睡眠操作")
|
||||
logger.info("执行睡眠操作", module="系统服务")
|
||||
subprocess.run(["systemctl", "suspend"])
|
||||
|
||||
elif mode == "KillSelf":
|
||||
|
||||
logger.info("执行退出主程序操作", module="系统服务")
|
||||
Config.main_window.close()
|
||||
QApplication.quit()
|
||||
sys.exit(0)
|
||||
|
||||
def kill_emulator_processes(self):
|
||||
"""这里暂时仅支持 MuMu 模拟器"""
|
||||
|
||||
logger.info("正在清除模拟器进程", module="系统服务")
|
||||
|
||||
keywords = ["Nemu", "nemu", "emulator", "MuMu"]
|
||||
for proc in psutil.process_iter(["pid", "name"]):
|
||||
try:
|
||||
pname = proc.info["name"].lower()
|
||||
if any(keyword.lower() in pname for keyword in keywords):
|
||||
proc.kill()
|
||||
logger.info(
|
||||
f"已关闭 MuMu 模拟器进程: {proc.info['name']}",
|
||||
module="系统服务",
|
||||
)
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied):
|
||||
continue
|
||||
|
||||
logger.success("模拟器进程清除完成", module="系统服务")
|
||||
|
||||
def is_startup(self) -> bool:
|
||||
"""判断程序是否已经开机自启"""
|
||||
|
||||
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)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
winreg.CloseKey(key)
|
||||
result = subprocess.run(
|
||||
["schtasks", "/query", "/tn", "AUTO_MAA_AutoStart"],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
return result.returncode == 0
|
||||
except Exception as e:
|
||||
logger.exception(f"检查任务计划程序失败: {e}", module="系统服务")
|
||||
return False
|
||||
|
||||
def get_window_info(self) -> list:
|
||||
"""获取当前窗口信息"""
|
||||
"""获取当前前台窗口信息"""
|
||||
|
||||
def callback(hwnd, window_info):
|
||||
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
|
||||
@@ -172,7 +310,13 @@ class _SystemHandler:
|
||||
return window_info
|
||||
|
||||
def kill_process(self, path: Path) -> None:
|
||||
"""根据路径中止进程"""
|
||||
"""
|
||||
根据路径中止进程
|
||||
|
||||
:param path: 进程路径
|
||||
"""
|
||||
|
||||
logger.info(f"开始中止进程: {path}", module="系统服务")
|
||||
|
||||
for pid in self.search_pids(path):
|
||||
killprocess = subprocess.Popen(
|
||||
@@ -182,8 +326,17 @@ class _SystemHandler:
|
||||
)
|
||||
killprocess.wait()
|
||||
|
||||
logger.success(f"进程已中止: {path}", module="系统服务")
|
||||
|
||||
def search_pids(self, path: Path) -> list:
|
||||
"""根据路径查找进程PID"""
|
||||
"""
|
||||
根据路径查找进程PID
|
||||
|
||||
:param path: 进程路径
|
||||
:return: 匹配的进程PID列表
|
||||
"""
|
||||
|
||||
logger.info(f"开始查找进程 PID: {path}", module="系统服务")
|
||||
|
||||
pids = []
|
||||
for proc in psutil.process_iter(["pid", "exe"]):
|
||||
|
||||
1052
app/ui/Widget.py
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA图形化界面包
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA调度中枢界面
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
@@ -33,8 +32,8 @@ from PySide6.QtWidgets import (
|
||||
QHBoxLayout,
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
BodyLabel,
|
||||
CardWidget,
|
||||
Pivot,
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
HeaderCardWidget,
|
||||
@@ -44,13 +43,12 @@ from qfluentwidgets import (
|
||||
SubtitleLabel,
|
||||
PushButton,
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QTextCursor
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
from app.core import Config, TaskManager, Task, MainInfoBar
|
||||
from .Widget import StatefulItemCard, ComboBoxMessageBox
|
||||
from app.core import Config, TaskManager, Task, MainInfoBar, logger
|
||||
from .Widget import StatefulItemCard, ComboBoxMessageBox, PivotArea
|
||||
|
||||
|
||||
class DispatchCenter(QWidget):
|
||||
@@ -60,13 +58,35 @@ class DispatchCenter(QWidget):
|
||||
|
||||
self.setObjectName("调度中枢")
|
||||
|
||||
self.pivot = Pivot(self)
|
||||
# 添加任务按钮
|
||||
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.Layout = QVBoxLayout(self)
|
||||
self.stackedWidget.setContentsMargins(0, 0, 0, 0)
|
||||
self.stackedWidget.setStyleSheet("background: transparent; border: none;")
|
||||
|
||||
self.script_list: Dict[str, DispatchBox] = {}
|
||||
self.script_list: Dict[str, DispatchCenter.DispatchBox] = {}
|
||||
|
||||
dispatch_box = DispatchBox("主调度台", self)
|
||||
# 添加主调度台
|
||||
dispatch_box = self.DispatchBox("主调度台", self)
|
||||
self.script_list["主调度台"] = dispatch_box
|
||||
self.stackedWidget.addWidget(self.script_list["主调度台"])
|
||||
self.pivot.addItem(
|
||||
@@ -76,7 +96,16 @@ class DispatchCenter(QWidget):
|
||||
icon=FluentIcon.CAFE,
|
||||
)
|
||||
|
||||
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
|
||||
# 顶栏组合
|
||||
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)
|
||||
|
||||
@@ -85,9 +114,15 @@ class DispatchCenter(QWidget):
|
||||
)
|
||||
|
||||
def add_board(self, task: Task) -> None:
|
||||
"""添加一个调度台界面"""
|
||||
"""
|
||||
为任务添加一个调度台界面并绑定信号
|
||||
|
||||
dispatch_box = DispatchBox(task.name, self)
|
||||
: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)
|
||||
@@ -106,24 +141,40 @@ class DispatchCenter(QWidget):
|
||||
|
||||
self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}")
|
||||
|
||||
logger.success(f"调度台 {task.name} 添加成功", module="调度中枢")
|
||||
|
||||
def del_board(self, name: str) -> None:
|
||||
"""删除指定子界面"""
|
||||
"""
|
||||
删除指定子界面
|
||||
|
||||
:param name: 子界面名称
|
||||
"""
|
||||
|
||||
logger.info(f"删除调度台:{name}", module="调度中枢")
|
||||
|
||||
self.pivot.setCurrentItem("主调度台")
|
||||
self.stackedWidget.removeWidget(self.script_list[name])
|
||||
self.script_list[name].deleteLater()
|
||||
self.script_list.pop(name)
|
||||
self.pivot.removeWidget(name)
|
||||
|
||||
logger.success(f"调度台 {name} 删除成功", module="调度中枢")
|
||||
|
||||
def connect_main_board(self, task: Task) -> None:
|
||||
"""连接主调度台"""
|
||||
"""
|
||||
将任务连接到主调度台
|
||||
|
||||
:param task: 任务对象
|
||||
"""
|
||||
|
||||
logger.info(f"主调度台载入任务:{task.name}", module="调度中枢")
|
||||
|
||||
self.script_list["主调度台"].top_bar.Lable.setText(
|
||||
f"{task.name} - {task.mode.replace("_主调度台","")}模式"
|
||||
f"{task.name} - {task.mode.replace('_主调度台','')}模式"
|
||||
)
|
||||
self.script_list["主调度台"].top_bar.Lable.show()
|
||||
self.script_list["主调度台"].top_bar.object.hide()
|
||||
self.script_list["主调度台"].top_bar.mode.hide()
|
||||
self.script_list["主调度台"].top_bar.multi_button.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(
|
||||
@@ -148,13 +199,21 @@ class DispatchCenter(QWidget):
|
||||
lambda logs: self.disconnect_main_board(task.name, logs)
|
||||
)
|
||||
|
||||
logger.success(f"主调度台成功载入:{task.name} ", module="调度中枢")
|
||||
|
||||
def disconnect_main_board(self, name: str, logs: list) -> None:
|
||||
"""断开主调度台"""
|
||||
"""
|
||||
断开主调度台
|
||||
|
||||
:param name: 任务名称
|
||||
:param logs: 任务日志列表
|
||||
"""
|
||||
|
||||
logger.info(f"主调度台断开任务:{name}", module="调度中枢")
|
||||
|
||||
self.script_list["主调度台"].top_bar.Lable.hide()
|
||||
self.script_list["主调度台"].top_bar.object.show()
|
||||
self.script_list["主调度台"].top_bar.mode.show()
|
||||
self.script_list["主调度台"].top_bar.multi_button.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(
|
||||
@@ -170,6 +229,8 @@ class DispatchCenter(QWidget):
|
||||
else:
|
||||
self.script_list["主调度台"].info.log_text.text.setText("没有任务被执行")
|
||||
|
||||
logger.success(f"主调度台成功断开:{name}", module="调度中枢")
|
||||
|
||||
def update_top_bar(self):
|
||||
"""更新顶栏"""
|
||||
|
||||
@@ -179,25 +240,25 @@ class DispatchCenter(QWidget):
|
||||
self.script_list["主调度台"].top_bar.object.addItem(
|
||||
(
|
||||
"队列"
|
||||
if info["Config"].get(info["Config"].queueSet_Name) == ""
|
||||
else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}"
|
||||
if info["Config"].get(info["Config"].QueueSet_Name) == ""
|
||||
else f"队列 - {info["Config"].get(info["Config"].QueueSet_Name)}"
|
||||
),
|
||||
userData=name,
|
||||
)
|
||||
|
||||
for name, info in Config.member_dict.items():
|
||||
for name, info in Config.script_dict.items():
|
||||
self.script_list["主调度台"].top_bar.object.addItem(
|
||||
(
|
||||
f"实例 - {info['Type']}"
|
||||
if info["Config"].get(info["Config"].MaaSet_Name) == ""
|
||||
else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}"
|
||||
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.member_dict) == 1:
|
||||
elif len(Config.script_dict) == 1:
|
||||
self.script_list["主调度台"].top_bar.object.setCurrentIndex(
|
||||
len(Config.queue_dict)
|
||||
)
|
||||
@@ -208,301 +269,357 @@ class DispatchCenter(QWidget):
|
||||
self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"])
|
||||
self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0)
|
||||
|
||||
def update_power_sign(self) -> None:
|
||||
"""更新电源设置"""
|
||||
|
||||
class DispatchBox(QWidget):
|
||||
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 __init__(self, name: str, parent=None):
|
||||
super().__init__(parent)
|
||||
def set_power_sign(self) -> None:
|
||||
"""设置所有任务完成后动作"""
|
||||
|
||||
self.setObjectName(name)
|
||||
if not Config.running_list:
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
self.top_bar = self.DispatchTopBar(self, name)
|
||||
self.info = self.DispatchInfoCard(self)
|
||||
|
||||
content_layout.addWidget(self.top_bar)
|
||||
content_layout.addWidget(self.info)
|
||||
|
||||
scrollArea.setWidget(content_widget)
|
||||
|
||||
layout.addWidget(scrollArea)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
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.multi_button = PushButton("添加任务")
|
||||
self.multi_button.clicked.connect(self.start_multi_task)
|
||||
self.main_button = PushButton("开始任务")
|
||||
self.main_button.clicked.connect(self.start_main_task)
|
||||
self.multi_button.hide()
|
||||
|
||||
Layout.addWidget(self.Lable)
|
||||
Layout.addWidget(self.object)
|
||||
Layout.addWidget(self.mode)
|
||||
Layout.addStretch(1)
|
||||
Layout.addWidget(self.multi_button)
|
||||
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("未选择调度对象")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度对象", "请选择后再开始任务", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if self.mode.currentIndex() == -1:
|
||||
logger.warning("未选择调度模式")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度模式", "请选择后再开始任务", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if self.object.currentData() in Config.running_list:
|
||||
logger.warning(f"任务已存在:{self.object.currentData()}")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "任务已存在", self.object.currentData(), 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if "调度队列" in self.object.currentData():
|
||||
|
||||
logger.info(f"用户添加任务:{self.object.currentData()}")
|
||||
TaskManager.add_task(
|
||||
f"{self.mode.currentText()}_主调度台",
|
||||
self.object.currentData(),
|
||||
Config.queue_dict[self.object.currentData()]["Config"].toDict(),
|
||||
)
|
||||
|
||||
elif "脚本" in self.object.currentData():
|
||||
|
||||
if Config.member_dict[self.object.currentData()]["Type"] == "Maa":
|
||||
|
||||
logger.info(f"用户添加任务:{self.object.currentData()}")
|
||||
TaskManager.add_task(
|
||||
f"{self.mode.currentText()}_主调度台",
|
||||
"自定义队列",
|
||||
{"Queue": {"Member_1": self.object.currentData()}},
|
||||
)
|
||||
|
||||
def start_multi_task(self):
|
||||
"""开始任务"""
|
||||
|
||||
# 获取所有可用的队列和实例
|
||||
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.member_dict.items():
|
||||
if name in Config.running_list:
|
||||
continue
|
||||
text_list.append(
|
||||
f"实例 - {info['Type']}"
|
||||
if info["Config"].get(info["Config"].MaaSet_Name) == ""
|
||||
else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}"
|
||||
)
|
||||
data_list.append(name)
|
||||
|
||||
choice = ComboBoxMessageBox(
|
||||
self.window(),
|
||||
"选择一个对象以添加相应多开任务",
|
||||
["选择调度对象"],
|
||||
[text_list],
|
||||
[data_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
|
||||
)
|
||||
|
||||
if choice.exec() and choice.input[0].currentIndex() != -1:
|
||||
else:
|
||||
|
||||
if choice.input[0].currentData() in Config.running_list:
|
||||
logger.warning(f"任务已存在:{choice.input[0].currentData()}")
|
||||
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", "任务已存在", choice.input[0].currentData(), 5000
|
||||
"warning", "未选择调度对象", "请选择后再开始任务", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if "调度队列" in choice.input[0].currentData():
|
||||
if self.mode.currentIndex() == -1:
|
||||
logger.warning("未选择调度模式", module="调度中枢")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度模式", "请选择后再开始任务", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
|
||||
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(
|
||||
"自动代理_新调度台",
|
||||
choice.input[0].currentData(),
|
||||
Config.queue_dict[choice.input[0].currentData()][
|
||||
"Config"
|
||||
].toDict(),
|
||||
f"{self.mode.currentText()}_主调度台",
|
||||
self.object.currentData(),
|
||||
Config.queue_dict[self.object.currentData()]["Config"].toDict(),
|
||||
)
|
||||
|
||||
elif "脚本" in choice.input[0].currentData():
|
||||
elif "脚本" in self.object.currentData():
|
||||
|
||||
if (
|
||||
Config.member_dict[choice.input[0].currentData()]["Type"]
|
||||
== "Maa"
|
||||
):
|
||||
logger.info(
|
||||
f"用户添加任务:{self.object.currentData()}", module="调度中枢"
|
||||
)
|
||||
TaskManager.add_task(
|
||||
f"{self.mode.currentText()}_主调度台",
|
||||
"自定义队列",
|
||||
{"Queue": {"Script_0": self.object.currentData()}},
|
||||
)
|
||||
|
||||
logger.info(f"用户添加任务:{choice.input[0].currentData()}")
|
||||
TaskManager.add_task(
|
||||
"自动代理_新调度台",
|
||||
f"自定义队列 - {choice.input[0].currentData()}",
|
||||
{"Queue": {"Member_1": choice.input[0].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):
|
||||
class DispatchInfoCard(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.setTitle("调度信息")
|
||||
|
||||
self.task_cards: List[StatefulItemCard] = []
|
||||
self.task = self.TaskInfoCard(self)
|
||||
self.user = self.UserInfoCard(self)
|
||||
self.log_text = self.LogCard(self)
|
||||
|
||||
def create_task(self, task_list: list):
|
||||
"""创建任务队列"""
|
||||
self.viewLayout.addWidget(self.task)
|
||||
self.viewLayout.addWidget(self.user)
|
||||
self.viewLayout.addWidget(self.log_text)
|
||||
|
||||
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.viewLayout.setStretch(0, 1)
|
||||
self.viewLayout.setStretch(1, 1)
|
||||
self.viewLayout.setStretch(2, 5)
|
||||
|
||||
self.task_cards = []
|
||||
def update_board(self, task_list: list, user_list: list, log: str):
|
||||
"""更新调度信息"""
|
||||
|
||||
for task in task_list:
|
||||
self.task.update_task(task_list)
|
||||
self.user.update_user(user_list)
|
||||
self.log_text.text.setText(log)
|
||||
|
||||
self.task_cards.append(StatefulItemCard(task))
|
||||
self.Layout.addWidget(self.task_cards[-1])
|
||||
class TaskInfoCard(HeaderCardWidget):
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("任务队列")
|
||||
|
||||
def update_task(self, task_list: list):
|
||||
"""更新任务队列"""
|
||||
self.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
for i in range(len(task_list)):
|
||||
self.task_cards: List[StatefulItemCard] = []
|
||||
|
||||
self.task_cards[i].update_status(task_list[i][1])
|
||||
def create_task(self, task_list: list):
|
||||
"""
|
||||
创建任务队列
|
||||
|
||||
class UserInfoCard(HeaderCardWidget):
|
||||
:param task_list: 包含任务信息的任务列表
|
||||
"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("用户队列")
|
||||
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.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
self.task_cards = []
|
||||
|
||||
self.user_cards: List[StatefulItemCard] = []
|
||||
for task in task_list:
|
||||
|
||||
def create_user(self, user_list: list):
|
||||
"""创建用户队列"""
|
||||
self.task_cards.append(StatefulItemCard(task))
|
||||
self.Layout.addWidget(self.task_cards[-1])
|
||||
|
||||
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.Layout.addStretch(1)
|
||||
|
||||
self.user_cards = []
|
||||
def update_task(self, task_list: list):
|
||||
"""
|
||||
更新任务队列信息
|
||||
|
||||
for user in user_list:
|
||||
:param task_list: 包含任务信息的任务列表
|
||||
"""
|
||||
|
||||
self.user_cards.append(StatefulItemCard(user))
|
||||
self.Layout.addWidget(self.user_cards[-1])
|
||||
for i in range(len(task_list)):
|
||||
|
||||
self.Layout.addStretch(1)
|
||||
self.task_cards[i].update_status(task_list[i][1])
|
||||
|
||||
def update_user(self, user_list: list):
|
||||
"""更新用户队列"""
|
||||
class UserInfoCard(HeaderCardWidget):
|
||||
|
||||
for i in range(len(user_list)):
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("用户队列")
|
||||
|
||||
self.user_cards[i].Label.setText(user_list[i][0])
|
||||
self.user_cards[i].update_status(user_list[i][1])
|
||||
self.Layout = QVBoxLayout()
|
||||
self.viewLayout.addLayout(self.Layout)
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
|
||||
class LogCard(HeaderCardWidget):
|
||||
self.user_cards: List[StatefulItemCard] = []
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("日志")
|
||||
def create_user(self, user_list: list):
|
||||
"""
|
||||
创建用户队列
|
||||
|
||||
self.text = TextBrowser()
|
||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||
self.viewLayout.addWidget(self.text)
|
||||
:param user_list: 包含用户信息的用户列表
|
||||
"""
|
||||
|
||||
self.text.textChanged.connect(self.to_end)
|
||||
while self.Layout.count() > 0:
|
||||
item = self.Layout.takeAt(0)
|
||||
if item.spacerItem():
|
||||
self.Layout.removeItem(item.spacerItem())
|
||||
elif item.widget():
|
||||
item.widget().deleteLater()
|
||||
|
||||
def to_end(self):
|
||||
"""滚动到底部"""
|
||||
self.user_cards = []
|
||||
|
||||
self.text.moveCursor(QTextCursor.End)
|
||||
self.text.ensureCursorVisible()
|
||||
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()
|
||||
|
||||
@@ -21,24 +21,18 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA更新器
|
||||
v1.2
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
import sys
|
||||
import json
|
||||
import zipfile
|
||||
import requests
|
||||
import subprocess
|
||||
import time
|
||||
import psutil
|
||||
import win32crypt
|
||||
import base64
|
||||
from packaging import version
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
|
||||
from PySide6.QtWidgets import QApplication, QDialog, QVBoxLayout
|
||||
from PySide6.QtWidgets import QDialog, QVBoxLayout
|
||||
from qfluentwidgets import (
|
||||
ProgressBar,
|
||||
IndeterminateProgressBar,
|
||||
@@ -46,11 +40,14 @@ from qfluentwidgets import (
|
||||
setTheme,
|
||||
Theme,
|
||||
)
|
||||
from PySide6.QtGui import QIcon, QCloseEvent
|
||||
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:
|
||||
"""将版本号列表转为可读的文本信息"""
|
||||
@@ -83,19 +80,33 @@ class DownloadProcess(QThread):
|
||||
) -> 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()
|
||||
|
||||
headers = {"Range": f"bytes={self.start_byte}-{self.end_byte}"}
|
||||
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:
|
||||
|
||||
@@ -104,17 +115,34 @@ class DownloadProcess(QThread):
|
||||
start_time = time.time()
|
||||
|
||||
response = requests.get(
|
||||
self.url, headers=headers, timeout=10, stream=True
|
||||
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 != 206:
|
||||
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:
|
||||
|
||||
@@ -133,10 +161,15 @@ class DownloadProcess(QThread):
|
||||
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
|
||||
|
||||
@@ -144,6 +177,11 @@ class DownloadProcess(QThread):
|
||||
|
||||
if self.check_times != -1:
|
||||
self.check_times -= 1
|
||||
|
||||
logger.exception(
|
||||
f"下载出错:{self.url},错误信息:{e},剩余重试次数:{self.check_times}",
|
||||
module="下载子线程",
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
@@ -151,6 +189,7 @@ class DownloadProcess(QThread):
|
||||
if self.download_path.exists():
|
||||
self.download_path.unlink()
|
||||
self.accomplish.emit(0)
|
||||
logger.error(f"下载失败:{self.url}", module="下载子线程")
|
||||
|
||||
|
||||
class ZipExtractProcess(QThread):
|
||||
@@ -162,14 +201,24 @@ class ZipExtractProcess(QThread):
|
||||
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():
|
||||
@@ -179,13 +228,21 @@ class ZipExtractProcess(QThread):
|
||||
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
|
||||
zip_ref.extractall(self.app_path)
|
||||
self.accomplish.emit()
|
||||
logger.success(
|
||||
f"解压完成:{self.download_path} 到 {self.app_path}",
|
||||
module="解压子线程",
|
||||
)
|
||||
break
|
||||
except PermissionError:
|
||||
if self.name == "AUTO_MAA":
|
||||
self.info.emit(f"解压出错:AUTO_MAA正在运行,正在尝试将其关闭")
|
||||
self.kill_process(self.app_path / "AUTO_MAA.exe")
|
||||
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:
|
||||
@@ -193,32 +250,9 @@ class ZipExtractProcess(QThread):
|
||||
e = str(e)
|
||||
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
|
||||
self.info.emit(f"解压更新时出错:\n{e}")
|
||||
logger.exception(f"解压更新时出错:{e}", module="解压子线程")
|
||||
return None
|
||||
|
||||
def kill_process(self, path: Path) -> None:
|
||||
"""根据路径中止进程"""
|
||||
|
||||
for pid in self.search_pids(path):
|
||||
killprocess = subprocess.Popen(
|
||||
f"taskkill /F /PID {pid}",
|
||||
shell=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
killprocess.wait()
|
||||
|
||||
def search_pids(self, path: Path) -> list:
|
||||
"""根据路径查找进程PID"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
class DownloadManager(QDialog):
|
||||
"""下载管理器"""
|
||||
@@ -237,14 +271,10 @@ class DownloadManager(QDialog):
|
||||
self.version = version
|
||||
self.config = config
|
||||
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
|
||||
self.version_path = app_path / "resources/version.json"
|
||||
self.download_process_dict: Dict[str, DownloadProcess] = {}
|
||||
self.timer_dict: Dict[str, QTimer] = {}
|
||||
self.if_speed_test_accomplish = False
|
||||
|
||||
self.setWindowTitle("AUTO_MAA更新器")
|
||||
self.setWindowIcon(
|
||||
QIcon(str(app_path / "resources/icons/AUTO_MAA_Updater.ico"))
|
||||
)
|
||||
self.resize(700, 70)
|
||||
|
||||
setTheme(Theme.AUTO, lazy=True)
|
||||
@@ -266,17 +296,27 @@ class DownloadManager(QDialog):
|
||||
|
||||
def run(self) -> None:
|
||||
|
||||
if self.name == "MAA":
|
||||
self.download_task1()
|
||||
elif self.name == "AUTO_MAA":
|
||||
logger.info(
|
||||
f"开始执行下载任务:{self.name},版本:{version_text(self.version)}",
|
||||
module="下载管理器",
|
||||
)
|
||||
|
||||
if self.name == "AUTO_MAA":
|
||||
if self.config["mode"] == "Proxy":
|
||||
self.test_speed_task1()
|
||||
self.speed_test_accomplish.connect(self.download_task1)
|
||||
self.start_test_speed()
|
||||
self.speed_test_accomplish.connect(self.start_download)
|
||||
elif self.config["mode"] == "MirrorChyan":
|
||||
self.download_task1()
|
||||
self.start_download()
|
||||
elif self.config["mode"] == "MirrorChyan":
|
||||
self.start_download()
|
||||
|
||||
def get_download_url(self, mode: str) -> Union[str, Dict[str, str]]:
|
||||
"""获取下载链接"""
|
||||
"""
|
||||
生成下载链接
|
||||
|
||||
:param mode: "测速" 或 "下载"
|
||||
:return: 测速模式返回 url 字典,下载模式返回 url 字符串
|
||||
"""
|
||||
|
||||
url_dict = {}
|
||||
|
||||
@@ -300,9 +340,6 @@ class DownloadManager(QDialog):
|
||||
|
||||
elif mode == "下载":
|
||||
|
||||
if self.name == "MAA":
|
||||
return f"https://jp-download.fearr.xyz/MAA/MAA-{version_text(self.version)}-win-x64.zip"
|
||||
|
||||
if self.name == "AUTO_MAA":
|
||||
|
||||
if self.config["mode"] == "Proxy":
|
||||
@@ -325,16 +362,37 @@ class DownloadManager(QDialog):
|
||||
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
|
||||
|
||||
def test_speed_task1(self) -> None:
|
||||
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
|
||||
@@ -342,6 +400,10 @@ class DownloadManager(QDialog):
|
||||
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:
|
||||
@@ -357,10 +419,11 @@ class DownloadManager(QDialog):
|
||||
)
|
||||
self.test_speed_result[name] = -1
|
||||
self.download_process_dict[name].accomplish.connect(
|
||||
partial(self.test_speed_task2, name)
|
||||
partial(self.check_test_speed, name)
|
||||
)
|
||||
|
||||
self.download_process_dict[name].start()
|
||||
|
||||
# 创建防超时定时器,30秒后强制停止测速
|
||||
timer = QTimer(self)
|
||||
timer.setSingleShot(True)
|
||||
timer.timeout.connect(partial(self.kill_speed_test, name))
|
||||
@@ -371,11 +434,22 @@ class DownloadManager(QDialog):
|
||||
self.update_progress(0, 1, 0)
|
||||
|
||||
def kill_speed_test(self, name: str) -> None:
|
||||
"""
|
||||
强制停止测速任务
|
||||
|
||||
:param name: 测速任务的名称
|
||||
"""
|
||||
|
||||
if name in self.download_process_dict:
|
||||
self.download_process_dict[name].requestInterruption()
|
||||
|
||||
def test_speed_task2(self, name: str, t: float) -> None:
|
||||
def check_test_speed(self, name: str, t: float) -> None:
|
||||
"""
|
||||
更新测速子任务wc信息,并检查测速任务是否允许结束
|
||||
|
||||
:param name: 测速任务的名称
|
||||
:param t: 测速任务的耗时
|
||||
"""
|
||||
|
||||
# 计算下载速度
|
||||
if self.isInterruptionRequested:
|
||||
@@ -409,16 +483,30 @@ class DownloadManager(QDialog):
|
||||
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 download_task1(self) -> None:
|
||||
def start_download(self) -> None:
|
||||
"""开始下载任务"""
|
||||
|
||||
if self.isInterruptionRequested:
|
||||
return None
|
||||
@@ -426,7 +514,16 @@ class DownloadManager(QDialog):
|
||||
url = self.get_download_url("下载")
|
||||
self.downloaded_size_list: List[List[int, bool]] = []
|
||||
|
||||
response = requests.head(url, timeout=10)
|
||||
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"]
|
||||
@@ -452,27 +549,34 @@ class DownloadManager(QDialog):
|
||||
# 创建下载子线程
|
||||
self.download_process_dict[f"part{i}"] = DownloadProcess(
|
||||
url,
|
||||
start_byte,
|
||||
end_byte,
|
||||
-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.download_task2, i)
|
||||
partial(self.update_download, i)
|
||||
)
|
||||
self.download_process_dict[f"part{i}"].accomplish.connect(
|
||||
partial(self.download_task3, i)
|
||||
partial(self.check_download, i)
|
||||
)
|
||||
self.download_process_dict[f"part{i}"].start()
|
||||
|
||||
def download_task2(self, index: str, current: int) -> None:
|
||||
"""更新下载进度"""
|
||||
def update_download(self, index: str, current: int) -> None:
|
||||
"""
|
||||
更新子任务下载进度,将信息更新到 UI 上
|
||||
|
||||
:param index: 下载任务的索引
|
||||
:param current: 当前下载大小
|
||||
"""
|
||||
|
||||
# 更新指定线程的下载进度
|
||||
self.downloaded_size_list[index][0] = current
|
||||
self.downloaded_size = sum([_[0] for _ in self.downloaded_size_list])
|
||||
self.update_progress(0, self.file_size, self.downloaded_size)
|
||||
|
||||
# 速度每秒更新一次
|
||||
if time.time() - self.last_time >= 1.0:
|
||||
self.speed = (
|
||||
(self.downloaded_size - self.last_download_size)
|
||||
@@ -491,7 +595,13 @@ class DownloadManager(QDialog):
|
||||
f"正在下载:{self.name} 已下载:{self.downloaded_size / 1048576:.2f}/{self.file_size / 1048576:.2f} MB ({self.downloaded_size / self.file_size * 100:.2f}%) 下载速度:{self.speed:.2f} KB/s",
|
||||
)
|
||||
|
||||
def download_task3(self, index: str, t: float) -> None:
|
||||
def check_download(self, index: str, t: float) -> None:
|
||||
"""
|
||||
更新下载子任务完成信息,检查下载任务是否完成,完成后自动执行后续处理任务
|
||||
|
||||
:param index: 下载任务的索引
|
||||
:param t: 下载任务的耗时
|
||||
"""
|
||||
|
||||
# 标记下载线程完成
|
||||
self.downloaded_size_list[index][1] = True
|
||||
@@ -512,6 +622,10 @@ class DownloadManager(QDialog):
|
||||
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(
|
||||
@@ -520,6 +634,11 @@ class DownloadManager(QDialog):
|
||||
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)
|
||||
|
||||
@@ -533,48 +652,44 @@ class DownloadManager(QDialog):
|
||||
self.zip_extract.start()
|
||||
self.zip_loop.exec()
|
||||
|
||||
self.update_info("正在删除已弃用的文件")
|
||||
if (self.app_path / "changes.json").exists():
|
||||
|
||||
with (self.app_path / "changes.json").open(mode="r", encoding="utf-8") as f:
|
||||
info: Dict[str, List[str]] = json.load(f)
|
||||
|
||||
if "deleted" in info:
|
||||
for file_path in info["deleted"]:
|
||||
if (self.app_path / file_path).exists():
|
||||
(self.app_path / file_path).unlink()
|
||||
|
||||
(self.app_path / "changes.json").unlink()
|
||||
|
||||
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 == "AUTO_MAA":
|
||||
subprocess.Popen(
|
||||
[self.app_path / "AUTO_MAA.exe"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
| subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
elif not self.isInterruptionRequested and self.name == "MAA":
|
||||
# 下载完成后打开对应程序
|
||||
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,
|
||||
)
|
||||
|
||||
self.update_info(f"{self.name}更新成功!")
|
||||
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)
|
||||
@@ -586,6 +701,9 @@ class DownloadManager(QDialog):
|
||||
self.progress_2.setValue(current)
|
||||
|
||||
def requestInterruption(self) -> None:
|
||||
"""请求中断下载任务"""
|
||||
|
||||
logger.info("收到下载任务中止请求", module="下载管理器")
|
||||
|
||||
self.isInterruptionRequested = True
|
||||
|
||||
@@ -609,147 +727,3 @@ class DownloadManager(QDialog):
|
||||
self.requestInterruption()
|
||||
|
||||
event.accept()
|
||||
|
||||
|
||||
class AUTO_MAA_Downloader(QApplication):
|
||||
def __init__(
|
||||
self, app_path: Path, name: str, main_version: list, config: dict
|
||||
) -> None:
|
||||
super().__init__()
|
||||
|
||||
self.main = DownloadManager(app_path, name, main_version, config)
|
||||
self.main.show()
|
||||
self.main.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
# 获取软件自身的路径
|
||||
app_path = Path(sys.argv[0]).resolve().parent
|
||||
|
||||
# 从本地版本信息文件获取当前版本信息
|
||||
if (app_path / "resources/version.json").exists():
|
||||
with (app_path / "resources/version.json").open(
|
||||
mode="r", encoding="utf-8"
|
||||
) as f:
|
||||
current_version_info = json.load(f)
|
||||
current_version = list(
|
||||
map(int, current_version_info["main_version"].split("."))
|
||||
)
|
||||
else:
|
||||
current_version = [0, 0, 0, 0]
|
||||
|
||||
# 从本地配置文件获取更新信息
|
||||
if (app_path / "config/config.json").exists():
|
||||
with (app_path / "config/config.json").open(mode="r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
if "Update" in config:
|
||||
|
||||
if "UpdateType" in config["Update"]:
|
||||
update_type = config["Update"]["UpdateType"]
|
||||
else:
|
||||
update_type = "stable"
|
||||
if "ProxyUrlList" in config["Update"]:
|
||||
proxy_list = config["Update"]["ProxyUrlList"]
|
||||
else:
|
||||
proxy_list = []
|
||||
if "ThreadNumb" in config["Update"]:
|
||||
thread_numb = config["Update"]["ThreadNumb"]
|
||||
else:
|
||||
thread_numb = 8
|
||||
if "MirrorChyanCDK" in config["Update"]:
|
||||
mirrorchyan_CDK = (
|
||||
win32crypt.CryptUnprotectData(
|
||||
base64.b64decode(config["Update"]["MirrorChyanCDK"]),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
0,
|
||||
)[1].decode("utf-8")
|
||||
if config["Update"]["MirrorChyanCDK"]
|
||||
else ""
|
||||
)
|
||||
else:
|
||||
mirrorchyan_CDK = ""
|
||||
|
||||
else:
|
||||
update_type = "stable"
|
||||
proxy_list = []
|
||||
thread_numb = 8
|
||||
mirrorchyan_CDK = ""
|
||||
else:
|
||||
update_type = "stable"
|
||||
proxy_list = []
|
||||
thread_numb = 8
|
||||
mirrorchyan_CDK = ""
|
||||
|
||||
# 从远程服务器获取最新版本信息
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.get(
|
||||
f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaDownloader¤t_version={version_text(current_version)}&cdk={mirrorchyan_CDK}&channel={update_type}",
|
||||
timeout=10,
|
||||
)
|
||||
version_info: Dict[str, Union[int, str, Dict[str, str]]] = response.json()
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
sys.exit(f"获取版本信息时出错:\n{err}")
|
||||
|
||||
if version_info["code"] == 0:
|
||||
|
||||
if "url" in version_info["data"]:
|
||||
download_config = {
|
||||
"mode": "MirrorChyan",
|
||||
"thread_numb": 1,
|
||||
"url": version_info["data"]["url"],
|
||||
}
|
||||
else:
|
||||
|
||||
download_config = {"mode": "Proxy", "thread_numb": thread_numb}
|
||||
else:
|
||||
sys.exit(f"获取版本信息时出错:{version_info["msg"]}")
|
||||
|
||||
remote_version = list(
|
||||
map(
|
||||
int,
|
||||
version_info["data"]["version_name"][1:].replace("-beta", "").split("."),
|
||||
)
|
||||
)
|
||||
|
||||
if download_config["mode"] == "Proxy":
|
||||
for _ in range(3):
|
||||
try:
|
||||
response = requests.get(
|
||||
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/download_info.json",
|
||||
timeout=10,
|
||||
)
|
||||
download_info = response.json()
|
||||
|
||||
download_config["proxy_list"] = list(
|
||||
set(proxy_list + download_info["proxy_list"])
|
||||
)
|
||||
download_config["download_dict"] = download_info["download_dict"]
|
||||
break
|
||||
except Exception as e:
|
||||
err = e
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
sys.exit(f"获取代理信息时出错:{err}")
|
||||
|
||||
if (app_path / "changes.json").exists():
|
||||
(app_path / "changes.json").unlink()
|
||||
|
||||
# 启动更新线程
|
||||
if version.parse(version_text(remote_version)) > version.parse(
|
||||
version_text(current_version)
|
||||
):
|
||||
app = AUTO_MAA_Downloader(
|
||||
app_path,
|
||||
"AUTO_MAA",
|
||||
remote_version,
|
||||
download_config,
|
||||
)
|
||||
sys.exit(app.exec())
|
||||
@@ -21,11 +21,10 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA历史记录界面
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
@@ -48,38 +47,55 @@ import subprocess
|
||||
from datetime import datetime, timedelta
|
||||
from functools import partial
|
||||
from pathlib import Path
|
||||
from typing import Union, List, Dict
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
from app.core import Config
|
||||
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.history_top_bar = self.HistoryTopBar(self)
|
||||
|
||||
self.history_top_bar.search_history.connect(self.reload_history)
|
||||
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()
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(self.history_top_bar)
|
||||
layout.addWidget(scrollArea)
|
||||
self.setLayout(layout)
|
||||
|
||||
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():
|
||||
@@ -95,9 +111,10 @@ class History(QWidget):
|
||||
datetime(end_date.year(), end_date.month(), end_date.day()),
|
||||
)
|
||||
|
||||
for date, user in history_dict.items():
|
||||
# 生成历史记录卡片并添加到布局中
|
||||
for date, user_dict in history_dict.items():
|
||||
|
||||
self.history_card_list.append(self.HistoryCard(mode, date, user, self))
|
||||
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)
|
||||
@@ -149,7 +166,13 @@ class History(QWidget):
|
||||
Layout.addWidget(self.search)
|
||||
|
||||
def select_date(self, date: str) -> None:
|
||||
"""选中最近一段时间并启动查询"""
|
||||
"""
|
||||
选中最近一段时间并启动查询
|
||||
|
||||
:param date: 选择的时间段("week" 或 "month")
|
||||
"""
|
||||
|
||||
logger.info(f"选择最近{date}的记录并开始查询", module="历史记录")
|
||||
|
||||
server_date = Config.server_date()
|
||||
if date == "week":
|
||||
@@ -167,14 +190,9 @@ class History(QWidget):
|
||||
self.search.clicked.emit()
|
||||
|
||||
class HistoryCard(QuickExpandGroupCard):
|
||||
"""历史记录卡片"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
mode: str,
|
||||
date: str,
|
||||
user: Union[List[Path], Dict[str, List[Path]]],
|
||||
parent=None,
|
||||
):
|
||||
def __init__(self, date: str, user_dict: Dict[str, List[Path]], parent=None):
|
||||
super().__init__(
|
||||
FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent
|
||||
)
|
||||
@@ -187,64 +205,29 @@ class History(QWidget):
|
||||
|
||||
self.user_history_card_list = []
|
||||
|
||||
if mode == "按日合并":
|
||||
|
||||
for user_path in user:
|
||||
self.user_history_card_list.append(
|
||||
self.UserHistoryCard(mode, user_path.stem, user_path, self)
|
||||
)
|
||||
Layout.addWidget(self.user_history_card_list[-1])
|
||||
|
||||
elif mode in ["按周合并", "按月合并"]:
|
||||
|
||||
for user, info in user.items():
|
||||
self.user_history_card_list.append(
|
||||
self.UserHistoryCard(mode, user, info, self)
|
||||
)
|
||||
Layout.addWidget(self.user_history_card_list[-1])
|
||||
# 生成用户历史记录卡片并添加到布局中
|
||||
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,
|
||||
mode: str,
|
||||
name: str,
|
||||
user_history: Union[Path, List[Path]],
|
||||
parent=None,
|
||||
):
|
||||
def __init__(self, name: str, user_history: List[Path], parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
self.setTitle(name)
|
||||
|
||||
if mode == "按日合并":
|
||||
self.user_history = user_history
|
||||
|
||||
self.user_history_path = user_history
|
||||
self.main_history = Config.load_maa_logs("总览", user_history)
|
||||
|
||||
self.index_card = self.IndexCard(
|
||||
self.main_history["条目索引"], self
|
||||
)
|
||||
self.index_card.index_changed.connect(self.update_info)
|
||||
self.viewLayout.addWidget(self.index_card)
|
||||
|
||||
elif mode in ["按周合并", "按月合并"]:
|
||||
|
||||
history = Config.merge_maa_logs("指定项", user_history)
|
||||
|
||||
self.main_history = {}
|
||||
self.main_history["统计数据"] = {
|
||||
"公招统计": list(history["recruit_statistics"].items())
|
||||
}
|
||||
|
||||
for game_id, drops in history["drop_statistics"].items():
|
||||
self.main_history["统计数据"][f"掉落统计:{game_id}"] = list(
|
||||
drops.items()
|
||||
)
|
||||
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)
|
||||
@@ -254,19 +237,56 @@ class History(QWidget):
|
||||
|
||||
self.update_info("数据总览")
|
||||
|
||||
def update_info(self, index: str) -> None:
|
||||
"""更新信息"""
|
||||
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 == "数据总览":
|
||||
|
||||
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()
|
||||
|
||||
for name, item_list in self.main_history["统计数据"].items():
|
||||
# 生成数据统计信息卡片组
|
||||
for name, item_list in self.get_statistics("数据总览").items():
|
||||
|
||||
statistics_card = self.StatisticsCard(name, item_list, self)
|
||||
self.statistics_card.addWidget(statistics_card)
|
||||
@@ -275,44 +295,26 @@ class History(QWidget):
|
||||
|
||||
else:
|
||||
|
||||
single_history = Config.load_maa_logs(
|
||||
"单项",
|
||||
self.user_history_path.with_suffix("")
|
||||
/ f"{index.replace(":","-")}.json",
|
||||
)
|
||||
|
||||
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()
|
||||
|
||||
for name, item_list in single_history["统计数据"].items():
|
||||
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)
|
||||
|
||||
self.log_card.text.setText(single_history["日志信息"])
|
||||
# 显示日志信息并绑定点击事件
|
||||
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(
|
||||
self.user_history_path.with_suffix("")
|
||||
/ f"{index.replace(":","-")}.log"
|
||||
)
|
||||
lambda: os.startfile(log_path)
|
||||
)
|
||||
self.log_card.open_dir.clicked.disconnect()
|
||||
self.log_card.open_dir.clicked.connect(
|
||||
lambda: subprocess.Popen(
|
||||
[
|
||||
"explorer",
|
||||
"/select,",
|
||||
str(
|
||||
self.user_history_path.with_suffix("")
|
||||
/ f"{index.replace(":","-")}.log"
|
||||
),
|
||||
]
|
||||
)
|
||||
lambda: subprocess.Popen(["explorer", "/select,", log_path])
|
||||
)
|
||||
self.log_card.show()
|
||||
|
||||
@@ -321,10 +323,11 @@ class History(QWidget):
|
||||
self.setMinimumHeight(300)
|
||||
|
||||
class IndexCard(HeaderCardWidget):
|
||||
"""历史记录索引卡片组"""
|
||||
|
||||
index_changed = Signal(str)
|
||||
|
||||
def __init__(self, index_list: list, parent=None):
|
||||
def __init__(self, history_list: List[Path], parent=None):
|
||||
super().__init__(parent)
|
||||
self.setTitle("记录条目")
|
||||
|
||||
@@ -334,17 +337,23 @@ class History(QWidget):
|
||||
|
||||
self.index_cards: List[StatefulItemCard] = []
|
||||
|
||||
# 生成索引卡片信息
|
||||
index_list = Config.merge_statistic_info(history_list)["index"]
|
||||
index_list.insert(0, ["数据总览", "运行", "数据总览"])
|
||||
|
||||
# 生成索引卡片组件并绑定点击事件
|
||||
for index in index_list:
|
||||
|
||||
self.index_cards.append(StatefulItemCard(index))
|
||||
self.index_cards.append(StatefulItemCard(index[:2]))
|
||||
self.index_cards[-1].clicked.connect(
|
||||
partial(self.index_changed.emit, index[0])
|
||||
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)
|
||||
@@ -367,6 +376,7 @@ class History(QWidget):
|
||||
self.Layout.addStretch(1)
|
||||
|
||||
class LogCard(HeaderCardWidget):
|
||||
"""历史记录日志卡片"""
|
||||
|
||||
def __init__(self, parent=None):
|
||||
super().__init__(parent)
|
||||
|
||||
@@ -21,11 +21,10 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主界面
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
@@ -49,7 +48,7 @@ import json
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
|
||||
from app.core import Config, MainInfoBar, Network
|
||||
from app.core import Config, MainInfoBar, Network, logger
|
||||
from .Widget import Banner, IconButton
|
||||
|
||||
|
||||
@@ -62,14 +61,6 @@ class Home(QWidget):
|
||||
self.banner = Banner()
|
||||
self.banner_text = TextBrowser()
|
||||
|
||||
widget = QWidget()
|
||||
Layout = QVBoxLayout(widget)
|
||||
|
||||
Layout.addWidget(self.banner)
|
||||
Layout.addWidget(self.banner_text)
|
||||
Layout.setStretch(0, 2)
|
||||
Layout.setStretch(1, 3)
|
||||
|
||||
v_layout = QVBoxLayout(self.banner)
|
||||
v_layout.setContentsMargins(0, 0, 0, 15)
|
||||
v_layout.setSpacing(5)
|
||||
@@ -146,20 +137,34 @@ class Home(QWidget):
|
||||
# 将底部水平布局添加到垂直布局
|
||||
v_layout.addLayout(h2_layout)
|
||||
|
||||
layout = QVBoxLayout()
|
||||
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.setWidget(widget)
|
||||
scrollArea.setContentsMargins(0, 0, 0, 0)
|
||||
scrollArea.setStyleSheet("background: transparent; border: none;")
|
||||
scrollArea.setWidget(content_widget)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(scrollArea)
|
||||
self.setLayout(layout)
|
||||
|
||||
self.set_banner()
|
||||
|
||||
def get_home_image(self) -> None:
|
||||
"""获取主页图片"""
|
||||
|
||||
logger.info("获取主页图片", module="主页")
|
||||
|
||||
if Config.get(Config.function_HomeImageMode) == "默认":
|
||||
pass
|
||||
|
||||
logger.info("使用默认主页图片", module="主页")
|
||||
|
||||
elif Config.get(Config.function_HomeImageMode) == "自定义":
|
||||
|
||||
file_path, _ = QFileDialog.getOpenFileName(
|
||||
@@ -178,7 +183,7 @@ class Home(QWidget):
|
||||
/ f"resources/images/Home/BannerCustomize{Path(file_path).suffix}",
|
||||
)
|
||||
|
||||
logger.info(f"自定义主页图片更换成功:{file_path}")
|
||||
logger.info(f"自定义主页图片更换成功:{file_path}", module="主页")
|
||||
MainInfoBar.push_info_bar(
|
||||
"success",
|
||||
"主页图片更换成功",
|
||||
@@ -187,7 +192,7 @@ class Home(QWidget):
|
||||
)
|
||||
|
||||
else:
|
||||
logger.warning("自定义主页图片更换失败:未选择图片文件")
|
||||
logger.warning("自定义主页图片更换失败:未选择图片文件", module="主页")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"主页图片更换失败",
|
||||
@@ -196,21 +201,24 @@ class Home(QWidget):
|
||||
)
|
||||
elif Config.get(Config.function_HomeImageMode) == "主题图像":
|
||||
|
||||
# 从远程服务器获取最新主题图像
|
||||
Network.set_info(
|
||||
# 从远程服务器获取最新主题图像信息
|
||||
network = Network.add_task(
|
||||
mode="get",
|
||||
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json",
|
||||
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/theme_image.json",
|
||||
)
|
||||
Network.start()
|
||||
Network.loop.exec()
|
||||
if Network.stutus_code == 200:
|
||||
theme_image = Network.response_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.error_message}")
|
||||
logger.warning(
|
||||
f"获取最新主题图像时出错:{network_result['error_message']}",
|
||||
module="主页",
|
||||
)
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"获取最新主题图像时出错",
|
||||
f"网络错误:{Network.stutus_code}",
|
||||
f"网络错误:{network_result['status_code']}",
|
||||
5000,
|
||||
)
|
||||
return None
|
||||
@@ -226,6 +234,7 @@ class Home(QWidget):
|
||||
else:
|
||||
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
|
||||
|
||||
# 检查主题图像是否需要更新
|
||||
if not (
|
||||
Config.app_path / "resources/images/Home/BannerTheme.jpg"
|
||||
).exists() or (
|
||||
@@ -234,22 +243,24 @@ class Home(QWidget):
|
||||
> time_local
|
||||
):
|
||||
|
||||
Network.set_info(
|
||||
network = Network.add_task(
|
||||
mode="get_file",
|
||||
url=theme_image["url"],
|
||||
path=Config.app_path / "resources/images/Home/BannerTheme.jpg",
|
||||
)
|
||||
Network.start()
|
||||
Network.loop.exec()
|
||||
network.loop.exec()
|
||||
network_result = Network.get_result(network)
|
||||
|
||||
if Network.stutus_code == 200:
|
||||
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"]}」下载成功")
|
||||
logger.success(
|
||||
f"主题图像「{theme_image["name"]}」下载成功", module="主页"
|
||||
)
|
||||
MainInfoBar.push_info_bar(
|
||||
"success",
|
||||
"主题图像下载成功",
|
||||
@@ -259,28 +270,29 @@ class Home(QWidget):
|
||||
|
||||
else:
|
||||
|
||||
logger.warning(f"下载最新主题图像时出错:{Network.error_message}")
|
||||
logger.warning(
|
||||
f"下载最新主题图像时出错:{network_result['error_message']}",
|
||||
module="主页",
|
||||
)
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"下载最新主题图像时出错",
|
||||
f"网络错误:{Network.stutus_code}",
|
||||
f"网络错误:{network_result['status_code']}",
|
||||
5000,
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
logger.info("主题图像已是最新")
|
||||
logger.info("主题图像已是最新", module="主页")
|
||||
MainInfoBar.push_info_bar(
|
||||
"info",
|
||||
"主题图像已是最新",
|
||||
"主题图像已是最新!",
|
||||
3000,
|
||||
"info", "主题图像已是最新", "主题图像已是最新!", 3000
|
||||
)
|
||||
|
||||
self.set_banner()
|
||||
|
||||
def set_banner(self):
|
||||
"""设置主页图像"""
|
||||
|
||||
if Config.get(Config.function_HomeImageMode) == "默认":
|
||||
self.banner.set_banner_image(
|
||||
str(Config.app_path / "resources/images/Home/BannerDefault.png")
|
||||
@@ -360,7 +372,7 @@ class ButtonGroup(SimpleCardWidget):
|
||||
doc_button = IconButton(
|
||||
FluentIcon.CHAT.icon(color=QColor("#fff")),
|
||||
tip_title="官方社群",
|
||||
tip_content="加入官方群聊【AUTO_MAA绝赞DeBug中!】",
|
||||
tip_content="加入官方群聊「AUTO_MAA绝赞DeBug中!」",
|
||||
isTooltip=True,
|
||||
)
|
||||
doc_button.setIconSize(QSize(32, 32))
|
||||
@@ -399,4 +411,6 @@ class ButtonGroup(SimpleCardWidget):
|
||||
|
||||
def open_sales(self):
|
||||
"""打开 MirrorChyan 链接"""
|
||||
QDesktopServices.openUrl(QUrl("https://mirrorchyan.com/"))
|
||||
QDesktopServices.openUrl(
|
||||
QUrl("https://mirrorchyan.com/zh/get-start?source=auto_maa-home")
|
||||
)
|
||||
|
||||
@@ -21,14 +21,12 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主界面
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
|
||||
from qfluentwidgets import (
|
||||
qconfig,
|
||||
Action,
|
||||
SystemTrayMenu,
|
||||
SplashScreen,
|
||||
@@ -42,14 +40,13 @@ from qfluentwidgets import (
|
||||
)
|
||||
from PySide6.QtGui import QIcon, QCloseEvent
|
||||
from PySide6.QtCore import QTimer
|
||||
from datetime import datetime, timedelta
|
||||
import shutil
|
||||
import darkdetect
|
||||
|
||||
from app.core import Config, TaskManager, MainTimer, MainInfoBar
|
||||
from app.core import Config, logger, TaskManager, MainTimer, MainInfoBar, SoundPlayer
|
||||
from app.services import Notify, Crypto, System
|
||||
from .home import Home
|
||||
from .member_manager import MemberManager
|
||||
from .script_manager import ScriptManager
|
||||
from .plan_manager import PlanManager
|
||||
from .queue_manager import QueueManager
|
||||
from .dispatch_center import DispatchCenter
|
||||
from .history import History
|
||||
@@ -57,6 +54,7 @@ from .setting import Setting
|
||||
|
||||
|
||||
class AUTO_MAA(MSFluentWindow):
|
||||
"""AUTO_MAA主界面"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
@@ -77,11 +75,14 @@ class AUTO_MAA(MSFluentWindow):
|
||||
self.splashScreen = SplashScreen(self.windowIcon(), self)
|
||||
self.show_ui("显示主窗口", if_quick=True)
|
||||
|
||||
# 设置主窗口的引用,便于各组件访问
|
||||
Config.main_window = self.window()
|
||||
|
||||
# 创建主窗口
|
||||
# 创建各子窗口
|
||||
logger.info("正在创建各子窗口", module="主窗口")
|
||||
self.home = Home(self)
|
||||
self.member_manager = MemberManager(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)
|
||||
@@ -95,12 +96,19 @@ class AUTO_MAA(MSFluentWindow):
|
||||
NavigationItemPosition.TOP,
|
||||
)
|
||||
self.addSubInterface(
|
||||
self.member_manager,
|
||||
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,
|
||||
@@ -129,25 +137,11 @@ class AUTO_MAA(MSFluentWindow):
|
||||
FluentIcon.SETTING,
|
||||
NavigationItemPosition.BOTTOM,
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (
|
||||
self.queue_manager.reload_member_name() if index == 2 else None
|
||||
)
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (
|
||||
self.dispatch_center.pivot.setCurrentItem("主调度台")
|
||||
if index == 3
|
||||
else None
|
||||
)
|
||||
)
|
||||
self.stackedWidget.currentChanged.connect(
|
||||
lambda index: (
|
||||
self.dispatch_center.update_top_bar() if index == 3 else None
|
||||
)
|
||||
)
|
||||
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
|
||||
)
|
||||
@@ -167,7 +161,7 @@ class AUTO_MAA(MSFluentWindow):
|
||||
# 开始任务菜单项
|
||||
self.tray_menu.addActions(
|
||||
[
|
||||
Action(FluentIcon.PLAY, "运行自动代理", triggered=self.start_main_task),
|
||||
Action(FluentIcon.PLAY, "运行启动时队列", triggered=self.start_up_task),
|
||||
Action(
|
||||
FluentIcon.PAUSE,
|
||||
"中止所有任务",
|
||||
@@ -182,17 +176,20 @@ class AUTO_MAA(MSFluentWindow):
|
||||
Action(
|
||||
FluentIcon.POWER_BUTTON,
|
||||
"退出主程序",
|
||||
triggered=lambda: (self.window().close(), QApplication.quit()),
|
||||
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.user_info_changed.connect(self.member_manager.refresh_dashboard)
|
||||
# 绑定各组件信号
|
||||
Config.sub_info_changed.connect(self.script_manager.refresh_dashboard)
|
||||
Config.power_sign_changed.connect(self.dispatch_center.update_power_sign)
|
||||
TaskManager.create_gui.connect(self.dispatch_center.add_board)
|
||||
TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board)
|
||||
Notify.push_info_bar.connect(MainInfoBar.push_info_bar)
|
||||
@@ -212,6 +209,8 @@ class AUTO_MAA(MSFluentWindow):
|
||||
self.themeListener.systemThemeChanged.connect(self.switch_theme)
|
||||
self.themeListener.start()
|
||||
|
||||
logger.success("AUTO_MAA主程序初始化完成", module="主窗口")
|
||||
|
||||
def switch_theme(self) -> None:
|
||||
"""切换主题"""
|
||||
|
||||
@@ -241,43 +240,6 @@ class AUTO_MAA(MSFluentWindow):
|
||||
else:
|
||||
self.setStyleSheet("background-color: #ffffff;")
|
||||
|
||||
def start_up_task(self) -> None:
|
||||
"""启动时任务"""
|
||||
|
||||
# 清理旧日志
|
||||
self.clean_old_logs()
|
||||
|
||||
# 清理临时更新器
|
||||
if (Config.app_path / "AUTO_Updater.active.exe").exists():
|
||||
try:
|
||||
(Config.app_path / "AUTO_Updater.active.exe").unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 检查密码
|
||||
self.setting.check_PASSWORD()
|
||||
|
||||
# 获取主题图像
|
||||
if Config.get(Config.function_HomeImageMode) == "主题图像":
|
||||
self.home.get_home_image()
|
||||
|
||||
# 直接运行主任务
|
||||
if Config.get(Config.start_IfRunDirectly):
|
||||
|
||||
self.start_main_task()
|
||||
|
||||
# 获取公告
|
||||
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()
|
||||
|
||||
def set_min_method(self) -> None:
|
||||
"""设置最小化方法"""
|
||||
|
||||
@@ -296,64 +258,14 @@ class AUTO_MAA(MSFluentWindow):
|
||||
if reason == QSystemTrayIcon.DoubleClick:
|
||||
self.show_ui("显示主窗口")
|
||||
|
||||
def clean_old_logs(self):
|
||||
"""
|
||||
删除超过用户设定天数的日志文件(基于目录日期)
|
||||
"""
|
||||
|
||||
if Config.get(Config.function_HistoryRetentionTime) == 0:
|
||||
logger.info("由于用户设置日志永久保留,跳过日志清理")
|
||||
return
|
||||
|
||||
deleted_count = 0
|
||||
|
||||
for date_folder in (Config.app_path / "history").iterdir():
|
||||
if not date_folder.is_dir():
|
||||
continue # 只处理日期文件夹
|
||||
|
||||
try:
|
||||
# 只检查 `YYYY-MM-DD` 格式的文件夹
|
||||
folder_date = datetime.strptime(date_folder.name, "%Y-%m-%d")
|
||||
if datetime.now() - folder_date > timedelta(
|
||||
days=Config.get(Config.function_HistoryRetentionTime)
|
||||
):
|
||||
shutil.rmtree(date_folder, ignore_errors=True)
|
||||
deleted_count += 1
|
||||
logger.info(f"已删除超期日志目录: {date_folder}")
|
||||
except ValueError:
|
||||
logger.warning(f"非日期格式的目录: {date_folder}")
|
||||
|
||||
logger.info(f"清理完成: {deleted_count} 个日期目录")
|
||||
|
||||
def start_main_task(self) -> None:
|
||||
"""启动主任务"""
|
||||
|
||||
if "调度队列_1" in Config.queue_dict:
|
||||
|
||||
logger.info("自动添加任务:调度队列_1")
|
||||
TaskManager.add_task(
|
||||
"自动代理_主调度台",
|
||||
"调度队列_1",
|
||||
Config.queue_dict["调度队列_1"]["Config"].toDict(),
|
||||
)
|
||||
|
||||
elif "脚本_1" in Config.member_dict:
|
||||
|
||||
logger.info("自动添加任务:脚本_1")
|
||||
TaskManager.add_task(
|
||||
"自动代理_主调度台", "自定义队列", {"Queue": {"Member_1": "脚本_1"}}
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
logger.warning("启动主任务失败:未找到有效的主任务配置文件")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "启动主任务失败", "“调度队列_1”与“脚本_1”均不存在", -1
|
||||
)
|
||||
|
||||
def show_ui(self, mode: str, if_quick: bool = False) -> None:
|
||||
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 == "显示主窗口":
|
||||
@@ -377,10 +289,19 @@ class AUTO_MAA(MSFluentWindow):
|
||||
self.window().setGeometry(location[0], location[1], size[0], size[1])
|
||||
self.window().show()
|
||||
if not if_quick:
|
||||
if Config.get(Config.ui_maximized):
|
||||
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()
|
||||
@@ -391,6 +312,15 @@ class AUTO_MAA(MSFluentWindow):
|
||||
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):
|
||||
@@ -421,23 +351,138 @@ class AUTO_MAA(MSFluentWindow):
|
||||
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.Timer.stop()
|
||||
MainTimer.Timer.deleteLater()
|
||||
MainTimer.LongTimer.stop()
|
||||
MainTimer.LongTimer.deleteLater()
|
||||
MainTimer.stop()
|
||||
TaskManager.stop_task("ALL")
|
||||
|
||||
# 关闭主题监听
|
||||
self.themeListener.terminate()
|
||||
self.themeListener.deleteLater()
|
||||
|
||||
logger.info("AUTO_MAA主程序关闭")
|
||||
logger.info("----------------END----------------")
|
||||
logger.info("AUTO_MAA主程序关闭", module="主窗口")
|
||||
logger.info("----------------END----------------", module="主窗口")
|
||||
|
||||
event.accept()
|
||||
|
||||
515
app/ui/plan_manager.py
Normal file
@@ -0,0 +1,515 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA计划管理界面
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from 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"],
|
||||
)
|
||||
@@ -21,11 +21,10 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA调度队列界面
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import (
|
||||
QWidget,
|
||||
QVBoxLayout,
|
||||
@@ -34,17 +33,15 @@ from PySide6.QtWidgets import (
|
||||
)
|
||||
from qfluentwidgets import (
|
||||
Action,
|
||||
Pivot,
|
||||
ScrollArea,
|
||||
FluentIcon,
|
||||
MessageBox,
|
||||
HeaderCardWidget,
|
||||
CommandBar,
|
||||
)
|
||||
from PySide6.QtCore import Qt
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
|
||||
from app.core import QueueConfig, Config, MainInfoBar
|
||||
from app.core import QueueConfig, Config, MainInfoBar, SoundPlayer, logger
|
||||
from .Widget import (
|
||||
SwitchSettingCard,
|
||||
ComboBoxSettingCard,
|
||||
@@ -52,6 +49,7 @@ from .Widget import (
|
||||
TimeEditSettingCard,
|
||||
NoOptionComboBoxSettingCard,
|
||||
HistoryCard,
|
||||
PivotArea,
|
||||
)
|
||||
|
||||
|
||||
@@ -71,36 +69,31 @@ class QueueManager(QWidget):
|
||||
# 逐个添加动作
|
||||
self.tools.addActions(
|
||||
[
|
||||
Action(FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_queue),
|
||||
Action(
|
||||
FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_setting_box
|
||||
),
|
||||
Action(
|
||||
FluentIcon.REMOVE_FROM,
|
||||
"删除调度队列",
|
||||
triggered=self.del_setting_box,
|
||||
FluentIcon.REMOVE_FROM, "删除调度队列", triggered=self.del_queue
|
||||
),
|
||||
]
|
||||
)
|
||||
self.tools.addSeparator()
|
||||
self.tools.addActions(
|
||||
[
|
||||
Action(
|
||||
FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_setting_box
|
||||
),
|
||||
Action(
|
||||
FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box
|
||||
),
|
||||
Action(FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_queue),
|
||||
Action(FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_queue),
|
||||
]
|
||||
)
|
||||
|
||||
layout.addWidget(self.tools)
|
||||
layout.addWidget(self.queue_manager)
|
||||
|
||||
def add_setting_box(self):
|
||||
def add_queue(self):
|
||||
"""添加一个调度队列"""
|
||||
|
||||
index = len(Config.queue_dict) + 1
|
||||
|
||||
logger.info(f"正在添加调度队列_{index}", module="队列管理")
|
||||
|
||||
# 初始化队列配置
|
||||
queue_config = QueueConfig()
|
||||
queue_config.load(
|
||||
Config.app_path / f"config/QueueConfig/调度队列_{index}.json", queue_config
|
||||
@@ -112,40 +105,41 @@ class QueueManager(QWidget):
|
||||
"Config": queue_config,
|
||||
}
|
||||
|
||||
self.queue_manager.add_QueueSettingBox(index)
|
||||
# 添加到配置界面
|
||||
self.queue_manager.add_SettingBox(index)
|
||||
self.queue_manager.switch_SettingBox(index)
|
||||
|
||||
logger.success(f"调度队列_{index} 添加成功")
|
||||
logger.success(f"调度队列_{index} 添加成功", module="队列管理")
|
||||
MainInfoBar.push_info_bar("success", "操作成功", f"添加 调度队列_{index}", 3000)
|
||||
SoundPlayer.play("添加调度队列")
|
||||
|
||||
def del_setting_box(self):
|
||||
def del_queue(self):
|
||||
"""删除一个调度队列实例"""
|
||||
|
||||
name = self.queue_manager.pivot.currentRouteKey()
|
||||
|
||||
if name == None:
|
||||
logger.warning("未选择调度队列")
|
||||
if name is None:
|
||||
logger.warning("未选择调度队列", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if name in Config.running_list:
|
||||
logger.warning("调度队列正在运行")
|
||||
logger.warning("调度队列正在运行", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "调度队列正在运行", "请先停止调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
choice = MessageBox(
|
||||
"确认",
|
||||
f"确定要删除 {name} 吗?",
|
||||
self.window(),
|
||||
)
|
||||
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():
|
||||
@@ -157,16 +151,17 @@ class QueueManager(QWidget):
|
||||
|
||||
self.queue_manager.show_SettingBox(max(int(name[5:]) - 1, 1))
|
||||
|
||||
logger.success(f"{name} 删除成功")
|
||||
logger.success(f"{name} 删除成功", module="队列管理")
|
||||
MainInfoBar.push_info_bar("success", "操作成功", f"删除 {name}", 3000)
|
||||
SoundPlayer.play("删除调度队列")
|
||||
|
||||
def left_setting_box(self):
|
||||
def left_queue(self):
|
||||
"""向左移动调度队列实例"""
|
||||
|
||||
name = self.queue_manager.pivot.currentRouteKey()
|
||||
|
||||
if name == None:
|
||||
logger.warning("未选择调度队列")
|
||||
if name is None:
|
||||
logger.warning("未选择调度队列", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||
)
|
||||
@@ -175,21 +170,24 @@ class QueueManager(QWidget):
|
||||
index = int(name[5:])
|
||||
|
||||
if index == 1:
|
||||
logger.warning("向左移动调度队列时已到达最左端")
|
||||
logger.warning("向左移动调度队列时已到达最左端", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "已经是第一个调度队列", "无法向左移动", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if name in Config.running_list or f"调度队列_{index-1}" in Config.running_list:
|
||||
logger.warning("相关调度队列正在运行")
|
||||
logger.warning("相关调度队列正在运行", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(f"正在左移调度队列 {name}", module="队列管理")
|
||||
|
||||
self.queue_manager.clear_SettingBox()
|
||||
|
||||
# 移动配置文件并同步到相关配置项
|
||||
Config.queue_dict[name]["Path"].rename(
|
||||
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
|
||||
)
|
||||
@@ -202,16 +200,16 @@ class QueueManager(QWidget):
|
||||
|
||||
self.queue_manager.show_SettingBox(index - 1)
|
||||
|
||||
logger.success(f"{name} 左移成功")
|
||||
logger.success(f"{name} 左移成功", module="队列管理")
|
||||
MainInfoBar.push_info_bar("success", "操作成功", f"左移 {name}", 3000)
|
||||
|
||||
def right_setting_box(self):
|
||||
def right_queue(self):
|
||||
"""向右移动调度队列实例"""
|
||||
|
||||
name = self.queue_manager.pivot.currentRouteKey()
|
||||
|
||||
if name == None:
|
||||
logger.warning("未选择调度队列")
|
||||
if name is None:
|
||||
logger.warning("未选择调度队列", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||
)
|
||||
@@ -220,21 +218,24 @@ class QueueManager(QWidget):
|
||||
index = int(name[5:])
|
||||
|
||||
if index == len(Config.queue_dict):
|
||||
logger.warning("向右移动调度队列时已到达最右端")
|
||||
logger.warning("向右移动调度队列时已到达最右端", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "已经是最后一个调度队列", "无法向右移动", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
if name in Config.running_list or f"调度队列_{index+1}" in Config.running_list:
|
||||
logger.warning("相关调度队列正在运行")
|
||||
logger.warning("相关调度队列正在运行", module="队列管理")
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
||||
)
|
||||
return None
|
||||
|
||||
logger.info(f"正在右移调度队列 {name}", module="队列管理")
|
||||
|
||||
self.queue_manager.clear_SettingBox()
|
||||
|
||||
# 移动配置文件并同步到相关配置项
|
||||
Config.queue_dict[name]["Path"].rename(
|
||||
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
|
||||
)
|
||||
@@ -247,56 +248,28 @@ class QueueManager(QWidget):
|
||||
|
||||
self.queue_manager.show_SettingBox(index + 1)
|
||||
|
||||
logger.success(f"{name} 右移成功")
|
||||
logger.success(f"{name} 右移成功", module="队列管理")
|
||||
MainInfoBar.push_info_bar("success", "操作成功", f"右移 {name}", 3000)
|
||||
|
||||
def reload_member_name(self):
|
||||
"""刷新调度队列成员"""
|
||||
def reload_script_name(self):
|
||||
"""刷新调度队列脚本成员名称"""
|
||||
|
||||
member_list = [
|
||||
["禁用"] + [_ for _ in Config.member_dict.keys()],
|
||||
# 获取脚本成员列表
|
||||
script_list = [
|
||||
["禁用"] + [_ for _ in Config.script_dict.keys()],
|
||||
["未启用"]
|
||||
+ [
|
||||
(
|
||||
k
|
||||
if v["Config"].get(v["Config"].MaaSet_Name) == ""
|
||||
else f"{k} - {v["Config"].get(v["Config"].MaaSet_Name)}"
|
||||
if v["Config"].get_name() == ""
|
||||
else f"{k} - {v["Config"].get_name()}"
|
||||
)
|
||||
for k, v in Config.member_dict.items()
|
||||
for k, v in Config.script_dict.items()
|
||||
],
|
||||
]
|
||||
for script in self.queue_manager.script_list:
|
||||
|
||||
script.task.card_Member_1.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_2.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_3.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_4.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_5.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_6.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_7.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_8.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_9.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
script.task.card_Member_10.reLoadOptions(
|
||||
value=member_list[0], texts=member_list[1]
|
||||
)
|
||||
for card in script.task.card_dict.values():
|
||||
card.reLoadOptions(value=script_list[0], texts=script_list[1])
|
||||
|
||||
class QueueSettingBox(QWidget):
|
||||
|
||||
@@ -305,15 +278,19 @@ class QueueManager(QWidget):
|
||||
|
||||
self.setObjectName("调度队列管理")
|
||||
|
||||
self.pivot = Pivot(self)
|
||||
self.pivotArea = PivotArea()
|
||||
self.pivot = self.pivotArea.pivot
|
||||
|
||||
self.stackedWidget = QStackedWidget(self)
|
||||
self.Layout = QVBoxLayout(self)
|
||||
self.stackedWidget.setContentsMargins(0, 0, 0, 0)
|
||||
self.stackedWidget.setStyleSheet("background: transparent; border: none;")
|
||||
|
||||
self.script_list: List[
|
||||
QueueManager.QueueSettingBox.QueueMemberSettingBox
|
||||
] = []
|
||||
|
||||
self.Layout.addWidget(self.pivot, 0, Qt.AlignHCenter)
|
||||
self.Layout = QVBoxLayout(self)
|
||||
self.Layout.addWidget(self.pivotArea)
|
||||
self.Layout.addWidget(self.stackedWidget)
|
||||
self.Layout.setContentsMargins(0, 0, 0, 0)
|
||||
|
||||
@@ -331,12 +308,17 @@ class QueueManager(QWidget):
|
||||
Config.search_queue()
|
||||
|
||||
for name in Config.queue_dict.keys():
|
||||
self.add_QueueSettingBox(int(name[5:]))
|
||||
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
|
||||
@@ -357,12 +339,12 @@ class QueueManager(QWidget):
|
||||
self.script_list.clear()
|
||||
self.pivot.clear()
|
||||
|
||||
def add_QueueSettingBox(self, uid: int) -> None:
|
||||
def add_SettingBox(self, uid: int) -> None:
|
||||
"""添加一个调度队列设置界面"""
|
||||
|
||||
maa_setting_box = self.QueueMemberSettingBox(uid, self)
|
||||
setting_box = self.QueueMemberSettingBox(uid, self)
|
||||
|
||||
self.script_list.append(maa_setting_box)
|
||||
self.script_list.append(setting_box)
|
||||
|
||||
self.stackedWidget.addWidget(self.script_list[-1])
|
||||
|
||||
@@ -376,14 +358,6 @@ class QueueManager(QWidget):
|
||||
self.setObjectName(f"调度队列_{uid}")
|
||||
self.config = Config.queue_dict[f"调度队列_{uid}"]["Config"]
|
||||
|
||||
layout = QVBoxLayout()
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
self.queue_set = self.QueueSetSettingCard(self.config, self)
|
||||
self.time = self.TimeSettingCard(self.config, self)
|
||||
self.task = self.TaskSettingCard(self.config, self)
|
||||
@@ -393,18 +367,24 @@ class QueueManager(QWidget):
|
||||
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)
|
||||
|
||||
self.setLayout(layout)
|
||||
|
||||
class QueueSetSettingCard(HeaderCardWidget):
|
||||
|
||||
def __init__(self, config: QueueConfig, parent=None):
|
||||
@@ -419,15 +399,23 @@ class QueueManager(QWidget):
|
||||
content="用于标识调度队列的名称",
|
||||
text="请输入调度队列名称",
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queueSet_Name,
|
||||
configItem=self.config.QueueSet_Name,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Enable = SwitchSettingCard(
|
||||
icon=FluentIcon.HOME,
|
||||
title="状态",
|
||||
content="调度队列状态,仅启用时会执行定时任务",
|
||||
self.card_StartUpEnabled = SwitchSettingCard(
|
||||
icon=FluentIcon.CHECKBOX,
|
||||
title="启动时运行",
|
||||
content="调度队列启动时运行状态,启用后将在软件启动时自动运行本队列",
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queueSet_Enabled,
|
||||
configItem=self.config.QueueSet_StartUpEnabled,
|
||||
parent=self,
|
||||
)
|
||||
self.card_TimeEnable = SwitchSettingCard(
|
||||
icon=FluentIcon.CHECKBOX,
|
||||
title="定时运行",
|
||||
content="调度队列定时运行状态,启用时会执行定时任务",
|
||||
qconfig=self.config,
|
||||
configItem=self.config.QueueSet_TimeEnabled,
|
||||
parent=self,
|
||||
)
|
||||
self.card_AfterAccomplish = ComboBoxSettingCard(
|
||||
@@ -440,15 +428,17 @@ class QueueManager(QWidget):
|
||||
"睡眠(win系统需禁用休眠)",
|
||||
"休眠",
|
||||
"关机",
|
||||
"关机(强制)",
|
||||
],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queueSet_AfterAccomplish,
|
||||
configItem=self.config.QueueSet_AfterAccomplish,
|
||||
parent=self,
|
||||
)
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_Name)
|
||||
Layout.addWidget(self.card_Enable)
|
||||
Layout.addWidget(self.card_StartUpEnabled)
|
||||
Layout.addWidget(self.card_TimeEnable)
|
||||
Layout.addWidget(self.card_AfterAccomplish)
|
||||
|
||||
self.viewLayout.addLayout(Layout)
|
||||
@@ -467,107 +457,29 @@ class QueueManager(QWidget):
|
||||
Layout_2 = QVBoxLayout(widget_2)
|
||||
Layout = QHBoxLayout()
|
||||
|
||||
self.card_Time_0 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 1",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_0,
|
||||
configItem_time=self.config.time_TimeSet_0,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_1 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 2",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_1,
|
||||
configItem_time=self.config.time_TimeSet_1,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_2 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 3",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_2,
|
||||
configItem_time=self.config.time_TimeSet_2,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_3 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 4",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_3,
|
||||
configItem_time=self.config.time_TimeSet_3,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_4 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 5",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_4,
|
||||
configItem_time=self.config.time_TimeSet_4,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_5 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 6",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_5,
|
||||
configItem_time=self.config.time_TimeSet_5,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_6 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 7",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_6,
|
||||
configItem_time=self.config.time_TimeSet_6,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_7 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 8",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_7,
|
||||
configItem_time=self.config.time_TimeSet_7,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_8 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 9",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_8,
|
||||
configItem_time=self.config.time_TimeSet_8,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Time_9 = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title="定时 10",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.time_TimeEnabled_9,
|
||||
configItem_time=self.config.time_TimeSet_9,
|
||||
parent=self,
|
||||
)
|
||||
self.card_dict: Dict[str, TimeEditSettingCard] = {}
|
||||
|
||||
for i in range(10):
|
||||
|
||||
self.card_dict[f"Time_{i}"] = TimeEditSettingCard(
|
||||
icon=FluentIcon.STOP_WATCH,
|
||||
title=f"定时 {i + 1}",
|
||||
content=None,
|
||||
qconfig=self.config,
|
||||
configItem_bool=self.config.config_item_dict["Time"][
|
||||
f"Enabled_{i}"
|
||||
],
|
||||
configItem_time=self.config.config_item_dict["Time"][
|
||||
f"Set_{i}"
|
||||
],
|
||||
parent=self,
|
||||
)
|
||||
|
||||
if i < 5:
|
||||
Layout_1.addWidget(self.card_dict[f"Time_{i}"])
|
||||
else:
|
||||
Layout_2.addWidget(self.card_dict[f"Time_{i}"])
|
||||
|
||||
Layout_1.addWidget(self.card_Time_0)
|
||||
Layout_1.addWidget(self.card_Time_1)
|
||||
Layout_1.addWidget(self.card_Time_2)
|
||||
Layout_1.addWidget(self.card_Time_3)
|
||||
Layout_1.addWidget(self.card_Time_4)
|
||||
Layout_2.addWidget(self.card_Time_5)
|
||||
Layout_2.addWidget(self.card_Time_6)
|
||||
Layout_2.addWidget(self.card_Time_7)
|
||||
Layout_2.addWidget(self.card_Time_8)
|
||||
Layout_2.addWidget(self.card_Time_9)
|
||||
Layout.addWidget(widget_1)
|
||||
Layout.addWidget(widget_2)
|
||||
|
||||
@@ -581,130 +493,41 @@ class QueueManager(QWidget):
|
||||
self.setTitle("任务队列")
|
||||
self.config = config
|
||||
|
||||
member_list = [
|
||||
["禁用"] + [_ for _ in Config.member_dict.keys()],
|
||||
script_list = [
|
||||
["禁用"] + [_ for _ in Config.script_dict.keys()],
|
||||
["未启用"]
|
||||
+ [
|
||||
(
|
||||
k
|
||||
if v["Config"].get(v["Config"].MaaSet_Name) == ""
|
||||
else f"{k} - {v["Config"].get(v["Config"].MaaSet_Name)}"
|
||||
if v["Config"].get_name() == ""
|
||||
else f"{k} - {v["Config"].get_name()}"
|
||||
)
|
||||
for k, v in Config.member_dict.items()
|
||||
for k, v in Config.script_dict.items()
|
||||
],
|
||||
]
|
||||
|
||||
self.card_Member_1 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 1",
|
||||
content="第一个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_1,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_2 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 2",
|
||||
content="第二个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_2,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_3 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 3",
|
||||
content="第三个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_3,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_4 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 4",
|
||||
content="第四个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_4,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_5 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 5",
|
||||
content="第五个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_5,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_6 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 6",
|
||||
content="第六个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_6,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_7 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 7",
|
||||
content="第七个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_7,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_8 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 8",
|
||||
content="第八个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_8,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_9 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 9",
|
||||
content="第九个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_9,
|
||||
parent=self,
|
||||
)
|
||||
self.card_Member_10 = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title="任务实例 10",
|
||||
content="第十个调起的脚本任务实例",
|
||||
value=member_list[0],
|
||||
texts=member_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.queue_Member_10,
|
||||
parent=self,
|
||||
)
|
||||
self.card_dict: Dict[
|
||||
str,
|
||||
NoOptionComboBoxSettingCard,
|
||||
] = {}
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_Member_1)
|
||||
Layout.addWidget(self.card_Member_2)
|
||||
Layout.addWidget(self.card_Member_3)
|
||||
Layout.addWidget(self.card_Member_4)
|
||||
Layout.addWidget(self.card_Member_5)
|
||||
Layout.addWidget(self.card_Member_6)
|
||||
Layout.addWidget(self.card_Member_7)
|
||||
Layout.addWidget(self.card_Member_8)
|
||||
Layout.addWidget(self.card_Member_9)
|
||||
Layout.addWidget(self.card_Member_10)
|
||||
|
||||
for i in range(10):
|
||||
|
||||
self.card_dict[f"Script_{i}"] = NoOptionComboBoxSettingCard(
|
||||
icon=FluentIcon.APPLICATION,
|
||||
title=f"任务实例 {i + 1}",
|
||||
content=f"第{i + 1}个调起的脚本任务实例",
|
||||
value=script_list[0],
|
||||
texts=script_list[1],
|
||||
qconfig=self.config,
|
||||
configItem=self.config.config_item_dict["Queue"][
|
||||
f"Script_{i}"
|
||||
],
|
||||
parent=self,
|
||||
)
|
||||
|
||||
Layout.addWidget(self.card_dict[f"Script_{i}"])
|
||||
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
3876
app/ui/script_manager.py
Normal file
@@ -21,12 +21,12 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA设置界面
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
from PySide6.QtWidgets import QApplication, QWidget, QVBoxLayout
|
||||
from PySide6.QtWidgets import QWidget, QVBoxLayout
|
||||
from PySide6.QtGui import QIcon
|
||||
from PySide6.QtCore import Qt
|
||||
from qfluentwidgets import (
|
||||
ScrollArea,
|
||||
@@ -48,8 +48,9 @@ from packaging import version
|
||||
from pathlib import Path
|
||||
from typing import Dict, Union
|
||||
|
||||
from app.core import Config, MainInfoBar, Network
|
||||
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,
|
||||
@@ -68,10 +69,8 @@ class Setting(QWidget):
|
||||
super().__init__(parent)
|
||||
self.setObjectName("设置")
|
||||
|
||||
content_widget = QWidget()
|
||||
content_layout = QVBoxLayout(content_widget)
|
||||
|
||||
self.function = FunctionSettingCard(self)
|
||||
self.voice = VoiceSettingCard(self)
|
||||
self.start = StartSettingCard(self)
|
||||
self.ui = UiSettingCard(self)
|
||||
self.notification = NotifySettingCard(self)
|
||||
@@ -86,12 +85,17 @@ class Setting(QWidget):
|
||||
)
|
||||
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)
|
||||
@@ -101,10 +105,12 @@ class Setting(QWidget):
|
||||
|
||||
scrollArea = ScrollArea()
|
||||
scrollArea.setWidgetResizable(True)
|
||||
scrollArea.setContentsMargins(0, 0, 0, 0)
|
||||
scrollArea.setStyleSheet("background: transparent; border: none;")
|
||||
scrollArea.setWidget(content_widget)
|
||||
layout = QVBoxLayout()
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.addWidget(scrollArea)
|
||||
self.setLayout(layout)
|
||||
|
||||
def agree_bilibili(self) -> None:
|
||||
"""授权bilibili游戏隐私政策"""
|
||||
@@ -113,11 +119,11 @@ class Setting(QWidget):
|
||||
|
||||
choice = MessageBox(
|
||||
"授权声明",
|
||||
"开启“托管bilibili游戏隐私政策”功能,即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》,并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
|
||||
"开启「托管bilibili游戏隐私政策」功能,即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》,并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
|
||||
self.window(),
|
||||
)
|
||||
if choice.exec():
|
||||
logger.success("确认授权bilibili游戏隐私政策")
|
||||
logger.success("确认授权bilibili游戏隐私政策", module="设置界面")
|
||||
MainInfoBar.push_info_bar(
|
||||
"success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000
|
||||
)
|
||||
@@ -125,7 +131,7 @@ class Setting(QWidget):
|
||||
Config.set(Config.function_IfAgreeBilibili, False)
|
||||
else:
|
||||
|
||||
logger.info("取消授权bilibili游戏隐私政策")
|
||||
logger.info("取消授权bilibili游戏隐私政策", module="设置界面")
|
||||
MainInfoBar.push_info_bar(
|
||||
"info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000
|
||||
)
|
||||
@@ -141,7 +147,7 @@ class Setting(QWidget):
|
||||
|
||||
choice = MessageBox(
|
||||
"风险声明",
|
||||
"开启“跳过MuMu启动广告”功能,即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告,并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?",
|
||||
"开启「跳过MuMu启动广告」功能,即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告,并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?",
|
||||
self.window(),
|
||||
)
|
||||
if choice.exec():
|
||||
@@ -151,7 +157,7 @@ class Setting(QWidget):
|
||||
|
||||
MuMu_splash_ads_path.touch()
|
||||
|
||||
logger.success("开启跳过MuMu启动广告功能")
|
||||
logger.success("开启跳过MuMu启动广告功能", module="设置界面")
|
||||
MainInfoBar.push_info_bar(
|
||||
"success", "操作成功", "已开启跳过MuMu启动广告功能", 3000
|
||||
)
|
||||
@@ -163,7 +169,7 @@ class Setting(QWidget):
|
||||
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
|
||||
MuMu_splash_ads_path.unlink()
|
||||
|
||||
logger.info("关闭跳过MuMu启动广告功能")
|
||||
logger.info("关闭跳过MuMu启动广告功能", module="设置界面")
|
||||
MainInfoBar.push_info_bar(
|
||||
"info", "操作成功", "已关闭跳过MuMu启动广告功能", 3000
|
||||
)
|
||||
@@ -174,6 +180,8 @@ class Setting(QWidget):
|
||||
if Config.key_path.exists():
|
||||
return None
|
||||
|
||||
logger.info("未设置管理密钥,开始要求用户进行设置", module="设置界面")
|
||||
|
||||
while True:
|
||||
|
||||
choice = LineEditMessageBox(
|
||||
@@ -181,6 +189,7 @@ class Setting(QWidget):
|
||||
)
|
||||
if choice.exec() and choice.input.text() != "":
|
||||
Crypto.get_PASSWORD(choice.input.text())
|
||||
logger.success("成功设置管理密钥", module="设置界面")
|
||||
break
|
||||
else:
|
||||
choice = MessageBox(
|
||||
@@ -200,10 +209,7 @@ class Setting(QWidget):
|
||||
while if_change:
|
||||
|
||||
choice = LineEditMessageBox(
|
||||
self.window(),
|
||||
"请输入旧的管理密钥",
|
||||
"旧管理密钥",
|
||||
"密码",
|
||||
self.window(), "请输入旧的管理密钥", "旧管理密钥", "密码"
|
||||
)
|
||||
if choice.exec() and choice.input.text() != "":
|
||||
|
||||
@@ -224,6 +230,7 @@ class Setting(QWidget):
|
||||
|
||||
# 修改管理密钥
|
||||
Crypto.change_PASSWORD(PASSWORD_old, choice.input.text())
|
||||
logger.success("成功修改管理密钥", module="设置界面")
|
||||
MainInfoBar.push_info_bar(
|
||||
"success", "操作成功", "管理密钥修改成功", 3000
|
||||
)
|
||||
@@ -248,79 +255,143 @@ class Setting(QWidget):
|
||||
choice.exec()
|
||||
else:
|
||||
choice = MessageBox(
|
||||
"确认",
|
||||
"您没有输入管理密钥,是否取消修改管理密钥?",
|
||||
self.window(),
|
||||
"确认", "您没有输入管理密钥,是否取消修改管理密钥?", 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(".")))
|
||||
|
||||
if Network.if_running and if_show:
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning", "请求速度过快", "上个网络请求还未结束,请稍等片刻", 5000
|
||||
)
|
||||
return None
|
||||
# 从远程服务器获取最新版本信息
|
||||
Network.set_info(
|
||||
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.start()
|
||||
Network.loop.exec()
|
||||
if Network.stutus_code == 200:
|
||||
version_info: Dict[str, Union[int, str, Dict[str, str]]] = (
|
||||
Network.response_json
|
||||
)
|
||||
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:
|
||||
logger.warning(f"获取版本信息时出错:{Network.error_message}")
|
||||
|
||||
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.stutus_code}",
|
||||
f"网络错误:{network_result['status_code']}",
|
||||
5000,
|
||||
)
|
||||
return None
|
||||
|
||||
if version_info["code"] != 0:
|
||||
|
||||
logger.error(f"获取版本信息时出错:{version_info["msg"]}")
|
||||
|
||||
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
|
||||
|
||||
remote_version = list(
|
||||
map(
|
||||
int,
|
||||
@@ -373,45 +444,78 @@ class Setting(QWidget):
|
||||
else:
|
||||
all_version_info[key] = value.copy()
|
||||
|
||||
version_info = {
|
||||
"更新总览": 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()
|
||||
},
|
||||
}
|
||||
|
||||
# 询问是否开始版本更新
|
||||
choice = NoticeMessageBox(self.window(), "版本更新", version_info)
|
||||
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():
|
||||
|
||||
with Config.version_path.open(mode="r", encoding="utf-8") as f:
|
||||
version_info = json.load(f)
|
||||
version_info["main_version"] = Config.VERSION
|
||||
with Config.version_path.open(mode="w", encoding="utf-8") as f:
|
||||
json.dump(version_info, f, ensure_ascii=False, indent=4)
|
||||
|
||||
if (Config.app_path / "AUTO_Updater.exe").exists():
|
||||
shutil.copy(
|
||||
Config.app_path / "AUTO_Updater.exe",
|
||||
Config.app_path / "AUTO_Updater.active.exe",
|
||||
)
|
||||
if "url" in version_info["data"]:
|
||||
download_config = {
|
||||
"mode": "MirrorChyan",
|
||||
"thread_numb": 1,
|
||||
"url": version_info["data"]["url"],
|
||||
}
|
||||
else:
|
||||
logger.error("更新器文件不存在")
|
||||
MainInfoBar.push_info_bar(
|
||||
"error", "更新器不存在", "请手动前往 GitHub 获取最新版本", -1
|
||||
)
|
||||
return None
|
||||
|
||||
subprocess.Popen(
|
||||
[Config.app_path / "AUTO_Updater.active.exe"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
| subprocess.CREATE_NO_WINDOW,
|
||||
# 从远程服务器获取代理信息
|
||||
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.window().close()
|
||||
QApplication.quit()
|
||||
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
|
||||
@@ -428,28 +532,54 @@ class Setting(QWidget):
|
||||
"发现新版本",
|
||||
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.set_info(
|
||||
network = Network.add_task(
|
||||
mode="get",
|
||||
url="https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json",
|
||||
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/notice.json",
|
||||
)
|
||||
Network.start()
|
||||
Network.loop.exec()
|
||||
if Network.stutus_code == 200:
|
||||
notice = Network.response_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.error_message}")
|
||||
logger.warning(
|
||||
f"获取最新公告时出错:{network_result['error_message']}",
|
||||
module="设置界面",
|
||||
)
|
||||
MainInfoBar.push_info_bar(
|
||||
"warning",
|
||||
"获取最新公告时出错",
|
||||
f"网络错误:{Network.stutus_code}",
|
||||
f"网络错误:{network_result['status_code']}",
|
||||
5000,
|
||||
)
|
||||
return None
|
||||
@@ -481,11 +611,38 @@ class Setting(QWidget):
|
||||
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()
|
||||
@@ -494,8 +651,9 @@ class Setting(QWidget):
|
||||
):
|
||||
|
||||
MainInfoBar.push_info_bar(
|
||||
"info", "有新公告", "请前往设置界面查看公告", 3600000
|
||||
"info", "有新公告", "请前往设置界面查看公告", 3600000, if_force=True
|
||||
)
|
||||
SoundPlayer.play("公告通知")
|
||||
return None
|
||||
|
||||
|
||||
@@ -588,7 +746,7 @@ class FunctionSettingCard(HeaderCardWidget):
|
||||
self.card_BossKey = LineEditSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="模拟器老板键",
|
||||
content="请输入对应的模拟器老板键,请直接输入文字,多个键位之间请用“+”隔开。如:“Alt+Q”",
|
||||
content="请输入对应的模拟器老板键,请直接输入文字,多个键位之间请用「+」隔开。如:「Alt+Q」",
|
||||
text="请以文字形式输入模拟器老板快捷键",
|
||||
qconfig=Config,
|
||||
configItem=Config.function_BossKey,
|
||||
@@ -604,6 +762,36 @@ class FunctionSettingCard(HeaderCardWidget):
|
||||
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):
|
||||
@@ -618,14 +806,6 @@ class StartSettingCard(HeaderCardWidget):
|
||||
configItem=Config.start_IfSelfStart,
|
||||
parent=self,
|
||||
)
|
||||
self.card_IfRunDirectly = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="启动后直接运行主任务",
|
||||
content="启动AUTO_MAA后自动运行自动代理任务,优先级:调度队列 1 > 脚本 1",
|
||||
qconfig=Config,
|
||||
configItem=Config.start_IfRunDirectly,
|
||||
parent=self,
|
||||
)
|
||||
self.card_IfMinimizeDirectly = SwitchSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="启动后直接最小化",
|
||||
@@ -637,7 +817,6 @@ class StartSettingCard(HeaderCardWidget):
|
||||
|
||||
Layout = QVBoxLayout()
|
||||
Layout.addWidget(self.card_IfSelfStart)
|
||||
Layout.addWidget(self.card_IfRunDirectly)
|
||||
Layout.addWidget(self.card_IfMinimizeDirectly)
|
||||
self.viewLayout.addLayout(Layout)
|
||||
|
||||
@@ -869,7 +1048,7 @@ class NotifySettingCard(HeaderCardWidget):
|
||||
self.card_ServerChanChannel = LineEditSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="ServerChanChannel代码",
|
||||
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
|
||||
content="可以留空,留空则默认。可以多个,请使用「|」隔开",
|
||||
text="请输入需要推送的Channel代码(SCT生效)",
|
||||
qconfig=Config,
|
||||
configItem=Config.notify_ServerChanChannel,
|
||||
@@ -878,7 +1057,7 @@ class NotifySettingCard(HeaderCardWidget):
|
||||
self.card_ServerChanTag = LineEditSettingCard(
|
||||
icon=FluentIcon.PAGE_RIGHT,
|
||||
title="Tag内容",
|
||||
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
|
||||
content="可以留空,留空则默认。可以多个,请使用「|」隔开",
|
||||
text="请输入加入推送的Tag(SC3生效)",
|
||||
qconfig=Config,
|
||||
configItem=Config.notify_ServerChanTag,
|
||||
@@ -944,9 +1123,17 @@ class SecuritySettingCard(HeaderCardWidget):
|
||||
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)
|
||||
|
||||
|
||||
@@ -988,6 +1175,15 @@ class UpdaterSettingCard(HeaderCardWidget):
|
||||
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="代理地址列表",
|
||||
@@ -1007,7 +1203,9 @@ class UpdaterSettingCard(HeaderCardWidget):
|
||||
parent=self,
|
||||
)
|
||||
mirrorchyan_url = HyperlinkButton(
|
||||
"https://mirrorchyan.com/", "获取Mirror酱CDK", self
|
||||
"https://mirrorchyan.com/zh/get-start?source=auto_maa-setting",
|
||||
"获取Mirror酱CDK",
|
||||
self,
|
||||
)
|
||||
self.card_MirrorChyanCDK.hBoxLayout.insertWidget(
|
||||
5, mirrorchyan_url, 0, Qt.AlignRight
|
||||
@@ -1018,6 +1216,7 @@ class UpdaterSettingCard(HeaderCardWidget):
|
||||
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)
|
||||
|
||||
93
app/utils/AUTO_MAA.iss
Normal file
@@ -0,0 +1,93 @@
|
||||
; 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;
|
||||
95
app/utils/ImageUtils.py
Normal file
@@ -0,0 +1,95 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2025 ClozyA
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA图像组件
|
||||
v4.4
|
||||
作者:ClozyA
|
||||
"""
|
||||
|
||||
import base64
|
||||
import hashlib
|
||||
from pathlib import Path
|
||||
|
||||
from PIL import Image
|
||||
|
||||
|
||||
class ImageUtils:
|
||||
@staticmethod
|
||||
def get_base64_from_file(image_path):
|
||||
"""从本地文件读取并返回base64编码字符串"""
|
||||
with open(image_path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
@staticmethod
|
||||
def calculate_md5_from_file(image_path):
|
||||
"""从本地文件读取并返回md5值(hex字符串)"""
|
||||
with open(image_path, "rb") as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def calculate_md5_from_base64(base64_content):
|
||||
"""从base64字符串计算md5"""
|
||||
image_data = base64.b64decode(base64_content)
|
||||
return hashlib.md5(image_data).hexdigest()
|
||||
|
||||
@staticmethod
|
||||
def compress_image_if_needed(image_path: Path, max_size_mb=2) -> Path:
|
||||
"""
|
||||
如果图片大于max_size_mb,则压缩并覆盖原文件,返回原始路径(Path对象)
|
||||
"""
|
||||
if hasattr(Image, "Resampling"): # Pillow 9.1.0及以后
|
||||
RESAMPLE = Image.Resampling.LANCZOS
|
||||
else:
|
||||
RESAMPLE = Image.ANTIALIAS
|
||||
|
||||
max_size = max_size_mb * 1024 * 1024
|
||||
if image_path.stat().st_size <= max_size:
|
||||
return image_path
|
||||
|
||||
img = Image.open(image_path)
|
||||
suffix = image_path.suffix.lower()
|
||||
quality = 90 if suffix in [".jpg", ".jpeg"] else None
|
||||
step = 5
|
||||
|
||||
if suffix in [".jpg", ".jpeg"]:
|
||||
while True:
|
||||
img.save(image_path, quality=quality, optimize=True)
|
||||
if image_path.stat().st_size <= max_size or quality <= 10:
|
||||
break
|
||||
quality -= step
|
||||
elif suffix == ".png":
|
||||
width, height = img.size
|
||||
while True:
|
||||
img.save(image_path, optimize=True)
|
||||
if (
|
||||
image_path.stat().st_size <= max_size
|
||||
or width <= 200
|
||||
or height <= 200
|
||||
):
|
||||
break
|
||||
width = int(width * 0.95)
|
||||
height = int(height * 0.95)
|
||||
img = img.resize((width, height), RESAMPLE)
|
||||
else:
|
||||
raise ValueError("仅支持JPG/JPEG和PNG格式图片的压缩。")
|
||||
|
||||
return image_path
|
||||
164
app/utils/ProcessManager.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA进程管理组件
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
|
||||
import psutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from datetime import datetime
|
||||
|
||||
from PySide6.QtCore import QTimer, QObject, Signal
|
||||
|
||||
|
||||
class ProcessManager(QObject):
|
||||
"""进程监视器类,用于跟踪主进程及其所有子进程的状态"""
|
||||
|
||||
processClosed = Signal()
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.main_pid = None
|
||||
self.tracked_pids = set()
|
||||
|
||||
self.check_timer = QTimer()
|
||||
self.check_timer.timeout.connect(self.check_processes)
|
||||
|
||||
def open_process(self, path: Path, args: list = [], tracking_time: int = 60) -> int:
|
||||
"""
|
||||
启动一个新进程并返回其pid,并开始监视该进程
|
||||
|
||||
:param path: 可执行文件的路径
|
||||
:param args: 启动参数列表
|
||||
:param tracking_time: 子进程追踪持续时间(秒)
|
||||
:return: 新进程的PID
|
||||
"""
|
||||
|
||||
process = subprocess.Popen(
|
||||
[path, *args],
|
||||
cwd=path.parent,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
|
||||
self.start_monitoring(process.pid, tracking_time)
|
||||
|
||||
def start_monitoring(self, pid: int, tracking_time: int = 60) -> None:
|
||||
"""
|
||||
启动进程监视器,跟踪指定的主进程及其子进程
|
||||
|
||||
:param pid: 被监视进程的PID
|
||||
:param tracking_time: 子进程追踪持续时间(秒)
|
||||
"""
|
||||
|
||||
self.clear()
|
||||
|
||||
self.main_pid = pid
|
||||
self.tracking_time = tracking_time
|
||||
|
||||
# 扫描并记录所有相关进程
|
||||
try:
|
||||
# 获取主进程
|
||||
main_proc = psutil.Process(self.main_pid)
|
||||
self.tracked_pids.add(self.main_pid)
|
||||
|
||||
# 递归获取所有子进程
|
||||
if tracking_time:
|
||||
for child in main_proc.children(recursive=True):
|
||||
self.tracked_pids.add(child.pid)
|
||||
|
||||
except psutil.NoSuchProcess:
|
||||
pass
|
||||
|
||||
# 启动持续追踪机制
|
||||
self.start_time = datetime.now()
|
||||
self.check_timer.start(100)
|
||||
|
||||
def check_processes(self) -> None:
|
||||
"""检查跟踪的进程是否仍在运行,并更新子进程列表"""
|
||||
|
||||
# 仅在时限内持续更新跟踪的进程列表,发现新的子进程
|
||||
if (datetime.now() - self.start_time).total_seconds() < self.tracking_time:
|
||||
|
||||
current_pids = set(self.tracked_pids)
|
||||
for pid in current_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
for child in proc.children():
|
||||
if child.pid not in self.tracked_pids:
|
||||
# 新发现的子进程
|
||||
self.tracked_pids.add(child.pid)
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
if not self.is_running():
|
||||
self.clear()
|
||||
self.processClosed.emit()
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""检查所有跟踪的进程是否还在运行"""
|
||||
|
||||
for pid in self.tracked_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
if proc.is_running():
|
||||
return True
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
return False
|
||||
|
||||
def kill(self, if_force: bool = False) -> None:
|
||||
"""停止监视器并中止所有跟踪的进程"""
|
||||
|
||||
self.check_timer.stop()
|
||||
|
||||
for pid in self.tracked_pids:
|
||||
try:
|
||||
proc = psutil.Process(pid)
|
||||
if if_force:
|
||||
kill_process = subprocess.Popen(
|
||||
["taskkill", "/F", "/T", "/PID", str(pid)],
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
kill_process.wait()
|
||||
proc.terminate()
|
||||
except psutil.NoSuchProcess:
|
||||
continue
|
||||
|
||||
if self.main_pid:
|
||||
self.processClosed.emit()
|
||||
self.clear()
|
||||
|
||||
def clear(self) -> None:
|
||||
"""清空跟踪的进程列表"""
|
||||
|
||||
self.main_pid = None
|
||||
self.check_timer.stop()
|
||||
self.tracked_pids.clear()
|
||||
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA工具包
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
@@ -29,6 +29,7 @@ __version__ = "4.2.0"
|
||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||
__license__ = "GPL-3.0 license"
|
||||
|
||||
from .downloader import DownloadManager
|
||||
from .ImageUtils import ImageUtils
|
||||
from .ProcessManager import ProcessManager
|
||||
|
||||
__all__ = ["DownloadManager"]
|
||||
__all__ = ["ImageUtils", "ProcessManager"]
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA打包程序
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
@@ -66,18 +66,17 @@ if __name__ == "__main__":
|
||||
version = json.load(f)
|
||||
|
||||
main_version_numb = list(map(int, version["main_version"].split(".")))
|
||||
updater_version_numb = list(map(int, version["updater_version"].split(".")))
|
||||
|
||||
print("Packaging AUTO_MAA main program ...")
|
||||
|
||||
os.system(
|
||||
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
|
||||
" --enable-plugins=pyside6 --windows-console-mode=disable"
|
||||
"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"]}"
|
||||
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"
|
||||
@@ -86,35 +85,10 @@ if __name__ == "__main__":
|
||||
|
||||
print("AUTO_MAA main program packaging completed !")
|
||||
|
||||
print("Packaging AUTO_MAA update program ...")
|
||||
|
||||
shutil.copy(root_path / "app/utils/downloader.py", root_path)
|
||||
os.system(
|
||||
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
|
||||
" --enable-plugins=pyside6 --windows-console-mode=disable"
|
||||
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA_Updater'"
|
||||
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA_Updater.ico"
|
||||
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
|
||||
f" --file-version={version["updater_version"]}"
|
||||
f" --product-version={version["main_version"]}"
|
||||
" --file-description='AUTO_MAA Component'"
|
||||
" --copyright='Copyright © 2024-2025 DLmaster361'"
|
||||
" --assume-yes-for-downloads --output-filename=AUTO_Updater"
|
||||
" --remove-output downloader.py"
|
||||
)
|
||||
(root_path / "downloader.py").unlink()
|
||||
|
||||
print("AUTO_MAA update program packaging completed !")
|
||||
print("start to create setup program ...")
|
||||
|
||||
(root_path / "AUTO_MAA").mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print("Start to move AUTO_MAA program ...")
|
||||
|
||||
shutil.move(root_path / "AUTO_MAA.exe", root_path / "AUTO_MAA/")
|
||||
shutil.move(root_path / "AUTO_Updater.exe", root_path / "AUTO_MAA/")
|
||||
|
||||
print("Start to copy rescourses ...")
|
||||
|
||||
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/")
|
||||
@@ -122,17 +96,38 @@ if __name__ == "__main__":
|
||||
shutil.copy(root_path / "README.md", root_path / "AUTO_MAA/")
|
||||
shutil.copy(root_path / "LICENSE", root_path / "AUTO_MAA/")
|
||||
|
||||
print("Start to compress ...")
|
||||
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",
|
||||
root_dir=root_path / "AUTO_MAA_Setup",
|
||||
base_dir=".",
|
||||
)
|
||||
shutil.rmtree(root_path / "AUTO_MAA")
|
||||
|
||||
print("compress completed !")
|
||||
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():
|
||||
@@ -143,6 +138,6 @@ if __name__ == "__main__":
|
||||
all_version_info[key] = value.copy()
|
||||
|
||||
(root_path / "version_info.txt").write_text(
|
||||
f"{version_text(main_version_numb)}\n{version_text(updater_version_numb)}\n<!--{json.dumps(version["version_info"], ensure_ascii=False)}-->\n{version_info_markdown(all_version_info)}",
|
||||
f"{version_text(main_version_numb)}\n\n<!--{json.dumps(version["version_info"], ensure_ascii=False)}-->\n{version_info_markdown(all_version_info)}",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
46
main.py
@@ -21,14 +21,44 @@
|
||||
"""
|
||||
AUTO_MAA
|
||||
AUTO_MAA主程序
|
||||
v4.3
|
||||
v4.4
|
||||
作者:DLmaster_361
|
||||
"""
|
||||
|
||||
from loguru import logger
|
||||
# 屏蔽广告
|
||||
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 sys
|
||||
|
||||
from app.core.logger import logger
|
||||
|
||||
|
||||
def is_admin() -> bool:
|
||||
"""检查当前程序是否以管理员身份运行"""
|
||||
try:
|
||||
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||
except:
|
||||
return False
|
||||
|
||||
|
||||
@logger.catch
|
||||
@@ -42,11 +72,17 @@ def main():
|
||||
from app.ui.main_window import AUTO_MAA
|
||||
|
||||
window = AUTO_MAA()
|
||||
window.show_ui("显示主窗口")
|
||||
window.show_ui("显示主窗口", if_start=True)
|
||||
window.start_up_task()
|
||||
sys.exit(application.exec())
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
main()
|
||||
if is_admin():
|
||||
main()
|
||||
else:
|
||||
ctypes.windll.shell32.ShellExecuteW(
|
||||
None, "runas", sys.executable, os.path.realpath(sys.argv[0]), None, 1
|
||||
)
|
||||
sys.exit(0)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
loguru
|
||||
plyer
|
||||
PySide6
|
||||
PySide6-Fluent-Widgets[full]
|
||||
psutil
|
||||
opencv-python
|
||||
pywin32
|
||||
pyautogui
|
||||
pycryptodome
|
||||
requests
|
||||
markdown
|
||||
Jinja2
|
||||
serverchan_sdk
|
||||
nuitka
|
||||
loguru==0.7.3
|
||||
plyer==2.1.0
|
||||
PySide6==6.9.1
|
||||
PySide6-Fluent-Widgets[full]==1.8.3
|
||||
psutil==7.0.0
|
||||
pywin32==310
|
||||
keyboard==0.13.5
|
||||
pycryptodome==3.23.0
|
||||
certifi==2025.4.26
|
||||
truststore==0.10.1
|
||||
requests==2.32.4
|
||||
markdown==3.8.2
|
||||
Jinja2==3.1.6
|
||||
nuitka==2.7.12
|
||||
pillow==11.3.0
|
||||
packaging==25.0
|
||||
|
||||
403
resources/docs/ChineseSimplified.isl
Normal file
@@ -0,0 +1,403 @@
|
||||
; *** Inno Setup version 6.4.0+ Chinese Simplified messages ***
|
||||
;
|
||||
; To download user-contributed translations of this file, go to:
|
||||
; https://jrsoftware.org/files/istrans/
|
||||
;
|
||||
; Note: When translating this text, do not add periods (.) to the end of
|
||||
; messages that didn't have them already, because on those messages Inno
|
||||
; Setup adds the periods automatically (appending a period would result in
|
||||
; two periods being displayed).
|
||||
;
|
||||
; Maintained by Zhenghan Yang
|
||||
; Email: 847320916@QQ.com
|
||||
; Translation based on network resource
|
||||
; The latest Translation is on https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||
;
|
||||
|
||||
[LangOptions]
|
||||
; The following three entries are very important. Be sure to read and
|
||||
; understand the '[LangOptions] section' topic in the help file.
|
||||
LanguageName=简体中文
|
||||
; If Language Name display incorrect, uncomment next line
|
||||
; LanguageName=<7B80><4F53><4E2D><6587>
|
||||
; About LanguageID, to reference link:
|
||||
; https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-lcid/a9eac961-e77d-41a6-90a5-ce1a8b0cdb9c
|
||||
LanguageID=$0804
|
||||
; About CodePage, to reference link:
|
||||
; https://docs.microsoft.com/en-us/windows/win32/intl/code-page-identifiers
|
||||
LanguageCodePage=936
|
||||
; If the language you are translating to requires special font faces or
|
||||
; sizes, uncomment any of the following entries and change them accordingly.
|
||||
;DialogFontName=
|
||||
;DialogFontSize=8
|
||||
;WelcomeFontName=Verdana
|
||||
;WelcomeFontSize=12
|
||||
;TitleFontName=Arial
|
||||
;TitleFontSize=29
|
||||
;CopyrightFontName=Arial
|
||||
;CopyrightFontSize=8
|
||||
|
||||
[Messages]
|
||||
|
||||
; *** 应用程序标题
|
||||
SetupAppTitle=安装
|
||||
SetupWindowTitle=安装 - %1
|
||||
UninstallAppTitle=卸载
|
||||
UninstallAppFullTitle=%1 卸载
|
||||
|
||||
; *** Misc. common
|
||||
InformationTitle=信息
|
||||
ConfirmTitle=确认
|
||||
ErrorTitle=错误
|
||||
|
||||
; *** SetupLdr messages
|
||||
SetupLdrStartupMessage=现在将安装 %1。您想要继续吗?
|
||||
LdrCannotCreateTemp=无法创建临时文件。安装程序已中止
|
||||
LdrCannotExecTemp=无法执行临时目录中的文件。安装程序已中止
|
||||
HelpTextNote=
|
||||
|
||||
; *** 启动错误消息
|
||||
LastErrorMessage=%1。%n%n错误 %2: %3
|
||||
SetupFileMissing=安装目录中缺少文件 %1。请修正这个问题或者获取程序的新副本。
|
||||
SetupFileCorrupt=安装文件已损坏。请获取程序的新副本。
|
||||
SetupFileCorruptOrWrongVer=安装文件已损坏,或是与这个安装程序的版本不兼容。请修正这个问题或获取新的程序副本。
|
||||
InvalidParameter=无效的命令行参数:%n%n%1
|
||||
SetupAlreadyRunning=安装程序正在运行。
|
||||
WindowsVersionNotSupported=此程序不支持当前计算机运行的 Windows 版本。
|
||||
WindowsServicePackRequired=此程序需要 %1 服务包 %2 或更高版本。
|
||||
NotOnThisPlatform=此程序不能在 %1 上运行。
|
||||
OnlyOnThisPlatform=此程序只能在 %1 上运行。
|
||||
OnlyOnTheseArchitectures=此程序只能安装到为下列处理器架构设计的 Windows 版本中:%n%n%1
|
||||
WinVersionTooLowError=此程序需要 %1 版本 %2 或更高。
|
||||
WinVersionTooHighError=此程序不能安装于 %1 版本 %2 或更高。
|
||||
AdminPrivilegesRequired=在安装此程序时您必须以管理员身份登录。
|
||||
PowerUserPrivilegesRequired=在安装此程序时您必须以管理员身份或有权限的用户组身份登录。
|
||||
SetupAppRunningError=安装程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||
UninstallAppRunningError=卸载程序发现 %1 当前正在运行。%n%n请先关闭正在运行的程序,然后点击“确定”继续,或点击“取消”退出。
|
||||
|
||||
; *** 启动问题
|
||||
PrivilegesRequiredOverrideTitle=选择安装程序模式
|
||||
PrivilegesRequiredOverrideInstruction=选择安装模式
|
||||
PrivilegesRequiredOverrideText1=%1 可以为所有用户安装(需要管理员权限),或仅为您安装。
|
||||
PrivilegesRequiredOverrideText2=%1 只能为您安装,或为所有用户安装(需要管理员权限)。
|
||||
PrivilegesRequiredOverrideAllUsers=为所有用户安装(&A)
|
||||
PrivilegesRequiredOverrideAllUsersRecommended=为所有用户安装(&A) (建议选项)
|
||||
PrivilegesRequiredOverrideCurrentUser=只为我安装(&M)
|
||||
PrivilegesRequiredOverrideCurrentUserRecommended=只为我安装(&M) (建议选项)
|
||||
|
||||
; *** 其他错误
|
||||
ErrorCreatingDir=安装程序无法创建目录“%1”
|
||||
ErrorTooManyFilesInDir=无法在目录“%1”中创建文件,因为里面包含太多文件
|
||||
|
||||
; *** 安装程序公共消息
|
||||
ExitSetupTitle=退出安装程序
|
||||
ExitSetupMessage=安装程序尚未完成。如果现在退出,将不会安装该程序。%n%n您之后可以再次运行安装程序完成安装。%n%n现在退出安装程序吗?
|
||||
AboutSetupMenuItem=关于安装程序(&A)...
|
||||
AboutSetupTitle=关于安装程序
|
||||
AboutSetupMessage=%1 版本 %2%n%3%n%n%1 主页:%n%4
|
||||
AboutSetupNote=
|
||||
TranslatorNote=简体中文翻译由Kira(847320916@qq.com)维护。项目地址:https://github.com/kira-96/Inno-Setup-Chinese-Simplified-Translation
|
||||
|
||||
; *** 按钮
|
||||
ButtonBack=< 上一步(&B)
|
||||
ButtonNext=下一步(&N) >
|
||||
ButtonInstall=安装(&I)
|
||||
ButtonOK=确定
|
||||
ButtonCancel=取消
|
||||
ButtonYes=是(&Y)
|
||||
ButtonYesToAll=全是(&A)
|
||||
ButtonNo=否(&N)
|
||||
ButtonNoToAll=全否(&O)
|
||||
ButtonFinish=完成(&F)
|
||||
ButtonBrowse=浏览(&B)...
|
||||
ButtonWizardBrowse=浏览(&R)...
|
||||
ButtonNewFolder=新建文件夹(&M)
|
||||
|
||||
; *** “选择语言”对话框消息
|
||||
SelectLanguageTitle=选择安装语言
|
||||
SelectLanguageLabel=选择安装时使用的语言。
|
||||
|
||||
; *** 公共向导文字
|
||||
ClickNext=点击“下一步”继续,或点击“取消”退出安装程序。
|
||||
BeveledLabel=
|
||||
BrowseDialogTitle=浏览文件夹
|
||||
BrowseDialogLabel=在下面的列表中选择一个文件夹,然后点击“确定”。
|
||||
NewFolderName=新建文件夹
|
||||
|
||||
; *** “欢迎”向导页
|
||||
WelcomeLabel1=欢迎使用 [name] 安装向导
|
||||
WelcomeLabel2=现在将安装 [name/ver] 到您的电脑中。%n%n建议您在继续安装前关闭所有其他应用程序。
|
||||
|
||||
; *** “密码”向导页
|
||||
WizardPassword=密码
|
||||
PasswordLabel1=这个安装程序有密码保护。
|
||||
PasswordLabel3=请输入密码,然后点击“下一步”继续。密码区分大小写。
|
||||
PasswordEditLabel=密码(&P):
|
||||
IncorrectPassword=您输入的密码不正确,请重新输入。
|
||||
|
||||
; *** “许可协议”向导页
|
||||
WizardLicense=许可协议
|
||||
LicenseLabel=请在继续安装前阅读以下重要信息。
|
||||
LicenseLabel3=请仔细阅读下列许可协议。在继续安装前您必须同意这些协议条款。
|
||||
LicenseAccepted=我同意此协议(&A)
|
||||
LicenseNotAccepted=我不同意此协议(&D)
|
||||
|
||||
; *** “信息”向导页
|
||||
WizardInfoBefore=信息
|
||||
InfoBeforeLabel=请在继续安装前阅读以下重要信息。
|
||||
InfoBeforeClickLabel=准备好继续安装后,点击“下一步”。
|
||||
WizardInfoAfter=信息
|
||||
InfoAfterLabel=请在继续安装前阅读以下重要信息。
|
||||
InfoAfterClickLabel=准备好继续安装后,点击“下一步”。
|
||||
|
||||
; *** “用户信息”向导页
|
||||
WizardUserInfo=用户信息
|
||||
UserInfoDesc=请输入您的信息。
|
||||
UserInfoName=用户名(&U):
|
||||
UserInfoOrg=组织(&O):
|
||||
UserInfoSerial=序列号(&S):
|
||||
UserInfoNameRequired=您必须输入用户名。
|
||||
|
||||
; *** “选择目标目录”向导页
|
||||
WizardSelectDir=选择目标位置
|
||||
SelectDirDesc=您想将 [name] 安装在哪里?
|
||||
SelectDirLabel3=安装程序将安装 [name] 到下面的文件夹中。
|
||||
SelectDirBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||
DiskSpaceGBLabel=至少需要有 [gb] GB 的可用磁盘空间。
|
||||
DiskSpaceMBLabel=至少需要有 [mb] MB 的可用磁盘空间。
|
||||
CannotInstallToNetworkDrive=安装程序无法安装到一个网络驱动器。
|
||||
CannotInstallToUNCPath=安装程序无法安装到一个 UNC 路径。
|
||||
InvalidPath=您必须输入一个带驱动器卷标的完整路径,例如:%n%nC:\APP%n%n或UNC路径:%n%n\\server\share
|
||||
InvalidDrive=您选定的驱动器或 UNC 共享不存在或不能访问。请选择其他位置。
|
||||
DiskSpaceWarningTitle=磁盘空间不足
|
||||
DiskSpaceWarning=安装程序至少需要 %1 KB 的可用空间才能安装,但选定驱动器只有 %2 KB 的可用空间。%n%n您一定要继续吗?
|
||||
DirNameTooLong=文件夹名称或路径太长。
|
||||
InvalidDirName=文件夹名称无效。
|
||||
BadDirName32=文件夹名称不能包含下列任何字符:%n%n%1
|
||||
DirExistsTitle=文件夹已存在
|
||||
DirExists=文件夹:%n%n%1%n%n已经存在。您一定要安装到这个文件夹中吗?
|
||||
DirDoesntExistTitle=文件夹不存在
|
||||
DirDoesntExist=文件夹:%n%n%1%n%n不存在。您想要创建此文件夹吗?
|
||||
|
||||
; *** “选择组件”向导页
|
||||
WizardSelectComponents=选择组件
|
||||
SelectComponentsDesc=您想安装哪些程序组件?
|
||||
SelectComponentsLabel2=选中您想安装的组件;取消您不想安装的组件。然后点击“下一步”继续。
|
||||
FullInstallation=完全安装
|
||||
; if possible don't translate 'Compact' as 'Minimal' (I mean 'Minimal' in your language)
|
||||
CompactInstallation=简洁安装
|
||||
CustomInstallation=自定义安装
|
||||
NoUninstallWarningTitle=组件已存在
|
||||
NoUninstallWarning=安装程序检测到下列组件已安装在您的电脑中:%n%n%1%n%n取消选中这些组件不会卸载它们。%n%n确定要继续吗?
|
||||
ComponentSize1=%1 KB
|
||||
ComponentSize2=%1 MB
|
||||
ComponentsDiskSpaceGBLabel=当前选择的组件需要至少 [gb] GB 的磁盘空间。
|
||||
ComponentsDiskSpaceMBLabel=当前选择的组件需要至少 [mb] MB 的磁盘空间。
|
||||
|
||||
; *** “选择附加任务”向导页
|
||||
WizardSelectTasks=选择附加任务
|
||||
SelectTasksDesc=您想要安装程序执行哪些附加任务?
|
||||
SelectTasksLabel2=选择您想要安装程序在安装 [name] 时执行的附加任务,然后点击“下一步”。
|
||||
|
||||
; *** “选择开始菜单文件夹”向导页
|
||||
WizardSelectProgramGroup=选择开始菜单文件夹
|
||||
SelectStartMenuFolderDesc=安装程序应该在哪里放置程序的快捷方式?
|
||||
SelectStartMenuFolderLabel3=安装程序将在下列“开始”菜单文件夹中创建程序的快捷方式。
|
||||
SelectStartMenuFolderBrowseLabel=点击“下一步”继续。如果您想选择其他文件夹,点击“浏览”。
|
||||
MustEnterGroupName=您必须输入一个文件夹名。
|
||||
GroupNameTooLong=文件夹名或路径太长。
|
||||
InvalidGroupName=无效的文件夹名字。
|
||||
BadGroupName=文件夹名不能包含下列任何字符:%n%n%1
|
||||
NoProgramGroupCheck2=不创建开始菜单文件夹(&D)
|
||||
|
||||
; *** “准备安装”向导页
|
||||
WizardReady=准备安装
|
||||
ReadyLabel1=安装程序准备就绪,现在可以开始安装 [name] 到您的电脑。
|
||||
ReadyLabel2a=点击“安装”继续此安装程序。如果您想重新考虑或修改任何设置,点击“上一步”。
|
||||
ReadyLabel2b=点击“安装”继续此安装程序。
|
||||
ReadyMemoUserInfo=用户信息:
|
||||
ReadyMemoDir=目标位置:
|
||||
ReadyMemoType=安装类型:
|
||||
ReadyMemoComponents=已选择组件:
|
||||
ReadyMemoGroup=开始菜单文件夹:
|
||||
ReadyMemoTasks=附加任务:
|
||||
|
||||
; *** TExtractionWizardPage wizard page and Extract7ZipArchive
|
||||
ExtractionLabel=正在提取附加文件...
|
||||
ButtonStopExtraction=停止提取(&S)
|
||||
StopExtraction=您确定要停止提取吗?
|
||||
ErrorExtractionAborted=提取已中止
|
||||
ErrorExtractionFailed=提取失败:%1
|
||||
|
||||
; *** TDownloadWizardPage wizard page and DownloadTemporaryFile
|
||||
DownloadingLabel=正在下载附加文件...
|
||||
ButtonStopDownload=停止下载(&S)
|
||||
StopDownload=您确定要停止下载吗?
|
||||
ErrorDownloadAborted=下载已中止
|
||||
ErrorDownloadFailed=下载失败:%1 %2
|
||||
ErrorDownloadSizeFailed=获取下载大小失败:%1 %2
|
||||
ErrorFileHash1=校验文件哈希失败:%1
|
||||
ErrorFileHash2=无效的文件哈希:预期 %1,实际 %2
|
||||
ErrorProgress=无效的进度:%1 / %2
|
||||
ErrorFileSize=文件大小错误:预期 %1,实际 %2
|
||||
|
||||
; *** “正在准备安装”向导页
|
||||
WizardPreparing=正在准备安装
|
||||
PreparingDesc=安装程序正在准备安装 [name] 到您的电脑。
|
||||
PreviousInstallNotCompleted=先前的程序安装或卸载未完成,您需要重启您的电脑以完成。%n%n在重启电脑后,再次运行安装程序以完成 [name] 的安装。
|
||||
CannotContinue=安装程序不能继续。请点击“取消”退出。
|
||||
ApplicationsFound=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。
|
||||
ApplicationsFound2=以下应用程序正在使用将由安装程序更新的文件。建议您允许安装程序自动关闭这些应用程序。安装完成后,安装程序将尝试重新启动这些应用程序。
|
||||
CloseApplications=自动关闭应用程序(&A)
|
||||
DontCloseApplications=不要关闭应用程序(&D)
|
||||
ErrorCloseApplications=安装程序无法自动关闭所有应用程序。建议您在继续之前,关闭所有在使用需要由安装程序更新的文件的应用程序。
|
||||
PrepareToInstallNeedsRestart=安装程序必须重启您的计算机。计算机重启后,请再次运行安装程序以完成 [name] 的安装。%n%n是否立即重新启动?
|
||||
|
||||
; *** “正在安装”向导页
|
||||
WizardInstalling=正在安装
|
||||
InstallingLabel=安装程序正在安装 [name] 到您的电脑,请稍候。
|
||||
|
||||
; *** “安装完成”向导页
|
||||
FinishedHeadingLabel=[name] 安装完成
|
||||
FinishedLabelNoIcons=安装程序已在您的电脑中安装了 [name]。
|
||||
FinishedLabel=安装程序已在您的电脑中安装了 [name]。您可以通过已安装的快捷方式运行此应用程序。
|
||||
ClickFinish=点击“完成”退出安装程序。
|
||||
FinishedRestartLabel=为完成 [name] 的安装,安装程序必须重新启动您的电脑。要立即重启吗?
|
||||
FinishedRestartMessage=为完成 [name] 的安装,安装程序必须重新启动您的电脑。%n%n要立即重启吗?
|
||||
ShowReadmeCheck=是,我想查阅自述文件
|
||||
YesRadio=是,立即重启电脑(&Y)
|
||||
NoRadio=否,稍后重启电脑(&N)
|
||||
; used for example as 'Run MyProg.exe'
|
||||
RunEntryExec=运行 %1
|
||||
; used for example as 'View Readme.txt'
|
||||
RunEntryShellExec=查阅 %1
|
||||
|
||||
; *** “安装程序需要下一张磁盘”提示
|
||||
ChangeDiskTitle=安装程序需要下一张磁盘
|
||||
SelectDiskLabel2=请插入磁盘 %1 并点击“确定”。%n%n如果这个磁盘中的文件可以在下列文件夹之外的文件夹中找到,请输入正确的路径或点击“浏览”。
|
||||
PathLabel=路径(&P):
|
||||
FileNotInDir2=“%2”中找不到文件“%1”。请插入正确的磁盘或选择其他文件夹。
|
||||
SelectDirectoryLabel=请指定下一张磁盘的位置。
|
||||
|
||||
; *** 安装状态消息
|
||||
SetupAborted=安装程序未完成安装。%n%n请修正这个问题并重新运行安装程序。
|
||||
AbortRetryIgnoreSelectAction=选择操作
|
||||
AbortRetryIgnoreRetry=重试(&T)
|
||||
AbortRetryIgnoreIgnore=忽略错误并继续(&I)
|
||||
AbortRetryIgnoreCancel=关闭安装程序
|
||||
|
||||
; *** 安装状态消息
|
||||
StatusClosingApplications=正在关闭应用程序...
|
||||
StatusCreateDirs=正在创建目录...
|
||||
StatusExtractFiles=正在解压缩文件...
|
||||
StatusCreateIcons=正在创建快捷方式...
|
||||
StatusCreateIniEntries=正在创建 INI 条目...
|
||||
StatusCreateRegistryEntries=正在创建注册表条目...
|
||||
StatusRegisterFiles=正在注册文件...
|
||||
StatusSavingUninstall=正在保存卸载信息...
|
||||
StatusRunProgram=正在完成安装...
|
||||
StatusRestartingApplications=正在重启应用程序...
|
||||
StatusRollback=正在撤销更改...
|
||||
|
||||
; *** 其他错误
|
||||
ErrorInternal2=内部错误:%1
|
||||
ErrorFunctionFailedNoCode=%1 失败
|
||||
ErrorFunctionFailed=%1 失败;错误代码 %2
|
||||
ErrorFunctionFailedWithMessage=%1 失败;错误代码 %2.%n%3
|
||||
ErrorExecutingProgram=无法执行文件:%n%1
|
||||
|
||||
; *** 注册表错误
|
||||
ErrorRegOpenKey=打开注册表项时出错:%n%1\%2
|
||||
ErrorRegCreateKey=创建注册表项时出错:%n%1\%2
|
||||
ErrorRegWriteKey=写入注册表项时出错:%n%1\%2
|
||||
|
||||
; *** INI 错误
|
||||
ErrorIniEntry=在文件“%1”中创建 INI 条目时出错。
|
||||
|
||||
; *** 文件复制错误
|
||||
FileAbortRetryIgnoreSkipNotRecommended=跳过此文件(&S) (不推荐)
|
||||
FileAbortRetryIgnoreIgnoreNotRecommended=忽略错误并继续(&I) (不推荐)
|
||||
SourceIsCorrupted=源文件已损坏
|
||||
SourceDoesntExist=源文件“%1”不存在
|
||||
ExistingFileReadOnly2=无法替换现有文件,它是只读的。
|
||||
ExistingFileReadOnlyRetry=移除只读属性并重试(&R)
|
||||
ExistingFileReadOnlyKeepExisting=保留现有文件(&K)
|
||||
ErrorReadingExistingDest=尝试读取现有文件时出错:
|
||||
FileExistsSelectAction=选择操作
|
||||
FileExists2=文件已经存在。
|
||||
FileExistsOverwriteExisting=覆盖已存在的文件(&O)
|
||||
FileExistsKeepExisting=保留现有的文件(&K)
|
||||
FileExistsOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||
ExistingFileNewerSelectAction=选择操作
|
||||
ExistingFileNewer2=现有的文件比安装程序将要安装的文件还要新。
|
||||
ExistingFileNewerOverwriteExisting=覆盖已存在的文件(&O)
|
||||
ExistingFileNewerKeepExisting=保留现有的文件(&K) (推荐)
|
||||
ExistingFileNewerOverwriteOrKeepAll=为所有冲突文件执行此操作(&D)
|
||||
ErrorChangingAttr=尝试更改下列现有文件的属性时出错:
|
||||
ErrorCreatingTemp=尝试在目标目录创建文件时出错:
|
||||
ErrorReadingSource=尝试读取下列源文件时出错:
|
||||
ErrorCopying=尝试复制下列文件时出错:
|
||||
ErrorReplacingExistingFile=尝试替换现有文件时出错:
|
||||
ErrorRestartReplace=重启并替换失败:
|
||||
ErrorRenamingTemp=尝试重命名下列目标目录中的一个文件时出错:
|
||||
ErrorRegisterServer=无法注册 DLL/OCX:%1
|
||||
ErrorRegSvr32Failed=RegSvr32 失败;退出代码 %1
|
||||
ErrorRegisterTypeLib=无法注册类库:%1
|
||||
|
||||
; *** 卸载显示名字标记
|
||||
; used for example as 'My Program (32-bit)'
|
||||
UninstallDisplayNameMark=%1 (%2)
|
||||
; used for example as 'My Program (32-bit, All users)'
|
||||
UninstallDisplayNameMarks=%1 (%2, %3)
|
||||
UninstallDisplayNameMark32Bit=32 位
|
||||
UninstallDisplayNameMark64Bit=64 位
|
||||
UninstallDisplayNameMarkAllUsers=所有用户
|
||||
UninstallDisplayNameMarkCurrentUser=当前用户
|
||||
|
||||
; *** 安装后错误
|
||||
ErrorOpeningReadme=尝试打开自述文件时出错。
|
||||
ErrorRestartingComputer=安装程序无法重启电脑,请手动重启。
|
||||
|
||||
; *** 卸载消息
|
||||
UninstallNotFound=文件“%1”不存在。无法卸载。
|
||||
UninstallOpenError=文件“%1”不能被打开。无法卸载。
|
||||
UninstallUnsupportedVer=此版本的卸载程序无法识别卸载日志文件“%1”的格式。无法卸载
|
||||
UninstallUnknownEntry=卸载日志中遇到一个未知条目 (%1)
|
||||
ConfirmUninstall=您确认要完全移除 %1 及其所有组件吗?
|
||||
UninstallOnlyOnWin64=仅允许在 64 位 Windows 中卸载此程序。
|
||||
OnlyAdminCanUninstall=仅使用管理员权限的用户能完成此卸载。
|
||||
UninstallStatusLabel=正在从您的电脑中移除 %1,请稍候。
|
||||
UninstalledAll=已顺利从您的电脑中移除 %1。
|
||||
UninstalledMost=%1 卸载完成。%n%n有部分内容未能被删除,但您可以手动删除它们。
|
||||
UninstalledAndNeedsRestart=为完成 %1 的卸载,需要重启您的电脑。%n%n立即重启电脑吗?
|
||||
UninstallDataCorrupted=文件“%1”已损坏。无法卸载
|
||||
|
||||
; *** 卸载状态消息
|
||||
ConfirmDeleteSharedFileTitle=删除共享的文件吗?
|
||||
ConfirmDeleteSharedFile2=系统表示下列共享的文件已不有其他程序使用。您希望卸载程序删除这些共享的文件吗?%n%n如果删除这些文件,但仍有程序在使用这些文件,则这些程序可能出现异常。如果您不能确定,请选择“否”,在系统中保留这些文件以免引发问题。
|
||||
SharedFileNameLabel=文件名:
|
||||
SharedFileLocationLabel=位置:
|
||||
WizardUninstalling=卸载状态
|
||||
StatusUninstalling=正在卸载 %1...
|
||||
|
||||
; *** Shutdown block reasons
|
||||
ShutdownBlockReasonInstallingApp=正在安装 %1。
|
||||
ShutdownBlockReasonUninstallingApp=正在卸载 %1。
|
||||
|
||||
; The custom messages below aren't used by Setup itself, but if you make
|
||||
; use of them in your scripts, you'll want to translate them.
|
||||
|
||||
[CustomMessages]
|
||||
|
||||
NameAndVersion=%1 版本 %2
|
||||
AdditionalIcons=附加快捷方式:
|
||||
CreateDesktopIcon=创建桌面快捷方式(&D)
|
||||
CreateQuickLaunchIcon=创建快速启动栏快捷方式(&Q)
|
||||
ProgramOnTheWeb=%1 网站
|
||||
UninstallProgram=卸载 %1
|
||||
LaunchProgram=运行 %1
|
||||
AssocFileExtension=将 %2 文件扩展名与 %1 建立关联(&A)
|
||||
AssocingFileExtension=正在将 %2 文件扩展名与 %1 建立关联...
|
||||
AutoStartProgramGroupDescription=启动:
|
||||
AutoStartProgram=自动启动 %1
|
||||
AddonHostProgramNotFound=您选择的文件夹中无法找到 %1。%n%n您要继续吗?
|
||||
@@ -24,8 +24,11 @@
|
||||
"MainFunction.Stage1": "" #主关卡
|
||||
"MainFunction.Stage2": "" #备选关卡1
|
||||
"MainFunction.Stage3": "" #备选关卡2
|
||||
"MainFunction.Stage4": "" #备选关卡3
|
||||
"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡
|
||||
"MainFunction.Series.Quantity": "1" #连战次数
|
||||
"MainFunction.Annihilation.UseCustom": "True" #自定义剿灭关卡
|
||||
"MainFunction.Annihilation.Stage": "Annihilation"、"Chernobog@Annihilation"、"LungmenOutskirts@Annihilation"、"LungmenDowntown@Annihilation" #自定义剿灭关卡号
|
||||
"Penguin.IsDrGrandet": "True" #博朗台模式
|
||||
"GUI.CustomStageCode": "False" #手动输入关卡名
|
||||
"GUI.UseAlternateStage": "False" #使用备选关卡
|
||||
@@ -44,7 +47,7 @@
|
||||
"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读
|
||||
"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址
|
||||
#设置
|
||||
"Start.ClientType": "Bilibili"、 "Official" #服务器
|
||||
"Start.ClientType": "Official"、"Bilibili"、"YoStarEN"、"YoStarJP"、"YoStarKR"、"txwy" #服务器
|
||||
G"Timer.Timer1": "False" #时间设置1
|
||||
"Connect.AdbPath" #ADB路径
|
||||
"Connect.Address": "127.0.0.1:16448" #连接地址
|
||||
@@ -58,4 +61,5 @@ G"GUI.UseTray": "True" #显示托盘图标
|
||||
G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘
|
||||
"Start.EmulatorPath" #模拟器路径
|
||||
"Start.EmulatorAddCommand": "-v 2" #附加命令
|
||||
"Start.EmulatorWaitSeconds": "10" #等待模拟器启动时间
|
||||
G"VersionUpdate.package": "MirrorChyanAppv5.15.6.zip" #更新包标识
|
||||
160
resources/html/general_result.html
Normal file
200
resources/html/general_statistics.html
Normal file
BIN
resources/icons/MirrorChyan.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 2.8 MiB After Width: | Height: | Size: 1.7 MiB |
BIN
resources/images/notification/six_star.png
Normal file
|
After Width: | Height: | Size: 222 KiB |
BIN
resources/images/notification/test_notify.png
Normal file
|
After Width: | Height: | Size: 32 KiB |