43 Commits

Author SHA1 Message Date
Gitea Actions
2cb4ac62a2 [skip ci] Update version to 1.4.8 2025-10-09 17:52:06 +08:00
Gitea Actions
2ba26b79dd 强制空提交 2025-10-09 17:51:47 +08:00
Gitea Actions
43b54a99c7 [skip ci] Update version to 1.4.7 2025-10-09 17:36:04 +08:00
Gitea Actions
cab5b606a9 解决job构建失败的问题 2025-10-09 17:32:05 +08:00
Gitea Actions
0c6d0608c5 [skip ci] Update version to 2025-10-09 17:30:36 +08:00
Gitea Actions
2734d375c2 解决job构建失败的问题 2025-10-09 17:30:19 +08:00
Gitea Actions
286fa4e697 [skip ci] Update version to 2025-10-09 17:24:44 +08:00
Gitea Actions
39ba4f9324 修改网络问题 2025-10-09 17:24:25 +08:00
Gitea Actions
8f30ab9790 [skip ci] Update version to 2025-10-09 17:21:38 +08:00
jjz
41b686fff6 修改网络问题 2025-10-09 17:21:19 +08:00
jjz
a925ca2c99 修改网络问题 2025-10-09 17:19:50 +08:00
jjz
4d18835dc6 强制空提交 2025-10-09 17:18:36 +08:00
jjz
14dc6714bd 强制空提交 2025-10-09 17:16:59 +08:00
jjz
99fec4e225 修改网络问题 2025-10-09 17:16:28 +08:00
jjz
ccf327cd57 强制空提交 2025-10-09 17:13:14 +08:00
jjz
1392e833c6 强制空提交 2025-10-09 17:11:08 +08:00
jjz
15953380d6 修改ci/cd 2025-10-09 17:10:01 +08:00
jjz
683ee4d1e2 强制空提交 2025-10-09 17:09:51 +08:00
jjz
3b77413971 强制空提交 2025-10-09 17:08:07 +08:00
jjz
4229fc6360 强制空提交 2025-10-09 17:01:17 +08:00
jjz
99ce72106c 修改ci/cd 2025-10-09 16:52:55 +08:00
jjz
afed746f9b 强制空提交 2025-10-09 16:49:35 +08:00
jjz
e2d9018879 强制空提交 2025-10-09 16:45:31 +08:00
jjz
44c79d6c29 强制空提交 2025-10-09 16:40:37 +08:00
jjz
1c9d66fa54 修改ci/cd 2025-10-09 16:37:40 +08:00
jjz
afb844f1e6 修改ci/cd 2025-10-09 16:30:08 +08:00
jjz
d226f41957 修改ci/cd 2025-10-09 16:25:59 +08:00
jjz
497f0a3719 修改ci/cd 2025-10-09 16:23:35 +08:00
jjz
a45788c4df 强制空提交 2025-10-09 16:22:49 +08:00
jjz
a869a31639 强制空提交 2025-10-09 16:13:29 +08:00
jjz
17f63bcc14 强制空提交 2025-10-09 16:08:52 +08:00
jjz
a14ef76b97 提交ci/cd文件 2025-10-09 15:37:25 +08:00
c08b2c1e54 Todo: 补充nsis集成需要的依赖 并提交uninstall logo的图片资源 并集成 2025-09-29 16:08:16 +08:00
4f8415bd6a Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	Utils/Dy/DyUtils.py
#	Utils/Pdd/PddUtils.py
#	WebSocket/BackendClient.py
#	WebSocket/backend_singleton.py
#	config.py
#	main.py
2025-09-29 15:41:28 +08:00
1ac50b99a9 Todo: 补充nsis集成需要的依赖 并提交uninstall logo的图片资源 并集成关于GUI版本控制显示代码 2025-09-29 15:38:34 +08:00
jjz
3a17ed0da1 初步实现提示用户更新 2025-09-29 14:49:32 +08:00
jjz
d5d205e575 实现pdd抖音平台cookie登录 2025-09-29 13:01:34 +08:00
jjz
4f2706d8d9 实现抖音验证码登录 2025-09-29 11:25:43 +08:00
c58cec750f Todo: 修改因exe_token错误导致的不断重连问题
Todo: 修改打包中.bat打包文件与测试打包脚本不一致问题
New: 新增installer安装包环境搭建数据
2025-09-28 17:00:02 +08:00
jjz
7f9894908d 修复回消息 2025-09-26 15:38:23 +08:00
jjz
13b30c6f75 Merge branch 'develop' of http://8.155.9.53:3000/magua/shuidrop_gui into develop 2025-09-26 14:40:16 +08:00
jjz
4dbc1a0ace 添加后端重连后剔除ws功能 2025-09-26 14:35:28 +08:00
jjz
19de5dee88 添加后端重连后剔除ws功能 2025-09-26 14:35:14 +08:00
16 changed files with 3056 additions and 278 deletions

419
.gitea/README.md Normal file
View File

@@ -0,0 +1,419 @@
# 🚀 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

@@ -0,0 +1,469 @@
#!/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

@@ -0,0 +1,166 @@
#!/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

@@ -0,0 +1,178 @@
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
for ($i = 1; $i -le 3; $i++) {
Write-Host "Attempt $i to create version record..."
python .gitea/scripts/gui_version_creator.py
if ($LASTEXITCODE -eq 0) {
Write-Host "Version record created successfully"
$SUCCESS = $true
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
}
# Verify version was updated
if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]
Write-Host "Version updated successfully: $VERSION"
}
}
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..."
# Read version from config.py
$VERSION = ""
if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]
Write-Host "New version: $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 $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: |
# Read version from config.py
$VERSION = "Unknown"
if (Test-Path "config.py") {
$configContent = Get-Content "config.py" -Raw
if ($configContent -match 'APP_VERSION\s*=\s*"([\d.]+)"') {
$VERSION = $matches[1]
}
}
Write-Host "=========================================="
Write-Host "GUI Version Release Summary"
Write-Host "=========================================="
Write-Host "Author: ${{ github.actor }}"
Write-Host "Branch: ${{ github.ref_name }}"
Write-Host "Version: $VERSION"
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host "Commit: ${{ github.sha }}"
Write-Host "=========================================="

View File

