32 Commits

Author SHA1 Message Date
Gitea Actions
2cb4ac62a2 [skip ci] Update version to 1.4.8 2025-10-09 17:52:06 +08:00
Gitea Actions
2ba26b79dd 强制空提交 2025-10-09 17:51:47 +08:00
Gitea Actions
43b54a99c7 [skip ci] Update version to 1.4.7 2025-10-09 17:36:04 +08:00
Gitea Actions
cab5b606a9 解决job构建失败的问题 2025-10-09 17:32:05 +08:00
Gitea Actions
0c6d0608c5 [skip ci] Update version to 2025-10-09 17:30:36 +08:00
Gitea Actions
2734d375c2 解决job构建失败的问题 2025-10-09 17:30:19 +08:00
Gitea Actions
286fa4e697 [skip ci] Update version to 2025-10-09 17:24:44 +08:00
Gitea Actions
39ba4f9324 修改网络问题 2025-10-09 17:24:25 +08:00
Gitea Actions
8f30ab9790 [skip ci] Update version to 2025-10-09 17:21:38 +08:00
jjz
41b686fff6 修改网络问题 2025-10-09 17:21:19 +08:00
jjz
a925ca2c99 修改网络问题 2025-10-09 17:19:50 +08:00
jjz
4d18835dc6 强制空提交 2025-10-09 17:18:36 +08:00
jjz
14dc6714bd 强制空提交 2025-10-09 17:16:59 +08:00
jjz
99fec4e225 修改网络问题 2025-10-09 17:16:28 +08:00
jjz
ccf327cd57 强制空提交 2025-10-09 17:13:14 +08:00
jjz
1392e833c6 强制空提交 2025-10-09 17:11:08 +08:00
jjz
15953380d6 修改ci/cd 2025-10-09 17:10:01 +08:00
jjz
683ee4d1e2 强制空提交 2025-10-09 17:09:51 +08:00
jjz
3b77413971 强制空提交 2025-10-09 17:08:07 +08:00
jjz
4229fc6360 强制空提交 2025-10-09 17:01:17 +08:00
jjz
99ce72106c 修改ci/cd 2025-10-09 16:52:55 +08:00
jjz
afed746f9b 强制空提交 2025-10-09 16:49:35 +08:00
jjz
e2d9018879 强制空提交 2025-10-09 16:45:31 +08:00
jjz
44c79d6c29 强制空提交 2025-10-09 16:40:37 +08:00
jjz
1c9d66fa54 修改ci/cd 2025-10-09 16:37:40 +08:00
jjz
afb844f1e6 修改ci/cd 2025-10-09 16:30:08 +08:00
jjz
d226f41957 修改ci/cd 2025-10-09 16:25:59 +08:00
jjz
497f0a3719 修改ci/cd 2025-10-09 16:23:35 +08:00
jjz
a45788c4df 强制空提交 2025-10-09 16:22:49 +08:00
jjz
a869a31639 强制空提交 2025-10-09 16:13:29 +08:00
jjz
17f63bcc14 强制空提交 2025-10-09 16:08:52 +08:00
jjz
a14ef76b97 提交ci/cd文件 2025-10-09 15:37:25 +08:00
26 changed files with 1138 additions and 8979 deletions

View File

@@ -258,9 +258,6 @@ class DatabaseVersionManager:
cursor.close() cursor.close()
logger.info(f"✅ 版本记录已保存到数据库 (ID: {record_id})") logger.info(f"✅ 版本记录已保存到数据库 (ID: {record_id})")
logger.info(f" 📦 版本: {version_record['version']}")
logger.info(f" 🔗 下载地址: {version_record.get('download_url', '')}")
logger.info(f" 📝 内容: {version_record['content'][:50]}...")
return True return True
except Exception as e: except Exception as e:
@@ -301,58 +298,32 @@ class DatabaseVersionManager:
def update_config_version(self, new_version: str): def update_config_version(self, new_version: str):
"""更新config.py中的APP_VERSION""" """更新config.py中的APP_VERSION"""
logger.info(f"正在更新config.py到版本: {new_version}")
logger.info(f"配置文件路径: {self.config_file}")
logger.info(f"配置文件绝对路径: {self.config_file.absolute()}")
logger.info(f"配置文件是否存在: {self.config_file.exists()}")
if not self.config_file.exists(): if not self.config_file.exists():
logger.error(f"配置文件不存在: {self.config_file}") logger.warning(f"配置文件不存在: {self.config_file}")
return False return False
try: try:
# 读取文件
with open(self.config_file, 'r', encoding='utf-8') as f: with open(self.config_file, 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
logger.info(f"已读取config.py文件大小: {len(content)} 字节")
# 查找当前版本
import re import re
pattern = r'APP_VERSION\s*=\s*["\'][\d.]+["\']' pattern = r'APP_VERSION\s*=\s*["\'][\d.]+["\']'
match = re.search(pattern, content)
if match:
old_version_line = match.group(0)
logger.info(f"找到当前版本行: {old_version_line}")
replacement = f'APP_VERSION = "{new_version}"' replacement = f'APP_VERSION = "{new_version}"'
if re.search(pattern, content):
new_content = re.sub(pattern, replacement, content) new_content = re.sub(pattern, replacement, content)
logger.info(f"准备写入新版本: {replacement}")
# 写入文件
with open(self.config_file, 'w', encoding='utf-8') as f: with open(self.config_file, 'w', encoding='utf-8') as f:
f.write(new_content) f.write(new_content)
# 验证写入
with open(self.config_file, 'r', encoding='utf-8') as f:
verify_content = f.read()
verify_match = re.search(pattern, verify_content)
if verify_match:
logger.info(f"✅ 写入验证成功: {verify_match.group(0)}")
logger.info(f"✅ 已更新 config.py: APP_VERSION = \"{new_version}\"") logger.info(f"✅ 已更新 config.py: APP_VERSION = \"{new_version}\"")
return True return True
else: else:
logger.error(f"未找到APP_VERSION配置项pattern: {pattern}") logger.warning("未找到APP_VERSION配置项")
logger.info(f"文件内容预览前200字符: {content[:200]}")
return False return False
except Exception as e: except Exception as e:
logger.error(f"更新config.py失败: {e}") logger.error(f"更新config.py失败: {e}")
import traceback
logger.error(traceback.format_exc())
return False return False
def create_version_record(self) -> dict: def create_version_record(self) -> dict:
@@ -398,11 +369,6 @@ class DatabaseVersionManager:
f"+{stats['lines_added']}/-{stats['lines_deleted']}") f"+{stats['lines_added']}/-{stats['lines_deleted']}")
# 6. 创建版本记录 # 6. 创建版本记录
# 生成下载地址KS3对象存储 - 广州节点,使用三级域名格式)
installer_filename = f"ShuiDi_AI_Assistant_Setup_v{next_version}.exe"
download_url = f"https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/{installer_filename}"
# 完整示例: https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.0.exe
version_record = { version_record = {
'version': next_version, 'version': next_version,
'update_type': update_type, 'update_type': update_type,
@@ -412,7 +378,6 @@ class DatabaseVersionManager:
'commit_short_hash': commit_info['short_hash'], 'commit_short_hash': commit_info['short_hash'],
'branch': commit_info['branch'], 'branch': commit_info['branch'],
'release_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'release_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'download_url': download_url, # 新增:下载地址
'stats': stats 'stats': stats
} }
@@ -440,7 +405,6 @@ class DatabaseVersionManager:
logger.info(f"📦 版本号: v{next_version}") logger.info(f"📦 版本号: v{next_version}")
logger.info(f"📂 类型: {update_type.upper()}") logger.info(f"📂 类型: {update_type.upper()}")
logger.info(f"👤 作者: {commit_info['author']}") logger.info(f"👤 作者: {commit_info['author']}")
logger.info(f"🔗 下载地址: {download_url}")
logger.info(f"📈 变更: {stats['files_changed']} 文件, " logger.info(f"📈 变更: {stats['files_changed']} 文件, "
f"+{stats['lines_added']}/-{stats['lines_deleted']}") f"+{stats['lines_added']}/-{stats['lines_deleted']}")
logger.info(f"💾 数据库: {'✅ 成功' if db_success else '❌ 失败'}") logger.info(f"💾 数据库: {'✅ 成功' if db_success else '❌ 失败'}")

View File

