Compare commits

1 Commits

Author SHA1 Message Date
68ebf173e3 backup: save local changes on 2025-09-26 2025-09-26 15:30:05 +08:00
7888 changed files with 2590860 additions and 2976 deletions

View File

@@ -1,419 +0,0 @@
# 🚀 GUI客户端自动化版本管理系统
## 📋 概述
**与后端完全一致的版本管理系统** - 直接连接PostgreSQL数据库
-**直接数据库操作** - 与后端使用相同方式PostgreSQL
-**完全一致** - 相同的表、相同的字段、相同的逻辑
-**极简设计** - 无需API配置一次永久生效
-**全自动** - 提交代码即可,自动版本递增
---
## 🎯 核心特性
### 存储方式(与后端完全一致)
```
后端Django GUI客户端
↓ ↓
Django ORM psycopg2
↓ ↓
└──────────┬──────────────────┘
PostgreSQL
web_versionhistory表
```
| 特性 | 后端 | GUI |
|------|------|-----|
| **操作方式** | Django ORM | psycopg2SQL |
| **数据库** | PostgreSQL (8.155.9.53) | **相同** |
| **表名** | web_versionhistory | **相同** |
| **版本类型** | "Web端" | "水滴智能通讯插件" |
---
## 🚀 快速开始2步
### 1⃣ 安装依赖
```bash
pip install psycopg2-binary
```
### 2⃣ 提交代码
```bash
# 新增功能MINOR版本
git commit -m "[minor] 新增拼多多平台支持"
# 修复BugPATCH版本
git commit -m "[patch] 修复京东连接超时"
# 重大变更MAJOR版本
git commit -m "[major] 重构WebSocket架构"
# 推送触发自动版本发布
git push origin main
```
**就这么简单!** 🎉
---
## 📖 版本号规则
采用**语义化版本**Semantic Versioning`MAJOR.MINOR.PATCH`
| 版本类型 | 说明 | 示例 |
|---------|------|------|
| `MAJOR` | 重大架构变更 | 1.0.0 → 2.0.0 |
| `MINOR` | 新增功能 | 1.0.0 → 1.1.0 |
| `PATCH` | Bug修复、优化 | 1.0.0 → 1.0.1 |
---
## 🔍 Commit Message 关键词
### 手动标记(优先级最高)
```bash
git commit -m "[major] 重构WebSocket通信架构"
git commit -m "[minor] 新增拼多多平台支持"
git commit -m "[patch] 修复京东连接超时问题"
```
### 自动识别关键词
**MAJOR**: `重构`, `refactor`, `架构`, `breaking`
**MINOR**: `新增`, `add`, `feature`, `功能`, `实现`
**PATCH**: `修复`, `fix`, `bug`, `优化`, `调整`
---
## 📁 文件结构
```
shuidrop_gui/
├── .gitea/
│ ├── workflows/
│ │ └── gui-version-release.yml # CI/CD工作流
│ ├── scripts/
│ │ ├── gui_version_creator.py # ⭐ 核心:版本创建器(直接数据库)
│ │ ├── view_version_history.py # 版本历史查看
│ │ └── init_version_system.py # 系统初始化
│ └── README.md # 本文档
├── version_history.json # 版本历史本地备份
├── config.py # 包含 APP_VERSION
└── VERSION_MANAGEMENT_GUIDE.md # 快速入门指南
```
---
## 🛠️ 常用命令
### 查看版本历史
```bash
# 查看最近10个版本
python .gitea/scripts/view_version_history.py --list
# 查看最新版本
python .gitea/scripts/view_version_history.py --latest
# 查看特定版本详情
python .gitea/scripts/view_version_history.py --detail 1.2.3
# 导出更新日志
python .gitea/scripts/view_version_history.py --export
```
### 本地测试
```bash
# 设置数据库连接
export DB_HOST=8.155.9.53
export DB_PORT=5400
export DB_NAME=ai_web
export DB_USER=user_emKCAb
export DB_PASSWORD=password_ee2iQ3
# 运行版本创建脚本
python .gitea/scripts/gui_version_creator.py
```
---
## 🔄 版本创建流程
```
提交代码
git push origin main
Gitea Actions触发
gui_version_creator.py
┌────────────────────────────────┐
│ 1. 连接PostgreSQL数据库 │
│ (8.155.9.53:5400/ai_web) │
│ 2. 查询最新版本 │
│ SELECT version FROM ... │
│ 3. 分析commit message │
│ 识别版本类型 │
│ 4. 计算新版本号 │
│ 1.0.5 → 1.1.0 │
│ 5. 插入数据库 │
│ INSERT INTO web_versionhistory...│
│ 6. 保存本地JSON备份 │
│ 7. 更新config.py │
│ APP_VERSION = "1.1.0" │
└────────────────────────────────┘
完成!
```
---
## 💾 存储策略
### 主要存储PostgreSQL数据库
```sql
-- web_versionhistory表与后端共用
INSERT INTO web_versionhistory (
version, -- "1.1.0"
type, -- "水滴智能通讯插件"
content, -- commit message
download_url, -- 下载地址
release_time, -- 发布时间
is_delete -- FALSE
) VALUES (...);
```
### 备份存储本地JSON
```json
// version_history.json快速查询/离线使用)
[
{
"version": "1.1.0",
"update_type": "minor",
"content": "新增拼多多平台支持",
"author": "张三",
"release_time": "2025-10-09 14:30:00",
"stats": {
"files_changed": 5,
"lines_added": 234,
"lines_deleted": 56
}
}
]
```
---
## ⚙️ CI/CD配置
### 触发条件
- **分支**: `main`
- **事件**: `push`
### 环境变量(已配置好)
```yaml
env:
DB_HOST: 8.155.9.53 # 数据库主机
DB_PORT: 5400 # 端口
DB_NAME: ai_web # 数据库名
DB_USER: user_emKCAb # 用户名
DB_PASSWORD: password_ee2iQ3 # 密码
```
**这些配置与后端完全一致!**
### 执行步骤
1. 📦 检出代码
2. 🐍 设置Python环境
3. 📦 安装依赖psycopg2-binary
4. 🏷️ 创建版本记录(直接数据库操作)
5. 📦 自动打包(可选)
---
## 📊 版本历史格式
### PostgreSQL数据库主要
```sql
-- 与后端共享web_versionhistory表
SELECT * FROM web_versionhistory
WHERE type = '水滴智能通讯插件'
ORDER BY release_time DESC;
```
### 本地JSON备份
```json
[
{
"version": "1.1.0",
"update_type": "minor",
"content": "新增拼多多平台支持",
"author": "张三",
"commit_hash": "abc123def456...",
"release_time": "2025-10-09 14:30:00",
"stats": {...}
}
]
```
---
## 🔧 常见问题
### Q: 为什么不用API
**A**: 直接数据库操作的优势:
- ✅ 与后端存储方式完全一致
- ✅ 无需后端开发接口
- ✅ 更简单、更可靠
- ✅ 减少中间层故障点
### Q: 数据库连接失败怎么办?
**A**: 不影响使用!
- ✅ 本地JSON备份仍会保存
- ✅ config.py仍会更新
- ✅ 后续可手动同步数据库
### Q: 如何跳过版本发布?
**A**: 在commit message中添加 `[skip ci]`
```bash
git commit -m "更新文档 [skip ci]"
```
### Q: 如何验证版本记录?
**A**:
```bash
# 方式1: 查看本地备份
python .gitea/scripts/view_version_history.py --latest
# 方式2: 查询数据库
psql -h 8.155.9.53 -p 5400 -U user_emKCAb -d ai_web \
-c "SELECT * FROM web_versionhistory WHERE type='水滴智能通讯插件' LIMIT 5;"
```
---
## 🎓 学习资源
### 1. 查看快速入门指南
```bash
cat VERSION_MANAGEMENT_GUIDE.md
```
### 2. 查看实施说明
```bash
cat IMPLEMENTATION_NOTES.md
```
### 3. 初始化系统
```bash
python .gitea/scripts/init_version_system.py
```
---
## 📞 完整示例
### 场景:新增拼多多平台支持
```bash
# 1. 开发功能
# 修改: Utils/Pdd/PddUtils.py, config.py 等
# 2. 提交代码
git commit -m "[minor] 新增拼多多平台支持,实现消息收发和自动重连"
git push origin main
```
**自动执行**
1. Gitea Actions检测push
2. 连接PostgreSQL数据库
3. 查询最新版本: `1.0.5`
4. 版本递增: `1.0.5``1.1.0`
5. **插入数据库**: `INSERT INTO web_versionhistory ...`
6. 保存本地备份: `version_history.json`
7. 更新配置: `APP_VERSION = "1.1.0"`
**验证结果**
```bash
$ python .gitea/scripts/view_version_history.py --latest
🏷️ 最新版本: v1.1.0
📅 发布时间: 2025-10-09 14:30:00
👤 发布者: 张三
📝 内容: 新增拼多多平台支持,实现消息收发和自动重连
```
---
## 💡 核心优势
### 1. 与后端完全一致
- ✅ 相同的数据库PostgreSQL
- ✅ 相同的表web_versionhistory
- ✅ 相同的字段结构
- ✅ 相同的时间处理逻辑
### 2. 极简设计
- ✅ 只需安装1个依赖psycopg2-binary
- ✅ 无需后端开发API
- ✅ 配置一次,永久生效
### 3. 全自动
- ✅ 提交代码自动触发
- ✅ 自动版本递增
- ✅ 自动更新配置
### 4. 双重保障
- ✅ PostgreSQL数据库主要
- ✅ 本地JSON备份保底
---
## 🚀 下一步行动
```bash
# 1. 安装依赖(必需)
pip install psycopg2-binary
# 2. 初始化系统(推荐)
python .gitea/scripts/init_version_system.py
# 3. 测试提交
git commit -m "[patch] 测试版本管理系统"
git push origin main
# 4. 验证结果
python .gitea/scripts/view_version_history.py --latest
```
---
**恭喜!** 🎉 你现在拥有了一套**与后端完全一致**的自动化版本管理系统!
**版本**: 3.0(最终版)
**最后更新**: 2025-10-09
**核心特点**: 直接PostgreSQL数据库操作与后端完全一致

View File