@@ -12,6 +12,8 @@ import threading
from datetime import datetime from datetime import datetime
from dataclasses import dataclass, asdict from dataclasses import dataclass, asdict
from urllib.parse import urlencode from urllib.parse import urlencode
import re
import random
import config import config
# 导入 message_arg 中的方法 # 导入 message_arg 中的方法
@@ -19,6 +21,503 @@ from Utils.Dy.message_arg import send_message, get_user_code, heartbeat_message
from Utils.message_models import PlatformMessage from Utils.message_models import PlatformMessage
# ===== 抖音登录相关类集成开始 =====
class DyLogin:
"""抖音登录核心类集成自Dylogin.py适配后端通知机制"""
def __init__(self, log_callback=None):
self.headers = {
"authority": "doudian-sso.jinritemai.com",
"accept": "application/json, text/plain, */*",
"accept-language": "zh-CN,zh;q=0.9",
"cache-control": "no-cache",
"content-type": "application/x-www-form-urlencoded",
"origin": "https://fxg.jinritemai.com",
"pragma": "no-cache",
"referer": "https://fxg.jinritemai.com/",
"sec-ch-ua": "\"Not_A Brand\";v=\"8\", \"Chromium\";v=\"120\", \"Google Chrome\";v=\"120\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Windows\"",
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
"x-requested-with": "XMLHttpRequest",
"x-tt-passport-csrf-token": "8aa53236d26cf3328f70ef5d7a52e2ed"
}
self.cookies = {
"passport_csrf_token": "8aa53236d26cf3328f70ef5d7a52e2ed",
"passport_csrf_token_default": "8aa53236d26cf3328f70ef5d7a52e2ed",
"ttwid": "1%7CFgSgrZadP_YyHmeFQZ8Sj2Qo2isOgOYHVWkzE4NIOMM%7C1759041029%7C80598fd5e92a57b97469827b096864a940b5de23748185987bc2f45f25f8c88b"
}
self.log_callback = log_callback
def _log(self, message, level="INFO"):
"""内部日志方法"""
if self.log_callback:
self.log_callback(message, level)
else:
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
color_map = {
"ERROR": "\033[91m",
"WARNING": "\033[93m",
"SUCCESS": "\033[92m",
"DEBUG": "\033[96m",
}
color = color_map.get(level, "")
reset = "\033[0m"
print(f"{color}[{timestamp}] [{level}] {message}{reset}")
@staticmethod
def encrypt(data=None):
"""抖音数据加密方法"""
if data is None:
return ""
e = lambda t: [ord(c) for c in str(t)]
e = e(data)
n = []
for r in range(len(e)):
n.append(hex(5 ^ e[r])[2:])
return "".join(n)
def send_activation_code(self, mobile_phone):
"""发送激活码"""
url = "https://doudian-sso.jinritemai.com/send_activation_code/v2/"
params = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"account_sdk_source_info": "7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273f63646976602927666d776a686061776c736077273f63646976602927766d60696961776c736077273f63646976602927756970626c6b76273f302927756077686c76766c6a6b76273f5e7e276b646860273f276b6a716c636c6664716c6a6b762729277671647160273f2761606b6c606127785829276c6b6b60774d606c626d71273f3c353329276c6b6b6077526c61716d273f3432353229276a707160774d606c626d71273f3435373229276a70716077526c61716d273f34323532292776716a64776260567164717076273f7e276c6b61607d60614147273f7e276c6167273f276a676f6066712729276a75606b273f2763706b66716c6a6b2729276c6b61607d60614147273f276a676f6066712729274c41474e607c57646b6260273f2763706b66716c6a6b2729276a75606b4164716467647660273f27706b6160636c6b60612729276c7656646364776c273f636469766029276d6476436071666d273f71777060782927696a66646956716a77646260273f7e276c76567075756a77714956716a77646260273f717770602927766c7f60273f343735343d34292772776c7160273f7177706078292776716a7764626054706a7164567164717076273f7e277076646260273f363c333133292774706a7164273f34323c30343632323c3329276c7655776c73647160273f6364697660787829277260676269273f7e2773606b616a77273f27426a6a626960254c6b662b252d4c6b7160692c27292777606b6160776077273f27444b424940252d4c6b71606929254c6b7160692d572c254c776c762d572c255d6025427764756d6c6676252d357d35353535443244352c25416c77606671364134342573765a305a352575765a305a35292541364134342c277829276b6a716c636c6664716c6a6b556077686c76766c6a6b273f2761606b6c6061272927756077636a7768646b6660273f7e27716c68604a776c626c6b273f3432303c3531343537363d353d2b3d2927707660614f564d606475566c7f60273f333534343d31323c29276b64736c6264716c6a6b516c686c6b62273f7e276160666a616061476a617c566c7f60273f3030313d2927606b71777c517c7560273f276b64736c6264716c6a6b2729276c6b6c716c64716a77517c7560273f276b64736c6264716c6a6b2729276b646860273f276d717175763f2a2a637d622b6f6c6b776c716068646c2b666a682a696a626c6b2a666a68686a6b27292777606b61607747696a666e6c6b62567164717076273f276b6a6b2867696a666e6c6b62272927766077736077516c686c6b62273f276c6b6b60772966616b286664666d602960616260296a776c626c6b272927627069605671647771273f276b6a6b602729276270696041707764716c6a6b273f276b6a6b602778782927776074706076715a6d6a7671273f27637d622b6f6c6b776c716068646c2b666a68272927776074706076715a7564716d6b646860273f272a696a626c6b2a666a68686a6b27292767776a72766077273f7e7878",
"msToken": "",
"X-Bogus": "DFSzswVL1tybaQLLC9jRHiB9Piu6",
"_signature": "_02B4Z6wo00001P1YC5QAAIDDwPERCIiHDXT9WA8AAFeUKIdq.vB20O6tjorWCqmtqK591BKCWaGNpder-vHZ3rvQkbxFhNXfTJcMBW66GwjlIj-NQ6d52sU1iZ1ediX423KeCS5rakvHWLrua8"
}
data = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"mix_mode": "1",
"service": "https://fxg.jinritemai.com",
"type": "3731",
"mobile": self.encrypt(data=mobile_phone),
"captcha_key": ""
}
response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data)
self._log(f"发送验证码响应: {response.text}", "DEBUG")
return response
def verify(self, mobile_phone, code):
"""验证手机验证码"""
url = "https://doudian-sso.jinritemai.com/quick_login/v2/"
params = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"account_sdk_source_info": "7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273f63646976602927666d776a686061776c736077273f63646976602927766d60696961776c736077273f63646976602927756970626c6b76273f302927756077686c76766c6a6b76273f5e7e276b646860273f276b6a716c636c6664716c6a6b762729277671647160273f2761606b6c606127785829276c6b6b60774d606c626d71273f3c353329276c6b6b6077526c61716d273f3432353229276a707160774d606c626d71273f3435373229276a70716077526c61716d273f34323532292776716a64776260567164717076273f7e276c6b61607d60614147273f7e276c6167273f276a676f6066712729276a75606b273f2763706b66716c6a6b2729276c6b61607d60614147273f276a676f6066712729274c41474e607c57646b6260273f2763706b66716c6a6b2729276a75606b4164716467647660273f27706b6160636c6b60612729276c7656646364776c273f636469766029276d6476436071666d273f71777060782927696a66646956716a77646260273f7e276c76567075756a77714956716a77646260273f717770602927766c7f60273f343735343d34292772776c7160273f7177706078292776716a7764626054706a7164567164717076273f7e277076646260273f363c333133292774706a7164273f34323c30343632323c3329276c7655776c73647160273f6364697660787829277260676269273f7e2773606b616a77273f27426a6a626960254c6b662b252d4c6b7160692c27292777606b6160776077273f27444b424940252d4c6b71606929254c6b7160692d572c254c776c762d572c255d6025427764756d6c6676252d357d35353535443244352c25416c77606671364134342573765a305a352575765a305a35292541364134342c277829276b6a716c636c6664716c6a6b556077686c76766c6a6b273f2761606b6c6061272927756077636a7768646b6660273f7e27716c68604a776c626c6b273f3432303c3531343537363d353d2b3d2927707660614f564d606475566c7f60273f33343337323d373c29276b64736c6264716c6a6b516c686c6b62273f7e276160666a616061476a617c566c7f60273f3030313d2927606b71777c517c7560273f276b64736c6264716c6a6b2729276c6b6c716c64716a77517c7560273f276b64736c6264716c6a6b2729276b646860273f276d717175763f2a2a637d622b6f6c6b776c716068646c2b666a682a696a626c6b2a666a68686a6b27292777606b61607747696a666e6c6b62567164717076273f276b6a6b2867696a666e6c6b62272927766077736077516c686c6b62273f276c6b6b60772966616e286664666d602960616260296a776c626c6b272927627069605671647771273f276b6a6b602729276270696041707764716c6a6b273f276b6a6b602778782927776074706076715a6d6a7671273f27637d622b6f6c6b776c716068646c2b666a68272927776074706076715a7564716d6b646860273f272a696a626c6b2a666a68686a6b27292767776a72766077273f7e7878",
"msToken": "",
"X-Bogus": "DFSzswVL1tybaQLLC9jRHiB9Piu6",
"_signature": "_02B4Z6wo00001n3abFwAAIDBQHN2wdsMnWZ92mjAAPe9KIdq.vB20O6tjorWCqmtqK591BKCWaGNpder-vHZ3rvQkbxFhNXfTJcMBW66GwjlIj-NQ6d52sU1iZ1ediX423KeCS5rakvHWLru09"
}
data = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"service": "https://fxg.jinritemai.com",
"subject_aid": "4966",
"mix_mode": "1",
"mobile": self.encrypt(data=mobile_phone),
"code": self.encrypt(data=code),
"captcha_key": "",
"ewid": "72c89cf89652d486d53b316711709044",
"seraph_did": "",
"web_did": "72c89cf89652d486d53b316711709044",
"pc_did": "",
"redirect_sso_to_login": "false"
}
response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data)
self.cookies.update(response.cookies.get_dict())
self._log(f"验证码验证响应: {response.text}", "DEBUG")
# 🔥 修复:增加错误处理,检查响应是否包含错误信息
ticket = None
try:
# 尝试解析JSON响应
response_data = response.json()
if "error_code" in response_data:
error_code = response_data.get("error_code", -1)
# 🔥 修复error_code=0表示成功非0才是错误
if error_code != 0:
# 抖音返回了错误信息
error_msg = response_data.get("description", "验证码验证失败")
self._log(f"抖音验证码验证失败: {error_msg} (错误码: {error_code})", "ERROR")
raise Exception(f"{error_msg}")
else:
# error_code=0表示成功尝试从redirect_url中提取ticket
self._log(f"✅ 抖音验证码验证成功 (错误码: {error_code})", "SUCCESS")
redirect_url = response_data.get("redirect_url", "")
if redirect_url:
# 从redirect_url中提取ticket
ticket_matches = re.findall(r'ticket=([^&]+)', redirect_url)
if ticket_matches:
ticket = ticket_matches[0]
self._log(f"✅ 从redirect_url中提取到ticket: {ticket[:20]}...", "SUCCESS")
except json.JSONDecodeError:
# 如果不是JSON响应继续用原来的方式解析
pass
# 🔥 修复如果JSON中没有获取到ticket尝试用原来的方式解析
if not ticket:
ticket_matches = re.findall('ticket=(.*?)",', response.text)
if ticket_matches:
ticket = ticket_matches[0]
self._log(f"✅ 从响应文本中提取到ticket: {ticket[:20]}...", "SUCCESS")
# 最终检查是否获取到ticket
if not ticket:
# 没有找到ticket说明验证失败
self._log("抖音验证码验证失败响应中未找到ticket信息", "ERROR")
raise Exception("验证码验证失败服务器未返回有效的ticket")
return ticket
def callback(self, ticket):
"""回调获取登录状态"""
url = f"https://fxg.jinritemai.com/passport/sso/login/callback/?next=https%3A%2F%2Ffxg.jinritemai.com%2Flogin%2Fcommon&ticket={ticket}"
response = requests.get(url, headers=self.headers, cookies=self.cookies, allow_redirects=False)
self.cookies.update(response.cookies.get_dict())
self._log(f"回调响应状态: {response.status_code}", "DEBUG")
def subject_list(self):
"""获取登录主体列表"""
url = "https://fxg.jinritemai.com/ecomauth/loginv1/get_login_subject"
params = {
"bus_type": "1",
"login_source": "doudian_pc_web",
"entry_source": "0",
"bus_child_type": "0",
"_lid": "438338769861"
}
response = requests.get(url, headers=self.headers, cookies=self.cookies, params=params)
response_data = response.json()
self._log(f"登录主体列表响应: {response_data}", "DEBUG")
login_subject_list = response_data.get("data", {}).get("login_subject_list", [])
if not login_subject_list:
raise Exception("未获取到登录主体列表")
login_subject_uid = login_subject_list[0].get("subject_id")
user_identity_id = login_subject_list[0].get("member_id")
encode_shop_id = login_subject_list[0].get("encode_shop_id")
return login_subject_uid, user_identity_id, encode_shop_id
def tab_shop_login(self, login_subject_uid, user_identity_id, encode_shop_id):
"""切换店铺登录"""
url = "https://doudian-sso.jinritemai.com/aff/subject/login/"
params = {
"subject_aid": "4966",
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"account_sdk_source_info": "7e276d64776172647760466a6b66707777606b667c273f3433292772606761776c736077273f63646976602927666d776a686061776c736077273f63646976602927766d60696961776c736077273f63646976602927756970626c6b76273f302927756077686c76766c6a6b76273f5e7e276b646860273f276b6a716c636c6664716c6a6b762729277671647160273f2761606b6c606127785829276c6b6b60774d606c626d71273f3c353329276c6b6b6077526c61716d273f3432353229276a707160774d606c626d71273f3435373229276a70716077526c61716d273f34323532292776716a64776260567164717076273f7e276c6b61607d60614147273f7e276c6167273f276a676f6066712729276a75606b273f2763706b66716c6a6b2729276c6b61607d60614147273f276a676f6066712729274c41474e607c57646b6260273f2763706b66716c6a6b2729276a75606b4164716467647660273f27706b6160636c6b60612729276c7656646364776c273f636469766029276d6476436071666d273f71777060782927696a66646956716a77646260273f7e276c76567075756a77714956716a77646260273f717770602927766c7f60273f343735343d32292772776c7160273f7177706078292776716a7764626054706a7164567164717076273f7e277076646260273f363c333133292774706a7164273f34323c30343632323c3329276c7655776c73647160273f6364697660787829277260676269273f7e2773606b616a77273f27426a6a626960254c6b662b252d4c6b7160692c27292777606b6160776077273f27444b424940252d4c6b71606929254c6b7160692d572c254c776c762d572c255d6025427764756d6c6676252d357d35353535443244352c25416c77606671364134342573765a305a352575765a305a35292541364134342c277829276b6a716c636c6664716c6a6b556077686c76766c6a6b273f2761606b6c6061272927756077636a7768646b6660273f7e27716c68604a776c626c6b273f3432303c3531343537363d353d2b3d2927707660614f564d606475566c7f60273f33343337323d373c29276b64736c6264716c6a6b516c686c6b62273f7e276160666a616061476a617c566c7f60273f3030313d2927606b71777c517c7560273f276b64736c6264716c6a6b2729276c6b6c716c64716a77517c7560273f276b64736c6264716c6a6b2729276b646860273f276d717175763f2a2a637d622b6f6c6b776c716068646c2b666a682a696a626c6b2a666a68686a6b27292777606b61607747696a666e6c6b62567164717076273f276b6a6b2867696a666e6c6b62272927766077736077516c686c6b62273f276c6b6b60772966616e286664666d602960616260296a776c626c6b272927627069605671647771273f276b6a6b602729276270696041707764716c6a6b273f276b6a6b602778782927776074706076715a6d6a7671273f27637d622b6f6c6b776c716068646c2b666a68272927776074706076715a7564716d6b646860273f272a696a626c6b2a666a68686a6b27292767776a72766077273f7e7878",
"msToken": "",
"X-Bogus": "DFSzswVL1tybaQLLC9jRHiB9Piu6",
"_signature": "_02B4Z6wo00001n3abFwAAIDBQHN2wdsMnWZ92mjAAPe9KIdq.vB20O6tjorWCqmtqK591BKCWaGNpder-vHZ3rvQkbxFhNXfTJcMBW66GwjlIj-NQ6d52sU1iZ1ediX423KeCS5rakvHWLru09"
}
data = {
"fp": "verify_mg3bm79v_56d5acc9_118c_ed40_6a1d_f3e184d5c666",
"aid": "4272",
"language": "zh",
"account_sdk_source": "web",
"service": "https://fxg.jinritemai.com",
"subject_aid": "4966",
"mix_mode": "1",
"login_subject_uid": login_subject_uid,
"user_identity_id": user_identity_id,
"encode_shop_id": encode_shop_id,
"captcha_key": "",
"ewid": "72c89cf89652d486d53b316711709044",
"seraph_did": "",
"web_did": "72c89cf89652d486d53b316711709044",
"pc_did": "",
"redirect_sso_to_login": "false"
}
response = requests.post(url, headers=self.headers, cookies=self.cookies, params=params, data=data)
self._log(f"切换店铺登录响应: {response.text}", "DEBUG")
ticket = re.findall('ticket=(.*?)",', response.text)[0]
self.cookies.update(response.cookies.get_dict())
# 🔥 执行完整的登录回调流程参考Dylogin.py
callback_url = f"https://fxg.jinritemai.com/passport/sso/aff/login/callback/?next=https%3A%2F%2Ffxg.jinritemai.com&ticket={ticket}&aid=4272&subject_aid=4966"
response = requests.get(callback_url, headers=self.headers, cookies=self.cookies, allow_redirects=False)
self.cookies.update(response.cookies.get_dict())
self._log(f"主登录回调响应状态: {response.status_code}", "DEBUG")
# 最终回调获取完整cookies
final_callback_url = f"https://fxg.jinritemai.com/ecomauth/loginv1/callback?login_source=doudian_pc_web&subject_aid=4966&encode_shop_id={encode_shop_id}&member_id={user_identity_id}&bus_child_type=0&entry_source=0&ecom_login_extra=&_lid=464136070178"
response = requests.get(final_callback_url, headers=self.headers, cookies=self.cookies)
self.cookies.update(response.cookies.get_dict())
self._log(f"最终回调响应状态: {response.status_code}", "DEBUG")
return ticket
def get_shop_config(self):
"""获取抖音平台配置信息SHOP_ID、PIGEON_CID等"""
try:
self._log("🔄 开始获取抖音平台配置", "DEBUG")
# 获取配置信息的API调用
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
'Accept': 'application/json, text/plain, */*',
'Referer': 'https://fxg.jinritemai.com/',
}
params = [
('biz_type', '4'),
('PIGEON_BIZ_TYPE', '2'),
('_ts', int(time.time() * 1000)),
('_pms', '1'),
('FUSION', 'true'),
]
response = requests.get(
'https://pigeon.jinritemai.com/chat/api/backstage/conversation/get_link_info',
params=params,
cookies=self.cookies,
headers=headers,
timeout=30
)
self._log(f"配置请求响应状态: {response.status_code}", "DEBUG")
self._log(f"配置响应内容: {response.text}", "DEBUG")
if response.status_code != 200:
self._log(f"❌ 配置请求失败: HTTP {response.status_code}", "ERROR")
return None
data = response.json()
if data.get('code') != 0:
error_msg = data.get('message', '未知错误')
self._log(f"❌ 获取配置失败: {error_msg}", "ERROR")
return None
config_data = data.get('data', {})
# 🔥 构造抖音平台必需的配置信息
shop_config = {
'SHOP_ID': '217051461', # 从登录响应或配置中获取
'PIGEON_CID': '1216524360102748', # 从配置中获取
}
# 如果API返回了相关信息使用API返回的值
if 'shopId' in config_data:
shop_config['SHOP_ID'] = str(config_data['shopId'])
if 'pigeonCid' in config_data:
shop_config['PIGEON_CID'] = str(config_data['pigeonCid'])
self._log(f"✅ 获取到抖音配置: SHOP_ID={shop_config['SHOP_ID']}, PIGEON_CID={shop_config['PIGEON_CID']}",
"SUCCESS")
return shop_config
except Exception as e:
self._log(f"❌ 获取抖音配置失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return None
def _send_verification_needed_message(self, store_id, phone_number=None):
"""向后端发送需要验证码的通知"""
try:
self._log(f"开始发送验证码需求通知店铺ID: {store_id}, 手机号: {phone_number}", "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
}
self._log(f"准备发送验证码通知消息: {message}", "DEBUG")
backend.send_message(message)
self._log("✅ 成功向后端发送验证码需求通知(含手机号)", "SUCCESS")
else:
self._log("❌ 后端客户端为空,无法发送验证码需求通知", "ERROR")
except Exception as e:
self._log(f"❌ 发送验证码需求通知失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def _send_verification_error_message(self, store_id, error_msg):
"""向后端发送验证码错误的通知"""
try:
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",
"store_id": store_id,
"status": False,
"content": error_msg
}
self._log(f"准备发送验证码错误消息: {message}", "DEBUG")
backend.send_message(message)
self._log("✅ 成功向后端发送验证码错误通知", "SUCCESS")
else:
self._log("❌ 后端客户端为空,无法发送验证码错误通知", "ERROR")
except Exception as e:
self._log(f"❌ 发送验证码错误通知失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def _send_login_success_message(self, store_id):
"""向后端发送登录成功的通知"""
try:
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信息
}
self._log(f"准备发送登录成功消息: {message}", "DEBUG")
backend.send_message(message)
self._log("✅ 成功向后端发送登录成功通知", "SUCCESS")
else:
self._log("❌ 获取后端客户端失败", "ERROR")
except Exception as e:
self._log(f"❌ 发送登录成功通知失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def _send_login_failure_message(self, store_id, error_msg):
"""向后端发送登录失败的通知"""
try:
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",
"store_id": store_id,
"status": False, # 登录失败
"content": error_msg # 官方返回的失败原因
}
self._log(f"准备发送登录失败消息: {message}", "DEBUG")
backend.send_message(message)
self._log("✅ 成功向后端发送登录失败通知", "SUCCESS")
else:
self._log("❌ 获取后端客户端失败", "ERROR")
except Exception as e:
self._log(f"❌ 发送登录失败通知失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
def request_verification_code(self, phone_number, store_id, original_phone=None):
"""向抖音平台请求发送验证码"""
self._log(f"开始请求验证码,手机号: {phone_number}, 店铺ID: {store_id}", "INFO")
try:
# 🔥 发送验证码到手机(抖音会自动加密原始手机号)
response = self.send_activation_code(phone_number)
self._log(f"发送验证码请求结果: {response.text}")
# 发送消息给后端,告知需要验证码(使用原始手机号)
self._log("准备向后端发送验证码需求通知", "INFO")
phone_for_backend = original_phone if original_phone else phone_number
self._send_verification_needed_message(store_id, phone_for_backend)
# 这里需要等待后端重新下发包含验证码的登录参数
return None
except Exception as e:
self._log(f"❌ 请求验证码失败: {e}", "ERROR")
self._send_verification_error_message(store_id, f"发送验证码失败: {str(e)}")
return None
def login_with_params(self, login_params, store_id=None):
"""使用后端下发的登录参数进行登录(与拼多多保持一致的实现)"""
self._log("🚀 [DyLogin] 开始使用参数登录", "INFO")
# 检查验证码字段(兼容 code 和 verification_code
verification_code = login_params.get("verification_code") or login_params.get("code", "")
phone_number = login_params.get("phone_number", "")
encrypted_phone = login_params.get("encrypted_phone", "")
self._log(f"📋 [DyLogin] 登录参数: phone_number={phone_number}, 包含验证码={bool(verification_code)}", "DEBUG")
try:
if not verification_code:
# 第一次登录,需要发送验证码
self._log("检测到需要手机验证码,正在调用发送验证码方法", "INFO")
self._log(f"为手机号 {phone_number} 发送验证码", "INFO")
# 🔥 抖音需要传递原始手机号让其自己加密,不能传递已经加密的手机号
self.request_verification_code(phone_number, store_id, phone_number)
return "need_verification_code"
else:
# 带验证码登录
self._log(f"开始验证码登录,验证码: {verification_code}", "INFO")
try:
# 🔥 抖音需要传递原始手机号让其自己加密
ticket = self.verify(phone_number, verification_code)
self._log(f"✅ 验证码验证成功获取到ticket", "SUCCESS")
# 执行后续登录流程
self.callback(ticket)
login_subject_uid, user_identity_id, encode_shop_id = self.subject_list()
self.tab_shop_login(login_subject_uid, user_identity_id, encode_shop_id)
# 🔥 获取抖音平台必需的配置信息SHOP_ID和PIGEON_CID
self._log("🔄 开始获取抖音平台配置信息", "INFO")
shop_config = self.get_shop_config()
if shop_config:
# 将配置信息添加到cookies中
self.cookies.update(shop_config)
self._log("🎉 登录成功!配置信息已获取", "SUCCESS")
# 🔥 发送登录成功通知给后端(与拼多多保持一致)
self._send_login_success_message(store_id)
return self.cookies
else:
error_msg = "获取抖音平台配置信息失败"
self._log(f"{error_msg}", "ERROR")
self._send_login_failure_message(store_id, error_msg)
return "login_failure"
except Exception as e:
# 验证码错误或其他登录失败
error_msg = f"验证码验证失败: {str(e)}"
self._log(f"{error_msg}", "ERROR")
self._send_verification_error_message(store_id, error_msg)
return "verification_code_error"
except Exception as e:
# 登录过程中的其他错误
error_msg = f"登录过程出错: {str(e)}"
self._log(f"{error_msg}", "ERROR")
self._send_login_failure_message(store_id, error_msg)
return "login_failure"
# ===== 抖音登录相关类集成结束 =====
# 抖音WebSocket管理器类 # 抖音WebSocket管理器类
class DouYinWebsocketManager: class DouYinWebsocketManager:
_instance = None _instance = None
@@ -100,53 +599,21 @@ class DouYinBackendService:
return True return True
async def send_message_to_backend(self, platform_message): async def send_message_to_backend(self, platform_message):
"""改为通过单后端连接发送,需携带store_id""" """🔥 改为通过单后端连接发送,与拼多多保持完全一致的逻辑"""
try: try:
from WebSocket.backend_singleton import get_backend_client from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client() backend = get_backend_client()
if not backend: if not backend:
return None return None
# 从platform_message中构造统一上行结构并附加store_id # 🔥 确保消息包含store_id与拼多多一致
body = platform_message.get('body', {}) if isinstance(platform_message, dict) else {} if isinstance(platform_message, dict):
sender_id = platform_message.get('sender', {}).get('id', '') if isinstance(platform_message, dict) else '' if 'store_id' not in platform_message and self.current_store_id:
platform_message['store_id'] = self.current_store_id
# 优先取消息内的store_id其次取body内再次退回当前会话store_id # 🔥 通过统一后端连接发送(与拼多多完全一致)
store_id = (platform_message.get('store_id') backend.send_message(platform_message)
or body.get('store_id') return True
or self.current_store_id
or '')
# 检查消息类型如果是特殊类型如staff_list保持原格式
message_type = platform_message.get('type', 'message')
if message_type == 'staff_list':
# 对于客服列表消息,直接转发原始格式
msg = platform_message.copy()
# 确保store_id正确
msg['store_id'] = store_id
else:
# 对于普通消息,使用原有的格式转换逻辑
msg_type = platform_message.get('msg_type', 'text')
content_for_backend = platform_message.get('content', '')
pin_image = platform_message.get('pin_image')
if not pin_image:
pin_image = ""
else:
pass
# 构造标准消息格式
msg = {
'type': 'message',
'content': content_for_backend,
'pin_image': pin_image,
'msg_type': msg_type,
'sender': {'id': sender_id},
'store_id': store_id
}
backend.send_message(msg)
return None
except Exception: except Exception:
return None return None
@@ -201,7 +668,7 @@ class DouYinMessageHandler:
print(f"[DY Handler] 创建实例 {self.instance_id} for store {store_id}") print(f"[DY Handler] 创建实例 {self.instance_id} for store {store_id}")
def get_casl(self): def get_casl(self):
"""获取可分配客服列表""" """获取可分配客服列表 - 根据原始代码实现"""
headers = { headers = {
"authority": "pigeon.jinritemai.com", "authority": "pigeon.jinritemai.com",
"accept": "application/json, text/plain, */*", "accept": "application/json, text/plain, */*",
@@ -226,16 +693,27 @@ class DouYinMessageHandler:
"_ts": int(time.time() * 1000), "_ts": int(time.time() * 1000),
"_pms": "1", "_pms": "1",
"FUSION": "true", "FUSION": "true",
"verifyFp": "", "verifyFp": "", # 🔥 恢复为空字符串,因为原始代码中也是空的
"_v": "1.0.1.3585" "_v": "1.0.1.3585"
} }
try: try:
self._log(f"🔄 正在获取客服列表cookies包含字段: {list(self.cookie.keys())}", "DEBUG")
# 🔥 按照原始代码的方式处理响应
response = requests.get(url, headers=headers, cookies=self.cookie, params=params).json() response = requests.get(url, headers=headers, cookies=self.cookie, params=params).json()
self._log(f"客服列表API响应内容: {response}", "DEBUG")
if response.get('code') == 0: if response.get('code') == 0:
return response.get('data', []) staff_data = response.get('data', [])
return None self._log(f"✅ 成功获取客服列表,共 {len(staff_data)} 个客服", "SUCCESS")
return staff_data
else:
error_msg = response.get('message', '未知错误')
self._log(f"❌ 客服列表API返回错误: code={response.get('code')}, message={error_msg}", "ERROR")
return None
except Exception as e: except Exception as e:
self._log(f"❌ 获取客服列表失败: {e}", "ERROR") self._log(f"❌ 获取客服列表失败: {e}", "ERROR")
import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return None return None
def transfer_conversation(self, receiver_id, shop_id, staff_id): def transfer_conversation(self, receiver_id, shop_id, staff_id):
@@ -282,18 +760,26 @@ class DouYinMessageHandler:
try: try:
# 获取客服列表 # 获取客服列表
staff_list = self.get_casl() staff_list = self.get_casl()
if not staff_list: if staff_list is None:
self._log("⚠️ 获取客服列表失败", "WARNING") self._log("⚠️ 获取客服列表失败", "WARNING")
return False return False
# 转换客服数据格式 # 🔥 处理空客服列表的情况API成功但无客服
if len(staff_list) == 0:
self._log("📋 当前无可分配客服,发送空列表到后端", "INFO")
# 继续处理,发送空列表给后端
# 转换客服数据格式 - 🔥 根据原始代码调试实际字段名
staff_infos = [] staff_infos = []
for staff in staff_list: for staff in staff_list:
# 打印原始数据结构用于调试
self._log(f"🔍 原始客服数据结构: {staff}", "DEBUG")
staff_info = StaffInfo( staff_info = StaffInfo(
staff_id=str(staff.get('staffId', '')), staff_id=str(staff.get('staffId', '') or staff.get('staff_id', '') or staff.get('id', '')),
name=staff.get('staffName', ''), name=staff.get('staffName', '') or staff.get('staff_name', '') or staff.get('name', ''),
status=staff.get('status', 0), status=str(staff.get('status', 0) or staff.get('state', 0)),
department=staff.get('department', ''), department=staff.get('department', '') or staff.get('dept', ''),
online=staff.get('online', True) online=staff.get('online', True)
) )
staff_infos.append(staff_info.to_dict()) staff_infos.append(staff_info.to_dict())
@@ -312,9 +798,15 @@ class DouYinMessageHandler:
# 发送到后端 # 发送到后端
await self.ai_service.send_message_to_backend(message_template.to_dict()) await self.ai_service.send_message_to_backend(message_template.to_dict())
self._log(f"发送客服列表消息的结构体为: {message_template.to_json()}") self._log(f"发送客服列表消息的结构体为: {message_template.to_json()}")
self._log(f"✅ [DY] 成功发送客服列表到后端,共 {len(staff_infos)} 个客服", "SUCCESS")
print(f"🔥 [DY] 客服列表已上传到后端: {len(staff_infos)} 个客服") if len(staff_infos) > 0:
print(f"[DY] 客服详情: {[{'id': s['staff_id'], 'name': s['name']} for s in staff_infos]}") self._log(f"[DY] 成功发送客服列表到后端,共 {len(staff_infos)} 个客服", "SUCCESS")
print(f"🔥 [DY] 客服列表已上传到后端: {len(staff_infos)} 个客服")
print(f"[DY] 客服详情: {[{'id': s['staff_id'], 'name': s['name']} for s in staff_infos]}")
else:
self._log(f"✅ [DY] 成功发送空客服列表到后端(当前无可分配客服)", "SUCCESS")
print(f"🔥 [DY] 空客服列表已上传到后端(当前无可分配客服)")
return True return True
except Exception as e: except Exception as e:
@@ -603,26 +1095,28 @@ class DouYinMessageHandler:
talk_id = user_info.get("talk_id", 0) talk_id = user_info.get("talk_id", 0)
p_id = user_info.get("p_id", 0) p_id = user_info.get("p_id", 0)
# 准备发送者和接收者信息 # 🔥 统一消息类型检测逻辑(与拼多多保持一致)
sender_info = { content = message_content
"id": str(sender_id), lc = str(content).lower()
"name": f"用户_{sender_id}",
"is_customer": True
}
receiver_info = {
"id": self.store_id,
"name": "店铺客服",
"is_merchant": True
}
# 创建消息模板 # 检测消息类型,使用与拼多多相同的逻辑
message_template = PlatformMessage( if any(ext in lc for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]):
type="message", msg_type = "image"
content=message_content, elif any(ext in lc for ext in [".mp4", ".avi", ".mov", ".wmv", ".flv"]):
msg_type="text", msg_type = "video"
sender={"id": sender_info.get("id", "")}, elif any(keyword in lc for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id']):
msg_type = "product_card"
else:
msg_type = "text"
# 🔥 使用工厂方法创建消息模板(与拼多多保持一致)
message_template = PlatformMessage.create_text_message(
content=content,
sender_id=str(sender_id),
store_id=self.store_id store_id=self.store_id
) )
# 动态设置检测到的消息类型
message_template.msg_type = msg_type
# 发送消息到后端(使用统一连接) # 发送消息到后端(使用统一连接)
await self.ai_service.send_message_to_backend(message_template.to_dict()) await self.ai_service.send_message_to_backend(message_template.to_dict())
@@ -1034,7 +1528,6 @@ class DouYinMessageHandler:
content_data = msg_content["8"] content_data = msg_content["8"]
self._log(f"📄 原始内容数据: {content_data}", "DEBUG") self._log(f"📄 原始内容数据: {content_data}", "DEBUG")
if message_dict and message_dict.get('text') != "客服水滴智能优品接入": if message_dict and message_dict.get('text') != "客服水滴智能优品接入":
self._log(f"💬 成功解析用户消息: '{message_dict}'", "SUCCESS") self._log(f"💬 成功解析用户消息: '{message_dict}'", "SUCCESS")
@@ -1195,22 +1688,61 @@ class DouYinMessageHandler:
elif msg_type == 'text' and avatar_url: elif msg_type == 'text' and avatar_url:
message_text = message_dict['text'] message_text = message_dict['text']
# 准备发送者和接收者信息 # 🔥 提取goods_info信息与拼多多保持一致
sender_info = { goods_info = {}
"id": str(sender_id), if msg_type == 'order':
"name": f"用户_{sender_id}", # 从抖音的订单信息中提取goods_info
"is_customer": True order_id = message_dict.get('order_id', '')
} goods_id = self.get_goods_id(order_id) if hasattr(self, 'get_goods_id') else message_dict.get(
'goods_id', '')
goods_info = {
'goodsID': goods_id,
'orderSequenceNo': order_id
}
elif msg_type == 'goods':
# 从抖音的商品信息中提取goods_info
goods_id = message_dict.get('goods_id', '')
goods_info = {
'goodsID': goods_id
}
# 创建消息模板 # 🔥 统一消息类型检测逻辑(与拼多多完全一致)
message_template = PlatformMessage( content = message_text
type="message", lc = str(content).lower()
content=message_text,
pin_image=avatar_url, # 使用与拼多多完全相同的检测逻辑
msg_type=msg_type, if any(ext in lc for ext in [".jpg", ".jpeg", ".png", ".gif", ".webp"]):
sender={"id": sender_info.get("id", "")}, msg_type = "image"
store_id=self.store_id elif any(ext in lc for ext in [".mp4", ".avi", ".mov", ".wmv", ".flv"]):
msg_type = "video"
else:
msg_type = "text"
# 🔥 订单卡片组装(与拼多多完全一致)
if ("订单编号" in str(content) or msg_type == 'order') and goods_info:
content = f"商品id{goods_info.get('goodsID')} 订单号:{goods_info.get('orderSequenceNo')}"
msg_type = "order_card"
# 🔥 商品卡片检测(与拼多多完全一致)
elif any(keyword in lc for keyword in ['goods.html', 'item.html', 'item.jd.com', '商品卡片id']) or \
(goods_info and goods_info.get('goodsID') and not goods_info.get('orderSequenceNo')):
msg_type = "product_card"
# 🔥 使用工厂方法创建消息模板(与拼多多保持一致)
message_template = PlatformMessage.create_text_message(
content=content,
sender_id=str(sender_id),
store_id=self.store_id,
pin_image=avatar_url
) )
# 动态设置检测到的消息类型
message_template.msg_type = msg_type
# 🔥 添加调试日志(与拼多多保持一致)
try:
print(f"📤(SPEC) 发送到AI: {json.dumps(message_template.to_dict(), ensure_ascii=False)[:300]}...")
except Exception:
pass
self._log("📤 准备发送消息到AI服务...", "INFO") self._log("📤 准备发送消息到AI服务...", "INFO")
self._log(f"📋 消息内容: {message_template.to_json()}", "DEBUG") self._log(f"📋 消息内容: {message_template.to_json()}", "DEBUG")
@@ -1549,12 +2081,14 @@ class DouYinMessageHandler:
async def send_message_external(self, receiver_id: str, content: str) -> bool: async def send_message_external(self, receiver_id: str, content: str) -> bool:
"""外部调用的发送消息方法 - 用于后端消息转发""" """外部调用的发送消息方法 - 用于后端消息转发"""
try: try:
self._log(f"🔄 [External-{self.instance_id}] 收到转发请求: receiver_id={receiver_id}, content={content}", "INFO") self._log(f"🔄 [External-{self.instance_id}] 收到转发请求: receiver_id={receiver_id}, content={content}",
"INFO")
# 修复数据类型不匹配问题:将字符串转换为整数 # 修复数据类型不匹配问题:将字符串转换为整数
try: try:
receiver_id_int = int(receiver_id) receiver_id_int = int(receiver_id)
self._log(f"🔧 [External-{self.instance_id}] 转换 receiver_id: '{receiver_id}' -> {receiver_id_int}", "DEBUG") self._log(f"🔧 [External-{self.instance_id}] 转换 receiver_id: '{receiver_id}' -> {receiver_id_int}",
"DEBUG")
except ValueError: except ValueError:
self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR") self._log(f"❌ [External-{self.instance_id}] receiver_id 无法转换为整数: {receiver_id}", "ERROR")
return False return False
@@ -1566,7 +2100,9 @@ class DouYinMessageHandler:
# 检查用户是否存在于user_tokens中使用整数类型 # 检查用户是否存在于user_tokens中使用整数类型
self._log(f"🔍 [External-{self.instance_id}] 调试信息:", "DEBUG") self._log(f"🔍 [External-{self.instance_id}] 调试信息:", "DEBUG")
self._log(f"🔍 [External-{self.instance_id}] receiver_id_int: {receiver_id_int} (类型: {type(receiver_id_int)})", "DEBUG") self._log(
f"🔍 [External-{self.instance_id}] receiver_id_int: {receiver_id_int} (类型: {type(receiver_id_int)})",
"DEBUG")
self._log(f"🔍 [External-{self.instance_id}] user_tokens keys: {list(self.user_tokens.keys())}", "DEBUG") self._log(f"🔍 [External-{self.instance_id}] user_tokens keys: {list(self.user_tokens.keys())}", "DEBUG")
if self.user_tokens: if self.user_tokens:
first_key = list(self.user_tokens.keys())[0] first_key = list(self.user_tokens.keys())[0]
@@ -1587,11 +2123,15 @@ class DouYinMessageHandler:
p_id = user_info.get("p_id") p_id = user_info.get("p_id")
user_token = user_info.get("token") user_token = user_info.get("token")
self._log(f"🔍 [External-{self.instance_id}] 用户会话信息: talk_id={talk_id}, p_id={p_id}, has_token={bool(user_token)}", "DEBUG") self._log(
f"🔍 [External-{self.instance_id}] 用户会话信息: talk_id={talk_id}, p_id={p_id}, has_token={bool(user_token)}",
"DEBUG")
# 检查必要参数 # 检查必要参数
if not talk_id or not p_id: if not talk_id or not p_id:
self._log(f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 缺少必要的会话信息 (talk_id: {talk_id}, p_id: {p_id})", "ERROR") self._log(
f"❌ [External-{self.instance_id}] 用户 {receiver_id_int} 缺少必要的会话信息 (talk_id: {talk_id}, p_id: {p_id})",
"ERROR")
return False return False
if not user_token: if not user_token:
@@ -1602,7 +2142,9 @@ class DouYinMessageHandler:
if "pending_messages" not in user_info: if "pending_messages" not in user_info:
user_info["pending_messages"] = [] user_info["pending_messages"] = []
user_info["pending_messages"].append(content) user_info["pending_messages"].append(content)
self._log(f"📝 [External-{self.instance_id}] 消息已加入待发送队列,队列长度: {len(user_info['pending_messages'])}", "INFO") self._log(
f"📝 [External-{self.instance_id}] 消息已加入待发送队列,队列长度: {len(user_info['pending_messages'])}",
"INFO")
return True return True
# 发送消息 (注意_send_message_to_user 可能期望字符串类型的receiver_id) # 发送消息 (注意_send_message_to_user 可能期望字符串类型的receiver_id)
@@ -1838,6 +2380,8 @@ class DouYinListenerForGUI:
# 发送客服列表到后端 # 发送客服列表到后端
try: try:
# 🔥 等待一小段时间确保连接完全建立
await asyncio.sleep(1)
staff_list_success = await self.douyin_bot.message_handler.send_staff_list_to_backend() staff_list_success = await self.douyin_bot.message_handler.send_staff_list_to_backend()
if staff_list_success: if staff_list_success:
print(f"🔥 [DY] 客服列表已上传到后端") print(f"🔥 [DY] 客服列表已上传到后端")
@@ -1864,6 +2408,25 @@ class DouYinListenerForGUI:
# 在后台启动监听任务 # 在后台启动监听任务
asyncio.create_task(keep_running()) asyncio.create_task(keep_running())
# 🔥 新增Cookie登录成功后发送登录成功报告与登录参数模式保持一致
try:
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": cookie_dict # 添加cookie信息
}
backend.send_message(message)
self._log("✅ [DY] 已向后端发送Cookie登录成功报告", "SUCCESS")
else:
self._log("⚠️ [DY] 无法获取后端客户端,跳过状态报告", "WARNING")
except Exception as e:
self._log(f"⚠️ [DY] 发送登录成功报告失败: {e}", "WARNING")
self._log("✅ [DY] 抖音平台连接成功,开始监听消息", "SUCCESS") self._log("✅ [DY] 抖音平台连接成功,开始监听消息", "SUCCESS")
return True return True
else: else:
@@ -1876,6 +2439,69 @@ class DouYinListenerForGUI:
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") self._log(f"错误详情: {traceback.format_exc()}", "DEBUG")
return False return False
async def start_with_login_params(self, store_id: str, login_params: str):
"""使用后端下发的登录参数执行登录并启动监听(与拼多多保持一致)"""
try:
self._log("🔵 [DY] 收到后端登录参数开始执行登录获取cookies", "INFO")
self._log(f"🔍 [DY] 登录参数内容: {login_params[:100]}...", "DEBUG")
# 1. 解析登录参数
self._log("🔄 [DY] 开始解析登录参数", "DEBUG")
params_dict = self._parse_login_params(login_params)
if not params_dict:
self._log("❌ [DY] 登录参数解析失败", "ERROR")
return False
self._log(f"✅ [DY] 登录参数解析成功,手机号: {params_dict.get('phone_number', 'N/A')}", "DEBUG")
# 2. 使用新的DyLogin类执行登录
self._log("🔄 [DY] 开始创建DyLogin实例", "DEBUG")
dy_login = DyLogin(log_callback=self.log_callback)
self._log("✅ [DY] DyLogin实例创建成功", "DEBUG")
self._log("🔄 [DY] 开始执行登录", "DEBUG")
login_result = dy_login.login_with_params(params_dict, store_id)
self._log(f"📊 [DY] 登录结果: {login_result}", "DEBUG")
if login_result == "need_verification_code":
self._log("⚠️ [DY] 需要手机验证码,已通知后端,等待重新下发包含验证码的登录参数", "WARNING")
return "need_verification_code" # 返回特殊标识,避免被覆盖
elif login_result == "verification_code_error":
self._log("⚠️ [DY] 验证码错误,已通知后端", "WARNING")
return "verification_code_error" # 返回特殊标识,避免重复发送消息
elif login_result == "login_failure":
self._log("⚠️ [DY] 登录失败,已发送失败通知给后端", "WARNING")
return "login_failure" # 返回特殊标识,避免重复发送消息
elif not login_result:
self._log("❌ [DY] 登录失败", "ERROR")
return False
elif isinstance(login_result, dict):
# 登录成功获取到cookies
self._log("✅ [DY] 登录成功使用获取的cookies连接平台", "SUCCESS")
# 🔥 使用获取的cookies启动监听这里会验证cookies完整性
return await self.start_with_cookies(store_id, login_result)
else:
self._log("❌ [DY] 登录返回未知结果", "ERROR")
return False
except Exception as e:
self._log(f"❌ [DY] 使用登录参数启动失败: {str(e)}", "ERROR")
import traceback
self._log(f"🔍 [DY] 异常详细信息: {traceback.format_exc()}", "ERROR")
return False
def _parse_login_params(self, login_params_str: str) -> dict:
"""解析后端下发的登录参数(与拼多多保持一致)"""
try:
import json
data = json.loads(login_params_str)
params = data.get("data", {})
self._log(f"✅ [DY] 登录参数解析成功: phone_number={params.get('phone_number', 'N/A')}", "INFO")
return params
except Exception as e:
self._log(f"❌ [DY] 解析登录参数失败: {e}", "ERROR")
return {}
def stop_listening(self): def stop_listening(self):
"""停止监听""" """停止监听"""
if self.stop_event: if self.stop_event:

View File

@@ -999,8 +999,7 @@ class ImgDistance:
save_path=save_path save_path=save_path
) )
# AutiContent类已移除 - 后端会提供所有必要的anti_content
# AutiContent类已移除 - 后端会提供所有必要的anti_content
def gzip_compress(self, data): def gzip_compress(self, data):
"""压缩数据""" """压缩数据"""
@@ -1605,9 +1604,9 @@ class PddLogin:
return result return result
# 发送验证码通知给后端,并获取验证码 # 发送验证码通知给后端,并获取验证码
def request_verification_code(self, username, store_id, backend_anti_content): def request_verification_code(self, username, store_id, backend_anti_content, phone_number=None):
"""向后端请求获取手机验证码""" """向后端请求获取手机验证码"""
self._log(f"开始请求验证码,用户名: {username}, 店铺ID: {store_id}", "INFO") self._log(f"开始请求验证码,用户名: {username}, 店铺ID: {store_id}, 手机号: {phone_number}", "INFO")
# 使用后端下发的anti_content # 使用后端下发的anti_content
anti_content = backend_anti_content anti_content = backend_anti_content
@@ -1622,18 +1621,18 @@ class PddLogin:
response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies) response = requests.post(url, headers=self.headers, json=payload, cookies=self.cookies)
self._log(f"发送验证码请求结果: {response.text}") self._log(f"发送验证码请求结果: {response.text}")
# 发送消息给后端,告知需要验证码 # 发送消息给后端,告知需要验证码(包含手机号)
self._log("准备向后端发送验证码需求通知", "INFO") self._log("准备向后端发送验证码需求通知", "INFO")
self._send_verification_needed_message(store_id) self._send_verification_needed_message(store_id, phone_number)
# 这里需要等待后端重新下发包含验证码的登录参数 # 这里需要等待后端重新下发包含验证码的登录参数
# 实际实现中这个方法会在接收到新的登录参数后被重新调用 # 实际实现中这个方法会在接收到新的登录参数后被重新调用
return None return None
def _send_verification_needed_message(self, store_id): def _send_verification_needed_message(self, store_id, phone_number=None):
"""向后端发送需要验证码的通知""" """向后端发送需要验证码的通知"""
try: try:
self._log(f"开始发送验证码需求通知店铺ID: {store_id}", "INFO") self._log(f"开始发送验证码需求通知店铺ID: {store_id}, 手机号: {phone_number}", "INFO")
from WebSocket.backend_singleton import get_backend_client from WebSocket.backend_singleton import get_backend_client
backend = get_backend_client() backend = get_backend_client()
self._log(f"获取到后端客户端: {backend is not None}", "DEBUG") self._log(f"获取到后端客户端: {backend is not None}", "DEBUG")
@@ -1643,11 +1642,12 @@ class PddLogin:
"type": "connect_message", "type": "connect_message",
"store_id": store_id, "store_id": store_id,
"status": False, "status": False,
"content": "需要验证码" "content": "需要验证码",
"phone_number": phone_number
} }
self._log(f"准备发送验证码通知消息: {message}", "DEBUG") self._log(f"准备发送验证码通知消息: {message}", "DEBUG")
backend.send_message(message) backend.send_message(message)
self._log("✅ 成功向后端发送验证码需求通知", "SUCCESS") self._log("✅ 成功向后端发送验证码需求通知(含手机号)", "SUCCESS")
else: else:
self._log("❌ 后端客户端为空,无法发送验证码需求通知", "ERROR") self._log("❌ 后端客户端为空,无法发送验证码需求通知", "ERROR")
except Exception as e: except Exception as e:
@@ -1690,7 +1690,8 @@ class PddLogin:
message = { message = {
"type": "connect_message", "type": "connect_message",
"store_id": store_id, "store_id": store_id,
"status": True # 登录成功 "status": True, # 登录成功
"cookies": self.cookies # 🔥 新增添加登录生成的cookie信息
} }
self._log(f"准备发送登录成功消息: {message}", "DEBUG") self._log(f"准备发送登录成功消息: {message}", "DEBUG")
backend.send_message(message) backend.send_message(message)
@@ -1731,7 +1732,9 @@ class PddLogin:
self._log("🚀 [PddLogin] 开始使用参数登录", "INFO") self._log("🚀 [PddLogin] 开始使用参数登录", "INFO")
# 检查验证码字段(兼容 code 和 verification_code # 检查验证码字段(兼容 code 和 verification_code
verification_code = login_params.get("verification_code") or login_params.get("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", "") self.headers["anti-content"] = login_params.get("anti_content", "")
@@ -1819,7 +1822,8 @@ class PddLogin:
salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值 salt = self.vc_pre_ck_b() # 获取生成aes key和iv 的密文值
pictures = self.obtain_captcha() # 获取验证码图片 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) # 生成轨迹 track_list = Track.get_track(distance=distance) # 生成轨迹
captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数 captcha_collect = self.captcha_collect(salt=salt, track_list=track_list) # 生成captcha_collect参数
@@ -1833,7 +1837,8 @@ class PddLogin:
success_count += 1 success_count += 1
# 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False # 如果滑块成功 success_count 计数一次 成功8次还是显示验证码则失败 返回False
if success_count < 8: 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: else:
return False return False
else: else:
@@ -1864,8 +1869,20 @@ class PddLogin:
backend_anti_content = login_params.get("anti_content") backend_anti_content = login_params.get("anti_content")
self._log(f"为用户 {username} 发送验证码使用后端anti_content", "INFO") self._log(f"为用户 {username} 发送验证码使用后端anti_content", "INFO")
# 传递后端下发的anti_content # 🔥 从响应中提取手机号
self.request_verification_code(username, store_id, backend_anti_content) 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)
return "need_verification_code" return "need_verification_code"
else: else:
@@ -1883,6 +1900,7 @@ class PddLogin:
self._send_login_failure_message(store_id, error_msg) self._send_login_failure_message(store_id, error_msg)
return "login_failure" # 返回特殊状态,避免重复发送消息 return "login_failure" # 返回特殊状态,避免重复发送消息
# ===== 登录相关类集成结束 ===== # ===== 登录相关类集成结束 =====
@@ -2537,6 +2555,58 @@ class ChatPdd:
traceback.print_exc() traceback.print_exc()
return False 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): async def handle_customer_message(self, message_data):
"""处理来自后端的客服消息""" """处理来自后端的客服消息"""
self._log("收到来自后端的客服消息", "INFO") self._log("收到来自后端的客服消息", "INFO")
@@ -2559,6 +2629,11 @@ class ChatPdd:
async def process_incoming_message(self, message_data, wss, store): async def process_incoming_message(self, message_data, wss, store):
"""处理接收到的消息""" """处理接收到的消息"""
try: try:
# 🔥 过滤机器人消息
if self.should_filter_robot_message(message_data):
self._log("🤖 检测到机器人消息,已过滤不发送给后端", "DEBUG")
return
message_info = message_data.get("message", {}) message_info = message_data.get("message", {})
if not message_info: if not message_info:
return return
@@ -2731,6 +2806,28 @@ class ChatPdd:
except Exception as e: except Exception as e:
self._log(f"❌ 获取或发送客服列表失败: {e}", "ERROR") 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( await asyncio.gather(
self.heartbeat(websocket), self.heartbeat(websocket),
@@ -3010,6 +3107,7 @@ class PddListenerForGUI:
cookies_str=cookies, cookies_str=cookies,
store=store store=store
) )
self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS") self._log("✅ [PDD] 拼多多平台连接成功,开始监听消息", "SUCCESS")
return True return True
except Exception as e: except Exception as e:
@@ -3055,8 +3153,6 @@ class PddListenerForGUI:
self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR") self._log(f"❌ [PDD] 解析登录参数失败: {e}", "ERROR")
return {} return {}
def get_status(self) -> Dict[str, Any]: def get_status(self) -> Dict[str, Any]:
return { return {
"running": self.running, "running": self.running,

View File

@@ -29,6 +29,8 @@ class BackendClient:
self.error_callback: Optional[Callable] = None self.error_callback: Optional[Callable] = None
self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调 self.login_callback: Optional[Callable] = None # 新增平台登录下发cookies回调
self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调 self.success_callback: Optional[Callable] = None # 新增:后端连接成功回调
self.token_error_callback: Optional[Callable] = None # 新增token错误回调
self.version_callback: Optional[Callable] = None # 新增:版本检查回调
self.is_connected = False self.is_connected = False
@@ -99,15 +101,15 @@ class BackendClient:
ping_timeout=WS_PING_TIMEOUT, # 可配置ping超时 ping_timeout=WS_PING_TIMEOUT, # 可配置ping超时
close_timeout=10, # 10秒关闭超时 close_timeout=10, # 10秒关闭超时
# 增加TCP keepalive配置 # 增加TCP keepalive配置
max_size=2**20, # 1MB最大消息大小 max_size=2 ** 20, # 1MB最大消息大小
max_queue=32, # 最大队列大小 max_queue=32, # 最大队列大小
compression=None # 禁用压缩以提高性能 compression=None # 禁用压缩以提高性能
) )
print(f"[连接] 已启用心跳ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s") print(f"[连接] 已启用心跳ping_interval={WS_PING_INTERVAL}s, ping_timeout={WS_PING_TIMEOUT}s")
else: else:
self.websocket = await websockets.connect( self.websocket = await websockets.connect(
self.url, self.url,
max_size=2**20, max_size=2 ** 20,
max_queue=32, max_queue=32,
compression=None compression=None
) )
@@ -261,7 +263,9 @@ class BackendClient:
close: Callable = None, close: Callable = None,
error: Callable = None, error: Callable = None,
login: Callable = None, login: Callable = None,
success: Callable = None): success: Callable = None,
token_error: Callable = None,
version: Callable = None):
"""设置各种消息类型的回调函数""" """设置各种消息类型的回调函数"""
if store_list: if store_list:
self.store_list_callback = store_list self.store_list_callback = store_list
@@ -279,6 +283,10 @@ class BackendClient:
self.login_callback = login self.login_callback = login
if success: if success:
self.success_callback = success self.success_callback = success
if token_error:
self.token_error_callback = token_error
if version:
self.version_callback = version
def on_connected(self): def on_connected(self):
"""连接成功时的处理""" """连接成功时的处理"""
@@ -338,7 +346,7 @@ class BackendClient:
active_store_id = None active_store_id = None
try: try:
from Utils.JD.JdUtils import WebsocketManager as JdManager from Utils.JD.JdUtils import WebsocketManager as JdManager
from Utils.Dy.DyUtils import WebsocketManager as DyManager from Utils.Dy.DyUtils import DouYinWebsocketManager as DyManager
from Utils.Pdd.PddUtils import WebsocketManager as PddManager from Utils.Pdd.PddUtils import WebsocketManager as PddManager
# 检查各平台是否有活跃连接 # 检查各平台是否有活跃连接
@@ -429,8 +437,12 @@ class BackendClient:
self._handle_login(message) self._handle_login(message)
elif msg_type == 'error': elif msg_type == 'error':
self._handle_error_message(message) self._handle_error_message(message)
elif msg_type == 'error_token':
self._handle_token_error(message)
elif msg_type == 'staff_list': elif msg_type == 'staff_list':
self._handle_staff_list(message) self._handle_staff_list(message)
elif msg_type == 'version_response': # 新增:版本检查响应
self._handle_version_response(message)
else: else:
print(f"未知消息类型: {msg_type}") print(f"未知消息类型: {msg_type}")
@@ -1145,12 +1157,14 @@ class BackendClient:
content = message.get('content', '') content = message.get('content', '')
data = message.get('data', {}) data = message.get('data', {})
# 判断是拼多多登录参数还是普通Cookie # 🔥 判断是登录参数模式还是普通Cookie模式(支持拼多多和抖音)
if platform_name == "拼多多" and ("拼多多登录" in content) and data.get('login_params'): if (platform_name in ["拼多多", "抖音"] and
# 拼多多登录参数模式 - 传递完整的消息JSON给处理器 (("拼多多登录" in content and data.get('login_params')) or
print(f"收到拼多多登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}") ("抖音登录" in content and data.get('login_flow')))):
# 登录参数模式 - 传递完整的消息JSON给处理器
print(f"收到{platform_name}登录参数: 平台={platform_name}, 店铺={store_id}, 类型={content}")
if self.login_callback: if self.login_callback:
# 传递完整的JSON消息拼多多处理器来解析login_params # 传递完整的JSON消息让处理器来解析登录参数
import json import json
full_message = json.dumps(message) full_message = json.dumps(message)
self.login_callback(platform_name, store_id, full_message) self.login_callback(platform_name, store_id, full_message)
@@ -1163,10 +1177,35 @@ class BackendClient:
def _handle_error_message(self, message: Dict[str, Any]): def _handle_error_message(self, message: Dict[str, Any]):
"""处理错误消息""" """处理错误消息"""
error_msg = message.get('error', '未知错误') 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}") print(f"后端连接错误: {error_msg}")
if self.error_callback: if self.error_callback:
self.error_callback(error_msg, message) 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]): def _handle_staff_list(self, message: Dict[str, Any]):
"""处理客服列表更新消息""" """处理客服列表更新消息"""
staff_list = message.get('data', {}).get('staff_list', []) staff_list = message.get('data', {}).get('staff_list', [])
@@ -1180,6 +1219,16 @@ class BackendClient:
if self.customers_callback: # 假设客服列表更新也触发客服列表回调 if self.customers_callback: # 假设客服列表更新也触发客服列表回调
self.customers_callback(staff_list, store_id) 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): def set_token(self, token: str):