@@ -1,182 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Upload GUI installer to KS3 object storage
- Target directory: installers/
- Set proper HTTP headers for browser download
"""
import os
import sys
import logging
from pathlib import Path
# 🔥 强制禁用代理,避免代理连接问题
# 必须在所有网络操作之前设置
os.environ['NO_PROXY'] = '*'
os.environ['HTTP_PROXY'] = ''
os.environ['HTTPS_PROXY'] = ''
os.environ['http_proxy'] = ''
os.environ['https_proxy'] = ''
os.environ['ALL_PROXY'] = ''
os.environ['all_proxy'] = ''
try:
from ks3.connection import Connection
except ImportError:
print("ERROR: ks3sdk not installed. Please run: pip install ks3sdk")
sys.exit(1)
# Configure logging
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# KS3 Configuration (using actual project config)
KS3_ACCESS_KEY = 'AKLT0Ey7Nq7ZSXykki0X0RGG'
KS3_SECRET_KEY = 'OONxkt9wwJa1FIco21vCbirs1HB6AGzDWdRyV0k2'
KS3_ENDPOINT = 'ks3-cn-guangzhou.ksyuncs.com' # Guangzhou region
KS3_BUCKET = 'shuidrop-chat-server'
KS3_IS_SECURE = True # Use HTTPS
KS3_PREFIX = 'installers/'
def get_ks3_connection():
"""Get KS3 connection"""
try:
connection = Connection(
KS3_ACCESS_KEY,
KS3_SECRET_KEY,
host=KS3_ENDPOINT,
is_secure=KS3_IS_SECURE,
timeout=1800, # Increase timeout to 30 minutes for large files
)
logger.info(f"KS3 connection established: {KS3_ENDPOINT}")
return connection
except Exception as e:
logger.error(f"KS3 connection failed: {e}")
return None
def find_latest_installer():
"""Find the latest generated installer"""
project_root = Path(__file__).parent.parent.parent
installer_dir = project_root / 'installer' / 'output'
if not installer_dir.exists():
logger.error(f"Installer directory not found: {installer_dir}")
return None
installers = list(installer_dir.glob('*.exe'))
if not installers:
logger.error(f"No installer files found in: {installer_dir}")
return None
latest_installer = max(installers, key=lambda p: p.stat().st_mtime)
file_size_mb = latest_installer.stat().st_size / 1024 / 1024
logger.info(f"Found installer: {latest_installer.name}")
logger.info(f"File size: {file_size_mb:.2f} MB")
return latest_installer
def progress_callback(uploaded_bytes, total_bytes):
"""Upload progress callback"""
if total_bytes > 0:
percentage = (uploaded_bytes / total_bytes) * 100
uploaded_mb = uploaded_bytes / 1024 / 1024
total_mb = total_bytes / 1024 / 1024
logger.info(f"Upload progress: {percentage:.1f}% ({uploaded_mb:.1f}/{total_mb:.1f} MB)")
def upload_installer(connection, installer_path):
"""Upload installer to KS3 with progress tracking"""
try:
bucket = connection.get_bucket(KS3_BUCKET)
ks3_key = f"{KS3_PREFIX}{installer_path.name}"
file_size = installer_path.stat().st_size
file_size_mb = file_size / 1024 / 1024
logger.info(f"Starting upload to KS3...")
logger.info(f"Target path: {ks3_key}")
logger.info(f"File size: {file_size_mb:.2f} MB")
logger.info(f"Estimated time: ~{int(file_size_mb / 2)} seconds (assuming 2MB/s)")
logger.info("Please wait, this may take several minutes...")
key = bucket.new_key(ks3_key)
# Upload file with public read permission and progress tracking
# Use cb parameter for progress callback (called every 5% or every 10MB)
key.set_contents_from_filename(
str(installer_path),
headers={
'Content-Type': 'application/octet-stream',
'Content-Disposition': f'attachment; filename="{installer_path.name}"',
'Cache-Control': 'public, max-age=3600',
'x-kss-storage-class': 'STANDARD',
'x-kss-acl': 'public-read' # Set public read permission in headers
},
policy='public-read', # Set ACL policy
cb=progress_callback, # Progress callback function
num_cb=10 # Call callback 10 times during upload (every 10%)
)
# Generate download URL (using KS3 third-level domain format)
# Format: https://{bucket}.{endpoint}/{key}
protocol = 'https' if KS3_IS_SECURE else 'http'
download_url = f"{protocol}://{KS3_BUCKET}.{KS3_ENDPOINT}/{ks3_key}"
logger.info("")
logger.info("=" * 70)
logger.info("Upload successful!")
logger.info(f"Download URL: {download_url}")
logger.info("=" * 70)
return download_url
except Exception as e:
logger.error("")
logger.error("=" * 70)
logger.error(f"Upload failed: {e}")
logger.error("=" * 70)
import traceback
logger.error(traceback.format_exc())
return None
def main():
"""Main function"""
logger.info("=" * 70)
logger.info("Starting GUI installer upload to KS3")
logger.info(f"KS3 Endpoint: {KS3_ENDPOINT}")
logger.info(f"Bucket: {KS3_BUCKET}")
logger.info(f"Target directory: {KS3_PREFIX}")
logger.info("Proxy disabled: Direct connection to KS3")
logger.info("=" * 70)
installer_path = find_latest_installer()
if not installer_path:
return 1
connection = get_ks3_connection()
if not connection:
return 1
download_url = upload_installer(connection, installer_path)
if not download_url:
return 1
logger.info("=" * 70)
logger.info("Task completed successfully!")
logger.info(f"Installer: {installer_path.name}")
logger.info(f"Download URL: {download_url}")
logger.info("=" * 70)
return 0
if __name__ == '__main__':
sys.exit(main())

View File

@@ -5,6 +5,7 @@
用于查看和管理GUI客户端的版本历史记录 用于查看和管理GUI客户端的版本历史记录
""" """
import sys
import json import json
import argparse import argparse
from pathlib import Path from pathlib import Path

View File

@@ -2,7 +2,7 @@ name: GUI Version Release
on: on:
push: push:
branches: [ master, develop ] # Trigger on master and develop branches branches: [ master ]
jobs: jobs:
gui-version-release: gui-version-release:
@@ -13,35 +13,24 @@ jobs:
working-directory: E:\shuidrop_gui working-directory: E:\shuidrop_gui
steps: steps:
# Step 1: Pull latest code # Step 1: Clone repository manually
- name: Pull latest code - name: Clone repository
shell: powershell shell: powershell
run: | run: |
Write-Host "=========================================="; Write-Host "Cloning repository..."
Write-Host "Pulling latest code";
Write-Host "==========================================";
Write-Host "Working directory: $(Get-Location)";
Write-Host "Branch: ${{ github.ref_name }}";
Write-Host "Commit: ${{ github.sha }}";
Write-Host "";
git config core.autocrlf false; # Change to workspace directory
git config core.longpaths true; cd E:\shuidrop_gui
Write-Host "Fetching from origin..."; Write-Host "Current directory: $(Get-Location)"
git fetch origin; Write-Host "Git status:"
git status
Write-Host "Checking out branch: ${{ github.ref_name }}"; Write-Host "Fetching latest changes..."
git checkout ${{ github.ref_name }}; git fetch origin
git reset --hard origin/master
Write-Host "Pulling latest changes...";
git reset --hard origin/${{ github.ref_name }};
Write-Host "";
Write-Host "Current status:";
git log -1 --oneline;
Write-Host "==========================================";
Write-Host "Repository ready"
# Step 2: Check Python environment # Step 2: Check Python environment
- name: Check Python environment - name: Check Python environment
@@ -78,13 +67,6 @@ jobs:
exit 1 exit 1
} }
pip install py-mini-racer
if ($LASTEXITCODE -ne 0) {
Write-Host "WARNING: Failed to install py-mini-racer (DY platform may not work)"
} else {
Write-Host "OK: py-mini-racer installed"
}
Write-Host "Dependencies installed successfully" Write-Host "Dependencies installed successfully"
# Step 4: Create GUI version record # Step 4: Create GUI version record
@@ -138,546 +120,59 @@ jobs:
DB_PASSWORD: password_ee2iQ3 DB_PASSWORD: password_ee2iQ3
DB_PORT: 5400 DB_PORT: 5400
# Step 4.5: Build production executable # Step 5: Commit changes back to repository
- name: Build production executable
if: success()
shell: powershell
env:
PYTHONIOENCODING: utf-8
run: |
Write-Host "==========================================";
Write-Host "Step 4.5: Build production executable";
Write-Host "==========================================";
# Check and install dependencies only if needed
Write-Host "Checking dependencies...";
# Check PyInstaller
$pyinstallerInstalled = python -c "try: import PyInstaller; print('installed')`nexcept: print('not-installed')" 2>$null;
if ($pyinstallerInstalled -ne "installed") {
Write-Host "Installing PyInstaller...";
python -m pip install pyinstaller --quiet;
} else {
Write-Host "PyInstaller: already installed";
}
# Check Pillow
$pillowInstalled = python -c "try: from PIL import Image; print('installed')`nexcept: print('not-installed')" 2>$null;
if ($pillowInstalled -ne "installed") {
Write-Host "Installing Pillow...";
python -m pip install Pillow --quiet;
} else {
Write-Host "Pillow: already installed";
}
Write-Host "";
Write-Host "Environment ready:";
Write-Host " Python:" (python --version);
Write-Host " PyInstaller:" (python -m PyInstaller --version);
Write-Host " Pillow:" (python -c "from PIL import Image; print(Image.__version__)");
Write-Host "";
python build_production.py;
if ($LASTEXITCODE -ne 0) {
Write-Host "Build failed";
exit 1;
}
Write-Host "Production build completed successfully";
Write-Host "";
# Step 4.6: Check or Install NSIS
- name: Check or Install NSIS
if: success()
shell: powershell
run: |
Write-Host "==========================================";
Write-Host "Step 4.6: Check or Install NSIS";
Write-Host "==========================================";
# Step 1: Check if NSIS is already installed
Write-Host "Checking if NSIS is already installed...";
$nsisPaths = @(
"C:\Program Files (x86)\NSIS",
"C:\Program Files\NSIS",
"C:\Tools\NSIS",
"$env:ProgramFiles\NSIS",
"$env:ProgramFiles(x86)\NSIS"
);
$nsisFound = $false;
$nsisPath = "";
foreach ($path in $nsisPaths) {
if (Test-Path $path) {
$makensisPath = Join-Path $path "makensis.exe";
if (Test-Path $makensisPath) {
$nsisPath = $path;
$nsisFound = $true;
Write-Host "Found NSIS at: $nsisPath";
break;
}
}
}
# Also check if makensis is in PATH
if (-not $nsisFound) {
$makensisInPath = Get-Command makensis -ErrorAction SilentlyContinue;
if ($makensisInPath) {
$nsisPath = Split-Path $makensisInPath.Source;
$nsisFound = $true;
Write-Host "Found makensis in PATH: $nsisPath";
}
}
if ($nsisFound) {
$env:Path = "$nsisPath;$env:Path";
Write-Host "Using existing NSIS installation";
& makensis /VERSION;
Write-Host "";
exit 0;
}
Write-Host "NSIS not found, attempting installation...";
Write-Host "";
# Step 2: Try Chocolatey
Write-Host "Method 1: Trying Chocolatey...";
$chocoInstalled = Get-Command choco -ErrorAction SilentlyContinue;
if ($chocoInstalled) {
try {
choco install nsis -y --no-progress --limit-output;
if ($LASTEXITCODE -eq 0) {
Start-Sleep -Seconds 3;
# Refresh PATH
$env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User");
$nsisPath = "C:\Program Files (x86)\NSIS";
if (-not (Test-Path $nsisPath)) {
$nsisPath = "C:\Program Files\NSIS";
}
if (Test-Path (Join-Path $nsisPath "makensis.exe")) {
$env:Path = "$nsisPath;$env:Path";
& makensis /VERSION;
Write-Host "NSIS installed via Chocolatey";
Write-Host "";
exit 0;
}
}
} catch {
Write-Host "Chocolatey installation failed: $_";
}
} else {
Write-Host "Chocolatey not available";
}
# Step 3: Try winget (Windows Package Manager)
Write-Host "";
Write-Host "Method 2: Trying winget...";
$wingetInstalled = Get-Command winget -ErrorAction SilentlyContinue;
if ($wingetInstalled) {
try {
winget install --id=NSIS.NSIS -e --silent --accept-package-agreements --accept-source-agreements;
if ($LASTEXITCODE -eq 0) {
Start-Sleep -Seconds 3;
$nsisPath = "C:\Program Files (x86)\NSIS";
if (-not (Test-Path $nsisPath)) {
$nsisPath = "C:\Program Files\NSIS";
}
if (Test-Path (Join-Path $nsisPath "makensis.exe")) {
$env:Path = "$nsisPath;$env:Path";
& makensis /VERSION;
Write-Host "NSIS installed via winget";
Write-Host "";
exit 0;
}
}
} catch {
Write-Host "winget installation failed: $_";
}
} else {
Write-Host "winget not available";
}
# Step 4: Manual installation is not possible due to network restrictions
Write-Host "";
Write-Host "========================================";
Write-Host "ERROR: NSIS is not installed and automatic installation failed";
Write-Host "";
Write-Host "Please install NSIS manually on the CI/CD runner:";
Write-Host "1. Download from: https://nsis.sourceforge.io/Download";
Write-Host "2. Install to: C:\Program Files (x86)\NSIS";
Write-Host "3. Or use: choco install nsis";
Write-Host "4. Or use: winget install NSIS.NSIS";
Write-Host "========================================";
exit 1;
# Step 4.7: Build NSIS installer
- name: Build NSIS installer
if: success()
shell: powershell
run: |
Write-Host "==========================================";
Write-Host "Step 4.7: Build NSIS installer";
Write-Host "==========================================";
# Ensure NSIS is in PATH
$nsisPath = "C:\Program Files (x86)\NSIS";
if (-not (Test-Path $nsisPath)) {
$nsisPath = "C:\Program Files\NSIS";
}
$env:Path = "$nsisPath;$env:Path";
Write-Host "Using NSIS from: $nsisPath";
cd installer;
python build_installer.py;
if ($LASTEXITCODE -ne 0) {
Write-Host "Installer build failed";
exit 1;
}
$installers = Get-ChildItem -Path "output" -Filter "*.exe" -ErrorAction SilentlyContinue;
if (-not $installers -or $installers.Count -eq 0) {
Write-Host "No installer file found";
exit 1;
}
$installerName = $installers[0].Name;
$installerSize = [math]::Round($installers[0].Length / 1MB, 2);
Write-Host "Installer built successfully";
Write-Host " Filename: $installerName";
Write-Host " Size: $installerSize MB";
Write-Host "";
# Step 4.8: Upload installer to KS3
- name: Upload installer to KS3
if: success()
shell: powershell
timeout-minutes: 30
run: |
Write-Host "==========================================";
Write-Host "Step 4.8: Upload installer to KS3";
Write-Host "==========================================";
Write-Host "NOTE: Large file upload may take 5-10 minutes";
Write-Host "";
# Install KS3 SDK
Write-Host "Installing KS3 SDK...";
python -m pip install ks3sdk --quiet;
if ($LASTEXITCODE -ne 0) {
Write-Host "ERROR: Failed to install ks3sdk";
Write-Host "Skipping KS3 upload (version release continues)";
exit 0;
}
Write-Host "Starting upload process...";
Write-Host "This may take several minutes, please be patient...";
Write-Host "";
# Run upload script
python .gitea/scripts/upload_installer_to_ks3.py;
if ($LASTEXITCODE -eq 0) {
Write-Host "";
Write-Host "OK: Installer uploaded to KS3 successfully";
} else {
Write-Host "";
Write-Host "WARNING: KS3 upload failed (exit code: $LASTEXITCODE)";
Write-Host "NOTICE: Version release continues";
Write-Host "NOTICE: You can manually upload the installer later";
Write-Host "";
Write-Host "Manual upload steps:";
Write-Host " 1. Find installer in: installer/output/";
Write-Host " 2. Upload to KS3 bucket: shuidrop-chat-server";
Write-Host " 3. Target path: installers/";
}
Write-Host "";
# Step 5: Commit version changes
- name: Commit version changes - name: Commit version changes
if: success() if: success()
shell: powershell shell: powershell
run: | run: |
Write-Host "========================================"; Write-Host "Committing version changes..."
Write-Host "Step 5: Commit version changes";
Write-Host "========================================";
# Read new version number # Read version from config.py
$VERSION = ""; $VERSION = ""
if (Test-Path "config.py") { if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw; $configContent = Get-Content "config.py" -Raw
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') { if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]; $VERSION = $matches[1]
Write-Host "New version: $VERSION"; Write-Host "New version: $VERSION"
} }
} }
# Configure Git git config user.name "Gitea Actions"
git config user.name "Gitea Actions Bot"; git config user.email "actions@gitea.local"
git config user.email "bot@gitea.local";
# Ensure we are on the correct branch (not detached HEAD) git add config.py
$BRANCH = "${{ github.ref_name }}"; git add version_history.json
$currentBranch = git rev-parse --abbrev-ref HEAD;
Write-Host "Current branch: $currentBranch";
Write-Host "Target branch: $BRANCH";
if ($currentBranch -ne $BRANCH) {
Write-Host "WARNING: Not on target branch, checking out...";
git checkout $BRANCH;
Write-Host "OK: Checked out to $BRANCH";
}
# Clean up any existing rebase state before starting
$rebaseExists = (Test-Path ".git/rebase-merge") -or (Test-Path ".git/rebase-apply");
if ($rebaseExists) {
Write-Host "WARNING: Found existing rebase state, cleaning up...";
git rebase --abort 2>$null;
Remove-Item -Path ".git/rebase-merge" -Recurse -Force -ErrorAction SilentlyContinue;
Remove-Item -Path ".git/rebase-apply" -Recurse -Force -ErrorAction SilentlyContinue;
Write-Host "OK: Rebase state cleaned";
}
# Check for changes
git add config.py version_history.json;
$hasChanges = git diff --staged --quiet;
$hasChanges = git diff --staged --quiet
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
Write-Host "Detected changes in version files"; git commit -m "[skip ci] Update version to $VERSION"
Write-Host ""; git push origin master
Write-Host "Version changes committed and pushed"
# KEY CHANGE: Pull first, then commit
Write-Host "Step 5.1: Pulling latest changes first...";
git fetch origin $BRANCH;
# Check if remote has updates
$LOCAL = git rev-parse HEAD;
$REMOTE = git rev-parse origin/$BRANCH;
if ($LOCAL -ne $REMOTE) {
Write-Host "Remote has new commits, need to merge...";
Write-Host "Local: $LOCAL";
Write-Host "Remote: $REMOTE";
Write-Host "";
# FIX: Stash local changes before pull to avoid conflicts
Write-Host "Stashing local version changes...";
git stash push -m "CI-CD-temp-stash" config.py version_history.json;
Write-Host "Pulling remote changes...";
# Pull remote changes (use merge strategy to avoid rebase conflicts)
git pull origin $BRANCH --no-rebase;
# Pop stashed changes back
Write-Host "Restoring version changes...";
$stashPopResult = git stash pop 2>&1;
$stashPopExitCode = $LASTEXITCODE;
if ($stashPopExitCode -ne 0) {
Write-Host "WARNING: Stash pop encountered conflicts, resolving...";
Write-Host "$stashPopResult";
# Check conflict files
$conflicts = git diff --name-only --diff-filter=U;
Write-Host "Conflict files: $conflicts";
# FIX: Smart conflict resolution to preserve user code
# For config.py: use remote version (user code), then re-apply version update
if ($conflicts -match "config.py") {
Write-Host "Resolving config.py conflict...";
# Use remote version (preserve user code)
git checkout --theirs config.py;
# Re-apply version update only
python .gitea/scripts/gui_version_creator.py;
git add config.py;
Write-Host "OK: Resolved config.py (preserved user code + updated version)";
}
# For version_history.json: use CI/CD version (append record)
if ($conflicts -match "version_history.json") {
Write-Host "Resolving version_history.json conflict...";
git checkout --ours version_history.json;
git add version_history.json;
Write-Host "OK: Resolved version_history.json (using CI/CD version)";
}
# Drop the stash after resolving
git stash drop 2>$null;
} else { } else {
Write-Host "OK: Stash pop successful"; Write-Host "No changes to commit"
# FIX: After stash pop, files are automatically merged
# config.py now contains:
# 1. User's code from remote (from pull)
# 2. Version update from stash (from gui_version_creator.py)
Write-Host "Checking if version update is preserved...";
# Verify version number is correct
$configContent = Get-Content "config.py" -Raw;
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$currentVersion = $matches[1];
Write-Host "Current APP_VERSION in config.py: $currentVersion";
Write-Host "Expected version: $VERSION";
if ($currentVersion -ne $VERSION) {
Write-Host "WARNING: Version mismatch, re-applying version update...";
# Re-execute version update (only modify APP_VERSION line)
python .gitea/scripts/gui_version_creator.py;
} else {
Write-Host "OK: Version is correct";
} }
}
# Add current files (includes user code + version update)
git add config.py version_history.json;
Write-Host "Files staged successfully";
}
} else {
Write-Host "No remote changes, proceeding with commit...";
# FIX: Ensure files are staged even if no remote changes
git add config.py version_history.json;
Write-Host "Files staged for commit";
}
Write-Host "";
Write-Host "Step 5.2: Committing version changes...";
# Check if there are changes to commit
$hasUncommitted = git diff --quiet; $diffExitCode1 = $LASTEXITCODE;
$hasStagedChanges = git diff --staged --quiet; $diffExitCode2 = $LASTEXITCODE;
if (($diffExitCode1 -ne 0) -or ($diffExitCode2 -ne 0)) {
Write-Host "Detected uncommitted changes, creating commit...";
git commit -m "[skip ci] Update version to v$VERSION" --no-verify;
if ($LASTEXITCODE -eq 0) {
Write-Host "OK: Commit successful";
} else {
Write-Host "ERROR: Commit failed";
}
} else {
Write-Host "No changes to commit (already committed in merge)";
Write-Host "Skipping commit step";
$LASTEXITCODE = 0;
}
if ($LASTEXITCODE -eq 0) {
Write-Host "";
Write-Host "Step 5.3: Pushing to remote...";
# Push to remote (retry up to 3 times with smart strategy)
$pushSuccess = $false;
for ($i = 1; $i -le 3; $i++) {
Write-Host "Push attempt $i/3...";
# Before each attempt, ensure clean state
if ($i -gt 1) {
Write-Host "Preparing for retry $i...";
# Step 1: Clean up any rebase state
$rebaseCheck = (Test-Path ".git/rebase-merge") -or (Test-Path ".git/rebase-apply");
if ($rebaseCheck) {
Write-Host "Cleaning up rebase state...";
git rebase --abort 2>$null;
Remove-Item -Path ".git/rebase-merge" -Recurse -Force -ErrorAction SilentlyContinue;
Remove-Item -Path ".git/rebase-apply" -Recurse -Force -ErrorAction SilentlyContinue;
}
# Step 2: Ensure on correct branch
$currentBranchCheck = git rev-parse --abbrev-ref HEAD;
if ($currentBranchCheck -ne $BRANCH) {
Write-Host "Checking out to branch $BRANCH...";
git checkout $BRANCH;
}
# Step 3: Fetch latest remote state
Write-Host "Fetching latest remote state...";
git fetch origin $BRANCH;
# Step 4: Record current version number
$currentVersionMatch = (Get-Content "config.py" -Raw) -match 'APP_VERSION\s*=\s*"([\d.]+)"';
$targetVersion = $matches[1];
Write-Host "Target version to apply: $targetVersion";
# Step 5: Reset to remote state
Write-Host "Resetting to remote state...";
git reset --hard origin/$BRANCH;
# Step 6: FIX - Re-apply version update only, do not overwrite entire file
Write-Host "Re-applying version update only...";
python .gitea/scripts/gui_version_creator.py;
# Step 7: Stage and commit again
git add config.py version_history.json;
git commit -m "[skip ci] Update version to v$VERSION" --no-verify;
Write-Host "Retry preparation complete";
Start-Sleep -Seconds 1;
}
# Attempt to push
git push origin $BRANCH;
if ($LASTEXITCODE -eq 0) {
Write-Host "OK: Push successful on attempt $i";
$pushSuccess = $true;
break;
} else {
Write-Host "WARNING: Push attempt $i failed";
}
}
if (-not $pushSuccess) {
Write-Host "ERROR: Push failed after 3 attempts";
Write-Host "NOTICE: This is not critical - version is already in database and KS3";
Write-Host "NOTICE: Git repository sync can be done manually later";
}
} else {
Write-Host "ERROR: Commit failed";
}
} else {
Write-Host "No changes to commit";
}
Write-Host "========================================";
Write-Host "";
# Step 6: Display summary # Step 6: Display summary
- name: Display summary - name: Display summary
if: always() if: always()
shell: powershell shell: powershell
run: | run: |
$VERSION = "Unknown"; # Read version from config.py
$VERSION = "Unknown"
if (Test-Path "config.py") { if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw; $configContent = Get-Content "config.py" -Raw
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') { if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]; $VERSION = $matches[1]
} }
} }
Write-Host "=========================================="; Write-Host "=========================================="
Write-Host "GUI Version Release Summary"; Write-Host "GUI Version Release Summary"
Write-Host "=========================================="; Write-Host "=========================================="
Write-Host "Author: ${{ github.actor }}"; Write-Host "Author: ${{ github.actor }}"
Write-Host "Branch: ${{ github.ref_name }}"; Write-Host "Branch: ${{ github.ref_name }}"
Write-Host "Version: $VERSION"; Write-Host "Version: $VERSION"
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"; Write-Host "Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "Commit: ${{ github.sha }}"; Write-Host "Commit: ${{ github.sha }}"
Write-Host "=========================================="; Write-Host "=========================================="

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ import uuid
import json import json
# 发送文本消息 # 发送消息
def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, text: str): def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, text: str):
""" """
构造发送消息消息体 构造发送消息消息体
@@ -78,7 +78,7 @@ def send_message(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, t
'15': [ '15': [
{'1': b'pigeon_source', '2': b'web'}, {'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'}, {'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': pigeon_sign.encode()}, # 🔥 修复:使用动态参数,不是硬编码 {'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
{'1': b'session_aid', '2': b'1383'}, {'1': b'session_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()}, {'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'}, {'1': b'app_name', '2': b'im'},
@@ -373,229 +373,3 @@ def heartbeat_message(pigeon_sign: str, token: str, session_did: str):
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'200': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}} message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'200': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
return value, message_type return value, message_type
# 🔥 新增:发送图片消息
def send_img(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, img: str, image_width: str = "2000", image_height: str = "1125", image_format: str = "png", image_size: str = "3157512"):
"""
构造发送图片消息体
:param image_width: 图片宽度
:param image_size: 图片大小
:param image_height: 图片高度
:param image_format: 图片格式
:param pigeon_sign: 接口返回
:param token: 接口返回
:param receiver_id: wss消息返回 对方用户id
:param shop_id: cookie自带
:param talk_id: wss消息返回 激活窗口id
:param session_did: cookie自带
:param p_id: wss消息返回
:param user_code: 用户token
:param img: 图片URI或URL
:return: (value, message_type)
"""
value = {
'1': 11778,
'2': int(time.time() * 1000),
'3': 10001,
'4': 1,
'5': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': pigeon_sign.encode()},
],
'7': {'14': 98},
'8': {
'1': 100,
'2': 11778,
'3': b'1.0.4-beta.2',
'4': token.encode(),
'5': 3,
'6': 3,
'7': b'2d97ea6:feat/add_init_callback',
'8': {
'100': {
'1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(),
'2': 11,
'3': p_id,
'4': "[图片]".encode(),
'5': [
{'1': b'type', '2': b'file_image'},
{'1': b'shop_id', '2': shop_id.encode()},
{'1': b'sender_role', '2': b'2'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'src', '2': b'pc'},
{'1': b'srcType', '2': b'1'},
{'1': b'source', '2': b'pc-web'},
{'1': b'receiver_id', '2': str(receiver_id).encode()},
{'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'},
{'1': b'tag_valid', '2': b'1'},
{'1': b'imageUrl', '2': img.encode()},
{'1': b'imageWidth', '2': image_width.encode()},
{'1': b'imageHeight', '2': image_height.encode()},
{'1': b'imageFormat', '2': image_format.encode()},
{'1': b'imageSize', '2': image_size.encode()},
{'1': b'uuid', '2': str(uuid.uuid4()).encode()},
{'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()},
{'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'sender_id', '2': b''},
{'1': b'biz_ext', '2': b'{}'},
{'1': b'p:from_source', '2': b'web'},
{'1': b's:mentioned_users', '2': b''},
{'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()}
],
'6': 1000,
'7': user_code.encode(),
'8': str(uuid.uuid4()).encode(),
'14': talk_id
}
},
'9': session_did.encode(),
'11': b'web',
'15': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
{'1': b'session_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'},
{'1': b'priority_region', '2': b'cn'},
{'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'cookie_enabled', '2': b'true'},
{'1': b'browser_language', '2': b'zh-CN'},
{'1': b'browser_platform', '2': b'Win32'},
{'1': b'browser_name', '2': b'Mozilla'},
{'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'browser_online', '2': b'true'},
{'1': b'screen_width', '2': b'1707'},
{'1': b'screen_height', '2': b'1067'},
{'1': b'referer', '2': b''},
{'1': b'timezone_name', '2': b'Asia/Shanghai'}
],
'18': 2
}
}
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'100': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'bytes', 'name': ''}, '14': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
return value, message_type
# 🔥 新增:发送视频消息
def send_video(pigeon_sign: str, token: str, receiver_id: str, shop_id: str, talk_id: int, session_did: str, p_id: int, user_code: str, vid: str, cover_url: str, height: str, width: str, duration: str):
"""
构造发送视频消息体
:param duration: 视频时长
:param width: 视频宽度
:param height: 视频高度
:param cover_url: 封面URL
:param pigeon_sign: 接口返回
:param token: 接口返回
:param receiver_id: wss消息返回 对方用户id
:param shop_id: cookie自带
:param talk_id: wss消息返回 激活窗口id
:param session_did: cookie自带
:param p_id: wss消息返回
:param user_code: 用户token
:param vid: 视频id
:return: (value, message_type)
"""
# 🔥 修复确保数值类型正确height/width为intduration为float
try:
height_int = int(height) if isinstance(height, str) else height
width_int = int(width) if isinstance(width, str) else width
duration_float = float(duration) if isinstance(duration, str) else duration
except (ValueError, TypeError):
# 如果转换失败,使用默认值
height_int = 1080
width_int = 1920
duration_float = 0.0
msg_render_model = json.dumps({
"msg_render_type": "video",
"render_body": {
"vid": vid,
"coverURL": cover_url,
"height": height_int,
"width": width_int,
"duration": duration_float
}
}).encode()
value = {
'1': 10015,
'2': int(time.time() * 1000),
'3': 10001,
'4': 1,
'5': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': pigeon_sign.encode()},
],
'7': {'14': 98},
'8': {
'1': 100,
'2': 10015,
'3': b'1.0.4-beta.2',
'4': token.encode(),
'5': 3,
'6': 3,
'7': b'2d97ea6:feat/add_init_callback',
'8': {
'100': {
'1': f"{receiver_id}:{shop_id}::2:1:pigeon".encode(),
'2': 11,
'3': p_id,
'4': "[视频]".encode(),
'5': [
{'1': b'type', '2': b'video'},
{'1': b'shop_id', '2': shop_id.encode()},
{'1': b'sender_role', '2': b'2'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'src', '2': b'pc'},
{'1': b'srcType', '2': b'1'},
{'1': b'source', '2': b'pc-web'},
{'1': b'receiver_id', '2': str(receiver_id).encode()},
{'1': b'hierarchical_dimension', '2': b'{"dynamic_dimension":"4541_1131_9042_6599_9420_6832_4050_3823_3994_8564_1528_0388_8667_2179_7948_1870_1949_0989_8012_6240_7898_7548_8852_6245_9393_3650_8570_4026_4034_4057_6537_8632_2068_8958_0363_2387_9033_3425_2238_0982_1935_8188_3817_8557_7931_3278_4065_1893_6049_6961_3814_4883_4401_6637_7282_3652_9354_0437_4769_4815_9572_7230_5054_3951_4852_2188_3505_6813_2570_5394_0729","goofy_id":"1.0.1.1508","desk_version":"0.0.0","open_stores":"0","memL":"","cpuL":"","session_throughput":0,"message_throughput_send":0,"message_throughput_revice":0}'},
{'1': b'msg_render_model', '2': msg_render_model},
{'1': b'uuid', '2': str(uuid.uuid4()).encode()},
{'1': b'start_scene', '2': b'1'},
{'1': b'track_info','2': json.dumps({"send_time": int(time.time() * 1000), "_send_delta": "77","_send_delta_2": "216"}).encode()},
{'1': b'user_agent', '2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'sender_id', '2': b''},
{'1': b'biz_ext', '2': b'{}'},
{'1': b'p:from_source', '2': b'web'},
{'1': b's:mentioned_users', '2': b''},
{'1': b's:client_message_id', '2': str(uuid.uuid4()).encode()}
],
'6': 1000,
'7': user_code.encode(),
'8': str(uuid.uuid4()).encode(),
'14': talk_id
}
},
'9': session_did.encode(),
'11': b'web',
'15': [
{'1': b'pigeon_source', '2': b'web'},
{'1': b'PIGEON_BIZ_TYPE', '2': b'2'},
{'1': b'pigeon_sign', '2': b'MIG6BAz2BNUON43WdlOBuGYEgZcsIho9ZjVP4yyExLShzXgAZtsvUMj2e3jZWeMZv+6+TNVQQMq3xSLrqiwcs2cCaOVBDuS6zGsWm5gBlGtlvOOLM5td2/9OS8P37t1sdkjN4BSH2mB7FlGItioZIsTh1sodn6pYCGj+45mtId3Itenufgai3Mnkpt573uoWJmagF8J3jVPHMFtdwd25Qf5vsWC2kB30glpQBBCbk2VO2ubMqctqQSzhI6uD'},
{'1': b'session_aid', '2': b'1383'},
{'1': b'session_did', '2': session_did.encode()},
{'1': b'app_name', '2': b'im'},
{'1': b'priority_region', '2': b'cn'},
{'1': b'user_agent','2': b'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'cookie_enabled', '2': b'true'},
{'1': b'browser_language', '2': b'zh-CN'},
{'1': b'browser_platform', '2': b'Win32'},
{'1': b'browser_name', '2': b'Mozilla'},
{'1': b'browser_version', '2': b'5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/137.0.0.0 Safari/537.36'},
{'1': b'browser_online', '2': b'true'},
{'1': b'screen_width', '2': b'1707'},
{'1': b'screen_height', '2': b'1067'},
{'1': b'referer', '2': b''},
{'1': b'timezone_name', '2': b'Asia/Shanghai'}
],
'18': 2
}
}
message_type = {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'int', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '7': {'type': 'message', 'message_typedef': {'14': {'type': 'int', 'name': ''}}, 'name': ''}, '8': {'type': 'message', 'message_typedef': {'1': {'type': 'int', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'bytes', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'int', 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'message', 'message_typedef': {'100': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'int', 'name': ''}, '3': {'type': 'int', 'name': ''}, '4': {'type': 'bytes', 'name': ''}, '5': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '6': {'type': 'int', 'name': ''}, '7': {'type': 'bytes', 'name': ''}, '8': {'type': 'bytes', 'name': ''}, '14': {'type': 'int', 'name': ''}}, 'name': ''}}, 'name': ''}, '9': {'type': 'bytes', 'name': ''}, '11': {'type': 'bytes', 'name': ''}, '15': {'type': 'message', 'message_typedef': {'1': {'type': 'bytes', 'name': ''}, '2': {'type': 'bytes', 'name': ''}}, 'name': ''}, '18': {'type': 'int', 'name': ''}}, 'name': ''}}
return value, message_type

View File

@@ -13,8 +13,6 @@ import requests
import json import json
import time import time
import threading import threading
import uuid
import os
# 定义持久化数据类 # 定义持久化数据类
@@ -157,14 +155,6 @@ class FixJdCookie:
self.max_reconnect_delay = 60.0 # 最大重连延迟 self.max_reconnect_delay = 60.0 # 最大重连延迟
self.reconnect_backoff = 1.5 # 退避系数 self.reconnect_backoff = 1.5 # 退避系数
# 🔥 存储认证信息,用于文件上传
self.cookies_str = None
self.current_aid = None
self.current_pin_zj = None
# 🔥 启动时清理过期临时文件
self._cleanup_old_temp_files(max_age_hours=24)
def _log(self, message, log_type="INFO"): def _log(self, message, log_type="INFO"):
"""内部日志方法""" """内部日志方法"""
if self.log_callback: if self.log_callback:
@@ -187,11 +177,6 @@ class FixJdCookie:
"""初始化 socket""" """初始化 socket"""
await self.send_heartbeat(ws, aid, pin_zj) await self.send_heartbeat(ws, aid, pin_zj)
print("开始监听初始化") print("开始监听初始化")
# 🔧 修复生成唯一设备ID避免多端互踢
import uuid
unique_device_id = f"shuidrop_gui_{uuid.uuid4().hex[:16]}"
auth = { auth = {
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(), "id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"aid": aid, "aid": aid,
@@ -200,9 +185,8 @@ class FixJdCookie:
"type": "auth", "type": "auth",
"body": {"presence": 1, "clientVersion": "2.6.3"}, "body": {"presence": 1, "clientVersion": "2.6.3"},
"to": {"app": "im.waiter"}, "to": {"app": "im.waiter"},
"from": {"app": "im.waiter", "pin": pin_zj, "clientType": "comet", "dvc": unique_device_id} "from": {"app": "im.waiter", "pin": pin_zj, "clientType": "comet", "dvc": "device1234"}
} }
print(f"[DEBUG] 使用唯一设备ID: {unique_device_id}")
await ws.send(json.dumps(auth)) await ws.send(json.dumps(auth))
@@ -226,80 +210,6 @@ class FixJdCookie:
} }
await ws.send(json.dumps(message)) await ws.send(json.dumps(message))
async def get_all_customer(self, ws, aid, pin_zj):
"""异步获取商家组织架构(客服列表)"""
id_str = hashlib.md5(str(int(time.time() * 1000) + 300).encode()).hexdigest()
message = {
"id": id_str,
"aid": aid,
"lang": "zh_CN",
"timestamp": int(time.time() * 1000),
"from": {
"app": "im.waiter", "pin": pin_zj, "art": "customerGroupMsg", "clientType": "comet"
},
"type": "org_new",
"body": {"param": pin_zj, "paramtype": "ByPin"}
}
await ws.send(json.dumps(message))
while True:
response = await ws.recv()
data = json.loads(response)
if data.get("id") == id_str and data.get("body", {}).get("groupname") == "商家组织架构":
group = data.get("body", {}).get("group")
if group:
try:
# 与旧实现保持一致的数据路径
self.customer_list = group[0].get("group")[0].get("users")
except Exception:
self.customer_list = []
return
async def send_staff_list_to_backend(self, store_id):
"""发送客服列表到后端(通过统一后端连接)"""
try:
if not hasattr(self, 'customer_list') or not self.customer_list:
self._log("⚠️ 客服列表为空", "WARNING")
return False
# 转换客服数据格式
staff_infos = []
for staff in self.customer_list:
staff_info = {
"staff_id": staff.get("pin", ""),
"name": staff.get("nickname", ""),
"status": staff.get("status", 0),
"department": staff.get("department", ""),
"online": staff.get("online", True)
}
staff_infos.append(staff_info)
# 通过后端统一连接发送
try:
from WebSocket.backend_singleton import get_backend_client
backend_client = get_backend_client()
if not backend_client or not getattr(backend_client, 'is_connected', False):
self._log("❌ 统一后端连接不可用", "ERROR")
return False
message = {
"type": "staff_list",
"content": "客服列表更新",
"data": {
"staff_list": staff_infos,
"total_count": len(staff_infos)
},
"store_id": str(store_id)
}
backend_client.send_message(message)
self._log(f"✅ 成功发送客服列表到后端,共 {len(staff_infos)} 个客服", "SUCCESS")
return True
except Exception as e:
self._log(f"❌ 发送客服列表到后端失败: {e}", "ERROR")
return False
except Exception as e:
self._log(f"❌ 发送客服列表到后端异常: {e}", "ERROR")
return False
async def transfer_customer(self, ws, aid, pin, pin_zj, chat_name): async def transfer_customer(self, ws, aid, pin, pin_zj, chat_name):
"""异步客服转接 在发送的消息为客服转接的关键词的时候""" """异步客服转接 在发送的消息为客服转接的关键词的时候"""
message = { message = {
@@ -323,27 +233,10 @@ class FixJdCookie:
traceback.print_exc() traceback.print_exc()
return False return False
async def send_message(self, ws, pin, aid, pin_zj, vender_id, content, msg_type="text"): async def send_message(self, ws, pin, aid, pin_zj, vender_id, content):
"""异步发送消息 - 支持文本/图片/视频 """异步发送单条消息"""
Args:
ws: WebSocket连接
pin: 客户pin
aid: 账号aid
pin_zj: 客服pin
vender_id: 商家ID
content: 消息内容文本内容或URL
msg_type: 消息类型 (text/image/video)
"""
try: try:
# 根据消息类型调用不同的发送方法 print('本地发送消息')
if msg_type == "image":
return await self.send_image_message(ws, pin, aid, pin_zj, vender_id, content)
elif msg_type == "video":
return await self.send_video_message(ws, pin, aid, pin_zj, vender_id, content)
else:
# 文本消息(默认)
print('本地发送文本消息')
message = { message = {
"ver": "4.3", "ver": "4.3",
"type": "chat_message", "type": "chat_message",
@@ -371,528 +264,6 @@ class FixJdCookie:
logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}") logger.error(f"消息发送过程中出现特殊异常异常信息为: {e}")
raise raise
def _get_temp_directory(self) -> str:
"""智能选择临时文件目录(优先级降级策略)
优先级:
1. 环境变量 SHUIDROP_TEMP_DIR用户自定义
2. 应用程序所在目录/temp如果有写权限且不在Program Files
3. 系统临时目录(兜底方案)
Returns:
str: 临时目录路径
"""
import tempfile
import sys
try:
# 优先级1用户自定义环境变量
custom_temp = os.getenv('SHUIDROP_TEMP_DIR')
if custom_temp:
custom_temp = os.path.join(custom_temp, "shuidrop_jd_temp_uploads")
try:
os.makedirs(custom_temp, exist_ok=True)
# 测试写权限
test_file = os.path.join(custom_temp, ".write_test")
with open(test_file, 'w') as f:
f.write("test")
os.remove(test_file)
self._log(f"✅ [JD路径] 使用自定义临时目录: {custom_temp}", "INFO")
return custom_temp
except Exception as e:
self._log(f"⚠️ [JD路径] 自定义目录不可用: {e}", "WARNING")
# 优先级2应用程序所在目录避免占用C盘
try:
# 获取可执行文件路径
if getattr(sys, 'frozen', False):
# 打包后
app_dir = os.path.dirname(sys.executable)
else:
# 开发环境
app_dir = os.path.dirname(os.path.abspath(__file__))
# 检查是否在 Program Files需要管理员权限
if 'Program Files' not in app_dir and 'Program Files (x86)' not in app_dir:
app_temp = os.path.join(app_dir, "temp_uploads", "jd")
os.makedirs(app_temp, exist_ok=True)
# 测试写权限
test_file = os.path.join(app_temp, ".write_test")
with open(test_file, 'w') as f:
f.write("test")
os.remove(test_file)
self._log(f"✅ [JD路径] 使用应用程序目录: {app_temp}", "INFO")
return app_temp
else:
self._log(f" [JD路径] 应用在Program Files跳过应用目录", "DEBUG")
except Exception as e:
self._log(f"⚠️ [JD路径] 应用目录不可用: {e}", "DEBUG")
# 优先级3系统临时目录兜底方案
system_temp = os.path.join(tempfile.gettempdir(), "shuidrop_jd_temp_uploads")
os.makedirs(system_temp, exist_ok=True)
self._log(f"✅ [JD路径] 使用系统临时目录: {system_temp}", "INFO")
return system_temp
except Exception as e:
# 最终兜底
self._log(f"❌ [JD路径] 所有路径策略失败,使用默认: {e}", "ERROR")
import tempfile
return os.path.join(tempfile.gettempdir(), "shuidrop_jd_temp_uploads")
def _get_file_extension(self, url: str, default_ext: str) -> str:
"""智能提取文件扩展名
Args:
url: 文件URL
default_ext: 默认扩展名jpg/mp4
Returns:
str: 文件扩展名
"""
try:
# 移除查询参数
url_without_params = url.split('?')[0]
# 检查是否有有效的文件扩展名
if '.' in url_without_params:
parts = url_without_params.split('.')
ext = parts[-1].lower()
# 验证扩展名是否合法(只包含字母数字)
if ext and len(ext) <= 5 and ext.isalnum():
# 常见图片/视频扩展名
valid_exts = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'mp4', 'avi', 'mov', 'wmv', 'flv', 'mkv']
if ext in valid_exts:
return ext
# 如果无法提取有效扩展名,使用默认值
return default_ext
except Exception:
return default_ext
def _cleanup_old_temp_files(self, max_age_hours=24):
"""清理过期的临时文件24小时以上
Args:
max_age_hours: 文件最大保留时间(小时)
"""
try:
# 使用智能路径选择
temp_dir = self._get_temp_directory()
if not os.path.exists(temp_dir):
return
now = time.time()
max_age_seconds = max_age_hours * 3600
cleaned_count = 0
# 遍历临时目录中的文件
for filename in os.listdir(temp_dir):
file_path = os.path.join(temp_dir, filename)
# 只处理文件,跳过目录
if not os.path.isfile(file_path):
continue
try:
# 检查文件年龄
file_mtime = os.path.getmtime(file_path)
file_age = now - file_mtime
if file_age > max_age_seconds:
file_size = os.path.getsize(file_path)
os.remove(file_path)
cleaned_count += 1
self._log(f"🗑️ [JD清理] 删除过期文件: {filename} ({file_size} bytes, {file_age/3600:.1f}小时前)", "DEBUG")
except Exception as e:
self._log(f"⚠️ [JD清理] 删除文件失败 {filename}: {e}", "DEBUG")
if cleaned_count > 0:
self._log(f"✅ [JD清理] 已清理 {cleaned_count} 个过期临时文件", "INFO")
except Exception as e:
self._log(f"⚠️ [JD清理] 临时文件清理失败: {e}", "DEBUG")
async def download_file(self, url: str, save_dir: str = None, max_retries: int = 3) -> str:
"""下载外部文件到本地(带重试机制 + 智能路径选择)
Args:
url: 文件URL
save_dir: 保存目录None时自动选择最佳路径
max_retries: 最大重试次数
Returns:
str: 本地文件路径
"""
# 使用智能路径选择策略
if save_dir is None:
save_dir = self._get_temp_directory()
# 确保目录存在
os.makedirs(save_dir, exist_ok=True)
# 智能提取文件扩展名
default_ext = 'jpg'
ext = self._get_file_extension(url, default_ext)
# 生成唯一文件名
file_name = f"jd_download_{uuid.uuid4().hex[:12]}.{ext}"
save_path = os.path.join(save_dir, file_name)
# 重试机制
for attempt in range(max_retries):
try:
if attempt > 0:
self._log(f"🔄 [JD下载] 第{attempt + 1}次重试下载...", "INFO")
else:
self._log(f"📥 [JD下载] 开始下载: {url[:100]}...", "INFO")
# 使用线程池下载
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None,
lambda: requests.get(url, timeout=60, headers={
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
})
)
# 检查HTTP状态
if response.status_code != 200:
self._log(f"❌ [JD下载] HTTP状态码: {response.status_code}", "ERROR")
if attempt < max_retries - 1:
await asyncio.sleep(2)
continue
raise Exception(f"下载失败HTTP状态码: {response.status_code}")
# 获取文件数据
file_data = response.content
file_size_kb = len(file_data) // 1024
# 检查文件大小限制50MB
if file_size_kb > 51200:
self._log(f"❌ [JD下载] 文件过大: {file_size_kb}KB超过50MB限制", "ERROR")
raise Exception(f"文件过大: {file_size_kb}KB")
# 检查文件是否为空
if file_size_kb == 0:
self._log(f"❌ [JD下载] 下载的文件为空", "ERROR")
if attempt < max_retries - 1:
await asyncio.sleep(1)
continue
raise Exception("下载的文件为空")
# 写入临时文件
with open(save_path, 'wb') as f:
f.write(file_data)
self._log(f"✅ [JD下载] 下载成功,大小: {file_size_kb}KB文件: {file_name}", "SUCCESS")
return save_path
except requests.exceptions.RequestException as e:
self._log(f"❌ [JD下载] 网络请求失败: {e}", "ERROR")
if attempt < max_retries - 1:
await asyncio.sleep(2)
continue
raise
except Exception as e:
self._log(f"❌ [JD下载] 下载失败: {e}", "ERROR")
if attempt < max_retries - 1:
await asyncio.sleep(1)
continue
raise
# 所有重试都失败
raise Exception(f"下载失败:已重试{max_retries}")
async def upload_file_to_jd(self, file_path: str, file_type: str) -> dict:
"""上传文件到京东服务器
Args:
file_path: 本地文件路径
file_type: 文件类型 (image/video)
Returns:
dict: {path: 京东URL, width: 宽度, height: 高度}
"""
try:
if not self.cookies_str or not self.current_aid or not self.current_pin_zj:
raise Exception("缺少必要的认证信息cookies/aid/pin")
self._log(f"📤 开始上传文件到京东: {file_path}", "INFO")
# 读取文件内容
with open(file_path, 'rb') as f:
file_content = f.read()
# 根据文件类型选择API和MIME类型
if file_type == "image" or any(ext in file_path.lower() for ext in ['.png', '.jpg', '.jpeg', '.gif']):
url = "https://imio.jd.com/uploadfile/file/uploadImg.action"
mime_type = 'image/jpeg'
elif file_type == "video" or '.mp4' in file_path.lower():
url = "https://imio.jd.com/uploadfile/file/uploadFile.action"
mime_type = 'video/mp4'
else:
raise Exception(f"不支持的文件类型: {file_path}")
# 准备请求
headers = {
"authority": "imio.jd.com",
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"origin": "https://dongdong.jd.com",
"pragma": "no-cache",
"referer": "https://dongdong.jd.com/",
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"cookie": self.cookies_str
}
files = {
'upload': (os.path.basename(file_path), file_content, mime_type)
}
data = {
'httpsEnable': 'true',
'clientType': 'comet',
'appId': 'im.waiter',
'pin': self.current_pin_zj,
'aid': self.current_aid
}
# 同步请求(在异步上下文中使用 run_in_executor
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None,
lambda: requests.post(url, headers=headers, files=files, data=data, timeout=30)
)
result = response.json()
if result.get('path'):
self._log(f"✅ 文件上传成功: {result.get('path')}", "SUCCESS")
return result
else:
raise Exception(f"上传失败: {result}")
except Exception as e:
self._log(f"❌ 文件上传失败: {e}", "ERROR")
raise
async def get_video_thumbnail(self, video_path: str) -> dict:
"""提取视频封面并上传
Args:
video_path: 视频文件路径
Returns:
dict: {path: 封面URL, width: 宽度, height: 高度}
"""
import cv2
try:
self._log(f"🎬 开始提取视频封面: {video_path}", "INFO")
# 提取视频第一帧
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_position = int(0 * fps) # 第0秒
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_position)
ret, frame = cap.read()
cap.release()
if not ret:
raise Exception("无法读取视频帧")
# 编码为JPG
success, encoded_image = cv2.imencode('.jpg', frame)
if not success:
raise Exception("无法编码图像")
thumbnail_content = encoded_image.tobytes()
# 上传封面(使用图片上传接口)
headers = {
"authority": "imio.jd.com",
"accept": "*/*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"origin": "https://dongdong.jd.com",
"pragma": "no-cache",
"referer": "https://dongdong.jd.com/",
"sec-ch-ua": '"Chromium";v="134", "Not:A-Brand";v="24", "Google Chrome";v="134"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
"cookie": self.cookies_str
}
url = "https://imio.jd.com/uploadfile/file/uploadImg.action"
files = {
'upload': ("thumbnail.jpg", thumbnail_content, 'image/jpeg')
}
data = {
'httpsEnable': 'true',
'clientType': 'comet',
'appId': 'im.waiter',
'pin': self.current_pin_zj,
'aid': self.current_aid
}
# 同步请求
loop = asyncio.get_event_loop()
response = await loop.run_in_executor(
None,
lambda: requests.post(url, headers=headers, files=files, data=data, timeout=30)
)
result = response.json()
if result.get('path'):
self._log(f"✅ 视频封面上传成功: {result.get('path')}", "SUCCESS")
return result
else:
raise Exception(f"封面上传失败: {result}")
except Exception as e:
self._log(f"❌ 视频封面提取失败: {e}", "ERROR")
raise
async def send_image_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, image_url: str):
"""发送图片消息
Args:
ws: WebSocket连接
pin: 客户pin
aid: 账号aid
pin_zj: 客服pin
vender_id: 商家ID
image_url: 图片URL外部URL需要下载后上传
"""
temp_file = None
try:
self._log(f"📷 开始发送图片消息: {image_url}", "INFO")
# 1. 下载图片
temp_file = await self.download_file(image_url)
# 2. 上传到京东服务器
upload_result = await self.upload_file_to_jd(temp_file, "image")
# 3. 发送图片消息
message = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": pin},
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(time.time() * 1000),
"readFlag": 0,
"body": {
"height": upload_result.get("height", 0),
"width": upload_result.get("width", 0),
"url": upload_result.get("path"),
"translated": "",
"param": {"cusVenderId": vender_id},
"type": "image"
}
}
await ws.send(json.dumps(message))
self._log(f"✅ 图片消息发送成功: {pin}", "SUCCESS")
except Exception as e:
self._log(f"❌ 图片消息发送失败: {e}", "ERROR")
raise
finally:
# 清理临时文件
if temp_file and os.path.exists(temp_file):
try:
os.remove(temp_file)
self._log(f"🗑️ 已清理临时文件: {temp_file}", "DEBUG")
except Exception:
pass
async def send_video_message(self, ws, pin: str, aid: str, pin_zj: str, vender_id: str, video_url: str):
"""发送视频消息
Args:
ws: WebSocket连接
pin: 客户pin
aid: 账号aid
pin_zj: 客服pin
vender_id: 商家ID
video_url: 视频URL外部URL需要下载后上传
"""
temp_video = None
try:
self._log(f"🎥 开始发送视频消息: {video_url}", "INFO")
# 1. 下载视频
temp_video = await self.download_file(video_url)
# 2. 提取并上传封面
thumbnail_result = await self.get_video_thumbnail(temp_video)
# 3. 上传视频
await asyncio.sleep(2) # 等待2秒避免请求过快
video_result = await self.upload_file_to_jd(temp_video, "video")
# 4. 发送视频消息
message = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": pin},
"id": hashlib.md5(str(int(time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(time.time() * 1000),
"readFlag": 0,
"body": {
"desc": "",
"duration": 6,
"param": {},
"reupload": "false",
"size": os.path.getsize(temp_video),
"thumbHeight": thumbnail_result.get("height", 0),
"thumbWidth": thumbnail_result.get("width", 0),
"thumbnail": thumbnail_result.get("path"),
"url": video_result.get("path"),
"type": "video"
}
}
await ws.send(json.dumps(message))
self._log(f"✅ 视频消息发送成功: {pin}", "SUCCESS")
except Exception as e:
self._log(f"❌ 视频消息发送失败: {e}", "ERROR")
raise
finally:
# 清理临时文件
if temp_video and os.path.exists(temp_video):
try:
os.remove(temp_video)
self._log(f"🗑️ 已清理临时视频: {temp_video}", "DEBUG")
except Exception:
pass
def get_userinfo(self, response_text): def get_userinfo(self, response_text):
"""获取用户 pin 并存入 pins 列表""" """获取用户 pin 并存入 pins 列表"""
pin = jsonpath.jsonpath(json.loads(response_text), "$..from.pin") pin = jsonpath.jsonpath(json.loads(response_text), "$..from.pin")
@@ -1104,11 +475,6 @@ class FixJdCookie:
print("✅ DEBUG 进入message_monitoring方法") print("✅ DEBUG 进入message_monitoring方法")
print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}") print(f"参数验证: cookies={bool(cookies_str)}, aid={aid}, pin_zj={pin_zj}")
# 🔥 保存认证信息,用于文件上传
self.cookies_str = cookies_str
self.current_aid = aid
self.current_pin_zj = pin_zj
# 连接后端AI服务 - 使用店铺ID或venderId # 连接后端AI服务 - 使用店铺ID或venderId
store_id = str(store.get('id', '')) or str(vender_id) store_id = str(store.get('id', '')) or str(vender_id)
self._log(f"🔗 尝试连接后端服务店铺ID: {store_id}", "DEBUG") self._log(f"🔗 尝试连接后端服务店铺ID: {store_id}", "DEBUG")
@@ -1140,13 +506,6 @@ class FixJdCookie:
# print(f"✅ 连接状态: open={ws.open}, closed={ws.closed}") # print(f"✅ 连接状态: open={ws.open}, closed={ws.closed}")
print(f"🖥️ 服务端地址: {ws.remote_address}") print(f"🖥️ 服务端地址: {ws.remote_address}")
# 连接成功后,获取并上报一次客服列表(最小改动恢复)
try:
await self.get_all_customer(ws, aid, pin_zj)
await self.send_staff_list_to_backend(store.get('id', ''))
except Exception as e:
self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR")
# --- 注册连接信息到全局管理 # --- 注册连接信息到全局管理
shop_key = f"京东:{store['id']}" shop_key = f"京东:{store['id']}"
loop = asyncio.get_running_loop() loop = asyncio.get_running_loop()
@@ -1157,8 +516,7 @@ class FixJdCookie:
aid=aid, aid=aid,
pin_zj=pin_zj, pin_zj=pin_zj,
platform="京东", platform="京东",
loop=loop, loop=loop
cookies_str=cookies_str # 🔥 传递cookies用于文件上传
) )
await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj) await self.waiter_status_switch(ws=ws, aid=aid, pin_zj=pin_zj)
@@ -1172,44 +530,12 @@ class FixJdCookie:
print(f"等待监听消息-{datetime.now()}") print(f"等待监听消息-{datetime.now()}")
response = await asyncio.wait_for(ws.recv(), timeout=1) response = await asyncio.wait_for(ws.recv(), timeout=1)
print(f"原始消息类型:{type(response)}, 消息体为: {response}") print(f"原始消息类型:{type(response)}, 消息体为: {response}")
# 🔧 修复:检测被踢下线消息
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
# 检查是否为server_msg类型且code=5被踢下线
if json_resp.get("type") == "server_msg":
body = json_resp.get("body", {})
if body.get("code") == 5:
msgtext = body.get("msgtext", "账号在其他设备登录")
self._log(f"⚠️ 收到被踢下线消息: {msgtext}", "WARNING")
self._log("⚠️ 检测到多端登录冲突,请确保:", "WARNING")
self._log(" 1. 关闭网页版京东咚咚", "WARNING")
self._log(" 2. 关闭其他客户端", "WARNING")
self._log(" 3. 确认只有本客户端连接", "WARNING")
# 通知GUI显示弹窗
try:
from WebSocket.backend_singleton import get_websocket_manager
ws_manager = get_websocket_manager()
if ws_manager:
# 从platform_listeners获取店铺名称
store_name = "京东店铺"
for key, info in ws_manager.platform_listeners.items():
if info.get('store_id') == store:
store_name = info.get('store_name', '') or "京东店铺"
break
# 传递 store_id 参数,用于通知后端
ws_manager.notify_platform_kicked("京东", store_name, msgtext, store_id=store)
except Exception as notify_error:
self._log(f"通知GUI失败: {notify_error}", "ERROR")
# 不再自动重连,等待用户处理
stop_event.set()
break
await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store) await self.process_incoming_message(response, ws, aid, pin_zj, vender_id, store)
# print(json_resp) # 安全解析消息
json_resp = json.loads(response) if isinstance(response, (str, bytes)) else response
print(json_resp)
ver = json_resp.get("ver") ver = json_resp.get("ver")
print(f"版本{ver}") print(f"版本{ver}")
@@ -1253,8 +579,9 @@ class FixJdCookie:
if not await self.handle_reconnect(e): if not await self.handle_reconnect(e):
break break
# 关闭后端服务连接JDBackendService没有close方法跳过 # 关闭后端服务连接
if self.backend_connected: if self.backend_connected:
await self.backend_service.close()
self.backend_connected = False self.backend_connected = False
self._log("🛑 消息监听已停止", "INFO") self._log("🛑 消息监听已停止", "INFO")

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,4 @@
# WebSocket/BackendClient.py # WebSocket/BackendClient.py
# hsc1
import json import json
import threading import threading
import time import time
@@ -32,9 +31,6 @@ class BackendClient:
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调 self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.token_error_callback: Optional[Callable] = None # 新增token错误回调 self.token_error_callback: Optional[Callable] = None # 新增token错误回调
self.version_callback: Optional[Callable] = None # 新增:版本检查回调 self.version_callback: Optional[Callable] = None # 新增:版本检查回调
self.disconnect_callback: Optional[Callable] = None # 新增:被踢下线回调
self.balance_insufficient_callback: Optional[Callable] = None # 新增:余额不足回调
self.log_callback: Optional[Callable] = None # 新增:日志回调
self.is_connected = False self.is_connected = False
@@ -50,18 +46,6 @@ class BackendClient:
self.loop = None self.loop = None
self.thread = None self.thread = None
def _log(self, message: str, level: str = "INFO"):
"""Unified logging method that works in both dev and packaged environments"""
# Always print to console (visible in dev mode)
print(f"[{level}] {message}")
# Also call the log callback if available (for file logging)
if self.log_callback:
try:
self.log_callback(message, level)
except Exception:
pass
def connect(self): def connect(self):
"""连接到WebSocket服务器""" """连接到WebSocket服务器"""
if self.is_connected: if self.is_connected:
@@ -105,7 +89,7 @@ class BackendClient:
"""连接并监听消息 - 带重连机制""" """连接并监听消息 - 带重连机制"""
while not self.should_stop: while not self.should_stop:
try: try:
self._log(f"正在连接后端WebSocket: {self.url}") print(f"正在连接后端WebSocket: {self.url}")
# 建立连接可配置的ping设置 # 建立连接可配置的ping设置
from config import WS_PING_INTERVAL, WS_PING_TIMEOUT, WS_ENABLE_PING from config import WS_PING_INTERVAL, WS_PING_TIMEOUT, WS_ENABLE_PING
@@ -121,7 +105,7 @@ class BackendClient:
max_queue=32, # 最大队列大小 max_queue=32, # 最大队列大小
compression=None # 禁用压缩以提高性能 compression=None # 禁用压缩以提高性能
) )
self._log(f"已启用心跳ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s") print(f"[连接] 已启用心跳ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s")
else: else:
self.websocket = await websockets.connect( self.websocket = await websockets.connect(
self.url, self.url,
@@ -129,44 +113,30 @@ class BackendClient:
max_queue=32, max_queue=32,
compression=None compression=None
) )
self._log("已禁用心跳机制", "WARNING") print("[连接] 已禁用心跳机制")
self.is_connected = True self.is_connected = True
# 🔥 在重置之前记录是否是重连(用于后续上报平台状态)
was_reconnecting = self.reconnect_attempts > 0
self.reconnect_attempts = 0 # 重置重连计数 self.reconnect_attempts = 0 # 重置重连计数
self.is_reconnecting = False self.is_reconnecting = False
self._log("后端WebSocket连接成功", "SUCCESS") print("后端WebSocket连接成功")
# 等待连接稳定后再发送状态通知 # 等待连接稳定后再发送状态通知
await asyncio.sleep(0.5) await asyncio.sleep(0.5)
# 发送连接状态通知给后端 # 发送连接状态通知给后端
self._notify_connection_status(True, is_reconnect=was_reconnecting) self._notify_connection_status(True)
self.on_connected(was_reconnecting) self.on_connected()
# 消息循环 # 消息循环
async for message in self.websocket: async for message in self.websocket:
try: try:
# 🔍 添加心跳检测日志 # 打印原始文本帧与长度
try: try:
raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message) raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message)
# 解析消息类型
data_preview = json.loads(message)
msg_type = data_preview.get('type', 'unknown')
# 心跳相关消息用DEBUG级别其他消息用INFO级别
if msg_type in ['pong', 'connection_status_ack']:
self._log(f"💓 [心跳] 收到后端响应: {msg_type}", "DEBUG")
else:
self._log(f"📨 [后端] 收到消息: type={msg_type}, 长度={raw_len}字节", "DEBUG")
print(f"后端发送消息体内容:{message}") print(f"后端发送消息体内容:{message}")
except Exception: except Exception:
pass pass
data = json.loads(message) data = json.loads(message)
self.on_message_received(data) self.on_message_received(data)
except json.JSONDecodeError: except json.JSONDecodeError:
@@ -178,13 +148,13 @@ class BackendClient:
# 详细分析断开原因 # 详细分析断开原因
if e.code == 1006: if e.code == 1006:
self._log("WebSocket异常关闭 (1006): 可能是心跳超时或网络问题", "WARNING") print(f"[重连] WebSocket异常关闭 (1006): 可能是心跳超时或网络问题")
elif e.code == 1000: elif e.code == 1000:
self._log("WebSocket正常关闭 (1000): 服务端主动断开", "INFO") print(f"[重连] WebSocket正常关闭 (1000): 服务端主动断开")
elif e.code == 1001: elif e.code == 1001:
self._log("WebSocket关闭 (1001): 端点离开", "INFO") print(f"[重连] WebSocket关闭 (1001): 端点离开")
else: else:
self._log(f"WebSocket关闭 ({e.code}): {e.reason}", "WARNING") print(f"[重连] WebSocket关闭 ({e.code}): {e.reason}")
self._handle_connection_closed(e) self._handle_connection_closed(e)
if not await self._should_reconnect(): if not await self._should_reconnect():
@@ -214,14 +184,14 @@ class BackendClient:
def _handle_connection_closed(self, error): def _handle_connection_closed(self, error):
"""处理连接关闭""" """处理连接关闭"""
error_msg = f"WebSocket连接已关闭: {error.code} {error.reason if hasattr(error, 'reason') else ''}" error_msg = f"WebSocket连接已关闭: {error.code} {error.reason if hasattr(error, 'reason') else ''}"
self._log(error_msg, "WARNING") print(f"[重连] {error_msg}")
# 特殊处理ping超时等情况 # 特殊处理ping超时等情况
if hasattr(error, 'code'): if hasattr(error, 'code'):
if error.code == 1011: # Internal error (ping timeout) if error.code == 1011: # Internal error (ping timeout)
self._log("检测到ping超时这是常见的网络问题", "WARNING") print("[重连] 检测到ping超时这是常见的网络问题")
elif error.code == 1006: # Abnormal closure elif error.code == 1006: # Abnormal closure
self._log("检测到异常关闭,可能是网络中断或心跳超时", "WARNING") print("[重连] 检测到异常关闭,可能是网络中断")
if not self.is_reconnecting: if not self.is_reconnecting:
self.on_error(error_msg) self.on_error(error_msg)
@@ -229,25 +199,25 @@ class BackendClient:
def _handle_network_error(self, error): def _handle_network_error(self, error):
"""处理网络错误""" """处理网络错误"""
error_msg = f"网络连接错误: {type(error).__name__} - {str(error)}" error_msg = f"网络连接错误: {type(error).__name__} - {str(error)}"
self._log(error_msg, "ERROR") print(f"[重连] {error_msg}")
if not self.is_reconnecting: if not self.is_reconnecting:
self.on_error(error_msg) self.on_error(error_msg)
def _handle_general_error(self, error): def _handle_general_error(self, error):
"""处理一般错误""" """处理一般错误"""
error_msg = f"WebSocket连接异常: {type(error).__name__} - {str(error)}" error_msg = f"WebSocket连接异常: {type(error).__name__} - {str(error)}"
self._log(error_msg, "ERROR") print(f"[重连] {error_msg}")
if not self.is_reconnecting: if not self.is_reconnecting:
self.on_error(error_msg) self.on_error(error_msg)
async def _should_reconnect(self) -> bool: async def _should_reconnect(self) -> bool:
"""判断是否应该重连""" """判断是否应该重连"""
if self.should_stop: if self.should_stop:
self._log("程序正在关闭,停止重连", "INFO") print("[重连] 程序正在关闭,停止重连")
return False return False
if self.reconnect_attempts >= self.max_reconnect_attempts: if self.reconnect_attempts >= self.max_reconnect_attempts:
self._log(f"已达到最大重连次数({self.max_reconnect_attempts}),停止重连", "ERROR") print(f"[重连] 已达到最大重连次数({self.max_reconnect_attempts}),停止重连")
# 通知上层重连失败 # 通知上层重连失败
if not self.is_reconnecting: if not self.is_reconnecting:
self.on_error(f"重连失败:已达到最大重连次数({self.max_reconnect_attempts})") self.on_error(f"重连失败:已达到最大重连次数({self.max_reconnect_attempts})")
@@ -265,7 +235,7 @@ class BackendClient:
self.reconnect_attempts += 1 self.reconnect_attempts += 1
self.is_reconnecting = True self.is_reconnecting = True
self._log(f"{self.reconnect_attempts}次重连尝试,等待{delay:.1f}秒...", "INFO") print(f"[重连] {self.reconnect_attempts}次重连尝试,等待{delay:.1f}秒...")
# 分割等待时间,支持快速退出 # 分割等待时间,支持快速退出
wait_steps = max(1, int(delay)) wait_steps = max(1, int(delay))
@@ -295,10 +265,7 @@ class BackendClient:
login: Callable = None, login: Callable = None,
success: Callable = None, success: Callable = None,
token_error: Callable = None, token_error: Callable = None,
version: Callable = None, version: Callable = None):
disconnect: Callable = None,
balance_insufficient: Callable = None,
log: Callable = None):
"""设置各种消息类型的回调函数""" """设置各种消息类型的回调函数"""
if store_list: if store_list:
self.store_list_callback = store_list self.store_list_callback = store_list
@@ -320,77 +287,56 @@ class BackendClient:
self.token_error_callback = token_error self.token_error_callback = token_error
if version: if version:
self.version_callback = version self.version_callback = version
if disconnect:
self.disconnect_callback = disconnect
if balance_insufficient:
self.balance_insufficient_callback = balance_insufficient
if log:
self.log_callback = log
def on_connected(self, was_reconnecting: bool = False): def on_connected(self):
"""连接成功时的处理""" """连接成功时的处理"""
if was_reconnecting: if self.reconnect_attempts > 0:
self._log("后端WebSocket重连成功", "SUCCESS") print(f"[重连] 后端WebSocket重连成功(第{self.reconnect_attempts}次尝试)")
# 重连成功后上报平台状态给后端
self._report_reconnect_status()
else: else:
self._log("后端WebSocket首次连接成功", "SUCCESS") print("后端WebSocket连接成功")
# 重连成功后可选择上报状态给后端
if self.reconnect_attempts > 0:
self._report_reconnect_status()
# 不再主动请求 get_store避免与后端不兼容导致协程未完成 # 不再主动请求 get_store避免与后端不兼容导致协程未完成
def _report_reconnect_status(self): def _report_reconnect_status(self):
"""重连成功后上报当前状态""" """重连成功后上报当前状态(可选)"""
try: try:
# 获取当前已连接的平台列表 # 获取当前已连接的平台列表
from WebSocket.backend_singleton import get_websocket_manager from WebSocket.backend_singleton import get_websocket_manager
manager = get_websocket_manager() manager = get_websocket_manager()
if hasattr(manager, 'platform_listeners') and manager.platform_listeners: if hasattr(manager, 'platform_listeners') and manager.platform_listeners:
platform_count = len(manager.platform_listeners)
self._log(f"🔄 检测到 {platform_count} 个活跃平台连接,准备重新上报状态", "INFO")
# 延迟1秒确保后端完全准备好
import time
time.sleep(1.0)
for platform_key, listener_info in manager.platform_listeners.items(): for platform_key, listener_info in manager.platform_listeners.items():
store_id = listener_info.get('store_id') store_id = listener_info.get('store_id')
platform = listener_info.get('platform') platform = listener_info.get('platform')
store_name = listener_info.get('store_name', '')
if store_id and platform: if store_id and platform:
# 上报平台仍在连接状态 # 上报平台仍在连接状态
try: try:
store_display = store_name or store_id[:8] + "..."
reconnect_message = { reconnect_message = {
"type": "connect_message", "type": "connect_message",
"store_id": store_id, "store_id": store_id,
"status": True, "status": True,
"cookies": "" # 重连时无需再次发送cookies "content": f"GUI重连成功{platform}平台状态正常"
} }
# 异步发送,不阻塞连接过程
# 同步发送,确保发送成功 asyncio.run_coroutine_threadsafe(
future = asyncio.run_coroutine_threadsafe(
self._send_to_backend(reconnect_message), self._send_to_backend(reconnect_message),
self.loop self.loop
) )
# 等待发送完成最多2秒 print(f"[重连] 已上报{platform}平台状态")
future.result(timeout=2)
self._log(f"✅ 已重新上报 {platform} 平台状态: {store_display}", "SUCCESS")
except Exception as e: except Exception as e:
self._log(f" 上报 {platform} 平台状态失败: {e}", "ERROR") print(f"[重连] 上报{platform}平台状态失败: {e}")
import traceback
self._log(f"详细错误: {traceback.format_exc()}", "DEBUG")
else: else:
self._log("当前无活跃平台连接,跳过状态上报", "INFO") print("[重连] 当前无活跃平台连接,跳过状态上报")
except Exception as e: except Exception as e:
self._log(f"状态上报过程异常: {e}", "ERROR") print(f"[重连] 状态上报过程异常: {e}")
import traceback
self._log(f"详细错误: {traceback.format_exc()}", "DEBUG")
def _notify_connection_status(self, connected: bool, is_reconnect: bool = False): def _notify_connection_status(self, connected: bool):
"""通知后端连接状态变化""" """通知后端连接状态变化"""
try: try:
if not self.loop: if not self.loop:
@@ -399,21 +345,30 @@ class BackendClient:
# 获取当前活跃平台的store_id # 获取当前活跃平台的store_id
active_store_id = None active_store_id = None
try: try:
from WebSocket.backend_singleton import get_websocket_manager from Utils.JD.JdUtils import WebsocketManager as JdManager
manager = get_websocket_manager() from Utils.Dy.DyUtils import DouYinWebsocketManager as DyManager
from Utils.Pdd.PddUtils import WebsocketManager as PddManager
# 从 platform_listeners 获取活跃平台 # 检查各平台是否有活跃连接
if hasattr(manager, 'platform_listeners') and manager.platform_listeners: for mgr_class, platform_name in [(JdManager, "京东"), (DyManager, "抖音"), (PddManager, "拼多多")]:
# 获取第一个活跃平台的 store_id try:
for platform_key, listener_info in manager.platform_listeners.items(): mgr = mgr_class()
store_id = listener_info.get('store_id') if hasattr(mgr, '_store') and mgr._store:
platform = listener_info.get('platform') for shop_key, entry in mgr._store.items():
if store_id and platform: if entry and entry.get('platform'):
# 从shop_key中提取store_id格式平台:store_id
if ':' in shop_key:
_, store_id = shop_key.split(':', 1)
active_store_id = store_id active_store_id = store_id
self._log(f"检测到活跃{platform}平台: {store_id}", "DEBUG") print(f"[状态] 检测到活跃{platform_name}平台: {store_id}")
break
if active_store_id:
break break
except Exception as e: except Exception as e:
self._log(f"获取活跃平台信息失败: {e}", "DEBUG") print(f"[状态] 检查{platform_name}平台失败: {e}")
continue
except Exception as e:
print(f"[状态] 获取活跃平台信息失败: {e}")
status_message = { status_message = {
"type": "connection_status", "type": "connection_status",
@@ -422,36 +377,26 @@ class BackendClient:
"client_uuid": self.uuid "client_uuid": self.uuid
} }
# 🔥 新增:如果是重连,添加重连标识
if is_reconnect:
status_message["is_reconnect"] = True
# 如果有活跃平台添加store_id # 如果有活跃平台添加store_id
if active_store_id: if active_store_id:
status_message["store_id"] = active_store_id status_message["store_id"] = active_store_id
self._log(f"添加store_id到状态消息: {active_store_id}", "DEBUG") print(f"[状态] 添加store_id到状态消息: {active_store_id}")
else: else:
self._log("未检测到活跃平台不添加store_id", "DEBUG") print(f"[状态] 未检测到活跃平台不添加store_id")
# 异步发送状态通知 # 异步发送状态通知
asyncio.run_coroutine_threadsafe( asyncio.run_coroutine_threadsafe(
self._send_to_backend(status_message), self._send_to_backend(status_message),
self.loop self.loop
) )
#
# # 🔥 等待发送完成(可选,避免警告)
# try:
# future.result(timeout=2) # 最多等待2秒
# except Exception as send_error:
# self._log(f"发送状态通知异常: {send_error}", "DEBUG")
status_text = "连接" if connected else "断开" status_text = "连接" if connected else "断开"
self._log(f"已通知后端GUI客户端{status_text}", "DEBUG") print(f"[状态] 已通知后端GUI客户端{status_text}")
except Exception as e: except Exception as e:
self._log(f"发送状态通知失败: {e}", "ERROR") print(f"[状态] 发送状态通知失败: {e}")
import traceback import traceback
self._log(f"详细错误: {traceback.format_exc()}", "DEBUG") print(f"[状态] 详细错误: {traceback.format_exc()}")
def on_message_received(self, message: Dict[str, Any]): def on_message_received(self, message: Dict[str, Any]):
"""处理接收到的消息 - 根据WebSocket文档v2更新""" """处理接收到的消息 - 根据WebSocket文档v2更新"""
@@ -498,10 +443,6 @@ class BackendClient:
self._handle_staff_list(message) self._handle_staff_list(message)
elif msg_type == 'version_response': # 新增:版本检查响应 elif msg_type == 'version_response': # 新增:版本检查响应
self._handle_version_response(message) self._handle_version_response(message)
elif msg_type == 'disconnect': # 新增:被踢下线
self._handle_disconnect(message)
elif msg_type == 'balance_insufficient': # 新增:余额不足
self._handle_balance_insufficient(message)
else: else:
print(f"未知消息类型: {msg_type}") print(f"未知消息类型: {msg_type}")
@@ -717,8 +658,7 @@ class BackendClient:
store_id = message.get('store_id', '') store_id = message.get('store_id', '')
data = message.get('data') data = message.get('data')
content = message.get('content', '') content = message.get('content', '')
msg_type = message.get('msg_type', 'text') # 获取消息类型,默认为text print(f"[{store_id}] [{message.get('msg_type', 'unknown')}] : {content}")
print(f"[{store_id}] [{msg_type}] : {content}")
# 尝试将后端AI/客服回复转发到对应平台 # 尝试将后端AI/客服回复转发到对应平台
try: try:
@@ -729,16 +669,13 @@ class BackendClient:
platform_type = self._get_platform_by_store_id(store_id) platform_type = self._get_platform_by_store_id(store_id)
if platform_type == "京东": if platform_type == "京东":
# 🔥 传递msg_type参数支持图片/视频等类型 self._forward_to_jd(store_id, recv_pin, content)
self._forward_to_jd(store_id, recv_pin, content, msg_type)
elif platform_type == "抖音": elif platform_type == "抖音":
# 传递msg_type参数支持图片/视频等类型 self._forward_to_douyin(store_id, recv_pin, content)
self._forward_to_douyin(store_id, recv_pin, content, msg_type)
elif platform_type == "千牛": elif platform_type == "千牛":
self._forward_to_qianniu(store_id, recv_pin, content) self._forward_to_qianniu(store_id, recv_pin, content)
elif platform_type == "拼多多": elif platform_type == "拼多多":
# 传递msg_type参数支持图片/视频等类型 self._forward_to_pdd(store_id, recv_pin, content)
self._forward_to_pdd(store_id, recv_pin, content, msg_type)
else: else:
print(f"[Forward] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}") print(f"[Forward] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
except Exception as e: except Exception as e:
@@ -762,15 +699,8 @@ class BackendClient:
print(f"获取平台类型失败: {e}") print(f"获取平台类型失败: {e}")
return "" return ""
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"): def _forward_to_jd(self, store_id: str, recv_pin: str, content: str):
"""转发消息到京东平台(支持文本/图片/视频) """转发消息到京东平台"""
Args:
store_id: 店铺ID
recv_pin: 接收者pin
content: 消息内容文本内容或URL
msg_type: 消息类型text/image/video
"""
try: try:
from Utils.JD.JdUtils import WebsocketManager as JDWSManager from Utils.JD.JdUtils import WebsocketManager as JDWSManager
jd_mgr = JDWSManager() jd_mgr = JDWSManager()
@@ -786,62 +716,52 @@ class BackendClient:
pin_zj = platform_info.get('pin_zj') pin_zj = platform_info.get('pin_zj')
vender_id = platform_info.get('vender_id') vender_id = platform_info.get('vender_id')
loop = platform_info.get('loop') loop = platform_info.get('loop')
cookies_str = platform_info.get('cookies_str') # 🔥 获取cookies用于文件上传
print( print(
f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} has_cookies={bool(cookies_str)} recv_pin={recv_pin} msg_type={msg_type}") f"[JD Forward] shop_key={shop_key} has_ws={bool(ws)} aid={aid} pin_zj={pin_zj} vender_id={vender_id} has_loop={bool(loop)} recv_pin={recv_pin}")
if ws and aid and pin_zj and vender_id and loop and content: if ws and aid and pin_zj and vender_id and loop and content:
# 🔥 获取 FixJdCookie 实例,使用其 send_message 方法(支持多媒体)
async def _send(): async def _send():
from Utils.JD.JdUtils import FixJdCookie import hashlib as _hashlib
# 创建临时实例用于发送 import time as _time
jd_instance = FixJdCookie() import json as _json
# 🔥 设置认证信息(用于图片/视频上传) msg = {
jd_instance.cookies_str = cookies_str "ver": "4.3",
jd_instance.current_aid = aid "type": "chat_message",
jd_instance.current_pin_zj = pin_zj "from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
# 调用支持多媒体的 send_message 方法 "to": {"app": "im.customer", "pin": recv_pin},
await jd_instance.send_message( "id": _hashlib.md5(str(int(_time.time() * 1000)).encode()).hexdigest(),
ws=ws, "lang": "zh_CN",
pin=recv_pin, "aid": aid,
aid=aid, "timestamp": int(_time.time() * 1000),
pin_zj=pin_zj, "readFlag": 0,
vender_id=vender_id, "body": {
content=content, "content": content,
msg_type=msg_type "translated": False,
) "param": {"cusVenderId": vender_id},
"type": "text"
}
}
await ws.send(_json.dumps(msg))
import asyncio as _asyncio import asyncio as _asyncio
_future = _asyncio.run_coroutine_threadsafe(_send(), loop) _future = _asyncio.run_coroutine_threadsafe(_send(), loop)
try: try:
_future.result(timeout=60) # 图片/视频需要更长时间 _future.result(timeout=2)
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, type={msg_type}, content_len={len(content)}") print(f"[JD Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
except Exception as fe: except Exception as fe:
print(f"[JD Forward] 转发提交失败: {fe}") print(f"[JD Forward] 转发提交失败: {fe}")
import traceback
traceback.print_exc()
else: else:
print("[JD Forward] 条件不足,未转发:", print("[JD Forward] 条件不足,未转发:",
{ {
'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj), 'has_ws': bool(ws), 'has_aid': bool(aid), 'has_pin_zj': bool(pin_zj),
'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_cookies': bool(cookies_str), 'has_vender_id': bool(vender_id), 'has_loop': bool(loop), 'has_content': bool(content)
'has_content': bool(content)
}) })
except Exception as e: except Exception as e:
print(f"[JD Forward] 转发失败: {e}") print(f"[JD Forward] 转发失败: {e}")
import traceback
traceback.print_exc()
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"): def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str):
"""转发消息到抖音平台 """转发消息到抖音平台"""
Args:
store_id: 店铺ID
recv_pin: 接收者ID
content: 消息内容
msg_type: 消息类型text/image/video
"""
try: try:
from Utils.Dy.DyUtils import DouYinWebsocketManager from Utils.Dy.DyUtils import DouYinWebsocketManager
dy_mgr = DouYinWebsocketManager() dy_mgr = DouYinWebsocketManager()
@@ -857,7 +777,7 @@ class BackendClient:
message_handler = platform_info.get('message_handler') message_handler = platform_info.get('message_handler')
print( print(
f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin} msg_type={msg_type}") f"[DY Forward] shop_key={shop_key} has_bot={bool(douyin_bot)} has_handler={bool(message_handler)} recv_pin={recv_pin}")
if douyin_bot and message_handler and content: if douyin_bot and message_handler and content:
# 在消息处理器的事件循环中发送消息 # 在消息处理器的事件循环中发送消息
@@ -866,16 +786,16 @@ class BackendClient:
# 获取消息处理器的事件循环 # 获取消息处理器的事件循环
loop = message_handler._loop loop = message_handler._loop
if loop and not loop.is_closed(): if loop and not loop.is_closed():
# 在事件循环中执行发送传递msg_type参数 # 在事件循环中执行发送
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
message_handler.send_message_external(recv_pin, content, msg_type), message_handler.send_message_external(recv_pin, content),
loop loop
) )
# 等待结果 # 等待结果
try: try:
result = future.result(timeout=30) # 图片/视频需要更长时间 result = future.result(timeout=5)
if result: if result:
print(f"[DY Forward] 已转发到平台: pin={recv_pin}, type={msg_type}, content_len={len(content)}") print(f"[DY Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
else: else:
print(f"[DY Forward] 转发失败: 消息处理器返回False") print(f"[DY Forward] 转发失败: 消息处理器返回False")
except Exception as fe: except Exception as fe:
@@ -909,15 +829,8 @@ class BackendClient:
except Exception as e: except Exception as e:
print(f"[QN Forward] 转发失败: {e}") print(f"[QN Forward] 转发失败: {e}")
def _forward_to_pdd(self, store_id: str, recv_pin: str, content: str, msg_type: str = "text"): def _forward_to_pdd(self, store_id: str, recv_pin: str, content: str):
"""转发消息到拼多多平台 """转发消息到拼多多平台"""
Args:
store_id: 店铺ID
recv_pin: 接收者ID
content: 消息内容文本或图片URL
msg_type: 消息类型text/image/video
"""
try: try:
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager() pdd_mgr = PDDWSManager()
@@ -933,22 +846,22 @@ class BackendClient:
loop = platform_info.get('loop') loop = platform_info.get('loop')
print( print(
f"[PDD Forward] shop_key={shop_key} has_pdd_instance={bool(pdd_instance)} has_loop={bool(loop)} recv_pin={recv_pin} msg_type={msg_type}") f"[PDD Forward] shop_key={shop_key} has_pdd_instance={bool(pdd_instance)} has_loop={bool(loop)} recv_pin={recv_pin}")
if pdd_instance and loop and content: if pdd_instance and loop and content:
# 在拼多多实例的事件循环中发送消息 # 在拼多多实例的事件循环中发送消息
def send_in_loop(): def send_in_loop():
try: try:
# 在事件循环中执行发送传递msg_type参数 # 在事件循环中执行发送
future = asyncio.run_coroutine_threadsafe( future = asyncio.run_coroutine_threadsafe(
pdd_instance.send_message_external(recv_pin, content, msg_type), pdd_instance.send_message_external(recv_pin, content),
loop loop
) )
# 等待结果 # 等待结果
try: try:
result = future.result(timeout=10) # 拼多多可能需要更长时间 result = future.result(timeout=10) # 拼多多可能需要更长时间
if result: if result:
print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, type={msg_type}, content_len={len(content)}") print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, content_len={len(content)}")
else: else:
print(f"[PDD Forward] 转发失败: 拼多多实例返回False") print(f"[PDD Forward] 转发失败: 拼多多实例返回False")
except Exception as fe: except Exception as fe:
@@ -1237,11 +1150,10 @@ class BackendClient:
self.get_store() self.get_store()
def _handle_login(self, message: Dict[str, Any]): def _handle_login(self, message: Dict[str, Any]):
"""处理平台登录消息新版type=login, cookies/login_params, store_id, platform_name, store_name""" """处理平台登录消息新版type=login, cookies/login_params, store_id, platform_name"""
cookies = message.get('cookies', '') cookies = message.get('cookies', '')
store_id = message.get('store_id', '') store_id = message.get('store_id', '')
platform_name = message.get('platform_name', '') platform_name = message.get('platform_name', '')
store_name = message.get('store_name', '') # 新增:获取店铺名称
content = message.get('content', '') content = message.get('content', '')
data = message.get('data', {}) data = message.get('data', {})
@@ -1250,18 +1162,17 @@ class BackendClient:
(("拼多多登录" in content and data.get('login_params')) or (("拼多多登录" in content and data.get('login_params')) or
("抖音登录" in content and data.get('login_flow')))): ("抖音登录" in content and data.get('login_flow')))):
# 登录参数模式 - 传递完整的消息JSON给处理器 # 登录参数模式 - 传递完整的消息JSON给处理器
print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_name or store_id}, 类型={content}") print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
if self.login_callback: if self.login_callback:
# 传递完整的JSON消息让处理器来解析登录参数 # 传递完整的JSON消息让处理器来解析登录参数
import json import json
full_message = json.dumps(message) full_message = json.dumps(message)
self.login_callback(platform_name, store_id, full_message, store_name) self.login_callback(platform_name, store_id, full_message)
else: else:
# 普通Cookie模式 # 普通Cookie模式
print( print(f"收到登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}")
f"收到登录指令: 平台={platform_name}, 店铺={store_name or store_id}, cookies_len={len(cookies) if cookies else 0}")
if self.login_callback: if self.login_callback:
self.login_callback(platform_name, store_id, cookies, store_name) self.login_callback(platform_name, store_id, cookies)
def _handle_error_message(self, message: Dict[str, Any]): def _handle_error_message(self, message: Dict[str, Any]):
"""处理错误消息""" """处理错误消息"""
@@ -1318,29 +1229,6 @@ class BackendClient:
if self.version_callback: if self.version_callback:
self.version_callback(message) self.version_callback(message)
def _handle_disconnect(self, message: Dict[str, Any]):
"""处理被踢下线消息"""
disconnect_message = message.get('content', '您的账号在其他设备登录,当前连接已断开')
print(f"[断开] 收到后端断开通知: {disconnect_message}")
# 停止重连机制(不再尝试重连)
self.should_stop = True
# 触发断开回调
if self.disconnect_callback:
self.disconnect_callback(disconnect_message)
def _handle_balance_insufficient(self, message: Dict[str, Any]):
"""处理余额不足消息"""
balance_message = message.get('content', '所有平台已断开。请充值条数后,重新连接')
self._log(f"⚠️ 余额不足: {balance_message}", "WARNING")
# 触发余额不足回调(在主线程中处理断开逻辑)
if self.balance_insufficient_callback:
self.balance_insufficient_callback(balance_message)
# ==================== 辅助方法 ==================== # ==================== 辅助方法 ====================
def set_token(self, token: str): def set_token(self, token: str):

View File

@@ -4,8 +4,6 @@ from typing import Optional, Callable
import threading import threading
import asyncio import asyncio
from threading import Thread from threading import Thread
from PyQt5.QtCore import QObject, pyqtSignal
# 创建新的后端客户端 # 创建新的后端客户端
from WebSocket.BackendClient import BackendClient from WebSocket.BackendClient import BackendClient
from Utils.JD.JdUtils import JDListenerForGUI as JDListenerForGUI_WS from Utils.JD.JdUtils import JDListenerForGUI as JDListenerForGUI_WS
@@ -13,12 +11,6 @@ from Utils.Dy.DyUtils import DouYinListenerForGUI as DYListenerForGUI_WS
from Utils.Pdd.PddUtils import PddListenerForGUI as PDDListenerForGUI_WS from Utils.Pdd.PddUtils import PddListenerForGUI as PDDListenerForGUI_WS
from Utils.QianNiu.QianNiuUtils import QianNiuListenerForGUI as QNListenerForGUI_WS from Utils.QianNiu.QianNiuUtils import QianNiuListenerForGUI as QNListenerForGUI_WS
class PlatformConnectionSignals(QObject):
"""平台连接信号(线程安全)"""
platform_connected = pyqtSignal(str, str) # (platform_name, store_id)
_backend_client = None _backend_client = None
@@ -44,27 +36,16 @@ class WebSocketManager:
self.gui_update_callback = None self.gui_update_callback = None
self.platform_listeners = {} # 存储各平台的监听器 self.platform_listeners = {} # 存储各平台的监听器
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增 self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
# 平台连接信号(线程安全)
self.platform_signals = PlatformConnectionSignals()
self.platform_signals.platform_connected.connect(self._on_platform_signal_received)
# 🔥 会话超时信号(用于拼多多会话过期重试)
self.session_timeout_signal = None
self.callbacks = { self.callbacks = {
'log': None, 'log': None,
'success': None, 'success': None,
'error': None, 'error': None,
'platform_connected': None, 'platform_connected': None,
'token_error': None, 'token_error': None,
'disconnect': None, # 新增:被踢下线回调
} }
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None, def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
platform_connected: Callable = None, platform_disconnected: Callable = None, platform_connected: Callable = None, token_error: Callable = None):
token_error: Callable = None, disconnect: Callable = None,
balance_insufficient: Callable = None):
"""设置回调函数""" """设置回调函数"""
if log: if log:
self.callbacks['log'] = log self.callbacks['log'] = log
@@ -74,14 +55,8 @@ class WebSocketManager:
self.callbacks['error'] = error self.callbacks['error'] = error
if platform_connected: # ← 新增 if platform_connected: # ← 新增
self.callbacks['platform_connected'] = platform_connected self.callbacks['platform_connected'] = platform_connected
if platform_disconnected:
self.callbacks['platform_disconnected'] = platform_disconnected
if token_error: if token_error:
self.callbacks['token_error'] = token_error self.callbacks['token_error'] = token_error
if disconnect:
self.callbacks['disconnect'] = disconnect
if balance_insufficient:
self.callbacks['balance_insufficient'] = balance_insufficient
def _log(self, message: str, level: str = "INFO"): def _log(self, message: str, level: str = "INFO"):
"""内部日志方法""" """内部日志方法"""
@@ -90,16 +65,8 @@ class WebSocketManager:
else: else:
print(f"[{level}] {message}") print(f"[{level}] {message}")
def _on_platform_signal_received(self, platform_name: str, store_id: str):
"""接收平台连接信号(在主线程中执行)"""
try:
self._log(f"📡 收到平台连接信号: {platform_name} (店铺:{store_id})", "INFO")
self._notify_platform_connected(platform_name)
except Exception as e:
self._log(f"处理平台连接信号失败: {e}", "ERROR")
def _notify_platform_connected(self, platform_name: str): def _notify_platform_connected(self, platform_name: str):
"""通知GUI平台连接成功(仅在主线程中调用)""" """通知GUI平台连接成功"""
try: try:
if platform_name not in self.connected_platforms: if platform_name not in self.connected_platforms:
self.connected_platforms.append(platform_name) self.connected_platforms.append(platform_name)
@@ -110,103 +77,16 @@ class WebSocketManager:
except Exception as e: except Exception as e:
self._log(f"通知平台连接失败: {e}", "ERROR") self._log(f"通知平台连接失败: {e}", "ERROR")
def notify_platform_kicked(self, platform_name: str, store_name: str, reason: str = "账号在其他设备登录",
store_id: str = None):
"""通知GUI平台被踢下线供平台监听器调用"""
try:
self._log(f"⚠️ 平台被踢下线: {platform_name} - {store_name}, 原因: {reason}", "WARNING")
# 从连接列表中移除
if platform_name in self.connected_platforms:
self.connected_platforms.remove(platform_name)
# 🔥 发送平台断开消息给后端status=false
if store_id and self.backend_client:
try:
disconnect_message = {
"type": "connect_message",
"store_id": store_id,
"status": False,
"cookies": ""
}
self.backend_client.send_message(disconnect_message)
self._log(f"✅ 已通知后端 {platform_name} 平台断开: {store_id}", "INFO")
except Exception as send_error:
self._log(f"❌ 发送平台断开消息失败: {send_error}", "ERROR")
# 通知GUI显示弹窗
if self.callbacks['platform_disconnected']:
self.callbacks['platform_disconnected'](platform_name, store_name, reason)
except Exception as e:
self._log(f"通知平台断开失败: {e}", "ERROR")
def disconnect_all_async(self):
"""异步断开所有连接(余额不足时调用)- 不阻塞当前线程"""
try:
self._log("🔴 收到余额不足通知,开始断开所有连接...", "ERROR")
# 1. 断开所有平台连接
if self.platform_listeners:
platform_count = len(self.platform_listeners)
self._log(f"正在断开 {platform_count} 个平台连接...", "INFO")
# 复制键列表,避免遍历时修改字典
platform_keys = list(self.platform_listeners.keys())
for key in platform_keys:
try:
listener_info = self.platform_listeners.get(key)
if listener_info:
platform_type = listener_info.get('platform', '')
self._log(f"断开 {platform_type} 平台连接: {key}", "INFO")
# 移除连接记录
self.platform_listeners.pop(key, None)
except Exception as e:
self._log(f"断开平台连接失败: {e}", "ERROR")
self._log(f"✅ 已断开所有 {platform_count} 个平台连接", "INFO")
# 清空连接列表
self.connected_platforms.clear()
# 2. 延迟断开后端连接(在新线程中执行,避免阻塞)
import threading
def _delayed_backend_disconnect():
try:
import time
time.sleep(0.5) # 延迟0.5秒,确保上面的操作完成
if self.backend_client:
self.backend_client.should_stop = True
self.backend_client.is_connected = False
self._log("✅ 已标记后端连接为断开状态", "INFO")
except Exception as e:
self._log(f"断开后端连接失败: {e}", "ERROR")
disconnect_thread = threading.Thread(target=_delayed_backend_disconnect, daemon=True)
disconnect_thread.start()
except Exception as e:
self._log(f"断开所有连接失败: {e}", "ERROR")
def connect_backend(self, token: str) -> bool: def connect_backend(self, token: str) -> bool:
"""连接后端WebSocket""" """连接后端WebSocket"""
try: try:
# 🔥 根据配置决定是否保存token # 1 保存token到配置
# 生产模式保存token方便用户下次自动加载
# 测试模式:不保存,避免多实例冲突
import config as cfg
if not cfg.is_multi_instance_mode():
try: try:
from config import set_saved_token from config import set_saved_token
set_saved_token(token) set_saved_token(token)
self._log("生产模式已保存token到配置文件", "INFO") except Exception:
except Exception as e: pass
self._log(f"保存token失败: {e}", "WARNING")
else:
self._log("测试模式不保存token支持多实例运行", "INFO")
# 2 获取或创建后端客户端 # 2 获取或创建后端客户端
backend = get_backend_client() backend = get_backend_client()
@@ -235,36 +115,19 @@ class WebSocketManager:
except Exception as e: except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR") self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_backend_login(platform_name: str, store_id: str, cookies: str, store_name: str = ""): def _on_backend_login(platform_name: str, store_id: str, cookies: str):
store_display = store_name or store_id
self._log( self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_display}, cookies_len={len(cookies) if cookies else 0}", f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO") "INFO")
self._handle_platform_login(platform_name, store_id, cookies, store_name) self._handle_platform_login(platform_name, store_id, cookies)
def _on_token_error(error_content: str): def _on_token_error(error_content: str):
self._log(f"Token验证失败: {error_content}", "ERROR") self._log(f"Token验证失败: {error_content}", "ERROR")
if self.callbacks['token_error']: if self.callbacks['token_error']:
self.callbacks['token_error'](error_content) self.callbacks['token_error'](error_content)
def _on_disconnect(disconnect_msg: str):
self._log(f"被踢下线: {disconnect_msg}", "WARNING")
if self.callbacks['disconnect']:
self.callbacks['disconnect'](disconnect_msg)
def _on_balance_insufficient(balance_msg: str):
"""余额不足回调"""
if self.callbacks['balance_insufficient']:
self.callbacks['balance_insufficient'](balance_msg)
def _on_log(message: str, level: str = "INFO"):
"""Backend client log callback"""
self._log(message, level)
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login, backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
token_error=_on_token_error, disconnect=_on_disconnect, token_error=_on_token_error)
balance_insufficient=_on_balance_insufficient,
log=_on_log)
if not backend.is_connected: if not backend.is_connected:
backend.connect() backend.connect()
@@ -278,12 +141,11 @@ class WebSocketManager:
backend = BackendClient.from_exe_token(token) backend = BackendClient.from_exe_token(token)
def _on_backend_login(platform_name: str, store_id: str, cookies: str, store_name: str = ""): def _on_backend_login(platform_name: str, store_id: str, cookies: str):
store_display = store_name or store_id
self._log( self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_display}, cookies_len={len(cookies) if cookies else 0}", f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO") "INFO")
self._handle_platform_login(platform_name, store_id, cookies, store_name) self._handle_platform_login(platform_name, store_id, cookies)
def _on_backend_success(): def _on_backend_success():
try: try:
@@ -300,24 +162,7 @@ class WebSocketManager:
if self.callbacks['token_error']: if self.callbacks['token_error']:
self.callbacks['token_error'](error_content) self.callbacks['token_error'](error_content)
def _on_disconnect(disconnect_msg: str): backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error)
self._log(f"被踢下线: {disconnect_msg}", "ERROR")
if self.callbacks['disconnect']:
self.callbacks['disconnect'](disconnect_msg)
def _on_balance_insufficient(balance_msg: str):
"""余额不足回调"""
if self.callbacks['balance_insufficient']:
self.callbacks['balance_insufficient'](balance_msg)
def _on_log(message: str, level: str = "INFO"):
"""Backend client log callback"""
self._log(message, level)
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success,
token_error=_on_token_error, disconnect=_on_disconnect,
balance_insufficient=_on_balance_insufficient,
log=_on_log)
backend.connect() backend.connect()
set_backend_client(backend) set_backend_client(backend)
@@ -331,106 +176,17 @@ class WebSocketManager:
self.callbacks['error'](str(e)) self.callbacks['error'](str(e))
return False return False
def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str, store_name: str = ""): def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str):
"""处理平台登录请求""" """处理平台登录请求"""
try: try:
# 🔥 检查并断开当前店铺的旧连接策略B先断开旧连接再建立新连接 # 🔥 检查并清理当前店铺的旧连接
store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式 store_key_pattern = f":{store_id}" # 匹配 "平台名:store_id" 格式
keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)] keys_to_remove = [key for key in self.platform_listeners.keys() if key.endswith(store_key_pattern)]
if keys_to_remove: if keys_to_remove:
self._log(f"🔄 检测到店铺 {store_id}复登录,断开 {len(keys_to_remove)} 个旧连接", "INFO") self._log(f"🔄 检测到店铺 {store_id}连,清理 {len(keys_to_remove)} 个旧连接", "INFO")
for key in keys_to_remove: for key in keys_to_remove:
listener_info = self.platform_listeners.get(key)
if listener_info:
platform_type = listener_info.get('platform', '')
# 从各平台的 WebsocketManager 中获取连接并关闭WebSocket
try:
if platform_type == "京东":
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
jd_mgr = JDWSManager()
conn_info = jd_mgr.get_connection(key)
if conn_info and conn_info.get('platform'):
ws = conn_info['platform'].get('ws')
if ws and hasattr(ws, 'close'):
try:
import asyncio
loop = conn_info['platform'].get('loop')
if loop and not loop.is_closed():
asyncio.run_coroutine_threadsafe(ws.close(), loop)
self._log(f"✅ 已关闭京东WebSocket连接: {key}", "DEBUG")
except Exception:
pass
jd_mgr.remove_connection(key)
self._log(f"✅ 已从京东管理器移除连接: {key}", "DEBUG")
elif platform_type == "抖音":
from Utils.Dy.DyUtils import DouYinWebsocketManager as DYWSManager
dy_mgr = DYWSManager()
conn_info = dy_mgr.get_connection(key)
if conn_info and conn_info.get('platform'):
ws = conn_info['platform'].get('ws')
if ws and hasattr(ws, 'close'):
try:
import asyncio
loop = conn_info['platform'].get('loop')
if loop and not loop.is_closed():
asyncio.run_coroutine_threadsafe(ws.close(), loop)
self._log(f"✅ 已关闭抖音WebSocket连接: {key}", "DEBUG")
except Exception:
pass
dy_mgr.remove_connection(key)
self._log(f"✅ 已从抖音管理器移除连接: {key}", "DEBUG")
elif platform_type == "千牛":
from Utils.QianNiu.QianNiuUtils import QianNiuWebsocketManager as QNWSManager
qn_mgr = QNWSManager()
qn_mgr.remove_connection(key)
self._log(f"✅ 已从千牛管理器移除连接: {key}", "DEBUG")
elif platform_type == "拼多多":
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager()
conn_info = pdd_mgr.get_connection(key)
# 清理旧listener的定时器避免遗留定时器触发
old_listener = listener_info.get('listener')
if old_listener and hasattr(old_listener, 'pdd_bot') and old_listener.pdd_bot:
try:
old_listener.pdd_bot._cancel_session_timeout_check()
self._log(f"✅ [PDD] 旧监听器的定时器已清理", "DEBUG")
except Exception:
pass
if conn_info and conn_info.get('platform'):
# 关闭WebSocket连接
ws = conn_info['platform'].get('ws')
if ws and hasattr(ws, 'close'):
try:
import asyncio
loop = conn_info['platform'].get('loop')
if loop and not loop.is_closed():
asyncio.run_coroutine_threadsafe(ws.close(), loop)
self._log(f"✅ 已关闭拼多多WebSocket连接: {key}", "DEBUG")
except Exception as ws_e:
self._log(f"⚠️ 关闭WebSocket时出错: {ws_e}", "DEBUG")
pdd_mgr.remove_connection(key)
self._log(f"✅ 已从拼多多管理器移除连接: {key}", "DEBUG")
except Exception as e:
self._log(f"⚠️ 移除{platform_type}连接时出错: {e}", "WARNING")
# 从监听器字典中移除
self.platform_listeners.pop(key, None) self.platform_listeners.pop(key, None)
# 给WebSocket一点时间完全关闭
import time
time.sleep(0.5)
self._log(f"✅ 旧连接已全部断开,准备建立新连接", "INFO")
# 平台名称映射 # 平台名称映射
platform_map = { platform_map = {
"淘宝": "千牛", "淘宝": "千牛",
@@ -455,16 +211,16 @@ class WebSocketManager:
if cookies == "login_success": if cookies == "login_success":
self._log("⚠️ 千牛平台收到空cookies但允许启动监听器", "WARNING") self._log("⚠️ 千牛平台收到空cookies但允许启动监听器", "WARNING")
cookies = "" # 清空cookies千牛不需要真实cookies cookies = "" # 清空cookies千牛不需要真实cookies
self._start_qianniu_listener(store_id, cookies, store_name) self._start_qianniu_listener(store_id, cookies)
elif normalized_platform == "京东": elif normalized_platform == "京东":
self._start_jd_listener(store_id, cookies, store_name) self._start_jd_listener(store_id, cookies)
elif normalized_platform == "抖音": elif normalized_platform == "抖音":
self._start_douyin_listener(store_id, cookies, store_name) self._start_douyin_listener(store_id, cookies)
elif normalized_platform == "拼多多": elif normalized_platform == "拼多多":
self._start_pdd_listener(store_id, cookies, store_name) self._start_pdd_listener(store_id, cookies)
else: else:
self._log(f"❌ 不支持的平台: {platform_name}", "ERROR") self._log(f"❌ 不支持的平台: {platform_name}", "ERROR")
@@ -489,20 +245,13 @@ class WebSocketManager:
self._log(f"❌ 启动版本检查器失败: {e}", "ERROR") self._log(f"❌ 启动版本检查器失败: {e}", "ERROR")
def _on_update_available(self, latest_version, download_url): def _on_update_available(self, latest_version, download_url):
"""发现新版本时的处理(在子线程中调用)""" """发现新版本时的处理"""
self._log(f"🔔 发现新版本 {latest_version}", "INFO") self._log(f"🔔 发现新版本 {latest_version}", "INFO")
# 通知主GUI显示更新提醒(通过 Qt 信号机制,线程安全) # 通知主GUI显示更新提醒
if hasattr(self, 'gui_update_callback') and self.gui_update_callback: if hasattr(self, 'gui_update_callback') and self.gui_update_callback:
try:
# 直接调用回调(回调内部使用信号机制调度到主线程)
self.gui_update_callback(latest_version, download_url) self.gui_update_callback(latest_version, download_url)
self._log(f"✅ 已调用更新回调", "DEBUG")
except Exception as e:
self._log(f"❌ 调用更新回调失败: {e}", "ERROR")
import traceback
self._log(f"详细错误: {traceback.format_exc()}", "ERROR")
def _start_jd_listener(self, store_id: str, cookies: str, store_name: str = ""): def _start_jd_listener(self, store_id: str, cookies: str):
"""启动京东平台监听""" """启动京东平台监听"""
try: try:
def _runner(): def _runner():
@@ -520,8 +269,7 @@ class WebSocketManager:
self.platform_listeners[f"京东:{store_id}"] = { self.platform_listeners[f"京东:{store_id}"] = {
'thread': thread, 'thread': thread,
'platform': '京东', 'platform': '京东',
'store_id': store_id, 'store_id': store_id
'store_name': store_name # 保存店铺名称
} }
# 上报连接状态给后端 # 上报连接状态给后端
@@ -552,7 +300,7 @@ class WebSocketManager:
except Exception as send_e: except Exception as send_e:
self._log(f"失败状态下报连接状态也失败: {send_e}", "ERROR") self._log(f"失败状态下报连接状态也失败: {send_e}", "ERROR")
def _start_douyin_listener(self, store_id: str, cookies: str, store_name: str = ""): def _start_douyin_listener(self, store_id: str, cookies: str):
"""启动抖音平台监听""" """启动抖音平台监听"""
try: try:
def _runner(): def _runner():
@@ -570,13 +318,14 @@ class WebSocketManager:
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies)) result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies))
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG") self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
# 详细的结果分析(仅日志记录GUI 已在主线程中通知 # 🔥 详细的结果分析(与拼多多完全一致
if result == "need_verification_code": if result == "need_verification_code":
self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS") self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
elif result == "verification_code_error": elif result == "verification_code_error":
self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING") self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING")
elif result: elif result:
self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS") self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
else: else:
self._log("❌ [DY] 登录失败", "ERROR") self._log("❌ [DY] 登录失败", "ERROR")
else: else:
@@ -601,9 +350,10 @@ class WebSocketManager:
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict)) result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG") self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
# Cookie启动成功时记录日志GUI 已在主线程中通知) # Cookie启动成功时也要通知GUI
if result: if result:
self._log("✅ [DY] Cookie启动成功平台连接已建立", "SUCCESS") self._log("✅ [DY] Cookie启动成功平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
# 🔥 移除不再在backend_singleton中发送connect_message # 🔥 移除不再在backend_singleton中发送connect_message
# 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送与拼多多保持一致 # 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送与拼多多保持一致
@@ -623,17 +373,13 @@ class WebSocketManager:
'thread': thread, 'thread': thread,
'platform': '抖音', 'platform': '抖音',
'store_id': store_id, 'store_id': store_id,
'store_name': store_name # 保存店铺名称
} }
# 更新监听器状态 # 更新监听器状态
if f"抖音:{store_id}" in self.platform_listeners: if f"抖音:{store_id}" in self.platform_listeners:
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success' self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
# ✅ 临时方案:启动后立即通知(因为 DY 监听器也会阻塞)
# DY 内部会处理验证码流程,失败时会向后端上报相应状态
self._log("已启动抖音平台监听", "SUCCESS") self._log("已启动抖音平台监听", "SUCCESS")
self._notify_platform_connected("抖音") # ← 立即通知
except Exception as e: except Exception as e:
self._log(f"启动抖音平台监听失败: {e}", "ERROR") self._log(f"启动抖音平台监听失败: {e}", "ERROR")
@@ -641,7 +387,7 @@ class WebSocketManager:
# 🔥 移除:确保失败时也不在这里上报状态 # 🔥 移除:确保失败时也不在这里上报状态
# 失败状态应该在DyLogin中处理与拼多多保持一致 # 失败状态应该在DyLogin中处理与拼多多保持一致
def _start_qianniu_listener(self, store_id: str, cookies: str, store_name: str = ""): def _start_qianniu_listener(self, store_id: str, cookies: str):
"""启动千牛平台监听(单连接多店铺架构)""" """启动千牛平台监听(单连接多店铺架构)"""
try: try:
# 获取用户token从后端客户端获取 # 获取用户token从后端客户端获取
@@ -670,7 +416,6 @@ class WebSocketManager:
'thread': thread, 'thread': thread,
'platform': '千牛', 'platform': '千牛',
'store_id': store_id, 'store_id': store_id,
'store_name': store_name, # 保存店铺名称
'exe_token': exe_token 'exe_token': exe_token
} }
@@ -702,46 +447,49 @@ class WebSocketManager:
except Exception as send_e: except Exception as send_e:
self._log(f"失败状态下报千牛平台连接状态也失败: {send_e}", "ERROR") self._log(f"失败状态下报千牛平台连接状态也失败: {send_e}", "ERROR")
def _start_pdd_listener(self, store_id: str, data: str, store_name: str = ""): def _start_pdd_listener(self, store_id: str, data: str):
"""启动拼多多平台监听""" """启动拼多多平台监听"""
try: try:
# 在创建新实例前,清理旧实例的定时器(避免遗留定时器触发)
shop_key = f"拼多多:{store_id}"
old_listener_info = self.platform_listeners.get(shop_key)
if old_listener_info:
try:
old_listener = old_listener_info.get('listener')
if old_listener and hasattr(old_listener, 'pdd_bot') and old_listener.pdd_bot:
old_listener.pdd_bot._cancel_session_timeout_check()
self._log(f"✅ 已清理旧监听器定时器", "DEBUG")
except Exception:
pass
def _runner(): def _runner():
try: try:
# 传递session_timeout_signal用于会话过期重试 self._log("🚀 开始创建拼多多监听器实例", "DEBUG")
listener = PDDListenerForGUI_WS( listener = PDDListenerForGUI_WS(log_callback=self._log)
log_callback=self._log, self._log("✅ 拼多多监听器实例创建成功", "DEBUG")
session_timeout_signal=self.session_timeout_signal
)
# 立即保存listener引用到platform_listeners用于后续清理定时器
shop_key = f"拼多多:{store_id}"
if shop_key in self.platform_listeners:
self.platform_listeners[shop_key]['listener'] = listener
# 判断是登录参数还是Cookie # 判断是登录参数还是Cookie
if self._is_pdd_login_params(data): if self._is_pdd_login_params(data):
# 使用登录参数启动 # 使用登录参数启动
self._log("📋 使用登录参数启动拼多多监听器", "INFO")
self._log("🔄 开始执行 start_with_login_params", "DEBUG")
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=data)) result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=data))
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
# 详细的结果分析
if result == "need_verification_code":
self._log("✅ [PDD] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
elif result == "verification_code_error":
self._log("⚠️ [PDD] 验证码错误,已发送错误通知给后端", "WARNING")
elif result:
self._log("✅ [PDD] 登录成功,平台连接已建立", "SUCCESS")
self._notify_platform_connected("拼多多")
else:
self._log("❌ [PDD] 登录失败", "ERROR")
else: else:
# 使用Cookie启动兼容旧方式 # 使用Cookie启动兼容旧方式
self._log("🍪 使用Cookie启动拼多多监听器", "INFO")
self._log("🔄 开始执行 start_with_cookies", "DEBUG")
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data)) result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
# Cookie启动成功时也要通知GUI
if result:
self._log("✅ [PDD] Cookie启动成功平台连接已建立", "SUCCESS")
self._notify_platform_connected("拼多多")
# 根据实际登录结果上报状态给后端 # 根据实际登录结果上报状态给后端
if self.backend_client and result not in ["need_verification_code", "verification_code_error", if self.backend_client and result not in ["need_verification_code", "verification_code_error",
"login_failure", "cookie_expired"]: "login_failure"]:
# 🔥 如果是特殊状态包括cookie_expired,说明通知已经发送,不需要重复发送 # 如果是特殊状态,说明通知已经在PddLogin中发送,不需要重复发送
try: try:
message = { message = {
"type": "connect_message", "type": "connect_message",
@@ -760,8 +508,6 @@ class WebSocketManager:
self._log("验证码错误错误通知已由PddLogin发送等待后端处理", "INFO") self._log("验证码错误错误通知已由PddLogin发送等待后端处理", "INFO")
elif result == "login_failure": elif result == "login_failure":
self._log("登录失败失败通知已由PddLogin发送等待后端处理", "INFO") self._log("登录失败失败通知已由PddLogin发送等待后端处理", "INFO")
elif result == "cookie_expired":
self._log("🔄 Cookie已过期过期通知已发送等待后端重新下发Cookie", "INFO")
return result return result
@@ -795,13 +541,9 @@ class WebSocketManager:
'thread': thread, 'thread': thread,
'platform': '拼多多', 'platform': '拼多多',
'store_id': store_id, 'store_id': store_id,
'store_name': store_name # 保存店铺名称
} }
# ✅ 临时方案:启动后立即通知(因为 PDD 监听器会阻塞,无法通过返回值判断) self._log("拼多多平台监听线程已启动,等待登录结果...", "INFO")
# PDD 内部会处理验证码流程,失败时会向后端上报 status=False
self._log("已启动拼多多平台监听", "SUCCESS")
self._notify_platform_connected("拼多多") # ← 立即通知
except Exception as e: except Exception as e:
self._log(f"启动拼多多平台监听失败: {e}", "ERROR") self._log(f"启动拼多多平台监听失败: {e}", "ERROR")
@@ -821,19 +563,29 @@ class WebSocketManager:
def _is_pdd_login_params(self, data: str) -> bool: def _is_pdd_login_params(self, data: str) -> bool:
"""判断是否为拼多多登录参数""" """判断是否为拼多多登录参数"""
try: try:
self._log(f"🔍 [DEBUG] 检查是否为登录参数,数据长度: {len(data)}", "DEBUG")
self._log(f"🔍 [DEBUG] 数据前100字符: {data[:100]}", "DEBUG")
import json import json
parsed_data = json.loads(data) parsed_data = json.loads(data)
self._log(f"🔍 [DEBUG] JSON解析成功键: {list(parsed_data.keys())}", "DEBUG")
login_params = parsed_data.get("data", {}).get("login_params", {}) login_params = parsed_data.get("data", {}).get("login_params", {})
self._log(f"🔍 [DEBUG] login_params存在: {bool(login_params)}", "DEBUG")
if not login_params: if not login_params:
self._log("🔍 [DEBUG] login_params为空返回False", "DEBUG")
return False return False
# 检查必需的登录参数字段 # 检查必需的登录参数字段
required_fields = ["username", "password", "anti_content", "risk_sign", "timestamp"] required_fields = ["username", "password", "anti_content", "risk_sign", "timestamp"]
has_all_fields = all(field in login_params for field in required_fields) has_all_fields = all(field in login_params for field in required_fields)
self._log(f"🔍 [DEBUG] 包含所有必需字段: {has_all_fields}", "DEBUG")
self._log(f"🔍 [DEBUG] 现有字段: {list(login_params.keys())}", "DEBUG")
return has_all_fields return has_all_fields
except Exception: except Exception as e:
self._log(f"🔍 [DEBUG] 解析失败: {e}", "DEBUG")
return False return False
def send_message(self, message: dict): def send_message(self, message: dict):
@@ -869,4 +621,3 @@ def get_websocket_manager() -> WebSocketManager:
if _websocket_manager is None: if _websocket_manager is None:
_websocket_manager = WebSocketManager() _websocket_manager = WebSocketManager()
return _websocket_manager return _websocket_manager

View File

@@ -1,489 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自动更新模块 - 支持后台下载、静默安装和自动重启
优化版:带重试、超时、自动重启功能
"""
import os
import sys
import subprocess
import urllib.request
import tempfile
import time
import socket
from pathlib import Path
from PyQt5.QtCore import QThread, pyqtSignal
import ctypes
class UpdateDownloader(QThread):
"""
优化版更新下载器线程
特性:
- 自动重试3次
- 30秒网络超时
- 文件大小验证
- 进度实时更新
"""
# 信号定义
progress = pyqtSignal(int, int) # (已下载字节, 总字节)
finished = pyqtSignal(str) # 下载完成,返回文件路径
error = pyqtSignal(str) # 下载失败,返回错误信息
retry_info = pyqtSignal(int, int) # 重试信息 (当前重试次数, 总重试次数)
def __init__(self, download_url, version, max_retries=3):
super().__init__()
self.download_url = download_url
self.version = version
self.max_retries = max_retries # 最大重试次数
self.is_cancelled = False
def run(self):
"""执行下载(带重试机制)"""
for attempt in range(self.max_retries):
try:
if attempt > 0:
# 显示重试信息
self.retry_info.emit(attempt + 1, self.max_retries)
time.sleep(2) # 等待2秒后重试
# 执行实际下载
self._download_file()
return # 下载成功,退出
except Exception as e:
error_msg = str(e)
print(f"[UpdateDownloader] 下载失败 (尝试 {attempt + 1}/{self.max_retries}): {error_msg}")
if self.is_cancelled:
# 用户取消,立即退出
self.error.emit("下载已取消")
return
if attempt < self.max_retries - 1:
# 还有重试机会,继续
continue
else:
# 所有重试都失败了
self.error.emit(f"下载失败(已重试{self.max_retries}次)\n错误: {error_msg}")
def _download_file(self):
"""实际下载逻辑(带超时和验证)"""
# 设置网络超时
socket.setdefaulttimeout(30) # 30秒超时
# 准备临时目录和文件名
temp_dir = tempfile.gettempdir()
filename = f"ShuiDi_AI_Assistant_Setup_v{self.version}.exe"
file_path = os.path.join(temp_dir, filename)
print(f"[UpdateDownloader] 开始下载到: {file_path}")
print(f"[UpdateDownloader] 下载地址: {self.download_url}")
# 删除旧文件(如果存在)
if os.path.exists(file_path):
try:
os.remove(file_path)
print(f"[UpdateDownloader] 已删除旧文件")
except Exception as e:
print(f"[UpdateDownloader] 删除旧文件失败: {e}")
# 下载文件(带进度回调)
def progress_callback(block_num, block_size, total_size):
if self.is_cancelled:
raise Exception("用户取消下载")
downloaded = block_num * block_size
# 避免超过总大小
if total_size > 0 and downloaded > total_size:
downloaded = total_size
self.progress.emit(downloaded, total_size)
# 执行下载
urllib.request.urlretrieve(
self.download_url,
file_path,
reporthook=progress_callback
)
# 验证下载的文件
if not os.path.exists(file_path):
raise Exception("下载的文件不存在")
file_size = os.path.getsize(file_path)
print(f"[UpdateDownloader] 下载完成,文件大小: {file_size / (1024*1024):.2f} MB")
# 验证文件大小至少1MB避免下载了错误页面
min_file_size = 1024 * 1024 # 1MB正式环境
# 🧪 测试小文件时可以临时改为min_file_size = 10 * 1024 # 10KB
if file_size < min_file_size:
raise Exception(f"下载的文件大小异常: {file_size} bytes可能不是有效的安装包")
# 发送完成信号
self.finished.emit(file_path)
def cancel(self):
"""取消下载"""
print("[UpdateDownloader] 用户取消下载")
self.is_cancelled = True
def run_as_admin(script_path):
"""
以管理员身份运行脚本Windows UAC提升
特点:
- 在GUI退出前弹出UAC提示
- 用户点击"允许"后,脚本以管理员身份运行
- 用户点击"取消"返回False
Args:
script_path: 批处理脚本路径
Returns:
bool: True表示成功启动用户点击允许False表示失败或用户取消
"""
try:
print(f"[RunAsAdmin] 请求管理员权限执行: {script_path}")
# 使用 ShellExecute 以管理员身份运行
# "runas" 参数会触发UAC提示
ret = ctypes.windll.shell32.ShellExecuteW(
None, # hwnd: 父窗口句柄None表示无父窗口
"runas", # lpOperation: 以管理员身份运行
"cmd.exe", # lpFile: 要执行的程序
f'/c "{script_path}"', # lpParameters: 命令行参数
None, # lpDirectory: 工作目录None表示当前目录
0 # nShowCmd: 0=隐藏窗口
)
# 返回值说明:
# >32: 成功
# <=32: 失败(具体错误码)
# - 5: 用户拒绝UAC点击"否"
# - 2: 文件未找到
# - 其他: 其他错误
if ret > 32:
print(f"[RunAsAdmin] ✅ 成功:用户已授权,脚本正在以管理员身份运行")
return True
elif ret == 5:
print(f"[RunAsAdmin] ⚠️ 用户取消了UAC授权返回码: {ret}")
return False
else:
print(f"[RunAsAdmin] ❌ 启动失败(返回码: {ret}")
return False
except Exception as e:
print(f"[RunAsAdmin] ❌ 异常: {e}")
import traceback
traceback.print_exc()
return False
def install_update_and_restart(installer_path):
"""
简化版更新安装文件替换方式不使用NSIS安装
工作流程:
1. 将安装包移动到程序目录
2. 创建替换脚本
3. 以管理员身份启动脚本提前弹出UAC
4. 脚本等待程序退出
5. 脚本执行静默安装
6. 重启程序
Args:
installer_path: 安装包路径
Returns:
bool: 是否成功启动安装
"""
try:
# 获取当前程序信息
if getattr(sys, 'frozen', False):
# 打包后的exe
current_exe = sys.executable
current_dir = os.path.dirname(current_exe)
exe_name = os.path.basename(current_exe)
else:
# 开发环境
current_exe = os.path.abspath("main.py")
current_dir = os.path.dirname(os.path.abspath(__file__))
exe_name = "main.exe"
print(f"[Updater] 当前程序: {current_exe}")
print(f"[Updater] 程序目录: {current_dir}")
print(f"[Updater] 主程序名: {exe_name}")
print(f"[Updater] 安装包路径: {installer_path}")
# 🔥 关键改进:将安装包移动到程序目录(避免跨盘符问题)
installer_name = os.path.basename(installer_path)
target_installer_path = os.path.join(current_dir, installer_name)
# 如果安装包不在程序目录,移动过去
if os.path.abspath(installer_path) != os.path.abspath(target_installer_path):
try:
import shutil
shutil.move(installer_path, target_installer_path)
print(f"[Updater] 安装包已移动到程序目录: {target_installer_path}")
installer_path = target_installer_path
except Exception as e:
print(f"[Updater] 移动安装包失败,使用原路径: {e}")
# 创建更新脚本
update_script_path = create_update_installer_script(
installer_path,
current_dir,
exe_name
)
# 🔥 关键修复以管理员身份启动批处理脚本提前触发UAC
print(f"[Updater] 以管理员身份启动更新脚本: {update_script_path}")
success = run_as_admin(update_script_path)
if success:
print(f"[Updater] ✅ 更新脚本已以管理员身份启动")
return True
else:
print(f"[Updater] ❌ 用户取消了UAC授权更新已取消")
return False
except Exception as e:
print(f"[Updater] ❌ 启动更新失败: {e}")
import traceback
traceback.print_exc()
return False
def create_update_installer_script(installer_path, install_dir, exe_name):
"""
创建更新安装脚本(在程序目录下执行)
工作流程:
1. 等待主程序退出
2. 在程序目录下执行NSIS安装
3. 清理安装包
4. 重启程序
Args:
installer_path: 安装包路径(应该已经在程序目录下)
install_dir: 程序安装目录
exe_name: 主程序文件名
Returns:
str: 脚本路径
"""
# 脚本放在程序目录下,避免权限问题
script_path = os.path.join(install_dir, "update_installer.bat")
# 转换为绝对路径(处理引号问题)
installer_path = os.path.abspath(installer_path)
install_dir = os.path.abspath(install_dir)
script_content = f"""@echo off
REM ShuiDi AI Assistant Auto Update Script
REM Encoding: UTF-8 BOM
title ShuiDi AI Assistant - Auto Update
echo ============================================
echo ShuiDi AI Assistant Auto Update
echo ============================================
echo.
REM Change to program directory
cd /d "{install_dir}"
echo INFO: Program directory: {install_dir}
echo.
REM Wait for main program to exit (max 60 seconds)
echo INFO: Waiting for main program to exit...
set count=0
:wait_main_exit
tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL
if NOT ERRORLEVEL 1 (
if %count% LSS 60 (
timeout /t 1 /nobreak > nul
set /a count+=1
goto wait_main_exit
) else (
echo WARN: Main program did not exit normally, forcing termination
taskkill /F /IM {exe_name} 2>nul
)
)
echo OK: Main program exited
echo.
REM Critical fix: Wait 2 seconds to ensure all file handles are released
echo INFO: Waiting for file handles to release...
timeout /t 2 /nobreak > nul
echo OK: File handles released
echo.
REM Critical fix: Delete old _internal directory to force clean install
echo INFO: Removing old _internal directory...
if exist "{install_dir}\\_internal" (
rd /s /q "{install_dir}\\_internal" 2>nul
if exist "{install_dir}\\_internal" (
echo WARN: Failed to delete _internal directory
) else (
echo OK: Old _internal directory removed successfully
)
) else (
echo INFO: No old _internal directory found
)
echo.
REM Execute NSIS silent installation
echo INFO: Starting installation...
echo INFO: Installer: "{installer_path}"
echo INFO: Target directory: "{install_dir}"
echo.
REM Execute installation (with admin rights from NSIS)
"{installer_path}" /S /D={install_dir}
REM Wait for installation to complete (extended to 35 seconds for full copy)
echo INFO: Waiting for installation to complete...
timeout /t 35 /nobreak > nul
REM Cleanup installer
echo INFO: Cleaning up installer...
del /f /q "{installer_path}" 2>nul
REM Start new version (with --after-update flag to show window on top)
echo INFO: Starting new version...
if exist "{install_dir}\\{exe_name}" (
start "" "{install_dir}\\{exe_name}" --after-update
echo OK: Program started successfully
) else (
echo ERROR: Program file not found: {exe_name}
echo INFO: Please start program manually
pause
)
REM Delete this script after delay
timeout /t 2 /nobreak > nul
del /f /q "%~f0" 2>nul
exit
"""
try:
with open(script_path, 'w', encoding='utf-8-sig') as f:
f.write(script_content)
print(f"[Updater] 更新脚本已创建: {script_path}")
except Exception as e:
print(f"[Updater] 创建更新脚本失败: {e}")
return script_path
def create_restart_launcher(install_dir, exe_name):
"""
创建重启启动器脚本
该脚本会:
1. 等待当前GUI进程退出
2. 等待NSIS安装完成15秒
3. 启动新版本GUI
4. 自我删除
Args:
install_dir: 安装目录
exe_name: 主程序文件名
Returns:
str: 脚本路径
"""
script_path = os.path.join(tempfile.gettempdir(), "shuidrop_restart_launcher.bat")
# 构建批处理脚本
script_content = f"""@echo off
chcp 65001 > nul
title ShuiDi AI Assistant - Update Helper
echo [%time%] Updating ShuiDi AI Assistant...
echo.
REM Wait for main program to exit (max 10 seconds)
echo [%time%] Waiting for main program to exit...
set count=0
:wait_main_exit
tasklist /FI "IMAGENAME eq {exe_name}" 2>NUL | find /I "{exe_name}" >NUL
if NOT ERRORLEVEL 1 (
if %count% LSS 10 (
timeout /t 1 /nobreak > nul
set /a count+=1
goto wait_main_exit
)
)
echo [%time%] Main program exited
REM Wait for installation to complete (15 seconds)
echo [%time%] Waiting for installation to complete...
timeout /t 15 /nobreak > nul
REM Clean up old installer files
echo [%time%] Cleaning up temporary files...
del /f /q "%TEMP%\\ShuiDi_AI_Assistant_Setup_*.exe" 2>nul
REM Start new version
echo [%time%] Starting new version...
cd /d "{install_dir}"
if exist "{exe_name}" (
start "" "{install_dir}\\{exe_name}"
echo [%time%] Program started successfully
) else (
echo [%time%] ERROR: Program file not found: {exe_name}
pause
)
REM Delete this script after delay
timeout /t 2 /nobreak > nul
del /f /q "%~f0" 2>nul
exit
"""
# Write script file with UTF-8 BOM encoding
try:
with open(script_path, 'w', encoding='utf-8-sig') as f:
f.write(script_content)
print(f"[Updater] Restart launcher created: {script_path}")
except Exception as e:
print(f"[Updater] Failed to create restart launcher: {e}")
return script_path
# 兼容性函数(保留旧接口)
def install_update_silently(installer_path):
"""
启动静默安装(不自动重启)
兼容旧接口,建议使用 install_update_and_restart
"""
return install_update_and_restart(installer_path)
if __name__ == "__main__":
# 测试代码
print("自动更新模块测试")
print("=" * 50)
# 测试重启启动器创建
script_path = create_restart_launcher("C:\\Program Files\\ShuiDi AI Assistant", "main.exe")
print(f"测试脚本路径: {script_path}")
if os.path.exists(script_path):
print("✅ 重启启动器创建成功")
with open(script_path, 'r', encoding='utf-8-sig') as f:
print("\n脚本内容预览:")
print("-" * 50)
print(f.read())
else:
print("❌ 重启启动器创建失败")