@@ -1,469 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
GUI客户端版本创建器
⚠️ 仅用于CI/CD环境Gitea Actions不打包到用户端GUI
安全说明:
- 本脚本包含数据库凭证仅在受控的CI/CD环境运行
- 用户端GUI不包含此脚本避免数据库凭证泄漏
- 用户端GUI只读取本地version_history.json文件
"""
import os
import sys
import json
import logging
from datetime import datetime
from pathlib import Path
from typing import Dict, Optional
# 添加项目根目录到路径
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
# 配置日志
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(levelname)s] %(message)s'
)
logger = logging.getLogger(__name__)
# 数据库配置(与后端一致)
# ⚠️ 仅在CI/CD环境使用不会打包到用户端
DB_CONFIG = {
'host': os.getenv('DB_HOST', '8.155.9.53'),
'port': int(os.getenv('DB_PORT', '5400')),
'database': os.getenv('DB_NAME', 'ai_web'),
'user': os.getenv('DB_USER', 'user_emKCAb'),
'password': os.getenv('DB_PASSWORD', 'password_ee2iQ3')
}
class GitUtils:
"""Git操作工具"""
@staticmethod
def run_command(args: list) -> Optional[str]:
"""执行Git命令"""
import subprocess
try:
result = subprocess.run(
['git'] + args,
capture_output=True,
text=True,
encoding='utf-8'
)
return result.stdout.strip() if result.returncode == 0 else None
except Exception as e:
logger.error(f"Git命令执行失败: {e}")
return None
@classmethod
def get_commit_info(cls) -> Dict[str, str]:
"""获取当前提交信息"""
return {
'message': cls.run_command(['log', '-1', '--pretty=format:%B']) or "自动发布",
'hash': cls.run_command(['rev-parse', 'HEAD']) or "",
'short_hash': (cls.run_command(['rev-parse', 'HEAD']) or "")[:8],
'author': cls.run_command(['log', '-1', '--pretty=format:%an']) or "系统",
'commit_date': cls.run_command(['log', '-1', '--pretty=format:%ci']) or "",
'branch': cls.run_command(['rev-parse', '--abbrev-ref', 'HEAD']) or "unknown"
}
@classmethod
def get_commit_stats(cls) -> Dict[str, int]:
"""获取提交统计"""
stats_output = cls.run_command(['diff', '--numstat', 'HEAD~1', 'HEAD'])
if not stats_output:
return {'files_changed': 0, 'lines_added': 0, 'lines_deleted': 0}
files_changed = 0
lines_added = 0
lines_deleted = 0
for line in stats_output.split('\n'):
if line.strip():
parts = line.split('\t')
if len(parts) >= 2:
try:
added = int(parts[0]) if parts[0].isdigit() else 0
deleted = int(parts[1]) if parts[1].isdigit() else 0
files_changed += 1
lines_added += added
lines_deleted += deleted
except (ValueError, IndexError):
continue
return {
'files_changed': files_changed,
'lines_added': lines_added,
'lines_deleted': lines_deleted
}
class DatabaseVersionManager:
"""
数据库版本管理器
⚠️ 安全警告仅在CI/CD环境使用
"""
def __init__(self):
self.project_root = PROJECT_ROOT
self.version_file = self.project_root / 'version_history.json'
self.config_file = self.project_root / 'config.py'
self.git_utils = GitUtils()
self.db_config = DB_CONFIG
self.conn = None
def connect_db(self):
"""连接数据库"""
try:
import psycopg2
self.conn = psycopg2.connect(**self.db_config)
logger.info("✅ 数据库连接成功")
return True
except ImportError:
logger.error("❌ psycopg2未安装请执行: pip install psycopg2-binary")
return False
except Exception as e:
logger.error(f"❌ 数据库连接失败: {e}")
return False
def close_db(self):
"""关闭数据库连接"""
if self.conn:
self.conn.close()
logger.info("数据库连接已关闭")
def analyze_update_type(self, commit_message: str) -> str:
"""分析更新类型"""
message = commit_message.lower()
# 手动标记优先
if '[major]' in message or '【major】' in message:
return 'major'
elif '[minor]' in message or '【minor】' in message:
return 'minor'
elif '[patch]' in message or '【patch】' in message:
return 'patch'
# 关键词识别
major_keywords = ['重构', 'refactor', '架构', 'framework', 'breaking', '大版本', '底层重写']
minor_keywords = ['新增', 'add', 'feature', '功能', 'feat:', 'new', '增加', 'implement', '实现']
patch_keywords = ['修复', 'fix', 'bug', '优化', 'optimize', 'improve', '调整', 'update', '界面', 'ui', '样式']
if any(keyword in message for keyword in major_keywords):
return 'major'
elif any(keyword in message for keyword in minor_keywords):
return 'minor'
elif any(keyword in message for keyword in patch_keywords):
return 'patch'
else:
return 'patch'
def get_latest_version_from_db(self) -> str:
"""从数据库获取最新版本"""
try:
cursor = self.conn.cursor()
cursor.execute("""
SELECT version FROM web_version_history
WHERE type = '水滴智能通讯插件' AND is_delete = FALSE
ORDER BY release_time DESC LIMIT 1
""")
result = cursor.fetchone()
cursor.close()
if result:
logger.info(f"📡 从数据库获取最新版本: v{result[0]}")
return result[0]
return "1.0.0"
except Exception as e:
logger.warning(f"⚠️ 从数据库获取版本失败: {e},使用默认版本")
return "1.0.0"
def calculate_next_version(self, update_type: str) -> str:
"""计算下一个版本号"""
current_version = self.get_latest_version_from_db()
try:
parts = current_version.split('.')
major = int(parts[0]) if len(parts) > 0 else 1
minor = int(parts[1]) if len(parts) > 1 else 0
patch = int(parts[2]) if len(parts) > 2 else 0
except (ValueError, IndexError):
return "1.0.0"
if update_type == 'major':
return f"{major + 1}.0.0"
elif update_type == 'minor':
return f"{major}.{minor + 1}.0"
else: # patch
return f"{major}.{minor}.{patch + 1}"
def check_duplicate_in_db(self, commit_hash: str) -> bool:
"""检查数据库中是否已存在该版本"""
if not commit_hash:
return False
commit_id = commit_hash[:16]
try:
cursor = self.conn.cursor()
cursor.execute("""
SELECT version FROM web_version_history
WHERE content LIKE %s AND type = '水滴智能通讯插件'
ORDER BY release_time DESC LIMIT 1
""", (f'%{commit_id}%',))
result = cursor.fetchone()
cursor.close()
if result:
logger.info(f"📡 数据库检测到重复版本: v{result[0]}")
return True
return False
except Exception as e:
logger.warning(f"⚠️ 检查重复版本失败: {e}")
return False
def save_to_database(self, version_record: dict) -> bool:
"""保存版本记录到数据库(与后端完全一致)"""
try:
# 时间处理:与后端保持一致
from datetime import timezone as dt_timezone
import uuid
beijing_time_naive = datetime.now()
beijing_time_as_utc = beijing_time_naive.replace(tzinfo=dt_timezone.utc)
# 生成UUID
record_id = str(uuid.uuid4())
cursor = self.conn.cursor()
cursor.execute("""
INSERT INTO web_version_history
(id, version, type, content, download_url, release_time, is_delete)
VALUES (%s, %s, %s, %s, %s, %s, %s)
""", (
record_id,
version_record['version'],
'水滴智能通讯插件',
version_record['content'],
version_record.get('download_url', ''),
beijing_time_as_utc,
False
))
self.conn.commit()
cursor.close()
logger.info(f"✅ 版本记录已保存到数据库 (ID: {record_id})")
return True
except Exception as e:
self.conn.rollback()
logger.error(f"❌ 保存到数据库失败: {e}")
import traceback
logger.error(traceback.format_exc())
return False
def save_local_backup(self, version_record: dict):
"""
保存本地JSON备份
⚠️ 此文件会被打包到用户端GUI用于版本历史查看
"""
try:
# 读取现有历史
history = []
if self.version_file.exists():
with open(self.version_file, 'r', encoding='utf-8') as f:
history = json.load(f)
# 添加新记录
history.insert(0, version_record)
# 保留最近50个版本
if len(history) > 50:
history = history[:50]
# 保存
with open(self.version_file, 'w', encoding='utf-8') as f:
json.dump(history, f, ensure_ascii=False, indent=2)
logger.info(f"✅ 本地备份已保存: {self.version_file}")
logger.info(f" 此文件将打包到用户端GUI用于版本历史查看")
except Exception as e:
logger.warning(f"⚠️ 保存本地备份失败: {e}")
def update_config_version(self, new_version: str):
"""更新config.py中的APP_VERSION"""
if not self.config_file.exists():
logger.warning(f"配置文件不存在: {self.config_file}")
return False
try:
with open(self.config_file, 'r', encoding='utf-8') as f:
content = f.read()
import re
pattern = r'APP_VERSION\s*=\s*["\'][\d.]+["\']'
replacement = f'APP_VERSION = "{new_version}"'
if re.search(pattern, content):
new_content = re.sub(pattern, replacement, content)
with open(self.config_file, 'w', encoding='utf-8') as f:
f.write(new_content)
logger.info(f"✅ 已更新 config.py: APP_VERSION = \"{new_version}\"")
return True
else:
logger.warning("未找到APP_VERSION配置项")
return False
except Exception as e:
logger.error(f"更新config.py失败: {e}")
return False
def create_version_record(self) -> dict:
"""
创建版本记录(直接数据库操作)
⚠️ 仅在CI/CD环境运行
"""
try:
logger.info("=" * 70)
logger.info("🚀 开始创建GUI版本记录CI/CD环境")
logger.info("=" * 70)
# 1. 连接数据库
if not self.connect_db():
return {'success': False, 'error': '数据库连接失败'}
# 2. 获取Git提交信息
commit_info = self.git_utils.get_commit_info()
logger.info(f"📝 提交消息: {commit_info['message'][:100]}")
logger.info(f"👤 提交作者: {commit_info['author']}")
logger.info(f"🔖 提交哈希: {commit_info['short_hash']}")
# 3. 检查是否重复
if self.check_duplicate_in_db(commit_info['hash']):
logger.info(f"⏭️ 版本记录已存在,跳过创建")
self.close_db()
return {
'success': True,
'duplicate': True,
'message': '版本记录已存在'
}
# 4. 分析版本信息
update_type = self.analyze_update_type(commit_info['message'])
next_version = self.calculate_next_version(update_type)
logger.info(f"📊 更新类型: {update_type.upper()}")
logger.info(f"🔢 新版本号: v{next_version}")
# 5. 获取提交统计
stats = self.git_utils.get_commit_stats()
logger.info(f"📈 代码统计: {stats['files_changed']} 文件, "
f"+{stats['lines_added']}/-{stats['lines_deleted']}")
# 6. 创建版本记录
version_record = {
'version': next_version,
'update_type': update_type,
'content': commit_info['message'],
'author': commit_info['author'],
'commit_hash': commit_info['hash'],
'commit_short_hash': commit_info['short_hash'],
'branch': commit_info['branch'],
'release_time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
'stats': stats
}
# 7. 保存到数据库主要存储CI/CD专用
logger.info("")
logger.info("💾 步骤1: 保存到PostgreSQL数据库...")
db_success = self.save_to_database(version_record)
# 8. 保存到本地JSON用户端可见
logger.info("💾 步骤2: 保存到本地JSON备份用户端可见...")
self.save_local_backup(version_record)
# 9. 更新config.py
logger.info("💾 步骤3: 更新config.py...")
self.update_config_version(next_version)
# 10. 关闭数据库连接
self.close_db()
# 11. 返回结果
logger.info("")
logger.info("=" * 70)
logger.info("✅ GUI版本记录创建完成")
logger.info("=" * 70)
logger.info(f"📦 版本号: v{next_version}")
logger.info(f"📂 类型: {update_type.upper()}")
logger.info(f"👤 作者: {commit_info['author']}")
logger.info(f"📈 变更: {stats['files_changed']} 文件, "
f"+{stats['lines_added']}/-{stats['lines_deleted']}")
logger.info(f"💾 数据库: {'✅ 成功' if db_success else '❌ 失败'}")
logger.info(f"💾 本地备份: ✅ 成功")
logger.info(f"💾 config.py: ✅ 成功")
logger.info("=" * 70)
logger.info("🔒 安全提示本脚本仅在CI/CD环境运行不会打包到用户端")
logger.info("=" * 70)
return {
'success': True,
'duplicate': False,
'version': next_version,
'update_type': update_type,
'author': commit_info['author'],
'content': commit_info['message'],
'stats': stats,
'db_saved': db_success
}
except Exception as e:
logger.error(f"❌ 创建版本记录失败: {e}")
import traceback
logger.error(traceback.format_exc())
if self.conn:
self.close_db()
return {
'success': False,
'error': str(e)
}
def main():
"""主函数"""
try:
logger.info("🔒 安全检查此脚本应仅在CI/CD环境运行")
logger.info(" 用户端GUI不应包含此脚本")
logger.info("")
manager = DatabaseVersionManager()
result = manager.create_version_record()
# 输出JSON结果供CI/CD使用
print(json.dumps(result, ensure_ascii=False, indent=2))
# 设置退出码
if not result['success']:
sys.exit(1)
except Exception as e:
logger.error(f"脚本执行失败: {e}")
print(json.dumps({
'success': False,
'error': str(e)
}, ensure_ascii=False))
sys.exit(1)
if __name__ == '__main__':
main()

View File

@@ -1,166 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
版本历史查看工具
用于查看和管理GUI客户端的版本历史记录
"""
import sys
import json
import argparse
from pathlib import Path
from datetime import datetime
PROJECT_ROOT = Path(__file__).resolve().parent.parent.parent
def load_version_history():
"""加载版本历史"""
version_file = PROJECT_ROOT / 'version_history.json'
if not version_file.exists():
return []
with open(version_file, 'r', encoding='utf-8') as f:
return json.load(f)
def display_version_list(limit: int = 10):
"""显示版本列表"""
history = load_version_history()
if not history:
print("📭 暂无版本历史记录")
return
print("=" * 80)
print(f"📦 GUI客户端版本历史 (共 {len(history)} 个版本)")
print("=" * 80)
for i, record in enumerate(history[:limit], 1):
version = record.get('version', 'Unknown')
update_type = record.get('update_type', 'unknown').upper()
author = record.get('author', 'Unknown')
release_time = record.get('release_time', 'Unknown')
content = record.get('content', '')[:60]
print(f"\n{i}. v{version} [{update_type}]")
print(f" 👤 作者: {author}")
print(f" 📅 时间: {release_time}")
print(f" 📝 内容: {content}{'...' if len(record.get('content', '')) > 60 else ''}")
stats = record.get('stats', {})
if stats:
print(f" 📊 变更: {stats.get('files_changed', 0)} 文件, "
f"+{stats.get('lines_added', 0)}/-{stats.get('lines_deleted', 0)}")
if len(history) > limit:
print(f"\n... 还有 {len(history) - limit} 个历史版本")
print("\n" + "=" * 80)
def display_version_detail(version: str):
"""显示特定版本的详细信息"""
history = load_version_history()
for record in history:
if record.get('version') == version:
print("=" * 80)
print(f"📦 GUI客户端版本详情: v{version}")
print("=" * 80)
print(f"版本号: v{record.get('version')}")
print(f"更新类型: {record.get('update_type', 'unknown').upper()}")
print(f"作者: {record.get('author')}")
print(f"发布时间: {record.get('release_time')}")
print(f"分支: {record.get('branch')}")
print(f"提交哈希: {record.get('commit_short_hash')}")
print(f"\n更新内容:\n{record.get('content')}")
stats = record.get('stats', {})
if stats:
print(f"\n提交统计:")
print(f" 文件变更: {stats.get('files_changed', 0)}")
print(f" 新增行数: +{stats.get('lines_added', 0)}")
print(f" 删除行数: -{stats.get('lines_deleted', 0)}")
print("=" * 80)
return
print(f"❌ 未找到版本: v{version}")
def display_latest_version():
"""显示最新版本"""
history = load_version_history()
if not history:
print("📭 暂无版本历史记录")
return
latest = history[0]
print(f"🏷️ 最新版本: v{latest.get('version')}")
print(f"📅 发布时间: {latest.get('release_time')}")
print(f"👤 发布者: {latest.get('author')}")
def export_changelog(output_file: str = None):
"""导出更新日志Markdown格式"""
history = load_version_history()
if not history:
print("📭 暂无版本历史记录")
return
output_file = output_file or (PROJECT_ROOT / 'CHANGELOG.md')
with open(output_file, 'w', encoding='utf-8') as f:
f.write("# 水滴GUI客户端更新日志\n\n")
f.write(f"*自动生成时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}*\n\n")
for record in history:
version = record.get('version')
update_type = record.get('update_type', 'patch').upper()
release_time = record.get('release_time')
author = record.get('author')
content = record.get('content', '')
f.write(f"## v{version} - {release_time}\n\n")
f.write(f"**类型**: {update_type} | **作者**: {author}\n\n")
f.write(f"{content}\n\n")
stats = record.get('stats', {})
if stats:
f.write(f"*变更统计: {stats.get('files_changed', 0)} 文件, "
f"+{stats.get('lines_added', 0)}/-{stats.get('lines_deleted', 0)} 行*\n\n")
f.write("---\n\n")
print(f"✅ 更新日志已导出到: {output_file}")
def main():
"""主函数"""
parser = argparse.ArgumentParser(description='GUI版本历史查看工具')
parser.add_argument('--list', '-l', action='store_true', help='显示版本列表')
parser.add_argument('--limit', type=int, default=10, help='显示数量限制默认10')
parser.add_argument('--detail', '-d', type=str, help='查看特定版本详情')
parser.add_argument('--latest', action='store_true', help='显示最新版本')
parser.add_argument('--export', '-e', action='store_true', help='导出更新日志')
parser.add_argument('--output', '-o', type=str, help='导出文件路径')
args = parser.parse_args()
if args.latest:
display_latest_version()
elif args.detail:
display_version_detail(args.detail)
elif args.export:
export_changelog(args.output)
elif args.list or not any(vars(args).values()):
display_version_list(args.limit)
else:
parser.print_help()
if __name__ == '__main__':
main()

View File