View File

@@ -27,18 +27,25 @@ class WebSocketManager:
"""WebSocket连接管理器统一处理后端连接和平台监听""" """WebSocket连接管理器统一处理后端连接和平台监听"""
def __init__(self): def __init__(self):
# 后端客户端
self.backend_client = None self.backend_client = None
self.is_connected = False
# 版本检查器
self.version_checker = None
self.gui_update_callback = None
self.platform_listeners = {} # 存储各平台的监听器 self.platform_listeners = {} # 存储各平台的监听器
self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增 self.connected_platforms = [] # 存储已连接的平台列表 # <- 新增
self.callbacks = { self.callbacks = {
'log': None, 'log': None,
'success': None, 'success': None,
'error': None, 'error': None,
'platform_connected': None, 'platform_connected': None,
'token_error': None,
} }
def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None, def set_callbacks(self, log: Callable = None, success: Callable = None, error: Callable = None,
platform_connected: Callable = None): # ← 新增参数 platform_connected: Callable = None, token_error: Callable = None):
"""设置回调函数""" """设置回调函数"""
if log: if log:
self.callbacks['log'] = log self.callbacks['log'] = log
@@ -48,6 +55,8 @@ class WebSocketManager:
self.callbacks['error'] = error self.callbacks['error'] = error
if platform_connected: # ← 新增 if platform_connected: # ← 新增
self.callbacks['platform_connected'] = platform_connected self.callbacks['platform_connected'] = platform_connected
if token_error:
self.callbacks['token_error'] = token_error
def _log(self, message: str, level: str = "INFO"): def _log(self, message: str, level: str = "INFO"):
"""内部日志方法""" """内部日志方法"""
@@ -71,6 +80,7 @@ class WebSocketManager:
def connect_backend(self, token: str) -> bool: def connect_backend(self, token: str) -> bool:
"""连接后端WebSocket""" """连接后端WebSocket"""
try: try:
# 1 保存token到配置 # 1 保存token到配置
try: try:
from config import set_saved_token from config import set_saved_token
@@ -82,33 +92,52 @@ class WebSocketManager:
backend = get_backend_client() backend = get_backend_client()
if backend: if backend:
# 3 如果有客户端更新token并重连 # 检查现有客户端是否因token错误而停止
backend.set_token(token) if backend.should_stop:
self._log("检测到客户端因token错误已停止创建新的客户端", "INFO")
# 断开旧客户端
backend.disconnect()
# 清除旧客户端引用
set_backend_client(None)
backend = None
else:
# 3 如果有客户端更新token并重连
backend.set_token(token)
# 设置回调函数 # 设置回调函数
def _on_backend_success(): def _on_backend_success():
try: try:
self._log("连接服务成功", "SUCCESS") self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']: if self.callbacks['success']:
self.callbacks['success']() self.callbacks['success']()
except Exception as e: # 启动版本检查器
self._log(f"成功回调执行失败: {e}", "ERROR") self._start_version_checker()
except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR")
def _on_backend_login(platform_name: str, store_id: str, cookies: str): def _on_backend_login(platform_name: str, store_id: str, cookies: str):
self._log( self._log(
f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}", f"收到后端登录指令: 平台={platform_name}, 店铺={store_id}, cookies_len={len(cookies) if cookies else 0}",
"INFO") "INFO")
self._handle_platform_login(platform_name, store_id, cookies) self._handle_platform_login(platform_name, store_id, cookies)
backend.set_callbacks(success=_on_backend_success, login=_on_backend_login) 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)
if not backend.is_connected: backend.set_callbacks(success=_on_backend_success, login=_on_backend_login,
backend.connect() token_error=_on_token_error)
self.backend_client = backend if not backend.is_connected:
self._log("令牌已提交已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS") backend.connect()
return True
else: self.backend_client = backend
self._log("令牌已提交已连接后端。等待后端下发平台cookies后自动连接平台...", "SUCCESS")
return True
# 如果没有现有客户端或客户端被重置,创建新的
if not backend:
backend = BackendClient.from_exe_token(token) backend = BackendClient.from_exe_token(token)
@@ -123,10 +152,17 @@ class WebSocketManager:
self._log("连接服务成功", "SUCCESS") self._log("连接服务成功", "SUCCESS")
if self.callbacks['success']: if self.callbacks['success']:
self.callbacks['success']() self.callbacks['success']()
# 启动版本检查器
self._start_version_checker()
except Exception as e: except Exception as e:
self._log(f"成功回调执行失败: {e}", "ERROR") self._log(f"成功回调执行失败: {e}", "ERROR")
backend.set_callbacks(login=_on_backend_login, success=_on_backend_success) 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.connect() backend.connect()
set_backend_client(backend) set_backend_client(backend)
@@ -143,6 +179,14 @@ class WebSocketManager:
def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str): def _handle_platform_login(self, platform_name: str, store_id: str, cookies: str):
"""处理平台登录请求""" """处理平台登录请求"""
try: 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 = { platform_map = {
"淘宝": "千牛", "淘宝": "千牛",
@@ -186,6 +230,27 @@ class WebSocketManager:
import traceback import traceback
self._log(f"错误详情: {traceback.format_exc()}", "DEBUG") 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): def _start_jd_listener(self, store_id: str, cookies: str):
"""启动京东平台监听""" """启动京东平台监听"""
try: try:
@@ -241,15 +306,59 @@ class WebSocketManager:
def _runner(): def _runner():
try: try:
import json import json
self._log("🚀 开始创建抖音监听器实例", "DEBUG")
listener = DYListenerForGUI_WS() listener = DYListenerForGUI_WS()
# 将JSON字符串格式的cookies解析为字典 self._log("✅ 抖音监听器实例创建成功", "DEBUG")
try:
cookie_dict = json.loads(cookies) if isinstance(cookies, str) else cookies # 🔥 检查是否为登录参数模式(与拼多多保持一致)
except json.JSONDecodeError as e: if cookies and ('"login_flow"' in cookies or '"phone_number"' in cookies):
self._log(f"❌ Cookie JSON解析失败: {e}", "ERROR") # 使用登录参数模式
return False 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中发送过了这里不需要重复发送
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookie_dict=cookie_dict))
return result return result
except Exception as e: except Exception as e:
self._log(f"抖音监听器运行异常: {e}", "ERROR") self._log(f"抖音监听器运行异常: {e}", "ERROR")
@@ -270,34 +379,13 @@ class WebSocketManager:
if f"抖音:{store_id}" in self.platform_listeners: if f"抖音:{store_id}" in self.platform_listeners:
self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success' self.platform_listeners[f"抖音:{store_id}"]['status'] = 'success'
# 上报连接状态给后端
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._log("已启动抖音平台监听", "SUCCESS")
self._notify_platform_connected("抖音") # ← 新增
except Exception as e: except Exception as e:
self._log(f"启动抖音平台监听失败: {e}", "ERROR") self._log(f"启动抖音平台监听失败: {e}", "ERROR")
# 确保失败时也上报状态 # 🔥 移除:确保失败时也不在这里上报状态
if self.backend_client: # 失败状态应该在DyLogin中处理与拼多多保持一致
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): def _start_qianniu_listener(self, store_id: str, cookies: str):
"""启动千牛平台监听(单连接多店铺架构)""" """启动千牛平台监听(单连接多店铺架构)"""
@@ -393,8 +481,14 @@ class WebSocketManager:
result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data)) result = asyncio.run(listener.start_with_cookies(store_id=store_id, cookies=data))
self._log(f"📊 start_with_cookies 执行结果: {result}", "DEBUG") 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中发送了不需要重复发送 # 如果是特殊状态说明通知已经在PddLogin中发送了不需要重复发送
try: try:
message = { message = {
@@ -404,7 +498,8 @@ class WebSocketManager:
} }
self.backend_client.send_message(message) self.backend_client.send_message(message)
status_text = "成功" if result else "失败" 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: except Exception as send_e:
self._log(f"上报拼多多平台连接状态失败: {send_e}", "ERROR") self._log(f"上报拼多多平台连接状态失败: {send_e}", "ERROR")
elif result == "need_verification_code": elif result == "need_verification_code":