View File

@@ -1,151 +1,129 @@
#!/usr/bin/env python3 #!/usr/bin/env python3
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
Production build script - Automatically disable logging functionality 生产环境打包脚本 - 自动禁用日志功能
""" """
import os import os
import shutil import shutil
import subprocess import subprocess
import sys
# Ensure script runs in correct directory
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(SCRIPT_DIR)
print(f"Working directory: {SCRIPT_DIR}")
def disable_logging(): def disable_logging():
"""Disable all logging functionality""" """自动禁用所有日志功能"""
print("Disabling logging functionality...") print("🔧 正在禁用日志功能...")
# Backup original files # 备份原文件
files_to_backup = ['main.py', 'exe_file_logger.py'] files_to_backup = ['main.py', 'exe_file_logger.py']
for file_name in files_to_backup: for file_name in files_to_backup:
if os.path.exists(file_name): if os.path.exists(file_name):
shutil.copy(file_name, f'{file_name}.backup') shutil.copy(file_name, f'{file_name}.backup')
print(f"Backed up {file_name}") print(f"✅ 已备份 {file_name}")
# 1. Modify main.py - comment out logging initialization # 1. 修改 main.py - 注释掉日志初始化
if os.path.exists('main.py'): if os.path.exists('main.py'):
with open('main.py', 'r', encoding='utf-8') as f: with open('main.py', 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Comment out logging related imports and initialization # 注释掉日志相关的导入和初始化
content = content.replace( content = content.replace(
'from exe_file_logger import setup_file_logging, log_to_file', 'from exe_file_logger import setup_file_logging, log_to_file',
'# from exe_file_logger import setup_file_logging, log_to_file # Production disabled' '# from exe_file_logger import setup_file_logging, log_to_file # 生产环境禁用'
).replace( ).replace(
'setup_file_logging()', 'setup_file_logging()',
'# setup_file_logging() # Production disabled' '# setup_file_logging() # 生产环境禁用'
).replace( ).replace(
'print("文件日志系统已在main.py中初始化")', 'print("文件日志系统已在main.py中初始化")',
'# print("文件日志系统已在main.py中初始化") # Production disabled' '# print("文件日志系统已在main.py中初始化") # 生产环境禁用'
) )
with open('main.py', 'w', encoding='utf-8') as f: with open('main.py', 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
print("Disabled logging in main.py") print("✅ 已禁用 main.py 中的日志初始化")
# 2. Modify exe_file_logger.py - make all functions no-op # 2. 修改 exe_file_logger.py - 让所有函数变成空操作
if os.path.exists('exe_file_logger.py'): if os.path.exists('exe_file_logger.py'):
with open('exe_file_logger.py', 'r', encoding='utf-8') as f: with open('exe_file_logger.py', 'r', encoding='utf-8') as f:
content = f.read() content = f.read()
# Replace setup_file_logging function with empty function # setup_file_logging 函数替换为空函数
content = content.replace( content = content.replace(
'def setup_file_logging():', 'def setup_file_logging():',
'def setup_file_logging():\n """Production - Logging disabled"""\n return None\n\ndef setup_file_logging_original():' 'def setup_file_logging():\n """生产环境 - 禁用文件日志"""\n return None\n\ndef setup_file_logging_original():'
).replace( ).replace(
'def log_to_file(message):', 'def log_to_file(message):',
'def log_to_file(message):\n """Production - Logging disabled"""\n pass\n\ndef log_to_file_original(message):' 'def log_to_file(message):\n """生产环境 - 禁用文件日志"""\n pass\n\ndef log_to_file_original(message):'
) )
with open('exe_file_logger.py', 'w', encoding='utf-8') as f: with open('exe_file_logger.py', 'w', encoding='utf-8') as f:
f.write(content) f.write(content)
print("Disabled logging in exe_file_logger.py") print("✅ 已禁用 exe_file_logger.py 中的日志功能")
print("All logging functionality disabled") print("✅ 所有日志功能已禁用")
def restore_logging(): def restore_logging():
"""Restore logging functionality""" """恢复日志功能"""
print("Restoring development configuration...")
files_to_restore = ['main.py', 'exe_file_logger.py'] files_to_restore = ['main.py', 'exe_file_logger.py']
restored_count = 0
for file_name in files_to_restore: for file_name in files_to_restore:
backup_file = f'{file_name}.backup' backup_file = f'{file_name}.backup'
try:
if os.path.exists(backup_file): if os.path.exists(backup_file):
# Ensure target file is writable
if os.path.exists(file_name):
os.chmod(file_name, 0o666)
shutil.copy(backup_file, file_name) shutil.copy(backup_file, file_name)
os.remove(backup_file) os.remove(backup_file)
print(f"[OK] Restored {file_name}") print(f"✅ 已恢复 {file_name}")
restored_count += 1 print("✅ 所有文件已恢复到开发环境配置")
else:
print(f"[WARN] Backup file not found: {backup_file}")
except Exception as e:
print(f"[ERROR] Failed to restore {file_name}: {e}")
if restored_count == len(files_to_restore):
print("[OK] All files restored to development configuration")
else:
print(f"[WARN] Restored {restored_count}/{len(files_to_restore)} files")
def main(): def main():
"""Main function""" """主函数"""
print("Production Build Tool (No Logging Version)") print("🔥 生产环境打包工具(无日志版本)")
print("=" * 60) print("=" * 60)
try: try:
# 1. Disable logging functionality # 1. 禁用日志功能
disable_logging() disable_logging()
# 2. Execute build # 2. 执行打包
print("\nStarting build...") print("\n🚀 开始打包...")
result = subprocess.run(['python', 'quick_build.py'], capture_output=False) result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
if result.returncode == 0: if result.returncode == 0:
# Verify output (quick_build.py outputs to MultiPlatformGUI) # 确保输出目录正确性
if os.path.exists('dist/MultiPlatformGUI'): if os.path.exists('dist/main') and not os.path.exists('dist/MultiPlatformGUI'):
print("\nProduction build completed successfully!") print("🔄 修正输出目录名称...")
print("Output directory: dist/MultiPlatformGUI/") try:
print("All logging disabled - client will not generate log files") os.rename('dist/main', 'dist/MultiPlatformGUI')
print("✅ 已重命名为: dist/MultiPlatformGUI/")
except Exception as e:
print(f"⚠️ 重命名失败: {e}")
# Verify key files # 验证最终输出
if os.path.exists('dist/MultiPlatformGUI'):
print("\n🎉 生产环境打包成功!")
print("📁 输出目录: dist/MultiPlatformGUI/")
print("🔇 已禁用所有日志功能,客户端不会产生日志文件")
# 验证关键文件
exe_path = 'dist/MultiPlatformGUI/main.exe' exe_path = 'dist/MultiPlatformGUI/main.exe'
if os.path.exists(exe_path): if os.path.exists(exe_path):
size = os.path.getsize(exe_path) / 1024 / 1024 # MB size = os.path.getsize(exe_path) / 1024 / 1024 # MB
print(f"Main executable: main.exe ({size:.1f} MB)") print(f"✅ 主程序: main.exe ({size:.1f} MB)")
else: else:
print("WARNING: Main executable not found") print("⚠️ 主程序文件未找到")
# 🔥 关键修复添加PyMiniRacer DLL修复步骤
print("\nPyMiniRacer DLL fix phase started...")
try:
import fix_pyminiracer_dll
if fix_pyminiracer_dll.auto_fix_after_build():
print("PyMiniRacer DLL fix completed")
else: else:
print("WARNING: PyMiniRacer DLL fix failed") print("\n❌ 输出目录验证失败")
except Exception as e: if os.path.exists('dist/main'):
print(f"WARNING: PyMiniRacer DLL fix error: {e}") print("💡 发现: dist/main/ 目录,请手动重命名为 MultiPlatformGUI")
else: else:
print("\nERROR: Output directory verification failed - dist/MultiPlatformGUI not found") print("\n❌ 打包失败")
else:
print("\nERROR: Build failed")
except Exception as e: except Exception as e:
print(f"ERROR: Build process error: {e}") print(f"❌ 打包过程出错: {e}")
finally: finally:
# 3. Restore logging functionality (for development) # 3. 恢复日志功能(用于开发)
print() print("\n🔄 恢复开发环境配置...")
restore_logging() restore_logging()
print("\n" + "=" * 60) print("\n" + "=" * 60)
input("按Enter键退出...")
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@@ -9,20 +9,19 @@ import json # 用于将令牌保存为 JSON 格式
# 后端服务器配置 # 后端服务器配置
# BACKEND_HOST = "192.168.5.233" # BACKEND_HOST = "192.168.5.233"
# BACKEND_HOST = "192.168.5.106" BACKEND_HOST = "192.168.5.12"
# BACKEND_HOST = "192.168.5.12" # BACKEND_HOST = "shuidrop.com"
BACKEND_HOST = "shuidrop.com"
# BACKEND_HOST = "test.shuidrop.com" # BACKEND_HOST = "test.shuidrop.com"
# BACKEND_PORT = "8000" BACKEND_PORT = "8000"
BACKEND_PORT = "" # BACKEND_PORT = ""
# BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}" BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
BACKEND_WS_URL = f"wss://{BACKEND_HOST}" # BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
# WebSocket配置 # WebSocket配置
WS_CONNECT_TIMEOUT = 16.0 WS_CONNECT_TIMEOUT = 16.0
WS_MESSAGE_TIMEOUT = 30.0 WS_MESSAGE_TIMEOUT = 30.0
WS_PING_INTERVAL = 15 # 10秒ping间隔提高检测频率 WS_PING_INTERVAL = 10 # 10秒ping间隔提高检测频率
WS_PING_TIMEOUT = 10 # 5秒ping超时更快检测断线 WS_PING_TIMEOUT = 5 # 5秒ping超时更快检测断线
WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳 WS_ENABLE_PING = True # 是否启用WebSocket原生ping心跳
WS_ENABLE_APP_PING = False # 禁用应用层ping心跳避免重复 WS_ENABLE_APP_PING = False # 禁用应用层ping心跳避免重复
@@ -38,48 +37,10 @@ FUTURE_TIMEOUT = 300 # 5分钟
# 终端日志配置(简化) # 终端日志配置(简化)
LOG_LEVEL = "INFO" LOG_LEVEL = "INFO"
VERSION = "1.0" VERSION = "1.0"
APP_VERSION = "1.4.8" # 应用版本号(用于版本检查)
# GUI配置 # GUI配置
WINDOW_TITLE = "AI回复连接入口-V1.0" WINDOW_TITLE = "AI回复连接入口-V1.0"
# 应用版本号(用于版本检查)
APP_VERSION = "1.5.71"
# 🔥 多实例运行模式开关
# - True: 测试模式多实例不保存token避免冲突
# - False: 生产模式单实例保存token自动加载
#
# 使用方法:
# 1. 修改此值MULTI_INSTANCE_MODE = False # 改为生产模式
# 2. 或设置环境变量SHUIDROP_MULTI_INSTANCE=0 # 临时切换到生产模式
MULTI_INSTANCE_MODE = True # 默认:测试模式
def is_multi_instance_mode() -> bool:
"""
检查是否为多实例模式(支持环境变量覆盖)
优先级:
1. 环境变量 SHUIDROP_MULTI_INSTANCE0=生产1=测试)
2. 配置文件 MULTI_INSTANCE_MODE
Returns:
bool: True=多实例模式False=单实例模式
"""
# 检查环境变量
env_value = os.getenv('SHUIDROP_MULTI_INSTANCE')
if env_value is not None:
# 0, false, False, no, No → 生产模式
if env_value.lower() in ('0', 'false', 'no'):
return False
# 1, true, True, yes, Yes → 测试模式
if env_value.lower() in ('1', 'true', 'yes'):
return True
# 使用配置文件值 (如果不做设置我们可以直接用编码变量进行控制是否可以允许多实例的方式运行)
return MULTI_INSTANCE_MODE
# 平台特定配置 # 平台特定配置
PLATFORMS = { PLATFORMS = {
"JD": { "JD": {

View File

@@ -1,344 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
自定义版本更新对话框
美观的布局和用户体验
"""
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QHBoxLayout, QLabel,
QPushButton, QTextEdit, QFrame)
from PyQt5.QtCore import Qt
from PyQt5.QtGui import QFont, QCursor
class UpdateNotificationDialog(QDialog):
"""
版本更新提示对话框
布局:
- 标题区
- 更新内容区
- 下载地址区(可复制)
- 按钮区(两行,美观布局)
"""
# 用户选择(返回值)
CHOICE_UPDATE_NOW = 1 # 立即更新
CHOICE_UPDATE_LATER = 2 # 稍后更新
CHOICE_IGNORE = 3 # 忽略此版本
def __init__(self, version, download_url, update_content="", parent=None):
super().__init__(parent)
self.version = version
self.download_url = download_url
self.update_content = update_content
self.user_choice = None
self.init_ui()
def init_ui(self):
"""初始化UI"""
self.setWindowTitle("版本更新")
self.setFixedWidth(550)
self.setMinimumHeight(400)
# 禁用关闭按钮(只能通过按钮关闭)
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
# 主布局
layout = QVBoxLayout()
layout.setSpacing(15)
layout.setContentsMargins(25, 20, 25, 20)
# ============ 标题区 ============
title_label = QLabel(f"🔔 发现新版本 v{self.version}")
title_label.setFont(QFont('Microsoft YaHei', 14, QFont.Bold))
title_label.setAlignment(Qt.AlignCenter)
title_label.setStyleSheet("color: #2c3e50; margin-bottom: 5px;")
layout.addWidget(title_label)
# 分隔线
line1 = QFrame()
line1.setFrameShape(QFrame.HLine)
line1.setStyleSheet("background: #ddd; max-height: 1px;")
layout.addWidget(line1)
# ============ 更新内容区(可选)============
# 🔥 只有在有更新内容时才显示
if self.update_content and self.update_content.strip():
content_label = QLabel("📝 更新内容:")
content_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold))
content_label.setStyleSheet("color: #34495e;")
layout.addWidget(content_label)
# 更新内容文本
content_text = QTextEdit()
content_text.setReadOnly(True)
content_text.setMaximumHeight(100)
content_text.setStyleSheet("""
QTextEdit {
border: 1px solid #ddd;
border-radius: 6px;
padding: 8px;
background: #f8f9fa;
font-size: 11px;
color: #555;
}
""")
content_text.setText(self.update_content)
layout.addWidget(content_text)
# ============ 下载地址区 ============
url_label = QLabel("📥 下载地址:")
url_label.setFont(QFont('Microsoft YaHei', 10, QFont.Bold))
url_label.setStyleSheet("color: #34495e; margin-top: 5px;")
layout.addWidget(url_label)
# 下载地址文本框(可选择复制)
self.url_text = QTextEdit()
self.url_text.setReadOnly(True)
self.url_text.setMaximumHeight(60)
self.url_text.setText(self.download_url)
self.url_text.setStyleSheet("""
QTextEdit {
border: 2px solid #4285f4;
border-radius: 6px;
padding: 8px;
background: #e3f2fd;
font-size: 10px;
color: #1565c0;
font-family: 'Consolas', 'Courier New', monospace;
}
""")
# 全选文本,方便复制
self.url_text.selectAll()
layout.addWidget(self.url_text)
# 复制提示
copy_hint = QLabel("💡 提示:上方地址已自动选中,按 Ctrl+C 即可复制")
copy_hint.setFont(QFont('Microsoft YaHei', 8))
copy_hint.setStyleSheet("color: #7f8c8d;")
layout.addWidget(copy_hint)
# 分隔线
line2 = QFrame()
line2.setFrameShape(QFrame.HLine)
line2.setStyleSheet("background: #ddd; max-height: 1px; margin-top: 10px;")
layout.addWidget(line2)
# ============ 操作说明区 ============
hint_label = QLabel(
"• 在线自动更新:自动下载并安装新版本\n"
"• 稍后更新:保留提示在状态栏,可随时点击更新\n"
"• 忽略此版本:本次运行不再提示此版本"
)
hint_label.setFont(QFont('Microsoft YaHei', 9))
hint_label.setStyleSheet("color: #666; margin: 5px 0;")
layout.addWidget(hint_label)
# ============ 按钮区(两行布局)============
# 第一行:主要操作
button_row1 = QHBoxLayout()
button_row1.setSpacing(10)
self.update_now_btn = QPushButton("🚀 在线自动更新")
self.update_now_btn.setFont(QFont('Microsoft YaHei', 11, QFont.Bold))
self.update_now_btn.setMinimumHeight(42)
self.update_now_btn.setCursor(QCursor(Qt.PointingHandCursor))
self.update_now_btn.clicked.connect(self.on_update_now)
self.update_later_btn = QPushButton("⏰ 稍后更新")
self.update_later_btn.setFont(QFont('Microsoft YaHei', 11))
self.update_later_btn.setMinimumHeight(42)
self.update_later_btn.setCursor(QCursor(Qt.PointingHandCursor))
self.update_later_btn.clicked.connect(self.on_update_later)
button_row1.addWidget(self.update_now_btn)
button_row1.addWidget(self.update_later_btn)
layout.addLayout(button_row1)
# 第二行:次要操作
button_row2 = QHBoxLayout()
button_row2.setSpacing(10)
self.ignore_btn = QPushButton("🚫 忽略此版本")
self.ignore_btn.setFont(QFont('Microsoft YaHei', 10))
self.ignore_btn.setMinimumHeight(38)
self.ignore_btn.setCursor(QCursor(Qt.PointingHandCursor))
self.ignore_btn.clicked.connect(self.on_ignore)
self.copy_btn = QPushButton("📋 复制下载地址")
self.copy_btn.setFont(QFont('Microsoft YaHei', 10))
self.copy_btn.setMinimumHeight(38)
self.copy_btn.setCursor(QCursor(Qt.PointingHandCursor))
self.copy_btn.clicked.connect(self.on_copy_url)
button_row2.addWidget(self.ignore_btn)
button_row2.addWidget(self.copy_btn)
layout.addLayout(button_row2)
self.setLayout(layout)
# 应用样式
self.apply_styles()
def apply_styles(self):
"""应用现代化样式"""
self.setStyleSheet("""
QDialog {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #f8fafb,
stop:1 #e8ecef
);
}
/* 主要操作按钮(立即更新、稍后更新)*/
QPushButton#updateNowBtn {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #4285f4,
stop:1 #1565c0
);
border: none;
color: white;
border-radius: 8px;
font-weight: bold;
}
QPushButton#updateNowBtn:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #5294f5,
stop:1 #1e74c1
);
}
QPushButton#updateLaterBtn {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #ff9800,
stop:1 #f57c00
);
border: none;
color: white;
border-radius: 8px;
}
QPushButton#updateLaterBtn:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #ffa726,
stop:1 #fb8c00
);
}
/* 次要操作按钮(忽略、复制)*/
QPushButton {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #f5f5f5,
stop:1 #e0e0e0
);
border: 2px solid #ccc;
border-radius: 8px;
color: #555;
}
QPushButton:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #e8e8e8,
stop:1 #d0d0d0
);
border: 2px solid #999;
}
""")
# 设置按钮对象名以应用特定样式
self.update_now_btn.setObjectName("updateNowBtn")
self.update_later_btn.setObjectName("updateLaterBtn")
def on_update_now(self):
"""立即更新"""
self.user_choice = self.CHOICE_UPDATE_NOW
self.accept()
def on_update_later(self):
"""稍后更新"""
self.user_choice = self.CHOICE_UPDATE_LATER
self.accept()
def on_ignore(self):
"""忽略此版本"""
self.user_choice = self.CHOICE_IGNORE
self.accept()
def on_copy_url(self):
"""复制下载地址"""
from PyQt5.QtWidgets import QApplication, QMessageBox
clipboard = QApplication.clipboard()
clipboard.setText(self.download_url)
# 显示复制成功提示(不关闭主对话框)
QMessageBox.information(
self,
"复制成功",
f"✅ 下载地址已复制到剪贴板!\n\n"
f"您可以直接粘贴(Ctrl+V)到浏览器下载。",
QMessageBox.Ok
)
def get_user_choice(self):
"""获取用户选择"""
return self.user_choice
if __name__ == "__main__":
# 测试对话框
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
print("=" * 60)
print("测试更新对话框")
print("=" * 60)
print("\n场景1有更新内容")
dialog = UpdateNotificationDialog(
version="1.5.64",
download_url="https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.64.exe",
update_content="• 修复了DY图片/视频上传问题\n• 优化了自动更新流程\n• 添加了圆形进度条"
)
dialog.exec_()
choice = dialog.get_user_choice()
if choice == UpdateNotificationDialog.CHOICE_UPDATE_NOW:
print("\n用户选择:在线自动更新")
elif choice == UpdateNotificationDialog.CHOICE_UPDATE_LATER:
print("\n用户选择:稍后更新")
elif choice == UpdateNotificationDialog.CHOICE_IGNORE:
print("\n用户选择:忽略此版本")
else:
print("\n用户关闭了对话框")
# 测试场景2无更新内容
print("\n" + "=" * 60)
print("场景2无更新内容应该隐藏更新内容区域")
print("=" * 60)
dialog2 = UpdateNotificationDialog(
version="1.5.65",
download_url="https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.65.exe",
update_content="" # 空内容
)
dialog2.exec_()
print("\n测试完成!")
sys.exit(0)

View File

@@ -1,136 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
PyMiniRacer DLL Auto-Fix Script
Automatically copy PyMiniRacer native DLL files after build
"""
import os
import shutil
import sys
from pathlib import Path
def find_mini_racer_dll():
"""Find PyMiniRacer DLL files"""
print("Searching for PyMiniRacer DLL files...")
try:
import py_mini_racer
pkg_path = Path(py_mini_racer.__file__).parent
# Find DLL files
dll_files = list(pkg_path.glob("*.dll")) + list(pkg_path.glob("*.pyd"))
if dll_files:
print(f"OK: Found PyMiniRacer native library files:")
for dll in dll_files:
print(f" - {dll.name} ({dll.stat().st_size / 1024:.1f} KB)")
return dll_files
else:
print("WARN: No DLL files found")
# Try subdirectories
for subdir in pkg_path.iterdir():
if subdir.is_dir():
dll_files = list(subdir.glob("*.dll")) + list(subdir.glob("*.pyd"))
if dll_files:
print(f"OK: Found in subdirectory {subdir.name}:")
for dll in dll_files:
print(f" - {dll.name}")
return dll_files
return []
except ImportError:
print("ERROR: PyMiniRacer is not installed")
return []
except Exception as e:
print(f"ERROR: Failed to find DLL: {e}")
return []
def copy_dll_to_dist():
"""Copy DLL files to dist directory"""
print("\n" + "=" * 70)
print("PyMiniRacer DLL Auto-Fix Tool")
print("=" * 70)
# Find dist directory
dist_dir = Path("dist/MultiPlatformGUI/_internal")
if not dist_dir.exists():
print(f"ERROR: dist directory not found: {dist_dir}")
print(" Please run: python quick_build.py or python build_production.py")
return False
print(f"OK: Found dist directory: {dist_dir}")
# Find source DLL
dll_files = find_mini_racer_dll()
if not dll_files:
print("\nERROR: Cannot find PyMiniRacer DLL files")
print(" Please ensure it is installed: pip install py-mini-racer")
return False
# Copy DLL files
print(f"\nCopying DLL files to dist directory...")
success_count = 0
for dll_file in dll_files:
try:
# Target path (put in _internal root directory)
target = dist_dir / dll_file.name
# Copy file
shutil.copy2(dll_file, target)
print(f"OK: Copied {dll_file.name} -> {target}")
success_count += 1
# Also copy to py_mini_racer subdirectory (if exists)
py_mini_racer_dir = dist_dir / "py_mini_racer"
if py_mini_racer_dir.exists():
target2 = py_mini_racer_dir / dll_file.name
shutil.copy2(dll_file, target2)
print(f"OK: Copied {dll_file.name} -> {target2}")
except Exception as e:
print(f"ERROR: Failed to copy {dll_file.name}: {e}")
if success_count > 0:
print(f"\nSUCCESS: Copied {success_count} file(s)")
print("\nVerification:")
print(f" Checking if file exists: {dist_dir / 'mini_racer.dll'}")
if (dist_dir / 'mini_racer.dll').exists():
size = (dist_dir / 'mini_racer.dll').stat().st_size / (1024 * 1024)
print(f" OK: mini_racer.dll exists ({size:.1f} MB)")
print("\nYou can now run:")
print(" cd dist\\MultiPlatformGUI")
print(" .\\main.exe")
print("\n PDD platform should work properly now!")
return True
else:
print("\nERROR: No files were copied successfully")
return False
def auto_fix_after_build():
"""Auto-fix after build (called by build script)"""
return copy_dll_to_dist()
def main():
"""Main function"""
success = copy_dll_to_dist()
if not success:
print("\nAlternative solutions:")
print(" 1. Manually install Node.js (to enable execjs)")
print(" 2. Check if PyMiniRacer is installed correctly")
print(" 3. Try a different version of py-mini-racer")
sys.exit(1)
print("\nDLL fix completed successfully!")
if __name__ == "__main__":
main()

