Compare commits
223 Commits
v4.3.4-bet
...
v4.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2c0e457976 | ||
|
|
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 | ||
|
|
4cbd921ab6 | ||
|
|
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 | ||
|
|
fba5395bf0 | ||
|
|
2c4508ee16 | ||
| d239443555 | |||
| e45ad08fab | |||
| ddf5d26c4b | |||
|
|
ce74dcf912 | ||
|
|
41412e1ef4 | ||
|
|
1395d48cd0 | ||
|
|
418c3d4742 | ||
|
|
17ec962a22 | ||
|
|
989ee73549 | ||
|
|
7e452e1253 | ||
|
|
5bdb5c8025 | ||
|
|
924a5fea0b | ||
|
|
b51a57a6ee | ||
|
|
4079188881 | ||
|
|
174163e305 | ||
|
|
0886439685 | ||
|
|
34bf5a4fe8 | ||
|
|
e6a97f2b17 | ||
|
|
fecff625a3 | ||
|
|
6f540036a0 | ||
|
|
86d72aec39 | ||
|
|
39876832f3 | ||
|
|
f3af6ddbbc | ||
|
|
ba7299e20c | ||
|
|
5db9d934b2 | ||
|
|
5c8eebf12c | ||
|
|
e725f6d2b2 | ||
|
|
494b655156 | ||
|
|
2940f2557c | ||
|
|
5e4660670f | ||
| e8d592ae76 | |||
|
|
97ea51df59 | ||
|
|
986061dc97 | ||
|
|
fe1910d16f | ||
|
|
63cb1aaa74 | ||
| 49ebd50077 | |||
|
|
4a6f874210 |
265
.github/workflows/build-app.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
name: Build AUTO_MAA
|
name: Build AUTO_MAA
|
||||||
|
|
||||||
@@ -28,9 +28,11 @@ permissions:
|
|||||||
actions: write
|
actions: write
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
||||||
pre_check:
|
pre_check:
|
||||||
name: Pre Checks
|
name: Pre Checks
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Repo Check
|
- name: Repo Check
|
||||||
id: repo_check
|
id: repo_check
|
||||||
@@ -40,135 +42,242 @@ jobs:
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
exit 0
|
exit 0
|
||||||
|
|
||||||
build_AUTO_MAA:
|
build_AUTO_MAA:
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
needs: pre_check
|
needs: pre_check
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v4
|
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
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: "3.12"
|
python-version: '3.12'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
run: |
|
run: |
|
||||||
python -m pip install --upgrade pip
|
python -m pip install --upgrade pip
|
||||||
pip install flake8 pytest
|
|
||||||
pip install -r requirements.txt
|
pip install -r requirements.txt
|
||||||
- name: Lint with flake8
|
|
||||||
|
- name: Get version
|
||||||
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
# stop the build if there are Python syntax errors or undefined names
|
$version = (Get-Content resources/version.json | ConvertFrom-Json).main_version
|
||||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
echo "main_version=$version" >> $env:GITHUB_OUTPUT
|
||||||
# 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: Nuitka build main program
|
||||||
- name: Package
|
uses: Nuitka/Nuitka-Action@main
|
||||||
id: package
|
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: |
|
run: |
|
||||||
copy app\utils\package.py .\
|
$root = "${{ github.workspace }}"
|
||||||
python package.py
|
$ver = "${{ steps.get_version.outputs.main_version }}"
|
||||||
- name: Read version
|
Copy-Item "$root/app" "$root/AUTO_MAA/app" -Recurse
|
||||||
id: read_version
|
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: |
|
run: |
|
||||||
$MAIN_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 1).Trim()
|
$root = "${{ github.workspace }}"
|
||||||
"AUTO_MAA_version=$MAIN_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
$ver = "${{ steps.get_version.outputs.main_version }}"
|
||||||
$UPDATER_VERSION=(Get-Content -Path "version_info.txt" -TotalCount 2 | Select-Object -Index 1).Trim()
|
$iss = Get-Content "$root/app/utils/AUTO_MAA.iss" -Raw
|
||||||
"updater_version=$UPDATER_VERSION" | Out-File -FilePath $env:GITHUB_ENV -Append
|
$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
|
- name: Upload Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: AUTO_MAA_${{ env.AUTO_MAA_version }}
|
name: AUTO_MAA_${{ steps.get_version.outputs.main_version }}
|
||||||
path: |
|
path: AUTO_MAA_${{ steps.get_version.outputs.main_version }}.zip
|
||||||
AUTO_MAA_${{ env.AUTO_MAA_version }}.zip
|
|
||||||
- name: Upload Version_Info Artifact
|
- name: Upload Version_Info Artifact
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: version_info
|
name: version_info
|
||||||
path: version_info.txt
|
path: version_info.txt
|
||||||
|
|
||||||
publish_release:
|
publish_release:
|
||||||
name: Publish release
|
name: Publish release
|
||||||
needs: build_AUTO_MAA
|
needs: build_AUTO_MAA
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v4
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
pattern: AUTO_MAA_*
|
pattern: AUTO_MAA_*
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
path: artifacts
|
path: artifacts
|
||||||
|
|
||||||
- name: Download Version_Info
|
- name: Download Version_Info
|
||||||
uses: actions/download-artifact@v4
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: version_info
|
name: version_info
|
||||||
path: ./
|
path: ./
|
||||||
- name: Check if release exists
|
|
||||||
id: check_if_release_exists
|
|
||||||
run: |
|
|
||||||
release_id=$(gh release view $(sed 's/\r$//g' <(head -n 1 version_info.txt)) --json id --jq .id || true)
|
|
||||||
if [[ -z $release_id ]]; then
|
|
||||||
echo "release_exists=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "release_exists=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
|
||||||
- name: Create release
|
- name: Create release
|
||||||
id: create_release
|
id: create_release
|
||||||
if: steps.check_if_release_exists.outputs.release_exists == 'false'
|
|
||||||
run: |
|
run: |
|
||||||
set -xe
|
set -xe
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
||||||
TAGNAME="$(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_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
||||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
NOTES="$NOTES_MAIN
|
||||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
|
||||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" artifacts/*
|
## 代码签名策略(Code signing policy)
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
Free code signing provided by [SignPath.io](https://signpath.io/), certificate by [SignPath Foundation](https://signpath.org/)
|
||||||
- name: Update release
|
|
||||||
id: update_release
|
- 审批人(Approvers): [DLmaster (@DLmaster361)](https://github.com/DLmaster361)
|
||||||
if: steps.check_if_release_exists.outputs.release_exists == 'true'
|
|
||||||
run: |
|
## 隐私政策(Privacy policy)
|
||||||
set -xe
|
|
||||||
shopt -s nullglob
|
除非用户、安装者或使用者特别要求,否则本程序不会将任何信息传输到其他网络系统。
|
||||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
|
||||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
This program will not transfer any information to other networked systems unless specifically requested by the user or the person installing or operating it.
|
||||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
|
||||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
[已有 Mirror酱 CDK ?前往 Mirror酱 高速下载](https://mirrorchyan.com/zh/projects?rid=AUTO_MAA&source=auto_maa-release)
|
||||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
|
||||||
gh release delete "$TAGNAME" --yes
|
\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
||||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" artifacts/*
|
if [ "${{ github.ref_name }}" == "main" ]; then
|
||||||
|
PRERELEASE_FLAG=""
|
||||||
|
else
|
||||||
|
PRERELEASE_FLAG="--prerelease"
|
||||||
|
fi
|
||||||
|
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" $PRERELEASE_FLAG artifacts/*
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
||||||
|
|
||||||
- name: Trigger MirrorChyanUploading
|
- name: Trigger MirrorChyanUploading
|
||||||
run: |
|
run: |
|
||||||
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan
|
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan
|
||||||
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note
|
gh workflow run --repo $GITHUB_REPOSITORY mirrorchyan_release_note
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
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/
|
|
||||||
- name: Install obsutil
|
|
||||||
run: |
|
|
||||||
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
|
|
||||||
tar -xzvf obsutil_linux_amd64.tar.gz --strip-components=1
|
|
||||||
chmod 755 obsutil
|
|
||||||
./obsutil version
|
|
||||||
- name: Upload Release to Huawei OBS
|
|
||||||
env:
|
|
||||||
OBS_AK: ${{ secrets.OBS_AK }}
|
|
||||||
OBS_SK: ${{ secrets.OBS_SK }}
|
|
||||||
OBS_ENDPOINT: ${{ secrets.OBS_ENDPOINT }}
|
|
||||||
OBS_BUCKET: ${{ secrets.OBS_BUCKET }}
|
|
||||||
run: |
|
|
||||||
./obsutil config -i $OBS_AK -k $OBS_SK -e $OBS_ENDPOINT
|
|
||||||
./obsutil cp artifacts/ obs://$OBS_BUCKET/releases/ -r -f
|
|
||||||
|
|||||||
174
.github/workflows/build-pre.yml
vendored
@@ -1,174 +0,0 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
|
||||||
# Copyright © <2024> <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/>.
|
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
|
||||||
|
|
||||||
name: Build AUTO_MAA_Pre
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
actions: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
pre_check:
|
|
||||||
name: Pre Checks
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Repo Check
|
|
||||||
id: repo_check
|
|
||||||
run: |
|
|
||||||
if [[ "$GITHUB_REPOSITORY" != "DLmaster361/AUTO_MAA" ]]; then
|
|
||||||
echo "When forking this repository to make your own builds, you have to adjust this check."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
exit 0
|
|
||||||
build_AUTO_MAA:
|
|
||||||
runs-on: windows-latest
|
|
||||||
needs: pre_check
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
- name: Set up Python 3.12
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
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
|
|
||||||
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
|
|
||||||
run: |
|
|
||||||
copy app\utils\package.py .\
|
|
||||||
python package.py
|
|
||||||
- name: Read version
|
|
||||||
id: read_version
|
|
||||||
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
|
|
||||||
- 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: Upload Version_Info Artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: version_info
|
|
||||||
path: version_info.txt
|
|
||||||
publish_prerelease:
|
|
||||||
name: Publish prerelease
|
|
||||||
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: Check if release exists
|
|
||||||
id: check_if_release_exists
|
|
||||||
run: |
|
|
||||||
release_id=$(gh release view $(sed 's/\r$//g' <(head -n 1 version_info.txt)) --json id --jq .id || true)
|
|
||||||
if [[ -z $release_id ]]; then
|
|
||||||
echo "release_exists=false" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "release_exists=true" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
|
||||||
- name: Create prerelease
|
|
||||||
id: create_prerelease
|
|
||||||
if: steps.check_if_release_exists.outputs.release_exists == 'false'
|
|
||||||
run: |
|
|
||||||
set -xe
|
|
||||||
shopt -s nullglob
|
|
||||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
|
||||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
|
||||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
|
||||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
|
||||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
|
||||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" --prerelease artifacts/*
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.WORKFLOW_TOKEN }}
|
|
||||||
- name: Update prerelease
|
|
||||||
id: update_prerelease
|
|
||||||
if: steps.check_if_release_exists.outputs.release_exists == 'true'
|
|
||||||
run: |
|
|
||||||
set -xe
|
|
||||||
shopt -s nullglob
|
|
||||||
NAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
|
||||||
TAGNAME="$(sed 's/\r$//g' <(head -n 1 version_info.txt))"
|
|
||||||
NOTES_MAIN="$(sed 's/\r$//g' <(tail -n +3 version_info.txt))"
|
|
||||||
NOTES_TAIL="\`\`\`本release通过GitHub Actions自动构建\`\`\`"
|
|
||||||
NOTES="$NOTES_MAIN<br><br>$NOTES_TAIL"
|
|
||||||
gh release delete "$TAGNAME" --yes
|
|
||||||
gh release create "$TAGNAME" --target "main" --title "$NAME" --notes "$NOTES" --prerelease 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/
|
|
||||||
- name: Install obsutil
|
|
||||||
run: |
|
|
||||||
wget https://obs-community.obs.cn-north-1.myhuaweicloud.com/obsutil/current/obsutil_linux_amd64.tar.gz
|
|
||||||
tar -xzvf obsutil_linux_amd64.tar.gz --strip-components=1
|
|
||||||
chmod 755 obsutil
|
|
||||||
./obsutil version
|
|
||||||
- name: Upload Release to Huawei OBS
|
|
||||||
env:
|
|
||||||
OBS_AK: ${{ secrets.OBS_AK }}
|
|
||||||
OBS_SK: ${{ secrets.OBS_SK }}
|
|
||||||
OBS_ENDPOINT: ${{ secrets.OBS_ENDPOINT }}
|
|
||||||
OBS_BUCKET: ${{ secrets.OBS_BUCKET }}
|
|
||||||
run: |
|
|
||||||
./obsutil config -i $OBS_AK -k $OBS_SK -e $OBS_ENDPOINT
|
|
||||||
./obsutil cp artifacts/ obs://$OBS_BUCKET/releases/ -r -f
|
|
||||||
9
.gitignore
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
__pycache__/
|
||||||
|
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"`
|
||||||
290
Go_Updater/api/client.go
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MirrorResponse 表示 MirrorChyan API 的响应结构
|
||||||
|
type MirrorResponse struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Data struct {
|
||||||
|
VersionName string `json:"version_name"`
|
||||||
|
VersionNumber int `json:"version_number"`
|
||||||
|
URL string `json:"url,omitempty"`
|
||||||
|
SHA256 string `json:"sha256,omitempty"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
OS string `json:"os"`
|
||||||
|
Arch string `json:"arch"`
|
||||||
|
UpdateType string `json:"update_type,omitempty"`
|
||||||
|
ReleaseNote string `json:"release_note"`
|
||||||
|
FileSize int64 `json:"filesize,omitempty"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateCheckParams 表示更新检查的参数
|
||||||
|
type UpdateCheckParams struct {
|
||||||
|
ResourceID string
|
||||||
|
CurrentVersion string
|
||||||
|
Channel string
|
||||||
|
UserAgent string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MirrorClient 定义 Mirror API 客户端的接口方法
|
||||||
|
type MirrorClient interface {
|
||||||
|
CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error)
|
||||||
|
IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool
|
||||||
|
GetDownloadURL(versionName string) string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Client 实现 MirrorClient 接口
|
||||||
|
type Client struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
baseURL string
|
||||||
|
downloadURL string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewClient 创建新的 Mirror API 客户端
|
||||||
|
func NewClient() *Client {
|
||||||
|
return &Client{
|
||||||
|
httpClient: &http.Client{
|
||||||
|
Timeout: 30 * time.Second,
|
||||||
|
},
|
||||||
|
baseURL: "https://mirrorchyan.com/api/resources",
|
||||||
|
downloadURL: "http://221.236.27.82:10197/d/AUTO_MAA",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckUpdate 调用 MirrorChyan API 检查更新
|
||||||
|
func (c *Client) CheckUpdate(params UpdateCheckParams) (*MirrorResponse, error) {
|
||||||
|
// 构建 API URL
|
||||||
|
apiURL := fmt.Sprintf("%s/%s/latest", c.baseURL, params.ResourceID)
|
||||||
|
|
||||||
|
// 解析 URL 并添加查询参数
|
||||||
|
u, err := url.Parse(apiURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 API URL 失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加查询参数
|
||||||
|
q := u.Query()
|
||||||
|
q.Set("current_version", params.CurrentVersion)
|
||||||
|
q.Set("channel", params.Channel)
|
||||||
|
q.Set("os", "") // 跨平台为空
|
||||||
|
q.Set("arch", "") // 跨平台为空
|
||||||
|
u.RawQuery = q.Encode()
|
||||||
|
|
||||||
|
// 创建 HTTP 请求
|
||||||
|
req, err := http.NewRequest("GET", u.String(), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("创建 HTTP 请求失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 设置 User-Agent 头
|
||||||
|
if params.UserAgent != "" {
|
||||||
|
req.Header.Set("User-Agent", params.UserAgent)
|
||||||
|
} else {
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送 HTTP 请求
|
||||||
|
resp, err := c.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("发送 HTTP 请求失败: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
// 检查 HTTP 状态码
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("API 返回非 200 状态码: %d", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 读取响应体
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("读取响应体失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析 JSON 响应
|
||||||
|
var mirrorResp MirrorResponse
|
||||||
|
if err := json.Unmarshal(body, &mirrorResp); err != nil {
|
||||||
|
return nil, fmt.Errorf("解析 JSON 响应失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mirrorResp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsUpdateAvailable 比较当前版本与 API 响应中的最新版本
|
||||||
|
func (c *Client) IsUpdateAvailable(response *MirrorResponse, currentVersion string) bool {
|
||||||
|
// 检查 API 响应是否成功
|
||||||
|
if response.Code != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从响应中获取最新版本
|
||||||
|
latestVersion := response.Data.VersionName
|
||||||
|
if latestVersion == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换版本格式以便比较
|
||||||
|
currentVersionNormalized := c.normalizeVersionForComparison(currentVersion)
|
||||||
|
latestVersionNormalized := c.normalizeVersionForComparison(latestVersion)
|
||||||
|
|
||||||
|
// 调试输出
|
||||||
|
// fmt.Printf("Current: %s -> %s\n", currentVersion, currentVersionNormalized)
|
||||||
|
// fmt.Printf("Latest: %s -> %s\n", latestVersion, latestVersionNormalized)
|
||||||
|
// fmt.Printf("Compare result: %d\n", compareVersions(currentVersionNormalized, latestVersionNormalized))
|
||||||
|
|
||||||
|
// 使用语义版本比较
|
||||||
|
return compareVersions(currentVersionNormalized, latestVersionNormalized) < 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeVersionForComparison 将不同版本格式转换为可比较格式
|
||||||
|
func (c *Client) normalizeVersionForComparison(version string) string {
|
||||||
|
// 处理 AUTO_MAA 版本格式: "4.4.1.3" -> "v4.4.1-beta3"
|
||||||
|
if !strings.HasPrefix(version, "v") && strings.Count(version, ".") == 3 {
|
||||||
|
parts := strings.Split(version, ".")
|
||||||
|
if len(parts) == 4 {
|
||||||
|
major, minor, patch, beta := parts[0], parts[1], parts[2], parts[3]
|
||||||
|
if beta == "0" {
|
||||||
|
return fmt.Sprintf("v%s.%s.%s", major, minor, patch)
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("v%s.%s.%s-beta%s", major, minor, patch, beta)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经是标准格式则直接返回
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// compareVersions 比较两个语义版本字符串
|
||||||
|
// 返回值: -1 如果 v1 < v2, 0 如果 v1 == v2, 1 如果 v1 > v2
|
||||||
|
func compareVersions(v1, v2 string) int {
|
||||||
|
// 通过移除 'v' 前缀来标准化版本
|
||||||
|
v1 = normalizeVersion(v1)
|
||||||
|
v2 = normalizeVersion(v2)
|
||||||
|
|
||||||
|
// 解析版本组件
|
||||||
|
parts1 := parseVersionParts(v1)
|
||||||
|
parts2 := parseVersionParts(v2)
|
||||||
|
|
||||||
|
// 比较每个组件
|
||||||
|
maxLen := len(parts1)
|
||||||
|
if len(parts2) > maxLen {
|
||||||
|
maxLen = len(parts2)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < maxLen; i++ {
|
||||||
|
var p1, p2 int
|
||||||
|
if i < len(parts1) {
|
||||||
|
p1 = parts1[i]
|
||||||
|
}
|
||||||
|
if i < len(parts2) {
|
||||||
|
p2 = parts2[i]
|
||||||
|
}
|
||||||
|
|
||||||
|
if p1 < p2 {
|
||||||
|
return -1
|
||||||
|
} else if p1 > p2 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// normalizeVersion 移除 'v' 前缀并处理常见版本格式
|
||||||
|
func normalizeVersion(version string) string {
|
||||||
|
if len(version) > 0 && (version[0] == 'v' || version[0] == 'V') {
|
||||||
|
return version[1:]
|
||||||
|
}
|
||||||
|
return version
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVersionParts 将版本字符串解析为数字组件,包括beta版本号
|
||||||
|
func parseVersionParts(version string) []int {
|
||||||
|
if version == "" {
|
||||||
|
return []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := make([]int, 0, 4)
|
||||||
|
current := 0
|
||||||
|
|
||||||
|
// 先检查是否包含 -beta
|
||||||
|
betaIndex := strings.Index(version, "-beta")
|
||||||
|
var mainVersion, betaVersion string
|
||||||
|
|
||||||
|
if betaIndex != -1 {
|
||||||
|
mainVersion = version[:betaIndex]
|
||||||
|
betaVersion = version[betaIndex+5:] // 跳过 "-beta"
|
||||||
|
} else {
|
||||||
|
mainVersion = version
|
||||||
|
betaVersion = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析主版本号 (major.minor.patch)
|
||||||
|
for _, char := range mainVersion {
|
||||||
|
if char >= '0' && char <= '9' {
|
||||||
|
current = current*10 + int(char-'0')
|
||||||
|
} else if char == '.' {
|
||||||
|
parts = append(parts, current)
|
||||||
|
current = 0
|
||||||
|
} else {
|
||||||
|
// 遇到非数字非点字符,停止解析
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 添加最后一个主版本组件
|
||||||
|
parts = append(parts, current)
|
||||||
|
|
||||||
|
// 确保至少有 3 个组件 (major.minor.patch)
|
||||||
|
for len(parts) < 3 {
|
||||||
|
parts = append(parts, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析beta版本号
|
||||||
|
if betaVersion != "" {
|
||||||
|
// 跳过可能的点号
|
||||||
|
if strings.HasPrefix(betaVersion, ".") {
|
||||||
|
betaVersion = betaVersion[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
betaNum := 0
|
||||||
|
for _, char := range betaVersion {
|
||||||
|
if char >= '0' && char <= '9' {
|
||||||
|
betaNum = betaNum*10 + int(char-'0')
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts = append(parts, betaNum)
|
||||||
|
} else {
|
||||||
|
// 非beta版本,添加0作为beta版本号
|
||||||
|
parts = append(parts, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDownloadURL 根据版本名生成下载站的下载 URL
|
||||||
|
func (c *Client) GetDownloadURL(versionName string) string {
|
||||||
|
// 将版本名转换为文件名格式
|
||||||
|
// 例如: "v4.4.0" -> "AUTO_MAA_v4.4.0.zip"
|
||||||
|
// 例如: "v4.4.1-beta3" -> "AUTO_MAA_v4.4.1-beta.3.zip"
|
||||||
|
filename := fmt.Sprintf("AUTO_MAA_%s.zip", versionName)
|
||||||
|
|
||||||
|
// 处理 beta 版本: 将 "beta3" 转换为 "beta.3"
|
||||||
|
if strings.Contains(filename, "-beta") && !strings.Contains(filename, "-beta.") {
|
||||||
|
filename = strings.Replace(filename, "-beta", "-beta.", 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s/%s", c.downloadURL, filename)
|
||||||
|
}
|
||||||
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
1035
Go_Updater/main.go
Normal file
178
Go_Updater/version/manager.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"AUTO_MAA_Go_Updater/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// VersionInfo 表示来自 version.json 的版本信息
|
||||||
|
type VersionInfo struct {
|
||||||
|
MainVersion string `json:"main_version"`
|
||||||
|
VersionInfo map[string]map[string][]string `json:"version_info"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsedVersion 表示解析后的版本,包含主版本号、次版本号、补丁版本号和测试版本号组件
|
||||||
|
type ParsedVersion struct {
|
||||||
|
Major int
|
||||||
|
Minor int
|
||||||
|
Patch int
|
||||||
|
Beta int
|
||||||
|
}
|
||||||
|
|
||||||
|
// VersionManager 处理版本相关操作
|
||||||
|
type VersionManager struct {
|
||||||
|
executableDir string
|
||||||
|
logger logger.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewVersionManager 创建新的版本管理器
|
||||||
|
func NewVersionManager() *VersionManager {
|
||||||
|
execPath, _ := os.Executable()
|
||||||
|
execDir := filepath.Dir(execPath)
|
||||||
|
return &VersionManager{
|
||||||
|
executableDir: execDir,
|
||||||
|
logger: logger.GetDefaultLogger(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createDefaultVersion 创建默认版本结构 v0.0.0
|
||||||
|
func (vm *VersionManager) createDefaultVersion() *VersionInfo {
|
||||||
|
return &VersionInfo{
|
||||||
|
MainVersion: "0.0.0.0", // 对应 v0.0.0
|
||||||
|
VersionInfo: make(map[string]map[string][]string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadVersionFromFile 从 resources/version.json 加载版本信息并处理回退
|
||||||
|
func (vm *VersionManager) LoadVersionFromFile() (*VersionInfo, error) {
|
||||||
|
versionPath := filepath.Join(vm.executableDir, "resources", "version.json")
|
||||||
|
|
||||||
|
data, err := os.ReadFile(versionPath)
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
fmt.Println("未读取到版本信息,使用默认版本进行更新。")
|
||||||
|
return vm.createDefaultVersion(), nil
|
||||||
|
}
|
||||||
|
vm.logger.Warn("读取版本文件 %s 失败: %v,将使用默认版本", versionPath, err)
|
||||||
|
return vm.createDefaultVersion(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var versionInfo VersionInfo
|
||||||
|
if err := json.Unmarshal(data, &versionInfo); err != nil {
|
||||||
|
vm.logger.Warn("解析版本文件 %s 失败: %v,将使用默认版本", versionPath, err)
|
||||||
|
return vm.createDefaultVersion(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vm.logger.Debug("成功从 %s 加载版本信息", versionPath)
|
||||||
|
return &versionInfo, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LoadVersionWithDefault 加载版本信息并保证回退到默认版本
|
||||||
|
func (vm *VersionManager) LoadVersionWithDefault() *VersionInfo {
|
||||||
|
versionInfo, err := vm.LoadVersionFromFile()
|
||||||
|
if err != nil {
|
||||||
|
// 这在更新的 LoadVersionFromFile 中不应该发生,但添加作为额外安全措施
|
||||||
|
vm.logger.Error("加载版本文件时出现意外错误: %v,使用默认版本", err)
|
||||||
|
return vm.createDefaultVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证我们有一个有效的版本结构
|
||||||
|
if versionInfo == nil {
|
||||||
|
vm.logger.Warn("版本信息为空,使用默认版本")
|
||||||
|
return vm.createDefaultVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionInfo.MainVersion == "" {
|
||||||
|
vm.logger.Warn("版本信息主版本为空,使用默认版本")
|
||||||
|
return vm.createDefaultVersion()
|
||||||
|
}
|
||||||
|
|
||||||
|
if versionInfo.VersionInfo == nil {
|
||||||
|
vm.logger.Debug("版本信息映射为空,初始化空映射")
|
||||||
|
versionInfo.VersionInfo = make(map[string]map[string][]string)
|
||||||
|
}
|
||||||
|
|
||||||
|
return versionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseVersion 解析版本字符串如 "4.4.1.3" 为组件
|
||||||
|
func ParseVersion(versionStr string) (*ParsedVersion, error) {
|
||||||
|
parts := strings.Split(versionStr, ".")
|
||||||
|
if len(parts) < 3 || len(parts) > 4 {
|
||||||
|
return nil, fmt.Errorf("无效的版本格式: %s", versionStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的主版本号: %s", parts[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的次版本号: %s", parts[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
patch, err := strconv.Atoi(parts[2])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的补丁版本号: %s", parts[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
beta := 0
|
||||||
|
if len(parts) == 4 {
|
||||||
|
beta, err = strconv.Atoi(parts[3])
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("无效的测试版本号: %s", parts[3])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedVersion{
|
||||||
|
Major: major,
|
||||||
|
Minor: minor,
|
||||||
|
Patch: patch,
|
||||||
|
Beta: beta,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToVersionString 将 ParsedVersion 转换回版本字符串格式
|
||||||
|
func (pv *ParsedVersion) ToVersionString() string {
|
||||||
|
if pv.Beta == 0 {
|
||||||
|
return fmt.Sprintf("%d.%d.%d.0", pv.Major, pv.Minor, pv.Patch)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%d.%d.%d.%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToDisplayVersion 将版本转换为显示格式 (v4.4.0 或 v4.4.1-beta3)
|
||||||
|
func (pv *ParsedVersion) ToDisplayVersion() string {
|
||||||
|
if pv.Beta == 0 {
|
||||||
|
return fmt.Sprintf("v%d.%d.%d", pv.Major, pv.Minor, pv.Patch)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("v%d.%d.%d-beta%d", pv.Major, pv.Minor, pv.Patch, pv.Beta)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetChannel 根据版本返回渠道 (stable 或 beta)
|
||||||
|
func (pv *ParsedVersion) GetChannel() string {
|
||||||
|
if pv.Beta == 0 {
|
||||||
|
return "stable"
|
||||||
|
}
|
||||||
|
return "beta"
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsNewer 检查此版本是否比其他版本更新
|
||||||
|
func (pv *ParsedVersion) IsNewer(other *ParsedVersion) bool {
|
||||||
|
if pv.Major != other.Major {
|
||||||
|
return pv.Major > other.Major
|
||||||
|
}
|
||||||
|
if pv.Minor != other.Minor {
|
||||||
|
return pv.Minor > other.Minor
|
||||||
|
}
|
||||||
|
if pv.Patch != other.Patch {
|
||||||
|
return pv.Patch > other.Patch
|
||||||
|
}
|
||||||
|
return pv.Beta > other.Beta
|
||||||
|
}
|
||||||
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()
|
||||||
|
)
|
||||||
68
README.md
@@ -1,19 +1,21 @@
|
|||||||
# AUTO_MAA
|
<h1 align="center">AUTO_MAA</h1>
|
||||||
|
<p align="center">
|
||||||
MAA多账号管理与自动化软件
|
MAA多账号管理与自动化软件<br><br>
|
||||||
|
<img alt="软件图标" src="https://github.com/DLmaster361/AUTO_MAA/blob/main/resources/images/AUTO_MAA.png">
|
||||||

|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
</h1>
|
<p align="center">
|
||||||
|
<a href="https://github.com/DLmaster361/AUTO_MAA/stargazers"><img alt="GitHub Stars" src="https://img.shields.io/github/stars/DLmaster361/AUTO_MAA?style=flat-square"></a>
|
||||||
[](https://github.com/DLmaster361/AUTO_MAA/stargazers)
|
<a href="https://github.com/DLmaster361/AUTO_MAA/network"><img alt="GitHub Forks" src="https://img.shields.io/github/forks/DLmaster361/AUTO_MAA?style=flat-square"></a>
|
||||||
[](https://github.com/DLmaster361/AUTO_MAA/network)
|
<a href="https://github.com/DLmaster361/AUTO_MAA/releases/latest"><img alt="GitHub Downloads" src="https://img.shields.io/github/downloads/DLmaster361/AUTO_MAA/total?style=flat-square"></a>
|
||||||
[](https://github.com/DLmaster361/AUTO_MAA/issues)
|
<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>
|
||||||
[](https://github.com/DLmaster361/AUTO_MAA/graphs/contributors)
|
<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>
|
||||||
[](https://github.com/DLmaster361/AUTO_MAA/blob/main/LICENSE)
|
<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>
|
||||||
</div>
|
<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>
|
||||||
|
|
||||||
## 软件介绍
|
## 软件介绍
|
||||||
|
|
||||||
@@ -21,6 +23,11 @@ MAA多账号管理与自动化软件
|
|||||||
|
|
||||||
本软件是明日方舟第三方软件`MAA`的第三方工具,即第3<sup>3</sup>方软件。旨在优化MAA多账号功能体验,并通过一些方法解决MAA项目未能解决的部分问题,提高代理的稳定性。
|
本软件是明日方舟第三方软件`MAA`的第三方工具,即第3<sup>3</sup>方软件。旨在优化MAA多账号功能体验,并通过一些方法解决MAA项目未能解决的部分问题,提高代理的稳定性。
|
||||||
|
|
||||||
|
- **集中管理**:一站式管理多个MAA脚本与多个用户配置,和凌乱的散装脚本窗口说再见!
|
||||||
|
- **无人值守**:自动处理MAA相关报错,再也不用为代理任务卡死时自己不在电脑旁烦恼啦!
|
||||||
|
- **配置灵活**:通过调度队列与脚本的组合,自由实现您能想到的所有调度需求!
|
||||||
|
- **信息统计**:自动统计用户的公招与关卡掉落物,看看这个月您的收益是多少!
|
||||||
|
|
||||||
### 原理
|
### 原理
|
||||||
|
|
||||||
本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能:
|
本软件可以存储多个明日方舟账号数据,并通过以下流程实现代理功能:
|
||||||
@@ -31,11 +38,9 @@ MAA多账号管理与自动化软件
|
|||||||
|
|
||||||
### 优势
|
### 优势
|
||||||
|
|
||||||
- **节省运行开销:** 只需要一份MAA软件与一个模拟器,无需多开就能完成多账号代理,羸弱的电脑也能代理日常。
|
- **高效稳定**:通过日志监测、异常处理等机制,保障代理任务顺利完成。
|
||||||
- **自定义空间大:** 依靠高级用户配置模式,支持MAA几乎所有设置选项自定义,支持模拟器多开。
|
- **简洁易用**:无需手动修改配置文件,实现自动化调度与多开管理。
|
||||||
- **调度方法自由:** 通过调度队列功能,自由实现MAA多开等多种调度方式。
|
- **兼容扩展**:支持 MAA 几乎所有的配置选项,满足不同用户需求。
|
||||||
- **一键代理无忧:** 无须中途手动修改MAA配置,将繁琐交给AUTO_MAA,把游戏留给自己。
|
|
||||||
- **代理结果复核:** 通过人工排查功能核实各用户代理情况,堵住自动代理的最后一丝风险。
|
|
||||||
|
|
||||||
## 重要声明
|
## 重要声明
|
||||||
|
|
||||||
@@ -47,13 +52,10 @@ MAA多账号管理与自动化软件
|
|||||||
- **传播:** AUTO_MAA原则上允许传播者自由传播本软件,但无论在何种传播过程中,不得删除项目作者与开发者所留版权声明,不得隐瞒项目作者与相关开发者的存在。由于软件性质,项目组不希望发现任何人在明日方舟官方媒体(包括官方媒体账号与森空岛社区等)或明日方舟游戏相关内容(包括同好群、线下活动与游戏内容讨论等)下提及AUTO_MAA或MAA,希望各位理解。
|
- **传播:** AUTO_MAA原则上允许传播者自由传播本软件,但无论在何种传播过程中,不得删除项目作者与开发者所留版权声明,不得隐瞒项目作者与相关开发者的存在。由于软件性质,项目组不希望发现任何人在明日方舟官方媒体(包括官方媒体账号与森空岛社区等)或明日方舟游戏相关内容(包括同好群、线下活动与游戏内容讨论等)下提及AUTO_MAA或MAA,希望各位理解。
|
||||||
- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL,相关成果再次分发时也必须使用GPL或兼容的协议开源。
|
- **衍生:** AUTO_MAA允许任何人对软件本体或软件部分代码进行二次开发或利用。但依据GPL,相关成果再次分发时也必须使用GPL或兼容的协议开源。
|
||||||
- **贡献:** 不论是直接参与软件的维护编写,或是撰写文档、测试、反馈BUG、给出建议、参与讨论,都为AUTO_MAA项目的发展完善做出了不可忽视的贡献。项目组提倡各位贡献者遵照GitHub开源社区惯例,发布Issues参与项目。避免私信或私发邮件(安全性漏洞或敏感问题除外),以帮助更多用户。
|
- **贡献:** 不论是直接参与软件的维护编写,或是撰写文档、测试、反馈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上提及的,项目组拥有最终解释权。
|
以上细则是本项目对GPL的相关补充与强调。未提及的以GPL为准,发生冲突的以本细则为准。如有不清楚的部分,请发Issues询问。若发生纠纷,相关内容也没有在Issues上提及的,项目组拥有最终解释权。
|
||||||
|
|
||||||
**注意**
|
|
||||||
|
|
||||||
- 由于本软件有修改其它目录JSON文件等行为,使用前请将AUTO_MAA添加入Windows Defender信任区以及防病毒软件的信任区或开发者目录,避免被误杀。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
# 使用方法
|
# 使用方法
|
||||||
@@ -70,6 +72,24 @@ MAA多账号管理与自动化软件
|
|||||||
|
|
||||||
可在[《AUTO_MAA开发者协作文档》](https://docs.qq.com/aio/DQ3Z5eHNxdmxFQmZX)的`开发任务`页面中查看开发进度。
|
可在[《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/)。
|
||||||
|
|
||||||
## 贡献者
|
## 贡献者
|
||||||
|
|
||||||
感谢以下贡献者对本项目做出的贡献
|
感谢以下贡献者对本项目做出的贡献
|
||||||
@@ -82,8 +102,6 @@ MAA多账号管理与自动化软件
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
感谢 [AoXuan (@ClozyA)](https://github.com/ClozyA) 为本项目提供的下载服务器
|
|
||||||
|
|
||||||
## Star History
|
## Star History
|
||||||
|
|
||||||
[](https://star-history.com/#DLmaster361/AUTO_MAA&Date)
|
[](https://star-history.com/#DLmaster361/AUTO_MAA&Date)
|
||||||
@@ -98,4 +116,4 @@ MAA多账号管理与自动化软件
|
|||||||
|
|
||||||
如果喜欢这个项目的话,给作者来杯咖啡吧!
|
如果喜欢这个项目的话,给作者来杯咖啡吧!
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA主程序包
|
AUTO_MAA主程序包
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -33,7 +33,6 @@ from .core import QueueConfig, MaaConfig, MaaUserConfig, Task, TaskManager, Main
|
|||||||
from .models import MaaManager
|
from .models import MaaManager
|
||||||
from .services import Notify, Crypto, System
|
from .services import Notify, Crypto, System
|
||||||
from .ui import AUTO_MAA
|
from .ui import AUTO_MAA
|
||||||
from .utils import DownloadManager
|
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"QueueConfig",
|
"QueueConfig",
|
||||||
@@ -47,5 +46,4 @@ __all__ = [
|
|||||||
"Crypto",
|
"Crypto",
|
||||||
"System",
|
"System",
|
||||||
"AUTO_MAA",
|
"AUTO_MAA",
|
||||||
"DownloadManager",
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA核心组件包
|
AUTO_MAA核心组件包
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -29,8 +29,19 @@ __version__ = "4.2.0"
|
|||||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||||
__license__ = "GPL-3.0 license"
|
__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 .main_info_bar import MainInfoBar
|
||||||
|
from .network import Network
|
||||||
|
from .sound_player import SoundPlayer
|
||||||
from .task_manager import Task, TaskManager
|
from .task_manager import Task, TaskManager
|
||||||
from .timer import MainTimer
|
from .timer import MainTimer
|
||||||
|
|
||||||
@@ -39,7 +50,13 @@ __all__ = [
|
|||||||
"QueueConfig",
|
"QueueConfig",
|
||||||
"MaaConfig",
|
"MaaConfig",
|
||||||
"MaaUserConfig",
|
"MaaUserConfig",
|
||||||
|
"MaaPlanConfig",
|
||||||
|
"GeneralConfig",
|
||||||
|
"GeneralSubConfig",
|
||||||
|
"logger",
|
||||||
"MainInfoBar",
|
"MainInfoBar",
|
||||||
|
"Network",
|
||||||
|
"SoundPlayer",
|
||||||
"Task",
|
"Task",
|
||||||
"TaskManager",
|
"TaskManager",
|
||||||
"MainTimer",
|
"MainTimer",
|
||||||
|
|||||||
2060
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)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,47 +16,64 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA信息通知栏
|
AUTO_MAA信息通知栏
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import InfoBar, InfoBarPosition
|
||||||
InfoBar,
|
|
||||||
InfoBarPosition,
|
from .logger import logger
|
||||||
)
|
from .config import Config
|
||||||
|
from .sound_player import SoundPlayer
|
||||||
|
|
||||||
|
|
||||||
class _MainInfoBar:
|
class _MainInfoBar:
|
||||||
"""信息通知栏"""
|
"""信息通知栏"""
|
||||||
|
|
||||||
def __init__(self, main_window=None):
|
# 模式到 InfoBar 方法的映射
|
||||||
|
mode_mapping = {
|
||||||
|
"success": InfoBar.success,
|
||||||
|
"warning": InfoBar.warning,
|
||||||
|
"error": InfoBar.error,
|
||||||
|
"info": InfoBar.info,
|
||||||
|
}
|
||||||
|
|
||||||
self.main_window = main_window
|
def push_info_bar(
|
||||||
|
self, mode: str, title: str, content: str, time: int, if_force: bool = False
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
推送消息到吐司通知栏
|
||||||
|
|
||||||
def push_info_bar(self, mode: str, title: str, content: str, time: int):
|
:param mode: 通知栏模式,支持 "success", "warning", "error", "info"
|
||||||
"""推送到信息通知栏"""
|
:param title: 通知栏标题
|
||||||
if self.main_window is None:
|
:type title: str
|
||||||
logger.error("信息通知栏未设置父窗口")
|
:param content: 通知栏内容
|
||||||
|
:type content: str
|
||||||
|
:param time: 显示时长,单位为毫秒
|
||||||
|
:type time: int
|
||||||
|
:param if_force: 是否强制推送
|
||||||
|
:type if_force: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
if Config.main_window is None:
|
||||||
|
logger.error("信息通知栏未设置父窗口", module="吐司通知栏")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 定义模式到 InfoBar 方法的映射
|
|
||||||
mode_mapping = {
|
|
||||||
"success": InfoBar.success,
|
|
||||||
"warning": InfoBar.warning,
|
|
||||||
"error": InfoBar.error,
|
|
||||||
"info": InfoBar.info,
|
|
||||||
}
|
|
||||||
|
|
||||||
# 根据 mode 获取对应的 InfoBar 方法
|
# 根据 mode 获取对应的 InfoBar 方法
|
||||||
info_bar_method = mode_mapping.get(mode)
|
info_bar_method = self.mode_mapping.get(mode)
|
||||||
if info_bar_method:
|
|
||||||
|
if not info_bar_method:
|
||||||
|
logger.error(f"未知的通知栏模式: {mode}", module="吐司通知栏")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if Config.main_window.isVisible():
|
||||||
|
# 主窗口可见时直接推送通知
|
||||||
info_bar_method(
|
info_bar_method(
|
||||||
title=title,
|
title=title,
|
||||||
content=content,
|
content=content,
|
||||||
@@ -64,10 +81,29 @@ class _MainInfoBar:
|
|||||||
isClosable=True,
|
isClosable=True,
|
||||||
position=InfoBarPosition.TOP_RIGHT,
|
position=InfoBarPosition.TOP_RIGHT,
|
||||||
duration=time,
|
duration=time,
|
||||||
parent=self.main_window,
|
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()
|
MainInfoBar = _MainInfoBar()
|
||||||
|
|||||||
308
app/core/network.py
Normal file
@@ -0,0 +1,308 @@
|
|||||||
|
# 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, QThread, QEventLoop
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
import truststore
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from .logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
class NetworkThread(QThread):
|
||||||
|
"""网络请求线程类"""
|
||||||
|
|
||||||
|
max_retries = 3
|
||||||
|
timeout = 10
|
||||||
|
backoff_factor = 0.1
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
mode: str,
|
||||||
|
url: str,
|
||||||
|
path: Path = None,
|
||||||
|
files: Dict = None,
|
||||||
|
data: Dict = None,
|
||||||
|
) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.setObjectName(
|
||||||
|
f"NetworkThread-{mode}-{re.sub(r'(&cdk=)[^&]+(&)', r'\1******\2', url)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"创建网络请求线程: {self.objectName()}", module="网络请求子线程")
|
||||||
|
|
||||||
|
self.mode = mode
|
||||||
|
self.url = url
|
||||||
|
self.path = path
|
||||||
|
self.files = files
|
||||||
|
self.data = data
|
||||||
|
|
||||||
|
from .config import Config
|
||||||
|
|
||||||
|
self.proxies = {
|
||||||
|
"http": Config.get(Config.update_ProxyAddress),
|
||||||
|
"https": Config.get(Config.update_ProxyAddress),
|
||||||
|
}
|
||||||
|
|
||||||
|
self.status_code = None
|
||||||
|
self.response_json = None
|
||||||
|
self.error_message = None
|
||||||
|
|
||||||
|
self.loop = QEventLoop()
|
||||||
|
|
||||||
|
truststore.inject_into_ssl() # 信任系统证书
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
|
def run(self) -> None:
|
||||||
|
"""运行网络请求线程"""
|
||||||
|
|
||||||
|
if self.mode == "get":
|
||||||
|
self.get_json(self.url)
|
||||||
|
elif self.mode == "get_file":
|
||||||
|
self.get_file(self.url, self.path)
|
||||||
|
elif self.mode == "upload_file":
|
||||||
|
self.upload_file(self.url, self.files, self.data)
|
||||||
|
|
||||||
|
def get_json(self, url: str) -> None:
|
||||||
|
"""
|
||||||
|
通过get方法获取json数据
|
||||||
|
|
||||||
|
:param url: 请求的URL
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"子线程 {self.objectName()} 开始网络请求", module="网络请求子线程")
|
||||||
|
|
||||||
|
response = None
|
||||||
|
|
||||||
|
for _ in range(self.max_retries):
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=self.timeout, proxies=self.proxies)
|
||||||
|
self.status_code = response.status_code
|
||||||
|
self.response_json = response.json()
|
||||||
|
self.error_message = None
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
self.status_code = response.status_code if response else None
|
||||||
|
self.response_json = None
|
||||||
|
self.error_message = str(e)
|
||||||
|
logger.exception(
|
||||||
|
f"子线程 {self.objectName()} 网络请求失败:{e},第{_+1}次尝试",
|
||||||
|
module="网络请求子线程",
|
||||||
|
)
|
||||||
|
time.sleep(self.backoff_factor)
|
||||||
|
|
||||||
|
self.loop.quit()
|
||||||
|
|
||||||
|
def get_file(self, url: str, path: Path) -> None:
|
||||||
|
"""
|
||||||
|
通过get方法下载文件到指定路径
|
||||||
|
|
||||||
|
:param url: 请求的URL
|
||||||
|
:param path: 下载文件的保存路径
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"子线程 {self.objectName()} 开始下载文件", module="网络请求子线程")
|
||||||
|
|
||||||
|
response = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
response = requests.get(url, timeout=self.timeout, proxies=self.proxies)
|
||||||
|
if response.status_code == 200:
|
||||||
|
with open(path, "wb") as file:
|
||||||
|
file.write(response.content)
|
||||||
|
self.status_code = response.status_code
|
||||||
|
self.error_message = None
|
||||||
|
else:
|
||||||
|
self.status_code = response.status_code
|
||||||
|
self.error_message = f"下载失败,状态码: {response.status_code}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.status_code = response.status_code if response else None
|
||||||
|
self.error_message = str(e)
|
||||||
|
logger.exception(
|
||||||
|
f"子线程 {self.objectName()} 网络请求失败:{e}", module="网络请求子线程"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.loop.quit()
|
||||||
|
|
||||||
|
def upload_file(self, url: str, files: Dict, data: Dict = None) -> None:
|
||||||
|
"""
|
||||||
|
通过POST方法上传文件
|
||||||
|
|
||||||
|
:param url: 请求的URL
|
||||||
|
:param files: 文件字典,格式为 {'file': ('filename', file_obj, 'content_type')}
|
||||||
|
:param data: 表单数据字典
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"子线程 {self.objectName()} 开始上传文件", module="网络请求子线程")
|
||||||
|
|
||||||
|
response = None
|
||||||
|
|
||||||
|
for _ in range(self.max_retries):
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
files=files,
|
||||||
|
data=data,
|
||||||
|
timeout=self.timeout,
|
||||||
|
proxies=self.proxies,
|
||||||
|
)
|
||||||
|
self.status_code = response.status_code
|
||||||
|
|
||||||
|
# 尝试解析JSON响应
|
||||||
|
try:
|
||||||
|
self.response_json = response.json()
|
||||||
|
except ValueError:
|
||||||
|
# 如果不是JSON格式,保存文本内容
|
||||||
|
self.response_json = {"text": response.text}
|
||||||
|
|
||||||
|
self.error_message = None
|
||||||
|
break
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.status_code = response.status_code if response else None
|
||||||
|
self.response_json = None
|
||||||
|
self.error_message = str(e)
|
||||||
|
logger.exception(
|
||||||
|
f"子线程 {self.objectName()} 文件上传失败:{e},第{_+1}次尝试",
|
||||||
|
module="网络请求子线程",
|
||||||
|
)
|
||||||
|
time.sleep(self.backoff_factor)
|
||||||
|
|
||||||
|
self.loop.quit()
|
||||||
|
|
||||||
|
|
||||||
|
class _Network(QObject):
|
||||||
|
"""网络请求线程管理类"""
|
||||||
|
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
|
||||||
|
self.task_queue = []
|
||||||
|
|
||||||
|
def add_task(
|
||||||
|
self,
|
||||||
|
mode: str,
|
||||||
|
url: str,
|
||||||
|
path: Path = None,
|
||||||
|
files: Dict = None,
|
||||||
|
data: Dict = None,
|
||||||
|
) -> NetworkThread:
|
||||||
|
"""
|
||||||
|
添加网络请求任务
|
||||||
|
|
||||||
|
:param mode: 请求模式,支持 "get", "get_file", "upload_file"
|
||||||
|
:param url: 请求的URL
|
||||||
|
:param path: 下载文件的保存路径,仅在 mode 为 "get_file" 时有效
|
||||||
|
:param files: 上传文件字典,仅在 mode 为 "upload_file" 时有效
|
||||||
|
:param data: 表单数据字典,仅在 mode 为 "upload_file" 时有效
|
||||||
|
:return: 返回创建的 NetworkThread 实例
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"添加网络请求任务: {mode} {url} {path}", module="网络请求")
|
||||||
|
|
||||||
|
network_thread = NetworkThread(mode, url, path, files, data)
|
||||||
|
|
||||||
|
self.task_queue.append(network_thread)
|
||||||
|
|
||||||
|
network_thread.start()
|
||||||
|
|
||||||
|
return network_thread
|
||||||
|
|
||||||
|
def upload_config_file(
|
||||||
|
self, file_path: Path, username: str = "", description: str = ""
|
||||||
|
) -> NetworkThread:
|
||||||
|
"""
|
||||||
|
上传配置文件到分享服务器
|
||||||
|
|
||||||
|
:param file_path: 要上传的文件路径
|
||||||
|
:param username: 用户名(可选)
|
||||||
|
:param description: 文件描述(必填)
|
||||||
|
:return: 返回创建的 NetworkThread 实例
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not file_path.exists():
|
||||||
|
raise FileNotFoundError(f"文件不存在: {file_path}")
|
||||||
|
|
||||||
|
if not description:
|
||||||
|
raise ValueError("文件描述不能为空")
|
||||||
|
|
||||||
|
# 准备上传的文件
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
files = {"file": (file_path.name, f.read(), "application/json")}
|
||||||
|
|
||||||
|
# 准备表单数据
|
||||||
|
data = {"description": description}
|
||||||
|
|
||||||
|
if username:
|
||||||
|
data["username"] = username
|
||||||
|
|
||||||
|
url = "http://221.236.27.82:10023/api/upload/share"
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"准备上传配置文件: {file_path.name},用户: {username or '匿名'},描述: {description}",
|
||||||
|
extra={"module": "网络请求"},
|
||||||
|
)
|
||||||
|
|
||||||
|
return self.add_task("upload_file", url, files=files, data=data)
|
||||||
|
|
||||||
|
def get_result(self, network_thread: NetworkThread) -> dict:
|
||||||
|
"""
|
||||||
|
获取网络请求结果
|
||||||
|
|
||||||
|
:param network_thread: 网络请求线程实例
|
||||||
|
:return: 包含状态码、响应JSON和错误信息的字典
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = {
|
||||||
|
"status_code": network_thread.status_code,
|
||||||
|
"response_json": network_thread.response_json,
|
||||||
|
"error_message": (
|
||||||
|
re.sub(r"(&cdk=)[^&]+(&)", r"\1******\2", network_thread.error_message)
|
||||||
|
if network_thread.error_message
|
||||||
|
else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
network_thread.quit()
|
||||||
|
network_thread.wait()
|
||||||
|
self.task_queue.remove(network_thread)
|
||||||
|
network_thread.deleteLater()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"网络请求结果: {result['status_code']},请求子线程已结束",
|
||||||
|
module="网络请求",
|
||||||
|
)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
Network = _Network()
|
||||||
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()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,34 +16,39 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA业务调度器
|
AUTO_MAA业务调度器
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from PySide6.QtCore import QThread, QObject, Signal
|
from PySide6.QtCore import QThread, QObject, Signal
|
||||||
from qfluentwidgets import MessageBox
|
from qfluentwidgets import MessageBox
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from packaging import version
|
||||||
from typing import Dict, Union
|
from typing import Dict, Union
|
||||||
|
|
||||||
|
from .logger import logger
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .main_info_bar import MainInfoBar
|
from .main_info_bar import MainInfoBar
|
||||||
from app.models import MaaManager
|
from .network import Network
|
||||||
from app.services import System
|
from .sound_player import SoundPlayer
|
||||||
|
from app.models import MaaManager, GeneralManager
|
||||||
|
|
||||||
|
|
||||||
class Task(QThread):
|
class Task(QThread):
|
||||||
"""业务线程"""
|
"""业务线程"""
|
||||||
|
|
||||||
|
check_maa_version = Signal(str)
|
||||||
push_info_bar = Signal(str, str, str, int)
|
push_info_bar = Signal(str, str, str, int)
|
||||||
|
play_sound = Signal(str)
|
||||||
question = Signal(str, str)
|
question = Signal(str, str)
|
||||||
question_response = Signal(bool)
|
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_task_list = Signal(list)
|
||||||
create_user_list = Signal(list)
|
create_user_list = Signal(list)
|
||||||
update_task_list = Signal(list)
|
update_task_list = Signal(list)
|
||||||
@@ -56,6 +61,8 @@ class Task(QThread):
|
|||||||
):
|
):
|
||||||
super(Task, self).__init__()
|
super(Task, self).__init__()
|
||||||
|
|
||||||
|
self.setObjectName(f"Task-{mode}-{name}")
|
||||||
|
|
||||||
self.mode = mode
|
self.mode = mode
|
||||||
self.name = name
|
self.name = name
|
||||||
self.info = info
|
self.info = info
|
||||||
@@ -69,30 +76,58 @@ class Task(QThread):
|
|||||||
|
|
||||||
if "设置MAA" in self.mode:
|
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.push_info_bar.emit("info", "设置MAA", self.name, 3000)
|
||||||
|
|
||||||
self.task = MaaManager(
|
self.task = MaaManager(
|
||||||
self.mode,
|
self.mode,
|
||||||
Config.member_dict[self.name],
|
Config.script_dict[self.name],
|
||||||
(None if "全局" in self.mode else self.info["SetMaaInfo"]["Path"]),
|
(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.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.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:
|
else:
|
||||||
|
|
||||||
|
logger.info(f"任务开始:{self.name}", module=f"业务 {self.name}")
|
||||||
self.task_list = [
|
self.task_list = [
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
value
|
value
|
||||||
if Config.member_dict[value]["Config"].get(
|
if Config.script_dict[value]["Config"].get_name() == ""
|
||||||
Config.member_dict[value]["Config"].MaaSet_Name
|
else f"{value} - {Config.script_dict[value]["Config"].get_name()}"
|
||||||
)
|
|
||||||
== ""
|
|
||||||
else f"{value} - {Config.member_dict[value]["Config"].get(Config.member_dict[value]["Config"].MaaSet_Name)}"
|
|
||||||
),
|
),
|
||||||
"等待",
|
"等待",
|
||||||
value,
|
value,
|
||||||
@@ -113,49 +148,107 @@ class Task(QThread):
|
|||||||
task[1] = "运行"
|
task[1] = "运行"
|
||||||
self.update_task_list.emit(self.task_list)
|
self.update_task_list.emit(self.task_list)
|
||||||
|
|
||||||
|
# 检查任务是否在运行列表中
|
||||||
if task[2] in Config.running_list:
|
if task[2] in Config.running_list:
|
||||||
|
|
||||||
task[1] = "跳过"
|
task[1] = "跳过"
|
||||||
self.update_task_list.emit(self.task_list)
|
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)
|
self.push_info_bar.emit("info", "跳过任务", task[0], 3000)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# 标记为运行中
|
||||||
Config.running_list.append(task[2])
|
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)
|
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.task = MaaManager(
|
||||||
self.mode[0:4],
|
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)
|
||||||
|
self.task.question.connect(self.question.emit)
|
||||||
|
self.question_response.disconnect()
|
||||||
|
self.question_response.connect(self.task.question_response.emit)
|
||||||
|
self.task.push_info_bar.connect(self.push_info_bar.emit)
|
||||||
|
self.task.play_sound.connect(self.play_sound.emit)
|
||||||
|
self.task.create_user_list.connect(self.create_user_list.emit)
|
||||||
|
self.task.update_user_list.connect(self.update_user_list.emit)
|
||||||
|
self.task.update_log_text.connect(self.update_log_text.emit)
|
||||||
|
self.task.update_user_info.connect(self.update_maa_user_info.emit)
|
||||||
|
self.task.accomplish.connect(
|
||||||
|
lambda log: self.task_accomplish(task[2], log)
|
||||||
|
)
|
||||||
|
|
||||||
|
elif Config.script_dict[task[2]]["Type"] == "General":
|
||||||
|
|
||||||
|
self.task = GeneralManager(
|
||||||
|
self.mode[0:4],
|
||||||
|
Config.script_dict[task[2]],
|
||||||
)
|
)
|
||||||
|
|
||||||
self.task.question.connect(self.question.emit)
|
self.task.question.connect(self.question.emit)
|
||||||
self.question_response.disconnect()
|
self.question_response.disconnect()
|
||||||
self.question_response.connect(self.task.question_response.emit)
|
self.question_response.connect(self.task.question_response.emit)
|
||||||
self.task.push_info_bar.connect(self.push_info_bar.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.create_user_list.connect(self.create_user_list.emit)
|
||||||
self.task.update_user_list.connect(self.update_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_log_text.connect(self.update_log_text.emit)
|
||||||
self.task.update_user_info.connect(self.update_user_info.emit)
|
self.task.update_sub_info.connect(self.update_general_sub_info.emit)
|
||||||
self.task.accomplish.connect(
|
self.task.accomplish.connect(
|
||||||
lambda log: self.task_accomplish(task[2], log)
|
lambda log: self.task_accomplish(task[2], log)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.task.run()
|
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])
|
Config.running_list.remove(task[2])
|
||||||
|
|
||||||
task[1] = "完成"
|
|
||||||
logger.info(f"任务完成:{task[0]}")
|
|
||||||
self.push_info_bar.emit("info", "任务完成", task[0], 3000)
|
|
||||||
|
|
||||||
self.accomplish.emit(self.logs)
|
self.accomplish.emit(self.logs)
|
||||||
|
|
||||||
def task_accomplish(self, name: str, log: dict):
|
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.logs.append([name, log])
|
||||||
self.task.deleteLater()
|
self.task.deleteLater()
|
||||||
@@ -166,18 +259,22 @@ class _TaskManager(QObject):
|
|||||||
|
|
||||||
create_gui = Signal(Task)
|
create_gui = Signal(Task)
|
||||||
connect_gui = Signal(Task)
|
connect_gui = Signal(Task)
|
||||||
push_info_bar = Signal(str, str, str, int)
|
|
||||||
|
|
||||||
def __init__(self, main_window=None):
|
def __init__(self):
|
||||||
super(_TaskManager, self).__init__()
|
super(_TaskManager, self).__init__()
|
||||||
|
|
||||||
self.main_window = main_window
|
|
||||||
self.task_dict: Dict[str, Task] = {}
|
self.task_dict: Dict[str, Task] = {}
|
||||||
|
|
||||||
def add_task(
|
def add_task(
|
||||||
self, mode: str, name: str, info: Dict[str, Dict[str, Union[str, int, bool]]]
|
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:
|
if name in Config.running_list or name in self.task_dict:
|
||||||
|
|
||||||
@@ -185,32 +282,47 @@ class _TaskManager(QObject):
|
|||||||
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
|
MainInfoBar.push_info_bar("warning", "任务已存在", name, 5000)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
logger.info(f"任务开始:{name}")
|
logger.info(f"任务开始:{name},模式:{mode}", module="业务调度")
|
||||||
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
|
MainInfoBar.push_info_bar("info", "任务开始", name, 3000)
|
||||||
|
SoundPlayer.play("任务开始")
|
||||||
|
|
||||||
|
# 标记任务为运行中
|
||||||
Config.running_list.append(name)
|
Config.running_list.append(name)
|
||||||
|
|
||||||
|
# 创建任务实例并连接信号
|
||||||
self.task_dict[name] = Task(mode, name, info)
|
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(
|
self.task_dict[name].question.connect(
|
||||||
lambda title, content: self.push_dialog(name, title, content)
|
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].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(
|
self.task_dict[name].accomplish.connect(
|
||||||
lambda logs: self.remove_task(mode, name, logs)
|
lambda logs: self.remove_task(mode, name, logs)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 向UI发送信号以创建或连接GUI
|
||||||
if "新调度台" in mode:
|
if "新调度台" in mode:
|
||||||
self.create_gui.emit(self.task_dict[name])
|
self.create_gui.emit(self.task_dict[name])
|
||||||
|
|
||||||
elif "主调度台" in mode:
|
elif "主调度台" in mode:
|
||||||
self.connect_gui.emit(self.task_dict[name])
|
self.connect_gui.emit(self.task_dict[name])
|
||||||
|
|
||||||
|
# 启动任务线程
|
||||||
self.task_dict[name].start()
|
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)
|
MainInfoBar.push_info_bar("info", "中止任务", name, 3000)
|
||||||
|
|
||||||
if name == "ALL":
|
if name == "ALL":
|
||||||
@@ -229,68 +341,116 @@ class _TaskManager(QObject):
|
|||||||
self.task_dict[name].quit()
|
self.task_dict[name].quit()
|
||||||
self.task_dict[name].wait()
|
self.task_dict[name].wait()
|
||||||
|
|
||||||
def remove_task(self, mode: str, name: str, logs: str):
|
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)
|
MainInfoBar.push_info_bar("info", "任务结束", name, 3000)
|
||||||
|
SoundPlayer.play("任务结束")
|
||||||
|
|
||||||
|
# 删除任务线程,移除运行中标记
|
||||||
self.task_dict[name].deleteLater()
|
self.task_dict[name].deleteLater()
|
||||||
|
|
||||||
if len(logs) > 0:
|
|
||||||
time = logs[0][1]["Time"]
|
|
||||||
history = ""
|
|
||||||
for log in logs:
|
|
||||||
Config.save_history(log[0], log[1])
|
|
||||||
history += (
|
|
||||||
f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n"
|
|
||||||
)
|
|
||||||
Config.save_history(name, {"Time": time, "History": history})
|
|
||||||
else:
|
|
||||||
Config.save_history(
|
|
||||||
name,
|
|
||||||
{
|
|
||||||
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
||||||
"History": "没有任务被执行",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
self.task_dict.pop(name)
|
self.task_dict.pop(name)
|
||||||
Config.running_list.remove(name)
|
Config.running_list.remove(name)
|
||||||
|
|
||||||
if "调度队列" in name and "人工排查" not in mode:
|
if "调度队列" in name and "人工排查" not in mode:
|
||||||
|
|
||||||
|
# 保存调度队列历史记录
|
||||||
|
if len(logs) > 0:
|
||||||
|
time = logs[0][1]["Time"]
|
||||||
|
history = ""
|
||||||
|
for log in logs:
|
||||||
|
history += f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n"
|
||||||
|
Config.save_history(name, {"Time": time, "History": history})
|
||||||
|
else:
|
||||||
|
Config.save_history(
|
||||||
|
name,
|
||||||
|
{
|
||||||
|
"Time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||||
|
"History": "没有任务被执行",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 根据调度队列情况设置电源状态
|
||||||
if (
|
if (
|
||||||
Config.queue_dict[name]["Config"].get(
|
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"
|
||||||
):
|
):
|
||||||
|
Config.set_power_sign(
|
||||||
from app.ui import ProgressRingMessageBox
|
Config.queue_dict[name]["Config"].get(
|
||||||
|
Config.queue_dict[name]["Config"].QueueSet_AfterAccomplish
|
||||||
mode_book = {
|
|
||||||
"Shutdown": "关机",
|
|
||||||
"Hibernate": "休眠",
|
|
||||||
"Sleep": "睡眠",
|
|
||||||
"KillSelf": "关闭AUTO_MAA",
|
|
||||||
}
|
|
||||||
|
|
||||||
choice = ProgressRingMessageBox(
|
|
||||||
self.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
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if Config.args.mode == "cli" and Config.power_sign == "NoAction":
|
||||||
|
Config.set_power_sign("KillSelf")
|
||||||
|
|
||||||
|
def check_maa_version(self, v: str) -> None:
|
||||||
|
"""
|
||||||
|
检查MAA版本,如果版本过低则推送通知
|
||||||
|
|
||||||
|
:param v: 当前MAA版本
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"检查MAA版本:{v}", module="业务调度")
|
||||||
|
network = Network.add_task(
|
||||||
|
mode="get",
|
||||||
|
url="https://mirrorchyan.com/api/resources/MAA/latest?user_agent=AutoMaaGui&os=win&arch=x64&channel=stable",
|
||||||
|
)
|
||||||
|
network.loop.exec()
|
||||||
|
network_result = Network.get_result(network)
|
||||||
|
if network_result["status_code"] == 200:
|
||||||
|
maa_info = network_result["response_json"]
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"获取MAA版本信息时出错:{network_result['error_message']}",
|
||||||
|
module="业务调度",
|
||||||
|
)
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"warning",
|
||||||
|
"获取MAA版本信息时出错",
|
||||||
|
f"网络错误:{network_result['status_code']}",
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if version.parse(maa_info["data"]["version_name"]) > version.parse(v):
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"检测到MAA版本过低:{v},最新版本:{maa_info['data']['version_name']}",
|
||||||
|
module="业务调度",
|
||||||
|
)
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"info",
|
||||||
|
"MAA版本过低",
|
||||||
|
f"当前版本:{v},最新稳定版:{maa_info['data']['version_name']}",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.success(
|
||||||
|
f"MAA版本检查完成:{v},最新版本:{maa_info['data']['version_name']}",
|
||||||
|
module="业务调度",
|
||||||
|
)
|
||||||
|
|
||||||
def push_dialog(self, name: str, title: str, content: str):
|
def push_dialog(self, name: str, title: str, content: str):
|
||||||
"""推送对话框"""
|
"""
|
||||||
|
推送来自任务线程的对话框
|
||||||
|
|
||||||
choice = MessageBox(title, content, self.main_window)
|
:param name: 任务名称
|
||||||
|
:param title: 对话框标题
|
||||||
|
:param content: 对话框内容
|
||||||
|
"""
|
||||||
|
|
||||||
|
choice = MessageBox(title, content, Config.main_window)
|
||||||
choice.yesButton.setText("是")
|
choice.yesButton.setText("是")
|
||||||
choice.cancelButton.setText("否")
|
choice.cancelButton.setText("否")
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,72 +16,89 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA主业务定时器
|
AUTO_MAA主业务定时器
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
from PySide6.QtCore import QObject, QTimer
|
||||||
from PySide6.QtWidgets import QWidget
|
|
||||||
from PySide6.QtCore import QTimer
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
import pyautogui
|
import keyboard
|
||||||
|
|
||||||
|
from .logger import logger
|
||||||
from .config import Config
|
from .config import Config
|
||||||
from .task_manager import TaskManager
|
from .task_manager import TaskManager
|
||||||
from app.services import System
|
from app.services import System
|
||||||
|
|
||||||
|
|
||||||
class _MainTimer(QWidget):
|
class _MainTimer(QObject):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.if_FailSafeException = False
|
|
||||||
|
|
||||||
self.Timer = QTimer()
|
self.Timer = QTimer()
|
||||||
self.Timer.timeout.connect(self.timed_start)
|
self.Timer.timeout.connect(self.timed_start)
|
||||||
self.Timer.timeout.connect(self.set_silence)
|
self.Timer.timeout.connect(self.set_silence)
|
||||||
self.Timer.start(1000)
|
self.Timer.timeout.connect(self.check_power)
|
||||||
|
|
||||||
self.LongTimer = QTimer()
|
self.LongTimer = QTimer()
|
||||||
self.LongTimer.timeout.connect(self.long_timed_task)
|
self.LongTimer.timeout.connect(self.long_timed_task)
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
"""启动定时器"""
|
||||||
|
|
||||||
|
logger.info("启动主定时器", module="主业务定时器")
|
||||||
|
self.Timer.start(1000)
|
||||||
self.LongTimer.start(3600000)
|
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):
|
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()
|
||||||
|
|
||||||
def timed_start(self):
|
def timed_start(self):
|
||||||
"""定时启动代理任务"""
|
"""定时启动代理任务"""
|
||||||
|
|
||||||
for name, info in Config.queue_dict.items():
|
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
|
continue
|
||||||
|
|
||||||
history = Config.get_history(name)
|
|
||||||
|
|
||||||
data = info["Config"].toDict()
|
data = info["Config"].toDict()
|
||||||
|
|
||||||
time_set = [
|
time_set = [
|
||||||
data["Time"][f"TimeSet_{_}"]
|
data["Time"][f"Set_{_}"]
|
||||||
for _ in range(10)
|
for _ in range(10)
|
||||||
if data["Time"][f"TimeEnabled_{_}"]
|
if data["Time"][f"Enabled_{_}"]
|
||||||
]
|
]
|
||||||
# 按时间调起代理任务
|
# 按时间调起代理任务
|
||||||
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
|
curtime = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||||
if (
|
if (
|
||||||
curtime[11:16] in time_set
|
curtime[11:16] in time_set
|
||||||
and curtime != history["Time"][:16]
|
and curtime
|
||||||
|
!= info["Config"].get(info["Config"].Data_LastProxyTime)[:16]
|
||||||
and name not in Config.running_list
|
and name not in Config.running_list
|
||||||
):
|
):
|
||||||
|
|
||||||
logger.info(f"定时任务:{name}")
|
logger.info(f"定时唤起任务:{name}。", module="主业务定时器")
|
||||||
TaskManager.add_task("自动代理_新调度台", name, data)
|
TaskManager.add_task("自动代理_新调度台", name, data)
|
||||||
|
|
||||||
def set_silence(self):
|
def set_silence(self):
|
||||||
@@ -94,22 +111,65 @@ class _MainTimer(QWidget):
|
|||||||
):
|
):
|
||||||
|
|
||||||
windows = System.get_window_info()
|
windows = System.get_window_info()
|
||||||
if any(
|
|
||||||
str(emulator_path) in window
|
emulator_windows = []
|
||||||
for window in windows
|
for window in windows:
|
||||||
for emulator_path in Config.silence_list
|
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:
|
try:
|
||||||
pyautogui.hotkey(
|
keyboard.press_and_release(
|
||||||
*[
|
"+".join(
|
||||||
_.strip().lower()
|
_.strip().lower()
|
||||||
for _ in Config.get(Config.function_BossKey).split("+")
|
for _ in Config.get(Config.function_BossKey).split("+")
|
||||||
]
|
)
|
||||||
)
|
)
|
||||||
except pyautogui.FailSafeException as e:
|
logger.info(
|
||||||
if not self.if_FailSafeException:
|
f"模拟按键:{Config.get(Config.function_BossKey)}",
|
||||||
logger.warning(f"FailSafeException: {e}")
|
module="主业务定时器",
|
||||||
self.if_FailSafeException = True
|
)
|
||||||
|
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()
|
MainTimer = _MainTimer()
|
||||||
|
|||||||
1916
app/models/MAA.py
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA模组包
|
AUTO_MAA模组包
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ __version__ = "4.2.0"
|
|||||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||||
__license__ = "GPL-3.0 license"
|
__license__ = "GPL-3.0 license"
|
||||||
|
|
||||||
|
from .general import GeneralManager
|
||||||
from .MAA import MaaManager
|
from .MAA import MaaManager
|
||||||
|
|
||||||
__all__ = ["MaaManager"]
|
__all__ = ["GeneralManager", "MaaManager"]
|
||||||
|
|||||||
1201
app/models/general.py
Normal file
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA服务包
|
AUTO_MAA服务包
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -32,5 +32,6 @@ __license__ = "GPL-3.0 license"
|
|||||||
from .notification import Notify
|
from .notification import Notify
|
||||||
from .security import Crypto
|
from .security import Crypto
|
||||||
from .system import System
|
from .system import System
|
||||||
|
from .skland import skland_sign_in
|
||||||
|
|
||||||
__all__ = ["Notify", "Crypto", "System"]
|
__all__ = ["Notify", "Crypto", "System", "skland_sign_in"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,45 +16,57 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA通知服务
|
AUTO_MAA通知服务
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from PySide6.QtWidgets import QWidget
|
|
||||||
from PySide6.QtCore import Signal
|
|
||||||
import requests
|
|
||||||
from loguru import logger
|
|
||||||
from plyer import notification
|
|
||||||
import re
|
import re
|
||||||
import smtplib
|
import smtplib
|
||||||
from email.mime.text import MIMEText
|
import time
|
||||||
from email.mime.multipart import MIMEMultipart
|
|
||||||
from email.header import Header
|
from email.header import Header
|
||||||
|
from email.mime.multipart import MIMEMultipart
|
||||||
|
from email.mime.text import MIMEText
|
||||||
from email.utils import formataddr
|
from email.utils import formataddr
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Union
|
||||||
|
|
||||||
from serverchan_sdk import sc_send
|
import requests
|
||||||
|
from PySide6.QtCore import QObject, Signal
|
||||||
|
|
||||||
from app.core import Config
|
from plyer import notification
|
||||||
|
|
||||||
|
from app.core import Config, logger
|
||||||
from app.services.security import Crypto
|
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)
|
push_info_bar = Signal(str, str, str, int)
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
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):
|
if Config.get(Config.notify_IfPushPlyer):
|
||||||
|
|
||||||
|
logger.info(f"推送系统通知:{title}", module="通知服务")
|
||||||
|
|
||||||
notification.notify(
|
notification.notify(
|
||||||
title=title,
|
title=title,
|
||||||
message=message,
|
message=message,
|
||||||
@@ -67,190 +79,364 @@ class Notification(QWidget):
|
|||||||
|
|
||||||
return True
|
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 (
|
if (
|
||||||
Config.get(Config.notify_SMTPServerAddress) == ""
|
Config.get(Config.notify_SMTPServerAddress) == ""
|
||||||
or Config.get(Config.notify_AuthorizationCode) == ""
|
or Config.get(Config.notify_AuthorizationCode) == ""
|
||||||
or not bool(
|
or not bool(
|
||||||
re.match(
|
re.match(
|
||||||
r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$",
|
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),
|
|
||||||
)
|
|
||||||
)
|
|
||||||
):
|
|
||||||
logger.error(
|
|
||||||
"请正确设置邮件通知的SMTP服务器地址、授权码、发件人地址和收件人地址"
|
|
||||||
)
|
|
||||||
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(),
|
|
||||||
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,
|
|
||||||
)
|
|
||||||
smtpObj.login(
|
|
||||||
Config.get(Config.notify_FromAddress),
|
Config.get(Config.notify_FromAddress),
|
||||||
Crypto.win_decryptor(Config.get(Config.notify_AuthorizationCode)),
|
|
||||||
)
|
)
|
||||||
smtpObj.sendmail(
|
)
|
||||||
|
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),
|
Config.get(Config.notify_FromAddress),
|
||||||
Config.get(Config.notify_ToAddress),
|
|
||||||
message.as_string(),
|
|
||||||
)
|
)
|
||||||
smtpObj.quit()
|
) # 发件人显示的名字
|
||||||
logger.success("邮件发送成功")
|
message["To"] = formataddr(
|
||||||
except Exception as e:
|
(Header("AUTO_MAA用户", "utf-8").encode(), to_address)
|
||||||
logger.error(f"发送邮件时出错:\n{e}")
|
) # 收件人显示的名字
|
||||||
self.push_info_bar.emit("error", "发送邮件时出错", f"{e}", -1)
|
message["Subject"] = Header(title, "utf-8")
|
||||||
|
|
||||||
def ServerChanPush(self, title, content):
|
if mode == "网页":
|
||||||
"""使用Server酱推送通知"""
|
message.attach(MIMEText(content, "html", "utf-8"))
|
||||||
|
|
||||||
if Config.get(Config.notify_IfServerChan):
|
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)
|
||||||
|
|
||||||
if Config.get(Config.notify_ServerChanKey) == "":
|
def ServerChanPush(
|
||||||
logger.error("请正确设置Server酱的SendKey")
|
self, title, content, send_key, tag, channel
|
||||||
self.push_info_bar.emit(
|
) -> Union[bool, str]:
|
||||||
"error",
|
"""
|
||||||
"Server酱通知推送异常",
|
使用Server酱推送通知
|
||||||
"请正确设置Server酱的SendKey",
|
|
||||||
-1,
|
: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("|")))
|
||||||
)
|
)
|
||||||
return None
|
|
||||||
else:
|
|
||||||
send_key = Config.get(Config.notify_ServerChanKey)
|
|
||||||
|
|
||||||
option = {}
|
tags = "|".join(_.strip() for _ in tag.split("|"))
|
||||||
is_valid = lambda s: s == "" or (
|
channels = "|".join(_.strip() for _ in channel.split("|"))
|
||||||
s == "|".join(s.split("|")) and (s.count("|") == 0 or all(s.split("|")))
|
|
||||||
)
|
|
||||||
"""
|
|
||||||
is_valid => True, 如果启用的话需要正确设置Tag和Channel。
|
|
||||||
允许空的Tag和Channel即不启用,但不允许例如a||b,|a|b,a|b|,||||
|
|
||||||
"""
|
|
||||||
send_tag = "|".join(
|
|
||||||
_.strip() for _ in Config.get(Config.notify_ServerChanTag).split("|")
|
|
||||||
)
|
|
||||||
send_channel = "|".join(
|
|
||||||
_.strip()
|
|
||||||
for _ in Config.get(Config.notify_ServerChanChannel).split("|")
|
|
||||||
)
|
|
||||||
|
|
||||||
if is_valid(send_tag):
|
options = {}
|
||||||
option["tags"] = send_tag
|
if is_valid(tags):
|
||||||
|
options["tags"] = tags
|
||||||
else:
|
else:
|
||||||
option["tags"] = ""
|
logger.warning("Server酱 Tag 配置不正确,将被忽略", module="通知服务")
|
||||||
logger.warning("请正确设置Auto_MAA中ServerChan的Tag。")
|
|
||||||
self.push_info_bar.emit(
|
self.push_info_bar.emit(
|
||||||
"warning",
|
"warning",
|
||||||
"Server酱通知推送异常",
|
"Server酱通知推送异常",
|
||||||
"请正确设置Auto_MAA中ServerChan的Tag。",
|
"请正确设置 ServerChan 的 Tag",
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
if is_valid(send_channel):
|
if is_valid(channels):
|
||||||
option["channel"] = send_channel
|
options["channel"] = channels
|
||||||
else:
|
else:
|
||||||
option["channel"] = ""
|
logger.warning(
|
||||||
logger.warning("请正确设置Auto_MAA中ServerChan的Channel。")
|
"Server酱 Channel 配置不正确,将被忽略", module="通知服务"
|
||||||
|
)
|
||||||
self.push_info_bar.emit(
|
self.push_info_bar.emit(
|
||||||
"warning",
|
"warning",
|
||||||
"Server酱通知推送异常",
|
"Server酱通知推送异常",
|
||||||
"请正确设置Auto_MAA中ServerChan的Channel。",
|
"请正确设置 ServerChan 的 Channel",
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
|
|
||||||
response = sc_send(send_key, title, content, option)
|
# 请求发送
|
||||||
if response["code"] == 0:
|
params = {"title": title, "desp": content, **options}
|
||||||
logger.info("Server酱推送通知成功")
|
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
|
return True
|
||||||
else:
|
else:
|
||||||
logger.info("Server酱推送通知失败")
|
error_code = result.get("code", "-1")
|
||||||
logger.error(response)
|
logger.exception(
|
||||||
|
f"Server酱通知推送失败:响应码:{error_code}", module="通知服务"
|
||||||
|
)
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error", "Server酱通知推送失败", f"响应码:{error_code}", -1
|
||||||
|
)
|
||||||
|
return f"Server酱通知推送失败:{error_code}"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"Server酱通知推送异常:{e}", module="通知服务")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"Server酱通知推送异常",
|
||||||
|
"请检查相关设置和网络连接。如全部配置正确,请稍后再试。",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
return f"Server酱通知推送异常:{str(e)}"
|
||||||
|
|
||||||
|
def CompanyWebHookBotPush(self, title, content, webhook_url) -> Union[bool, str]:
|
||||||
|
"""
|
||||||
|
使用企业微信群机器人推送通知
|
||||||
|
|
||||||
|
:param title: 通知标题
|
||||||
|
:param content: 通知内容
|
||||||
|
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||||
|
:return: bool or str
|
||||||
|
"""
|
||||||
|
|
||||||
|
if webhook_url == "":
|
||||||
|
logger.error("请正确设置企业微信群机器人的WebHook地址", module="通知服务")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"企业微信群机器人通知推送异常",
|
||||||
|
"请正确设置企业微信群机器人的WebHook地址",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
content = f"{title}\n{content}"
|
||||||
|
data = {"msgtype": "text", "text": {"content": content}}
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url=webhook_url,
|
||||||
|
json=data,
|
||||||
|
timeout=10,
|
||||||
|
proxies={
|
||||||
|
"http": Config.get(Config.update_ProxyAddress),
|
||||||
|
"https": Config.get(Config.update_ProxyAddress),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
info = response.json()
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
err = e
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
logger.error(f"推送企业微信群机器人时出错:{err}", module="通知服务")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"企业微信群机器人通知推送失败",
|
||||||
|
f"使用企业微信群机器人推送通知时出错:{err}",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if info["errcode"] == 0:
|
||||||
|
logger.success(f"企业微信群机器人推送通知成功:{title}", module="通知服务")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"企业微信群机器人推送通知失败:{info}", module="通知服务")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"企业微信群机器人通知推送失败",
|
||||||
|
f"使用企业微信群机器人推送通知时出错:{err}",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
return f"使用企业微信群机器人推送通知时出错:{err}"
|
||||||
|
|
||||||
|
def CompanyWebHookBotPushImage(self, image_path: Path, webhook_url: str) -> bool:
|
||||||
|
"""
|
||||||
|
使用企业微信群机器人推送图片通知
|
||||||
|
|
||||||
|
:param image_path: 图片文件路径
|
||||||
|
:param webhook_url: 企业微信群机器人的WebHook地址
|
||||||
|
:return: bool
|
||||||
|
"""
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 压缩图片
|
||||||
|
ImageUtils.compress_image_if_needed(image_path)
|
||||||
|
|
||||||
|
# 检查图片是否存在
|
||||||
|
if not image_path.exists():
|
||||||
|
logger.error(
|
||||||
|
"图片推送异常 | 图片不存在或者压缩失败,请检查图片路径是否正确",
|
||||||
|
module="通知服务",
|
||||||
|
)
|
||||||
self.push_info_bar.emit(
|
self.push_info_bar.emit(
|
||||||
"error",
|
"error",
|
||||||
"Server酱通知推送失败",
|
"企业微信群机器人通知推送异常",
|
||||||
f'使用Server酱推送通知时出错:\n{response["data"]['error']}',
|
"图片不存在或者压缩失败,请检查图片路径是否正确",
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
return f'使用Server酱推送通知时出错:\n{response["data"]['error']}'
|
return False
|
||||||
|
|
||||||
def CompanyWebHookBotPush(self, title, content):
|
if not webhook_url:
|
||||||
"""使用企业微信群机器人推送通知"""
|
logger.error(
|
||||||
if Config.get(Config.notify_IfCompanyWebHookBot):
|
"请正确设置企业微信群机器人的WebHook地址", module="通知服务"
|
||||||
|
)
|
||||||
if Config.get(Config.notify_CompanyWebHookBotUrl) == "":
|
|
||||||
logger.error("请正确设置企业微信群机器人的WebHook地址")
|
|
||||||
self.push_info_bar.emit(
|
self.push_info_bar.emit(
|
||||||
"error",
|
"error",
|
||||||
"企业微信群机器人通知推送异常",
|
"企业微信群机器人通知推送异常",
|
||||||
"请正确设置企业微信群机器人的WebHook地址",
|
"请正确设置企业微信群机器人的WebHook地址",
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
return None
|
return False
|
||||||
|
|
||||||
content = f"{title}\n{content}"
|
# 获取图片base64和md5
|
||||||
data = {"msgtype": "text", "text": {"content": content}}
|
try:
|
||||||
response = requests.post(
|
image_base64 = ImageUtils.get_base64_from_file(str(image_path))
|
||||||
url=Config.get(Config.notify_CompanyWebHookBotUrl),
|
image_md5 = ImageUtils.calculate_md5_from_file(str(image_path))
|
||||||
json=data,
|
except Exception as e:
|
||||||
)
|
logger.exception(f"图片编码或MD5计算失败:{e}", module="通知服务")
|
||||||
if response.json()["errcode"] == 0:
|
|
||||||
logger.info("企业微信群机器人推送通知成功")
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
logger.info("企业微信群机器人推送通知失败")
|
|
||||||
logger.error(response.json())
|
|
||||||
self.push_info_bar.emit(
|
self.push_info_bar.emit(
|
||||||
"error",
|
"error",
|
||||||
"企业微信群机器人通知推送失败",
|
"企业微信群机器人通知推送异常",
|
||||||
f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}',
|
f"图片编码或MD5计算失败:{e}",
|
||||||
-1,
|
-1,
|
||||||
)
|
)
|
||||||
return (
|
return False
|
||||||
f'使用企业微信群机器人推送通知时出错:\n{response.json()["errmsg"]}'
|
|
||||||
|
data = {
|
||||||
|
"msgtype": "image",
|
||||||
|
"image": {"base64": image_base64, "md5": image_md5},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _ in range(3):
|
||||||
|
try:
|
||||||
|
response = requests.post(
|
||||||
|
url=webhook_url,
|
||||||
|
json=data,
|
||||||
|
timeout=10,
|
||||||
|
proxies={
|
||||||
|
"http": Config.get(Config.update_ProxyAddress),
|
||||||
|
"https": Config.get(Config.update_ProxyAddress),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
info = response.json()
|
||||||
|
break
|
||||||
|
except requests.RequestException as e:
|
||||||
|
err = e
|
||||||
|
logger.exception(
|
||||||
|
f"推送企业微信群机器人图片第{_+1}次失败:{e}", module="通知服务"
|
||||||
|
)
|
||||||
|
time.sleep(0.1)
|
||||||
|
else:
|
||||||
|
logger.error("推送企业微信群机器人图片时出错", module="通知服务")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"企业微信群机器人图片推送失败",
|
||||||
|
f"使用企业微信群机器人推送图片时出错:{err}",
|
||||||
|
-1,
|
||||||
)
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if info.get("errcode") == 0:
|
||||||
|
logger.success(
|
||||||
|
f"企业微信群机器人推送图片成功:{image_path.name}",
|
||||||
|
module="通知服务",
|
||||||
|
)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logger.error(f"企业微信群机器人推送图片失败:{info}", module="通知服务")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"企业微信群机器人图片推送失败",
|
||||||
|
f"使用企业微信群机器人推送图片时出错:{info}",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"推送企业微信群机器人图片时发生未知异常:{e}")
|
||||||
|
self.push_info_bar.emit(
|
||||||
|
"error",
|
||||||
|
"企业微信群机器人图片推送失败",
|
||||||
|
f"发生未知异常:{e}",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
def send_test_notification(self):
|
def send_test_notification(self):
|
||||||
"""发送测试通知到所有已启用的通知渠道"""
|
"""发送测试通知到所有已启用的通知渠道"""
|
||||||
|
|
||||||
|
logger.info("发送测试通知到所有已启用的通知渠道", module="通知服务")
|
||||||
|
|
||||||
# 发送系统通知
|
# 发送系统通知
|
||||||
self.push_plyer(
|
self.push_plyer(
|
||||||
"测试通知",
|
"测试通知",
|
||||||
@@ -265,6 +451,7 @@ class Notification(QWidget):
|
|||||||
"文本",
|
"文本",
|
||||||
"AUTO_MAA测试通知",
|
"AUTO_MAA测试通知",
|
||||||
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
||||||
|
Config.get(Config.notify_ToAddress),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 发送Server酱通知
|
# 发送Server酱通知
|
||||||
@@ -272,6 +459,9 @@ class Notification(QWidget):
|
|||||||
self.ServerChanPush(
|
self.ServerChanPush(
|
||||||
"AUTO_MAA测试通知",
|
"AUTO_MAA测试通知",
|
||||||
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 AUTO_MAA 的通知功能已经正确配置且可以正常工作!",
|
||||||
|
Config.get(Config.notify_ServerChanKey),
|
||||||
|
Config.get(Config.notify_ServerChanTag),
|
||||||
|
Config.get(Config.notify_ServerChanChannel),
|
||||||
)
|
)
|
||||||
|
|
||||||
# 发送企业微信机器人通知
|
# 发送企业微信机器人通知
|
||||||
@@ -279,7 +469,14 @@ class Notification(QWidget):
|
|||||||
self.CompanyWebHookBotPush(
|
self.CompanyWebHookBotPush(
|
||||||
"AUTO_MAA测试通知",
|
"AUTO_MAA测试通知",
|
||||||
"这是 AUTO_MAA 外部通知测试信息。如果你看到了这段内容,说明 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
|
return True
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,27 +16,24 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA安全服务
|
AUTO_MAA安全服务
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import random
|
import random
|
||||||
import secrets
|
import secrets
|
||||||
import base64
|
import base64
|
||||||
import win32crypt
|
import win32crypt
|
||||||
from pathlib import Path
|
|
||||||
from Crypto.Cipher import AES
|
from Crypto.Cipher import AES
|
||||||
from Crypto.PublicKey import RSA
|
from Crypto.PublicKey import RSA
|
||||||
from Crypto.Cipher import PKCS1_OAEP
|
from Crypto.Cipher import PKCS1_OAEP
|
||||||
from Crypto.Util.Padding import pad, unpad
|
from Crypto.Util.Padding import pad, unpad
|
||||||
from typing import List, Dict, Union
|
|
||||||
|
|
||||||
from app.core import Config
|
from app.core import Config
|
||||||
|
|
||||||
@@ -44,7 +41,12 @@ from app.core import Config
|
|||||||
class CryptoHandler:
|
class CryptoHandler:
|
||||||
|
|
||||||
def get_PASSWORD(self, PASSWORD: str) -> None:
|
def get_PASSWORD(self, PASSWORD: str) -> None:
|
||||||
"""配置管理密钥"""
|
"""
|
||||||
|
配置管理密钥
|
||||||
|
|
||||||
|
:param PASSWORD: 管理密钥
|
||||||
|
:type PASSWORD: str
|
||||||
|
"""
|
||||||
|
|
||||||
# 生成目录
|
# 生成目录
|
||||||
Config.key_path.mkdir(parents=True, exist_ok=True)
|
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)
|
(Config.app_path / "data/key/private_key.bin").write_bytes(private_key_local)
|
||||||
|
|
||||||
def AUTO_encryptor(self, note: str) -> str:
|
def AUTO_encryptor(self, note: str) -> str:
|
||||||
"""使用AUTO_MAA的算法加密数据"""
|
"""
|
||||||
|
使用AUTO_MAA的算法加密数据
|
||||||
|
|
||||||
|
:param note: 数据明文
|
||||||
|
:type note: str
|
||||||
|
"""
|
||||||
|
|
||||||
if note == "":
|
if note == "":
|
||||||
return ""
|
return ""
|
||||||
@@ -100,7 +107,16 @@ class CryptoHandler:
|
|||||||
return base64.b64encode(encrypted).decode("utf-8")
|
return base64.b64encode(encrypted).decode("utf-8")
|
||||||
|
|
||||||
def AUTO_decryptor(self, note: str, PASSWORD: str) -> str:
|
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 == "":
|
if note == "":
|
||||||
return ""
|
return ""
|
||||||
@@ -142,32 +158,71 @@ class CryptoHandler:
|
|||||||
return note
|
return note
|
||||||
|
|
||||||
def change_PASSWORD(self, PASSWORD_old: str, PASSWORD_new: str) -> None:
|
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():
|
if script["Type"] == "Maa":
|
||||||
user["Password"] = self.AUTO_decryptor(
|
for user in script["UserData"].values():
|
||||||
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
|
user["Password"] = self.AUTO_decryptor(
|
||||||
)
|
user["Config"].get(user["Config"].Info_Password), PASSWORD_old
|
||||||
|
)
|
||||||
|
|
||||||
self.get_PASSWORD(PASSWORD_new)
|
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():
|
if script["Type"] == "Maa":
|
||||||
user["Config"].set(
|
for user in script["UserData"].values():
|
||||||
user["Config"].Info_Password, self.AUTO_encryptor(user["Password"])
|
user["Config"].set(
|
||||||
)
|
user["Config"].Info_Password,
|
||||||
user["Password"] = None
|
self.AUTO_encryptor(user["Password"]),
|
||||||
del 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(
|
def win_encryptor(
|
||||||
self, note: str, description: str = None, entropy: bytes = None
|
self, note: str, description: str = None, entropy: bytes = None
|
||||||
) -> str:
|
) -> 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 == "":
|
if note == "":
|
||||||
return ""
|
return ""
|
||||||
@@ -178,7 +233,16 @@ class CryptoHandler:
|
|||||||
return base64.b64encode(encrypted).decode("utf-8")
|
return base64.b64encode(encrypted).decode("utf-8")
|
||||||
|
|
||||||
def win_decryptor(self, note: str, entropy: bytes = None) -> str:
|
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 == "":
|
if note == "":
|
||||||
return ""
|
return ""
|
||||||
@@ -188,21 +252,15 @@ class CryptoHandler:
|
|||||||
)
|
)
|
||||||
return decrypted[1].decode("utf-8")
|
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:
|
def check_PASSWORD(self, PASSWORD: str) -> bool:
|
||||||
"""验证管理密钥"""
|
"""
|
||||||
|
验证管理密钥
|
||||||
|
|
||||||
|
:param PASSWORD: 管理密钥
|
||||||
|
:type PASSWORD: str
|
||||||
|
:return: 是否验证通过
|
||||||
|
:rtype: bool
|
||||||
|
"""
|
||||||
|
|
||||||
return bool(
|
return bool(
|
||||||
self.AUTO_decryptor(self.AUTO_encryptor("-"), PASSWORD) != "管理密钥错误"
|
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}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,27 +16,28 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA系统服务
|
AUTO_MAA系统服务
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
from PySide6.QtWidgets import QApplication
|
||||||
from PySide6.QtWidgets import QWidget
|
|
||||||
import sys
|
import sys
|
||||||
import ctypes
|
import ctypes
|
||||||
import win32gui
|
import win32gui
|
||||||
import win32process
|
import win32process
|
||||||
import winreg
|
|
||||||
import psutil
|
import psutil
|
||||||
import subprocess
|
import subprocess
|
||||||
|
import tempfile
|
||||||
|
import getpass
|
||||||
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.core import Config
|
from app.core import Config, logger
|
||||||
|
|
||||||
|
|
||||||
class _SystemHandler:
|
class _SystemHandler:
|
||||||
@@ -44,9 +45,7 @@ class _SystemHandler:
|
|||||||
ES_CONTINUOUS = 0x80000000
|
ES_CONTINUOUS = 0x80000000
|
||||||
ES_SYSTEM_REQUIRED = 0x00000001
|
ES_SYSTEM_REQUIRED = 0x00000001
|
||||||
|
|
||||||
def __init__(self, main_window: QWidget = None):
|
def __init__(self):
|
||||||
|
|
||||||
self.main_window = main_window
|
|
||||||
|
|
||||||
self.set_Sleep()
|
self.set_Sleep()
|
||||||
self.set_SelfStart()
|
self.set_SelfStart()
|
||||||
@@ -67,98 +66,237 @@ class _SystemHandler:
|
|||||||
"""同步开机自启"""
|
"""同步开机自启"""
|
||||||
|
|
||||||
if Config.get(Config.start_IfSelfStart) and not self.is_startup():
|
if Config.get(Config.start_IfSelfStart) and not self.is_startup():
|
||||||
key = winreg.OpenKey(
|
|
||||||
winreg.HKEY_CURRENT_USER,
|
# 创建任务计划
|
||||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
try:
|
||||||
winreg.KEY_SET_VALUE,
|
|
||||||
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
|
# 获取当前用户和时间
|
||||||
)
|
current_user = getpass.getuser()
|
||||||
winreg.SetValueEx(key, "AUTO_MAA", 0, winreg.REG_SZ, Config.app_path_sys)
|
current_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S")
|
||||||
winreg.CloseKey(key)
|
|
||||||
|
# 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():
|
elif not Config.get(Config.start_IfSelfStart) and self.is_startup():
|
||||||
key = winreg.OpenKey(
|
|
||||||
winreg.HKEY_CURRENT_USER,
|
try:
|
||||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
||||||
winreg.KEY_SET_VALUE,
|
result = subprocess.run(
|
||||||
winreg.KEY_ALL_ACCESS | winreg.KEY_WRITE | winreg.KEY_CREATE_SUB_KEY,
|
["schtasks", "/delete", "/tn", "AUTO_MAA_AutoStart", "/f"],
|
||||||
)
|
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||||
winreg.DeleteValue(key, "AUTO_MAA")
|
stdin=subprocess.DEVNULL,
|
||||||
winreg.CloseKey(key)
|
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:
|
def set_power(self, mode) -> None:
|
||||||
|
"""
|
||||||
|
执行系统电源操作
|
||||||
|
|
||||||
|
:param mode: 电源操作模式,支持 "NoAction", "Shutdown", "Hibernate", "Sleep", "KillSelf", "ShutdownForce"
|
||||||
|
"""
|
||||||
|
|
||||||
if sys.platform.startswith("win"):
|
if sys.platform.startswith("win"):
|
||||||
|
|
||||||
if mode == "None":
|
if mode == "NoAction":
|
||||||
|
|
||||||
logger.info("不执行系统电源操作")
|
logger.info("不执行系统电源操作", module="系统服务")
|
||||||
|
|
||||||
elif mode == "Shutdown":
|
elif mode == "Shutdown":
|
||||||
|
|
||||||
logger.info("执行关机操作")
|
self.kill_emulator_processes()
|
||||||
|
logger.info("执行关机操作", module="系统服务")
|
||||||
subprocess.run(["shutdown", "/s", "/t", "0"])
|
subprocess.run(["shutdown", "/s", "/t", "0"])
|
||||||
|
|
||||||
|
elif mode == "ShutdownForce":
|
||||||
|
logger.info("执行强制关机操作", module="系统服务")
|
||||||
|
subprocess.run(["shutdown", "/s", "/t", "0", "/f"])
|
||||||
|
|
||||||
elif mode == "Hibernate":
|
elif mode == "Hibernate":
|
||||||
|
|
||||||
logger.info("执行休眠操作")
|
logger.info("执行休眠操作", module="系统服务")
|
||||||
subprocess.run(["shutdown", "/h"])
|
subprocess.run(["shutdown", "/h"])
|
||||||
|
|
||||||
elif mode == "Sleep":
|
elif mode == "Sleep":
|
||||||
|
|
||||||
logger.info("执行睡眠操作")
|
logger.info("执行睡眠操作", module="系统服务")
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
|
["rundll32.exe", "powrprof.dll,SetSuspendState", "0,1,0"]
|
||||||
)
|
)
|
||||||
|
|
||||||
elif mode == "KillSelf":
|
elif mode == "KillSelf":
|
||||||
|
|
||||||
self.main_window.close()
|
logger.info("执行退出主程序操作", module="系统服务")
|
||||||
|
Config.main_window.close()
|
||||||
|
QApplication.quit()
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
elif sys.platform.startswith("linux"):
|
elif sys.platform.startswith("linux"):
|
||||||
|
|
||||||
if mode == "None":
|
if mode == "NoAction":
|
||||||
|
|
||||||
logger.info("不执行系统电源操作")
|
logger.info("不执行系统电源操作", module="系统服务")
|
||||||
|
|
||||||
elif mode == "Shutdown":
|
elif mode == "Shutdown":
|
||||||
|
|
||||||
logger.info("执行关机操作")
|
logger.info("执行关机操作", module="系统服务")
|
||||||
subprocess.run(["shutdown", "-h", "now"])
|
subprocess.run(["shutdown", "-h", "now"])
|
||||||
|
|
||||||
elif mode == "Hibernate":
|
elif mode == "Hibernate":
|
||||||
|
|
||||||
logger.info("执行休眠操作")
|
logger.info("执行休眠操作", module="系统服务")
|
||||||
subprocess.run(["systemctl", "hibernate"])
|
subprocess.run(["systemctl", "hibernate"])
|
||||||
|
|
||||||
elif mode == "Sleep":
|
elif mode == "Sleep":
|
||||||
|
|
||||||
logger.info("执行睡眠操作")
|
logger.info("执行睡眠操作", module="系统服务")
|
||||||
subprocess.run(["systemctl", "suspend"])
|
subprocess.run(["systemctl", "suspend"])
|
||||||
|
|
||||||
elif mode == "KillSelf":
|
elif mode == "KillSelf":
|
||||||
|
|
||||||
self.main_window.close()
|
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:
|
def is_startup(self) -> bool:
|
||||||
"""判断程序是否已经开机自启"""
|
"""判断程序是否已经开机自启"""
|
||||||
|
|
||||||
key = winreg.OpenKey(
|
|
||||||
winreg.HKEY_CURRENT_USER,
|
|
||||||
r"Software\Microsoft\Windows\CurrentVersion\Run",
|
|
||||||
0,
|
|
||||||
winreg.KEY_READ,
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
value, _ = winreg.QueryValueEx(key, "AUTO_MAA")
|
result = subprocess.run(
|
||||||
winreg.CloseKey(key)
|
["schtasks", "/query", "/tn", "AUTO_MAA_AutoStart"],
|
||||||
return True
|
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||||
except FileNotFoundError:
|
stdin=subprocess.DEVNULL,
|
||||||
winreg.CloseKey(key)
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
)
|
||||||
|
return result.returncode == 0
|
||||||
|
except Exception as e:
|
||||||
|
logger.exception(f"检查任务计划程序失败: {e}", module="系统服务")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def get_window_info(self) -> list:
|
def get_window_info(self) -> list:
|
||||||
"""获取当前窗口信息"""
|
"""获取当前前台窗口信息"""
|
||||||
|
|
||||||
def callback(hwnd, window_info):
|
def callback(hwnd, window_info):
|
||||||
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
|
if win32gui.IsWindowVisible(hwnd) and win32gui.GetWindowText(hwnd):
|
||||||
@@ -172,7 +310,13 @@ class _SystemHandler:
|
|||||||
return window_info
|
return window_info
|
||||||
|
|
||||||
def kill_process(self, path: Path) -> None:
|
def kill_process(self, path: Path) -> None:
|
||||||
"""根据路径中止进程"""
|
"""
|
||||||
|
根据路径中止进程
|
||||||
|
|
||||||
|
:param path: 进程路径
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"开始中止进程: {path}", module="系统服务")
|
||||||
|
|
||||||
for pid in self.search_pids(path):
|
for pid in self.search_pids(path):
|
||||||
killprocess = subprocess.Popen(
|
killprocess = subprocess.Popen(
|
||||||
@@ -182,8 +326,17 @@ class _SystemHandler:
|
|||||||
)
|
)
|
||||||
killprocess.wait()
|
killprocess.wait()
|
||||||
|
|
||||||
|
logger.success(f"进程已中止: {path}", module="系统服务")
|
||||||
|
|
||||||
def search_pids(self, path: Path) -> list:
|
def search_pids(self, path: Path) -> list:
|
||||||
"""根据路径查找进程PID"""
|
"""
|
||||||
|
根据路径查找进程PID
|
||||||
|
|
||||||
|
:param path: 进程路径
|
||||||
|
:return: 匹配的进程PID列表
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"开始查找进程 PID: {path}", module="系统服务")
|
||||||
|
|
||||||
pids = []
|
pids = []
|
||||||
for proc in psutil.process_iter(["pid", "exe"]):
|
for proc in psutil.process_iter(["pid", "exe"]):
|
||||||
|
|||||||
1182
app/ui/Widget.py
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA图形化界面包
|
AUTO_MAA图形化界面包
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,16 +16,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA调度中枢界面
|
AUTO_MAA调度中枢界面
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -33,8 +32,8 @@ from PySide6.QtWidgets import (
|
|||||||
QHBoxLayout,
|
QHBoxLayout,
|
||||||
)
|
)
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
|
BodyLabel,
|
||||||
CardWidget,
|
CardWidget,
|
||||||
Pivot,
|
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
HeaderCardWidget,
|
HeaderCardWidget,
|
||||||
@@ -44,13 +43,12 @@ from qfluentwidgets import (
|
|||||||
SubtitleLabel,
|
SubtitleLabel,
|
||||||
PushButton,
|
PushButton,
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from PySide6.QtGui import QTextCursor
|
from PySide6.QtGui import QTextCursor
|
||||||
from typing import List, Dict
|
from typing import List, Dict
|
||||||
|
|
||||||
|
|
||||||
from app.core import Config, TaskManager, Task, MainInfoBar
|
from app.core import Config, TaskManager, Task, MainInfoBar, logger
|
||||||
from .Widget import StatefulItemCard
|
from .Widget import StatefulItemCard, ComboBoxMessageBox, PivotArea
|
||||||
|
|
||||||
|
|
||||||
class DispatchCenter(QWidget):
|
class DispatchCenter(QWidget):
|
||||||
@@ -60,13 +58,35 @@ class DispatchCenter(QWidget):
|
|||||||
|
|
||||||
self.setObjectName("调度中枢")
|
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.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.script_list["主调度台"] = dispatch_box
|
||||||
self.stackedWidget.addWidget(self.script_list["主调度台"])
|
self.stackedWidget.addWidget(self.script_list["主调度台"])
|
||||||
self.pivot.addItem(
|
self.pivot.addItem(
|
||||||
@@ -76,7 +96,16 @@ class DispatchCenter(QWidget):
|
|||||||
icon=FluentIcon.CAFE,
|
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.addWidget(self.stackedWidget)
|
||||||
self.Layout.setContentsMargins(0, 0, 0, 0)
|
self.Layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
@@ -85,11 +114,17 @@ class DispatchCenter(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def add_board(self, task: Task) -> None:
|
def add_board(self, task: Task) -> None:
|
||||||
"""添加一个调度台界面"""
|
"""
|
||||||
|
为任务添加一个调度台界面并绑定信号
|
||||||
|
|
||||||
dispatch_box = DispatchBox(task.name, self)
|
:param task: 任务对象
|
||||||
|
"""
|
||||||
|
|
||||||
dispatch_box.top_bar.button.clicked.connect(
|
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)
|
lambda: TaskManager.stop_task(task.name)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -106,26 +141,43 @@ class DispatchCenter(QWidget):
|
|||||||
|
|
||||||
self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}")
|
self.pivot.addItem(routeKey=f"调度台_{task.name}", text=f"调度台 {task.name}")
|
||||||
|
|
||||||
|
logger.success(f"调度台 {task.name} 添加成功", module="调度中枢")
|
||||||
|
|
||||||
def del_board(self, name: str) -> None:
|
def del_board(self, name: str) -> None:
|
||||||
"""删除指定子界面"""
|
"""
|
||||||
|
删除指定子界面
|
||||||
|
|
||||||
|
:param name: 子界面名称
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"删除调度台:{name}", module="调度中枢")
|
||||||
|
|
||||||
self.pivot.setCurrentItem("主调度台")
|
self.pivot.setCurrentItem("主调度台")
|
||||||
self.stackedWidget.removeWidget(self.script_list[name])
|
self.stackedWidget.removeWidget(self.script_list[name])
|
||||||
self.script_list[name].deleteLater()
|
self.script_list[name].deleteLater()
|
||||||
|
self.script_list.pop(name)
|
||||||
self.pivot.removeWidget(name)
|
self.pivot.removeWidget(name)
|
||||||
|
|
||||||
|
logger.success(f"调度台 {name} 删除成功", module="调度中枢")
|
||||||
|
|
||||||
def connect_main_board(self, task: Task) -> None:
|
def connect_main_board(self, task: Task) -> None:
|
||||||
"""连接主调度台"""
|
"""
|
||||||
|
将任务连接到主调度台
|
||||||
|
|
||||||
|
:param task: 任务对象
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"主调度台载入任务:{task.name}", module="调度中枢")
|
||||||
|
|
||||||
self.script_list["主调度台"].top_bar.Lable.setText(
|
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.Lable.show()
|
||||||
self.script_list["主调度台"].top_bar.object.hide()
|
self.script_list["主调度台"].top_bar.object.hide()
|
||||||
self.script_list["主调度台"].top_bar.mode.hide()
|
self.script_list["主调度台"].top_bar.mode.hide()
|
||||||
self.script_list["主调度台"].top_bar.button.clicked.disconnect()
|
self.script_list["主调度台"].top_bar.main_button.clicked.disconnect()
|
||||||
self.script_list["主调度台"].top_bar.button.setText("中止任务")
|
self.script_list["主调度台"].top_bar.main_button.setText("中止任务")
|
||||||
self.script_list["主调度台"].top_bar.button.clicked.connect(
|
self.script_list["主调度台"].top_bar.main_button.clicked.connect(
|
||||||
lambda: TaskManager.stop_task(task.name)
|
lambda: TaskManager.stop_task(task.name)
|
||||||
)
|
)
|
||||||
task.create_task_list.connect(
|
task.create_task_list.connect(
|
||||||
@@ -143,22 +195,41 @@ class DispatchCenter(QWidget):
|
|||||||
task.update_log_text.connect(
|
task.update_log_text.connect(
|
||||||
self.script_list["主调度台"].info.log_text.text.setText
|
self.script_list["主调度台"].info.log_text.text.setText
|
||||||
)
|
)
|
||||||
task.accomplish.connect(lambda: self.disconnect_main_board(task.name))
|
task.accomplish.connect(
|
||||||
|
lambda logs: self.disconnect_main_board(task.name, logs)
|
||||||
|
)
|
||||||
|
|
||||||
def disconnect_main_board(self, name: str) -> None:
|
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.Lable.hide()
|
||||||
self.script_list["主调度台"].top_bar.object.show()
|
self.script_list["主调度台"].top_bar.object.show()
|
||||||
self.script_list["主调度台"].top_bar.mode.show()
|
self.script_list["主调度台"].top_bar.mode.show()
|
||||||
self.script_list["主调度台"].top_bar.button.clicked.disconnect()
|
self.script_list["主调度台"].top_bar.main_button.clicked.disconnect()
|
||||||
self.script_list["主调度台"].top_bar.button.setText("开始任务")
|
self.script_list["主调度台"].top_bar.main_button.setText("开始任务")
|
||||||
self.script_list["主调度台"].top_bar.button.clicked.connect(
|
self.script_list["主调度台"].top_bar.main_button.clicked.connect(
|
||||||
self.script_list["主调度台"].top_bar.start_task
|
self.script_list["主调度台"].top_bar.start_main_task
|
||||||
)
|
|
||||||
self.script_list["主调度台"].info.log_text.text.setText(
|
|
||||||
Config.get_history(name)["History"]
|
|
||||||
)
|
)
|
||||||
|
if len(logs) > 0:
|
||||||
|
history = ""
|
||||||
|
for log in logs:
|
||||||
|
history += (
|
||||||
|
f"任务名称:{log[0]},{log[1]["History"].replace("\n","\n ")}\n"
|
||||||
|
)
|
||||||
|
self.script_list["主调度台"].info.log_text.text.setText(history)
|
||||||
|
else:
|
||||||
|
self.script_list["主调度台"].info.log_text.text.setText("没有任务被执行")
|
||||||
|
|
||||||
|
logger.success(f"主调度台成功断开:{name}", module="调度中枢")
|
||||||
|
|
||||||
def update_top_bar(self):
|
def update_top_bar(self):
|
||||||
"""更新顶栏"""
|
"""更新顶栏"""
|
||||||
@@ -169,25 +240,25 @@ class DispatchCenter(QWidget):
|
|||||||
self.script_list["主调度台"].top_bar.object.addItem(
|
self.script_list["主调度台"].top_bar.object.addItem(
|
||||||
(
|
(
|
||||||
"队列"
|
"队列"
|
||||||
if info["Config"].get(info["Config"].queueSet_Name) == ""
|
if info["Config"].get(info["Config"].QueueSet_Name) == ""
|
||||||
else f"队列 - {info["Config"].get(info["Config"].queueSet_Name)}"
|
else f"队列 - {info["Config"].get(info["Config"].QueueSet_Name)}"
|
||||||
),
|
),
|
||||||
userData=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(
|
self.script_list["主调度台"].top_bar.object.addItem(
|
||||||
(
|
(
|
||||||
f"实例 - {info['Type']}"
|
f"实例 - {info['Type']}"
|
||||||
if info["Config"].get(info["Config"].MaaSet_Name) == ""
|
if info["Config"].get_name() == ""
|
||||||
else f"实例 - {info['Type']} - {info["Config"].get(info["Config"].MaaSet_Name)}"
|
else f"实例 - {info['Type']} - {info['Config'].get_name()}"
|
||||||
),
|
),
|
||||||
userData=name,
|
userData=name,
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(Config.queue_dict) == 1:
|
if len(Config.queue_dict) == 1:
|
||||||
self.script_list["主调度台"].top_bar.object.setCurrentIndex(0)
|
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(
|
self.script_list["主调度台"].top_bar.object.setCurrentIndex(
|
||||||
len(Config.queue_dict)
|
len(Config.queue_dict)
|
||||||
)
|
)
|
||||||
@@ -198,229 +269,357 @@ class DispatchCenter(QWidget):
|
|||||||
self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"])
|
self.script_list["主调度台"].top_bar.mode.addItems(["自动代理", "人工排查"])
|
||||||
self.script_list["主调度台"].top_bar.mode.setCurrentIndex(0)
|
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):
|
def set_power_sign(self) -> None:
|
||||||
super().__init__(parent)
|
"""设置所有任务完成后动作"""
|
||||||
|
|
||||||
self.setObjectName(name)
|
if not Config.running_list:
|
||||||
|
|
||||||
layout = QVBoxLayout()
|
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
|
||||||
|
)
|
||||||
|
|
||||||
scrollArea = ScrollArea()
|
else:
|
||||||
scrollArea.setWidgetResizable(True)
|
|
||||||
|
|
||||||
content_widget = QWidget()
|
Config.set_power_sign(self.power_combox.currentData())
|
||||||
content_layout = QVBoxLayout(content_widget)
|
|
||||||
|
|
||||||
self.top_bar = self.DispatchTopBar(self, name)
|
def start_multi_task(self) -> None:
|
||||||
self.info = self.DispatchInfoCard(self)
|
"""开始多开任务"""
|
||||||
|
|
||||||
content_layout.addWidget(self.top_bar)
|
# 获取所有可用的队列和实例
|
||||||
content_layout.addWidget(self.info)
|
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)
|
||||||
|
|
||||||
scrollArea.setWidget(content_widget)
|
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)
|
||||||
|
|
||||||
layout.addWidget(scrollArea)
|
choice = ComboBoxMessageBox(
|
||||||
|
self.window(),
|
||||||
|
"选择一个对象以添加相应多开任务",
|
||||||
|
["选择调度对象"],
|
||||||
|
[text_list],
|
||||||
|
[data_list],
|
||||||
|
)
|
||||||
|
|
||||||
self.setLayout(layout)
|
if choice.exec() and choice.input[0].currentIndex() != -1:
|
||||||
|
|
||||||
class DispatchTopBar(CardWidget):
|
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
|
||||||
|
|
||||||
def __init__(self, parent=None, name: str = 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)
|
super().__init__(parent)
|
||||||
|
|
||||||
Layout = QHBoxLayout(self)
|
self.setObjectName(name)
|
||||||
|
|
||||||
if name == "主调度台":
|
self.top_bar = self.DispatchTopBar(self, name)
|
||||||
|
self.info = self.DispatchInfoCard(self)
|
||||||
|
|
||||||
self.Lable = SubtitleLabel("", self)
|
content_widget = QWidget()
|
||||||
self.Lable.hide()
|
content_layout = QVBoxLayout(content_widget)
|
||||||
self.object = ComboBox()
|
content_layout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.object.setPlaceholderText("请选择调度对象")
|
content_layout.addWidget(self.top_bar)
|
||||||
self.mode = ComboBox()
|
content_layout.addWidget(self.info)
|
||||||
self.mode.setPlaceholderText("请选择调度模式")
|
|
||||||
|
|
||||||
self.button = PushButton("开始任务")
|
scrollArea = ScrollArea()
|
||||||
self.button.clicked.connect(self.start_task)
|
scrollArea.setWidgetResizable(True)
|
||||||
|
scrollArea.setContentsMargins(0, 0, 0, 0)
|
||||||
|
scrollArea.setStyleSheet("background: transparent; border: none;")
|
||||||
|
scrollArea.setWidget(content_widget)
|
||||||
|
|
||||||
Layout.addWidget(self.Lable)
|
layout = QVBoxLayout(self)
|
||||||
Layout.addWidget(self.object)
|
layout.addWidget(scrollArea)
|
||||||
Layout.addWidget(self.mode)
|
|
||||||
Layout.addStretch(1)
|
|
||||||
Layout.addWidget(self.button)
|
|
||||||
|
|
||||||
else:
|
class DispatchTopBar(CardWidget):
|
||||||
|
|
||||||
self.Lable = SubtitleLabel(name, self)
|
def __init__(self, parent=None, name: str = None):
|
||||||
self.button = PushButton("中止任务")
|
super().__init__(parent)
|
||||||
|
|
||||||
Layout.addWidget(self.Lable)
|
Layout = QHBoxLayout(self)
|
||||||
Layout.addStretch(1)
|
|
||||||
Layout.addWidget(self.button)
|
|
||||||
|
|
||||||
def start_task(self):
|
if name == "主调度台":
|
||||||
"""开始任务"""
|
|
||||||
|
|
||||||
if self.object.currentIndex() == -1:
|
self.Lable = SubtitleLabel("", self)
|
||||||
logger.warning("未选择调度对象")
|
self.Lable.hide()
|
||||||
MainInfoBar.push_info_bar(
|
self.object = ComboBox()
|
||||||
"warning", "未选择调度对象", "请选择后再开始任务", 5000
|
self.object.setPlaceholderText("请选择调度对象")
|
||||||
)
|
self.mode = ComboBox()
|
||||||
return None
|
self.mode.setPlaceholderText("请选择调度模式")
|
||||||
|
|
||||||
if self.mode.currentIndex() == -1:
|
self.main_button = PushButton("开始任务")
|
||||||
logger.warning("未选择调度模式")
|
self.main_button.clicked.connect(self.start_main_task)
|
||||||
MainInfoBar.push_info_bar(
|
|
||||||
"warning", "未选择调度模式", "请选择后再开始任务", 5000
|
|
||||||
)
|
|
||||||
return None
|
|
||||||
|
|
||||||
if self.object.currentData() in Config.running_list:
|
Layout.addWidget(self.Lable)
|
||||||
logger.warning(f"任务已存在:{self.object.currentData()}")
|
Layout.addWidget(self.object)
|
||||||
MainInfoBar.push_info_bar(
|
Layout.addWidget(self.mode)
|
||||||
"warning", "任务已存在", self.object.currentData(), 5000
|
Layout.addStretch(1)
|
||||||
)
|
Layout.addWidget(self.main_button)
|
||||||
return None
|
|
||||||
|
|
||||||
if "调度队列" in self.object.currentData():
|
else:
|
||||||
|
|
||||||
logger.info(f"用户添加任务:{self.object.currentData()}")
|
self.Lable = SubtitleLabel(name, self)
|
||||||
TaskManager.add_task(
|
self.main_button = PushButton("中止任务")
|
||||||
f"{self.mode.currentText()}_主调度台",
|
|
||||||
self.object.currentData(),
|
|
||||||
Config.queue_dict[self.object.currentData()]["Config"].toDict(),
|
|
||||||
)
|
|
||||||
|
|
||||||
elif "脚本" in self.object.currentData():
|
Layout.addWidget(self.Lable)
|
||||||
|
Layout.addStretch(1)
|
||||||
|
Layout.addWidget(self.main_button)
|
||||||
|
|
||||||
if Config.member_dict[self.object.currentData()]["Type"] == "Maa":
|
def start_main_task(self):
|
||||||
|
"""从主调度台开始任务"""
|
||||||
|
|
||||||
logger.info(f"用户添加任务:{self.object.currentData()}")
|
if self.object.currentIndex() == -1:
|
||||||
|
logger.warning("未选择调度对象", module="调度中枢")
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"warning", "未选择调度对象", "请选择后再开始任务", 5000
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.mode.currentIndex() == -1:
|
||||||
|
logger.warning("未选择调度模式", module="调度中枢")
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"warning", "未选择调度模式", "请选择后再开始任务", 5000
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if self.object.currentData() in Config.running_list:
|
||||||
|
logger.warning(
|
||||||
|
f"任务已存在:{self.object.currentData()}", module="调度中枢"
|
||||||
|
)
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"warning", "任务已存在", self.object.currentData(), 5000
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if (
|
||||||
|
"脚本" in self.object.currentData()
|
||||||
|
and Config.script_dict[self.object.currentData()]["Type"]
|
||||||
|
== "General"
|
||||||
|
and self.mode.currentData() == "人工排查"
|
||||||
|
):
|
||||||
|
logger.warning("通用脚本类型不存在人工排查功能", module="调度中枢")
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"warning", "不支持的任务", "通用脚本无人工排查功能", 5000
|
||||||
|
)
|
||||||
|
return None
|
||||||
|
|
||||||
|
if "调度队列" in self.object.currentData():
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"用户添加任务:{self.object.currentData()}", module="调度中枢"
|
||||||
|
)
|
||||||
|
TaskManager.add_task(
|
||||||
|
f"{self.mode.currentText()}_主调度台",
|
||||||
|
self.object.currentData(),
|
||||||
|
Config.queue_dict[self.object.currentData()]["Config"].toDict(),
|
||||||
|
)
|
||||||
|
|
||||||
|
elif "脚本" in self.object.currentData():
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"用户添加任务:{self.object.currentData()}", module="调度中枢"
|
||||||
|
)
|
||||||
TaskManager.add_task(
|
TaskManager.add_task(
|
||||||
f"{self.mode.currentText()}_主调度台",
|
f"{self.mode.currentText()}_主调度台",
|
||||||
"自定义队列",
|
"自定义队列",
|
||||||
{"Queue": {"Member_1": self.object.currentData()}},
|
{"Queue": {"Script_0": self.object.currentData()}},
|
||||||
)
|
)
|
||||||
|
|
||||||
class DispatchInfoCard(HeaderCardWidget):
|
class DispatchInfoCard(HeaderCardWidget):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
|
|
||||||
self.setTitle("调度信息")
|
|
||||||
|
|
||||||
self.task = self.TaskInfoCard(self)
|
|
||||||
self.user = self.UserInfoCard(self)
|
|
||||||
self.log_text = self.LogCard(self)
|
|
||||||
|
|
||||||
self.viewLayout.addWidget(self.task)
|
|
||||||
self.viewLayout.addWidget(self.user)
|
|
||||||
self.viewLayout.addWidget(self.log_text)
|
|
||||||
|
|
||||||
self.viewLayout.setStretch(0, 1)
|
|
||||||
self.viewLayout.setStretch(1, 1)
|
|
||||||
self.viewLayout.setStretch(2, 5)
|
|
||||||
|
|
||||||
def update_board(self, task_list: list, user_list: list, log: str):
|
|
||||||
"""更新调度信息"""
|
|
||||||
|
|
||||||
self.task.update_task(task_list)
|
|
||||||
self.user.update_user(user_list)
|
|
||||||
self.log_text.text.setText(log)
|
|
||||||
|
|
||||||
class TaskInfoCard(HeaderCardWidget):
|
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setTitle("任务队列")
|
|
||||||
|
|
||||||
self.Layout = QVBoxLayout()
|
self.setTitle("调度信息")
|
||||||
self.viewLayout.addLayout(self.Layout)
|
|
||||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
|
||||||
|
|
||||||
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:
|
self.viewLayout.setStretch(0, 1)
|
||||||
item = self.Layout.takeAt(0)
|
self.viewLayout.setStretch(1, 1)
|
||||||
if item.spacerItem():
|
self.viewLayout.setStretch(2, 5)
|
||||||
self.Layout.removeItem(item.spacerItem())
|
|
||||||
elif item.widget():
|
|
||||||
item.widget().deleteLater()
|
|
||||||
|
|
||||||
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))
|
class TaskInfoCard(HeaderCardWidget):
|
||||||
self.Layout.addWidget(self.task_cards[-1])
|
|
||||||
|
|
||||||
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):
|
while self.Layout.count() > 0:
|
||||||
super().__init__(parent)
|
item = self.Layout.takeAt(0)
|
||||||
self.setTitle("用户队列")
|
if item.spacerItem():
|
||||||
|
self.Layout.removeItem(item.spacerItem())
|
||||||
|
elif item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
self.Layout = QVBoxLayout()
|
self.task_cards = []
|
||||||
self.viewLayout.addLayout(self.Layout)
|
|
||||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
|
||||||
|
|
||||||
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:
|
self.Layout.addStretch(1)
|
||||||
item = self.Layout.takeAt(0)
|
|
||||||
if item.spacerItem():
|
|
||||||
self.Layout.removeItem(item.spacerItem())
|
|
||||||
elif item.widget():
|
|
||||||
item.widget().deleteLater()
|
|
||||||
|
|
||||||
self.user_cards = []
|
def update_task(self, task_list: list):
|
||||||
|
"""
|
||||||
|
更新任务队列信息
|
||||||
|
|
||||||
for user in user_list:
|
:param task_list: 包含任务信息的任务列表
|
||||||
|
"""
|
||||||
|
|
||||||
self.user_cards.append(StatefulItemCard(user))
|
for i in range(len(task_list)):
|
||||||
self.Layout.addWidget(self.user_cards[-1])
|
|
||||||
|
|
||||||
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.Layout = QVBoxLayout()
|
||||||
self.user_cards[i].update_status(user_list[i][1])
|
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):
|
def create_user(self, user_list: list):
|
||||||
super().__init__(parent)
|
"""
|
||||||
self.setTitle("日志")
|
创建用户队列
|
||||||
|
|
||||||
self.text = TextBrowser()
|
:param user_list: 包含用户信息的用户列表
|
||||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
"""
|
||||||
self.viewLayout.addWidget(self.text)
|
|
||||||
|
|
||||||
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)
|
for user in user_list:
|
||||||
self.text.ensureCursorVisible()
|
|
||||||
|
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()
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,27 +16,23 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA更新器
|
AUTO_MAA更新器
|
||||||
v1.2
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import sys
|
|
||||||
import json
|
|
||||||
import zipfile
|
import zipfile
|
||||||
import requests
|
import requests
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
import win32crypt
|
|
||||||
import base64
|
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PySide6.QtWidgets import QApplication, QDialog, QVBoxLayout
|
from PySide6.QtWidgets import QDialog, QVBoxLayout
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
ProgressBar,
|
ProgressBar,
|
||||||
IndeterminateProgressBar,
|
IndeterminateProgressBar,
|
||||||
@@ -44,11 +40,14 @@ from qfluentwidgets import (
|
|||||||
setTheme,
|
setTheme,
|
||||||
Theme,
|
Theme,
|
||||||
)
|
)
|
||||||
from PySide6.QtGui import QIcon, QCloseEvent
|
from PySide6.QtGui import QCloseEvent
|
||||||
from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop
|
from PySide6.QtCore import QThread, Signal, QTimer, QEventLoop
|
||||||
|
|
||||||
from typing import List, Dict, Union
|
from typing import List, Dict, Union
|
||||||
|
|
||||||
|
from app.core import Config, logger
|
||||||
|
from app.services import System
|
||||||
|
|
||||||
|
|
||||||
def version_text(version_numb: list) -> str:
|
def version_text(version_numb: list) -> str:
|
||||||
"""将版本号列表转为可读的文本信息"""
|
"""将版本号列表转为可读的文本信息"""
|
||||||
@@ -81,19 +80,33 @@ class DownloadProcess(QThread):
|
|||||||
) -> None:
|
) -> None:
|
||||||
super(DownloadProcess, self).__init__()
|
super(DownloadProcess, self).__init__()
|
||||||
|
|
||||||
|
self.setObjectName(f"DownloadProcess-{url}-{start_byte}-{end_byte}")
|
||||||
|
|
||||||
|
logger.info(f"创建下载子线程:{self.objectName()}", module="下载子线程")
|
||||||
|
|
||||||
self.url = url
|
self.url = url
|
||||||
self.start_byte = start_byte
|
self.start_byte = start_byte
|
||||||
self.end_byte = end_byte
|
self.end_byte = end_byte
|
||||||
self.download_path = download_path
|
self.download_path = download_path
|
||||||
self.check_times = check_times
|
self.check_times = check_times
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
|
||||||
# 清理可能存在的临时文件
|
# 清理可能存在的临时文件
|
||||||
if self.download_path.exists():
|
if self.download_path.exists():
|
||||||
self.download_path.unlink()
|
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:
|
while not self.isInterruptionRequested() and self.check_times != 0:
|
||||||
|
|
||||||
@@ -102,17 +115,34 @@ class DownloadProcess(QThread):
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
response = requests.get(
|
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:
|
if self.check_times != -1:
|
||||||
self.check_times -= 1
|
self.check_times -= 1
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"连接失败:{self.url},状态码:{response.status_code},剩余重试次数:{self.check_times}",
|
||||||
|
module="下载子线程",
|
||||||
|
)
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"连接成功:{self.url},状态码:{response.status_code}",
|
||||||
|
module="下载子线程",
|
||||||
|
)
|
||||||
|
|
||||||
downloaded_size = 0
|
downloaded_size = 0
|
||||||
with self.download_path.open(mode="wb") as f:
|
with self.download_path.open(mode="wb") as f:
|
||||||
|
|
||||||
@@ -131,10 +161,15 @@ class DownloadProcess(QThread):
|
|||||||
if self.download_path.exists():
|
if self.download_path.exists():
|
||||||
self.download_path.unlink()
|
self.download_path.unlink()
|
||||||
self.accomplish.emit(0)
|
self.accomplish.emit(0)
|
||||||
|
logger.info(f"下载中止:{self.url}", module="下载子线程")
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
self.accomplish.emit(time.time() - start_time)
|
self.accomplish.emit(time.time() - start_time)
|
||||||
|
logger.success(
|
||||||
|
f"下载完成:{self.url},实际下载大小:{downloaded_size} 字节,耗时:{time.time() - start_time:.2f} 秒",
|
||||||
|
module="下载子线程",
|
||||||
|
)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
@@ -142,6 +177,11 @@ class DownloadProcess(QThread):
|
|||||||
|
|
||||||
if self.check_times != -1:
|
if self.check_times != -1:
|
||||||
self.check_times -= 1
|
self.check_times -= 1
|
||||||
|
|
||||||
|
logger.exception(
|
||||||
|
f"下载出错:{self.url},错误信息:{e},剩余重试次数:{self.check_times}",
|
||||||
|
module="下载子线程",
|
||||||
|
)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -149,6 +189,7 @@ class DownloadProcess(QThread):
|
|||||||
if self.download_path.exists():
|
if self.download_path.exists():
|
||||||
self.download_path.unlink()
|
self.download_path.unlink()
|
||||||
self.accomplish.emit(0)
|
self.accomplish.emit(0)
|
||||||
|
logger.error(f"下载失败:{self.url}", module="下载子线程")
|
||||||
|
|
||||||
|
|
||||||
class ZipExtractProcess(QThread):
|
class ZipExtractProcess(QThread):
|
||||||
@@ -160,14 +201,24 @@ class ZipExtractProcess(QThread):
|
|||||||
def __init__(self, name: str, app_path: Path, download_path: Path) -> None:
|
def __init__(self, name: str, app_path: Path, download_path: Path) -> None:
|
||||||
super(ZipExtractProcess, self).__init__()
|
super(ZipExtractProcess, self).__init__()
|
||||||
|
|
||||||
|
self.setObjectName(f"ZipExtractProcess-{name}")
|
||||||
|
|
||||||
|
logger.info(f"创建解压子线程:{self.objectName()}", module="解压子线程")
|
||||||
|
|
||||||
self.name = name
|
self.name = name
|
||||||
self.app_path = app_path
|
self.app_path = app_path
|
||||||
self.download_path = download_path
|
self.download_path = download_path
|
||||||
|
|
||||||
|
@logger.catch
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"开始解压:{self.download_path} 到 {self.app_path}",
|
||||||
|
module="解压子线程",
|
||||||
|
)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
if self.isInterruptionRequested():
|
if self.isInterruptionRequested():
|
||||||
@@ -177,9 +228,21 @@ class ZipExtractProcess(QThread):
|
|||||||
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
|
with zipfile.ZipFile(self.download_path, "r") as zip_ref:
|
||||||
zip_ref.extractall(self.app_path)
|
zip_ref.extractall(self.app_path)
|
||||||
self.accomplish.emit()
|
self.accomplish.emit()
|
||||||
|
logger.success(
|
||||||
|
f"解压完成:{self.download_path} 到 {self.app_path}",
|
||||||
|
module="解压子线程",
|
||||||
|
)
|
||||||
break
|
break
|
||||||
except PermissionError:
|
except PermissionError:
|
||||||
self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭")
|
if self.name == "AUTO_MAA":
|
||||||
|
self.info.emit(f"解压出错:AUTO_MAA正在运行,正在尝试将其关闭")
|
||||||
|
System.kill_process(self.app_path / "AUTO_MAA.exe")
|
||||||
|
else:
|
||||||
|
self.info.emit(f"解压出错:{self.name}正在运行,正在等待其关闭")
|
||||||
|
logger.warning(
|
||||||
|
f"解压出错:{self.name}正在运行,正在等待其关闭",
|
||||||
|
module="解压子线程",
|
||||||
|
)
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -187,6 +250,7 @@ class ZipExtractProcess(QThread):
|
|||||||
e = str(e)
|
e = str(e)
|
||||||
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
|
e = "\n".join([e[_ : _ + 75] for _ in range(0, len(e), 75)])
|
||||||
self.info.emit(f"解压更新时出错:\n{e}")
|
self.info.emit(f"解压更新时出错:\n{e}")
|
||||||
|
logger.exception(f"解压更新时出错:{e}", module="解压子线程")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -199,28 +263,18 @@ class DownloadManager(QDialog):
|
|||||||
|
|
||||||
isInterruptionRequested = False
|
isInterruptionRequested = False
|
||||||
|
|
||||||
def __init__(
|
def __init__(self, app_path: Path, name: str, version: list, config: dict) -> None:
|
||||||
self,
|
|
||||||
app_path: Path,
|
|
||||||
name: str,
|
|
||||||
main_version: list,
|
|
||||||
config: dict,
|
|
||||||
) -> None:
|
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.app_path = app_path
|
self.app_path = app_path
|
||||||
self.name = name
|
self.name = name
|
||||||
self.main_version = main_version
|
self.version = version
|
||||||
self.config = config
|
self.config = config
|
||||||
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
|
self.download_path = app_path / "DOWNLOAD_TEMP.zip" # 临时下载文件的路径
|
||||||
self.version_path = app_path / "resources/version.json"
|
|
||||||
self.download_process_dict: Dict[str, DownloadProcess] = {}
|
self.download_process_dict: Dict[str, DownloadProcess] = {}
|
||||||
self.timer_dict: Dict[str, QTimer] = {}
|
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)
|
self.resize(700, 70)
|
||||||
|
|
||||||
setTheme(Theme.AUTO, lazy=True)
|
setTheme(Theme.AUTO, lazy=True)
|
||||||
@@ -242,43 +296,50 @@ class DownloadManager(QDialog):
|
|||||||
|
|
||||||
def run(self) -> None:
|
def run(self) -> None:
|
||||||
|
|
||||||
if self.name == "MAA":
|
logger.info(
|
||||||
self.download_task1()
|
f"开始执行下载任务:{self.name},版本:{version_text(self.version)}",
|
||||||
elif self.name == "AUTO_MAA":
|
module="下载管理器",
|
||||||
|
)
|
||||||
|
|
||||||
|
if self.name == "AUTO_MAA":
|
||||||
if self.config["mode"] == "Proxy":
|
if self.config["mode"] == "Proxy":
|
||||||
self.test_speed_task1()
|
self.start_test_speed()
|
||||||
self.speed_test_accomplish.connect(self.download_task1)
|
self.speed_test_accomplish.connect(self.start_download)
|
||||||
elif self.config["mode"] == "MirrorChyan":
|
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]]:
|
def get_download_url(self, mode: str) -> Union[str, Dict[str, str]]:
|
||||||
"""获取下载链接"""
|
"""
|
||||||
|
生成下载链接
|
||||||
|
|
||||||
|
:param mode: "测速" 或 "下载"
|
||||||
|
:return: 测速模式返回 url 字典,下载模式返回 url 字符串
|
||||||
|
"""
|
||||||
|
|
||||||
url_dict = {}
|
url_dict = {}
|
||||||
|
|
||||||
if mode == "测速":
|
if mode == "测速":
|
||||||
|
|
||||||
url_dict["GitHub站"] = (
|
url_dict["GitHub站"] = (
|
||||||
f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
)
|
)
|
||||||
url_dict["官方镜像站"] = (
|
url_dict["官方镜像站"] = (
|
||||||
f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
)
|
)
|
||||||
for name, download_url_head in self.config["download_dict"].items():
|
for name, download_url_head in self.config["download_dict"].items():
|
||||||
url_dict[name] = (
|
url_dict[name] = (
|
||||||
f"{download_url_head}AUTO_MAA_{version_text(self.main_version)}.zip"
|
f"{download_url_head}AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
)
|
)
|
||||||
for proxy_url in self.config["proxy_list"]:
|
for proxy_url in self.config["proxy_list"]:
|
||||||
url_dict[proxy_url] = (
|
url_dict[proxy_url] = (
|
||||||
f"{proxy_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
f"{proxy_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
)
|
)
|
||||||
return url_dict
|
return url_dict
|
||||||
|
|
||||||
elif mode == "下载":
|
elif mode == "下载":
|
||||||
|
|
||||||
if self.name == "MAA":
|
|
||||||
return f"https://jp-download.fearr.xyz/MAA/MAA-{version_text(self.main_version)}-win-x64.zip"
|
|
||||||
|
|
||||||
if self.name == "AUTO_MAA":
|
if self.name == "AUTO_MAA":
|
||||||
|
|
||||||
if self.config["mode"] == "Proxy":
|
if self.config["mode"] == "Proxy":
|
||||||
@@ -292,22 +353,46 @@ class DownloadManager(QDialog):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if selected_url == "GitHub站":
|
if selected_url == "GitHub站":
|
||||||
return f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
return f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
elif selected_url == "官方镜像站":
|
elif selected_url == "官方镜像站":
|
||||||
return f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
return f"https://gitee.com/DLmaster_361/AUTO_MAA/releases/download/{version_text(self.version)}/AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
elif selected_url in self.config["download_dict"].keys():
|
elif selected_url in self.config["download_dict"].keys():
|
||||||
return f"{self.config["download_dict"][selected_url]}AUTO_MAA_{version_text(self.main_version)}.zip"
|
return f"{self.config["download_dict"][selected_url]}AUTO_MAA_{version_text(self.version)}.zip"
|
||||||
else:
|
else:
|
||||||
return f"{selected_url}https://github.com/DLmaster361/AUTO_MAA/releases/download/{version_text(self.main_version)}/AUTO_MAA_{version_text(self.main_version)}.zip"
|
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":
|
elif self.config["mode"] == "MirrorChyan":
|
||||||
|
|
||||||
with requests.get(
|
with requests.get(
|
||||||
self.config["url"], allow_redirects=True, stream=True
|
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:
|
) as response:
|
||||||
if response.status_code == 200:
|
if response.status_code == 200:
|
||||||
return response.url
|
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:
|
if self.isInterruptionRequested:
|
||||||
return None
|
return None
|
||||||
@@ -315,6 +400,10 @@ class DownloadManager(QDialog):
|
|||||||
url_dict = self.get_download_url("测速")
|
url_dict = self.get_download_url("测速")
|
||||||
self.test_speed_result: Dict[str, float] = {}
|
self.test_speed_result: Dict[str, float] = {}
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"开始测速任务,链接:{list(url_dict.items())}", module="下载管理器"
|
||||||
|
)
|
||||||
|
|
||||||
for name, url in url_dict.items():
|
for name, url in url_dict.items():
|
||||||
|
|
||||||
if self.isInterruptionRequested:
|
if self.isInterruptionRequested:
|
||||||
@@ -330,10 +419,11 @@ class DownloadManager(QDialog):
|
|||||||
)
|
)
|
||||||
self.test_speed_result[name] = -1
|
self.test_speed_result[name] = -1
|
||||||
self.download_process_dict[name].accomplish.connect(
|
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()
|
self.download_process_dict[name].start()
|
||||||
|
|
||||||
|
# 创建防超时定时器,30秒后强制停止测速
|
||||||
timer = QTimer(self)
|
timer = QTimer(self)
|
||||||
timer.setSingleShot(True)
|
timer.setSingleShot(True)
|
||||||
timer.timeout.connect(partial(self.kill_speed_test, name))
|
timer.timeout.connect(partial(self.kill_speed_test, name))
|
||||||
@@ -344,11 +434,22 @@ class DownloadManager(QDialog):
|
|||||||
self.update_progress(0, 1, 0)
|
self.update_progress(0, 1, 0)
|
||||||
|
|
||||||
def kill_speed_test(self, name: str) -> None:
|
def kill_speed_test(self, name: str) -> None:
|
||||||
|
"""
|
||||||
|
强制停止测速任务
|
||||||
|
|
||||||
|
:param name: 测速任务的名称
|
||||||
|
"""
|
||||||
|
|
||||||
if name in self.download_process_dict:
|
if name in self.download_process_dict:
|
||||||
self.download_process_dict[name].requestInterruption()
|
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:
|
if self.isInterruptionRequested:
|
||||||
@@ -382,16 +483,30 @@ class DownloadManager(QDialog):
|
|||||||
if not self.download_process_dict:
|
if not self.download_process_dict:
|
||||||
self.download_process_clear.emit()
|
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()):
|
if any(speed == -1 for _, speed in self.test_speed_result.items()):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 保存测速结果
|
# 保存测速结果
|
||||||
self.config["speed_result"] = self.test_speed_result
|
self.config["speed_result"] = self.test_speed_result
|
||||||
|
logger.success(
|
||||||
|
f"测速完成,结果:{list(self.test_speed_result.items())}",
|
||||||
|
module="下载管理器",
|
||||||
|
)
|
||||||
|
|
||||||
self.update_info("测速完成!")
|
self.update_info("测速完成!")
|
||||||
self.speed_test_accomplish.emit()
|
self.speed_test_accomplish.emit()
|
||||||
|
|
||||||
def download_task1(self) -> None:
|
def start_download(self) -> None:
|
||||||
|
"""开始下载任务"""
|
||||||
|
|
||||||
if self.isInterruptionRequested:
|
if self.isInterruptionRequested:
|
||||||
return None
|
return None
|
||||||
@@ -399,7 +514,16 @@ class DownloadManager(QDialog):
|
|||||||
url = self.get_download_url("下载")
|
url = self.get_download_url("下载")
|
||||||
self.downloaded_size_list: List[List[int, bool]] = []
|
self.downloaded_size_list: List[List[int, bool]] = []
|
||||||
|
|
||||||
response = requests.head(url)
|
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))
|
self.file_size = int(response.headers.get("content-length", 0))
|
||||||
part_size = self.file_size // self.config["thread_numb"]
|
part_size = self.file_size // self.config["thread_numb"]
|
||||||
@@ -425,27 +549,34 @@ class DownloadManager(QDialog):
|
|||||||
# 创建下载子线程
|
# 创建下载子线程
|
||||||
self.download_process_dict[f"part{i}"] = DownloadProcess(
|
self.download_process_dict[f"part{i}"] = DownloadProcess(
|
||||||
url,
|
url,
|
||||||
start_byte,
|
-1 if self.config["mode"] == "MirrorChyan" else start_byte,
|
||||||
end_byte,
|
-1 if self.config["mode"] == "MirrorChyan" else end_byte,
|
||||||
self.download_path.with_suffix(f".part{i}"),
|
self.download_path.with_suffix(f".part{i}"),
|
||||||
1 if self.config["mode"] == "MirrorChyan" else -1,
|
1 if self.config["mode"] == "MirrorChyan" else -1,
|
||||||
)
|
)
|
||||||
self.downloaded_size_list.append([0, False])
|
self.downloaded_size_list.append([0, False])
|
||||||
self.download_process_dict[f"part{i}"].progress.connect(
|
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(
|
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()
|
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_list[index][0] = current
|
||||||
self.downloaded_size = sum([_[0] for _ in self.downloaded_size_list])
|
self.downloaded_size = sum([_[0] for _ in self.downloaded_size_list])
|
||||||
self.update_progress(0, self.file_size, self.downloaded_size)
|
self.update_progress(0, self.file_size, self.downloaded_size)
|
||||||
|
|
||||||
|
# 速度每秒更新一次
|
||||||
if time.time() - self.last_time >= 1.0:
|
if time.time() - self.last_time >= 1.0:
|
||||||
self.speed = (
|
self.speed = (
|
||||||
(self.downloaded_size - self.last_download_size)
|
(self.downloaded_size - self.last_download_size)
|
||||||
@@ -464,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",
|
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
|
self.downloaded_size_list[index][1] = True
|
||||||
@@ -485,6 +622,10 @@ class DownloadManager(QDialog):
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
# 合并下载的分段文件
|
# 合并下载的分段文件
|
||||||
|
logger.info(
|
||||||
|
f"所有分段下载完成:{self.name},开始合并分段文件到 {self.download_path}",
|
||||||
|
module="下载管理器",
|
||||||
|
)
|
||||||
with self.download_path.open(mode="wb") as outfile:
|
with self.download_path.open(mode="wb") as outfile:
|
||||||
for i in range(self.config["thread_numb"]):
|
for i in range(self.config["thread_numb"]):
|
||||||
with self.download_path.with_suffix(f".part{i}").open(
|
with self.download_path.with_suffix(f".part{i}").open(
|
||||||
@@ -493,6 +634,11 @@ class DownloadManager(QDialog):
|
|||||||
outfile.write(infile.read())
|
outfile.write(infile.read())
|
||||||
self.download_path.with_suffix(f".part{i}").unlink()
|
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_info("正在解压更新文件")
|
||||||
self.update_progress(0, 0, 0)
|
self.update_progress(0, 0, 0)
|
||||||
|
|
||||||
@@ -506,45 +652,44 @@ class DownloadManager(QDialog):
|
|||||||
self.zip_extract.start()
|
self.zip_extract.start()
|
||||||
self.zip_loop.exec()
|
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:
|
|
||||||
(self.app_path / file_path).unlink()
|
|
||||||
|
|
||||||
(self.app_path / "changes.json").unlink()
|
|
||||||
|
|
||||||
self.update_info("正在删除临时文件")
|
self.update_info("正在删除临时文件")
|
||||||
self.update_progress(0, 0, 0)
|
self.update_progress(0, 0, 0)
|
||||||
|
if (self.app_path / "changes.json").exists():
|
||||||
|
(self.app_path / "changes.json").unlink()
|
||||||
if self.download_path.exists():
|
if self.download_path.exists():
|
||||||
self.download_path.unlink()
|
self.download_path.unlink()
|
||||||
|
|
||||||
# 主程序更新完成后打开对应程序
|
# 下载完成后打开对应程序
|
||||||
if not self.isInterruptionRequested and self.name == "AUTO_MAA":
|
if not self.isInterruptionRequested and self.name == "MAA":
|
||||||
subprocess.Popen(
|
subprocess.Popen(
|
||||||
str(self.app_path / "AUTO_MAA.exe"),
|
[self.app_path / "MAA.exe"],
|
||||||
shell=True,
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
| subprocess.DETACHED_PROCESS
|
||||||
|
| subprocess.CREATE_NO_WINDOW,
|
||||||
)
|
)
|
||||||
elif not self.isInterruptionRequested and self.name == "MAA":
|
if self.name == "AUTO_MAA":
|
||||||
subprocess.Popen(
|
self.update_info(f"即将安装{self.name}")
|
||||||
str(self.app_path / "MAA.exe"),
|
else:
|
||||||
shell=True,
|
self.update_info(f"{self.name}下载成功!")
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
|
||||||
)
|
|
||||||
|
|
||||||
self.update_info(f"{self.name}更新成功!")
|
|
||||||
self.update_progress(0, 100, 100)
|
self.update_progress(0, 100, 100)
|
||||||
self.download_accomplish.emit()
|
self.download_accomplish.emit()
|
||||||
|
|
||||||
def update_info(self, text: str) -> None:
|
def update_info(self, text: str) -> None:
|
||||||
|
"""
|
||||||
|
更新信息文本
|
||||||
|
|
||||||
|
:param text: 要显示的信息文本
|
||||||
|
"""
|
||||||
self.info.setText(text)
|
self.info.setText(text)
|
||||||
|
|
||||||
def update_progress(self, begin: int, end: int, current: int) -> None:
|
def update_progress(self, begin: int, end: int, current: int) -> None:
|
||||||
|
"""
|
||||||
|
更新进度条
|
||||||
|
|
||||||
|
:param begin: 进度条起始值
|
||||||
|
:param end: 进度条结束值
|
||||||
|
:param current: 进度条当前值
|
||||||
|
"""
|
||||||
|
|
||||||
if begin == 0 and end == 0:
|
if begin == 0 and end == 0:
|
||||||
self.progress_2.setVisible(False)
|
self.progress_2.setVisible(False)
|
||||||
@@ -556,6 +701,9 @@ class DownloadManager(QDialog):
|
|||||||
self.progress_2.setValue(current)
|
self.progress_2.setValue(current)
|
||||||
|
|
||||||
def requestInterruption(self) -> None:
|
def requestInterruption(self) -> None:
|
||||||
|
"""请求中断下载任务"""
|
||||||
|
|
||||||
|
logger.info("收到下载任务中止请求", module="下载管理器")
|
||||||
|
|
||||||
self.isInterruptionRequested = True
|
self.isInterruptionRequested = True
|
||||||
|
|
||||||
@@ -579,147 +727,3 @@ class DownloadManager(QDialog):
|
|||||||
self.requestInterruption()
|
self.requestInterruption()
|
||||||
|
|
||||||
event.accept()
|
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}"
|
|
||||||
)
|
|
||||||
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"
|
|
||||||
)
|
|
||||||
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 remote_version > current_version:
|
|
||||||
app = AUTO_MAA_Downloader(
|
|
||||||
app_path,
|
|
||||||
"AUTO_MAA",
|
|
||||||
remote_version,
|
|
||||||
download_config,
|
|
||||||
)
|
|
||||||
sys.exit(app.exec())
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,16 +16,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA历史记录界面
|
AUTO_MAA历史记录界面
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -36,43 +35,67 @@ from qfluentwidgets import (
|
|||||||
FluentIcon,
|
FluentIcon,
|
||||||
HeaderCardWidget,
|
HeaderCardWidget,
|
||||||
PushButton,
|
PushButton,
|
||||||
ExpandGroupSettingCard,
|
|
||||||
TextBrowser,
|
TextBrowser,
|
||||||
|
CardWidget,
|
||||||
|
ComboBox,
|
||||||
|
ZhDatePicker,
|
||||||
|
SubtitleLabel,
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Signal
|
from PySide6.QtCore import Signal, QDate
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
from datetime import datetime, timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import List
|
from typing import List, Dict
|
||||||
|
|
||||||
|
|
||||||
from app.core import Config
|
from app.core import Config, SoundPlayer, logger
|
||||||
from .Widget import StatefulItemCard, QuantifiedItemCard
|
from .Widget import StatefulItemCard, QuantifiedItemCard, QuickExpandGroupCard
|
||||||
|
|
||||||
|
|
||||||
class History(QWidget):
|
class History(QWidget):
|
||||||
|
"""历史记录界面"""
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("历史记录")
|
self.setObjectName("历史记录")
|
||||||
|
|
||||||
|
self.history_top_bar = self.HistoryTopBar(self)
|
||||||
|
self.history_top_bar.search_history.connect(self.reload_history)
|
||||||
|
|
||||||
content_widget = QWidget()
|
content_widget = QWidget()
|
||||||
self.content_layout = QVBoxLayout(content_widget)
|
self.content_layout = QVBoxLayout(content_widget)
|
||||||
|
self.content_layout.setContentsMargins(0, 0, 11, 0)
|
||||||
|
|
||||||
scrollArea = ScrollArea()
|
scrollArea = ScrollArea()
|
||||||
scrollArea.setWidgetResizable(True)
|
scrollArea.setWidgetResizable(True)
|
||||||
|
scrollArea.setContentsMargins(0, 0, 0, 0)
|
||||||
|
scrollArea.setStyleSheet("background: transparent; border: none;")
|
||||||
scrollArea.setWidget(content_widget)
|
scrollArea.setWidget(content_widget)
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
|
layout.addWidget(self.history_top_bar)
|
||||||
layout.addWidget(scrollArea)
|
layout.addWidget(scrollArea)
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.history_card_list = []
|
self.history_card_list = []
|
||||||
|
|
||||||
self.refresh()
|
def reload_history(self, mode: str, start_date: QDate, end_date: QDate) -> None:
|
||||||
|
"""
|
||||||
|
加载历史记录界面
|
||||||
|
|
||||||
def refresh(self):
|
: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:
|
while self.content_layout.count() > 0:
|
||||||
item = self.content_layout.takeAt(0)
|
item = self.content_layout.takeAt(0)
|
||||||
if item.spacerItem():
|
if item.spacerItem():
|
||||||
@@ -82,177 +105,294 @@ class History(QWidget):
|
|||||||
|
|
||||||
self.history_card_list = []
|
self.history_card_list = []
|
||||||
|
|
||||||
history_dict = Config.search_history()
|
history_dict = Config.search_history(
|
||||||
|
mode,
|
||||||
|
datetime(start_date.year(), start_date.month(), start_date.day()),
|
||||||
|
datetime(end_date.year(), end_date.month(), end_date.day()),
|
||||||
|
)
|
||||||
|
|
||||||
for date, user_list in history_dict.items():
|
# 生成历史记录卡片并添加到布局中
|
||||||
|
for date, user_dict in history_dict.items():
|
||||||
|
|
||||||
self.history_card_list.append(HistoryCard(date, user_list, self))
|
self.history_card_list.append(self.HistoryCard(date, user_dict, self))
|
||||||
self.content_layout.addWidget(self.history_card_list[-1])
|
self.content_layout.addWidget(self.history_card_list[-1])
|
||||||
|
|
||||||
self.content_layout.addStretch(1)
|
self.content_layout.addStretch(1)
|
||||||
|
|
||||||
|
class HistoryTopBar(CardWidget):
|
||||||
|
"""历史记录顶部工具栏"""
|
||||||
|
|
||||||
class HistoryCard(ExpandGroupSettingCard):
|
search_history = Signal(str, QDate, QDate)
|
||||||
|
|
||||||
def __init__(self, date: str, user_list: List[Path], parent=None):
|
def __init__(self, parent=None):
|
||||||
super().__init__(
|
|
||||||
FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent
|
|
||||||
)
|
|
||||||
|
|
||||||
widget = QWidget()
|
|
||||||
Layout = QVBoxLayout(widget)
|
|
||||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
|
||||||
self.viewLayout.setSpacing(0)
|
|
||||||
self.addGroupWidget(widget)
|
|
||||||
|
|
||||||
self.user_history_card_list = []
|
|
||||||
|
|
||||||
for user_path in user_list:
|
|
||||||
|
|
||||||
self.user_history_card_list.append(self.UserHistoryCard(user_path, self))
|
|
||||||
Layout.addWidget(self.user_history_card_list[-1])
|
|
||||||
|
|
||||||
class UserHistoryCard(HeaderCardWidget):
|
|
||||||
|
|
||||||
def __init__(
|
|
||||||
self,
|
|
||||||
user_history_path: Path,
|
|
||||||
parent=None,
|
|
||||||
):
|
|
||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
|
|
||||||
self.setTitle(user_history_path.name.replace(".json", ""))
|
Layout = QHBoxLayout(self)
|
||||||
|
|
||||||
self.user_history_path = user_history_path
|
self.lable_1 = SubtitleLabel("查询范围:")
|
||||||
self.main_history = Config.load_maa_logs("总览", user_history_path)
|
self.start_date = ZhDatePicker()
|
||||||
|
self.start_date.setDate(QDate(2019, 5, 1))
|
||||||
|
self.lable_2 = SubtitleLabel("→")
|
||||||
|
self.end_date = ZhDatePicker()
|
||||||
|
server_date = Config.server_date()
|
||||||
|
self.end_date.setDate(
|
||||||
|
QDate(server_date.year, server_date.month, server_date.day)
|
||||||
|
)
|
||||||
|
self.mode = ComboBox()
|
||||||
|
self.mode.setPlaceholderText("请选择查询模式")
|
||||||
|
self.mode.addItems(["按日合并", "按周合并", "按月合并"])
|
||||||
|
|
||||||
self.index_card = self.IndexCard(self.main_history["条目索引"], self)
|
self.select_month = PushButton(FluentIcon.TAG, "最近一月")
|
||||||
self.statistics_card = QHBoxLayout()
|
self.select_week = PushButton(FluentIcon.TAG, "最近一周")
|
||||||
self.log_card = self.LogCard(self)
|
self.search = PushButton(FluentIcon.SEARCH, "查询")
|
||||||
|
self.select_month.clicked.connect(lambda: self.select_date("month"))
|
||||||
|
self.select_week.clicked.connect(lambda: self.select_date("week"))
|
||||||
|
self.search.clicked.connect(
|
||||||
|
lambda: self.search_history.emit(
|
||||||
|
self.mode.currentText(),
|
||||||
|
self.start_date.getDate(),
|
||||||
|
self.end_date.getDate(),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.index_card.index_changed.connect(self.update_info)
|
Layout.addWidget(self.lable_1)
|
||||||
|
Layout.addWidget(self.start_date)
|
||||||
|
Layout.addWidget(self.lable_2)
|
||||||
|
Layout.addWidget(self.end_date)
|
||||||
|
Layout.addWidget(self.mode)
|
||||||
|
Layout.addStretch(1)
|
||||||
|
Layout.addWidget(self.select_month)
|
||||||
|
Layout.addWidget(self.select_week)
|
||||||
|
Layout.addWidget(self.search)
|
||||||
|
|
||||||
self.viewLayout.addWidget(self.index_card)
|
def select_date(self, date: str) -> None:
|
||||||
self.viewLayout.addLayout(self.statistics_card)
|
"""
|
||||||
self.viewLayout.addWidget(self.log_card)
|
选中最近一段时间并启动查询
|
||||||
|
|
||||||
|
:param date: 选择的时间段("week" 或 "month")
|
||||||
|
"""
|
||||||
|
|
||||||
|
logger.info(f"选择最近{date}的记录并开始查询", module="历史记录")
|
||||||
|
|
||||||
|
server_date = Config.server_date()
|
||||||
|
if date == "week":
|
||||||
|
begin_date = server_date - timedelta(weeks=1)
|
||||||
|
elif date == "month":
|
||||||
|
begin_date = server_date - timedelta(days=30)
|
||||||
|
|
||||||
|
self.start_date.setDate(
|
||||||
|
QDate(begin_date.year, begin_date.month, begin_date.day)
|
||||||
|
)
|
||||||
|
self.end_date.setDate(
|
||||||
|
QDate(server_date.year, server_date.month, server_date.day)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.search.clicked.emit()
|
||||||
|
|
||||||
|
class HistoryCard(QuickExpandGroupCard):
|
||||||
|
"""历史记录卡片"""
|
||||||
|
|
||||||
|
def __init__(self, date: str, user_dict: Dict[str, List[Path]], parent=None):
|
||||||
|
super().__init__(
|
||||||
|
FluentIcon.HISTORY, date, f"{date}的历史运行记录与统计信息", parent
|
||||||
|
)
|
||||||
|
|
||||||
|
widget = QWidget()
|
||||||
|
Layout = QVBoxLayout(widget)
|
||||||
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
self.viewLayout.setSpacing(0)
|
self.viewLayout.setSpacing(0)
|
||||||
self.viewLayout.setStretch(0, 1)
|
self.addGroupWidget(widget)
|
||||||
self.viewLayout.setStretch(2, 4)
|
|
||||||
|
|
||||||
self.update_info("数据总览")
|
self.user_history_card_list = []
|
||||||
|
|
||||||
def update_info(self, index: str) -> None:
|
# 生成用户历史记录卡片并添加到布局中
|
||||||
"""更新信息"""
|
for user, info in user_dict.items():
|
||||||
|
self.user_history_card_list.append(
|
||||||
if index == "数据总览":
|
self.UserHistoryCard(user, info, self)
|
||||||
|
|
||||||
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():
|
|
||||||
|
|
||||||
statistics_card = self.StatisticsCard(name, item_list, self)
|
|
||||||
self.statistics_card.addWidget(statistics_card)
|
|
||||||
|
|
||||||
self.log_card.hide()
|
|
||||||
|
|
||||||
else:
|
|
||||||
|
|
||||||
single_history = Config.load_maa_logs(
|
|
||||||
"单项",
|
|
||||||
self.user_history_path.with_suffix("")
|
|
||||||
/ f"{index.replace(":","-")}.json",
|
|
||||||
)
|
)
|
||||||
|
Layout.addWidget(self.user_history_card_list[-1])
|
||||||
|
|
||||||
while self.statistics_card.count() > 0:
|
class UserHistoryCard(HeaderCardWidget):
|
||||||
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():
|
def __init__(self, name: str, user_history: List[Path], parent=None):
|
||||||
|
|
||||||
statistics_card = self.StatisticsCard(name, item_list, self)
|
|
||||||
self.statistics_card.addWidget(statistics_card)
|
|
||||||
|
|
||||||
self.log_card.text.setText(single_history["日志信息"])
|
|
||||||
self.log_card.button.clicked.disconnect()
|
|
||||||
self.log_card.button.clicked.connect(
|
|
||||||
lambda: os.startfile(
|
|
||||||
self.user_history_path.with_suffix("")
|
|
||||||
/ f"{index.replace(":","-")}.log"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
self.log_card.show()
|
|
||||||
|
|
||||||
self.viewLayout.setStretch(1, self.statistics_card.count())
|
|
||||||
|
|
||||||
self.setMinimumHeight(300)
|
|
||||||
|
|
||||||
class IndexCard(HeaderCardWidget):
|
|
||||||
|
|
||||||
index_changed = Signal(str)
|
|
||||||
|
|
||||||
def __init__(self, index_list: list, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setTitle("记录条目")
|
|
||||||
|
|
||||||
self.Layout = QVBoxLayout()
|
|
||||||
self.viewLayout.addLayout(self.Layout)
|
|
||||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
|
||||||
|
|
||||||
self.index_cards: List[StatefulItemCard] = []
|
|
||||||
|
|
||||||
for index in index_list:
|
|
||||||
|
|
||||||
self.index_cards.append(StatefulItemCard(index))
|
|
||||||
self.index_cards[-1].clicked.connect(
|
|
||||||
partial(self.index_changed.emit, index[0])
|
|
||||||
)
|
|
||||||
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)
|
super().__init__(parent)
|
||||||
self.setTitle(name)
|
self.setTitle(name)
|
||||||
|
|
||||||
self.Layout = QVBoxLayout()
|
self.user_history = user_history
|
||||||
self.viewLayout.addLayout(self.Layout)
|
|
||||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
|
||||||
|
|
||||||
self.item_cards: List[QuantifiedItemCard] = []
|
self.index_card = self.IndexCard(self.user_history, self)
|
||||||
|
self.index_card.index_changed.connect(self.update_info)
|
||||||
|
|
||||||
for item in item_list:
|
self.statistics_card = QHBoxLayout()
|
||||||
|
self.log_card = self.LogCard(self)
|
||||||
|
|
||||||
self.item_cards.append(QuantifiedItemCard(item))
|
self.viewLayout.addWidget(self.index_card)
|
||||||
self.Layout.addWidget(self.item_cards[-1])
|
self.viewLayout.addLayout(self.statistics_card)
|
||||||
|
self.viewLayout.addWidget(self.log_card)
|
||||||
|
self.viewLayout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
self.viewLayout.setSpacing(0)
|
||||||
|
self.viewLayout.setStretch(0, 1)
|
||||||
|
self.viewLayout.setStretch(2, 4)
|
||||||
|
|
||||||
if len(item_list) == 0:
|
self.update_info("数据总览")
|
||||||
self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""]))
|
|
||||||
|
|
||||||
self.Layout.addStretch(1)
|
def get_statistics(self, mode: str) -> dict:
|
||||||
|
"""
|
||||||
|
生成GUI相应结构化统计数据
|
||||||
|
|
||||||
class LogCard(HeaderCardWidget):
|
:param mode: 查询模式
|
||||||
|
:return: 结构化统计数据
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
history_info = Config.merge_statistic_info(
|
||||||
super().__init__(parent)
|
self.user_history if mode == "数据总览" else [Path(mode)]
|
||||||
self.setTitle("日志")
|
)
|
||||||
|
|
||||||
self.text = TextBrowser(self)
|
statistics_info = {}
|
||||||
self.button = PushButton("打开日志文件", self)
|
|
||||||
self.button.clicked.connect(lambda: print("打开日志文件"))
|
|
||||||
|
|
||||||
Layout = QVBoxLayout()
|
if "recruit_statistics" in history_info:
|
||||||
Layout.addWidget(self.text)
|
statistics_info["公招统计"] = list(
|
||||||
Layout.addWidget(self.button)
|
history_info["recruit_statistics"].items()
|
||||||
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
)
|
||||||
self.viewLayout.addLayout(Layout)
|
|
||||||
|
if "drop_statistics" in history_info:
|
||||||
|
for game_id, drops in history_info["drop_statistics"].items():
|
||||||
|
statistics_info[f"掉落统计:{game_id}"] = list(drops.items())
|
||||||
|
|
||||||
|
if mode == "数据总览" and "error_info" in history_info:
|
||||||
|
statistics_info["报错汇总"] = list(
|
||||||
|
history_info["error_info"].items()
|
||||||
|
)
|
||||||
|
|
||||||
|
return statistics_info
|
||||||
|
|
||||||
|
def update_info(self, index: str) -> None:
|
||||||
|
"""
|
||||||
|
更新信息到UI界面
|
||||||
|
|
||||||
|
:param index: 选择的索引
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 移除已有统计信息UI组件
|
||||||
|
while self.statistics_card.count() > 0:
|
||||||
|
item = self.statistics_card.takeAt(0)
|
||||||
|
if item.spacerItem():
|
||||||
|
self.statistics_card.removeItem(item.spacerItem())
|
||||||
|
elif item.widget():
|
||||||
|
item.widget().deleteLater()
|
||||||
|
|
||||||
|
# 统计信息上传至 UI
|
||||||
|
if index == "数据总览":
|
||||||
|
|
||||||
|
# 生成数据统计信息卡片组
|
||||||
|
for name, item_list in self.get_statistics("数据总览").items():
|
||||||
|
|
||||||
|
statistics_card = self.StatisticsCard(name, item_list, self)
|
||||||
|
self.statistics_card.addWidget(statistics_card)
|
||||||
|
|
||||||
|
self.log_card.hide()
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
single_history = self.get_statistics(index)
|
||||||
|
log_path = Path(index).with_suffix(".log")
|
||||||
|
|
||||||
|
# 生成单个历史记录的统计信息卡片组
|
||||||
|
for name, item_list in single_history.items():
|
||||||
|
statistics_card = self.StatisticsCard(name, item_list, self)
|
||||||
|
self.statistics_card.addWidget(statistics_card)
|
||||||
|
|
||||||
|
# 显示日志信息并绑定点击事件
|
||||||
|
with log_path.open("r", encoding="utf-8") as f:
|
||||||
|
log = f.read()
|
||||||
|
|
||||||
|
self.log_card.text.setText(log)
|
||||||
|
self.log_card.open_file.clicked.disconnect()
|
||||||
|
self.log_card.open_file.clicked.connect(
|
||||||
|
lambda: os.startfile(log_path)
|
||||||
|
)
|
||||||
|
self.log_card.open_dir.clicked.disconnect()
|
||||||
|
self.log_card.open_dir.clicked.connect(
|
||||||
|
lambda: subprocess.Popen(["explorer", "/select,", log_path])
|
||||||
|
)
|
||||||
|
self.log_card.show()
|
||||||
|
|
||||||
|
self.viewLayout.setStretch(1, self.statistics_card.count())
|
||||||
|
|
||||||
|
self.setMinimumHeight(300)
|
||||||
|
|
||||||
|
class IndexCard(HeaderCardWidget):
|
||||||
|
"""历史记录索引卡片组"""
|
||||||
|
|
||||||
|
index_changed = Signal(str)
|
||||||
|
|
||||||
|
def __init__(self, history_list: List[Path], parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("记录条目")
|
||||||
|
|
||||||
|
self.Layout = QVBoxLayout()
|
||||||
|
self.viewLayout.addLayout(self.Layout)
|
||||||
|
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||||
|
|
||||||
|
self.index_cards: List[StatefulItemCard] = []
|
||||||
|
|
||||||
|
# 生成索引卡片信息
|
||||||
|
index_list = Config.merge_statistic_info(history_list)["index"]
|
||||||
|
index_list.insert(0, ["数据总览", "运行", "数据总览"])
|
||||||
|
|
||||||
|
# 生成索引卡片组件并绑定点击事件
|
||||||
|
for index in index_list:
|
||||||
|
|
||||||
|
self.index_cards.append(StatefulItemCard(index[:2]))
|
||||||
|
self.index_cards[-1].clicked.connect(
|
||||||
|
partial(self.index_changed.emit, str(index[2]))
|
||||||
|
)
|
||||||
|
self.Layout.addWidget(self.index_cards[-1])
|
||||||
|
|
||||||
|
self.Layout.addStretch(1)
|
||||||
|
|
||||||
|
class StatisticsCard(HeaderCardWidget):
|
||||||
|
"""历史记录统计信息卡片组"""
|
||||||
|
|
||||||
|
def __init__(self, name: str, item_list: list, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle(name)
|
||||||
|
|
||||||
|
self.Layout = QVBoxLayout()
|
||||||
|
self.viewLayout.addLayout(self.Layout)
|
||||||
|
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||||
|
|
||||||
|
self.item_cards: List[QuantifiedItemCard] = []
|
||||||
|
|
||||||
|
for item in item_list:
|
||||||
|
|
||||||
|
self.item_cards.append(QuantifiedItemCard(item))
|
||||||
|
self.Layout.addWidget(self.item_cards[-1])
|
||||||
|
|
||||||
|
if len(item_list) == 0:
|
||||||
|
self.Layout.addWidget(QuantifiedItemCard(["暂无记录", ""]))
|
||||||
|
|
||||||
|
self.Layout.addStretch(1)
|
||||||
|
|
||||||
|
class LogCard(HeaderCardWidget):
|
||||||
|
"""历史记录日志卡片"""
|
||||||
|
|
||||||
|
def __init__(self, parent=None):
|
||||||
|
super().__init__(parent)
|
||||||
|
self.setTitle("日志")
|
||||||
|
|
||||||
|
self.text = TextBrowser(self)
|
||||||
|
self.open_file = PushButton("打开日志文件", self)
|
||||||
|
self.open_file.clicked.connect(lambda: print("打开日志文件"))
|
||||||
|
self.open_dir = PushButton("打开所在目录", self)
|
||||||
|
self.open_dir.clicked.connect(lambda: print("打开所在文件"))
|
||||||
|
|
||||||
|
Layout = QVBoxLayout()
|
||||||
|
h_layout = QHBoxLayout()
|
||||||
|
h_layout.addWidget(self.open_file)
|
||||||
|
h_layout.addWidget(self.open_dir)
|
||||||
|
Layout.addWidget(self.text)
|
||||||
|
Layout.addLayout(h_layout)
|
||||||
|
self.viewLayout.setContentsMargins(3, 0, 3, 3)
|
||||||
|
self.viewLayout.addLayout(Layout)
|
||||||
|
|||||||
138
app/ui/home.py
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,16 +16,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA主界面
|
AUTO_MAA主界面
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -45,13 +44,11 @@ from qfluentwidgets import (
|
|||||||
)
|
)
|
||||||
import re
|
import re
|
||||||
import shutil
|
import shutil
|
||||||
import requests
|
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.core import Config, MainInfoBar
|
from app.core import Config, MainInfoBar, Network, logger
|
||||||
from .Widget import Banner, IconButton
|
from .Widget import Banner, IconButton
|
||||||
|
|
||||||
|
|
||||||
@@ -64,14 +61,6 @@ class Home(QWidget):
|
|||||||
self.banner = Banner()
|
self.banner = Banner()
|
||||||
self.banner_text = TextBrowser()
|
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 = QVBoxLayout(self.banner)
|
||||||
v_layout.setContentsMargins(0, 0, 0, 15)
|
v_layout.setContentsMargins(0, 0, 0, 15)
|
||||||
v_layout.setSpacing(5)
|
v_layout.setSpacing(5)
|
||||||
@@ -148,20 +137,34 @@ class Home(QWidget):
|
|||||||
# 将底部水平布局添加到垂直布局
|
# 将底部水平布局添加到垂直布局
|
||||||
v_layout.addLayout(h2_layout)
|
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 = ScrollArea()
|
||||||
scrollArea.setWidgetResizable(True)
|
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)
|
layout.addWidget(scrollArea)
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
self.set_banner()
|
self.set_banner()
|
||||||
|
|
||||||
def get_home_image(self) -> None:
|
def get_home_image(self) -> None:
|
||||||
"""获取主页图片"""
|
"""获取主页图片"""
|
||||||
|
|
||||||
|
logger.info("获取主页图片", module="主页")
|
||||||
|
|
||||||
if Config.get(Config.function_HomeImageMode) == "默认":
|
if Config.get(Config.function_HomeImageMode) == "默认":
|
||||||
pass
|
|
||||||
|
logger.info("使用默认主页图片", module="主页")
|
||||||
|
|
||||||
elif Config.get(Config.function_HomeImageMode) == "自定义":
|
elif Config.get(Config.function_HomeImageMode) == "自定义":
|
||||||
|
|
||||||
file_path, _ = QFileDialog.getOpenFileName(
|
file_path, _ = QFileDialog.getOpenFileName(
|
||||||
@@ -180,7 +183,7 @@ class Home(QWidget):
|
|||||||
/ f"resources/images/Home/BannerCustomize{Path(file_path).suffix}",
|
/ f"resources/images/Home/BannerCustomize{Path(file_path).suffix}",
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"自定义主页图片更换成功:{file_path}")
|
logger.info(f"自定义主页图片更换成功:{file_path}", module="主页")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"success",
|
"success",
|
||||||
"主页图片更换成功",
|
"主页图片更换成功",
|
||||||
@@ -189,7 +192,7 @@ class Home(QWidget):
|
|||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
logger.warning("自定义主页图片更换失败:未选择图片文件")
|
logger.warning("自定义主页图片更换失败:未选择图片文件", module="主页")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning",
|
"warning",
|
||||||
"主页图片更换失败",
|
"主页图片更换失败",
|
||||||
@@ -198,24 +201,25 @@ class Home(QWidget):
|
|||||||
)
|
)
|
||||||
elif Config.get(Config.function_HomeImageMode) == "主题图像":
|
elif Config.get(Config.function_HomeImageMode) == "主题图像":
|
||||||
|
|
||||||
# 从远程服务器获取最新主题图像
|
# 从远程服务器获取最新主题图像信息
|
||||||
for _ in range(3):
|
network = Network.add_task(
|
||||||
try:
|
mode="get",
|
||||||
response = requests.get(
|
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/theme_image.json",
|
||||||
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/theme_image.json"
|
)
|
||||||
)
|
network.loop.exec()
|
||||||
theme_image = response.json()
|
network_result = Network.get_result(network)
|
||||||
break
|
if network_result["status_code"] == 200:
|
||||||
except Exception as e:
|
theme_image = network_result["response_json"]
|
||||||
err = e
|
|
||||||
time.sleep(0.1)
|
|
||||||
else:
|
else:
|
||||||
logger.error(f"获取最新主题图像时出错:\n{err}")
|
logger.warning(
|
||||||
|
f"获取最新主题图像时出错:{network_result['error_message']}",
|
||||||
|
module="主页",
|
||||||
|
)
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"error",
|
"warning",
|
||||||
"主题图像获取失败",
|
"获取最新主题图像时出错",
|
||||||
f"获取最新主题图像信息时出错!",
|
f"网络错误:{network_result['status_code']}",
|
||||||
-1,
|
5000,
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -230,6 +234,7 @@ class Home(QWidget):
|
|||||||
else:
|
else:
|
||||||
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
|
time_local = datetime.strptime("2000-01-01 00:00", "%Y-%m-%d %H:%M")
|
||||||
|
|
||||||
|
# 检查主题图像是否需要更新
|
||||||
if not (
|
if not (
|
||||||
Config.app_path / "resources/images/Home/BannerTheme.jpg"
|
Config.app_path / "resources/images/Home/BannerTheme.jpg"
|
||||||
).exists() or (
|
).exists() or (
|
||||||
@@ -238,15 +243,24 @@ class Home(QWidget):
|
|||||||
> time_local
|
> time_local
|
||||||
):
|
):
|
||||||
|
|
||||||
response = requests.get(theme_image["url"])
|
network = Network.add_task(
|
||||||
if response.status_code == 200:
|
mode="get_file",
|
||||||
|
url=theme_image["url"],
|
||||||
|
path=Config.app_path / "resources/images/Home/BannerTheme.jpg",
|
||||||
|
)
|
||||||
|
network.loop.exec()
|
||||||
|
network_result = Network.get_result(network)
|
||||||
|
|
||||||
with open(
|
if network_result["status_code"] == 200:
|
||||||
Config.app_path / "resources/images/Home/BannerTheme.jpg", "wb"
|
|
||||||
) as file:
|
|
||||||
file.write(response.content)
|
|
||||||
|
|
||||||
logger.info(f"主题图像「{theme_image["name"]}」下载成功")
|
with (Config.app_path / "resources/theme_image.json").open(
|
||||||
|
mode="w", encoding="utf-8"
|
||||||
|
) as f:
|
||||||
|
json.dump(theme_image, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
logger.success(
|
||||||
|
f"主题图像「{theme_image["name"]}」下载成功", module="主页"
|
||||||
|
)
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"success",
|
"success",
|
||||||
"主题图像下载成功",
|
"主题图像下载成功",
|
||||||
@@ -256,33 +270,29 @@ class Home(QWidget):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
logger.error("主题图像下载失败")
|
logger.warning(
|
||||||
MainInfoBar.push_info_bar(
|
f"下载最新主题图像时出错:{network_result['error_message']}",
|
||||||
"error",
|
module="主页",
|
||||||
"主题图像下载失败",
|
)
|
||||||
f"主题图像下载失败:{response.status_code}",
|
MainInfoBar.push_info_bar(
|
||||||
-1,
|
"warning",
|
||||||
|
"下载最新主题图像时出错",
|
||||||
|
f"网络错误:{network_result['status_code']}",
|
||||||
|
5000,
|
||||||
)
|
)
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
|
||||||
logger.info("主题图像已是最新")
|
logger.info("主题图像已是最新", module="主页")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"info",
|
"info", "主题图像已是最新", "主题图像已是最新!", 3000
|
||||||
"主题图像已是最新",
|
|
||||||
"主题图像已是最新!",
|
|
||||||
3000,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
self.set_banner()
|
self.set_banner()
|
||||||
|
|
||||||
def set_banner(self):
|
def set_banner(self):
|
||||||
"""设置主页图像"""
|
"""设置主页图像"""
|
||||||
|
|
||||||
if Config.get(Config.function_HomeImageMode) == "默认":
|
if Config.get(Config.function_HomeImageMode) == "默认":
|
||||||
self.banner.set_banner_image(
|
self.banner.set_banner_image(
|
||||||
str(Config.app_path / "resources/images/Home/BannerDefault.png")
|
str(Config.app_path / "resources/images/Home/BannerDefault.png")
|
||||||
@@ -362,7 +372,7 @@ class ButtonGroup(SimpleCardWidget):
|
|||||||
doc_button = IconButton(
|
doc_button = IconButton(
|
||||||
FluentIcon.CHAT.icon(color=QColor("#fff")),
|
FluentIcon.CHAT.icon(color=QColor("#fff")),
|
||||||
tip_title="官方社群",
|
tip_title="官方社群",
|
||||||
tip_content="加入官方群聊【AUTO_MAA绝赞DeBug中!】",
|
tip_content="加入官方群聊「AUTO_MAA绝赞DeBug中!」",
|
||||||
isTooltip=True,
|
isTooltip=True,
|
||||||
)
|
)
|
||||||
doc_button.setIconSize(QSize(32, 32))
|
doc_button.setIconSize(QSize(32, 32))
|
||||||
@@ -401,4 +411,6 @@ class ButtonGroup(SimpleCardWidget):
|
|||||||
|
|
||||||
def open_sales(self):
|
def open_sales(self):
|
||||||
"""打开 MirrorChyan 链接"""
|
"""打开 MirrorChyan 链接"""
|
||||||
QDesktopServices.openUrl(QUrl("https://mirrorchyan.com/"))
|
QDesktopServices.openUrl(
|
||||||
|
QUrl("https://mirrorchyan.com/zh/get-start?source=auto_maa-home")
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,19 +16,17 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA主界面
|
AUTO_MAA主界面
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
|
||||||
from PySide6.QtWidgets import QSystemTrayIcon
|
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
qconfig,
|
|
||||||
Action,
|
Action,
|
||||||
SystemTrayMenu,
|
SystemTrayMenu,
|
||||||
SplashScreen,
|
SplashScreen,
|
||||||
@@ -42,14 +40,13 @@ from qfluentwidgets import (
|
|||||||
)
|
)
|
||||||
from PySide6.QtGui import QIcon, QCloseEvent
|
from PySide6.QtGui import QIcon, QCloseEvent
|
||||||
from PySide6.QtCore import QTimer
|
from PySide6.QtCore import QTimer
|
||||||
from datetime import datetime, timedelta
|
|
||||||
import shutil
|
|
||||||
import darkdetect
|
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 app.services import Notify, Crypto, System
|
||||||
from .home import Home
|
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 .queue_manager import QueueManager
|
||||||
from .dispatch_center import DispatchCenter
|
from .dispatch_center import DispatchCenter
|
||||||
from .history import History
|
from .history import History
|
||||||
@@ -57,25 +54,35 @@ from .setting import Setting
|
|||||||
|
|
||||||
|
|
||||||
class AUTO_MAA(MSFluentWindow):
|
class AUTO_MAA(MSFluentWindow):
|
||||||
|
"""AUTO_MAA主界面"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
self.setWindowIcon(QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")))
|
self.setWindowIcon(QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")))
|
||||||
self.setWindowTitle("AUTO_MAA")
|
|
||||||
|
version_numb = list(map(int, Config.VERSION.split(".")))
|
||||||
|
version_text = (
|
||||||
|
f"v{'.'.join(str(_) for _ in version_numb[0:3])}"
|
||||||
|
if version_numb[3] == 0
|
||||||
|
else f"v{'.'.join(str(_) for _ in version_numb[0:3])}-beta.{version_numb[3]}"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.setWindowTitle(f"AUTO_MAA - {version_text}")
|
||||||
|
|
||||||
self.switch_theme()
|
self.switch_theme()
|
||||||
|
|
||||||
self.splashScreen = SplashScreen(self.windowIcon(), self)
|
self.splashScreen = SplashScreen(self.windowIcon(), self)
|
||||||
self.show_ui("显示主窗口", if_quick=True)
|
self.show_ui("显示主窗口", if_quick=True)
|
||||||
|
|
||||||
TaskManager.main_window = self.window()
|
# 设置主窗口的引用,便于各组件访问
|
||||||
MainInfoBar.main_window = self.window()
|
Config.main_window = self.window()
|
||||||
System.main_window = self.window()
|
|
||||||
|
|
||||||
# 创建主窗口
|
# 创建各子窗口
|
||||||
|
logger.info("正在创建各子窗口", module="主窗口")
|
||||||
self.home = Home(self)
|
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.queue_manager = QueueManager(self)
|
||||||
self.dispatch_center = DispatchCenter(self)
|
self.dispatch_center = DispatchCenter(self)
|
||||||
self.history = History(self)
|
self.history = History(self)
|
||||||
@@ -89,12 +96,19 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
NavigationItemPosition.TOP,
|
NavigationItemPosition.TOP,
|
||||||
)
|
)
|
||||||
self.addSubInterface(
|
self.addSubInterface(
|
||||||
self.member_manager,
|
self.script_manager,
|
||||||
FluentIcon.ROBOT,
|
FluentIcon.ROBOT,
|
||||||
"脚本管理",
|
"脚本管理",
|
||||||
FluentIcon.ROBOT,
|
FluentIcon.ROBOT,
|
||||||
NavigationItemPosition.TOP,
|
NavigationItemPosition.TOP,
|
||||||
)
|
)
|
||||||
|
self.addSubInterface(
|
||||||
|
self.plan_manager,
|
||||||
|
FluentIcon.CALENDAR,
|
||||||
|
"计划管理",
|
||||||
|
FluentIcon.CALENDAR,
|
||||||
|
NavigationItemPosition.TOP,
|
||||||
|
)
|
||||||
self.addSubInterface(
|
self.addSubInterface(
|
||||||
self.queue_manager,
|
self.queue_manager,
|
||||||
FluentIcon.BOOK_SHELF,
|
FluentIcon.BOOK_SHELF,
|
||||||
@@ -123,28 +137,11 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
FluentIcon.SETTING,
|
FluentIcon.SETTING,
|
||||||
NavigationItemPosition.BOTTOM,
|
NavigationItemPosition.BOTTOM,
|
||||||
)
|
)
|
||||||
self.stackedWidget.currentChanged.connect(
|
self.stackedWidget.currentChanged.connect(self.__currentChanged)
|
||||||
lambda index: (
|
logger.success("各子窗口创建完成", module="主窗口")
|
||||||
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(
|
|
||||||
lambda index: (self.history.refresh() if index == 4 else None)
|
|
||||||
)
|
|
||||||
|
|
||||||
# 创建系统托盘及其菜单
|
# 创建系统托盘及其菜单
|
||||||
|
logger.info("正在创建系统托盘", module="主窗口")
|
||||||
self.tray = QSystemTrayIcon(
|
self.tray = QSystemTrayIcon(
|
||||||
QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), self
|
QIcon(str(Config.app_path / "resources/icons/AUTO_MAA.ico")), self
|
||||||
)
|
)
|
||||||
@@ -164,7 +161,7 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
# 开始任务菜单项
|
# 开始任务菜单项
|
||||||
self.tray_menu.addActions(
|
self.tray_menu.addActions(
|
||||||
[
|
[
|
||||||
Action(FluentIcon.PLAY, "运行自动代理", triggered=self.start_main_task),
|
Action(FluentIcon.PLAY, "运行启动时队列", triggered=self.start_up_task),
|
||||||
Action(
|
Action(
|
||||||
FluentIcon.PAUSE,
|
FluentIcon.PAUSE,
|
||||||
"中止所有任务",
|
"中止所有任务",
|
||||||
@@ -176,14 +173,23 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
|
|
||||||
# 退出主程序菜单项
|
# 退出主程序菜单项
|
||||||
self.tray_menu.addAction(
|
self.tray_menu.addAction(
|
||||||
Action(FluentIcon.POWER_BUTTON, "退出主程序", triggered=self.window().close)
|
Action(
|
||||||
|
FluentIcon.POWER_BUTTON,
|
||||||
|
"退出主程序",
|
||||||
|
triggered=lambda: System.set_power("KillSelf"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 设置托盘菜单
|
# 设置托盘菜单
|
||||||
self.tray.setContextMenu(self.tray_menu)
|
self.tray.setContextMenu(self.tray_menu)
|
||||||
self.tray.activated.connect(self.on_tray_activated)
|
self.tray.activated.connect(self.on_tray_activated)
|
||||||
|
logger.success("系统托盘创建完成", module="主窗口")
|
||||||
|
|
||||||
Config.user_info_changed.connect(self.member_manager.refresh_dashboard)
|
self.set_min_method()
|
||||||
|
|
||||||
|
# 绑定各组件信号
|
||||||
|
Config.sub_info_changed.connect(self.script_manager.refresh_dashboard)
|
||||||
|
Config.power_sign_changed.connect(self.dispatch_center.update_power_sign)
|
||||||
TaskManager.create_gui.connect(self.dispatch_center.add_board)
|
TaskManager.create_gui.connect(self.dispatch_center.add_board)
|
||||||
TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board)
|
TaskManager.connect_gui.connect(self.dispatch_center.connect_main_board)
|
||||||
Notify.push_info_bar.connect(MainInfoBar.push_info_bar)
|
Notify.push_info_bar.connect(MainInfoBar.push_info_bar)
|
||||||
@@ -203,6 +209,8 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
self.themeListener.systemThemeChanged.connect(self.switch_theme)
|
self.themeListener.systemThemeChanged.connect(self.switch_theme)
|
||||||
self.themeListener.start()
|
self.themeListener.start()
|
||||||
|
|
||||||
|
logger.success("AUTO_MAA主程序初始化完成", module="主窗口")
|
||||||
|
|
||||||
def switch_theme(self) -> None:
|
def switch_theme(self) -> None:
|
||||||
"""切换主题"""
|
"""切换主题"""
|
||||||
|
|
||||||
@@ -232,43 +240,6 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
else:
|
else:
|
||||||
self.setStyleSheet("background-color: #ffffff;")
|
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_show=False)
|
|
||||||
|
|
||||||
# 检查更新
|
|
||||||
if Config.get(Config.update_IfAutoUpdate):
|
|
||||||
self.setting.check_update()
|
|
||||||
|
|
||||||
# 直接最小化
|
|
||||||
if Config.get(Config.start_IfMinimizeDirectly):
|
|
||||||
|
|
||||||
self.titleBar.minBtn.click()
|
|
||||||
|
|
||||||
def set_min_method(self) -> None:
|
def set_min_method(self) -> None:
|
||||||
"""设置最小化方法"""
|
"""设置最小化方法"""
|
||||||
|
|
||||||
@@ -287,90 +258,68 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
if reason == QSystemTrayIcon.DoubleClick:
|
if reason == QSystemTrayIcon.DoubleClick:
|
||||||
self.show_ui("显示主窗口")
|
self.show_ui("显示主窗口")
|
||||||
|
|
||||||
def clean_old_logs(self):
|
def show_ui(
|
||||||
"""
|
self, mode: str, if_quick: bool = False, if_start: bool = False
|
||||||
删除超过用户设定天数的日志文件(基于目录日期)
|
) -> None:
|
||||||
"""
|
|
||||||
|
|
||||||
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(
|
|
||||||
"自动代理_主调度台",
|
|
||||||
"主任务队列",
|
|
||||||
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:
|
|
||||||
"""配置窗口状态"""
|
"""配置窗口状态"""
|
||||||
|
|
||||||
|
if Config.args.mode != "gui":
|
||||||
|
return None
|
||||||
|
|
||||||
self.switch_theme()
|
self.switch_theme()
|
||||||
|
|
||||||
if mode == "显示主窗口":
|
if mode == "显示主窗口":
|
||||||
|
|
||||||
# 配置主窗口
|
# 配置主窗口
|
||||||
size = list(
|
if not self.window().isVisible():
|
||||||
map(
|
size = list(
|
||||||
int,
|
map(
|
||||||
Config.get(Config.ui_size).split("x"),
|
int,
|
||||||
|
Config.get(Config.ui_size).split("x"),
|
||||||
|
)
|
||||||
)
|
)
|
||||||
)
|
location = list(
|
||||||
location = list(
|
map(
|
||||||
map(
|
int,
|
||||||
int,
|
Config.get(Config.ui_location).split("x"),
|
||||||
Config.get(Config.ui_location).split("x"),
|
)
|
||||||
)
|
)
|
||||||
)
|
if self.window().isMaximized():
|
||||||
self.window().setGeometry(location[0], location[1], size[0], size[1])
|
self.window().showNormal()
|
||||||
self.window().show()
|
self.window().setGeometry(location[0], location[1], size[0], size[1])
|
||||||
|
self.window().show()
|
||||||
|
if not if_quick:
|
||||||
|
if (
|
||||||
|
Config.get(Config.ui_maximized)
|
||||||
|
and not self.window().isMaximized()
|
||||||
|
):
|
||||||
|
self.titleBar.maxBtn.click()
|
||||||
|
SoundPlayer.play("欢迎回来")
|
||||||
|
self.show_ui("配置托盘")
|
||||||
|
elif if_start:
|
||||||
|
if Config.get(Config.ui_maximized) and not self.window().isMaximized():
|
||||||
|
self.titleBar.maxBtn.click()
|
||||||
|
self.show_ui("配置托盘")
|
||||||
|
|
||||||
|
# 如果窗口不在屏幕内,则重置窗口位置
|
||||||
|
if not any(
|
||||||
|
self.window().geometry().intersects(screen.availableGeometry())
|
||||||
|
for screen in QApplication.screens()
|
||||||
|
):
|
||||||
|
self.window().showNormal()
|
||||||
|
self.window().setGeometry(100, 100, 1200, 700)
|
||||||
|
|
||||||
self.window().raise_()
|
self.window().raise_()
|
||||||
self.window().activateWindow()
|
self.window().activateWindow()
|
||||||
if not if_quick:
|
|
||||||
if Config.get(Config.ui_maximized):
|
while Config.info_bar_list:
|
||||||
self.window().showMaximized()
|
info_bar_item = Config.info_bar_list.pop(0)
|
||||||
self.set_min_method()
|
MainInfoBar.push_info_bar(
|
||||||
self.show_ui("配置托盘")
|
info_bar_item["mode"],
|
||||||
|
info_bar_item["title"],
|
||||||
|
info_bar_item["content"],
|
||||||
|
info_bar_item["time"],
|
||||||
|
)
|
||||||
|
|
||||||
elif mode == "配置托盘":
|
elif mode == "配置托盘":
|
||||||
|
|
||||||
@@ -392,6 +341,7 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
Config.ui_location,
|
Config.ui_location,
|
||||||
f"{self.geometry().x()}x{self.geometry().y()}",
|
f"{self.geometry().x()}x{self.geometry().y()}",
|
||||||
)
|
)
|
||||||
|
|
||||||
Config.set(Config.ui_maximized, self.window().isMaximized())
|
Config.set(Config.ui_maximized, self.window().isMaximized())
|
||||||
Config.save()
|
Config.save()
|
||||||
|
|
||||||
@@ -401,23 +351,138 @@ class AUTO_MAA(MSFluentWindow):
|
|||||||
self.window().hide()
|
self.window().hide()
|
||||||
self.tray.show()
|
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):
|
def closeEvent(self, event: QCloseEvent):
|
||||||
"""清理残余进程"""
|
"""清理残余进程"""
|
||||||
|
|
||||||
|
logger.info("保存窗口位置与大小信息", module="主窗口")
|
||||||
self.show_ui("隐藏到托盘", if_quick=True)
|
self.show_ui("隐藏到托盘", if_quick=True)
|
||||||
|
|
||||||
# 清理各功能线程
|
# 清理各功能线程
|
||||||
MainTimer.Timer.stop()
|
MainTimer.stop()
|
||||||
MainTimer.Timer.deleteLater()
|
|
||||||
MainTimer.LongTimer.stop()
|
|
||||||
MainTimer.LongTimer.deleteLater()
|
|
||||||
TaskManager.stop_task("ALL")
|
TaskManager.stop_task("ALL")
|
||||||
|
|
||||||
# 关闭主题监听
|
# 关闭主题监听
|
||||||
self.themeListener.terminate()
|
self.themeListener.terminate()
|
||||||
self.themeListener.deleteLater()
|
self.themeListener.deleteLater()
|
||||||
|
|
||||||
logger.info("AUTO_MAA主程序关闭")
|
logger.info("AUTO_MAA主程序关闭", module="主窗口")
|
||||||
logger.info("----------------END----------------")
|
logger.info("----------------END----------------", module="主窗口")
|
||||||
|
|
||||||
event.accept()
|
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"],
|
||||||
|
)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,16 +16,15 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA调度队列界面
|
AUTO_MAA调度队列界面
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtWidgets import (
|
||||||
QWidget,
|
QWidget,
|
||||||
QVBoxLayout,
|
QVBoxLayout,
|
||||||
@@ -34,24 +33,23 @@ from PySide6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
Action,
|
Action,
|
||||||
Pivot,
|
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
MessageBox,
|
MessageBox,
|
||||||
HeaderCardWidget,
|
HeaderCardWidget,
|
||||||
TextBrowser,
|
|
||||||
CommandBar,
|
CommandBar,
|
||||||
)
|
)
|
||||||
from PySide6.QtCore import Qt
|
from typing import List, Dict
|
||||||
from typing import List
|
|
||||||
|
|
||||||
from app.core import QueueConfig, Config, MainInfoBar
|
from app.core import QueueConfig, Config, MainInfoBar, SoundPlayer, logger
|
||||||
from .Widget import (
|
from .Widget import (
|
||||||
SwitchSettingCard,
|
SwitchSettingCard,
|
||||||
ComboBoxSettingCard,
|
ComboBoxSettingCard,
|
||||||
LineEditSettingCard,
|
LineEditSettingCard,
|
||||||
TimeEditSettingCard,
|
TimeEditSettingCard,
|
||||||
NoOptionComboBoxSettingCard,
|
NoOptionComboBoxSettingCard,
|
||||||
|
HistoryCard,
|
||||||
|
PivotArea,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -71,36 +69,31 @@ class QueueManager(QWidget):
|
|||||||
# 逐个添加动作
|
# 逐个添加动作
|
||||||
self.tools.addActions(
|
self.tools.addActions(
|
||||||
[
|
[
|
||||||
|
Action(FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_queue),
|
||||||
Action(
|
Action(
|
||||||
FluentIcon.ADD_TO, "新建调度队列", triggered=self.add_setting_box
|
FluentIcon.REMOVE_FROM, "删除调度队列", triggered=self.del_queue
|
||||||
),
|
|
||||||
Action(
|
|
||||||
FluentIcon.REMOVE_FROM,
|
|
||||||
"删除调度队列",
|
|
||||||
triggered=self.del_setting_box,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
self.tools.addSeparator()
|
self.tools.addSeparator()
|
||||||
self.tools.addActions(
|
self.tools.addActions(
|
||||||
[
|
[
|
||||||
Action(
|
Action(FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_queue),
|
||||||
FluentIcon.LEFT_ARROW, "向左移动", triggered=self.left_setting_box
|
Action(FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_queue),
|
||||||
),
|
|
||||||
Action(
|
|
||||||
FluentIcon.RIGHT_ARROW, "向右移动", triggered=self.right_setting_box
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
layout.addWidget(self.tools)
|
layout.addWidget(self.tools)
|
||||||
layout.addWidget(self.queue_manager)
|
layout.addWidget(self.queue_manager)
|
||||||
|
|
||||||
def add_setting_box(self):
|
def add_queue(self):
|
||||||
"""添加一个调度队列"""
|
"""添加一个调度队列"""
|
||||||
|
|
||||||
index = len(Config.queue_dict) + 1
|
index = len(Config.queue_dict) + 1
|
||||||
|
|
||||||
|
logger.info(f"正在添加调度队列_{index}", module="队列管理")
|
||||||
|
|
||||||
|
# 初始化队列配置
|
||||||
queue_config = QueueConfig()
|
queue_config = QueueConfig()
|
||||||
queue_config.load(
|
queue_config.load(
|
||||||
Config.app_path / f"config/QueueConfig/调度队列_{index}.json", queue_config
|
Config.app_path / f"config/QueueConfig/调度队列_{index}.json", queue_config
|
||||||
@@ -112,40 +105,41 @@ class QueueManager(QWidget):
|
|||||||
"Config": queue_config,
|
"Config": queue_config,
|
||||||
}
|
}
|
||||||
|
|
||||||
self.queue_manager.add_QueueSettingBox(index)
|
# 添加到配置界面
|
||||||
|
self.queue_manager.add_SettingBox(index)
|
||||||
self.queue_manager.switch_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)
|
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()
|
name = self.queue_manager.pivot.currentRouteKey()
|
||||||
|
|
||||||
if name == None:
|
if name is None:
|
||||||
logger.warning("未选择调度队列")
|
logger.warning("未选择调度队列", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if name in Config.running_list:
|
if name in Config.running_list:
|
||||||
logger.warning("调度队列正在运行")
|
logger.warning("调度队列正在运行", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "调度队列正在运行", "请先停止调度队列", 5000
|
"warning", "调度队列正在运行", "请先停止调度队列", 5000
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
choice = MessageBox(
|
choice = MessageBox("确认", f"确定要删除 {name} 吗?", self.window())
|
||||||
"确认",
|
|
||||||
f"确定要删除 {name} 吗?",
|
|
||||||
self.window(),
|
|
||||||
)
|
|
||||||
if choice.exec():
|
if choice.exec():
|
||||||
|
|
||||||
|
logger.info(f"正在删除调度队列 {name}", module="队列管理")
|
||||||
|
|
||||||
self.queue_manager.clear_SettingBox()
|
self.queue_manager.clear_SettingBox()
|
||||||
|
|
||||||
|
# 删除队列配置文件并同步到相关配置项
|
||||||
Config.queue_dict[name]["Path"].unlink()
|
Config.queue_dict[name]["Path"].unlink()
|
||||||
for i in range(int(name[5:]) + 1, len(Config.queue_dict) + 1):
|
for i in range(int(name[5:]) + 1, len(Config.queue_dict) + 1):
|
||||||
if Config.queue_dict[f"调度队列_{i}"]["Path"].exists():
|
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))
|
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)
|
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()
|
name = self.queue_manager.pivot.currentRouteKey()
|
||||||
|
|
||||||
if name == None:
|
if name is None:
|
||||||
logger.warning("未选择调度队列")
|
logger.warning("未选择调度队列", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||||
)
|
)
|
||||||
@@ -175,21 +170,24 @@ class QueueManager(QWidget):
|
|||||||
index = int(name[5:])
|
index = int(name[5:])
|
||||||
|
|
||||||
if index == 1:
|
if index == 1:
|
||||||
logger.warning("向左移动调度队列时已到达最左端")
|
logger.warning("向左移动调度队列时已到达最左端", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "已经是第一个调度队列", "无法向左移动", 5000
|
"warning", "已经是第一个调度队列", "无法向左移动", 5000
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if name in Config.running_list or f"调度队列_{index-1}" in Config.running_list:
|
if name in Config.running_list or f"调度队列_{index-1}" in Config.running_list:
|
||||||
logger.warning("相关调度队列正在运行")
|
logger.warning("相关调度队列正在运行", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info(f"正在左移调度队列 {name}", module="队列管理")
|
||||||
|
|
||||||
self.queue_manager.clear_SettingBox()
|
self.queue_manager.clear_SettingBox()
|
||||||
|
|
||||||
|
# 移动配置文件并同步到相关配置项
|
||||||
Config.queue_dict[name]["Path"].rename(
|
Config.queue_dict[name]["Path"].rename(
|
||||||
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
|
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
|
||||||
)
|
)
|
||||||
@@ -202,16 +200,16 @@ class QueueManager(QWidget):
|
|||||||
|
|
||||||
self.queue_manager.show_SettingBox(index - 1)
|
self.queue_manager.show_SettingBox(index - 1)
|
||||||
|
|
||||||
logger.success(f"{name} 左移成功")
|
logger.success(f"{name} 左移成功", module="队列管理")
|
||||||
MainInfoBar.push_info_bar("success", "操作成功", f"左移 {name}", 3000)
|
MainInfoBar.push_info_bar("success", "操作成功", f"左移 {name}", 3000)
|
||||||
|
|
||||||
def right_setting_box(self):
|
def right_queue(self):
|
||||||
"""向右移动调度队列实例"""
|
"""向右移动调度队列实例"""
|
||||||
|
|
||||||
name = self.queue_manager.pivot.currentRouteKey()
|
name = self.queue_manager.pivot.currentRouteKey()
|
||||||
|
|
||||||
if name == None:
|
if name is None:
|
||||||
logger.warning("未选择调度队列")
|
logger.warning("未选择调度队列", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
"warning", "未选择调度队列", "请先选择一个调度队列", 5000
|
||||||
)
|
)
|
||||||
@@ -220,21 +218,24 @@ class QueueManager(QWidget):
|
|||||||
index = int(name[5:])
|
index = int(name[5:])
|
||||||
|
|
||||||
if index == len(Config.queue_dict):
|
if index == len(Config.queue_dict):
|
||||||
logger.warning("向右移动调度队列时已到达最右端")
|
logger.warning("向右移动调度队列时已到达最右端", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "已经是最后一个调度队列", "无法向右移动", 5000
|
"warning", "已经是最后一个调度队列", "无法向右移动", 5000
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if name in Config.running_list or f"调度队列_{index+1}" in Config.running_list:
|
if name in Config.running_list or f"调度队列_{index+1}" in Config.running_list:
|
||||||
logger.warning("相关调度队列正在运行")
|
logger.warning("相关调度队列正在运行", module="队列管理")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
"warning", "相关调度队列正在运行", "请先停止调度队列", 5000
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info(f"正在右移调度队列 {name}", module="队列管理")
|
||||||
|
|
||||||
self.queue_manager.clear_SettingBox()
|
self.queue_manager.clear_SettingBox()
|
||||||
|
|
||||||
|
# 移动配置文件并同步到相关配置项
|
||||||
Config.queue_dict[name]["Path"].rename(
|
Config.queue_dict[name]["Path"].rename(
|
||||||
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
|
Config.queue_dict[name]["Path"].with_name("调度队列_0.json")
|
||||||
)
|
)
|
||||||
@@ -247,56 +248,28 @@ class QueueManager(QWidget):
|
|||||||
|
|
||||||
self.queue_manager.show_SettingBox(index + 1)
|
self.queue_manager.show_SettingBox(index + 1)
|
||||||
|
|
||||||
logger.success(f"{name} 右移成功")
|
logger.success(f"{name} 右移成功", module="队列管理")
|
||||||
MainInfoBar.push_info_bar("success", "操作成功", f"右移 {name}", 3000)
|
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
|
k
|
||||||
if v["Config"].get(v["Config"].MaaSet_Name) == ""
|
if v["Config"].get_name() == ""
|
||||||
else f"{k} - {v["Config"].get(v["Config"].MaaSet_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:
|
for script in self.queue_manager.script_list:
|
||||||
|
for card in script.task.card_dict.values():
|
||||||
script.task.card_Member_1.reLoadOptions(
|
card.reLoadOptions(value=script_list[0], texts=script_list[1])
|
||||||
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]
|
|
||||||
)
|
|
||||||
|
|
||||||
class QueueSettingBox(QWidget):
|
class QueueSettingBox(QWidget):
|
||||||
|
|
||||||
@@ -305,15 +278,19 @@ class QueueManager(QWidget):
|
|||||||
|
|
||||||
self.setObjectName("调度队列管理")
|
self.setObjectName("调度队列管理")
|
||||||
|
|
||||||
self.pivot = Pivot(self)
|
self.pivotArea = PivotArea()
|
||||||
|
self.pivot = self.pivotArea.pivot
|
||||||
|
|
||||||
self.stackedWidget = QStackedWidget(self)
|
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[
|
self.script_list: List[
|
||||||
QueueManager.QueueSettingBox.QueueMemberSettingBox
|
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.addWidget(self.stackedWidget)
|
||||||
self.Layout.setContentsMargins(0, 0, 0, 0)
|
self.Layout.setContentsMargins(0, 0, 0, 0)
|
||||||
|
|
||||||
@@ -331,12 +308,17 @@ class QueueManager(QWidget):
|
|||||||
Config.search_queue()
|
Config.search_queue()
|
||||||
|
|
||||||
for name in Config.queue_dict.keys():
|
for name in Config.queue_dict.keys():
|
||||||
self.add_QueueSettingBox(int(name[5:]))
|
self.add_SettingBox(int(name[5:]))
|
||||||
|
|
||||||
self.switch_SettingBox(index)
|
self.switch_SettingBox(index)
|
||||||
|
|
||||||
def switch_SettingBox(self, index: int, if_change_pivot: bool = True) -> None:
|
def switch_SettingBox(self, index: int, if_change_pivot: bool = True) -> None:
|
||||||
"""切换到指定的子界面"""
|
"""
|
||||||
|
切换到指定的子界面并切换到指定的子页面
|
||||||
|
|
||||||
|
:param index: 要切换到的子界面索引
|
||||||
|
:param if_change_pivot: 是否更改导航栏当前项
|
||||||
|
"""
|
||||||
|
|
||||||
if len(Config.queue_dict) == 0:
|
if len(Config.queue_dict) == 0:
|
||||||
return None
|
return None
|
||||||
@@ -357,12 +339,12 @@ class QueueManager(QWidget):
|
|||||||
self.script_list.clear()
|
self.script_list.clear()
|
||||||
self.pivot.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])
|
self.stackedWidget.addWidget(self.script_list[-1])
|
||||||
|
|
||||||
@@ -376,31 +358,33 @@ class QueueManager(QWidget):
|
|||||||
self.setObjectName(f"调度队列_{uid}")
|
self.setObjectName(f"调度队列_{uid}")
|
||||||
self.config = Config.queue_dict[f"调度队列_{uid}"]["Config"]
|
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.queue_set = self.QueueSetSettingCard(self.config, self)
|
||||||
self.time = self.TimeSettingCard(self.config, self)
|
self.time = self.TimeSettingCard(self.config, self)
|
||||||
self.task = self.TaskSettingCard(self.config, self)
|
self.task = self.TaskSettingCard(self.config, self)
|
||||||
self.history = self.HistoryCard(f"调度队列_{uid}", self)
|
self.history = HistoryCard(
|
||||||
|
qconfig=self.config,
|
||||||
|
configItem=self.config.Data_LastProxyHistory,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
|
content_widget = QWidget()
|
||||||
|
content_layout = QVBoxLayout(content_widget)
|
||||||
|
content_layout.setContentsMargins(0, 0, 11, 0)
|
||||||
content_layout.addWidget(self.queue_set)
|
content_layout.addWidget(self.queue_set)
|
||||||
content_layout.addWidget(self.time)
|
content_layout.addWidget(self.time)
|
||||||
content_layout.addWidget(self.task)
|
content_layout.addWidget(self.task)
|
||||||
content_layout.addWidget(self.history)
|
content_layout.addWidget(self.history)
|
||||||
content_layout.addStretch(1)
|
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)
|
scrollArea.setWidget(content_widget)
|
||||||
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
layout.addWidget(scrollArea)
|
layout.addWidget(scrollArea)
|
||||||
|
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
class QueueSetSettingCard(HeaderCardWidget):
|
class QueueSetSettingCard(HeaderCardWidget):
|
||||||
|
|
||||||
def __init__(self, config: QueueConfig, parent=None):
|
def __init__(self, config: QueueConfig, parent=None):
|
||||||
@@ -415,15 +399,23 @@ class QueueManager(QWidget):
|
|||||||
content="用于标识调度队列的名称",
|
content="用于标识调度队列的名称",
|
||||||
text="请输入调度队列名称",
|
text="请输入调度队列名称",
|
||||||
qconfig=self.config,
|
qconfig=self.config,
|
||||||
configItem=self.config.queueSet_Name,
|
configItem=self.config.QueueSet_Name,
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
self.card_Enable = SwitchSettingCard(
|
self.card_StartUpEnabled = SwitchSettingCard(
|
||||||
icon=FluentIcon.HOME,
|
icon=FluentIcon.CHECKBOX,
|
||||||
title="状态",
|
title="启动时运行",
|
||||||
content="调度队列状态",
|
content="调度队列启动时运行状态,启用后将在软件启动时自动运行本队列",
|
||||||
qconfig=self.config,
|
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,
|
parent=self,
|
||||||
)
|
)
|
||||||
self.card_AfterAccomplish = ComboBoxSettingCard(
|
self.card_AfterAccomplish = ComboBoxSettingCard(
|
||||||
@@ -436,15 +428,17 @@ class QueueManager(QWidget):
|
|||||||
"睡眠(win系统需禁用休眠)",
|
"睡眠(win系统需禁用休眠)",
|
||||||
"休眠",
|
"休眠",
|
||||||
"关机",
|
"关机",
|
||||||
|
"关机(强制)",
|
||||||
],
|
],
|
||||||
qconfig=self.config,
|
qconfig=self.config,
|
||||||
configItem=self.config.queueSet_AfterAccomplish,
|
configItem=self.config.QueueSet_AfterAccomplish,
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|
||||||
Layout = QVBoxLayout()
|
Layout = QVBoxLayout()
|
||||||
Layout.addWidget(self.card_Name)
|
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)
|
Layout.addWidget(self.card_AfterAccomplish)
|
||||||
|
|
||||||
self.viewLayout.addLayout(Layout)
|
self.viewLayout.addLayout(Layout)
|
||||||
@@ -463,107 +457,29 @@ class QueueManager(QWidget):
|
|||||||
Layout_2 = QVBoxLayout(widget_2)
|
Layout_2 = QVBoxLayout(widget_2)
|
||||||
Layout = QHBoxLayout()
|
Layout = QHBoxLayout()
|
||||||
|
|
||||||
self.card_Time_0 = TimeEditSettingCard(
|
self.card_dict: Dict[str, TimeEditSettingCard] = {}
|
||||||
icon=FluentIcon.STOP_WATCH,
|
|
||||||
title="定时 1",
|
for i in range(10):
|
||||||
content=None,
|
|
||||||
qconfig=self.config,
|
self.card_dict[f"Time_{i}"] = TimeEditSettingCard(
|
||||||
configItem_bool=self.config.time_TimeEnabled_0,
|
icon=FluentIcon.STOP_WATCH,
|
||||||
configItem_time=self.config.time_TimeSet_0,
|
title=f"定时 {i + 1}",
|
||||||
parent=self,
|
content=None,
|
||||||
)
|
qconfig=self.config,
|
||||||
self.card_Time_1 = TimeEditSettingCard(
|
configItem_bool=self.config.config_item_dict["Time"][
|
||||||
icon=FluentIcon.STOP_WATCH,
|
f"Enabled_{i}"
|
||||||
title="定时 2",
|
],
|
||||||
content=None,
|
configItem_time=self.config.config_item_dict["Time"][
|
||||||
qconfig=self.config,
|
f"Set_{i}"
|
||||||
configItem_bool=self.config.time_TimeEnabled_1,
|
],
|
||||||
configItem_time=self.config.time_TimeSet_1,
|
parent=self,
|
||||||
parent=self,
|
)
|
||||||
)
|
|
||||||
self.card_Time_2 = TimeEditSettingCard(
|
if i < 5:
|
||||||
icon=FluentIcon.STOP_WATCH,
|
Layout_1.addWidget(self.card_dict[f"Time_{i}"])
|
||||||
title="定时 3",
|
else:
|
||||||
content=None,
|
Layout_2.addWidget(self.card_dict[f"Time_{i}"])
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
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_1)
|
||||||
Layout.addWidget(widget_2)
|
Layout.addWidget(widget_2)
|
||||||
|
|
||||||
@@ -577,143 +493,41 @@ class QueueManager(QWidget):
|
|||||||
self.setTitle("任务队列")
|
self.setTitle("任务队列")
|
||||||
self.config = config
|
self.config = config
|
||||||
|
|
||||||
member_list = [
|
script_list = [
|
||||||
["禁用"] + [_ for _ in Config.member_dict.keys()],
|
["禁用"] + [_ for _ in Config.script_dict.keys()],
|
||||||
["未启用"]
|
["未启用"]
|
||||||
+ [
|
+ [
|
||||||
(
|
(
|
||||||
k
|
k
|
||||||
if v["Config"].get(v["Config"].MaaSet_Name) == ""
|
if v["Config"].get_name() == ""
|
||||||
else f"{k} - {v["Config"].get(v["Config"].MaaSet_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(
|
self.card_dict: Dict[
|
||||||
icon=FluentIcon.APPLICATION,
|
str,
|
||||||
title="任务实例 1",
|
NoOptionComboBoxSettingCard,
|
||||||
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,
|
|
||||||
)
|
|
||||||
|
|
||||||
Layout = QVBoxLayout()
|
Layout = QVBoxLayout()
|
||||||
Layout.addWidget(self.card_Member_1)
|
|
||||||
Layout.addWidget(self.card_Member_2)
|
for i in range(10):
|
||||||
Layout.addWidget(self.card_Member_3)
|
|
||||||
Layout.addWidget(self.card_Member_4)
|
self.card_dict[f"Script_{i}"] = NoOptionComboBoxSettingCard(
|
||||||
Layout.addWidget(self.card_Member_5)
|
icon=FluentIcon.APPLICATION,
|
||||||
Layout.addWidget(self.card_Member_6)
|
title=f"任务实例 {i + 1}",
|
||||||
Layout.addWidget(self.card_Member_7)
|
content=f"第{i + 1}个调起的脚本任务实例",
|
||||||
Layout.addWidget(self.card_Member_8)
|
value=script_list[0],
|
||||||
Layout.addWidget(self.card_Member_9)
|
texts=script_list[1],
|
||||||
Layout.addWidget(self.card_Member_10)
|
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)
|
self.viewLayout.addLayout(Layout)
|
||||||
|
|
||||||
class HistoryCard(HeaderCardWidget):
|
|
||||||
|
|
||||||
def __init__(self, name: str, parent=None):
|
|
||||||
super().__init__(parent)
|
|
||||||
self.setTitle("历史运行记录")
|
|
||||||
|
|
||||||
self.text = TextBrowser()
|
|
||||||
self.text.setMinimumHeight(300)
|
|
||||||
history = Config.get_history(name)
|
|
||||||
self.text.setPlainText(history["History"])
|
|
||||||
|
|
||||||
self.viewLayout.addWidget(self.text)
|
|
||||||
|
|||||||
3876
app/ui/script_manager.py
Normal file
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,27 +16,22 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA设置界面
|
AUTO_MAA设置界面
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
from PySide6.QtWidgets import QWidget, QVBoxLayout
|
||||||
from PySide6.QtWidgets import (
|
from PySide6.QtGui import QIcon
|
||||||
QWidget,
|
|
||||||
QApplication,
|
|
||||||
QVBoxLayout,
|
|
||||||
)
|
|
||||||
from PySide6.QtCore import Qt
|
from PySide6.QtCore import Qt
|
||||||
from qfluentwidgets import (
|
from qfluentwidgets import (
|
||||||
ScrollArea,
|
ScrollArea,
|
||||||
FluentIcon,
|
FluentIcon,
|
||||||
MessageBox,
|
MessageBox,
|
||||||
Dialog,
|
|
||||||
HyperlinkCard,
|
HyperlinkCard,
|
||||||
HeaderCardWidget,
|
HeaderCardWidget,
|
||||||
ExpandGroupSettingCard,
|
ExpandGroupSettingCard,
|
||||||
@@ -46,16 +41,16 @@ from qfluentwidgets import (
|
|||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
import time
|
|
||||||
import shutil
|
import shutil
|
||||||
import requests
|
|
||||||
import subprocess
|
import subprocess
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
|
from packaging import version
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Union
|
from typing import Dict, Union
|
||||||
|
|
||||||
from app.core import Config, MainInfoBar
|
from app.core import Config, MainInfoBar, Network, SoundPlayer, logger
|
||||||
from app.services import Crypto, System, Notify
|
from app.services import Crypto, System, Notify
|
||||||
|
from .downloader import DownloadManager
|
||||||
from .Widget import (
|
from .Widget import (
|
||||||
SwitchSettingCard,
|
SwitchSettingCard,
|
||||||
RangeSettingCard,
|
RangeSettingCard,
|
||||||
@@ -74,10 +69,8 @@ class Setting(QWidget):
|
|||||||
super().__init__(parent)
|
super().__init__(parent)
|
||||||
self.setObjectName("设置")
|
self.setObjectName("设置")
|
||||||
|
|
||||||
content_widget = QWidget()
|
|
||||||
content_layout = QVBoxLayout(content_widget)
|
|
||||||
|
|
||||||
self.function = FunctionSettingCard(self)
|
self.function = FunctionSettingCard(self)
|
||||||
|
self.voice = VoiceSettingCard(self)
|
||||||
self.start = StartSettingCard(self)
|
self.start = StartSettingCard(self)
|
||||||
self.ui = UiSettingCard(self)
|
self.ui = UiSettingCard(self)
|
||||||
self.notification = NotifySettingCard(self)
|
self.notification = NotifySettingCard(self)
|
||||||
@@ -92,10 +85,17 @@ class Setting(QWidget):
|
|||||||
)
|
)
|
||||||
self.start.card_IfSelfStart.checkedChanged.connect(System.set_SelfStart)
|
self.start.card_IfSelfStart.checkedChanged.connect(System.set_SelfStart)
|
||||||
self.security.card_changePASSWORD.clicked.connect(self.change_PASSWORD)
|
self.security.card_changePASSWORD.clicked.connect(self.change_PASSWORD)
|
||||||
self.updater.card_CheckUpdate.clicked.connect(self.check_update)
|
self.security.card_resetPASSWORD.clicked.connect(self.reset_PASSWORD)
|
||||||
self.other.card_Notice.clicked.connect(self.show_notice)
|
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.function)
|
||||||
|
content_layout.addWidget(self.voice)
|
||||||
content_layout.addWidget(self.start)
|
content_layout.addWidget(self.start)
|
||||||
content_layout.addWidget(self.ui)
|
content_layout.addWidget(self.ui)
|
||||||
content_layout.addWidget(self.notification)
|
content_layout.addWidget(self.notification)
|
||||||
@@ -105,10 +105,12 @@ class Setting(QWidget):
|
|||||||
|
|
||||||
scrollArea = ScrollArea()
|
scrollArea = ScrollArea()
|
||||||
scrollArea.setWidgetResizable(True)
|
scrollArea.setWidgetResizable(True)
|
||||||
|
scrollArea.setContentsMargins(0, 0, 0, 0)
|
||||||
|
scrollArea.setStyleSheet("background: transparent; border: none;")
|
||||||
scrollArea.setWidget(content_widget)
|
scrollArea.setWidget(content_widget)
|
||||||
layout = QVBoxLayout()
|
|
||||||
|
layout = QVBoxLayout(self)
|
||||||
layout.addWidget(scrollArea)
|
layout.addWidget(scrollArea)
|
||||||
self.setLayout(layout)
|
|
||||||
|
|
||||||
def agree_bilibili(self) -> None:
|
def agree_bilibili(self) -> None:
|
||||||
"""授权bilibili游戏隐私政策"""
|
"""授权bilibili游戏隐私政策"""
|
||||||
@@ -117,11 +119,11 @@ class Setting(QWidget):
|
|||||||
|
|
||||||
choice = MessageBox(
|
choice = MessageBox(
|
||||||
"授权声明",
|
"授权声明",
|
||||||
"开启“托管bilibili游戏隐私政策”功能,即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》,并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
|
"开启「托管bilibili游戏隐私政策」功能,即代表您已完整阅读并同意《哔哩哔哩弹幕网用户使用协议》、《哔哩哔哩隐私政策》和《哔哩哔哩游戏中心用户协议》,并授权AUTO_MAA在其认定需要时以其认定合适的方法替您处理相关弹窗\n\n是否同意授权?",
|
||||||
self.window(),
|
self.window(),
|
||||||
)
|
)
|
||||||
if choice.exec():
|
if choice.exec():
|
||||||
logger.success("确认授权bilibili游戏隐私政策")
|
logger.success("确认授权bilibili游戏隐私政策", module="设置界面")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000
|
"success", "操作成功", "已确认授权bilibili游戏隐私政策", 3000
|
||||||
)
|
)
|
||||||
@@ -129,7 +131,7 @@ class Setting(QWidget):
|
|||||||
Config.set(Config.function_IfAgreeBilibili, False)
|
Config.set(Config.function_IfAgreeBilibili, False)
|
||||||
else:
|
else:
|
||||||
|
|
||||||
logger.info("取消授权bilibili游戏隐私政策")
|
logger.info("取消授权bilibili游戏隐私政策", module="设置界面")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000
|
"info", "操作成功", "已取消授权bilibili游戏隐私政策", 3000
|
||||||
)
|
)
|
||||||
@@ -145,7 +147,7 @@ class Setting(QWidget):
|
|||||||
|
|
||||||
choice = MessageBox(
|
choice = MessageBox(
|
||||||
"风险声明",
|
"风险声明",
|
||||||
"开启“跳过MuMu启动广告”功能,即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告,并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?",
|
"开启「跳过MuMu启动广告」功能,即代表您已安装MuMu模拟器-12且允许AUTO_MAA以其认定合适的方法屏蔽MuMu启动广告,并接受此操作带来的风险\n\n此功能即时生效,是否仍要开启此功能?",
|
||||||
self.window(),
|
self.window(),
|
||||||
)
|
)
|
||||||
if choice.exec():
|
if choice.exec():
|
||||||
@@ -155,7 +157,7 @@ class Setting(QWidget):
|
|||||||
|
|
||||||
MuMu_splash_ads_path.touch()
|
MuMu_splash_ads_path.touch()
|
||||||
|
|
||||||
logger.success("开启跳过MuMu启动广告功能")
|
logger.success("开启跳过MuMu启动广告功能", module="设置界面")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"success", "操作成功", "已开启跳过MuMu启动广告功能", 3000
|
"success", "操作成功", "已开启跳过MuMu启动广告功能", 3000
|
||||||
)
|
)
|
||||||
@@ -167,7 +169,7 @@ class Setting(QWidget):
|
|||||||
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
|
if MuMu_splash_ads_path.exists() and MuMu_splash_ads_path.is_file():
|
||||||
MuMu_splash_ads_path.unlink()
|
MuMu_splash_ads_path.unlink()
|
||||||
|
|
||||||
logger.info("关闭跳过MuMu启动广告功能")
|
logger.info("关闭跳过MuMu启动广告功能", module="设置界面")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"info", "操作成功", "已关闭跳过MuMu启动广告功能", 3000
|
"info", "操作成功", "已关闭跳过MuMu启动广告功能", 3000
|
||||||
)
|
)
|
||||||
@@ -178,16 +180,16 @@ class Setting(QWidget):
|
|||||||
if Config.key_path.exists():
|
if Config.key_path.exists():
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
logger.info("未设置管理密钥,开始要求用户进行设置", module="设置界面")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
|
|
||||||
choice = LineEditMessageBox(
|
choice = LineEditMessageBox(
|
||||||
self.window(),
|
self.window(), "请设置您的管理密钥", "管理密钥", "密码"
|
||||||
"未检测到管理密钥,请设置您的管理密钥",
|
|
||||||
"管理密钥",
|
|
||||||
"密码",
|
|
||||||
)
|
)
|
||||||
if choice.exec() and choice.input.text() != "":
|
if choice.exec() and choice.input.text() != "":
|
||||||
Crypto.get_PASSWORD(choice.input.text())
|
Crypto.get_PASSWORD(choice.input.text())
|
||||||
|
logger.success("成功设置管理密钥", module="设置界面")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
choice = MessageBox(
|
choice = MessageBox(
|
||||||
@@ -207,10 +209,7 @@ class Setting(QWidget):
|
|||||||
while if_change:
|
while if_change:
|
||||||
|
|
||||||
choice = LineEditMessageBox(
|
choice = LineEditMessageBox(
|
||||||
self.window(),
|
self.window(), "请输入旧的管理密钥", "旧管理密钥", "密码"
|
||||||
"请输入旧的管理密钥",
|
|
||||||
"旧管理密钥",
|
|
||||||
"密码",
|
|
||||||
)
|
)
|
||||||
if choice.exec() and choice.input.text() != "":
|
if choice.exec() and choice.input.text() != "":
|
||||||
|
|
||||||
@@ -231,6 +230,7 @@ class Setting(QWidget):
|
|||||||
|
|
||||||
# 修改管理密钥
|
# 修改管理密钥
|
||||||
Crypto.change_PASSWORD(PASSWORD_old, choice.input.text())
|
Crypto.change_PASSWORD(PASSWORD_old, choice.input.text())
|
||||||
|
logger.success("成功修改管理密钥", module="设置界面")
|
||||||
MainInfoBar.push_info_bar(
|
MainInfoBar.push_info_bar(
|
||||||
"success", "操作成功", "管理密钥修改成功", 3000
|
"success", "操作成功", "管理密钥修改成功", 3000
|
||||||
)
|
)
|
||||||
@@ -255,75 +255,141 @@ class Setting(QWidget):
|
|||||||
choice.exec()
|
choice.exec()
|
||||||
else:
|
else:
|
||||||
choice = MessageBox(
|
choice = MessageBox(
|
||||||
"确认",
|
"确认", "您没有输入管理密钥,是否取消修改管理密钥?", self.window()
|
||||||
"您没有输入管理密钥,是否取消修改管理密钥?",
|
|
||||||
self.window(),
|
|
||||||
)
|
)
|
||||||
if choice.exec():
|
if choice.exec():
|
||||||
break
|
break
|
||||||
|
|
||||||
def check_update(self) -> None:
|
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(".")))
|
current_version = list(map(int, Config.VERSION.split(".")))
|
||||||
|
|
||||||
# 从远程服务器获取最新版本信息
|
# 从远程服务器获取最新版本信息
|
||||||
for _ in range(3):
|
network = Network.add_task(
|
||||||
try:
|
mode="get",
|
||||||
response = requests.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)}",
|
||||||
f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaGui¤t_version={version_text(current_version)}&cdk={Crypto.win_decryptor(Config.get(Config.update_MirrorChyanCDK))}&channel={Config.get(Config.update_UpdateType)}"
|
)
|
||||||
)
|
network.loop.exec()
|
||||||
version_info: Dict[str, Union[int, str, Dict[str, str]]] = (
|
network_result = Network.get_result(network)
|
||||||
response.json()
|
if network_result["status_code"] == 200:
|
||||||
)
|
version_info: Dict[str, Union[int, str, Dict[str, str]]] = network_result[
|
||||||
break
|
"response_json"
|
||||||
except Exception as e:
|
]
|
||||||
err = e
|
|
||||||
time.sleep(0.1)
|
|
||||||
else:
|
else:
|
||||||
choice = MessageBox(
|
|
||||||
"错误",
|
if network_result["response_json"]:
|
||||||
f"获取版本信息时出错:\n{err}",
|
|
||||||
self.window(),
|
version_info = network_result["response_json"]
|
||||||
|
|
||||||
|
if version_info["code"] != 0:
|
||||||
|
|
||||||
|
logger.error(
|
||||||
|
f"获取版本信息时出错:{version_info['msg']}", module="设置界面"
|
||||||
|
)
|
||||||
|
|
||||||
|
error_remark_dict = {
|
||||||
|
1001: "获取版本信息的URL参数不正确",
|
||||||
|
7001: "填入的 CDK 已过期",
|
||||||
|
7002: "填入的 CDK 错误",
|
||||||
|
7003: "填入的 CDK 今日下载次数已达上限",
|
||||||
|
7004: "填入的 CDK 类型和待下载的资源不匹配",
|
||||||
|
7005: "填入的 CDK 已被封禁",
|
||||||
|
8001: "对应架构和系统下的资源不存在",
|
||||||
|
8002: "错误的系统参数",
|
||||||
|
8003: "错误的架构参数",
|
||||||
|
8004: "错误的更新通道参数",
|
||||||
|
1: version_info["msg"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if version_info["code"] in error_remark_dict:
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"error",
|
||||||
|
"获取版本信息时出错",
|
||||||
|
error_remark_dict[version_info["code"]],
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"error",
|
||||||
|
"获取版本信息时出错",
|
||||||
|
"意料之外的错误,请及时联系项目组以获取来自 Mirror 酱的技术支持",
|
||||||
|
-1,
|
||||||
|
)
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
logger.warning(
|
||||||
|
f"获取版本信息时出错:{network_result['error_message']}",
|
||||||
|
module="设置界面",
|
||||||
|
)
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"warning",
|
||||||
|
"获取版本信息时出错",
|
||||||
|
f"网络错误:{network_result['status_code']}",
|
||||||
|
5000,
|
||||||
)
|
)
|
||||||
choice.cancelButton.hide()
|
|
||||||
choice.buttonLayout.insertStretch(1)
|
|
||||||
if choice.exec():
|
|
||||||
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
|
return None
|
||||||
|
|
||||||
remote_version = list(
|
remote_version = list(
|
||||||
@@ -335,8 +401,16 @@ class Setting(QWidget):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# 有版本更新
|
if (
|
||||||
if remote_version > current_version:
|
if_show
|
||||||
|
or (
|
||||||
|
not if_show
|
||||||
|
and if_first
|
||||||
|
and not Config.get(Config.function_UnattendedMode)
|
||||||
|
)
|
||||||
|
) and version.parse(version_text(remote_version)) > version.parse(
|
||||||
|
version_text(current_version)
|
||||||
|
):
|
||||||
|
|
||||||
version_info_json: Dict[str, Dict[str, str]] = json.loads(
|
version_info_json: Dict[str, Dict[str, str]] = json.loads(
|
||||||
re.sub(
|
re.sub(
|
||||||
@@ -353,9 +427,11 @@ class Setting(QWidget):
|
|||||||
all_version_info = {}
|
all_version_info = {}
|
||||||
for v_i in [
|
for v_i in [
|
||||||
info
|
info
|
||||||
for version, info in version_info_json.items()
|
for ver, info in version_info_json.items()
|
||||||
if list(map(int, version.split("."))) > current_version
|
if version.parse(version_text(list(map(int, ver.split(".")))))
|
||||||
|
> version.parse(version_text(current_version))
|
||||||
]:
|
]:
|
||||||
|
|
||||||
for key, value in v_i.items():
|
for key, value in v_i.items():
|
||||||
if key in update_version_info:
|
if key in update_version_info:
|
||||||
update_version_info[key] += value.copy()
|
update_version_info[key] += value.copy()
|
||||||
@@ -368,73 +444,144 @@ class Setting(QWidget):
|
|||||||
else:
|
else:
|
||||||
all_version_info[key] = value.copy()
|
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():
|
if choice.exec():
|
||||||
|
|
||||||
with Config.version_path.open(mode="r", encoding="utf-8") as f:
|
if "url" in version_info["data"]:
|
||||||
version_info = json.load(f)
|
download_config = {
|
||||||
version_info["main_version"] = Config.VERSION
|
"mode": "MirrorChyan",
|
||||||
with Config.version_path.open(mode="w", encoding="utf-8") as f:
|
"thread_numb": 1,
|
||||||
json.dump(version_info, f, ensure_ascii=False, indent=4)
|
"url": version_info["data"]["url"],
|
||||||
|
}
|
||||||
if (Config.app_path / "AUTO_Updater.exe").exists():
|
|
||||||
shutil.copy(
|
|
||||||
Config.app_path / "AUTO_Updater.exe",
|
|
||||||
Config.app_path / "AUTO_Updater.active.exe",
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
logger.error("更新器文件不存在")
|
|
||||||
MainInfoBar.push_info_bar(
|
# 从远程服务器获取代理信息
|
||||||
"error", "更新器不存在", "请手动前往 GitHub 获取最新版本", -1
|
network = Network.add_task(
|
||||||
|
mode="get",
|
||||||
|
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/download_info.json",
|
||||||
)
|
)
|
||||||
return None
|
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
|
||||||
|
|
||||||
subprocess.Popen(
|
download_config = {
|
||||||
str(Config.app_path / "AUTO_Updater.active.exe"),
|
"mode": "Proxy",
|
||||||
shell=True,
|
"thread_numb": Config.get(Config.update_ThreadNumb),
|
||||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
"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()
|
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 (
|
||||||
else:
|
if_show
|
||||||
MainInfoBar.push_info_bar("success", "更新检查", "已是最新版本~", 3000)
|
or if_first
|
||||||
|
or version.parse(version_text(remote_version))
|
||||||
|
> version.parse(version_text(current_version))
|
||||||
|
):
|
||||||
|
|
||||||
def show_notice(self, if_show: bool = True):
|
if version.parse(version_text(remote_version)) > version.parse(
|
||||||
|
version_text(current_version)
|
||||||
|
):
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"info",
|
||||||
|
"发现新版本",
|
||||||
|
f"{version_text(current_version)} --> {version_text(remote_version)}",
|
||||||
|
3600000,
|
||||||
|
if_force=True,
|
||||||
|
)
|
||||||
|
SoundPlayer.play("有新版本")
|
||||||
|
else:
|
||||||
|
MainInfoBar.push_info_bar("success", "更新检查", "已是最新版本~", 3000)
|
||||||
|
SoundPlayer.play("无新版本")
|
||||||
|
|
||||||
|
def start_setup(self) -> None:
|
||||||
|
"""启动安装程序"""
|
||||||
|
|
||||||
|
logger.info("启动安装程序", module="设置界面")
|
||||||
|
subprocess.Popen(
|
||||||
|
[
|
||||||
|
Config.app_path / "AUTO_MAA-Setup.exe",
|
||||||
|
"/SP-",
|
||||||
|
"/SILENT",
|
||||||
|
"/NOCANCEL",
|
||||||
|
"/FORCECLOSEAPPLICATIONS",
|
||||||
|
"/LANG=Chinese",
|
||||||
|
f"/DIR={Config.app_path}",
|
||||||
|
],
|
||||||
|
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||||
|
| subprocess.DETACHED_PROCESS
|
||||||
|
| subprocess.CREATE_NO_WINDOW,
|
||||||
|
)
|
||||||
|
System.set_power("KillSelf")
|
||||||
|
|
||||||
|
def show_notice(self, if_show: bool = False, if_first: bool = False) -> None:
|
||||||
"""显示公告"""
|
"""显示公告"""
|
||||||
|
|
||||||
# 从远程服务器获取最新公告
|
# 从远程服务器获取最新公告
|
||||||
for _ in range(3):
|
network = Network.add_task(
|
||||||
try:
|
mode="get",
|
||||||
response = requests.get(
|
url="http://221.236.27.82:10197/d/AUTO_MAA/Server/notice.json",
|
||||||
"https://gitee.com/DLmaster_361/AUTO_MAA/raw/server/notice.json"
|
)
|
||||||
)
|
network.loop.exec()
|
||||||
notice = response.json()
|
network_result = Network.get_result(network)
|
||||||
break
|
if network_result["status_code"] == 200:
|
||||||
except Exception as e:
|
notice = network_result["response_json"]
|
||||||
err = e
|
|
||||||
time.sleep(0.1)
|
|
||||||
else:
|
else:
|
||||||
logger.warning(f"获取最新公告时出错:\n{err}")
|
logger.warning(
|
||||||
if if_show:
|
f"获取最新公告时出错:{network_result['error_message']}",
|
||||||
choice = Dialog(
|
module="设置界面",
|
||||||
"网络错误",
|
)
|
||||||
f"获取最新公告时出错:\n{err}",
|
MainInfoBar.push_info_bar(
|
||||||
self,
|
"warning",
|
||||||
)
|
"获取最新公告时出错",
|
||||||
choice.cancelButton.hide()
|
f"网络错误:{network_result['status_code']}",
|
||||||
choice.buttonLayout.insertStretch(1)
|
5000,
|
||||||
choice.exec()
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
if (Config.app_path / "resources/notice.json").exists():
|
if (Config.app_path / "resources/notice.json").exists():
|
||||||
@@ -454,19 +601,60 @@ class Setting(QWidget):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if if_show or (
|
if if_show or (
|
||||||
datetime.now()
|
if_first
|
||||||
|
and datetime.now()
|
||||||
> datetime.strptime(notice["time"], "%Y-%m-%d %H:%M")
|
> datetime.strptime(notice["time"], "%Y-%m-%d %H:%M")
|
||||||
> time_local
|
> time_local
|
||||||
|
and not Config.get(Config.function_UnattendedMode)
|
||||||
):
|
):
|
||||||
|
|
||||||
choice = NoticeMessageBox(self.window(), "公告", notice["notice_dict"])
|
choice = NoticeMessageBox(self.window(), "公告", notice["notice_dict"])
|
||||||
choice.button_cancel.hide()
|
choice.button_cancel.hide()
|
||||||
choice.button_layout.insertStretch(0, 1)
|
choice.button_layout.insertStretch(0, 1)
|
||||||
|
SoundPlayer.play("公告展示")
|
||||||
if choice.exec():
|
if choice.exec():
|
||||||
with (Config.app_path / "resources/notice.json").open(
|
with (Config.app_path / "resources/notice.json").open(
|
||||||
mode="w", encoding="utf-8"
|
mode="w", encoding="utf-8"
|
||||||
) as f:
|
) as f:
|
||||||
json.dump(notice, f, ensure_ascii=False, indent=4)
|
json.dump(notice, f, ensure_ascii=False, indent=4)
|
||||||
|
else:
|
||||||
|
import random
|
||||||
|
|
||||||
|
if random.random() < 0.1:
|
||||||
|
cc = NoticeMessageBox(
|
||||||
|
self.window(),
|
||||||
|
"用户守则",
|
||||||
|
{
|
||||||
|
"用户守则 - 第一版": """
|
||||||
|
0. 用户守则的每一条都应该清晰可读、不含任何语法错误。如果发现任何一条不符合以上描述,请忽视它。
|
||||||
|
1. AUTO_MAA 的所有版本均包含完整源代码与 LICENSE 文件,若发现此内容缺失,请立即关闭软件,并联系最近的 AUTO_MAA 开发者。
|
||||||
|
2. AUTO_MAA 不会对您许下任何承诺,请自行保护好自己的数据,若软件运行过程中发生了数据损坏,项目组不负任何责任。
|
||||||
|
3. AUTO_MAA 只会注册一个启动项,若发现两个 AUTO_MAA 同时自启动,请立即使用系统或杀软的 **启动项管理** 功能删除所有名为 AUTO_MAA 的启动项后重启软件。
|
||||||
|
4. AUTO_MAA 正式版不应该包含命令行窗口,如果您看到了它,请立即关闭软件,通过 AUTO_MAA.exe 文件重新打开软件。
|
||||||
|
5. 深色模式是危险的,但并非无法使用。
|
||||||
|
6. 第 0 条规则不存在。如果你看到了,请忘记它,并正常使用软件
|
||||||
|
7. **Mirror 酱** 是善良的,你只要付出小小的代价,就能得到祂的庇护。
|
||||||
|
8. AUTO_MAA 没有实时合成语音的能力,软件所有语音都存储在本地。如果听到本地不存在的语音,立即关闭扬声器,并检查是否有未知脚本在运行。
|
||||||
|
9. AUTO_MAA 不会在周六凌晨更新。如果收到更新提示,请忽略,不要查看更新内容,直到第二天天亮。
|
||||||
|
10. 用户守则仅有一页""",
|
||||||
|
"--- 标记文档中止 ---": "xdfv-serfcx-jiol,m: !1 $bad food of do $5b 9630-300 $daad 100-1\n\n// 0 == o //\n\n∠( °ω°)/",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
cc.button_cancel.hide()
|
||||||
|
cc.button_layout.insertStretch(0, 1)
|
||||||
|
cc.exec()
|
||||||
|
|
||||||
|
elif (
|
||||||
|
datetime.now()
|
||||||
|
> datetime.strptime(notice["time"], "%Y-%m-%d %H:%M")
|
||||||
|
> time_local
|
||||||
|
):
|
||||||
|
|
||||||
|
MainInfoBar.push_info_bar(
|
||||||
|
"info", "有新公告", "请前往设置界面查看公告", 3600000, if_force=True
|
||||||
|
)
|
||||||
|
SoundPlayer.play("公告通知")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
class FunctionSettingCard(HeaderCardWidget):
|
class FunctionSettingCard(HeaderCardWidget):
|
||||||
@@ -488,7 +676,7 @@ class FunctionSettingCard(HeaderCardWidget):
|
|||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="历史记录保留时间",
|
title="历史记录保留时间",
|
||||||
content="选择历史记录的保留时间,超期自动清理",
|
content="选择历史记录的保留时间,超期自动清理",
|
||||||
texts=["7 天", "15 天", "30 天", "60 天", "永久"],
|
texts=["7 天", "15 天", "30 天", "60 天", "90 天", "半年", "一年", "永久"],
|
||||||
qconfig=Config,
|
qconfig=Config,
|
||||||
configItem=Config.function_HistoryRetentionTime,
|
configItem=Config.function_HistoryRetentionTime,
|
||||||
parent=self,
|
parent=self,
|
||||||
@@ -502,6 +690,14 @@ class FunctionSettingCard(HeaderCardWidget):
|
|||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
self.card_IfSilence = self.SilenceSettingCard(self)
|
self.card_IfSilence = self.SilenceSettingCard(self)
|
||||||
|
self.card_UnattendedMode = SwitchSettingCard(
|
||||||
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
|
title="无人值守模式",
|
||||||
|
content="开启后AUTO_MAA不再主动弹出对话框,以免影响代理任务运行",
|
||||||
|
qconfig=Config,
|
||||||
|
configItem=Config.function_UnattendedMode,
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
self.card_IfAgreeBilibili = SwitchSettingCard(
|
self.card_IfAgreeBilibili = SwitchSettingCard(
|
||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="托管bilibili游戏隐私政策",
|
title="托管bilibili游戏隐私政策",
|
||||||
@@ -524,6 +720,7 @@ class FunctionSettingCard(HeaderCardWidget):
|
|||||||
Layout.addWidget(self.card_HistoryRetentionTime)
|
Layout.addWidget(self.card_HistoryRetentionTime)
|
||||||
Layout.addWidget(self.card_IfAllowSleep)
|
Layout.addWidget(self.card_IfAllowSleep)
|
||||||
Layout.addWidget(self.card_IfSilence)
|
Layout.addWidget(self.card_IfSilence)
|
||||||
|
Layout.addWidget(self.card_UnattendedMode)
|
||||||
Layout.addWidget(self.card_IfAgreeBilibili)
|
Layout.addWidget(self.card_IfAgreeBilibili)
|
||||||
Layout.addWidget(self.card_IfSkipMumuSplashAds)
|
Layout.addWidget(self.card_IfSkipMumuSplashAds)
|
||||||
self.viewLayout.addLayout(Layout)
|
self.viewLayout.addLayout(Layout)
|
||||||
@@ -549,8 +746,8 @@ class FunctionSettingCard(HeaderCardWidget):
|
|||||||
self.card_BossKey = LineEditSettingCard(
|
self.card_BossKey = LineEditSettingCard(
|
||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="模拟器老板键",
|
title="模拟器老板键",
|
||||||
content="输入模拟器老板快捷键,以“+”分隔",
|
content="请输入对应的模拟器老板键,请直接输入文字,多个键位之间请用「+」隔开。如:「Alt+Q」",
|
||||||
text="请输入安卓模拟器老板键",
|
text="请以文字形式输入模拟器老板快捷键",
|
||||||
qconfig=Config,
|
qconfig=Config,
|
||||||
configItem=Config.function_BossKey,
|
configItem=Config.function_BossKey,
|
||||||
parent=self,
|
parent=self,
|
||||||
@@ -565,6 +762,36 @@ class FunctionSettingCard(HeaderCardWidget):
|
|||||||
self.addGroupWidget(widget)
|
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):
|
class StartSettingCard(HeaderCardWidget):
|
||||||
|
|
||||||
def __init__(self, parent=None):
|
def __init__(self, parent=None):
|
||||||
@@ -579,14 +806,6 @@ class StartSettingCard(HeaderCardWidget):
|
|||||||
configItem=Config.start_IfSelfStart,
|
configItem=Config.start_IfSelfStart,
|
||||||
parent=self,
|
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(
|
self.card_IfMinimizeDirectly = SwitchSettingCard(
|
||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="启动后直接最小化",
|
title="启动后直接最小化",
|
||||||
@@ -598,7 +817,6 @@ class StartSettingCard(HeaderCardWidget):
|
|||||||
|
|
||||||
Layout = QVBoxLayout()
|
Layout = QVBoxLayout()
|
||||||
Layout.addWidget(self.card_IfSelfStart)
|
Layout.addWidget(self.card_IfSelfStart)
|
||||||
Layout.addWidget(self.card_IfRunDirectly)
|
|
||||||
Layout.addWidget(self.card_IfMinimizeDirectly)
|
Layout.addWidget(self.card_IfMinimizeDirectly)
|
||||||
self.viewLayout.addLayout(Layout)
|
self.viewLayout.addLayout(Layout)
|
||||||
|
|
||||||
@@ -830,7 +1048,7 @@ class NotifySettingCard(HeaderCardWidget):
|
|||||||
self.card_ServerChanChannel = LineEditSettingCard(
|
self.card_ServerChanChannel = LineEditSettingCard(
|
||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="ServerChanChannel代码",
|
title="ServerChanChannel代码",
|
||||||
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
|
content="可以留空,留空则默认。可以多个,请使用「|」隔开",
|
||||||
text="请输入需要推送的Channel代码(SCT生效)",
|
text="请输入需要推送的Channel代码(SCT生效)",
|
||||||
qconfig=Config,
|
qconfig=Config,
|
||||||
configItem=Config.notify_ServerChanChannel,
|
configItem=Config.notify_ServerChanChannel,
|
||||||
@@ -839,7 +1057,7 @@ class NotifySettingCard(HeaderCardWidget):
|
|||||||
self.card_ServerChanTag = LineEditSettingCard(
|
self.card_ServerChanTag = LineEditSettingCard(
|
||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="Tag内容",
|
title="Tag内容",
|
||||||
content="可以留空,留空则默认。可以多个,请使用“|”隔开",
|
content="可以留空,留空则默认。可以多个,请使用「|」隔开",
|
||||||
text="请输入加入推送的Tag(SC3生效)",
|
text="请输入加入推送的Tag(SC3生效)",
|
||||||
qconfig=Config,
|
qconfig=Config,
|
||||||
configItem=Config.notify_ServerChanTag,
|
configItem=Config.notify_ServerChanTag,
|
||||||
@@ -905,9 +1123,17 @@ class SecuritySettingCard(HeaderCardWidget):
|
|||||||
content="修改用于解密用户密码的管理密钥",
|
content="修改用于解密用户密码的管理密钥",
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
|
self.card_resetPASSWORD = PushSettingCard(
|
||||||
|
text="重置",
|
||||||
|
icon=FluentIcon.VPN,
|
||||||
|
title="重置管理密钥",
|
||||||
|
content="重置用于解密用户密码的管理密钥",
|
||||||
|
parent=self,
|
||||||
|
)
|
||||||
|
|
||||||
Layout = QVBoxLayout()
|
Layout = QVBoxLayout()
|
||||||
Layout.addWidget(self.card_changePASSWORD)
|
Layout.addWidget(self.card_changePASSWORD)
|
||||||
|
Layout.addWidget(self.card_resetPASSWORD)
|
||||||
self.viewLayout.addLayout(Layout)
|
self.viewLayout.addLayout(Layout)
|
||||||
|
|
||||||
|
|
||||||
@@ -949,6 +1175,15 @@ class UpdaterSettingCard(HeaderCardWidget):
|
|||||||
configItem=Config.update_ThreadNumb,
|
configItem=Config.update_ThreadNumb,
|
||||||
parent=self,
|
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(
|
self.card_ProxyUrlList = UrlListSettingCard(
|
||||||
icon=FluentIcon.SETTING,
|
icon=FluentIcon.SETTING,
|
||||||
title="代理地址列表",
|
title="代理地址列表",
|
||||||
@@ -968,7 +1203,9 @@ class UpdaterSettingCard(HeaderCardWidget):
|
|||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
mirrorchyan_url = HyperlinkButton(
|
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(
|
self.card_MirrorChyanCDK.hBoxLayout.insertWidget(
|
||||||
5, mirrorchyan_url, 0, Qt.AlignRight
|
5, mirrorchyan_url, 0, Qt.AlignRight
|
||||||
@@ -979,6 +1216,7 @@ class UpdaterSettingCard(HeaderCardWidget):
|
|||||||
Layout.addWidget(self.card_IfAutoUpdate)
|
Layout.addWidget(self.card_IfAutoUpdate)
|
||||||
Layout.addWidget(self.card_UpdateType)
|
Layout.addWidget(self.card_UpdateType)
|
||||||
Layout.addWidget(self.card_ThreadNumb)
|
Layout.addWidget(self.card_ThreadNumb)
|
||||||
|
Layout.addWidget(self.card_ProxyAddress)
|
||||||
Layout.addWidget(self.card_ProxyUrlList)
|
Layout.addWidget(self.card_ProxyUrlList)
|
||||||
Layout.addWidget(self.card_MirrorChyanCDK)
|
Layout.addWidget(self.card_MirrorChyanCDK)
|
||||||
self.viewLayout.addLayout(Layout)
|
self.viewLayout.addLayout(Layout)
|
||||||
@@ -999,9 +1237,9 @@ class OtherSettingCard(HeaderCardWidget):
|
|||||||
)
|
)
|
||||||
self.card_UserDocs = HyperlinkCard(
|
self.card_UserDocs = HyperlinkCard(
|
||||||
url="https://clozya.github.io/AUTOMAA_docs",
|
url="https://clozya.github.io/AUTOMAA_docs",
|
||||||
text="访问",
|
text="查看指南",
|
||||||
icon=FluentIcon.PAGE_RIGHT,
|
icon=FluentIcon.PAGE_RIGHT,
|
||||||
title="AUTO_MAA官方文档站",
|
title="用户指南",
|
||||||
content="访问AUTO_MAA的官方文档站,获取使用指南和项目相关信息",
|
content="访问AUTO_MAA的官方文档站,获取使用指南和项目相关信息",
|
||||||
parent=self,
|
parent=self,
|
||||||
)
|
)
|
||||||
|
|||||||
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()
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA工具包
|
AUTO_MAA工具包
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -29,6 +29,7 @@ __version__ = "4.2.0"
|
|||||||
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
__author__ = "DLmaster361 <DLmaster_361@163.com>"
|
||||||
__license__ = "GPL-3.0 license"
|
__license__ = "GPL-3.0 license"
|
||||||
|
|
||||||
from .downloader import DownloadManager
|
from .ImageUtils import ImageUtils
|
||||||
|
from .ProcessManager import ProcessManager
|
||||||
|
|
||||||
__all__ = ["DownloadManager"]
|
__all__ = ["ImageUtils", "ProcessManager"]
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,12 +16,12 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA打包程序
|
AUTO_MAA打包程序
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -66,55 +66,29 @@ if __name__ == "__main__":
|
|||||||
version = json.load(f)
|
version = json.load(f)
|
||||||
|
|
||||||
main_version_numb = list(map(int, version["main_version"].split(".")))
|
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 ...")
|
print("Packaging AUTO_MAA main program ...")
|
||||||
|
|
||||||
os.system(
|
os.system(
|
||||||
"powershell -Command python -m nuitka --standalone --onefile --mingw64"
|
"powershell -Command python -m nuitka --standalone --onefile --mingw64 --windows-uac-admin"
|
||||||
" --enable-plugins=pyside6 --windows-console-mode=disable"
|
" --enable-plugins=pyside6 --windows-console-mode=attach"
|
||||||
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA'"
|
" --onefile-tempdir-spec='{TEMP}\\AUTO_MAA'"
|
||||||
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA.ico"
|
" --windows-icon-from-ico=resources\\icons\\AUTO_MAA.ico"
|
||||||
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
|
" --company-name='AUTO_MAA Team' --product-name=AUTO_MAA"
|
||||||
f" --file-version={version["main_version"]}"
|
f" --file-version={version['main_version']}"
|
||||||
f" --product-version={version["main_version"]}"
|
f" --product-version={version['main_version']}"
|
||||||
" --file-description='AUTO_MAA Component'"
|
" --file-description='AUTO_MAA Component'"
|
||||||
" --copyright='Copyright © 2024 DLmaster361'"
|
" --copyright='Copyright © 2024-2025 DLmaster361'"
|
||||||
" --assume-yes-for-downloads --output-filename=AUTO_MAA"
|
" --assume-yes-for-downloads --output-filename=AUTO_MAA"
|
||||||
" --remove-output main.py"
|
" --remove-output main.py"
|
||||||
)
|
)
|
||||||
|
|
||||||
print("AUTO_MAA main program packaging completed !")
|
print("AUTO_MAA main program packaging completed !")
|
||||||
|
|
||||||
print("Packaging AUTO_MAA update program ...")
|
print("start to create setup 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 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 !")
|
|
||||||
|
|
||||||
(root_path / "AUTO_MAA").mkdir(parents=True, exist_ok=True)
|
(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_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 / "app", root_path / "AUTO_MAA/app")
|
||||||
shutil.copytree(root_path / "resources", root_path / "AUTO_MAA/resources")
|
shutil.copytree(root_path / "resources", root_path / "AUTO_MAA/resources")
|
||||||
shutil.copy(root_path / "main.py", root_path / "AUTO_MAA/")
|
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 / "README.md", root_path / "AUTO_MAA/")
|
||||||
shutil.copy(root_path / "LICENSE", 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(
|
shutil.make_archive(
|
||||||
base_name=root_path / f"AUTO_MAA_{version_text(main_version_numb)}",
|
base_name=root_path / f"AUTO_MAA_{version_text(main_version_numb)}",
|
||||||
format="zip",
|
format="zip",
|
||||||
root_dir=root_path / "AUTO_MAA",
|
root_dir=root_path / "AUTO_MAA_Setup",
|
||||||
base_dir=".",
|
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 = {}
|
all_version_info = {}
|
||||||
for v_i in version["version_info"].values():
|
for v_i in version["version_info"].values():
|
||||||
@@ -143,6 +138,6 @@ if __name__ == "__main__":
|
|||||||
all_version_info[key] = value.copy()
|
all_version_info[key] = value.copy()
|
||||||
|
|
||||||
(root_path / "version_info.txt").write_text(
|
(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",
|
encoding="utf-8",
|
||||||
)
|
)
|
||||||
|
|||||||
56
main.py
@@ -1,5 +1,5 @@
|
|||||||
# <AUTO_MAA:A MAA Multi Account Management and Automation Tool>
|
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||||
# Copyright © <2024> <DLmaster361>
|
# Copyright © 2024-2025 DLmaster361
|
||||||
|
|
||||||
# This file is part of AUTO_MAA.
|
# This file is part of AUTO_MAA.
|
||||||
|
|
||||||
@@ -16,27 +16,55 @@
|
|||||||
# You should have received a copy of the GNU General Public License
|
# You should have received a copy of the GNU General Public License
|
||||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
# DLmaster_361@163.com
|
# Contact: DLmaster_361@163.com
|
||||||
|
|
||||||
"""
|
"""
|
||||||
AUTO_MAA
|
AUTO_MAA
|
||||||
AUTO_MAA主程序
|
AUTO_MAA主程序
|
||||||
v4.3
|
v4.4
|
||||||
作者:DLmaster_361
|
作者:DLmaster_361
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from loguru import logger
|
# 屏蔽广告
|
||||||
from PySide6.QtWidgets import QApplication
|
import builtins
|
||||||
from PySide6.QtCore import Qt
|
|
||||||
from qfluentwidgets import FluentTranslator
|
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 sys
|
||||||
|
import ctypes
|
||||||
|
from PySide6.QtWidgets import QApplication
|
||||||
|
from qfluentwidgets import FluentTranslator
|
||||||
|
|
||||||
|
from app.core.logger import logger
|
||||||
|
|
||||||
|
|
||||||
|
def is_admin() -> bool:
|
||||||
|
"""检查当前程序是否以管理员身份运行"""
|
||||||
|
try:
|
||||||
|
return ctypes.windll.shell32.IsUserAnAdmin()
|
||||||
|
except:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
@logger.catch
|
@logger.catch
|
||||||
def main():
|
def main():
|
||||||
|
|
||||||
application = QApplication(sys.argv)
|
application = QApplication(sys.argv)
|
||||||
QApplication.setAttribute(Qt.AA_DontCreateNativeWidgetSiblings)
|
|
||||||
|
|
||||||
translator = FluentTranslator()
|
translator = FluentTranslator()
|
||||||
application.installTranslator(translator)
|
application.installTranslator(translator)
|
||||||
@@ -44,11 +72,17 @@ def main():
|
|||||||
from app.ui.main_window import AUTO_MAA
|
from app.ui.main_window import AUTO_MAA
|
||||||
|
|
||||||
window = AUTO_MAA()
|
window = AUTO_MAA()
|
||||||
window.show_ui("显示主窗口")
|
window.show_ui("显示主窗口", if_start=True)
|
||||||
window.start_up_task()
|
window.start_up_task()
|
||||||
sys.exit(application.exec())
|
sys.exit(application.exec())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
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
|
loguru==0.7.3
|
||||||
plyer
|
plyer==2.1.0
|
||||||
PySide6
|
PySide6==6.9.1
|
||||||
PySide6-Fluent-Widgets[full]
|
PySide6-Fluent-Widgets[full]==1.8.3
|
||||||
psutil
|
psutil==7.0.0
|
||||||
opencv-python
|
pywin32==310
|
||||||
pywin32
|
keyboard==0.13.5
|
||||||
pyautogui
|
pycryptodome==3.23.0
|
||||||
pycryptodome
|
certifi==2025.4.26
|
||||||
requests
|
truststore==0.10.1
|
||||||
markdown
|
requests==2.32.4
|
||||||
Jinja2
|
markdown==3.8.2
|
||||||
serverchan_sdk
|
Jinja2==3.1.6
|
||||||
nuitka
|
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您要继续吗?
|
||||||
@@ -6,18 +6,29 @@
|
|||||||
"TaskQueue.Recruiting.IsChecked": "True" #自动公招
|
"TaskQueue.Recruiting.IsChecked": "True" #自动公招
|
||||||
"TaskQueue.Base.IsChecked": "True" #基建换班
|
"TaskQueue.Base.IsChecked": "True" #基建换班
|
||||||
"TaskQueue.Combat.IsChecked": "True" #刷理智
|
"TaskQueue.Combat.IsChecked": "True" #刷理智
|
||||||
"TaskQueue.Mission.IsChecked": "True" #领取奖励
|
|
||||||
"TaskQueue.Mall.IsChecked": "True" #获取信用及购物
|
"TaskQueue.Mall.IsChecked": "True" #获取信用及购物
|
||||||
|
"TaskQueue.Mission.IsChecked": "True" #领取奖励
|
||||||
"TaskQueue.AutoRoguelike.IsChecked": "False" #自动肉鸽
|
"TaskQueue.AutoRoguelike.IsChecked": "False" #自动肉鸽
|
||||||
"TaskQueue.Reclamation.IsChecked": "False" #生息演算
|
"TaskQueue.Reclamation.IsChecked": "False" #生息演算
|
||||||
|
"TaskQueue.Order.WakeUp": "0"
|
||||||
|
"TaskQueue.Order.Recruiting": "1"
|
||||||
|
"TaskQueue.Order.Base": "2"
|
||||||
|
"TaskQueue.Order.Combat": "3"
|
||||||
|
"TaskQueue.Order.Mall": "4"
|
||||||
|
"TaskQueue.Order.Mission": "5"
|
||||||
|
"TaskQueue.Order.AutoRoguelike": "6"
|
||||||
|
"TaskQueue.Order.Reclamation": "7"
|
||||||
#刷理智
|
#刷理智
|
||||||
"MainFunction.UseMedicine": "True" #吃理智药
|
"MainFunction.UseMedicine": "True" #吃理智药
|
||||||
"MainFunction.UseMedicine.Quantity": "999" #吃理智药数量
|
"MainFunction.UseMedicine.Quantity": "999" #吃理智药数量
|
||||||
"MainFunction.Stage1": "" #主关卡
|
"MainFunction.Stage1": "" #主关卡
|
||||||
"MainFunction.Stage2": "" #备选关卡1
|
"MainFunction.Stage2": "" #备选关卡1
|
||||||
"MainFunction.Stage3": "" #备选关卡2
|
"MainFunction.Stage3": "" #备选关卡2
|
||||||
|
"MainFunction.Stage4": "" #备选关卡3
|
||||||
"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡
|
"Fight.RemainingSanityStage": "Annihilation" #剩余理智关卡
|
||||||
"MainFunction.Series.Quantity": "1" #连战次数
|
"MainFunction.Series.Quantity": "1" #连战次数
|
||||||
|
"MainFunction.Annihilation.UseCustom": "True" #自定义剿灭关卡
|
||||||
|
"MainFunction.Annihilation.Stage": "Annihilation"、"Chernobog@Annihilation"、"LungmenOutskirts@Annihilation"、"LungmenDowntown@Annihilation" #自定义剿灭关卡号
|
||||||
"Penguin.IsDrGrandet": "True" #博朗台模式
|
"Penguin.IsDrGrandet": "True" #博朗台模式
|
||||||
"GUI.CustomStageCode": "False" #手动输入关卡名
|
"GUI.CustomStageCode": "False" #手动输入关卡名
|
||||||
"GUI.UseAlternateStage": "False" #使用备选关卡
|
"GUI.UseAlternateStage": "False" #使用备选关卡
|
||||||
@@ -30,14 +41,16 @@
|
|||||||
"Penguin.EnablePenguin": "True" #上报企鹅物流
|
"Penguin.EnablePenguin": "True" #上报企鹅物流
|
||||||
"Yituliu.EnableYituliu": "True" #上报一图流
|
"Yituliu.EnableYituliu": "True" #上报一图流
|
||||||
#基建换班
|
#基建换班
|
||||||
"Infrast.CustomInfrastEnabled": "True" #启用自定义基建配置
|
"Infrast.InfrastMode": "Normal"、"Rotation"、"Custom" #基建模式
|
||||||
"Infrast.CustomInfrastPlanIndex": "1" #自定义基建配置索引
|
"Infrast.CustomInfrastPlanIndex": "1" #自定义基建配置索引号
|
||||||
"Infrast.DefaultInfrast": "user_defined" #内置配置
|
"Infrast.DefaultInfrast": "user_defined" #内置配置
|
||||||
"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读
|
"Infrast.IsCustomInfrastFileReadOnly": "False" #自定义基建配置文件只读
|
||||||
"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址
|
"Infrast.CustomInfrastFile": "" #自定义基建配置文件地址
|
||||||
#设置
|
#设置
|
||||||
"Start.ClientType": "Bilibili"、 "Official" #服务器
|
"Start.ClientType": "Official"、"Bilibili"、"YoStarEN"、"YoStarJP"、"YoStarKR"、"txwy" #服务器
|
||||||
G"Timer.Timer1": "False" #时间设置1
|
G"Timer.Timer1": "False" #时间设置1
|
||||||
|
"Connect.AdbPath" #ADB路径
|
||||||
|
"Connect.Address": "127.0.0.1:16448" #连接地址
|
||||||
G"VersionUpdate.ScheduledUpdateCheck": "True" #定时检查更新
|
G"VersionUpdate.ScheduledUpdateCheck": "True" #定时检查更新
|
||||||
G"VersionUpdate.AutoDownloadUpdatePackage": "True" #自动下载更新包
|
G"VersionUpdate.AutoDownloadUpdatePackage": "True" #自动下载更新包
|
||||||
G"VersionUpdate.AutoInstallUpdatePackage": "True" #自动安装更新包
|
G"VersionUpdate.AutoInstallUpdatePackage": "True" #自动安装更新包
|
||||||
@@ -47,4 +60,6 @@ G"Start.MinimizeDirectly": "True" #启动MAA后直接最小化
|
|||||||
G"GUI.UseTray": "True" #显示托盘图标
|
G"GUI.UseTray": "True" #显示托盘图标
|
||||||
G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘
|
G"GUI.MinimizeToTray": "False" #最小化时隐藏至托盘
|
||||||
"Start.EmulatorPath" #模拟器路径
|
"Start.EmulatorPath" #模拟器路径
|
||||||
"Connect.AdbPath" #ADB路径
|
"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 |
|
Before Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 21 KiB |
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 |