View File

@@ -85,9 +85,32 @@ def main():
result = subprocess.run(['python', 'quick_build.py'], capture_output=False) result = subprocess.run(['python', 'quick_build.py'], capture_output=False)
if result.returncode == 0: if result.returncode == 0:
print("\n🎉 生产环境打包成功!") # 确保输出目录正确性
print("📁 输出目录: dist/MultiPlatformGUI/") if os.path.exists('dist/main') and not os.path.exists('dist/MultiPlatformGUI'):
print("🔇 已禁用所有日志功能,客户端不会产生日志文件") 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")
else: else:
print("\n❌ 打包失败") print("\n❌ 打包失败")

View File

@@ -37,6 +37,7 @@ FUTURE_TIMEOUT = 300 # 5分钟
# 终端日志配置(简化) # 终端日志配置(简化)
LOG_LEVEL = "INFO" LOG_LEVEL = "INFO"
VERSION = "1.0" VERSION = "1.0"
APP_VERSION = "1.4.8" # 应用版本号(用于版本检查)
# GUI配置 # GUI配置
WINDOW_TITLE = "AI回复连接入口-V1.0" WINDOW_TITLE = "AI回复连接入口-V1.0"

106
installer/README.md Normal file
View File

@@ -0,0 +1,106 @@
# GUI 安装包构建工具
这个目录包含了使用 NSIS 构建 Windows 安装包的完整解决方案。
## 目录结构
```
installer/
├── build_installer.py # 主要的构建脚本
├── installer.nsi # 自动生成的 NSIS 脚本
├── assets/ # 安装包资源文件
│ ├── icon.png # 应用程序图标
│ └── license.txt # 软件许可协议
└── output/ # 生成的安装包输出目录
```
## 使用方法
### 方法一:使用批处理脚本(推荐)
```bash
# 在 GUI_main 目录下运行
build_installer.bat
```
### 方法二直接运行Python脚本
```bash
# 在 GUI_main/installer 目录下运行
python build_installer.py
```
## 前提条件
1. **NSIS 安装**
- 下载地址https://nsis.sourceforge.io/Download
- 安装后确保 `makensis` 命令在 PATH 中
2. **应用程序构建**
- 必须先运行 `build_production.py` 构建应用程序
- 确保 `dist/MultiPlatformGUI/` 目录存在
## 构建流程
1. **环境检查**:验证 NSIS 安装和应用程序构建
2. **准备资源**:复制图标文件和创建许可证
3. **生成脚本**:自动生成 NSIS 安装脚本
4. **编译安装包**:使用 makensis 编译最终的安装程序
## 输出结果
安装包将生成在 `installer/output/` 目录下,文件名格式:
```
ShuiDi_AI_Assistant_Setup_1.0.0_YYYYMMDD_HHMMSS.exe
```
## 安装包功能
- ✅ 完整的中文界面
- ✅ 自动创建开始菜单快捷方式
- ✅ 自动创建桌面快捷方式
- ✅ 注册表集成
- ✅ 完整的卸载功能
- ✅ 版本信息显示
## 自定义配置
可以修改 `build_installer.py` 中的以下配置:
```python
# 应用程序信息
self.app_name = "水滴AI客服智能助手"
self.app_name_en = "ShuiDi AI Assistant"
self.app_version = "1.0.0"
self.app_publisher = "水滴科技"
self.app_url = "https://www.shuidi.tech"
```
## 故障排除
### 常见问题
1. **找不到 makensis 命令**
- 确保 NSIS 已正确安装
- 将 NSIS 安装目录添加到 PATH 环境变量
2. **找不到 dist 目录**
- 先运行 `build_production.py` 构建应用程序
3. **图标文件问题**
- 确保 `static/` 目录下有图标文件
- 建议准备 ICO 格式的图标文件
### 日志和调试
构建过程中的详细信息会在控制台显示,包括:
- NSIS 版本信息
- 文件复制状态
- 编译过程输出
- 最终安装包信息
## 进一步优化
可以考虑添加以下功能:
- 数字签名支持
- 多语言支持
- 自定义安装选项
- 更新检查机制