View File

@@ -21,69 +21,48 @@ class NSISInstaller:
self.assets_dir = self.script_dir / "assets" self.assets_dir = self.script_dir / "assets"
self.nsis_script = self.script_dir / "installer.nsi" self.nsis_script = self.script_dir / "installer.nsi"
# 应用程序信息使用英文避免CI/CD环境乱码 # 应用程序信息
self.app_name = "ShuiDi AI Assistant" self.app_name = "水滴AI客服智能助手"
self.app_name_en = "ShuiDi AI Assistant" self.app_name_en = "ShuiDi AI Assistant"
self.app_version = "1.0.0"
# 从 config.py 读取版本号(自动同步) self.app_publisher = "水滴智能科技"
self.app_version = self._get_version_from_config()
self.app_publisher = "Shuidrop Technology"
self.app_url = "https://shuidrop.com/" self.app_url = "https://shuidrop.com/"
self.exe_name = "main.exe" self.exe_name = "main.exe"
def _get_version_from_config(self):
"""从 config.py 读取版本号"""
try:
config_file = self.project_root / "config.py"
if config_file.exists():
with open(config_file, 'r', encoding='utf-8') as f:
content = f.read()
import re
match = re.search(r'APP_VERSION\s*=\s*["\']([^"\']+)["\']', content)
if match:
version = match.group(1)
print(f"Read version from config.py: v{version}")
return version
except Exception as e:
print(f"WARNING: Failed to read version: {e}, using default version")
return "1.0.0"
def check_prerequisites(self): def check_prerequisites(self):
"""检查构建前提条件""" """检查构建前提条件"""
print("Checking build prerequisites...") print("🔍 检查构建前提条件...")
# 检查NSIS安装 # 检查NSIS安装
try: try:
result = subprocess.run(['makensis', '/VERSION'], result = subprocess.run(['makensis', '/VERSION'],
capture_output=True, text=True, check=True) capture_output=True, text=True, check=True)
nsis_version = result.stdout.strip() nsis_version = result.stdout.strip()
print(f"NSIS version: {nsis_version}") print(f"NSIS 版本: {nsis_version}")
except (subprocess.CalledProcessError, FileNotFoundError): except (subprocess.CalledProcessError, FileNotFoundError):
print("ERROR: NSIS or makensis command not found") print("❌ 错误: 未找到NSIS或makensis命令")
print(" Please download and install NSIS from https://nsis.sourceforge.io/Download") print(" 请从 https://nsis.sourceforge.io/Download 下载并安装NSIS")
print(" Make sure to add NSIS to system PATH") print(" 确保将NSIS添加到系统PATH环境变量")
return False return False
# 检查dist目录 # 检查dist目录
if not self.dist_dir.exists(): if not self.dist_dir.exists():
print(f"ERROR: Build output directory not found: {self.dist_dir}") print(f"❌ 错误: 未找到构建输出目录 {self.dist_dir}")
print(" Please run build_production.py first") print(" 请先运行 build_production.py 构建应用程序")
return False return False
# 检查主程序文件 # 检查主程序文件
exe_path = self.dist_dir / self.exe_name exe_path = self.dist_dir / self.exe_name
if not exe_path.exists(): if not exe_path.exists():
print(f"ERROR: Main executable not found: {exe_path}") print(f"❌ 错误: 未找到主程序文件 {exe_path}")
return False return False
print(f"Found main executable: {exe_path}") print(f"✅ 找到主程序: {exe_path}")
return True return True
def prepare_assets(self): def prepare_assets(self):
"""准备安装包资源文件""" """准备安装包资源文件"""
print("Preparing asset files...") print("📁 准备资源文件...")
# 创建assets目录 # 创建assets目录
self.assets_dir.mkdir(exist_ok=True) self.assets_dir.mkdir(exist_ok=True)
@@ -107,14 +86,14 @@ class NSISInstaller:
img = Image.open(icon_source) img = Image.open(icon_source)
ico_path = self.assets_dir / "icon.ico" ico_path = self.assets_dir / "icon.ico"
img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)]) img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
print(f"Converted main icon: {icon_source.name} -> icon.ico") print(f"✅ 转换主程序图标: {icon_source.name} -> icon.ico")
icon_found = True icon_found = True
break break
except ImportError: except ImportError:
print("WARNING: PIL library not installed, skipping icon conversion") print("⚠️ 未安装PIL库跳过图标转换")
break break
except Exception as e: except Exception as e:
print(f"WARNING: Main icon conversion failed: {e}") print(f"⚠️ 主程序图标转换失败: {e}")
continue continue
# 转换卸载程序图标 # 转换卸载程序图标
@@ -125,17 +104,17 @@ class NSISInstaller:
img = Image.open(uninstall_icon_source) img = Image.open(uninstall_icon_source)
uninstall_ico_path = self.assets_dir / "uninstall_icon.ico" uninstall_ico_path = self.assets_dir / "uninstall_icon.ico"
img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)]) img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
print(f"Converted uninstall icon: {uninstall_icon_source.name} -> uninstall_icon.ico") print(f"✅ 转换卸载程序图标: {uninstall_icon_source.name} -> uninstall_icon.ico")
uninstall_icon_found = True uninstall_icon_found = True
except ImportError: except ImportError:
print("WARNING: PIL library not installed, skipping uninstall icon conversion") print("⚠️ 未安装PIL库跳过卸载图标转换")
except Exception as e: except Exception as e:
print(f"WARNING: Uninstall icon conversion failed: {e}") print(f"⚠️ 卸载程序图标转换失败: {e}")
else: else:
print("WARNING: Uninstall icon not found, will use main icon") print("⚠️ 未找到卸载程序专用图标,将使用主程序图标")
if not icon_found: if not icon_found:
print("WARNING: Main icon not found, will use default icon") print("⚠️ 未找到主程序图标文件,将使用默认图标")
return { return {
'main_icon': icon_found, 'main_icon': icon_found,
@@ -144,24 +123,16 @@ class NSISInstaller:
def generate_nsis_script(self, icon_info): def generate_nsis_script(self, icon_info):
"""生成NSIS安装脚本""" """生成NSIS安装脚本"""
print("Generating NSIS installer script...") print("📝 生成NSIS安装脚本...")
# 标准化安装包命名(不带时间戳和空格,便于固定下载地址) timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
# 将应用名称中的空格替换为下划线 installer_name = f"{self.app_name_en}_Setup_{self.app_version}_{timestamp}.exe"
app_name_safe = self.app_name_en.replace(' ', '_')
installer_name = f"{app_name_safe}_Setup_v{self.app_version}.exe"
# 示例: ShuiDi_AI_Assistant_Setup_v1.4.12.exe
print(f"Installer name: {installer_name}") nsis_content = f'''# 水滴AI客服智能助手 NSIS 安装脚本
nsis_content = f'''# ShuiDi AI Assistant NSIS Installer Script
# 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")} # 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
Unicode True Unicode True
# 🔥 请求管理员权限(解决静默安装时的"Access denied"问题)
RequestExecutionLevel admin
# 定义应用程序信息 # 定义应用程序信息
!define APP_NAME "{self.app_name}" !define APP_NAME "{self.app_name}"
!define APP_NAME_EN "{self.app_name_en}" !define APP_NAME_EN "{self.app_name_en}"
@@ -200,7 +171,7 @@ InstallDirRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallP
# 版本信息 # 版本信息
VIProductVersion "{self.app_version}.0" VIProductVersion "{self.app_version}.0"
VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}" VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}"
VIAddVersionKey /LANG=2052 "Comments" "ShuiDi AI Assistant" VIAddVersionKey /LANG=2052 "Comments" "水滴AI客服智能助手"
VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}" VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}"
VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序" VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序"
VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}" VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}"
@@ -267,7 +238,7 @@ SectionEnd
with open(self.nsis_script, 'w', encoding='utf-8-sig') as f: with open(self.nsis_script, 'w', encoding='utf-8-sig') as f:
f.write(nsis_content) f.write(nsis_content)
print(f"NSIS script generated: {self.nsis_script}") print(f"NSIS脚本已生成: {self.nsis_script}")
return installer_name return installer_name
def create_license_file(self): def create_license_file(self):
@@ -292,11 +263,11 @@ SectionEnd
with open(license_file, 'w', encoding='utf-8-sig') as f: with open(license_file, 'w', encoding='utf-8-sig') as f:
f.write(license_content) f.write(license_content)
print(f"License file created: {license_file}") print(f"✅ 许可证文件已创建: {license_file}")
def build_installer(self): def build_installer(self):
"""构建安装包""" """构建安装包"""
print("Building NSIS installer...") print("🚀 开始构建NSIS安装包...")
# 创建输出目录 # 创建输出目录
self.output_dir.mkdir(exist_ok=True) self.output_dir.mkdir(exist_ok=True)
@@ -307,22 +278,22 @@ SectionEnd
result = subprocess.run(cmd, cwd=str(self.script_dir), result = subprocess.run(cmd, cwd=str(self.script_dir),
capture_output=True, text=True, check=True) capture_output=True, text=True, check=True)
print("NSIS compilation successful") print("NSIS编译成功")
if result.stdout: if result.stdout:
print("NSIS output:") print("NSIS输出:")
print(result.stdout) print(result.stdout)
return True return True
except subprocess.CalledProcessError as e: except subprocess.CalledProcessError as e:
print("ERROR: NSIS compilation failed") print("❌ NSIS编译失败")
print(f"Error message: {e.stderr}") print(f"错误信息: {e.stderr}")
return False return False
def run(self): def run(self):
"""执行完整的构建流程""" """执行完整的构建流程"""
print("=" * 60) print("=" * 60)
print(f"Installer Build Tool for {self.app_name}") print(f"🔧 {self.app_name} 安装包构建工具")
print("=" * 60) print("=" * 60)
try: try:
@@ -348,18 +319,18 @@ SectionEnd
if installer_path.exists(): if installer_path.exists():
installer_size = installer_path.stat().st_size / (1024 * 1024) installer_size = installer_path.stat().st_size / (1024 * 1024)
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("Installer build completed successfully!") print("🎉 安装包构建成功!")
print(f"Installer location: {installer_path}") print(f"📁 安装包位置: {installer_path}")
print(f"File size: {installer_size:.1f} MB") print(f"📏 文件大小: {installer_size:.1f} MB")
print("Ready to distribute to users") print("🚀 可以直接分发给用户使用")
print("=" * 60) print("=" * 60)
return True return True
else: else:
print("ERROR: Installer file not found") print("❌ 安装包文件未找到")
return False return False
except Exception as e: except Exception as e:
print(f"ERROR: Build process error: {e}") print(f"❌ 构建过程出错: {e}")
return False return False
def main(): def main():

