Merge remote-tracking branch 'origin/feature/refactor' into feature/refactor
This commit is contained in:
@@ -28,6 +28,34 @@ from app.models.schema import *
|
||||
router = APIRouter(prefix="/api/info", tags=["信息获取"])
|
||||
|
||||
|
||||
@router.post(
|
||||
"/version",
|
||||
summary="获取后端git版本信息",
|
||||
response_model=VersionOut,
|
||||
status_code=200,
|
||||
)
|
||||
async def get_git_version() -> VersionOut:
|
||||
|
||||
try:
|
||||
is_latest, commit_hash, commit_time = await Config.get_git_version()
|
||||
except Exception as e:
|
||||
return VersionOut(
|
||||
code=500,
|
||||
status="error",
|
||||
message=f"{type(e).__name__}: {str(e)}",
|
||||
if_latest=False,
|
||||
current_hash="",
|
||||
current_time="",
|
||||
current_version="",
|
||||
)
|
||||
return VersionOut(
|
||||
if_latest=is_latest,
|
||||
current_hash=commit_hash,
|
||||
current_time=commit_time,
|
||||
current_version=Config.version(),
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/combox/stage",
|
||||
summary="获取关卡号下拉框信息",
|
||||
|
||||
@@ -28,11 +28,11 @@ import sqlite3
|
||||
import calendar
|
||||
import requests
|
||||
import truststore
|
||||
from git import Repo
|
||||
from pathlib import Path
|
||||
from fastapi import WebSocket
|
||||
from urllib.parse import quote
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, date, timezone
|
||||
from datetime import datetime, timedelta, date
|
||||
from typing import Literal, Optional
|
||||
|
||||
from app.models.ConfigBase import *
|
||||
@@ -597,6 +597,7 @@ class AppConfig(GlobalConfig):
|
||||
self.config_path.mkdir(parents=True, exist_ok=True)
|
||||
self.history_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.repo = Repo(Path.cwd())
|
||||
self.server: Optional[uvicorn.Server] = None
|
||||
self.websocket: Optional[WebSocket] = None
|
||||
self.silence_dict: Dict[Path, datetime] = {}
|
||||
@@ -908,6 +909,26 @@ class AppConfig(GlobalConfig):
|
||||
else:
|
||||
await Config.websocket.send_json(data)
|
||||
|
||||
async def get_git_version(self) -> tuple[bool, str, str]:
|
||||
|
||||
# 获取当前 commit
|
||||
current_commit = self.repo.head.commit
|
||||
|
||||
# 获取 commit 哈希
|
||||
commit_hash = current_commit.hexsha
|
||||
|
||||
# 获取 commit 时间
|
||||
commit_time = datetime.fromtimestamp(current_commit.committed_date)
|
||||
|
||||
# 检查是否为最新 commit
|
||||
# 获取远程分支的最新 commit
|
||||
origin = self.repo.remotes.origin
|
||||
origin.fetch() # 拉取最新信息
|
||||
remote_commit = self.repo.commit(f"origin/{self.repo.active_branch.name}")
|
||||
is_latest = bool(current_commit.hexsha == remote_commit.hexsha)
|
||||
|
||||
return is_latest, commit_hash, commit_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
async def add_script(
|
||||
self, script: Literal["MAA", "General"]
|
||||
) -> tuple[uuid.UUID, ConfigBase]:
|
||||
|
||||
@@ -34,6 +34,13 @@ class InfoOut(OutBase):
|
||||
data: Dict[str, Any] = Field(..., description="收到的服务器数据")
|
||||
|
||||
|
||||
class VersionOut(OutBase):
|
||||
if_latest: bool = Field(..., description="后端代码是否为最新")
|
||||
current_hash: str = Field(..., description="后端代码当前哈希值")
|
||||
current_time: str = Field(..., description="后端代码当前时间戳")
|
||||
current_version: str = Field(..., description="后端当前版本号")
|
||||
|
||||
|
||||
class NoticeOut(OutBase):
|
||||
if_need_show: bool = Field(..., description="是否需要显示公告")
|
||||
data: Dict[str, str] = Field(
|
||||
|
||||
@@ -22,6 +22,7 @@
|
||||
import re
|
||||
import time
|
||||
import json
|
||||
import asyncio
|
||||
import zipfile
|
||||
import requests
|
||||
import subprocess
|
||||
@@ -209,7 +210,7 @@ class _UpdateHandler:
|
||||
f"连接失败: {download_url}, 状态码: {response.status_code}, 剩余重试次数: {check_times}"
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
continue
|
||||
|
||||
logger.info(f"连接成功: {download_url}, 状态码: {response.status_code}")
|
||||
@@ -225,17 +226,6 @@ class _UpdateHandler:
|
||||
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Update",
|
||||
data={
|
||||
"downloaded_size": downloaded_size,
|
||||
"file_size": file_size,
|
||||
"speed": speed,
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
# 更新指定线程的下载进度, 每秒更新一次
|
||||
if time.time() - last_time >= 1.0:
|
||||
@@ -247,6 +237,18 @@ class _UpdateHandler:
|
||||
last_download_size = downloaded_size
|
||||
last_time = time.time()
|
||||
|
||||
await Config.send_json(
|
||||
WebSocketMessage(
|
||||
id="Update",
|
||||
type="Update",
|
||||
data={
|
||||
"downloaded_size": downloaded_size,
|
||||
"file_size": file_size,
|
||||
"speed": speed,
|
||||
},
|
||||
).model_dump()
|
||||
)
|
||||
|
||||
(Path.cwd() / "download.temp").rename(
|
||||
Path.cwd() / f"UpdatePack_{self.remote_version}.zip"
|
||||
)
|
||||
@@ -276,7 +278,7 @@ class _UpdateHandler:
|
||||
logger.info(
|
||||
f"下载出错: {download_url}, 错误信息: {e}, 剩余重试次数: {check_times}"
|
||||
)
|
||||
time.sleep(1)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
else:
|
||||
|
||||
|
||||
@@ -249,7 +249,7 @@ async def skland_sign_in(token) -> dict:
|
||||
f"{character.get("nickName")}({character.get("channelName")})"
|
||||
)
|
||||
|
||||
time.sleep(3)
|
||||
await asyncio.sleep(3)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"version": "1.0.1",
|
||||
"main": "dist-electron/main.js",
|
||||
"scripts": {
|
||||
"dev": "concurrently \"vite\" \"yarn watch:main\" \"yarn electron-dev\"",
|
||||
|
||||
@@ -98,6 +98,8 @@ export type { TimeSetGetOut } from './models/TimeSetGetOut';
|
||||
export type { TimeSetIndexItem } from './models/TimeSetIndexItem';
|
||||
export type { TimeSetReorderIn } from './models/TimeSetReorderIn';
|
||||
export type { TimeSetUpdateIn } from './models/TimeSetUpdateIn';
|
||||
export type { UpdateCheckIn } from './models/UpdateCheckIn';
|
||||
export type { UpdateCheckOut } from './models/UpdateCheckOut';
|
||||
export type { UserConfig_Notify } from './models/UserConfig_Notify';
|
||||
export type { UserCreateOut } from './models/UserCreateOut';
|
||||
export type { UserDeleteIn } from './models/UserDeleteIn';
|
||||
@@ -109,5 +111,6 @@ export type { UserReorderIn } from './models/UserReorderIn';
|
||||
export type { UserSetIn } from './models/UserSetIn';
|
||||
export type { UserUpdateIn } from './models/UserUpdateIn';
|
||||
export type { ValidationError } from './models/ValidationError';
|
||||
export type { VersionOut } from './models/VersionOut';
|
||||
|
||||
export { Service } from './services/Service';
|
||||
|
||||
@@ -3,26 +3,25 @@
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type HistoryIndexItem = {
|
||||
/**
|
||||
* 日期
|
||||
*/
|
||||
date: string;
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
status: HistoryIndexItem.status;
|
||||
/**
|
||||
* 对应JSON文件
|
||||
*/
|
||||
jsonFile: string;
|
||||
};
|
||||
export namespace HistoryIndexItem {
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
export enum status {
|
||||
DONE = '完成',
|
||||
ERROR = '异常',
|
||||
}
|
||||
/**
|
||||
* 日期
|
||||
*/
|
||||
date: string
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
status: HistoryIndexItem.status
|
||||
/**
|
||||
* 对应JSON文件
|
||||
*/
|
||||
jsonFile: string
|
||||
}
|
||||
export namespace HistoryIndexItem {
|
||||
/**
|
||||
* 状态
|
||||
*/
|
||||
export enum status {
|
||||
DONE = '完成',
|
||||
ERROR = '异常',
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
11
frontend/src/api/models/UpdateCheckIn.ts
Normal file
11
frontend/src/api/models/UpdateCheckIn.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateCheckIn = {
|
||||
/**
|
||||
* 当前前端版本号
|
||||
*/
|
||||
current_version: string;
|
||||
};
|
||||
|
||||
31
frontend/src/api/models/UpdateCheckOut.ts
Normal file
31
frontend/src/api/models/UpdateCheckOut.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type UpdateCheckOut = {
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
code?: number;
|
||||
/**
|
||||
* 操作状态
|
||||
*/
|
||||
status?: string;
|
||||
/**
|
||||
* 操作消息
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* 是否需要更新前端
|
||||
*/
|
||||
if_need_update: boolean;
|
||||
/**
|
||||
* 最新前端版本号
|
||||
*/
|
||||
latest_version: string;
|
||||
/**
|
||||
* 版本更新信息字典
|
||||
*/
|
||||
update_info: Record<string, Array<string>>;
|
||||
};
|
||||
|
||||
35
frontend/src/api/models/VersionOut.ts
Normal file
35
frontend/src/api/models/VersionOut.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
/* generated using openapi-typescript-codegen -- do not edit */
|
||||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type VersionOut = {
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
code?: number;
|
||||
/**
|
||||
* 操作状态
|
||||
*/
|
||||
status?: string;
|
||||
/**
|
||||
* 操作消息
|
||||
*/
|
||||
message?: string;
|
||||
/**
|
||||
* 后端代码是否为最新
|
||||
*/
|
||||
if_latest: boolean;
|
||||
/**
|
||||
* 后端代码当前哈希值
|
||||
*/
|
||||
current_hash: string;
|
||||
/**
|
||||
* 后端代码当前时间戳
|
||||
*/
|
||||
current_time: string;
|
||||
/**
|
||||
* 后端当前版本号
|
||||
*/
|
||||
current_version: string;
|
||||
};
|
||||
|
||||
@@ -53,6 +53,8 @@ import type { TimeSetGetIn } from '../models/TimeSetGetIn';
|
||||
import type { TimeSetGetOut } from '../models/TimeSetGetOut';
|
||||
import type { TimeSetReorderIn } from '../models/TimeSetReorderIn';
|
||||
import type { TimeSetUpdateIn } from '../models/TimeSetUpdateIn';
|
||||
import type { UpdateCheckIn } from '../models/UpdateCheckIn';
|
||||
import type { UpdateCheckOut } from '../models/UpdateCheckOut';
|
||||
import type { UserCreateOut } from '../models/UserCreateOut';
|
||||
import type { UserDeleteIn } from '../models/UserDeleteIn';
|
||||
import type { UserGetIn } from '../models/UserGetIn';
|
||||
@@ -61,10 +63,22 @@ import type { UserInBase } from '../models/UserInBase';
|
||||
import type { UserReorderIn } from '../models/UserReorderIn';
|
||||
import type { UserSetIn } from '../models/UserSetIn';
|
||||
import type { UserUpdateIn } from '../models/UserUpdateIn';
|
||||
import type { VersionOut } from '../models/VersionOut';
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
export class Service {
|
||||
/**
|
||||
* 获取后端git版本信息
|
||||
* @returns VersionOut Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getGitVersionApiInfoVersionPost(): CancelablePromise<VersionOut> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/info/version',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 获取关卡号下拉框信息
|
||||
* @param requestBody
|
||||
@@ -956,4 +970,45 @@ export class Service {
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 检查更新
|
||||
* @param requestBody
|
||||
* @returns UpdateCheckOut Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static checkUpdateApiUpdateCheckPost(
|
||||
requestBody: UpdateCheckIn,
|
||||
): CancelablePromise<UpdateCheckOut> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/update/check',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
422: `Validation Error`,
|
||||
},
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 下载更新
|
||||
* @returns OutBase Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static downloadUpdateApiUpdateDownloadPost(): CancelablePromise<OutBase> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/update/download',
|
||||
});
|
||||
}
|
||||
/**
|
||||
* 安装更新
|
||||
* @returns OutBase Successful Response
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static installUpdateApiUpdateInstallPost(): CancelablePromise<OutBase> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/api/update/install',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
<span class="logo-glow" aria-hidden="true"></span>
|
||||
<img src="@/assets/AUTO-MAS.ico" alt="AUTO-MAS" class="title-logo" />
|
||||
<span class="title-text">AUTO-MAS</span>
|
||||
<span class="version-text">
|
||||
v{{ version }}
|
||||
<span v-if="updateInfo?.if_need_update" class="update-hint" :title="getUpdateTooltip()">
|
||||
检测到更新 {{ updateInfo.latest_version }} 请尽快更新
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,26 +22,18 @@
|
||||
<!-- 右侧:窗口控制按钮 -->
|
||||
<div class="title-bar-right">
|
||||
<div class="window-controls">
|
||||
<button
|
||||
class="control-button minimize-button"
|
||||
@click="minimizeWindow"
|
||||
title="最小化"
|
||||
>
|
||||
<button class="control-button minimize-button" @click="minimizeWindow" title="最小化">
|
||||
<MinusOutlined />
|
||||
</button>
|
||||
<button
|
||||
class="control-button maximize-button"
|
||||
<button
|
||||
class="control-button maximize-button"
|
||||
@click="toggleMaximize"
|
||||
:title="isMaximized ? '还原' : '最大化'"
|
||||
>
|
||||
<BorderOutlined v-if="!isMaximized" />
|
||||
<CopyOutlined v-else />
|
||||
</button>
|
||||
<button
|
||||
class="control-button close-button"
|
||||
@click="closeWindow"
|
||||
title="关闭"
|
||||
>
|
||||
<button class="control-button close-button" @click="closeWindow" title="关闭">
|
||||
<CloseOutlined />
|
||||
</button>
|
||||
</div>
|
||||
@@ -47,10 +45,47 @@
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { MinusOutlined, BorderOutlined, CopyOutlined, CloseOutlined } from '@ant-design/icons-vue'
|
||||
import { useTheme } from '@/composables/useTheme'
|
||||
import { Service } from '@/api'
|
||||
import type { UpdateCheckOut } from '@/api'
|
||||
|
||||
const { isDark } = useTheme()
|
||||
const isMaximized = ref(false)
|
||||
|
||||
// 使用 import.meta.env 或直接定义版本号,确保打包后可用
|
||||
const version = import.meta.env.VITE_APP_VERSION || '获取版本失败!'
|
||||
const updateInfo = ref<UpdateCheckOut | null>(null)
|
||||
|
||||
// 获取是否有更新
|
||||
const getAppVersion = async () => {
|
||||
try {
|
||||
const ver = await Service.checkUpdateApiUpdateCheckPost({
|
||||
current_version: version,
|
||||
})
|
||||
updateInfo.value = ver
|
||||
return ver || '获取版本失败!'
|
||||
} catch (error) {
|
||||
console.error('Failed to get app version:', error)
|
||||
return '获取版本失败!'
|
||||
}
|
||||
}
|
||||
|
||||
// 生成更新提示的详细信息
|
||||
const getUpdateTooltip = () => {
|
||||
if (!updateInfo.value?.update_info) return ''
|
||||
|
||||
const updateDetails = []
|
||||
for (const [category, items] of Object.entries(updateInfo.value.update_info)) {
|
||||
if (items && items.length > 0) {
|
||||
updateDetails.push(`${category}:`)
|
||||
items.forEach(item => {
|
||||
updateDetails.push(`• ${item}`)
|
||||
})
|
||||
updateDetails.push('')
|
||||
}
|
||||
}
|
||||
return updateDetails.join('\n')
|
||||
}
|
||||
|
||||
const minimizeWindow = async () => {
|
||||
try {
|
||||
await window.electronAPI?.windowMinimize()
|
||||
@@ -62,7 +97,7 @@ const minimizeWindow = async () => {
|
||||
const toggleMaximize = async () => {
|
||||
try {
|
||||
await window.electronAPI?.windowMaximize()
|
||||
isMaximized.value = await window.electronAPI?.windowIsMaximized() || false
|
||||
isMaximized.value = (await window.electronAPI?.windowIsMaximized()) || false
|
||||
} catch (error) {
|
||||
console.error('Failed to toggle maximize:', error)
|
||||
}
|
||||
@@ -78,10 +113,11 @@ const closeWindow = async () => {
|
||||
|
||||
onMounted(async () => {
|
||||
try {
|
||||
isMaximized.value = await window.electronAPI?.windowIsMaximized() || false
|
||||
isMaximized.value = (await window.electronAPI?.windowIsMaximized()) || false
|
||||
} catch (error) {
|
||||
console.error('Failed to get window state:', error)
|
||||
}
|
||||
await getAppVersion()
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -124,11 +160,11 @@ onMounted(async () => {
|
||||
left: 55px; /* 调整:更贴近图标 */
|
||||
top: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
width: 200px; /* 缩小尺寸以适配 32px 高度 */
|
||||
width: 200px; /* 缩小尺寸以适配 32px 高度 */
|
||||
height: 100px;
|
||||
pointer-events: none;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0,0,0,0) 70%);
|
||||
background: radial-gradient(circle at 50% 50%, var(--ant-color-primary) 0%, rgba(0, 0, 0, 0) 70%);
|
||||
filter: blur(24px); /* 降低模糊避免越界过多 */
|
||||
opacity: 0.4;
|
||||
z-index: 0;
|
||||
@@ -153,10 +189,23 @@ onMounted(async () => {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.version-text {
|
||||
font-size: 13px;
|
||||
font-weight: 400;
|
||||
opacity: 0.8;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
||||
.title-bar-dark .title-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.title-bar-dark .version-text {
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
.title-bar-center {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
@@ -218,4 +267,79 @@ onMounted(async () => {
|
||||
.title-bar-dark .maximize-button:hover {
|
||||
background: rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
</style>
|
||||
|
||||
.update-hint {
|
||||
font-weight: 500;
|
||||
margin-left: 4px;
|
||||
cursor: help;
|
||||
background: linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #8b00ff, #ff0000);
|
||||
background-size: 400% 400%;
|
||||
-webkit-background-clip: text;
|
||||
background-clip: text;
|
||||
-webkit-text-fill-color: transparent;
|
||||
animation:
|
||||
rainbow-flow 3s ease-in-out infinite,
|
||||
glow-pulse 2s ease-in-out infinite;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.update-hint::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
left: -2px;
|
||||
right: -2px;
|
||||
bottom: -2px;
|
||||
background: linear-gradient(45deg, #ff0000, #ff7f00, #ffff00, #00ff00, #8b00ff, #ff0000);
|
||||
background-size: 400% 400%;
|
||||
border-radius: 4px;
|
||||
z-index: -1;
|
||||
opacity: 0.3;
|
||||
filter: blur(8px);
|
||||
animation: rainbow-flow 3s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.title-bar-dark .update-hint::before {
|
||||
opacity: 0.5;
|
||||
filter: blur(10px);
|
||||
}
|
||||
|
||||
@keyframes rainbow-flow {
|
||||
0% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
50% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0% 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes glow-pulse {
|
||||
0% {
|
||||
filter: brightness(1) saturate(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
50% {
|
||||
filter: brightness(1.2) saturate(1.3);
|
||||
transform: scale(1.02);
|
||||
}
|
||||
100% {
|
||||
filter: brightness(1) saturate(1);
|
||||
transform: scale(1);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.7;
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -5,9 +5,18 @@ export interface ElectronAPI {
|
||||
selectFolder: () => Promise<string | null>
|
||||
selectFile: (filters?: Array<{ name: string; extensions: string[] }>) => Promise<string | null>
|
||||
|
||||
// 窗口控制
|
||||
windowMinimize: () => Promise<void>
|
||||
windowMaximize: () => Promise<void>
|
||||
windowClose: () => Promise<void>
|
||||
windowIsMaximized: () => Promise<boolean>
|
||||
|
||||
// 管理员权限检查
|
||||
checkAdmin: () => Promise<boolean>
|
||||
|
||||
// 重启为管理员
|
||||
restartAsAdmin: () => Promise<void>
|
||||
|
||||
// 环境检查
|
||||
checkEnvironment: () => Promise<{
|
||||
pythonExists: boolean
|
||||
@@ -53,7 +62,6 @@ export interface ElectronAPI {
|
||||
callback: (progress: { progress: number; status: string; message: string }) => void
|
||||
) => void
|
||||
removeDownloadProgressListener: () => void
|
||||
restartAsAdmin: () => Promise<void>
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -2,6 +2,9 @@ import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import path from 'path'
|
||||
|
||||
// 读取package.json中的版本号
|
||||
const packageJson = require('./package.json')
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue()],
|
||||
@@ -12,4 +15,8 @@ export default defineConfig({
|
||||
'@': path.resolve(__dirname, './src'),
|
||||
},
|
||||
},
|
||||
define: {
|
||||
// 在编译时将版本号注入到环境变量中
|
||||
'import.meta.env.VITE_APP_VERSION': JSON.stringify(packageJson.version)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -12,6 +12,7 @@ dependencies = [
|
||||
"websockets==15.0.1",
|
||||
"aiofiles==24.1.0",
|
||||
"aiohttp==3.12.15",
|
||||
"gitpython==3.1.45",
|
||||
"plyer==2.1.0",
|
||||
"psutil==7.0.0",
|
||||
"jinja2==3.1.6",
|
||||
|
||||
@@ -5,6 +5,7 @@ uvicorn==0.35.0
|
||||
websockets==15.0.1
|
||||
aiofiles==24.1.0
|
||||
aiohttp==3.12.15
|
||||
gitpython==3.1.45
|
||||
plyer==2.1.0
|
||||
psutil==7.0.0
|
||||
jinja2==3.1.6
|
||||
|
||||
379
updater.py
379
updater.py
@@ -1,379 +0,0 @@
|
||||
# AUTO_MAA:A MAA Multi Account Management and Automation Tool
|
||||
# Copyright © 2024-2025 DLmaster361
|
||||
|
||||
# This file is part of AUTO_MAA.
|
||||
|
||||
# AUTO_MAA is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published
|
||||
# by the Free Software Foundation, either version 3 of the License,
|
||||
# or (at your option) any later version.
|
||||
|
||||
# AUTO_MAA is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty
|
||||
# of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
||||
# the GNU General Public License for more details.
|
||||
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with AUTO_MAA. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
# Contact: DLmaster_361@163.com
|
||||
|
||||
|
||||
import re
|
||||
import sys
|
||||
import time
|
||||
import json
|
||||
import psutil
|
||||
import base64
|
||||
import zipfile
|
||||
import requests
|
||||
import argparse
|
||||
import truststore
|
||||
import subprocess
|
||||
import win32crypt
|
||||
|
||||
from packaging import version
|
||||
from pathlib import Path
|
||||
from typing import List, Dict
|
||||
|
||||
current_dir = Path(__file__).resolve().parent
|
||||
if str(current_dir) not in sys.path:
|
||||
sys.path.insert(0, str(current_dir))
|
||||
|
||||
|
||||
MIRROR_ERROR_INFO = {
|
||||
1001: "获取版本信息的URL参数不正确",
|
||||
7001: "填入的 CDK 已过期",
|
||||
7002: "填入的 CDK 错误",
|
||||
7003: "填入的 CDK 今日下载次数已达上限",
|
||||
7004: "填入的 CDK 类型和待下载的资源不匹配",
|
||||
7005: "填入的 CDK 已被封禁",
|
||||
8001: "对应架构和系统下的资源不存在",
|
||||
8002: "错误的系统参数",
|
||||
8003: "错误的架构参数",
|
||||
8004: "错误的更新通道参数",
|
||||
1: "未知错误类型",
|
||||
}
|
||||
|
||||
|
||||
def dpapi_decrypt(note: str, entropy: None | bytes = None) -> str:
|
||||
"""
|
||||
使用Windows DPAPI解密数据
|
||||
|
||||
:param note: 数据密文
|
||||
:type note: str
|
||||
:param entropy: 随机熵
|
||||
:type entropy: bytes
|
||||
:return: 解密后的明文
|
||||
:rtype: str
|
||||
"""
|
||||
|
||||
if note == "":
|
||||
return ""
|
||||
|
||||
decrypted = win32crypt.CryptUnprotectData(
|
||||
base64.b64decode(note), entropy, None, None, 0
|
||||
)
|
||||
return decrypted[1].decode("utf-8")
|
||||
|
||||
|
||||
def kill_process(path: Path) -> None:
|
||||
"""
|
||||
根据路径中止进程
|
||||
|
||||
:param path: 进程路径
|
||||
"""
|
||||
|
||||
print(f"开始中止进程: {path}")
|
||||
|
||||
for pid in search_pids(path):
|
||||
killprocess = subprocess.Popen(
|
||||
f"taskkill /F /T /PID {pid}",
|
||||
shell=True,
|
||||
creationflags=subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
killprocess.wait()
|
||||
|
||||
print(f"进程已中止: {path}")
|
||||
|
||||
|
||||
def search_pids(path: Path) -> list:
|
||||
"""
|
||||
根据路径查找进程PID
|
||||
|
||||
:param path: 进程路径
|
||||
:return: 匹配的进程PID列表
|
||||
"""
|
||||
|
||||
print(f"开始查找进程 PID: {path}")
|
||||
|
||||
pids = []
|
||||
for proc in psutil.process_iter(["pid", "exe"]):
|
||||
try:
|
||||
if proc.info["exe"] and proc.info["exe"].lower() == str(path).lower():
|
||||
pids.append(proc.info["pid"])
|
||||
except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess):
|
||||
# 进程可能在此期间已结束或无法访问, 忽略这些异常
|
||||
pass
|
||||
return pids
|
||||
|
||||
|
||||
truststore.inject_into_ssl()
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="AUTO-MAS更新器", description="为AUTO-MAS前端提供更新服务"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--version", "-v", type=str, required=False, default=None, help="前端程序版本号"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if (Path.cwd() / "config/Config.json").exists():
|
||||
config = json.loads(
|
||||
(Path.cwd() / "config/Config.json").read_text(encoding="utf-8")
|
||||
).get(
|
||||
"Update",
|
||||
{
|
||||
"MirrorChyanCDK": "",
|
||||
"ProxyAddress": "",
|
||||
"Source": "GitHub",
|
||||
"UpdateType": "stable",
|
||||
},
|
||||
)
|
||||
else:
|
||||
config = {
|
||||
"MirrorChyanCDK": "",
|
||||
"ProxyAddress": "",
|
||||
"Source": "GitHub",
|
||||
"UpdateType": "stable",
|
||||
}
|
||||
|
||||
if (
|
||||
config.get("Source", "GitHub") == "MirrorChyan"
|
||||
and dpapi_decrypt(config.get("MirrorChyanCDK", "")) == ""
|
||||
):
|
||||
print("使用 MirrorChyan源但未填写 MirrorChyanCDK, 转用 GitHub 源")
|
||||
config["Source"] = "GitHub"
|
||||
config["MirrorChyanCDK"] = ""
|
||||
|
||||
print(f"当前配置: {config}")
|
||||
|
||||
|
||||
download_source = config.get("Source", "GitHub")
|
||||
proxies = {
|
||||
"http": config.get("ProxyAddress", ""),
|
||||
"https": config.get("ProxyAddress", ""),
|
||||
}
|
||||
|
||||
if args.version:
|
||||
current_version = args.version
|
||||
else:
|
||||
current_version = "v0.0.0"
|
||||
|
||||
print(f"当前版本: {current_version}")
|
||||
|
||||
|
||||
response = requests.get(
|
||||
f"https://mirrorchyan.com/api/resources/AUTO_MAA/latest?user_agent=AutoMaaGui¤t_version={current_version}&cdk={dpapi_decrypt(config.get('MirrorChyanCDK', ''))}&channel={config.get('UpdateType', 'stable')}",
|
||||
timeout=10,
|
||||
proxies=proxies,
|
||||
)
|
||||
if response.status_code == 200:
|
||||
version_info = response.json()
|
||||
else:
|
||||
try:
|
||||
result = response.json()
|
||||
|
||||
if result["code"] != 0:
|
||||
if result["code"] in MIRROR_ERROR_INFO:
|
||||
print(f"获取版本信息时出错: {MIRROR_ERROR_INFO[result['code']]}")
|
||||
else:
|
||||
print(
|
||||
"获取版本信息时出错: 意料之外的错误, 请及时联系项目组以获取来自 Mirror 酱的技术支持"
|
||||
)
|
||||
print(f" {result['msg']}")
|
||||
sys.exit(1)
|
||||
except Exception:
|
||||
print(f"获取版本信息时出错: {response.text}")
|
||||
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
remote_version = version_info["data"]["version_name"]
|
||||
|
||||
if version.parse(remote_version) > version.parse(current_version):
|
||||
|
||||
# 版本更新信息
|
||||
print(f"发现新版本: {remote_version}, 当前版本: {current_version}")
|
||||
|
||||
version_info_json: Dict[str, Dict[str, List[str]]] = json.loads(
|
||||
re.sub(
|
||||
r"^<!--\s*(.*?)\s*-->$",
|
||||
r"\1",
|
||||
version_info["data"]["release_note"].splitlines()[0],
|
||||
)
|
||||
)
|
||||
|
||||
update_version_info = {}
|
||||
for v_i in [
|
||||
info
|
||||
for ver, info in version_info_json.items()
|
||||
if version.parse(ver) > version.parse(current_version)
|
||||
]:
|
||||
|
||||
for key, value in v_i.items():
|
||||
if key not in update_version_info:
|
||||
update_version_info[key] = []
|
||||
update_version_info[key] += value
|
||||
|
||||
for key, value in update_version_info.items():
|
||||
print(f"{key}: ")
|
||||
for v in value:
|
||||
print(f" - {v}")
|
||||
|
||||
if download_source == "GitHub":
|
||||
|
||||
download_url = f"https://github.com/DLmaster361/AUTO_MAA/releases/download/{remote_version}/AUTO_MAA_{remote_version}.zip"
|
||||
|
||||
elif download_source == "MirrorChyan":
|
||||
if "url" in version_info["data"]:
|
||||
with requests.get(
|
||||
version_info["data"]["url"],
|
||||
allow_redirects=True,
|
||||
timeout=10,
|
||||
stream=True,
|
||||
proxies=proxies,
|
||||
) as response:
|
||||
if response.status_code == 200:
|
||||
download_url = response.url
|
||||
else:
|
||||
print(f"MirrorChyan 未返回下载链接, 使用自建下载站")
|
||||
download_url = f"https://download.auto-mas.top/d/AUTO_MAA/AUTO_MAA_{remote_version}.zip"
|
||||
|
||||
elif download_source == "AutoSite":
|
||||
download_url = (
|
||||
f"https://download.auto-mas.top/d/AUTO_MAA/AUTO_MAA_{remote_version}.zip"
|
||||
)
|
||||
|
||||
else:
|
||||
print(f"未知的下载源: {download_source}, 请检查配置文件")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"开始下载: {download_url}")
|
||||
|
||||
# 清理可能存在的临时文件
|
||||
if (Path.cwd() / "download.temp").exists():
|
||||
(Path.cwd() / "download.temp").unlink()
|
||||
|
||||
check_times = 3
|
||||
while check_times != 0:
|
||||
|
||||
try:
|
||||
|
||||
start_time = time.time()
|
||||
|
||||
response = requests.get(
|
||||
download_url, timeout=10, stream=True, proxies=proxies
|
||||
)
|
||||
|
||||
if response.status_code not in [200, 206]:
|
||||
|
||||
if check_times != -1:
|
||||
check_times -= 1
|
||||
|
||||
print(
|
||||
f"连接失败: {download_url}, 状态码: {response.status_code}, 剩余重试次数: {check_times}",
|
||||
)
|
||||
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
print(f"连接成功: {download_url}, 状态码: {response.status_code}")
|
||||
|
||||
file_size = int(response.headers.get("content-length", 0))
|
||||
downloaded_size = 0
|
||||
last_download_size = 0
|
||||
last_time = time.time()
|
||||
with (Path.cwd() / "download.temp").open(mode="wb") as f:
|
||||
|
||||
for chunk in response.iter_content(chunk_size=8192):
|
||||
|
||||
f.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
# 更新指定线程的下载进度, 每秒更新一次
|
||||
if time.time() - last_time >= 1.0:
|
||||
speed = (
|
||||
(downloaded_size - last_download_size)
|
||||
/ (time.time() - last_time)
|
||||
/ 1024
|
||||
)
|
||||
last_download_size = downloaded_size
|
||||
last_time = time.time()
|
||||
|
||||
if speed >= 1024:
|
||||
print(
|
||||
f"正在下载: AUTO-MAS 已下载: {downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB ({downloaded_size / file_size * 100:.2f}%) 下载速度: {speed / 1024:.2f} MB/s",
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"正在下载: AUTO-MAS 已下载: {downloaded_size / 1048576:.2f}/{file_size / 1048576:.2f} MB ({downloaded_size / file_size * 100:.2f}%) 下载速度: {speed:.2f} KB/s",
|
||||
)
|
||||
|
||||
print(
|
||||
f"下载完成: {download_url}, 实际下载大小: {downloaded_size} 字节, 耗时: {time.time() - start_time:.2f} 秒",
|
||||
)
|
||||
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
|
||||
if check_times != -1:
|
||||
check_times -= 1
|
||||
|
||||
print(
|
||||
f"下载出错: {download_url}, 错误信息: {e}, 剩余重试次数: {check_times}",
|
||||
)
|
||||
time.sleep(1)
|
||||
|
||||
else:
|
||||
|
||||
if (Path.cwd() / "download.temp").exists():
|
||||
(Path.cwd() / "download.temp").unlink()
|
||||
print(f"下载失败: {download_url}")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"开始解压: {Path.cwd() / 'download.temp'} 到 {Path.cwd()}")
|
||||
|
||||
while True:
|
||||
|
||||
try:
|
||||
with zipfile.ZipFile(Path.cwd() / "download.temp", "r") as zip_ref:
|
||||
zip_ref.extractall(Path.cwd())
|
||||
print(f"解压完成: {Path.cwd() / 'download.temp'} 到 {Path.cwd()}")
|
||||
break
|
||||
except PermissionError:
|
||||
print(f"解压出错: AUTO_MAA正在运行, 正在尝试将其关闭")
|
||||
kill_process(Path.cwd() / "AUTO_MAA.exe")
|
||||
time.sleep(1)
|
||||
|
||||
print("正在删除临时文件")
|
||||
if (Path.cwd() / "changes.json").exists():
|
||||
(Path.cwd() / "changes.json").unlink()
|
||||
if (Path.cwd() / "download.temp").exists():
|
||||
(Path.cwd() / "download.temp").unlink()
|
||||
|
||||
print("正在启动AUTO_MAA")
|
||||
subprocess.Popen(
|
||||
[Path.cwd() / "AUTO_MAA.exe"],
|
||||
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP
|
||||
| subprocess.DETACHED_PROCESS
|
||||
| subprocess.CREATE_NO_WINDOW,
|
||||
)
|
||||
|
||||
print("更新完成")
|
||||
sys.exit(0)
|
||||
|
||||
else:
|
||||
|
||||
print(f"当前版本为最新版本: {current_version}")
|
||||
sys.exit(0)
|
||||
Reference in New Issue
Block a user