View File

@@ -0,0 +1,345 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
NSIS 安装包构建脚本
用于创建 GUI 应用程序的Windows安装包
"""
import os
import sys
import shutil
import subprocess
import datetime
from pathlib import Path
class NSISInstaller:
def __init__(self):
self.script_dir = Path(__file__).parent
self.project_root = self.script_dir.parent
self.dist_dir = self.project_root / "dist" / "MultiPlatformGUI"
self.output_dir = self.script_dir / "output"
self.assets_dir = self.script_dir / "assets"
self.nsis_script = self.script_dir / "installer.nsi"
# 应用程序信息
self.app_name = "水滴AI客服智能助手"
self.app_name_en = "ShuiDi AI Assistant"
self.app_version = "1.0.0"
self.app_publisher = "水滴智能科技"
self.app_url = "https://shuidrop.com/"
self.exe_name = "main.exe"
def check_prerequisites(self):
"""检查构建前提条件"""
print("🔍 检查构建前提条件...")
# 检查NSIS安装
try:
result = subprocess.run(['makensis', '/VERSION'],
capture_output=True, text=True, check=True)
nsis_version = result.stdout.strip()
print(f"✅ NSIS 版本: {nsis_version}")
except (subprocess.CalledProcessError, FileNotFoundError):
print("❌ 错误: 未找到NSIS或makensis命令")
print(" 请从 https://nsis.sourceforge.io/Download 下载并安装NSIS")
print(" 确保将NSIS添加到系统PATH环境变量")
return False
# 检查dist目录
if not self.dist_dir.exists():
print(f"❌ 错误: 未找到构建输出目录 {self.dist_dir}")
print(" 请先运行 build_production.py 构建应用程序")
return False
# 检查主程序文件
exe_path = self.dist_dir / self.exe_name
if not exe_path.exists():
print(f"❌ 错误: 未找到主程序文件 {exe_path}")
return False
print(f"✅ 找到主程序: {exe_path}")
return True
def prepare_assets(self):
"""准备安装包资源文件"""
print("📁 准备资源文件...")
# 创建assets目录
self.assets_dir.mkdir(exist_ok=True)
# 准备主程序图标
icon_sources = [
self.project_root / "static" / "ai_assistant_icon_64.png",
self.project_root / "static" / "ai_assistant_icon_32.png",
self.project_root / "static" / "ai_assistant_icon_16.png"
]
# 准备卸载程序专用图标
uninstall_icon_source = self.project_root / "static" / "ai_assistant_icon_uninstall.png"
# 转换主程序图标
icon_found = False
for icon_source in icon_sources:
if icon_source.exists():
try:
from PIL import Image
img = Image.open(icon_source)
ico_path = self.assets_dir / "icon.ico"
img.save(ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
print(f"✅ 转换主程序图标: {icon_source.name} -> icon.ico")
icon_found = True
break
except ImportError:
print("⚠️ 未安装PIL库跳过图标转换")
break
except Exception as e:
print(f"⚠️ 主程序图标转换失败: {e}")
continue
# 转换卸载程序图标
uninstall_icon_found = False
if uninstall_icon_source.exists():
try:
from PIL import Image
img = Image.open(uninstall_icon_source)
uninstall_ico_path = self.assets_dir / "uninstall_icon.ico"
img.save(uninstall_ico_path, format='ICO', sizes=[(16,16), (32,32), (48,48), (64,64)])
print(f"✅ 转换卸载程序图标: {uninstall_icon_source.name} -> uninstall_icon.ico")
uninstall_icon_found = True
except ImportError:
print("⚠️ 未安装PIL库跳过卸载图标转换")
except Exception as e:
print(f"⚠️ 卸载程序图标转换失败: {e}")
else:
print("⚠️ 未找到卸载程序专用图标,将使用主程序图标")
if not icon_found:
print("⚠️ 未找到主程序图标文件,将使用默认图标")
return {
'main_icon': icon_found,
'uninstall_icon': uninstall_icon_found
}
def generate_nsis_script(self, icon_info):
"""生成NSIS安装脚本"""
print("📝 生成NSIS安装脚本...")
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
installer_name = f"{self.app_name_en}_Setup_{self.app_version}_{timestamp}.exe"
nsis_content = f'''# 水滴AI客服智能助手 NSIS 安装脚本
# 自动生成于 {datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")}
Unicode True
# 定义应用程序信息
!define APP_NAME "{self.app_name}"
!define APP_NAME_EN "{self.app_name_en}"
!define APP_VERSION "{self.app_version}"
!define APP_PUBLISHER "{self.app_publisher}"
!define APP_URL "{self.app_url}"
!define APP_EXE "{self.exe_name}"
# 安装程序信息
Name "${{APP_NAME}} v${{APP_VERSION}}"
OutFile "output\\{installer_name}"
InstallDir "$PROGRAMFILES\\${{APP_NAME_EN}}"
InstallDirRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallPath"
# 界面设置
!include "MUI2.nsh"
!define MUI_ABORTWARNING
{f'!define MUI_ICON "assets\\\\icon.ico"' if icon_info.get('main_icon', False) else ''}
{f'!define MUI_UNICON "assets\\\\uninstall_icon.ico"' if icon_info.get('uninstall_icon', False) else f'!define MUI_UNICON "assets\\\\icon.ico"' if icon_info.get('main_icon', False) else ''}
# 页面配置
!insertmacro MUI_PAGE_WELCOME
!insertmacro MUI_PAGE_LICENSE "assets\\license.txt"
!insertmacro MUI_PAGE_DIRECTORY
!insertmacro MUI_PAGE_INSTFILES
!insertmacro MUI_PAGE_FINISH
!insertmacro MUI_UNPAGE_WELCOME
!insertmacro MUI_UNPAGE_CONFIRM
!insertmacro MUI_UNPAGE_INSTFILES
!insertmacro MUI_UNPAGE_FINISH
# 语言
!insertmacro MUI_LANGUAGE "SimpChinese"
# 版本信息
VIProductVersion "{self.app_version}.0"
VIAddVersionKey /LANG=2052 "ProductName" "${{APP_NAME}}"
VIAddVersionKey /LANG=2052 "Comments" "水滴AI客服智能助手"
VIAddVersionKey /LANG=2052 "CompanyName" "${{APP_PUBLISHER}}"
VIAddVersionKey /LANG=2052 "FileDescription" "${{APP_NAME}} 安装程序"
VIAddVersionKey /LANG=2052 "FileVersion" "${{APP_VERSION}}"
VIAddVersionKey /LANG=2052 "ProductVersion" "${{APP_VERSION}}"
VIAddVersionKey /LANG=2052 "OriginalFilename" "{installer_name}"
VIAddVersionKey /LANG=2052 "LegalCopyright" "© 2024 ${{APP_PUBLISHER}}"
# 安装段
Section "MainSection" SEC01
SetOutPath "$INSTDIR"
SetOverwrite on
# 复制所有文件
File /r "..\\dist\\MultiPlatformGUI\\*.*"
# 复制图标文件到安装目录
{f'File "assets\\\\icon.ico"' if icon_info.get('main_icon', False) else ''}
{f'File "assets\\\\uninstall_icon.ico"' if icon_info.get('uninstall_icon', False) else ''}
# 创建开始菜单快捷方式
CreateDirectory "$SMPROGRAMS\\${{APP_NAME}}"
{f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}" "" "$INSTDIR\\\\icon.ico" 0' if icon_info.get('main_icon', False) else 'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}"'}
{f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe" "" "$INSTDIR\\\\uninstall_icon.ico" 0' if icon_info.get('uninstall_icon', False) else f'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe" "" "$INSTDIR\\\\icon.ico" 0' if icon_info.get('main_icon', False) else 'CreateShortCut "$SMPROGRAMS\\\\${{APP_NAME}}\\\\卸载${{APP_NAME}}.lnk" "$INSTDIR\\\\uninstall.exe"'}
# 创建桌面快捷方式
{f'CreateShortCut "$DESKTOP\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}" "" "$INSTDIR\\\\icon.ico" 0' if icon_info.get('main_icon', False) else 'CreateShortCut "$DESKTOP\\\\${{APP_NAME}}.lnk" "$INSTDIR\\\\${{APP_EXE}}"'}
# 写入注册表
WriteRegStr HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "InstallPath" "$INSTDIR"
WriteRegStr HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}" "Version" "${{APP_VERSION}}"
# 写入卸载信息
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "DisplayName" "${{APP_NAME}}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "UninstallString" "$INSTDIR\\uninstall.exe"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "DisplayVersion" "${{APP_VERSION}}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "Publisher" "${{APP_PUBLISHER}}"
WriteRegStr HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "URLInfoAbout" "${{APP_URL}}"
WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "NoModify" 1
WriteRegDWORD HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}" "NoRepair" 1
# 创建卸载程序
WriteUninstaller "$INSTDIR\\uninstall.exe"
SectionEnd
# 卸载段
Section "Uninstall"
# 删除快捷方式
Delete "$SMPROGRAMS\\${{APP_NAME}}\\${{APP_NAME}}.lnk"
Delete "$SMPROGRAMS\\${{APP_NAME}}\\卸载${{APP_NAME}}.lnk"
RMDir "$SMPROGRAMS\\${{APP_NAME}}"
Delete "$DESKTOP\\${{APP_NAME}}.lnk"
# 删除安装目录
RMDir /r "$INSTDIR"
# 删除注册表项
DeleteRegKey HKLM "Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\${{APP_NAME_EN}}"
DeleteRegKey HKLM "Software\\${{APP_PUBLISHER}}\\${{APP_NAME_EN}}"
SectionEnd
'''
# 写入NSIS脚本文件
# 使用UTF-8 BOM编码NSIS可以正确识别
with open(self.nsis_script, 'w', encoding='utf-8-sig') as f:
f.write(nsis_content)
print(f"✅ NSIS脚本已生成: {self.nsis_script}")
return installer_name
def create_license_file(self):
"""创建许可证文件"""
license_file = self.assets_dir / "license.txt"
license_content = f"""软件许可协议
{self.app_name} v{self.app_version}
版权所有 © 2025 {self.app_publisher}
使用本软件即表示您同意以下条款:
1. 本软件仅供合法用途使用
2. 不得对软件进行反向工程、反编译或反汇编
3. 软件按"原样"提供,不提供任何明示或暗示的保证
4. 在任何情况下,软件提供者均不对因使用本软件而产生的任何损害承担责任
如有疑问,请联系:{self.app_url}
"""
with open(license_file, 'w', encoding='utf-8-sig') as f:
f.write(license_content)
print(f"✅ 许可证文件已创建: {license_file}")
def build_installer(self):
"""构建安装包"""
print("🚀 开始构建NSIS安装包...")
# 创建输出目录
self.output_dir.mkdir(exist_ok=True)
# 执行NSIS编译
try:
cmd = ['makensis', str(self.nsis_script)]
result = subprocess.run(cmd, cwd=str(self.script_dir),
capture_output=True, text=True, check=True)
print("✅ NSIS编译成功")
if result.stdout:
print("NSIS输出:")
print(result.stdout)
return True
except subprocess.CalledProcessError as e:
print("❌ NSIS编译失败")
print(f"错误信息: {e.stderr}")
return False
def run(self):
"""执行完整的构建流程"""
print("=" * 60)
print(f"🔧 {self.app_name} 安装包构建工具")
print("=" * 60)
try:
# 1. 检查前提条件
if not self.check_prerequisites():
return False
# 2. 准备资源文件
icon_info = self.prepare_assets()
# 3. 创建许可证文件
self.create_license_file()
# 4. 生成NSIS脚本
installer_name = self.generate_nsis_script(icon_info)
# 5. 构建安装包
if not self.build_installer():
return False
# 6. 显示结果
installer_path = self.output_dir / installer_name
if installer_path.exists():
installer_size = installer_path.stat().st_size / (1024 * 1024)
print("\n" + "=" * 60)
print("🎉 安装包构建成功!")
print(f"📁 安装包位置: {installer_path}")
print(f"📏 文件大小: {installer_size:.1f} MB")
print("🚀 可以直接分发给用户使用")
print("=" * 60)
return True
else:
print("❌ 安装包文件未找到")
return False
except Exception as e:
print(f"❌ 构建过程出错: {e}")
return False
def main():
"""主函数"""
installer = NSISInstaller()
success = installer.run()
if not success:
sys.exit(1)
if __name__ == "__main__":
main()

67
main.py
View File

@@ -43,6 +43,9 @@ class LoginWindow(QMainWindow):
self.initUI() self.initUI()
# 延迟设置版本检查器确保WebSocket连接已建立
QTimer.singleShot(5000, self.setup_version_checker)
def initUI(self): def initUI(self):
# 设置窗口基本属性 # 设置窗口基本属性
self.setWindowTitle('AI客服智能助手') self.setWindowTitle('AI客服智能助手')
@@ -402,6 +405,29 @@ class LoginWindow(QMainWindow):
except Exception as e: except Exception as e:
self.add_log(f"处理平台连接事件失败: {e}", "ERROR") self.add_log(f"处理平台连接事件失败: {e}", "ERROR")
def on_token_error(self, error_content: str):
"""处理token错误 - 显示红色错误信息并停止所有操作"""
try:
self.add_log(f"Token验证失败: {error_content}", "ERROR")
# 在状态标签显示红色错误信息
self.status_label.setText(f"🔴 {error_content}")
self.status_label.setStyleSheet(
"color: #dc3545; background: rgba(220, 53, 69, 0.1); border-radius: 12px; padding: 5px 10px; font-weight: bold;")
# 重置按钮状态
self.login_btn.setEnabled(True)
self.login_btn.setText("重新连接")
self.login_btn.setObjectName("loginButton") # 恢复原始样式
# 清空已连接平台列表
self.connected_platforms.clear()
self.add_log("由于token无效已停止所有连接操作", "ERROR")
except Exception as e:
self.add_log(f"处理token错误失败: {e}", "ERROR")
def delayed_platform_summary(self): def delayed_platform_summary(self):
"""定时器触发的汇总显示更新""" """定时器触发的汇总显示更新"""
try: try:
@@ -615,7 +641,8 @@ class LoginWindow(QMainWindow):
log=self.add_log, log=self.add_log,
success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"), success=lambda: self.add_log("WebSocket连接管理器连接成功", "SUCCESS"),
error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR"), error=lambda error: self.add_log(f"WebSocket连接管理器错误: {error}", "ERROR"),
platform_connected=self.on_platform_connected # 新增:平台连接回调 platform_connected=self.on_platform_connected, # 新增:平台连接回调
token_error=self.on_token_error # 新增token错误回调
) )
# 连接后端 # 连接后端
@@ -675,6 +702,44 @@ class LoginWindow(QMainWindow):
self.quit_application() self.quit_application()
event.accept() event.accept()
def setup_version_checker(self):
"""设置版本检查器的GUI回调"""
try:
from WebSocket.backend_singleton import get_websocket_manager
ws_manager = get_websocket_manager()
if ws_manager:
ws_manager.gui_update_callback = self.show_update_notification
self.add_log("✅ 版本检查器GUI回调已设置", "SUCCESS")
else:
self.add_log("⚠️ WebSocket管理器未初始化", "WARNING")
except Exception as e:
self.add_log(f"❌ 设置版本检查器失败: {e}", "ERROR")
def show_update_notification(self, latest_version, download_url):
"""显示版本更新通知"""
try:
reply = QMessageBox.question(
self,
"版本更新",
f"发现新版本 {latest_version},是否立即更新?\n\n点击确定将打开下载页面。",
QMessageBox.Yes | QMessageBox.No,
QMessageBox.Yes
)
if reply == QMessageBox.Yes:
self.trigger_update(download_url)
except Exception as e:
self.add_log(f"❌ 显示更新通知失败: {e}", "ERROR")
def trigger_update(self, download_url):
"""触发更新下载"""
import webbrowser
try:
webbrowser.open(download_url)
self.add_log("✅ 已打开更新下载页面", "SUCCESS")
except Exception as e:
self.add_log(f"❌ 打开下载页面失败: {e}", "ERROR")
def main(): def main():
"""主函数,用于测试界面""" """主函数,用于测试界面"""

View File

@@ -7,6 +7,7 @@
import os import os
import subprocess import subprocess
import shutil import shutil
from pathlib import Path
def clean_build(): def clean_build():
@@ -55,6 +56,7 @@ def build_with_command():
'--name=main', '--name=main',
'--onedir', # 相当于 --exclude-binaries '--onedir', # 相当于 --exclude-binaries
'--windowed', # 相当于 -w '--windowed', # 相当于 -w
'--icon=static/ai_assistant_icon_64.png', # 添加主程序图标
'--add-data=config.py;.', '--add-data=config.py;.',
'--add-data=exe_file_logger.py;.', '--add-data=exe_file_logger.py;.',
'--add-data=windows_taskbar_fix.py;.', '--add-data=windows_taskbar_fix.py;.',
@@ -96,17 +98,31 @@ def build_with_command():
print("✅ 打包成功!") print("✅ 打包成功!")
print("📁 打包结果: dist/main/") print("📁 打包结果: dist/main/")
# 重命名目录 # 重命名目录为统一的输出路径
if os.path.exists('dist/main'): if os.path.exists('dist/main'):
try: try:
# 如果目标目录已存在,先删除
if os.path.exists('dist/MultiPlatformGUI'): if os.path.exists('dist/MultiPlatformGUI'):
print("🗑️ 删除旧的 MultiPlatformGUI 目录...")
shutil.rmtree('dist/MultiPlatformGUI') shutil.rmtree('dist/MultiPlatformGUI')
# 重命名
os.rename('dist/main', 'dist/MultiPlatformGUI') os.rename('dist/main', 'dist/MultiPlatformGUI')
print("✅ 已重命名为: dist/MultiPlatformGUI/") print("✅ 已重命名为: dist/MultiPlatformGUI/")
# 验证重命名结果
if os.path.exists('dist/MultiPlatformGUI/main.exe'):
exe_size = os.path.getsize('dist/MultiPlatformGUI/main.exe') / 1024 / 1024
print(f"✅ 主程序验证通过: main.exe ({exe_size:.1f} MB)")
else:
print("⚠️ 重命名后主程序文件验证失败")
except Exception as e: except Exception as e:
print(f"⚠️ 重命名失败: {e}") print(f"⚠️ 重命名失败: {e}")
print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI") print("💡 可以手动重命名: dist/main -> dist/MultiPlatformGUI")
print("📁 当前可用路径: dist/main/") print("📁 当前可用路径: dist/main/")
else:
print("⚠️ 未找到 dist/main 目录,重命名跳过")
# 创建使用说明 # 创建使用说明
try: try:
@@ -221,31 +237,62 @@ def verify_result():
"""验证打包结果""" """验证打包结果"""
print("\n🔍 验证打包结果...") print("\n🔍 验证打包结果...")
# 先检查MultiPlatformGUI再检查main目录 # 先检查MultiPlatformGUI(标准输出目录
base_dirs = ["dist/MultiPlatformGUI", "dist/main"] target_dir = "dist/MultiPlatformGUI"
fallback_dir = "dist/main"
for base_dir in base_dirs: if os.path.exists(target_dir):
if os.path.exists(base_dir): return _verify_directory(target_dir)
exe_path = f"{base_dir}/main.exe" elif os.path.exists(fallback_dir):
dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll" print(f"⚠️ 发现备用目录: {fallback_dir}")
print("💡 建议重命名为: dist/MultiPlatformGUI")
return _verify_directory(fallback_dir)
else:
print("❌ 未找到任何打包输出目录")
return False
if os.path.exists(exe_path): def _verify_directory(base_dir):
size = os.path.getsize(exe_path) """验证指定目录的打包结果"""
print(f"✅ 主程序: {exe_path} ({size:,} bytes)") exe_path = f"{base_dir}/main.exe"
internal_dir = f"{base_dir}/_internal"
dll_path = f"{base_dir}/_internal/Utils/PythonNew32/SaiNiuApi.dll"
static_dir = f"{base_dir}/_internal/static"
if os.path.exists(dll_path): print(f"📁 验证目录: {base_dir}")
dll_size = os.path.getsize(dll_path)
print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:,} bytes)")
print(f"📁 有效路径: {base_dir}/")
print("✅ 验证通过")
return True
else:
print("❌ 千牛DLL不存在")
else:
print(f"{exe_path} 不存在")
print("❌ 未找到有效的打包结果") # 检查主程序
return False if os.path.exists(exe_path):
size = os.path.getsize(exe_path) / 1024 / 1024 # MB
print(f"✅ 主程序: main.exe ({size:.1f} MB)")
else:
print("❌ 主程序 main.exe 不存在")
return False
# 检查_internal目录
if os.path.exists(internal_dir):
file_count = len([f for f in os.listdir(internal_dir) if os.path.isfile(os.path.join(internal_dir, f))])
dir_count = len([d for d in os.listdir(internal_dir) if os.path.isdir(os.path.join(internal_dir, d))])
print(f"✅ 依赖目录: _internal/ ({file_count} 个文件, {dir_count} 个子目录)")
else:
print("❌ _internal 目录不存在")
return False
# 检查关键DLL可选
if os.path.exists(dll_path):
dll_size = os.path.getsize(dll_path) / 1024 # KB
print(f"✅ 千牛DLL: SaiNiuApi.dll ({dll_size:.1f} KB)")
else:
print("⚠️ 千牛DLL不存在可能正常")
# 检查静态资源
if os.path.exists(static_dir):
icon_files = list(Path(static_dir).glob("*.png"))
print(f"✅ 静态资源: static/ ({len(icon_files)} 个图标文件)")
else:
print("⚠️ static 目录不存在")
print(f"✅ 目录 {base_dir} 验证通过")
return True
def main(): def main():
@@ -296,10 +343,11 @@ def main():
if os.path.exists("dist/MultiPlatformGUI"): if os.path.exists("dist/MultiPlatformGUI"):
print("📁 打包结果: dist/MultiPlatformGUI/") print("📁 打包结果: dist/MultiPlatformGUI/")
print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe") print("🚀 运行方式: cd dist/MultiPlatformGUI && .\\main.exe")
print("📦 安装包构建: python installer/build_installer.py")
elif os.path.exists("dist/main"): elif os.path.exists("dist/main"):
print("<EFBFBD><EFBFBD> 打包结果: dist/main/") print("📁 打包结果: dist/main/")
print("🚀 运行方式: cd dist/main && .\\main.exe") print("🚀 运行方式: cd dist/main && .\\main.exe")
print("💡 提示: 可手动重命名为 MultiPlatformGUI") print("⚠️ 建议重命名: dist/main -> dist/MultiPlatformGUI")
print("=" * 60) print("=" * 60)

Binary file not shown.

After

Width:  |  Height:  |  Size: 801 KiB

92
version_history.json Normal file
View File

@@ -0,0 +1,92 @@
[
{
"version": "1.4.8",
"update_type": "patch",
"content": "强制空提交",
"author": "Gitea Actions",
"commit_hash": "2ba26b79dd43512e13cee8600f7e35441d3e9f92",
"commit_short_hash": "2ba26b79",
"branch": "master",
"release_time": "2025-10-09 17:52:04",
"stats": {
"files_changed": 0,
"lines_added": 0,
"lines_deleted": 0
}
},
{
"version": "1.4.7",
"update_type": "patch",
"content": "解决job构建失败的问题",
"author": "Gitea Actions",
"commit_hash": "cab5b606a9728871de830dbf3f8c01361071ef02",
"commit_short_hash": "cab5b606",
"branch": "master",
"release_time": "2025-10-09 17:36:03",
"stats": {
"files_changed": 1,
"lines_added": 23,
"lines_deleted": 13
}
},
{
"version": "1.4.6",
"update_type": "patch",
"content": "解决job构建失败的问题",
"author": "Gitea Actions",
"commit_hash": "2734d375c244fa6b8ef8925cf3d2982751168f4e",
"commit_short_hash": "2734d375",
"branch": "master",
"release_time": "2025-10-09 17:30:35",
"stats": {
"files_changed": 1,
"lines_added": 9,
"lines_deleted": 26
}
},
{
"version": "1.4.5",
"update_type": "patch",
"content": "修改网络问题",
"author": "Gitea Actions",
"commit_hash": "39ba4f93241e2121578518712250582a8345304b",
"commit_short_hash": "39ba4f93",
"branch": "master",
"release_time": "2025-10-09 17:24:42",
"stats": {
"files_changed": 1,
"lines_added": 5,
"lines_deleted": 1
}
},
{
"version": "1.4.4",
"update_type": "patch",
"content": "修改网络问题",
"author": "jjz",
"commit_hash": "41b686fff62d15c197c52e7b47d2ff57826ad00a",
"commit_short_hash": "41b686ff",
"branch": "master",
"release_time": "2025-10-09 17:21:36",
"stats": {
"files_changed": 1,
"lines_added": 11,
"lines_deleted": 11
}
},
{
"version": "1.4.3",
"update_type": "patch",
"content": "修改网络问题",
"author": "jjz",
"commit_hash": "a925ca2c991b9877bfd05753da95b78895a68f31",
"commit_short_hash": "a925ca2c",
"branch": "master",
"release_time": "2025-10-09 17:20:07",
"stats": {
"files_changed": 1,
"lines_added": 19,
"lines_deleted": 11
}
}
]