1131
main.py

File diff suppressed because it is too large Load Diff

View File

@@ -12,57 +12,54 @@ from pathlib import Path
def clean_build(): def clean_build():
"""清理构建目录""" """清理构建目录"""
print("Cleaning old build files...") print("🧹 清理旧的构建文件...")
# 跳过进程结束步骤,避免影响当前脚本运行 # 跳过进程结束步骤,避免影响当前脚本运行
print("Skipping process termination step...") print("🔄 跳过进程结束步骤(避免脚本自终止)...")
# 等待少量时间确保文件句柄释放 # 等待少量时间确保文件句柄释放
import time import time
print("Waiting for file handles to release...") print("⏳ 等待文件句柄释放...")
time.sleep(0.5) time.sleep(0.5)
dirs_to_clean = ['dist', 'build'] dirs_to_clean = ['dist', 'build']
for dir_name in dirs_to_clean: for dir_name in dirs_to_clean:
print(f"Checking directory: {dir_name}") print(f"🔍 检查目录: {dir_name}")
if os.path.exists(dir_name): if os.path.exists(dir_name):
try: try:
print(f"Deleting: {dir_name}") print(f"🗑️ 正在删除: {dir_name}")
shutil.rmtree(dir_name) shutil.rmtree(dir_name)
print(f"Deleted: {dir_name}") print(f"✅ 已删除: {dir_name}")
except PermissionError as e: except PermissionError as e:
print(f"WARNING: {dir_name} is in use, attempting force delete... Error: {e}") print(f"⚠️ {dir_name} 被占用,尝试强制删除... 错误: {e}")
try: try:
subprocess.run(['rd', '/s', '/q', dir_name], subprocess.run(['rd', '/s', '/q', dir_name],
shell=True, check=True) shell=True, check=True)
print(f"Force delete successful: {dir_name}") print(f"✅ 强制删除成功: {dir_name}")
except Exception as e2: except Exception as e2:
print(f"ERROR: Cannot delete {dir_name}: {e2}") print(f"❌ 无法删除 {dir_name}: {e2}")
print("TIP: Please delete manually or run force_clean_dist.bat") print("💡 请手动删除或运行 force_clean_dist.bat")
except Exception as e: except Exception as e:
print(f"ERROR: Failed to delete {dir_name}: {e}") print(f"❌ 删除 {dir_name} 时出错: {e}")
else: else:
print(f"INFO: {dir_name} does not exist, skipping") print(f" {dir_name} 不存在,跳过")
print("Cleaning phase completed") print("✅ 清理阶段完成")
def build_with_command(): def build_with_command():
"""使用命令行参数直接打包""" """使用命令行参数直接打包"""
print("Starting build...") print("🚀 开始打包...")
cmd = [ cmd = [
'pyinstaller', 'pyinstaller',
'--name=MultiPlatformGUI', # 直接使用最终目录名 '--name=main',
'--onedir', # 相当于 --exclude-binaries '--onedir', # 相当于 --exclude-binaries
'--windowed', # 相当于 -w '--windowed', # 相当于 -w
'--icon=static/ai_assistant_icon_64.png', # 添加主程序图标 '--icon=static/ai_assistant_icon_64.png', # 添加主程序图标
'--add-data=config.py;.', '--add-data=config.py;.',
'--add-data=exe_file_logger.py;.', '--add-data=exe_file_logger.py;.',
'--add-data=windows_taskbar_fix.py;.', '--add-data=windows_taskbar_fix.py;.',
'--add-data=auto_updater.py;.',
'--add-data=update_dialog.py;.',
'--add-data=version_checker.py;.',
'--add-data=Utils/PythonNew32;Utils/PythonNew32', '--add-data=Utils/PythonNew32;Utils/PythonNew32',
'--add-data=Utils/JD;Utils/JD', '--add-data=Utils/JD;Utils/JD',
'--add-data=Utils/Dy;Utils/Dy', '--add-data=Utils/Dy;Utils/Dy',
@@ -90,44 +87,42 @@ def build_with_command():
'--hidden-import=WebSocket.backend_singleton', '--hidden-import=WebSocket.backend_singleton',
'--hidden-import=WebSocket.BackendClient', '--hidden-import=WebSocket.BackendClient',
'--hidden-import=windows_taskbar_fix', '--hidden-import=windows_taskbar_fix',
'--hidden-import=py_mini_racer', # PDD平台内置JavaScript引擎
'--hidden-import=py_mini_racer.py_mini_racer',
'--collect-all=py_mini_racer', # 🔧 收集所有PyMiniRacer文件包括DLL
'main.py' 'main.py'
] ]
try: try:
print(f"go to compile: {' '.join(cmd[:5])}... (all{len(cmd)} args)") print(f"执行命令: {' '.join(cmd[:5])}... ({len(cmd)}个参数)")
result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8') result = subprocess.run(cmd, capture_output=True, text=True, encoding='utf-8')
if result.returncode == 0: if result.returncode == 0:
print("Build successful!") print("✅ 打包成功!")
print("Build result: dist/MultiPlatformGUI/") print("📁 打包结果: dist/main/")
# 验证输出目录(不需要重命名了) # 重命名目录为统一的输出路径
if os.path.exists('dist/MultiPlatformGUI/MultiPlatformGUI.exe'): if os.path.exists('dist/main'):
# PyInstaller 生成的 exe 名称是 MultiPlatformGUI.exe
# 重命名为 main.exe保持兼容性
try: try:
old_exe = 'dist/MultiPlatformGUI/MultiPlatformGUI.exe' # 如果目标目录已存在,先删除
new_exe = 'dist/MultiPlatformGUI/main.exe' if os.path.exists('dist/MultiPlatformGUI'):
print("🗑️ 删除旧的 MultiPlatformGUI 目录...")
shutil.rmtree('dist/MultiPlatformGUI')
if os.path.exists(new_exe): # 重命名
os.remove(new_exe) os.rename('dist/main', 'dist/MultiPlatformGUI')
print("✅ 已重命名为: dist/MultiPlatformGUI/")
os.rename(old_exe, new_exe) # 验证重命名结果
print("Main executable renamed: MultiPlatformGUI.exe -> main.exe") if os.path.exists('dist/MultiPlatformGUI/main.exe'):
exe_size = os.path.getsize(new_exe) / 1024 / 1024
print(f"Main executable verified: main.exe ({exe_size:.1f} MB)")
except Exception as e:
print(f"WARNING: Failed to rename main executable: {e}")
elif os.path.exists('dist/MultiPlatformGUI/main.exe'):
# 如果已经是 main.exe直接验证
exe_size = os.path.getsize('dist/MultiPlatformGUI/main.exe') / 1024 / 1024 exe_size = os.path.getsize('dist/MultiPlatformGUI/main.exe') / 1024 / 1024
print(f"Main executable verified: main.exe ({exe_size:.1f} MB)") print(f"✅ 主程序验证通过: main.exe ({exe_size:.1f} MB)")
else: else:
print("WARNING: Main executable file not found") print("⚠️ 重命名后主程序文件验证失败")
except Exception as e:
print(f"⚠️ 重命名失败: {e}")
print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI")
print("📁 当前可用路径: dist/main/")
else:
print("⚠️ 未找到 dist/main 目录,重命名跳过")
# 创建使用说明 # 创建使用说明
try: try:
@@ -137,14 +132,14 @@ def build_with_command():
return True return True
else: else:
print("ERROR: Build failed") print("❌ 打包失败")
if result.stderr: if result.stderr:
print("Error messages:") print("错误信息:")
print(result.stderr) print(result.stderr)
return False return False
except Exception as e: except Exception as e:
print(f"ERROR: Build process error: {e}") print(f"❌ 打包过程出错: {e}")
return False return False
@@ -203,9 +198,9 @@ MultiPlatformGUI/
try: try:
with open('dist/MultiPlatformGUI/使用说明.txt', 'w', encoding='utf-8') as f: with open('dist/MultiPlatformGUI/使用说明.txt', 'w', encoding='utf-8') as f:
f.write(guide_content) f.write(guide_content)
print("Usage guide file generated") print("✅ 已生成使用说明文件")
except: except:
print("WARNING: Usage guide generation failed") print("⚠️ 使用说明生成失败")
def create_usage_guide_fallback(): def create_usage_guide_fallback():
@@ -233,148 +228,139 @@ cd dist/main
try: try:
with open('dist/main/使用说明.txt', 'w', encoding='utf-8') as f: with open('dist/main/使用说明.txt', 'w', encoding='utf-8') as f:
f.write(guide_content) f.write(guide_content)
print("Usage guide file generated (fallback path)") print("✅ 已生成使用说明文件 (备用路径)")
except: except:
print("WARNING: Usage guide generation failed") print("⚠️ 使用说明生成失败")
def verify_result(): def verify_result():
"""验证打包结果""" """验证打包结果"""
print("\nVerifying build result...") print("\n🔍 验证打包结果...")
# 检查MultiPlatformGUI目录(统一输出目录) # 优先检查MultiPlatformGUI(标准输出目录)
target_dir = "dist/MultiPlatformGUI" target_dir = "dist/MultiPlatformGUI"
fallback_dir = "dist/main"
if os.path.exists(target_dir): if os.path.exists(target_dir):
return _verify_directory(target_dir) return _verify_directory(target_dir)
elif os.path.exists(fallback_dir):
print(f"⚠️ 发现备用目录: {fallback_dir}")
print("💡 建议重命名为: dist/MultiPlatformGUI")
return _verify_directory(fallback_dir)
else: else:
print("ERROR: Build output directory not found: dist/MultiPlatformGUI") print("❌ 未找到任何打包输出目录")
print("TIP: Please check if PyInstaller executed successfully")
return False return False
def _verify_directory(base_dir): def _verify_directory(base_dir):
"""验证指定目录的打包结果""" """验证指定目录的打包结果"""
# 检查可能的 exe 名称
exe_path = f"{base_dir}/main.exe" exe_path = f"{base_dir}/main.exe"
if not os.path.exists(exe_path):
exe_path = f"{base_dir}/MultiPlatformGUI.exe"
internal_dir = f"{base_dir}/_internal" internal_dir = f"{base_dir}/_internal"
dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll" dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll"
static_dir = f"{base_dir}/_internal/static" static_dir = f"{base_dir}/_internal/static"
print(f"Verifying directory: {base_dir}") print(f"📁 验证目录: {base_dir}")
# 检查主程序 # 检查主程序
if os.path.exists(exe_path): if os.path.exists(exe_path):
size = os.path.getsize(exe_path) / 1024 / 1024 # MB size = os.path.getsize(exe_path) / 1024 / 1024 # MB
print(f"Main executable: main.exe ({size:.1f} MB)") print(f"✅ 主程序: main.exe ({size:.1f} MB)")
else: else:
print("ERROR: Main executable main.exe does not exist") print("❌ 主程序 main.exe 不存在")
return False return False
# 检查_internal目录 # 检查_internal目录
if os.path.exists(internal_dir): if os.path.exists(internal_dir):
file_count = len([f for f in os.listdir(internal_dir) if os.path.isfile(os.path.join(internal_dir, f))]) file_count = len([f for f in os.listdir(internal_dir) if os.path.isfile(os.path.join(internal_dir, f))])
dir_count = len([d for d in os.listdir(internal_dir) if os.path.isdir(os.path.join(internal_dir, d))]) dir_count = len([d for d in os.listdir(internal_dir) if os.path.isdir(os.path.join(internal_dir, d))])
print(f"Dependencies directory: _internal/ ({file_count} files, {dir_count} subdirectories)") print(f"✅ 依赖目录: _internal/ ({file_count} 个文件, {dir_count} 个子目录)")
else: else:
print("ERROR: _internal directory does not exist") print(" _internal 目录不存在")
return False return False
# 检查关键DLL可选 # 检查关键DLL可选
if os.path.exists(dll_path): if os.path.exists(dll_path):
dll_size = os.path.getsize(dll_path) / 1024 # KB dll_size = os.path.getsize(dll_path) / 1024 # KB
print(f"QianNiu DLL: SaiNiuApi.dll ({dll_size:.1f} KB)") print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:.1f} KB)")
else: else:
print("WARNING: QianNiu DLL does not exist (may be normal)") print("⚠️ 千牛DLL不存在可能正常")
# 检查静态资源 # 检查静态资源
if os.path.exists(static_dir): if os.path.exists(static_dir):
icon_files = list(Path(static_dir).glob("*.png")) icon_files = list(Path(static_dir).glob("*.png"))
print(f"Static resources: static/ ({len(icon_files)} icon files)") print(f"✅ 静态资源: static/ ({len(icon_files)} 个图标文件)")
else: else:
print("WARNING: static directory does not exist") print("⚠️ static 目录不存在")
print(f"Directory {base_dir} verification passed") print(f"✅ 目录 {base_dir} 验证通过")
return True return True
def main(): def main():
"""主函数""" """主函数"""
print("Multi-Platform Customer Service GUI Quick Build Tool") print("🔥 多平台客服GUI快速打包工具")
print("=" * 60) print("=" * 60)
try: try:
# 检查依赖 # 检查依赖
print("Checking build dependencies...") print("🔍 检查打包依赖...")
try: try:
import subprocess import subprocess
result = subprocess.run(['pyinstaller', '--version'], result = subprocess.run(['pyinstaller', '--version'],
capture_output=True, text=True, check=False) capture_output=True, text=True, check=False)
if result.returncode == 0: if result.returncode == 0:
print(f"PyInstaller version: {result.stdout.strip()}") print(f"PyInstaller 版本: {result.stdout.strip()}")
else: else:
print("ERROR: PyInstaller not installed or unavailable") print(" PyInstaller 未安装或不可用")
print("TIP: Run: pip install pyinstaller") print("💡 请运行: pip install pyinstaller")
return False return False
except Exception as e: except Exception as e:
print(f"ERROR: Failed to check PyInstaller: {e}") print(f"❌ 检查PyInstaller时出错: {e}")
return False return False
# 清理 # 清理
print("\nCleaning phase started...") print("\n📍 开始清理阶段...")
clean_build() clean_build()
print("Cleaning phase completed") print("📍 清理阶段完成")
# 打包 # 打包
print("\nBuild phase started...") print("\n📍 开始打包阶段...")
if not build_with_command(): if not build_with_command():
print("ERROR: Build phase failed") print("❌ 打包阶段失败")
return False return False
print("Build phase completed") print("📍 打包阶段完成")
# 验证 # 验证
print("\nVerification phase started...") print("\n📍 开始验证阶段...")
if not verify_result(): if not verify_result():
print("ERROR: Verification phase failed") print("❌ 验证阶段失败")
return False return False
print("Verification phase completed") print("📍 验证阶段完成")
# 🔧 修复PyMiniRacer DLL
print("\nPyMiniRacer DLL fix phase started...")
try:
import fix_pyminiracer_dll
if fix_pyminiracer_dll.auto_fix_after_build():
print("PyMiniRacer DLL fix completed")
else:
print("WARNING: PyMiniRacer DLL fix failed - PDD platform may not work without Node.js")
except Exception as e:
print(f"WARNING: PyMiniRacer DLL fix error: {e}")
print(" PDD platform may need Node.js environment")
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("Build completed successfully!") print("🎉 打包完成!")
# 显示打包结果 # 智能显示可用路径
if os.path.exists("dist/MultiPlatformGUI"): if os.path.exists("dist/MultiPlatformGUI"):
print("Build result: dist/MultiPlatformGUI/") print("📁 打包结果: dist/MultiPlatformGUI/")
print("Run command: cd dist/MultiPlatformGUI && .\\main.exe") print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe")
print("Create installer: python installer/build_installer.py") print("📦 安装包构建: python installer/build_installer.py")
elif os.path.exists("dist/main"):
print("📁 打包结果: dist/main/")
print("🚀 运行方式: cd dist/main && .\\main.exe")
print("⚠️ 建议重命名: dist/main -> dist/MultiPlatformGUI")
print("=" * 60) print("=" * 60)
return True return True
except Exception as e: except Exception as e:
print(f"ERROR: Build failed: {e}") print(f"❌ 打包失败: {e}")
return False return False
if __name__ == "__main__": if __name__ == "__main__":
success = main() success = main()
if success: if success:
print("\nReady for testing!") print("\n✅ 可以开始测试了!")
else: else:
print("\nERROR: Please check error messages and retry") print("\n❌ 请检查错误信息并重试")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 199 KiB