@@ -1,181 +0,0 @@
name: GUI Version Release
on:
push:
branches: [ master ]
jobs:
gui-version-release:
runs-on: windows
defaults:
run:
working-directory: E:\shuidrop_gui
steps:
# Step 1: Clone repository manually
- name: Clone repository
shell: powershell
run: |
Write-Host "Cloning repository..."
# Change to workspace directory
cd E:\shuidrop_gui
Write-Host "Current directory: $(Get-Location)"
Write-Host "Git status:"
git status
Write-Host "Fetching latest changes..."
git fetch origin
git reset --hard origin/master
Write-Host "Repository ready"
# Step 2: Check Python environment
- name: Check Python environment
shell: powershell
run: |
Write-Host "Python version:"
try {
python --version
} catch {
Write-Host "Python not installed"
}
Write-Host "Pip version:"
try {
pip --version
} catch {
Write-Host "Pip not installed"
}
# Step 3: Install dependencies
- name: Install dependencies
shell: powershell
run: |
Write-Host "Installing psycopg2-binary..."
python -m pip install --upgrade pip
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to upgrade pip"
exit 1
}
pip install psycopg2-binary
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to install psycopg2-binary"
exit 1
}
Write-Host "Dependencies installed successfully"
# Step 4: Create GUI version record
- name: Create version record
id: create_version
shell: powershell
run: |
Write-Host "Starting GUI version release process..."
Write-Host "Commit hash: ${{ github.sha }}"
Write-Host "Commit author: ${{ github.actor }}"
Write-Host "Branch: ${{ github.ref_name }}"
# Retry mechanism: maximum 3 attempts
$SUCCESS = $false
$VERSION = ""
for ($i = 1; $i -le 3; $i++) {
Write-Host "Attempt $i to create version record..."
$output = python .gitea/scripts/gui_version_creator.py | Out-String
if ($LASTEXITCODE -eq 0) {
Write-Host "Version record created successfully"
$SUCCESS = $true
# Extract version from JSON output (last line)
$jsonLine = ($output -split "`n" | Where-Object { $_ -match '^\{' } | Select-Object -Last 1)
if ($jsonLine) {
$json = $jsonLine | ConvertFrom-Json
$VERSION = $json.version
Write-Host "Created version: $VERSION"
}
break
} else {
Write-Host "Attempt $i failed"
if ($i -eq 3) {
Write-Host "All 3 attempts failed"
} else {
Write-Host "Waiting 5 seconds before retry..."
Start-Sleep -Seconds 5
}
}
}
if (-not $SUCCESS) {
Write-Host "Version creation failed"
exit 1
}
# Set version for next steps
echo "VERSION=$VERSION" >> $env:GITHUB_ENV
env:
DB_HOST: 8.155.9.53
DB_NAME: ai_web
DB_USER: user_emKCAb
DB_PASSWORD: password_ee2iQ3
DB_PORT: 5400
# Step 5: Commit changes back to repository
- name: Commit version changes
if: success()
shell: powershell
run: |
Write-Host "Committing version changes..."
Write-Host "New version: $env:VERSION"
git config user.name "Gitea Actions"
git config user.email "actions@gitea.local"
git add config.py
git add version_history.json
$hasChanges = git diff --staged --quiet
if ($LASTEXITCODE -ne 0) {
git commit -m "[skip ci] Update version to $env:VERSION"
git push origin master
Write-Host "Version changes committed and pushed"
} else {
Write-Host "No changes to commit"
}
# Step 6: Display summary
- name: Display summary
if: always()
shell: powershell
run: |
$summaryFile = $env:GITHUB_STEP_SUMMARY
if ($summaryFile) {
"## GUI Version Release Summary" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
"" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
if ("${{ job.status }}" -eq "success") {
"**Status**: Version release succeeded" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
} else {
"**Status**: Version release failed" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
}
"**Author**: ${{ github.actor }}" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
"**Branch**: ${{ github.ref_name }}" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
"**Time**: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
"**Commit**: ${{ github.sha }}" | Out-File -FilePath $summaryFile -Append -Encoding UTF8
} else {
Write-Host "=========================================="
Write-Host "GUI Version Release Summary"
Write-Host "=========================================="
Write-Host "Author: ${{ github.actor }}"
Write-Host "Branch: ${{ github.ref_name }}"
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "Commit: ${{ github.sha }}"
Write-Host "Status: ${{ job.status }}"
Write-Host "=========================================="
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -40,7 +40,7 @@ from Utils.message_models import PlatformMessage
class Track:
"""滑块轨迹生成类"""
@staticmethod
def get_track(distance):
distance = int(distance) + random.randint(7, 11) + 0.8
@@ -853,7 +853,7 @@ class Track:
class ImgDistance:
"""滑块图像识别距离计算类"""
def __init__(self, bg, tp):
self.bg = bg
self.tp = tp
@@ -999,7 +999,8 @@ class ImgDistance:
save_path=save_path
)
# AutiContent类已移除 - 后端会提供所有必要的anti_content
# AutiContent类已移除 - 后端会提供所有必要的anti_content
def gzip_compress(self, data):
"""压缩数据"""
@@ -1010,7 +1011,7 @@ class ImgDistance:
return result
except Exception as e:
logger.error(f"调用gzipCompress函数失败: {e}")
# fallback处理简单返回原数据
logger.warning("使用fallback处理返回原数据")
return data
@@ -1018,7 +1019,7 @@ class ImgDistance:
class EncryptTool:
"""加密工具类"""
@staticmethod
def aes_encrypt(text, key, iv):
cipher = AES.new(key.encode('utf-8'), AES.MODE_CBC, iv.encode('utf-8'))
@@ -1058,7 +1059,7 @@ class EncryptTool:
class PddLogin:
"""拼多多登录核心类"""
def __init__(self, log_callback=None):
super().__init__()
self.login_api = "https://mms.pinduoduo.com/janus/api/auth"
@@ -1604,14 +1605,14 @@ class PddLogin:
return result
# 发送验证码通知给后端,并获取验证码
def request_verification_code(self, username, store_id, backend_anti_content, phone_number=None):
def request_verification_code(self, username, store_id, backend_anti_content):
"""向后端请求获取手机验证码"""
self._log(f"开始请求验证码,用户名: {username}, 店铺ID: {store_id}, 手机号: {phone_number}", "INFO")
self._log(f"开始请求验证码,用户名: {username}, 店铺ID: {store_id}", "INFO")
# 使用后端下发的anti_content
anti_content = backend_anti_content
self._log(f"使用后端下发的anti_content", "DEBUG")
self.headers["anti-content"] = anti_content
url = "https://mms.pinduoduo.com/janus/api/user/getLoginVerificationCode"
payload = {
@@ -1620,34 +1621,33 @@ class PddLogin:
}
response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies)
self._log(f"发送验证码请求结果: {response.text}")
# 发送消息给后端,告知需要验证码(包含手机号)
# 发送消息给后端,告知需要验证码
self._log("准备向后端发送验证码需求通知", "INFO")
self._send_verification_needed_message(store_id, phone_number)
self._send_verification_needed_message(store_id)
# 这里需要等待后端重新下发包含验证码的登录参数
# 实际实现中这个方法会在接收到新的登录参数后被重新调用
return None
def _send_verification_needed_message(self, store_id, phone_number=None):
def _send_verification_needed_message(self, store_id):
"""向后端发送需要验证码的通知"""
try:
self._log(f"开始发送验证码需求通知店铺ID: {store_id}, 手机号: {phone_number}", "INFO")
self._log(f"开始发送验证码需求通知店铺ID: {store_id}", "INFO")
from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client()
self._log(f"获取到后端客户端: {backend is not None}", "DEBUG")
if backend:
message = {
"type": "connect_message",
"store_id": store_id,
"status": False,
"content": "需要验证码",
"phone_number": phone_number
"content": "需要验证码"
}
self._log(f"准备发送验证码通知消息: {message}", "DEBUG")
backend.send_message(message)
self._log("✅ 成功向后端发送验证码需求通知(含手机号)", "SUCCESS")
self._log("✅ 成功向后端发送验证码需求通知", "SUCCESS")
else:
self._log("❌ 后端客户端为空,无法发送验证码需求通知", "ERROR")
except Exception as e:
@@ -1661,7 +1661,7 @@ class PddLogin:
self._log(f"开始发送验证码错误通知店铺ID: {store_id}, 错误: {error_msg}", "INFO")
from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client()
if backend:
message = {
"type": "connect_message",
@@ -1685,13 +1685,12 @@ class PddLogin:
self._log(f"开始发送登录成功通知店铺ID: {store_id}", "INFO")
from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client()
if backend:
message = {
"type": "connect_message",
"store_id": store_id,
"status": True, # 登录成功
"cookies": self.cookies # 🔥 新增添加登录生成的cookie信息
"status": True # 登录成功
}
self._log(f"准备发送登录成功消息: {message}", "DEBUG")
backend.send_message(message)
@@ -1709,7 +1708,7 @@ class PddLogin:
self._log(f"开始发送登录失败通知店铺ID: {store_id}, 错误: {error_msg}", "INFO")
from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client()
if backend:
message = {
"type": "connect_message",
@@ -1732,12 +1731,10 @@ class PddLogin:
self._log("🚀 [PddLogin] 开始使用参数登录", "INFO")
# 检查验证码字段(兼容 code 和 verification_code
verification_code = login_params.get("verification_code") or login_params.get("code", "")
self._log(
f"📋 [PddLogin] 登录参数: username={login_params.get('username', 'N/A')}, 包含验证码={bool(verification_code)}",
"DEBUG")
self._log(f"📋 [PddLogin] 登录参数: username={login_params.get('username', 'N/A')}, 包含验证码={bool(verification_code)}", "DEBUG")
self.headers["anti-content"] = login_params.get("anti_content", "")
# 直接使用后端提供的参数构建登录请求
ts = login_params.get("timestamp", int(round(time.time() * 1000)))
payload = {
@@ -1803,12 +1800,12 @@ class PddLogin:
self._log(f"🔍 [Debug] 登录请求URL: {self.login_api}", "DEBUG")
self._log(f"🔍 [Debug] 登录请求payload: {payload}", "DEBUG")
self._log(f"🔍 [Debug] 登录请求headers: {dict(self.headers)}", "DEBUG")
response = requests.post(self.login_api, headers=self.headers, json=payload, cookies=self.cookies)
self.cookies.update(response.cookies.get_dict())
self._log(f"登录响应状态码: {response.status_code}")
self._log(f"登录响应内容: {response.text}")
# 检查响应内容
if "需要手机验证" in response.text:
self._log("✅ 检测到需要手机验证的响应", "INFO")
@@ -1822,8 +1819,7 @@ class PddLogin:
salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值
pictures = self.obtain_captcha() # 获取验证码图片
distance = round((ImgDistance(bg=pictures[0], tp=pictures[1]).main() * (272 / 320)) + (48.75 / 2),
2) # 计算距离
distance = round((ImgDistance(bg=pictures[0], tp=pictures[1]).main() * (272 / 320)) + (48.75 / 2), 2) # 计算距离
track_list = Track.get_track(distance=distance) # 生成轨迹
captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数
@@ -1837,8 +1833,7 @@ class PddLogin:
success_count += 1
# 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False
if success_count < 8:
return self.login_with_params(login_params=login_params, store_id=store_id,
success_count=success_count)
return self.login_with_params(login_params=login_params, store_id=store_id, success_count=success_count)
else:
return False
else:
@@ -1858,7 +1853,7 @@ class PddLogin:
response_data = response.json()
error_msg = response_data.get('errorMsg', '验证码验证失败')
self._log(f"服务器返回错误: {error_msg}", "WARNING")
# 不要重新发送验证码请求,直接报告验证失败
self._send_verification_error_message(store_id, error_msg) # 直接使用官方错误信息
return "verification_code_error" # 返回特殊状态,避免重复发送消息
@@ -1868,21 +1863,9 @@ class PddLogin:
username = login_params.get("username")
backend_anti_content = login_params.get("anti_content")
self._log(f"为用户 {username} 发送验证码使用后端anti_content", "INFO")
# 🔥 从响应中提取手机号
phone_number = None
try:
response_json = response.json()
phone_number = response_json.get("result") # 手机号在result字段中
if phone_number and isinstance(phone_number, str):
self._log(f"🔍 从登录响应中提取到手机号: {phone_number}", "SUCCESS")
else:
self._log("⚠️ 响应中的result字段不包含有效手机号", "WARNING")
except Exception as e:
self._log(f"❌ 提取手机号时出错: {e}", "DEBUG")
# 传递后端下发的anti_content和手机号
self.request_verification_code(username, store_id, backend_anti_content, phone_number)
# 传递后端下发的anti_content
self.request_verification_code(username, store_id, backend_anti_content)
return "need_verification_code"
else:
@@ -1900,7 +1883,6 @@ class PddLogin:
self._send_login_failure_message(store_id, error_msg)
return "login_failure" # 返回特殊状态,避免重复发送消息
# ===== 登录相关类集成结束 =====
@@ -1988,13 +1970,13 @@ class ChatPdd:
raise FileNotFoundError(f"找不到必需的JS文件: {file_path}")
return True
@staticmethod
def _get_resource_path(relative_path):
"""获取资源文件的绝对路径兼容PyInstaller打包环境"""
try:
print(f"[DEBUG] 正在解析资源路径: {relative_path}")
# PyInstaller环境下的基础路径
if hasattr(sys, '_MEIPASS'):
# PyInstaller 临时目录
@@ -2010,10 +1992,10 @@ class ChatPdd:
# 向上两级目录到项目根目录
base_path = os.path.dirname(os.path.dirname(base_path))
print(f"[DEBUG] 开发环境,计算的项目根目录: {base_path}")
resource_path = os.path.join(base_path, relative_path)
print(f"[DEBUG] 拼接后的完整资源路径: {resource_path}")
# 检查路径是否存在
if os.path.exists(resource_path):
print(f"[DEBUG] ✅ 资源路径存在: {resource_path}")
@@ -2030,7 +2012,7 @@ class ChatPdd:
print(f"[DEBUG] 📄 {item}")
except Exception as e:
print(f"[DEBUG] 无法列出目录内容: {e}")
return resource_path
except Exception as e:
print(f"[ERROR] 获取资源路径失败: {e}")
@@ -2555,58 +2537,6 @@ class ChatPdd:
traceback.print_exc()
return False
def should_filter_robot_message(self, message_data):
"""专门判断是否为机器人消息需要过滤"""
try:
message_info = message_data.get("message", {})
# 1. 基于消息类型过滤机器人特殊消息
msg_type = message_info.get("type")
if msg_type == 31: # 机器人干预消息(如:机器人已暂停接待)
return True
# 2. 基于模板名称识别机器人消息
template_name = message_info.get("template_name", "")
robot_templates = [
"mall_robot_man_intervention_and_restart", # 机器人暂停接待消息
"mall_robot_text_msg", # 机器人自动回复消息
# 可根据实际情况添加更多机器人模板
]
if template_name in robot_templates:
return True
# 3. 基于机器人特殊标志过滤
if message_info.get("conv_silent") is True: # 静默会话标志
return True
if message_info.get("no_unreply_hint") == 1: # 无需回复提示标志
return True
# 4. 基于消息内容识别机器人提示消息
content = message_info.get("content", "")
robot_content_patterns = [
"机器人未找到对应的回复",
"机器人已暂停接待",
">>点此【立即恢复接待】<<",
"点击添加",
"[当前用户来自",
]
if any(pattern in content for pattern in robot_content_patterns):
return True
# 5. 基于biz_context中的机器人标识
biz_context = message_info.get("biz_context", {})
if biz_context.get("robot_msg_id"): # 有机器人消息ID
return True
# 不是机器人消息,不过滤
return False
except Exception as e:
self._log(f"判断机器人消息时出错: {e}", "DEBUG")
return False # 出错时不过滤,保持原有行为
async def handle_customer_message(self, message_data):
"""处理来自后端的客服消息"""
self._log("收到来自后端的客服消息", "INFO")
@@ -2629,11 +2559,6 @@ class ChatPdd:
async def process_incoming_message(self, message_data, wss, store):
"""处理接收到的消息"""
try:
# 🔥 过滤机器人消息
if self.should_filter_robot_message(message_data):
self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG")
return
message_info = message_data.get("message", {})
if not message_info:
return
@@ -2806,28 +2731,6 @@ class ChatPdd:
except Exception as e:
self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR")
# 🔥 新增Cookie登录成功后发送登录成功报告与登录参数模式保持一致
try:
if self.backend_service and hasattr(self, 'store_id') and self.store_id:
# 构建cookie字典从cookies_str解析
cookie_dict = {}
if hasattr(self, 'cookie') and self.cookie:
cookie_dict = self.cookie
message = {
"type": "connect_message",
"store_id": self.store_id,
"status": True, # 登录成功
"cookies": cookie_dict # 添加cookie信息
}
# 🔥 修复:使用正确的方法名 send_message_to_backend
await self.backend_service.send_message_to_backend(message)
self._log("✅ [PDD] 已向后端发送Cookie登录成功报告", "SUCCESS")
else:
self._log("⚠️ [PDD] 无法发送登录成功报告backend_service或store_id缺失", "WARNING")
except Exception as e:
self._log(f"⚠️ [PDD] 发送登录成功报告失败: {e}", "WARNING")
# 启动消息监听和心跳
await asyncio.gather(
self.heartbeat(websocket),
@@ -2986,11 +2889,11 @@ class PddListenerForGUI:
self._log("🔄 [PDD] 开始创建PddLogin实例", "DEBUG")
pdd_login = PddLogin(log_callback=self.log_callback)
self._log("✅ [PDD] PddLogin实例创建成功", "DEBUG")
self._log("🔄 [PDD] 开始执行登录", "DEBUG")
login_result = pdd_login.login_with_params(params_dict, store_id)
self._log(f"📊 [PDD] 登录结果: {login_result}", "DEBUG")
if login_result == "need_verification_code":
self._log("⚠️ [PDD] 需要手机验证码,已通知后端,等待重新下发包含验证码的登录参数", "WARNING")
return "need_verification_code" # 返回特殊标识,避免被覆盖
@@ -3006,7 +2909,7 @@ class PddListenerForGUI:
elif isinstance(login_result, dict):
# 登录成功获取到cookies
self._log("✅ [PDD] 登录成功使用获取的cookies连接平台", "SUCCESS")
# 将cookies字典转换为字符串格式与原有逻辑兼容
import json
cookies_str = json.dumps(login_result)
@@ -3107,7 +3010,6 @@ class PddListenerForGUI:
cookies_str=cookies,
store=store
)
self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS")
return True
except Exception as e:
@@ -3153,6 +3055,8 @@ class PddListenerForGUI:
self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR")
return {}
def get_status(self) -> Dict[str, Any]:
return {
"running": self.running,

3160
Utils/Pdd/PddUtilsOld.py Normal file

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -1 +1 @@
D:/GUI_main/Utils/PythonNew32/Log/SaiNiu_202509180015.log
D:/GUI_main/Utils/PythonNew32/Log/SaiNiu_202509191651.log

View File

@@ -1,12 +1,12 @@
=== SaiNiu DLL 启动脚本开始 ===
Python版本: 3.13.5 (tags/v3.13.5:6cb20a2, Jun 11 2025, 15:56:57) [MSC v.1943 32 bit (Intel)]
当前工作目录: D:\GUI_main\Utils\PythonNew32
时间戳: 2025-09-17 18:52:56
时间戳: 2025-09-19 16:44:51
正在加载 SaiNiuApi.dll...
[OK] SaiNiuApi.dll 加载成功
[OK] DLL函数类型定义完成
正在启动服务器 - 端口: 3030
[OK] Access_ServerStart 服务器启动结果: {"code":200,"msg":"调用成功","app_v":"3.0.9.0","passkey":"1758106377811"}
[OK] Access_ServerStart 服务器启动结果: {"code":200,"msg":"调用成功","app_v":"3.0.9.0","passkey":"1758271492787"}
=== DLL服务启动完成进入监控模式 ===
[HEARTBEAT] DLL服务心跳 #1 - 服务正常运行中...
[HEARTBEAT] DLL服务心跳 #2 - 服务正常运行中...

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -29,8 +29,6 @@ class BackendClient:
self.error_callback: Optional[Callable] = None
self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.token_error_callback: Optional[Callable] = None # 新增token错误回调
self.version_callback: Optional[Callable] = None # 新增:版本检查回调
self.is_connected = False
@@ -101,15 +99,15 @@ class BackendClient:
ping_timeout=WS_PING_TIMEOUT, # 可配置ping超时
close_timeout=10, # 10秒关闭超时
# 增加TCP keepalive配置
max_size=2 ** 20, # 1MB最大消息大小
max_queue=32, # 最大队列大小
max_size=2**20, # 1MB最大消息大小
max_queue=32, # 最大队列大小
compression=None # 禁用压缩以提高性能
)
print(f"[连接] 已启用心跳ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s")
else:
self.websocket = await websockets.connect(
self.url,
max_size=2 ** 20,
max_size=2**20,
max_queue=32,
compression=None
)
@@ -119,13 +117,13 @@ class BackendClient:
self.reconnect_attempts = 0 # 重置重连计数
self.is_reconnecting = False
print("后端WebSocket连接成功")
# 等待连接稳定后再发送状态通知
await asyncio.sleep(0.5)
# 发送连接状态通知给后端
self._notify_connection_status(True)
self.on_connected()
# 消息循环
@@ -145,7 +143,7 @@ class BackendClient:
except websockets.ConnectionClosed as e:
self.is_connected = False
self._notify_connection_status(False) # 通知断开
# 详细分析断开原因
if e.code == 1006:
print(f"[重连] WebSocket异常关闭 (1006): 可能是心跳超时或网络问题")
@@ -155,7 +153,7 @@ class BackendClient:
print(f"[重连] WebSocket关闭 (1001): 端点离开")
else:
print(f"[重连] WebSocket关闭 ({e.code}): {e.reason}")
self._handle_connection_closed(e)
if not await self._should_reconnect():
break
@@ -263,9 +261,7 @@ class BackendClient:
close: Callable = None,
error: Callable = None,
login: Callable = None,
success: Callable = None,
token_error: Callable = None,
version: Callable = None):
success: Callable = None):
"""设置各种消息类型的回调函数"""
if store_list:
self.store_list_callback = store_list
@@ -283,10 +279,6 @@ class BackendClient:
self.login_callback = login
if success:
self.success_callback = success
if token_error:
self.token_error_callback = token_error
if version:
self.version_callback = version
def on_connected(self):
"""连接成功时的处理"""
@@ -341,14 +333,14 @@ class BackendClient:
try:
if not self.loop:
return
# 获取当前活跃平台的store_id
active_store_id = None
try:
from Utils.JD.JdUtils import WebsocketManager as JdManager
from Utils.Dy.DyUtils import DouYinWebsocketManager as DyManager
from Utils.Dy.DyUtils import WebsocketManager as DyManager
from Utils.Pdd.PddUtils import WebsocketManager as PddManager
# 检查各平台是否有活跃连接
for mgr_class, platform_name in [(JdManager, "京东"), (DyManager, "抖音"), (PddManager, "拼多多")]:
try:
@@ -369,30 +361,30 @@ class BackendClient:
continue
except Exception as e:
print(f"[状态] 获取活跃平台信息失败: {e}")
status_message = {
"type": "connection_status",
"status": connected,
"timestamp": int(time.time()),
"client_uuid": self.uuid
}
# 如果有活跃平台添加store_id
if active_store_id:
status_message["store_id"] = active_store_id
print(f"[状态] 添加store_id到状态消息: {active_store_id}")
else:
print(f"[状态] 未检测到活跃平台不添加store_id")
# 异步发送状态通知
asyncio.run_coroutine_threadsafe(
self._send_to_backend(status_message),
self.loop
)
status_text = "连接" if connected else "断开"
print(f"[状态] 已通知后端GUI客户端{status_text}")
except Exception as e:
print(f"[状态] 发送状态通知失败: {e}")
import traceback
@@ -437,12 +429,8 @@ class BackendClient:
self._handle_login(message)
elif msg_type == 'error':
self._handle_error_message(message)
elif msg_type == 'error_token':
self._handle_token_error(message)
elif msg_type == 'staff_list':
self._handle_staff_list(message)
elif msg_type == 'version_response': # 新增:版本检查响应
self._handle_version_response(message)
else:
print(f"未知消息类型: {msg_type}")
@@ -1157,14 +1145,12 @@ class BackendClient:
content = message.get('content', '')
data = message.get('data', {})
# 🔥 判断是登录参数模式还是普通Cookie模式(支持拼多多和抖音)
if (platform_name in ["拼多多", "抖音"] and
(("拼多多登录" in content and data.get('login_params')) or
("抖音登录" in content and data.get('login_flow')))):
# 登录参数模式 - 传递完整的消息JSON给处理器
print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
# 判断是拼多多登录参数还是普通Cookie
if platform_name == "拼多多" and ("拼多多登录" in content) and data.get('login_params'):
# 拼多多登录参数模式 - 传递完整的消息JSON给处理器
print(f"收到拼多多登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
if self.login_callback:
# 传递完整的JSON消息让处理器来解析登录参数
# 传递完整的JSON消息拼多多处理器来解析login_params
import json
full_message = json.dumps(message)
self.login_callback(platform_name, store_id, full_message)
@@ -1177,35 +1163,10 @@ class BackendClient:
def _handle_error_message(self, message: Dict[str, Any]):
"""处理错误消息"""
error_msg = message.get('error', '未知错误')
content = message.get('content', '')
# 检查是否为token错误无论type是error还是error_token
if content == "无效的exe_token" or "无效的exe_token" in content:
print(f"[错误] 检测到token错误: {content}")
self._handle_token_error(message)
return
print(f"后端连接错误: {error_msg}")
if self.error_callback:
self.error_callback(error_msg, message)
def _handle_token_error(self, message: Dict[str, Any]):
"""处理token错误消息 - 无效token时停止重连并显示错误"""
error_content = message.get('content', '无效的exe_token')
print(f"[错误] Token验证失败: {error_content}")
# 停止重连机制
self.should_stop = True
self.is_reconnecting = False
# 触发token错误回调
if self.token_error_callback:
self.token_error_callback(error_content)
# 主动关闭连接
if self.websocket:
asyncio.run_coroutine_threadsafe(self.websocket.close(), self.loop)
def _handle_staff_list(self, message: Dict[str, Any]):
"""处理客服列表更新消息"""
staff_list = message.get('data', {}).get('staff_list', [])
@@ -1219,16 +1180,6 @@ class BackendClient:
if self.customers_callback: # 假设客服列表更新也触发客服列表回调
self.customers_callback(staff_list, store_id)
def _handle_version_response(self, message: Dict[str, Any]):
"""处理版本检查响应"""
latest_version = message.get('latest_version')
download_url = message.get('download_url')
print(f"收到版本检查响应: 最新版本={latest_version}, 下载地址={download_url}")
if self.version_callback:
self.version_callback(message)
# ==================== 辅助方法 ====================
def set_token(self, token: str):

View File

@@ -0,0 +1,982 @@
# WebSocket/BackendClient.py
import json
import threading
import websockets
import uuid
import asyncio
from typing import List, Dict, Any, Optional, Callable
from config import get_gui_ws_url
class BackendClient:
"""后端WebSocket客户端"""
def __init__(self, url: str, token: str = None):
self.token = token
self.uuid = str(uuid.uuid4())
self.url = url
# 消息处理回调函数
self.store_list_callback: Optional[Callable] = None
self.customers_callback: Optional[Callable] = None
self.message_callback: Optional[Callable] = None
self.transfer_callback: Optional[Callable] = None
self.close_callback: Optional[Callable] = None
self.error_callback: Optional[Callable] = None
self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.is_connected = False
def connect(self):
"""连接到WebSocket服务器"""
if self.is_connected:
return
self.thread = threading.Thread(target=self._run_loop, daemon=True)
self.thread.start()
def disconnect(self):
"""断开WebSocket连接"""
if self.loop and self.loop.is_running():
asyncio.run_coroutine_threadsafe(self._close(), self.loop)
if self.thread and self.thread.is_alive():
self.thread.join(timeout=3)
self.is_connected = False
async def _close(self):
"""异步关闭连接"""
if self.websocket:
await self.websocket.close()
self.is_connected = False
def _run_loop(self):
"""在新线程中运行事件循环"""
self.loop = asyncio.new_event_loop()
asyncio.set_event_loop(self.loop)
try:
self.loop.run_until_complete(self._connect_and_listen())
except Exception as e:
print(f"WebSocket异常: {e}")
finally:
self.loop.close()
async def _connect_and_listen(self):
"""连接并监听消息"""
try:
self.websocket = await websockets.connect(self.url)
self.is_connected = True
self.on_connected()
async for message in self.websocket:
try:
# 打印原始文本帧与长度
try:
raw_len = len(message.encode('utf-8')) if isinstance(message, str) else len(message)
print(f"后端发送消息体内容:{message}")
except Exception:
pass
data = json.loads(message)
self.on_message_received(data)
except json.JSONDecodeError:
print(f"JSON解析错误: {message}")
except Exception as e:
self.is_connected = False
self.on_error(str(e))
@classmethod
def from_exe_token(cls, exe_token: str):
"""使用 exe_token 构造单连接客户端ws/gui/<token>/"""
url = get_gui_ws_url(exe_token)
return cls(url=url, token=exe_token)
def set_callbacks(self,
store_list: Callable = None,
customers: Callable = None,
message: Callable = None,
transfer: Callable = None,
close: Callable = None,
error: Callable = None,
login: Callable = None,
success: Callable = None):
"""设置各种消息类型的回调函数"""
if store_list:
self.store_list_callback = store_list
if customers:
self.customers_callback = customers
if message:
self.message_callback = message
if transfer:
self.transfer_callback = transfer
if close:
self.close_callback = close
if error:
self.error_callback = error
if login:
self.login_callback = login
if success:
self.success_callback = success
def on_connected(self):
"""连接成功时的处理"""
print("后端WebSocket连接成功")
# 不再主动请求 get_store避免与后端不兼容导致协程未完成
def on_message_received(self, message: Dict[str, Any]):
"""处理接收到的消息 - 根据WebSocket文档v2更新"""
# 统一打印后端下发的完整消息结构体
try:
import json as _json
print("=== Backend -> GUI Message ===")
print(_json.dumps(message, ensure_ascii=False, indent=2))
print("=== End Message ===")
except Exception:
pass
msg_type = message.get('type')
try:
if msg_type == 'get_store':
self._handle_store_list(message)
elif msg_type == 'get_customers':
self._handle_customers_list(message)
elif msg_type == 'success':
print("后端连接服务成功")
# 可在此触发上层UI通知
if self.success_callback:
try:
self.success_callback()
except Exception:
pass
elif msg_type == 'message':
self._handle_message(message)
elif msg_type == 'transfer':
self._handle_transfer(message)
elif msg_type == 'close':
self._handle_close(message)
elif msg_type == 'pong':
self._handle_pong(message)
elif msg_type == 'connect_success': # 兼容旧版
self._handle_connect_success(message)
elif msg_type == 'login': # 新版后台下发平台cookies
self._handle_login(message)
elif msg_type == 'error':
self._handle_error_message(message)
elif msg_type == 'staff_list':
self._handle_staff_list(message)
else:
print(f"未知消息类型: {msg_type}")
except Exception as e:
error_msg = f"处理消息异常: {e}"
print(error_msg)
if self.error_callback:
self.error_callback(error_msg, message)
def on_error(self, error: str):
"""错误处理"""
print(f"后端连接错误: {error}")
if self.error_callback:
self.error_callback(error, None)
# ==================== 发送消息方法 ====================
def send_message(self, message: Dict[str, Any]):
"""
发送消息到后端
Args:
message: 要发送的消息字典
"""
if not self.is_connected or not self.loop:
raise Exception("WebSocket未连接")
future = asyncio.run_coroutine_threadsafe(
self._send_to_backend(message), self.loop
)
return future.result(timeout=8)
async def _send_to_backend(self, message: Dict[str, Any]):
"""异步发送消息到后端"""
if not self.websocket:
raise Exception("WebSocket连接不存在")
import json
message_str = json.dumps(message, ensure_ascii=False)
await self.websocket.send(message_str)
print(f"发送消息到后端: {message}")
def send_ping(self, custom_uuid: str = None, custom_token: str = None):
"""
发送心跳包
如果接收到关闭的消息后心跳包要带上token
"""
ping_message = {
'type': 'ping',
'uuid': custom_uuid or self.uuid
}
token = custom_token or self.token
if token:
ping_message['token'] = token
return self.send_message(ping_message)
def get_store(self):
"""获取店铺信息"""
message = {
'type': 'get_store',
'token': self.token or ''
}
return self.send_message(message)
def send_text_message(self, content: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送文本消息 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': content,
'msg_type': 'text',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_image_message(self, image_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送图片消息 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': image_url,
'msg_type': 'image',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_video_message(self, video_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送视频消息 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': video_url,
'msg_type': 'video',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_order_card(self, product_id: str, order_number: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送订单卡片 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': f'商品id{product_id} 订单号:{order_number}',
'msg_type': 'order_card',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_product_card(self, product_url: str, sender_id: str, store_id: str, pin_image: str = None):
"""发送商品卡片 - 根据WebSocket文档v2更新"""
message = {
'type': 'message',
'content': product_url,
'msg_type': 'product_card',
'sender': {'id': sender_id},
'store_id': store_id
}
if pin_image:
message['pin_image'] = pin_image
return self.send_message(message)
def send_staff_list(self, staff_list: List[Dict], store_id: str):
"""发送客服列表 - 根据WebSocket文档v2更新"""
message = {
'type': 'staff_list',
'content': '客服列表更新',
'data': {'staff_list': staff_list},
'store_id': store_id
}
return self.send_message(message)
# 保持向后兼容的旧方法(标记为已废弃)
def send_file_message(self, content: str, uid: str, pin: str, store_id: str):
"""发送文件消息 - 已废弃请使用send_video_message或send_image_message"""
print("警告: send_file_message已废弃请根据文件类型使用send_video_message或send_image_message")
# 尝试自动检测文件类型
content_lower = content.lower()
if any(ext in content_lower for ext in ['.mp4', '.avi', '.mov', '.wmv', '.flv']):
return self.send_video_message(content, uid, store_id)
elif any(ext in content_lower for ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp']):
return self.send_image_message(content, uid, store_id)
else:
# 默认作为文本消息发送
return self.send_text_message(content, uid, store_id)
def send_transfer(self, customer: str, pin: str, store_id: str):
"""发送转接消息"""
message = {
'type': 'transfer',
'customer': customer, # 客服名称
'pin': pin, # 顾客名称
'store_id': store_id
}
return self.send_message(message)
def close_store(self, store_id: str):
"""关闭店铺"""
message = {
'type': 'close',
'store_id': store_id
}
return self.send_message(message)
# ==================== 消息处理方法 ====================
def _handle_store_list(self, message: Dict[str, Any]):
"""处理店铺列表"""
store_list = message.get('store_list', [])
print(f"获取到{len(store_list)}个店铺:")
for store in store_list:
merchant_name = store.get('merchant_name', '')
store_id = store.get('store_id', '')
store_platform = store.get('store_platform', '')
print(f" - {merchant_name} (ID: {store_id}, 平台: {store_platform})")
if self.store_list_callback:
self.store_list_callback(store_list)
def _handle_customers_list(self, message: Dict[str, Any]):
"""处理客服列表"""
customers = message.get('customers', [])
store_id = message.get('store_id', '')
print(f"店铺{store_id}的客服列表,共{len(customers)}个客服:")
for customer in customers:
pin = customer.get('pin', '')
nickname = customer.get('nickname', '')
print(f" - {nickname} ({pin})")
if self.customers_callback:
self.customers_callback(customers, store_id)
def _handle_message(self, message: Dict[str, Any]):
"""处理消息"""
store_id = message.get('store_id', '')
data = message.get('data')
content = message.get('content', '')
print(f"[{store_id}] [{message.get('msg_type', 'unknown')}] : {content}")
# 尝试将后端AI/客服回复转发到对应平台
try:
receiver = message.get('receiver') or (data.get('receiver') if isinstance(data, dict) else None) or {}
recv_pin = receiver.get('id')
if recv_pin and store_id:
# 根据store_id动态确定平台类型
platform_type = self._get_platform_by_store_id(store_id)
if platform_type == "京东":
self._forward_to_jd(store_id, recv_pin, content)
elif platform_type == "抖音":
self._forward_to_douyin(store_id, recv_pin, content)
elif platform_type == "千牛":
self._forward_to_qianniu(store_id, recv_pin, content)
elif platform_type == "拼多多":
self._forward_to_pdd(store_id, recv_pin, content)
else:
print(f"[Forward] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
except Exception as e:
print(f"转发到平台失败: {e}")
if self.message_callback:
self.message_callback(data if data else message, store_id)
def _get_platform_by_store_id(self, store_id: str) -> str:
"""根据店铺ID获取平台类型"""
try:
# 从WebSocket管理器获取平台信息
from WebSocket.backend_singleton import get_websocket_manager
manager = get_websocket_manager()
if manager and hasattr(manager, 'platform_listeners'):
for key, listener_info in manager.platform_listeners.items():
if listener_info.get('store_id') == store_id:
return listener_info.get('platform', '')
return ""
except Exception as e:
print(f"获取平台类型失败: {e}")
return ""
def _forward_to_jd(self, store_id: str, recv_pin: str, content: str):
"""转发消息到京东平台"""
try:
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
jd_mgr = JDWSManager()
shop_key = f"京东:{store_id}"
entry = jd_mgr.get_connection(shop_key)
if not entry:
print(f"[JD Forward] 未找到连接: {shop_key}")
return
platform_info = (entry or {}).get('platform') or {}
ws = platform_info.get('ws')
aid = platform_info.get('aid')
pin_zj = platform_info.get('pin_zj')
vender_id = platform_info.get('vender_id')
loop = platform_info.get('loop')
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)} recv_pin={recv_pin}")
if ws and aid and pin_zj and vender_id and loop and content:
async def _send():
import hashlib as _hashlib
import time as _time
import json as _json
msg = {
"ver": "4.3",
"type": "chat_message",
"from": {"pin": pin_zj, "app": "im.waiter", "clientType": "comet"},
"to": {"app": "im.customer", "pin": recv_pin},
"id": _hashlib.md5(str(int(_time.time() * 1000)).encode()).hexdigest(),
"lang": "zh_CN",
"aid": aid,
"timestamp": int(_time.time() * 1000),
"readFlag": 0,
"body": {
"content": content,
"translated": False,
"param": {"cusVenderId": vender_id},
"type": "text"
}
}
await ws.send(_json.dumps(msg))
import asyncio as _asyncio
_future = _asyncio.run_coroutine_threadsafe(_send(), loop)
try:
_future.result(timeout=2)
print(f"[JD Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
except Exception as fe:
print(f"[JD Forward] 转发提交失败: {fe}")
else:
print("[JD Forward] 条件不足,未转发:",
{
'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_content': bool(content)
})
except Exception as e:
print(f"[JD Forward] 转发失败: {e}")
def _forward_to_douyin(self, store_id: str, recv_pin: str, content: str):
"""转发消息到抖音平台"""
try:
from Utils.Dy.DyUtils import DouYinWebsocketManager
dy_mgr = DouYinWebsocketManager()
shop_key = f"抖音:{store_id}"
entry = dy_mgr.get_connection(shop_key)
if not entry:
print(f"[DY Forward] 未找到连接: {shop_key}")
return
platform_info = entry.get('platform', {})
douyin_bot = platform_info.get('douyin_bot')
message_handler = platform_info.get('message_handler')
print(
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:
# 在消息处理器的事件循环中发送消息
def send_in_loop():
try:
# 获取消息处理器的事件循环
loop = message_handler._loop
if loop and not loop.is_closed():
# 在事件循环中执行发送
future = asyncio.run_coroutine_threadsafe(
message_handler.send_message_external(recv_pin, content),
loop
)
# 等待结果
try:
result = future.result(timeout=5)
if result:
print(f"[DY Forward] 已转发到平台: pin={recv_pin}, content_len={len(content)}")
else:
print(f"[DY Forward] 转发失败: 消息处理器返回False")
except Exception as fe:
print(f"[DY Forward] 转发执行失败: {fe}")
else:
print(f"[DY Forward] 事件循环不可用")
except Exception as e:
print(f"[DY Forward] 发送过程异常: {e}")
# 在新线程中执行发送操作
import threading
send_thread = threading.Thread(target=send_in_loop, daemon=True)
send_thread.start()
else:
print("[DY Forward] 条件不足,未转发:",
{
'has_bot': bool(douyin_bot),
'has_handler': bool(message_handler),
'has_content': bool(content)
})
except Exception as e:
print(f"[DY Forward] 转发失败: {e}")
def _forward_to_qianniu(self, store_id: str, recv_pin: str, content: str):
"""转发消息到千牛平台"""
try:
# TODO: 实现千牛平台的消息转发逻辑
print(
f"[QN Forward] 千牛平台消息转发功能待实现: store_id={store_id}, recv_pin={recv_pin}, content={content}")
except Exception as e:
print(f"[QN Forward] 转发失败: {e}")
def _forward_to_pdd(self, store_id: str, recv_pin: str, content: str):
"""转发消息到拼多多平台"""
try:
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager()
shop_key = f"拼多多:{store_id}"
entry = pdd_mgr.get_connection(shop_key)
if not entry:
print(f"[PDD Forward] 未找到连接: {shop_key}")
return
platform_info = entry.get('platform', {})
pdd_instance = platform_info.get('pdd_instance')
loop = platform_info.get('loop')
print(
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:
# 在拼多多实例的事件循环中发送消息
def send_in_loop():
try:
# 在事件循环中执行发送
future = asyncio.run_coroutine_threadsafe(
pdd_instance.send_message_external(recv_pin, content),
loop
)
# 等待结果
try:
result = future.result(timeout=10) # 拼多多可能需要更长时间
if result:
print(f"[PDD Forward] 已转发到平台: uid={recv_pin}, content_len={len(content)}")
else:
print(f"[PDD Forward] 转发失败: 拼多多实例返回False")
except Exception as fe:
print(f"[PDD Forward] 转发执行失败: {fe}")
except Exception as e:
print(f"[PDD Forward] 发送过程异常: {e}")
# 在新线程中执行发送操作
import threading
send_thread = threading.Thread(target=send_in_loop, daemon=True)
send_thread.start()
else:
print("[PDD Forward] 条件不足,未转发:",
{
'has_pdd_instance': bool(pdd_instance),
'has_loop': bool(loop),
'has_content': bool(content)
})
except Exception as e:
print(f"[PDD Forward] 转发失败: {e}")
def _transfer_to_pdd(self, customer_service_id: str, user_id: str, store_id: str):
"""执行拼多多平台转接操作"""
try:
from Utils.Pdd.PddUtils import WebsocketManager as PDDWSManager
pdd_mgr = PDDWSManager()
shop_key = f"拼多多:{store_id}"
entry = pdd_mgr.get_connection(shop_key)
if not entry:
print(f"[PDD Transfer] 未找到拼多多连接: {shop_key}")
return
platform_info = entry.get('platform', {})
pdd_instance = platform_info.get('pdd_instance')
loop = platform_info.get('loop')
print(f"[PDD Transfer] 找到拼多多连接,准备执行转接: user_id={user_id}, cs_id={customer_service_id}")
if pdd_instance and loop:
# 设置目标客服ID并执行转接
def transfer_in_loop():
try:
# 设置目标客服ID
pdd_instance.csid = customer_service_id
# 在事件循环中执行转接
future = asyncio.run_coroutine_threadsafe(
pdd_instance.handle_transfer_message({
"content": customer_service_id,
"receiver": {"id": user_id}
}),
loop
)
# 等待转接结果
try:
result = future.result(timeout=15) # 转接可能需要更长时间
if result:
print(f"[PDD Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
else:
print(f"[PDD Transfer] ❌ 转接失败: user_id={user_id}")
except Exception as fe:
print(f"[PDD Transfer] 转接执行失败: {fe}")
except Exception as e:
print(f"[PDD Transfer] 转接过程异常: {e}")
# 在新线程中执行转接操作
import threading
transfer_thread = threading.Thread(target=transfer_in_loop, daemon=True)
transfer_thread.start()
else:
print(f"[PDD Transfer] 条件不足: has_pdd_instance={bool(pdd_instance)}, has_loop={bool(loop)}")
except Exception as e:
print(f"[PDD Transfer] 拼多多转接失败: {e}")
def _transfer_to_jd(self, customer_service_id: str, user_id: str, store_id: str):
"""执行京东平台转接操作"""
try:
from Utils.JD.JdUtils import WebsocketManager as JDWSManager
jd_mgr = JDWSManager()
shop_key = f"京东:{store_id}"
entry = jd_mgr.get_connection(shop_key)
if not entry:
print(f"[JD Transfer] 未找到京东连接: {shop_key}")
return
platform_info = entry.get('platform', {})
websocket = platform_info.get('ws') # 京东使用'ws'字段
aid = platform_info.get('aid', '')
pin_zj = platform_info.get('pin_zj', '')
print(f"[JD Transfer] 找到京东连接,准备执行转接: user_id={user_id}, cs_id={customer_service_id}")
print(f"[JD Transfer] 连接信息: has_ws={bool(websocket)}, aid={aid}, pin_zj={pin_zj}")
if websocket:
# 执行转接操作
def transfer_in_loop():
try:
# 导入京东工具类
from Utils.JD.JdUtils import FixJdCookie
# 创建临时实例用于转接
jd_instance = FixJdCookie()
# 执行转接操作
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
result = loop.run_until_complete(
jd_instance.transfer_customer(
websocket, aid, user_id, pin_zj, customer_service_id
)
)
if result:
print(f"[JD Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
else:
print(f"[JD Transfer] ❌ 转接失败: user_id={user_id}")
finally:
loop.close()
except Exception as e:
print(f"[JD Transfer] 转接过程异常: {e}")
# 在新线程中执行转接操作
import threading
transfer_thread = threading.Thread(target=transfer_in_loop, daemon=True)
transfer_thread.start()
else:
print(f"[JD Transfer] 条件不足: has_ws={bool(websocket)}, aid='{aid}', pin_zj='{pin_zj}'")
except Exception as e:
print(f"[JD Transfer] 京东转接失败: {e}")
def _transfer_to_dy(self, customer_service_id: str, user_id: str, store_id: str):
"""执行抖音平台转接操作"""
try:
from Utils.Dy.DyUtils import DouYinWebsocketManager as DYWSManager
dy_mgr = DYWSManager()
shop_key = f"抖音:{store_id}"
entry = dy_mgr.get_connection(shop_key)
if not entry:
print(f"[DY Transfer] 未找到抖音连接: {shop_key}")
return
platform_info = entry.get('platform', {})
dy_instance = platform_info.get('douyin_bot') # 修正字段名称
cookie_dict = platform_info.get('cookie', {})
print(f"[DY Transfer] 找到抖音连接,准备执行转接: user_id={user_id}, cs_id={customer_service_id}")
print(f"[DY Transfer] 连接信息: has_douyin_bot={bool(dy_instance)}, has_cookie={bool(cookie_dict)}")
if dy_instance:
# 执行转接操作
def transfer_in_loop():
try:
# 抖音转接通过message_handler执行
if dy_instance and hasattr(dy_instance, 'message_handler') and dy_instance.message_handler:
# 获取实际的抖音店铺ID从cookie中获取
shop_id = cookie_dict.get('SHOP_ID', store_id) # 优先使用cookie中的SHOP_ID
print(f"[DY Transfer] 使用shop_id: {shop_id}")
print(
f"[DY Transfer] 转接参数: receiver_id={user_id}, shop_id={shop_id}, staff_id={customer_service_id}")
# 检查是否是自己转给自己的情况
try:
# 获取可用客服列表来验证
staff_list = dy_instance.message_handler.get_casl()
if staff_list:
print(f"[DY Transfer] 当前可用客服数量: {len(staff_list)}")
if len(staff_list) <= 1:
print(f"[DY Transfer] ⚠️ 只有一个客服在线,可能无法转接")
# 查找目标客服信息
target_staff = None
for staff in staff_list:
if str(staff.get('staffId', '')) == str(customer_service_id):
target_staff = staff
break
if target_staff:
print(
f"[DY Transfer] 找到目标客服: {target_staff.get('staffName', 'Unknown')} (ID: {customer_service_id})")
else:
print(f"[DY Transfer] ⚠️ 未找到目标客服ID: {customer_service_id}")
print(
f"[DY Transfer] 可用客服列表: {[{'id': s.get('staffId'), 'name': s.get('staffName')} for s in staff_list]}")
else:
print(f"[DY Transfer] ⚠️ 无法获取客服列表")
except Exception as e:
print(f"[DY Transfer] 获取客服列表时出错: {e}")
# 执行同步转接操作
result = dy_instance.message_handler.transfer_conversation(
receiver_id=user_id,
shop_id=shop_id,
staff_id=customer_service_id
)
if result:
print(f"[DY Transfer] ✅ 转接成功: user_id={user_id} -> cs_id={customer_service_id}")
else:
print(f"[DY Transfer] ❌ 转接失败: user_id={user_id}")
print(
f"[DY Transfer] 💡 可能原因1) 只有一个客服无法转接 2) 客服ID不存在 3) 权限不足 4) 会话状态不允许转接")
else:
print(f"[DY Transfer] ⚠️ 抖音实例或message_handler不可用")
except Exception as e:
print(f"[DY Transfer] 转接过程异常: {e}")
# 在新线程中执行转接操作
import threading
transfer_thread = threading.Thread(target=transfer_in_loop, daemon=True)
transfer_thread.start()
else:
print(f"[DY Transfer] 条件不足: has_douyin_bot={bool(dy_instance)}")
except Exception as e:
print(f"[DY Transfer] 抖音转接失败: {e}")
def _transfer_to_qianniu(self, customer_service_id: str, user_id: str, store_id: str):
"""执行千牛平台转接操作"""
try:
# TODO: 实现千牛平台转接逻辑
print(f"[QN Transfer] 千牛平台转接功能待实现: user_id={user_id}, cs_id={customer_service_id}")
except Exception as e:
print(f"[QN Transfer] 千牛转接失败: {e}")
def _handle_transfer(self, message: Dict[str, Any]):
"""处理转接消息"""
# 新版转接消息格式: {"type": "transfer", "content": "客服ID", "receiver": {"id": "用户ID"}, "store_id": "店铺ID"}
customer_service_id = message.get('content', '') # 目标客服ID
receiver_info = message.get('receiver', {})
user_id = receiver_info.get('id', '') # 用户ID
store_id = message.get('store_id', '')
print(f"转接消息: 顾客{user_id}已转接给客服{customer_service_id} (店铺: {store_id})")
# 根据店铺ID确定平台类型并执行转接
try:
platform_type = self._get_platform_by_store_id(store_id)
if platform_type == "京东":
# 京东转接逻辑
self._transfer_to_jd(customer_service_id, user_id, store_id)
elif platform_type == "抖音":
# 抖音转接逻辑
self._transfer_to_dy(customer_service_id, user_id, store_id)
elif platform_type == "千牛":
# 千牛转接逻辑
self._transfer_to_qianniu(customer_service_id, user_id, store_id)
elif platform_type == "拼多多":
self._transfer_to_pdd(customer_service_id, user_id, store_id)
else:
print(f"[Transfer] 未知平台类型或未找到店铺: {platform_type}, store_id={store_id}")
except Exception as e:
print(f"执行转接操作失败: {e}")
# 保持旧版回调兼容性
if self.transfer_callback:
self.transfer_callback(customer_service_id, user_id, store_id)
def _handle_close(self, message: Dict[str, Any]):
"""处理店铺关闭"""
store_id = message.get('store_id', '')
print(f"店铺{store_id}已关闭")
if self.close_callback:
self.close_callback(store_id)
def _handle_pong(self, message: Dict[str, Any]):
"""处理心跳响应"""
uuid_received = message.get('uuid', '')
print(f"收到心跳响应: {uuid_received}")
def _handle_connect_success(self, message: Dict[str, Any]):
"""处理连接成功消息(旧版兼容)"""
print("后端连接成功(connect_success)")
if self.token:
self.get_store()
def _handle_login(self, message: Dict[str, Any]):
"""处理平台登录消息新版type=login, cookies/login_params, store_id, platform_name"""
cookies = message.get('cookies', '')
store_id = message.get('store_id', '')
platform_name = message.get('platform_name', '')
content = message.get('content', '')
data = message.get('data', {})
# 判断是拼多多登录参数还是普通Cookie
if platform_name == "拼多多" and ("拼多多登录" in content) and data.get('login_params'):
# 拼多多登录参数模式 - 传递完整的消息JSON给处理器
print(f"收到拼多多登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
if self.login_callback:
# 传递完整的JSON消息让拼多多处理器来解析login_params
import json
full_message = json.dumps(message)
self.login_callback(platform_name, store_id, full_message)
else:
# 普通Cookie模式
print(f"收到登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}")
if self.login_callback:
self.login_callback(platform_name, store_id, cookies)
def _handle_error_message(self, message: Dict[str, Any]):
"""处理错误消息"""
error_msg = message.get('error', '未知错误')
print(f"后端连接错误: {error_msg}")
if self.error_callback:
self.error_callback(error_msg, message)
def _handle_staff_list(self, message: Dict[str, Any]):
"""处理客服列表更新消息"""
staff_list = message.get('data', {}).get('staff_list', [])
store_id = message.get('store_id', '')
print(f"店铺{store_id}的客服列表已更新,共{len(staff_list)}个客服:")
for staff in staff_list:
pin = staff.get('pin', '')
nickname = staff.get('nickname', '')
print(f" - {nickname} ({pin})")
if self.customers_callback: # 假设客服列表更新也触发客服列表回调
self.customers_callback(staff_list, store_id)
# ==================== 辅助方法 ====================
def set_token(self, token: str):
"""设置或更新令牌"""
self.token = token
def get_connection_info(self) -> Dict[str, Any]:
"""获取连接信息"""
return {
'url': self.url,
'token': self.token,
'uuid': self.uuid,
'is_connected': self.is_connected
}
# 使用示例
if __name__ == '__main__':
pass
# import time
#
# def on_store_list(stores):
# print(f"回调: 收到{len(stores)}个店铺")
#
# def on_message(data, store_id):
# print(f"回调: 店铺{store_id}收到消息: {data.get('content')}")
#
# def on_error(error, message):
# print(f"回调: 发生错误: {error}")
#
# def on_login(platform_name, store_id, cookies):
# print(f"回调: 登录指令 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}")
# # 此处触发对应平台的WS连接/更新cookies逻辑并将该平台WS与store_id绑定
#
# def on_success():
# print("回调: 后端连接成功")
#
# # 创建客户端(新版:单连接)
# client = BackendClient.from_exe_token("your_exe_token_here")
#
# # 设置回调
# client.set_callbacks(
# store_list=on_store_list,
# message=on_message,
# error=on_error,
# login=on_login,
# success=on_success
# )
#
# try:
# # 连接
# client.connect()
#
# # 等待连接
# time.sleep(2)
#
# # 发送心跳
# client.send_ping()
#
# # 保持运行
# while True:
# time.sleep(30)
# client.send_ping()
#
# except KeyboardInterrupt:
# print("用户中断")
# finally:
# client.disconnect()

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

View File

@@ -27,25 +27,18 @@ class WebSocketManager:
"""WebSocket连接管理器统一处理后端连接和平台监听"""
def __init__(self):
# 后端客户端
self.backend_client = None
self.is_connected = False
# 版本检查器
self.version_checker = None
self.gui_update_callback = None
self.platform_listeners = {} # 存储各平台的监听器
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
self.callbacks = {
'log': None,
'success': None,
'error': None,
'platform_connected': None,
'token_error': None,
}
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
platform_connected: Callable = None, token_error: Callable = None):
platform_connected: Callable = None): # ← 新增参数
"""设置回调函数"""
if log:
self.callbacks['log'] = log
@@ -55,8 +48,6 @@ class WebSocketManager:
self.callbacks['error'] = error
if platform_connected: # ← 新增
self.callbacks['platform_connected'] = platform_connected
if token_error:
self.callbacks['token_error'] = token_error
def _log(self, message: str, level: str = "INFO"):
"""内部日志方法"""
@@ -80,7 +71,6 @@ class WebSocketManager:
def connect_backend(self, token: str) -> bool:
"""连接后端WebSocket"""
try:
# 1 保存token到配置
try:
from config import set_saved_token
@@ -92,52 +82,33 @@ class WebSocketManager:
backend = get_backend_client()
if backend:
# 检查现有客户端是否因token错误而停止
if backend.should_stop:
self._log("检测到客户端因token错误已停止创建新的客户端", "INFO")
# 断开旧客户端
backend.disconnect()
# 清除旧客户端引用
set_backend_client(None)
backend = None
else:
# 3 如果有客户端更新token并重连
backend.set_token(token)
# 3 如果有客户端更新token并重连
backend.set_token(token)
# 设置回调函数
def _on_backend_success():
try:
self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']:
self.callbacks['success']()
# 启动版本检查器
self._start_version_checker()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
# 设置回调函数
def _on_backend_success():
try:
self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']:
self.callbacks['success']()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO")
self._handle_platform_login(platform_name, store_id, cookies)
def _on_backend_login(platform_name: str, store_id: str, cookies: str):
self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO")
self._handle_platform_login(platform_name, store_id, cookies)
def _on_token_error(error_content: str):
self._log(f"Token验证失败: {error_content}", "ERROR")
if self.callbacks['token_error']:
self.callbacks['token_error'](error_content)
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)
if not backend.is_connected:
backend.connect()
if not backend.is_connected:
backend.connect()
self.backend_client = backend
self._log("令牌已提交已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
return True
# 如果没有现有客户端或客户端被重置,创建新的
if not backend:
self.backend_client = backend
self._log("令牌已提交已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
return True
else:
backend = BackendClient.from_exe_token(token)
@@ -152,17 +123,10 @@ class WebSocketManager:
self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']:
self.callbacks['success']()
# 启动版本检查器
self._start_version_checker()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_token_error(error_content: str):
self._log(f"Token验证失败: {error_content}", "ERROR")
if self.callbacks['token_error']:
self.callbacks['token_error'](error_content)
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success, token_error=_on_token_error)
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success)
backend.connect()
set_backend_client(backend)
@@ -179,14 +143,6 @@ class WebSocketManager:
def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str):
"""处理平台登录请求"""
try:
# 🔥 检查并清理当前店铺的旧连接
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)]
if keys_to_remove:
self._log(f"🔄 检测到店铺 {store_id} 重连,清理 {len(keys_to_remove)} 个旧连接", "INFO")
for key in keys_to_remove:
self.platform_listeners.pop(key, None)
# 平台名称映射
platform_map = {
"淘宝": "千牛",
@@ -230,27 +186,6 @@ class WebSocketManager:
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def _start_version_checker(self):
"""启动版本检查器"""
try:
from version_checker import VersionChecker
self.version_checker = VersionChecker(
backend_client=self.backend_client,
update_callback=self._on_update_available
)
self.backend_client.version_callback = self.version_checker.handle_version_response
self.version_checker.start()
self._log("✅ 版本检查器已启动将每10分钟检查一次更新", "SUCCESS")
except Exception as e:
self._log(f"❌ 启动版本检查器失败: {e}", "ERROR")
def _on_update_available(self, latest_version, download_url):
"""发现新版本时的处理"""
self._log(f"🔔 发现新版本 {latest_version}", "INFO")
# 通知主GUI显示更新提醒
if hasattr(self, 'gui_update_callback') and self.gui_update_callback:
self.gui_update_callback(latest_version, download_url)
def _start_jd_listener(self, store_id: str, cookies: str):
"""启动京东平台监听"""
try:
@@ -306,59 +241,15 @@ class WebSocketManager:
def _runner():
try:
import json
self._log("🚀 开始创建抖音监听器实例", "DEBUG")
listener = DYListenerForGUI_WS()
self._log("✅ 抖音监听器实例创建成功", "DEBUG")
# 🔥 检查是否为登录参数模式(与拼多多保持一致)
if cookies and ('"login_flow"' in cookies or '"phone_number"' in cookies):
# 使用登录参数模式
self._log("📋 使用登录参数启动抖音监听器", "INFO")
self._log("🔄 开始执行 start_with_login_params", "DEBUG")
result = asyncio.run(listener.start_with_login_params(store_id=store_id, login_params=cookies))
self._log(f"📊 start_with_login_params 执行结果: {result}", "DEBUG")
# 🔥 详细的结果分析(与拼多多完全一致)
if result == "need_verification_code":
self._log("✅ [DY] 登录流程正常,已发送验证码需求通知给后端", "SUCCESS")
elif result == "verification_code_error":
self._log("⚠️ [DY] 验证码错误,已发送错误通知给后端", "WARNING")
elif result:
self._log("✅ [DY] 登录成功,平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
else:
self._log("❌ [DY] 登录失败", "ERROR")
else:
# 传统cookie模式保留兼容性
self._log("🍪 使用Cookie启动抖音监听器", "INFO")
self._log("🔄 开始执行 start_with_cookies", "DEBUG")
try:
# 🔥 修复尝试JSON解析失败时用ast.literal_eval解析Python字典字符串
if isinstance(cookies, str):
try:
cookie_dict = json.loads(cookies)
except json.JSONDecodeError:
# 后端发送的是Python字典字符串格式使用ast.literal_eval
import ast
cookie_dict = ast.literal_eval(cookies)
self._log("✅ 使用ast.literal_eval成功解析cookies", "DEBUG")
else:
cookie_dict = cookies
except (json.JSONDecodeError, ValueError, SyntaxError) as e:
self._log(f"❌ Cookie解析失败: {e}", "ERROR")
return False
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG")
# Cookie启动成功时也要通知GUI
if result:
self._log("✅ [DY] Cookie启动成功平台连接已建立", "SUCCESS")
self._notify_platform_connected("抖音")
# 🔥 移除不再在backend_singleton中发送connect_message
# 抖音的连接状态报告应该在DyUtils中的DyLogin类中发送与拼多多保持一致
# 所有特殊状态通知都已经在DyLogin中发送过了这里不需要重复发送
# 将JSON字符串格式的cookies解析为字典
try:
cookie_dict = json.loads(cookies) if isinstance(cookies, str) else cookies
except json.JSONDecodeError as e:
self._log(f"❌ Cookie JSON解析失败: {e}", "ERROR")
return False
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
return result
except Exception as e:
self._log(f"抖音监听器运行异常: {e}", "ERROR")
@@ -379,13 +270,34 @@ class WebSocketManager:
if f"抖音:{store_id}" in self.platform_listeners:
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
# 上报连接状态给后端
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": True
})
self._log("已上报抖音平台连接状态: 成功", "INFO")
except Exception as e:
self._log(f"上报抖音平台连接状态失败: {e}", "WARNING")
self._log("已启动抖音平台监听", "SUCCESS")
self._notify_platform_connected("抖音") # ← 新增
except Exception as e:
self._log(f"启动抖音平台监听失败: {e}", "ERROR")
# 🔥 移除:确保失败时也不在这里上报状态
# 失败状态应该在DyLogin中处理与拼多多保持一致
# 确保失败时也上报状态
if self.backend_client:
try:
self.backend_client.send_message({
"type": "connect_message",
"store_id": store_id,
"status": False
})
except Exception as send_e:
self._log(f"失败状态下报抖音平台连接状态也失败: {send_e}", "ERROR")
def _start_qianniu_listener(self, store_id: str, cookies: str):
"""启动千牛平台监听(单连接多店铺架构)"""
@@ -455,7 +367,7 @@ class WebSocketManager:
self._log("🚀 开始创建拼多多监听器实例", "DEBUG")
listener = PDDListenerForGUI_WS(log_callback=self._log)
self._log("✅ 拼多多监听器实例创建成功", "DEBUG")
# 判断是登录参数还是Cookie
if self._is_pdd_login_params(data):
# 使用登录参数启动
@@ -463,7 +375,7 @@ class WebSocketManager:
self._log("🔄 开始执行 start_with_login_params", "DEBUG")
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")
@@ -480,15 +392,9 @@ class WebSocketManager:
self._log("🔄 开始执行 start_with_cookies", "DEBUG")
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",
"login_failure"]:
if self.backend_client and result not in ["need_verification_code", "verification_code_error", "login_failure"]:
# 如果是特殊状态说明通知已经在PddLogin中发送了不需要重复发送
try:
message = {
@@ -498,8 +404,7 @@ class WebSocketManager:
}
self.backend_client.send_message(message)
status_text = "成功" if result else "失败"
self._log(f"上报拼多多平台连接状态{status_text}: {message}",
"SUCCESS" if result else "ERROR")
self._log(f"上报拼多多平台连接状态{status_text}: {message}", "SUCCESS" if result else "ERROR")
except Exception as send_e:
self._log(f"上报拼多多平台连接状态失败: {send_e}", "ERROR")
elif result == "need_verification_code":
@@ -508,9 +413,9 @@ class WebSocketManager:
self._log("验证码错误错误通知已由PddLogin发送等待后端处理", "INFO")
elif result == "login_failure":
self._log("登录失败失败通知已由PddLogin发送等待后端处理", "INFO")
return result
except Exception as e:
self._log(f"拼多多监听器运行异常: {e}", "ERROR")
import traceback
@@ -565,24 +470,24 @@ class WebSocketManager:
try:
self._log(f"🔍 [DEBUG] 检查是否为登录参数,数据长度: {len(data)}", "DEBUG")
self._log(f"🔍 [DEBUG] 数据前100字符: {data[:100]}", "DEBUG")
import json
parsed_data = json.loads(data)
self._log(f"🔍 [DEBUG] JSON解析成功键: {list(parsed_data.keys())}", "DEBUG")
login_params = parsed_data.get("data", {}).get("login_params", {})
self._log(f"🔍 [DEBUG] login_params存在: {bool(login_params)}", "DEBUG")
if not login_params:
self._log("🔍 [DEBUG] login_params为空返回False", "DEBUG")
return False
# 检查必需的登录参数字段
required_fields = ["username", "password", "anti_content", "risk_sign", "timestamp"]
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
except Exception as e:
self._log(f"🔍 [DEBUG] 解析失败: {e}", "DEBUG")

Binary file not shown.

Binary file not shown.

Binary file not shown.

50414
build/main/Analysis-00.toc Normal file

File diff suppressed because it is too large Load Diff

23925
build/main/COLLECT-00.toc Normal file

File diff suppressed because it is too large Load Diff

77
build/main/EXE-00.toc Normal file
View File

@@ -0,0 +1,77 @@
('D:\\GUI_main\\build\\main\\main.exe',
False,
False,
True,
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\bootloader\\images\\icon-windowed.ico',
None,
False,
False,
b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly xmlns='
b'"urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">\n <trustInfo x'
b'mlns="urn:schemas-microsoft-com:asm.v3">\n <security>\n <requested'
b'Privileges>\n <requestedExecutionLevel level="asInvoker" uiAccess='
b'"false"/>\n </requestedPrivileges>\n </security>\n </trustInfo>\n '
b'<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">\n <'
b'application>\n <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f'
b'0}"/>\n <supportedOS Id="{35138b9a-5d96-4fbd-8e2d-a2440225f93a}"/>\n '
b' <supportedOS Id="{4a2f28e3-53b9-4441-ba9c-d69d4a4a6e38}"/>\n <s'
b'upportedOS Id="{1f676c76-80e1-4239-95bb-83d0f6d0da78}"/>\n <supporte'
b'dOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}"/>\n </application>\n <'
b'/compatibility>\n <application xmlns="urn:schemas-microsoft-com:asm.v3">'
b'\n <windowsSettings>\n <longPathAware xmlns="http://schemas.micros'
b'oft.com/SMI/2016/WindowsSettings">true</longPathAware>\n </windowsSett'
b'ings>\n </application>\n <dependency>\n <dependentAssembly>\n <ass'
b'emblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version='
b'"6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" langua'
b'ge="*"/>\n </dependentAssembly>\n </dependency>\n</assembly>',
True,
False,
None,
None,
None,
'D:\\GUI_main\\build\\main\\main.pkg',
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz', 'D:\\GUI_main\\build\\main\\PYZ-00.pyz', 'PYZ'),
('struct', 'D:\\GUI_main\\build\\main\\localpycs\\struct.pyc', 'PYMODULE'),
('pyimod01_archive',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth_inspect',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
'PYSOURCE'),
('pyi_rth_setuptools',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_setuptools.py',
'PYSOURCE'),
('pyi_rth_cryptography_openssl',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_cryptography_openssl.py',
'PYSOURCE'),
('pyi_rth_pyqt5',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pyqt5.py',
'PYSOURCE'),
('main', 'D:\\GUI_main\\main.py', 'PYSOURCE')],
[],
False,
False,
1758767002,
[('runw.exe',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\bootloader\\Windows-64bit-intel\\runw.exe',
'EXECUTABLE')],
'D:\\Python\\python313.dll')

55
build/main/PKG-00.toc Normal file
View File

@@ -0,0 +1,55 @@
('D:\\GUI_main\\build\\main\\main.pkg',
{'BINARY': True,
'DATA': True,
'EXECUTABLE': True,
'EXTENSION': True,
'PYMODULE': True,
'PYSOURCE': True,
'PYZ': False,
'SPLASH': True,
'SYMLINK': False},
[('pyi-contents-directory _internal', '', 'OPTION'),
('PYZ-00.pyz', 'D:\\GUI_main\\build\\main\\PYZ-00.pyz', 'PYZ'),
('struct', 'D:\\GUI_main\\build\\main\\localpycs\\struct.pyc', 'PYMODULE'),
('pyimod01_archive',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod01_archive.pyc',
'PYMODULE'),
('pyimod02_importers',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod02_importers.pyc',
'PYMODULE'),
('pyimod03_ctypes',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod03_ctypes.pyc',
'PYMODULE'),
('pyimod04_pywin32',
'D:\\GUI_main\\build\\main\\localpycs\\pyimod04_pywin32.pyc',
'PYMODULE'),
('pyiboot01_bootstrap',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\loader\\pyiboot01_bootstrap.py',
'PYSOURCE'),
('pyi_rth_inspect',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_inspect.py',
'PYSOURCE'),
('pyi_rth_pkgutil',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pkgutil.py',
'PYSOURCE'),
('pyi_rth_multiprocessing',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_multiprocessing.py',
'PYSOURCE'),
('pyi_rth_setuptools',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_setuptools.py',
'PYSOURCE'),
('pyi_rth_cryptography_openssl',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\_pyinstaller_hooks_contrib\\rthooks\\pyi_rth_cryptography_openssl.py',
'PYSOURCE'),
('pyi_rth_pyqt5',
'D:\\Kris\\shuidrop_gui\\venv\\Lib\\site-packages\\PyInstaller\\hooks\\rthooks\\pyi_rth_pyqt5.py',
'PYSOURCE'),
('main', 'D:\\GUI_main\\main.py', 'PYSOURCE')],
'python313.dll',
True,
False,
False,
[],
None,
None,
None)

BIN
build/main/PYZ-00.pyz Normal file

Binary file not shown.

3002
build/main/PYZ-00.toc Normal file

File diff suppressed because it is too large Load Diff

BIN
build/main/base_library.zip Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build/main/main.exe Normal file

Binary file not shown.

BIN
build/main/main.pkg Normal file

Binary file not shown.

288
build/main/warn-main.txt Normal file
View File

@@ -0,0 +1,288 @@
This file lists modules PyInstaller was not able to find. This does not
necessarily mean this module is required for running your program. Python and
Python 3rd-party packages include a lot of conditional or optional modules. For
example the module 'ntpath' only exists on Windows, whereas the module
'posixpath' only exists on Posix systems.
Types if import:
* top-level: imported at the top-level - look at these first
* conditional: imported within an if-statement
* delayed: imported within a function
* optional: imported within a try-except-statement
IMPORTANT: Do NOT post this list to the issue-tracker. Use it as a basis for
tracking down the missing module yourself. Thanks!
missing module named pwd - imported by posixpath (delayed, conditional, optional), shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional), netrc (delayed, optional), getpass (delayed, optional), http.server (delayed, optional), psutil (optional), setuptools._distutils.util (delayed, conditional, optional), setuptools._vendor.backports.tarfile (optional), setuptools._distutils.archive_util (optional)
missing module named grp - imported by shutil (delayed, optional), tarfile (optional), pathlib._local (optional), subprocess (delayed, conditional, optional), setuptools._vendor.backports.tarfile (optional), setuptools._distutils.archive_util (optional)
missing module named 'collections.abc' - imported by traceback (top-level), typing (top-level), inspect (top-level), logging (top-level), importlib.resources.readers (top-level), selectors (top-level), tracemalloc (top-level), websockets.imports (top-level), asyncio.base_events (top-level), http.client (top-level), asyncio.coroutines (top-level), websockets.asyncio.client (top-level), websockets.client (top-level), websockets.datastructures (top-level), websockets.frames (top-level), websockets.extensions.base (top-level), websockets.http11 (top-level), websockets.headers (top-level), websockets.protocol (top-level), websockets.streams (top-level), websockets.extensions.permessage_deflate (top-level), websockets.asyncio.connection (top-level), websockets.asyncio.messages (top-level), werkzeug.wrappers.request (top-level), werkzeug.datastructures.accept (top-level), werkzeug.datastructures.structures (top-level), markupsafe (top-level), typing_extensions (top-level), werkzeug.datastructures.cache_control (top-level), werkzeug.datastructures.mixins (top-level), werkzeug.datastructures.auth (top-level), werkzeug.datastructures.csp (top-level), werkzeug.datastructures.etag (top-level), werkzeug.datastructures.file_storage (top-level), werkzeug.datastructures.headers (top-level), werkzeug.datastructures.range (top-level), werkzeug.middleware.shared_data (top-level), websockets.asyncio.server (top-level), websockets.server (top-level), multidict._abc (top-level), multidict._multidict_py (top-level), multidict (conditional), attr._compat (top-level), attr._make (top-level), yarl._query (top-level), yarl._url (top-level), propcache._helpers_py (top-level), yarl._path (top-level), aiohttp.web (top-level), aiohttp.abc (top-level), frozenlist (top-level), aiohttp.client_reqrep (top-level), aiohttp.multipart (top-level), aiohttp.compression_utils (conditional), aiohttp.payload (top-level), aiohttp.connector (conditional), aiohttp.web_response (top-level), aiohttp.client_middlewares (top-level), aiohttp.cookiejar (top-level), requests.compat (top-level), OpenSSL.SSL (top-level), OpenSSL.crypto (top-level), socks (optional), google.protobuf.internal.containers (optional), google.protobuf.internal.well_known_types (optional), numpy.lib._npyio_impl (top-level), numpy.lib._function_base_impl (top-level), numpy._typing._nested_sequence (conditional), numpy._typing._shape (top-level), numpy._typing._dtype_like (top-level), numpy._typing._array_like (top-level), numpy.random.bit_generator (top-level), numpy.random.mtrand (top-level), numpy.random._generator (top-level), setuptools (top-level), setuptools._distutils.filelist (top-level), setuptools._distutils.util (top-level), setuptools._vendor.jaraco.functools (top-level), setuptools._vendor.more_itertools.more (top-level), setuptools._vendor.more_itertools.recipes (top-level), setuptools._distutils._modified (top-level), setuptools._distutils.compat (top-level), setuptools._distutils.spawn (top-level), setuptools._distutils.compilers.C.base (top-level), setuptools._distutils.fancy_getopt (top-level), setuptools._reqs (top-level), setuptools.discovery (top-level), setuptools.dist (top-level), setuptools._distutils.command.bdist (top-level), setuptools._distutils.core (top-level), setuptools._distutils.cmd (top-level), setuptools._distutils.dist (top-level), configparser (top-level), setuptools._distutils.extension (top-level), setuptools.config.setupcfg (top-level), setuptools.config.expand (top-level), setuptools.config.pyprojecttoml (top-level), setuptools.config._apply_pyprojecttoml (top-level), tomllib._parser (top-level), setuptools._vendor.tomli._parser (top-level), setuptools.command.egg_info (top-level), setuptools._distutils.command.build (top-level), setuptools._distutils.command.sdist (top-level), setuptools.glob (top-level), setuptools.command._requirestxt (top-level), setuptools.command.bdist_wheel (top-level), setuptools._vendor.wheel.cli.convert (top-level), setuptools._vendor.wheel.cli.tags (top-level), setuptools._distutils.command.build_ext (top-level), _pyrepl.types (top-level), _pyrepl.readline (top-level), setuptools._distutils.compilers.C.msvc (top-level), websockets.legacy.auth (top-level), websockets.legacy.server (top-level), websockets.legacy.protocol (top-level), websockets.legacy.framing (top-level), websockets.legacy.client (top-level), websockets.sync.client (top-level), websockets.sync.connection (top-level), websockets.sync.server (top-level)
missing module named posix - imported by os (conditional, optional), posixpath (optional), shutil (conditional), importlib._bootstrap_external (conditional), _pyrepl.unix_console (delayed, optional)
missing module named resource - imported by posix (top-level)
missing module named _frozen_importlib_external - imported by importlib._bootstrap (delayed), importlib (optional), importlib.abc (optional), zipimport (top-level)
excluded module named _frozen_importlib - imported by importlib (optional), importlib.abc (optional), zipimport (top-level)
missing module named annotationlib - imported by typing_extensions (conditional), attr._compat (conditional)
missing module named _posixsubprocess - imported by subprocess (conditional), multiprocessing.util (delayed)
missing module named fcntl - imported by subprocess (optional), _pyrepl.unix_console (top-level)
missing module named vms_lib - imported by platform (delayed, optional)
missing module named 'java.lang' - imported by platform (delayed, optional)
missing module named java - imported by platform (delayed)
missing module named _posixshmem - imported by multiprocessing.resource_tracker (conditional), multiprocessing.shared_memory (conditional)
missing module named multiprocessing.set_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_start_method - imported by multiprocessing (top-level), multiprocessing.spawn (top-level)
missing module named multiprocessing.get_context - imported by multiprocessing (top-level), multiprocessing.pool (top-level), multiprocessing.managers (top-level), multiprocessing.sharedctypes (top-level), loguru._logger (top-level)
missing module named multiprocessing.TimeoutError - imported by multiprocessing (top-level), multiprocessing.pool (top-level)
missing module named _scproxy - imported by urllib.request (conditional)
missing module named termios - imported by getpass (optional), tty (top-level), _pyrepl.pager (delayed, optional), werkzeug._reloader (delayed, optional), _pyrepl.unix_console (top-level), _pyrepl.fancy_termios (top-level), _pyrepl.unix_eventqueue (top-level)
missing module named multiprocessing.BufferTooShort - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.AuthenticationError - imported by multiprocessing (top-level), multiprocessing.connection (top-level)
missing module named multiprocessing.current_process - imported by multiprocessing (top-level), loguru._logger (top-level)
missing module named multiprocessing.Value - imported by multiprocessing (top-level), werkzeug.debug (top-level)
missing module named usercustomize - imported by site (delayed, optional)
missing module named sitecustomize - imported by site (delayed, optional)
missing module named _curses - imported by curses (top-level), curses.has_key (top-level), _pyrepl.curses (optional)
missing module named readline - imported by code (delayed, conditional, optional), cmd (delayed, conditional, optional), rlcompleter (optional), pdb (delayed, optional), site (delayed, optional), websockets.cli (delayed, optional)
missing module named _typeshed - imported by werkzeug._internal (conditional), numpy.random.bit_generator (top-level), setuptools._distutils.dist (conditional), setuptools.glob (conditional), setuptools.compat.py311 (conditional)
missing module named _manylinux - imported by packaging._manylinux (delayed, optional), setuptools._vendor.packaging._manylinux (delayed, optional), setuptools._vendor.wheel.vendored.packaging._manylinux (delayed, optional)
missing module named importlib_resources - imported by setuptools._vendor.jaraco.text (optional)
missing module named trove_classifiers - imported by setuptools.config._validate_pyproject.formats (optional)
missing module named pyimod02_importers - imported by D:\Kris\shuidrop_gui\venv\Lib\site-packages\PyInstaller\hooks\rthooks\pyi_rth_pkgutil.py (delayed)
missing module named aiocontextvars - imported by loguru._contextvars (delayed, conditional)
missing module named exceptiongroup - imported by loguru._better_exceptions (conditional, optional)
missing module named IPython - imported by loguru._colorama (delayed, conditional, optional)
missing module named ipykernel - imported by loguru._colorama (delayed, conditional, optional)
missing module named 'setuptools._distutils.msvc9compiler' - imported by cffi._shimmed_dist_utils (conditional, optional)
missing module named collections.Callable - imported by collections (optional), socks (optional), cffi.api (optional)
missing module named _dummy_thread - imported by numpy._core.arrayprint (optional), cffi.lock (conditional, optional)
missing module named dummy_thread - imported by cffi.lock (conditional, optional)
missing module named thread - imported by cffi.lock (conditional, optional), cffi.cparser (conditional, optional)
missing module named cStringIO - imported by cffi.ffiplatform (optional)
missing module named cPickle - imported by pycparser.ply.yacc (delayed, optional)
missing module named cffi._pycparser - imported by cffi (optional), cffi.cparser (optional)
missing module named imp - imported by Crypto.Util._raw_api (conditional), cffi.verifier (conditional), cffi._imp_emulation (optional)
missing module named StringIO - imported by six (conditional), Crypto.Util.py3compat (conditional)
missing module named numpy._core.void - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.vecmat - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ushort - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.unsignedinteger - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ulonglong - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ulong - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.uintp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.uintc - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.uint64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.uint32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.uint16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.uint - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ubyte - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.trunc - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.true_divide - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.timedelta64 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.tanh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.tan - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.subtract - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.str_ - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.square - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.spacing - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.sinh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.signedinteger - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.short - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.rint - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.right_shift - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.remainder - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.radians - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.rad2deg - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.power - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.positive - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.pi - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.not_equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.negative - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.modf - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.mod - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.minimum - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.maximum - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.matvec - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.longdouble - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.long - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_xor - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_or - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_not - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logical_and - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logaddexp2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.logaddexp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log1p - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.log - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.less_equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.less - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.left_shift - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ldexp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.lcm - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.integer - imported by numpy._core (conditional), numpy (conditional), numpy.fft._helper (top-level)
missing module named numpy._core.int8 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.int64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.int32 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.int16 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.hypot - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.heaviside - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.half - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.greater_equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.greater - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.gcd - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.frompyfunc - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.frexp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fmod - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fmin - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fmax - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.floor_divide - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.floor - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.floating - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.float_power - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.float16 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.fabs - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.expm1 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.exp - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.euler_gamma - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.equal - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.e - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.divmod - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.degrees - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.deg2rad - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.datetime64 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.cosh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.cos - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.copysign - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.conjugate - imported by numpy._core (conditional), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.conj - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.complex64 - imported by numpy._core (conditional), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.clongdouble - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.character - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.ceil - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.cbrt - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bytes_ - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.byte - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bool_ - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_xor - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_or - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_count - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.bitwise_and - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arctanh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arctan2 - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arctan - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arcsinh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arcsin - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arccosh - imported by numpy._core (conditional), numpy (conditional)
missing module named numpy._core.arccos - imported by numpy._core (conditional), numpy (conditional)
missing module named threadpoolctl - imported by numpy.lib._utils_impl (delayed, optional)
missing module named numpy._core.ones - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.hstack - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.atleast_1d - imported by numpy._core (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.atleast_3d - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
missing module named numpy._core.vstack - imported by numpy._core (top-level), numpy.lib._shape_base_impl (top-level), numpy (conditional)
missing module named numpy._core.linspace - imported by numpy._core (top-level), numpy.lib._index_tricks_impl (top-level), numpy (conditional)
missing module named numpy._core.result_type - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.number - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.max - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.array2string - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.signbit - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.isscalar - imported by numpy._core (delayed), numpy.testing._private.utils (delayed), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.isnat - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
missing module named numpy._core.array_repr - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
missing module named numpy._core.arange - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
missing module named numpy._core.float32 - imported by numpy._core (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.vecdot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.matrix_transpose - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.matmul - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.tensordot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.outer - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.cross - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.trace - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.diagonal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.reciprocal - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.sort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.argsort - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.sign - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.isnan - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.count_nonzero - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.divide - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.swapaxes - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.object_ - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.asanyarray - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.intp - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy._array_api_info (top-level)
missing module named numpy._core.atleast_2d - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.prod - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.amax - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.amin - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.moveaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.errstate - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.finfo - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.isfinite - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.sum - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.sqrt - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.multiply - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.add - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.dot - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.inf - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.all - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (delayed), numpy (conditional)
missing module named numpy._core.newaxis - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.complexfloating - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.inexact - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.cdouble - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.csingle - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.double - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.single - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.intc - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.empty_like - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level)
missing module named numpy._core.empty - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy (conditional), numpy.fft._helper (top-level)
missing module named numpy._core.zeros - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.array - imported by numpy._core (top-level), numpy.linalg._linalg (top-level), numpy.testing._private.utils (top-level), numpy.lib._polynomial_impl (top-level), numpy (conditional)
missing module named numpy._core.iinfo - imported by numpy._core (top-level), numpy.lib._twodim_base_impl (top-level), numpy (conditional)
missing module named numpy._core.transpose - imported by numpy._core (top-level), numpy.lib._function_base_impl (top-level), numpy.linalg._linalg (top-level), numpy (conditional)
missing module named numpy._core.ndarray - imported by numpy._core (top-level), numpy.lib._utils_impl (top-level), numpy.testing._private.utils (top-level), numpy (conditional)
missing module named numpy._core.asarray - imported by numpy._core (top-level), numpy.lib._array_utils_impl (top-level), numpy.linalg._linalg (top-level), numpy (conditional), numpy.fft._pocketfft (top-level), numpy.fft._helper (top-level)
missing module named 'numpy_distutils.cpuinfo' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
missing module named 'numpy_distutils.fcompiler' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
missing module named 'numpy_distutils.command' - imported by numpy.f2py.diagnose (delayed, conditional, optional)
missing module named numpy_distutils - imported by numpy.f2py.diagnose (delayed, optional)
missing module named numpy.random.RandomState - imported by numpy.random (top-level), numpy.random._generator (top-level)
missing module named yaml - imported by numpy.__config__ (delayed)
missing module named numpy._distributor_init_local - imported by numpy (optional), numpy._distributor_init (optional)
missing module named simplejson - imported by jsonpath (conditional, optional), requests.compat (conditional, optional)
missing module named dummy_threading - imported by requests.cookies (optional)
missing module named zstandard - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named compression - imported by urllib3.util.request (optional), urllib3.response (optional)
missing module named 'h2.events' - imported by urllib3.http2.connection (top-level)
missing module named 'h2.connection' - imported by urllib3.http2.connection (top-level)
missing module named h2 - imported by urllib3.http2.connection (top-level)
missing module named brotli - imported by aiohttp.compression_utils (optional), urllib3.util.request (optional), urllib3.response (optional)
missing module named brotlicffi - imported by aiohttp.compression_utils (optional), urllib3.util.request (optional), urllib3.response (optional)
missing module named win_inet_pton - imported by socks (conditional, optional)
missing module named bcrypt - imported by cryptography.hazmat.primitives.serialization.ssh (optional)
missing module named cryptography.x509.UnsupportedExtension - imported by cryptography.x509 (optional), urllib3.contrib.pyopenssl (optional)
missing module named 'pyodide.ffi' - imported by urllib3.contrib.emscripten.fetch (delayed, optional)
missing module named pyodide - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named js - imported by urllib3.contrib.emscripten.fetch (top-level)
missing module named PyV8 - imported by execjs._pyv8runtime (delayed, optional)
missing module named wsaccel - imported by websocket._utils (optional)
missing module named 'python_socks.sync' - imported by websocket._http (optional), websockets.sync.client (optional)
missing module named 'python_socks._types' - imported by websocket._http (optional)
missing module named 'python_socks._errors' - imported by websocket._http (optional)
missing module named 'wsaccel.xormask' - imported by websocket._abnf (optional)
missing module named google.protobuf.internal.use_pure_python - imported by google.protobuf.internal (conditional, optional), google.protobuf.internal.api_implementation (conditional, optional)
missing module named google.protobuf.internal._api_implementation - imported by google.protobuf.internal (optional), google.protobuf.internal.api_implementation (optional)
missing module named six.moves.range - imported by six.moves (top-level), google.protobuf.internal.python_message (top-level)
runtime module named six.moves - imported by google.protobuf.internal.python_message (top-level)
missing module named google.protobuf.pyext._message - imported by google.protobuf.pyext (conditional), google.protobuf.descriptor (conditional), google.protobuf.pyext.cpp_message (top-level)
missing module named google.protobuf.enable_deterministic_proto_serialization - imported by google.protobuf (optional), google.protobuf.internal.api_implementation (optional)
missing module named google.protobuf._use_fast_cpp_protos - imported by google.protobuf (conditional, optional), google.protobuf.internal.api_implementation (conditional, optional)
missing module named uvloop - imported by aiohttp.worker (delayed)
missing module named asyncio.DefaultEventLoopPolicy - imported by asyncio (delayed, conditional), asyncio.events (delayed, conditional)
missing module named async_timeout - imported by aiohttp.helpers (conditional), aiohttp.web_ws (conditional), aiohttp.client_ws (conditional)
missing module named 'gunicorn.workers' - imported by aiohttp.worker (top-level)
missing module named gunicorn - imported by aiohttp.worker (top-level)
missing module named aiodns - imported by aiohttp.resolver (optional)
missing module named '_typeshed.wsgi' - imported by werkzeug.exceptions (conditional), werkzeug.http (conditional), werkzeug.wsgi (conditional), werkzeug.utils (conditional), werkzeug.wrappers.response (conditional), werkzeug.test (conditional), werkzeug.datastructures.headers (conditional), werkzeug.formparser (conditional), werkzeug.wrappers.request (conditional), werkzeug.serving (conditional), werkzeug.debug (conditional), werkzeug.middleware.shared_data (conditional), werkzeug.routing.exceptions (conditional), werkzeug.routing.map (conditional)
missing module named 'watchdog.observers' - imported by werkzeug._reloader (delayed)
missing module named 'watchdog.events' - imported by werkzeug._reloader (delayed)
missing module named watchdog - imported by werkzeug._reloader (delayed)
missing module named python_socks - imported by websockets.asyncio.client (optional), websockets.sync.client (optional)
missing module named 'python_socks.async_' - imported by websockets.asyncio.client (optional)

43838
build/main/xref-main.html Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -85,32 +85,9 @@ def main():
result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
if result.returncode == 0:
# 确保输出目录正确性
if os.path.exists('dist/main') and not os.path.exists('dist/MultiPlatformGUI'):
print("🔄 修正输出目录名称...")
try:
os.rename('dist/main', 'dist/MultiPlatformGUI')
print("✅ 已重命名为: dist/MultiPlatformGUI/")
except Exception as e:
print(f"⚠️ 重命名失败: {e}")
# 验证最终输出
if os.path.exists('dist/MultiPlatformGUI'):
print("\n🎉 生产环境打包成功!")
print("📁 输出目录: dist/MultiPlatformGUI/")
print("🔇 已禁用所有日志功能,客户端不会产生日志文件")
# 验证关键文件
exe_path = 'dist/MultiPlatformGUI/main.exe'
if os.path.exists(exe_path):
size = os.path.getsize(exe_path) / 1024 / 1024 # MB
print(f"✅ 主程序: main.exe ({size:.1f} MB)")
else:
print("⚠️ 主程序文件未找到")
else:
print("\n❌ 输出目录验证失败")
if os.path.exists('dist/main'):
print("💡 发现: dist/main/ 目录,请手动重命名为 MultiPlatformGUI")
print("\n🎉 生产环境打包成功!")
print("📁 输出目录: dist/MultiPlatformGUI/")
print("🔇 已禁用所有日志功能,客户端不会产生日志文件")
else:
print("\n❌ 打包失败")

230
check_chrome_config.py Normal file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Chrome配置检查脚本
验证32位ChromeDriver是否能正常驱动64位Chrome
"""
import os
import sys
import platform
import subprocess
def check_system_info():
"""检查系统信息"""
print("🖥️ 系统信息检查")
print("=" * 40)
# 系统架构
system_arch = platform.architecture()[0]
machine = platform.machine()
print(f"系统架构: {system_arch}")
print(f"处理器: {machine}")
# 判断是否64位系统
is_64bit = system_arch == '64bit' or 'AMD64' in machine
print(f"64位系统: {'✅ 是' if is_64bit else '❌ 否'}")
return is_64bit
def check_chrome_installation():
"""检查Chrome安装情况"""
print("\n🌐 Chrome浏览器检查")
print("=" * 40)
# Chrome可能的安装路径
chrome_paths = [
r"C:\Program Files\Google\Chrome\Application\chrome.exe",
r"C:\Program Files (x86)\Google\Chrome\Application\chrome.exe",
r"C:\Users\{}\AppData\Local\Google\Chrome\Application\chrome.exe".format(os.getenv('USERNAME', ''))
]
chrome_found = False
chrome_path = None
chrome_is_64bit = False
for path in chrome_paths:
if os.path.exists(path):
chrome_found = True
chrome_path = path
# 如果Chrome在Program Files目录通常是64位
chrome_is_64bit = "Program Files" in path and "(x86)" not in path
print(f"✅ 找到Chrome: {path}")
print(f"Chrome位数: {'64位' if chrome_is_64bit else '32位或未确定'}")
break
if not chrome_found:
print("❌ 未找到Chrome浏览器")
return False, None, False
# 尝试获取Chrome版本
try:
result = subprocess.run([chrome_path, '--version'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version = result.stdout.strip()
print(f"Chrome版本: {version}")
else:
print("⚠️ 无法获取Chrome版本")
except Exception as e:
print(f"⚠️ 获取版本失败: {e}")
return True, chrome_path, chrome_is_64bit
def check_chromedriver():
"""检查ChromeDriver"""
print("\n🚗 ChromeDriver检查")
print("=" * 40)
# 可能的ChromeDriver路径
driver_paths = [
"chromedriver.exe",
"downloads/chromedriver.exe",
"./chromedriver.exe"
]
driver_found = False
driver_path = None
for path in driver_paths:
if os.path.exists(path):
driver_found = True
driver_path = path
print(f"✅ 找到ChromeDriver: {path}")
break
if not driver_found:
print("❌ 未找到ChromeDriver")
return False, None
# 获取ChromeDriver版本和架构信息
try:
result = subprocess.run([driver_path, '--version'],
capture_output=True, text=True, timeout=5)
if result.returncode == 0:
version_info = result.stdout.strip()
print(f"ChromeDriver版本: {version_info}")
# ChromeDriver通常是32位的这是正常的
print("📋 说明: ChromeDriver通常是32位程序这是正常的")
else:
print("⚠️ 无法获取ChromeDriver版本")
except Exception as e:
print(f"⚠️ 获取版本失败: {e}")
return True, driver_path
def test_selenium_connection():
"""测试Selenium连接"""
print("\n🧪 Selenium连接测试")
print("=" * 40)
try:
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.chrome.options import Options
print("✅ Selenium模块导入成功")
# 查找ChromeDriver
driver_paths = ["chromedriver.exe", "downloads/chromedriver.exe", "./chromedriver.exe"]
driver_path = None
for path in driver_paths:
if os.path.exists(path):
driver_path = path
break
if not driver_path:
print("❌ 未找到ChromeDriver跳过连接测试")
return False
# 配置Chrome选项
chrome_options = Options()
chrome_options.add_argument('--headless') # 无头模式,不显示浏览器
chrome_options.add_argument('--no-sandbox')
chrome_options.add_argument('--disable-dev-shm-usage')
# 创建服务
service = Service(driver_path)
print("🚀 正在启动Chrome WebDriver...")
# 创建WebDriver
driver = webdriver.Chrome(service=service, options=chrome_options)
# 简单测试
driver.get("data:text/html,<html><body><h1>Test Page</h1></body></html>")
title = driver.title
# 获取浏览器信息
user_agent = driver.execute_script("return navigator.userAgent")
driver.quit()
print("✅ Chrome WebDriver连接成功")
print(f"📄 测试页面标题: {title}")
print(f"🔍 浏览器信息: {user_agent}")
# 分析浏览器位数
if "WOW64" in user_agent or "Win64; x64" in user_agent:
print("🎯 确认: 运行的是64位Chrome浏览器")
else:
print("🎯 运行的Chrome浏览器位数: 32位或未确定")
return True
except ImportError:
print("❌ 未安装Selenium模块")
print("💡 安装命令: pip install selenium")
return False
except Exception as e:
print(f"❌ 连接测试失败: {e}")
return False
def main():
"""主函数"""
print("🔍 Chrome配置全面检查")
print("=" * 50)
# 系统信息检查
is_64bit_system = check_system_info()
# Chrome检查
chrome_found, chrome_path, chrome_is_64bit = check_chrome_installation()
# ChromeDriver检查
driver_found, driver_path = check_chromedriver()
# Selenium测试
selenium_ok = test_selenium_connection()
# 总结
print("\n📋 检查总结")
print("=" * 50)
print(f"64位系统: {'' if is_64bit_system else ''}")
print(f"Chrome浏览器: {'' if chrome_found else ''}")
print(f"ChromeDriver: {'' if driver_found else ''}")
print(f"Selenium测试: {'' if selenium_ok else ''}")
print("\n💡 重要说明:")
print("• 32位ChromeDriver可以完美驱动64位Chrome浏览器")
print("• 这是Google官方的标准设计不存在兼容性问题")
print("• ChromeDriver通过网络协议与Chrome通信位数无关")
if chrome_found and driver_found:
print("\n🎉 配置正常可以开始使用Selenium了")
else:
print("\n⚠️ 需要安装缺失的组件")
if not chrome_found:
print("📥 下载Chrome: https://www.google.com/chrome/")
if not driver_found:
print("📥 下载ChromeDriver: https://chromedriver.chromium.org/downloads")
if __name__ == "__main__":
main()
print()
input("按Enter键退出...")

View File

@@ -0,0 +1,113 @@
@echo off
setlocal enabledelayedexpansion
echo.
echo ==========================================
echo Chrome Complete Cleaner
echo ==========================================
echo.
echo WARNING: This script will completely remove Chrome
echo Including: Program files, user data, registry entries
echo.
set /p confirm="Continue? (Y/N): "
if /i not "%confirm%"=="Y" (
echo Operation cancelled
pause
exit /b 0
)
echo.
echo Starting Chrome cleanup...
echo.
REM 1. Kill Chrome processes
echo Stopping Chrome processes...
taskkill /f /im chrome.exe >nul 2>&1
taskkill /f /im chromedriver.exe >nul 2>&1
taskkill /f /im GoogleUpdate.exe >nul 2>&1
taskkill /f /im GoogleCrashHandler.exe >nul 2>&1
taskkill /f /im GoogleCrashHandler64.exe >nul 2>&1
timeout /t 3 /nobreak >nul
REM 2. Stop Google services
echo Stopping Google services...
sc stop gupdate >nul 2>&1
sc stop gupdatem >nul 2>&1
sc stop GoogleChromeElevationService >nul 2>&1
REM 3. Delete program files
echo Deleting program files...
if exist "C:\Program Files\Google\" (
echo Removing: C:\Program Files\Google
rmdir /s /q "C:\Program Files\Google\" >nul 2>&1
)
if exist "C:\Program Files (x86)\Google\" (
echo Removing: C:\Program Files ^(x86^)\Google
rmdir /s /q "C:\Program Files (x86)\Google\" >nul 2>&1
)
REM 4. Delete current user data
echo Deleting user data...
if exist "%LOCALAPPDATA%\Google\" (
echo Removing current user data
rmdir /s /q "%LOCALAPPDATA%\Google\" >nul 2>&1
)
if exist "%APPDATA%\Google\" (
echo Removing current user config
rmdir /s /q "%APPDATA%\Google\" >nul 2>&1
)
REM 5. Clean registry
echo Cleaning registry...
reg delete "HKEY_CURRENT_USER\Software\Google" /f >nul 2>&1
reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\Google" /f >nul 2>&1
reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Google" /f >nul 2>&1
reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Google" /f >nul 2>&1
reg delete "HKEY_CURRENT_USER\Software\Policies\Google" /f >nul 2>&1
REM 6. Clean installer records
echo Cleaning installer records...
reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\Google Chrome" /f >nul 2>&1
reg delete "HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\Google Chrome" /f >nul 2>&1
REM 7. Clean shortcuts
echo Cleaning shortcuts...
del "%PUBLIC%\Desktop\Google Chrome.lnk" >nul 2>&1
del "%USERPROFILE%\Desktop\Google Chrome.lnk" >nul 2>&1
rmdir /s /q "%PROGRAMDATA%\Microsoft\Windows\Start Menu\Programs\Google Chrome\" >nul 2>&1
rmdir /s /q "%APPDATA%\Microsoft\Windows\Start Menu\Programs\Google Chrome\" >nul 2>&1
REM 8. Clean scheduled tasks
echo Cleaning scheduled tasks...
schtasks /delete /tn "GoogleUpdateTaskMachineCore" /f >nul 2>&1
schtasks /delete /tn "GoogleUpdateTaskMachineUA" /f >nul 2>&1
REM 9. Delete services
echo Deleting services...
sc delete gupdate >nul 2>&1
sc delete gupdatem >nul 2>&1
sc delete GoogleChromeElevationService >nul 2>&1
REM 10. Clean temp files
echo Cleaning temp files...
del /s /q "%TEMP%\chrome*" >nul 2>&1
del /s /q "%TEMP%\google*" >nul 2>&1
REM 11. Flush DNS
echo Flushing DNS cache...
ipconfig /flushdns >nul 2>&1
echo.
echo Chrome cleanup completed!
echo.
echo Cleaned items:
echo - Program files
echo - User data and configs
echo - Registry entries
echo - Shortcuts
echo - Scheduled tasks
echo - System services
echo - Temp files
echo.
echo You can now reinstall Chrome!
echo.
pause

View File

@@ -9,13 +9,13 @@ import json # 用于将令牌保存为 JSON 格式
# 后端服务器配置
# BACKEND_HOST = "192.168.5.233"
BACKEND_HOST = "192.168.5.12"
# BACKEND_HOST = "shuidrop.com"
# BACKEND_HOST = "192.168.5.12"
BACKEND_HOST = "shuidrop.com"
# BACKEND_HOST = "test.shuidrop.com"
BACKEND_PORT = "8000"
# BACKEND_PORT = ""
BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
# BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
# BACKEND_PORT = "8000"
BACKEND_PORT = ""
# BACKEND_WS_URL = f"ws://{BACKEND_HOST}:{BACKEND_PORT}"
BACKEND_WS_URL = f"wss://{BACKEND_HOST}"
# WebSocket配置
WS_CONNECT_TIMEOUT = 16.0
@@ -37,7 +37,6 @@ FUTURE_TIMEOUT = 300 # 5分钟
# 终端日志配置(简化)
LOG_LEVEL = "INFO"
VERSION = "1.0"
APP_VERSION = "1.0.1" # 应用版本号(用于版本检查)
# GUI配置
WINDOW_TITLE = "AI回复连接入口-V1.0"

183
diagnose_connection.py Normal file
View File

@@ -0,0 +1,183 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
WebSocket连接诊断工具
快速定位502错误的原因
"""
import asyncio
import websockets
import requests
import socket
from urllib.parse import urlparse
import ssl
import sys
from config import BACKEND_HOST, BACKEND_WS_URL, get_gui_ws_url, get_saved_token
def test_basic_connectivity():
"""测试基础网络连通性"""
print("=== 1. 基础连通性测试 ===")
# DNS解析测试
try:
ip = socket.gethostbyname(BACKEND_HOST)
print(f"✅ DNS解析成功: {BACKEND_HOST} -> {ip}")
except Exception as e:
print(f"❌ DNS解析失败: {e}")
return False
# HTTP连通性测试
try:
response = requests.get(f"https://{BACKEND_HOST}", timeout=10)
print(f"✅ HTTPS连接成功: 状态码 {response.status_code}")
except requests.exceptions.SSLError as e:
print(f"⚠️ SSL证书问题: {e}")
return False
except requests.exceptions.ConnectionError as e:
print(f"❌ 连接失败: {e}")
return False
except requests.exceptions.Timeout:
print(f"❌ 连接超时")
return False
except Exception as e:
print(f"⚠️ HTTPS测试异常: {e}")
return True
def test_websocket_endpoint():
"""测试WebSocket端点"""
print("\n=== 2. WebSocket端点测试 ===")
# 获取token
token = get_saved_token()
if not token:
print("❌ 未找到有效token")
return False
print(f"📝 使用token: {token[:20]}...")
# 构建WebSocket URL
ws_url = get_gui_ws_url(token)
print(f"🔗 WebSocket URL: {ws_url}")
return ws_url
async def test_websocket_connection(ws_url):
"""异步测试WebSocket连接"""
print("\n=== 3. WebSocket连接测试 ===")
try:
print("🔄 尝试连接WebSocket...")
# 添加连接超时
websocket = await asyncio.wait_for(
websockets.connect(ws_url),
timeout=15.0
)
print("✅ WebSocket连接成功!")
# 测试发送消息
test_message = {"type": "ping", "data": "connection_test"}
await websocket.send(str(test_message))
print("✅ 消息发送成功")
# 等待响应
try:
response = await asyncio.wait_for(websocket.recv(), timeout=5.0)
print(f"✅ 收到响应: {response}")
except asyncio.TimeoutError:
print("⚠️ 未收到响应(超时5秒)")
await websocket.close()
print("✅ 连接正常关闭")
return True
except websockets.exceptions.InvalidStatusCode as e:
if e.status_code == 502:
print("❌ HTTP 502 Bad Gateway - 后端服务器问题")
print("💡 可能原因:")
print(" - 后端服务未启动")
print(" - Nginx配置错误")
print(" - 服务器资源不足")
else:
print(f"❌ HTTP状态码错误: {e.status_code}")
return False
except websockets.exceptions.ConnectionClosedError as e:
print(f"❌ 连接被关闭: {e}")
return False
except websockets.exceptions.WebSocketException as e:
print(f"❌ WebSocket异常: {e}")
return False
except asyncio.TimeoutError:
print("❌ 连接超时 - 服务器可能未响应")
return False
except Exception as e:
print(f"❌ 未知错误: {type(e).__name__}: {e}")
return False
def test_alternative_endpoints():
"""测试备用连接方式"""
print("\n=== 4. 备用端点测试 ===")
# 测试HTTP API端点
try:
api_url = f"https://{BACKEND_HOST}/api/health"
response = requests.get(api_url, timeout=10)
print(f"✅ API端点响应: {response.status_code}")
except Exception as e:
print(f"⚠️ API端点不可用: {e}")
# 测试WebSocket根路径
try:
root_ws_url = f"wss://{BACKEND_HOST}/ws/"
print(f"🔗 测试根WebSocket路径: {root_ws_url}")
except Exception as e:
print(f"⚠️ 根路径测试失败: {e}")
async def main():
"""主诊断流程"""
print("🔍 WebSocket连接诊断工具")
print("=" * 50)
# 1. 基础连通性
if not test_basic_connectivity():
print("\n❌ 基础连通性测试失败,请检查网络连接")
return
# 2. WebSocket端点配置
ws_url = test_websocket_endpoint()
if not ws_url:
print("\n❌ WebSocket端点配置错误")
return
# 3. WebSocket连接测试
success = await test_websocket_connection(ws_url)
# 4. 备用测试
test_alternative_endpoints()
# 5. 总结和建议
print("\n" + "=" * 50)
if success:
print("🎉 诊断完成: WebSocket连接正常!")
else:
print("❌ 诊断完成: 发现连接问题")
print("\n🔧 建议解决方案:")
print("1. 联系服务器管理员检查后端服务状态")
print("2. 检查Nginx WebSocket配置")
print("3. 确认防火墙和安全组设置")
print("4. 检查服务器资源使用情况")
print("5. 验证SSL证书有效性")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n⏹️ 诊断被用户中断")
except Exception as e:
print(f"\n\n💥 诊断工具异常: {e}")

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
dist/main/_internal/Crypto/Hash/_MD2.pyd vendored Normal file

Binary file not shown.

BIN
dist/main/_internal/Crypto/Hash/_MD4.pyd vendored Normal file

Binary file not shown.

BIN
dist/main/_internal/Crypto/Hash/_MD5.pyd vendored Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1 @@
pip

View File

@@ -0,0 +1,28 @@
Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,92 @@
Metadata-Version: 2.1
Name: MarkupSafe
Version: 3.0.2
Summary: Safely add untrusted strings to HTML/XML markup.
Maintainer-email: Pallets <contact@palletsprojects.com>
License: Copyright 2010 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Project-URL: Donate, https://palletsprojects.com/donate
Project-URL: Documentation, https://markupsafe.palletsprojects.com/
Project-URL: Changes, https://markupsafe.palletsprojects.com/changes/
Project-URL: Source, https://github.com/pallets/markupsafe/
Project-URL: Chat, https://discord.gg/pallets
Classifier: Development Status :: 5 - Production/Stable
Classifier: Environment :: Web Environment
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: BSD License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Text Processing :: Markup :: HTML
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE.txt
# MarkupSafe
MarkupSafe implements a text object that escapes characters so it is
safe to use in HTML and XML. Characters that have special meanings are
replaced so that they display as the actual characters. This mitigates
injection attacks, meaning untrusted user input can safely be displayed
on a page.
## Examples
```pycon
>>> from markupsafe import Markup, escape
>>> # escape replaces special characters and wraps in Markup
>>> escape("<script>alert(document.cookie);</script>")
Markup('&lt;script&gt;alert(document.cookie);&lt;/script&gt;')
>>> # wrap in Markup to mark text "safe" and prevent escaping
>>> Markup("<strong>Hello</strong>")
Markup('<strong>hello</strong>')
>>> escape(Markup("<strong>Hello</strong>"))
Markup('<strong>hello</strong>')
>>> # Markup is a str subclass
>>> # methods and operators escape their arguments
>>> template = Markup("Hello <em>{name}</em>")
>>> template.format(name='"World"')
Markup('Hello <em>&#34;World&#34;</em>')
```
## Donate
The Pallets organization develops and supports MarkupSafe and other
popular packages. In order to grow the community of contributors and
users, and allow the maintainers to devote more time to the projects,
[please donate today][].
[please donate today]: https://palletsprojects.com/donate

View File

@@ -0,0 +1,15 @@
MarkupSafe-3.0.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
MarkupSafe-3.0.2.dist-info/LICENSE.txt,sha256=RjHsDbX9kKVH4zaBcmTGeYIUM4FG-KyUtKV_lu6MnsQ,1503
MarkupSafe-3.0.2.dist-info/METADATA,sha256=nhoabjupBG41j_JxPCJ3ylgrZ6Fx8oMCFbiLF9Kafqc,4067
MarkupSafe-3.0.2.dist-info/RECORD,,
MarkupSafe-3.0.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
MarkupSafe-3.0.2.dist-info/WHEEL,sha256=-v_yZ08fSknsoT62oIKG9wp1eCBV9_ao2rO4BeIReTY,101
MarkupSafe-3.0.2.dist-info/top_level.txt,sha256=qy0Plje5IJuvsCBjejJyhDCjEAdcDLK_2agVcex8Z6U,11
markupsafe/__init__.py,sha256=pREerPwvinB62tNCMOwqxBS2YHV6R52Wcq1d-rB4Z5o,13609
markupsafe/__pycache__/__init__.cpython-313.pyc,,
markupsafe/__pycache__/_native.cpython-313.pyc,,
markupsafe/_native.py,sha256=2ptkJ40yCcp9kq3L1NqpgjfpZB-obniYKFFKUOkHh4Q,218
markupsafe/_speedups.c,sha256=SglUjn40ti9YgQAO--OgkSyv9tXq9vvaHyVhQows4Ok,4353
markupsafe/_speedups.cp313-win_amd64.pyd,sha256=7MA12j0aUiSeNpFy-98h_pPSqgCpLeRacgp3I-j00Yo,13312
markupsafe/_speedups.pyi,sha256=LSDmXYOefH4HVpAXuL8sl7AttLw0oXh1njVoVZp2wqQ,42
markupsafe/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0

View File

@@ -0,0 +1,5 @@
Wheel-Version: 1.0
Generator: setuptools (75.2.0)
Root-Is-Purelib: false
Tag: cp313-cp313-win_amd64

View File

@@ -0,0 +1 @@
markupsafe

Some files were not shown because too many files have changed in this diff Show More