View File

@@ -1,4 +1,4 @@
window = globalThis; window = globalThis;;
var CryptoJS = CryptoJS || (function (Math, undefined) { var CryptoJS = CryptoJS || (function (Math, undefined) {
var C = {}; var C = {};

View File

@@ -1,4 +1,4 @@
window = globalThis; window = globalThis;;
var CryptoJS = CryptoJS || (function (Math, undefined) { var CryptoJS = CryptoJS || (function (Math, undefined) {
var C = {}; var C = {};

View File

@@ -1,474 +0,0 @@
var CryptoJS = CryptoJS || (function (Math, undefined) {
var C = {};
var C_lib = C.lib = {};
var Base = C_lib.Base = (function () {
function F() {};
return {
extend: function (overrides) {
F.prototype = this;
var subtype = new F();
if (overrides) {
subtype.mixIn(overrides);
}
if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
subtype.init = function () {
subtype.$super.init.apply(this, arguments);
};
}
subtype.init.prototype = subtype;
subtype.$super = this;
return subtype;
}, create: function () {
var instance = this.extend();
instance.init.apply(instance, arguments);
return instance;
}, init: function () {}, mixIn: function (properties) {
for (var propertyName in properties) {
if (properties.hasOwnProperty(propertyName)) {
this[propertyName] = properties[propertyName];
}
}
if (properties.hasOwnProperty('toString')) {
this.toString = properties.toString;
}
}, clone: function () {
return this.init.prototype.extend(this);
}
};
}());
var WordArray = C_lib.WordArray = Base.extend({
init: function (words, sigBytes) {
words = this.words = words || [];
if (sigBytes != undefined) {
this.sigBytes = sigBytes;
} else {
this.sigBytes = words.length * 4;
}
}, toString: function (encoder) {
return (encoder || Hex).stringify(this);
}, concat: function (wordArray) {
var thisWords = this.words;
var thatWords = wordArray.words;
var thisSigBytes = this.sigBytes;
var thatSigBytes = wordArray.sigBytes;
this.clamp();
if (thisSigBytes % 4) {
for (var i = 0; i < thatSigBytes; i++) {
var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
}
} else if (thatWords.length > 0xffff) {
for (var i = 0; i < thatSigBytes; i += 4) {
thisWords[(thisSigBytes + i) >>> 2] = thatWords[i >>> 2];
}
} else {
thisWords.push.apply(thisWords, thatWords);
}
this.sigBytes += thatSigBytes;
return this;
}, clamp: function () {
var words = this.words;
var sigBytes = this.sigBytes;
words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
words.length = Math.ceil(sigBytes / 4);
}, clone: function () {
var clone = Base.clone.call(this);
clone.words = this.words.slice(0);
return clone;
}, random: function (nBytes) {
var words = [];
var r = (function (m_w) {
var m_w = m_w;
var m_z = 0x3ade68b1;
var mask = 0xffffffff;
return function () {
m_z = (0x9069 * (m_z & 0xFFFF) + (m_z >> 0x10)) & mask;
m_w = (0x4650 * (m_w & 0xFFFF) + (m_w >> 0x10)) & mask;
var result = ((m_z << 0x10) + m_w) & mask;
result /= 0x100000000;
result += 0.5;
return result * (Math.random() > .5 ? 1 : -1);
}
});
for (var i = 0, rcache; i < nBytes; i += 4) {
var _r = r((rcache || Math.random()) * 0x100000000);
rcache = _r() * 0x3ade67b7;
words.push((_r() * 0x100000000) | 0);
}
return new WordArray.init(words, nBytes);
}
});
var C_enc = C.enc = {};
var Hex = C_enc.Hex = {
stringify: function (wordArray) {
var words = wordArray.words;
var sigBytes = wordArray.sigBytes;
var hexChars = [];
for (var i = 0; i < sigBytes; i++) {
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
hexChars.push((bite >>> 4).toString(16));
hexChars.push((bite & 0x0f).toString(16));
}
return hexChars.join('');
}, parse: function (hexStr) {
var hexStrLength = hexStr.length;
var words = [];
for (var i = 0; i < hexStrLength; i += 2) {
words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
}
return new WordArray.init(words, hexStrLength / 2);
}
};
var Latin1 = C_enc.Latin1 = {
stringify: function (wordArray) {
var words = wordArray.words;
var sigBytes = wordArray.sigBytes;
var latin1Chars = [];
for (var i = 0; i < sigBytes; i++) {
var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
latin1Chars.push(String.fromCharCode(bite));
}
return latin1Chars.join('');
}, parse: function (latin1Str) {
var latin1StrLength = latin1Str.length;
var words = [];
for (var i = 0; i < latin1StrLength; i++) {
words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
}
return new WordArray.init(words, latin1StrLength);
}
};
var Utf8 = C_enc.Utf8 = {
stringify: function (wordArray) {
try {
return decodeURIComponent(escape(Latin1.stringify(wordArray)));
} catch (e) {
throw new Error('Malformed UTF-8 data');
}
}, parse: function (utf8Str) {
return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
}
};
var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
reset: function () {
this._data = new WordArray.init();
this._nDataBytes = 0;
}, _append: function (data) {
if (typeof data == 'string') {
data = Utf8.parse(data);
}
this._data.concat(data);
this._nDataBytes += data.sigBytes;
}, _process: function (doFlush) {
var data = this._data;
var dataWords = data.words;
var dataSigBytes = data.sigBytes;
var blockSize = this.blockSize;
var blockSizeBytes = blockSize * 4;
var nBlocksReady = dataSigBytes / blockSizeBytes;
if (doFlush) {
nBlocksReady = Math.ceil(nBlocksReady);
} else {
nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
}
var nWordsReady = nBlocksReady * blockSize;
var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);
if (nWordsReady) {
for (var offset = 0; offset < nWordsReady; offset += blockSize) {
this._doProcessBlock(dataWords, offset);
}
var processedWords = dataWords.splice(0, nWordsReady);
data.sigBytes -= nBytesReady;
}
return new WordArray.init(processedWords, nBytesReady);
}, clone: function () {
var clone = Base.clone.call(this);
clone._data = this._data.clone();
return clone;
}, _minBufferSize: 0
});
var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
cfg: Base.extend(),
init: function (cfg) {
this.cfg = this.cfg.extend(cfg);
this.reset();
}, reset: function () {
BufferedBlockAlgorithm.reset.call(this);
this._doReset();
}, update: function (messageUpdate) {
this._append(messageUpdate);
this._process();
return this;
}, finalize: function (messageUpdate) {
if (messageUpdate) {
this._append(messageUpdate);
}
var hash = this._doFinalize();
return hash;
}, blockSize: 512 / 32,
_createHelper: function (hasher) {
return function (message, cfg) {
return new hasher.init(cfg).finalize(message);
};
}, _createHmacHelper: function (hasher) {
return function (message, key) {
return new C_algo.HMAC.init(hasher, key).finalize(message);
};
}
});
var C_algo = C.algo = {};
return C;
}(Math));
(function () {
var C = CryptoJS;
var C_lib = C.lib;
var Base = C_lib.Base;
var C_enc = C.enc;
var Utf8 = C_enc.Utf8;
var C_algo = C.algo;
var HMAC = C_algo.HMAC = Base.extend({
init: function (hasher, key) {
hasher = this._hasher = new hasher.init();
if (typeof key == 'string') {
key = Utf8.parse(key);
}
var hasherBlockSize = hasher.blockSize;
var hasherBlockSizeBytes = hasherBlockSize * 4;
if (key.sigBytes > hasherBlockSizeBytes) {
key = hasher.finalize(key);
}
key.clamp();
var oKey = this._oKey = key.clone();
var iKey = this._iKey = key.clone();
var oKeyWords = oKey.words;
var iKeyWords = iKey.words;
for (var i = 0; i < hasherBlockSize; i++) {
oKeyWords[i] ^= 0x5c5c5c5c;
iKeyWords[i] ^= 0x36363636;
}
oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes;
this.reset();
}, reset: function () {
var hasher = this._hasher;
hasher.reset();
hasher.update(this._iKey);
}, update: function (messageUpdate) {
this._hasher.update(messageUpdate);
return this;
}, finalize: function (messageUpdate) {
var hasher = this._hasher;
var innerHash = hasher.finalize(messageUpdate);
hasher.reset();
var hmac = hasher.finalize(this._oKey.clone().concat(innerHash));
return hmac;
}
});
}());
(function (Math) {
var C = CryptoJS;
var C_lib = C.lib;
var WordArray = C_lib.WordArray;
var Hasher = C_lib.Hasher;
var C_algo = C.algo;
var H = [];
var K = [];
(function () {
function isPrime(n) {
var sqrtN = Math.sqrt(n);
for (var factor = 2; factor <= sqrtN; factor++) {
if (!(n % factor)) {
return false;
}
}
return true;
}
function getFractionalBits(n) {
return ((n - (n | 0)) * 0x100000000) | 0;
}
var n = 2;
var nPrime = 0;
while (nPrime < 64) {
if (isPrime(n)) {
if (nPrime < 8) {
H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
}
K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));
nPrime++;
}
n++;
}
}());
var W = [];
var SHA256 = C_algo.SHA256 = Hasher.extend({
_doReset: function () {
this._hash = new WordArray.init(H.slice(0));
}, _doProcessBlock: function (M, offset) {
var H = this._hash.words;
var a = H[0];
var b = H[1];
var c = H[2];
var d = H[3];
var e = H[4];
var f = H[5];
var g = H[6];
var h = H[7];
for (var i = 0; i < 64; i++) {
if (i < 16) {
W[i] = M[offset + i] | 0;
} else {
var gamma0x = W[i - 15];
var gamma0 = ((gamma0x << 25) | (gamma0x >>> 7)) ^ ((gamma0x << 14) | (gamma0x >>> 18)) ^ (gamma0x >>> 3);
var gamma1x = W[i - 2];
var gamma1 = ((gamma1x << 15) | (gamma1x >>> 17)) ^ ((gamma1x << 13) | (gamma1x >>> 19)) ^ (gamma1x >>> 10);
W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
}
var ch = (e & f) ^ (~e & g);
var maj = (a & b) ^ (a & c) ^ (b & c);
var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7) | (e >>> 25));
var t1 = h + sigma1 + ch + K[i] + W[i];
var t2 = sigma0 + maj;
h = g;
g = f;
f = e;
e = (d + t1) | 0;
d = c;
c = b;
b = a;
a = (t1 + t2) | 0;
}
H[0] = (H[0] + a) | 0;
H[1] = (H[1] + b) | 0;
H[2] = (H[2] + c) | 0;
H[3] = (H[3] + d) | 0;
H[4] = (H[4] + e) | 0;
H[5] = (H[5] + f) | 0;
H[6] = (H[6] + g) | 0;
H[7] = (H[7] + h) | 0;
}, _doFinalize: function () {
var data = this._data;
var dataWords = data.words;
var nBitsTotal = this._nDataBytes * 8;
var nBitsLeft = data.sigBytes * 8;
dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
data.sigBytes = dataWords.length * 4;
this._process();
return this._hash;
}, clone: function () {
var clone = Hasher.clone.call(this);
clone._hash = this._hash.clone();
return clone;
}
});
C.SHA256 = Hasher._createHelper(SHA256);
C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
}(Math));
function u(e) {
try {
return encodeURIComponent(e).replace(/[^A-Za-z0-9_.~\-%]+/g, escape).replace(/[*]/g, (function(e) {
return "%".concat(e.charCodeAt(0).toString(16).toUpperCase())
}
))
} catch (e) {
return ""
}
}
function d(e) {
return Object.keys(e).sort().map((function(t) {
var n = e[t];
if (null != n) {
var r = u(t);
if (r)
return Array.isArray(n) ? "".concat(r, "=").concat(n.map(u).sort().join("&".concat(r, "="))) : "".concat(r, "=").concat(u(n))
}
}
)).filter((function(e) {
return e
}
)).join("&")
};
var glo = ""
function canonicalString(method, params, x_amz_date, x_amz_security_token, data){
var date = "x-amz-date:" + x_amz_date;
var token = "x-amz-security-token:" + x_amz_security_token;
if (data){
var sha512 = CryptoJS.SHA256(JSON.stringify(data).replace(" ", "")).toString()
glo = sha512;
var content = "x-amz-content-sha256:" + sha512;
var sss = content + "\n" + date + "\n" + token
var ttt = JSON.stringify(data).replace(" ", "");
var zzz = "x-amz-content-sha256;x-amz-date;x-amz-security-token"
}else{
var sss = date + "\n" + token
var zzz = "x-amz-date;x-amz-security-token"
var ttt = ""
}
var e = []
, t = "/";
return e.push(method),
e.push(t),
e.push(d(params) || ""),
e.push("".concat((sss), "\n")),
e.push(zzz),
e.push(CryptoJS.SHA256(ttt)),
e.join("\n")
}
function stringToSign(method, type, key, params, x_amz_date, x_amz_security_token, data){
var t = [];
return t.push("AWS4-HMAC-SHA256"),
t.push(x_amz_date),
t.push(key[0] + "/cn-north-1/" + type + "/aws4_request"),
t.push(CryptoJS.SHA256(canonicalString(method, params, x_amz_date, x_amz_security_token, data))),
t.join("\n")
}
function signature(method, type, secret_access_key, x_amz_date, x_amz_security_token, params, data){
var key = x_amz_date.split("T");
var o = CryptoJS.HmacSHA256(key[0], secret_access_key);
var i = CryptoJS.HmacSHA256("cn-north-1", o);
var s = CryptoJS.HmacSHA256(type, i);
var n = CryptoJS.HmacSHA256("aws4_request", s);
return CryptoJS.HmacSHA256(stringToSign(method, type, key, params, x_amz_date, x_amz_security_token, data), n).toString();
}
// console.log(signature("POST", "vod", "AWS4D2OsX7tT/swv6JehKT62QmrUjrnR+KYH63SNma7buX3P46UoZqcHH+JHvykozVlr", "20251024T033313Z", "STS2eyJMVEFjY2Vzc0tleUlEIjoiQUtMVE5tRmhZVGhsWTJJM1lqbGpORGc0WkRsbE1UTXlZelJoWkRkaU1EaGlaV1EiLCJBY2Nlc3NLZXlJRCI6IkFLVFBOVEF6TVRSalpqSTNZV00wTkRjd1lUazBNak14WVRrelpEUXhPRGxsWXpjIiwiU2lnbmVkU2VjcmV0QWNjZXNzS2V5IjoiTWcrU1laeXlCajNyc05UL0dKaFlHYTcxUEloWURvMms1Z1J4Q2QyQk5nUDV6NXdZaWpyeEhwUmYzT1dKNW9PRWNwK3FCTVNGNXhsaG40Y3hhOHhDZmkrQWgvZU0wWS95QVhqbGhMZWJmYU09IiwiRXhwaXJlZFRpbWUiOjE3NjEyODAxNDksIlBvbGljeVN0cmluZyI6IntcIlN0YXRlbWVudFwiOlt7XCJFZmZlY3RcIjpcIkFsbG93XCIsXCJBY3Rpb25cIjpbXCJ2b2Q6QXBwbHlVcGxvYWRcIixcInZvZDpBcHBseVVwbG9hZElubmVyXCIsXCJ2b2Q6Q29tbWl0VXBsb2FkXCIsXCJ2b2Q6Q29tbWl0VXBsb2FkSW5uZXJcIixcInZvZDpHZXRVcGxvYWRDYW5kaWRhdGVzXCIsXCJJbWFnZVg6QXBwbHlJbWFnZVVwbG9hZFwiLFwiSW1hZ2VYOkNvbW1pdEltYWdlVXBsb2FkXCIsXCJJbWFnZVg6QXBwbHlVcGxvYWRJbWFnZUZpbGVcIixcIkltYWdlWDpDb21taXRVcGxvYWRJbWFnZUZpbGVcIl0sXCJSZXNvdXJjZVwiOltcIipcIl0sXCJDb25kaXRpb25cIjpcIntcXFwiUFNNXFxcIjpcXFwiY21wLmVjb20ucGlnZW9uX2FwaVxcXCJ9XCJ9XX0iLCJTaWduYXR1cmUiOiIxOTBmNzkxZTU2NTk0MzgwOGIzNTllYTJhOWI4OGExOTAyZDE5MTEwODBhMmNmYmVhMTQwZmVmYWNhYTViNGU5In0=", {
// "Action": "CommitUploadInner",
// "Version": "2020-11-19",
// "SpaceName": "pigeon-video"
// }, {
// "SessionKey": "eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyNzY3OTI3Nzk2NDYzMTUiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlWSnJSM2xRUkVNaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VG1wTmVFOVVTWE5KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2V1hwTlZHdDVURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUbENZbTFhVDFKWFZrWlJibXg0WWxab1EwOUZXbTVoVmtGNlVrZHNTVk51UW5CbFdGcElZakJHVlZaVVVYZGhSVTB5U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM0eVh6UlNVakpXYkROeWN6SmFUV3B5ZGt4eFEyTmxkVE54WTI1RWFGa3RPWGhmU1RkTGFtRXdUVE5KSW4wPTo0ZTU0MTNhZTY2OTFjM2U2ZTU2ODc0NGEyYmQzNzZiMDFiODY0YjI4OGRhODg0MzY4ZDA1YzJiMTE4ZWE1MGM4IiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L29BbmZORWVFQnlxbVhCOEZnaVAzRGlISnBpeXZHb0FUVTQwaEM2IiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0ZjZ1N29nNjVvcGJ1YWk1cTAifQ==",
// "Functions": [
// {
// "name": "GetMeta"
// },
// {
// "name": "Snapshot",
// "input": {
// "SnapshotTime": 0
// }
// }
// ]
// }))
//console.log(d({"SessionKey":"eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyMTU2Nzc4NDc0NTE1ODMiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlVRlhkVmx1VDNBaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VFVSSmQwNTZZM05KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2UVhsTlJHTXpURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUa3pVMVZTV1U5V1JtMVNTR2hFV214V2RsbFlXblZYYlhjelVsZGFRbG93TURSU01GWlpVV3BTZGxFd1RrNVZhMFp1U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM1SVJEWktTRGxYUzJoVFh6Uk9TRWhyY0VGSVQzZHpPV3hrTmxjNGNXeGFSVGcwTkVKTVFtOVZXa3BySW4wPTpjM2I3YjRmY2U1ZjFlMDQ4NWNhYTFhMjNmNTE1ZTM0NjUxN2QxNjI3YzQ3YzAyNDdlMjlmNTE4ODcxYjlhYzNhIiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L293SURYOVFmRHhDZlVvYXZuWmw3RWZBZ004R0VYQjRvQ0NNUkFnIiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0MDlmZm9nNjVxczRwMWdsMGcifQ==","Functions":[{"name":"GetMeta"},{"name":"Snapshot","input":{"SnapshotTime":0}}]}))
// console.log(CryptoJS.SHA256(JSON.stringify({"SessionKey":"eyJhY2NvdW50VHlwZSI6InNwYWNlIiwiZW5jcnlwdEtleSI6IiIsImVuY3J5cHRNb2RlIjoiIiwiZXh0cmEiOiJlZGdlX25vZGU9bGZcdTAwMjZmaWxlX3NpemU9NDY1MDQ5LjAwMDAwMFx1MDAyNmhvc3Q9dG9zLWQtY3QtbGYuc25zc2RrLmNvbVx1MDAyNnByb3ZpbmNlPUd1YW5nZG9uZ1x1MDAyNnJlZ2lvbj1DTlx1MDAyNnN0cmF0ZWd5PWxvbmdfbWVtb3J5X2ZpbHRlcl92Mlx1MDAyNnRlZGR5X2VkZ2VfaG9zdD10b3MtZC1jdC1sZi5zbnNzZGsuY29tXHUwMDI2dXBsb2FkX21vZGU9c2VyaWFsXHUwMDI2dXNlcl9pcD0xNC4yMy45MS4yMzBcdTAwMjZ2aWRjPWxxXHUwMDI2dnRzPTE3NjEyMTU2Nzc4NDc0NTE1ODMiLCJmaWxlVHlwZSI6InZpZGVvIiwibWVkaWFUeXBlIjoiIiwibWV0YUNvbmZpZyI6IntcImFjY3VyYXRlXCI6ZmFsc2UsXCJuZWVkX3Bvc3RlclwiOnRydWUsXCJza2lwX2JsYWNrX2RldGVjdFwiOmZhbHNlLFwid2hpdGVfZGV0ZWN0XCI6ZmFsc2UsXCJzZXRfY29udGVudF90eXBlXCI6ZmFsc2UsXCJmZl9tZXRhZGF0YVwiOmZhbHNlLFwibmVlZF9tZDVcIjpmYWxzZSxcIm5lZWRfc2hhMjU2XCI6ZmFsc2UsXCJuZWVkX3JlZHVuZGFudF9oYXNoXCI6ZmFsc2UsXCJzc19hZnRlcl9pbnB1dFwiOmZhbHNlLFwibmVlZF9leGFjdF9mb3JtYXRcIjpmYWxzZSxcImttc19lbmNyeXB0X2tleVwiOlwiXCIsXCJlbmNyeXB0X3Bvc3Rlcl9rZXlcIjpcIlwifSIsInNjZW5lIjoiIiwidG9rZW4iOiJleUpvYjNOMElqb2lkRzl6TFdRdFkzUXRiR1l1YzI1emMyUnJMbU52YlNJc0ltNXZibU5sSWpvaVlVRlhkVmx1VDNBaUxDSjFjR3h2WVdSZmMybG5iaUk2SWxOd1lXTmxTMlY1TDNCcFoyVnZiaTEyYVdSbGJ5OHdMenAyWlhKemFXOXVPbll5T21WNVNtaGlSMk5wVDJsS1NWVjZTVEZPYVVselNXNVNOV05EU1RaSmEzQllWa05LT1M1bGVVcHNaVWhCYVU5cVJUTk9ha1Y2VFVSSmQwNTZZM05KYms1d1dqSTFhR1JJVm5sYVZXeDFXbTA0YVU5dWMybFpWMDVxV2xoT2VsTXlWalZKYW05cFdtMUdjbHBXT1doWk1rNXNZek5PWm1FeVZqVkphWGRwV1c1V2FtRXlWakJKYW05cFpFYzVla3hYVG5WTVdGbDBXWHBOZUUxVVJURkphWGRwV2xob2QyRllTbXhKYW05NFRucFplRTE2UVhsTlJHTXpURU5LYldGWGVHeFRWelZ0WWpOTmFVOXNkRGRKYlRsd1drVjBiR1ZUU1RaSmJUa3pVMVZTV1U5V1JtMVNTR2hFV214V2RsbFlXblZYYlhjelVsZGFRbG93TURSU01GWlpVV3BTZGxFd1RrNVZhMFp1U1dsM2FWcHRiSE5hVmxJMVkwZFZhVTlwU1hkSmJqRmtURU5LYkdWSVVubFpVMGsyWlhsS2FGa3lUblprVnpVd1dETkNlV0l5VWpGWk0xRnBUMmxLTW1JeVVXbE1RMHBwWWtjNWFtRXhPWFJpTWxKc1NXcHZhVWxwZDJsWk1qbDFaRWRXZFdSR09UQmxXRUpzV0RKS2MySXlUbkpKYW05cFpURjNhV0pYYkhSYVZqbDNXVE5TWTBscWIzZE1SbmRwWWxjNWExcFdkMmxQYWtGeldFTktkR0ZYTVd4WU1uaHdZek5TWTBscWNIVmtWM2h6VEVaM2FWa3lPWFZhYlhod1dUTlNabGx0ZUhaWk1uUmpTV3B3YlZsWGVIcGFXREJwVEVOS2JHSnRUbmxsV0VJd1dESkdjMW95T0dsUGFVbHBURU5LYkdKdFRubGxXRUl3V0RKMGJHVlRTVFpKYVVselNXNU9kMWxYVG14SmFtOXBZMGRzYmxwWE9YVk1XRnB3V2tkV2RrbHVNVGxtVVM1SVJEWktTRGxYUzJoVFh6Uk9TRWhyY0VGSVQzZHpPV3hrTmxjNGNXeGFSVGcwTkVKTVFtOVZXa3BySW4wPTpjM2I3YjRmY2U1ZjFlMDQ4NWNhYTFhMjNmNTE1ZTM0NjUxN2QxNjI3YzQ3YzAyNDdlMjlmNTE4ODcxYjlhYzNhIiwidG9wRG9tYWluIjoib3Blbi5ieXRlZGFuY2VhcGkuY29tIiwidXJpIjoidG9zLWNuLXYtYzMxMTE1L293SURYOVFmRHhDZlVvYXZuWmw3RWZBZ004R0VYQjRvQ0NNUkFnIiwidXNlSXNwU2NoZWR1bGluZyI6ImZhbHNlIiwidmlkIjoidjBkZDM4ZzEwMDAwZDN0MDlmZm9nNjVxczRwMWdsMGcifQ==","Functions":[{"name":"GetMeta"},{"name":"Snapshot","input":{"SnapshotTime":0}}]}).replace(" ")).toString())

View File

@@ -1,518 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
现代化圆形进度条更新对话框
展示完整更新流程:下载 → UAC授权 → 安装 → 重启
"""
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QLabel,
QPushButton, QHBoxLayout, QWidget)
from PyQt5.QtCore import Qt, QTimer, QPropertyAnimation, QEasingCurve, pyqtProperty
from PyQt5.QtGui import QFont, QPainter, QColor, QPen, QLinearGradient, QConicalGradient
from PyQt5.QtCore import QRectF, QPointF
import math
class CircularProgressBar(QWidget):
"""
自定义圆形进度条组件
特性:
- 顺时针旋转动画
- 渐变色圆环
- 中心显示百分比
- 支持平滑过渡
"""
def __init__(self, parent=None):
super().__init__(parent)
self._value = 0 # 当前进度值0-100
self._max_value = 100
self._text = "0%"
self.setMinimumSize(200, 200)
# 动画效果
self.animation = QPropertyAnimation(self, b"value")
self.animation.setDuration(500) # 500ms过渡动画
self.animation.setEasingCurve(QEasingCurve.OutCubic)
@pyqtProperty(int)
def value(self):
return self._value
@value.setter
def value(self, val):
self._value = val
self.update() # 触发重绘
def setValue(self, val):
"""设置进度值(带动画)"""
if val != self._value:
self.animation.stop()
self.animation.setStartValue(self._value)
self.animation.setEndValue(val)
self.animation.start()
def setText(self, text):
"""设置中心文本"""
self._text = text
self.update()
def paintEvent(self, event):
"""绘制圆形进度条"""
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing) # 抗锯齿
# 计算中心和半径
width = self.width()
height = self.height()
size = min(width, height)
center = QPointF(width / 2, height / 2)
radius = size / 2 - 15 # 留出边距
# 绘制背景圆环(灰色)
pen = QPen()
pen.setWidth(12)
pen.setColor(QColor(230, 230, 230))
pen.setCapStyle(Qt.RoundCap)
painter.setPen(pen)
rect = QRectF(center.x() - radius, center.y() - radius,
radius * 2, radius * 2)
painter.drawArc(rect, 90 * 16, -360 * 16) # 绘制完整圆环
# 绘制进度圆环(渐变色)
if self._value > 0:
# 创建圆锥渐变(顺时针旋转效果)
gradient = QConicalGradient(center, 90) # 从顶部开始
gradient.setColorAt(0.0, QColor(66, 133, 244)) # 蓝色
gradient.setColorAt(0.5, QColor(25, 118, 210)) # 深蓝
gradient.setColorAt(1.0, QColor(21, 101, 192)) # 更深蓝
pen.setBrush(gradient)
pen.setColor(QColor(66, 133, 244))
painter.setPen(pen)
# 计算进度角度(顺时针,从顶部开始)
span_angle = int(-(self._value / self._max_value) * 360 * 16)
painter.drawArc(rect, 90 * 16, span_angle)
# 绘制中心文本(百分比)
painter.setPen(QColor(44, 62, 80))
font = QFont('Microsoft YaHei', 28, QFont.Bold)
painter.setFont(font)
painter.drawText(rect, Qt.AlignCenter, self._text)
class UpdateProgressDialog(QDialog):
"""
现代化更新进度对话框
更新流程阶段:
1. 下载安装包 (0-50%)
2. 等待UAC授权 (50-60%)
3. 执行安装 (60-90%)
4. 等待程序重启 (90-100%)
"""
# 流程阶段定义
STAGE_DOWNLOAD = 0 # 下载阶段 (0-50%)
STAGE_UAC = 1 # UAC授权阶段 (50-60%)
STAGE_INSTALL = 2 # 安装阶段 (60-90%)
STAGE_RESTART = 3 # 重启阶段 (90-100%)
def __init__(self, version, parent=None):
super().__init__(parent)
self.version = version
self.downloader = None
self.current_stage = self.STAGE_DOWNLOAD
self.download_progress = 0 # 下载进度0-100
self.restart_progress = 0 # 重启阶段进度0-100用于外部检查
self.init_ui()
def init_ui(self):
"""初始化UI"""
self.setWindowTitle("正在更新")
self.setFixedSize(500, 600)
# 禁止关闭按钮(只能通过取消按钮关闭)
self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint | Qt.CustomizeWindowHint)
# 主布局
layout = QVBoxLayout()
layout.setSpacing(20)
layout.setContentsMargins(30, 30, 30, 30)
# ============ 标题区域 ============
self.title_label = QLabel(f"正在更新到 v{self.version}")
self.title_label.setFont(QFont('Microsoft YaHei', 16, QFont.Bold))
self.title_label.setAlignment(Qt.AlignCenter)
self.title_label.setStyleSheet("color: #2c3e50; margin-bottom: 10px;")
layout.addWidget(self.title_label)
# ============ 圆形进度条 ============
self.circular_progress = CircularProgressBar()
layout.addWidget(self.circular_progress, alignment=Qt.AlignCenter)
# ============ 阶段提示 ============
self.stage_label = QLabel("📥 正在下载安装包...")
self.stage_label.setFont(QFont('Microsoft YaHei', 12))
self.stage_label.setAlignment(Qt.AlignCenter)
self.stage_label.setStyleSheet("color: #34495e; margin-top: 10px;")
layout.addWidget(self.stage_label)
# ============ 详细状态 ============
self.status_label = QLabel("准备开始下载...")
self.status_label.setAlignment(Qt.AlignCenter)
self.status_label.setStyleSheet("color: #7f8c8d; font-size: 11px;")
self.status_label.setMinimumHeight(25)
layout.addWidget(self.status_label)
# ============ 重试信息(默认隐藏)============
self.retry_label = QLabel("")
self.retry_label.setAlignment(Qt.AlignCenter)
self.retry_label.setStyleSheet("color: #ff9800; font-size: 11px; font-weight: bold;")
self.retry_label.setMinimumHeight(25)
self.retry_label.hide()
layout.addWidget(self.retry_label)
# ============ 按钮区域 ============
button_layout = QHBoxLayout()
button_layout.addStretch()
self.cancel_button = QPushButton("取消更新")
self.cancel_button.setFixedWidth(120)
self.cancel_button.setFixedHeight(36)
self.cancel_button.clicked.connect(self.cancel_download)
button_layout.addWidget(self.cancel_button)
button_layout.addStretch()
layout.addLayout(button_layout)
self.setLayout(layout)
# 应用样式
self.apply_styles()
def apply_styles(self):
"""应用现代化样式"""
self.setStyleSheet("""
QDialog {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #f8fafb,
stop:0.5 #f1f4f6,
stop:1 #e8ecef
);
}
QLabel {
background: transparent;
}
QPushButton {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #f5f5f5,
stop:1 #e0e0e0
);
border: 2px solid #ccc;
border-radius: 8px;
padding: 8px 20px;
font-size: 12px;
font-weight: bold;
font-family: 'Microsoft YaHei';
color: #555;
}
QPushButton:hover {
background: qlineargradient(
x1:0, y1:0, x2:0, y2:1,
stop:0 #e8e8e8,
stop:1 #d0d0d0
);
border: 2px solid #999;
}
QPushButton:pressed {
background: #d0d0d0;
}
QPushButton:disabled {
background: #f0f0f0;
color: #aaa;
border: 2px solid #ddd;
}
""")
def update_progress(self, downloaded, total):
"""
更新下载进度阶段10-50%
Args:
downloaded: 已下载字节数
total: 总字节数
"""
if self.current_stage != self.STAGE_DOWNLOAD:
return # 不在下载阶段,忽略
if total > 0:
# 计算下载百分比0-100
download_percent = int((downloaded / total) * 100)
self.download_progress = download_percent
# 映射到总进度的0-50%
overall_progress = int(download_percent * 0.5)
# 更新圆形进度条
self.circular_progress.setValue(overall_progress)
self.circular_progress.setText(f"{overall_progress}%")
# 转换为MB显示
downloaded_mb = downloaded / (1024 * 1024)
total_mb = total / (1024 * 1024)
# 更新状态文本
self.status_label.setText(f"已下载 {downloaded_mb:.1f} MB / {total_mb:.1f} MB")
else:
# 无法获取总大小时显示不确定状态
self.status_label.setText("正在下载,请稍候...")
def show_retry_info(self, current_retry, max_retries):
"""
显示重试信息
Args:
current_retry: 当前重试次数
max_retries: 最大重试次数
"""
self.retry_label.setText(f"⚠️ 下载失败,正在重试... ({current_retry}/{max_retries})")
self.retry_label.show()
# 重置下载进度
self.download_progress = 0
self.circular_progress.setValue(0)
self.circular_progress.setText("0%")
self.status_label.setText("正在重试连接...")
# 更新标题
self.title_label.setText(f"正在重试下载 v{self.version}")
def download_finished(self, file_path):
"""
下载完成进入UAC授权阶段阶段250-60%
Args:
file_path: 下载的文件路径
"""
self.current_stage = self.STAGE_UAC
# 更新进度到50%
self.circular_progress.setValue(50)
self.circular_progress.setText("50%")
# 更新阶段提示
self.stage_label.setText("🔐 等待管理员授权...")
self.title_label.setText(f"✅ 下载完成")
self.status_label.setText("请在UAC窗口点击「允许」以继续...")
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
# 禁用取消按钮
self.cancel_button.setEnabled(False)
self.retry_label.hide()
# 启动UAC等待动画50% → 60%
self.start_uac_waiting_animation()
def start_uac_waiting_animation(self):
"""启动UAC等待动画进度条在50-60%之间缓慢移动)"""
self.uac_timer = QTimer(self)
self.uac_progress = 50
def update_uac_progress():
self.uac_progress += 0.5 # 每次增加0.5%
if self.uac_progress <= 60:
self.circular_progress.setValue(int(self.uac_progress))
self.circular_progress.setText(f"{int(self.uac_progress)}%")
else:
# 超过60%停止(等待用户授权)
self.uac_timer.stop()
self.uac_timer.timeout.connect(update_uac_progress)
self.uac_timer.start(500) # 每500ms更新一次
def uac_authorized(self):
"""
用户已授权UAC进入安装阶段阶段360-90%
"""
# 停止UAC等待动画
if hasattr(self, 'uac_timer'):
self.uac_timer.stop()
self.current_stage = self.STAGE_INSTALL
# 更新进度到60%
self.circular_progress.setValue(60)
self.circular_progress.setText("60%")
# 更新阶段提示
self.stage_label.setText("⚙️ 正在安装新版本...")
self.title_label.setText("正在安装")
self.status_label.setText("正在替换程序文件,请稍候...")
self.status_label.setStyleSheet("color: #007bff; font-size: 11px; font-weight: bold;")
# 启动安装进度动画60% → 90%
self.start_install_animation()
def start_install_animation(self):
"""启动安装进度动画60% → 90%"""
self.install_timer = QTimer(self)
self.install_progress = 60
def update_install_progress():
self.install_progress += 1 # 每次增加1%
if self.install_progress <= 90:
self.circular_progress.setValue(self.install_progress)
self.circular_progress.setText(f"{self.install_progress}%")
else:
# 安装完成,进入重启阶段
self.install_timer.stop()
self.prepare_restart()
self.install_timer.timeout.connect(update_install_progress)
self.install_timer.start(800) # 每800ms增加1%共24秒
def prepare_restart(self):
"""
准备重启阶段490-100%
"""
self.current_stage = self.STAGE_RESTART
# 更新进度到90%
self.circular_progress.setValue(90)
self.circular_progress.setText("90%")
# 更新阶段提示
self.stage_label.setText("🚀 准备启动新版本...")
self.title_label.setText("即将完成")
self.status_label.setText("程序即将重启,请稍候...")
self.status_label.setStyleSheet("color: #28a745; font-size: 11px; font-weight: bold;")
# 启动重启倒计时动画90% → 100%
self.start_restart_animation()
def start_restart_animation(self):
"""启动重启倒计时动画90% → 100%"""
self.restart_timer = QTimer(self)
self.restart_progress = 90
def update_restart_progress():
self.restart_progress += 2 # 每次增加2%
if self.restart_progress <= 100:
self.circular_progress.setValue(self.restart_progress)
self.circular_progress.setText(f"{self.restart_progress}%")
if self.restart_progress == 100:
# 🔥 到达100%,更新提示但不自动关闭
self.status_label.setText("✅ 安装完成!等待启动确认...")
self.stage_label.setText("🎉 更新成功!")
self.restart_timer.stop() # 停止定时器
else:
self.restart_timer.stop()
self.restart_timer.timeout.connect(update_restart_progress)
self.restart_timer.start(500) # 每500ms增加2%共2.5秒)
def download_error(self, error_msg):
"""
下载失败
Args:
error_msg: 错误信息
"""
self.title_label.setText(f"❌ 下载失败")
# 错误信息截断(避免太长)
if len(error_msg) > 80:
display_msg = error_msg[:80] + "..."
else:
display_msg = error_msg
self.stage_label.setText("⚠️ 更新失败")
self.status_label.setText(display_msg)
self.status_label.setStyleSheet("color: #dc3545; font-size: 11px; font-weight: bold;")
self.cancel_button.setText("关闭")
self.cancel_button.setEnabled(True)
self.retry_label.hide()
def cancel_download(self):
"""取消下载"""
if self.current_stage == self.STAGE_DOWNLOAD and self.downloader and self.downloader.isRunning():
# 下载阶段可以取消
from PyQt5.QtWidgets import QMessageBox
reply = QMessageBox.question(
self,
"确认取消",
"确定要取消更新吗?",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.No
)
if reply == QMessageBox.Yes:
self.downloader.cancel()
self.downloader.wait()
self.status_label.setText("更新已取消")
self.reject() # 会自动清理定时器
else:
# 其他阶段或下载已完成,直接关闭
self.reject() # 会自动清理定时器
def cleanup_timers(self):
"""清理所有定时器"""
if hasattr(self, 'uac_timer'):
self.uac_timer.stop()
if hasattr(self, 'install_timer'):
self.install_timer.stop()
if hasattr(self, 'restart_timer'):
self.restart_timer.stop()
def accept(self):
"""对话框接受时清理资源"""
self.cleanup_timers()
super().accept()
def reject(self):
"""对话框拒绝时清理资源"""
self.cleanup_timers()
super().reject()
if __name__ == "__main__":
# 测试代码
import sys
from PyQt5.QtWidgets import QApplication
app = QApplication(sys.argv)
# 创建测试对话框
dialog = UpdateProgressDialog("1.5.55")
dialog.show()
# 模拟完整更新流程
def simulate_update():
# 阶段1模拟下载 (0-50%)
def simulate_download():
total_size = 100 * 1024 * 1024 # 100MB
for i in range(0, 101, 5):
downloaded = int(i * total_size / 100)
dialog.update_progress(downloaded, total_size)
QApplication.processEvents()
QTimer.singleShot(100, lambda: None) # 延迟
# 下载完成
QTimer.singleShot(2000, lambda: dialog.download_finished("/tmp/test.exe"))
# 阶段2模拟UAC授权 (50-60%)
QTimer.singleShot(5000, lambda: dialog.uac_authorized())
simulate_download()
QTimer.singleShot(500, simulate_update)
sys.exit(app.exec_())

View File

@@ -1,216 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
版本检查器
定期检查后端是否有新版本并通知GUI更新
"""
import threading
import time
import logging
from typing import Callable, Optional
logger = logging.getLogger(__name__)
class VersionChecker:
"""
版本检查器
定期向后端发送版本检查请求,如果有新版本则触发更新回调
"""
def __init__(self, backend_client, update_callback: Optional[Callable] = None):
"""
初始化版本检查器
Args:
backend_client: 后端WebSocket客户端实例
update_callback: 发现新版本时的回调函数 callback(latest_version, download_url)
"""
self.backend_client = backend_client
self.update_callback = update_callback
self.is_running = False
self.check_interval = 600 # 10分钟检查一次
self.thread = None
# 从config获取当前版本
try:
from config import APP_VERSION
self.current_version = APP_VERSION
logger.info(f"✅ 版本检查器初始化成功,当前版本: v{self.current_version}")
except ImportError:
self.current_version = "1.0.0"
logger.warning("⚠️ 无法从config读取APP_VERSION使用默认版本: 1.0.0")
def start(self):
"""启动版本检查线程"""
if self.is_running:
logger.warning("版本检查器已在运行中")
return
self.is_running = True
self.thread = threading.Thread(target=self._check_loop, daemon=True)
self.thread.start()
logger.info(f"✅ 版本检查器已启动,每{self.check_interval}秒检查一次更新")
def stop(self):
"""停止版本检查"""
self.is_running = False
if self.thread:
self.thread.join(timeout=2)
logger.info("版本检查器已停止")
def _check_loop(self):
"""版本检查循环"""
# 等待5秒后首次检查避免启动时立即检查
time.sleep(5)
while self.is_running:
try:
self._perform_version_check()
except Exception as e:
logger.error(f"版本检查失败: {e}")
# 等待下次检查
time.sleep(self.check_interval)
def _perform_version_check(self):
"""执行版本检查"""
try:
if not self.backend_client or not self.backend_client.is_connected:
logger.debug("后端未连接,跳过版本检查")
return
# 向后端发送版本检查请求
message = {
'type': 'version_check',
'current_version': self.current_version,
'client_type': 'gui'
}
logger.debug(f"发送版本检查请求: v{self.current_version}")
self.backend_client.send_message(message)
except Exception as e:
logger.error(f"发送版本检查请求失败: {e}")
def handle_version_response(self, response: dict):
"""
处理后端返回的版本检查响应
Args:
response: 后端响应消息
{
"type": "version_response",
"has_update": true,
"latest_version": "1.5.0",
"current_version": "1.4.7",
"download_url": "https://example.com/download/v1.5.0",
"update_content": "修复了若干Bug"
}
"""
try:
has_update = response.get('has_update', None) # 改为 None便于后续判断
latest_version = response.get('latest_version', '')
current_version = response.get('current_version', self.current_version)
download_url = response.get('download_url', '')
update_content = response.get('update_content', '')
logger.info(f"📡 收到版本检查响应: 当前v{current_version}, 最新v{latest_version}")
# 如果后端没有返回 has_update 字段,自动通过版本比较判断
if has_update is None and latest_version:
# 自动比较版本号
compare_result = self.compare_versions(latest_version, current_version)
has_update = compare_result > 0 # latest > current 表示有更新
logger.info(f"🔍 后端未返回has_update字段通过版本比较判断: {has_update}")
if has_update and latest_version:
logger.info(f"🔔 发现新版本: v{latest_version}")
if update_content:
logger.info(f"📝 更新内容: {update_content[:100]}")
# 触发更新回调
if self.update_callback:
try:
self.update_callback(latest_version, download_url)
except Exception as e:
logger.error(f"执行更新回调失败: {e}")
else:
logger.debug(f"✅ 当前已是最新版本: v{current_version}")
except Exception as e:
logger.error(f"处理版本响应失败: {e}")
def compare_versions(self, version1: str, version2: str) -> int:
"""
比较两个版本号
Args:
version1: 版本1 (如 "1.4.7")
version2: 版本2 (如 "1.5.0")
Returns:
1: version1 > version2
0: version1 == version2
-1: version1 < version2
"""
try:
v1_parts = [int(x) for x in version1.split('.')]
v2_parts = [int(x) for x in version2.split('.')]
# 补齐长度
max_len = max(len(v1_parts), len(v2_parts))
v1_parts.extend([0] * (max_len - len(v1_parts)))
v2_parts.extend([0] * (max_len - len(v2_parts)))
# 逐位比较
for v1, v2 in zip(v1_parts, v2_parts):
if v1 > v2:
return 1
elif v1 < v2:
return -1
return 0
except Exception as e:
logger.error(f"版本比较失败: {e}")
return 0
# 使用示例
if __name__ == '__main__':
# 配置日志
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(levelname)s] %(message)s'
)
# 模拟使用
class MockBackendClient:
is_connected = True
def send_message(self, msg):
print(f"发送消息: {msg}")
def on_update_available(version, url):
print(f"🔔 发现新版本: {version}, 下载地址: {url}")
client = MockBackendClient()
checker = VersionChecker(client, on_update_available)
checker.start()
# 模拟接收后端响应
time.sleep(2)
response = {
"type": "version_response",
"has_update": True,
"latest_version": "1.5.0",
"current_version": "1.4.7",
"download_url": "https://example.com/download",
"update_content": "修复了若干Bug"
}
checker.handle_version_response(response)
time.sleep(2)
checker.stop()

View File

@@ -1,802 +1,92 @@
[ [
{ {
"version": "1.5.71", "version": "1.4.8",
"update_type": "patch", "update_type": "patch",
"content": "[skip ci] Update version to v1.5.70", "content": "强制空提交",
"author": "Gitea Actions Bot", "author": "Gitea Actions",
"commit_hash": "14991ae2aa2c36ee51e4b8ef080d333712e4faac", "commit_hash": "2ba26b79dd43512e13cee8600f7e35441d3e9f92",
"commit_short_hash": "14991ae2", "commit_short_hash": "2ba26b79",
"branch": "develop", "branch": "master",
"release_time": "2025-11-04 16:45:16", "release_time": "2025-10-09 17:52:04",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.71.exe",
"stats": { "stats": {
"files_changed": 2, "files_changed": 0,
"lines_added": 17, "lines_added": 0,
"lines_deleted": 17
}
},
{
"version": "1.5.70",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.69",
"author": "Gitea Actions Bot",
"commit_hash": "32a17fb255f3ace34262c8b34679b2129147f01c",
"commit_short_hash": "32a17fb2",
"branch": "jiang",
"release_time": "2025-11-04 14:24:14",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.70.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.69",
"update_type": "patch",
"content": "[patch] 修复因自动更新回归代码config.py因覆盖逻辑导致的新增代码被覆盖的逻辑",
"author": "kris 郝",
"commit_hash": "4f17092305e1f73e7c19b28d751d5bbaf6895022",
"commit_short_hash": "4f170923",
"branch": "develop",
"release_time": "2025-11-03 15:51:19",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.69.exe",
"stats": {
"files_changed": 1,
"lines_added": 25,
"lines_deleted": 15
}
},
{
"version": "1.5.67",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.66",
"author": "Gitea Actions Bot",
"commit_hash": "2083516b8c66377b5f13df0989fce0eeb82c105e",
"commit_short_hash": "2083516b",
"branch": "develop",
"release_time": "2025-11-03 11:31:45",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.67.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.66",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.65",
"author": "Gitea Actions Bot",
"commit_hash": "27b15773e0fffa15a65e4a69f3b9cd2097d8c24b",
"commit_short_hash": "27b15773",
"branch": "develop",
"release_time": "2025-10-31 13:49:16",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.66.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.65",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.64",
"author": "Gitea Actions Bot",
"commit_hash": "5440aa7d6dd075237772087f3c2ded8ebeb0d3b9",
"commit_short_hash": "5440aa7d",
"branch": "develop",
"release_time": "2025-10-30 16:54:42",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.65.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.64",
"update_type": "patch",
"content": "[patch] 处理DY安装包打包后图片视频暂存路径部分权限问题 处理编码问题 处理js内置环境dll补充",
"author": "haosicheng",
"commit_hash": "30ac26160ba7dbf90b1f17c9b062f3a965987649",
"commit_short_hash": "30ac2616",
"branch": "develop",
"release_time": "2025-10-30 14:58:17",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.64.exe",
"stats": {
"files_changed": 1,
"lines_added": 2,
"lines_deleted": 1
}
},
{
"version": "1.5.60",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.59",
"author": "Gitea Actions Bot",
"commit_hash": "a807bdb74de2cb1934dac1d7b83109407877acd6",
"commit_short_hash": "a807bdb7",
"branch": "develop",
"release_time": "2025-10-30 09:25:26",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.60.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.59",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.58",
"author": "Gitea Actions Bot",
"commit_hash": "791f98bf61f88e3d9b05e4941f8145a0d9b29f8a",
"commit_short_hash": "791f98bf",
"branch": "develop",
"release_time": "2025-10-29 17:05:23",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.59.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.58",
"update_type": "patch",
"content": "[patch] 新增自动更新功能 模块 优化更新后打开--不被遮挡",
"author": "haosicheng",
"commit_hash": "e024150c5aaa2d376f7ca371af7f819d6a458ced",
"commit_short_hash": "e024150c",
"branch": "develop",
"release_time": "2025-10-29 15:56:21",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.58.exe",
"stats": {
"files_changed": 1,
"lines_added": 2,
"lines_deleted": 2
}
},
{
"version": "1.5.56",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.55",
"author": "Gitea Actions Bot",
"commit_hash": "8f1b51ef4f7b7a832c06e008afea81a000d144cc",
"commit_short_hash": "8f1b51ef",
"branch": "develop",
"release_time": "2025-10-29 14:58:30",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.56.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.55",
"update_type": "patch",
"content": "[patch] 新增自动更新功能 模块Test-center",
"author": "haosicheng",
"commit_hash": "16d5d95c4ef1dd29537a718de5c87fb4f2aa4f48",
"commit_short_hash": "16d5d95c",
"branch": "develop",
"release_time": "2025-10-28 17:16:13",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.55.exe",
"stats": {
"files_changed": 2,
"lines_added": 133,
"lines_deleted": 32
}
},
{
"version": "1.5.54",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.53",
"author": "Gitea Actions Bot",
"commit_hash": "3d155fb0976f7efb2233d4e1444ded916b9d962f",
"commit_short_hash": "3d155fb0",
"branch": "develop",
"release_time": "2025-10-28 16:01:33",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.54.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 17
}
},
{
"version": "1.5.53",
"update_type": "patch",
"content": "[patch] 新增DY 图片 视频上传 发送等方法逻辑集成 优化抖音心跳维护与弹性发送心跳包 DY集成内置js环境 PDD取消过滤系统机器消息",
"author": "haosicheng",
"commit_hash": "1f1deb5f7ff9886951f2d996cc39716a1f9d10ac",
"commit_short_hash": "1f1deb5f",
"branch": "develop",
"release_time": "2025-10-27 17:34:09",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.53.exe",
"stats": {
"files_changed": 5,
"lines_added": 1556,
"lines_deleted": 64
}
},
{
"version": "1.5.51",
"update_type": "patch",
"content": "[patch] 因pdd内部回复消息接口与登录接口不同步导致监听过程中发送消息接口返回会话过期处理优化逻辑 并设计开发提示用户 并提供用户是否重新发送会话过期消息体按钮",
"author": "haosicheng",
"commit_hash": "7a1ad474396b151b921f5a55925e63b87f039564",
"commit_short_hash": "7a1ad474",
"branch": "develop",
"release_time": "2025-10-25 09:09:31",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.51.exe",
"stats": {
"files_changed": 3,
"lines_added": 393,
"lines_deleted": 50
}
},
{
"version": "1.5.49",
"update_type": "patch",
"content": "[patch] 重连机制新增is_reconnect参数",
"author": "haosicheng",
"commit_hash": "9f0c81ea82af30d82b9db7ec9b9668f444ba62b5",
"commit_short_hash": "9f0c81ea",
"branch": "develop",
"release_time": "2025-10-23 09:14:58",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.49.exe",
"stats": {
"files_changed": 2,
"lines_added": 8,
"lines_deleted": 4
}
},
{
"version": "1.5.47",
"update_type": "patch",
"content": "[patch] 修复抖音消息格式 过滤抖音系统消息 失效后发送结构体给后端",
"author": "Gitea Actions Bot",
"commit_hash": "782c56f9b634d05dfd7e913017cf57fdf197ff10",
"commit_short_hash": "782c56f9",
"branch": "develop",
"release_time": "2025-10-22 17:14:33",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.47.exe",
"stats": {
"files_changed": 1,
"lines_added": 92,
"lines_deleted": 7
}
},
{
"version": "1.5.46",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.45",
"author": "Gitea Actions Bot",
"commit_hash": "099c3e2db2b367b575477578eb46b018972925ac",
"commit_short_hash": "099c3e2d",
"branch": "develop",
"release_time": "2025-10-22 14:50:54",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.46.exe",
"stats": {
"files_changed": 2,
"lines_added": 18,
"lines_deleted": 2
}
},
{
"version": "1.5.45",
"update_type": "patch",
"content": "[patch] DY-token检测逻辑修改 修改本地打包环境路径锁定",
"author": "haosicheng",
"commit_hash": "ff6203d387aedb0bbe7c7728efaa2a4fdba7d882",
"commit_short_hash": "ff6203d3",
"branch": "develop",
"release_time": "2025-10-22 13:16:48",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.45.exe",
"stats": {
"files_changed": 2,
"lines_added": 141,
"lines_deleted": 59
}
},
{
"version": "1.5.43",
"update_type": "patch",
"content": "[patch] 禁用ks3代理",
"author": "haosicheng",
"commit_hash": "cf72bc68271a7952940bc07b24b5b9f3b73250ae",
"commit_short_hash": "cf72bc68",
"branch": "develop",
"release_time": "2025-10-21 16:19:34",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.43.exe",
"stats": {
"files_changed": 1,
"lines_added": 1,
"lines_deleted": 5
}
},
{
"version": "1.5.38",
"update_type": "patch",
"content": "[patch] 处理PDD登录时get_auth_token方法报出的response返回异常未处理报错问题",
"author": "haosicheng",
"commit_hash": "449ebf83cbf88852ca6918420f8a4e491dcdeaa4",
"commit_short_hash": "449ebf83",
"branch": "develop",
"release_time": "2025-10-18 17:15:36",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.38.exe",
"stats": {
"files_changed": 1,
"lines_added": 2,
"lines_deleted": 0 "lines_deleted": 0
} }
}, },
{ {
"version": "1.5.36", "version": "1.4.7",
"update_type": "patch", "update_type": "patch",
"content": "[patch] 新增用户余额不足 交互模式代码", "content": "解决job构建失败的问题",
"author": "haosicheng", "author": "Gitea Actions",
"commit_hash": "8eb03ddedcad2d276ad1f2d4aa68252f963633c2", "commit_hash": "cab5b606a9728871de830dbf3f8c01361071ef02",
"commit_short_hash": "8eb03dde", "commit_short_hash": "cab5b606",
"branch": "develop", "branch": "master",
"release_time": "2025-10-17 17:38:19", "release_time": "2025-10-09 17:36:03",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.36.exe",
"stats": {
"files_changed": 4,
"lines_added": 225,
"lines_deleted": 77
}
},
{
"version": "1.5.35",
"update_type": "patch",
"content": "[patch] 新增拼多多处理聊天页商品卡片",
"author": "Gitea Actions Bot",
"commit_hash": "3853b8faf8a55095c133730ce9b080ca3a68b3dd",
"commit_short_hash": "3853b8fa",
"branch": "develop",
"release_time": "2025-10-17 17:05:28",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.35.exe",
"stats": { "stats": {
"files_changed": 1, "files_changed": 1,
"lines_added": 15, "lines_added": 23,
"lines_deleted": 1
}
},
{
"version": "1.5.34",
"update_type": "patch",
"content": "[patch] config配置修改 ping机制",
"author": "haosicheng",
"commit_hash": "fe2b29da9c483ae82d6e7f6a95c66f561683731f",
"commit_short_hash": "fe2b29da",
"branch": "develop",
"release_time": "2025-10-17 15:50:54",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.34.exe",
"stats": {
"files_changed": 1,
"lines_added": 4,
"lines_deleted": 3
}
},
{
"version": "1.5.33",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.32",
"author": "Gitea Actions Bot",
"commit_hash": "90bf763bded8c6cb163ded90bf014cbdab61c312",
"commit_short_hash": "90bf763b",
"branch": "develop",
"release_time": "2025-10-17 15:21:22",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.33.exe",
"stats": {
"files_changed": 2,
"lines_added": 20,
"lines_deleted": 5
}
},
{
"version": "1.5.32",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.31",
"author": "Gitea Actions Bot",
"commit_hash": "e2ab13d599a6d5484f097826358764c5e3ad1436",
"commit_short_hash": "e2ab13d5",
"branch": "develop",
"release_time": "2025-10-17 11:29:44",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.32.exe",
"stats": {
"files_changed": 2,
"lines_added": 150,
"lines_deleted": 134
}
},
{
"version": "1.5.31",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.30",
"author": "Gitea Actions Bot",
"commit_hash": "e15c1db49b2feb7aee650de5d23681bd9842ede8",
"commit_short_hash": "e15c1db4",
"branch": "develop",
"release_time": "2025-10-15 16:27:08",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.31.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 1
}
},
{
"version": "1.5.30",
"update_type": "patch",
"content": "[patch] 优化提交检测合并逻辑",
"author": "haosicheng",
"commit_hash": "340670742c41a6de65a76fb24b25959c82618793",
"commit_short_hash": "34067074",
"branch": "develop",
"release_time": "2025-10-15 09:55:32",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.30.exe",
"stats": {
"files_changed": 1,
"lines_added": 19,
"lines_deleted": 2
}
},
{
"version": "1.5.28",
"update_type": "patch",
"content": "[patch] 修改打包逻辑 优化js代码因环境不兼容问题处理方案开发",
"author": "haosicheng",
"commit_hash": "9a4e2bba7982129b4c0f305482dac0d0e97dfb24",
"commit_short_hash": "9a4e2bba",
"branch": "develop",
"release_time": "2025-10-14 18:07:38",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.28.exe",
"stats": {
"files_changed": 3,
"lines_added": 5,
"lines_deleted": 5
}
},
{
"version": "1.5.27",
"update_type": "patch",
"content": "[patch] 修正京东丢失客服列表功能",
"author": "Gitea Actions Bot",
"commit_hash": "7312d3f91a5810eae8c6d41688bdc735def78459",
"commit_short_hash": "7312d3f9",
"branch": "develop",
"release_time": "2025-10-14 17:27:26",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.27.exe",
"stats": {
"files_changed": 2,
"lines_added": 82,
"lines_deleted": 1
}
},
{
"version": "1.5.26",
"update_type": "patch",
"content": "[skip ci] Update version to v1.5.25",
"author": "Gitea Actions Bot",
"commit_hash": "5e9e87bbba56a98f27e1d115018c856e1e266f3f",
"commit_short_hash": "5e9e87bb",
"branch": "develop",
"release_time": "2025-10-13 17:07:40",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.26.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 1
}
},
{
"version": "1.5.25",
"update_type": "patch",
"content": "[patch] 修改打包固定项缓存复用 提升打包效率",
"author": "haosicheng",
"commit_hash": "3c57b990649e9061e09f6665f92aa7a09bdcf79f",
"commit_short_hash": "3c57b990",
"branch": "develop",
"release_time": "2025-10-13 15:32:15",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.25.exe",
"stats": {
"files_changed": 2,
"lines_added": 1,
"lines_deleted": 44
}
},
{
"version": "1.5.21",
"update_type": "patch",
"content": "[patch] 修正pdd不识别系统消息的问题",
"author": "Gitea Actions Bot",
"commit_hash": "f651e0e3f02a42f14c86a1d54f366e06cd9b5c49",
"commit_short_hash": "f651e0e3",
"branch": "develop",
"release_time": "2025-10-13 14:04:33",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.21.exe",
"stats": {
"files_changed": 1,
"lines_added": 3259,
"lines_deleted": 3254
}
},
{
"version": "1.5.20",
"update_type": "patch",
"content": "[patch] 修改逻辑: 在用户误触或开了多个GUI程序的时候不能同时建立多个连接 确保一个账号只能建立一个与后端的连接 友好提示的集成review 修改powershell语法问题",
"author": "haosicheng",
"commit_hash": "5896b422f73104cb7b9dba00d19936736f584a4b",
"commit_short_hash": "5896b422",
"branch": "develop",
"release_time": "2025-10-13 13:40:48",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.20.exe",
"stats": {
"files_changed": 1,
"lines_added": 171,
"lines_deleted": 171
}
},
{
"version": "1.5.13",
"update_type": "patch",
"content": "[patch] 修正ks3问题且优化pillow依赖安装时间",
"author": "Gitea Actions Bot",
"commit_hash": "d8154025ea712deafa30b7c0a8434d1ba4c2cab7",
"commit_short_hash": "d8154025",
"branch": "develop",
"release_time": "2025-10-11 17:11:50",
"download_url": "https://shuidrop-chat-server.ks3-cn-guangzhou.ksyuncs.com/installers/ShuiDi_AI_Assistant_Setup_v1.5.13.exe",
"stats": {
"files_changed": 6,
"lines_added": 53,
"lines_deleted": 21
}
},
{
"version": "1.5.12",
"update_type": "patch",
"content": "[patch] 修正ks3问题",
"author": "Gitea Actions Bot",
"commit_hash": "5c73bbf8bce718195e1d4b4f727ede53a3921c1a",
"commit_short_hash": "5c73bbf8",
"branch": "develop",
"release_time": "2025-10-11 16:54:44",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.12.exe",
"stats": {
"files_changed": 3,
"lines_added": 29,
"lines_deleted": 13 "lines_deleted": 13
} }
}, },
{ {
"version": "1.5.11", "version": "1.4.6",
"update_type": "patch", "update_type": "patch",
"content": "[patch] 修正ks3包名问题", "content": "解决job构建失败的问题",
"author": "Gitea Actions Bot", "author": "Gitea Actions",
"commit_hash": "bcb76c0a99d8ec689610a12d2942bd486e381358", "commit_hash": "2734d375c244fa6b8ef8925cf3d2982751168f4e",
"commit_short_hash": "bcb76c0a", "commit_short_hash": "2734d375",
"branch": "develop", "branch": "master",
"release_time": "2025-10-11 16:44:27", "release_time": "2025-10-09 17:30:35",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.11.exe",
"stats": {
"files_changed": 5,
"lines_added": 29,
"lines_deleted": 6
}
},
{
"version": "1.5.10",
"update_type": "patch",
"content": "[patch] 修正Pillow依赖冲突问题",
"author": "Gitea Actions Bot",
"commit_hash": "99446eca748e93c289012a8465a94e4ae7ca8961",
"commit_short_hash": "99446eca",
"branch": "develop",
"release_time": "2025-10-11 16:34:25",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.10.exe",
"stats": {
"files_changed": 3,
"lines_added": 30,
"lines_deleted": 14
}
},
{
"version": "1.5.9",
"update_type": "patch",
"content": "[patch] 修正Pillow安装步骤",
"author": "Gitea Actions Bot",
"commit_hash": "14726466248048dc40e1e4d84119a0bc5da41432",
"commit_short_hash": "14726466",
"branch": "develop",
"release_time": "2025-10-11 16:30:23",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.9.exe",
"stats": {
"files_changed": 3,
"lines_added": 29,
"lines_deleted": 2
}
},
{
"version": "1.5.8",
"update_type": "patch",
"content": "[patch] 安装Pillow依赖",
"author": "Gitea Actions Bot",
"commit_hash": "b685e64e7119e680cb55f323c882e036b08c0784",
"commit_short_hash": "b685e64e",
"branch": "develop",
"release_time": "2025-10-11 16:27:55",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.8.exe",
"stats": {
"files_changed": 3,
"lines_added": 21,
"lines_deleted": 1
}
},
{
"version": "1.5.7",
"update_type": "patch",
"content": "[patch] 测试是否安装成功",
"author": "Gitea Actions Bot",
"commit_hash": "ee7c76ef68dd8e2672ec1a6b984ee65edab583bf",
"commit_short_hash": "ee7c76ef",
"branch": "develop",
"release_time": "2025-10-11 16:24:59",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.7.exe",
"stats": {
"files_changed": 3,
"lines_added": 22,
"lines_deleted": 6
}
},
{
"version": "1.5.6",
"update_type": "patch",
"content": "[patch] 修改nsis安装流程",
"author": "Gitea Actions Bot",
"commit_hash": "afcd360603aa89b41f32ac59c4527103b1aa33f9",
"commit_short_hash": "afcd3606",
"branch": "develop",
"release_time": "2025-10-11 16:20:24",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.6.exe",
"stats": {
"files_changed": 3,
"lines_added": 121,
"lines_deleted": 96
}
},
{
"version": "1.5.5",
"update_type": "patch",
"content": "[patch] 修改nsis安装流程",
"author": "Gitea Actions Bot",
"commit_hash": "3352a59ff9c90d09b080f82e04e06c881becea80",
"commit_short_hash": "3352a59f",
"branch": "develop",
"release_time": "2025-10-11 16:16:08",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.5.exe",
"stats": {
"files_changed": 3,
"lines_added": 96,
"lines_deleted": 54
}
},
{
"version": "1.5.4",
"update_type": "patch",
"content": "[patch] 修改nsis安装流程",
"author": "Gitea Actions Bot",
"commit_hash": "468a092fd93c81e08481b212fd730f11964ff928",
"commit_short_hash": "468a092f",
"branch": "develop",
"release_time": "2025-10-11 16:11:56",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.4.exe",
"stats": {
"files_changed": 3,
"lines_added": 97,
"lines_deleted": 12
}
},
{
"version": "1.5.3",
"update_type": "patch",
"content": "[patch] 解决乱码bug",
"author": "Gitea Actions Bot",
"commit_hash": "7c0667cb39c2795ce9901e141e1a9ff698a4955b",
"commit_short_hash": "7c0667cb",
"branch": "develop",
"release_time": "2025-10-11 16:09:00",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.3.exe",
"stats": {
"files_changed": 3,
"lines_added": 60,
"lines_deleted": 5
}
},
{
"version": "1.5.2",
"update_type": "patch",
"content": "[patch] 解决乱码bug",
"author": "Gitea Actions Bot",
"commit_hash": "2d05024e82ac0c16eff30625195486f7c6862ea8",
"commit_short_hash": "2d05024e",
"branch": "develop",
"release_time": "2025-10-11 16:05:03",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.2.exe",
"stats": {
"files_changed": 5,
"lines_added": 207,
"lines_deleted": 192
}
},
{
"version": "1.5.1",
"update_type": "patch",
"content": "[patch] 添加develop用于测试",
"author": "Gitea Actions Bot",
"commit_hash": "2dddcc9804ad4a6966439f2139110ab639ca1452",
"commit_short_hash": "2dddcc98",
"branch": "develop",
"release_time": "2025-10-11 15:56:40",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.1.exe",
"stats": { "stats": {
"files_changed": 1, "files_changed": 1,
"lines_added": 1, "lines_added": 9,
"lines_deleted": 1 "lines_deleted": 26
} }
}, },
{ {
"version": "1.5.0", "version": "1.4.5",
"update_type": "minor",
"content": "实现更新版本管理",
"author": "Gitea Actions Bot",
"commit_hash": "8ecec1edbed34ef40869cf7a0c07fc33e81d9c37",
"commit_short_hash": "8ecec1ed",
"branch": "develop",
"release_time": "2025-10-11 15:47:09",
"download_url": "https://ks3-cn-guangzhou.ksyuncs.com/shuidrop-chat-server/installers/ShuiDi_AI_Assistant_Setup_v1.5.0.exe",
"stats": {
"files_changed": 6,
"lines_added": 265,
"lines_deleted": 168
}
},
{
"version": "1.4.29",
"update_type": "patch", "update_type": "patch",
"content": "[skip ci] Update version to v1.4.28", "content": "修改网络问题",
"author": "Gitea Actions Bot", "author": "Gitea Actions",
"commit_hash": "2dd384e8e48ed7f142bae43821b08db862c2bcca", "commit_hash": "39ba4f93241e2121578518712250582a8345304b",
"commit_short_hash": "2dd384e8", "commit_short_hash": "39ba4f93",
"branch": "develop", "branch": "master",
"release_time": "2025-10-11 15:30:32", "release_time": "2025-10-09 17:24:42",
"download_url": "https://shuidrop.com/download/gui/ShuiDi_AI_Assistant_Setup_v1.4.29.exe",
"stats": {
"files_changed": 2,
"lines_added": 17,
"lines_deleted": 1
}
},
{
"version": "1.4.28",
"update_type": "patch",
"content": "[patch] PDD登录状态GUI回调优化 与 非原生git指令导致check任务失败",
"author": "haosicheng",
"commit_hash": "c6ae22c7e10ff560a117f7dec0929f43c224d720",
"commit_short_hash": "c6ae22c7",
"branch": "develop",
"release_time": "2025-10-11 10:17:19",
"download_url": "https://shuidrop.com/download/gui/ShuiDi_AI_Assistant_Setup_v1.4.28.exe",
"stats": { "stats": {
"files_changed": 1, "files_changed": 1,
"lines_added": 0, "lines_added": 5,
"lines_deleted": 1 "lines_deleted": 1
} }
},
{
"version": "1.4.4",
"update_type": "patch",
"content": "修改网络问题",
"author": "jjz",
"commit_hash": "41b686fff62d15c197c52e7b47d2ff57826ad00a",
"commit_short_hash": "41b686ff",
"branch": "master",
"release_time": "2025-10-09 17:21:36",
"stats": {
"files_changed": 1,
"lines_added": 11,
"lines_deleted": 11
}
},
{
"version": "1.4.3",
"update_type": "patch",
"content": "修改网络问题",
"author": "jjz",
"commit_hash": "a925ca2c991b9877bfd05753da95b78895a68f31",
"commit_short_hash": "a925ca2c",
"branch": "master",
"release_time": "2025-10-09 17:20:07",
"stats": {
"files_changed": 1,
"lines_added": 19,
"lines_deleted": 11
}
} }
] ]